diff --git a/syft/pkg/cataloger/golang/cataloger.go b/syft/pkg/cataloger/golang/cataloger.go index 985376d6c..6798b15a2 100644 --- a/syft/pkg/cataloger/golang/cataloger.go +++ b/syft/pkg/cataloger/golang/cataloger.go @@ -4,16 +4,9 @@ Package golang provides a concrete Cataloger implementation relating to packages package golang import ( - "context" - "fmt" "regexp" - "strings" - "github.com/anchore/syft/internal" "github.com/anchore/syft/internal/mimetype" - "github.com/anchore/syft/syft/artifact" - "github.com/anchore/syft/syft/cpe" - "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg/cataloger/generic" ) @@ -30,102 +23,17 @@ func NewGoModuleFileCataloger(opts CatalogerConfig) pkg.Cataloger { c := goModCataloger{ licenses: newGoLicenses(modFileCatalogerName, opts), } - return &progressingCataloger{ - cataloger: generic.NewCataloger(modFileCatalogerName). - WithParserByGlobs(c.parseGoModFile, "**/go.mod"), - } + + return generic.NewCataloger(modFileCatalogerName). + WithParserByGlobs(c.parseGoModFile, "**/go.mod") } // NewGoModuleBinaryCataloger returns a new cataloger object that searches within binaries built by the go compiler. func NewGoModuleBinaryCataloger(opts CatalogerConfig) pkg.Cataloger { - return &progressingCataloger{ - cataloger: generic.NewCataloger(binaryCatalogerName). - WithParserByMimeTypes( - newGoBinaryCataloger(opts).parseGoBinary, - mimetype.ExecutableMIMETypeSet.List()..., - ), - } -} - -type progressingCataloger struct { - cataloger *generic.Cataloger -} - -func (p *progressingCataloger) Name() string { - return p.cataloger.Name() -} - -func (p *progressingCataloger) Catalog(ctx context.Context, resolver file.Resolver) ([]pkg.Package, []artifact.Relationship, error) { - pkgs, relationships, err := p.cataloger.Catalog(ctx, resolver) - goCompilerPkgs := []pkg.Package{} - totalLocations := file.NewLocationSet() - for _, goPkg := range pkgs { - mValue, ok := goPkg.Metadata.(pkg.GolangBinaryBuildinfoEntry) - if !ok { - continue - } - // go binary packages should only contain a single location - for _, location := range goPkg.Locations.ToSlice() { - if !totalLocations.Contains(location) { - stdLibPkg := newGoStdLib(mValue.GoCompiledVersion, goPkg.Locations) - if stdLibPkg != nil { - goCompilerPkgs = append(goCompilerPkgs, *stdLibPkg) - totalLocations.Add(location) - } - } - } - } - pkgs = append(pkgs, goCompilerPkgs...) - return pkgs, relationships, err -} - -func newGoStdLib(version string, location file.LocationSet) *pkg.Package { - stdlibCpe, err := generateStdlibCpe(version) - if err != nil { - return nil - } - goCompilerPkg := &pkg.Package{ - Name: "stdlib", - Version: version, - PURL: packageURL("stdlib", strings.TrimPrefix(version, "go")), - CPEs: []cpe.CPE{stdlibCpe}, - Locations: location, - Licenses: pkg.NewLicenseSet(pkg.NewLicense("BSD-3-Clause")), - Language: pkg.Go, - Type: pkg.GoModulePkg, - Metadata: pkg.GolangBinaryBuildinfoEntry{ - GoCompiledVersion: version, - }, - } - goCompilerPkg.SetID() - - return goCompilerPkg -} - -func generateStdlibCpe(version string) (stdlibCpe cpe.CPE, err error) { - // GoCompiledVersion when pulled from a binary is prefixed by go - version = strings.TrimPrefix(version, "go") - - // we also need to trim starting from the first + to - // correctly extract potential rc candidate information for cpe generation - // ex: 2.0.0-rc.1+build.123 -> 2.0.0-rc.1; if no + is found then + is returned - after, _, found := strings.Cut("+", version) - if found { - version = after - } - - // extracting and - // https://regex101.com/r/985GsI/1 - captureGroups := internal.MatchNamedCaptureGroups(versionCandidateGroups, version) - vr, ok := captureGroups["version"] - if !ok || vr == "" { - return stdlibCpe, fmt.Errorf("could not match candidate version for: %s", version) - } - - cpeString := fmt.Sprintf("cpe:2.3:a:golang:go:%s:-:*:*:*:*:*:*", captureGroups["version"]) - if candidate, ok := captureGroups["candidate"]; ok && candidate != "" { - cpeString = fmt.Sprintf("cpe:2.3:a:golang:go:%s:%s:*:*:*:*:*:*", vr, candidate) - } - - return cpe.New(cpeString, cpe.GeneratedSource) + return generic.NewCataloger(binaryCatalogerName). + WithParserByMimeTypes( + newGoBinaryCataloger(opts).parseGoBinary, + mimetype.ExecutableMIMETypeSet.List()..., + ). + WithProcessors(stdlibProcessor) } diff --git a/syft/pkg/cataloger/golang/cataloger_test.go b/syft/pkg/cataloger/golang/cataloger_test.go index 240944539..ed8296565 100644 --- a/syft/pkg/cataloger/golang/cataloger_test.go +++ b/syft/pkg/cataloger/golang/cataloger_test.go @@ -8,6 +8,92 @@ import ( "github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest" ) +func Test_PackageCataloger_Binary(t *testing.T) { + + tests := []struct { + name string + fixture string + expectedPkgs []string + expectedRels []string + }{ + { + name: "simple module with dependencies", + fixture: "image-small", + expectedPkgs: []string{ + "anchore.io/not/real @ (devel) (/run-me)", + "github.com/andybalholm/brotli @ v1.0.1 (/run-me)", + "github.com/dsnet/compress @ v0.0.2-0.20210315054119-f66993602bf5 (/run-me)", + "github.com/golang/snappy @ v0.0.2 (/run-me)", + "github.com/klauspost/compress @ v1.11.4 (/run-me)", + "github.com/klauspost/pgzip @ v1.2.5 (/run-me)", + "github.com/mholt/archiver/v3 @ v3.5.1 (/run-me)", + "github.com/nwaples/rardecode @ v1.1.0 (/run-me)", + "github.com/pierrec/lz4/v4 @ v4.1.2 (/run-me)", + "github.com/ulikunitz/xz @ v0.5.9 (/run-me)", + "github.com/xi2/xz @ v0.0.0-20171230120015-48954b6210f8 (/run-me)", + "stdlib @ go1.22.3 (/run-me)", + }, + expectedRels: []string{ + "github.com/andybalholm/brotli @ v1.0.1 (/run-me) [dependency-of] anchore.io/not/real @ (devel) (/run-me)", + "github.com/dsnet/compress @ v0.0.2-0.20210315054119-f66993602bf5 (/run-me) [dependency-of] anchore.io/not/real @ (devel) (/run-me)", + "github.com/golang/snappy @ v0.0.2 (/run-me) [dependency-of] anchore.io/not/real @ (devel) (/run-me)", + "github.com/klauspost/compress @ v1.11.4 (/run-me) [dependency-of] anchore.io/not/real @ (devel) (/run-me)", + "github.com/klauspost/pgzip @ v1.2.5 (/run-me) [dependency-of] anchore.io/not/real @ (devel) (/run-me)", + "github.com/mholt/archiver/v3 @ v3.5.1 (/run-me) [dependency-of] anchore.io/not/real @ (devel) (/run-me)", + "github.com/nwaples/rardecode @ v1.1.0 (/run-me) [dependency-of] anchore.io/not/real @ (devel) (/run-me)", + "github.com/pierrec/lz4/v4 @ v4.1.2 (/run-me) [dependency-of] anchore.io/not/real @ (devel) (/run-me)", + "github.com/ulikunitz/xz @ v0.5.9 (/run-me) [dependency-of] anchore.io/not/real @ (devel) (/run-me)", + "github.com/xi2/xz @ v0.0.0-20171230120015-48954b6210f8 (/run-me) [dependency-of] anchore.io/not/real @ (devel) (/run-me)", + "stdlib @ go1.22.3 (/run-me) [dependency-of] anchore.io/not/real @ (devel) (/run-me)", + }, + }, + { + name: "partially built binary", + // the difference is the build flags used to build the binary... they will not reference the module directly + // see the dockerfile for details + fixture: "image-not-a-module", + expectedPkgs: []string{ + "command-line-arguments @ (devel) (/run-me)", // this is the difference! + "github.com/andybalholm/brotli @ v1.0.1 (/run-me)", + "github.com/dsnet/compress @ v0.0.2-0.20210315054119-f66993602bf5 (/run-me)", + "github.com/golang/snappy @ v0.0.2 (/run-me)", + "github.com/klauspost/compress @ v1.11.4 (/run-me)", + "github.com/klauspost/pgzip @ v1.2.5 (/run-me)", + "github.com/mholt/archiver/v3 @ v3.5.1 (/run-me)", + "github.com/nwaples/rardecode @ v1.1.0 (/run-me)", + "github.com/pierrec/lz4/v4 @ v4.1.2 (/run-me)", + "github.com/ulikunitz/xz @ v0.5.9 (/run-me)", + "github.com/xi2/xz @ v0.0.0-20171230120015-48954b6210f8 (/run-me)", + "stdlib @ go1.22.3 (/run-me)", + }, + expectedRels: []string{ + "github.com/andybalholm/brotli @ v1.0.1 (/run-me) [dependency-of] command-line-arguments @ (devel) (/run-me)", + "github.com/dsnet/compress @ v0.0.2-0.20210315054119-f66993602bf5 (/run-me) [dependency-of] command-line-arguments @ (devel) (/run-me)", + "github.com/golang/snappy @ v0.0.2 (/run-me) [dependency-of] command-line-arguments @ (devel) (/run-me)", + "github.com/klauspost/compress @ v1.11.4 (/run-me) [dependency-of] command-line-arguments @ (devel) (/run-me)", + "github.com/klauspost/pgzip @ v1.2.5 (/run-me) [dependency-of] command-line-arguments @ (devel) (/run-me)", + "github.com/mholt/archiver/v3 @ v3.5.1 (/run-me) [dependency-of] command-line-arguments @ (devel) (/run-me)", + "github.com/nwaples/rardecode @ v1.1.0 (/run-me) [dependency-of] command-line-arguments @ (devel) (/run-me)", + "github.com/pierrec/lz4/v4 @ v4.1.2 (/run-me) [dependency-of] command-line-arguments @ (devel) (/run-me)", + "github.com/ulikunitz/xz @ v0.5.9 (/run-me) [dependency-of] command-line-arguments @ (devel) (/run-me)", + "github.com/xi2/xz @ v0.0.0-20171230120015-48954b6210f8 (/run-me) [dependency-of] command-line-arguments @ (devel) (/run-me)", + "stdlib @ go1.22.3 (/run-me) [dependency-of] command-line-arguments @ (devel) (/run-me)", + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + pkgtest.NewCatalogTester(). + WithImageResolver(t, test.fixture). + ExpectsPackageStrings(test.expectedPkgs). + ExpectsRelationshipStrings(test.expectedRels). + TestCataloger(t, NewGoModuleBinaryCataloger(DefaultCatalogerConfig())) + }) + } + +} + func Test_Mod_Cataloger_Globs(t *testing.T) { tests := []struct { name string diff --git a/syft/pkg/cataloger/golang/parse_go_binary.go b/syft/pkg/cataloger/golang/parse_go_binary.go index a6940dcf6..f3ea97180 100644 --- a/syft/pkg/cataloger/golang/parse_go_binary.go +++ b/syft/pkg/cataloger/golang/parse_go_binary.go @@ -69,17 +69,38 @@ func (c *goBinaryCataloger) parseGoBinary(_ context.Context, resolver file.Resol mods := scanFile(unionReader, reader.RealPath) internal.CloseAndLogError(reader.ReadCloser, reader.RealPath) + var rels []artifact.Relationship for _, mod := range mods { - pkgs = append(pkgs, c.buildGoPkgInfo(resolver, reader.Location, mod, mod.arch, unionReader)...) + var depPkgs []pkg.Package + mainPkg, depPkgs := c.buildGoPkgInfo(resolver, reader.Location, mod, mod.arch, unionReader) + if mainPkg != nil { + rels = createModuleRelationships(*mainPkg, depPkgs) + pkgs = append(pkgs, *mainPkg) + } + pkgs = append(pkgs, depPkgs...) } - return pkgs, nil, nil + return pkgs, rels, nil } -func (c *goBinaryCataloger) buildGoPkgInfo(resolver file.Resolver, location file.Location, mod *extendedBuildInfo, arch string, reader io.ReadSeekCloser) []pkg.Package { +func createModuleRelationships(main pkg.Package, deps []pkg.Package) []artifact.Relationship { + var relationships []artifact.Relationship + + for _, dep := range deps { + relationships = append(relationships, artifact.Relationship{ + From: dep, + To: main, + Type: artifact.DependencyOfRelationship, + }) + } + + return relationships +} + +func (c *goBinaryCataloger) buildGoPkgInfo(resolver file.Resolver, location file.Location, mod *extendedBuildInfo, arch string, reader io.ReadSeekCloser) (*pkg.Package, []pkg.Package) { var pkgs []pkg.Package if mod == nil { - return pkgs + return nil, pkgs } var empty debug.Module @@ -110,13 +131,12 @@ func (c *goBinaryCataloger) buildGoPkgInfo(resolver file.Resolver, location file } if mod.Main == empty { - return pkgs + return nil, pkgs } main := c.makeGoMainPackage(resolver, mod, arch, location, reader) - pkgs = append(pkgs, main) - return pkgs + return &main, pkgs } func (c *goBinaryCataloger) makeGoMainPackage(resolver file.Resolver, mod *extendedBuildInfo, arch string, location file.Location, reader io.ReadSeekCloser) pkg.Package { diff --git a/syft/pkg/cataloger/golang/parse_go_binary_test.go b/syft/pkg/cataloger/golang/parse_go_binary_test.go index 67a61bdfc..29de14f35 100644 --- a/syft/pkg/cataloger/golang/parse_go_binary_test.go +++ b/syft/pkg/cataloger/golang/parse_go_binary_test.go @@ -964,7 +964,10 @@ func TestBuildGoPkgInfo(t *testing.T) { c := newGoBinaryCataloger(DefaultCatalogerConfig()) reader, err := unionreader.GetUnionReader(io.NopCloser(strings.NewReader(test.binaryContent))) require.NoError(t, err) - pkgs := c.buildGoPkgInfo(fileresolver.Empty{}, location, test.mod, test.mod.arch, reader) + mainPkg, pkgs := c.buildGoPkgInfo(fileresolver.Empty{}, location, test.mod, test.mod.arch, reader) + if mainPkg != nil { + pkgs = append(pkgs, *mainPkg) + } require.Len(t, pkgs, len(test.expected)) for i, p := range pkgs { pkgtest.AssertPackagesEqual(t, test.expected[i], p) diff --git a/syft/pkg/cataloger/golang/stdlib_package.go b/syft/pkg/cataloger/golang/stdlib_package.go new file mode 100644 index 000000000..19d37fe89 --- /dev/null +++ b/syft/pkg/cataloger/golang/stdlib_package.go @@ -0,0 +1,100 @@ +package golang + +import ( + "fmt" + "strings" + + "github.com/anchore/syft/internal" + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/cpe" + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/pkg" +) + +func stdlibProcessor(pkgs []pkg.Package, relationships []artifact.Relationship, err error) ([]pkg.Package, []artifact.Relationship, error) { + compilerPkgs, newRelationships := stdlibPackageAndRelationships(pkgs) + return append(pkgs, compilerPkgs...), append(relationships, newRelationships...), err +} + +func stdlibPackageAndRelationships(pkgs []pkg.Package) ([]pkg.Package, []artifact.Relationship) { + var goCompilerPkgs []pkg.Package + var relationships []artifact.Relationship + totalLocations := file.NewLocationSet() + for _, goPkg := range pkgs { + mValue, ok := goPkg.Metadata.(pkg.GolangBinaryBuildinfoEntry) + if !ok { + continue + } + + // go binary packages should only contain a single location + for _, location := range goPkg.Locations.ToSlice() { + if totalLocations.Contains(location) { + continue + } + + stdLibPkg := newGoStdLib(mValue.GoCompiledVersion, goPkg.Locations) + if stdLibPkg != nil { + goCompilerPkgs = append(goCompilerPkgs, *stdLibPkg) + totalLocations.Add(location) + } + + relationships = append(relationships, artifact.Relationship{ + From: *stdLibPkg, + To: goPkg, + Type: artifact.DependencyOfRelationship, + }) + } + } + return goCompilerPkgs, relationships +} + +func newGoStdLib(version string, location file.LocationSet) *pkg.Package { + stdlibCpe, err := generateStdlibCpe(version) + if err != nil { + return nil + } + goCompilerPkg := &pkg.Package{ + Name: "stdlib", + Version: version, + PURL: packageURL("stdlib", strings.TrimPrefix(version, "go")), + CPEs: []cpe.CPE{stdlibCpe}, + Locations: location, + Licenses: pkg.NewLicenseSet(pkg.NewLicense("BSD-3-Clause")), + Language: pkg.Go, + Type: pkg.GoModulePkg, + Metadata: pkg.GolangBinaryBuildinfoEntry{ + GoCompiledVersion: version, + }, + } + goCompilerPkg.SetID() + + return goCompilerPkg +} + +func generateStdlibCpe(version string) (stdlibCpe cpe.CPE, err error) { + // GoCompiledVersion when pulled from a binary is prefixed by go + version = strings.TrimPrefix(version, "go") + + // we also need to trim starting from the first + to + // correctly extract potential rc candidate information for cpe generation + // ex: 2.0.0-rc.1+build.123 -> 2.0.0-rc.1; if no + is found then + is returned + after, _, found := strings.Cut("+", version) + if found { + version = after + } + + // extracting and + // https://regex101.com/r/985GsI/1 + captureGroups := internal.MatchNamedCaptureGroups(versionCandidateGroups, version) + vr, ok := captureGroups["version"] + if !ok || vr == "" { + return stdlibCpe, fmt.Errorf("could not match candidate version for: %s", version) + } + + cpeString := fmt.Sprintf("cpe:2.3:a:golang:go:%s:-:*:*:*:*:*:*", captureGroups["version"]) + if candidate, ok := captureGroups["candidate"]; ok && candidate != "" { + cpeString = fmt.Sprintf("cpe:2.3:a:golang:go:%s:%s:*:*:*:*:*:*", vr, candidate) + } + + return cpe.New(cpeString, cpe.GeneratedSource) +} diff --git a/syft/pkg/cataloger/golang/stdlib_package_test.go b/syft/pkg/cataloger/golang/stdlib_package_test.go new file mode 100644 index 000000000..44653f82b --- /dev/null +++ b/syft/pkg/cataloger/golang/stdlib_package_test.go @@ -0,0 +1,137 @@ +package golang + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/anchore/syft/internal/cmptest" + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/cpe" + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/pkg" +) + +func Test_stdlibPackageAndRelationships(t *testing.T) { + + tests := []struct { + name string + pkgs []pkg.Package + wantPkgs int + wantRels int + }{ + { + name: "no packages", + }, + { + name: "ignore non-go-binary packages", + pkgs: []pkg.Package{ + { + Name: "not-go", + Version: "1.0.0", + Metadata: pkg.GolangModuleEntry{}, + }, + }, + wantPkgs: 0, + wantRels: 0, + }, + { + name: "with go-binary packages -- missing location", + pkgs: []pkg.Package{ + { + Name: "github.com/something/go", + Version: "1.0.0", + Metadata: pkg.GolangBinaryBuildinfoEntry{ + GoCompiledVersion: "go1.22.2", + MainModule: "github.com/something/go", + }, + }, + }, + wantPkgs: 0, + wantRels: 0, + }, + { + name: "with go-binary packages", + pkgs: []pkg.Package{ + { + Name: "github.com/something/go", + Version: "1.0.0", + Locations: file.NewLocationSet(file.NewLocation("/bin/my-app")), + Metadata: pkg.GolangBinaryBuildinfoEntry{ + GoCompiledVersion: "go1.22.2", + MainModule: "github.com/something/go", + }, + }, + }, + wantPkgs: 1, + wantRels: 1, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotPkgs, gotRels := stdlibPackageAndRelationships(tt.pkgs) + assert.Len(t, gotPkgs, tt.wantPkgs) + assert.Len(t, gotRels, tt.wantRels) + }) + } +} + +func Test_stdlibPackageAndRelationships_values(t *testing.T) { + loc := file.NewLocation("/bin/my-app") + locSet := file.NewLocationSet(loc) + p := pkg.Package{ + Name: "github.com/something/go", + Version: "1.0.0", + Locations: locSet, + Metadata: pkg.GolangBinaryBuildinfoEntry{ + GoCompiledVersion: "go1.22.2", + MainModule: "github.com/something/go", + }, + } + p.SetID() + + expectedPkg := pkg.Package{ + Name: "stdlib", + Version: "go1.22.2", + PURL: packageURL("stdlib", "1.22.2"), + Language: pkg.Go, + Type: pkg.GoModulePkg, + Licenses: pkg.NewLicenseSet(pkg.NewLicense("BSD-3-Clause")), + CPEs: []cpe.CPE{ + { + Attributes: cpe.MustAttributes("cpe:2.3:a:golang:go:1.22.2:-:*:*:*:*:*:*"), + Source: "syft-generated", + }, + }, + Locations: locSet, + Metadata: pkg.GolangBinaryBuildinfoEntry{ + GoCompiledVersion: "go1.22.2", + }, + } + + expectedPkg.SetID() + + expectedRel := artifact.Relationship{ + From: expectedPkg, + To: p, + Type: artifact.DependencyOfRelationship, + } + + gotPkgs, gotRels := stdlibPackageAndRelationships([]pkg.Package{p}) + require.Len(t, gotPkgs, 1) + + gotPkg := gotPkgs[0] + if d := cmp.Diff(expectedPkg, gotPkg, cmptest.DefaultCommonOptions()...); d != "" { + t.Errorf("unexpected package (-want +got): %s", d) + } + + require.Len(t, gotRels, 1) + gotRel := gotRels[0] + + if d := cmp.Diff(expectedRel, gotRel, cmptest.DefaultCommonOptions()...); d != "" { + t.Errorf("unexpected relationship (-want +got): %s", d) + } + +} diff --git a/syft/pkg/cataloger/golang/test-fixtures/image-not-a-module/.gitignore b/syft/pkg/cataloger/golang/test-fixtures/image-not-a-module/.gitignore new file mode 100644 index 000000000..0158cf7ba --- /dev/null +++ b/syft/pkg/cataloger/golang/test-fixtures/image-not-a-module/.gitignore @@ -0,0 +1 @@ +/run-me \ No newline at end of file diff --git a/syft/pkg/cataloger/golang/test-fixtures/image-not-a-module/Dockerfile b/syft/pkg/cataloger/golang/test-fixtures/image-not-a-module/Dockerfile new file mode 100644 index 000000000..38b26c721 --- /dev/null +++ b/syft/pkg/cataloger/golang/test-fixtures/image-not-a-module/Dockerfile @@ -0,0 +1,25 @@ +FROM --platform=linux/amd64 golang:1.22 AS builder + +RUN mkdir /app +WORKDIR /app + +COPY go.mod go.sum ./ +RUN go mod download +COPY main.go main.go + +# building with "." vs "main.go" is a difference in the buildinfo section +# specifically with main.go the buildinfo section will contain the following: +# +# path command-line-arguments +# +# instead of +# +# mod anchore.io/not/real +# +RUN CGO_ENABLED=0 GOOS=linux go build -o run-me main.go + + +FROM scratch + +COPY --from=builder /app/run-me /run-me +ENTRYPOINT ["/run-me"] \ No newline at end of file diff --git a/syft/pkg/cataloger/golang/test-fixtures/image-not-a-module/go.mod b/syft/pkg/cataloger/golang/test-fixtures/image-not-a-module/go.mod new file mode 100644 index 000000000..83808c4b9 --- /dev/null +++ b/syft/pkg/cataloger/golang/test-fixtures/image-not-a-module/go.mod @@ -0,0 +1,17 @@ +module anchore.io/not/real + +go 1.22.1 + +require github.com/mholt/archiver/v3 v3.5.1 + +require ( + github.com/andybalholm/brotli v1.0.1 // indirect + github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect + github.com/golang/snappy v0.0.2 // indirect + github.com/klauspost/compress v1.11.4 // indirect + github.com/klauspost/pgzip v1.2.5 // indirect + github.com/nwaples/rardecode v1.1.0 // indirect + github.com/pierrec/lz4/v4 v4.1.2 // indirect + github.com/ulikunitz/xz v0.5.9 // indirect + github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect +) diff --git a/syft/pkg/cataloger/golang/test-fixtures/image-not-a-module/go.sum b/syft/pkg/cataloger/golang/test-fixtures/image-not-a-module/go.sum new file mode 100644 index 000000000..f132d7176 --- /dev/null +++ b/syft/pkg/cataloger/golang/test-fixtures/image-not-a-module/go.sum @@ -0,0 +1,26 @@ +github.com/andybalholm/brotli v1.0.1 h1:KqhlKozYbRtJvsPrrEeXcO+N2l6NYT5A2QAFmSULpEc= +github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= +github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY= +github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s= +github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= +github.com/golang/snappy v0.0.2 h1:aeE13tS0IiQgFjYdoL8qN3K1N2bXXtI6Vi51/y7BpMw= +github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.11.4 h1:kz40R/YWls3iqT9zX9AHN3WoVsrAWVyui5sxuLqiXqU= +github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE= +github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= +github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo= +github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4= +github.com/nwaples/rardecode v1.1.0 h1:vSxaY8vQhOcVr4mm5e8XllHWTiM4JF507A0Katqw7MQ= +github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= +github.com/pierrec/lz4/v4 v4.1.2 h1:qvY3YFXRQE/XB8MlLzJH7mSzBs74eA2gg52YTk6jUPM= +github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/ulikunitz/xz v0.5.9 h1:RsKRIA2MO8x56wkkcd3LbtcE/uMszhb6DpRf+3uwa3I= +github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= +github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/syft/pkg/cataloger/golang/test-fixtures/image-not-a-module/main.go b/syft/pkg/cataloger/golang/test-fixtures/image-not-a-module/main.go new file mode 100644 index 000000000..ffff01e89 --- /dev/null +++ b/syft/pkg/cataloger/golang/test-fixtures/image-not-a-module/main.go @@ -0,0 +1,19 @@ +package main + +import "github.com/mholt/archiver/v3" + +func main() { + + z := archiver.Zip{ + MkdirAll: true, + SelectiveCompression: true, + ContinueOnError: false, + OverwriteExisting: false, + ImplicitTopLevelFolder: false, + } + + err := z.Archive([]string{"main.go"}, "test.zip") + if err != nil { + panic(err) + } +} diff --git a/syft/pkg/cataloger/golang/test-fixtures/image-small/.gitignore b/syft/pkg/cataloger/golang/test-fixtures/image-small/.gitignore new file mode 100644 index 000000000..0158cf7ba --- /dev/null +++ b/syft/pkg/cataloger/golang/test-fixtures/image-small/.gitignore @@ -0,0 +1 @@ +/run-me \ No newline at end of file diff --git a/syft/pkg/cataloger/golang/test-fixtures/image-small/Dockerfile b/syft/pkg/cataloger/golang/test-fixtures/image-small/Dockerfile new file mode 100644 index 000000000..cf78c974b --- /dev/null +++ b/syft/pkg/cataloger/golang/test-fixtures/image-small/Dockerfile @@ -0,0 +1,16 @@ +FROM --platform=linux/amd64 golang:1.22 AS builder + +RUN mkdir /app +WORKDIR /app + +COPY go.mod go.sum ./ +RUN go mod download +COPY main.go main.go + +RUN CGO_ENABLED=0 GOOS=linux go build -o run-me . + + +FROM scratch + +COPY --from=builder /app/run-me /run-me +ENTRYPOINT ["/run-me"] \ No newline at end of file diff --git a/syft/pkg/cataloger/golang/test-fixtures/image-small/go.mod b/syft/pkg/cataloger/golang/test-fixtures/image-small/go.mod new file mode 100644 index 000000000..83808c4b9 --- /dev/null +++ b/syft/pkg/cataloger/golang/test-fixtures/image-small/go.mod @@ -0,0 +1,17 @@ +module anchore.io/not/real + +go 1.22.1 + +require github.com/mholt/archiver/v3 v3.5.1 + +require ( + github.com/andybalholm/brotli v1.0.1 // indirect + github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect + github.com/golang/snappy v0.0.2 // indirect + github.com/klauspost/compress v1.11.4 // indirect + github.com/klauspost/pgzip v1.2.5 // indirect + github.com/nwaples/rardecode v1.1.0 // indirect + github.com/pierrec/lz4/v4 v4.1.2 // indirect + github.com/ulikunitz/xz v0.5.9 // indirect + github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect +) diff --git a/syft/pkg/cataloger/golang/test-fixtures/image-small/go.sum b/syft/pkg/cataloger/golang/test-fixtures/image-small/go.sum new file mode 100644 index 000000000..f132d7176 --- /dev/null +++ b/syft/pkg/cataloger/golang/test-fixtures/image-small/go.sum @@ -0,0 +1,26 @@ +github.com/andybalholm/brotli v1.0.1 h1:KqhlKozYbRtJvsPrrEeXcO+N2l6NYT5A2QAFmSULpEc= +github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= +github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY= +github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s= +github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= +github.com/golang/snappy v0.0.2 h1:aeE13tS0IiQgFjYdoL8qN3K1N2bXXtI6Vi51/y7BpMw= +github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.11.4 h1:kz40R/YWls3iqT9zX9AHN3WoVsrAWVyui5sxuLqiXqU= +github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE= +github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= +github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo= +github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4= +github.com/nwaples/rardecode v1.1.0 h1:vSxaY8vQhOcVr4mm5e8XllHWTiM4JF507A0Katqw7MQ= +github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= +github.com/pierrec/lz4/v4 v4.1.2 h1:qvY3YFXRQE/XB8MlLzJH7mSzBs74eA2gg52YTk6jUPM= +github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/ulikunitz/xz v0.5.9 h1:RsKRIA2MO8x56wkkcd3LbtcE/uMszhb6DpRf+3uwa3I= +github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= +github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/syft/pkg/cataloger/golang/test-fixtures/image-small/main.go b/syft/pkg/cataloger/golang/test-fixtures/image-small/main.go new file mode 100644 index 000000000..ffff01e89 --- /dev/null +++ b/syft/pkg/cataloger/golang/test-fixtures/image-small/main.go @@ -0,0 +1,19 @@ +package main + +import "github.com/mholt/archiver/v3" + +func main() { + + z := archiver.Zip{ + MkdirAll: true, + SelectiveCompression: true, + ContinueOnError: false, + OverwriteExisting: false, + ImplicitTopLevelFolder: false, + } + + err := z.Archive([]string{"main.go"}, "test.zip") + if err != nil { + panic(err) + } +} diff --git a/syft/pkg/cataloger/internal/pkgtest/test_generic_parser.go b/syft/pkg/cataloger/internal/pkgtest/test_generic_parser.go index 4384e3e21..a3cf11dec 100644 --- a/syft/pkg/cataloger/internal/pkgtest/test_generic_parser.go +++ b/syft/pkg/cataloger/internal/pkgtest/test_generic_parser.go @@ -2,8 +2,10 @@ package pkgtest import ( "context" + "fmt" "io" "os" + "sort" "strings" "testing" @@ -40,6 +42,7 @@ type CatalogTester struct { compareOptions []cmp.Option locationComparer cmptest.LocationComparer licenseComparer cmptest.LicenseComparer + packageStringer func(pkg.Package) string customAssertions []func(t *testing.T, pkgs []pkg.Package, relationships []artifact.Relationship) } @@ -48,6 +51,7 @@ func NewCatalogTester() *CatalogTester { wantErr: require.NoError, locationComparer: cmptest.DefaultLocationComparer, licenseComparer: cmptest.DefaultLicenseComparer, + packageStringer: stringPackage, ignoreUnfulfilledPathResponses: map[string][]string{ "FilesByPath": { // most catalogers search for a linux release, which will not be fulfilled in testing @@ -187,6 +191,23 @@ func (p *CatalogTester) Expects(pkgs []pkg.Package, relationships []artifact.Rel return p } +func (p *CatalogTester) WithPackageStringer(fn func(pkg.Package) string) *CatalogTester { + p.packageStringer = fn + return p +} + +func (p *CatalogTester) ExpectsPackageStrings(expected []string) *CatalogTester { + return p.ExpectsAssertion(func(t *testing.T, pkgs []pkg.Package, _ []artifact.Relationship) { + diffPackages(t, expected, pkgs, p.packageStringer) + }) +} + +func (p *CatalogTester) ExpectsRelationshipStrings(expected []string) *CatalogTester { + return p.ExpectsAssertion(func(t *testing.T, pkgs []pkg.Package, relationships []artifact.Relationship) { + diffRelationships(t, expected, relationships, pkgs, p.packageStringer) + }) +} + func (p *CatalogTester) ExpectsResolverPathResponses(locations []string) *CatalogTester { p.expectedPathResponses = locations return p @@ -347,3 +368,70 @@ func AssertPackagesEqual(t *testing.T, a, b pkg.Package) { t.Errorf("unexpected packages from parsing (-expected +actual)\n%s", diff) } } + +func diffPackages(t *testing.T, expected []string, actual []pkg.Package, pkgStringer func(pkg.Package) string) { + t.Helper() + sort.Strings(expected) + if d := cmp.Diff(expected, stringPackages(actual, pkgStringer)); d != "" { + t.Errorf("unexpected package strings (-want, +got): %s", d) + } +} + +func diffRelationships(t *testing.T, expected []string, actual []artifact.Relationship, pkgs []pkg.Package, pkgStringer func(pkg.Package) string) { + t.Helper() + pkgsByID := make(map[artifact.ID]pkg.Package) + for _, p := range pkgs { + pkgsByID[p.ID()] = p + } + sort.Strings(expected) + if d := cmp.Diff(expected, stringRelationships(actual, pkgsByID, pkgStringer)); d != "" { + t.Errorf("unexpected relationship strings (-want, +got): %s", d) + } +} + +func stringRelationships(relationships []artifact.Relationship, nameLookup map[artifact.ID]pkg.Package, pkgStringer func(pkg.Package) string) []string { + var result []string + for _, r := range relationships { + var fromName, toName string + { + fromPkg, ok := nameLookup[r.From.ID()] + if !ok { + fromName = string(r.From.ID()) + } else { + fromName = pkgStringer(fromPkg) + } + } + + { + toPkg, ok := nameLookup[r.To.ID()] + if !ok { + toName = string(r.To.ID()) + } else { + toName = pkgStringer(toPkg) + } + } + + result = append(result, fromName+" ["+string(r.Type)+"] "+toName) + } + sort.Strings(result) + return result +} + +func stringPackages(pkgs []pkg.Package, pkgStringer func(pkg.Package) string) []string { + var result []string + for _, p := range pkgs { + result = append(result, pkgStringer(p)) + } + sort.Strings(result) + return result +} + +func stringPackage(p pkg.Package) string { + locs := p.Locations.ToSlice() + var loc string + if len(locs) > 0 { + loc = p.Locations.ToSlice()[0].RealPath + } + + return fmt.Sprintf("%s @ %s (%s)", p.Name, p.Version, loc) +} diff --git a/syft/pkg/cataloger/python/cataloger_test.go b/syft/pkg/cataloger/python/cataloger_test.go index ed12fbb7f..df30307ca 100644 --- a/syft/pkg/cataloger/python/cataloger_test.go +++ b/syft/pkg/cataloger/python/cataloger_test.go @@ -4,13 +4,10 @@ import ( "context" "fmt" "path" - "sort" "testing" - "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/require" - "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest" @@ -482,52 +479,20 @@ func Test_PackageCataloger_SitePackageRelationships(t *testing.T) { t.Run(test.name, func(t *testing.T) { pkgtest.NewCatalogTester(). WithImageResolver(t, test.fixture). - ExpectsAssertion(func(t *testing.T, pkgs []pkg.Package, relationships []artifact.Relationship) { - diffRelationships(t, test.expectedRelationships, relationships, pkgs) - }). + WithPackageStringer(stringPackage). + ExpectsRelationshipStrings(test.expectedRelationships). TestCataloger(t, NewInstalledPackageCataloger()) }) } } -func diffRelationships(t *testing.T, expected []string, actual []artifact.Relationship, pkgs []pkg.Package) { - pkgsByID := make(map[artifact.ID]pkg.Package) - for _, p := range pkgs { - pkgsByID[p.ID()] = p - } - sort.Strings(expected) - if d := cmp.Diff(expected, stringRelationships(actual, pkgsByID)); d != "" { - t.Errorf("unexpected relationships (-want, +got): %s", d) +func stringPackage(p pkg.Package) string { + locs := p.Locations.ToSlice() + var loc string + if len(locs) > 0 { + // we want the location of the site-packages, not the metadata file + loc = path.Dir(path.Dir(p.Locations.ToSlice()[0].RealPath)) } -} - -func stringRelationships(relationships []artifact.Relationship, nameLookup map[artifact.ID]pkg.Package) []string { - var result []string - for _, r := range relationships { - var fromName, toName string - { - fromPkg, ok := nameLookup[r.From.ID()] - if !ok { - fromName = string(r.From.ID()) - } else { - loc := path.Dir(path.Dir(fromPkg.Locations.ToSlice()[0].RealPath)) - fromName = fmt.Sprintf("%s @ %s (%s)", fromPkg.Name, fromPkg.Version, loc) - } - } - - { - toPkg, ok := nameLookup[r.To.ID()] - if !ok { - toName = string(r.To.ID()) - } else { - loc := path.Dir(path.Dir(toPkg.Locations.ToSlice()[0].RealPath)) - toName = fmt.Sprintf("%s @ %s (%s)", toPkg.Name, toPkg.Version, loc) - } - } - - result = append(result, fromName+" ["+string(r.Type)+"] "+toName) - } - sort.Strings(result) - return result - + + return fmt.Sprintf("%s @ %s (%s)", p.Name, p.Version, loc) }