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' 33 34PACKAGE_NAME_PRODUCT = 'PRODUCT' 35PACKAGE_NAME_PLATFORM = 'PLATFORM' 36 37VALUE_NOASSERTION = 'NOASSERTION' 38VALUE_NONE = 'NONE' 39 40 41class PackageExternalRefCategory: 42 SECURITY = 'SECURITY' 43 PACKAGE_MANAGER = 'PACKAGE-MANAGER' 44 PERSISTENT_ID = 'PERSISTENT-ID' 45 OTHER = 'OTHER' 46 47 48class PackageExternalRefType: 49 cpe22Type = 'cpe22Type' 50 cpe23Type = 'cpe23Type' 51 52 53@dataclass 54class PackageExternalRef: 55 category: PackageExternalRefCategory 56 type: PackageExternalRefType 57 locator: str 58 59 60@dataclass 61class Package: 62 name: str 63 id: str 64 version: str = None 65 supplier: str = None 66 download_location: str = None 67 files_analyzed: bool = False 68 verification_code: str = None 69 file_ids: List[str] = field(default_factory=list) 70 external_refs: List[PackageExternalRef] = field(default_factory=list) 71 72 73@dataclass 74class File: 75 id: str 76 name: str 77 checksum: str 78 79 80class RelationshipType: 81 DESCRIBES = 'DESCRIBES' 82 VARIANT_OF = 'VARIANT_OF' 83 GENERATED_FROM = 'GENERATED_FROM' 84 CONTAINS = 'CONTAINS' 85 STATIC_LINK = 'STATIC_LINK' 86 87 88@dataclass 89class Relationship: 90 id1: str 91 relationship: RelationshipType 92 id2: str 93 94 95@dataclass 96class DocumentExternalReference: 97 id: str 98 uri: str 99 checksum: str 100 101 102@dataclass 103class Document: 104 name: str 105 namespace: str 106 id: str = SPDXID_DOC 107 describes: str = SPDXID_PRODUCT 108 creators: List[str] = field(default_factory=list) 109 created: str = None 110 external_refs: List[DocumentExternalReference] = field(default_factory=list) 111 packages: List[Package] = field(default_factory=list) 112 files: List[File] = field(default_factory=list) 113 relationships: List[Relationship] = field(default_factory=list) 114 115 def add_external_ref(self, external_ref): 116 if not any(external_ref.uri == ref.uri for ref in self.external_refs): 117 self.external_refs.append(external_ref) 118 119 def add_package(self, package): 120 if not any(package.id == p.id for p in self.packages): 121 self.packages.append(package) 122 123 def add_relationship(self, rel): 124 if not any(rel.id1 == r.id1 and rel.id2 == r.id2 and rel.relationship == r.relationship 125 for r in self.relationships): 126 self.relationships.append(rel) 127 128 def generate_packages_verification_code(self): 129 for package in self.packages: 130 if not package.file_ids: 131 continue 132 133 checksums = [] 134 for file in self.files: 135 if file.id in package.file_ids: 136 checksums.append(file.checksum.split(': ')[1]) 137 checksums.sort() 138 h = hashlib.sha1() 139 h.update(''.join(checksums).encode(encoding='utf-8')) 140 package.verification_code = h.hexdigest() 141 142def encode_for_spdxid(s): 143 """Simple encode for string values used in SPDXID which uses the charset of A-Za-Z0-9.-""" 144 result = '' 145 for c in s: 146 if c.isalnum() or c in '.-': 147 result += c 148 elif c in '_@/': 149 result += '-' 150 else: 151 result += '0x' + c.encode('utf-8').hex() 152 153 return result.lstrip('-')