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.
Files changed (25) hide show
  1. {fosslight_binary-5.1.30 → fosslight_binary-5.1.32}/PKG-INFO +3 -2
  2. {fosslight_binary-5.1.30 → fosslight_binary-5.1.32}/pyproject.toml +3 -2
  3. {fosslight_binary-5.1.30 → fosslight_binary-5.1.32}/src/fosslight_binary/_binary_dao.py +32 -2
  4. fosslight_binary-5.1.32/src/fosslight_binary/_exclude.py +15 -0
  5. {fosslight_binary-5.1.30 → fosslight_binary-5.1.32}/src/fosslight_binary/_help.py +2 -0
  6. {fosslight_binary-5.1.30 → fosslight_binary-5.1.32}/src/fosslight_binary/_jar_analysis.py +122 -49
  7. {fosslight_binary-5.1.30 → fosslight_binary-5.1.32}/src/fosslight_binary/_simple_mode.py +11 -4
  8. {fosslight_binary-5.1.30 → fosslight_binary-5.1.32}/src/fosslight_binary/binary_analysis.py +14 -9
  9. {fosslight_binary-5.1.30 → fosslight_binary-5.1.32}/src/fosslight_binary.egg-info/PKG-INFO +3 -2
  10. {fosslight_binary-5.1.30 → fosslight_binary-5.1.32}/src/fosslight_binary.egg-info/SOURCES.txt +3 -1
  11. {fosslight_binary-5.1.30 → fosslight_binary-5.1.32}/src/fosslight_binary.egg-info/requires.txt +2 -1
  12. fosslight_binary-5.1.32/tests/test_kb_ssl.py +54 -0
  13. {fosslight_binary-5.1.30 → fosslight_binary-5.1.32}/LICENSE +0 -0
  14. {fosslight_binary-5.1.30 → fosslight_binary-5.1.32}/LICENSES/Apache-2.0.txt +0 -0
  15. {fosslight_binary-5.1.30 → fosslight_binary-5.1.32}/LICENSES/LicenseRef-3rd-party-licenses.txt +0 -0
  16. {fosslight_binary-5.1.30 → fosslight_binary-5.1.32}/MANIFEST.in +0 -0
  17. {fosslight_binary-5.1.30 → fosslight_binary-5.1.32}/README.md +0 -0
  18. {fosslight_binary-5.1.30 → fosslight_binary-5.1.32}/setup.cfg +0 -0
  19. {fosslight_binary-5.1.30 → fosslight_binary-5.1.32}/src/fosslight_binary/__init__.py +0 -0
  20. {fosslight_binary-5.1.30 → fosslight_binary-5.1.32}/src/fosslight_binary/_binary.py +0 -0
  21. {fosslight_binary-5.1.30 → fosslight_binary-5.1.32}/src/fosslight_binary/cli.py +0 -0
  22. {fosslight_binary-5.1.30 → fosslight_binary-5.1.32}/src/fosslight_binary.egg-info/dependency_links.txt +0 -0
  23. {fosslight_binary-5.1.30 → fosslight_binary-5.1.32}/src/fosslight_binary.egg-info/entry_points.txt +0 -0
  24. {fosslight_binary-5.1.30 → fosslight_binary-5.1.32}/src/fosslight_binary.egg-info/top_level.txt +0 -0
  25. {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.30
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.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.30"
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.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(request, timeout=_PROBE_TIMEOUT_SEC) as response:
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(request, timeout=_HTTP_TIMEOUT_SEC) as response:
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
- _CENTRAL_SEARCH_URL = "https://search.maven.org/solrsearch/select"
21
- _REQUEST_TIMEOUT = 10 # seconds – used for HEAD / POM download
22
- _CENTRAL_SEARCH_TIMEOUT = 2.5 # seconds – tight timeout for Search API (retried on timeout)
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
- try:
119
- params = {"q": f"1:{sha1}", "rows": 1, "wt": "json"}
120
- resp = requests.get(_CENTRAL_SEARCH_URL, params=params, timeout=_timeout)
121
- resp.raise_for_status()
122
- docs = resp.json().get("response", {}).get("docs", [])
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": doc.get("g", ""),
128
- "artifactId": doc.get("a", ""),
129
- "version": doc.get("v") or doc.get("latestVersion", ""),
167
+ "groupId": groupId,
168
+ "artifactId": artifactId,
169
+ "version": version,
130
170
  }, False
131
- except requests.exceptions.Timeout:
132
- logger.debug(f"Maven Central SHA-1 search timed out ({sha1}) – will retry")
133
- return {}, True
134
- except Exception as ex:
135
- if _is_network_error(ex):
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} – will retry")
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 _build_central_jar_url(group_id, artifact_id, version):
171
- if not (group_id and artifact_id and version):
172
- return ""
173
- group_path = group_id.replace('.', '/')
174
- return f"https://repo1.maven.org/maven2/{group_path}/{artifact_id}/{version}/{artifact_id}-{version}.jar"
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 _exists_in_central(group_id, artifact_id, version):
178
- if not (group_id and artifact_id and version):
179
- return False
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=_REQUEST_TIMEOUT, allow_redirects=True)
183
- return resp.status_code == 200
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: {group_id}:{artifact_id}:{version}")
232
+ _warn_network_once(f"jar existence check: {url}")
187
233
  else:
188
- logger.debug(f"Central existence check failed ({group_id}:{artifact_id}:{version}): {ex}")
189
- return False
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, skip_central=False):
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 skip_central:
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 – will retry")
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}) – using JAR pom.xml for license")
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 – will retry")
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 – falling back to JAR internals")
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
- return any(oss.name and oss.license for oss in oss_list)
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 – skipping JAR OSS analysis.")
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 – queued for retry (attempt 1/{_MAX_RETRY})")
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
- " – falling back to JAR internals")
377
- result, _ = _process_one_jar(jar_path, rel_path, sha1, skip_central=True)
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
- return zipfile.is_zipfile(filename) or tarfile.is_tarfile(filename)
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 re.search(r".*sources\.jar", path.lower()):
45
+ if is_jar_file(path) and not path.lower().endswith('-sources.jar'):
39
46
  bin_list_exclude_compressed.append(path)
40
- elif re.search(r".*sources\.jar", path.lower()) or is_compressed_file(path):
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') or formats[i].startswith('cyclonedx'):
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
- if formats[i].startswith('spdx'):
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}'
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(path_to_find_bin, path_to_exclude, REMOVE_FILE_EXTENSION_SIMPLE)
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(path_to_find_bin, path_to_exclude)
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.30
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.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
@@ -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
@@ -9,7 +9,8 @@ py-tlsh
9
9
  pytz
10
10
  XlsxWriter
11
11
  PyYAML
12
- fosslight_util>=2.2.2
12
+ fosslight_util>=2.2.14
13
+ truststore
13
14
 
14
15
  [:"darwin" in sys_platform]
15
16
  python-magic
@@ -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)