diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 935f727e..b2cd6938 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -107,6 +107,8 @@ jobs: -e GOFLAGS=-buildvcs=false \ "$BUILDER_IMAGE" \ sh -eux <<'SCRIPT' + go version + # types/netcap.pb.go is generated from netcap.proto and gitignored, so # a clean checkout has no types package and every build fails with # "undefined: Alert". protoc and the gogofaster plugin ship in the diff --git a/cmd/capture/webui/connections_handlers.go b/cmd/capture/webui/connections_handlers.go index e9d2f9f1..763b6cd3 100644 --- a/cmd/capture/webui/connections_handlers.go +++ b/cmd/capture/webui/connections_handlers.go @@ -24,7 +24,6 @@ import ( "encoding/base64" "encoding/json" "fmt" - "io" stdio "io" "log" "net/http" @@ -208,44 +207,8 @@ func filterConnectionsByIPVersion(connections []ConnectionSummary, ipVersionFilt func readConnections(outDir string) ([]ConnectionSummary, error) { filePath := filepath.Join(outDir, "Connection.ncap.gz") - // Check if file exists - if _, err := os.Stat(filePath); os.IsNotExist(err) { - log.Printf("[WebUI] Connection file not found: %s", filePath) - return []ConnectionSummary{}, nil - } - - // Read Connection records - reader, err := NewAuditRecordReader(filePath) - if err != nil { - return nil, err - } - defer reader.Close() - - // Read header - _, err = reader.ReadHeader() - if err != nil { - return nil, err - } - connections := make([]ConnectionSummary, 0) - - // Read all records - for { - record, err := reader.NextRecord() - if err != nil { - if err == io.EOF { - break - } - log.Printf("[WebUI] Error reading Connection record: %v", err) - continue - } - - // Type assert to Connection - conn, ok := record.(*types.Connection) - if !ok { - continue - } - + err := visitAuditRecords(filePath, "Connection", func(conn *types.Connection) { connections = append(connections, ConnectionSummary{ TimestampFirst: conn.TimestampFirst, TimestampLast: conn.TimestampLast, @@ -299,6 +262,9 @@ func readConnections(outDir string) ([]ConnectionSummary, error) { // Community ID for cross-tool correlation CommunityID: conn.CommunityID, }) + }) + if err != nil { + return nil, err } // Sort by total size descending (or by timestamp - could be configurable) diff --git a/cmd/capture/webui/devices_handlers.go b/cmd/capture/webui/devices_handlers.go index e7ad78e2..65b032f1 100644 --- a/cmd/capture/webui/devices_handlers.go +++ b/cmd/capture/webui/devices_handlers.go @@ -21,10 +21,8 @@ package webui import ( "encoding/json" - "io" "log" "net/http" - "os" "path/filepath" "sort" @@ -99,44 +97,8 @@ func (s *Server) handleDevices(w http.ResponseWriter, r *http.Request) { func readDeviceProfiles(outDir string) ([]DeviceProfileSummary, error) { filePath := filepath.Join(outDir, "DeviceProfile.ncap.gz") - // Check if file exists - if _, err := os.Stat(filePath); os.IsNotExist(err) { - log.Printf("[WebUI] DeviceProfile file not found: %s", filePath) - return []DeviceProfileSummary{}, nil - } - - // Read DeviceProfile records - reader, err := NewAuditRecordReader(filePath) - if err != nil { - return nil, err - } - defer reader.Close() - - // Read header - _, err = reader.ReadHeader() - if err != nil { - return nil, err - } - devices := make([]DeviceProfileSummary, 0) - - // Read all records - for { - record, err := reader.NextRecord() - if err != nil { - if err == io.EOF { - break - } - log.Printf("[WebUI] Error reading DeviceProfile record: %v", err) - continue - } - - // Type assert to DeviceProfile - deviceProfile, ok := record.(*types.DeviceProfile) - if !ok { - continue - } - + err := visitAuditRecords(filePath, "DeviceProfile", func(deviceProfile *types.DeviceProfile) { devices = append(devices, DeviceProfileSummary{ MacAddr: deviceProfile.MacAddr, DeviceManufacturer: deviceProfile.DeviceManufacturer, @@ -154,6 +116,9 @@ func readDeviceProfiles(outDir string) ([]DeviceProfileSummary, error) { OS: deviceProfile.OS, Roles: deviceProfile.Roles, }) + }) + if err != nil { + return nil, err } // Sort by packet count descending diff --git a/cmd/capture/webui/reader.go b/cmd/capture/webui/reader.go index 3d3ff3a3..d0313471 100644 --- a/cmd/capture/webui/reader.go +++ b/cmd/capture/webui/reader.go @@ -22,8 +22,12 @@ package webui import ( "compress/gzip" "encoding/json" + "errors" + "fmt" "io" + "log" "os" + "reflect" "strings" "github.com/gogo/protobuf/proto" @@ -99,6 +103,56 @@ func (r *AuditRecordReader) NextRecord() (proto.Message, error) { return msg, nil } +// ErrAuditRecordTypeMismatch indicates that a decoded record is not the requested type. +var ErrAuditRecordTypeMismatch = errors.New("audit record type mismatch") + +// NextAs reads a header-selected record and asserts its type, consuming it even on mismatch. +func (r *AuditRecordReader) NextAs[T proto.Message]() (T, error) { + var zero T + msg, err := r.NextRecord() + if err != nil { + return zero, err + } + record, ok := msg.(T) + if !ok { + return zero, fmt.Errorf("%w: got %T, want %v", ErrAuditRecordTypeMismatch, msg, reflect.TypeFor[T]()) + } + return record, nil +} + +func visitAuditRecords[T proto.Message](path, label string, visit func(T)) error { + if _, err := os.Stat(path); os.IsNotExist(err) { + log.Printf("[WebUI] %s file not found: %s", label, path) + return nil + } + + reader, err := NewAuditRecordReader(path) + if err != nil { + return err + } + defer reader.Close() + if _, err := reader.ReadHeader(); err != nil { + return err + } + + for { + record, err := reader.NextAs[T]() + if err != nil { + if err == io.EOF { + break + } + if errors.Is(err, ErrAuditRecordTypeMismatch) { + continue + } + // Preserve skip-on-error behavior; persistent I/O errors can repeat indefinitely. + log.Printf("[WebUI] Error reading %s record: %v", label, err) + continue + } + visit(record) + } + return nil +} + // NextAsJSON reads the next audit record and returns it as JSON func (r *AuditRecordReader) NextAsJSON() (string, error) { msg, err := r.NextRecord() diff --git a/cmd/capture/webui/reader_handlers_test.go b/cmd/capture/webui/reader_handlers_test.go new file mode 100644 index 00000000..654959ef --- /dev/null +++ b/cmd/capture/webui/reader_handlers_test.go @@ -0,0 +1,120 @@ +package webui + +import ( + "bytes" + "compress/gzip" + "errors" + "io" + "os" + "path/filepath" + "reflect" + "testing" + + "github.com/gogo/protobuf/proto" + + "github.com/dreadl0ck/netcap/internal/delimited" + "github.com/dreadl0ck/netcap/types" +) + +func TestAuditRecordHandlerReaders(t *testing.T) { + for _, tc := range []struct { + name string + kind types.Type + records []proto.Message + read func(string) (any, error) + want any + }{ + {"Connection", types.Type_NC_Connection, []proto.Message{ + &types.Connection{SrcIP: "192.0.2.1", NumPackets: 42, TotalSize: 10}, + &types.Connection{TotalSize: 30}, + &types.Connection{TotalSize: 20}, + }, func(dir string) (any, error) { + return readConnections(dir) + }, []ConnectionSummary{{TotalSize: 30}, {TotalSize: 20}, {SrcIP: "192.0.2.1", NumPackets: 42, TotalSize: 10}}}, + {"Service", types.Type_NC_Service, []proto.Message{ + &types.Service{Name: "https", Port: 443, BytesServer: 10}, + &types.Service{BytesClient: 30}, + &types.Service{BytesServer: 15, BytesClient: 5}, + }, func(dir string) (any, error) { + return readServices(dir) + }, []ServiceSummary{{BytesClient: 30}, {BytesServer: 15, BytesClient: 5}, {Name: "https", Port: 443, BytesServer: 10}}}, + {"DeviceProfile", types.Type_NC_DeviceProfile, []proto.Message{ + &types.DeviceProfile{MacAddr: "00:11:22:33:44:55", NumPackets: 7, Bytes: 100}, + &types.DeviceProfile{NumPackets: 30}, + &types.DeviceProfile{NumPackets: 20}, + }, func(dir string) (any, error) { + return readDeviceProfiles(dir) + }, []DeviceProfileSummary{{NumPackets: 30}, {NumPackets: 20}, {MacAddr: "00:11:22:33:44:55", NumPackets: 7, Bytes: 100}}}, + } { + for _, scenario := range []string{"missing", "wrong-type", "header-only", "malformed-header", "invalid-gzip", "malformed-record", "valid"} { + t.Run(tc.name+"/"+scenario, func(t *testing.T) { + dir := t.TempDir() + if scenario != "missing" { + kind, records := tc.kind, tc.records + if scenario == "wrong-type" { + kind, records = types.Type_NC_Service, []proto.Message{&types.Service{Name: "wrong"}, &types.Service{Name: "also wrong"}} + if tc.kind == types.Type_NC_Service { + kind, records = types.Type_NC_Connection, []proto.Message{&types.Connection{SrcIP: "192.0.2.2"}, &types.Connection{}} + } + } + if scenario == "header-only" { + records = nil + } + var data bytes.Buffer + gz := gzip.NewWriter(&data) + writer := delimited.NewWriter(gz) + if scenario == "malformed-header" { + if err := writer.Put([]byte{0x80}); err != nil { + t.Fatal(err) + } + } else if err := writer.PutProto(&types.Header{Type: kind}); err != nil { + t.Fatal(err) + } + if scenario == "malformed-record" { + // Valid framing allows the next record to be read after protobuf decoding fails. + if err := writer.Put([]byte{0x80}); err != nil { + t.Fatal(err) + } + } + for _, record := range records { + if err := writer.PutProto(record); err != nil { + t.Fatal(err) + } + } + if err := gz.Close(); err != nil { + t.Fatal(err) + } + if scenario == "invalid-gzip" { + data.Reset() + data.WriteString("not a gzip file") + } + if err := os.WriteFile(filepath.Join(dir, tc.name+".ncap.gz"), data.Bytes(), 0600); err != nil { + t.Fatal(err) + } + } + got, err := tc.read(dir) + if scenario == "malformed-header" || scenario == "invalid-gzip" { + wantErr := io.ErrUnexpectedEOF + if scenario == "invalid-gzip" { + wantErr = gzip.ErrHeader + } + if !errors.Is(err, wantErr) || !reflect.ValueOf(got).IsNil() { + t.Fatalf("read = (%#v, %v), want (nil, %v)", got, err, wantErr) + } + return + } + if err != nil { + t.Fatal(err) + } + value := reflect.ValueOf(got) + if scenario == "valid" || scenario == "malformed-record" { + if !reflect.DeepEqual(got, tc.want) { + t.Fatalf("read = %#v, want %#v", got, tc.want) + } + } else if value.IsNil() || value.Len() != 0 { + t.Fatalf("read = %#v, want empty nonnil slice", got) + } + }) + } + } +} diff --git a/cmd/capture/webui/reader_test.go b/cmd/capture/webui/reader_test.go new file mode 100644 index 00000000..0c698acb --- /dev/null +++ b/cmd/capture/webui/reader_test.go @@ -0,0 +1,189 @@ +package webui + +import ( + "bytes" + "compress/gzip" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/gogo/protobuf/proto" + + "github.com/dreadl0ck/netcap/internal/delimited" + "github.com/dreadl0ck/netcap/types" +) + +func auditReaderFixture(t *testing.T, compressed bool, header *types.Header, records ...[]byte) *AuditRecordReader { + t.Helper() + var data bytes.Buffer + w := delimited.NewWriter(&data) + if header != nil { + if err := w.PutProto(header); err != nil { + t.Fatal(err) + } + } + for _, record := range records { + if err := w.Put(record); err != nil { + t.Fatal(err) + } + } + path := filepath.Join(t.TempDir(), "records.ncap") + if compressed { + var zipped bytes.Buffer + gz := gzip.NewWriter(&zipped) + if _, err := gz.Write(data.Bytes()); err != nil { + t.Fatal(err) + } + if err := gz.Close(); err != nil { + t.Fatal(err) + } + data = zipped + path += ".gz" + } + if err := os.WriteFile(path, data.Bytes(), 0600); err != nil { + t.Fatal(err) + } + r, err := NewAuditRecordReader(path) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if err := r.Close(); err != nil { + t.Error(err) + } + }) + return r +} + +func TestAuditRecordReaderNextAs(t *testing.T) { + for _, tc := range []struct { + name string + kind types.Type + want proto.Message + next func(*AuditRecordReader) (proto.Message, error) + }{ + {"connection", types.Type_NC_Connection, &types.Connection{SrcIP: "192.0.2.1", NumPackets: 42}, func(r *AuditRecordReader) (proto.Message, error) { + return r.NextAs[*types.Connection]() + }}, + {"service", types.Type_NC_Service, &types.Service{Name: "https", Port: 443}, func(r *AuditRecordReader) (proto.Message, error) { + return r.NextAs[*types.Service]() + }}, + {"device", types.Type_NC_DeviceProfile, &types.DeviceProfile{MacAddr: "00:11:22:33:44:55", NumPackets: 7}, func(r *AuditRecordReader) (proto.Message, error) { + return r.NextAs[*types.DeviceProfile]() + }}, + } { + for _, compression := range []struct { + name string + gzip bool + }{{"plain", false}, {"gzip", true}} { + t.Run(tc.name+"/"+compression.name, func(t *testing.T) { + payload, err := proto.Marshal(tc.want) + if err != nil { + t.Fatal(err) + } + r := auditReaderFixture(t, compression.gzip, &types.Header{Type: tc.kind}, payload, payload) + if _, err := r.ReadHeader(); err != nil { + t.Fatal(err) + } + got, err := tc.next(r) + if err != nil || !proto.Equal(got, tc.want) { + t.Fatalf("NextAs = (%v, %v), want %v", got, err, tc.want) + } + // The interface constraint must preserve the header-selected concrete type. + got, err = r.NextAs[proto.Message]() + if err != nil || !proto.Equal(got, tc.want) { + t.Fatalf("NextAs[proto.Message] = (%v, %v), want %v", got, err, tc.want) + } + got, err = r.NextAs[proto.Message]() + if got != nil || err != io.EOF { + t.Fatalf("NextAs at EOF = (%v, %v), want (nil, EOF)", got, err) + } + }) + } + } +} + +func TestAuditRecordReaderNextAsErrors(t *testing.T) { + for _, compressed := range []bool{false, true} { + for _, tc := range []struct { + name string + kind types.Type + records [][]byte + want error + }{ + {"header-only", types.Type_NC_Connection, nil, io.EOF}, + {"unknown-type", types.Type(-1), nil, io.EOF}, + {"mismatch", types.Type_NC_Service, [][]byte{{}}, ErrAuditRecordTypeMismatch}, + {"malformed-record", types.Type_NC_Service, [][]byte{{0x80}}, io.ErrUnexpectedEOF}, + } { + t.Run(fmt.Sprintf("%s/gzip=%t", tc.name, compressed), func(t *testing.T) { + r := auditReaderFixture(t, compressed, &types.Header{Type: tc.kind}, tc.records...) + if _, err := r.ReadHeader(); err != nil { + t.Fatal(err) + } + got, err := r.NextAs[*types.Connection]() + if got != nil || !errors.Is(err, tc.want) { + t.Fatalf("NextAs = (%v, %v), want (nil, %v)", got, err, tc.want) + } + if tc.want == ErrAuditRecordTypeMismatch { + for _, name := range []string{"*types.Service", "*types.Connection"} { + if !strings.Contains(err.Error(), name) { + t.Errorf("mismatch %q does not describe %s", err, name) + } + } + } + got, err = r.NextAs[*types.Connection]() + if got != nil || err != io.EOF { + t.Fatalf("NextAs after error = (%v, %v), want (nil, EOF)", got, err) + } + }) + } + } +} + +func TestAuditRecordReaderHeaderErrors(t *testing.T) { + for _, tc := range []struct { + name string + records [][]byte + want error + }{ + {"empty", nil, io.EOF}, + {"malformed", [][]byte{{0x80}}, io.ErrUnexpectedEOF}, + } { + t.Run(tc.name, func(t *testing.T) { + r := auditReaderFixture(t, false, nil, tc.records...) + header, err := r.ReadHeader() + if header != nil || !errors.Is(err, tc.want) { + t.Fatalf("ReadHeader = (%v, %v), want (nil, %v)", header, err, tc.want) + } + }) + } +} + +func TestAuditRecordReaderNextRecordCompatibility(t *testing.T) { + r := auditReaderFixture(t, false, &types.Header{Type: types.Type_NC_Connection}, []byte{}, []byte{0x80}) + if _, err := r.ReadHeader(); err != nil { + t.Fatal(err) + } + got, err := r.NextRecord() + if _, ok := got.(*types.Connection); !ok || err != nil { + t.Fatalf("NextRecord = (%T, %v), want (*types.Connection, nil)", got, err) + } + got, err = r.NextRecord() + if got != nil || !errors.Is(err, io.ErrUnexpectedEOF) { + t.Fatalf("malformed NextRecord = (%v, %v)", got, err) + } + got, err = r.NextRecord() + if got != nil || err != io.EOF { + t.Fatalf("NextRecord at EOF = (%v, %v)", got, err) + } + r.recordType = types.Type(-1) + got, err = r.NextRecord() + if _, ok := got.(*types.Connection); !ok || err != io.EOF { + t.Fatalf("unknown-type NextRecord = (%T, %v), want (*types.Connection, EOF)", got, err) + } +} diff --git a/cmd/capture/webui/services_handlers.go b/cmd/capture/webui/services_handlers.go index 544ea3a1..3b17923c 100644 --- a/cmd/capture/webui/services_handlers.go +++ b/cmd/capture/webui/services_handlers.go @@ -21,10 +21,8 @@ package webui import ( "encoding/json" - "io" "log" "net/http" - "os" "path/filepath" "sort" @@ -94,44 +92,8 @@ func (s *Server) handleServices(w http.ResponseWriter, r *http.Request) { func readServices(outDir string) ([]ServiceSummary, error) { filePath := filepath.Join(outDir, "Service.ncap.gz") - // Check if file exists - if _, err := os.Stat(filePath); os.IsNotExist(err) { - log.Printf("[WebUI] Service file not found: %s", filePath) - return []ServiceSummary{}, nil - } - - // Read Service records - reader, err := NewAuditRecordReader(filePath) - if err != nil { - return nil, err - } - defer reader.Close() - - // Read header - _, err = reader.ReadHeader() - if err != nil { - return nil, err - } - services := make([]ServiceSummary, 0) - - // Read all records - for { - record, err := reader.NextRecord() - if err != nil { - if err == io.EOF { - break - } - log.Printf("[WebUI] Error reading Service record: %v", err) - continue - } - - // Type assert to Service - svc, ok := record.(*types.Service) - if !ok { - continue - } - + err := visitAuditRecords(filePath, "Service", func(svc *types.Service) { services = append(services, ServiceSummary{ Timestamp: svc.Timestamp, IP: svc.IP, @@ -153,6 +115,9 @@ func readServices(outDir string) ([]ServiceSummary, error) { DetectedProtocolName: svc.DetectedProtocolName, MatchedProbeID: svc.MatchedProbeID, }) + }) + if err != nil { + return nil, err } // Sort by bytes (server + client) descending diff --git a/decoder/packet/arp.go b/decoder/packet/arp.go index d03cbfe0..cfb3b1c6 100644 --- a/decoder/packet/arp.go +++ b/decoder/packet/arp.go @@ -24,7 +24,6 @@ import ( "github.com/dreadl0ck/netcap/types" "github.com/gogo/protobuf/proto" - "github.com/gopacket/gopacket" "github.com/gopacket/gopacket/layers" ) @@ -43,45 +42,43 @@ var arpDecoder = newGoPacketDecoder( types.Type_NC_ARP, layers.LayerTypeARP, "The Address Resolution Protocol resolves IP to hardware addresses", - func(layer gopacket.Layer, timestamp int64) proto.Message { - if arp, ok := layer.(*layers.ARP); ok { - srcProtoAddr := parseIPv4(arp.SourceProtAddress) - dstProtoAddr := parseIPv4(arp.DstProtAddress) + typedLayerHandler(decodeARP), +) - // Determine operation name - opName := arpOperationNames[arp.Operation] - if opName == "" { - opName = "Unknown" - } +func decodeARP(arp *layers.ARP, timestamp int64) proto.Message { + srcProtoAddr := parseIPv4(arp.SourceProtAddress) + dstProtoAddr := parseIPv4(arp.DstProtAddress) - // Gratuitous ARP: reply sent without being requested, or request for own IP - // Common pattern: sender IP == target IP in a reply - isGratuitous := arp.Operation == 2 && bytes.Equal(arp.SourceProtAddress, arp.DstProtAddress) + // Determine operation name + opName := arpOperationNames[arp.Operation] + if opName == "" { + opName = "Unknown" + } - // ARP Probe: sender IP is 0.0.0.0 (used for duplicate address detection) - isProbe := bytes.Equal(arp.SourceProtAddress, zeroIPv4) + // Gratuitous ARP: reply sent without being requested, or request for own IP + // Common pattern: sender IP == target IP in a reply + isGratuitous := arp.Operation == 2 && bytes.Equal(arp.SourceProtAddress, arp.DstProtAddress) - // ARP Announcement: sender IP == target IP in a request - isAnnouncement := arp.Operation == 1 && bytes.Equal(arp.SourceProtAddress, arp.DstProtAddress) + // ARP Probe: sender IP is 0.0.0.0 (used for duplicate address detection) + isProbe := bytes.Equal(arp.SourceProtAddress, zeroIPv4) - return &types.ARP{ - Timestamp: timestamp, - AddrType: int32(arp.AddrType), - Protocol: int32(arp.Protocol), - HwAddressSize: int32(arp.HwAddressSize), - ProtocolAddressSize: int32(arp.ProtAddressSize), - Operation: int32(arp.Operation), - SrcHwAddress: formatMac(arp.SourceHwAddress), - SrcProtocolAddress: srcProtoAddr, - DstHwAddress: formatMac(arp.DstHwAddress), - DstProtocolAddress: dstProtoAddr, - IsGratuitous: isGratuitous, - IsProbe: isProbe, - IsAnnouncement: isAnnouncement, - OperationName: opName, - } - } + // ARP Announcement: sender IP == target IP in a request + isAnnouncement := arp.Operation == 1 && bytes.Equal(arp.SourceProtAddress, arp.DstProtAddress) - return nil - }, -) + return &types.ARP{ + Timestamp: timestamp, + AddrType: int32(arp.AddrType), + Protocol: int32(arp.Protocol), + HwAddressSize: int32(arp.HwAddressSize), + ProtocolAddressSize: int32(arp.ProtAddressSize), + Operation: int32(arp.Operation), + SrcHwAddress: formatMac(arp.SourceHwAddress), + SrcProtocolAddress: srcProtoAddr, + DstHwAddress: formatMac(arp.DstHwAddress), + DstProtocolAddress: dstProtoAddr, + IsGratuitous: isGratuitous, + IsProbe: isProbe, + IsAnnouncement: isAnnouncement, + OperationName: opName, + } +} diff --git a/decoder/packet/eth.go b/decoder/packet/eth.go index 31e6ba57..87eeab8e 100644 --- a/decoder/packet/eth.go +++ b/decoder/packet/eth.go @@ -23,7 +23,6 @@ import ( "net" "github.com/gogo/protobuf/proto" - "github.com/gopacket/gopacket" "github.com/gopacket/gopacket/layers" "github.com/dreadl0ck/netcap/types" @@ -50,43 +49,41 @@ var ethernetDecoder = newGoPacketDecoder( types.Type_NC_Ethernet, layers.LayerTypeEthernet, "Ethernet is a family of computer networking technologies commonly used in local area networks, metropolitan area networks and wide area networks", - func(layer gopacket.Layer, timestamp int64) proto.Message { - if eth, ok := layer.(*layers.Ethernet); ok { - var e float64 - if conf.CalculateEntropy { - e = entropy(eth.Payload) - } + typedLayerHandler(decodeEthernet), +) - // Get EtherType name - ethTypeName := ethernetTypeNames[eth.EthernetType] - if ethTypeName == "" { - ethTypeName = "Unknown" - } +func decodeEthernet(eth *layers.Ethernet, timestamp int64) proto.Message { + var e float64 + if conf.CalculateEntropy { + e = entropy(eth.Payload) + } - // Check MAC address properties - // Broadcast: ff:ff:ff:ff:ff:ff - isBroadcast := eth.DstMAC.String() == broadcastMAC.String() + // Get EtherType name + ethTypeName := ethernetTypeNames[eth.EthernetType] + if ethTypeName == "" { + ethTypeName = "Unknown" + } - // Multicast: first bit of first byte is 1 (01:xx:xx:xx:xx:xx pattern) - isMulticast := len(eth.DstMAC) > 0 && (eth.DstMAC[0]&0x01) == 0x01 && !isBroadcast + // Check MAC address properties + // Broadcast: ff:ff:ff:ff:ff:ff + isBroadcast := eth.DstMAC.String() == broadcastMAC.String() - // Locally administered: second bit of first byte is 1 - isLocallyAdmin := len(eth.SrcMAC) > 0 && (eth.SrcMAC[0]&0x02) == 0x02 + // Multicast: first bit of first byte is 1 (01:xx:xx:xx:xx:xx pattern) + isMulticast := len(eth.DstMAC) > 0 && (eth.DstMAC[0]&0x01) == 0x01 && !isBroadcast - return &types.Ethernet{ - Timestamp: timestamp, - SrcMAC: eth.SrcMAC.String(), - DstMAC: eth.DstMAC.String(), - EthernetType: int32(eth.EthernetType), - PayloadEntropy: e, - PayloadSize: int32(len(eth.Payload)), - EthernetTypeName: ethTypeName, - IsBroadcast: isBroadcast, - IsMulticast: isMulticast, - IsLocallyAdministered: isLocallyAdmin, - } - } + // Locally administered: second bit of first byte is 1 + isLocallyAdmin := len(eth.SrcMAC) > 0 && (eth.SrcMAC[0]&0x02) == 0x02 - return nil - }, -) + return &types.Ethernet{ + Timestamp: timestamp, + SrcMAC: eth.SrcMAC.String(), + DstMAC: eth.DstMAC.String(), + EthernetType: int32(eth.EthernetType), + PayloadEntropy: e, + PayloadSize: int32(len(eth.Payload)), + EthernetTypeName: ethTypeName, + IsBroadcast: isBroadcast, + IsMulticast: isMulticast, + IsLocallyAdministered: isLocallyAdmin, + } +} diff --git a/decoder/packet/gopacket_decoder.go b/decoder/packet/gopacket_decoder.go index 157a7997..467198eb 100644 --- a/decoder/packet/gopacket_decoder.go +++ b/decoder/packet/gopacket_decoder.go @@ -23,7 +23,6 @@ package packet import ( "fmt" "log" - "strings" "sync" "sync/atomic" "time" @@ -99,61 +98,11 @@ func (dec *GoPacketDecoder) NumRecords() int64 { func InitGoPacketDecoders(c *config.Config) (decoders map[gopacket.LayerType][]*GoPacketDecoder, err error) { decoders = map[gopacket.LayerType][]*GoPacketDecoder{} - var ( - // values from command-line flags - in = strings.Split(c.IncludeDecoders, ",") - ex = strings.Split(c.ExcludeDecoders, ",") - - // include map - inMap = make(map[string]bool) - ) - - // Work with a copy of the gopacket decoders slice to avoid mutating - // package-level state. Prior implementations reassigned - // defaultGoPacketDecoders so a single test using IncludeDecoders - // permanently shrunk the global slice for the rest of the run, killing - // TCP/Ethernet/IPv4 record output for every later test. - active := append([]*GoPacketDecoder(nil), defaultGoPacketDecoders...) - - // if there are includes and the first item is not an empty string - if len(in) > 0 && in[0] != "" { // iterate over includes - for _, name := range in { - if name != "" { // check if proto exists - if _, ok := decoderutils.AllDecoderNames[name]; !ok { - return nil, errors.Wrap(ErrInvalidDecoder, name) - } - - // add to include map - inMap[name] = true - } - } - - // iterate over gopacket decoders and collect those that are named in the includeMap - var selection []*GoPacketDecoder - for _, e := range active { - if _, ok := inMap[e.Layer.String()]; ok { - selection = append(selection, e) - } - } - active = selection - } - - // iterate over excluded decoders - for _, name := range ex { - if name != "" { // check if proto exists - if _, ok := decoderutils.AllDecoderNames[name]; !ok { - return nil, errors.Wrap(ErrInvalidDecoder, name) - } - - // remove named decoder from the active slice - for i, e := range active { - if name == e.Layer.String() { - // remove decoder - active = append(active[:i], active[i+1:]...) - break - } - } - } + active, err := decoderutils.SelectDecoders(defaultGoPacketDecoders, c.IncludeDecoders, c.ExcludeDecoders, func(d *GoPacketDecoder) string { + return d.Layer.String() + }, ErrInvalidDecoder) + if err != nil { + return nil, err } var ( diff --git a/decoder/packet/packet_decoder.go b/decoder/packet/packet_decoder.go index 991ce831..ef4be804 100644 --- a/decoder/packet/packet_decoder.go +++ b/decoder/packet/packet_decoder.go @@ -150,61 +150,9 @@ func newAccumulatingPacketDecoder(t types.Type, name, description string, postin // InitPacketDecoders initializes all packet decoders. func InitPacketDecoders(c *config.Config) (decoders []DecoderAPI, err error) { - var ( - // values from command-line flags - in = strings.Split(c.IncludeDecoders, ",") - ex = strings.Split(c.ExcludeDecoders, ",") - - // include map - inMap = make(map[string]bool) - ) - - // Work with a copy of the decoders slice to avoid mutating package-level - // state. This is critical for test isolation: prior implementations - // reassigned defaultPacketDecoders, so a single test using IncludeDecoders - // would permanently shrink the global slice for the rest of the run. - active := append([]DecoderAPI(nil), defaultPacketDecoders...) - - // if there are includes and the first item is not an empty string - if len(in) > 0 && in[0] != "" { // iterate over includes - for _, name := range in { - if name != "" { // check if proto exists - if _, ok := decoderutils.AllDecoderNames[name]; !ok { - return nil, errors.Wrap(ErrInvalidDecoder, name) - } - - // add to include map - inMap[name] = true - } - } - - // iterate over packet decoders and collect those that are named in the includeMap - var selection []DecoderAPI - for _, e := range active { - if _, ok := inMap[e.GetName()]; ok { - selection = append(selection, e) - } - } - active = selection - } - - // iterate over excluded decoders - for _, name := range ex { - if name != "" { // check if proto exists - if _, ok := decoderutils.AllDecoderNames[name]; !ok { - return nil, errors.Wrap(ErrInvalidDecoder, name) - } - - // remove named decoder from the active slice - for i, e := range active { - if name == e.GetName() { - // remove decoder - active = append(active[:i], active[i+1:]...) - - break - } - } - } + active, err := decoderutils.SelectDecoders(defaultPacketDecoders, c.IncludeDecoders, c.ExcludeDecoders, DecoderAPI.GetName, ErrInvalidDecoder) + if err != nil { + return nil, err } var ( diff --git a/decoder/packet/typed_handler.go b/decoder/packet/typed_handler.go new file mode 100644 index 00000000..4b829a33 --- /dev/null +++ b/decoder/packet/typed_handler.go @@ -0,0 +1,17 @@ +package packet + +import ( + "github.com/gogo/protobuf/proto" + "github.com/gopacket/gopacket" +) + +// typedLayerHandler adapts a typed converter to the runtime layer registry. +func typedLayerHandler[L gopacket.Layer](decode func(L, int64) proto.Message) goPacketDecoderHandler { + return func(layer gopacket.Layer, timestamp int64) proto.Message { + typed, ok := layer.(L) + if !ok { + return nil + } + return decode(typed, timestamp) + } +} diff --git a/decoder/packet/typed_handler_test.go b/decoder/packet/typed_handler_test.go new file mode 100644 index 00000000..9ea4972d --- /dev/null +++ b/decoder/packet/typed_handler_test.go @@ -0,0 +1,188 @@ +package packet + +import ( + "fmt" + "testing" + + "github.com/gogo/protobuf/proto" + "github.com/gopacket/gopacket" + "github.com/gopacket/gopacket/layers" + + "github.com/dreadl0ck/netcap/decoder/config" + "github.com/dreadl0ck/netcap/types" +) + +func legacyARPHandler(layer gopacket.Layer, timestamp int64) proto.Message { + if arp, ok := layer.(*layers.ARP); ok { + return decodeARP(arp, timestamp) + } + return nil +} + +func legacyEthernetHandler(layer gopacket.Layer, timestamp int64) proto.Message { + if eth, ok := layer.(*layers.Ethernet); ok { + return decodeEthernet(eth, timestamp) + } + return nil +} + +func TestTypedARPHandler(t *testing.T) { + const timestamp int64 = 1723456789123456789 + for _, tc := range []struct { + name string + operation uint16 + src, dst []byte + srcText string + operationName string + gratuitous, probe, announcement bool + }{ + {"request", 1, []byte{192, 0, 2, 1}, []byte{192, 0, 2, 2}, "192.0.2.1", "Request", false, false, false}, + {"reply", 2, []byte{192, 0, 2, 1}, []byte{192, 0, 2, 2}, "192.0.2.1", "Reply", false, false, false}, + {"gratuitous", 2, []byte{192, 0, 2, 2}, []byte{192, 0, 2, 2}, "192.0.2.2", "Reply", true, false, false}, + {"probe", 1, []byte{0, 0, 0, 0}, []byte{192, 0, 2, 2}, "0.0.0.0", "Request", false, true, false}, + {"announcement", 1, []byte{192, 0, 2, 2}, []byte{192, 0, 2, 2}, "192.0.2.2", "Request", false, false, true}, + {"rarp-request", 3, []byte{192, 0, 2, 1}, []byte{192, 0, 2, 2}, "192.0.2.1", "RARP Request", false, false, false}, + {"rarp-reply", 4, []byte{192, 0, 2, 1}, []byte{192, 0, 2, 2}, "192.0.2.1", "RARP Reply", false, false, false}, + {"unknown", 99, []byte{192, 0, 2, 1}, []byte{192, 0, 2, 2}, "192.0.2.1", "Unknown", false, false, false}, + } { + t.Run(tc.name, func(t *testing.T) { + layer := &layers.ARP{ + AddrType: layers.LinkTypeEthernet, Protocol: layers.EthernetTypeIPv4, + HwAddressSize: 6, ProtAddressSize: 4, Operation: tc.operation, + SourceHwAddress: []byte{0, 1, 2, 3, 4, 5}, DstHwAddress: []byte{6, 7, 8, 9, 10, 11}, + SourceProtAddress: tc.src, DstProtAddress: tc.dst, + } + want := &types.ARP{ + Timestamp: timestamp, AddrType: 1, Protocol: 0x0800, + HwAddressSize: 6, ProtocolAddressSize: 4, Operation: int32(tc.operation), + SrcHwAddress: "00:01:02:03:04:05", DstHwAddress: "06:07:08:09:0a:0b", + SrcProtocolAddress: tc.srcText, DstProtocolAddress: "192.0.2.2", + OperationName: tc.operationName, IsGratuitous: tc.gratuitous, + IsProbe: tc.probe, IsAnnouncement: tc.announcement, + } + got := arpDecoder.Handler(layer, timestamp) + if !proto.Equal(got, want) { + t.Fatalf("got %v; want %v", got, want) + } + if legacy := legacyARPHandler(layer, timestamp); !proto.Equal(got, legacy) { + t.Fatalf("generic %v differs from legacy %v", got, legacy) + } + }) + } +} + +func TestTypedEthernetHandler(t *testing.T) { + previous := conf + conf = &config.Config{} + t.Cleanup(func() { conf = previous }) + const timestamp int64 = 1723456789123456789 + for _, tc := range []struct { + name string + src, dst []byte + srcText, dstText string + etherType layers.EthernetType + typeName string + broadcast, multicast, local bool + }{ + {"unicast", []byte{0, 1, 2, 3, 4, 5}, []byte{6, 7, 8, 9, 10, 11}, "00:01:02:03:04:05", "06:07:08:09:0a:0b", layers.EthernetTypeIPv4, "IPv4", false, false, false}, + {"broadcast", []byte{2, 1, 2, 3, 4, 5}, []byte{255, 255, 255, 255, 255, 255}, "02:01:02:03:04:05", "ff:ff:ff:ff:ff:ff", layers.EthernetTypeARP, "ARP", true, false, true}, + {"multicast", []byte{0, 1, 2, 3, 4, 5}, []byte{1, 0, 94, 0, 0, 1}, "00:01:02:03:04:05", "01:00:5e:00:00:01", layers.EthernetTypeIPv6, "IPv6", false, true, false}, + {"empty-unknown", nil, nil, "", "", layers.EthernetType(0xffff), "Unknown", false, false, false}, + } { + for _, calculateEntropy := range []bool{false, true} { + t.Run(fmt.Sprintf("%s/entropy=%t", tc.name, calculateEntropy), func(t *testing.T) { + conf.CalculateEntropy = calculateEntropy + layer := &layers.Ethernet{ + BaseLayer: layers.BaseLayer{Payload: []byte{0, 1, 2, 3}}, + SrcMAC: tc.src, DstMAC: tc.dst, EthernetType: tc.etherType, + } + want := &types.Ethernet{ + Timestamp: timestamp, SrcMAC: tc.srcText, DstMAC: tc.dstText, + EthernetType: int32(tc.etherType), EthernetTypeName: tc.typeName, PayloadSize: 4, + IsBroadcast: tc.broadcast, IsMulticast: tc.multicast, IsLocallyAdministered: tc.local, + } + if calculateEntropy { + want.PayloadEntropy = 2 + } + got := ethernetDecoder.Handler(layer, timestamp) + if !proto.Equal(got, want) { + t.Fatalf("got %v; want %v", got, want) + } + if legacy := legacyEthernetHandler(layer, timestamp); !proto.Equal(got, legacy) { + t.Fatalf("generic %v differs from legacy %v", got, legacy) + } + }) + } + } +} + +func TestTypedLayerHandlerNil(t *testing.T) { + for _, decoder := range []*GoPacketDecoder{arpDecoder, ethernetDecoder} { + for _, layer := range []gopacket.Layer{nil, &layers.IPv4{}} { + if got := decoder.Handler(layer, 123); got != nil { + t.Fatalf("%s mismatch returned non-nil interface: %#v", decoder.GetName(), got) + } + } + } + if arpDecoder.Handler(&layers.Ethernet{}, 123) != nil || ethernetDecoder.Handler(&layers.ARP{}, 123) != nil { + t.Fatal("cross-protocol mismatch returned non-nil") + } + calls := 0 + handler := typedLayerHandler(func(layer *layers.ARP, timestamp int64) proto.Message { + calls++ + if layer != nil || timestamp != 123 { + t.Fatalf("converter arguments: %v, %d", layer, timestamp) + } + return nil + }) + if handler(nil, 123) != nil || handler(&layers.Ethernet{}, 123) != nil || calls != 0 { + t.Fatal("mismatch must return nil without calling converter") + } + // A matching typed nil still passes the assertion, just as in the legacy adapter. + if handler((*layers.ARP)(nil), 123) != nil || calls != 1 { + t.Fatal("matching typed nil or converter nil result was not preserved") + } +} + +var typedHandlerBenchmarkSink proto.Message + +func BenchmarkTypedLayerHandler(b *testing.B) { + previous := conf + conf = &config.Config{} + b.Cleanup(func() { conf = previous }) + arp := &layers.ARP{ + AddrType: layers.LinkTypeEthernet, Protocol: layers.EthernetTypeIPv4, + HwAddressSize: 6, ProtAddressSize: 4, Operation: 1, + SourceHwAddress: []byte{0, 1, 2, 3, 4, 5}, DstHwAddress: []byte{6, 7, 8, 9, 10, 11}, + SourceProtAddress: []byte{192, 0, 2, 1}, DstProtAddress: []byte{192, 0, 2, 2}, + } + eth := &layers.Ethernet{ + BaseLayer: layers.BaseLayer{Payload: []byte{0, 1, 2, 3, 0, 1, 2, 3}}, + SrcMAC: []byte{0, 1, 2, 3, 4, 5}, DstMAC: []byte{6, 7, 8, 9, 10, 11}, EthernetType: layers.EthernetTypeIPv4, + } + for _, tc := range []struct { + name string + layer gopacket.Layer + legacy, generic goPacketDecoderHandler + calculateEntropy bool + }{ + {"ARP", arp, legacyARPHandler, arpDecoder.Handler, false}, + {"Ethernet", eth, legacyEthernetHandler, ethernetDecoder.Handler, false}, + {"EthernetEntropy", eth, legacyEthernetHandler, ethernetDecoder.Handler, true}, + } { + b.Run(tc.name, func(b *testing.B) { + conf.CalculateEntropy = tc.calculateEntropy + for _, adapter := range []struct { + name string + handler goPacketDecoderHandler + }{{"legacy", tc.legacy}, {"generic", tc.generic}} { + b.Run(adapter.name, func(b *testing.B) { + b.ReportAllocs() + for b.Loop() { + typedHandlerBenchmarkSink = adapter.handler(tc.layer, 1723456789123456789) + } + }) + } + }) + } +} diff --git a/decoder/stream/abstract.go b/decoder/stream/abstract.go index a9f5572d..ca1496da 100644 --- a/decoder/stream/abstract.go +++ b/decoder/stream/abstract.go @@ -3,7 +3,6 @@ package stream import ( "fmt" "log" - "strings" "sync" "time" @@ -86,62 +85,9 @@ func ApplyActionToAbstractDecodersAsync(action func(api core.DecoderAPI)) { // InitAbstractDecoders initializes all stream decoders. func InitAbstractDecoders(c *config.Config) (decoders []core.DecoderAPI, err error) { tls.RecordDecoder.Writer = nil - var ( - // values from command-line flags - in = strings.Split(c.IncludeDecoders, ",") - ex = strings.Split(c.ExcludeDecoders, ",") - - // include map - inMap = make(map[string]bool) - ) - - // Work with a copy of the decoders slice to avoid mutating package-level - // state. Prior implementations reassigned DefaultAbstractDecoders so a - // single test using IncludeDecoders permanently emptied the global slice - // for the remainder of the run, breaking File/Service/Mail/etc. output - // for every later test. - active := append([]core.DecoderAPI(nil), DefaultAbstractDecoders...) - - // if there are includes and the first item is not an empty string - if len(in) > 0 && in[0] != "" { // iterate over includes - for _, name := range in { - if name != "" { // check if proto exists - if _, ok := decoderutils.AllDecoderNames[name]; !ok { - return nil, errors.Wrap(errInvalidAbstractDecoder, name) - } - - // add to include map - inMap[name] = true - } - } - - // iterate over packet decoders and collect those that are named in the includeMap - var selection []core.DecoderAPI - for _, dec := range active { - if _, ok := inMap[dec.GetName()]; ok { - selection = append(selection, dec) - } - } - active = selection - } - - // iterate over excluded decoders - for _, name := range ex { - if name != "" { // check if proto exists - if _, ok := decoderutils.AllDecoderNames[name]; !ok { - return nil, errors.Wrap(errInvalidAbstractDecoder, name) - } - - // remove named decoder from the active slice - for i, dec := range active { - if name == dec.GetName() { - // remove decoder - active = append(active[:i], active[i+1:]...) - - break - } - } - } + active, err := decoderutils.SelectDecoders(DefaultAbstractDecoders, c.IncludeDecoders, c.ExcludeDecoders, core.DecoderAPI.GetName, errInvalidAbstractDecoder) + if err != nil { + return nil, err } var ( diff --git a/decoder/stream/tls_selection_test.go b/decoder/stream/tls_selection_test.go index 3ae02ea3..d918d140 100644 --- a/decoder/stream/tls_selection_test.go +++ b/decoder/stream/tls_selection_test.go @@ -1,12 +1,38 @@ package stream import ( + "errors" "testing" "github.com/dreadl0ck/netcap/decoder/config" "github.com/dreadl0ck/netcap/decoder/stream/tls" + netio "github.com/dreadl0ck/netcap/io" ) +func TestTLSRecordWriterResetBeforeValidation(t *testing.T) { + previous := tls.RecordDecoder.Writer + t.Cleanup(func() { tls.RecordDecoder.Writer = previous }) + for _, tt := range []struct { + name, include, exclude string + }{ + {"include", "InvalidTLSSelection", ""}, + {"exclude", "TLSRecord", "InvalidTLSSelection"}, + } { + t.Run(tt.name, func(t *testing.T) { + writer := netio.NewAuditRecordWriter(&netio.WriterConfig{Null: true}) + defer writer.Close(0) + tls.RecordDecoder.Writer = writer + selected, err := InitAbstractDecoders(&config.Config{IncludeDecoders: tt.include, ExcludeDecoders: tt.exclude}) + if selected != nil || !errors.Is(err, errInvalidAbstractDecoder) { + t.Fatalf("selection = %v, error = %v", selected, err) + } + if tls.RecordDecoder.Writer != nil { + t.Fatal("TLS record writer not reset before validation") + } + }) + } +} + func TestTLSRecordWriterSelection(t *testing.T) { previous := config.Instance t.Cleanup(func() { config.Instance = previous; tls.Decoder.Writer = nil; tls.RecordDecoder.Writer = nil }) diff --git a/decoder/utils/selection.go b/decoder/utils/selection.go new file mode 100644 index 00000000..4f38257a --- /dev/null +++ b/decoder/utils/selection.go @@ -0,0 +1,48 @@ +package utils + +import ( + "strings" + + "github.com/pkg/errors" +) + +// SelectDecoders selects a private slice of defaults using globally registered names. +func SelectDecoders[T any](defaults []T, include, exclude string, nameOf func(T) string, invalidDecoder error) ([]T, error) { + active := append([]T(nil), defaults...) + in := strings.Split(include, ",") + // A leading empty name disables the entire include phase, including validation. + if in[0] != "" { + inMap := make(map[string]bool) + for _, name := range in { + if name != "" { + if _, ok := AllDecoderNames[name]; !ok { + return nil, errors.Wrap(invalidDecoder, name) + } + inMap[name] = true + } + } + + var selection []T + for _, decoder := range active { + if inMap[nameOf(decoder)] { + selection = append(selection, decoder) + } + } + active = selection + } + + for _, name := range strings.Split(exclude, ",") { + if name != "" { + if _, ok := AllDecoderNames[name]; !ok { + return nil, errors.Wrap(invalidDecoder, name) + } + for i, decoder := range active { + if name == nameOf(decoder) { + active = append(active[:i], active[i+1:]...) + break + } + } + } + } + return active, nil +} diff --git a/decoder/utils/selection_test.go b/decoder/utils/selection_test.go new file mode 100644 index 00000000..4cb8ea91 --- /dev/null +++ b/decoder/utils/selection_test.go @@ -0,0 +1,113 @@ +package utils + +import ( + "errors" + "reflect" + "testing" + + pkgerrors "github.com/pkg/errors" +) + +func TestSelectDecoders(t *testing.T) { + previous := AllDecoderNames + AllDecoderNames = map[string]struct{}{"A": {}, "B": {}, "C": {}, "Other": {}} + t.Cleanup(func() { AllDecoderNames = previous }) + sentinel := errors.New("invalid test decoder") + defaults := []string{"A", "B", "A", "C"} + for _, tt := range []struct { + name string + defaults []string + include, exclude string + want []string + errName string + }{ + {"defaults", defaults, "", "", defaults, ""}, + {"nil defaults", nil, "", "", nil, ""}, + {"empty defaults", []string{}, "", "", nil, ""}, + {"nil include", nil, "A", "", nil, ""}, + {"nil exclude", nil, "", "A", nil, ""}, + {"empty tokens", defaults, ",,", ",,", defaults, ""}, + {"leading empty disables include", defaults, ",B", "", defaults, ""}, + {"leading empty skips validation", defaults, ",Missing", "B", []string{"A", "A", "C"}, ""}, + {"include order and duplicates", defaults, "C,A,A", "", []string{"A", "A", "C"}, ""}, + {"include empty tokens", defaults, "B,,C,", "", []string{"B", "C"}, ""}, + {"global include absent locally", defaults, "Other", "", nil, ""}, + {"global exclude absent locally", defaults, "", "Other", defaults, ""}, + {"exclude absent from selection", defaults, "B", "A", []string{"B"}, ""}, + {"exclude first duplicate", defaults, "", "A", []string{"B", "A", "C"}, ""}, + {"exclude repeated duplicate", defaults, "", "A,A,A", []string{"B", "C"}, ""}, + {"exclude empty tokens", defaults, "", ",A,,C,", []string{"B", "A"}, ""}, + {"include then exclude", defaults, "C,A", "A", []string{"A", "C"}, ""}, + {"exclude all is nonnil", defaults, "", "A,B,A,C", []string{}, ""}, + {"exclude selected all is nonnil", defaults, "B", "B,B", []string{}, ""}, + {"no selection remains nil", defaults, "Other", "A", nil, ""}, + {"unregistered defaults allowed", []string{"Local"}, "", "", []string{"Local"}, ""}, + {"unregistered local include", []string{"Local"}, "Local", "", nil, "Local"}, + {"unregistered local exclude", []string{"Local"}, "", "Local", nil, "Local"}, + {"unknown include", defaults, "Missing", "", nil, "Missing"}, + {"unknown exclude", defaults, "", "Missing", nil, "Missing"}, + {"first include error", defaults, "A,Missing,Unknown", "BadExclude", nil, "Missing"}, + {"first exclude error after deletion", defaults, "", "A,Missing,Unknown", nil, "Missing"}, + {"validate excludes after empty selection", defaults, "Other", "Missing", nil, "Missing"}, + {"disabled include still validates exclude", defaults, ",Missing", "Unknown", nil, "Unknown"}, + {"validate include without defaults", nil, "Missing", "", nil, "Missing"}, + {"validate exclude without defaults", nil, "", "Missing", nil, "Missing"}, + {"include no trim", defaults, " A", "", nil, " A"}, + {"exclude no trim", defaults, "", "A ", nil, "A "}, + {"include no case folding", defaults, "a", "", nil, "a"}, + {"exclude no case folding", defaults, "", "b", nil, "b"}, + } { + t.Run(tt.name, func(t *testing.T) { + // Spare capacity catches writes beyond the input slice's length too. + backing := append(append([]string{}, tt.defaults...), "guard", "guard") + input := backing[:len(tt.defaults)] + if tt.defaults == nil { + input = nil + } + before := append([]string(nil), backing...) + got, err := SelectDecoders(input, tt.include, tt.exclude, func(s string) string { return s }, sentinel) + if tt.errName != "" { + if !errors.Is(err, sentinel) || pkgerrors.Cause(err) != sentinel || err.Error() != tt.errName+": "+sentinel.Error() { + t.Fatalf("error = %v, want wrapped sentinel for %q", err, tt.errName) + } + } else if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, tt.want) { + t.Fatalf("selection = %#v, want %#v", got, tt.want) + } + if len(got) > 0 { + got[0] = "changed" + } + got = append(got, "appended") + if !reflect.DeepEqual(backing, before) { + t.Fatalf("defaults backing array changed: %v, want %v", backing, before) + } + }) + } +} + +func TestSelectDecodersConcreteTypes(t *testing.T) { + previous := AllDecoderNames + AllDecoderNames = map[string]struct{}{"A": {}, "B": {}} + t.Cleanup(func() { AllDecoderNames = previous }) + type decoder struct { + name string + data []int + } + a, b, duplicate := decoder{"A", []int{1}}, decoder{"B", []int{2}}, decoder{"A", []int{3}} + sentinel := errors.New("invalid") + values, err := SelectDecoders([]decoder{a, b, duplicate}, "B,A", "A", func(d decoder) string { return d.name }, sentinel) + if err != nil || !reflect.DeepEqual(values, []decoder{b, duplicate}) { + t.Fatalf("value selection = %v, %v", values, err) + } + pointers, err := SelectDecoders([]*decoder{&a, &b, &duplicate}, "B,A", "A", func(d *decoder) string { return d.name }, sentinel) + if err != nil || !reflect.DeepEqual(pointers, []*decoder{&b, &duplicate}) { + t.Fatalf("pointer selection = %v, %v", pointers, err) + } + // Only the slice is copied; selected decoder objects retain their identity. + pointers[0].data[0] = 42 + if b.data[0] != 42 { + t.Fatal("selected decoder was deep-copied") + } +} diff --git a/docker/builders/alpine-builder.Dockerfile b/docker/builders/alpine-builder.Dockerfile index e989d5bd..5334b0a4 100644 --- a/docker/builders/alpine-builder.Dockerfile +++ b/docker/builders/alpine-builder.Dockerfile @@ -1,6 +1,7 @@ # Base Alpine builder image for netcap musl builds # This image contains all build dependencies and can be reused across builds ARG TARGETPLATFORM=linux/amd64 +# Floating minor tag: rebuilds pick up 1.27.x patch releases automatically. FROM --platform=$TARGETPLATFORM golang:1.27-alpine # Install all build dependencies diff --git a/docker/builders/alpine-dpi-builder.Dockerfile b/docker/builders/alpine-dpi-builder.Dockerfile index f9744cef..89ce09d7 100644 --- a/docker/builders/alpine-dpi-builder.Dockerfile +++ b/docker/builders/alpine-dpi-builder.Dockerfile @@ -1,6 +1,7 @@ # Base Alpine builder image for netcap musl builds with DPI support # This image contains all build dependencies including nDPI and libprotoident ARG TARGETPLATFORM=linux/amd64 +# Floating minor tag: rebuilds pick up 1.27.x patch releases automatically. FROM --platform=$TARGETPLATFORM golang:1.27-alpine # Install all build dependencies diff --git a/docker/builders/ubuntu-builder.Dockerfile b/docker/builders/ubuntu-builder.Dockerfile index 85201201..d8579144 100644 --- a/docker/builders/ubuntu-builder.Dockerfile +++ b/docker/builders/ubuntu-builder.Dockerfile @@ -27,10 +27,11 @@ RUN apt-get clean && \ ca-certificates && \ rm -rf /var/lib/apt/lists/* -# Install Go 1.25.1 manually -RUN wget https://go.dev/dl/go1.25.1.linux-amd64.tar.gz && \ - tar -C /usr/local -xzf go1.25.1.linux-amd64.tar.gz && \ - rm go1.25.1.linux-amd64.tar.gz +# Install Go 1.27.0, verified against the go.dev release checksum. +RUN wget https://go.dev/dl/go1.27.0.linux-amd64.tar.gz && \ + echo '675c26c449cbb18fc24b74650de1eabbae6e16f64326fd85a283fb3b58280685 go1.27.0.linux-amd64.tar.gz' | sha256sum -c - && \ + tar -C /usr/local -xzf go1.27.0.linux-amd64.tar.gz && \ + rm go1.27.0.linux-amd64.tar.gz # Set Go environment ENV PATH="/usr/local/go/bin:${PATH}" @@ -82,7 +83,7 @@ RUN test -f /usr/local/include/yara_x.h || (echo "yara_x.h missing" && exit 1) & WORKDIR /workspace # Verify Go installation -RUN go version +RUN go version && test "$(go env GOVERSION)" = go1.27.0 # This image is ready to accept source code and build CMD ["/bin/bash"] diff --git a/docker/builders/ubuntu-dpi-builder.Dockerfile b/docker/builders/ubuntu-dpi-builder.Dockerfile index 3f0ec315..9c43bd8a 100644 --- a/docker/builders/ubuntu-dpi-builder.Dockerfile +++ b/docker/builders/ubuntu-dpi-builder.Dockerfile @@ -29,10 +29,11 @@ RUN apt-get clean && \ ca-certificates && \ rm -rf /var/lib/apt/lists/* -# Install Go 1.25.1 manually -RUN wget https://go.dev/dl/go1.25.1.linux-amd64.tar.gz && \ - tar -C /usr/local -xzf go1.25.1.linux-amd64.tar.gz && \ - rm go1.25.1.linux-amd64.tar.gz +# Install Go 1.27.0, verified against the go.dev release checksum. +RUN wget https://go.dev/dl/go1.27.0.linux-amd64.tar.gz && \ + echo '675c26c449cbb18fc24b74650de1eabbae6e16f64326fd85a283fb3b58280685 go1.27.0.linux-amd64.tar.gz' | sha256sum -c - && \ + tar -C /usr/local -xzf go1.27.0.linux-amd64.tar.gz && \ + rm go1.27.0.linux-amd64.tar.gz # Set Go environment ENV PATH="/usr/local/go/bin:${PATH}" @@ -119,7 +120,7 @@ RUN test -f /usr/local/include/yara_x.h || (echo "yara_x.h missing" && exit 1) & WORKDIR /workspace # Verify Go installation -RUN go version +RUN go version && test "$(go env GOVERSION)" = go1.27.0 # Verify libraries are installed RUN ldconfig -p | grep -E '(ndpi|trace|proto)' diff --git a/docker/dbs-server/Dockerfile b/docker/dbs-server/Dockerfile index f22bbd88..3aacd8df 100644 --- a/docker/dbs-server/Dockerfile +++ b/docker/dbs-server/Dockerfile @@ -2,6 +2,7 @@ # Lightweight container for running the database rebuild service # Build stage +# Floating minor tag: rebuilds pick up 1.27.x patch releases automatically. FROM golang:1.27-alpine AS builder # Install build dependencies (minimal set for nodpi build) diff --git a/internal/mcp/client.go b/internal/mcp/client.go index 4725cbef..07c4bd21 100644 --- a/internal/mcp/client.go +++ b/internal/mcp/client.go @@ -60,6 +60,20 @@ func (c *NetcapClient) Get(path string, query url.Values) (json.RawMessage, erro return c.do(req) } +// GetAs performs Get and decodes the response, returning the zero value on error. +func (c *NetcapClient) GetAs[T any](path string, query url.Values) (T, error) { + var zero T + raw, err := c.Get(path, query) + if err != nil { + return zero, err + } + var result T + if err := json.Unmarshal(raw, &result); err != nil { + return zero, fmt.Errorf("decoding GET %s response: %w", path, err) + } + return result, nil +} + // PostJSON sends a JSON-encoded POST request and returns the raw response. func (c *NetcapClient) PostJSON(path string, body any) (json.RawMessage, error) { var buf bytes.Buffer diff --git a/internal/mcp/client_test.go b/internal/mcp/client_test.go new file mode 100644 index 00000000..b0736f63 --- /dev/null +++ b/internal/mcp/client_test.go @@ -0,0 +1,106 @@ +package mcp + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" +) + +func TestGetAs(t *testing.T) { + type response struct { + Name string `json:"name"` + Count int `json:"count"` + } + for _, tt := range []struct { + name string + body string + status int + want response + decodeError bool + }{ + {name: "success", body: `{"name":"netcap","count":2}`, status: 200, want: response{"netcap", 2}}, + {name: "zero", body: `{}`, status: 200}, + {name: "null", body: `null`, status: 200}, + {name: "malformed", body: `{"name":`, status: 200, decodeError: true}, + {name: "partial decode", body: `{"name":"discard","count":"bad"}`, status: 200, decodeError: true}, + {name: "empty", status: 204, decodeError: true}, + {name: "http error", body: `upstream failed`, status: 500}, + {name: "redirect status policy", body: `{"count":3}`, status: 302, want: response{Count: 3}}, + } { + t.Run(tt.name, func(t *testing.T) { + query := url.Values{"inputFile": {"a b&c.pcap"}, "tag": {"one", "two"}} + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet || r.URL.Path != "/api/test" || r.URL.RawQuery != query.Encode() || r.Header.Get("Accept") != "application/json" { + t.Errorf("unexpected request: %s %s %v", r.Method, r.URL, r.Header) + } + w.WriteHeader(tt.status) + fmt.Fprint(w, tt.body) + })) + defer ts.Close() + client := NewNetcapClient(ts.URL) + got, err := client.GetAs[response]("/api/test", query) + if got != tt.want { + t.Fatalf("got %+v, want %+v", got, tt.want) + } + if tt.decodeError { + var syntax *json.SyntaxError + var mismatch *json.UnmarshalTypeError + if err == nil || !strings.Contains(err.Error(), "decoding GET /api/test response:") || (!errors.As(err, &syntax) && !errors.As(err, &mismatch)) { + t.Fatalf("expected wrapped JSON error, got %v", err) + } + } else if (err != nil) != (tt.status >= 400) { + t.Fatalf("unexpected error: %v", err) + } + raw, rawErr := client.Get("/api/test", query) + if tt.status >= 400 { + if raw != nil || rawErr == nil || err == nil || err.Error() != rawErr.Error() { + t.Fatalf("HTTP error differs from Get: %q, %v, %v", raw, rawErr, err) + } + } else if rawErr != nil || string(raw) != tt.body { + t.Fatalf("Get changed raw response: %q, %v", raw, rawErr) + } + }) + } +} + +func TestGetAsNil(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, "null") + })) + defer ts.Close() + c := NewNetcapClient(ts.URL) + if got, err := c.GetAs[map[string]any]("/", nil); got != nil || err != nil { + t.Fatalf("null map: %v, %v", got, err) + } + if got, err := c.GetAs[[]string]("/", nil); got != nil || err != nil { + t.Fatalf("null slice: %v, %v", got, err) + } + if got, err := c.GetAs[*int]("/", nil); got != nil || err != nil { + t.Fatalf("null pointer: %v, %v", got, err) + } +} + +type failingTransport struct{ err error } + +func (f failingTransport) RoundTrip(*http.Request) (*http.Response, error) { + return nil, f.err +} + +func TestGetAsTransportError(t *testing.T) { + want := errors.New("transport failed") + c := NewNetcapClient("http://netcap.invalid") + c.hc.Transport = failingTransport{want} + got, err := c.GetAs[*int]("/api/test", nil) + if got != nil || !errors.Is(err, want) { + t.Fatalf("got %v, %v; want nil and wrapped transport error", got, err) + } + raw, rawErr := c.Get("/api/test", nil) + if raw != nil || !errors.Is(rawErr, want) || rawErr.Error() != err.Error() { + t.Fatalf("transport error differs from Get: %q, %v", raw, rawErr) + } +} diff --git a/internal/mcp/tools_list_sessions_test.go b/internal/mcp/tools_list_sessions_test.go index d0240a30..da4d8f07 100644 --- a/internal/mcp/tools_list_sessions_test.go +++ b/internal/mcp/tools_list_sessions_test.go @@ -127,6 +127,25 @@ func TestListSessionsLocalMode(t *testing.T) { } } +func TestCollectSessionsDecodeFallback(t *testing.T) { + for _, body := range []string{`{"sessions":`, `null`, `{}`, `{"sessions":null}`, `{"sessions":[]}`} { + t.Run(body, func(t *testing.T) { + f := &fakeWebui{tryResp: body, filesResp: `[]`} + ts := httptest.NewServer(f.handler(t)) + defer ts.Close() + srv := mustNewServer(t, ts.URL) + sessions, mode, err := srv.collectSessions(NewNetcapClient(ts.URL)) + wantMode := "local" + if body == `{"sessions":[]}` { + wantMode = "service" + } + if err != nil || mode != wantMode || sessions == nil || len(sessions) != 0 { + t.Fatalf("got %v, %q, %v; want non-nil empty sessions in %s mode", sessions, mode, err, wantMode) + } + }) + } +} + func mustNewServer(t *testing.T, baseURL string) *Server { t.Helper() srv, err := New(Options{BaseURL: baseURL}) diff --git a/internal/mcp/tools_live.go b/internal/mcp/tools_live.go index 0d3e868d..0d88f70b 100644 --- a/internal/mcp/tools_live.go +++ b/internal/mcp/tools_live.go @@ -8,7 +8,6 @@ package mcp import ( "context" - "encoding/json" "fmt" mcplib "github.com/mark3labs/mcp-go/mcp" @@ -65,20 +64,16 @@ func (s *Server) registerLiveCaptureTools() error { } func (s *Server) handleLiveStatus(_ context.Context, _ mcplib.CallToolRequest) (*mcplib.CallToolResult, error) { - raw, err := s.newClient().Get("/api/status", nil) + st, err := s.newClient().GetAs[map[string]any]("/api/status", nil) if err != nil { return errResult(fmt.Errorf("status: %w", err)), nil } - var st map[string]any - if jErr := json.Unmarshal(raw, &st); jErr != nil { - return errResult(fmt.Errorf("decode status: %w", jErr)), nil - } out := map[string]any{ - "is_live_mode": st["isLiveMode"], - "is_processing": st["isProcessing"], + "is_live_mode": st["isLiveMode"], + "is_processing": st["isProcessing"], "current_session": st["sessionId"], - "active_input": st["activeInputFile"], - "server_started": st["serverStarted"], + "active_input": st["activeInputFile"], + "server_started": st["serverStarted"], } return jsonResult(out), nil } diff --git a/internal/mcp/tools_pipeline.go b/internal/mcp/tools_pipeline.go index 16115fe4..2b6acf8d 100644 --- a/internal/mcp/tools_pipeline.go +++ b/internal/mcp/tools_pipeline.go @@ -284,27 +284,20 @@ func (s *Server) handleListSessions(_ context.Context, req mcplib.CallToolReques // collectSessions probes service mode first, falling back to local-mode // file enumeration. Always returns sessions in the unified shape. func (s *Server) collectSessions(client *NetcapClient) ([]map[string]any, string, error) { - if raw, err := client.Get("/api/try/sessions", nil); err == nil { - var resp struct { - Sessions []map[string]any `json:"sessions"` - } - if jsonErr := json.Unmarshal(raw, &resp); jsonErr == nil && resp.Sessions != nil { - out := make([]map[string]any, 0, len(resp.Sessions)) - for _, sess := range resp.Sessions { - out = append(out, unifiedSession("service", sess)) - } - return out, "service", nil + if resp, err := client.GetAs[struct { + Sessions []map[string]any `json:"sessions"` + }]("/api/try/sessions", nil); err == nil && resp.Sessions != nil { + out := make([]map[string]any, 0, len(resp.Sessions)) + for _, sess := range resp.Sessions { + out = append(out, unifiedSession("service", sess)) } + return out, "service", nil } - raw, err := client.Get("/api/files/input", nil) + files, err := client.GetAs[[]map[string]any]("/api/files/input", nil) if err != nil { return nil, "", fmt.Errorf("listing input files: %w", err) } - var files []map[string]any - if jsonErr := json.Unmarshal(raw, &files); jsonErr != nil { - return nil, "", fmt.Errorf("decoding file list: %w", jsonErr) - } out := make([]map[string]any, 0, len(files)+1) for _, f := range files { out = append(out, unifiedSession("local", f)) @@ -313,9 +306,9 @@ func (s *Server) collectSessions(client *NetcapClient) ([]map[string]any, string // CLI preload not yet visible in /api/files/input — synthesise a // minimal row so the LLM can still call analytical tools. out = append(out, unifiedSession("local", map[string]any{ - "path": s.activeSession, - "name": s.activeSession, - "isCompleted": false, + "path": s.activeSession, + "name": s.activeSession, + "isCompleted": false, "cli_preloaded_flag": true, })) } diff --git a/maltego/arp.go b/maltego/arp.go index a248ce25..5e86c317 100644 --- a/maltego/arp.go +++ b/maltego/arp.go @@ -44,7 +44,7 @@ type ARPCountFunc func() // ARPTransformationFunc is a transformation over ARP audit records. // //goland:noinspection GoUnnecessarilyExportedIdentifiers -type ARPTransformationFunc = func(lt maltego.LocalTransform, trx *maltego.Transform, arp *types.ARP, min, max uint64, path string, ip string) +type ARPTransformationFunc = transformationFunc[types.ARP] // ARPTransform applies a maltego transformation over ARP audit records. func ARPTransform(count ARPCountFunc, transform ARPTransformationFunc, continueTransform bool) { diff --git a/maltego/callbacks.go b/maltego/callbacks.go new file mode 100644 index 00000000..ace540b0 --- /dev/null +++ b/maltego/callbacks.go @@ -0,0 +1,10 @@ +package maltego + +import "github.com/dreadl0ck/maltego" + +// The selected address can be an IP or MAC, depending on the transform. +type transformationFunc[T any] = func(lt maltego.LocalTransform, trx *maltego.Transform, record *T, min, max uint64, path, address string) + +type transformationWithMACFunc[T any] = func(lt maltego.LocalTransform, trx *maltego.Transform, record *T, min, max uint64, path, mac, ip string) + +type countFunc[T any] = func(record *T, mac string, min, max *uint64) diff --git a/maltego/callbacks_test.go b/maltego/callbacks_test.go new file mode 100644 index 00000000..db0a69a8 --- /dev/null +++ b/maltego/callbacks_test.go @@ -0,0 +1,43 @@ +package maltego + +import ( + "github.com/dreadl0ck/maltego" + "github.com/dreadl0ck/netcap/types" +) + +// Compile-time checks for literals, alias assignments, and exact type identity. +func callbackAssignability() { + var http HTTPTransformationFunc = func(maltego.LocalTransform, *maltego.Transform, *types.HTTP, uint64, uint64, string, string) {} + var genericHTTP transformationFunc[types.HTTP] = http + http = genericHTTP + var _ *func(maltego.LocalTransform, *maltego.Transform, *types.HTTP, uint64, uint64, string, string) = &http + var _ *transformationFunc[types.HTTP] = &http + + var device deviceProfileTransformationFunc = func(maltego.LocalTransform, *maltego.Transform, *types.DeviceProfile, uint64, uint64, string, string) { + } + var genericDevice transformationFunc[types.DeviceProfile] = device + device = genericDevice + + var ssh SSHTransformationFunc = func(maltego.LocalTransform, *maltego.Transform, *types.SSH, uint64, uint64, string, string, string) {} + var genericSSH transformationWithMACFunc[types.SSH] = ssh + ssh = genericSSH + var _ *func(maltego.LocalTransform, *maltego.Transform, *types.SSH, uint64, uint64, string, string, string) = &ssh + var _ *transformationWithMACFunc[types.SSH] = &ssh + + var host HostTransformationFunc = func(maltego.LocalTransform, *maltego.Transform, *types.Host, uint64, uint64, string, string, string) { + } + var ip IPTransformationFunc = host + var genericHost transformationWithMACFunc[types.Host] = ip + host = genericHost + var _ *IPTransformationFunc = &host + + var count SSHCountFunc = func(*types.SSH, string, *uint64, *uint64) {} + var genericCount countFunc[types.SSH] = count + count = genericCount + var _ *func(*types.SSH, string, *uint64, *uint64) = &count + var _ *countFunc[types.SSH] = &count + + var service serviceCountFunc = func(*types.Service, string, *uint64, *uint64) {} + var genericService countFunc[types.Service] = service + service = genericService +} diff --git a/maltego/deviceProfile.go b/maltego/deviceProfile.go index d297154b..f44fdba8 100644 --- a/maltego/deviceProfile.go +++ b/maltego/deviceProfile.go @@ -83,7 +83,7 @@ func countIP(ips map[string]*types.Host, ip string, min, max *uint64) { type deviceProfileCountFunc = func(profile *types.DeviceProfile, mac string, min, max *uint64, ips map[string]*types.Host) // deviceProfileTransformationFunc is transform over DeviceProfiles. -type deviceProfileTransformationFunc = func(lt maltego.LocalTransform, trx *maltego.Transform, profile *types.DeviceProfile, min, max uint64, path string, mac string) +type deviceProfileTransformationFunc = transformationFunc[types.DeviceProfile] // DeviceProfileTransform applies a maltego transformation DeviceProfile audit records. func DeviceProfileTransform(count deviceProfileCountFunc, transform deviceProfileTransformationFunc) { diff --git a/maltego/dhcpv4.go b/maltego/dhcpv4.go index f5db097c..bd44a4ad 100644 --- a/maltego/dhcpv4.go +++ b/maltego/dhcpv4.go @@ -43,7 +43,7 @@ type DHCPCountFunc func() // DHCPV4TransformationFunc is a transformation over DHCPv4 audit records. // //goland:noinspection GoUnnecessarilyExportedIdentifiers -type DHCPV4TransformationFunc = func(lt maltego.LocalTransform, trx *maltego.Transform, dhcp *types.DHCPv4, min, max uint64, path string, ip string) +type DHCPV4TransformationFunc = transformationFunc[types.DHCPv4] // DHCPV4Transform applies a maltego transformation over DHCP audit records. func DHCPV4Transform(count DHCPCountFunc, transform DHCPV4TransformationFunc, continueTransform bool) { diff --git a/maltego/dhcpv6.go b/maltego/dhcpv6.go index ac34caa0..dcbea962 100644 --- a/maltego/dhcpv6.go +++ b/maltego/dhcpv6.go @@ -38,7 +38,7 @@ import ( // DHCPV6TransformationFunc is a transformation over DHCPv6 audit records. // //goland:noinspection GoUnnecessarilyExportedIdentifiers -type DHCPV6TransformationFunc = func(lt maltego.LocalTransform, trx *maltego.Transform, dhcp *types.DHCPv6, min, max uint64, path string, ip string) +type DHCPV6TransformationFunc = transformationFunc[types.DHCPv6] // DHCPV6Transform applies a maltego transformation over DHCP audit records. func DHCPV6Transform(count DHCPCountFunc, transform DHCPV6TransformationFunc, continueTransform bool) { diff --git a/maltego/dns.go b/maltego/dns.go index 1e75c2f2..7c22dde6 100644 --- a/maltego/dns.go +++ b/maltego/dns.go @@ -43,7 +43,7 @@ type DNSCountFunc func() // DNSTransformationFunc is a transformation over DNS audit records. // //goland:noinspection GoUnnecessarilyExportedIdentifiers -type DNSTransformationFunc = func(lt maltego.LocalTransform, trx *maltego.Transform, dns *types.DNS, min, max uint64, path string, ip string) +type DNSTransformationFunc = transformationFunc[types.DNS] // DNSTransform applies a maltego transformation over DNS audit records. func DNSTransform(count DNSCountFunc, transform DNSTransformationFunc, continueTransform bool) { diff --git a/maltego/ethernet.go b/maltego/ethernet.go index 9469a10b..a37ddecc 100644 --- a/maltego/ethernet.go +++ b/maltego/ethernet.go @@ -43,7 +43,7 @@ type EthernetCountFunc func() // EthernetTransformationFunc is a transformation over Ethernet audit records. // //goland:noinspection GoUnnecessarilyExportedIdentifiers -type EthernetTransformationFunc = func(lt maltego.LocalTransform, trx *maltego.Transform, ethernet *types.Ethernet, min, max uint64, path string, ip string) +type EthernetTransformationFunc = transformationFunc[types.Ethernet] // EthernetTransform applies a maltego transformation over Ethernet audit records. func EthernetTransform(count EthernetCountFunc, transform EthernetTransformationFunc, continueTransform bool) { diff --git a/maltego/exploit.go b/maltego/exploit.go index 72ecac93..f588238c 100644 --- a/maltego/exploit.go +++ b/maltego/exploit.go @@ -36,10 +36,10 @@ import ( ) // exploitTransformationFunc is a transformation over Exploit exploits for a selected Exploit. -type exploitTransformationFunc = func(lt maltego.LocalTransform, trx *maltego.Transform, exploit *types.Exploit, min, max uint64, exploitsFile string, mac string, ip string) +type exploitTransformationFunc = transformationWithMACFunc[types.Exploit] // deviceProfileCountFunc is a function that counts something over DeviceProfiles. -type exploitCountFunc = func(exploit *types.Exploit, mac string, min, max *uint64) +type exploitCountFunc = countFunc[types.Exploit] // ExploitTransform applies a maltego transformation over Exploit exploits seen for a target Exploit. func ExploitTransform(count exploitCountFunc, transform exploitTransformationFunc) { diff --git a/maltego/file.go b/maltego/file.go index fb2ee478..5a271d10 100644 --- a/maltego/file.go +++ b/maltego/file.go @@ -39,7 +39,7 @@ import ( type filesCountFunc func() // filesTransformationFunc is a transformation over File audit records. -type filesTransformationFunc = func(lt maltego.LocalTransform, trx *maltego.Transform, file *types.File, min, max uint64, path string, ip string) +type filesTransformationFunc = transformationFunc[types.File] // FilesTransform applies a maltego transformation over File audit records. func FilesTransform(count filesCountFunc, transform filesTransformationFunc) { diff --git a/maltego/host.go b/maltego/host.go index 8ba9482f..f4d6167a 100644 --- a/maltego/host.go +++ b/maltego/host.go @@ -39,7 +39,7 @@ import ( // IPTransformationFunc is a transformation over IP profiles for a selected DeviceProfile. // //goland:noinspection GoUnnecessarilyExportedIdentifiers -type IPTransformationFunc = func(lt maltego.LocalTransform, trx *maltego.Transform, profile *types.Host, min, max uint64, path string, mac string, ip string) +type IPTransformationFunc = transformationWithMACFunc[types.Host] // deviceProfileCountFunc is a function that counts something over DeviceProfiles. type hostCountFunc = func(profile *types.Host, mac string, min, max *uint64, ips map[string]*types.Host) @@ -57,7 +57,7 @@ var CountIPPackets = func(profile *types.Host, mac string, min, max *uint64, _ m // HostTransformationFunc is a transformation over IP profiles // //goland:noinspection GoUnnecessarilyExportedIdentifiers -type HostTransformationFunc = func(lt maltego.LocalTransform, trx *maltego.Transform, profile *types.Host, min, max uint64, path string, mac string, ip string) +type HostTransformationFunc = transformationWithMACFunc[types.Host] // HostTransform applies a maltego transformation over IP profiles func HostTransform(count hostCountFunc, transform HostTransformationFunc) { diff --git a/maltego/http.go b/maltego/http.go index 0a9012ab..b71599f3 100644 --- a/maltego/http.go +++ b/maltego/http.go @@ -43,7 +43,7 @@ type HTTPCountFunc = func(http *types.HTTP, min, max *uint64) // HTTPTransformationFunc is a transformation over HTTP audit records. // //goland:noinspection GoUnnecessarilyExportedIdentifiers -type HTTPTransformationFunc = func(lt maltego.LocalTransform, trx *maltego.Transform, http *types.HTTP, min, max uint64, path string, ip string) +type HTTPTransformationFunc = transformationFunc[types.HTTP] // HTTPTransform applies a maltego transformation over HTTP audit records. func HTTPTransform(count HTTPCountFunc, transform HTTPTransformationFunc, continueTransform bool) { diff --git a/maltego/icmpv4.go b/maltego/icmpv4.go index 54074313..057bb1c5 100644 --- a/maltego/icmpv4.go +++ b/maltego/icmpv4.go @@ -37,7 +37,7 @@ type ICMPv4CountFunc func() // ICMPv4TransformationFunc is a transformation over ICMPv4 audit records. // //goland:noinspection GoUnnecessarilyExportedIdentifiers -type ICMPv4TransformationFunc = func(lt maltego.LocalTransform, trx *maltego.Transform, icmp *types.ICMPv4, min, max uint64, path string, ip string) +type ICMPv4TransformationFunc = transformationFunc[types.ICMPv4] // ICMPv4Transform applies a maltego transformation over ICMPv4 audit records. func ICMPv4Transform(count ICMPv4CountFunc, transform ICMPv4TransformationFunc) { diff --git a/maltego/icmpv6.go b/maltego/icmpv6.go index f49b6837..4a4e7269 100644 --- a/maltego/icmpv6.go +++ b/maltego/icmpv6.go @@ -37,7 +37,7 @@ type ICMPv6CountFunc func() // ICMPv6TransformationFunc is a transformation over ICMPv6 audit records. // //goland:noinspection GoUnnecessarilyExportedIdentifiers -type ICMPv6TransformationFunc = func(lt maltego.LocalTransform, trx *maltego.Transform, icmp *types.ICMPv6, min, max uint64, path string, ip string) +type ICMPv6TransformationFunc = transformationFunc[types.ICMPv6] // ICMPv6Transform applies a maltego transformation over ICMPv6 audit records. func ICMPv6Transform(count ICMPv6CountFunc, transform ICMPv6TransformationFunc) { diff --git a/maltego/igmp.go b/maltego/igmp.go index 6859e88f..82049a0a 100644 --- a/maltego/igmp.go +++ b/maltego/igmp.go @@ -43,7 +43,7 @@ type IGMPCountFunc func() // IGMPTransformationFunc is a transformation over IGMP audit records. // //goland:noinspection GoUnnecessarilyExportedIdentifiers -type IGMPTransformationFunc = func(lt maltego.LocalTransform, trx *maltego.Transform, igmp *types.IGMP, min, max uint64, path string, ip string) +type IGMPTransformationFunc = transformationFunc[types.IGMP] // IGMPTransform applies a maltego transformation over IGMP audit records. func IGMPTransform(count IGMPCountFunc, transform IGMPTransformationFunc, continueTransform bool) { diff --git a/maltego/ipv4.go b/maltego/ipv4.go index 01bab580..6499ba5d 100644 --- a/maltego/ipv4.go +++ b/maltego/ipv4.go @@ -41,7 +41,7 @@ type ipCountFunc = func(ip string, min, max *uint64) // IPv4TransformationFunc is a transformation over IPv4 audit records // //goland:noinspection GoUnnecessarilyExportedIdentifiers -type IPv4TransformationFunc = func(lt maltego.LocalTransform, trx *maltego.Transform, ipv4 *types.IPv4, min, max uint64, path string, mac string, ip string) +type IPv4TransformationFunc = transformationWithMACFunc[types.IPv4] // IPv4Transform applies a maltego transformation over IP profiles func IPv4Transform(count ipCountFunc, transform IPv4TransformationFunc, continueTransform bool) { diff --git a/maltego/ipv6.go b/maltego/ipv6.go index 7acd589d..3cc97164 100644 --- a/maltego/ipv6.go +++ b/maltego/ipv6.go @@ -41,7 +41,7 @@ type ipv6CountFunc = func(ip string, min, max *uint64) // IPv6TransformationFunc is a transformation over IPv6 audit records // //goland:noinspection GoUnnecessarilyExportedIdentifiers -type IPv6TransformationFunc = func(lt maltego.LocalTransform, trx *maltego.Transform, ipv6 *types.IPv6, min, max uint64, path string, mac string, ip string) +type IPv6TransformationFunc = transformationWithMACFunc[types.IPv6] // IPv6Transform applies a maltego transformation over IP profiles func IPv6Transform(count ipv6CountFunc, transform IPv6TransformationFunc, continueTransform bool) { diff --git a/maltego/ipv6hopbyhop.go b/maltego/ipv6hopbyhop.go index 4e549d85..a238bbbf 100644 --- a/maltego/ipv6hopbyhop.go +++ b/maltego/ipv6hopbyhop.go @@ -39,7 +39,7 @@ import ( // IPv6HopByHopTransformationFunc is a transformation over IPv6HopByHop audit records // //goland:noinspection GoUnnecessarilyExportedIdentifiers -type IPv6HopByHopTransformationFunc = func(lt maltego.LocalTransform, trx *maltego.Transform, ipv6 *types.IPv6HopByHop, min, max uint64, path string, mac string, ip string) +type IPv6HopByHopTransformationFunc = transformationWithMACFunc[types.IPv6HopByHop] // IPv6HopByHopTransform applies a maltego transformation over IP profiles func IPv6HopByHopTransform(count ipv6CountFunc, transform IPv6HopByHopTransformationFunc) { diff --git a/maltego/mail.go b/maltego/mail.go index 649c8f34..f64b1d39 100644 --- a/maltego/mail.go +++ b/maltego/mail.go @@ -44,7 +44,7 @@ type MailCountFunc func() // MailTransformationFunc is a transformation over Mail audit records. // //goland:noinspection GoUnnecessarilyExportedIdentifiers -type MailTransformationFunc = func(lt maltego.LocalTransform, trx *maltego.Transform, mail *types.Mail, min, max uint64, path string, ip string) +type MailTransformationFunc = transformationFunc[types.Mail] // MailTransform applies a maltego transformation over Mail audit records. func MailTransform(count MailCountFunc, transform MailTransformationFunc) { diff --git a/maltego/ntp.go b/maltego/ntp.go index 9e6ca6b8..bcb5d8c4 100644 --- a/maltego/ntp.go +++ b/maltego/ntp.go @@ -43,7 +43,7 @@ type NTPCountFunc func() // NTPTransformationFunc is a transformation over NTP audit records. // //goland:noinspection GoUnnecessarilyExportedIdentifiers -type NTPTransformationFunc = func(lt maltego.LocalTransform, trx *maltego.Transform, ntp *types.NTP, min, max uint64, path string, ip string) +type NTPTransformationFunc = transformationFunc[types.NTP] // NTPTransform applies a maltego transformation over NTP audit records. func NTPTransform(count NTPCountFunc, transform NTPTransformationFunc, continueTransform bool) { diff --git a/maltego/pop3.go b/maltego/pop3.go index 3d703ebe..ff16ecf8 100644 --- a/maltego/pop3.go +++ b/maltego/pop3.go @@ -43,7 +43,7 @@ type POP3CountFunc func() // POP3TransformationFunc is a transformation over POP3 audit records. // //goland:noinspection GoUnnecessarilyExportedIdentifiers -type POP3TransformationFunc = func(lt maltego.LocalTransform, trx *maltego.Transform, pop3 *types.POP3, min, max uint64, path string, ip string) +type POP3TransformationFunc = transformationFunc[types.POP3] // POP3Transform applies a maltego transformation over POP3 audit records. func POP3Transform(count POP3CountFunc, transform POP3TransformationFunc, continueTransform bool) { diff --git a/maltego/secret.go b/maltego/secret.go index ba175f00..d0dbdc31 100644 --- a/maltego/secret.go +++ b/maltego/secret.go @@ -36,10 +36,10 @@ import ( ) // secretTransformationFunc is a transformation over Secret records for a selected Secret. -type secretTransformationFunc = func(lt maltego.LocalTransform, trx *maltego.Transform, profile *types.Secret, min, max uint64, path string, mac string, ip string) +type secretTransformationFunc = transformationWithMACFunc[types.Secret] // secretCountFunc is a function that counts something over Secret records. -type secretCountFunc = func(credentials *types.Secret, mac string, min, max *uint64) +type secretCountFunc = countFunc[types.Secret] // SecretTransform applies a maltego transformation over Secret records seen for a target Secret. func SecretTransform(count secretCountFunc, transform secretTransformationFunc) { diff --git a/maltego/service.go b/maltego/service.go index 0730c198..deaff104 100644 --- a/maltego/service.go +++ b/maltego/service.go @@ -37,10 +37,10 @@ import ( ) // serviceTransformationFunc is a transformation over Service profiles for a selected Service. -type serviceTransformationFunc = func(lt maltego.LocalTransform, trx *maltego.Transform, profile *types.Service, min, max uint64, path string, mac string, ip string) +type serviceTransformationFunc = transformationWithMACFunc[types.Service] // deviceProfileCountFunc is a function that counts something over DeviceProfiles. -type serviceCountFunc = func(service *types.Service, mac string, min, max *uint64) +type serviceCountFunc = countFunc[types.Service] // ServiceTransform applies a maltego transformation over Service profiles seen for a target Service. func ServiceTransform(count serviceCountFunc, transform serviceTransformationFunc, continueTransform bool) { diff --git a/maltego/smtp.go b/maltego/smtp.go index 4405f794..7fba7575 100644 --- a/maltego/smtp.go +++ b/maltego/smtp.go @@ -43,7 +43,7 @@ type SMTPCountFunc func() // SMTPTransformationFunc is a transformation over SMTP audit records. // //goland:noinspection GoUnnecessarilyExportedIdentifiers -type SMTPTransformationFunc = func(lt maltego.LocalTransform, trx *maltego.Transform, smtp *types.SMTP, min, max uint64, path string, ip string) +type SMTPTransformationFunc = transformationFunc[types.SMTP] // SMTPTransform applies a maltego transformation over SMTP audit records. func SMTPTransform(count SMTPCountFunc, transform SMTPTransformationFunc, continueTransform bool) { diff --git a/maltego/software.go b/maltego/software.go index 905120c1..78ef1b9f 100644 --- a/maltego/software.go +++ b/maltego/software.go @@ -37,10 +37,10 @@ import ( ) // softwareTransformationFunc is a transformation over Software profiles for a selected Software. -type softwareTransformationFunc = func(lt maltego.LocalTransform, trx *maltego.Transform, profile *types.Software, min, max uint64, path string, mac string, ip string) +type softwareTransformationFunc = transformationWithMACFunc[types.Software] // deviceProfileCountFunc is a function that counts something over DeviceProfiles. -type softwareCountFunc = func(software *types.Software, mac string, min, max *uint64) +type softwareCountFunc = countFunc[types.Software] // SoftwareTransform applies a maltego transformation over Software profiles seen for a target Software. func SoftwareTransform(count softwareCountFunc, transform softwareTransformationFunc) { diff --git a/maltego/ssh.go b/maltego/ssh.go index f8888314..6b41a791 100644 --- a/maltego/ssh.go +++ b/maltego/ssh.go @@ -38,12 +38,12 @@ import ( // SSHTransformationFunc is a transformation over SSH sshs for a selected SSH. // //goland:noinspection GoUnnecessarilyExportedIdentifiers -type SSHTransformationFunc = func(lt maltego.LocalTransform, trx *maltego.Transform, ssh *types.SSH, min, max uint64, sshsFile string, mac string, ip string) +type SSHTransformationFunc = transformationWithMACFunc[types.SSH] // SSHCountFunc deviceProfileCountFunc is a function that counts something over DeviceProfiles. // //goland:noinspection GoUnnecessarilyExportedIdentifiers -type SSHCountFunc = func(ssh *types.SSH, mac string, min, max *uint64) +type SSHCountFunc = countFunc[types.SSH] // SSHTransform applies a maltego transformation over SSH sshs seen for a target SSH. func SSHTransform(count SSHCountFunc, transform SSHTransformationFunc) { diff --git a/maltego/tcp.go b/maltego/tcp.go index 570f2292..2164869f 100644 --- a/maltego/tcp.go +++ b/maltego/tcp.go @@ -43,7 +43,7 @@ type TCPCountFunc func() // TCPTransformationFunc is a transformation over TCP audit records. // //goland:noinspection GoUnnecessarilyExportedIdentifiers -type TCPTransformationFunc = func(lt maltego.LocalTransform, trx *maltego.Transform, tcp *types.TCP, min, max uint64, path string, ip string) +type TCPTransformationFunc = transformationFunc[types.TCP] // TCPTransform applies a maltego transformation over TCP audit records. func TCPTransform(count TCPCountFunc, transform TCPTransformationFunc, continueTransform bool) { diff --git a/maltego/tls_client_hello.go b/maltego/tls_client_hello.go index 068bea7a..fc68e1bf 100644 --- a/maltego/tls_client_hello.go +++ b/maltego/tls_client_hello.go @@ -43,7 +43,7 @@ type TLSClientHelloCountFunc func() // TLSClientHelloTransformationFunc is a transformation over TLSClientHello audit records. // //goland:noinspection GoUnnecessarilyExportedIdentifiers -type TLSClientHelloTransformationFunc = func(lt maltego.LocalTransform, trx *maltego.Transform, hello *types.TLSClientHello, min, max uint64, path string, ip string) +type TLSClientHelloTransformationFunc = transformationFunc[types.TLSClientHello] // TLSClientHelloTransform applies a maltego transformation over TLSClientHello audit records. func TLSClientHelloTransform(count TLSClientHelloCountFunc, transform TLSClientHelloTransformationFunc) { diff --git a/maltego/tls_server_hello.go b/maltego/tls_server_hello.go index 26d5c75c..7955edec 100644 --- a/maltego/tls_server_hello.go +++ b/maltego/tls_server_hello.go @@ -43,7 +43,7 @@ type TLSServerHelloCountFunc func() // TLSServerHelloTransformationFunc is a transformation over TLSServerHello audit records. // //goland:noinspection GoUnnecessarilyExportedIdentifiers -type TLSServerHelloTransformationFunc = func(lt maltego.LocalTransform, trx *maltego.Transform, pop3 *types.TLSServerHello, min, max uint64, path string, ip string) +type TLSServerHelloTransformationFunc = transformationFunc[types.TLSServerHello] // TLSServerHelloTransform applies a maltego transformation over TLSServerHello audit records. func TLSServerHelloTransform(count TLSServerHelloCountFunc, transform TLSServerHelloTransformationFunc) { diff --git a/maltego/udp.go b/maltego/udp.go index 11222ea4..d4b1ebac 100644 --- a/maltego/udp.go +++ b/maltego/udp.go @@ -43,7 +43,7 @@ type UDPCountFunc func() // UDPTransformationFunc is a transformation over UDP audit records. // //goland:noinspection GoUnnecessarilyExportedIdentifiers -type UDPTransformationFunc = func(lt maltego.LocalTransform, trx *maltego.Transform, udp *types.UDP, min, max uint64, path string, ip string) +type UDPTransformationFunc = transformationFunc[types.UDP] // UDPTransform applies a maltego transformation over UDP audit records. func UDPTransform(count UDPCountFunc, transform UDPTransformationFunc, continueTransform bool) { diff --git a/maltego/vulnerability.go b/maltego/vulnerability.go index 4ea0535f..a74dfd3a 100644 --- a/maltego/vulnerability.go +++ b/maltego/vulnerability.go @@ -36,10 +36,10 @@ import ( ) // vulnerabilityTransformationFunc is a transformation over Vulnerability vulns for a selected Vulnerability. -type vulnerabilityTransformationFunc = func(lt maltego.LocalTransform, trx *maltego.Transform, vuln *types.Vulnerability, min, max uint64, vulnsFile string, mac string, ip string) +type vulnerabilityTransformationFunc = transformationWithMACFunc[types.Vulnerability] // deviceProfileCountFunc is a function that counts something over DeviceProfiles. -type vulnerabilityCountFunc = func(vuln *types.Vulnerability, mac string, min, max *uint64) +type vulnerabilityCountFunc = countFunc[types.Vulnerability] // VulnerabilityTransform applies a maltego transformation over Vulnerability vulns seen for a target Vulnerability. func VulnerabilityTransform(count vulnerabilityCountFunc, transform vulnerabilityTransformationFunc) {