add relationships for go binary packages (#2912)

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
Alex Goodman 2024-05-30 11:37:17 -04:00 committed by GitHub
parent ac34808b9c
commit f4a69e6d35
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 628 additions and 154 deletions

View File

@ -4,16 +4,9 @@ Package golang provides a concrete Cataloger implementation relating to packages
package golang package golang
import ( import (
"context"
"fmt"
"regexp" "regexp"
"strings"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/mimetype" "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"
"github.com/anchore/syft/syft/pkg/cataloger/generic" "github.com/anchore/syft/syft/pkg/cataloger/generic"
) )
@ -30,102 +23,17 @@ func NewGoModuleFileCataloger(opts CatalogerConfig) pkg.Cataloger {
c := goModCataloger{ c := goModCataloger{
licenses: newGoLicenses(modFileCatalogerName, opts), licenses: newGoLicenses(modFileCatalogerName, opts),
} }
return &progressingCataloger{
cataloger: generic.NewCataloger(modFileCatalogerName). return generic.NewCataloger(modFileCatalogerName).
WithParserByGlobs(c.parseGoModFile, "**/go.mod"), WithParserByGlobs(c.parseGoModFile, "**/go.mod")
}
} }
// NewGoModuleBinaryCataloger returns a new cataloger object that searches within binaries built by the go compiler. // NewGoModuleBinaryCataloger returns a new cataloger object that searches within binaries built by the go compiler.
func NewGoModuleBinaryCataloger(opts CatalogerConfig) pkg.Cataloger { func NewGoModuleBinaryCataloger(opts CatalogerConfig) pkg.Cataloger {
return &progressingCataloger{ return generic.NewCataloger(binaryCatalogerName).
cataloger: generic.NewCataloger(binaryCatalogerName).
WithParserByMimeTypes( WithParserByMimeTypes(
newGoBinaryCataloger(opts).parseGoBinary, newGoBinaryCataloger(opts).parseGoBinary,
mimetype.ExecutableMIMETypeSet.List()..., mimetype.ExecutableMIMETypeSet.List()...,
), ).
} WithProcessors(stdlibProcessor)
}
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 +<metadata> 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 <version> and <candidate>
// 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)
} }

View File

@ -8,6 +8,92 @@ import (
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest" "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) { func Test_Mod_Cataloger_Globs(t *testing.T) {
tests := []struct { tests := []struct {
name string name string

View File

@ -69,17 +69,38 @@ func (c *goBinaryCataloger) parseGoBinary(_ context.Context, resolver file.Resol
mods := scanFile(unionReader, reader.RealPath) mods := scanFile(unionReader, reader.RealPath)
internal.CloseAndLogError(reader.ReadCloser, reader.RealPath) internal.CloseAndLogError(reader.ReadCloser, reader.RealPath)
var rels []artifact.Relationship
for _, mod := range mods { 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 var pkgs []pkg.Package
if mod == nil { if mod == nil {
return pkgs return nil, pkgs
} }
var empty debug.Module var empty debug.Module
@ -110,13 +131,12 @@ func (c *goBinaryCataloger) buildGoPkgInfo(resolver file.Resolver, location file
} }
if mod.Main == empty { if mod.Main == empty {
return pkgs return nil, pkgs
} }
main := c.makeGoMainPackage(resolver, mod, arch, location, reader) 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 { func (c *goBinaryCataloger) makeGoMainPackage(resolver file.Resolver, mod *extendedBuildInfo, arch string, location file.Location, reader io.ReadSeekCloser) pkg.Package {

View File

@ -964,7 +964,10 @@ func TestBuildGoPkgInfo(t *testing.T) {
c := newGoBinaryCataloger(DefaultCatalogerConfig()) c := newGoBinaryCataloger(DefaultCatalogerConfig())
reader, err := unionreader.GetUnionReader(io.NopCloser(strings.NewReader(test.binaryContent))) reader, err := unionreader.GetUnionReader(io.NopCloser(strings.NewReader(test.binaryContent)))
require.NoError(t, err) 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)) require.Len(t, pkgs, len(test.expected))
for i, p := range pkgs { for i, p := range pkgs {
pkgtest.AssertPackagesEqual(t, test.expected[i], p) pkgtest.AssertPackagesEqual(t, test.expected[i], p)

View File

@ -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 +<metadata> 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 <version> and <candidate>
// 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)
}

View File

@ -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)
}
}

View File

@ -0,0 +1 @@
/run-me

View File

@ -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"]

View File

@ -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
)

View File

@ -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=

View File

@ -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)
}
}

View File

@ -0,0 +1 @@
/run-me

View File

@ -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"]

View File

@ -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
)

View File

@ -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=

View File

@ -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)
}
}

View File

@ -2,8 +2,10 @@ package pkgtest
import ( import (
"context" "context"
"fmt"
"io" "io"
"os" "os"
"sort"
"strings" "strings"
"testing" "testing"
@ -40,6 +42,7 @@ type CatalogTester struct {
compareOptions []cmp.Option compareOptions []cmp.Option
locationComparer cmptest.LocationComparer locationComparer cmptest.LocationComparer
licenseComparer cmptest.LicenseComparer licenseComparer cmptest.LicenseComparer
packageStringer func(pkg.Package) string
customAssertions []func(t *testing.T, pkgs []pkg.Package, relationships []artifact.Relationship) customAssertions []func(t *testing.T, pkgs []pkg.Package, relationships []artifact.Relationship)
} }
@ -48,6 +51,7 @@ func NewCatalogTester() *CatalogTester {
wantErr: require.NoError, wantErr: require.NoError,
locationComparer: cmptest.DefaultLocationComparer, locationComparer: cmptest.DefaultLocationComparer,
licenseComparer: cmptest.DefaultLicenseComparer, licenseComparer: cmptest.DefaultLicenseComparer,
packageStringer: stringPackage,
ignoreUnfulfilledPathResponses: map[string][]string{ ignoreUnfulfilledPathResponses: map[string][]string{
"FilesByPath": { "FilesByPath": {
// most catalogers search for a linux release, which will not be fulfilled in testing // 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 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 { func (p *CatalogTester) ExpectsResolverPathResponses(locations []string) *CatalogTester {
p.expectedPathResponses = locations p.expectedPathResponses = locations
return p 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) 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)
}

View File

@ -4,13 +4,10 @@ import (
"context" "context"
"fmt" "fmt"
"path" "path"
"sort"
"testing" "testing"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest" "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) { t.Run(test.name, func(t *testing.T) {
pkgtest.NewCatalogTester(). pkgtest.NewCatalogTester().
WithImageResolver(t, test.fixture). WithImageResolver(t, test.fixture).
ExpectsAssertion(func(t *testing.T, pkgs []pkg.Package, relationships []artifact.Relationship) { WithPackageStringer(stringPackage).
diffRelationships(t, test.expectedRelationships, relationships, pkgs) ExpectsRelationshipStrings(test.expectedRelationships).
}).
TestCataloger(t, NewInstalledPackageCataloger()) TestCataloger(t, NewInstalledPackageCataloger())
}) })
} }
} }
func diffRelationships(t *testing.T, expected []string, actual []artifact.Relationship, pkgs []pkg.Package) { func stringPackage(p pkg.Package) string {
pkgsByID := make(map[artifact.ID]pkg.Package) locs := p.Locations.ToSlice()
for _, p := range pkgs { var loc string
pkgsByID[p.ID()] = p if len(locs) > 0 {
} // we want the location of the site-packages, not the metadata file
sort.Strings(expected) loc = path.Dir(path.Dir(p.Locations.ToSlice()[0].RealPath))
if d := cmp.Diff(expected, stringRelationships(actual, pkgsByID)); d != "" {
t.Errorf("unexpected relationships (-want, +got): %s", d)
}
} }
func stringRelationships(relationships []artifact.Relationship, nameLookup map[artifact.ID]pkg.Package) []string { return fmt.Sprintf("%s @ %s (%s)", p.Name, p.Version, loc)
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
} }