11 from datetime
import datetime
13 from license_expression
import (
14 get_spdx_licensing, LicenseExpression, combine_expressions
16 from spdx_tools.spdx.model
import (
17 Actor, ActorType, Checksum, ChecksumAlgorithm, CreationInfo, Document, File,
18 FileType, Package, PackageVerificationCode, Relationship, RelationshipType,
19 SpdxNoAssertion, ExternalPackageRef, ExternalPackageRefCategory, SpdxNone,
20 ExtractedLicensingInfo
22 from spdx_tools.spdx.validation.document_validator
import \
23 validate_full_spdx_document
24 from spdx_tools.spdx.validation.validation_message
import ValidationMessage
25 from spdx_tools.spdx.writer.write_anything
import write_file
27 from .CliOptions
import CliOptions
28 from .ReportBase
import ReportBase
29 from .Scanners
import Scanners, ScanResultList
36 :ivar cli_options: CliOptions object
37 :ivar report_files: Dictionary of SPDX files with SPDX ID as key
38 :ivar license_package_set: Set of licenses found in package
39 :ivar creation_info: Report creation info
40 :ivar document: Report document
41 :ivar package: Report package
42 :ivar scanner: Scanners object
45 def __init__(self, cli_options: CliOptions, scanner: Scanners):
47 :param cli_options: CliOptions to use
48 :param scanner: Scanners to use
54 self._license_cache: dict[str, LicenseExpression] = {}
57 self.report_files: dict[str, File] = {}
58 self.license_package_set: set[str] = set()
59 self.package_verification_set: dict[str, dict[str, list[str]]] = {}
60 self.creation_info: CreationInfo = CreationInfo(
61 spdx_version=
"SPDX-2.3",
62 spdx_id=
"SPDXRef-DOCUMENT",
63 name=
"FOSSology CI Report",
64 data_license=
"CC0-1.0",
65 document_namespace=
"https://fossology.org",
68 ActorType.ORGANIZATION,
"FOSSology",
69 "fossology@fossology.org"
71 ], created=datetime.now(),
73 self.document: Document = Document(self.creation_info)
75 parent_package = self.
scannerscannerscanner.get_scan_packages().parent_package
76 project_name = parent_package.get(
'name',
'').strip()
82 self.package: Package = Package(
84 spdx_id=
"SPDXRef-Package",
86 download_location=SpdxNoAssertion(),
87 release_date=datetime.now(),
89 if parent_package.get(
'description')
is not None:
90 self.package.description = parent_package[
'description']
92 author = parent_package.get(
'author')
93 if author
and author !=
"":
94 self.package.originator = Actor(
95 ActorType.ORGANIZATION,
99 self.package.originator = SpdxNoAssertion()
101 url = parent_package.get(
'url')
102 if url
and url !=
"":
103 self.package.download_location = url
105 self.package.download_location = SpdxNoAssertion()
107 self.document.packages = [self.package]
108 self.dependent_packages: dict[str, Package] = {}
109 self.extracted_licenses: dict[str, ExtractedLicensingInfo] = {}
111 def __get_license_or_ref(self, lic: str) -> LicenseExpression:
112 if lic
in self._license_cache:
113 return self._license_cache[lic]
116 license_spdx = re.sub(
117 r'[^\da-zA-Z.\-_]',
'-',
118 f
"LicenseRef-fossology-{lic}"
120 if license_spdx
not in self.extracted_licenses:
121 self.extracted_licenses[license_spdx] = ExtractedLicensingInfo(
122 license_id=license_spdx,
124 extracted_text=f
"The license text for {license_spdx} has to be "
127 lic_expression = self.
_spdx_lic_cache_spdx_lic_cache.parse(license_spdx)
128 self._license_cache[lic] = lic_expression
129 return lic_expression
133 Add scan result from license scanner to report.
135 :param package: Package to which the file belongs.
136 :param scan_result: Scan result from license scanner.
138 raw_licenses_strings = [lic[
'license']
for lic
in scan_result.result]
140 raw_licenses_strings}
141 parsed_expressions_list =
list(parsed_expressions_set)
143 all_allowed_licenses = all(
149 if all_allowed_licenses:
150 file.license_concluded = combine_expressions(
151 expressions=parsed_expressions_list, relation=
'AND', unique=
False
154 file.license_concluded = SpdxNoAssertion()
156 file.license_info_in_file = parsed_expressions_list
159 package.license_info_from_files =
list(
160 set(package.license_info_from_files) | parsed_expressions_set
164 if file.license_concluded != SpdxNoAssertion():
165 if package.license_concluded
in (SpdxNoAssertion(), SpdxNone()):
166 package.license_concluded = file.license_concluded
168 package.license_concluded = (
169 package.license_concluded & file.license_concluded).simplify()
172 self, scan_result: ScanResultList, package: Package
175 Create a new SPDX File for given scan result and populate common fields.
177 :param scan_result: Scan result from scanner.
178 :param package: Package to which the file belongs.
179 :return: New SPDX File
181 md5_hash, sha1_hash, sha256_hash = self.
__get_file_info__get_file_info(scan_result)
183 if file_spdx_id
not in self.report_files:
185 name=scan_result.file,
186 spdx_id=file_spdx_id,
188 Checksum(ChecksumAlgorithm.MD5, md5_hash),
189 Checksum(ChecksumAlgorithm.SHA1, sha1_hash),
190 Checksum(ChecksumAlgorithm.SHA256, sha256_hash),
192 file_types=[FileType.SOURCE],
193 license_concluded=SpdxNoAssertion()
195 self.report_files[file_spdx_id] = spdx_file
196 contains_relationship = Relationship(
198 RelationshipType.CONTAINS,
201 self.document.relationships.append(contains_relationship)
203 pkg_verification_data = self.package_verification_set.setdefault(
205 'checksums': [],
'excluded_files': []
210 pkg_verification_data[
'excluded_files'].append(spdx_file.name)
212 pkg_verification_data[
'checksums'].append(sha1_hash)
214 return self.report_files[file_spdx_id]
217 self, package: Package, copyright_result: ScanResultList
220 Add scan result from copyright agent. If the file does not exist, creates a
223 :param copyright_result: Scan result from copyright scanner.
226 file.copyright_text =
"\n".join(
228 cpy.get(
'content',
'')
for cpy
in copyright_result.result
235 Get different hash for the file in scan result.
237 :param scan_result: Scan result from scanners.
238 :return: Tuple of md5, sha1 and sha256 checksums.
240 md5_hash = hashlib.md5()
241 sha1_hash = hashlib.sha1()
242 sha256_hash = hashlib.sha256()
243 with open(scan_result.path,
"rb")
as f:
244 for byte_block
in iter(
lambda: f.read(4096), b
""):
245 md5_hash.update(byte_block)
246 sha1_hash.update(byte_block)
247 sha256_hash.update(byte_block)
248 return md5_hash.hexdigest(), sha1_hash.hexdigest(), sha256_hash.hexdigest()
253 Generate SPDX ID for file in scan result.
255 :param sha256_hash: SHA 256 checksum of the file
256 :param pkg_name: Package to which the file belongs.
257 :return: SPDX ID for the file.
259 return f
"SPDXRef-File-{pkg_name}-{sha256_hash}"
264 Generate SPDX ID for a package/component.
266 :param component: Package/component to get SPDX ID for.
267 :return: SPDX ID for the package.
269 pkg_name = component.get(
'name',
'')
270 pkg_version = component.get(
'version',
'')
271 return "SPDXRef-Package-" + hashlib.md5(
272 f
"{pkg_name}_{pkg_version}".encode(
'utf-8', errors=
'ignore')
277 Validate the document and write the SPDX file.
279 :param file_name: Location to store the report.
281 validation_messages: list[ValidationMessage] = validate_full_spdx_document(
285 if validation_messages:
286 for message
in validation_messages:
288 f
"SPDX Validation Warning: {message.validation_message}\n"
289 f
"Context: {message.context}"
292 "SPDX document validation failed. See logs for details."
295 logging.info(
"SPDX document validated successfully.")
299 self.document, file_name, validate=
False
304 Finalize the document by setting relations between packages and files.
305 At the same time, add all the licenses from files to the package and
306 calculate the verification code, without the excluded files.
315 def __create_packages(self) -> None:
316 parent_name = self.
scannerscannerscanner.get_scan_packages().parent_package.get(
320 self.package.spdx_id = re.sub(
321 r'[^A-Za-z0-9\-_.]',
'-',
322 f
"SPDXRef-Package-{parent_name}"
324 describes_relationship = Relationship(
326 RelationshipType.DESCRIBES,
329 self.document.relationships.append(describes_relationship)
331 for purl, component
in (
332 self.
scannerscannerscanner.get_scan_packages().dependencies.items()
335 self.document.packages.append(package)
336 depends_on_relationship = Relationship(
337 self.package.spdx_id,
338 RelationshipType.DEPENDS_ON,
341 self.document.relationships.append(depends_on_relationship)
345 For a given component, create a package and add it to the list.
347 :param component: Component to create package for.
348 :return: Create or get existing package.
351 if pkg_spdx_id
not in self.dependent_packages:
352 self.dependent_packages[pkg_spdx_id] = Package(
354 name=component.get(
'name',
'UNKNOWN'),
355 version=component.get(
'version',
'UNKNOWN'),
356 download_location=component.get(
357 'fossology_download_url', SpdxNoAssertion()
359 license_info_from_files=[],
360 license_concluded=SpdxNone(),
363 purl_ref = ExternalPackageRef(
364 category=ExternalPackageRefCategory.PACKAGE_MANAGER,
365 reference_type=
'purl',
366 locator=component.get(
'purl')
368 self.dependent_packages[pkg_spdx_id].external_references.append(purl_ref)
370 vcs_url = component.get(
'vcs_url')
372 vcs_ref = ExternalPackageRef(
373 category=ExternalPackageRefCategory.OTHER, reference_type=
'vcs',
376 self.dependent_packages[pkg_spdx_id].external_references.append(vcs_ref)
378 homepage_url = component.get(
'homepage_url')
380 homepage_ref = ExternalPackageRef(
381 category=ExternalPackageRefCategory.OTHER, reference_type=
'homepage',
384 self.dependent_packages[pkg_spdx_id].external_references.append(
387 return self.dependent_packages[pkg_spdx_id]
389 def __create_license_files(self) -> None:
391 self.
scannerscannerscanner.get_scan_packages().parent_package, self.package
393 for component
in self.
scannerscannerscanner.get_scan_packages().dependencies.values():
400 def __create_copyright_files(self) -> None:
402 self.
scannerscannerscanner.get_scan_packages().parent_package, self.package
404 for component
in self.
scannerscannerscanner.get_scan_packages().dependencies.values():
411 def __create_license_file_from_component(
412 self, component: dict, package: Package
414 for result
in component.get(
'SCANNER_RESULTS', []):
417 def __create_copyright_file_from_component(
418 self, component: dict, package: Package
420 for result
in component.get(
'COPYRIGHT_RESULT', []):
423 def __add_files_to_document(self) -> None:
424 self.document.files =
list(self.report_files.values())
426 def __add_extracted_licenses(self) -> None:
427 self.document.extracted_licensing_info =
list(
428 self.extracted_licenses.values()
431 def __update_package_verification_code(self) -> None:
432 for package
in self.document.packages:
435 package.verification_code = code
438 self, package_spdx_id: str
439 ) -> PackageVerificationCode |
None:
441 Calculate package verification code for the list of checksums and return it.
443 :param package_spdx_id: Package SPDX ID to calculate the verification
445 :return: Package Verification Code based on SPDX specification.
447 pkg_data = self.package_verification_set.
get(package_spdx_id)
451 checksums = pkg_data.get(
'checksums', [])
452 excluded_files = pkg_data.get(
'excluded_files', [])
456 verification_code = hashlib.sha1(
457 "".join(checksums).encode(
'utf-8', errors=
'ignore')
460 return PackageVerificationCode(
461 value=verification_code,
462 excluded_files=excluded_files
def finalize_document(self)
None __update_package_verification_code(self)
def __init__(self, CliOptions cli_options, Scanners scanner)
None __add_files_to_document(self)
None __add_extracted_licenses(self)
File __get_spdx_file(self, ScanResultList scan_result, Package package)
None __create_copyright_file_from_component(self, dict component, Package package)
def __add_copyright_file(self, Package package, ScanResultList copyright_result)
None __create_copyright_files(self)
Package __get_package_for_component(self, dict component)
LicenseExpression __get_license_or_ref(self, str lic)
None __create_license_files(self)
None __create_license_file_from_component(self, dict component, Package package)
def __add_license_file(self, Package package, ScanResultList scan_result)
None __create_packages(self)
tuple[str, str, str] __get_file_info(ScanResultList scan_result)
str __get_package_spdx_id(dict component)
PackageVerificationCode|None __calculate_verification_code(self, str package_spdx_id)
str __get_file_spdx_id(str sha256_hash, str pkg_name)
def write_report(self, str file_name)
list_t type structure used to keep various lists. (e.g. there are multiple lists).