10 import multiprocessing
12 from subprocess
import Popen, PIPE
13 from typing
import Any
15 from .CliOptions
import CliOptions
16 from .Packages
import Packages
23 _NO_LICENSE = frozenset({
24 'No_license_found',
'NOASSERTION',
'NONE',
'UnclassifiedLicense',
30 Store scan results from agents.
32 :ivar file: File location
33 :ivar path: Actual location of file
34 :ivar result: License list for file
38 result: set[str] =
None
40 def __init__(self, file: str, path: str, result: set[str]):
48 Store scan results from agents with result as a list of dictionaries.
50 :ivar file: File location
51 :ivar path: Actual location of file
52 :ivar result: License list for file as a list of dictionaries
56 result: list[dict] =
None
58 def __init__(self, file: str, path: str, result: list[dict]):
66 Handle all the data from different scanners.
68 :ivar nomos_path: path to nomos bin
69 :ivar copyright_path: path to copyright bin
70 :ivar keyword_path: path to keyword bin
71 :ivar ojo_path: path to ojo bin
72 :ivar cli_options: CliOptions object
74 nomos_path: str =
'/bin/nomossa'
75 copyright_path: str =
'/bin/copyright'
76 keyword_path: str =
'/bin/keyword'
77 ojo_path: str =
'/bin/ojo'
79 def __init__(self, cli_options: CliOptions, scan_packages: Packages):
81 Initialize the cli_options
83 :param cli_options: CliOptions object to use
84 :type cli_options: CliOptions
85 :param scan_packages: ScanPackages for references
86 :type scan_packages: Packages
88 self.cli_options: CliOptions = cli_options
89 self.scan_packages: Packages = scan_packages
91 self.cli_options.allowlist.get(
'licenses', [])
94 def get_scan_packages(self) -> Packages:
95 return self.scan_packages
99 Check if the path is allow listed
101 The function used fnmatch to check if the path is in allow list or not.
103 :param path: path to check
104 :return: True if the path is in allow list, False otherwise
106 for pattern
in self.cli_options.allowlist.get(
'exclude', []):
107 if fnmatch.fnmatchcase(path, pattern):
113 Normalize the given path against the given directory.
115 :param path: path to normalize
116 :param against: directory to normalize against
117 :return: Normalized path
119 if not against.endswith(os.sep):
121 start_index_of_prefix = path.find(against)
122 if start_index_of_prefix == -1:
125 relative_path_start_index = start_index_of_prefix + len(against)
126 return path[relative_path_start_index:]
129 self, scanner_path: str, dir_to_scan: str, extra_args: list[str] =
None
132 Helper to execute a scanner command and return its JSON output.
134 command = [scanner_path,
"-J",
"-d", dir_to_scan]
136 command.extend(extra_args)
140 process = Popen(command, stdout=PIPE, text=
True, encoding=
'UTF-8')
141 stdout, stderr = process.communicate()
143 if process.returncode != 0:
144 msg = (f
"Scanner {scanner_path} exited with error code "
145 f
"{process.returncode}. Stderr: {stderr}")
147 raise RuntimeError(msg)
150 if not stdout.strip():
153 return json.loads(stdout.strip())
154 except FileNotFoundError
as e:
155 print(f
"Error: Scanner executable not found at {scanner_path}")
157 except json.JSONDecodeError
as e:
158 print(f
"Error: Failed to decode JSON from scanner {scanner_path} output.")
159 print(f
"Raw output: {stdout}")
161 except Exception
as e:
162 print(f
"An unexpected error occurred while running {scanner_path}: {e}")
167 Get the raw results from nomos scanner
169 :return: raw json from nomos
171 extra_args = [
"-S",
"-l",
"-n", str(multiprocessing.cpu_count() - 1)]
173 self.nomos_path, dir_to_scan, extra_args
178 Get the raw results from ojo scanner
180 :return: raw json from ojo, normalized to nomos format
186 if isinstance(raw, list):
190 'file': entry.get(
'file',
''),
191 'licenses': entry.get(
'results')
or []
193 return {
'results': transformed}
198 Get the raw results from copyright scanner
200 :return: raw json from copyright
206 Get the raw results from keyword scanner
208 :return: raw json from keyword
213 self, component: dict, is_parent: bool, scanner_func: callable,
214 result_key: str, whole: bool =
False, all_results: bool =
False
215 ) -> list[ScanResult] | list[ScanResultList]:
217 Generalized function to process results from a single scanner for a given
218 component. Set `result_key` to 'results' for copyrights and 'licenses' for
221 dir_to_scan = self.cli_options.diff_dir
if is_parent
else os.path.join(
222 component[
'download_dir'], component[
'base_dir']
225 raw_results = scanner_func(dir_to_scan)
226 processed_list: list[ScanResult] | list[ScanResultList] = []
227 raw_results_list: list[
228 dict[str, str | list[dict[str, str | int]] |
None]] = []
230 if isinstance(raw_results, dict):
231 if 'results' in raw_results:
232 raw_results_list = raw_results[
'results']
233 elif isinstance(raw_results, list):
234 raw_results_list = raw_results
236 if not raw_results_list:
237 return processed_list
239 for result_entry
in raw_results_list:
242 'file' not in result_entry
243 or result_key
not in result_entry
244 or result_entry.get(result_key) ==
"Unable to read file"
248 file_path = self.
__normalize_path__normalize_path(result_entry[
'file'], dir_to_scan)
250 if self.cli_options.repo
and not all_results
and self.
is_excluded_pathis_excluded_path(
255 current_findings: set[str] | list[dict[str, Any]] = set()
if not whole \
258 findings_list = result_entry.get(result_key,
None)
259 if findings_list
is None:
262 for finding
in findings_list:
269 result_key ==
'results'
270 and 'type' in finding
271 and finding[
'type'] ==
'statement'
272 and finding.get(
'content')
274 current_findings.append(finding)
276 result_key ==
'licenses'
277 and finding.get(
'license')
not in _NO_LICENSE
279 current_findings.append(finding)
282 content = finding.get(
'content')
or finding.get(
'license')
283 content = content.strip()
285 result_key ==
'results'
286 and 'type' in finding
287 and finding[
'type'] !=
'statement'
291 if content
and content
not in _NO_LICENSE:
292 current_findings.add(content)
299 processed_list.append(
303 processed_list.append(
304 ScanResult(file_path, result_entry[
'file'], current_findings)
307 return processed_list
310 self, all_results: bool =
False, whole: bool =
False
313 Set the formatted results from copyright scanner for the components.
315 if not self.cli_options.scan_only_deps:
316 self.scan_packages.parent_package[
318 component=self.scan_packages.parent_package, is_parent=
True,
320 whole=whole, all_results=all_results
322 for purl
in self.scan_packages.dependencies.keys():
323 component = self.scan_packages.dependencies[purl]
325 component=component, is_parent=
False,
327 whole=whole, all_results=all_results
332 Get the formatted results from keyword scanner
334 if not self.cli_options.scan_only_deps:
335 self.scan_packages.parent_package[
337 component=self.scan_packages.parent_package, is_parent=
True,
341 for purl
in self.scan_packages.dependencies.keys():
342 component = self.scan_packages.dependencies[purl]
344 component=component, is_parent=
False,
351 Update the packages with formatted results of nomos scanner
353 if not self.cli_options.scan_only_deps:
354 self.scan_packages.parent_package[
356 component=self.scan_packages.parent_package, is_parent=
True,
357 scanner_func=self.
__get_nomos_result__get_nomos_result, result_key=
'licenses', whole=whole
359 for purl
in self.scan_packages.dependencies.keys():
360 component = self.scan_packages.dependencies[purl]
362 component=component, is_parent=
False,
363 scanner_func=self.
__get_nomos_result__get_nomos_result, result_key=
'licenses', whole=whole
368 Update the packages with formatted results of ojo scanner
370 if not self.cli_options.scan_only_deps:
371 self.scan_packages.parent_package[
373 component=self.scan_packages.parent_package, is_parent=
True,
374 scanner_func=self.
__get_ojo_result__get_ojo_result, result_key=
'licenses', whole=whole
376 for purl
in self.scan_packages.dependencies.keys():
377 component = self.scan_packages.dependencies[purl]
379 component=component, is_parent=
False,
380 scanner_func=self.
__get_ojo_result__get_ojo_result, result_key=
'licenses', whole=whole
384 self, nomos_licenses: list[ScanResult] | list[ScanResultList],
385 ojo_licenses: list[ScanResult] | list[ScanResultList],
387 ) -> list[ScanResult] | list[ScanResultList]:
389 Merge the results from nomos and ojo based on file name
391 nomos_dict = {entry.file: entry
for entry
in nomos_licenses}
393 for ojo_entry
in ojo_licenses:
394 if ojo_entry.file
in nomos_dict:
395 existing = nomos_dict[ojo_entry.file]
398 item.get(
'license')
for item
in existing.result
399 if isinstance(item, dict)
401 for item
in ojo_entry.result:
402 lic = item.get(
'license')
if isinstance(item, dict)
else item
404 existing.result.append(item)
407 existing.result.update(ojo_entry.result)
410 nomos_licenses.append(ojo_entry)
411 return nomos_licenses
414 self, scan_results: list[ScanResult] =
None,
415 scan_results_whole: list[ScanResultList] =
None, whole: bool =
False
416 ) -> list[ScanResult] | list[ScanResultList]:
418 Get results where license check failed.
421 if whole
and scan_results_whole
is not None:
422 for row
in scan_results_whole:
423 if self.cli_options.repo
and self.
is_excluded_pathis_excluded_path(row.file):
427 failed_licenses_list = [
428 lic
for lic
in row.result
if
431 if failed_licenses_list:
432 final_results.append(
435 elif not whole
and scan_results
is not None:
436 for row
in scan_results:
437 if self.cli_options.repo
and self.
is_excluded_pathis_excluded_path(row.file):
442 lic
for lic
in row.result
if
446 final_results.append(
ScanResult(row.file, row.path, failed_licenses))
451 Get copyrights from files which are not allow listed.
454 return [row
for row
in copyright_results
if
455 self.cli_options.repo
and not self.
is_excluded_pathis_excluded_path(row.file)]
459 Get list of copyright scan results from the package list.
461 copyright_results = []
462 copyright_results.extend(
463 self.scan_packages.parent_package.get(
'COPYRIGHT_RESULT', [])
465 for dep
in self.scan_packages.dependencies.values():
466 copyright_results.extend(dep.get(
'COPYRIGHT_RESULT', []))
467 return copyright_results
471 Get list of keywords scan results from the package list.
474 keyword_results.extend(
475 self.scan_packages.parent_package.get(
'KEYWORD_RESULT', [])
477 for dep
in self.scan_packages.dependencies.values():
478 keyword_results.extend(dep.get(
'KEYWORD_RESULT', []))
479 return keyword_results
483 Get list of license scan results from the package list.
486 scanner_results.extend(
487 self.scan_packages.parent_package.get(
'SCANNER_RESULTS', [])
489 for dep
in self.scan_packages.dependencies.values():
490 scanner_results.extend(dep.get(
'SCANNER_RESULTS', []))
491 return scanner_results
494 self, whole: bool =
False
495 ) -> list[ScanResult] | list[ScanResultList]:
497 Get the formatted list of license scanner findings
499 The list contains the merged result of nomos/ojo scanner based on
505 scan_results_whole=scanner_results, whole=
True
509 return failed_licenses
514 item.file, item.path,
515 {res[
'license']
for res
in item.result
if 'license' in res}
516 )
for item
in failed_licenses
521 Set the key `SCANNER_RESULTS` for all components in scan_packages using
522 nomos and ojo scanners (whichever is selected).
524 if self.cli_options.nomos:
526 if self.cli_options.ojo:
529 if self.cli_options.nomos
and self.cli_options.ojo:
533 self.scan_packages.parent_package[
535 self.scan_packages.parent_package.get(
'NOMOS_RESULT', []),
536 self.scan_packages.parent_package.get(
'OJO_RESULT', []),
539 for purl
in self.scan_packages.dependencies.keys():
540 component = self.scan_packages.dependencies[purl]
542 component.get(
'NOMOS_RESULT', []), component.get(
'OJO_RESULT', []),
546 scanner_key =
'NOMOS_RESULT' if self.cli_options.nomos
else 'OJO_RESULT'
548 self.scan_packages.parent_package[
549 'SCANNER_RESULTS'] = self.scan_packages.parent_package.get(
552 for purl
in self.scan_packages.dependencies.keys():
553 component = self.scan_packages.dependencies[purl]
554 component[
'SCANNER_RESULTS'] = component.get(scanner_key, [])
list[ScanResultList] get_keyword_results(self)
def __init__(self, CliOptions cli_options, Packages scan_packages)
list[ScanResultList] get_license_results(self)
dict __get_keyword_results(self, str dir_to_scan)
None set_scanner_results(self, bool whole=False)
None set_copyright_list(self, bool all_results=False, bool whole=False)
dict __get_ojo_result(self, str dir_to_scan)
dict __get_copyright_results(self, str dir_to_scan)
list[ScanResult]|list[ScanResultList] _process_single_scanner_package(self, dict component, bool is_parent, callable scanner_func, str result_key, bool whole=False, bool all_results=False)
dict _execute_scanner_command(self, str scanner_path, str dir_to_scan, list[str] extra_args=None)
list[ScanResult]|list[ScanResultList] results_are_allow_listed(self, bool whole=False)
str __normalize_path(self, str path, str against)
dict __get_nomos_result(self, str dir_to_scan)
None set_keyword_list(self, bool whole=False)
None __set_license_nomos(self, bool whole=False)
list[ScanResult]|list[ScanResultList] get_non_allow_listed_results(self, list[ScanResult] scan_results=None, list[ScanResultList] scan_results_whole=None, bool whole=False)
None __set_license_ojo(self, bool whole=False)
list[ScanResultList] get_copyright_results(self)
list[ScanResult]|list[ScanResultList] __merge_nomos_ojo(self, list[ScanResult]|list[ScanResultList] nomos_licenses, list[ScanResult]|list[ScanResultList] ojo_licenses, bool whole=False)
list[ScanResult] get_non_allow_listed_copyrights(self)
bool is_excluded_path(self, str path)