Skip to content

Knowledge Graph API ​

The knowledge graph was a Neo4j-backed set of relationship queries across CVEs, packages and weaknesses. Its backend is not deployed. Relationship questions are answered by Relations (GET /api/v1/relations/{id}), which reads the search index and the dossier and needs no graph database. This page says exactly what the /graph routes answer today, so that a 503 from one of them is read as "not deployed" and never as "nothing found".

What answers today ​

One route answers, from OSV rather than a graph:

GET /api/v1/graph/package/:name/vulns?ecosystem=npm

Every other /graph route returns 503 with code: "GRAPH_UNAVAILABLE". The scheduler's graph_sync and graph_nvd_enrichment tasks are skipped on every run for the same reason, so there is no graph data anywhere to be stale.

RouteStatusWhere the same question is answered
GET /graph/package/:name/vulnsAnswers, from OSVOSV Vulnerabilities for every ecosystem at once
GET /graph/stats503 GRAPH_UNAVAILABLEStatistics
GET /graph/cve/:id/affected503 GRAPH_UNAVAILABLERelations affects edges, or canonical.affectedPackages in the CVE dossier
GET /graph/cve/:id/related503 GRAPH_UNAVAILABLERelations for the graph around the CVE, Aliases for its other ids
GET /graph/package/:name/transitive503 GRAPH_UNAVAILABLENot answered anywhere; SBOM Analysis scans a dependency list you supply
GET /graph/cwe/:id/cves503 GRAPH_UNAVAILABLEcanonical.cwes in the dossier gives the weaknesses of one CVE; the reverse lookup is not answered
GET /graph/packages/top, /graph/packages/search, /graph/sample503 GRAPH_UNAVAILABLESearch

The 503 body ​

json
{
  "success": false,
  "error": "The knowledge graph backend is not deployed, so this route cannot answer.",
  "code": "GRAPH_UNAVAILABLE",
  "hint": "Not a statement about the CVE, package or weakness asked for. ...",
  "links": {
    "relations": "https://api.vulnpatch.dev/api/v1/relations/{id}",
    "dossier": "https://api.vulnpatch.dev/api/v1/cve/{id}/dossier",
    "osv": "https://api.vulnpatch.dev/api/v1/osv/{package}"
  }
}

The response carries Cache-Control: no-store. A client that receives it has learnt nothing about the identifier it asked after.

Get CVEs for Package ​

Returns the vulnerabilities OSV.dev lists for a package in one ecosystem. The path keeps its /graph prefix because the package pages on cve.vulnpatch.dev call it; the answer comes from OSV, and the body says so in source.

GET /api/v1/graph/package/:name/vulns?ecosystem=:ecosystem

Parameters ​

ParameterInRequiredDescription
namepathYesPackage name as the ecosystem spells it (e.g. lodash)
ecosystemqueryYesOSV ecosystem (npm, PyPI, Go, crates.io, Debian, ...). OSV rejects a query without one, so the route answers 400 rather than sending it.

Response ​

json
{
  "success": true,
  "data": {
    "package": "lodash",
    "ecosystem": "npm",
    "source": "osv",
    "vulnerabilities": [
      {
        "cveId": "CVE-2021-23337",
        "advisoryId": "GHSA-35jh-r3h4-6jhm",
        "cveIds": ["CVE-2021-23337", "CVE-2026-4800"],
        "upstream": [],
        "severity": "high",
        "score": 7.2,
        "published": "2021-05-06T16:05:51Z",
        "description": "Command Injection in lodash"
      }
    ],
    "count": 1
  },
  "timestamp": "2026-09-28T00:12:41.020Z"
}

One row per OSV record. cveId is the CVE where OSV assigned one and the record's id otherwise; advisoryId is always the record's id, and cveIds lists every CVE it names. A record is left out only when every CVE it names is already on an earlier row, so a GHSA and a PYSEC for one CVE are one row, but an advisory that names a second CVE is kept. A downstream rebuild, such as Root's ROOT-OS-ALPINE-320-CVE-2024-47611, names its CVE only in upstream: it is not the CVE record, and cveId is its own id.

FieldDescription
cveIdThe CVE id where OSV has assigned one, otherwise the advisory id (GHSA-...)
severitylow, medium, high, critical or unknown, from the OSV severity block
scoreCVSS base score where OSV carries one, otherwise null
publishedOSV publication time
timestampWhen this answer was produced. OSV is queried live; nothing is cached on this route

Rows are ordered most recently published first. Fixed versions are not in this shape; the OSV Details route carries the affected ranges for one advisory.

Failures ​

StatuscodeMeaning
400ECOSYSTEM_REQUIREDNo ecosystem given. links.osv points at /osv/:package, which asks every ecosystem
500SOURCE_UNAVAILABLEOSV did not answer. Not a statement that the package is clean; retry
429Rate limited, as on every route

An empty vulnerabilities list with success: true means OSV lists nothing for that package in that ecosystem. A failure is never rendered as an empty list.

Where this stands ​

The graph queries were a visualisation experiment. The Neo4j instance behind them stopped being maintained and began failing, and the one route visitors reached, the package lookup, was moved to OSV so that package pages stopped reporting an outage as "no known vulnerabilities". The code for the remaining routes and for the scheduler tasks is still in the worker and would answer again if NEO4J_URI and NEO4J_PASSWORD were bound, but nothing populates that graph and its contents would start empty.

Relationship questions that the graph promised, and where they are answered now:

  • The graph around an identifier: Relations, read from the search index and the dossier, with the source of every edge
  • What does this CVE affect: canonical.affectedPackages in the dossier, from the CNA and NVD records
  • What weakness is this: canonical.cwes in the dossier, with the source of each
  • Other ids for the same advisory: Aliases
  • Transitive dependency risk: not answered. SBOM Analysis scans a dependency list you already hold