diff --git a/integration/client_server_test.go b/integration/client_server_test.go index a57460497db7..0b37cb253abf 100644 --- a/integration/client_server_test.go +++ b/integration/client_server_test.go @@ -13,6 +13,8 @@ import ( dockercontainer "github.com/docker/docker/api/types/container" "github.com/docker/go-connections/nat" + "github.com/package-url/packageurl-go" + lom "github.com/samber/lo/mutable" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/testcontainers/testcontainers-go" @@ -70,6 +72,16 @@ func TestClientServer(t *testing.T) { override: func(_ *testing.T, want, _ *types.Report) { want.Metadata.OS.Name = "3.10" want.Results[0].Target = "testdata/fixtures/images/alpine-39.tar.gz (alpine 3.10)" + for i := range want.Results[0].Vulnerabilities { + p := *want.Results[0].Vulnerabilities[i].PkgIdentifier.PURL // Copy PURL to avoid shadowing overwrite + lom.Map(p.Qualifiers, func(q packageurl.Qualifier) packageurl.Qualifier { + if q.Key == "distro" { + q.Value = "3.10" + } + return q + }) + want.Results[0].Vulnerabilities[i].PkgIdentifier.PURL = &p + } }, golden: goldenAlpine39, }, diff --git a/integration/standalone_tar_test.go b/integration/standalone_tar_test.go index f2cca80d65ee..cdd334071500 100644 --- a/integration/standalone_tar_test.go +++ b/integration/standalone_tar_test.go @@ -8,6 +8,8 @@ import ( "strings" "testing" + "github.com/package-url/packageurl-go" + lom "github.com/samber/lo/mutable" "github.com/stretchr/testify/require" ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" @@ -477,6 +479,16 @@ func TestTarWithOverride(t *testing.T) { override: func(_ *testing.T, want, _ *types.Report) { want.Metadata.OS.Name = "3.10" want.Results[0].Target = "testdata/fixtures/images/alpine-39.tar.gz (alpine 3.10)" + for i := range want.Results[0].Vulnerabilities { + p := *want.Results[0].Vulnerabilities[i].PkgIdentifier.PURL // Copy PURL to avoid shadowing overwrite + lom.Map(p.Qualifiers, func(q packageurl.Qualifier) packageurl.Qualifier { + if q.Key == "distro" { + q.Value = "3.10" + } + return q + }) + want.Results[0].Vulnerabilities[i].PkgIdentifier.PURL = &p + } }, golden: goldenAlpine39, }, diff --git a/pkg/scan/local/service.go b/pkg/scan/local/service.go index 700e861286cc..24169b81eb49 100644 --- a/pkg/scan/local/service.go +++ b/pkg/scan/local/service.go @@ -10,6 +10,7 @@ import ( "sync" "github.com/samber/lo" + lom "github.com/samber/lo/mutable" "golang.org/x/xerrors" dbTypes "github.com/aquasecurity/trivy-db/pkg/types" @@ -21,6 +22,7 @@ import ( "github.com/aquasecurity/trivy/pkg/iac/rego" "github.com/aquasecurity/trivy/pkg/licensing" "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/purl" "github.com/aquasecurity/trivy/pkg/scan/langpkg" "github.com/aquasecurity/trivy/pkg/scan/ospkg" "github.com/aquasecurity/trivy/pkg/set" @@ -85,6 +87,20 @@ func (s Service) Scan(ctx context.Context, targetName, artifactKey string, blobK log.Info("Overriding detected OS with provided distro", log.String("detected", detail.OS.String()), log.String("provided", options.Distro.String())) detail.OS = options.Distro + + // Override OS packages PURL to update the distro, + // preserving the correlation between the OS and package PURLs. + lom.Map(detail.Packages, func(pkg ftypes.Package) ftypes.Package { + p, pErr := purl.New(detail.OS.Family, types.Metadata{OS: &detail.OS}, pkg) + if pErr != nil { + log.Error("Failed to create PackageURL", log.Err(err)) + return pkg + } + + pkg.Identifier.PURL = p.Unwrap() + return pkg + }) + } target := types.ScanTarget{ diff --git a/pkg/scan/local/service_test.go b/pkg/scan/local/service_test.go index b8ed8698908c..09200e6f5f6e 100644 --- a/pkg/scan/local/service_test.go +++ b/pkg/scan/local/service_test.go @@ -303,18 +303,35 @@ func TestScanner_Scan(t *testing.T) { fixtures: []string{"testdata/fixtures/happy.yaml"}, setupCache: func(t *testing.T) cache.Cache { c := cache.NewMemoryCache() + // Override muslPkg with `3.10` distro in PURL + pkg := muslPkg + pkg.Identifier.PURL = &packageurl.PackageURL{ + Type: muslPkg.Identifier.PURL.Type, + Namespace: muslPkg.Identifier.PURL.Namespace, + Name: muslPkg.Identifier.PURL.Name, + Version: muslPkg.Identifier.PURL.Version, + Qualifiers: packageurl.Qualifiers{ + packageurl.Qualifier{ + Key: "distro", + Value: "3.10", + }, + }, + } + require.NoError(t, c.PutBlob(t.Context(), "sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10", ftypes.BlobInfo{ SchemaVersion: ftypes.BlobJSONSchemaVersion, Size: 1000, DiffID: "sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10", OS: ftypes.OS{ Family: ftypes.Alpine, - Name: "3.11", + Name: "3.10", }, PackageInfos: []ftypes.PackageInfo{ { FilePath: "lib/apk/db/installed", - Packages: []ftypes.Package{muslPkg}, + Packages: []ftypes.Package{ + pkg, + }, }, }, })) @@ -363,6 +380,71 @@ func TestScanner_Scan(t *testing.T) { }, }, }, + { + name: "happy path with empty OS rewriting", + args: args{ + target: "alpine:unknown-os", + layerIDs: []string{"sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10"}, + options: types.ScanOptions{ + PkgTypes: []string{ + types.PkgTypeOS, + types.PkgTypeLibrary, + }, + PkgRelationships: ftypes.Relationships, + Scanners: types.Scanners{types.VulnerabilityScanner}, + VulnSeveritySources: []dbTypes.SourceID{"auto"}, + Distro: ftypes.OS{ + Family: "alpine", + Name: "3.11", + }, + }, + }, + setupCache: func(t *testing.T) cache.Cache { + c := cache.NewMemoryCache() + // Override muslPkg with empty PURL, because OS not found + pkg := muslPkg + pkg.Identifier.PURL = nil + + require.NoError(t, c.PutBlob(t.Context(), "sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10", ftypes.BlobInfo{ + SchemaVersion: ftypes.BlobJSONSchemaVersion, + Size: 1000, + DiffID: "sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10", + // OS not found, but OS package exists + PackageInfos: []ftypes.PackageInfo{ + { + FilePath: "lib/apk/db/installed", + Packages: []ftypes.Package{ + pkg, + }, + }, + }, + })) + return c + }, + want: types.ScanResponse{ + Results: types.Results{ + { + Target: "alpine:unknown-os (alpine 3.11)", + Class: types.ClassOSPkg, + Type: ftypes.Alpine, + Packages: ftypes.Packages{ + muslPkg, + }, + }, + }, + OS: ftypes.OS{ + Family: "alpine", + Name: "3.11", + Eosl: true, + }, + Layers: ftypes.Layers{ + { + Size: 1000, + DiffID: "sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10", + }, + }, + }, + }, { name: "happy path license scanner (exclude language-specific packages)", args: args{