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""" 18Generate the SBOM of the current target product in SPDX format. 19Usage example: 20 generate-sbom.py --output_file out/target/product/vsoc_x86_64/sbom.spdx \ 21 --metadata out/target/product/vsoc_x86_64/sbom-metadata.csv \ 22 --build_version $(cat out/target/product/vsoc_x86_64/build_fingerprint.txt) \ 23 --product_mfr=Google 24""" 25 26import argparse 27import csv 28import datetime 29import google.protobuf.text_format as text_format 30import hashlib 31import os 32import metadata_file_pb2 33import sbom_data 34import sbom_writers 35 36 37# Package type 38PKG_SOURCE = 'SOURCE' 39PKG_UPSTREAM = 'UPSTREAM' 40PKG_PREBUILT = 'PREBUILT' 41 42# Security tag 43NVD_CPE23 = 'NVD-CPE2.3:' 44 45# Report 46ISSUE_NO_METADATA = 'No metadata generated in Make for installed files:' 47ISSUE_NO_METADATA_FILE = 'No METADATA file found for installed file:' 48ISSUE_METADATA_FILE_INCOMPLETE = 'METADATA file incomplete:' 49ISSUE_UNKNOWN_SECURITY_TAG_TYPE = 'Unknown security tag type:' 50ISSUE_INSTALLED_FILE_NOT_EXIST = 'Non-exist installed files:' 51INFO_METADATA_FOUND_FOR_PACKAGE = 'METADATA file found for packages:' 52 53SOONG_PREBUILT_MODULE_TYPES = [ 54 'android_app_import', 55 'android_library_import', 56 'cc_prebuilt_binary', 57 'cc_prebuilt_library', 58 'cc_prebuilt_library_headers', 59 'cc_prebuilt_library_shared', 60 'cc_prebuilt_library_static', 61 'cc_prebuilt_object', 62 'dex_import', 63 'java_import', 64 'java_sdk_library_import', 65 'java_system_modules_import', 66 'libclang_rt_prebuilt_library_static', 67 'libclang_rt_prebuilt_library_shared', 68 'llvm_prebuilt_library_static', 69 'ndk_prebuilt_object', 70 'ndk_prebuilt_shared_stl', 71 'nkd_prebuilt_static_stl', 72 'prebuilt_apex', 73 'prebuilt_bootclasspath_fragment', 74 'prebuilt_dsp', 75 'prebuilt_firmware', 76 'prebuilt_kernel_modules', 77 'prebuilt_rfsa', 78 'prebuilt_root', 79 'rust_prebuilt_dylib', 80 'rust_prebuilt_library', 81 'rust_prebuilt_rlib', 82 'vndk_prebuilt_shared', 83] 84 85 86def get_args(): 87 parser = argparse.ArgumentParser() 88 parser.add_argument('-v', '--verbose', action='store_true', default=False, help='Print more information.') 89 parser.add_argument('--output_file', required=True, help='The generated SBOM file in SPDX format.') 90 parser.add_argument('--metadata', required=True, help='The SBOM metadata file path.') 91 parser.add_argument('--build_version', required=True, help='The build version.') 92 parser.add_argument('--product_mfr', required=True, help='The product manufacturer.') 93 parser.add_argument('--json', action='store_true', default=False, help='Generated SBOM file in SPDX JSON format') 94 parser.add_argument('--unbundled_apk', action='store_true', default=False, help='Generate SBOM for unbundled APKs') 95 parser.add_argument('--unbundled_apex', action='store_true', default=False, help='Generate SBOM for unbundled APEXs') 96 97 return parser.parse_args() 98 99 100def log(*info): 101 if args.verbose: 102 for i in info: 103 print(i) 104 105 106def encode_for_spdxid(s): 107 """Simple encode for string values used in SPDXID which uses the charset of A-Za-Z0-9.-""" 108 result = '' 109 for c in s: 110 if c.isalnum() or c in '.-': 111 result += c 112 elif c in '_@/': 113 result += '-' 114 else: 115 result += '0x' + c.encode('utf-8').hex() 116 117 return result.lstrip('-') 118 119 120def new_package_id(package_name, type): 121 return f'SPDXRef-{type}-{encode_for_spdxid(package_name)}' 122 123 124def new_file_id(file_path): 125 return f'SPDXRef-{encode_for_spdxid(file_path)}' 126 127 128def checksum(file_path): 129 h = hashlib.sha1() 130 if os.path.islink(file_path): 131 h.update(os.readlink(file_path).encode('utf-8')) 132 else: 133 with open(file_path, 'rb') as f: 134 h.update(f.read()) 135 return f'SHA1: {h.hexdigest()}' 136 137 138def is_soong_prebuilt_module(file_metadata): 139 return (file_metadata['soong_module_type'] and 140 file_metadata['soong_module_type'] in SOONG_PREBUILT_MODULE_TYPES) 141 142 143def is_source_package(file_metadata): 144 module_path = file_metadata['module_path'] 145 return module_path.startswith('external/') and not is_prebuilt_package(file_metadata) 146 147 148def is_prebuilt_package(file_metadata): 149 module_path = file_metadata['module_path'] 150 if module_path: 151 return (module_path.startswith('prebuilts/') or 152 is_soong_prebuilt_module(file_metadata) or 153 file_metadata['is_prebuilt_make_module']) 154 155 kernel_module_copy_files = file_metadata['kernel_module_copy_files'] 156 if kernel_module_copy_files and not kernel_module_copy_files.startswith('ANDROID-GEN:'): 157 return True 158 159 return False 160 161 162def get_source_package_info(file_metadata, metadata_file_path): 163 """Return source package info exists in its METADATA file, currently including name, security tag 164 and external SBOM reference. 165 166 See go/android-spdx and go/android-sbom-gen for more details. 167 """ 168 if not metadata_file_path: 169 return file_metadata['module_path'], [] 170 171 metadata_proto = metadata_file_protos[metadata_file_path] 172 external_refs = [] 173 for tag in metadata_proto.third_party.security.tag: 174 if tag.lower().startswith((NVD_CPE23 + 'cpe:2.3:').lower()): 175 external_refs.append( 176 sbom_data.PackageExternalRef(category=sbom_data.PackageExternalRefCategory.SECURITY, 177 type=sbom_data.PackageExternalRefType.cpe23Type, 178 locator=tag.removeprefix(NVD_CPE23))) 179 elif tag.lower().startswith((NVD_CPE23 + 'cpe:/').lower()): 180 external_refs.append( 181 sbom_data.PackageExternalRef(category=sbom_data.PackageExternalRefCategory.SECURITY, 182 type=sbom_data.PackageExternalRefType.cpe22Type, 183 locator=tag.removeprefix(NVD_CPE23))) 184 185 if metadata_proto.name: 186 return metadata_proto.name, external_refs 187 else: 188 return os.path.basename(metadata_file_path), external_refs # return the directory name only as package name 189 190 191def get_prebuilt_package_name(file_metadata, metadata_file_path): 192 """Return name of a prebuilt package, which can be from the METADATA file, metadata file path, 193 module path or kernel module's source path if the installed file is a kernel module. 194 195 See go/android-spdx and go/android-sbom-gen for more details. 196 """ 197 name = None 198 if metadata_file_path: 199 metadata_proto = metadata_file_protos[metadata_file_path] 200 if metadata_proto.name: 201 name = metadata_proto.name 202 else: 203 name = metadata_file_path 204 elif file_metadata['module_path']: 205 name = file_metadata['module_path'] 206 elif file_metadata['kernel_module_copy_files']: 207 src_path = file_metadata['kernel_module_copy_files'].split(':')[0] 208 name = os.path.dirname(src_path) 209 210 return name.removeprefix('prebuilts/').replace('/', '-') 211 212 213def get_metadata_file_path(file_metadata): 214 """Search for METADATA file of a package and return its path.""" 215 metadata_path = '' 216 if file_metadata['module_path']: 217 metadata_path = file_metadata['module_path'] 218 elif file_metadata['kernel_module_copy_files']: 219 metadata_path = os.path.dirname(file_metadata['kernel_module_copy_files'].split(':')[0]) 220 221 while metadata_path and not os.path.exists(metadata_path + '/METADATA'): 222 metadata_path = os.path.dirname(metadata_path) 223 224 return metadata_path 225 226 227def get_package_version(metadata_file_path): 228 """Return a package's version in its METADATA file.""" 229 if not metadata_file_path: 230 return None 231 metadata_proto = metadata_file_protos[metadata_file_path] 232 return metadata_proto.third_party.version 233 234 235def get_package_homepage(metadata_file_path): 236 """Return a package's homepage URL in its METADATA file.""" 237 if not metadata_file_path: 238 return None 239 metadata_proto = metadata_file_protos[metadata_file_path] 240 if metadata_proto.third_party.homepage: 241 return metadata_proto.third_party.homepage 242 for url in metadata_proto.third_party.url: 243 if url.type == metadata_file_pb2.URL.Type.HOMEPAGE: 244 return url.value 245 246 return None 247 248 249def get_package_download_location(metadata_file_path): 250 """Return a package's code repository URL in its METADATA file.""" 251 if not metadata_file_path: 252 return None 253 metadata_proto = metadata_file_protos[metadata_file_path] 254 if metadata_proto.third_party.url: 255 urls = sorted(metadata_proto.third_party.url, key=lambda url: url.type) 256 if urls[0].type != metadata_file_pb2.URL.Type.HOMEPAGE: 257 return urls[0].value 258 elif len(urls) > 1: 259 return urls[1].value 260 261 return None 262 263 264def get_sbom_fragments(installed_file_metadata, metadata_file_path): 265 """Return SPDX fragment of source/prebuilt packages, which usually contains a SOURCE/PREBUILT 266 package, a UPSTREAM package and an external SBOM document reference if sbom_ref defined in its 267 METADATA file. 268 269 See go/android-spdx and go/android-sbom-gen for more details. 270 """ 271 external_doc_ref = None 272 packages = [] 273 relationships = [] 274 275 # Info from METADATA file 276 homepage = get_package_homepage(metadata_file_path) 277 version = get_package_version(metadata_file_path) 278 download_location = get_package_download_location(metadata_file_path) 279 280 if is_source_package(installed_file_metadata): 281 # Source fork packages 282 name, external_refs = get_source_package_info(installed_file_metadata, metadata_file_path) 283 source_package_id = new_package_id(name, PKG_SOURCE) 284 source_package = sbom_data.Package(id=source_package_id, name=name, version=args.build_version, 285 download_location=sbom_data.VALUE_NONE, 286 supplier='Organization: ' + args.product_mfr, 287 external_refs=external_refs) 288 289 upstream_package_id = new_package_id(name, PKG_UPSTREAM) 290 upstream_package = sbom_data.Package(id=upstream_package_id, name=name, version=version, 291 supplier=('Organization: ' + homepage) if homepage else sbom_data.VALUE_NOASSERTION, 292 download_location=download_location) 293 packages += [source_package, upstream_package] 294 relationships.append(sbom_data.Relationship(id1=source_package_id, 295 relationship=sbom_data.RelationshipType.VARIANT_OF, 296 id2=upstream_package_id)) 297 elif is_prebuilt_package(installed_file_metadata): 298 # Prebuilt fork packages 299 name = get_prebuilt_package_name(installed_file_metadata, metadata_file_path) 300 prebuilt_package_id = new_package_id(name, PKG_PREBUILT) 301 prebuilt_package = sbom_data.Package(id=prebuilt_package_id, 302 name=name, 303 download_location=sbom_data.VALUE_NONE, 304 version=version if version else args.build_version, 305 supplier='Organization: ' + args.product_mfr) 306 307 upstream_package_id = new_package_id(name, PKG_UPSTREAM) 308 upstream_package = sbom_data.Package(id=upstream_package_id, name=name, version = version, 309 supplier=('Organization: ' + homepage) if homepage else sbom_data.VALUE_NOASSERTION, 310 download_location=download_location) 311 packages += [prebuilt_package, upstream_package] 312 relationships.append(sbom_data.Relationship(id1=prebuilt_package_id, 313 relationship=sbom_data.RelationshipType.VARIANT_OF, 314 id2=upstream_package_id)) 315 316 if metadata_file_path: 317 metadata_proto = metadata_file_protos[metadata_file_path] 318 if metadata_proto.third_party.WhichOneof('sbom') == 'sbom_ref': 319 sbom_url = metadata_proto.third_party.sbom_ref.url 320 sbom_checksum = metadata_proto.third_party.sbom_ref.checksum 321 upstream_element_id = metadata_proto.third_party.sbom_ref.element_id 322 if sbom_url and sbom_checksum and upstream_element_id: 323 doc_ref_id = f'DocumentRef-{PKG_UPSTREAM}-{encode_for_spdxid(name)}' 324 external_doc_ref = sbom_data.DocumentExternalReference(id=doc_ref_id, 325 uri=sbom_url, 326 checksum=sbom_checksum) 327 relationships.append( 328 sbom_data.Relationship(id1=upstream_package_id, 329 relationship=sbom_data.RelationshipType.VARIANT_OF, 330 id2=doc_ref_id + ':' + upstream_element_id)) 331 332 return external_doc_ref, packages, relationships 333 334 335def save_report(report_file_path, report): 336 with open(report_file_path, 'w', encoding='utf-8') as report_file: 337 for type, issues in report.items(): 338 report_file.write(type + '\n') 339 for issue in issues: 340 report_file.write('\t' + issue + '\n') 341 report_file.write('\n') 342 343 344# Validate the metadata generated by Make for installed files and report if there is no metadata. 345def installed_file_has_metadata(installed_file_metadata, report): 346 installed_file = installed_file_metadata['installed_file'] 347 module_path = installed_file_metadata['module_path'] 348 product_copy_files = installed_file_metadata['product_copy_files'] 349 kernel_module_copy_files = installed_file_metadata['kernel_module_copy_files'] 350 is_platform_generated = installed_file_metadata['is_platform_generated'] 351 352 if (not module_path and 353 not product_copy_files and 354 not kernel_module_copy_files and 355 not is_platform_generated and 356 not installed_file.endswith('.fsv_meta')): 357 report[ISSUE_NO_METADATA].append(installed_file) 358 return False 359 360 return True 361 362 363def report_metadata_file(metadata_file_path, installed_file_metadata, report): 364 if metadata_file_path: 365 report[INFO_METADATA_FOUND_FOR_PACKAGE].append( 366 'installed_file: {}, module_path: {}, METADATA file: {}'.format( 367 installed_file_metadata['installed_file'], 368 installed_file_metadata['module_path'], 369 metadata_file_path + '/METADATA')) 370 371 package_metadata = metadata_file_pb2.Metadata() 372 with open(metadata_file_path + '/METADATA', 'rt') as f: 373 text_format.Parse(f.read(), package_metadata) 374 375 if not metadata_file_path in metadata_file_protos: 376 metadata_file_protos[metadata_file_path] = package_metadata 377 if not package_metadata.name: 378 report[ISSUE_METADATA_FILE_INCOMPLETE].append(f'{metadata_file_path}/METADATA does not has "name"') 379 380 if not package_metadata.third_party.version: 381 report[ISSUE_METADATA_FILE_INCOMPLETE].append( 382 f'{metadata_file_path}/METADATA does not has "third_party.version"') 383 384 for tag in package_metadata.third_party.security.tag: 385 if not tag.startswith(NVD_CPE23): 386 report[ISSUE_UNKNOWN_SECURITY_TAG_TYPE].append( 387 f'Unknown security tag type: {tag} in {metadata_file_path}/METADATA') 388 else: 389 report[ISSUE_NO_METADATA_FILE].append( 390 "installed_file: {}, module_path: {}".format( 391 installed_file_metadata['installed_file'], installed_file_metadata['module_path'])) 392 393 394def generate_sbom_for_unbundled_apk(): 395 with open(args.metadata, newline='') as sbom_metadata_file: 396 reader = csv.DictReader(sbom_metadata_file) 397 doc = sbom_data.Document(name=args.build_version, 398 namespace=f'https://www.google.com/sbom/spdx/android/{args.build_version}', 399 creators=['Organization: ' + args.product_mfr]) 400 for installed_file_metadata in reader: 401 installed_file = installed_file_metadata['installed_file'] 402 if args.output_file != installed_file_metadata['build_output_path'] + '.spdx.json': 403 continue 404 405 module_path = installed_file_metadata['module_path'] 406 package_id = new_package_id(module_path, PKG_PREBUILT) 407 package = sbom_data.Package(id=package_id, 408 name=module_path, 409 version=args.build_version, 410 supplier='Organization: ' + args.product_mfr) 411 file_id = new_file_id(installed_file) 412 file = sbom_data.File(id=file_id, 413 name=installed_file, 414 checksum=checksum(installed_file_metadata['build_output_path'])) 415 relationship = sbom_data.Relationship(id1=file_id, 416 relationship=sbom_data.RelationshipType.GENERATED_FROM, 417 id2=package_id) 418 doc.add_package(package) 419 doc.files.append(file) 420 doc.describes = file_id 421 doc.add_relationship(relationship) 422 doc.created = datetime.datetime.now(tz=datetime.timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ') 423 break 424 425 with open(args.output_file, 'w', encoding='utf-8') as file: 426 sbom_writers.JSONWriter.write(doc, file) 427 fragment_file = args.output_file.removesuffix('.spdx.json') + '-fragment.spdx' 428 with open(fragment_file, 'w', encoding='utf-8') as file: 429 sbom_writers.TagValueWriter.write(doc, file, fragment=True) 430 431 432def main(): 433 global args 434 args = get_args() 435 log('Args:', vars(args)) 436 437 if args.unbundled_apk: 438 generate_sbom_for_unbundled_apk() 439 return 440 441 global metadata_file_protos 442 metadata_file_protos = {} 443 444 product_package = sbom_data.Package(id=sbom_data.SPDXID_PRODUCT, 445 name=sbom_data.PACKAGE_NAME_PRODUCT, 446 download_location=sbom_data.VALUE_NONE, 447 version=args.build_version, 448 supplier='Organization: ' + args.product_mfr, 449 files_analyzed=True) 450 451 doc = sbom_data.Document(name=args.build_version, 452 namespace=f'https://www.google.com/sbom/spdx/android/{args.build_version}', 453 creators=['Organization: ' + args.product_mfr]) 454 if not args.unbundled_apex: 455 doc.packages.append(product_package) 456 457 doc.packages.append(sbom_data.Package(id=sbom_data.SPDXID_PLATFORM, 458 name=sbom_data.PACKAGE_NAME_PLATFORM, 459 download_location=sbom_data.VALUE_NONE, 460 version=args.build_version, 461 supplier='Organization: ' + args.product_mfr)) 462 463 # Report on some issues and information 464 report = { 465 ISSUE_NO_METADATA: [], 466 ISSUE_NO_METADATA_FILE: [], 467 ISSUE_METADATA_FILE_INCOMPLETE: [], 468 ISSUE_UNKNOWN_SECURITY_TAG_TYPE: [], 469 ISSUE_INSTALLED_FILE_NOT_EXIST: [], 470 INFO_METADATA_FOUND_FOR_PACKAGE: [], 471 } 472 473 # Scan the metadata in CSV file and create the corresponding package and file records in SPDX 474 with open(args.metadata, newline='') as sbom_metadata_file: 475 reader = csv.DictReader(sbom_metadata_file) 476 for installed_file_metadata in reader: 477 installed_file = installed_file_metadata['installed_file'] 478 module_path = installed_file_metadata['module_path'] 479 product_copy_files = installed_file_metadata['product_copy_files'] 480 kernel_module_copy_files = installed_file_metadata['kernel_module_copy_files'] 481 build_output_path = installed_file_metadata['build_output_path'] 482 is_static_lib = installed_file_metadata['is_static_lib'] 483 484 if not installed_file_has_metadata(installed_file_metadata, report): 485 continue 486 if not is_static_lib and not (os.path.islink(build_output_path) or os.path.isfile(build_output_path)): 487 # Ignore non-existing static library files for now since they are not shipped on devices. 488 report[ISSUE_INSTALLED_FILE_NOT_EXIST].append(installed_file) 489 continue 490 491 file_id = new_file_id(installed_file) 492 # TODO(b/285453664): Soong should report the information of statically linked libraries to Make. 493 # This happens when a different sanitized version of static libraries is used in linking. 494 # As a workaround, use the following SHA1 checksum for static libraries created by Soong, if .a files could not be 495 # located correctly because Soong doesn't report the information to Make. 496 sha1 = 'SHA1: da39a3ee5e6b4b0d3255bfef95601890afd80709' # SHA1 of empty string 497 if os.path.islink(build_output_path) or os.path.isfile(build_output_path): 498 sha1 = checksum(build_output_path) 499 doc.files.append(sbom_data.File(id=file_id, 500 name=installed_file, 501 checksum=sha1)) 502 503 if not is_static_lib: 504 if not args.unbundled_apex: 505 product_package.file_ids.append(file_id) 506 elif len(doc.files) > 1: 507 doc.add_relationship(sbom_data.Relationship(doc.files[0].id, sbom_data.RelationshipType.CONTAINS, file_id)) 508 509 if is_source_package(installed_file_metadata) or is_prebuilt_package(installed_file_metadata): 510 metadata_file_path = get_metadata_file_path(installed_file_metadata) 511 report_metadata_file(metadata_file_path, installed_file_metadata, report) 512 513 # File from source fork packages or prebuilt fork packages 514 external_doc_ref, pkgs, rels = get_sbom_fragments(installed_file_metadata, metadata_file_path) 515 if len(pkgs) > 0: 516 if external_doc_ref: 517 doc.add_external_ref(external_doc_ref) 518 for p in pkgs: 519 doc.add_package(p) 520 for rel in rels: 521 doc.add_relationship(rel) 522 fork_package_id = pkgs[0].id # The first package should be the source/prebuilt fork package 523 doc.add_relationship(sbom_data.Relationship(id1=file_id, 524 relationship=sbom_data.RelationshipType.GENERATED_FROM, 525 id2=fork_package_id)) 526 elif module_path or installed_file_metadata['is_platform_generated']: 527 # File from PLATFORM package 528 doc.add_relationship(sbom_data.Relationship(id1=file_id, 529 relationship=sbom_data.RelationshipType.GENERATED_FROM, 530 id2=sbom_data.SPDXID_PLATFORM)) 531 elif product_copy_files: 532 # Format of product_copy_files: <source path>:<dest path> 533 src_path = product_copy_files.split(':')[0] 534 # So far product_copy_files are copied from directory system, kernel, hardware, frameworks and device, 535 # so process them as files from PLATFORM package 536 doc.add_relationship(sbom_data.Relationship(id1=file_id, 537 relationship=sbom_data.RelationshipType.GENERATED_FROM, 538 id2=sbom_data.SPDXID_PLATFORM)) 539 elif installed_file.endswith('.fsv_meta'): 540 # See build/make/core/Makefile:2988 541 doc.add_relationship(sbom_data.Relationship(id1=file_id, 542 relationship=sbom_data.RelationshipType.GENERATED_FROM, 543 id2=sbom_data.SPDXID_PLATFORM)) 544 elif kernel_module_copy_files.startswith('ANDROID-GEN'): 545 # For the four files generated for _dlkm, _ramdisk partitions 546 # See build/make/core/Makefile:323 547 doc.add_relationship(sbom_data.Relationship(id1=file_id, 548 relationship=sbom_data.RelationshipType.GENERATED_FROM, 549 id2=sbom_data.SPDXID_PLATFORM)) 550 551 # Process static libraries and whole static libraries the installed file links to 552 static_libs = installed_file_metadata['static_libraries'] 553 whole_static_libs = installed_file_metadata['whole_static_libraries'] 554 all_static_libs = (static_libs + ' ' + whole_static_libs).strip() 555 if all_static_libs: 556 for lib in all_static_libs.split(' '): 557 doc.add_relationship(sbom_data.Relationship(id1=file_id, 558 relationship=sbom_data.RelationshipType.STATIC_LINK, 559 id2=new_file_id(lib + '.a'))) 560 561 if args.unbundled_apex: 562 doc.describes = doc.files[0].id 563 564 # Save SBOM records to output file 565 doc.generate_packages_verification_code() 566 doc.created = datetime.datetime.now(tz=datetime.timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ') 567 prefix = args.output_file 568 if prefix.endswith('.spdx'): 569 prefix = prefix.removesuffix('.spdx') 570 elif prefix.endswith('.spdx.json'): 571 prefix = prefix.removesuffix('.spdx.json') 572 573 output_file = prefix + '.spdx' 574 if args.unbundled_apex: 575 output_file = prefix + '-fragment.spdx' 576 with open(output_file, 'w', encoding="utf-8") as file: 577 sbom_writers.TagValueWriter.write(doc, file, fragment=args.unbundled_apex) 578 if args.json: 579 with open(prefix + '.spdx.json', 'w', encoding="utf-8") as file: 580 sbom_writers.JSONWriter.write(doc, file) 581 582 save_report(prefix + '-gen-report.txt', report) 583 584 585if __name__ == '__main__': 586 main() 587