1# Copyright (c) 2014 Ahmed H. Ismail 2# Licensed under the Apache License, Version 2.0 (the "License"); 3# you may not use this file except in compliance with the License. 4# You may obtain a copy of the License at 5# http://www.apache.org/licenses/LICENSE-2.0 6# Unless required by applicable law or agreed to in writing, software 7# distributed under the License is distributed on an "AS IS" BASIS, 8# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9# See the License for the specific language governing permissions and 10# limitations under the License. 11 12from __future__ import absolute_import 13from __future__ import print_function 14from __future__ import unicode_literals 15 16import hashlib 17 18from six.moves import reduce 19 20from spdx import checksum 21from spdx import creationinfo 22from spdx import document 23from spdx import utils 24 25 26class Package(object): 27 28 """ 29 Represent an analyzed Package. 30 Fields: 31 - name : Mandatory, string. 32 - spdx_id: Uniquely identify any element in an SPDX document which may be 33 referenced by other elements. Mandatory, one. Type: str. 34 - version: Optional, string. 35 - file_name: Optional, string. 36 - supplier: Optional, Organization or Person or NO_ASSERTION. 37 - originator: Optional, Organization or Person. 38 - download_location: Mandatory, URL as string. 39 - files_analyzed: Indicates whether the file content of this package has 40 been available for or subjected to analysis when creating the SPDX 41 document. If "false" indicates packages that represent metadata or URI 42 references to a project, product, artifact, distribution or a component. 43 If set to "false", the package must not contain any files. 44 Optional, boolean. 45 - homepage: Optional, URL as string or NONE or NO_ASSERTION. 46 - verif_code: Mandatory string. 47 - check_sum: Optional , spdx.checksum.Algorithm. 48 - source_info: Optional string. 49 - conc_lics: Mandatory spdx.document.License or spdx.utils.SPDXNone or 50 - spdx.utils.NoAssert. 51 - license_declared : Mandatory spdx.document.License or spdx.utils.SPDXNone or 52 - spdx.utils.NoAssert. 53 - license_comment : optional string. 54 - licenses_from_files: list of spdx.document.License or spdx.utils.SPDXNone or 55 - spdx.utils.NoAssert. 56 - cr_text: Copyright text, string , utils.NoAssert or utils.SPDXNone. Mandatory. 57 - summary: Optional str. 58 - description: Optional str. 59 - comment: Comments about the package being described, optional one. 60 Type: str 61 - files: List of files in package, atleast one. 62 - verif_exc_files : list of file names excluded from verification code or None. 63 - ext_pkg_refs : External references referenced within the given package. 64 Optional, one or many. Type: ExternalPackageRef 65 """ 66 67 def __init__(self, name=None, spdx_id=None, download_location=None, 68 version=None, file_name=None, supplier=None, originator=None): 69 self.name = name 70 self.spdx_id = spdx_id 71 self.version = version 72 self.file_name = file_name 73 self.supplier = supplier 74 self.originator = originator 75 self.download_location = download_location 76 self.files_analyzed = None 77 self.homepage = None 78 self.verif_code = None 79 self.check_sum = None 80 self.source_info = None 81 self.conc_lics = None 82 self.license_declared = None 83 self.license_comment = None 84 self.licenses_from_files = [] 85 self.cr_text = None 86 self.summary = None 87 self.description = None 88 self.comment = None 89 self.files = [] 90 self.verif_exc_files = [] 91 self.pkg_ext_refs = [] 92 93 def add_file(self, fil): 94 self.files.append(fil) 95 96 def add_lics_from_file(self, lics): 97 self.licenses_from_files.append(lics) 98 99 def add_exc_file(self, filename): 100 self.verif_exc_files.append(filename) 101 102 def add_pkg_ext_refs(self, pkg_ext_ref): 103 self.pkg_ext_refs.append(pkg_ext_ref) 104 105 def validate(self, messages=None): 106 """ 107 Validate the package fields. 108 Append user friendly error messages to the `messages` list. 109 """ 110 messages = self.validate_checksum(messages) 111 messages = self.validate_optional_str_fields(messages) 112 messages = self.validate_mandatory_str_fields(messages) 113 messages = self.validate_files(messages) 114 messages = self.validate_pkg_ext_refs(messages) 115 messages = self.validate_mandatory_fields(messages) 116 messages = self.validate_optional_fields(messages) 117 118 return messages 119 120 def validate_optional_fields(self, messages): 121 if self.originator and not isinstance(self.originator, (utils.NoAssert, creationinfo.Creator)): 122 messages = messages + [ 123 'Package originator must be instance of ' 124 'spdx.utils.NoAssert or spdx.creationinfo.Creator' 125 ] 126 127 if self.supplier and not isinstance(self.supplier, (utils.NoAssert, creationinfo.Creator)): 128 messages = messages + [ 129 'Package supplier must be instance of ' 130 'spdx.utils.NoAssert or spdx.creationinfo.Creator' 131 ] 132 133 return messages 134 135 def validate_pkg_ext_refs(self, messages=None): 136 for ref in self.pkg_ext_refs: 137 if isinstance(ref, ExternalPackageRef): 138 messages = ref.validate(messages) 139 else: 140 messages = messages + [ 141 'External package references must be of the type ' 142 'spdx.package.ExternalPackageRef and not ' + str(type(ref)) 143 ] 144 145 return messages 146 147 def validate_mandatory_fields(self, messages): 148 if not isinstance(self.conc_lics, (utils.SPDXNone, utils.NoAssert, document.License)): 149 messages = messages + [ 150 'Package concluded license must be instance of ' 151 'spdx.utils.SPDXNone or spdx.utils.NoAssert or ' 152 'spdx.document.License' 153 ] 154 155 if not isinstance(self.license_declared, (utils.SPDXNone, utils.NoAssert, document.License)): 156 messages = messages + [ 157 'Package declared license must be instance of ' 158 'spdx.utils.SPDXNone or spdx.utils.NoAssert or ' 159 'spdx.document.License' 160 ] 161 162 # FIXME: this is obscure and unreadable 163 license_from_file_check = lambda prev, el: prev and isinstance(el, (document.License, utils.SPDXNone, utils.NoAssert)) 164 if not reduce(license_from_file_check, self.licenses_from_files, True): 165 messages = messages + [ 166 'Each element in licenses_from_files must be instance of ' 167 'spdx.utils.SPDXNone or spdx.utils.NoAssert or ' 168 'spdx.document.License' 169 ] 170 171 if not self.licenses_from_files: 172 messages = messages + [ 173 'Package licenses_from_files can not be empty' 174 ] 175 176 return messages 177 178 def validate_files(self, messages): 179 if not self.files: 180 messages = messages + [ 181 'Package must have at least one file.' 182 ] 183 else: 184 for f in self.files: 185 messages = f.validate(messages) 186 187 return messages 188 189 def validate_optional_str_fields(self, messages): 190 """Fields marked as optional and of type string in class 191 docstring must be of a type that provides __str__ method. 192 """ 193 FIELDS = [ 194 'file_name', 195 'version', 196 'homepage', 197 'source_info', 198 'summary', 199 'description', 200 'comment' 201 ] 202 messages = self.validate_str_fields(FIELDS, True, messages) 203 204 return messages 205 206 def validate_mandatory_str_fields(self, messages): 207 """Fields marked as Mandatory and of type string in class 208 docstring must be of a type that provides __str__ method. 209 """ 210 FIELDS = ['name', 'spdx_id', 'download_location', 'verif_code', 'cr_text'] 211 messages = self.validate_str_fields(FIELDS, False, messages) 212 213 return messages 214 215 def validate_str_fields(self, fields, optional, messages): 216 """Helper for validate_mandatory_str_field and 217 validate_optional_str_fields""" 218 for field_str in fields: 219 field = getattr(self, field_str) 220 if field is not None: 221 # FIXME: this does not make sense??? 222 attr = getattr(field, '__str__', None) 223 if not callable(attr): 224 messages = messages + [ 225 '{0} must provide __str__ method.'.format(field) 226 ] 227 # Continue checking. 228 elif not optional: 229 messages = messages + [ 230 'Package {0} can not be None.'.format(field_str) 231 ] 232 233 return messages 234 235 def validate_checksum(self, messages): 236 if not isinstance(self.check_sum, checksum.Algorithm): 237 messages = messages + [ 238 'Package checksum must be instance of spdx.checksum.Algorithm' 239 ] 240 else: 241 if self.check_sum.identifier != 'SHA1': 242 messages = messages + ['File checksum algorithm must be SHA1'] 243 244 return messages 245 246 def calc_verif_code(self): 247 hashes = [] 248 249 for file_entry in self.files: 250 if (isinstance(file_entry.chk_sum, checksum.Algorithm) and 251 file_entry.chk_sum.identifier == 'SHA1'): 252 sha1 = file_entry.chk_sum.value 253 else: 254 sha1 = file_entry.calc_chksum() 255 hashes.append(sha1) 256 257 hashes.sort() 258 259 sha1 = hashlib.sha1() 260 sha1.update(''.join(hashes).encode('utf-8')) 261 return sha1.hexdigest() 262 263 def has_optional_field(self, field): 264 return getattr(self, field, None) is not None 265 266 267class ExternalPackageRef(object): 268 """ 269 An External Reference allows a Package to reference an external source of 270 additional information, metadata, enumerations, asset identifiers, or 271 downloadable content believed to be relevant to the Package. 272 Fields: 273 - category: "SECURITY" or "PACKAGE-MANAGER" or "OTHER". 274 - pkg_ext_ref_type: A unique string containing letters, numbers, ".","-". 275 - locator: A unique string with no spaces necessary to access the 276 package-specific information, metadata, or content within the target 277 location. 278 - comment: To provide information about the purpose and target of the 279 reference. 280 """ 281 282 def __init__(self, category=None, pkg_ext_ref_type=None, locator=None, 283 comment=None): 284 self.category = category 285 self.pkg_ext_ref_type = pkg_ext_ref_type 286 self.locator = locator 287 self.comment = comment 288 289 def validate(self, messages=None): 290 """ 291 Validate all fields of the ExternalPackageRef class and update the 292 messages list with user friendly error messages for display. 293 """ 294 messages = self.validate_category(messages) 295 messages = self.validate_pkg_ext_ref_type(messages) 296 messages = self.validate_locator(messages) 297 298 return messages 299 300 def validate_category(self, messages=None): 301 if self.category is None: 302 messages = messages + ['ExternalPackageRef has no category.'] 303 304 return messages 305 306 def validate_pkg_ext_ref_type(self, messages=None): 307 if self.pkg_ext_ref_type is None: 308 messages = messages + ['ExternalPackageRef has no type.'] 309 310 return messages 311 312 def validate_locator(self, messages=None): 313 if self.locator is None: 314 messages = messages + ['ExternalPackageRef has no locator.'] 315 316 return messages 317