Skip to content

Commit

Permalink
include image labels in cycloneDX SBOM (#2294)
Browse files Browse the repository at this point in the history
* include image labels in SBOM

Signed-off-by: Benji Visser <benji@093b.org>

* update tests

Signed-off-by: Benji Visser <benji@093b.org>

* gocritic

Signed-off-by: Benji Visser <benji@093b.org>

* add properties

Signed-off-by: Benji Visser <benji@093b.org>

* add decoder

Signed-off-by: Benji Visser <benji@093b.org>

* update golden snapshots

Signed-off-by: Benji Visser <benji@093b.org>

* decodeProperties

Signed-off-by: Benji Visser <benji@093b.org>

* add test

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* remove the snapshot test changes

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* restore snapshots

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Benji Visser <benji@093b.org>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
  • Loading branch information
noqcks and wagoodman committed Nov 8, 2023
1 parent 502971a commit 0891d35
Show file tree
Hide file tree
Showing 7 changed files with 128 additions and 2 deletions.
7 changes: 7 additions & 0 deletions syft/format/common/cyclonedxhelpers/decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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:
Expand Down
1 change: 0 additions & 1 deletion syft/format/common/cyclonedxhelpers/decoder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
12 changes: 11 additions & 1 deletion syft/format/common/cyclonedxhelpers/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,8 @@ func toBomDescriptor(name, version string, srcMetadata source.Description) *cycl
Version: version,
},
},
Component: toBomDescriptorComponent(srcMetadata),
Properties: toBomProperties(srcMetadata),
Component: toBomDescriptorComponent(srcMetadata),
}
}

Expand Down Expand Up @@ -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
Expand Down
94 changes: 94 additions & 0 deletions syft/format/common/cyclonedxhelpers/format_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
}
})
}
}
13 changes: 13 additions & 0 deletions syft/format/common/cyclonedxhelpers/properties.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package cyclonedxhelpers

import (
"strings"

"github.com/CycloneDX/cyclonedx-go"

"github.com/anchore/syft/syft/format/common"
Expand All @@ -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
}
2 changes: 2 additions & 0 deletions syft/source/stereoscope_image_metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions syft/source/stereoscope_image_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}

Expand Down

0 comments on commit 0891d35

Please sign in to comment.