Skip to content

qazbnm456/cve-reverser

Repository files navigation

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.

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-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):

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_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 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 — <outdir>/<YEAR>/<CVE_ID>/: 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:

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

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 <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.

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:

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:

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

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.

About

An autonomous RLM agent that reverses disclosed CVEs from their public patches into local-lab PoCs and Nuclei detection templates.

Resources

License

Stars

1 star

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors