diff --git a/syft/format/common/cyclonedxhelpers/decoder.go b/syft/format/common/cyclonedxhelpers/decoder.go index 13a297e80..37de22a9a 100644 --- a/syft/format/common/cyclonedxhelpers/decoder.go +++ b/syft/format/common/cyclonedxhelpers/decoder.go @@ -213,6 +213,12 @@ func extractComponents(meta *cyclonedx.Metadata) source.Description { switch c.Type { case cyclonedx.ComponentTypeContainer: + var labels map[string]string + + if meta.Properties != nil { + labels = decodeProperties(*meta.Properties, "syft:image:labels:") + } + return source.Description{ ID: "", // TODO: can we decode alias name-version somehow? (it isn't be encoded in the first place yet) @@ -221,6 +227,7 @@ func extractComponents(meta *cyclonedx.Metadata) source.Description { UserInput: c.Name, ID: c.BOMRef, ManifestDigest: c.Version, + Labels: labels, }, } case cyclonedx.ComponentTypeFile: diff --git a/syft/format/common/cyclonedxhelpers/decoder_test.go b/syft/format/common/cyclonedxhelpers/decoder_test.go index cbdebb777..411b98b6e 100644 --- a/syft/format/common/cyclonedxhelpers/decoder_test.go +++ b/syft/format/common/cyclonedxhelpers/decoder_test.go @@ -97,7 +97,6 @@ func Test_decode(t *testing.T) { CPE: "cpe:2.3:*:another:package:2:*:*:*:*:*:*:*", PackageURL: "pkg:apk/alpine/alpine-baselayout@3.2.0-r16?arch=x86_64&upstream=alpine-baselayout&distro=alpine-3.14.2", Properties: &[]cyclonedx.Property{ - { Name: "foundBy", Value: "apkdb-cataloger", diff --git a/syft/format/common/cyclonedxhelpers/format.go b/syft/format/common/cyclonedxhelpers/format.go index f99e826d3..b5d964877 100644 --- a/syft/format/common/cyclonedxhelpers/format.go +++ b/syft/format/common/cyclonedxhelpers/format.go @@ -121,7 +121,8 @@ func toBomDescriptor(name, version string, srcMetadata source.Description) *cycl Version: version, }, }, - Component: toBomDescriptorComponent(srcMetadata), + Properties: toBomProperties(srcMetadata), + Component: toBomDescriptorComponent(srcMetadata), } } @@ -190,6 +191,15 @@ func toDependencies(relationships []artifact.Relationship) []cyclonedx.Dependenc return result } +func toBomProperties(srcMetadata source.Description) *[]cyclonedx.Property { + metadata, ok := srcMetadata.Metadata.(source.StereoscopeImageSourceMetadata) + if ok { + props := encodeProperties(metadata.Labels, "syft:image:labels") + return &props + } + return nil +} + func toBomDescriptorComponent(srcMetadata source.Description) *cyclonedx.Component { name := srcMetadata.Name version := srcMetadata.Version diff --git a/syft/format/common/cyclonedxhelpers/format_test.go b/syft/format/common/cyclonedxhelpers/format_test.go index c62afb8f4..b792dea1b 100644 --- a/syft/format/common/cyclonedxhelpers/format_test.go +++ b/syft/format/common/cyclonedxhelpers/format_test.go @@ -5,12 +5,14 @@ import ( "testing" "github.com/CycloneDX/cyclonedx-go" + "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/sbom" + "github.com/anchore/syft/syft/source" ) func Test_formatCPE(t *testing.T) { @@ -138,3 +140,95 @@ func Test_relationships(t *testing.T) { }) } } + +func Test_toBomDescriptor(t *testing.T) { + type args struct { + name string + version string + srcMetadata source.Description + } + tests := []struct { + name string + args args + want *cyclonedx.Metadata + }{ + { + name: "with image labels source metadata", + args: args{ + name: "test-image", + version: "1.0.0", + srcMetadata: source.Description{ + Metadata: source.StereoscopeImageSourceMetadata{ + Labels: map[string]string{ + "key1": "value1", + }, + }, + }, + }, + want: &cyclonedx.Metadata{ + Timestamp: "", + Lifecycles: nil, + Tools: &[]cyclonedx.Tool{ + { + Vendor: "anchore", + Name: "test-image", + Version: "1.0.0", + Hashes: nil, + ExternalReferences: nil, + }, + }, + Authors: nil, + Component: &cyclonedx.Component{ + BOMRef: "", + MIMEType: "", + Type: "container", + Supplier: nil, + Author: "", + Publisher: "", + Group: "", + Name: "", + Version: "", + Description: "", + Scope: "", + Hashes: nil, + Licenses: nil, + Copyright: "", + CPE: "", + PackageURL: "", + SWID: nil, + Modified: nil, + Pedigree: nil, + ExternalReferences: nil, + Properties: nil, + Components: nil, + Evidence: nil, + ReleaseNotes: nil, + }, + Manufacture: nil, + Supplier: nil, + Licenses: nil, + Properties: &[]cyclonedx.Property{ + { + Name: "syft:image:labels:key1", + Value: "value1", + }, + }}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + subject := toBomDescriptor(tt.args.name, tt.args.version, tt.args.srcMetadata) + + require.NotEmpty(t, subject.Component.BOMRef) + subject.Timestamp = "" // not under test + + require.NotNil(t, subject.Component) + require.NotEmpty(t, subject.Component.BOMRef) + subject.Component.BOMRef = "" // not under test + + if d := cmp.Diff(tt.want, subject); d != "" { + t.Errorf("toBomDescriptor() mismatch (-want +got):\n%s", d) + } + }) + } +} diff --git a/syft/format/common/cyclonedxhelpers/properties.go b/syft/format/common/cyclonedxhelpers/properties.go index 243bd5397..b89d6d1be 100644 --- a/syft/format/common/cyclonedxhelpers/properties.go +++ b/syft/format/common/cyclonedxhelpers/properties.go @@ -1,6 +1,8 @@ package cyclonedxhelpers import ( + "strings" + "github.com/CycloneDX/cyclonedx-go" "github.com/anchore/syft/syft/format/common" @@ -19,3 +21,14 @@ func encodeProperties(obj interface{}, prefix string) (out []cyclonedx.Property) } return } + +func decodeProperties(properties []cyclonedx.Property, prefix string) map[string]string { + labels := make(map[string]string) + for _, property := range properties { + if strings.HasPrefix(property.Name, prefix) { + labelName := strings.TrimPrefix(property.Name, prefix) + labels[labelName] = property.Value + } + } + return labels +} diff --git a/syft/source/stereoscope_image_metadata.go b/syft/source/stereoscope_image_metadata.go index ade4f5923..8637995fc 100644 --- a/syft/source/stereoscope_image_metadata.go +++ b/syft/source/stereoscope_image_metadata.go @@ -18,6 +18,7 @@ type StereoscopeImageSourceMetadata struct { Architecture string `json:"architecture"` Variant string `json:"architectureVariant,omitempty"` OS string `json:"os"` + Labels map[string]string `json:"labels,omitempty"` } // StereoscopeLayerMetadata represents all static metadata that defines what a container image layer is. @@ -48,6 +49,7 @@ func NewStereoscopeImageMetadata(img *image.Image, userInput string) Stereoscope Architecture: img.Metadata.Architecture, Variant: img.Metadata.Variant, OS: img.Metadata.OS, + Labels: img.Metadata.Config.Config.Labels, } // populate image metadata diff --git a/syft/source/stereoscope_image_source.go b/syft/source/stereoscope_image_source.go index fc5defeed..53bca0025 100644 --- a/syft/source/stereoscope_image_source.go +++ b/syft/source/stereoscope_image_source.go @@ -192,6 +192,7 @@ func imageMetadataFromStereoscopeImage(img *image.Image, reference string) Stere Architecture: img.Metadata.Architecture, Variant: img.Metadata.Variant, OS: img.Metadata.OS, + Labels: img.Metadata.Config.Config.Labels, } }