diff --git a/pkg/resolver/network/network_test.go b/pkg/resolver/network/network_test.go new file mode 100644 index 0000000..034fc2e --- /dev/null +++ b/pkg/resolver/network/network_test.go @@ -0,0 +1,147 @@ +package network + +import ( + "testing" + + "github.com/sbomit/sbomit/pkg/attestation" + "github.com/sbomit/sbomit/pkg/resolver" +) + +// mockDomainResolver implements DomainResolver for testing. +type mockDomainResolver struct { + domains []string + packages []resolver.PackageInfo +} + +func (m *mockDomainResolver) Domains() []string { + return m.domains +} + +func (m *mockDomainResolver) Resolve(conn NetworkConnection) []resolver.PackageInfo { + return m.packages +} + +func TestExtractConnections(t *testing.T) { + attestations := []attestation.TypedAttestation{ + { + Type: "network-trace", + Data: map[string]interface{}{ + "network_trace": map[string]interface{}{ + "connections": []interface{}{ + map[string]interface{}{ + "protocol": "tcp", + "destination": map[string]interface{}{ + "hostname": "example.com", + "ip": "192.0.2.1", + }, + "http_exchanges": []interface{}{ + map[string]interface{}{ + "request": map[string]interface{}{ + "url": "https://example.com/pkg", + "method": "GET", + "headers": map[string]interface{}{ + "Referer": []interface{}{"https://ref.com"}, + }, + }, + "response": map[string]interface{}{ + "status_code": float64(200), + "body": map[string]interface{}{ + "hash": "abcdef123456", + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + Type: "other-type", + Data: map[string]interface{}{}, + }, + } + + conns := ExtractConnections(attestations) + + if len(conns) != 1 { + t.Fatalf("expected 1 connection, got %d", len(conns)) + } + + conn := conns[0] + if conn.Protocol != "tcp" { + t.Errorf("expected protocol tcp, got %s", conn.Protocol) + } + if conn.Hostname != "example.com" { + t.Errorf("expected hostname example.com, got %s", conn.Hostname) + } + if conn.IP != "192.0.2.1" { + t.Errorf("expected IP 192.0.2.1, got %s", conn.IP) + } + if len(conn.Exchanges) != 1 { + t.Fatalf("expected 1 exchange, got %d", len(conn.Exchanges)) + } + + ex := conn.Exchanges[0] + if ex.URL != "https://example.com/pkg" { + t.Errorf("expected URL, got %s", ex.URL) + } + if ex.Method != "GET" { + t.Errorf("expected Method GET, got %s", ex.Method) + } + if ex.Referer != "https://ref.com" { + t.Errorf("expected Referer, got %s", ex.Referer) + } + if ex.StatusCode != 200 { + t.Errorf("expected StatusCode 200, got %d", ex.StatusCode) + } + if ex.BodyHash != "abcdef123456" { + t.Errorf("expected BodyHash, got %s", ex.BodyHash) + } +} + +func TestResolveAllRoutingAndDeduplication(t *testing.T) { + c := &Chain{byDomain: make(map[string]DomainResolver)} + dr1 := &mockDomainResolver{ + domains: []string{"example.com"}, + packages: []resolver.PackageInfo{ + {Name: "pkgA", Version: "1.0", PURL: "pkg:generic/pkgA@1.0"}, + {Name: "pkgA", Version: "1.0", PURL: "pkg:generic/pkgA@1.0"}, // Dupe + {Name: "pkgC", Version: "1.0", PURL: ""}, // No PURL, should be skipped + }, + } + dr2 := &mockDomainResolver{ + domains: []string{"other.com"}, + packages: []resolver.PackageInfo{ + {Name: "pkgB", Version: "2.0", PURL: "pkg:generic/pkgB@2.0"}, + }, + } + c.byDomain["example.com"] = dr1 + c.byDomain["other.com"] = dr2 + + conns := []NetworkConnection{ + {Hostname: "example.com"}, + {Hostname: "other.com"}, + {Hostname: "unhandled.com"}, + } + + pkgs := c.ResolveAll(conns) + + if len(pkgs) != 2 { + t.Fatalf("expected 2 packages after deduplication, got %d", len(pkgs)) + } + + foundA := false + foundB := false + for _, p := range pkgs { + if p.PURL == "pkg:generic/pkgA@1.0" { + foundA = true + } else if p.PURL == "pkg:generic/pkgB@2.0" { + foundB = true + } + } + + if !foundA || !foundB { + t.Errorf("expected both pkgA and pkgB to be resolved, got pkgs=%+v", pkgs) + } +} \ No newline at end of file diff --git a/pkg/resolver/network/python_test.go b/pkg/resolver/network/python_test.go new file mode 100644 index 0000000..5828979 --- /dev/null +++ b/pkg/resolver/network/python_test.go @@ -0,0 +1,96 @@ +package network + +import ( + "testing" +) + +func TestPythonNetworkResolver(t *testing.T) { + tests := []struct { + name string + url string + statusCode int + expectPkg bool + expectedName string + expectedVer string + }{ + { + name: "valid pypi api", + url: "https://pypi.org/pypi/requests/2.31.0/json", + statusCode: 200, + expectPkg: true, + expectedName: "requests", + expectedVer: "2.31.0", + }, + { + name: "valid wheel file", + url: "https://files.pythonhosted.org/packages/db/12/3456/requests-2.31.0-py3-none-any.whl", + statusCode: 200, + expectPkg: true, + expectedName: "requests", + expectedVer: "2.31.0", + }, + { + name: "valid source tarball", + url: "https://files.pythonhosted.org/packages/source/r/requests/requests-2.31.0.tar.gz", + statusCode: 200, + expectPkg: true, + expectedName: "requests", + expectedVer: "2.31.0", + }, + { + name: "failed status code", + url: "https://pypi.org/pypi/requests/2.31.0/json", + statusCode: 404, + expectPkg: false, + }, + { + name: "no status code (captured)", + url: "https://pypi.org/pypi/requests/2.31.0/json", + statusCode: 0, + expectPkg: true, + expectedName: "requests", + expectedVer: "2.31.0", + }, + { + name: "invalid url matching pattern", + url: "https://pypi.org/something/else/2.31.0/json", + statusCode: 200, + expectPkg: false, + }, + } + + resolver := NewPythonNetworkResolver() + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + conn := NetworkConnection{ + Hostname: "pypi.org", + IP: "151.101.1.1", + Exchanges: []NetworkExchange{ + { + URL: tc.url, + StatusCode: tc.statusCode, + }, + }, + } + + pkgs := resolver.Resolve(conn) + if tc.expectPkg { + if len(pkgs) != 1 { + t.Fatalf("expected 1 package, got %d", len(pkgs)) + } + pkg := pkgs[0] + if pkg.Name != tc.expectedName { + t.Errorf("expected name %s, got %s", tc.expectedName, pkg.Name) + } + if pkg.Version != tc.expectedVer { + t.Errorf("expected version %s, got %s", tc.expectedVer, pkg.Version) + } + } else { + if len(pkgs) != 0 { + t.Fatalf("expected 0 packages, got %d", len(pkgs)) + } + } + }) + } +} \ No newline at end of file