Skip to content

How CWE Explorer links CVEs to CWEs

This document explains where the data comes from when you give cwex a CVE identifier, how the links between a CVE and its CWEs are built and merged, and how those CWEs are placed in MITRE's View 1000 hierarchy. Figures are from the CWE 5.0 catalog (2026-10-28).

The short version

A CWE is a type of weakness ("Deserialization of Untrusted Data"). A CVE is one concrete vulnerability in one product ("Log4Shell in Apache Log4j2"). Several parties state which CWE types a CVE is an instance of. CWE Explorer collects those statements from three places, merges them into one list of links (each one labeled with who made it), and then draws the CWEs inside the View 1000 hierarchy, which comes from the CWE catalog.

flowchart LR
    subgraph online["Online, cached for 7 days"]
        ORG["CVE.org API<br/>official CVE record"]
        NVD["NVD API 2.0<br/>NIST analysis"]
    end
    subgraph offline["Offline: the CWE catalog"]
        XML["cwec_latest.xml<br/>CWE 5.0"]
    end
    ORG -- "CNA and ADP problemTypes<br/>CVSS, description, dates" --> MERGE
    NVD -- "the NVD's own weaknesses<br/>CVSS" --> MERGE
    XML -- "Observed_Examples<br/>(CVEs cited by each CWE)" --> MERGE
    MERGE["cve.py<br/>merge the links<br/>and keep their sources"] --> REC["CVE record<br/>links + CVSS"]
    XML -- "ChildOf links of View 1000" --> HIER["Hierarchy<br/>957 weaknesses"]
    REC --> PLACE{"Is the CWE in<br/>View 1000?"}
    HIER --> PLACE
    PLACE -- yes --> OUT["Tree, TUI, web graph, JSON:<br/>lineages up to the pillars"]
    PLACE -- no --> NOTE["Listed with an explanation:<br/>grouping, deprecated, NVD-CWE-noinfo…"]
Where the data comes from. CVE.org and the NVD are queried online and cached for seven days; the CWE catalog is a local file. The links from the three sources are merged into one CVE record, and each linked CWE is placed in View 1000 or listed with an explanation.

How the two catalogs relate

The CVE and CWE catalogs are separate and are maintained by different people. The only links between them are statements of the form "CVE X is an instance of CWE Y", and the link always goes from the CVE to the CWE, with one exception: the CWE catalog itself cites some CVEs as observed examples of a weakness, which is the same relationship seen from the other side.

erDiagram
    CVE ||--o{ LINK : "is linked by"
    LINK }o--|| CWE_REF : "points to"
    LINK ||--|{ SOURCE : "is asserted by"
    CWE_REF |o--o| WEAKNESS : "resolves to, if in View 1000"
    WEAKNESS }o--o{ WEAKNESS : "ChildOf (View 1000)"
    WEAKNESS ||--o{ OBSERVED_EXAMPLE : "cites"
    OBSERVED_EXAMPLE }o--|| CVE : "refers to"

    CVE {
        string id "CVE-2021-44228"
        string state "PUBLISHED or REJECTED"
        date published
        string assigner "CNA short name"
        list cvss "score, severity, version, source"
    }
    LINK {
        string ref "CWE-502, or NVD-CWE-noinfo"
        int cwe "null for NVD-CWE-* values"
        list sources "apache (CNA), NVD, ..."
    }
    SOURCE {
        string name "CNA, ADP, NVD or CWE catalog"
    }
    WEAKNESS {
        int id
        string abstraction "Pillar, Class, Base, Variant, Compound"
        string status "Stable, Draft, Incomplete..."
    }
The data model. A CVE has links to CWE references, each asserted by one or more sources. A reference resolves to a weakness when it is in View 1000, and the catalog closes the loop by citing some CVEs as observed examples.

Things worth noticing in this model:

  • It is many to many. A CVE can have several CWEs (Log4Shell has five), and a CWE has thousands of CVEs. The links described here start from a CVE. The other direction, every CVE of a CWE, is asked of the NVD alone with cwex 79 --cves (see CVEs of a CWE): it gives a count and the most recent CVEs, not links merged from every source.
  • The link points to a CWE reference, not always to a weakness. The reference may be a grouping (formerly a category), a view, a deprecated entry, or one of the NVD's placeholders (NVD-CWE-noinfo, NVD-CWE-Other). Those links are kept and explained, but they have no place in the hierarchy.
  • Each link remembers every source that asserts it. That is the information you need to judge a link, because sources often disagree.

Data sources

Who the CNAs, the ADPs, the NVD and the CWE catalog are, and how they relate, is explained in The CVE ecosystem. This section covers what the tool reads from each source.

Source Endpoint What CWE Explorer reads Offline
CVE.org record https://cveawg.mitre.org/api/cve/{id} (CVE JSON 5) State, publication date, CNA, title, description, CWEs of the CNA and of each ADP, CVSS No (cached)
NVD https://services.nvd.nist.gov/rest/json/cves/2.0?cveId={id} CWEs assigned by NVD analysts, NVD's CVSS, and the description, dates and state if CVE.org did not answer No (cached)
CWE catalog https://cwe.mitre.org/data/xml/cwec_latest.xml.zip (already downloaded for the hierarchy) CVEs cited as observed examples, and the ChildOf links of View 1000 Yes
FIRST https://api.first.org/data/v1/epss?cve={id} The EPSS score: probability of exploitation in the next 30 days, percentile and date No (cached for a day)
NVD, by weakness https://services.nvd.nist.gov/rest/json/cves/2.0?cweId=CWE-{id} For --cves: how many CVEs list the CWE, and the most recent ones No (cached for a day)

CVE.org: the official record

The CVE record is published by the CNA (CVE Numbering Authority) that assigned the identifier, usually the vendor. ADPs (Authorized Data Publishers), such as CISA's Vulnrichment program, can add their own container to the same record. Both may include problemTypes with CWE identifiers:

{
  "cveMetadata": {
    "cveId": "CVE-2021-44228",
    "state": "PUBLISHED",
    "assignerShortName": "apache",
    "datePublished": "2021-12-10T00:00:00.000Z"
  },
  "containers": {
    "cna": {
      "problemTypes": [{"descriptions": [
        {"type": "CWE", "lang": "en", "cweId": "CWE-502",
         "description": "CWE-502 Deserialization of Untrusted Data"}
      ]}]
    },
    "adp": [{"providerMetadata": {"shortName": "CISA-ADP"}, "problemTypes": ["…"], "metrics": ["…"]}]
  }
}
Field Used as
cveMetadata.state, datePublished, assignerShortName State, publication date and CNA
containers.cna.title, descriptions[lang=en] Title and description (rejectedReasons for rejected CVEs)
containers.cna.problemTypes[].descriptions[].cweId Links with source <CNA> (CNA), for example apache (CNA)
containers.adp[].problemTypes[]…cweId Links with the ADP's short name as source, for example CISA-ADP
containers.{cna,adp[]}.metrics[].cvssV4_0, cvssV3_1, cvssV3_0, cvssV2_0 CVSS scores, with the same sources
containers.adp[].metrics[].other with type: "ssvc" CISA's SSVC assessment: its options, version and timestamp
containers.adp[].metrics[].other with type: "kev" The KEV entry: dateAdded and a link to CISA's catalog

NVD: NIST's analysis

The NVD republishes every CVE and adds its own analysis. Its weaknesses list mixes the NVD's own CWEs with copies of what the CNA and the ADPs said, distinguished by source:

"weaknesses": [
  {"source": "security@apache.org", "type": "Secondary",
   "description": [{"value": "CWE-20"}, {"value": "CWE-400"}, {"value": "CWE-502"}]},
  {"source": "nvd@nist.gov", "type": "Secondary",
   "description": [{"value": "CWE-917"}]}
]

Only the entries whose source is nvd@nist.gov are taken, with the source NVD. The others are copies of the CVE.org record, which is read directly. If CVE.org could not be reached, they are used too, labeled <source> (via NVD). The NVD's own CVSS metrics (metrics.cvssMetricV40, V31, V30, V2 with source nvd@nist.gov) are added with the source NVD. The NVD also copies CISA's KEV data (cisaExploitAdd, cisaActionDue, cisaVulnerabilityName, cisaRequiredAction), which completes the KEV entry from CVE.org with the deadline and CISA's name for the vulnerability.

The CWE catalog: observed examples

Many CWE entries list real CVEs that illustrate the weakness. When the catalog is parsed, each of those citations is indexed as CVE → CWEs that cite it:

<Weakness ID="917" Name="Improper Neutralization of Special Elements used in an Expression Language Statement" …>
  <Observed_Examples>
    <Observed_Example>
      <Reference>CVE-2021-44228</Reference>
      <Description>Product does not neutralize ${xyz} style expressions, allowing remote
        code execution. (log4shell vulnerability in log4j)</Description>
      <Link>https://www.cve.org/CVERecord?id=CVE-2021-44228</Link>
    </Observed_Example>
  </Observed_Examples>
</Weakness>

The catalog cites 2,202 distinct CVEs (from 1999 to 2026) in 3,172 citations spread over 593 weaknesses, all of them in View 1000. Most of those CVEs (1,485) are cited by a single CWE. The record is CVE-2014-1266 (Apple's "goto fail"), cited by 10 CWEs. This index is stored with the catalog cache (catalog.json), so it works offline and is also embedded in the web page.

The same XML provides the hierarchy: only Related_Weakness elements with Nature="ChildOf" and View_ID="1000" are used. CWE-917, for example, also declares ChildOf CWE-74 in view 1003 (the one the NVD uses), which is ignored.

lookup_cve() in cwe_explorer/cve.py runs these steps. The browser does the same in fetchCve() and lookupCve() (see In the browser).

sequenceDiagram
    autonumber
    participant T as cwex
    participant C as Cache<br/>cve/ID.json
    participant O as CVE.org API
    participant N as NVD API
    participant X as CWE catalog
    T->>C: read the saved copy
    alt copy younger than 7 days and no --update
        C-->>T: saved record
    else no copy, old copy or --update
        T->>O: GET /api/cve/{id}
        O-->>T: CVE record, 404, or network error
        T->>N: GET /cves/2.0?cveId={id}
        N-->>T: analysis, no results, or network error
        alt both answered and at least one knows the CVE
            T->>C: save the merged record
        else a source failed
            Note over T: use what arrived, add a note, do not save
        end
    end
    T->>X: CWEs whose observed examples cite the CVE
    X-->>T: links with source "CWE catalog (observed example)"
How cwex builds the list of links for a CVE: a cached copy when it is recent enough, otherwise CVE.org and the NVD, and always the catalog's observed examples.

Merge rules

  1. Normalization. A value is a link if it starts with CWE-<number>: the cweId field, or the text of a description such as CWE-79 Improper Neutralization…. NVD-CWE-noinfo and NVD-CWE-Other are kept as special links. Anything else, such as n/a or free text, is ignored.
  2. One link per CWE. If several sources name the same CWE, it is a single link whose sources list grows (CWE-917 · from NVD, CWE catalog (observed example)).
  3. Source order. Links appear in the order they are first found: the CNA, then each ADP in record order, then the NVD, then the CWE catalog. The official links therefore come before the observed examples.
  4. NVD entries. Only the NVD's own (nvd@nist.gov), unless CVE.org was unreachable (see above).
  5. CVSS. One score per version and source, rounded to one decimal, with the severity capitalized. The web graph labels the CVE node with the newest version available (4.0 over 3.1 over 3.0 over 2.0).
  6. Text fields. State, dates and description come from CVE.org; the NVD only fills them in when CVE.org did not answer.

Each link is then resolved against the hierarchy:

  • The CWE is in View 1000 (957 weaknesses, including the deprecated CWE-377, which still has children): it becomes one of the CVE's linked CWEs.
  • It is not: the link stays in the list with an explanation, the same one you get when you query that ID directly. For example, "CWE-16 (Configuration) is a grouping (formerly a Category), not a weakness, so it is not part of View 1000", or "NVD-CWE-noinfo: the NVD found insufficient information to assign a CWE".

The linked CWEs are drawn together:

  • Tree and interactive explorer. The lineages of every linked CWE are merged into a single tree (build_rows() with several focuses), so shared ancestors appear only once. Linked CWEs are marked with ◀. If a linked CWE is an ancestor of another one, as CWE-74 is of CWE-917 in Log4Shell, the tree follows the path down to the second one.
  • Web graph. The CVE is one more node (internal ID -1), whose parents are its linked CWEs. The layered layout therefore places it one column to the right of the deepest one, joined by dotted edges. The panel of each linked CWE links back to the CVE.
  • --ancestors / --descendants. These run once for each linked CWE.

By default a CVE is shown with --depth 0: the lineages of its CWEs and nothing below them, so the whole picture fits. The children of each CWE, which are the more specific alternatives, are one +N click or one --depth 1 away.

Two worked examples

Log4Shell (CVE-2021-44228)

Link apache (CNA) CISA-ADP NVD CWE catalog
CWE-502 Deserialization of Untrusted Data (Base) ✓
CWE-400 Uncontrolled Resource Consumption (Class) ✓
CWE-20 Improper Input Validation (Class) ✓
CWE-917 Improper Neutralization of Special Elements used in an Expression Language Statement (Base) ✓ ✓
CWE-74 Improper Neutralization of Special Elements in Output Used by a Downstream Component (Class) ✓

CISA's container adds a CVSS 3.1 score (10.0 Critical) but no CWE. The NVD's own analysis disagrees with Apache's: it points to CWE-917, expression language injection, which describes the ${…} lookups the attack abuses. The CWE catalog cites the CVE under CWE-917 and under its ancestor CWE-74. Merged in View 1000:

flowchart LR
    P664["Pi CWE-664<br/>Improper Control of a Resource<br/>Through its Lifetime"] --> C913["Cl CWE-913<br/>Improper Control of Dynamically-<br/>Managed Code Resources"]
    C913 --> B502["Ba CWE-502<br/>Deserialization of Untrusted Data"]
    P664 --> C400["Cl CWE-400<br/>Uncontrolled Resource Consumption"]
    P707["Pi CWE-707<br/>Improper Neutralization"] --> C20["Cl CWE-20<br/>Improper Input Validation"]
    P707 --> C74["Cl CWE-74<br/>Improper Neutralization of Special<br/>Elements in Output Used by a<br/>Downstream Component"]
    C74 --> C77["Cl CWE-77<br/>Improper Neutralization of Special<br/>Elements used in a Command"]
    C77 --> B917["Ba CWE-917<br/>Improper Neutralization of Special<br/>Elements used in an Expression<br/>Language Statement"]
    B502 -. "apache (CNA)" .-> CVE(["CVE-2021-44228<br/>Log4Shell · 10.0 Critical"])
    C400 -. "apache (CNA)" .-> CVE
    C20 -. "apache (CNA)" .-> CVE
    B917 -. "NVD, catalog" .-> CVE
    C74 -. "catalog" .-> CVE
The five CWEs of Log4Shell (CVE-2021-44228) placed in View 1000. Solid arrows go from parent to child; dotted arrows join each CWE to the CVE and are labeled with who made the link. Apache's CWEs fall under two pillars, and the NVD's CWE-917 sits deep in the injection branch.

The picture shows at a glance what the list of links does not: Apache's three CWEs belong to two different pillars, and the NVD's CWE-917 sits at the bottom of the injection branch, three levels below the pillar.

Heartbleed (CVE-2014-0160)

Link redhat (CNA) CISA-ADP NVD CWE catalog
CWE-125 Out-of-bounds Read (Base) ✓ ✓ ✓
CWE-126 Buffer Over-read (Variant) ✓
CWE-130 Improper Handling of Length Parameter Inconsistency (Base) ✓
CWE-240 Improper Handling of Inconsistent Structural Elements (Class) ✓

The CNA's record says n/a, so the official link comes only from CISA and the NVD. The catalog cites Heartbleed under four CWEs. One of them, CWE-126, is a child of the official CWE-125 (and also of CWE-788). Two others, CWE-240 and its child CWE-130, sit in a different pillar. CWE-240 alone has three lineages: two through CWE-228, from the pillars CWE-707 and CWE-703, and one directly from CWE-707.

flowchart LR
    P664["Pi CWE-664"] --> C118["Cl CWE-118<br/>Incorrect Access of<br/>Indexable Resource"]
    C118 --> C119["Cl CWE-119<br/>Improper Restriction of Operations<br/>within the Bounds of a Memory Buffer"]
    C119 --> B125["Ba CWE-125<br/>Out-of-bounds Read"]
    C119 --> B788["Ba CWE-788<br/>Access of Memory Location<br/>After End of Buffer"]
    B125 --> V126["Va CWE-126<br/>Buffer Over-read"]
    B788 --> V126
    P707["Pi CWE-707"] --> C228["Cl CWE-228<br/>Improper Handling of<br/>Syntactically Invalid Structure"]
    C228 --> B237["Ba CWE-237<br/>Improper Handling of<br/>Structural Elements"]
    B237 --> C240["Cl CWE-240<br/>Improper Handling of Inconsistent<br/>Structural Elements"]
    P703["Pi CWE-703"] --> C228
    P707 --> C240
    C240 --> B130["Ba CWE-130<br/>Improper Handling of Length<br/>Parameter Inconsistency"]
    B125 -. "CISA-ADP, NVD, catalog" .-> CVE(["CVE-2014-0160<br/>Heartbleed · 7.5 High"])
    V126 -. "catalog" .-> CVE
    B130 -. "catalog" .-> CVE
    C240 -. "catalog" .-> CVE
The four CWEs of Heartbleed (CVE-2014-0160) placed in View 1000. CWE-126 has two parents, and CWE-240 has three lineages from two pillars. Only CWE-125 comes from an official source (CISA and the NVD); the rest are the catalog's observed examples.

Caching and working offline

Situation Result
A copy younger than 7 days exists, and no --update The saved copy, without any network access. Observed examples always come from the current catalog.
CVE.org and the NVD both answer, and at least one knows the CVE A fresh record, saved for 7 days
One of them fails What the other returned, plus a note. It is not saved, so the next run tries again.
Both fail, and an older copy exists The older copy, plus a note with the date it was saved
Both fail, there is no copy, and the catalog cites the CVE Only the catalog's links, plus a note for each source that failed
Both fail, there is no copy, and the catalog does not cite it Error: could not reach CVE.org or the NVD, and the CWE catalog does not cite it
Both answer that the CVE does not exist Error: does not exist in CVE.org or the NVD (unless the catalog cites it)

Copies are saved as ~/.cache/cwe-explorer/cve/<ID>.json (or under CWEX_CACHE). EPSS scores are saved apart, in epss/<ID>.json, for one day, because FIRST recalculates them daily; when FIRST cannot be reached, an older score is shown with a note. Without an API key, the NVD allows 5 requests every 30 seconds. Set NVD_API_KEY to send a key with every NVD request (see NVD API key). If the NVD rejects the key it answers 404, which the tool reports as a failed source instead of taking it for "unknown CVE": for a CVE it does not know, the NVD answers 200 with no results.

In the browser

The web page applies the same rules in JavaScript (fetchCve(), lookupCve() and setCve() in cwe_explorer/web/viewer.js). It can therefore look up any CVE, not only the one it was generated for:

  • Where to type it. In the search box, or in the address (#CVE-2014-0160, a link you can share).
  • How it is fetched. CVE.org, the NVD and FIRST allow requests from any origin (CORS), so the page queries them directly, in parallel. The CVEs in the NVD section of a CWE asks the NVD by weakness in the same way.
  • Offline. The catalog's observed-example index is embedded in the page (about 55 KB), so links from the catalog are available without a connection, as in the terminal.
  • Caching. Lookups made from the page are not cached between visits.

Output for other programs

cwex CVE-… --ancestors --json (or --descendants) returns the merged record and one result per linked CWE. Every field is described in the JSON reference.

{
  "schema": 1,
  "type": "cve-relatives",
  "catalog": {"name": "CWE", "version": "5.0", "date": "2026-10-28", "view": "1000", "view_name": "Complete Weakness Hierarchy"},
  "mode": "ancestors",
  "depth": 1,
  "cve": {
    "id": "CVE-2021-44228",
    "state": "PUBLISHED",
    "published": "2021-12-10",
    "assigner": "apache",
    "title": "Apache Log4j2 JNDI features do not protect against attacker controlled LDAP and other JNDI related endpoints",
    "description": "Apache Log4j2 2.0-beta9 through 2.15.0 …",
    "cvss": [
      {"version": "3.1", "score": "10.0", "severity": "Critical",
       "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H", "source": "CISA-ADP"}
    ],
    "mappings": [
      {"ref": "CWE-502", "cwe": 502, "sources": ["apache (CNA)"], "in_view": true, "note": null},
      {"ref": "CWE-917", "cwe": 917, "sources": ["NVD", "CWE catalog (observed example)"], "in_view": true, "note": null}
    ],
    "notes": [],
    "kev": {"date_added": "2021-12-10", "due_date": "2021-12-24",
            "name": "Apache Log4j2 Remote Code Execution Vulnerability", "required_action": "…",
            "url": "https://www.cisa.gov/known-exploited-vulnerabilities-catalog?field_cve=CVE-2021-44228",
            "sources": ["CISA-ADP", "NVD"]},
    "ssvc": {"options": [["Exploitation", "active"], ["Automatable", "yes"], ["Technical Impact", "total"]],
             "version": "2.0.3", "date": "2025-02-04", "source": "CISA-ADP"},
    "epss": {"score": 0.99999, "percentile": 1.0, "date": "2026-11-03"},
    "url": "https://www.cve.org/CVERecord?id=CVE-2021-44228",
    "nvd_url": "https://nvd.nist.gov/vuln/detail/CVE-2021-44228"
  },
  "weaknesses": [
    {
      "schema": 1, "type": "relatives", "id": 502, "cwe": 502,
      "name": "Deserialization of Untrusted Data", "abstraction": "Base",
      "mode": "ancestors", "depth": 1, "count": 1, "note": null,
      "results": [
        {"id": 913, "name": "Improper Control of Dynamically-Managed Code Resources", "distance": 1,
         "via": [502], "primary_parent": true, "primary_lineage": true}
      ]
    }
  ]
}

(Shortened: the real output has every link, CVSS score and linked CWE, and each result also carries abstraction, status, published and url.) A link that cannot be placed has "in_view": false and its explanation in note.

Limitations

  • Sources disagree, and no winner is chosen. All links are shown with their sources so that you can judge them.
  • Coverage depends on the CNA. Many CVEs, older ones especially, have no CWE in their CNA record, and some have no CWE from any source.
  • Observed examples are citations, not official links. CWE authors choose them to illustrate a weakness, so they are often more specific or more general than the official link (Heartbleed under CWE-126, CWE-130 and CWE-240).
  • One view at a time. Links are placed in View 1000, or in the view chosen with --view (cwex CVE-2021-44228 --view 1003 shows where the NVD's own view puts them). Links to groupings, views, deprecated entries and NVD placeholders, and to CWEs outside the chosen view, are listed but cannot be drawn.
  • From a CWE to its CVEs, only through the NVD. --cves counts the CVEs whose NVD record lists the CWE; it does not merge sources the way a CVE lookup does. The catalog's observed examples are the only other CWE → CVE information, and they are not exhaustive.

Where it lives in the code

File Role
cwe_explorer/data.py Parses the CWE XML, including the observed index (CVE → citing CWEs) stored in catalog.json
cwe_explorer/cve.py parse_cve_id(), lookup_cve() (cache, CVE.org, NVD, fallback), merge rules (CVE.add(), _from_cve_org(), _from_nvd()), exploitation signals (_signal(), CVE.add_kev(), fetch_epss()), cves_for_cwe() and the NVD rate limit (_nvd_wait()), mapping_note(), linked_cwes(), cve_payload()
cwe_explorer/render_term.py CVE card (cve_fields()), merged tree (build_rows() with several focuses), print_cve()
cwe_explorer/tui.py The CVE row, / lookups of other CVEs, history that remembers the loaded CVE
cwe_explorer/render_html.py Puts the merged record and the observed index into the page
cwe_explorer/web/viewer.js CVE node, panel and live lookups (setCve(), fetchCve(), lookupCve())
tests/test_cwe_explorer.py CVETest: merging, cache, offline fallback, merged tree and CLI, with the APIs simulated