fosslight-binary 5.1.30__tar.gz → 5.1.31__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 (24) hide show
  1. {fosslight_binary-5.1.30 → fosslight_binary-5.1.31}/PKG-INFO +3 -2
  2. {fosslight_binary-5.1.30 → fosslight_binary-5.1.31}/pyproject.toml +3 -2
  3. {fosslight_binary-5.1.30 → fosslight_binary-5.1.31}/src/fosslight_binary/_binary_dao.py +32 -2
  4. {fosslight_binary-5.1.30 → fosslight_binary-5.1.31}/src/fosslight_binary/_help.py +2 -0
  5. {fosslight_binary-5.1.30 → fosslight_binary-5.1.31}/src/fosslight_binary/_jar_analysis.py +82 -34
  6. {fosslight_binary-5.1.30 → fosslight_binary-5.1.31}/src/fosslight_binary.egg-info/PKG-INFO +3 -2
  7. {fosslight_binary-5.1.30 → fosslight_binary-5.1.31}/src/fosslight_binary.egg-info/SOURCES.txt +2 -1
  8. {fosslight_binary-5.1.30 → fosslight_binary-5.1.31}/src/fosslight_binary.egg-info/requires.txt +2 -1
  9. fosslight_binary-5.1.31/tests/test_kb_ssl.py +54 -0
  10. {fosslight_binary-5.1.30 → fosslight_binary-5.1.31}/LICENSE +0 -0
  11. {fosslight_binary-5.1.30 → fosslight_binary-5.1.31}/LICENSES/Apache-2.0.txt +0 -0
  12. {fosslight_binary-5.1.30 → fosslight_binary-5.1.31}/LICENSES/LicenseRef-3rd-party-licenses.txt +0 -0
  13. {fosslight_binary-5.1.30 → fosslight_binary-5.1.31}/MANIFEST.in +0 -0
  14. {fosslight_binary-5.1.30 → fosslight_binary-5.1.31}/README.md +0 -0
  15. {fosslight_binary-5.1.30 → fosslight_binary-5.1.31}/setup.cfg +0 -0
  16. {fosslight_binary-5.1.30 → fosslight_binary-5.1.31}/src/fosslight_binary/__init__.py +0 -0
  17. {fosslight_binary-5.1.30 → fosslight_binary-5.1.31}/src/fosslight_binary/_binary.py +0 -0
  18. {fosslight_binary-5.1.30 → fosslight_binary-5.1.31}/src/fosslight_binary/_simple_mode.py +0 -0
  19. {fosslight_binary-5.1.30 → fosslight_binary-5.1.31}/src/fosslight_binary/binary_analysis.py +0 -0
  20. {fosslight_binary-5.1.30 → fosslight_binary-5.1.31}/src/fosslight_binary/cli.py +0 -0
  21. {fosslight_binary-5.1.30 → fosslight_binary-5.1.31}/src/fosslight_binary.egg-info/dependency_links.txt +0 -0
  22. {fosslight_binary-5.1.30 → fosslight_binary-5.1.31}/src/fosslight_binary.egg-info/entry_points.txt +0 -0
  23. {fosslight_binary-5.1.30 → fosslight_binary-5.1.31}/src/fosslight_binary.egg-info/top_level.txt +0 -0
  24. {fosslight_binary-5.1.30 → fosslight_binary-5.1.31}/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.31
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.8
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.31"
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.8",
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:
@@ -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,25 @@
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
22
  _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)
23
+ _REQUEST_TIMEOUT = 10 # seconds - used for HEAD / POM download
24
+ _CENTRAL_SEARCH_TIMEOUT = 2.5 # seconds - tight timeout for Search API (retried on timeout)
25
+ _MAVEN_JAR_HTTP_TIMEOUT = (2, 2) # match Util probe timeouts for multi-repo jar checks
23
26
  _MAX_RETRY = 3 # maximum Central API retry attempts per JAR
24
27
  _central_network_warned = False # Flag to suppress repeated network-unavailable warnings within one run
28
+ _COORD_TOKEN = re.compile(r'[A-Za-z0-9._+-]+') # shape of a Maven groupId / artifactId / version
25
29
 
26
30
 
27
31
  def _sha1_of_file(filepath):
@@ -123,13 +127,21 @@ def _search_central_by_sha1(sha1, timeout=None):
123
127
  if not docs:
124
128
  return {}, False
125
129
  doc = docs[0]
130
+ groupId = doc.get("g", "")
131
+ artifactId = doc.get("a", "")
132
+ version = doc.get("v") or doc.get("latestVersion", "")
133
+
134
+ if not (groupId and artifactId and version):
135
+ logger.debug(f"Maven Central returned an incomplete document for {sha1}: {doc}")
136
+ return {}, False
137
+
126
138
  return {
127
- "groupId": doc.get("g", ""),
128
- "artifactId": doc.get("a", ""),
129
- "version": doc.get("v") or doc.get("latestVersion", ""),
139
+ "groupId": groupId,
140
+ "artifactId": artifactId,
141
+ "version": version,
130
142
  }, False
131
143
  except requests.exceptions.Timeout:
132
- logger.debug(f"Maven Central SHA-1 search timed out ({sha1}) – will retry")
144
+ logger.debug(f"Maven Central SHA-1 search timed out ({sha1}) - will retry")
133
145
  return {}, True
134
146
  except Exception as ex:
135
147
  if _is_network_error(ex):
@@ -157,7 +169,7 @@ def _download_pom_to_tempfile(group_id, artifact_id, version, timeout=None):
157
169
  logger.debug(f"POM downloaded to {tmp.name} from {url}")
158
170
  return tmp.name, False
159
171
  except requests.exceptions.Timeout:
160
- logger.debug(f"POM download timed out from {url} – will retry")
172
+ logger.debug(f"POM download timed out from {url} - will retry")
161
173
  any_timeout = True
162
174
  except Exception as ex:
163
175
  if _is_network_error(ex):
@@ -167,40 +179,69 @@ def _download_pom_to_tempfile(group_id, artifact_id, version, timeout=None):
167
179
  return None, any_timeout
168
180
 
169
181
 
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"
182
+ def _is_maven_coordinate(*parts):
183
+ """True only if every part could be a Maven coordinate token.
184
+
185
+ MANIFEST.MF vendor fields are display names ("The Apache Software
186
+ Foundation", "Google, Inc.", "%bundleVendor"), not groupIds. Rejecting them
187
+ keeps bogus URLs out of the report and avoids HEAD requests that can only 404.
188
+ """
189
+ return all(p and _COORD_TOKEN.fullmatch(p) for p in parts)
175
190
 
176
191
 
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)
192
+ def _maven_jar_url_exists(url, timeout=None):
193
+ """True if a Maven host serves this jar URL (HEAD, with GET fallback)."""
194
+ _timeout = timeout if timeout is not None else _MAVEN_JAR_HTTP_TIMEOUT
181
195
  try:
182
- resp = requests.head(url, timeout=_REQUEST_TIMEOUT, allow_redirects=True)
183
- return resp.status_code == 200
196
+ resp = requests.head(url, timeout=_timeout, allow_redirects=True)
197
+ if resp.status_code == 200:
198
+ return True
199
+ # Some Maven hosts reject HEAD; fall back to a streamed GET (Util pattern).
200
+ if resp.status_code in (403, 405, 501):
201
+ resp = requests.get(url, stream=True, allow_redirects=True, timeout=_timeout)
202
+ try:
203
+ return resp.status_code == 200
204
+ finally:
205
+ resp.close()
184
206
  except Exception as ex:
185
207
  if _is_network_error(ex):
186
- _warn_network_once(f"existence check: {group_id}:{artifact_id}:{version}")
208
+ _warn_network_once(f"jar existence check: {url}")
187
209
  else:
188
- logger.debug(f"Central existence check failed ({group_id}:{artifact_id}:{version}): {ex}")
189
- return False
210
+ logger.debug(f"Maven jar existence check failed ({url}): {ex}")
211
+ return False
212
+
190
213
 
214
+ def _find_jar_download_url(group_id, artifact_id, version):
215
+ """Return the first known-repo URL that hosts ``{artifact}-{version}.jar``.
216
+ Uses FOSSLight Util's repository order (group hints + MAVEN_REPOSITORY_BASES)
217
+ so non-Central artifacts (Google, Confluent, Spring, …) can still fill
218
+ Download Location.
219
+ """
220
+ if not _is_maven_coordinate(group_id, artifact_id, version):
221
+ return ""
222
+ group_path = group_id.replace('.', '/')
223
+ jar_name = f"{artifact_id}-{version}.jar"
224
+ for repo_base in _maven_repo_bases_for(group_path):
225
+ url = f"{repo_base}/{group_path}/{artifact_id}/{version}/{jar_name}"
226
+ if _maven_jar_url_exists(url):
227
+ logger.debug(f"Maven jar found: {url}")
228
+ return url
229
+ return ""
191
230
 
192
- def _process_one_jar(jar_path, rel_path, sha1, search_timeout=None, skip_central=False):
231
+
232
+ def _process_one_jar(jar_path, rel_path, sha1, search_timeout=None, skip_central_search=False):
193
233
  groupId = artifactId = version = project_url = license_str = ''
194
234
  confirmed_in_central = False
235
+ trusted_coordinates = False
195
236
  source = ''
196
237
 
197
- if skip_central:
238
+ if skip_central_search:
198
239
  central_info = {}
199
240
  timed_out = False
200
241
  else:
201
242
  central_info, timed_out = _search_central_by_sha1(sha1, timeout=search_timeout)
202
243
  if timed_out:
203
- logger.debug(f"{rel_path}: Central SHA-1 search timed out – will retry")
244
+ logger.debug(f"{rel_path}: Central SHA-1 search timed out - will retry")
204
245
  return None, True
205
246
 
206
247
  g2, a2, v2, url2, pom_tmp_path = _read_pom_from_jar(jar_path)
@@ -215,10 +256,11 @@ def _process_one_jar(jar_path, rel_path, sha1, search_timeout=None, skip_central
215
256
  names_match = (central_oss_name == jar_oss_name and c_version == v2)
216
257
 
217
258
  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")
259
+ logger.debug(f"{rel_path}: Central and JAR pom.xml match ({central_oss_name} {c_version}) - using JAR pom.xml for license")
219
260
  groupId, artifactId, version, project_url = g2, a2, v2, url2
220
261
  source = 'pom.xml'
221
262
  confirmed_in_central = True
263
+ trusted_coordinates = True
222
264
 
223
265
  if pom_tmp_path:
224
266
  try:
@@ -239,11 +281,12 @@ def _process_one_jar(jar_path, rel_path, sha1, search_timeout=None, skip_central
239
281
  groupId, artifactId, version = c_groupId, c_artifactId, c_version
240
282
  source = 'Maven Central'
241
283
  confirmed_in_central = True
284
+ trusted_coordinates = True
242
285
 
243
286
  tmp_path, timed_out = _download_pom_to_tempfile(
244
287
  groupId, artifactId, version, timeout=search_timeout)
245
288
  if timed_out:
246
- logger.debug(f"{rel_path}: POM download timed out – will retry")
289
+ logger.debug(f"{rel_path}: POM download timed out - will retry")
247
290
  if pom_tmp_path:
248
291
  try:
249
292
  os.remove(pom_tmp_path)
@@ -271,11 +314,12 @@ def _process_one_jar(jar_path, rel_path, sha1, search_timeout=None, skip_central
271
314
  pass
272
315
 
273
316
  else:
274
- logger.debug(f"{rel_path}: not found in Maven Central – falling back to JAR internals")
317
+ logger.debug(f"{rel_path}: not found in Maven Central - falling back to JAR internals")
275
318
 
276
319
  if g2 or a2:
277
320
  groupId, artifactId, version, project_url = g2, a2, v2, url2
278
321
  source = 'pom.xml'
322
+ trusted_coordinates = True
279
323
 
280
324
  if pom_tmp_path:
281
325
  try:
@@ -291,8 +335,6 @@ def _process_one_jar(jar_path, rel_path, sha1, search_timeout=None, skip_central
291
335
  os.remove(pom_tmp_path)
292
336
  except Exception:
293
337
  pass
294
- if not confirmed_in_central and not skip_central:
295
- confirmed_in_central = _exists_in_central(groupId, artifactId, version)
296
338
 
297
339
  if not (groupId and artifactId):
298
340
  g3, a3, v3, url3 = _read_manifest_from_jar(jar_path)
@@ -300,14 +342,20 @@ def _process_one_jar(jar_path, rel_path, sha1, search_timeout=None, skip_central
300
342
  groupId = g3
301
343
  artifactId = a3
302
344
  version = version or v3
345
+ confirmed_in_central = False
303
346
  project_url = project_url or url3
304
347
  source = 'MANIFEST.MF'
348
+ trusted_coordinates = False
305
349
 
306
350
  if not (groupId or artifactId):
307
351
  return None, False
308
352
 
353
+ if confirmed_in_central or trusted_coordinates:
354
+ dl_url = _find_jar_download_url(groupId, artifactId, version)
355
+ else:
356
+ dl_url = ""
357
+
309
358
  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
359
 
312
360
  oss = OssItem(oss_name, version, license_str, dl_url)
313
361
  oss.comment = source
@@ -345,7 +393,7 @@ def analyze_jar_file(path_to_find_bin, path_to_exclude):
345
393
  jar_files.append(os.path.join(root_dir, fname))
346
394
 
347
395
  if not jar_files:
348
- logger.info("No .jar files found – skipping JAR OSS analysis.")
396
+ logger.info("No .jar files found - skipping JAR OSS analysis.")
349
397
  return jar_items, success
350
398
 
351
399
  for jar_path in jar_files:
@@ -361,7 +409,7 @@ def analyze_jar_file(path_to_find_bin, path_to_exclude):
361
409
  jar_path, rel_path, sha1, search_timeout=_CENTRAL_SEARCH_TIMEOUT)
362
410
 
363
411
  if needs_retry:
364
- logger.debug(f"{rel_path}: Central API timed out – queued for retry (attempt 1/{_MAX_RETRY})")
412
+ logger.debug(f"{rel_path}: Central API timed out - queued for retry (attempt 1/{_MAX_RETRY})")
365
413
  pending_sha1s.add(sha1)
366
414
  retry_queue.append((jar_path, rel_path, sha1, 1))
367
415
  elif result is not None:
@@ -373,8 +421,8 @@ def analyze_jar_file(path_to_find_bin, path_to_exclude):
373
421
  if attempt >= _MAX_RETRY:
374
422
  logger.warning(
375
423
  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)
424
+ " - falling back to JAR internals")
425
+ result, _ = _process_one_jar(jar_path, rel_path, sha1, skip_central_search=True)
378
426
  if result is not None:
379
427
  _store_jar_result(jar_items, sha1, result)
380
428
  continue
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fosslight_binary
3
- Version: 5.1.30
3
+ Version: 5.1.31
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.8
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
@@ -19,4 +19,5 @@ src/fosslight_binary.egg-info/dependency_links.txt
19
19
  src/fosslight_binary.egg-info/entry_points.txt
20
20
  src/fosslight_binary.egg-info/requires.txt
21
21
  src/fosslight_binary.egg-info/top_level.txt
22
- tests/test_fosslight_binary.py
22
+ tests/test_fosslight_binary.py
23
+ 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.8
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)