refactor(cyclonedx): implement json.Unmarshaler (#2662)

* refactor(cyclonedx): implement json.Unmarshaler

* fix: use pointer
This commit is contained in:
Teppei Fukuda
2022-08-04 14:15:33 +03:00
committed by GitHub
parent df0b5e40db
commit e848e6d009
5 changed files with 84 additions and 82 deletions

View File

@@ -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)
}

View File

@@ -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{

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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) {