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.
- {fosslight_source-2.3.5/src/fosslight_source.egg-info → fosslight_source-2.3.7}/PKG-INFO +1 -1
- {fosslight_source-2.3.5 → fosslight_source-2.3.7}/pyproject.toml +1 -1
- {fosslight_source-2.3.5 → fosslight_source-2.3.7}/src/fosslight_source/_scan_item.py +1 -0
- {fosslight_source-2.3.5 → fosslight_source-2.3.7}/src/fosslight_source/cli.py +2 -2
- {fosslight_source-2.3.5 → fosslight_source-2.3.7}/src/fosslight_source/run_manifest_extractor.py +43 -0
- {fosslight_source-2.3.5 → fosslight_source-2.3.7}/src/fosslight_source/run_scanoss.py +66 -11
- {fosslight_source-2.3.5 → fosslight_source-2.3.7/src/fosslight_source.egg-info}/PKG-INFO +1 -1
- {fosslight_source-2.3.5 → fosslight_source-2.3.7}/src/fosslight_source.egg-info/SOURCES.txt +1 -0
- fosslight_source-2.3.7/tests/test_manifest_composer.py +53 -0
- {fosslight_source-2.3.5 → fosslight_source-2.3.7}/LICENSE +0 -0
- {fosslight_source-2.3.5 → fosslight_source-2.3.7}/MANIFEST.in +0 -0
- {fosslight_source-2.3.5 → fosslight_source-2.3.7}/README.md +0 -0
- {fosslight_source-2.3.5 → fosslight_source-2.3.7}/setup.cfg +0 -0
- {fosslight_source-2.3.5 → fosslight_source-2.3.7}/src/fosslight_source/__init__.py +0 -0
- {fosslight_source-2.3.5 → fosslight_source-2.3.7}/src/fosslight_source/_help.py +0 -0
- {fosslight_source-2.3.5 → fosslight_source-2.3.7}/src/fosslight_source/_kb_client.py +0 -0
- {fosslight_source-2.3.5 → fosslight_source-2.3.7}/src/fosslight_source/_license_matched.py +0 -0
- {fosslight_source-2.3.5 → fosslight_source-2.3.7}/src/fosslight_source/_merge.py +0 -0
- {fosslight_source-2.3.5 → fosslight_source-2.3.7}/src/fosslight_source/_parsing_scancode_file_item.py +0 -0
- {fosslight_source-2.3.5 → fosslight_source-2.3.7}/src/fosslight_source/_parsing_scanoss_file.py +0 -0
- {fosslight_source-2.3.5 → fosslight_source-2.3.7}/src/fosslight_source/_scancode_ignore_binaries.py +0 -0
- {fosslight_source-2.3.5 → fosslight_source-2.3.7}/src/fosslight_source/run_scancode.py +0 -0
- {fosslight_source-2.3.5 → fosslight_source-2.3.7}/src/fosslight_source/run_spdx_extractor.py +0 -0
- {fosslight_source-2.3.5 → fosslight_source-2.3.7}/src/fosslight_source.egg-info/dependency_links.txt +0 -0
- {fosslight_source-2.3.5 → fosslight_source-2.3.7}/src/fosslight_source.egg-info/entry_points.txt +0 -0
- {fosslight_source-2.3.5 → fosslight_source-2.3.7}/src/fosslight_source.egg-info/requires.txt +0 -0
- {fosslight_source-2.3.5 → fosslight_source-2.3.7}/src/fosslight_source.egg-info/top_level.txt +0 -0
- {fosslight_source-2.3.5 → fosslight_source-2.3.7}/tests/test_tox.py +0 -0
|
@@ -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"
|
|
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:
|
{fosslight_source-2.3.5 → fosslight_source-2.3.7}/src/fosslight_source/run_manifest_extractor.py
RENAMED
|
@@ -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
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
100
|
-
|
|
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):
|
|
@@ -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)) == []
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fosslight_source-2.3.5 → fosslight_source-2.3.7}/src/fosslight_source/_parsing_scanoss_file.py
RENAMED
|
File without changes
|
{fosslight_source-2.3.5 → fosslight_source-2.3.7}/src/fosslight_source/_scancode_ignore_binaries.py
RENAMED
|
File without changes
|
|
File without changes
|
{fosslight_source-2.3.5 → fosslight_source-2.3.7}/src/fosslight_source/run_spdx_extractor.py
RENAMED
|
File without changes
|
{fosslight_source-2.3.5 → fosslight_source-2.3.7}/src/fosslight_source.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{fosslight_source-2.3.5 → fosslight_source-2.3.7}/src/fosslight_source.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{fosslight_source-2.3.5 → fosslight_source-2.3.7}/src/fosslight_source.egg-info/requires.txt
RENAMED
|
File without changes
|
{fosslight_source-2.3.5 → fosslight_source-2.3.7}/src/fosslight_source.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|