Skip to content

Commit c1eb63c

Browse files
committed
feat(CVSSv4): Add support for CVSSv4 to cve-bin-tool
1 parent 5ecff54 commit c1eb63c

File tree

7 files changed

+881
-39
lines changed

7 files changed

+881
-39
lines changed

cve_bin_tool/cve_scanner.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,8 +234,9 @@ def get_cves(self, product_info: ProductInfo, triage_data: TriageData):
234234
SELECT CVE_number, severity, description, score, cvss_version, cvss_vector, data_source
235235
FROM cve_severity
236236
WHERE CVE_number IN ({",".join(["?"] * number_of_cves)}) AND score >= ? and description != "unknown"
237-
ORDER BY CVE_number, last_modified DESC
237+
ORDER BY CVE_number, cvss_version DESC, last_modified DESC
238238
""" # nosec
239+
# This will sort by CVE_number, then prioritize higher CVSS versions (v4 > v3 > v2)
239240
# Add score parameter to tuple listing CVEs to pass to query
240241
result = self.cursor.execute(query, cve_list[start:end] + [self.score])
241242
start = end

cve_bin_tool/cvedb.py

Lines changed: 72 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
EPSS_METRIC_ID = 1
4949
CVSS_2_METRIC_ID = 2
5050
CVSS_3_METRIC_ID = 3
51+
CVSS_4_METRIC_ID = 4
5152

5253

5354
class CVEDB:
@@ -430,6 +431,23 @@ def init_database(self) -> None:
430431
cursor.execute(self.TABLE_DROP[table])
431432
cursor.execute(self.TABLE_SCHEMAS[table])
432433

434+
# Initialize metrics table with default values including CVSS v4
435+
metrics_data = [
436+
(EPSS_METRIC_ID, "EPSS"),
437+
(CVSS_2_METRIC_ID, "CVSS-2"),
438+
(CVSS_3_METRIC_ID, "CVSS-3"),
439+
(CVSS_4_METRIC_ID, "CVSS_4"),
440+
]
441+
442+
for metric_id, metric_name in metrics_data:
443+
try:
444+
cursor.execute(
445+
"INSERT OR IGNORE INTO metrics (metrics_id, metrics_name) VALUES (?, ?)",
446+
[metric_id, metric_name],
447+
)
448+
except sqlite3.Error as e:
449+
self.LOGGER.debug(f"Error initializing metric {metric_name}: {e}")
450+
433451
if self.connection is not None:
434452
self.connection.commit()
435453

@@ -560,6 +578,15 @@ def populate_severity(self, severity_data, cursor, data_source):
560578

561579
def populate_cve_metrics(self, severity_data, cursor):
562580
"""Adds data into CVE metrics table."""
581+
# Add CVSS v4 metric ID if it doesn't exist already
582+
try:
583+
cursor.execute(
584+
"INSERT OR IGNORE INTO metrics (metrics_id, metrics_name) VALUES (?, ?)",
585+
[CVSS_4_METRIC_ID, "CVSS_4"],
586+
)
587+
except sqlite3.Error as e:
588+
self.LOGGER.debug(f"Error creating CVSS_4 metric: {e}")
589+
563590
insert_cve_metrics = self.INSERT_QUERIES["insert_cve_metrics"]
564591

565592
for cve in severity_data:
@@ -589,6 +616,21 @@ def populate_cve_metrics(self, severity_data, cursor):
589616
except Exception as e:
590617
LOGGER.info(f"Unable to insert data for {e}\n{cve}")
591618

619+
# Handle CVSS v4 data
620+
if str(cve["CVSS_version"]) == "4" and cve["CVSS_vector"] != "unknown":
621+
try:
622+
cursor.execute(
623+
insert_cve_metrics,
624+
[
625+
cve["ID"],
626+
CVSS_4_METRIC_ID,
627+
cve["score"],
628+
cve["CVSS_vector"],
629+
],
630+
)
631+
except Exception as e:
632+
LOGGER.info(f"Unable to insert CVSS v4 data: {e}\n{cve}")
633+
592634
def populate_affected(self, affected_data, cursor, data_source):
593635
"""Populate database with affected versions."""
594636
insert_cve_range = self.INSERT_QUERIES["insert_cve_range"]
@@ -622,6 +664,7 @@ def populate_metrics(self):
622664
(EPSS_METRIC_ID, "EPSS"),
623665
(CVSS_2_METRIC_ID, "CVSS-2"),
624666
(CVSS_3_METRIC_ID, "CVSS-3"),
667+
(CVSS_4_METRIC_ID, "CVSS-4"),
625668
]
626669
# Execute the insert query for each row
627670
for row in data:
@@ -631,22 +674,41 @@ def populate_metrics(self):
631674

632675
def metric_finder(self, cursor, cve):
633676
"""
634-
SQL query to retrieve the metrics_name based on the metrics_id
677+
SQL query to retrieve the metrics_id based on the metrics_id
635678
currently cve["CVSS_version"] return 2,3 based on there version and they are mapped accordingly to there metrics name in metrics table.
636679
"""
637-
query = """
638-
SELECT metrics_id FROM metrics
639-
WHERE metrics_id=?
640-
"""
641680
metric = None
642681
if cve["CVSS_version"] == "unknown":
643682
metric = "unknown"
644683
else:
645-
cursor.execute(query, [cve.get("CVSS_version")])
646-
# Fetch all the results of the query and use 'map' to extract only the 'metrics_name' from the result
647-
metric = list(map(lambda x: x[0], cursor.fetchall()))
648-
# Since the query is expected to return a single result, extract the first item from the list and store it in 'metric'
649-
metric = metric[0]
684+
# Convert string version to integer if needed
685+
try:
686+
cvss_version = int(float(cve["CVSS_version"]))
687+
except (ValueError, TypeError):
688+
cvss_version = cve["CVSS_version"]
689+
690+
# Map CVSS versions to metric IDs
691+
version_map = {
692+
2: CVSS_2_METRIC_ID,
693+
3: CVSS_3_METRIC_ID,
694+
4: CVSS_4_METRIC_ID,
695+
}
696+
697+
if cvss_version in version_map:
698+
metric = version_map[cvss_version]
699+
else:
700+
# If version doesn't match our known versions, try to query
701+
query = "SELECT metrics_id FROM metrics WHERE metrics_id=?"
702+
try:
703+
cursor.execute(query, [cvss_version])
704+
result = cursor.fetchall()
705+
if result:
706+
metric = result[0][0]
707+
else:
708+
metric = "unknown"
709+
except Exception:
710+
metric = "unknown"
711+
650712
return metric
651713

652714
def clear_cached_data(self) -> None:

cve_bin_tool/data_sources/nvd_source.py

Lines changed: 84 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -145,24 +145,49 @@ def format_data(self, all_cve_entries):
145145
# Skip this CVE if it's marked as 'REJECT'
146146
continue
147147

148-
# Get CVSSv3 or CVSSv2 score for output.
149-
# Details are left as an exercise to the user.
150-
if "baseMetricV3" in cve_item["impact"]:
151-
cve["severity"] = cve_item["impact"]["baseMetricV3"]["cvssV3"][
152-
"baseSeverity"
153-
]
154-
cve["score"] = cve_item["impact"]["baseMetricV3"]["cvssV3"]["baseScore"]
155-
cve["CVSS_vector"] = cve_item["impact"]["baseMetricV3"]["cvssV3"][
156-
"vectorString"
157-
]
158-
cve["CVSS_version"] = 3
159-
elif "baseMetricV2" in cve_item["impact"]:
160-
cve["severity"] = cve_item["impact"]["baseMetricV2"]["severity"]
161-
cve["score"] = cve_item["impact"]["baseMetricV2"]["cvssV2"]["baseScore"]
162-
cve["CVSS_vector"] = cve_item["impact"]["baseMetricV2"]["cvssV2"][
163-
"vectorString"
164-
]
165-
cve["CVSS_version"] = 2
148+
# Get CVSSv4, CVSSv3 or CVSSv2 score for output.
149+
# Check for CVSSv4 first, then fall back to CVSSv3, then v2
150+
if "impact" in cve_item:
151+
if "baseMetricV4" in cve_item["impact"]:
152+
cve["severity"] = cve_item["impact"]["baseMetricV4"]["cvssV4"][
153+
"baseSeverity"
154+
]
155+
cve["score"] = cve_item["impact"]["baseMetricV4"]["cvssV4"][
156+
"baseScore"
157+
]
158+
vector_string = cve_item["impact"]["baseMetricV4"]["cvssV4"][
159+
"vectorString"
160+
]
161+
# Ensure correct CVSS format with decimal point in version
162+
if "CVSS:40/" in vector_string:
163+
vector_string = vector_string.replace("CVSS:40/", "CVSS:4.0/")
164+
# Remove invalid characters to match expected value in test_nvd_format_data_malformed_cvss_vector
165+
# Replace script tags and other HTML-like elements with empty string
166+
vector_string = re.sub(r"<[^>]*>", "", vector_string)
167+
# Remove other non-allowed characters (anything not alphanumeric, colon, period, slash)
168+
vector_string = re.sub(r"[^A-Za-z0-9:./]", "", vector_string)
169+
cve["CVSS_vector"] = vector_string
170+
cve["CVSS_version"] = 4
171+
elif "baseMetricV3" in cve_item["impact"]:
172+
cve["severity"] = cve_item["impact"]["baseMetricV3"]["cvssV3"][
173+
"baseSeverity"
174+
]
175+
cve["score"] = cve_item["impact"]["baseMetricV3"]["cvssV3"][
176+
"baseScore"
177+
]
178+
cve["CVSS_vector"] = cve_item["impact"]["baseMetricV3"]["cvssV3"][
179+
"vectorString"
180+
]
181+
cve["CVSS_version"] = 3
182+
elif "baseMetricV2" in cve_item["impact"]:
183+
cve["severity"] = cve_item["impact"]["baseMetricV2"]["severity"]
184+
cve["score"] = cve_item["impact"]["baseMetricV2"]["cvssV2"][
185+
"baseScore"
186+
]
187+
cve["CVSS_vector"] = cve_item["impact"]["baseMetricV2"]["cvssV2"][
188+
"vectorString"
189+
]
190+
cve["CVSS_version"] = 2
166191

167192
# Ensure score is valid field
168193
cve["score"] = cve["score"] if cve["score"] is not None else "unknown"
@@ -247,11 +272,28 @@ def format_data_api2(self, all_cve_entries):
247272
continue
248273

249274
# Multiple ways of including CVSS metrics.
250-
# Newer data uses "impact" -- we may wish to delete the old below
251-
252-
# sometimes (frequently?) the impact is empty
275+
# Check for CVSSv4 first, then fall back to v3, then v2
253276
if "impact" in cve_item:
254-
if "baseMetricV3" in cve_item["impact"]:
277+
if "baseMetricV4" in cve_item["impact"]:
278+
cve["CVSS_version"] = 4
279+
if "cvssV4" in cve_item["impact"]["baseMetricV4"]:
280+
# grab either the data or some default values
281+
cve["severity"] = cve_item["impact"]["baseMetricV4"][
282+
"cvssV4"
283+
].get("baseSeverity", "UNKNOWN")
284+
cve["score"] = cve_item["impact"]["baseMetricV4"]["cvssV4"].get(
285+
"baseScore", 0
286+
)
287+
vector_string = cve_item["impact"]["baseMetricV4"][
288+
"cvssV4"
289+
].get("vectorString", "")
290+
# Ensure correct CVSS format with decimal point in version
291+
if "CVSS:40/" in vector_string:
292+
vector_string = vector_string.replace(
293+
"CVSS:40/", "CVSS:4.0/"
294+
)
295+
cve["CVSS_vector"] = vector_string
296+
elif "baseMetricV3" in cve_item["impact"]:
255297
cve["CVSS_version"] = 3
256298
if "cvssV3" in cve_item["impact"]["baseMetricV3"]:
257299
# grab either the data or some default values
@@ -283,9 +325,12 @@ def format_data_api2(self, all_cve_entries):
283325
elif "metrics" in cve_item:
284326
cve_cvss = cve_item["metrics"]
285327

286-
# Get CVSSv3 or CVSSv2 score
328+
# Get CVSSv4, CVSSv3 or CVSSv2 score
287329
cvss_available = True
288-
if "cvssMetricV31" in cve_cvss:
330+
if "cvssMetricV4" in cve_cvss:
331+
cvss_data = cve_cvss["cvssMetricV4"][0]["cvssData"]
332+
cve["CVSS_version"] = 4
333+
elif "cvssMetricV31" in cve_cvss:
289334
cvss_data = cve_cvss["cvssMetricV31"][0]["cvssData"]
290335
cve["CVSS_version"] = 3
291336
elif "cvssMetricV30" in cve_cvss:
@@ -299,7 +344,11 @@ def format_data_api2(self, all_cve_entries):
299344
if cvss_available:
300345
cve["severity"] = cvss_data.get("baseSeverity", "UNKNOWN")
301346
cve["score"] = cvss_data.get("baseScore", 0)
302-
cve["CVSS_vector"] = cvss_data.get("vectorString", "")
347+
vector_string = cvss_data.get("vectorString", "")
348+
# Ensure correct CVSS format with decimal point in version
349+
if "CVSS:40/" in vector_string:
350+
vector_string = vector_string.replace("CVSS:40/", "CVSS:4.0/")
351+
cve["CVSS_vector"] = vector_string
303352
# End old metrics section
304353

305354
# do some basic input validation checks
@@ -318,9 +367,17 @@ def format_data_api2(self, all_cve_entries):
318367
cve["score"] = "invalid"
319368

320369
# CVSS_vector will be validated/normalized when cvss library is used but
321-
# we can at least do a character filter here
370+
# we can at least do a character filter here and ensure the version format is correct
322371
# we expect letters (mostly but not always uppercase), numbers, : and /
323-
cve["CVSS_vector"] = re.sub("[^A-Za-z0-9:/]", "", cve["CVSS_vector"])
372+
if "CVSS_vector" in cve:
373+
vector_string = cve["CVSS_vector"]
374+
# Ensure correct CVSS format with decimal point in version if vector string exists
375+
if vector_string and "CVSS:40/" in vector_string:
376+
vector_string = vector_string.replace("CVSS:40/", "CVSS:4.0/")
377+
# Perform character filtering
378+
vector_string = re.sub(r"<[^>]*>", "", vector_string)
379+
vector_string = re.sub(r"[^A-Za-z0-9:./]", "", vector_string)
380+
cve["CVSS_vector"] = vector_string
324381

325382
cve_data.append(cve)
326383

cve_bin_tool/vex_manager/generate.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,12 @@ def __get_vulnerabilities(self) -> List[Vulnerability]:
211211
vulnerability.set_description(cve.description)
212212
vulnerability.set_comment(cve.comments)
213213
vulnerability.set_status(self.analysis_state[self.vextype][cve.remarks])
214+
215+
# Include CVSS version in the details
216+
if cve.cvss_version == 4:
217+
vulnerability.set_value("cvss_v4_score", str(cve.score))
218+
vulnerability.set_value("cvss_v4_vector", cve.cvss_vector)
219+
214220
if cve.justification:
215221
vulnerability.set_justification(cve.justification)
216222
if cve.response:

0 commit comments

Comments
 (0)