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""" 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 28import hashlib 29 30SPDXID_DOC = 'SPDXRef-DOCUMENT' 31SPDXID_PRODUCT = 'SPDXRef-PRODUCT' 32SPDXID_PLATFORM = 'SPDXRef-PLATFORM' 33SPDXID_LICENSE_APACHE = 'LicenseRef-Android-Apache-2.0' 34 35PACKAGE_NAME_PRODUCT = 'PRODUCT' 36PACKAGE_NAME_PLATFORM = 'PLATFORM' 37 38VALUE_NOASSERTION = 'NOASSERTION' 39VALUE_NONE = 'NONE' 40 41 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 54@dataclass(frozen=True) 55class 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) 72 declared_license_ids: List[str] = field(default_factory=list) 73 74 75@dataclass 76class File: 77 id: str 78 name: str 79 checksum: str 80 concluded_license_ids: List[str] = field(default_factory=list) 81 82 83class RelationshipType: 84 DESCRIBES = 'DESCRIBES' 85 VARIANT_OF = 'VARIANT_OF' 86 GENERATED_FROM = 'GENERATED_FROM' 87 CONTAINS = 'CONTAINS' 88 STATIC_LINK = 'STATIC_LINK' 89 90 91@dataclass(frozen=True) 92class Relationship: 93 id1: str 94 relationship: RelationshipType 95 id2: str 96 97 98@dataclass(frozen=True) 99class DocumentExternalReference: 100 id: str 101 uri: str 102 checksum: str 103 104 105@dataclass(frozen=True) 106class License: 107 id: str 108 text: str 109 name: str 110 111 112@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) 124 licenses: List[License] = field(default_factory=list) 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): 131 p = next((p for p in self.packages if package.id == p.id), None) 132 if not p: 133 self.packages.append(package) 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) 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) 143 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 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: 156 checksums.append(file.checksum.split(': ')[1]) 157 checksums.sort() 158 h = hashlib.sha1() 159 h.update(''.join(checksums).encode(encoding='utf-8')) 160 package.verification_code = h.hexdigest() 161 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('-')