From e3d10d2512a3e3bc81c2f3d6ed0ccb977beda8f9 Mon Sep 17 00:00:00 2001 From: chenk Date: Thu, 22 Jun 2023 11:33:27 +0300 Subject: [PATCH] feat: cyclondx sbom custom property support (#4688) * feat: custom property support Signed-off-by: chenk * feat: custom property support Signed-off-by: chenk * feat: custom property support Signed-off-by: chenk * feat: custom property support Signed-off-by: chenk * feat: custom property support Signed-off-by: chenk * feat: custom property support Signed-off-by: chenk * feat: custom property support Signed-off-by: chenk --------- Signed-off-by: chenk --- go.mod | 2 +- go.sum | 4 +- pkg/k8s/scanner/scanner.go | 62 ++++++++++++++++------ pkg/k8s/scanner/scanner_test.go | 48 +++++++++-------- pkg/sbom/cyclonedx/core/cyclonedx.go | 32 ++++++++---- pkg/sbom/cyclonedx/core/cyclonedx_test.go | 42 +++++++-------- pkg/sbom/cyclonedx/marshal.go | 63 ++++++++++++++--------- 7 files changed, 159 insertions(+), 94 deletions(-) diff --git a/go.mod b/go.mod index 5ea02f54bb26..45969577af55 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,7 @@ require ( github.com/aquasecurity/tml v0.6.1 github.com/aquasecurity/trivy-db v0.0.0-20230515061101-378ab9ed302c github.com/aquasecurity/trivy-java-db v0.0.0-20230209231723-7cddb1406728 - github.com/aquasecurity/trivy-kubernetes v0.5.7-0.20230619083756-6eb60789faeb + github.com/aquasecurity/trivy-kubernetes v0.5.7-0.20230621132350-8e98a8fabf9d github.com/aws/aws-sdk-go v1.44.245 github.com/aws/aws-sdk-go-v2 v1.18.0 github.com/aws/aws-sdk-go-v2/config v1.18.25 diff --git a/go.sum b/go.sum index e4d0363708cb..b0e4ffb3562a 100644 --- a/go.sum +++ b/go.sum @@ -347,8 +347,8 @@ github.com/aquasecurity/trivy-db v0.0.0-20230515061101-378ab9ed302c h1:mFMfHmb5G github.com/aquasecurity/trivy-db v0.0.0-20230515061101-378ab9ed302c/go.mod h1:s7x7CTxYeiFf6gPOakSsg4mCD93au4dbYplG4h0FGrs= github.com/aquasecurity/trivy-java-db v0.0.0-20230209231723-7cddb1406728 h1:0eS+V7SXHgqoT99tV1mtMW6HL4HdoB9qGLMCb1fZp8A= github.com/aquasecurity/trivy-java-db v0.0.0-20230209231723-7cddb1406728/go.mod h1:Ldya37FLi0e/5Cjq2T5Bty7cFkzUDwTcPeQua+2M8i8= -github.com/aquasecurity/trivy-kubernetes v0.5.7-0.20230619083756-6eb60789faeb h1:5htNYck1farYXZrNfTuAgfULRQJnK73eQ+1Rj1CE8tA= -github.com/aquasecurity/trivy-kubernetes v0.5.7-0.20230619083756-6eb60789faeb/go.mod h1:GCm7uq++jz7Ij8cA9mAorpKJ9/qSBCl7v6EKYA8DxJ8= +github.com/aquasecurity/trivy-kubernetes v0.5.7-0.20230621132350-8e98a8fabf9d h1:jcuZYglWR+KQOcb6Tg/vXM3yecMAEJmH9FDIbPRH0JQ= +github.com/aquasecurity/trivy-kubernetes v0.5.7-0.20230621132350-8e98a8fabf9d/go.mod h1:GCm7uq++jz7Ij8cA9mAorpKJ9/qSBCl7v6EKYA8DxJ8= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= diff --git a/pkg/k8s/scanner/scanner.go b/pkg/k8s/scanner/scanner.go index 77d0815b73cf..d847d7e1ed6c 100644 --- a/pkg/k8s/scanner/scanner.go +++ b/pkg/k8s/scanner/scanner.go @@ -4,12 +4,14 @@ import ( "bytes" "context" "fmt" + "sort" "strings" "golang.org/x/xerrors" ms "github.com/mitchellh/mapstructure" "github.com/package-url/packageurl-go" + "github.com/samber/lo" "github.com/aquasecurity/go-version/pkg/version" @@ -32,6 +34,13 @@ import ( "github.com/aquasecurity/trivy/pkg/types" ) +const ( + k8sCoreComponentNamespace = core.Namespace + "k8s:component" + ":" + k8sComponentType = "Type" + k8sComponentName = "Name" + k8sComponentNode = "node" +) + type Scanner struct { cluster string runner cmd.Runner @@ -215,16 +224,16 @@ func clusterInfoToReportResources(allArtifact []*artifacts.Artifact, clusterName Type: cdx.ComponentTypeContainer, Name: name, Version: cDigest, - Properties: map[string]string{ - cyc.PropertyPkgID: fmt.Sprintf("%s:%s", name, version), - cyc.PropertyPkgType: oci, + Properties: []core.Property{ + {Name: cyc.PropertyPkgID, Value: fmt.Sprintf("%s:%s", name, version)}, + {Name: cyc.PropertyPkgType, Value: oci}, }, }) } rootComponent := &core.Component{ Name: comp.Name, Type: cdx.ComponentTypeApplication, - Properties: comp.Properties, + Properties: toProperties(comp.Properties, k8sCoreComponentNamespace), Components: imageComponents, } coreComponents = append(coreComponents, rootComponent) @@ -289,34 +298,41 @@ func nodeComponent(nf bom.NodeInfo) *core.Component { osName, osVersion := osNameVersion(nf.OsImage) runtimeName, runtimeVersion := runtimeNameVersion(nf.ContainerRuntimeVersion) kubeletVersion := sanitizedVersion(nf.KubeletVersion) + properties := toProperties(nf.Properties, "") + properties = append(properties, toProperties(map[string]string{ + k8sComponentType: k8sComponentNode, + k8sComponentName: nf.NodeName, + }, k8sCoreComponentNamespace)...) return &core.Component{ Type: cdx.ComponentTypeContainer, Name: nf.NodeName, - Properties: nf.Properties, + Properties: properties, Components: []*core.Component{ { Type: cdx.ComponentTypeOS, Name: osName, Version: osVersion, - Properties: map[string]string{ - "Class": types.ClassOSPkg, - "Type": osName, + Properties: []core.Property{ + {Name: "Class", Value: types.ClassOSPkg}, + {Name: "Type", Value: osName}, }, }, { Type: cdx.ComponentTypeApplication, Name: nodeCoreComponents, - Properties: map[string]string{ - "Class": types.ClassLangPkg, - "Type": golang, + Properties: []core.Property{ + {Name: "Class", Value: types.ClassLangPkg}, + {Name: "Type", Value: golang}, }, Components: []*core.Component{ { Type: cdx.ComponentTypeLibrary, Name: kubelet, Version: kubeletVersion, - Properties: map[string]string{ - cyc.PropertyPkgType: golang, + Properties: []core.Property{ + {Name: k8sComponentType, Value: k8sComponentNode, Namespace: k8sCoreComponentNamespace}, + {Name: k8sComponentName, Value: kubelet, Namespace: k8sCoreComponentNamespace}, + {Name: cyc.PropertyPkgType, Value: golang}, }, PackageURL: &purl.PackageURL{ PackageURL: *packageurl.NewPackageURL(golang, "", kubelet, kubeletVersion, packageurl.Qualifiers{}, ""), @@ -326,8 +342,10 @@ func nodeComponent(nf bom.NodeInfo) *core.Component { Type: cdx.ComponentTypeLibrary, Name: runtimeName, Version: runtimeVersion, - Properties: map[string]string{ - cyc.PropertyPkgType: golang, + Properties: []core.Property{ + {Name: k8sComponentType, Value: k8sComponentNode, Namespace: k8sCoreComponentNamespace}, + {Name: k8sComponentName, Value: runtimeName, Namespace: k8sCoreComponentNamespace}, + {Name: cyc.PropertyPkgType, Value: golang}, }, PackageURL: &purl.PackageURL{ PackageURL: *packageurl.NewPackageURL(golang, "", runtimeName, runtimeVersion, packageurl.Qualifiers{}, ""), @@ -338,3 +356,17 @@ func nodeComponent(nf bom.NodeInfo) *core.Component { }, } } + +func toProperties(props map[string]string, namespace string) []core.Property { + properties := lo.MapToSlice(props, func(k, v string) core.Property { + return core.Property{ + Name: k, + Value: v, + Namespace: namespace, + } + }) + sort.Slice(properties, func(i, j int) bool { + return properties[i].Name < properties[j].Name + }) + return properties +} diff --git a/pkg/k8s/scanner/scanner_test.go b/pkg/k8s/scanner/scanner_test.go index f2105bfaaa52..314435ad1f28 100644 --- a/pkg/k8s/scanner/scanner_test.go +++ b/pkg/k8s/scanner/scanner_test.go @@ -78,8 +78,8 @@ func TestK8sClusterInfoReport(t *testing.T) { { Type: cdx.ComponentTypeApplication, Name: "kube-apiserver-kind-control-plane", - Properties: map[string]string{ - "ControlPlaneComponents": "kube-apiserver", + Properties: []core.Property{ + {Name: "ControlPlaneComponents", Value: "kube-apiserver", Namespace: k8sCoreComponentNamespace}, }, Components: []*core.Component{ { @@ -102,9 +102,9 @@ func TestK8sClusterInfoReport(t *testing.T) { }, }, }, - Properties: map[string]string{ - cyc.PropertyPkgID: "k8s.gcr.io/kube-apiserver:1.21.1", - cyc.PropertyPkgType: "oci", + Properties: []core.Property{ + {Name: cyc.PropertyPkgID, Value: "k8s.gcr.io/kube-apiserver:1.21.1"}, + {Name: cyc.PropertyPkgType, Value: "oci"}, }, }, }, @@ -112,37 +112,41 @@ func TestK8sClusterInfoReport(t *testing.T) { { Type: cdx.ComponentTypeContainer, Name: "kind-control-plane", - Properties: map[string]string{ - "Architecture": "arm64", - "HostName": "kind-control-plane", - "KernelVersion": "6.2.15-300.fc38.aarch64", - "NodeRole": "master", - "OperatingSystem": "linux", + Properties: []core.Property{ + {Name: "Architecture", Value: "arm64"}, + {Name: "HostName", Value: "kind-control-plane"}, + {Name: "KernelVersion", Value: "6.2.15-300.fc38.aarch64"}, + {Name: "NodeRole", Value: "master"}, + {Name: "OperatingSystem", Value: "linux"}, + {Name: k8sComponentName, Value: "kind-control-plane", Namespace: k8sCoreComponentNamespace}, + {Name: k8sComponentType, Value: "node", Namespace: k8sCoreComponentNamespace}, }, Components: []*core.Component{ { Type: cdx.ComponentTypeOS, Name: "ubuntu", Version: "21.04", - Properties: map[string]string{ - "Class": "os-pkgs", - "Type": "ubuntu", + Properties: []core.Property{ + {Name: "Class", Value: "os-pkgs", Namespace: ""}, + {Name: "Type", Value: "ubuntu", Namespace: ""}, }, }, { Type: cdx.ComponentTypeApplication, Name: "node-core-components", - Properties: map[string]string{ - "Class": "lang-pkgs", - "Type": "golang", + Properties: []core.Property{ + {Name: "Class", Value: "lang-pkgs", Namespace: ""}, + {Name: "Type", Value: "golang", Namespace: ""}, }, Components: []*core.Component{ { Type: cdx.ComponentTypeLibrary, Name: "k8s.io/kubelet", Version: "1.21.1", - Properties: map[string]string{ - "PkgType": "golang", + Properties: []core.Property{ + {Name: k8sComponentType, Value: "node", Namespace: k8sCoreComponentNamespace}, + {Name: k8sComponentName, Value: "k8s.io/kubelet", Namespace: k8sCoreComponentNamespace}, + {Name: "PkgType", Value: "golang", Namespace: ""}, }, PackageURL: &purl.PackageURL{ PackageURL: packageurl.PackageURL{ @@ -157,8 +161,10 @@ func TestK8sClusterInfoReport(t *testing.T) { Type: cdx.ComponentTypeLibrary, Name: "github.com/containerd/containerd", Version: "1.5.2", - Properties: map[string]string{ - cyc.PropertyPkgType: "golang", + Properties: []core.Property{ + {Name: k8sComponentType, Value: "node", Namespace: k8sCoreComponentNamespace}, + {Name: k8sComponentName, Value: "github.com/containerd/containerd", Namespace: k8sCoreComponentNamespace}, + {Name: "PkgType", Value: "golang", Namespace: ""}, }, PackageURL: &purl.PackageURL{ PackageURL: packageurl.PackageURL{ diff --git a/pkg/sbom/cyclonedx/core/cyclonedx.go b/pkg/sbom/cyclonedx/core/cyclonedx.go index dc7e08128a8b..b3276dc4f338 100644 --- a/pkg/sbom/cyclonedx/core/cyclonedx.go +++ b/pkg/sbom/cyclonedx/core/cyclonedx.go @@ -59,12 +59,18 @@ type Component struct { Licenses []string Hashes []digest.Digest Supplier string - Properties map[string]string + Properties []Property Components []*Component Vulnerabilities []types.DetectedVulnerability } +type Property struct { + Name string + Value string + Namespace string +} + func NewCycloneDX(version string, opts ...Option) *CycloneDX { c := &CycloneDX{ appVersion: version, @@ -291,17 +297,23 @@ func (c *CycloneDX) Licenses(licenses []string) *cdx.Licenses { return lo.ToPtr(cdx.Licenses(choices)) } -func (c *CycloneDX) Properties(props map[string]string) []cdx.Property { - properties := lo.MapToSlice(props, func(k, v string) cdx.Property { - return cdx.Property{ - Name: Namespace + k, - Value: v, +func (c *CycloneDX) Properties(properties []Property) []cdx.Property { + cdxProps := make([]cdx.Property, 0, len(properties)) + for _, property := range properties { + namespace := Namespace + if len(property.Namespace) > 0 { + namespace = property.Namespace } + cdxProps = append(cdxProps, + cdx.Property{ + Name: namespace + property.Name, + Value: property.Value, + }) + } + sort.Slice(cdxProps, func(i, j int) bool { + return cdxProps[i].Name < cdxProps[j].Name }) - sort.Slice(properties, func(i, j int) bool { - return properties[i].Name < properties[j].Name - }) - return properties + return cdxProps } func IsTrivySBOM(c *cdx.BOM) bool { diff --git a/pkg/sbom/cyclonedx/core/cyclonedx_test.go b/pkg/sbom/cyclonedx/core/cyclonedx_test.go index 5a86a5667f6f..f6c2041e3178 100644 --- a/pkg/sbom/cyclonedx/core/cyclonedx_test.go +++ b/pkg/sbom/cyclonedx/core/cyclonedx_test.go @@ -32,8 +32,8 @@ func TestMarshaler_CoreComponent(t *testing.T) { { Type: cdx.ComponentTypeApplication, Name: "kube-apiserver-kind-control-plane", - Properties: map[string]string{ - "control_plane_components": "kube-apiserver", + Properties: []core.Property{ + {Name: "control_plane_components", Value: "kube-apiserver"}, }, Components: []*core.Component{ { @@ -57,9 +57,9 @@ func TestMarshaler_CoreComponent(t *testing.T) { }, }, Hashes: []digest.Digest{"sha256:18e61c783b41758dd391ab901366ec3546b26fae00eef7e223d1f94da808e02f"}, - Properties: map[string]string{ - "PkgID": "k8s.gcr.io/kube-apiserver:1.21.1", - "PkgType": "oci", + Properties: []core.Property{ + {Name: "PkgID", Value: "k8s.gcr.io/kube-apiserver:1.21.1"}, + {Name: "PkgType", Value: "oci"}, }, }, }, @@ -67,37 +67,37 @@ func TestMarshaler_CoreComponent(t *testing.T) { { Type: cdx.ComponentTypeContainer, Name: "kind-control-plane", - Properties: map[string]string{ - "architecture": "arm64", - "host_name": "kind-control-plane", - "kernel_version": "6.2.13-300.fc38.aarch64", - "node_role": "master", - "operating_system": "linux", + Properties: []core.Property{ + {Name: "architecture", Value: "arm64"}, + {Name: "host_name", Value: "kind-control-plane"}, + {Name: "kernel_version", Value: "6.2.13-300.fc38.aarch64"}, + {Name: "node_role", Value: "master"}, + {Name: "operating_system", Value: "linux"}, }, Components: []*core.Component{ { Type: cdx.ComponentTypeOS, Name: "ubuntu", Version: "21.04", - Properties: map[string]string{ - "Class": "os-pkgs", - "Type": "ubuntu", + Properties: []core.Property{ + {Name: "Class", Value: "os-pkgs"}, + {Name: "Type", Value: "ubuntu"}, }, }, { Type: cdx.ComponentTypeApplication, Name: "node-core-components", - Properties: map[string]string{ - "Class": "lang-pkgs", - "Type": "golang", + Properties: []core.Property{ + {Name: "Class", Value: "lang-pkgs"}, + {Name: "Type", Value: "golang"}, }, Components: []*core.Component{ { Type: cdx.ComponentTypeLibrary, Name: "kubelet", Version: "1.21.1", - Properties: map[string]string{ - "PkgType": "golang", + Properties: []core.Property{ + {Name: "PkgType", Value: "golang"}, }, PackageURL: &purl.PackageURL{ PackageURL: packageurl.PackageURL{ @@ -112,8 +112,8 @@ func TestMarshaler_CoreComponent(t *testing.T) { Type: cdx.ComponentTypeLibrary, Name: "containerd", Version: "1.5.2", - Properties: map[string]string{ - "PkgType": "golang", + Properties: []core.Property{ + {Name: "PkgType", Value: "golang"}, }, PackageURL: &purl.PackageURL{ PackageURL: packageurl.PackageURL{ diff --git a/pkg/sbom/cyclonedx/marshal.go b/pkg/sbom/cyclonedx/marshal.go index 258607b77fd3..fc562f9d7d31 100644 --- a/pkg/sbom/cyclonedx/marshal.go +++ b/pkg/sbom/cyclonedx/marshal.go @@ -216,14 +216,17 @@ func (e *Marshaler) rootComponent(r types.Report) (*core.Component, error) { Name: r.ArtifactName, } - props := map[string]string{ - PropertySchemaVersion: strconv.Itoa(r.SchemaVersion), + props := []core.Property{ + {Name: PropertySchemaVersion, Value: strconv.Itoa(r.SchemaVersion)}, } switch r.ArtifactType { case ftypes.ArtifactContainerImage: root.Type = cdx.ComponentTypeContainer - props[PropertyImageID] = r.Metadata.ImageID + props = append(props, core.Property{ + Name: PropertyImageID, + Value: r.Metadata.ImageID, + }) p, err := purl.NewPackageURL(purl.TypeOCI, r.Metadata, ftypes.Package{}) if err != nil { @@ -239,17 +242,29 @@ func (e *Marshaler) rootComponent(r types.Report) (*core.Component, error) { } if r.Metadata.Size != 0 { - props[PropertySize] = strconv.FormatInt(r.Metadata.Size, 10) + props = append(props, core.Property{ + Name: PropertySize, + Value: strconv.FormatInt(r.Metadata.Size, 10), + }) } if len(r.Metadata.RepoDigests) > 0 { - props[PropertyRepoDigest] = strings.Join(r.Metadata.RepoDigests, ",") + props = append(props, core.Property{ + Name: PropertyRepoDigest, + Value: strings.Join(r.Metadata.RepoDigests, ","), + }) } if len(r.Metadata.DiffIDs) > 0 { - props[PropertyDiffID] = strings.Join(r.Metadata.DiffIDs, ",") + props = append(props, core.Property{ + Name: PropertyDiffID, + Value: strings.Join(r.Metadata.DiffIDs, ","), + }) } if len(r.Metadata.RepoTags) > 0 { - props[PropertyRepoTag] = strings.Join(r.Metadata.RepoTags, ",") + props = append(props, core.Property{ + Name: PropertyRepoTag, + Value: strings.Join(r.Metadata.RepoTags, ","), + }) } root.Properties = filterProperties(props) @@ -260,9 +275,9 @@ func (e *Marshaler) rootComponent(r types.Report) (*core.Component, error) { func (e *Marshaler) resultComponent(r types.Result, osFound *ftypes.OS) *core.Component { component := &core.Component{ Name: r.Target, - Properties: map[string]string{ - PropertyType: r.Type, - PropertyClass: string(r.Class), + Properties: []core.Property{ + {Name: PropertyType, Value: r.Type}, + {Name: PropertyClass, Value: string(r.Class)}, }, } @@ -298,17 +313,17 @@ func pkgComponent(pkg Package) (*core.Component, error) { group = pu.Namespace } - properties := map[string]string{ - PropertyPkgID: pkg.ID, - PropertyPkgType: pkg.Type, - PropertyFilePath: pkg.FilePath, - PropertySrcName: pkg.SrcName, - PropertySrcVersion: pkg.SrcVersion, - PropertySrcRelease: pkg.SrcRelease, - PropertySrcEpoch: strconv.Itoa(pkg.SrcEpoch), - PropertyModularitylabel: pkg.Modularitylabel, - PropertyLayerDigest: pkg.Layer.Digest, - PropertyLayerDiffID: pkg.Layer.DiffID, + properties := []core.Property{ + {Name: PropertyPkgID, Value: pkg.ID}, + {Name: PropertyPkgType, Value: pkg.Type}, + {Name: PropertyFilePath, Value: pkg.FilePath}, + {Name: PropertySrcName, Value: pkg.SrcName}, + {Name: PropertySrcVersion, Value: pkg.SrcVersion}, + {Name: PropertySrcRelease, Value: pkg.SrcRelease}, + {Name: PropertySrcEpoch, Value: strconv.Itoa(pkg.SrcEpoch)}, + {Name: PropertyModularitylabel, Value: pkg.Modularitylabel}, + {Name: PropertyLayerDigest, Value: pkg.Layer.Digest}, + {Name: PropertyLayerDiffID, Value: pkg.Layer.DiffID}, } return &core.Component{ @@ -325,8 +340,8 @@ func pkgComponent(pkg Package) (*core.Component, error) { }, nil } -func filterProperties(props map[string]string) map[string]string { - return lo.OmitBy(props, func(key string, value string) bool { - return value == "" || (key == PropertySrcEpoch && value == "0") +func filterProperties(props []core.Property) []core.Property { + return lo.Filter(props, func(property core.Property, index int) bool { + return !(property.Value == "" || (property.Name == PropertySrcEpoch && property.Value == "0")) }) }