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.
- {fosslight_binary-5.1.29 → fosslight_binary-5.1.31}/PKG-INFO +3 -3
- {fosslight_binary-5.1.29 → fosslight_binary-5.1.31}/pyproject.toml +3 -3
- fosslight_binary-5.1.31/src/fosslight_binary/_binary_dao.py +309 -0
- {fosslight_binary-5.1.29 → fosslight_binary-5.1.31}/src/fosslight_binary/_help.py +6 -3
- {fosslight_binary-5.1.29 → fosslight_binary-5.1.31}/src/fosslight_binary/_jar_analysis.py +82 -34
- {fosslight_binary-5.1.29 → fosslight_binary-5.1.31}/src/fosslight_binary/binary_analysis.py +4 -2
- {fosslight_binary-5.1.29 → fosslight_binary-5.1.31}/src/fosslight_binary/cli.py +13 -5
- {fosslight_binary-5.1.29 → fosslight_binary-5.1.31}/src/fosslight_binary.egg-info/PKG-INFO +3 -3
- {fosslight_binary-5.1.29 → fosslight_binary-5.1.31}/src/fosslight_binary.egg-info/SOURCES.txt +2 -1
- {fosslight_binary-5.1.29 → fosslight_binary-5.1.31}/src/fosslight_binary.egg-info/requires.txt +2 -2
- fosslight_binary-5.1.31/tests/test_kb_ssl.py +54 -0
- fosslight_binary-5.1.29/src/fosslight_binary/_binary_dao.py +0 -186
- {fosslight_binary-5.1.29 → fosslight_binary-5.1.31}/LICENSE +0 -0
- {fosslight_binary-5.1.29 → fosslight_binary-5.1.31}/LICENSES/Apache-2.0.txt +0 -0
- {fosslight_binary-5.1.29 → fosslight_binary-5.1.31}/LICENSES/LicenseRef-3rd-party-licenses.txt +0 -0
- {fosslight_binary-5.1.29 → fosslight_binary-5.1.31}/MANIFEST.in +0 -0
- {fosslight_binary-5.1.29 → fosslight_binary-5.1.31}/README.md +0 -0
- {fosslight_binary-5.1.29 → fosslight_binary-5.1.31}/setup.cfg +0 -0
- {fosslight_binary-5.1.29 → fosslight_binary-5.1.31}/src/fosslight_binary/__init__.py +0 -0
- {fosslight_binary-5.1.29 → fosslight_binary-5.1.31}/src/fosslight_binary/_binary.py +0 -0
- {fosslight_binary-5.1.29 → fosslight_binary-5.1.31}/src/fosslight_binary/_simple_mode.py +0 -0
- {fosslight_binary-5.1.29 → fosslight_binary-5.1.31}/src/fosslight_binary.egg-info/dependency_links.txt +0 -0
- {fosslight_binary-5.1.29 → fosslight_binary-5.1.31}/src/fosslight_binary.egg-info/entry_points.txt +0 -0
- {fosslight_binary-5.1.29 → fosslight_binary-5.1.31}/src/fosslight_binary.egg-info/top_level.txt +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
#
|
|
52
|
-
fosslight_binary
|
|
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
|
|
22
|
-
_CENTRAL_SEARCH_TIMEOUT = 2.5 # seconds
|
|
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":
|
|
128
|
-
"artifactId":
|
|
129
|
-
"version":
|
|
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})
|
|
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}
|
|
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
|
|
171
|
-
if
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
|
178
|
-
if
|
|
179
|
-
|
|
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=
|
|
183
|
-
|
|
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: {
|
|
208
|
+
_warn_network_once(f"jar existence check: {url}")
|
|
187
209
|
else:
|
|
188
|
-
logger.debug(f"
|
|
189
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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})
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
"
|
|
377
|
-
result, _ = _process_one_jar(jar_path, rel_path, sha1,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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.
|
|
85
|
-
|
|
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(
|
|
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.
|
|
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.
|
|
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
|
{fosslight_binary-5.1.29 → fosslight_binary-5.1.31}/src/fosslight_binary.egg-info/SOURCES.txt
RENAMED
|
@@ -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
|
|
@@ -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 = ""
|
|
File without changes
|
|
File without changes
|
{fosslight_binary-5.1.29 → fosslight_binary-5.1.31}/LICENSES/LicenseRef-3rd-party-licenses.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fosslight_binary-5.1.29 → fosslight_binary-5.1.31}/src/fosslight_binary.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{fosslight_binary-5.1.29 → fosslight_binary-5.1.31}/src/fosslight_binary.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|