github.com/lineaje-labs/syft@v0.98.1-0.20231227153149-9e393f60ff1b/syft/format/common/cyclonedxhelpers/decoder.go (about)

     1  package cyclonedxhelpers
     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/format/common"
    11  	"github.com/anchore/syft/syft/linux"
    12  	"github.com/anchore/syft/syft/pkg"
    13  	"github.com/anchore/syft/syft/sbom"
    14  	"github.com/anchore/syft/syft/source"
    15  )
    16  
    17  func ToSyftModel(bom *cyclonedx.BOM) (*sbom.SBOM, error) {
    18  	if bom == nil {
    19  		return nil, fmt.Errorf("no content defined in CycloneDX BOM")
    20  	}
    21  
    22  	s := &sbom.SBOM{
    23  		Artifacts: sbom.Artifacts{
    24  			Packages:          pkg.NewCollection(),
    25  			LinuxDistribution: linuxReleaseFromComponents(*bom.Components),
    26  		},
    27  		Source:     extractComponents(bom.Metadata),
    28  		Descriptor: extractDescriptor(bom.Metadata),
    29  	}
    30  
    31  	idMap := make(map[string]interface{})
    32  
    33  	if err := collectBomPackages(bom, s, idMap); err != nil {
    34  		return nil, err
    35  	}
    36  
    37  	collectRelationships(bom, s, idMap)
    38  
    39  	return s, nil
    40  }
    41  
    42  func collectBomPackages(bom *cyclonedx.BOM, s *sbom.SBOM, idMap map[string]interface{}) error {
    43  	if bom.Components == nil {
    44  		return fmt.Errorf("no components are defined in the CycloneDX BOM")
    45  	}
    46  	for i := range *bom.Components {
    47  		collectPackages(&(*bom.Components)[i], s, idMap)
    48  	}
    49  	return nil
    50  }
    51  
    52  func collectPackages(component *cyclonedx.Component, s *sbom.SBOM, idMap map[string]interface{}) {
    53  	switch component.Type {
    54  	case cyclonedx.ComponentTypeOS:
    55  	case cyclonedx.ComponentTypeContainer:
    56  	case cyclonedx.ComponentTypeApplication, cyclonedx.ComponentTypeFramework, cyclonedx.ComponentTypeLibrary:
    57  		p := decodeComponent(component)
    58  		idMap[component.BOMRef] = p
    59  		syftID := extractSyftPacakgeID(component.BOMRef)
    60  		if syftID != "" {
    61  			idMap[syftID] = p
    62  		}
    63  		// TODO there must be a better way than needing to call this manually:
    64  		p.SetID()
    65  		s.Artifacts.Packages.Add(*p)
    66  	}
    67  
    68  	if component.Components != nil {
    69  		for i := range *component.Components {
    70  			collectPackages(&(*component.Components)[i], s, idMap)
    71  		}
    72  	}
    73  }
    74  
    75  func extractSyftPacakgeID(i string) string {
    76  	instance, err := packageurl.FromString(i)
    77  	if err != nil {
    78  		return ""
    79  	}
    80  	for _, q := range instance.Qualifiers {
    81  		if q.Key == "package-id" {
    82  			return q.Value
    83  		}
    84  	}
    85  	return ""
    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  		common.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 := common.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 := common.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  	switch c.Type {
   215  	case cyclonedx.ComponentTypeContainer:
   216  		var labels map[string]string
   217  
   218  		if meta.Properties != nil {
   219  			labels = decodeProperties(*meta.Properties, "syft:image:labels:")
   220  		}
   221  
   222  		return source.Description{
   223  			ID: "",
   224  			// TODO: can we decode alias name-version somehow? (it isn't be encoded in the first place yet)
   225  
   226  			Metadata: source.StereoscopeImageSourceMetadata{
   227  				UserInput:      c.Name,
   228  				ID:             c.BOMRef,
   229  				ManifestDigest: c.Version,
   230  				Labels:         labels,
   231  			},
   232  		}
   233  	case cyclonedx.ComponentTypeFile:
   234  		// TODO: can we decode alias name-version somehow? (it isn't be encoded in the first place yet)
   235  
   236  		// TODO: this is lossy... we can't know if this is a file or a directory
   237  		return source.Description{
   238  			ID:       "",
   239  			Metadata: source.FileSourceMetadata{Path: c.Name},
   240  		}
   241  	}
   242  	return source.Description{}
   243  }
   244  
   245  // if there is more than one tool in meta.Tools' list the last item will be used
   246  // as descriptor. If there is a way to know which tool to use here please fix it.
   247  func extractDescriptor(meta *cyclonedx.Metadata) (desc sbom.Descriptor) {
   248  	if meta == nil || meta.Tools == nil {
   249  		return
   250  	}
   251  
   252  	for _, t := range *meta.Tools {
   253  		desc.Name = t.Name
   254  		desc.Version = t.Version
   255  	}
   256  
   257  	return
   258  }