owasp-depscan 5.5.0__py3-none-any.whl → 6.0.0a3__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.
Files changed (34) hide show
  1. depscan/__init__.py +8 -0
  2. depscan/cli.py +719 -827
  3. depscan/cli_options.py +302 -0
  4. depscan/lib/audit.py +3 -1
  5. depscan/lib/bom.py +387 -289
  6. depscan/lib/config.py +86 -337
  7. depscan/lib/explainer.py +389 -101
  8. depscan/lib/license.py +11 -10
  9. depscan/lib/logger.py +65 -17
  10. depscan/lib/package_query/__init__.py +0 -0
  11. depscan/lib/package_query/cargo_pkg.py +124 -0
  12. depscan/lib/package_query/metadata.py +170 -0
  13. depscan/lib/package_query/npm_pkg.py +345 -0
  14. depscan/lib/package_query/pkg_query.py +195 -0
  15. depscan/lib/package_query/pypi_pkg.py +113 -0
  16. depscan/lib/tomlparse.py +116 -0
  17. depscan/lib/utils.py +34 -188
  18. owasp_depscan-6.0.0a3.dist-info/METADATA +388 -0
  19. {owasp_depscan-5.5.0.dist-info → owasp_depscan-6.0.0a3.dist-info}/RECORD +28 -25
  20. {owasp_depscan-5.5.0.dist-info → owasp_depscan-6.0.0a3.dist-info}/WHEEL +1 -1
  21. vendor/choosealicense.com/_licenses/cern-ohl-p-2.0.txt +1 -1
  22. vendor/choosealicense.com/_licenses/cern-ohl-s-2.0.txt +1 -1
  23. vendor/choosealicense.com/_licenses/cern-ohl-w-2.0.txt +2 -2
  24. vendor/choosealicense.com/_licenses/mit-0.txt +1 -1
  25. vendor/spdx/json/licenses.json +904 -677
  26. depscan/lib/analysis.py +0 -1554
  27. depscan/lib/csaf.py +0 -1860
  28. depscan/lib/normalize.py +0 -312
  29. depscan/lib/orasclient.py +0 -142
  30. depscan/lib/pkg_query.py +0 -532
  31. owasp_depscan-5.5.0.dist-info/METADATA +0 -580
  32. {owasp_depscan-5.5.0.dist-info → owasp_depscan-6.0.0a3.dist-info}/entry_points.txt +0 -0
  33. {owasp_depscan-5.5.0.dist-info → owasp_depscan-6.0.0a3.dist-info/licenses}/LICENSE +0 -0
  34. {owasp_depscan-5.5.0.dist-info → owasp_depscan-6.0.0a3.dist-info}/top_level.txt +0 -0
depscan/lib/analysis.py DELETED
@@ -1,1554 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
-
3
- import contextlib
4
- import json
5
- import os.path
6
- import re
7
- from collections import OrderedDict, defaultdict
8
- from dataclasses import dataclass
9
- from typing import Dict, List, Optional
10
-
11
- import cvss
12
- from cvss import CVSSError
13
- from packageurl import PackageURL
14
- from rich import box
15
- from rich.markdown import Markdown
16
- from rich.panel import Panel
17
- from rich.style import Style
18
- from rich.table import Table
19
- from rich.tree import Tree
20
- from vdb.lib import CPE_FULL_REGEX
21
- from vdb.lib.config import placeholder_exclude_version, placeholder_fix_version
22
- from vdb.lib.utils import get_cvss3_from_vector, get_cvss4_from_vector, parse_cpe, parse_purl
23
-
24
- from depscan.lib import config
25
- from depscan.lib.logger import LOG, console
26
- from depscan.lib.utils import max_version
27
-
28
- NEWLINE = "\\n"
29
-
30
- CWE_SPLITTER = re.compile(r"(?<=CWE-)[0-9]\d{0,5}", re.IGNORECASE)
31
-
32
-
33
- def best_fixed_location(sug_version, orig_fixed_location):
34
- """
35
- Compares the suggested version with the version from the original fixed
36
- location and returns the best version based on the major versions.
37
- See: https://github.com/AppThreat/dep-scan/issues/72
38
-
39
- :param sug_version: Suggested version
40
- :param orig_fixed_location: Version from original fixed location
41
- :return: Version
42
- """
43
- if (
44
- not orig_fixed_location
45
- and sug_version
46
- and sug_version != placeholder_fix_version
47
- ):
48
- return sug_version
49
- if sug_version and orig_fixed_location:
50
- if sug_version == placeholder_fix_version:
51
- return ""
52
- tmp_a = sug_version.split(".")[0]
53
- tmp_b = orig_fixed_location.split(".")[0]
54
- if tmp_a == tmp_b:
55
- return sug_version
56
- # Handle the placeholder version used by OS distros
57
- if orig_fixed_location == placeholder_fix_version:
58
- return ""
59
- return orig_fixed_location
60
-
61
-
62
- def distro_package(package_issue):
63
- """
64
- Determines if a given Common Platform Enumeration (CPE) belongs to an
65
- operating system (OS) distribution.
66
- TODO: Clarify parameter
67
- :param package_issue: An object
68
- :return: bool
69
- """
70
- if package_issue:
71
- all_parts = CPE_FULL_REGEX.match(
72
- package_issue["affected_location"].get("cpe_uri")
73
- )
74
- if (
75
- all_parts
76
- and all_parts.group("vendor")
77
- and all_parts.group("vendor") in config.LINUX_DISTRO_WITH_EDITIONS
78
- and all_parts.group("edition")
79
- and all_parts.group("edition") != "*"
80
- ):
81
- return True
82
- return False
83
-
84
-
85
- def retrieve_bom_dependency_tree(bom_file):
86
- """
87
- Method to retrieve the dependency tree from a CycloneDX SBOM
88
-
89
- :param bom_file: Sbom to be loaded
90
- :return: Dependency tree as a list
91
- """
92
- if not bom_file:
93
- return [], None
94
- try:
95
- with open(bom_file, encoding="utf-8") as bfp:
96
- bom_data = json.load(bfp)
97
- if bom_data:
98
- return bom_data.get("dependencies", []), bom_data
99
- except json.JSONDecodeError:
100
- pass
101
- return [], None
102
-
103
-
104
- def retrieve_oci_properties(bom_data):
105
- """
106
- Retrieves OCI properties from the given BOM data.
107
-
108
- :param bom_data: The BOM data to retrieve OCI properties from.
109
- :type bom_data: dict
110
-
111
- :return: A dictionary containing the retrieved OCI properties.
112
- :rtype: dict
113
- """
114
- props = {}
115
- if not bom_data:
116
- return props
117
- for p in bom_data.get("metadata", {}).get("properties", []):
118
- if p.get("name", "").startswith("oci:image:"):
119
- props[p.get("name")] = p.get("value")
120
- return props
121
-
122
-
123
- def get_pkg_display(tree_pkg, current_pkg, extra_text=None):
124
- """
125
- Construct a string that can be used for display
126
-
127
- :param tree_pkg: Package to display
128
- :param current_pkg: The package currently being processed
129
- :param extra_text: Additional text to append to the display string
130
- :return: Constructed display string
131
- """
132
- full_pkg_display = current_pkg
133
- highlightable = tree_pkg and (
134
- tree_pkg == current_pkg or tree_pkg in current_pkg
135
- )
136
- if tree_pkg:
137
- if current_pkg.startswith("pkg:"):
138
- purl_obj = parse_purl(current_pkg)
139
- if purl_obj:
140
- version_used = purl_obj.get("version")
141
- if version_used:
142
- full_pkg_display = (
143
- f"""{purl_obj.get("name")}@{version_used}"""
144
- )
145
- if extra_text and highlightable:
146
- full_pkg_display = f"{full_pkg_display} {extra_text}"
147
- return full_pkg_display
148
-
149
-
150
- def get_tree_style(purl, p):
151
- """
152
- Return a rich style to be used in a tree
153
-
154
- :param purl: Package purl to compare
155
- :param p: Package reference to check against purl
156
- :return: The rich style to be used in a tree visualization.
157
- """
158
- if purl and (purl == p or purl in p):
159
- return Style(color="#FF753D", bold=True, italic=False)
160
- return Style(color="#7C8082", bold=False, italic=True)
161
-
162
-
163
- def pkg_sub_tree(
164
- purl,
165
- full_pkg,
166
- bom_dependency_tree,
167
- pkg_severity=None,
168
- as_tree=False,
169
- extra_text=None,
170
- ):
171
- """
172
- Method to locate and return a package tree from a dependency tree
173
-
174
- :param purl: The package purl to compare.
175
- :param full_pkg: The package reference to check against purl.
176
- :param bom_dependency_tree: The dependency tree.
177
- :param pkg_severity: The severity of the package vulnerability.
178
- :param as_tree: Flag indicating whether to return as a rich tree object.
179
- :param extra_text: Additional text to append to the display string.
180
- """
181
- pkg_tree = []
182
- if full_pkg and not purl:
183
- purl = full_pkg
184
- if not bom_dependency_tree:
185
- return [purl], Tree(
186
- get_pkg_display(purl, purl, extra_text=extra_text),
187
- style=Style(
188
- color="bright_red" if pkg_severity == "CRITICAL" else None
189
- ),
190
- )
191
- if len(bom_dependency_tree) > 1:
192
- for dep in bom_dependency_tree[1:]:
193
- ref = dep.get("ref")
194
- depends_on = dep.get("dependsOn", [])
195
- if purl in ref:
196
- if not pkg_tree or (pkg_tree and ref != pkg_tree[-1]):
197
- pkg_tree.append(ref)
198
- elif purl in depends_on and purl not in pkg_tree:
199
- pkg_tree.append(ref)
200
- pkg_tree.append(purl)
201
- break
202
- # We need to iterate again to identify any parent for the parent
203
- if pkg_tree and len(bom_dependency_tree) > 1:
204
- for dep in bom_dependency_tree[1:]:
205
- if pkg_tree[0] in dep.get("dependsOn", []):
206
- if dep.get("ref") not in pkg_tree:
207
- pkg_tree.insert(0, dep.get("ref"))
208
- break
209
- if as_tree and pkg_tree:
210
- tree = Tree(
211
- get_pkg_display(purl, pkg_tree[0], extra_text=extra_text),
212
- style=get_tree_style(purl, pkg_tree[0]),
213
- )
214
- if len(pkg_tree) > 1:
215
- subtree = tree
216
- for p in pkg_tree[1:]:
217
- subtree = subtree.add(
218
- get_pkg_display(purl, p, extra_text=extra_text),
219
- style=get_tree_style(purl, p),
220
- )
221
- return pkg_tree, tree
222
- return pkg_tree, Tree(
223
- get_pkg_display(purl, purl, extra_text=extra_text),
224
- style=Style(color="bright_red" if pkg_severity == "CRITICAL" else None),
225
- )
226
-
227
-
228
- def is_lang_sw_edition(package_issue):
229
- """Check if the specified sw_edition belongs to any application package type"""
230
- if package_issue and package_issue["affected_location"].get("cpe_uri"):
231
- all_parts = CPE_FULL_REGEX.match(
232
- package_issue["affected_location"].get("cpe_uri")
233
- )
234
- if not all_parts or all_parts.group("sw_edition") in ("*", "-"):
235
- return True
236
- if (
237
- config.LANG_PKG_TYPES.get(all_parts.group("sw_edition"))
238
- or all_parts.group("sw_edition") in config.LANG_PKG_TYPES.values()
239
- ):
240
- return True
241
- return False
242
- return True
243
-
244
-
245
- def is_os_target_sw(package_issue):
246
- """
247
- Since we rely on NVD, we filter those target_sw that definitely belong to a language
248
- """
249
- if package_issue and package_issue["affected_location"].get("cpe_uri"):
250
- all_parts = CPE_FULL_REGEX.match(
251
- package_issue["affected_location"].get("cpe_uri")
252
- )
253
- if (
254
- all_parts
255
- and all_parts.group("target_sw") not in ("*", "-")
256
- and (
257
- config.LANG_PKG_TYPES.get(all_parts.group("target_sw"))
258
- or all_parts.group("target_sw")
259
- in config.LANG_PKG_TYPES.values()
260
- )
261
- ):
262
- return False
263
- return True
264
-
265
-
266
- @dataclass
267
- class PrepareVdrOptions:
268
- project_type: str
269
- results: List
270
- pkg_aliases: Dict
271
- purl_aliases: Dict
272
- sug_version_dict: Dict
273
- scoped_pkgs: Dict
274
- no_vuln_table: bool
275
- bom_file: Optional[str]
276
- direct_purls: Dict
277
- reached_purls: Dict
278
-
279
-
280
- def prepare_vdr(options: PrepareVdrOptions):
281
- """
282
- Generates a report summary of the dependency scan results, creates a
283
- vulnerability table and a top priority table for packages that require
284
- attention, prints the recommendations, and returns a list of
285
- vulnerability details.
286
-
287
- :param options: An instance of PrepareVdrOptions containing the function parameters.
288
- :return: Vulnerability details, dictionary of prioritized items
289
- :rtype: Tuple[List, Dict]
290
- """
291
- if not options.results:
292
- return [], {}
293
- table = Table(
294
- title=f"Dependency Scan Results ({options.project_type.upper()})",
295
- box=box.DOUBLE_EDGE,
296
- header_style="bold magenta",
297
- show_lines=True,
298
- min_width=150,
299
- )
300
- ids_seen = {}
301
- direct_purls = options.direct_purls or {}
302
- reached_purls = options.reached_purls or {}
303
- required_pkgs = options.scoped_pkgs.get("required", [])
304
- optional_pkgs = options.scoped_pkgs.get("optional", [])
305
- fp_count = 0
306
- pkg_attention_count = 0
307
- critical_count = 0
308
- malicious_count = 0
309
- has_poc_count = 0
310
- has_reachable_poc_count = 0
311
- has_exploit_count = 0
312
- has_reachable_exploit_count = 0
313
- fix_version_count = 0
314
- wont_fix_version_count = 0
315
- has_os_packages = False
316
- has_redhat_packages = False
317
- has_ubuntu_packages = False
318
- distro_packages_count = 0
319
- pkg_group_rows = defaultdict(list)
320
- pkg_vulnerabilities = []
321
- # Retrieve any dependency tree from the SBOM
322
- bom_dependency_tree, bom_data = retrieve_bom_dependency_tree(
323
- options.bom_file
324
- )
325
- oci_props = retrieve_oci_properties(bom_data)
326
- oci_product_types = oci_props.get("oci:image:componentTypes", "")
327
- for h in [
328
- "Dependency Tree" if len(bom_dependency_tree) > 0 else "CVE",
329
- "Insights",
330
- "Fix Version",
331
- "Severity",
332
- "Score",
333
- ]:
334
- justify = "left"
335
- if h == "Score":
336
- justify = "right"
337
- table.add_column(header=h, justify=justify, vertical="top")
338
- for vuln_occ_dict in options.results:
339
- # If CVSS v4 data is available, override the severity and cvss_score
340
- if vuln_occ_dict.get("cvss4_vector_string"):
341
- cvss4_obj = get_cvss4_from_vector(vuln_occ_dict.get("cvss4_vector_string"))
342
- vuln_occ_dict["cvss_score"] = cvss4_obj.get("baseScore")
343
- vuln_occ_dict["severity"] = cvss4_obj.get("baseSeverity").upper()
344
- vid = vuln_occ_dict.get("id")
345
- problem_type = vuln_occ_dict.get("problem_type")
346
- cwes = []
347
- if problem_type:
348
- cwes = split_cwe(problem_type)
349
- has_flagged_cwe = False
350
- package_issue = vuln_occ_dict.get("package_issue")
351
- matched_by = vuln_occ_dict.get("matched_by")
352
- full_pkg = package_issue["affected_location"].get("package")
353
- project_type_pkg = (
354
- f"{options.project_type}:"
355
- f"{package_issue['affected_location'].get('package')}"
356
- )
357
- if package_issue["affected_location"].get("vendor"):
358
- full_pkg = (
359
- f"{package_issue['affected_location'].get('vendor')}:"
360
- f"{package_issue['affected_location'].get('package')}"
361
- )
362
- elif package_issue["affected_location"].get("cpe_uri"):
363
- vendor, _, _, _ = parse_cpe(
364
- package_issue["affected_location"].get("cpe_uri")
365
- )
366
- if vendor:
367
- full_pkg = (
368
- f"{vendor}:"
369
- f"{package_issue['affected_location'].get('package')}"
370
- )
371
- if matched_by:
372
- version = matched_by.split("|")[-1]
373
- full_pkg = full_pkg + ":" + version
374
- # De-alias package names
375
- if options.pkg_aliases.get(full_pkg):
376
- full_pkg = options.pkg_aliases.get(full_pkg)
377
- else:
378
- full_pkg = options.pkg_aliases.get(full_pkg.lower(), full_pkg)
379
- version_used = package_issue["affected_location"].get("version")
380
- purl = options.purl_aliases.get(full_pkg, full_pkg)
381
- package_type = None
382
- insights = []
383
- plain_insights = []
384
- if vid.startswith("MAL-"):
385
- insights.append("[bright_red]:stop_sign: Malicious[/bright_red]")
386
- plain_insights.append("Malicious")
387
- malicious_count += 1
388
- purl_obj = None
389
- vendor = package_issue["affected_location"].get("vendor")
390
- # If the match was based on name and version alone then the alias might legitimately lack a full purl
391
- # Such results are usually false positives but could yield good hits at times
392
- # So, instead of suppressing fully we try our best to tune and reduce the FP
393
- if not purl.startswith("pkg:"):
394
- if options.project_type in config.OS_PKG_TYPES:
395
- if vendor and (
396
- vendor in config.LANG_PKG_TYPES.values()
397
- or config.LANG_PKG_TYPES.get(vendor)
398
- ):
399
- fp_count += 1
400
- continue
401
- # Some nvd data might match application CVEs for
402
- # OS vendors which can be filtered
403
- if not is_os_target_sw(package_issue):
404
- fp_count += 1
405
- continue
406
- # Issue #320 - Malware matches without purl are false positives
407
- if vid.startswith("MAL-"):
408
- fp_count += 1
409
- malicious_count -= 1
410
- continue
411
- else:
412
- purl_obj = parse_purl(purl)
413
- # Issue #320 - Malware matches without purl are false positives
414
- if not purl_obj and vid.startswith("MAL-"):
415
- fp_count += 1
416
- malicious_count -= 1
417
- continue
418
- if purl_obj:
419
- version_used = purl_obj.get("version")
420
- package_type = purl_obj.get("type")
421
- qualifiers = purl_obj.get("qualifiers", {})
422
- # Filter application CVEs from distros
423
- if (
424
- config.LANG_PKG_TYPES.get(package_type)
425
- or package_type in config.LANG_PKG_TYPES.values()
426
- ) and (
427
- (vendor and vendor in config.OS_PKG_TYPES)
428
- or not is_lang_sw_edition(package_issue)
429
- ):
430
- fp_count += 1
431
- continue
432
- if package_type in config.OS_PKG_TYPES:
433
- # Bug #208 - do not report application CVEs
434
- if vendor and (
435
- vendor in config.LANG_PKG_TYPES.values()
436
- or config.LANG_PKG_TYPES.get(vendor)
437
- ):
438
- fp_count += 1
439
- continue
440
- if package_type and (
441
- package_type in config.LANG_PKG_TYPES.values()
442
- or config.LANG_PKG_TYPES.get(package_type)
443
- ):
444
- fp_count += 1
445
- continue
446
- if (
447
- vendor
448
- and oci_product_types
449
- and vendor not in oci_product_types
450
- ):
451
- # Bug #170 - do not report CVEs belonging to other distros
452
- if vendor in config.OS_PKG_TYPES:
453
- fp_count += 1
454
- continue
455
- # Some nvd data might match application CVEs for
456
- # OS vendors which can be filtered
457
- if not is_os_target_sw(package_issue):
458
- fp_count += 1
459
- continue
460
- insights.append(
461
- f"[#7C8082]:telescope: Vendor {vendor}[/#7C8082]"
462
- )
463
- plain_insights.append(f"Vendor {vendor}")
464
- has_os_packages = True
465
- for acwe in cwes:
466
- if acwe in config.OS_VULN_KEY_CWES:
467
- has_flagged_cwe = True
468
- break
469
- # Don't flag the cwe for ignorable os packages
470
- if has_flagged_cwe and (
471
- purl_obj.get("name") in config.OS_PKG_UNINSTALLABLE
472
- or purl_obj.get("name") in config.OS_PKG_IGNORABLE
473
- or vendor in config.OS_PKG_IGNORABLE
474
- ):
475
- has_flagged_cwe = False
476
- else:
477
- if (
478
- purl_obj.get("name") in config.OS_PKG_IGNORABLE
479
- or vendor in config.OS_PKG_IGNORABLE
480
- ):
481
- insights.append(
482
- "[#7C8082]:mute: Suppress for containers[/#7C8082]"
483
- )
484
- plain_insights.append("Suppress for containers")
485
- elif (
486
- purl_obj.get("name") in config.OS_PKG_UNINSTALLABLE
487
- ):
488
- insights.append(
489
- "[#7C8082]:scissors: Uninstall candidate[/#7C8082]"
490
- )
491
- plain_insights.append("Uninstall candidate")
492
- # If the flag remains after all the suppressions then add it as an insight
493
- if has_flagged_cwe:
494
- insights.append(
495
- "[#7C8082]:triangular_flag: Flagged weakness[/#7C8082]"
496
- )
497
- plain_insights.append("Flagged weakness")
498
- if qualifiers:
499
- if "ubuntu" in qualifiers.get("distro", ""):
500
- has_ubuntu_packages = True
501
- if "rhel" in qualifiers.get("distro", ""):
502
- has_redhat_packages = True
503
- if ids_seen.get(vid + purl):
504
- fp_count += 1
505
- continue
506
- # Mark this CVE + pkg as seen to avoid duplicates
507
- ids_seen[vid + purl] = True
508
- # Find the best fix version
509
- fixed_location = best_fixed_location(
510
- options.sug_version_dict.get(purl), package_issue["fixed_location"]
511
- )
512
- if (
513
- options.sug_version_dict.get(purl) == placeholder_fix_version
514
- or package_issue["fixed_location"] == placeholder_fix_version
515
- ):
516
- wont_fix_version_count += 1
517
- package_usage = "N/A"
518
- plain_package_usage = "N/A"
519
- pkg_severity = vuln_occ_dict.get("severity")
520
- is_required = False
521
- pkg_requires_attn = False
522
- related_urls = vuln_occ_dict.get("related_urls")
523
- clinks = classify_links(
524
- related_urls,
525
- )
526
- if direct_purls.get(purl):
527
- is_required = True
528
- elif not direct_purls and (
529
- purl in required_pkgs
530
- or full_pkg in required_pkgs
531
- or project_type_pkg in required_pkgs
532
- ):
533
- is_required = True
534
- if pkg_severity in ("CRITICAL", "HIGH"):
535
- if is_required:
536
- pkg_attention_count += 1
537
- if fixed_location:
538
- fix_version_count += 1
539
- if (
540
- clinks.get("vendor") or package_type in config.OS_PKG_TYPES
541
- ) and pkg_severity == "CRITICAL":
542
- critical_count += 1
543
- # Locate this package in the tree
544
- pkg_tree_list, p_rich_tree = pkg_sub_tree(
545
- purl,
546
- full_pkg.replace(":", "/"),
547
- bom_dependency_tree,
548
- pkg_severity=pkg_severity,
549
- as_tree=True,
550
- extra_text=f":left_arrow: {vid}",
551
- )
552
- if is_required and package_type not in config.OS_PKG_TYPES:
553
- if direct_purls.get(purl):
554
- package_usage = (
555
- f":direct_hit: Used in [info]"
556
- f"{str(direct_purls.get(purl))}"
557
- f"[/info] locations"
558
- )
559
- plain_package_usage = (
560
- f"Used in {str(direct_purls.get(purl))} locations"
561
- )
562
- else:
563
- package_usage = ":direct_hit: Direct dependency"
564
- plain_package_usage = "Direct dependency"
565
- elif (
566
- not optional_pkgs and pkg_tree_list and len(pkg_tree_list) > 1
567
- ) or (
568
- purl in optional_pkgs
569
- or full_pkg in optional_pkgs
570
- or project_type_pkg in optional_pkgs
571
- ):
572
- if package_type in config.OS_PKG_TYPES:
573
- package_usage = (
574
- "[spring_green4]:notebook: Local install[/spring_green4]"
575
- )
576
- plain_package_usage = "Local install"
577
- has_os_packages = True
578
- else:
579
- package_usage = (
580
- "[spring_green4]:notebook: Indirect "
581
- "dependency[/spring_green4]"
582
- )
583
- plain_package_usage = "Indirect dependency"
584
- if package_usage != "N/A":
585
- insights.append(package_usage)
586
- plain_insights.append(plain_package_usage)
587
- if clinks.get("poc") or clinks.get("Bug Bounty"):
588
- if reached_purls.get(purl):
589
- insights.append(
590
- "[yellow]:notebook_with_decorative_cover: Reachable Bounty target[/yellow]"
591
- )
592
- plain_insights.append("Reachable Bounty target")
593
- has_reachable_poc_count += 1
594
- has_reachable_exploit_count += 1
595
- pkg_requires_attn = True
596
- elif direct_purls.get(purl):
597
- insights.append(
598
- "[yellow]:notebook_with_decorative_cover: Bug Bounty target[/yellow]"
599
- )
600
- plain_insights.append("Bug Bounty target")
601
- else:
602
- insights.append(
603
- "[yellow]:notebook_with_decorative_cover: Has PoC[/yellow]"
604
- )
605
- plain_insights.append("Has PoC")
606
- has_poc_count += 1
607
- if pkg_severity in ("CRITICAL", "HIGH"):
608
- pkg_requires_attn = True
609
- if (clinks.get("vendor") and package_type not in config.OS_PKG_TYPES) or reached_purls.get(purl):
610
- if reached_purls.get(purl):
611
- # If it has a poc, an insight might have gotten added above
612
- if not pkg_requires_attn:
613
- insights.append(":receipt: Reachable")
614
- plain_insights.append("Reachable")
615
- else:
616
- insights.append(":receipt: Vendor Confirmed")
617
- plain_insights.append("Vendor Confirmed")
618
- if clinks.get("exploit"):
619
- if reached_purls.get(purl) or direct_purls.get(purl):
620
- insights.append(
621
- "[bright_red]:exclamation_mark: Reachable and Exploitable[/bright_red]"
622
- )
623
- plain_insights.append("Reachable and Exploitable")
624
- has_reachable_exploit_count += 1
625
- # Fail safe. Packages with exploits and direct usage without
626
- # a reachable flow are still considered reachable to reduce
627
- # false negatives
628
- if not reached_purls.get(purl):
629
- reached_purls[purl] = 1
630
- elif has_flagged_cwe:
631
- if (vendor and vendor in ("gnu",)) or (
632
- purl_obj and purl_obj.get("name") in ("glibc", "openssl")
633
- ):
634
- insights.append(
635
- "[bright_red]:exclamation_mark: Reachable and Exploitable[/bright_red]"
636
- )
637
- plain_insights.append("Reachable and Exploitable")
638
- has_reachable_exploit_count += 1
639
- else:
640
- insights.append(
641
- "[bright_red]:exclamation_mark: Exploitable[/bright_red]"
642
- )
643
- plain_insights.append("Exploitable")
644
- has_exploit_count += 1
645
- else:
646
- insights.append(
647
- "[bright_red]:exclamation_mark: Known Exploits[/bright_red]"
648
- )
649
- plain_insights.append("Known Exploits")
650
- has_exploit_count += 1
651
- pkg_requires_attn = True
652
- if distro_package(package_issue):
653
- insights.append(
654
- "[spring_green4]:direct_hit: Distro specific[/spring_green4]"
655
- )
656
- plain_insights.append("Distro specific")
657
- distro_packages_count += 1
658
- has_os_packages = True
659
- if pkg_requires_attn and fixed_location and purl:
660
- pkg_group_rows[purl].append(
661
- {
662
- "id": vid,
663
- "fixed_location": fixed_location,
664
- "p_rich_tree": p_rich_tree,
665
- }
666
- )
667
- if not options.no_vuln_table:
668
- table.add_row(
669
- p_rich_tree,
670
- "\n".join(insights),
671
- fixed_location,
672
- f"""{"[bright_red]" if pkg_severity == "CRITICAL" else ""}{vuln_occ_dict.get("severity")}""",
673
- f"""{"[bright_red]" if pkg_severity == "CRITICAL" else ""}{vuln_occ_dict.get("cvss_score")}""",
674
- )
675
- if purl:
676
- source = {}
677
- if vid.startswith("CVE"):
678
- source = {
679
- "name": "NVD",
680
- "url": f"https://nvd.nist.gov/vuln/detail/{vid}",
681
- }
682
- elif vid.startswith("GHSA") or vid.startswith("npm"):
683
- source = {
684
- "name": "GitHub",
685
- "url": f"https://github.com/advisories/{vid}",
686
- }
687
- versions = [{"version": version_used, "status": "affected"}]
688
- recommendation = ""
689
- if fixed_location:
690
- versions.append(
691
- {"version": fixed_location, "status": "unaffected"}
692
- )
693
- recommendation = f"Update to {fixed_location} or later"
694
- affects = [{"ref": purl, "versions": versions}]
695
- analysis = {}
696
- if clinks.get("exploit"):
697
- analysis = {
698
- "state": "exploitable",
699
- "detail": f'See {clinks.get("exploit")}',
700
- }
701
- elif clinks.get("poc"):
702
- analysis = {
703
- "state": "in_triage",
704
- "detail": f'See {clinks.get("poc")}',
705
- }
706
- elif pkg_tree_list and len(pkg_tree_list) > 1:
707
- analysis = {
708
- "state": "in_triage",
709
- "detail": f"Dependency Tree: {json.dumps(pkg_tree_list)}",
710
- }
711
- ratings = cvss_to_vdr_rating(vuln_occ_dict)
712
- properties = [
713
- {
714
- "name": "depscan:insights",
715
- "value": "\\n".join(plain_insights),
716
- },
717
- {
718
- "name": "depscan:prioritized",
719
- "value": "true" if pkg_group_rows.get(purl) else "false",
720
- },
721
- ]
722
- affected_version_range = get_version_range(package_issue, purl)
723
- if affected_version_range:
724
- properties.append(affected_version_range)
725
- advisories = []
726
- for k, v in clinks.items():
727
- advisories.append({"title": k, "url": v})
728
- vuln = {
729
- "bom-ref": f"{vid}/{purl}",
730
- "id": vid,
731
- "source": source,
732
- "ratings": ratings,
733
- "cwes": cwes,
734
- "description": vuln_occ_dict.get("short_description"),
735
- "recommendation": recommendation,
736
- "advisories": advisories,
737
- "analysis": analysis,
738
- "affects": affects,
739
- "properties": properties,
740
- }
741
- if source_orig_time := vuln_occ_dict.get("source_orig_time"):
742
- vuln["published"] = source_orig_time
743
- if source_update_time := vuln_occ_dict.get("source_update_time"):
744
- vuln["updated"] = source_update_time
745
- pkg_vulnerabilities.append(vuln)
746
- # If the user doesn't want any table output return quickly
747
- if options.no_vuln_table:
748
- return pkg_vulnerabilities, pkg_group_rows
749
- if pkg_vulnerabilities:
750
- console.print()
751
- console.print(table)
752
- if pkg_group_rows:
753
- psection = Markdown(
754
- """
755
- Next Steps
756
- ----------
757
-
758
- Below are the vulnerabilities prioritized by depscan. Follow your team's remediation workflow to mitigate these findings.
759
- """,
760
- justify="left",
761
- )
762
- console.print(psection)
763
- utable = Table(
764
- title=f"Top Priority ({options.project_type.upper()})",
765
- box=box.DOUBLE_EDGE,
766
- header_style="bold magenta",
767
- show_lines=True,
768
- min_width=150,
769
- )
770
- for h in ("Package", "CVEs", "Fix Version", "Reachable"):
771
- utable.add_column(header=h, vertical="top")
772
- for k, v in pkg_group_rows.items():
773
- cve_list = []
774
- fv = None
775
- for c in v:
776
- cve_list.append(c.get("id"))
777
- if not fv:
778
- fv = c.get("fixed_location")
779
- utable.add_row(
780
- v[0].get("p_rich_tree"),
781
- "\n".join(sorted(cve_list, reverse=True)),
782
- f"[bright_green]{fv}[/bright_green]",
783
- "[warning]Yes[/warning]" if reached_purls.get(k) else "",
784
- )
785
- console.print()
786
- console.print(utable)
787
- console.print()
788
- if malicious_count:
789
- rmessage = ":stop_sign: Malicious package found! Treat this as a [bold]security incident[/bold] and follow your organization's playbook to remove this package from all affected applications."
790
- if malicious_count > 1:
791
- rmessage = f":stop_sign: {malicious_count} malicious packages found in this project! Treat this as a [bold]security incident[/bold] and follow your organization's playbook to remove the packages from all affected applications."
792
- console.print(
793
- Panel(
794
- rmessage,
795
- title="Action Required",
796
- expand=False,
797
- )
798
- )
799
- elif options.scoped_pkgs or has_exploit_count:
800
- if not pkg_attention_count and has_exploit_count:
801
- if has_reachable_exploit_count:
802
- rmessage = (
803
- f":point_right: [magenta]{has_reachable_exploit_count}"
804
- f"[/magenta] out of {len(pkg_vulnerabilities)} vulnerabilities "
805
- f"have [dark magenta]reachable[/dark magenta] exploits and requires your ["
806
- f"magenta]immediate[/magenta] attention."
807
- )
808
- else:
809
- rmessage = (
810
- f":point_right: [magenta]{has_exploit_count}"
811
- f"[/magenta] out of {len(pkg_vulnerabilities)} vulnerabilities "
812
- f"have known exploits and requires your ["
813
- f"magenta]immediate[/magenta] attention."
814
- )
815
- if not has_os_packages:
816
- rmessage += (
817
- "\nAdditional workarounds and configuration "
818
- "changes might be required to remediate these "
819
- "vulnerabilities."
820
- )
821
- if not options.scoped_pkgs:
822
- rmessage += (
823
- "\nNOTE: Package usage analysis was not "
824
- "performed for this project."
825
- )
826
- else:
827
- rmessage += (
828
- "\n:scissors: Consider trimming this image by removing any "
829
- "unwanted packages. Alternatively, use a slim "
830
- "base image."
831
- )
832
- if distro_packages_count and distro_packages_count < len(
833
- pkg_vulnerabilities
834
- ):
835
- if (
836
- len(pkg_vulnerabilities)
837
- > config.max_distro_vulnerabilities
838
- ):
839
- rmessage += f"\nNOTE: Check if the base image or the kernel version used is End-of-Life (EOL)."
840
- else:
841
- rmessage += (
842
- f"\nNOTE: [magenta]{distro_packages_count}"
843
- f"[/magenta] distro-specific vulnerabilities "
844
- f"out of {len(pkg_vulnerabilities)} could be prioritized "
845
- f"for updates."
846
- )
847
- if has_redhat_packages:
848
- rmessage += """\nNOTE: Vulnerabilities in RedHat packages with status "out of support" or "won't fix" are excluded from this result."""
849
- if has_ubuntu_packages:
850
- rmessage += """\nNOTE: Vulnerabilities in Ubuntu packages with status "DNE" or "needs-triaging" are excluded from this result."""
851
- console.print(
852
- Panel(
853
- rmessage,
854
- title="Recommendation",
855
- expand=False,
856
- )
857
- )
858
- elif pkg_attention_count:
859
- if has_reachable_exploit_count:
860
- rmessage = (
861
- f":point_right: Prioritize the [magenta]{has_reachable_exploit_count}"
862
- f"[/magenta] [bold magenta]reachable[/bold magenta] vulnerabilities with known exploits."
863
- )
864
- elif has_exploit_count:
865
- rmessage = (
866
- f":point_right: Prioritize the [magenta]{has_exploit_count}"
867
- f"[/magenta] vulnerabilities with known exploits."
868
- )
869
- else:
870
- rmessage = (
871
- f":point_right: [info]{pkg_attention_count}"
872
- f"[/info] out of {len(pkg_vulnerabilities)} vulnerabilities "
873
- f"requires your attention."
874
- )
875
- if fix_version_count:
876
- if fix_version_count == pkg_attention_count:
877
- rmessage += (
878
- "\n:white_heavy_check_mark: You can update ["
879
- "bright_green]all[/bright_green] the "
880
- "packages using the mentioned fix version to "
881
- "remediate."
882
- )
883
- else:
884
- v_text = (
885
- "vulnerability"
886
- if fix_version_count == 1
887
- else "vulnerabilities"
888
- )
889
- rmessage += (
890
- f"\nYou can remediate [bright_green]"
891
- f"{fix_version_count}[/bright_green] "
892
- f"{v_text} "
893
- f"by updating the packages using the fix "
894
- f"version :thumbsup:"
895
- )
896
- console.print(
897
- Panel(
898
- rmessage,
899
- title="Recommendation",
900
- expand=False,
901
- )
902
- )
903
- elif critical_count:
904
- console.print(
905
- Panel(
906
- f":white_medium_small_square: Prioritize the [magenta]{critical_count}"
907
- f"[/magenta] critical vulnerabilities confirmed by the "
908
- f"vendor.",
909
- title="Recommendation",
910
- expand=False,
911
- )
912
- )
913
- else:
914
- if has_os_packages:
915
- rmessage = (
916
- ":white_medium_small_square: Prioritize any vulnerabilities in libraries such "
917
- "as glibc, openssl, or libcurl.\nAdditionally, "
918
- "prioritize the vulnerabilities with 'Flagged weakness' under insights."
919
- )
920
- rmessage += (
921
- "\nVulnerabilities in Linux Kernel packages can "
922
- "be usually ignored in containerized "
923
- "environments as long as the vulnerability "
924
- "doesn't lead to any 'container-escape' type "
925
- "vulnerabilities."
926
- )
927
- if has_redhat_packages:
928
- rmessage += """\nNOTE: Vulnerabilities in RedHat packages
929
- with status "out of support" or "won't fix" are excluded
930
- from this result."""
931
- if has_ubuntu_packages:
932
- rmessage += """\nNOTE: Vulnerabilities in Ubuntu packages
933
- with status "DNE" or "needs-triaging" are excluded from
934
- this result."""
935
- console.print(Panel(rmessage, title="Recommendation"))
936
- else:
937
- rmessage = None
938
- if reached_purls:
939
- rmessage = ":white_check_mark: No package requires immediate attention since the major vulnerabilities are not reachable."
940
- elif direct_purls:
941
- rmessage = ":white_check_mark: No package requires immediate attention since the major vulnerabilities are found only in dev packages and indirect dependencies."
942
- if rmessage:
943
- console.print(
944
- Panel(
945
- rmessage,
946
- title="Recommendation",
947
- expand=False,
948
- )
949
- )
950
- elif critical_count:
951
- console.print(
952
- Panel(
953
- f":white_medium_small_square: Prioritize the [magenta]{critical_count}"
954
- f"[/magenta] critical vulnerabilities confirmed by the vendor.",
955
- title="Recommendation",
956
- expand=False,
957
- )
958
- )
959
- if reached_purls:
960
- sorted_reached_purls = sorted(
961
- ((value, key) for (key, value) in reached_purls.items()),
962
- reverse=True,
963
- )[:3]
964
- sorted_reached_dict = OrderedDict(
965
- (k, v) for v, k in sorted_reached_purls
966
- )
967
- rsection = Markdown(
968
- """
969
- Proactive Measures
970
- ------------------
971
-
972
- Below are the top reachable packages identified by depscan. Setup alerts and notifications to actively monitor these packages for new vulnerabilities and exploits.
973
- """,
974
- justify="left",
975
- )
976
- console.print(rsection)
977
- rtable = Table(
978
- title="Top Reachable Packages",
979
- box=box.DOUBLE_EDGE,
980
- header_style="bold magenta",
981
- show_lines=True,
982
- min_width=150,
983
- )
984
- for h in ("Package", "Reachable Flows"):
985
- rtable.add_column(header=h, vertical="top")
986
- for k, v in sorted_reached_dict.items():
987
- rtable.add_row(k, str(v))
988
- console.print()
989
- console.print(rtable)
990
- console.print()
991
- return pkg_vulnerabilities, pkg_group_rows
992
-
993
-
994
- def get_version_range(package_issue, purl):
995
- """
996
- Generates a version range object for inclusion in the vdr file.
997
-
998
- :param package_issue: Vulnerability data dict
999
- :param purl: Package URL string
1000
-
1001
- :return: A list containing a dictionary with version range information.
1002
- """
1003
- new_prop = {}
1004
- if (affected_location := package_issue.get("affected_location")) and (
1005
- affected_version := affected_location.get("version")
1006
- ):
1007
- try:
1008
- ppurl = PackageURL.from_string(purl)
1009
- new_prop = {
1010
- "name": "affectedVersionRange",
1011
- "value": f"{ppurl.name}@" f"{affected_version}",
1012
- }
1013
- if ppurl.namespace:
1014
- new_prop["value"] = f'{ppurl.namespace}/{new_prop["value"]}'
1015
- except ValueError:
1016
- ppurl = purl.split("@")
1017
- if len(ppurl) == 2:
1018
- new_prop = {
1019
- "name": "affectedVersionRange",
1020
- "value": f"{ppurl[0]}@{affected_version}",
1021
- }
1022
-
1023
- return new_prop
1024
-
1025
-
1026
- def cvss_to_vdr_rating(vuln_occ_dict):
1027
- """
1028
- Generates a rating object for inclusion in the vdr file.
1029
-
1030
- :param vuln_occ_dict: Vulnerability data
1031
-
1032
- :return: A list containing a dictionary with CVSS score information.
1033
- """
1034
- ratings = []
1035
- # Support for cvss v4
1036
- if vuln_occ_dict.get("cvss4_vector_string") and (vector_string := vuln_occ_dict.get("cvss4_vector_string")):
1037
- cvss4_obj = get_cvss4_from_vector(vector_string)
1038
- ratings.append(
1039
- {
1040
- "method": "CVSSv4",
1041
- "score": cvss4_obj.get("baseScore"),
1042
- "severity": cvss4_obj.get("baseSeverity").lower(),
1043
- "vector": vector_string
1044
- }
1045
- )
1046
- if vuln_occ_dict.get("cvss_v3") and (
1047
- vector_string := vuln_occ_dict["cvss_v3"].get("vector_string")
1048
- ):
1049
- with contextlib.suppress(CVSSError):
1050
- cvss3_obj = get_cvss3_from_vector(vector_string)
1051
- method = cvss3_obj.get("version")
1052
- method = method.replace(".", "").replace("0", "")
1053
- ratings.append(
1054
- {
1055
- "method": f"CVSSv{method}",
1056
- "score": cvss3_obj.get("baseScore"),
1057
- "severity": cvss3_obj.get("baseSeverity").lower(),
1058
- "vector": vector_string
1059
- }
1060
- )
1061
- return ratings
1062
-
1063
-
1064
- def split_cwe(cwe):
1065
- """
1066
- Split the given CWE string into a list of CWE IDs.
1067
-
1068
- :param cwe: The problem issue taken from a vulnerability object
1069
-
1070
- :return: A list of CWE IDs
1071
- :rtype: list
1072
- """
1073
- cwe_ids = []
1074
-
1075
- if isinstance(cwe, str):
1076
- cwe_ids = re.findall(CWE_SPLITTER, cwe)
1077
- elif isinstance(cwe, list):
1078
- cwes = "|".join(cwe)
1079
- cwe_ids = re.findall(CWE_SPLITTER, cwes)
1080
-
1081
- with contextlib.suppress(ValueError, TypeError):
1082
- cwe_ids = [int(cwe_id) for cwe_id in cwe_ids]
1083
- return cwe_ids
1084
-
1085
-
1086
- def summary_stats(results):
1087
- """
1088
- Generate summary stats
1089
-
1090
- :param results: List of scan results objects with severity attribute.
1091
- :return: A dictionary containing the summary statistics for the severity
1092
- levels of the vulnerabilities in the results list.
1093
- """
1094
- if not results:
1095
- LOG.info("No oss vulnerabilities detected ✅")
1096
- return None
1097
- summary = {
1098
- "UNSPECIFIED": 0,
1099
- "LOW": 0,
1100
- "MEDIUM": 0,
1101
- "HIGH": 0,
1102
- "CRITICAL": 0,
1103
- }
1104
- for res in results:
1105
- summary[res.get("severity")] += 1
1106
- return summary
1107
-
1108
-
1109
- def jsonl_report(
1110
- project_type,
1111
- results,
1112
- pkg_aliases,
1113
- purl_aliases,
1114
- sug_version_dict,
1115
- scoped_pkgs,
1116
- out_file_name,
1117
- direct_purls,
1118
- reached_purls,
1119
- ):
1120
- """
1121
- DEPRECATED: Produce vulnerability occurrence report in jsonlines format
1122
- This method should use the pkg_vulnerabilities from prepare_vdr
1123
-
1124
- :param scoped_pkgs: A dict of lists of required/optional/excluded packages.
1125
- :param sug_version_dict: A dict mapping package names to suggested versions.
1126
- :param purl_aliases: A dict mapping package names to their purl aliases.
1127
- :param project_type: Project type
1128
- :param results: List of vulnerabilities found
1129
- :param pkg_aliases: Package alias
1130
- :param out_file_name: Output filename
1131
- :param direct_purls: A list of direct purls
1132
- :param reached_purls: A list of reached purls
1133
- """
1134
- ids_seen = {}
1135
- required_pkgs = scoped_pkgs.get("required", [])
1136
- optional_pkgs = scoped_pkgs.get("optional", [])
1137
- excluded_pkgs = scoped_pkgs.get("excluded", [])
1138
- with open(out_file_name, "w", encoding="utf-8") as outfile:
1139
- for vuln_occ_dict in results:
1140
- vid = vuln_occ_dict.get("id")
1141
- package_issue = vuln_occ_dict.get("package_issue")
1142
- if not package_issue.get("affected_location"):
1143
- continue
1144
- full_pkg = package_issue["affected_location"].get("package")
1145
- if package_issue["affected_location"].get("vendor"):
1146
- full_pkg = (
1147
- f"{package_issue['affected_location'].get('vendor')}:"
1148
- f"{package_issue['affected_location'].get('package')}"
1149
- )
1150
- elif package_issue["affected_location"].get("cpe_uri"):
1151
- vendor, _, _, _ = parse_cpe(
1152
- package_issue["affected_location"].get("cpe_uri")
1153
- )
1154
- if vendor:
1155
- full_pkg = (
1156
- f"{vendor}:"
1157
- f"{package_issue['affected_location'].get('package')}"
1158
- )
1159
- # De-alias package names
1160
- full_pkg = pkg_aliases.get(full_pkg, full_pkg)
1161
- full_pkg_display = full_pkg
1162
- version_used = package_issue["affected_location"].get("version")
1163
- purl = purl_aliases.get(full_pkg, full_pkg)
1164
- if purl:
1165
- purl_obj = parse_purl(purl)
1166
- if purl_obj:
1167
- version_used = purl_obj.get("version")
1168
- if purl_obj.get("namespace"):
1169
- full_pkg = f"""{purl_obj.get("namespace")}/
1170
- {purl_obj.get("name")}@{purl_obj.get("version")}"""
1171
- else:
1172
- full_pkg = f"""{purl_obj.get("name")}@{purl_obj
1173
- .get("version")}"""
1174
- if ids_seen.get(vid + purl):
1175
- continue
1176
- # On occasions, this could still result in duplicates if the
1177
- # package exists with and without a purl
1178
- ids_seen[vid + purl] = True
1179
- project_type_pkg = f"""{project_type}:{package_issue["affected_location"].get("package")}"""
1180
- fixed_location = best_fixed_location(
1181
- sug_version_dict.get(purl),
1182
- package_issue["fixed_location"],
1183
- )
1184
- package_usage = "N/A"
1185
- if (
1186
- direct_purls.get(purl)
1187
- or purl in required_pkgs
1188
- or full_pkg in required_pkgs
1189
- or project_type_pkg in required_pkgs
1190
- ):
1191
- package_usage = "required"
1192
- elif (
1193
- purl in optional_pkgs
1194
- or full_pkg in optional_pkgs
1195
- or project_type_pkg in optional_pkgs
1196
- ):
1197
- package_usage = "optional"
1198
- elif (
1199
- purl in excluded_pkgs
1200
- or full_pkg in excluded_pkgs
1201
- or project_type_pkg in excluded_pkgs
1202
- ):
1203
- package_usage = "excluded"
1204
- data_obj = {
1205
- "id": vid,
1206
- "package": full_pkg_display,
1207
- "purl": purl,
1208
- "package_type": vuln_occ_dict.get("type"),
1209
- "package_usage": package_usage,
1210
- "version": version_used,
1211
- "fix_version": fixed_location,
1212
- "severity": vuln_occ_dict.get("severity"),
1213
- "cvss_score": vuln_occ_dict.get("cvss_score"),
1214
- "short_description": vuln_occ_dict.get("short_description"),
1215
- "related_urls": vuln_occ_dict.get("related_urls"),
1216
- "occurrence_count": direct_purls.get(purl, 0),
1217
- "reachable_flows": reached_purls.get(purl, 0),
1218
- }
1219
- json.dump(data_obj, outfile)
1220
- outfile.write("\n")
1221
-
1222
-
1223
- def analyse_pkg_risks(
1224
- project_type, scoped_pkgs, risk_results, risk_report_file=None
1225
- ):
1226
- """
1227
- Identify package risk and write to a json file
1228
-
1229
- :param project_type: Project type
1230
- :param scoped_pkgs: A dict of lists of required/optional/excluded packages.
1231
- :param risk_results: A dict of the risk metrics and scope for each package.
1232
- :param risk_report_file: Path to the JSON file for the risk audit findings.
1233
- """
1234
- if not risk_results:
1235
- return
1236
- table = Table(
1237
- title=f"Risk Audit Summary ({project_type})",
1238
- box=box.DOUBLE_EDGE,
1239
- header_style="bold magenta",
1240
- min_width=150,
1241
- )
1242
- report_data = []
1243
- required_pkgs = scoped_pkgs.get("required", [])
1244
- optional_pkgs = scoped_pkgs.get("optional", [])
1245
- excluded_pkgs = scoped_pkgs.get("excluded", [])
1246
- headers = ["Package", "Used?", "Risk Score", "Identified Risks"]
1247
- for h in headers:
1248
- justify = "left"
1249
- if h == "Risk Score":
1250
- justify = "right"
1251
- table.add_column(header=h, justify=justify)
1252
- for pkg, risk_obj in risk_results.items():
1253
- if not risk_obj:
1254
- continue
1255
- risk_metrics = risk_obj.get("risk_metrics")
1256
- scope = risk_obj.get("scope")
1257
- project_type_pkg = f"{project_type}:{pkg}".lower()
1258
- if project_type_pkg in required_pkgs:
1259
- scope = "required"
1260
- elif project_type_pkg in optional_pkgs:
1261
- scope = "optional"
1262
- elif project_type_pkg in excluded_pkgs:
1263
- scope = "excluded"
1264
- package_usage = "N/A"
1265
- package_usage_simple = "N/A"
1266
- if scope == "required":
1267
- package_usage = "[bright_green][bold]Yes"
1268
- package_usage_simple = "Yes"
1269
- if scope == "optional":
1270
- package_usage = "[magenta]No"
1271
- package_usage_simple = "No"
1272
- if not risk_metrics:
1273
- continue
1274
- if risk_metrics.get("risk_score") and (
1275
- risk_metrics.get("risk_score") > config.pkg_max_risk_score
1276
- or risk_metrics.get("pkg_private_on_public_registry_risk")
1277
- or risk_metrics.get("pkg_deprecated_risk")
1278
- ):
1279
- risk_score = f"""{round(risk_metrics.get("risk_score"), 2)}"""
1280
- data = [
1281
- pkg,
1282
- package_usage,
1283
- risk_score,
1284
- ]
1285
- edata = [
1286
- pkg,
1287
- package_usage_simple,
1288
- risk_score,
1289
- ]
1290
- risk_categories = []
1291
- risk_categories_simple = []
1292
- for rk, rv in risk_metrics.items():
1293
- if rk.endswith("_risk") and rv is True:
1294
- rcat = rk.replace("_risk", "")
1295
- help_text = config.risk_help_text.get(rcat)
1296
- # Only add texts that are available.
1297
- if help_text:
1298
- if rcat in (
1299
- "pkg_deprecated",
1300
- "pkg_private_on_public_registry",
1301
- ):
1302
- risk_categories.append(f":cross_mark: {help_text}")
1303
- else:
1304
- risk_categories.append(f":warning: {help_text}")
1305
- risk_categories_simple.append(help_text)
1306
- data.append("\n".join(risk_categories))
1307
- edata.append(", ".join(risk_categories_simple))
1308
- table.add_row(*data)
1309
- report_data.append(dict(zip(headers, edata)))
1310
- if report_data:
1311
- console.print(table)
1312
- # Store the risk audit findings in jsonl format
1313
- if risk_report_file:
1314
- with open(risk_report_file, "w", encoding="utf-8") as outfile:
1315
- for row in report_data:
1316
- json.dump(row, outfile)
1317
- outfile.write("\n")
1318
- else:
1319
- LOG.info("No package risks detected ✅")
1320
-
1321
-
1322
- def analyse_licenses(project_type, licenses_results, license_report_file=None):
1323
- """
1324
- Analyze package licenses
1325
-
1326
- :param project_type: Project type
1327
- :param licenses_results: A dict with the license results for each package.
1328
- :param license_report_file: Output filename for the license report.
1329
- """
1330
- if not licenses_results:
1331
- return
1332
- table = Table(
1333
- title=f"License Scan Summary ({project_type})",
1334
- box=box.DOUBLE_EDGE,
1335
- header_style="bold magenta",
1336
- min_width=150,
1337
- )
1338
- headers = ["Package", "Version", "License Id", "License conditions"]
1339
- for h in headers:
1340
- table.add_column(header=h)
1341
- report_data = []
1342
- for pkg, ll in licenses_results.items():
1343
- pkg_ver = pkg.split("@")
1344
- for lic in ll:
1345
- if not lic:
1346
- data = [*pkg_ver, "Unknown license"]
1347
- table.add_row(*data)
1348
- report_data.append(dict(zip(headers, data)))
1349
- elif lic["condition_flag"]:
1350
- conditions_str = ", ".join(lic["conditions"])
1351
- if "http" not in conditions_str:
1352
- conditions_str = (
1353
- conditions_str.replace("--", " for ")
1354
- .replace("-", " ")
1355
- .title()
1356
- )
1357
- data = [
1358
- *pkg_ver,
1359
- "{}{}".format(
1360
- (
1361
- "[cyan]"
1362
- if "GPL" in lic["spdx-id"]
1363
- or "CC-BY-" in lic["spdx-id"]
1364
- or "Facebook" in lic["spdx-id"]
1365
- or "WTFPL" in lic["spdx-id"]
1366
- else ""
1367
- ),
1368
- lic["spdx-id"],
1369
- ),
1370
- conditions_str,
1371
- ]
1372
- table.add_row(*data)
1373
- report_data.append(dict(zip(headers, data)))
1374
- if report_data:
1375
- console.print(table)
1376
- # Store the license scan findings in jsonl format
1377
- if license_report_file:
1378
- with open(license_report_file, "w", encoding="utf-8") as outfile:
1379
- for row in report_data:
1380
- json.dump(row, outfile)
1381
- outfile.write("\n")
1382
- else:
1383
- LOG.info("No license violation detected ✅")
1384
-
1385
-
1386
- def suggest_version(results, pkg_aliases=None, purl_aliases=None):
1387
- """
1388
- Provide version suggestions
1389
-
1390
- :param results: List of package issue objects or dicts
1391
- :param pkg_aliases: Dict of package names and aliases
1392
- :param purl_aliases: Dict of purl names and aliases
1393
- :return: Dict mapping each package to its suggested version
1394
- """
1395
- pkg_fix_map = {}
1396
- sug_map = {}
1397
- if not pkg_aliases:
1398
- pkg_aliases = {}
1399
- if not purl_aliases:
1400
- purl_aliases = {}
1401
- for res in results:
1402
- if isinstance(res, dict):
1403
- full_pkg = res.get("package")
1404
- fixed_location = res.get("fix_version")
1405
- matched_by = res.get("matched_by")
1406
- else:
1407
- package_issue = res.package_issue
1408
- full_pkg = package_issue.affected_location.package
1409
- fixed_location = package_issue.fixed_location
1410
- matched_by = res.matched_by
1411
- if package_issue.affected_location.vendor:
1412
- full_pkg = (
1413
- f"{package_issue.affected_location.vendor}:"
1414
- f"{package_issue.affected_location.package}"
1415
- )
1416
- if matched_by:
1417
- version = matched_by.split("|")[-1]
1418
- full_pkg = full_pkg + ":" + version
1419
- # De-alias package names
1420
- if purl_aliases.get(full_pkg):
1421
- full_pkg = purl_aliases.get(full_pkg)
1422
- else:
1423
- full_pkg = pkg_aliases.get(full_pkg, full_pkg)
1424
- version_upgrades = pkg_fix_map.get(full_pkg, set())
1425
- if fixed_location not in (
1426
- placeholder_fix_version,
1427
- placeholder_exclude_version,
1428
- ):
1429
- version_upgrades.add(fixed_location)
1430
- pkg_fix_map[full_pkg] = version_upgrades
1431
- for k, v in pkg_fix_map.items():
1432
- # Don't go near certain packages
1433
- if "kernel" in k or "openssl" in k or "openssh" in k:
1434
- continue
1435
- if v:
1436
- mversion = max_version(list(v))
1437
- if mversion:
1438
- sug_map[k] = mversion
1439
- return sug_map
1440
-
1441
-
1442
- def classify_links(related_urls):
1443
- """
1444
- Method to classify and identify well-known links
1445
-
1446
- :param related_urls: List of URLs
1447
- :return: Dictionary of classified links and URLs
1448
- """
1449
- clinks = {}
1450
- for rurl in related_urls:
1451
- if "github.com" in rurl and "/pull" in rurl:
1452
- clinks["GitHub PR"] = rurl
1453
- elif "github.com" in rurl and "/issues" in rurl:
1454
- clinks["GitHub Issue"] = rurl
1455
- elif "poc" in rurl:
1456
- clinks["poc"] = rurl
1457
- elif "apache.org" in rurl and "security" in rurl:
1458
- clinks["Apache Security"] = rurl
1459
- clinks["vendor"] = rurl
1460
- elif "debian.org" in rurl and "security" in rurl:
1461
- clinks["Debian Security"] = rurl
1462
- clinks["vendor"] = rurl
1463
- elif "security.gentoo.org" in rurl:
1464
- clinks["Gentoo Security"] = rurl
1465
- clinks["vendor"] = rurl
1466
- elif "usn.ubuntu.com" in rurl:
1467
- clinks["Ubuntu Security"] = rurl
1468
- clinks["vendor"] = rurl
1469
- elif "rubyonrails-security" in rurl:
1470
- clinks["Ruby Security"] = rurl
1471
- clinks["vendor"] = rurl
1472
- elif "support.apple.com" in rurl:
1473
- clinks["Apple Security"] = rurl
1474
- clinks["vendor"] = rurl
1475
- elif "gitlab.alpinelinux.org" in rurl or "bugs.busybox.net" in rurl:
1476
- clinks["vendor"] = rurl
1477
- elif "redhat.com" in rurl or "oracle.com" in rurl:
1478
- clinks["vendor"] = rurl
1479
- elif (
1480
- "openwall.com" in rurl
1481
- or "oss-security" in rurl
1482
- or "www.mail-archive.com" in rurl
1483
- or "lists.debian.org" in rurl
1484
- or "lists.fedoraproject.org" in rurl
1485
- or "portal.msrc.microsoft.com" in rurl
1486
- or "lists.opensuse.org" in rurl
1487
- ):
1488
- clinks["Mailing List"] = rurl
1489
- clinks["vendor"] = rurl
1490
- elif (
1491
- "exploit-db" in rurl
1492
- or "exploit-database" in rurl
1493
- or "seebug.org" in rurl
1494
- or "seclists.org" in rurl
1495
- or "nu11secur1ty" in rurl
1496
- ):
1497
- clinks["exploit"] = rurl
1498
- elif "github.com/advisories" in rurl:
1499
- clinks["GitHub Advisory"] = rurl
1500
- elif (
1501
- "hackerone" in rurl
1502
- or "bugcrowd" in rurl
1503
- or "bug-bounty" in rurl
1504
- or "huntr.dev" in rurl
1505
- or "bounties" in rurl
1506
- ):
1507
- clinks["Bug Bounty"] = rurl
1508
- elif "cwe.mitre.org" in rurl:
1509
- clinks["cwe"] = rurl
1510
- else:
1511
- clinks["other"] = rurl
1512
- return clinks
1513
-
1514
-
1515
- def find_purl_usages(bom_file, src_dir, reachables_slices_file):
1516
- """
1517
- Generates a list of reachable elements based on the given BOM file.
1518
-
1519
- :param bom_file: The path to the BOM file.
1520
- :type bom_file: str
1521
- :param src_dir: Source directory
1522
- :type src_dir: str
1523
- :param reachables_slices_file: Path to the reachables slices file
1524
- :type reachables_slices_file: str
1525
-
1526
- :return: Tuple of direct_purls and reached_purls based on the occurrence and
1527
- callstack evidences from the BOM. If reachables slices json were
1528
- found, the file is read first.
1529
- """
1530
- direct_purls = defaultdict(int)
1531
- reached_purls = defaultdict(int)
1532
- if (
1533
- not reachables_slices_file
1534
- and src_dir
1535
- and os.path.exists(os.path.join(src_dir, "reachables.slices.json"))
1536
- ):
1537
- reachables_slices_file = os.path.join(src_dir, "reachables.slices.json")
1538
- if reachables_slices_file:
1539
- with open(reachables_slices_file, "r", encoding="utf-8") as f:
1540
- reachables = json.load(f).get("reachables")
1541
- for flow in reachables:
1542
- if len(flow.get("purls", [])) > 0:
1543
- for apurl in flow.get("purls"):
1544
- reached_purls[apurl] += 1
1545
- if bom_file and os.path.exists(bom_file):
1546
- # For now we will also include usability slice as well
1547
- with open(bom_file, "r", encoding="utf-8") as f:
1548
- data = json.load(f)
1549
-
1550
- for c in data["components"]:
1551
- purl = c.get("purl", "")
1552
- if c.get("evidence") and c["evidence"].get("occurrences"):
1553
- direct_purls[purl] += len(c["evidence"].get("occurrences"))
1554
- return dict(direct_purls), dict(reached_purls)