github.com/devseccon/trivy@v0.47.1-0.20231123133102-bd902a0bd996/pkg/sbom/cyclonedx/marshal.go (about)

     1  package cyclonedx
     2  
     3  import (
     4  	"fmt"
     5  	"strconv"
     6  	"strings"
     7  
     8  	cdx "github.com/CycloneDX/cyclonedx-go"
     9  	"github.com/samber/lo"
    10  	"golang.org/x/xerrors"
    11  
    12  	"github.com/devseccon/trivy/pkg/digest"
    13  	ftypes "github.com/devseccon/trivy/pkg/fanal/types"
    14  	"github.com/devseccon/trivy/pkg/purl"
    15  	"github.com/devseccon/trivy/pkg/sbom/cyclonedx/core"
    16  	"github.com/devseccon/trivy/pkg/scanner/utils"
    17  	"github.com/devseccon/trivy/pkg/types"
    18  )
    19  
    20  const (
    21  	PropertySchemaVersion = "SchemaVersion"
    22  	PropertyType          = "Type"
    23  	PropertyClass         = "Class"
    24  
    25  	// Image properties
    26  	PropertySize       = "Size"
    27  	PropertyImageID    = "ImageID"
    28  	PropertyRepoDigest = "RepoDigest"
    29  	PropertyDiffID     = "DiffID"
    30  	PropertyRepoTag    = "RepoTag"
    31  
    32  	// Package properties
    33  	PropertyPkgID           = "PkgID"
    34  	PropertyPkgType         = "PkgType"
    35  	PropertySrcName         = "SrcName"
    36  	PropertySrcVersion      = "SrcVersion"
    37  	PropertySrcRelease      = "SrcRelease"
    38  	PropertySrcEpoch        = "SrcEpoch"
    39  	PropertyModularitylabel = "Modularitylabel"
    40  	PropertyFilePath        = "FilePath"
    41  	PropertyLayerDigest     = "LayerDigest"
    42  	PropertyLayerDiffID     = "LayerDiffID"
    43  )
    44  
    45  var (
    46  	ErrInvalidBOMLink = xerrors.New("invalid bomLink format error")
    47  )
    48  
    49  type Marshaler struct {
    50  	core *core.CycloneDX
    51  }
    52  
    53  func NewMarshaler(version string) *Marshaler {
    54  	return &Marshaler{
    55  		core: core.NewCycloneDX(version),
    56  	}
    57  }
    58  
    59  // Marshal converts the Trivy report to the CycloneDX format
    60  func (e *Marshaler) Marshal(report types.Report) (*cdx.BOM, error) {
    61  	// Convert
    62  	root, err := e.MarshalReport(report)
    63  	if err != nil {
    64  		return nil, xerrors.Errorf("failed to marshal report: %w", err)
    65  	}
    66  
    67  	return e.core.Marshal(root), nil
    68  }
    69  
    70  func (e *Marshaler) MarshalReport(r types.Report) (*core.Component, error) {
    71  	// Metadata component
    72  	root, err := e.rootComponent(r)
    73  	if err != nil {
    74  		return nil, err
    75  	}
    76  
    77  	for _, result := range r.Results {
    78  		components, err := e.marshalResult(r.Metadata, result)
    79  		if err != nil {
    80  			return nil, err
    81  		}
    82  		root.Components = append(root.Components, components...)
    83  	}
    84  	return root, nil
    85  }
    86  
    87  func (e *Marshaler) marshalResult(metadata types.Metadata, result types.Result) ([]*core.Component, error) {
    88  	if result.Type == ftypes.NodePkg || result.Type == ftypes.PythonPkg ||
    89  		result.Type == ftypes.GemSpec || result.Type == ftypes.Jar || result.Type == ftypes.CondaPkg {
    90  		// If a package is language-specific package that isn't associated with a lock file,
    91  		// it will be a dependency of a component under "metadata".
    92  		// e.g.
    93  		//   Container component (alpine:3.15) ----------------------- #1
    94  		//     -> Library component (npm package, express-4.17.3) ---- #2
    95  		//     -> Library component (python package, django-4.0.2) --- #2
    96  		//     -> etc.
    97  		// ref. https://cyclonedx.org/use-cases/#inventory
    98  
    99  		// Dependency graph from #1 to #2
   100  		components, err := e.marshalPackages(metadata, result)
   101  		if err != nil {
   102  			return nil, err
   103  		}
   104  		return components, nil
   105  	} else if result.Class == types.ClassOSPkg || result.Class == types.ClassLangPkg {
   106  		// If a package is OS package, it will be a dependency of "Operating System" component.
   107  		// e.g.
   108  		//   Container component (alpine:3.15) --------------------- #1
   109  		//     -> Operating System Component (Alpine Linux 3.15) --- #2
   110  		//       -> Library component (bash-4.12) ------------------ #3
   111  		//       -> Library component (vim-8.2)   ------------------ #3
   112  		//       -> etc.
   113  		//
   114  		// Else if a package is language-specific package associated with a lock file,
   115  		// it will be a dependency of "Application" component.
   116  		// e.g.
   117  		//   Container component (alpine:3.15) ------------------------ #1
   118  		//     -> Application component (/app/package-lock.json) ------ #2
   119  		//       -> Library component (npm package, express-4.17.3) --- #3
   120  		//       -> Library component (npm package, lodash-4.17.21) --- #3
   121  		//       -> etc.
   122  
   123  		// #2
   124  		appComponent := e.resultComponent(result, metadata.OS)
   125  
   126  		// #3
   127  		components, err := e.marshalPackages(metadata, result)
   128  		if err != nil {
   129  			return nil, err
   130  		}
   131  
   132  		// Dependency graph from #2 to #3
   133  		appComponent.Components = components
   134  
   135  		// Dependency graph from #1 to #2
   136  		return []*core.Component{appComponent}, nil
   137  	}
   138  	return nil, nil
   139  }
   140  
   141  func (e *Marshaler) marshalPackages(metadata types.Metadata, result types.Result) ([]*core.Component, error) {
   142  	// Get dependency parents first
   143  	parents := ftypes.Packages(result.Packages).ParentDeps()
   144  
   145  	// Group vulnerabilities by package ID
   146  	vulns := lo.GroupBy(result.Vulnerabilities, func(v types.DetectedVulnerability) string {
   147  		return lo.Ternary(v.PkgID == "", fmt.Sprintf("%s@%s", v.PkgName, v.InstalledVersion), v.PkgID)
   148  	})
   149  
   150  	// Create package map
   151  	pkgs := lo.SliceToMap(result.Packages, func(pkg ftypes.Package) (string, Package) {
   152  		pkgID := lo.Ternary(pkg.ID == "", fmt.Sprintf("%s@%s", pkg.Name, utils.FormatVersion(pkg)), pkg.ID)
   153  		return pkgID, Package{
   154  			Type:            result.Type,
   155  			Metadata:        metadata,
   156  			Package:         pkg,
   157  			Vulnerabilities: vulns[pkgID],
   158  		}
   159  	})
   160  
   161  	var directComponents []*core.Component
   162  	for _, pkg := range pkgs {
   163  		// Skip indirect dependencies
   164  		if pkg.Indirect && len(parents[pkg.ID]) != 0 {
   165  			continue
   166  		}
   167  
   168  		// Recursive packages from direct dependencies
   169  		if component, err := e.marshalPackage(pkg, pkgs, make(map[string]*core.Component)); err != nil {
   170  			return nil, nil
   171  		} else if component != nil {
   172  			directComponents = append(directComponents, component)
   173  		}
   174  	}
   175  
   176  	return directComponents, nil
   177  }
   178  
   179  type Package struct {
   180  	ftypes.Package
   181  	Type            ftypes.TargetType
   182  	Metadata        types.Metadata
   183  	Vulnerabilities []types.DetectedVulnerability
   184  }
   185  
   186  func (e *Marshaler) marshalPackage(pkg Package, pkgs map[string]Package, components map[string]*core.Component,
   187  ) (*core.Component, error) {
   188  	if c, ok := components[pkg.ID]; ok {
   189  		return c, nil
   190  	}
   191  
   192  	component, err := pkgComponent(pkg)
   193  	if err != nil {
   194  		return nil, xerrors.Errorf("failed to parse pkg: %w", err)
   195  	}
   196  
   197  	// Skip component that can't be converted from `Package`
   198  	if component == nil {
   199  		return nil, nil
   200  	}
   201  	components[pkg.ID] = component
   202  
   203  	// Iterate dependencies
   204  	for _, dep := range pkg.DependsOn {
   205  		childPkg, ok := pkgs[dep]
   206  		if !ok {
   207  			continue
   208  		}
   209  
   210  		child, err := e.marshalPackage(childPkg, pkgs, components)
   211  		if err != nil {
   212  			return nil, xerrors.Errorf("failed to parse pkg: %w", err)
   213  		}
   214  		component.Components = append(component.Components, child)
   215  	}
   216  	return component, nil
   217  }
   218  
   219  func (e *Marshaler) rootComponent(r types.Report) (*core.Component, error) {
   220  	root := &core.Component{
   221  		Name: r.ArtifactName,
   222  	}
   223  
   224  	props := []core.Property{
   225  		{
   226  			Name:  PropertySchemaVersion,
   227  			Value: strconv.Itoa(r.SchemaVersion),
   228  		},
   229  	}
   230  
   231  	switch r.ArtifactType {
   232  	case ftypes.ArtifactContainerImage:
   233  		root.Type = cdx.ComponentTypeContainer
   234  		props = append(props, core.Property{
   235  			Name:  PropertyImageID,
   236  			Value: r.Metadata.ImageID,
   237  		})
   238  
   239  		p, err := purl.NewPackageURL(purl.TypeOCI, r.Metadata, ftypes.Package{})
   240  		if err != nil {
   241  			return nil, xerrors.Errorf("failed to new package url for oci: %w", err)
   242  		}
   243  		if p != nil {
   244  			root.PackageURL = p
   245  		}
   246  
   247  	case ftypes.ArtifactVM:
   248  		root.Type = cdx.ComponentTypeContainer
   249  	case ftypes.ArtifactFilesystem, ftypes.ArtifactRepository:
   250  		root.Type = cdx.ComponentTypeApplication
   251  	}
   252  
   253  	if r.Metadata.Size != 0 {
   254  		props = append(props, core.Property{
   255  			Name:  PropertySize,
   256  			Value: strconv.FormatInt(r.Metadata.Size, 10),
   257  		})
   258  	}
   259  
   260  	if len(r.Metadata.RepoDigests) > 0 {
   261  		props = append(props, core.Property{
   262  			Name:  PropertyRepoDigest,
   263  			Value: strings.Join(r.Metadata.RepoDigests, ","),
   264  		})
   265  	}
   266  	if len(r.Metadata.DiffIDs) > 0 {
   267  		props = append(props, core.Property{
   268  			Name:  PropertyDiffID,
   269  			Value: strings.Join(r.Metadata.DiffIDs, ","),
   270  		})
   271  	}
   272  	if len(r.Metadata.RepoTags) > 0 {
   273  		props = append(props, core.Property{
   274  			Name:  PropertyRepoTag,
   275  			Value: strings.Join(r.Metadata.RepoTags, ","),
   276  		})
   277  	}
   278  
   279  	root.Properties = filterProperties(props)
   280  
   281  	return root, nil
   282  }
   283  
   284  func (e *Marshaler) resultComponent(r types.Result, osFound *ftypes.OS) *core.Component {
   285  	component := &core.Component{
   286  		Name: r.Target,
   287  		Properties: []core.Property{
   288  			{
   289  				Name:  PropertyType,
   290  				Value: string(r.Type),
   291  			},
   292  			{
   293  				Name:  PropertyClass,
   294  				Value: string(r.Class),
   295  			},
   296  		},
   297  	}
   298  
   299  	switch r.Class {
   300  	case types.ClassOSPkg:
   301  		// UUID needs to be generated since Operating System Component cannot generate PURL.
   302  		// https://cyclonedx.org/use-cases/#known-vulnerabilities
   303  		if osFound != nil {
   304  			component.Name = string(osFound.Family)
   305  			component.Version = osFound.Name
   306  		}
   307  		component.Type = cdx.ComponentTypeOS
   308  	case types.ClassLangPkg:
   309  		// UUID needs to be generated since Application Component cannot generate PURL.
   310  		// https://cyclonedx.org/use-cases/#known-vulnerabilities
   311  		component.Type = cdx.ComponentTypeApplication
   312  	}
   313  
   314  	return component
   315  }
   316  
   317  func pkgComponent(pkg Package) (*core.Component, error) {
   318  	pu, err := purl.NewPackageURL(pkg.Type, pkg.Metadata, pkg.Package)
   319  	if err != nil {
   320  		return nil, xerrors.Errorf("failed to new package purl: %w", err)
   321  	}
   322  
   323  	name := pkg.Name
   324  	version := pkg.Version
   325  	var group string
   326  	// there are cases when we can't build purl
   327  	// e.g. local Go packages
   328  	if pu != nil {
   329  		version = pu.Version
   330  		// use `group` field for GroupID and `name` for ArtifactID for jar files
   331  		if pkg.Type == ftypes.Jar {
   332  			name = pu.Name
   333  			group = pu.Namespace
   334  		}
   335  	}
   336  
   337  	properties := []core.Property{
   338  		{
   339  			Name:  PropertyPkgID,
   340  			Value: pkg.ID,
   341  		},
   342  		{
   343  			Name:  PropertyPkgType,
   344  			Value: string(pkg.Type),
   345  		},
   346  		{
   347  			Name:  PropertyFilePath,
   348  			Value: pkg.FilePath,
   349  		},
   350  		{
   351  			Name:  PropertySrcName,
   352  			Value: pkg.SrcName,
   353  		},
   354  		{
   355  			Name:  PropertySrcVersion,
   356  			Value: pkg.SrcVersion,
   357  		},
   358  		{
   359  			Name:  PropertySrcRelease,
   360  			Value: pkg.SrcRelease,
   361  		},
   362  		{
   363  			Name:  PropertySrcEpoch,
   364  			Value: strconv.Itoa(pkg.SrcEpoch),
   365  		},
   366  		{
   367  			Name:  PropertyModularitylabel,
   368  			Value: pkg.Modularitylabel,
   369  		},
   370  		{
   371  			Name:  PropertyLayerDigest,
   372  			Value: pkg.Layer.Digest,
   373  		},
   374  		{
   375  			Name:  PropertyLayerDiffID,
   376  			Value: pkg.Layer.DiffID,
   377  		},
   378  	}
   379  
   380  	return &core.Component{
   381  		Type:            cdx.ComponentTypeLibrary,
   382  		Name:            name,
   383  		Group:           group,
   384  		Version:         version,
   385  		PackageURL:      pu,
   386  		Supplier:        pkg.Maintainer,
   387  		Licenses:        pkg.Licenses,
   388  		Hashes:          lo.Ternary(pkg.Digest == "", nil, []digest.Digest{pkg.Digest}),
   389  		Properties:      filterProperties(properties),
   390  		Vulnerabilities: pkg.Vulnerabilities,
   391  	}, nil
   392  }
   393  
   394  func filterProperties(props []core.Property) []core.Property {
   395  	return lo.Filter(props, func(property core.Property, index int) bool {
   396  		return !(property.Value == "" || (property.Name == PropertySrcEpoch && property.Value == "0"))
   397  	})
   398  }