mirror of
https://github.com/anchore/syft.git
synced 2026-10-30 22:12:28 +01:00
fix: keep embedded SBOM relationships from adding files to the scan
The bitnami and `sbom` catalogers decode SBOMs found in the scanned image and kept the relationships from them. The decoder nests a file under another the SBOM says CONTAINS it, and makes a file the root package DESCRIBES the scan root, so an embedded SBOM could inject archive chains or root files that encoders then rebuilt in the output. - both catalogers now drop relationships with a file endpoint that is nested or at the scan root - bitnami also keeps only relationships from its own packages Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/anchore/syft/syft/format"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/generic"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/internal/embeddedsbom"
|
||||
)
|
||||
|
||||
const catalogerName = "bitnami-cataloger"
|
||||
@@ -109,24 +110,27 @@ func parseSBOM(_ context.Context, resolver file.Resolver, _ *generic.Environment
|
||||
return pkgs, filterRelationships(s.Relationships, pkgs), nil
|
||||
}
|
||||
|
||||
// filterRelationships filters out relationships that are not related to Bitnami packages
|
||||
// filterRelationships filters out relationships that are not from Bitnami packages
|
||||
// and replaces the package information with the one with completed info
|
||||
func filterRelationships(relationships []artifact.Relationship, pkgs []pkg.Package) []artifact.Relationship {
|
||||
var result []artifact.Relationship
|
||||
for _, r := range relationships {
|
||||
if value, ok := r.From.(pkg.Package); ok {
|
||||
found := false
|
||||
for _, p := range pkgs {
|
||||
if value.PURL == p.PURL {
|
||||
r.From = p
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
continue
|
||||
for _, r := range embeddedsbom.Relationships(relationships) {
|
||||
// only what a package says about itself is kept
|
||||
value, ok := r.From.(pkg.Package)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
found := false
|
||||
for _, p := range pkgs {
|
||||
if value.PURL == p.PURL {
|
||||
r.From = p
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
if value, ok := r.To.(pkg.Package); ok {
|
||||
found := false
|
||||
for _, p := range pkgs {
|
||||
|
||||
@@ -2,6 +2,8 @@ package bitnami
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -622,3 +624,67 @@ func TestBitnamiCataloger(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_filterRelationships_keepsOnlyPackageRelationships(t *testing.T) {
|
||||
p := pkg.Package{Name: "postgresql", PURL: "pkg:bitnami/postgresql@17.2.0"}
|
||||
jar := file.NewLocation("app.jar")
|
||||
inner := file.NewLocation("BOOT-INF/lib/log4j-core-2.14.1.jar")
|
||||
// as the decoder nests it when the SBOM also says the jar CONTAINS it
|
||||
nested := file.NewLocationFromCoordinates(file.Coordinates{RealPath: "/app.jar", FullPath: "/app.jar!/BOOT-INF/lib/log4j-core-2.14.1.jar"})
|
||||
relationships := []artifact.Relationship{
|
||||
{From: p, To: jar, Type: artifact.ContainsRelationship},
|
||||
{From: jar, To: inner, Type: artifact.ContainsRelationship},
|
||||
{From: jar, To: p, Type: artifact.ContainsRelationship},
|
||||
{From: p, To: nested, Type: artifact.ContainsRelationship},
|
||||
}
|
||||
|
||||
got := filterRelationships(relationships, []pkg.Package{p})
|
||||
|
||||
require.Equal(t, relationships[:1], got)
|
||||
}
|
||||
|
||||
func Test_parseSBOM_nestingInTheSBOMIsNotKept(t *testing.T) {
|
||||
// a file the SBOM says another CONTAINS decodes as nested within it, and one the SBOM DESCRIBES as the scan
|
||||
// root; neither is anything the scan found
|
||||
const head = `{
|
||||
"spdxVersion":"SPDX-2.3","dataLicense":"CC0-1.0","SPDXID":"SPDXRef-DOCUMENT","name":"x","documentNamespace":"https://x/y",
|
||||
"creationInfo":{"created":"2024-01-01T00:00:00Z","creators":["Tool: x"]},
|
||||
"packages":[{"name":"postgresql","SPDXID":"SPDXRef-pg","versionInfo":"17.2.0","downloadLocation":"NONE",
|
||||
"externalRefs":[{"referenceCategory":"PACKAGE-MANAGER","referenceType":"purl","referenceLocator":"pkg:bitnami/postgresql@17.2.0"}]}],
|
||||
"files":[{"fileName":"app.jar","SPDXID":"SPDXRef-F1","checksums":[{"algorithm":"SHA1","checksumValue":"0000000000000000000000000000000000000000"}]},
|
||||
{"fileName":"BOOT-INF/lib/log4j-core-2.14.1.jar","SPDXID":"SPDXRef-F2","checksums":[{"algorithm":"SHA1","checksumValue":"0000000000000000000000000000000000000000"}]}],
|
||||
"relationships":[`
|
||||
tests := []struct {
|
||||
name string
|
||||
relationships string
|
||||
}{
|
||||
{
|
||||
name: "nested",
|
||||
relationships: `{"spdxElementId":"SPDXRef-DOCUMENT","relationshipType":"DESCRIBES","relatedSpdxElement":"SPDXRef-pg"},
|
||||
{"spdxElementId":"SPDXRef-pg","relationshipType":"CONTAINS","relatedSpdxElement":"SPDXRef-F2"},
|
||||
{"spdxElementId":"SPDXRef-F1","relationshipType":"CONTAINS","relatedSpdxElement":"SPDXRef-F2"}`,
|
||||
},
|
||||
{
|
||||
name: "scan root",
|
||||
relationships: `{"spdxElementId":"SPDXRef-DOCUMENT","relationshipType":"DESCRIBES","relatedSpdxElement":"SPDXRef-pg"},
|
||||
{"spdxElementId":"SPDXRef-pg","relationshipType":"DESCRIBES","relatedSpdxElement":"SPDXRef-F1"},
|
||||
{"spdxElementId":"SPDXRef-pg","relationshipType":"CONTAINS","relatedSpdxElement":"SPDXRef-F1"},
|
||||
{"spdxElementId":"SPDXRef-F1","relationshipType":"CONTAINS","relatedSpdxElement":"SPDXRef-F2"}`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
loc := file.NewLocation("/opt/bitnami/postgresql/.spdx-postgresql.spdx")
|
||||
doc := head + tt.relationships + "]}"
|
||||
_, relationships, err := parseSBOM(context.Background(), file.NewMockResolverForPaths(), nil, file.NewLocationReadCloser(loc, io.NopCloser(strings.NewReader(doc))))
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, r := range relationships {
|
||||
if l, ok := r.To.(file.Location); ok {
|
||||
require.Empty(t, l.FullPath, "%+v", r)
|
||||
require.NotContains(t, []string{"", "/"}, l.RealPath, "%+v", r)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
Package embeddedsbom holds what catalogers that read an SBOM found within the scan share.
|
||||
*/
|
||||
package embeddedsbom
|
||||
|
||||
import (
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
)
|
||||
|
||||
// Relationships keeps the relationships decoded from an embedded SBOM that may enter the scan's own results. A
|
||||
// file the SBOM nests within an archive, or makes the scan root, is nothing this scan found, and encoders would
|
||||
// rebuild archives and roots from it.
|
||||
func Relationships(relationships []artifact.Relationship) []artifact.Relationship {
|
||||
var out []artifact.Relationship
|
||||
for _, r := range relationships {
|
||||
if plainFile(r.From) && plainFile(r.To) {
|
||||
out = append(out, r)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// plainFile reports false for a file endpoint nested in an archive or at the scan root, and true otherwise.
|
||||
func plainFile(v artifact.Identifiable) bool {
|
||||
var c file.Coordinates
|
||||
switch v := v.(type) {
|
||||
case file.Location:
|
||||
c = v.Coordinates
|
||||
case file.Coordinates:
|
||||
c = v
|
||||
default:
|
||||
return true
|
||||
}
|
||||
return c.FullPath == "" && c.RealPath != "" && c.RealPath != "/"
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package embeddedsbom
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
func TestRelationships(t *testing.T) {
|
||||
p := pkg.Package{Name: "postgresql"}
|
||||
flat := file.NewLocation("/opt/app.jar")
|
||||
nested := file.NewLocationFromCoordinates(file.Coordinates{RealPath: "/app.jar", FullPath: "/app.jar!/lib/a.jar"})
|
||||
root := file.NewLocation("")
|
||||
|
||||
keep := []artifact.Relationship{
|
||||
{From: p, To: p, Type: artifact.DependencyOfRelationship},
|
||||
{From: p, To: flat, Type: artifact.ContainsRelationship},
|
||||
{From: p, To: flat.Coordinates, Type: artifact.ContainsRelationship},
|
||||
}
|
||||
drop := []artifact.Relationship{
|
||||
{From: p, To: nested, Type: artifact.ContainsRelationship},
|
||||
{From: p, To: nested.Coordinates, Type: artifact.ContainsRelationship},
|
||||
{From: p, To: root, Type: artifact.ContainsRelationship},
|
||||
{From: p, To: file.NewLocation("/"), Type: artifact.ContainsRelationship},
|
||||
{From: root, To: flat, Type: artifact.ContainsRelationship},
|
||||
}
|
||||
|
||||
assert.Equal(t, keep, Relationships(append(append([]artifact.Relationship{}, keep...), drop...)))
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/anchore/syft/syft/format"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/generic"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/internal/embeddedsbom"
|
||||
)
|
||||
|
||||
const catalogerName = "sbom-cataloger"
|
||||
@@ -53,7 +54,7 @@ func parseSBOM(_ context.Context, _ file.Resolver, _ *generic.Environment, reade
|
||||
}
|
||||
|
||||
var pkgs []pkg.Package
|
||||
relationships := s.Relationships
|
||||
relationships := embeddedsbom.Relationships(s.Relationships)
|
||||
for _, p := range s.Artifacts.Packages.Sorted() {
|
||||
// replace all locations on the package with the location of the SBOM file.
|
||||
// Why not keep the original list of locations? Since the "locations" field is meant to capture
|
||||
|
||||
Reference in New Issue
Block a user