diff --git a/cmd/grype/cli/commands/root.go b/cmd/grype/cli/commands/root.go index b4b8dd8f2fb..b2e64ed6f0d 100644 --- a/cmd/grype/cli/commands/root.go +++ b/cmd/grype/cli/commands/root.go @@ -12,6 +12,7 @@ import ( "github.com/anchore/clio" "github.com/anchore/grype/cmd/grype/cli/options" "github.com/anchore/grype/grype" + "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/event" "github.com/anchore/grype/grype/event/parsers" "github.com/anchore/grype/grype/grypeerr" @@ -35,7 +36,6 @@ import ( "github.com/anchore/grype/internal/stringutil" "github.com/anchore/syft/syft" "github.com/anchore/syft/syft/cataloging" - "github.com/anchore/syft/syft/linux" syftPkg "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/sbom" ) @@ -261,15 +261,10 @@ func applyDistroHint(pkgs []pkg.Package, context *pkg.Context, opts *options.Gry if len(split) > 1 { v = split[1] } - context.Distro = &linux.Release{ - PrettyName: d, - Name: d, - ID: d, - IDLike: []string{ - d, - }, - Version: v, - VersionID: v, + var err error + context.Distro, err = distro.NewFromNameVersion(d, v) + if err != nil { + log.WithFields("distro", opts.Distro, "error", err).Warn("unable to parse distro") } } diff --git a/cmd/grype/cli/commands/root_test.go b/cmd/grype/cli/commands/root_test.go index bd1d27b80dc..e50d3408908 100644 --- a/cmd/grype/cli/commands/root_test.go +++ b/cmd/grype/cli/commands/root_test.go @@ -28,24 +28,24 @@ func Test_applyDistroHint(t *testing.T) { applyDistroHint([]pkg.Package{}, &ctx, &cfg) assert.NotNil(t, ctx.Distro) - assert.Equal(t, "alpine", ctx.Distro.Name) + assert.Equal(t, "alpine", ctx.Distro.Name()) assert.Equal(t, "3.10", ctx.Distro.Version) // does override an existing distro - cfg.Distro = "ubuntu:latest" + cfg.Distro = "ubuntu:24.04" applyDistroHint([]pkg.Package{}, &ctx, &cfg) assert.NotNil(t, ctx.Distro) - assert.Equal(t, "ubuntu", ctx.Distro.Name) - assert.Equal(t, "latest", ctx.Distro.Version) + assert.Equal(t, "ubuntu", ctx.Distro.Name()) + assert.Equal(t, "24.04", ctx.Distro.Version) // doesn't remove an existing distro when empty cfg.Distro = "" applyDistroHint([]pkg.Package{}, &ctx, &cfg) assert.NotNil(t, ctx.Distro) - assert.Equal(t, "ubuntu", ctx.Distro.Name) - assert.Equal(t, "latest", ctx.Distro.Version) + assert.Equal(t, "ubuntu", ctx.Distro.Name()) + assert.Equal(t, "24.04", ctx.Distro.Version) } func Test_getProviderConfig(t *testing.T) { diff --git a/go.mod b/go.mod index a69bb4c9efb..1ae7937ba62 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( github.com/anchore/go-version v1.2.2-0.20210903204242-51efa5b487c4 github.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115 github.com/anchore/stereoscope v0.1.4 - github.com/anchore/syft v1.23.2-0.20250509082920-0b78186a97a0 + github.com/anchore/syft v1.23.2-0.20250512173324-621d21eb04ca github.com/aquasecurity/go-pep440-version v0.0.1 github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de github.com/bmatcuk/doublestar/v2 v2.0.4 diff --git a/go.sum b/go.sum index 03fb3333d84..c050d466770 100644 --- a/go.sum +++ b/go.sum @@ -708,8 +708,8 @@ github.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115 h1:ZyRCmiE github.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115/go.mod h1:KoYIv7tdP5+CC9VGkeZV4/vGCKsY55VvoG+5dadg4YI= github.com/anchore/stereoscope v0.1.4 h1:e+iT9UdUzLBabWGe84hn5sTHDRioY+4IHsVzJXuJlek= github.com/anchore/stereoscope v0.1.4/go.mod h1:omWgXDEp/XfqCJlZXIByEo1c3ArZg/qTJ5LBKVLAIdw= -github.com/anchore/syft v1.23.2-0.20250509082920-0b78186a97a0 h1:Jnsz2S3H6PtGM2A11vqyD6Iyl479lY2be9lNWGLjkuA= -github.com/anchore/syft v1.23.2-0.20250509082920-0b78186a97a0/go.mod h1:rD7NI0LzopzDuVe5SW+NOUAcbaRe8TYwwourGwBYZZk= +github.com/anchore/syft v1.23.2-0.20250512173324-621d21eb04ca h1:GY1N+iXgj9rdibkf46vm+VQkASGGtW4NtDSNDAZSUuk= +github.com/anchore/syft v1.23.2-0.20250512173324-621d21eb04ca/go.mod h1:rD7NI0LzopzDuVe5SW+NOUAcbaRe8TYwwourGwBYZZk= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= diff --git a/grype/deprecated.go b/grype/deprecated.go index fc8ec72b29f..050974fef36 100644 --- a/grype/deprecated.go +++ b/grype/deprecated.go @@ -1,6 +1,7 @@ package grype import ( + "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/matcher" "github.com/anchore/grype/grype/pkg" @@ -8,7 +9,6 @@ import ( "github.com/anchore/grype/internal/log" "github.com/anchore/stereoscope/pkg/image" "github.com/anchore/syft/syft" - "github.com/anchore/syft/syft/linux" "github.com/anchore/syft/syft/source" ) @@ -33,7 +33,7 @@ func FindVulnerabilities(store vulnerability.Provider, userImageStr string, scop } // TODO: deprecated, will remove before v1.0.0 -func FindVulnerabilitiesForPackage(store vulnerability.Provider, d *linux.Release, matchers []match.Matcher, packages []pkg.Package) match.Matches { +func FindVulnerabilitiesForPackage(store vulnerability.Provider, d *distro.Distro, matchers []match.Matcher, packages []pkg.Package) match.Matches { exclusionProvider, _ := store.(match.ExclusionProvider) // TODO v5 is an exclusion provider, but v6 is not runner := VulnerabilityMatcher{ VulnerabilityProvider: store, diff --git a/grype/distro/distro.go b/grype/distro/distro.go index 13f95224366..50cf68855c8 100644 --- a/grype/distro/distro.go +++ b/grype/distro/distro.go @@ -6,6 +6,7 @@ import ( hashiVer "github.com/hashicorp/go-version" + "github.com/anchore/grype/internal/log" "github.com/anchore/syft/syft/linux" ) @@ -43,6 +44,13 @@ func New(t Type, version, label string, idLikes ...string) (*Distro, error) { } } + for i := range idLikes { + typ, ok := IDMapping[strings.TrimSpace(idLikes[i])] + if ok { + idLikes[i] = typ.String() + } + } + return &Distro{ Type: t, major: major, @@ -54,6 +62,35 @@ func New(t Type, version, label string, idLikes ...string) (*Distro, error) { }, nil } +// NewFromNameVersion creates a new Distro object derived from the provided name and version +func NewFromNameVersion(name, version string) (*Distro, error) { + var codename string + + // if there are no digits in the version, it is likely a codename + if !strings.ContainsAny(version, "0123456789") { + codename = version + version = "" + } + + typ := IDMapping[name] + if typ == "" { + typ = Type(name) + } + return New(typ, version, codename, string(typ)) +} + +// FromRelease attempts to get a distro from the linux release, only logging any errors +func FromRelease(linuxRelease *linux.Release) *Distro { + if linuxRelease == nil { + return nil + } + d, err := NewFromRelease(*linuxRelease) + if err != nil { + log.WithFields("error", err).Warn("unable to create distro from linux distribution") + } + return d +} + // NewFromRelease creates a new Distro object derived from a syft linux.Release object. func NewFromRelease(release linux.Release) (*Distro, error) { t := TypeFromRelease(release) @@ -105,6 +142,8 @@ func (d Distro) String() string { versionStr := "(version unknown)" if d.Version != "" { versionStr = d.Version + } else if d.Codename != "" { + versionStr = d.Codename } return fmt.Sprintf("%s %s", d.Type, versionStr) } diff --git a/grype/distro/distro_test.go b/grype/distro/distro_test.go index e399b7d2732..c9ca73e6972 100644 --- a/grype/distro/distro_test.go +++ b/grype/distro/distro_test.go @@ -29,10 +29,12 @@ func Test_NewDistroFromRelease(t *testing.T) { ID: "centos", VersionID: "8", Version: "7", + IDLike: []string{"rhel"}, }, expected: &Distro{ Type: CentOS, Version: "8", + IDLike: []string{"redhat"}, }, major: "8", minor: "", diff --git a/grype/internal/packagemetadata/discover_type_names.go b/grype/internal/packagemetadata/discover_type_names.go index 617e18d8585..a59748758c3 100644 --- a/grype/internal/packagemetadata/discover_type_names.go +++ b/grype/internal/packagemetadata/discover_type_names.go @@ -16,7 +16,7 @@ import ( var metadataExceptions = strset.New( "FileMetadata", - "PURLFileMetadata", + "SBOMFileMetadata", "PURLLiteralMetadata", "CPELiteralMetadata", ) diff --git a/grype/pkg/context.go b/grype/pkg/context.go index 5f46a6f9f9c..0279e24a401 100644 --- a/grype/pkg/context.go +++ b/grype/pkg/context.go @@ -1,11 +1,11 @@ package pkg import ( - "github.com/anchore/syft/syft/linux" + "github.com/anchore/grype/grype/distro" "github.com/anchore/syft/syft/source" ) type Context struct { Source *source.Description - Distro *linux.Release + Distro *distro.Distro } diff --git a/grype/pkg/package.go b/grype/pkg/package.go index 6a58eef2a96..abf6b244d30 100644 --- a/grype/pkg/package.go +++ b/grype/pkg/package.go @@ -3,15 +3,16 @@ package pkg import ( "fmt" "regexp" + "slices" "strings" "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/internal/log" "github.com/anchore/grype/internal/stringutil" + "github.com/anchore/packageurl-go" "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/file" - "github.com/anchore/syft/syft/linux" syftPkg "github.com/anchore/syft/syft/pkg" cpes "github.com/anchore/syft/syft/pkg/cataloger/common/cpe" ) @@ -44,7 +45,7 @@ type Package struct { Metadata interface{} // This is NOT 1-for-1 the syft metadata! Only the select data needed for vulnerability matching } -func New(p syftPkg.Package) Package { +func New(p syftPkg.Package, enhancers ...Enhancer) Package { metadata, upstreams := dataFromPkg(p) licenseObjs := p.Licenses.ToSlice() @@ -57,7 +58,7 @@ func New(p syftPkg.Package) Package { licenses = []string{} } - return Package{ + out := Package{ ID: ID(p.ID()), Name: p.Name, Version: p.Version, @@ -70,13 +71,25 @@ func New(p syftPkg.Package) Package { Upstreams: upstreams, Metadata: metadata, } + + if len(enhancers) > 0 { + purl, err := packageurl.FromString(p.PURL) + if err != nil { + log.WithFields("purl", purl, "error", err).Debug("unable to parse PURL") + } + for _, e := range enhancers { + e(&out, purl, p) + } + } + + return out } -func FromCollection(catalog *syftPkg.Collection, config SynthesisConfig) []Package { - return FromPackages(catalog.Sorted(), config) +func FromCollection(catalog *syftPkg.Collection, config SynthesisConfig, enhancers ...Enhancer) []Package { + return FromPackages(catalog.Sorted(), config, enhancers...) } -func FromPackages(syftpkgs []syftPkg.Package, config SynthesisConfig) []Package { +func FromPackages(syftpkgs []syftPkg.Package, config SynthesisConfig, enhancers ...Enhancer) []Package { var pkgs []Package for _, p := range syftpkgs { if len(p.CPEs) == 0 { @@ -87,7 +100,7 @@ func FromPackages(syftpkgs []syftPkg.Package, config SynthesisConfig) []Package log.Debugf("no CPEs for package: %s", p) } } - pkgs = append(pkgs, New(p)) + pkgs = append(pkgs, New(p, enhancers...)) } return pkgs @@ -98,7 +111,7 @@ func (p Package) String() string { return fmt.Sprintf("Pkg(type=%s, name=%s, version=%s, upstreams=%d)", p.Type, p.Name, p.Version, len(p.Upstreams)) } -func removePackagesByOverlap(catalog *syftPkg.Collection, relationships []artifact.Relationship, distro *linux.Release) *syftPkg.Collection { +func removePackagesByOverlap(catalog *syftPkg.Collection, relationships []artifact.Relationship, distro *distro.Distro) *syftPkg.Collection { byOverlap := map[artifact.ID]artifact.Relationship{} for _, r := range relationships { if r.Type == artifact.OwnershipByFileOverlapRelationship { @@ -151,23 +164,23 @@ func excludePackage(comprehensiveDistroFeed bool, p syftPkg.Package, parent syft // distroFeedIsComprehensive returns true if the distro feed // is comprehensive enough that we can drop packages owned by distro packages // before matching. -func distroFeedIsComprehensive(distro *linux.Release) bool { +func distroFeedIsComprehensive(dst *distro.Distro) bool { // TODO: this mechanism should be re-examined once https://github.com/anchore/grype/issues/1426 // is addressed - if distro == nil { + if dst == nil { return false } - if distro.ID == "amzn" { + if dst.Type == distro.AmazonLinux { // AmazonLinux shows "like rhel" but is not an rhel clone // and does not have an exhaustive vulnerability feed. return false } for _, d := range comprehensiveDistros { - if strings.EqualFold(d, distro.ID) { + if strings.EqualFold(string(d), dst.Name()) { return true } - for _, n := range distro.IDLike { - if strings.EqualFold(d, n) { + for _, n := range dst.IDLike { + if strings.EqualFold(string(d), n) { return true } } @@ -177,13 +190,13 @@ func distroFeedIsComprehensive(distro *linux.Release) bool { // computed by: // sqlite3 vulnerability.db 'select distinct namespace from vulnerability where fix_state in ("wont-fix", "not-fixed") order by namespace;' | cut -d ':' -f 1 | sort | uniq -// then removing 'github' and replacing 'redhat' with 'rhel' -var comprehensiveDistros = []string{ - "azurelinux", - "debian", - "mariner", - "rhel", - "ubuntu", +// then removing 'github' +var comprehensiveDistros = []distro.Type{ + distro.Azure, + distro.Debian, + distro.Mariner, + distro.RedHat, + distro.Ubuntu, } func isOSPackage(p syftPkg.Package) bool { @@ -195,7 +208,7 @@ func isOSPackage(p syftPkg.Package) bool { } } -func dataFromPkg(p syftPkg.Package) (interface{}, []UpstreamPackage) { +func dataFromPkg(p syftPkg.Package) (any, []UpstreamPackage) { var metadata interface{} var upstreams []UpstreamPackage @@ -222,6 +235,7 @@ func dataFromPkg(p syftPkg.Package) (interface{}, []UpstreamPackage) { case syftPkg.JavaVMInstallation: metadata = javaVMDataFromPkg(p) } + return metadata, upstreams } @@ -409,3 +423,93 @@ func ByID(id ID, pkgs []Package) *Package { } return nil } + +func parseUpstream(pkgName string, value string, pkgType syftPkg.Type) []UpstreamPackage { + if pkgType == syftPkg.RpmPkg { + return handleSourceRPM(pkgName, value) + } + return handleDefaultUpstream(pkgName, value) +} + +func handleDefaultUpstream(pkgName string, value string) []UpstreamPackage { + fields := strings.Split(value, "@") + switch len(fields) { + case 2: + if fields[0] == pkgName { + return nil + } + return []UpstreamPackage{ + { + Name: fields[0], + Version: fields[1], + }, + } + case 1: + if fields[0] == pkgName { + return nil + } + return []UpstreamPackage{ + { + Name: fields[0], + }, + } + } + return nil +} + +func setUpstreamsFromPURL(out *Package, purl packageurl.PackageURL, syftPkg syftPkg.Package) { + if len(out.Upstreams) == 0 { + out.Upstreams = upstreamsFromPURL(purl, syftPkg.Type) + } +} + +// upstreamsFromPURL reads any additional data Grype can use, which is ignored by Syft's PURL conversion +func upstreamsFromPURL(purl packageurl.PackageURL, pkgType syftPkg.Type) (upstreams []UpstreamPackage) { + for _, qualifier := range purl.Qualifiers { + if qualifier.Key == syftPkg.PURLQualifierUpstream { + for _, newUpstream := range parseUpstream(purl.Name, qualifier.Value, pkgType) { + if slices.Contains(upstreams, newUpstream) { + continue + } + upstreams = append(upstreams, newUpstream) + } + } + } + return upstreams +} + +func setDistroFromPURL(out *Package, purl packageurl.PackageURL, _ syftPkg.Package) { + if out.Distro == nil { + out.Distro = distroFromPURL(purl) + } +} + +// distroFromPURL reads distro data for Grype can use, which is ignored by Syft's PURL conversion +func distroFromPURL(purl packageurl.PackageURL) (d *distro.Distro) { + var distroName, distroVersion string + + for _, qualifier := range purl.Qualifiers { + if qualifier.Key == syftPkg.PURLQualifierDistro { + fields := strings.SplitN(qualifier.Value, "-", 2) + distroName = fields[0] + if len(fields) > 1 { + distroVersion = fields[1] + } + } + } + + if distroName != "" { + var err error + d, err = distro.NewFromNameVersion(distroName, distroVersion) + if err != nil { + log.WithFields("purl", purl, "error", err).Debug("unable to create distro from a release") + d = nil + } + } + + return d +} + +type Enhancer func(out *Package, purl packageurl.PackageURL, pkg syftPkg.Package) + +var purlEnhancers = []Enhancer{setUpstreamsFromPURL, setDistroFromPURL} diff --git a/grype/pkg/package_test.go b/grype/pkg/package_test.go index 1267b3eaa0f..bb64df534f9 100644 --- a/grype/pkg/package_test.go +++ b/grype/pkg/package_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/assert" + "github.com/anchore/grype/grype/distro" "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/file" @@ -983,35 +984,35 @@ func Test_RemovePackagesByOverlap(t *testing.T) { }, { name: "python bindings for system RPM install", - sbom: withDistro(catalogWithOverlaps( + sbom: withLinuxRelease(catalogWithOverlaps( []string{"rpm:python3-rpm@4.14.3-26.el8", "python:rpm@4.14.3"}, []string{"rpm:python3-rpm@4.14.3-26.el8 -> python:rpm@4.14.3"}), "rhel"), expectedPackages: []string{"rpm:python3-rpm@4.14.3-26.el8"}, }, { name: "amzn linux doesn't remove packages in this way", - sbom: withDistro(catalogWithOverlaps( + sbom: withLinuxRelease(catalogWithOverlaps( []string{"rpm:python3-rpm@4.14.3-26.el8", "python:rpm@4.14.3"}, []string{"rpm:python3-rpm@4.14.3-26.el8 -> python:rpm@4.14.3"}), "amzn"), expectedPackages: []string{"rpm:python3-rpm@4.14.3-26.el8", "python:rpm@4.14.3"}, }, { name: "remove overlapping package when parent version is prefix of child version", - sbom: withDistro(catalogWithOverlaps( + sbom: withLinuxRelease(catalogWithOverlaps( []string{"rpm:kernel-rt-core@5.14.0-503.40.1.el9_5", "linux-kernel:linux-kernel@5.14.0-503.40.1.el9_5.x86_64+rt"}, []string{"rpm:kernel-rt-core@5.14.0-503.40.1.el9_5 -> linux-kernel:linux-kernel@5.14.0-503.40.1.el9_5.x86_64+rt"}), "rhel"), expectedPackages: []string{"rpm:kernel-rt-core@5.14.0-503.40.1.el9_5"}, }, { name: "remove overlapping package when child version is prefix of parent version", - sbom: withDistro(catalogWithOverlaps( + sbom: withLinuxRelease(catalogWithOverlaps( []string{"rpm:kernel-rt-core@5.14.0-503.40.1.el9_5+rt", "linux-kernel:linux-kernel@5.14.0-503.40.1.el9_5"}, []string{"rpm:kernel-rt-core@5.14.0-503.40.1.el9_5+rt -> linux-kernel:linux-kernel@5.14.0-503.40.1.el9_5"}), "rhel"), expectedPackages: []string{"rpm:kernel-rt-core@5.14.0-503.40.1.el9_5+rt"}, }, { name: "do not remove overlapping package when versions are not similar", - sbom: withDistro(catalogWithOverlaps( + sbom: withLinuxRelease(catalogWithOverlaps( []string{"rpm:kernel@5.14.0-503.40.1.el9_5", "linux-kernel:linux-kernel@6.17"}, []string{"rpm:kernel@5.14.0-503.40.1.el9_5 -> linux-kernel:linux-kernel@6.17"}), "rhel"), expectedPackages: []string{"rpm:kernel@5.14.0-503.40.1.el9_5", "linux-kernel:linux-kernel@6.17"}, @@ -1019,7 +1020,8 @@ func Test_RemovePackagesByOverlap(t *testing.T) { } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - catalog := removePackagesByOverlap(test.sbom.Artifacts.Packages, test.sbom.Relationships, test.sbom.Artifacts.LinuxDistribution) + d := distro.FromRelease(test.sbom.Artifacts.LinuxDistribution) + catalog := removePackagesByOverlap(test.sbom.Artifacts.Packages, test.sbom.Relationships, d) pkgs := FromCollection(catalog, SynthesisConfig{}) var pkgNames []string for _, p := range pkgs { @@ -1100,7 +1102,7 @@ func catalogWithOverlaps(packages []string, overlaps []string) *sbom.SBOM { } } -func withDistro(s *sbom.SBOM, id string) *sbom.SBOM { +func withLinuxRelease(s *sbom.SBOM, id string) *sbom.SBOM { s.Artifacts.LinuxDistribution = &linux.Release{ ID: id, } diff --git a/grype/pkg/provider.go b/grype/pkg/provider.go index 165d490ad00..e06af83b23e 100644 --- a/grype/pkg/provider.go +++ b/grype/pkg/provider.go @@ -6,6 +6,7 @@ import ( "github.com/bmatcuk/doublestar/v2" + "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/internal/log" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/sbom" @@ -15,20 +16,17 @@ var errDoesNotProvide = fmt.Errorf("cannot provide packages from the given sourc // Provide a set of packages and context metadata describing where they were sourced from. func Provide(userInput string, config ProviderConfig) ([]Package, Context, *sbom.SBOM, error) { - packages, ctx, s, err := syftSBOMProvider(userInput, config) - if !errors.Is(err, errDoesNotProvide) { - if len(config.Exclusions) > 0 { - var exclusionsErr error - packages, exclusionsErr = filterPackageExclusions(packages, config.Exclusions) - if exclusionsErr != nil { - return nil, ctx, s, exclusionsErr - } - } - log.WithFields("input", userInput).Trace("interpreting input as an SBOM document") - return packages, ctx, s, err + packages, ctx, s, err := provide(userInput, config) + if err != nil { + return nil, Context{}, nil, err } + setContextDistro(packages, &ctx) + return packages, ctx, s, nil +} - packages, ctx, s, err = purlProvider(userInput) +// Provide a set of packages and context metadata describing where they were sourced from. +func provide(userInput string, config ProviderConfig) ([]Package, Context, *sbom.SBOM, error) { + packages, ctx, s, err := purlProvider(userInput, config) if !errors.Is(err, errDoesNotProvide) { log.WithFields("input", userInput).Trace("interpreting input as one or more PURLs") return packages, ctx, s, err @@ -40,6 +38,19 @@ func Provide(userInput string, config ProviderConfig) ([]Package, Context, *sbom return packages, ctx, s, err } + packages, ctx, s, err = syftSBOMProvider(userInput, config) + if !errors.Is(err, errDoesNotProvide) { + if len(config.Exclusions) > 0 { + var exclusionsErr error + packages, exclusionsErr = filterPackageExclusions(packages, config.Exclusions) + if exclusionsErr != nil { + return nil, ctx, s, exclusionsErr + } + } + log.WithFields("input", userInput).Trace("interpreting input as an SBOM document") + return packages, ctx, s, err + } + log.WithFields("input", userInput).Trace("passing input to syft for interpretation") return syftProvider(userInput, config) } @@ -92,3 +103,29 @@ func locationMatches(location file.Location, exclusion string) (bool, error) { } return matchesRealPath || matchesVirtualPath, nil } + +func setContextDistro(packages []Package, ctx *Context) { + if ctx.Distro != nil { + return + } + var singleDistro *distro.Distro + for _, p := range packages { + if p.Distro == nil { + continue + } + if singleDistro == nil { + singleDistro = p.Distro + continue + } + if singleDistro.Type != p.Distro.Type || + singleDistro.Version != p.Distro.Version || + singleDistro.Codename != p.Distro.Codename { + return + } + } + + // if there is one distro (with one version) represented, use that + if singleDistro != nil { + ctx.Distro = singleDistro + } +} diff --git a/grype/pkg/purl_provider.go b/grype/pkg/purl_provider.go index 8fa83e21a04..740f34960fe 100644 --- a/grype/pkg/purl_provider.go +++ b/grype/pkg/purl_provider.go @@ -1,21 +1,11 @@ package pkg import ( - "bufio" "fmt" "io" - "os" "strings" - "github.com/scylladb/go-set/strset" - - "github.com/anchore/go-homedir" - "github.com/anchore/grype/grype/distro" - "github.com/anchore/grype/internal/log" - "github.com/anchore/packageurl-go" - "github.com/anchore/syft/syft/cpe" - "github.com/anchore/syft/syft/linux" - "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/format" "github.com/anchore/syft/syft/sbom" "github.com/anchore/syft/syft/source" ) @@ -23,41 +13,28 @@ import ( const ( purlInputPrefix = "purl:" singlePurlInputPrefix = "pkg:" - cpesQualifierKey = "cpes" ) type PURLLiteralMetadata struct { PURL string } -type PURLFileMetadata struct { - Path string -} - -func purlProvider(userInput string) ([]Package, Context, *sbom.SBOM, error) { +func purlProvider(userInput string, config ProviderConfig) ([]Package, Context, *sbom.SBOM, error) { reader, ctx, err := getPurlReader(userInput) if err != nil { return nil, Context{}, nil, err } - return decodePurlsFromReader(reader, ctx) + s, _, _, err := format.Decode(reader) + if s == nil { + return nil, Context{}, nil, fmt.Errorf("unable to decode purl: %w", err) + } + + return FromCollection(s.Artifacts.Packages, config.SynthesisConfig, purlEnhancers...), ctx, s, nil } func getPurlReader(userInput string) (r io.Reader, ctx Context, err error) { - switch { - case strings.HasPrefix(userInput, purlInputPrefix): - path := strings.TrimPrefix(userInput, purlInputPrefix) - ctx.Source = &source.Description{ - Metadata: PURLFileMetadata{ - Path: path, - }, - } - file, err := openPurlFile(path) - if err != nil { - return nil, ctx, err - } - return file, ctx, nil - case strings.HasPrefix(userInput, singlePurlInputPrefix): + if strings.HasPrefix(userInput, singlePurlInputPrefix) { ctx.Source = &source.Description{ Metadata: PURLLiteralMetadata{ PURL: userInput, @@ -67,210 +44,3 @@ func getPurlReader(userInput string) (r io.Reader, ctx Context, err error) { } return nil, ctx, errDoesNotProvide } - -func openPurlFile(path string) (*os.File, error) { - expandedPath, err := homedir.Expand(path) - if err != nil { - return nil, fmt.Errorf("unable to open purls: %w", err) - } - - f, err := os.Open(expandedPath) - if err != nil { - return nil, fmt.Errorf("unable to open file %s: %w", expandedPath, err) - } - - return f, nil -} - -func createLinuxRelease(name string, version string) *linux.Release { - var codename string - - // if there are no digits in the version, it is likely a codename - if !strings.ContainsAny(version, "0123456789") { - codename = version - version = "" - } - - return &linux.Release{ - Name: name, - ID: name, - IDLike: []string{name}, - Version: version, - VersionCodename: codename, - } -} - -func decodePurlsFromReader(reader io.Reader, ctx Context) ([]Package, Context, *sbom.SBOM, error) { - scanner := bufio.NewScanner(reader) - var packages []Package - var syftPkgs []pkg.Package - - distros := make(map[string]*strset.Set) - for scanner.Scan() { - rawLine := scanner.Text() - p, syftPkg, distroName, distroVersion, err := purlToPackage(rawLine) - if err != nil { - return nil, Context{}, nil, err - } - if distroName != "" { - if _, ok := distros[distroName]; !ok { - distros[distroName] = strset.New() - } - distros[distroName].Add(distroVersion) - } - if p != nil { - packages = append(packages, *p) - } - if syftPkg != nil { - syftPkgs = append(syftPkgs, *syftPkg) - } - } - - if err := scanner.Err(); err != nil { - return nil, Context{}, nil, err - } - - s := &sbom.SBOM{ - Artifacts: sbom.Artifacts{ - Packages: pkg.NewCollection(syftPkgs...), - }, - } - // Do we have multiple purls - // purl litteral <-- - // purl file <-- FileMetadata - - // if there is one distro (with one version) represented, use that - if len(distros) == 1 { - for name, versions := range distros { - if versions.Size() == 1 { - version := versions.List()[0] - ctx.Distro = createLinuxRelease(name, version) - s.Artifacts.LinuxDistribution = createLinuxRelease(name, version) - } - } - } - - return packages, ctx, s, nil -} - -//nolint:funlen -func purlToPackage(rawLine string) (*Package, *pkg.Package, string, string, error) { - purl, err := packageurl.FromString(rawLine) - if err != nil { - return nil, nil, "", "", fmt.Errorf("unable to decode purl %s: %w", rawLine, err) - } - - var cpes []cpe.CPE - var upstreams []UpstreamPackage - var distroName, distroVersion string - epoch := "0" - - pkgType := pkg.TypeByName(purl.Type) - - for _, qualifier := range purl.Qualifiers { - switch qualifier.Key { - case cpesQualifierKey: - rawCpes := strings.Split(qualifier.Value, ",") - for _, rawCpe := range rawCpes { - c, err := cpe.New(rawCpe, "") - if err != nil { - return nil, nil, "", "", fmt.Errorf("unable to decode cpe %s in purl %s: %w", rawCpe, rawLine, err) - } - cpes = append(cpes, c) - } - case pkg.PURLQualifierEpoch: - epoch = qualifier.Value - case pkg.PURLQualifierUpstream: - upstreams = append(upstreams, parseUpstream(purl.Name, qualifier.Value, pkgType)...) - case pkg.PURLQualifierDistro: - name, version := parseDistroQualifier(qualifier.Value) - if name != "" && version != "" { - distroName = name - distroVersion = version - } - } - } - - version := purl.Version - if purl.Type == packageurl.TypeRPM && !strings.HasPrefix(purl.Version, fmt.Sprintf("%s:", epoch)) { - version = fmt.Sprintf("%s:%s", epoch, purl.Version) - } - - name := purl.Name - if pkgType == pkg.GoModulePkg && purl.Namespace != "" { - name = purl.Namespace + "/" + name - } - - syftPkg := pkg.Package{ - Name: name, - Version: version, - Type: pkgType, - CPEs: cpes, - PURL: purl.String(), - Language: pkg.LanguageByName(purl.Type), - } - - syftPkg.SetID() - - distribution, err := distro.NewFromRelease(*createLinuxRelease(distroName, distroVersion)) - if err != nil { - log.Trace("Unable to create Distro from a release: %s", err) - distribution = nil - } - - return &Package{ - ID: ID(purl.String()), - CPEs: cpes, - Name: name, - Version: version, - Type: pkgType, - Language: pkg.LanguageByName(purl.Type), - PURL: purl.String(), - Upstreams: upstreams, - Distro: distribution, - }, &syftPkg, distroName, distroVersion, nil -} - -func parseDistroQualifier(value string) (string, string) { - fields := strings.SplitN(value, "-", 2) - switch len(fields) { - case 2: - return fields[0], fields[1] - case 1: - return fields[0], "" - } - return "", "" -} - -func parseUpstream(pkgName string, value string, pkgType pkg.Type) []UpstreamPackage { - if pkgType == pkg.RpmPkg { - return handleSourceRPM(pkgName, value) - } - return handleDefaultUpstream(pkgName, value) -} - -func handleDefaultUpstream(pkgName string, value string) []UpstreamPackage { - fields := strings.Split(value, "@") - switch len(fields) { - case 2: - if fields[0] == pkgName { - return nil - } - return []UpstreamPackage{ - { - Name: fields[0], - Version: fields[1], - }, - } - case 1: - if fields[0] == pkgName { - return nil - } - return []UpstreamPackage{ - { - Name: fields[0], - }, - } - } - return nil -} diff --git a/grype/pkg/purl_provider_test.go b/grype/pkg/purl_provider_test.go index a2dd50583dc..a63d55735ba 100644 --- a/grype/pkg/purl_provider_test.go +++ b/grype/pkg/purl_provider_test.go @@ -8,10 +8,7 @@ import ( "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/distro" - "github.com/anchore/syft/syft/file" - "github.com/anchore/syft/syft/linux" "github.com/anchore/syft/syft/pkg" - "github.com/anchore/syft/syft/sbom" "github.com/anchore/syft/syft/source" ) @@ -21,7 +18,6 @@ func Test_PurlProvider(t *testing.T) { userInput string context Context pkgs []Package - sbom *sbom.SBOM wantErr require.ErrorAssertionFunc }{ { @@ -42,26 +38,15 @@ func Test_PurlProvider(t *testing.T) { PURL: "pkg:apk/curl@7.61.1", }, }, - sbom: &sbom.SBOM{ - Artifacts: sbom.Artifacts{ - Packages: pkg.NewCollection(pkg.Package{ - Name: "curl", - Version: "7.61.1", - Type: pkg.ApkPkg, - PURL: "pkg:apk/curl@7.61.1", - }), - }, - }, }, { name: "os with codename", userInput: "pkg:deb/debian/sysv-rc@2.88dsf-59?arch=all&distro=debian-jessie&upstream=sysvinit", context: Context{ - Distro: &linux.Release{ - Name: "debian", - ID: "debian", - IDLike: []string{"debian"}, - VersionCodename: "jessie", // important! + Distro: &distro.Distro{ + Type: "debian", + IDLike: []string{"debian"}, + Codename: "jessie", // important! }, Source: &source.Description{ Metadata: PURLLiteralMetadata{ @@ -83,22 +68,6 @@ func Test_PurlProvider(t *testing.T) { }, }, }, - sbom: &sbom.SBOM{ - Artifacts: sbom.Artifacts{ - Packages: pkg.NewCollection(pkg.Package{ - Name: "sysv-rc", - Version: "2.88dsf-59", - Type: pkg.DebPkg, - PURL: "pkg:deb/debian/sysv-rc@2.88dsf-59?arch=all&distro=debian-jessie&upstream=sysvinit", - }), - LinuxDistribution: &linux.Release{ - Name: "debian", - ID: "debian", - IDLike: []string{"debian"}, - VersionCodename: "jessie", - }, - }, - }, }, { name: "default upstream", @@ -123,16 +92,6 @@ func Test_PurlProvider(t *testing.T) { }, }, }, - sbom: &sbom.SBOM{ - Artifacts: sbom.Artifacts{ - Packages: pkg.NewCollection(pkg.Package{ - Name: "libcrypto3", - Version: "3.3.2", - Type: pkg.ApkPkg, - PURL: "pkg:apk/libcrypto3@3.3.2?upstream=openssl", - }), - }, - }, }, { name: "upstream with version", @@ -158,25 +117,14 @@ func Test_PurlProvider(t *testing.T) { }, }, }, - sbom: &sbom.SBOM{ - Artifacts: sbom.Artifacts{ - Packages: pkg.NewCollection(pkg.Package{ - Name: "libcrypto3", - Version: "3.3.2", - Type: pkg.ApkPkg, - PURL: "pkg:apk/libcrypto3@3.3.2?upstream=openssl%403.2.1", - }), - }, - }, }, { name: "upstream for source RPM", userInput: "pkg:rpm/redhat/systemd-x@239-82.el8_10.2?arch=aarch64&distro=rhel-8.10&upstream=systemd-239-82.el8_10.2.src.rpm", context: Context{ - Distro: &linux.Release{ - Name: "rhel", - ID: "rhel", - IDLike: []string{"rhel"}, + Distro: &distro.Distro{ + Type: "redhat", + IDLike: []string{"redhat"}, Version: "8.10", }, Source: &source.Description{ @@ -188,10 +136,10 @@ func Test_PurlProvider(t *testing.T) { pkgs: []Package{ { Name: "systemd-x", - Version: "0:239-82.el8_10.2", + Version: "239-82.el8_10.2", Type: pkg.RpmPkg, PURL: "pkg:rpm/redhat/systemd-x@239-82.el8_10.2?arch=aarch64&distro=rhel-8.10&upstream=systemd-239-82.el8_10.2.src.rpm", - Distro: &distro.Distro{Type: distro.RedHat, Version: "8.10", Codename: "", IDLike: []string{"rhel"}}, + Distro: &distro.Distro{Type: distro.RedHat, Version: "8.10", Codename: "", IDLike: []string{"redhat"}}, Upstreams: []UpstreamPackage{ { Name: "systemd", @@ -200,31 +148,14 @@ func Test_PurlProvider(t *testing.T) { }, }, }, - sbom: &sbom.SBOM{ - Artifacts: sbom.Artifacts{ - Packages: pkg.NewCollection(pkg.Package{ - Name: "systemd-x", - Version: "0:239-82.el8_10.2", - Type: pkg.RpmPkg, - PURL: "pkg:rpm/redhat/systemd-x@239-82.el8_10.2?arch=aarch64&distro=rhel-8.10&upstream=systemd-239-82.el8_10.2.src.rpm", - }), - LinuxDistribution: &linux.Release{ - Name: "rhel", - ID: "rhel", - IDLike: []string{"rhel"}, - Version: "8.10", - }, - }, - }, }, { name: "RPM with epoch", userInput: "pkg:rpm/redhat/dbus-common@1.12.8-26.el8?arch=noarch&distro=rhel-8.10&epoch=1&upstream=dbus-1.12.8-26.el8.src.rpm", context: Context{ - Distro: &linux.Release{ - Name: "rhel", - ID: "rhel", - IDLike: []string{"rhel"}, + Distro: &distro.Distro{ + Type: "redhat", + IDLike: []string{"redhat"}, Version: "8.10", }, Source: &source.Description{ @@ -239,7 +170,7 @@ func Test_PurlProvider(t *testing.T) { Version: "1:1.12.8-26.el8", Type: pkg.RpmPkg, PURL: "pkg:rpm/redhat/dbus-common@1.12.8-26.el8?arch=noarch&distro=rhel-8.10&epoch=1&upstream=dbus-1.12.8-26.el8.src.rpm", - Distro: &distro.Distro{Type: distro.RedHat, Version: "8.10", Codename: "", IDLike: []string{"rhel"}}, + Distro: &distro.Distro{Type: distro.RedHat, Version: "8.10", Codename: "", IDLike: []string{"redhat"}}, Upstreams: []UpstreamPackage{ { Name: "dbus", @@ -248,104 +179,13 @@ func Test_PurlProvider(t *testing.T) { }, }, }, - sbom: &sbom.SBOM{ - Artifacts: sbom.Artifacts{ - Packages: pkg.NewCollection(pkg.Package{ - Name: "dbus-common", - Version: "1:1.12.8-26.el8", - Type: pkg.RpmPkg, - PURL: "pkg:rpm/redhat/dbus-common@1.12.8-26.el8?arch=noarch&distro=rhel-8.10&epoch=1&upstream=dbus-1.12.8-26.el8.src.rpm", - }), - LinuxDistribution: &linux.Release{ - Name: "rhel", - ID: "rhel", - IDLike: []string{"rhel"}, - Version: "8.10", - }, - }, - }, - }, - { - name: "takes multiple purls", - userInput: "purl:test-fixtures/purl/valid-purl.txt", - context: Context{ - Distro: &linux.Release{ - Name: "debian", - ID: "debian", - IDLike: []string{"debian"}, - Version: "8", - }, - Source: &source.Description{ - Metadata: PURLFileMetadata{ - Path: "test-fixtures/purl/valid-purl.txt", - }, - }, - }, - pkgs: []Package{ - { - Name: "sysv-rc", - Version: "2.88dsf-59", - Type: pkg.DebPkg, - PURL: "pkg:deb/debian/sysv-rc@2.88dsf-59?arch=all&distro=debian-8&upstream=sysvinit", - Distro: &distro.Distro{Type: distro.Debian, Version: "8", Codename: "", IDLike: []string{"debian"}}, - Upstreams: []UpstreamPackage{ - { - Name: "sysvinit", - }, - }, - }, - { - Name: "ant", - Version: "1.10.8", - Type: pkg.JavaPkg, - PURL: "pkg:maven/org.apache.ant/ant@1.10.8", - }, - { - Name: "log4j-core", - Version: "2.14.1", - Type: pkg.JavaPkg, - PURL: "pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1", - }, - }, - sbom: &sbom.SBOM{ - Artifacts: sbom.Artifacts{ - Packages: pkg.NewCollection( - pkg.Package{ - Name: "sysv-rc", - Version: "2.88dsf-59", - Type: pkg.DebPkg, - PURL: "pkg:deb/debian/sysv-rc@2.88dsf-59?arch=all&distro=debian-8&upstream=sysvinit", - }, - pkg.Package{ - Name: "ant", - Version: "1.10.8", - Type: pkg.JavaPkg, - Language: pkg.Java, - PURL: "pkg:maven/org.apache.ant/ant@1.10.8", - }, - pkg.Package{ - Name: "log4j-core", - Version: "2.14.1", - Type: pkg.JavaPkg, - Language: pkg.Java, - PURL: "pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1", - }), - LinuxDistribution: &linux.Release{ - Name: "debian", - ID: "debian", - IDLike: []string{"debian"}, - Version: "8", - }, - }, - }, }, { name: "infer context when distro is present for single purl", userInput: "pkg:apk/curl@7.61.1?arch=aarch64&distro=alpine-3.20.3", context: Context{ - Distro: &linux.Release{ - Name: "alpine", - ID: "alpine", + Distro: &distro.Distro{ + Type: "alpine", IDLike: []string{"alpine"}, Version: "3.20.3", }, @@ -364,22 +204,6 @@ func Test_PurlProvider(t *testing.T) { Distro: &distro.Distro{Type: distro.Alpine, Version: "3.20.3", Codename: "", IDLike: []string{"alpine"}}, }, }, - sbom: &sbom.SBOM{ - Artifacts: sbom.Artifacts{ - Packages: pkg.NewCollection(pkg.Package{ - Name: "curl", - Version: "7.61.1", - Type: pkg.ApkPkg, - PURL: "pkg:apk/curl@7.61.1?arch=aarch64&distro=alpine-3.20.3", - }), - LinuxDistribution: &linux.Release{ - Name: "alpine", - ID: "alpine", - IDLike: []string{"alpine"}, - Version: "3.20.3", - }, - }, - }, }, { name: "include namespace in name when purl is type Golang", @@ -397,17 +221,6 @@ func Test_PurlProvider(t *testing.T) { PURL: "pkg:golang/k8s.io/ingress-nginx@v1.11.2", }, }, - sbom: &sbom.SBOM{ - Artifacts: sbom.Artifacts{ - Packages: pkg.NewCollection(pkg.Package{ - Name: "k8s.io/ingress-nginx", - Version: "v1.11.2", - Type: pkg.GoModulePkg, - Language: pkg.Go, - PURL: "pkg:golang/k8s.io/ingress-nginx@v1.11.2", - }), - }, - }, }, { name: "include complex namespace in name when purl is type Golang", @@ -425,17 +238,6 @@ func Test_PurlProvider(t *testing.T) { PURL: "pkg:golang/github.com/wazuh/wazuh@v4.5.0", }, }, - sbom: &sbom.SBOM{ - Artifacts: sbom.Artifacts{ - Packages: pkg.NewCollection(pkg.Package{ - Name: "github.com/wazuh/wazuh", - Version: "v4.5.0", - Type: pkg.GoModulePkg, - PURL: "pkg:golang/github.com/wazuh/wazuh@v4.5.0", - Language: pkg.Go, - }), - }, - }, }, { name: "do not include namespace when given blank input blank", @@ -453,149 +255,12 @@ func Test_PurlProvider(t *testing.T) { PURL: "pkg:golang/wazuh@v4.5.0", }, }, - sbom: &sbom.SBOM{ - Artifacts: sbom.Artifacts{ - Packages: pkg.NewCollection(pkg.Package{ - Name: "wazuh", - Version: "v4.5.0", - Type: pkg.GoModulePkg, - PURL: "pkg:golang/wazuh@v4.5.0", - Language: pkg.Go, - }), - }, - }, }, { - name: "infer context when distro is present for multiple similar purls", - userInput: "purl:test-fixtures/purl/homogeneous-os.txt", - context: Context{ - Distro: &linux.Release{ - Name: "alpine", - ID: "alpine", - IDLike: []string{"alpine"}, - Version: "3.20.3", - }, - Source: &source.Description{ - Metadata: PURLFileMetadata{ - Path: "test-fixtures/purl/homogeneous-os.txt", - }, - }, - }, - pkgs: []Package{ - { - Name: "openssl", - Version: "3.2.1", - Type: pkg.ApkPkg, - PURL: "pkg:apk/openssl@3.2.1?arch=aarch64&distro=alpine-3.20.3", - Distro: &distro.Distro{Type: distro.Alpine, Version: "3.20.3", Codename: "", IDLike: []string{"alpine"}}, - }, - { - Name: "curl", - Version: "7.61.1", - Type: pkg.ApkPkg, - PURL: "pkg:apk/curl@7.61.1?arch=aarch64&distro=alpine-3.20.3", - Distro: &distro.Distro{Type: distro.Alpine, Version: "3.20.3", Codename: "", IDLike: []string{"alpine"}}, - }, - }, - sbom: &sbom.SBOM{ - Artifacts: sbom.Artifacts{ - Packages: pkg.NewCollection(pkg.Package{ - Name: "openssl", - Version: "3.2.1", - Type: pkg.ApkPkg, - PURL: "pkg:apk/openssl@3.2.1?arch=aarch64&distro=alpine-3.20.3", - }, - pkg.Package{ - Name: "curl", - Version: "7.61.1", - Type: pkg.ApkPkg, - PURL: "pkg:apk/curl@7.61.1?arch=aarch64&distro=alpine-3.20.3", - }), - LinuxDistribution: &linux.Release{ - Name: "alpine", - ID: "alpine", - IDLike: []string{"alpine"}, - Version: "3.20.3", - }, - }, - }, - }, - { - name: "different distro info in purls does not infer context", - userInput: "purl:test-fixtures/purl/different-os.txt", - context: Context{ - // important: no distro info inferred - Source: &source.Description{ - Metadata: PURLFileMetadata{ - Path: "test-fixtures/purl/different-os.txt", - }, - }, - }, - pkgs: []Package{ - { - Name: "openssl", - Version: "3.2.1", - Type: pkg.ApkPkg, - PURL: "pkg:apk/openssl@3.2.1?arch=aarch64&distro=alpine-3.20.3", - Distro: &distro.Distro{Type: distro.Alpine, Version: "3.20.3", Codename: "", IDLike: []string{"alpine"}}, - }, - { - Name: "curl", - Version: "7.61.1", - Type: pkg.ApkPkg, - PURL: "pkg:apk/curl@7.61.1?arch=aarch64&distro=alpine-3.20.2", - Distro: &distro.Distro{Type: distro.Alpine, Version: "3.20.2", Codename: "", IDLike: []string{"alpine"}}, - }, - }, - sbom: &sbom.SBOM{ - Artifacts: sbom.Artifacts{ - Packages: pkg.NewCollection(pkg.Package{ - Name: "openssl", - Version: "3.2.1", - Type: pkg.ApkPkg, - PURL: "pkg:apk/openssl@3.2.1?arch=aarch64&distro=alpine-3.20.3", - }, - pkg.Package{ - Name: "curl", - Version: "7.61.1", - Type: pkg.ApkPkg, - PURL: "pkg:apk/curl@7.61.1?arch=aarch64&distro=alpine-3.20.2", - }), - }, - }, - }, - { - name: "fails on path with nonexistant file", - userInput: "purl:tttt/empty.txt", - wantErr: require.Error, - }, - { - name: "fails on invalid path", - userInput: "purl:~&&", - wantErr: require.Error, - }, - { - name: "allow empty purl file", - userInput: "purl:test-fixtures/purl/empty.json", - sbom: &sbom.SBOM{}, - context: Context{ - Source: &source.Description{ - Metadata: PURLFileMetadata{ - Path: "test-fixtures/purl/empty.json", - }, - }, - }, - }, - { - name: "fails on invalid purl in file", + name: "fails on purl list input", userInput: "purl:test-fixtures/purl/invalid-purl.txt", wantErr: require.Error, }, - { - name: "fails on invalid cpe in file", - userInput: "purl:test-fixtures/purl/invalid-cpe.txt", - wantErr: require.Error, - }, { name: "invalid prefix", userInput: "dir:test-fixtures/purl", @@ -603,23 +268,14 @@ func Test_PurlProvider(t *testing.T) { }, } - opts := []cmp.Option{ - cmpopts.IgnoreFields(Package{}, "ID", "Locations", "Licenses", "Metadata", "Language", "CPEs"), - cmpopts.IgnoreUnexported(distro.Distro{}), - } - - syftPkgOpts := []cmp.Option{ - cmpopts.IgnoreFields(pkg.Package{}, "id"), - cmpopts.IgnoreUnexported(pkg.Package{}, file.LocationSet{}, pkg.LicenseSet{}), - } - for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { if tc.wantErr == nil { tc.wantErr = require.NoError } - packages, ctx, gotSBOM, err := purlProvider(tc.userInput) + packages, ctx, _, err := purlProvider(tc.userInput, ProviderConfig{}) + setContextDistro(packages, &ctx) tc.wantErr(t, err) if err != nil { @@ -627,35 +283,20 @@ func Test_PurlProvider(t *testing.T) { return } - if d := cmp.Diff(tc.context, ctx, opts...); d != "" { + if d := cmp.Diff(tc.context, ctx, diffOpts...); d != "" { t.Errorf("unexpected context (-want +got):\n%s", d) } require.Len(t, packages, len(tc.pkgs)) for idx, expected := range tc.pkgs { - if d := cmp.Diff(expected, packages[idx], opts...); d != "" { + if d := cmp.Diff(expected, packages[idx], diffOpts...); d != "" { t.Errorf("unexpected context (-want +got):\n%s", d) } } - - gotSyftPkgs := gotSBOM.Artifacts.Packages.Sorted() - wantSyftPkgs := tc.sbom.Artifacts.Packages.Sorted() - require.Equal(t, len(gotSyftPkgs), len(wantSyftPkgs)) - for idx, wantPkg := range wantSyftPkgs { - if d := cmp.Diff(wantPkg, gotSyftPkgs[idx], syftPkgOpts...); d != "" { - t.Errorf("unexpected Syft Pkg (-want +got):\n%s", d) - } - } - - wantSyftDistro := tc.sbom.Artifacts.LinuxDistribution - gotDistro := gotSBOM.Artifacts.LinuxDistribution - if wantSyftDistro == nil { - require.Nil(t, gotDistro) - return - } - - if d := cmp.Diff(wantSyftDistro, gotDistro); d != "" { - t.Errorf("unexpected Syft Distro (-want +got):\n%s", d) - } }) } } + +var diffOpts = []cmp.Option{ + cmpopts.IgnoreFields(Package{}, "ID", "Locations", "Licenses", "Language", "CPEs"), + cmpopts.IgnoreUnexported(distro.Distro{}), +} diff --git a/grype/pkg/syft_provider.go b/grype/pkg/syft_provider.go index 8cfaec2de1f..9bd8a609cd1 100644 --- a/grype/pkg/syft_provider.go +++ b/grype/pkg/syft_provider.go @@ -5,6 +5,7 @@ import ( "errors" "github.com/anchore/go-collections" + "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/internal/log" "github.com/anchore/stereoscope" "github.com/anchore/stereoscope/pkg/image" @@ -19,14 +20,7 @@ func syftProvider(userInput string, config ProviderConfig) ([]Package, Context, if err != nil { return nil, Context{}, nil, err } - - defer func() { - if src != nil { - if err := src.Close(); err != nil { - log.Tracef("unable to close source: %+v", err) - } - } - }() + defer log.CloseAndLogError(src, "syft source") s, err := syft.CreateSBOM(context.Background(), src, config.SBOMOptions) if err != nil { @@ -37,14 +31,16 @@ func syftProvider(userInput string, config ProviderConfig) ([]Package, Context, return nil, Context{}, nil, errors.New("no SBOM provided") } - pkgCatalog := removePackagesByOverlap(s.Artifacts.Packages, s.Relationships, s.Artifacts.LinuxDistribution) - srcDescription := src.Describe() + d := distro.FromRelease(s.Artifacts.LinuxDistribution) + + pkgCatalog := removePackagesByOverlap(s.Artifacts.Packages, s.Relationships, d) + packages := FromCollection(pkgCatalog, config.SynthesisConfig) pkgCtx := Context{ Source: &srcDescription, - Distro: s.Artifacts.LinuxDistribution, + Distro: d, } return packages, pkgCtx, s, nil diff --git a/grype/pkg/syft_sbom_provider.go b/grype/pkg/syft_sbom_provider.go index 4c7b88ba7a1..f2ebf9bbd48 100644 --- a/grype/pkg/syft_sbom_provider.go +++ b/grype/pkg/syft_sbom_provider.go @@ -11,66 +11,70 @@ import ( "github.com/gabriel-vasile/mimetype" "github.com/anchore/go-homedir" + "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/internal" "github.com/anchore/grype/internal/log" "github.com/anchore/syft/syft/format" + "github.com/anchore/syft/syft/format/syftjson" "github.com/anchore/syft/syft/sbom" ) +type SBOMFileMetadata struct { + Path string +} + func syftSBOMProvider(userInput string, config ProviderConfig) ([]Package, Context, *sbom.SBOM, error) { - s, err := getSBOM(userInput) + s, fmtID, path, err := getSBOM(userInput) if err != nil { return nil, Context{}, nil, err } - catalog := removePackagesByOverlap(s.Artifacts.Packages, s.Relationships, s.Artifacts.LinuxDistribution) + src := s.Source + if src.Metadata == nil && path != "" { + src.Metadata = SBOMFileMetadata{ + Path: path, + } + } + + d := distro.FromRelease(s.Artifacts.LinuxDistribution) - return FromCollection(catalog, config.SynthesisConfig), Context{ - Source: &s.Source, - Distro: s.Artifacts.LinuxDistribution, - }, s, nil -} + catalog := removePackagesByOverlap(s.Artifacts.Packages, s.Relationships, d) -func newInputInfo(scheme, contentTye string) *inputInfo { - return &inputInfo{ - Scheme: scheme, - ContentType: contentTye, + var enhancers []Enhancer + if fmtID != syftjson.ID { + enhancers = purlEnhancers } -} -type inputInfo struct { - ContentType string - Scheme string + return FromCollection(catalog, config.SynthesisConfig, enhancers...), Context{ + Source: &src, + Distro: d, + }, s, nil } -func getSBOM(userInput string) (*sbom.SBOM, error) { - reader, err := getSBOMReader(userInput) +func getSBOM(userInput string) (*sbom.SBOM, sbom.FormatID, string, error) { + reader, path, err := getSBOMReader(userInput) if err != nil { - return nil, err + return nil, "", path, err } + s, fmtID, err := readSBOM(reader) + return s, fmtID, path, err +} + +func readSBOM(reader io.ReadSeeker) (*sbom.SBOM, sbom.FormatID, error) { s, fmtID, _, err := format.Decode(reader) if err != nil { - return nil, fmt.Errorf("unable to decode sbom: %w", err) + return nil, "", fmt.Errorf("unable to decode sbom: %w", err) } if fmtID == "" || s == nil { - return nil, errDoesNotProvide + return nil, "", errDoesNotProvide } - return s, nil + return s, fmtID, nil } -func getSBOMReader(userInput string) (r io.ReadSeeker, err error) { - r, _, err = extractReaderAndInfo(userInput) - if err != nil { - return nil, err - } - - return r, nil -} - -func extractReaderAndInfo(userInput string) (io.ReadSeeker, *inputInfo, error) { +func getSBOMReader(userInput string) (io.ReadSeeker, string, error) { switch { // the order of cases matter case userInput == "": @@ -78,44 +82,39 @@ func extractReaderAndInfo(userInput string) (io.ReadSeeker, *inputInfo, error) { // options from the CLI, otherwise we should not assume there is any valid input from stdin. r, err := stdinReader() if err != nil { - return nil, nil, err + return nil, "", err } return decodeStdin(r) + case explicitlySpecifyingPurlList(userInput): + filepath := strings.TrimPrefix(userInput, purlInputPrefix) + return openFile(filepath) + case explicitlySpecifyingSBOM(userInput): filepath := strings.TrimPrefix(userInput, "sbom:") - return parseSBOM("sbom", filepath) + return openFile(filepath) case isPossibleSBOM(userInput): - return parseSBOM("", userInput) + return openFile(userInput) default: - return nil, nil, errDoesNotProvide + return nil, "", errDoesNotProvide } } -func parseSBOM(scheme, path string) (io.ReadSeeker, *inputInfo, error) { - r, err := openFile(path) - if err != nil { - return nil, nil, err - } - info := newInputInfo(scheme, "sbom") - return r, info, nil -} - -func decodeStdin(r io.Reader) (io.ReadSeeker, *inputInfo, error) { +func decodeStdin(r io.Reader) (io.ReadSeeker, string, error) { b, err := io.ReadAll(r) if err != nil { - return nil, nil, fmt.Errorf("failed reading stdin: %w", err) + return nil, "", fmt.Errorf("failed reading stdin: %w", err) } reader := bytes.NewReader(b) _, err = reader.Seek(0, io.SeekStart) if err != nil { - return nil, nil, fmt.Errorf("failed to parse stdin: %w", err) + return nil, "", fmt.Errorf("failed to parse stdin: %w", err) } - return reader, newInputInfo("", "sbom"), nil + return reader, "", nil } func stdinReader() (io.Reader, error) { @@ -131,37 +130,26 @@ func stdinReader() (io.Reader, error) { return os.Stdin, nil } -func closeFile(f *os.File) { - if f == nil { - return - } - - err := f.Close() - if err != nil { - log.Warnf("failed to close file %s: %v", f.Name(), err) - } -} - -func openFile(path string) (*os.File, error) { +func openFile(path string) (io.ReadSeekCloser, string, error) { expandedPath, err := homedir.Expand(path) if err != nil { - return nil, fmt.Errorf("unable to open SBOM: %w", err) + return nil, path, fmt.Errorf("unable to open SBOM: %w", err) } f, err := os.Open(expandedPath) if err != nil { - return nil, fmt.Errorf("unable to open file %s: %w", expandedPath, err) + return nil, path, fmt.Errorf("unable to open file %s: %w", expandedPath, err) } - return f, nil + return f, path, nil } func isPossibleSBOM(userInput string) bool { - f, err := openFile(userInput) + f, path, err := openFile(userInput) if err != nil { return false } - defer closeFile(f) + defer log.CloseAndLogError(f, path) mType, err := mimetype.DetectReader(f) if err != nil { @@ -185,3 +173,7 @@ func isAncestorOfMimetype(mType *mimetype.MIME, expected string) bool { func explicitlySpecifyingSBOM(userInput string) bool { return strings.HasPrefix(userInput, "sbom:") } + +func explicitlySpecifyingPurlList(userInput string) bool { + return strings.HasPrefix(userInput, purlInputPrefix) +} diff --git a/grype/pkg/syft_sbom_provider_test.go b/grype/pkg/syft_sbom_provider_test.go index 2d2e48614d1..5d18d06570c 100644 --- a/grype/pkg/syft_sbom_provider_test.go +++ b/grype/pkg/syft_sbom_provider_test.go @@ -1,15 +1,19 @@ package pkg import ( + "slices" "strings" "testing" "github.com/go-test/deep" + "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/anchore/grype/grype/distro" "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/file" - "github.com/anchore/syft/syft/linux" + "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/source" ) @@ -219,8 +223,8 @@ func TestParseSyftJSON(t *testing.T) { }, }, }, - Distro: &linux.Release{ - Name: "alpine", + Distro: &distro.Distro{ + Type: "alpine", Version: "3.12.0", }, }, @@ -337,9 +341,184 @@ var springImageTestCase = struct { RepoDigests: []string{"springio/gs-spring-boot-docker@sha256:39c2ffc784f5f34862e22c1f2ccdbcb62430736114c13f60111eabdb79decb08"}, }, }, - Distro: &linux.Release{ - Name: "debian", + Distro: &distro.Distro{ + Type: "debian", Version: "9", }, }, } + +func Test_PurlList(t *testing.T) { + tests := []struct { + name string + userInput string + context Context + pkgs []Package + wantErr require.ErrorAssertionFunc + }{ + { + name: "takes multiple purls", + userInput: "purl:test-fixtures/purl/valid-purl.txt", + context: Context{ + Distro: &distro.Distro{ + Type: "debian", + IDLike: []string{"debian"}, + Version: "8", + }, + Source: &source.Description{ + Metadata: SBOMFileMetadata{ + Path: "test-fixtures/purl/valid-purl.txt", + }, + }, + }, + pkgs: []Package{ + { + Name: "ant", + Version: "1.10.8", + Type: pkg.JavaPkg, + PURL: "pkg:maven/org.apache.ant/ant@1.10.8", + Metadata: JavaMetadata{ + PomArtifactID: "ant", + PomGroupID: "org.apache.ant", + }, + }, + { + Name: "log4j-core", + Version: "2.14.1", + Type: pkg.JavaPkg, + PURL: "pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1", + Metadata: JavaMetadata{ + PomArtifactID: "log4j-core", + PomGroupID: "org.apache.logging.log4j", + }, + }, + { + Name: "sysv-rc", + Version: "2.88dsf-59", + Type: pkg.DebPkg, + PURL: "pkg:deb/debian/sysv-rc@2.88dsf-59?arch=all&distro=debian-8&upstream=sysvinit", + Distro: &distro.Distro{Type: distro.Debian, Version: "8", Codename: "", IDLike: []string{"debian"}}, + Upstreams: []UpstreamPackage{ + { + Name: "sysvinit", + }, + }, + }, + }, + }, + { + name: "infer context when distro is present for multiple similar purls", + userInput: "purl:test-fixtures/purl/homogeneous-os.txt", + context: Context{ + Distro: &distro.Distro{ + Type: "alpine", + IDLike: []string{"alpine"}, + Version: "3.20.3", + }, + Source: &source.Description{ + Metadata: SBOMFileMetadata{ + Path: "test-fixtures/purl/homogeneous-os.txt", + }, + }, + }, + pkgs: []Package{ + { + Name: "openssl", + Version: "3.2.1", + Type: pkg.ApkPkg, + PURL: "pkg:apk/openssl@3.2.1?arch=aarch64&distro=alpine-3.20.3", + Distro: &distro.Distro{Type: distro.Alpine, Version: "3.20.3", Codename: "", IDLike: []string{"alpine"}}, + }, + { + Name: "curl", + Version: "7.61.1", + Type: pkg.ApkPkg, + PURL: "pkg:apk/curl@7.61.1?arch=aarch64&distro=alpine-3.20.3", + Distro: &distro.Distro{Type: distro.Alpine, Version: "3.20.3", Codename: "", IDLike: []string{"alpine"}}, + }, + }, + }, + { + name: "different distro info in purls does not infer context", + userInput: "purl:test-fixtures/purl/different-os.txt", + context: Context{ + // important: no distro info inferred + Source: &source.Description{ + Metadata: SBOMFileMetadata{ + Path: "test-fixtures/purl/different-os.txt", + }, + }, + }, + pkgs: []Package{ + { + Name: "openssl", + Version: "3.2.1", + Type: pkg.ApkPkg, + PURL: "pkg:apk/openssl@3.2.1?arch=aarch64&distro=alpine-3.20.3", + Distro: &distro.Distro{Type: distro.Alpine, Version: "3.20.3", Codename: "", IDLike: []string{"alpine"}}, + }, + { + Name: "curl", + Version: "7.61.1", + Type: pkg.ApkPkg, + PURL: "pkg:apk/curl@7.61.1?arch=aarch64&distro=alpine-3.20.2", + Distro: &distro.Distro{Type: distro.Alpine, Version: "3.20.2", Codename: "", IDLike: []string{"alpine"}}, + }, + }, + }, + { + name: "fails on path with nonexistant file", + userInput: "purl:tttt/empty.txt", + wantErr: require.Error, + }, + { + name: "fails on invalid path", + userInput: "purl:~&&", + wantErr: require.Error, + }, + { + name: "fails for empty purl file", + userInput: "purl:test-fixtures/purl/empty.json", + wantErr: require.Error, + }, + { + name: "fails on invalid purl in file", + userInput: "purl:test-fixtures/purl/invalid-purl.txt", + wantErr: require.Error, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if tc.wantErr == nil { + tc.wantErr = require.NoError + } + + packages, ctx, _, err := Provide(tc.userInput, ProviderConfig{}) + + tc.wantErr(t, err) + if err != nil { + require.Nil(t, packages) + return + } + + if d := cmp.Diff(tc.context, ctx, diffOpts...); d != "" { + t.Errorf("unexpected context (-want +got):\n%s", d) + } + require.Len(t, packages, len(tc.pkgs)) + + slices.SortFunc(packages, func(a, b Package) int { + return strings.Compare(a.Name, b.Name) + }) + slices.SortFunc(tc.pkgs, func(a, b Package) int { + return strings.Compare(a.Name, b.Name) + }) + + for idx, expected := range tc.pkgs { + if d := cmp.Diff(expected, packages[idx], diffOpts...); d != "" { + t.Errorf("unexpected context (-want +got):\n%s", d) + } + } + }) + } +} diff --git a/grype/presenter/internal/test_helpers.go b/grype/presenter/internal/test_helpers.go index aab61adc688..aa2b528abb8 100644 --- a/grype/presenter/internal/test_helpers.go +++ b/grype/presenter/internal/test_helpers.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/require" "github.com/anchore/clio" + "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/presenter/models" @@ -15,7 +16,6 @@ import ( "github.com/anchore/stereoscope/pkg/image" "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/file" - "github.com/anchore/syft/syft/linux" syftPkg "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/sbom" syftSource "github.com/anchore/syft/syft/source" @@ -506,8 +506,8 @@ func generateContext(t *testing.T, scheme SyftSource) pkg.Context { return pkg.Context{ Source: &desc, - Distro: &linux.Release{ - Name: "centos", + Distro: &distro.Distro{ + Type: "centos", IDLike: []string{ "centos", }, diff --git a/grype/presenter/json/presenter_test.go b/grype/presenter/json/presenter_test.go index 3910b55ce9e..dc2929ae518 100644 --- a/grype/presenter/json/presenter_test.go +++ b/grype/presenter/json/presenter_test.go @@ -12,11 +12,11 @@ import ( "github.com/anchore/clio" "github.com/anchore/go-testutils" + "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/presenter/internal" "github.com/anchore/grype/grype/presenter/models" - "github.com/anchore/syft/syft/linux" "github.com/anchore/syft/syft/source" ) @@ -85,8 +85,8 @@ func TestEmptyJsonPresenter(t *testing.T) { ctx := pkg.Context{ Source: &source.Description{}, - Distro: &linux.Release{ - ID: "centos", + Distro: &distro.Distro{ + Type: "centos", IDLike: []string{"rhel"}, Version: "8.0", }, diff --git a/grype/presenter/models/distribution.go b/grype/presenter/models/distribution.go index e105a8dac79..a8b9d792afa 100644 --- a/grype/presenter/models/distribution.go +++ b/grype/presenter/models/distribution.go @@ -2,8 +2,6 @@ package models import ( "github.com/anchore/grype/grype/distro" - "github.com/anchore/grype/internal/log" - "github.com/anchore/syft/syft/linux" ) // distribution provides information about a detected Linux distribution. @@ -14,24 +12,11 @@ type distribution struct { } // newDistribution creates a struct with the Linux distribution to be represented in JSON. -func newDistribution(r *linux.Release) distribution { - if r == nil { +func newDistribution(d *distro.Distro) distribution { + if d == nil { return distribution{} } - // attempt to use the strong distro type (like the matchers do) - d, err := distro.NewFromRelease(*r) - if err != nil { - log.Warnf("unable to determine linux distribution: %+v", err) - - // as a fallback use the raw release information - return distribution{ - Name: r.ID, - Version: r.VersionID, - IDLike: cleanIDLike(r.IDLike), - } - } - return distribution{ Name: d.Name(), Version: d.Version, diff --git a/grype/presenter/models/document_test.go b/grype/presenter/models/document_test.go index d81f0dc144f..0cb95916935 100644 --- a/grype/presenter/models/document_test.go +++ b/grype/presenter/models/document_test.go @@ -7,10 +7,10 @@ import ( "github.com/stretchr/testify/assert" "github.com/anchore/clio" + "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/vulnerability" - "github.com/anchore/syft/syft/linux" syftPkg "github.com/anchore/syft/syft/pkg" syftSource "github.com/anchore/syft/syft/source" ) @@ -75,8 +75,8 @@ func TestPackagesAreSorted(t *testing.T) { Source: &syftSource.Description{ Metadata: syftSource.DirectoryMetadata{}, }, - Distro: &linux.Release{ - ID: "centos", + Distro: &distro.Distro{ + Type: "centos", IDLike: []string{"rhel"}, Version: "8.0", }, @@ -136,8 +136,8 @@ func TestFixSuggestedVersion(t *testing.T) { Source: &syftSource.Description{ Metadata: syftSource.DirectoryMetadata{}, }, - Distro: &linux.Release{ - ID: "centos", + Distro: &distro.Distro{ + Type: "centos", IDLike: []string{"rhel"}, Version: "8.0", }, diff --git a/grype/presenter/models/source.go b/grype/presenter/models/source.go index 6bbb1fe6158..f21c0674f2f 100644 --- a/grype/presenter/models/source.go +++ b/grype/presenter/models/source.go @@ -15,9 +15,9 @@ type source struct { // newSource creates a new source object to be represented into JSON. func newSource(src syftSource.Description) (source, error) { switch m := src.Metadata.(type) { - case pkg.PURLFileMetadata: + case pkg.SBOMFileMetadata: return source{ - Type: "purl-file", + Type: "sbom-file", Target: m.Path, }, nil case pkg.PURLLiteralMetadata: diff --git a/grype/presenter/models/source_test.go b/grype/presenter/models/source_test.go index 325b772a4e8..851fdf9a076 100644 --- a/grype/presenter/models/source_test.go +++ b/grype/presenter/models/source_test.go @@ -65,12 +65,12 @@ func TestNewSource(t *testing.T) { { name: "purl-file", metadata: syftSource.Description{ - Metadata: pkg.PURLFileMetadata{ + Metadata: pkg.SBOMFileMetadata{ Path: "/path/to/purls.txt", }, }, expected: source{ - Type: "purl-file", + Type: "sbom-file", Target: "/path/to/purls.txt", }, }, diff --git a/grype/presenter/sarif/presenter.go b/grype/presenter/sarif/presenter.go index ac8d0b2843c..4bd6a3cc765 100644 --- a/grype/presenter/sarif/presenter.go +++ b/grype/presenter/sarif/presenter.go @@ -407,8 +407,8 @@ func (p Presenter) resultMessage(m models.Match) sarif.Message { src = fmt.Sprintf("at: %s", path) case pkg.PURLLiteralMetadata: src = fmt.Sprintf("from purl literal %q", meta.PURL) - case pkg.PURLFileMetadata: - src = fmt.Sprintf("from purl file %s", meta.Path) + case pkg.SBOMFileMetadata: + src = fmt.Sprintf("from SBOM file %s", meta.Path) } message := fmt.Sprintf("A %s vulnerability in %s package: %s, version %s was found %s", severityText(m), m.Artifact.Type, m.Artifact.Name, m.Artifact.Version, src) diff --git a/grype/version/constraint_expression_test.go b/grype/version/constraint_expression_test.go index 5b0420ed343..4f5e8ba40f6 100644 --- a/grype/version/constraint_expression_test.go +++ b/grype/version/constraint_expression_test.go @@ -1,11 +1,11 @@ package version import ( - "github.com/google/go-cmp/cmp" - "github.com/stretchr/testify/require" "testing" "github.com/go-test/deep" + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/require" ) func TestScanExpression(t *testing.T) { diff --git a/grype/vulnerability_matcher.go b/grype/vulnerability_matcher.go index e0889ae50e7..0df6da3ca63 100644 --- a/grype/vulnerability_matcher.go +++ b/grype/vulnerability_matcher.go @@ -20,7 +20,6 @@ import ( "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/internal/bus" "github.com/anchore/grype/internal/log" - "github.com/anchore/syft/syft/linux" syftPkg "github.com/anchore/syft/syft/pkg" ) @@ -139,7 +138,7 @@ func (m *VulnerabilityMatcher) mergeIgnoredMatches(allIgnoredMatches ...[]match. //nolint:funlen func (m *VulnerabilityMatcher) searchDBForMatches( - release *linux.Release, + d *distro.Distro, packages []pkg.Package, progressMonitor *monitorWriter, ) (match.Matches, error) { @@ -147,19 +146,6 @@ func (m *VulnerabilityMatcher) searchDBForMatches( var allIgnored []match.IgnoredMatch matcherIndex, defaultMatcher := newMatcherIndex(m.Matchers) - var d *distro.Distro - if release != nil { - var err error - d, err = distro.NewFromRelease(*release) - if err != nil { - log.Warnf("unable to determine linux distribution: %+v", err) - } - if d != nil && d.Disabled() { - log.Warnf("unsupported linux distribution: %s", d.Name()) - return match.NewMatches(), nil - } - } - if defaultMatcher == nil { defaultMatcher = stock.NewStockMatcher(stock.MatcherConfig{UseCPEs: true}) } diff --git a/grype/vulnerability_matcher_test.go b/grype/vulnerability_matcher_test.go index baf5658b849..b9a7010578e 100644 --- a/grype/vulnerability_matcher_test.go +++ b/grype/vulnerability_matcher_test.go @@ -29,7 +29,6 @@ import ( "github.com/anchore/grype/internal/bus" "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/file" - "github.com/anchore/syft/syft/linux" syftPkg "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/source" ) @@ -264,9 +263,9 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) { }, }, context: pkg.Context{ - Distro: &linux.Release{ - ID: "debian", - VersionID: "8", + Distro: &distro.Distro{ + Type: "debian", + Version: "8", }, }, }, @@ -281,9 +280,9 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) { neutron2013Pkg, }, context: pkg.Context{ - Distro: &linux.Release{ - ID: "debian", - VersionID: "8", + Distro: &distro.Distro{ + Type: "debian", + Version: "8", }, }, }, @@ -336,9 +335,9 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) { neutron2013Pkg, }, context: pkg.Context{ - Distro: &linux.Release{ - ID: "debian", - VersionID: "8", + Distro: &distro.Distro{ + Type: "debian", + Version: "8", }, }, }, @@ -410,9 +409,9 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) { }, }, }, - Distro: &linux.Release{ - ID: "debian", - VersionID: "8", + Distro: &distro.Distro{ + Type: "debian", + Version: "8", }, }, }, @@ -1069,9 +1068,9 @@ func Test_fatalErrors(t *testing.T) { }, }, pkg.Context{ - Distro: &linux.Release{ - ID: "debian", - VersionID: "8", + Distro: &distro.Distro{ + Type: "debian", + Version: "8", }, }, ) diff --git a/test/integration/match_by_image_test.go b/test/integration/match_by_image_test.go index 254fa2459e6..9a226cc2157 100644 --- a/test/integration/match_by_image_test.go +++ b/test/integration/match_by_image_test.go @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/require" "github.com/anchore/grype/grype" + "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/matcher" "github.com/anchore/grype/grype/matcher/dotnet" @@ -30,7 +31,6 @@ import ( "github.com/anchore/syft/syft" "github.com/anchore/syft/syft/cataloging/pkgcataloging" "github.com/anchore/syft/syft/cpe" - "github.com/anchore/syft/syft/linux" syftPkg "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/source" ) @@ -787,7 +787,7 @@ func TestMatchByImage(t *testing.T) { }, }) - actualResults := grype.FindVulnerabilitiesForPackage(theProvider, s.Artifacts.LinuxDistribution, matchers, pkg.FromCollection(s.Artifacts.Packages, pkg.SynthesisConfig{})) + actualResults := grype.FindVulnerabilitiesForPackage(theProvider, distro.FromRelease(s.Artifacts.LinuxDistribution), matchers, pkg.FromCollection(s.Artifacts.Packages, pkg.SynthesisConfig{})) for _, m := range actualResults.Sorted() { for _, d := range m.Details { observedMatchers.Add(string(d.Matcher)) @@ -945,7 +945,6 @@ func vexMatches(t *testing.T, ignoredMatches []match.IgnoredMatch, vexStatus vex }, }, }, - Distro: &linux.Release{}, } vexedMatches, ignoredMatches, err := vexMatcher.ApplyVEX(pctx, &matches, ignoredMatches)