fosslight-source 2.3.5__tar.gz → 2.3.7__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.5/src/fosslight_source.egg-info → fosslight_source-2.3.7}/PKG-INFO +1 -1
  2. {fosslight_source-2.3.5 → fosslight_source-2.3.7}/pyproject.toml +1 -1
  3. {fosslight_source-2.3.5 → fosslight_source-2.3.7}/src/fosslight_source/_scan_item.py +1 -0
  4. {fosslight_source-2.3.5 → fosslight_source-2.3.7}/src/fosslight_source/cli.py +2 -2
  5. {fosslight_source-2.3.5 → fosslight_source-2.3.7}/src/fosslight_source/run_manifest_extractor.py +43 -0
  6. {fosslight_source-2.3.5 → fosslight_source-2.3.7}/src/fosslight_source/run_scanoss.py +66 -11
  7. {fosslight_source-2.3.5 → fosslight_source-2.3.7/src/fosslight_source.egg-info}/PKG-INFO +1 -1
  8. {fosslight_source-2.3.5 → fosslight_source-2.3.7}/src/fosslight_source.egg-info/SOURCES.txt +1 -0
  9. fosslight_source-2.3.7/tests/test_manifest_composer.py +53 -0
  10. {fosslight_source-2.3.5 → fosslight_source-2.3.7}/LICENSE +0 -0
  11. {fosslight_source-2.3.5 → fosslight_source-2.3.7}/MANIFEST.in +0 -0
  12. {fosslight_source-2.3.5 → fosslight_source-2.3.7}/README.md +0 -0
  13. {fosslight_source-2.3.5 → fosslight_source-2.3.7}/setup.cfg +0 -0
  14. {fosslight_source-2.3.5 → fosslight_source-2.3.7}/src/fosslight_source/__init__.py +0 -0
  15. {fosslight_source-2.3.5 → fosslight_source-2.3.7}/src/fosslight_source/_help.py +0 -0
  16. {fosslight_source-2.3.5 → fosslight_source-2.3.7}/src/fosslight_source/_kb_client.py +0 -0
  17. {fosslight_source-2.3.5 → fosslight_source-2.3.7}/src/fosslight_source/_license_matched.py +0 -0
  18. {fosslight_source-2.3.5 → fosslight_source-2.3.7}/src/fosslight_source/_merge.py +0 -0
  19. {fosslight_source-2.3.5 → fosslight_source-2.3.7}/src/fosslight_source/_parsing_scancode_file_item.py +0 -0
  20. {fosslight_source-2.3.5 → fosslight_source-2.3.7}/src/fosslight_source/_parsing_scanoss_file.py +0 -0
  21. {fosslight_source-2.3.5 → fosslight_source-2.3.7}/src/fosslight_source/_scancode_ignore_binaries.py +0 -0
  22. {fosslight_source-2.3.5 → fosslight_source-2.3.7}/src/fosslight_source/run_scancode.py +0 -0
  23. {fosslight_source-2.3.5 → fosslight_source-2.3.7}/src/fosslight_source/run_spdx_extractor.py +0 -0
  24. {fosslight_source-2.3.5 → fosslight_source-2.3.7}/src/fosslight_source.egg-info/dependency_links.txt +0 -0
  25. {fosslight_source-2.3.5 → fosslight_source-2.3.7}/src/fosslight_source.egg-info/entry_points.txt +0 -0
  26. {fosslight_source-2.3.5 → fosslight_source-2.3.7}/src/fosslight_source.egg-info/requires.txt +0 -0
  27. {fosslight_source-2.3.5 → fosslight_source-2.3.7}/src/fosslight_source.egg-info/top_level.txt +0 -0
  28. {fosslight_source-2.3.5 → fosslight_source-2.3.7}/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.5
3
+ Version: 2.3.7
4
4
  Summary: FOSSLight Source Scanner
5
5
  Author: LG Electronics
6
6
  License-Expression: Apache-2.0
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
7
7
 
8
8
  [project]
9
9
  name = "fosslight_source"
10
- version = "2.3.5"
10
+ version = "2.3.7"
11
11
  description = "FOSSLight Source Scanner"
12
12
  readme = "README.md"
13
13
  license = "Apache-2.0"
@@ -18,6 +18,7 @@ _notice_filename = ['licen[cs]e[s]?', 'notice[s]?', 'legal', 'copyright[s]?', 'c
18
18
  _manifest_filename = [
19
19
  r'.*\.pom$',
20
20
  r'package\.json$',
21
+ r'composer\.json$',
21
22
  r'setup\.py$',
22
23
  r'setup\.cfg$',
23
24
  r'pyproject\.toml$',
@@ -219,13 +219,13 @@ def create_report_file(
219
219
  scan_item.set_cover_comment("(No OSS detected.)")
220
220
 
221
221
  if scanoss_skipped:
222
- is_kb_success = run_kb_msg != "" and "Completed" in run_kb_msg
222
+ is_kb_success = run_kb_msg != "" and run_kb_msg.endswith("Completed")
223
223
  if is_kb_success:
224
224
  scan_item.set_cover_comment("SCANOSS replaced with KB")
225
225
  else:
226
226
  scan_item.set_cover_comment("SCANOSS skipped")
227
227
 
228
- if run_kb_msg:
228
+ if run_kb_msg and not run_kb_msg.endswith("Completed"):
229
229
  scan_item.set_cover_comment(run_kb_msg)
230
230
  display_mode = selected_scanner
231
231
  if selected_scanner == ALL_MODE:
@@ -74,6 +74,43 @@ def get_licenses_from_package_json(file_path: str) -> list[str]:
74
74
  return unique
75
75
 
76
76
 
77
+ def get_licenses_from_composer_json(file_path: str) -> list[str]:
78
+ """Extract licenses from Composer ``composer.json`` ``license`` field.
79
+
80
+ Composer schema allows a string or an array of strings
81
+ (SPDX identifiers / expressions).
82
+ """
83
+ try:
84
+ with open(file_path, 'r', encoding='utf-8') as f:
85
+ data = json.load(f)
86
+ except Exception as ex:
87
+ logger.info(f"Failed to read composer.json {file_path}: {ex}")
88
+ return []
89
+
90
+ if not isinstance(data, dict):
91
+ return []
92
+
93
+ licenses: list[str] = []
94
+ license_field = data.get('license')
95
+
96
+ def append_license_value(value) -> None:
97
+ if isinstance(value, str):
98
+ token = value.strip()
99
+ if token:
100
+ licenses.extend(_split_spdx_expression(token))
101
+ elif isinstance(value, list):
102
+ for item in value:
103
+ append_license_value(item)
104
+
105
+ append_license_value(license_field)
106
+
107
+ unique: list[str] = []
108
+ for lic in licenses:
109
+ if lic not in unique:
110
+ unique.append(lic)
111
+ return unique
112
+
113
+
77
114
  def get_licenses_from_setup_cfg(file_path: str) -> list[str]:
78
115
  try:
79
116
  import configparser
@@ -323,6 +360,12 @@ def get_manifest_licenses(file_path: str) -> list[str]:
323
360
  except Exception as ex:
324
361
  logger.info(f"Failed to extract license from package.json {file_path}: {ex}")
325
362
  return []
363
+ elif os.path.basename(file_path).lower() == 'composer.json':
364
+ try:
365
+ return get_licenses_from_composer_json(file_path)
366
+ except Exception as ex:
367
+ logger.info(f"Failed to extract license from composer.json {file_path}: {ex}")
368
+ return []
326
369
  elif os.path.basename(file_path).lower() == 'setup.cfg':
327
370
  try:
328
371
  return get_licenses_from_setup_cfg(file_path)
@@ -59,6 +59,7 @@ def run_scanoss_py(path_to_scan: str, output_path: str = "", format: list = [],
59
59
  if os.path.exists(output_json_file):
60
60
  os.remove(output_json_file)
61
61
 
62
+ output_buffer = io.StringIO()
62
63
  try:
63
64
  logger.debug(f"|---Running SCANOSS on {path_to_scan}")
64
65
  scanoss_settings = ScanossSettings()
@@ -69,22 +70,76 @@ def run_scanoss_py(path_to_scan: str, output_path: str = "", format: list = [],
69
70
  scan_options=ScanType.SCAN_SNIPPETS.value,
70
71
  nb_threads=num_threads if num_threads > 0 else 10,
71
72
  scanoss_settings=scanoss_settings,
72
- timeout=timeout
73
+ timeout=timeout,
74
+ retry=0
73
75
  )
74
- 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
+
75
105
  with contextlib.redirect_stdout(output_buffer), contextlib.redirect_stderr(output_buffer):
76
106
  scanner.scan_folder_with_options(scan_dir=path_to_scan)
77
- captured_output = output_buffer.getvalue()
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
107
+ except Exception as error:
108
+ logger.debug(f"SCANOSS execution failed: {error}")
109
+
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)
80
134
  if timeout_occurred or api_limit_exceed:
81
135
  scanoss_skipped = True
82
- if timeout_occurred:
83
- logger.debug("SCANOSS skipped (Timeout)")
84
- elif api_limit_exceed:
136
+ if api_limit_exceed:
85
137
  logger.debug("SCANOSS skipped (API Limit Exceeded)")
138
+ elif timeout_occurred:
139
+ logger.debug("SCANOSS skipped (Timeout)")
86
140
 
87
- if os.path.isfile(output_json_file):
141
+ if os.path.isfile(output_json_file):
142
+ try:
88
143
  logger.debug("|---SCANOSS Parsing")
89
144
  with open(output_json_file, "r") as st_json:
90
145
  st_python = json.load(st_json)
@@ -96,8 +151,8 @@ def run_scanoss_py(path_to_scan: str, output_path: str = "", format: list = [],
96
151
  with open(output_json_file, "r") as st_json:
97
152
  st_python = json.load(st_json)
98
153
  scanoss_file_list = parsing_scan_result(st_python, excluded_files)
99
- except Exception as error:
100
- logger.debug(f"SCANOSS Parsing {path_to_scan}: {error}")
154
+ except Exception as error:
155
+ logger.debug(f"SCANOSS Parsing {path_to_scan}: {error}")
101
156
 
102
157
  if not write_json_file:
103
158
  if os.path.isfile(output_json_file):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fosslight_source
3
- Version: 2.3.5
3
+ Version: 2.3.7
4
4
  Summary: FOSSLight Source Scanner
5
5
  Author: LG Electronics
6
6
  License-Expression: Apache-2.0
@@ -22,4 +22,5 @@ src/fosslight_source.egg-info/dependency_links.txt
22
22
  src/fosslight_source.egg-info/entry_points.txt
23
23
  src/fosslight_source.egg-info/requires.txt
24
24
  src/fosslight_source.egg-info/top_level.txt
25
+ tests/test_manifest_composer.py
25
26
  tests/test_tox.py
@@ -0,0 +1,53 @@
1
+ # Copyright (c) 2026 LG Electronics Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Tests for composer.json manifest license extraction."""
4
+
5
+ from fosslight_source._scan_item import is_manifest_file
6
+ from fosslight_source.run_manifest_extractor import (
7
+ get_licenses_from_composer_json,
8
+ get_manifest_licenses,
9
+ )
10
+
11
+
12
+ def test_is_manifest_file_recognizes_composer_json():
13
+ assert is_manifest_file("/tmp/project/composer.json") is True
14
+ assert is_manifest_file("/tmp/project/Composer.JSON") is True
15
+
16
+
17
+ def test_get_licenses_from_composer_json_string(tmp_path):
18
+ path = tmp_path / "composer.json"
19
+ path.write_text('{"name": "acme/demo", "license": "MIT"}', encoding="utf-8")
20
+ assert get_licenses_from_composer_json(str(path)) == ["MIT"]
21
+
22
+
23
+ def test_get_licenses_from_composer_json_array(tmp_path):
24
+ path = tmp_path / "composer.json"
25
+ path.write_text(
26
+ '{"name": "acme/demo", "license": ["LGPL-2.1-only", "GPL-3.0-or-later"]}',
27
+ encoding="utf-8",
28
+ )
29
+ assert get_licenses_from_composer_json(str(path)) == [
30
+ "LGPL-2.1-only",
31
+ "GPL-3.0-or-later",
32
+ ]
33
+
34
+
35
+ def test_get_licenses_from_composer_json_spdx_expression(tmp_path):
36
+ path = tmp_path / "composer.json"
37
+ path.write_text(
38
+ '{"name": "acme/demo", "license": "(MIT OR Apache-2.0)"}',
39
+ encoding="utf-8",
40
+ )
41
+ assert get_licenses_from_composer_json(str(path)) == ["MIT", "Apache-2.0"]
42
+
43
+
44
+ def test_get_manifest_licenses_dispatches_composer_json(tmp_path):
45
+ path = tmp_path / "composer.json"
46
+ path.write_text('{"license": "BSD-3-Clause"}', encoding="utf-8")
47
+ assert get_manifest_licenses(str(path)) == ["BSD-3-Clause"]
48
+
49
+
50
+ def test_get_licenses_from_composer_json_missing_license(tmp_path):
51
+ path = tmp_path / "composer.json"
52
+ path.write_text('{"name": "acme/demo"}', encoding="utf-8")
53
+ assert get_licenses_from_composer_json(str(path)) == []