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 }