blob: 1cb864db758dc62c4039dbb599e2afde225663d4 [file] [log] [blame]
#!/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(sbom_doc, package, fragment):
download_location = sbom_data.VALUE_NOASSERTION
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('')
if package.id == sbom_doc.describes and not fragment:
tagvalues.append(
f'{Tags.RELATIONSHIP}: {sbom_doc.id} {sbom_data.RelationshipType.DESCRIBES} {sbom_doc.describes}')
tagvalues.append('')
for file in sbom_doc.files:
if file.id in package.file_ids:
tagvalues += TagValueWriter.marshal_file(file)
return tagvalues
@staticmethod
def marshal_packages(sbom_doc, fragment):
tagvalues = []
marshaled_relationships = []
i = 0
packages = sbom_doc.packages
while i < len(packages):
if (i + 1 < len(packages)
and packages[i].id.startswith('SPDXRef-SOURCE-')
and packages[i + 1].id.startswith('SPDXRef-UPSTREAM-')):
# Output SOURCE, UPSTREAM packages and their VARIANT_OF relationship together, so they are close to each other
# in SBOMs in tagvalue format.
tagvalues += TagValueWriter.marshal_package(sbom_doc, packages[i], fragment)
tagvalues += TagValueWriter.marshal_package(sbom_doc, packages[i + 1], fragment)
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(sbom_doc, packages[i], fragment)
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, fragment):
tagvalues = []
files_in_packages = []
for package in sbom_doc.packages:
files_in_packages += package.file_ids
for file in sbom_doc.files:
if file.id in files_in_packages:
continue
tagvalues += TagValueWriter.marshal_file(file)
if file.id == sbom_doc.describes and not fragment:
# Fragment is not a full SBOM document so the relationship DESCRIBES is not applicable.
tagvalues.append(
f'{Tags.RELATIONSHIP}: {sbom_doc.id} {sbom_data.RelationshipType.DESCRIBES} {sbom_doc.describes}')
tagvalues.append('')
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)
content += TagValueWriter.marshal_files(sbom_doc, fragment)
tagvalues, marshaled_relationships = TagValueWriter.marshal_packages(sbom_doc, fragment)
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 sbom_data.VALUE_NOASSERTION,
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))