## https://sploitus.com/exploit?id=18B47518-E19D-5895-934A-7DC8CABAEB2B
# cve-reverser
**Reverse publicly disclosed WordPress plugin CVEs from their patches into a local-lab PoC and a
Nuclei detection template. A traced, trainable RLM agent, not a one-shot script.**
`cve-reverser` reads a WordPress plugin's already-published security patch, works out which
vulnerability the fix closes, and produces two artifacts: a proof-of-concept you can run against a
local lab you stand up yourself, and a defensive Nuclei template that detects the flaw. It fetches the
advisory, diffs the patch, reasons about the root cause, and drafts the detector on its own.
The workflow is inspired by NahamSec's *"I Made an AI Agent That Reverses CVEs While I Sleep"*
([youtu.be/C7TbsqTmzWs](https://youtu.be/C7TbsqTmzWs)), which wired a Claude Code Skill to reverse a
fresh critical CVE each morning. A Skill runs, writes files, and forgets. `cve-reverser` keeps the
trajectory instead: it records **every planner turn, tool call, and sub-model escalation** to a
replayable JSONL trace, exportable as a reward-free SFT/RL dataset. The reasoning is inspectable and the
policy is trainable on its own runs.
Built on [`rlm-kit`](https://github.com/qazbnm456/rlm-kit), a small, reusable scaffold over DSPy
Recursive Language Models (RLM). `cve-reverser` is a downstream **consumer** of it and follows a set of
hard-won conventions from that scaffold (see [Conventions](#conventions)).
> **Ethics & scope.** `cve-reverser` reverses **publicly disclosed** CVEs from their
> **already-published** patches (the diff is public the moment the plugin ships the fix). Every PoC
> targets a **local WordPress lab you stand up yourself**; the Nuclei templates are **defensive
> detection** artifacts. It **never executes, scans, connects to, or attacks anything**. It reads source
> and authors text. This is the same authorized-research and bug-bounty-practice workflow the video
> demonstrates.
## Model roles
Three ROLES, configured by env (never a fixed model name β see [`.env.example`](.env.example)):
| Role | Env | Job |
|---|---|---|
| planner (`CR_ROOT_LM`) | your choice | cheap orchestrator β drives the fetchβdiffβanalyseβspec loop |
| lifeline (`CR_SUB_LM`, `llm_query`) | your choice | expensive security brain for a gap the planner can't fill. **Never writes the template.** |
| generator (`CR_GENERATOR_LM`) | your choice | the **only** author of template YAML β reached through a tool (**the seam**, below) |
## The generator seam β self-authoring now, harness-delegated later
The template generator is a single **tool seam** (`generator_tool.py`) so it can be swapped without
touching anything else:
- **`CR_TEMPLATE_BACKEND=self`** (default, open-source-ready): a general model authors the YAML from the
planner's structured spec β the video's "model self-authors" approach β wired via rlm-kit's
`make_model_tool` (chat + transient-retry + validate + circuit-breaker), validated by the pure-Python
nuclei `postprocess`.
- **`CR_TEMPLATE_BACKEND=harness`** (future): delegate template authoring to a mature downstream harness
via an rlm-kit `make_harness_tool`. Only the `chat_fn` behind the tool changes; the
planner, the judgement-only `PlannerResult`, `assemble_result`, `render`, and `rl_export` are all
untouched. It is a documented stub today (`NotImplementedError` with a pointer) β the seam is visible,
not silently missing β so the swap lands cleanly when it ships.
The planner **never carries YAML**: it SUBMITs judgement + a `template_id` key, and the system re-sources
the verbatim YAML from the generator call and derives validity from `postprocess` on read
(`assemble_result`). Generator-only-author is therefore *structural*, and the SFT SUBMIT turn stays clean.
## Two kinds of validation
- **Format** (`postprocess.py`, deterministic, zero-LLM, no subprocess): extract YAML β `yaml.safe_load`
β structural checks (id / info / β₯1 protocol block with matchers) + a few gates (duplicate keys,
canonical author, CVE relevance). Safe to run **inside** the sandboxed RLM tool. Sets `TemplateItem.valid`.
- **Authoritative, optional** (`nuclei_binary.py`): if the `nuclei` binary is on PATH, `nuclei -validate`
runs **host-side, post-run** (from the CLI) and its verdict is appended to the report. **Never in-loop**
β a subprocess from inside the live dspy.RLM/asyncio process reliably hangs (a hard-won lesson).
- **Semantic** (the planner): whether the template fires on *this* bug (`matches_patch`) is the planner's
judgement, grounded in the diff β not decided by code.
## What one run does
1. A **lead** is listed from the **Patchstack** database (`feed.py`) β the same source the video author
uses. `today` pulls the current High/Critical (CVSS β₯ 7) WordPress **plugin** vulns, keeps those
disclosed in the last ~24h that are **not** admin-only, orders them highest-CVSS-first (leaning to
unauthenticated + juicy classes), and caps at 10. A deterministic host-side step; nothing qualifies β it stops.
2. `ReverseCVE` runs as an RLM inside a `TraceRecorder`. The planner reads the reversing skills,
`fetch_url`s the **vulnerable** (and, when a patch exists, the **fixed**) source from wordpress.org
SVN, **diffs** them in the sandbox, traces **source β sink**, builds a **local-lab PoC**, and drives
the `generate_nuclei_template` loop until the detector is valid and fires on the bug. It SUBMITs a
judgement-only `PlannerResult`. **No official patch?** (plugin closed/abandoned) it reverses from the
vulnerable source directly and synthesizes a labelled recommended fix.
3. The system assembles the canonical result (attaching the generator's verbatim YAML + validity) and the
CLI writes a **per-CVE deliverable folder** β the video author's layout β `///`:
`README.md` (analysis), `poc.md`, `report.md`, `nuclei.yaml`, `changes.diff`, plus our `run-report.md`
(the trajectory). The JSONL trace + `responses/{id}.json` are kept flat for replay/export.
The fetched patch/source is threaded to the closed models via an **evidence "Oracle bridge"**
(`evidence.py`) so they author/answer from the real code, not stale training.
## Try it offline (no model, no sandbox)
`render` / `response` / `export` operate on a recorded trace β `response` and `export` handle even a
failed/cancelled run, while `render` needs one that finalized. A realistic sample ships in
[`examples/sample_trace.jsonl`](examples/sample_trace.jsonl):
```bash
python examples/make_sample_trace.py # regenerate the sample (no live model)
python -m cve_reverser render examples/sample_trace.jsonl CVE-2026-00000 # the "what happened" report
python -m cve_reverser response examples/sample_trace.jsonl CVE-2026-00000 # the API-shaped JSON
python -m cve_reverser export "examples/sample_trace.jsonl" dataset.json # reward-free SFT/RL datasets
```
## Run it live
```bash
brew install deno
cp .env.example .env # set CR_ROOT_LM / CR_SUB_LM / CR_API_KEY (+ CR_BASE_URL), CR_INTERPRETER=deno
set -a && source .env && set +a
python -m cve_reverser one CVE-2026-12345 some-plugin --vuln 3.1.5 --fixed 3.1.6 # one named CVE
python -m cve_reverser today # today's High/Critical plugin CVEs (Patchstack)
python -m cve_reverser today --limit 3 # cap this run
```
Each run writes a per-CVE deliverable folder `///` (`README.md`, `poc.md`,
`report.md`, `nuclei.yaml`, `changes.diff`, `run-report.md`), plus the flat `traces/{id}.jsonl`,
`sources/{id}.txt`, and `responses/{id}.json`. `today` **skips any CVE whose folder already exists**
(deduped across runs), so cron can re-run it safely.
**"While I sleep"** β schedule `today` from cron:
```cron
0 6 * * * cd /path/to/cve-reverser && /usr/bin/env -S bash -lc 'source .env && python -m cve_reverser today'
```
**Publish to GitHub (opt-in)** β set `CR_GITHUB_REPO` + `CR_GITHUB_TOKEN` and pass `--publish`. Committing
the finished per-CVE folder is **deterministic host-side plumbing** (it runs *after* the RLM finalizes,
straight against the GitHub REST API β the planner never gets write credentials, so it stays out of the
trajectory), which keeps the sub-LM/tool split honest: only choices the policy *makes* are tools. It
commits `//` and, for a **CVSS β₯ 9.0 unauthenticated** bug, opens a `[CRITICAL]` issue.
## The improvement loop (why this beats a Skill)
Every run's trajectory is on disk, so `rl_export` turns it into **reward-free** training data:
```python
from cve_reverser import export_dataset
from rlm_kit.trace import group_by_run, load_events
ds = export_dataset(group_by_run(load_events("traces/CVE-2026-12345.jsonl")))
# ds["generator"] β single-turn specβYAMLβverdict records (SFT subset = ok and is_final; GRPO = all)
# ds["sft_turns"] β per-planner-turn SFT samples (the RLM recipe, arXiv 2512.24601)
# ds["actions"] β every planner/tool/lifeline action, in order
# ds["labels"] β per-run complete/valid (the SFT keep-filter) β FACTS, from postprocess
# ds["metrics"] β steps/generator_calls/gave_up_early/hit_iteration_cap β¦ (a trainer's reward raw material)
```
It never computes a reward: composing a reward and training (GRPO/SFT) is a separate downstream project.
This repo is the **rollout** stage.
## Conventions
- **Three roles**, referred to by role, set by env. **Judgement-only SUBMIT** + assemble-on-read (a
self-reported `valid` can never drift from the bytes). **Generator-only-author** made structural. A
**terse MISSION-framed prompt** (`reverse.py`) with the deep how-to in **progressive-disclosure skills**
(`skills/`, incl. cognitive-procedure skills like `plan-reversal-strategy` / `verify-before-finalize`).
- **CLI owns all artifacts**, re-renderable from the trace; **`import cve_reverser` is dspy-free** (lazy
`__init__`); **pluggable fetch** + **evidence Oracle bridge**; **reward-free `rl_export`**; the
`run_start` meta **self-describes the run's config** (roles, budgets, author, lead) so offline readers
read real per-run values; the **CVE-relevance gate** catches a "corrected" post-cutoff CVE.
## Layout
```
cve_reverser/
config.py ReverseConfig β three roles + flags + the template-backend seam (dspy-free)
schema.py judgement-only PlannerResult + assembled ReversalResult + the API response (dspy-free)
reverse.py ReverseCVE(RLMTask) + the terse MISSION-framed INSTRUCTIONS + setup()
generator_tool.py make_template_tool β the SEAM (self now, harness later)
postprocess.py pure-Python Nuclei format validation (in-loop safe) + CVE-relevance gates
nuclei_binary.py optional host-side `nuclei -validate` (never in-loop)
fetch_tool.py pluggable SSRF-guarded fetch (direct) + evidence
evidence.py the Oracle bridge (fetched patch β the closed models)
feed.py Patchstack lister (list_todays_cves + parse_patchstack) + wordpress.org SVN helpers
render.py assemble_result + result_from_events + the per-CVE folder (README/poc/report/diff) + run-report
response.py build_response β the API-shaped JSON
rl_export.py export_dataset + labels/metrics (reward-free)
publish.py host-side GitHub publishing of the per-CVE folder (opt-in, deterministic)
cli.py one / today / render / response / export β owns all artifacts
skills/ the reversing KB (knowledge-only, progressive disclosure)
tests/ offline tests (DummyLM + mock interpreter)
studio/ the visual console β a uv-workspace member package (its own pyproject), NOT in the
cve_reverser wheel; reads this package's traces/responses. See studio/README.md.
```
`studio/` is a separate package in the same repo (a uv **workspace member**), so a trace-schema change
here and its reader move together, yet the console's web stack stays out of the harness wheel. Launch it
from the repo root β the artifacts dir defaults to this root, no `CR_ARTIFACTS_DIR` needed:
```bash
uv run --package cve-reverser-studio uvicorn cve_reverser_studio.app:app --reload # http://127.0.0.1:8000
uv run pytest studio/tests # the console's own tests
```
## Develop
```bash
uv sync && uv run pytest # self-contained: pulls rlm-kit from git, runs the suite
uv run pytest studio/tests # the studio console's own contract tests
# Co-developing rlm-kit locally? Overlay it editable so your local edits are picked up:
# uv pip install -e ../rlm-kit
```
## License
MIT.