xref: /aosp_15_r20/build/make/tools/sbom/sbom_data.py (revision 9e94795a3d4ef5c1d47486f9a02bb378756cea8a)
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('-')