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 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions vulnerabilities/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,7 @@ class Meta:
"latest_non_vulnerable_version",
"affected_by_vulnerabilities",
"fixing_vulnerabilities",
"risk",
]


Expand Down
2 changes: 2 additions & 0 deletions vulnerabilities/improvers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from vulnerabilities.pipelines import enhance_with_kev
from vulnerabilities.pipelines import enhance_with_metasploit
from vulnerabilities.pipelines import flag_ghost_packages
from vulnerabilities.pipelines import risk_package

IMPROVERS_REGISTRY = [
valid_versions.GitHubBasicImprover,
Expand All @@ -37,6 +38,7 @@
enhance_with_kev.VulnerabilityKevPipeline,
enhance_with_metasploit.MetasploitImproverPipeline,
enhance_with_exploitdb.ExploitDBImproverPipeline,
risk_package.RiskPackagePipeline,
]

IMPROVERS_REGISTRY = {
Expand Down
23 changes: 23 additions & 0 deletions vulnerabilities/migrations/0074_package_risk.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 4.2.16 on 2024-10-22 06:49

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("vulnerabilities", "0073_delete_packagerelatedvulnerability"),
]

operations = [
migrations.AddField(
model_name="package",
name="risk",
field=models.DecimalField(
decimal_places=2,
help_text="Enter a risk score between 0.00 and 10.00, where higher values indicate greater vulnerability risk for the package.",
ziadhany marked this conversation as resolved.
Show resolved Hide resolved
max_digits=4,
null=True,
),
),
]
8 changes: 8 additions & 0 deletions vulnerabilities/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,14 @@ class Package(PackageURLMixin):
help_text="True if the package does not exist in the upstream package manager or its repository.",
)

risk = models.DecimalField(
ziadhany marked this conversation as resolved.
Show resolved Hide resolved
null=True,
max_digits=4,
decimal_places=2,
help_text="Enter a risk score between 0.00 and 10.00, where higher values "
"indicate greater vulnerability risk for the package.",
ziadhany marked this conversation as resolved.
Show resolved Hide resolved
)

objects = PackageQuerySet.as_manager()

def save(self, *args, **kwargs):
Expand Down
30 changes: 30 additions & 0 deletions vulnerabilities/pipelines/risk_package.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from vulnerabilities.models import Package
from vulnerabilities.pipelines import VulnerableCodePipeline
from vulnerabilities.risk import calculate_pkg_risk
ziadhany marked this conversation as resolved.
Show resolved Hide resolved


class RiskPackagePipeline(VulnerableCodePipeline):
ziadhany marked this conversation as resolved.
Show resolved Hide resolved
"""
Risk Assessment Pipeline for Package Vulnerabilities: Iterate through the packages and evaluate their associated risk.
"""

pipeline_id = "risk_package"
ziadhany marked this conversation as resolved.
Show resolved Hide resolved
license_expression = None

@classmethod
def steps(cls):
return (cls.add_risk_package,)

def add_risk_package(self):
keshav-space marked this conversation as resolved.
Show resolved Hide resolved
self.log(f"Add risk package pipeline ")
keshav-space marked this conversation as resolved.
Show resolved Hide resolved

updatables = []
for pkg in Package.objects.filter(affected_by_vulnerabilities__isnull=False):
ziadhany marked this conversation as resolved.
Show resolved Hide resolved
risk = calculate_pkg_risk(pkg)
pkg.risk = risk
updatables.append(pkg)
keshav-space marked this conversation as resolved.
Show resolved Hide resolved

# Bulk update the 'risk' field for all packages
Package.objects.bulk_update(objs=updatables, fields=["risk"], batch_size=1000)

self.log(f"Successfully added risk package pipeline ")
ziadhany marked this conversation as resolved.
Show resolved Hide resolved
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
ziadhany marked this conversation as resolved.
Show resolved Hide resolved

from vulnerabilities.models import AffectedByPackageRelatedVulnerability
from vulnerabilities.models import Exploit
from vulnerabilities.models import Package
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__))
WEIGHT_CONFIG_PATH = os.path.join(BASE_DIR, "../weight_config.json")
ziadhany marked this conversation as resolved.
Show resolved Hide resolved
DEFAULT_WEIGHT = 1
WEIGHT_CONFIG = load_json(WEIGHT_CONFIG_PATH)


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
"""

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 = []
for key, value in WEIGHT_CONFIG.items():
if severity.reference.url.startswith(key):
weights.append(value)
continue
weights.append(DEFAULT_WEIGHT)

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.
"""
ziadhany marked this conversation as resolved.
Show resolved Hide resolved

result = []
for pkg_related_vul in AffectedByPackageRelatedVulnerability.objects.filter(
package=package
).prefetch_related("vulnerability"):
risk = calculate_vulnerability_risk(pkg_related_vul.vulnerability)
if not risk:
continue
result.append(risk)
ziadhany marked this conversation as resolved.
Show resolved Hide resolved

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 package.risk %}
<a target="_self">{{ package.risk }}</a>
{% endif %}
</td>
</tr>
</tbody>
</table>
</div>
Expand Down
24 changes: 24 additions & 0 deletions vulnerabilities/tests/pipelines/test_risk_pipeline.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import pytest

from vulnerabilities.models import AffectedByPackageRelatedVulnerability
from vulnerabilities.models import Package
from vulnerabilities.pipelines.risk_package import RiskPackagePipeline
from vulnerabilities.tests.test_risk import vulnerability


@pytest.mark.django_db
def test_simple_risk_pipeline(vulnerability):
pkg = Package.objects.create(type="pypi", name="foo", version="2.3.0")
assert Package.objects.count() == 1

improver = RiskPackagePipeline()
improver.execute()

assert pkg.risk is None

AffectedByPackageRelatedVulnerability.objects.create(package=pkg, vulnerability=vulnerability)
improver = RiskPackagePipeline()
improver.execute()

pkg = Package.objects.get(type="pypi", name="foo", version="2.3.0")
assert str(pkg.risk) == str(3.11)
1 change: 1 addition & 0 deletions vulnerabilities/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,7 @@ def test_api_with_lesser_and_greater_fixed_by_packages(self):
}
],
"resource_url": "http://testserver/packages/pkg:maven/com.fasterxml.jackson.core/[email protected]",
"risk": None,
}

assert response == expected
Expand Down
Loading
Loading