blob: 1cb864db758dc62c4039dbb599e2afde225663d4 [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
Wei Lid2636952023-05-30 15:03:03 -070088 def marshal_package(sbom_doc, package, fragment):
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('')
Wei Lid2636952023-05-30 15:03:03 -0700110
111 if package.id == sbom_doc.describes and not fragment:
112 tagvalues.append(
113 f'{Tags.RELATIONSHIP}: {sbom_doc.id} {sbom_data.RelationshipType.DESCRIBES} {sbom_doc.describes}')
114 tagvalues.append('')
115
116 for file in sbom_doc.files:
117 if file.id in package.file_ids:
118 tagvalues += TagValueWriter.marshal_file(file)
119
Wei Lidec97b12023-04-07 16:45:17 -0700120 return tagvalues
121
122 @staticmethod
Wei Lid2636952023-05-30 15:03:03 -0700123 def marshal_packages(sbom_doc, fragment):
Wei Lidec97b12023-04-07 16:45:17 -0700124 tagvalues = []
125 marshaled_relationships = []
126 i = 0
127 packages = sbom_doc.packages
128 while i < len(packages):
Wei Lid2636952023-05-30 15:03:03 -0700129 if (i + 1 < len(packages)
130 and packages[i].id.startswith('SPDXRef-SOURCE-')
131 and packages[i + 1].id.startswith('SPDXRef-UPSTREAM-')):
132 # Output SOURCE, UPSTREAM packages and their VARIANT_OF relationship together, so they are close to each other
133 # in SBOMs in tagvalue format.
134 tagvalues += TagValueWriter.marshal_package(sbom_doc, packages[i], fragment)
135 tagvalues += TagValueWriter.marshal_package(sbom_doc, packages[i + 1], fragment)
Wei Lidec97b12023-04-07 16:45:17 -0700136 rel = next((r for r in sbom_doc.relationships if
137 r.id1 == packages[i].id and
138 r.id2 == packages[i + 1].id and
139 r.relationship == sbom_data.RelationshipType.VARIANT_OF), None)
140 if rel:
141 marshaled_relationships.append(rel)
142 tagvalues.append(TagValueWriter.marshal_relationship(rel))
143 tagvalues.append('')
144
145 i += 2
146 else:
Wei Lid2636952023-05-30 15:03:03 -0700147 tagvalues += TagValueWriter.marshal_package(sbom_doc, packages[i], fragment)
Wei Lidec97b12023-04-07 16:45:17 -0700148 i += 1
149
150 return tagvalues, marshaled_relationships
151
152 @staticmethod
153 def marshal_file(file):
154 tagvalues = [
155 f'{Tags.FILE_NAME}: {file.name}',
156 f'{Tags.SPDXID}: {file.id}',
157 f'{Tags.FILE_CHECKSUM}: {file.checksum}',
158 '',
159 ]
160
161 return tagvalues
162
163 @staticmethod
Wei Lid2636952023-05-30 15:03:03 -0700164 def marshal_files(sbom_doc, fragment):
Wei Lidec97b12023-04-07 16:45:17 -0700165 tagvalues = []
Wei Lid2636952023-05-30 15:03:03 -0700166 files_in_packages = []
167 for package in sbom_doc.packages:
168 files_in_packages += package.file_ids
Wei Lidec97b12023-04-07 16:45:17 -0700169 for file in sbom_doc.files:
Wei Lid2636952023-05-30 15:03:03 -0700170 if file.id in files_in_packages:
Wei Lifd7e6512023-05-05 10:49:28 -0700171 continue
Wei Lidec97b12023-04-07 16:45:17 -0700172 tagvalues += TagValueWriter.marshal_file(file)
Wei Lid2636952023-05-30 15:03:03 -0700173 if file.id == sbom_doc.describes and not fragment:
174 # Fragment is not a full SBOM document so the relationship DESCRIBES is not applicable.
175 tagvalues.append(
176 f'{Tags.RELATIONSHIP}: {sbom_doc.id} {sbom_data.RelationshipType.DESCRIBES} {sbom_doc.describes}')
177 tagvalues.append('')
Wei Lidec97b12023-04-07 16:45:17 -0700178 return tagvalues
179
180 @staticmethod
181 def marshal_relationship(rel):
182 return f'{Tags.RELATIONSHIP}: {rel.id1} {rel.relationship} {rel.id2}'
183
184 @staticmethod
185 def marshal_relationships(sbom_doc, marshaled_rels):
186 tagvalues = []
187 sorted_rels = sorted(sbom_doc.relationships, key=lambda r: r.id2 + r.id1)
188 for rel in sorted_rels:
189 if any(r.id1 == rel.id1 and r.id2 == rel.id2 and r.relationship == rel.relationship
190 for r in marshaled_rels):
191 continue
192 tagvalues.append(TagValueWriter.marshal_relationship(rel))
193 tagvalues.append('')
194 return tagvalues
195
196 @staticmethod
197 def write(sbom_doc, file, fragment=False):
198 content = []
199 if not fragment:
200 content += TagValueWriter.marshal_doc_headers(sbom_doc)
Wei Lid2636952023-05-30 15:03:03 -0700201 content += TagValueWriter.marshal_files(sbom_doc, fragment)
202 tagvalues, marshaled_relationships = TagValueWriter.marshal_packages(sbom_doc, fragment)
Wei Lidec97b12023-04-07 16:45:17 -0700203 content += tagvalues
204 content += TagValueWriter.marshal_relationships(sbom_doc, marshaled_relationships)
205 file.write('\n'.join(content))
206
207
208class PropNames:
209 # Common
210 SPDXID = 'SPDXID'
211 SPDX_VERSION = 'spdxVersion'
212 DATA_LICENSE = 'dataLicense'
213 NAME = 'name'
214 DOCUMENT_NAMESPACE = 'documentNamespace'
215 CREATION_INFO = 'creationInfo'
216 CREATORS = 'creators'
217 CREATED = 'created'
218 EXTERNAL_DOCUMENT_REF = 'externalDocumentRefs'
219 DOCUMENT_DESCRIBES = 'documentDescribes'
220 EXTERNAL_DOCUMENT_ID = 'externalDocumentId'
221 EXTERNAL_DOCUMENT_URI = 'spdxDocument'
222 EXTERNAL_DOCUMENT_CHECKSUM = 'checksum'
223 ALGORITHM = 'algorithm'
224 CHECKSUM_VALUE = 'checksumValue'
225
226 # Package
227 PACKAGES = 'packages'
228 PACKAGE_DOWNLOAD_LOCATION = 'downloadLocation'
229 PACKAGE_VERSION = 'versionInfo'
230 PACKAGE_SUPPLIER = 'supplier'
231 FILES_ANALYZED = 'filesAnalyzed'
232 PACKAGE_VERIFICATION_CODE = 'packageVerificationCode'
233 PACKAGE_VERIFICATION_CODE_VALUE = 'packageVerificationCodeValue'
234 PACKAGE_EXTERNAL_REFS = 'externalRefs'
235 PACKAGE_EXTERNAL_REF_CATEGORY = 'referenceCategory'
236 PACKAGE_EXTERNAL_REF_TYPE = 'referenceType'
237 PACKAGE_EXTERNAL_REF_LOCATOR = 'referenceLocator'
238 PACKAGE_HAS_FILES = 'hasFiles'
239
240 # File
241 FILES = 'files'
242 FILE_NAME = 'fileName'
243 FILE_CHECKSUMS = 'checksums'
244
245 # Relationship
246 RELATIONSHIPS = 'relationships'
247 REL_ELEMENT_ID = 'spdxElementId'
248 REL_RELATED_ELEMENT_ID = 'relatedSpdxElement'
249 REL_TYPE = 'relationshipType'
250
251
252class JSONWriter:
253 @staticmethod
254 def marshal_doc_headers(sbom_doc):
255 headers = {
256 PropNames.SPDX_VERSION: SPDX_VER,
257 PropNames.DATA_LICENSE: DATA_LIC,
258 PropNames.SPDXID: sbom_doc.id,
259 PropNames.NAME: sbom_doc.name,
260 PropNames.DOCUMENT_NAMESPACE: sbom_doc.namespace,
261 PropNames.CREATION_INFO: {}
262 }
263 creators = [creator for creator in sbom_doc.creators]
264 headers[PropNames.CREATION_INFO][PropNames.CREATORS] = creators
265 headers[PropNames.CREATION_INFO][PropNames.CREATED] = sbom_doc.created
266 external_refs = []
267 for doc_ref in sbom_doc.external_refs:
268 checksum = doc_ref.checksum.split(': ')
269 external_refs.append({
270 PropNames.EXTERNAL_DOCUMENT_ID: f'{doc_ref.id}',
271 PropNames.EXTERNAL_DOCUMENT_URI: doc_ref.uri,
272 PropNames.EXTERNAL_DOCUMENT_CHECKSUM: {
273 PropNames.ALGORITHM: checksum[0],
274 PropNames.CHECKSUM_VALUE: checksum[1]
275 }
276 })
277 if external_refs:
278 headers[PropNames.EXTERNAL_DOCUMENT_REF] = external_refs
279 headers[PropNames.DOCUMENT_DESCRIBES] = [sbom_doc.describes]
280
281 return headers
282
283 @staticmethod
284 def marshal_packages(sbom_doc):
285 packages = []
286 for p in sbom_doc.packages:
287 package = {
288 PropNames.NAME: p.name,
289 PropNames.SPDXID: p.id,
Wei Li52908252023-04-14 18:49:42 -0700290 PropNames.PACKAGE_DOWNLOAD_LOCATION: p.download_location if p.download_location else sbom_data.VALUE_NOASSERTION,
Wei Lidec97b12023-04-07 16:45:17 -0700291 PropNames.FILES_ANALYZED: p.files_analyzed
292 }
293 if p.version:
294 package[PropNames.PACKAGE_VERSION] = p.version
295 if p.supplier:
296 package[PropNames.PACKAGE_SUPPLIER] = p.supplier
297 if p.verification_code:
298 package[PropNames.PACKAGE_VERIFICATION_CODE] = {
299 PropNames.PACKAGE_VERIFICATION_CODE_VALUE: p.verification_code
300 }
301 if p.external_refs:
302 package[PropNames.PACKAGE_EXTERNAL_REFS] = []
303 for ref in p.external_refs:
304 ext_ref = {
305 PropNames.PACKAGE_EXTERNAL_REF_CATEGORY: ref.category,
306 PropNames.PACKAGE_EXTERNAL_REF_TYPE: ref.type,
307 PropNames.PACKAGE_EXTERNAL_REF_LOCATOR: ref.locator,
308 }
309 package[PropNames.PACKAGE_EXTERNAL_REFS].append(ext_ref)
310 if p.file_ids:
311 package[PropNames.PACKAGE_HAS_FILES] = []
312 for file_id in p.file_ids:
313 package[PropNames.PACKAGE_HAS_FILES].append(file_id)
314
315 packages.append(package)
316
317 return {PropNames.PACKAGES: packages}
318
319 @staticmethod
320 def marshal_files(sbom_doc):
321 files = []
322 for f in sbom_doc.files:
323 file = {
324 PropNames.FILE_NAME: f.name,
325 PropNames.SPDXID: f.id
326 }
327 checksum = f.checksum.split(': ')
328 file[PropNames.FILE_CHECKSUMS] = [{
329 PropNames.ALGORITHM: checksum[0],
330 PropNames.CHECKSUM_VALUE: checksum[1],
331 }]
332 files.append(file)
333 return {PropNames.FILES: files}
334
335 @staticmethod
336 def marshal_relationships(sbom_doc):
337 relationships = []
338 sorted_rels = sorted(sbom_doc.relationships, key=lambda r: r.relationship + r.id2 + r.id1)
339 for r in sorted_rels:
340 rel = {
341 PropNames.REL_ELEMENT_ID: r.id1,
342 PropNames.REL_RELATED_ELEMENT_ID: r.id2,
343 PropNames.REL_TYPE: r.relationship,
344 }
345 relationships.append(rel)
346
347 return {PropNames.RELATIONSHIPS: relationships}
348
349 @staticmethod
350 def write(sbom_doc, file):
351 doc = {}
352 doc.update(JSONWriter.marshal_doc_headers(sbom_doc))
353 doc.update(JSONWriter.marshal_packages(sbom_doc))
354 doc.update(JSONWriter.marshal_files(sbom_doc))
355 doc.update(JSONWriter.marshal_relationships(sbom_doc))
356 file.write(json.dumps(doc, indent=4))