Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Calculating Risk in VulnerableCode #1593

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
128 changes: 128 additions & 0 deletions vulnerabilities/risk.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import os
import re

from vulnerabilities.models import Exploit
from vulnerabilities.models import Package
from vulnerabilities.models import PackageRelatedVulnerability
from vulnerabilities.models import Vulnerability
from vulnerabilities.models import VulnerabilityReference
from vulnerabilities.severity_systems import EPSS
from vulnerabilities.utils import load_json

BASE_DIR = os.path.dirname(os.path.abspath(__file__))


def get_weighted_severity(severities):
"""
Weighted Severity is the maximum value obtained when each Severity is multiplied
by its associated Weight/10.
Example of Weighted Severity: max(7*(10/10), 8*(3/10), 6*(8/10)) = 7
"""
weight_config_path = os.path.join(BASE_DIR, "..", "weight_config.json")
weight_config = load_json(weight_config_path)

score_map = {
"low": 3,
"moderate": 6.9,
"medium": 6.9,
"high": 8.9,
"important": 8.9,
"critical": 10.0,
"urgent": 10.0,
}

score_list = []
for severity in severities:
weights = [
value
for regex_key, value in weight_config.items()
if re.match(regex_key, severity.reference.url)
]

if not weights:
return 0

max_weight = float(max(weights)) / 10
vul_score = severity.value
try:
vul_score = float(vul_score)
vul_score_value = vul_score * max_weight
except ValueError:
vul_score = vul_score.lower()
vul_score_value = score_map.get(vul_score, 0) * max_weight

score_list.append(vul_score_value)
return max(score_list) if score_list else 0


def get_exploitability_level(exploits, references, severities):
"""
Exploitability refers to the potential or
probability of a software package vulnerability being exploited by
malicious actors to compromise systems, applications, or networks.
It is determined automatically by discovery of exploits.
"""
# no exploit known ( default .5)
exploit_level = 0.5

if exploits:
# Automatable Exploit with PoC script published OR known exploits (KEV) in the wild OR known ransomware
exploit_level = 2

elif severities:
# high EPSS.
epss = severities.filter(
scoring_system=EPSS.identifier,
)
epss = any(float(epss.value) > 0.8 for epss in epss)
if epss:
exploit_level = 2

elif references:
# PoC/Exploit script published
ref_exploits = references.filter(
reference_type=VulnerabilityReference.EXPLOIT,
)
if ref_exploits:
exploit_level = 1

return exploit_level


def calculate_vulnerability_risk(vulnerability: Vulnerability):
"""
Risk may be expressed as a number ranging from 0 to 10.
Risk is calculated from weighted severity and exploitability values.
It is the maximum value of (the weighted severity multiplied by its exploitability) or 10

Risk = min(weighted severity * exploitability, 10)
"""
references = vulnerability.references
severities = vulnerability.severities.select_related("reference")
exploits = Exploit.objects.filter(vulnerability=vulnerability)
if references.exists() or severities.exists() or exploits.exists():
weighted_severity = get_weighted_severity(severities)
exploitability = get_exploitability_level(exploits, references, severities)
return min(weighted_severity * exploitability, 10)


def calculate_pkg_risk(package: Package):
"""
Calculate the risk for a package by iterating over all vulnerabilities that affects this package
and determining the associated risk.
"""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for pkg in Package.objects.all().paginated():
pkg.risk = calc(pkg)
pkg.save()


result = []
for pkg_related_vul in PackageRelatedVulnerability.objects.filter(
package=package, fix=False
).prefetch_related("vulnerability"):
if pkg_related_vul:
risk = calculate_vulnerability_risk(pkg_related_vul.vulnerability)
if not risk:
continue
result.append(risk)

if not result:
return

return f"{max(result):.2f}"
10 changes: 10 additions & 0 deletions vulnerabilities/templates/package_details.html
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,16 @@
{% endif %}
</td>
</tr>
<tr>
<td class="two-col-left">
Risk
</td>
<td class="two-col-right">
{% if risk %}
<a target="_self">{{ risk }}</a>
{% endif %}
</td>
</tr>
</tbody>
</table>
</div>
Expand Down
164 changes: 164 additions & 0 deletions vulnerabilities/tests/test_risk.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import pytest

from vulnerabilities.models import Exploit
from vulnerabilities.models import Vulnerability
from vulnerabilities.models import VulnerabilityReference
from vulnerabilities.models import VulnerabilityRelatedReference
from vulnerabilities.models import VulnerabilitySeverity
from vulnerabilities.models import Weakness
from vulnerabilities.risk import calculate_vulnerability_risk
from vulnerabilities.risk import get_exploitability_level
from vulnerabilities.risk import get_weighted_severity
from vulnerabilities.severity_systems import CVSSV3
from vulnerabilities.severity_systems import EPSS
from vulnerabilities.severity_systems import GENERIC


@pytest.fixture
@pytest.mark.django_db
def vulnerability():
vul = Vulnerability(vulnerability_id="VCID-Existing")
vul.save()

reference1 = VulnerabilityReference.objects.create(
reference_id="",
url="https://nvd.nist.gov/vuln/detail/CVE-xxxx-xxx1",
)

VulnerabilitySeverity.objects.create(
reference=reference1,
scoring_system=CVSSV3.identifier,
scoring_elements="CVSS:3.0/AV:P/AC:H/PR:H/UI:R/S:C/C:H/I:H/A:N/E:H/RL:O/RC:R/CR:H/MAC:H/MC:L",
value="6.5",
)

VulnerabilitySeverity.objects.create(
reference=reference1,
scoring_system=GENERIC.identifier,
value="MODERATE", # 6.9
)

VulnerabilityRelatedReference.objects.create(reference=reference1, vulnerability=vul)

weaknesses = Weakness.objects.create(cwe_id=119)
vul.weaknesses.add(weaknesses)
return vul


@pytest.fixture
@pytest.mark.django_db
def exploit():
vul = Vulnerability(vulnerability_id="VCID-Exploit")
vul.save()
return Exploit.objects.create(vulnerability=vul, description="exploit description")


@pytest.fixture
@pytest.mark.django_db
def vulnerability_with_exploit_ref():
vul = Vulnerability(vulnerability_id="VCID-Exploit-Ref")
vul.save()

reference_exploit = VulnerabilityReference.objects.create(
reference_id="",
reference_type=VulnerabilityReference.EXPLOIT,
url="https://nvd.nist.gov/vuln/detail/CVE-xxxx-xxxx2",
)

VulnerabilityRelatedReference.objects.create(reference=reference_exploit, vulnerability=vul)
return vul


@pytest.fixture
@pytest.mark.django_db
def high_epss_score():
vul = Vulnerability(vulnerability_id="VCID-HIGH-EPSS")
vul.save()

reference1 = VulnerabilityReference.objects.create(
reference_id="",
url="https://nvd.nist.gov/vuln/detail/CVE-xxxx-xxx3",
)

VulnerabilitySeverity.objects.create(
reference=reference1,
scoring_system=EPSS.identifier,
value=".9",
)

VulnerabilityRelatedReference.objects.create(reference=reference1, vulnerability=vul)
return vul.severities


@pytest.fixture
@pytest.mark.django_db
def low_epss_score():
vul = Vulnerability(vulnerability_id="VCID-LOW-EPSS")
vul.save()

reference1 = VulnerabilityReference.objects.create(
reference_id="",
url="https://nvd.nist.gov/vuln/detail/CVE-xxxx-xxx4",
)

VulnerabilitySeverity.objects.create(
reference=reference1,
scoring_system=EPSS.identifier,
value=".3",
)

VulnerabilityRelatedReference.objects.create(reference=reference1, vulnerability=vul)
return vul.severities


@pytest.mark.django_db
def test_exploitability_level(
exploit,
vulnerability_with_exploit_ref,
high_epss_score,
low_epss_score,
vulnerability,
):

assert get_exploitability_level(exploit, None, None) == 2

assert get_exploitability_level(None, None, high_epss_score) == 2

assert get_exploitability_level(None, None, low_epss_score) == 0.5

assert (
get_exploitability_level(
None,
vulnerability_with_exploit_ref.references,
vulnerability_with_exploit_ref.severities,
)
== 1
)

assert get_exploitability_level(None, None, None) == 0.5


@pytest.mark.django_db
def test_get_weighted_severity(vulnerability):
severities = vulnerability.severities.all()
assert get_weighted_severity(severities) == 6.210000000000001

reference2 = VulnerabilityReference.objects.create(
reference_id="",
url="https://security-tracker.debian.org/tracker/CVE-2019-13057",
)

VulnerabilitySeverity.objects.create(
reference=reference2,
scoring_system=GENERIC.identifier,
value="CRITICAL",
)

VulnerabilityRelatedReference.objects.create(reference=reference2, vulnerability=vulnerability)
new_severities = vulnerability.severities.all()
assert get_weighted_severity(new_severities) == 9


@pytest.mark.django_db
def test_calculate_vulnerability_risk(vulnerability):
assert calculate_vulnerability_risk(vulnerability) == 3.1050000000000004
3 changes: 2 additions & 1 deletion vulnerabilities/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from vulnerabilities.forms import PackageSearchForm
from vulnerabilities.forms import VulnerabilitySearchForm
from vulnerabilities.models import VulnerabilityStatusType
from vulnerabilities.risk import calculate_pkg_risk
from vulnerabilities.severity_systems import EPSS
from vulnerabilities.severity_systems import SCORING_SYSTEMS
from vulnerabilities.utils import get_severity_range
Expand Down Expand Up @@ -122,7 +123,7 @@ def get_context_data(self, **kwargs):
context["fixing_vulnerabilities"] = package.fixing.order_by("vulnerability_id")
context["package_search_form"] = PackageSearchForm(self.request.GET)
context["fixed_package_details"] = package.fixed_package_details

context["risk"] = calculate_pkg_risk(package)
context["history"] = list(package.history)
return context

Expand Down
5 changes: 5 additions & 0 deletions weight_config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"https://nvd\\.nist\\.gov/.*": 9,
"https:\\/\\/security-tracker\\.debian\\.org\\/.*": 9,
"^(?:http|ftp)s?://": 1
}
Loading