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 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 file_write, json_load
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
- QUART_AVAILABLE = False
68
+ SERVER_LIB = None
69
69
  try:
70
- from quart import Quart, request
70
+ from server_lib import simple, ServerOptions
71
71
 
72
- app = Quart(__name__, static_folder=None)
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
- vdr_file = bom_file.replace(".cdx.json", ".vdr.json") if bom_file else None
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 QUART_AVAILABLE:
514
- return run_server(args)
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
- utils.export_pdf(html_report_file, pdf_report_file)
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
- tools = metadata.get("tools", {}).get("components", {})
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.json", recursive=True)
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
- console.print(rsection)
50
+ any_endpoints_shown = False
51
51
  for ospec in openapi_spec_files:
52
- pattern_methods = print_endpoints(ospec)
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 (reachables_data := json_load(sf, log=LOG)) and reachables_data.get(
69
- "reachables"
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
- for areach in reachables.get("reachables", []):
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 not source_sink_desc or not flow_tree or len(flow_tree.children) < 5:
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) < 5:
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
- file_loc = f"{flow.get('parentFileName').replace('src/main/java/', '').replace('src/main/scala/', '')}#{flow.get('lineNumber')} "
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
- return is_binary_string(open(src, "rb").read(1024))
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,