From aebe843c6f0307d5c263c25257caf3e6ea827c78 Mon Sep 17 00:00:00 2001 From: Sambhav Kothari Date: Wed, 19 Jan 2022 16:43:16 +0000 Subject: [PATCH] Improve CycloneDX format output (#710) * Improve CycloneDX format output ## Additions to CycloneDX output * CPEs * Authors * Publishers * External References (Website, Distribution, VCS) * Description Signed-off-by: Sambhav Kothari --- .../formats/common/cyclonedxhelpers/author.go | 32 ++++ .../common/cyclonedxhelpers/author_test.go | 87 +++++++++++ .../common/cyclonedxhelpers/component.go | 26 ++++ .../formats/common/cyclonedxhelpers/cpe.go | 12 ++ .../common/cyclonedxhelpers/cpe_test.go | 57 ++++++++ .../common/cyclonedxhelpers/description.go | 15 ++ .../cyclonedxhelpers/description_test.go | 56 +++++++ .../cyclonedxhelpers/external_references.go | 65 +++++++++ .../external_references_test.go | 133 +++++++++++++++++ .../formats/common/cyclonedxhelpers/format.go | 83 +++++++---- .../common/cyclonedxhelpers/licenses.go | 24 +++ .../common/cyclonedxhelpers/licenses_test.go | 83 +++++++++++ .../common/cyclonedxhelpers/properties.go | 68 +++++++++ .../cyclonedxhelpers/properties_test.go | 138 ++++++++++++++++++ .../common/cyclonedxhelpers/publisher.go | 19 +++ .../common/cyclonedxhelpers/publisher_test.go | 65 +++++++++ .../formats/cyclonedx13json/encoder_test.go | 4 +- .../TestCycloneDxDirectoryEncoder.golden | 71 ++++++++- .../snapshot/TestCycloneDxImageEncoder.golden | 79 +++++++++- .../stereoscope-fixture-image-simple.golden | Bin 15360 -> 15360 bytes .../formats/cyclonedx13xml/encoder_test.go | 3 +- .../TestCycloneDxDirectoryEncoder.golden | 30 +++- .../snapshot/TestCycloneDxImageEncoder.golden | 32 +++- .../stereoscope-fixture-image-simple.golden | Bin 15360 -> 15360 bytes syft/linux/release.go | 12 +- syft/pkg/apk_metadata.go | 12 +- syft/pkg/dpkg_metadata.go | 6 +- syft/pkg/golang_bin_metadata.go | 6 +- syft/pkg/package.go | 12 +- syft/pkg/rpmdb_metadata.go | 8 +- syft/source/coordinates.go | 4 +- 31 files changed, 1163 insertions(+), 79 deletions(-) create mode 100644 internal/formats/common/cyclonedxhelpers/author.go create mode 100644 internal/formats/common/cyclonedxhelpers/author_test.go create mode 100644 internal/formats/common/cyclonedxhelpers/component.go create mode 100644 internal/formats/common/cyclonedxhelpers/cpe.go create mode 100644 internal/formats/common/cyclonedxhelpers/cpe_test.go create mode 100644 internal/formats/common/cyclonedxhelpers/description.go create mode 100644 internal/formats/common/cyclonedxhelpers/description_test.go create mode 100644 internal/formats/common/cyclonedxhelpers/external_references.go create mode 100644 internal/formats/common/cyclonedxhelpers/external_references_test.go create mode 100644 internal/formats/common/cyclonedxhelpers/licenses.go create mode 100644 internal/formats/common/cyclonedxhelpers/licenses_test.go create mode 100644 internal/formats/common/cyclonedxhelpers/properties.go create mode 100644 internal/formats/common/cyclonedxhelpers/properties_test.go create mode 100644 internal/formats/common/cyclonedxhelpers/publisher.go create mode 100644 internal/formats/common/cyclonedxhelpers/publisher_test.go diff --git a/internal/formats/common/cyclonedxhelpers/author.go b/internal/formats/common/cyclonedxhelpers/author.go new file mode 100644 index 000000000..9f2cff9e6 --- /dev/null +++ b/internal/formats/common/cyclonedxhelpers/author.go @@ -0,0 +1,32 @@ +package cyclonedxhelpers + +import ( + "fmt" + "strings" + + "github.com/anchore/syft/syft/pkg" +) + +func Author(p pkg.Package) string { + if hasMetadata(p) { + switch metadata := p.Metadata.(type) { + case pkg.NpmPackageJSONMetadata: + return metadata.Author + case pkg.PythonPackageMetadata: + author := metadata.Author + if metadata.AuthorEmail != "" { + if author == "" { + return metadata.AuthorEmail + } + author += fmt.Sprintf(" <%s>", metadata.AuthorEmail) + } + return author + case pkg.GemMetadata: + if len(metadata.Authors) > 0 { + return strings.Join(metadata.Authors, ",") + } + return "" + } + } + return "" +} diff --git a/internal/formats/common/cyclonedxhelpers/author_test.go b/internal/formats/common/cyclonedxhelpers/author_test.go new file mode 100644 index 000000000..6da4e77f6 --- /dev/null +++ b/internal/formats/common/cyclonedxhelpers/author_test.go @@ -0,0 +1,87 @@ +package cyclonedxhelpers + +import ( + "testing" + + "github.com/anchore/syft/syft/pkg" + "github.com/stretchr/testify/assert" +) + +func Test_Author(t *testing.T) { + tests := []struct { + name string + input pkg.Package + expected string + }{ + { + // note: since this is an optional field, no value is preferred over NONE or NOASSERTION + name: "no metadata", + input: pkg.Package{}, + expected: "", + }, + { + name: "from gem", + input: pkg.Package{ + Metadata: pkg.GemMetadata{ + Authors: []string{ + "auth1", + "auth2", + }, + }, + }, + expected: "auth1,auth2", + }, + { + name: "from npm", + input: pkg.Package{ + Metadata: pkg.NpmPackageJSONMetadata{ + Author: "auth", + }, + }, + expected: "auth", + }, + { + name: "from python - just name", + input: pkg.Package{ + Metadata: pkg.PythonPackageMetadata{ + Author: "auth", + }, + }, + expected: "auth", + }, + { + name: "from python - just email", + input: pkg.Package{ + Metadata: pkg.PythonPackageMetadata{ + AuthorEmail: "auth@auth.gov", + }, + }, + expected: "auth@auth.gov", + }, + { + name: "from python - both name and email", + input: pkg.Package{ + Metadata: pkg.PythonPackageMetadata{ + Author: "auth", + AuthorEmail: "auth@auth.gov", + }, + }, + expected: "auth ", + }, + { + // note: since this is an optional field, no value is preferred over NONE or NOASSERTION + name: "empty", + input: pkg.Package{ + Metadata: pkg.NpmPackageJSONMetadata{ + Author: "", + }, + }, + expected: "", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.expected, Author(test.input)) + }) + } +} diff --git a/internal/formats/common/cyclonedxhelpers/component.go b/internal/formats/common/cyclonedxhelpers/component.go new file mode 100644 index 000000000..5ff83aa9d --- /dev/null +++ b/internal/formats/common/cyclonedxhelpers/component.go @@ -0,0 +1,26 @@ +package cyclonedxhelpers + +import ( + "github.com/CycloneDX/cyclonedx-go" + "github.com/anchore/syft/syft/pkg" +) + +func Component(p pkg.Package) cyclonedx.Component { + return cyclonedx.Component{ + Type: cyclonedx.ComponentTypeLibrary, + Name: p.Name, + Version: p.Version, + PackageURL: p.PURL, + Licenses: Licenses(p), + CPE: CPE(p), + Author: Author(p), + Publisher: Publisher(p), + Description: Description(p), + ExternalReferences: ExternalReferences(p), + Properties: Properties(p), + } +} + +func hasMetadata(p pkg.Package) bool { + return p.Metadata != nil +} diff --git a/internal/formats/common/cyclonedxhelpers/cpe.go b/internal/formats/common/cyclonedxhelpers/cpe.go new file mode 100644 index 000000000..6b43f847e --- /dev/null +++ b/internal/formats/common/cyclonedxhelpers/cpe.go @@ -0,0 +1,12 @@ +package cyclonedxhelpers + +import "github.com/anchore/syft/syft/pkg" + +func CPE(p pkg.Package) string { + // Since the CPEs in a package are sorted by specificity + // we can extract the first CPE as the one to output in cyclonedx + if len(p.CPEs) > 0 { + return pkg.CPEString(p.CPEs[0]) + } + return "" +} diff --git a/internal/formats/common/cyclonedxhelpers/cpe_test.go b/internal/formats/common/cyclonedxhelpers/cpe_test.go new file mode 100644 index 000000000..81d6f6f07 --- /dev/null +++ b/internal/formats/common/cyclonedxhelpers/cpe_test.go @@ -0,0 +1,57 @@ +package cyclonedxhelpers + +import ( + "testing" + + "github.com/anchore/syft/syft/pkg" + "github.com/stretchr/testify/assert" +) + +func Test_CPE(t *testing.T) { + testCPE := pkg.MustCPE("cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*") + testCPE2 := pkg.MustCPE("cpe:2.3:a:name:name2:3.2:*:*:*:*:*:*:*") + tests := []struct { + name string + input pkg.Package + expected string + }{ + { + // note: since this is an optional field, no value is preferred over NONE or NOASSERTION + name: "no metadata", + input: pkg.Package{ + CPEs: []pkg.CPE{}, + }, + expected: "", + }, + { + name: "single CPE", + input: pkg.Package{ + CPEs: []pkg.CPE{ + testCPE, + }, + }, + expected: "cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*", + }, + { + name: "multiple CPEs", + input: pkg.Package{ + CPEs: []pkg.CPE{ + testCPE2, + testCPE, + }, + }, + expected: "cpe:2.3:a:name:name2:3.2:*:*:*:*:*:*:*", + }, + { + // note: since this is an optional field, no value is preferred over NONE or NOASSERTION + name: "empty", + input: pkg.Package{}, + expected: "", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.expected, CPE(test.input)) + }) + } +} diff --git a/internal/formats/common/cyclonedxhelpers/description.go b/internal/formats/common/cyclonedxhelpers/description.go new file mode 100644 index 000000000..176c01989 --- /dev/null +++ b/internal/formats/common/cyclonedxhelpers/description.go @@ -0,0 +1,15 @@ +package cyclonedxhelpers + +import "github.com/anchore/syft/syft/pkg" + +func Description(p pkg.Package) string { + if hasMetadata(p) { + switch metadata := p.Metadata.(type) { + case pkg.ApkMetadata: + return metadata.Description + case pkg.NpmPackageJSONMetadata: + return metadata.Description + } + } + return "" +} diff --git a/internal/formats/common/cyclonedxhelpers/description_test.go b/internal/formats/common/cyclonedxhelpers/description_test.go new file mode 100644 index 000000000..0b8dec874 --- /dev/null +++ b/internal/formats/common/cyclonedxhelpers/description_test.go @@ -0,0 +1,56 @@ +package cyclonedxhelpers + +import ( + "testing" + + "github.com/anchore/syft/syft/pkg" + "github.com/stretchr/testify/assert" +) + +func Test_Description(t *testing.T) { + tests := []struct { + name string + input pkg.Package + expected string + }{ + { + // note: since this is an optional field, no value is preferred over NONE or NOASSERTION + name: "no metadata", + input: pkg.Package{}, + expected: "", + }, + { + name: "from apk", + input: pkg.Package{ + Metadata: pkg.ApkMetadata{ + Description: "a description!", + }, + }, + expected: "a description!", + }, + { + name: "from npm", + input: pkg.Package{ + Metadata: pkg.NpmPackageJSONMetadata{ + Description: "a description!", + }, + }, + expected: "a description!", + }, + { + // note: since this is an optional field, no value is preferred over NONE or NOASSERTION + name: "empty", + input: pkg.Package{ + Metadata: pkg.NpmPackageJSONMetadata{ + Homepage: "", + }, + }, + expected: "", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.expected, Description(test.input)) + }) + } +} diff --git a/internal/formats/common/cyclonedxhelpers/external_references.go b/internal/formats/common/cyclonedxhelpers/external_references.go new file mode 100644 index 000000000..f109ad199 --- /dev/null +++ b/internal/formats/common/cyclonedxhelpers/external_references.go @@ -0,0 +1,65 @@ +package cyclonedxhelpers + +import ( + "fmt" + + "github.com/CycloneDX/cyclonedx-go" + "github.com/anchore/syft/syft/pkg" +) + +func ExternalReferences(p pkg.Package) *[]cyclonedx.ExternalReference { + refs := []cyclonedx.ExternalReference{} + if hasMetadata(p) { + switch metadata := p.Metadata.(type) { + case pkg.ApkMetadata: + if metadata.URL != "" { + refs = append(refs, cyclonedx.ExternalReference{ + URL: metadata.URL, + Type: cyclonedx.ERTypeDistribution, + }) + } + case pkg.CargoPackageMetadata: + if metadata.Source != "" { + refs = append(refs, cyclonedx.ExternalReference{ + URL: metadata.Source, + Type: cyclonedx.ERTypeDistribution, + }) + } + case pkg.NpmPackageJSONMetadata: + if metadata.URL != "" { + refs = append(refs, cyclonedx.ExternalReference{ + URL: metadata.URL, + Type: cyclonedx.ERTypeDistribution, + }) + } + if metadata.Homepage != "" { + refs = append(refs, cyclonedx.ExternalReference{ + URL: metadata.Homepage, + Type: cyclonedx.ERTypeWebsite, + }) + } + case pkg.GemMetadata: + if metadata.Homepage != "" { + refs = append(refs, cyclonedx.ExternalReference{ + URL: metadata.Homepage, + Type: cyclonedx.ERTypeWebsite, + }) + } + case pkg.PythonPackageMetadata: + if metadata.DirectURLOrigin != nil && metadata.DirectURLOrigin.URL != "" { + ref := cyclonedx.ExternalReference{ + URL: metadata.DirectURLOrigin.URL, + Type: cyclonedx.ERTypeVCS, + } + if metadata.DirectURLOrigin.CommitID != "" { + ref.Comment = fmt.Sprintf("commit: %s", metadata.DirectURLOrigin.CommitID) + } + refs = append(refs, ref) + } + } + } + if len(refs) > 0 { + return &refs + } + return nil +} diff --git a/internal/formats/common/cyclonedxhelpers/external_references_test.go b/internal/formats/common/cyclonedxhelpers/external_references_test.go new file mode 100644 index 000000000..9f22f75cd --- /dev/null +++ b/internal/formats/common/cyclonedxhelpers/external_references_test.go @@ -0,0 +1,133 @@ +package cyclonedxhelpers + +import ( + "testing" + + "github.com/CycloneDX/cyclonedx-go" + "github.com/anchore/syft/syft/pkg" + "github.com/stretchr/testify/assert" +) + +func Test_ExternalReferences(t *testing.T) { + tests := []struct { + name string + input pkg.Package + expected *[]cyclonedx.ExternalReference + }{ + { + name: "no metadata", + input: pkg.Package{}, + expected: nil, + }, + { + name: "from apk", + input: pkg.Package{ + Metadata: pkg.ApkMetadata{ + URL: "http://a-place.gov", + }, + }, + expected: &[]cyclonedx.ExternalReference{ + {URL: "http://a-place.gov", Type: cyclonedx.ERTypeDistribution}, + }, + }, + { + name: "from npm", + input: pkg.Package{ + Metadata: pkg.NpmPackageJSONMetadata{ + URL: "http://a-place.gov", + }, + }, + expected: &[]cyclonedx.ExternalReference{ + {URL: "http://a-place.gov", Type: cyclonedx.ERTypeDistribution}, + }, + }, + { + name: "from cargo lock", + input: pkg.Package{ + Name: "ansi_term", + Version: "0.12.1", + Language: pkg.Rust, + Type: pkg.RustPkg, + MetadataType: pkg.RustCargoPackageMetadataType, + Licenses: nil, + Metadata: pkg.CargoPackageMetadata{ + Name: "ansi_term", + Version: "0.12.1", + Source: "registry+https://github.com/rust-lang/crates.io-index", + Checksum: "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2", + Dependencies: []string{ + "winapi", + }, + }, + }, + expected: &[]cyclonedx.ExternalReference{ + {URL: "registry+https://github.com/rust-lang/crates.io-index", Type: cyclonedx.ERTypeDistribution}, + }, + }, + { + name: "from npm with homepage", + input: pkg.Package{ + Metadata: pkg.NpmPackageJSONMetadata{ + URL: "http://a-place.gov", + Homepage: "http://homepage", + }, + }, + expected: &[]cyclonedx.ExternalReference{ + {URL: "http://a-place.gov", Type: cyclonedx.ERTypeDistribution}, + {URL: "http://homepage", Type: cyclonedx.ERTypeWebsite}, + }, + }, + { + name: "from gem", + input: pkg.Package{ + Metadata: pkg.GemMetadata{ + Homepage: "http://a-place.gov", + }, + }, + expected: &[]cyclonedx.ExternalReference{ + {URL: "http://a-place.gov", Type: cyclonedx.ERTypeWebsite}, + }, + }, + { + name: "from python direct url", + input: pkg.Package{ + Metadata: pkg.PythonPackageMetadata{ + DirectURLOrigin: &pkg.PythonDirectURLOriginInfo{ + URL: "http://a-place.gov", + }, + }, + }, + expected: &[]cyclonedx.ExternalReference{ + {URL: "http://a-place.gov", Type: cyclonedx.ERTypeVCS}, + }, + }, + { + name: "from python direct url with commit", + input: pkg.Package{ + Metadata: pkg.PythonPackageMetadata{ + DirectURLOrigin: &pkg.PythonDirectURLOriginInfo{ + URL: "http://a-place.gov", + CommitID: "test", + }, + }, + }, + expected: &[]cyclonedx.ExternalReference{ + {URL: "http://a-place.gov", Type: cyclonedx.ERTypeVCS, Comment: "commit: test"}, + }, + }, + { + name: "empty", + input: pkg.Package{ + Metadata: pkg.NpmPackageJSONMetadata{ + URL: "", + }, + }, + expected: nil, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.expected, ExternalReferences(test.input)) + }) + } +} diff --git a/internal/formats/common/cyclonedxhelpers/format.go b/internal/formats/common/cyclonedxhelpers/format.go index 96397af7f..b472556be 100644 --- a/internal/formats/common/cyclonedxhelpers/format.go +++ b/internal/formats/common/cyclonedxhelpers/format.go @@ -6,7 +6,7 @@ import ( "github.com/CycloneDX/cyclonedx-go" "github.com/anchore/syft/internal" "github.com/anchore/syft/internal/version" - "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/linux" "github.com/anchore/syft/syft/sbom" "github.com/anchore/syft/syft/source" "github.com/google/uuid" @@ -25,13 +25,63 @@ func ToFormatModel(s sbom.SBOM) *cyclonedx.BOM { packages := s.Artifacts.PackageCatalog.Sorted() components := make([]cyclonedx.Component, len(packages)) for i, p := range packages { - components[i] = toComponent(p) + components[i] = Component(p) } + components = append(components, toOSComponent(s.Artifacts.LinuxDistribution)...) cdxBOM.Components = &components - return cdxBOM } +func toOSComponent(distro *linux.Release) []cyclonedx.Component { + if distro == nil { + return []cyclonedx.Component{} + } + eRefs := &[]cyclonedx.ExternalReference{} + if distro.BugReportURL != "" { + *eRefs = append(*eRefs, cyclonedx.ExternalReference{ + URL: distro.BugReportURL, + Type: cyclonedx.ERTypeIssueTracker, + }) + } + if distro.HomeURL != "" { + *eRefs = append(*eRefs, cyclonedx.ExternalReference{ + URL: distro.HomeURL, + Type: cyclonedx.ERTypeWebsite, + }) + } + if distro.SupportURL != "" { + *eRefs = append(*eRefs, cyclonedx.ExternalReference{ + URL: distro.SupportURL, + Type: cyclonedx.ERTypeOther, + Comment: "support", + }) + } + if distro.PrivacyPolicyURL != "" { + *eRefs = append(*eRefs, cyclonedx.ExternalReference{ + URL: distro.PrivacyPolicyURL, + Type: cyclonedx.ERTypeOther, + Comment: "privacyPolicy", + }) + } + if len(*eRefs) == 0 { + eRefs = nil + } + props := getCycloneDXProperties(*distro) + if len(*props) == 0 { + props = nil + } + return []cyclonedx.Component{ + { + Type: cyclonedx.ComponentTypeOS, + Name: distro.Name, + Version: distro.Version, + CPE: distro.CPEName, + ExternalReferences: eRefs, + Properties: props, + }, + } +} + // NewBomDescriptor returns a new BomDescriptor tailored for the current time and "syft" tool details. func toBomDescriptor(name, version string, srcMetadata source.Metadata) *cyclonedx.Metadata { return &cyclonedx.Metadata{ @@ -47,16 +97,6 @@ func toBomDescriptor(name, version string, srcMetadata source.Metadata) *cyclone } } -func toComponent(p pkg.Package) cyclonedx.Component { - return cyclonedx.Component{ - Type: cyclonedx.ComponentTypeLibrary, - Name: p.Name, - Version: p.Version, - PackageURL: p.PURL, - Licenses: toLicenses(p.Licenses), - } -} - func toBomDescriptorComponent(srcMetadata source.Metadata) *cyclonedx.Component { switch srcMetadata.Scheme { case source.ImageScheme: @@ -74,20 +114,3 @@ func toBomDescriptorComponent(srcMetadata source.Metadata) *cyclonedx.Component return nil } - -func toLicenses(ls []string) *cyclonedx.Licenses { - if len(ls) == 0 { - return nil - } - - lc := make(cyclonedx.Licenses, len(ls)) - for i, licenseName := range ls { - lc[i] = cyclonedx.LicenseChoice{ - License: &cyclonedx.License{ - Name: licenseName, - }, - } - } - - return &lc -} diff --git a/internal/formats/common/cyclonedxhelpers/licenses.go b/internal/formats/common/cyclonedxhelpers/licenses.go new file mode 100644 index 000000000..c1e2c44a2 --- /dev/null +++ b/internal/formats/common/cyclonedxhelpers/licenses.go @@ -0,0 +1,24 @@ +package cyclonedxhelpers + +import ( + "github.com/CycloneDX/cyclonedx-go" + "github.com/anchore/syft/internal/spdxlicense" + "github.com/anchore/syft/syft/pkg" +) + +func Licenses(p pkg.Package) *cyclonedx.Licenses { + lc := cyclonedx.Licenses{} + for _, licenseName := range p.Licenses { + if value, exists := spdxlicense.ID(licenseName); exists { + lc = append(lc, cyclonedx.LicenseChoice{ + License: &cyclonedx.License{ + ID: value, + }, + }) + } + } + if len(lc) > 0 { + return &lc + } + return nil +} diff --git a/internal/formats/common/cyclonedxhelpers/licenses_test.go b/internal/formats/common/cyclonedxhelpers/licenses_test.go new file mode 100644 index 000000000..41839a0d1 --- /dev/null +++ b/internal/formats/common/cyclonedxhelpers/licenses_test.go @@ -0,0 +1,83 @@ +package cyclonedxhelpers + +import ( + "testing" + + "github.com/CycloneDX/cyclonedx-go" + "github.com/anchore/syft/syft/pkg" + "github.com/stretchr/testify/assert" +) + +func Test_License(t *testing.T) { + tests := []struct { + name string + input pkg.Package + expected *cyclonedx.Licenses + }{ + { + name: "no licenses", + input: pkg.Package{}, + expected: nil, + }, + { + name: "no SPDX licenses", + input: pkg.Package{ + Licenses: []string{ + "made-up", + }, + }, + expected: nil, + }, + { + name: "with SPDX license", + input: pkg.Package{ + Licenses: []string{ + "MIT", + }, + }, + expected: &cyclonedx.Licenses{ + {License: &cyclonedx.License{ID: "MIT"}}, + }, + }, + { + name: "with SPDX license expression", + input: pkg.Package{ + Licenses: []string{ + "MIT", + "GPL-3.0", + }, + }, + expected: &cyclonedx.Licenses{ + {License: &cyclonedx.License{ID: "MIT"}}, + {License: &cyclonedx.License{ID: "GPL-3.0"}}, + }, + }, + { + name: "cap insensitive", + input: pkg.Package{ + Licenses: []string{ + "gpl-3.0", + }, + }, + expected: &cyclonedx.Licenses{ + {License: &cyclonedx.License{ID: "GPL-3.0"}}, + }, + }, + { + name: "debian to spdx conversion", + input: pkg.Package{ + Licenses: []string{ + "GPL-2", + }, + }, + expected: &cyclonedx.Licenses{ + {License: &cyclonedx.License{ID: "GPL-2.0"}}, + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.expected, Licenses(test.input)) + }) + } +} diff --git a/internal/formats/common/cyclonedxhelpers/properties.go b/internal/formats/common/cyclonedxhelpers/properties.go new file mode 100644 index 000000000..5e5b782ca --- /dev/null +++ b/internal/formats/common/cyclonedxhelpers/properties.go @@ -0,0 +1,68 @@ +package cyclonedxhelpers + +import ( + "fmt" + "reflect" + + "github.com/CycloneDX/cyclonedx-go" + "github.com/anchore/syft/syft/pkg" +) + +func Properties(p pkg.Package) *[]cyclonedx.Property { + props := []cyclonedx.Property{} + props = append(props, *getCycloneDXProperties(p)...) + if len(p.Locations) > 0 { + for _, l := range p.Locations { + props = append(props, *getCycloneDXProperties(l.Coordinates)...) + } + } + if hasMetadata(p) { + props = append(props, *getCycloneDXProperties(p.Metadata)...) + } + if len(props) > 0 { + return &props + } + return nil +} + +func getCycloneDXProperties(m interface{}) *[]cyclonedx.Property { + props := []cyclonedx.Property{} + structValue := reflect.ValueOf(m) + // we can only handle top level structs as interfaces for now + if structValue.Kind() != reflect.Struct { + return &props + } + structType := structValue.Type() + for i := 0; i < structValue.NumField(); i++ { + if name, value := getCycloneDXPropertyName(structType.Field(i)), getCycloneDXPropertyValue(structValue.Field(i)); name != "" && value != "" { + props = append(props, cyclonedx.Property{ + Name: name, + Value: value, + }) + } + } + return &props +} + +func getCycloneDXPropertyName(field reflect.StructField) string { + if value, exists := field.Tag.Lookup("cyclonedx"); exists { + return value + } + return "" +} + +func getCycloneDXPropertyValue(field reflect.Value) string { + if field.IsZero() { + return "" + } + switch field.Kind() { + case reflect.String, reflect.Bool, reflect.Int, reflect.Float32, reflect.Float64, reflect.Complex128, reflect.Complex64: + if field.CanInterface() { + return fmt.Sprint(field.Interface()) + } + return "" + case reflect.Ptr: + return getCycloneDXPropertyValue(reflect.Indirect(field)) + } + return "" +} diff --git a/internal/formats/common/cyclonedxhelpers/properties_test.go b/internal/formats/common/cyclonedxhelpers/properties_test.go new file mode 100644 index 000000000..96a5a4c47 --- /dev/null +++ b/internal/formats/common/cyclonedxhelpers/properties_test.go @@ -0,0 +1,138 @@ +package cyclonedxhelpers + +import ( + "testing" + + "github.com/CycloneDX/cyclonedx-go" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/source" + "github.com/stretchr/testify/assert" +) + +func Test_Properties(t *testing.T) { + epoch := 2 + tests := []struct { + name string + input pkg.Package + expected *[]cyclonedx.Property + }{ + { + name: "no metadata", + input: pkg.Package{}, + expected: nil, + }, + { + name: "from apk", + input: pkg.Package{ + FoundBy: "cataloger", + Locations: []source.Location{ + {Coordinates: source.Coordinates{RealPath: "test"}}, + }, + Metadata: pkg.ApkMetadata{ + Package: "libc-utils", + OriginPackage: "libc-dev", + Maintainer: "Natanael Copa ", + Version: "0.7.2-r0", + License: "BSD", + Architecture: "x86_64", + URL: "http://alpinelinux.org", + Description: "Meta package to pull in correct libc", + Size: 0, + InstalledSize: 4096, + PullDependencies: "musl-utils", + PullChecksum: "Q1p78yvTLG094tHE1+dToJGbmYzQE=", + GitCommitOfAport: "97b1c2842faa3bfa30f5811ffbf16d5ff9f1a479", + Files: []pkg.ApkFileRecord{}, + }, + }, + expected: &[]cyclonedx.Property{ + {Name: "foundBy", Value: "cataloger"}, + {Name: "path", Value: "test"}, + {Name: "originPackage", Value: "libc-dev"}, + {Name: "installedSize", Value: "4096"}, + {Name: "pullDependencies", Value: "musl-utils"}, + {Name: "pullChecksum", Value: "Q1p78yvTLG094tHE1+dToJGbmYzQE="}, + {Name: "gitCommitOfApkPort", Value: "97b1c2842faa3bfa30f5811ffbf16d5ff9f1a479"}, + }, + }, + { + name: "from dpkg", + input: pkg.Package{ + MetadataType: pkg.DpkgMetadataType, + Metadata: pkg.DpkgMetadata{ + Package: "tzdata", + Version: "2020a-0+deb10u1", + Source: "tzdata-dev", + SourceVersion: "1.0", + Architecture: "all", + InstalledSize: 3036, + Maintainer: "GNU Libc Maintainers ", + Files: []pkg.DpkgFileRecord{}, + }, + }, + expected: &[]cyclonedx.Property{ + {Name: "metadataType", Value: "DpkgMetadata"}, + {Name: "source", Value: "tzdata-dev"}, + {Name: "sourceVersion", Value: "1.0"}, + {Name: "installedSize", Value: "3036"}, + }, + }, + { + name: "from go bin", + input: pkg.Package{ + Name: "golang.org/x/net", + Version: "v0.0.0-20211006190231-62292e806868", + Language: pkg.Go, + Type: pkg.GoModulePkg, + MetadataType: pkg.GolangBinMetadataType, + Metadata: pkg.GolangBinMetadata{ + GoCompiledVersion: "1.17", + Architecture: "amd64", + H1Digest: "h1:KlOXYy8wQWTUJYFgkUI40Lzr06ofg5IRXUK5C7qZt1k=", + }, + }, + expected: &[]cyclonedx.Property{ + {Name: "language", Value: pkg.Go.String()}, + {Name: "type", Value: "go-module"}, + {Name: "metadataType", Value: "GolangBinMetadata"}, + {Name: "goCompiledVersion", Value: "1.17"}, + {Name: "architecture", Value: "amd64"}, + {Name: "h1Digest", Value: "h1:KlOXYy8wQWTUJYFgkUI40Lzr06ofg5IRXUK5C7qZt1k="}, + }, + }, + { + name: "from rpm", + input: pkg.Package{ + Name: "dive", + Version: "0.9.2-1", + Type: pkg.RpmPkg, + MetadataType: pkg.RpmdbMetadataType, + Metadata: pkg.RpmdbMetadata{ + Name: "dive", + Epoch: &epoch, + Arch: "x86_64", + Release: "1", + Version: "0.9.2", + SourceRpm: "dive-0.9.2-1.src.rpm", + Size: 12406784, + License: "MIT", + Vendor: "", + Files: []pkg.RpmdbFileRecord{}, + }, + }, + expected: &[]cyclonedx.Property{ + {Name: "type", Value: "rpm"}, + {Name: "metadataType", Value: "RpmdbMetadata"}, + {Name: "epoch", Value: "2"}, + {Name: "release", Value: "1"}, + {Name: "sourceRpm", Value: "dive-0.9.2-1.src.rpm"}, + {Name: "size", Value: "12406784"}, + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.expected, Properties(test.input)) + }) + } +} diff --git a/internal/formats/common/cyclonedxhelpers/publisher.go b/internal/formats/common/cyclonedxhelpers/publisher.go new file mode 100644 index 000000000..71e64d5c4 --- /dev/null +++ b/internal/formats/common/cyclonedxhelpers/publisher.go @@ -0,0 +1,19 @@ +package cyclonedxhelpers + +import ( + "github.com/anchore/syft/syft/pkg" +) + +func Publisher(p pkg.Package) string { + if hasMetadata(p) { + switch metadata := p.Metadata.(type) { + case pkg.ApkMetadata: + return metadata.Maintainer + case pkg.RpmdbMetadata: + return metadata.Vendor + case pkg.DpkgMetadata: + return metadata.Maintainer + } + } + return "" +} diff --git a/internal/formats/common/cyclonedxhelpers/publisher_test.go b/internal/formats/common/cyclonedxhelpers/publisher_test.go new file mode 100644 index 000000000..d1f68c154 --- /dev/null +++ b/internal/formats/common/cyclonedxhelpers/publisher_test.go @@ -0,0 +1,65 @@ +package cyclonedxhelpers + +import ( + "testing" + + "github.com/anchore/syft/syft/pkg" + "github.com/stretchr/testify/assert" +) + +func Test_Publisher(t *testing.T) { + tests := []struct { + name string + input pkg.Package + expected string + }{ + { + // note: since this is an optional field, no value is preferred over NONE or NOASSERTION + name: "no metadata", + input: pkg.Package{}, + expected: "", + }, + { + name: "from apk", + input: pkg.Package{ + Metadata: pkg.ApkMetadata{ + Maintainer: "auth", + }, + }, + expected: "auth", + }, + { + name: "from rpm", + input: pkg.Package{ + Metadata: pkg.RpmdbMetadata{ + Vendor: "auth", + }, + }, + expected: "auth", + }, + { + name: "from dpkg", + input: pkg.Package{ + Metadata: pkg.DpkgMetadata{ + Maintainer: "auth", + }, + }, + expected: "auth", + }, + { + // note: since this is an optional field, no value is preferred over NONE or NOASSERTION + name: "empty", + input: pkg.Package{ + Metadata: pkg.NpmPackageJSONMetadata{ + Author: "", + }, + }, + expected: "", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.expected, Publisher(test.input)) + }) + } +} diff --git a/internal/formats/cyclonedx13json/encoder_test.go b/internal/formats/cyclonedx13json/encoder_test.go index b8c8f9edb..22361e889 100644 --- a/internal/formats/cyclonedx13json/encoder_test.go +++ b/internal/formats/cyclonedx13json/encoder_test.go @@ -33,8 +33,8 @@ func TestCycloneDxImageEncoder(t *testing.T) { func cycloneDxRedactor(s []byte) []byte { serialPattern := regexp.MustCompile(`urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}`) rfc3339Pattern := regexp.MustCompile(`([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(([Zz])|([\+|\-]([01][0-9]|2[0-3]):[0-5][0-9]))`) - - for _, pattern := range []*regexp.Regexp{serialPattern, rfc3339Pattern} { + sha256Pattern := regexp.MustCompile(`sha256:[A-Fa-f0-9]{64}`) + for _, pattern := range []*regexp.Regexp{serialPattern, rfc3339Pattern, sha256Pattern} { s = pattern.ReplaceAll(s, []byte("redacted")) } return s diff --git a/internal/formats/cyclonedx13json/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden b/internal/formats/cyclonedx13json/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden index 76b0c7a15..e76154bc9 100644 --- a/internal/formats/cyclonedx13json/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden +++ b/internal/formats/cyclonedx13json/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden @@ -1,10 +1,10 @@ { "bomFormat": "CycloneDX", "specVersion": "1.3", - "serialNumber": "urn:uuid:a81dc685-cf22-48e0-bda5-65ea1a8bca5b", + "serialNumber": "urn:uuid:258d2616-5b1f-48cd-82a3-d6c95e262950", "version": 1, "metadata": { - "timestamp": "2021-12-03T13:17:26-08:00", + "timestamp": "2022-01-14T22:47:00Z", "tools": [ { "vendor": "anchore", @@ -26,17 +26,78 @@ "licenses": [ { "license": { - "name": "MIT" + "id": "MIT" } } ], - "purl": "a-purl-2" + "cpe": "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*", + "purl": "a-purl-2", + "properties": [ + { + "name": "foundBy", + "value": "the-cataloger-1" + }, + { + "name": "language", + "value": "python" + }, + { + "name": "type", + "value": "python" + }, + { + "name": "metadataType", + "value": "PythonPackageMetadata" + }, + { + "name": "path", + "value": "/some/path/pkg1" + } + ] }, { "type": "library", "name": "package-2", "version": "2.0.1", - "purl": "a-purl-2" + "cpe": "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*", + "purl": "a-purl-2", + "properties": [ + { + "name": "foundBy", + "value": "the-cataloger-2" + }, + { + "name": "type", + "value": "deb" + }, + { + "name": "metadataType", + "value": "DpkgMetadata" + }, + { + "name": "path", + "value": "/some/path/pkg1" + } + ] + }, + { + "type": "operating-system", + "name": "debian", + "version": "1.2.3", + "properties": [ + { + "name": "prettyName", + "value": "debian" + }, + { + "name": "id", + "value": "debian" + }, + { + "name": "versionID", + "value": "1.2.3" + } + ] } ] } diff --git a/internal/formats/cyclonedx13json/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden b/internal/formats/cyclonedx13json/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden index 8c17c2948..9505eb865 100644 --- a/internal/formats/cyclonedx13json/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden +++ b/internal/formats/cyclonedx13json/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden @@ -1,10 +1,10 @@ { "bomFormat": "CycloneDX", "specVersion": "1.3", - "serialNumber": "urn:uuid:2156ac1f-c838-4e93-8dc5-a3874ffeb967", + "serialNumber": "urn:uuid:8a84b1cf-e918-4842-a6a8-c7fdafc55bc0", "version": 1, "metadata": { - "timestamp": "2021-12-03T13:17:26-08:00", + "timestamp": "2022-01-14T22:47:00Z", "tools": [ { "vendor": "anchore", @@ -26,17 +26,86 @@ "licenses": [ { "license": { - "name": "MIT" + "id": "MIT" } } ], - "purl": "a-purl-1" + "cpe": "cpe:2.3:*:some:package:1:*:*:*:*:*:*:*", + "purl": "a-purl-1", + "properties": [ + { + "name": "foundBy", + "value": "the-cataloger-1" + }, + { + "name": "language", + "value": "python" + }, + { + "name": "type", + "value": "python" + }, + { + "name": "metadataType", + "value": "PythonPackageMetadata" + }, + { + "name": "path", + "value": "/somefile-1.txt" + }, + { + "name": "layerID", + "value": "sha256:16e64541f2ddf59a90391ce7bb8af90313f7d373f2105d88f3d3267b72e0ebab" + } + ] }, { "type": "library", "name": "package-2", "version": "2.0.1", - "purl": "a-purl-2" + "cpe": "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*", + "purl": "a-purl-2", + "properties": [ + { + "name": "foundBy", + "value": "the-cataloger-2" + }, + { + "name": "type", + "value": "deb" + }, + { + "name": "metadataType", + "value": "DpkgMetadata" + }, + { + "name": "path", + "value": "/somefile-2.txt" + }, + { + "name": "layerID", + "value": "sha256:de6c235f76ea24c8503ec08891445b5d6a8bdf8249117ed8d8b0b6fb3ebe4f67" + } + ] + }, + { + "type": "operating-system", + "name": "debian", + "version": "1.2.3", + "properties": [ + { + "name": "prettyName", + "value": "debian" + }, + { + "name": "id", + "value": "debian" + }, + { + "name": "versionID", + "value": "1.2.3" + } + ] } ] } diff --git a/internal/formats/cyclonedx13json/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden b/internal/formats/cyclonedx13json/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden index 3d93b6d3ad1c86098e440073bcf8cfdf5eea53e1..c483fa49b75941b2cfb6a6819ff94a681f25ee41 100644 GIT binary patch literal 15360 zcmeHOZExE)5ccQ&3Xl7m*nFpCU>~xkKnoO0(Pka6pa>|wBwB6BkmRC4kpI4u>^N?+ zMoXlq8A`!`M3Fq5?szos;UhwsawN7!<0N9G_Y)S?;cIte-{TyY9lK0QOTOY|x)t4uE(r~oYv^93}xDgRNrVqJ4kRoa}tuWXv3 znb-5u?V;_xd{y1O=?dt&X?a{-Y_E@&F8f+eQ##5FD6wh(GW(2X*=j!DLoXKaM(fMh z3w^dx^g}*hEnInB^JiXsO0%=WbffLdtST;+d76P<>%K?XZ>w~^74=3ZZocV$wTE)J zMDsLTeL+_(vD9~USI1B1`oa~HdJXl>qb2_N-Sr<)M1}bOra^nGDD#E$>D=v;N%f`b zQSgqa7XM>R<#ztZoQyCbip${t9TPs;Wp$pG<9Yy$&viMjr)ZZ|<&!-w2o4qq76=vy z7I-EWkXR;>QJOjuMLGc>kW#?ll#rO}NXN`L>ZPLE8N%UAW9PMHF&D;L?a342f1pFa z|ArVMo&Enb_#X}V{}A3EjYz=%hB5RreM;x}?~eb~8$a9pZ@vFR2n+cCAR}gj|8*S( z0NyQi0jXo(SR#eZ$9+O;ERoO|VKdGq91|839x%oGFvb4`#(IL@9vr{=b-F5x={z@j zJ}r|po9&dh#hZ%e*18Cn=;LER+9?d!hoZE)Fz0FIOtmU%7+5c?qS?n;^+;|qdNe}ryWq0_0 zkoJZ3-w>nc5`81Bxyc_JHcU^M+E+}OItlxV7z0$88x~PZ<`4VURbBwE=j5`b6XQ%W zP8+yj%~3i{pdUIoJZyHp*VKpcbh{L8s1V~{#z{@t`IJ_mrpXtJHM((qb7FJz$rbhb zY4RDPtGzZgc2SFeRijyf@2IgZKTr+GhIbOuJyhg*<$+#wiK>fbowhc7of2Njrrw{X z7V1t=d9JCDGa{Yjf)fuEZ!cmUW58LAQ%MqO4+X-!vP>~g2^KbrJhO~Sl_=`4OLPKK zH?k!Nsqw!zH3zYITz_;%c#rJBV1ZzP JV1eE(@DIZvjQs!r literal 15360 zcmeHOTW{Mo6wdR0g{Qs7Ht(bi?4fH4v_P>GZPo!Rih$xJ(P~SEBo_&S{P!Knj$?am z)kLz<3k3t>^@#iqk90mB8hfg2qL?y9Ag3+2)O%%-k1=6b3(TYz+QdS}i7?nu&ZWUT z<^=Ir$#{I!ej&t=cx7{l9(o`pt{eKfg3wgb@-9^t!xu?@&s&^-mBifTKZYoqTCfcXdCz{znHv`3~)H z|HcsLZ2$k3dDb$g7W>B>i(UH{6bJinfwhBff zwKA4_DW$hqdvGlzPZCF&N<{2pNC@I;XT!z@dL`Ox?m?C*oUU9wFoFmkVmw6j46zBr z6Q)L-63h|*r*4>Ml}f0eFS);y^yZ7$zb zHqD}m*VED+N9(eD0aMv6bOm%>6`obs>-6c|W!rL^+-YWD5UcjjvX9XuTTG|N(bE~s zY?*v9(-*6OzRjnLnJe#V{?3aJX?Agvu15PTtBUJ+p4JdA`<`ZhEYj&()JvVX>8kr} zgUq*&$Ghm&le6c)j~8Vzp5{hR$7Pacla2g3dtcEMEwhk}-aiJ=F|3S-BB~2>nO4qJ zi=tMUo>{>_Ob(bw(==OLMYp|}~ZU%O&d`_P}gTHgN*9KfCTKjs{T_y1=B z?XjZFXU?ZnHzcF#s_IejhNu?(BSaegQ;9GWk^uk1k^%oah-dnf`~OSb59;qIe7(zG zH`ErA0fB%(Kp-IS10x`Tomqxs=_#?)VM>uVf|g`oH}@hGqEvA7a_vM4zEG_k6lxu01ieEmm8Tuq_f>)7GzL{+DT4Awd*?=uU+9~{v|i>P=K#&uiM+@U;@A zWK-|YQww$PqViG`E+)n*2nQ(UF~QghZg6ZIY}gnM4tZiMF-|2q7KxxxOhrs%5o_3v zk+ES6age%_#E2zMb8LiCPFN!or?94oYm+#L23P`NCFhYLLOXb*@)lWU9AjR@MDXbS z?RQ9~4`Hx9Kcnkr-Cp8#YC1#Q$J_H}L;YXsiDs9O8fdgZdpw z>}>zt{eL6~-r4^p2nGNDAggE<|LeL8Y8QF!xdWR-Lj*B9e1_1Z`Gr{Bmtu7r9{Zua z-mZ$@PyB~SN&^2s#0y}X^zP@0?eU-H{Kr=OhX?)dM;SX(E7(VAc0mT0x?G2Wb{d6j zKp-Fx5C{nTa0twFmU>rKUlYf9?Bv_N|D4miGvW0K@#g#o4&VRB!u_Cr5IuRiIyo8j z5(@+-_}b0$Gkvj)EL5&=d1>;w8~XIBK07x|>qC#jGM&w*ZlWNvC?LRqlu9gw1(=sw z!P(Fl!UQZvUO+92oh06y82)^Mq_M(b<&h)A@T5Nf_p&~N);z91Is^0Q?`#}|Gv5F5 zAiW%_aRIt2Q;4LcuH4r%+w7m*|2OE){)Gtkzb|!c$-Rln77MLohn - + - 2021-12-03T13:16:45-08:00 + 2022-01-14T22:46:49Z anchore @@ -20,15 +20,39 @@ 1.0.1 - MIT + MIT + cpe:2.3:*:some:package:2:*:*:*:*:*:*:* a-purl-2 + + the-cataloger-1 + python + python + PythonPackageMetadata + /some/path/pkg1 + package-2 2.0.1 + cpe:2.3:*:some:package:2:*:*:*:*:*:*:* a-purl-2 + + the-cataloger-2 + deb + DpkgMetadata + /some/path/pkg1 + + + + debian + 1.2.3 + + debian + debian + 1.2.3 + \ No newline at end of file diff --git a/internal/formats/cyclonedx13xml/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden b/internal/formats/cyclonedx13xml/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden index d3531d0c9..584216018 100644 --- a/internal/formats/cyclonedx13xml/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden +++ b/internal/formats/cyclonedx13xml/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden @@ -1,7 +1,7 @@ - + - 2021-12-03T13:16:45-08:00 + 2022-01-14T22:46:49Z anchore @@ -20,15 +20,41 @@ 1.0.1 - MIT + MIT + cpe:2.3:*:some:package:1:*:*:*:*:*:*:* a-purl-1 + + the-cataloger-1 + python + python + PythonPackageMetadata + /somefile-1.txt + sha256:16e64541f2ddf59a90391ce7bb8af90313f7d373f2105d88f3d3267b72e0ebab + package-2 2.0.1 + cpe:2.3:*:some:package:2:*:*:*:*:*:*:* a-purl-2 + + the-cataloger-2 + deb + DpkgMetadata + /somefile-2.txt + sha256:de6c235f76ea24c8503ec08891445b5d6a8bdf8249117ed8d8b0b6fb3ebe4f67 + + + + debian + 1.2.3 + + debian + debian + 1.2.3 + \ No newline at end of file diff --git a/internal/formats/cyclonedx13xml/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden b/internal/formats/cyclonedx13xml/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden index 3d93b6d3ad1c86098e440073bcf8cfdf5eea53e1..c483fa49b75941b2cfb6a6819ff94a681f25ee41 100644 GIT binary patch literal 15360 zcmeHOZExE)5ccQ&3Xl7m*nFpCU>~xkKnoO0(Pka6pa>|wBwB6BkmRC4kpI4u>^N?+ zMoXlq8A`!`M3Fq5?szos;UhwsawN7!<0N9G_Y)S?;cIte-{TyY9lK0QOTOY|x)t4uE(r~oYv^93}xDgRNrVqJ4kRoa}tuWXv3 znb-5u?V;_xd{y1O=?dt&X?a{-Y_E@&F8f+eQ##5FD6wh(GW(2X*=j!DLoXKaM(fMh z3w^dx^g}*hEnInB^JiXsO0%=WbffLdtST;+d76P<>%K?XZ>w~^74=3ZZocV$wTE)J zMDsLTeL+_(vD9~USI1B1`oa~HdJXl>qb2_N-Sr<)M1}bOra^nGDD#E$>D=v;N%f`b zQSgqa7XM>R<#ztZoQyCbip${t9TPs;Wp$pG<9Yy$&viMjr)ZZ|<&!-w2o4qq76=vy z7I-EWkXR;>QJOjuMLGc>kW#?ll#rO}NXN`L>ZPLE8N%UAW9PMHF&D;L?a342f1pFa z|ArVMo&Enb_#X}V{}A3EjYz=%hB5RreM;x}?~eb~8$a9pZ@vFR2n+cCAR}gj|8*S( z0NyQi0jXo(SR#eZ$9+O;ERoO|VKdGq91|839x%oGFvb4`#(IL@9vr{=b-F5x={z@j zJ}r|po9&dh#hZ%e*18Cn=;LER+9?d!hoZE)Fz0FIOtmU%7+5c?qS?n;^+;|qdNe}ryWq0_0 zkoJZ3-w>nc5`81Bxyc_JHcU^M+E+}OItlxV7z0$88x~PZ<`4VURbBwE=j5`b6XQ%W zP8+yj%~3i{pdUIoJZyHp*VKpcbh{L8s1V~{#z{@t`IJ_mrpXtJHM((qb7FJz$rbhb zY4RDPtGzZgc2SFeRijyf@2IgZKTr+GhIbOuJyhg*<$+#wiK>fbowhc7of2Njrrw{X z7V1t=d9JCDGa{Yjf)fuEZ!cmUW58LAQ%MqO4+X-!vP>~g2^KbrJhO~Sl_=`4OLPKK zH?k!Nsqw!zH3zYITz_;%c#rJBV1ZzP JV1eE(@DIZvjQs!r literal 15360 zcmeHOTW{Mo6wdR0g{Qs7Ht(bi?4fH4v_P>GZPo!Rih$xJ(P~SEBo_&S{P!Knj$?am z)kLz<3k3t>^@#iqk90mB8hfg2qL?y9Ag3+2)O%%-k1=6b3(TYz+QdS}i7?nu&ZWUT z<^=Ir$#{I!ej&t=cx7{l9(o`pt{eKfg3wgb@-9^t!xu?@&s&^-mBifTKZYoqTCfcXdCz{znHv`3~)H z|HcsLZ2$k3dDb$g7W>B>i(UH{6bJinfwhBff zwKA4_DW$hqdvGlzPZCF&N<{2pNC@I;XT!z@dL`Ox?m?C*oUU9wFoFmkVmw6j46zBr z6Q)L-63h|*r*4>Ml}f0eFS);y^yZ7$zb zHqD}m*VED+N9(eD0aMv6bOm%>6`obs>-6c|W!rL^+-YWD5UcjjvX9XuTTG|N(bE~s zY?*v9(-*6OzRjnLnJe#V{?3aJX?Agvu15PTtBUJ+p4JdA`<`ZhEYj&()JvVX>8kr} zgUq*&$Ghm&le6c)j~8Vzp5{hR$7Pacla2g3dtcEMEwhk}-aiJ=F|3S-BB~2>nO4qJ zi=tMUo>{>_Ob(bw(==OLMYp|}~ZU%O&d`_P}gTHgN*9KfCTKjs{T_y1=B z?XjZFXU?ZnHzcF#s_IejhNu?(BSaegQ;9GWk^uk1k^%oah-dnf`~OSb59;qIe7(zG zH`ErA0fB%(Kp-IS10x`Tomqxs=_#?)VM>uVf|g`oH}@hGqEvA7a_vM4zEG_k6lxu01ieEmm8Tuq_f>)7GzL{+DT4Awd*?=uU+9~{v|i>P=K#&uiM+@U;@A zWK-|YQww$PqViG`E+)n*2nQ(UF~QghZg6ZIY}gnM4tZiMF-|2q7KxxxOhrs%5o_3v zk+ES6age%_#E2zMb8LiCPFN!or?94oYm+#L23P`NCFhYLLOXb*@)lWU9AjR@MDXbS z?RQ9~4`Hx9Kcnkr-Cp8#YC1#Q$J_H}L;YXsiDs9O8fdgZdpw z>}>zt{eL6~-r4^p2nGNDAggE<|LeL8Y8QF!xdWR-Lj*B9e1_1Z`Gr{Bmtu7r9{Zua z-mZ$@PyB~SN&^2s#0y}X^zP@0?eU-H{Kr=OhX?)dM;SX(E7(VAc0mT0x?G2Wb{d6j zKp-Fx5C{nTa0twFmU>rKUlYf9?Bv_N|D4miGvW0K@#g#o4&VRB!u_Cr5IuRiIyo8j z5(@+-_}b0$Gkvj)EL5&=d1>;w8~XIBK07x|>qC#jGM&w*ZlWNvC?LRqlu9gw1(=sw z!P(Fl!UQZvUO+92oh06y82)^Mq_M(b<&h)A@T5Nf_p&~N);z91Is^0Q?`#}|Gv5F5 zAiW%_aRIt2Q;4LcuH4r%+w7m*|2OE){)Gtkzb|!c$-Rln77MLohn