github.com/anchore/syft@v1.38.2/syft/format/common/spdxhelpers/to_format_model.go (about)

     1  //nolint:gosec // sha1 is used as a required hash function for SPDX, not a crypto function
     2  package spdxhelpers
     3  
     4  import (
     5  	"crypto/sha1"
     6  	"fmt"
     7  	"path"
     8  	"slices"
     9  	"sort"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/distribution/reference"
    14  	"github.com/spdx/tools-golang/spdx"
    15  
    16  	"github.com/anchore/packageurl-go"
    17  	"github.com/anchore/syft/internal/log"
    18  	"github.com/anchore/syft/internal/mimetype"
    19  	"github.com/anchore/syft/internal/relationship"
    20  	"github.com/anchore/syft/internal/spdxlicense"
    21  	"github.com/anchore/syft/syft/artifact"
    22  	"github.com/anchore/syft/syft/file"
    23  	formatInternal "github.com/anchore/syft/syft/format/internal"
    24  	"github.com/anchore/syft/syft/format/internal/spdxutil/helpers"
    25  	"github.com/anchore/syft/syft/pkg"
    26  	"github.com/anchore/syft/syft/sbom"
    27  	"github.com/anchore/syft/syft/source"
    28  )
    29  
    30  const (
    31  	noAssertion = "NOASSERTION"
    32  
    33  	spdxPrimaryPurposeContainer = "CONTAINER"
    34  	spdxPrimaryPurposeFile      = "FILE"
    35  	spdxPrimaryPurposeOther     = "OTHER"
    36  
    37  	prefixImage     = "Image"
    38  	prefixDirectory = "Directory"
    39  	prefixFile      = "File"
    40  	prefixSnap      = "Snap"
    41  	prefixUnknown   = "Unknown"
    42  )
    43  
    44  // ToFormatModel creates and populates a new SPDX document struct that follows the SPDX 2.3
    45  // spec from the given SBOM model.
    46  //
    47  //nolint:funlen
    48  func ToFormatModel(s sbom.SBOM) *spdx.Document {
    49  	name, namespace := helpers.DocumentNameAndNamespace(s.Source, s.Descriptor)
    50  
    51  	rels := relationship.NewIndex(s.Relationships...)
    52  	packages, otherLicenses := toPackages(rels, s.Artifacts.Packages, s)
    53  
    54  	allRelationships := toRelationships(rels.All())
    55  
    56  	// for valid SPDX we need a document describes relationship
    57  	describesID := spdx.ElementID("DOCUMENT")
    58  
    59  	rootPackage := toRootPackage(s.Source)
    60  	if rootPackage != nil {
    61  		describesID = rootPackage.PackageSPDXIdentifier
    62  
    63  		// add all relationships from the document root to all other packages
    64  		allRelationships = append(allRelationships, toRootRelationships(rootPackage, packages)...)
    65  
    66  		// append the root package
    67  		packages = append(packages, rootPackage)
    68  	}
    69  
    70  	// add a relationship for the package the document describes
    71  	documentDescribesRelationship := &spdx.Relationship{
    72  		RefA: spdx.DocElementID{
    73  			ElementRefID: "DOCUMENT",
    74  		},
    75  		Relationship: string(helpers.DescribesRelationship),
    76  		RefB: spdx.DocElementID{
    77  			ElementRefID: describesID,
    78  		},
    79  	}
    80  
    81  	// add the root document relationship
    82  	allRelationships = append(allRelationships, documentDescribesRelationship)
    83  
    84  	return &spdx.Document{
    85  		// 6.1: SPDX Version; should be in the format "SPDX-x.x"
    86  		// Cardinality: mandatory, one
    87  		SPDXVersion: spdx.Version,
    88  
    89  		// 6.2: Data License; should be "CC0-1.0"
    90  		// Cardinality: mandatory, one
    91  		DataLicense: spdx.DataLicense,
    92  
    93  		// 6.3: SPDX Identifier; should be "DOCUMENT" to represent mandatory identifier of SPDXRef-DOCUMENT
    94  		// Cardinality: mandatory, one
    95  		SPDXIdentifier: "DOCUMENT",
    96  
    97  		// 6.4: Document Name
    98  		// Cardinality: mandatory, one
    99  		DocumentName: name,
   100  
   101  		// 6.5: Document Namespace
   102  		// Cardinality: mandatory, one
   103  		// Purpose: Provide an SPDX document specific namespace as a unique absolute Uniform Resource
   104  		// Identifier (URI) as specified in RFC-3986, with the exception of the ‘#’ delimiter. The SPDX
   105  		// Document URI cannot contain a URI "part" (e.g. the "#" character), since the ‘#’ is used in SPDX
   106  		// element URIs (packages, files, snippets, etc) to separate the document namespace from the
   107  		// element’s SPDX identifier. Additionally, a scheme (e.g. “https:”) is required.
   108  
   109  		// The URI must be unique for the SPDX document including the specific version of the SPDX document.
   110  		// If the SPDX document is updated, thereby creating a new version, a new URI for the updated
   111  		// document must be used. There can only be one URI for an SPDX document and only one SPDX document
   112  		// for a given URI.
   113  
   114  		// Note that the URI does not have to be accessible. It is only intended to provide a unique ID.
   115  		// In many cases, the URI will point to a web accessible document, but this should not be assumed
   116  		// to be the case.
   117  
   118  		DocumentNamespace: namespace,
   119  
   120  		// 6.6: External Document References
   121  		// Cardinality: optional, one or many
   122  		ExternalDocumentReferences: nil,
   123  
   124  		// 6.11: Document Comment
   125  		// Cardinality: optional, one
   126  		DocumentComment: "",
   127  
   128  		CreationInfo: &spdx.CreationInfo{
   129  			// 6.7: License List Version
   130  			// Cardinality: optional, one
   131  			LicenseListVersion: trimPatchVersion(spdxlicense.Version),
   132  
   133  			// 6.8: Creators: may have multiple keys for Person, Organization
   134  			//      and/or Tool
   135  			// Cardinality: mandatory, one or many
   136  			Creators: []spdx.Creator{
   137  				{
   138  					Creator:     "Anchore, Inc",
   139  					CreatorType: "Organization",
   140  				},
   141  				{
   142  					Creator:     s.Descriptor.Name + "-" + s.Descriptor.Version,
   143  					CreatorType: "Tool",
   144  				},
   145  			},
   146  
   147  			// 6.9: Created: data format YYYY-MM-DDThh:mm:ssZ
   148  			// Cardinality: mandatory, one
   149  			Created: time.Now().UTC().Format(time.RFC3339),
   150  
   151  			// 6.10: Creator Comment
   152  			// Cardinality: optional, one
   153  			CreatorComment: "",
   154  		},
   155  		Packages:      packages,
   156  		Files:         toFiles(s),
   157  		Relationships: allRelationships,
   158  		OtherLicenses: convertOtherLicense(otherLicenses),
   159  	}
   160  }
   161  
   162  func toRootRelationships(rootPackage *spdx.Package, packages []*spdx.Package) (out []*spdx.Relationship) {
   163  	for _, p := range packages {
   164  		out = append(out, &spdx.Relationship{
   165  			RefA: spdx.DocElementID{
   166  				ElementRefID: rootPackage.PackageSPDXIdentifier,
   167  			},
   168  			Relationship: string(helpers.ContainsRelationship),
   169  			RefB: spdx.DocElementID{
   170  				ElementRefID: p.PackageSPDXIdentifier,
   171  			},
   172  		})
   173  	}
   174  	return
   175  }
   176  
   177  //nolint:funlen
   178  func toRootPackage(s source.Description) *spdx.Package {
   179  	var prefix string
   180  
   181  	name := s.Name
   182  	version := s.Version
   183  
   184  	var purl *packageurl.PackageURL
   185  	purpose := ""
   186  	var checksums []spdx.Checksum
   187  	switch m := s.Metadata.(type) {
   188  	case source.ImageMetadata:
   189  		prefix = prefixImage
   190  		purpose = spdxPrimaryPurposeContainer
   191  
   192  		qualifiers := packageurl.Qualifiers{
   193  			{
   194  				Key:   "arch",
   195  				Value: m.Architecture,
   196  			},
   197  		}
   198  
   199  		ref, _ := reference.Parse(m.UserInput)
   200  		if ref, ok := ref.(reference.NamedTagged); ok {
   201  			qualifiers = append(qualifiers, packageurl.Qualifier{
   202  				Key:   "tag",
   203  				Value: ref.Tag(),
   204  			})
   205  		}
   206  
   207  		c := toChecksum(m.ManifestDigest)
   208  		if c != nil {
   209  			checksums = append(checksums, *c)
   210  			purl = &packageurl.PackageURL{
   211  				Type:       "oci",
   212  				Name:       s.Name,
   213  				Version:    m.ManifestDigest,
   214  				Qualifiers: qualifiers,
   215  			}
   216  		}
   217  
   218  	case source.DirectoryMetadata:
   219  		prefix = prefixDirectory
   220  		purpose = spdxPrimaryPurposeFile
   221  
   222  	case source.FileMetadata:
   223  		prefix = prefixFile
   224  		purpose = spdxPrimaryPurposeFile
   225  
   226  		for _, d := range m.Digests {
   227  			checksums = append(checksums, spdx.Checksum{
   228  				Algorithm: toChecksumAlgorithm(d.Algorithm),
   229  				Value:     d.Value,
   230  			})
   231  		}
   232  
   233  	case source.SnapMetadata:
   234  		prefix = prefixSnap
   235  		purpose = spdxPrimaryPurposeContainer
   236  
   237  		for _, d := range m.Digests {
   238  			checksums = append(checksums, spdx.Checksum{
   239  				Algorithm: toChecksumAlgorithm(d.Algorithm),
   240  				Value:     d.Value,
   241  			})
   242  		}
   243  
   244  	default:
   245  		prefix = prefixUnknown
   246  		purpose = spdxPrimaryPurposeOther
   247  
   248  		if name == "" {
   249  			name = s.ID
   250  		}
   251  	}
   252  
   253  	p := &spdx.Package{
   254  		PackageName:               name,
   255  		PackageSPDXIdentifier:     spdx.ElementID(helpers.SanitizeElementID(fmt.Sprintf("DocumentRoot-%s-%s", prefix, name))),
   256  		PackageVersion:            version,
   257  		PackageChecksums:          checksums,
   258  		PackageExternalReferences: nil,
   259  		PrimaryPackagePurpose:     purpose,
   260  		PackageSupplier:           toSPDXSupplier(s),
   261  		PackageCopyrightText:      helpers.NOASSERTION,
   262  		PackageDownloadLocation:   helpers.NOASSERTION,
   263  		PackageLicenseConcluded:   helpers.NOASSERTION,
   264  		PackageLicenseDeclared:    helpers.NOASSERTION,
   265  	}
   266  
   267  	if purl != nil {
   268  		p.PackageExternalReferences = []*spdx.PackageExternalReference{
   269  			{
   270  				Category: string(helpers.PackageManagerReferenceCategory),
   271  				RefType:  string(helpers.PurlExternalRefType),
   272  				Locator:  purl.String(),
   273  			},
   274  		}
   275  	}
   276  
   277  	return p
   278  }
   279  
   280  func toSPDXID(identifiable artifact.Identifiable) spdx.ElementID {
   281  	id := string(identifiable.ID())
   282  	if strings.HasPrefix(id, "SPDXRef-") {
   283  		// this is already an SPDX ID, no need to change it (except for the prefix)
   284  		return spdx.ElementID(helpers.SanitizeElementID(strings.TrimPrefix(id, "SPDXRef-")))
   285  	}
   286  	maxLen := 40
   287  	switch it := identifiable.(type) {
   288  	case pkg.Package:
   289  		if strings.HasPrefix(id, "Package-") {
   290  			// this is already an SPDX ID, no need to change it
   291  			return spdx.ElementID(helpers.SanitizeElementID(id))
   292  		}
   293  		switch {
   294  		case it.Type != "" && it.Name != "":
   295  			id = fmt.Sprintf("Package-%s-%s-%s", it.Type, it.Name, id)
   296  		case it.Name != "":
   297  			id = fmt.Sprintf("Package-%s-%s", it.Name, id)
   298  		case it.Type != "":
   299  			id = fmt.Sprintf("Package-%s-%s", it.Type, id)
   300  		default:
   301  			id = fmt.Sprintf("Package-%s", id)
   302  		}
   303  	case file.Coordinates:
   304  		if strings.HasPrefix(id, "File-") {
   305  			// this is already an SPDX ID, no need to change it. Note: there is no way to reach this case today
   306  			// from within syft, however, this covers future cases where the ID can be overridden
   307  			return spdx.ElementID(helpers.SanitizeElementID(id))
   308  		}
   309  		p := ""
   310  		parts := strings.Split(it.RealPath, "/")
   311  		for i := len(parts); i > 0; i-- {
   312  			part := parts[i-1]
   313  			if len(part) == 0 {
   314  				continue
   315  			}
   316  			if i < len(parts) && len(p)+len(part)+3 > maxLen {
   317  				p = "..." + p
   318  				break
   319  			}
   320  			p = path.Join(part, p)
   321  		}
   322  		id = fmt.Sprintf("File-%s-%s", p, id)
   323  	}
   324  	// NOTE: the spdx library prepend SPDXRef-, so we don't do it here
   325  	return spdx.ElementID(helpers.SanitizeElementID(id))
   326  }
   327  
   328  func toSPDXSupplier(s source.Description) *spdx.Supplier {
   329  	supplier := helpers.NOASSERTION
   330  	if s.Supplier != "" {
   331  		supplier = s.Supplier
   332  	}
   333  
   334  	supplierType := ""
   335  	if supplier != helpers.NOASSERTION {
   336  		supplierType = helpers.SUPPLIERORG
   337  	}
   338  
   339  	return &spdx.Supplier{
   340  		Supplier:     supplier,
   341  		SupplierType: supplierType,
   342  	}
   343  }
   344  
   345  // packages populates all Package Information from the package Collection (see https://spdx.github.io/spdx-spec/3-package-information/)
   346  //
   347  //nolint:funlen
   348  func toPackages(rels *relationship.Index, catalog *pkg.Collection, sbom sbom.SBOM) (results []*spdx.Package, otherLicenses []spdx.OtherLicense) {
   349  	otherLicenseSet := helpers.NewSPDXOtherLicenseSet()
   350  	for _, p := range catalog.Sorted() {
   351  		// name should be guaranteed to be unique but semantically useful and stable
   352  		id := toSPDXID(p)
   353  
   354  		// If the Concluded License is not the same as the Declared License, a written explanation should be provided
   355  		// in the Comments on License field (section 7.16). With respect to NOASSERTION, a written explanation in
   356  		// the Comments on License field (section 7.16) is preferred.
   357  		// extract these correctly to the spdx license format
   358  		concluded, declared, ol := helpers.License(p)
   359  		otherLicenseSet.Add(ol...)
   360  
   361  		// two ways to get filesAnalyzed == true:
   362  		// 1. syft has generated a sha1 digest for the package itself - usually in the java cataloger
   363  		// 2. syft has generated a sha1 digest for the package's contents
   364  		packageChecksums, filesAnalyzed := toPackageChecksums(p)
   365  
   366  		packageVerificationCode := newPackageVerificationCode(rels, p, sbom)
   367  		if packageVerificationCode != nil {
   368  			filesAnalyzed = true
   369  		}
   370  
   371  		// invalid SPDX document state
   372  		if filesAnalyzed && packageVerificationCode == nil {
   373  			// this is an invalid document state
   374  			// we reset the filesAnalyzed flag to false to avoid
   375  			// cases where a package digest was generated but there was
   376  			// not enough metadata to generate a verification code regarding the files
   377  			filesAnalyzed = false
   378  		}
   379  
   380  		results = append(results, &spdx.Package{
   381  			// NOT PART OF SPEC
   382  			// flag: does this "package" contain files that were in fact "unpackaged",
   383  			// e.g. included directly in the Document without being in a Package?
   384  			IsUnpackaged: false,
   385  
   386  			// 7.1: Package Name
   387  			// Cardinality: mandatory, one
   388  			PackageName: p.Name,
   389  
   390  			// 7.2: Package SPDX Identifier: "SPDXRef-[idstring]"
   391  			// Cardinality: mandatory, one
   392  			PackageSPDXIdentifier: id,
   393  
   394  			// 7.3: Package Version
   395  			// Cardinality: optional, one
   396  			PackageVersion: p.Version,
   397  
   398  			// 7.4: Package File Name
   399  			// Cardinality: optional, one
   400  			PackageFileName: "",
   401  
   402  			// 7.5: Package Supplier: may have single result for either Person or Organization,
   403  			//                        or NOASSERTION
   404  			// Cardinality: optional, one
   405  
   406  			// 7.6: Package Originator: may have single result for either Person or Organization,
   407  			//                          or NOASSERTION
   408  			// Cardinality: optional, one
   409  			PackageSupplier: toPackageSupplier(p, sbom.Source.Supplier),
   410  
   411  			PackageOriginator: toPackageOriginator(p),
   412  
   413  			// 7.7: Package Download Location
   414  			// Cardinality: mandatory, one
   415  			// NONE if there is no download location whatsoever.
   416  			// NOASSERTION if:
   417  			//   (i) the SPDX file creator has attempted to but cannot reach a reasonable objective determination;
   418  			//   (ii) the SPDX file creator has made no attempt to determine this field; or
   419  			//   (iii) the SPDX file creator has intentionally provided no information (no meaning should be implied by doing so).
   420  			PackageDownloadLocation: helpers.DownloadLocation(p),
   421  
   422  			// 7.8: FilesAnalyzed
   423  			// Cardinality: optional, one; default value is "true" if omitted
   424  
   425  			// Purpose: Indicates whether the file content of this package has been available for or subjected to
   426  			// analysis when creating the SPDX document. If false, indicates packages that represent metadata or
   427  			// URI references to a project, product, artifact, distribution or a component. If false, the package
   428  			// must not contain any files.
   429  
   430  			// Intent: A package can refer to a project, product, artifact, distribution or a component that is
   431  			// external to the SPDX document.
   432  			FilesAnalyzed: filesAnalyzed,
   433  			// NOT PART OF SPEC: did FilesAnalyzed tag appear?
   434  			IsFilesAnalyzedTagPresent: true,
   435  
   436  			// 7.9: Package Verification Code
   437  			// Cardinality: optional, one if filesAnalyzed is true / omitted;
   438  			//              zero (must be omitted) if filesAnalyzed is false
   439  			PackageVerificationCode: packageVerificationCode,
   440  
   441  			// 7.10: Package Checksum: may have keys for SHA1, SHA256 and/or MD5
   442  			// Cardinality: optional, one or many
   443  
   444  			// 7.10.1 Purpose: Provide an independently reproducible mechanism that permits unique identification of
   445  			// a specific package that correlates to the data in this SPDX file. This identifier enables a recipient
   446  			// to determine if any file in the original package has been changed. If the SPDX file is to be included
   447  			// in a package, this value should not be calculated. The SHA-1 algorithm will be used to provide the
   448  			// checksum by default.
   449  			PackageChecksums: packageChecksums,
   450  
   451  			// 7.11: Package Home Page
   452  			// Cardinality: optional, one
   453  			PackageHomePage: helpers.Homepage(p),
   454  
   455  			// 7.12: Source Information
   456  			// Cardinality: optional, one
   457  			PackageSourceInfo: helpers.SourceInfo(p),
   458  
   459  			// 7.13: Concluded License: SPDX License Expression, "NONE" or "NOASSERTION"
   460  			// Cardinality: mandatory, one
   461  			// Purpose: Contain the license the SPDX file creator has concluded as governing the
   462  			// package or alternative values, if the governing license cannot be determined.
   463  			PackageLicenseConcluded: concluded,
   464  
   465  			// 7.14: All Licenses Info from Files: SPDX License Expression, "NONE" or "NOASSERTION"
   466  			// Cardinality: mandatory, one or many if filesAnalyzed is true / omitted;
   467  			//              zero (must be omitted) if filesAnalyzed is false
   468  			PackageLicenseInfoFromFiles: nil,
   469  
   470  			// 7.15: Declared License: SPDX License Expression, "NONE" or "NOASSERTION"
   471  			// Cardinality: mandatory, one
   472  			// Purpose: List the licenses that have been declared by the authors of the package.
   473  			// Any license information that does not originate from the package authors, e.g. license
   474  			// information from a third party repository, should not be included in this field.
   475  			PackageLicenseDeclared: declared,
   476  
   477  			// 7.16: Comments on License
   478  			// Cardinality: optional, one
   479  			PackageLicenseComments: "",
   480  
   481  			// 7.17: Copyright Text: copyright notice(s) text, "NONE" or "NOASSERTION"
   482  			// Cardinality: mandatory, one
   483  			// Purpose: IdentifyFormat the copyright holders of the package, as well as any dates present. This will be a free form text field extracted from package information files. The options to populate this field are limited to:
   484  			//
   485  			// Any text related to a copyright notice, even if not complete;
   486  			// NONE if the package contains no copyright information whatsoever; or
   487  			// NOASSERTION, if
   488  			//   (i) the SPDX document creator has made no attempt to determine this field; or
   489  			//   (ii) the SPDX document creator has intentionally provided no information (no meaning should be implied by doing so).
   490  			//
   491  			PackageCopyrightText: noAssertion,
   492  
   493  			// 7.18: Package Summary Description
   494  			// Cardinality: optional, one
   495  			PackageSummary: "",
   496  
   497  			// 7.19: Package Detailed Description
   498  			// Cardinality: optional, one
   499  			PackageDescription: helpers.Description(p),
   500  
   501  			// 7.20: Package Comment
   502  			// Cardinality: optional, one
   503  			PackageComment: "",
   504  
   505  			// 7.21: Package External Reference
   506  			// Cardinality: optional, one or many
   507  			PackageExternalReferences: formatSPDXExternalRefs(p),
   508  
   509  			// 7.22: Package External Reference Comment
   510  			// Cardinality: conditional (optional, one) for each External Reference
   511  			// contained within PackageExternalReference2_1 struct, if present
   512  
   513  			// 7.23: Package Attribution Text
   514  			// Cardinality: optional, one or many
   515  			PackageAttributionTexts: nil,
   516  		})
   517  	}
   518  	return results, otherLicenseSet.ToSlice()
   519  }
   520  
   521  func toPackageChecksums(p pkg.Package) ([]spdx.Checksum, bool) {
   522  	filesAnalyzed := false
   523  	var checksums []spdx.Checksum
   524  	switch meta := p.Metadata.(type) {
   525  	// we generate digest for some Java packages
   526  	// spdx.github.io/spdx-spec/package-information/#710-package-checksum-field
   527  	case pkg.JavaArchive:
   528  		// if syft has generated the digest here then filesAnalyzed is true
   529  		if len(meta.ArchiveDigests) > 0 {
   530  			filesAnalyzed = true
   531  			for _, digest := range meta.ArchiveDigests {
   532  				algo := strings.ToUpper(digest.Algorithm)
   533  				checksums = append(checksums, spdx.Checksum{
   534  					Algorithm: spdx.ChecksumAlgorithm(algo),
   535  					Value:     digest.Value,
   536  				})
   537  			}
   538  		}
   539  	case pkg.GolangBinaryBuildinfoEntry:
   540  		// because the H1 digest is found in the Golang metadata we cannot claim that the files were analyzed
   541  		algo, hexStr, err := helpers.HDigestToSHA(meta.H1Digest)
   542  		if err != nil {
   543  			log.Debugf("invalid h1digest: %s: %v", meta.H1Digest, err)
   544  			break
   545  		}
   546  		algo = strings.ToUpper(algo)
   547  		checksums = append(checksums, spdx.Checksum{
   548  			Algorithm: spdx.ChecksumAlgorithm(algo),
   549  			Value:     hexStr,
   550  		})
   551  	case pkg.OpamPackage:
   552  		for _, checksum := range meta.Checksums {
   553  			parts := strings.Split(checksum, "=")
   554  			checksums = append(checksums, spdx.Checksum{
   555  				Algorithm: spdx.ChecksumAlgorithm(strings.ToUpper(parts[0])),
   556  				Value:     parts[1],
   557  			})
   558  		}
   559  	}
   560  	return checksums, filesAnalyzed
   561  }
   562  
   563  func toPackageOriginator(p pkg.Package) *spdx.Originator {
   564  	kind, originator := helpers.Originator(p)
   565  	if kind == "" || originator == "" {
   566  		return nil
   567  	}
   568  	return &spdx.Originator{
   569  		Originator:     originator,
   570  		OriginatorType: kind,
   571  	}
   572  }
   573  
   574  func toPackageSupplier(p pkg.Package, sbomSupplier string) *spdx.Supplier {
   575  	kind, supplier := helpers.Supplier(p)
   576  	if kind == "" || supplier == "" {
   577  		supplier := helpers.NOASSERTION
   578  		supplierType := ""
   579  		if sbomSupplier != "" {
   580  			supplier = sbomSupplier
   581  			supplierType = helpers.SUPPLIERORG
   582  		}
   583  		return &spdx.Supplier{
   584  			Supplier:     supplier,
   585  			SupplierType: supplierType,
   586  		}
   587  	}
   588  	return &spdx.Supplier{
   589  		Supplier:     supplier,
   590  		SupplierType: kind,
   591  	}
   592  }
   593  
   594  func formatSPDXExternalRefs(p pkg.Package) (refs []*spdx.PackageExternalReference) {
   595  	for _, ref := range helpers.ExternalRefs(p) {
   596  		refs = append(refs, &spdx.PackageExternalReference{
   597  			Category:           string(ref.ReferenceCategory),
   598  			RefType:            string(ref.ReferenceType),
   599  			Locator:            ref.ReferenceLocator,
   600  			ExternalRefComment: ref.Comment,
   601  		})
   602  	}
   603  	return refs
   604  }
   605  
   606  func toRelationships(relationships []artifact.Relationship) (result []*spdx.Relationship) {
   607  	for _, r := range relationships {
   608  		exists, relationshipType, comment := lookupRelationship(r.Type)
   609  
   610  		if !exists {
   611  			log.Debugf("unable to convert relationship to SPDX, dropping: %+v", r)
   612  			continue
   613  		}
   614  
   615  		// FIXME: we are only currently including Package -> * relationships
   616  		if _, ok := r.From.(pkg.Package); !ok {
   617  			log.Debugf("skipping non-package relationship: %+v", r)
   618  			continue
   619  		}
   620  
   621  		result = append(result, &spdx.Relationship{
   622  			RefA: spdx.DocElementID{
   623  				ElementRefID: toSPDXID(r.From),
   624  			},
   625  			Relationship: string(relationshipType),
   626  			RefB: spdx.DocElementID{
   627  				ElementRefID: toSPDXID(r.To),
   628  			},
   629  			RelationshipComment: comment,
   630  		})
   631  	}
   632  	return result
   633  }
   634  
   635  func lookupRelationship(ty artifact.RelationshipType) (bool, helpers.RelationshipType, string) {
   636  	switch ty {
   637  	case artifact.ContainsRelationship:
   638  		return true, helpers.ContainsRelationship, ""
   639  	case artifact.DependencyOfRelationship:
   640  		return true, helpers.DependencyOfRelationship, ""
   641  	case artifact.OwnershipByFileOverlapRelationship:
   642  		return true, helpers.OtherRelationship, fmt.Sprintf("%s: indicates that the parent package claims ownership of a child package since the parent metadata indicates overlap with a location that a cataloger found the child package by", ty)
   643  	case artifact.EvidentByRelationship:
   644  		return true, helpers.OtherRelationship, fmt.Sprintf("%s: indicates the package's existence is evident by the given file", ty)
   645  	}
   646  	return false, "", ""
   647  }
   648  
   649  func toFiles(s sbom.SBOM) (results []*spdx.File) {
   650  	artifacts := s.Artifacts
   651  
   652  	_, coordinateSorter := formatInternal.GetLocationSorters(s)
   653  
   654  	coordinates := s.AllCoordinates()
   655  	slices.SortFunc(coordinates, coordinateSorter)
   656  
   657  	for _, c := range coordinates {
   658  		var metadata *file.Metadata
   659  		if metadataForLocation, exists := artifacts.FileMetadata[c]; exists {
   660  			metadata = &metadataForLocation
   661  		}
   662  
   663  		var digests []file.Digest
   664  		if digestsForLocation, exists := artifacts.FileDigests[c]; exists {
   665  			digests = digestsForLocation
   666  		}
   667  
   668  		// if we don't have any metadata or digests for this location
   669  		// then the file is most likely a symlink or non-regular file
   670  		// for now we include a 0 sha1 digest as requested by the spdx spec
   671  		// TODO: update location code in core SBOM so that we can map complex links
   672  		// back to their real file digest location.
   673  		if len(digests) == 0 {
   674  			digests = append(digests, file.Digest{Algorithm: "sha1", Value: "0000000000000000000000000000000000000000"})
   675  		}
   676  
   677  		// TODO: add file classifications (?) and content as a snippet
   678  
   679  		var comment string
   680  		if c.FileSystemID != "" {
   681  			comment = fmt.Sprintf("layerID: %s", c.FileSystemID)
   682  		}
   683  
   684  		relativePath, err := convertAbsoluteToRelative(c.RealPath)
   685  		if err != nil {
   686  			log.Debugf("unable to convert relative path '%s' to absolute path: %s", c.RealPath, err)
   687  			relativePath = c.RealPath
   688  		}
   689  
   690  		results = append(results, &spdx.File{
   691  			FileSPDXIdentifier: toSPDXID(c),
   692  			FileComment:        comment,
   693  			// required, no attempt made to determine license information
   694  			LicenseConcluded:  noAssertion,
   695  			FileCopyrightText: noAssertion,
   696  			Checksums:         toFileChecksums(digests),
   697  			FileName:          relativePath,
   698  			FileTypes:         toFileTypes(metadata),
   699  			LicenseInfoInFiles: []string{ // required in SPDX 2.2
   700  				helpers.NOASSERTION,
   701  			},
   702  		})
   703  	}
   704  
   705  	// sort by real path then virtual path to ensure the result is stable across multiple runs
   706  	sort.SliceStable(results, func(i, j int) bool {
   707  		if results[i].FileName == results[j].FileName {
   708  			return results[i].FileSPDXIdentifier < results[j].FileSPDXIdentifier
   709  		}
   710  		return results[i].FileName < results[j].FileName
   711  	})
   712  	return results
   713  }
   714  
   715  func toFileChecksums(digests []file.Digest) (checksums []spdx.Checksum) {
   716  	checksums = make([]spdx.Checksum, 0, len(digests))
   717  	for _, digest := range digests {
   718  		checksums = append(checksums, spdx.Checksum{
   719  			Algorithm: toChecksumAlgorithm(digest.Algorithm),
   720  			Value:     digest.Value,
   721  		})
   722  	}
   723  	return checksums
   724  }
   725  
   726  // toChecksum takes a checksum in the format <algorithm>:<hash> and returns an spdx.Checksum or nil if the string is invalid
   727  func toChecksum(algorithmHash string) *spdx.Checksum {
   728  	parts := strings.Split(algorithmHash, ":")
   729  	if len(parts) < 2 {
   730  		return nil
   731  	}
   732  	return &spdx.Checksum{
   733  		Algorithm: toChecksumAlgorithm(parts[0]),
   734  		Value:     parts[1],
   735  	}
   736  }
   737  
   738  func toChecksumAlgorithm(algorithm string) spdx.ChecksumAlgorithm {
   739  	// this needs to be an uppercase version of our algorithm
   740  	return spdx.ChecksumAlgorithm(strings.ToUpper(algorithm))
   741  }
   742  
   743  func toFileTypes(metadata *file.Metadata) (ty []string) {
   744  	if metadata == nil {
   745  		return nil
   746  	}
   747  
   748  	mimeTypePrefix := strings.Split(metadata.MIMEType, "/")[0]
   749  	switch mimeTypePrefix {
   750  	case "image":
   751  		ty = append(ty, string(helpers.ImageFileType))
   752  	case "video":
   753  		ty = append(ty, string(helpers.VideoFileType))
   754  	case "application":
   755  		ty = append(ty, string(helpers.ApplicationFileType))
   756  	case "text":
   757  		ty = append(ty, string(helpers.TextFileType))
   758  	case "audio":
   759  		ty = append(ty, string(helpers.AudioFileType))
   760  	}
   761  
   762  	if mimetype.IsExecutable(metadata.MIMEType) {
   763  		ty = append(ty, string(helpers.BinaryFileType))
   764  	}
   765  
   766  	if mimetype.IsArchive(metadata.MIMEType) {
   767  		ty = append(ty, string(helpers.ArchiveFileType))
   768  	}
   769  
   770  	// TODO: add support for source, spdx, and documentation file types
   771  	if len(ty) == 0 {
   772  		ty = append(ty, string(helpers.OtherFileType))
   773  	}
   774  
   775  	return ty
   776  }
   777  
   778  // TODO: handle SPDX excludes file case
   779  // f file is an "excludes" file, skip it /* exclude SPDX analysis file(s) */
   780  // see: https://spdx.github.io/spdx-spec/v2.3/package-information/#79-package-verification-code-field
   781  // the above link contains the SPDX algorithm for a package verification code
   782  func newPackageVerificationCode(rels *relationship.Index, p pkg.Package, sbom sbom.SBOM) *spdx.PackageVerificationCode {
   783  	// key off of the spdx contains relationship;
   784  	// spdx validator will fail if a package claims to contain a file, but no sha1 provided
   785  	// if a sha1 for a file is provided, then the validator will fail if the package does not have
   786  	// a package verification code
   787  	coordinates := rels.Coordinates(p, artifact.ContainsRelationship)
   788  	var digests []file.Digest
   789  	for _, c := range coordinates {
   790  		digest := sbom.Artifacts.FileDigests[c]
   791  		if len(digest) == 0 {
   792  			continue
   793  		}
   794  
   795  		var d file.Digest
   796  		for _, digest := range digest {
   797  			if digest.Algorithm == "sha1" {
   798  				d = digest
   799  				break
   800  			}
   801  		}
   802  		digests = append(digests, d)
   803  	}
   804  
   805  	if len(digests) == 0 {
   806  		return nil
   807  	}
   808  
   809  	// sort templist in ascending order by SHA1 value
   810  	sort.SliceStable(digests, func(i, j int) bool {
   811  		return digests[i].Value < digests[j].Value
   812  	})
   813  
   814  	// filelist = templist with "/n"s removed. /* ordered sequence of SHA1 values with no separators
   815  	var b strings.Builder
   816  	for _, digest := range digests {
   817  		b.WriteString(digest.Value)
   818  	}
   819  
   820  	//nolint:gosec
   821  	hasher := sha1.New()
   822  	_, _ = hasher.Write([]byte(b.String()))
   823  	return &spdx.PackageVerificationCode{
   824  		// 7.9.1: Package Verification Code Value
   825  		// Cardinality: mandatory, one
   826  		Value: fmt.Sprintf("%+x", hasher.Sum(nil)),
   827  	}
   828  }
   829  
   830  // SPDX 2.2 spec requires that the patch version be removed from the semver string
   831  // for the license list version field
   832  func trimPatchVersion(semver string) string {
   833  	parts := strings.Split(semver, ".")
   834  	if len(parts) >= 3 {
   835  		return strings.Join(parts[:2], ".")
   836  	}
   837  	return semver
   838  }
   839  
   840  // spdx requires that the file name field is a relative filename
   841  // with the root of the package archive or directory
   842  func convertAbsoluteToRelative(absPath string) (string, error) {
   843  	// Ensure the absolute path is absolute (although it should already be)
   844  	if !path.IsAbs(absPath) {
   845  		// already relative
   846  		log.Debugf("%s is already relative", absPath)
   847  		return absPath, nil
   848  	}
   849  
   850  	// we use "/" here given that we're converting absolute paths from root to relative
   851  	relPath, found := strings.CutPrefix(absPath, "/")
   852  	if !found {
   853  		return "", fmt.Errorf("error calculating relative path: %s", absPath)
   854  	}
   855  
   856  	return relPath, nil
   857  }
   858  
   859  func convertOtherLicense(otherLicenses []spdx.OtherLicense) []*spdx.OtherLicense {
   860  	if len(otherLicenses) == 0 {
   861  		return nil
   862  	}
   863  
   864  	result := make([]*spdx.OtherLicense, 0, len(otherLicenses))
   865  	for i := range otherLicenses {
   866  		result = append(result, &otherLicenses[i])
   867  	}
   868  	return result
   869  }