New CVEs likely affecting nixpkgs
CVE records queued for mapping to nixpkgs attributes. The list is a bounded, non-exhaustive set of corroborated project matches; it does not establish that a package is vulnerable. When a nixpkgs security issue is filed later, the entry records how many hours elapsed after our mapping. When a nixpkgs pull request fixes the CVE, with or without an issue, the entry records that too.
GET /api/v1/nix/ahead
GET /api/v1/nix/ahead?tracked=no
GET /api/v1/nix/ahead?fixed=no
GET /api/v1/nix/ahead?inRange=1The same list is on cve.vulnpatch.dev/signal/nix.
How it is built
Every five minutes the delta poll reads CVE records from the CVE Program and queues those with affected products. The same poll maps queued records. It reads every KV queue page (up to 20 pages), processes oldest records first, and maps for as long as its 35-second budget lasts, up to 60 records a run. One mapping may hold the run for at most 12 seconds; past that it is retried on a later run, so a slow package lookup cannot take the time the records behind it need. Queue records have a three-day TTL; a surge can still cause incomplete coverage. coverage.queue exposes the observed backlog, age, and whether the scan was complete. coverage.processing.lastRun is the last run as it went: how many it mapped, how long it took, and what ended it (drained, cap or budget), beside the budget and cap it was sized for, so a run that stops short of its capacity is visible rather than inferred. Failure after three attempts is counted in coverage.processing.failed and retained for 30 days for audit, not silently classified as a non-match.
The list itself holds the 400 most recently mapped entries, by first mapping time, for at most 30 days; at the volume of mid-October 2026 (100 to 150 kept a day) that is under three days, and coverage.window says what it actually spans rather than the retention it could allow. An entry the cap lets go is counted in coverage.window.evicted, one retention ages out in coverage.window.agedOut. KV emits no event when a queued record's TTL lapses, so each run snapshots the pending queue with each key's expiration, and a key gone from the next complete listing after its expiration is counted in coverage.window.expiredUnmapped (a key gone before then was mapped or is listing latency, and is never counted). expiresWithin24h still warns ahead of time.
Each of those events, and every entry the list keeps, is also written to a per-CVE record outside the list; see What is remembered. coverage.backlog is the queue as the last run assessed it: healthy, growing (the oldest queued record past six hours, or the queue deepening across the last twelve runs with every run ending on its budget or cap) or stalled (a record waiting over a day, or a record lost to expiry on this run), with reason, pending, oldestAgeHours and mappedPerRun over those runs, which coverage.processing.recentRuns lists. A stalled queue makes the nixpkgs_ahead scheduler task report degraded, which /health carries as a failed task -- a record lost to expiry while queued is never allowed to resolve unseen.
For each queued record:
- The record's affected products go through the package mapper, which asks the search.nixos.org package index and a complete copy of Repology. A product the CNA wrote as "Name (identifier)" is also looked up by the identifier.
- Each package found is checked against the record: it is listed only when a reference in the record names the package homepage's repository or site, or the homepage names the record's vendor and product. Names are shared: nixpkgs
rancheris the Rancher command-line tool, not the Rancher server a CVE was about. A match by name alone is counted innameOnlySkippedand not listed; the CVE dossier keeps it as an unverified discovery lead, with no version comparison. Generic words such asgatewayare recorded as ignored identity evidence, not treated as a project match. - A corroborated match is then checked against the nixpkgs security review's own verdicts (
tracker.security.nixos.org, the same source/cve/:idreads), in two shapes. Ordinarily a rejected suggestion names the specific attributes it bundled and rejected together (packages), and only those are dismissed. A suggestion can also reject a CVE withpackagesempty, meaning the tracker's own matcher proposed no nixpkgs candidate at all and a human confirmed that (rejection_reason: "no_matches"); that dismisses every candidate for the CVE, named or not, since nothing was ever named to key a per-attribute join on. Either way the CVE is counted intrackerDismissedSkippedand not listed: this feed exists to surface what nixpkgs has not yet looked at, and relisting something already dismissed would misrepresent it as open. - Only after project identity is corroborated is each channel's version compared with the record's affected ranges (text constraints, version and lessThan ranges, status changes within a range, and the kernel's per-series form). See
versionVerdictin Agent Support. - A record whose products map to no attribute is not listed. One whose lookup failed upstream is retried on later runs.
Every half hour, and at once when a CVE is newly added, the stored list of nixpkgs security issues is read, and any listed CVE that now has an issue gets tracker and leadHours. trackerCheckedAt on an entry says when it was last checked; an entry without it has not been checked yet. trackerReadAt is when that last happened and trackerListedAt when the stored list was itself fetched from GitHub, so a match is only as recent as the latter. The list is never crawled from here.
trackerReadError is present only when the most recent attempt to read the stored issue list failed or found it empty -- not simply when a check is not yet due. trackerReadAt and trackerListedAt are then from the last success, not a recent check, and should be read as stale rather than fresh. trackerMatchedNone is present and true when the most recent read succeeded against a non-empty issue list but matched none of the entries awaiting one -- plausible on a quiet run, but the same shape a wrongly-read or truncated issue list would produce, so worth a second look if it persists across several checks in a row. Neither field appears when the last check succeeded and found something, or found nothing to look for.
The tracker is not the only way nixpkgs answers a CVE. A fix can reach nixpkgs as a merged pull request carried by a channel with no security issue ever filed for it; to the tracker check alone that reads as untouched. So each run also checks a few listed entries against the merged nixpkgs pull requests the dossier assembles (the same evidence as nixpkgs.fixEvidence on /cve/:id/dossier). An entry is first checked an hour after mapping, and again every twelve hours until a merged fix is carried by a channel, which is final. At most four entries are checked per run, under a six-second budget of the step's own inside the 35-second run, and only when the worker holds a GitHub token: one check is about six GitHub calls, so four every five minutes is about fifty search calls and under three hundred core calls an hour. The step is reported in coverage.processing.lastRun.fixEvidence apart from the mapping loop, with what ended it: none_due, drained, cap, budget, error (GitHub unreadable, so the remaining entries wait) or no_token.
What a check finds is recorded on the entry as fix. fix.merged true with channels is a merged pull request on that channel's release branch, and carriedByChannel says whether the channel's observed revision has it yet (null when that could not be checked). fix.merged true with no channels is a merge onto a branch no channel builds from, such as staging. fix.merged false is a pull request open and not merged: nixpkgs has answered, the fix has not shipped. A pull request whose title names another package is not taken. fixCheckedAt says when the entry was last checked; an entry with fix null and fixCheckedAt set was checked and nothing was found, and one without fixCheckedAt has not been checked yet. fixCheckFailedAt marks a check GitHub could not answer; it is tried again after an hour.
An entry with a merged-but-not-yet-channel-carried fix stays listed, marked, the same way a tracker-filed entry does: the lead over the tracker is still the measurement. Once a fix is both merged and carried by a channel, it is final and is evicted 48 hours later -- well before the general 30-day retention window -- since nothing further about it will change. fixed=no is the open exposure, entries no merged pull request is known to fix.
publishedAt is the CVE List v5 publication time, not necessarily first public disclosure. publicAdvisory is populated only when the CVE record references a GitHub security advisory and its API confirms this CVE ID, or when the CNA supplies a datePublic. Repository advisory dates are read from the repository advisory API, not the later GitHub global advisory database. mappedAt is when Vulnpatch first mapped the record, and a later re-queue of the same record (a CNA's update) never resets it; remappedAt, when present, is the latest mapping. leadHours compares that first mapping with tracker issue filing, not first public disclosure, and is negative when the issue came first. A null publicAdvisory means no independently dated advisory was obtained, not that the CVE was first disclosed on publishedAt.
What is remembered
A CVE queued for mapping can be lost before it is ever listed: the pending queue has a three-day TTL, and the display list itself is capped and cuts entries by publication date. Without a separate record of this, a later re-queue (a CNA update to the CVE) is mapped as if for the first time, measuring its lead over nixpkgs security from the wrong starting point -- or losing the fact that it was ever queued at all.
Every CVE the feed keeps, lets go or loses now has a record of its own in the backend, nixpkgs_ahead_seen:<cveId>, which neither the cap nor the queue TTL touches: the first mappedAt, queuedAt, publishedAt, the tracker and fix facts as last known with leadHours and trackerFirst (whether nixpkgs filed before the first mapping), the candidate attribute names, and a lifecycle status with statusAt: active (listed), evicted_after_mapping (the cap let it go), aged_out (retention) or expired_unmapped (queued, never mapped, with expiry.queuedAt and expiry.expiredAt). A re-queue consults it: a listed CVE is not mapped again; one the list let go is mapped again under its original mappedAt with its tracker and fix facts carried, and stays listed only if that mapping is recent enough for the window; one that expired unmapped is mapped for the first time with the expiry kept on the record. Every re-queue of a CVE the record already knows, on any of those paths, adds one to requeues and sets requeuedAt. A mapping that finds no corroborated candidate leaves no record the first time, since nothing was lost and the mapping is reproducible from the CVE record; a re-mapping of a known CVE that finds nothing notes lastOutcome on it.
Records are written once a run has saved the list itself, so a list write that fails leaves no record saying active for an entry that was never kept. A record whose own write fails is carried in the list's stored state and written on the next run, up to 1,000 of them; any beyond that are counted in coverage.processing.memoryDropped.
Records written after the fact
One more status, lost_before_memory, belongs to no run. It marks a record written by hand for a CVE the feed held and lost before the records began, when the loss left no trace of which fate it was. Such a record states what is known and nothing more: backfill says when it was composed (recordedAt), when Vulnpatch's ingest first saw the CVE (ingestSeenAt), which fates the evidence leaves open (possibleStatuses, none of them asserted) and, in note, the facts and how they are known. Every field the record cannot know is null, statusAt, mappedAt, leadHours and trackerFirst among them, and requeues is null rather than 0 until a re-queue is counted, since earlier ones went uncounted. backfill is null on every other record.
If a CNA update queues such a CVE again, it is mapped as for the first time and the backfill stays on its record beside the new facts.
coverage.window and coverage.processing count these records. Each one is served by CVE id. The values below are illustrative, not a real record:
GET /api/v1/nix/ahead/CVE-2099-00001{
"success": true,
"data": {
"cveId": "CVE-2099-00001",
"status": "evicted_after_mapping",
"statusAt": "2026-10-20T12:00:00.000Z",
"publishedAt": "2026-10-15T00:06:43.000Z",
"queuedAt": "2026-10-15T00:10:00.000Z",
"mappedAt": "2026-10-15T00:20:00.000Z",
"lastMappedAt": "2026-10-15T00:20:00.000Z",
"severity": "medium",
"inAffectedRange": 2,
"candidateCount": 2,
"attrs": ["examplepkg"],
"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,
"requeueExpiredAt": null,
"lastOutcome": "listed",
"updatedAt": "2026-10-20T12:00:00.000Z",
"backfill": null,
"listed": false,
"recordsSince": "2026-10-14T20:09:10.614Z",
"method": "..."
},
"timestamp": "2026-10-20T12:05:00.000Z"
}Every field is present, null when the record does not hold it, and the same holds inside tracker, fix.pr, expiry and backfill. listed is status being active.
mappedAtis the first mapping that listed the CVE, which no re-queue resets. A CVE that expired unmapped has it null and its loss inexpiry; a later loss of the same CVE replacesexpiry, so it holds the latest.lastMappedAtis the latest completed mapping, listed or not, andlastOutcomewhat it found:listed,not_mappedordismissed_by_tracker.failedis a lookup that failed upstream, which is not a mapping and does not movelastMappedAt.- The record is as of
updatedAt. While the CVE is listed,GET /api/v1/nix/aheadis the current view; after it leaves the list,trackerandfixare as last known and are not rechecked.
The record is a fixed set of fields whatever a CVE's history: re-queues are counted in requeues, with only the latest requeuedAt and requeueExpiredAt kept, and attrs holds at most 12 names, so there is nothing to page.
The response is built from a fixed list of fields, not the stored record, so a field the poll adds to its record for its own use is not published by accident; the nested tracker, fix and expiry objects are filtered the same way. Nothing in it is private: the dates, the issue and the pull request are all public facts, and the storage key is not exposed.
| Status | Code | Meaning |
|---|---|---|
| 200 | The record | |
| 400 | BAD_CVE_ID | Not a CVE id. Case does not matter |
| 404 | NOT_REMEMBERED | The feed has kept no record of this CVE since recordsSince (2026-10-14T20:09 UTC): it named no affected product or matched no nixpkgs package with corroborated identity, or awaits mapping. It says nothing about whether nixpkgs is affected; the CVE dossier has the evidence |
| 503 | STORE_UNAVAILABLE | The record could not be read, which is not the same as no record |
The CVE dossier (GET /api/v1/cve/{id}/dossier) carries a summary of the same record as nixpkgsAhead, read when the dossier is served: the lifecycle, tracker, lead and fix fields, without those the dossier already states elsewhere, and a record link back here. It is absent when there is no record and { "unavailable": true } when the record could not be read. See agent support.
What it is not
Candidates, not confirmations. A version inside an affected range can still carry the fix as a nixpkgs patch without a new version, and a mapping by package name can be wrong for a package that shares its name with another project. The CVE page for each entry shows the full record with every source.
Parameters
| Parameter | Values | Meaning |
|---|---|---|
tracked | all (default), no, yes | Only the CVEs without a nixpkgs security issue, or only those with one |
fixed | all (default), no, yes | Only the CVEs no merged nixpkgs pull request is known to fix, or only those one fixes. An open pull request does not count as fixed |
inRange | 1 | Only the CVEs with at least one channel version inside the affected ranges |
limit | integer, 1-400 | Caps the number of entries returned. Omitted means no cap |
Response
{
"success": true,
"data": {
"entries": [
{
"cveId": "CVE-2026-53493",
"publishedAt": "2026-09-25T00:06:43.298Z",
"publishedAtSource": "cve_list_v5",
"publicAdvisory": null,
"mappedAt": "2026-09-28T12:05:00.000Z",
"severity": "medium",
"summary": "containerd is an open-source container runtime...",
"candidates": [
{ "identity": "vendor_and_product", "attr": "containerd", "channel": "nixos-25.11", "version": "2.2.1", "derivedFrom": "containerd",
"verdict": { "status": "in_affected_range", "fixedIn": "2.2.9", "range": ">= 2.1.0, < 2.2.9" } }
],
"candidateCount": 1,
"candidatesTruncated": false,
"inAffectedRange": 1,
"tracker": null,
"fix": null
}
],
"counts": { "listed": 1, "untracked": 1, "inAffectedRange": 1, "trackedLater": 0, "trackedFirst": 0,
"fixed": 0, "fixProposed": 0, "unaddressed": 1, "fixChecked": 0 },
"updatedAt": "2026-09-28T12:05:00.000Z",
"trackerReadAt": "2026-09-28T12:05:00.000Z",
"coverage": {
"queue": { "pending": 0, "complete": true, "oldestQueuedAt": null, "observedAt": "2026-09-28T12:05:00.000Z", "atRisk": false },
"processing": { "mapped": 1, "kept": 1, "notMapped": 0, "failed": 0,
"expired": 0, "evicted": 0, "agedOut": 0, "requeued": 0, "expiryChecked": true,
"recentRuns": [{ "at": "2026-09-28T12:05:00.000Z", "mapped": 1, "kept": 1, "timedOut": 0, "endedBy": "drained", "durationMs": 1830, "pending": 0, "expired": 0 }],
"lastRun": { "at": "2026-09-28T12:05:00.000Z", "durationMs": 1830, "endedBy": "drained", "mapped": 1, "kept": 1, "retried": 0, "dropped": 0,
"failed": 0, "timedOut": 0, "expired": 0, "evicted": 0, "agedOut": 0, "requeued": 0, "remembered": 1, "memoryWriteFailures": 0,
"queueUnreadable": 0, "queueRemoveFailures": 0,
"budgetMs": 35000, "maxMappings": 60, "mappingTimeoutMs": 12000,
"fixEvidence": { "due": 0, "checked": 0, "found": 0, "merged": 0, "failed": 0, "timedOut": 0, "endedBy": "none_due", "durationMs": 0,
"budgetMs": 11000, "maxChecks": 4, "checkTimeoutMs": 5000 } } },
"backlog": { "state": "healthy", "reason": null, "pending": 0, "oldestAgeHours": 0, "mappedPerRun": 1, "pendingChange": null,
"runsConsidered": 1, "expiredLastRun": 0, "assessedAt": "2026-09-28T12:05:00.000Z" },
"retentionDays": 30,
"maxListedEntries": 400,
"window": { "oldestMappedAt": "2026-09-28T12:05:00.000Z", "newestMappedAt": "2026-09-28T12:05:00.000Z", "full": false,
"evicted": 0, "agedOut": 0, "expiredUnmapped": 0 },
"exhaustive": false,
"warning": null
},
"method": "..."
}
}leadHours appears once the tracker has filed: hours from our mapping to the issue, negative when the issue came first. fix, when set, is the nixpkgs pull request found for the CVE: { merged, pr: { number, url, title, mergedAt, baseBranch }, channels, carriedByChannel, foundAt }. counts.fixed is the entries a merged pull request fixes, counts.fixProposed those with a pull request open, counts.unaddressed those with neither an issue nor a pull request as of the last check, and counts.fixChecked how many have been checked at all. Each candidate's identity says how it was shown to be the CVE's project. candidateCount counts all corroborated package/channel rows for an entry; candidates returns at most 12, with affected-range rows first. candidatesTruncated says whether the sample is incomplete. The full CVE dossier remains the place to inspect all package evidence. The top-level counts describe only the returned, filtered entries, not all published CVEs. coverage.processing counters start when this version of the poll first runs; they are not all-time ingestion totals. coverage.queue.pending is a snapshot from the last poll, not a live count.
How the reference UI presents it
No field in the response changed for this; it is how cve.vulnpatch.dev/signal/nix orders the same entries, computed entirely client-side from mappedAt, inAffectedRange and each entry's standing (untracked, then answered, then fixed). The entries array itself carries no section marker.
The panel splits entries into two groups by age, rather than a single sort that tries to mean both "newest" and "most exposed" at once: an entry whose exposure happens to match an unrelated OSV-indexed advisory could otherwise sit at the top of a feed titled "New CVEs" for as long as nothing else beat it on exposure, however old it actually was.
- New this week:
mappedAtwithin the last seven days, sorted bymappedAtdescending,inAffectedRangeas the tiebreak. - Older than a week: everything else, sorted by standing first (nothing from nixpkgs yet, then an issue or pull request, then a merged fix), then
inAffectedRangedescending. Each row also shows how many days it has sat there, computed asnow - mappedAt. - nixpkgs filed first: entries with
leadHoursat or below zero, taken out of both groups above whatever theirmappedAt, since a tracker issue that predates the mapping is not a lead; sorted byinAffectedRangedescending, each row stating how long before the mapping the issue was filed.
The summary line above the groups reads coverage.window for what the list spans and what has left it, and shows coverage.backlog only when its state is not healthy.
A consumer building its own view is free to use the same fields differently; this describes the reference client's choice, not a contract.
| Status | Code | Meaning |
|---|---|---|
| 200 | The list | |
| 400 | BAD_FILTER | tracked or fixed is not one of all, no, yes; or limit is out of range |
| 503 | NOT_BUILT | No list yet; the poll builds it as new CVEs arrive. Never to be read as nothing new |
| 503 | STORE_UNAVAILABLE | The list could not be read |