mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-23 15:37:50 -08:00
394 lines
10 KiB
Go
394 lines
10 KiB
Go
package io
|
|
|
|
import (
|
|
"errors"
|
|
"slices"
|
|
"sort"
|
|
"strconv"
|
|
"sync"
|
|
|
|
debver "github.com/knqyf263/go-deb-version"
|
|
rpmver "github.com/knqyf263/go-rpm-version"
|
|
"github.com/package-url/packageurl-go"
|
|
"github.com/samber/lo"
|
|
"golang.org/x/xerrors"
|
|
|
|
"github.com/aquasecurity/trivy/pkg/dependency"
|
|
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
|
|
"github.com/aquasecurity/trivy/pkg/log"
|
|
"github.com/aquasecurity/trivy/pkg/purl"
|
|
"github.com/aquasecurity/trivy/pkg/sbom/core"
|
|
"github.com/aquasecurity/trivy/pkg/types"
|
|
"github.com/aquasecurity/trivy/pkg/uuid"
|
|
)
|
|
|
|
var (
|
|
ErrPURLEmpty = errors.New("purl empty error")
|
|
ErrUnsupportedType = errors.New("unsupported type")
|
|
)
|
|
|
|
type Decoder struct {
|
|
bom *core.BOM
|
|
|
|
osID uuid.UUID
|
|
pkgs map[uuid.UUID]*ftypes.Package
|
|
apps map[uuid.UUID]*ftypes.Application
|
|
|
|
logger *log.Logger
|
|
}
|
|
|
|
func NewDecoder(bom *core.BOM) *Decoder {
|
|
return &Decoder{
|
|
bom: bom,
|
|
pkgs: make(map[uuid.UUID]*ftypes.Package),
|
|
apps: make(map[uuid.UUID]*ftypes.Application),
|
|
logger: log.WithPrefix("sbom"),
|
|
}
|
|
}
|
|
|
|
func (m *Decoder) Decode(sbom *types.SBOM) error {
|
|
// Parse the root component
|
|
if err := m.decodeRoot(sbom); err != nil {
|
|
return xerrors.Errorf("failed to decode root component: %w", err)
|
|
}
|
|
|
|
// Parse all components
|
|
if err := m.decodeComponents(sbom); err != nil {
|
|
return xerrors.Errorf("failed to decode components: %w", err)
|
|
}
|
|
|
|
// Build dependency graph between packages
|
|
m.buildDependencyGraph()
|
|
|
|
// Add OS packages
|
|
m.addOSPkgs(sbom)
|
|
|
|
// Add language-specific packages
|
|
m.addLangPkgs(sbom)
|
|
|
|
// Add remaining packages
|
|
if err := m.addOrphanPkgs(sbom); err != nil {
|
|
return xerrors.Errorf("failed to aggregate packages: %w", err)
|
|
}
|
|
|
|
sort.Slice(sbom.Applications, func(i, j int) bool {
|
|
if sbom.Applications[i].Type != sbom.Applications[j].Type {
|
|
return sbom.Applications[i].Type < sbom.Applications[j].Type
|
|
}
|
|
return sbom.Applications[i].FilePath < sbom.Applications[j].FilePath
|
|
})
|
|
|
|
sbom.BOM = m.bom
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *Decoder) decodeRoot(s *types.SBOM) error {
|
|
root := m.bom.Root()
|
|
if root == nil {
|
|
return nil // No root found
|
|
}
|
|
|
|
var err error
|
|
for _, prop := range root.Properties {
|
|
switch prop.Name {
|
|
case core.PropertyImageID:
|
|
s.Metadata.ImageID = prop.Value
|
|
case core.PropertySize:
|
|
if s.Metadata.Size, err = strconv.ParseInt(prop.Value, 10, 64); err != nil {
|
|
return xerrors.Errorf("failed to convert size: %w", err)
|
|
}
|
|
case core.PropertyRepoDigest:
|
|
s.Metadata.RepoDigests = append(s.Metadata.RepoDigests, prop.Value)
|
|
case core.PropertyDiffID:
|
|
s.Metadata.DiffIDs = append(s.Metadata.DiffIDs, prop.Value)
|
|
case core.PropertyRepoTag:
|
|
s.Metadata.RepoTags = append(s.Metadata.RepoTags, prop.Value)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *Decoder) decodeComponents(sbom *types.SBOM) error {
|
|
onceMultiOSWarn := sync.OnceFunc(func() {
|
|
m.logger.Warn("Multiple OS components are not supported, taking the first one and ignoring the rest")
|
|
})
|
|
|
|
for id, c := range m.bom.Components() {
|
|
switch c.Type {
|
|
case core.TypeOS:
|
|
if m.osID != uuid.Nil {
|
|
onceMultiOSWarn()
|
|
continue
|
|
}
|
|
m.osID = id
|
|
sbom.Metadata.OS = &ftypes.OS{
|
|
Family: ftypes.OSType(c.Name),
|
|
Name: c.Version,
|
|
}
|
|
continue
|
|
case core.TypeApplication:
|
|
if app := m.decodeApplication(c); app.Type != "" {
|
|
m.apps[id] = app
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Third-party SBOMs may contain packages in types other than "Library"
|
|
if c.Type == core.TypeLibrary || c.PkgIdentifier.PURL != nil {
|
|
pkg, err := m.decodeLibrary(c)
|
|
if errors.Is(err, ErrUnsupportedType) || errors.Is(err, ErrPURLEmpty) {
|
|
continue
|
|
} else if err != nil {
|
|
return xerrors.Errorf("failed to decode library: %w", err)
|
|
}
|
|
m.pkgs[id] = pkg
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// buildDependencyGraph builds a dependency graph between packages
|
|
func (m *Decoder) buildDependencyGraph() {
|
|
for id, rels := range m.bom.Relationships() {
|
|
pkg, ok := m.pkgs[id]
|
|
if !ok {
|
|
continue
|
|
}
|
|
for _, rel := range rels {
|
|
dep, ok := m.pkgs[rel.Dependency]
|
|
if !ok {
|
|
continue
|
|
}
|
|
pkg.DependsOn = append(pkg.DependsOn, dep.ID)
|
|
}
|
|
continue
|
|
}
|
|
}
|
|
|
|
func (m *Decoder) decodeApplication(c *core.Component) *ftypes.Application {
|
|
var app ftypes.Application
|
|
for _, prop := range c.Properties {
|
|
if prop.Name == core.PropertyType {
|
|
app.Type = ftypes.LangType(prop.Value)
|
|
}
|
|
}
|
|
|
|
// Aggregation Types use the name of the language (e.g. `Java`, `Python`, etc.) as the component name.
|
|
// Other language files use the file path as their name.
|
|
if !slices.Contains(ftypes.AggregatingTypes, app.Type) {
|
|
app.FilePath = c.Name
|
|
}
|
|
return &app
|
|
}
|
|
|
|
func (m *Decoder) decodeLibrary(c *core.Component) (*ftypes.Package, error) {
|
|
p := (*purl.PackageURL)(c.PkgIdentifier.PURL)
|
|
if p == nil {
|
|
log.Debug("Skipping a component without PURL",
|
|
log.String("name", c.Name), log.String("version", c.Version))
|
|
return nil, ErrPURLEmpty
|
|
}
|
|
|
|
pkg := p.Package()
|
|
if p.Class() == types.ClassUnknown {
|
|
log.Debug("Skipping a component with an unsupported type",
|
|
log.String("name", c.Name), log.String("version", c.Version), log.String("type", p.Type))
|
|
return nil, ErrUnsupportedType
|
|
}
|
|
pkg.Name = m.pkgName(pkg, c)
|
|
pkg.ID = dependency.ID(p.LangType(), pkg.Name, p.Version) // Re-generate ID with the updated name
|
|
|
|
var err error
|
|
for _, prop := range c.Properties {
|
|
switch prop.Name {
|
|
case core.PropertyPkgID:
|
|
pkg.ID = prop.Value
|
|
case core.PropertyFilePath:
|
|
pkg.FilePath = prop.Value
|
|
case core.PropertySrcName:
|
|
pkg.SrcName = prop.Value
|
|
case core.PropertySrcVersion:
|
|
pkg.SrcVersion = prop.Value
|
|
case core.PropertySrcRelease:
|
|
pkg.SrcRelease = prop.Value
|
|
case core.PropertySrcEpoch:
|
|
if pkg.SrcEpoch, err = strconv.Atoi(prop.Value); err != nil {
|
|
return nil, xerrors.Errorf("invalid src epoch: %w", err)
|
|
}
|
|
case core.PropertyModularitylabel:
|
|
pkg.Modularitylabel = prop.Value
|
|
case core.PropertyLayerDigest:
|
|
pkg.Layer.Digest = prop.Value
|
|
case core.PropertyLayerDiffID:
|
|
pkg.Layer.DiffID = prop.Value
|
|
}
|
|
}
|
|
|
|
pkg.Identifier.BOMRef = c.PkgIdentifier.BOMRef
|
|
pkg.Licenses = c.Licenses
|
|
|
|
for _, f := range c.Files {
|
|
if f.Path != "" && pkg.FilePath == "" {
|
|
pkg.FilePath = f.Path
|
|
}
|
|
// An empty path represents a package digest
|
|
if f.Path == "" && len(f.Digests) > 0 {
|
|
pkg.Digest = f.Digests[0]
|
|
}
|
|
}
|
|
|
|
if p.Class() == types.ClassOSPkg {
|
|
m.fillSrcPkg(c, pkg)
|
|
}
|
|
|
|
return pkg, nil
|
|
}
|
|
|
|
// pkgName returns the package name.
|
|
// PURL loses case-sensitivity (e.g. Go, Npm, PyPI), so we have to use an original package name.
|
|
func (m *Decoder) pkgName(pkg *ftypes.Package, c *core.Component) string {
|
|
p := c.PkgIdentifier.PURL
|
|
|
|
// A name from PURL takes precedence for CocoaPods since it has subpath.
|
|
if c.PkgIdentifier.PURL.Type == packageurl.TypeCocoapods {
|
|
return pkg.Name
|
|
}
|
|
|
|
if c.Group != "" {
|
|
if p.Type == packageurl.TypeMaven || p.Type == packageurl.TypeGradle {
|
|
return c.Group + ":" + c.Name
|
|
}
|
|
return c.Group + "/" + c.Name
|
|
}
|
|
return c.Name
|
|
}
|
|
|
|
func (m *Decoder) fillSrcPkg(c *core.Component, pkg *ftypes.Package) {
|
|
if c.SrcName != "" && pkg.SrcName == "" {
|
|
pkg.SrcName = c.SrcName
|
|
}
|
|
m.parseSrcVersion(pkg, c.SrcVersion)
|
|
|
|
// Source info was added from component or properties
|
|
if pkg.SrcName != "" && pkg.SrcVersion != "" {
|
|
return
|
|
}
|
|
|
|
// Fill source package information for components in third-party SBOMs .
|
|
if pkg.SrcName == "" {
|
|
pkg.SrcName = pkg.Name
|
|
}
|
|
if pkg.SrcVersion == "" {
|
|
pkg.SrcVersion = pkg.Version
|
|
}
|
|
if pkg.SrcRelease == "" {
|
|
pkg.SrcRelease = pkg.Release
|
|
}
|
|
if pkg.SrcEpoch == 0 {
|
|
pkg.SrcEpoch = pkg.Epoch
|
|
}
|
|
}
|
|
|
|
// parseSrcVersion parses the version of the source package.
|
|
func (m *Decoder) parseSrcVersion(pkg *ftypes.Package, ver string) {
|
|
if ver == "" {
|
|
return
|
|
}
|
|
switch pkg.Identifier.PURL.Type {
|
|
case packageurl.TypeRPM:
|
|
v := rpmver.NewVersion(ver)
|
|
pkg.SrcEpoch = v.Epoch()
|
|
pkg.SrcVersion = v.Version()
|
|
pkg.SrcRelease = v.Release()
|
|
case packageurl.TypeDebian:
|
|
v, err := debver.NewVersion(ver)
|
|
if err != nil {
|
|
log.Debug("Failed to parse Debian version", log.Err(err))
|
|
return
|
|
}
|
|
pkg.SrcEpoch = v.Epoch()
|
|
pkg.SrcVersion = v.Version()
|
|
pkg.SrcRelease = v.Revision()
|
|
}
|
|
}
|
|
|
|
// addOSPkgs traverses relationships and adds OS packages
|
|
func (m *Decoder) addOSPkgs(sbom *types.SBOM) {
|
|
var pkgs []ftypes.Package
|
|
for _, rel := range m.bom.Relationships()[m.osID] {
|
|
pkg, ok := m.pkgs[rel.Dependency]
|
|
if !ok {
|
|
continue
|
|
}
|
|
pkgs = append(pkgs, *pkg)
|
|
delete(m.pkgs, rel.Dependency) // Delete the added package
|
|
}
|
|
if len(pkgs) == 0 {
|
|
return
|
|
}
|
|
sbom.Packages = []ftypes.PackageInfo{{Packages: pkgs}}
|
|
}
|
|
|
|
// addLangPkgs traverses relationships and adds language-specific packages
|
|
func (m *Decoder) addLangPkgs(sbom *types.SBOM) {
|
|
for id, app := range m.apps {
|
|
for _, rel := range m.bom.Relationships()[id] {
|
|
pkg, ok := m.pkgs[rel.Dependency]
|
|
if !ok {
|
|
continue
|
|
}
|
|
app.Packages = append(app.Packages, *pkg)
|
|
delete(m.pkgs, rel.Dependency) // Delete the added package
|
|
}
|
|
sbom.Applications = append(sbom.Applications, *app)
|
|
}
|
|
}
|
|
|
|
// addOrphanPkgs adds orphan packages.
|
|
// Orphan packages are packages that are not related to any components.
|
|
func (m *Decoder) addOrphanPkgs(sbom *types.SBOM) error {
|
|
osPkgMap := make(map[string]ftypes.Packages)
|
|
langPkgMap := make(map[ftypes.LangType]ftypes.Packages)
|
|
for _, pkg := range m.pkgs {
|
|
p := (*purl.PackageURL)(pkg.Identifier.PURL)
|
|
switch p.Class() {
|
|
case types.ClassOSPkg:
|
|
osPkgMap[p.Type] = append(osPkgMap[p.Type], *pkg)
|
|
case types.ClassLangPkg:
|
|
langType := p.LangType()
|
|
langPkgMap[langType] = append(langPkgMap[langType], *pkg)
|
|
}
|
|
}
|
|
|
|
if len(osPkgMap) > 1 {
|
|
return xerrors.Errorf("multiple types of OS packages in SBOM are not supported (%q)", lo.Keys(osPkgMap))
|
|
}
|
|
|
|
// Add OS packages only when OS is detected.
|
|
for _, pkgs := range osPkgMap {
|
|
if sbom.Metadata.OS == nil || !sbom.Metadata.OS.Detected() {
|
|
log.Warn("Ignore the OS package as no OS is detected.")
|
|
break
|
|
}
|
|
|
|
// TODO: mismatch between the OS and the packages should be rejected.
|
|
// e.g. OS: debian, Packages: rpm
|
|
sort.Sort(pkgs)
|
|
sbom.Packages = append(sbom.Packages, ftypes.PackageInfo{Packages: pkgs})
|
|
|
|
break // Just take the first element
|
|
}
|
|
|
|
// Add language-specific packages
|
|
for pkgType, pkgs := range langPkgMap {
|
|
sort.Sort(pkgs)
|
|
sbom.Applications = append(sbom.Applications, ftypes.Application{
|
|
Type: pkgType,
|
|
Packages: pkgs,
|
|
})
|
|
}
|
|
return nil
|
|
}
|