xref: /aosp_15_r20/build/make/tools/sbom/sbom_writers.py (revision 9e94795a3d4ef5c1d47486f9a02bb378756cea8a)
1*9e94795aSAndroid Build Coastguard Worker#!/usr/bin/env python3
2*9e94795aSAndroid Build Coastguard Worker#
3*9e94795aSAndroid Build Coastguard Worker# Copyright (C) 2023 The Android Open Source Project
4*9e94795aSAndroid Build Coastguard Worker#
5*9e94795aSAndroid Build Coastguard Worker# Licensed under the Apache License, Version 2.0 (the "License");
6*9e94795aSAndroid Build Coastguard Worker# you may not use this file except in compliance with the License.
7*9e94795aSAndroid Build Coastguard Worker# You may obtain a copy of the License at
8*9e94795aSAndroid Build Coastguard Worker#
9*9e94795aSAndroid Build Coastguard Worker#      http://www.apache.org/licenses/LICENSE-2.0
10*9e94795aSAndroid Build Coastguard Worker#
11*9e94795aSAndroid Build Coastguard Worker# Unless required by applicable law or agreed to in writing, software
12*9e94795aSAndroid Build Coastguard Worker# distributed under the License is distributed on an "AS IS" BASIS,
13*9e94795aSAndroid Build Coastguard Worker# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14*9e94795aSAndroid Build Coastguard Worker# See the License for the specific language governing permissions and
15*9e94795aSAndroid Build Coastguard Worker# limitations under the License.
16*9e94795aSAndroid Build Coastguard Worker
17*9e94795aSAndroid Build Coastguard Worker"""
18*9e94795aSAndroid Build Coastguard WorkerSerialize objects defined in package sbom_data to SPDX format: tagvalue, JSON.
19*9e94795aSAndroid Build Coastguard Worker"""
20*9e94795aSAndroid Build Coastguard Worker
21*9e94795aSAndroid Build Coastguard Workerimport json
22*9e94795aSAndroid Build Coastguard Workerimport sbom_data
23*9e94795aSAndroid Build Coastguard Worker
24*9e94795aSAndroid Build Coastguard WorkerSPDX_VER = 'SPDX-2.3'
25*9e94795aSAndroid Build Coastguard WorkerDATA_LIC = 'CC0-1.0'
26*9e94795aSAndroid Build Coastguard Worker
27*9e94795aSAndroid Build Coastguard Worker
28*9e94795aSAndroid Build Coastguard Workerclass Tags:
29*9e94795aSAndroid Build Coastguard Worker  # Common
30*9e94795aSAndroid Build Coastguard Worker  SPDXID = 'SPDXID'
31*9e94795aSAndroid Build Coastguard Worker  SPDX_VERSION = 'SPDXVersion'
32*9e94795aSAndroid Build Coastguard Worker  DATA_LICENSE = 'DataLicense'
33*9e94795aSAndroid Build Coastguard Worker  DOCUMENT_NAME = 'DocumentName'
34*9e94795aSAndroid Build Coastguard Worker  DOCUMENT_NAMESPACE = 'DocumentNamespace'
35*9e94795aSAndroid Build Coastguard Worker  CREATED = 'Created'
36*9e94795aSAndroid Build Coastguard Worker  CREATOR = 'Creator'
37*9e94795aSAndroid Build Coastguard Worker  EXTERNAL_DOCUMENT_REF = 'ExternalDocumentRef'
38*9e94795aSAndroid Build Coastguard Worker
39*9e94795aSAndroid Build Coastguard Worker  # Package
40*9e94795aSAndroid Build Coastguard Worker  PACKAGE_NAME = 'PackageName'
41*9e94795aSAndroid Build Coastguard Worker  PACKAGE_DOWNLOAD_LOCATION = 'PackageDownloadLocation'
42*9e94795aSAndroid Build Coastguard Worker  PACKAGE_VERSION = 'PackageVersion'
43*9e94795aSAndroid Build Coastguard Worker  PACKAGE_SUPPLIER = 'PackageSupplier'
44*9e94795aSAndroid Build Coastguard Worker  FILES_ANALYZED = 'FilesAnalyzed'
45*9e94795aSAndroid Build Coastguard Worker  PACKAGE_VERIFICATION_CODE = 'PackageVerificationCode'
46*9e94795aSAndroid Build Coastguard Worker  PACKAGE_EXTERNAL_REF = 'ExternalRef'
47*9e94795aSAndroid Build Coastguard Worker  # Package license
48*9e94795aSAndroid Build Coastguard Worker  PACKAGE_LICENSE_CONCLUDED = 'PackageLicenseConcluded'
49*9e94795aSAndroid Build Coastguard Worker  PACKAGE_LICENSE_INFO_FROM_FILES = 'PackageLicenseInfoFromFiles'
50*9e94795aSAndroid Build Coastguard Worker  PACKAGE_LICENSE_DECLARED = 'PackageLicenseDeclared'
51*9e94795aSAndroid Build Coastguard Worker  PACKAGE_LICENSE_COMMENTS = 'PackageLicenseComments'
52*9e94795aSAndroid Build Coastguard Worker
53*9e94795aSAndroid Build Coastguard Worker  # File
54*9e94795aSAndroid Build Coastguard Worker  FILE_NAME = 'FileName'
55*9e94795aSAndroid Build Coastguard Worker  FILE_CHECKSUM = 'FileChecksum'
56*9e94795aSAndroid Build Coastguard Worker  # File license
57*9e94795aSAndroid Build Coastguard Worker  FILE_LICENSE_CONCLUDED = 'LicenseConcluded'
58*9e94795aSAndroid Build Coastguard Worker  FILE_LICENSE_INFO_IN_FILE = 'LicenseInfoInFile'
59*9e94795aSAndroid Build Coastguard Worker  FILE_LICENSE_COMMENTS = 'LicenseComments'
60*9e94795aSAndroid Build Coastguard Worker  FILE_COPYRIGHT_TEXT = 'FileCopyrightText'
61*9e94795aSAndroid Build Coastguard Worker  FILE_NOTICE = 'FileNotice'
62*9e94795aSAndroid Build Coastguard Worker  FILE_ATTRIBUTION_TEXT = 'FileAttributionText'
63*9e94795aSAndroid Build Coastguard Worker
64*9e94795aSAndroid Build Coastguard Worker  # Relationship
65*9e94795aSAndroid Build Coastguard Worker  RELATIONSHIP = 'Relationship'
66*9e94795aSAndroid Build Coastguard Worker
67*9e94795aSAndroid Build Coastguard Worker  # License
68*9e94795aSAndroid Build Coastguard Worker  LICENSE_ID = 'LicenseID'
69*9e94795aSAndroid Build Coastguard Worker  LICENSE_NAME = 'LicenseName'
70*9e94795aSAndroid Build Coastguard Worker  LICENSE_EXTRACTED_TEXT = 'ExtractedText'
71*9e94795aSAndroid Build Coastguard Worker
72*9e94795aSAndroid Build Coastguard Worker
73*9e94795aSAndroid Build Coastguard Workerclass TagValueWriter:
74*9e94795aSAndroid Build Coastguard Worker  @staticmethod
75*9e94795aSAndroid Build Coastguard Worker  def marshal_doc_headers(sbom_doc):
76*9e94795aSAndroid Build Coastguard Worker    headers = [
77*9e94795aSAndroid Build Coastguard Worker      f'{Tags.SPDX_VERSION}: {SPDX_VER}',
78*9e94795aSAndroid Build Coastguard Worker      f'{Tags.DATA_LICENSE}: {DATA_LIC}',
79*9e94795aSAndroid Build Coastguard Worker      f'{Tags.SPDXID}: {sbom_doc.id}',
80*9e94795aSAndroid Build Coastguard Worker      f'{Tags.DOCUMENT_NAME}: {sbom_doc.name}',
81*9e94795aSAndroid Build Coastguard Worker      f'{Tags.DOCUMENT_NAMESPACE}: {sbom_doc.namespace}',
82*9e94795aSAndroid Build Coastguard Worker    ]
83*9e94795aSAndroid Build Coastguard Worker    for creator in sbom_doc.creators:
84*9e94795aSAndroid Build Coastguard Worker      headers.append(f'{Tags.CREATOR}: {creator}')
85*9e94795aSAndroid Build Coastguard Worker    headers.append(f'{Tags.CREATED}: {sbom_doc.created}')
86*9e94795aSAndroid Build Coastguard Worker    for doc_ref in sbom_doc.external_refs:
87*9e94795aSAndroid Build Coastguard Worker      headers.append(
88*9e94795aSAndroid Build Coastguard Worker        f'{Tags.EXTERNAL_DOCUMENT_REF}: {doc_ref.id} {doc_ref.uri} {doc_ref.checksum}')
89*9e94795aSAndroid Build Coastguard Worker    headers.append('')
90*9e94795aSAndroid Build Coastguard Worker    return headers
91*9e94795aSAndroid Build Coastguard Worker
92*9e94795aSAndroid Build Coastguard Worker  @staticmethod
93*9e94795aSAndroid Build Coastguard Worker  def marshal_package(sbom_doc, package, fragment):
94*9e94795aSAndroid Build Coastguard Worker    download_location = sbom_data.VALUE_NOASSERTION
95*9e94795aSAndroid Build Coastguard Worker    if package.download_location:
96*9e94795aSAndroid Build Coastguard Worker      download_location = package.download_location
97*9e94795aSAndroid Build Coastguard Worker    tagvalues = [
98*9e94795aSAndroid Build Coastguard Worker      f'{Tags.PACKAGE_NAME}: {package.name}',
99*9e94795aSAndroid Build Coastguard Worker      f'{Tags.SPDXID}: {package.id}',
100*9e94795aSAndroid Build Coastguard Worker      f'{Tags.PACKAGE_DOWNLOAD_LOCATION}: {download_location}',
101*9e94795aSAndroid Build Coastguard Worker      f'{Tags.FILES_ANALYZED}: {str(package.files_analyzed).lower()}',
102*9e94795aSAndroid Build Coastguard Worker    ]
103*9e94795aSAndroid Build Coastguard Worker    if package.version:
104*9e94795aSAndroid Build Coastguard Worker      tagvalues.append(f'{Tags.PACKAGE_VERSION}: {package.version}')
105*9e94795aSAndroid Build Coastguard Worker    if package.supplier:
106*9e94795aSAndroid Build Coastguard Worker      tagvalues.append(f'{Tags.PACKAGE_SUPPLIER}: {package.supplier}')
107*9e94795aSAndroid Build Coastguard Worker
108*9e94795aSAndroid Build Coastguard Worker    license = sbom_data.VALUE_NOASSERTION
109*9e94795aSAndroid Build Coastguard Worker    if package.declared_license_ids:
110*9e94795aSAndroid Build Coastguard Worker      license = ' OR '.join(package.declared_license_ids)
111*9e94795aSAndroid Build Coastguard Worker    tagvalues.append(f'{Tags.PACKAGE_LICENSE_DECLARED}: {license}')
112*9e94795aSAndroid Build Coastguard Worker
113*9e94795aSAndroid Build Coastguard Worker    if package.verification_code:
114*9e94795aSAndroid Build Coastguard Worker      tagvalues.append(f'{Tags.PACKAGE_VERIFICATION_CODE}: {package.verification_code}')
115*9e94795aSAndroid Build Coastguard Worker    if package.external_refs:
116*9e94795aSAndroid Build Coastguard Worker      for external_ref in package.external_refs:
117*9e94795aSAndroid Build Coastguard Worker        tagvalues.append(
118*9e94795aSAndroid Build Coastguard Worker          f'{Tags.PACKAGE_EXTERNAL_REF}: {external_ref.category} {external_ref.type} {external_ref.locator}')
119*9e94795aSAndroid Build Coastguard Worker
120*9e94795aSAndroid Build Coastguard Worker    tagvalues.append('')
121*9e94795aSAndroid Build Coastguard Worker
122*9e94795aSAndroid Build Coastguard Worker    if package.id == sbom_doc.describes and not fragment:
123*9e94795aSAndroid Build Coastguard Worker      tagvalues.append(
124*9e94795aSAndroid Build Coastguard Worker          f'{Tags.RELATIONSHIP}: {sbom_doc.id} {sbom_data.RelationshipType.DESCRIBES} {sbom_doc.describes}')
125*9e94795aSAndroid Build Coastguard Worker      tagvalues.append('')
126*9e94795aSAndroid Build Coastguard Worker
127*9e94795aSAndroid Build Coastguard Worker    for file in sbom_doc.files:
128*9e94795aSAndroid Build Coastguard Worker      if file.id in package.file_ids:
129*9e94795aSAndroid Build Coastguard Worker        tagvalues += TagValueWriter.marshal_file(file)
130*9e94795aSAndroid Build Coastguard Worker
131*9e94795aSAndroid Build Coastguard Worker    return tagvalues
132*9e94795aSAndroid Build Coastguard Worker
133*9e94795aSAndroid Build Coastguard Worker  @staticmethod
134*9e94795aSAndroid Build Coastguard Worker  def marshal_packages(sbom_doc, fragment):
135*9e94795aSAndroid Build Coastguard Worker    tagvalues = []
136*9e94795aSAndroid Build Coastguard Worker    marshaled_relationships = []
137*9e94795aSAndroid Build Coastguard Worker    i = 0
138*9e94795aSAndroid Build Coastguard Worker    packages = sbom_doc.packages
139*9e94795aSAndroid Build Coastguard Worker    while i < len(packages):
140*9e94795aSAndroid Build Coastguard Worker      if (i + 1 < len(packages)
141*9e94795aSAndroid Build Coastguard Worker          and packages[i].id.startswith('SPDXRef-SOURCE-')
142*9e94795aSAndroid Build Coastguard Worker          and packages[i + 1].id.startswith('SPDXRef-UPSTREAM-')):
143*9e94795aSAndroid Build Coastguard Worker        # Output SOURCE, UPSTREAM packages and their VARIANT_OF relationship together, so they are close to each other
144*9e94795aSAndroid Build Coastguard Worker        # in SBOMs in tagvalue format.
145*9e94795aSAndroid Build Coastguard Worker        tagvalues += TagValueWriter.marshal_package(sbom_doc, packages[i], fragment)
146*9e94795aSAndroid Build Coastguard Worker        tagvalues += TagValueWriter.marshal_package(sbom_doc, packages[i + 1], fragment)
147*9e94795aSAndroid Build Coastguard Worker        rel = next((r for r in sbom_doc.relationships if
148*9e94795aSAndroid Build Coastguard Worker                    r.id1 == packages[i].id and
149*9e94795aSAndroid Build Coastguard Worker                    r.id2 == packages[i + 1].id and
150*9e94795aSAndroid Build Coastguard Worker                    r.relationship == sbom_data.RelationshipType.VARIANT_OF), None)
151*9e94795aSAndroid Build Coastguard Worker        if rel:
152*9e94795aSAndroid Build Coastguard Worker          marshaled_relationships.append(rel)
153*9e94795aSAndroid Build Coastguard Worker          tagvalues.append(TagValueWriter.marshal_relationship(rel))
154*9e94795aSAndroid Build Coastguard Worker          tagvalues.append('')
155*9e94795aSAndroid Build Coastguard Worker
156*9e94795aSAndroid Build Coastguard Worker        i += 2
157*9e94795aSAndroid Build Coastguard Worker      else:
158*9e94795aSAndroid Build Coastguard Worker        tagvalues += TagValueWriter.marshal_package(sbom_doc, packages[i], fragment)
159*9e94795aSAndroid Build Coastguard Worker        i += 1
160*9e94795aSAndroid Build Coastguard Worker
161*9e94795aSAndroid Build Coastguard Worker    return tagvalues, marshaled_relationships
162*9e94795aSAndroid Build Coastguard Worker
163*9e94795aSAndroid Build Coastguard Worker  @staticmethod
164*9e94795aSAndroid Build Coastguard Worker  def marshal_file(file):
165*9e94795aSAndroid Build Coastguard Worker    tagvalues = [
166*9e94795aSAndroid Build Coastguard Worker      f'{Tags.FILE_NAME}: {file.name}',
167*9e94795aSAndroid Build Coastguard Worker      f'{Tags.SPDXID}: {file.id}',
168*9e94795aSAndroid Build Coastguard Worker      f'{Tags.FILE_CHECKSUM}: {file.checksum}',
169*9e94795aSAndroid Build Coastguard Worker    ]
170*9e94795aSAndroid Build Coastguard Worker    license = sbom_data.VALUE_NOASSERTION
171*9e94795aSAndroid Build Coastguard Worker    if file.concluded_license_ids:
172*9e94795aSAndroid Build Coastguard Worker      license = ' OR '.join(file.concluded_license_ids)
173*9e94795aSAndroid Build Coastguard Worker    tagvalues.append(f'{Tags.FILE_LICENSE_CONCLUDED}: {license}')
174*9e94795aSAndroid Build Coastguard Worker    tagvalues.append('')
175*9e94795aSAndroid Build Coastguard Worker
176*9e94795aSAndroid Build Coastguard Worker    return tagvalues
177*9e94795aSAndroid Build Coastguard Worker
178*9e94795aSAndroid Build Coastguard Worker  @staticmethod
179*9e94795aSAndroid Build Coastguard Worker  def marshal_files(sbom_doc, fragment):
180*9e94795aSAndroid Build Coastguard Worker    tagvalues = []
181*9e94795aSAndroid Build Coastguard Worker    files_in_packages = []
182*9e94795aSAndroid Build Coastguard Worker    for package in sbom_doc.packages:
183*9e94795aSAndroid Build Coastguard Worker      files_in_packages += package.file_ids
184*9e94795aSAndroid Build Coastguard Worker    for file in sbom_doc.files:
185*9e94795aSAndroid Build Coastguard Worker      if file.id in files_in_packages:
186*9e94795aSAndroid Build Coastguard Worker        continue
187*9e94795aSAndroid Build Coastguard Worker      tagvalues += TagValueWriter.marshal_file(file)
188*9e94795aSAndroid Build Coastguard Worker      if file.id == sbom_doc.describes and not fragment:
189*9e94795aSAndroid Build Coastguard Worker        # Fragment is not a full SBOM document so the relationship DESCRIBES is not applicable.
190*9e94795aSAndroid Build Coastguard Worker        tagvalues.append(
191*9e94795aSAndroid Build Coastguard Worker            f'{Tags.RELATIONSHIP}: {sbom_doc.id} {sbom_data.RelationshipType.DESCRIBES} {sbom_doc.describes}')
192*9e94795aSAndroid Build Coastguard Worker        tagvalues.append('')
193*9e94795aSAndroid Build Coastguard Worker    return tagvalues
194*9e94795aSAndroid Build Coastguard Worker
195*9e94795aSAndroid Build Coastguard Worker  @staticmethod
196*9e94795aSAndroid Build Coastguard Worker  def marshal_relationship(rel):
197*9e94795aSAndroid Build Coastguard Worker    return f'{Tags.RELATIONSHIP}: {rel.id1} {rel.relationship} {rel.id2}'
198*9e94795aSAndroid Build Coastguard Worker
199*9e94795aSAndroid Build Coastguard Worker  @staticmethod
200*9e94795aSAndroid Build Coastguard Worker  def marshal_relationships(sbom_doc, marshaled_rels):
201*9e94795aSAndroid Build Coastguard Worker    tagvalues = []
202*9e94795aSAndroid Build Coastguard Worker    sorted_rels = sorted(sbom_doc.relationships, key=lambda r: r.id2 + r.id1)
203*9e94795aSAndroid Build Coastguard Worker    for rel in sorted_rels:
204*9e94795aSAndroid Build Coastguard Worker      if any(r.id1 == rel.id1 and r.id2 == rel.id2 and r.relationship == rel.relationship
205*9e94795aSAndroid Build Coastguard Worker             for r in marshaled_rels):
206*9e94795aSAndroid Build Coastguard Worker        continue
207*9e94795aSAndroid Build Coastguard Worker      tagvalues.append(TagValueWriter.marshal_relationship(rel))
208*9e94795aSAndroid Build Coastguard Worker    tagvalues.append('')
209*9e94795aSAndroid Build Coastguard Worker    return tagvalues
210*9e94795aSAndroid Build Coastguard Worker
211*9e94795aSAndroid Build Coastguard Worker  @staticmethod
212*9e94795aSAndroid Build Coastguard Worker  def marshal_license(license):
213*9e94795aSAndroid Build Coastguard Worker    tagvalues = []
214*9e94795aSAndroid Build Coastguard Worker    tagvalues.append(f'{Tags.LICENSE_ID}: {license.id}')
215*9e94795aSAndroid Build Coastguard Worker    tagvalues.append(f'{Tags.LICENSE_NAME}: {license.name}')
216*9e94795aSAndroid Build Coastguard Worker    tagvalues.append(f'{Tags.LICENSE_EXTRACTED_TEXT}: <text>{license.text}</text>')
217*9e94795aSAndroid Build Coastguard Worker    return tagvalues
218*9e94795aSAndroid Build Coastguard Worker
219*9e94795aSAndroid Build Coastguard Worker  @staticmethod
220*9e94795aSAndroid Build Coastguard Worker  def marshal_licenses(sbom_doc):
221*9e94795aSAndroid Build Coastguard Worker    tagvalues = []
222*9e94795aSAndroid Build Coastguard Worker    for license in sbom_doc.licenses:
223*9e94795aSAndroid Build Coastguard Worker      tagvalues += TagValueWriter.marshal_license(license)
224*9e94795aSAndroid Build Coastguard Worker      tagvalues.append('')
225*9e94795aSAndroid Build Coastguard Worker    return tagvalues
226*9e94795aSAndroid Build Coastguard Worker
227*9e94795aSAndroid Build Coastguard Worker  @staticmethod
228*9e94795aSAndroid Build Coastguard Worker  def write(sbom_doc, file, fragment=False):
229*9e94795aSAndroid Build Coastguard Worker    content = []
230*9e94795aSAndroid Build Coastguard Worker    if not fragment:
231*9e94795aSAndroid Build Coastguard Worker      content += TagValueWriter.marshal_doc_headers(sbom_doc)
232*9e94795aSAndroid Build Coastguard Worker    content += TagValueWriter.marshal_files(sbom_doc, fragment)
233*9e94795aSAndroid Build Coastguard Worker    tagvalues, marshaled_relationships = TagValueWriter.marshal_packages(sbom_doc, fragment)
234*9e94795aSAndroid Build Coastguard Worker    content += tagvalues
235*9e94795aSAndroid Build Coastguard Worker    content += TagValueWriter.marshal_relationships(sbom_doc, marshaled_relationships)
236*9e94795aSAndroid Build Coastguard Worker    content += TagValueWriter.marshal_licenses(sbom_doc)
237*9e94795aSAndroid Build Coastguard Worker    file.write('\n'.join(content))
238*9e94795aSAndroid Build Coastguard Worker
239*9e94795aSAndroid Build Coastguard Worker
240*9e94795aSAndroid Build Coastguard Workerclass PropNames:
241*9e94795aSAndroid Build Coastguard Worker  # Common
242*9e94795aSAndroid Build Coastguard Worker  SPDXID = 'SPDXID'
243*9e94795aSAndroid Build Coastguard Worker  SPDX_VERSION = 'spdxVersion'
244*9e94795aSAndroid Build Coastguard Worker  DATA_LICENSE = 'dataLicense'
245*9e94795aSAndroid Build Coastguard Worker  NAME = 'name'
246*9e94795aSAndroid Build Coastguard Worker  DOCUMENT_NAMESPACE = 'documentNamespace'
247*9e94795aSAndroid Build Coastguard Worker  CREATION_INFO = 'creationInfo'
248*9e94795aSAndroid Build Coastguard Worker  CREATORS = 'creators'
249*9e94795aSAndroid Build Coastguard Worker  CREATED = 'created'
250*9e94795aSAndroid Build Coastguard Worker  EXTERNAL_DOCUMENT_REF = 'externalDocumentRefs'
251*9e94795aSAndroid Build Coastguard Worker  DOCUMENT_DESCRIBES = 'documentDescribes'
252*9e94795aSAndroid Build Coastguard Worker  EXTERNAL_DOCUMENT_ID = 'externalDocumentId'
253*9e94795aSAndroid Build Coastguard Worker  EXTERNAL_DOCUMENT_URI = 'spdxDocument'
254*9e94795aSAndroid Build Coastguard Worker  EXTERNAL_DOCUMENT_CHECKSUM = 'checksum'
255*9e94795aSAndroid Build Coastguard Worker  ALGORITHM = 'algorithm'
256*9e94795aSAndroid Build Coastguard Worker  CHECKSUM_VALUE = 'checksumValue'
257*9e94795aSAndroid Build Coastguard Worker
258*9e94795aSAndroid Build Coastguard Worker  # Package
259*9e94795aSAndroid Build Coastguard Worker  PACKAGES = 'packages'
260*9e94795aSAndroid Build Coastguard Worker  PACKAGE_DOWNLOAD_LOCATION = 'downloadLocation'
261*9e94795aSAndroid Build Coastguard Worker  PACKAGE_VERSION = 'versionInfo'
262*9e94795aSAndroid Build Coastguard Worker  PACKAGE_SUPPLIER = 'supplier'
263*9e94795aSAndroid Build Coastguard Worker  FILES_ANALYZED = 'filesAnalyzed'
264*9e94795aSAndroid Build Coastguard Worker  PACKAGE_VERIFICATION_CODE = 'packageVerificationCode'
265*9e94795aSAndroid Build Coastguard Worker  PACKAGE_VERIFICATION_CODE_VALUE = 'packageVerificationCodeValue'
266*9e94795aSAndroid Build Coastguard Worker  PACKAGE_EXTERNAL_REFS = 'externalRefs'
267*9e94795aSAndroid Build Coastguard Worker  PACKAGE_EXTERNAL_REF_CATEGORY = 'referenceCategory'
268*9e94795aSAndroid Build Coastguard Worker  PACKAGE_EXTERNAL_REF_TYPE = 'referenceType'
269*9e94795aSAndroid Build Coastguard Worker  PACKAGE_EXTERNAL_REF_LOCATOR = 'referenceLocator'
270*9e94795aSAndroid Build Coastguard Worker  PACKAGE_HAS_FILES = 'hasFiles'
271*9e94795aSAndroid Build Coastguard Worker  PACKAGE_LICENSE_DECLARED = 'licenseDeclared'
272*9e94795aSAndroid Build Coastguard Worker
273*9e94795aSAndroid Build Coastguard Worker  # File
274*9e94795aSAndroid Build Coastguard Worker  FILES = 'files'
275*9e94795aSAndroid Build Coastguard Worker  FILE_NAME = 'fileName'
276*9e94795aSAndroid Build Coastguard Worker  FILE_CHECKSUMS = 'checksums'
277*9e94795aSAndroid Build Coastguard Worker  FILE_LICENSE_CONCLUDED = 'licenseConcluded'
278*9e94795aSAndroid Build Coastguard Worker
279*9e94795aSAndroid Build Coastguard Worker  # Relationship
280*9e94795aSAndroid Build Coastguard Worker  RELATIONSHIPS = 'relationships'
281*9e94795aSAndroid Build Coastguard Worker  REL_ELEMENT_ID = 'spdxElementId'
282*9e94795aSAndroid Build Coastguard Worker  REL_RELATED_ELEMENT_ID = 'relatedSpdxElement'
283*9e94795aSAndroid Build Coastguard Worker  REL_TYPE = 'relationshipType'
284*9e94795aSAndroid Build Coastguard Worker
285*9e94795aSAndroid Build Coastguard Worker  # License
286*9e94795aSAndroid Build Coastguard Worker  LICENSES = 'hasExtractedLicensingInfos'
287*9e94795aSAndroid Build Coastguard Worker  LICENSE_ID = 'licenseId'
288*9e94795aSAndroid Build Coastguard Worker  LICENSE_NAME = 'name'
289*9e94795aSAndroid Build Coastguard Worker  LICENSE_EXTRACTED_TEXT = 'extractedText'
290*9e94795aSAndroid Build Coastguard Worker
291*9e94795aSAndroid Build Coastguard Worker
292*9e94795aSAndroid Build Coastguard Workerclass JSONWriter:
293*9e94795aSAndroid Build Coastguard Worker  @staticmethod
294*9e94795aSAndroid Build Coastguard Worker  def marshal_doc_headers(sbom_doc):
295*9e94795aSAndroid Build Coastguard Worker    headers = {
296*9e94795aSAndroid Build Coastguard Worker      PropNames.SPDX_VERSION: SPDX_VER,
297*9e94795aSAndroid Build Coastguard Worker      PropNames.DATA_LICENSE: DATA_LIC,
298*9e94795aSAndroid Build Coastguard Worker      PropNames.SPDXID: sbom_doc.id,
299*9e94795aSAndroid Build Coastguard Worker      PropNames.NAME: sbom_doc.name,
300*9e94795aSAndroid Build Coastguard Worker      PropNames.DOCUMENT_NAMESPACE: sbom_doc.namespace,
301*9e94795aSAndroid Build Coastguard Worker      PropNames.CREATION_INFO: {}
302*9e94795aSAndroid Build Coastguard Worker    }
303*9e94795aSAndroid Build Coastguard Worker    creators = [creator for creator in sbom_doc.creators]
304*9e94795aSAndroid Build Coastguard Worker    headers[PropNames.CREATION_INFO][PropNames.CREATORS] = creators
305*9e94795aSAndroid Build Coastguard Worker    headers[PropNames.CREATION_INFO][PropNames.CREATED] = sbom_doc.created
306*9e94795aSAndroid Build Coastguard Worker    external_refs = []
307*9e94795aSAndroid Build Coastguard Worker    for doc_ref in sbom_doc.external_refs:
308*9e94795aSAndroid Build Coastguard Worker      checksum = doc_ref.checksum.split(': ')
309*9e94795aSAndroid Build Coastguard Worker      external_refs.append({
310*9e94795aSAndroid Build Coastguard Worker        PropNames.EXTERNAL_DOCUMENT_ID: f'{doc_ref.id}',
311*9e94795aSAndroid Build Coastguard Worker        PropNames.EXTERNAL_DOCUMENT_URI: doc_ref.uri,
312*9e94795aSAndroid Build Coastguard Worker        PropNames.EXTERNAL_DOCUMENT_CHECKSUM: {
313*9e94795aSAndroid Build Coastguard Worker          PropNames.ALGORITHM: checksum[0],
314*9e94795aSAndroid Build Coastguard Worker          PropNames.CHECKSUM_VALUE: checksum[1]
315*9e94795aSAndroid Build Coastguard Worker        }
316*9e94795aSAndroid Build Coastguard Worker      })
317*9e94795aSAndroid Build Coastguard Worker    if external_refs:
318*9e94795aSAndroid Build Coastguard Worker      headers[PropNames.EXTERNAL_DOCUMENT_REF] = external_refs
319*9e94795aSAndroid Build Coastguard Worker    headers[PropNames.DOCUMENT_DESCRIBES] = [sbom_doc.describes]
320*9e94795aSAndroid Build Coastguard Worker
321*9e94795aSAndroid Build Coastguard Worker    return headers
322*9e94795aSAndroid Build Coastguard Worker
323*9e94795aSAndroid Build Coastguard Worker  @staticmethod
324*9e94795aSAndroid Build Coastguard Worker  def marshal_packages(sbom_doc):
325*9e94795aSAndroid Build Coastguard Worker    packages = []
326*9e94795aSAndroid Build Coastguard Worker    for p in sbom_doc.packages:
327*9e94795aSAndroid Build Coastguard Worker      package = {
328*9e94795aSAndroid Build Coastguard Worker        PropNames.NAME: p.name,
329*9e94795aSAndroid Build Coastguard Worker        PropNames.SPDXID: p.id,
330*9e94795aSAndroid Build Coastguard Worker        PropNames.PACKAGE_DOWNLOAD_LOCATION: p.download_location if p.download_location else sbom_data.VALUE_NOASSERTION,
331*9e94795aSAndroid Build Coastguard Worker        PropNames.FILES_ANALYZED: p.files_analyzed
332*9e94795aSAndroid Build Coastguard Worker      }
333*9e94795aSAndroid Build Coastguard Worker      if p.version:
334*9e94795aSAndroid Build Coastguard Worker        package[PropNames.PACKAGE_VERSION] = p.version
335*9e94795aSAndroid Build Coastguard Worker      if p.supplier:
336*9e94795aSAndroid Build Coastguard Worker        package[PropNames.PACKAGE_SUPPLIER] = p.supplier
337*9e94795aSAndroid Build Coastguard Worker      package[PropNames.PACKAGE_LICENSE_DECLARED] = sbom_data.VALUE_NOASSERTION
338*9e94795aSAndroid Build Coastguard Worker      if p.declared_license_ids:
339*9e94795aSAndroid Build Coastguard Worker        package[PropNames.PACKAGE_LICENSE_DECLARED] = ' OR '.join(p.declared_license_ids)
340*9e94795aSAndroid Build Coastguard Worker      if p.verification_code:
341*9e94795aSAndroid Build Coastguard Worker        package[PropNames.PACKAGE_VERIFICATION_CODE] = {
342*9e94795aSAndroid Build Coastguard Worker          PropNames.PACKAGE_VERIFICATION_CODE_VALUE: p.verification_code
343*9e94795aSAndroid Build Coastguard Worker        }
344*9e94795aSAndroid Build Coastguard Worker      if p.external_refs:
345*9e94795aSAndroid Build Coastguard Worker        package[PropNames.PACKAGE_EXTERNAL_REFS] = []
346*9e94795aSAndroid Build Coastguard Worker        for ref in p.external_refs:
347*9e94795aSAndroid Build Coastguard Worker          ext_ref = {
348*9e94795aSAndroid Build Coastguard Worker            PropNames.PACKAGE_EXTERNAL_REF_CATEGORY: ref.category,
349*9e94795aSAndroid Build Coastguard Worker            PropNames.PACKAGE_EXTERNAL_REF_TYPE: ref.type,
350*9e94795aSAndroid Build Coastguard Worker            PropNames.PACKAGE_EXTERNAL_REF_LOCATOR: ref.locator,
351*9e94795aSAndroid Build Coastguard Worker          }
352*9e94795aSAndroid Build Coastguard Worker          package[PropNames.PACKAGE_EXTERNAL_REFS].append(ext_ref)
353*9e94795aSAndroid Build Coastguard Worker      if p.file_ids:
354*9e94795aSAndroid Build Coastguard Worker        package[PropNames.PACKAGE_HAS_FILES] = []
355*9e94795aSAndroid Build Coastguard Worker        for file_id in p.file_ids:
356*9e94795aSAndroid Build Coastguard Worker          package[PropNames.PACKAGE_HAS_FILES].append(file_id)
357*9e94795aSAndroid Build Coastguard Worker
358*9e94795aSAndroid Build Coastguard Worker      packages.append(package)
359*9e94795aSAndroid Build Coastguard Worker
360*9e94795aSAndroid Build Coastguard Worker    return {PropNames.PACKAGES: packages}
361*9e94795aSAndroid Build Coastguard Worker
362*9e94795aSAndroid Build Coastguard Worker  @staticmethod
363*9e94795aSAndroid Build Coastguard Worker  def marshal_files(sbom_doc):
364*9e94795aSAndroid Build Coastguard Worker    files = []
365*9e94795aSAndroid Build Coastguard Worker    for f in sbom_doc.files:
366*9e94795aSAndroid Build Coastguard Worker      file = {
367*9e94795aSAndroid Build Coastguard Worker        PropNames.FILE_NAME: f.name,
368*9e94795aSAndroid Build Coastguard Worker        PropNames.SPDXID: f.id
369*9e94795aSAndroid Build Coastguard Worker      }
370*9e94795aSAndroid Build Coastguard Worker      checksum = f.checksum.split(': ')
371*9e94795aSAndroid Build Coastguard Worker      file[PropNames.FILE_CHECKSUMS] = [{
372*9e94795aSAndroid Build Coastguard Worker        PropNames.ALGORITHM: checksum[0],
373*9e94795aSAndroid Build Coastguard Worker        PropNames.CHECKSUM_VALUE: checksum[1],
374*9e94795aSAndroid Build Coastguard Worker      }]
375*9e94795aSAndroid Build Coastguard Worker      file[PropNames.FILE_LICENSE_CONCLUDED] = sbom_data.VALUE_NOASSERTION
376*9e94795aSAndroid Build Coastguard Worker      if f.concluded_license_ids:
377*9e94795aSAndroid Build Coastguard Worker        file[PropNames.FILE_LICENSE_CONCLUDED] = ' OR '.join(f.concluded_license_ids)
378*9e94795aSAndroid Build Coastguard Worker      files.append(file)
379*9e94795aSAndroid Build Coastguard Worker    return {PropNames.FILES: files}
380*9e94795aSAndroid Build Coastguard Worker
381*9e94795aSAndroid Build Coastguard Worker  @staticmethod
382*9e94795aSAndroid Build Coastguard Worker  def marshal_relationships(sbom_doc):
383*9e94795aSAndroid Build Coastguard Worker    relationships = []
384*9e94795aSAndroid Build Coastguard Worker    sorted_rels = sorted(sbom_doc.relationships, key=lambda r: r.relationship + r.id2 + r.id1)
385*9e94795aSAndroid Build Coastguard Worker    for r in sorted_rels:
386*9e94795aSAndroid Build Coastguard Worker      rel = {
387*9e94795aSAndroid Build Coastguard Worker        PropNames.REL_ELEMENT_ID: r.id1,
388*9e94795aSAndroid Build Coastguard Worker        PropNames.REL_RELATED_ELEMENT_ID: r.id2,
389*9e94795aSAndroid Build Coastguard Worker        PropNames.REL_TYPE: r.relationship,
390*9e94795aSAndroid Build Coastguard Worker      }
391*9e94795aSAndroid Build Coastguard Worker      relationships.append(rel)
392*9e94795aSAndroid Build Coastguard Worker
393*9e94795aSAndroid Build Coastguard Worker    return {PropNames.RELATIONSHIPS: relationships}
394*9e94795aSAndroid Build Coastguard Worker
395*9e94795aSAndroid Build Coastguard Worker  @staticmethod
396*9e94795aSAndroid Build Coastguard Worker  def marshal_licenses(sbom_doc):
397*9e94795aSAndroid Build Coastguard Worker    licenses = []
398*9e94795aSAndroid Build Coastguard Worker    for l in sbom_doc.licenses:
399*9e94795aSAndroid Build Coastguard Worker      licenses.append({
400*9e94795aSAndroid Build Coastguard Worker          PropNames.LICENSE_ID: l.id,
401*9e94795aSAndroid Build Coastguard Worker          PropNames.LICENSE_NAME: l.name,
402*9e94795aSAndroid Build Coastguard Worker          PropNames.LICENSE_EXTRACTED_TEXT: f'<text>{l.text}</text>'
403*9e94795aSAndroid Build Coastguard Worker      })
404*9e94795aSAndroid Build Coastguard Worker    return {PropNames.LICENSES: licenses}
405*9e94795aSAndroid Build Coastguard Worker
406*9e94795aSAndroid Build Coastguard Worker  @staticmethod
407*9e94795aSAndroid Build Coastguard Worker  def write(sbom_doc, file):
408*9e94795aSAndroid Build Coastguard Worker    doc = {}
409*9e94795aSAndroid Build Coastguard Worker    doc.update(JSONWriter.marshal_doc_headers(sbom_doc))
410*9e94795aSAndroid Build Coastguard Worker    doc.update(JSONWriter.marshal_packages(sbom_doc))
411*9e94795aSAndroid Build Coastguard Worker    doc.update(JSONWriter.marshal_files(sbom_doc))
412*9e94795aSAndroid Build Coastguard Worker    doc.update(JSONWriter.marshal_relationships(sbom_doc))
413*9e94795aSAndroid Build Coastguard Worker    doc.update(JSONWriter.marshal_licenses(sbom_doc))
414*9e94795aSAndroid Build Coastguard Worker    file.write(json.dumps(doc, indent=4))
415