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
|
||||
|
||||
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,131 +46,56 @@ 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),
|
||||
Licenses: decodeLicenses(c),
|
||||
Language: pkg.Language(findPropertyValue(c, "language")),
|
||||
Type: typ,
|
||||
CPEs: decodeCPEs(c),
|
||||
PURL: purl,
|
||||
MetadataType: metaType,
|
||||
Metadata: meta,
|
||||
Name: c.Name,
|
||||
Version: c.Version,
|
||||
Locations: decodeLocations(values),
|
||||
Licenses: decodeLicenses(c),
|
||||
CPEs: decodeCPEs(c),
|
||||
PURL: c.PackageURL,
|
||||
}
|
||||
|
||||
common.DecodeInto(p, values, "syft:package", CycloneDXFields)
|
||||
|
||||
p.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
|
||||
}
|
||||
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()
|
||||
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
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
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,
|
||||
})
|
||||
}
|
||||
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),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
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",
|
||||
"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"
|
||||
}
|
||||
]
|
||||
|
||||
@ -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"
|
||||
}
|
||||
]
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,10 +66,16 @@ func TestEncodeDecodeEncodeCycleComparison(t *testing.T) {
|
||||
by2 = test.redactor(by2)
|
||||
}
|
||||
|
||||
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))
|
||||
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))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user