Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(cyclonedx): implement json.Unmarshaler #2662

Merged
merged 2 commits into from
Aug 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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