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 3d93b6d3a..c483fa49b 100644 Binary files a/internal/formats/cyclonedx13json/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden and b/internal/formats/cyclonedx13json/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden differ diff --git a/internal/formats/cyclonedx13xml/encoder_test.go b/internal/formats/cyclonedx13xml/encoder_test.go index 0635eaeb0..6c34c0db6 100644 --- a/internal/formats/cyclonedx13xml/encoder_test.go +++ b/internal/formats/cyclonedx13xml/encoder_test.go @@ -33,8 +33,9 @@ func TestCycloneDxImageEncoder(t *testing.T) { func cycloneDxRedactor(s []byte) []byte { serialPattern := regexp.MustCompile(`serialNumber="[a-zA-Z0-9\-:]+"`) 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]))`) + sha256Pattern := regexp.MustCompile(`sha256:[A-Fa-f0-9]{64}`) - for _, pattern := range []*regexp.Regexp{serialPattern, rfc3339Pattern} { + for _, pattern := range []*regexp.Regexp{serialPattern, rfc3339Pattern, sha256Pattern} { s = pattern.ReplaceAll(s, []byte("redacted")) } return s diff --git a/internal/formats/cyclonedx13xml/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden b/internal/formats/cyclonedx13xml/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden index 427ed1aef..4b4ad5552 100644 --- a/internal/formats/cyclonedx13xml/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden +++ b/internal/formats/cyclonedx13xml/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden @@ -1,7 +1,7 @@ - + - 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 3d93b6d3a..c483fa49b 100644 Binary files a/internal/formats/cyclonedx13xml/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden and b/internal/formats/cyclonedx13xml/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden differ diff --git a/syft/linux/release.go b/syft/linux/release.go index dce399236..8fdb23645 100644 --- a/syft/linux/release.go +++ b/syft/linux/release.go @@ -2,14 +2,14 @@ package linux // Release represents Linux Distribution release information as specified from https://www.freedesktop.org/software/systemd/man/os-release.html type Release struct { - PrettyName string // A pretty operating system name in a format suitable for presentation to the user. + PrettyName string `cyclonedx:"prettyName"` // A pretty operating system name in a format suitable for presentation to the user. Name string // identifies the operating system, without a version component, and suitable for presentation to the user. - ID string // identifies the operating system, excluding any version information and suitable for processing by scripts or usage in generated filenames. - IDLike []string // list of operating system identifiers in the same syntax as the ID= setting. It should list identifiers of operating systems that are closely related to the local operating system in regards to packaging and programming interfaces. + ID string `cyclonedx:"id"` // identifies the operating system, excluding any version information and suitable for processing by scripts or usage in generated filenames. + IDLike []string `cyclonedx:"idLike"` // list of operating system identifiers in the same syntax as the ID= setting. It should list identifiers of operating systems that are closely related to the local operating system in regards to packaging and programming interfaces. Version string // identifies the operating system version, excluding any OS name information, possibly including a release code name, and suitable for presentation to the user. - VersionID string // identifies the operating system version, excluding any OS name information or release code name, and suitable for processing by scripts or usage in generated filenames. - Variant string // identifies a specific variant or edition of the operating system suitable for presentation to the user. - VariantID string // identifies a specific variant or edition of the operating system. This may be interpreted by other packages in order to determine a divergent default configuration. + VersionID string `cyclonedx:"versionID"` // identifies the operating system version, excluding any OS name information or release code name, and suitable for processing by scripts or usage in generated filenames. + Variant string `cyclonedx:"variant"` // identifies a specific variant or edition of the operating system suitable for presentation to the user. + VariantID string `cyclonedx:"variantID"` // identifies a specific variant or edition of the operating system. This may be interpreted by other packages in order to determine a divergent default configuration. HomeURL string SupportURL string BugReportURL string diff --git a/syft/pkg/apk_metadata.go b/syft/pkg/apk_metadata.go index 497202f5a..c967255ab 100644 --- a/syft/pkg/apk_metadata.go +++ b/syft/pkg/apk_metadata.go @@ -20,18 +20,18 @@ var _ FileOwner = (*ApkMetadata)(nil) // - https://git.alpinelinux.org/apk-tools/tree/src/database.c type ApkMetadata struct { Package string `mapstructure:"P" json:"package"` - OriginPackage string `mapstructure:"o" json:"originPackage"` + OriginPackage string `mapstructure:"o" json:"originPackage" cyclonedx:"originPackage"` Maintainer string `mapstructure:"m" json:"maintainer"` Version string `mapstructure:"V" json:"version"` License string `mapstructure:"L" json:"license"` Architecture string `mapstructure:"A" json:"architecture"` URL string `mapstructure:"U" json:"url"` Description string `mapstructure:"T" json:"description"` - Size int `mapstructure:"S" json:"size"` - InstalledSize int `mapstructure:"I" json:"installedSize"` - PullDependencies string `mapstructure:"D" json:"pullDependencies"` - PullChecksum string `mapstructure:"C" json:"pullChecksum"` - GitCommitOfAport string `mapstructure:"c" json:"gitCommitOfApkPort"` + Size int `mapstructure:"S" json:"size" cyclonedx:"size"` + InstalledSize int `mapstructure:"I" json:"installedSize" cyclonedx:"installedSize"` + PullDependencies string `mapstructure:"D" json:"pullDependencies" cyclonedx:"pullDependencies"` + PullChecksum string `mapstructure:"C" json:"pullChecksum" cyclonedx:"pullChecksum"` + GitCommitOfAport string `mapstructure:"c" json:"gitCommitOfApkPort" cyclonedx:"gitCommitOfApkPort"` Files []ApkFileRecord `json:"files"` } diff --git a/syft/pkg/dpkg_metadata.go b/syft/pkg/dpkg_metadata.go index 07c105cd5..0063ef231 100644 --- a/syft/pkg/dpkg_metadata.go +++ b/syft/pkg/dpkg_metadata.go @@ -18,12 +18,12 @@ var _ FileOwner = (*DpkgMetadata)(nil) // at http://manpages.ubuntu.com/manpages/xenial/man1/dpkg-query.1.html in the --showformat section. type DpkgMetadata struct { Package string `mapstructure:"Package" json:"package"` - Source string `mapstructure:"Source" json:"source"` + Source string `mapstructure:"Source" json:"source" cyclonedx:"source"` Version string `mapstructure:"Version" json:"version"` - SourceVersion string `mapstructure:"SourceVersion" json:"sourceVersion"` + SourceVersion string `mapstructure:"SourceVersion" json:"sourceVersion" cyclonedx:"sourceVersion"` Architecture string `mapstructure:"Architecture" json:"architecture"` Maintainer string `mapstructure:"Maintainer" json:"maintainer"` - InstalledSize int `mapstructure:"InstalledSize" json:"installedSize"` + InstalledSize int `mapstructure:"InstalledSize" json:"installedSize" cyclonedx:"installedSize"` Files []DpkgFileRecord `json:"files"` } diff --git a/syft/pkg/golang_bin_metadata.go b/syft/pkg/golang_bin_metadata.go index 946b5f876..e77a4b0f0 100644 --- a/syft/pkg/golang_bin_metadata.go +++ b/syft/pkg/golang_bin_metadata.go @@ -2,7 +2,7 @@ package pkg // GolangBinMetadata represents all captured data for a Golang Binary type GolangBinMetadata struct { - GoCompiledVersion string `json:"goCompiledVersion"` - Architecture string `json:"architecture"` - H1Digest string `json:"h1Digest"` + GoCompiledVersion string `json:"goCompiledVersion" cyclonedx:"goCompiledVersion"` + Architecture string `json:"architecture" cyclonedx:"architecture"` + H1Digest string `json:"h1Digest" cyclonedx:"h1Digest"` } diff --git a/syft/pkg/package.go b/syft/pkg/package.go index 604432a86..6215bf594 100644 --- a/syft/pkg/package.go +++ b/syft/pkg/package.go @@ -17,14 +17,14 @@ type Package struct { id artifact.ID `hash:"ignore"` Name string // the package name Version string // the version of the package - FoundBy string // the specific cataloger that discovered this package + FoundBy string `cyclonedx:"foundBy"` // the specific cataloger that discovered this package Locations []source.Location // the locations that lead to the discovery of this package (note: this is not necessarily the locations that make up this package) Licenses []string // licenses discovered with the package metadata - Language Language // the language ecosystem this package belongs to (e.g. JavaScript, Python, etc) - Type Type // the package type (e.g. Npm, Yarn, Python, Rpm, Deb, etc) - CPEs []CPE `hash:"ignore"` // all possible Common Platform Enumerators (note: this is NOT included in the definition of the ID since all fields on a CPE are derived from other fields) - PURL string `hash:"ignore"` // the Package URL (see https://github.com/package-url/purl-spec) (note: this is NOT included in the definition of the ID since all fields on a pURL are derived from other fields) - MetadataType MetadataType // the shape of the additional data in the "metadata" field + Language Language `cyclonedx:"language"` // the language ecosystem this package belongs to (e.g. JavaScript, Python, etc) + Type Type `cyclonedx:"type"` // the package type (e.g. Npm, Yarn, Python, Rpm, Deb, etc) + CPEs []CPE `hash:"ignore"` // all possible Common Platform Enumerators (note: this is NOT included in the definition of the ID since all fields on a CPE are derived from other fields) + PURL string `hash:"ignore"` // the Package URL (see https://github.com/package-url/purl-spec) (note: this is NOT included in the definition of the ID since all fields on a pURL are derived from other fields) + MetadataType MetadataType `cyclonedx:"metadataType"` // the shape of the additional data in the "metadata" field Metadata interface{} // additional data found while parsing the package source } diff --git a/syft/pkg/rpmdb_metadata.go b/syft/pkg/rpmdb_metadata.go index 5d600eebe..faf1abbf9 100644 --- a/syft/pkg/rpmdb_metadata.go +++ b/syft/pkg/rpmdb_metadata.go @@ -21,11 +21,11 @@ var _ FileOwner = (*RpmdbMetadata)(nil) type RpmdbMetadata struct { Name string `json:"name"` Version string `json:"version"` - Epoch *int `json:"epoch"` + Epoch *int `json:"epoch" cyclonedx:"epoch"` Arch string `json:"architecture"` - Release string `json:"release"` - SourceRpm string `json:"sourceRpm"` - Size int `json:"size"` + Release string `json:"release" cyclonedx:"release"` + SourceRpm string `json:"sourceRpm" cyclonedx:"sourceRpm"` + Size int `json:"size" cyclonedx:"size"` License string `json:"license"` Vendor string `json:"vendor"` Files []RpmdbFileRecord `json:"files"` diff --git a/syft/source/coordinates.go b/syft/source/coordinates.go index b97199588..653670792 100644 --- a/syft/source/coordinates.go +++ b/syft/source/coordinates.go @@ -10,8 +10,8 @@ import ( // Coordinates contains the minimal information needed to describe how to find a file within any possible source object (e.g. image and directory sources) type Coordinates struct { - RealPath string `json:"path"` // The path where all path ancestors have no hardlinks / symlinks - FileSystemID string `json:"layerID,omitempty"` // An ID representing the filesystem. For container images, this is a layer digest. For directories or a root filesystem, this is blank. + RealPath string `json:"path" cyclonedx:"path"` // The path where all path ancestors have no hardlinks / symlinks + FileSystemID string `json:"layerID,omitempty" cyclonedx:"layerID"` // An ID representing the filesystem. For container images, this is a layer digest. For directories or a root filesystem, this is blank. } // CoordinateSet represents a set of string types.