github.com/anchore/syft@v1.38.2/syft/format/internal/cyclonedxutil/helpers/decoder.go (about)

     1  package helpers
     2  
     3  import (
     4  	"fmt"
     5  
     6  	"github.com/CycloneDX/cyclonedx-go"
     7  
     8  	"github.com/anchore/syft/syft/artifact"
     9  	"github.com/anchore/syft/syft/linux"
    10  	"github.com/anchore/syft/syft/pkg"
    11  	"github.com/anchore/syft/syft/sbom"
    12  	"github.com/anchore/syft/syft/source"
    13  )
    14  
    15  func ToSyftModel(bom *cyclonedx.BOM) (*sbom.SBOM, error) {
    16  	if bom == nil {
    17  		return nil, fmt.Errorf("no content defined in CycloneDX BOM")
    18  	}
    19  
    20  	s := &sbom.SBOM{
    21  		Artifacts: sbom.Artifacts{
    22  			Packages:          pkg.NewCollection(),
    23  			LinuxDistribution: linuxReleaseFromComponents(*bom.Components),
    24  		},
    25  		Source:     extractComponents(bom.Metadata),
    26  		Descriptor: extractDescriptor(bom.Metadata),
    27  	}
    28  
    29  	idMap := make(map[string]interface{})
    30  
    31  	if err := collectBomPackages(bom, s, idMap); err != nil {
    32  		return nil, err
    33  	}
    34  
    35  	collectRelationships(bom, s, idMap)
    36  
    37  	return s, nil
    38  }
    39  
    40  func collectBomPackages(bom *cyclonedx.BOM, s *sbom.SBOM, idMap map[string]interface{}) error {
    41  	componentsPresent := false
    42  	if bom.Components != nil {
    43  		for i := range *bom.Components {
    44  			collectPackages(&(*bom.Components)[i], s, idMap)
    45  		}
    46  		componentsPresent = true
    47  	}
    48  
    49  	if bom.Metadata != nil && bom.Metadata.Component != nil {
    50  		collectPackages(bom.Metadata.Component, s, idMap)
    51  		componentsPresent = true
    52  	}
    53  
    54  	if !componentsPresent {
    55  		return fmt.Errorf("no components are defined in the CycloneDX BOM")
    56  	}
    57  
    58  	return nil
    59  }
    60  
    61  func collectPackages(component *cyclonedx.Component, s *sbom.SBOM, idMap map[string]interface{}) {
    62  	switch component.Type {
    63  	case cyclonedx.ComponentTypeOS:
    64  	case cyclonedx.ComponentTypeContainer:
    65  	case cyclonedx.ComponentTypeApplication, cyclonedx.ComponentTypeFramework, cyclonedx.ComponentTypeLibrary, cyclonedx.ComponentTypeMachineLearningModel:
    66  		p := decodeComponent(component)
    67  		idMap[component.BOMRef] = p
    68  		if component.BOMRef != "" {
    69  			// always prefer the IDs from the SBOM over derived IDs
    70  			p.OverrideID(artifact.ID(component.BOMRef))
    71  		} else {
    72  			p.SetID()
    73  		}
    74  		syftID := p.ID()
    75  		if syftID != "" {
    76  			idMap[string(syftID)] = p
    77  		}
    78  		s.Artifacts.Packages.Add(*p)
    79  	}
    80  
    81  	if component.Components != nil {
    82  		for i := range *component.Components {
    83  			collectPackages(&(*component.Components)[i], s, idMap)
    84  		}
    85  	}
    86  }
    87  
    88  func linuxReleaseFromComponents(components []cyclonedx.Component) *linux.Release {
    89  	for i := range components {
    90  		component := &components[i]
    91  		if component.Type == cyclonedx.ComponentTypeOS {
    92  			return linuxReleaseFromOSComponent(component)
    93  		}
    94  	}
    95  	return nil
    96  }
    97  
    98  func linuxReleaseFromOSComponent(component *cyclonedx.Component) *linux.Release {
    99  	if component == nil {
   100  		return nil
   101  	}
   102  
   103  	var name string
   104  	var version string
   105  	if component.SWID != nil {
   106  		name = component.SWID.Name
   107  		version = component.SWID.Version
   108  	}
   109  	if name == "" {
   110  		name = component.Name
   111  	}
   112  	if name == "" {
   113  		name = getPropertyValue(component, "id")
   114  	}
   115  	if version == "" {
   116  		version = component.Version
   117  	}
   118  	if version == "" {
   119  		version = getPropertyValue(component, "versionID")
   120  	}
   121  
   122  	rel := &linux.Release{
   123  		CPEName:    component.CPE,
   124  		PrettyName: name,
   125  		Name:       name,
   126  		ID:         name,
   127  		IDLike:     []string{name},
   128  		Version:    version,
   129  		VersionID:  version,
   130  	}
   131  	if component.ExternalReferences != nil {
   132  		for _, ref := range *component.ExternalReferences {
   133  			switch ref.Type {
   134  			case cyclonedx.ERTypeIssueTracker:
   135  				rel.BugReportURL = ref.URL
   136  			case cyclonedx.ERTypeWebsite:
   137  				rel.HomeURL = ref.URL
   138  			case cyclonedx.ERTypeOther:
   139  				switch ref.Comment {
   140  				case "support":
   141  					rel.SupportURL = ref.URL
   142  				case "privacyPolicy":
   143  					rel.PrivacyPolicyURL = ref.URL
   144  				}
   145  			}
   146  		}
   147  	}
   148  
   149  	if component.Properties != nil {
   150  		values := map[string]string{}
   151  		for _, p := range *component.Properties {
   152  			values[p.Name] = p.Value
   153  		}
   154  		DecodeInto(&rel, values, "syft:distro", CycloneDXFields)
   155  	}
   156  
   157  	return rel
   158  }
   159  
   160  func getPropertyValue(component *cyclonedx.Component, name string) string {
   161  	if component.Properties != nil {
   162  		for _, p := range *component.Properties {
   163  			if p.Name == name {
   164  				return p.Value
   165  			}
   166  		}
   167  	}
   168  	return ""
   169  }
   170  
   171  func collectRelationships(bom *cyclonedx.BOM, s *sbom.SBOM, idMap map[string]interface{}) {
   172  	if bom.Dependencies == nil {
   173  		return
   174  	}
   175  	for _, d := range *bom.Dependencies {
   176  		if d.Dependencies == nil {
   177  			continue
   178  		}
   179  
   180  		toPtr, toExists := idMap[d.Ref]
   181  		if !toExists {
   182  			continue
   183  		}
   184  		to, ok := PtrToStruct(toPtr).(artifact.Identifiable)
   185  		if !ok {
   186  			continue
   187  		}
   188  
   189  		for _, t := range *d.Dependencies {
   190  			fromPtr, fromExists := idMap[t]
   191  			if !fromExists {
   192  				continue
   193  			}
   194  			from, ok := PtrToStruct(fromPtr).(artifact.Identifiable)
   195  			if !ok {
   196  				continue
   197  			}
   198  			s.Relationships = append(s.Relationships, artifact.Relationship{
   199  				From: from,
   200  				To:   to,
   201  				// match assumptions in encoding, that this is the only type of relationship captured:
   202  				Type: artifact.DependencyOfRelationship,
   203  			})
   204  		}
   205  	}
   206  }
   207  
   208  func extractComponents(meta *cyclonedx.Metadata) source.Description {
   209  	if meta == nil || meta.Component == nil {
   210  		return source.Description{}
   211  	}
   212  	c := meta.Component
   213  
   214  	supplier := ""
   215  	// First check component-level supplier
   216  	if c.Supplier != nil && c.Supplier.Name != "" {
   217  		supplier = c.Supplier.Name
   218  	}
   219  	// Fall back to metadata-level supplier if component supplier is not set
   220  	if supplier == "" && meta.Supplier != nil && meta.Supplier.Name != "" {
   221  		supplier = meta.Supplier.Name
   222  	}
   223  
   224  	switch c.Type {
   225  	case cyclonedx.ComponentTypeContainer:
   226  		var labels map[string]string
   227  
   228  		if meta.Properties != nil {
   229  			labels = decodeProperties(*meta.Properties, "syft:image:labels:")
   230  		}
   231  
   232  		return source.Description{
   233  			ID:       "",
   234  			Supplier: supplier,
   235  			// TODO: can we decode alias name-version somehow? (it isn't be encoded in the first place yet)
   236  
   237  			Metadata: source.ImageMetadata{
   238  				UserInput:      c.Name,
   239  				ID:             c.BOMRef,
   240  				ManifestDigest: c.Version,
   241  				Labels:         labels,
   242  			},
   243  		}
   244  	case cyclonedx.ComponentTypeFile:
   245  		// TODO: can we decode alias name-version somehow? (it isn't be encoded in the first place yet)
   246  
   247  		// TODO: this is lossy... we can't know if this is a file or a directory
   248  		return source.Description{
   249  			ID:       "",
   250  			Supplier: supplier,
   251  			Metadata: source.FileMetadata{Path: c.Name},
   252  		}
   253  	}
   254  	return source.Description{}
   255  }
   256  
   257  // if there is more than one tool in meta.Tools' list the last item will be used
   258  // as descriptor. If there is a way to know which tool to use here please fix it.
   259  func extractDescriptor(meta *cyclonedx.Metadata) (desc sbom.Descriptor) {
   260  	if meta == nil || meta.Tools == nil {
   261  		return
   262  	}
   263  
   264  	// handle 1.5 component element
   265  	if meta.Tools.Components != nil {
   266  		for _, t := range *meta.Tools.Components {
   267  			desc.Name = t.Name
   268  			desc.Version = t.Version
   269  			return
   270  		}
   271  	}
   272  
   273  	// handle pre-1.5 tool element
   274  	if meta.Tools.Tools != nil {
   275  		for _, t := range *meta.Tools.Tools {
   276  			desc.Name = t.Name
   277  			desc.Version = t.Version
   278  			return
   279  		}
   280  	}
   281  
   282  	return
   283  }