From 6a33b80048cf6753e569f3754be6b2543621ef5c Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Tue, 8 Oct 2024 09:31:33 -0400 Subject: [PATCH] prototype: start bitnami cataloger Bitnami images have spdx SBOMs at predictable paths, and Syft could more accurately identify the software in these images by scanning those SBOMs. Start work on this by forking the sbom-cataloger as a new bitnami-cataloger. Signed-off-by: Will Murphy --- internal/task/package_tasks.go | 2 + syft/pkg/cataloger/bitnami/cataloger.go | 83 +++++++++++++++++++ syft/pkg/cataloger/bitnami/cataloger_test.go | 41 +++++++++ .../opt/bitnami/redis/.spdx-redis.spdx | 42 ++++++++++ 4 files changed, 168 insertions(+) create mode 100644 syft/pkg/cataloger/bitnami/cataloger.go create mode 100644 syft/pkg/cataloger/bitnami/cataloger_test.go create mode 100644 syft/pkg/cataloger/bitnami/test-fixtures/opt/bitnami/redis/.spdx-redis.spdx diff --git a/internal/task/package_tasks.go b/internal/task/package_tasks.go index ff7f622fb..eb090fac8 100644 --- a/internal/task/package_tasks.go +++ b/internal/task/package_tasks.go @@ -6,6 +6,7 @@ import ( "github.com/anchore/syft/syft/pkg/cataloger/alpine" "github.com/anchore/syft/syft/pkg/cataloger/arch" "github.com/anchore/syft/syft/pkg/cataloger/binary" + bitnamiSbomCataloger "github.com/anchore/syft/syft/pkg/cataloger/bitnami" "github.com/anchore/syft/syft/pkg/cataloger/cpp" "github.com/anchore/syft/syft/pkg/cataloger/dart" "github.com/anchore/syft/syft/pkg/cataloger/debian" @@ -150,6 +151,7 @@ func DefaultPackageTaskFactories() PackageTaskFactories { pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, "linux", "kernel", ), newSimplePackageTaskFactory(sbomCataloger.NewCataloger, "sbom"), // note: not evidence of installed packages + newSimplePackageTaskFactory(bitnamiSbomCataloger.NewCataloger, "bitnami", pkgcataloging.InstalledTag), newSimplePackageTaskFactory(wordpress.NewWordpressPluginCataloger, pkgcataloging.DirectoryTag, pkgcataloging.ImageTag, "wordpress"), } } diff --git a/syft/pkg/cataloger/bitnami/cataloger.go b/syft/pkg/cataloger/bitnami/cataloger.go new file mode 100644 index 000000000..402863120 --- /dev/null +++ b/syft/pkg/cataloger/bitnami/cataloger.go @@ -0,0 +1,83 @@ +/* +Package bitnami provides a concrete Cataloger implementation for capturing packages embedded within Bitnami SBOM files. +*/ +package bitnami + +import ( + "bytes" + "context" + "fmt" + "io" + + "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/format" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/generic" +) + +const catalogerName = "bitnami-cataloger" + +// NewCataloger returns a new SBOM cataloger object loaded from saved SBOM JSON. +func NewCataloger() pkg.Cataloger { + return generic.NewCataloger(catalogerName). + WithParserByGlobs(parseSBOM, + "/opt/bitnami/**/*.spdx", + ) +} + +// TODO: this is copied from the sbom-cataloger +// it should probably be slimmed down so as not to duplicate +// parts of the SBOM cataloger that it doesn't need. +func parseSBOM(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { + readSeeker, err := adaptToReadSeeker(reader) + if err != nil { + return nil, nil, fmt.Errorf("unable to read SBOM file %q: %w", reader.Location.RealPath, err) + } + s, _, _, err := format.Decode(readSeeker) + if err != nil { + return nil, nil, err + } + + if s == nil { + log.WithFields("path", reader.Location.RealPath).Trace("file is not an SBOM") + return nil, nil, nil + } + + var pkgs []pkg.Package + 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 + // where there is evidence of this file, and the catalogers have not run against any file other than, + // the SBOM, this is the only location that is relevant for this cataloger. + p.Locations = file.NewLocationSet( + reader.Location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation), + ) + p.FoundBy = catalogerName + + pkgs = append(pkgs, p) + relationships = append(relationships, artifact.Relationship{ + From: p, + To: reader.Location.Coordinates, + Type: artifact.DescribedByRelationship, + }) + } + + return pkgs, relationships, nil +} + +func adaptToReadSeeker(reader io.Reader) (io.ReadSeeker, error) { + // with the stereoscope API and default file.Resolver implementation here in syft, odds are very high that + // the underlying reader is already a ReadSeeker, so we can just return it as-is. We still want to + if rs, ok := reader.(io.ReadSeeker); ok { + return rs, nil + } + + log.Debug("SBOM cataloger reader is not a ReadSeeker, reading entire SBOM into memory") + + var buff bytes.Buffer + _, err := io.Copy(&buff, reader) + return bytes.NewReader(buff.Bytes()), err +} diff --git a/syft/pkg/cataloger/bitnami/cataloger_test.go b/syft/pkg/cataloger/bitnami/cataloger_test.go new file mode 100644 index 000000000..4ff82ad4a --- /dev/null +++ b/syft/pkg/cataloger/bitnami/cataloger_test.go @@ -0,0 +1,41 @@ +package bitnami + +import ( + "testing" + + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestBitnamiCataloger(t *testing.T) { + tests := []struct { + name string + fixture string + wantPkgs []pkg.Package + wantError require.ErrorAssertionFunc + }{ + { + name: "simple-redis-sbom", + fixture: "test-fixtures", + // TODO: add package assertions + }, + // TODO: add another test case or too, maybe an error case + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pkgtest.NewCatalogTester(). + FromDirectory(t, tt.fixture). + ExpectsAssertion(func(t *testing.T, pkgs []pkg.Package, relationships []artifact.Relationship) { + for i, p := range pkgs { + assert.Equal(t, p.Name, tt.wantPkgs[i].Name) + assert.Equal(t, p.Version, tt.wantPkgs[i].Version) + } + }). + TestCataloger(t, NewCataloger()) + }) + } +} diff --git a/syft/pkg/cataloger/bitnami/test-fixtures/opt/bitnami/redis/.spdx-redis.spdx b/syft/pkg/cataloger/bitnami/test-fixtures/opt/bitnami/redis/.spdx-redis.spdx new file mode 100644 index 000000000..734e8f1f5 --- /dev/null +++ b/syft/pkg/cataloger/bitnami/test-fixtures/opt/bitnami/redis/.spdx-redis.spdx @@ -0,0 +1,42 @@ +{ + "SPDXID": "SPDXRef-redis", + "spdxVersion": "SPDX-2.3", + "creationInfo": { + "created": "2024-08-08T11:12:35.680Z", + "creators": [ + "Organization: VMware, Inc." + ] + }, + "name": "SPDX document for Redis(R) 7.4.0", + "dataLicense": "CC0-1.0", + "documentDescribes": [ + "SPDXRef-redis" + ], + "documentNamespace": "redis-7.4.0", + "packages": [ + { + "SPDXID": "SPDXRef-redis", + "name": "redis", + "versionInfo": "7.4.0-0", + "downloadLocation": "http://download.redis.io/releases/redis-7.4.0.tar.gz", + "licenseConcluded": "RSALv2", + "licenseDeclared": "RSALv2", + "filesAnalyzed": false, + "externalRefs": [ + { + "referenceCategory": "SECURITY", + "referenceType": "cpe23Type", + "referenceLocator": "cpe:2.3:*:redis:redis:7.4.0:*:*:*:*:*:*:*" + }, + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:bitnami/redis@7.4.0-0?arch=arm64&distro=debian-12" + } + ], + "copyrightText": "NOASSERTION" + } + ], + "files": [], + "relationships": [] +} \ No newline at end of file