github.com/nextlinux/gosbom@v0.81.1-0.20230627115839-1ff50c281391/gosbom/formats/common/cyclonedxhelpers/decoder.go (about)

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