github.com/google/osv-scalibr@v0.4.1/converter/spdx/spdx.go (about) 1 // Copyright 2025 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package spdx provides utilities for creating SPDX SBOMs. 16 package spdx 17 18 import ( 19 "fmt" 20 "regexp" 21 "time" 22 23 "bitbucket.org/creachadair/stringset" 24 "github.com/google/osv-scalibr/log" 25 "github.com/google/osv-scalibr/result" 26 "github.com/google/uuid" 27 "github.com/spdx/tools-golang/spdx/v2/common" 28 "github.com/spdx/tools-golang/spdx/v2/v2_3" 29 ) 30 31 const ( 32 // NoAssertion indicates that we don't claim anything about the value of a given field. 33 NoAssertion = "NOASSERTION" 34 // SPDXRefPrefix is the prefix used in reference IDs in the SPDX document. 35 SPDXRefPrefix = "SPDXRef-" 36 // SPDXDocumentID is the string identifier used to refer to the SPDX document. 37 SPDXDocumentID = "SPDXRef-DOCUMENT" 38 ) 39 40 // spdx_id must only contain letters, numbers, "." and "-" 41 var spdxIDInvalidCharRe = regexp.MustCompile(`[^a-zA-Z0-9.-]`) 42 43 // Config describes custom settings that should be applied to the generated SPDX file. 44 type Config struct { 45 DocumentName string 46 DocumentNamespace string 47 Creators []common.Creator 48 } 49 50 // ToSPDX23 converts the SCALIBR scan results into an SPDX v2.3 document. 51 func ToSPDX23(r *result.ScanResult, c Config) *v2_3.Document { 52 packages := make([]*v2_3.Package, 0, len(r.Inventory.Packages)+1) 53 54 // Add a main package that contains all other top-level packages. 55 mainPackageID := SPDXRefPrefix + "Package-main-" + uuid.New().String() 56 packages = append(packages, &v2_3.Package{ 57 PackageName: "main", 58 PackageSPDXIdentifier: common.ElementID(mainPackageID), 59 PackageVersion: "0", 60 PackageSupplier: &common.Supplier{ 61 Supplier: NoAssertion, 62 SupplierType: NoAssertion, 63 }, 64 PackageDownloadLocation: NoAssertion, 65 IsFilesAnalyzedTagPresent: false, 66 }) 67 68 relationships := make([]*v2_3.Relationship, 0, 1+2*len(r.Inventory.Packages)) 69 relationships = append(relationships, &v2_3.Relationship{ 70 RefA: toDocElementID(SPDXDocumentID), 71 RefB: toDocElementID(mainPackageID), 72 Relationship: "DESCRIBES", 73 }) 74 75 allOtherLicenses := stringset.Set{} 76 77 for _, pkg := range r.Inventory.Packages { 78 p := pkg.PURL() 79 if p == nil { 80 log.Warnf("Package %v has no PURL, skipping", pkg) 81 continue 82 } 83 pName := p.Name 84 pVersion := p.Version 85 if pName == "" || pVersion == "" { 86 log.Warnf("Package %v PURL name or version empty, skipping", pkg) 87 continue 88 } 89 pID := SPDXRefPrefix + "Package-" + replaceSPDXIDInvalidChars(pName) + "-" + uuid.New().String() 90 pSourceInfo := "" 91 if len(pkg.Plugins) > 0 { 92 pSourceInfo = fmt.Sprintf("Identified by the %s extractor", pkg.Plugins[0]) 93 } 94 if len(pkg.Locations) == 1 { 95 pSourceInfo += " from " + pkg.Locations[0] 96 } else if l := len(pkg.Locations); l > 1 { 97 pSourceInfo += fmt.Sprintf(" from %d locations, including %s and %s", l, pkg.Locations[0], pkg.Locations[1]) 98 } 99 100 licensesConcluded, otherLicenses := LicenseExpression(pkg.Licenses) 101 allOtherLicenses.Update(otherLicenses) 102 103 packages = append(packages, &v2_3.Package{ 104 PackageName: pName, 105 PackageSPDXIdentifier: common.ElementID(pID), 106 PackageVersion: pVersion, 107 PackageSupplier: &common.Supplier{ 108 Supplier: NoAssertion, 109 SupplierType: NoAssertion, 110 }, 111 PackageDownloadLocation: NoAssertion, 112 PackageLicenseConcluded: licensesConcluded, 113 PackageLicenseDeclared: NoAssertion, 114 IsFilesAnalyzedTagPresent: false, 115 PackageSourceInfo: pSourceInfo, 116 PackageExternalReferences: []*v2_3.PackageExternalReference{ 117 { 118 Category: "PACKAGE-MANAGER", 119 RefType: "purl", 120 Locator: p.String(), 121 }, 122 }, 123 }) 124 // TODO(b/313658493): Add a DESCRIBES relationship or a DocumentDescribes field. 125 relationships = append(relationships, &v2_3.Relationship{ 126 RefA: toDocElementID(mainPackageID), 127 RefB: toDocElementID(pID), 128 Relationship: "CONTAINS", 129 }) 130 relationships = append(relationships, &v2_3.Relationship{ 131 RefA: toDocElementID(pID), 132 RefB: toDocElementID(NoAssertion), 133 Relationship: "CONTAINS", 134 }) 135 } 136 name := c.DocumentName 137 if name == "" { 138 name = "SCALIBR-generated SPDX" 139 } 140 namespace := c.DocumentNamespace 141 if namespace == "" { 142 namespace = "https://spdx.google/" + uuid.New().String() 143 } 144 creators := []common.Creator{ 145 { 146 CreatorType: "Tool", 147 Creator: "SCALIBR", 148 }, 149 } 150 creators = append(creators, c.Creators...) 151 return &v2_3.Document{ 152 SPDXVersion: "SPDX-2.3", 153 DataLicense: "CC0-1.0", 154 SPDXIdentifier: "DOCUMENT", 155 DocumentName: name, 156 DocumentNamespace: namespace, 157 CreationInfo: &v2_3.CreationInfo{ 158 Creators: creators, 159 Created: time.Now().UTC().Format("2006-01-02T15:04:05Z"), 160 }, 161 Packages: packages, 162 Relationships: relationships, 163 OtherLicenses: ToOtherLicenses(allOtherLicenses), 164 } 165 } 166 167 func replaceSPDXIDInvalidChars(id string) string { 168 return spdxIDInvalidCharRe.ReplaceAllString(id, "-") 169 } 170 171 func toDocElementID(id string) common.DocElementID { 172 if id == NoAssertion { 173 return common.DocElementID{ 174 SpecialID: NoAssertion, 175 } 176 } 177 return common.DocElementID{ 178 ElementRefID: common.ElementID(id), 179 } 180 }