blob: 85dee9d1a511a506991b7963983febbb737195ca [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):
Wei Li52908252023-04-14 18:49:42 -070089 download_location = sbom_data.VALUE_NOASSERTION
Wei Lidec97b12023-04-07 16:45:17 -070090 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
Wei Lifd7e6512023-05-05 10:49:28 -0700113 def marshal_described_element(sbom_doc, fragment):
Wei Lidec97b12023-04-07 16:45:17 -0700114 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])
Wei Lifd7e6512023-05-05 10:49:28 -0700120 if not fragment:
121 tagvalues.append(
122 f'{Tags.RELATIONSHIP}: {sbom_doc.id} {sbom_data.RelationshipType.DESCRIBES} {sbom_doc.describes}')
Wei Lidec97b12023-04-07 16:45:17 -0700123
124 tagvalues.append('')
125 return tagvalues
126
127 file = [f for f in sbom_doc.files if f.id == sbom_doc.describes]
128 if file:
Wei Lifd7e6512023-05-05 10:49:28 -0700129 tagvalues = TagValueWriter.marshal_file(file[0])
130 if not fragment:
131 tagvalues.append(
132 f'{Tags.RELATIONSHIP}: {sbom_doc.id} {sbom_data.RelationshipType.DESCRIBES} {sbom_doc.describes}')
Wei Lidec97b12023-04-07 16:45:17 -0700133
134 return tagvalues
135
136 return None
137
138 @staticmethod
139 def marshal_packages(sbom_doc):
140 tagvalues = []
141 marshaled_relationships = []
142 i = 0
143 packages = sbom_doc.packages
144 while i < len(packages):
145 if packages[i].id == sbom_doc.describes:
146 i += 1
147 continue
148
149 if i + 1 < len(packages) \
150 and packages[i].id.startswith('SPDXRef-SOURCE-') \
151 and packages[i + 1].id.startswith('SPDXRef-UPSTREAM-'):
152 tagvalues += TagValueWriter.marshal_package(packages[i])
153 tagvalues += TagValueWriter.marshal_package(packages[i + 1])
154 rel = next((r for r in sbom_doc.relationships if
155 r.id1 == packages[i].id and
156 r.id2 == packages[i + 1].id and
157 r.relationship == sbom_data.RelationshipType.VARIANT_OF), None)
158 if rel:
159 marshaled_relationships.append(rel)
160 tagvalues.append(TagValueWriter.marshal_relationship(rel))
161 tagvalues.append('')
162
163 i += 2
164 else:
165 tagvalues += TagValueWriter.marshal_package(packages[i])
166 i += 1
167
168 return tagvalues, marshaled_relationships
169
170 @staticmethod
171 def marshal_file(file):
172 tagvalues = [
173 f'{Tags.FILE_NAME}: {file.name}',
174 f'{Tags.SPDXID}: {file.id}',
175 f'{Tags.FILE_CHECKSUM}: {file.checksum}',
176 '',
177 ]
178
179 return tagvalues
180
181 @staticmethod
182 def marshal_files(sbom_doc):
183 tagvalues = []
184 for file in sbom_doc.files:
Wei Lifd7e6512023-05-05 10:49:28 -0700185 if file.id == sbom_doc.describes:
186 continue
Wei Lidec97b12023-04-07 16:45:17 -0700187 tagvalues += TagValueWriter.marshal_file(file)
188 return tagvalues
189
190 @staticmethod
191 def marshal_relationship(rel):
192 return f'{Tags.RELATIONSHIP}: {rel.id1} {rel.relationship} {rel.id2}'
193
194 @staticmethod
195 def marshal_relationships(sbom_doc, marshaled_rels):
196 tagvalues = []
197 sorted_rels = sorted(sbom_doc.relationships, key=lambda r: r.id2 + r.id1)
198 for rel in sorted_rels:
199 if any(r.id1 == rel.id1 and r.id2 == rel.id2 and r.relationship == rel.relationship
200 for r in marshaled_rels):
201 continue
202 tagvalues.append(TagValueWriter.marshal_relationship(rel))
203 tagvalues.append('')
204 return tagvalues
205
206 @staticmethod
207 def write(sbom_doc, file, fragment=False):
208 content = []
209 if not fragment:
210 content += TagValueWriter.marshal_doc_headers(sbom_doc)
Wei Lifd7e6512023-05-05 10:49:28 -0700211 described_element = TagValueWriter.marshal_described_element(sbom_doc, fragment)
212 if described_element:
213 content += described_element
Wei Lidec97b12023-04-07 16:45:17 -0700214 content += TagValueWriter.marshal_files(sbom_doc)
215 tagvalues, marshaled_relationships = TagValueWriter.marshal_packages(sbom_doc)
216 content += tagvalues
217 content += TagValueWriter.marshal_relationships(sbom_doc, marshaled_relationships)
218 file.write('\n'.join(content))
219
220
221class PropNames:
222 # Common
223 SPDXID = 'SPDXID'
224 SPDX_VERSION = 'spdxVersion'
225 DATA_LICENSE = 'dataLicense'
226 NAME = 'name'
227 DOCUMENT_NAMESPACE = 'documentNamespace'
228 CREATION_INFO = 'creationInfo'
229 CREATORS = 'creators'
230 CREATED = 'created'
231 EXTERNAL_DOCUMENT_REF = 'externalDocumentRefs'
232 DOCUMENT_DESCRIBES = 'documentDescribes'
233 EXTERNAL_DOCUMENT_ID = 'externalDocumentId'
234 EXTERNAL_DOCUMENT_URI = 'spdxDocument'
235 EXTERNAL_DOCUMENT_CHECKSUM = 'checksum'
236 ALGORITHM = 'algorithm'
237 CHECKSUM_VALUE = 'checksumValue'
238
239 # Package
240 PACKAGES = 'packages'
241 PACKAGE_DOWNLOAD_LOCATION = 'downloadLocation'
242 PACKAGE_VERSION = 'versionInfo'
243 PACKAGE_SUPPLIER = 'supplier'
244 FILES_ANALYZED = 'filesAnalyzed'
245 PACKAGE_VERIFICATION_CODE = 'packageVerificationCode'
246 PACKAGE_VERIFICATION_CODE_VALUE = 'packageVerificationCodeValue'
247 PACKAGE_EXTERNAL_REFS = 'externalRefs'
248 PACKAGE_EXTERNAL_REF_CATEGORY = 'referenceCategory'
249 PACKAGE_EXTERNAL_REF_TYPE = 'referenceType'
250 PACKAGE_EXTERNAL_REF_LOCATOR = 'referenceLocator'
251 PACKAGE_HAS_FILES = 'hasFiles'
252
253 # File
254 FILES = 'files'
255 FILE_NAME = 'fileName'
256 FILE_CHECKSUMS = 'checksums'
257
258 # Relationship
259 RELATIONSHIPS = 'relationships'
260 REL_ELEMENT_ID = 'spdxElementId'
261 REL_RELATED_ELEMENT_ID = 'relatedSpdxElement'
262 REL_TYPE = 'relationshipType'
263
264
265class JSONWriter:
266 @staticmethod
267 def marshal_doc_headers(sbom_doc):
268 headers = {
269 PropNames.SPDX_VERSION: SPDX_VER,
270 PropNames.DATA_LICENSE: DATA_LIC,
271 PropNames.SPDXID: sbom_doc.id,
272 PropNames.NAME: sbom_doc.name,
273 PropNames.DOCUMENT_NAMESPACE: sbom_doc.namespace,
274 PropNames.CREATION_INFO: {}
275 }
276 creators = [creator for creator in sbom_doc.creators]
277 headers[PropNames.CREATION_INFO][PropNames.CREATORS] = creators
278 headers[PropNames.CREATION_INFO][PropNames.CREATED] = sbom_doc.created
279 external_refs = []
280 for doc_ref in sbom_doc.external_refs:
281 checksum = doc_ref.checksum.split(': ')
282 external_refs.append({
283 PropNames.EXTERNAL_DOCUMENT_ID: f'{doc_ref.id}',
284 PropNames.EXTERNAL_DOCUMENT_URI: doc_ref.uri,
285 PropNames.EXTERNAL_DOCUMENT_CHECKSUM: {
286 PropNames.ALGORITHM: checksum[0],
287 PropNames.CHECKSUM_VALUE: checksum[1]
288 }
289 })
290 if external_refs:
291 headers[PropNames.EXTERNAL_DOCUMENT_REF] = external_refs
292 headers[PropNames.DOCUMENT_DESCRIBES] = [sbom_doc.describes]
293
294 return headers
295
296 @staticmethod
297 def marshal_packages(sbom_doc):
298 packages = []
299 for p in sbom_doc.packages:
300 package = {
301 PropNames.NAME: p.name,
302 PropNames.SPDXID: p.id,
Wei Li52908252023-04-14 18:49:42 -0700303 PropNames.PACKAGE_DOWNLOAD_LOCATION: p.download_location if p.download_location else sbom_data.VALUE_NOASSERTION,
Wei Lidec97b12023-04-07 16:45:17 -0700304 PropNames.FILES_ANALYZED: p.files_analyzed
305 }
306 if p.version:
307 package[PropNames.PACKAGE_VERSION] = p.version
308 if p.supplier:
309 package[PropNames.PACKAGE_SUPPLIER] = p.supplier
310 if p.verification_code:
311 package[PropNames.PACKAGE_VERIFICATION_CODE] = {
312 PropNames.PACKAGE_VERIFICATION_CODE_VALUE: p.verification_code
313 }
314 if p.external_refs:
315 package[PropNames.PACKAGE_EXTERNAL_REFS] = []
316 for ref in p.external_refs:
317 ext_ref = {
318 PropNames.PACKAGE_EXTERNAL_REF_CATEGORY: ref.category,
319 PropNames.PACKAGE_EXTERNAL_REF_TYPE: ref.type,
320 PropNames.PACKAGE_EXTERNAL_REF_LOCATOR: ref.locator,
321 }
322 package[PropNames.PACKAGE_EXTERNAL_REFS].append(ext_ref)
323 if p.file_ids:
324 package[PropNames.PACKAGE_HAS_FILES] = []
325 for file_id in p.file_ids:
326 package[PropNames.PACKAGE_HAS_FILES].append(file_id)
327
328 packages.append(package)
329
330 return {PropNames.PACKAGES: packages}
331
332 @staticmethod
333 def marshal_files(sbom_doc):
334 files = []
335 for f in sbom_doc.files:
336 file = {
337 PropNames.FILE_NAME: f.name,
338 PropNames.SPDXID: f.id
339 }
340 checksum = f.checksum.split(': ')
341 file[PropNames.FILE_CHECKSUMS] = [{
342 PropNames.ALGORITHM: checksum[0],
343 PropNames.CHECKSUM_VALUE: checksum[1],
344 }]
345 files.append(file)
346 return {PropNames.FILES: files}
347
348 @staticmethod
349 def marshal_relationships(sbom_doc):
350 relationships = []
351 sorted_rels = sorted(sbom_doc.relationships, key=lambda r: r.relationship + r.id2 + r.id1)
352 for r in sorted_rels:
353 rel = {
354 PropNames.REL_ELEMENT_ID: r.id1,
355 PropNames.REL_RELATED_ELEMENT_ID: r.id2,
356 PropNames.REL_TYPE: r.relationship,
357 }
358 relationships.append(rel)
359
360 return {PropNames.RELATIONSHIPS: relationships}
361
362 @staticmethod
363 def write(sbom_doc, file):
364 doc = {}
365 doc.update(JSONWriter.marshal_doc_headers(sbom_doc))
366 doc.update(JSONWriter.marshal_packages(sbom_doc))
367 doc.update(JSONWriter.marshal_files(sbom_doc))
368 doc.update(JSONWriter.marshal_relationships(sbom_doc))
369 file.write(json.dumps(doc, indent=4))