github.com/kastenhq/syft@v0.0.0-20230821225854-0710af25cdbe/syft/formats/common/cyclonedxhelpers/decoder.go (about)

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