If you are reading this blog post via a 3rd party source it is very likely that many parts of it will not render correctly (usually, the interactive graphs). Please view the post on dogesec.com for the full interactive viewing experience.
tl;dr
Follow along as I show you how to store 200,000 CVEs as STIX objects, then use CVSS, EPSS, CISA KEV and CPE data to search and filter them.
Overview
In the past I’ve discussed how to use the NVD APIs to write STIX Patterns to detect products vulnerable to a CVE.
That post only tell a small part of the story.
Here at DOGESEC we use NVD data to create a range of STIX objects.
Having the CVEs in STIX (which is represented in JSON) makes it easy to store, query and expose the data.
In this post I will show you how to import the data into a graph database and write some queries to understand it better. What I really want to demonstrate is how quick, easy, and cheap (it’s free), this is to do.
Import CVE STIX data
cve2stix contains logic to turn published vulnerabilities into STIX Objects.
Here is a graph of how the STIX objects are created and linked;
We maintain an archive of this data online, available to anyone.
In stix2arango we’ve created a script to automatically download this data and insert it into ArangoDB for processing. I won’t go in to exactly how this works, but once you’ve installed stix2arango all you need to do is run;
python3 utilities/arango_cti_processor/insert_archive_cve.py \
--database blog_demo
Query the data
Count of CVEs
Once the import completes you can use the ArangoDB query interface to filter and manipulate the objects. Here are some examples to get you started.
Show a count of all published Vulnerabilities;
FOR doc in nvd_cve_vertex_collection
FILTER doc.type == "vulnerability"
COLLECT WITH COUNT INTO length
RETURN length
[
253333
]
Break the count of CVEs out by the month they were published;
FOR doc in nvd_cve_vertex_collection
FILTER doc.type == "vulnerability"
COLLECT year = DATE_YEAR(doc.created), month = DATE_MONTH(doc.created) WITH COUNT INTO count
SORT year DESC, month DESC
RETURN [{ year: year, month: month, count: count }]
[
[
{
"year": 2024,
"month": 7,
"count": 2034
}
],
[
{
"year": 2024,
"month": 6,
"count": 2562
}
],
[
{
"year": 2024,
"month": 5,
"count": 4801
}
],
Results truncated for brevity.
Let me show you what the latest Vulnerability object indexed looks like:
FOR doc in nvd_cve_vertex_collection
FILTER doc.type == "vulnerability"
SORT doc.created DESC
LIMIT 1
LET keys = ATTRIBUTES(doc)
LET filteredKeys = keys[* FILTER !STARTS_WITH(CURRENT, "_")]
RETURN [KEEP(doc, filteredKeys)]
[
[
{
"created": "2024-07-31T11:15:11.010Z",
"created_by_ref": "identity--562918ee-d5da-5579-b6a1-fae50cc6bad3",
"description": "The Tainacan plugin for WordPress is vulnerable to unauthorized access of data due to a missing capability check on the 'get_file' function in all versions up to, and including, 0.21.7. The function is also vulnerable to directory traversal. This makes it possible for authenticated attackers, with Subscriber-level access and above, to read the contents of arbitrary files on the server, which can contain sensitive information.",
"extensions": {
"extension-definition--2c5c13af-ee92-5246-9ba7-0b958f8cd34a": {
"extension_type": "toplevel-property-extension"
}
},
"external_references": [
{
"source_name": "cve",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2024-7135",
"external_id": "CVE-2024-7135"
},
{
"source_name": "cwe",
"url": "https://cwe.mitre.org/data/definitions/CWE-862.html",
"external_id": "CWE-862"
},
{
"source_name": "[email protected]",
"description": "",
"url": "https://plugins.trac.wordpress.org/browser/tainacan/trunk/classes/api/endpoints/class-tainacan-rest-background-processes-controller.php#L370"
},
{
"source_name": "[email protected]",
"description": "",
"url": "https://plugins.trac.wordpress.org/browser/tainacan/trunk/classes/api/endpoints/class-tainacan-rest-background-processes-controller.php#L378"
},
{
"source_name": "[email protected]",
"description": "",
"url": "https://plugins.trac.wordpress.org/changeset/3127693/"
},
{
"source_name": "[email protected]",
"description": "",
"url": "https://www.wordfence.com/threat-intel/vulnerabilities/id/e4dd0c6a-75af-4b53-ac13-fc4ef0e9001d?source=cve"
}
],
"id": "vulnerability--c51f60bc-ed38-5fce-b5cb-e17773b8b02d",
"modified": "2024-07-31T12:57:02.300Z",
"name": "CVE-2024-7135",
"object_marking_refs": [
"marking-definition--94868c89-83c2-464b-929b-a1a8aa3c8487",
"marking-definition--562918ee-d5da-5579-b6a1-fae50cc6bad3"
],
"spec_version": "2.1",
"type": "vulnerability",
"x_cvss": {
"v3_1": {
"base_score": 6.5,
"base_severity": "MEDIUM",
"exploitability_score": 2.8,
"impact_score": 3.6,
"vector_string": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N"
}
},
"x_epss": {
"date": "2024-08-30",
"percentile": "0.306560000",
"score": "0.000680000"
}
}
]
]
There is lots of useful data inside the STIX object, all of which I can pivot on.
CVSS data
CVSS v2_0
, v3_0
, v_3_1
, and v4_0
scores are indexed, depending on what scoring version NVD has published.
For example, here are all the objects with CVSSv4 scores calculated;
FOR doc in nvd_cve_vertex_collection
FILTER doc.type == "vulnerability"
FILTER HAS(doc, "x_cvss") AND HAS(doc.x_cvss, "v4_0")
LET keys = ATTRIBUTES(doc)
LET filteredKeys = keys[* FILTER !STARTS_WITH(CURRENT, "_")]
RETURN [KEEP(doc, filteredKeys)]
[
[
{
"created": "2024-04-18T23:15:07.650Z",
"created_by_ref": "identity--562918ee-d5da-5579-b6a1-fae50cc6bad3",
"description": "Electrolink transmitters store credentials in clear-text. Use of these credentials could allow an attacker to access the system.",
"extensions": {
"extension-definition--2c5c13af-ee92-5246-9ba7-0b958f8cd34a": {
"extension_type": "toplevel-property-extension"
}
},
"external_references": [
{
"source_name": "cve",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2024-3742",
"external_id": "CVE-2024-3742"
},
{
"source_name": "cwe",
"url": "https://cwe.mitre.org/data/definitions/CWE-312.html",
"external_id": "CWE-312"
},
{
"source_name": "[email protected]",
"description": "",
"url": "https://www.cisa.gov/news-events/ics-advisories/icsa-24-107-02"
}
],
"id": "vulnerability--3782b4c7-3919-5d5d-9e4f-22d712f7e0b9",
"modified": "2024-05-28T17:15:11.450Z",
"name": "CVE-2024-3742",
"object_marking_refs": [
"marking-definition--94868c89-83c2-464b-929b-a1a8aa3c8487",
"marking-definition--562918ee-d5da-5579-b6a1-fae50cc6bad3"
],
"spec_version": "2.1",
"type": "vulnerability",
"x_cvss": {
"v3_1": {
"base_score": 7.5,
"base_severity": "HIGH",
"exploitability_score": 3.9,
"impact_score": 3.6,
"vector_string": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N"
},
"v4_0": {
"base_score": 8.7,
"base_severity": "HIGH",
"vector_string": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X"
}
},
"x_epss": {
"date": "2024-08-29",
"percentile": "0.224640000",
"score": "0.000540000"
}
}
],
Results truncated for brevity.
A simple search might be to include only results where CVSS base_severity=HIGH
;
FOR doc in nvd_cve_vertex_collection
FILTER doc.type == "vulnerability"
FILTER LENGTH(
FOR cvssVersion IN ATTRIBUTES(doc.x_cvss)
FILTER doc.x_cvss[cvssVersion].base_severity == "HIGH"
RETURN 1
) > 0
SORT doc.modified DESC
RETURN doc.name
[
"CVE-2024-37973",
"CVE-2023-36516",
"CVE-2023-38393",
"CVE-2023-38394",
"CVE-2023-39312",
"CVE-2024-34444",
You can use a query to break out the vector_string
;
FOR doc in nvd_cve_vertex_collection
FILTER doc.type == "vulnerability"
SORT doc.created DESC
LET keys = ATTRIBUTES(doc)
LET filteredKeys = keys[* FILTER !STARTS_WITH(CURRENT, "_")]
// Extract the vector string and split it
LET cvss_versions = ATTRIBUTES(doc.x_cvss)
LET cvss_vectors = MERGE(
FOR version IN cvss_versions
LET vector = SPLIT(doc.x_cvss[version].vector_string, "/")
RETURN MERGE(
{ CVSS: version },
MERGE(
FOR part IN vector
LET key_value = SPLIT(part, ":")
RETURN {[key_value[0]]: key_value[1]}
)
)
)
RETURN {
name: doc.name,
cvss_vectors: cvss_vectors
}
[
{
"name": "CVE-2024-7135",
"cvss_vectors": {
"UI": "N",
"S": "U",
"PR": "L",
"I": "N",
"A": "N",
"C": "H",
"AV": "N",
"CVSS": "3.1",
"AC": "L"
}
},
Now it becomes possible to filter CVEs by different parts of the vector string.
For example, where the Attack Vector is Local (AV:L
);
FOR doc in nvd_cve_vertex_collection
FILTER doc.type == "vulnerability"
SORT doc.created DESC
LET cvss_versions = ATTRIBUTES(doc.x_cvss)
LET cvss_vectors = (
FOR version IN cvss_versions
LET vector = SPLIT(doc.x_cvss[version].vector_string, "/")
LET parsed_vector = MERGE(
{ CVSS: version },
MERGE(
FOR part IN vector
LET key_value = SPLIT(part, ":")
RETURN {[key_value[0]]: key_value[1]}
)
)
FILTER parsed_vector.AV == "L"
RETURN parsed_vector
)
FILTER LENGTH(cvss_vectors) > 0
RETURN {
name: doc.name,
cvss_vectors: cvss_vectors
}
This search considers all CVSS versions. However, you should be aware not all versions of CVSS have the same parts in the vector string. As an example, look at the vector_string
for CVSSv3.1 vs. CVSSv4.0 in the CVE-2024-3742 Vulnerability object printed earlier in this post.
"vector_string": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N"
"vector_string": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X"
EPSS data
CVSS has its flaws. I won’t start a debate here. All I will do is say; EPSS is an additional scoring system that used alongside CVSS can be very helpful to prioritise vulnerabilities.
Let me get the top 100 results of published Vulnerabilities sorted by EPSS score (descending);
FOR doc in nvd_cve_vertex_collection
FILTER doc.type == "vulnerability"
SORT TO_NUMBER(doc.x_epss.score) DESC
LIMIT 100
RETURN [{ name: doc.name, epss_score: doc.x_epss.score, epss_percentile: doc.x_epss.percentile }]
[
{
"name": "CVE-2019-1653",
"epss_score": "0.975640000",
"epss_percentile": "1.000000000"
},
{
"name": "CVE-2018-7600",
"epss_score": "0.975600000",
"epss_percentile": "1.000000000"
},
{
"name": "CVE-2019-2725",
"epss_score": "0.975590000",
"epss_percentile": "0.999990000"
},
{
"name": "CVE-2019-16662",
"epss_score": "0.975580000",
"epss_percentile": "0.999990000"
},
Results truncated for brevity.
It is important to note here the EPSS scores change over time. Thus the x_epss.date
property is very important to consider to determine how up-to-date the scores are (you can use arango_cti_processor to update these objects with the most recent EPSS scores).
CISA KEV
CISA maintain a list of Known Exploited vulnerabilities – a list of vulnerabilities that have been exploited in the wild.
cve2stix models these as Sighting objects, so I can quickly surface these in a search too;
FOR doc in nvd_cve_vertex_collection
FILTER doc.type == "sighting"
SORT doc.created DESC
LET external_ids = doc.external_references[*].external_id
RETURN [{ description: doc.description, cve_id: external_ids }]
[
[
{
"description": "CISA KEV: Acronis Cyber Infrastructure (ACI) Insecure Default Password Vulnerability\n\n Apply mitigations per vendor instructions or discontinue use of the product if mitigations are unavailable.\n\n Action due by: 2024-08-19",
"cve_id": [
"CVE-2023-45249"
]
}
],
[
{
"description": "CISA KEV: ServiceNow Incomplete List of Disallowed Inputs Vulnerability\n\n Apply mitigations per vendor instructions or discontinue use of the product if mitigations are unavailable.\n\n Action due by: 2024-08-19",
"cve_id": [
"CVE-2024-5217"
]
}
],
[
{
"description": "CISA KEV: ServiceNow Improper Input Validation Vulnerability\n\n Apply mitigations per vendor instructions or discontinue use of the product if mitigations are unavailable.\n\n Action due by: 2024-08-19",
"cve_id": [
"CVE-2024-4879"
]
}
],
Results truncated for brevity.
Using CPE data to make it relevant to your organisation
I know what you’re thinking; “this is interesting, but not particularly useful. All I care about is CVEs that affect my organisation”.
The good news; the STIX Indicator objects created for the CVE (and linked to the Vulnerability object) contain the information about the products (and product configorations) that are vulnerable.
Lets take a look at CVE-2023-45249
reported by CISA to be a known exploited vulnerability (as uncovered in the last search);
FOR doc in nvd_cve_vertex_collection
FILTER doc.type == "indicator"
AND doc.name == "CVE-2023-45249"
LET keys = ATTRIBUTES(doc)
LET filteredKeys = keys[* FILTER !STARTS_WITH(CURRENT, "_")]
RETURN [KEEP(doc, filteredKeys)]
[
[
{
"created": "2024-07-24T14:15:04.867Z",
"created_by_ref": "identity--562918ee-d5da-5579-b6a1-fae50cc6bad3",
"description": "Remote command execution due to use of default passwords. The following products are affected: Acronis Cyber Infrastructure (ACI) before build 5.0.1-61, Acronis Cyber Infrastructure (ACI) before build 5.1.1-71, Acronis Cyber Infrastructure (ACI) before build 5.2.1-69, Acronis Cyber Infrastructure (ACI) before build 5.3.1-53, Acronis Cyber Infrastructure (ACI) before build 5.4.4-132.",
"extensions": {
"extension-definition--ad995824-2901-5f6e-890b-561130a239d4": {
"extension_type": "toplevel-property-extension"
}
},
"external_references": [
{
"source_name": "cve",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2023-45249",
"external_id": "CVE-2023-45249"
}
],
"id": "indicator--9e789ed4-1bba-54a2-8b50-72f7abbee92c",
"indicator_types": [
"compromised"
],
"modified": "2024-07-30T14:34:18.393Z",
"name": "CVE-2023-45249",
"object_marking_refs": [
"marking-definition--94868c89-83c2-464b-929b-a1a8aa3c8487",
"marking-definition--562918ee-d5da-5579-b6a1-fae50cc6bad3"
],
"pattern": "([software:cpe='cpe:2.3:a:acronis:cyber_infrastructure:*:*:*:*:*:*:*:*' OR software:cpe='cpe:2.3:a:acronis:cyber_infrastructure:*:*:*:*:*:*:*:*' OR software:cpe='cpe:2.3:a:acronis:cyber_infrastructure:*:*:*:*:*:*:*:*' OR software:cpe='cpe:2.3:a:acronis:cyber_infrastructure:*:*:*:*:*:*:*:*' OR software:cpe='cpe:2.3:a:acronis:cyber_infrastructure:*:*:*:*:*:*:*:*'])",
"pattern_type": "stix",
"pattern_version": "2.1",
"spec_version": "2.1",
"type": "indicator",
"valid_from": "2024-07-24T14:15:04.867Z",
"x_cpes": {
"not_vulnerable": [],
"vulnerable": [
{
"criteria": "cpe:2.3:a:acronis:cyber_infrastructure:*:*:*:*:*:*:*:*",
"matchCriteriaId": "1A50BD85-127D-48B6-BEDE-00CA3BAFCBAD"
},
{
"criteria": "cpe:2.3:a:acronis:cyber_infrastructure:*:*:*:*:*:*:*:*",
"matchCriteriaId": "E5337751-14F1-4E10-80F3-EF7ED6D4B2B1"
},
{
"criteria": "cpe:2.3:a:acronis:cyber_infrastructure:*:*:*:*:*:*:*:*",
"matchCriteriaId": "29CDCE6A-0B62-4FE6-8033-0C790B81BF6A"
},
{
"criteria": "cpe:2.3:a:acronis:cyber_infrastructure:*:*:*:*:*:*:*:*",
"matchCriteriaId": "6125F2EC-7736-4088-AA1F-3017713AB03E"
},
{
"criteria": "cpe:2.3:a:acronis:cyber_infrastructure:*:*:*:*:*:*:*:*",
"matchCriteriaId": "EC427A5A-F1D5-4DEA-A005-BC4676668571"
}
]
}
}
]
]
Here we can see all the CPE Match Strings found in the pattern
that are vulnerable (x_cpes.vulnerable
) (all of them in this case).
This data allows you to start with a full or partial CPE match string of a product and detect what CVEs are vulnerable.
Let me show you all CVEs that the product acronis:cyber_infrastructure
is vulnerable too (and the STIX patterns to identify the combinations of products that make it vulnerable, e.g. if it needs to be running on a specific OS);
FOR doc in nvd_cve_vertex_collection
FILTER doc.type == "indicator"
FILTER LENGTH(
FOR item IN doc.x_cpes.vulnerable
FILTER CONTAINS(item.criteria, "acronis:cyber_infrastructure")
RETURN 1
) > 0
RETURN { name: doc.name, pattern: doc.pattern }
[
{
"name": "CVE-2023-2782",
"pattern": "([software:cpe='cpe:2.3:a:acronis:cyber_infrastructure:*:*:*:*:*:*:*:*'])"
},
{
"name": "CVE-2023-2360",
"pattern": "([software:cpe='cpe:2.3:a:acronis:cyber_infrastructure:*:*:*:*:*:*:*:*'])"
},
{
"name": "CVE-2023-45249",
"pattern": "([software:cpe='cpe:2.3:a:acronis:cyber_infrastructure:*:*:*:*:*:*:*:*' OR software:cpe='cpe:2.3:a:acronis:cyber_infrastructure:*:*:*:*:*:*:*:*' OR software:cpe='cpe:2.3:a:acronis:cyber_infrastructure:*:*:*:*:*:*:*:*' OR software:cpe='cpe:2.3:a:acronis:cyber_infrastructure:*:*:*:*:*:*:*:*' OR software:cpe='cpe:2.3:a:acronis:cyber_infrastructure:*:*:*:*:*:*:*:*'])"
}
]
Going further
The objects are stored and connected in a graph database which makes traversal and grouping of vulnerabilities fairly trivial.
Though these sort of queries are mostly useful for research.
If all you want is to prioritise vulnerabilities that affect you, crafting searches that filter on CPEs then use CVSS, EPSS and CISA KEV to rank and sort them will get you off to a good start.
Vulmatch
Straightforward vulnerability management. Know when software you use is vulnerable, how it is being exploited, and how to detect an attack.
Discuss this post
Head on over to the DOGESEC community to discuss this post.
Never miss an update
Sign up to receive new articles in your inbox as they published.