github.com/anchore/syft@v1.4.2-0.20240516191711-1bec1fc5d397/syft/format/common/cyclonedxhelpers/to_format_model.go (about)

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