Wei Li | dec97b1 | 2023-04-07 16:45:17 -0700 | [diff] [blame] | 1 | #!/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 | """ |
| 18 | Define data classes that model SBOMs defined by SPDX. The data classes could be |
| 19 | written out to different formats (tagvalue, JSON, etc) of SPDX with corresponding |
| 20 | writer utilities. |
| 21 | |
| 22 | Rrefer to SPDX 2.3 spec: https://spdx.github.io/spdx-spec/v2.3/ and go/android-spdx for details of |
| 23 | fields in each data class. |
| 24 | """ |
| 25 | |
| 26 | from dataclasses import dataclass, field |
| 27 | from typing import List |
Wei Li | d263695 | 2023-05-30 15:03:03 -0700 | [diff] [blame] | 28 | import hashlib |
Wei Li | dec97b1 | 2023-04-07 16:45:17 -0700 | [diff] [blame] | 29 | |
| 30 | SPDXID_DOC = 'SPDXRef-DOCUMENT' |
| 31 | SPDXID_PRODUCT = 'SPDXRef-PRODUCT' |
| 32 | SPDXID_PLATFORM = 'SPDXRef-PLATFORM' |
Wei Li | c6b4046 | 2024-06-17 22:29:28 -0700 | [diff] [blame] | 33 | SPDXID_LICENSE_APACHE = 'LicenseRef-Android-Apache-2.0' |
Wei Li | dec97b1 | 2023-04-07 16:45:17 -0700 | [diff] [blame] | 34 | |
| 35 | PACKAGE_NAME_PRODUCT = 'PRODUCT' |
| 36 | PACKAGE_NAME_PLATFORM = 'PLATFORM' |
| 37 | |
Wei Li | 5290825 | 2023-04-14 18:49:42 -0700 | [diff] [blame] | 38 | VALUE_NOASSERTION = 'NOASSERTION' |
| 39 | VALUE_NONE = 'NONE' |
| 40 | |
Wei Li | dec97b1 | 2023-04-07 16:45:17 -0700 | [diff] [blame] | 41 | |
| 42 | class PackageExternalRefCategory: |
| 43 | SECURITY = 'SECURITY' |
| 44 | PACKAGE_MANAGER = 'PACKAGE-MANAGER' |
| 45 | PERSISTENT_ID = 'PERSISTENT-ID' |
| 46 | OTHER = 'OTHER' |
| 47 | |
| 48 | |
| 49 | class PackageExternalRefType: |
| 50 | cpe22Type = 'cpe22Type' |
| 51 | cpe23Type = 'cpe23Type' |
| 52 | |
| 53 | |
Wei Li | c6b4046 | 2024-06-17 22:29:28 -0700 | [diff] [blame] | 54 | @dataclass(frozen=True) |
Wei Li | dec97b1 | 2023-04-07 16:45:17 -0700 | [diff] [blame] | 55 | class PackageExternalRef: |
| 56 | category: PackageExternalRefCategory |
| 57 | type: PackageExternalRefType |
| 58 | locator: str |
| 59 | |
| 60 | |
| 61 | @dataclass |
| 62 | class 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 Li | c6b4046 | 2024-06-17 22:29:28 -0700 | [diff] [blame] | 72 | declared_license_ids: List[str] = field(default_factory=list) |
Wei Li | dec97b1 | 2023-04-07 16:45:17 -0700 | [diff] [blame] | 73 | |
| 74 | |
| 75 | @dataclass |
| 76 | class File: |
| 77 | id: str |
| 78 | name: str |
| 79 | checksum: str |
Wei Li | c6b4046 | 2024-06-17 22:29:28 -0700 | [diff] [blame] | 80 | concluded_license_ids: List[str] = field(default_factory=list) |
Wei Li | dec97b1 | 2023-04-07 16:45:17 -0700 | [diff] [blame] | 81 | |
| 82 | |
| 83 | class RelationshipType: |
| 84 | DESCRIBES = 'DESCRIBES' |
| 85 | VARIANT_OF = 'VARIANT_OF' |
| 86 | GENERATED_FROM = 'GENERATED_FROM' |
Wei Li | fd7e651 | 2023-05-05 10:49:28 -0700 | [diff] [blame] | 87 | CONTAINS = 'CONTAINS' |
Wei Li | d263695 | 2023-05-30 15:03:03 -0700 | [diff] [blame] | 88 | STATIC_LINK = 'STATIC_LINK' |
Wei Li | dec97b1 | 2023-04-07 16:45:17 -0700 | [diff] [blame] | 89 | |
| 90 | |
Wei Li | c6b4046 | 2024-06-17 22:29:28 -0700 | [diff] [blame] | 91 | @dataclass(frozen=True) |
Wei Li | dec97b1 | 2023-04-07 16:45:17 -0700 | [diff] [blame] | 92 | class Relationship: |
| 93 | id1: str |
| 94 | relationship: RelationshipType |
| 95 | id2: str |
| 96 | |
| 97 | |
Wei Li | c6b4046 | 2024-06-17 22:29:28 -0700 | [diff] [blame] | 98 | @dataclass(frozen=True) |
Wei Li | dec97b1 | 2023-04-07 16:45:17 -0700 | [diff] [blame] | 99 | class DocumentExternalReference: |
| 100 | id: str |
| 101 | uri: str |
| 102 | checksum: str |
| 103 | |
| 104 | |
Wei Li | c6b4046 | 2024-06-17 22:29:28 -0700 | [diff] [blame] | 105 | @dataclass(frozen=True) |
| 106 | class License: |
| 107 | id: str |
| 108 | text: str |
| 109 | name: str |
| 110 | |
| 111 | |
Wei Li | dec97b1 | 2023-04-07 16:45:17 -0700 | [diff] [blame] | 112 | @dataclass |
| 113 | class 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 Li | c6b4046 | 2024-06-17 22:29:28 -0700 | [diff] [blame] | 124 | licenses: List[License] = field(default_factory=list) |
Wei Li | dec97b1 | 2023-04-07 16:45:17 -0700 | [diff] [blame] | 125 | |
| 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 Li | c6b4046 | 2024-06-17 22:29:28 -0700 | [diff] [blame] | 131 | p = next((p for p in self.packages if package.id == p.id), None) |
| 132 | if not p: |
Wei Li | dec97b1 | 2023-04-07 16:45:17 -0700 | [diff] [blame] | 133 | self.packages.append(package) |
Wei Li | c6b4046 | 2024-06-17 22:29:28 -0700 | [diff] [blame] | 134 | 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 Li | dec97b1 | 2023-04-07 16:45:17 -0700 | [diff] [blame] | 138 | |
| 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 Li | d263695 | 2023-05-30 15:03:03 -0700 | [diff] [blame] | 143 | |
Wei Li | c6b4046 | 2024-06-17 22:29:28 -0700 | [diff] [blame] | 144 | 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 Li | d263695 | 2023-05-30 15:03:03 -0700 | [diff] [blame] | 148 | 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 Li | f99db99 | 2023-07-31 14:12:52 -0700 | [diff] [blame] | 156 | checksums.append(file.checksum.split(': ')[1]) |
Wei Li | d263695 | 2023-05-30 15:03:03 -0700 | [diff] [blame] | 157 | checksums.sort() |
| 158 | h = hashlib.sha1() |
| 159 | h.update(''.join(checksums).encode(encoding='utf-8')) |
| 160 | package.verification_code = h.hexdigest() |
Wei Li | c134b76 | 2023-10-17 23:52:30 -0700 | [diff] [blame] | 161 | |
| 162 | def 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('-') |