Skip to content

Relations ​

The relationship graph around one identifier: the advisories filed under a CVE, the distribution notices that repackage it and the fixes they shipped, the packages an advisory names, and from the CVE's dossier its weaknesses, the nixpkgs security issues that track it and the attributes it maps to. Every edge names the table or dossier field it was read from. Automated nixpkgs candidates are separate from attributes listed on a security issue.

GET /api/v1/relations/{id}

Nothing behind this route is synced or scanned. The index is read by key (advisory_aliases, advisory_upstream, distribution_fixes and the advisory row itself), each CVE's dossier is read from its cache, and a package root is answered by OSV's query API. The graph is therefore as current as the index load and the dossier cache, and the response says which of those it read.

The same graph is drawn for a person at cve.vulnpatch.dev/graph/{id}, reachable from every CVE page as "Explore relations".

bash
curl https://api.vulnpatch.dev/api/v1/relations/CVE-2021-44228
curl "https://api.vulnpatch.dev/api/v1/relations/GHSA-jfh8-c2jp-5v3q?depth=2"
curl "https://api.vulnpatch.dev/api/v1/relations/pkg:npm/lodash?format=mermaid"

What {id} can be ​

FormRoot typeExpanded through
CVE-2021-44228cveThe index by key, and the cached dossier
GHSA-jfh8-c2jp-5v3q, DSA-5020-1, CGA-..., any family the index keysadvisoryThe index by key; OSV by id when the index holds no row for it yet
pkg:npm/lodash, pkg:pip/requests, pkg:maven/...packageOSV's query API, as the root only
CWE-79cweNot expanded: nothing indexes CVEs by weakness
nixpkgs:python3Packages.requestsnixpkgs_attributeNot expanded: nothing indexes CVEs by attribute

Case does not matter for an advisory id: ghsa-jfh8-c2jp-5v3q is resolved to the spelling the index files the row under. The index is a periodic load, so an advisory published since the last one has no row yet; its relations are then read from OSV by id (at most three such reads per level), and its edges carry osv.* sources. OSV imports reviewed GitHub advisories only, so a GHSA OSV does not hold is read from GitHub's advisory API, with edges sourced github.advisory.

A package's ecosystem is taken in OSV's spelling or a package manager's (pip is PyPI, cargo is crates.io, maven is Maven), so one package is one node whichever way a source spelled it. A CPE vendor or product from a CVE record is a product node when its ecosystem is not recognized as a package ecosystem. A package named by an advisory but unknown to OSV remains a package node and carries osvEcosystem: false.

Parameters ​

ParameterDefaultMaximumMeaning
depth12Levels out from the root. Beyond the value given the response is clamped, and depth in the body says what was used.
limit25100Neighbours per relation per expanded node. The whole graph is capped at 300 nodes and 600 edges.
formatjsonmermaid renders the same graph as a Mermaid flowchart, served as text/vnd.mermaid.

Twelve nodes at most are expanded per level; the rest are listed in notExpanded so a client can ask for each on its own. Eight dossiers at most are read per request.

Response ​

json
{
  "success": true,
  "data": {
    "root": "cve:CVE-2021-44228",
    "rootType": "cve",
    "depth": 1,
    "limit": 25,
    "nodes": [
      { "id": "cve:CVE-2021-44228", "type": "cve", "key": "CVE-2021-44228", "label": "CVE-2021-44228", "severity": "critical", "summary": "Apache Log4j2 JNDI features..." },
      { "id": "advisory:GHSA-jfh8-c2jp-5v3q", "type": "advisory", "key": "GHSA-jfh8-c2jp-5v3q", "label": "GHSA-jfh8-c2jp-5v3q", "source": "Maven", "summary": "Remote code injection in Log4j" },
      { "id": "advisory:DSA-5020-1", "type": "distribution_notice", "key": "DSA-5020-1", "label": "DSA-5020-1", "source": "Debian", "ecosystem": "Debian" },
      { "id": "cwe:CWE-917", "type": "cwe", "key": "CWE-917", "label": "CWE-917", "name": "Expression Language Injection" },
      { "id": "nixpkgs_issue:149354", "type": "nixpkgs_issue", "key": "149354", "label": "#149354", "state": "closed", "url": "https://github.com/NixOS/nixpkgs/issues/149354" },
      { "id": "nixpkgs_attribute:log4j", "type": "nixpkgs_attribute", "key": "log4j", "label": "log4j", "channels": ["nixos-unstable"] }
    ],
    "edges": [
      { "id": "alias_of|advisory:GHSA-jfh8-c2jp-5v3q|cve:CVE-2021-44228", "type": "alias_of", "from": "advisory:GHSA-jfh8-c2jp-5v3q", "to": "cve:CVE-2021-44228", "source": "advisory_aliases" },
      { "id": "fixes|advisory:DSA-5020-1|cve:CVE-2021-44228", "type": "fixes", "from": "advisory:DSA-5020-1", "to": "cve:CVE-2021-44228", "source": "distribution_fixes" },
      { "id": "weakness|cve:CVE-2021-44228|cwe:CWE-917", "type": "weakness", "from": "cve:CVE-2021-44228", "to": "cwe:CWE-917", "source": "dossier.canonical.cwes" },
      { "id": "tracked_by|cve:CVE-2021-44228|nixpkgs_issue:149354", "type": "tracked_by", "from": "cve:CVE-2021-44228", "to": "nixpkgs_issue:149354", "source": "dossier.nixpkgs.trackerIssues" },
      { "id": "candidate_match|cve:CVE-2021-44228|nixpkgs_attribute:log4j", "type": "candidate_match", "from": "cve:CVE-2021-44228", "to": "nixpkgs_attribute:log4j", "source": "dossier.nixpkgs.candidates" }
    ],
    "truncated": false,
    "expanded": [{ "id": "cve:CVE-2021-44228", "source": "index", "truncated": false }],
    "notExpanded": [
      { "id": "advisory:GHSA-jfh8-c2jp-5v3q", "reason": "Beyond the requested depth." },
      { "id": "cwe:CWE-917", "reason": "The index keys CVEs by advisory, not by weakness, so the CVEs sharing a CWE are not read." }
    ],
    "coverage": {
      "index": "read",
      "indexSources": { "available": true, "sources": [{ "source": "osv", "rowCount": 125000, "indexedAt": "2026-09-28T11:00:00.000Z", "complete": true }], "latestIndexedAt": "2026-09-28T11:00:00.000Z", "incompleteSources": [] },
      "dossier": { "CVE-2021-44228": "cached" },
      "osv": "not_needed",
      "cweCatalog": { "status": "read", "requested": 1, "enriched": 1, "fetched": 1, "omitted": 0, "unresolved": 0, "version": "4.20", "versionCheckedAt": "2026-09-28T12:00:00.000Z" }
    },
    "failures": [],
    "partial": false,
    "links": {
      "self": "https://api.vulnpatch.dev/api/v1/relations/CVE-2021-44228?depth=1&limit=25",
      "mermaid": "https://api.vulnpatch.dev/api/v1/relations/CVE-2021-44228?depth=1&limit=25&format=mermaid",
      "explorer": "https://cve.vulnpatch.dev/graph/CVE-2021-44228",
      "dossier": "https://api.vulnpatch.dev/api/v1/cve/CVE-2021-44228/dossier"
    }
  },
  "timestamp": "2026-09-28T12:00:00.000Z"
}

Nodes ​

TypeWhat it isReached from
cveA CVE recordAn advisory's alias list, a notice's upstream or fix, OSV for a package
advisoryAn advisory in the index (GHSA, RUSTSEC, PYSEC, ...)advisory_aliases
distribution_noticeA redistributor's notice (Debian, Ubuntu, Chainguard, ...)advisory_upstream, distribution_fixes or its source
packageA package an advisory names, or a recognized package ecosystem in a CVE recordadvisories.packages, dossier.canonical.affectedPackages
productA vendor or product identifier from a CVE record, not a package coordinatedossier.canonical.affectedPackages
cweA weakness classificationdossier.canonical.cwes
nixpkgs_issueA nixpkgs security issue on GitHubdossier.nixpkgs.trackerIssues
nixpkgs_attributeA nixpkgs attributeAn issue's attribute list, or a candidate mapping

OSV's own record for a CVE (an index row whose id is the CVE id, from the GIT or unmapped corpus) is folded into the CVE node, its summary and packages with it, rather than drawn as a second node aliasing the CVE.

A CVE node read with its dossier also carries state (REJECTED for a withdrawn record, which then has replaced_by edges to the records its rejection notice names), kev (true or false when CISA's catalogue was read, null when it was not) and exploitMaturity.

An advisory and a distribution notice share the advisory: id prefix. They are one kind of index row, typed by its source, so a row first met through a fix edge and later read with its source is one node rather than two.

Edges ​

TypeFromToSource
alias_ofadvisorycve or advisoryadvisory_aliases, advisories.aliases, dossier.canonical.aliases, osv.aliases
upstream_ofcvedistribution_notice or advisoryadvisory_upstream, osv.upstream
fixesdistribution_noticecvedistribution_fixes
affectscve or advisorypackage or productadvisories.packages, dossier.canonical.affectedPackages, osv.query, osv.affected
replaced_bycvecvedossier.canonical.rejection.consultIds
weaknesscvecwedossier.canonical.cwes
tracked_bycvenixpkgs_issuedossier.nixpkgs.trackerIssues
candidate_matchcvenixpkgs_attributedossier.nixpkgs.candidates
maps_tonixpkgs_issuenixpkgs_attributedossier.nixpkgs.trackerIssues.attributes

An affects edge carries fixedVersion when its source gives one.

For a package root, each OSV record is one node that affects the package and has an alias_of edge to every CVE it names, so an advisory naming two CVEs shows both.

candidate_match is derived from the dossier and has not been reviewed by the nixpkgs security team. In the explorer it uses a dashed orange line. maps_to comes only from the attributes listed on a linked security issue.

CWE nodes include the name supplied by the CVE record and a short definition from MITRE when available. Vulnpatch keeps definitions in KV without an expiry, then checks MITRE's catalogue version each time it builds a graph with CWE nodes. It refreshes changed definitions before returning them. If MITRE cannot confirm the current version, the graph shows the CVE's own label and a direct MITRE link, but not cached definition text. Those responses are not cached. API nodes carry MITRE's required attribution and licence notice.

Reading the caps ​

  • truncated: a cap cut the graph. expanded[].truncated says which node's relations were cut at limit; the 300-node and 600-edge ceilings set it too.
  • notExpanded: each node whose relations were not read, with why. A leaf type, beyond the depth, over the twelve-per-level cap or a source that could not be read.
  • coverage.dossier: per CVE expanded, cached, not_cached, unreadable, not_read (the eight-per-request cap) or no_store. A CVE whose dossier is not_cached has no weakness, package or nixpkgs edges in the graph until the dossier is built; read it, then ask again. Such a graph is served with Cache-Control: no-store, so the second ask sees the dossier's edges.
  • coverage.indexSources: each indexed corpus's row count, last load and completeness. Counts may overlap. latestIndexedAt is the newest load, not a claim that all sources are current or complete.
  • coverage.cweCatalog: MITRE version and check time, plus requested and enriched counts. A missing definition does not invalidate the graph.

Four answers, kept apart ​

StatusCodeMeaning
200The graph. partial: true means a source on a deeper level could not be read; failures names it and the response is not cached.
400NOT_A_RELATION_IDThe path segment names nothing a graph can start from. Nothing was read.
404NOT_INDEXEDNothing held knows the root: for an advisory, neither the index nor OSV. For a CVE, links.dossier builds the dossier, after which its weaknesses, packages and nixpkgs status join the graph.
503INDEX_UNAVAILABLEThe index could not be read for the root's own level. Says nothing about the identifier and is never cached.
503SOURCE_UNAVAILABLEOSV could not answer for a package root, or for an advisory root the index does not hold. Says nothing about the id.

Mermaid ​

GET /api/v1/relations/CVE-2021-44228?format=mermaid
graph LR
  n0(["CVE-2021-44228"]):::cve
  n1["GHSA-jfh8-c2jp-5v3q"]:::advisory
  n1 -- alias_of --> n0
  classDef cve stroke-width:2px
  %% not expanded n1: Beyond the requested depth.

A partial graph adds a %% partial: comment line for each source that could not be read. Node ids are positional and labels are entity-escaped, so nothing from an upstream reaches the syntax. Shapes distinguish the types: a stadium for a CVE, a rectangle for an advisory or notice, a subroutine box for a package, a hexagon for a weakness and a parallelogram for a nixpkgs issue or attribute.