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