Files
trivy/pkg/sbom/io/decode.go
Teppei Fukuda 983ac15f22 ci: add depguard (#6963)
Signed-off-by: knqyf263 <knqyf263@gmail.com>
2024-06-20 02:48:08 +00:00

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
}