Merge "Create separate python libraries for the following logic and refactor SBOM generation script accordingly."
This commit is contained in:
@@ -70,22 +70,6 @@ python_binary_host {
|
|||||||
srcs: ["generate_gts_shared_report.py"],
|
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 {
|
python_binary_host {
|
||||||
name: "list_files",
|
name: "list_files",
|
||||||
main: "list_files.py",
|
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 datetime
|
||||||
import google.protobuf.text_format as text_format
|
import google.protobuf.text_format as text_format
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
|
||||||
import os
|
import os
|
||||||
import metadata_file_pb2
|
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
|
# Package type
|
||||||
PKG_SOURCE = 'SOURCE'
|
PKG_SOURCE = 'SOURCE'
|
||||||
@@ -111,44 +72,6 @@ def log(*info):
|
|||||||
print(i)
|
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):
|
def encode_for_spdxid(s):
|
||||||
"""Simple encode for string values used in SPDXID which uses the charset of A-Za-Z0-9.-"""
|
"""Simple encode for string values used in SPDXID which uses the charset of A-Za-Z0-9.-"""
|
||||||
result = ''
|
result = ''
|
||||||
@@ -167,19 +90,10 @@ def new_package_id(package_name, type):
|
|||||||
return f'SPDXRef-{type}-{encode_for_spdxid(package_name)}'
|
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):
|
def new_file_id(file_path):
|
||||||
return f'SPDXRef-{encode_for_spdxid(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):
|
def checksum(file_path):
|
||||||
file_path = args.product_out_dir + '/' + file_path
|
file_path = args.product_out_dir + '/' + file_path
|
||||||
h = hashlib.sha1()
|
h = hashlib.sha1()
|
||||||
@@ -243,6 +157,11 @@ def is_prebuilt_package(file_metadata):
|
|||||||
|
|
||||||
|
|
||||||
def get_source_package_info(file_metadata, metadata_file_path):
|
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:
|
if not metadata_file_path:
|
||||||
return file_metadata['module_path'], []
|
return file_metadata['module_path'], []
|
||||||
|
|
||||||
@@ -250,9 +169,15 @@ def get_source_package_info(file_metadata, metadata_file_path):
|
|||||||
external_refs = []
|
external_refs = []
|
||||||
for tag in metadata_proto.third_party.security.tag:
|
for tag in metadata_proto.third_party.security.tag:
|
||||||
if tag.lower().startswith((NVD_CPE23 + 'cpe:2.3:').lower()):
|
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()):
|
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:
|
if metadata_proto.name:
|
||||||
return metadata_proto.name, external_refs
|
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):
|
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
|
name = None
|
||||||
if metadata_file_path:
|
if metadata_file_path:
|
||||||
metadata_proto = metadata_file_protos[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):
|
def get_metadata_file_path(file_metadata):
|
||||||
|
"""Search for METADATA file of a package and return its path."""
|
||||||
metadata_path = ''
|
metadata_path = ''
|
||||||
if file_metadata['module_path']:
|
if file_metadata['module_path']:
|
||||||
metadata_path = 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):
|
def get_package_version(metadata_file_path):
|
||||||
|
"""Return a package's version in its METADATA file."""
|
||||||
if not metadata_file_path:
|
if not metadata_file_path:
|
||||||
return None
|
return None
|
||||||
metadata_proto = metadata_file_protos[metadata_file_path]
|
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):
|
def get_package_homepage(metadata_file_path):
|
||||||
|
"""Return a package's homepage URL in its METADATA file."""
|
||||||
if not metadata_file_path:
|
if not metadata_file_path:
|
||||||
return None
|
return None
|
||||||
metadata_proto = metadata_file_protos[metadata_file_path]
|
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):
|
def get_package_download_location(metadata_file_path):
|
||||||
|
"""Return a package's code repository URL in its METADATA file."""
|
||||||
if not metadata_file_path:
|
if not metadata_file_path:
|
||||||
return None
|
return None
|
||||||
metadata_proto = metadata_file_protos[metadata_file_path]
|
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):
|
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
|
external_doc_ref = None
|
||||||
packages = []
|
packages = []
|
||||||
relationships = []
|
relationships = []
|
||||||
@@ -338,18 +278,26 @@ def get_sbom_fragments(installed_file_metadata, metadata_file_path):
|
|||||||
# Source fork packages
|
# Source fork packages
|
||||||
name, external_refs = get_source_package_info(installed_file_metadata, metadata_file_path)
|
name, external_refs = get_source_package_info(installed_file_metadata, metadata_file_path)
|
||||||
source_package_id = new_package_id(name, PKG_SOURCE)
|
source_package_id = new_package_id(name, PKG_SOURCE)
|
||||||
source_package = new_package_record(source_package_id, name, args.build_version, args.product_mfr,
|
source_package = sbom_data.Package(id=source_package_id, name=name, version=args.build_version,
|
||||||
external_refs=external_refs)
|
supplier='Organization: ' + args.product_mfr,
|
||||||
|
external_refs=external_refs)
|
||||||
|
|
||||||
upstream_package_id = new_package_id(name, PKG_UPSTREAM)
|
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]
|
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):
|
elif is_prebuilt_package(installed_file_metadata):
|
||||||
# Prebuilt fork packages
|
# Prebuilt fork packages
|
||||||
name = get_prebuilt_package_name(installed_file_metadata, metadata_file_path)
|
name = get_prebuilt_package_name(installed_file_metadata, metadata_file_path)
|
||||||
prebuilt_package_id = new_package_id(name, PKG_PREBUILT)
|
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)
|
packages.append(prebuilt_package)
|
||||||
|
|
||||||
if metadata_file_path:
|
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
|
sbom_checksum = metadata_proto.third_party.sbom_ref.checksum
|
||||||
upstream_element_id = metadata_proto.third_party.sbom_ref.element_id
|
upstream_element_id = metadata_proto.third_party.sbom_ref.element_id
|
||||||
if sbom_url and sbom_checksum and upstream_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(
|
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
|
return external_doc_ref, packages, relationships
|
||||||
|
|
||||||
|
|
||||||
def generate_package_verification_code(files):
|
def generate_package_verification_code(files):
|
||||||
checksums = [file[FILE_CHECKSUM] for file in files]
|
checksums = [file.checksum for file in files]
|
||||||
checksums.sort()
|
checksums.sort()
|
||||||
h = hashlib.sha1()
|
h = hashlib.sha1()
|
||||||
h.update(''.join(checksums).encode(encoding='utf-8'))
|
h.update(''.join(checksums).encode(encoding='utf-8'))
|
||||||
return h.hexdigest()
|
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):
|
def save_report(report):
|
||||||
prefix, _ = os.path.splitext(args.output_file)
|
prefix, _ = os.path.splitext(args.output_file)
|
||||||
with open(prefix + '-gen-report.txt', 'w', encoding='utf-8') as report_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')
|
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.
|
# 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):
|
def installed_file_has_metadata(installed_file_metadata, report):
|
||||||
installed_file = installed_file_metadata['installed_file']
|
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']))
|
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:
|
with open(args.metadata, newline='') as sbom_metadata_file:
|
||||||
reader = csv.DictReader(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:
|
for installed_file_metadata in reader:
|
||||||
installed_file = installed_file_metadata['installed_file']
|
installed_file = installed_file_metadata['installed_file']
|
||||||
if args.output_file != args.product_out_dir + installed_file + ".spdx":
|
if args.output_file != args.product_out_dir + installed_file + ".spdx":
|
||||||
continue
|
continue
|
||||||
|
|
||||||
module_path = installed_file_metadata['module_path']
|
module_path = installed_file_metadata['module_path']
|
||||||
package_id = new_package_id(encode_for_spdxid(module_path), PKG_PREBUILT)
|
package_id = new_package_id(module_path, PKG_PREBUILT)
|
||||||
package = new_package_record(package_id, module_path, args.build_version, args.product_mfr)
|
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_id = new_file_id(installed_file)
|
||||||
file = new_file_record(file_id, installed_file, checksum(installed_file))
|
file = sbom_data.File(id=file_id, name=installed_file, checksum=checksum(installed_file))
|
||||||
relationship = new_relationship_record(file_id, REL_GENERATED_FROM, package_id)
|
relationship = sbom_data.Relationship(id1=file_id,
|
||||||
records = [package, file, relationship]
|
relationship=sbom_data.RelationshipType.GENERATED_FROM,
|
||||||
write_tagvalue_sbom(records)
|
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
|
break
|
||||||
|
|
||||||
|
with open(args.output_file, 'w', encoding="utf-8") as file:
|
||||||
|
sbom_writers.TagValueWriter.write(doc, file, fragment=True)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
global args
|
global args
|
||||||
@@ -580,21 +426,27 @@ def main():
|
|||||||
log('Args:', vars(args))
|
log('Args:', vars(args))
|
||||||
|
|
||||||
if args.unbundled:
|
if args.unbundled:
|
||||||
generate_fragment()
|
generate_sbom_for_unbundled()
|
||||||
return
|
return
|
||||||
|
|
||||||
global metadata_file_protos
|
global metadata_file_protos
|
||||||
metadata_file_protos = {}
|
metadata_file_protos = {}
|
||||||
|
|
||||||
doc_id = 'SPDXRef-DOCUMENT'
|
doc = sbom_data.Document(name=args.build_version,
|
||||||
doc_header = new_doc_header(doc_id)
|
namespace=f'https://www.google.com/sbom/spdx/android/{args.build_version}',
|
||||||
|
creators=['Organization: ' + args.product_mfr])
|
||||||
|
|
||||||
product_package_id = 'SPDXRef-PRODUCT'
|
product_package = sbom_data.Package(id=sbom_data.SPDXID_PRODUCT,
|
||||||
product_package = new_package_record(product_package_id, 'PRODUCT', args.build_version, args.product_mfr,
|
name=sbom_data.PACKAGE_NAME_PRODUCT,
|
||||||
files_analyzed='true')
|
version=args.build_version,
|
||||||
|
supplier='Organization: ' + args.product_mfr,
|
||||||
|
files_analyzed=True)
|
||||||
|
doc.packages.append(product_package)
|
||||||
|
|
||||||
platform_package_id = 'SPDXRef-PLATFORM'
|
doc.packages.append(sbom_data.Package(id=sbom_data.SPDXID_PLATFORM,
|
||||||
platform_package = new_package_record(platform_package_id, 'PLATFORM', args.build_version, args.product_mfr)
|
name=sbom_data.PACKAGE_NAME_PLATFORM,
|
||||||
|
version=args.build_version,
|
||||||
|
supplier='Organization: ' + args.product_mfr))
|
||||||
|
|
||||||
# Report on some issues and information
|
# Report on some issues and information
|
||||||
report = {
|
report = {
|
||||||
@@ -607,10 +459,6 @@ def main():
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Scan the metadata in CSV file and create the corresponding package and file records in SPDX
|
# 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:
|
with open(args.metadata, newline='') as sbom_metadata_file:
|
||||||
reader = csv.DictReader(sbom_metadata_file)
|
reader = csv.DictReader(sbom_metadata_file)
|
||||||
for installed_file_metadata in reader:
|
for installed_file_metadata in reader:
|
||||||
@@ -627,7 +475,9 @@ def main():
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
file_id = new_file_id(installed_file)
|
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):
|
if is_source_package(installed_file_metadata) or is_prebuilt_package(installed_file_metadata):
|
||||||
metadata_file_path = get_metadata_file_path(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
|
# File from source fork packages or prebuilt fork packages
|
||||||
external_doc_ref, pkgs, rels = get_sbom_fragments(installed_file_metadata, metadata_file_path)
|
external_doc_ref, pkgs, rels = get_sbom_fragments(installed_file_metadata, metadata_file_path)
|
||||||
if len(pkgs) > 0:
|
if len(pkgs) > 0:
|
||||||
if external_doc_ref and external_doc_ref not in doc_header[EXTERNAL_DOCUMENT_REF]:
|
if external_doc_ref:
|
||||||
doc_header[EXTERNAL_DOCUMENT_REF].append(external_doc_ref)
|
doc.add_external_ref(external_doc_ref)
|
||||||
for p in pkgs:
|
for p in pkgs:
|
||||||
if not p[SPDXID] in package_ids:
|
doc.add_package(p)
|
||||||
package_ids.append(p[SPDXID])
|
|
||||||
package_records.append(p)
|
|
||||||
for rel in rels:
|
for rel in rels:
|
||||||
if not rel in package_records:
|
doc.add_relationship(rel)
|
||||||
package_records.append(rel)
|
fork_package_id = pkgs[0].id # The first package should be the source/prebuilt fork package
|
||||||
fork_package_id = pkgs[0][SPDXID] # The first package should be the source/prebuilt fork package
|
doc.add_relationship(sbom_data.Relationship(id1=file_id,
|
||||||
rels_file_gen_from.append(new_relationship_record(file_id, REL_GENERATED_FROM, fork_package_id))
|
relationship=sbom_data.RelationshipType.GENERATED_FROM,
|
||||||
|
id2=fork_package_id))
|
||||||
elif module_path or installed_file_metadata['is_platform_generated']:
|
elif module_path or installed_file_metadata['is_platform_generated']:
|
||||||
# File from PLATFORM package
|
# 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:
|
elif product_copy_files:
|
||||||
# Format of product_copy_files: <source path>:<dest path>
|
# Format of product_copy_files: <source path>:<dest path>
|
||||||
src_path = product_copy_files.split(':')[0]
|
src_path = product_copy_files.split(':')[0]
|
||||||
# So far product_copy_files are copied from directory system, kernel, hardware, frameworks and device,
|
# So far product_copy_files are copied from directory system, kernel, hardware, frameworks and device,
|
||||||
# so process them as files from PLATFORM package
|
# 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'):
|
elif installed_file.endswith('.fsv_meta'):
|
||||||
# See build/make/core/Makefile:2988
|
# 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'):
|
elif kernel_module_copy_files.startswith('ANDROID-GEN'):
|
||||||
# For the four files generated for _dlkm, _ramdisk partitions
|
# For the four files generated for _dlkm, _ramdisk partitions
|
||||||
# See build/make/core/Makefile:323
|
# 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)
|
product_package.verification_code = generate_package_verification_code(doc.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
|
|
||||||
|
|
||||||
# Save SBOM records to output file
|
# Save SBOM records to output file
|
||||||
doc_header[CREATED] = datetime.datetime.now(tz=datetime.timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
|
doc.created = datetime.datetime.now(tz=datetime.timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||||
write_tagvalue_sbom(all_records)
|
with open(args.output_file, 'w', encoding="utf-8") as file:
|
||||||
|
sbom_writers.TagValueWriter.write(doc, file)
|
||||||
if args.json:
|
if args.json:
|
||||||
write_json_sbom(all_records, product_package_id)
|
with open(args.output_file+'.json', 'w', encoding="utf-8") as file:
|
||||||
|
sbom_writers.JSONWriter.write(doc, file)
|
||||||
save_report(report)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
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
|
Reference in New Issue
Block a user