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.

Posted by:

David Greenwood

David Greenwood, Do Only Good Everyday




Discuss this post


Head on over to the DOGESEC community to discuss this post.

DOGESEC community

Never miss an update


Sign up to receive new articles in your inbox as they published.

Your subscription could not be saved. Please try again.
Your subscription has been successful.