mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-12 07:40:48 -08:00
refactor(cyclonedx): implement json.Unmarshaler (#2662)
* refactor(cyclonedx): implement json.Unmarshaler * fix: use pointer
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user