fix(docker): fix non-det scan results for images with embedded SBOM (#9866)

Co-authored-by: DmitriyLewen <dmitriy.lewen@smartforce.io>
This commit is contained in:
Andre Oganesian
2026-01-12 05:10:07 -05:00
committed by GitHub
parent 60eb3f0a2f
commit 7f71b577a0
4 changed files with 110 additions and 12 deletions

View File

@@ -235,6 +235,9 @@ func ApplyLayers(layers []ftypes.BlobInfo) ftypes.ArtifactDetail {
}
}
// Filter OS packages with mismatched PURL namespace
mergedLayer.Packages = filterMismatchedOSPkgs(mergedLayer.OS.Family, mergedLayer.Packages)
// De-duplicate same debian packages from different dirs
// cf. https://github.com/aquasecurity/trivy/issues/8297
mergedLayer.Packages = xslices.ZeroToNil(lo.UniqBy(mergedLayer.Packages, func(pkg ftypes.Package) string {
@@ -343,3 +346,41 @@ func secretFindingsContains(findings []ftypes.SecretFinding, finding ftypes.Secr
}
return false
}
// purlMatchesOS checks if a package's PURL namespace matches the detected OS family.
// Returns true if the package should be kept (matches OS or has no PURL/namespace).
// Returns false if the package should be filtered out (has PURL with mismatched namespace).
func purlMatchesOS(pkg ftypes.Package, osFamily ftypes.OSType) bool {
if pkg.Identifier.PURL == nil || osFamily == "" {
return true // Keep packages without PURL or when OS is not detected
}
if pkg.Identifier.PURL.Namespace == "" {
return true // Keep packages without namespace
}
return pkg.Identifier.PURL.Namespace == osFamily.PurlNamespace()
}
// filterMismatchedOSPkgs removes OS packages whose PURL namespace doesn't match the detected OS.
// Packages with pre-existing PURLs are typically from SBOM files embedded in the image.
func filterMismatchedOSPkgs(osFamily ftypes.OSType, pkgs ftypes.Packages) ftypes.Packages {
if osFamily == "" {
return pkgs // No OS detected, keep all packages
}
var filtered int
result := lo.Filter(pkgs, func(pkg ftypes.Package, _ int) bool {
if purlMatchesOS(pkg, osFamily) {
return true
}
filtered++
return false
})
if filtered > 0 {
log.WithPrefix("applier").Warn("Some OS packages were skipped due to mismatched PURL namespace",
log.Int("pkg_count", filtered),
log.String("detected_os", string(osFamily)))
}
return result
}

View File

@@ -1462,6 +1462,56 @@ func TestApplyLayers(t *testing.T) {
},
},
},
{
// Duplicate packages with different PURL namespaces, prefer OS-matching PURL
name: "prefer OS-matching PURL during deduplication",
inputLayers: []types.BlobInfo{
{
SchemaVersion: 2,
OS: types.OS{Family: "chainguard", Name: "20230201"},
PackageInfos: []types.PackageInfo{
{
FilePath: "lib/apk/db/installed",
Packages: types.Packages{
{Name: "libcrypto3", Version: "3.0.0"}, // No PURL - will get chainguard
{
Name: "libcrypto3",
Version: "3.0.0",
Identifier: types.PkgIdentifier{
PURL: &packageurl.PackageURL{
Type: packageurl.TypeApk,
Namespace: "wolfi", // Mismatched
Name: "libcrypto3",
Version: "3.0.0",
Qualifiers: packageurl.Qualifiers{{Key: "distro", Value: "wolfi"}},
},
},
},
},
},
},
},
},
want: types.ArtifactDetail{
OS: types.OS{Family: "chainguard", Name: "20230201"},
Packages: types.Packages{
{
Name: "libcrypto3",
Version: "3.0.0",
Identifier: types.PkgIdentifier{
UID: "bdca9f208c0174a0",
PURL: &packageurl.PackageURL{
Type: packageurl.TypeApk,
Namespace: "chainguard", // Matches OS
Name: "libcrypto3",
Version: "3.0.0",
Qualifiers: packageurl.Qualifiers{{Key: "distro", Value: "20230201"}},
},
},
},
},
},
},
}
for _, tt := range tests {

View File

@@ -48,6 +48,22 @@ const (
Wolfi OSType = "wolfi"
)
// PurlNamespace returns the normalized namespace for Package URL (PURL) representation.
// For SUSE-based distributions (SLES, SLE Micro), it returns "suse".
// For openSUSE variants (Tumbleweed, Leap), it returns "opensuse".
// For all other OSTypes, it returns the string representation of the OSType.
func (o OSType) PurlNamespace() string {
// SLES string has whitespace, also highlevel family is not the same as distro
if o == SLES || o == SLEMicro {
return "suse"
}
if o == OpenSUSETumbleweed || o == OpenSUSELeap {
return "opensuse"
}
return string(o)
}
// OSTypeAliases is a map of aliases for operating systems.
var OSTypeAliases = map[OSType]OSType{
// This is used to map the old family names to the new ones for backward compatibility.

View File

@@ -77,7 +77,7 @@ func New(t ftypes.TargetType, metadata types.Metadata, pkg ftypes.Package) (*Pac
switch ptype {
case packageurl.TypeRPM:
ns, qs := parseRPM(metadata.OS, pkg.Modularitylabel)
namespace = string(ns)
namespace = ns
qualifiers = append(qualifiers, qs...)
case packageurl.TypeDebian:
qualifiers = append(qualifiers, parseDeb(metadata.OS)...)
@@ -369,20 +369,11 @@ func parseDeb(fos *ftypes.OS) packageurl.Qualifiers {
}
// ref. https://github.com/package-url/purl-spec/blob/a748c36ad415c8aeffe2b8a4a5d8a50d16d6d85f/PURL-TYPES.rst#rpm
func parseRPM(fos *ftypes.OS, modularityLabel string) (ftypes.OSType, packageurl.Qualifiers) {
func parseRPM(fos *ftypes.OS, modularityLabel string) (string, packageurl.Qualifiers) {
if fos == nil {
return "", packageurl.Qualifiers{}
}
family := fos.Family
// SLES string has whitespace, also highlevel family is not the same as distro
if fos.Family == ftypes.SLES || fos.Family == ftypes.SLEMicro {
family = "suse"
}
if fos.Family == ftypes.OpenSUSETumbleweed || fos.Family == ftypes.OpenSUSELeap {
family = "opensuse"
}
qualifiers := packageurl.Qualifiers{
{
Key: "distro",
@@ -396,7 +387,7 @@ func parseRPM(fos *ftypes.OS, modularityLabel string) (ftypes.OSType, packageurl
Value: modularityLabel,
})
}
return family, qualifiers
return fos.Family.PurlNamespace(), qualifiers
}
// ref. https://github.com/package-url/purl-spec/blob/a748c36ad415c8aeffe2b8a4a5d8a50d16d6d85f/PURL-TYPES.rst#maven