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 <skothari44@bloomberg.net>
This commit is contained in:
Sambhav Kothari 2022-01-19 16:43:16 +00:00 committed by GitHub
parent 829e500aa9
commit aebe843c6f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 1163 additions and 79 deletions

View File

@ -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 ""
}

View File

@ -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 <auth@auth.gov>",
},
{
// 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))
})
}
}

View File

@ -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
}

View File

@ -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 ""
}

View File

@ -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))
})
}
}

View File

@ -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 ""
}

View File

@ -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))
})
}
}

View File

@ -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
}

View File

@ -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))
})
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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))
})
}
}

View File

@ -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 ""
}

View File

@ -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 <ncopa@alpinelinux.org>",
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 <debian-glibc@lists.debian.org>",
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))
})
}
}

View File

@ -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 ""
}

View File

@ -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))
})
}
}

View File

@ -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

View File

@ -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"
}
]
}
]
}

View File

@ -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"
}
]
}
]
}

View File

@ -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

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.3" serialNumber="urn:uuid:7b1c3b1d-ea3b-4022-9dcc-80f4b4cbce36" version="1">
<bom xmlns="http://cyclonedx.org/schema/bom/1.3" serialNumber="urn:uuid:16a426e7-fcc7-4b94-abd2-66c67569cc44" version="1">
<metadata>
<timestamp>2021-12-03T13:16:45-08:00</timestamp>
<timestamp>2022-01-14T22:46:49Z</timestamp>
<tools>
<tool>
<vendor>anchore</vendor>
@ -20,15 +20,39 @@
<version>1.0.1</version>
<licenses>
<license>
<name>MIT</name>
<id>MIT</id>
</license>
</licenses>
<cpe>cpe:2.3:*:some:package:2:*:*:*:*:*:*:*</cpe>
<purl>a-purl-2</purl>
<properties>
<property name="foundBy">the-cataloger-1</property>
<property name="language">python</property>
<property name="type">python</property>
<property name="metadataType">PythonPackageMetadata</property>
<property name="path">/some/path/pkg1</property>
</properties>
</component>
<component type="library">
<name>package-2</name>
<version>2.0.1</version>
<cpe>cpe:2.3:*:some:package:2:*:*:*:*:*:*:*</cpe>
<purl>a-purl-2</purl>
<properties>
<property name="foundBy">the-cataloger-2</property>
<property name="type">deb</property>
<property name="metadataType">DpkgMetadata</property>
<property name="path">/some/path/pkg1</property>
</properties>
</component>
<component type="operating-system">
<name>debian</name>
<version>1.2.3</version>
<properties>
<property name="prettyName">debian</property>
<property name="id">debian</property>
<property name="versionID">1.2.3</property>
</properties>
</component>
</components>
</bom>

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.3" serialNumber="urn:uuid:66bda3e1-888a-4d43-b906-7fd96d428753" version="1">
<bom xmlns="http://cyclonedx.org/schema/bom/1.3" serialNumber="urn:uuid:a3d776c6-a2ca-4116-8b99-b3538dd1a460" version="1">
<metadata>
<timestamp>2021-12-03T13:16:45-08:00</timestamp>
<timestamp>2022-01-14T22:46:49Z</timestamp>
<tools>
<tool>
<vendor>anchore</vendor>
@ -20,15 +20,41 @@
<version>1.0.1</version>
<licenses>
<license>
<name>MIT</name>
<id>MIT</id>
</license>
</licenses>
<cpe>cpe:2.3:*:some:package:1:*:*:*:*:*:*:*</cpe>
<purl>a-purl-1</purl>
<properties>
<property name="foundBy">the-cataloger-1</property>
<property name="language">python</property>
<property name="type">python</property>
<property name="metadataType">PythonPackageMetadata</property>
<property name="path">/somefile-1.txt</property>
<property name="layerID">sha256:16e64541f2ddf59a90391ce7bb8af90313f7d373f2105d88f3d3267b72e0ebab</property>
</properties>
</component>
<component type="library">
<name>package-2</name>
<version>2.0.1</version>
<cpe>cpe:2.3:*:some:package:2:*:*:*:*:*:*:*</cpe>
<purl>a-purl-2</purl>
<properties>
<property name="foundBy">the-cataloger-2</property>
<property name="type">deb</property>
<property name="metadataType">DpkgMetadata</property>
<property name="path">/somefile-2.txt</property>
<property name="layerID">sha256:de6c235f76ea24c8503ec08891445b5d6a8bdf8249117ed8d8b0b6fb3ebe4f67</property>
</properties>
</component>
<component type="operating-system">
<name>debian</name>
<version>1.2.3</version>
<properties>
<property name="prettyName">debian</property>
<property name="id">debian</property>
<property name="versionID">1.2.3</property>
</properties>
</component>
</components>
</bom>

View File

@ -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

View File

@ -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"`
}

View File

@ -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"`
}

View File

@ -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"`
}

View File

@ -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
}

View File

@ -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"`

View File

@ -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.