Spaces:
Sleeping
Sleeping
Commit ·
9556146
1
Parent(s): 8722538
initial deploy — jairaj files, teammate files pending
Browse files- Dockerfile +6 -0
- README.md +0 -10
- __pycache__/openenv.cpython-313.pyc +0 -0
- agents/__init__.py +0 -0
- agents/__pycache__/__init__.cpython-313.pyc +0 -0
- agents/__pycache__/cannon_prompt.cpython-313.pyc +0 -0
- agents/__pycache__/wall_prompt.cpython-313.pyc +0 -0
- agents/cannon_prompt.py +8 -0
- agents/wall_prompt.py +8 -0
- client/__init__.py +1 -0
- client/__pycache__/__init__.cpython-313.pyc +0 -0
- client/__pycache__/client.cpython-313.pyc +0 -0
- client/client.py +17 -0
- environment/__pycache__/curriculum.cpython-313.pyc +0 -0
- environment/__pycache__/models.cpython-313.pyc +0 -0
- environment/__pycache__/server.cpython-313.pyc +0 -0
- environment/curriculum.py +18 -0
- environment/judge/__init__.py +1 -0
- environment/judge/__pycache__/__init__.cpython-313.pyc +0 -0
- environment/judge/__pycache__/reward.cpython-313.pyc +0 -0
- environment/judge/__pycache__/verifier.cpython-313.pyc +0 -0
- environment/server.py +45 -0
- environment/vulnerable_app/stage_1/app.py +55 -0
- openenv.py +22 -0
- openenv.yaml +13 -0
- training/train_grpo.ipynb +0 -0
- ui/demo.py +29 -0
Dockerfile
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.10-slim
|
| 2 |
+
WORKDIR /app
|
| 3 |
+
COPY . .
|
| 4 |
+
RUN pip install openenv flask fastapi uvicorn httpx gradio requests bandit
|
| 5 |
+
EXPOSE 7860
|
| 6 |
+
CMD ["openenv", "run", "--port", "7860"]
|
README.md
CHANGED
|
@@ -1,10 +0,0 @@
|
|
| 1 |
-
---
|
| 2 |
-
title: Cannon And Wall
|
| 3 |
-
emoji: 😻
|
| 4 |
-
colorFrom: green
|
| 5 |
-
colorTo: blue
|
| 6 |
-
sdk: docker
|
| 7 |
-
pinned: false
|
| 8 |
-
---
|
| 9 |
-
|
| 10 |
-
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
__pycache__/openenv.cpython-313.pyc
ADDED
|
Binary file (1.43 kB). View file
|
|
|
agents/__init__.py
ADDED
|
File without changes
|
agents/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (175 Bytes). View file
|
|
|
agents/__pycache__/cannon_prompt.cpython-313.pyc
ADDED
|
Binary file (733 Bytes). View file
|
|
|
agents/__pycache__/wall_prompt.cpython-313.pyc
ADDED
|
Binary file (691 Bytes). View file
|
|
|
agents/cannon_prompt.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
def build_cannon_prompt(source_code: str, stage: int, round: int) -> str:
|
| 2 |
+
return f"""You are a Red Team security analyst. Stage {stage}, Round {round}.
|
| 3 |
+
Analyze this source code and find ONE vulnerability.
|
| 4 |
+
Respond ONLY in this exact JSON format:
|
| 5 |
+
{{"agent":"cannon","vuln_type":"sqli|xss|broken_auth","line_number":0,"explanation":"...","proof_of_concept":"..."}}
|
| 6 |
+
|
| 7 |
+
SOURCE CODE:
|
| 8 |
+
{source_code}"""
|
agents/wall_prompt.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
def build_wall_prompt(source_code: str, cannon_report: dict, stage: int) -> str:
|
| 2 |
+
return f"""You are a Blue Team security engineer. Stage {stage}.
|
| 3 |
+
Red team found: {cannon_report}
|
| 4 |
+
Patch the vulnerability. Respond ONLY in this exact JSON format:
|
| 5 |
+
{{"agent":"wall","patched_code":"...full fixed file...","explanation":"..."}}
|
| 6 |
+
|
| 7 |
+
SOURCE CODE:
|
| 8 |
+
{source_code}"""
|
client/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# client package
|
client/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (175 Bytes). View file
|
|
|
client/__pycache__/client.cpython-313.pyc
ADDED
|
Binary file (1.63 kB). View file
|
|
|
client/client.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import httpx
|
| 2 |
+
|
| 3 |
+
class CannonWallClient:
|
| 4 |
+
def __init__(self, base_url: str = "http://localhost:7860"):
|
| 5 |
+
self.base_url = base_url
|
| 6 |
+
|
| 7 |
+
def reset(self, stage: int = 1) -> dict:
|
| 8 |
+
r = httpx.post(f"{self.base_url}/reset", params={"stage": stage})
|
| 9 |
+
return r.json()
|
| 10 |
+
|
| 11 |
+
def step(self, action: dict) -> dict:
|
| 12 |
+
r = httpx.post(f"{self.base_url}/step", json=action)
|
| 13 |
+
return r.json()
|
| 14 |
+
|
| 15 |
+
def state(self) -> dict:
|
| 16 |
+
r = httpx.get(f"{self.base_url}/state")
|
| 17 |
+
return r.json()
|
environment/__pycache__/curriculum.cpython-313.pyc
ADDED
|
Binary file (1.21 kB). View file
|
|
|
environment/__pycache__/models.cpython-313.pyc
ADDED
|
Binary file (1.29 kB). View file
|
|
|
environment/__pycache__/server.cpython-313.pyc
ADDED
|
Binary file (2.54 kB). View file
|
|
|
environment/curriculum.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
|
| 3 |
+
STAGES = {
|
| 4 |
+
1: {"file": "environment/vulnerable_app/stage_1/app.py",
|
| 5 |
+
"vulns": ["sqli"], "description": "Single SQLi in login form"},
|
| 6 |
+
2: {"file": "environment/vulnerable_app/stage_1/app.py",
|
| 7 |
+
"vulns": ["xss"], "description": "Single XSS in comment box"},
|
| 8 |
+
3: {"file": "environment/vulnerable_app/stage_1/app.py",
|
| 9 |
+
"vulns": ["sqli", "xss", "broken_auth"], "description": "All three vulns"}
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
def next_stage(current: int, scores: dict) -> int:
|
| 13 |
+
if scores.get("wall", 0) > 8:
|
| 14 |
+
return min(current + 1, max(STAGES.keys()))
|
| 15 |
+
return current
|
| 16 |
+
|
| 17 |
+
def get_stage_source(stage: int) -> str:
|
| 18 |
+
return Path(STAGES[stage]["file"]).read_text()
|
environment/judge/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# judge package
|
environment/judge/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (186 Bytes). View file
|
|
|
environment/judge/__pycache__/reward.cpython-313.pyc
ADDED
|
Binary file (2.85 kB). View file
|
|
|
environment/judge/__pycache__/verifier.cpython-313.pyc
ADDED
|
Binary file (6.17 kB). View file
|
|
|
environment/server.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from openenv import Environment
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
from environment.models import Observation
|
| 4 |
+
from environment.judge.verifier import verify_cannon, verify_wall
|
| 5 |
+
from environment.judge.reward import cannon_reward, wall_reward
|
| 6 |
+
|
| 7 |
+
STAGE_FILES = {
|
| 8 |
+
1: "environment/vulnerable_app/stage_1/app.py"
|
| 9 |
+
}
|
| 10 |
+
GROUND_TRUTH = {
|
| 11 |
+
1: {"vuln_type": "sqli", "line_number_range": [10, 20]}
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
class CannonWallEnvironment(Environment):
|
| 15 |
+
def reset(self, stage: int = 1):
|
| 16 |
+
source = Path(STAGE_FILES[stage]).read_text()
|
| 17 |
+
self.state = {
|
| 18 |
+
"round": 1,
|
| 19 |
+
"stage": stage,
|
| 20 |
+
"source_code": source,
|
| 21 |
+
"scores": {"cannon": 0.0, "wall": 0.0},
|
| 22 |
+
"last_cannon_report": None
|
| 23 |
+
}
|
| 24 |
+
return self.state
|
| 25 |
+
|
| 26 |
+
def step(self, action: dict):
|
| 27 |
+
agent = action.get("agent")
|
| 28 |
+
if agent == "cannon":
|
| 29 |
+
v = verify_cannon(action)
|
| 30 |
+
if not v["valid"]:
|
| 31 |
+
return {"reward": 0, "issues": v["issues"]}
|
| 32 |
+
r = cannon_reward(action, GROUND_TRUTH[self.state["stage"]])
|
| 33 |
+
self.state["scores"]["cannon"] += r
|
| 34 |
+
self.state["last_cannon_report"] = action
|
| 35 |
+
return {"reward": r, "valid": True}
|
| 36 |
+
elif agent == "wall":
|
| 37 |
+
test_results = verify_wall(
|
| 38 |
+
action.get("patched_code", ""),
|
| 39 |
+
self.state["stage"]
|
| 40 |
+
)
|
| 41 |
+
r = wall_reward(action.get("patched_code",""), [], test_results)
|
| 42 |
+
self.state["scores"]["wall"] += r
|
| 43 |
+
self.state["round"] += 1
|
| 44 |
+
return {"reward": r, "test_results": test_results}
|
| 45 |
+
return {"error": "unknown agent"}
|
environment/vulnerable_app/stage_1/app.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# stage_1/app.py
|
| 2 |
+
# Cannon & Wall -- Vulnerable Target App (Stage 1)
|
| 3 |
+
# WARNING: This file is intentionally vulnerable for security training purposes.
|
| 4 |
+
# DO NOT deploy this in production.
|
| 5 |
+
|
| 6 |
+
from flask import Flask, request, session, jsonify
|
| 7 |
+
|
| 8 |
+
app = Flask(__name__)
|
| 9 |
+
app.secret_key = "hardcoded_secret_123"
|
| 10 |
+
|
| 11 |
+
# Lines 10-20: SQLi vulnerability lives here
|
| 12 |
+
@app.route("/login", methods=["POST"])
|
| 13 |
+
def login():
|
| 14 |
+
username = request.form.get("username", "")
|
| 15 |
+
password = request.form.get("password", "")
|
| 16 |
+
# VULN: sqli -- string interpolation directly into SQL query
|
| 17 |
+
query = f"SELECT * FROM users WHERE username='{username}' AND password='{password}'"
|
| 18 |
+
# Attacker input: ' OR 1=1-- bypasses this check entirely
|
| 19 |
+
users = {"admin": "password123", "user1": "secret"}
|
| 20 |
+
if username in users and users[username] == password:
|
| 21 |
+
session["user"] = username
|
| 22 |
+
return jsonify({"status": "ok", "user": username})
|
| 23 |
+
return jsonify({"status": "fail"}), 401
|
| 24 |
+
|
| 25 |
+
# Lines 25-35: XSS vulnerability lives here
|
| 26 |
+
@app.route("/comments", methods=["GET"])
|
| 27 |
+
def comments():
|
| 28 |
+
comment = request.args.get("comment", "")
|
| 29 |
+
# VULN: xss -- user input rendered directly into HTML without escaping
|
| 30 |
+
return f"<html><body><div>{comment}</div></body></html>"
|
| 31 |
+
# Attacker input: <script>alert('xss')</script> executes in browser
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
# blank line above (line 35)
|
| 36 |
+
# blank line above (line 36)
|
| 37 |
+
# blank line above (line 37)
|
| 38 |
+
# blank line above (line 38)
|
| 39 |
+
# Lines 40-55: Broken Auth vulnerability lives here
|
| 40 |
+
@app.route("/dashboard", methods=["GET"])
|
| 41 |
+
def dashboard():
|
| 42 |
+
# VULN: broken_auth -- session user is set from request without server validation
|
| 43 |
+
if request.args.get("user"):
|
| 44 |
+
session["user"] = request.args.get("user")
|
| 45 |
+
user = session.get("user")
|
| 46 |
+
if not user:
|
| 47 |
+
return jsonify({"status": "unauthorized"}), 401
|
| 48 |
+
return jsonify({"status": "ok", "dashboard": f"Welcome {user}"})
|
| 49 |
+
|
| 50 |
+
@app.route("/health")
|
| 51 |
+
def health():
|
| 52 |
+
return jsonify({"status": "running"})
|
| 53 |
+
|
| 54 |
+
if __name__ == "__main__":
|
| 55 |
+
app.run(debug=False, port=5001)
|
openenv.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Local shim for the OpenEnv Environment base class.
|
| 3 |
+
The PyPI `openenv` package (v0.1.x) exposes only openenv.env.Env
|
| 4 |
+
and does not include an `Environment` class. This module provides
|
| 5 |
+
the abstract base that CannonWallEnvironment subclasses, matching
|
| 6 |
+
the interface declared in openenv.yaml.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
class Environment:
|
| 10 |
+
"""Base class for OpenEnv-compatible environments."""
|
| 11 |
+
|
| 12 |
+
def __init__(self):
|
| 13 |
+
self.state = {}
|
| 14 |
+
|
| 15 |
+
def reset(self, **kwargs) -> dict:
|
| 16 |
+
raise NotImplementedError
|
| 17 |
+
|
| 18 |
+
def step(self, action: dict) -> dict:
|
| 19 |
+
raise NotImplementedError
|
| 20 |
+
|
| 21 |
+
def get_state(self) -> dict:
|
| 22 |
+
return self.state
|
openenv.yaml
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: cannon-and-wall
|
| 2 |
+
version: 1.0.0
|
| 3 |
+
author: Jairaj S
|
| 4 |
+
theme: self-improvement+multi-agent
|
| 5 |
+
base_class: Environment
|
| 6 |
+
entry: environment/server.py
|
| 7 |
+
client: client/client.py
|
| 8 |
+
port: 7860
|
| 9 |
+
tags:
|
| 10 |
+
- security
|
| 11 |
+
- red-team
|
| 12 |
+
- self-play
|
| 13 |
+
- rl
|
training/train_grpo.ipynb
ADDED
|
File without changes
|
ui/demo.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
from client.client import CannonWallClient
|
| 3 |
+
|
| 4 |
+
client = CannonWallClient()
|
| 5 |
+
|
| 6 |
+
def start_round(stage):
|
| 7 |
+
obs = client.reset(stage=int(stage))
|
| 8 |
+
return obs["source_code"], "Round started. Source loaded.", ""
|
| 9 |
+
|
| 10 |
+
def get_scores():
|
| 11 |
+
s = client.state()
|
| 12 |
+
scores = s.get("scores", {})
|
| 13 |
+
return f"Cannon: {scores.get('cannon',0)} | Wall: {scores.get('wall',0)} | Round: {s.get('round',1)}"
|
| 14 |
+
|
| 15 |
+
with gr.Blocks() as demo:
|
| 16 |
+
with gr.Tab("Run Episode"):
|
| 17 |
+
stage_dd = gr.Dropdown([1,2,3], value=1, label="Stage")
|
| 18 |
+
start_btn = gr.Button("Start Round")
|
| 19 |
+
source_box = gr.Textbox(label="Source Code", lines=20)
|
| 20 |
+
status_box = gr.Textbox(label="Status")
|
| 21 |
+
output_box = gr.Textbox(label="Agent Output")
|
| 22 |
+
start_btn.click(start_round, inputs=[stage_dd], outputs=[source_box, status_box, output_box])
|
| 23 |
+
with gr.Tab("Scores"):
|
| 24 |
+
score_btn = gr.Button("Refresh Scores")
|
| 25 |
+
score_box = gr.Textbox(label="Current Scores")
|
| 26 |
+
score_btn.click(get_scores, outputs=[score_box])
|
| 27 |
+
|
| 28 |
+
if __name__ == "__main__":
|
| 29 |
+
demo.launch(server_port=7861)
|