Skip to content

Commit 59ab556

Browse files
authored
Merge branch 'main' into use_bulk_create_in_migrations
2 parents 109c2db + 0e13b55 commit 59ab556

18 files changed

+3429
-0
lines changed

vulnerabilities/api.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,7 @@ class Meta:
359359
"latest_non_vulnerable_version",
360360
"affected_by_vulnerabilities",
361361
"fixing_vulnerabilities",
362+
"risk_score",
362363
]
363364

364365

vulnerabilities/improvers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from vulnerabilities.improvers import valid_versions
1111
from vulnerabilities.improvers import vulnerability_status
1212
from vulnerabilities.pipelines import VulnerableCodePipeline
13+
from vulnerabilities.pipelines import compute_package_risk
1314
from vulnerabilities.pipelines import enhance_with_exploitdb
1415
from vulnerabilities.pipelines import enhance_with_kev
1516
from vulnerabilities.pipelines import enhance_with_metasploit
@@ -37,6 +38,7 @@
3738
enhance_with_kev.VulnerabilityKevPipeline,
3839
enhance_with_metasploit.MetasploitImproverPipeline,
3940
enhance_with_exploitdb.ExploitDBImproverPipeline,
41+
compute_package_risk.ComputePackageRiskPipeline,
4042
]
4143

4244
IMPROVERS_REGISTRY = {
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Generated by Django 4.2.16 on 2024-10-29 10:55
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("vulnerabilities", "0074_update_pysec_advisory_created_by"),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name="package",
15+
name="risk_score",
16+
field=models.DecimalField(
17+
decimal_places=2,
18+
help_text="Risk score between 0.00 and 10.00, where higher values indicate greater vulnerability risk for the package.",
19+
max_digits=4,
20+
null=True,
21+
),
22+
),
23+
]

vulnerabilities/models.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -636,6 +636,14 @@ class Package(PackageURLMixin):
636636
help_text="True if the package does not exist in the upstream package manager or its repository.",
637637
)
638638

639+
risk_score = models.DecimalField(
640+
null=True,
641+
max_digits=4,
642+
decimal_places=2,
643+
help_text="Risk score between 0.00 and 10.00, where higher values "
644+
"indicate greater vulnerability risk for the package.",
645+
)
646+
639647
objects = PackageQuerySet.as_manager()
640648

641649
def save(self, *args, **kwargs):
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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+
from aboutcode.pipeline import LoopProgress
11+
12+
from vulnerabilities.models import Package
13+
from vulnerabilities.pipelines import VulnerableCodePipeline
14+
from vulnerabilities.risk import compute_package_risk
15+
16+
17+
class ComputePackageRiskPipeline(VulnerableCodePipeline):
18+
"""
19+
Compute risk score for packages.
20+
21+
See https://github.com/aboutcode-org/vulnerablecode/issues/1543
22+
"""
23+
24+
pipeline_id = "compute_package_risk"
25+
license_expression = None
26+
27+
@classmethod
28+
def steps(cls):
29+
return (cls.add_package_risk_score,)
30+
31+
def add_package_risk_score(self):
32+
affected_packages = Package.objects.filter(
33+
affected_by_vulnerabilities__isnull=False
34+
).distinct()
35+
36+
self.log(f"Calculating risk for {affected_packages.count():,d} affected package records")
37+
38+
progress = LoopProgress(total_iterations=affected_packages.count(), logger=self.log)
39+
40+
updatables = []
41+
updated_package_count = 0
42+
batch_size = 5000
43+
44+
for package in progress.iter(affected_packages.paginated()):
45+
risk_score = compute_package_risk(package)
46+
47+
if not risk_score:
48+
continue
49+
50+
package.risk_score = risk_score
51+
updatables.append(package)
52+
53+
if len(updatables) >= batch_size:
54+
updated_package_count += bulk_update_package_risk_score(
55+
packages=updatables,
56+
logger=self.log,
57+
)
58+
updated_package_count += bulk_update_package_risk_score(
59+
packages=updatables,
60+
logger=self.log,
61+
)
62+
self.log(f"Successfully added risk score for {updated_package_count:,d} package")
63+
64+
65+
def bulk_update_package_risk_score(packages, logger):
66+
package_count = 0
67+
if packages:
68+
try:
69+
Package.objects.bulk_update(objs=packages, fields=["risk_score"])
70+
package_count += len(packages)
71+
except Exception as e:
72+
logger(f"Error updating packages: {e}")
73+
packages.clear()
74+
return package_count

vulnerabilities/pipelines/enhance_with_exploitdb.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
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+
110
import csv
211
import io
312
import logging

vulnerabilities/pipelines/enhance_with_kev.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
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+
110
import logging
211
from traceback import format_exc as traceback_format_exc
312

vulnerabilities/pipelines/enhance_with_metasploit.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
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+
110
import logging
211
from traceback import format_exc as traceback_format_exc
312

vulnerabilities/risk.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
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}"

vulnerabilities/templates/package_details.html

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,20 @@
112112
{% endif %}
113113
</td>
114114
</tr>
115+
<tr>
116+
<td class="two-col-left">
117+
<span
118+
class="has-tooltip-multiline has-tooltip-black has-tooltip-arrow has-tooltip-text-left"
119+
data-tooltip="Risk is expressed as a number ranging from 0 to 10. It is calculated based on weighted severity and exploitability values. The risk score is the maximum value of either the weighted severity multiplied by its exploitability or 10.">
120+
Risk
121+
</span>
122+
</td>
123+
<td class="two-col-right">
124+
{% if package.risk_score %}
125+
<a target="_self">{{ package.risk_score }}</a>
126+
{% endif %}
127+
</td>
128+
</tr>
115129
</tbody>
116130
</table>
117131
</div>

0 commit comments

Comments
 (0)