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