diff --git a/pkg/resolver/go.go b/pkg/resolver/go.go index e5aa2fc..1cd9e1b 100644 --- a/pkg/resolver/go.go +++ b/pkg/resolver/go.go @@ -111,10 +111,10 @@ func (r *GoResolver) isGoPath(p string) bool { } func (r *GoResolver) extractModuleVersion(p string) (string, string, bool) { - if matches := r.moduleDirRe.FindStringSubmatch(p); len(matches) == 3 { + if matches := r.moduleCacheRe.FindStringSubmatch(p); len(matches) == 4 { return matches[1], matches[2], true } - if matches := r.moduleCacheRe.FindStringSubmatch(p); len(matches) == 4 { + if matches := r.moduleDirRe.FindStringSubmatch(p); len(matches) == 3 { return matches[1], matches[2], true } return "", "", false diff --git a/pkg/resolver/go_test.go b/pkg/resolver/go_test.go new file mode 100644 index 0000000..cfa0064 --- /dev/null +++ b/pkg/resolver/go_test.go @@ -0,0 +1,131 @@ +package resolver + +import ( + "reflect" + "testing" +) + +func TestDecodeGoModulePath(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "regular path", + input: "github.com/stretchr/testify", + expected: "github.com/stretchr/testify", + }, + { + name: "uppercase encoded", + input: "github.com/!sirupsen/logrus", + expected: "github.com/Sirupsen/logrus", + }, + { + name: "multiple uppercase encoded", + input: "github.com/!azure/!a!z!u!r!e", + expected: "github.com/Azure/AZURE", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := DecodeGoModulePath(tt.input) + if result != tt.expected { + t.Errorf("DecodeGoModulePath(%q) = %q; want %q", tt.input, result, tt.expected) + } + }) + } +} + +func TestGoResolver_Resolve(t *testing.T) { + resolver := NewGoResolver() + + tests := []struct { + name string + files []FileInfo + expectPackages []PackageInfo + expectRemaining int + }{ + { + name: "resolves regular go module", + files: []FileInfo{ + {Path: "/home/user/go/pkg/mod/github.com/google/uuid@v1.3.0/uuid.go"}, + {Path: "/home/user/go/pkg/mod/github.com/google/uuid@v1.3.0/util.go"}, + }, + expectPackages: []PackageInfo{ + { + Name: "github.com/google/uuid", + Version: "v1.3.0", + Ecosystem: "golang", + PURL: "pkg:golang/github.com/google/uuid@v1.3.0", + FoundBy: "attestation:go", + }, + }, + expectRemaining: 0, + }, + { + name: "resolves encoded uppercase path", + files: []FileInfo{ + {Path: "/home/user/go/pkg/mod/github.com/!sirupsen/logrus@v1.8.1/logger.go"}, + }, + expectPackages: []PackageInfo{ + { + Name: "github.com/Sirupsen/logrus", // Decoded + Version: "v1.8.1", + Ecosystem: "golang", + PURL: "pkg:golang/github.com/Sirupsen/logrus@v1.8.1", + FoundBy: "attestation:go", + }, + }, + expectRemaining: 0, + }, + { + name: "resolves cache download info", + files: []FileInfo{ + {Path: "/home/user/go/pkg/mod/cache/download/golang.org/x/sys/@v/v0.0.0-20220114195835-da31bd327af9.info"}, + }, + expectPackages: []PackageInfo{ + { + Name: "golang.org/x/sys", + Version: "v0.0.0-20220114195835-da31bd327af9", + Ecosystem: "golang", + PURL: "pkg:golang/golang.org/x/sys@v0.0.0-20220114195835-da31bd327af9", + FoundBy: "attestation:go", + }, + }, + expectRemaining: 0, + }, + { + name: "ignores non-module paths", + files: []FileInfo{ + {Path: "/usr/local/go/src/fmt/print.go"}, + {Path: "cmd/main.go"}, + }, + expectPackages: nil, + expectRemaining: 2, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + packages, remaining := resolver.Resolve(tt.files) + + if len(packages) != len(tt.expectPackages) { + t.Fatalf("expected %d packages, got %d", len(tt.expectPackages), len(packages)) + } + + for i := range packages { + // Avoid checking hashes array allocation inside deepEqual + packages[i].Hashes = nil + if !reflect.DeepEqual(packages[i], tt.expectPackages[i]) { + t.Errorf("Package mismatch at index %d.\nGot: %+v\nWant: %+v", i, packages[i], tt.expectPackages[i]) + } + } + + if len(remaining) != tt.expectRemaining { + t.Errorf("expected %d remaining files, got %d", tt.expectRemaining, len(remaining)) + } + }) + } +} \ No newline at end of file diff --git a/pkg/resolver/python_test.go b/pkg/resolver/python_test.go new file mode 100644 index 0000000..298ee09 --- /dev/null +++ b/pkg/resolver/python_test.go @@ -0,0 +1,147 @@ +package resolver + +import ( + "reflect" + "testing" +) + +func TestPythonNormalizePackageName(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "already normalized", + input: "requests", + expected: "requests", + }, + { + name: "with uppercase", + input: "Flask", + expected: "flask", + }, + { + name: "with underscore", + input: "foo_bar", + expected: "foo-bar", + }, + { + name: "with multiple underscores", + input: "foo_bar_baz", + expected: "foo-bar-baz", + }, + { + name: "with multiple hyphens", + input: "foo--bar", + expected: "foo-bar", + }, + { + name: "complex mixed naming", + input: "Some_Weird--Package", + expected: "some-weird-package", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := NormalizePackageName(tt.input) + if result != tt.expected { + t.Errorf("NormalizePackageName(%q) = %q; want %q", tt.input, result, tt.expected) + } + }) + } +} + +func TestPythonResolver_Resolve(t *testing.T) { + resolver := NewPythonResolver() + + tests := []struct { + name string + files []FileInfo + expectPackages []PackageInfo + expectRemaining int + }{ + { + name: "resolves dist-info", + files: []FileInfo{ + {Path: "/usr/lib/python3/dist-packages/requests-2.28.1.dist-info/METADATA"}, + {Path: "/usr/lib/python3/dist-packages/requests/api.py"}, // Should be left as remaining by Resolve, filtered later + }, + expectPackages: []PackageInfo{ + { + Name: "requests", + Version: "2.28.1", + Ecosystem: "pypi", + PURL: "pkg:pypi/requests@2.28.1", + FoundBy: "attestation:python", + }, + }, + expectRemaining: 1, // The api.py file + }, + { + name: "resolves egg-info", + files: []FileInfo{ + {Path: "site-packages/PyYAML-6.0.egg-info/PKG-INFO"}, + }, + expectPackages: []PackageInfo{ + { + Name: "pyyaml", + Version: "6.0", + Ecosystem: "pypi", + PURL: "pkg:pypi/pyyaml@6.0", + FoundBy: "attestation:python", + }, + }, + expectRemaining: 0, + }, + { + name: "deduplicates packages", + files: []FileInfo{ + {Path: "site-packages/flask-2.0.1.dist-info/METADATA"}, + {Path: "site-packages/flask-2.0.1.dist-info/RECORD"}, + }, + expectPackages: []PackageInfo{ + { + Name: "flask", + Version: "2.0.1", + Ecosystem: "pypi", + PURL: "pkg:pypi/flask@2.0.1", + FoundBy: "attestation:python", + }, + }, + expectRemaining: 0, + }, + { + name: "ignores non-python paths", + files: []FileInfo{ + {Path: "/usr/bin/python3"}, + {Path: "/etc/passwd"}, + }, + expectPackages: nil, + expectRemaining: 2, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + packages, remaining := resolver.Resolve(tt.files) + + if len(packages) != len(tt.expectPackages) { + t.Fatalf("expected %d packages, got %d", len(tt.expectPackages), len(packages)) + } + + for i := range packages { + // Don't check hashes in this basic test + packages[i].Hashes = nil + if !reflect.DeepEqual(packages[i], tt.expectPackages[i]) { + t.Errorf("Package mismatch at index %d.\nGot: %+v\nWant: %+v", i, packages[i], tt.expectPackages[i]) + } + } + + if len(remaining) != tt.expectRemaining { + t.Errorf("expected %d remaining files, got %d", tt.expectRemaining, len(remaining)) + } + }) + } +}