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('-')