blob: 77bccbb73a3084f55e1bb3e0dac26b338517f383 [file] [log] [blame]
Wei Lia3265ef2024-02-05 14:49:50 -08001# !/usr/bin/env python3
2#
3# Copyright (C) 2024 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 gen_sbom.py --output_file out/soong/sbom/aosp_cf_x86_64_phone/sbom.spdx \
21 --metadata out/soong/metadata/aosp_cf_x86_64_phone/metadata.db \
22 --product_out out/target/vsoc_x86_64
23 --soong_out out/soong
24 --build_version $(cat out/target/product/vsoc_x86_64/build_fingerprint.txt) \
25 --product_mfr=Google
26"""
27
28import argparse
Wei Li0c6bc1a2024-09-23 21:24:13 +000029import compliance_metadata
Wei Lia3265ef2024-02-05 14:49:50 -080030import datetime
31import google.protobuf.text_format as text_format
32import hashlib
33import os
34import pathlib
35import queue
36import metadata_file_pb2
37import sbom_data
38import sbom_writers
Wei Lia3265ef2024-02-05 14:49:50 -080039
40# Package type
41PKG_SOURCE = 'SOURCE'
42PKG_UPSTREAM = 'UPSTREAM'
43PKG_PREBUILT = 'PREBUILT'
44
45# Security tag
46NVD_CPE23 = 'NVD-CPE2.3:'
47
48# Report
49ISSUE_NO_METADATA = 'No metadata generated in Make for installed files:'
50ISSUE_NO_METADATA_FILE = 'No METADATA file found for installed file:'
51ISSUE_METADATA_FILE_INCOMPLETE = 'METADATA file incomplete:'
52ISSUE_UNKNOWN_SECURITY_TAG_TYPE = 'Unknown security tag type:'
53ISSUE_INSTALLED_FILE_NOT_EXIST = 'Non-existent installed files:'
54ISSUE_NO_MODULE_FOUND_FOR_STATIC_DEP = 'No module found for static dependency files:'
55INFO_METADATA_FOUND_FOR_PACKAGE = 'METADATA file found for packages:'
56
57SOONG_PREBUILT_MODULE_TYPES = [
58 'android_app_import',
59 'android_library_import',
60 'cc_prebuilt_binary',
61 'cc_prebuilt_library',
62 'cc_prebuilt_library_headers',
63 'cc_prebuilt_library_shared',
64 'cc_prebuilt_library_static',
65 'cc_prebuilt_object',
66 'dex_import',
67 'java_import',
68 'java_sdk_library_import',
69 'java_system_modules_import',
70 'libclang_rt_prebuilt_library_static',
71 'libclang_rt_prebuilt_library_shared',
72 'llvm_prebuilt_library_static',
73 'ndk_prebuilt_object',
74 'ndk_prebuilt_shared_stl',
75 'nkd_prebuilt_static_stl',
76 'prebuilt_apex',
77 'prebuilt_bootclasspath_fragment',
78 'prebuilt_dsp',
79 'prebuilt_firmware',
80 'prebuilt_kernel_modules',
81 'prebuilt_rfsa',
82 'prebuilt_root',
83 'rust_prebuilt_dylib',
84 'rust_prebuilt_library',
85 'rust_prebuilt_rlib',
86 'vndk_prebuilt_shared',
87]
88
89THIRD_PARTY_IDENTIFIER_TYPES = [
90 # Types defined in metadata_file.proto
91 'Git',
92 'SVN',
93 'Hg',
94 'Darcs',
Wei Li3617a6f2025-01-29 23:18:13 -080095 'Piper',
Wei Lia3265ef2024-02-05 14:49:50 -080096 'VCS',
97 'Archive',
98 'PrebuiltByAlphabet',
99 'LocalSource',
100 'Other',
101 # OSV ecosystems defined at https://ossf.github.io/osv-schema/#affectedpackage-field.
102 'Go',
103 'npm',
104 'OSS-Fuzz',
105 'PyPI',
106 'RubyGems',
107 'crates.io',
108 'Hackage',
109 'GHC',
110 'Packagist',
111 'Maven',
112 'NuGet',
113 'Linux',
114 'Debian',
115 'Alpine',
116 'Hex',
117 'Android',
118 'GitHub Actions',
119 'Pub',
120 'ConanCenter',
121 'Rocky Linux',
122 'AlmaLinux',
123 'Bitnami',
124 'Photon OS',
125 'CRAN',
126 'Bioconductor',
127 'SwiftURL'
128]
129
130
131def get_args():
132 parser = argparse.ArgumentParser()
133 parser.add_argument('-v', '--verbose', action='store_true', default=False, help='Print more information.')
134 parser.add_argument('-d', '--debug', action='store_true', default=False, help='Debug mode')
135 parser.add_argument('--output_file', required=True, help='The generated SBOM file in SPDX format.')
136 parser.add_argument('--metadata', required=True, help='The metadata DB file path.')
137 parser.add_argument('--product_out', required=True, help='The path of PRODUCT_OUT, e.g. out/target/product/vsoc_x86_64.')
138 parser.add_argument('--soong_out', required=True, help='The path of Soong output directory, e.g. out/soong')
139 parser.add_argument('--build_version', required=True, help='The build version.')
140 parser.add_argument('--product_mfr', required=True, help='The product manufacturer.')
141 parser.add_argument('--json', action='store_true', default=False, help='Generated SBOM file in SPDX JSON format')
142
143 return parser.parse_args()
144
145
146def log(*info):
147 if args.verbose:
148 for i in info:
149 print(i)
150
151
152def new_package_id(package_name, type):
153 return f'SPDXRef-{type}-{sbom_data.encode_for_spdxid(package_name)}'
154
155
156def new_file_id(file_path):
157 return f'SPDXRef-{sbom_data.encode_for_spdxid(file_path)}'
158
159
160def new_license_id(license_name):
161 return f'LicenseRef-{sbom_data.encode_for_spdxid(license_name)}'
162
163
164def checksum(file_path):
165 h = hashlib.sha1()
166 if os.path.islink(file_path):
167 h.update(os.readlink(file_path).encode('utf-8'))
168 else:
169 with open(file_path, 'rb') as f:
170 h.update(f.read())
171 return f'SHA1: {h.hexdigest()}'
172
173
174def is_soong_prebuilt_module(file_metadata):
175 return (file_metadata['soong_module_type'] and
176 file_metadata['soong_module_type'] in SOONG_PREBUILT_MODULE_TYPES)
177
178
179def is_source_package(file_metadata):
180 module_path = file_metadata['module_path']
181 return module_path.startswith('external/') and not is_prebuilt_package(file_metadata)
182
183
184def is_prebuilt_package(file_metadata):
185 module_path = file_metadata['module_path']
186 if module_path:
187 return (module_path.startswith('prebuilts/') or
188 is_soong_prebuilt_module(file_metadata) or
189 file_metadata['is_prebuilt_make_module'])
190
191 kernel_module_copy_files = file_metadata['kernel_module_copy_files']
192 if kernel_module_copy_files and not kernel_module_copy_files.startswith('ANDROID-GEN:'):
193 return True
194
195 return False
196
197
198def get_source_package_info(file_metadata, metadata_file_path):
199 """Return source package info exists in its METADATA file, currently including name, security tag
200 and external SBOM reference.
201
202 See go/android-spdx and go/android-sbom-gen for more details.
203 """
204 if not metadata_file_path:
205 return file_metadata['module_path'], []
206
207 metadata_proto = metadata_file_protos[metadata_file_path]
208 external_refs = []
209 for tag in metadata_proto.third_party.security.tag:
210 if tag.lower().startswith((NVD_CPE23 + 'cpe:2.3:').lower()):
211 external_refs.append(
212 sbom_data.PackageExternalRef(category=sbom_data.PackageExternalRefCategory.SECURITY,
213 type=sbom_data.PackageExternalRefType.cpe23Type,
214 locator=tag.removeprefix(NVD_CPE23)))
215 elif tag.lower().startswith((NVD_CPE23 + 'cpe:/').lower()):
216 external_refs.append(
217 sbom_data.PackageExternalRef(category=sbom_data.PackageExternalRefCategory.SECURITY,
218 type=sbom_data.PackageExternalRefType.cpe22Type,
219 locator=tag.removeprefix(NVD_CPE23)))
220
221 if metadata_proto.name:
222 return metadata_proto.name, external_refs
223 else:
224 return os.path.basename(metadata_file_path), external_refs # return the directory name only as package name
225
226
227def get_prebuilt_package_name(file_metadata, metadata_file_path):
228 """Return name of a prebuilt package, which can be from the METADATA file, metadata file path,
229 module path or kernel module's source path if the installed file is a kernel module.
230
231 See go/android-spdx and go/android-sbom-gen for more details.
232 """
233 name = None
234 if metadata_file_path:
235 metadata_proto = metadata_file_protos[metadata_file_path]
236 if metadata_proto.name:
237 name = metadata_proto.name
238 else:
239 name = metadata_file_path
240 elif file_metadata['module_path']:
241 name = file_metadata['module_path']
242 elif file_metadata['kernel_module_copy_files']:
243 src_path = file_metadata['kernel_module_copy_files'].split(':')[0]
244 name = os.path.dirname(src_path)
245
246 return name.removeprefix('prebuilts/').replace('/', '-')
247
248
249def get_metadata_file_path(file_metadata):
250 """Search for METADATA file of a package and return its path."""
251 metadata_path = ''
252 if file_metadata['module_path']:
253 metadata_path = file_metadata['module_path']
254 elif file_metadata['kernel_module_copy_files']:
255 metadata_path = os.path.dirname(file_metadata['kernel_module_copy_files'].split(':')[0])
256
257 while metadata_path and not os.path.exists(metadata_path + '/METADATA'):
258 metadata_path = os.path.dirname(metadata_path)
259
260 return metadata_path
261
262
263def get_package_version(metadata_file_path):
264 """Return a package's version in its METADATA file."""
265 if not metadata_file_path:
266 return None
267 metadata_proto = metadata_file_protos[metadata_file_path]
268 return metadata_proto.third_party.version
269
270
271def get_package_homepage(metadata_file_path):
272 """Return a package's homepage URL in its METADATA file."""
273 if not metadata_file_path:
274 return None
275 metadata_proto = metadata_file_protos[metadata_file_path]
276 if metadata_proto.third_party.homepage:
277 return metadata_proto.third_party.homepage
278 for url in metadata_proto.third_party.url:
279 if url.type == metadata_file_pb2.URL.Type.HOMEPAGE:
280 return url.value
281
282 return None
283
284
285def get_package_download_location(metadata_file_path):
286 """Return a package's code repository URL in its METADATA file."""
287 if not metadata_file_path:
288 return None
289 metadata_proto = metadata_file_protos[metadata_file_path]
290 if metadata_proto.third_party.url:
291 urls = sorted(metadata_proto.third_party.url, key=lambda url: url.type)
292 if urls[0].type != metadata_file_pb2.URL.Type.HOMEPAGE:
293 return urls[0].value
294 elif len(urls) > 1:
295 return urls[1].value
296
297 return None
298
299
300def get_license_text(license_files):
301 license_text = ''
302 for license_file in license_files:
303 if args.debug:
304 license_text += '#### Content from ' + license_file + '\n'
305 else:
306 license_text += pathlib.Path(license_file).read_text(errors='replace') + '\n\n'
307 return license_text
308
309
310def get_sbom_fragments(installed_file_metadata, metadata_file_path):
311 """Return SPDX fragment of source/prebuilt packages, which usually contains a SOURCE/PREBUILT
312 package, a UPSTREAM package and an external SBOM document reference if sbom_ref defined in its
313 METADATA file.
314
315 See go/android-spdx and go/android-sbom-gen for more details.
316 """
317 external_doc_ref = None
318 packages = []
319 relationships = []
320 licenses = []
321
322 # Info from METADATA file
323 homepage = get_package_homepage(metadata_file_path)
324 version = get_package_version(metadata_file_path)
325 download_location = get_package_download_location(metadata_file_path)
326
327 lics = db.get_package_licenses(installed_file_metadata['module_path'])
328 if not lics:
329 lics = db.get_package_licenses(metadata_file_path)
330
331 if lics:
332 for license_name, license_files in lics.items():
333 if not license_files:
334 continue
335 license_id = new_license_id(license_name)
336 if license_name not in licenses_text:
337 licenses_text[license_name] = get_license_text(license_files.split(' '))
338 licenses.append(sbom_data.License(id=license_id, name=license_name, text=licenses_text[license_name]))
339
340 if is_source_package(installed_file_metadata):
341 # Source fork packages
342 name, external_refs = get_source_package_info(installed_file_metadata, metadata_file_path)
343 source_package_id = new_package_id(name, PKG_SOURCE)
344 source_package = sbom_data.Package(id=source_package_id, name=name, version=args.build_version,
345 download_location=sbom_data.VALUE_NONE,
346 supplier='Organization: ' + args.product_mfr,
347 external_refs=external_refs)
348
349 upstream_package_id = new_package_id(name, PKG_UPSTREAM)
350 upstream_package = sbom_data.Package(id=upstream_package_id, name=name, version=version,
351 supplier=(
352 'Organization: ' + homepage) if homepage else sbom_data.VALUE_NOASSERTION,
353 download_location=download_location)
354 packages += [source_package, upstream_package]
355 relationships.append(sbom_data.Relationship(id1=source_package_id,
356 relationship=sbom_data.RelationshipType.VARIANT_OF,
357 id2=upstream_package_id))
358
359 for license in licenses:
360 source_package.declared_license_ids.append(license.id)
361 upstream_package.declared_license_ids.append(license.id)
362
363 elif is_prebuilt_package(installed_file_metadata):
364 # Prebuilt fork packages
365 name = get_prebuilt_package_name(installed_file_metadata, metadata_file_path)
366 prebuilt_package_id = new_package_id(name, PKG_PREBUILT)
367 prebuilt_package = sbom_data.Package(id=prebuilt_package_id,
368 name=name,
369 download_location=sbom_data.VALUE_NONE,
370 version=version if version else args.build_version,
371 supplier='Organization: ' + args.product_mfr)
372
373 upstream_package_id = new_package_id(name, PKG_UPSTREAM)
374 upstream_package = sbom_data.Package(id=upstream_package_id, name=name, version=version,
375 supplier=(
376 'Organization: ' + homepage) if homepage else sbom_data.VALUE_NOASSERTION,
377 download_location=download_location)
378 packages += [prebuilt_package, upstream_package]
379 relationships.append(sbom_data.Relationship(id1=prebuilt_package_id,
380 relationship=sbom_data.RelationshipType.VARIANT_OF,
381 id2=upstream_package_id))
382 for license in licenses:
383 prebuilt_package.declared_license_ids.append(license.id)
384 upstream_package.declared_license_ids.append(license.id)
385
386 if metadata_file_path:
387 metadata_proto = metadata_file_protos[metadata_file_path]
388 if metadata_proto.third_party.WhichOneof('sbom') == 'sbom_ref':
389 sbom_url = metadata_proto.third_party.sbom_ref.url
390 sbom_checksum = metadata_proto.third_party.sbom_ref.checksum
391 upstream_element_id = metadata_proto.third_party.sbom_ref.element_id
392 if sbom_url and sbom_checksum and upstream_element_id:
393 doc_ref_id = f'DocumentRef-{PKG_UPSTREAM}-{sbom_data.encode_for_spdxid(name)}'
394 external_doc_ref = sbom_data.DocumentExternalReference(id=doc_ref_id,
395 uri=sbom_url,
396 checksum=sbom_checksum)
397 relationships.append(
398 sbom_data.Relationship(id1=upstream_package_id,
399 relationship=sbom_data.RelationshipType.VARIANT_OF,
400 id2=doc_ref_id + ':' + upstream_element_id))
401
402 return external_doc_ref, packages, relationships, licenses
403
404
405def save_report(report_file_path, report):
406 with open(report_file_path, 'w', encoding='utf-8') as report_file:
407 for type, issues in report.items():
408 report_file.write(type + '\n')
409 for issue in issues:
410 report_file.write('\t' + issue + '\n')
411 report_file.write('\n')
412
413
414# Validate the metadata generated by Make for installed files and report if there is no metadata.
415def installed_file_has_metadata(installed_file_metadata, report):
416 installed_file = installed_file_metadata['installed_file']
417 module_path = installed_file_metadata['module_path']
418 product_copy_files = installed_file_metadata['product_copy_files']
419 kernel_module_copy_files = installed_file_metadata['kernel_module_copy_files']
420 is_platform_generated = installed_file_metadata['is_platform_generated']
421
422 if (not module_path and
423 not product_copy_files and
424 not kernel_module_copy_files and
425 not is_platform_generated and
426 not installed_file.endswith('.fsv_meta')):
427 report[ISSUE_NO_METADATA].append(installed_file)
428 return False
429
430 return True
431
432
433# Validate identifiers in a package's METADATA.
434# 1) Only known identifier type is allowed
435# 2) Only one identifier's primary_source can be true
436def validate_package_metadata(metadata_file_path, package_metadata):
437 primary_source_found = False
438 for identifier in package_metadata.third_party.identifier:
439 if identifier.type not in THIRD_PARTY_IDENTIFIER_TYPES:
440 sys.exit(f'Unknown value of third_party.identifier.type in {metadata_file_path}/METADATA: {identifier.type}.')
441 if primary_source_found and identifier.primary_source:
442 sys.exit(
443 f'Field "primary_source" is set to true in multiple third_party.identifier in {metadata_file_path}/METADATA.')
444 primary_source_found = identifier.primary_source
445
446
447def report_metadata_file(metadata_file_path, installed_file_metadata, report):
448 if metadata_file_path:
449 report[INFO_METADATA_FOUND_FOR_PACKAGE].append(
450 'installed_file: {}, module_path: {}, METADATA file: {}'.format(
451 installed_file_metadata['installed_file'],
452 installed_file_metadata['module_path'],
453 metadata_file_path + '/METADATA'))
454
455 package_metadata = metadata_file_pb2.Metadata()
456 with open(metadata_file_path + '/METADATA', 'rt') as f:
457 text_format.Parse(f.read(), package_metadata)
458
459 validate_package_metadata(metadata_file_path, package_metadata)
460
461 if not metadata_file_path in metadata_file_protos:
462 metadata_file_protos[metadata_file_path] = package_metadata
463 if not package_metadata.name:
464 report[ISSUE_METADATA_FILE_INCOMPLETE].append(f'{metadata_file_path}/METADATA does not has "name"')
465
466 if not package_metadata.third_party.version:
467 report[ISSUE_METADATA_FILE_INCOMPLETE].append(
468 f'{metadata_file_path}/METADATA does not has "third_party.version"')
469
470 for tag in package_metadata.third_party.security.tag:
471 if not tag.startswith(NVD_CPE23):
472 report[ISSUE_UNKNOWN_SECURITY_TAG_TYPE].append(
473 f'Unknown security tag type: {tag} in {metadata_file_path}/METADATA')
474 else:
475 report[ISSUE_NO_METADATA_FILE].append(
476 "installed_file: {}, module_path: {}".format(
477 installed_file_metadata['installed_file'], installed_file_metadata['module_path']))
478
479
480# If a file is from a source fork or prebuilt fork package, add its package information to SBOM
481def add_package_of_file(file_id, file_metadata, doc, report):
482 metadata_file_path = get_metadata_file_path(file_metadata)
483 report_metadata_file(metadata_file_path, file_metadata, report)
484
485 external_doc_ref, pkgs, rels, licenses = get_sbom_fragments(file_metadata, metadata_file_path)
486 if len(pkgs) > 0:
487 if external_doc_ref:
488 doc.add_external_ref(external_doc_ref)
489 for p in pkgs:
490 doc.add_package(p)
491 for rel in rels:
492 doc.add_relationship(rel)
493 fork_package_id = pkgs[0].id # The first package should be the source/prebuilt fork package
494 doc.add_relationship(sbom_data.Relationship(id1=file_id,
495 relationship=sbom_data.RelationshipType.GENERATED_FROM,
496 id2=fork_package_id))
497 for license in licenses:
498 doc.add_license(license)
499
500
501# Add STATIC_LINK relationship for static dependencies of a file
502def add_static_deps_of_file(file_id, file_metadata, doc):
503 if not file_metadata['static_dep_files'] and not file_metadata['whole_static_dep_files']:
504 return
505 static_dep_files = []
506 if file_metadata['static_dep_files']:
507 static_dep_files += file_metadata['static_dep_files'].split(' ')
508 if file_metadata['whole_static_dep_files']:
509 static_dep_files += file_metadata['whole_static_dep_files'].split(' ')
510
511 for dep_file in static_dep_files:
512 # Static libs are not shipped on devices, so names are derived from .intermediates paths.
513 doc.add_relationship(sbom_data.Relationship(id1=file_id,
514 relationship=sbom_data.RelationshipType.STATIC_LINK,
515 id2=new_file_id(
516 dep_file.removeprefix(args.soong_out + '/.intermediates/'))))
517
518
519def add_licenses_of_file(file_id, file_metadata, doc):
520 lics = db.get_module_licenses(file_metadata.get('name', ''), file_metadata['module_path'])
521 if lics:
522 file = next(f for f in doc.files if file_id == f.id)
523 for license_name, license_files in lics.items():
524 if not license_files:
525 continue
526 license_id = new_license_id(license_name)
527 file.concluded_license_ids.append(license_id)
528 if license_name not in licenses_text:
529 license_text = get_license_text(license_files.split(' '))
530 licenses_text[license_name] = license_text
531
532 doc.add_license(sbom_data.License(id=license_id, name=license_name, text=licenses_text[license_name]))
533
534
535def get_all_transitive_static_dep_files_of_installed_files(installed_files_metadata, db, report):
536 # Find all transitive static dep files of all installed files
537 q = queue.Queue()
538 for installed_file_metadata in installed_files_metadata:
539 if installed_file_metadata['static_dep_files']:
540 for f in installed_file_metadata['static_dep_files'].split(' '):
541 q.put(f)
542 if installed_file_metadata['whole_static_dep_files']:
543 for f in installed_file_metadata['whole_static_dep_files'].split(' '):
544 q.put(f)
545
546 all_static_dep_files = {}
547 while not q.empty():
548 dep_file = q.get()
549 if dep_file in all_static_dep_files:
550 # It has been processed
551 continue
552
553 all_static_dep_files[dep_file] = True
554 soong_module = db.get_soong_module_of_built_file(dep_file)
555 if not soong_module:
556 # This should not happen, add to report[ISSUE_NO_MODULE_FOUND_FOR_STATIC_DEP]
557 report[ISSUE_NO_MODULE_FOUND_FOR_STATIC_DEP].append(f)
558 continue
559
560 if soong_module['static_dep_files']:
561 for f in soong_module['static_dep_files'].split(' '):
562 if f not in all_static_dep_files:
563 q.put(f)
564 if soong_module['whole_static_dep_files']:
565 for f in soong_module['whole_static_dep_files'].split(' '):
566 if f not in all_static_dep_files:
567 q.put(f)
568
569 return sorted(all_static_dep_files.keys())
570
571
Wei Lia3265ef2024-02-05 14:49:50 -0800572def main():
573 global args
574 args = get_args()
575 log('Args:', vars(args))
576
577 global db
Wei Li0c6bc1a2024-09-23 21:24:13 +0000578 db = compliance_metadata.MetadataDb(args.metadata)
579 if args.debug:
580 db.dump_debug_db(os.path.dirname(args.output_file) + '/compliance-metadata-debug.db')
581
Wei Lia3265ef2024-02-05 14:49:50 -0800582 global metadata_file_protos
583 metadata_file_protos = {}
584 global licenses_text
585 licenses_text = {}
586
587 product_package_id = sbom_data.SPDXID_PRODUCT
588 product_package_name = sbom_data.PACKAGE_NAME_PRODUCT
589 product_package = sbom_data.Package(id=product_package_id,
590 name=product_package_name,
591 download_location=sbom_data.VALUE_NONE,
592 version=args.build_version,
593 supplier='Organization: ' + args.product_mfr,
594 files_analyzed=True)
595 doc_name = args.build_version
596 doc = sbom_data.Document(name=doc_name,
597 namespace=f'https://www.google.com/sbom/spdx/android/{doc_name}',
598 creators=['Organization: ' + args.product_mfr],
599 describes=product_package_id)
600
601 doc.packages.append(product_package)
602 doc.packages.append(sbom_data.Package(id=sbom_data.SPDXID_PLATFORM,
603 name=sbom_data.PACKAGE_NAME_PLATFORM,
604 download_location=sbom_data.VALUE_NONE,
605 version=args.build_version,
606 supplier='Organization: ' + args.product_mfr,
607 declared_license_ids=[sbom_data.SPDXID_LICENSE_APACHE]))
608
609 # Report on some issues and information
610 report = {
611 ISSUE_NO_METADATA: [],
612 ISSUE_NO_METADATA_FILE: [],
613 ISSUE_METADATA_FILE_INCOMPLETE: [],
614 ISSUE_UNKNOWN_SECURITY_TAG_TYPE: [],
615 ISSUE_INSTALLED_FILE_NOT_EXIST: [],
616 ISSUE_NO_MODULE_FOUND_FOR_STATIC_DEP: [],
617 INFO_METADATA_FOUND_FOR_PACKAGE: [],
618 }
619
620 # Get installed files and corresponding make modules' metadata if an installed file is from a make module.
621 installed_files_metadata = db.get_installed_files()
622
623 # Find which Soong module an installed file is from and merge metadata from Make and Soong
624 for installed_file_metadata in installed_files_metadata:
625 soong_module = db.get_soong_module_of_installed_file(installed_file_metadata['installed_file'])
626 if soong_module:
627 # Merge soong metadata to make metadata
628 installed_file_metadata.update(soong_module)
629 else:
630 # For make modules soong_module_type should be empty
631 installed_file_metadata['soong_module_type'] = ''
632 installed_file_metadata['static_dep_files'] = ''
633 installed_file_metadata['whole_static_dep_files'] = ''
634
635 # Scan the metadata and create the corresponding package and file records in SPDX
636 for installed_file_metadata in installed_files_metadata:
637 installed_file = installed_file_metadata['installed_file']
638 module_path = installed_file_metadata['module_path']
639 product_copy_files = installed_file_metadata['product_copy_files']
640 kernel_module_copy_files = installed_file_metadata['kernel_module_copy_files']
641 build_output_path = installed_file
642 installed_file = installed_file.removeprefix(args.product_out)
643
644 if not installed_file_has_metadata(installed_file_metadata, report):
645 continue
646 if not (os.path.islink(build_output_path) or os.path.isfile(build_output_path)):
647 report[ISSUE_INSTALLED_FILE_NOT_EXIST].append(installed_file)
648 continue
649
650 file_id = new_file_id(installed_file)
651 sha1 = checksum(build_output_path)
652 f = sbom_data.File(id=file_id, name=installed_file, checksum=sha1)
653 doc.files.append(f)
654 product_package.file_ids.append(file_id)
655
656 if is_source_package(installed_file_metadata) or is_prebuilt_package(installed_file_metadata):
657 add_package_of_file(file_id, installed_file_metadata, doc, report)
658
659 elif module_path or installed_file_metadata['is_platform_generated']:
660 # File from PLATFORM package
661 doc.add_relationship(sbom_data.Relationship(id1=file_id,
662 relationship=sbom_data.RelationshipType.GENERATED_FROM,
663 id2=sbom_data.SPDXID_PLATFORM))
664 if installed_file_metadata['is_platform_generated']:
665 f.concluded_license_ids = [sbom_data.SPDXID_LICENSE_APACHE]
666
667 elif product_copy_files:
668 # Format of product_copy_files: <source path>:<dest path>
669 src_path = product_copy_files.split(':')[0]
670 # So far product_copy_files are copied from directory system, kernel, hardware, frameworks and device,
671 # so process them as files from PLATFORM package
672 doc.add_relationship(sbom_data.Relationship(id1=file_id,
673 relationship=sbom_data.RelationshipType.GENERATED_FROM,
674 id2=sbom_data.SPDXID_PLATFORM))
675 if installed_file_metadata['license_text']:
676 if installed_file_metadata['license_text'] == 'build/soong/licenses/LICENSE':
677 f.concluded_license_ids = [sbom_data.SPDXID_LICENSE_APACHE]
678
679 elif installed_file.endswith('.fsv_meta'):
680 doc.add_relationship(sbom_data.Relationship(id1=file_id,
681 relationship=sbom_data.RelationshipType.GENERATED_FROM,
682 id2=sbom_data.SPDXID_PLATFORM))
683 f.concluded_license_ids = [sbom_data.SPDXID_LICENSE_APACHE]
684
685 elif kernel_module_copy_files.startswith('ANDROID-GEN'):
686 # For the four files generated for _dlkm, _ramdisk partitions
687 doc.add_relationship(sbom_data.Relationship(id1=file_id,
688 relationship=sbom_data.RelationshipType.GENERATED_FROM,
689 id2=sbom_data.SPDXID_PLATFORM))
690
691 # Process static dependencies of the installed file
692 add_static_deps_of_file(file_id, installed_file_metadata, doc)
693
694 # Add licenses of the installed file
695 add_licenses_of_file(file_id, installed_file_metadata, doc)
696
697 # Add all static library files to SBOM
698 for dep_file in get_all_transitive_static_dep_files_of_installed_files(installed_files_metadata, db, report):
699 filepath = dep_file.removeprefix(args.soong_out + '/.intermediates/')
700 file_id = new_file_id(filepath)
701 # SHA1 of empty string. Sometimes .a files might not be built.
702 sha1 = 'SHA1: da39a3ee5e6b4b0d3255bfef95601890afd80709'
703 if os.path.islink(dep_file) or os.path.isfile(dep_file):
704 sha1 = checksum(dep_file)
705 doc.files.append(sbom_data.File(id=file_id,
706 name=filepath,
707 checksum=sha1))
708 file_metadata = {
709 'installed_file': dep_file,
710 'is_prebuilt_make_module': False
711 }
Wei Li3617a6f2025-01-29 23:18:13 -0800712 soong_module = db.get_soong_module_of_built_file(dep_file)
713 if not soong_module:
714 continue
715 file_metadata.update(soong_module)
Wei Lia671bcb2025-01-16 15:16:30 -0800716 if is_source_package(file_metadata) or is_prebuilt_package(file_metadata):
717 add_package_of_file(file_id, file_metadata, doc, report)
718 else:
719 # Other static lib files are generated from the platform
720 doc.add_relationship(sbom_data.Relationship(id1=file_id,
721 relationship=sbom_data.RelationshipType.GENERATED_FROM,
722 id2=sbom_data.SPDXID_PLATFORM))
Wei Lia3265ef2024-02-05 14:49:50 -0800723
724 # Add relationships for static deps of static libraries
725 add_static_deps_of_file(file_id, file_metadata, doc)
726
727 # Add licenses of the static lib
728 add_licenses_of_file(file_id, file_metadata, doc)
729
730 # Save SBOM records to output file
731 doc.generate_packages_verification_code()
732 doc.created = datetime.datetime.now(tz=datetime.timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
733 prefix = args.output_file
734 if prefix.endswith('.spdx'):
735 prefix = prefix.removesuffix('.spdx')
736 elif prefix.endswith('.spdx.json'):
737 prefix = prefix.removesuffix('.spdx.json')
738
739 output_file = prefix + '.spdx'
740 with open(output_file, 'w', encoding="utf-8") as file:
741 sbom_writers.TagValueWriter.write(doc, file)
742 if args.json:
743 with open(prefix + '.spdx.json', 'w', encoding="utf-8") as file:
744 sbom_writers.JSONWriter.write(doc, file)
745
746 save_report(prefix + '-gen-report.txt', report)
747
748
749if __name__ == '__main__':
750 main()