owasp-depscan 6.0.0a3__py3-none-any.whl → 6.0.0b4__py3-none-any.whl
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.
- depscan/cli.py +36 -251
- depscan/cli_options.py +13 -0
- depscan/lib/bom.py +5 -1
- depscan/lib/explainer.py +43 -19
- depscan/lib/utils.py +3 -38
- {owasp_depscan-6.0.0a3.dist-info → owasp_depscan-6.0.0b4.dist-info}/METADATA +14 -17
- {owasp_depscan-6.0.0a3.dist-info → owasp_depscan-6.0.0b4.dist-info}/RECORD +14 -14
- {owasp_depscan-6.0.0a3.dist-info → owasp_depscan-6.0.0b4.dist-info}/WHEEL +1 -1
- {owasp_depscan-6.0.0a3.dist-info → owasp_depscan-6.0.0b4.dist-info}/entry_points.txt +0 -1
- vendor/choosealicense.com/_licenses/blueoak-1.0.0.txt +1 -1
- vendor/choosealicense.com/_licenses/osl-3.0.txt +1 -1
- vendor/spdx/json/licenses.json +985 -701
- {owasp_depscan-6.0.0a3.dist-info → owasp_depscan-6.0.0b4.dist-info}/licenses/LICENSE +0 -0
- {owasp_depscan-6.0.0a3.dist-info → owasp_depscan-6.0.0b4.dist-info}/top_level.txt +0 -0
depscan/cli.py
CHANGED
|
@@ -2,10 +2,8 @@
|
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
3
|
|
|
4
4
|
import contextlib
|
|
5
|
-
import json
|
|
6
5
|
import os
|
|
7
6
|
import sys
|
|
8
|
-
import tempfile
|
|
9
7
|
from typing import List
|
|
10
8
|
|
|
11
9
|
from analysis_lib import (
|
|
@@ -24,7 +22,7 @@ from analysis_lib.utils import (
|
|
|
24
22
|
)
|
|
25
23
|
from analysis_lib.vdr import VDRAnalyzer
|
|
26
24
|
from analysis_lib.reachability import get_reachability_impl
|
|
27
|
-
from custom_json_diff.lib.utils import
|
|
25
|
+
from custom_json_diff.lib.utils import json_load
|
|
28
26
|
from rich.panel import Panel
|
|
29
27
|
from rich.terminal_theme import DEFAULT_TERMINAL_THEME, MONOKAI
|
|
30
28
|
from vdb.lib import config
|
|
@@ -54,6 +52,8 @@ from depscan.lib.config import (
|
|
|
54
52
|
from depscan.lib.license import build_license_data, bulk_lookup
|
|
55
53
|
from depscan.lib.logger import DEBUG, LOG, SPINNER, console, IS_CI
|
|
56
54
|
|
|
55
|
+
from reporting_lib.htmlgen import ReportGenerator
|
|
56
|
+
|
|
57
57
|
if sys.platform == "win32" and os.environ.get("PYTHONIOENCODING") is None:
|
|
58
58
|
sys.stdin.reconfigure(encoding="utf-8")
|
|
59
59
|
sys.stdout.reconfigure(encoding="utf-8")
|
|
@@ -65,14 +65,11 @@ LOGO = """
|
|
|
65
65
|
|
|
|
66
66
|
"""
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
SERVER_LIB = None
|
|
69
69
|
try:
|
|
70
|
-
from
|
|
70
|
+
from server_lib import simple, ServerOptions
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
app.config.from_prefixed_env()
|
|
74
|
-
app.config["PROVIDE_AUTOMATIC_OPTIONS"] = True
|
|
75
|
-
QUART_AVAILABLE = True
|
|
72
|
+
SERVER_LIB = simple
|
|
76
73
|
except ImportError:
|
|
77
74
|
pass
|
|
78
75
|
|
|
@@ -100,6 +97,7 @@ def vdr_analyze_summarize(
|
|
|
100
97
|
scoped_pkgs,
|
|
101
98
|
bom_file,
|
|
102
99
|
bom_dir,
|
|
100
|
+
reports_dir,
|
|
103
101
|
pkg_list,
|
|
104
102
|
reachability_analyzer,
|
|
105
103
|
reachability_options,
|
|
@@ -115,6 +113,7 @@ def vdr_analyze_summarize(
|
|
|
115
113
|
:param scoped_pkgs: Dict containing package scopes.
|
|
116
114
|
:param bom_file: Single BOM file.
|
|
117
115
|
:param bom_dir: Directory containining bom files.
|
|
116
|
+
:param reports_dir: Directory containining report files.
|
|
118
117
|
:param pkg_list: Direct list of packages when the bom file is empty.
|
|
119
118
|
:param reachability_analyzer: Reachability Analyzer specified.
|
|
120
119
|
:param reachability_options: Reachability Analyzer options.
|
|
@@ -165,7 +164,11 @@ def vdr_analyze_summarize(
|
|
|
165
164
|
)
|
|
166
165
|
ds_version = get_version()
|
|
167
166
|
vdr_result = VDRAnalyzer(vdr_options=options).process()
|
|
168
|
-
|
|
167
|
+
# Set vdr_file in report folder
|
|
168
|
+
vdr_file = (
|
|
169
|
+
os.path.join(reports_dir, os.path.basename(bom_file)) if bom_file else None
|
|
170
|
+
)
|
|
171
|
+
vdr_file = vdr_file.replace(".cdx.json", ".vdr.json") if vdr_file else None
|
|
169
172
|
if not vdr_file and bom_dir:
|
|
170
173
|
vdr_file = os.path.join(bom_dir, DEPSCAN_DEFAULT_VDR_FILE)
|
|
171
174
|
if vdr_result.success:
|
|
@@ -231,237 +234,6 @@ def set_project_types(args, src_dir):
|
|
|
231
234
|
return pkg_list, project_types_list
|
|
232
235
|
|
|
233
236
|
|
|
234
|
-
if QUART_AVAILABLE:
|
|
235
|
-
|
|
236
|
-
@app.get("/")
|
|
237
|
-
async def index():
|
|
238
|
-
"""
|
|
239
|
-
|
|
240
|
-
:return: An empty dictionary
|
|
241
|
-
"""
|
|
242
|
-
return {}
|
|
243
|
-
|
|
244
|
-
@app.get("/download-vdb")
|
|
245
|
-
async def download_vdb():
|
|
246
|
-
"""
|
|
247
|
-
|
|
248
|
-
:return: a JSON response indicating the status of the caching operation.
|
|
249
|
-
"""
|
|
250
|
-
if db_lib.needs_update(days=0, hours=VDB_AGE_HOURS, default_status=False):
|
|
251
|
-
if not ORAS_AVAILABLE:
|
|
252
|
-
return {
|
|
253
|
-
"error": "true",
|
|
254
|
-
"message": "The oras package must be installed to automatically download the vulnerability database. Install depscan using `pip install owasp-depscan[all]` or use the official container image.",
|
|
255
|
-
}
|
|
256
|
-
if download_image(vdb_database_url, config.DATA_DIR):
|
|
257
|
-
return {
|
|
258
|
-
"error": "false",
|
|
259
|
-
"message": "vulnerability database downloaded successfully",
|
|
260
|
-
}
|
|
261
|
-
return {
|
|
262
|
-
"error": "true",
|
|
263
|
-
"message": "vulnerability database did not get downloaded correctly. Check the server logs.",
|
|
264
|
-
}
|
|
265
|
-
return {
|
|
266
|
-
"error": "false",
|
|
267
|
-
"message": "vulnerability database already exists",
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
@app.route("/scan", methods=["GET", "POST"])
|
|
271
|
-
async def run_scan():
|
|
272
|
-
"""
|
|
273
|
-
:return: A JSON response containing the SBOM file path and a list of
|
|
274
|
-
vulnerabilities found in the scanned packages
|
|
275
|
-
"""
|
|
276
|
-
q = request.args
|
|
277
|
-
params = await request.get_json()
|
|
278
|
-
uploaded_bom_file = await request.files
|
|
279
|
-
|
|
280
|
-
url = None
|
|
281
|
-
path = None
|
|
282
|
-
multi_project = None
|
|
283
|
-
project_type = None
|
|
284
|
-
results = []
|
|
285
|
-
profile = "generic"
|
|
286
|
-
deep = False
|
|
287
|
-
suggest_mode = True if q.get("suggest") in ("true", "1") else False
|
|
288
|
-
fuzzy_search = True if q.get("fuzzy_search") in ("true", "1") else False
|
|
289
|
-
if q.get("url"):
|
|
290
|
-
url = q.get("url")
|
|
291
|
-
if q.get("path"):
|
|
292
|
-
path = q.get("path")
|
|
293
|
-
if q.get("multiProject"):
|
|
294
|
-
multi_project = q.get("multiProject", "").lower() in ("true", "1")
|
|
295
|
-
if q.get("deep"):
|
|
296
|
-
deep = q.get("deep", "").lower() in ("true", "1")
|
|
297
|
-
if q.get("type"):
|
|
298
|
-
project_type = q.get("type")
|
|
299
|
-
if q.get("profile"):
|
|
300
|
-
profile = q.get("profile")
|
|
301
|
-
if params is not None:
|
|
302
|
-
if not url and params.get("url"):
|
|
303
|
-
url = params.get("url")
|
|
304
|
-
if not path and params.get("path"):
|
|
305
|
-
path = params.get("path")
|
|
306
|
-
if not multi_project and params.get("multiProject"):
|
|
307
|
-
multi_project = params.get("multiProject", "").lower() in (
|
|
308
|
-
"true",
|
|
309
|
-
"1",
|
|
310
|
-
)
|
|
311
|
-
if not deep and params.get("deep"):
|
|
312
|
-
deep = params.get("deep", "").lower() in (
|
|
313
|
-
"true",
|
|
314
|
-
"1",
|
|
315
|
-
)
|
|
316
|
-
if not project_type and params.get("type"):
|
|
317
|
-
project_type = params.get("type")
|
|
318
|
-
if not profile and params.get("profile"):
|
|
319
|
-
profile = params.get("profile")
|
|
320
|
-
|
|
321
|
-
if not path and not url and (uploaded_bom_file.get("file", None) is None):
|
|
322
|
-
return {
|
|
323
|
-
"error": "true",
|
|
324
|
-
"message": "path or url or a bom file upload is required",
|
|
325
|
-
}, 400
|
|
326
|
-
if not project_type:
|
|
327
|
-
return {"error": "true", "message": "project type is required"}, 400
|
|
328
|
-
if db_lib.needs_update(days=0, hours=VDB_AGE_HOURS, default_status=False):
|
|
329
|
-
return (
|
|
330
|
-
{
|
|
331
|
-
"error": "true",
|
|
332
|
-
"message": "Vulnerability database is empty. Prepare the "
|
|
333
|
-
"vulnerability database by invoking /download-vdb endpoint "
|
|
334
|
-
"before running scans.",
|
|
335
|
-
},
|
|
336
|
-
500,
|
|
337
|
-
{"Content-Type": "application/json"},
|
|
338
|
-
)
|
|
339
|
-
|
|
340
|
-
cdxgen_server = app.config.get("CDXGEN_SERVER_URL")
|
|
341
|
-
bom_file_path = None
|
|
342
|
-
|
|
343
|
-
if uploaded_bom_file.get("file", None) is not None:
|
|
344
|
-
bom_file = uploaded_bom_file["file"]
|
|
345
|
-
bom_file_content = bom_file.read().decode("utf-8")
|
|
346
|
-
try:
|
|
347
|
-
_ = json.loads(bom_file_content)
|
|
348
|
-
except Exception as e:
|
|
349
|
-
LOG.info(e)
|
|
350
|
-
return (
|
|
351
|
-
{
|
|
352
|
-
"error": "true",
|
|
353
|
-
"message": "The uploaded file must be a valid JSON or XML.",
|
|
354
|
-
},
|
|
355
|
-
400,
|
|
356
|
-
{"Content-Type": "application/json"},
|
|
357
|
-
)
|
|
358
|
-
|
|
359
|
-
LOG.debug("Processing uploaded file")
|
|
360
|
-
bom_file_suffix = str(bom_file.filename).rsplit(".", maxsplit=1)[-1]
|
|
361
|
-
tmp_bom_file = tempfile.NamedTemporaryFile(
|
|
362
|
-
delete=False, suffix=f".bom.{bom_file_suffix}"
|
|
363
|
-
)
|
|
364
|
-
path = tmp_bom_file.name
|
|
365
|
-
file_write(path, bom_file_content)
|
|
366
|
-
|
|
367
|
-
# Path points to a project directory
|
|
368
|
-
# Bug# 233. Path could be a url
|
|
369
|
-
if url or (path and os.path.isdir(path)):
|
|
370
|
-
with tempfile.NamedTemporaryFile(delete=False, suffix=".bom.json") as bfp:
|
|
371
|
-
project_type_list = project_type.split(",")
|
|
372
|
-
bom_status = create_bom(
|
|
373
|
-
bfp.name,
|
|
374
|
-
path,
|
|
375
|
-
{
|
|
376
|
-
"url": url,
|
|
377
|
-
"path": path,
|
|
378
|
-
"project_type": project_type_list,
|
|
379
|
-
"multiProject": multi_project,
|
|
380
|
-
"cdxgen_server": cdxgen_server,
|
|
381
|
-
"profile": profile,
|
|
382
|
-
"deep": deep,
|
|
383
|
-
},
|
|
384
|
-
)
|
|
385
|
-
if bom_status:
|
|
386
|
-
LOG.debug("BOM file was generated successfully at %s", bfp.name)
|
|
387
|
-
bom_file_path = bfp.name
|
|
388
|
-
|
|
389
|
-
# Path points to a SBOM file
|
|
390
|
-
else:
|
|
391
|
-
if os.path.exists(path):
|
|
392
|
-
bom_file_path = path
|
|
393
|
-
# Direct purl-based lookups are not supported yet.
|
|
394
|
-
if bom_file_path is not None:
|
|
395
|
-
pkg_list, _ = get_pkg_list(bom_file_path)
|
|
396
|
-
# Here we are assuming there will be only one type
|
|
397
|
-
if project_type in type_audit_map:
|
|
398
|
-
audit_results = audit(project_type, pkg_list)
|
|
399
|
-
if audit_results:
|
|
400
|
-
results = results + audit_results
|
|
401
|
-
if not pkg_list:
|
|
402
|
-
LOG.debug("Empty package search attempted!")
|
|
403
|
-
else:
|
|
404
|
-
LOG.debug("Scanning %d oss dependencies for issues", len(pkg_list))
|
|
405
|
-
bom_data = json_load(bom_file_path)
|
|
406
|
-
if not bom_data:
|
|
407
|
-
return (
|
|
408
|
-
{
|
|
409
|
-
"error": "true",
|
|
410
|
-
"message": "Unable to generate SBOM. Check your input path or url.",
|
|
411
|
-
},
|
|
412
|
-
400,
|
|
413
|
-
{"Content-Type": "application/json"},
|
|
414
|
-
)
|
|
415
|
-
options = VdrAnalysisKV(
|
|
416
|
-
project_type,
|
|
417
|
-
results,
|
|
418
|
-
pkg_aliases={},
|
|
419
|
-
purl_aliases={},
|
|
420
|
-
suggest_mode=suggest_mode,
|
|
421
|
-
scoped_pkgs={},
|
|
422
|
-
no_vuln_table=True,
|
|
423
|
-
bom_file=bom_file_path,
|
|
424
|
-
pkg_list=[],
|
|
425
|
-
direct_purls={},
|
|
426
|
-
reached_purls={},
|
|
427
|
-
console=console,
|
|
428
|
-
logger=LOG,
|
|
429
|
-
fuzzy_search=fuzzy_search,
|
|
430
|
-
)
|
|
431
|
-
vdr_result = VDRAnalyzer(vdr_options=options).process()
|
|
432
|
-
if vdr_result.success:
|
|
433
|
-
pkg_vulnerabilities = vdr_result.pkg_vulnerabilities
|
|
434
|
-
if pkg_vulnerabilities:
|
|
435
|
-
bom_data["vulnerabilities"] = pkg_vulnerabilities
|
|
436
|
-
return json.dumps(bom_data), 200, {"Content-Type": "application/json"}
|
|
437
|
-
return (
|
|
438
|
-
{
|
|
439
|
-
"error": "true",
|
|
440
|
-
"message": "Unable to generate SBOM. Check your input path or url.",
|
|
441
|
-
},
|
|
442
|
-
500,
|
|
443
|
-
{"Content-Type": "application/json"},
|
|
444
|
-
)
|
|
445
|
-
|
|
446
|
-
def run_server(args):
|
|
447
|
-
"""
|
|
448
|
-
Run depscan as server
|
|
449
|
-
|
|
450
|
-
:param args: Command line arguments passed to the function.
|
|
451
|
-
"""
|
|
452
|
-
print(LOGO)
|
|
453
|
-
console.print(
|
|
454
|
-
f"Depscan server running on {args.server_host}:{args.server_port}"
|
|
455
|
-
)
|
|
456
|
-
app.config["CDXGEN_SERVER_URL"] = args.cdxgen_server
|
|
457
|
-
app.run(
|
|
458
|
-
host=args.server_host,
|
|
459
|
-
port=args.server_port,
|
|
460
|
-
debug=os.getenv("SCAN_DEBUG_MODE") == "debug",
|
|
461
|
-
use_reloader=False,
|
|
462
|
-
)
|
|
463
|
-
|
|
464
|
-
|
|
465
237
|
def run_depscan(args):
|
|
466
238
|
"""
|
|
467
239
|
Detects the project type, performs various scans and audits,
|
|
@@ -510,8 +282,20 @@ def run_depscan(args):
|
|
|
510
282
|
os.environ["CDXGEN_DEBUG_MODE"] = "debug"
|
|
511
283
|
LOG.setLevel(DEBUG)
|
|
512
284
|
if args.server_mode:
|
|
513
|
-
if
|
|
514
|
-
|
|
285
|
+
if SERVER_LIB:
|
|
286
|
+
server_options = ServerOptions(
|
|
287
|
+
server_host=args.server_host,
|
|
288
|
+
server_port=args.server_port,
|
|
289
|
+
cdxgen_server=args.cdxgen_server,
|
|
290
|
+
allowed_hosts=args.server_allowed_hosts,
|
|
291
|
+
allowed_paths=args.server_allowed_paths,
|
|
292
|
+
console=console,
|
|
293
|
+
logger=LOG,
|
|
294
|
+
debug=args.enable_debug or os.environ.get("SCAN_DEBUG_MODE") == "debug",
|
|
295
|
+
create_bom=create_bom,
|
|
296
|
+
max_content_length=os.getenv("DEPSCAN_SERVER_MAX_CONTENT_LENGTH"),
|
|
297
|
+
)
|
|
298
|
+
return simple.run_server(server_options)
|
|
515
299
|
else:
|
|
516
300
|
LOG.info(
|
|
517
301
|
"The required packages for server mode are unavailable. Reinstall depscan using `pip install owasp-depscan[all]`."
|
|
@@ -557,9 +341,7 @@ def run_depscan(args):
|
|
|
557
341
|
bom_dir_mode = args.bom_dir and os.path.exists(args.bom_dir)
|
|
558
342
|
# Are we running with a config file
|
|
559
343
|
config_file_mode = args.config and os.path.exists(args.config)
|
|
560
|
-
depscan_options = {**vars(args)}
|
|
561
|
-
depscan_options["src_dir"] = src_dir
|
|
562
|
-
depscan_options["reports_dir"] = reports_dir
|
|
344
|
+
depscan_options = {**vars(args), "src_dir": src_dir, "reports_dir": reports_dir}
|
|
563
345
|
# Is the user looking for semantic analysis?
|
|
564
346
|
# We can default to this when run against a BOM directory
|
|
565
347
|
if (
|
|
@@ -617,15 +399,11 @@ def run_depscan(args):
|
|
|
617
399
|
html_report_file = depscan_options.get(
|
|
618
400
|
"html_report_file", os.path.join(reports_dir, "depscan.html")
|
|
619
401
|
)
|
|
620
|
-
pdf_report_file = depscan_options.get(
|
|
621
|
-
"pdf_report_file", os.path.join(reports_dir, "depscan.pdf")
|
|
622
|
-
)
|
|
623
402
|
txt_report_file = depscan_options.get(
|
|
624
403
|
"txt_report_file", os.path.join(reports_dir, "depscan.txt")
|
|
625
404
|
)
|
|
626
405
|
run_config_file = os.path.join(reports_dir, "depscan.toml.sample")
|
|
627
406
|
depscan_options["html_report_file"] = html_report_file
|
|
628
|
-
depscan_options["pdf_report_file"] = pdf_report_file
|
|
629
407
|
depscan_options["txt_report_file"] = txt_report_file
|
|
630
408
|
# Create reports directory
|
|
631
409
|
if reports_dir and not os.path.exists(reports_dir):
|
|
@@ -934,6 +712,7 @@ def run_depscan(args):
|
|
|
934
712
|
scoped_pkgs=scoped_pkgs,
|
|
935
713
|
bom_file=bom_files[0] if len(bom_files) == 1 else None,
|
|
936
714
|
bom_dir=args.bom_dir,
|
|
715
|
+
reports_dir=args.reports_dir,
|
|
937
716
|
pkg_list=pkg_list,
|
|
938
717
|
reachability_analyzer=reachability_analyzer,
|
|
939
718
|
reachability_options=reachability_options,
|
|
@@ -975,7 +754,13 @@ def run_depscan(args):
|
|
|
975
754
|
theme=(MONOKAI if os.getenv("USE_DARK_THEME") else DEFAULT_TERMINAL_THEME),
|
|
976
755
|
)
|
|
977
756
|
console.save_text(txt_report_file, clear=False)
|
|
978
|
-
|
|
757
|
+
# Prettify the rich html report
|
|
758
|
+
html_report_generator = ReportGenerator(
|
|
759
|
+
input_rich_html_path=html_report_file,
|
|
760
|
+
report_output_path=html_report_file,
|
|
761
|
+
raw_content=False,
|
|
762
|
+
)
|
|
763
|
+
html_report_generator.parse_and_generate_report()
|
|
979
764
|
# This logic needs refactoring
|
|
980
765
|
# render report into template if wished
|
|
981
766
|
if args.report_template and os.path.isfile(args.report_template):
|
depscan/cli_options.py
CHANGED
|
@@ -245,6 +245,19 @@ def build_parser():
|
|
|
245
245
|
dest="server_port",
|
|
246
246
|
help="depscan server port",
|
|
247
247
|
)
|
|
248
|
+
parser.add_argument(
|
|
249
|
+
"--server-allowed-hosts",
|
|
250
|
+
nargs="*",
|
|
251
|
+
help="List of allowed hostnames or IPs that can access the server (e.g., 'localhost 192.168.1.10'). If unspecified, no host allowlist is enforced.",
|
|
252
|
+
default=None,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
parser.add_argument(
|
|
256
|
+
"--server-allowed-paths",
|
|
257
|
+
nargs="*",
|
|
258
|
+
help="List of allowed filesystem paths that can be scanned by the server. Restricts `path` parameter in /scan requests.",
|
|
259
|
+
default=None,
|
|
260
|
+
)
|
|
248
261
|
parser.add_argument(
|
|
249
262
|
"--cdxgen-server",
|
|
250
263
|
default=os.getenv("CDXGEN_SERVER_URL"),
|
depscan/lib/bom.py
CHANGED
|
@@ -556,7 +556,11 @@ def annotate_vdr(vdr_file, txt_report_file):
|
|
|
556
556
|
return
|
|
557
557
|
vdr = json_load(vdr_file)
|
|
558
558
|
metadata = vdr.get("metadata", {})
|
|
559
|
-
|
|
559
|
+
# Some cyclonedx sbom don't containg tools.components
|
|
560
|
+
if "components" in metadata.get("tools"):
|
|
561
|
+
tools = metadata.get("tools", {}).get("components", {})
|
|
562
|
+
else:
|
|
563
|
+
tools = {}
|
|
560
564
|
with open(txt_report_file, errors="ignore", encoding="utf-8") as txt_fp:
|
|
561
565
|
report = txt_fp.read()
|
|
562
566
|
annotations = vdr.get("annotations", []) or []
|
depscan/lib/explainer.py
CHANGED
|
@@ -33,7 +33,7 @@ def explain(project_type, src_dir, bom_dir, vdr_file, vdr_result, explanation_mo
|
|
|
33
33
|
pattern_methods = {}
|
|
34
34
|
has_any_explanation = False
|
|
35
35
|
has_any_crypto_flows = False
|
|
36
|
-
slices_files = glob.glob(f"{bom_dir}/**/*reachables.slices
|
|
36
|
+
slices_files = glob.glob(f"{bom_dir}/**/*reachables.slices*.json", recursive=True)
|
|
37
37
|
openapi_spec_files = None
|
|
38
38
|
# Should we explain the endpoints and Code Hotspots
|
|
39
39
|
if explanation_mode in (
|
|
@@ -47,9 +47,14 @@ def explain(project_type, src_dir, bom_dir, vdr_file, vdr_result, explanation_mo
|
|
|
47
47
|
rsection = Markdown("""## Service Endpoints
|
|
48
48
|
|
|
49
49
|
The following endpoints and code hotspots were identified by depscan. Verify that proper authentication and authorization mechanisms are in place to secure them.""")
|
|
50
|
-
|
|
50
|
+
any_endpoints_shown = False
|
|
51
51
|
for ospec in openapi_spec_files:
|
|
52
|
-
pattern_methods = print_endpoints(
|
|
52
|
+
pattern_methods = print_endpoints(
|
|
53
|
+
ospec, rsection if not any_endpoints_shown else None
|
|
54
|
+
)
|
|
55
|
+
if not any_endpoints_shown and pattern_methods:
|
|
56
|
+
any_endpoints_shown = True
|
|
57
|
+
|
|
53
58
|
# Return early for endpoints only explanations
|
|
54
59
|
if explanation_mode in ("Endpoints",):
|
|
55
60
|
return
|
|
@@ -65,9 +70,10 @@ The following endpoints and code hotspots were identified by depscan. Verify tha
|
|
|
65
70
|
if "-" in fn:
|
|
66
71
|
section_label = f"# Explanations for {fn.split('-')[0].upper()}"
|
|
67
72
|
console.print(Markdown(section_label))
|
|
68
|
-
if
|
|
69
|
-
|
|
70
|
-
|
|
73
|
+
if reachables_data := json_load(sf, log=LOG):
|
|
74
|
+
# Backwards compatibility
|
|
75
|
+
if isinstance(reachables_data, dict) and reachables_data.get("reachables"):
|
|
76
|
+
reachables_data = reachables_data.get("reachables")
|
|
71
77
|
if explanation_mode in ("NonReachables",):
|
|
72
78
|
rsection = Markdown(
|
|
73
79
|
f"""## {section_title}
|
|
@@ -109,7 +115,7 @@ def _track_usage_targets(usage_targets, usages_object):
|
|
|
109
115
|
usage_targets.add(f"{file}#{l}")
|
|
110
116
|
|
|
111
117
|
|
|
112
|
-
def print_endpoints(ospec):
|
|
118
|
+
def print_endpoints(ospec, header_section=None):
|
|
113
119
|
if not ospec:
|
|
114
120
|
return
|
|
115
121
|
paths = json_load(ospec).get("paths") or {}
|
|
@@ -151,6 +157,9 @@ def print_endpoints(ospec):
|
|
|
151
157
|
sorted_areas.sort()
|
|
152
158
|
rtable.add_row(k, ("\n".join(v)).upper(), "\n".join(sorted_areas))
|
|
153
159
|
if pattern_methods:
|
|
160
|
+
# Print the header section
|
|
161
|
+
if header_section:
|
|
162
|
+
console.print(header_section)
|
|
154
163
|
console.print()
|
|
155
164
|
console.print(rtable)
|
|
156
165
|
return pattern_methods
|
|
@@ -178,13 +187,17 @@ def explain_reachables(
|
|
|
178
187
|
reachable_explanations = 0
|
|
179
188
|
checked_flows = 0
|
|
180
189
|
has_crypto_flows = False
|
|
190
|
+
explained_ids = {}
|
|
181
191
|
purls_reachable_explanations = defaultdict(int)
|
|
182
192
|
source_reachable_explanations = defaultdict(int)
|
|
183
193
|
sink_reachable_explanations = defaultdict(int)
|
|
184
194
|
has_explanation = False
|
|
185
195
|
header_shown = False
|
|
186
196
|
has_cpp_flow = False
|
|
187
|
-
|
|
197
|
+
# Backwards compatibility
|
|
198
|
+
if isinstance(reachables, dict) and reachables.get("reachables"):
|
|
199
|
+
reachables = reachables.get("reachables")
|
|
200
|
+
for areach in reachables:
|
|
188
201
|
cpp_flow = is_cpp_flow(areach.get("flows"))
|
|
189
202
|
if not has_cpp_flow and cpp_flow:
|
|
190
203
|
has_cpp_flow = True
|
|
@@ -194,16 +207,9 @@ def explain_reachables(
|
|
|
194
207
|
or (not areach.get("purls") and not cpp_flow)
|
|
195
208
|
):
|
|
196
209
|
continue
|
|
197
|
-
# Focus only on the prioritized list if available
|
|
198
|
-
# if project_type in ("java",) and pkg_group_rows:
|
|
199
|
-
# is_prioritized = False
|
|
200
|
-
# for apurl in areach.get("purls"):
|
|
201
|
-
# if pkg_group_rows.get(apurl):
|
|
202
|
-
# is_prioritized = True
|
|
203
|
-
# if not is_prioritized:
|
|
204
|
-
# continue
|
|
205
210
|
(
|
|
206
211
|
flow_tree,
|
|
212
|
+
added_ids,
|
|
207
213
|
comment,
|
|
208
214
|
source_sink_desc,
|
|
209
215
|
source_code_str,
|
|
@@ -218,7 +224,13 @@ def explain_reachables(
|
|
|
218
224
|
project_type,
|
|
219
225
|
vdr_result,
|
|
220
226
|
)
|
|
221
|
-
if
|
|
227
|
+
# The goal is to reduce duplicate explanations by checking if a given flow is similar to one we have explained
|
|
228
|
+
# before. We do this by checking the node ids, source-sink explanations, purl tags and so on.
|
|
229
|
+
added_ids_str = "-".join(added_ids)
|
|
230
|
+
# Have we seen this sequence before?
|
|
231
|
+
if explained_ids.get(added_ids_str) or len(added_ids) < 4:
|
|
232
|
+
continue
|
|
233
|
+
if not source_sink_desc or not flow_tree or len(flow_tree.children) < 4:
|
|
222
234
|
continue
|
|
223
235
|
# In non-reachables mode, we are not interested in reachable flows.
|
|
224
236
|
if (
|
|
@@ -269,6 +281,7 @@ def explain_reachables(
|
|
|
269
281
|
header_shown = True
|
|
270
282
|
console.print()
|
|
271
283
|
console.print(rtable)
|
|
284
|
+
explained_ids[added_ids_str] = True
|
|
272
285
|
reachable_explanations += 1
|
|
273
286
|
if purls_str:
|
|
274
287
|
purls_reachable_explanations[purls_str] += 1
|
|
@@ -428,7 +441,7 @@ def filter_tags(tags):
|
|
|
428
441
|
|
|
429
442
|
|
|
430
443
|
def is_filterable_code(project_type, code):
|
|
431
|
-
if len(code) <
|
|
444
|
+
if len(code) < 3:
|
|
432
445
|
return True
|
|
433
446
|
for c in (
|
|
434
447
|
"console.log",
|
|
@@ -455,8 +468,16 @@ def flow_to_str(explanation_mode, flow, project_type):
|
|
|
455
468
|
and flow.get("lineNumber")
|
|
456
469
|
and not flow.get("parentFileName").startswith("unknown")
|
|
457
470
|
):
|
|
458
|
-
|
|
471
|
+
# strip common prefixes
|
|
472
|
+
name = flow.get("parentFileName", "")
|
|
473
|
+
for p in ("src/main/java/", "src/main/scala/"):
|
|
474
|
+
name = name.removeprefix(p)
|
|
475
|
+
file_loc = f"{name}#{flow.get('lineNumber')} "
|
|
459
476
|
node_desc = flow.get("code").split("\n")[0]
|
|
477
|
+
if (len(node_desc) < 3 or node_desc.endswith("{")) and len(flow.get("code")) > 3:
|
|
478
|
+
node_desc = " ".join(flow.get("code", "").split())
|
|
479
|
+
if "(" in node_desc:
|
|
480
|
+
node_desc = node_desc.split("(")[0] + "() ..."
|
|
460
481
|
if node_desc.endswith("("):
|
|
461
482
|
node_desc = f":diamond_suit: {node_desc})"
|
|
462
483
|
elif node_desc.startswith("return "):
|
|
@@ -510,6 +531,7 @@ def explain_flows(explanation_mode, flows, purls, project_type, vdr_result):
|
|
|
510
531
|
if purls:
|
|
511
532
|
purls_str = "\n".join(purls)
|
|
512
533
|
comments.append(f"[info]Reachable Packages:[/info]\n{purls_str}")
|
|
534
|
+
added_ids = []
|
|
513
535
|
added_flows = []
|
|
514
536
|
added_node_desc = []
|
|
515
537
|
has_check_tag = False
|
|
@@ -547,6 +569,7 @@ def explain_flows(explanation_mode, flows, purls, project_type, vdr_result):
|
|
|
547
569
|
if flow_str in added_flows or node_desc in added_node_desc:
|
|
548
570
|
continue
|
|
549
571
|
added_flows.append(flow_str)
|
|
572
|
+
added_ids.append(str(aflow.get("id", "")))
|
|
550
573
|
added_node_desc.append(node_desc)
|
|
551
574
|
if not tree:
|
|
552
575
|
tree = Tree(flow_str)
|
|
@@ -561,6 +584,7 @@ def explain_flows(explanation_mode, flows, purls, project_type, vdr_result):
|
|
|
561
584
|
)
|
|
562
585
|
return (
|
|
563
586
|
tree,
|
|
587
|
+
added_ids,
|
|
564
588
|
"\n".join(comments),
|
|
565
589
|
source_sink_desc,
|
|
566
590
|
source_code_str,
|
depscan/lib/utils.py
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import ast
|
|
2
2
|
import os
|
|
3
3
|
import re
|
|
4
|
-
import shutil
|
|
5
|
-
from datetime import datetime
|
|
6
4
|
|
|
7
5
|
from custom_json_diff.lib.utils import file_read, file_write, json_load
|
|
8
6
|
from jinja2 import Environment
|
|
@@ -84,11 +82,12 @@ def is_exe(src):
|
|
|
84
82
|
Detect if the source is a binary file
|
|
85
83
|
|
|
86
84
|
:param src: Source path
|
|
87
|
-
:return True if binary file. False otherwise.
|
|
85
|
+
:return: True if binary file. False otherwise.
|
|
88
86
|
"""
|
|
89
87
|
if os.path.isfile(src):
|
|
90
88
|
try:
|
|
91
|
-
|
|
89
|
+
with open(src, "rb") as f:
|
|
90
|
+
return is_binary_string(f.read(1024))
|
|
92
91
|
except Exception:
|
|
93
92
|
return False
|
|
94
93
|
return False
|
|
@@ -228,40 +227,6 @@ def get_all_imports(src_dir):
|
|
|
228
227
|
return import_list
|
|
229
228
|
|
|
230
229
|
|
|
231
|
-
def export_pdf(
|
|
232
|
-
html_file,
|
|
233
|
-
pdf_file,
|
|
234
|
-
title="DepScan Analysis",
|
|
235
|
-
footer=f"Report generated by OWASP dep-scan at {datetime.now().strftime('%B %d, %Y %H:%M')}",
|
|
236
|
-
):
|
|
237
|
-
"""
|
|
238
|
-
Method to export html as pdf using pdfkit
|
|
239
|
-
"""
|
|
240
|
-
pdf_options = {
|
|
241
|
-
"page-size": "A2",
|
|
242
|
-
"margin-top": "0.5in",
|
|
243
|
-
"margin-right": "0.25in",
|
|
244
|
-
"margin-bottom": "0.5in",
|
|
245
|
-
"margin-left": "0.25in",
|
|
246
|
-
"encoding": "UTF-8",
|
|
247
|
-
"outline": None,
|
|
248
|
-
"title": title,
|
|
249
|
-
"footer-right": footer,
|
|
250
|
-
"minimum-font-size": "12",
|
|
251
|
-
"disable-smart-shrinking": "",
|
|
252
|
-
}
|
|
253
|
-
if shutil.which("wkhtmltopdf"):
|
|
254
|
-
try:
|
|
255
|
-
import pdfkit
|
|
256
|
-
|
|
257
|
-
if not pdf_file and html_file:
|
|
258
|
-
pdf_file = html_file.replace(".html", ".pdf")
|
|
259
|
-
if os.path.exists(html_file):
|
|
260
|
-
pdfkit.from_file(html_file, pdf_file, options=pdf_options)
|
|
261
|
-
except Exception:
|
|
262
|
-
pass
|
|
263
|
-
|
|
264
|
-
|
|
265
230
|
def render_template_report(
|
|
266
231
|
vdr_file,
|
|
267
232
|
bom_file,
|