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

     1  package cyclonedxhelpers
     2  
     3  import (
     4  	"time"
     5  
     6  	"github.com/CycloneDX/cyclonedx-go"
     7  	"github.com/google/uuid"
     8  	"github.com/nextlinux/gosbom/gosbom/artifact"
     9  	"github.com/nextlinux/gosbom/gosbom/cpe"
    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  	"github.com/nextlinux/gosbom/internal"
    15  	"github.com/nextlinux/gosbom/internal/log"
    16  )
    17  
    18  func ToFormatModel(s sbom.SBOM) *cyclonedx.BOM {
    19  	cdxBOM := cyclonedx.NewBOM()
    20  
    21  	// NOTE(jonasagx): cycloneDX requires URN uuids (URN returns the RFC 2141 URN form of uuid):
    22  	// https://github.com/CycloneDX/specification/blob/master/schema/bom-1.3-strict.schema.json#L36
    23  	// "pattern": "^urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
    24  	cdxBOM.SerialNumber = uuid.New().URN()
    25  	cdxBOM.Metadata = toBomDescriptor(internal.ApplicationName, s.Descriptor.Version, s.Source)
    26  
    27  	packages := s.Artifacts.Packages.Sorted()
    28  	components := make([]cyclonedx.Component, len(packages))
    29  	for i, p := range packages {
    30  		components[i] = encodeComponent(p)
    31  	}
    32  	components = append(components, toOSComponent(s.Artifacts.LinuxDistribution)...)
    33  	cdxBOM.Components = &components
    34  
    35  	dependencies := toDependencies(s.Relationships)
    36  	if len(dependencies) > 0 {
    37  		cdxBOM.Dependencies = &dependencies
    38  	}
    39  
    40  	return cdxBOM
    41  }
    42  
    43  func toOSComponent(distro *linux.Release) []cyclonedx.Component {
    44  	if distro == nil {
    45  		return []cyclonedx.Component{}
    46  	}
    47  	eRefs := &[]cyclonedx.ExternalReference{}
    48  	if distro.BugReportURL != "" {
    49  		*eRefs = append(*eRefs, cyclonedx.ExternalReference{
    50  			URL:  distro.BugReportURL,
    51  			Type: cyclonedx.ERTypeIssueTracker,
    52  		})
    53  	}
    54  	if distro.HomeURL != "" {
    55  		*eRefs = append(*eRefs, cyclonedx.ExternalReference{
    56  			URL:  distro.HomeURL,
    57  			Type: cyclonedx.ERTypeWebsite,
    58  		})
    59  	}
    60  	if distro.SupportURL != "" {
    61  		*eRefs = append(*eRefs, cyclonedx.ExternalReference{
    62  			URL:     distro.SupportURL,
    63  			Type:    cyclonedx.ERTypeOther,
    64  			Comment: "support",
    65  		})
    66  	}
    67  	if distro.PrivacyPolicyURL != "" {
    68  		*eRefs = append(*eRefs, cyclonedx.ExternalReference{
    69  			URL:     distro.PrivacyPolicyURL,
    70  			Type:    cyclonedx.ERTypeOther,
    71  			Comment: "privacyPolicy",
    72  		})
    73  	}
    74  	if len(*eRefs) == 0 {
    75  		eRefs = nil
    76  	}
    77  	props := encodeProperties(distro, "gosbom:distro")
    78  	var properties *[]cyclonedx.Property
    79  	if len(props) > 0 {
    80  		properties = &props
    81  	}
    82  	return []cyclonedx.Component{
    83  		{
    84  			Type: cyclonedx.ComponentTypeOS,
    85  			// FIXME is it idiomatic to be using SWID here for specific name and version information?
    86  			SWID: &cyclonedx.SWID{
    87  				TagID:   distro.ID,
    88  				Name:    distro.ID,
    89  				Version: distro.VersionID,
    90  			},
    91  			Description: distro.PrettyName,
    92  			Name:        distro.ID,
    93  			Version:     distro.VersionID,
    94  			// TODO should we add a PURL?
    95  			CPE:                formatCPE(distro.CPEName),
    96  			ExternalReferences: eRefs,
    97  			Properties:         properties,
    98  		},
    99  	}
   100  }
   101  
   102  func formatCPE(cpeString string) string {
   103  	c, err := cpe.New(cpeString)
   104  	if err != nil {
   105  		log.Debugf("skipping invalid CPE: %s", cpeString)
   106  		return ""
   107  	}
   108  	return cpe.String(c)
   109  }
   110  
   111  // NewBomDescriptor returns a new BomDescriptor tailored for the current time and "gosbom" tool details.
   112  func toBomDescriptor(name, version string, srcMetadata source.Metadata) *cyclonedx.Metadata {
   113  	return &cyclonedx.Metadata{
   114  		Timestamp: time.Now().Format(time.RFC3339),
   115  		Tools: &[]cyclonedx.Tool{
   116  			{
   117  				Vendor:  "anchore",
   118  				Name:    name,
   119  				Version: version,
   120  			},
   121  		},
   122  		Component: toBomDescriptorComponent(srcMetadata),
   123  	}
   124  }
   125  
   126  // used to indicate that a relationship listed under the gosbom artifact package can be represented as a cyclonedx dependency.
   127  // NOTE: CycloneDX provides the ability to describe components and their dependency on other components.
   128  // The dependency graph is capable of representing both direct and transitive relationships.
   129  // If a relationship is either direct or transitive it can be included in this function.
   130  // An example of a relationship to not include would be: OwnershipByFileOverlapRelationship.
   131  func isExpressiblePackageRelationship(ty artifact.RelationshipType) bool {
   132  	switch ty {
   133  	case artifact.DependencyOfRelationship:
   134  		return true
   135  	default:
   136  		return false
   137  	}
   138  }
   139  
   140  func toDependencies(relationships []artifact.Relationship) []cyclonedx.Dependency {
   141  	result := make([]cyclonedx.Dependency, 0)
   142  	for _, r := range relationships {
   143  		exists := isExpressiblePackageRelationship(r.Type)
   144  		if !exists {
   145  			log.Debugf("unable to convert relationship from CycloneDX 1.4 JSON, dropping: %+v", r)
   146  			continue
   147  		}
   148  
   149  		// we only capture package-to-package relationships for now
   150  		fromPkg, ok := r.From.(*pkg.Package)
   151  		if !ok {
   152  			continue
   153  		}
   154  
   155  		toPkg, ok := r.To.(*pkg.Package)
   156  		if !ok {
   157  			continue
   158  		}
   159  
   160  		// ind dep
   161  
   162  		innerDeps := []string{}
   163  		innerDeps = append(innerDeps, deriveBomRef(*fromPkg))
   164  		result = append(result, cyclonedx.Dependency{
   165  			Ref:          deriveBomRef(*toPkg),
   166  			Dependencies: &innerDeps,
   167  		})
   168  	}
   169  	return result
   170  }
   171  
   172  func toBomDescriptorComponent(srcMetadata source.Metadata) *cyclonedx.Component {
   173  	name := srcMetadata.Name
   174  	switch srcMetadata.Scheme {
   175  	case source.ImageScheme:
   176  		if name == "" {
   177  			name = srcMetadata.ImageMetadata.UserInput
   178  		}
   179  		bomRef, err := artifact.IDByHash(srcMetadata.ImageMetadata.ID)
   180  		if err != nil {
   181  			log.Warnf("unable to get fingerprint of image metadata=%s: %+v", srcMetadata.ImageMetadata.ID, err)
   182  		}
   183  		return &cyclonedx.Component{
   184  			BOMRef:  string(bomRef),
   185  			Type:    cyclonedx.ComponentTypeContainer,
   186  			Name:    name,
   187  			Version: srcMetadata.ImageMetadata.ManifestDigest,
   188  		}
   189  	case source.DirectoryScheme, source.FileScheme:
   190  		if name == "" {
   191  			name = srcMetadata.Path
   192  		}
   193  		bomRef, err := artifact.IDByHash(srcMetadata.Path)
   194  		if err != nil {
   195  			log.Warnf("unable to get fingerprint of source metadata path=%s: %+v", srcMetadata.Path, err)
   196  		}
   197  		return &cyclonedx.Component{
   198  			BOMRef: string(bomRef),
   199  			Type:   cyclonedx.ComponentTypeFile,
   200  			Name:   name,
   201  		}
   202  	}
   203  
   204  	return nil
   205  }