mirror of
https://github.com/anchore/syft.git
synced 2025-11-17 08:23:15 +01:00
--------- Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
284 lines
6.7 KiB
Go
284 lines
6.7 KiB
Go
package helpers
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/CycloneDX/cyclonedx-go"
|
|
|
|
"github.com/anchore/syft/syft/artifact"
|
|
"github.com/anchore/syft/syft/linux"
|
|
"github.com/anchore/syft/syft/pkg"
|
|
"github.com/anchore/syft/syft/sbom"
|
|
"github.com/anchore/syft/syft/source"
|
|
)
|
|
|
|
func ToSyftModel(bom *cyclonedx.BOM) (*sbom.SBOM, error) {
|
|
if bom == nil {
|
|
return nil, fmt.Errorf("no content defined in CycloneDX BOM")
|
|
}
|
|
|
|
s := &sbom.SBOM{
|
|
Artifacts: sbom.Artifacts{
|
|
Packages: pkg.NewCollection(),
|
|
LinuxDistribution: linuxReleaseFromComponents(*bom.Components),
|
|
},
|
|
Source: extractComponents(bom.Metadata),
|
|
Descriptor: extractDescriptor(bom.Metadata),
|
|
}
|
|
|
|
idMap := make(map[string]interface{})
|
|
|
|
if err := collectBomPackages(bom, s, idMap); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
collectRelationships(bom, s, idMap)
|
|
|
|
return s, nil
|
|
}
|
|
|
|
func collectBomPackages(bom *cyclonedx.BOM, s *sbom.SBOM, idMap map[string]interface{}) error {
|
|
componentsPresent := false
|
|
if bom.Components != nil {
|
|
for i := range *bom.Components {
|
|
collectPackages(&(*bom.Components)[i], s, idMap)
|
|
}
|
|
componentsPresent = true
|
|
}
|
|
|
|
if bom.Metadata != nil && bom.Metadata.Component != nil {
|
|
collectPackages(bom.Metadata.Component, s, idMap)
|
|
componentsPresent = true
|
|
}
|
|
|
|
if !componentsPresent {
|
|
return fmt.Errorf("no components are defined in the CycloneDX BOM")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func collectPackages(component *cyclonedx.Component, s *sbom.SBOM, idMap map[string]interface{}) {
|
|
switch component.Type {
|
|
case cyclonedx.ComponentTypeOS:
|
|
case cyclonedx.ComponentTypeContainer:
|
|
case cyclonedx.ComponentTypeApplication, cyclonedx.ComponentTypeFramework, cyclonedx.ComponentTypeLibrary, cyclonedx.ComponentTypeMachineLearningModel:
|
|
p := decodeComponent(component)
|
|
idMap[component.BOMRef] = p
|
|
if component.BOMRef != "" {
|
|
// always prefer the IDs from the SBOM over derived IDs
|
|
p.OverrideID(artifact.ID(component.BOMRef))
|
|
} else {
|
|
p.SetID()
|
|
}
|
|
syftID := p.ID()
|
|
if syftID != "" {
|
|
idMap[string(syftID)] = p
|
|
}
|
|
s.Artifacts.Packages.Add(*p)
|
|
}
|
|
|
|
if component.Components != nil {
|
|
for i := range *component.Components {
|
|
collectPackages(&(*component.Components)[i], s, idMap)
|
|
}
|
|
}
|
|
}
|
|
|
|
func linuxReleaseFromComponents(components []cyclonedx.Component) *linux.Release {
|
|
for i := range components {
|
|
component := &components[i]
|
|
if component.Type == cyclonedx.ComponentTypeOS {
|
|
return linuxReleaseFromOSComponent(component)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func linuxReleaseFromOSComponent(component *cyclonedx.Component) *linux.Release {
|
|
if component == nil {
|
|
return nil
|
|
}
|
|
|
|
var name string
|
|
var version string
|
|
if component.SWID != nil {
|
|
name = component.SWID.Name
|
|
version = component.SWID.Version
|
|
}
|
|
if name == "" {
|
|
name = component.Name
|
|
}
|
|
if name == "" {
|
|
name = getPropertyValue(component, "id")
|
|
}
|
|
if version == "" {
|
|
version = component.Version
|
|
}
|
|
if version == "" {
|
|
version = getPropertyValue(component, "versionID")
|
|
}
|
|
|
|
rel := &linux.Release{
|
|
CPEName: component.CPE,
|
|
PrettyName: name,
|
|
Name: name,
|
|
ID: name,
|
|
IDLike: []string{name},
|
|
Version: version,
|
|
VersionID: version,
|
|
}
|
|
if component.ExternalReferences != nil {
|
|
for _, ref := range *component.ExternalReferences {
|
|
switch ref.Type {
|
|
case cyclonedx.ERTypeIssueTracker:
|
|
rel.BugReportURL = ref.URL
|
|
case cyclonedx.ERTypeWebsite:
|
|
rel.HomeURL = ref.URL
|
|
case cyclonedx.ERTypeOther:
|
|
switch ref.Comment {
|
|
case "support":
|
|
rel.SupportURL = ref.URL
|
|
case "privacyPolicy":
|
|
rel.PrivacyPolicyURL = ref.URL
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if component.Properties != nil {
|
|
values := map[string]string{}
|
|
for _, p := range *component.Properties {
|
|
values[p.Name] = p.Value
|
|
}
|
|
DecodeInto(&rel, values, "syft:distro", CycloneDXFields)
|
|
}
|
|
|
|
return rel
|
|
}
|
|
|
|
func getPropertyValue(component *cyclonedx.Component, name string) string {
|
|
if component.Properties != nil {
|
|
for _, p := range *component.Properties {
|
|
if p.Name == name {
|
|
return p.Value
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func collectRelationships(bom *cyclonedx.BOM, s *sbom.SBOM, idMap map[string]interface{}) {
|
|
if bom.Dependencies == nil {
|
|
return
|
|
}
|
|
for _, d := range *bom.Dependencies {
|
|
if d.Dependencies == nil {
|
|
continue
|
|
}
|
|
|
|
toPtr, toExists := idMap[d.Ref]
|
|
if !toExists {
|
|
continue
|
|
}
|
|
to, ok := PtrToStruct(toPtr).(artifact.Identifiable)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
for _, t := range *d.Dependencies {
|
|
fromPtr, fromExists := idMap[t]
|
|
if !fromExists {
|
|
continue
|
|
}
|
|
from, ok := PtrToStruct(fromPtr).(artifact.Identifiable)
|
|
if !ok {
|
|
continue
|
|
}
|
|
s.Relationships = append(s.Relationships, artifact.Relationship{
|
|
From: from,
|
|
To: to,
|
|
// match assumptions in encoding, that this is the only type of relationship captured:
|
|
Type: artifact.DependencyOfRelationship,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func extractComponents(meta *cyclonedx.Metadata) source.Description {
|
|
if meta == nil || meta.Component == nil {
|
|
return source.Description{}
|
|
}
|
|
c := meta.Component
|
|
|
|
supplier := ""
|
|
// First check component-level supplier
|
|
if c.Supplier != nil && c.Supplier.Name != "" {
|
|
supplier = c.Supplier.Name
|
|
}
|
|
// Fall back to metadata-level supplier if component supplier is not set
|
|
if supplier == "" && meta.Supplier != nil && meta.Supplier.Name != "" {
|
|
supplier = meta.Supplier.Name
|
|
}
|
|
|
|
switch c.Type {
|
|
case cyclonedx.ComponentTypeContainer:
|
|
var labels map[string]string
|
|
|
|
if meta.Properties != nil {
|
|
labels = decodeProperties(*meta.Properties, "syft:image:labels:")
|
|
}
|
|
|
|
return source.Description{
|
|
ID: "",
|
|
Supplier: supplier,
|
|
// TODO: can we decode alias name-version somehow? (it isn't be encoded in the first place yet)
|
|
|
|
Metadata: source.ImageMetadata{
|
|
UserInput: c.Name,
|
|
ID: c.BOMRef,
|
|
ManifestDigest: c.Version,
|
|
Labels: labels,
|
|
},
|
|
}
|
|
case cyclonedx.ComponentTypeFile:
|
|
// TODO: can we decode alias name-version somehow? (it isn't be encoded in the first place yet)
|
|
|
|
// TODO: this is lossy... we can't know if this is a file or a directory
|
|
return source.Description{
|
|
ID: "",
|
|
Supplier: supplier,
|
|
Metadata: source.FileMetadata{Path: c.Name},
|
|
}
|
|
}
|
|
return source.Description{}
|
|
}
|
|
|
|
// if there is more than one tool in meta.Tools' list the last item will be used
|
|
// as descriptor. If there is a way to know which tool to use here please fix it.
|
|
func extractDescriptor(meta *cyclonedx.Metadata) (desc sbom.Descriptor) {
|
|
if meta == nil || meta.Tools == nil {
|
|
return
|
|
}
|
|
|
|
// handle 1.5 component element
|
|
if meta.Tools.Components != nil {
|
|
for _, t := range *meta.Tools.Components {
|
|
desc.Name = t.Name
|
|
desc.Version = t.Version
|
|
return
|
|
}
|
|
}
|
|
|
|
// handle pre-1.5 tool element
|
|
if meta.Tools.Tools != nil {
|
|
for _, t := range *meta.Tools.Tools {
|
|
desc.Name = t.Name
|
|
desc.Version = t.Version
|
|
return
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|