fosslight-binary 5.1.29__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 (25) hide show
  1. {fosslight_binary-5.1.29 → fosslight_binary-5.1.31}/PKG-INFO +3 -3
  2. {fosslight_binary-5.1.29 → fosslight_binary-5.1.31}/pyproject.toml +3 -3
  3. fosslight_binary-5.1.31/src/fosslight_binary/_binary_dao.py +309 -0
  4. {fosslight_binary-5.1.29 → fosslight_binary-5.1.31}/src/fosslight_binary/_help.py +6 -3
  5. {fosslight_binary-5.1.29 → fosslight_binary-5.1.31}/src/fosslight_binary/_jar_analysis.py +82 -34
  6. {fosslight_binary-5.1.29 → fosslight_binary-5.1.31}/src/fosslight_binary/binary_analysis.py +4 -2
  7. {fosslight_binary-5.1.29 → fosslight_binary-5.1.31}/src/fosslight_binary/cli.py +13 -5
  8. {fosslight_binary-5.1.29 → fosslight_binary-5.1.31}/src/fosslight_binary.egg-info/PKG-INFO +3 -3
  9. {fosslight_binary-5.1.29 → fosslight_binary-5.1.31}/src/fosslight_binary.egg-info/SOURCES.txt +2 -1
  10. {fosslight_binary-5.1.29 → fosslight_binary-5.1.31}/src/fosslight_binary.egg-info/requires.txt +2 -2
  11. fosslight_binary-5.1.31/tests/test_kb_ssl.py +54 -0
  12. fosslight_binary-5.1.29/src/fosslight_binary/_binary_dao.py +0 -186
  13. {fosslight_binary-5.1.29 → fosslight_binary-5.1.31}/LICENSE +0 -0
  14. {fosslight_binary-5.1.29 → fosslight_binary-5.1.31}/LICENSES/Apache-2.0.txt +0 -0
  15. {fosslight_binary-5.1.29 → fosslight_binary-5.1.31}/LICENSES/LicenseRef-3rd-party-licenses.txt +0 -0
  16. {fosslight_binary-5.1.29 → fosslight_binary-5.1.31}/MANIFEST.in +0 -0
  17. {fosslight_binary-5.1.29 → fosslight_binary-5.1.31}/README.md +0 -0
  18. {fosslight_binary-5.1.29 → fosslight_binary-5.1.31}/setup.cfg +0 -0
  19. {fosslight_binary-5.1.29 → fosslight_binary-5.1.31}/src/fosslight_binary/__init__.py +0 -0
  20. {fosslight_binary-5.1.29 → fosslight_binary-5.1.31}/src/fosslight_binary/_binary.py +0 -0
  21. {fosslight_binary-5.1.29 → fosslight_binary-5.1.31}/src/fosslight_binary/_simple_mode.py +0 -0
  22. {fosslight_binary-5.1.29 → fosslight_binary-5.1.31}/src/fosslight_binary.egg-info/dependency_links.txt +0 -0
  23. {fosslight_binary-5.1.29 → fosslight_binary-5.1.31}/src/fosslight_binary.egg-info/entry_points.txt +0 -0
  24. {fosslight_binary-5.1.29 → fosslight_binary-5.1.31}/src/fosslight_binary.egg-info/top_level.txt +0 -0
  25. {fosslight_binary-5.1.29 → 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.29
3
+ Version: 5.1.31
4
4
  Summary: FOSSLight Binary Scanner
5
5
  Author: LG Electronics
6
6
  License-Expression: Apache-2.0
@@ -24,13 +24,13 @@ Requires-Dist: defusedxml
24
24
  Requires-Dist: numpy
25
25
  Requires-Dist: pandas
26
26
  Requires-Dist: parmap
27
- Requires-Dist: psycopg2-binary
28
27
  Requires-Dist: python-dateutil
29
28
  Requires-Dist: py-tlsh
30
29
  Requires-Dist: pytz
31
30
  Requires-Dist: XlsxWriter
32
31
  Requires-Dist: PyYAML
33
- Requires-Dist: fosslight_util>=2.2.2
32
+ Requires-Dist: fosslight_util>=2.2.8
33
+ Requires-Dist: truststore
34
34
  Requires-Dist: python-magic-bin; sys_platform == "win32"
35
35
  Requires-Dist: python-magic; "darwin" in sys_platform
36
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.29"
10
+ version = "5.1.31"
11
11
  description = "FOSSLight Binary Scanner"
12
12
  readme = "README.md"
13
13
  license = "Apache-2.0"
@@ -31,13 +31,13 @@ dependencies = [
31
31
  "numpy",
32
32
  "pandas",
33
33
  "parmap",
34
- "psycopg2-binary",
35
34
  "python-dateutil",
36
35
  "py-tlsh",
37
36
  "pytz",
38
37
  "XlsxWriter",
39
38
  "PyYAML",
40
- "fosslight_util>=2.2.2",
39
+ "fosslight_util>=2.2.8",
40
+ "truststore",
41
41
  "python-magic-bin; sys_platform == 'win32'",
42
42
  "python-magic; 'darwin' in sys_platform",
43
43
  "python-magic; 'linux' in sys_platform",
@@ -0,0 +1,309 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ # Copyright (c) 2020 LG Electronics Inc.
4
+ # SPDX-License-Identifier: Apache-2.0
5
+ """Binary DB lookup via ldb_service POST /binary/match."""
6
+
7
+ import json
8
+ import logging
9
+ import os
10
+ import ssl
11
+ import urllib.error
12
+ import urllib.request
13
+ from typing import Dict, List, Optional, Tuple
14
+ from urllib.parse import urlparse
15
+
16
+ from fosslight_binary._binary import TLSH_CHECKSUM_NULL
17
+ from fosslight_util.oss_item import OssItem
18
+ import fosslight_util.constant as constant
19
+
20
+ logger = logging.getLogger(constant.LOGGER_NAME)
21
+
22
+ DEFAULT_KB_URL = "http://fosslight-kb.lge.com/"
23
+ _BINARY_MATCH_PATH = "/binary/match"
24
+ _HTTP_TIMEOUT_SEC = 120
25
+ _PROBE_TIMEOUT_SEC = 10
26
+ _DEFAULT_CHUNK_SIZE = 3000
27
+
28
+
29
+ def _get_chunk_size() -> int:
30
+ raw = os.environ.get("BINARY_MATCH_CHUNK_SIZE", str(_DEFAULT_CHUNK_SIZE))
31
+ try:
32
+ chunk_size = int(raw)
33
+ except ValueError:
34
+ logger.warning(f"Invalid BINARY_MATCH_CHUNK_SIZE={raw!r}; using {_DEFAULT_CHUNK_SIZE}")
35
+ return _DEFAULT_CHUNK_SIZE
36
+ if chunk_size <= 0:
37
+ logger.warning(
38
+ f"BINARY_MATCH_CHUNK_SIZE must be > 0 (got {chunk_size}); using {_DEFAULT_CHUNK_SIZE}"
39
+ )
40
+ return _DEFAULT_CHUNK_SIZE
41
+ return chunk_size
42
+
43
+
44
+ _CHUNK_SIZE = _get_chunk_size()
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()
72
+
73
+
74
+ def resolve_kb_config(kb_url: str = "", kb_token: str = "") -> Tuple[str, str]:
75
+ url = (kb_url or os.environ.get("KB_URL", DEFAULT_KB_URL)).strip() or DEFAULT_KB_URL
76
+ token = (kb_token or "").strip() or (os.environ.get("KB_TOKEN") or "").strip()
77
+ return f"{url.rstrip('/')}/", token
78
+
79
+
80
+ def _is_valid_kb_url_format(kb_url: str) -> bool:
81
+ parsed = urlparse(kb_url)
82
+ return parsed.scheme in ("http", "https") and bool(parsed.netloc)
83
+
84
+
85
+ def check_binary_match_endpoint(kb_url: str, kb_token: str = "") -> Tuple[bool, str]:
86
+ """
87
+ Return (available, cover_comment).
88
+
89
+ cover_comment:
90
+ - ``KB({kb_url}) Unreachable`` when the host cannot be reached
91
+ - ``KB({kb_url}) Skipped`` when /binary/match is missing (HTTP 404)
92
+ - empty for other skip cases (invalid URL)
93
+ """
94
+ if not _is_valid_kb_url_format(kb_url):
95
+ logger.warning(f"Invalid KB URL format: {kb_url}")
96
+ return False, ""
97
+
98
+ endpoint = f"{kb_url.rstrip('/')}{_BINARY_MATCH_PATH}"
99
+ data = json.dumps({"items": []}).encode("utf-8")
100
+ request = urllib.request.Request(endpoint, data=data, method="POST")
101
+ request.add_header("Accept", "application/json")
102
+ request.add_header("Content-Type", "application/json")
103
+ if kb_token:
104
+ request.add_header("Authorization", f"Bearer {kb_token}")
105
+
106
+ try:
107
+ with urllib.request.urlopen(
108
+ request, timeout=_PROBE_TIMEOUT_SEC, context=create_kb_ssl_context()
109
+ ) as response:
110
+ response.read()
111
+ return True, ""
112
+ except urllib.error.HTTPError as ex:
113
+ if ex.code == 404:
114
+ logger.warning(
115
+ f"KB binary match endpoint not found (HTTP 404): {endpoint}. "
116
+ "Skipping binary match API."
117
+ )
118
+ return False, f"KB({kb_url}) Skipped"
119
+ logger.debug(
120
+ f"KB binary match endpoint responded with HTTP {ex.code}; treated as available"
121
+ )
122
+ return True, ""
123
+ except urllib.error.URLError as ex:
124
+ logger.warning(f"KB binary match endpoint unreachable: {ex}. Skipping binary match API.")
125
+ return False, f"KB({kb_url}) Unreachable"
126
+ except Exception as ex:
127
+ logger.warning(
128
+ f"Failed to check KB binary match endpoint: {ex}. Skipping binary match API."
129
+ )
130
+ return False, f"KB({kb_url}) Unreachable"
131
+
132
+
133
+ def _is_unknown_checksum(checksum: str) -> bool:
134
+ """True when checksum was not computed (empty or TLSH_CHECKSUM_NULL)."""
135
+ return (not checksum) or checksum == TLSH_CHECKSUM_NULL
136
+
137
+
138
+ def _is_unknown_tlsh(tlsh: str) -> bool:
139
+ """True when tlsh was not computed (empty or TLSH_CHECKSUM_NULL)."""
140
+ return (not tlsh) or tlsh == TLSH_CHECKSUM_NULL
141
+
142
+
143
+ def _match_key(filename: str, checksum: str, index: int) -> MatchKey:
144
+ """Dedupe key. Unknown checksums stay unique per list index (no filename-only merge)."""
145
+ if _is_unknown_checksum(checksum):
146
+ return filename, f"__unknown_{index}"
147
+ return filename, checksum
148
+
149
+
150
+ def _build_deduped_payload(
151
+ bin_info_list,
152
+ tlsh_null: str,
153
+ ) -> Tuple[List[dict], Dict[MatchKey, str]]:
154
+ """Deduplicate by filename+checksum; return API payload and key→api_id map.
155
+
156
+ Items with empty/\"0\" checksum are not deduped — each keeps its own API entry
157
+ (and its own tlsh) so distinct files that share a basename are not conflated.
158
+ Items with both checksum and tlsh unknown are omitted (nothing to match).
159
+ """
160
+ key_to_id: Dict[MatchKey, str] = {}
161
+ items_payload: List[dict] = []
162
+
163
+ for index, item in enumerate(bin_info_list):
164
+ if item.exclude:
165
+ continue
166
+ filename = item.binary_name_without_path
167
+ checksum = item.checksum or ""
168
+ tlsh = item.tlsh or tlsh_null
169
+ if _is_unknown_checksum(checksum) and _is_unknown_tlsh(tlsh):
170
+ continue
171
+ key = _match_key(filename, checksum, index)
172
+ if not _is_unknown_checksum(checksum) and key in key_to_id:
173
+ continue
174
+ api_id = str(len(items_payload))
175
+ key_to_id[key] = api_id
176
+ items_payload.append({
177
+ "id": api_id,
178
+ "filename": filename,
179
+ "checksum": checksum,
180
+ "tlsh": tlsh,
181
+ })
182
+
183
+ return items_payload, key_to_id
184
+
185
+
186
+ def _apply_match_result_to_item(item, result: dict) -> bool:
187
+ """Apply a /binary/match result to one binary item. Returns True if matched."""
188
+ if not result or not result.get("matched"):
189
+ return False
190
+ oss_rows = result.get("oss_items") or []
191
+ if not oss_rows:
192
+ return False
193
+
194
+ if not item.found_in_jar_analysis and item.oss_items:
195
+ item.oss_items = []
196
+
197
+ bin_oss_items = []
198
+ for row in oss_rows:
199
+ if item.found_in_jar_analysis:
200
+ break
201
+ oss_from_db = OssItem(
202
+ row.get("oss_name") or "",
203
+ row.get("oss_version") or "",
204
+ row.get("license") or "",
205
+ )
206
+ if bin_oss_items:
207
+ if not any(
208
+ oss_item.name == oss_from_db.name
209
+ and oss_item.version == oss_from_db.version
210
+ and oss_item.license == oss_from_db.license
211
+ for oss_item in bin_oss_items
212
+ ):
213
+ bin_oss_items.append(oss_from_db)
214
+ else:
215
+ bin_oss_items.append(oss_from_db)
216
+
217
+ if bin_oss_items:
218
+ item.set_oss_items(bin_oss_items)
219
+ item.comment = "Binary DB result"
220
+ item.found_in_bin_db = True
221
+ return True
222
+ return False
223
+
224
+
225
+ def get_oss_info_from_db(bin_info_list, kb_url: str = "", kb_token: str = ""):
226
+ """
227
+ Call ldb_service /binary/match and attach OSS info to binary items.
228
+ Deduplicates by filename+checksum before the API call and maps results back.
229
+ Returns (bin_info_list, matched_count, kb_cover_comment).
230
+ """
231
+ _cnt_auto_identified = 0
232
+ if not bin_info_list:
233
+ return bin_info_list, _cnt_auto_identified, ""
234
+
235
+ base_url, token = resolve_kb_config(kb_url, kb_token)
236
+ available, kb_cover_comment = check_binary_match_endpoint(base_url, token)
237
+ if not available:
238
+ return bin_info_list, _cnt_auto_identified, kb_cover_comment
239
+
240
+ items_payload, key_to_id = _build_deduped_payload(bin_info_list, TLSH_CHECKSUM_NULL)
241
+ if not items_payload:
242
+ return bin_info_list, _cnt_auto_identified, ""
243
+
244
+ results_by_id = {}
245
+ try:
246
+ for chunk_start in range(0, len(items_payload), _CHUNK_SIZE):
247
+ chunk = items_payload[chunk_start: chunk_start + _CHUNK_SIZE]
248
+ response = _post_binary_match(base_url, token, chunk)
249
+ if response is None:
250
+ logger.warning(
251
+ f"Binary match chunk failed "
252
+ f"({chunk_start}:{chunk_start + len(chunk)}); "
253
+ "keeping results so far and continuing with next chunks."
254
+ )
255
+ continue
256
+ for result in response.get("results", []):
257
+ results_by_id[str(result.get("id"))] = result
258
+ except Exception as ex:
259
+ logger.warning(f"Binary match API failed: {ex}")
260
+
261
+ for index, item in enumerate(bin_info_list):
262
+ if item.exclude:
263
+ continue
264
+ key = _match_key(item.binary_name_without_path, item.checksum or "", index)
265
+ api_id = key_to_id.get(key)
266
+ if api_id is None:
267
+ continue
268
+ result = results_by_id.get(api_id)
269
+ if _apply_match_result_to_item(item, result):
270
+ _cnt_auto_identified += 1
271
+
272
+ return bin_info_list, _cnt_auto_identified, ""
273
+
274
+
275
+ def _post_binary_match(kb_url: str, kb_token: str, items: list) -> Optional[dict]:
276
+ data = json.dumps({"items": items}).encode("utf-8")
277
+ request = urllib.request.Request(
278
+ f"{kb_url.rstrip('/')}{_BINARY_MATCH_PATH}",
279
+ data=data,
280
+ method="POST",
281
+ )
282
+ request.add_header("Accept", "application/json")
283
+ request.add_header("Content-Type", "application/json")
284
+ if kb_token:
285
+ request.add_header("Authorization", f"Bearer {kb_token}")
286
+
287
+ try:
288
+ with urllib.request.urlopen(
289
+ request, timeout=_HTTP_TIMEOUT_SEC, context=create_kb_ssl_context()
290
+ ) as response:
291
+ body = response.read().decode()
292
+ return json.loads(body) if body else {}
293
+ except urllib.error.HTTPError as ex:
294
+ body = ""
295
+ try:
296
+ body = ex.read().decode()
297
+ except Exception:
298
+ pass
299
+ if ex.code == 404:
300
+ logger.warning(
301
+ f"Binary match endpoint not found (HTTP 404): "
302
+ f"{kb_url.rstrip('/')}{_BINARY_MATCH_PATH}"
303
+ )
304
+ else:
305
+ logger.warning(f"Binary match HTTP {ex.code}: {body or ex.reason}")
306
+ return None
307
+ except urllib.error.URLError as ex:
308
+ logger.debug(f"Binary match unreachable: {ex}")
309
+ return None
@@ -32,7 +32,10 @@ _HELP_MESSAGE_BINARY = f"""
32
32
 
33
33
  🔍 Scanner-Specific Options
34
34
  ────────────────────────────────────────────────────────────────────
35
- -d <db_url> DB Connection (format: 'postgresql://user:pass@host:port/db')
35
+ --kb_url <url> KB API URL (priority: parameter > KB_URL env > default)
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.
36
39
  --notice Print the open source license notice text
37
40
  --no_correction Skip OSS information correction with sbom-info.yaml
38
41
  --correct_fpath <path> Path to custom sbom-info.yaml file
@@ -48,8 +51,8 @@ _HELP_MESSAGE_BINARY = f"""
48
51
  # Generate output in specific format
49
52
  fosslight_binary -f excel -o results/
50
53
 
51
- # Connect to Binary DB for OSS information
52
- fosslight_binary -d "postgresql://user:pass@localhost:5432/exampledb"
54
+ # Binary DB lookup via ldb_service
55
+ fosslight_binary --kb_url http://fosslight-kb.lge.com/ --kb_token <token>
53
56
  """
54
57
 
55
58
 
@@ -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
@@ -169,7 +169,7 @@ def get_file_list(path_to_find, excluded_files):
169
169
  return file_cnt, bin_list, found_jar
170
170
 
171
171
 
172
- def find_binaries(path_to_find_bin, output_dir, formats, dburl="", simple_mode=False,
172
+ def find_binaries(path_to_find_bin, output_dir, formats, kb_url="", kb_token="", simple_mode=False,
173
173
  correct_mode=True, correct_filepath="", path_to_exclude=[],
174
174
  all_exclude_mode=()):
175
175
  global start_time, finish_time, _root_path, _result_log
@@ -254,7 +254,7 @@ def find_binaries(path_to_find_bin, output_dir, formats, dburl="", simple_mode=F
254
254
  else:
255
255
  logger.warning("Could not find OSS information for some jar files.")
256
256
 
257
- return_list, db_loaded_cnt = get_oss_info_from_db(return_list, dburl)
257
+ return_list, db_loaded_cnt, kb_cover_msg = get_oss_info_from_db(return_list, kb_url, kb_token)
258
258
  return_list = sorted(return_list, key=lambda row: (row.bin_name_with_path))
259
259
  scan_item.append_file_items(return_list, PKG_NAME)
260
260
  if correct_mode:
@@ -267,6 +267,8 @@ def find_binaries(path_to_find_bin, output_dir, formats, dburl="", simple_mode=F
267
267
 
268
268
  finish_time = current_timestamp_utc()
269
269
  scan_item.set_cover_comment(f"Detected binaries: {len(return_list)} (Scanned Files : {cnt_file_except_skipped})")
270
+ if kb_cover_msg:
271
+ scan_item.set_cover_comment(kb_cover_msg)
270
272
  scan_item.set_cover_finish_time(finish_time)
271
273
 
272
274
  for combined_path_and_file, output_extension, output_format in zip(result_reports, output_extensions, formats):
@@ -36,7 +36,8 @@ def main():
36
36
  path_to_exclude = []
37
37
  output_dir = ""
38
38
  format = []
39
- db_url = ""
39
+ kb_url = ""
40
+ kb_token = ""
40
41
  simple_mode = False
41
42
  correct_mode = True
42
43
 
@@ -46,9 +47,10 @@ def main():
46
47
  parser.add_argument('-s', '--simple', action='store_true', required=False, help=argparse.SUPPRESS)
47
48
  parser.add_argument('-p', '--path', type=str, required=False)
48
49
  parser.add_argument('-o', '--output', type=str, required=False)
49
- parser.add_argument('-d', '--dburl', type=str, default='', required=False)
50
50
  parser.add_argument('-f', '--formats', type=str, required=False, nargs="*")
51
51
  parser.add_argument('-e', '--exclude', nargs="*", required=False, default=[])
52
+ parser.add_argument('--kb_url', type=str, required=False, default="")
53
+ parser.add_argument('--kb_token', type=str, required=False, default="")
52
54
  parser.add_argument('--notice', action='store_true', required=False)
53
55
  parser.add_argument('--no_correction', action='store_true', required=False)
54
56
  parser.add_argument('--correct_fpath', nargs=1, type=str, required=False)
@@ -81,8 +83,11 @@ def main():
81
83
  if args.output: # -o option
82
84
  output_dir = args.output
83
85
 
84
- if args.dburl: # -d option
85
- db_url = args.dburl
86
+ if args.kb_url:
87
+ kb_url = args.kb_url
88
+
89
+ if args.kb_token:
90
+ kb_token = args.kb_token
86
91
 
87
92
  if args.formats: # -f option
88
93
  format = list(args.formats)
@@ -113,7 +118,10 @@ def main():
113
118
  timer.setDaemon(True)
114
119
  timer.start()
115
120
 
116
- find_binaries(path_to_find_bin, output_dir, format, db_url, simple_mode, correct_mode, correct_filepath, path_to_exclude)
121
+ find_binaries(
122
+ path_to_find_bin, output_dir, format, kb_url, kb_token,
123
+ simple_mode, correct_mode, correct_filepath, path_to_exclude,
124
+ )
117
125
 
118
126
 
119
127
  if __name__ == '__main__':
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fosslight_binary
3
- Version: 5.1.29
3
+ Version: 5.1.31
4
4
  Summary: FOSSLight Binary Scanner
5
5
  Author: LG Electronics
6
6
  License-Expression: Apache-2.0
@@ -24,13 +24,13 @@ Requires-Dist: defusedxml
24
24
  Requires-Dist: numpy
25
25
  Requires-Dist: pandas
26
26
  Requires-Dist: parmap
27
- Requires-Dist: psycopg2-binary
28
27
  Requires-Dist: python-dateutil
29
28
  Requires-Dist: py-tlsh
30
29
  Requires-Dist: pytz
31
30
  Requires-Dist: XlsxWriter
32
31
  Requires-Dist: PyYAML
33
- Requires-Dist: fosslight_util>=2.2.2
32
+ Requires-Dist: fosslight_util>=2.2.8
33
+ Requires-Dist: truststore
34
34
  Requires-Dist: python-magic-bin; sys_platform == "win32"
35
35
  Requires-Dist: python-magic; "darwin" in sys_platform
36
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
@@ -4,13 +4,13 @@ defusedxml
4
4
  numpy
5
5
  pandas
6
6
  parmap
7
- psycopg2-binary
8
7
  python-dateutil
9
8
  py-tlsh
10
9
  pytz
11
10
  XlsxWriter
12
11
  PyYAML
13
- fosslight_util>=2.2.2
12
+ fosslight_util>=2.2.8
13
+ truststore
14
14
 
15
15
  [:"darwin" in sys_platform]
16
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)
@@ -1,186 +0,0 @@
1
- #!/usr/bin/env python
2
- # -*- coding: utf-8 -*-
3
- # Copyright (c) 2020 LG Electronics Inc.
4
- # SPDX-License-Identifier: Apache-2.0
5
-
6
- import tlsh
7
- import logging
8
- import psycopg2
9
- import pandas as pd
10
- import socket
11
- from urllib.parse import urlparse
12
- from fosslight_binary._binary import TLSH_CHECKSUM_NULL
13
- from fosslight_util.oss_item import OssItem
14
- import fosslight_util.constant as constant
15
-
16
- columns = ['filename', 'pathname', 'checksum', 'tlshchecksum', 'ossname',
17
- 'ossversion', 'license', 'platformname',
18
- 'platformversion']
19
- conn = ""
20
- cur = ""
21
- logger = logging.getLogger(constant.LOGGER_NAME)
22
- DB_URL_DEFAULT = "postgresql://bin_analysis_script_user:script_123@bat.lge.com:5432/bat"
23
-
24
-
25
- def get_oss_info_from_db(bin_info_list, dburl=""):
26
- _cnt_auto_identified = 0
27
- conn_str, dbc = get_connection_string(dburl)
28
- # DB URL에서 host 추출
29
- try:
30
- db_host = dbc.hostname
31
- except Exception as ex:
32
- logger.warning(f"Failed to parse DB URL for host: {ex}")
33
- db_host = None
34
-
35
- is_internal = False
36
- if db_host:
37
- try:
38
- # DNS lookup 시도
39
- socket.gethostbyname(db_host)
40
- is_internal = True
41
- except Exception:
42
- is_internal = False
43
-
44
- if is_internal:
45
- connect_to_lge_bin_db(conn_str)
46
- if conn != "" and cur != "":
47
- for item in bin_info_list:
48
- bin_oss_items = []
49
- tlsh_value = item.tlsh
50
- checksum_value = item.checksum
51
- bin_file_name = item.binary_name_without_path
52
-
53
- df_result = get_oss_info_by_tlsh_and_filename(
54
- bin_file_name, checksum_value, tlsh_value)
55
- if df_result is not None and len(df_result) > 0:
56
- _cnt_auto_identified += 1
57
- # Initialize the saved contents at .jar analyzing only once
58
- if not item.found_in_jar_analysis and item.oss_items:
59
- item.oss_items = []
60
-
61
- for idx, row in df_result.iterrows():
62
- if not item.found_in_jar_analysis:
63
- oss_from_db = OssItem(row['ossname'], row['ossversion'], row['license'])
64
-
65
- if bin_oss_items:
66
- if not any(oss_item.name == oss_from_db.name
67
- and oss_item.version == oss_from_db.version
68
- and oss_item.license == oss_from_db.license
69
- for oss_item in bin_oss_items):
70
- bin_oss_items.append(oss_from_db)
71
- else:
72
- bin_oss_items.append(oss_from_db)
73
-
74
- if bin_oss_items:
75
- item.set_oss_items(bin_oss_items)
76
- item.comment = "Binary DB result"
77
- item.found_in_binary = True
78
- else:
79
- logger.warning(f"Internal network detected, but DB connection to '{db_host}' failed. Skipping DB query.")
80
- disconnect_lge_bin_db()
81
- else:
82
- logger.debug(f"Binary DB host '{db_host}' is not reachable. Skipping DB query.")
83
- return bin_info_list, _cnt_auto_identified
84
-
85
-
86
- def get_connection_string(dburl):
87
- # dburl format : 'postgresql://username:password@host:port/database_name'
88
- connection_string = ""
89
- user_dburl = True
90
- dbc = ""
91
- if dburl == "" or dburl is None:
92
- user_dburl = False
93
- dburl = DB_URL_DEFAULT
94
- try:
95
- if user_dburl:
96
- logger.debug("DB URL:" + dburl)
97
- dbc = urlparse(dburl)
98
- connection_string = "dbname={dbname} user={user} host={host} password={password} port={port}" \
99
- .format(dbname=dbc.path.lstrip('/'),
100
- user=dbc.username,
101
- host=dbc.hostname,
102
- password=dbc.password,
103
- port=dbc.port)
104
- except Exception as ex:
105
- if user_dburl:
106
- logger.warning(f"(Minor) Failed to parsing db url : {ex}")
107
-
108
- return connection_string, dbc
109
-
110
-
111
- def get_oss_info_by_tlsh_and_filename(file_name, checksum_value, tlsh_value):
112
- sql_statement = "SELECT filename,pathname,checksum,tlshchecksum,ossname,ossversion,\
113
- license,platformname,platformversion FROM lgematching "
114
- sql_statement_checksum = " WHERE filename=%s AND checksum=%s;" # Using parameterized query
115
- sql_statement_filename = "SELECT DISTINCT ON (tlshchecksum) tlshchecksum FROM lgematching WHERE filename=%s;" # Using parameterized query
116
-
117
- final_result_item = ""
118
-
119
- df_result = get_list_by_using_query(
120
- sql_statement + sql_statement_checksum, columns, (file_name, checksum_value))
121
- # Found a file with the same checksum.
122
- if df_result is not None and len(df_result) > 0:
123
- final_result_item = df_result
124
- else:
125
- # Match tlsh and fileName
126
- df_result = get_list_by_using_query(
127
- sql_statement_filename, ['tlshchecksum'], (file_name,))
128
- if df_result is None or len(df_result) <= 0:
129
- final_result_item = ""
130
- elif tlsh_value == TLSH_CHECKSUM_NULL: # Couldn't get the tlsh of a file.
131
- final_result_item = ""
132
- else:
133
- matched_tlsh = ""
134
- matched_tlsh_diff = -1
135
- for row in df_result.tlshchecksum:
136
- try:
137
- if row != TLSH_CHECKSUM_NULL:
138
- tlsh_diff = tlsh.diff(row, tlsh_value)
139
- if tlsh_diff <= 120: # MATCHED
140
- if (matched_tlsh_diff < 0) or (tlsh_diff < matched_tlsh_diff):
141
- matched_tlsh_diff = tlsh_diff
142
- matched_tlsh = row
143
- except Exception as ex:
144
- logger.warning(f"* (Minor) Error_tlsh_comparison: {ex}")
145
- if matched_tlsh != "":
146
- final_result_item = get_list_by_using_query(
147
- sql_statement + " WHERE filename=%s AND tlshchecksum=%s;", columns, (file_name, matched_tlsh))
148
-
149
- return final_result_item
150
-
151
-
152
- def get_list_by_using_query(sql_query, columns, params=None):
153
- result_rows = "" # DataFrame
154
- try:
155
- if params:
156
- cur.execute(sql_query, params)
157
- else:
158
- cur.execute(sql_query)
159
- rows = cur.fetchall()
160
-
161
- if rows is not None and len(rows) > 0:
162
- result_rows = pd.DataFrame(data=rows, columns=columns)
163
- except Exception as ex:
164
- logger.error(f"Database query error: {ex}")
165
- result_rows = ""
166
- return result_rows
167
-
168
-
169
- def disconnect_lge_bin_db():
170
- try:
171
- cur.close()
172
- conn.close()
173
- except:
174
- pass
175
-
176
-
177
- def connect_to_lge_bin_db(connection_string):
178
- global conn, cur
179
-
180
- try:
181
- conn = psycopg2.connect(connection_string)
182
- cur = conn.cursor()
183
- except Exception as ex:
184
- logger.debug(f"(Minor) Can't connect to Binary DB. : {ex}")
185
- conn = ""
186
- cur = ""