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

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