diff --git a/pkg/sbom/cyclonedx/marshal.go b/pkg/sbom/cyclonedx/marshal.go index 75dd2db93b..785d4fc405 100644 --- a/pkg/sbom/cyclonedx/marshal.go +++ b/pkg/sbom/cyclonedx/marshal.go @@ -56,8 +56,7 @@ func NewMarshaler(version string) Marshaler { // MarshalReport converts the Trivy report to the CycloneDX format func (m *Marshaler) MarshalReport(ctx context.Context, report types.Report) (*cdx.BOM, error) { // Convert into an intermediate representation - opts := core.Options{GenerateBOMRef: true} - bom, err := sbomio.NewEncoder(opts).Encode(report) + bom, err := sbomio.NewEncoder(sbomio.WithBOMRef()).Encode(report) if err != nil { return nil, xerrors.Errorf("failed to marshal report: %w", err) } diff --git a/pkg/sbom/io/encode.go b/pkg/sbom/io/encode.go index dfaca17d06..1cdc77f170 100644 --- a/pkg/sbom/io/encode.go +++ b/pkg/sbom/io/encode.go @@ -19,21 +19,49 @@ import ( "github.com/aquasecurity/trivy/pkg/types" ) -type Encoder struct { - bom *core.BOM - opts core.Options +type EncoderOption func(*Encoder) + +// WithBOMRef enables BOM-Ref generation for CycloneDX components +func WithBOMRef() EncoderOption { + return func(e *Encoder) { + e.bomOpts.GenerateBOMRef = true + } } -func NewEncoder(opts core.Options) *Encoder { - return &Encoder{opts: opts} +// WithParents enables holding parent maps in the BOM structure +func WithParents() EncoderOption { + return func(e *Encoder) { + e.bomOpts.Parents = true + } +} + +// ForceRegenerate forces regeneration of BOM instead of reusing existing one +func ForceRegenerate() EncoderOption { + return func(e *Encoder) { + e.forceRegenerate = true + } +} + +type Encoder struct { + bom *core.BOM + bomOpts core.Options + forceRegenerate bool +} + +func NewEncoder(opts ...EncoderOption) *Encoder { + e := &Encoder{} + for _, opt := range opts { + opt(e) + } + return e } func (e *Encoder) Encode(report types.Report) (*core.BOM, error) { - // When report.BOM is not nil, reuse the existing BOM structure. + // When report.BOM is not nil, reuse the existing BOM structure unless ForceRegenerate is set. // This happens in two scenarios: // 1. SBOM scanning: When scanning an existing SBOM file to refresh vulnerabilities // 2. Library usage: When using Trivy as a library with a custom BOM in the report - if report.BOM != nil { + if report.BOM != nil && !e.forceRegenerate { return e.reuseExistingBOM(report) } // Metadata component @@ -42,7 +70,7 @@ func (e *Encoder) Encode(report types.Report) (*core.BOM, error) { return nil, xerrors.Errorf("failed to create root component: %w", err) } - e.bom = core.NewBOM(e.opts) + e.bom = core.NewBOM(e.bomOpts) if report.BOM != nil { e.bom.SerialNumber = report.BOM.SerialNumber e.bom.Version = report.BOM.Version diff --git a/pkg/sbom/io/encode_test.go b/pkg/sbom/io/encode_test.go index 2d67f29c4c..e64d6960d7 100644 --- a/pkg/sbom/io/encode_test.go +++ b/pkg/sbom/io/encode_test.go @@ -1466,8 +1466,7 @@ func TestEncoder_Encode(t *testing.T) { t.Run(tt.name, func(t *testing.T) { uuid.SetFakeUUID(t, "3ff14136-e09f-4df9-80ea-%012d") - opts := core.Options{GenerateBOMRef: true} - got, err := sbomio.NewEncoder(opts).Encode(tt.report) + got, err := sbomio.NewEncoder(sbomio.WithBOMRef()).Encode(tt.report) if tt.wantErr != "" { require.ErrorContains(t, err, tt.wantErr) return diff --git a/pkg/sbom/spdx/marshal.go b/pkg/sbom/spdx/marshal.go index 3af2fbb80e..dad1d7ca4e 100644 --- a/pkg/sbom/spdx/marshal.go +++ b/pkg/sbom/spdx/marshal.go @@ -115,7 +115,7 @@ func NewMarshaler(version string, opts ...marshalOption) *Marshaler { func (m *Marshaler) MarshalReport(ctx context.Context, report types.Report) (*spdx.Document, error) { // Convert into an intermediate representation - bom, err := sbomio.NewEncoder(core.Options{}).Encode(report) + bom, err := sbomio.NewEncoder().Encode(report) if err != nil { return nil, xerrors.Errorf("failed to marshal report: %w", err) } diff --git a/pkg/vex/vex.go b/pkg/vex/vex.go index 925dbeaa6c..49fd62eccd 100644 --- a/pkg/vex/vex.go +++ b/pkg/vex/vex.go @@ -75,7 +75,7 @@ func Filter(ctx context.Context, report *types.Report, opts Options) error { } // NOTE: This method call has a side effect on the report - bom, err := sbomio.NewEncoder(core.Options{Parents: true}).Encode(*report) + bom, err := sbomio.NewEncoder(sbomio.WithParents(), sbomio.ForceRegenerate()).Encode(*report) if err != nil { return xerrors.Errorf("unable to encode the SBOM: %w", err) } diff --git a/pkg/vex/vex_test.go b/pkg/vex/vex_test.go index 55c93566e4..3a36154bff 100644 --- a/pkg/vex/vex_test.go +++ b/pkg/vex/vex_test.go @@ -665,13 +665,18 @@ func createCycloneDXBOMWithSpringComponent() *core.BOM { bom := core.NewBOM(core.Options{}) bom.SerialNumber = "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79" bom.Version = 1 + pkgIdentifier := ftypes.PkgIdentifier{ + // Components got from scanned SBOM files don't have UID + BOMRef: springPackage.Identifier.BOMRef, + PURL: springPackage.Identifier.PURL, + } // Add the spring component to match vuln1's BOM-Ref springComponent := &core.Component{ Type: core.TypeLibrary, Name: springPackage.Identifier.PURL.Name, Group: springPackage.Identifier.PURL.Namespace, Version: springPackage.Version, - PkgIdentifier: springPackage.Identifier, + PkgIdentifier: pkgIdentifier, } bom.AddComponent(springComponent) return bom