Skip to content

Commit 1561efe

Browse files
authored
Merge pull request #1481 from ziadhany/epss
Add Support to EPSS
2 parents 84a35db + 0fe73ef commit 1561efe

File tree

11 files changed

+325
-62
lines changed

11 files changed

+325
-62
lines changed

vulnerabilities/api.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,14 @@
3838
class VulnerabilitySeveritySerializer(serializers.ModelSerializer):
3939
class Meta:
4040
model = VulnerabilitySeverity
41-
fields = ["value", "scoring_system", "scoring_elements"]
41+
fields = ["value", "scoring_system", "scoring_elements", "published_at"]
42+
43+
def to_representation(self, instance):
44+
data = super().to_representation(instance)
45+
published_at = data.get("published_at", None)
46+
if not published_at:
47+
data.pop("published_at")
48+
return data
4249

4350

4451
class VulnerabilityReferenceSerializer(serializers.ModelSerializer):

vulnerabilities/import_runner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ def process_inferences(inferences: List[Inference], advisory: Advisory, improver
189189
defaults={
190190
"value": str(severity.value),
191191
"scoring_elements": str(severity.scoring_elements),
192+
"published_at": str(severity.published_at),
192193
},
193194
)
194195
if updated:

vulnerabilities/importer.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,17 @@ class VulnerabilitySeverity:
5252
system: ScoringSystem
5353
value: str
5454
scoring_elements: str = ""
55+
published_at: Optional[datetime.datetime] = None
5556

5657
def to_dict(self):
58+
published_at_dict = (
59+
{"published_at": self.published_at.isoformat()} if self.published_at else {}
60+
)
5761
return {
5862
"system": self.system.identifier,
5963
"value": self.value,
6064
"scoring_elements": self.scoring_elements,
65+
**published_at_dict,
6166
}
6267

6368
@classmethod
@@ -70,6 +75,7 @@ def from_dict(cls, severity: dict):
7075
system=SCORING_SYSTEMS[severity["system"]],
7176
value=severity["value"],
7277
scoring_elements=severity.get("scoring_elements", ""),
78+
published_at=severity.get("published_at"),
7379
)
7480

7581

vulnerabilities/importers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from vulnerabilities.importers import debian
1616
from vulnerabilities.importers import debian_oval
1717
from vulnerabilities.importers import elixir_security
18+
from vulnerabilities.importers import epss
1819
from vulnerabilities.importers import fireeye
1920
from vulnerabilities.importers import gentoo
2021
from vulnerabilities.importers import github
@@ -71,6 +72,7 @@
7172
oss_fuzz.OSSFuzzImporter,
7273
ruby.RubyImporter,
7374
github_osv.GithubOSVImporter,
75+
epss.EPSSImporter,
7476
]
7577

7678
IMPORTERS_REGISTRY = {x.qualified_name: x for x in IMPORTERS_REGISTRY}

vulnerabilities/importers/epss.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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/nexB/vulnerablecode for support or download.
7+
# See https://aboutcode.org for more information about nexB OSS projects.
8+
#
9+
import csv
10+
import gzip
11+
import logging
12+
import urllib.request
13+
from datetime import datetime
14+
from typing import Iterable
15+
16+
from vulnerabilities import severity_systems
17+
from vulnerabilities.importer import AdvisoryData
18+
from vulnerabilities.importer import Importer
19+
from vulnerabilities.importer import Reference
20+
from vulnerabilities.importer import VulnerabilitySeverity
21+
22+
logger = logging.getLogger(__name__)
23+
24+
25+
class EPSSImporter(Importer):
26+
"""Exploit Prediction Scoring System (EPSS) Importer"""
27+
28+
advisory_url = "https://epss.cyentia.com/epss_scores-current.csv.gz"
29+
spdx_license_expression = "unknown"
30+
importer_name = "EPSS Importer"
31+
32+
def advisory_data(self) -> Iterable[AdvisoryData]:
33+
response = urllib.request.urlopen(self.advisory_url)
34+
with gzip.open(response, "rb") as f:
35+
lines = [l.decode("utf-8") for l in f.readlines()]
36+
37+
epss_reader = csv.reader(lines)
38+
model_version, score_date = next(
39+
epss_reader
40+
) # score_date='score_date:2024-05-19T00:00:00+0000'
41+
published_at = datetime.strptime(score_date[11::], "%Y-%m-%dT%H:%M:%S%z")
42+
43+
next(epss_reader) # skip the header row
44+
for epss_row in epss_reader:
45+
cve, score, percentile = epss_row
46+
47+
if not cve or not score or not percentile:
48+
logger.error(f"Invalid epss row: {epss_row}")
49+
continue
50+
51+
severity = VulnerabilitySeverity(
52+
system=severity_systems.EPSS,
53+
value=score,
54+
scoring_elements=percentile,
55+
published_at=published_at,
56+
)
57+
58+
references = Reference(
59+
url=f"https://api.first.org/data/v1/epss?cve={cve}",
60+
severities=[severity],
61+
)
62+
63+
yield AdvisoryData(
64+
aliases=[cve],
65+
references=[references],
66+
url=self.advisory_url,
67+
)
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Generated by Django 4.1.13 on 2024-08-06 09:38
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("vulnerabilities", "0058_alter_vulnerabilityreference_options_and_more"),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name="vulnerabilityseverity",
15+
name="published_at",
16+
field=models.DateTimeField(
17+
blank=True,
18+
help_text="UTC Date of publication of the vulnerability severity",
19+
null=True,
20+
),
21+
),
22+
migrations.AlterField(
23+
model_name="vulnerabilityseverity",
24+
name="scoring_system",
25+
field=models.CharField(
26+
choices=[
27+
("cvssv2", "CVSSv2 Base Score"),
28+
("cvssv3", "CVSSv3 Base Score"),
29+
("cvssv3.1", "CVSSv3.1 Base Score"),
30+
("rhbs", "RedHat Bugzilla severity"),
31+
("rhas", "RedHat Aggregate severity"),
32+
("archlinux", "Archlinux Vulnerability Group Severity"),
33+
("cvssv3.1_qr", "CVSSv3.1 Qualitative Severity Rating"),
34+
("generic_textual", "Generic textual severity rating"),
35+
("apache_httpd", "Apache Httpd Severity"),
36+
("apache_tomcat", "Apache Tomcat Severity"),
37+
("epss", "Exploit Prediction Scoring System"),
38+
],
39+
help_text="Identifier for the scoring system used. Available choices are: cvssv2: CVSSv2 Base Score,\ncvssv3: CVSSv3 Base Score,\ncvssv3.1: CVSSv3.1 Base Score,\nrhbs: RedHat Bugzilla severity,\nrhas: RedHat Aggregate severity,\narchlinux: Archlinux Vulnerability Group Severity,\ncvssv3.1_qr: CVSSv3.1 Qualitative Severity Rating,\ngeneric_textual: Generic textual severity rating,\napache_httpd: Apache Httpd Severity,\napache_tomcat: Apache Tomcat Severity,\nepss: Exploit Prediction Scoring System ",
40+
max_length=50,
41+
),
42+
),
43+
]

vulnerabilities/models.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -936,6 +936,10 @@ class VulnerabilitySeverity(models.Model):
936936
"For example a CVSS vector string as used to compute a CVSS score.",
937937
)
938938

939+
published_at = models.DateTimeField(
940+
blank=True, null=True, help_text="UTC Date of publication of the vulnerability severity"
941+
)
942+
939943
class Meta:
940944
unique_together = ["reference", "scoring_system", "value"]
941945
ordering = ["reference", "scoring_system", "value"]

vulnerabilities/severity_systems.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,19 @@ def get(self, scoring_elements: str) -> dict:
157157
"Low",
158158
]
159159

160+
161+
@dataclasses.dataclass(order=True)
162+
class EPSSScoringSystem(ScoringSystem):
163+
def compute(self, scoring_elements: str):
164+
return NotImplementedError
165+
166+
167+
EPSS = EPSSScoringSystem(
168+
identifier="epss",
169+
name="Exploit Prediction Scoring System",
170+
url="https://www.first.org/epss/",
171+
)
172+
160173
SCORING_SYSTEMS = {
161174
system.identifier: system
162175
for system in (
@@ -170,5 +183,6 @@ def get(self, scoring_elements: str) -> dict:
170183
GENERIC,
171184
APACHE_HTTPD,
172185
APACHE_TOMCAT,
186+
EPSS,
173187
)
174188
}

0 commit comments

Comments
 (0)