fosslight-source 2.3.3__tar.gz → 2.3.5__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.3/src/fosslight_source.egg-info → fosslight_source-2.3.5}/PKG-INFO +2 -2
  2. {fosslight_source-2.3.3 → fosslight_source-2.3.5}/pyproject.toml +5 -2
  3. {fosslight_source-2.3.3 → fosslight_source-2.3.5}/src/fosslight_source/_help.py +1 -1
  4. {fosslight_source-2.3.3 → fosslight_source-2.3.5}/src/fosslight_source/_parsing_scancode_file_item.py +22 -8
  5. fosslight_source-2.3.5/src/fosslight_source/_scancode_ignore_binaries.py +69 -0
  6. {fosslight_source-2.3.3 → fosslight_source-2.3.5}/src/fosslight_source/cli.py +89 -35
  7. {fosslight_source-2.3.3 → fosslight_source-2.3.5}/src/fosslight_source/run_scancode.py +104 -85
  8. {fosslight_source-2.3.3 → fosslight_source-2.3.5}/src/fosslight_source/run_scanoss.py +15 -5
  9. {fosslight_source-2.3.3 → fosslight_source-2.3.5/src/fosslight_source.egg-info}/PKG-INFO +2 -2
  10. {fosslight_source-2.3.3 → fosslight_source-2.3.5}/src/fosslight_source.egg-info/SOURCES.txt +1 -0
  11. fosslight_source-2.3.5/src/fosslight_source.egg-info/entry_points.txt +5 -0
  12. {fosslight_source-2.3.3 → fosslight_source-2.3.5}/src/fosslight_source.egg-info/requires.txt +1 -1
  13. fosslight_source-2.3.3/src/fosslight_source.egg-info/entry_points.txt +0 -2
  14. {fosslight_source-2.3.3 → fosslight_source-2.3.5}/LICENSE +0 -0
  15. {fosslight_source-2.3.3 → fosslight_source-2.3.5}/MANIFEST.in +0 -0
  16. {fosslight_source-2.3.3 → fosslight_source-2.3.5}/README.md +0 -0
  17. {fosslight_source-2.3.3 → fosslight_source-2.3.5}/setup.cfg +0 -0
  18. {fosslight_source-2.3.3 → fosslight_source-2.3.5}/src/fosslight_source/__init__.py +0 -0
  19. {fosslight_source-2.3.3 → fosslight_source-2.3.5}/src/fosslight_source/_kb_client.py +0 -0
  20. {fosslight_source-2.3.3 → fosslight_source-2.3.5}/src/fosslight_source/_license_matched.py +0 -0
  21. {fosslight_source-2.3.3 → fosslight_source-2.3.5}/src/fosslight_source/_merge.py +0 -0
  22. {fosslight_source-2.3.3 → fosslight_source-2.3.5}/src/fosslight_source/_parsing_scanoss_file.py +0 -0
  23. {fosslight_source-2.3.3 → fosslight_source-2.3.5}/src/fosslight_source/_scan_item.py +0 -0
  24. {fosslight_source-2.3.3 → fosslight_source-2.3.5}/src/fosslight_source/run_manifest_extractor.py +0 -0
  25. {fosslight_source-2.3.3 → fosslight_source-2.3.5}/src/fosslight_source/run_spdx_extractor.py +0 -0
  26. {fosslight_source-2.3.3 → fosslight_source-2.3.5}/src/fosslight_source.egg-info/dependency_links.txt +0 -0
  27. {fosslight_source-2.3.3 → fosslight_source-2.3.5}/src/fosslight_source.egg-info/top_level.txt +0 -0
  28. {fosslight_source-2.3.3 → fosslight_source-2.3.5}/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.3
3
+ Version: 2.3.5
4
4
  Summary: FOSSLight Source Scanner
5
5
  Author: LG Electronics
6
6
  License-Expression: Apache-2.0
@@ -24,8 +24,8 @@ 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
28
+ Requires-Dist: lxml>=6.0.1
29
29
  Requires-Dist: cryptography<49; platform_system == "Darwin" and platform_machine == "x86_64"
30
30
  Requires-Dist: fingerprints==1.2.3
31
31
  Requires-Dist: normality==2.6.1
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
7
7
 
8
8
  [project]
9
9
  name = "fosslight_source"
10
- version = "2.3.3"
10
+ version = "2.3.5"
11
11
  description = "FOSSLight Source Scanner"
12
12
  readme = "README.md"
13
13
  license = "Apache-2.0"
@@ -33,8 +33,8 @@ 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",
37
+ "lxml>=6.0.1",
38
38
  # cryptography 49.x does not provide macOS x86_64 wheels, causing source builds to require OpenSSL/pkg-config.
39
39
  "cryptography<49; platform_system == 'Darwin' and platform_machine == 'x86_64'",
40
40
  "fingerprints==1.2.3",
@@ -55,6 +55,9 @@ Download = "https://github.com/fosslight/fosslight_source_scanner"
55
55
  [project.scripts]
56
56
  fosslight_source = "fosslight_source.cli:main"
57
57
 
58
+ [project.entry-points."scancode_pre_scan"]
59
+ ignore-binaries = "fosslight_source._scancode_ignore_binaries:IgnoreBinaries"
60
+
58
61
  [tool.setuptools]
59
62
  package-dir = {"" = "src"}
60
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,8 +218,12 @@ 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 "Completed" in run_kb_msg
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
228
  if run_kb_msg:
223
229
  scan_item.set_cover_comment(run_kb_msg)
@@ -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)
@@ -572,7 +627,7 @@ def run_scanners(
572
627
  excluded_path_without_dot,
573
628
  excluded_files,
574
629
  cnt_file_except_skipped) = get_excluded_paths(path_to_scan, path_to_exclude_with_filename)
575
- logger.debug(f"Skipped paths: {excluded_path_with_default_exclusion}")
630
+ logger.debug(f"Skipped paths count: {len(excluded_path_with_default_exclusion)}")
576
631
 
577
632
  if not selected_scanner:
578
633
  selected_scanner = ALL_MODE
@@ -580,14 +635,17 @@ def run_scanners(
580
635
  success, result_log[RESULT_KEY], scancode_result, license_list = run_scan(
581
636
  path_to_scan, output_file_name, write_json_file, num_cores, True,
582
637
  print_matched_text, formats, called_by_cli, time_out, correct_mode,
583
- correct_filepath, excluded_path_with_default_exclusion,
584
- excluded_files, hide_progress,
638
+ correct_filepath, path_to_exclude,
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:
@@ -598,23 +656,21 @@ def run_scanners(
598
656
  run_kb_msg = f"KB({kb_url}) Unreachable"
599
657
 
600
658
  spdx_downloads, manifest_licenses = metadata_collector(path_to_scan, excluded_files)
601
- merged_result, kb_status_message, kb_requested_count, kb_returned_count = merge_results(
659
+ merged_result, kb_status_message, kb_requested_count, _ = merge_results(
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}"
608
667
  elif run_kb and kb_requested_count > 0:
609
- run_kb_msg = (
610
- f"KB({kb_url}) response : {kb_returned_count}/"
611
- f" requested: {kb_requested_count}"
612
- )
668
+ run_kb_msg = f"KB({kb_url}) : Completed"
613
669
  mark_oss_info_correction_files_as_excluded(merged_result)
614
670
  scan_item = create_report_file(start_time, merged_result, license_list, scanoss_result, selected_scanner,
615
671
  print_matched_text, output_path, output_files, output_extensions, correct_mode,
616
672
  correct_filepath, path_to_scan, excluded_path_without_dot, formats,
617
- 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,
618
674
  merge_by_folder)
619
675
  else:
620
676
  print_help_msg_source_scanner()
@@ -656,7 +712,7 @@ def metadata_collector(path_to_scan: str, excluded_files: set) -> dict:
656
712
 
657
713
  - Traverse files with exclusions applied
658
714
  - spdx_downloads: {rel_path: [download_urls]}
659
- - manifest_licenses: {rel_path: [license_names]}
715
+ - manifest_licenses: {rel_path: [license_names]} (empty list if extraction failed)
660
716
 
661
717
  :return: (spdx_downloads, manifest_licenses)
662
718
  """
@@ -676,9 +732,7 @@ def metadata_collector(path_to_scan: str, excluded_files: set) -> dict:
676
732
  spdx_downloads[rel_path_file] = downloads
677
733
 
678
734
  if is_manifest_file(file_path):
679
- licenses = get_manifest_licenses(file_path)
680
- if licenses:
681
- manifest_licenses[rel_path_file] = licenses
735
+ manifest_licenses[rel_path_file] = get_manifest_licenses(file_path) or []
682
736
 
683
737
  return spdx_downloads, manifest_licenses
684
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 Tuple, Iterable
26
25
 
27
26
  logger = logging.getLogger(constant.LOGGER_NAME)
28
27
  warnings.filterwarnings("ignore", category=FutureWarning)
@@ -62,42 +61,53 @@ def _apply_scancode_unset_workaround(kwargs: dict) -> None:
62
61
  logger.debug("scancode UNSET workaround skipped: %s", ex)
63
62
 
64
63
 
65
- def _directory_ignore_pattern(dir_name: str) -> str:
66
- """Path-based glob for a directory name (avoids matching the scan root itself)."""
67
- normalized = dir_name.strip().strip("/").replace("\\", "/")
68
- if not normalized:
69
- return dir_name
70
- return f"**/{normalized}/**"
71
-
72
-
73
- def _default_scancode_coarse_ignore_patterns() -> frozenset:
74
- """
75
- Coarse ignore patterns aligned with fosslight_util.get_excluded_paths() rules.
76
- Directory names use path-based globs (e.g. **/tests/**) so they do not match
77
- the scan root directory name itself.
78
- """
79
- patterns = {".*"}
80
- for name in PACKAGE_DIRECTORY + EXCLUDE_DIRECTORY:
81
- patterns.add(_directory_ignore_pattern(name))
82
- for ext in EXCLUDE_FILE_EXTENSION:
83
- patterns.add(f"*.{ext}")
84
- for name in EXCLUDE_FILENAME:
85
- patterns.add(name)
86
- return frozenset(patterns)
64
+ _WILDCARD_EXTENSIONS = {
65
+ "png", "mp3", "wav", "comp", "bin", "o", "db", "tflite",
66
+ "ttf", "exe", "dll", "jpg", "jpeg", "gif",
67
+ "zip", "tar", "tgz", "gz",
68
+ "bmp", "webp", "ico",
69
+ } | {ext.lower() for ext in EXCLUDE_FILE_EXTENSION}
70
+
71
+
72
+ def _normalize_custom_pattern(pattern: str, abs_path_to_scan: str) -> set:
73
+ pat = pattern.replace('\\', '/').strip()
74
+ if not pat:
75
+ return set()
76
+
77
+ patterns_to_add = {pat}
78
+
79
+ if pat.endswith("/**"):
80
+ base = pat[:-3].rstrip("/")
81
+ if base:
82
+ patterns_to_add.add(base)
83
+ elif pat.endswith("/*"):
84
+ base = pat[:-2].rstrip("/")
85
+ if base:
86
+ patterns_to_add.add(base)
87
+ patterns_to_add.add(f"{base}/**")
88
+ elif pat.endswith("/"):
89
+ base = pat.rstrip("/")
90
+ if base:
91
+ patterns_to_add.add(base)
92
+ patterns_to_add.add(f"{base}/**")
93
+ patterns_to_add.add(f"{base}/*")
94
+ else:
95
+ full_path = os.path.join(abs_path_to_scan, pat)
96
+ if os.path.isdir(full_path):
97
+ patterns_to_add.add(f"{pat}/**")
98
+ patterns_to_add.add(f"{pat}/*")
87
99
 
100
+ return patterns_to_add
88
101
 
89
- def _is_covered_by_coarse_ignore(rel_path: str, coarse_patterns: Iterable[str]) -> bool:
90
- excludes = {pattern: "" for pattern in coarse_patterns}
91
- return not is_included(rel_path, includes={}, excludes=excludes)
92
102
 
103
+ def _expand_custom_exclude_pattern(pattern: str, abs_path_to_scan: str) -> set:
104
+ exclude_path_normalized = os.path.normpath(
105
+ pattern.replace('\\', '/').strip()
106
+ ).replace("\\", "/")
107
+ if not exclude_path_normalized:
108
+ return set()
93
109
 
94
- def _add_path_to_exclude_pattern(
95
- patterns: set,
96
- exclude_path: str,
97
- abs_path_to_scan: str,
98
- coarse_patterns: frozenset,
99
- ) -> None:
100
- exclude_path_normalized = os.path.normpath(exclude_path).replace("\\", "/")
110
+ patterns = set(_normalize_custom_pattern(exclude_path_normalized, abs_path_to_scan))
101
111
 
102
112
  if exclude_path_normalized.endswith("/**"):
103
113
  base_dir = exclude_path_normalized[:-3].rstrip("/")
@@ -105,17 +115,15 @@ def _add_path_to_exclude_pattern(
105
115
  full_exclude_path = os.path.join(abs_path_to_scan, base_dir)
106
116
  if os.path.isdir(full_exclude_path):
107
117
  patterns.add(base_dir)
108
- patterns.add(exclude_path_normalized)
109
- else:
110
- patterns.add(exclude_path_normalized)
118
+ patterns.add(exclude_path_normalized)
111
119
  else:
112
120
  patterns.add(exclude_path_normalized)
113
- return
121
+ return patterns
114
122
 
115
123
  has_glob_chars = any(char in exclude_path_normalized for char in ['*', '?', '['])
116
124
  if has_glob_chars:
117
125
  patterns.add(exclude_path_normalized)
118
- return
126
+ return patterns
119
127
 
120
128
  full_exclude_path = os.path.join(abs_path_to_scan, exclude_path_normalized)
121
129
  if os.path.isdir(full_exclude_path):
@@ -126,29 +134,51 @@ def _add_path_to_exclude_pattern(
126
134
  else:
127
135
  patterns.add(exclude_path_normalized)
128
136
  elif os.path.isfile(full_exclude_path):
129
- if not _is_covered_by_coarse_ignore(exclude_path_normalized, coarse_patterns):
137
+ ext = os.path.splitext(exclude_path_normalized)[1].lstrip('.').lower()
138
+ if ext in _WILDCARD_EXTENSIONS:
139
+ patterns.add(f"*.{ext}")
140
+ else:
130
141
  patterns.add(f"**/{exclude_path_normalized}")
131
142
  else:
132
- patterns.add(exclude_path_normalized)
143
+ ext = os.path.splitext(exclude_path_normalized)[1].lstrip('.').lower()
144
+ if ext in _WILDCARD_EXTENSIONS:
145
+ patterns.add(f"*.{ext}")
146
+ else:
147
+ patterns.add(exclude_path_normalized)
133
148
 
149
+ return patterns
134
150
 
135
- def _build_scancode_ignore_patterns(
136
- path_to_exclude: list,
137
- abs_path_to_scan: str,
138
- binary_paths: list,
139
- ) -> tuple:
140
- coarse_patterns = _default_scancode_coarse_ignore_patterns()
141
- patterns = set(coarse_patterns)
142
151
 
143
- for path in path_to_exclude or []:
144
- if os.path.isabs(path):
145
- exclude_path = os.path.relpath(path, abs_path_to_scan)
146
- else:
147
- exclude_path = path
148
- _add_path_to_exclude_pattern(patterns, exclude_path, abs_path_to_scan, coarse_patterns)
152
+ def _directory_ignore_pattern(dir_name: str) -> str:
153
+ """Path-based glob for a directory name (avoids matching the scan root itself)."""
154
+ normalized = dir_name.strip().strip("/").replace("\\", "/")
155
+ if not normalized:
156
+ return dir_name
157
+ return f"**/{normalized}/**"
149
158
 
150
- for rel_path in binary_paths:
151
- patterns.add(f"**/{rel_path}")
159
+
160
+ def _default_scancode_ignore_patterns(
161
+ path_to_exclude: list = None,
162
+ abs_path_to_scan: str = ""
163
+ ) -> tuple:
164
+ """
165
+ Coarse ignore patterns aligned with fosslight_util.get_excluded_paths() rules.
166
+ Directory names use path-based globs (e.g. **/tests/**) so they do not match
167
+ the scan root directory name itself.
168
+ Binary files are excluded separately via scancode --ignore-binaries.
169
+ """
170
+ patterns = {".*"}
171
+ for name in PACKAGE_DIRECTORY + EXCLUDE_DIRECTORY:
172
+ patterns.add(_directory_ignore_pattern(name))
173
+ for ext in EXCLUDE_FILE_EXTENSION:
174
+ patterns.add(f"*.{ext}")
175
+ for name in EXCLUDE_FILENAME:
176
+ patterns.add(name)
177
+
178
+ for pattern in path_to_exclude or []:
179
+ if os.path.isabs(pattern):
180
+ pattern = os.path.relpath(pattern, abs_path_to_scan)
181
+ patterns.update(_expand_custom_exclude_pattern(pattern, abs_path_to_scan))
152
182
 
153
183
  return tuple(sorted(patterns))
154
184
 
@@ -160,7 +190,8 @@ def run_scan(
160
190
  formats: list = [], called_by_cli: bool = False,
161
191
  time_out: int = 120, correct_mode: bool = True,
162
192
  correct_filepath: str = "", path_to_exclude: list = [],
163
- excluded_files: list = [], hide_progress: bool = False
193
+ excluded_files: list = [], hide_progress: bool = False,
194
+ ui_mode: bool = False
164
195
  ) -> Tuple[bool, str, list, list]:
165
196
  if not called_by_cli:
166
197
  global logger
@@ -198,8 +229,13 @@ def run_scan(
198
229
  output_json_file = ""
199
230
 
200
231
  if not called_by_cli:
201
- logger, _result_log = init_log(os.path.join(output_path, f"fosslight_log_src_{_file_time}.txt"),
202
- True, logging.INFO, logging.DEBUG, _PKG_NAME, path_to_scan, path_to_exclude)
232
+ log_file_path = os.path.join(
233
+ output_path, f"fosslight_log_src_{_file_time}.txt"
234
+ )
235
+ logger, _result_log = init_log(
236
+ log_file_path, True, logging.INFO, logging.DEBUG,
237
+ _PKG_NAME, path_to_scan, path_to_exclude
238
+ )
203
239
 
204
240
  logger.info(f"Tool Info : {_result_log['Tool Info']}")
205
241
 
@@ -213,22 +249,8 @@ def run_scan(
213
249
  pretty_params["path_to_exclude"] = path_to_exclude
214
250
  pretty_params["output_file"] = output_file_name
215
251
  abs_path_to_scan = os.path.abspath(path_to_scan)
216
- binary_paths = []
217
- for root, _, files in os.walk(path_to_scan):
218
- for name in files:
219
- full_path = os.path.join(root, name)
220
- try:
221
- if not check_binary(full_path, True):
222
- continue
223
- except Exception:
224
- continue
225
- rel_path = os.path.relpath(full_path, abs_path_to_scan)
226
- rel_norm = os.path.normpath(rel_path).replace("\\", "/")
227
- binary_paths.append(rel_norm)
228
- logger.debug(f"Excluded binary from scancode: {rel_norm}")
229
-
230
- ignore_tuple = _build_scancode_ignore_patterns(
231
- path_to_exclude, abs_path_to_scan, binary_paths
252
+ ignore_tuple = _default_scancode_ignore_patterns(
253
+ path_to_exclude, abs_path_to_scan
232
254
  )
233
255
  logger.debug(f"Scancode ignore patterns: {len(ignore_tuple)}")
234
256
 
@@ -241,12 +263,14 @@ def run_scan(
241
263
  "processes": num_cores,
242
264
  "pretty_params": pretty_params,
243
265
  "output_json_pp": output_json_file,
244
- "only_findings": True,
266
+ # Non-UI: only files with findings (license/copyright). UI: all files.
267
+ "only_findings": not ui_mode,
245
268
  "license_text": True,
246
269
  "url": True,
247
270
  "timeout": time_out,
248
271
  "include": (),
249
272
  "ignore": ignore_tuple,
273
+ "ignore_binaries": True,
250
274
  "quiet": hide_progress
251
275
  }
252
276
  _apply_scancode_unset_workaround(kwargs)
@@ -262,8 +286,9 @@ def run_scan(
262
286
  _result_log["Error_files"] = error_msg
263
287
  msg = "Failed to analyze :" + error_msg
264
288
  if "files" in results:
265
- rc, result_list, parsing_msg, license_list = parsing_file_item(results["files"],
266
- 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
+ )
267
292
  if parsing_msg:
268
293
  _result_log["Parsing Log"] = parsing_msg
269
294
  if rc:
@@ -271,12 +296,6 @@ def run_scan(
271
296
  success = True
272
297
  result_list = sorted(
273
298
  result_list, key=lambda row: (''.join(row.licenses)))
274
-
275
- for scan_item in result_list:
276
- if os.path.isdir(scan_item.source_name_or_path):
277
- continue
278
- if check_binary(os.path.join(path_to_scan, scan_item.source_name_or_path), True):
279
- scan_item.exclude = True
280
299
  except Exception as ex:
281
300
  success = False
282
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,17 +41,18 @@ 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)
@@ -66,13 +68,21 @@ def run_scanoss_py(path_to_scan: str, output_path: str = "", format: list = [],
66
68
  scan_output=output_json_file,
67
69
  scan_options=ScanType.SCAN_SNIPPETS.value,
68
70
  nb_threads=num_threads if num_threads > 0 else 10,
69
- scanoss_settings=scanoss_settings
71
+ scanoss_settings=scanoss_settings,
72
+ timeout=timeout
70
73
  )
71
74
  output_buffer = io.StringIO()
72
75
  with contextlib.redirect_stdout(output_buffer), contextlib.redirect_stderr(output_buffer):
73
76
  scanner.scan_folder_with_options(scan_dir=path_to_scan)
74
77
  captured_output = output_buffer.getvalue()
75
78
  api_limit_exceed = "due to service limits being exceeded" in captured_output
79
+ timeout_occurred = "The SCANOSS API request timed out" in captured_output
80
+ if timeout_occurred or api_limit_exceed:
81
+ scanoss_skipped = True
82
+ if timeout_occurred:
83
+ logger.debug("SCANOSS skipped (Timeout)")
84
+ elif api_limit_exceed:
85
+ logger.debug("SCANOSS skipped (API Limit Exceeded)")
76
86
 
77
87
  if os.path.isfile(output_json_file):
78
88
  logger.debug("|---SCANOSS Parsing")
@@ -97,4 +107,4 @@ def run_scanoss_py(path_to_scan: str, output_path: str = "", format: list = [],
97
107
 
98
108
  logger.info(f"|---Number of files detected with SCANOSS: {(len(scanoss_file_list))}")
99
109
 
100
- return scanoss_file_list, api_limit_exceed
110
+ 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.3
3
+ Version: 2.3.5
4
4
  Summary: FOSSLight Source Scanner
5
5
  Author: LG Electronics
6
6
  License-Expression: Apache-2.0
@@ -24,8 +24,8 @@ 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
28
+ Requires-Dist: lxml>=6.0.1
29
29
  Requires-Dist: cryptography<49; platform_system == "Darwin" and platform_machine == "x86_64"
30
30
  Requires-Dist: fingerprints==1.2.3
31
31
  Requires-Dist: normality==2.6.1
@@ -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,8 +6,8 @@ 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
10
+ lxml>=6.0.1
11
11
  fingerprints==1.2.3
12
12
  normality==2.6.1
13
13
  tqdm
@@ -1,2 +0,0 @@
1
- [console_scripts]
2
- fosslight_source = fosslight_source.cli:main