Add CycloneDX decoder (#811)

This commit is contained in:
Keith Zantow 2022-02-18 11:19:02 -05:00 committed by GitHub
parent 4b16737b2f
commit 20c1d14f6e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 1027 additions and 66 deletions

View File

@ -7,7 +7,7 @@ import (
"github.com/anchore/syft/syft/pkg"
)
func Author(p pkg.Package) string {
func encodeAuthor(p pkg.Package) string {
if hasMetadata(p) {
switch metadata := p.Metadata.(type) {
case pkg.NpmPackageJSONMetadata:
@ -30,3 +30,18 @@ func Author(p pkg.Package) string {
}
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, ",")
}
}

View File

@ -7,7 +7,7 @@ import (
"github.com/stretchr/testify/assert"
)
func Test_Author(t *testing.T) {
func Test_encodeAuthor(t *testing.T) {
tests := []struct {
name string
input pkg.Package
@ -81,7 +81,7 @@ func Test_Author(t *testing.T) {
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, Author(test.input))
assert.Equal(t, test.expected, encodeAuthor(test.input))
})
}
}

View File

@ -1,27 +1,164 @@
package cyclonedxhelpers
import (
"fmt"
"reflect"
"strconv"
"github.com/CycloneDX/cyclonedx-go"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
func Component(p pkg.Package) cyclonedx.Component {
func encodeComponent(p pkg.Package) cyclonedx.Component {
return cyclonedx.Component{
Type: cyclonedx.ComponentTypeLibrary,
Name: p.Name,
Group: Group(p),
Group: encodeGroup(p),
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),
Licenses: encodeLicenses(p),
CPE: encodeCPE(p),
Author: encodeAuthor(p),
Publisher: encodePublisher(p),
Description: encodeDescription(p),
ExternalReferences: encodeExternalReferences(p),
Properties: encodeProperties(p),
}
}
func hasMetadata(p pkg.Package) bool {
return p.Metadata != nil
}
func decodeComponent(c *cyclonedx.Component) *pkg.Package {
typ := pkg.Type(findPropertyValue(c, "type"))
purl := c.PackageURL
if typ == "" && purl != "" {
typ = pkg.TypeFromPURL(purl)
}
metaType, meta := decodePackageMetadata(c)
p := &pkg.Package{
Name: c.Name,
Version: c.Version,
FoundBy: findPropertyValue(c, "foundBy"),
Locations: decodeLocations(c),
Licenses: decodeLicenses(c),
Language: pkg.Language(findPropertyValue(c, "language")),
Type: typ,
CPEs: decodeCPEs(c),
PURL: purl,
MetadataType: metaType,
Metadata: meta,
}
return p
}
func decodeLocations(c *cyclonedx.Component) (out []source.Location) {
if c.Properties != nil {
props := *c.Properties
for i := 0; i < len(props)-1; i++ {
if props[i].Name == "path" && props[i+1].Name == "layerID" {
out = append(out, source.Location{
Coordinates: source.Coordinates{
RealPath: props[i].Value,
FileSystemID: props[i+1].Value,
},
})
i++
}
}
}
return
}
func mapAllProps(c *cyclonedx.Component, obj reflect.Value) {
value := obj
if value.Kind() == reflect.Ptr {
value = value.Elem()
}
structType := value.Type()
if structType.Kind() != reflect.Struct {
return
}
for i := 0; i < value.NumField(); i++ {
field := structType.Field(i)
fieldType := field.Type
fieldValue := value.Field(i)
name, mapped := field.Tag.Lookup("cyclonedx")
if !mapped {
continue
}
if fieldType.Kind() == reflect.Ptr {
fieldType = fieldType.Elem()
if fieldValue.IsNil() {
newValue := reflect.New(fieldType)
fieldValue.Set(newValue)
}
fieldValue = fieldValue.Elem()
}
propertyValue := findPropertyValue(c, name)
switch fieldType.Kind() {
case reflect.String:
if fieldValue.CanSet() {
fieldValue.SetString(propertyValue)
} else {
msg := fmt.Sprintf("unable to set field: %s.%s", structType.Name(), field.Name)
log.Info(msg)
}
case reflect.Bool:
if b, err := strconv.ParseBool(propertyValue); err == nil {
fieldValue.SetBool(b)
}
case reflect.Int:
if i, err := strconv.Atoi(propertyValue); err == nil {
fieldValue.SetInt(int64(i))
}
case reflect.Float32, reflect.Float64:
if i, err := strconv.ParseFloat(propertyValue, 64); err == nil {
fieldValue.SetFloat(i)
}
case reflect.Struct:
mapAllProps(c, fieldValue)
case reflect.Complex128, reflect.Complex64:
fallthrough
case reflect.Ptr:
msg := fmt.Sprintf("decoding CycloneDX properties to a pointer is not supported: %s.%s", field.Type.Name(), field.Name)
log.Warnf(msg)
}
}
}
func decodePackageMetadata(c *cyclonedx.Component) (pkg.MetadataType, interface{}) {
if c.Properties != nil {
typ := pkg.MetadataType(findPropertyValue(c, "metadataType"))
if typ != "" {
meta := reflect.New(pkg.MetadataTypeByName[typ])
metaPtr := meta.Interface()
// Map all dynamic properties
mapAllProps(c, meta.Elem())
// 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 typ, meta.Elem().Interface()
}
}
return pkg.UnknownMetadataType, nil
}

View File

@ -1,8 +1,12 @@
package cyclonedxhelpers
import "github.com/anchore/syft/syft/pkg"
import (
"github.com/CycloneDX/cyclonedx-go"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/pkg"
)
func CPE(p pkg.Package) string {
func encodeCPE(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 {
@ -10,3 +14,17 @@ func CPE(p pkg.Package) string {
}
return ""
}
func decodeCPEs(c *cyclonedx.Component) []pkg.CPE {
// FIXME we not encoding all the CPEs (see above), so here we just use the single provided one
if c.CPE != "" {
cp, err := pkg.NewCPE(c.CPE)
if err != nil {
log.Warnf("invalid CPE: %s", c.CPE)
} else {
return []pkg.CPE{cp}
}
}
return []pkg.CPE{}
}

View File

@ -7,7 +7,7 @@ import (
"github.com/stretchr/testify/assert"
)
func Test_CPE(t *testing.T) {
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 {
@ -51,7 +51,7 @@ func Test_CPE(t *testing.T) {
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, CPE(test.input))
assert.Equal(t, test.expected, encodeCPE(test.input))
})
}
}

View File

@ -0,0 +1,222 @@
package cyclonedxhelpers
import (
"fmt"
"io"
"github.com/CycloneDX/cyclonedx-go"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/format"
"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) format.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) {
return fmt.Errorf("not a valid CycloneDX document")
}
return nil
}
}
func GetDecoder(format cyclonedx.BOMFileFormat) format.Decoder {
return func(reader io.Reader) (*sbom.SBOM, error) {
bom := &cyclonedx.BOM{}
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) {
meta := source.Metadata{}
if bom.Metadata != nil {
meta = decodeMetadata(bom.Metadata.Component)
}
s := &sbom.SBOM{
Artifacts: sbom.Artifacts{
PackageCatalog: pkg.NewCatalog(),
LinuxDistribution: linuxReleaseFromComponents(*bom.Components),
},
Source: meta,
//Descriptor: sbom.Descriptor{},
}
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
}
}
}
}
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 decodeMetadata(component *cyclonedx.Component) source.Metadata {
switch component.Type {
case cyclonedx.ComponentTypeContainer:
return source.Metadata{
Scheme: source.ImageScheme,
ImageMetadata: source.ImageMetadata{
UserInput: component.Name,
ID: component.BOMRef,
ManifestDigest: component.Version,
},
}
case cyclonedx.ComponentTypeFile:
return source.Metadata{
Scheme: source.FileScheme, // or source.DirectoryScheme
Path: component.Name,
ImageMetadata: source.ImageMetadata{
UserInput: component.Name,
ID: component.BOMRef,
ManifestDigest: component.Version,
},
}
}
return source.Metadata{}
}

View File

@ -0,0 +1,260 @@
package cyclonedxhelpers
import (
"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))
}
}
})
}
}

View File

@ -2,7 +2,7 @@ package cyclonedxhelpers
import "github.com/anchore/syft/syft/pkg"
func Description(p pkg.Package) string {
func encodeDescription(p pkg.Package) string {
if hasMetadata(p) {
switch metadata := p.Metadata.(type) {
case pkg.ApkMetadata:
@ -13,3 +13,12 @@ func Description(p pkg.Package) string {
}
return ""
}
func decodeDescription(description string, metadata interface{}) {
switch meta := metadata.(type) {
case *pkg.ApkMetadata:
meta.Description = description
case *pkg.NpmPackageJSONMetadata:
meta.Description = description
}
}

View File

@ -7,7 +7,7 @@ import (
"github.com/stretchr/testify/assert"
)
func Test_Description(t *testing.T) {
func Test_encodeDescription(t *testing.T) {
tests := []struct {
name string
input pkg.Package
@ -50,7 +50,7 @@ func Test_Description(t *testing.T) {
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, Description(test.input))
assert.Equal(t, test.expected, encodeDescription(test.input))
})
}
}

View File

@ -2,12 +2,13 @@ package cyclonedxhelpers
import (
"fmt"
"strings"
"github.com/CycloneDX/cyclonedx-go"
"github.com/anchore/syft/syft/pkg"
)
func ExternalReferences(p pkg.Package) *[]cyclonedx.ExternalReference {
func encodeExternalReferences(p pkg.Package) *[]cyclonedx.ExternalReference {
refs := []cyclonedx.ExternalReference{}
if hasMetadata(p) {
switch metadata := p.Metadata.(type) {
@ -63,3 +64,51 @@ func ExternalReferences(p pkg.Package) *[]cyclonedx.ExternalReference {
}
return nil
}
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.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 ""
}

View File

@ -8,7 +8,7 @@ import (
"github.com/stretchr/testify/assert"
)
func Test_ExternalReferences(t *testing.T) {
func Test_encodeExternalReferences(t *testing.T) {
tests := []struct {
name string
input pkg.Package
@ -127,7 +127,7 @@ func Test_ExternalReferences(t *testing.T) {
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, ExternalReferences(test.input))
assert.Equal(t, test.expected, encodeExternalReferences(test.input))
})
}
}

View File

@ -27,7 +27,7 @@ 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] = Component(p)
components[i] = encodeComponent(p)
}
components = append(components, toOSComponent(s.Artifacts.LinuxDistribution)...)
cdxBOM.Components = &components
@ -80,9 +80,17 @@ func toOSComponent(distro *linux.Release) []cyclonedx.Component {
}
return []cyclonedx.Component{
{
Type: cyclonedx.ComponentTypeOS,
Name: distro.Name,
Version: distro.Version,
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: props,

View File

@ -2,7 +2,7 @@ package cyclonedxhelpers
import "github.com/anchore/syft/syft/pkg"
func Group(p pkg.Package) string {
func encodeGroup(p pkg.Package) string {
if hasMetadata(p) {
if metadata, ok := p.Metadata.(pkg.JavaMetadata); ok && metadata.PomProperties != nil {
return metadata.PomProperties.GroupID
@ -10,3 +10,12 @@ func Group(p pkg.Package) string {
}
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
}
}

View File

@ -7,7 +7,7 @@ import (
"github.com/stretchr/testify/assert"
)
func TestGroup(t *testing.T) {
func Test_encodeGroup(t *testing.T) {
tests := []struct {
name string
input pkg.Package
@ -46,7 +46,7 @@ func TestGroup(t *testing.T) {
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, Group(test.input))
assert.Equal(t, test.expected, encodeGroup(test.input))
})
}
}

View File

@ -6,7 +6,7 @@ import (
"github.com/anchore/syft/syft/pkg"
)
func Licenses(p pkg.Package) *cyclonedx.Licenses {
func encodeLicenses(p pkg.Package) *cyclonedx.Licenses {
lc := cyclonedx.Licenses{}
for _, licenseName := range p.Licenses {
if value, exists := spdxlicense.ID(licenseName); exists {
@ -22,3 +22,12 @@ func Licenses(p pkg.Package) *cyclonedx.Licenses {
}
return nil
}
func decodeLicenses(c *cyclonedx.Component) (out []string) {
if c.Licenses != nil {
for _, l := range *c.Licenses {
out = append(out, l.License.ID)
}
}
return
}

View File

@ -8,7 +8,7 @@ import (
"github.com/stretchr/testify/assert"
)
func Test_License(t *testing.T) {
func Test_encodeLicense(t *testing.T) {
tests := []struct {
name string
input pkg.Package
@ -77,7 +77,7 @@ func Test_License(t *testing.T) {
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, Licenses(test.input))
assert.Equal(t, test.expected, encodeLicenses(test.input))
})
}
}

View File

@ -8,7 +8,7 @@ import (
"github.com/anchore/syft/syft/pkg"
)
func Properties(p pkg.Package) *[]cyclonedx.Property {
func encodeProperties(p pkg.Package) *[]cyclonedx.Property {
props := []cyclonedx.Property{}
props = append(props, *getCycloneDXProperties(p)...)
if len(p.Locations) > 0 {
@ -76,3 +76,12 @@ func getCycloneDXPropertyValue(field reflect.Value) interface{} {
}
return ""
}
func findPropertyValue(c *cyclonedx.Component, name string) string {
for _, p := range *c.Properties {
if p.Name == name {
return p.Value
}
}
return ""
}

View File

@ -9,7 +9,7 @@ import (
"github.com/stretchr/testify/assert"
)
func Test_Properties(t *testing.T) {
func Test_encodeProperties(t *testing.T) {
epoch := 2
tests := []struct {
name string
@ -132,7 +132,7 @@ func Test_Properties(t *testing.T) {
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, Properties(test.input))
assert.Equal(t, test.expected, encodeProperties(test.input))
})
}
}

View File

@ -4,7 +4,7 @@ import (
"github.com/anchore/syft/syft/pkg"
)
func Publisher(p pkg.Package) string {
func encodePublisher(p pkg.Package) string {
if hasMetadata(p) {
switch metadata := p.Metadata.(type) {
case pkg.ApkMetadata:
@ -17,3 +17,14 @@ func Publisher(p pkg.Package) string {
}
return ""
}
func decodePublisher(publisher string, metadata interface{}) {
switch meta := metadata.(type) {
case *pkg.ApkMetadata:
meta.Maintainer = publisher
case *pkg.RpmdbMetadata:
meta.Vendor = publisher
case *pkg.DpkgMetadata:
meta.Maintainer = publisher
}
}

View File

@ -7,7 +7,7 @@ import (
"github.com/stretchr/testify/assert"
)
func Test_Publisher(t *testing.T) {
func Test_encodePublisher(t *testing.T) {
tests := []struct {
name string
input pkg.Package
@ -59,7 +59,7 @@ func Test_Publisher(t *testing.T) {
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, Publisher(test.input))
assert.Equal(t, test.expected, encodePublisher(test.input))
})
}
}

View File

@ -0,0 +1,70 @@
package cyclonedx13json
import (
"fmt"
"os"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_decodeJSON(t *testing.T) {
tests := []struct {
file string
err bool
distro string
packages []string
}{
{
file: "snapshot/TestCycloneDxDirectoryEncoder.golden",
distro: "debian:1.2.3",
packages: []string{"package-1:1.0.1", "package-2:2.0.1"},
},
{
file: "snapshot/TestCycloneDxImageEncoder.golden",
distro: "debian:1.2.3",
packages: []string{"package-1:1.0.1", "package-2:2.0.1"},
},
{
file: "image-simple/Dockerfile",
err: true,
},
}
for _, test := range tests {
t.Run(test.file, func(t *testing.T) {
reader, err := os.Open("test-fixtures/" + test.file)
assert.NoError(t, err)
if test.err {
err = Format().Validate(reader)
assert.Error(t, err)
return
}
bom, err := Format().Decode(reader)
assert.NoError(t, err)
split := strings.SplitN(test.distro, ":", 2)
name := split[0]
version := split[1]
assert.Equal(t, bom.Artifacts.LinuxDistribution.ID, name)
assert.Equal(t, bom.Artifacts.LinuxDistribution.Version, version)
pkgs:
for _, pkg := range test.packages {
split = strings.SplitN(pkg, ":", 2)
name = split[0]
version = split[1]
for p := range bom.Artifacts.PackageCatalog.Enumerate() {
if p.Name == name {
assert.Equal(t, version, p.Version)
continue pkgs
}
}
assert.Fail(t, fmt.Sprintf("package should be present: %s", pkg))
}
})
}
}

View File

@ -1,12 +1,16 @@
package cyclonedx13json
import "github.com/anchore/syft/syft/format"
import (
"github.com/CycloneDX/cyclonedx-go"
"github.com/anchore/syft/internal/formats/common/cyclonedxhelpers"
"github.com/anchore/syft/syft/format"
)
func Format() format.Format {
return format.NewFormat(
format.CycloneDxJSONOption,
encoder,
nil,
nil,
cyclonedxhelpers.GetDecoder(cyclonedx.BOMFileFormatJSON),
cyclonedxhelpers.GetValidator(cyclonedx.BOMFileFormatJSON),
)
}

View File

@ -1,10 +1,10 @@
{
"bomFormat": "CycloneDX",
"specVersion": "1.3",
"serialNumber": "urn:uuid:258d2616-5b1f-48cd-82a3-d6c95e262950",
"serialNumber": "urn:uuid:326afa86-5620-4a80-8f2b-7f283b954b9b",
"version": 1,
"metadata": {
"timestamp": "2022-01-14T22:47:00Z",
"timestamp": "2022-02-10T17:19:38-05:00",
"tools": [
{
"vendor": "anchore",
@ -13,6 +13,7 @@
}
],
"component": {
"bom-ref": "163686ac6e30c752",
"type": "file",
"name": "/some/path",
"version": ""
@ -84,6 +85,12 @@
"type": "operating-system",
"name": "debian",
"version": "1.2.3",
"description": "debian",
"swid": {
"tagId": "debian",
"name": "debian",
"version": "1.2.3"
},
"properties": [
{
"name": "prettyName",

View File

@ -1,10 +1,10 @@
{
"bomFormat": "CycloneDX",
"specVersion": "1.3",
"serialNumber": "urn:uuid:8a84b1cf-e918-4842-a6a8-c7fdafc55bc0",
"serialNumber": "urn:uuid:761a2036-0f25-4787-bf28-f5e9a7d9a0bf",
"version": 1,
"metadata": {
"timestamp": "2022-01-14T22:47:00Z",
"timestamp": "2022-02-10T17:19:38-05:00",
"tools": [
{
"vendor": "anchore",
@ -13,6 +13,7 @@
}
],
"component": {
"bom-ref": "4f9453fd20e0cf80",
"type": "container",
"name": "user-image-input",
"version": "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368"
@ -55,7 +56,7 @@
},
{
"name": "layerID",
"value": "sha256:16e64541f2ddf59a90391ce7bb8af90313f7d373f2105d88f3d3267b72e0ebab"
"value": "sha256:41e7295da66c405eb3a4df29188dcf80f622f9304d487033a86d4a22e3f01abe"
}
]
},
@ -84,7 +85,7 @@
},
{
"name": "layerID",
"value": "sha256:de6c235f76ea24c8503ec08891445b5d6a8bdf8249117ed8d8b0b6fb3ebe4f67"
"value": "sha256:68a2c166dcb3acf6b7303e995ca1fe7d794bd3b5852a0b4048f9c96b796086aa"
}
]
},
@ -92,6 +93,12 @@
"type": "operating-system",
"name": "debian",
"version": "1.2.3",
"description": "debian",
"swid": {
"tagId": "debian",
"name": "debian",
"version": "1.2.3"
},
"properties": [
{
"name": "prettyName",

View File

@ -0,0 +1,70 @@
package cyclonedx13xml
import (
"fmt"
"os"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_decodeXML(t *testing.T) {
tests := []struct {
file string
err bool
distro string
packages []string
}{
{
file: "snapshot/TestCycloneDxDirectoryEncoder.golden",
distro: "debian:1.2.3",
packages: []string{"package-1:1.0.1", "package-2:2.0.1"},
},
{
file: "snapshot/TestCycloneDxImageEncoder.golden",
distro: "debian:1.2.3",
packages: []string{"package-1:1.0.1", "package-2:2.0.1"},
},
{
file: "image-simple/Dockerfile",
err: true,
},
}
for _, test := range tests {
t.Run(test.file, func(t *testing.T) {
reader, err := os.Open("test-fixtures/" + test.file)
assert.NoError(t, err)
if test.err {
err = Format().Validate(reader)
assert.Error(t, err)
return
}
bom, err := Format().Decode(reader)
assert.NoError(t, err)
split := strings.SplitN(test.distro, ":", 2)
name := split[0]
version := split[1]
assert.Equal(t, bom.Artifacts.LinuxDistribution.ID, name)
assert.Equal(t, bom.Artifacts.LinuxDistribution.Version, version)
pkgs:
for _, pkg := range test.packages {
split = strings.SplitN(pkg, ":", 2)
name = split[0]
version = split[1]
for p := range bom.Artifacts.PackageCatalog.Enumerate() {
if p.Name == name {
assert.Equal(t, version, p.Version)
continue pkgs
}
}
assert.Fail(t, fmt.Sprintf("package should be present: %s", pkg))
}
})
}
}

View File

@ -1,12 +1,16 @@
package cyclonedx13xml
import "github.com/anchore/syft/syft/format"
import (
"github.com/CycloneDX/cyclonedx-go"
"github.com/anchore/syft/internal/formats/common/cyclonedxhelpers"
"github.com/anchore/syft/syft/format"
)
func Format() format.Format {
return format.NewFormat(
format.CycloneDxXMLOption,
encoder,
nil,
nil,
cyclonedxhelpers.GetDecoder(cyclonedx.BOMFileFormatXML),
cyclonedxhelpers.GetValidator(cyclonedx.BOMFileFormatXML),
)
}

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:16a426e7-fcc7-4b94-abd2-66c67569cc44" version="1">
<bom xmlns="http://cyclonedx.org/schema/bom/1.3" serialNumber="urn:uuid:25133adc-01af-46ad-a198-002034e54097" version="1">
<metadata>
<timestamp>2022-01-14T22:46:49Z</timestamp>
<timestamp>2022-02-10T17:18:31-05:00</timestamp>
<tools>
<tool>
<vendor>anchore</vendor>
@ -9,7 +9,7 @@
<version>[not provided]</version>
</tool>
</tools>
<component type="file">
<component bom-ref="163686ac6e30c752" type="file">
<name>/some/path</name>
<version></version>
</component>
@ -48,6 +48,8 @@
<component type="operating-system">
<name>debian</name>
<version>1.2.3</version>
<description>debian</description>
<swid tagId="debian" name="debian" version="1.2.3"></swid>
<properties>
<property name="prettyName">debian</property>
<property name="id">debian</property>

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:a3d776c6-a2ca-4116-8b99-b3538dd1a460" version="1">
<bom xmlns="http://cyclonedx.org/schema/bom/1.3" serialNumber="urn:uuid:e702d78f-9372-4458-ac38-be8e80fc3005" version="1">
<metadata>
<timestamp>2022-01-14T22:46:49Z</timestamp>
<timestamp>2022-02-10T17:18:31-05:00</timestamp>
<tools>
<tool>
<vendor>anchore</vendor>
@ -9,7 +9,7 @@
<version>[not provided]</version>
</tool>
</tools>
<component type="container">
<component bom-ref="4f9453fd20e0cf80" type="container">
<name>user-image-input</name>
<version>sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368</version>
</component>
@ -31,7 +31,7 @@
<property name="type">python</property>
<property name="metadataType">PythonPackageMetadata</property>
<property name="path">/somefile-1.txt</property>
<property name="layerID">sha256:16e64541f2ddf59a90391ce7bb8af90313f7d373f2105d88f3d3267b72e0ebab</property>
<property name="layerID">sha256:41e7295da66c405eb3a4df29188dcf80f622f9304d487033a86d4a22e3f01abe</property>
</properties>
</component>
<component type="library">
@ -44,12 +44,14 @@
<property name="type">deb</property>
<property name="metadataType">DpkgMetadata</property>
<property name="path">/somefile-2.txt</property>
<property name="layerID">sha256:de6c235f76ea24c8503ec08891445b5d6a8bdf8249117ed8d8b0b6fb3ebe4f67</property>
<property name="layerID">sha256:68a2c166dcb3acf6b7303e995ca1fe7d794bd3b5852a0b4048f9c96b796086aa</property>
</properties>
</component>
<component type="operating-system">
<name>debian</name>
<version>1.2.3</version>
<description>debian</description>
<swid tagId="debian" name="debian" version="1.2.3"></swid>
<properties>
<property name="prettyName">debian</property>
<property name="id">debian</property>

View File

@ -1,14 +1,12 @@
package rust
import "github.com/anchore/syft/syft/pkg"
package pkg
type CargoMetadata struct {
Packages []pkg.CargoPackageMetadata `toml:"package"`
Packages []CargoPackageMetadata `toml:"package"`
}
// Pkgs returns all of the packages referenced within the Cargo.lock metadata.
func (m CargoMetadata) Pkgs() []*pkg.Package {
pkgs := make([]*pkg.Package, 0)
func (m CargoMetadata) Pkgs() []*Package {
pkgs := make([]*Package, 0)
for _, p := range m.Packages {
if p.Dependencies == nil {

View File

@ -20,7 +20,7 @@ func parseCargoLock(_ string, reader io.Reader) ([]*pkg.Package, []artifact.Rela
return nil, nil, fmt.Errorf("unable to load Cargo.lock for parsing: %v", err)
}
metadata := CargoMetadata{}
metadata := pkg.CargoMetadata{}
err = tree.Unmarshal(&metadata)
if err != nil {
return nil, nil, fmt.Errorf("unable to parse Cargo.lock: %v", err)

View File

@ -1,5 +1,9 @@
package pkg
import (
"reflect"
)
// MetadataType represents the data shape stored within pkg.Package.Metadata.
type MetadataType string
@ -33,3 +37,17 @@ var AllMetadataTypes = []MetadataType{
GolangBinMetadataType,
PhpComposerJSONMetadataType,
}
var MetadataTypeByName = map[MetadataType]reflect.Type{
ApkMetadataType: reflect.TypeOf(ApkMetadata{}),
DpkgMetadataType: reflect.TypeOf(DpkgMetadata{}),
GemMetadataType: reflect.TypeOf(GemMetadata{}),
JavaMetadataType: reflect.TypeOf(JavaMetadata{}),
NpmPackageJSONMetadataType: reflect.TypeOf(NpmPackageJSONMetadata{}),
RpmdbMetadataType: reflect.TypeOf(RpmdbMetadata{}),
PythonPackageMetadataType: reflect.TypeOf(PythonPackageMetadata{}),
RustCargoPackageMetadataType: reflect.TypeOf(CargoMetadata{}),
KbPackageMetadataType: reflect.TypeOf(KbPackageMetadata{}),
GolangBinMetadataType: reflect.TypeOf(GolangBinMetadata{}),
PhpComposerJSONMetadataType: reflect.TypeOf(PhpComposerJSONMetadata{}),
}

View File

@ -2,6 +2,7 @@ package integration
import (
"bytes"
"regexp"
"testing"
"github.com/anchore/syft/syft"
@ -20,11 +21,27 @@ import (
// encode-decode-encode loop which will detect lossy behavior in both directions.
func TestEncodeDecodeEncodeCycleComparison(t *testing.T) {
tests := []struct {
format format.Option
format format.Option
redactor func(in []byte) []byte
}{
{
format: format.JSONOption,
},
{
format: format.CycloneDxJSONOption,
redactor: func(in []byte) []byte {
in = regexp.MustCompile("\"(timestamp|serialNumber|bom-ref)\": \"[^\"]+").ReplaceAll(in, []byte{})
return in
},
},
{
format: format.CycloneDxXMLOption,
redactor: func(in []byte) []byte {
in = regexp.MustCompile("(serialNumber|bom-ref)=\"[^\"]+").ReplaceAll(in, []byte{})
in = regexp.MustCompile("<timestamp>[^<]+</timestamp>").ReplaceAll(in, []byte{})
return in
},
},
}
for _, test := range tests {
t.Run(string(test.format), func(t *testing.T) {
@ -33,6 +50,7 @@ func TestEncodeDecodeEncodeCycleComparison(t *testing.T) {
by1, err := syft.Encode(originalSBOM, test.format)
assert.NoError(t, err)
newSBOM, newFormat, err := syft.Decode(bytes.NewReader(by1))
assert.NoError(t, err)
assert.Equal(t, test.format, newFormat)
@ -40,6 +58,11 @@ func TestEncodeDecodeEncodeCycleComparison(t *testing.T) {
by2, err := syft.Encode(*newSBOM, test.format)
assert.NoError(t, err)
if test.redactor != nil {
by1 = test.redactor(by1)
by2 = test.redactor(by2)
}
if !assert.True(t, bytes.Equal(by1, by2)) {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(string(by1), string(by2), true)