Skip to content

How it works

This is the page for checking our work: we read a security record as it read on the day it was published, read it again as it reads today, and report every difference. Four thresholds are choices rather than measurements and are named below; everything else is counted, and you can repeat any one finding yourself in about a minute.

0.43%

of comparable CVE records moved their fix version after publication.

228 records of the 53,461 comparable ones, in the 2026-09-07 snapshot. How this is counted ↓

This is the share that has moved so far, and records published recently have had less time to be edited. Following each record only for as long as it has been watched, an estimated 0.42% move within 90 days of publication and 0.47% within a year. Of the 26,257 records watched for a full year, 0.27% moved within it.

Every figure on this site is counted from one analysis, computed 2026-09-07. It does not change between builds, so a record edited since is not reflected here. Figures on this page last changed 2026-09-05, when the method did (what changed).

Check one yourself

Whether you have to take any of this on trust. You do not.

All three catalogs are public, and every finding names the exact upstream commit it came from, printed on the record's own page and in the observed_commit column of the downloadable file.

The commit a finding names is the commit that MADE the change, so it holds the state AFTER it. Read it against the record's first commit, which is its state at publication, or against the named commit's parent for the state immediately before the edit. Comparing the named commit against HEAD compares the after-state with itself and shows nothing.

git clone --single-branch https://github.com/CVEProject/cvelistV5.git
cd cvelistV5
P=cves/2026/14xxx/CVE-2026-14678.json
F=.containers.cna.affected

# as published, the record's first commit
git show "$(git log --reverse --format=%H -- "$P" | head -1):$P" | jq "$F"

# immediately before the change (the named commit's parent)
git show "<commit>^:$P" | jq "$F"

# today
git show "HEAD:$P" | jq "$F"

One exception: the older fix-version file keeps the thirteen columns it has always had, and none of them is the commit. Its header names the one catalog commit the whole file was computed at, and /api/v1/cve/{id} returns the commit behind every change to a record.

If we have a finding wrong, that is where it shows. A dispute reduces to a commit pair, which is the only kind of argument worth having about a number like this. KEV findings verify the same way against the KEV mirror, our own copy of CISA's list kept in Git, whose rows name a commit in that repository rather than in the CVE catalog.

What a personal check establishes

How record comparison, finding lookup and advisory aliases differ, and why an absent finding is never an asset assurance.

A personal check looks up the submitted identifiers in the shipped finding rows. CVE records, CISA catalog entries and GitHub advisories have different source windows. A match in one source does not establish comparison coverage in another.

The CVE comparison index records whether paired published and later records were read and selected eligible fields examined. “Compared” refers only to the listed fields; it does not assert that the entire record stayed unchanged, or that every release branch was comparable. “Unknown” means no comparison membership was established. “Out of scope” means the CVE ID is outside the configured years. “Uncomparable” means the available record pair could not support the configured comparisons. Older snapshots without an index return unknown coverage.

Advisories can be found through the CVE IDs they name or through a direct GHSA ID lookup. An advisory is an entry in the GitHub Advisory Database, the GHSA page a Dependabot alert links to. It is reached from a CVE id only when the advisory names that CVE id itself, and many advisories name none. So no advisory here means none was found among the advisories that name one of these ids. It does not mean there is nothing to find.

An advisory reached by its own ID is looked up directly. Its CVE aliases, when available, also enter the separate CVE record checks. The advisory dataset ships findings, not a membership index of every unchanged advisory. No matched advisory row can mean no selected change, an advisory outside this analysis, an unknown ID, or an unsupported comparison. It does not establish that the advisory stayed unchanged, nor that the ID is wrong.

One advisory can name several packages and release branches. A version beside a GHSA ID is explicitly refused because it does not identify the package to compare. The check API accepts supported ecosystem:package@version coordinates and returns a separate verdict for each advisory, package and release branch. Incomplete package results remain incomplete; an uncomparable branch receives no version verdict.

A first fixed boundary and a last affected boundary are different claims. A position between published boundaries is a comparison of stated versions, not evidence of the version installed on an asset. No result establishes that a system is exposed, unaffected or safely patched. Refused rows remain outside the counted findings, and source windows and truncated results remain limits on absence.

What counts as a change

Which edits we look for, in which unit each is counted, and why two tables are never added together.

Every finding compares published source values. The evidence identifies what changed and when it appeared in the source history. It does not establish a user’s decisions or a system’s exposure.

We watch 15 kinds of change: 11 to a CVE record or a CISA KEV entry, and 4 to a GitHub advisory. We have found at least one edit of 14 of those kinds in what we have read so far. A page that shows fewer than 15 is showing only the kinds we have found something in, and says so beside the figure.

Two tables below, because there are two units and they are never added. A CVE record is the entry in the CVE Program's catalog that a CVE id points at. A GitHub advisory is the GHSA page a Dependabot alert links to. One row is one change to one named thing: a record and one of its products, or an advisory and one of its packages.

Rows, not records: a moved fix version and an added product are counted once per record and per product, every other CVE and KEV kind once per record, and the advisory kinds once per advisory or per advisory and package. The table of every kind gives both figures and never adds them.

Four short names run through this site. A CVE is the numbered public entry for one security flaw. CVSS is the 0 to 10 severity score a record carries, and the bands it is read in, Low through Critical. KEV is CISA's list of the flaws it knows are being attacked. GHSA is the id GitHub gives an advisory in its own database. The publisher under a vendor is its CNA, the CVE Numbering Authority: the organisation that writes and edits the CVE record, in the short code the CVE Program assigns it: cisa-cg is CISA's industrial control systems group, certcc the CERT Coordination Center. Who raises a fix version →

Changes to a CVE record, or to CISA's exploited list

ChangeWhat is comparedRecordsRows
Added to CISA KEVCISA added the CVE to its Known Exploited Vulnerabilities catalog.446447
Ransomware use confirmedCISA changed its known ransomware campaign use field from Unknown to Known.107107
Fix version movedThe record states a higher fix boundary on the same release branch. This does not establish that the earlier fix failed.228365
Affected range extendedThe record states a higher last affected version on the same release branch.1,0741,156
Product addedThe record names an affected product absent from its initial listing.2,6057,598
CVSS base score changedThe published maximum base score changed on the same CVSS version and assessment context. This measures a record revision, not exposure.1,3361,336
Forensic triage now requiredCISA added a forensic triage requirement to the entry.00
KEV required action changedCISA revised the required action text.99
KEV due date movedCISA revised the remediation due date.33
Removed from CISA KEVCISA removed the CVE from its catalog.22
Record state changedThe CVE record changed publication state.976976

Changes to a GitHub advisory

ChangeWhat is comparedAdvisoriesRows
Advisory fix version movedThe advisory states a higher fixed version for the same package and release branch.131151
Package added to advisoryThe advisory added an affected package.1,0591,695
Advisory severity changedThe advisory changed its published severity label. The evidence identifies whether its scoring vector also changed.2,8002,800
Advisory withdrawnThe advisory now carries a withdrawal timestamp.380380

Only changes that survived the filtering are counted. The next section is the filtering: what we compared, what we refused and why, and what we never compared at all. What neither table above can see, and what a missing row does not prove, is one control below.

How revision intervals are measured

Where each kind's clock starts, and why a long wait is never a claim about when the earlier answer became wrong.

Time to revision counts from the earlier stated value's starting date to its first observed replacement. Each kind starts its count somewhere different: the day the CVE record or advisory was published for a publication comparison, or the observed introduction of the prior field value for an adjacent CISA KEV edit. An entry's KEV addition date is separate context. If the prior value was already present when history begins, or a missing file interrupts the interval, its exact duration is unknown and excluded from duration medians.

It is the interval to the first recorded revision, never a claim about when it became wrong. The appearance of the value reported today is a separate timestamp: an intermediate edit may have already replaced the original value. Source edits alone do not establish when a fix became insufficient, whether a release shipped, or whether anybody installed it.

These starting points make intervals across kinds different measurements. Publication-to-current boundary rows carryfirstReplacedAt for the original interval andobservedAt for the reported value's appearance. Cohort incidence uses the first counted movement on that branch. Where nothing dates a change the cell reads Duration unknown rather than a number nothing supports, which is not the same as zero days.

Adjacent advisory transitions compare consecutive readable states on the same release branch. Their interval begins when the prior value first appeared and ends when the next value appeared. They have their own identities and current, superseded, reversed or unconfirmed status. They are excluded from publication-to-current figures. A missing history state breaks continuity; a later fully observed interval can still be reported. The check API and saved review queue carry these transitions separately.

What we refuse to count

What we throw away before publishing a vendor's name, and how you check that judgement one row at a time.

There are two filters, not one, they run at different stages, and they are checked in different ways.

The first filter compares a change and then refuses it. It has refused 18,323 of the 30,322 changes it compared to a CVE record or a KEV entry, which is about one in two of them. Those 18,323 looked like changes, did not survive our checks, and are counted nowhere on this site. Without that filter the ledger would read 30,322 changes rather than 11,999. Every refused row is still published and marked, so you can read our judgement one row at a time and disagree with it. The GitHub advisory side runs its own copy of the same filter and refused a further 2,291 version pairs, which it counts by reason rather than writing them out as rows. That is a different unit again and is never added to the 18,323. The second filter refuses 6 shapes before any comparison happens, so those leave no row at all and can only be checked by reading. Neither is estimated.

The rows you can open are a sample. 3,930 of the 18,323 refused rows are in the file the site reads, sampled at most 250 refused rows for each reason we refuse a change, newest first. The counts are not sampled: the 18,323 above and the Refused column in the table of every kind are whole-corpus figures, and the per-class totals behind them are complete. It is the row you can read that is capped, not the count.

What survives both is the figure this site publishes: 0.43% of comparable CVE records moved their fix version after publication. Every class below was found by shipping a wrong result and catching it.

Comparable is a narrower group than “all CVE records”: a record inside our window that named a fix version when it was published, written in a form we can put in order. 228 records of the 53,461 comparable ones moved. The same count against every record in the catalog would be a smaller number answering a different question.

Compared, then refused

35 shapes look like an edit that matters and are not one. 27 of the 35 can refuse a change to a CVE record or a KEV entry, a renumbered product line being the commonest; the other 8 apply only to an advisory, in the other unit. The row still exists, is in the download, and is shown marked as not counted, so our judgement can be checked one row at a time.

Refused asWhy it is not a finding
Product line renumberedThe front of the version number changed while a meaningful tail survived, as 10.0.22621.4751 became 10.0.22631.4751. That corrects which build line the record is describing; it does not move a fix forward. The tail need not match byte for byte, because two build lines shipping on the same Patch Tuesday drift a revision or two apart, sometimes downward. The advisory side files the same shape under the same name, against a package's release branches rather than a vendor's build lines.
Another branch advancedA record states one boundary per supported release branch, and a publisher adding a NEWER branch raises the product's highest boundary without the branch anyone actually patched moving at all. PostgreSQL 15 to 15.6 read as 15.6 to 16.2 once a 16 branch appeared. Boundaries are now matched branch by branch, and this row is what the old maximum would have reported: every branch stated at publication is still stated, so the version named then still holds.
Release branches rewrittenA branch the record stated at publication is gone and another appeared in its place, so no branch can be matched to the one a reader acted on. The product's highest boundary rose, but not on any branch that can be followed from publication to now. Refused rather than guessed.
Same claim, other operatorThe record said 'affected through 12.11' and now says 'affected before 12.11.1', or the reverse. 'Through X' and 'before the next release' are one claim written two ways; nothing about which versions are affected changed. The next release means one component of the version one higher and everything after it zero, or a single .1 appended.
Version respelledThe same version written differently: V6_0_0_7 became V6_00_07 with the range start rewritten in the same change, or 4.5-149 became 149 on a product named 'iSherlock 4.5'. The digits that carry the meaning did not change, only their spelling.
Prerelease named as the boundaryOne side names a beta or release candidate, such as 2.4.8-beta1 or 3.3.0-rc1. A prerelease is not a version anyone was told to patch to, and reading past its qualifier orders it above the stable release it precedes. The advisory side has always refused this shape.
Description already named the later valueOn the day the record was published, its own description already named the version the structured field was later changed to: Apple's 'fixed in iPadOS 17.7.4' beside a field reading 17.7. The field was truncated and later regenerated to match the prose. Nobody reading the record patched to 17.7 and stopped.
Build number corrected to the one that shippedA build-numbered version (four or more components) whose last component alone moved within 14 days of publication: 10.0.17763.9115 became 10.0.17763.9121 five days after Patch Tuesday, where the publisher's own update history says .9121 is the build that shipped. The record was corrected to the released build; no fix moved.
Boundary below its own startThe range the record stated at publication ended before it began: 10.0.25398.0 up to but not including 10.0.22621.5472, a build from another Windows line. Nobody was on a version inside an empty range, so the corrected boundary moved no decision. A malformed range, refused rather than compared.
The earlier release did not shipThe publisher's release evidence says the version originally named by the record was never shipped. The structured record was corrected, but this cannot support a story about an installed fix being superseded. The source receipt remains beside the refused row.
Added entry does not state affected statusThe newly named entry has no affirmative affected status. An unaffected or unknown entry is not a newly affected product; its catalog edit remains visible as refused evidence.
Release branch added to an existing product nameA wildcard release-line label was appended to a product already named at publication. This can describe a branch-specific boundary, but it does not establish a newly affected product. The structured edit remains available as a refused row.
A variant beneath an already affected parentThe added entry names a recognized support or installation variant of a parent already stated as affected. The edit may clarify scope; it does not establish that an additional deployed product became affected. This conservative refusal can miss a real variant addition. The original and revised scope remain available for review.
Product identity rewritten with unchanged scopeA removed and added entry retain the same structured identity or affected scope while their labels change. The conservative classifier refuses this as a new affected product rather than turning a rename into an applicability alert.
The prior instruction already required triageThe forensic-triage field was introduced, but the preceding required-action text already stated that instruction. This is a metadata change, not evidence of a newly imposed obligation. The field and instruction receipts are preserved.
Entry date corrected, deadline followedThe due date moved by exactly the number of days the entry's own dateAdded moved in the same commit. CISA corrected the day the entry was recorded and the deadline followed arithmetically; the obligation itself did not move.
Ecosystem relabelledThe same package name was already listed at publication under another ecosystem label, and that pair is gone now: purl-type:swift became swifturl. The package did not become affected; its label changed.
Package name normalisedThe added name is the same package spelled the way its registry normalises it: oslo.middleware became oslo-middleware, slice_deque became slice-deque. Nobody depending on the old spelling was newly affected.
Product relabelledA near twin of the added product vanished from the same record in the same edit, as adobe/illustrator became adobe/illustrator desktop 2025. That is a rename, not a newly affected product.
Already named at publicationThe added product name was already spelled out inside a product the record named at publication. Apple's 'iOS and iPadOS' later gained a separate 'iPadOS'; a comma-joined list of 150 projector models was split into 150 rows. Nobody reading the original record could have concluded they were unaffected, so the split adds no product. On the advisory side, a Go module added beside a subpackage of it the advisory already listed is the same shape.
No products named at publicationThe record named no affected product when it was published, so nobody could have concluded 'we're not affected' from it. There was no decision to invalidate.
Added row names no productThe added row carries a placeholder meaning 'no product named' rather than a product. It cannot be matched against anything you run, so it is not counted.
RESERVED stub at publicationAt its first commit the record was a placeholder marked RESERVED. It named nothing, so nothing was ever checked against it, and everything it later gained is ordinary publication rather than a later edit.
CVSS assessments are not comparableThe two displayed maxima cannot be matched to the same scoring version, CNA and assessment context. Ambiguous metrics and v2 qualitative bands are excluded.
CVSS version set changedNo score was revised. The highest score moved because a score under a different version of the CVSS scale was added or removed beside the existing one. The record did not change its opinion of the flaw.
Placeholder became a real recordPublication itself. A record leaving the RESERVED placeholder state is the ordinary path into the catalog, not a record pulled out from under someone's work.
Due date backfilled into the pastThe new deadline was already in the past on the day CISA wrote it. One housekeeping edit rewrote 15 historical deadlines, covering PrintNightmare, Zerologon and the ProxyLogon and Pulse Secure sets, to a date three and a half years gone. A deadline moved to a day already past moves nothing anyone can still act on.
Same-day catalog correctionThe entry was withdrawn within a day of being listed: 59 and 74 minutes for the two rows this catches. That is CISA publishing and retracting, not a listing that had time to force an escalation.
Advisory rewrote its version rangesA release branch the advisory stated at publication is gone, or a surviving range absorbed or split one, so a comparison would not be comparing the same branch. Branches that survive with the same start are compared; only the rewritten ones are refused. This still refuses more than any other class on the advisory side.
Fixed version moved downThe version named as the fix moved DOWN rather than up. That is the advisory narrowing what it claims, not a fix moving forward, and nobody who took the higher upgrade is left short by it. Stored and shown, never counted.
Two versions that cannot be put in orderThe version reader refuses the pair, because one side carries a pre-release marker such as beta, rc, dev or snapshot, or a part it cannot read at all. Putting them in order anyway would invent a move out of characters neither version stated. The CVE catalog side refuses the same shape.
Version scheme changedOne side of the pair numbers its releases by year and the other does not, so the two are not on one scale at all. The PyPI package keystone moved from 8.0.0 (OpenStack Liberty, 2015) to 2012.1 (Essex, 2012): comparing them as numbers reads 2012 as greater than 8 and would tell a reader to downgrade. Refused in both directions.
Go pseudo-version completedTwo Go pseudo-versions where one is the other written out more precisely: 0.0.0-20220716 completed to 0.0.0-20220716132853-b47438d. They name the same commit of the same module, and nobody could have taken the first as an upgrade, so the pair is refused rather than counted.
Two products' boundaries swappedThe value the record states now was ANOTHER product's boundary in the same record at publication, and that product now states this row's old value: one commit swapped two values between two product entries (SQL Server 2022 GDR and CU 19 in CVE-2025-49719, OLE DB and ODBC Driver 18 in CVE-2023-28304). The same builds, relabelled; no fix moved.
Corrected inside the publishing batchThe fixed version the advisory first stated was replaced inside the batch of commits that first published the advisory, minutes after it appeared: a Go pseudo-version replaced by the tag it precedes, a Liferay module version corrected 97 seconds later. A coordinate corrected inside the batch that published it is the publication, not a fix that moved after it, and the 2026-09-05 validation sample found both of its hard false positives in this class.

Never compared, so they leave no row at all

6 shapes are not compared in the first place. A refused row above still exists and is shown uncounted. These produce no row anywhere, in any download, so counting rows will not find them and reading this is the only way to check them.

Never comparedWhy it is not comparable
Versions written as git commit hashesThe Linux kernel's publisher states its affected ranges as git commit hashes. A hash has no order, and reading its digits as a number would invent findings out of nothing. Never compared.
Several fix versions in one fieldSome publishers put a whole list of per-branch fix versions in the single field meant for one. Reading the first entry compares one version against a list. A value carrying a comma is not one version and is never put in order.
Version numbers nobody could readReading a version stops at the first part that is not a number: a publisher's placeholder such as 9.0.XX.X, or a stray dot such as 10.0.20348..2762. A difference at or beyond that point is not compared. A version that is merely short is unaffected, so 17.7 to 17.7.4 is still a finding.
Records published before the catalog's history beginsThe CVE catalog's own change history starts in 2023. An earlier record arrives in one bulk migration, so what it said on the day it was published cannot be recovered and it is out of scope for every kind of change measured in that catalog. Never compared at all, rather than compared and found clean. The KEV kinds are not bound by this floor and do carry findings for records published earlier.
KEV entries already listed when the watching beganA change to CISA's list is visible only from the first day we mirrored it onward. The entries already listed on that day are the starting picture and produce no row, so a CVE that joined the list earlier shows no addition here. Its addition was never watched, rather than watched and found absent, and nothing is estimated to fill the gap.
Advisories GitHub did not reviewOnly the advisories GitHub reviewed itself are read. The rest are imported from other databases, so a change there is churn upstream rather than GitHub revising its own claim, and it is never compared. An unreviewed advisory that changed carries no row here: never compared at all, rather than compared and found clean. This source has no 2023 floor, and its reviewed history runs back to 2017. It does have a late arrival problem: an advisory that reached the repository long after GitHub published it has the state on its arrival day read as its state at publication, so changes made in between are invisible. The CVE catalog has the same weakness on a smaller share of its records.

There is very likely a 36th shape we have not caught yet. Two of the ones above were only written down after they had already been refusing rows for months. If you find another, the download is where it will show.

Every kind, side by side

How much of each kind we found, how much of it is readable on this site, and how much was refused.

14 of 15 kinds have found something. Records and advisories are different units; an advisory count is never added to a record count.

Kind of changeWhat is comparedRecords, How many distinct records these changes touch. For the advisory kinds this counts advisories, marked adv.Changes, How many changes of this kind the whole analysis found.Here, How many of those changes are published on this site and searchable on the changes page. Where it is fewer, an absence there is not evidence that nothing changed.Refused, Rows this kind's false-positive filter refused. They are counted in no figure on this site.Median days to revision, The middle value of days to the first recorded revision, for the changes of this kind that could be dated at all.
Added to CISA KEVCISA added the CVE to its Known Exploited Vulnerabilities catalog. CISA KEV4464474470Duration unknown
Ransomware use confirmedCISA changed its known ransomware campaign use field from Unknown to Known. CISA KEV107107107019.0 d
Forensic triage now requiredCISA added a forensic triage requirement to the entry. CISA KEV00038Duration unknown
Fix version movedThe record states a higher fix boundary on the same release branch. This does not establish that the earlier fix failed. CVE catalog2283653654,34928.0 d
Affected range extendedThe record states a higher last affected version on the same release branch. CVE catalog1,0741,1561,156910340.4 d
Product addedThe record names an affected product absent from its initial listing. CVE catalog2,6057,5987,59812,56163.1 d
CVSS base score changedThe published maximum base score changed on the same CVSS version and assessment context. This measures a record revision, not exposure. CVE catalog1,3361,3361,33644520.0 d
KEV required action changedCISA revised the required action text. CISA KEV999010.0 d
KEV due date movedCISA revised the remediation due date. CISA KEV333180.1 d
Removed from CISA KEVCISA removed the CVE from its catalog. CISA KEV222216.4 d
Record state changedThe CVE record changed publication state. CVE catalog976976976018.8 d
Advisory fix version movedThe advisory states a higher fixed version for the same package and release branch. GitHub advisories131adv1511511,964pairs13.0 d
Package added to advisoryThe advisory added an affected package. GitHub advisories1,059adv1,6951,695327pairs161.1 d
Advisory severity changedThe advisory changed its published severity label. The evidence identifies whether its scoring vector also changed. GitHub advisories2,800adv2,8002,8000pairs12.6 d
Advisory withdrawnThe advisory now carries a withdrawal timestamp. GitHub advisories380adv3803800pairs81.8 d

Every counted advisory change is published here, so an absence in the advisory rows is an absence of findings in what that source could see. That is still not an all-clear: the engine reads only the advisories GitHub reviewed itself, and it compares each advisory as first mirrored against the advisory today, so an edit made before an advisory was copied into that repository is invisible to it. Both windows, with their figures, are the next section. The advisory changes, by registry →

Here equals Changes in every row above: every counted change of every kind is in the file the site reads, so an absence on the changes page is an absence of findings rather than a row the file could not carry.

What each catalog cannot see

Where the blind spots are, and what a missing row does not prove.

We read three catalogs and each has a blind spot. None is corrected for, filled in or estimated away, and each is stated next to the figures it shapes.

Not checked: two blind spots bound the table above. A CVE record published before 2023 is never compared, and a flaw already on CISA's exploited list on the day we started watching shows no addition here. So a 0, or a record you expected and cannot find, is not evidence that it never moved.

Not checked: only the advisories GitHub reviewed itself are read, and one that reached the database long after publication has its arrival-day state read as its state at publication. Neither an unreviewed advisory nor a change made before that arrival produces a row here. Figures are in the third row below.

Zero is a finding count, not an unchecked kind. Forensic triage now required is checked against everything we have read and has found nothing. It is listed at 0 rather than dropped, because a kind missing from this table reads as a kind nobody looked at.

SourceCoversCannot see
CVE catalogRecords published 2023 onwardAnything before 2023. The catalog's own edit history begins there, so an earlier record arrives in one bulk migration and what it said on the day it was published cannot be recovered.
CISA KEVChanges from 2025-01-27, the day we started watchingAnything that changed before that day, including every flaw already on the list. Those changes are invisible to everyone, not only to us, and nothing is estimated to fill the gap.
GitHub advisoriesGitHub-reviewed advisories only (advisories/github-reviewed/). No 2023 floor on this source.Advisories GitHub did not review, which are imported from other databases. A change there is churn upstream, not GitHub revising its own claim. Version numbers we cannot put in order are refused, never guessed. And advisories that arrived late: 13,779 of the 35,259 reviewed advisories, 39.1% of them, reached this database more than a week after GitHub published them, 6,102 of them in a single commit. For those, what we read as the state at publication is the state on the day the advisory arrived, so every change made in between is invisible here.

Two more things a per-year rate cannot show

Backfilled means the record was written into the catalog later than it was published, with any edits made in the meantime already applied.

Not checked: a record can only be seen to move while we keep watching it. The newest year has had the least time to be amended, so its rate is a floor, not a final figure. Years are not adjusted for this.

Not checked: where the first commit lands more than 7 days after the record's own published date, the catalog backfilled it, so what we read as the state at publication already contains whatever was amended in between. Moves inside that window are invisible and are not estimated. This is part of why the earliest in-scope year reads low; the larger part is that the 2026 bulk re-edits by a few publishers fell on 2024-2025 records.

What this does not claim

  • Not that any vendor acted in bad faith. Raising a fix version when the first patch proves incomplete is the right thing to do, and a raised version does not show that it was: publishers also re-issue builds and regenerate fields. What is missing is the notification: no alert reopens the ticket you closed on the old number. Evidence of a record changing, not an accusation, and not evidence that the first fix was incomplete.
  • Not that you are vulnerable. It says the record you relied on changed. Whether that reaches you depends on what you actually run.
  • Not a complete census. Each catalog has a blind spot, and where we publish only part of a list we say so. An absence is never evidence that nothing changed.

How much of the rate is how long we watched

Whether a lower recent rate means the records got better or means we have watched them for less time.

A record published last month has had a month to be edited; one from 2023 has had years. So the headline share, which counts every move seen so far, is not one number about the catalog but a mixture of how long each record has been watched. Two ways to hold that constant, and they disagree in an instructive way.

First, a fixed window: for each year of publication (the date the record was published, not the year in its CVE id), only the records old enough to have been watched for the whole window are counted, and a move counts only if it fell inside it. 5,787 of the records stating a fix were published in a different year from the one in their id, which is why this table and the per-year tables elsewhere on the site do not share their years.

Fix version moved, by year of publication and window

Share of records stating a fix version. Each cell is the records old enough for that window; “so far” is the headline's method over the records that carry a publication date.

PublishedSo farWithin 30 dWithin 90 dWithin 180 dWithin 365 d
20230.28%16 of 5,6730.12%7 of 5,6730.25%14 of 5,6730.25%14 of 5,6730.25%14 of 5,673
20240.30%36 of 11,9520.13%16 of 11,9520.23%27 of 11,9520.26%31 of 11,9520.29%35 of 11,952
20250.42%55 of 13,1470.33%43 of 13,1470.37%49 of 13,1470.40%52 of 13,1470.25%22 of 8,632
20260.53%121 of 22,6870.56%94 of 16,7660.85%74 of 8,7550.51%11 of 2,137not stated
All years0.43%228 of 53,4590.34%160 of 47,5380.41%164 of 39,5270.33%108 of 32,9090.27%71 of 26,257

Affected range extended, by year of publication and window

Share of records stating a last-affected version. Each cell is the records old enough for that window; “so far” is the headline's method over the records that carry a publication date.

PublishedSo farWithin 30 dWithin 90 dWithin 180 dWithin 365 d
20231.10%50 of 4,5590.18%8 of 4,5590.22%10 of 4,5590.22%10 of 4,5590.35%16 of 4,559
20242.07%221 of 10,6530.21%22 of 10,6530.30%32 of 10,6530.37%39 of 10,6530.39%42 of 10,653
20255.58%721 of 12,9150.16%21 of 12,9150.22%29 of 12,9150.80%103 of 12,9153.93%384 of 9,780
20260.74%82 of 11,0260.51%45 of 8,7400.79%35 of 4,4522.01%34 of 1,692not stated
All years2.74%1,074 of 39,1530.26%96 of 36,8670.33%106 of 32,5790.62%186 of 29,8191.77%442 of 24,992

Second, a survival curve. Every record is followed from its publication until it moved or until the last catalog commit we read, whichever came first, and the curve is the share estimated to have moved by day N given that following (a Kaplan-Meier estimate, right-censored at the mirror head). A record published forty days ago contributes forty days of not moving and nothing about day 41.

Fix version moved: share moved by day N53,459 records stating a fix version · own axis

By day 90, 0.42% of these records had moved; by day 365, 0.47%; by day 730, 0.49%.

0%0.2%0.4%0.6%03090180365540730Days since publicationShare movedBy day 30: 0.34%By day 90: 0.42%0.42%By day 180: 0.45%By day 365: 0.47%0.47%By day 540: 0.49%By day 730: 0.49%0.49%Fix version moved: estimated share of records stating a fix version whose record had moved, by days since publication. By day 730: 0.49%.

228 records moved; 53,231 had not by the last commit read (2026-09-06) and are censored there, not counted as never moving. Median follow-up 355.4 days. 2 records with no publication date are left out.

Show the numbers
By dayMoved by thenRecords movedStill watched
10.05%2853,404
70.12%6352,437
300.34%17047,378
900.42%20739,363
1800.45%21832,801
3650.47%22526,186
5400.49%22819,918
7300.49%22813,554
Affected range extended: share moved by day N39,153 records stating a last-affected version · own axis

By day 90, 0.39% of these records had moved; by day 365, 2.38%; by day 730, 4.02%.

0%2%4%6%03090180365540730Days since publicationShare movedBy day 30: 0.26%By day 90: 0.39%0.39%By day 180: 0.67%By day 365: 2.4%2.4%By day 540: 3.4%By day 730: 4.0%4.0%Affected range extended: estimated share of records stating a last-affected version whose record had moved, by days since publication. By day 730: 4.0%.

1,074 records moved; 38,079 had not by the last commit read (2026-09-06) and are censored there, not counted as never moving. Median follow-up 516.2 days. 2 records with no publication date are left out.

Show the numbers
By dayMoved by thenRecords movedStill watched
10.04%1639,122
70.13%5138,483
300.26%9936,771
900.39%14332,473
1800.67%23229,633
3652.38%69524,550
5403.44%93818,423
7304.02%1,02311,018

The two adjustments do not agree, and the reason is in the table. The curve pools every year under one pace of editing; the table shows the pace is not shared. Records published in 2026 moved within 30 days at 0.56%, against 0.13% for those published in 2024, so the curve's early rise is carried by the newest records and its 0.47% by one year sits above the 0.27% of the 26,257 records actually watched for a full year. Neither is the wrong number; they answer different questions, and the headline answers a third.

The rate under each definition

How much the headline moves when the definition moves.

The reader picks the question; the number ranges from 0.18% to 1.54%. Every row is the fix-version rate recomputed under one alternative definition, from the same rows, with the population it divides by named beside it.

DefinitionRecords movedDenominatorRate
As shipped: a stated fix version raised on the same release branchof records stating a fix22853,4610.43%0.37% to 0.49%
Excluding corrections within a day of publicationof records stating a fix20053,4610.37%0.33% to 0.43%
Excluding commits that changed 50 or more counted rows at onceof records stating a fix17353,4610.32%0.28% to 0.38%
Within 365 days of publication, records published at least a year before the mirror headof records stating a fix, published at least 365 days ago7126,2570.27%0.21% to 0.34%
The pre-split definition: either boundary raised, including a last-affected version with no fix statedof records stating any orderable boundary1,30184,3871.54%1.46% to 1.63%
Excluding the two publishers with the most counted records (microsoft, VulnCheck)of records stating a fix, those publishers left out8345,8160.18%0.15% to 0.22%
Records published in 2023 (by datePublished), so farof records stating a fix, published 2023165,6730.28%0.17% to 0.46%
Records published in 2024 (by datePublished), so farof records stating a fix, published 20243611,9520.30%0.22% to 0.42%
Records published in 2025 (by datePublished), so farof records stating a fix, published 20255513,1470.42%0.32% to 0.54%
Records published in 2026 (by datePublished), so farof records stating a fix, published 202612122,6870.53%0.45% to 0.64%

The smaller figure under each rate is its 95% Wilson interval. “Same-day” removes a record whose first counted move came under a day after publication; the same-day flag on a row marks a published value first replaced under a day by any edit, counted or refused, so the flag can mark more records than this row removes. “Bulk” is a correcting commit carrying 50 or more counted rows. The pre-split row is the definition this site used before 2026-09-03, for the record.

A random sample, read back by hand

What a person found when they checked the machine's work.

This is a historical candidate audit before its findings were repaired. Regression checks of those repairs do not constitute a new independent sample of the current build.

A seeded random sample of 30 counted finding rows of the 365 (seed 2026-09-05b, drawn 2026-09-05) was read against raw git history, including the first state, the states beside the edit and the pinned head, and classified by agent-2026-09-05b.

1 hard false positives in 30, 95% Wilson interval 0.6% to 16.7%. ambiguous 1, clean 22, clean bulk 4, clean same day 2, hard fp 1.

The sampled rows, source states and classifications →

What this cannot tell you

Thirteen things to hold against every figure here before quoting one.

Several are partly addressed by the method as it stands; the part that is not is stated in each.

Two fields, two claims

A record bounds an affected range with lessThan (the first fixed version) or lessThanOrEqual (the last affected version, no fix stated). They are two kinds over two denominators: 53,461 records state a fix, 39,155 a last-affected version. A raised last-affected version is never a fix that moved.

Records are not independent

A publisher re-syncing its catalog moves hundreds of records in one commit. 55 of the 228 counted records sat in commits changing 50 or more counted rows; the rows cluster into 269 edit events, the largest Patchstack with 507 records. Rates are shares of records; events are shown beside them.

Publisher versioning conventions

Microsoft cites the cumulative-update build; a build number whose last component alone moved within 14 days is refused (1,373 rows). Apple's and Adobe's structured fields regenerated to a version their description already named are refused (1,295 rows). What remains still cannot say the first fix was incomplete.

Structured fields, not prose

Only the structured affected ranges are read. Where the description named the later version on the day of publication, the structured field was wrong and the row is quarantined. A later version stated only in prose is never read, so a fix announced that way is invisible here.

Lag dating

The commit named is the first at which the reported value appears on the same release branch: the dating replays the classifier and looks for the row's own before and after pair. A change that arrived only through a merge commit ships with no lag rather than a guessed one.

The headline is right-censored

This is the share that has moved so far, and records published recently have had less time to be edited. Following each record only for as long as it has been watched, an estimated 0.42% move within 90 days of publication and 0.47% within a year. Of the 26,257 records watched for a full year, 0.27% moved within it.

History begins in March 2023

Per-change history in the catalog begins 2023-03-28. 2,982 records of the 2023 cohort (9.7% of those with a date) reached the catalog more than a week after publication, most in that first batch, so what is read as their state at publication already carries any edit made before the batch.

What the records outside the population are

84,869 of the 169,256 records compared state no orderable boundary and are never measured or ranked: 79,446 list exact affected versions with no range, 4,615 name no version at all, 723 state only git hashes and 85 mix hashes with exact versions. A publisher listing exact versions is never ranked; its absence is not a clean record.

Id year is not publication year

Every per-year table on this site buckets by the year in the CVE id. 5,787 of the 53,461 records stating a fix were published in a different year from their id. The cohort table above is the one place bucketed by publication date, and its years are not the same years.

Same-day corrections are counted

28 of the 228 counted records made their first counted move within a day of publication. They are counted, and the sensitivity panel removes them. 32 carry the same-day flag: on those the published value was first replaced within a day by any edit, counted or refused, so the flag marks more records than the panel removes. The advisory side flags edits within its publication batch. Timing alone does not establish whether anyone acted on an earlier value.

Score disagreement is not drift

Two scorers were compared catalog-wide: 169,256 records, 219 scored by both on a shared CVSS version, 39 crossing a band, the publisher the higher scorer in 17 and the second scorer in 22. A gap is a difference of judgement, never a correction, and is never added to any figure above.

Validation is one sample

30 rows drawn with seed 2026-09-05b, 1 hard false positives, interval 0.6% to 16.7%. The interval applies to the sampled candidate and its defined population. Ambiguous scope and incorrectly labelled subtypes are disclosed separately. Repairs prompted by that sample do not establish a zero false-positive rate or complete recall.

Four thresholds are choices

A build revision within 1.5%: the same build relabelled. First commit more than 7 days after publication: a backfill. A build number whose last component alone moved within 14 days was corrected to the shipped build. Fifty or more counted rows in a commit is bulk. None is a measurement.

How long a CVE looked ordinary

What the median wait measures, and the reading of it that is wrong.

A flaw sat on the CVE list for a median of 307.5 days before CISA confirmed it was being attacked and added it to the exploited list. That is measured across 1,636 of the 1,695 entries on the list today, meaning every entry whose two published dates could be read, whether or not we watched it being added. Entries CISA listed before the CVE record itself was published are left out rather than counted as zero days.

This is not how long it took to be attacked. CISA lists a flaw when it confirms the attacks, not when they start, so the listing always comes later than the real thing. What this measures is how long the CVE looked ordinary to everyone triaging it.

Where the data comes from

Which repositories are read, under which licences, and that nothing else is consulted.

Three public repositories, read directly. No feed, no vendor bulletin, no scanner.

We do not read these catalogs live. We keep our own dated copy of each, the mirror named below, and can only compare states from the first copy we hold onward. One of those copies is a backfill, a single bulk load that copied thousands of advisories in at once, so for those advisories the earliest state we can see is the day of the bulk load, not the day they were published.

  • CVE Program catalog, at CVEProject/cvelistV5. A wait measured on one of its rows is counted from the record's own publication date. Read at 1f78fd2580c1, committed 2026-09-06.
  • CISA Known Exploited Vulnerabilities catalog, at cisagov/kev-data. A wait measured on one of its rows is counted from the observed introduction of the prior KEV field value. Read at ac9d37166471, committed 2026-09-04. 296 catalog commits from 2025-01-27 to 2026-09-04, 1,695 entries on the list today. 1 of them shipped a file we could not parse and was not diffed; the previous state was kept, so its changes are dated to the next readable commit rather than lost. This list carries no date for a change to a field, so apart from an addition CISA dates to the same day, a KEV change is dated by the mirror commit that first shows it: at or before that time, and no earlier than the commit before it.
  • GitHub Advisory Database, at github/advisory-database. A wait measured on one of its rows is counted from the advisory's own publication date. Read at f6d4e024d0b3. 35,259 GitHub-reviewed advisories compared, 17 whose first published state could not be read and were not compared. How much of it arrived late.

Shipped snapshot computed 2026-09-07 from catalog commit 1f78fd2580c1. Real findings, not live ones: records amended since are not reflected. A later fix version is evidence that the record changed, not evidence that the first fix was incomplete.

Every figure on this site is taken from one analysis, computed 2026-09-07 against the commits above, and shipped as one file the site reads at build time. Nothing is answered live, and every page answers from the same file.

Each catalog is published under its own licence, and the copyright, trademark and attribution notices those licences require are on the sources page. This site is not affiliated with, endorsed by or sponsored by any of them.

Take the file →

What changed in the method

What happens when we get one wrong, and who gets to say so.

Every finding here is derived mechanically, and mechanical derivation gets things wrong. A dispute is settled against the commit pair, not against our opinion. An upheld dispute is fixed in the classifier rather than in an exception list, so one correction moves every affected record and the published totals with it, and the change is written into the log below with the headline figure before and after. A correction cannot be made silently.

If your organisation is named on this site you have a standing right of reply, and it does not depend on us being wrong: send us the answer and it is published beside the finding. Dispute a finding, or reply to one →

2026-09-05 Validation pass: four refusal classes

**Later the same day — ranking rule.** research/cohorts.py:cna_statistics now orders eligible publishers by the Wilson 95% LOWER bound (ties by point rate, then name), which is the rule /cna states; it had ordered by point rate, so the page and /api/v1/cna published different #1s. The fields written from that order — years[].cnas[].rank, publishers[].rank, overlapsNext, and cnaRanking.distinguishableThrough on each year and on the headline — are now the single source. Year=all #1 before: CERT-PL (rate 0.0356, lower bound 0.0181); after: microsoft (rate 0.0274, lower bound 0.0232), CERT-PL #2. Per-year #1s are unchanged (microsoft 2023/2024/2026, CERT-PL 2025); distinguishableThrough is unchanged everywhere (0 for year=all and 2025, 1 for the other years). No finding row or class moved. Also in this pass: description_already_stated on an open-start range now considers only HEAD boundaries the publication did not already state, preferring the stated line, so an unchanged sibling line the prose named on day one is not evidence about a line that later moved (negative test beside the CVE-2025-31242 case); 0 of the 1,295 receipts changed. research/euvd_retention.py now catches http.client.HTTPException (IncompleteRead, BadStatusLine) as a failed receipt instead of a traceback.

Mirror heads unchanged from the previous entry (cvelistV5 2f0a1513d3a7, kev-data ac9d37166471, advisory-database 8c29a441a784), so every figure below is the method and nothing else. Motivation: the seeded sample research/out/validation/2026-09-05.json (seed 2026-09-05, 30 counted fix_version_moved rows read back from git) found 4 hard false positives of 30, rate 0.1333, Wilson 95% [0.0531, 0.2968]; a second read-back of 6 counted advisory rows (2026-09-05-part2-hand-check.json) found 2 of 6. Each false positive was traced to a shape, the shape was confirmed against git for every counted row that carried it, and each is now a named class with a positive and a negative test (research/test_amendments.py, research/test_ghsa.py).

Headline before -> after: fix_version_moved 235 records / 375 rows of 53,368 stating a fix (0.44%, median 28.1 d) -> 228 records / 365 rows of 53,368 (0.43%, median 28.0 d); 275 -> 269 events. affected_range_extended 1,073 records / 1,155 rows of 39,099 (2.74%, median 342.0 d) -> 1,074 / 1,156 of 39,099 (2.75%, median 340.4 d). advisory_fix_moved 141 advisories / 163 rows -> 131 advisories / 151 rows (median 11.6 d -> 13.0 d over 124 -> 112 dated rows). No other kind moved.

The classes and keying rules

**boundary_swapped_between_products** (6 rows quarantined, 4 of them previously counted: CVE-2025-49717/49718/49719 SQL Server 2022 CU 19 16.0.1140.6 -> 16.0.4200.1 at 1c5d3e7ae299, CVE-2023-28304 OLE DB Driver 18 18.2.1.1 -> 18.6.5 at 2366c3496fa). The after-value was another product's boundary in the same record at publication and that product now states the before-value: the commit exchanged two values between two product entries. Both halves are required; two products converging on one new build stay counted (research/amendments.py:_swapped_with).

**description_already_stated, open-start variant** (CVE-2025-31242 macOS 13.7 -> 13.7.3 and 14.7 -> 14.7.3, 2 rows). The publication description said "fixed in macOS Ventura 13.7.6"; commit 59a4b3b0acc2 regenerated the entry to < 13.7.3 and < 13.7.6. The matcher paired 13.7 with 13.7.3, which the prose never named. For an open-start range the class now tests every HEAD boundary of the product above the stated one (_described_successor). A description naming a value the record does not state at HEAD is still no evidence.

**changes[] fixes keyed by their own release line** (CVE-2025-6203 Vault Enterprise). 0 < 1.20.2 with changes[] unaffected at 1.19.9, 1.18.14 and 1.16.25 states four branches. Keyed by the parent's "0", each changes[] fix was paired with the open range's lessThan 1.21.2 and three cross-branch rows were counted; keyed by their own major.minor the record's real moves are 1.16.25 -> 1.16.27, 1.19.9 -> 1.19.11 and 1.20.2 -> 1.20.5, all counted, and the vanished 1.18 line yields nothing (3 rows out, 3 in). The same keying surfaced three moves the old pairing had refused as operator_switched (CVE-2024-34383 seopress 7.7 -> 7.7.2, CVE-2024-35684 elasticpress 5.1.1 -> 5.1.2, CVE-2024-54254 1.6.3 -> 1.6.3.1: a changes[] fix previously paired with the ceiling on the same value), and dropped three whose publication fix is still stated on a keyed branch at HEAD (CVE-2024-6156 lxd 5.21.2 -> 6.1, now other_branch_advanced; CVE-2024-7322 EmberZNet 7.4.4 -> 8.1.0) or was withdrawn rather than raised (CVE-2024-22162). A still-stated open boundary is now recognised on any range of the product, not only an open one (_verbatim). 85 operator_switched receipts on the fix_version_moved kind (180 -> 95) were this pairing of a changes[] fix with the ceiling at the same value; none was ever counted. On affected_range_extended, 33 counted rows that paired a raised ceiling with the changes[] fix now pair it with the raised ceiling (same count, the after-value is now the last-affected version, which resolves the "ambiguous" mapsvg row of the 2026-09-05 sample); and a ceiling raised to the release a HEAD fix names (<= 1.4.1 -> <= 1.5.0 beside unaffected at 1.5.0, gavias Krowd CVE-2025-32595) stays operator_switched (_ceiling_is_the_fix). One extension row is new (CVE-2024-7262 WPS Office <= 12.2.0.13489 -> < 12.2.0.16412, previously hidden behind a since-lowered changes[] fix).

**One row per identical (vendor, product, kind, before, after) move** (CVE-2026-68480 Linux 6.6.149 -> 6.6.150 stated as both an open lessThan and a keyed unaffected start; CVE-2024-8404 PaperCut 23.0.9 -> 24.1.7 as a lessThan and a changes[] entry; 2 rows). The previous rule collapsed only the Linux lt/unaffected twin. Two stated-start branches sharing one fix that moved to one value are now one row too (t_distinct_release_starts_with_equal_boundary_values_are_one_row, inverted). A quarantined spelling wins over a counted one; a keyed branch over the open range.

**unshipped_release** (+1 row: CVE-2026-41080 libexpat 2.7.6 -> 2.8.0). research/evidence/unshipped-releases.json now carries the libexpat entry: the GitHub tags API observed 2026-09-05T18:52:20Z lists R_2_7_5 (released 2026-03-17) then R_2_8_0 (2026-04-24) and no R_2_7_6.

**publication_batch_correction** (GHSA, 12 rows quarantined, 163 -> 151). A counted fix move whose original value was replaced within PUBLICATION_BATCH_HOURS (1 h) of the advisory's first commit. Evidence: 12 of 163 counted rows carried inPublicationBatch; the 2 hard false positives in the 6-row advisory read-back were both in that set (GHSA-vrh2-rprg-rgc6, a Go pseudo-version replaced 4 minutes later by the 4.0.5 tag it precedes; GHSA-qgj5-4qvg-2f8c, Liferay Portal 7.4.3.132 replaced 97 s later by the module's own 15.2.1) and the 4 outside it were true by record. All 12 were corrected between 97 s and 35 min after first appearing in the database (wasmvm x3 at 34 min, mattermost/server/v8 x4 at 6-19 min, pterodactyl wings at 6 min, zephyr at 24 min, Blogifier at 35 min). For 3 of them (published 2022, first committed 2024-2026) the lag measured from published — 607, 1,046 and 1,329 days — is a backfill artifact the mirror cannot support: it saw the original value stand for minutes. A coordinate corrected inside the batch that published it is the publication, not a fix that moved after it. The rows ship as suspect with firstReplacementInPublicationBatch; the inPublicationBatch flag on the other advisory kinds is unchanged. The fix_version_moved kind's own inPublicationBatch figure is now 0 by construction and the block carries quarantinedPublicationBatchCorrections.

Sanitation before any index

research/snapshot.py sanitised only the seven row arrays, in finalize(), after vendors[], events[], publishers[], years[].cnas, advisories.comparison.packages and kevImpact had been built from raw names. A 300-character vendor produced a vendors[] entry whose page found zero rows (names on rows are capped at 200) and the gate passed. Rows are now sanitised in build() before any index reads them, and every name-carrying index goes through the same clean_name; pinned by test_snapshot.py:t_indices_are_built_from_sanitised_rows.

Validation against the new snapshot

research/out/validation/2026-09-05b.json (seed 2026-09-05b, 30 of the 365 counted rows, reviewer agent-2026-09-05b): 30/30 classified, hard false positives 1, rate 0.0333, Wilson 95% [0.0059, 0.1667]; by class clean 22, clean_same_day 2, clean_bulk 4, hard_fp 1, ambiguous 1. The hard false positive is CVE-2025-14756 (TP-Link Archer MR600 v5 1.0.0 0.9.1 v0001.0 Build 250930 Rel.63611n -> 1.1.0 ..., 7.8 h): only the leading firmware label changed and the build token is byte-identical, a shape no class sees because the parser stops at the space. One row; not implemented as a class. The ambiguous row is CVE-2023-38171 (the 2024-12-10 uniform +1 rewrite of 107 Visual Studio / .NET records), the same verdict the 2026-09-05 sample gave that record's 17.4 row. Two samples are not a trend: the earlier interval [0.0531, 0.2968] and this one overlap.

Not changed: web/src/lib/signals.ts must register boundary_swapped_between_products and publication_batch_correction before web/src/lib/signals.test.ts will pass against this snapshot.

2026-09-05 Expansion gates: preserve precision, retain observations

No new finding kind was admitted. A full source refresh retained 235 records / 375 first-fixed rows; the measurable population rose from 53,358 to 53,368. Last-affected extensions remain 1,073 records / 1,155 rows, now over 39,099 measurable records instead of 39,090. Between the snapshots committed at bb3c829 (built 2026-09-05T00:06Z, multiyear engine 2026.09.04) and 1a95911 (05:32Z, engine 2026.09.05) one kind's count moved: cvss_changed 1,534 -> 1,336 records (quarantined 247 -> 445), the CVSS identity audit recorded in the drift-acceptance blocks below (198 maximum pairs excluded because the scoring version or assessment context was not comparable). The source refresh between 1a95911 and 998f513 (06:37Z, same engines) changed no kind's finding-row count; only the two measurable populations moved. The 2026-09-04 implementation table further down this file still shows cvss_changed at 1,534 because it was generated before that audit. The measurable-population changes are source-population changes, not a classifier relaxation.

The proposed NVD configuration signal was held after testing every counted CNA and GHSA fix move. The inherited 199/375 claim compared Grype version strings across products; it did not establish NVD CPE identity. Pinned NVD configurations yielded 183 raw old-bound matches among 375 CNA rows. Conservative vendor/product matching outside Microsoft and distro publishers left nine candidates in two CVE records. GHSA's 42 raw candidate rows still need package-to-CPE matching. These are review populations, not a published scanner-staleness rate. Current-value disagreement cannot by itself date continuous lag.

The KEV forensic-field proposal would reverse a correct refusal: all 38 initial Yes flags repeated byte-identical requiredAction text already requiring triage. They remain quarantined. The short-window join found 363 current entries with 3-day or 14-day windows: 199 lack deadline-era CVE history, 94 never supply an orderable structured fix, 66 have one by the deadline, and four first supply one later. Source review of those four found delayed enrichment or an earlier mitigation path. Missing CNA structure cannot establish an unavailable remedy or noncompliance; the dedicated deadline view is held.

EUVD collection is independent research retention, outside the runtime snapshot and comparison-index kind budget. An initial updated-date window retained 3,048 records in 31 pages. It is neither a full catalog nor an atomic snapshot. A nonrandom 200-record sample yielded 177 identical-vector CNA score agreements and 23 withheld comparisons; no new signal is justified. The prepared workflow keeps artifacts for 90 days; durable retention and remote activation remain separate operational steps. Snapshot schema stays 3, with 15 comparison kinds.

The README generator now uses CVE-ID-year labels for ID-year populations and the KEV engine's exact prior-field intervals for medians. It no longer derives KEV field age from dateAdded. Bare --write and --check now operate on both READMEs, fixing an unattended-refresh path that previously only printed figures. Standalone launch blocks can be recounted directly from the delivered snapshot. No new independent precision estimate or claim of paid demand was established.

Later the same day the 76 open NVD candidate rows were adjudicated with explicit per-row identity decisions (audit/2026-09-05/expansion/nvd-experiment.md, last section): 23 CNA rows in 15 CVE records across 5 publishers and 31 GHSA rows in 28 advisories are supported disagreements; 21 rows are held on identity or scope. That meets the materiality gate the experiment proposed, and the signal is still not built: it is a review queue, not a rate, and the session kept the existing signals as the public set instead; nothing was deployed, and production still serves the 2026-09-03 build.

The unattended refresh could not bootstrap its mirrors: a full clone of cvelistV5 timed out after 30 minutes on a hosted runner (run 33961187243). research/sync_mirrors.py now bootstraps a new mirror at depth 1 and deepens it in checkpoints (measured 2026-09-05: depth 1 in 72 s, 5,000 commits in 3 min 25 s); the workflow saves the mirror cache after the sync step whether or not the run succeeds, so an interrupted bootstrap resumes. A shallow mirror is refused by every engine, so no snapshot can be built from partial history.

2026-09-04 Phase 2: rigor surfaces

Same mirror heads and engines as the Phase 1 entry; the headline is unchanged (263 / 53,060 = 0.50%; 1,073 / 38,941 = 2.76%). What changed is what ships beside it, computed by research/cohorts.py from the per-record populations measure.py now keeps:

  • headline.cohorts: publication-year cohorts (by datePublished, not the id year) under fixed 30/90/180/365-day windows, both kinds. Fix moves within a year of publication, over the 26,118 records watched for a full year: 75, 0.29%. Range extensions: 419 / 24,843, 1.69%.
  • headline.survival: Kaplan-Meier cumulative incidence of a first counted move, right-censored at the mirror head. Fix moves by day 90: 0.48%; by day 365: 0.55%. Range extensions by day 365: 2.39%; by day 730: 4.04%. The curve pools cohorts whose pace differs (2026 records moved within 30 days at 0.57%, 2024 records at 0.13%), which is why it sits above the fixed-window figure.
  • headline.sensitivity: the fix-version rate under each definition, 0.21% (excluding microsoft and PostgreSQL) to 1.59% (the pre-split lt-or-lte definition over 83,784 records stating any boundary).
  • Per-CNA rows carry a 95% Wilson interval, an empirical-Bayes rate shrunk toward the year's pooled rate by 100 phantom records, the median lag of that CNA's moved records, distinct correcting commits, and the shape of what moved. The ranking floor is now on the ranked denominator (>= 100 records stating a fix) and the numerator (>= 5 moved); across all years 5 publishers clear both, 28 have too few moved records, 15 sit below the measurable floor, and only rank 1 (PostgreSQL, 28 / 108, 18.6%–34.9%) is separated from the next by its interval.
  • headline.excludedShapes: the 84,640 records with no orderable boundary are 79,233 with exact versions and no range, 4,607 with no version entry, 715 with git hashes only, 85 mixing hashes and exact versions.
  • The right-censoring sentence beside every headline rate is computed from headline.survival and headline.cohorts; the /moves lag histogram (six unequal buckets drawn as equal bars) is replaced by the survival curve; the classified validation sample ships at web/public/validation/.

2026-09-04 Phase 3: the promotion model on every rescore

Same mirror heads, same engines, same run as the Phase 2 entry (cvelistV5 344647659eb1, kev-data 782e9b631fbe, advisory-database c8bfe3111d4e, schema 2). **No figure on this site moved.** research/check_snapshot.py reported +0.0% on every headline figure and no row change on any of the fifteen kinds; this entry adds FIELDS, not findings.

What is new: a rescore now ships the shape of its band move as a fact of the row, rather than leaving every surface to derive one from a pair of strings.

**Two fields on every cvss_changed row** (research/multiyear.py:promotion):

  • bandsCrossed — signed integer. Positive = the score landed in a worse band, negative = a milder one, 0 = it never left the band it was in. ABSENT means the score could not be read as a band at all; 0 is a finding and absence is a gap, and the two must never render the same.
  • bandJumpBAND[after] * 10 + (BAND[after] - BAND[before]) with BAND = {none:0, low:1, medium:2, high:3, critical:4}, present ONLY on a promotion and null on everything else. Destination first, distance second, so every -> critical (41–44) outranks every -> high (31–33): the destination decides what a defender must do today, the distance only decides how wrong the closed decision was. A row that carries a rank IS a promotion, which is the field's second job.

bandDirection, bandCrossed, bandBefore, bandAfter and bandTransitions are unchanged and were not replaced.

**Every band move in the corpus**, from web/src/data/snapshot.json (counted from the shipped file; the command and its output are in the commit body):

before -> afternbandsCrossedbandJump
medium -> high216+131
high -> medium204-1
critical -> high143-1
high -> critical63+141
low -> medium58+121
critical -> medium42-2
medium -> critical34+242
medium -> low33-1
none -> medium13+222
low -> high5+232
none -> high4+333
high -> low4-2
critical -> low3-3
high -> none3-3
none -> critical2+444
none -> low2+111
low -> critical2+343
low -> none1-1
medium -> medium3500
high -> high2610
critical -> critical820
low -> low90

**headline.rescores**, the landing-page rollup, counted in RECORDS:

records
total1,534
promoted (landed in a worse band)399
demoted (landed in a milder band)433
withinBand (never left its band)702
toCritical (landed in Critical)101
jumped2Plus (crossed 2+ bands upward)60

promoted + demoted + withinBand = 1,534 and nothing else here does: toCritical and jumped2Plus are SUBSETS of promoted and overlap each other. The object carries its own note naming the unit and stating that, because a record count sitting beside this site's row counts is exactly where two units get added together. Every one of these figures reproduces the recomputation in docs/decisions/2026-09-04-lead-promotions-and-look.md §2 — 399, 433, 101, medium→high 216, high→critical 63, low→medium 58, medium→critical 34 — with ONE correction: §2 estimated big_jump at "47… recount at build time" and the built figure is **60** (medium→critical 34, none→medium 13, low→high 5, none→high 4, low→critical 2, none→critical 2).

**The caveat, and why it is a shipped field rather than page copy.** A ranked list of promotions reads as a list of things that got more dangerous. It is not one, so cvss_changed now ships:

> A rescore changes the SCORE, not the flaw: the software is exactly what it > was, someone's published opinion of how severe it is moved, and no rescore is > evidence that any system is exposed.

as its own caveat key, inside its filterNote, and inside headline.rescores.note, so a surface that renders any one of the three prints it. It is pinned word for word in research/test_cvss.py and asserted present on all three surfaces by research/check_snapshot.py. This is why cvss_changed sits in the priority group and not exposure, and it is never deleted.

**Pinned**: research/test_cvss.py, 12 cases, wired into scripts/gate.sh — one per promotion shape including all four none -> x (a 0.0 score is falsy in every language this data passes through, and a truthiness test would drop the family), every demotion shape as negative and unranked, the within-band case (bandsCrossed: 0, bandJump: null), an unreadable band as null rather than zero, the destination-then-distance ordering, the rollup's partition and its record grain, and the quarantined cvss_version_set_changed row counted in nothing. research/check_snapshot.py gained a rescores block asserting the rollup against the shipped rows rather than trusting it.

2026-09-04 Audited scope, interval clocks and independent comparison coverage

Generated by python3 audit/2026-09-04-implementation/release-changes.py --write. The before snapshot is the committed takeover baseline. This run includes both a source refresh and classifier repairs; their aggregate difference is not attributed solely to either. Frozen audit samples remain tied to their original candidate.

KindBefore records or advisoriesAfter records or advisoriesBefore rowsAfter rows
fix_version_moved (records)263235401375
affected_range_extended (records)1,0731,0731,1551,155
added_to_kev (records)445446446447
cvss_changed (records)1,5341,5341,5341,534
due_date_changed (records)3333
forensic_triage_required (records)380380
product_added (records)4,3522,60511,8307,598
ransomware_confirmed (records)105107105107
removed_from_kev (records)2222
required_action_changed (records)9999
state_changed (records)973976973976
advisory_fix_moved (advisories)139141161163
advisory_withdrawn (advisories)380380380380
advisory_package_added (advisories)1,0591,0591,6951,695
advisory_severity_changed (advisories)2,8002,8002,8002,800

First-fixed boundary increases: 235 records / 53,358 records stating a first-fixed boundary = 0.44%. The same findings occupy 375 release-branch rows. Last-affected boundary increases: 1,073 records / 39,090 records stating a last-affected boundary = 2.74%. These state no fix.

The records, rows, advisories and adjacent events above are separate units.

Product additions require affirmative affected status. Identity rewrites, existing release-line labels, and recognized variants below already affected parents are refused with receipts. The variant rule is conservative and may miss real scope additions. Affirmative publisher evidence excludes an unshipped release from the installable-fix narrative. KEV forensic-field introductions are refused when the existing instruction already required triage. Neither repair widens a denominator.

Interval endpoints are separate: original-value replacement, first counted upward movement for incidence, and first appearance of the reported current value. KEV edits measure the observed prior field interval. Missing starts or history gaps withhold exact durations. Timestamp compression now converts UTC offsets rather than dropping them. No interval dates when the earlier value became wrong.

GHSA scoring-version additions/replacements are separated from rescoring an existing CVSS version. Same-branch pairs survive unrelated sibling changes; Go pseudo-versions follow their prerelease ordering. Adjacent first-fixed transitions are a separately labelled operational population with current, superseded, reversed or unconfirmed status. They never contribute to the publication-to-current headline.

Comparison membership is independent of emitted findings. Unknown IDs and packages cannot produce a clean check. Pooled publisher denominators come directly from the engine population. Branch identity, confidence, refusal and interval metadata survive every shared adapter. Lossless dictionary columns preserve the existing byte ceiling.

Saved browser/CLI review capsules capture explicit inputs, source commits, coverage and semantic evidence fingerprints. Saving or closing a page does not acknowledge evidence. Late or undated evidence enters the queue; absent evidence does not imply retraction. Export and import are customer controlled. Unsupported follows are refused.

Source freshness requires successful upstream observations, not a recent rebuild. Dependency audit failures stop the gate. Prepared workflows remain inactive remotely until the owner authorizes the push and configures delivery. No deploy or publication was performed as part of this local repair.

Source heads for this comparison: - cvelistV5: 2264869573be46ec93b7adbf6c959fe81ee4fb6b; observed 2026-09-04T23:16:02.104916+00:00. - kev: ac9d37166471fbed6a03dff6c84f05e9c6b1d2c2; observed 2026-09-04T23:16:03.291184+00:00. - ghsa: 773157cb8dbb51ab5e9d2da0dcfc36a186d3d032; observed 2026-09-04T23:16:05.659808+00:00.

2026-09-03 Phase 1: the engine says what the site promises

Mirror cvelistV5 344647659eb1 (2026-09-03T21:53Z), kev-data 782e9b631fbe, advisory-database c8bfe3111d4e. Engines 2026.09.03, snapshot schema 1 -> 2. Before figures are the snapshot committed at 4dff989, built from the same mirror heads, so every difference below is the method and nothing else.

fix_version_moved

beforeafter
records2,381263
rows4,052401
denominator70,298 records with any orderable boundary53,060 records stating a FIX (lessThan or an unaffected start)
rate3.39%0.50%
median lag (records)82.4 d22.0 d (28.1 d excluding same-day; 16.4 d excluding bulk commits)
same-day records26228
records in bulk commits (>= 50 rows)1,71854
edit events (cna, commit)not computed275; largest Patchstack ca4181d3623e 507 records (affected_range_extended)
by year (rate of records stating a fix)1.18% / 2.87% / 5.15% / 3.11%2023 0.25% (22/8,775) · 2024 0.28% (33/11,692) · 2025 0.51% (59/11,676) · 2026 0.71% (149/20,917)
excluding the two largest publishers (microsoft, PostgreSQL)99 / 48,140 = 0.21%

Why it fell, in order of size (rows quarantined, totals.quarantined_by_class):

classrowswhat it is
corrected_to_shipped_build (new)1,375a build number (4+ components) whose last component alone moved within 14 days of publication — Microsoft's Patch Tuesday build corrections (.9115 -> .9121)
description_already_stated (new)1,294the record's own description named the later value on the day it was published (Apple 17.7 / "fixed in 17.7.4"; Adobe)
other_branch_advanced (redefined)925every boundary stated at publication is still stated; only a newer one was added (Red Hat's second container tag; Microsoft's second build on one line)
product_line_renumbered650unchanged rule, now applied per branch
branch_restructured (new)457a branch vanished and another appeared; nothing can be matched
operator_switched (new)358<= X rewritten as < Y with Y the same or the next release
version_respelled (new)206CERTVDE V6_0_0_7 -> V6_00_07 (198), twcert 4.5-149 -> 149 (8)
prerelease_boundary (new)32a beta or release candidate on either side
boundary_below_start (new)8the publication range's boundary sat below its own start (10.0.25398.0 < 10.0.22621.5472): an empty, malformed range
version_scheme_changed (new)3calendar versioning on one side only

And the split: 1,214 of the old 2,381 records had only a lessThanOrEqual boundary at publication and are now affected_range_extended, not fix moves.

Per-branch keying also ADDED rows the maximum-per-product engine could not see: a move on an older branch while a newer branch's boundary stayed put (CVE-2024-23222, WebKit, in KEV: macOS 12.7 -> 12.7.3 is now emitted; on that record the description named 12.7.3 on day one, so it is quarantined as description_already_stated). 1,936 Linux records gained a measurable fix boundary from status: unaffected branch entries.

Lag: the located commit is now the first at which the REPORTED value appears on the SAME branch. CVE-2025-43378 15.7 -> 15.7.2 moved from 43.8 d (a commit that only added a < 26.1 line) to 149.7 d.

affected_range_extended (new kind)

1,073 records / 1,155 rows of 38,941 records stating a last-affected version (lessThanOrEqual) at publication: 2.76%. Median 342.0 d (91.7 d excluding bulk commits). 909 rows quarantined under the same classes. Its decision sentence is "our version is above the last affected one"; it is never rendered under a fix-version heading. Patchstack (542 records in 2025) and Wordfence carry most of it, in three April-2026 re-sync commits.

The other cvelistV5 kinds

Unchanged in count: product_added 4,352 records / 11,830 rows (8,295 quarantined), cvss_changed 1,534 (247 quarantined), state_changed 973. They are now classified in the same pass as the boundary rows (one loop, one classifier).

KEV

New kinds forensic_triage_required (38 entries: the forensicTriage field appeared in kev-data commit 2d5aab3aef54 on 2026-09-01 under BOD 26-04, set retroactively) and required_action_changed (9). New quarantine class date_added_corrected (3 of the 21 due-date changes: the entry date and the deadline moved by the same delta). due_date_changed counted 6 -> 3. Relabels (shortDescription 46, notes 36, vulnerabilityName 24, product 20, vendorProject 11, cwes 5) are counted and never events. window.maxGapDays 17.9, 25 gaps over five days; every row carries observedAtIsSyncTime. Lag median 308.0 d over 1,635 (unchanged from Phase 0).

GHSA

range_restructured refuses only the rewritten branches (a vanished key; a survivor that absorbed or split a sibling) instead of every pair on the package: advisory_fix_moved 153 -> 161 rows across 133 -> 139 advisories (the eight new rows are fastify, xstream, docker x2, wagtail, graphql x2, quarkus-vertx); range_restructured_quarantined 1,100 -> 1,679 (no longer padded by 722 unchanged pairs; now includes splits); fix_version_retreated 263 -> 268. advisory_package_added 2,022 -> 1,695 verified + 327 quarantined (already_named_at_publication 289, ecosystem_relabelled 27, name_normalised 11). advisory_severity_changed 2,800 = first_scored 1,553 + band_relabelled 243 + rescored 1,004 (severitySubkind on every row). advisory_withdrawn 380 = duplicate 227 + retracted 94 + other 59 (withdrawnAs). Every kind is now dated by replay; fixMoveLag median 60.7 -> 58.1 d (buckets on the shared edges), severityChangeLag median 59.7 d, packageAddedLag median 582.7 d.

Score disagreement

Catalog-wide (every record under cves/2023..2026 at HEAD, 168,424) instead of the 5,917 records that happened to survive the old byte budget: rows 10 -> 574; band crossings 10 -> 491 (the second scorer higher in 140, the vendor higher in 351); 1,906 records doubly scored on a shared CVSS version. Three CNA placeholder scores (0.0 with an all-N vector) refused as vendor_placeholder_zero. The scorer is named (secondScorer: CISA-ADP 1,289 pairs / 392 crossings, redhat-SADP 617 / 99); the word "independent" is gone.

The file

Schema 2. Row arrays are columnar per kind; commits are 12-character shas; timestamps at second precision; generatedAt is the build time. Every verified row ships (product_added was 30% shipped under the old byte budget); quarantined rows are sampled at 250 per class. 5,593,166 bytes. research/refresh.py runs every engine with explicit mirror paths and writes out/manifest.json; research/check_snapshot.py runs in the gate.

Validation

research/validate.py --seed 2026-09-03 drew 30 of the 401 counted rows and shipped their four states; classified by hand against them: 25 clean, 4 clean but corrected within a day, 1 ambiguous (reactor-netty, where a sibling branch retreated in the same edit), 0 hard false positives — Wilson 95% 0.0%–11.3%. A first draw on the same seed, before the fixes below, had 3 hard false positives in 30 (Red Hat's second container tag and Microsoft's second build on one line read as moves; a Microsoft range whose boundary sat below its own start). Those became the still-stated rule, boundary_below_start, and a dedupe of one fix stated twice (the Linux CNA's affected range and its unaffected twin), and moved the headline 275 -> 263 records, 481 -> 401 rows.

Not done in this phase

Scope stays by CVE-ID year; ~10,292 pre-2023 IDs first published inside the mirror's history are not compared (documented in multiyear.py).

The one section to read before quoting any of this about a named company.

A change shown here is a change to a public record, evidenced by a commit anyone can read in the publisher's own history. It is not an assertion of wrongdoing, negligence or bad faith by any publisher or vendor, not evidence that any fix was incomplete, and not a statement about anyone's systems.

Nothing here is professional, security, legal or compliance advice, and nothing here is a substitute for the vendor's own advisory or for a scanner. It reports what a published record said and what it says now. It does not know what you run, and it cannot tell you whether you are exposed.

Raising a fix version when the first patch proves incomplete is the right thing to do, and a record that gets corrected is a record being maintained. What this site measures is the absence of a notification, not the quality of anybody's engineering.

Terms of use · Privacy · Sources, licences and marks · Dispute a finding