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, actionablePackages, affectedProducts, 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.
Package coordinates versus vendor/product evidence
canonical.affectedPackages merges every source's affected-entity data by name, including entities that name only a vendor and product (an NVD CPE applicability match, or a CNA's own bare vendor/product statement) with no package-manager coordinate at all. Log4Shell's own CVE List entry, for example, is "Apache Software Foundation / Apache Log4j2" — not a Maven coordinate — and NVD's CPE data for the same CVE names Siemens products (comos, mindsphere, siguard-dsa) that are not packages either. canonical.affectedPackages carries all of this as one undifferentiated list.
Two further fields classify the same underlying evidence and separate the two kinds:
canonical.actionablePackages: every entity that names a real package-manager or distribution coordinate — a GitHub/OSV ecosystem package, a tracker-listed nixpkgs attribute, or a CNApackageNamecorroborated by one of those for the same CVE. This is the list to read for "what should I actually upgrade."canonical.affectedProducts: every entity that is vendor/product evidence rather than a package, as a discriminated union onevidenceType:cpe_applicability(source: "nvd"): an NVD CPE applicability match, withvendor,product, and anapplicabilityarray of every CPE/version-range row NVD gave for that vendor+product (none discarded).vendor_statement(source: "cve_list_v5"): a CNA's own barevendor/productpair, with whatever version evidence the record stated directly underversionEvidence({version, basis}).unclassified_package_name: apackageNamethe record stated with no corroborating ecosystem-aware source (source: "cve_list_v5"); a nixpkgs security issue's title-guessed package name with no structurednixpkgsAttrsto confirm it (source: "nix"); or a package name resolved only through Vulnpatch's dependency graph with no cached dossier to corroborate it (source: "graph"), each with areason.
Every entry carries
bestAvailable:truemeans this CVE's own affected entities include zeroactionablePackagesrows, so this is the strongest identity/remediation evidence available for it — not that the CVE lacks evidence generally. Heartbleed (CVE-2014-0160) still has a description, severity, references and exploit signals;bestAvailablesays nothing about those.
Both fields are classified independently, per CVE: a dossier with real package coordinates (Log4Shell) still lists its Siemens CPE rows under affectedProducts, and a dossier with only vendor/product evidence (Heartbleed) lists actionablePackages as empty rather than promoting a Siemens or OpenSSL CPE row to a package for lack of anything better.
canonical.affectedPackages is deprecated in favor of canonical.actionablePackages. It is unchanged for any reader already consuming it — the same merge logic, including the vendor/product noise described above. New integrations should read canonical.actionablePackages for packages and canonical.affectedProducts for vendor/product evidence instead; affectedPackages will eventually stop carrying a vendor/product name as if it were a package.
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=allBoth are large: a well-covered CVE's full sources payload runs well past 100 KB, dwarfing the dossier's own agent-facing document. 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, or the CVE Program's own register confirms it is PUBLISHED, but enrichment has not landed yet. Treat like reserved; request ?fresh. |
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
agentHints.knownFixedVersions entries carry name (v53), the same field agentHints.affectedPackages uses for a package. package is kept alongside it, deprecated, for a consumer already reading the field this carried before v53; use name.
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, or SSVC says poc -- each a curated source that only ever lists an entry someone verified),referenced(only a GitHub repository matching this CVE's id alongside "exploit"/"poc" in its name, description or topics -- a text match GitHub's own search makes, not a review of the repository's contents, and not the same claim aspoc),unreported(every source answered and none matched) orunknown(a source did not answer;sourcesUnreachednames it).unreportedandunknownare different answers: never readunknownas no exploit.pocandreferencedare different answers for the same reason: a GitHub match earnspoconly when corroborated by a curated source, never on its own -- a repository named after a CVE id is not, by itself, evidence of a working exploit for it. 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, andseverity,cvssScore,cwes,capecs,ssvcandexploitMaturityare allunknownor empty/null whatever a secondary source still says — a GitHub PoC repo or an upstream CWE classification is a real fact about the code, not a fact about whether a withdrawn record is valid to act on.agentHints. dataCompleteForAutomationandsafeForAutomationarefalsefor a rejected record regardless of any package mapping made before the rejection. The sources keep their own 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
The dossier is reached through two read-only operations, both GET on the same endpoint: get_cve_dossier (plain) and refresh_cve_dossier (?fresh=true). Both validate the id against CVE-YYYY-NNNNN+ and return JSON identical in shape to the dossier endpoint.
These two are not the whole tool surface. The full, current list of operations -- each with its path, whether it is read-only, and the field-tier vocabulary it uses -- is served live and is the one to read rather than this page's own description of it: GET /api/v1/agent-manifest (also at /.well-known/vulnpatch-agent.json). That list includes the ingestion-control operations under /ops/v1/ingest/cve/{id} (POST to queue ingestion, POST .../refresh-source/{source}, POST .../repair, GET .../history) and /ops/v1/ingest/jobs (GET, and GET .../{jobId}) for an agent that needs to drive a refresh or inspect ingest job state directly, identified by the X-Agent-Name header convention, rather than only requesting one through ?fresh=true.
Nixpkgs coverage
Every dossier carries a nixpkgs block. It is the one ecosystem no other CVE database reports on. The block always states which of these is true: a CVE already fixed in nixpkgs, one never tracked there, or one with no nixpkgs relevance at all -- none of these are silent.
"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 lists the names and channels) |
no_nixpkgs_match | every eligible name was attempted and nothing resolved to a nixpkgs attribute; mapping says when the copy of Repology consulted was taken |
package_list_truncated | the resolver's per-CVE budget (four names) cut a longer affected-package list down before any lookup ran; mapping and mapping say how many were checked and how many eligible names never were. A gap, not a result — the untried names may be the ones that match |
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, nvd or upstream_commit, 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. Two statuses overrule the version comparison, which then sits under versionComparison: fixed_by_merged_pr (a merged nixpkgs pull request for this CVE on the candidate's channel; pr, carriedByChannel) and fixed_by_applied_patch (source: 'nixpkgs_patch'). The second means the package definition, read at the channel's observed revision (definitionPath), applies a patch for every upstream fix commit the CVE record references: by content (grade: 'fix_patch_content_match', every changed line of each commit present, and at least two distinctive lines) or fetched from each commit (fix_commit_referenced). note states what it does not establish: that those commits are the whole fix, which the record's references assert, and whether the flaw is reachable in the build.
A corroborated candidate still in_affected_range after the pull request check carries patchEvidence: {checked: true, grade, interpretation, revision, definitionPath, fixCoverage, evidence, gaps} when the definition was read, or {checked: false, reason} when it was not (at most three definitions are read per lookup, a background rebuild reads only answers already held, and a channel without an observed revision is not guessed). Only the two grades above change versionVerdict. partial_fix_present (some, not all, of the fix commits), cve_referenced_patch (a patch file names the CVE), no_evidence and unresolved change nothing, and none of them is evidence that the package is vulnerable: patches applied from another file, such as an override, are not seen. exposure.fixedByAppliedPatch lists the candidates fixed this way, apart from exposure.affected. A version with a patch level the ranges do not use (glibc 2.40-224) is unknown. A record whose only affected-version entry is a bare commit hash is never compared by version at all; when the record also names a fix on the same repository and nixpkgs' own fetchFromGitHub pin for the attribute can be read, the two commits are ordered against each other instead (source: 'upstream_commit', reason states which pin and which fix). That ordering itself reads unknown (never a guess) when the pin and the fix sit on separate lines of history, such as a backport or a cherry-pick, or when GitHub could not be asked; reason says which. Each candidate also carries identity: same_repository when a reference in the record and the package homepage name the same repository, same_site when a reference points at the package homepage's own domain and a non-generic word from the record's product name also appears in the package's own homepage URL, outside the bare domain name itself, 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. Without a homepage it is unknown. same_site_unconfirmed is a weaker, uncorroborated case: a reference shares the package homepage's domain, but nothing specific to the product appears in that homepage URL -- a domain can host more than one project under one owner (a company blog, a personal site, a forge with many repos at that host), so the bare domain alone is not treated as identity. 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. A bare product name in a CVE's scope (e.g. a generic term like "Gateway") can resolve by exact name lookup to an unrelated nixpkgs package with the same name; the suggestion listing that package is then rejected, but the rejection does not turn every proposed name in it into a confirmed affected-package finding, and it does not prove no nixpkgs package could be affected either. The CVE page collapses rejected suggestions and unverified name-only matches by default; the dossier retains both for audit. A bare-name match whose project identity is name_only and whose suggestion was rejected is kept as dismissed evidence, not surfaced as an affected nixpkgs attribute.
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.
Ahead of nixpkgs security issues
A dossier whose CVE the nixpkgs ahead feed has kept, let go or lost carries nixpkgsAhead, placed after nixpkgs: whether the feed mapped the CVE before a nixpkgs security issue was filed for it, and what became of that lead. It is a summary of the feed's record; record links the full one at GET /api/v1/nix/ahead/{cveId}.
"nixpkgsAhead": {
"status": "evicted_after_mapping",
"statusAt": "2026-10-20T12:00:00.000Z",
"listed": false,
"queuedAt": "2026-10-15T00:10:00.000Z",
"mappedAt": "2026-10-15T00:20:00.000Z",
"lastMappedAt": "2026-10-15T00:20:00.000Z",
"lastOutcome": "listed",
"tracker": { "number": 1, "url": "https://github.com/NixOS/nixpkgs/issues/1", "filedAt": "2026-10-16T00:20:00Z" },
"trackerCheckedAt": "2026-10-16T00:30:00.000Z",
"leadHours": 24,
"trackerFirst": false,
"fix": null,
"fixCheckedAt": "2026-10-16T02:00:00.000Z",
"expiry": null,
"requeues": 0,
"requeuedAt": null,
"updatedAt": "2026-10-20T12:00:00.000Z",
"backfill": null,
"record": "https://api.vulnpatch.dev/api/v1/nix/ahead/CVE-2099-00001",
"note": "..."
}The values are illustrative. Each field means what it means on the full record, and every one is present, null when the record does not hold it. The full record's publishedAt and severity are left out because canonical states them, and its attrs, candidateCount and inAffectedRange because they are the feed's own mapping at the time, which the nixpkgs block answers from fuller evidence.
status is one of active, evicted_after_mapping, aged_out, expired_unmapped or lost_before_memory. The last marks a record written by hand after the fact, for a CVE the feed held and lost before its records began by a fate that left no trace. Its backfill states what is known and which fates remain possible, without asserting one, and the fields it cannot know, including mappedAt, leadHours and requeues, are null. backfill is null on every record the feed wrote itself. See records written after the fact.
- Absent means the feed holds no record of the CVE. It keeps one only for a CVE it listed or lost since 2026-10-14T20:09 UTC, a few percent of new CVEs and none older than the feed, so the field is absent from nearly every dossier. Absence says nothing about whether nixpkgs is affected.
{ "unavailable": true, "record": ..., "note": ... }means the record could not be read for this response. That is not absence; fetchrecordor retry.
The field is read when the dossier is served, not when it was built: the cached document lives for hours and the record changes every five-minute run, so an X-Cache: HIT dossier still carries the current record, as of its updatedAt and up to about a minute behind it at the edge. For the same reason it is not covered by assessmentId, and POST /ops/v1/ingest/cve/:id returns the document without 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
agentHints.dataCompleteForAutomation measures data completeness: no severity conflicts, packages known, an official record present. It is not a grant of autonomy to act on a CVE -- only a statement that the data itself is complete enough to. agentHints.safeForAutomation carries the identical value as a deprecated alias of the same field. 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.
An incomplete nixpkgs security-tracker crawl sets both fields to false. The dossier's nix provenance remains checked: true because the source was consulted, adds partial: true, and exposes nixpkgs_mapping in quality.coverageGaps; an absent tracker issue is not a checked negative.
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/nixpkgsSBOM scanning
An agent auditing a project's dependencies can post a CycloneDX or SPDX JSON document directly; no credential is required. See SBOM analysis for the request and response shape, the per-field coverage meanings, and the component and payload caps.
POST /api/v1/sbom/scan
POST /api/v1/sbom/time-travelRead summary.complete before reading vulnerableComponents: a false value means some of the document went unexamined (failedComponents, unsupportedComponents, noPurlComponents, or notAnalyzed), and a zero-vulnerability answer from a partial scan is not a clean bill of health.
A finding carries producerAnalysis when the uploaded document's own CycloneDX vulnerabilities[] already states an analysis for that CVE (not_affected, resolved, exploitable, and the rest). This is the document producer's own claim, surfaced for a reader to check against the finding; it never suppresses or filters a finding on its own. Treat a not_affected claim as something to verify, not as grounds to skip the CVE.
summary.minimumElementsGaps names which CISA SBOM minimum-elements fields (hashes, licenses, supplier, tools, subject) the document lacks entirely. Informational only; never a reason a scan is degraded or blocked.
sbom/time-travel answers what was known about the document's components as of a past date, including the fix-date caveat in the response fields.