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 }