CVE triage: bands, budgets, and what a deferral costs
This is the answer to "what is your triage latency?" — the question a CRA-adjacent reviewer asks, and the one a queue of open issues cannot answer on its own.
Two separate things watch the CVE queue, and it matters which is which:
| Question it asks | What it blocks | Where | |
|---|---|---|---|
| CVE-response gate | Has every disclosure been looked at? | the release tag | .github/workflows/release.yml |
| Ageing gate | Has anything been sitting too long? | nothing | .github/workflows/cve-watcher.yml, job age-gate |
The ageing gate blocks nothing on purpose. A release that cannot be cut is what
turned cve-deferred into the cheapest way out of the first gate; a second gate
holding the same lever would produce the second such label. It is a standing red
check, and its only job is to make latency visible without making it expensive to
hide.
Severity bands
Bands are the CVSS v3.1 qualitative ratings, parsed from the CVSS score in the
issue title and applied as a sev/* label when the watcher opens the issue.
| Band | CVSS | Label | Age budget |
|---|---|---|---|
| Critical | ≥ 9.0 | sev/critical |
3 days |
| High | 7.0 – 8.9 | sev/high |
7 days |
| Medium | 4.0 – 6.9 | sev/medium |
21 days |
| Low | < 4.0 | sev/low |
60 days |
| Unknown | no score parsed | sev/unknown |
3 days |
The budget is counted from issue creation and is an allowance, not a deadline already missed: an issue exactly at its budget is still clean, one day past it is not.
sev/unknown is held to the critical budget rather than the most lenient one.
A disclosure nobody has classified is the one that should surface fastest, and
the fix — correct the title — is cheap. The alternative lets a malformed title
age out of sight forever, which is the one outcome the gate exists to prevent.
When the watcher cannot parse a score it says so in the issue body rather than
guessing a band.
Deferral
cve-deferred moves an issue from budget accounting to date accounting. It is
exempt from its band's budget and judged against the date it names instead.
That exemption is the whole content of a deferral: somebody read the disclosure, decided it waits, and said until when. Holding a dated deferral to the budget as well would mean a correctly-deferred critical fails on day four no matter what anybody does — scoring the honest disposition and the dishonest one the same.
A deferral must name a date. Any of these, in the issue body or any comment:
target date: 2026-09-30
deferred-until: 2026-09-30
**Target: 2026-09-30.**
All three spellings are accepted. The obligation is to say when, not to say it
in a particular punctuation. On a re-deferral the latest date wins; a
date-shaped string that is not a date (2026-13-45) reads as no date at all.
Past-due deferrals are treated differently by the two guards, and the difference is deliberate:
scripts/check_cve_deferrals.pyruns at tag time and only reports a passed date. A scheduling note somebody typed weeks ago must not kill an unrelated release.scripts/check_cve_ageing.pyruns on cron and fails on one. There is no tag to protect, and a deferral is only honest while the date it names is still ahead.
Current queue
As of 2026-09-12, the queue is empty: eight issues (#707–#714) opened 2026-09-09 to 2026-09-11 and all eight were dispositioned on 2026-09-12.
Seven were in scope and six needed no new rule:
| Issue | CVE | Disposition |
|---|---|---|
| #707 | CVE-2026-87911 (CRITICAL 9.6) | awslabs.postgres-mcp-server floor was already 1.1.7. Only the rule's threat description moved — from "incomplete SQL disallowed-input list" to COPY ... TO PROGRAM OS command injection, and the severity from MEDIUM to CRITICAL. |
| #711 | CVE-2026-78573 (CRITICAL 9.8) | Default credentials in ContextForge 1.0.0–1.0.7, already under the 1.0.9 floor. Rule severity raised to CRITICAL. |
| #710 #712 #713 | CVE-2026-85025, CVE-2026-78575, CVE-2026-81941 | All scoped Langflow 1.0.0–1.11.5. One floor bump, 1.11.3 → 1.11.6. |
| #709 | CVE-2026-88938 | knowns through 0.33.0, which is the newest published release. No fix exists, so the pin became presence-only. |
| #708 | CVE-2026-87913 | The only new rule: awslabs.security-agent-mcp-server < 0.2.0. |
#714
(CVE-2026-89622) was closed as out of scope: it is a use-after-free in the
Linux kernel's HID: mcp2221 driver. MCP2221 is a Microchip USB-to-I2C bridge
chip; the acronym collides with Model Context Protocol and nothing else does.
The watcher matched on the string, which is the correct bias for a watcher.
The lesson from this wave is the mirror of 2026-09-08's. That one moved because a disposition was wrong. This one moved because most of the queue was already covered — two CVEs were under existing floors and three more were a single floor bump on a package already pinned. Reading the queue against the pin table before reaching for new rules turned what looked like eight rules into one.
Two registry lookups changed what shipping meant again: Langflow 1.11.6 exists
and is the real floor, and knowns has no fixed release at all, so a floor
would have been a fiction.
Historical: as of 2026-09-08
At that point the ageing gate was green — every open issue inside its budget.
| Band | Open | Deferred |
|---|---|---|
sev/critical |
0 | 0 |
sev/high |
1 | 1 |
sev/medium |
1 | 1 |
sev/low |
0 | 0 |
| Total | 2 | 2 |
Ten issues closed on 2026-09-08. Nine shipped coverage — five new pins plus one
raised floor, see CHANGELOG.cves.md — and one closed as out of scope because
SiYuan has no pinnable artifact on any registry this scanner reads.
That wave included #656
(CVE-2026-81096, CVSS 10), which had been open since 2026-08-27. It is worth
naming why it sat: the deferral was waiting on a generic deny-list-sandbox
detector, which is a rule to design. But the CVE itself had a vendor fix at
tooluniverse 1.3.0 the whole time, and pinning it is a complete answer to the
disclosure. Holding a CRITICAL issue open for five weeks against a detector that
is not a CVE response confused two different pieces of work. The detector is now
tracked on its own issue, where a date means something.
Both remaining issues are deferred to 2026-09-20 and are one piece of work: #693 (CVE-2026-85666, OGX) and #699 (CVE-2026-86122, Rowboat) are the same class — a destination taken from MCP configuration and fetched server-side with no link-local/metadata guard — and one rule covers both. Neither product is pinnable: OGX is identified by commit rather than release, and Rowboat's PyPI and npm names belong to unrelated projects. A rule is the only available response, which is why these two are the ones that stayed.
This section states a queue depth on a date and is not regenerated. Read the open
cve-responseissues for the live count, and theage-gatejob for whether it is currently inside budget. Shipped coverage and its timing live inCHANGELOG.cves.md; the disclosure-to-rule latency figure is generated intodocs/cve-latency.md.
What released the 2026-09-08 wave
Worth recording, because it is the same lesson as 2026-09-04 and it is not about time. Every one of those issues was dispositioned "a bug inside a third-party server's own binary, no consumer-side signal". That is true of the defect and false of the dependency — and the dependency is what this scanner pins. Re-reading the queue against that distinction moved nine issues. Three registry lookups then changed what shipping meant: one package had no fixed release at all, one needed a floor a release above anything NVD or GitHub stated, and one existing pin was already calling three vulnerable releases patched.
Clearing an issue
Every open cve-response issue ends in exactly one of these, per
RELEASING.md §5:
| Disposition | Action |
|---|---|
| rule shipped | close (completed), citing the rule and the CHANGELOG.cves.md row |
| out of scope / unreachable | close (not planned), one-line reason, label wontfix-static |
| in scope, rule queued | stays open, label cve-deferred, dated disposition comment |