Skip to content

Commit

Permalink
refactor(cyclonedx): implement json.Unmarshaler (#2662)
Browse files Browse the repository at this point in the history
* refactor(cyclonedx): implement json.Unmarshaler

* fix: use pointer
  • Loading branch information
knqyf263 authored Aug 4, 2022
1 parent df0b5e4 commit e848e6d
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 83 deletions.
37 changes: 27 additions & 10 deletions pkg/fanal/artifact/sbom/sbom.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/sbom/cyclonedx/marshal.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
109 changes: 48 additions & 61 deletions pkg/sbom/cyclonedx/unmarshal.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package cyclonedx

import (
"io"
"bytes"
"sort"
"strconv"
"strings"
Expand All @@ -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)

var (
osInfo *ftypes.OS
apps []ftypes.Application
pkgInfos []ftypes.PackageInfo
seen = make(map[string]struct{})
)
for bomRef := range u.dependencies {
component := u.components[bomRef]
c.dependencies = dependencyMap(bom.Dependencies)
c.components = componentMap(bom.Metadata, bom.Components)

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
Expand All @@ -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
}
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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
})
Expand Down Expand Up @@ -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
}
Expand All @@ -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
}
Expand Down
6 changes: 4 additions & 2 deletions pkg/sbom/cyclonedx/unmarshal_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cyclonedx_test

import (
"encoding/json"
"os"
"testing"

Expand Down Expand Up @@ -196,15 +197,16 @@ 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)
return
}

// Not compare the CycloneDX field
got := *cdx.SBOM
got.CycloneDX = nil

require.NoError(t, err)
Expand Down
14 changes: 5 additions & 9 deletions pkg/sbom/sbom.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down

0 comments on commit e848e6d

Please sign in to comment.