mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-24 03:58:12 -08:00
324 lines
9.5 KiB
Go
324 lines
9.5 KiB
Go
package cyclonedx
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
cdx "github.com/CycloneDX/cyclonedx-go"
|
|
"github.com/samber/lo"
|
|
"golang.org/x/xerrors"
|
|
|
|
"github.com/aquasecurity/trivy/pkg/digest"
|
|
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
|
|
"github.com/aquasecurity/trivy/pkg/purl"
|
|
"github.com/aquasecurity/trivy/pkg/sbom/cyclonedx/core"
|
|
"github.com/aquasecurity/trivy/pkg/scanner/utils"
|
|
"github.com/aquasecurity/trivy/pkg/types"
|
|
)
|
|
|
|
const (
|
|
PropertySchemaVersion = "SchemaVersion"
|
|
PropertyType = "Type"
|
|
PropertyClass = "Class"
|
|
|
|
// Image properties
|
|
PropertySize = "Size"
|
|
PropertyImageID = "ImageID"
|
|
PropertyRepoDigest = "RepoDigest"
|
|
PropertyDiffID = "DiffID"
|
|
PropertyRepoTag = "RepoTag"
|
|
|
|
// Package properties
|
|
PropertyPkgID = "PkgID"
|
|
PropertyPkgType = "PkgType"
|
|
PropertySrcName = "SrcName"
|
|
PropertySrcVersion = "SrcVersion"
|
|
PropertySrcRelease = "SrcRelease"
|
|
PropertySrcEpoch = "SrcEpoch"
|
|
PropertyModularitylabel = "Modularitylabel"
|
|
PropertyFilePath = "FilePath"
|
|
PropertyLayerDigest = "LayerDigest"
|
|
PropertyLayerDiffID = "LayerDiffID"
|
|
)
|
|
|
|
var (
|
|
ErrInvalidBOMLink = xerrors.New("invalid bomLink format error")
|
|
)
|
|
|
|
type Marshaler struct {
|
|
core *core.CycloneDX
|
|
}
|
|
|
|
func NewMarshaler(version string, opts ...core.Option) *Marshaler {
|
|
return &Marshaler{
|
|
core: core.NewCycloneDX(version, opts...),
|
|
}
|
|
}
|
|
|
|
// Marshal converts the Trivy report to the CycloneDX format
|
|
func (e *Marshaler) Marshal(report types.Report) (*cdx.BOM, error) {
|
|
// Convert
|
|
root, err := e.MarshalReport(report)
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("failed to marshal report: %w", err)
|
|
}
|
|
|
|
return e.core.Marshal(root), nil
|
|
}
|
|
|
|
func (e *Marshaler) MarshalReport(r types.Report) (*core.Component, error) {
|
|
// Metadata component
|
|
root, err := e.rootComponent(r)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, result := range r.Results {
|
|
components, err := e.marshalResult(r.Metadata, result)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
root.Components = append(root.Components, components...)
|
|
}
|
|
return root, nil
|
|
}
|
|
|
|
func (e *Marshaler) marshalResult(metadata types.Metadata, result types.Result) ([]*core.Component, error) {
|
|
if result.Type == ftypes.NodePkg || result.Type == ftypes.PythonPkg ||
|
|
result.Type == ftypes.GemSpec || result.Type == ftypes.Jar || result.Type == ftypes.CondaPkg {
|
|
// If a package is language-specific package that isn't associated with a lock file,
|
|
// it will be a dependency of a component under "metadata".
|
|
// e.g.
|
|
// Container component (alpine:3.15) ----------------------- #1
|
|
// -> Library component (npm package, express-4.17.3) ---- #2
|
|
// -> Library component (python package, django-4.0.2) --- #2
|
|
// -> etc.
|
|
// ref. https://cyclonedx.org/use-cases/#inventory
|
|
|
|
// Dependency graph from #1 to #2
|
|
components, err := e.marshalPackages(metadata, result)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return components, nil
|
|
} else if result.Class == types.ClassOSPkg || result.Class == types.ClassLangPkg {
|
|
// If a package is OS package, it will be a dependency of "Operating System" component.
|
|
// e.g.
|
|
// Container component (alpine:3.15) --------------------- #1
|
|
// -> Operating System Component (Alpine Linux 3.15) --- #2
|
|
// -> Library component (bash-4.12) ------------------ #3
|
|
// -> Library component (vim-8.2) ------------------ #3
|
|
// -> etc.
|
|
//
|
|
// Else if a package is language-specific package associated with a lock file,
|
|
// it will be a dependency of "Application" component.
|
|
// e.g.
|
|
// Container component (alpine:3.15) ------------------------ #1
|
|
// -> Application component (/app/package-lock.json) ------ #2
|
|
// -> Library component (npm package, express-4.17.3) --- #3
|
|
// -> Library component (npm package, lodash-4.17.21) --- #3
|
|
// -> etc.
|
|
|
|
// #2
|
|
appComponent := e.resultComponent(result, metadata.OS)
|
|
|
|
// #3
|
|
components, err := e.marshalPackages(metadata, result)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Dependency graph from #2 to #3
|
|
appComponent.Components = components
|
|
|
|
// Dependency graph from #1 to #2
|
|
return []*core.Component{appComponent}, nil
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func (e *Marshaler) marshalPackages(metadata types.Metadata, result types.Result) ([]*core.Component, error) {
|
|
// Get dependency parents first
|
|
parents := ftypes.Packages(result.Packages).ParentDeps()
|
|
|
|
// Group vulnerabilities by package ID
|
|
vulns := lo.GroupBy(result.Vulnerabilities, func(v types.DetectedVulnerability) string {
|
|
return lo.Ternary(v.PkgID == "", fmt.Sprintf("%s@%s", v.PkgName, v.InstalledVersion), v.PkgID)
|
|
})
|
|
|
|
// Create package map
|
|
pkgs := lo.SliceToMap(result.Packages, func(pkg ftypes.Package) (string, Package) {
|
|
pkgID := lo.Ternary(pkg.ID == "", fmt.Sprintf("%s@%s", pkg.Name, utils.FormatVersion(pkg)), pkg.ID)
|
|
return pkgID, Package{
|
|
Type: result.Type,
|
|
Metadata: metadata,
|
|
Package: pkg,
|
|
Vulnerabilities: vulns[pkgID],
|
|
}
|
|
})
|
|
|
|
var directComponents []*core.Component
|
|
for _, pkg := range pkgs {
|
|
// Skip indirect dependencies
|
|
if pkg.Indirect && len(parents[pkg.ID]) != 0 {
|
|
continue
|
|
}
|
|
|
|
// Recursive packages from direct dependencies
|
|
if component, err := e.marshalPackage(pkg, pkgs, map[string]*core.Component{}); err != nil {
|
|
return nil, nil
|
|
} else if component != nil {
|
|
directComponents = append(directComponents, component)
|
|
}
|
|
}
|
|
|
|
return directComponents, nil
|
|
}
|
|
|
|
type Package struct {
|
|
ftypes.Package
|
|
Type string
|
|
Metadata types.Metadata
|
|
Vulnerabilities []types.DetectedVulnerability
|
|
}
|
|
|
|
func (e *Marshaler) marshalPackage(pkg Package, pkgs map[string]Package, components map[string]*core.Component,
|
|
) (*core.Component, error) {
|
|
if c, ok := components[pkg.ID]; ok {
|
|
return c, nil
|
|
}
|
|
|
|
component, err := pkgComponent(pkg)
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("failed to parse pkg: %w", err)
|
|
}
|
|
components[pkg.ID] = component
|
|
|
|
// Iterate dependencies
|
|
for _, dep := range pkg.DependsOn {
|
|
childPkg, ok := pkgs[dep]
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
child, err := e.marshalPackage(childPkg, pkgs, components)
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("failed to parse pkg: %w", err)
|
|
}
|
|
component.Components = append(component.Components, child)
|
|
}
|
|
return component, nil
|
|
}
|
|
|
|
func (e *Marshaler) rootComponent(r types.Report) (*core.Component, error) {
|
|
root := &core.Component{
|
|
Name: r.ArtifactName,
|
|
}
|
|
|
|
props := map[string]string{
|
|
PropertySchemaVersion: strconv.Itoa(r.SchemaVersion),
|
|
}
|
|
|
|
switch r.ArtifactType {
|
|
case ftypes.ArtifactContainerImage:
|
|
root.Type = cdx.ComponentTypeContainer
|
|
props[PropertyImageID] = r.Metadata.ImageID
|
|
|
|
p, err := purl.NewPackageURL(purl.TypeOCI, r.Metadata, ftypes.Package{})
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("failed to new package url for oci: %w", err)
|
|
} else if p.Type != "" {
|
|
root.PackageURL = &p
|
|
}
|
|
|
|
case ftypes.ArtifactVM:
|
|
root.Type = cdx.ComponentTypeContainer
|
|
case ftypes.ArtifactFilesystem, ftypes.ArtifactRemoteRepository:
|
|
root.Type = cdx.ComponentTypeApplication
|
|
}
|
|
|
|
if r.Metadata.Size != 0 {
|
|
props[PropertySize] = strconv.FormatInt(r.Metadata.Size, 10)
|
|
}
|
|
|
|
if len(r.Metadata.RepoDigests) > 0 {
|
|
props[PropertyRepoDigest] = strings.Join(r.Metadata.RepoDigests, ",")
|
|
}
|
|
if len(r.Metadata.DiffIDs) > 0 {
|
|
props[PropertyDiffID] = strings.Join(r.Metadata.DiffIDs, ",")
|
|
}
|
|
if len(r.Metadata.RepoTags) > 0 {
|
|
props[PropertyRepoTag] = strings.Join(r.Metadata.RepoTags, ",")
|
|
}
|
|
|
|
root.Properties = filterProperties(props)
|
|
|
|
return root, nil
|
|
}
|
|
|
|
func (e *Marshaler) resultComponent(r types.Result, osFound *ftypes.OS) *core.Component {
|
|
component := &core.Component{
|
|
Name: r.Target,
|
|
Properties: map[string]string{
|
|
PropertyType: r.Type,
|
|
PropertyClass: string(r.Class),
|
|
},
|
|
}
|
|
|
|
switch r.Class {
|
|
case types.ClassOSPkg:
|
|
// UUID needs to be generated since Operating System Component cannot generate PURL.
|
|
// https://cyclonedx.org/use-cases/#known-vulnerabilities
|
|
if osFound != nil {
|
|
component.Name = osFound.Family
|
|
component.Version = osFound.Name
|
|
}
|
|
component.Type = cdx.ComponentTypeOS
|
|
case types.ClassLangPkg:
|
|
// UUID needs to be generated since Application Component cannot generate PURL.
|
|
// https://cyclonedx.org/use-cases/#known-vulnerabilities
|
|
component.Type = cdx.ComponentTypeApplication
|
|
}
|
|
|
|
return component
|
|
}
|
|
|
|
func pkgComponent(pkg Package) (*core.Component, error) {
|
|
pu, err := purl.NewPackageURL(pkg.Type, pkg.Metadata, pkg.Package)
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("failed to new package purl: %w", err)
|
|
}
|
|
|
|
properties := map[string]string{
|
|
PropertyPkgID: pkg.ID,
|
|
PropertyPkgType: pkg.Type,
|
|
PropertyFilePath: pkg.FilePath,
|
|
PropertySrcName: pkg.SrcName,
|
|
PropertySrcVersion: pkg.SrcVersion,
|
|
PropertySrcRelease: pkg.SrcRelease,
|
|
PropertySrcEpoch: strconv.Itoa(pkg.SrcEpoch),
|
|
PropertyModularitylabel: pkg.Modularitylabel,
|
|
PropertyLayerDigest: pkg.Layer.Digest,
|
|
PropertyLayerDiffID: pkg.Layer.DiffID,
|
|
}
|
|
|
|
return &core.Component{
|
|
Type: cdx.ComponentTypeLibrary,
|
|
Name: pkg.Name,
|
|
Version: pu.Version,
|
|
PackageURL: &pu,
|
|
Supplier: pkg.Maintainer,
|
|
Licenses: pkg.Licenses,
|
|
Hashes: lo.Ternary(pkg.Digest == "", nil, []digest.Digest{pkg.Digest}),
|
|
Properties: filterProperties(properties),
|
|
Vulnerabilities: pkg.Vulnerabilities,
|
|
}, nil
|
|
}
|
|
|
|
func filterProperties(props map[string]string) map[string]string {
|
|
return lo.OmitBy(props, func(key string, value string) bool {
|
|
return value == "" || (key == PropertySrcEpoch && value == "0")
|
|
})
|
|
}
|