blob: 54057c9da64d0f1b7f5494f05b0fbcd209113375 [file] [log] [blame]
Wei Li49933362023-01-04 17:13:47 -08001#!/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 --product_out_dir=out/target/product/vsoc_x86_64 \
23 --build_version $(cat out/target/product/vsoc_x86_64/build_fingerprint.txt) \
24 --product_mfr=Google
25"""
26
27import argparse
28import csv
29import datetime
30import google.protobuf.text_format as text_format
31import hashlib
32import json
33import os
34import metadata_file_pb2
35
36# Common
37SPDXID = 'SPDXID'
38SPDX_VERSION = 'SPDXVersion'
39DATA_LICENSE = 'DataLicense'
40DOCUMENT_NAME = 'DocumentName'
41DOCUMENT_NAMESPACE = 'DocumentNamespace'
42CREATED = 'Created'
43CREATOR = 'Creator'
44EXTERNAL_DOCUMENT_REF = 'ExternalDocumentRef'
45
46# Package
47PACKAGE_NAME = 'PackageName'
48PACKAGE_DOWNLOAD_LOCATION = 'PackageDownloadLocation'
49PACKAGE_VERSION = 'PackageVersion'
50PACKAGE_SUPPLIER = 'PackageSupplier'
51FILES_ANALYZED = 'FilesAnalyzed'
52PACKAGE_VERIFICATION_CODE = 'PackageVerificationCode'
53PACKAGE_EXTERNAL_REF = 'ExternalRef'
54# Package license
55PACKAGE_LICENSE_CONCLUDED = 'PackageLicenseConcluded'
56PACKAGE_LICENSE_INFO_FROM_FILES = 'PackageLicenseInfoFromFiles'
57PACKAGE_LICENSE_DECLARED = 'PackageLicenseDeclared'
58PACKAGE_LICENSE_COMMENTS = 'PackageLicenseComments'
59
60# File
61FILE_NAME = 'FileName'
62FILE_CHECKSUM = 'FileChecksum'
63# File license
64FILE_LICENSE_CONCLUDED = 'LicenseConcluded'
65FILE_LICENSE_INFO_IN_FILE = 'LicenseInfoInFile'
66FILE_LICENSE_COMMENTS = 'LicenseComments'
67FILE_COPYRIGHT_TEXT = 'FileCopyrightText'
68FILE_NOTICE = 'FileNotice'
69FILE_ATTRIBUTION_TEXT = 'FileAttributionText'
70
71# Relationship
72RELATIONSHIP = 'Relationship'
73REL_DESCRIBES = 'DESCRIBES'
74REL_VARIANT_OF = 'VARIANT_OF'
75REL_GENERATED_FROM = 'GENERATED_FROM'
76
77# Package type
78PKG_SOURCE = 'SOURCE'
79PKG_UPSTREAM = 'UPSTREAM'
80PKG_PREBUILT = 'PREBUILT'
81
82# Security tag
83NVD_CPE23 = 'NVD-CPE2.3:'
84
85# Report
86ISSUE_NO_METADATA = 'No metadata generated in Make for installed files:'
87ISSUE_NO_METADATA_FILE = 'No METADATA file found for installed file:'
88ISSUE_METADATA_FILE_INCOMPLETE = 'METADATA file incomplete:'
89ISSUE_UNKNOWN_SECURITY_TAG_TYPE = "Unknown security tag type:"
90INFO_METADATA_FOUND_FOR_PACKAGE = 'METADATA file found for packages:'
91
92
93def get_args():
94 parser = argparse.ArgumentParser()
95 parser.add_argument('-v', '--verbose', action='store_true', default=False, help='Print more information.')
96 parser.add_argument('--output_file', required=True, help='The generated SBOM file in SPDX format.')
97 parser.add_argument('--metadata', required=True, help='The SBOM metadata file path.')
98 parser.add_argument('--product_out_dir', required=True, help='The parent directory of all the installed files.')
99 parser.add_argument('--build_version', required=True, help='The build version.')
100 parser.add_argument('--product_mfr', required=True, help='The product manufacturer.')
101 parser.add_argument('--json', action='store_true', default=False, help='Generated SBOM file in SPDX JSON format')
102 parser.add_argument('--unbundled', action='store_true', default=False, help='Generate SBOM file for unbundled module')
103
104 return parser.parse_args()
105
106
107def log(*info):
108 if args.verbose:
109 for i in info:
110 print(i)
111
112
113def new_doc_header(doc_id):
114 return {
115 SPDX_VERSION: 'SPDX-2.3',
116 DATA_LICENSE: 'CC0-1.0',
117 SPDXID: doc_id,
118 DOCUMENT_NAME: args.build_version,
119 DOCUMENT_NAMESPACE: 'https://www.google.com/sbom/spdx/android/' + args.build_version,
120 CREATOR: 'Organization: Google, LLC',
121 CREATED: '<timestamp>',
122 EXTERNAL_DOCUMENT_REF: [],
123 }
124
125
126def new_package_record(id, name, version, supplier, download_location=None, files_analyzed='false', external_refs=[]):
127 package = {
128 PACKAGE_NAME: name,
129 SPDXID: id,
130 PACKAGE_DOWNLOAD_LOCATION: download_location if download_location else 'NONE',
131 FILES_ANALYZED: files_analyzed,
132 }
133 if version:
134 package[PACKAGE_VERSION] = version
135 if supplier:
136 package[PACKAGE_SUPPLIER] = 'Organization: ' + supplier
137 if external_refs:
138 package[PACKAGE_EXTERNAL_REF] = external_refs
139
140 return package
141
142
143def new_file_record(id, name, checksum):
144 return {
145 FILE_NAME: name,
146 SPDXID: id,
147 FILE_CHECKSUM: checksum
148 }
149
150
151def encode_for_spdxid(s):
152 """Simple encode for string values used in SPDXID which uses the charset of A-Za-Z0-9.-"""
153 result = ''
154 for c in s:
155 if c.isalnum() or c in '.-':
156 result += c
157 elif c in '_@/':
158 result += '-'
159 else:
160 result += '0x' + c.encode('utf-8').hex()
161
162 return result.lstrip('-')
163
164
165def new_package_id(package_name, type):
166 return 'SPDXRef-{}-{}'.format(type, encode_for_spdxid(package_name))
167
168
169def new_external_doc_ref(package_name, sbom_url, sbom_checksum):
170 doc_ref_id = 'DocumentRef-{}-{}'.format(PKG_UPSTREAM, encode_for_spdxid(package_name))
171 return '{}: {} {} {}'.format(EXTERNAL_DOCUMENT_REF, doc_ref_id, sbom_url, sbom_checksum), doc_ref_id
172
173
174def new_file_id(file_path):
175 return 'SPDXRef-' + encode_for_spdxid(file_path)
176
177
178def new_relationship_record(id1, relationship, id2):
179 return '{}: {} {} {}'.format(RELATIONSHIP, id1, relationship, id2)
180
181
182def checksum(file_path):
183 file_path = args.product_out_dir + '/' + file_path
184 h = hashlib.sha1()
185 if os.path.islink(file_path):
186 h.update(os.readlink(file_path).encode('utf-8'))
187 else:
188 with open(file_path, "rb") as f:
189 h.update(f.read())
190 return "SHA1: " + h.hexdigest()
191
192
193def is_soong_prebuilt_module(file_metadata):
194 return file_metadata['soong_module_type'] and file_metadata['soong_module_type'] in [
195 'android_app_import', 'android_library_import', 'cc_prebuilt_binary', 'cc_prebuilt_library',
196 'cc_prebuilt_library_headers', 'cc_prebuilt_library_shared', 'cc_prebuilt_library_static', 'cc_prebuilt_object',
197 'dex_import', 'java_import', 'java_sdk_library_import', 'java_system_modules_import',
198 'libclang_rt_prebuilt_library_static', 'libclang_rt_prebuilt_library_shared', 'llvm_prebuilt_library_static',
199 'ndk_prebuilt_object', 'ndk_prebuilt_shared_stl', 'nkd_prebuilt_static_stl', 'prebuilt_apex',
200 'prebuilt_bootclasspath_fragment', 'prebuilt_dsp', 'prebuilt_firmware', 'prebuilt_kernel_modules',
201 'prebuilt_rfsa', 'prebuilt_root', 'rust_prebuilt_dylib', 'rust_prebuilt_library', 'rust_prebuilt_rlib',
202 'vndk_prebuilt_shared',
203
204 # 'android_test_import',
205 # 'cc_prebuilt_test_library_shared',
206 # 'java_import_host',
207 # 'java_test_import',
208 # 'llvm_host_prebuilt_library_shared',
209 # 'prebuilt_apis',
210 # 'prebuilt_build_tool',
211 # 'prebuilt_defaults',
212 # 'prebuilt_etc',
213 # 'prebuilt_etc_host',
214 # 'prebuilt_etc_xml',
215 # 'prebuilt_font',
216 # 'prebuilt_hidl_interfaces',
217 # 'prebuilt_platform_compat_config',
218 # 'prebuilt_stubs_sources',
219 # 'prebuilt_usr_share',
220 # 'prebuilt_usr_share_host',
221 # 'soong_config_module_type_import',
222 ]
223
224
225def is_source_package(file_metadata):
226 module_path = file_metadata['module_path']
227 return module_path.startswith('external/') and not is_prebuilt_package(file_metadata)
228
229
230def is_prebuilt_package(file_metadata):
231 module_path = file_metadata['module_path']
232 if module_path:
233 return (module_path.startswith('prebuilts/') or
234 is_soong_prebuilt_module(file_metadata) or
235 file_metadata['is_prebuilt_make_module'])
236
237 kernel_module_copy_files = file_metadata['kernel_module_copy_files']
238 if kernel_module_copy_files and not kernel_module_copy_files.startswith('ANDROID-GEN:'):
239 return True
240
241 return False
242
243
244def get_source_package_info(file_metadata, metadata_file_path):
245 if not metadata_file_path:
246 return file_metadata['module_path'], []
247
248 metadata_proto = metadata_file_protos[metadata_file_path]
249 external_refs = []
250 for tag in metadata_proto.third_party.security.tag:
251 if tag.lower().startswith((NVD_CPE23 + 'cpe:2.3:').lower()):
252 external_refs.append("{}: SECURITY cpe23Type {}".format(PACKAGE_EXTERNAL_REF, tag.removeprefix(NVD_CPE23)))
253 elif tag.lower().startswith((NVD_CPE23 + 'cpe:/').lower()):
254 external_refs.append("{}: SECURITY cpe22Type {}".format(PACKAGE_EXTERNAL_REF, tag.removeprefix(NVD_CPE23)))
255
256 if metadata_proto.name:
257 return metadata_proto.name, external_refs
258 else:
259 return os.path.basename(metadata_file_path), external_refs # return the directory name only as package name
260
261
262def get_prebuilt_package_name(file_metadata, metadata_file_path):
263 name = None
264 if metadata_file_path:
265 metadata_proto = metadata_file_protos[metadata_file_path]
266 if metadata_proto.name:
267 name = metadata_proto.name
268 else:
269 name = metadata_file_path
270 elif file_metadata['module_path']:
271 name = file_metadata['module_path']
272 elif file_metadata['kernel_module_copy_files']:
273 src_path = file_metadata['kernel_module_copy_files'].split(':')[0]
274 name = os.path.dirname(src_path)
275
276 return name.removeprefix('prebuilts/').replace('/', '-')
277
278
279def get_metadata_file_path(file_metadata):
280 metadata_path = ''
281 if file_metadata['module_path']:
282 metadata_path = file_metadata['module_path']
283 elif file_metadata['kernel_module_copy_files']:
284 metadata_path = os.path.dirname(file_metadata['kernel_module_copy_files'].split(':')[0])
285
286 while metadata_path and not os.path.exists(metadata_path + '/METADATA'):
287 metadata_path = os.path.dirname(metadata_path)
288
289 return metadata_path
290
291
292def get_package_version(metadata_file_path):
293 if not metadata_file_path:
294 return None
295 metadata_proto = metadata_file_protos[metadata_file_path]
296 return metadata_proto.third_party.version
297
298
299def get_package_homepage(metadata_file_path):
300 if not metadata_file_path:
301 return None
302 metadata_proto = metadata_file_protos[metadata_file_path]
303 if metadata_proto.third_party.homepage:
304 return metadata_proto.third_party.homepage
305 for url in metadata_proto.third_party.url:
306 if url.type == metadata_file_pb2.URL.Type.HOMEPAGE:
307 return url.value
308
309 return None
310
311
312def get_package_download_location(metadata_file_path):
313 if not metadata_file_path:
314 return None
315 metadata_proto = metadata_file_protos[metadata_file_path]
316 if metadata_proto.third_party.url:
317 urls = sorted(metadata_proto.third_party.url, key=lambda url: url.type)
318 if urls[0].type != metadata_file_pb2.URL.Type.HOMEPAGE:
319 return urls[0].value
320 elif len(urls) > 1:
321 return urls[1].value
322
323 return None
324
325
326def get_sbom_fragments(installed_file_metadata, metadata_file_path):
327 external_doc_ref = None
328 packages = []
329 relationships = []
330
331 # Info from METADATA file
332 homepage = get_package_homepage(metadata_file_path)
333 version = get_package_version(metadata_file_path)
334 download_location = get_package_download_location(metadata_file_path)
335
336 if is_source_package(installed_file_metadata):
337 # Source fork packages
338 name, external_refs = get_source_package_info(installed_file_metadata, metadata_file_path)
339 source_package_id = new_package_id(name, PKG_SOURCE)
340 source_package = new_package_record(source_package_id, name, args.build_version, args.product_mfr,
341 external_refs=external_refs)
342
343 upstream_package_id = new_package_id(name, PKG_UPSTREAM)
344 upstream_package = new_package_record(upstream_package_id, name, version, homepage, download_location)
345 packages += [source_package, upstream_package]
346 relationships.append(new_relationship_record(source_package_id, REL_VARIANT_OF, upstream_package_id))
347 elif is_prebuilt_package(installed_file_metadata):
348 # Prebuilt fork packages
349 name = get_prebuilt_package_name(installed_file_metadata, metadata_file_path)
350 prebuilt_package_id = new_package_id(name, PKG_PREBUILT)
351 prebuilt_package = new_package_record(prebuilt_package_id, name, args.build_version, args.product_mfr)
352 packages.append(prebuilt_package)
353
354 if metadata_file_path:
355 metadata_proto = metadata_file_protos[metadata_file_path]
356 if metadata_proto.third_party.WhichOneof('sbom') == 'sbom_ref':
357 sbom_url = metadata_proto.third_party.sbom_ref.url
358 sbom_checksum = metadata_proto.third_party.sbom_ref.checksum
359 upstream_element_id = metadata_proto.third_party.sbom_ref.element_id
360 if sbom_url and sbom_checksum and upstream_element_id:
361 external_doc_ref, doc_ref_id = new_external_doc_ref(name, sbom_url, sbom_checksum)
362 relationships.append(
363 new_relationship_record(prebuilt_package_id, REL_VARIANT_OF, doc_ref_id + ':' + upstream_element_id))
364
365 return external_doc_ref, packages, relationships
366
367
368def generate_package_verification_code(files):
369 checksums = [file[FILE_CHECKSUM] for file in files]
370 checksums.sort()
371 h = hashlib.sha1()
372 h.update(''.join(checksums).encode(encoding='utf-8'))
373 return h.hexdigest()
374
375
376def write_record(f, record):
377 if record.__class__.__name__ == 'dict':
378 for k, v in record.items():
379 if k == EXTERNAL_DOCUMENT_REF or k == PACKAGE_EXTERNAL_REF:
380 for ref in v:
381 f.write(ref + '\n')
382 else:
383 f.write('{}: {}\n'.format(k, v))
384 elif record.__class__.__name__ == 'str':
385 f.write(record + '\n')
386 f.write('\n')
387
388
389def write_tagvalue_sbom(all_records):
390 with open(args.output_file, 'w', encoding="utf-8") as output_file:
391 for rec in all_records:
392 write_record(output_file, rec)
393
394
395def write_json_sbom(all_records, product_package_id):
396 doc = {}
397 product_package = None
398 for r in all_records:
399 if r.__class__.__name__ == 'dict':
400 if DOCUMENT_NAME in r: # Doc header
401 doc['spdxVersion'] = r[SPDX_VERSION]
402 doc['dataLicense'] = r[DATA_LICENSE]
403 doc[SPDXID] = r[SPDXID]
404 doc['name'] = r[DOCUMENT_NAME]
405 doc['documentNamespace'] = r[DOCUMENT_NAMESPACE]
406 doc['creationInfo'] = {
407 'creators': [r[CREATOR]],
408 'created': r[CREATED],
409 }
410 doc['externalDocumentRefs'] = []
411 for ref in r[EXTERNAL_DOCUMENT_REF]:
412 # ref is 'ExternalDocumentRef: <doc id> <doc url> SHA1: xxxxx'
413 fields = ref.split(' ')
414 doc_ref = {
415 'externalDocumentId': fields[1],
416 'spdxDocument': fields[2],
417 'checksum': {
418 'algorithm': fields[3][:-1],
419 'checksumValue': fields[4]
420 }
421 }
422 doc['externalDocumentRefs'].append(doc_ref)
423 doc['documentDescribes'] = []
424 doc['packages'] = []
425 doc['files'] = []
426 doc['relationships'] = []
427
428 elif PACKAGE_NAME in r: # packages
429 package = {
430 'name': r[PACKAGE_NAME],
431 SPDXID: r[SPDXID],
432 'downloadLocation': r[PACKAGE_DOWNLOAD_LOCATION],
433 'filesAnalyzed': r[FILES_ANALYZED] == "true"
434 }
435 if PACKAGE_VERSION in r:
436 package['versionInfo'] = r[PACKAGE_VERSION]
437 if PACKAGE_SUPPLIER in r:
438 package['supplier'] = r[PACKAGE_SUPPLIER]
439 if PACKAGE_VERIFICATION_CODE in r:
440 package['packageVerificationCode'] = {
441 'packageVerificationCodeValue': r[PACKAGE_VERIFICATION_CODE]
442 }
443 if PACKAGE_EXTERNAL_REF in r:
444 package['externalRefs'] = []
445 for ref in r[PACKAGE_EXTERNAL_REF]:
446 # ref is 'ExternalRef: SECURITY cpe22Type cpe:/a:jsoncpp_project:jsoncpp:1.9.4'
447 fields = ref.split(' ')
448 ext_ref = {
449 'referenceCategory': fields[1],
450 'referenceType': fields[2],
451 'referenceLocator': fields[3],
452 }
453 package['externalRefs'].append(ext_ref)
454
455 doc['packages'].append(package)
456 if r[SPDXID] == product_package_id:
457 product_package = package
458 product_package['hasFiles'] = []
459
460 elif FILE_NAME in r: # files
461 file = {
462 'fileName': r[FILE_NAME],
463 SPDXID: r[SPDXID]
464 }
465 checksum = r[FILE_CHECKSUM].split(': ')
466 file['checksums'] = [{
467 'algorithm': checksum[0],
468 'checksumValue': checksum[1],
469 }]
470 doc['files'].append(file)
471 product_package['hasFiles'].append(r[SPDXID])
472
473 elif r.__class__.__name__ == 'str':
474 if r.startswith(RELATIONSHIP):
475 # r is 'Relationship: <spdxid> <relationship> <spdxid>'
476 fields = r.split(' ')
477 rel = {
478 'spdxElementId': fields[1],
479 'relatedSpdxElement': fields[3],
480 'relationshipType': fields[2],
481 }
482 if fields[2] == REL_DESCRIBES:
483 doc['documentDescribes'].append(fields[3])
484 else:
485 doc['relationships'].append(rel)
486
487 with open(args.output_file + '.json', 'w', encoding="utf-8") as output_file:
488 output_file.write(json.dumps(doc, indent=4))
489
490
491def save_report(report):
492 prefix, _ = os.path.splitext(args.output_file)
493 with open(prefix + '-gen-report.txt', 'w', encoding="utf-8") as report_file:
494 for type, issues in report.items():
495 report_file.write(type + '\n')
496 for issue in issues:
497 report_file.write('\t' + issue + '\n')
498 report_file.write('\n')
499
500
501def sort_rels(rel):
502 # rel = 'Relationship file_id GENERATED_FROM package_id'
503 fields = rel.split(' ')
504 return fields[3] + fields[1]
505
506
507# Validate the metadata generated by Make for installed files and report if there is no metadata.
508def installed_file_has_metadata(installed_file_metadata, report):
509 installed_file = installed_file_metadata['installed_file']
510 module_path = installed_file_metadata['module_path']
511 product_copy_files = installed_file_metadata['product_copy_files']
512 kernel_module_copy_files = installed_file_metadata['kernel_module_copy_files']
513 is_platform_generated = installed_file_metadata['is_platform_generated']
514
515 if (not module_path and
516 not product_copy_files and
517 not kernel_module_copy_files and
518 not is_platform_generated and
519 not installed_file.endswith('.fsv_meta')):
520 report[ISSUE_NO_METADATA].append(installed_file)
521 return False
522
523 return True
524
525
526def report_metadata_file(metadata_file_path, installed_file_metadata, report):
527 if metadata_file_path:
528 report[INFO_METADATA_FOUND_FOR_PACKAGE].append(
529 "installed_file: {}, module_path: {}, METADATA file: {}".format(
530 installed_file_metadata['installed_file'],
531 installed_file_metadata['module_path'],
532 metadata_file_path + '/METADATA'))
533
534 package_metadata = metadata_file_pb2.Metadata()
535 with open(metadata_file_path + '/METADATA', "rt") as f:
536 text_format.Parse(f.read(), package_metadata)
537
538 if not metadata_file_path in metadata_file_protos:
539 metadata_file_protos[metadata_file_path] = package_metadata
540 if not package_metadata.name:
541 report[ISSUE_METADATA_FILE_INCOMPLETE].append('{} does not has "name"'.format(metadata_file_path + '/METADATA'))
542
543 if not package_metadata.third_party.version:
544 report[ISSUE_METADATA_FILE_INCOMPLETE].append(
545 '{} does not has "third_party.version"'.format(metadata_file_path + '/METADATA'))
546
547 for tag in package_metadata.third_party.security.tag:
548 if not tag.startswith(NVD_CPE23):
549 report[ISSUE_UNKNOWN_SECURITY_TAG_TYPE].append(
550 "Unknown security tag type: {} in {}".format(tag, metadata_file_path + '/METADATA'))
551 else:
552 report[ISSUE_NO_METADATA_FILE].append(
553 "installed_file: {}, module_path: {}".format(
554 installed_file_metadata['installed_file'], installed_file_metadata['module_path']))
555
556
557def generate_fragment():
558 with open(args.metadata, newline='') as sbom_metadata_file:
559 reader = csv.DictReader(sbom_metadata_file)
560 for installed_file_metadata in reader:
561 installed_file = installed_file_metadata['installed_file']
562 if args.output_file != args.product_out_dir + installed_file + ".spdx":
563 continue
564
565 module_path = installed_file_metadata['module_path']
566 package_id = new_package_id(encode_for_spdxid(module_path), PKG_PREBUILT)
567 package = new_package_record(package_id, module_path, args.build_version, args.product_mfr)
568 file_id = new_file_id(installed_file)
569 file = new_file_record(file_id, installed_file, checksum(installed_file))
570 relationship = new_relationship_record(file_id, REL_GENERATED_FROM, package_id)
571 records = [package, file, relationship]
572 write_tagvalue_sbom(records)
573 break
574
575
576def main():
577 global args
578 args = get_args()
579 log("Args:", vars(args))
580
581 if args.unbundled:
582 generate_fragment()
583 return
584
585 global metadata_file_protos
586 metadata_file_protos = {}
587
588 doc_id = 'SPDXRef-DOCUMENT'
589 doc_header = new_doc_header(doc_id)
590
591 product_package_id = 'SPDXRef-PRODUCT'
592 product_package = new_package_record(product_package_id, 'PRODUCT', args.build_version, args.product_mfr,
593 files_analyzed='true')
594
595 platform_package_id = 'SPDXRef-PLATFORM'
596 platform_package = new_package_record(platform_package_id, 'PLATFORM', args.build_version, args.product_mfr)
597
598 # Report on some issues and information
599 report = {
600 ISSUE_NO_METADATA: [],
601 ISSUE_NO_METADATA_FILE: [],
602 ISSUE_METADATA_FILE_INCOMPLETE: [],
603 ISSUE_UNKNOWN_SECURITY_TAG_TYPE: [],
604 INFO_METADATA_FOUND_FOR_PACKAGE: []
605 }
606
607 # Scan the metadata in CSV file and create the corresponding package and file records in SPDX
608 product_files = []
609 package_ids = []
610 package_records = []
611 rels_file_gen_from = []
612 with open(args.metadata, newline='') as sbom_metadata_file:
613 reader = csv.DictReader(sbom_metadata_file)
614 for installed_file_metadata in reader:
615 installed_file = installed_file_metadata['installed_file']
616 module_path = installed_file_metadata['module_path']
617 product_copy_files = installed_file_metadata['product_copy_files']
618 kernel_module_copy_files = installed_file_metadata['kernel_module_copy_files']
619
620 if not installed_file_has_metadata(installed_file_metadata, report):
621 continue
622
623 file_id = new_file_id(installed_file)
624 product_files.append(new_file_record(file_id, installed_file, checksum(installed_file)))
625
626 if is_source_package(installed_file_metadata) or is_prebuilt_package(installed_file_metadata):
627 metadata_file_path = get_metadata_file_path(installed_file_metadata)
628 report_metadata_file(metadata_file_path, installed_file_metadata, report)
629
630 # File from source fork packages or prebuilt fork packages
631 external_doc_ref, pkgs, rels = get_sbom_fragments(installed_file_metadata, metadata_file_path)
632 if len(pkgs) > 0:
633 if external_doc_ref and external_doc_ref not in doc_header[EXTERNAL_DOCUMENT_REF]:
634 doc_header[EXTERNAL_DOCUMENT_REF].append(external_doc_ref)
635 for p in pkgs:
636 if not p[SPDXID] in package_ids:
637 package_ids.append(p[SPDXID])
638 package_records.append(p)
639 for rel in rels:
640 if not rel in package_records:
641 package_records.append(rel)
642 fork_package_id = pkgs[0][SPDXID] # The first package should be the source/prebuilt fork package
643 rels_file_gen_from.append(new_relationship_record(file_id, REL_GENERATED_FROM, fork_package_id))
644 elif module_path or installed_file_metadata['is_platform_generated']:
645 # File from PLATFORM package
646 rels_file_gen_from.append(new_relationship_record(file_id, REL_GENERATED_FROM, platform_package_id))
647 elif product_copy_files:
648 # Format of product_copy_files: <source path>:<dest path>
649 src_path = product_copy_files.split(':')[0]
650 # So far product_copy_files are copied from directory system, kernel, hardware, frameworks and device,
651 # so process them as files from PLATFORM package
652 rels_file_gen_from.append(new_relationship_record(file_id, REL_GENERATED_FROM, platform_package_id))
653 elif installed_file.endswith('.fsv_meta'):
654 # See build/make/core/Makefile:2988
655 rels_file_gen_from.append(new_relationship_record(file_id, REL_GENERATED_FROM, platform_package_id))
656 elif kernel_module_copy_files.startswith('ANDROID-GEN'):
657 # For the four files generated for _dlkm, _ramdisk partitions
658 # See build/make/core/Makefile:323
659 rels_file_gen_from.append(new_relationship_record(file_id, REL_GENERATED_FROM, platform_package_id))
660
661 product_package[PACKAGE_VERIFICATION_CODE] = generate_package_verification_code(product_files)
662
663 all_records = [
664 doc_header,
665 product_package,
666 new_relationship_record(doc_id, REL_DESCRIBES, product_package_id),
667 ]
668 all_records += product_files
669 all_records.append(platform_package)
670 all_records += package_records
671 rels_file_gen_from.sort(key=sort_rels)
672 all_records += rels_file_gen_from
673
674 # Save SBOM records to output file
675 doc_header[CREATED] = datetime.datetime.now(tz=datetime.timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
676 write_tagvalue_sbom(all_records)
677 if args.json:
678 write_json_sbom(all_records, product_package_id)
679
680 save_report(report)
681
682
683if __name__ == '__main__':
684 main()