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