cannon-and-wall / README.md
CystronCode's picture
Update README.md
a9083ad verified
|
Raw
History Blame Contribute Delete
10.1 kB
---
title: Cannon And Wall
emoji: πŸ”΄πŸ”΅
colorFrom: red
colorTo: blue
sdk: docker
pinned: false
---
# πŸ”΄πŸ”΅ Cannon & Wall β€” RedBlue Arena
> *A self-improving attacker vs defender RL environment for training LLMs on cybersecurity reasoning.*
**Author:** Jairaj S | **Hackathon:** Meta PyTorch Γ— OpenEnv Γ— Scaler School of Technology, April 2026
**Theme:** Self-Improving Agents (Theme 4) + Multi-Agent Interactions (Theme 1)
---
## πŸ”— Links
| Resource | URL |
|---|---|
| πŸ€— HuggingFace Space (live env) | https://huggingface.co/spaces/CystronCode/cannon-and-wall |
| πŸŽ₯ Demo video (YouTube <2 min) |https://youtu.be/E63MU02_1Y4|
| πŸ“ Blog writeup (this Space) | [Blog.md](Blog.md) |
| πŸ““ Training notebook (Colab) | https://colab.research.google.com/drive/1uTSt6DahNVXoAZ0hlpyzrXrn-F6ehc9z?usp=sharing |
| πŸ“ˆ Reward curves (W&B) | https://wandb.ai/models-r-v-c-e/cannon-and-wall/runs/5v7yweib |
---
## 🧠 The Problem
LLMs are surprisingly bad at security reasoning β€” not because they lack knowledge,
but because they have never been trained to think adversarially in a loop.
Current benchmarks test security knowledge statically. There is no environment where:
- An attacker agent gets smarter by failing to find bugs
- A defender agent gets smarter by getting exploited
- Both co-evolve through self-play, round after round
**Cannon & Wall fills that gap.**
---
## 🏟️ What Is the Environment?
A sandboxed Flask web application with seeded OWASP Top 3 vulnerabilities.
Two LLM agents compete inside it:
| Agent | Role | What it does |
|---|---|---|
| πŸ”΄ **Cannon** | Attacker | Reads source code, finds vulnerabilities, proposes exploits |
| πŸ”΅ **Wall** | Defender | Reads Cannon's report, patches the code, hardens the app |
| βš–οΈ **Judge** | Verifier | Runs deterministic checks β€” no LLM-as-judge |
### Vulnerability scope
- SQL Injection (SQLi)
- Cross-Site Scripting (XSS)
- Broken Authentication
### Key design decision β€” textual reasoning only
Neither agent executes live exploits. All reasoning happens over source code as text.
This keeps the environment safe, reproducible, and trainable.
---
## πŸ” The 3-Phase Self-Play Loop
```
Phase 1 β€” ATTACK
Cannon reads the vulnerable Flask app source code
Cannon outputs: { vuln_type, line_number, explanation, proof_of_concept }
Phase 2 β€” PATCH
Wall reads the source code + Cannon's report
Wall outputs: { patched_code, explanation }
Phase 3 β€” BYPASS
Cannon reads original code + Wall's patched version
Cannon tries to find a remaining vulnerability or bypass
Judge scores both agents after each bypass phase.
Loser faces a harder variant next round.
Both agents improve β€” neither can memorize.
```
---
## πŸ† Reward Logic
```python
# Cannon (Attacker)
+10 real vulnerability correctly identified
+5 correct vulnerability type (sqli / xss / broken_auth)
-5 false positive reported
+15 bypass succeeded (Wall's patch failed)
# Wall (Defender)
+5 per vulnerability correctly patched (up to 3)
+5 patched code still works (functionality preserved)
-5 patch introduced a new vulnerability
+5 bypass attempt failed (patch held)
# All rewards normalized to 0.0-1.0 for OpenEnv compliance
# Per-component rewards logged as separate W&B columns (rollout/attack_real_vuln_found, etc.)
```
---
## πŸ“ Environment Structure
```
cannon-and-wall/
β”œβ”€β”€ openenv.yaml # OpenEnv manifest (v1.1 β€” action_space + observation_space)
β”œβ”€β”€ openenv.py # Environment base class (local stub)
β”œβ”€β”€ app.py # FastAPI wrapper (reset / step / state)
β”œβ”€β”€ Dockerfile # Container definition
β”œβ”€β”€ requirements.txt # Dependencies
β”‚
β”œβ”€β”€ environment/
β”‚ β”œβ”€β”€ server.py # CannonWallEnvironment class
β”‚ β”œβ”€β”€ models.py # Pydantic schemas
β”‚ β”œβ”€β”€ curriculum.py # Stage progression logic
β”‚ β”œβ”€β”€ vulnerable_app/
β”‚ β”‚ β”œβ”€β”€ stage_1/app.py # Explicit SQLi + XSS + Broken Auth
β”‚ β”‚ β”œβ”€β”€ stage_2/app.py # Split routes, aliased variables (medium)
β”‚ β”‚ └── stage_3/app.py # Chained + obfuscated vulns (hard)
β”‚ └── judge/
β”‚ β”œβ”€β”€ verifier.py # AST-level SQLi check + bandit static analysis
β”‚ └── reward.py # Multi-component reward calculator
β”‚
β”œβ”€β”€ client/
β”‚ β”œβ”€β”€ client.py # CannonWallClient (httpx)
β”‚ └── models.py # Client-side Pydantic models
β”‚
β”œβ”€β”€ agents/
β”‚ β”œβ”€β”€ cannon_prompt.py # Red agent system prompt + helpers
β”‚ └── wall_prompt.py # Blue agent system prompt + helpers
β”‚
β”œβ”€β”€ training/
β”‚ └── train_grpo.ipynb # TRL + Unsloth GRPO training notebook
β”‚
└── ui/
└── demo.py # Gradio live demo + leaderboard
```
---
## πŸ›‘οΈ Anti-Reward-Hacking Measures
- Judge is **fully deterministic** β€” AST-level checks + bandit static analysis (no LLM-as-judge)
- **Multiple independent reward components** β€” gaming one does not help overall score
- **Proof-of-concept validation** β€” Cannon must provide a working exploit pattern
- **Hard episode limit** β€” MAX_ROUNDS=3, prevents infinite loops
- **Dangerous import rejection** β€” patches with `import os`, `exec()`, `eval()` rejected instantly
- **Functionality preservation check** β€” Wall must not break the app to score
- **AST-level SQLi verifier** β€” parses patched code to confirm parameterized `execute()` is actually used, not just that `?` appears in a comment
---
## πŸŽ“ Curriculum
| Stage | File | Task | Vulnerabilities |
|---|---|---|---|
| 1 | `stage_1/app.py` | Single-file login form | SQLi + XSS + Broken Auth (explicit f-string) |
| 2 | `stage_2/app.py` | Split `/auth` + `/search` routes | SQLi (string concat) + XSS (aliased variable names) |
| 3 | `stage_3/app.py` | Chained + obfuscated portal | SQLi (string join) + XSS (inside href attribute) + Broken Auth (cookie override) |
Escalation triggers when Wall's raw reward exceeds 8 for 3 consecutive episodes (agent is no longer challenged).
---
## πŸ› οΈ Training Stack
| Component | Tool |
|---|---|
| RL algorithm | GRPO via HuggingFace TRL |
| Efficiency layer | transformers + peft + bitsandbytes (4-bit QLoRA) |
| Base model | Qwen/Qwen2.5-3B-Instruct |
| Environment | OpenEnv (Docker, HF Spaces) |
| Experiment tracking | Weights & Biases (per-component reward columns) |
| Deployment | HuggingFace Spaces (Docker) |
---
## πŸ”¬ Before vs After Training β€” Qualitative Example
| | Untrained (step 0) | Trained (step 50) |
|---|---|---|
| **vuln_type** | `"xss"` (wrong) | `"sqli"` (correct) |
| **line_number** | `5` (no vuln there) | `16` (correct β€” f-string query) |
| **proof_of_concept** | `"<script>test</script>"` | `"' OR 1=1-- "` |
| **Judge verdict** | False positive (βˆ’5 pts) | Real vuln + correct type (+15 pts) |
| **Cannon reward** | 0.000 (normalized) | 0.571 (normalized) |
The untrained model guesses XSS on the wrong line. After GRPO training it correctly identifies the SQLi on line 16 with a valid bypass PoC.
---
## πŸ“ˆ Results
### Reward curve β€” 50 GRPO gradient steps
![Reward curve](assets/reward_curve.png)
*X-axis: GRPO training step (each step = 4 completions sampled β†’ environment reward β†’ `optimizer.step()`).
Y-axis: normalized reward (0 = worst, 1 = best).
Dashed line: random-agent baseline (0.020) β€” random JSON with valid PoC patterns, no model reasoning.
Cannon reward rises from near-zero baseline to ~0.57 within 50 steps β€” a 25Γ— improvement demonstrating real learning.
Wall remains stable at ~0.71 throughout β€” it patches correctly regardless of Cannon's quality.*
### Quantitative results
| Metric | Value |
|---|---|
| Cannon reward at step 0 (random baseline) | 0.020 |
| Cannon reward at step 50 (GRPO trained) | 0.570 |
| Improvement over baseline | **25Γ—** |
| Wall reward (stable throughout) | ~0.714 |
| Wall patch validity rate | 100% |
| GRPO gradient steps | 50 |
| Completions per step (G) | 4 |
| Base model | Qwen/Qwen2.5-3B-Instruct |
| LoRA rank | 16 |
| Training time (Colab T4) | ~35 min |
> **Training note:** Each step samples G=4 completions from the model, queries the live
> environment for rewards, computes group-relative advantages (r_i βˆ’ mean(r)), and calls
> `optimizer.step()` to update the LoRA adapter weights. This is real gradient-based
> learning β€” not an inference evaluation loop.
---
## ▢️ Running Locally
```bash
git clone https://huggingface.co/spaces/CystronCode/cannon-and-wall
cd cannon-and-wall
pip install -r requirements.txt
python app.py
# Test stage routing
curl -X POST "http://localhost:7860/reset?stage=1" # loads stage_1/app.py
curl -X POST "http://localhost:7860/reset?stage=2" # loads stage_2/app.py (split routes)
curl -X POST "http://localhost:7860/reset?stage=3" # loads stage_3/app.py (chained vulns)
```
---
## πŸ”Œ API Endpoints
| Method | Endpoint | Description |
|---|---|---|
| POST | `/reset?stage=1` | Start new episode, returns source code |
| POST | `/step` | Send agent action, returns reward + observation |
| GET | `/state` | Read current episode state |
| GET | `/docs` | FastAPI Swagger UI |
---
## πŸ’‘ Why This Matters
Security is one of the few domains where verification is fully objective,
self-play is naturally adversarial, and the task is genuinely hard for current LLMs.
A model trained in Cannon & Wall learns to reason about code vulnerabilities through
adversarial rounds β€” not through static examples. The AST-level verifier and
multi-component reward make it hard to game without actually improving security reasoning.
---
## πŸ“„ License
MIT
---
*Built for the Meta PyTorch x OpenEnv Hackathon, Scaler School of Technology, Bangalore β€” April 2026*