blob: fc5c704cf1660da6e5c5bae6a85e7fb5f9d6e8bc [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"""
18Define data classes that model SBOMs defined by SPDX. The data classes could be
19written out to different formats (tagvalue, JSON, etc) of SPDX with corresponding
20writer utilities.
21
22Rrefer to SPDX 2.3 spec: https://spdx.github.io/spdx-spec/v2.3/ and go/android-spdx for details of
23fields in each data class.
24"""
25
26from dataclasses import dataclass, field
27from typing import List
Wei Lid2636952023-05-30 15:03:03 -070028import hashlib
Wei Lidec97b12023-04-07 16:45:17 -070029
30SPDXID_DOC = 'SPDXRef-DOCUMENT'
31SPDXID_PRODUCT = 'SPDXRef-PRODUCT'
32SPDXID_PLATFORM = 'SPDXRef-PLATFORM'
Wei Lic6b40462024-06-17 22:29:28 -070033SPDXID_LICENSE_APACHE = 'LicenseRef-Android-Apache-2.0'
Wei Lidec97b12023-04-07 16:45:17 -070034
35PACKAGE_NAME_PRODUCT = 'PRODUCT'
36PACKAGE_NAME_PLATFORM = 'PLATFORM'
37
Wei Li52908252023-04-14 18:49:42 -070038VALUE_NOASSERTION = 'NOASSERTION'
39VALUE_NONE = 'NONE'
40
Wei Lidec97b12023-04-07 16:45:17 -070041
42class PackageExternalRefCategory:
43 SECURITY = 'SECURITY'
44 PACKAGE_MANAGER = 'PACKAGE-MANAGER'
45 PERSISTENT_ID = 'PERSISTENT-ID'
46 OTHER = 'OTHER'
47
48
49class PackageExternalRefType:
50 cpe22Type = 'cpe22Type'
51 cpe23Type = 'cpe23Type'
52
53
Wei Lic6b40462024-06-17 22:29:28 -070054@dataclass(frozen=True)
Wei Lidec97b12023-04-07 16:45:17 -070055class PackageExternalRef:
56 category: PackageExternalRefCategory
57 type: PackageExternalRefType
58 locator: str
59
60
61@dataclass
62class Package:
63 name: str
64 id: str
65 version: str = None
66 supplier: str = None
67 download_location: str = None
68 files_analyzed: bool = False
69 verification_code: str = None
70 file_ids: List[str] = field(default_factory=list)
71 external_refs: List[PackageExternalRef] = field(default_factory=list)
Wei Lic6b40462024-06-17 22:29:28 -070072 declared_license_ids: List[str] = field(default_factory=list)
Wei Lidec97b12023-04-07 16:45:17 -070073
74
75@dataclass
76class File:
77 id: str
78 name: str
79 checksum: str
Wei Lic6b40462024-06-17 22:29:28 -070080 concluded_license_ids: List[str] = field(default_factory=list)
Wei Lidec97b12023-04-07 16:45:17 -070081
82
83class RelationshipType:
84 DESCRIBES = 'DESCRIBES'
85 VARIANT_OF = 'VARIANT_OF'
86 GENERATED_FROM = 'GENERATED_FROM'
Wei Lifd7e6512023-05-05 10:49:28 -070087 CONTAINS = 'CONTAINS'
Wei Lid2636952023-05-30 15:03:03 -070088 STATIC_LINK = 'STATIC_LINK'
Wei Lidec97b12023-04-07 16:45:17 -070089
90
Wei Lic6b40462024-06-17 22:29:28 -070091@dataclass(frozen=True)
Wei Lidec97b12023-04-07 16:45:17 -070092class Relationship:
93 id1: str
94 relationship: RelationshipType
95 id2: str
96
97
Wei Lic6b40462024-06-17 22:29:28 -070098@dataclass(frozen=True)
Wei Lidec97b12023-04-07 16:45:17 -070099class DocumentExternalReference:
100 id: str
101 uri: str
102 checksum: str
103
104
Wei Lic6b40462024-06-17 22:29:28 -0700105@dataclass(frozen=True)
106class License:
107 id: str
108 text: str
109 name: str
110
111
Wei Lidec97b12023-04-07 16:45:17 -0700112@dataclass
113class Document:
114 name: str
115 namespace: str
116 id: str = SPDXID_DOC
117 describes: str = SPDXID_PRODUCT
118 creators: List[str] = field(default_factory=list)
119 created: str = None
120 external_refs: List[DocumentExternalReference] = field(default_factory=list)
121 packages: List[Package] = field(default_factory=list)
122 files: List[File] = field(default_factory=list)
123 relationships: List[Relationship] = field(default_factory=list)
Wei Lic6b40462024-06-17 22:29:28 -0700124 licenses: List[License] = field(default_factory=list)
Wei Lidec97b12023-04-07 16:45:17 -0700125
126 def add_external_ref(self, external_ref):
127 if not any(external_ref.uri == ref.uri for ref in self.external_refs):
128 self.external_refs.append(external_ref)
129
130 def add_package(self, package):
Wei Lic6b40462024-06-17 22:29:28 -0700131 p = next((p for p in self.packages if package.id == p.id), None)
132 if not p:
Wei Lidec97b12023-04-07 16:45:17 -0700133 self.packages.append(package)
Wei Lic6b40462024-06-17 22:29:28 -0700134 else:
135 for license_id in package.declared_license_ids:
136 if license_id not in p.declared_license_ids:
137 p.declared_license_ids.append(license_id)
Wei Lidec97b12023-04-07 16:45:17 -0700138
139 def add_relationship(self, rel):
140 if not any(rel.id1 == r.id1 and rel.id2 == r.id2 and rel.relationship == r.relationship
141 for r in self.relationships):
142 self.relationships.append(rel)
Wei Lid2636952023-05-30 15:03:03 -0700143
Wei Lic6b40462024-06-17 22:29:28 -0700144 def add_license(self, license):
145 if not any(license.id == l.id for l in self.licenses):
146 self.licenses.append(license)
147
Wei Lid2636952023-05-30 15:03:03 -0700148 def generate_packages_verification_code(self):
149 for package in self.packages:
150 if not package.file_ids:
151 continue
152
153 checksums = []
154 for file in self.files:
155 if file.id in package.file_ids:
Wei Lif99db992023-07-31 14:12:52 -0700156 checksums.append(file.checksum.split(': ')[1])
Wei Lid2636952023-05-30 15:03:03 -0700157 checksums.sort()
158 h = hashlib.sha1()
159 h.update(''.join(checksums).encode(encoding='utf-8'))
160 package.verification_code = h.hexdigest()
Wei Lic134b762023-10-17 23:52:30 -0700161
162def encode_for_spdxid(s):
163 """Simple encode for string values used in SPDXID which uses the charset of A-Za-Z0-9.-"""
164 result = ''
165 for c in s:
166 if c.isalnum() or c in '.-':
167 result += c
168 elif c in '_@/':
169 result += '-'
170 else:
171 result += '0x' + c.encode('utf-8').hex()
172
173 return result.lstrip('-')