diff --git a/go.mod b/go.mod index 41f8ca4065ba..87a4ce6acf5f 100644 --- a/go.mod +++ b/go.mod @@ -91,6 +91,7 @@ require ( github.com/testcontainers/testcontainers-go/modules/localstack v0.21.0 github.com/tetratelabs/wazero v1.2.1 github.com/twitchtv/twirp v8.1.2+incompatible + github.com/xeipuuv/gojsonschema v1.2.0 github.com/xlab/treeprint v1.1.0 go.etcd.io/bbolt v1.3.7 go.uber.org/zap v1.24.0 @@ -344,7 +345,6 @@ require ( github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect - github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/yashtewari/glob-intersection v0.1.0 // indirect github.com/yuin/gopher-lua v1.1.0 // indirect github.com/zclconf/go-cty v1.10.0 // indirect diff --git a/pkg/k8s/report/cyclonedx.go b/pkg/k8s/report/cyclonedx.go index 3cd841310f2d..a7bbf69d026f 100644 --- a/pkg/k8s/report/cyclonedx.go +++ b/pkg/k8s/report/cyclonedx.go @@ -15,12 +15,12 @@ type CycloneDXWriter struct { } // NewCycloneDXWriter constract new CycloneDXWriter -func NewCycloneDXWriter(output io.Writer, format cdx.BOMFileFormat, appVersion string, opts ...core.Option) CycloneDXWriter { +func NewCycloneDXWriter(output io.Writer, format cdx.BOMFileFormat, appVersion string) CycloneDXWriter { encoder := cdx.NewBOMEncoder(output, format) encoder.SetPretty(true) return CycloneDXWriter{ encoder: encoder, - marshaler: core.NewCycloneDX(appVersion, opts...), + marshaler: core.NewCycloneDX(appVersion), } } diff --git a/pkg/sbom/cyclonedx/core/cyclonedx.go b/pkg/sbom/cyclonedx/core/cyclonedx.go index 53d9c1fa7234..a7f37801727e 100644 --- a/pkg/sbom/cyclonedx/core/cyclonedx.go +++ b/pkg/sbom/cyclonedx/core/cyclonedx.go @@ -7,17 +7,17 @@ import ( "strings" cdx "github.com/CycloneDX/cyclonedx-go" - "github.com/google/uuid" "github.com/samber/lo" "golang.org/x/exp/slices" - "k8s.io/utils/clock" dtypes "github.com/aquasecurity/trivy-db/pkg/types" "github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability" + "github.com/aquasecurity/trivy/pkg/clock" "github.com/aquasecurity/trivy/pkg/digest" "github.com/aquasecurity/trivy/pkg/log" "github.com/aquasecurity/trivy/pkg/purl" "github.com/aquasecurity/trivy/pkg/types" + "github.com/aquasecurity/trivy/pkg/uuid" ) const ( @@ -29,26 +29,8 @@ const ( timeLayout = "2006-01-02T15:04:05+00:00" ) -type NewUUID func() uuid.UUID - -type Option func(dx *CycloneDX) - -func WithClock(clock clock.Clock) Option { - return func(opts *CycloneDX) { - opts.clock = clock - } -} - -func WithNewUUID(newUUID NewUUID) Option { - return func(opts *CycloneDX) { - opts.newUUID = newUUID - } -} - type CycloneDX struct { appVersion string - clock clock.Clock - newUUID NewUUID } type Component struct { @@ -72,22 +54,15 @@ type Property struct { Namespace string } -func NewCycloneDX(version string, opts ...Option) *CycloneDX { - c := &CycloneDX{ +func NewCycloneDX(version string) *CycloneDX { + return &CycloneDX{ appVersion: version, - clock: clock.RealClock{}, - newUUID: uuid.New, - } - for _, opt := range opts { - opt(c) } - - return c } func (c *CycloneDX) Marshal(root *Component) *cdx.BOM { bom := cdx.NewBOM() - bom.SerialNumber = c.newUUID().URN() + bom.SerialNumber = uuid.New().URN() bom.Metadata = c.Metadata() components := map[string]*cdx.Component{} @@ -200,14 +175,14 @@ func (c *CycloneDX) marshalVulnerability(bomRef string, vuln types.DetectedVulne func (c *CycloneDX) BOMRef(component *Component) string { // PURL takes precedence over UUID if component.PackageURL == nil { - return c.newUUID().String() + return uuid.New().String() } return component.PackageURL.BOMRef() } func (c *CycloneDX) Metadata() *cdx.Metadata { return &cdx.Metadata{ - Timestamp: c.clock.Now().UTC().Format(timeLayout), + Timestamp: clock.Now().UTC().Format(timeLayout), Tools: &[]cdx.Tool{ { Vendor: ToolVendor, @@ -306,7 +281,8 @@ func (c *CycloneDX) Licenses(licenses []string) *cdx.Licenses { choices := lo.Map(licenses, func(license string, i int) cdx.LicenseChoice { return cdx.LicenseChoice{ License: &cdx.License{ - Name: license}, + Name: license, + }, } }) return lo.ToPtr(cdx.Licenses(choices)) diff --git a/pkg/sbom/cyclonedx/core/cyclonedx_test.go b/pkg/sbom/cyclonedx/core/cyclonedx_test.go index 2fc142d1e0a9..025a2871c491 100644 --- a/pkg/sbom/cyclonedx/core/cyclonedx_test.go +++ b/pkg/sbom/cyclonedx/core/cyclonedx_test.go @@ -1,19 +1,18 @@ package core_test import ( - "fmt" "testing" "time" - "github.com/aquasecurity/trivy/pkg/digest" - "github.com/aquasecurity/trivy/pkg/purl" - "github.com/aquasecurity/trivy/pkg/sbom/cyclonedx/core" - cdx "github.com/CycloneDX/cyclonedx-go" - "github.com/google/uuid" "github.com/package-url/packageurl-go" "github.com/stretchr/testify/assert" - fake "k8s.io/utils/clock/testing" + + "github.com/aquasecurity/trivy/pkg/clock" + "github.com/aquasecurity/trivy/pkg/digest" + "github.com/aquasecurity/trivy/pkg/purl" + "github.com/aquasecurity/trivy/pkg/sbom/cyclonedx/core" + "github.com/aquasecurity/trivy/pkg/uuid" ) func TestMarshaler_CoreComponent(t *testing.T) { @@ -33,7 +32,10 @@ func TestMarshaler_CoreComponent(t *testing.T) { Type: cdx.ComponentTypeApplication, Name: "kube-apiserver-kind-control-plane", Properties: []core.Property{ - {Name: "control_plane_components", Value: "kube-apiserver"}, + { + Name: "control_plane_components", + Value: "kube-apiserver", + }, }, Components: []*core.Component{ { @@ -58,8 +60,14 @@ func TestMarshaler_CoreComponent(t *testing.T) { }, Hashes: []digest.Digest{"sha256:18e61c783b41758dd391ab901366ec3546b26fae00eef7e223d1f94da808e02f"}, Properties: []core.Property{ - {Name: "PkgID", Value: "k8s.gcr.io/kube-apiserver:1.21.1"}, - {Name: "PkgType", Value: "oci"}, + { + Name: "PkgID", + Value: "k8s.gcr.io/kube-apiserver:1.21.1", + }, + { + Name: "PkgType", + Value: "oci", + }, }, }, }, @@ -68,11 +76,26 @@ func TestMarshaler_CoreComponent(t *testing.T) { Type: cdx.ComponentTypeContainer, Name: "kind-control-plane", 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"}, + { + 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{ { @@ -80,16 +103,28 @@ func TestMarshaler_CoreComponent(t *testing.T) { Name: "ubuntu", Version: "21.04", Properties: []core.Property{ - {Name: "Class", Value: "os-pkgs"}, - {Name: "Type", Value: "ubuntu"}, + { + Name: "Class", + Value: "os-pkgs", + }, + { + Name: "Type", + Value: "ubuntu", + }, }, }, { Type: cdx.ComponentTypeApplication, Name: "node-core-components", Properties: []core.Property{ - {Name: "Class", Value: "lang-pkgs"}, - {Name: "Type", Value: "golang"}, + { + Name: "Class", + Value: "lang-pkgs", + }, + { + Name: "Type", + Value: "golang", + }, }, Components: []*core.Component{ { @@ -97,7 +132,10 @@ func TestMarshaler_CoreComponent(t *testing.T) { Name: "kubelet", Version: "1.21.1", Properties: []core.Property{ - {Name: "PkgType", Value: "golang"}, + { + Name: "PkgType", + Value: "golang", + }, }, PackageURL: &purl.PackageURL{ PackageURL: packageurl.PackageURL{ @@ -113,7 +151,10 @@ func TestMarshaler_CoreComponent(t *testing.T) { Name: "containerd", Version: "1.5.2", Properties: []core.Property{ - {Name: "PkgType", Value: "golang"}, + { + Name: "PkgType", + Value: "golang", + }, }, PackageURL: &purl.PackageURL{ PackageURL: packageurl.PackageURL{ @@ -321,16 +362,13 @@ func TestMarshaler_CoreComponent(t *testing.T) { }, }, } - clock := fake.NewFakeClock(time.Date(2021, 8, 25, 12, 20, 30, 5, time.UTC)) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - var count int - newUUID := func() uuid.UUID { - count++ - return uuid.Must(uuid.Parse(fmt.Sprintf("3ff14136-e09f-4df9-80ea-%012d", count))) - } - marshaler := core.NewCycloneDX("dev", core.WithClock(clock), core.WithNewUUID(newUUID)) + clock.SetFakeTime(t, time.Date(2021, 8, 25, 12, 20, 30, 5, time.UTC)) + uuid.SetFakeUUID(t, "3ff14136-e09f-4df9-80ea-%012d") + + marshaler := core.NewCycloneDX("dev") got := marshaler.Marshal(tt.rootComponent) assert.Equal(t, tt.want, got) }) diff --git a/pkg/sbom/cyclonedx/marshal.go b/pkg/sbom/cyclonedx/marshal.go index 035263dc8122..9ff482a7bdf5 100644 --- a/pkg/sbom/cyclonedx/marshal.go +++ b/pkg/sbom/cyclonedx/marshal.go @@ -50,9 +50,9 @@ type Marshaler struct { core *core.CycloneDX } -func NewMarshaler(version string, opts ...core.Option) *Marshaler { +func NewMarshaler(version string) *Marshaler { return &Marshaler{ - core: core.NewCycloneDX(version, opts...), + core: core.NewCycloneDX(version), } } @@ -217,7 +217,10 @@ func (e *Marshaler) rootComponent(r types.Report) (*core.Component, error) { } props := []core.Property{ - {Name: PropertySchemaVersion, Value: strconv.Itoa(r.SchemaVersion)}, + { + Name: PropertySchemaVersion, + Value: strconv.Itoa(r.SchemaVersion), + }, } switch r.ArtifactType { @@ -276,8 +279,14 @@ func (e *Marshaler) resultComponent(r types.Result, osFound *ftypes.OS) *core.Co component := &core.Component{ Name: r.Target, Properties: []core.Property{ - {Name: PropertyType, Value: r.Type}, - {Name: PropertyClass, Value: string(r.Class)}, + { + Name: PropertyType, + Value: r.Type, + }, + { + Name: PropertyClass, + Value: string(r.Class), + }, }, } @@ -314,16 +323,46 @@ func pkgComponent(pkg Package) (*core.Component, error) { } 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}, + { + 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{ diff --git a/pkg/sbom/cyclonedx/marshal_test.go b/pkg/sbom/cyclonedx/marshal_test.go index bfb3d7161662..b32739630ea3 100644 --- a/pkg/sbom/cyclonedx/marshal_test.go +++ b/pkg/sbom/cyclonedx/marshal_test.go @@ -1,28 +1,24 @@ package cyclonedx_test import ( - "fmt" "testing" "time" - "github.com/aquasecurity/trivy/pkg/sbom/cyclonedx/core" - - "github.com/aquasecurity/trivy/pkg/sbom/cyclonedx" - cdx "github.com/CycloneDX/cyclonedx-go" v1 "github.com/google/go-containerregistry/pkg/v1" - "github.com/google/uuid" "github.com/samber/lo" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - fake "k8s.io/utils/clock/testing" dtypes "github.com/aquasecurity/trivy-db/pkg/types" "github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability" + "github.com/aquasecurity/trivy/pkg/clock" fos "github.com/aquasecurity/trivy/pkg/fanal/analyzer/os" ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" "github.com/aquasecurity/trivy/pkg/report" + "github.com/aquasecurity/trivy/pkg/sbom/cyclonedx" "github.com/aquasecurity/trivy/pkg/types" + "github.com/aquasecurity/trivy/pkg/uuid" ) func TestMarshaler_Marshal(t *testing.T) { @@ -1585,17 +1581,12 @@ func TestMarshaler_Marshal(t *testing.T) { }, } - clock := fake.NewFakeClock(time.Date(2021, 8, 25, 12, 20, 30, 5, time.UTC)) - for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - var count int - newUUID := func() uuid.UUID { - count++ - return uuid.Must(uuid.Parse(fmt.Sprintf("3ff14136-e09f-4df9-80ea-%012d", count))) - } + clock.SetFakeTime(t, time.Date(2021, 8, 25, 12, 20, 30, 5, time.UTC)) + uuid.SetFakeUUID(t, "3ff14136-e09f-4df9-80ea-%012d") - marshaler := cyclonedx.NewMarshaler("dev", core.WithClock(clock), core.WithNewUUID(newUUID)) + marshaler := cyclonedx.NewMarshaler("dev") got, err := marshaler.Marshal(tt.inputReport) require.NoError(t, err) assert.Equal(t, tt.want, got) diff --git a/pkg/sbom/spdx/marshal.go b/pkg/sbom/spdx/marshal.go index 266b02f7e3c0..1444d228f69a 100644 --- a/pkg/sbom/spdx/marshal.go +++ b/pkg/sbom/spdx/marshal.go @@ -7,15 +7,14 @@ import ( "strings" "time" - "github.com/google/uuid" "github.com/mitchellh/hashstructure/v2" "github.com/samber/lo" "github.com/spdx/tools-golang/spdx" "github.com/spdx/tools-golang/spdx/v2/common" "golang.org/x/exp/maps" "golang.org/x/xerrors" - "k8s.io/utils/clock" + "github.com/aquasecurity/trivy/pkg/clock" "github.com/aquasecurity/trivy/pkg/digest" ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" "github.com/aquasecurity/trivy/pkg/licensing" @@ -24,6 +23,7 @@ import ( "github.com/aquasecurity/trivy/pkg/purl" "github.com/aquasecurity/trivy/pkg/scanner/utils" "github.com/aquasecurity/trivy/pkg/types" + "github.com/aquasecurity/trivy/pkg/uuid" ) const ( @@ -77,30 +77,14 @@ var ( type Marshaler struct { format spdx.Document - clock clock.Clock - newUUID newUUID hasher Hash appVersion string // Trivy version. It needed for `creator` field } type Hash func(v interface{}, format hashstructure.Format, opts *hashstructure.HashOptions) (uint64, error) -type newUUID func() uuid.UUID - type marshalOption func(*Marshaler) -func WithClock(clock clock.Clock) marshalOption { - return func(opts *Marshaler) { - opts.clock = clock - } -} - -func WithNewUUID(newUUID newUUID) marshalOption { - return func(opts *Marshaler) { - opts.newUUID = newUUID - } -} - func WithHasher(hasher Hash) marshalOption { return func(opts *Marshaler) { opts.hasher = hasher @@ -110,8 +94,6 @@ func WithHasher(hasher Hash) marshalOption { func NewMarshaler(version string, opts ...marshalOption) *Marshaler { m := &Marshaler{ format: spdx.Document{}, - clock: clock.RealClock{}, - newUUID: uuid.New, hasher: hashstructure.Hash, appVersion: version, } @@ -192,7 +174,7 @@ func (m *Marshaler) Marshal(r types.Report) (*spdx.Document, error) { CreatorType: "Tool", }, }, - Created: m.clock.Now().UTC().Format(time.RFC3339), + Created: clock.Now().UTC().Format(time.RFC3339), }, Packages: toPackages(packages), Relationships: relationShips, @@ -456,7 +438,7 @@ func getDocumentNamespace(r types.Report, m *Marshaler) string { DocumentNamespace, string(r.ArtifactType), strings.ReplaceAll(strings.ReplaceAll(r.ArtifactName, "https://", ""), "http://", ""), // remove http(s):// prefix when scanning repos - m.newUUID().String(), + uuid.New().String(), ) } diff --git a/pkg/sbom/spdx/marshal_test.go b/pkg/sbom/spdx/marshal_test.go index b889d86f12a3..3cbdbe226b76 100644 --- a/pkg/sbom/spdx/marshal_test.go +++ b/pkg/sbom/spdx/marshal_test.go @@ -1,25 +1,24 @@ package spdx_test import ( - "fmt" "hash/fnv" "testing" "time" v1 "github.com/google/go-containerregistry/pkg/v1" - "github.com/google/uuid" "github.com/mitchellh/hashstructure/v2" "github.com/spdx/tools-golang/spdx" "github.com/spdx/tools-golang/spdx/v2/common" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - fake "k8s.io/utils/clock/testing" + "github.com/aquasecurity/trivy/pkg/clock" fos "github.com/aquasecurity/trivy/pkg/fanal/analyzer/os" ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" "github.com/aquasecurity/trivy/pkg/report" tspdx "github.com/aquasecurity/trivy/pkg/sbom/spdx" "github.com/aquasecurity/trivy/pkg/types" + "github.com/aquasecurity/trivy/pkg/uuid" ) func TestMarshaler_Marshal(t *testing.T) { @@ -839,16 +838,8 @@ func TestMarshaler_Marshal(t *testing.T) { }, } - clock := fake.NewFakeClock(time.Date(2021, 8, 25, 12, 20, 30, 5, time.UTC)) - for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - var count int - newUUID := func() uuid.UUID { - count++ - return uuid.Must(uuid.Parse(fmt.Sprintf("3ff14136-e09f-4df9-80ea-%012d", count))) - } - // Fake function calculating the hash value h := fnv.New64() hasher := func(v interface{}, format hashstructure.Format, opts *hashstructure.HashOptions) (uint64, error) { @@ -873,7 +864,10 @@ func TestMarshaler_Marshal(t *testing.T) { return h.Sum64(), nil } - marshaler := tspdx.NewMarshaler("0.38.1", tspdx.WithClock(clock), tspdx.WithNewUUID(newUUID), tspdx.WithHasher(hasher)) + clock.SetFakeTime(t, time.Date(2021, 8, 25, 12, 20, 30, 5, time.UTC)) + uuid.SetFakeUUID(t, "3ff14136-e09f-4df9-80ea-%012d") + + marshaler := tspdx.NewMarshaler("0.38.1", tspdx.WithHasher(hasher)) spdxDoc, err := marshaler.Marshal(tc.inputReport) require.NoError(t, err) diff --git a/pkg/uuid/uuid.go b/pkg/uuid/uuid.go new file mode 100644 index 000000000000..2841b7f099bc --- /dev/null +++ b/pkg/uuid/uuid.go @@ -0,0 +1,28 @@ +package uuid + +import ( + "fmt" + "testing" + + "github.com/google/uuid" +) + +var newUUID func() uuid.UUID = uuid.New + +// SetFakeUUID sets a fake UUID for testing. +// The 'format' is used to generate a fake UUID and +// must contain a single '%d' which will be replaced with a counter. +func SetFakeUUID(t *testing.T, format string) { + var count int + newUUID = func() uuid.UUID { + count++ + return uuid.Must(uuid.Parse(fmt.Sprintf(format, count))) + } + t.Cleanup(func() { + newUUID = uuid.New + }) +} + +func New() uuid.UUID { + return newUUID() +}