fosslight-binary 5.1.30__tar.gz → 5.1.32__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_binary-5.1.30 → fosslight_binary-5.1.32}/PKG-INFO +3 -2
- {fosslight_binary-5.1.30 → fosslight_binary-5.1.32}/pyproject.toml +3 -2
- {fosslight_binary-5.1.30 → fosslight_binary-5.1.32}/src/fosslight_binary/_binary_dao.py +32 -2
- fosslight_binary-5.1.32/src/fosslight_binary/_exclude.py +15 -0
- {fosslight_binary-5.1.30 → fosslight_binary-5.1.32}/src/fosslight_binary/_help.py +2 -0
- {fosslight_binary-5.1.30 → fosslight_binary-5.1.32}/src/fosslight_binary/_jar_analysis.py +122 -49
- {fosslight_binary-5.1.30 → fosslight_binary-5.1.32}/src/fosslight_binary/_simple_mode.py +11 -4
- {fosslight_binary-5.1.30 → fosslight_binary-5.1.32}/src/fosslight_binary/binary_analysis.py +14 -9
- {fosslight_binary-5.1.30 → fosslight_binary-5.1.32}/src/fosslight_binary.egg-info/PKG-INFO +3 -2
- {fosslight_binary-5.1.30 → fosslight_binary-5.1.32}/src/fosslight_binary.egg-info/SOURCES.txt +3 -1
- {fosslight_binary-5.1.30 → fosslight_binary-5.1.32}/src/fosslight_binary.egg-info/requires.txt +2 -1
- fosslight_binary-5.1.32/tests/test_kb_ssl.py +54 -0
- {fosslight_binary-5.1.30 → fosslight_binary-5.1.32}/LICENSE +0 -0
- {fosslight_binary-5.1.30 → fosslight_binary-5.1.32}/LICENSES/Apache-2.0.txt +0 -0
- {fosslight_binary-5.1.30 → fosslight_binary-5.1.32}/LICENSES/LicenseRef-3rd-party-licenses.txt +0 -0
- {fosslight_binary-5.1.30 → fosslight_binary-5.1.32}/MANIFEST.in +0 -0
- {fosslight_binary-5.1.30 → fosslight_binary-5.1.32}/README.md +0 -0
- {fosslight_binary-5.1.30 → fosslight_binary-5.1.32}/setup.cfg +0 -0
- {fosslight_binary-5.1.30 → fosslight_binary-5.1.32}/src/fosslight_binary/__init__.py +0 -0
- {fosslight_binary-5.1.30 → fosslight_binary-5.1.32}/src/fosslight_binary/_binary.py +0 -0
- {fosslight_binary-5.1.30 → fosslight_binary-5.1.32}/src/fosslight_binary/cli.py +0 -0
- {fosslight_binary-5.1.30 → fosslight_binary-5.1.32}/src/fosslight_binary.egg-info/dependency_links.txt +0 -0
- {fosslight_binary-5.1.30 → fosslight_binary-5.1.32}/src/fosslight_binary.egg-info/entry_points.txt +0 -0
- {fosslight_binary-5.1.30 → fosslight_binary-5.1.32}/src/fosslight_binary.egg-info/top_level.txt +0 -0
- {fosslight_binary-5.1.30 → fosslight_binary-5.1.32}/tests/test_fosslight_binary.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fosslight_binary
|
|
3
|
-
Version: 5.1.
|
|
3
|
+
Version: 5.1.32
|
|
4
4
|
Summary: FOSSLight Binary Scanner
|
|
5
5
|
Author: LG Electronics
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -29,7 +29,8 @@ Requires-Dist: py-tlsh
|
|
|
29
29
|
Requires-Dist: pytz
|
|
30
30
|
Requires-Dist: XlsxWriter
|
|
31
31
|
Requires-Dist: PyYAML
|
|
32
|
-
Requires-Dist: fosslight_util>=2.2.
|
|
32
|
+
Requires-Dist: fosslight_util>=2.2.14
|
|
33
|
+
Requires-Dist: truststore
|
|
33
34
|
Requires-Dist: python-magic-bin; sys_platform == "win32"
|
|
34
35
|
Requires-Dist: python-magic; "darwin" in sys_platform
|
|
35
36
|
Requires-Dist: python-magic; "linux" in sys_platform
|
|
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
|
|
7
7
|
|
|
8
8
|
[project]
|
|
9
9
|
name = "fosslight_binary"
|
|
10
|
-
version = "5.1.
|
|
10
|
+
version = "5.1.32"
|
|
11
11
|
description = "FOSSLight Binary Scanner"
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
license = "Apache-2.0"
|
|
@@ -36,7 +36,8 @@ dependencies = [
|
|
|
36
36
|
"pytz",
|
|
37
37
|
"XlsxWriter",
|
|
38
38
|
"PyYAML",
|
|
39
|
-
"fosslight_util>=2.2.
|
|
39
|
+
"fosslight_util>=2.2.14",
|
|
40
|
+
"truststore",
|
|
40
41
|
"python-magic-bin; sys_platform == 'win32'",
|
|
41
42
|
"python-magic; 'darwin' in sys_platform",
|
|
42
43
|
"python-magic; 'linux' in sys_platform",
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import json
|
|
8
8
|
import logging
|
|
9
9
|
import os
|
|
10
|
+
import ssl
|
|
10
11
|
import urllib.error
|
|
11
12
|
import urllib.request
|
|
12
13
|
from typing import Dict, List, Optional, Tuple
|
|
@@ -43,6 +44,31 @@ def _get_chunk_size() -> int:
|
|
|
43
44
|
_CHUNK_SIZE = _get_chunk_size()
|
|
44
45
|
|
|
45
46
|
MatchKey = Tuple[str, str]
|
|
47
|
+
_KB_SSL_VERIFY_FALSE = {"0", "false", "no", "off"}
|
|
48
|
+
_logged_insecure_kb_ssl = False
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def kb_ssl_verify_enabled() -> bool:
|
|
52
|
+
raw = os.environ.get("KB_SSL_VERIFY", "true")
|
|
53
|
+
return raw.strip().lower() not in _KB_SSL_VERIFY_FALSE
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def create_kb_ssl_context() -> ssl.SSLContext:
|
|
57
|
+
"""TLS context for KB HTTPS. Uses the OS trust store (Windows store, like the browser)."""
|
|
58
|
+
global _logged_insecure_kb_ssl
|
|
59
|
+
if not kb_ssl_verify_enabled():
|
|
60
|
+
if not _logged_insecure_kb_ssl:
|
|
61
|
+
logger.warning("KB TLS certificate verification is disabled (KB_SSL_VERIFY)")
|
|
62
|
+
_logged_insecure_kb_ssl = True
|
|
63
|
+
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
|
64
|
+
ctx.check_hostname = False
|
|
65
|
+
ctx.verify_mode = ssl.CERT_NONE
|
|
66
|
+
return ctx
|
|
67
|
+
try:
|
|
68
|
+
import truststore
|
|
69
|
+
return truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
|
70
|
+
except ImportError:
|
|
71
|
+
return ssl.create_default_context()
|
|
46
72
|
|
|
47
73
|
|
|
48
74
|
def resolve_kb_config(kb_url: str = "", kb_token: str = "") -> Tuple[str, str]:
|
|
@@ -78,7 +104,9 @@ def check_binary_match_endpoint(kb_url: str, kb_token: str = "") -> Tuple[bool,
|
|
|
78
104
|
request.add_header("Authorization", f"Bearer {kb_token}")
|
|
79
105
|
|
|
80
106
|
try:
|
|
81
|
-
with urllib.request.urlopen(
|
|
107
|
+
with urllib.request.urlopen(
|
|
108
|
+
request, timeout=_PROBE_TIMEOUT_SEC, context=create_kb_ssl_context()
|
|
109
|
+
) as response:
|
|
82
110
|
response.read()
|
|
83
111
|
return True, ""
|
|
84
112
|
except urllib.error.HTTPError as ex:
|
|
@@ -257,7 +285,9 @@ def _post_binary_match(kb_url: str, kb_token: str, items: list) -> Optional[dict
|
|
|
257
285
|
request.add_header("Authorization", f"Bearer {kb_token}")
|
|
258
286
|
|
|
259
287
|
try:
|
|
260
|
-
with urllib.request.urlopen(
|
|
288
|
+
with urllib.request.urlopen(
|
|
289
|
+
request, timeout=_HTTP_TIMEOUT_SEC, context=create_kb_ssl_context()
|
|
290
|
+
) as response:
|
|
261
291
|
body = response.read().decode()
|
|
262
292
|
return json.loads(body) if body else {}
|
|
263
293
|
except urllib.error.HTTPError as ex:
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Copyright (c) 2026 LG Electronics Inc.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Binary-scanner filename excludes."""
|
|
5
|
+
|
|
6
|
+
from fosslight_util.exclude import is_excluded_filename
|
|
7
|
+
|
|
8
|
+
# Scanner output binaries that should not be re-analyzed.
|
|
9
|
+
EXCLUDE_FILENAME_BINARY = frozenset({
|
|
10
|
+
"fosslight_bin", "fosslight_bin.exe",
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def is_excluded_binary_filename(file_path: str) -> bool:
|
|
15
|
+
return is_excluded_filename(file_path, EXCLUDE_FILENAME_BINARY)
|
|
@@ -34,6 +34,8 @@ _HELP_MESSAGE_BINARY = f"""
|
|
|
34
34
|
────────────────────────────────────────────────────────────────────
|
|
35
35
|
--kb_url <url> KB API URL (priority: parameter > KB_URL env > default)
|
|
36
36
|
--kb_token <token> KB bearer token (priority: parameter > KB_TOKEN env)
|
|
37
|
+
HTTPS uses the OS certificate store. Set KB_SSL_VERIFY=false
|
|
38
|
+
only if certificate verification must be skipped.
|
|
37
39
|
--notice Print the open source license notice text
|
|
38
40
|
--no_correction Skip OSS information correction with sbom-info.yaml
|
|
39
41
|
--correct_fpath <path> Path to custom sbom-info.yaml file
|
|
@@ -7,21 +7,32 @@
|
|
|
7
7
|
import hashlib
|
|
8
8
|
import logging
|
|
9
9
|
import os
|
|
10
|
+
import re
|
|
10
11
|
import tempfile
|
|
11
12
|
import zipfile
|
|
12
13
|
import defusedxml.ElementTree as ET
|
|
13
14
|
import requests
|
|
14
15
|
import fosslight_util.constant as constant
|
|
16
|
+
from fosslight_util._get_downloadable_url import _maven_repo_bases_for
|
|
15
17
|
from fosslight_util.get_pom_license import get_license_from_pom
|
|
16
18
|
from fosslight_util.oss_item import OssItem
|
|
17
19
|
|
|
18
20
|
logger = logging.getLogger(constant.LOGGER_NAME)
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
# Both hosts serve the same Central Solr index and the same g/a/v fields, but
|
|
23
|
+
# search.maven.org answers only about one query in six: the TCP connect always
|
|
24
|
+
# succeeds and then the response never starts, so no timeout budget rescues it.
|
|
25
|
+
# The Portal host answers every query in about half a second, so it goes first.
|
|
26
|
+
_CENTRAL_SEARCH_URLS = (
|
|
27
|
+
"https://central.sonatype.com/solrsearch/select",
|
|
28
|
+
"https://search.maven.org/solrsearch/select",
|
|
29
|
+
)
|
|
30
|
+
_REQUEST_TIMEOUT = 10 # seconds - used for HEAD / POM download
|
|
31
|
+
_CENTRAL_SEARCH_TIMEOUT = 2.5 # seconds - per-host budget, retried on timeout
|
|
32
|
+
_MAVEN_JAR_HTTP_TIMEOUT = (2, 2) # match Util probe timeouts for multi-repo jar checks
|
|
23
33
|
_MAX_RETRY = 3 # maximum Central API retry attempts per JAR
|
|
24
34
|
_central_network_warned = False # Flag to suppress repeated network-unavailable warnings within one run
|
|
35
|
+
_COORD_TOKEN = re.compile(r'[A-Za-z0-9._+-]+') # shape of a Maven groupId / artifactId / version
|
|
25
36
|
|
|
26
37
|
|
|
27
38
|
def _sha1_of_file(filepath):
|
|
@@ -112,31 +123,56 @@ def _warn_network_once(context=""):
|
|
|
112
123
|
|
|
113
124
|
|
|
114
125
|
def _search_central_by_sha1(sha1, timeout=None):
|
|
126
|
+
"""Look up Maven coordinates by JAR checksum, trying each Central host in turn.
|
|
127
|
+
|
|
128
|
+
An answer from any host is authoritative for the whole index, so a miss ends
|
|
129
|
+
the search. ``timed_out`` is reported only when no host answered at all.
|
|
130
|
+
"""
|
|
115
131
|
if not sha1:
|
|
116
132
|
return {}, False
|
|
117
133
|
_timeout = timeout if timeout is not None else _CENTRAL_SEARCH_TIMEOUT
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
134
|
+
params = {"q": f"1:{sha1}", "rows": 1, "wt": "json"}
|
|
135
|
+
any_timeout = False
|
|
136
|
+
unreachable = False
|
|
137
|
+
|
|
138
|
+
for url in _CENTRAL_SEARCH_URLS:
|
|
139
|
+
try:
|
|
140
|
+
resp = requests.get(url, params=params, timeout=_timeout)
|
|
141
|
+
resp.raise_for_status()
|
|
142
|
+
docs = resp.json().get("response", {}).get("docs", [])
|
|
143
|
+
except requests.exceptions.Timeout:
|
|
144
|
+
logger.debug(f"Maven Central SHA-1 search timed out ({sha1}) at {url} - will retry")
|
|
145
|
+
any_timeout = True
|
|
146
|
+
continue
|
|
147
|
+
except Exception as ex:
|
|
148
|
+
if _is_network_error(ex):
|
|
149
|
+
unreachable = True
|
|
150
|
+
else:
|
|
151
|
+
logger.debug(f"Maven Central SHA-1 search failed ({sha1}) at {url}: {ex}")
|
|
152
|
+
continue
|
|
153
|
+
|
|
123
154
|
if not docs:
|
|
124
155
|
return {}, False
|
|
156
|
+
|
|
125
157
|
doc = docs[0]
|
|
158
|
+
groupId = doc.get("g", "")
|
|
159
|
+
artifactId = doc.get("a", "")
|
|
160
|
+
version = doc.get("v") or doc.get("latestVersion", "")
|
|
161
|
+
|
|
162
|
+
if not (groupId and artifactId and version):
|
|
163
|
+
logger.debug(f"Maven Central returned an incomplete document for {sha1}: {doc}")
|
|
164
|
+
return {}, False
|
|
165
|
+
|
|
126
166
|
return {
|
|
127
|
-
"groupId":
|
|
128
|
-
"artifactId":
|
|
129
|
-
"version":
|
|
167
|
+
"groupId": groupId,
|
|
168
|
+
"artifactId": artifactId,
|
|
169
|
+
"version": version,
|
|
130
170
|
}, False
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
_warn_network_once(f"SHA-1 search: {sha1[:10]}...")
|
|
137
|
-
else:
|
|
138
|
-
logger.debug(f"Maven Central SHA-1 search failed ({sha1}): {ex}")
|
|
139
|
-
return {}, False
|
|
171
|
+
|
|
172
|
+
# Warn only once every host has failed: a fallback answer is not a failure.
|
|
173
|
+
if unreachable:
|
|
174
|
+
_warn_network_once(f"SHA-1 search: {sha1[:10]}...")
|
|
175
|
+
return {}, any_timeout
|
|
140
176
|
|
|
141
177
|
|
|
142
178
|
def _download_pom_to_tempfile(group_id, artifact_id, version, timeout=None):
|
|
@@ -157,7 +193,7 @@ def _download_pom_to_tempfile(group_id, artifact_id, version, timeout=None):
|
|
|
157
193
|
logger.debug(f"POM downloaded to {tmp.name} from {url}")
|
|
158
194
|
return tmp.name, False
|
|
159
195
|
except requests.exceptions.Timeout:
|
|
160
|
-
logger.debug(f"POM download timed out from {url}
|
|
196
|
+
logger.debug(f"POM download timed out from {url} - will retry")
|
|
161
197
|
any_timeout = True
|
|
162
198
|
except Exception as ex:
|
|
163
199
|
if _is_network_error(ex):
|
|
@@ -167,40 +203,69 @@ def _download_pom_to_tempfile(group_id, artifact_id, version, timeout=None):
|
|
|
167
203
|
return None, any_timeout
|
|
168
204
|
|
|
169
205
|
|
|
170
|
-
def
|
|
171
|
-
if
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
206
|
+
def _is_maven_coordinate(*parts):
|
|
207
|
+
"""True only if every part could be a Maven coordinate token.
|
|
208
|
+
|
|
209
|
+
MANIFEST.MF vendor fields are display names ("The Apache Software
|
|
210
|
+
Foundation", "Google, Inc.", "%bundleVendor"), not groupIds. Rejecting them
|
|
211
|
+
keeps bogus URLs out of the report and avoids HEAD requests that can only 404.
|
|
212
|
+
"""
|
|
213
|
+
return all(p and _COORD_TOKEN.fullmatch(p) for p in parts)
|
|
175
214
|
|
|
176
215
|
|
|
177
|
-
def
|
|
178
|
-
if
|
|
179
|
-
|
|
180
|
-
url = _build_central_jar_url(group_id, artifact_id, version)
|
|
216
|
+
def _maven_jar_url_exists(url, timeout=None):
|
|
217
|
+
"""True if a Maven host serves this jar URL (HEAD, with GET fallback)."""
|
|
218
|
+
_timeout = timeout if timeout is not None else _MAVEN_JAR_HTTP_TIMEOUT
|
|
181
219
|
try:
|
|
182
|
-
resp = requests.head(url, timeout=
|
|
183
|
-
|
|
220
|
+
resp = requests.head(url, timeout=_timeout, allow_redirects=True)
|
|
221
|
+
if resp.status_code == 200:
|
|
222
|
+
return True
|
|
223
|
+
# Some Maven hosts reject HEAD; fall back to a streamed GET (Util pattern).
|
|
224
|
+
if resp.status_code in (403, 405, 501):
|
|
225
|
+
resp = requests.get(url, stream=True, allow_redirects=True, timeout=_timeout)
|
|
226
|
+
try:
|
|
227
|
+
return resp.status_code == 200
|
|
228
|
+
finally:
|
|
229
|
+
resp.close()
|
|
184
230
|
except Exception as ex:
|
|
185
231
|
if _is_network_error(ex):
|
|
186
|
-
_warn_network_once(f"existence check: {
|
|
232
|
+
_warn_network_once(f"jar existence check: {url}")
|
|
187
233
|
else:
|
|
188
|
-
logger.debug(f"
|
|
189
|
-
|
|
234
|
+
logger.debug(f"Maven jar existence check failed ({url}): {ex}")
|
|
235
|
+
return False
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _find_jar_download_url(group_id, artifact_id, version):
|
|
239
|
+
"""Return the first known-repo URL that hosts ``{artifact}-{version}.jar``.
|
|
240
|
+
Uses FOSSLight Util's repository order (group hints + MAVEN_REPOSITORY_BASES)
|
|
241
|
+
so non-Central artifacts (Google, Confluent, Spring, …) can still fill
|
|
242
|
+
Download Location.
|
|
243
|
+
"""
|
|
244
|
+
if not _is_maven_coordinate(group_id, artifact_id, version):
|
|
245
|
+
return ""
|
|
246
|
+
group_path = group_id.replace('.', '/')
|
|
247
|
+
jar_name = f"{artifact_id}-{version}.jar"
|
|
248
|
+
for repo_base in _maven_repo_bases_for(group_path):
|
|
249
|
+
url = f"{repo_base}/{group_path}/{artifact_id}/{version}/{jar_name}"
|
|
250
|
+
if _maven_jar_url_exists(url):
|
|
251
|
+
logger.debug(f"Maven jar found: {url}")
|
|
252
|
+
return url
|
|
253
|
+
return ""
|
|
190
254
|
|
|
191
255
|
|
|
192
|
-
def _process_one_jar(jar_path, rel_path, sha1, search_timeout=None,
|
|
256
|
+
def _process_one_jar(jar_path, rel_path, sha1, search_timeout=None, skip_central_search=False):
|
|
193
257
|
groupId = artifactId = version = project_url = license_str = ''
|
|
194
258
|
confirmed_in_central = False
|
|
259
|
+
trusted_coordinates = False
|
|
195
260
|
source = ''
|
|
196
261
|
|
|
197
|
-
if
|
|
262
|
+
if skip_central_search:
|
|
198
263
|
central_info = {}
|
|
199
264
|
timed_out = False
|
|
200
265
|
else:
|
|
201
266
|
central_info, timed_out = _search_central_by_sha1(sha1, timeout=search_timeout)
|
|
202
267
|
if timed_out:
|
|
203
|
-
logger.debug(f"{rel_path}: Central SHA-1 search timed out
|
|
268
|
+
logger.debug(f"{rel_path}: Central SHA-1 search timed out - will retry")
|
|
204
269
|
return None, True
|
|
205
270
|
|
|
206
271
|
g2, a2, v2, url2, pom_tmp_path = _read_pom_from_jar(jar_path)
|
|
@@ -215,10 +280,11 @@ def _process_one_jar(jar_path, rel_path, sha1, search_timeout=None, skip_central
|
|
|
215
280
|
names_match = (central_oss_name == jar_oss_name and c_version == v2)
|
|
216
281
|
|
|
217
282
|
if names_match:
|
|
218
|
-
logger.debug(f"{rel_path}: Central and JAR pom.xml match ({central_oss_name} {c_version})
|
|
283
|
+
logger.debug(f"{rel_path}: Central and JAR pom.xml match ({central_oss_name} {c_version}) - using JAR pom.xml for license")
|
|
219
284
|
groupId, artifactId, version, project_url = g2, a2, v2, url2
|
|
220
285
|
source = 'pom.xml'
|
|
221
286
|
confirmed_in_central = True
|
|
287
|
+
trusted_coordinates = True
|
|
222
288
|
|
|
223
289
|
if pom_tmp_path:
|
|
224
290
|
try:
|
|
@@ -239,11 +305,12 @@ def _process_one_jar(jar_path, rel_path, sha1, search_timeout=None, skip_central
|
|
|
239
305
|
groupId, artifactId, version = c_groupId, c_artifactId, c_version
|
|
240
306
|
source = 'Maven Central'
|
|
241
307
|
confirmed_in_central = True
|
|
308
|
+
trusted_coordinates = True
|
|
242
309
|
|
|
243
310
|
tmp_path, timed_out = _download_pom_to_tempfile(
|
|
244
311
|
groupId, artifactId, version, timeout=search_timeout)
|
|
245
312
|
if timed_out:
|
|
246
|
-
logger.debug(f"{rel_path}: POM download timed out
|
|
313
|
+
logger.debug(f"{rel_path}: POM download timed out - will retry")
|
|
247
314
|
if pom_tmp_path:
|
|
248
315
|
try:
|
|
249
316
|
os.remove(pom_tmp_path)
|
|
@@ -271,11 +338,12 @@ def _process_one_jar(jar_path, rel_path, sha1, search_timeout=None, skip_central
|
|
|
271
338
|
pass
|
|
272
339
|
|
|
273
340
|
else:
|
|
274
|
-
logger.debug(f"{rel_path}: not found in Maven Central
|
|
341
|
+
logger.debug(f"{rel_path}: not found in Maven Central - falling back to JAR internals")
|
|
275
342
|
|
|
276
343
|
if g2 or a2:
|
|
277
344
|
groupId, artifactId, version, project_url = g2, a2, v2, url2
|
|
278
345
|
source = 'pom.xml'
|
|
346
|
+
trusted_coordinates = True
|
|
279
347
|
|
|
280
348
|
if pom_tmp_path:
|
|
281
349
|
try:
|
|
@@ -291,8 +359,6 @@ def _process_one_jar(jar_path, rel_path, sha1, search_timeout=None, skip_central
|
|
|
291
359
|
os.remove(pom_tmp_path)
|
|
292
360
|
except Exception:
|
|
293
361
|
pass
|
|
294
|
-
if not confirmed_in_central and not skip_central:
|
|
295
|
-
confirmed_in_central = _exists_in_central(groupId, artifactId, version)
|
|
296
362
|
|
|
297
363
|
if not (groupId and artifactId):
|
|
298
364
|
g3, a3, v3, url3 = _read_manifest_from_jar(jar_path)
|
|
@@ -300,14 +366,20 @@ def _process_one_jar(jar_path, rel_path, sha1, search_timeout=None, skip_central
|
|
|
300
366
|
groupId = g3
|
|
301
367
|
artifactId = a3
|
|
302
368
|
version = version or v3
|
|
369
|
+
confirmed_in_central = False
|
|
303
370
|
project_url = project_url or url3
|
|
304
371
|
source = 'MANIFEST.MF'
|
|
372
|
+
trusted_coordinates = False
|
|
305
373
|
|
|
306
374
|
if not (groupId or artifactId):
|
|
307
375
|
return None, False
|
|
308
376
|
|
|
377
|
+
if confirmed_in_central or trusted_coordinates:
|
|
378
|
+
dl_url = _find_jar_download_url(groupId, artifactId, version)
|
|
379
|
+
else:
|
|
380
|
+
dl_url = ""
|
|
381
|
+
|
|
309
382
|
oss_name = f"{groupId}:{artifactId}" if groupId and artifactId else (artifactId or groupId)
|
|
310
|
-
dl_url = _build_central_jar_url(groupId, artifactId, version) if confirmed_in_central else ""
|
|
311
383
|
|
|
312
384
|
oss = OssItem(oss_name, version, license_str, dl_url)
|
|
313
385
|
oss.comment = source
|
|
@@ -319,7 +391,8 @@ def _process_one_jar(jar_path, rel_path, sha1, search_timeout=None, skip_central
|
|
|
319
391
|
|
|
320
392
|
|
|
321
393
|
def _has_valid_jar_oss(oss_list):
|
|
322
|
-
|
|
394
|
+
"""True if any entry is identified. A missing license is reported as empty."""
|
|
395
|
+
return any(oss.name for oss in oss_list)
|
|
323
396
|
|
|
324
397
|
|
|
325
398
|
def _store_jar_result(jar_items, sha1, result):
|
|
@@ -345,7 +418,7 @@ def analyze_jar_file(path_to_find_bin, path_to_exclude):
|
|
|
345
418
|
jar_files.append(os.path.join(root_dir, fname))
|
|
346
419
|
|
|
347
420
|
if not jar_files:
|
|
348
|
-
logger.info("No .jar files found
|
|
421
|
+
logger.info("No .jar files found - skipping JAR OSS analysis.")
|
|
349
422
|
return jar_items, success
|
|
350
423
|
|
|
351
424
|
for jar_path in jar_files:
|
|
@@ -361,7 +434,7 @@ def analyze_jar_file(path_to_find_bin, path_to_exclude):
|
|
|
361
434
|
jar_path, rel_path, sha1, search_timeout=_CENTRAL_SEARCH_TIMEOUT)
|
|
362
435
|
|
|
363
436
|
if needs_retry:
|
|
364
|
-
logger.debug(f"{rel_path}: Central API timed out
|
|
437
|
+
logger.debug(f"{rel_path}: Central API timed out - queued for retry (attempt 1/{_MAX_RETRY})")
|
|
365
438
|
pending_sha1s.add(sha1)
|
|
366
439
|
retry_queue.append((jar_path, rel_path, sha1, 1))
|
|
367
440
|
elif result is not None:
|
|
@@ -373,8 +446,8 @@ def analyze_jar_file(path_to_find_bin, path_to_exclude):
|
|
|
373
446
|
if attempt >= _MAX_RETRY:
|
|
374
447
|
logger.warning(
|
|
375
448
|
f"{rel_path}: Maven Central API timed out after {_MAX_RETRY} attempts"
|
|
376
|
-
"
|
|
377
|
-
result, _ = _process_one_jar(jar_path, rel_path, sha1,
|
|
449
|
+
" - falling back to JAR internals")
|
|
450
|
+
result, _ = _process_one_jar(jar_path, rel_path, sha1, skip_central_search=True)
|
|
378
451
|
if result is not None:
|
|
379
452
|
_store_jar_result(jar_items, sha1, result)
|
|
380
453
|
continue
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
# Copyright (c) 2024 LG Electronics Inc.
|
|
5
5
|
# SPDX-License-Identifier: Apache-2.0
|
|
6
6
|
import os
|
|
7
|
-
import re
|
|
8
7
|
import logging
|
|
9
8
|
import zipfile
|
|
10
9
|
import tarfile
|
|
10
|
+
import magic
|
|
11
11
|
import yaml
|
|
12
12
|
import fosslight_util.constant as constant
|
|
13
13
|
from fosslight_util.set_log import init_log
|
|
@@ -18,7 +18,14 @@ logger = logging.getLogger(constant.LOGGER_NAME)
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
def is_compressed_file(filename):
|
|
21
|
-
|
|
21
|
+
if zipfile.is_zipfile(filename) or tarfile.is_tarfile(filename):
|
|
22
|
+
return True
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
file_type = magic.from_file(filename).lower()
|
|
26
|
+
return 'archive' in file_type or 'compressed' in file_type
|
|
27
|
+
except Exception:
|
|
28
|
+
return False
|
|
22
29
|
|
|
23
30
|
|
|
24
31
|
def is_jar_file(filename):
|
|
@@ -35,9 +42,9 @@ def exclude_bin_for_simple_mode(binary_list):
|
|
|
35
42
|
|
|
36
43
|
path = bin.bin_name_with_path
|
|
37
44
|
|
|
38
|
-
if is_jar_file(path) and not
|
|
45
|
+
if is_jar_file(path) and not path.lower().endswith('-sources.jar'):
|
|
39
46
|
bin_list_exclude_compressed.append(path)
|
|
40
|
-
elif
|
|
47
|
+
elif path.lower().endswith('-sources.jar') or is_compressed_file(path):
|
|
41
48
|
compressed_list.append(path)
|
|
42
49
|
else:
|
|
43
50
|
bin_list_exclude_compressed.append(path)
|
|
@@ -22,6 +22,7 @@ from fosslight_util.time import current_timestamp_utc, format_running_time, time
|
|
|
22
22
|
from fosslight_util.correct import correct_with_yaml
|
|
23
23
|
from fosslight_util.oss_item import ScannerItem
|
|
24
24
|
from fosslight_util.exclude import get_excluded_paths
|
|
25
|
+
from ._exclude import EXCLUDE_FILENAME_BINARY, is_excluded_binary_filename
|
|
25
26
|
import hashlib
|
|
26
27
|
import tlsh
|
|
27
28
|
from io import open
|
|
@@ -93,15 +94,14 @@ def init(path_to_find_bin, output_file_name, formats, path_to_exclude=[]):
|
|
|
93
94
|
for i, output_extension in enumerate(output_extensions):
|
|
94
95
|
if output_files[i] is None or output_files[i] == "":
|
|
95
96
|
if formats:
|
|
96
|
-
if formats[i].startswith('spdx')
|
|
97
|
+
if formats[i].startswith('spdx'):
|
|
97
98
|
if platform.system() == 'Windows':
|
|
98
99
|
logger.warning(f'{formats[i]} is not supported on Windows. Please remove {formats[i]} from format.')
|
|
99
100
|
to_remove.append(i)
|
|
100
101
|
else:
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
output_files[i] = f'fosslight_cyclonedx_bin_{file_time}'
|
|
102
|
+
output_files[i] = f"fosslight_spdx_bin_{file_time}"
|
|
103
|
+
elif formats[i].startswith('cyclonedx'):
|
|
104
|
+
output_files[i] = f'fosslight_cyclonedx_bin_{file_time}'
|
|
105
105
|
else:
|
|
106
106
|
if output_extension == _json_ext:
|
|
107
107
|
output_files[i] = f"fosslight_opossum_bin_{file_time}"
|
|
@@ -147,7 +147,7 @@ def get_file_list(path_to_find, excluded_files):
|
|
|
147
147
|
for file in files:
|
|
148
148
|
bin_with_path = os.path.join(root, file)
|
|
149
149
|
rel_path_file = os.path.relpath(bin_with_path, path_to_find).replace('\\', '/')
|
|
150
|
-
if rel_path_file in excluded_files:
|
|
150
|
+
if rel_path_file in excluded_files or is_excluded_binary_filename(rel_path_file):
|
|
151
151
|
continue
|
|
152
152
|
file_lower_case = file.lower()
|
|
153
153
|
extension = os.path.splitext(file_lower_case)[1][1:].strip()
|
|
@@ -200,10 +200,14 @@ def find_binaries(path_to_find_bin, output_dir, formats, kb_url="", kb_token="",
|
|
|
200
200
|
excluded_path_with_default_exclusion, excluded_path_without_dot, excluded_files, cnt_file_except_skipped = all_exclude_mode
|
|
201
201
|
elif simple_mode:
|
|
202
202
|
excluded_path_with_default_exclusion, excluded_path_without_dot, excluded_files, cnt_file_except_skipped \
|
|
203
|
-
= get_excluded_paths(
|
|
203
|
+
= get_excluded_paths(
|
|
204
|
+
path_to_find_bin, path_to_exclude, REMOVE_FILE_EXTENSION_SIMPLE,
|
|
205
|
+
exclude_filenames=EXCLUDE_FILENAME_BINARY)
|
|
204
206
|
else:
|
|
205
207
|
excluded_path_with_default_exclusion, excluded_path_without_dot, excluded_files, cnt_file_except_skipped \
|
|
206
|
-
= get_excluded_paths(
|
|
208
|
+
= get_excluded_paths(
|
|
209
|
+
path_to_find_bin, path_to_exclude,
|
|
210
|
+
exclude_filenames=EXCLUDE_FILENAME_BINARY)
|
|
207
211
|
logger.debug(f"Skipped paths: {excluded_path_with_default_exclusion}")
|
|
208
212
|
|
|
209
213
|
if not os.path.isdir(path_to_find_bin):
|
|
@@ -273,7 +277,8 @@ def find_binaries(path_to_find_bin, output_dir, formats, kb_url="", kb_token="",
|
|
|
273
277
|
|
|
274
278
|
for combined_path_and_file, output_extension, output_format in zip(result_reports, output_extensions, formats):
|
|
275
279
|
results.append(write_output_file(combined_path_and_file, output_extension, scan_item,
|
|
276
|
-
BIN_EXT_HEADER, HIDE_HEADER, output_format
|
|
280
|
+
BIN_EXT_HEADER, HIDE_HEADER, output_format,
|
|
281
|
+
scanner_covers=[scan_item.cover]))
|
|
277
282
|
|
|
278
283
|
except Exception as ex:
|
|
279
284
|
error_occured(error_msg=str(ex), exit=False)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fosslight_binary
|
|
3
|
-
Version: 5.1.
|
|
3
|
+
Version: 5.1.32
|
|
4
4
|
Summary: FOSSLight Binary Scanner
|
|
5
5
|
Author: LG Electronics
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -29,7 +29,8 @@ Requires-Dist: py-tlsh
|
|
|
29
29
|
Requires-Dist: pytz
|
|
30
30
|
Requires-Dist: XlsxWriter
|
|
31
31
|
Requires-Dist: PyYAML
|
|
32
|
-
Requires-Dist: fosslight_util>=2.2.
|
|
32
|
+
Requires-Dist: fosslight_util>=2.2.14
|
|
33
|
+
Requires-Dist: truststore
|
|
33
34
|
Requires-Dist: python-magic-bin; sys_platform == "win32"
|
|
34
35
|
Requires-Dist: python-magic; "darwin" in sys_platform
|
|
35
36
|
Requires-Dist: python-magic; "linux" in sys_platform
|
{fosslight_binary-5.1.30 → fosslight_binary-5.1.32}/src/fosslight_binary.egg-info/SOURCES.txt
RENAMED
|
@@ -8,6 +8,7 @@ LICENSES/LicenseRef-3rd-party-licenses.txt
|
|
|
8
8
|
src/fosslight_binary/__init__.py
|
|
9
9
|
src/fosslight_binary/_binary.py
|
|
10
10
|
src/fosslight_binary/_binary_dao.py
|
|
11
|
+
src/fosslight_binary/_exclude.py
|
|
11
12
|
src/fosslight_binary/_help.py
|
|
12
13
|
src/fosslight_binary/_jar_analysis.py
|
|
13
14
|
src/fosslight_binary/_simple_mode.py
|
|
@@ -19,4 +20,5 @@ src/fosslight_binary.egg-info/dependency_links.txt
|
|
|
19
20
|
src/fosslight_binary.egg-info/entry_points.txt
|
|
20
21
|
src/fosslight_binary.egg-info/requires.txt
|
|
21
22
|
src/fosslight_binary.egg-info/top_level.txt
|
|
22
|
-
tests/test_fosslight_binary.py
|
|
23
|
+
tests/test_fosslight_binary.py
|
|
24
|
+
tests/test_kb_ssl.py
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Copyright (c) 2026 LG Electronics Inc.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Tests for KB HTTPS SSL context selection."""
|
|
4
|
+
|
|
5
|
+
import ssl
|
|
6
|
+
from unittest.mock import patch
|
|
7
|
+
|
|
8
|
+
from fosslight_binary._binary_dao import create_kb_ssl_context, kb_ssl_verify_enabled
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_kb_ssl_verify_enabled_default(monkeypatch):
|
|
12
|
+
monkeypatch.delenv("KB_SSL_VERIFY", raising=False)
|
|
13
|
+
assert kb_ssl_verify_enabled() is True
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_kb_ssl_verify_disabled_values(monkeypatch):
|
|
17
|
+
for value in ("false", "0", "no", "OFF"):
|
|
18
|
+
monkeypatch.setenv("KB_SSL_VERIFY", value)
|
|
19
|
+
assert kb_ssl_verify_enabled() is False
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_create_kb_ssl_context_insecure(monkeypatch):
|
|
23
|
+
monkeypatch.setenv("KB_SSL_VERIFY", "false")
|
|
24
|
+
ctx = create_kb_ssl_context()
|
|
25
|
+
assert ctx.verify_mode == ssl.CERT_NONE
|
|
26
|
+
assert ctx.check_hostname is False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_create_kb_ssl_context_uses_truststore_when_verify_on(monkeypatch):
|
|
30
|
+
monkeypatch.setenv("KB_SSL_VERIFY", "true")
|
|
31
|
+
ctx = create_kb_ssl_context()
|
|
32
|
+
assert ctx.verify_mode != ssl.CERT_NONE
|
|
33
|
+
assert ctx.check_hostname is True
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_probe_passes_ssl_context():
|
|
37
|
+
from fosslight_binary._binary_dao import check_binary_match_endpoint
|
|
38
|
+
|
|
39
|
+
class _Resp:
|
|
40
|
+
def read(self):
|
|
41
|
+
return b"{}"
|
|
42
|
+
|
|
43
|
+
def __enter__(self):
|
|
44
|
+
return self
|
|
45
|
+
|
|
46
|
+
def __exit__(self, *args):
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
with patch("fosslight_binary._binary_dao.urllib.request.urlopen", return_value=_Resp()) as urlopen:
|
|
50
|
+
available, comment = check_binary_match_endpoint("https://kb.example/", "")
|
|
51
|
+
assert available is True
|
|
52
|
+
assert comment == ""
|
|
53
|
+
assert urlopen.call_args.kwargs["context"] is not None
|
|
54
|
+
assert isinstance(urlopen.call_args.kwargs["context"], ssl.SSLContext)
|
|
File without changes
|
|
File without changes
|
{fosslight_binary-5.1.30 → fosslight_binary-5.1.32}/LICENSES/LicenseRef-3rd-party-licenses.txt
RENAMED
|
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_binary-5.1.30 → fosslight_binary-5.1.32}/src/fosslight_binary.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{fosslight_binary-5.1.30 → fosslight_binary-5.1.32}/src/fosslight_binary.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|