diff --git a/syft/pkg/cataloger/bitnami/capabilities.yaml b/syft/pkg/cataloger/bitnami/capabilities.yaml index 69ff615fe..4edca292e 100644 --- a/syft/pkg/cataloger/bitnami/capabilities.yaml +++ b/syft/pkg/cataloger/bitnami/capabilities.yaml @@ -13,6 +13,35 @@ catalogers: - installed - package parsers: # AUTO-GENERATED structure + - function: parseComponentsJSON + detector: # AUTO-GENERATED + method: glob # AUTO-GENERATED + criteria: # AUTO-GENERATED + - /opt/bitnami/.bitnami_components.json + metadata_types: # AUTO-GENERATED + - pkg.BitnamiSBOMEntry + package_types: # AUTO-GENERATED + - bitnami + json_schema_types: # AUTO-GENERATED + - BitnamiSbomEntry + capabilities: # MANUAL - preserved across regeneration + - name: license + default: false + comment: legacy components.json format does not include license information + - name: dependency.depth + default: [] + comment: legacy format has no dependency relationships + - name: dependency.edges + default: "" + - name: dependency.kinds + default: [] + - name: package_manager.files.listing + default: false + - name: package_manager.files.digests + default: false + - name: package_manager.package_integrity_hash + default: false + comment: digest field exists but is not captured in metadata - function: parseSBOM detector: # AUTO-GENERATED method: glob # AUTO-GENERATED diff --git a/syft/pkg/cataloger/bitnami/cataloger.go b/syft/pkg/cataloger/bitnami/cataloger.go index dd876f56e..71f6518e2 100644 --- a/syft/pkg/cataloger/bitnami/cataloger.go +++ b/syft/pkg/cataloger/bitnami/cataloger.go @@ -23,6 +23,9 @@ func NewCataloger() pkg.Cataloger { return generic.NewCataloger(catalogerName). WithParserByGlobs(parseSBOM, "/opt/bitnami/**/.spdx-*.spdx", + ). + WithParserByGlobs(parseComponentsJSON, + "/opt/bitnami/.bitnami_components.json", ) } diff --git a/syft/pkg/cataloger/bitnami/cataloger_test.go b/syft/pkg/cataloger/bitnami/cataloger_test.go index aa4c55e7e..7f56e30b4 100644 --- a/syft/pkg/cataloger/bitnami/cataloger_test.go +++ b/syft/pkg/cataloger/bitnami/cataloger_test.go @@ -448,6 +448,109 @@ func TestBitnamiCataloger(t *testing.T) { }, } + mongodbComponentsPkgs := []pkg.Package{ + { + Name: "gosu", + Version: "1.14.0-1", + Type: pkg.BitnamiPkg, + Locations: file.NewLocationSet(file.NewLocation("opt/bitnami/.bitnami_components.json")), + FoundBy: catalogerName, + PURL: "pkg:bitnami/gosu@1.14.0-1?arch=amd64&distro=debian-10", + Metadata: &pkg.BitnamiSBOMEntry{ + Name: "gosu", + Version: "1.14.0", + Revision: "1", + Architecture: "amd64", + Distro: "debian-10", + Path: "opt/bitnami/gosu", + }, + }, + { + Name: "mongodb", + Version: "4.4.11-2", + Type: pkg.BitnamiPkg, + Locations: file.NewLocationSet(file.NewLocation("opt/bitnami/.bitnami_components.json")), + FoundBy: catalogerName, + PURL: "pkg:bitnami/mongodb@4.4.11-2?arch=amd64&distro=debian-10", + Metadata: &pkg.BitnamiSBOMEntry{ + Name: "mongodb", + Version: "4.4.11", + Revision: "2", + Architecture: "amd64", + Distro: "debian-10", + Path: "opt/bitnami/mongodb", + }, + }, + { + Name: "render-template", + Version: "1.0.1-5", + Type: pkg.BitnamiPkg, + Locations: file.NewLocationSet(file.NewLocation("opt/bitnami/.bitnami_components.json")), + FoundBy: catalogerName, + PURL: "pkg:bitnami/render-template@1.0.1-5?arch=amd64&distro=debian-10", + Metadata: &pkg.BitnamiSBOMEntry{ + Name: "render-template", + Version: "1.0.1", + Revision: "5", + Architecture: "amd64", + Distro: "debian-10", + Path: "opt/bitnami/render-template", + }, + }, + { + Name: "wait-for-port", + Version: "1.0.1-5", + Type: pkg.BitnamiPkg, + Locations: file.NewLocationSet(file.NewLocation("opt/bitnami/.bitnami_components.json")), + FoundBy: catalogerName, + PURL: "pkg:bitnami/wait-for-port@1.0.1-5?arch=amd64&distro=debian-10", + Metadata: &pkg.BitnamiSBOMEntry{ + Name: "wait-for-port", + Version: "1.0.1", + Revision: "5", + Architecture: "amd64", + Distro: "debian-10", + Path: "opt/bitnami/wait-for-port", + }, + }, + { + Name: "yq", + Version: "4.16.2-2", + Type: pkg.BitnamiPkg, + Locations: file.NewLocationSet(file.NewLocation("opt/bitnami/.bitnami_components.json")), + FoundBy: catalogerName, + PURL: "pkg:bitnami/yq@4.16.2-2?arch=amd64&distro=debian-10", + Metadata: &pkg.BitnamiSBOMEntry{ + Name: "yq", + Version: "4.16.2", + Revision: "2", + Architecture: "amd64", + Distro: "debian-10", + Path: "opt/bitnami/yq", + }, + }, + } + pkg.Sort(mongodbComponentsPkgs) + + postgresqlComponentsPkgs := []pkg.Package{ + { + Name: "postgresql", + Version: "11.22.0-4", + Type: pkg.BitnamiPkg, + Locations: file.NewLocationSet(file.NewLocation("opt/bitnami/.bitnami_components.json")), + FoundBy: catalogerName, + PURL: "pkg:bitnami/postgresql@11.22.0-4?arch=amd64&distro=debian-11", + Metadata: &pkg.BitnamiSBOMEntry{ + Name: "postgresql", + Version: "11.22.0", + Revision: "4", + Architecture: "amd64", + Distro: "debian-11", + Path: "opt/bitnami/postgresql", + }, + }, + } + tests := []struct { name string fixture string @@ -489,6 +592,20 @@ func TestBitnamiCataloger(t *testing.T) { wantPkgs: []pkg.Package{redisMainPkg}, wantRelationships: nil, }, + { + name: "parse legacy .bitnami_components.json (MongoDB with multiple components)", + fixture: "test-fixtures/components-json-mongodb", + wantPkgs: mongodbComponentsPkgs, + wantRelationships: nil, + wantErr: require.NoError, + }, + { + name: "parse legacy .bitnami_components.json (PostgreSQL single component, no digest)", + fixture: "test-fixtures/components-json-postgresql", + wantPkgs: postgresqlComponentsPkgs, + wantRelationships: nil, + wantErr: require.NoError, + }, } for _, tt := range tests { diff --git a/syft/pkg/cataloger/bitnami/parse_components.go b/syft/pkg/cataloger/bitnami/parse_components.go new file mode 100644 index 000000000..155a89831 --- /dev/null +++ b/syft/pkg/cataloger/bitnami/parse_components.go @@ -0,0 +1,100 @@ +package bitnami + +import ( + "context" + "encoding/json" + "fmt" + "path/filepath" + "sort" + "strings" + + "github.com/anchore/packageurl-go" + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/generic" +) + +type componentEntry struct { + Arch string `json:"arch"` + Digest string `json:"digest,omitempty"` + Distro string `json:"distro"` + Type string `json:"type"` + Version string `json:"version"` +} + +func parseComponentsJSON(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { + var components map[string]componentEntry + + decoder := json.NewDecoder(reader) + if err := decoder.Decode(&components); err != nil { + return nil, nil, fmt.Errorf("unable to parse .bitnami_components.json: %w", err) + } + + var pkgs []pkg.Package + + names := make([]string, 0, len(components)) + for name := range components { + names = append(names, name) + } + sort.Strings(names) + + for _, name := range names { + entry := components[name] + + version, revision := parseVersionRevision(entry.Version) + + var qualifiers []packageurl.Qualifier + if entry.Arch != "" { + qualifiers = append(qualifiers, packageurl.Qualifier{Key: "arch", Value: entry.Arch}) + } + if entry.Distro != "" { + qualifiers = append(qualifiers, packageurl.Qualifier{Key: "distro", Value: entry.Distro}) + } + + purl := packageurl.NewPackageURL( + "bitnami", + "", + name, + entry.Version, + qualifiers, + "", + ).String() + + metadata := &pkg.BitnamiSBOMEntry{ + Name: name, + Version: version, + Revision: revision, + Architecture: entry.Arch, + Distro: entry.Distro, + Path: filepath.Join(filepath.Dir(reader.RealPath), name), + } + + p := pkg.Package{ + Name: name, + Version: entry.Version, + Type: pkg.BitnamiPkg, + FoundBy: catalogerName, + PURL: purl, + Locations: file.NewLocationSet( + reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation), + ), + Metadata: metadata, + } + + p.SetID() + pkgs = append(pkgs, p) + } + + // seems legacy format doesnt have relation between pkgs + return pkgs, nil, nil +} + +func parseVersionRevision(fullVersion string) (version, revision string) { + lastHyphen := strings.LastIndex(fullVersion, "-") + if lastHyphen == -1 { + return fullVersion, "" + } + + return fullVersion[:lastHyphen], fullVersion[lastHyphen+1:] +} diff --git a/syft/pkg/cataloger/bitnami/test-fixtures/components-json-mongodb/opt/bitnami/.bitnami_components.json b/syft/pkg/cataloger/bitnami/test-fixtures/components-json-mongodb/opt/bitnami/.bitnami_components.json new file mode 100644 index 000000000..9a043e10f --- /dev/null +++ b/syft/pkg/cataloger/bitnami/test-fixtures/components-json-mongodb/opt/bitnami/.bitnami_components.json @@ -0,0 +1,37 @@ +{ + "gosu": { + "arch": "amd64", + "digest": "16f1a317859b06ae82e816b30f98f28b4707d18fe6cc3881bff535192a7715dc", + "distro": "debian-10", + "type": "NAMI", + "version": "1.14.0-1" + }, + "mongodb": { + "arch": "amd64", + "digest": "d21fc890385eae0aaa394ce6491ef304d28a5cc43f860def6badee401fe55bc5", + "distro": "debian-10", + "type": "NAMI", + "version": "4.4.11-2" + }, + "render-template": { + "arch": "amd64", + "digest": "9e312b4a7e16a55d08e67c4fd69c91000e4dcc4af149d59915c49375b83852af", + "distro": "debian-10", + "type": "NAMI", + "version": "1.0.1-5" + }, + "wait-for-port": { + "arch": "amd64", + "digest": "1e34030c18f0ec2467fa5f1b1fbad24add217f671c3a61628f7b8671391f9676", + "distro": "debian-10", + "type": "NAMI", + "version": "1.0.1-5" + }, + "yq": { + "arch": "amd64", + "digest": "1c135708aaa8cb69936471de63563de08e97b7d0bfb4126d41b54a149557c5c0", + "distro": "debian-10", + "type": "NAMI", + "version": "4.16.2-2" + } +} diff --git a/syft/pkg/cataloger/bitnami/test-fixtures/components-json-postgresql/opt/bitnami/.bitnami_components.json b/syft/pkg/cataloger/bitnami/test-fixtures/components-json-postgresql/opt/bitnami/.bitnami_components.json new file mode 100644 index 000000000..2bdadd283 --- /dev/null +++ b/syft/pkg/cataloger/bitnami/test-fixtures/components-json-postgresql/opt/bitnami/.bitnami_components.json @@ -0,0 +1,8 @@ +{ + "postgresql": { + "arch": "amd64", + "distro": "debian-11", + "type": "NAMI", + "version": "11.22.0-4" + } +}