|
| 1 | +# |
| 2 | +# Copyright (c) nexB Inc. and others. All rights reserved. |
| 3 | +# VulnerableCode is a trademark of nexB Inc. |
| 4 | +# SPDX-License-Identifier: Apache-2.0 |
| 5 | +# See http://www.apache.org/licenses/LICENSE-2.0 for the license text. |
| 6 | +# See https://github.com/aboutcode-org/vulnerablecode for support or download. |
| 7 | +# See https://aboutcode.org for more information about nexB OSS projects. |
| 8 | +# |
| 9 | + |
| 10 | + |
| 11 | +from urllib.parse import urlparse |
| 12 | + |
| 13 | +from vulnerabilities.models import AffectedByPackageRelatedVulnerability |
| 14 | +from vulnerabilities.models import Exploit |
| 15 | +from vulnerabilities.models import Package |
| 16 | +from vulnerabilities.models import Vulnerability |
| 17 | +from vulnerabilities.models import VulnerabilityReference |
| 18 | +from vulnerabilities.severity_systems import EPSS |
| 19 | +from vulnerabilities.weight_config import WEIGHT_CONFIG |
| 20 | + |
| 21 | +DEFAULT_WEIGHT = 5 |
| 22 | + |
| 23 | + |
| 24 | +def get_weighted_severity(severities): |
| 25 | + """ |
| 26 | + Weighted Severity is the maximum value obtained when each Severity is multiplied |
| 27 | + by its associated Weight/10. |
| 28 | + Example of Weighted Severity: max(7*(10/10), 8*(3/10), 6*(8/10)) = 7 |
| 29 | + """ |
| 30 | + |
| 31 | + score_map = { |
| 32 | + "low": 3, |
| 33 | + "moderate": 6.9, |
| 34 | + "medium": 6.9, |
| 35 | + "high": 8.9, |
| 36 | + "important": 8.9, |
| 37 | + "critical": 10.0, |
| 38 | + "urgent": 10.0, |
| 39 | + } |
| 40 | + |
| 41 | + score_list = [] |
| 42 | + for severity in severities: |
| 43 | + parsed_url = urlparse(severity.reference.url) |
| 44 | + severity_source = parsed_url.netloc.replace("www.", "", 1) |
| 45 | + weight = WEIGHT_CONFIG.get(severity_source, DEFAULT_WEIGHT) |
| 46 | + max_weight = float(weight) / 10 |
| 47 | + vul_score = severity.value |
| 48 | + try: |
| 49 | + vul_score = float(vul_score) |
| 50 | + vul_score_value = vul_score * max_weight |
| 51 | + except ValueError: |
| 52 | + vul_score = vul_score.lower() |
| 53 | + vul_score_value = score_map.get(vul_score, 0) * max_weight |
| 54 | + |
| 55 | + score_list.append(vul_score_value) |
| 56 | + return max(score_list) if score_list else 0 |
| 57 | + |
| 58 | + |
| 59 | +def get_exploitability_level(exploits, references, severities): |
| 60 | + """ |
| 61 | + Exploitability refers to the potential or |
| 62 | + probability of a software package vulnerability being exploited by |
| 63 | + malicious actors to compromise systems, applications, or networks. |
| 64 | + It is determined automatically by discovery of exploits. |
| 65 | + """ |
| 66 | + # no exploit known ( default .5) |
| 67 | + exploit_level = 0.5 |
| 68 | + |
| 69 | + if exploits: |
| 70 | + # Automatable Exploit with PoC script published OR known exploits (KEV) in the wild OR known ransomware |
| 71 | + exploit_level = 2 |
| 72 | + |
| 73 | + elif severities: |
| 74 | + # high EPSS. |
| 75 | + epss = severities.filter( |
| 76 | + scoring_system=EPSS.identifier, |
| 77 | + ) |
| 78 | + epss = any(float(epss.value) > 0.8 for epss in epss) |
| 79 | + if epss: |
| 80 | + exploit_level = 2 |
| 81 | + |
| 82 | + elif references: |
| 83 | + # PoC/Exploit script published |
| 84 | + ref_exploits = references.filter( |
| 85 | + reference_type=VulnerabilityReference.EXPLOIT, |
| 86 | + ) |
| 87 | + if ref_exploits: |
| 88 | + exploit_level = 1 |
| 89 | + |
| 90 | + return exploit_level |
| 91 | + |
| 92 | + |
| 93 | +def compute_vulnerability_risk(vulnerability: Vulnerability): |
| 94 | + """ |
| 95 | + Risk may be expressed as a number ranging from 0 to 10. |
| 96 | + Risk is calculated from weighted severity and exploitability values. |
| 97 | + It is the maximum value of (the weighted severity multiplied by its exploitability) or 10 |
| 98 | +
|
| 99 | + Risk = min(weighted severity * exploitability, 10) |
| 100 | + """ |
| 101 | + references = vulnerability.references |
| 102 | + severities = vulnerability.severities.select_related("reference") |
| 103 | + exploits = Exploit.objects.filter(vulnerability=vulnerability) |
| 104 | + if references.exists() or severities.exists() or exploits.exists(): |
| 105 | + weighted_severity = get_weighted_severity(severities) |
| 106 | + exploitability = get_exploitability_level(exploits, references, severities) |
| 107 | + return min(weighted_severity * exploitability, 10) |
| 108 | + |
| 109 | + |
| 110 | +def compute_package_risk(package: Package): |
| 111 | + """ |
| 112 | + Calculate the risk for a package by iterating over all vulnerabilities that affects this package |
| 113 | + and determining the associated risk. |
| 114 | + """ |
| 115 | + |
| 116 | + result = [] |
| 117 | + for pkg_related_vul in AffectedByPackageRelatedVulnerability.objects.filter( |
| 118 | + package=package |
| 119 | + ).prefetch_related("vulnerability"): |
| 120 | + if risk := compute_vulnerability_risk(pkg_related_vul.vulnerability): |
| 121 | + result.append(risk) |
| 122 | + |
| 123 | + if not result: |
| 124 | + return |
| 125 | + |
| 126 | + return f"{max(result):.2f}" |
0 commit comments