返回资讯中心
外部精选
软件工程
#开发者实时流

A CVE triage pipeline that keeps asking, because day one tells you nothing

Rappaport is an open-source CVE triage pipeline that computes priority in plain code and lets the language model only summarise. Built on a finding from two weeks of lab data: on the day a CVE is published there is almo…

Hacker News 最新kravmaga9992026/10/31 · 2 天前30 分钟阅读

以下正文同步自 Hacker News 最新,版权归原站所有,已转换为易读排版。

Rappaport is an open-source CVE triage pipeline that computes priority in plain code and lets the language model only summarise. Built on a finding from two weeks of lab data: on the day a CVE is published there is almost nothing to rank it by, so the real work is re-asking, not scoring.

Scope: cve-digest v0.7.26.0, unattended on a single home-lab host via a systemd timer, 16-31 October 2026.

Method: cve-digest v0.7.26.0 ranking live NVD, EPSS, KEV and OSV data unattended on one home-lab host, 16-31 October 2026, read from the lab's Splunk, plus one 878-CVE laptop window compared against a CVSS-7 threshold.

The pipeline is called Rappaport, after a scientist in Lem’s His Master’s Voice, a novel in which a team of researchers reads meaning into a signal from the stars and slowly learns that the meaning tells them more about themselves than about the signal. That is the failure I kept meeting in the field. A detection stack returns a confident verdict, and the confidence comes from how little it has seen, not from the vulnerability itself. When partial signal can corrupt the verdict, priority cannot be read off whatever a record claims about its own severity. It has to be computed from signal that actually predicts exploitation, before anything a model could be talked into gets near it.

So I built a CVE triage pipeline on one rule: the language model is the least-trusted component in it, not the oracle. Priority is decided in plain code before the model runs. The model has no tools, no write access, and no authority to change a ranking. It writes the summary and nothing else. The code is open source, on GitHub.

Then I left it running against live feeds, first once a day and then twice, and watched. The architecture held; what I had misjudged was the problem itself. I had assumed the hard part of triage was ranking what sits in front of you, but on the day a CVE is published there is almost nothing in front of you to rank it by. The real design problem turned out to be re-asking, not scoring.

Anyone who does vulnerability management for a living knows the questions that arrive with every newly published CVE. Is it one of these?

People who do this full time get lost in that list. The person handling security part time at a small company has no chance at all, and the volume only ever grows: more tooling, more scanners, more model-written advisories, all of it adding to the pile rather than to the decision. The list is not a complaint about bad data. It is a map of seven different questions wearing one label, and most tools answer whichever is cheapest and quietly drop the rest.

That is the problem I wanted to take seriously. Not to score vulnerabilities more cleverly, but to be honest about which of those seven questions a machine can actually answer, and to hand the rest to a person on purpose.

Line those seven questions up against the signals that actually exist, and the picture gets clearer and narrower at once.

Two of those rows have no machine answer: the configuration case and the undisclosed zero-day. Both need a person, and the honest move there is to flag the item and step back, not to guess.

The rest can be computed before anything that reasons in prose is involved. That severity is the wrong axis is not an opinion; it is a finding that has held up for more than a decade. A case-control study of CVSS scores against real-world exploits found that ranking on a high CVSS score alone is not an economically effective patching policy, cutting real risk only marginally. So CVSS enters the ranking only as impact context, scaled by exploitation signal, never as the thing that decides. And the one signal that can lift an item on its own, confirmed exploitation, is read from a catalogue rather than inferred by a model, because the moment a model is allowed to decide, the text it reads becomes an attack surface. That is the next section.

The thesis of the whole thing is that a prompt is not a control but a wish. The real controls are order and scope: what is computed before the model runs is out of its reach, and what it is never handed, it cannot use.

So priority is decided first, in plain code, and only then does the pipeline decide whether to spend a model on an item at all. Act-today items get a written summary; everything below gets a deterministic one-liner, so the lower tier costs no tokens and offers no attack surface. When the model does run, the prompt has two channels that never mix: trusted facts assembled by the code, and untrusted data copied from the feed, held apart so that a sentence inside a CVE description cannot pose as an instruction. The output is validated against a schema before anything downstream reads it. And if a known-exploited item carries language in its own fields that argues for its own downgrade, it is pulled out of the automated path and handed to a human, with the event logged.

The strongest design decision in the project was a subtraction. Early on, a read-only tool layer let the model look things up; I took it out and moved all bounded agency into Pirx, a separate project with its own threat model and a versioned contract between the two. The reason is simple. “The model decides nothing, ever” is a claim you can check by reading one function; “the model decides almost nothing” has already lost the argument, and no guardrailing wins it back. The claim was worth more than the capability, so the capability went.

The last piece is choosing how each part fails, one boundary at a time, instead of reaching for a single default. The estate index fails open: an unusable lookup can rank an item too high, and the next run corrects it. The ticket-deduplication index fails closed: the cost of getting that one wrong is an irreversible burst of duplicates in someone else’s system of record, so in doubt it does nothing. When a write budget is exceeded the whole batch aborts rather than writing the first few and stopping halfway. Each direction is chosen against the cost of being wrong in that exact place, and written down with its reason.

None of this is proof against prompt injection. The guardrails are threat-modelled and mapped to the usual catalogues, not yet proven by a published adversarial evaluation; that is being built, and until it exists the honest word is “threat-modelled”, not “tested”. What the pipeline can prove today is narrower and, to an auditor, more useful. The ranking is deterministic, and every run can print its own derivation rule by rule, including the rules that were evaluated and did not fire. A decision you can read line by line, down to the rules that changed nothing, is not the same thing as a model you take on trust.

The literature is clear about what to rank on. Jacobs and colleagues showed the size of the gap: a CVSS 7-and-above threshold flags about 58% of all CVEs, while ranking by the probability of exploitation reaches around 82% of what actually gets exploited at roughly 7% of that effort. Rank by probability, not by score. I built the ranker to do exactly that.

The first full day I ran it, one pass on my laptop before any of this went unattended, looked like the paper come to life. One 24-hour window from NVD, 878 freshly published CVEs. A CVSS 7 threshold would have handed an analyst 505 of them. The pipeline handed over one. But that one was promoted by its presence in CISA’s known-exploited catalogue, not by its exploitation score. On that day the compression came from KEV, and the probability model had said nothing.

Then I stopped curating days and let it run on a systemd timer in a home lab. Ten unattended days, 3,846 records ranked, and the act-today tier was empty across all ten. Not one record reached the top two priorities. EPSS had not scored nine records in ten, and the highest probability it returned over the whole stretch was 0.0126. The queue that actually formed was neither the urgent tier nor the noise a severity filter leaves behind. It was a review lane that carried no usable signal at all, because on the day of publication there was none to carry. It ran to about a hundred records a day on average, but anything but steady: over four hundred on one day, a single record on another. The effort-and-coverage curve from the literature is a property of mature CVEs. A freshly published one has not earned its place on it yet. An independent five-year evaluation reports the same thing from the other side: score EPSS on the day of publication rather than a month later, and the queue inflates several-fold while its measured efficiency collapses.

That reframed the whole problem, and it exposed three ways the pipeline was quietly losing items. Each one became a lane: on every run the pipeline reads more than the new window, and routes what it cannot yet decide back to a store the next run will ask about again.

The first was a vulnerability scored CVSS 9.4, ranked P3 one morning, entered into KEV the same day, and never seen again, because the window reads by publication date and that item had already been read. CISA adds most entries days after a CVE appears. Barracuda, measuring the catalogue, puts the median gap from publication to a KEV entry at fourteen days in 2026, up from six in 2023. That is a vendor’s figure, not an independent one, but the lag it reports is exactly the failure this lane exists to catch. So the catalogue is now re-read on every run for entries added since the last one, whatever their publication date. The lab’s first run with that lane ranked eight such entries, all P1, in under four seconds. The act-today tier came from the lane, not from the window.

The second miss was quieter. Two items scored CVSS 9.2 were ranked P3 with EPSS still pending. Days later their EPSS scores were 0.9998 and 1.0000, about as close to certain as that model goes. They carried a CVSS, so the review lane never held them, and nothing re-scored them. Now any P3 item is scored again on every run for fourteen days and reported once if it climbs. The honest epilogue is that those two particular items reached P1 through the KEV lane before the watch could lift them. A signal’s timing matters as much as its source.

The third was the estate question, is this even ours, and every way I got it wrong is worth stating. The join runs through package URLs against OSV, an exact key with no lossy name translation. It also read the wrong field for months, so Debian and Ubuntu packages could never register as present until I fixed it. Binary package names match nothing; only source names do. The per-run coverage-gap count measures how much of the window OSV can speak to, not how complete your inventory is, and treating the one as the other is an easy and expensive mistake. And an item is only ever declared absent on the strength of an operator declaring the inventory complete, never inferred from OSV staying silent. Not knowing because a lookup failed is a different fact from not knowing because no source recognises the item. The design weighs them the same, because neither is evidence of absence.

Two worries arrive together here: that something this small is just a script, and that something this careful is one more server to keep alive. Neither holds.

The triage runs in seconds. A full day of 878 published CVEs finished in about forty seconds on a laptop, most of it spent waiting on public APIs rather than computing anything. Smaller days finish faster. The only expensive part is optional: the estate index, the layer that answers whether a vulnerability is even yours, takes around seven and a half minutes to build cold. It is cached for eighteen hours, so it rebuilds roughly once a day, and a warm run reads it back in under forty seconds. Switch the estate layer off and that cost leaves with it.

There is no agent, no scanner, no daemon, no database server. One Python process and a SQLite file. It sits after whate

正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。

打开原文
去社区讨论 →