refactor: move formats from internal into syft module (#1172)

This commit is contained in:
Chapman Pendery
2022-09-13 11:20:52 -04:00
committed by GitHub
parent c5dca001e2
commit 9097614f3b
185 changed files with 80 additions and 80 deletions
@@ -0,0 +1,47 @@
package cyclonedxhelpers
import (
"fmt"
"strings"
"github.com/anchore/syft/syft/pkg"
)
func encodeAuthor(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 ""
}
func decodeAuthor(author string, metadata interface{}) {
switch meta := metadata.(type) {
case *pkg.NpmPackageJSONMetadata:
meta.Author = author
case *pkg.PythonPackageMetadata:
parts := strings.SplitN(author, " <", 2)
meta.Author = parts[0]
if len(parts) > 1 {
meta.AuthorEmail = strings.TrimSuffix(parts[1], ">")
}
case *pkg.GemMetadata:
meta.Authors = strings.Split(author, ",")
}
}
@@ -0,0 +1,87 @@
package cyclonedxhelpers
import (
"testing"
"github.com/anchore/syft/syft/pkg"
"github.com/stretchr/testify/assert"
)
func Test_encodeAuthor(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, encodeAuthor(test.input))
})
}
}
@@ -0,0 +1,128 @@
package cyclonedxhelpers
import (
"reflect"
"github.com/CycloneDX/cyclonedx-go"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/syft/formats/common"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
func encodeComponent(p pkg.Package) cyclonedx.Component {
props := encodeProperties(p, "syft:package")
props = append(props, encodeCPEs(p)...)
locations := p.Locations.ToSlice()
if len(locations) > 0 {
props = append(props, encodeProperties(locations, "syft:location")...)
}
if hasMetadata(p) {
props = append(props, encodeProperties(p.Metadata, "syft:metadata")...)
}
var properties *[]cyclonedx.Property
if len(props) > 0 {
properties = &props
}
return cyclonedx.Component{
Type: cyclonedx.ComponentTypeLibrary,
Name: p.Name,
Group: encodeGroup(p),
Version: p.Version,
PackageURL: p.PURL,
Licenses: encodeLicenses(p),
CPE: encodeSingleCPE(p),
Author: encodeAuthor(p),
Publisher: encodePublisher(p),
Description: encodeDescription(p),
ExternalReferences: encodeExternalReferences(p),
Properties: properties,
BOMRef: deriveBomRef(p),
}
}
func deriveBomRef(p pkg.Package) string {
// try and parse the PURL if possible and append syft id to it, to make
// the purl unique in the BOM.
// TODO: In the future we may want to dedupe by PURL and combine components with
// the same PURL while preserving their unique metadata.
if parsedPURL, err := packageurl.FromString(p.PURL); err == nil {
parsedPURL.Qualifiers = append(parsedPURL.Qualifiers, packageurl.Qualifier{Key: "package-id", Value: string(p.ID())})
return parsedPURL.ToString()
}
// fallback is to use strictly the ID if there is no valid pURL
return string(p.ID())
}
func hasMetadata(p pkg.Package) bool {
return p.Metadata != nil
}
func decodeComponent(c *cyclonedx.Component) *pkg.Package {
values := map[string]string{}
if c.Properties != nil {
for _, p := range *c.Properties {
values[p.Name] = p.Value
}
}
p := &pkg.Package{
Name: c.Name,
Version: c.Version,
Locations: decodeLocations(values),
Licenses: decodeLicenses(c),
CPEs: decodeCPEs(c),
PURL: c.PackageURL,
}
common.DecodeInto(p, values, "syft:package", CycloneDXFields)
p.MetadataType = pkg.CleanMetadataType(p.MetadataType)
p.Metadata = decodePackageMetadata(values, c, p.MetadataType)
if p.Type == "" {
p.Type = pkg.TypeFromPURL(p.PURL)
}
if p.Language == "" {
p.Language = pkg.LanguageFromPURL(p.PURL)
}
return p
}
func decodeLocations(vals map[string]string) source.LocationSet {
v := common.Decode(reflect.TypeOf([]source.Location{}), vals, "syft:location", CycloneDXFields)
out, ok := v.([]source.Location)
if !ok {
out = nil
}
return source.NewLocationSet(out...)
}
func decodePackageMetadata(vals map[string]string, c *cyclonedx.Component, typ pkg.MetadataType) interface{} {
if typ != "" && c.Properties != nil {
metaTyp, ok := pkg.MetadataTypeByName[typ]
if !ok {
return nil
}
metaPtrTyp := reflect.PtrTo(metaTyp)
metaPtr := common.Decode(metaPtrTyp, vals, "syft:metadata", CycloneDXFields)
// Map all explicit metadata properties
decodeAuthor(c.Author, metaPtr)
decodeGroup(c.Group, metaPtr)
decodePublisher(c.Publisher, metaPtr)
decodeDescription(c.Description, metaPtr)
decodeExternalReferences(c, metaPtr)
// return the actual interface{} -> struct ... not interface{} -> *struct
return common.PtrToStruct(metaPtr)
}
return nil
}
@@ -0,0 +1,243 @@
package cyclonedxhelpers
import (
"fmt"
"testing"
"github.com/CycloneDX/cyclonedx-go"
"github.com/stretchr/testify/assert"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
func Test_encodeComponentProperties(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.NewLocationSet(
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: "syft:package:foundBy", Value: "cataloger"},
{Name: "syft:location:0:path", Value: "test"},
{Name: "syft:metadata:gitCommitOfApkPort", Value: "97b1c2842faa3bfa30f5811ffbf16d5ff9f1a479"},
{Name: "syft:metadata:installedSize", Value: "4096"},
{Name: "syft:metadata:originPackage", Value: "libc-dev"},
{Name: "syft:metadata:pullChecksum", Value: "Q1p78yvTLG094tHE1+dToJGbmYzQE="},
{Name: "syft:metadata:pullDependencies", Value: "musl-utils"},
{Name: "syft:metadata:size", Value: "0"},
},
},
{
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: "syft:package:metadataType", Value: "DpkgMetadata"},
{Name: "syft:metadata:installedSize", Value: "3036"},
{Name: "syft:metadata:source", Value: "tzdata-dev"},
{Name: "syft:metadata:sourceVersion", Value: "1.0"},
},
},
{
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: "syft:package:language", Value: pkg.Go.String()},
{Name: "syft:package:metadataType", Value: "GolangBinMetadata"},
{Name: "syft:package:type", Value: "go-module"},
{Name: "syft:metadata:architecture", Value: "amd64"},
{Name: "syft:metadata:goCompiledVersion", Value: "1.17"},
{Name: "syft:metadata:h1Digest", Value: "h1:KlOXYy8wQWTUJYFgkUI40Lzr06ofg5IRXUK5C7qZt1k="},
},
},
{
name: "from rpm",
input: pkg.Package{
Name: "dive",
Version: "0.9.2-1",
Type: pkg.RpmPkg,
MetadataType: pkg.RpmMetadataType,
Metadata: pkg.RpmMetadata{
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: "syft:package:metadataType", Value: "RpmMetadata"},
{Name: "syft:package:type", Value: "rpm"},
{Name: "syft:metadata:epoch", Value: "2"},
{Name: "syft:metadata:release", Value: "1"},
{Name: "syft:metadata:size", Value: "12406784"},
{Name: "syft:metadata:sourceRpm", Value: "dive-0.9.2-1.src.rpm"},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
c := encodeComponent(test.input)
assert.Equal(t, test.expected, c.Properties)
})
}
}
func Test_deriveBomRef(t *testing.T) {
pkgWithPurl := pkg.Package{
Name: "django",
Version: "1.11.1",
PURL: "pkg:pypi/django@1.11.1",
}
pkgWithPurl.SetID()
pkgWithOutPurl := pkg.Package{
Name: "django",
Version: "1.11.1",
PURL: "",
}
pkgWithOutPurl.SetID()
pkgWithBadPurl := pkg.Package{
Name: "django",
Version: "1.11.1",
PURL: "pkg:pyjango@1.11.1",
}
pkgWithBadPurl.SetID()
tests := []struct {
name string
pkg pkg.Package
want string
}{
{
name: "use pURL-id hybrid",
pkg: pkgWithPurl,
want: fmt.Sprintf("pkg:pypi/django@1.11.1?package-id=%s", pkgWithPurl.ID()),
},
{
name: "fallback to ID when pURL is invalid",
pkg: pkgWithBadPurl,
want: string(pkgWithBadPurl.ID()),
},
{
name: "fallback to ID when pURL is missing",
pkg: pkgWithOutPurl,
want: string(pkgWithOutPurl.ID()),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.pkg.ID()
assert.Equal(t, tt.want, deriveBomRef(tt.pkg))
})
}
}
func Test_decodeComponent(t *testing.T) {
tests := []struct {
name string
component cyclonedx.Component
wantLanguage pkg.Language
wantMetadataType pkg.MetadataType
}{
{
name: "derive language from pURL if missing",
component: cyclonedx.Component{
Name: "ch.qos.logback/logback-classic",
Version: "1.2.3",
PackageURL: "pkg:maven/ch.qos.logback/logback-classic@1.2.3",
Type: "library",
BOMRef: "pkg:maven/ch.qos.logback/logback-classic@1.2.3",
},
wantLanguage: pkg.Java,
},
{
name: "handle existing RpmdbMetadata type",
component: cyclonedx.Component{
Name: "acl",
Version: "2.2.53-1.el8",
PackageURL: "pkg:rpm/centos/acl@2.2.53-1.el8?arch=x86_64&upstream=acl-2.2.53-1.el8.src.rpm&distro=centos-8",
Type: "library",
BOMRef: "pkg:rpm/centos/acl@2.2.53-1.el8?arch=x86_64&upstream=acl-2.2.53-1.el8.src.rpm&distro=centos-8",
Properties: &[]cyclonedx.Property{
{
Name: "syft:package:metadataType",
Value: "RpmdbMetadata",
},
},
},
wantMetadataType: pkg.RpmMetadataType,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := decodeComponent(&tt.component)
if tt.wantLanguage != "" {
assert.Equal(t, tt.wantLanguage, p.Language)
}
if tt.wantMetadataType != "" {
assert.Equal(t, tt.wantMetadataType, p.MetadataType)
}
})
}
}
@@ -0,0 +1,57 @@
package cyclonedxhelpers
import (
"github.com/CycloneDX/cyclonedx-go"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/pkg"
)
func encodeSingleCPE(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 ""
}
func encodeCPEs(p pkg.Package) (out []cyclonedx.Property) {
for i, c := range p.CPEs {
// first CPE is "most specific" and already encoded as the component CPE
if i == 0 {
continue
}
out = append(out, cyclonedx.Property{
Name: "syft:cpe23",
Value: pkg.CPEString(c),
})
}
return
}
func decodeCPEs(c *cyclonedx.Component) (out []pkg.CPE) {
if c.CPE != "" {
cp, err := pkg.NewCPE(c.CPE)
if err != nil {
log.Warnf("invalid CPE: %s", c.CPE)
} else {
out = append(out, cp)
}
}
if c.Properties != nil {
for _, p := range *c.Properties {
if p.Name == "syft:cpe23" {
cp, err := pkg.NewCPE(p.Value)
if err != nil {
log.Warnf("invalid CPE: %s", p.Value)
} else {
out = append(out, cp)
}
}
}
}
return
}
@@ -0,0 +1,58 @@
package cyclonedxhelpers
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/anchore/syft/syft/pkg"
)
func Test_encodeCPE(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, encodeSingleCPE(test.input))
})
}
}
@@ -0,0 +1,251 @@
package cyclonedxhelpers
import (
"fmt"
"io"
"github.com/CycloneDX/cyclonedx-go"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/formats/common"
"github.com/anchore/syft/syft/linux"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
)
func GetValidator(format cyclonedx.BOMFileFormat) sbom.Validator {
return func(reader io.Reader) error {
bom := &cyclonedx.BOM{}
err := cyclonedx.NewBOMDecoder(reader, format).Decode(bom)
if err != nil {
return err
}
// random JSON does not necessarily cause an error (e.g. SPDX)
if (cyclonedx.BOM{} == *bom || bom.Components == nil) {
return fmt.Errorf("not a valid CycloneDX document")
}
return nil
}
}
func GetDecoder(format cyclonedx.BOMFileFormat) sbom.Decoder {
return func(reader io.Reader) (*sbom.SBOM, error) {
bom := &cyclonedx.BOM{
Components: &[]cyclonedx.Component{},
}
err := cyclonedx.NewBOMDecoder(reader, format).Decode(bom)
if err != nil {
return nil, err
}
s, err := ToSyftModel(bom)
if err != nil {
return nil, err
}
return s, nil
}
}
func ToSyftModel(bom *cyclonedx.BOM) (*sbom.SBOM, error) {
if bom == nil {
return nil, fmt.Errorf("no content defined in CycloneDX BOM")
}
s := &sbom.SBOM{
Artifacts: sbom.Artifacts{
PackageCatalog: pkg.NewCatalog(),
LinuxDistribution: linuxReleaseFromComponents(*bom.Components),
},
Source: extractComponents(bom.Metadata),
Descriptor: extractDescriptor(bom.Metadata),
}
idMap := make(map[string]interface{})
if err := collectBomPackages(bom, s, idMap); err != nil {
return nil, err
}
collectRelationships(bom, s, idMap)
return s, nil
}
func collectBomPackages(bom *cyclonedx.BOM, s *sbom.SBOM, idMap map[string]interface{}) error {
if bom.Components == nil {
return fmt.Errorf("no components are defined in the CycloneDX BOM")
}
for i := range *bom.Components {
collectPackages(&(*bom.Components)[i], s, idMap)
}
return nil
}
func collectPackages(component *cyclonedx.Component, s *sbom.SBOM, idMap map[string]interface{}) {
switch component.Type {
case cyclonedx.ComponentTypeOS:
case cyclonedx.ComponentTypeContainer:
case cyclonedx.ComponentTypeApplication, cyclonedx.ComponentTypeFramework, cyclonedx.ComponentTypeLibrary:
p := decodeComponent(component)
idMap[component.BOMRef] = p
// TODO there must be a better way than needing to call this manually:
p.SetID()
s.Artifacts.PackageCatalog.Add(*p)
}
if component.Components != nil {
for i := range *component.Components {
collectPackages(&(*component.Components)[i], s, idMap)
}
}
}
func linuxReleaseFromComponents(components []cyclonedx.Component) *linux.Release {
for i := range components {
component := &components[i]
if component.Type == cyclonedx.ComponentTypeOS {
return linuxReleaseFromOSComponent(component)
}
}
return nil
}
func linuxReleaseFromOSComponent(component *cyclonedx.Component) *linux.Release {
if component == nil {
return nil
}
var name string
var version string
if component.SWID != nil {
name = component.SWID.Name
version = component.SWID.Version
}
if name == "" {
name = component.Name
}
if name == "" {
name = getPropertyValue(component, "id")
}
if version == "" {
version = component.Version
}
if version == "" {
version = getPropertyValue(component, "versionID")
}
rel := &linux.Release{
CPEName: component.CPE,
PrettyName: name,
Name: name,
ID: name,
IDLike: []string{name},
Version: version,
VersionID: version,
}
if component.ExternalReferences != nil {
for _, ref := range *component.ExternalReferences {
switch ref.Type {
case cyclonedx.ERTypeIssueTracker:
rel.BugReportURL = ref.URL
case cyclonedx.ERTypeWebsite:
rel.HomeURL = ref.URL
case cyclonedx.ERTypeOther:
switch ref.Comment {
case "support":
rel.SupportURL = ref.URL
case "privacyPolicy":
rel.PrivacyPolicyURL = ref.URL
}
}
}
}
if component.Properties != nil {
values := map[string]string{}
for _, p := range *component.Properties {
values[p.Name] = p.Value
}
common.DecodeInto(&rel, values, "syft:distro", CycloneDXFields)
}
return rel
}
func getPropertyValue(component *cyclonedx.Component, name string) string {
if component.Properties != nil {
for _, p := range *component.Properties {
if p.Name == name {
return p.Value
}
}
}
return ""
}
func collectRelationships(bom *cyclonedx.BOM, s *sbom.SBOM, idMap map[string]interface{}) {
if bom.Dependencies == nil {
return
}
for _, d := range *bom.Dependencies {
from, fromOk := idMap[d.Ref].(artifact.Identifiable)
if fromOk {
if d.Dependencies == nil {
continue
}
for _, t := range *d.Dependencies {
to, toOk := idMap[t.Ref].(artifact.Identifiable)
if toOk {
s.Relationships = append(s.Relationships, artifact.Relationship{
From: from,
To: to,
Type: artifact.DependencyOfRelationship, // FIXME this information is lost
})
}
}
}
}
}
func extractComponents(meta *cyclonedx.Metadata) source.Metadata {
if meta == nil || meta.Component == nil {
return source.Metadata{}
}
c := meta.Component
image := source.ImageMetadata{
UserInput: c.Name,
ID: c.BOMRef,
ManifestDigest: c.Version,
}
switch c.Type {
case cyclonedx.ComponentTypeContainer:
return source.Metadata{
Scheme: source.ImageScheme,
ImageMetadata: image,
}
case cyclonedx.ComponentTypeFile:
return source.Metadata{
Scheme: source.FileScheme, // or source.DirectoryScheme
Path: c.Name,
ImageMetadata: image,
}
}
return source.Metadata{}
}
// if there is more than one tool in meta.Tools' list the last item will be used
// as descriptor. If there is a way to know which tool to use here please fix it.
func extractDescriptor(meta *cyclonedx.Metadata) (desc sbom.Descriptor) {
if meta == nil || meta.Tools == nil {
return
}
for _, t := range *meta.Tools {
desc.Name = t.Name
desc.Version = t.Version
}
return
}
@@ -0,0 +1,297 @@
package cyclonedxhelpers
import (
"bytes"
"encoding/json"
"fmt"
"testing"
"github.com/CycloneDX/cyclonedx-go"
"github.com/stretchr/testify/assert"
)
func Test_decode(t *testing.T) {
type expected struct {
os string
pkg string
ver string
relation string
purl string
cpe string
}
tests := []struct {
name string
input cyclonedx.BOM
expected []expected
}{
{
name: "basic mapping from cyclonedx",
input: cyclonedx.BOM{
Metadata: nil,
Components: &[]cyclonedx.Component{
{
BOMRef: "p1",
Type: cyclonedx.ComponentTypeLibrary,
Name: "package-1",
Version: "1.0.1",
Description: "",
Hashes: nil,
Licenses: &cyclonedx.Licenses{
{
License: &cyclonedx.License{
ID: "MIT",
},
},
},
CPE: "cpe:2.3:*:some:package:1:*:*:*:*:*:*:*",
PackageURL: "pkg:some/package-1@1.0.1?arch=arm64&upstream=upstream1&distro=alpine-1",
ExternalReferences: &[]cyclonedx.ExternalReference{
{
URL: "",
Comment: "",
Hashes: nil,
Type: "",
},
},
Properties: &[]cyclonedx.Property{
{
Name: "foundBy",
Value: "the-cataloger-1",
},
{
Name: "language",
Value: "python",
},
{
Name: "type",
Value: "python",
},
{
Name: "metadataType",
Value: "PythonPackageMetadata",
},
{
Name: "path",
Value: "/some/path/pkg1",
},
},
Components: nil,
Evidence: nil,
},
{
BOMRef: "p2",
Type: cyclonedx.ComponentTypeLibrary,
Name: "package-2",
Version: "2.0.2",
Hashes: nil,
Licenses: &cyclonedx.Licenses{
{
License: &cyclonedx.License{
ID: "MIT",
},
},
},
CPE: "cpe:2.3:*:another:package:2:*:*:*:*:*:*:*",
PackageURL: "pkg:alpine/alpine-baselayout@3.2.0-r16?arch=x86_64&upstream=alpine-baselayout&distro=alpine-3.14.2",
Properties: &[]cyclonedx.Property{
{
Name: "foundBy",
Value: "apkdb-cataloger",
},
{
Name: "type",
Value: "apk",
},
{
Name: "metadataType",
Value: "ApkMetadata",
},
{
Name: "path",
Value: "/lib/apk/db/installed",
},
{
Name: "layerID",
Value: "sha256:9fb3aa2f8b8023a4bebbf92aa567caf88e38e969ada9f0ac12643b2847391635",
},
{
Name: "originPackage",
Value: "zlib",
},
{
Name: "size",
Value: "51213",
},
{
Name: "installedSize",
Value: "110592",
},
{
Name: "pullDependencies",
Value: "so:libc.musl-x86_64.so.1",
},
{
Name: "pullChecksum",
Value: "Q1uss4DfpvL16Nw2YUTwmzGBABz3Y=",
},
{
Name: "gitCommitOfApkPort",
Value: "d2bfb22c8e8f67ad7d8d02704f35ec4d2a19f9b9",
},
},
},
{
Type: cyclonedx.ComponentTypeOS,
Name: "debian",
Version: "1.2.3",
Hashes: nil,
Licenses: &cyclonedx.Licenses{
{
License: &cyclonedx.License{
ID: "MIT",
},
},
},
Properties: &[]cyclonedx.Property{
{
Name: "prettyName",
Value: "debian",
},
{
Name: "id",
Value: "debian",
},
{
Name: "versionID",
Value: "1.2.3",
},
},
Components: nil,
Evidence: nil,
},
},
Dependencies: &[]cyclonedx.Dependency{
{
Ref: "p1",
Dependencies: &[]cyclonedx.Dependency{
{
Ref: "p2",
},
},
},
},
},
expected: []expected{
{
os: "debian",
ver: "1.2.3",
},
{
pkg: "package-1",
ver: "1.0.1",
cpe: "cpe:2.3:*:some:package:1:*:*:*:*:*:*:*",
purl: "pkg:some/package-1@1.0.1?arch=arm64&upstream=upstream1&distro=alpine-1",
relation: "package-2",
},
{
pkg: "package-2",
ver: "2.0.2",
purl: "pkg:alpine/alpine-baselayout@3.2.0-r16?arch=x86_64&upstream=alpine-baselayout&distro=alpine-3.14.2",
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
sbom, err := ToSyftModel(&test.input)
assert.NoError(t, err)
test:
for _, e := range test.expected {
if e.os != "" {
assert.Equal(t, e.os, sbom.Artifacts.LinuxDistribution.ID)
assert.Equal(t, e.ver, sbom.Artifacts.LinuxDistribution.VersionID)
}
if e.pkg != "" {
for p := range sbom.Artifacts.PackageCatalog.Enumerate() {
if e.pkg != p.Name {
continue
}
assert.Equal(t, e.ver, p.Version)
if e.cpe != "" {
foundCPE := false
for _, c := range p.CPEs {
cstr := c.BindToFmtString()
if e.cpe == cstr {
foundCPE = true
break
}
}
if !foundCPE {
assert.Fail(t, fmt.Sprintf("CPE not found in package: %s", e.cpe))
}
}
if e.purl != "" {
assert.Equal(t, e.purl, p.PURL)
}
if e.relation != "" {
foundRelation := false
for _, r := range sbom.Relationships {
p := sbom.Artifacts.PackageCatalog.Package(r.To.ID())
if e.relation == p.Name {
foundRelation = true
break
}
}
if !foundRelation {
assert.Fail(t, fmt.Sprintf("relation not found: %s", e.relation))
}
}
continue test
}
assert.Fail(t, fmt.Sprintf("package should be present: %s", e.pkg))
}
}
})
}
}
func Test_missingDataDecode(t *testing.T) {
bom := &cyclonedx.BOM{
Metadata: nil,
Components: &[]cyclonedx.Component{},
}
_, err := ToSyftModel(bom)
assert.NoError(t, err)
bom.Metadata = &cyclonedx.Metadata{}
_, err = ToSyftModel(bom)
assert.NoError(t, err)
pkg := decodeComponent(&cyclonedx.Component{
Licenses: &cyclonedx.Licenses{
{
License: nil,
},
},
})
assert.Len(t, pkg.Licenses, 0)
}
func Test_missingComponentsDecode(t *testing.T) {
bom := &cyclonedx.BOM{}
bomBytes, _ := json.Marshal(&bom)
decode := GetDecoder(cyclonedx.BOMFileFormatJSON)
_, err := decode(bytes.NewReader(bomBytes))
assert.NoError(t, err)
}
@@ -0,0 +1,24 @@
package cyclonedxhelpers
import "github.com/anchore/syft/syft/pkg"
func encodeDescription(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 ""
}
func decodeDescription(description string, metadata interface{}) {
switch meta := metadata.(type) {
case *pkg.ApkMetadata:
meta.Description = description
case *pkg.NpmPackageJSONMetadata:
meta.Description = description
}
}
@@ -0,0 +1,56 @@
package cyclonedxhelpers
import (
"testing"
"github.com/anchore/syft/syft/pkg"
"github.com/stretchr/testify/assert"
)
func Test_encodeDescription(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, encodeDescription(test.input))
})
}
}
@@ -0,0 +1,159 @@
package cyclonedxhelpers
import (
"fmt"
"strings"
syftFile "github.com/anchore/syft/syft/file"
"github.com/CycloneDX/cyclonedx-go"
"github.com/anchore/syft/syft/pkg"
)
//nolint:funlen, gocognit
func encodeExternalReferences(p pkg.Package) *[]cyclonedx.ExternalReference {
var 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.JavaMetadata:
if len(metadata.ArchiveDigests) > 0 {
for _, digest := range metadata.ArchiveDigests {
refs = append(refs, cyclonedx.ExternalReference{
URL: "",
Type: cyclonedx.ERTypeBuildMeta,
Hashes: &[]cyclonedx.Hash{{
Algorithm: toCycloneDXAlgorithm(digest.Algorithm),
Value: digest.Value,
}},
})
}
}
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
}
// supported algorithm in cycloneDX as of 1.4
// "MD5", "SHA-1", "SHA-256", "SHA-384", "SHA-512",
// "SHA3-256", "SHA3-384", "SHA3-512", "BLAKE2b-256", "BLAKE2b-384", "BLAKE2b-512", "BLAKE3"
// syft supported digests: cmd/syft/cli/eventloop/tasks.go
// MD5, SHA1, SHA256
func toCycloneDXAlgorithm(algorithm string) cyclonedx.HashAlgorithm {
validMap := map[string]cyclonedx.HashAlgorithm{
"sha1": cyclonedx.HashAlgorithm("SHA-1"),
"md5": cyclonedx.HashAlgorithm("MD5"),
"sha256": cyclonedx.HashAlgorithm("SHA-256"),
}
return validMap[algorithm]
}
func decodeExternalReferences(c *cyclonedx.Component, metadata interface{}) {
if c.ExternalReferences == nil {
return
}
switch meta := metadata.(type) {
case *pkg.ApkMetadata:
meta.URL = refURL(c, cyclonedx.ERTypeDistribution)
case *pkg.CargoPackageMetadata:
meta.Source = refURL(c, cyclonedx.ERTypeDistribution)
case *pkg.NpmPackageJSONMetadata:
meta.URL = refURL(c, cyclonedx.ERTypeDistribution)
meta.Homepage = refURL(c, cyclonedx.ERTypeWebsite)
case *pkg.GemMetadata:
meta.Homepage = refURL(c, cyclonedx.ERTypeWebsite)
case *pkg.JavaMetadata:
var digests []syftFile.Digest
if ref := findExternalRef(c, cyclonedx.ERTypeBuildMeta); ref != nil {
if ref.Hashes != nil {
for _, hash := range *ref.Hashes {
digests = append(digests, syftFile.Digest{
Algorithm: syftFile.CleanDigestAlgorithmName(string(hash.Algorithm)),
Value: hash.Value,
})
}
}
}
meta.ArchiveDigests = digests
case *pkg.PythonPackageMetadata:
if meta.DirectURLOrigin == nil {
meta.DirectURLOrigin = &pkg.PythonDirectURLOriginInfo{}
}
meta.DirectURLOrigin.URL = refURL(c, cyclonedx.ERTypeVCS)
meta.DirectURLOrigin.CommitID = strings.TrimPrefix(refComment(c, cyclonedx.ERTypeVCS), "commit: ")
}
}
func findExternalRef(c *cyclonedx.Component, typ cyclonedx.ExternalReferenceType) *cyclonedx.ExternalReference {
if c.ExternalReferences != nil {
for _, r := range *c.ExternalReferences {
if r.Type == typ {
return &r
}
}
}
return nil
}
func refURL(c *cyclonedx.Component, typ cyclonedx.ExternalReferenceType) string {
if r := findExternalRef(c, typ); r != nil {
return r.URL
}
return ""
}
func refComment(c *cyclonedx.Component, typ cyclonedx.ExternalReferenceType) string {
if r := findExternalRef(c, typ); r != nil {
return r.Comment
}
return ""
}
@@ -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_encodeExternalReferences(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, encodeExternalReferences(test.input))
})
}
}
@@ -0,0 +1,180 @@
package cyclonedxhelpers
import (
"time"
"github.com/CycloneDX/cyclonedx-go"
"github.com/google/uuid"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/linux"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
)
func ToFormatModel(s sbom.SBOM) *cyclonedx.BOM {
cdxBOM := cyclonedx.NewBOM()
// NOTE(jonasagx): cycloneDX requires URN uuids (URN returns the RFC 2141 URN form of uuid):
// https://github.com/CycloneDX/specification/blob/master/schema/bom-1.3-strict.schema.json#L36
// "pattern": "^urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
cdxBOM.SerialNumber = uuid.New().URN()
cdxBOM.Metadata = toBomDescriptor(internal.ApplicationName, s.Descriptor.Version, s.Source)
packages := s.Artifacts.PackageCatalog.Sorted()
components := make([]cyclonedx.Component, len(packages))
for i, p := range packages {
components[i] = encodeComponent(p)
}
components = append(components, toOSComponent(s.Artifacts.LinuxDistribution)...)
cdxBOM.Components = &components
dependencies := toDependencies(s.Relationships)
if len(dependencies) > 0 {
cdxBOM.Dependencies = &dependencies
}
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 := encodeProperties(distro, "syft:distro")
var properties *[]cyclonedx.Property
if len(props) > 0 {
properties = &props
}
return []cyclonedx.Component{
{
Type: cyclonedx.ComponentTypeOS,
// FIXME is it idiomatic to be using SWID here for specific name and version information?
SWID: &cyclonedx.SWID{
TagID: distro.ID,
Name: distro.ID,
Version: distro.VersionID,
},
Description: distro.PrettyName,
Name: distro.ID,
Version: distro.VersionID,
// TODO should we add a PURL?
CPE: distro.CPEName,
ExternalReferences: eRefs,
Properties: properties,
},
}
}
// 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{
Timestamp: time.Now().Format(time.RFC3339),
Tools: &[]cyclonedx.Tool{
{
Vendor: "anchore",
Name: name,
Version: version,
},
},
Component: toBomDescriptorComponent(srcMetadata),
}
}
// used to indicate that a relationship listed under the syft artifact package can be represented as a cyclonedx dependency.
// NOTE: CycloneDX provides the ability to describe components and their dependency on other components.
// The dependency graph is capable of representing both direct and transitive relationships.
// If a relationship is either direct or transitive it can be included in this function.
// An example of a relationship to not include would be: OwnershipByFileOverlapRelationship.
func isExpressiblePackageRelationship(ty artifact.RelationshipType) bool {
switch ty {
case artifact.RuntimeDependencyOfRelationship:
return true
case artifact.DevDependencyOfRelationship:
return true
case artifact.BuildDependencyOfRelationship:
return true
case artifact.DependencyOfRelationship:
return true
}
return false
}
func toDependencies(relationships []artifact.Relationship) []cyclonedx.Dependency {
result := make([]cyclonedx.Dependency, 0)
for _, r := range relationships {
exists := isExpressiblePackageRelationship(r.Type)
if !exists {
log.Debugf("unable to convert relationship from CycloneDX 1.4 JSON, dropping: %+v", r)
continue
}
innerDeps := []cyclonedx.Dependency{}
innerDeps = append(innerDeps, cyclonedx.Dependency{Ref: string(r.From.ID())})
result = append(result, cyclonedx.Dependency{
Ref: string(r.To.ID()),
Dependencies: &innerDeps,
})
}
return result
}
func toBomDescriptorComponent(srcMetadata source.Metadata) *cyclonedx.Component {
switch srcMetadata.Scheme {
case source.ImageScheme:
bomRef, err := artifact.IDByHash(srcMetadata.ImageMetadata.ID)
if err != nil {
log.Warnf("unable to get fingerprint of image metadata=%s: %+v", srcMetadata.ImageMetadata.ID, err)
}
return &cyclonedx.Component{
BOMRef: string(bomRef),
Type: cyclonedx.ComponentTypeContainer,
Name: srcMetadata.ImageMetadata.UserInput,
Version: srcMetadata.ImageMetadata.ManifestDigest,
}
case source.DirectoryScheme, source.FileScheme:
bomRef, err := artifact.IDByHash(srcMetadata.Path)
if err != nil {
log.Warnf("unable to get fingerprint of source metadata path=%s: %+v", srcMetadata.Path, err)
}
return &cyclonedx.Component{
BOMRef: string(bomRef),
Type: cyclonedx.ComponentTypeFile,
Name: srcMetadata.Path,
}
}
return nil
}
@@ -0,0 +1,21 @@
package cyclonedxhelpers
import "github.com/anchore/syft/syft/pkg"
func encodeGroup(p pkg.Package) string {
if hasMetadata(p) {
if metadata, ok := p.Metadata.(pkg.JavaMetadata); ok && metadata.PomProperties != nil {
return metadata.PomProperties.GroupID
}
}
return ""
}
func decodeGroup(group string, metadata interface{}) {
if meta, ok := metadata.(*pkg.JavaMetadata); ok {
if meta.PomProperties == nil {
meta.PomProperties = &pkg.PomProperties{}
}
meta.PomProperties.GroupID = group
}
}
@@ -0,0 +1,52 @@
package cyclonedxhelpers
import (
"testing"
"github.com/anchore/syft/syft/pkg"
"github.com/stretchr/testify/assert"
)
func Test_encodeGroup(t *testing.T) {
tests := []struct {
name string
input pkg.Package
expected string
}{
{
name: "no metadata",
input: pkg.Package{},
expected: "",
},
{
name: "metadata is not Java",
input: pkg.Package{
Metadata: pkg.NpmPackageJSONMetadata{},
},
expected: "",
},
{
name: "metadata is Java but pom properties is empty",
input: pkg.Package{
Metadata: pkg.JavaMetadata{},
},
expected: "",
},
{
name: "metadata is Java and contains pomProperties",
input: pkg.Package{
Metadata: pkg.JavaMetadata{
PomProperties: &pkg.PomProperties{
GroupID: "test",
},
},
},
expected: "test",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, encodeGroup(test.input))
})
}
}
@@ -0,0 +1,35 @@
package cyclonedxhelpers
import (
"github.com/CycloneDX/cyclonedx-go"
"github.com/anchore/syft/internal/spdxlicense"
"github.com/anchore/syft/syft/pkg"
)
func encodeLicenses(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
}
func decodeLicenses(c *cyclonedx.Component) (out []string) {
if c.Licenses != nil {
for _, l := range *c.Licenses {
if l.License != nil {
out = append(out, l.License.ID)
}
}
}
return
}
@@ -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_encodeLicense(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-only"}},
},
},
{
name: "cap insensitive",
input: pkg.Package{
Licenses: []string{
"gpl-3.0",
},
},
expected: &cyclonedx.Licenses{
{License: &cyclonedx.License{ID: "GPL-3.0-only"}},
},
},
{
name: "debian to spdx conversion",
input: pkg.Package{
Licenses: []string{
"GPL-2",
},
},
expected: &cyclonedx.Licenses{
{License: &cyclonedx.License{ID: "GPL-2.0-only"}},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, encodeLicenses(test.input))
})
}
}
@@ -0,0 +1,21 @@
package cyclonedxhelpers
import (
"github.com/CycloneDX/cyclonedx-go"
"github.com/anchore/syft/syft/formats/common"
)
var (
CycloneDXFields = common.RequiredTag("cyclonedx")
)
func encodeProperties(obj interface{}, prefix string) (out []cyclonedx.Property) {
for _, p := range common.Sorted(common.Encode(obj, prefix, CycloneDXFields)) {
out = append(out, cyclonedx.Property{
Name: p.Name,
Value: p.Value,
})
}
return
}
@@ -0,0 +1,30 @@
package cyclonedxhelpers
import (
"github.com/anchore/syft/syft/pkg"
)
func encodePublisher(p pkg.Package) string {
if hasMetadata(p) {
switch metadata := p.Metadata.(type) {
case pkg.ApkMetadata:
return metadata.Maintainer
case pkg.RpmMetadata:
return metadata.Vendor
case pkg.DpkgMetadata:
return metadata.Maintainer
}
}
return ""
}
func decodePublisher(publisher string, metadata interface{}) {
switch meta := metadata.(type) {
case *pkg.ApkMetadata:
meta.Maintainer = publisher
case *pkg.RpmMetadata:
meta.Vendor = publisher
case *pkg.DpkgMetadata:
meta.Maintainer = publisher
}
}
@@ -0,0 +1,65 @@
package cyclonedxhelpers
import (
"testing"
"github.com/anchore/syft/syft/pkg"
"github.com/stretchr/testify/assert"
)
func Test_encodePublisher(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.RpmMetadata{
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, encodePublisher(test.input))
})
}
}