Skip to content

Commit 530cb52

Browse files
committed
Add exploitability and weighted_severity fields to the
Vulnerability model. Create a pipeline for vulnerability risk assessment. Signed-off-by: ziad hany <[email protected]>
1 parent 5cdca84 commit 530cb52

File tree

9 files changed

+183
-12
lines changed

9 files changed

+183
-12
lines changed

vulnerabilities/api.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,9 @@ class Meta:
251251
"weaknesses",
252252
"exploits",
253253
"severity_range_score",
254+
"exploitability",
255+
"weighted_severity",
256+
"risk_score",
254257
]
255258

256259

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Generated by Django 4.2.16 on 2024-11-08 14:07
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("vulnerabilities", "0076_alter_packagechangelog_software_version_and_more"),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name="vulnerability",
15+
name="exploitability",
16+
field=models.DecimalField(
17+
decimal_places=2,
18+
help_text="Exploitability refers to the potential or probability of a software package vulnerability being \n exploited by malicious actors to compromise systems, applications, or networks. \n It is determined automatically by the discovery of exploits.",
19+
max_digits=4,
20+
null=True,
21+
),
22+
),
23+
migrations.AddField(
24+
model_name="vulnerability",
25+
name="weighted_severity",
26+
field=models.DecimalField(
27+
decimal_places=2,
28+
help_text="Weighted Severity is the maximum value obtained when each Severity is multiplied by its associated Weight/10.",
29+
max_digits=4,
30+
null=True,
31+
),
32+
),
33+
]

vulnerabilities/models.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,35 @@ class Vulnerability(models.Model):
202202
choices=VulnerabilityStatusType.choices, default=VulnerabilityStatusType.PUBLISHED
203203
)
204204

205+
exploitability = models.DecimalField(
206+
null=True,
207+
max_digits=4,
208+
decimal_places=2,
209+
help_text="""Exploitability refers to the potential or probability of a software package vulnerability being
210+
exploited by malicious actors to compromise systems, applications, or networks.
211+
It is determined automatically by the discovery of exploits.""",
212+
)
213+
214+
weighted_severity = models.DecimalField(
215+
null=True,
216+
max_digits=4,
217+
decimal_places=2,
218+
help_text="Weighted Severity is the maximum value obtained when each Severity is multiplied by its associated Weight/10.",
219+
)
220+
221+
@property
222+
def risk_score(self):
223+
"""
224+
Risk expressed as a number ranging from 0 to 10.
225+
Risk is calculated from weighted severity and exploitability values.
226+
It is the maximum value of (the weighted severity multiplied by its exploitability) or 10
227+
228+
Risk = min(weighted severity * exploitability, 10)
229+
"""
230+
if self.exploitability is not None and self.weighted_severity is not None:
231+
return f"{min(float(self.exploitability) * float(self.weighted_severity), 10.0):.2f}"
232+
return None
233+
205234
objects = VulnerabilityQuerySet.as_manager()
206235

207236
class Meta:

vulnerabilities/pipelines/compute_package_risk.py

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@
99

1010
from aboutcode.pipeline import LoopProgress
1111

12+
from vulnerabilities.models import AffectedByPackageRelatedVulnerability
1213
from vulnerabilities.models import Package
14+
from vulnerabilities.models import Vulnerability
1315
from vulnerabilities.pipelines import VulnerableCodePipeline
1416
from vulnerabilities.risk import compute_package_risk
17+
from vulnerabilities.risk import compute_vulnerability_risk
1518

1619

1720
class ComputePackageRiskPipeline(VulnerableCodePipeline):
@@ -26,7 +29,44 @@ class ComputePackageRiskPipeline(VulnerableCodePipeline):
2629

2730
@classmethod
2831
def steps(cls):
29-
return (cls.add_package_risk_score,)
32+
return (cls.add_vulnerability_risk_score, cls.add_package_risk_score)
33+
34+
def add_vulnerability_risk_score(self):
35+
affected_vulnerabilities = Vulnerability.objects.filter(
36+
affectedbypackagerelatedvulnerability__isnull=False
37+
)
38+
39+
self.log(
40+
f"Calculating risk for {affected_vulnerabilities.count():,d} vulnerability with a affected packages records"
41+
)
42+
43+
progress = LoopProgress(total_iterations=affected_vulnerabilities.count(), logger=self.log)
44+
45+
updatables = []
46+
updated_vulnerability_count = 0
47+
batch_size = 5000
48+
49+
for vulnerability in progress.iter(affected_vulnerabilities.paginated()):
50+
51+
vulnerability = compute_vulnerability_risk(vulnerability)
52+
53+
if not vulnerability:
54+
continue
55+
56+
updatables.append(vulnerability)
57+
58+
if len(updatables) >= batch_size:
59+
updated_vulnerability_count += bulk_update_vulnerability_risk_score(
60+
vulnerabilities=updatables,
61+
logger=self.log,
62+
)
63+
updated_vulnerability_count += bulk_update_vulnerability_risk_score(
64+
vulnerabilities=updatables,
65+
logger=self.log,
66+
)
67+
self.log(
68+
f"Successfully added risk score for {updated_vulnerability_count:,d} vulnerability"
69+
)
3070

3171
def add_package_risk_score(self):
3272
affected_packages = Package.objects.filter(
@@ -72,3 +112,17 @@ def bulk_update_package_risk_score(packages, logger):
72112
logger(f"Error updating packages: {e}")
73113
packages.clear()
74114
return package_count
115+
116+
117+
def bulk_update_vulnerability_risk_score(vulnerabilities, logger):
118+
vulnerabilities_count = 0
119+
if vulnerabilities:
120+
try:
121+
Vulnerability.objects.bulk_update(
122+
objs=vulnerabilities, fields=["weighted_severity", "exploitability"]
123+
)
124+
vulnerabilities_count += len(vulnerabilities)
125+
except Exception as e:
126+
logger(f"Error updating vulnerability: {e}")
127+
vulnerabilities.clear()
128+
return vulnerabilities_count

vulnerabilities/risk.py

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -92,19 +92,31 @@ def get_exploitability_level(exploits, references, severities):
9292

9393
def compute_vulnerability_risk(vulnerability: Vulnerability):
9494
"""
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
95+
Computes the risk score for a given vulnerability.
9896
99-
Risk = min(weighted severity * exploitability, 10)
97+
Risk is expressed as a number ranging from 0 to 10 and is calculated based on:
98+
- Weighted severity: a value derived from the associated severities of the vulnerability.
99+
- Exploitability: a measure of how easily the vulnerability can be exploited.
100+
101+
The risk score is computed as:
102+
Risk = min(weighted_severity * exploitability, 10)
103+
104+
Args:
105+
vulnerability (Vulnerability): The vulnerability object to compute the risk for.
106+
107+
Returns:
108+
Vulnerability: The updated vulnerability object with computed risk-related attributes.
109+
110+
Notes:
111+
- If there are no associated references, severities, or exploits, the computation is skipped.
100112
"""
101113
references = vulnerability.references
102114
severities = vulnerability.severities.select_related("reference")
103115
exploits = Exploit.objects.filter(vulnerability=vulnerability)
104116
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)
117+
vulnerability.weighted_severity = get_weighted_severity(severities)
118+
vulnerability.exploitability = get_exploitability_level(exploits, references, severities)
119+
return vulnerability
108120

109121

110122
def compute_package_risk(package: Package):
@@ -117,8 +129,8 @@ def compute_package_risk(package: Package):
117129
for pkg_related_vul in AffectedByPackageRelatedVulnerability.objects.filter(
118130
package=package
119131
).prefetch_related("vulnerability"):
120-
if risk := compute_vulnerability_risk(pkg_related_vul.vulnerability):
121-
result.append(risk)
132+
if risk := pkg_related_vul.vulnerability.risk_score:
133+
result.append(float(risk))
122134

123135
if not result:
124136
return

vulnerabilities/templates/vulnerability_details.html

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,38 @@
121121
<td class="two-col-left">Status</td>
122122
<td class="two-col-right">{{ status }}</td>
123123
</tr>
124+
125+
<tr>
126+
<td class="two-col-left"
127+
data-tooltip="Exploitability refers to the potential or probability of a software package vulnerability being
128+
exploited by malicious actors to compromise systems, applications, or networks.
129+
It is determined automatically by the discovery of exploits.">
130+
Exploitability</td>
131+
<td class="two-col-right wrap-strings">
132+
{{ vulnerability.exploitability }}
133+
</td>
134+
</tr>
135+
136+
<tr>
137+
<td class="two-col-left"
138+
data-tooltip="Weighted Severity is the maximum value obtained when each Severity is multiplied by its associated Weight/10."
139+
>Weighted Severity</td>
140+
<td class="two-col-right wrap-strings">
141+
{{ vulnerability.weighted_severity }}
142+
</td>
143+
</tr>
144+
145+
<tr>
146+
<td class="two-col-left"
147+
data-tooltip="Risk expressed as a number ranging from 0 to 10. It is calculated by multiplying
148+
the weighted severity and exploitability values, capped at a maximum of 10.
149+
"
150+
>Risk</td>
151+
<td class="two-col-right wrap-strings">
152+
{{ vulnerability.risk_score }}
153+
</td>
154+
</tr>
155+
124156
</tbody>
125157
</table>
126158
</div>

vulnerabilities/tests/pipelines/test_compute_package_risk.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
# See https://github.com/aboutcode-org/vulnerablecode for support or download.
77
# See https://aboutcode.org for more information about nexB OSS projects.
88
#
9+
from decimal import Decimal
910

1011
import pytest
1112

@@ -30,4 +31,4 @@ def test_simple_risk_pipeline(vulnerability):
3031
improver.execute()
3132

3233
pkg = Package.objects.get(type="pypi", name="foo", version="2.3.0")
33-
assert str(pkg.risk_score) == str(3.11)
34+
assert f"{pkg.risk_score:.2f}" == "3.10"

vulnerabilities/tests/test_api.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,9 @@ def test_api_with_single_vulnerability(self):
300300
},
301301
],
302302
"exploits": [],
303+
"risk_score": None,
304+
"exploitability": None,
305+
"weighted_severity": None,
303306
}
304307

305308
def test_api_with_single_vulnerability_with_filters(self):
@@ -346,6 +349,9 @@ def test_api_with_single_vulnerability_with_filters(self):
346349
},
347350
],
348351
"exploits": [],
352+
"risk_score": None,
353+
"exploitability": None,
354+
"weighted_severity": None,
349355
}
350356

351357

vulnerabilities/tests/test_risk.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,4 +170,5 @@ def test_get_weighted_severity(vulnerability):
170170

171171
@pytest.mark.django_db
172172
def test_compute_vulnerability_risk(vulnerability):
173-
assert compute_vulnerability_risk(vulnerability) == 3.1050000000000004
173+
vulnerability = compute_vulnerability_risk(vulnerability)
174+
assert vulnerability.risk_score == str(3.11)

0 commit comments

Comments
 (0)