• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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