owasp-depscan 5.1.3__py3-none-any.whl → 5.1.5__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.
Potentially problematic release.
This version of owasp-depscan might be problematic. Click here for more details.
- depscan/cli.py +12 -93
- depscan/lib/analysis.py +325 -169
- depscan/lib/config.py +113 -5
- depscan/lib/csaf.py +1327 -1451
- depscan/lib/logger.py +8 -4
- depscan/lib/orasclient.py +127 -0
- {owasp_depscan-5.1.3.dist-info → owasp_depscan-5.1.5.dist-info}/METADATA +43 -4
- {owasp_depscan-5.1.3.dist-info → owasp_depscan-5.1.5.dist-info}/RECORD +12 -11
- {owasp_depscan-5.1.3.dist-info → owasp_depscan-5.1.5.dist-info}/LICENSE +0 -0
- {owasp_depscan-5.1.3.dist-info → owasp_depscan-5.1.5.dist-info}/WHEEL +0 -0
- {owasp_depscan-5.1.3.dist-info → owasp_depscan-5.1.5.dist-info}/entry_points.txt +0 -0
- {owasp_depscan-5.1.3.dist-info → owasp_depscan-5.1.5.dist-info}/top_level.txt +0 -0
depscan/lib/analysis.py
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
1
4
|
import json
|
|
2
5
|
import os.path
|
|
6
|
+
import re
|
|
3
7
|
from collections import OrderedDict, defaultdict
|
|
4
8
|
from dataclasses import dataclass
|
|
5
9
|
from typing import Dict, List, Optional
|
|
6
10
|
|
|
11
|
+
import cvss
|
|
12
|
+
from cvss import CVSSError
|
|
13
|
+
from packageurl import PackageURL
|
|
7
14
|
from rich import box
|
|
8
15
|
from rich.markdown import Markdown
|
|
9
16
|
from rich.panel import Panel
|
|
@@ -18,11 +25,11 @@ from depscan.lib import config
|
|
|
18
25
|
from depscan.lib.logger import LOG, console
|
|
19
26
|
from depscan.lib.utils import max_version
|
|
20
27
|
|
|
21
|
-
# -*- coding: utf-8 -*-
|
|
22
|
-
|
|
23
28
|
|
|
24
29
|
NEWLINE = "\\n"
|
|
25
30
|
|
|
31
|
+
CWE_SPLITTER = re.compile(r"(?<=CWE-)[0-9]\d{0,5}", re.IGNORECASE)
|
|
32
|
+
|
|
26
33
|
|
|
27
34
|
def best_fixed_location(sug_version, orig_fixed_location):
|
|
28
35
|
"""
|
|
@@ -43,9 +50,9 @@ def best_fixed_location(sug_version, orig_fixed_location):
|
|
|
43
50
|
if sug_version and orig_fixed_location:
|
|
44
51
|
if sug_version == placeholder_fix_version:
|
|
45
52
|
return ""
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if
|
|
53
|
+
tmp_a = sug_version.split(".")[0]
|
|
54
|
+
tmp_b = orig_fixed_location.split(".")[0]
|
|
55
|
+
if tmp_a == tmp_b:
|
|
49
56
|
return sug_version
|
|
50
57
|
# Handle the placeholder version used by OS distros
|
|
51
58
|
if orig_fixed_location == placeholder_fix_version:
|
|
@@ -90,12 +97,21 @@ def retrieve_bom_dependency_tree(bom_file):
|
|
|
90
97
|
bom_data = json.load(bfp)
|
|
91
98
|
if bom_data:
|
|
92
99
|
return bom_data.get("dependencies", []), bom_data
|
|
93
|
-
except
|
|
100
|
+
except json.JSONDecodeError:
|
|
94
101
|
pass
|
|
95
102
|
return [], None
|
|
96
103
|
|
|
97
104
|
|
|
98
105
|
def retrieve_oci_properties(bom_data):
|
|
106
|
+
"""
|
|
107
|
+
Retrieves OCI properties from the given BOM data.
|
|
108
|
+
|
|
109
|
+
:param bom_data: The BOM data to retrieve OCI properties from.
|
|
110
|
+
:type bom_data: dict
|
|
111
|
+
|
|
112
|
+
:return: A dictionary containing the retrieved OCI properties.
|
|
113
|
+
:rtype: dict
|
|
114
|
+
"""
|
|
99
115
|
props = {}
|
|
100
116
|
if not bom_data:
|
|
101
117
|
return props
|
|
@@ -119,17 +135,14 @@ def get_pkg_display(tree_pkg, current_pkg, extra_text=None):
|
|
|
119
135
|
tree_pkg == current_pkg or tree_pkg in current_pkg
|
|
120
136
|
)
|
|
121
137
|
if tree_pkg:
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
)
|
|
131
|
-
except Exception:
|
|
132
|
-
pass
|
|
138
|
+
if current_pkg.startswith("pkg:"):
|
|
139
|
+
purl_obj = parse_purl(current_pkg)
|
|
140
|
+
if purl_obj:
|
|
141
|
+
version_used = purl_obj.get("version")
|
|
142
|
+
if version_used:
|
|
143
|
+
full_pkg_display = (
|
|
144
|
+
f"""{purl_obj.get("name")}@{version_used}"""
|
|
145
|
+
)
|
|
133
146
|
if extra_text and highlightable:
|
|
134
147
|
full_pkg_display = f"{full_pkg_display} {extra_text}"
|
|
135
148
|
return full_pkg_display
|
|
@@ -234,7 +247,8 @@ def prepare_vdr(options: PrepareVdrOptions):
|
|
|
234
247
|
vulnerability details.
|
|
235
248
|
|
|
236
249
|
:param options: An instance of PrepareVdrOptions containing the function parameters.
|
|
237
|
-
:return:
|
|
250
|
+
:return: Vulnerability details, dictionary of prioritized items
|
|
251
|
+
:rtype: Tuple[List, Dict]
|
|
238
252
|
"""
|
|
239
253
|
if not options.results:
|
|
240
254
|
return [], {}
|
|
@@ -246,14 +260,11 @@ def prepare_vdr(options: PrepareVdrOptions):
|
|
|
246
260
|
min_width=150,
|
|
247
261
|
)
|
|
248
262
|
ids_seen = {}
|
|
249
|
-
direct_purls = options.direct_purls
|
|
250
|
-
|
|
251
|
-
direct_purls = {}
|
|
252
|
-
reached_purls = options.reached_purls
|
|
253
|
-
if not reached_purls:
|
|
254
|
-
reached_purls = {}
|
|
263
|
+
direct_purls = options.direct_purls or {}
|
|
264
|
+
reached_purls = options.reached_purls or {}
|
|
255
265
|
required_pkgs = options.scoped_pkgs.get("required", [])
|
|
256
266
|
optional_pkgs = options.scoped_pkgs.get("optional", [])
|
|
267
|
+
fp_count = 0
|
|
257
268
|
pkg_attention_count = 0
|
|
258
269
|
critical_count = 0
|
|
259
270
|
has_poc_count = 0
|
|
@@ -288,6 +299,10 @@ def prepare_vdr(options: PrepareVdrOptions):
|
|
|
288
299
|
for vuln_occ_dict in options.results:
|
|
289
300
|
vid = vuln_occ_dict.get("id")
|
|
290
301
|
problem_type = vuln_occ_dict.get("problem_type")
|
|
302
|
+
cwes = []
|
|
303
|
+
if problem_type:
|
|
304
|
+
cwes = split_cwe(problem_type)
|
|
305
|
+
has_flagged_cwe = False
|
|
291
306
|
package_issue = vuln_occ_dict.get("package_issue")
|
|
292
307
|
matched_by = vuln_occ_dict.get("matched_by")
|
|
293
308
|
full_pkg = package_issue["affected_location"].get("package")
|
|
@@ -309,7 +324,6 @@ def prepare_vdr(options: PrepareVdrOptions):
|
|
|
309
324
|
f"{vendor}:"
|
|
310
325
|
f"{package_issue['affected_location'].get('package')}"
|
|
311
326
|
)
|
|
312
|
-
version = None
|
|
313
327
|
if matched_by:
|
|
314
328
|
version = matched_by.split("|")[-1]
|
|
315
329
|
full_pkg = full_pkg + ":" + version
|
|
@@ -323,55 +337,86 @@ def prepare_vdr(options: PrepareVdrOptions):
|
|
|
323
337
|
package_type = None
|
|
324
338
|
insights = []
|
|
325
339
|
plain_insights = []
|
|
340
|
+
purl_obj = None
|
|
341
|
+
vendor = None
|
|
326
342
|
if purl and purl.startswith("pkg:"):
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
343
|
+
purl_obj = parse_purl(purl)
|
|
344
|
+
if purl_obj:
|
|
345
|
+
version_used = purl_obj.get("version")
|
|
346
|
+
package_type = purl_obj.get("type")
|
|
347
|
+
qualifiers = purl_obj.get("qualifiers", {})
|
|
348
|
+
if package_type in config.OS_PKG_TYPES:
|
|
349
|
+
vendor = package_issue["affected_location"].get("vendor")
|
|
350
|
+
if (
|
|
351
|
+
vendor
|
|
352
|
+
and oci_product_types
|
|
353
|
+
and vendor not in oci_product_types
|
|
354
|
+
):
|
|
355
|
+
# Bug #170 - do not report CVEs belonging to other distros
|
|
356
|
+
if vendor in config.OS_PKG_TYPES:
|
|
357
|
+
fp_count += 1
|
|
358
|
+
continue
|
|
359
|
+
# Some nvd data might match application CVEs for
|
|
360
|
+
# OS vendors which can be filtered
|
|
361
|
+
if package_issue["affected_location"].get("cpe_uri"):
|
|
362
|
+
all_parts = CPE_FULL_REGEX.match(
|
|
363
|
+
package_issue["affected_location"].get(
|
|
364
|
+
"cpe_uri"
|
|
365
|
+
)
|
|
366
|
+
)
|
|
367
|
+
if (
|
|
368
|
+
all_parts
|
|
369
|
+
and all_parts.group("target_sw") != "*"
|
|
370
|
+
and all_parts.group("target_sw")
|
|
371
|
+
not in config.OS_PKG_TYPES
|
|
372
|
+
):
|
|
373
|
+
fp_count += 1
|
|
374
|
+
continue
|
|
375
|
+
insights.append(
|
|
376
|
+
f"[#7C8082]:telescope: Vendor {vendor}[/#7C8082]"
|
|
377
|
+
)
|
|
378
|
+
plain_insights.append(f"Vendor {vendor}")
|
|
379
|
+
has_os_packages = True
|
|
380
|
+
for acwe in cwes:
|
|
381
|
+
if acwe in config.OS_VULN_KEY_CWES:
|
|
382
|
+
has_flagged_cwe = True
|
|
383
|
+
break
|
|
384
|
+
# Don't flag the cwe for ignorable os packages
|
|
385
|
+
if has_flagged_cwe and (
|
|
386
|
+
purl_obj.get("name") in config.OS_PKG_UNINSTALLABLE
|
|
387
|
+
or purl_obj.get("name") in config.OS_PKG_IGNORABLE
|
|
388
|
+
or vendor in config.OS_PKG_IGNORABLE
|
|
389
|
+
):
|
|
390
|
+
has_flagged_cwe = False
|
|
391
|
+
else:
|
|
334
392
|
if (
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
and package_issue["affected_location"].get("vendor")
|
|
338
|
-
not in oci_product_types
|
|
393
|
+
purl_obj.get("name") in config.OS_PKG_IGNORABLE
|
|
394
|
+
or vendor in config.OS_PKG_IGNORABLE
|
|
339
395
|
):
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
)
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
if package_issue["affected_location"].get(
|
|
359
|
-
"vendor"
|
|
360
|
-
) not in ("suse",):
|
|
361
|
-
insights.append(
|
|
362
|
-
f"[#7C8082]:telescope: Vendor {package_issue['affected_location'].get('vendor')}"
|
|
363
|
-
)
|
|
364
|
-
plain_insights.append(
|
|
365
|
-
f"Vendor {package_issue['affected_location'].get('vendor')}"
|
|
366
|
-
)
|
|
367
|
-
has_os_packages = True
|
|
396
|
+
insights.append(
|
|
397
|
+
"[#7C8082]:mute: Suppress for containers[/#7C8082]"
|
|
398
|
+
)
|
|
399
|
+
plain_insights.append("Suppress for containers")
|
|
400
|
+
elif (
|
|
401
|
+
purl_obj.get("name") in config.OS_PKG_UNINSTALLABLE
|
|
402
|
+
):
|
|
403
|
+
insights.append(
|
|
404
|
+
"[#7C8082]:scissors: Uninstall candidate[/#7C8082]"
|
|
405
|
+
)
|
|
406
|
+
plain_insights.append("Uninstall candidate")
|
|
407
|
+
# If the flag remains after all the suppressions then add it as an insight
|
|
408
|
+
if has_flagged_cwe:
|
|
409
|
+
insights.append(
|
|
410
|
+
"[#7C8082]:triangular_flag: Flagged weakness[/#7C8082]"
|
|
411
|
+
)
|
|
412
|
+
plain_insights.append("Flagged weakness")
|
|
413
|
+
if qualifiers:
|
|
368
414
|
if "ubuntu" in qualifiers.get("distro", ""):
|
|
369
415
|
has_ubuntu_packages = True
|
|
370
416
|
if "rhel" in qualifiers.get("distro", ""):
|
|
371
417
|
has_redhat_packages = True
|
|
372
|
-
except Exception:
|
|
373
|
-
pass
|
|
374
418
|
if ids_seen.get(vid + purl):
|
|
419
|
+
fp_count += 1
|
|
375
420
|
continue
|
|
376
421
|
# Mark this CVE + pkg as seen to avoid duplicates
|
|
377
422
|
ids_seen[vid + purl] = True
|
|
@@ -421,7 +466,11 @@ def prepare_vdr(options: PrepareVdrOptions):
|
|
|
421
466
|
)
|
|
422
467
|
if is_required and package_type not in config.OS_PKG_TYPES:
|
|
423
468
|
if direct_purls.get(purl):
|
|
424
|
-
package_usage =
|
|
469
|
+
package_usage = (
|
|
470
|
+
f":direct_hit: Used in [info]"
|
|
471
|
+
f"{str(direct_purls.get(purl))}"
|
|
472
|
+
f"[/info] locations"
|
|
473
|
+
)
|
|
425
474
|
plain_package_usage = (
|
|
426
475
|
f"Used in {str(direct_purls.get(purl))} locations"
|
|
427
476
|
)
|
|
@@ -442,7 +491,10 @@ def prepare_vdr(options: PrepareVdrOptions):
|
|
|
442
491
|
plain_package_usage = "Local install"
|
|
443
492
|
has_os_packages = True
|
|
444
493
|
else:
|
|
445
|
-
package_usage =
|
|
494
|
+
package_usage = (
|
|
495
|
+
"[spring_green4]:notebook: Indirect "
|
|
496
|
+
"dependency[/spring_green4]"
|
|
497
|
+
)
|
|
446
498
|
plain_package_usage = "Indirect dependency"
|
|
447
499
|
if package_usage != "N/A":
|
|
448
500
|
insights.append(package_usage)
|
|
@@ -483,10 +535,26 @@ def prepare_vdr(options: PrepareVdrOptions):
|
|
|
483
535
|
)
|
|
484
536
|
plain_insights.append("Reachable and Exploitable")
|
|
485
537
|
has_reachable_exploit_count += 1
|
|
486
|
-
# Fail safe. Packages with exploits and direct usage without
|
|
487
|
-
# are still considered reachable to reduce
|
|
538
|
+
# Fail safe. Packages with exploits and direct usage without
|
|
539
|
+
# a reachable flow are still considered reachable to reduce
|
|
540
|
+
# false negatives
|
|
488
541
|
if not reached_purls.get(purl):
|
|
489
542
|
reached_purls[purl] = 1
|
|
543
|
+
elif has_flagged_cwe:
|
|
544
|
+
if (vendor and vendor in ("gnu",)) or (
|
|
545
|
+
purl_obj and purl_obj.get("name") in ("glibc", "openssl")
|
|
546
|
+
):
|
|
547
|
+
insights.append(
|
|
548
|
+
"[bright_red]:exclamation_mark: Reachable and Exploitable[/bright_red]"
|
|
549
|
+
)
|
|
550
|
+
plain_insights.append("Reachable and Exploitable")
|
|
551
|
+
has_reachable_exploit_count += 1
|
|
552
|
+
else:
|
|
553
|
+
insights.append(
|
|
554
|
+
"[bright_red]:exclamation_mark: Exploitable[/bright_red]"
|
|
555
|
+
)
|
|
556
|
+
plain_insights.append("Exploitable")
|
|
557
|
+
has_exploit_count += 1
|
|
490
558
|
else:
|
|
491
559
|
insights.append(
|
|
492
560
|
"[bright_red]:exclamation_mark: Known Exploits[/bright_red]"
|
|
@@ -553,74 +621,54 @@ def prepare_vdr(options: PrepareVdrOptions):
|
|
|
553
621
|
"state": "in_triage",
|
|
554
622
|
"detail": f"Dependency Tree: {json.dumps(pkg_tree_list)}",
|
|
555
623
|
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
score = float(vuln_occ_dict.get("cvss_score"))
|
|
559
|
-
except Exception:
|
|
560
|
-
pass
|
|
561
|
-
sev_to_use = pkg_severity.lower()
|
|
562
|
-
if sev_to_use not in (
|
|
563
|
-
"critical",
|
|
564
|
-
"high",
|
|
565
|
-
"medium",
|
|
566
|
-
"low",
|
|
567
|
-
"info",
|
|
568
|
-
"none",
|
|
569
|
-
):
|
|
570
|
-
sev_to_use = "unknown"
|
|
571
|
-
ratings = [
|
|
624
|
+
ratings = cvss_to_vdr_rating(vuln_occ_dict)
|
|
625
|
+
properties = [
|
|
572
626
|
{
|
|
573
|
-
"
|
|
574
|
-
"
|
|
575
|
-
|
|
576
|
-
|
|
627
|
+
"name": "depscan:insights",
|
|
628
|
+
"value": "\\n".join(plain_insights),
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
"name": "depscan:prioritized",
|
|
632
|
+
"value": "true" if pkg_group_rows.get(purl) else "false",
|
|
633
|
+
},
|
|
577
634
|
]
|
|
635
|
+
affected_version_range = get_version_range(package_issue, purl)
|
|
636
|
+
if affected_version_range:
|
|
637
|
+
properties.append(affected_version_range)
|
|
578
638
|
advisories = []
|
|
579
639
|
for k, v in clinks.items():
|
|
580
640
|
advisories.append({"title": k, "url": v})
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
"properties": [
|
|
601
|
-
{
|
|
602
|
-
"name": "depscan:insights",
|
|
603
|
-
"value": "\\n".join(plain_insights),
|
|
604
|
-
},
|
|
605
|
-
{
|
|
606
|
-
"name": "depscan:prioritized",
|
|
607
|
-
"value": "true"
|
|
608
|
-
if pkg_group_rows.get(purl)
|
|
609
|
-
else "false",
|
|
610
|
-
},
|
|
611
|
-
],
|
|
612
|
-
}
|
|
613
|
-
)
|
|
641
|
+
vuln = {
|
|
642
|
+
"bom-ref": f"{vid}/{purl}",
|
|
643
|
+
"id": vid,
|
|
644
|
+
"source": source,
|
|
645
|
+
"ratings": ratings,
|
|
646
|
+
"cwes": cwes,
|
|
647
|
+
"description": vuln_occ_dict.get("short_description"),
|
|
648
|
+
"recommendation": recommendation,
|
|
649
|
+
"advisories": advisories,
|
|
650
|
+
"analysis": analysis,
|
|
651
|
+
"affects": affects,
|
|
652
|
+
"properties": properties,
|
|
653
|
+
}
|
|
654
|
+
if source_orig_time := vuln_occ_dict.get("source_orig_time"):
|
|
655
|
+
vuln["published"] = source_orig_time
|
|
656
|
+
if source_update_time := vuln_occ_dict.get("source_update_time"):
|
|
657
|
+
vuln["updated"] = source_update_time
|
|
658
|
+
pkg_vulnerabilities.append(vuln)
|
|
659
|
+
|
|
614
660
|
if not options.no_vuln_table:
|
|
615
661
|
console.print()
|
|
616
662
|
console.print(table)
|
|
617
|
-
console.print()
|
|
618
663
|
if pkg_group_rows:
|
|
619
664
|
psection = Markdown(
|
|
620
|
-
"""
|
|
665
|
+
"""
|
|
666
|
+
Next Steps
|
|
667
|
+
----------
|
|
621
668
|
|
|
622
669
|
Below are the vulnerabilities prioritized by depscan. Follow your team's remediation workflow to mitigate these findings.
|
|
623
|
-
"""
|
|
670
|
+
""",
|
|
671
|
+
justify="left",
|
|
624
672
|
)
|
|
625
673
|
console.print(psection)
|
|
626
674
|
utable = Table(
|
|
@@ -653,14 +701,14 @@ Below are the vulnerabilities prioritized by depscan. Follow your team's remedia
|
|
|
653
701
|
if has_reachable_exploit_count:
|
|
654
702
|
rmessage = (
|
|
655
703
|
f":point_right: [magenta]{has_reachable_exploit_count}"
|
|
656
|
-
f"[/magenta] out of {len(
|
|
704
|
+
f"[/magenta] out of {len(pkg_vulnerabilities)} vulnerabilities "
|
|
657
705
|
f"have [dark magenta]reachable[/dark magenta] exploits and requires your ["
|
|
658
706
|
f"magenta]immediate[/magenta] attention."
|
|
659
707
|
)
|
|
660
708
|
else:
|
|
661
709
|
rmessage = (
|
|
662
710
|
f":point_right: [magenta]{has_exploit_count}"
|
|
663
|
-
f"[/magenta] out of {len(
|
|
711
|
+
f"[/magenta] out of {len(pkg_vulnerabilities)} vulnerabilities "
|
|
664
712
|
f"have known exploits and requires your ["
|
|
665
713
|
f"magenta]immediate[/magenta] attention."
|
|
666
714
|
)
|
|
@@ -677,19 +725,25 @@ Below are the vulnerabilities prioritized by depscan. Follow your team's remedia
|
|
|
677
725
|
)
|
|
678
726
|
else:
|
|
679
727
|
rmessage += (
|
|
680
|
-
"\
|
|
728
|
+
"\n:scissors: Consider trimming this image by removing any "
|
|
681
729
|
"unwanted packages. Alternatively, use a slim "
|
|
682
730
|
"base image."
|
|
683
731
|
)
|
|
684
732
|
if distro_packages_count and distro_packages_count < len(
|
|
685
|
-
|
|
733
|
+
pkg_vulnerabilities
|
|
686
734
|
):
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
f"
|
|
692
|
-
|
|
735
|
+
if (
|
|
736
|
+
len(pkg_vulnerabilities)
|
|
737
|
+
> config.max_distro_vulnerabilities
|
|
738
|
+
):
|
|
739
|
+
rmessage += f"\nNOTE: Check if the base image or the kernel version used is End-of-Life (EOL)."
|
|
740
|
+
else:
|
|
741
|
+
rmessage += (
|
|
742
|
+
f"\nNOTE: [magenta]{distro_packages_count}"
|
|
743
|
+
f"[/magenta] distro-specific vulnerabilities "
|
|
744
|
+
f"out of {len(pkg_vulnerabilities)} could be prioritized "
|
|
745
|
+
f"for updates."
|
|
746
|
+
)
|
|
693
747
|
if has_redhat_packages:
|
|
694
748
|
rmessage += """\nNOTE: Vulnerabilities in RedHat packages with status "out of support" or "won't fix" are excluded from this result."""
|
|
695
749
|
if has_ubuntu_packages:
|
|
@@ -715,7 +769,7 @@ Below are the vulnerabilities prioritized by depscan. Follow your team's remedia
|
|
|
715
769
|
else:
|
|
716
770
|
rmessage = (
|
|
717
771
|
f":point_right: [info]{pkg_attention_count}"
|
|
718
|
-
f"[/info] out of {len(
|
|
772
|
+
f"[/info] out of {len(pkg_vulnerabilities)} vulnerabilities "
|
|
719
773
|
f"requires your attention."
|
|
720
774
|
)
|
|
721
775
|
if fix_version_count:
|
|
@@ -727,10 +781,15 @@ Below are the vulnerabilities prioritized by depscan. Follow your team's remedia
|
|
|
727
781
|
"remediate."
|
|
728
782
|
)
|
|
729
783
|
else:
|
|
784
|
+
v_text = (
|
|
785
|
+
"vulnerability"
|
|
786
|
+
if fix_version_count == 1
|
|
787
|
+
else "vulnerabilities"
|
|
788
|
+
)
|
|
730
789
|
rmessage += (
|
|
731
790
|
f"\nYou can remediate [bright_green]"
|
|
732
791
|
f"{fix_version_count}[/bright_green] "
|
|
733
|
-
f"{
|
|
792
|
+
f"{v_text} "
|
|
734
793
|
f"by updating the packages using the fix "
|
|
735
794
|
f"version :thumbsup:"
|
|
736
795
|
)
|
|
@@ -756,11 +815,7 @@ Below are the vulnerabilities prioritized by depscan. Follow your team's remedia
|
|
|
756
815
|
rmessage = (
|
|
757
816
|
":white_medium_small_square: Prioritize any vulnerabilities in libraries such "
|
|
758
817
|
"as glibc, openssl, or libcurl.\nAdditionally, "
|
|
759
|
-
"prioritize the vulnerabilities
|
|
760
|
-
"provide executable binaries when there is a "
|
|
761
|
-
"Remote Code Execution or File Write "
|
|
762
|
-
"vulnerability in the containerized application "
|
|
763
|
-
"or service."
|
|
818
|
+
"prioritize the vulnerabilities with 'Flagged weakness' under insights."
|
|
764
819
|
)
|
|
765
820
|
rmessage += (
|
|
766
821
|
"\nVulnerabilities in Linux Kernel packages can "
|
|
@@ -817,10 +872,13 @@ Below are the vulnerabilities prioritized by depscan. Follow your team's remedia
|
|
|
817
872
|
(k, v) for v, k in sorted_reached_purls
|
|
818
873
|
)
|
|
819
874
|
rsection = Markdown(
|
|
820
|
-
"""
|
|
875
|
+
"""
|
|
876
|
+
Proactive Measures
|
|
877
|
+
------------------
|
|
821
878
|
|
|
822
879
|
Below are the top reachable packages identified by depscan. Setup alerts and notifications to actively monitor these packages for new vulnerabilities and exploits.
|
|
823
|
-
"""
|
|
880
|
+
""",
|
|
881
|
+
justify="left",
|
|
824
882
|
)
|
|
825
883
|
console.print(rsection)
|
|
826
884
|
rtable = Table(
|
|
@@ -840,6 +898,99 @@ Below are the top reachable packages identified by depscan. Setup alerts and not
|
|
|
840
898
|
return pkg_vulnerabilities, pkg_group_rows
|
|
841
899
|
|
|
842
900
|
|
|
901
|
+
def get_version_range(package_issue, purl):
|
|
902
|
+
"""
|
|
903
|
+
Generates a version range object for inclusion in the vdr file.
|
|
904
|
+
|
|
905
|
+
:param package_issue: Vulnerability data dict
|
|
906
|
+
:param purl: Package URL string
|
|
907
|
+
|
|
908
|
+
:return: A list containing a dictionary with version range information.
|
|
909
|
+
"""
|
|
910
|
+
new_prop = {}
|
|
911
|
+
if (affected_location := package_issue.get("affected_location")) and (
|
|
912
|
+
affected_version := affected_location.get("version")
|
|
913
|
+
):
|
|
914
|
+
try:
|
|
915
|
+
ppurl = PackageURL.from_string(purl)
|
|
916
|
+
new_prop = {
|
|
917
|
+
"name": "affectedVersionRange",
|
|
918
|
+
"value": f"{ppurl.name}@" f"{affected_version}",
|
|
919
|
+
}
|
|
920
|
+
if ppurl.namespace:
|
|
921
|
+
new_prop["value"] = f'{ppurl.namespace}/{new_prop["value"]}'
|
|
922
|
+
except ValueError:
|
|
923
|
+
ppurl = purl.split("@")
|
|
924
|
+
if len(ppurl) == 2:
|
|
925
|
+
new_prop = {
|
|
926
|
+
"name": "affectedVersionRange",
|
|
927
|
+
"value": f"{ppurl[0]}@{affected_version}",
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
return new_prop
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
def cvss_to_vdr_rating(vuln_occ_dict):
|
|
934
|
+
"""
|
|
935
|
+
Generates a rating object for inclusion in the vdr file.
|
|
936
|
+
|
|
937
|
+
:param vuln_occ_dict: Vulnerability data
|
|
938
|
+
|
|
939
|
+
:return: A list containing a dictionary with CVSS score information.
|
|
940
|
+
"""
|
|
941
|
+
cvss_score = vuln_occ_dict.get("cvss_score", 2.0)
|
|
942
|
+
with contextlib.suppress(ValueError, TypeError):
|
|
943
|
+
cvss_score = float(cvss_score)
|
|
944
|
+
if (pkg_severity := vuln_occ_dict.get("severity", "").lower()) not in (
|
|
945
|
+
"critical",
|
|
946
|
+
"high",
|
|
947
|
+
"medium",
|
|
948
|
+
"low",
|
|
949
|
+
"info",
|
|
950
|
+
"none",
|
|
951
|
+
):
|
|
952
|
+
pkg_severity = "unknown"
|
|
953
|
+
ratings = [
|
|
954
|
+
{
|
|
955
|
+
"score": cvss_score,
|
|
956
|
+
"severity": pkg_severity,
|
|
957
|
+
}
|
|
958
|
+
]
|
|
959
|
+
method = "31"
|
|
960
|
+
if vuln_occ_dict.get("cvss_v3") and (
|
|
961
|
+
vector_string := vuln_occ_dict["cvss_v3"].get("vector_string")
|
|
962
|
+
):
|
|
963
|
+
ratings[0]["vector"] = vector_string
|
|
964
|
+
with contextlib.suppress(CVSSError):
|
|
965
|
+
method = cvss.CVSS3(vector_string).as_json().get("version")
|
|
966
|
+
method = method.replace(".", "").replace("0", "")
|
|
967
|
+
ratings[0]["method"] = f"CVSSv{method}"
|
|
968
|
+
|
|
969
|
+
return ratings
|
|
970
|
+
|
|
971
|
+
|
|
972
|
+
def split_cwe(cwe):
|
|
973
|
+
"""
|
|
974
|
+
Split the given CWE string into a list of CWE IDs.
|
|
975
|
+
|
|
976
|
+
:param cwe: The problem issue taken from a vulnerability object
|
|
977
|
+
|
|
978
|
+
:return: A list of CWE IDs
|
|
979
|
+
:rtype: list
|
|
980
|
+
"""
|
|
981
|
+
cwe_ids = []
|
|
982
|
+
|
|
983
|
+
if isinstance(cwe, str):
|
|
984
|
+
cwe_ids = re.findall(CWE_SPLITTER, cwe)
|
|
985
|
+
elif isinstance(cwe, list):
|
|
986
|
+
cwes = "|".join(cwe)
|
|
987
|
+
cwe_ids = re.findall(CWE_SPLITTER, cwes)
|
|
988
|
+
|
|
989
|
+
with contextlib.suppress(ValueError, TypeError):
|
|
990
|
+
cwe_ids = [int(cwe_id) for cwe_id in cwe_ids]
|
|
991
|
+
return cwe_ids
|
|
992
|
+
|
|
993
|
+
|
|
843
994
|
def summary_stats(results):
|
|
844
995
|
"""
|
|
845
996
|
Generate summary stats
|
|
@@ -875,7 +1026,8 @@ def jsonl_report(
|
|
|
875
1026
|
reached_purls,
|
|
876
1027
|
):
|
|
877
1028
|
"""
|
|
878
|
-
Produce vulnerability occurrence report in jsonlines format
|
|
1029
|
+
DEPRECATED: Produce vulnerability occurrence report in jsonlines format
|
|
1030
|
+
This method should use the pkg_vulnerabilities from prepare_vdr
|
|
879
1031
|
|
|
880
1032
|
:param scoped_pkgs: A dict of lists of required/optional/excluded packages.
|
|
881
1033
|
:param sug_version_dict: A dict mapping package names to suggested versions.
|
|
@@ -884,6 +1036,8 @@ def jsonl_report(
|
|
|
884
1036
|
:param results: List of vulnerabilities found
|
|
885
1037
|
:param pkg_aliases: Package alias
|
|
886
1038
|
:param out_file_name: Output filename
|
|
1039
|
+
:param direct_purls: A list of direct purls
|
|
1040
|
+
:param reached_purls: A list of reached purls
|
|
887
1041
|
"""
|
|
888
1042
|
ids_seen = {}
|
|
889
1043
|
required_pkgs = scoped_pkgs.get("required", [])
|
|
@@ -916,18 +1070,15 @@ def jsonl_report(
|
|
|
916
1070
|
version_used = package_issue["affected_location"].get("version")
|
|
917
1071
|
purl = purl_aliases.get(full_pkg, full_pkg)
|
|
918
1072
|
if purl:
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
.get("version")}"""
|
|
929
|
-
except Exception:
|
|
930
|
-
pass
|
|
1073
|
+
purl_obj = parse_purl(purl)
|
|
1074
|
+
if purl_obj:
|
|
1075
|
+
version_used = purl_obj.get("version")
|
|
1076
|
+
if purl_obj.get("namespace"):
|
|
1077
|
+
full_pkg = f"""{purl_obj.get("namespace")}/
|
|
1078
|
+
{purl_obj.get("name")}@{purl_obj.get("version")}"""
|
|
1079
|
+
else:
|
|
1080
|
+
full_pkg = f"""{purl_obj.get("name")}@{purl_obj
|
|
1081
|
+
.get("version")}"""
|
|
931
1082
|
if ids_seen.get(vid + purl):
|
|
932
1083
|
continue
|
|
933
1084
|
# On occasions, this could still result in duplicates if the
|
|
@@ -1143,6 +1294,7 @@ def suggest_version(results, pkg_aliases=None, purl_aliases=None):
|
|
|
1143
1294
|
|
|
1144
1295
|
:param results: List of package issue objects or dicts
|
|
1145
1296
|
:param pkg_aliases: Dict of package names and aliases
|
|
1297
|
+
:param purl_aliases: Dict of purl names and aliases
|
|
1146
1298
|
:return: Dict mapping each package to its suggested version
|
|
1147
1299
|
"""
|
|
1148
1300
|
pkg_fix_map = {}
|
|
@@ -1166,7 +1318,6 @@ def suggest_version(results, pkg_aliases=None, purl_aliases=None):
|
|
|
1166
1318
|
f"{package_issue.affected_location.vendor}:"
|
|
1167
1319
|
f"{package_issue.affected_location.package}"
|
|
1168
1320
|
)
|
|
1169
|
-
version = None
|
|
1170
1321
|
if matched_by:
|
|
1171
1322
|
version = matched_by.split("|")[-1]
|
|
1172
1323
|
full_pkg = full_pkg + ":" + version
|
|
@@ -1176,9 +1327,9 @@ def suggest_version(results, pkg_aliases=None, purl_aliases=None):
|
|
|
1176
1327
|
else:
|
|
1177
1328
|
full_pkg = pkg_aliases.get(full_pkg, full_pkg)
|
|
1178
1329
|
version_upgrades = pkg_fix_map.get(full_pkg, set())
|
|
1179
|
-
if (
|
|
1180
|
-
|
|
1181
|
-
|
|
1330
|
+
if fixed_location not in (
|
|
1331
|
+
placeholder_fix_version,
|
|
1332
|
+
placeholder_exclude_version,
|
|
1182
1333
|
):
|
|
1183
1334
|
version_upgrades.add(fixed_location)
|
|
1184
1335
|
pkg_fix_map[full_pkg] = version_upgrades
|
|
@@ -1261,6 +1412,8 @@ def classify_links(related_urls):
|
|
|
1261
1412
|
clinks["Bug Bounty"] = rurl
|
|
1262
1413
|
elif "cwe.mitre.org" in rurl:
|
|
1263
1414
|
clinks["cwe"] = rurl
|
|
1415
|
+
else:
|
|
1416
|
+
clinks["other"] = rurl
|
|
1264
1417
|
return clinks
|
|
1265
1418
|
|
|
1266
1419
|
|
|
@@ -1268,12 +1421,16 @@ def find_purl_usages(bom_file, src_dir, reachables_slices_file):
|
|
|
1268
1421
|
"""
|
|
1269
1422
|
Generates a list of reachable elements based on the given BOM file.
|
|
1270
1423
|
|
|
1271
|
-
:param bom_file
|
|
1272
|
-
:
|
|
1424
|
+
:param bom_file: The path to the BOM file.
|
|
1425
|
+
:type bom_file: str
|
|
1426
|
+
:param src_dir: Source directory
|
|
1427
|
+
:type src_dir: str
|
|
1273
1428
|
:param reachables_slices_file: Path to the reachables slices file
|
|
1429
|
+
:type reachables_slices_file: str
|
|
1274
1430
|
|
|
1275
|
-
:return: Tuple of direct_purls and reached_purls based on the occurrence and
|
|
1276
|
-
|
|
1431
|
+
:return: Tuple of direct_purls and reached_purls based on the occurrence and
|
|
1432
|
+
callstack evidences from the BOM. If reachables slices json were
|
|
1433
|
+
found, the file is read first.
|
|
1277
1434
|
"""
|
|
1278
1435
|
direct_purls = defaultdict(int)
|
|
1279
1436
|
reached_purls = defaultdict(int)
|
|
@@ -1297,7 +1454,6 @@ def find_purl_usages(bom_file, src_dir, reachables_slices_file):
|
|
|
1297
1454
|
|
|
1298
1455
|
for c in data["components"]:
|
|
1299
1456
|
purl = c["purl"]
|
|
1300
|
-
if c.get("evidence"):
|
|
1301
|
-
|
|
1302
|
-
direct_purls[purl] += len(c["evidence"].get("occurrences"))
|
|
1457
|
+
if c.get("evidence") and c["evidence"].get("occurrences"):
|
|
1458
|
+
direct_purls[purl] += len(c["evidence"].get("occurrences"))
|
|
1303
1459
|
return dict(direct_purls), dict(reached_purls)
|