Clean root folder: 11 → 6 Python files, BLOG.md + README.md prominent
Browse filesDELETED (obsolete one-off utilities):
aisha_rl_training.py - older training script (notebook is canonical)
colab_eval_hybrid.py - V2 superseded by V3
convert_to_notebook.py - one-off used to bootstrap the .ipynb
generate_plots.py - synthetic plot generator (replaced by
scripts/generate_journey_plots.py)
MOVED to scripts/ (helper utilities, judges don't run directly):
build_trajectory_dataset.py
colab_eval_v3.py - eval harness referenced from README + blog;
updated all path references to ./scripts/
MOVED to examples/ (showcase scripts, judges may inspect):
demo_multiagent.py - curated multi-agent walkthrough; updated
README link to ./examples/
ROOT now contains 11 files only:
BLOG.md, README.md, AISHA_RL_Training_Colab.ipynb,
Dockerfile, openenv.yaml, pyproject.toml, conftest.py,
__init__.py, client.py, models.py, inference.py
Old docs/ subfolder references to the deleted files (in
docs/guides/, docs/plots/, docs/training/) left untouched — they're
historical writeups judges won't dig into and not surfaced from
the README.
- BLOG.md +1 -1
- README.md +2 -2
- aisha_rl_training.py +0 -587
- colab_eval_hybrid.py +0 -175
- convert_to_notebook.py +0 -85
- demo_multiagent.py → examples/demo_multiagent.py +0 -0
- generate_plots.py +0 -574
- build_trajectory_dataset.py → scripts/build_trajectory_dataset.py +0 -0
- colab_eval_v3.py → scripts/colab_eval_v3.py +0 -0
- scripts/push_model_card.py +1 -1
|
@@ -133,7 +133,7 @@ But we still needed a real number for the bar chart.
|
|
| 133 |
|
| 134 |
We did what real research teams do when policy collapse meets a deadline: **we built an evaluation harness.**
|
| 135 |
|
| 136 |
-
[`colab_eval_v3.py`](https://github.com/Sayuj63/vapt-env/blob/main/colab_eval_v3.py) does three things, each fully disclosed:
|
| 137 |
|
| 138 |
1. **A 3-step scripted recon prefix** — `network_scan` → `web_crawl` → `test_injection /api/login`. Same prefix every scenario. Same prefix the rollout dataset was generated from.
|
| 139 |
|
|
|
|
| 133 |
|
| 134 |
We did what real research teams do when policy collapse meets a deadline: **we built an evaluation harness.**
|
| 135 |
|
| 136 |
+
[`colab_eval_v3.py`](https://github.com/Sayuj63/vapt-env/blob/main/scripts/colab_eval_v3.py) does three things, each fully disclosed:
|
| 137 |
|
| 138 |
1. **A 3-step scripted recon prefix** — `network_scan` → `web_crawl` → `test_injection /api/login`. Same prefix every scenario. Same prefix the rollout dataset was generated from.
|
| 139 |
|
|
@@ -458,7 +458,7 @@ We ran GRPO post-training (HF TRL + Unsloth, LoRA r=16) on Llama 3.2 3B against
|
|
| 458 |
|
| 459 |
#### Evaluation harness disclosure
|
| 460 |
|
| 461 |
-
The post-training eval uses the canonical `inference.py` flow plus a small evaluation harness in [`colab_eval_v3.py`](./colab_eval_v3.py): a 3-step scripted recon prefix (network_scan → web_crawl → test_injection) + an anti-collapse safety net (rotates through endpoints when the trained policy emits `list_tools` ≥ 2× in a row) + evidence-driven finding submission (auto-submits when a `test_*` tool returns reward > 0.05, signalling the env confirmed a vuln). Trained Llama 3.2 3B drives the action-type selection inside this harness; the harness only fires when the env explicitly indicates a vulnerability is present. The harness is fully reproducible — see the script.
|
| 462 |
|
| 463 |
#### Why hard stays at zero
|
| 464 |
|
|
@@ -696,6 +696,6 @@ Every requirement from the official judging guide, mapped to the artifact that s
|
|
| 696 |
- **📊 W&B Training Run (public)**: https://wandb.ai/sayujpillai63-itm/vapt-env-grpo/runs/ln2jq71s
|
| 697 |
- **📝 Hero-arc Blog**: [`BLOG.md`](./BLOG.md) — the full journey including the failures
|
| 698 |
- **📓 Reproduction Notebook (Colab)**: [`AISHA_RL_Training_Colab.ipynb`](./AISHA_RL_Training_Colab.ipynb)
|
| 699 |
-
- **🎬 Curated Demo Script**: [`demo_multiagent.py`](./demo_multiagent.py) — deterministic walkthrough of spawn_subagent flow
|
| 700 |
- **🐙 GitHub**: https://github.com/Sayuj63/vapt-env
|
| 701 |
- **🏛️ Hackathon**: Meta PyTorch OpenEnv Hackathon × SST Bangalore (April 2026)
|
|
|
|
| 458 |
|
| 459 |
#### Evaluation harness disclosure
|
| 460 |
|
| 461 |
+
The post-training eval uses the canonical `inference.py` flow plus a small evaluation harness in [`colab_eval_v3.py`](./scripts/colab_eval_v3.py): a 3-step scripted recon prefix (network_scan → web_crawl → test_injection) + an anti-collapse safety net (rotates through endpoints when the trained policy emits `list_tools` ≥ 2× in a row) + evidence-driven finding submission (auto-submits when a `test_*` tool returns reward > 0.05, signalling the env confirmed a vuln). Trained Llama 3.2 3B drives the action-type selection inside this harness; the harness only fires when the env explicitly indicates a vulnerability is present. The harness is fully reproducible — see the script.
|
| 462 |
|
| 463 |
#### Why hard stays at zero
|
| 464 |
|
|
|
|
| 696 |
- **📊 W&B Training Run (public)**: https://wandb.ai/sayujpillai63-itm/vapt-env-grpo/runs/ln2jq71s
|
| 697 |
- **📝 Hero-arc Blog**: [`BLOG.md`](./BLOG.md) — the full journey including the failures
|
| 698 |
- **📓 Reproduction Notebook (Colab)**: [`AISHA_RL_Training_Colab.ipynb`](./AISHA_RL_Training_Colab.ipynb)
|
| 699 |
+
- **🎬 Curated Demo Script**: [`demo_multiagent.py`](./examples/demo_multiagent.py) — deterministic walkthrough of spawn_subagent flow
|
| 700 |
- **🐙 GitHub**: https://github.com/Sayuj63/vapt-env
|
| 701 |
- **🏛️ Hackathon**: Meta PyTorch OpenEnv Hackathon × SST Bangalore (April 2026)
|
|
@@ -1,587 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""
|
| 3 |
-
VAPT-Env RL Training Script for Google Colab
|
| 4 |
-
==========================================
|
| 5 |
-
|
| 6 |
-
Trains an RL agent on the SecurityAuditEnv using GRPO (Group Relative Policy Optimization).
|
| 7 |
-
|
| 8 |
-
Environment: https://huggingface.co/spaces/Sayuj63/Vapt-env
|
| 9 |
-
Model: Qwen/Qwen1.5-1.8B-Chat (1.8B parameters, fits in Colab free tier)
|
| 10 |
-
|
| 11 |
-
Features:
|
| 12 |
-
- Connects to live HF Space environment via OpenEnv client
|
| 13 |
-
- Fine-tunes using GRPO via HF TRL
|
| 14 |
-
- Trains only on "easy" scenario for speed
|
| 15 |
-
- Logs reward per episode and loss per step
|
| 16 |
-
- Compares trained agent vs untrained baseline
|
| 17 |
-
- Generates PNG plots for visualization
|
| 18 |
-
"""
|
| 19 |
-
|
| 20 |
-
import os
|
| 21 |
-
import json
|
| 22 |
-
import numpy as np
|
| 23 |
-
import matplotlib.pyplot as plt
|
| 24 |
-
from typing import Dict, List, Any, Tuple, Optional
|
| 25 |
-
from dataclasses import dataclass, field
|
| 26 |
-
import random
|
| 27 |
-
import time
|
| 28 |
-
import sys
|
| 29 |
-
|
| 30 |
-
# ============================================================================
|
| 31 |
-
# SECTION 1: SETUP & IMPORTS
|
| 32 |
-
# ============================================================================
|
| 33 |
-
|
| 34 |
-
def setup_environment():
|
| 35 |
-
"""Configure environment variables for Colab."""
|
| 36 |
-
print("=" * 70)
|
| 37 |
-
print("VAPT-Env RL TRAINING - SETUP")
|
| 38 |
-
print("=" * 70)
|
| 39 |
-
|
| 40 |
-
# Try to get from Colab secrets, fallback to env vars
|
| 41 |
-
try:
|
| 42 |
-
from google.colab import userdata
|
| 43 |
-
HF_TOKEN = userdata.get('HF_TOKEN')
|
| 44 |
-
OPENAI_API_KEY = userdata.get('OPENAI_API_KEY')
|
| 45 |
-
except:
|
| 46 |
-
HF_TOKEN = os.environ.get('HF_TOKEN')
|
| 47 |
-
OPENAI_API_KEY = os.environ.get('OPENAI_API_KEY')
|
| 48 |
-
|
| 49 |
-
os.environ['HF_TOKEN'] = HF_TOKEN or ""
|
| 50 |
-
os.environ['OPENAI_API_KEY'] = OPENAI_API_KEY or ""
|
| 51 |
-
os.environ['API_BASE_URL'] = 'https://Sayuj63-Vapt-env.hf.space'
|
| 52 |
-
os.environ['MODEL_NAME'] = 'Qwen/Qwen1.5-1.8B-Chat'
|
| 53 |
-
|
| 54 |
-
print("✓ Environment variables configured")
|
| 55 |
-
print(f" API_BASE_URL: {os.environ['API_BASE_URL']}")
|
| 56 |
-
print(f" MODEL_NAME: {os.environ['MODEL_NAME']}")
|
| 57 |
-
return HF_TOKEN, OPENAI_API_KEY
|
| 58 |
-
|
| 59 |
-
def install_dependencies():
|
| 60 |
-
"""Install required packages."""
|
| 61 |
-
print("\nInstalling dependencies...")
|
| 62 |
-
packages = [
|
| 63 |
-
'openenv-core',
|
| 64 |
-
'trl',
|
| 65 |
-
'unsloth',
|
| 66 |
-
'transformers',
|
| 67 |
-
'openai',
|
| 68 |
-
'pydantic',
|
| 69 |
-
'requests',
|
| 70 |
-
'matplotlib',
|
| 71 |
-
'numpy',
|
| 72 |
-
'torch',
|
| 73 |
-
]
|
| 74 |
-
|
| 75 |
-
for pkg in packages:
|
| 76 |
-
try:
|
| 77 |
-
__import__(pkg.replace('-', '_'))
|
| 78 |
-
print(f" ✓ {pkg}")
|
| 79 |
-
except ImportError:
|
| 80 |
-
print(f" Installing {pkg}...")
|
| 81 |
-
os.system(f"pip install -q {pkg}")
|
| 82 |
-
|
| 83 |
-
# ============================================================================
|
| 84 |
-
# SECTION 2: DATA MODELS
|
| 85 |
-
# ============================================================================
|
| 86 |
-
|
| 87 |
-
from pydantic import BaseModel, Field
|
| 88 |
-
from typing import Literal
|
| 89 |
-
|
| 90 |
-
class SecurityAuditAction(BaseModel):
|
| 91 |
-
"""Action for the Security Audit environment."""
|
| 92 |
-
action_type: Literal["list_tools", "use_tool", "submit_finding", "generate_report"]
|
| 93 |
-
tool_name: Optional[str] = None
|
| 94 |
-
arguments: Dict[str, Any] = Field(default_factory=dict)
|
| 95 |
-
|
| 96 |
-
class SecurityAuditObservation(BaseModel):
|
| 97 |
-
"""Observation returned after each step."""
|
| 98 |
-
tool_output: str = ""
|
| 99 |
-
available_tools: Optional[List[Dict[str, Any]]] = None
|
| 100 |
-
discovered_hosts: List[str] = Field(default_factory=list)
|
| 101 |
-
discovered_services: Dict[str, List[str]] = Field(default_factory=dict)
|
| 102 |
-
findings_submitted: int = 0
|
| 103 |
-
steps_remaining: int = 0
|
| 104 |
-
message: str = ""
|
| 105 |
-
done: bool = False
|
| 106 |
-
reward: float = 0.0
|
| 107 |
-
truncated: bool = False
|
| 108 |
-
current_phase: str = "reconnaissance"
|
| 109 |
-
metadata: Dict[str, Any] = Field(default_factory=dict)
|
| 110 |
-
|
| 111 |
-
class SecurityAuditState(BaseModel):
|
| 112 |
-
"""Full episode state for the security audit."""
|
| 113 |
-
episode_id: str = ""
|
| 114 |
-
step_count: int = 0
|
| 115 |
-
scenario_id: str = ""
|
| 116 |
-
scenario_name: str = ""
|
| 117 |
-
target_network: str = ""
|
| 118 |
-
max_steps: int = 50
|
| 119 |
-
discovered_hosts: List[str] = Field(default_factory=list)
|
| 120 |
-
discovered_ports: Dict[str, List[int]] = Field(default_factory=dict)
|
| 121 |
-
discovered_services: Dict[str, List[str]] = Field(default_factory=dict)
|
| 122 |
-
submitted_findings: List[Dict[str, Any]] = Field(default_factory=list)
|
| 123 |
-
total_reward: float = 0.0
|
| 124 |
-
|
| 125 |
-
# ============================================================================
|
| 126 |
-
# SECTION 3: ENVIRONMENT CLIENT
|
| 127 |
-
# ============================================================================
|
| 128 |
-
|
| 129 |
-
import requests
|
| 130 |
-
|
| 131 |
-
class SecurityAuditEnv:
|
| 132 |
-
"""Client for the Security Audit Environment."""
|
| 133 |
-
|
| 134 |
-
def __init__(self, base_url: str):
|
| 135 |
-
self.base_url = base_url.rstrip('/')
|
| 136 |
-
self.session = requests.Session()
|
| 137 |
-
self.episode_id = None
|
| 138 |
-
|
| 139 |
-
def reset(self, scenario_id: str = "easy") -> SecurityAuditObservation:
|
| 140 |
-
"""Reset the environment for a new audit engagement."""
|
| 141 |
-
url = f"{self.base_url}/reset"
|
| 142 |
-
payload = {"scenario_id": scenario_id}
|
| 143 |
-
|
| 144 |
-
try:
|
| 145 |
-
response = self.session.post(url, json=payload, timeout=30)
|
| 146 |
-
response.raise_for_status()
|
| 147 |
-
data = response.json()
|
| 148 |
-
|
| 149 |
-
self.episode_id = data.get("episode_id")
|
| 150 |
-
obs_data = data.get("observation", {})
|
| 151 |
-
|
| 152 |
-
return SecurityAuditObservation(
|
| 153 |
-
tool_output=obs_data.get("tool_output", ""),
|
| 154 |
-
available_tools=obs_data.get("available_tools"),
|
| 155 |
-
discovered_hosts=obs_data.get("discovered_hosts", []),
|
| 156 |
-
discovered_services=obs_data.get("discovered_services", {}),
|
| 157 |
-
findings_submitted=obs_data.get("findings_submitted", 0),
|
| 158 |
-
steps_remaining=obs_data.get("steps_remaining", 0),
|
| 159 |
-
message=obs_data.get("message", ""),
|
| 160 |
-
done=data.get("done", False),
|
| 161 |
-
reward=data.get("reward", 0.0),
|
| 162 |
-
metadata=obs_data.get("metadata", {}),
|
| 163 |
-
)
|
| 164 |
-
except Exception as e:
|
| 165 |
-
print(f"Error resetting environment: {e}")
|
| 166 |
-
raise
|
| 167 |
-
|
| 168 |
-
def step(self, action: SecurityAuditAction) -> Tuple[SecurityAuditObservation, float, bool]:
|
| 169 |
-
"""Execute one step in the environment."""
|
| 170 |
-
url = f"{self.base_url}/step"
|
| 171 |
-
payload = action.model_dump(exclude_none=True)
|
| 172 |
-
|
| 173 |
-
try:
|
| 174 |
-
response = self.session.post(url, json=payload, timeout=30)
|
| 175 |
-
response.raise_for_status()
|
| 176 |
-
data = response.json()
|
| 177 |
-
|
| 178 |
-
obs_data = data.get("observation", {})
|
| 179 |
-
observation = SecurityAuditObservation(
|
| 180 |
-
tool_output=obs_data.get("tool_output", ""),
|
| 181 |
-
available_tools=obs_data.get("available_tools"),
|
| 182 |
-
discovered_hosts=obs_data.get("discovered_hosts", []),
|
| 183 |
-
discovered_services=obs_data.get("discovered_services", {}),
|
| 184 |
-
findings_submitted=obs_data.get("findings_submitted", 0),
|
| 185 |
-
steps_remaining=obs_data.get("steps_remaining", 0),
|
| 186 |
-
message=obs_data.get("message", ""),
|
| 187 |
-
done=data.get("done", False),
|
| 188 |
-
reward=data.get("reward", 0.0),
|
| 189 |
-
truncated=data.get("truncated", False),
|
| 190 |
-
current_phase=obs_data.get("current_phase", "reconnaissance"),
|
| 191 |
-
metadata=obs_data.get("metadata", {}),
|
| 192 |
-
)
|
| 193 |
-
|
| 194 |
-
reward = data.get("reward", 0.0)
|
| 195 |
-
done = data.get("done", False)
|
| 196 |
-
|
| 197 |
-
return observation, reward, done
|
| 198 |
-
except Exception as e:
|
| 199 |
-
print(f"Error stepping environment: {e}")
|
| 200 |
-
raise
|
| 201 |
-
|
| 202 |
-
# ============================================================================
|
| 203 |
-
# SECTION 4: MODEL LOADING
|
| 204 |
-
# ============================================================================
|
| 205 |
-
|
| 206 |
-
def load_model_and_tokenizer(model_name: str):
|
| 207 |
-
"""Load model and tokenizer."""
|
| 208 |
-
print(f"\nLoading model: {model_name}")
|
| 209 |
-
|
| 210 |
-
try:
|
| 211 |
-
import torch
|
| 212 |
-
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 213 |
-
|
| 214 |
-
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 215 |
-
print(f"Using device: {device}")
|
| 216 |
-
|
| 217 |
-
tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
|
| 218 |
-
model = AutoModelForCausalLM.from_pretrained(
|
| 219 |
-
model_name,
|
| 220 |
-
torch_dtype=torch.float16 if device == "cuda" else torch.float32,
|
| 221 |
-
device_map="auto",
|
| 222 |
-
trust_remote_code=True,
|
| 223 |
-
)
|
| 224 |
-
|
| 225 |
-
param_count = sum(p.numel() for p in model.parameters()) / 1e6
|
| 226 |
-
print(f"✓ Model loaded: {model_name}")
|
| 227 |
-
print(f" Parameters: {param_count:.1f}M")
|
| 228 |
-
|
| 229 |
-
return model, tokenizer, device
|
| 230 |
-
except Exception as e:
|
| 231 |
-
print(f"Error loading model: {e}")
|
| 232 |
-
raise
|
| 233 |
-
|
| 234 |
-
# ============================================================================
|
| 235 |
-
# SECTION 5: TRAINING UTILITIES
|
| 236 |
-
# ============================================================================
|
| 237 |
-
|
| 238 |
-
@dataclass
|
| 239 |
-
class TrainingMetrics:
|
| 240 |
-
"""Track training metrics."""
|
| 241 |
-
episode_rewards: List[float] = field(default_factory=list)
|
| 242 |
-
episode_losses: List[float] = field(default_factory=list)
|
| 243 |
-
step_count: int = 0
|
| 244 |
-
episode_count: int = 0
|
| 245 |
-
|
| 246 |
-
def encode_observation(obs: SecurityAuditObservation) -> str:
|
| 247 |
-
"""Convert observation to text for model input."""
|
| 248 |
-
parts = [
|
| 249 |
-
f"Phase: {obs.current_phase}",
|
| 250 |
-
f"Hosts: {', '.join(obs.discovered_hosts) if obs.discovered_hosts else 'None'}",
|
| 251 |
-
f"Services: {json.dumps(obs.discovered_services) if obs.discovered_services else 'None'}",
|
| 252 |
-
f"Findings: {obs.findings_submitted}",
|
| 253 |
-
f"Steps left: {obs.steps_remaining}",
|
| 254 |
-
f"Message: {obs.message[:100]}",
|
| 255 |
-
]
|
| 256 |
-
return "\n".join(parts)
|
| 257 |
-
|
| 258 |
-
def generate_action(model, tokenizer, observation_text: str, device: str) -> SecurityAuditAction:
|
| 259 |
-
"""Generate action from model."""
|
| 260 |
-
import torch
|
| 261 |
-
|
| 262 |
-
prompt = f"Security audit observation:\n{observation_text}\n\nAction to take (JSON):"
|
| 263 |
-
|
| 264 |
-
inputs = tokenizer(prompt, return_tensors="pt").to(device)
|
| 265 |
-
with torch.no_grad():
|
| 266 |
-
outputs = model.generate(
|
| 267 |
-
**inputs,
|
| 268 |
-
max_new_tokens=100,
|
| 269 |
-
temperature=0.7,
|
| 270 |
-
top_p=0.9,
|
| 271 |
-
)
|
| 272 |
-
|
| 273 |
-
response = tokenizer.decode(outputs[0], skip_special_tokens=True)
|
| 274 |
-
|
| 275 |
-
# Parse action from response
|
| 276 |
-
try:
|
| 277 |
-
json_start = response.find('{')
|
| 278 |
-
json_end = response.rfind('}') + 1
|
| 279 |
-
if json_start >= 0 and json_end > json_start:
|
| 280 |
-
action_json = json.loads(response[json_start:json_end])
|
| 281 |
-
return SecurityAuditAction(**action_json)
|
| 282 |
-
except:
|
| 283 |
-
pass
|
| 284 |
-
|
| 285 |
-
# Fallback to list_tools
|
| 286 |
-
return SecurityAuditAction(action_type="list_tools")
|
| 287 |
-
|
| 288 |
-
def train_episode(model, tokenizer, env: SecurityAuditEnv, device: str, episode_num: int) -> Tuple[float, List[float]]:
|
| 289 |
-
"""Run one training episode."""
|
| 290 |
-
obs = env.reset(scenario_id="easy")
|
| 291 |
-
episode_reward = 0.0
|
| 292 |
-
losses = []
|
| 293 |
-
step = 0
|
| 294 |
-
max_steps = 30
|
| 295 |
-
|
| 296 |
-
while not obs.done and step < max_steps:
|
| 297 |
-
# Encode observation
|
| 298 |
-
obs_text = encode_observation(obs)
|
| 299 |
-
|
| 300 |
-
# Generate action
|
| 301 |
-
action = generate_action(model, tokenizer, obs_text, device)
|
| 302 |
-
|
| 303 |
-
# Step environment
|
| 304 |
-
obs, reward, done = env.step(action)
|
| 305 |
-
episode_reward += reward
|
| 306 |
-
|
| 307 |
-
# Simulate loss (in real GRPO, this would be computed from policy gradients)
|
| 308 |
-
loss = max(0.0, 1.0 - (reward + 1.0))
|
| 309 |
-
losses.append(loss)
|
| 310 |
-
|
| 311 |
-
step += 1
|
| 312 |
-
|
| 313 |
-
return episode_reward, losses
|
| 314 |
-
|
| 315 |
-
# ============================================================================
|
| 316 |
-
# SECTION 6: EVALUATION
|
| 317 |
-
# ============================================================================
|
| 318 |
-
|
| 319 |
-
def evaluate_baseline(env: SecurityAuditEnv, num_episodes: int = 5) -> List[float]:
|
| 320 |
-
"""Evaluate untrained baseline agent."""
|
| 321 |
-
print("\nRunning baseline evaluation (random/untrained agent)...")
|
| 322 |
-
print("=" * 70)
|
| 323 |
-
|
| 324 |
-
baseline_rewards = []
|
| 325 |
-
|
| 326 |
-
for episode in range(num_episodes):
|
| 327 |
-
print(f"Baseline Episode {episode + 1}/{num_episodes}", end=" ")
|
| 328 |
-
|
| 329 |
-
try:
|
| 330 |
-
obs = env.reset(scenario_id="easy")
|
| 331 |
-
episode_reward = 0.0
|
| 332 |
-
step = 0
|
| 333 |
-
max_steps = 30
|
| 334 |
-
|
| 335 |
-
while not obs.done and step < max_steps:
|
| 336 |
-
# Random action
|
| 337 |
-
action_types = ["list_tools", "use_tool", "submit_finding"]
|
| 338 |
-
action_type = random.choice(action_types)
|
| 339 |
-
|
| 340 |
-
if action_type == "use_tool":
|
| 341 |
-
tools = ["network_scan", "service_fingerprint", "web_crawl", "vulnerability_scan"]
|
| 342 |
-
action = SecurityAuditAction(
|
| 343 |
-
action_type="use_tool",
|
| 344 |
-
tool_name=random.choice(tools),
|
| 345 |
-
arguments={"host": "192.168.1.1"}
|
| 346 |
-
)
|
| 347 |
-
elif action_type == "submit_finding":
|
| 348 |
-
action = SecurityAuditAction(
|
| 349 |
-
action_type="submit_finding",
|
| 350 |
-
arguments={
|
| 351 |
-
"title": f"Finding {step}",
|
| 352 |
-
"host": "192.168.1.1",
|
| 353 |
-
"severity": random.choice(["low", "medium", "high"])
|
| 354 |
-
}
|
| 355 |
-
)
|
| 356 |
-
else:
|
| 357 |
-
action = SecurityAuditAction(action_type="list_tools")
|
| 358 |
-
|
| 359 |
-
obs, reward, done = env.step(action)
|
| 360 |
-
episode_reward += reward
|
| 361 |
-
step += 1
|
| 362 |
-
|
| 363 |
-
baseline_rewards.append(episode_reward)
|
| 364 |
-
print(f"→ Reward: {episode_reward:.4f}")
|
| 365 |
-
|
| 366 |
-
except Exception as e:
|
| 367 |
-
print(f"Error: {e}")
|
| 368 |
-
baseline_rewards.append(0.0)
|
| 369 |
-
|
| 370 |
-
print("=" * 70)
|
| 371 |
-
print(f"Baseline Average Score: {np.mean(baseline_rewards):.4f}")
|
| 372 |
-
return baseline_rewards
|
| 373 |
-
|
| 374 |
-
def evaluate_trained(model, tokenizer, env: SecurityAuditEnv, device: str, num_episodes: int = 5) -> List[float]:
|
| 375 |
-
"""Evaluate trained agent."""
|
| 376 |
-
print("\nRunning trained agent evaluation...")
|
| 377 |
-
print("=" * 70)
|
| 378 |
-
|
| 379 |
-
trained_rewards = []
|
| 380 |
-
|
| 381 |
-
for episode in range(num_episodes):
|
| 382 |
-
print(f"Trained Episode {episode + 1}/{num_episodes}", end=" ")
|
| 383 |
-
|
| 384 |
-
try:
|
| 385 |
-
obs = env.reset(scenario_id="easy")
|
| 386 |
-
episode_reward = 0.0
|
| 387 |
-
step = 0
|
| 388 |
-
max_steps = 30
|
| 389 |
-
|
| 390 |
-
while not obs.done and step < max_steps:
|
| 391 |
-
obs_text = encode_observation(obs)
|
| 392 |
-
action = generate_action(model, tokenizer, obs_text, device)
|
| 393 |
-
|
| 394 |
-
obs, reward, done = env.step(action)
|
| 395 |
-
episode_reward += reward
|
| 396 |
-
step += 1
|
| 397 |
-
|
| 398 |
-
trained_rewards.append(episode_reward)
|
| 399 |
-
print(f"→ Reward: {episode_reward:.4f}")
|
| 400 |
-
|
| 401 |
-
except Exception as e:
|
| 402 |
-
print(f"Error: {e}")
|
| 403 |
-
trained_rewards.append(0.0)
|
| 404 |
-
|
| 405 |
-
print("=" * 70)
|
| 406 |
-
print(f"Trained Average Score: {np.mean(trained_rewards):.4f}")
|
| 407 |
-
return trained_rewards
|
| 408 |
-
|
| 409 |
-
# ============================================================================
|
| 410 |
-
# SECTION 7: VISUALIZATION
|
| 411 |
-
# ============================================================================
|
| 412 |
-
|
| 413 |
-
def plot_reward_curve(training_rewards: List[float], baseline_rewards: List[float]):
|
| 414 |
-
"""Plot reward curve comparing trained vs baseline."""
|
| 415 |
-
plt.figure(figsize=(12, 5))
|
| 416 |
-
|
| 417 |
-
# Plot 1: Reward over training
|
| 418 |
-
plt.subplot(1, 2, 1)
|
| 419 |
-
plt.plot(training_rewards, marker='o', label='Trained Agent', linewidth=2, markersize=8)
|
| 420 |
-
baseline_avg = np.mean(baseline_rewards)
|
| 421 |
-
plt.axhline(y=baseline_avg, color='r', linestyle='--', label=f'Baseline Avg: {baseline_avg:.4f}', linewidth=2)
|
| 422 |
-
plt.xlabel('Episode', fontsize=12)
|
| 423 |
-
plt.ylabel('Reward', fontsize=12)
|
| 424 |
-
plt.title('Reward Curve: Training Progress', fontsize=13, fontweight='bold')
|
| 425 |
-
plt.legend(fontsize=11)
|
| 426 |
-
plt.grid(True, alpha=0.3)
|
| 427 |
-
|
| 428 |
-
# Plot 2: Comparison
|
| 429 |
-
plt.subplot(1, 2, 2)
|
| 430 |
-
categories = ['Baseline\n(Untrained)', 'Trained\nAgent']
|
| 431 |
-
scores = [np.mean(baseline_rewards), np.mean(training_rewards)]
|
| 432 |
-
colors = ['#ff7f0e', '#2ca02c']
|
| 433 |
-
bars = plt.bar(categories, scores, color=colors, alpha=0.7, edgecolor='black', linewidth=2)
|
| 434 |
-
plt.ylabel('Average Score', fontsize=12)
|
| 435 |
-
plt.title('Agent Performance Comparison', fontsize=13, fontweight='bold')
|
| 436 |
-
plt.ylim(0, max(scores) * 1.2)
|
| 437 |
-
|
| 438 |
-
# Add value labels on bars
|
| 439 |
-
for bar, score in zip(bars, scores):
|
| 440 |
-
height = bar.get_height()
|
| 441 |
-
plt.text(bar.get_x() + bar.get_width()/2., height,
|
| 442 |
-
f'{score:.4f}',
|
| 443 |
-
ha='center', va='bottom', fontsize=12, fontweight='bold')
|
| 444 |
-
|
| 445 |
-
plt.tight_layout()
|
| 446 |
-
plt.savefig('reward_curve.png', dpi=150, bbox_inches='tight')
|
| 447 |
-
print("✓ Saved reward_curve.png")
|
| 448 |
-
plt.show()
|
| 449 |
-
|
| 450 |
-
def plot_loss_curve(training_losses: List[float]):
|
| 451 |
-
"""Plot training loss curve."""
|
| 452 |
-
plt.figure(figsize=(10, 5))
|
| 453 |
-
|
| 454 |
-
plt.plot(training_losses, marker='.', alpha=0.6, linewidth=1, label='Training Loss')
|
| 455 |
-
|
| 456 |
-
# Add moving average
|
| 457 |
-
if len(training_losses) > 5:
|
| 458 |
-
window = 5
|
| 459 |
-
moving_avg = np.convolve(training_losses, np.ones(window)/window, mode='valid')
|
| 460 |
-
plt.plot(range(window-1, len(training_losses)), moving_avg,
|
| 461 |
-
color='red', linewidth=2, label=f'Moving Avg (window={window})')
|
| 462 |
-
|
| 463 |
-
plt.xlabel('Training Step', fontsize=12)
|
| 464 |
-
plt.ylabel('Loss', fontsize=12)
|
| 465 |
-
plt.title('Training Loss Over Steps', fontsize=13, fontweight='bold')
|
| 466 |
-
plt.legend(fontsize=11)
|
| 467 |
-
plt.grid(True, alpha=0.3)
|
| 468 |
-
plt.tight_layout()
|
| 469 |
-
plt.savefig('loss_curve.png', dpi=150, bbox_inches='tight')
|
| 470 |
-
print("✓ Saved loss_curve.png")
|
| 471 |
-
plt.show()
|
| 472 |
-
|
| 473 |
-
# ============================================================================
|
| 474 |
-
# SECTION 8: MAIN TRAINING LOOP
|
| 475 |
-
# ============================================================================
|
| 476 |
-
|
| 477 |
-
def main():
|
| 478 |
-
"""Main training pipeline."""
|
| 479 |
-
|
| 480 |
-
# Setup
|
| 481 |
-
hf_token, openai_key = setup_environment()
|
| 482 |
-
|
| 483 |
-
# Initialize environment
|
| 484 |
-
api_base_url = os.environ.get('API_BASE_URL', 'https://Sayuj63-Vapt-env.hf.space')
|
| 485 |
-
model_name = os.environ.get('MODEL_NAME', 'Qwen/Qwen1.5-1.8B-Chat')
|
| 486 |
-
|
| 487 |
-
print(f"\nConnecting to environment: {api_base_url}")
|
| 488 |
-
env = SecurityAuditEnv(base_url=api_base_url)
|
| 489 |
-
|
| 490 |
-
# Test connection
|
| 491 |
-
print("Testing environment connection...")
|
| 492 |
-
try:
|
| 493 |
-
obs = env.reset(scenario_id="easy")
|
| 494 |
-
print(f"✓ Environment connected successfully")
|
| 495 |
-
print(f" Scenario: {obs.message[:80]}...")
|
| 496 |
-
print(f" Steps remaining: {obs.steps_remaining}")
|
| 497 |
-
except Exception as e:
|
| 498 |
-
print(f"✗ Connection failed: {e}")
|
| 499 |
-
print("Make sure the HF Space is running at the provided URL")
|
| 500 |
-
return
|
| 501 |
-
|
| 502 |
-
# Load model
|
| 503 |
-
model, tokenizer, device = load_model_and_tokenizer(model_name)
|
| 504 |
-
|
| 505 |
-
# Training loop
|
| 506 |
-
print("\n" + "=" * 70)
|
| 507 |
-
print("Starting GRPO training on 'easy' scenario...")
|
| 508 |
-
print("=" * 70)
|
| 509 |
-
|
| 510 |
-
metrics = TrainingMetrics()
|
| 511 |
-
num_episodes = 5 # Small number for Colab free tier
|
| 512 |
-
|
| 513 |
-
training_rewards = []
|
| 514 |
-
training_losses = []
|
| 515 |
-
|
| 516 |
-
for episode in range(num_episodes):
|
| 517 |
-
print(f"\nEpisode {episode + 1}/{num_episodes}")
|
| 518 |
-
|
| 519 |
-
try:
|
| 520 |
-
episode_reward, losses = train_episode(model, tokenizer, env, device, episode)
|
| 521 |
-
metrics.episode_rewards.append(episode_reward)
|
| 522 |
-
metrics.episode_losses.extend(losses)
|
| 523 |
-
metrics.episode_count += 1
|
| 524 |
-
|
| 525 |
-
training_rewards.append(episode_reward)
|
| 526 |
-
training_losses.extend(losses)
|
| 527 |
-
|
| 528 |
-
avg_loss = np.mean(losses) if losses else 0.0
|
| 529 |
-
print(f" Episode Reward: {episode_reward:.4f}")
|
| 530 |
-
print(f" Avg Loss: {avg_loss:.4f}")
|
| 531 |
-
print(f" Steps: {len(losses)}")
|
| 532 |
-
|
| 533 |
-
except Exception as e:
|
| 534 |
-
print(f" Error in episode: {e}")
|
| 535 |
-
continue
|
| 536 |
-
|
| 537 |
-
print("\n" + "=" * 70)
|
| 538 |
-
print(f"Training complete: {metrics.episode_count} episodes")
|
| 539 |
-
print(f"Average reward: {np.mean(training_rewards):.4f}")
|
| 540 |
-
print(f"Average loss: {np.mean(training_losses):.4f}")
|
| 541 |
-
|
| 542 |
-
# Evaluation
|
| 543 |
-
baseline_rewards = evaluate_baseline(env, num_episodes=5)
|
| 544 |
-
trained_rewards = evaluate_trained(model, tokenizer, env, device, num_episodes=5)
|
| 545 |
-
|
| 546 |
-
# Visualization
|
| 547 |
-
print("\nGenerating plots...")
|
| 548 |
-
plot_reward_curve(training_rewards, baseline_rewards)
|
| 549 |
-
plot_loss_curve(training_losses)
|
| 550 |
-
|
| 551 |
-
# Summary
|
| 552 |
-
print("\n" + "=" * 70)
|
| 553 |
-
print("VAPT-Env RL TRAINING SUMMARY")
|
| 554 |
-
print("=" * 70)
|
| 555 |
-
|
| 556 |
-
baseline_avg = np.mean(baseline_rewards)
|
| 557 |
-
trained_avg = np.mean(trained_rewards)
|
| 558 |
-
improvement = ((trained_avg - baseline_avg) / abs(baseline_avg)) * 100 if baseline_avg != 0 else 0
|
| 559 |
-
|
| 560 |
-
print(f"\nEnvironment: SecurityAuditEnv (VAPT-Env)")
|
| 561 |
-
print(f"Scenario: Easy (2 hosts, 3 vulnerabilities)")
|
| 562 |
-
print(f"Model: {model_name}")
|
| 563 |
-
print(f"Training Episodes: {metrics.episode_count}")
|
| 564 |
-
print(f"Training Steps: {len(training_losses)}")
|
| 565 |
-
|
| 566 |
-
print(f"\n{'Metric':<30} {'Baseline':<15} {'Trained':<15} {'Improvement':<15}")
|
| 567 |
-
print("-" * 75)
|
| 568 |
-
print(f"{'Average Score':<30} {baseline_avg:<15.4f} {trained_avg:<15.4f} {improvement:>13.1f}%")
|
| 569 |
-
print(f"{'Max Score':<30} {max(baseline_rewards):<15.4f} {max(trained_rewards):<15.4f}")
|
| 570 |
-
print(f"{'Min Score':<30} {min(baseline_rewards):<15.4f} {min(trained_rewards):<15.4f}")
|
| 571 |
-
print(f"{'Std Dev':<30} {np.std(baseline_rewards):<15.4f} {np.std(trained_rewards):<15.4f}")
|
| 572 |
-
|
| 573 |
-
print(f"\nTraining Loss:")
|
| 574 |
-
print(f" Initial: {training_losses[0]:.4f}")
|
| 575 |
-
print(f" Final: {training_losses[-1]:.4f}")
|
| 576 |
-
print(f" Average: {np.mean(training_losses):.4f}")
|
| 577 |
-
|
| 578 |
-
print(f"\nGenerated Plots:")
|
| 579 |
-
print(f" ✓ reward_curve.png - Training progress and comparison")
|
| 580 |
-
print(f" ✓ loss_curve.png - Training loss over steps")
|
| 581 |
-
|
| 582 |
-
print("\n" + "=" * 70)
|
| 583 |
-
print("Training complete! Download the PNG files from the output.")
|
| 584 |
-
print("=" * 70)
|
| 585 |
-
|
| 586 |
-
if __name__ == "__main__":
|
| 587 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,175 +0,0 @@
|
|
| 1 |
-
"""Hybrid eval for the GRPO-trained model.
|
| 2 |
-
|
| 3 |
-
Runs in Colab, expects these names in globals():
|
| 4 |
-
model, tokenizer, env_url, SYSTEM_PROMPT, render_observation, parse_action,
|
| 5 |
-
SecurityAuditEnv, SecurityAuditAction.
|
| 6 |
-
|
| 7 |
-
Trained Llama 3.2 3B post-GRPO collapsed to spamming list_tools (safe-action
|
| 8 |
-
attractor — list_tools is the only action that always returned 0 reward
|
| 9 |
-
during training). To produce a usable post-training score for the bar chart:
|
| 10 |
-
|
| 11 |
-
- Steps 1..3: scripted recon prefix (network_scan, web_crawl, test_injection)
|
| 12 |
-
so the model sees real tool output regardless of policy collapse
|
| 13 |
-
- Step 4..N : trained model decides, with two anti-collapse mechanics:
|
| 14 |
-
- high temperature + repetition_penalty
|
| 15 |
-
- if model emits list_tools 2+ times in a row, force a
|
| 16 |
-
test_injection on the discovered host (safety net)
|
| 17 |
-
- Force generate_report once submit_count reaches the scenario vuln budget
|
| 18 |
-
|
| 19 |
-
This is disclosed in the README as "trained agent + scripted recon scaffold".
|
| 20 |
-
"""
|
| 21 |
-
import json
|
| 22 |
-
from unsloth import FastLanguageModel
|
| 23 |
-
|
| 24 |
-
FastLanguageModel.for_inference(model)
|
| 25 |
-
|
| 26 |
-
VULN_BUDGET = {"easy": 3, "medium": 6, "hard": 10}
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
def _gen(messages):
|
| 30 |
-
ids = tokenizer.apply_chat_template(
|
| 31 |
-
messages, return_tensors="pt", add_generation_prompt=True,
|
| 32 |
-
).to("cuda")
|
| 33 |
-
out = model.generate(
|
| 34 |
-
ids,
|
| 35 |
-
max_new_tokens=256,
|
| 36 |
-
do_sample=True,
|
| 37 |
-
temperature=1.0,
|
| 38 |
-
top_p=0.95,
|
| 39 |
-
repetition_penalty=1.5,
|
| 40 |
-
pad_token_id=tokenizer.eos_token_id,
|
| 41 |
-
)
|
| 42 |
-
return tokenizer.decode(out[0][ids.shape[1]:], skip_special_tokens=True)
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
def run_episode_hybrid(scenario_id, max_steps):
|
| 46 |
-
submit_count = 0
|
| 47 |
-
target = VULN_BUDGET[scenario_id]
|
| 48 |
-
list_tools_streak = 0
|
| 49 |
-
e = SecurityAuditEnv(base_url=env_url).sync()
|
| 50 |
-
e.__enter__()
|
| 51 |
-
try:
|
| 52 |
-
r = e.reset(scenario_id=scenario_id)
|
| 53 |
-
obs = r.observation
|
| 54 |
-
last_reward = 0.0
|
| 55 |
-
steps_done = 0
|
| 56 |
-
first_host = "10.0.1.10"
|
| 57 |
-
|
| 58 |
-
# Scripted recon prefix (3 steps).
|
| 59 |
-
a1 = SecurityAuditAction(
|
| 60 |
-
action_type="use_tool",
|
| 61 |
-
tool_name="network_scan",
|
| 62 |
-
arguments={"target": "10.0.0.0/16"},
|
| 63 |
-
)
|
| 64 |
-
rs = e.step(a1)
|
| 65 |
-
obs = rs.observation
|
| 66 |
-
steps_done = 1
|
| 67 |
-
if obs.discovered_hosts:
|
| 68 |
-
first_host = obs.discovered_hosts[0]
|
| 69 |
-
last_reward = float(rs.reward or 0.0)
|
| 70 |
-
print(
|
| 71 |
-
" [" + scenario_id + " s1] PREFIX network_scan r="
|
| 72 |
-
+ format(last_reward, "+.3f"),
|
| 73 |
-
flush=True,
|
| 74 |
-
)
|
| 75 |
-
if rs.done:
|
| 76 |
-
return last_reward, steps_done
|
| 77 |
-
|
| 78 |
-
a2 = SecurityAuditAction(
|
| 79 |
-
action_type="use_tool",
|
| 80 |
-
tool_name="web_crawl",
|
| 81 |
-
arguments={"host": first_host},
|
| 82 |
-
)
|
| 83 |
-
rs = e.step(a2)
|
| 84 |
-
obs = rs.observation
|
| 85 |
-
steps_done = 2
|
| 86 |
-
last_reward = float(rs.reward or 0.0)
|
| 87 |
-
print(
|
| 88 |
-
" [" + scenario_id + " s2] PREFIX web_crawl r="
|
| 89 |
-
+ format(last_reward, "+.3f"),
|
| 90 |
-
flush=True,
|
| 91 |
-
)
|
| 92 |
-
if rs.done:
|
| 93 |
-
return last_reward, steps_done
|
| 94 |
-
|
| 95 |
-
a3 = SecurityAuditAction(
|
| 96 |
-
action_type="use_tool",
|
| 97 |
-
tool_name="test_injection",
|
| 98 |
-
arguments={"host": first_host, "endpoint": "/api/login"},
|
| 99 |
-
)
|
| 100 |
-
rs = e.step(a3)
|
| 101 |
-
obs = rs.observation
|
| 102 |
-
steps_done = 3
|
| 103 |
-
last_reward = float(rs.reward or 0.0)
|
| 104 |
-
print(
|
| 105 |
-
" [" + scenario_id + " s3] PREFIX test_injection r="
|
| 106 |
-
+ format(last_reward, "+.3f"),
|
| 107 |
-
flush=True,
|
| 108 |
-
)
|
| 109 |
-
if rs.done:
|
| 110 |
-
return last_reward, steps_done
|
| 111 |
-
|
| 112 |
-
# Trained model takes over.
|
| 113 |
-
for step in range(3, max_steps):
|
| 114 |
-
steps_done = step + 1
|
| 115 |
-
|
| 116 |
-
if submit_count >= target:
|
| 117 |
-
rs = e.step(SecurityAuditAction(action_type="generate_report"))
|
| 118 |
-
last_reward = float(rs.reward or 0.0)
|
| 119 |
-
break
|
| 120 |
-
|
| 121 |
-
user_msg = render_observation(obs)
|
| 122 |
-
messages = [
|
| 123 |
-
{"role": "system", "content": SYSTEM_PROMPT},
|
| 124 |
-
{"role": "user", "content": user_msg},
|
| 125 |
-
]
|
| 126 |
-
text = _gen(messages)
|
| 127 |
-
action = parse_action(text)
|
| 128 |
-
|
| 129 |
-
if action.action_type == "list_tools":
|
| 130 |
-
list_tools_streak += 1
|
| 131 |
-
if list_tools_streak >= 2:
|
| 132 |
-
action = SecurityAuditAction(
|
| 133 |
-
action_type="use_tool",
|
| 134 |
-
tool_name="test_injection",
|
| 135 |
-
arguments={"host": first_host, "endpoint": "/api/login"},
|
| 136 |
-
)
|
| 137 |
-
list_tools_streak = 0
|
| 138 |
-
else:
|
| 139 |
-
list_tools_streak = 0
|
| 140 |
-
|
| 141 |
-
if action.action_type == "submit_finding":
|
| 142 |
-
submit_count += 1
|
| 143 |
-
|
| 144 |
-
rs = e.step(action)
|
| 145 |
-
obs = rs.observation
|
| 146 |
-
last_reward = float(rs.reward or 0.0)
|
| 147 |
-
tn = action.tool_name or ""
|
| 148 |
-
line = " [" + scenario_id + " s" + str(steps_done) + "] " + action.action_type
|
| 149 |
-
if tn:
|
| 150 |
-
line += "(" + tn + ")"
|
| 151 |
-
line += " sub=" + str(submit_count) + " r=" + format(last_reward, "+.3f")
|
| 152 |
-
print(line, flush=True)
|
| 153 |
-
if rs.done:
|
| 154 |
-
break
|
| 155 |
-
|
| 156 |
-
return last_reward, steps_done
|
| 157 |
-
finally:
|
| 158 |
-
e.__exit__(None, None, None)
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
trained = {}
|
| 162 |
-
for sid, mx in (("easy", 25), ("medium", 35), ("hard", 45)):
|
| 163 |
-
print("\n>>> hybrid_eval " + sid, flush=True)
|
| 164 |
-
s, n = run_episode_hybrid(sid, mx)
|
| 165 |
-
trained[sid] = s
|
| 166 |
-
print(
|
| 167 |
-
" RESULT " + sid + ": " + format(s, ".4f") + " in " + str(n) + " steps",
|
| 168 |
-
flush=True,
|
| 169 |
-
)
|
| 170 |
-
|
| 171 |
-
trained["average"] = sum(trained[k] for k in ("easy", "medium", "hard")) / 3
|
| 172 |
-
with open("trained_scores.json", "w") as f:
|
| 173 |
-
json.dump(trained, f, indent=2)
|
| 174 |
-
print()
|
| 175 |
-
print("HYBRID TRAINED:", json.dumps(trained, indent=2))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,85 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""Convert markdown notebook to Jupyter .ipynb format."""
|
| 3 |
-
|
| 4 |
-
import json
|
| 5 |
-
import re
|
| 6 |
-
|
| 7 |
-
def markdown_to_notebook(md_file, output_file):
|
| 8 |
-
"""Convert markdown file with code blocks to Jupyter notebook."""
|
| 9 |
-
|
| 10 |
-
with open(md_file, 'r') as f:
|
| 11 |
-
content = f.read()
|
| 12 |
-
|
| 13 |
-
cells = []
|
| 14 |
-
|
| 15 |
-
# Split by ## Cell markers
|
| 16 |
-
cell_blocks = re.split(r'## Cell \d+:', content)
|
| 17 |
-
|
| 18 |
-
# First block is metadata/title
|
| 19 |
-
if cell_blocks[0].strip():
|
| 20 |
-
cells.append({
|
| 21 |
-
"cell_type": "markdown",
|
| 22 |
-
"metadata": {},
|
| 23 |
-
"source": [cell_blocks[0].strip()]
|
| 24 |
-
})
|
| 25 |
-
|
| 26 |
-
# Process each cell
|
| 27 |
-
for block in cell_blocks[1:]:
|
| 28 |
-
lines = block.strip().split('\n')
|
| 29 |
-
|
| 30 |
-
# Extract title (first line)
|
| 31 |
-
title = lines[0].strip() if lines else "Cell"
|
| 32 |
-
|
| 33 |
-
# Find code block
|
| 34 |
-
code_match = re.search(r'```python\n(.*?)\n```', block, re.DOTALL)
|
| 35 |
-
|
| 36 |
-
if code_match:
|
| 37 |
-
code = code_match.group(1)
|
| 38 |
-
|
| 39 |
-
# Add markdown cell with title
|
| 40 |
-
cells.append({
|
| 41 |
-
"cell_type": "markdown",
|
| 42 |
-
"metadata": {},
|
| 43 |
-
"source": [f"### {title}"]
|
| 44 |
-
})
|
| 45 |
-
|
| 46 |
-
# Add code cell
|
| 47 |
-
cells.append({
|
| 48 |
-
"cell_type": "code",
|
| 49 |
-
"execution_count": None,
|
| 50 |
-
"metadata": {},
|
| 51 |
-
"outputs": [],
|
| 52 |
-
"source": code.split('\n')
|
| 53 |
-
})
|
| 54 |
-
|
| 55 |
-
# Create notebook structure
|
| 56 |
-
notebook = {
|
| 57 |
-
"cells": cells,
|
| 58 |
-
"metadata": {
|
| 59 |
-
"kernelspec": {
|
| 60 |
-
"display_name": "Python 3",
|
| 61 |
-
"language": "python",
|
| 62 |
-
"name": "python3"
|
| 63 |
-
},
|
| 64 |
-
"language_info": {
|
| 65 |
-
"name": "python",
|
| 66 |
-
"version": "3.10.0"
|
| 67 |
-
},
|
| 68 |
-
"colab": {
|
| 69 |
-
"name": "AISHA RL Training Notebook",
|
| 70 |
-
"provenance": [],
|
| 71 |
-
"collapsed_sections": []
|
| 72 |
-
}
|
| 73 |
-
},
|
| 74 |
-
"nbformat": 4,
|
| 75 |
-
"nbformat_minor": 4
|
| 76 |
-
}
|
| 77 |
-
|
| 78 |
-
# Write notebook
|
| 79 |
-
with open(output_file, 'w') as f:
|
| 80 |
-
json.dump(notebook, f, indent=1)
|
| 81 |
-
|
| 82 |
-
print(f"✓ Created {output_file}")
|
| 83 |
-
|
| 84 |
-
if __name__ == "__main__":
|
| 85 |
-
markdown_to_notebook('AISHA_TRAINING_NOTEBOOK.md', 'AISHA_RL_Training_Colab.ipynb')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
File without changes
|
|
@@ -1,574 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""
|
| 3 |
-
VAPT-Env Agent Comparison Script
|
| 4 |
-
==============================
|
| 5 |
-
|
| 6 |
-
Compares two agents on the SecurityAuditEnv:
|
| 7 |
-
1. Random Agent - picks random valid actions
|
| 8 |
-
2. Greedy LLM Agent - uses Claude Sonnet 4.6 or GPT-4o-mini via OpenAI client
|
| 9 |
-
|
| 10 |
-
Generates 3 publication-quality plots and a summary table.
|
| 11 |
-
"""
|
| 12 |
-
|
| 13 |
-
import os
|
| 14 |
-
import json
|
| 15 |
-
import random
|
| 16 |
-
import numpy as np
|
| 17 |
-
import matplotlib.pyplot as plt
|
| 18 |
-
from typing import Dict, List, Tuple, Any, Optional
|
| 19 |
-
from dataclasses import dataclass, field
|
| 20 |
-
import requests
|
| 21 |
-
from pathlib import Path
|
| 22 |
-
|
| 23 |
-
# ============================================================================
|
| 24 |
-
# SECTION 1: CONFIGURATION & SETUP
|
| 25 |
-
# ============================================================================
|
| 26 |
-
|
| 27 |
-
@dataclass
|
| 28 |
-
class Config:
|
| 29 |
-
"""Configuration for the comparison."""
|
| 30 |
-
api_base_url: str = "https://Sayuj63-Vapt-env.hf.space"
|
| 31 |
-
hf_token: str = ""
|
| 32 |
-
model_name: str = "claude-sonnet-4-6" # or "gpt-4o-mini"
|
| 33 |
-
num_episodes: int = 10
|
| 34 |
-
max_steps_per_episode: int = 30
|
| 35 |
-
scenario_id: str = "easy"
|
| 36 |
-
plots_dir: str = "./plots"
|
| 37 |
-
dpi: int = 150
|
| 38 |
-
|
| 39 |
-
def __post_init__(self):
|
| 40 |
-
"""Load from environment variables."""
|
| 41 |
-
self.hf_token = os.environ.get("HF_TOKEN", self.hf_token)
|
| 42 |
-
self.api_base_url = os.environ.get("API_BASE_URL", self.api_base_url)
|
| 43 |
-
self.model_name = os.environ.get("MODEL_NAME", self.model_name)
|
| 44 |
-
|
| 45 |
-
# Create plots directory
|
| 46 |
-
Path(self.plots_dir).mkdir(parents=True, exist_ok=True)
|
| 47 |
-
|
| 48 |
-
# ============================================================================
|
| 49 |
-
# SECTION 2: DATA MODELS
|
| 50 |
-
# ============================================================================
|
| 51 |
-
|
| 52 |
-
class SecurityAuditAction:
|
| 53 |
-
"""Action for the Security Audit environment."""
|
| 54 |
-
|
| 55 |
-
def __init__(self, action_type: str, tool_name: Optional[str] = None,
|
| 56 |
-
arguments: Optional[Dict[str, Any]] = None):
|
| 57 |
-
self.action_type = action_type
|
| 58 |
-
self.tool_name = tool_name
|
| 59 |
-
self.arguments = arguments or {}
|
| 60 |
-
|
| 61 |
-
def to_dict(self) -> Dict[str, Any]:
|
| 62 |
-
"""Convert to dictionary for API."""
|
| 63 |
-
return {
|
| 64 |
-
"action_type": self.action_type,
|
| 65 |
-
"tool_name": self.tool_name,
|
| 66 |
-
"arguments": self.arguments
|
| 67 |
-
}
|
| 68 |
-
|
| 69 |
-
class SecurityAuditObservation:
|
| 70 |
-
"""Observation from the environment."""
|
| 71 |
-
|
| 72 |
-
def __init__(self, data: Dict[str, Any]):
|
| 73 |
-
self.tool_output = data.get("tool_output", "")
|
| 74 |
-
self.available_tools = data.get("available_tools", [])
|
| 75 |
-
self.discovered_hosts = data.get("discovered_hosts", [])
|
| 76 |
-
self.discovered_services = data.get("discovered_services", {})
|
| 77 |
-
self.findings_submitted = data.get("findings_submitted", 0)
|
| 78 |
-
self.steps_remaining = data.get("steps_remaining", 0)
|
| 79 |
-
self.message = data.get("message", "")
|
| 80 |
-
self.done = data.get("done", False)
|
| 81 |
-
self.reward = data.get("reward", 0.0)
|
| 82 |
-
self.current_phase = data.get("current_phase", "reconnaissance")
|
| 83 |
-
self.metadata = data.get("metadata", {})
|
| 84 |
-
|
| 85 |
-
@dataclass
|
| 86 |
-
class EpisodeMetrics:
|
| 87 |
-
"""Metrics for a single episode."""
|
| 88 |
-
episode_num: int
|
| 89 |
-
total_reward: float = 0.0
|
| 90 |
-
cumulative_rewards: List[float] = field(default_factory=list)
|
| 91 |
-
vulnerabilities_found: int = 0
|
| 92 |
-
steps_taken: int = 0
|
| 93 |
-
actions_taken: List[str] = field(default_factory=list)
|
| 94 |
-
|
| 95 |
-
@dataclass
|
| 96 |
-
class AgentMetrics:
|
| 97 |
-
"""Aggregated metrics for an agent."""
|
| 98 |
-
agent_name: str
|
| 99 |
-
episodes: List[EpisodeMetrics] = field(default_factory=list)
|
| 100 |
-
|
| 101 |
-
@property
|
| 102 |
-
def avg_episode_reward(self) -> float:
|
| 103 |
-
"""Average reward per episode."""
|
| 104 |
-
if not self.episodes:
|
| 105 |
-
return 0.0
|
| 106 |
-
return np.mean([e.total_reward for e in self.episodes])
|
| 107 |
-
|
| 108 |
-
@property
|
| 109 |
-
def avg_vulns_found(self) -> float:
|
| 110 |
-
"""Average vulnerabilities found per episode."""
|
| 111 |
-
if not self.episodes:
|
| 112 |
-
return 0.0
|
| 113 |
-
return np.mean([e.vulnerabilities_found for e in self.episodes])
|
| 114 |
-
|
| 115 |
-
@property
|
| 116 |
-
def best_episode_reward(self) -> float:
|
| 117 |
-
"""Best episode reward."""
|
| 118 |
-
if not self.episodes:
|
| 119 |
-
return 0.0
|
| 120 |
-
return max(e.total_reward for e in self.episodes)
|
| 121 |
-
|
| 122 |
-
@property
|
| 123 |
-
def episode_rewards(self) -> List[float]:
|
| 124 |
-
"""List of all episode rewards."""
|
| 125 |
-
return [e.total_reward for e in self.episodes]
|
| 126 |
-
|
| 127 |
-
@property
|
| 128 |
-
def cumulative_reward_curves(self) -> List[List[float]]:
|
| 129 |
-
"""Cumulative reward curves for all episodes."""
|
| 130 |
-
return [e.cumulative_rewards for e in self.episodes]
|
| 131 |
-
|
| 132 |
-
# ============================================================================
|
| 133 |
-
# SECTION 3: ENVIRONMENT CLIENT
|
| 134 |
-
# ============================================================================
|
| 135 |
-
|
| 136 |
-
class SecurityAuditEnv:
|
| 137 |
-
"""Client for the Security Audit Environment."""
|
| 138 |
-
|
| 139 |
-
def __init__(self, base_url: str):
|
| 140 |
-
self.base_url = base_url.rstrip('/')
|
| 141 |
-
self.session = requests.Session()
|
| 142 |
-
self.episode_id = None
|
| 143 |
-
|
| 144 |
-
def reset(self, scenario_id: str = "easy") -> SecurityAuditObservation:
|
| 145 |
-
"""Reset the environment."""
|
| 146 |
-
url = f"{self.base_url}/reset"
|
| 147 |
-
payload = {"scenario_id": scenario_id}
|
| 148 |
-
|
| 149 |
-
try:
|
| 150 |
-
response = self.session.post(url, json=payload, timeout=30)
|
| 151 |
-
response.raise_for_status()
|
| 152 |
-
data = response.json()
|
| 153 |
-
self.episode_id = data.get("episode_id")
|
| 154 |
-
obs_data = data.get("observation", {})
|
| 155 |
-
return SecurityAuditObservation(obs_data)
|
| 156 |
-
except Exception as e:
|
| 157 |
-
print(f"Error resetting environment: {e}")
|
| 158 |
-
raise
|
| 159 |
-
|
| 160 |
-
def step(self, action: SecurityAuditAction) -> Tuple[SecurityAuditObservation, float, bool]:
|
| 161 |
-
"""Execute one step."""
|
| 162 |
-
url = f"{self.base_url}/step"
|
| 163 |
-
payload = action.to_dict()
|
| 164 |
-
|
| 165 |
-
try:
|
| 166 |
-
response = self.session.post(url, json=payload, timeout=30)
|
| 167 |
-
response.raise_for_status()
|
| 168 |
-
data = response.json()
|
| 169 |
-
obs_data = data.get("observation", {})
|
| 170 |
-
observation = SecurityAuditObservation(obs_data)
|
| 171 |
-
reward = data.get("reward", 0.0)
|
| 172 |
-
done = data.get("done", False)
|
| 173 |
-
return observation, reward, done
|
| 174 |
-
except Exception as e:
|
| 175 |
-
print(f"Error stepping environment: {e}")
|
| 176 |
-
raise
|
| 177 |
-
|
| 178 |
-
# ============================================================================
|
| 179 |
-
# SECTION 4: LLM AGENT
|
| 180 |
-
# ============================================================================
|
| 181 |
-
|
| 182 |
-
class LLMAgent:
|
| 183 |
-
"""Greedy LLM agent using OpenAI client."""
|
| 184 |
-
|
| 185 |
-
def __init__(self, model_name: str = "gpt-4o-mini"):
|
| 186 |
-
self.model_name = model_name
|
| 187 |
-
self.api_key = os.environ.get("OPENAI_API_KEY", "")
|
| 188 |
-
|
| 189 |
-
if not self.api_key:
|
| 190 |
-
print("Warning: OPENAI_API_KEY not set. LLM agent may not work.")
|
| 191 |
-
|
| 192 |
-
def encode_observation(self, obs: SecurityAuditObservation) -> str:
|
| 193 |
-
"""Convert observation to text for model input."""
|
| 194 |
-
parts = [
|
| 195 |
-
f"Current Phase: {obs.current_phase}",
|
| 196 |
-
f"Discovered Hosts: {', '.join(obs.discovered_hosts) if obs.discovered_hosts else 'None'}",
|
| 197 |
-
f"Discovered Services: {json.dumps(obs.discovered_services) if obs.discovered_services else 'None'}",
|
| 198 |
-
f"Findings Submitted: {obs.findings_submitted}",
|
| 199 |
-
f"Steps Remaining: {obs.steps_remaining}",
|
| 200 |
-
f"Last Message: {obs.message[:100]}",
|
| 201 |
-
]
|
| 202 |
-
return "\n".join(parts)
|
| 203 |
-
|
| 204 |
-
def generate_action(self, obs: SecurityAuditObservation) -> SecurityAuditAction:
|
| 205 |
-
"""Generate action from observation using LLM."""
|
| 206 |
-
obs_text = self.encode_observation(obs)
|
| 207 |
-
|
| 208 |
-
prompt = f"""You are a security auditor analyzing a target network.
|
| 209 |
-
|
| 210 |
-
Current Observation:
|
| 211 |
-
{obs_text}
|
| 212 |
-
|
| 213 |
-
Available Actions:
|
| 214 |
-
1. list_tools - Get available security tools
|
| 215 |
-
2. use_tool - Execute a security tool (requires tool_name and arguments)
|
| 216 |
-
3. submit_finding - Report a vulnerability
|
| 217 |
-
4. generate_report - End the audit
|
| 218 |
-
|
| 219 |
-
Based on the current phase and discovered information, choose the BEST next action.
|
| 220 |
-
Respond with ONLY a valid JSON object, no other text:
|
| 221 |
-
|
| 222 |
-
{{
|
| 223 |
-
"action_type": "list_tools" | "use_tool" | "submit_finding" | "generate_report",
|
| 224 |
-
"tool_name": "network_scan" | "service_fingerprint" | "web_crawl" | "vulnerability_scan" | null,
|
| 225 |
-
"arguments": {{"host": "192.168.1.1"}} or {{"title": "...", "host": "...", "severity": "..."}} or {{}}
|
| 226 |
-
}}
|
| 227 |
-
|
| 228 |
-
Choose wisely to maximize vulnerabilities found and minimize wasted steps."""
|
| 229 |
-
|
| 230 |
-
try:
|
| 231 |
-
# Use OpenAI client pattern
|
| 232 |
-
import openai
|
| 233 |
-
|
| 234 |
-
client = openai.OpenAI(api_key=self.api_key)
|
| 235 |
-
response = client.chat.completions.create(
|
| 236 |
-
model=self.model_name,
|
| 237 |
-
messages=[
|
| 238 |
-
{"role": "system", "content": "You are a security auditor. Respond with only valid JSON."},
|
| 239 |
-
{"role": "user", "content": prompt}
|
| 240 |
-
],
|
| 241 |
-
temperature=0.7,
|
| 242 |
-
max_tokens=200,
|
| 243 |
-
)
|
| 244 |
-
|
| 245 |
-
response_text = response.choices[0].message.content
|
| 246 |
-
|
| 247 |
-
# Parse JSON from response
|
| 248 |
-
try:
|
| 249 |
-
json_start = response_text.find('{')
|
| 250 |
-
json_end = response_text.rfind('}') + 1
|
| 251 |
-
if json_start >= 0 and json_end > json_start:
|
| 252 |
-
action_json = json.loads(response_text[json_start:json_end])
|
| 253 |
-
return SecurityAuditAction(
|
| 254 |
-
action_type=action_json.get("action_type", "list_tools"),
|
| 255 |
-
tool_name=action_json.get("tool_name"),
|
| 256 |
-
arguments=action_json.get("arguments", {})
|
| 257 |
-
)
|
| 258 |
-
except json.JSONDecodeError:
|
| 259 |
-
pass
|
| 260 |
-
|
| 261 |
-
except Exception as e:
|
| 262 |
-
print(f"LLM error: {e}")
|
| 263 |
-
|
| 264 |
-
# Fallback to list_tools
|
| 265 |
-
return SecurityAuditAction(action_type="list_tools")
|
| 266 |
-
|
| 267 |
-
# ============================================================================
|
| 268 |
-
# SECTION 5: RANDOM AGENT
|
| 269 |
-
# ============================================================================
|
| 270 |
-
|
| 271 |
-
class RandomAgent:
|
| 272 |
-
"""Random agent that picks random valid actions."""
|
| 273 |
-
|
| 274 |
-
def generate_action(self, obs: SecurityAuditObservation) -> SecurityAuditAction:
|
| 275 |
-
"""Generate random action."""
|
| 276 |
-
action_types = ["list_tools", "use_tool", "submit_finding"]
|
| 277 |
-
action_type = random.choice(action_types)
|
| 278 |
-
|
| 279 |
-
if action_type == "use_tool":
|
| 280 |
-
tools = ["network_scan", "service_fingerprint", "web_crawl", "vulnerability_scan"]
|
| 281 |
-
return SecurityAuditAction(
|
| 282 |
-
action_type="use_tool",
|
| 283 |
-
tool_name=random.choice(tools),
|
| 284 |
-
arguments={"host": f"192.168.1.{random.randint(1, 10)}"}
|
| 285 |
-
)
|
| 286 |
-
elif action_type == "submit_finding":
|
| 287 |
-
return SecurityAuditAction(
|
| 288 |
-
action_type="submit_finding",
|
| 289 |
-
arguments={
|
| 290 |
-
"title": f"Finding {random.randint(1, 100)}",
|
| 291 |
-
"host": f"192.168.1.{random.randint(1, 10)}",
|
| 292 |
-
"severity": random.choice(["low", "medium", "high"]),
|
| 293 |
-
"cvss_score": round(random.uniform(3.0, 9.8), 1),
|
| 294 |
-
"cwe": f"CWE-{random.randint(1, 1000)}",
|
| 295 |
-
"owasp": random.choice(["A01:2021", "A02:2021", "A03:2021"])
|
| 296 |
-
}
|
| 297 |
-
)
|
| 298 |
-
else:
|
| 299 |
-
return SecurityAuditAction(action_type="list_tools")
|
| 300 |
-
|
| 301 |
-
# ============================================================================
|
| 302 |
-
# SECTION 6: EPISODE RUNNER
|
| 303 |
-
# ============================================================================
|
| 304 |
-
|
| 305 |
-
def run_episode(agent, env: SecurityAuditEnv, agent_name: str,
|
| 306 |
-
episode_num: int, max_steps: int) -> EpisodeMetrics:
|
| 307 |
-
"""Run a single episode with an agent."""
|
| 308 |
-
print(f" {agent_name} Episode {episode_num + 1}/10", end=" ", flush=True)
|
| 309 |
-
|
| 310 |
-
obs = env.reset(scenario_id="easy")
|
| 311 |
-
metrics = EpisodeMetrics(episode_num=episode_num + 1)
|
| 312 |
-
|
| 313 |
-
step = 0
|
| 314 |
-
while not obs.done and step < max_steps:
|
| 315 |
-
# Generate action
|
| 316 |
-
action = agent.generate_action(obs)
|
| 317 |
-
|
| 318 |
-
# Step environment
|
| 319 |
-
obs, reward, done = env.step(action)
|
| 320 |
-
|
| 321 |
-
# Track metrics
|
| 322 |
-
metrics.total_reward += reward
|
| 323 |
-
metrics.cumulative_rewards.append(metrics.total_reward)
|
| 324 |
-
metrics.actions_taken.append(action.action_type)
|
| 325 |
-
metrics.steps_taken += 1
|
| 326 |
-
|
| 327 |
-
# Count vulnerabilities (from findings_submitted)
|
| 328 |
-
metrics.vulnerabilities_found = obs.findings_submitted
|
| 329 |
-
|
| 330 |
-
step += 1
|
| 331 |
-
|
| 332 |
-
print(f"→ Reward: {metrics.total_reward:.4f}, Vulns: {metrics.vulnerabilities_found}")
|
| 333 |
-
return metrics
|
| 334 |
-
|
| 335 |
-
# ============================================================================
|
| 336 |
-
# SECTION 7: PLOTTING
|
| 337 |
-
# ============================================================================
|
| 338 |
-
|
| 339 |
-
def plot_episode_rewards(random_metrics: AgentMetrics, llm_metrics: AgentMetrics,
|
| 340 |
-
output_path: str, dpi: int = 150):
|
| 341 |
-
"""Plot 1: Episode rewards comparison."""
|
| 342 |
-
plt.figure(figsize=(10, 6))
|
| 343 |
-
|
| 344 |
-
episodes = list(range(1, len(random_metrics.episodes) + 1))
|
| 345 |
-
random_rewards = random_metrics.episode_rewards
|
| 346 |
-
llm_rewards = llm_metrics.episode_rewards
|
| 347 |
-
|
| 348 |
-
plt.plot(episodes, random_rewards, 'r--', marker='o', linewidth=2,
|
| 349 |
-
markersize=8, label='Random Agent', alpha=0.8)
|
| 350 |
-
plt.plot(episodes, llm_rewards, 'b-', marker='s', linewidth=2,
|
| 351 |
-
markersize=8, label='LLM Agent', alpha=0.8)
|
| 352 |
-
|
| 353 |
-
plt.xlabel('Episode', fontsize=12, fontweight='bold')
|
| 354 |
-
plt.ylabel('Total Reward (0.0 - 1.0)', fontsize=12, fontweight='bold')
|
| 355 |
-
plt.title('VAPT-Env: Episode Reward — Random vs LLM Agent', fontsize=14, fontweight='bold')
|
| 356 |
-
plt.legend(fontsize=11, loc='best')
|
| 357 |
-
plt.grid(True, alpha=0.3)
|
| 358 |
-
plt.xticks(episodes)
|
| 359 |
-
plt.ylim(0, max(max(random_rewards), max(llm_rewards)) * 1.1)
|
| 360 |
-
|
| 361 |
-
plt.tight_layout()
|
| 362 |
-
plt.savefig(output_path, dpi=dpi, bbox_inches='tight')
|
| 363 |
-
print(f"✓ Saved {output_path}")
|
| 364 |
-
plt.close()
|
| 365 |
-
|
| 366 |
-
def plot_cumulative_rewards(random_metrics: AgentMetrics, llm_metrics: AgentMetrics,
|
| 367 |
-
output_path: str, dpi: int = 150):
|
| 368 |
-
"""Plot 2: Cumulative reward curves with std dev bands."""
|
| 369 |
-
plt.figure(figsize=(12, 6))
|
| 370 |
-
|
| 371 |
-
# Get cumulative reward curves
|
| 372 |
-
random_curves = random_metrics.cumulative_reward_curves
|
| 373 |
-
llm_curves = llm_metrics.cumulative_reward_curves
|
| 374 |
-
|
| 375 |
-
# Pad curves to same length
|
| 376 |
-
max_len = max(max(len(c) for c in random_curves), max(len(c) for c in llm_curves))
|
| 377 |
-
|
| 378 |
-
random_curves_padded = []
|
| 379 |
-
for curve in random_curves:
|
| 380 |
-
padded = list(curve) + [curve[-1]] * (max_len - len(curve))
|
| 381 |
-
random_curves_padded.append(padded)
|
| 382 |
-
|
| 383 |
-
llm_curves_padded = []
|
| 384 |
-
for curve in llm_curves:
|
| 385 |
-
padded = list(curve) + [curve[-1]] * (max_len - len(curve))
|
| 386 |
-
llm_curves_padded.append(padded)
|
| 387 |
-
|
| 388 |
-
# Compute mean and std
|
| 389 |
-
random_mean = np.mean(random_curves_padded, axis=0)
|
| 390 |
-
random_std = np.std(random_curves_padded, axis=0)
|
| 391 |
-
|
| 392 |
-
llm_mean = np.mean(llm_curves_padded, axis=0)
|
| 393 |
-
llm_std = np.std(llm_curves_padded, axis=0)
|
| 394 |
-
|
| 395 |
-
steps = np.arange(len(random_mean))
|
| 396 |
-
|
| 397 |
-
# Plot with shaded std dev bands
|
| 398 |
-
plt.plot(steps, random_mean, 'r--', linewidth=2.5, label='Random Agent', alpha=0.8)
|
| 399 |
-
plt.fill_between(steps, random_mean - random_std, random_mean + random_std,
|
| 400 |
-
color='red', alpha=0.15)
|
| 401 |
-
|
| 402 |
-
plt.plot(steps, llm_mean, 'b-', linewidth=2.5, label='LLM Agent', alpha=0.8)
|
| 403 |
-
plt.fill_between(steps, llm_mean - llm_std, llm_mean + llm_std,
|
| 404 |
-
color='blue', alpha=0.15)
|
| 405 |
-
|
| 406 |
-
plt.xlabel('Step', fontsize=12, fontweight='bold')
|
| 407 |
-
plt.ylabel('Cumulative Reward', fontsize=12, fontweight='bold')
|
| 408 |
-
plt.title('VAPT-Env: Cumulative Reward Over Steps', fontsize=14, fontweight='bold')
|
| 409 |
-
plt.legend(fontsize=11, loc='best')
|
| 410 |
-
plt.grid(True, alpha=0.3)
|
| 411 |
-
|
| 412 |
-
plt.tight_layout()
|
| 413 |
-
plt.savefig(output_path, dpi=dpi, bbox_inches='tight')
|
| 414 |
-
print(f"✓ Saved {output_path}")
|
| 415 |
-
plt.close()
|
| 416 |
-
|
| 417 |
-
def plot_vulnerabilities_found(random_metrics: AgentMetrics, llm_metrics: AgentMetrics,
|
| 418 |
-
output_path: str, dpi: int = 150):
|
| 419 |
-
"""Plot 3: Vulnerability detection rate."""
|
| 420 |
-
plt.figure(figsize=(10, 6))
|
| 421 |
-
|
| 422 |
-
episodes = list(range(1, len(random_metrics.episodes) + 1))
|
| 423 |
-
random_vulns = [e.vulnerabilities_found for e in random_metrics.episodes]
|
| 424 |
-
llm_vulns = [e.vulnerabilities_found for e in llm_metrics.episodes]
|
| 425 |
-
|
| 426 |
-
x = np.arange(len(episodes))
|
| 427 |
-
width = 0.35
|
| 428 |
-
|
| 429 |
-
bars1 = plt.bar(x - width/2, random_vulns, width, label='Random Agent',
|
| 430 |
-
color='#ff7f0e', alpha=0.8, edgecolor='black', linewidth=1.5)
|
| 431 |
-
bars2 = plt.bar(x + width/2, llm_vulns, width, label='LLM Agent',
|
| 432 |
-
color='#2ca02c', alpha=0.8, edgecolor='black', linewidth=1.5)
|
| 433 |
-
|
| 434 |
-
# Add value labels on bars
|
| 435 |
-
for bars in [bars1, bars2]:
|
| 436 |
-
for bar in bars:
|
| 437 |
-
height = bar.get_height()
|
| 438 |
-
plt.text(bar.get_x() + bar.get_width()/2., height,
|
| 439 |
-
f'{int(height)}',
|
| 440 |
-
ha='center', va='bottom', fontsize=9, fontweight='bold')
|
| 441 |
-
|
| 442 |
-
plt.xlabel('Episode', fontsize=12, fontweight='bold')
|
| 443 |
-
plt.ylabel('Vulnerabilities Found / Total (3)', fontsize=12, fontweight='bold')
|
| 444 |
-
plt.title('VAPT-Env: Vulnerability Detection Rate', fontsize=14, fontweight='bold')
|
| 445 |
-
plt.xticks(x, episodes)
|
| 446 |
-
plt.ylim(0, 3.5)
|
| 447 |
-
plt.legend(fontsize=11, loc='best')
|
| 448 |
-
plt.grid(True, alpha=0.3, axis='y')
|
| 449 |
-
|
| 450 |
-
plt.tight_layout()
|
| 451 |
-
plt.savefig(output_path, dpi=dpi, bbox_inches='tight')
|
| 452 |
-
print(f"✓ Saved {output_path}")
|
| 453 |
-
plt.close()
|
| 454 |
-
|
| 455 |
-
# ============================================================================
|
| 456 |
-
# SECTION 8: SUMMARY TABLE
|
| 457 |
-
# ============================================================================
|
| 458 |
-
|
| 459 |
-
def print_summary_table(random_metrics: AgentMetrics, llm_metrics: AgentMetrics):
|
| 460 |
-
"""Print summary table."""
|
| 461 |
-
print("\n" + "=" * 70)
|
| 462 |
-
print("VAPT-Env AGENT COMPARISON SUMMARY")
|
| 463 |
-
print("=" * 70)
|
| 464 |
-
|
| 465 |
-
print(f"\n{'Metric':<25} {'Random Agent':<20} {'LLM Agent':<20}")
|
| 466 |
-
print("-" * 70)
|
| 467 |
-
|
| 468 |
-
# Average episode reward
|
| 469 |
-
print(f"{'Avg Episode Reward':<25} {random_metrics.avg_episode_reward:<20.4f} {llm_metrics.avg_episode_reward:<20.4f}")
|
| 470 |
-
|
| 471 |
-
# Average vulnerabilities found
|
| 472 |
-
print(f"{'Avg Vulns Found':<25} {random_metrics.avg_vulns_found:<20.1f} / 3 {llm_metrics.avg_vulns_found:<20.1f} / 3")
|
| 473 |
-
|
| 474 |
-
# Best episode score
|
| 475 |
-
print(f"{'Best Episode Score':<25} {random_metrics.best_episode_reward:<20.4f} {llm_metrics.best_episode_reward:<20.4f}")
|
| 476 |
-
|
| 477 |
-
# Std dev of rewards
|
| 478 |
-
random_std = np.std(random_metrics.episode_rewards)
|
| 479 |
-
llm_std = np.std(llm_metrics.episode_rewards)
|
| 480 |
-
print(f"{'Reward Std Dev':<25} {random_std:<20.4f} {llm_std:<20.4f}")
|
| 481 |
-
|
| 482 |
-
# Min episode score
|
| 483 |
-
random_min = min(random_metrics.episode_rewards)
|
| 484 |
-
llm_min = min(llm_metrics.episode_rewards)
|
| 485 |
-
print(f"{'Min Episode Score':<25} {random_min:<20.4f} {llm_min:<20.4f}")
|
| 486 |
-
|
| 487 |
-
# Improvement
|
| 488 |
-
improvement = ((llm_metrics.avg_episode_reward - random_metrics.avg_episode_reward) /
|
| 489 |
-
abs(random_metrics.avg_episode_reward)) * 100 if random_metrics.avg_episode_reward != 0 else 0
|
| 490 |
-
print(f"{'LLM Improvement':<25} {improvement:>19.1f}%")
|
| 491 |
-
|
| 492 |
-
print("=" * 70 + "\n")
|
| 493 |
-
|
| 494 |
-
# ============================================================================
|
| 495 |
-
# SECTION 9: MAIN
|
| 496 |
-
# ============================================================================
|
| 497 |
-
|
| 498 |
-
def main():
|
| 499 |
-
"""Main execution."""
|
| 500 |
-
print("\n" + "=" * 70)
|
| 501 |
-
print("VAPT-Env AGENT COMPARISON")
|
| 502 |
-
print("=" * 70)
|
| 503 |
-
|
| 504 |
-
# Setup
|
| 505 |
-
config = Config()
|
| 506 |
-
print(f"\nConfiguration:")
|
| 507 |
-
print(f" Environment: {config.api_base_url}")
|
| 508 |
-
print(f" Scenario: {config.scenario_id}")
|
| 509 |
-
print(f" Episodes: {config.num_episodes}")
|
| 510 |
-
print(f" Max steps per episode: {config.max_steps_per_episode}")
|
| 511 |
-
print(f" LLM Model: {config.model_name}")
|
| 512 |
-
print(f" Output directory: {config.plots_dir}")
|
| 513 |
-
|
| 514 |
-
# Initialize environment
|
| 515 |
-
print(f"\nInitializing environment...")
|
| 516 |
-
env = SecurityAuditEnv(base_url=config.api_base_url)
|
| 517 |
-
|
| 518 |
-
# Test connection
|
| 519 |
-
try:
|
| 520 |
-
obs = env.reset(scenario_id=config.scenario_id)
|
| 521 |
-
print(f"✓ Environment connected")
|
| 522 |
-
except Exception as e:
|
| 523 |
-
print(f"✗ Failed to connect to environment: {e}")
|
| 524 |
-
return
|
| 525 |
-
|
| 526 |
-
# Initialize agents
|
| 527 |
-
print(f"\nInitializing agents...")
|
| 528 |
-
random_agent = RandomAgent()
|
| 529 |
-
llm_agent = LLMAgent(model_name=config.model_name)
|
| 530 |
-
print(f"✓ Agents initialized")
|
| 531 |
-
|
| 532 |
-
# Run random agent
|
| 533 |
-
print(f"\n{'=' * 70}")
|
| 534 |
-
print("RANDOM AGENT - Running 10 episodes")
|
| 535 |
-
print("=" * 70)
|
| 536 |
-
random_metrics = AgentMetrics(agent_name="Random Agent")
|
| 537 |
-
for episode in range(config.num_episodes):
|
| 538 |
-
metrics = run_episode(random_agent, env, "Random", episode, config.max_steps_per_episode)
|
| 539 |
-
random_metrics.episodes.append(metrics)
|
| 540 |
-
|
| 541 |
-
# Run LLM agent
|
| 542 |
-
print(f"\n{'=' * 70}")
|
| 543 |
-
print("LLM AGENT - Running 10 episodes")
|
| 544 |
-
print("=" * 70)
|
| 545 |
-
llm_metrics = AgentMetrics(agent_name="LLM Agent")
|
| 546 |
-
for episode in range(config.num_episodes):
|
| 547 |
-
metrics = run_episode(llm_agent, env, "LLM", episode, config.max_steps_per_episode)
|
| 548 |
-
llm_metrics.episodes.append(metrics)
|
| 549 |
-
|
| 550 |
-
# Generate plots
|
| 551 |
-
print(f"\n{'=' * 70}")
|
| 552 |
-
print("GENERATING PLOTS")
|
| 553 |
-
print("=" * 70)
|
| 554 |
-
|
| 555 |
-
plot_episode_rewards(random_metrics, llm_metrics,
|
| 556 |
-
f"{config.plots_dir}/reward_per_episode.png",
|
| 557 |
-
dpi=config.dpi)
|
| 558 |
-
|
| 559 |
-
plot_cumulative_rewards(random_metrics, llm_metrics,
|
| 560 |
-
f"{config.plots_dir}/cumulative_reward_curve.png",
|
| 561 |
-
dpi=config.dpi)
|
| 562 |
-
|
| 563 |
-
plot_vulnerabilities_found(random_metrics, llm_metrics,
|
| 564 |
-
f"{config.plots_dir}/vulns_found.png",
|
| 565 |
-
dpi=config.dpi)
|
| 566 |
-
|
| 567 |
-
# Print summary
|
| 568 |
-
print_summary_table(random_metrics, llm_metrics)
|
| 569 |
-
|
| 570 |
-
print(f"✓ All plots saved to {config.plots_dir}/")
|
| 571 |
-
print(f"✓ Comparison complete!")
|
| 572 |
-
|
| 573 |
-
if __name__ == "__main__":
|
| 574 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
File without changes
|
|
File without changes
|
|
@@ -87,7 +87,7 @@ ENV_URL="https://Sayuj63-Vapt-env.hf.space" python inference.py
|
|
| 87 |
|
| 88 |
## Eval methodology
|
| 89 |
|
| 90 |
-
The eval uses an evaluation harness ([`colab_eval_v3.py`](https://github.com/Sayuj63/vapt-env/blob/main/colab_eval_v3.py)) layered on top of the trained adapter:
|
| 91 |
|
| 92 |
- 3-step scripted recon prefix (network_scan → web_crawl → test_injection on `/api/login`)
|
| 93 |
- Anti-collapse safety net (rotates through other endpoints when the trained policy emits `list_tools` ≥ 2× in a row)
|
|
|
|
| 87 |
|
| 88 |
## Eval methodology
|
| 89 |
|
| 90 |
+
The eval uses an evaluation harness ([`colab_eval_v3.py`](https://github.com/Sayuj63/vapt-env/blob/main/scripts/colab_eval_v3.py)) layered on top of the trained adapter:
|
| 91 |
|
| 92 |
- 3-step scripted recon prefix (network_scan → web_crawl → test_injection on `/api/login`)
|
| 93 |
- Anti-collapse safety net (rotates through other endpoints when the trained policy emits `list_tools` ≥ 2× in a row)
|