Skip to content

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 ​

http
GET /api/v1/cve/:id/dossier

Example:

bash
curl https://api.vulnpatch.dev/api/v1/cve/CVE-2024-3094/dossier

By 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 as needsIngest, needsTriage, and dataCompleteForAutomation.
  • ingest: freshness metadata including lastIngestedAt, 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=all

These 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.

ValueMeaning
publishedAt least one authority has usable data.
partialSome sources have data, but the canonical MITRE record is missing.
reservedThe CVE id is reserved with MITRE but details have not been published.
rejectedThe CVE has been rejected by the numbering authority. Do not use it for remediation.
withdrawnThe CVE record was withdrawn.
awaiting_ingestThe 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:

json
{
  "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:

text
https://cve.vulnpatch.dev/CVE-2024-3094

Requesting 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:

http
GET /api/v1/cve/:id/dossier?fresh=true
bash
curl "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), each ready, partial or blocked, with requires naming the input that would unblock it. determine_exposure is always blocked on installed_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) or unknown (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 are cve_list_v5 (the CNA record), github, nvd and cisa_adp. name is 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}. level is active (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) or unknown (a source did not answer; sourcesUnreached names it). The last two are different answers: never read unknown as 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 level poc; only an exploit module is exploit code. Vulnpatch republishes no exploit code, template body or comment text; each exploit source appears in provenance with its licence (Exploit-DB GPL-2.0, Metasploit BSD-3-Clause, Nuclei MIT).
  • canonical.cvssSource: whose score cvssScore is: cve_list_v5, github, nvd, osv or cisa_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 a quality.conflicts entry.
  • canonical.rejection: for a REJECTED record, the CNA's notice as {reason, date, consultIds}, with consultIds the CVEs the notice says to read instead. canonical.description is the notice, severity is unknown and cvssScore is 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 Analysis and Undergoing Analysis mean NVD holds the record but has added nothing of its own yet, and nvd_enrichment stays among the coverage gaps; Deferred means 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_adp joins the entry's sources) when it agrees with them. A CISA classification that shares nothing with the others is a {field: "cwe"} entry in quality.conflicts carrying each side's ids; it is not merged. A CWE disagreement does not withhold dataCompleteForAutomation; 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 naming cisa_adp, and evidenceStatus.severity reads contradicted. When CISA's score is the only one, evidenceStatus.severity is single_source with basis cisa_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:

StatusMeaning
recordedThe snapshots held this CVE; points is its series.
not_observedSnapshots were read (archivesRead) and none held this CVE. Not evidence the score was steady.
unavailableThe 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:

  1. Fetch GET /api/v1/cve/:id/dossier.
  2. Read agentHints. If needsIngest is true, retry with ?fresh=true.
  3. Use agentHints.knownFixedVersions, quality.conflicts, and provenance before recommending remediation.
  4. Link users back to links.html so 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. Use fresh: true to 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.

json
"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:

valuemeaning
adjudicatedthe NixOS security team ruled on one or more suggestions for this CVE; see adjudications for the decision and its proposed package list
trackeda nixpkgs security issue references this CVE; see trackerIssues
derivedattributes were derived from Repology or search.nixos.org; candidates, not confirmations
carried_unresolvednixpkgs 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_matchevery lookup completed and nothing resolved to a nixpkgs attribute; mapping.repologyCopyAt says when the copy of Repology consulted was taken
not_applicableevery affected product is part of Microsoft Windows or Apple's operating systems, which nixpkgs does not package; platform states the basis
not_in_nixpkgs_indexneither 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_attemptedno affected package could be looked up, so no mapping ran; evidence of nothing either way
mapping_failedthe attribute derivation failed upstream; a lookup failure, not a finding
not_evaluatednixpkgs 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. verdict is adjudicated_affected (accepted or published) or adjudicated_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 as candidates derived 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:

json
{
  "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
json
{
  "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.

http
GET /api/v1/cve/:id/osv
GET /api/v1/feed/osv/nixpkgs