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