fosslight-source 2.3.4__tar.gz → 2.3.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. {fosslight_source-2.3.4/src/fosslight_source.egg-info → fosslight_source-2.3.6}/PKG-INFO +1 -2
  2. {fosslight_source-2.3.4 → fosslight_source-2.3.6}/pyproject.toml +4 -2
  3. {fosslight_source-2.3.4 → fosslight_source-2.3.6}/src/fosslight_source/_help.py +1 -1
  4. {fosslight_source-2.3.4 → fosslight_source-2.3.6}/src/fosslight_source/_parsing_scancode_file_item.py +22 -8
  5. fosslight_source-2.3.6/src/fosslight_source/_scancode_ignore_binaries.py +69 -0
  6. {fosslight_source-2.3.4 → fosslight_source-2.3.6}/src/fosslight_source/cli.py +85 -28
  7. {fosslight_source-2.3.4 → fosslight_source-2.3.6}/src/fosslight_source/run_scancode.py +14 -128
  8. {fosslight_source-2.3.4 → fosslight_source-2.3.6}/src/fosslight_source/run_scanoss.py +76 -11
  9. {fosslight_source-2.3.4 → fosslight_source-2.3.6/src/fosslight_source.egg-info}/PKG-INFO +1 -2
  10. {fosslight_source-2.3.4 → fosslight_source-2.3.6}/src/fosslight_source.egg-info/SOURCES.txt +1 -0
  11. fosslight_source-2.3.6/src/fosslight_source.egg-info/entry_points.txt +5 -0
  12. {fosslight_source-2.3.4 → fosslight_source-2.3.6}/src/fosslight_source.egg-info/requires.txt +0 -1
  13. fosslight_source-2.3.4/src/fosslight_source.egg-info/entry_points.txt +0 -2
  14. {fosslight_source-2.3.4 → fosslight_source-2.3.6}/LICENSE +0 -0
  15. {fosslight_source-2.3.4 → fosslight_source-2.3.6}/MANIFEST.in +0 -0
  16. {fosslight_source-2.3.4 → fosslight_source-2.3.6}/README.md +0 -0
  17. {fosslight_source-2.3.4 → fosslight_source-2.3.6}/setup.cfg +0 -0
  18. {fosslight_source-2.3.4 → fosslight_source-2.3.6}/src/fosslight_source/__init__.py +0 -0
  19. {fosslight_source-2.3.4 → fosslight_source-2.3.6}/src/fosslight_source/_kb_client.py +0 -0
  20. {fosslight_source-2.3.4 → fosslight_source-2.3.6}/src/fosslight_source/_license_matched.py +0 -0
  21. {fosslight_source-2.3.4 → fosslight_source-2.3.6}/src/fosslight_source/_merge.py +0 -0
  22. {fosslight_source-2.3.4 → fosslight_source-2.3.6}/src/fosslight_source/_parsing_scanoss_file.py +0 -0
  23. {fosslight_source-2.3.4 → fosslight_source-2.3.6}/src/fosslight_source/_scan_item.py +0 -0
  24. {fosslight_source-2.3.4 → fosslight_source-2.3.6}/src/fosslight_source/run_manifest_extractor.py +0 -0
  25. {fosslight_source-2.3.4 → fosslight_source-2.3.6}/src/fosslight_source/run_spdx_extractor.py +0 -0
  26. {fosslight_source-2.3.4 → fosslight_source-2.3.6}/src/fosslight_source.egg-info/dependency_links.txt +0 -0
  27. {fosslight_source-2.3.4 → fosslight_source-2.3.6}/src/fosslight_source.egg-info/top_level.txt +0 -0
  28. {fosslight_source-2.3.4 → fosslight_source-2.3.6}/tests/test_tox.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fosslight_source
3
- Version: 2.3.4
3
+ Version: 2.3.6
4
4
  Summary: FOSSLight Source Scanner
5
5
  Author: LG Electronics
6
6
  License-Expression: Apache-2.0
@@ -24,7 +24,6 @@ Requires-Dist: fosslight_util>=2.2.2
24
24
  Requires-Dist: PyYAML
25
25
  Requires-Dist: wheel>=0.38.1
26
26
  Requires-Dist: intbitset
27
- Requires-Dist: fosslight_binary>=5.1.22
28
27
  Requires-Dist: scancode-toolkit>=32.0.2
29
28
  Requires-Dist: lxml>=6.0.1
30
29
  Requires-Dist: cryptography<49; platform_system == "Darwin" and platform_machine == "x86_64"
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
7
7
 
8
8
  [project]
9
9
  name = "fosslight_source"
10
- version = "2.3.4"
10
+ version = "2.3.6"
11
11
  description = "FOSSLight Source Scanner"
12
12
  readme = "README.md"
13
13
  license = "Apache-2.0"
@@ -33,7 +33,6 @@ dependencies = [
33
33
  "PyYAML",
34
34
  "wheel>=0.38.1",
35
35
  "intbitset",
36
- "fosslight_binary>=5.1.22",
37
36
  "scancode-toolkit>=32.0.2",
38
37
  "lxml>=6.0.1",
39
38
  # cryptography 49.x does not provide macOS x86_64 wheels, causing source builds to require OpenSSL/pkg-config.
@@ -56,6 +55,9 @@ Download = "https://github.com/fosslight/fosslight_source_scanner"
56
55
  [project.scripts]
57
56
  fosslight_source = "fosslight_source.cli:main"
58
57
 
58
+ [project.entry-points."scancode_pre_scan"]
59
+ ignore-binaries = "fosslight_source._scancode_ignore_binaries:IgnoreBinaries"
60
+
59
61
  [tool.setuptools]
60
62
  package-dir = {"" = "src"}
61
63
 
@@ -37,7 +37,7 @@ _HELP_MESSAGE_SOURCE_SCANNER = f"""
37
37
  ────────────────────────────────────────────────────────────────────
38
38
  -s <mode> Choose mode: scancode, scanoss, kb, or all(default)
39
39
  -c <number> Number of CPU cores/threads to use for scanning
40
- -t <seconds> Timeout in seconds for ScanCode scanning
40
+ -t <seconds> Timeout in seconds for ScanCode and SCANOSS scanning
41
41
  -j Generate raw scanner results in JSON format
42
42
  --no_merge Keep source paths file-based without folder merge
43
43
  --no_correction Skip OSS information correction with sbom-info.yaml
@@ -73,7 +73,9 @@ def get_error_from_header(header_item: list) -> Tuple[bool, str]:
73
73
  return has_error, str_error
74
74
 
75
75
 
76
- def parsing_scancode_32_earlier(scancode_file_list: list, has_error: bool = False) -> Tuple[bool, list, list, dict]:
76
+ def parsing_scancode_32_earlier(
77
+ scancode_file_list: list, has_error: bool = False, ui_mode: bool = False
78
+ ) -> Tuple[bool, list, list, dict]:
77
79
  rc = True
78
80
  msg = []
79
81
  scancode_file_item = []
@@ -118,7 +120,10 @@ def parsing_scancode_32_earlier(scancode_file_list: list, has_error: bool = Fals
118
120
 
119
121
  # Set the license value
120
122
  license_detected = []
121
- if licenses is None or licenses == "":
123
+ if not licenses:
124
+ licenses = []
125
+ # Keep license and/or copyright findings; UI keeps finding-less files too.
126
+ if not licenses and not copyright_value_list and not ui_mode:
122
127
  continue
123
128
 
124
129
  license_expression_list = file.get("license_expressions", {})
@@ -191,6 +196,9 @@ def parsing_scancode_32_earlier(scancode_file_list: list, has_error: bool = Fals
191
196
  set(license_expression_list))
192
197
  result_item.comment = ','.join(license_expression_list)
193
198
 
199
+ scancode_file_item.append(result_item)
200
+ elif copyright_value_list or ui_mode:
201
+ result_item.copyright = copyright_value_list
194
202
  scancode_file_item.append(result_item)
195
203
  except Exception as ex:
196
204
  msg.append(f"Error Parsing item: {ex}")
@@ -223,7 +231,7 @@ def get_license_expression_spdx(license_expression: str) -> str:
223
231
 
224
232
 
225
233
  def parsing_scancode_32_later(
226
- scancode_file_list: list, has_error: bool = False
234
+ scancode_file_list: list, has_error: bool = False, ui_mode: bool = False
227
235
  ) -> Tuple[bool, list, list, dict]:
228
236
  rc = True
229
237
  msg = []
@@ -258,9 +266,10 @@ def parsing_scancode_32_later(
258
266
  copyright_value_list.append(copyright_data)
259
267
  license_detected = []
260
268
  licenses = file.get("license_detections", [])
261
- if not licenses:
269
+ # Keep license and/or copyright findings; UI keeps finding-less files too.
270
+ if not licenses and not copyright_value_list and not ui_mode:
262
271
  continue
263
- for lic in licenses:
272
+ for lic in licenses or []:
264
273
  matched_lic_list = lic.get("matches", [])
265
274
  for matched_lic in matched_lic_list:
266
275
  found_lic_list = matched_lic.get("license_expression", "")
@@ -322,7 +331,8 @@ def parsing_scancode_32_later(
322
331
 
323
332
 
324
333
  def parsing_file_item(
325
- scancode_file_list: list, has_error: bool, need_matched_license: bool = False
334
+ scancode_file_list: list, has_error: bool, need_matched_license: bool = False,
335
+ ui_mode: bool = False
326
336
  ) -> Tuple[bool, list, list, dict]:
327
337
 
328
338
  rc = True
@@ -330,9 +340,13 @@ def parsing_file_item(
330
340
 
331
341
  first_item = next(iter(scancode_file_list or []), {})
332
342
  if "licenses" in first_item:
333
- rc, scancode_file_item, msg, license_list = parsing_scancode_32_earlier(scancode_file_list, has_error)
343
+ rc, scancode_file_item, msg, license_list = parsing_scancode_32_earlier(
344
+ scancode_file_list, has_error, ui_mode
345
+ )
334
346
  else:
335
- rc, scancode_file_item, msg, license_list = parsing_scancode_32_later(scancode_file_list, has_error)
347
+ rc, scancode_file_item, msg, license_list = parsing_scancode_32_later(
348
+ scancode_file_list, has_error, ui_mode
349
+ )
336
350
  if not need_matched_license:
337
351
  license_list = {}
338
352
  return rc, scancode_file_item, msg, license_list
@@ -0,0 +1,69 @@
1
+ # Copyright (c) 2018 nexB Inc. and others.
2
+ # Copyright (c) 2026 LG Electronics Inc.
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ #
5
+ # Vendored from scancode-ignore-binaries (aboutcode-org/scancode-plugins)
6
+ # so PyPI installs do not need a GitHub git dependency.
7
+ # SPDX-PackageDownloadLocation: https://github.com/aboutcode-org/scancode-plugins/tree/main/misc/scancode-ignore-binaries
8
+
9
+ from plugincode.pre_scan import PreScanPlugin
10
+ from plugincode.pre_scan import pre_scan_impl
11
+ from commoncode.cliutils import PluggableCommandLineOption
12
+ from commoncode.cliutils import PRE_SCAN_GROUP
13
+ from typecode.contenttype import get_type
14
+
15
+
16
+ @pre_scan_impl
17
+ class IgnoreBinaries(PreScanPlugin):
18
+ """
19
+ Ignore binary files.
20
+ """
21
+
22
+ options = [
23
+ PluggableCommandLineOption(
24
+ ('--ignore-binaries',),
25
+ is_flag=True,
26
+ help='Ignore binary files.',
27
+ sort_order=10,
28
+ help_group=PRE_SCAN_GROUP,
29
+ )
30
+ ]
31
+
32
+ def is_enabled(self, ignore_binaries, **kwargs):
33
+ return ignore_binaries
34
+
35
+ def process_codebase(self, codebase, ignore_binaries, **kwargs):
36
+ """
37
+ Remove binary Resources from the resource tree.
38
+ """
39
+ if not ignore_binaries:
40
+ return
41
+
42
+ resources_to_remove = []
43
+ for resource in codebase.walk():
44
+ if not resource.is_file:
45
+ continue
46
+ if is_binary(resource.location):
47
+ resources_to_remove.append(resource)
48
+
49
+ for resource in resources_to_remove:
50
+ resource.remove(codebase)
51
+
52
+
53
+ def is_binary(location):
54
+ """
55
+ Return True if the resource at location is a binary file.
56
+ """
57
+ t = get_type(location)
58
+ return (
59
+ t.is_binary
60
+ or t.is_archive
61
+ or t.is_media
62
+ or t.is_office_doc
63
+ or t.is_compressed
64
+ or t.is_filesystem
65
+ or t.is_winexe
66
+ or t.is_elf
67
+ or t.is_java_class
68
+ or t.is_data
69
+ )
@@ -90,6 +90,7 @@ def main() -> None:
90
90
  parser.add_argument('--kb_url', type=str, required=False, default="")
91
91
  parser.add_argument('--kb_token', type=str, required=False, default="")
92
92
  parser.add_argument('--no_merge', action='store_true', required=False)
93
+ parser.add_argument('--ui', action='store_true', required=False)
93
94
 
94
95
  args = parser.parse_args()
95
96
 
@@ -121,6 +122,7 @@ def main() -> None:
121
122
  kb_url = args.kb_url
122
123
  kb_token = args.kb_token
123
124
  merge_by_folder = not args.no_merge
125
+ ui_mode = args.ui
124
126
 
125
127
  time_out = args.timeout
126
128
  core = args.cores
@@ -130,7 +132,7 @@ def main() -> None:
130
132
  print_matched_text, formats, time_out, correct_mode, correct_filepath,
131
133
  selected_scanner, path_to_exclude, hide_progress=hide_progress,
132
134
  kb_url=kb_url, kb_token=kb_token,
133
- merge_by_folder=merge_by_folder)
135
+ merge_by_folder=merge_by_folder, ui_mode=ui_mode)
134
136
 
135
137
  _result_log["Scan Result"] = result[1]
136
138
  try:
@@ -149,7 +151,7 @@ def create_report_file(
149
151
  output_path: str = "", output_files: list = [],
150
152
  output_extensions: list = [], correct_mode: bool = True,
151
153
  correct_filepath: str = "", path_to_scan: str = "", path_to_exclude: list = [],
152
- formats: list = [], api_limit_exceed: bool = False, files_count: int = 0, final_output_path: str = "",
154
+ formats: list = [], scanoss_skipped: bool = False, files_count: int = 0, final_output_path: str = "",
153
155
  run_kb_msg: str = "", merge_by_folder: bool = True
154
156
  ) -> 'ScannerItem':
155
157
  """
@@ -216,10 +218,14 @@ def create_report_file(
216
218
  else:
217
219
  scan_item.set_cover_comment("(No OSS detected.)")
218
220
 
219
- if api_limit_exceed:
220
- scan_item.set_cover_comment("SCANOSS skipped (API limits)")
221
+ if scanoss_skipped:
222
+ is_kb_success = run_kb_msg != "" and run_kb_msg.endswith("Completed")
223
+ if is_kb_success:
224
+ scan_item.set_cover_comment("SCANOSS replaced with KB")
225
+ else:
226
+ scan_item.set_cover_comment("SCANOSS skipped")
221
227
 
222
- if run_kb_msg:
228
+ if run_kb_msg and not run_kb_msg.endswith("Completed"):
223
229
  scan_item.set_cover_comment(run_kb_msg)
224
230
  display_mode = selected_scanner
225
231
  if selected_scanner == ALL_MODE:
@@ -390,11 +396,20 @@ def _collect_kb_file_hashes(
390
396
  def merge_results(
391
397
  scancode_result: list = [], scanoss_result: list = [], spdx_downloads: dict = {},
392
398
  path_to_scan: str = "", run_kb: bool = False, manifest_licenses: dict = {},
393
- excluded_files: set = None, hide_progress: bool = False, kb_url: str = "", kb_token: str = ""
399
+ excluded_files: set = None, hide_progress: bool = False, kb_url: str = "", kb_token: str = "",
400
+ ui_mode: bool = False
394
401
  ) -> tuple[list, Optional[str], int, int]:
395
402
 
396
403
  """
397
404
  Merge scanner results and spdx parsing result.
405
+
406
+ ScanOSS items for files already present in scancode_result (same path) do not
407
+ replace the ScanCode license; only OSS name, version, and download location
408
+ from ScanOSS are applied. ScanOSS-only files are appended in full.
409
+
410
+ When ui_mode is False, items with both empty download location and empty
411
+ license are removed after merging.
412
+
398
413
  :param scancode_result: list of scancode results in SourceItem.
399
414
  :param scanoss_result: list of scanoss results in SourceItem.
400
415
  :param spdx_downloads: dictionary of spdx parsed results.
@@ -403,12 +418,27 @@ def merge_results(
403
418
  :param excluded_files: set of relative paths to exclude from KB-only file discovery.
404
419
  :param kb_url: KB API base URL.
405
420
  :param kb_token: KB API bearer token.
421
+ :param ui_mode: if False, drop items with no download location and no license.
406
422
  :return: (merged_result, kb failure message, requested file_hash count, returned match count).
407
423
  """
408
424
  if excluded_files is None:
409
425
  excluded_files = set()
410
426
 
411
- scancode_result.extend([item for item in scanoss_result if item not in scancode_result])
427
+ # Merge ScanOSS into ScanCode results.
428
+ # When ScanCode already detected a license for the same file, keep that license
429
+ # and only load OSS name, version, and download location from ScanOSS.
430
+ # Files found only by ScanOSS are appended as-is (including ScanOSS licenses).
431
+ for scanoss_item in scanoss_result:
432
+ if scanoss_item in scancode_result:
433
+ scancode_item = scancode_result[scancode_result.index(scanoss_item)]
434
+ if scanoss_item.oss_name:
435
+ scancode_item.oss_name = scanoss_item.oss_name
436
+ if scanoss_item.oss_version:
437
+ scancode_item.oss_version = scanoss_item.oss_version
438
+ if scanoss_item.download_location:
439
+ scancode_item.download_location = scanoss_item.download_location
440
+ else:
441
+ scancode_result.append(scanoss_item)
412
442
 
413
443
  # If download loc. in SPDX form found, overwrite the scanner result.
414
444
  # If scanner result doesn't exist, create a new row.
@@ -424,19 +454,16 @@ def merge_results(
424
454
  if manifest_licenses:
425
455
  for file_name, licenses in manifest_licenses.items():
426
456
  valid_licenses = [lic.strip() for lic in licenses if isinstance(lic, str) and lic.strip()]
427
- if not valid_licenses:
457
+ # Non-UI: skip manifests with no extracted licenses.
458
+ # UI: keep/create the row and mark is_manifest_file even without licenses.
459
+ if not valid_licenses and not ui_mode:
428
460
  continue
429
- if file_name in scancode_result:
430
- merged_result_item = scancode_result[scancode_result.index(file_name)]
461
+ item = _get_or_append_source_item(scancode_result, file_name)
462
+ item.is_manifest_file = True
463
+ if valid_licenses:
431
464
  # overwrite existing detected licenses with manifest-provided licenses
432
- merged_result_item.licenses = [] # clear existing licenses (setter clears when value falsy)
433
- merged_result_item.licenses = valid_licenses
434
- merged_result_item.is_manifest_file = True
435
- else:
436
- new_result_item = SourceItem(file_name)
437
- new_result_item.licenses = valid_licenses
438
- new_result_item.is_manifest_file = True
439
- scancode_result.append(new_result_item)
465
+ item.licenses = [] # clear existing licenses (setter clears when value falsy)
466
+ item.licenses = valid_licenses
440
467
 
441
468
  kb_origin_urls: dict[str, str] = {}
442
469
  kb_status_message: Optional[str] = None
@@ -465,9 +492,36 @@ def merge_results(
465
492
  if extra_item.download_location:
466
493
  scancode_result.append(extra_item)
467
494
 
495
+ if not ui_mode:
496
+ scancode_result[:] = [
497
+ item for item in scancode_result
498
+ if not _has_empty_download_and_license(item)
499
+ ]
500
+
468
501
  return scancode_result, kb_status_message, kb_requested_count, kb_returned_count
469
502
 
470
503
 
504
+ def _get_or_append_source_item(scancode_result: list, file_name: str) -> SourceItem:
505
+ if file_name in scancode_result:
506
+ return scancode_result[scancode_result.index(file_name)]
507
+ item = SourceItem(file_name)
508
+ scancode_result.append(item)
509
+ return item
510
+
511
+
512
+ def _has_empty_download_and_license(item: SourceItem) -> bool:
513
+ downloads = item.download_location
514
+ if isinstance(downloads, str):
515
+ has_download = bool(downloads.strip())
516
+ else:
517
+ has_download = bool(downloads) and any(bool(d and str(d).strip()) for d in downloads)
518
+
519
+ licenses = item.licenses
520
+ has_license = bool(licenses) and any(bool(lic and str(lic).strip()) for lic in licenses)
521
+
522
+ return (not has_download) and (not has_license)
523
+
524
+
471
525
  def _finalize_temp_output(
472
526
  temp_output_path: str,
473
527
  final_output_path: str,
@@ -502,7 +556,8 @@ def run_scanners(
502
556
  correct_mode: bool = True, correct_filepath: str = "",
503
557
  selected_scanner: str = ALL_MODE, path_to_exclude: list = [],
504
558
  all_exclude_mode: tuple = (), hide_progress: bool = False,
505
- kb_url: str = "", kb_token: str = "", merge_by_folder: bool = True
559
+ kb_url: str = "", kb_token: str = "", merge_by_folder: bool = True,
560
+ ui_mode: bool = False
506
561
  ) -> Tuple[bool, str, 'ScannerItem', list, list]:
507
562
  """
508
563
  Run Scancode and scanoss.py for the given path.
@@ -532,7 +587,7 @@ def run_scanners(
532
587
  spdx_downloads = {}
533
588
  result_log = {}
534
589
  scan_item = []
535
- api_limit_exceed = False
590
+ scanoss_skipped = False
536
591
  kb_url, kb_token = resolve_kb_config(kb_url, kb_token)
537
592
 
538
593
  success, msg, output_path, output_files, output_extensions, formats = check_output_formats_v2(output_file_name, formats)
@@ -582,12 +637,15 @@ def run_scanners(
582
637
  print_matched_text, formats, called_by_cli, time_out, correct_mode,
583
638
  correct_filepath, path_to_exclude,
584
639
  hide_progress=hide_progress,
640
+ ui_mode=ui_mode,
585
641
  )
586
642
  excluded_files = set(excluded_files) if excluded_files else set()
587
643
  if selected_scanner in ['scanoss', ALL_MODE]:
588
- scanoss_result, api_limit_exceed = run_scanoss_py(path_to_scan, output_path, formats, True, num_cores,
589
- excluded_path_with_default_exclusion, excluded_files,
590
- write_json_file, hide_progress)
644
+ scanoss_result, scanoss_skipped = run_scanoss_py(
645
+ path_to_scan, output_path, formats, True, num_cores,
646
+ excluded_path_with_default_exclusion, excluded_files,
647
+ write_json_file, hide_progress, timeout=time_out
648
+ )
591
649
 
592
650
  run_kb_msg = ""
593
651
  if selected_scanner in SCANNER_TYPE:
@@ -602,6 +660,7 @@ def run_scanners(
602
660
  scancode_result, scanoss_result, spdx_downloads,
603
661
  path_to_scan, run_kb, manifest_licenses, excluded_files,
604
662
  hide_progress, kb_url, kb_token,
663
+ ui_mode=ui_mode,
605
664
  )
606
665
  if kb_status_message:
607
666
  run_kb_msg = f"KB({kb_url}) {kb_status_message}"
@@ -611,7 +670,7 @@ def run_scanners(
611
670
  scan_item = create_report_file(start_time, merged_result, license_list, scanoss_result, selected_scanner,
612
671
  print_matched_text, output_path, output_files, output_extensions, correct_mode,
613
672
  correct_filepath, path_to_scan, excluded_path_without_dot, formats,
614
- api_limit_exceed, cnt_file_except_skipped, final_output_path, run_kb_msg,
673
+ scanoss_skipped, cnt_file_except_skipped, final_output_path, run_kb_msg,
615
674
  merge_by_folder)
616
675
  else:
617
676
  print_help_msg_source_scanner()
@@ -653,7 +712,7 @@ def metadata_collector(path_to_scan: str, excluded_files: set) -> dict:
653
712
 
654
713
  - Traverse files with exclusions applied
655
714
  - spdx_downloads: {rel_path: [download_urls]}
656
- - manifest_licenses: {rel_path: [license_names]}
715
+ - manifest_licenses: {rel_path: [license_names]} (empty list if extraction failed)
657
716
 
658
717
  :return: (spdx_downloads, manifest_licenses)
659
718
  """
@@ -673,9 +732,7 @@ def metadata_collector(path_to_scan: str, excluded_files: set) -> dict:
673
732
  spdx_downloads[rel_path_file] = downloads
674
733
 
675
734
  if is_manifest_file(file_path):
676
- licenses = get_manifest_licenses(file_path)
677
- if licenses:
678
- manifest_licenses[rel_path_file] = licenses
735
+ manifest_licenses[rel_path_file] = get_manifest_licenses(file_path) or []
679
736
 
680
737
  return spdx_downloads, manifest_licenses
681
738
 
@@ -7,6 +7,8 @@ import os
7
7
  import multiprocessing
8
8
  import warnings
9
9
  import logging
10
+ from typing import Tuple
11
+
10
12
  from scancode import cli
11
13
  import fosslight_util.constant as constant
12
14
  from fosslight_util.set_log import init_log
@@ -14,15 +16,12 @@ from fosslight_util.time import current_timestamp_utc, timestamp_for_filename
14
16
  from ._parsing_scancode_file_item import parsing_file_item
15
17
  from ._parsing_scancode_file_item import get_error_from_header
16
18
  from fosslight_util.output_format import check_output_formats_v2
17
- from fosslight_binary.binary_analysis import check_binary
18
19
  from fosslight_util.exclude import (
19
20
  EXCLUDE_DIRECTORY,
20
21
  EXCLUDE_FILE_EXTENSION,
21
22
  EXCLUDE_FILENAME,
22
23
  PACKAGE_DIRECTORY,
23
24
  )
24
- from commoncode.fileset import is_included
25
- from typing import Callable, Tuple
26
25
 
27
26
  logger = logging.getLogger(constant.LOGGER_NAME)
28
27
  warnings.filterwarnings("ignore", category=FutureWarning)
@@ -68,8 +67,6 @@ _WILDCARD_EXTENSIONS = {
68
67
  "zip", "tar", "tgz", "gz",
69
68
  "bmp", "webp", "ico",
70
69
  } | {ext.lower() for ext in EXCLUDE_FILE_EXTENSION}
71
- _SKIP_DIR_NAMES = frozenset(name.lower() for name in PACKAGE_DIRECTORY + EXCLUDE_DIRECTORY)
72
- _SKIP_EXTS = frozenset(ext.lower() for ext in EXCLUDE_FILE_EXTENSION)
73
70
 
74
71
 
75
72
  def _normalize_custom_pattern(pattern: str, abs_path_to_scan: str) -> set:
@@ -160,14 +157,15 @@ def _directory_ignore_pattern(dir_name: str) -> str:
160
157
  return f"**/{normalized}/**"
161
158
 
162
159
 
163
- def _default_scancode_coarse_ignore_patterns(
160
+ def _default_scancode_ignore_patterns(
164
161
  path_to_exclude: list = None,
165
162
  abs_path_to_scan: str = ""
166
- ) -> frozenset:
163
+ ) -> tuple:
167
164
  """
168
165
  Coarse ignore patterns aligned with fosslight_util.get_excluded_paths() rules.
169
166
  Directory names use path-based globs (e.g. **/tests/**) so they do not match
170
167
  the scan root directory name itself.
168
+ Binary files are excluded separately via scancode --ignore-binaries.
171
169
  """
172
170
  patterns = {".*"}
173
171
  for name in PACKAGE_DIRECTORY + EXCLUDE_DIRECTORY:
@@ -182,87 +180,6 @@ def _default_scancode_coarse_ignore_patterns(
182
180
  pattern = os.path.relpath(pattern, abs_path_to_scan)
183
181
  patterns.update(_expand_custom_exclude_pattern(pattern, abs_path_to_scan))
184
182
 
185
- return frozenset(patterns)
186
-
187
-
188
- def _to_excludes_dict(patterns) -> dict:
189
- return {pattern: "exclude" for pattern in patterns}
190
-
191
-
192
- def _is_path_covered(rel_path: str, excludes: dict) -> bool:
193
- return not is_included(rel_path, includes={}, excludes=excludes)
194
-
195
-
196
- def _add_ignore_pattern(
197
- patterns: set,
198
- excludes: dict,
199
- pattern: str,
200
- *,
201
- sample_path: str = None,
202
- ) -> bool:
203
- if pattern in patterns:
204
- return False
205
- if sample_path and _is_path_covered(sample_path, excludes):
206
- return False
207
- patterns.add(pattern)
208
- excludes[pattern] = "exclude"
209
- return True
210
-
211
-
212
- def _make_pre_scan_skip_filter(
213
- coarse_patterns: frozenset,
214
- ) -> Tuple[dict, Callable[[str, str], bool], Callable[[str, str], bool]]:
215
- excludes = _to_excludes_dict(coarse_patterns)
216
-
217
- def should_skip_dir(dir_name: str, rel_dir: str) -> bool:
218
- if dir_name.startswith('.'):
219
- return True
220
- if dir_name.lower() in _SKIP_DIR_NAMES:
221
- return True
222
- return _is_path_covered(f"{rel_dir}/_", excludes)
223
-
224
- def should_skip_file(file_name: str, rel_path: str) -> bool:
225
- if file_name.startswith('.'):
226
- return True
227
- ext = os.path.splitext(file_name)[1].lstrip('.').lower()
228
- if ext in _SKIP_EXTS:
229
- return True
230
- return _is_path_covered(rel_path, excludes)
231
-
232
- return excludes, should_skip_dir, should_skip_file
233
-
234
-
235
- def _add_binary_ignore_patterns(
236
- patterns: set,
237
- excludes: dict,
238
- binary_paths: list,
239
- ) -> None:
240
- extensions = set()
241
- no_ext_paths = []
242
-
243
- for rel_path in binary_paths:
244
- ext = os.path.splitext(rel_path)[1].lstrip('.').lower()
245
- if ext:
246
- extensions.add(ext)
247
- else:
248
- no_ext_paths.append(rel_path)
249
-
250
- for ext in extensions:
251
- _add_ignore_pattern(patterns, excludes, f"*.{ext}")
252
-
253
- for rel_path in no_ext_paths:
254
- _add_ignore_pattern(
255
- patterns, excludes, f"**/{rel_path}", sample_path=rel_path
256
- )
257
-
258
-
259
- def _build_scancode_ignore_patterns(
260
- coarse_patterns: frozenset,
261
- binary_paths: list,
262
- ) -> tuple:
263
- patterns = set(coarse_patterns)
264
- excludes = _to_excludes_dict(coarse_patterns)
265
- _add_binary_ignore_patterns(patterns, excludes, binary_paths)
266
183
  return tuple(sorted(patterns))
267
184
 
268
185
 
@@ -273,7 +190,8 @@ def run_scan(
273
190
  formats: list = [], called_by_cli: bool = False,
274
191
  time_out: int = 120, correct_mode: bool = True,
275
192
  correct_filepath: str = "", path_to_exclude: list = [],
276
- excluded_files: list = [], hide_progress: bool = False
193
+ excluded_files: list = [], hide_progress: bool = False,
194
+ ui_mode: bool = False
277
195
  ) -> Tuple[bool, str, list, list]:
278
196
  if not called_by_cli:
279
197
  global logger
@@ -331,38 +249,9 @@ def run_scan(
331
249
  pretty_params["path_to_exclude"] = path_to_exclude
332
250
  pretty_params["output_file"] = output_file_name
333
251
  abs_path_to_scan = os.path.abspath(path_to_scan)
334
- binary_paths = []
335
- coarse_patterns = _default_scancode_coarse_ignore_patterns(
252
+ ignore_tuple = _default_scancode_ignore_patterns(
336
253
  path_to_exclude, abs_path_to_scan
337
254
  )
338
- _, should_skip_dir, should_skip_file = _make_pre_scan_skip_filter(
339
- coarse_patterns
340
- )
341
-
342
- for root, dirs, files in os.walk(path_to_scan):
343
- rel_root = os.path.relpath(root, abs_path_to_scan).replace("\\", "/")
344
- dirs[:] = [
345
- d for d in dirs
346
- if not should_skip_dir(
347
- d, d if rel_root == "." else f"{rel_root}/{d}"
348
- )
349
- ]
350
- for name in files:
351
- rel_path = name if rel_root == "." else f"{rel_root}/{name}"
352
- if should_skip_file(name, rel_path):
353
- continue
354
- full_path = os.path.join(root, name)
355
- try:
356
- if not check_binary(full_path, True):
357
- continue
358
- except Exception:
359
- continue
360
- binary_paths.append(rel_path)
361
- logger.debug(f"Excluded binary from scancode: {rel_path}")
362
-
363
- ignore_tuple = _build_scancode_ignore_patterns(
364
- coarse_patterns, binary_paths
365
- )
366
255
  logger.debug(f"Scancode ignore patterns: {len(ignore_tuple)}")
367
256
 
368
257
  kwargs = {
@@ -374,12 +263,14 @@ def run_scan(
374
263
  "processes": num_cores,
375
264
  "pretty_params": pretty_params,
376
265
  "output_json_pp": output_json_file,
377
- "only_findings": True,
266
+ # Non-UI: only files with findings (license/copyright). UI: all files.
267
+ "only_findings": not ui_mode,
378
268
  "license_text": True,
379
269
  "url": True,
380
270
  "timeout": time_out,
381
271
  "include": (),
382
272
  "ignore": ignore_tuple,
273
+ "ignore_binaries": True,
383
274
  "quiet": hide_progress
384
275
  }
385
276
  _apply_scancode_unset_workaround(kwargs)
@@ -395,8 +286,9 @@ def run_scan(
395
286
  _result_log["Error_files"] = error_msg
396
287
  msg = "Failed to analyze :" + error_msg
397
288
  if "files" in results:
398
- rc, result_list, parsing_msg, license_list = parsing_file_item(results["files"],
399
- has_error, need_license)
289
+ rc, result_list, parsing_msg, license_list = parsing_file_item(
290
+ results["files"], has_error, need_license, ui_mode=ui_mode
291
+ )
400
292
  if parsing_msg:
401
293
  _result_log["Parsing Log"] = parsing_msg
402
294
  if rc:
@@ -404,12 +296,6 @@ def run_scan(
404
296
  success = True
405
297
  result_list = sorted(
406
298
  result_list, key=lambda row: (''.join(row.licenses)))
407
-
408
- for scan_item in result_list:
409
- if os.path.isdir(scan_item.source_name_or_path):
410
- continue
411
- if check_binary(os.path.join(path_to_scan, scan_item.source_name_or_path), True):
412
- scan_item.exclude = True
413
299
  except Exception as ex:
414
300
  success = False
415
301
  msg = str(ex)
@@ -31,7 +31,8 @@ def get_scanoss_extra_info(scanned_result: dict) -> list:
31
31
  def run_scanoss_py(path_to_scan: str, output_path: str = "", format: list = [],
32
32
  called_by_cli: bool = False, num_threads: int = -1,
33
33
  path_to_exclude: list = [], excluded_files: set = None,
34
- write_json_file: bool = False, hide_progress: bool = False) -> Tuple[list, bool]:
34
+ write_json_file: bool = False, hide_progress: bool = False,
35
+ timeout: int = 120) -> Tuple[list, bool]:
35
36
  """
36
37
  Run scanoss.py for the given path.
37
38
 
@@ -40,23 +41,25 @@ def run_scanoss_py(path_to_scan: str, output_path: str = "", format: list = [],
40
41
  :param format: Output file format (not being used except when calling check_output_format).
41
42
  :param called_by_cli: if not called by cli, initialize logger.
42
43
  :param write_json_file: if requested, keep the raw files.
44
+ :param timeout: timeout in seconds for SCANOSS API request.
43
45
  :return scanoss_file_list: list of ScanItem (scanned result by files).
44
46
  """
45
47
 
46
48
  scanoss_file_list = []
47
- api_limit_exceed = False
49
+ scanoss_skipped = False
48
50
  try:
49
51
  importlib_metadata.distribution("scanoss")
50
52
  except Exception as error:
51
53
  logger.warning(f"{error}. Skipping scan with scanoss.")
52
54
  logger.warning("Please install scanoss and dataclasses before run fosslight_source with scanoss option.")
53
- return scanoss_file_list, api_limit_exceed
55
+ return scanoss_file_list, scanoss_skipped
54
56
 
55
57
  output_json_file = os.path.join(output_path, SCANOSS_OUTPUT_FILE)
56
58
  output_wfp_file = os.path.join(output_path, SCANOSS_RESULT_FILE)
57
59
  if os.path.exists(output_json_file):
58
60
  os.remove(output_json_file)
59
61
 
62
+ output_buffer = io.StringIO()
60
63
  try:
61
64
  logger.debug(f"|---Running SCANOSS on {path_to_scan}")
62
65
  scanoss_settings = ScanossSettings()
@@ -66,15 +69,77 @@ def run_scanoss_py(path_to_scan: str, output_path: str = "", format: list = [],
66
69
  scan_output=output_json_file,
67
70
  scan_options=ScanType.SCAN_SNIPPETS.value,
68
71
  nb_threads=num_threads if num_threads > 0 else 10,
69
- scanoss_settings=scanoss_settings
72
+ scanoss_settings=scanoss_settings,
73
+ timeout=timeout,
74
+ retry=0
70
75
  )
71
- output_buffer = io.StringIO()
76
+
77
+ # Check API connectivity & API Limit using dummy WFP POST
78
+ try:
79
+ logger.debug(f"|---Checking SCANOSS API connectivity to {scanner.scanoss_api.url}")
80
+ dummy_wfp = "file=72214db4e1e543018d1bafe86ea3b444,21,dummy.txt\nfh2=b200cd2eff5d535886e598b3a833aab5\n"
81
+ ping_response = scanner.scanoss_api.session.post(
82
+ scanner.scanoss_api.url,
83
+ files={'file': ('dummy.wfp', dummy_wfp)},
84
+ headers=scanner.scanoss_api.headers,
85
+ timeout=5
86
+ )
87
+ if ping_response.status_code != 200:
88
+ response_text = ping_response.text.lower()
89
+ is_limit = ping_response.status_code in [429, 503]
90
+ is_limit = is_limit or "rate limit" in response_text
91
+ is_limit = is_limit or "limits being exceeded" in response_text
92
+ if is_limit:
93
+ logger.debug(f"[SCANOSS] API Limit Exceeded: HTTP {ping_response.status_code}")
94
+ elif ping_response.status_code in [401, 403]:
95
+ logger.debug(f"[SCANOSS] Authentication Failed: HTTP {ping_response.status_code}")
96
+ else:
97
+ logger.debug(f"[SCANOSS] API is not ready: HTTP {ping_response.status_code}")
98
+ scanoss_skipped = True
99
+ return scanoss_file_list, scanoss_skipped
100
+ except Exception as ping_error:
101
+ logger.debug(f"[SCANOSS] Connection failed to {scanner.scanoss_api.url}: {ping_error}")
102
+ scanoss_skipped = True
103
+ return scanoss_file_list, scanoss_skipped
104
+
72
105
  with contextlib.redirect_stdout(output_buffer), contextlib.redirect_stderr(output_buffer):
73
106
  scanner.scan_folder_with_options(scan_dir=path_to_scan)
74
- captured_output = output_buffer.getvalue()
75
- api_limit_exceed = "due to service limits being exceeded" in captured_output
107
+ except Exception as error:
108
+ logger.debug(f"SCANOSS execution failed: {error}")
76
109
 
77
- if os.path.isfile(output_json_file):
110
+ captured_output = output_buffer.getvalue()
111
+ if captured_output:
112
+ for line in captured_output.splitlines():
113
+ line_strip = line.strip()
114
+ if line_strip.startswith("ERROR:") or "rejected" in line_strip:
115
+ logger.debug(f"[SCANOSS] {line_strip}")
116
+
117
+ api_limit_patterns = [
118
+ "due to service limits being exceeded",
119
+ "service limits/rate limit being exceeded",
120
+ "Rate limit exceeded",
121
+ "HTTP 429"
122
+ ]
123
+ timeout_patterns = [
124
+ "The SCANOSS API request timed out",
125
+ "Service unavailable (HTTP 503)",
126
+ "The SCANOSS API is currently unavailable",
127
+ "ConnectionError communicating with",
128
+ "The SCANOSS API request failed",
129
+ "Connection aborted",
130
+ "RemoteDisconnected"
131
+ ]
132
+ api_limit_exceed = any(p in captured_output for p in api_limit_patterns)
133
+ timeout_occurred = any(p in captured_output for p in timeout_patterns)
134
+ if timeout_occurred or api_limit_exceed:
135
+ scanoss_skipped = True
136
+ if api_limit_exceed:
137
+ logger.debug("SCANOSS skipped (API Limit Exceeded)")
138
+ elif timeout_occurred:
139
+ logger.debug("SCANOSS skipped (Timeout)")
140
+
141
+ if os.path.isfile(output_json_file):
142
+ try:
78
143
  logger.debug("|---SCANOSS Parsing")
79
144
  with open(output_json_file, "r") as st_json:
80
145
  st_python = json.load(st_json)
@@ -86,8 +151,8 @@ def run_scanoss_py(path_to_scan: str, output_path: str = "", format: list = [],
86
151
  with open(output_json_file, "r") as st_json:
87
152
  st_python = json.load(st_json)
88
153
  scanoss_file_list = parsing_scan_result(st_python, excluded_files)
89
- except Exception as error:
90
- logger.debug(f"SCANOSS Parsing {path_to_scan}: {error}")
154
+ except Exception as error:
155
+ logger.debug(f"SCANOSS Parsing {path_to_scan}: {error}")
91
156
 
92
157
  if not write_json_file:
93
158
  if os.path.isfile(output_json_file):
@@ -97,4 +162,4 @@ def run_scanoss_py(path_to_scan: str, output_path: str = "", format: list = [],
97
162
 
98
163
  logger.info(f"|---Number of files detected with SCANOSS: {(len(scanoss_file_list))}")
99
164
 
100
- return scanoss_file_list, api_limit_exceed
165
+ return scanoss_file_list, scanoss_skipped
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fosslight_source
3
- Version: 2.3.4
3
+ Version: 2.3.6
4
4
  Summary: FOSSLight Source Scanner
5
5
  Author: LG Electronics
6
6
  License-Expression: Apache-2.0
@@ -24,7 +24,6 @@ Requires-Dist: fosslight_util>=2.2.2
24
24
  Requires-Dist: PyYAML
25
25
  Requires-Dist: wheel>=0.38.1
26
26
  Requires-Dist: intbitset
27
- Requires-Dist: fosslight_binary>=5.1.22
28
27
  Requires-Dist: scancode-toolkit>=32.0.2
29
28
  Requires-Dist: lxml>=6.0.1
30
29
  Requires-Dist: cryptography<49; platform_system == "Darwin" and platform_machine == "x86_64"
@@ -10,6 +10,7 @@ src/fosslight_source/_merge.py
10
10
  src/fosslight_source/_parsing_scancode_file_item.py
11
11
  src/fosslight_source/_parsing_scanoss_file.py
12
12
  src/fosslight_source/_scan_item.py
13
+ src/fosslight_source/_scancode_ignore_binaries.py
13
14
  src/fosslight_source/cli.py
14
15
  src/fosslight_source/run_manifest_extractor.py
15
16
  src/fosslight_source/run_scancode.py
@@ -0,0 +1,5 @@
1
+ [console_scripts]
2
+ fosslight_source = fosslight_source.cli:main
3
+
4
+ [scancode_pre_scan]
5
+ ignore-binaries = fosslight_source._scancode_ignore_binaries:IgnoreBinaries
@@ -6,7 +6,6 @@ fosslight_util>=2.2.2
6
6
  PyYAML
7
7
  wheel>=0.38.1
8
8
  intbitset
9
- fosslight_binary>=5.1.22
10
9
  scancode-toolkit>=32.0.2
11
10
  lxml>=6.0.1
12
11
  fingerprints==1.2.3
@@ -1,2 +0,0 @@
1
- [console_scripts]
2
- fosslight_source = fosslight_source.cli:main