diff --git a/internal/formats/common/cyclonedxhelpers/component.go b/internal/formats/common/cyclonedxhelpers/component.go index ef42ab600..27b26d236 100644 --- a/internal/formats/common/cyclonedxhelpers/component.go +++ b/internal/formats/common/cyclonedxhelpers/component.go @@ -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 } diff --git a/internal/formats/common/cyclonedxhelpers/properties_test.go b/internal/formats/common/cyclonedxhelpers/component_test.go similarity index 63% rename from internal/formats/common/cyclonedxhelpers/properties_test.go rename to internal/formats/common/cyclonedxhelpers/component_test.go index a6cf701c4..b267abe6d 100644 --- a/internal/formats/common/cyclonedxhelpers/properties_test.go +++ b/internal/formats/common/cyclonedxhelpers/component_test.go @@ -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) }) } } diff --git a/internal/formats/common/cyclonedxhelpers/cpe.go b/internal/formats/common/cyclonedxhelpers/cpe.go index 0c241d2a9..ecee0dc39 100644 --- a/internal/formats/common/cyclonedxhelpers/cpe.go +++ b/internal/formats/common/cyclonedxhelpers/cpe.go @@ -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 } diff --git a/internal/formats/common/cyclonedxhelpers/cpe_test.go b/internal/formats/common/cyclonedxhelpers/cpe_test.go index 4678b9a92..1eff79ac6 100644 --- a/internal/formats/common/cyclonedxhelpers/cpe_test.go +++ b/internal/formats/common/cyclonedxhelpers/cpe_test.go @@ -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)) }) } } diff --git a/internal/formats/common/cyclonedxhelpers/format.go b/internal/formats/common/cyclonedxhelpers/format.go index 997a5f85e..1df56aae2 100644 --- a/internal/formats/common/cyclonedxhelpers/format.go +++ b/internal/formats/common/cyclonedxhelpers/format.go @@ -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, }, } } diff --git a/internal/formats/common/cyclonedxhelpers/properties.go b/internal/formats/common/cyclonedxhelpers/properties.go index 090f0a37d..a43052b92 100644 --- a/internal/formats/common/cyclonedxhelpers/properties.go +++ b/internal/formats/common/cyclonedxhelpers/properties.go @@ -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 } diff --git a/internal/formats/common/property_encoder.go b/internal/formats/common/property_encoder.go new file mode 100644 index 000000000..39aa2a8f6 --- /dev/null +++ b/internal/formats/common/property_encoder.go @@ -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() +} diff --git a/internal/formats/common/property_encoder_test.go b/internal/formats/common/property_encoder_test.go new file mode 100644 index 000000000..2ff63a1f2 --- /dev/null +++ b/internal/formats/common/property_encoder_test.go @@ -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) + }) + } +} diff --git a/internal/formats/cyclonedx13json/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden b/internal/formats/cyclonedx13json/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden index d9edf73bf..9b39e40d7 100644 --- a/internal/formats/cyclonedx13json/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden +++ b/internal/formats/cyclonedx13json/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden @@ -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" } ] diff --git a/internal/formats/cyclonedx13json/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden b/internal/formats/cyclonedx13json/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden index 9358fa51b..0295718a2 100644 --- a/internal/formats/cyclonedx13json/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden +++ b/internal/formats/cyclonedx13json/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden @@ -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" } ] diff --git a/internal/formats/cyclonedx13xml/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden b/internal/formats/cyclonedx13xml/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden index d5bb2a495..233d71875 100644 --- a/internal/formats/cyclonedx13xml/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden +++ b/internal/formats/cyclonedx13xml/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden @@ -1,7 +1,7 @@ - + - 2022-02-10T17:18:31-05:00 + 2022-02-25T12:54:44-05:00 anchore @@ -26,11 +26,11 @@ cpe:2.3:*:some:package:2:*:*:*:*:*:*:* a-purl-2 - the-cataloger-1 - python - python - PythonPackageMetadata - /some/path/pkg1 + the-cataloger-1 + python + PythonPackageMetadata + python + /some/path/pkg1 @@ -39,10 +39,11 @@ cpe:2.3:*:some:package:2:*:*:*:*:*:*:* a-purl-2 - the-cataloger-2 - deb - DpkgMetadata - /some/path/pkg1 + the-cataloger-2 + DpkgMetadata + deb + /some/path/pkg1 + 0 @@ -51,9 +52,10 @@ debian - debian - debian - 1.2.3 + debian + like! + debian + 1.2.3 diff --git a/internal/formats/cyclonedx13xml/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden b/internal/formats/cyclonedx13xml/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden index eb6b8634b..221b2b6e9 100644 --- a/internal/formats/cyclonedx13xml/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden +++ b/internal/formats/cyclonedx13xml/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden @@ -1,7 +1,7 @@ - + - 2022-02-10T17:18:31-05:00 + 2022-02-25T12:54:44-05:00 anchore @@ -26,12 +26,12 @@ cpe:2.3:*:some:package:1:*:*:*:*:*:*:* a-purl-1 - the-cataloger-1 - python - python - PythonPackageMetadata - /somefile-1.txt - sha256:41e7295da66c405eb3a4df29188dcf80f622f9304d487033a86d4a22e3f01abe + the-cataloger-1 + python + PythonPackageMetadata + python + sha256:41e7295da66c405eb3a4df29188dcf80f622f9304d487033a86d4a22e3f01abe + /somefile-1.txt @@ -40,11 +40,12 @@ cpe:2.3:*:some:package:2:*:*:*:*:*:*:* a-purl-2 - the-cataloger-2 - deb - DpkgMetadata - /somefile-2.txt - sha256:68a2c166dcb3acf6b7303e995ca1fe7d794bd3b5852a0b4048f9c96b796086aa + the-cataloger-2 + DpkgMetadata + deb + sha256:68a2c166dcb3acf6b7303e995ca1fe7d794bd3b5852a0b4048f9c96b796086aa + /somefile-2.txt + 0 @@ -53,9 +54,10 @@ debian - debian - debian - 1.2.3 + debian + like! + debian + 1.2.3 diff --git a/syft/source/location.go b/syft/source/location.go index 17c3e473c..3f14ed413 100644 --- a/syft/source/location.go +++ b/syft/source/location.go @@ -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 diff --git a/test/integration/encode_decode_cycle_test.go b/test/integration/encode_decode_cycle_test.go index 9d222a4d1..56788cd4f 100644 --- a/test/integration/encode_decode_cycle_test.go +++ b/test/integration/encode_decode_cycle_test.go @@ -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("[^<]+").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)) + } } }) }