From e848e6d009af45cda073133c7c8d3220a082335c Mon Sep 17 00:00:00 2001 From: Teppei Fukuda Date: Thu, 4 Aug 2022 14:15:33 +0300 Subject: [PATCH] refactor(cyclonedx): implement json.Unmarshaler (#2662) * refactor(cyclonedx): implement json.Unmarshaler * fix: use pointer --- pkg/fanal/artifact/sbom/sbom.go | 37 ++++++--- pkg/sbom/cyclonedx/marshal.go | 2 +- pkg/sbom/cyclonedx/unmarshal.go | 107 ++++++++++++--------------- pkg/sbom/cyclonedx/unmarshal_test.go | 6 +- pkg/sbom/sbom.go | 14 ++-- 5 files changed, 84 insertions(+), 82 deletions(-) diff --git a/pkg/fanal/artifact/sbom/sbom.go b/pkg/fanal/artifact/sbom/sbom.go index 3356dbee0f..c0a418a76f 100644 --- a/pkg/fanal/artifact/sbom/sbom.go +++ b/pkg/fanal/artifact/sbom/sbom.go @@ -59,18 +59,11 @@ func (a Artifact) Inspect(_ context.Context) (types.ArtifactReference, error) { return types.ArtifactReference{}, xerrors.Errorf("seek error: %w", err) } - var unmarshaler sbom.Unmarshaler - switch format { - case sbom.FormatCycloneDXJSON: - unmarshaler = cyclonedx.NewJSONUnmarshaler() - default: - return types.ArtifactReference{}, xerrors.Errorf("%s scanning is not yet supported", format) - - } - bom, err := unmarshaler.Unmarshal(f) + bom, err := a.Decode(f, format) if err != nil { - return types.ArtifactReference{}, xerrors.Errorf("failed to unmarshal: %w", err) + return types.ArtifactReference{}, xerrors.Errorf("SBOM decode error: %w", err) } + blobInfo := types.BlobInfo{ SchemaVersion: types.BlobJSONSchemaVersion, OS: bom.OS, @@ -104,6 +97,30 @@ func (a Artifact) Inspect(_ context.Context) (types.ArtifactReference, error) { }, nil } +func (a Artifact) Decode(f io.Reader, format sbom.Format) (sbom.SBOM, error) { + var ( + v interface{} + bom sbom.SBOM + decoder interface{ Decode(any) error } + ) + + switch format { + case sbom.FormatCycloneDXJSON: + v = &cyclonedx.CycloneDX{SBOM: &bom} + decoder = json.NewDecoder(f) + default: + return sbom.SBOM{}, xerrors.Errorf("%s scanning is not yet supported", format) + + } + + // Decode a file content into sbom.SBOM + if err := decoder.Decode(v); err != nil { + return sbom.SBOM{}, xerrors.Errorf("failed to decode: %w", err) + } + + return bom, nil +} + func (a Artifact) Clean(reference types.ArtifactReference) error { return a.cache.DeleteBlobs(reference.BlobIDs) } diff --git a/pkg/sbom/cyclonedx/marshal.go b/pkg/sbom/cyclonedx/marshal.go index 9a24a9c623..b04d5cab62 100644 --- a/pkg/sbom/cyclonedx/marshal.go +++ b/pkg/sbom/cyclonedx/marshal.go @@ -367,7 +367,7 @@ func (e *Marshaler) reportToCdxComponent(r types.Report) (*cdx.Component, error) return component, nil } -func (e Marshaler) resultToCdxComponent(r types.Result, osFound *ftypes.OS) cdx.Component { +func (e *Marshaler) resultToCdxComponent(r types.Result, osFound *ftypes.OS) cdx.Component { component := cdx.Component{ Name: r.Target, Properties: &[]cdx.Property{ diff --git a/pkg/sbom/cyclonedx/unmarshal.go b/pkg/sbom/cyclonedx/unmarshal.go index a03971a7c6..b36f884381 100644 --- a/pkg/sbom/cyclonedx/unmarshal.go +++ b/pkg/sbom/cyclonedx/unmarshal.go @@ -1,7 +1,7 @@ package cyclonedx import ( - "io" + "bytes" "sort" "strconv" "strings" @@ -15,54 +15,46 @@ import ( "github.com/aquasecurity/trivy/pkg/sbom" ) -type Unmarshaler struct { - format cdx.BOMFileFormat +type CycloneDX struct { + *sbom.SBOM dependencies map[string][]string components map[string]cdx.Component } -func NewJSONUnmarshaler() sbom.Unmarshaler { - return &Unmarshaler{ - format: cdx.BOMFileFormatJSON, +func (c *CycloneDX) UnmarshalJSON(b []byte) error { + if c.SBOM == nil { + c.SBOM = &sbom.SBOM{} } -} - -func (u *Unmarshaler) Unmarshal(r io.Reader) (sbom.SBOM, error) { bom := cdx.NewBOM() - decoder := cdx.NewBOMDecoder(r, u.format) + decoder := cdx.NewBOMDecoder(bytes.NewReader(b), cdx.BOMFileFormatJSON) if err := decoder.Decode(bom); err != nil { - return sbom.SBOM{}, xerrors.Errorf("CycloneDX decode error: %w", err) + return xerrors.Errorf("CycloneDX decode error: %w", err) } - u.dependencies = dependencyMap(bom.Dependencies) - u.components = componentMap(bom.Metadata, bom.Components) + c.dependencies = dependencyMap(bom.Dependencies) + c.components = componentMap(bom.Metadata, bom.Components) - var ( - osInfo *ftypes.OS - apps []ftypes.Application - pkgInfos []ftypes.PackageInfo - seen = make(map[string]struct{}) - ) - for bomRef := range u.dependencies { - component := u.components[bomRef] + var seen = make(map[string]struct{}) + for bomRef := range c.dependencies { + component := c.components[bomRef] switch component.Type { case cdx.ComponentTypeOS: // OS info and OS packages - osInfo = toOS(component) - pkgInfo, err := u.parseOSPkgs(component, seen) + c.OS = toOS(component) + pkgInfo, err := c.parseOSPkgs(component, seen) if err != nil { - return sbom.SBOM{}, xerrors.Errorf("failed to parse os packages: %w", err) + return xerrors.Errorf("failed to parse os packages: %w", err) } - pkgInfos = append(pkgInfos, pkgInfo) + c.Packages = append(c.Packages, pkgInfo) case cdx.ComponentTypeApplication: // It would be a lock file in a CycloneDX report generated by Trivy if lookupProperty(component.Properties, PropertyType) == "" { continue } - app, err := u.parseLangPkgs(component, seen) + app, err := c.parseLangPkgs(component, seen) if err != nil { - return sbom.SBOM{}, xerrors.Errorf("failed to parse language packages: %w", err) + return xerrors.Errorf("failed to parse language packages: %w", err) } - apps = append(apps, *app) + c.Applications = append(c.Applications, *app) case cdx.ComponentTypeLibrary: // It is an individual package not associated with any lock files and should be processed later. // e.g. .gemspec, .egg and .wheel @@ -71,7 +63,7 @@ func (u *Unmarshaler) Unmarshal(r io.Reader) (sbom.SBOM, error) { } var libComponents []cdx.Component - for ref, component := range u.components { + for ref, component := range c.components { if _, ok := seen[ref]; ok { continue } @@ -82,15 +74,15 @@ func (u *Unmarshaler) Unmarshal(r io.Reader) (sbom.SBOM, error) { aggregatedApps, err := aggregateLangPkgs(libComponents) if err != nil { - return sbom.SBOM{}, xerrors.Errorf("failed to aggregate packages: %w", err) + return xerrors.Errorf("failed to aggregate packages: %w", err) } - apps = append(apps, aggregatedApps...) + c.Applications = append(c.Applications, aggregatedApps...) - sort.Slice(apps, func(i, j int) bool { - if apps[i].Type != apps[j].Type { - return apps[i].Type < apps[j].Type + sort.Slice(c.Applications, func(i, j int) bool { + if c.Applications[i].Type != c.Applications[j].Type { + return c.Applications[i].Type < c.Applications[j].Type } - return apps[i].FilePath < apps[j].FilePath + return c.Applications[i].FilePath < c.Applications[j].FilePath }) var metadata ftypes.Metadata @@ -102,29 +94,24 @@ func (u *Unmarshaler) Unmarshal(r io.Reader) (sbom.SBOM, error) { } var components []ftypes.Component - for _, c := range lo.FromPtr(bom.Components) { - components = append(components, toTrivyCdxComponent(c)) + for _, component := range lo.FromPtr(bom.Components) { + components = append(components, toTrivyCdxComponent(component)) } - return sbom.SBOM{ - OS: osInfo, - Packages: pkgInfos, - Applications: apps, - - // Keep the original SBOM - CycloneDX: &ftypes.CycloneDX{ - BOMFormat: bom.BOMFormat, - SpecVersion: bom.SpecVersion, - SerialNumber: bom.SerialNumber, - Version: bom.Version, - Metadata: metadata, - Components: components, - }, - }, nil + // Keep the original SBOM + c.CycloneDX = &ftypes.CycloneDX{ + BOMFormat: bom.BOMFormat, + SpecVersion: bom.SpecVersion, + SerialNumber: bom.SerialNumber, + Version: bom.Version, + Metadata: metadata, + Components: components, + } + return nil } -func (u *Unmarshaler) parseOSPkgs(component cdx.Component, seen map[string]struct{}) (ftypes.PackageInfo, error) { - components := u.walkDependencies(component.BOMRef) +func (c *CycloneDX) parseOSPkgs(component cdx.Component, seen map[string]struct{}) (ftypes.PackageInfo, error) { + components := c.walkDependencies(component.BOMRef) pkgs, err := parsePkgs(components, seen) if err != nil { return ftypes.PackageInfo{}, xerrors.Errorf("failed to parse os package: %w", err) @@ -135,8 +122,8 @@ func (u *Unmarshaler) parseOSPkgs(component cdx.Component, seen map[string]struc }, nil } -func (u *Unmarshaler) parseLangPkgs(component cdx.Component, seen map[string]struct{}) (*ftypes.Application, error) { - components := u.walkDependencies(component.BOMRef) +func (c *CycloneDX) parseLangPkgs(component cdx.Component, seen map[string]struct{}) (*ftypes.Application, error) { + components := c.walkDependencies(component.BOMRef) components = lo.UniqBy(components, func(c cdx.Component) string { return c.BOMRef }) @@ -175,10 +162,10 @@ func parsePkgs(components []cdx.Component, seen map[string]struct{}) ([]ftypes.P // - type: Application 3 // - type: Library D // - type: Library E -func (u *Unmarshaler) walkDependencies(rootRef string) []cdx.Component { +func (c *CycloneDX) walkDependencies(rootRef string) []cdx.Component { var components []cdx.Component - for _, dep := range u.dependencies[rootRef] { - component, ok := u.components[dep] + for _, dep := range c.dependencies[rootRef] { + component, ok := c.components[dep] if !ok { continue } @@ -188,7 +175,7 @@ func (u *Unmarshaler) walkDependencies(rootRef string) []cdx.Component { components = append(components, component) } - components = append(components, u.walkDependencies(dep)...) + components = append(components, c.walkDependencies(dep)...) } return components } diff --git a/pkg/sbom/cyclonedx/unmarshal_test.go b/pkg/sbom/cyclonedx/unmarshal_test.go index 30bf355890..c59a0d88fe 100644 --- a/pkg/sbom/cyclonedx/unmarshal_test.go +++ b/pkg/sbom/cyclonedx/unmarshal_test.go @@ -1,6 +1,7 @@ package cyclonedx_test import ( + "encoding/json" "os" "testing" @@ -196,8 +197,8 @@ func TestUnmarshaler_Unmarshal(t *testing.T) { require.NoError(t, err) defer f.Close() - unmarshaler := cyclonedx.NewJSONUnmarshaler() - got, err := unmarshaler.Unmarshal(f) + var cdx cyclonedx.CycloneDX + err = json.NewDecoder(f).Decode(&cdx) if tt.wantErr != "" { require.Error(t, err) assert.Contains(t, err.Error(), tt.wantErr) @@ -205,6 +206,7 @@ func TestUnmarshaler_Unmarshal(t *testing.T) { } // Not compare the CycloneDX field + got := *cdx.SBOM got.CycloneDX = nil require.NoError(t, err) diff --git a/pkg/sbom/sbom.go b/pkg/sbom/sbom.go index 928f1623ed..dfe3c99fb7 100644 --- a/pkg/sbom/sbom.go +++ b/pkg/sbom/sbom.go @@ -19,18 +19,14 @@ type SBOM struct { CycloneDX *types.CycloneDX } -type Unmarshaler interface { - Unmarshal(io.Reader) (SBOM, error) -} - type Format string const ( - FormatCycloneDXJSON = "cyclonedx-json" - FormatCycloneDXXML = "cyclonedx-xml" - FormatSPDXJSON = "spdx-json" - FormatSPDXXML = "spdx-xml" - FormatUnknown = "unknown" + FormatCycloneDXJSON Format = "cyclonedx-json" + FormatCycloneDXXML Format = "cyclonedx-xml" + FormatSPDXJSON Format = "spdx-json" + FormatSPDXXML Format = "spdx-xml" + FormatUnknown Format = "unknown" ) func DetectFormat(r io.ReadSeeker) (Format, error) {