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.
eventer-4.4.2.mp4
The studio console: paste an advisory, watch the reversal stream live, then read the tier-graded Nuclei template.
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), 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, 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).
Ethics & scope.
cve-reverserreverses 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.
Three ROLES, configured by env (never a fixed model name — see .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 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'smake_model_tool(chat + transient-retry + validate + circuit-breaker), validated by the pure-Python nucleipostprocess.CR_TEMPLATE_BACKEND=harness(future): delegate template authoring to a mature downstream harness via an rlm-kitmake_harness_tool. Only thechat_fnbehind the tool changes; the planner, the judgement-onlyPlannerResult,assemble_result,render, andrl_exportare all untouched. It is a documented stub today (NotImplementedErrorwith 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.
- 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. SetsTemplateItem.valid. - Authoritative, optional (
nuclei_binary.py): if thenucleibinary is on PATH,nuclei -validateruns 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.
- A lead is listed from the Patchstack database (
feed.py) — the same source the video author uses.todaypulls 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. ReverseCVEruns as an RLM inside aTraceRecorder. The planner reads the reversing skills,fetch_urls 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 thegenerate_nuclei_templateloop until the detector is valid and fires on the bug. It SUBMITs a judgement-onlyPlannerResult. No official patch? (plugin closed/abandoned) it reverses from the vulnerable source directly and synthesizes a labelled recommended fix.- 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 —
<outdir>/<YEAR>/<CVE_ID>/:README.md(analysis),poc.md,report.md,nuclei.yaml,changes.diff, plus ourrun-report.md(the trajectory). The JSONL trace +responses/{id}.jsonare 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.
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:
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 datasetsbrew 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 runEach run writes a per-CVE deliverable folder <outdir>/<YEAR>/<CVE_ID>/ (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:
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 <YEAR>/<CVE_ID>/ and, for a CVSS ≥ 9.0 unauthenticated bug, opens a [CRITICAL] issue.
Every run's trajectory is on disk, so rl_export turns it into reward-free training data:
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.
- Three roles, referred to by role, set by env. Judgement-only SUBMIT + assemble-on-read (a
self-reported
validcan 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 likeplan-reversal-strategy/verify-before-finalize). - CLI owns all artifacts, re-renderable from the trace;
import cve_reverseris dspy-free (lazy__init__); pluggable fetch + evidence Oracle bridge; reward-freerl_export; therun_startmeta 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.
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:
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 testsuv 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-kitMIT.