Merge "Create separate python libraries for the following logic and refactor SBOM generation script accordingly." am: ebf41e9a91
am: 28fc5a97a7
Original change: https://android-review.googlesource.com/c/platform/build/+/2525827 Change-Id: I1e65b41eedd122d3258270a78168ef2ec83e60c9 Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
commit
074bfc1264
9 changed files with 1020 additions and 285 deletions
|
@ -70,22 +70,6 @@ python_binary_host {
|
|||
srcs: ["generate_gts_shared_report.py"],
|
||||
}
|
||||
|
||||
python_binary_host {
|
||||
name: "generate-sbom",
|
||||
srcs: [
|
||||
"generate-sbom.py",
|
||||
],
|
||||
version: {
|
||||
py3: {
|
||||
embedded_launcher: true,
|
||||
},
|
||||
},
|
||||
libs: [
|
||||
"metadata_file_proto_py",
|
||||
"libprotobuf-python",
|
||||
],
|
||||
}
|
||||
|
||||
python_binary_host {
|
||||
name: "list_files",
|
||||
main: "list_files.py",
|
||||
|
|
53
tools/sbom/Android.bp
Normal file
53
tools/sbom/Android.bp
Normal file
|
@ -0,0 +1,53 @@
|
|||
// Copyright (C) 2023 The Android Open Source Project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
python_binary_host {
|
||||
name: "generate-sbom",
|
||||
srcs: [
|
||||
"generate-sbom.py",
|
||||
],
|
||||
version: {
|
||||
py3: {
|
||||
embedded_launcher: true,
|
||||
},
|
||||
},
|
||||
libs: [
|
||||
"metadata_file_proto_py",
|
||||
"libprotobuf-python",
|
||||
"sbom_lib",
|
||||
],
|
||||
}
|
||||
|
||||
python_library_host {
|
||||
name: "sbom_lib",
|
||||
srcs: [
|
||||
"sbom_data.py",
|
||||
"sbom_writers.py",
|
||||
],
|
||||
}
|
||||
|
||||
python_test_host {
|
||||
name: "sbom_writers_test",
|
||||
main: "sbom_writers_test.py",
|
||||
srcs: [
|
||||
"sbom_writers_test.py",
|
||||
],
|
||||
data: [
|
||||
"testdata/*",
|
||||
],
|
||||
libs: [
|
||||
"sbom_lib",
|
||||
],
|
||||
test_suites: ["general-tests"],
|
||||
}
|
|
@ -29,50 +29,11 @@ import csv
|
|||
import datetime
|
||||
import google.protobuf.text_format as text_format
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import metadata_file_pb2
|
||||
import sbom_data
|
||||
import sbom_writers
|
||||
|
||||
# Common
|
||||
SPDXID = 'SPDXID'
|
||||
SPDX_VERSION = 'SPDXVersion'
|
||||
DATA_LICENSE = 'DataLicense'
|
||||
DOCUMENT_NAME = 'DocumentName'
|
||||
DOCUMENT_NAMESPACE = 'DocumentNamespace'
|
||||
CREATED = 'Created'
|
||||
CREATOR = 'Creator'
|
||||
EXTERNAL_DOCUMENT_REF = 'ExternalDocumentRef'
|
||||
|
||||
# Package
|
||||
PACKAGE_NAME = 'PackageName'
|
||||
PACKAGE_DOWNLOAD_LOCATION = 'PackageDownloadLocation'
|
||||
PACKAGE_VERSION = 'PackageVersion'
|
||||
PACKAGE_SUPPLIER = 'PackageSupplier'
|
||||
FILES_ANALYZED = 'FilesAnalyzed'
|
||||
PACKAGE_VERIFICATION_CODE = 'PackageVerificationCode'
|
||||
PACKAGE_EXTERNAL_REF = 'ExternalRef'
|
||||
# Package license
|
||||
PACKAGE_LICENSE_CONCLUDED = 'PackageLicenseConcluded'
|
||||
PACKAGE_LICENSE_INFO_FROM_FILES = 'PackageLicenseInfoFromFiles'
|
||||
PACKAGE_LICENSE_DECLARED = 'PackageLicenseDeclared'
|
||||
PACKAGE_LICENSE_COMMENTS = 'PackageLicenseComments'
|
||||
|
||||
# File
|
||||
FILE_NAME = 'FileName'
|
||||
FILE_CHECKSUM = 'FileChecksum'
|
||||
# File license
|
||||
FILE_LICENSE_CONCLUDED = 'LicenseConcluded'
|
||||
FILE_LICENSE_INFO_IN_FILE = 'LicenseInfoInFile'
|
||||
FILE_LICENSE_COMMENTS = 'LicenseComments'
|
||||
FILE_COPYRIGHT_TEXT = 'FileCopyrightText'
|
||||
FILE_NOTICE = 'FileNotice'
|
||||
FILE_ATTRIBUTION_TEXT = 'FileAttributionText'
|
||||
|
||||
# Relationship
|
||||
RELATIONSHIP = 'Relationship'
|
||||
REL_DESCRIBES = 'DESCRIBES'
|
||||
REL_VARIANT_OF = 'VARIANT_OF'
|
||||
REL_GENERATED_FROM = 'GENERATED_FROM'
|
||||
|
||||
# Package type
|
||||
PKG_SOURCE = 'SOURCE'
|
||||
|
@ -111,44 +72,6 @@ def log(*info):
|
|||
print(i)
|
||||
|
||||
|
||||
def new_doc_header(doc_id):
|
||||
return {
|
||||
SPDX_VERSION: 'SPDX-2.3',
|
||||
DATA_LICENSE: 'CC0-1.0',
|
||||
SPDXID: doc_id,
|
||||
DOCUMENT_NAME: args.build_version,
|
||||
DOCUMENT_NAMESPACE: f'https://www.google.com/sbom/spdx/android/{args.build_version}',
|
||||
CREATOR: 'Organization: Google, LLC',
|
||||
CREATED: '<timestamp>',
|
||||
EXTERNAL_DOCUMENT_REF: [],
|
||||
}
|
||||
|
||||
|
||||
def new_package_record(id, name, version, supplier, download_location=None, files_analyzed='false', external_refs=[]):
|
||||
package = {
|
||||
PACKAGE_NAME: name,
|
||||
SPDXID: id,
|
||||
PACKAGE_DOWNLOAD_LOCATION: download_location if download_location else 'NONE',
|
||||
FILES_ANALYZED: files_analyzed,
|
||||
}
|
||||
if version:
|
||||
package[PACKAGE_VERSION] = version
|
||||
if supplier:
|
||||
package[PACKAGE_SUPPLIER] = f'Organization: {supplier}'
|
||||
if external_refs:
|
||||
package[PACKAGE_EXTERNAL_REF] = external_refs
|
||||
|
||||
return package
|
||||
|
||||
|
||||
def new_file_record(id, name, checksum):
|
||||
return {
|
||||
FILE_NAME: name,
|
||||
SPDXID: id,
|
||||
FILE_CHECKSUM: checksum
|
||||
}
|
||||
|
||||
|
||||
def encode_for_spdxid(s):
|
||||
"""Simple encode for string values used in SPDXID which uses the charset of A-Za-Z0-9.-"""
|
||||
result = ''
|
||||
|
@ -167,19 +90,10 @@ def new_package_id(package_name, type):
|
|||
return f'SPDXRef-{type}-{encode_for_spdxid(package_name)}'
|
||||
|
||||
|
||||
def new_external_doc_ref(package_name, sbom_url, sbom_checksum):
|
||||
doc_ref_id = f'DocumentRef-{PKG_UPSTREAM}-{encode_for_spdxid(package_name)}'
|
||||
return f'{EXTERNAL_DOCUMENT_REF}: {doc_ref_id} {sbom_url} {sbom_checksum}', doc_ref_id
|
||||
|
||||
|
||||
def new_file_id(file_path):
|
||||
return f'SPDXRef-{encode_for_spdxid(file_path)}'
|
||||
|
||||
|
||||
def new_relationship_record(id1, relationship, id2):
|
||||
return f'{RELATIONSHIP}: {id1} {relationship} {id2}'
|
||||
|
||||
|
||||
def checksum(file_path):
|
||||
file_path = args.product_out_dir + '/' + file_path
|
||||
h = hashlib.sha1()
|
||||
|
@ -243,6 +157,11 @@ def is_prebuilt_package(file_metadata):
|
|||
|
||||
|
||||
def get_source_package_info(file_metadata, metadata_file_path):
|
||||
"""Return source package info exists in its METADATA file, currently including name, security tag
|
||||
and external SBOM reference.
|
||||
|
||||
See go/android-spdx and go/android-sbom-gen for more details.
|
||||
"""
|
||||
if not metadata_file_path:
|
||||
return file_metadata['module_path'], []
|
||||
|
||||
|
@ -250,9 +169,15 @@ def get_source_package_info(file_metadata, metadata_file_path):
|
|||
external_refs = []
|
||||
for tag in metadata_proto.third_party.security.tag:
|
||||
if tag.lower().startswith((NVD_CPE23 + 'cpe:2.3:').lower()):
|
||||
external_refs.append(f'{PACKAGE_EXTERNAL_REF}: SECURITY cpe23Type {tag.removeprefix(NVD_CPE23)}')
|
||||
external_refs.append(
|
||||
sbom_data.PackageExternalRef(category=sbom_data.PackageExternalRefCategory.SECURITY,
|
||||
type=sbom_data.PackageExternalRefType.cpe23Type,
|
||||
locator=tag.removeprefix(NVD_CPE23)))
|
||||
elif tag.lower().startswith((NVD_CPE23 + 'cpe:/').lower()):
|
||||
external_refs.append(f'{PACKAGE_EXTERNAL_REF}: SECURITY cpe22Type {tag.removeprefix(NVD_CPE23)}')
|
||||
external_refs.append(
|
||||
sbom_data.PackageExternalRef(category=sbom_data.PackageExternalRefCategory.SECURITY,
|
||||
type=sbom_data.PackageExternalRefType.cpe22Type,
|
||||
locator=tag.removeprefix(NVD_CPE23)))
|
||||
|
||||
if metadata_proto.name:
|
||||
return metadata_proto.name, external_refs
|
||||
|
@ -261,6 +186,11 @@ def get_source_package_info(file_metadata, metadata_file_path):
|
|||
|
||||
|
||||
def get_prebuilt_package_name(file_metadata, metadata_file_path):
|
||||
"""Return name of a prebuilt package, which can be from the METADATA file, metadata file path,
|
||||
module path or kernel module's source path if the installed file is a kernel module.
|
||||
|
||||
See go/android-spdx and go/android-sbom-gen for more details.
|
||||
"""
|
||||
name = None
|
||||
if metadata_file_path:
|
||||
metadata_proto = metadata_file_protos[metadata_file_path]
|
||||
|
@ -278,6 +208,7 @@ def get_prebuilt_package_name(file_metadata, metadata_file_path):
|
|||
|
||||
|
||||
def get_metadata_file_path(file_metadata):
|
||||
"""Search for METADATA file of a package and return its path."""
|
||||
metadata_path = ''
|
||||
if file_metadata['module_path']:
|
||||
metadata_path = file_metadata['module_path']
|
||||
|
@ -291,6 +222,7 @@ def get_metadata_file_path(file_metadata):
|
|||
|
||||
|
||||
def get_package_version(metadata_file_path):
|
||||
"""Return a package's version in its METADATA file."""
|
||||
if not metadata_file_path:
|
||||
return None
|
||||
metadata_proto = metadata_file_protos[metadata_file_path]
|
||||
|
@ -298,6 +230,7 @@ def get_package_version(metadata_file_path):
|
|||
|
||||
|
||||
def get_package_homepage(metadata_file_path):
|
||||
"""Return a package's homepage URL in its METADATA file."""
|
||||
if not metadata_file_path:
|
||||
return None
|
||||
metadata_proto = metadata_file_protos[metadata_file_path]
|
||||
|
@ -311,6 +244,7 @@ def get_package_homepage(metadata_file_path):
|
|||
|
||||
|
||||
def get_package_download_location(metadata_file_path):
|
||||
"""Return a package's code repository URL in its METADATA file."""
|
||||
if not metadata_file_path:
|
||||
return None
|
||||
metadata_proto = metadata_file_protos[metadata_file_path]
|
||||
|
@ -325,6 +259,12 @@ def get_package_download_location(metadata_file_path):
|
|||
|
||||
|
||||
def get_sbom_fragments(installed_file_metadata, metadata_file_path):
|
||||
"""Return SPDX fragment of source/prebuilt packages, which usually contains a SOURCE/PREBUILT
|
||||
package, a UPSTREAM package if it's a source package and a external SBOM document reference if
|
||||
it's a prebuilt package with sbom_ref defined in its METADATA file.
|
||||
|
||||
See go/android-spdx and go/android-sbom-gen for more details.
|
||||
"""
|
||||
external_doc_ref = None
|
||||
packages = []
|
||||
relationships = []
|
||||
|
@ -338,18 +278,26 @@ def get_sbom_fragments(installed_file_metadata, metadata_file_path):
|
|||
# Source fork packages
|
||||
name, external_refs = get_source_package_info(installed_file_metadata, metadata_file_path)
|
||||
source_package_id = new_package_id(name, PKG_SOURCE)
|
||||
source_package = new_package_record(source_package_id, name, args.build_version, args.product_mfr,
|
||||
external_refs=external_refs)
|
||||
source_package = sbom_data.Package(id=source_package_id, name=name, version=args.build_version,
|
||||
supplier='Organization: ' + args.product_mfr,
|
||||
external_refs=external_refs)
|
||||
|
||||
upstream_package_id = new_package_id(name, PKG_UPSTREAM)
|
||||
upstream_package = new_package_record(upstream_package_id, name, version, homepage, download_location)
|
||||
upstream_package = sbom_data.Package(id=upstream_package_id, name=name, version=version,
|
||||
supplier='Organization: ' + homepage if homepage else None,
|
||||
download_location=download_location)
|
||||
packages += [source_package, upstream_package]
|
||||
relationships.append(new_relationship_record(source_package_id, REL_VARIANT_OF, upstream_package_id))
|
||||
relationships.append(sbom_data.Relationship(id1=source_package_id,
|
||||
relationship=sbom_data.RelationshipType.VARIANT_OF,
|
||||
id2=upstream_package_id))
|
||||
elif is_prebuilt_package(installed_file_metadata):
|
||||
# Prebuilt fork packages
|
||||
name = get_prebuilt_package_name(installed_file_metadata, metadata_file_path)
|
||||
prebuilt_package_id = new_package_id(name, PKG_PREBUILT)
|
||||
prebuilt_package = new_package_record(prebuilt_package_id, name, args.build_version, args.product_mfr)
|
||||
prebuilt_package = sbom_data.Package(id=prebuilt_package_id,
|
||||
name=name,
|
||||
version=args.build_version,
|
||||
supplier='Organization: ' + args.product_mfr)
|
||||
packages.append(prebuilt_package)
|
||||
|
||||
if metadata_file_path:
|
||||
|
@ -359,136 +307,26 @@ def get_sbom_fragments(installed_file_metadata, metadata_file_path):
|
|||
sbom_checksum = metadata_proto.third_party.sbom_ref.checksum
|
||||
upstream_element_id = metadata_proto.third_party.sbom_ref.element_id
|
||||
if sbom_url and sbom_checksum and upstream_element_id:
|
||||
external_doc_ref, doc_ref_id = new_external_doc_ref(name, sbom_url, sbom_checksum)
|
||||
doc_ref_id = f'DocumentRef-{PKG_UPSTREAM}-{encode_for_spdxid(name)}'
|
||||
external_doc_ref = sbom_data.DocumentExternalReference(id=doc_ref_id,
|
||||
uri=sbom_url,
|
||||
checksum=sbom_checksum)
|
||||
relationships.append(
|
||||
new_relationship_record(prebuilt_package_id, REL_VARIANT_OF, doc_ref_id + ':' + upstream_element_id))
|
||||
sbom_data.Relationship(id1=prebuilt_package_id,
|
||||
relationship=sbom_data.RelationshipType.VARIANT_OF,
|
||||
id2=doc_ref_id + ':' + upstream_element_id))
|
||||
|
||||
return external_doc_ref, packages, relationships
|
||||
|
||||
|
||||
def generate_package_verification_code(files):
|
||||
checksums = [file[FILE_CHECKSUM] for file in files]
|
||||
checksums = [file.checksum for file in files]
|
||||
checksums.sort()
|
||||
h = hashlib.sha1()
|
||||
h.update(''.join(checksums).encode(encoding='utf-8'))
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def write_record(f, record):
|
||||
if record.__class__.__name__ == 'dict':
|
||||
for k, v in record.items():
|
||||
if k == EXTERNAL_DOCUMENT_REF or k == PACKAGE_EXTERNAL_REF:
|
||||
for ref in v:
|
||||
f.write(ref + '\n')
|
||||
else:
|
||||
f.write('{}: {}\n'.format(k, v))
|
||||
elif record.__class__.__name__ == 'str':
|
||||
f.write(record + '\n')
|
||||
f.write('\n')
|
||||
|
||||
|
||||
def write_tagvalue_sbom(all_records):
|
||||
with open(args.output_file, 'w', encoding="utf-8") as output_file:
|
||||
for rec in all_records:
|
||||
write_record(output_file, rec)
|
||||
|
||||
|
||||
def write_json_sbom(all_records, product_package_id):
|
||||
doc = {}
|
||||
product_package = None
|
||||
for r in all_records:
|
||||
if r.__class__.__name__ == 'dict':
|
||||
if DOCUMENT_NAME in r: # Doc header
|
||||
doc['spdxVersion'] = r[SPDX_VERSION]
|
||||
doc['dataLicense'] = r[DATA_LICENSE]
|
||||
doc[SPDXID] = r[SPDXID]
|
||||
doc['name'] = r[DOCUMENT_NAME]
|
||||
doc['documentNamespace'] = r[DOCUMENT_NAMESPACE]
|
||||
doc['creationInfo'] = {
|
||||
'creators': [r[CREATOR]],
|
||||
'created': r[CREATED],
|
||||
}
|
||||
doc['externalDocumentRefs'] = []
|
||||
for ref in r[EXTERNAL_DOCUMENT_REF]:
|
||||
# ref is 'ExternalDocumentRef: <doc id> <doc url> SHA1: xxxxx'
|
||||
fields = ref.split(' ')
|
||||
doc_ref = {
|
||||
'externalDocumentId': fields[1],
|
||||
'spdxDocument': fields[2],
|
||||
'checksum': {
|
||||
'algorithm': fields[3][:-1],
|
||||
'checksumValue': fields[4]
|
||||
}
|
||||
}
|
||||
doc['externalDocumentRefs'].append(doc_ref)
|
||||
doc['documentDescribes'] = []
|
||||
doc['packages'] = []
|
||||
doc['files'] = []
|
||||
doc['relationships'] = []
|
||||
|
||||
elif PACKAGE_NAME in r: # packages
|
||||
package = {
|
||||
'name': r[PACKAGE_NAME],
|
||||
SPDXID: r[SPDXID],
|
||||
'downloadLocation': r[PACKAGE_DOWNLOAD_LOCATION],
|
||||
'filesAnalyzed': r[FILES_ANALYZED] == "true"
|
||||
}
|
||||
if PACKAGE_VERSION in r:
|
||||
package['versionInfo'] = r[PACKAGE_VERSION]
|
||||
if PACKAGE_SUPPLIER in r:
|
||||
package['supplier'] = r[PACKAGE_SUPPLIER]
|
||||
if PACKAGE_VERIFICATION_CODE in r:
|
||||
package['packageVerificationCode'] = {
|
||||
'packageVerificationCodeValue': r[PACKAGE_VERIFICATION_CODE]
|
||||
}
|
||||
if PACKAGE_EXTERNAL_REF in r:
|
||||
package['externalRefs'] = []
|
||||
for ref in r[PACKAGE_EXTERNAL_REF]:
|
||||
# ref is 'ExternalRef: SECURITY cpe22Type cpe:/a:jsoncpp_project:jsoncpp:1.9.4'
|
||||
fields = ref.split(' ')
|
||||
ext_ref = {
|
||||
'referenceCategory': fields[1],
|
||||
'referenceType': fields[2],
|
||||
'referenceLocator': fields[3],
|
||||
}
|
||||
package['externalRefs'].append(ext_ref)
|
||||
|
||||
doc['packages'].append(package)
|
||||
if r[SPDXID] == product_package_id:
|
||||
product_package = package
|
||||
product_package['hasFiles'] = []
|
||||
|
||||
elif FILE_NAME in r: # files
|
||||
file = {
|
||||
'fileName': r[FILE_NAME],
|
||||
SPDXID: r[SPDXID]
|
||||
}
|
||||
checksum = r[FILE_CHECKSUM].split(': ')
|
||||
file['checksums'] = [{
|
||||
'algorithm': checksum[0],
|
||||
'checksumValue': checksum[1],
|
||||
}]
|
||||
doc['files'].append(file)
|
||||
product_package['hasFiles'].append(r[SPDXID])
|
||||
|
||||
elif r.__class__.__name__ == 'str':
|
||||
if r.startswith(RELATIONSHIP):
|
||||
# r is 'Relationship: <spdxid> <relationship> <spdxid>'
|
||||
fields = r.split(' ')
|
||||
rel = {
|
||||
'spdxElementId': fields[1],
|
||||
'relatedSpdxElement': fields[3],
|
||||
'relationshipType': fields[2],
|
||||
}
|
||||
if fields[2] == REL_DESCRIBES:
|
||||
doc['documentDescribes'].append(fields[3])
|
||||
else:
|
||||
doc['relationships'].append(rel)
|
||||
|
||||
with open(args.output_file + '.json', 'w', encoding="utf-8") as output_file:
|
||||
output_file.write(json.dumps(doc, indent=4))
|
||||
|
||||
|
||||
def save_report(report):
|
||||
prefix, _ = os.path.splitext(args.output_file)
|
||||
with open(prefix + '-gen-report.txt', 'w', encoding='utf-8') as report_file:
|
||||
|
@ -499,12 +337,6 @@ def save_report(report):
|
|||
report_file.write('\n')
|
||||
|
||||
|
||||
def sort_rels(rel):
|
||||
# rel = 'Relationship file_id GENERATED_FROM package_id'
|
||||
fields = rel.split(' ')
|
||||
return fields[3] + fields[1]
|
||||
|
||||
|
||||
# Validate the metadata generated by Make for installed files and report if there is no metadata.
|
||||
def installed_file_has_metadata(installed_file_metadata, report):
|
||||
installed_file = installed_file_metadata['installed_file']
|
||||
|
@ -555,24 +387,38 @@ def report_metadata_file(metadata_file_path, installed_file_metadata, report):
|
|||
installed_file_metadata['installed_file'], installed_file_metadata['module_path']))
|
||||
|
||||
|
||||
def generate_fragment():
|
||||
def generate_sbom_for_unbundled():
|
||||
with open(args.metadata, newline='') as sbom_metadata_file:
|
||||
reader = csv.DictReader(sbom_metadata_file)
|
||||
doc = sbom_data.Document(name=args.build_version,
|
||||
namespace=f'https://www.google.com/sbom/spdx/android/{args.build_version}',
|
||||
creators=['Organization: ' + args.product_mfr])
|
||||
for installed_file_metadata in reader:
|
||||
installed_file = installed_file_metadata['installed_file']
|
||||
if args.output_file != args.product_out_dir + installed_file + ".spdx":
|
||||
continue
|
||||
|
||||
module_path = installed_file_metadata['module_path']
|
||||
package_id = new_package_id(encode_for_spdxid(module_path), PKG_PREBUILT)
|
||||
package = new_package_record(package_id, module_path, args.build_version, args.product_mfr)
|
||||
package_id = new_package_id(module_path, PKG_PREBUILT)
|
||||
package = sbom_data.Package(id=package_id,
|
||||
name=module_path,
|
||||
version=args.build_version,
|
||||
supplier='Organization: ' + args.product_mfr)
|
||||
file_id = new_file_id(installed_file)
|
||||
file = new_file_record(file_id, installed_file, checksum(installed_file))
|
||||
relationship = new_relationship_record(file_id, REL_GENERATED_FROM, package_id)
|
||||
records = [package, file, relationship]
|
||||
write_tagvalue_sbom(records)
|
||||
file = sbom_data.File(id=file_id, name=installed_file, checksum=checksum(installed_file))
|
||||
relationship = sbom_data.Relationship(id1=file_id,
|
||||
relationship=sbom_data.RelationshipType.GENERATED_FROM,
|
||||
id2=package_id)
|
||||
doc.add_package(package)
|
||||
doc.files.append(file)
|
||||
doc.describes = file_id
|
||||
doc.add_relationship(relationship)
|
||||
doc.created = datetime.datetime.now(tz=datetime.timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||
break
|
||||
|
||||
with open(args.output_file, 'w', encoding="utf-8") as file:
|
||||
sbom_writers.TagValueWriter.write(doc, file, fragment=True)
|
||||
|
||||
|
||||
def main():
|
||||
global args
|
||||
|
@ -580,21 +426,27 @@ def main():
|
|||
log('Args:', vars(args))
|
||||
|
||||
if args.unbundled:
|
||||
generate_fragment()
|
||||
generate_sbom_for_unbundled()
|
||||
return
|
||||
|
||||
global metadata_file_protos
|
||||
metadata_file_protos = {}
|
||||
|
||||
doc_id = 'SPDXRef-DOCUMENT'
|
||||
doc_header = new_doc_header(doc_id)
|
||||
doc = sbom_data.Document(name=args.build_version,
|
||||
namespace=f'https://www.google.com/sbom/spdx/android/{args.build_version}',
|
||||
creators=['Organization: ' + args.product_mfr])
|
||||
|
||||
product_package_id = 'SPDXRef-PRODUCT'
|
||||
product_package = new_package_record(product_package_id, 'PRODUCT', args.build_version, args.product_mfr,
|
||||
files_analyzed='true')
|
||||
product_package = sbom_data.Package(id=sbom_data.SPDXID_PRODUCT,
|
||||
name=sbom_data.PACKAGE_NAME_PRODUCT,
|
||||
version=args.build_version,
|
||||
supplier='Organization: ' + args.product_mfr,
|
||||
files_analyzed=True)
|
||||
doc.packages.append(product_package)
|
||||
|
||||
platform_package_id = 'SPDXRef-PLATFORM'
|
||||
platform_package = new_package_record(platform_package_id, 'PLATFORM', args.build_version, args.product_mfr)
|
||||
doc.packages.append(sbom_data.Package(id=sbom_data.SPDXID_PLATFORM,
|
||||
name=sbom_data.PACKAGE_NAME_PLATFORM,
|
||||
version=args.build_version,
|
||||
supplier='Organization: ' + args.product_mfr))
|
||||
|
||||
# Report on some issues and information
|
||||
report = {
|
||||
|
@ -607,10 +459,6 @@ def main():
|
|||
}
|
||||
|
||||
# Scan the metadata in CSV file and create the corresponding package and file records in SPDX
|
||||
product_files = []
|
||||
package_ids = []
|
||||
package_records = []
|
||||
rels_file_gen_from = []
|
||||
with open(args.metadata, newline='') as sbom_metadata_file:
|
||||
reader = csv.DictReader(sbom_metadata_file)
|
||||
for installed_file_metadata in reader:
|
||||
|
@ -627,7 +475,9 @@ def main():
|
|||
continue
|
||||
|
||||
file_id = new_file_id(installed_file)
|
||||
product_files.append(new_file_record(file_id, installed_file, checksum(installed_file)))
|
||||
doc.files.append(
|
||||
sbom_data.File(id=file_id, name=installed_file, checksum=checksum(installed_file)))
|
||||
product_package.file_ids.append(file_id)
|
||||
|
||||
if is_source_package(installed_file_metadata) or is_prebuilt_package(installed_file_metadata):
|
||||
metadata_file_path = get_metadata_file_path(installed_file_metadata)
|
||||
|
@ -636,54 +486,50 @@ def main():
|
|||
# File from source fork packages or prebuilt fork packages
|
||||
external_doc_ref, pkgs, rels = get_sbom_fragments(installed_file_metadata, metadata_file_path)
|
||||
if len(pkgs) > 0:
|
||||
if external_doc_ref and external_doc_ref not in doc_header[EXTERNAL_DOCUMENT_REF]:
|
||||
doc_header[EXTERNAL_DOCUMENT_REF].append(external_doc_ref)
|
||||
if external_doc_ref:
|
||||
doc.add_external_ref(external_doc_ref)
|
||||
for p in pkgs:
|
||||
if not p[SPDXID] in package_ids:
|
||||
package_ids.append(p[SPDXID])
|
||||
package_records.append(p)
|
||||
doc.add_package(p)
|
||||
for rel in rels:
|
||||
if not rel in package_records:
|
||||
package_records.append(rel)
|
||||
fork_package_id = pkgs[0][SPDXID] # The first package should be the source/prebuilt fork package
|
||||
rels_file_gen_from.append(new_relationship_record(file_id, REL_GENERATED_FROM, fork_package_id))
|
||||
doc.add_relationship(rel)
|
||||
fork_package_id = pkgs[0].id # The first package should be the source/prebuilt fork package
|
||||
doc.add_relationship(sbom_data.Relationship(id1=file_id,
|
||||
relationship=sbom_data.RelationshipType.GENERATED_FROM,
|
||||
id2=fork_package_id))
|
||||
elif module_path or installed_file_metadata['is_platform_generated']:
|
||||
# File from PLATFORM package
|
||||
rels_file_gen_from.append(new_relationship_record(file_id, REL_GENERATED_FROM, platform_package_id))
|
||||
doc.add_relationship(sbom_data.Relationship(id1=file_id,
|
||||
relationship=sbom_data.RelationshipType.GENERATED_FROM,
|
||||
id2=sbom_data.SPDXID_PLATFORM))
|
||||
elif product_copy_files:
|
||||
# Format of product_copy_files: <source path>:<dest path>
|
||||
src_path = product_copy_files.split(':')[0]
|
||||
# So far product_copy_files are copied from directory system, kernel, hardware, frameworks and device,
|
||||
# so process them as files from PLATFORM package
|
||||
rels_file_gen_from.append(new_relationship_record(file_id, REL_GENERATED_FROM, platform_package_id))
|
||||
doc.add_relationship(sbom_data.Relationship(id1=file_id,
|
||||
relationship=sbom_data.RelationshipType.GENERATED_FROM,
|
||||
id2=sbom_data.SPDXID_PLATFORM))
|
||||
elif installed_file.endswith('.fsv_meta'):
|
||||
# See build/make/core/Makefile:2988
|
||||
rels_file_gen_from.append(new_relationship_record(file_id, REL_GENERATED_FROM, platform_package_id))
|
||||
doc.add_relationship(sbom_data.Relationship(id1=file_id,
|
||||
relationship=sbom_data.RelationshipType.GENERATED_FROM,
|
||||
id2=sbom_data.SPDXID_PLATFORM))
|
||||
elif kernel_module_copy_files.startswith('ANDROID-GEN'):
|
||||
# For the four files generated for _dlkm, _ramdisk partitions
|
||||
# See build/make/core/Makefile:323
|
||||
rels_file_gen_from.append(new_relationship_record(file_id, REL_GENERATED_FROM, platform_package_id))
|
||||
doc.add_relationship(sbom_data.Relationship(id1=file_id,
|
||||
relationship=sbom_data.RelationshipType.GENERATED_FROM,
|
||||
id2=sbom_data.SPDXID_PLATFORM))
|
||||
|
||||
product_package[PACKAGE_VERIFICATION_CODE] = generate_package_verification_code(product_files)
|
||||
|
||||
all_records = [
|
||||
doc_header,
|
||||
product_package,
|
||||
new_relationship_record(doc_id, REL_DESCRIBES, product_package_id),
|
||||
]
|
||||
all_records += product_files
|
||||
all_records.append(platform_package)
|
||||
all_records += package_records
|
||||
rels_file_gen_from.sort(key=sort_rels)
|
||||
all_records += rels_file_gen_from
|
||||
product_package.verification_code = generate_package_verification_code(doc.files)
|
||||
|
||||
# Save SBOM records to output file
|
||||
doc_header[CREATED] = datetime.datetime.now(tz=datetime.timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||
write_tagvalue_sbom(all_records)
|
||||
doc.created = datetime.datetime.now(tz=datetime.timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||
with open(args.output_file, 'w', encoding="utf-8") as file:
|
||||
sbom_writers.TagValueWriter.write(doc, file)
|
||||
if args.json:
|
||||
write_json_sbom(all_records, product_package_id)
|
||||
|
||||
save_report(report)
|
||||
with open(args.output_file+'.json', 'w', encoding="utf-8") as file:
|
||||
sbom_writers.JSONWriter.write(doc, file)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
120
tools/sbom/sbom_data.py
Normal file
120
tools/sbom/sbom_data.py
Normal file
|
@ -0,0 +1,120 @@
|
|||
#!/usr/bin/env python3
|
||||
#
|
||||
# Copyright (C) 2023 The Android Open Source Project
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""
|
||||
Define data classes that model SBOMs defined by SPDX. The data classes could be
|
||||
written out to different formats (tagvalue, JSON, etc) of SPDX with corresponding
|
||||
writer utilities.
|
||||
|
||||
Rrefer to SPDX 2.3 spec: https://spdx.github.io/spdx-spec/v2.3/ and go/android-spdx for details of
|
||||
fields in each data class.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List
|
||||
|
||||
SPDXID_DOC = 'SPDXRef-DOCUMENT'
|
||||
SPDXID_PRODUCT = 'SPDXRef-PRODUCT'
|
||||
SPDXID_PLATFORM = 'SPDXRef-PLATFORM'
|
||||
|
||||
PACKAGE_NAME_PRODUCT = 'PRODUCT'
|
||||
PACKAGE_NAME_PLATFORM = 'PLATFORM'
|
||||
|
||||
|
||||
class PackageExternalRefCategory:
|
||||
SECURITY = 'SECURITY'
|
||||
PACKAGE_MANAGER = 'PACKAGE-MANAGER'
|
||||
PERSISTENT_ID = 'PERSISTENT-ID'
|
||||
OTHER = 'OTHER'
|
||||
|
||||
|
||||
class PackageExternalRefType:
|
||||
cpe22Type = 'cpe22Type'
|
||||
cpe23Type = 'cpe23Type'
|
||||
|
||||
|
||||
@dataclass
|
||||
class PackageExternalRef:
|
||||
category: PackageExternalRefCategory
|
||||
type: PackageExternalRefType
|
||||
locator: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Package:
|
||||
name: str
|
||||
id: str
|
||||
version: str = None
|
||||
supplier: str = None
|
||||
download_location: str = None
|
||||
files_analyzed: bool = False
|
||||
verification_code: str = None
|
||||
file_ids: List[str] = field(default_factory=list)
|
||||
external_refs: List[PackageExternalRef] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class File:
|
||||
id: str
|
||||
name: str
|
||||
checksum: str
|
||||
|
||||
|
||||
class RelationshipType:
|
||||
DESCRIBES = 'DESCRIBES'
|
||||
VARIANT_OF = 'VARIANT_OF'
|
||||
GENERATED_FROM = 'GENERATED_FROM'
|
||||
|
||||
|
||||
@dataclass
|
||||
class Relationship:
|
||||
id1: str
|
||||
relationship: RelationshipType
|
||||
id2: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class DocumentExternalReference:
|
||||
id: str
|
||||
uri: str
|
||||
checksum: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Document:
|
||||
name: str
|
||||
namespace: str
|
||||
id: str = SPDXID_DOC
|
||||
describes: str = SPDXID_PRODUCT
|
||||
creators: List[str] = field(default_factory=list)
|
||||
created: str = None
|
||||
external_refs: List[DocumentExternalReference] = field(default_factory=list)
|
||||
packages: List[Package] = field(default_factory=list)
|
||||
files: List[File] = field(default_factory=list)
|
||||
relationships: List[Relationship] = field(default_factory=list)
|
||||
|
||||
def add_external_ref(self, external_ref):
|
||||
if not any(external_ref.uri == ref.uri for ref in self.external_refs):
|
||||
self.external_refs.append(external_ref)
|
||||
|
||||
def add_package(self, package):
|
||||
if not any(package.id == p.id for p in self.packages):
|
||||
self.packages.append(package)
|
||||
|
||||
def add_relationship(self, rel):
|
||||
if not any(rel.id1 == r.id1 and rel.id2 == r.id2 and rel.relationship == r.relationship
|
||||
for r in self.relationships):
|
||||
self.relationships.append(rel)
|
365
tools/sbom/sbom_writers.py
Normal file
365
tools/sbom/sbom_writers.py
Normal file
|
@ -0,0 +1,365 @@
|
|||
#!/usr/bin/env python3
|
||||
#
|
||||
# Copyright (C) 2023 The Android Open Source Project
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""
|
||||
Serialize objects defined in package sbom_data to SPDX format: tagvalue, JSON.
|
||||
"""
|
||||
|
||||
import json
|
||||
import sbom_data
|
||||
|
||||
SPDX_VER = 'SPDX-2.3'
|
||||
DATA_LIC = 'CC0-1.0'
|
||||
|
||||
|
||||
class Tags:
|
||||
# Common
|
||||
SPDXID = 'SPDXID'
|
||||
SPDX_VERSION = 'SPDXVersion'
|
||||
DATA_LICENSE = 'DataLicense'
|
||||
DOCUMENT_NAME = 'DocumentName'
|
||||
DOCUMENT_NAMESPACE = 'DocumentNamespace'
|
||||
CREATED = 'Created'
|
||||
CREATOR = 'Creator'
|
||||
EXTERNAL_DOCUMENT_REF = 'ExternalDocumentRef'
|
||||
|
||||
# Package
|
||||
PACKAGE_NAME = 'PackageName'
|
||||
PACKAGE_DOWNLOAD_LOCATION = 'PackageDownloadLocation'
|
||||
PACKAGE_VERSION = 'PackageVersion'
|
||||
PACKAGE_SUPPLIER = 'PackageSupplier'
|
||||
FILES_ANALYZED = 'FilesAnalyzed'
|
||||
PACKAGE_VERIFICATION_CODE = 'PackageVerificationCode'
|
||||
PACKAGE_EXTERNAL_REF = 'ExternalRef'
|
||||
# Package license
|
||||
PACKAGE_LICENSE_CONCLUDED = 'PackageLicenseConcluded'
|
||||
PACKAGE_LICENSE_INFO_FROM_FILES = 'PackageLicenseInfoFromFiles'
|
||||
PACKAGE_LICENSE_DECLARED = 'PackageLicenseDeclared'
|
||||
PACKAGE_LICENSE_COMMENTS = 'PackageLicenseComments'
|
||||
|
||||
# File
|
||||
FILE_NAME = 'FileName'
|
||||
FILE_CHECKSUM = 'FileChecksum'
|
||||
# File license
|
||||
FILE_LICENSE_CONCLUDED = 'LicenseConcluded'
|
||||
FILE_LICENSE_INFO_IN_FILE = 'LicenseInfoInFile'
|
||||
FILE_LICENSE_COMMENTS = 'LicenseComments'
|
||||
FILE_COPYRIGHT_TEXT = 'FileCopyrightText'
|
||||
FILE_NOTICE = 'FileNotice'
|
||||
FILE_ATTRIBUTION_TEXT = 'FileAttributionText'
|
||||
|
||||
# Relationship
|
||||
RELATIONSHIP = 'Relationship'
|
||||
|
||||
|
||||
class TagValueWriter:
|
||||
@staticmethod
|
||||
def marshal_doc_headers(sbom_doc):
|
||||
headers = [
|
||||
f'{Tags.SPDX_VERSION}: {SPDX_VER}',
|
||||
f'{Tags.DATA_LICENSE}: {DATA_LIC}',
|
||||
f'{Tags.SPDXID}: {sbom_doc.id}',
|
||||
f'{Tags.DOCUMENT_NAME}: {sbom_doc.name}',
|
||||
f'{Tags.DOCUMENT_NAMESPACE}: {sbom_doc.namespace}',
|
||||
]
|
||||
for creator in sbom_doc.creators:
|
||||
headers.append(f'{Tags.CREATOR}: {creator}')
|
||||
headers.append(f'{Tags.CREATED}: {sbom_doc.created}')
|
||||
for doc_ref in sbom_doc.external_refs:
|
||||
headers.append(
|
||||
f'{Tags.EXTERNAL_DOCUMENT_REF}: {doc_ref.id} {doc_ref.uri} {doc_ref.checksum}')
|
||||
headers.append('')
|
||||
return headers
|
||||
|
||||
@staticmethod
|
||||
def marshal_package(package):
|
||||
download_location = 'NONE'
|
||||
if package.download_location:
|
||||
download_location = package.download_location
|
||||
tagvalues = [
|
||||
f'{Tags.PACKAGE_NAME}: {package.name}',
|
||||
f'{Tags.SPDXID}: {package.id}',
|
||||
f'{Tags.PACKAGE_DOWNLOAD_LOCATION}: {download_location}',
|
||||
f'{Tags.FILES_ANALYZED}: {str(package.files_analyzed).lower()}',
|
||||
]
|
||||
if package.version:
|
||||
tagvalues.append(f'{Tags.PACKAGE_VERSION}: {package.version}')
|
||||
if package.supplier:
|
||||
tagvalues.append(f'{Tags.PACKAGE_SUPPLIER}: {package.supplier}')
|
||||
if package.verification_code:
|
||||
tagvalues.append(f'{Tags.PACKAGE_VERIFICATION_CODE}: {package.verification_code}')
|
||||
if package.external_refs:
|
||||
for external_ref in package.external_refs:
|
||||
tagvalues.append(
|
||||
f'{Tags.PACKAGE_EXTERNAL_REF}: {external_ref.category} {external_ref.type} {external_ref.locator}')
|
||||
|
||||
tagvalues.append('')
|
||||
return tagvalues
|
||||
|
||||
@staticmethod
|
||||
def marshal_described_element(sbom_doc):
|
||||
if not sbom_doc.describes:
|
||||
return None
|
||||
|
||||
product_package = [p for p in sbom_doc.packages if p.id == sbom_doc.describes]
|
||||
if product_package:
|
||||
tagvalues = TagValueWriter.marshal_package(product_package[0])
|
||||
tagvalues.append(
|
||||
f'{Tags.RELATIONSHIP}: {sbom_doc.id} {sbom_data.RelationshipType.DESCRIBES} {sbom_doc.describes}')
|
||||
|
||||
tagvalues.append('')
|
||||
return tagvalues
|
||||
|
||||
file = [f for f in sbom_doc.files if f.id == sbom_doc.describes]
|
||||
if file:
|
||||
tagvalues = [
|
||||
f'{Tags.RELATIONSHIP}: {sbom_doc.id} {sbom_data.RelationshipType.DESCRIBES} {sbom_doc.describes}'
|
||||
]
|
||||
|
||||
return tagvalues
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def marshal_packages(sbom_doc):
|
||||
tagvalues = []
|
||||
marshaled_relationships = []
|
||||
i = 0
|
||||
packages = sbom_doc.packages
|
||||
while i < len(packages):
|
||||
if packages[i].id == sbom_doc.describes:
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if i + 1 < len(packages) \
|
||||
and packages[i].id.startswith('SPDXRef-SOURCE-') \
|
||||
and packages[i + 1].id.startswith('SPDXRef-UPSTREAM-'):
|
||||
tagvalues += TagValueWriter.marshal_package(packages[i])
|
||||
tagvalues += TagValueWriter.marshal_package(packages[i + 1])
|
||||
rel = next((r for r in sbom_doc.relationships if
|
||||
r.id1 == packages[i].id and
|
||||
r.id2 == packages[i + 1].id and
|
||||
r.relationship == sbom_data.RelationshipType.VARIANT_OF), None)
|
||||
if rel:
|
||||
marshaled_relationships.append(rel)
|
||||
tagvalues.append(TagValueWriter.marshal_relationship(rel))
|
||||
tagvalues.append('')
|
||||
|
||||
i += 2
|
||||
else:
|
||||
tagvalues += TagValueWriter.marshal_package(packages[i])
|
||||
i += 1
|
||||
|
||||
return tagvalues, marshaled_relationships
|
||||
|
||||
@staticmethod
|
||||
def marshal_file(file):
|
||||
tagvalues = [
|
||||
f'{Tags.FILE_NAME}: {file.name}',
|
||||
f'{Tags.SPDXID}: {file.id}',
|
||||
f'{Tags.FILE_CHECKSUM}: {file.checksum}',
|
||||
'',
|
||||
]
|
||||
|
||||
return tagvalues
|
||||
|
||||
@staticmethod
|
||||
def marshal_files(sbom_doc):
|
||||
tagvalues = []
|
||||
for file in sbom_doc.files:
|
||||
tagvalues += TagValueWriter.marshal_file(file)
|
||||
return tagvalues
|
||||
|
||||
@staticmethod
|
||||
def marshal_relationship(rel):
|
||||
return f'{Tags.RELATIONSHIP}: {rel.id1} {rel.relationship} {rel.id2}'
|
||||
|
||||
@staticmethod
|
||||
def marshal_relationships(sbom_doc, marshaled_rels):
|
||||
tagvalues = []
|
||||
sorted_rels = sorted(sbom_doc.relationships, key=lambda r: r.id2 + r.id1)
|
||||
for rel in sorted_rels:
|
||||
if any(r.id1 == rel.id1 and r.id2 == rel.id2 and r.relationship == rel.relationship
|
||||
for r in marshaled_rels):
|
||||
continue
|
||||
tagvalues.append(TagValueWriter.marshal_relationship(rel))
|
||||
tagvalues.append('')
|
||||
return tagvalues
|
||||
|
||||
@staticmethod
|
||||
def write(sbom_doc, file, fragment=False):
|
||||
content = []
|
||||
if not fragment:
|
||||
content += TagValueWriter.marshal_doc_headers(sbom_doc)
|
||||
described_element = TagValueWriter.marshal_described_element(sbom_doc)
|
||||
if described_element:
|
||||
content += described_element
|
||||
content += TagValueWriter.marshal_files(sbom_doc)
|
||||
tagvalues, marshaled_relationships = TagValueWriter.marshal_packages(sbom_doc)
|
||||
content += tagvalues
|
||||
content += TagValueWriter.marshal_relationships(sbom_doc, marshaled_relationships)
|
||||
file.write('\n'.join(content))
|
||||
|
||||
|
||||
class PropNames:
|
||||
# Common
|
||||
SPDXID = 'SPDXID'
|
||||
SPDX_VERSION = 'spdxVersion'
|
||||
DATA_LICENSE = 'dataLicense'
|
||||
NAME = 'name'
|
||||
DOCUMENT_NAMESPACE = 'documentNamespace'
|
||||
CREATION_INFO = 'creationInfo'
|
||||
CREATORS = 'creators'
|
||||
CREATED = 'created'
|
||||
EXTERNAL_DOCUMENT_REF = 'externalDocumentRefs'
|
||||
DOCUMENT_DESCRIBES = 'documentDescribes'
|
||||
EXTERNAL_DOCUMENT_ID = 'externalDocumentId'
|
||||
EXTERNAL_DOCUMENT_URI = 'spdxDocument'
|
||||
EXTERNAL_DOCUMENT_CHECKSUM = 'checksum'
|
||||
ALGORITHM = 'algorithm'
|
||||
CHECKSUM_VALUE = 'checksumValue'
|
||||
|
||||
# Package
|
||||
PACKAGES = 'packages'
|
||||
PACKAGE_DOWNLOAD_LOCATION = 'downloadLocation'
|
||||
PACKAGE_VERSION = 'versionInfo'
|
||||
PACKAGE_SUPPLIER = 'supplier'
|
||||
FILES_ANALYZED = 'filesAnalyzed'
|
||||
PACKAGE_VERIFICATION_CODE = 'packageVerificationCode'
|
||||
PACKAGE_VERIFICATION_CODE_VALUE = 'packageVerificationCodeValue'
|
||||
PACKAGE_EXTERNAL_REFS = 'externalRefs'
|
||||
PACKAGE_EXTERNAL_REF_CATEGORY = 'referenceCategory'
|
||||
PACKAGE_EXTERNAL_REF_TYPE = 'referenceType'
|
||||
PACKAGE_EXTERNAL_REF_LOCATOR = 'referenceLocator'
|
||||
PACKAGE_HAS_FILES = 'hasFiles'
|
||||
|
||||
# File
|
||||
FILES = 'files'
|
||||
FILE_NAME = 'fileName'
|
||||
FILE_CHECKSUMS = 'checksums'
|
||||
|
||||
# Relationship
|
||||
RELATIONSHIPS = 'relationships'
|
||||
REL_ELEMENT_ID = 'spdxElementId'
|
||||
REL_RELATED_ELEMENT_ID = 'relatedSpdxElement'
|
||||
REL_TYPE = 'relationshipType'
|
||||
|
||||
|
||||
class JSONWriter:
|
||||
@staticmethod
|
||||
def marshal_doc_headers(sbom_doc):
|
||||
headers = {
|
||||
PropNames.SPDX_VERSION: SPDX_VER,
|
||||
PropNames.DATA_LICENSE: DATA_LIC,
|
||||
PropNames.SPDXID: sbom_doc.id,
|
||||
PropNames.NAME: sbom_doc.name,
|
||||
PropNames.DOCUMENT_NAMESPACE: sbom_doc.namespace,
|
||||
PropNames.CREATION_INFO: {}
|
||||
}
|
||||
creators = [creator for creator in sbom_doc.creators]
|
||||
headers[PropNames.CREATION_INFO][PropNames.CREATORS] = creators
|
||||
headers[PropNames.CREATION_INFO][PropNames.CREATED] = sbom_doc.created
|
||||
external_refs = []
|
||||
for doc_ref in sbom_doc.external_refs:
|
||||
checksum = doc_ref.checksum.split(': ')
|
||||
external_refs.append({
|
||||
PropNames.EXTERNAL_DOCUMENT_ID: f'{doc_ref.id}',
|
||||
PropNames.EXTERNAL_DOCUMENT_URI: doc_ref.uri,
|
||||
PropNames.EXTERNAL_DOCUMENT_CHECKSUM: {
|
||||
PropNames.ALGORITHM: checksum[0],
|
||||
PropNames.CHECKSUM_VALUE: checksum[1]
|
||||
}
|
||||
})
|
||||
if external_refs:
|
||||
headers[PropNames.EXTERNAL_DOCUMENT_REF] = external_refs
|
||||
headers[PropNames.DOCUMENT_DESCRIBES] = [sbom_doc.describes]
|
||||
|
||||
return headers
|
||||
|
||||
@staticmethod
|
||||
def marshal_packages(sbom_doc):
|
||||
packages = []
|
||||
for p in sbom_doc.packages:
|
||||
package = {
|
||||
PropNames.NAME: p.name,
|
||||
PropNames.SPDXID: p.id,
|
||||
PropNames.PACKAGE_DOWNLOAD_LOCATION: p.download_location if p.download_location else 'NONE',
|
||||
PropNames.FILES_ANALYZED: p.files_analyzed
|
||||
}
|
||||
if p.version:
|
||||
package[PropNames.PACKAGE_VERSION] = p.version
|
||||
if p.supplier:
|
||||
package[PropNames.PACKAGE_SUPPLIER] = p.supplier
|
||||
if p.verification_code:
|
||||
package[PropNames.PACKAGE_VERIFICATION_CODE] = {
|
||||
PropNames.PACKAGE_VERIFICATION_CODE_VALUE: p.verification_code
|
||||
}
|
||||
if p.external_refs:
|
||||
package[PropNames.PACKAGE_EXTERNAL_REFS] = []
|
||||
for ref in p.external_refs:
|
||||
ext_ref = {
|
||||
PropNames.PACKAGE_EXTERNAL_REF_CATEGORY: ref.category,
|
||||
PropNames.PACKAGE_EXTERNAL_REF_TYPE: ref.type,
|
||||
PropNames.PACKAGE_EXTERNAL_REF_LOCATOR: ref.locator,
|
||||
}
|
||||
package[PropNames.PACKAGE_EXTERNAL_REFS].append(ext_ref)
|
||||
if p.file_ids:
|
||||
package[PropNames.PACKAGE_HAS_FILES] = []
|
||||
for file_id in p.file_ids:
|
||||
package[PropNames.PACKAGE_HAS_FILES].append(file_id)
|
||||
|
||||
packages.append(package)
|
||||
|
||||
return {PropNames.PACKAGES: packages}
|
||||
|
||||
@staticmethod
|
||||
def marshal_files(sbom_doc):
|
||||
files = []
|
||||
for f in sbom_doc.files:
|
||||
file = {
|
||||
PropNames.FILE_NAME: f.name,
|
||||
PropNames.SPDXID: f.id
|
||||
}
|
||||
checksum = f.checksum.split(': ')
|
||||
file[PropNames.FILE_CHECKSUMS] = [{
|
||||
PropNames.ALGORITHM: checksum[0],
|
||||
PropNames.CHECKSUM_VALUE: checksum[1],
|
||||
}]
|
||||
files.append(file)
|
||||
return {PropNames.FILES: files}
|
||||
|
||||
@staticmethod
|
||||
def marshal_relationships(sbom_doc):
|
||||
relationships = []
|
||||
sorted_rels = sorted(sbom_doc.relationships, key=lambda r: r.relationship + r.id2 + r.id1)
|
||||
for r in sorted_rels:
|
||||
rel = {
|
||||
PropNames.REL_ELEMENT_ID: r.id1,
|
||||
PropNames.REL_RELATED_ELEMENT_ID: r.id2,
|
||||
PropNames.REL_TYPE: r.relationship,
|
||||
}
|
||||
relationships.append(rel)
|
||||
|
||||
return {PropNames.RELATIONSHIPS: relationships}
|
||||
|
||||
@staticmethod
|
||||
def write(sbom_doc, file):
|
||||
doc = {}
|
||||
doc.update(JSONWriter.marshal_doc_headers(sbom_doc))
|
||||
doc.update(JSONWriter.marshal_packages(sbom_doc))
|
||||
doc.update(JSONWriter.marshal_files(sbom_doc))
|
||||
doc.update(JSONWriter.marshal_relationships(sbom_doc))
|
||||
file.write(json.dumps(doc, indent=4))
|
153
tools/sbom/sbom_writers_test.py
Normal file
153
tools/sbom/sbom_writers_test.py
Normal file
|
@ -0,0 +1,153 @@
|
|||
#!/usr/bin/env python3
|
||||
#
|
||||
# Copyright (C) 2023 The Android Open Source Project
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import io
|
||||
import pathlib
|
||||
import unittest
|
||||
import sbom_data
|
||||
import sbom_writers
|
||||
|
||||
BUILD_FINGER_PRINT = 'build_finger_print'
|
||||
SUPPLIER_GOOGLE = 'Organization: Google'
|
||||
SUPPLIER_UPSTREAM = 'Organization: upstream'
|
||||
|
||||
SPDXID_PREBUILT_PACKAGE1 = 'SPDXRef-PREBUILT-package1'
|
||||
SPDXID_SOURCE_PACKAGE1 = 'SPDXRef-SOURCE-package1'
|
||||
SPDXID_UPSTREAM_PACKAGE1 = 'SPDXRef-UPSTREAM-package1'
|
||||
|
||||
SPDXID_FILE1 = 'SPDXRef-file1'
|
||||
SPDXID_FILE2 = 'SPDXRef-file2'
|
||||
SPDXID_FILE3 = 'SPDXRef-file3'
|
||||
|
||||
|
||||
class SBOMWritersTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
# SBOM of a product
|
||||
self.sbom_doc = sbom_data.Document(name='test doc',
|
||||
namespace='http://www.google.com/sbom/spdx/android',
|
||||
creators=[SUPPLIER_GOOGLE],
|
||||
created='2023-03-31T22:17:58Z',
|
||||
describes=sbom_data.SPDXID_PRODUCT)
|
||||
self.sbom_doc.add_external_ref(
|
||||
sbom_data.DocumentExternalReference(id='DocumentRef-external_doc_ref',
|
||||
uri='external_doc_uri',
|
||||
checksum='SHA1: 1234567890'))
|
||||
self.sbom_doc.add_package(
|
||||
sbom_data.Package(id=sbom_data.SPDXID_PRODUCT,
|
||||
name=sbom_data.PACKAGE_NAME_PRODUCT,
|
||||
supplier=SUPPLIER_GOOGLE,
|
||||
version=BUILD_FINGER_PRINT,
|
||||
files_analyzed=True,
|
||||
verification_code='123456',
|
||||
file_ids=[SPDXID_FILE1, SPDXID_FILE2, SPDXID_FILE3]))
|
||||
|
||||
self.sbom_doc.add_package(
|
||||
sbom_data.Package(id=sbom_data.SPDXID_PLATFORM,
|
||||
name=sbom_data.PACKAGE_NAME_PLATFORM,
|
||||
supplier=SUPPLIER_GOOGLE,
|
||||
version=BUILD_FINGER_PRINT,
|
||||
))
|
||||
|
||||
self.sbom_doc.add_package(
|
||||
sbom_data.Package(id=SPDXID_PREBUILT_PACKAGE1,
|
||||
name='Prebuilt package1',
|
||||
supplier=SUPPLIER_GOOGLE,
|
||||
version=BUILD_FINGER_PRINT,
|
||||
))
|
||||
|
||||
self.sbom_doc.add_package(
|
||||
sbom_data.Package(id=SPDXID_SOURCE_PACKAGE1,
|
||||
name='Source package1',
|
||||
supplier=SUPPLIER_GOOGLE,
|
||||
version=BUILD_FINGER_PRINT,
|
||||
external_refs=[sbom_data.PackageExternalRef(
|
||||
category=sbom_data.PackageExternalRefCategory.SECURITY,
|
||||
type=sbom_data.PackageExternalRefType.cpe22Type,
|
||||
locator='cpe:/a:jsoncpp_project:jsoncpp:1.9.4')]
|
||||
))
|
||||
|
||||
self.sbom_doc.add_package(
|
||||
sbom_data.Package(id=SPDXID_UPSTREAM_PACKAGE1,
|
||||
name='Upstream package1',
|
||||
supplier=SUPPLIER_UPSTREAM,
|
||||
version='1.1',
|
||||
))
|
||||
|
||||
self.sbom_doc.add_relationship(sbom_data.Relationship(id1=SPDXID_SOURCE_PACKAGE1,
|
||||
relationship=sbom_data.RelationshipType.VARIANT_OF,
|
||||
id2=SPDXID_UPSTREAM_PACKAGE1))
|
||||
|
||||
self.sbom_doc.files.append(
|
||||
sbom_data.File(id=SPDXID_FILE1, name='/bin/file1', checksum='SHA1: 11111'))
|
||||
self.sbom_doc.files.append(
|
||||
sbom_data.File(id=SPDXID_FILE2, name='/bin/file2', checksum='SHA1: 22222'))
|
||||
self.sbom_doc.files.append(
|
||||
sbom_data.File(id=SPDXID_FILE3, name='/bin/file3', checksum='SHA1: 33333'))
|
||||
|
||||
self.sbom_doc.add_relationship(sbom_data.Relationship(id1=SPDXID_FILE1,
|
||||
relationship=sbom_data.RelationshipType.GENERATED_FROM,
|
||||
id2=sbom_data.SPDXID_PLATFORM))
|
||||
self.sbom_doc.add_relationship(sbom_data.Relationship(id1=SPDXID_FILE2,
|
||||
relationship=sbom_data.RelationshipType.GENERATED_FROM,
|
||||
id2=SPDXID_PREBUILT_PACKAGE1))
|
||||
self.sbom_doc.add_relationship(sbom_data.Relationship(id1=SPDXID_FILE3,
|
||||
relationship=sbom_data.RelationshipType.GENERATED_FROM,
|
||||
id2=SPDXID_SOURCE_PACKAGE1
|
||||
))
|
||||
|
||||
# SBOM fragment of a APK
|
||||
self.unbundled_sbom_doc = sbom_data.Document(name='test doc',
|
||||
namespace='http://www.google.com/sbom/spdx/android',
|
||||
creators=[SUPPLIER_GOOGLE],
|
||||
created='2023-03-31T22:17:58Z',
|
||||
describes=SPDXID_FILE1)
|
||||
|
||||
self.unbundled_sbom_doc.files.append(
|
||||
sbom_data.File(id=SPDXID_FILE1, name='/bin/file1.apk', checksum='SHA1: 11111'))
|
||||
self.unbundled_sbom_doc.add_package(
|
||||
sbom_data.Package(id=SPDXID_SOURCE_PACKAGE1,
|
||||
name='Unbundled apk package',
|
||||
supplier=SUPPLIER_GOOGLE,
|
||||
version=BUILD_FINGER_PRINT))
|
||||
self.unbundled_sbom_doc.add_relationship(sbom_data.Relationship(id1=SPDXID_FILE1,
|
||||
relationship=sbom_data.RelationshipType.GENERATED_FROM,
|
||||
id2=SPDXID_SOURCE_PACKAGE1))
|
||||
|
||||
def test_tagvalue_writer(self):
|
||||
with io.StringIO() as output:
|
||||
sbom_writers.TagValueWriter.write(self.sbom_doc, output)
|
||||
expected_output = pathlib.Path('testdata/expected_tagvalue_sbom.spdx').read_text()
|
||||
self.maxDiff = None
|
||||
self.assertEqual(expected_output, output.getvalue())
|
||||
|
||||
def test_tagvalue_writer_unbundled(self):
|
||||
with io.StringIO() as output:
|
||||
sbom_writers.TagValueWriter.write(self.unbundled_sbom_doc, output, fragment=True)
|
||||
expected_output = pathlib.Path('testdata/expected_tagvalue_sbom_unbundled.spdx').read_text()
|
||||
self.maxDiff = None
|
||||
self.assertEqual(expected_output, output.getvalue())
|
||||
|
||||
def test_json_writer(self):
|
||||
with io.StringIO() as output:
|
||||
sbom_writers.JSONWriter.write(self.sbom_doc, output)
|
||||
expected_output = pathlib.Path('testdata/expected_json_sbom.spdx.json').read_text()
|
||||
self.maxDiff = None
|
||||
self.assertEqual(expected_output, output.getvalue())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(verbosity=2)
|
137
tools/sbom/testdata/expected_json_sbom.spdx.json
vendored
Normal file
137
tools/sbom/testdata/expected_json_sbom.spdx.json
vendored
Normal file
|
@ -0,0 +1,137 @@
|
|||
{
|
||||
"spdxVersion": "SPDX-2.3",
|
||||
"dataLicense": "CC0-1.0",
|
||||
"SPDXID": "SPDXRef-DOCUMENT",
|
||||
"name": "test doc",
|
||||
"documentNamespace": "http://www.google.com/sbom/spdx/android",
|
||||
"creationInfo": {
|
||||
"creators": [
|
||||
"Organization: Google"
|
||||
],
|
||||
"created": "2023-03-31T22:17:58Z"
|
||||
},
|
||||
"externalDocumentRefs": [
|
||||
{
|
||||
"externalDocumentId": "DocumentRef-external_doc_ref",
|
||||
"spdxDocument": "external_doc_uri",
|
||||
"checksum": {
|
||||
"algorithm": "SHA1",
|
||||
"checksumValue": "1234567890"
|
||||
}
|
||||
}
|
||||
],
|
||||
"documentDescribes": [
|
||||
"SPDXRef-PRODUCT"
|
||||
],
|
||||
"packages": [
|
||||
{
|
||||
"name": "PRODUCT",
|
||||
"SPDXID": "SPDXRef-PRODUCT",
|
||||
"downloadLocation": "NONE",
|
||||
"filesAnalyzed": true,
|
||||
"versionInfo": "build_finger_print",
|
||||
"supplier": "Organization: Google",
|
||||
"packageVerificationCode": {
|
||||
"packageVerificationCodeValue": "123456"
|
||||
},
|
||||
"hasFiles": [
|
||||
"SPDXRef-file1",
|
||||
"SPDXRef-file2",
|
||||
"SPDXRef-file3"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "PLATFORM",
|
||||
"SPDXID": "SPDXRef-PLATFORM",
|
||||
"downloadLocation": "NONE",
|
||||
"filesAnalyzed": false,
|
||||
"versionInfo": "build_finger_print",
|
||||
"supplier": "Organization: Google"
|
||||
},
|
||||
{
|
||||
"name": "Prebuilt package1",
|
||||
"SPDXID": "SPDXRef-PREBUILT-package1",
|
||||
"downloadLocation": "NONE",
|
||||
"filesAnalyzed": false,
|
||||
"versionInfo": "build_finger_print",
|
||||
"supplier": "Organization: Google"
|
||||
},
|
||||
{
|
||||
"name": "Source package1",
|
||||
"SPDXID": "SPDXRef-SOURCE-package1",
|
||||
"downloadLocation": "NONE",
|
||||
"filesAnalyzed": false,
|
||||
"versionInfo": "build_finger_print",
|
||||
"supplier": "Organization: Google",
|
||||
"externalRefs": [
|
||||
{
|
||||
"referenceCategory": "SECURITY",
|
||||
"referenceType": "cpe22Type",
|
||||
"referenceLocator": "cpe:/a:jsoncpp_project:jsoncpp:1.9.4"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Upstream package1",
|
||||
"SPDXID": "SPDXRef-UPSTREAM-package1",
|
||||
"downloadLocation": "NONE",
|
||||
"filesAnalyzed": false,
|
||||
"versionInfo": "1.1",
|
||||
"supplier": "Organization: upstream"
|
||||
}
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"fileName": "/bin/file1",
|
||||
"SPDXID": "SPDXRef-file1",
|
||||
"checksums": [
|
||||
{
|
||||
"algorithm": "SHA1",
|
||||
"checksumValue": "11111"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"fileName": "/bin/file2",
|
||||
"SPDXID": "SPDXRef-file2",
|
||||
"checksums": [
|
||||
{
|
||||
"algorithm": "SHA1",
|
||||
"checksumValue": "22222"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"fileName": "/bin/file3",
|
||||
"SPDXID": "SPDXRef-file3",
|
||||
"checksums": [
|
||||
{
|
||||
"algorithm": "SHA1",
|
||||
"checksumValue": "33333"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"relationships": [
|
||||
{
|
||||
"spdxElementId": "SPDXRef-file1",
|
||||
"relatedSpdxElement": "SPDXRef-PLATFORM",
|
||||
"relationshipType": "GENERATED_FROM"
|
||||
},
|
||||
{
|
||||
"spdxElementId": "SPDXRef-file2",
|
||||
"relatedSpdxElement": "SPDXRef-PREBUILT-package1",
|
||||
"relationshipType": "GENERATED_FROM"
|
||||
},
|
||||
{
|
||||
"spdxElementId": "SPDXRef-file3",
|
||||
"relatedSpdxElement": "SPDXRef-SOURCE-package1",
|
||||
"relationshipType": "GENERATED_FROM"
|
||||
},
|
||||
{
|
||||
"spdxElementId": "SPDXRef-SOURCE-package1",
|
||||
"relatedSpdxElement": "SPDXRef-UPSTREAM-package1",
|
||||
"relationshipType": "VARIANT_OF"
|
||||
}
|
||||
]
|
||||
}
|
65
tools/sbom/testdata/expected_tagvalue_sbom.spdx
vendored
Normal file
65
tools/sbom/testdata/expected_tagvalue_sbom.spdx
vendored
Normal file
|
@ -0,0 +1,65 @@
|
|||
SPDXVersion: SPDX-2.3
|
||||
DataLicense: CC0-1.0
|
||||
SPDXID: SPDXRef-DOCUMENT
|
||||
DocumentName: test doc
|
||||
DocumentNamespace: http://www.google.com/sbom/spdx/android
|
||||
Creator: Organization: Google
|
||||
Created: 2023-03-31T22:17:58Z
|
||||
ExternalDocumentRef: DocumentRef-external_doc_ref external_doc_uri SHA1: 1234567890
|
||||
|
||||
PackageName: PRODUCT
|
||||
SPDXID: SPDXRef-PRODUCT
|
||||
PackageDownloadLocation: NONE
|
||||
FilesAnalyzed: true
|
||||
PackageVersion: build_finger_print
|
||||
PackageSupplier: Organization: Google
|
||||
PackageVerificationCode: 123456
|
||||
|
||||
Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-PRODUCT
|
||||
|
||||
FileName: /bin/file1
|
||||
SPDXID: SPDXRef-file1
|
||||
FileChecksum: SHA1: 11111
|
||||
|
||||
FileName: /bin/file2
|
||||
SPDXID: SPDXRef-file2
|
||||
FileChecksum: SHA1: 22222
|
||||
|
||||
FileName: /bin/file3
|
||||
SPDXID: SPDXRef-file3
|
||||
FileChecksum: SHA1: 33333
|
||||
|
||||
PackageName: PLATFORM
|
||||
SPDXID: SPDXRef-PLATFORM
|
||||
PackageDownloadLocation: NONE
|
||||
FilesAnalyzed: false
|
||||
PackageVersion: build_finger_print
|
||||
PackageSupplier: Organization: Google
|
||||
|
||||
PackageName: Prebuilt package1
|
||||
SPDXID: SPDXRef-PREBUILT-package1
|
||||
PackageDownloadLocation: NONE
|
||||
FilesAnalyzed: false
|
||||
PackageVersion: build_finger_print
|
||||
PackageSupplier: Organization: Google
|
||||
|
||||
PackageName: Source package1
|
||||
SPDXID: SPDXRef-SOURCE-package1
|
||||
PackageDownloadLocation: NONE
|
||||
FilesAnalyzed: false
|
||||
PackageVersion: build_finger_print
|
||||
PackageSupplier: Organization: Google
|
||||
ExternalRef: SECURITY cpe22Type cpe:/a:jsoncpp_project:jsoncpp:1.9.4
|
||||
|
||||
PackageName: Upstream package1
|
||||
SPDXID: SPDXRef-UPSTREAM-package1
|
||||
PackageDownloadLocation: NONE
|
||||
FilesAnalyzed: false
|
||||
PackageVersion: 1.1
|
||||
PackageSupplier: Organization: upstream
|
||||
|
||||
Relationship: SPDXRef-SOURCE-package1 VARIANT_OF SPDXRef-UPSTREAM-package1
|
||||
|
||||
Relationship: SPDXRef-file1 GENERATED_FROM SPDXRef-PLATFORM
|
||||
Relationship: SPDXRef-file2 GENERATED_FROM SPDXRef-PREBUILT-package1
|
||||
Relationship: SPDXRef-file3 GENERATED_FROM SPDXRef-SOURCE-package1
|
12
tools/sbom/testdata/expected_tagvalue_sbom_unbundled.spdx
vendored
Normal file
12
tools/sbom/testdata/expected_tagvalue_sbom_unbundled.spdx
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
FileName: /bin/file1.apk
|
||||
SPDXID: SPDXRef-file1
|
||||
FileChecksum: SHA1: 11111
|
||||
|
||||
PackageName: Unbundled apk package
|
||||
SPDXID: SPDXRef-SOURCE-package1
|
||||
PackageDownloadLocation: NONE
|
||||
FilesAnalyzed: false
|
||||
PackageVersion: build_finger_print
|
||||
PackageSupplier: Organization: Google
|
||||
|
||||
Relationship: SPDXRef-file1 GENERATED_FROM SPDXRef-SOURCE-package1
|
Loading…
Reference in a new issue