Sayuj63 commited on
Commit
faf47cc
·
1 Parent(s): 310e726

Clean root folder: 11 → 6 Python files, BLOG.md + README.md prominent

Browse files

DELETED (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 CHANGED
@@ -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
 
README.md CHANGED
@@ -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)
aisha_rl_training.py DELETED
@@ -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()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
colab_eval_hybrid.py DELETED
@@ -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))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
convert_to_notebook.py DELETED
@@ -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')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
demo_multiagent.py → examples/demo_multiagent.py RENAMED
File without changes
generate_plots.py DELETED
@@ -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()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
build_trajectory_dataset.py → scripts/build_trajectory_dataset.py RENAMED
File without changes
colab_eval_v3.py → scripts/colab_eval_v3.py RENAMED
File without changes
scripts/push_model_card.py CHANGED
@@ -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)