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