Agent Support
Vulnpatch exposes CVE dossiers designed for agent runtimes. A dossier is a single JSON document that combines canonical CVE fields, source provenance, data-quality gaps, ingest status, and action hints.
CVE Dossier
GET /api/v1/cve/:id/dossierExample:
curl https://api.vulnpatch.dev/api/v1/cve/CVE-2024-3094/dossierBy default the response is the agent document, and nothing else:
canonical: normalized CVE fields for agent reasoning (id, severity, cvssScore, affectedPackages, epss, kev, availability).provenance: source names, source URLs, integrity hashes, and fetch timestamps.quality: missing fields, conflicts, grade (A–F), and completeness score.agentHints: recommended actions, known fixed versions, and booleans such asneedsIngest,needsTriage, anddataCompleteForAutomation.ingest: freshness metadata includinglastIngestedAt,sourcesChecked, and the canonical dossier URL.links: the human CVE page, API lookup, and dossier URL.
canonical.publishedAt and canonical.updatedAt are source-record timestamps, not the time Vulnpatch fetched the evidence. publishedAtSource and updatedAtSource identify which record supplied each timestamp; for CVE List v5, the publication time comes from cveMetadata.datePublished. The dossier's canonical.assigner gives the CVE Program organization ID and short name that assigned the CVE ID. This is not a claim that the assigner authored every source fact. The CNA container's providerMetadata.dateUpdated is a different field and must not be described as its publication time. ingest.lastIngestedAt is when Vulnpatch assembled its evidence.
For CVE-2021-44228 that is roughly 6 KB.
Requesting raw upstream data
Two heavier blocks are available on request:
sources: raw payloads from MITRE, NVD, OSV, GitHub Advisories, Nixpkgs, EPSS, and KEV.lookup: the full multi-source envelope from/api/v1/cve/:id, for drilling down without a second request.
GET /api/v1/cve/:id/dossier?include=sources
GET /api/v1/cve/:id/dossier?include=sources,lookup
GET /api/v1/cve/:id/dossier?include=allThese were previously returned by default, which made a single dossier ~195 KB for a well-covered CVE, about 50k tokens, of which the agent document was 3%. lookup also largely duplicates sources. Ask for them when you need to audit a specific upstream payload; otherwise the default is what you want.
An unrecognised include value returns 400 rather than being ignored, so a typo cannot silently cost you data you believed you had requested.
Availability
canonical.availability signals the publication state of the CVE using a stable enum. Agents should branch on this rather than parsing free-text.
| Value | Meaning |
|---|---|
published | At least one authority has usable data. |
partial | Some sources have data, but the canonical MITRE record is missing. |
reserved | The CVE id is reserved with MITRE but details have not been published. |
rejected | The CVE has been rejected by the numbering authority. Do not use it for remediation. |
withdrawn | The CVE record was withdrawn. |
awaiting_ingest | The id appears in the MITRE delta feed but enrichment has not landed yet. Treat like reserved; request ?fresh=true. |
A response header X-Dossier-Availability carries the same value for quick inspection.
Not found
If no authority has allocated the id, the dossier endpoint returns 404 with a structured body:
{
"success": false,
"error": "not_found",
"code": "CVE_UNKNOWN",
"hint": "No authority has allocated CVE-9999-99999. Verify the ID follows the CVE-YYYY-NNNNN+ format and check for typos."
}Branch on code; it is stable. The same contract is enforced on the raw lookup endpoint.
Agents should fetch the dossier before making remediation or triage decisions. The human-readable view of the same record is available at:
https://cve.vulnpatch.dev/CVE-2024-3094Requesting a fresh dossier
If agentHints.needsIngest is true, or the fetched data looks stale, append ?fresh=true to bypass cache and have the server reconcile the dossier from upstream sources before responding:
GET /api/v1/cve/:id/dossier?fresh=truecurl "https://api.vulnpatch.dev/api/v1/cve/CVE-2024-3094/dossier?fresh=true"?fresh=true is rate-limited and may be disabled for unauthenticated callers under load; treat the cached dossier as the normal path and ?fresh=true as the refresh hint.
What a dossier cannot yet tell you
Dossier v23 adds four fields, all additive; nothing existing changed.
agentHints.tasks: the questions an agent asks of a CVE (determine_affected_packages,determine_fixed_version,determine_exploitation,determine_nixpkgs_status,determine_exposure), eachready,partialorblocked, withrequiresnaming the input that would unblock it.determine_exposureis always blocked oninstalled_package_version: only you know what you run.agentHints.missingEvidence: each coverage gap as{code, fact, whyItMatters, howToObtain: {type, detail}}. Always present; an empty list means nothing is missing, not that nobody looked.agentHints.nextAction: one step from a closed set (refresh,map_package,wait_for_upstream,provide_inventory,none), with a reason and a priority.canonical.evidenceStatus: a grade and its basis for severity, the fixed version, KEV, EPSS and nixpkgs:confirmed(two independent sources agree),single_source,inferred,contradicted,not_observed(read, nothing there) orunknown(not read). There are no confidence numbers: nothing in the pipeline produces a calibrated probability.assessmentId: a hash of the CVE, the dossier version and each source's integrity hash. It names the inputs a dossier was built from, so two readings can be shown to rest on the same snapshots. It says nothing about what an agent concluded.
Dossier v24 adds canonical.aliases, the other ids the lookup's own sources file the CVE under, and links.aliases, which points at /api/v1/aliases/{id}. That endpoint is the reverse direction: an agent holding a GHSA, RUSTSEC or DSA id reads it to find the CVE, and so the dossier, by key.
The manifest at /.well-known/vulnpatch-agent.json (also /api/v1/agent-manifest) lists the operations with readOnly and requiresConfirmation, the field tiers (facts, inferences, recommendations) and these vocabularies, and every operation in it is a path in openapi.json.
Weakness, CISA ADP and the score's owner
Dossier v24 adds four canonical fields, all additive.
canonical.cwes: the weakness classifications, one entry per CWE id as{id, name, sources}. The sources arecve_list_v5(the CNA record),github,nvdandcisa_adp.nameis the wording a source gave, or null when only NVD (which publishes ids alone) classified the record.canonical.capecs: CAPEC attack patterns the CNA filed under its impacts, in the same shape. Most records carry none.canonical.ssvc: CISA's SSVC decision points from the record's ADP container:exploitation(none, poc, active),automatable(yes, no),technicalImpact(partial, total) and the decision's timestamp. Null when the record carries no CISA decision, which says nothing about exploitation.canonical.exploitMaturity: how far exploitation has been shown to go, as{level, basis, sourcesUnreached, note, kev, ssvcExploitation, exploitdb, metasploit, nuclei, githubPoc, scannedAt}.levelisactive(in CISA KEV, or CISA's SSVC decision says active),poc(an Exploit-DB entry, a Metasploit exploit module, a Nuclei template, a GitHub proof-of-concept repository or SSVC says poc),unreported(every source answered and none matched) orunknown(a source did not answer;sourcesUnreachednames it). The last two are different answers: never readunknownas no exploit. Each artefact block carries identifiers, names, dates, ranks and a link, capped at ten with the total stated, and is null when its source did not answer. A Metasploit auxiliary or post module is listed but does not make the levelpoc; only an exploit module is exploit code. Vulnpatch republishes no exploit code, template body or comment text; each exploit source appears inprovenancewith its licence (Exploit-DB GPL-2.0, Metasploit BSD-3-Clause, Nuclei MIT).canonical.cvssSource: whose scorecvssScoreis:cve_list_v5,github,nvd,osvorcisa_adp. The CNA's score comes first, then NVD's own analysis, then GitHub's, then CISA's as a fallback. NVD carries the CNA's and CISA's metrics beside its own; a metric it merely relays is attributed to the CNA or CISA, never counted as NVD's, so a CNA score is not "confirmed" by NVD's copy of it, and NVD's own disagreement with a CNA is aquality.conflictsentry.canonical.rejection: for a REJECTED record, the CNA's notice as{reason, date, consultIds}, withconsultIdsthe CVEs the notice says to read instead.canonical.descriptionis the notice,severityisunknownandcvssScoreis null whatever a secondary source still says; the sources keep their values. Null for a record that stands.canonical.tags: qualifiers the CNA or NVD put on the record, lowercased:disputed,unsupported-when-assigned,exclusively-hosted-service. Empty when none. Read before acting.provenance[].analysis(nvd entry): NVD's own status for the record.Received,Awaiting AnalysisandUndergoing Analysismean NVD holds the record but has added nothing of its own yet, andnvd_enrichmentstays among the coverage gaps;Deferredmeans it will not.
CISA's ADP container ("CISA-ADP" in containers.adp[]) classifies and scores records whose CNA did not. Its values never replace a CNA, GitHub or NVD value, and never override one silently:
- Its CWE is used when no other source classified the record, and shown as corroboration (
cisa_adpjoins the entry's sources) when it agrees with them. A CISA classification that shares nothing with the others is a{field: "cwe"}entry inquality.conflictscarrying each side's ids; it is not merged. A CWE disagreement does not withholddataCompleteForAutomation; only a severity disagreement does. - Its CVSS ranks after the CNA's, NVD's own and GitHub's. When the CNA has a score of its own and CISA's severity differs, the disagreement is a
{field: "severity"}conflict namingcisa_adp, andevidenceStatus.severityreadscontradicted. When CISA's score is the only one,evidenceStatus.severityissingle_sourcewith basiscisa_adp.
The lookup envelope (include=lookup) carries the container's CWE, CVSS and SSVC under primary.adp, and primary.severitySource says whether the CNA slot holds the CNA's score (cna) or CISA's (cisa_adp).
EPSS history
GET /api/v1/cve/:id/lifecycle carries epssHistory: the daily EPSS series behind the epss_spike phase, as {status, archivesRead, points, note}. points is oldest first, one per FIRST publication date, from the archived daily snapshots of the last 30 days. The archive holds the scores of CVEs with a nixpkgs security issue, so the status matters:
| Status | Meaning |
|---|---|
recorded | The snapshots held this CVE; points is its series. |
not_observed | Snapshots were read (archivesRead) and none held this CVE. Not evidence the score was steady. |
unavailable | The archive could not be read, or no snapshot from the last 30 days exists. |
No per-CVE history is stored beyond those snapshots. A series longer than 30 days, or for CVEs outside the archive, would need storage that does not exist yet.
Runtime Guidance
Recommended agent flow:
- Fetch
GET /api/v1/cve/:id/dossier. - Read
agentHints. IfneedsIngestis true, retry with?fresh=true. - Use
agentHints.knownFixedVersions,quality.conflicts, andprovenancebefore recommending remediation. - Link users back to
links.htmlso humans can inspect the same evidence.
Do not mutate canonical CVE data directly from model output. The agent requests a refresh; deterministic server code performs the source fetches, normalization, hashing, and storage.
Agent Tools
Agent runtimes that integrate with Vulnpatch expose two dossier-aware tools backed by the public dossier endpoint:
lookup_cve_dossier({ cve_id, fresh? }): returns the dossier JSON. Usefresh: trueto request a refresh.queue_cve_ingest({ cve_id }): signals that the dossier is stale or incomplete and requests reconciliation. Rate-limited per CVE.
Both tools validate cve_id against CVE-YYYY-NNNNN+ and return JSON identical in shape to the dossier endpoint.
Nixpkgs coverage
Every dossier carries a nixpkgs block. It is the one ecosystem no other CVE database reports on, and previously its absence was indistinguishable from irrelevance. A CVE already fixed in nixpkgs, one never tracked, and one with no nixpkgs relevance all rendered as silence.
"nixpkgs": {
"tracked": false,
"coverage": "no_nixpkgs_match",
"trackerIssues": [],
"searchUrl": "https://github.com/NixOS/nixpkgs/issues?q=CVE-2021-44228",
"note": "Searched and found nothing: no open nixpkgs security issue references this CVE, ..."
}coverage is one of:
| value | meaning |
|---|---|
adjudicated | the NixOS security team ruled on one or more suggestions for this CVE; see adjudications for the decision and its proposed package list |
tracked | a nixpkgs security issue references this CVE; see trackerIssues |
derived | attributes were derived from Repology or search.nixos.org; candidates, not confirmations |
carried_unresolved | nixpkgs packages the affected project, according to Vulnpatch's complete copy of Repology, but the package index has no attribute of that name; a likely match whose attribute is not yet identified, not an absence (mapping.carried lists the names and channels) |
no_nixpkgs_match | every lookup completed and nothing resolved to a nixpkgs attribute; mapping.repologyCopyAt says when the copy of Repology consulted was taken |
not_applicable | every affected product is part of Microsoft Windows or Apple's operating systems, which nixpkgs does not package; platform states the basis |
not_in_nixpkgs_index | neither live Repology nor a complete, recent copy of it could answer, and the nixpkgs package index has no package of these names; a package nixpkgs names differently would be missed |
not_attempted | no affected package could be looked up, so no mapping ran; evidence of nothing either way |
mapping_failed | the attribute derivation failed upstream; a lookup failure, not a finding |
not_evaluated | nixpkgs sources have not been consulted for this CVE yet |
Each entry in candidates carries versionVerdict. A version is compared with source ranges only after the candidate's upstream project identity is corroborated. status is in_affected_range, outside_affected_range (a source lists it as unaffected), outside_listed_ranges (outside every range listed, with no default stated, so not a clean bill) or unknown; source is cve_record, github, osv or nvd, with the range matched and, when known, fixedIn. It is about the version only: nixpkgs can apply a fix as a patch without changing the version. A version with a patch level the ranges do not use (glibc 2.40-224) is unknown, and git-hash ranges are never compared. Each candidate also carries identity: same_repository or same_site when a reference in the record points at the package homepage's repository or site, vendor_and_product when the homepage names the record's vendor and product, name_only when nothing shows the package is this CVE's project (nixpkgs rancher is the Rancher CLI, not the Rancher server), generic_name_only when the only shared name tokens are generic words such as gateway, and unknown without a homepage. identity.matched names informative shared tokens while identity.ignored names generic ones. Neither token list establishes identity. exposure.affected lists only corroborated candidates in an affected range; exposure.identityUnverified counts unverified candidates, whose versionVerdict is unknown with reason package identity not established. A derived candidate also listed in a rejected security-review suggestion moves from candidates to dismissedCandidates in the dossier. It remains auditable there, but must not be treated as an active mapping or exposure signal.
Each entry in trackerIssues carries package, packageSource and attributes. packageSource is issue_body when the security tracker listed the attribute in the issue, title when the name was read out of a title a person wrote, and null when the title named nothing. A title reading is a guess about a package, not an attribute; attributes lists every attribute the tracker named, since an issue about python313Packages.foo usually names python314Packages.foo too.
securityReview says whether the security review's own API was asked about this CVE (consulted) and how it answered (outcomes). It is asked only when an issue or a candidate points at nixpkgs, so a no_nixpkgs_match result means the mapping found nothing and the review was not consulted; the note says so.
The derived note names the source the candidates came from: Repology, the Repology mirror Vulnpatch keeps, or the search.nixos.org package index. A candidate from the index is an exact pname match in the newest generation of each tracked channel; an attribute from another language's package set (a TeX package called go for a Go module) is left out when the record names the ecosystem, and an operating system named as a product is never resolved to an attribute at all.
Security review verdicts
The block also carries verdicts read from the public API of the NixOS security team's review. Its automatic matcher files suggestions and the security team accepts, rejects or publishes them; those human rulings outrank anything Vulnpatch derives.
adjudications: suggestions the team ruled on.verdictisadjudicated_affected(accepted or published) oradjudicated_not_affected(rejected). Each entry names the suggestion, its proposed package list and per-channel versions, and the review URL.trackerCandidates: the tracker's own machine matches still pending triage. These are candidates, the same evidence class ascandidatesderived from Repology, and are never presented as adjudications.
An adjudicated rejection is an answer about the suggestion, not a separate human review of every attribute in its packages object. For example, CVE-2026-88771 concerns Citrix NetScaler, while the dismissed suggestion lists many unrelated gateway packages. Do not turn those proposed names into affected-package findings, and do not treat the absence of a confirmed match as proof that no nixpkgs package could be affected. The CVE page collapses rejected suggestions and unverified name-only matches by default; the dossier retains both for audit. In this example, Gateway is a bare product name under the Citrix NetScaler scope. Its exact name lookup found JetBrains Gateway; project identity was name_only, and the security review's suggestion was rejected. Those records are kept as dismissed evidence, not surfaced as affected nixpkgs attributes.
no_tracker_issue is not a claim that nixpkgs is unaffected. A package may already be patched, or may never have been tracked. Agents should treat it as "no signal", follow searchUrl, and prefer coverage over inferring anything from an empty trackerIssues array.
Candidate attributes can originate from an exact package-name lookup in the nixpkgs index. That lookup establishes a matching name, not upstream project identity; inspect identity and the source before acting on it.
Why a fixed version is missing
fixedVersion: null was ambiguous, so each affected package now carries the reason alongside it:
{
"name": "org.xbib.elasticsearch:log4j",
"ecosystem": "maven",
"fixedVersion": null,
"fixedVersionStatus": "unspecified",
"fixedVersionNote": "The GitHub advisory did not provide a first patched version for this package.",
"source": "github"
}fixedVersionStatus is known or unspecified. There is deliberately no "no fix exists" value: no upstream source tells us that, so asserting it would be a guess. unspecified means the source named the package without naming a patched release. Look at another source; it does not mean the package is unfixable.
quality.missingFields gains nixpkgs_mapping when no nixpkgs data is present, and fix_versions when packages are known but not one carries a patched release, so both gaps are measurable rather than merely visible.
OSV ranges of type GIT are never used as a fixed version: their fixed event is a commit hash, not a release, and relaying it told agents to upgrade a package "to" a 40-character SHA. When only a GIT range reports a fix, the package stays unspecified and the patch commit is still reachable through references.patches.
dataCompleteForAutomation, formerly safeForAutomation
agentHints.safeForAutomation measured data completeness (no severity conflicts, packages known, official record present) but its name read as a grant of autonomy, which the evidence on automated patching does not support. It is renamed dataCompleteForAutomation to say what it measures. The old key ships alongside with the same value for one release and is deprecated: consumers should switch now, and must not treat either key as a judgment that automated remediation is safe.
KEV: not listed is an answer
canonical.kev is null both for CVEs not in the CISA KEV catalog and, a different thing entirely, when the catalog itself could not be consulted. The two are distinguished by quality.missingFields: kev_status appears there only when the catalog was unavailable. When the catalog was present and the CVE simply is not in it, kev is null and kev_status is absent from missingFields. That is a checked negative, as of the catalog release recorded in the lookup envelope's kevChecked block ({ catalogVersion, dateReleased }).
Source provenance history
Every raw upstream payload is retained, addressed by the SHA-256 of its canonical form. Storage is written only when content actually changes, so a quiet upstream on the thirty-minute refresh costs nothing.
GET /api/v1/cve/:id/provenance{
"cveId": "CVE-2021-44228",
"changes": [
{
"source": "github",
"from": "aaa…", "to": "bbb…",
"at": "2026-08-05T00:00:00Z",
"added": [], "removed": ["vulnerabilities"], "changed": ["severity"],
"regressed": true,
"regressionReason": "payload lost 1 field(s) present in the previous version: vulnerabilities"
}
],
"summary": { "totalChanges": 2, "regressions": 1, "sources": ["github"] }
}regressed is the entry worth noticing: a source that dropped fields it previously reported is usually an upstream failure rather than a correction. That is the signal behind the dossier refusing to replace a complete cached document with a thinner rebuild.
Fetch any retained payload by content hash to see exactly what a source said:
GET /api/v1/cve/:id/provenance?source=github&hash=<hash>Payloads are retained for 90 days; change-log entries outlive them, so an entry may reference a pruned snapshot and return 404 SNAPSHOT_UNKNOWN. The comparison is field-level, not character-level, and ignores key and array ordering, since upstreams reorder freely and that is not a change.
OSV export
OSV.dev has no Nix or nixpkgs ecosystem, so Vulnpatch builds and publishes its own nixpkgs feed in OSV format. See nixpkgs in OSV format for the record shape, the evidence behind each entry and how to read the coverage fields.
GET /api/v1/cve/:id/osv
GET /api/v1/feed/osv/nixpkgs