1#!/usr/bin/env python3 2# 3# Copyright (C) 2023 The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17""" 18Serialize objects defined in package sbom_data to SPDX format: tagvalue, JSON. 19""" 20 21import json 22import sbom_data 23 24SPDX_VER = 'SPDX-2.3' 25DATA_LIC = 'CC0-1.0' 26 27 28class Tags: 29 # Common 30 SPDXID = 'SPDXID' 31 SPDX_VERSION = 'SPDXVersion' 32 DATA_LICENSE = 'DataLicense' 33 DOCUMENT_NAME = 'DocumentName' 34 DOCUMENT_NAMESPACE = 'DocumentNamespace' 35 CREATED = 'Created' 36 CREATOR = 'Creator' 37 EXTERNAL_DOCUMENT_REF = 'ExternalDocumentRef' 38 39 # Package 40 PACKAGE_NAME = 'PackageName' 41 PACKAGE_DOWNLOAD_LOCATION = 'PackageDownloadLocation' 42 PACKAGE_VERSION = 'PackageVersion' 43 PACKAGE_SUPPLIER = 'PackageSupplier' 44 FILES_ANALYZED = 'FilesAnalyzed' 45 PACKAGE_VERIFICATION_CODE = 'PackageVerificationCode' 46 PACKAGE_EXTERNAL_REF = 'ExternalRef' 47 # Package license 48 PACKAGE_LICENSE_CONCLUDED = 'PackageLicenseConcluded' 49 PACKAGE_LICENSE_INFO_FROM_FILES = 'PackageLicenseInfoFromFiles' 50 PACKAGE_LICENSE_DECLARED = 'PackageLicenseDeclared' 51 PACKAGE_LICENSE_COMMENTS = 'PackageLicenseComments' 52 53 # File 54 FILE_NAME = 'FileName' 55 FILE_CHECKSUM = 'FileChecksum' 56 # File license 57 FILE_LICENSE_CONCLUDED = 'LicenseConcluded' 58 FILE_LICENSE_INFO_IN_FILE = 'LicenseInfoInFile' 59 FILE_LICENSE_COMMENTS = 'LicenseComments' 60 FILE_COPYRIGHT_TEXT = 'FileCopyrightText' 61 FILE_NOTICE = 'FileNotice' 62 FILE_ATTRIBUTION_TEXT = 'FileAttributionText' 63 64 # Relationship 65 RELATIONSHIP = 'Relationship' 66 67 68class TagValueWriter: 69 @staticmethod 70 def marshal_doc_headers(sbom_doc): 71 headers = [ 72 f'{Tags.SPDX_VERSION}: {SPDX_VER}', 73 f'{Tags.DATA_LICENSE}: {DATA_LIC}', 74 f'{Tags.SPDXID}: {sbom_doc.id}', 75 f'{Tags.DOCUMENT_NAME}: {sbom_doc.name}', 76 f'{Tags.DOCUMENT_NAMESPACE}: {sbom_doc.namespace}', 77 ] 78 for creator in sbom_doc.creators: 79 headers.append(f'{Tags.CREATOR}: {creator}') 80 headers.append(f'{Tags.CREATED}: {sbom_doc.created}') 81 for doc_ref in sbom_doc.external_refs: 82 headers.append( 83 f'{Tags.EXTERNAL_DOCUMENT_REF}: {doc_ref.id} {doc_ref.uri} {doc_ref.checksum}') 84 headers.append('') 85 return headers 86 87 @staticmethod 88 def marshal_package(sbom_doc, package, fragment): 89 download_location = sbom_data.VALUE_NOASSERTION 90 if package.download_location: 91 download_location = package.download_location 92 tagvalues = [ 93 f'{Tags.PACKAGE_NAME}: {package.name}', 94 f'{Tags.SPDXID}: {package.id}', 95 f'{Tags.PACKAGE_DOWNLOAD_LOCATION}: {download_location}', 96 f'{Tags.FILES_ANALYZED}: {str(package.files_analyzed).lower()}', 97 ] 98 if package.version: 99 tagvalues.append(f'{Tags.PACKAGE_VERSION}: {package.version}') 100 if package.supplier: 101 tagvalues.append(f'{Tags.PACKAGE_SUPPLIER}: {package.supplier}') 102 if package.verification_code: 103 tagvalues.append(f'{Tags.PACKAGE_VERIFICATION_CODE}: {package.verification_code}') 104 if package.external_refs: 105 for external_ref in package.external_refs: 106 tagvalues.append( 107 f'{Tags.PACKAGE_EXTERNAL_REF}: {external_ref.category} {external_ref.type} {external_ref.locator}') 108 109 tagvalues.append('') 110 111 if package.id == sbom_doc.describes and not fragment: 112 tagvalues.append( 113 f'{Tags.RELATIONSHIP}: {sbom_doc.id} {sbom_data.RelationshipType.DESCRIBES} {sbom_doc.describes}') 114 tagvalues.append('') 115 116 for file in sbom_doc.files: 117 if file.id in package.file_ids: 118 tagvalues += TagValueWriter.marshal_file(file) 119 120 return tagvalues 121 122 @staticmethod 123 def marshal_packages(sbom_doc, fragment): 124 tagvalues = [] 125 marshaled_relationships = [] 126 i = 0 127 packages = sbom_doc.packages 128 while i < len(packages): 129 if (i + 1 < len(packages) 130 and packages[i].id.startswith('SPDXRef-SOURCE-') 131 and packages[i + 1].id.startswith('SPDXRef-UPSTREAM-')): 132 # Output SOURCE, UPSTREAM packages and their VARIANT_OF relationship together, so they are close to each other 133 # in SBOMs in tagvalue format. 134 tagvalues += TagValueWriter.marshal_package(sbom_doc, packages[i], fragment) 135 tagvalues += TagValueWriter.marshal_package(sbom_doc, packages[i + 1], fragment) 136 rel = next((r for r in sbom_doc.relationships if 137 r.id1 == packages[i].id and 138 r.id2 == packages[i + 1].id and 139 r.relationship == sbom_data.RelationshipType.VARIANT_OF), None) 140 if rel: 141 marshaled_relationships.append(rel) 142 tagvalues.append(TagValueWriter.marshal_relationship(rel)) 143 tagvalues.append('') 144 145 i += 2 146 else: 147 tagvalues += TagValueWriter.marshal_package(sbom_doc, packages[i], fragment) 148 i += 1 149 150 return tagvalues, marshaled_relationships 151 152 @staticmethod 153 def marshal_file(file): 154 tagvalues = [ 155 f'{Tags.FILE_NAME}: {file.name}', 156 f'{Tags.SPDXID}: {file.id}', 157 f'{Tags.FILE_CHECKSUM}: {file.checksum}', 158 '', 159 ] 160 161 return tagvalues 162 163 @staticmethod 164 def marshal_files(sbom_doc, fragment): 165 tagvalues = [] 166 files_in_packages = [] 167 for package in sbom_doc.packages: 168 files_in_packages += package.file_ids 169 for file in sbom_doc.files: 170 if file.id in files_in_packages: 171 continue 172 tagvalues += TagValueWriter.marshal_file(file) 173 if file.id == sbom_doc.describes and not fragment: 174 # Fragment is not a full SBOM document so the relationship DESCRIBES is not applicable. 175 tagvalues.append( 176 f'{Tags.RELATIONSHIP}: {sbom_doc.id} {sbom_data.RelationshipType.DESCRIBES} {sbom_doc.describes}') 177 tagvalues.append('') 178 return tagvalues 179 180 @staticmethod 181 def marshal_relationship(rel): 182 return f'{Tags.RELATIONSHIP}: {rel.id1} {rel.relationship} {rel.id2}' 183 184 @staticmethod 185 def marshal_relationships(sbom_doc, marshaled_rels): 186 tagvalues = [] 187 sorted_rels = sorted(sbom_doc.relationships, key=lambda r: r.id2 + r.id1) 188 for rel in sorted_rels: 189 if any(r.id1 == rel.id1 and r.id2 == rel.id2 and r.relationship == rel.relationship 190 for r in marshaled_rels): 191 continue 192 tagvalues.append(TagValueWriter.marshal_relationship(rel)) 193 tagvalues.append('') 194 return tagvalues 195 196 @staticmethod 197 def write(sbom_doc, file, fragment=False): 198 content = [] 199 if not fragment: 200 content += TagValueWriter.marshal_doc_headers(sbom_doc) 201 content += TagValueWriter.marshal_files(sbom_doc, fragment) 202 tagvalues, marshaled_relationships = TagValueWriter.marshal_packages(sbom_doc, fragment) 203 content += tagvalues 204 content += TagValueWriter.marshal_relationships(sbom_doc, marshaled_relationships) 205 file.write('\n'.join(content)) 206 207 208class PropNames: 209 # Common 210 SPDXID = 'SPDXID' 211 SPDX_VERSION = 'spdxVersion' 212 DATA_LICENSE = 'dataLicense' 213 NAME = 'name' 214 DOCUMENT_NAMESPACE = 'documentNamespace' 215 CREATION_INFO = 'creationInfo' 216 CREATORS = 'creators' 217 CREATED = 'created' 218 EXTERNAL_DOCUMENT_REF = 'externalDocumentRefs' 219 DOCUMENT_DESCRIBES = 'documentDescribes' 220 EXTERNAL_DOCUMENT_ID = 'externalDocumentId' 221 EXTERNAL_DOCUMENT_URI = 'spdxDocument' 222 EXTERNAL_DOCUMENT_CHECKSUM = 'checksum' 223 ALGORITHM = 'algorithm' 224 CHECKSUM_VALUE = 'checksumValue' 225 226 # Package 227 PACKAGES = 'packages' 228 PACKAGE_DOWNLOAD_LOCATION = 'downloadLocation' 229 PACKAGE_VERSION = 'versionInfo' 230 PACKAGE_SUPPLIER = 'supplier' 231 FILES_ANALYZED = 'filesAnalyzed' 232 PACKAGE_VERIFICATION_CODE = 'packageVerificationCode' 233 PACKAGE_VERIFICATION_CODE_VALUE = 'packageVerificationCodeValue' 234 PACKAGE_EXTERNAL_REFS = 'externalRefs' 235 PACKAGE_EXTERNAL_REF_CATEGORY = 'referenceCategory' 236 PACKAGE_EXTERNAL_REF_TYPE = 'referenceType' 237 PACKAGE_EXTERNAL_REF_LOCATOR = 'referenceLocator' 238 PACKAGE_HAS_FILES = 'hasFiles' 239 240 # File 241 FILES = 'files' 242 FILE_NAME = 'fileName' 243 FILE_CHECKSUMS = 'checksums' 244 245 # Relationship 246 RELATIONSHIPS = 'relationships' 247 REL_ELEMENT_ID = 'spdxElementId' 248 REL_RELATED_ELEMENT_ID = 'relatedSpdxElement' 249 REL_TYPE = 'relationshipType' 250 251 252class JSONWriter: 253 @staticmethod 254 def marshal_doc_headers(sbom_doc): 255 headers = { 256 PropNames.SPDX_VERSION: SPDX_VER, 257 PropNames.DATA_LICENSE: DATA_LIC, 258 PropNames.SPDXID: sbom_doc.id, 259 PropNames.NAME: sbom_doc.name, 260 PropNames.DOCUMENT_NAMESPACE: sbom_doc.namespace, 261 PropNames.CREATION_INFO: {} 262 } 263 creators = [creator for creator in sbom_doc.creators] 264 headers[PropNames.CREATION_INFO][PropNames.CREATORS] = creators 265 headers[PropNames.CREATION_INFO][PropNames.CREATED] = sbom_doc.created 266 external_refs = [] 267 for doc_ref in sbom_doc.external_refs: 268 checksum = doc_ref.checksum.split(': ') 269 external_refs.append({ 270 PropNames.EXTERNAL_DOCUMENT_ID: f'{doc_ref.id}', 271 PropNames.EXTERNAL_DOCUMENT_URI: doc_ref.uri, 272 PropNames.EXTERNAL_DOCUMENT_CHECKSUM: { 273 PropNames.ALGORITHM: checksum[0], 274 PropNames.CHECKSUM_VALUE: checksum[1] 275 } 276 }) 277 if external_refs: 278 headers[PropNames.EXTERNAL_DOCUMENT_REF] = external_refs 279 headers[PropNames.DOCUMENT_DESCRIBES] = [sbom_doc.describes] 280 281 return headers 282 283 @staticmethod 284 def marshal_packages(sbom_doc): 285 packages = [] 286 for p in sbom_doc.packages: 287 package = { 288 PropNames.NAME: p.name, 289 PropNames.SPDXID: p.id, 290 PropNames.PACKAGE_DOWNLOAD_LOCATION: p.download_location if p.download_location else sbom_data.VALUE_NOASSERTION, 291 PropNames.FILES_ANALYZED: p.files_analyzed 292 } 293 if p.version: 294 package[PropNames.PACKAGE_VERSION] = p.version 295 if p.supplier: 296 package[PropNames.PACKAGE_SUPPLIER] = p.supplier 297 if p.verification_code: 298 package[PropNames.PACKAGE_VERIFICATION_CODE] = { 299 PropNames.PACKAGE_VERIFICATION_CODE_VALUE: p.verification_code 300 } 301 if p.external_refs: 302 package[PropNames.PACKAGE_EXTERNAL_REFS] = [] 303 for ref in p.external_refs: 304 ext_ref = { 305 PropNames.PACKAGE_EXTERNAL_REF_CATEGORY: ref.category, 306 PropNames.PACKAGE_EXTERNAL_REF_TYPE: ref.type, 307 PropNames.PACKAGE_EXTERNAL_REF_LOCATOR: ref.locator, 308 } 309 package[PropNames.PACKAGE_EXTERNAL_REFS].append(ext_ref) 310 if p.file_ids: 311 package[PropNames.PACKAGE_HAS_FILES] = [] 312 for file_id in p.file_ids: 313 package[PropNames.PACKAGE_HAS_FILES].append(file_id) 314 315 packages.append(package) 316 317 return {PropNames.PACKAGES: packages} 318 319 @staticmethod 320 def marshal_files(sbom_doc): 321 files = [] 322 for f in sbom_doc.files: 323 file = { 324 PropNames.FILE_NAME: f.name, 325 PropNames.SPDXID: f.id 326 } 327 checksum = f.checksum.split(': ') 328 file[PropNames.FILE_CHECKSUMS] = [{ 329 PropNames.ALGORITHM: checksum[0], 330 PropNames.CHECKSUM_VALUE: checksum[1], 331 }] 332 files.append(file) 333 return {PropNames.FILES: files} 334 335 @staticmethod 336 def marshal_relationships(sbom_doc): 337 relationships = [] 338 sorted_rels = sorted(sbom_doc.relationships, key=lambda r: r.relationship + r.id2 + r.id1) 339 for r in sorted_rels: 340 rel = { 341 PropNames.REL_ELEMENT_ID: r.id1, 342 PropNames.REL_RELATED_ELEMENT_ID: r.id2, 343 PropNames.REL_TYPE: r.relationship, 344 } 345 relationships.append(rel) 346 347 return {PropNames.RELATIONSHIPS: relationships} 348 349 @staticmethod 350 def write(sbom_doc, file): 351 doc = {} 352 doc.update(JSONWriter.marshal_doc_headers(sbom_doc)) 353 doc.update(JSONWriter.marshal_packages(sbom_doc)) 354 doc.update(JSONWriter.marshal_files(sbom_doc)) 355 doc.update(JSONWriter.marshal_relationships(sbom_doc)) 356 file.write(json.dumps(doc, indent=4)) 357