mirror of
https://github.com/anchore/syft.git
synced 2025-11-17 16:33:21 +01:00
Update CycloneDX to use syft namespace and output multiple CPEs (#849)
This commit is contained in:
parent
d2f28e0eb1
commit
edac8c7bf7
@ -1,18 +1,30 @@
|
|||||||
package cyclonedxhelpers
|
package cyclonedxhelpers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/CycloneDX/cyclonedx-go"
|
"github.com/CycloneDX/cyclonedx-go"
|
||||||
|
|
||||||
"github.com/anchore/syft/internal/log"
|
"github.com/anchore/syft/internal/formats/common"
|
||||||
"github.com/anchore/syft/syft/pkg"
|
"github.com/anchore/syft/syft/pkg"
|
||||||
"github.com/anchore/syft/syft/source"
|
"github.com/anchore/syft/syft/source"
|
||||||
)
|
)
|
||||||
|
|
||||||
func encodeComponent(p pkg.Package) cyclonedx.Component {
|
func encodeComponent(p pkg.Package) cyclonedx.Component {
|
||||||
|
props := encodeProperties(p, "syft:package")
|
||||||
|
props = append(props, encodeCPEs(p)...)
|
||||||
|
if len(p.Locations) > 0 {
|
||||||
|
props = append(props, encodeProperties(p.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{
|
return cyclonedx.Component{
|
||||||
Type: cyclonedx.ComponentTypeLibrary,
|
Type: cyclonedx.ComponentTypeLibrary,
|
||||||
Name: p.Name,
|
Name: p.Name,
|
||||||
@ -20,12 +32,12 @@ func encodeComponent(p pkg.Package) cyclonedx.Component {
|
|||||||
Version: p.Version,
|
Version: p.Version,
|
||||||
PackageURL: p.PURL,
|
PackageURL: p.PURL,
|
||||||
Licenses: encodeLicenses(p),
|
Licenses: encodeLicenses(p),
|
||||||
CPE: encodeCPE(p),
|
CPE: encodeSingleCPE(p),
|
||||||
Author: encodeAuthor(p),
|
Author: encodeAuthor(p),
|
||||||
Publisher: encodePublisher(p),
|
Publisher: encodePublisher(p),
|
||||||
Description: encodeDescription(p),
|
Description: encodeDescription(p),
|
||||||
ExternalReferences: encodeExternalReferences(p),
|
ExternalReferences: encodeExternalReferences(p),
|
||||||
Properties: encodeProperties(p),
|
Properties: properties,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,131 +46,56 @@ func hasMetadata(p pkg.Package) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func decodeComponent(c *cyclonedx.Component) *pkg.Package {
|
func decodeComponent(c *cyclonedx.Component) *pkg.Package {
|
||||||
typ := pkg.Type(findPropertyValue(c, "type"))
|
values := map[string]string{}
|
||||||
purl := c.PackageURL
|
for _, p := range *c.Properties {
|
||||||
if typ == "" && purl != "" {
|
values[p.Name] = p.Value
|
||||||
typ = pkg.TypeFromPURL(purl)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
metaType, meta := decodePackageMetadata(c)
|
|
||||||
|
|
||||||
p := &pkg.Package{
|
p := &pkg.Package{
|
||||||
Name: c.Name,
|
Name: c.Name,
|
||||||
Version: c.Version,
|
Version: c.Version,
|
||||||
FoundBy: findPropertyValue(c, "foundBy"),
|
Locations: decodeLocations(values),
|
||||||
Locations: decodeLocations(c),
|
Licenses: decodeLicenses(c),
|
||||||
Licenses: decodeLicenses(c),
|
CPEs: decodeCPEs(c),
|
||||||
Language: pkg.Language(findPropertyValue(c, "language")),
|
PURL: c.PackageURL,
|
||||||
Type: typ,
|
}
|
||||||
CPEs: decodeCPEs(c),
|
|
||||||
PURL: purl,
|
common.DecodeInto(p, values, "syft:package", CycloneDXFields)
|
||||||
MetadataType: metaType,
|
|
||||||
Metadata: meta,
|
p.Metadata = decodePackageMetadata(values, c, p.MetadataType)
|
||||||
|
|
||||||
|
if p.Type == "" {
|
||||||
|
p.Type = pkg.TypeFromPURL(p.PURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
func decodeLocations(c *cyclonedx.Component) (out []source.Location) {
|
func decodeLocations(vals map[string]string) []source.Location {
|
||||||
if c.Properties != nil {
|
v := common.Decode(reflect.TypeOf([]source.Location{}), vals, "syft:location", CycloneDXFields)
|
||||||
props := *c.Properties
|
out, _ := v.([]source.Location)
|
||||||
for i := 0; i < len(props)-1; i++ {
|
return out
|
||||||
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) {
|
func decodePackageMetadata(vals map[string]string, c *cyclonedx.Component, typ pkg.MetadataType) interface{} {
|
||||||
value := obj
|
if typ != "" && c.Properties != nil {
|
||||||
if value.Kind() == reflect.Ptr {
|
metaTyp, ok := pkg.MetadataTypeByName[typ]
|
||||||
value = value.Elem()
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
structType := value.Type()
|
return nil
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,12 +4,13 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/CycloneDX/cyclonedx-go"
|
"github.com/CycloneDX/cyclonedx-go"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
"github.com/anchore/syft/syft/pkg"
|
"github.com/anchore/syft/syft/pkg"
|
||||||
"github.com/anchore/syft/syft/source"
|
"github.com/anchore/syft/syft/source"
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_encodeProperties(t *testing.T) {
|
func Test_encodeComponentProperties(t *testing.T) {
|
||||||
epoch := 2
|
epoch := 2
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@ -46,13 +47,14 @@ func Test_encodeProperties(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
expected: &[]cyclonedx.Property{
|
expected: &[]cyclonedx.Property{
|
||||||
{Name: "foundBy", Value: "cataloger"},
|
{Name: "syft:package:foundBy", Value: "cataloger"},
|
||||||
{Name: "path", Value: "test"},
|
{Name: "syft:location:0:path", Value: "test"},
|
||||||
{Name: "originPackage", Value: "libc-dev"},
|
{Name: "syft:metadata:gitCommitOfApkPort", Value: "97b1c2842faa3bfa30f5811ffbf16d5ff9f1a479"},
|
||||||
{Name: "installedSize", Value: "4096"},
|
{Name: "syft:metadata:installedSize", Value: "4096"},
|
||||||
{Name: "pullDependencies", Value: "musl-utils"},
|
{Name: "syft:metadata:originPackage", Value: "libc-dev"},
|
||||||
{Name: "pullChecksum", Value: "Q1p78yvTLG094tHE1+dToJGbmYzQE="},
|
{Name: "syft:metadata:pullChecksum", Value: "Q1p78yvTLG094tHE1+dToJGbmYzQE="},
|
||||||
{Name: "gitCommitOfApkPort", Value: "97b1c2842faa3bfa30f5811ffbf16d5ff9f1a479"},
|
{Name: "syft:metadata:pullDependencies", Value: "musl-utils"},
|
||||||
|
{Name: "syft:metadata:size", Value: "0"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -71,10 +73,10 @@ func Test_encodeProperties(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
expected: &[]cyclonedx.Property{
|
expected: &[]cyclonedx.Property{
|
||||||
{Name: "metadataType", Value: "DpkgMetadata"},
|
{Name: "syft:package:metadataType", Value: "DpkgMetadata"},
|
||||||
{Name: "source", Value: "tzdata-dev"},
|
{Name: "syft:metadata:installedSize", Value: "3036"},
|
||||||
{Name: "sourceVersion", Value: "1.0"},
|
{Name: "syft:metadata:source", Value: "tzdata-dev"},
|
||||||
{Name: "installedSize", Value: "3036"},
|
{Name: "syft:metadata:sourceVersion", Value: "1.0"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -92,12 +94,12 @@ func Test_encodeProperties(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
expected: &[]cyclonedx.Property{
|
expected: &[]cyclonedx.Property{
|
||||||
{Name: "language", Value: pkg.Go.String()},
|
{Name: "syft:package:language", Value: pkg.Go.String()},
|
||||||
{Name: "type", Value: "go-module"},
|
{Name: "syft:package:metadataType", Value: "GolangBinMetadata"},
|
||||||
{Name: "metadataType", Value: "GolangBinMetadata"},
|
{Name: "syft:package:type", Value: "go-module"},
|
||||||
{Name: "goCompiledVersion", Value: "1.17"},
|
{Name: "syft:metadata:architecture", Value: "amd64"},
|
||||||
{Name: "architecture", Value: "amd64"},
|
{Name: "syft:metadata:goCompiledVersion", Value: "1.17"},
|
||||||
{Name: "h1Digest", Value: "h1:KlOXYy8wQWTUJYFgkUI40Lzr06ofg5IRXUK5C7qZt1k="},
|
{Name: "syft:metadata:h1Digest", Value: "h1:KlOXYy8wQWTUJYFgkUI40Lzr06ofg5IRXUK5C7qZt1k="},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -121,18 +123,19 @@ func Test_encodeProperties(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
expected: &[]cyclonedx.Property{
|
expected: &[]cyclonedx.Property{
|
||||||
{Name: "type", Value: "rpm"},
|
{Name: "syft:package:metadataType", Value: "RpmdbMetadata"},
|
||||||
{Name: "metadataType", Value: "RpmdbMetadata"},
|
{Name: "syft:package:type", Value: "rpm"},
|
||||||
{Name: "epoch", Value: "2"},
|
{Name: "syft:metadata:epoch", Value: "2"},
|
||||||
{Name: "release", Value: "1"},
|
{Name: "syft:metadata:release", Value: "1"},
|
||||||
{Name: "sourceRpm", Value: "dive-0.9.2-1.src.rpm"},
|
{Name: "syft:metadata:size", Value: "12406784"},
|
||||||
{Name: "size", Value: "12406784"},
|
{Name: "syft:metadata:sourceRpm", Value: "dive-0.9.2-1.src.rpm"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
assert.Equal(t, test.expected, encodeProperties(test.input))
|
c := encodeComponent(test.input)
|
||||||
|
assert.Equal(t, test.expected, c.Properties)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2,11 +2,12 @@ package cyclonedxhelpers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/CycloneDX/cyclonedx-go"
|
"github.com/CycloneDX/cyclonedx-go"
|
||||||
|
|
||||||
"github.com/anchore/syft/internal/log"
|
"github.com/anchore/syft/internal/log"
|
||||||
"github.com/anchore/syft/syft/pkg"
|
"github.com/anchore/syft/syft/pkg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func encodeCPE(p pkg.Package) string {
|
func encodeSingleCPE(p pkg.Package) string {
|
||||||
// Since the CPEs in a package are sorted by specificity
|
// Since the CPEs in a package are sorted by specificity
|
||||||
// we can extract the first CPE as the one to output in cyclonedx
|
// we can extract the first CPE as the one to output in cyclonedx
|
||||||
if len(p.CPEs) > 0 {
|
if len(p.CPEs) > 0 {
|
||||||
@ -15,16 +16,42 @@ func encodeCPE(p pkg.Package) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func decodeCPEs(c *cyclonedx.Component) []pkg.CPE {
|
func encodeCPEs(p pkg.Package) (out []cyclonedx.Property) {
|
||||||
// FIXME we not encoding all the CPEs (see above), so here we just use the single provided one
|
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 != "" {
|
if c.CPE != "" {
|
||||||
cp, err := pkg.NewCPE(c.CPE)
|
cp, err := pkg.NewCPE(c.CPE)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("invalid CPE: %s", c.CPE)
|
log.Warnf("invalid CPE: %s", c.CPE)
|
||||||
} else {
|
} else {
|
||||||
return []pkg.CPE{cp}
|
out = append(out, cp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return []pkg.CPE{}
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,8 +3,9 @@ package cyclonedxhelpers
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/anchore/syft/syft/pkg"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/pkg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_encodeCPE(t *testing.T) {
|
func Test_encodeCPE(t *testing.T) {
|
||||||
@ -51,7 +52,7 @@ func Test_encodeCPE(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
assert.Equal(t, test.expected, encodeCPE(test.input))
|
assert.Equal(t, test.expected, encodeSingleCPE(test.input))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/CycloneDX/cyclonedx-go"
|
"github.com/CycloneDX/cyclonedx-go"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
|
||||||
"github.com/anchore/syft/internal"
|
"github.com/anchore/syft/internal"
|
||||||
"github.com/anchore/syft/internal/log"
|
"github.com/anchore/syft/internal/log"
|
||||||
"github.com/anchore/syft/internal/version"
|
"github.com/anchore/syft/internal/version"
|
||||||
@ -11,7 +13,6 @@ import (
|
|||||||
"github.com/anchore/syft/syft/linux"
|
"github.com/anchore/syft/syft/linux"
|
||||||
"github.com/anchore/syft/syft/sbom"
|
"github.com/anchore/syft/syft/sbom"
|
||||||
"github.com/anchore/syft/syft/source"
|
"github.com/anchore/syft/syft/source"
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func ToFormatModel(s sbom.SBOM) *cyclonedx.BOM {
|
func ToFormatModel(s sbom.SBOM) *cyclonedx.BOM {
|
||||||
@ -74,9 +75,10 @@ func toOSComponent(distro *linux.Release) []cyclonedx.Component {
|
|||||||
if len(*eRefs) == 0 {
|
if len(*eRefs) == 0 {
|
||||||
eRefs = nil
|
eRefs = nil
|
||||||
}
|
}
|
||||||
props := getCycloneDXProperties(*distro)
|
props := encodeProperties(distro, "syft:distro")
|
||||||
if len(*props) == 0 {
|
var properties *[]cyclonedx.Property
|
||||||
props = nil
|
if len(props) > 0 {
|
||||||
|
properties = &props
|
||||||
}
|
}
|
||||||
return []cyclonedx.Component{
|
return []cyclonedx.Component{
|
||||||
{
|
{
|
||||||
@ -93,7 +95,7 @@ func toOSComponent(distro *linux.Release) []cyclonedx.Component {
|
|||||||
// TODO should we add a PURL?
|
// TODO should we add a PURL?
|
||||||
CPE: distro.CPEName,
|
CPE: distro.CPEName,
|
||||||
ExternalReferences: eRefs,
|
ExternalReferences: eRefs,
|
||||||
Properties: props,
|
Properties: properties,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,87 +1,21 @@
|
|||||||
package cyclonedxhelpers
|
package cyclonedxhelpers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
|
|
||||||
"github.com/CycloneDX/cyclonedx-go"
|
"github.com/CycloneDX/cyclonedx-go"
|
||||||
"github.com/anchore/syft/syft/pkg"
|
|
||||||
|
"github.com/anchore/syft/internal/formats/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
func encodeProperties(p pkg.Package) *[]cyclonedx.Property {
|
var (
|
||||||
props := []cyclonedx.Property{}
|
CycloneDXFields = common.RequiredTag("cyclonedx")
|
||||||
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 {
|
func encodeProperties(obj interface{}, prefix string) (out []cyclonedx.Property) {
|
||||||
props := []cyclonedx.Property{}
|
for _, p := range common.Sorted(common.Encode(obj, prefix, CycloneDXFields)) {
|
||||||
structValue := reflect.ValueOf(m)
|
out = append(out, cyclonedx.Property{
|
||||||
if structValue.Kind() != reflect.Struct {
|
Name: p.Name,
|
||||||
return &props
|
Value: p.Value,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
structType := structValue.Type()
|
return
|
||||||
for i := 0; i < structValue.NumField(); i++ {
|
|
||||||
if name, value := getCycloneDXPropertyName(structType.Field(i)), getCycloneDXPropertyValue(structValue.Field(i)); name != "" && value != "" {
|
|
||||||
// In the case of the value is a struct and has cyclonedx tag with name "-"
|
|
||||||
// call the getCycloneDXProperties recursively.
|
|
||||||
if name == "-" && reflect.ValueOf(value).Kind() == reflect.Struct {
|
|
||||||
props = append(props, *getCycloneDXProperties(value)...)
|
|
||||||
} else if reflect.ValueOf(value).Kind() == reflect.String {
|
|
||||||
props = append(props, cyclonedx.Property{
|
|
||||||
Name: name,
|
|
||||||
Value: fmt.Sprint(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) interface{} {
|
|
||||||
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.Struct:
|
|
||||||
if field.CanInterface() {
|
|
||||||
return field.Interface()
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
case reflect.Ptr:
|
|
||||||
return getCycloneDXPropertyValue(reflect.Indirect(field))
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func findPropertyValue(c *cyclonedx.Component, name string) string {
|
|
||||||
for _, p := range *c.Properties {
|
|
||||||
if p.Name == name {
|
|
||||||
return p.Value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
}
|
||||||
|
|||||||
322
internal/formats/common/property_encoder.go
Normal file
322
internal/formats/common/property_encoder.go
Normal file
@ -0,0 +1,322 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/internal/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FieldName return a flag to indicate this is a valid field and a name to use
|
||||||
|
type FieldName func(field reflect.StructField) (string, bool)
|
||||||
|
|
||||||
|
// OptionalTag given a tag name, will return the defined tag or fall back to lower camel case field name
|
||||||
|
func OptionalTag(tag string) FieldName {
|
||||||
|
return func(f reflect.StructField) (string, bool) {
|
||||||
|
if n, ok := f.Tag.Lookup(tag); ok {
|
||||||
|
return n, true
|
||||||
|
}
|
||||||
|
return lowerFirst(f.Name), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrimOmitempty trims `,omitempty` from the name
|
||||||
|
func TrimOmitempty(fn FieldName) FieldName {
|
||||||
|
return func(f reflect.StructField) (string, bool) {
|
||||||
|
if v, ok := fn(f); ok {
|
||||||
|
return strings.TrimSuffix(v, ",omitempty"), true
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequiredTag based on the given tag, only use a field if present
|
||||||
|
func RequiredTag(tag string) FieldName {
|
||||||
|
return func(f reflect.StructField) (string, bool) {
|
||||||
|
if n, ok := f.Tag.Lookup(tag); ok {
|
||||||
|
return n, true
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// OptionalJSONTag uses field names defined in json tags, if available
|
||||||
|
OptionalJSONTag = TrimOmitempty(OptionalTag("json"))
|
||||||
|
)
|
||||||
|
|
||||||
|
// lowerFirst converts the first character of the string to lower case
|
||||||
|
func lowerFirst(s string) string {
|
||||||
|
return strings.ToLower(s[0:1]) + s[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode recursively encodes the object's properties as an ordered set of NameValue pairs
|
||||||
|
func Encode(obj interface{}, prefix string, fn FieldName) map[string]string {
|
||||||
|
if obj == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
props := map[string]string{}
|
||||||
|
encode(props, reflect.ValueOf(obj), prefix, fn)
|
||||||
|
return props
|
||||||
|
}
|
||||||
|
|
||||||
|
// NameValue a simple type to store stringified name/value pairs
|
||||||
|
type NameValue struct {
|
||||||
|
Name string
|
||||||
|
Value string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sorted returns a sorted set of NameValue pairs
|
||||||
|
func Sorted(values map[string]string) (out []NameValue) {
|
||||||
|
var keys []string
|
||||||
|
for k := range values {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
for _, k := range keys {
|
||||||
|
out = append(out, NameValue{
|
||||||
|
Name: k,
|
||||||
|
Value: values[k],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(out map[string]string, value reflect.Value, prefix string, fn FieldName) {
|
||||||
|
if !value.IsValid() || value.Type() == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
typ := value.Type()
|
||||||
|
|
||||||
|
switch typ.Kind() {
|
||||||
|
case reflect.Ptr:
|
||||||
|
if value.IsNil() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
value = value.Elem()
|
||||||
|
encode(out, value, prefix, fn)
|
||||||
|
case reflect.String:
|
||||||
|
v := value.String()
|
||||||
|
if v != "" {
|
||||||
|
out[prefix] = v
|
||||||
|
}
|
||||||
|
case reflect.Bool:
|
||||||
|
v := value.Bool()
|
||||||
|
out[prefix] = strconv.FormatBool(v)
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
v := value.Int()
|
||||||
|
out[prefix] = strconv.FormatInt(v, 10)
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
v := value.Uint()
|
||||||
|
out[prefix] = strconv.FormatUint(v, 10)
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
v := value.Float()
|
||||||
|
out[prefix] = fmt.Sprintf("%f", v)
|
||||||
|
case reflect.Array, reflect.Slice:
|
||||||
|
for idx := 0; idx < value.Len(); idx++ {
|
||||||
|
encode(out, value.Index(idx), fmt.Sprintf("%s:%d", prefix, idx), fn)
|
||||||
|
}
|
||||||
|
case reflect.Struct:
|
||||||
|
for i := 0; i < typ.NumField(); i++ {
|
||||||
|
pv := value.Field(i)
|
||||||
|
f := typ.Field(i)
|
||||||
|
name, ok := fieldName(f, prefix, fn)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
encode(out, pv, name, fn)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
log.Warnf("skipping encoding of unsupported property: %s", prefix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fieldName gets the name of the field using the provided FieldName function
|
||||||
|
func fieldName(f reflect.StructField, prefix string, fn FieldName) (string, bool) {
|
||||||
|
name, ok := fn(f)
|
||||||
|
if !ok {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
if name == "" {
|
||||||
|
return prefix, true
|
||||||
|
}
|
||||||
|
if prefix != "" {
|
||||||
|
name = fmt.Sprintf("%s:%s", prefix, name)
|
||||||
|
}
|
||||||
|
return name, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode based on the given type, applies all values to hydrate a new instance
|
||||||
|
func Decode(typ reflect.Type, values map[string]string, prefix string, fn FieldName) interface{} {
|
||||||
|
isPtr := false
|
||||||
|
for typ.Kind() == reflect.Ptr {
|
||||||
|
typ = typ.Elem()
|
||||||
|
isPtr = true
|
||||||
|
}
|
||||||
|
|
||||||
|
isSlice := false
|
||||||
|
if typ.Kind() == reflect.Slice {
|
||||||
|
typ = reflect.PtrTo(typ)
|
||||||
|
isSlice = true
|
||||||
|
}
|
||||||
|
|
||||||
|
v := reflect.New(typ)
|
||||||
|
|
||||||
|
decode(values, v, prefix, fn)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case isSlice && isPtr:
|
||||||
|
return v.Elem().Interface()
|
||||||
|
case isSlice:
|
||||||
|
return PtrToStruct(v.Elem().Interface())
|
||||||
|
case isPtr:
|
||||||
|
return v.Interface()
|
||||||
|
}
|
||||||
|
return v.Elem().Interface()
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeInto decodes all values to hydrate the given object instance
|
||||||
|
func DecodeInto(obj interface{}, values map[string]string, prefix string, fn FieldName) {
|
||||||
|
value := reflect.ValueOf(obj)
|
||||||
|
|
||||||
|
for value.Type().Kind() == reflect.Ptr {
|
||||||
|
value = value.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
decode(values, value, prefix, fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// nolint: funlen, gocognit, gocyclo
|
||||||
|
func decode(vals map[string]string, value reflect.Value, prefix string, fn FieldName) bool {
|
||||||
|
if !value.IsValid() || value.Type() == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
typ := value.Type()
|
||||||
|
|
||||||
|
incoming, valid := vals[prefix]
|
||||||
|
switch typ.Kind() {
|
||||||
|
case reflect.Ptr:
|
||||||
|
t := typ.Elem()
|
||||||
|
v := value
|
||||||
|
if v.IsNil() {
|
||||||
|
v = reflect.New(t)
|
||||||
|
}
|
||||||
|
if decode(vals, v.Elem(), prefix, fn) && value.CanSet() {
|
||||||
|
o := v.Interface()
|
||||||
|
log.Infof("%v", o)
|
||||||
|
value.Set(v)
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case reflect.String:
|
||||||
|
if valid {
|
||||||
|
value.SetString(incoming)
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case reflect.Bool:
|
||||||
|
if !valid {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if b, err := strconv.ParseBool(incoming); err == nil {
|
||||||
|
value.SetBool(b)
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
if !valid {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if i, err := strconv.ParseInt(incoming, 10, 64); err == nil {
|
||||||
|
value.SetInt(i)
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
if !valid {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if i, err := strconv.ParseUint(incoming, 10, 64); err == nil {
|
||||||
|
value.SetUint(i)
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
if !valid {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if i, err := strconv.ParseFloat(incoming, 64); err == nil {
|
||||||
|
value.SetFloat(i)
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case reflect.Array, reflect.Slice:
|
||||||
|
values := false
|
||||||
|
t := typ.Elem()
|
||||||
|
slice := reflect.MakeSlice(typ, 0, 0)
|
||||||
|
for idx := 0; ; idx++ {
|
||||||
|
// test for index
|
||||||
|
str := fmt.Sprintf("%s:%d", prefix, idx)
|
||||||
|
// create new placeholder and decode values
|
||||||
|
newType := t
|
||||||
|
if t.Kind() == reflect.Ptr {
|
||||||
|
newType = t.Elem()
|
||||||
|
}
|
||||||
|
v := reflect.New(newType)
|
||||||
|
if decode(vals, v.Elem(), str, fn) {
|
||||||
|
// append to slice
|
||||||
|
if t.Kind() != reflect.Ptr {
|
||||||
|
v = v.Elem()
|
||||||
|
}
|
||||||
|
slice = reflect.Append(slice, v)
|
||||||
|
values = true
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if values {
|
||||||
|
value.Set(slice)
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case reflect.Struct:
|
||||||
|
values := false
|
||||||
|
for i := 0; i < typ.NumField(); i++ {
|
||||||
|
f := typ.Field(i)
|
||||||
|
v := value.Field(i)
|
||||||
|
|
||||||
|
name, ok := fieldName(f, prefix, fn)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if decode(vals, v, name, fn) {
|
||||||
|
values = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return values
|
||||||
|
default:
|
||||||
|
log.Warnf("unable to set field: %s", prefix)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func PtrToStruct(ptr interface{}) interface{} {
|
||||||
|
v := reflect.ValueOf(ptr)
|
||||||
|
if v.IsZero() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
switch v.Type().Kind() {
|
||||||
|
case reflect.Ptr:
|
||||||
|
return PtrToStruct(v.Elem().Interface())
|
||||||
|
case reflect.Interface:
|
||||||
|
return PtrToStruct(v.Elem().Interface())
|
||||||
|
}
|
||||||
|
return v.Interface()
|
||||||
|
}
|
||||||
143
internal/formats/common/property_encoder_test.go
Normal file
143
internal/formats/common/property_encoder_test.go
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
type T1 struct {
|
||||||
|
Name string
|
||||||
|
Val int
|
||||||
|
ValU uint
|
||||||
|
Flag bool `json:"bool_flag"`
|
||||||
|
Float float64
|
||||||
|
T2 T2
|
||||||
|
T2Ptr *T2
|
||||||
|
T2Arr []T2
|
||||||
|
T2PtrArr []*T2
|
||||||
|
StrArr []string
|
||||||
|
IntArr []int
|
||||||
|
FloatArr []float64
|
||||||
|
BoolArr []bool
|
||||||
|
T3Arr []T3
|
||||||
|
}
|
||||||
|
|
||||||
|
type T2 struct {
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
type T3 struct {
|
||||||
|
T4Arr []T4
|
||||||
|
}
|
||||||
|
|
||||||
|
type T4 struct {
|
||||||
|
Typ string
|
||||||
|
IntPtr *int
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_EncodeDecodeCycle(t *testing.T) {
|
||||||
|
val := 99
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
value interface{}
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "all values",
|
||||||
|
value: T1{
|
||||||
|
Name: "name",
|
||||||
|
Val: 10,
|
||||||
|
ValU: 16,
|
||||||
|
Flag: true,
|
||||||
|
Float: 1.2,
|
||||||
|
T2: T2{
|
||||||
|
Name: "embedded t2",
|
||||||
|
},
|
||||||
|
T2Ptr: &T2{
|
||||||
|
"t2 ptr",
|
||||||
|
},
|
||||||
|
T2Arr: []T2{
|
||||||
|
{"t2 elem 0"},
|
||||||
|
{"t2 elem 1"},
|
||||||
|
},
|
||||||
|
T2PtrArr: []*T2{
|
||||||
|
{"t2 ptr v1"},
|
||||||
|
{"t2 ptr v2"},
|
||||||
|
},
|
||||||
|
StrArr: []string{"s 1", "s 2", "s 3"},
|
||||||
|
IntArr: []int{9, 12, -1},
|
||||||
|
FloatArr: []float64{-23.99, 15.234321, 39912342314},
|
||||||
|
BoolArr: []bool{false, true, true, true, false},
|
||||||
|
T3Arr: []T3{
|
||||||
|
{
|
||||||
|
T4Arr: []T4{
|
||||||
|
{
|
||||||
|
Typ: "t4 nested typ 1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Typ: "t4 nested typ 2",
|
||||||
|
IntPtr: &val,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nil values",
|
||||||
|
value: T1{
|
||||||
|
Name: "t1 test",
|
||||||
|
Val: 0,
|
||||||
|
ValU: 0,
|
||||||
|
Flag: false,
|
||||||
|
Float: 0,
|
||||||
|
T2: T2{},
|
||||||
|
T2Ptr: nil,
|
||||||
|
T2Arr: nil,
|
||||||
|
T2PtrArr: nil,
|
||||||
|
StrArr: nil,
|
||||||
|
IntArr: nil,
|
||||||
|
FloatArr: nil,
|
||||||
|
BoolArr: nil,
|
||||||
|
T3Arr: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "array values",
|
||||||
|
value: []T2{
|
||||||
|
{"t2 elem 0"},
|
||||||
|
{"t2 elem 1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "array ptr",
|
||||||
|
value: &[]T2{
|
||||||
|
{"t2 elem 0"},
|
||||||
|
{"t2 elem 1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
vals := Encode(test.value, "props", OptionalJSONTag)
|
||||||
|
|
||||||
|
typ := reflect.TypeOf(test.value)
|
||||||
|
|
||||||
|
if typ.Kind() != reflect.Slice && typ.Kind() != reflect.Ptr {
|
||||||
|
assert.NotEmpty(t, vals["props:bool_flag"])
|
||||||
|
|
||||||
|
t2 := T1{}
|
||||||
|
DecodeInto(&t2, vals, "props", OptionalJSONTag)
|
||||||
|
|
||||||
|
assert.EqualValues(t, test.value, t2)
|
||||||
|
}
|
||||||
|
|
||||||
|
t3 := Decode(typ, vals, "props", OptionalJSONTag)
|
||||||
|
|
||||||
|
assert.EqualValues(t, test.value, t3)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"bomFormat": "CycloneDX",
|
"bomFormat": "CycloneDX",
|
||||||
"specVersion": "1.3",
|
"specVersion": "1.3",
|
||||||
"serialNumber": "urn:uuid:326afa86-5620-4a80-8f2b-7f283b954b9b",
|
"serialNumber": "urn:uuid:195a66a2-6d39-472e-b62b-0cafb9bfedd4",
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"timestamp": "2022-02-10T17:19:38-05:00",
|
"timestamp": "2022-02-25T12:54:25-05:00",
|
||||||
"tools": [
|
"tools": [
|
||||||
{
|
{
|
||||||
"vendor": "anchore",
|
"vendor": "anchore",
|
||||||
@ -35,23 +35,23 @@
|
|||||||
"purl": "a-purl-2",
|
"purl": "a-purl-2",
|
||||||
"properties": [
|
"properties": [
|
||||||
{
|
{
|
||||||
"name": "foundBy",
|
"name": "syft:package:foundBy",
|
||||||
"value": "the-cataloger-1"
|
"value": "the-cataloger-1"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "language",
|
"name": "syft:package:language",
|
||||||
"value": "python"
|
"value": "python"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "type",
|
"name": "syft:package:metadataType",
|
||||||
"value": "python"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "metadataType",
|
|
||||||
"value": "PythonPackageMetadata"
|
"value": "PythonPackageMetadata"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "path",
|
"name": "syft:package:type",
|
||||||
|
"value": "python"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "syft:location:0:path",
|
||||||
"value": "/some/path/pkg1"
|
"value": "/some/path/pkg1"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -64,20 +64,24 @@
|
|||||||
"purl": "a-purl-2",
|
"purl": "a-purl-2",
|
||||||
"properties": [
|
"properties": [
|
||||||
{
|
{
|
||||||
"name": "foundBy",
|
"name": "syft:package:foundBy",
|
||||||
"value": "the-cataloger-2"
|
"value": "the-cataloger-2"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "type",
|
"name": "syft:package:metadataType",
|
||||||
"value": "deb"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "metadataType",
|
|
||||||
"value": "DpkgMetadata"
|
"value": "DpkgMetadata"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "path",
|
"name": "syft:package:type",
|
||||||
|
"value": "deb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "syft:location:0:path",
|
||||||
"value": "/some/path/pkg1"
|
"value": "/some/path/pkg1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "syft:metadata:installedSize",
|
||||||
|
"value": "0"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -93,15 +97,19 @@
|
|||||||
},
|
},
|
||||||
"properties": [
|
"properties": [
|
||||||
{
|
{
|
||||||
"name": "prettyName",
|
"name": "syft:distro:id",
|
||||||
"value": "debian"
|
"value": "debian"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "id",
|
"name": "syft:distro:idLike:0",
|
||||||
|
"value": "like!"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "syft:distro:prettyName",
|
||||||
"value": "debian"
|
"value": "debian"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "versionID",
|
"name": "syft:distro:versionID",
|
||||||
"value": "1.2.3"
|
"value": "1.2.3"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"bomFormat": "CycloneDX",
|
"bomFormat": "CycloneDX",
|
||||||
"specVersion": "1.3",
|
"specVersion": "1.3",
|
||||||
"serialNumber": "urn:uuid:761a2036-0f25-4787-bf28-f5e9a7d9a0bf",
|
"serialNumber": "urn:uuid:78116a1b-b709-4734-8411-d0e339308edd",
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"timestamp": "2022-02-10T17:19:38-05:00",
|
"timestamp": "2022-02-25T12:54:25-05:00",
|
||||||
"tools": [
|
"tools": [
|
||||||
{
|
{
|
||||||
"vendor": "anchore",
|
"vendor": "anchore",
|
||||||
@ -35,28 +35,28 @@
|
|||||||
"purl": "a-purl-1",
|
"purl": "a-purl-1",
|
||||||
"properties": [
|
"properties": [
|
||||||
{
|
{
|
||||||
"name": "foundBy",
|
"name": "syft:package:foundBy",
|
||||||
"value": "the-cataloger-1"
|
"value": "the-cataloger-1"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "language",
|
"name": "syft:package:language",
|
||||||
"value": "python"
|
"value": "python"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "type",
|
"name": "syft:package:metadataType",
|
||||||
"value": "python"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "metadataType",
|
|
||||||
"value": "PythonPackageMetadata"
|
"value": "PythonPackageMetadata"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "path",
|
"name": "syft:package:type",
|
||||||
"value": "/somefile-1.txt"
|
"value": "python"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "layerID",
|
"name": "syft:location:0:layerID",
|
||||||
"value": "sha256:41e7295da66c405eb3a4df29188dcf80f622f9304d487033a86d4a22e3f01abe"
|
"value": "sha256:41e7295da66c405eb3a4df29188dcf80f622f9304d487033a86d4a22e3f01abe"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "syft:location:0:path",
|
||||||
|
"value": "/somefile-1.txt"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -68,24 +68,28 @@
|
|||||||
"purl": "a-purl-2",
|
"purl": "a-purl-2",
|
||||||
"properties": [
|
"properties": [
|
||||||
{
|
{
|
||||||
"name": "foundBy",
|
"name": "syft:package:foundBy",
|
||||||
"value": "the-cataloger-2"
|
"value": "the-cataloger-2"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "type",
|
"name": "syft:package:metadataType",
|
||||||
"value": "deb"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "metadataType",
|
|
||||||
"value": "DpkgMetadata"
|
"value": "DpkgMetadata"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "path",
|
"name": "syft:package:type",
|
||||||
|
"value": "deb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "syft:location:0:layerID",
|
||||||
|
"value": "sha256:68a2c166dcb3acf6b7303e995ca1fe7d794bd3b5852a0b4048f9c96b796086aa"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "syft:location:0:path",
|
||||||
"value": "/somefile-2.txt"
|
"value": "/somefile-2.txt"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "layerID",
|
"name": "syft:metadata:installedSize",
|
||||||
"value": "sha256:68a2c166dcb3acf6b7303e995ca1fe7d794bd3b5852a0b4048f9c96b796086aa"
|
"value": "0"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -101,15 +105,19 @@
|
|||||||
},
|
},
|
||||||
"properties": [
|
"properties": [
|
||||||
{
|
{
|
||||||
"name": "prettyName",
|
"name": "syft:distro:id",
|
||||||
"value": "debian"
|
"value": "debian"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "id",
|
"name": "syft:distro:idLike:0",
|
||||||
|
"value": "like!"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "syft:distro:prettyName",
|
||||||
"value": "debian"
|
"value": "debian"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "versionID",
|
"name": "syft:distro:versionID",
|
||||||
"value": "1.2.3"
|
"value": "1.2.3"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<bom xmlns="http://cyclonedx.org/schema/bom/1.3" serialNumber="urn:uuid:25133adc-01af-46ad-a198-002034e54097" version="1">
|
<bom xmlns="http://cyclonedx.org/schema/bom/1.3" serialNumber="urn:uuid:dd1d1863-04be-414c-9b2a-bdc0e0f25e9f" version="1">
|
||||||
<metadata>
|
<metadata>
|
||||||
<timestamp>2022-02-10T17:18:31-05:00</timestamp>
|
<timestamp>2022-02-25T12:54:44-05:00</timestamp>
|
||||||
<tools>
|
<tools>
|
||||||
<tool>
|
<tool>
|
||||||
<vendor>anchore</vendor>
|
<vendor>anchore</vendor>
|
||||||
@ -26,11 +26,11 @@
|
|||||||
<cpe>cpe:2.3:*:some:package:2:*:*:*:*:*:*:*</cpe>
|
<cpe>cpe:2.3:*:some:package:2:*:*:*:*:*:*:*</cpe>
|
||||||
<purl>a-purl-2</purl>
|
<purl>a-purl-2</purl>
|
||||||
<properties>
|
<properties>
|
||||||
<property name="foundBy">the-cataloger-1</property>
|
<property name="syft:package:foundBy">the-cataloger-1</property>
|
||||||
<property name="language">python</property>
|
<property name="syft:package:language">python</property>
|
||||||
<property name="type">python</property>
|
<property name="syft:package:metadataType">PythonPackageMetadata</property>
|
||||||
<property name="metadataType">PythonPackageMetadata</property>
|
<property name="syft:package:type">python</property>
|
||||||
<property name="path">/some/path/pkg1</property>
|
<property name="syft:location:0:path">/some/path/pkg1</property>
|
||||||
</properties>
|
</properties>
|
||||||
</component>
|
</component>
|
||||||
<component type="library">
|
<component type="library">
|
||||||
@ -39,10 +39,11 @@
|
|||||||
<cpe>cpe:2.3:*:some:package:2:*:*:*:*:*:*:*</cpe>
|
<cpe>cpe:2.3:*:some:package:2:*:*:*:*:*:*:*</cpe>
|
||||||
<purl>a-purl-2</purl>
|
<purl>a-purl-2</purl>
|
||||||
<properties>
|
<properties>
|
||||||
<property name="foundBy">the-cataloger-2</property>
|
<property name="syft:package:foundBy">the-cataloger-2</property>
|
||||||
<property name="type">deb</property>
|
<property name="syft:package:metadataType">DpkgMetadata</property>
|
||||||
<property name="metadataType">DpkgMetadata</property>
|
<property name="syft:package:type">deb</property>
|
||||||
<property name="path">/some/path/pkg1</property>
|
<property name="syft:location:0:path">/some/path/pkg1</property>
|
||||||
|
<property name="syft:metadata:installedSize">0</property>
|
||||||
</properties>
|
</properties>
|
||||||
</component>
|
</component>
|
||||||
<component type="operating-system">
|
<component type="operating-system">
|
||||||
@ -51,9 +52,10 @@
|
|||||||
<description>debian</description>
|
<description>debian</description>
|
||||||
<swid tagId="debian" name="debian" version="1.2.3"></swid>
|
<swid tagId="debian" name="debian" version="1.2.3"></swid>
|
||||||
<properties>
|
<properties>
|
||||||
<property name="prettyName">debian</property>
|
<property name="syft:distro:id">debian</property>
|
||||||
<property name="id">debian</property>
|
<property name="syft:distro:idLike:0">like!</property>
|
||||||
<property name="versionID">1.2.3</property>
|
<property name="syft:distro:prettyName">debian</property>
|
||||||
|
<property name="syft:distro:versionID">1.2.3</property>
|
||||||
</properties>
|
</properties>
|
||||||
</component>
|
</component>
|
||||||
</components>
|
</components>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<bom xmlns="http://cyclonedx.org/schema/bom/1.3" serialNumber="urn:uuid:e702d78f-9372-4458-ac38-be8e80fc3005" version="1">
|
<bom xmlns="http://cyclonedx.org/schema/bom/1.3" serialNumber="urn:uuid:153353a9-d9f4-40f6-be23-3d56487930c1" version="1">
|
||||||
<metadata>
|
<metadata>
|
||||||
<timestamp>2022-02-10T17:18:31-05:00</timestamp>
|
<timestamp>2022-02-25T12:54:44-05:00</timestamp>
|
||||||
<tools>
|
<tools>
|
||||||
<tool>
|
<tool>
|
||||||
<vendor>anchore</vendor>
|
<vendor>anchore</vendor>
|
||||||
@ -26,12 +26,12 @@
|
|||||||
<cpe>cpe:2.3:*:some:package:1:*:*:*:*:*:*:*</cpe>
|
<cpe>cpe:2.3:*:some:package:1:*:*:*:*:*:*:*</cpe>
|
||||||
<purl>a-purl-1</purl>
|
<purl>a-purl-1</purl>
|
||||||
<properties>
|
<properties>
|
||||||
<property name="foundBy">the-cataloger-1</property>
|
<property name="syft:package:foundBy">the-cataloger-1</property>
|
||||||
<property name="language">python</property>
|
<property name="syft:package:language">python</property>
|
||||||
<property name="type">python</property>
|
<property name="syft:package:metadataType">PythonPackageMetadata</property>
|
||||||
<property name="metadataType">PythonPackageMetadata</property>
|
<property name="syft:package:type">python</property>
|
||||||
<property name="path">/somefile-1.txt</property>
|
<property name="syft:location:0:layerID">sha256:41e7295da66c405eb3a4df29188dcf80f622f9304d487033a86d4a22e3f01abe</property>
|
||||||
<property name="layerID">sha256:41e7295da66c405eb3a4df29188dcf80f622f9304d487033a86d4a22e3f01abe</property>
|
<property name="syft:location:0:path">/somefile-1.txt</property>
|
||||||
</properties>
|
</properties>
|
||||||
</component>
|
</component>
|
||||||
<component type="library">
|
<component type="library">
|
||||||
@ -40,11 +40,12 @@
|
|||||||
<cpe>cpe:2.3:*:some:package:2:*:*:*:*:*:*:*</cpe>
|
<cpe>cpe:2.3:*:some:package:2:*:*:*:*:*:*:*</cpe>
|
||||||
<purl>a-purl-2</purl>
|
<purl>a-purl-2</purl>
|
||||||
<properties>
|
<properties>
|
||||||
<property name="foundBy">the-cataloger-2</property>
|
<property name="syft:package:foundBy">the-cataloger-2</property>
|
||||||
<property name="type">deb</property>
|
<property name="syft:package:metadataType">DpkgMetadata</property>
|
||||||
<property name="metadataType">DpkgMetadata</property>
|
<property name="syft:package:type">deb</property>
|
||||||
<property name="path">/somefile-2.txt</property>
|
<property name="syft:location:0:layerID">sha256:68a2c166dcb3acf6b7303e995ca1fe7d794bd3b5852a0b4048f9c96b796086aa</property>
|
||||||
<property name="layerID">sha256:68a2c166dcb3acf6b7303e995ca1fe7d794bd3b5852a0b4048f9c96b796086aa</property>
|
<property name="syft:location:0:path">/somefile-2.txt</property>
|
||||||
|
<property name="syft:metadata:installedSize">0</property>
|
||||||
</properties>
|
</properties>
|
||||||
</component>
|
</component>
|
||||||
<component type="operating-system">
|
<component type="operating-system">
|
||||||
@ -53,9 +54,10 @@
|
|||||||
<description>debian</description>
|
<description>debian</description>
|
||||||
<swid tagId="debian" name="debian" version="1.2.3"></swid>
|
<swid tagId="debian" name="debian" version="1.2.3"></swid>
|
||||||
<properties>
|
<properties>
|
||||||
<property name="prettyName">debian</property>
|
<property name="syft:distro:id">debian</property>
|
||||||
<property name="id">debian</property>
|
<property name="syft:distro:idLike:0">like!</property>
|
||||||
<property name="versionID">1.2.3</property>
|
<property name="syft:distro:prettyName">debian</property>
|
||||||
|
<property name="syft:distro:versionID">1.2.3</property>
|
||||||
</properties>
|
</properties>
|
||||||
</component>
|
</component>
|
||||||
</components>
|
</components>
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import (
|
|||||||
// Location represents a path relative to a particular filesystem resolved to a specific file.Reference. This struct is used as a key
|
// Location represents a path relative to a particular filesystem resolved to a specific file.Reference. This struct is used as a key
|
||||||
// in content fetching to uniquely identify a file relative to a request (the VirtualPath).
|
// in content fetching to uniquely identify a file relative to a request (the VirtualPath).
|
||||||
type Location struct {
|
type Location struct {
|
||||||
Coordinates
|
Coordinates `cyclonedx:""` // Empty string here means there is no intermediate property name, e.g. syft:locations:0:path without "coordinates"
|
||||||
// note: it is IMPORTANT to ignore anything but the coordinates for a Location when considering the ID (hash value)
|
// note: it is IMPORTANT to ignore anything but the coordinates for a Location when considering the ID (hash value)
|
||||||
// since the coordinates are the minimally correct ID for a location (symlinks should not come into play)
|
// since the coordinates are the minimally correct ID for a location (symlinks should not come into play)
|
||||||
VirtualPath string `hash:"ignore"` // The path to the file which may or may not have hardlinks / symlinks
|
VirtualPath string `hash:"ignore"` // The path to the file which may or may not have hardlinks / symlinks
|
||||||
|
|||||||
@ -23,21 +23,24 @@ func TestEncodeDecodeEncodeCycleComparison(t *testing.T) {
|
|||||||
tests := []struct {
|
tests := []struct {
|
||||||
format format.Option
|
format format.Option
|
||||||
redactor func(in []byte) []byte
|
redactor func(in []byte) []byte
|
||||||
|
json bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
format: format.JSONOption,
|
format: format.JSONOption,
|
||||||
|
json: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
format: format.CycloneDxJSONOption,
|
format: format.CycloneDxJSONOption,
|
||||||
redactor: func(in []byte) []byte {
|
redactor: func(in []byte) []byte {
|
||||||
in = regexp.MustCompile("\"(timestamp|serialNumber|bom-ref)\": \"[^\"]+").ReplaceAll(in, []byte{})
|
in = regexp.MustCompile("\"(timestamp|serialNumber|bom-ref)\": \"[^\"]+\",").ReplaceAll(in, []byte{})
|
||||||
return in
|
return in
|
||||||
},
|
},
|
||||||
|
json: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
format: format.CycloneDxXMLOption,
|
format: format.CycloneDxXMLOption,
|
||||||
redactor: func(in []byte) []byte {
|
redactor: func(in []byte) []byte {
|
||||||
in = regexp.MustCompile("(serialNumber|bom-ref)=\"[^\"]+").ReplaceAll(in, []byte{})
|
in = regexp.MustCompile("(serialNumber|bom-ref)=\"[^\"]+\"").ReplaceAll(in, []byte{})
|
||||||
in = regexp.MustCompile("<timestamp>[^<]+</timestamp>").ReplaceAll(in, []byte{})
|
in = regexp.MustCompile("<timestamp>[^<]+</timestamp>").ReplaceAll(in, []byte{})
|
||||||
return in
|
return in
|
||||||
},
|
},
|
||||||
@ -63,10 +66,16 @@ func TestEncodeDecodeEncodeCycleComparison(t *testing.T) {
|
|||||||
by2 = test.redactor(by2)
|
by2 = test.redactor(by2)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !assert.True(t, bytes.Equal(by1, by2)) {
|
if test.json {
|
||||||
dmp := diffmatchpatch.New()
|
s1 := string(by1)
|
||||||
diffs := dmp.DiffMain(string(by1), string(by2), true)
|
s2 := string(by2)
|
||||||
t.Errorf("diff: %s", dmp.DiffPrettyText(diffs))
|
assert.JSONEq(t, s1, s2)
|
||||||
|
} else {
|
||||||
|
if !assert.True(t, bytes.Equal(by1, by2)) {
|
||||||
|
dmp := diffmatchpatch.New()
|
||||||
|
diffs := dmp.DiffMain(string(by1), string(by2), true)
|
||||||
|
t.Errorf("diff: %s", dmp.DiffPrettyText(diffs))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user