Update CycloneDX to use syft namespace and output multiple CPEs (#849)

This commit is contained in:
Keith Zantow 2022-03-01 17:37:52 -05:00 committed by GitHub
parent d2f28e0eb1
commit edac8c7bf7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 715 additions and 317 deletions

View File

@ -1,18 +1,30 @@
package cyclonedxhelpers
import (
"fmt"
"reflect"
"strconv"
"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/source"
)
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{
Type: cyclonedx.ComponentTypeLibrary,
Name: p.Name,
@ -20,12 +32,12 @@ func encodeComponent(p pkg.Package) cyclonedx.Component {
Version: p.Version,
PackageURL: p.PURL,
Licenses: encodeLicenses(p),
CPE: encodeCPE(p),
CPE: encodeSingleCPE(p),
Author: encodeAuthor(p),
Publisher: encodePublisher(p),
Description: encodeDescription(p),
ExternalReferences: encodeExternalReferences(p),
Properties: encodeProperties(p),
Properties: properties,
}
}
@ -34,119 +46,45 @@ func hasMetadata(p pkg.Package) bool {
}
func decodeComponent(c *cyclonedx.Component) *pkg.Package {
typ := pkg.Type(findPropertyValue(c, "type"))
purl := c.PackageURL
if typ == "" && purl != "" {
typ = pkg.TypeFromPURL(purl)
values := map[string]string{}
for _, p := range *c.Properties {
values[p.Name] = p.Value
}
metaType, meta := decodePackageMetadata(c)
p := &pkg.Package{
Name: c.Name,
Version: c.Version,
FoundBy: findPropertyValue(c, "foundBy"),
Locations: decodeLocations(c),
Locations: decodeLocations(values),
Licenses: decodeLicenses(c),
Language: pkg.Language(findPropertyValue(c, "language")),
Type: typ,
CPEs: decodeCPEs(c),
PURL: purl,
MetadataType: metaType,
Metadata: meta,
PURL: c.PackageURL,
}
common.DecodeInto(p, values, "syft:package", CycloneDXFields)
p.Metadata = decodePackageMetadata(values, c, p.MetadataType)
if p.Type == "" {
p.Type = pkg.TypeFromPURL(p.PURL)
}
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 decodeLocations(vals map[string]string) []source.Location {
v := common.Decode(reflect.TypeOf([]source.Location{}), vals, "syft:location", CycloneDXFields)
out, _ := v.([]source.Location)
return out
}
func mapAllProps(c *cyclonedx.Component, obj reflect.Value) {
value := obj
if value.Kind() == reflect.Ptr {
value = value.Elem()
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
}
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())
metaPtrTyp := reflect.PtrTo(metaTyp)
metaPtr := common.Decode(metaPtrTyp, vals, "syft:metadata", CycloneDXFields)
// Map all explicit metadata properties
decodeAuthor(c.Author, metaPtr)
@ -155,10 +93,9 @@ func decodePackageMetadata(c *cyclonedx.Component) (pkg.MetadataType, interface{
decodeDescription(c.Description, metaPtr)
decodeExternalReferences(c, metaPtr)
// return the actual interface{} | struct ( not interface{} | *struct )
return typ, meta.Elem().Interface()
}
// return the actual interface{} -> struct ... not interface{} -> *struct
return common.PtrToStruct(metaPtr)
}
return pkg.UnknownMetadataType, nil
return nil
}

View File

@ -4,12 +4,13 @@ import (
"testing"
"github.com/CycloneDX/cyclonedx-go"
"github.com/stretchr/testify/assert"
"github.com/anchore/syft/syft/pkg"
"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
tests := []struct {
name string
@ -46,13 +47,14 @@ func Test_encodeProperties(t *testing.T) {
},
},
expected: &[]cyclonedx.Property{
{Name: "foundBy", Value: "cataloger"},
{Name: "path", Value: "test"},
{Name: "originPackage", Value: "libc-dev"},
{Name: "installedSize", Value: "4096"},
{Name: "pullDependencies", Value: "musl-utils"},
{Name: "pullChecksum", Value: "Q1p78yvTLG094tHE1+dToJGbmYzQE="},
{Name: "gitCommitOfApkPort", Value: "97b1c2842faa3bfa30f5811ffbf16d5ff9f1a479"},
{Name: "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"},
},
},
{
@ -71,10 +73,10 @@ func Test_encodeProperties(t *testing.T) {
},
},
expected: &[]cyclonedx.Property{
{Name: "metadataType", Value: "DpkgMetadata"},
{Name: "source", Value: "tzdata-dev"},
{Name: "sourceVersion", Value: "1.0"},
{Name: "installedSize", Value: "3036"},
{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"},
},
},
{
@ -92,12 +94,12 @@ func Test_encodeProperties(t *testing.T) {
},
},
expected: &[]cyclonedx.Property{
{Name: "language", Value: pkg.Go.String()},
{Name: "type", Value: "go-module"},
{Name: "metadataType", Value: "GolangBinMetadata"},
{Name: "goCompiledVersion", Value: "1.17"},
{Name: "architecture", Value: "amd64"},
{Name: "h1Digest", Value: "h1:KlOXYy8wQWTUJYFgkUI40Lzr06ofg5IRXUK5C7qZt1k="},
{Name: "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="},
},
},
{
@ -121,18 +123,19 @@ func Test_encodeProperties(t *testing.T) {
},
},
expected: &[]cyclonedx.Property{
{Name: "type", Value: "rpm"},
{Name: "metadataType", Value: "RpmdbMetadata"},
{Name: "epoch", Value: "2"},
{Name: "release", Value: "1"},
{Name: "sourceRpm", Value: "dive-0.9.2-1.src.rpm"},
{Name: "size", Value: "12406784"},
{Name: "syft:package:metadataType", Value: "RpmdbMetadata"},
{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) {
assert.Equal(t, test.expected, encodeProperties(test.input))
c := encodeComponent(test.input)
assert.Equal(t, test.expected, c.Properties)
})
}
}

View File

@ -2,11 +2,12 @@ package cyclonedxhelpers
import (
"github.com/CycloneDX/cyclonedx-go"
"github.com/anchore/syft/internal/log"
"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
// we can extract the first CPE as the one to output in cyclonedx
if len(p.CPEs) > 0 {
@ -15,16 +16,42 @@ func encodeCPE(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
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 {
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
}

View File

@ -3,8 +3,9 @@ package cyclonedxhelpers
import (
"testing"
"github.com/anchore/syft/syft/pkg"
"github.com/stretchr/testify/assert"
"github.com/anchore/syft/syft/pkg"
)
func Test_encodeCPE(t *testing.T) {
@ -51,7 +52,7 @@ func Test_encodeCPE(t *testing.T) {
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, encodeCPE(test.input))
assert.Equal(t, test.expected, encodeSingleCPE(test.input))
})
}
}

View File

@ -4,6 +4,8 @@ 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/internal/version"
@ -11,7 +13,6 @@ import (
"github.com/anchore/syft/syft/linux"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
"github.com/google/uuid"
)
func ToFormatModel(s sbom.SBOM) *cyclonedx.BOM {
@ -74,9 +75,10 @@ func toOSComponent(distro *linux.Release) []cyclonedx.Component {
if len(*eRefs) == 0 {
eRefs = nil
}
props := getCycloneDXProperties(*distro)
if len(*props) == 0 {
props = nil
props := encodeProperties(distro, "syft:distro")
var properties *[]cyclonedx.Property
if len(props) > 0 {
properties = &props
}
return []cyclonedx.Component{
{
@ -93,7 +95,7 @@ func toOSComponent(distro *linux.Release) []cyclonedx.Component {
// TODO should we add a PURL?
CPE: distro.CPEName,
ExternalReferences: eRefs,
Properties: props,
Properties: properties,
},
}
}

View File

@ -1,87 +1,21 @@
package cyclonedxhelpers
import (
"fmt"
"reflect"
"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 {
props := []cyclonedx.Property{}
props = append(props, *getCycloneDXProperties(p)...)
if len(p.Locations) > 0 {
for _, l := range p.Locations {
props = append(props, *getCycloneDXProperties(l.Coordinates)...)
}
}
if hasMetadata(p) {
props = append(props, *getCycloneDXProperties(p.Metadata)...)
}
if len(props) > 0 {
return &props
}
return nil
}
var (
CycloneDXFields = common.RequiredTag("cyclonedx")
)
func getCycloneDXProperties(m interface{}) *[]cyclonedx.Property {
props := []cyclonedx.Property{}
structValue := reflect.ValueOf(m)
if structValue.Kind() != reflect.Struct {
return &props
}
structType := structValue.Type()
for i := 0; i < structValue.NumField(); i++ {
if name, value := getCycloneDXPropertyName(structType.Field(i)), getCycloneDXPropertyValue(structValue.Field(i)); name != "" && value != "" {
// 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),
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 &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 ""
return
}

View 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()
}

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

View File

@ -1,10 +1,10 @@
{
"bomFormat": "CycloneDX",
"specVersion": "1.3",
"serialNumber": "urn:uuid:326afa86-5620-4a80-8f2b-7f283b954b9b",
"serialNumber": "urn:uuid:195a66a2-6d39-472e-b62b-0cafb9bfedd4",
"version": 1,
"metadata": {
"timestamp": "2022-02-10T17:19:38-05:00",
"timestamp": "2022-02-25T12:54:25-05:00",
"tools": [
{
"vendor": "anchore",
@ -35,23 +35,23 @@
"purl": "a-purl-2",
"properties": [
{
"name": "foundBy",
"name": "syft:package:foundBy",
"value": "the-cataloger-1"
},
{
"name": "language",
"name": "syft:package:language",
"value": "python"
},
{
"name": "type",
"value": "python"
},
{
"name": "metadataType",
"name": "syft:package:metadataType",
"value": "PythonPackageMetadata"
},
{
"name": "path",
"name": "syft:package:type",
"value": "python"
},
{
"name": "syft:location:0:path",
"value": "/some/path/pkg1"
}
]
@ -64,20 +64,24 @@
"purl": "a-purl-2",
"properties": [
{
"name": "foundBy",
"name": "syft:package:foundBy",
"value": "the-cataloger-2"
},
{
"name": "type",
"value": "deb"
},
{
"name": "metadataType",
"name": "syft:package:metadataType",
"value": "DpkgMetadata"
},
{
"name": "path",
"name": "syft:package:type",
"value": "deb"
},
{
"name": "syft:location:0:path",
"value": "/some/path/pkg1"
},
{
"name": "syft:metadata:installedSize",
"value": "0"
}
]
},
@ -93,15 +97,19 @@
},
"properties": [
{
"name": "prettyName",
"name": "syft:distro:id",
"value": "debian"
},
{
"name": "id",
"name": "syft:distro:idLike:0",
"value": "like!"
},
{
"name": "syft:distro:prettyName",
"value": "debian"
},
{
"name": "versionID",
"name": "syft:distro:versionID",
"value": "1.2.3"
}
]

View File

@ -1,10 +1,10 @@
{
"bomFormat": "CycloneDX",
"specVersion": "1.3",
"serialNumber": "urn:uuid:761a2036-0f25-4787-bf28-f5e9a7d9a0bf",
"serialNumber": "urn:uuid:78116a1b-b709-4734-8411-d0e339308edd",
"version": 1,
"metadata": {
"timestamp": "2022-02-10T17:19:38-05:00",
"timestamp": "2022-02-25T12:54:25-05:00",
"tools": [
{
"vendor": "anchore",
@ -35,28 +35,28 @@
"purl": "a-purl-1",
"properties": [
{
"name": "foundBy",
"name": "syft:package:foundBy",
"value": "the-cataloger-1"
},
{
"name": "language",
"name": "syft:package:language",
"value": "python"
},
{
"name": "type",
"value": "python"
},
{
"name": "metadataType",
"name": "syft:package:metadataType",
"value": "PythonPackageMetadata"
},
{
"name": "path",
"value": "/somefile-1.txt"
"name": "syft:package:type",
"value": "python"
},
{
"name": "layerID",
"name": "syft:location:0:layerID",
"value": "sha256:41e7295da66c405eb3a4df29188dcf80f622f9304d487033a86d4a22e3f01abe"
},
{
"name": "syft:location:0:path",
"value": "/somefile-1.txt"
}
]
},
@ -68,24 +68,28 @@
"purl": "a-purl-2",
"properties": [
{
"name": "foundBy",
"name": "syft:package:foundBy",
"value": "the-cataloger-2"
},
{
"name": "type",
"value": "deb"
},
{
"name": "metadataType",
"name": "syft:package:metadataType",
"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"
},
{
"name": "layerID",
"value": "sha256:68a2c166dcb3acf6b7303e995ca1fe7d794bd3b5852a0b4048f9c96b796086aa"
"name": "syft:metadata:installedSize",
"value": "0"
}
]
},
@ -101,15 +105,19 @@
},
"properties": [
{
"name": "prettyName",
"name": "syft:distro:id",
"value": "debian"
},
{
"name": "id",
"name": "syft:distro:idLike:0",
"value": "like!"
},
{
"name": "syft:distro:prettyName",
"value": "debian"
},
{
"name": "versionID",
"name": "syft:distro:versionID",
"value": "1.2.3"
}
]

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: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>
<timestamp>2022-02-10T17:18:31-05:00</timestamp>
<timestamp>2022-02-25T12:54:44-05:00</timestamp>
<tools>
<tool>
<vendor>anchore</vendor>
@ -26,11 +26,11 @@
<cpe>cpe:2.3:*:some:package:2:*:*:*:*:*:*:*</cpe>
<purl>a-purl-2</purl>
<properties>
<property name="foundBy">the-cataloger-1</property>
<property name="language">python</property>
<property name="type">python</property>
<property name="metadataType">PythonPackageMetadata</property>
<property name="path">/some/path/pkg1</property>
<property name="syft:package:foundBy">the-cataloger-1</property>
<property name="syft:package:language">python</property>
<property name="syft:package:metadataType">PythonPackageMetadata</property>
<property name="syft:package:type">python</property>
<property name="syft:location:0:path">/some/path/pkg1</property>
</properties>
</component>
<component type="library">
@ -39,10 +39,11 @@
<cpe>cpe:2.3:*:some:package:2:*:*:*:*:*:*:*</cpe>
<purl>a-purl-2</purl>
<properties>
<property name="foundBy">the-cataloger-2</property>
<property name="type">deb</property>
<property name="metadataType">DpkgMetadata</property>
<property name="path">/some/path/pkg1</property>
<property name="syft:package:foundBy">the-cataloger-2</property>
<property name="syft:package:metadataType">DpkgMetadata</property>
<property name="syft:package:type">deb</property>
<property name="syft:location:0:path">/some/path/pkg1</property>
<property name="syft:metadata:installedSize">0</property>
</properties>
</component>
<component type="operating-system">
@ -51,9 +52,10 @@
<description>debian</description>
<swid tagId="debian" name="debian" version="1.2.3"></swid>
<properties>
<property name="prettyName">debian</property>
<property name="id">debian</property>
<property name="versionID">1.2.3</property>
<property name="syft:distro:id">debian</property>
<property name="syft:distro:idLike:0">like!</property>
<property name="syft:distro:prettyName">debian</property>
<property name="syft:distro:versionID">1.2.3</property>
</properties>
</component>
</components>

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: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>
<timestamp>2022-02-10T17:18:31-05:00</timestamp>
<timestamp>2022-02-25T12:54:44-05:00</timestamp>
<tools>
<tool>
<vendor>anchore</vendor>
@ -26,12 +26,12 @@
<cpe>cpe:2.3:*:some:package:1:*:*:*:*:*:*:*</cpe>
<purl>a-purl-1</purl>
<properties>
<property name="foundBy">the-cataloger-1</property>
<property name="language">python</property>
<property name="type">python</property>
<property name="metadataType">PythonPackageMetadata</property>
<property name="path">/somefile-1.txt</property>
<property name="layerID">sha256:41e7295da66c405eb3a4df29188dcf80f622f9304d487033a86d4a22e3f01abe</property>
<property name="syft:package:foundBy">the-cataloger-1</property>
<property name="syft:package:language">python</property>
<property name="syft:package:metadataType">PythonPackageMetadata</property>
<property name="syft:package:type">python</property>
<property name="syft:location:0:layerID">sha256:41e7295da66c405eb3a4df29188dcf80f622f9304d487033a86d4a22e3f01abe</property>
<property name="syft:location:0:path">/somefile-1.txt</property>
</properties>
</component>
<component type="library">
@ -40,11 +40,12 @@
<cpe>cpe:2.3:*:some:package:2:*:*:*:*:*:*:*</cpe>
<purl>a-purl-2</purl>
<properties>
<property name="foundBy">the-cataloger-2</property>
<property name="type">deb</property>
<property name="metadataType">DpkgMetadata</property>
<property name="path">/somefile-2.txt</property>
<property name="layerID">sha256:68a2c166dcb3acf6b7303e995ca1fe7d794bd3b5852a0b4048f9c96b796086aa</property>
<property name="syft:package:foundBy">the-cataloger-2</property>
<property name="syft:package:metadataType">DpkgMetadata</property>
<property name="syft:package:type">deb</property>
<property name="syft:location:0:layerID">sha256:68a2c166dcb3acf6b7303e995ca1fe7d794bd3b5852a0b4048f9c96b796086aa</property>
<property name="syft:location:0:path">/somefile-2.txt</property>
<property name="syft:metadata:installedSize">0</property>
</properties>
</component>
<component type="operating-system">
@ -53,9 +54,10 @@
<description>debian</description>
<swid tagId="debian" name="debian" version="1.2.3"></swid>
<properties>
<property name="prettyName">debian</property>
<property name="id">debian</property>
<property name="versionID">1.2.3</property>
<property name="syft:distro:id">debian</property>
<property name="syft:distro:idLike:0">like!</property>
<property name="syft:distro:prettyName">debian</property>
<property name="syft:distro:versionID">1.2.3</property>
</properties>
</component>
</components>

View File

@ -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
// in content fetching to uniquely identify a file relative to a request (the VirtualPath).
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)
// 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

View File

@ -23,21 +23,24 @@ func TestEncodeDecodeEncodeCycleComparison(t *testing.T) {
tests := []struct {
format format.Option
redactor func(in []byte) []byte
json bool
}{
{
format: format.JSONOption,
json: true,
},
{
format: format.CycloneDxJSONOption,
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
},
json: true,
},
{
format: format.CycloneDxXMLOption,
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{})
return in
},
@ -63,11 +66,17 @@ func TestEncodeDecodeEncodeCycleComparison(t *testing.T) {
by2 = test.redactor(by2)
}
if test.json {
s1 := string(by1)
s2 := string(by2)
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))
}
}
})
}
}