Spaces:
Sleeping
Sleeping
Upload CyberSecurity_OWASP environment
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .dockerignore +9 -0
- .hfignore +12 -0
- 00_PROJECT_BRIEF.md +145 -0
- 01_ARCHITECTURE.md +479 -0
- AGENTS.md +1197 -0
- Dockerfile +28 -0
- README.md +158 -4
- __init__.py +22 -0
- bug_mutator.py +17 -0
- client.py +39 -0
- evals.py +63 -0
- fixture_generator.py +17 -0
- models.py +81 -0
- openenv.yaml +7 -0
- policy_graph.py +105 -0
- pyproject.toml +48 -0
- rewards.py +66 -0
- safety.py +17 -0
- scenario_compiler.py +46 -0
- scripts/docker_build.sh +3 -0
- scripts/docker_run.sh +3 -0
- scripts/generate_scenarios.sh +3 -0
- scripts/modal_ephemeral_train.py +163 -0
- scripts/modal_run_ephemeral.sh +3 -0
- scripts/push_space.sh +3 -0
- scripts/run_local.sh +3 -0
- scripts/smoke_test.sh +3 -0
- server/CyberSecurity_OWASP_environment.py +366 -0
- server/Dockerfile +80 -0
- server/__init__.py +11 -0
- server/app.py +62 -0
- server/requirements.txt +6 -0
- server/reward_engine.py +49 -0
- template_renderer.py +97 -0
- tests/__init__.py +1 -0
- tests/helpers.py +51 -0
- tests/test_anti_cheat.py +16 -0
- tests/test_invalid_actions.py +48 -0
- tests/test_models.py +14 -0
- tests/test_reset_step_state.py +25 -0
- tests/test_rewards.py +67 -0
- tests/test_rollouts.py +29 -0
- tests/test_seed_reproducibility.py +10 -0
- training/configs/grpo_small.yaml +9 -0
- training/eval_before_after.py +29 -0
- training/reward_funcs.py +25 -0
- training/rollout.py +84 -0
- training/trackio_utils.py +40 -0
- training/train_grpo.py +46 -0
- uv.lock +0 -0
.dockerignore
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.git
|
| 2 |
+
.venv
|
| 3 |
+
__pycache__
|
| 4 |
+
*.pyc
|
| 5 |
+
.pytest_cache
|
| 6 |
+
openenv_CyberSecurity_OWASP.egg-info
|
| 7 |
+
outputs
|
| 8 |
+
.env
|
| 9 |
+
.env.*
|
.hfignore
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.git/
|
| 2 |
+
.venv/
|
| 3 |
+
__pycache__/
|
| 4 |
+
**/__pycache__/
|
| 5 |
+
*.pyc
|
| 6 |
+
.pytest_cache/
|
| 7 |
+
openenv_CyberSecurity_OWASP.egg-info/
|
| 8 |
+
outputs/logs/*
|
| 9 |
+
outputs/evals/*
|
| 10 |
+
outputs/rollouts/*
|
| 11 |
+
.env
|
| 12 |
+
.env.*
|
00_PROJECT_BRIEF.md
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 00_PROJECT_BRIEF.md
|
| 2 |
+
|
| 3 |
+
# CyberSecurity_OWASP — Project Brief
|
| 4 |
+
|
| 5 |
+
## 1. One-line summary
|
| 6 |
+
|
| 7 |
+
`CyberSecurity_OWASP` is an OpenEnv reinforcement-learning environment where a **single LLM agent learns the full defensive workflow for OWASP access-control bugs**: understand the intended authorization policy, discover a broken access-control path in a local synthetic app, patch the code, and prove that the fix blocks unauthorized access without breaking valid user flows.
|
| 8 |
+
|
| 9 |
+
## 2. Problem
|
| 10 |
+
|
| 11 |
+
Broken access control remains one of the most important web-application security risks because the correct behavior is usually **application-specific**. Generic scanners can find some missing checks, but they often lack enough context to answer the real engineering question:
|
| 12 |
+
|
| 13 |
+
> “Given this app’s policy, users, roles, tenants, routes, and data model, is this behavior intended or a security bug?”
|
| 14 |
+
|
| 15 |
+
Modern LLMs can read code, reason about tests, and propose patches, but they still struggle with:
|
| 16 |
+
|
| 17 |
+
- distinguishing intended public/feature behavior from accidental over-permission;
|
| 18 |
+
- following authorization logic across routes, middleware, ORM queries, tenants, roles, and ownership checks;
|
| 19 |
+
- validating that a patch fixes the bug without introducing regressions;
|
| 20 |
+
- avoiding reward hacking when tests are visible or too narrow;
|
| 21 |
+
- generalizing across app templates instead of memorizing one codebase.
|
| 22 |
+
|
| 23 |
+
`CyberSecurity_OWASP` turns this into a trainable environment.
|
| 24 |
+
|
| 25 |
+
## 3. What the environment trains
|
| 26 |
+
|
| 27 |
+
The environment trains **one agent**, not a separate red-team and blue-team pair. The same model must perform the entire secure-repair loop:
|
| 28 |
+
|
| 29 |
+
1. **Understand policy** — read the policy graph, user roles, route intent, tenant rules, and allowed operations.
|
| 30 |
+
2. **Discover evidence** — use safe local requests, logs, route metadata, and visible tests to identify the likely access-control failure.
|
| 31 |
+
3. **Patch** — edit application code, middleware, route guards, query filters, or policy mappings.
|
| 32 |
+
4. **Validate** — run public tests, policy checks, and regression tests.
|
| 33 |
+
5. **Submit** — final answer is judged by deterministic hidden tests and reward logic.
|
| 34 |
+
|
| 35 |
+
## 4. Scope for MVP
|
| 36 |
+
|
| 37 |
+
The MVP should focus on **OWASP A01: Broken Access Control** with ASVS-inspired access-control requirements.
|
| 38 |
+
|
| 39 |
+
Initial scenario families:
|
| 40 |
+
|
| 41 |
+
1. Missing route-level authorization check.
|
| 42 |
+
2. Insecure direct object reference / object ownership bug.
|
| 43 |
+
3. Cross-tenant data leakage.
|
| 44 |
+
4. Role confusion: user/admin/support/editor boundary error.
|
| 45 |
+
5. Client-side-only authorization assumption.
|
| 46 |
+
6. Query filter omission in list/search/export endpoint.
|
| 47 |
+
7. Over-broad update/delete permission.
|
| 48 |
+
8. Feature route intentionally public, so the agent must not over-secure it.
|
| 49 |
+
|
| 50 |
+
Recommended MVP size: **8 scenario families × 3 app templates × 25 seeds = 600 trainable scenarios**, with separate held-out families and hidden seeds for evaluation.
|
| 51 |
+
|
| 52 |
+
## 5. Why this is useful
|
| 53 |
+
|
| 54 |
+
This environment is useful because it targets a real gap between today’s scanners and useful defensive agents:
|
| 55 |
+
|
| 56 |
+
- **Scanners detect patterns.** This environment trains policy-aware reasoning.
|
| 57 |
+
- **Unit tests check known cases.** This environment includes hidden authorization invariants.
|
| 58 |
+
- **Static repair can overfit.** This environment forces the model to preserve valid business behavior.
|
| 59 |
+
- **One-app benchmarks are easy to memorize.** This environment compiles many equivalent-but-different apps from policy graphs, templates, route shapes, schema names, and hidden test seeds.
|
| 60 |
+
|
| 61 |
+
The outcome is a model that becomes better at a practical DevSecOps workflow: safely reviewing and repairing authorization logic in small-to-medium web apps.
|
| 62 |
+
|
| 63 |
+
## 6. What success looks like
|
| 64 |
+
|
| 65 |
+
A successful submission should show **measurable reward improvement** and better held-out security behavior after RL training.
|
| 66 |
+
|
| 67 |
+
### Minimum success criteria
|
| 68 |
+
|
| 69 |
+
- Environment runs through OpenEnv `reset`, `step`, and `state` APIs.
|
| 70 |
+
- Hosted on Hugging Face Spaces.
|
| 71 |
+
- Provides a minimal GRPO/TRL or Unsloth training script.
|
| 72 |
+
- Tracks training/eval metrics with Trackio or equivalent.
|
| 73 |
+
- Shows reward curves and before/after agent behavior.
|
| 74 |
+
- Uses deterministic reward as the primary reward source.
|
| 75 |
+
- Keeps hidden tests hidden from the agent.
|
| 76 |
+
|
| 77 |
+
### Target metrics
|
| 78 |
+
|
| 79 |
+
| Metric | MVP target |
|
| 80 |
+
|---|---:|
|
| 81 |
+
| Valid episode completion rate | ≥ 85% |
|
| 82 |
+
| Hidden authorization test pass rate | ≥ 65% after initial RL run |
|
| 83 |
+
| Regression preservation rate | ≥ 80% |
|
| 84 |
+
| Held-out scenario success lift vs base model | ≥ +15 percentage points |
|
| 85 |
+
| Reward-hacking incidents found in eval | 0 critical |
|
| 86 |
+
| Median patch size | ≤ 3 files changed |
|
| 87 |
+
|
| 88 |
+
## 7. Core design principle
|
| 89 |
+
|
| 90 |
+
The environment should reward **correct defensive repair**, not exploit creativity. The discovery stage exists only to help the agent gather enough local evidence to make a safe patch. The reward engine must never reward real-world misuse, data exfiltration, persistence, credential theft, or evasion behavior.
|
| 91 |
+
|
| 92 |
+
## 8. Deliverables for engineers
|
| 93 |
+
|
| 94 |
+
Initial implementation should produce:
|
| 95 |
+
|
| 96 |
+
```text
|
| 97 |
+
CyberSecurity_OWASP/
|
| 98 |
+
├── 00_PROJECT_BRIEF.md
|
| 99 |
+
├── 01_ARCHITECTURE.md
|
| 100 |
+
├── README.md
|
| 101 |
+
├── pyproject.toml
|
| 102 |
+
├── openenv.yaml
|
| 103 |
+
├── cybersecurity_owasp/
|
| 104 |
+
│ ├── __init__.py
|
| 105 |
+
│ ├── models.py
|
| 106 |
+
│ ├── client.py
|
| 107 |
+
│ ├── rewards.py
|
| 108 |
+
│ ├── scenarios/
|
| 109 |
+
│ │ ├── compiler.py
|
| 110 |
+
│ │ ├── policy_graph.py
|
| 111 |
+
│ │ ├── templates/
|
| 112 |
+
│ │ └── seeds/
|
| 113 |
+
│ ├── apps/
|
| 114 |
+
│ │ ├── fastapi_basic/
|
| 115 |
+
│ │ ├── express_basic/
|
| 116 |
+
│ │ └── django_basic/
|
| 117 |
+
│ ├── evals/
|
| 118 |
+
│ │ ├── public_tests.py
|
| 119 |
+
│ │ ├── hidden_invariants.py
|
| 120 |
+
│ │ └── heldout_eval.py
|
| 121 |
+
│ └── server/
|
| 122 |
+
│ ├── environment.py
|
| 123 |
+
│ ├── app.py
|
| 124 |
+
│ ├── requirements.txt
|
| 125 |
+
│ └── Dockerfile
|
| 126 |
+
├── training/
|
| 127 |
+
│ ├── train_grpo.py
|
| 128 |
+
│ ├── rollout.py
|
| 129 |
+
│ └── eval_before_after.py
|
| 130 |
+
└── outputs/
|
| 131 |
+
├── logs/
|
| 132 |
+
├── evals/
|
| 133 |
+
└── reward_curves/
|
| 134 |
+
```
|
| 135 |
+
|
| 136 |
+
## 9. Source notes and credibility
|
| 137 |
+
|
| 138 |
+
| Source | How it informs this project | Credibility |
|
| 139 |
+
|---|---|---:|
|
| 140 |
+
| OWASP Top 10 2025 / A01 Broken Access Control | Confirms current relevance of Broken Access Control as a top web-app risk. | 10/10 |
|
| 141 |
+
| OWASP ASVS | Provides security-control requirements that can be translated into policy invariants and hidden tests. | 9.5/10 |
|
| 142 |
+
| OpenEnv build/deploy docs | Defines the required OpenEnv structure: models, server, client, Docker, HF Spaces deployment. | 8.5/10 |
|
| 143 |
+
| Hackathon judging criteria | Aligns deliverables with scoring: innovation, storytelling, reward improvement, and training pipeline. | 9/10 |
|
| 144 |
+
| TRL/OpenEnv GRPO example | Shows a practical pattern for environment rollouts, reward functions, and Trackio logging. | 8/10 |
|
| 145 |
+
|
01_ARCHITECTURE.md
ADDED
|
@@ -0,0 +1,479 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 01_ARCHITECTURE.md
|
| 2 |
+
|
| 3 |
+
# CyberSecurity_OWASP — Architecture
|
| 4 |
+
|
| 5 |
+
## 1. System goal
|
| 6 |
+
|
| 7 |
+
`CyberSecurity_OWASP` is an OpenEnv environment for training a **single LLM policy** to perform a complete defensive authorization-repair workflow:
|
| 8 |
+
|
| 9 |
+
```text
|
| 10 |
+
Understand policy → discover local evidence → patch code → validate → submit
|
| 11 |
+
```
|
| 12 |
+
|
| 13 |
+
The environment is intentionally not a two-agent red-team/blue-team setup. The agent is one model with one trajectory. It must learn both sides of the defensive workflow: finding the policy violation and fixing it safely.
|
| 14 |
+
|
| 15 |
+
## 2. Final architecture diagram
|
| 16 |
+
|
| 17 |
+
```mermaid
|
| 18 |
+
flowchart TB
|
| 19 |
+
%% =========================
|
| 20 |
+
%% Offline Build Layer
|
| 21 |
+
%% =========================
|
| 22 |
+
subgraph A[Offline Scenario Factory]
|
| 23 |
+
A1[Policy Graph Generator\nroles, users, tenants, ownership, route intent]
|
| 24 |
+
A2[App Template Library\nFastAPI, Express, Django MVP templates]
|
| 25 |
+
A3[Bug Injector\nmissing guard, IDOR, tenant leak, role confusion, query omission]
|
| 26 |
+
A4[Scenario Compiler\nmaterializes app + DB + public tests + hidden invariants]
|
| 27 |
+
A5[Split Manager\ntrain seeds, validation seeds, hidden held-out seeds]
|
| 28 |
+
A1 --> A4
|
| 29 |
+
A2 --> A4
|
| 30 |
+
A3 --> A4
|
| 31 |
+
A5 --> A4
|
| 32 |
+
end
|
| 33 |
+
|
| 34 |
+
%% =========================
|
| 35 |
+
%% OpenEnv Runtime
|
| 36 |
+
%% =========================
|
| 37 |
+
subgraph B[CyberSecurity_OWASP OpenEnv Server]
|
| 38 |
+
B1[reset\(\)\nselect scenario + start sandbox]
|
| 39 |
+
B2[Sandbox App Runtime\nlocal app, DB fixture, logs, route map]
|
| 40 |
+
B3[Tool API exposed through step\(action\)\nReadFile, ListRoutes, SendLocalRequest, RunTests, ApplyPatch, SubmitFix]
|
| 41 |
+
B4[State Store\nepisode_id, step_count, scenario_id, patch diff, test history]
|
| 42 |
+
B5[Deterministic Reward Engine\npolicy tests + hidden tests + regression tests + penalties]
|
| 43 |
+
B6[state\(\)\nstructured metadata for debugging/eval]
|
| 44 |
+
B1 --> B2
|
| 45 |
+
B2 --> B3
|
| 46 |
+
B3 --> B4
|
| 47 |
+
B4 --> B5
|
| 48 |
+
B4 --> B6
|
| 49 |
+
end
|
| 50 |
+
|
| 51 |
+
%% =========================
|
| 52 |
+
%% Agent + Training
|
| 53 |
+
%% =========================
|
| 54 |
+
subgraph C[Single LLM Agent]
|
| 55 |
+
C1[Observation Parser]
|
| 56 |
+
C2[Planner\npolicy reasoning + patch strategy]
|
| 57 |
+
C3[Action Generator\nchooses next OpenEnv action]
|
| 58 |
+
C1 --> C2 --> C3
|
| 59 |
+
end
|
| 60 |
+
|
| 61 |
+
subgraph D[Training + Evaluation]
|
| 62 |
+
D1[Rollout Loop\nreset → step* → final reward]
|
| 63 |
+
D2[GRPO / TRL / Unsloth Training]
|
| 64 |
+
D3[Trackio Metrics\nreward curves, pass rates, patch size, steps]
|
| 65 |
+
D4[Held-out Eval Suite\nunseen templates, seeds, names, route structures]
|
| 66 |
+
D5[Demo Artifacts\nbefore/after traces, mini-blog, 2-minute video]
|
| 67 |
+
D1 --> D2 --> D3
|
| 68 |
+
D3 --> D4 --> D5
|
| 69 |
+
end
|
| 70 |
+
|
| 71 |
+
A4 --> B1
|
| 72 |
+
C3 -->|typed action| B3
|
| 73 |
+
B3 -->|observation + reward + done| C1
|
| 74 |
+
B5 --> D1
|
| 75 |
+
D2 --> C1
|
| 76 |
+
B5 --> D4
|
| 77 |
+
```
|
| 78 |
+
|
| 79 |
+
## 3. Component responsibilities
|
| 80 |
+
|
| 81 |
+
### 3.1 Scenario Factory
|
| 82 |
+
|
| 83 |
+
The scenario factory generates many small but realistic web apps from a structured authorization policy.
|
| 84 |
+
|
| 85 |
+
It should output:
|
| 86 |
+
|
| 87 |
+
- application code;
|
| 88 |
+
- route map;
|
| 89 |
+
- database fixture;
|
| 90 |
+
- user/session/token fixtures;
|
| 91 |
+
- policy graph;
|
| 92 |
+
- intentionally injected access-control bug;
|
| 93 |
+
- public tests visible to the agent;
|
| 94 |
+
- hidden tests invisible to the agent;
|
| 95 |
+
- metadata for eval and debugging.
|
| 96 |
+
|
| 97 |
+
The scenario compiler is the main anti-overfitting mechanism. It should vary:
|
| 98 |
+
|
| 99 |
+
- route names;
|
| 100 |
+
- schema names;
|
| 101 |
+
- ORM query structure;
|
| 102 |
+
- framework template;
|
| 103 |
+
- role names;
|
| 104 |
+
- tenant IDs;
|
| 105 |
+
- object ownership patterns;
|
| 106 |
+
- file layout;
|
| 107 |
+
- visible test coverage;
|
| 108 |
+
- hidden invariant seeds.
|
| 109 |
+
|
| 110 |
+
### 3.2 Policy Graph Generator
|
| 111 |
+
|
| 112 |
+
The policy graph is the ground truth for intended behavior.
|
| 113 |
+
|
| 114 |
+
Example internal representation:
|
| 115 |
+
|
| 116 |
+
```yaml
|
| 117 |
+
resources:
|
| 118 |
+
invoice:
|
| 119 |
+
owner_field: owner_user_id
|
| 120 |
+
tenant_field: tenant_id
|
| 121 |
+
roles:
|
| 122 |
+
user:
|
| 123 |
+
can:
|
| 124 |
+
- read:invoice where owner_user_id == actor.user_id
|
| 125 |
+
- update:invoice where owner_user_id == actor.user_id and status != locked
|
| 126 |
+
support:
|
| 127 |
+
can:
|
| 128 |
+
- read:invoice where tenant_id == actor.tenant_id
|
| 129 |
+
admin:
|
| 130 |
+
can:
|
| 131 |
+
- read:any_invoice where tenant_id == actor.tenant_id
|
| 132 |
+
- update:any_invoice where tenant_id == actor.tenant_id
|
| 133 |
+
public_routes:
|
| 134 |
+
- GET /health
|
| 135 |
+
- GET /pricing
|
| 136 |
+
forbidden:
|
| 137 |
+
- cross_tenant_read
|
| 138 |
+
- cross_tenant_update
|
| 139 |
+
- user_reads_other_user_invoice
|
| 140 |
+
```
|
| 141 |
+
|
| 142 |
+
The policy graph prevents false rewards for over-securing intentionally public or intentionally allowed routes.
|
| 143 |
+
|
| 144 |
+
### 3.3 Bug Injector
|
| 145 |
+
|
| 146 |
+
The bug injector creates controlled, defensive lab scenarios. It should only generate bugs inside local synthetic apps.
|
| 147 |
+
|
| 148 |
+
MVP bug classes:
|
| 149 |
+
|
| 150 |
+
| Bug class | Example failure mode | Expected fix type |
|
| 151 |
+
|---|---|---|
|
| 152 |
+
| Missing route guard | Protected endpoint lacks authorization middleware | Add policy check/middleware |
|
| 153 |
+
| IDOR / ownership bug | User can access another user’s object by changing ID | Add owner check in query/policy |
|
| 154 |
+
| Tenant leak | Tenant A can list Tenant B records | Add tenant filter |
|
| 155 |
+
| Role confusion | Support/editor/admin boundary is wrong | Correct role-to-permission mapping |
|
| 156 |
+
| Client-side-only auth | Server trusts UI to hide forbidden action | Enforce server-side authorization |
|
| 157 |
+
| Query omission | List/export/search endpoint lacks auth filter | Filter query by actor permissions |
|
| 158 |
+
| Over-broad mutation | User can update/delete forbidden object | Add mutation permission check |
|
| 159 |
+
| Public route decoy | Agent may wrongly lock down intended public endpoint | Preserve intended public behavior |
|
| 160 |
+
|
| 161 |
+
### 3.4 OpenEnv Server
|
| 162 |
+
|
| 163 |
+
The OpenEnv server should implement the standard lifecycle:
|
| 164 |
+
|
| 165 |
+
- `reset()` — initialize a fresh scenario instance.
|
| 166 |
+
- `step(action)` — execute one typed action and return observation, reward, and done.
|
| 167 |
+
- `state()` — expose episode metadata for debugging and evaluation.
|
| 168 |
+
|
| 169 |
+
Recommended package/class names:
|
| 170 |
+
|
| 171 |
+
```text
|
| 172 |
+
Repo name: CyberSecurity_OWASP
|
| 173 |
+
Python package: cybersecurity_owasp
|
| 174 |
+
Client class: CyberSecurityOWASPEnv
|
| 175 |
+
Action class: CyberSecurityOWASPAction
|
| 176 |
+
Observation: CyberSecurityOWASPObservation
|
| 177 |
+
State: CyberSecurityOWASPState
|
| 178 |
+
```
|
| 179 |
+
|
| 180 |
+
### 3.5 Tool API
|
| 181 |
+
|
| 182 |
+
The agent should interact through typed actions. Keep the interface small enough for RL but expressive enough for realistic repair.
|
| 183 |
+
|
| 184 |
+
```python
|
| 185 |
+
@dataclass
|
| 186 |
+
class CyberSecurityOWASPAction(Action):
|
| 187 |
+
action_type: Literal[
|
| 188 |
+
"read_file",
|
| 189 |
+
"list_files",
|
| 190 |
+
"list_routes",
|
| 191 |
+
"inspect_policy",
|
| 192 |
+
"send_local_request",
|
| 193 |
+
"run_public_tests",
|
| 194 |
+
"apply_patch",
|
| 195 |
+
"submit_fix",
|
| 196 |
+
]
|
| 197 |
+
arguments: dict
|
| 198 |
+
```
|
| 199 |
+
|
| 200 |
+
Recommended actions:
|
| 201 |
+
|
| 202 |
+
| Action | Purpose | Safety boundary |
|
| 203 |
+
|---|---|---|
|
| 204 |
+
| `inspect_policy` | Read intended authorization rules. | Only synthetic policy. |
|
| 205 |
+
| `list_routes` | See local app route map. | No internet target. |
|
| 206 |
+
| `read_file` | Inspect selected source file. | Sandbox allowlist only. |
|
| 207 |
+
| `send_local_request` | Validate behavior against local app. | Local generated app only. |
|
| 208 |
+
| `run_public_tests` | Run visible tests. | No hidden test disclosure. |
|
| 209 |
+
| `apply_patch` | Modify source through unified diff. | Patch size and file allowlist limits. |
|
| 210 |
+
| `submit_fix` | End episode and trigger hidden eval. | Final hidden score only, no leaked test details. |
|
| 211 |
+
|
| 212 |
+
### 3.6 Observation schema
|
| 213 |
+
|
| 214 |
+
Observations should be compact and structured.
|
| 215 |
+
|
| 216 |
+
```python
|
| 217 |
+
@dataclass
|
| 218 |
+
class CyberSecurityOWASPObservation(Observation):
|
| 219 |
+
message: str
|
| 220 |
+
visible_policy_summary: str
|
| 221 |
+
route_summary: list[dict]
|
| 222 |
+
last_action_result: dict
|
| 223 |
+
public_test_summary: dict
|
| 224 |
+
patch_summary: dict
|
| 225 |
+
done_reason: str | None = None
|
| 226 |
+
```
|
| 227 |
+
|
| 228 |
+
Do not expose hidden test bodies, hidden expected outputs, or seed-specific solution hints.
|
| 229 |
+
|
| 230 |
+
### 3.7 State schema
|
| 231 |
+
|
| 232 |
+
State should support debugging and training analytics.
|
| 233 |
+
|
| 234 |
+
```python
|
| 235 |
+
@dataclass
|
| 236 |
+
class CyberSecurityOWASPState(State):
|
| 237 |
+
episode_id: str
|
| 238 |
+
scenario_id: str
|
| 239 |
+
split: Literal["train", "validation", "heldout"]
|
| 240 |
+
step_count: int = 0
|
| 241 |
+
max_steps: int = 30
|
| 242 |
+
scenario_family: str = ""
|
| 243 |
+
app_template: str = ""
|
| 244 |
+
files_touched: list[str] = field(default_factory=list)
|
| 245 |
+
public_tests_passed: int = 0
|
| 246 |
+
public_tests_total: int = 0
|
| 247 |
+
hidden_tests_passed: int = 0
|
| 248 |
+
hidden_tests_total: int = 0
|
| 249 |
+
accumulated_reward: float = 0.0
|
| 250 |
+
```
|
| 251 |
+
|
| 252 |
+
## 4. Episode lifecycle
|
| 253 |
+
|
| 254 |
+
```text
|
| 255 |
+
1. reset()
|
| 256 |
+
- sample train/validation scenario seed
|
| 257 |
+
- compile app from policy graph + template + injected bug
|
| 258 |
+
- start local sandbox app and DB fixture
|
| 259 |
+
- return initial observation
|
| 260 |
+
|
| 261 |
+
2. agent loop
|
| 262 |
+
- inspect policy/routes/files
|
| 263 |
+
- send local requests only inside sandbox
|
| 264 |
+
- run public tests
|
| 265 |
+
- apply one or more patches
|
| 266 |
+
- rerun public tests
|
| 267 |
+
|
| 268 |
+
3. submit_fix
|
| 269 |
+
- freeze patch
|
| 270 |
+
- run public tests
|
| 271 |
+
- run hidden authorization invariants
|
| 272 |
+
- run regression tests
|
| 273 |
+
- compute deterministic reward
|
| 274 |
+
- return final observation, reward, done=True
|
| 275 |
+
|
| 276 |
+
4. logging
|
| 277 |
+
- record scenario_id, action trace, patch diff, reward components
|
| 278 |
+
- send metrics to Trackio during training/eval
|
| 279 |
+
```
|
| 280 |
+
|
| 281 |
+
## 5. Reward design
|
| 282 |
+
|
| 283 |
+
The reward should be deterministic, decomposed, and resistant to reward hacking.
|
| 284 |
+
|
| 285 |
+
Recommended reward formula:
|
| 286 |
+
|
| 287 |
+
```text
|
| 288 |
+
R = 0.35 * public_policy_tests
|
| 289 |
+
+ 0.30 * hidden_authz_invariants
|
| 290 |
+
+ 0.15 * regression_preservation
|
| 291 |
+
+ 0.10 * evidence_quality
|
| 292 |
+
+ 0.05 * patch_minimality
|
| 293 |
+
+ 0.05 * efficiency
|
| 294 |
+
- penalties
|
| 295 |
+
```
|
| 296 |
+
|
| 297 |
+
### Reward components
|
| 298 |
+
|
| 299 |
+
| Component | Weight | What it rewards |
|
| 300 |
+
|---|---:|---|
|
| 301 |
+
| Public policy tests | 0.35 | Agent fixes known failing behavior. |
|
| 302 |
+
| Hidden authz invariants | 0.30 | Patch generalizes beyond visible tests. |
|
| 303 |
+
| Regression preservation | 0.15 | Valid user flows and intended public routes still work. |
|
| 304 |
+
| Evidence quality | 0.10 | Agent gathered relevant policy/test/file evidence before patching. |
|
| 305 |
+
| Patch minimality | 0.05 | Small focused patches instead of broad rewrites. |
|
| 306 |
+
| Efficiency | 0.05 | Fewer wasted steps and repeated actions. |
|
| 307 |
+
|
| 308 |
+
### Penalties
|
| 309 |
+
|
| 310 |
+
| Penalty | Trigger |
|
| 311 |
+
|---|---|
|
| 312 |
+
| `-0.25` | Breaks public route intentionally marked public. |
|
| 313 |
+
| `-0.25` | Deletes tests, policy file, or route instead of fixing authorization. |
|
| 314 |
+
| `-0.20` | Hardcodes seed-specific IDs, users, tenants, or hidden assumptions. |
|
| 315 |
+
| `-0.15` | Over-broad denial that blocks legitimate authorized users. |
|
| 316 |
+
| `-0.10` | Patch exceeds file or diff-size budget. |
|
| 317 |
+
| `-1.00` | Attempts external network access, credential extraction, persistence, or unsafe behavior. |
|
| 318 |
+
|
| 319 |
+
The LLM judge, if used at all, should only annotate trace quality for analysis. It must not decide security-critical reward.
|
| 320 |
+
|
| 321 |
+
## 6. Hidden tests and anti-overfitting
|
| 322 |
+
|
| 323 |
+
Hidden tests are necessary because visible tests can be gamed or memorized. They should test policy invariants rather than exact implementation details.
|
| 324 |
+
|
| 325 |
+
Use **4 anti-overfitting layers**:
|
| 326 |
+
|
| 327 |
+
1. **Seed diversity** — route names, user IDs, tenant IDs, object names, and schemas change every episode.
|
| 328 |
+
2. **Template diversity** — same policy bug appears in different frameworks and file layouts.
|
| 329 |
+
3. **Hidden invariant tests** — final reward uses unseen authorization cases.
|
| 330 |
+
4. **Held-out eval split** — at least 20% of scenario families/seeds are never used in training.
|
| 331 |
+
|
| 332 |
+
Recommended split:
|
| 333 |
+
|
| 334 |
+
```text
|
| 335 |
+
Train: 70%
|
| 336 |
+
Validation: 10%
|
| 337 |
+
Held-out: 20%
|
| 338 |
+
```
|
| 339 |
+
|
| 340 |
+
## 7. Evaluation plan
|
| 341 |
+
|
| 342 |
+
Run before/after evaluation on the same held-out suite.
|
| 343 |
+
|
| 344 |
+
### Metrics
|
| 345 |
+
|
| 346 |
+
| Metric | Meaning |
|
| 347 |
+
|---|---|
|
| 348 |
+
| `episode_success_rate` | Public + hidden + regression tests pass. |
|
| 349 |
+
| `hidden_authz_pass_rate` | Security-critical hidden checks pass. |
|
| 350 |
+
| `regression_pass_rate` | Normal valid behavior remains intact. |
|
| 351 |
+
| `oversecure_rate` | Agent blocks intended legitimate/public behavior. |
|
| 352 |
+
| `patch_compile_rate` | Patch applies and app still runs. |
|
| 353 |
+
| `median_steps_to_submit` | Efficiency of the repair workflow. |
|
| 354 |
+
| `median_files_changed` | Patch focus/minimality. |
|
| 355 |
+
| `reward_hacking_rate` | Attempts to delete tests, hardcode fixtures, or bypass eval. |
|
| 356 |
+
|
| 357 |
+
### Eval table template
|
| 358 |
+
|
| 359 |
+
| Model | Split | Success | Hidden authz | Regression | Oversecure | Median steps | Median files changed |
|
| 360 |
+
|---|---|---:|---:|---:|---:|---:|---:|
|
| 361 |
+
| Base model | heldout | TBD | TBD | TBD | TBD | TBD | TBD |
|
| 362 |
+
| RL-trained model | heldout | TBD | TBD | TBD | TBD | TBD | TBD |
|
| 363 |
+
|
| 364 |
+
## 8. Training flow
|
| 365 |
+
|
| 366 |
+
```text
|
| 367 |
+
1. Build CyberSecurity_OWASP OpenEnv server.
|
| 368 |
+
2. Generate 600 MVP scenarios.
|
| 369 |
+
3. Run baseline eval with the base model.
|
| 370 |
+
4. Train with GRPO/TRL or Unsloth using rollout episodes.
|
| 371 |
+
5. Log reward components to Trackio.
|
| 372 |
+
6. Run held-out eval every N training steps.
|
| 373 |
+
7. Inspect failure clusters.
|
| 374 |
+
8. Add scenario mutations only if failures reveal overfitting.
|
| 375 |
+
9. Produce final demo: before/after trace + reward curve + held-out eval table.
|
| 376 |
+
```
|
| 377 |
+
|
| 378 |
+
Recommended initial training setup:
|
| 379 |
+
|
| 380 |
+
```text
|
| 381 |
+
Model: Qwen/Qwen3-1.7B or similar small instruct model
|
| 382 |
+
Algorithm: GRPO via TRL or Unsloth-compatible loop
|
| 383 |
+
Dataset prompt: repeated task instruction with randomized scenario IDs
|
| 384 |
+
Max steps per episode: 30
|
| 385 |
+
Rollouts per prompt: 2-4
|
| 386 |
+
Logging: Trackio
|
| 387 |
+
Primary eval: held-out deterministic test pass rate
|
| 388 |
+
```
|
| 389 |
+
|
| 390 |
+
## 9. Deployment architecture
|
| 391 |
+
|
| 392 |
+
The environment should be runnable in 3 modes:
|
| 393 |
+
|
| 394 |
+
| Mode | Purpose |
|
| 395 |
+
|---|---|
|
| 396 |
+
| Local Uvicorn | Fast engineer iteration. |
|
| 397 |
+
| Docker | Reproducible local training/eval. |
|
| 398 |
+
| Hugging Face Spaces | Public hackathon demo and OpenEnv-compliant hosting. |
|
| 399 |
+
|
| 400 |
+
Expected endpoints:
|
| 401 |
+
|
| 402 |
+
```text
|
| 403 |
+
/ws OpenEnv client session
|
| 404 |
+
/health health check
|
| 405 |
+
/reset debug reset
|
| 406 |
+
/step debug step
|
| 407 |
+
/state debug state
|
| 408 |
+
/docs FastAPI docs
|
| 409 |
+
/web optional web UI
|
| 410 |
+
```
|
| 411 |
+
|
| 412 |
+
## 10. Implementation milestones
|
| 413 |
+
|
| 414 |
+
### Milestone 1 — Skeleton environment
|
| 415 |
+
|
| 416 |
+
- `models.py`
|
| 417 |
+
- `client.py`
|
| 418 |
+
- `server/environment.py`
|
| 419 |
+
- `server/app.py`
|
| 420 |
+
- `server/Dockerfile`
|
| 421 |
+
- `openenv.yaml`
|
| 422 |
+
- health check
|
| 423 |
+
- one hand-written scenario
|
| 424 |
+
|
| 425 |
+
### Milestone 2 — Scenario compiler
|
| 426 |
+
|
| 427 |
+
- policy graph format
|
| 428 |
+
- app template renderer
|
| 429 |
+
- bug injector
|
| 430 |
+
- DB fixture generator
|
| 431 |
+
- public and hidden test generator
|
| 432 |
+
|
| 433 |
+
### Milestone 3 — Reward engine
|
| 434 |
+
|
| 435 |
+
- public test score
|
| 436 |
+
- hidden invariant score
|
| 437 |
+
- regression score
|
| 438 |
+
- patch minimality score
|
| 439 |
+
- safety/reward-hacking penalties
|
| 440 |
+
- reward component logging
|
| 441 |
+
|
| 442 |
+
### Milestone 4 — Training script
|
| 443 |
+
|
| 444 |
+
- rollout loop
|
| 445 |
+
- GRPO/TRL or Unsloth training script
|
| 446 |
+
- Trackio logging
|
| 447 |
+
- checkpoint save/push
|
| 448 |
+
- baseline and post-training eval
|
| 449 |
+
|
| 450 |
+
### Milestone 5 — Hackathon demo
|
| 451 |
+
|
| 452 |
+
- HF Spaces deployment
|
| 453 |
+
- mini-blog
|
| 454 |
+
- 2-minute video
|
| 455 |
+
- before/after traces
|
| 456 |
+
- reward curve
|
| 457 |
+
- held-out eval table
|
| 458 |
+
|
| 459 |
+
## 11. Engineering notes
|
| 460 |
+
|
| 461 |
+
- Keep scenario apps small: ideally 5-15 files each.
|
| 462 |
+
- Prefer deterministic tests over LLM judging.
|
| 463 |
+
- Hide final hidden test details from observations.
|
| 464 |
+
- Log enough trace data to debug failures but never leak hidden tests to the agent.
|
| 465 |
+
- Include intentionally public routes and allowed cross-role cases so the model does not learn “add auth everywhere.”
|
| 466 |
+
- The best demo is not just “agent finds bug,” but “agent learns not to break valid business behavior.”
|
| 467 |
+
|
| 468 |
+
## 12. Source notes and credibility
|
| 469 |
+
|
| 470 |
+
| Source | How it informs this architecture | Credibility |
|
| 471 |
+
|---|---|---:|
|
| 472 |
+
| OWASP Top 10 2025 / A01 Broken Access Control | Confirms why access control is the right security focus. | 10/10 |
|
| 473 |
+
| OWASP ASVS access-control guidance | Informs policy invariants and server-side authorization checks. | 9.5/10 |
|
| 474 |
+
| OpenEnv environment-building docs | Defines required models, reset/step/state, FastAPI server, Docker, and client. | 8.5/10 |
|
| 475 |
+
| OpenEnv quickstart/architecture docs | Informs WebSocket client/server design, typed EnvClient, and container isolation. | 8.5/10 |
|
| 476 |
+
| OpenEnv deployment docs | Informs HF Spaces deployment, endpoints, Docker workflow, and installable client package. | 8.5/10 |
|
| 477 |
+
| Hackathon judging criteria | Informs demo priorities: innovation, storytelling, reward improvement, and training pipeline. | 9/10 |
|
| 478 |
+
| TRL/OpenEnv training example | Informs rollout function, decomposed reward functions, and Trackio logging pattern. | 8/10 |
|
| 479 |
+
|
AGENTS.md
ADDED
|
@@ -0,0 +1,1197 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# AGENTS.md — CyberSecurity_OWASP Builder Instructions
|
| 2 |
+
|
| 3 |
+
## Purpose
|
| 4 |
+
|
| 5 |
+
This repository implements **CyberSecurity_OWASP**, an OpenEnv-compliant RL environment for training a **single LLM agent** to perform a defensive application-security workflow:
|
| 6 |
+
|
| 7 |
+
```text
|
| 8 |
+
inspect generated app + policy -> discover authorization bug -> submit safe finding -> patch code -> preserve intended behavior
|
| 9 |
+
```
|
| 10 |
+
|
| 11 |
+
The environment must train the model to do real interactive work, not answer static security questions. The model must act step by step through typed OpenEnv actions, observe consequences, receive deterministic reward, and improve through RL.
|
| 12 |
+
|
| 13 |
+
The canonical repository and OpenEnv environment name is **`CyberSecurity_OWASP`**. Use this exact name in `openenv.yaml`, `pyproject.toml`, HF Spaces repo naming, Docker image tags, Trackio run names, command examples, and documentation.
|
| 14 |
+
|
| 15 |
+
The target stack is:
|
| 16 |
+
|
| 17 |
+
```text
|
| 18 |
+
CyberSecurity_OWASP OpenEnv environment
|
| 19 |
+
-> deterministic verifier + hidden tests
|
| 20 |
+
-> rollout loop
|
| 21 |
+
-> HF TRL / Unsloth GRPO
|
| 22 |
+
-> Trackio logging
|
| 23 |
+
-> held-out evaluation
|
| 24 |
+
-> HF Spaces deployment
|
| 25 |
+
```
|
| 26 |
+
|
| 27 |
+
The final project must show measurable improvement in reward, exploit-block rate, regression-preservation rate, and held-out generalization after training.
|
| 28 |
+
|
| 29 |
+
---
|
| 30 |
+
|
| 31 |
+
## Product definition
|
| 32 |
+
|
| 33 |
+
CyberSecurity_OWASP generates a new local application scenario every `reset(seed)`. Each episode contains:
|
| 34 |
+
|
| 35 |
+
- a policy graph describing users, roles, tenants, resources, ownership, permissions, and public routes;
|
| 36 |
+
- a generated FastAPI-style application workspace;
|
| 37 |
+
- exactly one injected OWASP A01-style authorization defect;
|
| 38 |
+
- visible tests for normal behavior;
|
| 39 |
+
- hidden invariant tests for authorization correctness, regression protection, public-route preservation, and anti-cheat checks.
|
| 40 |
+
|
| 41 |
+
The environment has **one LLM agent**, not separate red-team and blue-team LLMs. The environment itself acts as the scenario generator, tool server, verifier, and judge.
|
| 42 |
+
|
| 43 |
+
---
|
| 44 |
+
|
| 45 |
+
## Highest-priority objectives
|
| 46 |
+
|
| 47 |
+
When making implementation decisions, optimize in this order:
|
| 48 |
+
|
| 49 |
+
1. **Verifier correctness**: deterministic tests must decide whether the patch actually fixes the authorization defect.
|
| 50 |
+
2. **Reward integrity**: reward must be hard to hack and must punish insecure or regressive patches.
|
| 51 |
+
3. **Anti-overfitting**: the model must generalize across apps, layouts, policies, domains, names, and bug families.
|
| 52 |
+
4. **OpenEnv compliance**: expose typed `Action`, `Observation`, and `State`; implement `reset()`, `step(action)`, and `state` correctly.
|
| 53 |
+
5. **Trainability**: baseline model should sometimes get partial reward; curriculum should make early learning possible.
|
| 54 |
+
6. **Real-world usefulness**: the workflow should resemble secure code review / AppSec authorization repair.
|
| 55 |
+
7. **Demo clarity**: show before/after rollouts, reward curves, and why the trained model improved.
|
| 56 |
+
8. **Hackathon competitiveness**: prioritize a novel, interactive, professionally useful environment with a coherent training pipeline.
|
| 57 |
+
|
| 58 |
+
Do not train before the environment, verifier, anti-cheat tests, and before/after evaluation are stable.
|
| 59 |
+
|
| 60 |
+
---
|
| 61 |
+
|
| 62 |
+
## Hackathon alignment requirements
|
| 63 |
+
|
| 64 |
+
The implementation must satisfy these minimum requirements:
|
| 65 |
+
|
| 66 |
+
- use the latest OpenEnv release;
|
| 67 |
+
- include a minimal HF TRL or Unsloth training script;
|
| 68 |
+
- use Trackio as the default tracker for training and evaluation;
|
| 69 |
+
- be deployable as an OpenEnv-compliant Hugging Face Space;
|
| 70 |
+
- include a README / mini-blog style explanation;
|
| 71 |
+
- show baseline-vs-trained improvement.
|
| 72 |
+
|
| 73 |
+
Optimize for judging:
|
| 74 |
+
|
| 75 |
+
| Criterion | Weight | CyberSecurity_OWASP evidence |
|
| 76 |
+
|---|---:|---|
|
| 77 |
+
| Environment innovation | 40% | procedural OWASP authorization-repair environment with generated code, policy, and hidden verifier |
|
| 78 |
+
| Storytelling | 30% | single LLM learns discover + patch, before/after security behavior |
|
| 79 |
+
| Showing improvement in rewards | 20% | reward curves, exploit-block pass rate, regression-preservation rate |
|
| 80 |
+
| Reward/training pipeline | 10% | deterministic reward, GRPO/PPO rollout loop, Trackio metrics |
|
| 81 |
+
|
| 82 |
+
---
|
| 83 |
+
|
| 84 |
+
## Non-negotiable environment design
|
| 85 |
+
|
| 86 |
+
CyberSecurity_OWASP must be a **single-agent** environment:
|
| 87 |
+
|
| 88 |
+
```text
|
| 89 |
+
phase = discover -> patch -> done
|
| 90 |
+
```
|
| 91 |
+
|
| 92 |
+
Do not implement a two-LLM red-team/blue-team setup. The single model must learn both discovery and repair.
|
| 93 |
+
|
| 94 |
+
The environment must be defensive and local only. It must never target real systems or teach unauthorized exploitation. All probing must be limited to the generated local workspace controlled by the environment.
|
| 95 |
+
|
| 96 |
+
---
|
| 97 |
+
|
| 98 |
+
## Required repository structure
|
| 99 |
+
|
| 100 |
+
Prefer this structure:
|
| 101 |
+
|
| 102 |
+
```text
|
| 103 |
+
.
|
| 104 |
+
├── AGENTS.md
|
| 105 |
+
├── README.md
|
| 106 |
+
├── 00_PROJECT_BRIEF.md
|
| 107 |
+
├── 01_ARCHITECTURE.md
|
| 108 |
+
├── pyproject.toml
|
| 109 |
+
├── openenv.yaml
|
| 110 |
+
├── envs/
|
| 111 |
+
│ └── CyberSecurity_OWASP/
|
| 112 |
+
│ ├── __init__.py
|
| 113 |
+
│ ├── models.py
|
| 114 |
+
│ ├── client.py
|
| 115 |
+
│ ├── README.md
|
| 116 |
+
│ ├── rewards.py
|
| 117 |
+
│ ├── validators.py
|
| 118 |
+
│ ├── safety.py
|
| 119 |
+
│ ├── evals.py
|
| 120 |
+
│ ├── server/
|
| 121 |
+
│ │ ├── __init__.py
|
| 122 |
+
│ │ ├── app.py
|
| 123 |
+
│ │ ├── environment.py
|
| 124 |
+
│ │ ├── scenario_compiler.py
|
| 125 |
+
│ │ ├── policy_graph.py
|
| 126 |
+
│ │ ├── template_renderer.py
|
| 127 |
+
│ │ ├── bug_mutator.py
|
| 128 |
+
│ │ ├── fixture_generator.py
|
| 129 |
+
│ │ ├── reward_engine.py
|
| 130 |
+
│ │ ├── requirements.txt
|
| 131 |
+
│ │ └── Dockerfile
|
| 132 |
+
│ ├── templates/
|
| 133 |
+
│ │ └── fastapi_basic/
|
| 134 |
+
│ ├── scenario_cache/
|
| 135 |
+
│ │ ├── train/
|
| 136 |
+
│ │ ├── validation/
|
| 137 |
+
│ │ └── hidden_eval/
|
| 138 |
+
│ └── tests/
|
| 139 |
+
│ ├── test_models.py
|
| 140 |
+
│ ├── test_reset_step_state.py
|
| 141 |
+
│ ├── test_rewards.py
|
| 142 |
+
│ ├── test_anti_cheat.py
|
| 143 |
+
│ ├── test_seed_reproducibility.py
|
| 144 |
+
│ ├── test_invalid_actions.py
|
| 145 |
+
│ └── test_rollouts.py
|
| 146 |
+
├── training/
|
| 147 |
+
│ ├── train_grpo.py
|
| 148 |
+
│ ├── rollout.py
|
| 149 |
+
│ ├── reward_funcs.py
|
| 150 |
+
│ ├── eval_before_after.py
|
| 151 |
+
│ ├── trackio_utils.py
|
| 152 |
+
│ └── configs/
|
| 153 |
+
│ └── grpo_small.yaml
|
| 154 |
+
├── scripts/
|
| 155 |
+
│ ├── run_local.sh
|
| 156 |
+
│ ├── docker_build.sh
|
| 157 |
+
│ ├── docker_run.sh
|
| 158 |
+
│ ├── smoke_test.sh
|
| 159 |
+
│ ├── generate_scenarios.sh
|
| 160 |
+
│ └── push_space.sh
|
| 161 |
+
├── assets/
|
| 162 |
+
│ └── anti_overfitting_training_flow_diagram.png
|
| 163 |
+
└── outputs/
|
| 164 |
+
├── logs/
|
| 165 |
+
├── evals/
|
| 166 |
+
└── rollouts/
|
| 167 |
+
```
|
| 168 |
+
|
| 169 |
+
If `openenv init CyberSecurity_OWASP` creates a different structure, preserve the generated structure and add the missing files around it.
|
| 170 |
+
|
| 171 |
+
---
|
| 172 |
+
|
| 173 |
+
## Architecture overview
|
| 174 |
+
|
| 175 |
+
CyberSecurity_OWASP has 7 main components:
|
| 176 |
+
|
| 177 |
+
1. **Policy Graph + Domain Sampler** — samples users, roles, tenants, ownership, public routes, and business exceptions.
|
| 178 |
+
2. **Template / Framework Randomizer** — renders FastAPI-style apps with randomized layouts and naming.
|
| 179 |
+
3. **A01 Bug Mutator** — injects one authorization defect per scenario.
|
| 180 |
+
4. **Fixture + Hidden Test Generator** — creates users, resources, visible tests, and hidden invariant tests.
|
| 181 |
+
5. **OpenEnv Server** — exposes typed `Action`, `Observation`, and `State` through `reset`, `step`, and `state`.
|
| 182 |
+
6. **LLM Agent + LoRA** — one model performs discover + patch.
|
| 183 |
+
7. **Deterministic Reward Engine** — hidden tests score exploit blocking, normal-flow preservation, patch quality, and anti-cheat.
|
| 184 |
+
|
| 185 |
+
An optional LLM reviewer may score rationale quality and ASVS/OWASP mapping only. It must not provide the primary reward.
|
| 186 |
+
|
| 187 |
+
---
|
| 188 |
+
|
| 189 |
+
## Scenario compiler requirements
|
| 190 |
+
|
| 191 |
+
### Policy Graph + Domain Sampler
|
| 192 |
+
|
| 193 |
+
The policy graph is the source of truth. It must define:
|
| 194 |
+
|
| 195 |
+
- users;
|
| 196 |
+
- tenants;
|
| 197 |
+
- roles;
|
| 198 |
+
- resources;
|
| 199 |
+
- ownership relationships;
|
| 200 |
+
- role permissions;
|
| 201 |
+
- public routes;
|
| 202 |
+
- business exceptions.
|
| 203 |
+
|
| 204 |
+
Initial domains:
|
| 205 |
+
|
| 206 |
+
| Domain | Example resources | Example policy rule |
|
| 207 |
+
|---|---|---|
|
| 208 |
+
| invoices | invoices, payments, accounts | owner or billing admin can read invoice |
|
| 209 |
+
| support | tickets, comments, customer records | assigned agent can update ticket |
|
| 210 |
+
| projects | projects, documents, milestones | project member can read project docs |
|
| 211 |
+
| marketplace | orders, returns, seller records | buyer owns own orders; seller owns own listings |
|
| 212 |
+
| HR | employee profiles, reviews, payroll records | HR admin can read employee records |
|
| 213 |
+
|
| 214 |
+
### Template / Framework Randomizer
|
| 215 |
+
|
| 216 |
+
First version: FastAPI only. Still randomize structure so the model cannot memorize one app.
|
| 217 |
+
|
| 218 |
+
Randomize:
|
| 219 |
+
|
| 220 |
+
- path naming;
|
| 221 |
+
- parameter names;
|
| 222 |
+
- helper names;
|
| 223 |
+
- folder layout;
|
| 224 |
+
- route/service/auth split;
|
| 225 |
+
- fixture names;
|
| 226 |
+
- error messages within valid policy bounds.
|
| 227 |
+
|
| 228 |
+
Examples:
|
| 229 |
+
|
| 230 |
+
```text
|
| 231 |
+
/routes/invoices.py
|
| 232 |
+
/api/billing.py
|
| 233 |
+
/controllers/accounts.py
|
| 234 |
+
/services/access.py
|
| 235 |
+
/authz/guards.py
|
| 236 |
+
```
|
| 237 |
+
|
| 238 |
+
### A01 Bug Mutator
|
| 239 |
+
|
| 240 |
+
Inject exactly one primary bug per scenario.
|
| 241 |
+
|
| 242 |
+
Initial bug families:
|
| 243 |
+
|
| 244 |
+
| Bug family | Defect | Desired repair pattern |
|
| 245 |
+
|---|---|---|
|
| 246 |
+
| BOLA/IDOR | resource ID lookup lacks owner/tenant check | check server-side owner/tenant relation |
|
| 247 |
+
| BFLA | privileged route lacks role/function check | add reusable role or permission guard |
|
| 248 |
+
| tenant leak | request/header tenant ID is trusted | derive tenant from authenticated principal or server-side mapping |
|
| 249 |
+
| JWT claim trust | mutable claim is treated as authoritative | verify against server-side user/role record |
|
| 250 |
+
| public-route trap | route is intentionally public | do not over-secure public allowlisted route |
|
| 251 |
+
|
| 252 |
+
### Fixture + Hidden Test Generator
|
| 253 |
+
|
| 254 |
+
Visible tests should check that the app boots and normal happy paths work.
|
| 255 |
+
|
| 256 |
+
Hidden tests must check:
|
| 257 |
+
|
| 258 |
+
- exploit request is blocked;
|
| 259 |
+
- legitimate owner flow still works;
|
| 260 |
+
- legitimate admin/support flow still works;
|
| 261 |
+
- public routes remain public;
|
| 262 |
+
- cross-tenant access is denied;
|
| 263 |
+
- randomized IDs/names defeat hardcoded patches;
|
| 264 |
+
- hidden tests, fixtures, oracle, and reward files are not modified.
|
| 265 |
+
|
| 266 |
+
### Scenario Cache + Seeded Reset
|
| 267 |
+
|
| 268 |
+
Training should use cached scenarios for speed.
|
| 269 |
+
|
| 270 |
+
Recommended first cache:
|
| 271 |
+
|
| 272 |
+
| Split | Seeds | Purpose |
|
| 273 |
+
|---|---:|---|
|
| 274 |
+
| train | 500–1,000 | RL rollouts |
|
| 275 |
+
| validation | 100–200 | checkpoint selection and curriculum signal |
|
| 276 |
+
| hidden_eval | 100–200 | final generalization proof |
|
| 277 |
+
|
| 278 |
+
### Hold-Out Generalization Splitter
|
| 279 |
+
|
| 280 |
+
Hold out at least 4 dimensions:
|
| 281 |
+
|
| 282 |
+
1. domains;
|
| 283 |
+
2. policy graph shapes;
|
| 284 |
+
3. code layouts;
|
| 285 |
+
4. bug-family/domain combinations.
|
| 286 |
+
|
| 287 |
+
Example: train on invoices/support/projects, evaluate on marketplace/HR.
|
| 288 |
+
|
| 289 |
+
---
|
| 290 |
+
|
| 291 |
+
## OpenEnv model definitions
|
| 292 |
+
|
| 293 |
+
Implement these in `envs/CyberSecurity_OWASP/models.py`.
|
| 294 |
+
|
| 295 |
+
```python
|
| 296 |
+
from dataclasses import dataclass, field
|
| 297 |
+
from typing import Any, Literal
|
| 298 |
+
from openenv.core.env_server import Action, Observation, State
|
| 299 |
+
|
| 300 |
+
CyberSecurityOWASPPhase = Literal["discover", "patch", "done"]
|
| 301 |
+
CyberSecurityOWASPSplit = Literal["train", "validation", "hidden_eval"]
|
| 302 |
+
|
| 303 |
+
@dataclass
|
| 304 |
+
class CyberSecurityOWASPAction(Action):
|
| 305 |
+
tool_name: Literal[
|
| 306 |
+
"inspect_policy_graph",
|
| 307 |
+
"list_routes",
|
| 308 |
+
"read_openapi",
|
| 309 |
+
"read_file",
|
| 310 |
+
"search_code",
|
| 311 |
+
"send_local_request",
|
| 312 |
+
"compare_identities",
|
| 313 |
+
"submit_finding",
|
| 314 |
+
"patch_file",
|
| 315 |
+
"run_visible_tests",
|
| 316 |
+
"submit_fix",
|
| 317 |
+
"noop",
|
| 318 |
+
]
|
| 319 |
+
arguments: dict[str, Any] = field(default_factory=dict)
|
| 320 |
+
|
| 321 |
+
@dataclass
|
| 322 |
+
class CyberSecurityOWASPObservation(Observation):
|
| 323 |
+
phase: CyberSecurityOWASPPhase
|
| 324 |
+
message: str
|
| 325 |
+
task_brief: str
|
| 326 |
+
visible_policy_hint: dict[str, Any] = field(default_factory=dict)
|
| 327 |
+
workspace_summary: dict[str, Any] = field(default_factory=dict)
|
| 328 |
+
available_actions: list[str] = field(default_factory=list)
|
| 329 |
+
last_tool_result: str = ""
|
| 330 |
+
last_action_valid: bool = True
|
| 331 |
+
last_action_error: str | None = None
|
| 332 |
+
visible_test_result: str | None = None
|
| 333 |
+
reward_breakdown: dict[str, float] = field(default_factory=dict)
|
| 334 |
+
done_reason: str | None = None
|
| 335 |
+
|
| 336 |
+
@dataclass
|
| 337 |
+
class CyberSecurityOWASPState(State):
|
| 338 |
+
episode_id: str = ""
|
| 339 |
+
task_id: str = ""
|
| 340 |
+
seed: int = 0
|
| 341 |
+
split: CyberSecurityOWASPSplit = "train"
|
| 342 |
+
difficulty: int = 0
|
| 343 |
+
domain: str = ""
|
| 344 |
+
bug_family: str = ""
|
| 345 |
+
phase: CyberSecurityOWASPPhase = "discover"
|
| 346 |
+
step_count: int = 0
|
| 347 |
+
max_steps: int = 40
|
| 348 |
+
done: bool = False
|
| 349 |
+
success: bool = False
|
| 350 |
+
failure_reason: str | None = None
|
| 351 |
+
finding_submitted: bool = False
|
| 352 |
+
patch_submitted: bool = False
|
| 353 |
+
accumulated_reward: float = 0.0
|
| 354 |
+
last_reward: float = 0.0
|
| 355 |
+
action_history: list[dict[str, Any]] = field(default_factory=list)
|
| 356 |
+
reward_history: list[dict[str, float]] = field(default_factory=list)
|
| 357 |
+
visible_facts: dict[str, Any] = field(default_factory=dict)
|
| 358 |
+
hidden_facts: dict[str, Any] = field(default_factory=dict)
|
| 359 |
+
metrics: dict[str, Any] = field(default_factory=dict)
|
| 360 |
+
anti_cheat_flags: list[str] = field(default_factory=list)
|
| 361 |
+
```
|
| 362 |
+
|
| 363 |
+
---
|
| 364 |
+
|
| 365 |
+
## Action design and phase gating
|
| 366 |
+
|
| 367 |
+
Actions must be explicit, typed, serializable, and constrained. Invalid actions must not crash the server.
|
| 368 |
+
|
| 369 |
+
### Phase-gated tools
|
| 370 |
+
|
| 371 |
+
| Phase | Allowed tools |
|
| 372 |
+
|---|---|
|
| 373 |
+
| discover | `inspect_policy_graph`, `list_routes`, `read_openapi`, `read_file`, `search_code`, `send_local_request`, `compare_identities`, `submit_finding`, `noop` |
|
| 374 |
+
| patch | `read_file`, `search_code`, `patch_file`, `run_visible_tests`, `send_local_request`, `submit_fix`, `noop` |
|
| 375 |
+
| done | no state-changing tools; return stable done observation |
|
| 376 |
+
|
| 377 |
+
### Tool contracts
|
| 378 |
+
|
| 379 |
+
`inspect_policy_graph`
|
| 380 |
+
: Returns public policy hints. Must not reveal hidden bug labels or hidden tests.
|
| 381 |
+
|
| 382 |
+
`list_routes`
|
| 383 |
+
: Returns route method/path summaries from the generated app.
|
| 384 |
+
|
| 385 |
+
`read_openapi`
|
| 386 |
+
: Returns generated OpenAPI metadata.
|
| 387 |
+
|
| 388 |
+
`read_file`
|
| 389 |
+
: Reads editable workspace files only. Must block hidden tests, reward files, oracle files, and host files.
|
| 390 |
+
|
| 391 |
+
`search_code`
|
| 392 |
+
: Searches editable workspace files only.
|
| 393 |
+
|
| 394 |
+
`send_local_request`
|
| 395 |
+
: Sends a request to the local generated app only. Must block external URLs and host network access.
|
| 396 |
+
|
| 397 |
+
`compare_identities`
|
| 398 |
+
: Runs the same local request as two generated users and summarizes behavioral differences.
|
| 399 |
+
|
| 400 |
+
`submit_finding`
|
| 401 |
+
: Accepts structured evidence of the suspected authorization bug. Required before patch phase unless curriculum level explicitly allows blind patching.
|
| 402 |
+
|
| 403 |
+
`patch_file`
|
| 404 |
+
: Applies a bounded unified diff to editable app files only.
|
| 405 |
+
|
| 406 |
+
`run_visible_tests`
|
| 407 |
+
: Runs visible tests only. Must not run or reveal hidden tests.
|
| 408 |
+
|
| 409 |
+
`submit_fix`
|
| 410 |
+
: Triggers hidden evaluation.
|
| 411 |
+
|
| 412 |
+
---
|
| 413 |
+
|
| 414 |
+
## Observation rules
|
| 415 |
+
|
| 416 |
+
Observations should provide enough information to act but must not leak the answer.
|
| 417 |
+
|
| 418 |
+
Include:
|
| 419 |
+
|
| 420 |
+
- current phase;
|
| 421 |
+
- task brief;
|
| 422 |
+
- visible policy hints;
|
| 423 |
+
- workspace summary;
|
| 424 |
+
- available tools;
|
| 425 |
+
- previous tool output;
|
| 426 |
+
- visible test output;
|
| 427 |
+
- public reward breakdown after terminal evaluation.
|
| 428 |
+
|
| 429 |
+
Do not include:
|
| 430 |
+
|
| 431 |
+
- hidden bug family if not meant to be visible;
|
| 432 |
+
- hidden test contents;
|
| 433 |
+
- hidden oracle;
|
| 434 |
+
- exact exploit path labels;
|
| 435 |
+
- hidden seed split labels that allow memorization;
|
| 436 |
+
- reward implementation details that allow proxy hacking.
|
| 437 |
+
|
| 438 |
+
---
|
| 439 |
+
|
| 440 |
+
## State rules
|
| 441 |
+
|
| 442 |
+
State is the source of truth for deterministic replay and debugging.
|
| 443 |
+
|
| 444 |
+
Required state properties:
|
| 445 |
+
|
| 446 |
+
- `reset(seed)` must create a fresh independent state;
|
| 447 |
+
- same seed + same action sequence should produce same result;
|
| 448 |
+
- each WebSocket session must be isolated;
|
| 449 |
+
- `step_count` increments once per processed action;
|
| 450 |
+
- terminal states return stable done observations;
|
| 451 |
+
- hidden facts never appear in observations;
|
| 452 |
+
- all actions and reward breakdowns are stored for debugging.
|
| 453 |
+
|
| 454 |
+
---
|
| 455 |
+
|
| 456 |
+
## Environment API contract
|
| 457 |
+
|
| 458 |
+
Implement in `envs/CyberSecurity_OWASP/server/environment.py`.
|
| 459 |
+
|
| 460 |
+
```python
|
| 461 |
+
from openenv.core.env_server import Environment
|
| 462 |
+
from ..models import CyberSecurityOWASPAction, CyberSecurityOWASPObservation, CyberSecurityOWASPState
|
| 463 |
+
|
| 464 |
+
class CyberSecurityOWASPEnvironment(Environment):
|
| 465 |
+
def __init__(self):
|
| 466 |
+
super().__init__()
|
| 467 |
+
self._state = CyberSecurityOWASPState()
|
| 468 |
+
|
| 469 |
+
def reset(self) -> CyberSecurityOWASPObservation:
|
| 470 |
+
...
|
| 471 |
+
|
| 472 |
+
def step(self, action: CyberSecurityOWASPAction) -> CyberSecurityOWASPObservation:
|
| 473 |
+
...
|
| 474 |
+
|
| 475 |
+
@property
|
| 476 |
+
def state(self) -> CyberSecurityOWASPState:
|
| 477 |
+
return self._state
|
| 478 |
+
```
|
| 479 |
+
|
| 480 |
+
`step(action)` must follow this order:
|
| 481 |
+
|
| 482 |
+
1. If done, return stable done observation.
|
| 483 |
+
2. Validate action and phase permissions.
|
| 484 |
+
3. Increment step count.
|
| 485 |
+
4. Execute the tool.
|
| 486 |
+
5. Update state/history.
|
| 487 |
+
6. Run verifier if `submit_finding`, `run_visible_tests`, or `submit_fix`.
|
| 488 |
+
7. Compute reward components.
|
| 489 |
+
8. Check terminal conditions.
|
| 490 |
+
9. Return observation, reward, and done through OpenEnv step result handling.
|
| 491 |
+
|
| 492 |
+
---
|
| 493 |
+
|
| 494 |
+
## FastAPI server and client
|
| 495 |
+
|
| 496 |
+
`envs/CyberSecurity_OWASP/server/app.py` must use the OpenEnv FastAPI helper.
|
| 497 |
+
|
| 498 |
+
```python
|
| 499 |
+
from openenv.core.env_server import create_fastapi_app
|
| 500 |
+
from ..models import CyberSecurityOWASPAction, CyberSecurityOWASPObservation
|
| 501 |
+
from .environment import CyberSecurityOWASPEnvironment
|
| 502 |
+
|
| 503 |
+
env = CyberSecurityOWASPEnvironment()
|
| 504 |
+
app = create_fastapi_app(env, CyberSecurityOWASPAction, CyberSecurityOWASPObservation)
|
| 505 |
+
```
|
| 506 |
+
|
| 507 |
+
`envs/CyberSecurity_OWASP/client.py` must parse `StepResult`, observations, rewards, done flags, and state into typed classes.
|
| 508 |
+
|
| 509 |
+
---
|
| 510 |
+
|
| 511 |
+
## Deterministic reward engine
|
| 512 |
+
|
| 513 |
+
The reward engine is the task specification. Build it before training.
|
| 514 |
+
|
| 515 |
+
Maximum reward: **15.0**.
|
| 516 |
+
|
| 517 |
+
| Component | Reward |
|
| 518 |
+
|---|---:|
|
| 519 |
+
| valid finding with reproducible local evidence | +2.0 |
|
| 520 |
+
| finding tied to violated policy rule | +1.0 |
|
| 521 |
+
| hidden exploit blocked | +5.0 |
|
| 522 |
+
| legitimate owner/admin/support flows preserved | +3.0 |
|
| 523 |
+
| public routes preserved | +1.0 |
|
| 524 |
+
| patch is localized, reusable, and policy-aligned | +2.0 |
|
| 525 |
+
| visible tests pass and app boots | +1.0 |
|
| 526 |
+
| exploit still works | -6.0 |
|
| 527 |
+
| legitimate flow regression | -5.0 |
|
| 528 |
+
| public route incorrectly locked | -3.0 |
|
| 529 |
+
| hardcoded user/tenant/resource fix | -4.0 |
|
| 530 |
+
| modified tests, fixtures, oracle, or hidden files | -8.0 |
|
| 531 |
+
| app does not parse or boot | -6.0 |
|
| 532 |
+
|
| 533 |
+
The reward breakdown must use stable keys:
|
| 534 |
+
|
| 535 |
+
```python
|
| 536 |
+
reward_breakdown = {
|
| 537 |
+
"discovery": discovery_reward,
|
| 538 |
+
"security": security_reward,
|
| 539 |
+
"regression": regression_reward,
|
| 540 |
+
"public_routes": public_route_reward,
|
| 541 |
+
"patch_quality": patch_quality_reward,
|
| 542 |
+
"visible_tests": visible_tests_reward,
|
| 543 |
+
"safety": safety_reward,
|
| 544 |
+
"anti_cheat": anti_cheat_reward,
|
| 545 |
+
"total": total_reward,
|
| 546 |
+
}
|
| 547 |
+
```
|
| 548 |
+
|
| 549 |
+
Do not let explanation quality dominate the reward. If using an LLM reviewer, keep it separate from `total` or cap it at a small non-primary auxiliary signal.
|
| 550 |
+
|
| 551 |
+
---
|
| 552 |
+
|
| 553 |
+
## Verifier requirements
|
| 554 |
+
|
| 555 |
+
Create `envs/CyberSecurity_OWASP/validators.py` and `envs/CyberSecurity_OWASP/server/reward_engine.py`.
|
| 556 |
+
|
| 557 |
+
Required verifier functions:
|
| 558 |
+
|
| 559 |
+
```python
|
| 560 |
+
def verify_finding(state: CyberSecurityOWASPState, finding: dict) -> dict:
|
| 561 |
+
...
|
| 562 |
+
|
| 563 |
+
|
| 564 |
+
def run_hidden_security_tests(state: CyberSecurityOWASPState) -> dict:
|
| 565 |
+
...
|
| 566 |
+
|
| 567 |
+
|
| 568 |
+
def run_hidden_regression_tests(state: CyberSecurityOWASPState) -> dict:
|
| 569 |
+
...
|
| 570 |
+
|
| 571 |
+
|
| 572 |
+
def detect_cheating(state: CyberSecurityOWASPState, action: CyberSecurityOWASPAction) -> list[str]:
|
| 573 |
+
...
|
| 574 |
+
|
| 575 |
+
|
| 576 |
+
def compute_reward(state: CyberSecurityOWASPState, action: CyberSecurityOWASPAction, verifier_result: dict) -> dict[str, float]:
|
| 577 |
+
...
|
| 578 |
+
```
|
| 579 |
+
|
| 580 |
+
Verifier tests must prove:
|
| 581 |
+
|
| 582 |
+
- correct patch receives high reward;
|
| 583 |
+
- exploit-only finding without patch does not complete the episode;
|
| 584 |
+
- deny-all patch fails regression tests;
|
| 585 |
+
- hardcoded patch fails randomized hidden tests;
|
| 586 |
+
- modified hidden files produce anti-cheat penalty;
|
| 587 |
+
- visible-test-only patch does not guarantee high reward;
|
| 588 |
+
- repeated intermediate actions cannot inflate reward indefinitely.
|
| 589 |
+
|
| 590 |
+
---
|
| 591 |
+
|
| 592 |
+
## Anti-overfitting requirements
|
| 593 |
+
|
| 594 |
+
CyberSecurity_OWASP must prevent overfitting to one app or scenario.
|
| 595 |
+
|
| 596 |
+
Use all of these defenses:
|
| 597 |
+
|
| 598 |
+
| Risk | Required defense |
|
| 599 |
+
|---|---|
|
| 600 |
+
| memorizes one app | many domains and templates |
|
| 601 |
+
| memorizes route names | randomized path, resource, parameter, helper names |
|
| 602 |
+
| memorizes bug location | vary route/service/auth layer placement |
|
| 603 |
+
| learns deny-all patch | hidden positive-flow and public-route tests |
|
| 604 |
+
| learns hardcoded patch | randomized users, tenants, resource IDs, role names |
|
| 605 |
+
| overfits visible tests | hidden invariant tests and held-out eval |
|
| 606 |
+
| overfits one bug family | curriculum-sampled bug mix |
|
| 607 |
+
| overfits one code layout | hold out entire layouts and domains |
|
| 608 |
+
| optimizes explanation only | deterministic reward is primary |
|
| 609 |
+
|
| 610 |
+
Acceptance target: at least **20%** of domain/layout/bug combinations must be held out from training.
|
| 611 |
+
|
| 612 |
+
---
|
| 613 |
+
|
| 614 |
+
## Safety and cybersecurity boundaries
|
| 615 |
+
|
| 616 |
+
This is a defensive AppSec training environment.
|
| 617 |
+
|
| 618 |
+
Allowed:
|
| 619 |
+
|
| 620 |
+
- local generated app probing;
|
| 621 |
+
- authorization reasoning;
|
| 622 |
+
- secure patching;
|
| 623 |
+
- visible and hidden test execution;
|
| 624 |
+
- policy-to-code mapping;
|
| 625 |
+
- defensive vulnerability validation in sandbox.
|
| 626 |
+
|
| 627 |
+
Forbidden:
|
| 628 |
+
|
| 629 |
+
- real-world exploitation;
|
| 630 |
+
- credential theft;
|
| 631 |
+
- persistence/evasion/malware behavior;
|
| 632 |
+
- scanning external targets;
|
| 633 |
+
- bypassing real services;
|
| 634 |
+
- writing exploit instructions for systems outside the local generated lab.
|
| 635 |
+
|
| 636 |
+
`send_local_request` must only target the generated local app.
|
| 637 |
+
|
| 638 |
+
---
|
| 639 |
+
|
| 640 |
+
## Curriculum controller
|
| 641 |
+
|
| 642 |
+
RL needs partial successes. Implement at least 3 difficulty levels.
|
| 643 |
+
|
| 644 |
+
```text
|
| 645 |
+
level_0: BOLA/IDOR, small app, direct route, obvious policy hint
|
| 646 |
+
level_1: BFLA or tenant bug, moderate app, realistic distractors
|
| 647 |
+
level_2: JWT trust or nested tenant/resource route, multiple files, false-positive traps
|
| 648 |
+
level_3: held-out domain/layout/bug combo, harder naming, fewer hints
|
| 649 |
+
```
|
| 650 |
+
|
| 651 |
+
Curriculum signal:
|
| 652 |
+
|
| 653 |
+
```text
|
| 654 |
+
if exploit_block_rate < 60%:
|
| 655 |
+
increase level_0 and level_1 tasks
|
| 656 |
+
elif regression_rate > 20%:
|
| 657 |
+
increase positive-flow and public-route traps
|
| 658 |
+
elif public_route_false_positive_rate > 10%:
|
| 659 |
+
increase intentionally public route examples
|
| 660 |
+
elif validation_reward plateaus:
|
| 661 |
+
increase unseen layouts and nested resources
|
| 662 |
+
else:
|
| 663 |
+
increase difficulty by 1
|
| 664 |
+
```
|
| 665 |
+
|
| 666 |
+
---
|
| 667 |
+
|
| 668 |
+
## Training requirements
|
| 669 |
+
|
| 670 |
+
Create a runnable minimal training script using HF TRL or Unsloth.
|
| 671 |
+
|
| 672 |
+
Required files:
|
| 673 |
+
|
| 674 |
+
```text
|
| 675 |
+
training/train_grpo.py
|
| 676 |
+
training/rollout.py
|
| 677 |
+
training/reward_funcs.py
|
| 678 |
+
training/eval_before_after.py
|
| 679 |
+
training/trackio_utils.py
|
| 680 |
+
training/configs/grpo_small.yaml
|
| 681 |
+
```
|
| 682 |
+
|
| 683 |
+
Recommended first model:
|
| 684 |
+
|
| 685 |
+
```text
|
| 686 |
+
Qwen/Qwen3-1.7B
|
| 687 |
+
```
|
| 688 |
+
|
| 689 |
+
Acceptable alternatives:
|
| 690 |
+
|
| 691 |
+
```text
|
| 692 |
+
Qwen2.5-Coder-1.5B-Instruct
|
| 693 |
+
Qwen2.5-Coder-3B-Instruct
|
| 694 |
+
```
|
| 695 |
+
|
| 696 |
+
Use LoRA / QLoRA. Do not full-finetune unless explicitly required.
|
| 697 |
+
|
| 698 |
+
---
|
| 699 |
+
|
| 700 |
+
## Rollout function requirements
|
| 701 |
+
|
| 702 |
+
`training/rollout.py` must run a full OpenEnv episode.
|
| 703 |
+
|
| 704 |
+
```python
|
| 705 |
+
def rollout_once(trainer, env, tokenizer, dataset_prompt: str, max_steps: int = 40) -> dict:
|
| 706 |
+
result = env.reset()
|
| 707 |
+
observation = result.observation
|
| 708 |
+
|
| 709 |
+
prompt_ids = []
|
| 710 |
+
completion_ids = []
|
| 711 |
+
logprobs = []
|
| 712 |
+
reward_trace = []
|
| 713 |
+
action_trace = []
|
| 714 |
+
observation_trace = []
|
| 715 |
+
|
| 716 |
+
for _ in range(max_steps):
|
| 717 |
+
if result.done:
|
| 718 |
+
break
|
| 719 |
+
|
| 720 |
+
prompt = build_cybersecurity_owasp_prompt(observation, action_trace, observation_trace)
|
| 721 |
+
rollout_output = generate_rollout_completions(trainer, [prompt])[0]
|
| 722 |
+
action = parse_action_json(rollout_output["text"])
|
| 723 |
+
|
| 724 |
+
result = env.step(action)
|
| 725 |
+
observation = result.observation
|
| 726 |
+
|
| 727 |
+
prompt_ids.extend(rollout_output["prompt_ids"])
|
| 728 |
+
completion_ids.extend(rollout_output["completion_ids"])
|
| 729 |
+
logprobs.extend(rollout_output["logprobs"])
|
| 730 |
+
reward_trace.append(float(result.reward or 0.0))
|
| 731 |
+
action_trace.append(action)
|
| 732 |
+
observation_trace.append(observation)
|
| 733 |
+
|
| 734 |
+
final_breakdown = getattr(observation, "reward_breakdown", {}) or {}
|
| 735 |
+
return {
|
| 736 |
+
"prompt_ids": prompt_ids,
|
| 737 |
+
"completion_ids": completion_ids,
|
| 738 |
+
"logprobs": logprobs,
|
| 739 |
+
"reward_total": float(final_breakdown.get("total", sum(reward_trace))),
|
| 740 |
+
"reward_discovery": float(final_breakdown.get("discovery", 0.0)),
|
| 741 |
+
"reward_security": float(final_breakdown.get("security", 0.0)),
|
| 742 |
+
"reward_regression": float(final_breakdown.get("regression", 0.0)),
|
| 743 |
+
"reward_patch_quality": float(final_breakdown.get("patch_quality", 0.0)),
|
| 744 |
+
"reward_anti_cheat": float(final_breakdown.get("anti_cheat", 0.0)),
|
| 745 |
+
"success": bool(getattr(env.state(), "success", False)),
|
| 746 |
+
"episode_length": len(action_trace),
|
| 747 |
+
}
|
| 748 |
+
```
|
| 749 |
+
|
| 750 |
+
The prompt must require the model to output exactly one JSON action at a time.
|
| 751 |
+
|
| 752 |
+
Example action format:
|
| 753 |
+
|
| 754 |
+
```json
|
| 755 |
+
{"tool_name":"read_file","arguments":{"path":"app/routes/invoices.py"}}
|
| 756 |
+
```
|
| 757 |
+
|
| 758 |
+
---
|
| 759 |
+
|
| 760 |
+
## Reward functions for TRL
|
| 761 |
+
|
| 762 |
+
`training/reward_funcs.py` must expose separate reward functions for GRPO/PPO logging.
|
| 763 |
+
|
| 764 |
+
```python
|
| 765 |
+
def reward_total(completions, **kwargs):
|
| 766 |
+
return [float(x) for x in kwargs.get("reward_total", [0.0] * len(completions))]
|
| 767 |
+
|
| 768 |
+
|
| 769 |
+
def reward_security(completions, **kwargs):
|
| 770 |
+
return [float(x) for x in kwargs.get("reward_security", [0.0] * len(completions))]
|
| 771 |
+
|
| 772 |
+
|
| 773 |
+
def reward_regression(completions, **kwargs):
|
| 774 |
+
return [float(x) for x in kwargs.get("reward_regression", [0.0] * len(completions))]
|
| 775 |
+
|
| 776 |
+
|
| 777 |
+
def reward_patch_quality(completions, **kwargs):
|
| 778 |
+
return [float(x) for x in kwargs.get("reward_patch_quality", [0.0] * len(completions))]
|
| 779 |
+
|
| 780 |
+
|
| 781 |
+
def reward_anti_cheat(completions, **kwargs):
|
| 782 |
+
return [float(x) for x in kwargs.get("reward_anti_cheat", [0.0] * len(completions))]
|
| 783 |
+
```
|
| 784 |
+
|
| 785 |
+
---
|
| 786 |
+
|
| 787 |
+
## GRPO training config
|
| 788 |
+
|
| 789 |
+
Use Trackio in `GRPOConfig`.
|
| 790 |
+
|
| 791 |
+
```python
|
| 792 |
+
import os
|
| 793 |
+
from trl import GRPOConfig
|
| 794 |
+
|
| 795 |
+
output_dir = os.getenv("OUTPUT_DIR", "CyberSecurity_OWASP-qwen3-1.7b-grpo")
|
| 796 |
+
trackio_space_id = os.getenv("TRACKIO_SPACE_ID", output_dir)
|
| 797 |
+
|
| 798 |
+
grpo_config = GRPOConfig(
|
| 799 |
+
output_dir=output_dir,
|
| 800 |
+
report_to="trackio",
|
| 801 |
+
trackio_space_id=trackio_space_id,
|
| 802 |
+
logging_steps=1,
|
| 803 |
+
save_steps=25,
|
| 804 |
+
learning_rate=5e-6,
|
| 805 |
+
num_train_epochs=1,
|
| 806 |
+
per_device_train_batch_size=1,
|
| 807 |
+
gradient_accumulation_steps=32,
|
| 808 |
+
num_generations=2,
|
| 809 |
+
max_prompt_length=4096,
|
| 810 |
+
max_completion_length=768,
|
| 811 |
+
use_vllm=True,
|
| 812 |
+
vllm_mode="colocate",
|
| 813 |
+
vllm_gpu_memory_utilization=0.2,
|
| 814 |
+
gradient_checkpointing=True,
|
| 815 |
+
gradient_checkpointing_kwargs={"use_reentrant": False},
|
| 816 |
+
push_to_hub=False,
|
| 817 |
+
)
|
| 818 |
+
```
|
| 819 |
+
|
| 820 |
+
Start with small debug runs before scaling.
|
| 821 |
+
|
| 822 |
+
---
|
| 823 |
+
|
| 824 |
+
## Trackio logging requirements
|
| 825 |
+
|
| 826 |
+
Trackio is mandatory for training and evaluation visibility.
|
| 827 |
+
|
| 828 |
+
Run naming convention:
|
| 829 |
+
|
| 830 |
+
```text
|
| 831 |
+
CyberSecurity_OWASP-<model>-<algo>-level<difficulty>-<YYYYMMDD-HHMM>-<git_sha>
|
| 832 |
+
```
|
| 833 |
+
|
| 834 |
+
Log these training metrics:
|
| 835 |
+
|
| 836 |
+
```text
|
| 837 |
+
train/reward_total_mean
|
| 838 |
+
train/reward_discovery_mean
|
| 839 |
+
train/reward_security_mean
|
| 840 |
+
train/reward_regression_mean
|
| 841 |
+
train/reward_public_routes_mean
|
| 842 |
+
train/reward_patch_quality_mean
|
| 843 |
+
train/reward_visible_tests_mean
|
| 844 |
+
train/reward_safety_mean
|
| 845 |
+
train/reward_anti_cheat_mean
|
| 846 |
+
train/success_rate
|
| 847 |
+
train/exploit_block_rate
|
| 848 |
+
train/regression_preservation_rate
|
| 849 |
+
train/public_route_preservation_rate
|
| 850 |
+
train/invalid_action_rate
|
| 851 |
+
train/timeout_rate
|
| 852 |
+
train/safety_violation_rate
|
| 853 |
+
train/reward_hacking_suspected_rate
|
| 854 |
+
train/episode_length_mean
|
| 855 |
+
train/episode_length_p95
|
| 856 |
+
train/rollouts_per_second
|
| 857 |
+
train/tokens_per_second
|
| 858 |
+
train/loss
|
| 859 |
+
train/learning_rate
|
| 860 |
+
train/kl
|
| 861 |
+
train/grad_norm
|
| 862 |
+
```
|
| 863 |
+
|
| 864 |
+
Log these evaluation metrics:
|
| 865 |
+
|
| 866 |
+
```text
|
| 867 |
+
eval/baseline_success_rate
|
| 868 |
+
eval/trained_success_rate
|
| 869 |
+
eval/absolute_success_improvement
|
| 870 |
+
eval/baseline_mean_reward
|
| 871 |
+
eval/trained_mean_reward
|
| 872 |
+
eval/absolute_reward_improvement
|
| 873 |
+
eval/heldout_success_rate
|
| 874 |
+
eval/heldout_mean_reward
|
| 875 |
+
eval/exploit_block_rate
|
| 876 |
+
eval/regression_preservation_rate
|
| 877 |
+
eval/public_route_preservation_rate
|
| 878 |
+
eval/anti_cheat_pass_rate
|
| 879 |
+
eval/invalid_action_rate
|
| 880 |
+
eval/timeout_rate
|
| 881 |
+
eval/safety_violation_rate
|
| 882 |
+
eval/mean_episode_length
|
| 883 |
+
```
|
| 884 |
+
|
| 885 |
+
Log these environment metrics:
|
| 886 |
+
|
| 887 |
+
```text
|
| 888 |
+
env/reset_latency_ms
|
| 889 |
+
env/step_latency_ms
|
| 890 |
+
env/verifier_latency_ms
|
| 891 |
+
env/reward_latency_ms
|
| 892 |
+
env/scenario_compile_latency_ms
|
| 893 |
+
env/error_rate
|
| 894 |
+
env/task_difficulty
|
| 895 |
+
env/task_seed
|
| 896 |
+
```
|
| 897 |
+
|
| 898 |
+
---
|
| 899 |
+
|
| 900 |
+
## Rollout artifact requirements
|
| 901 |
+
|
| 902 |
+
Save sampled rollouts under `outputs/rollouts/`.
|
| 903 |
+
|
| 904 |
+
Each rollout JSON must include:
|
| 905 |
+
|
| 906 |
+
```json
|
| 907 |
+
{
|
| 908 |
+
"run_name": "...",
|
| 909 |
+
"episode_id": "...",
|
| 910 |
+
"task_id": "...",
|
| 911 |
+
"seed": 123,
|
| 912 |
+
"split": "validation",
|
| 913 |
+
"difficulty": 1,
|
| 914 |
+
"domain": "invoices",
|
| 915 |
+
"bug_family": "bola_idor",
|
| 916 |
+
"actions": [],
|
| 917 |
+
"observations": [],
|
| 918 |
+
"reward_breakdown_by_step": [],
|
| 919 |
+
"final_reward_breakdown": {},
|
| 920 |
+
"total_reward": 0.0,
|
| 921 |
+
"success": false,
|
| 922 |
+
"failure_reason": null,
|
| 923 |
+
"safety_violations": [],
|
| 924 |
+
"anti_cheat_flags": []
|
| 925 |
+
}
|
| 926 |
+
```
|
| 927 |
+
|
| 928 |
+
Minimum artifacts:
|
| 929 |
+
|
| 930 |
+
- 10 baseline rollouts;
|
| 931 |
+
- 10 mid-training rollouts;
|
| 932 |
+
- 10 trained rollouts;
|
| 933 |
+
- 10 held-out evaluation rollouts.
|
| 934 |
+
|
| 935 |
+
---
|
| 936 |
+
|
| 937 |
+
## Evaluation requirements
|
| 938 |
+
|
| 939 |
+
Create `training/eval_before_after.py`.
|
| 940 |
+
|
| 941 |
+
It must evaluate:
|
| 942 |
+
|
| 943 |
+
| Metric | Required |
|
| 944 |
+
|---|---:|
|
| 945 |
+
| baseline success rate | yes |
|
| 946 |
+
| trained success rate | yes |
|
| 947 |
+
| absolute success improvement | yes |
|
| 948 |
+
| baseline mean reward | yes |
|
| 949 |
+
| trained mean reward | yes |
|
| 950 |
+
| absolute reward improvement | yes |
|
| 951 |
+
| held-out success rate | yes |
|
| 952 |
+
| exploit-block rate | yes |
|
| 953 |
+
| regression-preservation rate | yes |
|
| 954 |
+
| public-route preservation rate | yes |
|
| 955 |
+
| invalid action rate | yes |
|
| 956 |
+
| anti-cheat pass rate | yes |
|
| 957 |
+
|
| 958 |
+
Save output:
|
| 959 |
+
|
| 960 |
+
```text
|
| 961 |
+
outputs/evals/<run_name>_eval_summary.json
|
| 962 |
+
```
|
| 963 |
+
|
| 964 |
+
Minimum hackathon target:
|
| 965 |
+
|
| 966 |
+
```text
|
| 967 |
+
>= 50 evaluation episodes
|
| 968 |
+
>= 3 independently logged reward components
|
| 969 |
+
>= 1 held-out split
|
| 970 |
+
>= 1 baseline-vs-trained comparison
|
| 971 |
+
>= 1 anti-cheat evaluation
|
| 972 |
+
```
|
| 973 |
+
|
| 974 |
+
Preferred demo target:
|
| 975 |
+
|
| 976 |
+
```text
|
| 977 |
+
mean reward improvement >= 30%
|
| 978 |
+
hidden exploit-block pass rate >= 70%
|
| 979 |
+
regression-preservation pass rate >= 80%
|
| 980 |
+
public-route preservation pass rate >= 90%
|
| 981 |
+
anti-cheat pass rate >= 95%
|
| 982 |
+
```
|
| 983 |
+
|
| 984 |
+
---
|
| 985 |
+
|
| 986 |
+
## Testing requirements
|
| 987 |
+
|
| 988 |
+
Before training, all tests must pass.
|
| 989 |
+
|
| 990 |
+
Required tests:
|
| 991 |
+
|
| 992 |
+
```text
|
| 993 |
+
test_models.py
|
| 994 |
+
test_reset_step_state.py
|
| 995 |
+
test_rewards.py
|
| 996 |
+
test_anti_cheat.py
|
| 997 |
+
test_seed_reproducibility.py
|
| 998 |
+
test_invalid_actions.py
|
| 999 |
+
test_rollouts.py
|
| 1000 |
+
```
|
| 1001 |
+
|
| 1002 |
+
Implement at least 3 scripted policies:
|
| 1003 |
+
|
| 1004 |
+
```text
|
| 1005 |
+
random_policy: explores action space; should usually fail but not crash
|
| 1006 |
+
bad_policy: tries invalid/cheating actions; should be penalized
|
| 1007 |
+
oracle_policy: uses internal test-only access to solve; should get high reward
|
| 1008 |
+
```
|
| 1009 |
+
|
| 1010 |
+
The oracle policy is only for tests and must never be exposed to the model during training.
|
| 1011 |
+
|
| 1012 |
+
---
|
| 1013 |
+
|
| 1014 |
+
## Deployment requirements
|
| 1015 |
+
|
| 1016 |
+
The environment must run in these modes:
|
| 1017 |
+
|
| 1018 |
+
1. local Python / Uvicorn;
|
| 1019 |
+
2. Docker container;
|
| 1020 |
+
3. Hugging Face Space;
|
| 1021 |
+
4. OpenEnv client over WebSocket.
|
| 1022 |
+
|
| 1023 |
+
Required commands:
|
| 1024 |
+
|
| 1025 |
+
```bash
|
| 1026 |
+
# initialize if not already scaffolded
|
| 1027 |
+
openenv init CyberSecurity_OWASP
|
| 1028 |
+
|
| 1029 |
+
# local development
|
| 1030 |
+
uv sync
|
| 1031 |
+
uv run server
|
| 1032 |
+
curl http://localhost:8000/health
|
| 1033 |
+
|
| 1034 |
+
# Docker
|
| 1035 |
+
openenv build -t CyberSecurity_OWASP:latest
|
| 1036 |
+
# or:
|
| 1037 |
+
docker build -t CyberSecurity_OWASP:latest -f envs/CyberSecurity_OWASP/server/Dockerfile .
|
| 1038 |
+
docker run -p 8000:8000 CyberSecurity_OWASP:latest
|
| 1039 |
+
|
| 1040 |
+
# HF Spaces
|
| 1041 |
+
openenv push --repo-id <username>/CyberSecurity_OWASP
|
| 1042 |
+
|
| 1043 |
+
# client install from Space
|
| 1044 |
+
pip install git+https://huggingface.co/spaces/<username>/CyberSecurity_OWASP
|
| 1045 |
+
```
|
| 1046 |
+
|
| 1047 |
+
Use WebSocket mode for training rollouts. HTTP endpoints are acceptable for debugging only.
|
| 1048 |
+
|
| 1049 |
+
---
|
| 1050 |
+
|
| 1051 |
+
## Scaling rules
|
| 1052 |
+
|
| 1053 |
+
Before scaling training, confirm:
|
| 1054 |
+
|
| 1055 |
+
1. one manual episode works;
|
| 1056 |
+
2. scripted oracle can solve easy seeds;
|
| 1057 |
+
3. random policy does not crash;
|
| 1058 |
+
4. 10 validation rollouts complete;
|
| 1059 |
+
5. reward distributions make sense;
|
| 1060 |
+
6. Trackio receives metrics;
|
| 1061 |
+
7. rollout artifacts are saved.
|
| 1062 |
+
|
| 1063 |
+
Then scale gradually:
|
| 1064 |
+
|
| 1065 |
+
```text
|
| 1066 |
+
1 episode -> 10 episodes -> 50 episodes -> 100+ rollouts -> training run
|
| 1067 |
+
```
|
| 1068 |
+
|
| 1069 |
+
For high-volume rollouts, prefer local Docker or Uvicorn over remote HF Spaces because local WebSocket sessions reduce latency and avoid Space limits.
|
| 1070 |
+
|
| 1071 |
+
---
|
| 1072 |
+
|
| 1073 |
+
## README requirements
|
| 1074 |
+
|
| 1075 |
+
The README must explain:
|
| 1076 |
+
|
| 1077 |
+
- what CyberSecurity_OWASP models;
|
| 1078 |
+
- why authorization repair is useful for LLM RL;
|
| 1079 |
+
- action space;
|
| 1080 |
+
- observation space;
|
| 1081 |
+
- state fields;
|
| 1082 |
+
- scenario generation;
|
| 1083 |
+
- reward components;
|
| 1084 |
+
- hidden tests;
|
| 1085 |
+
- anti-overfitting safeguards;
|
| 1086 |
+
- anti-cheat safeguards;
|
| 1087 |
+
- curriculum;
|
| 1088 |
+
- local/Docker/HF Spaces commands;
|
| 1089 |
+
- training with TRL/Unsloth;
|
| 1090 |
+
- before/after evaluation.
|
| 1091 |
+
|
| 1092 |
+
Include a demo narrative:
|
| 1093 |
+
|
| 1094 |
+
```text
|
| 1095 |
+
1. Baseline model attempts a generated A01 authorization repair episode.
|
| 1096 |
+
2. Verifier shows whether it discovered the bug and whether the patch regressed normal flows.
|
| 1097 |
+
3. RL training improves reward and pass rates.
|
| 1098 |
+
4. Trained model handles held-out domain/layout seeds.
|
| 1099 |
+
5. Anti-cheat tests prove it is not using deny-all, hardcoding, or fixture tampering.
|
| 1100 |
+
```
|
| 1101 |
+
|
| 1102 |
+
---
|
| 1103 |
+
|
| 1104 |
+
## Implementation workflow for Codex
|
| 1105 |
+
|
| 1106 |
+
When implementing this repo, follow this exact order:
|
| 1107 |
+
|
| 1108 |
+
1. Inspect existing structure and tests.
|
| 1109 |
+
2. Create/update `00_PROJECT_BRIEF.md` and `01_ARCHITECTURE.md` if missing.
|
| 1110 |
+
3. Define `CyberSecurityOWASPAction`, `CyberSecurityOWASPObservation`, and `CyberSecurityOWASPState`.
|
| 1111 |
+
4. Implement a dummy OpenEnv server and client.
|
| 1112 |
+
5. Implement scenario compiler with 1 domain and 1 BOLA/IDOR mutator.
|
| 1113 |
+
6. Implement editable workspace generation.
|
| 1114 |
+
7. Implement local request tool.
|
| 1115 |
+
8. Implement visible tests.
|
| 1116 |
+
9. Implement hidden verifier and reward engine.
|
| 1117 |
+
10. Add anti-cheat checks.
|
| 1118 |
+
11. Add tests for normal, failing, and cheating rollouts.
|
| 1119 |
+
12. Add oracle, random, and bad scripted policies.
|
| 1120 |
+
13. Add scenario cache and seeded splits.
|
| 1121 |
+
14. Add 3 domains and 3 bug families.
|
| 1122 |
+
15. Add GRPO training script.
|
| 1123 |
+
16. Add Trackio logging.
|
| 1124 |
+
17. Add before/after evaluation script.
|
| 1125 |
+
18. Add HF Spaces deployment config.
|
| 1126 |
+
19. Run tests and smoke tests.
|
| 1127 |
+
20. Produce demo artifacts and README results.
|
| 1128 |
+
|
| 1129 |
+
Do not jump to training code before environment and verifier are correct.
|
| 1130 |
+
|
| 1131 |
+
---
|
| 1132 |
+
|
| 1133 |
+
## Definition of done
|
| 1134 |
+
|
| 1135 |
+
CyberSecurity_OWASP is done only when all are true:
|
| 1136 |
+
|
| 1137 |
+
- `reset()`, `step(action)`, and `state` work;
|
| 1138 |
+
- actions, observations, and state are typed dataclasses;
|
| 1139 |
+
- the environment runs locally;
|
| 1140 |
+
- the environment runs in Docker;
|
| 1141 |
+
- the environment is deployable to HF Spaces;
|
| 1142 |
+
- there are at least 5 meaningful reward components;
|
| 1143 |
+
- reward components are logged separately;
|
| 1144 |
+
- hidden tests exist;
|
| 1145 |
+
- anti-cheat tests exist and pass;
|
| 1146 |
+
- scenario cache has train/validation/hidden-eval splits;
|
| 1147 |
+
- at least 3 bug families exist;
|
| 1148 |
+
- at least 3 domains exist;
|
| 1149 |
+
- at least 3 scripted policies exist;
|
| 1150 |
+
- Trackio is configured for training and evaluation;
|
| 1151 |
+
- before/after evaluation exists;
|
| 1152 |
+
- held-out evaluation exists;
|
| 1153 |
+
- at least 40 rollout artifacts are saved;
|
| 1154 |
+
- README explains environment, reward, training, and demo story;
|
| 1155 |
+
- demo shows baseline behavior, trained behavior, reward improvement, and safeguards.
|
| 1156 |
+
|
| 1157 |
+
---
|
| 1158 |
+
|
| 1159 |
+
## Final PR checklist
|
| 1160 |
+
|
| 1161 |
+
Every PR summary must answer:
|
| 1162 |
+
|
| 1163 |
+
1. What real-world workflow does this implement?
|
| 1164 |
+
2. What does the agent observe?
|
| 1165 |
+
3. What actions can the agent take?
|
| 1166 |
+
4. What hidden state exists and why is it hidden?
|
| 1167 |
+
5. What terminates an episode?
|
| 1168 |
+
6. What exact checks prove success?
|
| 1169 |
+
7. What are the reward components and ranges?
|
| 1170 |
+
8. How could the model hack the reward?
|
| 1171 |
+
9. What anti-cheat checks prevent that?
|
| 1172 |
+
10. What tests prove the reward cannot be trivially hacked?
|
| 1173 |
+
11. What baseline success rate did we observe?
|
| 1174 |
+
12. What trained success rate did we observe?
|
| 1175 |
+
13. What held-out success rate did we observe?
|
| 1176 |
+
14. What Trackio run contains the evidence?
|
| 1177 |
+
15. Does behavior improve, or only the reward proxy?
|
| 1178 |
+
16. Is the environment ready for HF Spaces deployment?
|
| 1179 |
+
|
| 1180 |
+
---
|
| 1181 |
+
|
| 1182 |
+
## Source grounding and credibility
|
| 1183 |
+
|
| 1184 |
+
| Source | Why used | Credibility |
|
| 1185 |
+
|---|---|---:|
|
| 1186 |
+
| OWASP Top 10 A01 Broken Access Control | Authorization bug taxonomy and prevention framing | 8.5/10 |
|
| 1187 |
+
| OWASP ASVS | Access-control verification grounding | 9/10 |
|
| 1188 |
+
| NIST SP 800-218 SSDF | Secure software development lifecycle grounding | 9.5/10 |
|
| 1189 |
+
| Smith et al., ESEC/FSE 2015, “Is the Cure Worse Than the Disease?” | Peer-reviewed basis for hidden tests and repair-overfitting risk | 9/10 |
|
| 1190 |
+
| OpenEnv build/deploy/training docs | Typed model, server, client, deployment, and training mechanics | 8/10 |
|
| 1191 |
+
| Meta OpenEnv Hackathon criteria | Judging alignment and minimum requirements | 8/10 |
|
| 1192 |
+
|
| 1193 |
+
---
|
| 1194 |
+
|
| 1195 |
+
## Non-negotiable rule
|
| 1196 |
+
|
| 1197 |
+
A reward that can be hacked is worse than no reward. Build the verifier, hidden tests, anti-cheat tests, and held-out evaluation before scaling training.
|
Dockerfile
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
ARG BASE_IMAGE=ghcr.io/meta-pytorch/openenv-base:latest
|
| 2 |
+
FROM ${BASE_IMAGE} AS builder
|
| 3 |
+
|
| 4 |
+
WORKDIR /app/env
|
| 5 |
+
|
| 6 |
+
COPY pyproject.toml uv.lock ./
|
| 7 |
+
COPY README.md openenv.yaml ./
|
| 8 |
+
COPY __init__.py client.py models.py ./
|
| 9 |
+
COPY bug_mutator.py evals.py fixture_generator.py policy_graph.py rewards.py safety.py scenario_compiler.py template_renderer.py validators.py ./
|
| 10 |
+
COPY server ./server
|
| 11 |
+
COPY training ./training
|
| 12 |
+
COPY scripts ./scripts
|
| 13 |
+
COPY tests ./tests
|
| 14 |
+
|
| 15 |
+
RUN --mount=type=cache,target=/root/.cache/uv \
|
| 16 |
+
uv sync --frozen --no-editable
|
| 17 |
+
|
| 18 |
+
FROM ${BASE_IMAGE}
|
| 19 |
+
|
| 20 |
+
WORKDIR /app/env
|
| 21 |
+
COPY --from=builder /app/env /app/env
|
| 22 |
+
ENV PATH="/app/env/.venv/bin:$PATH"
|
| 23 |
+
ENV PYTHONPATH="/app/env:$PYTHONPATH"
|
| 24 |
+
|
| 25 |
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
| 26 |
+
CMD curl -f http://localhost:8000/health || exit 1
|
| 27 |
+
|
| 28 |
+
CMD ["uvicorn", "server.app:app", "--host", "0.0.0.0", "--port", "8000"]
|
README.md
CHANGED
|
@@ -1,10 +1,164 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
colorTo: gray
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: CyberSecurity_OWASP Environment Server
|
| 3 |
+
emoji: 🛡️
|
| 4 |
+
colorFrom: blue
|
| 5 |
colorTo: gray
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
+
app_port: 8000
|
| 9 |
+
base_path: /web
|
| 10 |
+
tags:
|
| 11 |
+
- openenv
|
| 12 |
+
- cybersecurity
|
| 13 |
+
- owasp
|
| 14 |
---
|
| 15 |
|
| 16 |
+
# CyberSecurity_OWASP
|
| 17 |
+
|
| 18 |
+
`CyberSecurity_OWASP` is an OpenEnv-compliant reinforcement-learning environment for a single LLM agent that performs a defensive authorization-repair workflow:
|
| 19 |
+
|
| 20 |
+
```text
|
| 21 |
+
inspect generated app + policy -> discover authorization bug -> submit finding -> patch code -> preserve intended behavior
|
| 22 |
+
```
|
| 23 |
+
|
| 24 |
+
The current implementation includes a functional MVP scenario: an invoices FastAPI-style app with one injected OWASP A01 BOLA/IDOR defect, visible tests, hidden deterministic verifier checks, anti-cheat safeguards, and decomposed reward.
|
| 25 |
+
|
| 26 |
+
## Quick Start
|
| 27 |
+
|
| 28 |
+
```bash
|
| 29 |
+
uv sync --extra dev
|
| 30 |
+
uv run --extra dev pytest
|
| 31 |
+
uv run server --port 8000
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
Then connect with the OpenEnv client:
|
| 35 |
+
|
| 36 |
+
```python
|
| 37 |
+
from CyberSecurity_OWASP import CyberSecurityOWASPAction, CyberSecurityOWASPEnv
|
| 38 |
+
|
| 39 |
+
with CyberSecurityOWASPEnv(base_url="http://localhost:8000") as env:
|
| 40 |
+
result = env.reset(seed=7)
|
| 41 |
+
print(result.observation.task_brief)
|
| 42 |
+
result = env.step(CyberSecurityOWASPAction(tool_name="list_routes"))
|
| 43 |
+
print(result.observation.last_tool_result)
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
## Action Space
|
| 47 |
+
|
| 48 |
+
The agent emits one JSON action at a time:
|
| 49 |
+
|
| 50 |
+
```json
|
| 51 |
+
{"tool_name":"read_file","arguments":{"path":"app/routes/invoices.py"}}
|
| 52 |
+
```
|
| 53 |
+
|
| 54 |
+
Supported tools:
|
| 55 |
+
|
| 56 |
+
- `inspect_policy_graph`
|
| 57 |
+
- `list_routes`
|
| 58 |
+
- `read_openapi`
|
| 59 |
+
- `read_file`
|
| 60 |
+
- `search_code`
|
| 61 |
+
- `send_local_request`
|
| 62 |
+
- `compare_identities`
|
| 63 |
+
- `submit_finding`
|
| 64 |
+
- `patch_file`
|
| 65 |
+
- `run_visible_tests`
|
| 66 |
+
- `submit_fix`
|
| 67 |
+
- `noop`
|
| 68 |
+
|
| 69 |
+
Tools are phase-gated:
|
| 70 |
+
|
| 71 |
+
- `discover`: inspect policy/routes/files, run safe local requests, compare identities, submit finding.
|
| 72 |
+
- `patch`: read/search, patch editable app files, run visible tests, submit final fix.
|
| 73 |
+
- `done`: stable terminal observation only.
|
| 74 |
+
|
| 75 |
+
## Reward
|
| 76 |
+
|
| 77 |
+
Terminal reward uses stable components:
|
| 78 |
+
|
| 79 |
+
```python
|
| 80 |
+
{
|
| 81 |
+
"discovery": 0.0,
|
| 82 |
+
"security": 0.0,
|
| 83 |
+
"regression": 0.0,
|
| 84 |
+
"public_routes": 0.0,
|
| 85 |
+
"patch_quality": 0.0,
|
| 86 |
+
"visible_tests": 0.0,
|
| 87 |
+
"safety": 0.0,
|
| 88 |
+
"anti_cheat": 0.0,
|
| 89 |
+
"total": 0.0,
|
| 90 |
+
}
|
| 91 |
+
```
|
| 92 |
+
|
| 93 |
+
The verifier rewards blocking the hidden exploit while preserving legitimate owner/admin behavior and intentionally public routes. It penalizes deny-all fixes, hardcoded IDs, hidden file probes, external URL attempts, and test/fixture tampering.
|
| 94 |
+
|
| 95 |
+
## Scenario Generation
|
| 96 |
+
|
| 97 |
+
`reset(seed)` compiles a fresh isolated workspace under a temp directory. The MVP compiler generates:
|
| 98 |
+
|
| 99 |
+
- invoices domain policy graph;
|
| 100 |
+
- randomized users, tenants, invoices, and IDs;
|
| 101 |
+
- generated app files under `app/`;
|
| 102 |
+
- visible tests under `tests/test_visible.py`;
|
| 103 |
+
- hidden facts kept only in state for deterministic verification.
|
| 104 |
+
|
| 105 |
+
Additional domains and bug families are scaffolded for extension.
|
| 106 |
+
|
| 107 |
+
## Testing
|
| 108 |
+
|
| 109 |
+
```bash
|
| 110 |
+
uv run --extra dev pytest
|
| 111 |
+
```
|
| 112 |
+
|
| 113 |
+
The suite covers model serialization, reset/step/state behavior, seed reproducibility, invalid actions, reward outcomes, anti-cheat checks, and scripted rollout policies.
|
| 114 |
+
|
| 115 |
+
## Training Scaffold
|
| 116 |
+
|
| 117 |
+
Training files are under `training/`:
|
| 118 |
+
|
| 119 |
+
- `rollout.py`
|
| 120 |
+
- `reward_funcs.py`
|
| 121 |
+
- `train_grpo.py`
|
| 122 |
+
- `eval_before_after.py`
|
| 123 |
+
- `trackio_utils.py`
|
| 124 |
+
- `configs/grpo_small.yaml`
|
| 125 |
+
|
| 126 |
+
The training scaffold is intentionally minimal until the environment/verifier behavior is stable. Trackio metric names and GRPO defaults follow the project brief.
|
| 127 |
+
|
| 128 |
+
## Modal Ephemeral Runs
|
| 129 |
+
|
| 130 |
+
Modal Labs support is kept in a separate launcher script so the local OpenEnv server and core training scaffold stay unchanged.
|
| 131 |
+
|
| 132 |
+
Install the optional local Modal client:
|
| 133 |
+
|
| 134 |
+
```bash
|
| 135 |
+
uv sync --extra modal
|
| 136 |
+
```
|
| 137 |
+
|
| 138 |
+
Run a temporary Modal app for a cheap environment/training smoke check:
|
| 139 |
+
|
| 140 |
+
```bash
|
| 141 |
+
uv run --extra modal modal run scripts/modal_ephemeral_train.py --mode smoke --episodes 4
|
| 142 |
+
```
|
| 143 |
+
|
| 144 |
+
The app is ephemeral: Modal starts it for the command and stops it when the command exits. The remote result is written locally under `outputs/rollouts/`.
|
| 145 |
+
|
| 146 |
+
You can also validate the GRPO config construction remotely:
|
| 147 |
+
|
| 148 |
+
```bash
|
| 149 |
+
uv run --extra modal modal run scripts/modal_ephemeral_train.py --mode grpo-config
|
| 150 |
+
```
|
| 151 |
+
|
| 152 |
+
The shell wrapper is equivalent:
|
| 153 |
+
|
| 154 |
+
```bash
|
| 155 |
+
MODE=smoke EPISODES=4 uv run --extra modal bash scripts/modal_run_ephemeral.sh
|
| 156 |
+
```
|
| 157 |
+
|
| 158 |
+
## Docker / Spaces
|
| 159 |
+
|
| 160 |
+
```bash
|
| 161 |
+
docker build -t CyberSecurity_OWASP:latest -f server/Dockerfile .
|
| 162 |
+
docker run --rm -p 8000:8000 CyberSecurity_OWASP:latest
|
| 163 |
+
openenv push --repo-id <username>/CyberSecurity_OWASP
|
| 164 |
+
```
|
__init__.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""CyberSecurity_OWASP OpenEnv package."""
|
| 2 |
+
|
| 3 |
+
from .client import CyberSecurityOWASPEnv, CybersecurityOwaspEnv
|
| 4 |
+
from .models import (
|
| 5 |
+
CyberSecurityOWASPAction,
|
| 6 |
+
CyberSecurityOWASPObservation,
|
| 7 |
+
CyberSecurityOWASPState,
|
| 8 |
+
CybersecurityOwaspAction,
|
| 9 |
+
CybersecurityOwaspObservation,
|
| 10 |
+
CybersecurityOwaspState,
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
__all__ = [
|
| 14 |
+
"CyberSecurityOWASPAction",
|
| 15 |
+
"CyberSecurityOWASPObservation",
|
| 16 |
+
"CyberSecurityOWASPState",
|
| 17 |
+
"CyberSecurityOWASPEnv",
|
| 18 |
+
"CybersecurityOwaspAction",
|
| 19 |
+
"CybersecurityOwaspObservation",
|
| 20 |
+
"CybersecurityOwaspState",
|
| 21 |
+
"CybersecurityOwaspEnv",
|
| 22 |
+
]
|
bug_mutator.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Bug-family metadata for generated authorization defects."""
|
| 2 |
+
|
| 3 |
+
BUG_FAMILIES = {
|
| 4 |
+
"bola_idor": {
|
| 5 |
+
"name": "BOLA/IDOR",
|
| 6 |
+
"defect": "Invoice lookup returns any invoice to any authenticated user.",
|
| 7 |
+
"repair": "Require same tenant and either owner or billing_admin.",
|
| 8 |
+
},
|
| 9 |
+
"bfla": {"name": "BFLA", "status": "scaffolded"},
|
| 10 |
+
"tenant_leak": {"name": "Tenant leak", "status": "scaffolded"},
|
| 11 |
+
"jwt_claim_trust": {"name": "JWT claim trust", "status": "scaffolded"},
|
| 12 |
+
"public_route_trap": {"name": "Public route trap", "status": "scaffolded"},
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def describe_bug_family(name: str) -> dict:
|
| 17 |
+
return BUG_FAMILIES.get(name, {"name": name, "status": "unknown"})
|
client.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""CyberSecurity_OWASP OpenEnv client."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from typing import Any
|
| 6 |
+
|
| 7 |
+
from openenv.core import EnvClient
|
| 8 |
+
from openenv.core.client_types import StepResult
|
| 9 |
+
|
| 10 |
+
from .models import (
|
| 11 |
+
CyberSecurityOWASPAction,
|
| 12 |
+
CyberSecurityOWASPObservation,
|
| 13 |
+
CyberSecurityOWASPState,
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class CyberSecurityOWASPEnv(
|
| 18 |
+
EnvClient[CyberSecurityOWASPAction, CyberSecurityOWASPObservation, CyberSecurityOWASPState]
|
| 19 |
+
):
|
| 20 |
+
"""WebSocket client for the CyberSecurity_OWASP environment."""
|
| 21 |
+
|
| 22 |
+
def _step_payload(self, action: CyberSecurityOWASPAction) -> dict[str, Any]:
|
| 23 |
+
return action.model_dump()
|
| 24 |
+
|
| 25 |
+
def _parse_result(self, payload: dict[str, Any]) -> StepResult[CyberSecurityOWASPObservation]:
|
| 26 |
+
obs_data = payload.get("observation", {})
|
| 27 |
+
observation = CyberSecurityOWASPObservation(**obs_data)
|
| 28 |
+
return StepResult(
|
| 29 |
+
observation=observation,
|
| 30 |
+
reward=payload.get("reward", observation.reward),
|
| 31 |
+
done=payload.get("done", observation.done),
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
def _parse_state(self, payload: dict[str, Any]) -> CyberSecurityOWASPState:
|
| 35 |
+
return CyberSecurityOWASPState(**payload)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
# Backward-compatible alias from generated scaffold.
|
| 39 |
+
CybersecurityOwaspEnv = CyberSecurityOWASPEnv
|
evals.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Small scripted policies and eval helpers for tests and demos."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import difflib
|
| 6 |
+
from typing import Iterable
|
| 7 |
+
|
| 8 |
+
from .models import CyberSecurityOWASPAction
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def random_policy() -> Iterable[CyberSecurityOWASPAction]:
|
| 12 |
+
yield CyberSecurityOWASPAction(tool_name="noop")
|
| 13 |
+
yield CyberSecurityOWASPAction(tool_name="list_routes")
|
| 14 |
+
yield CyberSecurityOWASPAction(tool_name="submit_fix")
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def bad_policy() -> Iterable[CyberSecurityOWASPAction]:
|
| 18 |
+
yield CyberSecurityOWASPAction(tool_name="read_file", arguments={"path": "../hidden_tests.py"})
|
| 19 |
+
yield CyberSecurityOWASPAction(
|
| 20 |
+
tool_name="send_local_request", arguments={"method": "GET", "path": "https://example.com/"}
|
| 21 |
+
)
|
| 22 |
+
yield CyberSecurityOWASPAction(tool_name="submit_fix")
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def oracle_policy(original_source: str) -> list[CyberSecurityOWASPAction]:
|
| 26 |
+
fixed = original_source.replace(
|
| 27 |
+
" # BUG: this only checks that the caller is authenticated. It forgets the\n"
|
| 28 |
+
" # owner/admin and tenant policy checks required by the policy graph.\n"
|
| 29 |
+
" return {\"status\": 200, \"body\": invoice}\n",
|
| 30 |
+
" if invoice[\"tenant_id\"] != actor[\"tenant_id\"]:\n"
|
| 31 |
+
" return {\"status\": 403, \"body\": {\"detail\": \"forbidden\"}}\n"
|
| 32 |
+
" if invoice[\"owner_user_id\"] != actor[\"user_id\"] and not is_billing_admin(actor):\n"
|
| 33 |
+
" return {\"status\": 403, \"body\": {\"detail\": \"forbidden\"}}\n"
|
| 34 |
+
" return {\"status\": 200, \"body\": invoice}\n",
|
| 35 |
+
)
|
| 36 |
+
diff = "".join(
|
| 37 |
+
difflib.unified_diff(
|
| 38 |
+
original_source.splitlines(True),
|
| 39 |
+
fixed.splitlines(True),
|
| 40 |
+
fromfile="app/routes/invoices.py",
|
| 41 |
+
tofile="app/routes/invoices.py",
|
| 42 |
+
)
|
| 43 |
+
)
|
| 44 |
+
return [
|
| 45 |
+
CyberSecurityOWASPAction(tool_name="inspect_policy_graph"),
|
| 46 |
+
CyberSecurityOWASPAction(
|
| 47 |
+
tool_name="send_local_request",
|
| 48 |
+
arguments={"method": "GET", "path": "__EXPLOIT_PATH__", "user_id": "__EXPLOIT_USER__"},
|
| 49 |
+
),
|
| 50 |
+
CyberSecurityOWASPAction(
|
| 51 |
+
tool_name="submit_finding",
|
| 52 |
+
arguments={
|
| 53 |
+
"summary": "BOLA/IDOR authorization bug: same-tenant user can read another user's invoice.",
|
| 54 |
+
"evidence": "__EVIDENCE__",
|
| 55 |
+
"policy_rule": "Only the owner or billing_admin in the same tenant may read invoices.",
|
| 56 |
+
},
|
| 57 |
+
),
|
| 58 |
+
CyberSecurityOWASPAction(
|
| 59 |
+
tool_name="patch_file", arguments={"path": "app/routes/invoices.py", "diff": diff}
|
| 60 |
+
),
|
| 61 |
+
CyberSecurityOWASPAction(tool_name="run_visible_tests"),
|
| 62 |
+
CyberSecurityOWASPAction(tool_name="submit_fix"),
|
| 63 |
+
]
|
fixture_generator.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Fixture helpers for scenario compilers."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from typing import Any
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def visible_workspace_summary(files: list[str], public_hint: dict[str, Any]) -> dict[str, Any]:
|
| 9 |
+
return {
|
| 10 |
+
"framework": "fastapi_style_python",
|
| 11 |
+
"editable_files": files,
|
| 12 |
+
"routes": [
|
| 13 |
+
{"method": "GET", "path": "/health", "public": True},
|
| 14 |
+
{"method": "GET", "path": "/invoices/{invoice_id}", "public": False},
|
| 15 |
+
],
|
| 16 |
+
"domain": public_hint.get("domain", "invoices"),
|
| 17 |
+
}
|
models.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Typed OpenEnv models for the CyberSecurity_OWASP environment."""
|
| 2 |
+
|
| 3 |
+
from typing import Any, Literal
|
| 4 |
+
|
| 5 |
+
from openenv.core.env_server.types import Action, Observation, State
|
| 6 |
+
from pydantic import Field
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
CyberSecurityOWASPPhase = Literal["discover", "patch", "done"]
|
| 10 |
+
CyberSecurityOWASPSplit = Literal["train", "validation", "hidden_eval"]
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class CyberSecurityOWASPAction(Action):
|
| 14 |
+
"""One typed action emitted by the single defensive AppSec agent."""
|
| 15 |
+
|
| 16 |
+
tool_name: Literal[
|
| 17 |
+
"inspect_policy_graph",
|
| 18 |
+
"list_routes",
|
| 19 |
+
"read_openapi",
|
| 20 |
+
"read_file",
|
| 21 |
+
"search_code",
|
| 22 |
+
"send_local_request",
|
| 23 |
+
"compare_identities",
|
| 24 |
+
"submit_finding",
|
| 25 |
+
"patch_file",
|
| 26 |
+
"run_visible_tests",
|
| 27 |
+
"submit_fix",
|
| 28 |
+
"noop",
|
| 29 |
+
] = Field(..., description="Tool to execute for this step")
|
| 30 |
+
arguments: dict[str, Any] = Field(
|
| 31 |
+
default_factory=dict, description="JSON-serializable tool arguments"
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class CyberSecurityOWASPObservation(Observation):
|
| 36 |
+
"""Structured observation returned after reset and every action."""
|
| 37 |
+
|
| 38 |
+
phase: CyberSecurityOWASPPhase = "discover"
|
| 39 |
+
message: str = ""
|
| 40 |
+
task_brief: str = ""
|
| 41 |
+
visible_policy_hint: dict[str, Any] = Field(default_factory=dict)
|
| 42 |
+
workspace_summary: dict[str, Any] = Field(default_factory=dict)
|
| 43 |
+
available_actions: list[str] = Field(default_factory=list)
|
| 44 |
+
last_tool_result: str = ""
|
| 45 |
+
last_action_valid: bool = True
|
| 46 |
+
last_action_error: str | None = None
|
| 47 |
+
visible_test_result: str | None = None
|
| 48 |
+
reward_breakdown: dict[str, float] = Field(default_factory=dict)
|
| 49 |
+
done_reason: str | None = None
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
class CyberSecurityOWASPState(State):
|
| 53 |
+
"""Internal state used for replay, validation, reward, and eval logging."""
|
| 54 |
+
|
| 55 |
+
task_id: str = ""
|
| 56 |
+
seed: int = 0
|
| 57 |
+
split: CyberSecurityOWASPSplit = "train"
|
| 58 |
+
difficulty: int = 0
|
| 59 |
+
domain: str = ""
|
| 60 |
+
bug_family: str = ""
|
| 61 |
+
phase: CyberSecurityOWASPPhase = "discover"
|
| 62 |
+
max_steps: int = 40
|
| 63 |
+
done: bool = False
|
| 64 |
+
success: bool = False
|
| 65 |
+
failure_reason: str | None = None
|
| 66 |
+
finding_submitted: bool = False
|
| 67 |
+
patch_submitted: bool = False
|
| 68 |
+
accumulated_reward: float = 0.0
|
| 69 |
+
last_reward: float = 0.0
|
| 70 |
+
action_history: list[dict[str, Any]] = Field(default_factory=list)
|
| 71 |
+
reward_history: list[dict[str, float]] = Field(default_factory=list)
|
| 72 |
+
visible_facts: dict[str, Any] = Field(default_factory=dict)
|
| 73 |
+
hidden_facts: dict[str, Any] = Field(default_factory=dict)
|
| 74 |
+
metrics: dict[str, Any] = Field(default_factory=dict)
|
| 75 |
+
anti_cheat_flags: list[str] = Field(default_factory=list)
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
# Backward-compatible aliases from the OpenEnv scaffold.
|
| 79 |
+
CybersecurityOwaspAction = CyberSecurityOWASPAction
|
| 80 |
+
CybersecurityOwaspObservation = CyberSecurityOWASPObservation
|
| 81 |
+
CybersecurityOwaspState = CyberSecurityOWASPState
|
openenv.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
spec_version: 1
|
| 2 |
+
name: CyberSecurity_OWASP
|
| 3 |
+
type: space
|
| 4 |
+
runtime: fastapi
|
| 5 |
+
app: server.app:app
|
| 6 |
+
port: 8000
|
| 7 |
+
|
policy_graph.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Policy graph generation for MVP authorization-repair scenarios."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import random
|
| 6 |
+
from dataclasses import dataclass
|
| 7 |
+
from typing import Any
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
@dataclass(frozen=True)
|
| 11 |
+
class CompiledPolicy:
|
| 12 |
+
public_hint: dict[str, Any]
|
| 13 |
+
hidden_facts: dict[str, Any]
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def build_invoice_policy(seed: int) -> CompiledPolicy:
|
| 17 |
+
"""Create a deterministic invoices policy graph with randomized IDs."""
|
| 18 |
+
|
| 19 |
+
rng = random.Random(seed)
|
| 20 |
+
suffix = rng.randint(1000, 9999)
|
| 21 |
+
tenant_a = f"tenant-{suffix}-a"
|
| 22 |
+
tenant_b = f"tenant-{suffix}-b"
|
| 23 |
+
owner = f"user-{suffix}-owner"
|
| 24 |
+
intruder = f"user-{suffix}-intruder"
|
| 25 |
+
admin = f"user-{suffix}-billing-admin"
|
| 26 |
+
owner_invoice = f"inv-{suffix}-owned"
|
| 27 |
+
other_invoice = f"inv-{suffix}-other"
|
| 28 |
+
foreign_invoice = f"inv-{suffix}-foreign"
|
| 29 |
+
|
| 30 |
+
users = {
|
| 31 |
+
owner: {"user_id": owner, "tenant_id": tenant_a, "roles": ["user"]},
|
| 32 |
+
intruder: {"user_id": intruder, "tenant_id": tenant_a, "roles": ["user"]},
|
| 33 |
+
admin: {"user_id": admin, "tenant_id": tenant_a, "roles": ["billing_admin"]},
|
| 34 |
+
f"user-{suffix}-outside": {
|
| 35 |
+
"user_id": f"user-{suffix}-outside",
|
| 36 |
+
"tenant_id": tenant_b,
|
| 37 |
+
"roles": ["user"],
|
| 38 |
+
},
|
| 39 |
+
}
|
| 40 |
+
invoices = {
|
| 41 |
+
owner_invoice: {
|
| 42 |
+
"invoice_id": owner_invoice,
|
| 43 |
+
"tenant_id": tenant_a,
|
| 44 |
+
"owner_user_id": owner,
|
| 45 |
+
"amount": 1200,
|
| 46 |
+
"status": "open",
|
| 47 |
+
},
|
| 48 |
+
other_invoice: {
|
| 49 |
+
"invoice_id": other_invoice,
|
| 50 |
+
"tenant_id": tenant_a,
|
| 51 |
+
"owner_user_id": intruder,
|
| 52 |
+
"amount": 410,
|
| 53 |
+
"status": "open",
|
| 54 |
+
},
|
| 55 |
+
foreign_invoice: {
|
| 56 |
+
"invoice_id": foreign_invoice,
|
| 57 |
+
"tenant_id": tenant_b,
|
| 58 |
+
"owner_user_id": f"user-{suffix}-outside",
|
| 59 |
+
"amount": 990,
|
| 60 |
+
"status": "open",
|
| 61 |
+
},
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
public_hint = {
|
| 65 |
+
"domain": "invoices",
|
| 66 |
+
"policy_rules": [
|
| 67 |
+
"Authenticated users may read invoices they own.",
|
| 68 |
+
"Billing admins may read invoices in their own tenant.",
|
| 69 |
+
"Users must not read another user's invoice unless they have a billing_admin role.",
|
| 70 |
+
"Cross-tenant invoice reads are forbidden.",
|
| 71 |
+
"GET /health is intentionally public.",
|
| 72 |
+
],
|
| 73 |
+
"users": {
|
| 74 |
+
alias: {
|
| 75 |
+
"user_id": value["user_id"],
|
| 76 |
+
"tenant_id": value["tenant_id"],
|
| 77 |
+
"roles": value["roles"],
|
| 78 |
+
}
|
| 79 |
+
for alias, value in {
|
| 80 |
+
"owner": users[owner],
|
| 81 |
+
"same_tenant_other_user": users[intruder],
|
| 82 |
+
"billing_admin": users[admin],
|
| 83 |
+
}.items()
|
| 84 |
+
},
|
| 85 |
+
"resources": {
|
| 86 |
+
"owned_invoice": owner_invoice,
|
| 87 |
+
"same_tenant_other_invoice": other_invoice,
|
| 88 |
+
"foreign_tenant_invoice": foreign_invoice,
|
| 89 |
+
},
|
| 90 |
+
"public_routes": [{"method": "GET", "path": "/health"}],
|
| 91 |
+
}
|
| 92 |
+
hidden_facts = {
|
| 93 |
+
"users": users,
|
| 94 |
+
"invoices": invoices,
|
| 95 |
+
"owner_user_id": owner,
|
| 96 |
+
"intruder_user_id": intruder,
|
| 97 |
+
"admin_user_id": admin,
|
| 98 |
+
"owner_invoice_id": owner_invoice,
|
| 99 |
+
"other_invoice_id": other_invoice,
|
| 100 |
+
"foreign_invoice_id": foreign_invoice,
|
| 101 |
+
"tenant_a": tenant_a,
|
| 102 |
+
"tenant_b": tenant_b,
|
| 103 |
+
"bug_family": "bola_idor",
|
| 104 |
+
}
|
| 105 |
+
return CompiledPolicy(public_hint=public_hint, hidden_facts=hidden_facts)
|
pyproject.toml
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the BSD-style license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
[build-system]
|
| 8 |
+
requires = ["setuptools>=45", "wheel"]
|
| 9 |
+
build-backend = "setuptools.build_meta"
|
| 10 |
+
|
| 11 |
+
[project]
|
| 12 |
+
name = "openenv-CyberSecurity_OWASP"
|
| 13 |
+
version = "0.1.0"
|
| 14 |
+
description = "Cybersecurity Owasp environment for OpenEnv"
|
| 15 |
+
requires-python = ">=3.10"
|
| 16 |
+
dependencies = [
|
| 17 |
+
# Core OpenEnv runtime (provides FastAPI server + HTTP client types)
|
| 18 |
+
# install from github
|
| 19 |
+
# "openenv-core[core] @ git+https://github.com/meta-pytorch/OpenEnv.git",
|
| 20 |
+
"openenv-core[core]>=0.2.2",
|
| 21 |
+
# Environment-specific dependencies
|
| 22 |
+
# Add all dependencies needed for your environment here
|
| 23 |
+
# Examples:
|
| 24 |
+
# "numpy>=1.19.0",
|
| 25 |
+
# "torch>=2.0.0",
|
| 26 |
+
# "gymnasium>=0.29.0",
|
| 27 |
+
# "openspiel>=1.0.0",
|
| 28 |
+
# "smolagents>=1.22.0,<2",
|
| 29 |
+
]
|
| 30 |
+
|
| 31 |
+
[project.optional-dependencies]
|
| 32 |
+
dev = [
|
| 33 |
+
"pytest>=8.0.0",
|
| 34 |
+
"pytest-cov>=4.0.0",
|
| 35 |
+
]
|
| 36 |
+
modal = [
|
| 37 |
+
"modal>=1.1.0",
|
| 38 |
+
]
|
| 39 |
+
|
| 40 |
+
[project.scripts]
|
| 41 |
+
# Server entry point - enables running via: uv run --project . server
|
| 42 |
+
# or: python -m CyberSecurity_OWASP.server.app
|
| 43 |
+
server = "CyberSecurity_OWASP.server.app:main"
|
| 44 |
+
|
| 45 |
+
[tool.setuptools]
|
| 46 |
+
include-package-data = true
|
| 47 |
+
packages = ["CyberSecurity_OWASP", "CyberSecurity_OWASP.server"]
|
| 48 |
+
package-dir = { "CyberSecurity_OWASP" = ".", "CyberSecurity_OWASP.server" = "server" }
|
rewards.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Reward computation for CyberSecurity_OWASP."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from .models import CyberSecurityOWASPAction, CyberSecurityOWASPState
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
REWARD_KEYS = (
|
| 9 |
+
"discovery",
|
| 10 |
+
"security",
|
| 11 |
+
"regression",
|
| 12 |
+
"public_routes",
|
| 13 |
+
"patch_quality",
|
| 14 |
+
"visible_tests",
|
| 15 |
+
"safety",
|
| 16 |
+
"anti_cheat",
|
| 17 |
+
"total",
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def empty_reward() -> dict[str, float]:
|
| 22 |
+
return {key: 0.0 for key in REWARD_KEYS}
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def compute_reward(
|
| 26 |
+
state: CyberSecurityOWASPState,
|
| 27 |
+
action: CyberSecurityOWASPAction,
|
| 28 |
+
verifier_result: dict,
|
| 29 |
+
) -> dict[str, float]:
|
| 30 |
+
reward = empty_reward()
|
| 31 |
+
if action.tool_name == "submit_finding":
|
| 32 |
+
finding = verifier_result.get("finding", {})
|
| 33 |
+
reward["discovery"] = (2.0 if finding.get("valid") else 0.0) + (
|
| 34 |
+
1.0 if finding.get("tied_to_policy") else 0.0
|
| 35 |
+
)
|
| 36 |
+
elif action.tool_name == "run_visible_tests":
|
| 37 |
+
visible = verifier_result.get("visible", {})
|
| 38 |
+
reward["visible_tests"] = 1.0 if visible.get("passed") else 0.0
|
| 39 |
+
elif action.tool_name == "submit_fix":
|
| 40 |
+
security = verifier_result.get("security", {})
|
| 41 |
+
regression = verifier_result.get("regression", {})
|
| 42 |
+
public_routes = verifier_result.get("public_routes", {})
|
| 43 |
+
quality = verifier_result.get("patch_quality", {})
|
| 44 |
+
visible = verifier_result.get("visible", {})
|
| 45 |
+
reward["security"] = 5.0 if security.get("passed") else -6.0
|
| 46 |
+
reward["regression"] = 3.0 if regression.get("passed") else -5.0
|
| 47 |
+
reward["public_routes"] = 1.0 if public_routes.get("passed") else -3.0
|
| 48 |
+
reward["patch_quality"] = 2.0 if quality.get("passed") else 0.0
|
| 49 |
+
reward["visible_tests"] = 1.0 if visible.get("passed") else -6.0
|
| 50 |
+
if quality.get("hardcoded"):
|
| 51 |
+
reward["anti_cheat"] -= 4.0
|
| 52 |
+
if quality.get("deny_all"):
|
| 53 |
+
reward["safety"] -= 5.0
|
| 54 |
+
|
| 55 |
+
flags = verifier_result.get("anti_cheat_flags", []) or []
|
| 56 |
+
if flags:
|
| 57 |
+
if "external_network_attempt" in flags:
|
| 58 |
+
reward["safety"] -= 8.0
|
| 59 |
+
if "blocked_file_probe" in flags:
|
| 60 |
+
reward["anti_cheat"] -= 8.0
|
| 61 |
+
if "hardcoded_hidden_identifier" in flags:
|
| 62 |
+
reward["anti_cheat"] -= 4.0
|
| 63 |
+
|
| 64 |
+
total = sum(value for key, value in reward.items() if key != "total")
|
| 65 |
+
reward["total"] = min(15.0, total) if total > 0 else total
|
| 66 |
+
return reward
|
safety.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Safety boundaries for local-only defensive AppSec episodes."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
FORBIDDEN_BEHAVIOR = (
|
| 7 |
+
"external network access",
|
| 8 |
+
"host filesystem reads",
|
| 9 |
+
"hidden test access",
|
| 10 |
+
"oracle access",
|
| 11 |
+
"credential extraction",
|
| 12 |
+
"persistence or evasion",
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def is_local_route(path: str) -> bool:
|
| 17 |
+
return path.startswith("/") and not path.startswith("//") and "://" not in path
|
scenario_compiler.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Deterministic scenario compiler for CyberSecurity_OWASP."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import tempfile
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from typing import Any
|
| 8 |
+
|
| 9 |
+
from .fixture_generator import visible_workspace_summary
|
| 10 |
+
from .policy_graph import build_invoice_policy
|
| 11 |
+
from .template_renderer import render_fastapi_basic
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def compile_scenario(seed: int, split: str = "train", difficulty: int = 0) -> dict[str, Any]:
|
| 15 |
+
"""Compile one isolated MVP authorization-repair scenario."""
|
| 16 |
+
|
| 17 |
+
compiled = build_invoice_policy(seed)
|
| 18 |
+
workspace = Path(tempfile.mkdtemp(prefix=f"cybersecurity_owasp_{split}_{seed}_"))
|
| 19 |
+
editable_files = render_fastapi_basic(workspace, compiled.public_hint, compiled.hidden_facts)
|
| 20 |
+
task_id = f"{split}-invoices-bola-{seed}"
|
| 21 |
+
hidden = dict(compiled.hidden_facts)
|
| 22 |
+
hidden.update(
|
| 23 |
+
{
|
| 24 |
+
"workspace": str(workspace),
|
| 25 |
+
"editable_files": editable_files,
|
| 26 |
+
"initial_file_hashes": {
|
| 27 |
+
path: (workspace / path).read_text(encoding="utf-8")
|
| 28 |
+
for path in editable_files
|
| 29 |
+
},
|
| 30 |
+
}
|
| 31 |
+
)
|
| 32 |
+
return {
|
| 33 |
+
"task_id": task_id,
|
| 34 |
+
"workspace": workspace,
|
| 35 |
+
"domain": "invoices",
|
| 36 |
+
"bug_family": "bola_idor",
|
| 37 |
+
"difficulty": difficulty,
|
| 38 |
+
"task_brief": (
|
| 39 |
+
"Inspect the generated invoices app and policy. Find the broken "
|
| 40 |
+
"authorization behavior, submit a finding with local evidence, patch "
|
| 41 |
+
"the app, preserve intended owner/admin/public behavior, then submit."
|
| 42 |
+
),
|
| 43 |
+
"public_hint": compiled.public_hint,
|
| 44 |
+
"workspace_summary": visible_workspace_summary(editable_files, compiled.public_hint),
|
| 45 |
+
"hidden_facts": hidden,
|
| 46 |
+
}
|
scripts/docker_build.sh
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
set -euo pipefail
|
| 3 |
+
docker build -t CyberSecurity_OWASP:latest -f server/Dockerfile .
|
scripts/docker_run.sh
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
set -euo pipefail
|
| 3 |
+
docker run --rm -p "${PORT:-8000}:8000" CyberSecurity_OWASP:latest
|
scripts/generate_scenarios.sh
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
set -euo pipefail
|
| 3 |
+
uv run python -c "from CyberSecurity_OWASP.scenario_compiler import compile_scenario; [compile_scenario(i) for i in range(3)]; print('generated 3 smoke scenarios')"
|
scripts/modal_ephemeral_train.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Ephemeral Modal Labs launcher for CyberSecurity_OWASP training smoke runs.
|
| 2 |
+
|
| 3 |
+
Run from the repo root:
|
| 4 |
+
|
| 5 |
+
modal run scripts/modal_ephemeral_train.py --mode smoke --episodes 4
|
| 6 |
+
|
| 7 |
+
This intentionally stays separate from ``training/train_grpo.py``. It packages
|
| 8 |
+
the local repo into a temporary Modal app and returns compact JSON artifacts to
|
| 9 |
+
the local process, so the run disappears when ``modal run`` exits.
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
import json
|
| 15 |
+
from datetime import datetime
|
| 16 |
+
from pathlib import Path
|
| 17 |
+
from typing import Any
|
| 18 |
+
|
| 19 |
+
import modal
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
APP_NAME = "CyberSecurity_OWASP-ephemeral-training"
|
| 23 |
+
REMOTE_PROJECT = "/root/CyberSecurity_OWASP"
|
| 24 |
+
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
| 25 |
+
|
| 26 |
+
app = modal.App(APP_NAME)
|
| 27 |
+
|
| 28 |
+
image = (
|
| 29 |
+
modal.Image.debian_slim(python_version="3.11")
|
| 30 |
+
.apt_install("git")
|
| 31 |
+
.add_local_dir(
|
| 32 |
+
PROJECT_ROOT,
|
| 33 |
+
remote_path=REMOTE_PROJECT,
|
| 34 |
+
copy=True,
|
| 35 |
+
ignore=[
|
| 36 |
+
".git",
|
| 37 |
+
".venv",
|
| 38 |
+
"__pycache__",
|
| 39 |
+
".pytest_cache",
|
| 40 |
+
"outputs",
|
| 41 |
+
"*.pyc",
|
| 42 |
+
],
|
| 43 |
+
)
|
| 44 |
+
.run_commands(f"pip install -e {REMOTE_PROJECT}")
|
| 45 |
+
.workdir(REMOTE_PROJECT)
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class NoopTrainer:
|
| 50 |
+
"""Deterministic placeholder policy for cheap Modal smoke runs."""
|
| 51 |
+
|
| 52 |
+
def generate_rollout_completions(self, prompts: list[str]) -> list[dict[str, Any]]:
|
| 53 |
+
return [
|
| 54 |
+
{
|
| 55 |
+
"text": '{"tool_name":"noop","arguments":{}}',
|
| 56 |
+
"prompt_ids": [],
|
| 57 |
+
"completion_ids": [],
|
| 58 |
+
"logprobs": [],
|
| 59 |
+
}
|
| 60 |
+
for _ in prompts
|
| 61 |
+
]
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
@app.function(image=image, timeout=60 * 30)
|
| 65 |
+
def run_ephemeral_smoke(episodes: int = 4, seed_start: int = 0) -> dict[str, Any]:
|
| 66 |
+
from CyberSecurity_OWASP.models import CyberSecurityOWASPAction
|
| 67 |
+
from CyberSecurity_OWASP.server.CyberSecurity_OWASP_environment import (
|
| 68 |
+
CybersecurityOwaspEnvironment,
|
| 69 |
+
)
|
| 70 |
+
from training.rollout import rollout_once
|
| 71 |
+
|
| 72 |
+
baseline = []
|
| 73 |
+
oracle = []
|
| 74 |
+
|
| 75 |
+
for offset in range(episodes):
|
| 76 |
+
seed = seed_start + offset
|
| 77 |
+
|
| 78 |
+
baseline_env = CybersecurityOwaspEnvironment()
|
| 79 |
+
baseline_env.reset(seed=seed, split="validation")
|
| 80 |
+
baseline.append(rollout_once(NoopTrainer(), baseline_env, max_steps=5))
|
| 81 |
+
|
| 82 |
+
oracle_env = CybersecurityOwaspEnvironment()
|
| 83 |
+
oracle_env.reset(seed=seed, split="validation")
|
| 84 |
+
hidden = oracle_env.state.hidden_facts
|
| 85 |
+
oracle_env.step(
|
| 86 |
+
CyberSecurityOWASPAction(
|
| 87 |
+
tool_name="submit_finding",
|
| 88 |
+
arguments={
|
| 89 |
+
"summary": "BOLA/IDOR authorization bug in invoice read route.",
|
| 90 |
+
"evidence": (
|
| 91 |
+
f"user {hidden['owner_user_id']} can request invoice "
|
| 92 |
+
f"{hidden['other_invoice_id']} despite the owner/admin policy"
|
| 93 |
+
),
|
| 94 |
+
"policy_rule": "Only owner or billing_admin in same tenant may read invoices.",
|
| 95 |
+
},
|
| 96 |
+
)
|
| 97 |
+
)
|
| 98 |
+
source = (
|
| 99 |
+
Path(hidden["workspace"]) / "app/routes/invoices.py"
|
| 100 |
+
).read_text(encoding="utf-8")
|
| 101 |
+
fixed = source.replace(
|
| 102 |
+
" # BUG: this only checks that the caller is authenticated. It forgets the\n"
|
| 103 |
+
" # owner/admin and tenant policy checks required by the policy graph.\n"
|
| 104 |
+
" return {\"status\": 200, \"body\": invoice}\n",
|
| 105 |
+
" if invoice[\"tenant_id\"] != actor[\"tenant_id\"]:\n"
|
| 106 |
+
" return {\"status\": 403, \"body\": {\"detail\": \"forbidden\"}}\n"
|
| 107 |
+
" if invoice[\"owner_user_id\"] != actor[\"user_id\"] and not is_billing_admin(actor):\n"
|
| 108 |
+
" return {\"status\": 403, \"body\": {\"detail\": \"forbidden\"}}\n"
|
| 109 |
+
" return {\"status\": 200, \"body\": invoice}\n",
|
| 110 |
+
)
|
| 111 |
+
oracle_env.step(
|
| 112 |
+
CyberSecurityOWASPAction(
|
| 113 |
+
tool_name="patch_file",
|
| 114 |
+
arguments={"path": "app/routes/invoices.py", "content": fixed},
|
| 115 |
+
)
|
| 116 |
+
)
|
| 117 |
+
oracle_env.step(CyberSecurityOWASPAction(tool_name="run_visible_tests"))
|
| 118 |
+
final = oracle_env.step(CyberSecurityOWASPAction(tool_name="submit_fix"))
|
| 119 |
+
oracle.append(
|
| 120 |
+
{
|
| 121 |
+
"seed": seed,
|
| 122 |
+
"success": oracle_env.state.success,
|
| 123 |
+
"reward_total": final.reward_breakdown.get("total", 0.0),
|
| 124 |
+
"reward_breakdown": final.reward_breakdown,
|
| 125 |
+
}
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
def mean(items: list[dict[str, Any]], key: str) -> float:
|
| 129 |
+
return sum(float(item.get(key, 0.0)) for item in items) / max(1, len(items))
|
| 130 |
+
|
| 131 |
+
return {
|
| 132 |
+
"run_name": f"{APP_NAME}-{datetime.utcnow().strftime('%Y%m%d-%H%M%S')}",
|
| 133 |
+
"mode": "smoke",
|
| 134 |
+
"episodes": episodes,
|
| 135 |
+
"seed_start": seed_start,
|
| 136 |
+
"baseline_mean_reward": mean(baseline, "reward_total"),
|
| 137 |
+
"oracle_mean_reward": mean(oracle, "reward_total"),
|
| 138 |
+
"oracle_success_rate": mean(oracle, "success"),
|
| 139 |
+
"baseline": baseline,
|
| 140 |
+
"oracle": oracle,
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
@app.function(image=image, timeout=60 * 10)
|
| 145 |
+
def run_grpo_config_check() -> str:
|
| 146 |
+
from training.train_grpo import build_grpo_config
|
| 147 |
+
|
| 148 |
+
return str(build_grpo_config())
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
@app.local_entrypoint()
|
| 152 |
+
def main(mode: str = "smoke", episodes: int = 4, seed_start: int = 0) -> None:
|
| 153 |
+
if mode == "smoke":
|
| 154 |
+
result = run_ephemeral_smoke.remote(episodes=episodes, seed_start=seed_start)
|
| 155 |
+
output_dir = PROJECT_ROOT / "outputs" / "rollouts"
|
| 156 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 157 |
+
output_path = output_dir / f"{result['run_name']}.json"
|
| 158 |
+
output_path.write_text(json.dumps(result, indent=2, sort_keys=True), encoding="utf-8")
|
| 159 |
+
print(json.dumps({"saved": str(output_path), **result}, indent=2, sort_keys=True))
|
| 160 |
+
elif mode == "grpo-config":
|
| 161 |
+
print(run_grpo_config_check.remote())
|
| 162 |
+
else:
|
| 163 |
+
raise ValueError("mode must be 'smoke' or 'grpo-config'")
|
scripts/modal_run_ephemeral.sh
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
set -euo pipefail
|
| 3 |
+
modal run scripts/modal_ephemeral_train.py --mode "${MODE:-smoke}" --episodes "${EPISODES:-4}" --seed-start "${SEED_START:-0}"
|
scripts/push_space.sh
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
set -euo pipefail
|
| 3 |
+
openenv push --repo-id "${HF_REPO_ID:?set HF_REPO_ID, e.g. username/CyberSecurity_OWASP}"
|
scripts/run_local.sh
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
set -euo pipefail
|
| 3 |
+
uv run server --port "${PORT:-8000}"
|
scripts/smoke_test.sh
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
set -euo pipefail
|
| 3 |
+
uv run pytest tests/test_models.py tests/test_reset_step_state.py
|
server/CyberSecurity_OWASP_environment.py
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""CyberSecurity_OWASP OpenEnv environment implementation."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import json
|
| 6 |
+
import shutil
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from typing import Any
|
| 9 |
+
from uuid import uuid4
|
| 10 |
+
|
| 11 |
+
from openenv.core.env_server.interfaces import Environment
|
| 12 |
+
|
| 13 |
+
try:
|
| 14 |
+
from ..models import (
|
| 15 |
+
CyberSecurityOWASPAction,
|
| 16 |
+
CyberSecurityOWASPObservation,
|
| 17 |
+
CyberSecurityOWASPState,
|
| 18 |
+
)
|
| 19 |
+
from ..scenario_compiler import compile_scenario
|
| 20 |
+
from ..safety import is_local_route
|
| 21 |
+
from ..validators import detect_cheating, is_path_allowed, simulate_request
|
| 22 |
+
from .reward_engine import evaluate_action
|
| 23 |
+
except ImportError: # pragma: no cover
|
| 24 |
+
from models import CyberSecurityOWASPAction, CyberSecurityOWASPObservation, CyberSecurityOWASPState
|
| 25 |
+
from scenario_compiler import compile_scenario
|
| 26 |
+
from safety import is_local_route
|
| 27 |
+
from validators import detect_cheating, is_path_allowed, simulate_request
|
| 28 |
+
from server.reward_engine import evaluate_action
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
ALLOWED_TOOLS = {
|
| 32 |
+
"discover": {
|
| 33 |
+
"inspect_policy_graph",
|
| 34 |
+
"list_routes",
|
| 35 |
+
"read_openapi",
|
| 36 |
+
"read_file",
|
| 37 |
+
"search_code",
|
| 38 |
+
"send_local_request",
|
| 39 |
+
"compare_identities",
|
| 40 |
+
"submit_finding",
|
| 41 |
+
"noop",
|
| 42 |
+
},
|
| 43 |
+
"patch": {
|
| 44 |
+
"read_file",
|
| 45 |
+
"search_code",
|
| 46 |
+
"patch_file",
|
| 47 |
+
"run_visible_tests",
|
| 48 |
+
"send_local_request",
|
| 49 |
+
"submit_fix",
|
| 50 |
+
"noop",
|
| 51 |
+
},
|
| 52 |
+
"done": set(),
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
class CybersecurityOwaspEnvironment(
|
| 57 |
+
Environment[CyberSecurityOWASPAction, CyberSecurityOWASPObservation, CyberSecurityOWASPState]
|
| 58 |
+
):
|
| 59 |
+
"""Single-agent defensive authorization-repair environment."""
|
| 60 |
+
|
| 61 |
+
SUPPORTS_CONCURRENT_SESSIONS = True
|
| 62 |
+
|
| 63 |
+
def __init__(self):
|
| 64 |
+
super().__init__()
|
| 65 |
+
self._state = CyberSecurityOWASPState(episode_id=str(uuid4()))
|
| 66 |
+
self._task_brief = ""
|
| 67 |
+
self._visible_policy_hint: dict[str, Any] = {}
|
| 68 |
+
self._workspace_summary: dict[str, Any] = {}
|
| 69 |
+
self._last_done_observation: CyberSecurityOWASPObservation | None = None
|
| 70 |
+
|
| 71 |
+
def reset(
|
| 72 |
+
self,
|
| 73 |
+
seed: int | None = None,
|
| 74 |
+
episode_id: str | None = None,
|
| 75 |
+
split: str = "train",
|
| 76 |
+
difficulty: int = 0,
|
| 77 |
+
**_: Any,
|
| 78 |
+
) -> CyberSecurityOWASPObservation:
|
| 79 |
+
self.close()
|
| 80 |
+
actual_seed = int(seed if seed is not None else 0)
|
| 81 |
+
scenario = compile_scenario(actual_seed, split=split, difficulty=difficulty)
|
| 82 |
+
self._state = CyberSecurityOWASPState(
|
| 83 |
+
episode_id=episode_id or str(uuid4()),
|
| 84 |
+
task_id=scenario["task_id"],
|
| 85 |
+
seed=actual_seed,
|
| 86 |
+
split=split,
|
| 87 |
+
difficulty=difficulty,
|
| 88 |
+
domain=scenario["domain"],
|
| 89 |
+
bug_family=scenario["bug_family"],
|
| 90 |
+
phase="discover",
|
| 91 |
+
step_count=0,
|
| 92 |
+
max_steps=40,
|
| 93 |
+
done=False,
|
| 94 |
+
success=False,
|
| 95 |
+
visible_facts={"workspace_summary": scenario["workspace_summary"]},
|
| 96 |
+
hidden_facts=scenario["hidden_facts"],
|
| 97 |
+
metrics={"reset_count": 1},
|
| 98 |
+
)
|
| 99 |
+
self._task_brief = scenario["task_brief"]
|
| 100 |
+
self._visible_policy_hint = scenario["public_hint"]
|
| 101 |
+
self._workspace_summary = scenario["workspace_summary"]
|
| 102 |
+
self._last_done_observation = None
|
| 103 |
+
return self._observation("Scenario ready. Start in discover phase.", reward=0.0)
|
| 104 |
+
|
| 105 |
+
def step(
|
| 106 |
+
self,
|
| 107 |
+
action: CyberSecurityOWASPAction,
|
| 108 |
+
timeout_s: float | None = None,
|
| 109 |
+
**_: Any,
|
| 110 |
+
) -> CyberSecurityOWASPObservation:
|
| 111 |
+
if self._state.done:
|
| 112 |
+
return self._last_done_observation or self._observation(
|
| 113 |
+
"Episode is already done.", reward=0.0, done_reason=self._state.failure_reason
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
anti_cheat_flags = detect_cheating(self._state, action)
|
| 117 |
+
for flag in anti_cheat_flags:
|
| 118 |
+
if flag not in self._state.anti_cheat_flags:
|
| 119 |
+
self._state.anti_cheat_flags.append(flag)
|
| 120 |
+
|
| 121 |
+
self._state.step_count += 1
|
| 122 |
+
self._state.action_history.append(
|
| 123 |
+
{"tool_name": action.tool_name, "arguments": action.arguments}
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
if action.tool_name not in ALLOWED_TOOLS[self._state.phase]:
|
| 127 |
+
verifier, reward = evaluate_action(self._state, action, anti_cheat_flags)
|
| 128 |
+
return self._finish_step(
|
| 129 |
+
"Action is not allowed in the current phase.",
|
| 130 |
+
reward,
|
| 131 |
+
valid=False,
|
| 132 |
+
error=f"{action.tool_name} is not allowed during {self._state.phase}",
|
| 133 |
+
verifier=verifier,
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
try:
|
| 137 |
+
result, verifier, reward, visible_tests = self._execute(action, anti_cheat_flags)
|
| 138 |
+
return self._finish_step(
|
| 139 |
+
result,
|
| 140 |
+
reward,
|
| 141 |
+
valid=True,
|
| 142 |
+
verifier=verifier,
|
| 143 |
+
visible_test_result=visible_tests,
|
| 144 |
+
)
|
| 145 |
+
except Exception as exc: # keep malformed agent actions from crashing the server
|
| 146 |
+
verifier, reward = evaluate_action(self._state, action, anti_cheat_flags)
|
| 147 |
+
return self._finish_step(
|
| 148 |
+
"Tool execution failed.",
|
| 149 |
+
reward,
|
| 150 |
+
valid=False,
|
| 151 |
+
error=str(exc),
|
| 152 |
+
verifier=verifier,
|
| 153 |
+
)
|
| 154 |
+
|
| 155 |
+
@property
|
| 156 |
+
def state(self) -> CyberSecurityOWASPState:
|
| 157 |
+
return self._state
|
| 158 |
+
|
| 159 |
+
def close(self) -> None:
|
| 160 |
+
workspace = self._state.hidden_facts.get("workspace")
|
| 161 |
+
if workspace:
|
| 162 |
+
shutil.rmtree(workspace, ignore_errors=True)
|
| 163 |
+
|
| 164 |
+
def _execute(
|
| 165 |
+
self, action: CyberSecurityOWASPAction, anti_cheat_flags: list[str]
|
| 166 |
+
) -> tuple[str, dict, dict[str, float], str | None]:
|
| 167 |
+
verifier: dict = {"anti_cheat_flags": anti_cheat_flags}
|
| 168 |
+
reward = {key: 0.0 for key in (
|
| 169 |
+
"discovery",
|
| 170 |
+
"security",
|
| 171 |
+
"regression",
|
| 172 |
+
"public_routes",
|
| 173 |
+
"patch_quality",
|
| 174 |
+
"visible_tests",
|
| 175 |
+
"safety",
|
| 176 |
+
"anti_cheat",
|
| 177 |
+
"total",
|
| 178 |
+
)}
|
| 179 |
+
visible_tests = None
|
| 180 |
+
args = action.arguments or {}
|
| 181 |
+
|
| 182 |
+
if action.tool_name == "noop":
|
| 183 |
+
return "No operation.", verifier, reward, None
|
| 184 |
+
if action.tool_name == "inspect_policy_graph":
|
| 185 |
+
return json.dumps(self._visible_policy_hint, indent=2, sort_keys=True), verifier, reward, None
|
| 186 |
+
if action.tool_name == "list_routes":
|
| 187 |
+
return json.dumps(self._workspace_summary["routes"], indent=2), verifier, reward, None
|
| 188 |
+
if action.tool_name == "read_openapi":
|
| 189 |
+
return json.dumps(
|
| 190 |
+
{
|
| 191 |
+
"openapi": "3.1.0",
|
| 192 |
+
"info": {"title": "Generated invoices app", "version": "0.1.0"},
|
| 193 |
+
"paths": {
|
| 194 |
+
"/health": {"get": {"x-public": True}},
|
| 195 |
+
"/invoices/{invoice_id}": {"get": {"x-public": False}},
|
| 196 |
+
},
|
| 197 |
+
},
|
| 198 |
+
indent=2,
|
| 199 |
+
), verifier, reward, None
|
| 200 |
+
if action.tool_name == "read_file":
|
| 201 |
+
path = self._resolve_path(str(args.get("path", "")))
|
| 202 |
+
return path.read_text(encoding="utf-8"), verifier, reward, None
|
| 203 |
+
if action.tool_name == "search_code":
|
| 204 |
+
return self._search_code(str(args.get("query", ""))), verifier, reward, None
|
| 205 |
+
if action.tool_name == "send_local_request":
|
| 206 |
+
if not is_local_route(str(args.get("path", ""))):
|
| 207 |
+
raise ValueError("send_local_request only accepts local route paths")
|
| 208 |
+
response = simulate_request(
|
| 209 |
+
self._state,
|
| 210 |
+
str(args.get("method", "GET")),
|
| 211 |
+
str(args.get("path", "")),
|
| 212 |
+
args.get("user_id"),
|
| 213 |
+
)
|
| 214 |
+
return json.dumps(response, indent=2, sort_keys=True), verifier, reward, None
|
| 215 |
+
if action.tool_name == "compare_identities":
|
| 216 |
+
path = str(args.get("path", ""))
|
| 217 |
+
first = str(args.get("first_user_id", ""))
|
| 218 |
+
second = str(args.get("second_user_id", ""))
|
| 219 |
+
if not is_local_route(path):
|
| 220 |
+
raise ValueError("compare_identities only accepts local route paths")
|
| 221 |
+
response = {
|
| 222 |
+
"first": simulate_request(self._state, str(args.get("method", "GET")), path, first),
|
| 223 |
+
"second": simulate_request(self._state, str(args.get("method", "GET")), path, second),
|
| 224 |
+
}
|
| 225 |
+
return json.dumps(response, indent=2, sort_keys=True), verifier, reward, None
|
| 226 |
+
if action.tool_name == "submit_finding":
|
| 227 |
+
verifier, reward = evaluate_action(self._state, action, anti_cheat_flags)
|
| 228 |
+
if verifier.get("finding", {}).get("valid"):
|
| 229 |
+
self._state.finding_submitted = True
|
| 230 |
+
self._state.phase = "patch"
|
| 231 |
+
return "Finding accepted. Patch phase unlocked.", verifier, reward, None
|
| 232 |
+
return "Finding was not specific enough to unlock patching.", verifier, reward, None
|
| 233 |
+
if action.tool_name == "patch_file":
|
| 234 |
+
path = self._resolve_path(str(args.get("path", "")), write=True)
|
| 235 |
+
if "content" in args:
|
| 236 |
+
path.write_text(str(args["content"]), encoding="utf-8")
|
| 237 |
+
else:
|
| 238 |
+
self._apply_unified_diff(path, str(args.get("diff", "")))
|
| 239 |
+
return f"Patched {args.get('path')}.", verifier, reward, None
|
| 240 |
+
if action.tool_name == "run_visible_tests":
|
| 241 |
+
verifier, reward = evaluate_action(self._state, action, anti_cheat_flags)
|
| 242 |
+
visible_tests = json.dumps(verifier.get("visible", {}), indent=2, sort_keys=True)
|
| 243 |
+
return visible_tests, verifier, reward, visible_tests
|
| 244 |
+
if action.tool_name == "submit_fix":
|
| 245 |
+
verifier, reward = evaluate_action(self._state, action, anti_cheat_flags)
|
| 246 |
+
self._state.patch_submitted = True
|
| 247 |
+
security = verifier.get("security", {}).get("passed", False)
|
| 248 |
+
regression = verifier.get("regression", {}).get("passed", False)
|
| 249 |
+
public = verifier.get("public_routes", {}).get("passed", False)
|
| 250 |
+
quality = verifier.get("patch_quality", {}).get("passed", False)
|
| 251 |
+
self._state.success = bool(security and regression and public and quality)
|
| 252 |
+
self._state.done = True
|
| 253 |
+
self._state.phase = "done"
|
| 254 |
+
self._state.failure_reason = None if self._state.success else "hidden_verifier_failed"
|
| 255 |
+
return json.dumps(verifier, indent=2, sort_keys=True), verifier, reward, None
|
| 256 |
+
raise ValueError(f"Unhandled tool {action.tool_name}")
|
| 257 |
+
|
| 258 |
+
def _finish_step(
|
| 259 |
+
self,
|
| 260 |
+
message: str,
|
| 261 |
+
reward: dict[str, float],
|
| 262 |
+
*,
|
| 263 |
+
valid: bool,
|
| 264 |
+
error: str | None = None,
|
| 265 |
+
verifier: dict | None = None,
|
| 266 |
+
visible_test_result: str | None = None,
|
| 267 |
+
) -> CyberSecurityOWASPObservation:
|
| 268 |
+
self._state.last_reward = float(reward.get("total", 0.0))
|
| 269 |
+
self._state.accumulated_reward += self._state.last_reward
|
| 270 |
+
self._state.reward_history.append(reward)
|
| 271 |
+
if self._state.step_count >= self._state.max_steps and not self._state.done:
|
| 272 |
+
self._state.done = True
|
| 273 |
+
self._state.phase = "done"
|
| 274 |
+
self._state.failure_reason = "max_steps_exceeded"
|
| 275 |
+
obs = self._observation(
|
| 276 |
+
message,
|
| 277 |
+
reward=self._state.last_reward,
|
| 278 |
+
valid=valid,
|
| 279 |
+
error=error,
|
| 280 |
+
reward_breakdown=reward,
|
| 281 |
+
visible_test_result=visible_test_result,
|
| 282 |
+
done_reason=self._state.failure_reason,
|
| 283 |
+
)
|
| 284 |
+
if self._state.done:
|
| 285 |
+
self._last_done_observation = obs
|
| 286 |
+
return obs
|
| 287 |
+
|
| 288 |
+
def _observation(
|
| 289 |
+
self,
|
| 290 |
+
message: str,
|
| 291 |
+
*,
|
| 292 |
+
reward: float,
|
| 293 |
+
valid: bool = True,
|
| 294 |
+
error: str | None = None,
|
| 295 |
+
reward_breakdown: dict[str, float] | None = None,
|
| 296 |
+
visible_test_result: str | None = None,
|
| 297 |
+
done_reason: str | None = None,
|
| 298 |
+
) -> CyberSecurityOWASPObservation:
|
| 299 |
+
return CyberSecurityOWASPObservation(
|
| 300 |
+
phase=self._state.phase,
|
| 301 |
+
message=message,
|
| 302 |
+
task_brief=self._task_brief,
|
| 303 |
+
visible_policy_hint=self._visible_policy_hint,
|
| 304 |
+
workspace_summary=self._workspace_summary,
|
| 305 |
+
available_actions=sorted(ALLOWED_TOOLS[self._state.phase]),
|
| 306 |
+
last_tool_result=message,
|
| 307 |
+
last_action_valid=valid,
|
| 308 |
+
last_action_error=error,
|
| 309 |
+
visible_test_result=visible_test_result,
|
| 310 |
+
reward_breakdown=reward_breakdown or {},
|
| 311 |
+
done_reason=done_reason,
|
| 312 |
+
done=self._state.done,
|
| 313 |
+
reward=reward,
|
| 314 |
+
metadata={"episode_id": self._state.episode_id, "step_count": self._state.step_count},
|
| 315 |
+
)
|
| 316 |
+
|
| 317 |
+
def _resolve_path(self, path: str, *, write: bool = False) -> Path:
|
| 318 |
+
allowed, normalized_or_error = is_path_allowed(self._state, path, write=write)
|
| 319 |
+
if not allowed:
|
| 320 |
+
raise ValueError(normalized_or_error)
|
| 321 |
+
return Path(str(self._state.hidden_facts["workspace"])) / normalized_or_error
|
| 322 |
+
|
| 323 |
+
def _search_code(self, query: str) -> str:
|
| 324 |
+
if not query:
|
| 325 |
+
raise ValueError("query is required")
|
| 326 |
+
results: list[str] = []
|
| 327 |
+
workspace = Path(str(self._state.hidden_facts["workspace"]))
|
| 328 |
+
for rel in self._state.hidden_facts.get("editable_files", []):
|
| 329 |
+
path = workspace / rel
|
| 330 |
+
text = path.read_text(encoding="utf-8")
|
| 331 |
+
for idx, line in enumerate(text.splitlines(), start=1):
|
| 332 |
+
if query.lower() in line.lower():
|
| 333 |
+
results.append(f"{rel}:{idx}: {line}")
|
| 334 |
+
return "\n".join(results) or "No matches."
|
| 335 |
+
|
| 336 |
+
def _apply_unified_diff(self, path: Path, diff: str) -> None:
|
| 337 |
+
if not diff.strip():
|
| 338 |
+
raise ValueError("diff or content is required")
|
| 339 |
+
original = path.read_text(encoding="utf-8").splitlines(True)
|
| 340 |
+
output: list[str] = []
|
| 341 |
+
old_index = 0
|
| 342 |
+
lines = diff.splitlines(True)
|
| 343 |
+
i = 0
|
| 344 |
+
while i < len(lines):
|
| 345 |
+
line = lines[i]
|
| 346 |
+
if not line.startswith("@@"):
|
| 347 |
+
i += 1
|
| 348 |
+
continue
|
| 349 |
+
old_start = int(line.split()[1].split(",")[0][1:])
|
| 350 |
+
output.extend(original[old_index : old_start - 1])
|
| 351 |
+
old_index = old_start - 1
|
| 352 |
+
i += 1
|
| 353 |
+
while i < len(lines) and not lines[i].startswith("@@"):
|
| 354 |
+
hunk_line = lines[i]
|
| 355 |
+
if hunk_line.startswith(" "):
|
| 356 |
+
output.append(original[old_index])
|
| 357 |
+
old_index += 1
|
| 358 |
+
elif hunk_line.startswith("-"):
|
| 359 |
+
old_index += 1
|
| 360 |
+
elif hunk_line.startswith("+"):
|
| 361 |
+
output.append(hunk_line[1:])
|
| 362 |
+
elif hunk_line.startswith("\\"):
|
| 363 |
+
pass
|
| 364 |
+
i += 1
|
| 365 |
+
output.extend(original[old_index:])
|
| 366 |
+
path.write_text("".join(output), encoding="utf-8")
|
server/Dockerfile
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the BSD-style license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
# Multi-stage build using openenv-base
|
| 8 |
+
# This Dockerfile is flexible and works for both:
|
| 9 |
+
# - In-repo environments (with local OpenEnv sources)
|
| 10 |
+
# - Standalone environments (with openenv from PyPI/Git)
|
| 11 |
+
# The build script (openenv build) handles context detection and sets appropriate build args.
|
| 12 |
+
|
| 13 |
+
ARG BASE_IMAGE=ghcr.io/meta-pytorch/openenv-base:latest
|
| 14 |
+
FROM ${BASE_IMAGE} AS builder
|
| 15 |
+
|
| 16 |
+
WORKDIR /app
|
| 17 |
+
|
| 18 |
+
# Ensure git is available (required for installing dependencies from VCS)
|
| 19 |
+
RUN apt-get update && \
|
| 20 |
+
apt-get install -y --no-install-recommends git && \
|
| 21 |
+
rm -rf /var/lib/apt/lists/*
|
| 22 |
+
|
| 23 |
+
# Build argument to control whether we're building standalone or in-repo
|
| 24 |
+
ARG BUILD_MODE=in-repo
|
| 25 |
+
ARG ENV_NAME=CyberSecurity_OWASP
|
| 26 |
+
|
| 27 |
+
# Copy environment code (always at root of build context)
|
| 28 |
+
COPY . /app/env
|
| 29 |
+
|
| 30 |
+
# For in-repo builds, openenv is already vendored in the build context
|
| 31 |
+
# For standalone builds, openenv will be installed via pyproject.toml
|
| 32 |
+
WORKDIR /app/env
|
| 33 |
+
|
| 34 |
+
# Ensure uv is available (for local builds where base image lacks it)
|
| 35 |
+
RUN if ! command -v uv >/dev/null 2>&1; then \
|
| 36 |
+
curl -LsSf https://astral.sh/uv/install.sh | sh && \
|
| 37 |
+
mv /root/.local/bin/uv /usr/local/bin/uv && \
|
| 38 |
+
mv /root/.local/bin/uvx /usr/local/bin/uvx; \
|
| 39 |
+
fi
|
| 40 |
+
|
| 41 |
+
# Install dependencies using uv sync
|
| 42 |
+
# If uv.lock exists, use it; otherwise resolve on the fly
|
| 43 |
+
RUN --mount=type=cache,target=/root/.cache/uv \
|
| 44 |
+
if [ -f uv.lock ]; then \
|
| 45 |
+
uv sync --frozen --no-install-project --no-editable; \
|
| 46 |
+
else \
|
| 47 |
+
uv sync --no-install-project --no-editable; \
|
| 48 |
+
fi
|
| 49 |
+
|
| 50 |
+
RUN --mount=type=cache,target=/root/.cache/uv \
|
| 51 |
+
if [ -f uv.lock ]; then \
|
| 52 |
+
uv sync --frozen --no-editable; \
|
| 53 |
+
else \
|
| 54 |
+
uv sync --no-editable; \
|
| 55 |
+
fi
|
| 56 |
+
|
| 57 |
+
# Final runtime stage
|
| 58 |
+
FROM ${BASE_IMAGE}
|
| 59 |
+
|
| 60 |
+
WORKDIR /app
|
| 61 |
+
|
| 62 |
+
# Copy the virtual environment from builder
|
| 63 |
+
COPY --from=builder /app/env/.venv /app/.venv
|
| 64 |
+
|
| 65 |
+
# Copy the environment code
|
| 66 |
+
COPY --from=builder /app/env /app/env
|
| 67 |
+
|
| 68 |
+
# Set PATH to use the virtual environment
|
| 69 |
+
ENV PATH="/app/.venv/bin:$PATH"
|
| 70 |
+
|
| 71 |
+
# Set PYTHONPATH so imports work correctly
|
| 72 |
+
ENV PYTHONPATH="/app/env:$PYTHONPATH"
|
| 73 |
+
|
| 74 |
+
# Health check
|
| 75 |
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
| 76 |
+
CMD curl -f http://localhost:8000/health || exit 1
|
| 77 |
+
|
| 78 |
+
# Run the FastAPI server
|
| 79 |
+
# The module path is constructed to work with the /app/env structure
|
| 80 |
+
CMD ["sh", "-c", "cd /app/env && uvicorn server.app:app --host 0.0.0.0 --port 8000"]
|
server/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the BSD-style license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
"""Cybersecurity Owasp environment server components."""
|
| 8 |
+
|
| 9 |
+
from .CyberSecurity_OWASP_environment import CybersecurityOwaspEnvironment
|
| 10 |
+
|
| 11 |
+
__all__ = ["CybersecurityOwaspEnvironment"]
|
server/app.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the BSD-style license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
"""FastAPI application for the CyberSecurity_OWASP OpenEnv server."""
|
| 8 |
+
|
| 9 |
+
try:
|
| 10 |
+
from openenv.core.env_server.http_server import create_app
|
| 11 |
+
except Exception as e: # pragma: no cover
|
| 12 |
+
raise ImportError(
|
| 13 |
+
"openenv is required for the web interface. Install dependencies with '\n uv sync\n'"
|
| 14 |
+
) from e
|
| 15 |
+
|
| 16 |
+
try:
|
| 17 |
+
from ..models import CyberSecurityOWASPAction, CyberSecurityOWASPObservation
|
| 18 |
+
from .CyberSecurity_OWASP_environment import CybersecurityOwaspEnvironment
|
| 19 |
+
except ModuleNotFoundError:
|
| 20 |
+
from models import CyberSecurityOWASPAction, CyberSecurityOWASPObservation
|
| 21 |
+
from server.CyberSecurity_OWASP_environment import CybersecurityOwaspEnvironment
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
# Create the app with web interface and README integration
|
| 25 |
+
app = create_app(
|
| 26 |
+
CybersecurityOwaspEnvironment,
|
| 27 |
+
CyberSecurityOWASPAction,
|
| 28 |
+
CyberSecurityOWASPObservation,
|
| 29 |
+
env_name="CyberSecurity_OWASP",
|
| 30 |
+
max_concurrent_envs=4,
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def main(host: str = "0.0.0.0", port: int = 8000):
|
| 35 |
+
"""
|
| 36 |
+
Entry point for direct execution via uv run or python -m.
|
| 37 |
+
|
| 38 |
+
This function enables running the server without Docker:
|
| 39 |
+
uv run --project . server
|
| 40 |
+
uv run --project . server --port 8001
|
| 41 |
+
python -m CyberSecurity_OWASP.server.app
|
| 42 |
+
|
| 43 |
+
Args:
|
| 44 |
+
host: Host address to bind to (default: "0.0.0.0")
|
| 45 |
+
port: Port number to listen on (default: 8000)
|
| 46 |
+
|
| 47 |
+
For production deployments, consider using uvicorn directly with
|
| 48 |
+
multiple workers:
|
| 49 |
+
uvicorn CyberSecurity_OWASP.server.app:app --workers 4
|
| 50 |
+
"""
|
| 51 |
+
import uvicorn
|
| 52 |
+
|
| 53 |
+
uvicorn.run(app, host=host, port=port)
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
if __name__ == "__main__":
|
| 57 |
+
import argparse
|
| 58 |
+
|
| 59 |
+
parser = argparse.ArgumentParser()
|
| 60 |
+
parser.add_argument("--port", type=int, default=8000)
|
| 61 |
+
args = parser.parse_args()
|
| 62 |
+
main(port=args.port)
|
server/requirements.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
openenv[core]>=0.2.0
|
| 2 |
+
fastapi>=0.115.0
|
| 3 |
+
uvicorn>=0.24.0
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
|
server/reward_engine.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Server-side verifier aggregation for terminal scoring."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
try:
|
| 6 |
+
from ..models import CyberSecurityOWASPAction, CyberSecurityOWASPState
|
| 7 |
+
from ..rewards import compute_reward
|
| 8 |
+
from ..validators import (
|
| 9 |
+
patch_quality,
|
| 10 |
+
run_hidden_regression_tests,
|
| 11 |
+
run_hidden_security_tests,
|
| 12 |
+
run_public_route_tests,
|
| 13 |
+
run_visible_tests,
|
| 14 |
+
verify_finding,
|
| 15 |
+
)
|
| 16 |
+
except ImportError: # pragma: no cover
|
| 17 |
+
from models import CyberSecurityOWASPAction, CyberSecurityOWASPState
|
| 18 |
+
from rewards import compute_reward
|
| 19 |
+
from validators import (
|
| 20 |
+
patch_quality,
|
| 21 |
+
run_hidden_regression_tests,
|
| 22 |
+
run_hidden_security_tests,
|
| 23 |
+
run_public_route_tests,
|
| 24 |
+
run_visible_tests,
|
| 25 |
+
verify_finding,
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def evaluate_action(
|
| 30 |
+
state: CyberSecurityOWASPState,
|
| 31 |
+
action: CyberSecurityOWASPAction,
|
| 32 |
+
anti_cheat_flags: list[str] | None = None,
|
| 33 |
+
) -> tuple[dict, dict[str, float]]:
|
| 34 |
+
verifier_result: dict = {"anti_cheat_flags": anti_cheat_flags or []}
|
| 35 |
+
if action.tool_name == "submit_finding":
|
| 36 |
+
verifier_result["finding"] = verify_finding(state, action.arguments)
|
| 37 |
+
elif action.tool_name == "run_visible_tests":
|
| 38 |
+
verifier_result["visible"] = run_visible_tests(state)
|
| 39 |
+
elif action.tool_name == "submit_fix":
|
| 40 |
+
verifier_result.update(
|
| 41 |
+
{
|
| 42 |
+
"visible": run_visible_tests(state),
|
| 43 |
+
"security": run_hidden_security_tests(state),
|
| 44 |
+
"regression": run_hidden_regression_tests(state),
|
| 45 |
+
"public_routes": run_public_route_tests(state),
|
| 46 |
+
"patch_quality": patch_quality(state),
|
| 47 |
+
}
|
| 48 |
+
)
|
| 49 |
+
return verifier_result, compute_reward(state, action, verifier_result)
|
template_renderer.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Render small FastAPI-style generated workspaces."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from typing import Any
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def render_fastapi_basic(workspace: Path, public_hint: dict[str, Any], hidden: dict[str, Any]) -> list[str]:
|
| 10 |
+
"""Render the MVP vulnerable invoices app and visible tests."""
|
| 11 |
+
|
| 12 |
+
app_dir = workspace / "app"
|
| 13 |
+
routes_dir = app_dir / "routes"
|
| 14 |
+
tests_dir = workspace / "tests"
|
| 15 |
+
routes_dir.mkdir(parents=True, exist_ok=True)
|
| 16 |
+
tests_dir.mkdir(parents=True, exist_ok=True)
|
| 17 |
+
|
| 18 |
+
(app_dir / "__init__.py").write_text("", encoding="utf-8")
|
| 19 |
+
(routes_dir / "__init__.py").write_text("", encoding="utf-8")
|
| 20 |
+
(app_dir / "data.py").write_text(
|
| 21 |
+
"USERS = " + repr(hidden["users"]) + "\n\nINVOICES = " + repr(hidden["invoices"]) + "\n",
|
| 22 |
+
encoding="utf-8",
|
| 23 |
+
)
|
| 24 |
+
(app_dir / "auth.py").write_text(
|
| 25 |
+
"""from .data import USERS
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def get_actor(user_id: str):
|
| 29 |
+
return USERS.get(user_id)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def is_billing_admin(actor: dict) -> bool:
|
| 33 |
+
return "billing_admin" in actor.get("roles", [])
|
| 34 |
+
""",
|
| 35 |
+
encoding="utf-8",
|
| 36 |
+
)
|
| 37 |
+
(routes_dir / "invoices.py").write_text(
|
| 38 |
+
"""from app.auth import get_actor, is_billing_admin
|
| 39 |
+
from app.data import INVOICES
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def get_invoice(invoice_id: str, user_id: str):
|
| 43 |
+
actor = get_actor(user_id)
|
| 44 |
+
if actor is None:
|
| 45 |
+
return {"status": 401, "body": {"detail": "unknown user"}}
|
| 46 |
+
|
| 47 |
+
invoice = INVOICES.get(invoice_id)
|
| 48 |
+
if invoice is None:
|
| 49 |
+
return {"status": 404, "body": {"detail": "invoice not found"}}
|
| 50 |
+
|
| 51 |
+
# BUG: this only checks that the caller is authenticated. It forgets the
|
| 52 |
+
# owner/admin and tenant policy checks required by the policy graph.
|
| 53 |
+
return {"status": 200, "body": invoice}
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def health():
|
| 57 |
+
return {"status": 200, "body": {"status": "ok"}}
|
| 58 |
+
""",
|
| 59 |
+
encoding="utf-8",
|
| 60 |
+
)
|
| 61 |
+
(app_dir / "main.py").write_text(
|
| 62 |
+
"""from app.routes.invoices import get_invoice, health
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
ROUTES = [
|
| 66 |
+
{"method": "GET", "path": "/health", "handler": health, "public": True},
|
| 67 |
+
{"method": "GET", "path": "/invoices/{invoice_id}", "handler": get_invoice, "public": False},
|
| 68 |
+
]
|
| 69 |
+
""",
|
| 70 |
+
encoding="utf-8",
|
| 71 |
+
)
|
| 72 |
+
(tests_dir / "test_visible.py").write_text(
|
| 73 |
+
f"""from app.routes.invoices import get_invoice, health
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def test_health_is_public():
|
| 77 |
+
assert health()["status"] == 200
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def test_owner_can_read_own_invoice():
|
| 81 |
+
result = get_invoice("{hidden['owner_invoice_id']}", "{hidden['owner_user_id']}")
|
| 82 |
+
assert result["status"] == 200
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def test_admin_can_read_same_tenant_invoice():
|
| 86 |
+
result = get_invoice("{hidden['other_invoice_id']}", "{hidden['admin_user_id']}")
|
| 87 |
+
assert result["status"] == 200
|
| 88 |
+
""",
|
| 89 |
+
encoding="utf-8",
|
| 90 |
+
)
|
| 91 |
+
return [
|
| 92 |
+
"app/main.py",
|
| 93 |
+
"app/auth.py",
|
| 94 |
+
"app/data.py",
|
| 95 |
+
"app/routes/invoices.py",
|
| 96 |
+
"tests/test_visible.py",
|
| 97 |
+
]
|
tests/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Test package for CyberSecurity_OWASP."""
|
tests/helpers.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
|
| 3 |
+
from CyberSecurity_OWASP.models import CyberSecurityOWASPAction
|
| 4 |
+
from CyberSecurity_OWASP.server.CyberSecurity_OWASP_environment import CybersecurityOwaspEnvironment
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def make_env(seed: int = 7) -> CybersecurityOwaspEnvironment:
|
| 8 |
+
env = CybersecurityOwaspEnvironment()
|
| 9 |
+
env.reset(seed=seed, episode_id=f"test-{seed}")
|
| 10 |
+
return env
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def submit_valid_finding(env: CybersecurityOwaspEnvironment):
|
| 14 |
+
hidden = env.state.hidden_facts
|
| 15 |
+
return env.step(
|
| 16 |
+
CyberSecurityOWASPAction(
|
| 17 |
+
tool_name="submit_finding",
|
| 18 |
+
arguments={
|
| 19 |
+
"summary": "BOLA IDOR authorization bug lets a same-tenant user read another user's invoice.",
|
| 20 |
+
"evidence": (
|
| 21 |
+
f"user {hidden['owner_user_id']} can request invoice "
|
| 22 |
+
f"{hidden['other_invoice_id']} even though policy requires owner or billing_admin"
|
| 23 |
+
),
|
| 24 |
+
"policy_rule": "owner or billing_admin in same tenant only",
|
| 25 |
+
},
|
| 26 |
+
)
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def secure_invoice_source(env: CybersecurityOwaspEnvironment) -> str:
|
| 31 |
+
workspace = Path(env.state.hidden_facts["workspace"])
|
| 32 |
+
source = (workspace / "app/routes/invoices.py").read_text(encoding="utf-8")
|
| 33 |
+
return source.replace(
|
| 34 |
+
" # BUG: this only checks that the caller is authenticated. It forgets the\n"
|
| 35 |
+
" # owner/admin and tenant policy checks required by the policy graph.\n"
|
| 36 |
+
" return {\"status\": 200, \"body\": invoice}\n",
|
| 37 |
+
" if invoice[\"tenant_id\"] != actor[\"tenant_id\"]:\n"
|
| 38 |
+
" return {\"status\": 403, \"body\": {\"detail\": \"forbidden\"}}\n"
|
| 39 |
+
" if invoice[\"owner_user_id\"] != actor[\"user_id\"] and not is_billing_admin(actor):\n"
|
| 40 |
+
" return {\"status\": 403, \"body\": {\"detail\": \"forbidden\"}}\n"
|
| 41 |
+
" return {\"status\": 200, \"body\": invoice}\n",
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def apply_secure_patch(env: CybersecurityOwaspEnvironment):
|
| 46 |
+
return env.step(
|
| 47 |
+
CyberSecurityOWASPAction(
|
| 48 |
+
tool_name="patch_file",
|
| 49 |
+
arguments={"path": "app/routes/invoices.py", "content": secure_invoice_source(env)},
|
| 50 |
+
)
|
| 51 |
+
)
|
tests/test_anti_cheat.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from CyberSecurity_OWASP.models import CyberSecurityOWASPAction
|
| 2 |
+
|
| 3 |
+
from .helpers import make_env, submit_valid_finding
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def test_patch_hidden_identifier_flagged():
|
| 7 |
+
env = make_env(50)
|
| 8 |
+
submit_valid_finding(env)
|
| 9 |
+
hidden_id = env.state.hidden_facts["other_invoice_id"]
|
| 10 |
+
env.step(
|
| 11 |
+
CyberSecurityOWASPAction(
|
| 12 |
+
tool_name="patch_file",
|
| 13 |
+
arguments={"path": "app/routes/invoices.py", "content": f"# {hidden_id}\n"},
|
| 14 |
+
)
|
| 15 |
+
)
|
| 16 |
+
assert "hardcoded_hidden_identifier" in env.state.anti_cheat_flags
|
tests/test_invalid_actions.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from CyberSecurity_OWASP.models import CyberSecurityOWASPAction
|
| 2 |
+
|
| 3 |
+
from .helpers import make_env, submit_valid_finding
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def test_wrong_phase_action_is_structured_error():
|
| 7 |
+
env = make_env(30)
|
| 8 |
+
obs = env.step(
|
| 9 |
+
CyberSecurityOWASPAction(
|
| 10 |
+
tool_name="patch_file",
|
| 11 |
+
arguments={"path": "app/routes/invoices.py", "content": ""},
|
| 12 |
+
)
|
| 13 |
+
)
|
| 14 |
+
assert obs.last_action_valid is False
|
| 15 |
+
assert "not allowed" in (obs.last_action_error or "")
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def test_blocked_hidden_path_does_not_crash():
|
| 19 |
+
env = make_env(31)
|
| 20 |
+
obs = env.step(CyberSecurityOWASPAction(tool_name="read_file", arguments={"path": "../hidden.py"}))
|
| 21 |
+
assert obs.last_action_valid is False
|
| 22 |
+
assert "blocked" in (obs.last_action_error or "")
|
| 23 |
+
assert "blocked_file_probe" in env.state.anti_cheat_flags
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def test_external_request_is_blocked():
|
| 27 |
+
env = make_env(32)
|
| 28 |
+
obs = env.step(
|
| 29 |
+
CyberSecurityOWASPAction(
|
| 30 |
+
tool_name="send_local_request",
|
| 31 |
+
arguments={"method": "GET", "path": "https://example.com"},
|
| 32 |
+
)
|
| 33 |
+
)
|
| 34 |
+
assert obs.last_action_valid is False
|
| 35 |
+
assert "external_network_attempt" in env.state.anti_cheat_flags
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def test_visible_tests_are_not_patchable():
|
| 39 |
+
env = make_env(33)
|
| 40 |
+
submit_valid_finding(env)
|
| 41 |
+
obs = env.step(
|
| 42 |
+
CyberSecurityOWASPAction(
|
| 43 |
+
tool_name="patch_file",
|
| 44 |
+
arguments={"path": "tests/test_visible.py", "content": ""},
|
| 45 |
+
)
|
| 46 |
+
)
|
| 47 |
+
assert obs.last_action_valid is False
|
| 48 |
+
assert "not patchable" in (obs.last_action_error or "")
|
tests/test_models.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from CyberSecurity_OWASP import (
|
| 2 |
+
CyberSecurityOWASPAction,
|
| 3 |
+
CyberSecurityOWASPObservation,
|
| 4 |
+
CyberSecurityOWASPState,
|
| 5 |
+
)
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def test_models_serialize():
|
| 9 |
+
action = CyberSecurityOWASPAction(tool_name="noop")
|
| 10 |
+
assert action.model_dump()["tool_name"] == "noop"
|
| 11 |
+
obs = CyberSecurityOWASPObservation(phase="discover", message="ok")
|
| 12 |
+
assert obs.model_dump()["phase"] == "discover"
|
| 13 |
+
state = CyberSecurityOWASPState(episode_id="e1", seed=1)
|
| 14 |
+
assert state.model_dump()["seed"] == 1
|
tests/test_reset_step_state.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from CyberSecurity_OWASP.models import CyberSecurityOWASPAction
|
| 2 |
+
|
| 3 |
+
from .helpers import make_env
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def test_reset_initializes_scenario_and_state():
|
| 7 |
+
env = make_env(10)
|
| 8 |
+
state = env.state
|
| 9 |
+
assert state.seed == 10
|
| 10 |
+
assert state.phase == "discover"
|
| 11 |
+
assert state.domain == "invoices"
|
| 12 |
+
assert state.bug_family == "bola_idor"
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def test_step_count_and_done_stability():
|
| 16 |
+
env = make_env(11)
|
| 17 |
+
env.step(CyberSecurityOWASPAction(tool_name="noop"))
|
| 18 |
+
assert env.state.step_count == 1
|
| 19 |
+
env.state.done = True
|
| 20 |
+
env.state.phase = "done"
|
| 21 |
+
first = env.step(CyberSecurityOWASPAction(tool_name="noop"))
|
| 22 |
+
second = env.step(CyberSecurityOWASPAction(tool_name="noop"))
|
| 23 |
+
assert first.done is True
|
| 24 |
+
assert second.done is True
|
| 25 |
+
assert env.state.step_count == 1
|
tests/test_rewards.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from CyberSecurity_OWASP.models import CyberSecurityOWASPAction
|
| 2 |
+
|
| 3 |
+
from .helpers import apply_secure_patch, make_env, secure_invoice_source, submit_valid_finding
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def test_oracle_patch_gets_high_reward():
|
| 7 |
+
env = make_env(40)
|
| 8 |
+
finding = submit_valid_finding(env)
|
| 9 |
+
assert finding.reward_breakdown["discovery"] == 3.0
|
| 10 |
+
apply_secure_patch(env)
|
| 11 |
+
visible = env.step(CyberSecurityOWASPAction(tool_name="run_visible_tests"))
|
| 12 |
+
assert visible.reward_breakdown["visible_tests"] == 1.0
|
| 13 |
+
final = env.step(CyberSecurityOWASPAction(tool_name="submit_fix"))
|
| 14 |
+
assert env.state.success is True
|
| 15 |
+
assert final.reward_breakdown["total"] >= 12.0
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def test_finding_without_patch_does_not_succeed():
|
| 19 |
+
env = make_env(41)
|
| 20 |
+
submit_valid_finding(env)
|
| 21 |
+
final = env.step(CyberSecurityOWASPAction(tool_name="submit_fix"))
|
| 22 |
+
assert env.state.success is False
|
| 23 |
+
assert final.reward_breakdown["security"] < 0
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def test_deny_all_patch_fails_regression():
|
| 27 |
+
env = make_env(42)
|
| 28 |
+
submit_valid_finding(env)
|
| 29 |
+
source = secure_invoice_source(env).replace(
|
| 30 |
+
" if invoice[\"tenant_id\"] != actor[\"tenant_id\"]:\n",
|
| 31 |
+
" return {\"status\": 403, \"body\": {\"detail\": \"forbidden\"}}\n"
|
| 32 |
+
" if invoice[\"tenant_id\"] != actor[\"tenant_id\"]:\n",
|
| 33 |
+
)
|
| 34 |
+
env.step(
|
| 35 |
+
CyberSecurityOWASPAction(
|
| 36 |
+
tool_name="patch_file",
|
| 37 |
+
arguments={"path": "app/routes/invoices.py", "content": source},
|
| 38 |
+
)
|
| 39 |
+
)
|
| 40 |
+
final = env.step(CyberSecurityOWASPAction(tool_name="submit_fix"))
|
| 41 |
+
assert final.reward_breakdown["regression"] < 0
|
| 42 |
+
assert env.state.success is False
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def test_hardcoded_patch_is_penalized():
|
| 46 |
+
env = make_env(43)
|
| 47 |
+
submit_valid_finding(env)
|
| 48 |
+
hidden = env.state.hidden_facts
|
| 49 |
+
source = secure_invoice_source(env) + f"\n# hardcoded {hidden['owner_invoice_id']}\n"
|
| 50 |
+
env.step(
|
| 51 |
+
CyberSecurityOWASPAction(
|
| 52 |
+
tool_name="patch_file",
|
| 53 |
+
arguments={"path": "app/routes/invoices.py", "content": source},
|
| 54 |
+
)
|
| 55 |
+
)
|
| 56 |
+
final = env.step(CyberSecurityOWASPAction(tool_name="submit_fix"))
|
| 57 |
+
assert final.reward_breakdown["anti_cheat"] < 0
|
| 58 |
+
assert env.state.success is False
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def test_visible_tests_only_does_not_get_high_reward():
|
| 62 |
+
env = make_env(44)
|
| 63 |
+
submit_valid_finding(env)
|
| 64 |
+
visible = env.step(CyberSecurityOWASPAction(tool_name="run_visible_tests"))
|
| 65 |
+
assert visible.reward_breakdown["visible_tests"] == 1.0
|
| 66 |
+
final = env.step(CyberSecurityOWASPAction(tool_name="submit_fix"))
|
| 67 |
+
assert final.reward_breakdown["total"] < 5.0
|
tests/test_rollouts.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from CyberSecurity_OWASP.evals import bad_policy, random_policy
|
| 2 |
+
from CyberSecurity_OWASP.models import CyberSecurityOWASPAction
|
| 3 |
+
|
| 4 |
+
from .helpers import apply_secure_patch, make_env, submit_valid_finding
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def test_random_policy_does_not_crash():
|
| 8 |
+
env = make_env(60)
|
| 9 |
+
for action in random_policy():
|
| 10 |
+
obs = env.step(action)
|
| 11 |
+
assert obs is not None
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def test_bad_policy_is_penalized_or_flagged():
|
| 15 |
+
env = make_env(61)
|
| 16 |
+
for action in bad_policy():
|
| 17 |
+
obs = env.step(action)
|
| 18 |
+
assert env.state.anti_cheat_flags
|
| 19 |
+
assert obs.reward <= 0
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def test_scripted_oracle_solves_episode():
|
| 23 |
+
env = make_env(62)
|
| 24 |
+
submit_valid_finding(env)
|
| 25 |
+
apply_secure_patch(env)
|
| 26 |
+
env.step(CyberSecurityOWASPAction(tool_name="run_visible_tests"))
|
| 27 |
+
final = env.step(CyberSecurityOWASPAction(tool_name="submit_fix"))
|
| 28 |
+
assert final.done is True
|
| 29 |
+
assert env.state.success is True
|
tests/test_seed_reproducibility.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .helpers import make_env
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
def test_same_seed_reproducible_visible_facts():
|
| 5 |
+
a = make_env(22)
|
| 6 |
+
b = make_env(22)
|
| 7 |
+
assert a.state.task_id == b.state.task_id
|
| 8 |
+
assert a.state.hidden_facts["owner_invoice_id"] == b.state.hidden_facts["owner_invoice_id"]
|
| 9 |
+
assert a.state.hidden_facts["other_invoice_id"] == b.state.hidden_facts["other_invoice_id"]
|
| 10 |
+
assert a.state.visible_facts == b.state.visible_facts
|
training/configs/grpo_small.yaml
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
model_name: Qwen/Qwen3-1.7B
|
| 2 |
+
algo: grpo
|
| 3 |
+
environment: CyberSecurity_OWASP
|
| 4 |
+
max_steps: 40
|
| 5 |
+
num_generations: 2
|
| 6 |
+
per_device_train_batch_size: 1
|
| 7 |
+
gradient_accumulation_steps: 32
|
| 8 |
+
learning_rate: 0.000005
|
| 9 |
+
report_to: trackio
|
training/eval_before_after.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Baseline-vs-trained evaluation scaffold for CyberSecurity_OWASP."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import json
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def summarize_runs(baseline: list[dict], trained: list[dict], heldout: list[dict]) -> dict:
|
| 10 |
+
def mean(items: list[dict], key: str) -> float:
|
| 11 |
+
return sum(float(item.get(key, 0.0)) for item in items) / max(1, len(items))
|
| 12 |
+
|
| 13 |
+
return {
|
| 14 |
+
"baseline_success_rate": mean(baseline, "success"),
|
| 15 |
+
"trained_success_rate": mean(trained, "success"),
|
| 16 |
+
"absolute_success_improvement": mean(trained, "success") - mean(baseline, "success"),
|
| 17 |
+
"baseline_mean_reward": mean(baseline, "reward_total"),
|
| 18 |
+
"trained_mean_reward": mean(trained, "reward_total"),
|
| 19 |
+
"absolute_reward_improvement": mean(trained, "reward_total") - mean(baseline, "reward_total"),
|
| 20 |
+
"heldout_success_rate": mean(heldout, "success"),
|
| 21 |
+
"heldout_mean_reward": mean(heldout, "reward_total"),
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def save_eval_summary(run_name: str, summary: dict) -> Path:
|
| 26 |
+
output = Path("outputs/evals") / f"{run_name}_eval_summary.json"
|
| 27 |
+
output.parent.mkdir(parents=True, exist_ok=True)
|
| 28 |
+
output.write_text(json.dumps(summary, indent=2, sort_keys=True), encoding="utf-8")
|
| 29 |
+
return output
|
training/reward_funcs.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Reward functions exposed for TRL/GRPO logging."""
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
def _values(name: str, completions, kwargs):
|
| 5 |
+
return [float(x) for x in kwargs.get(name, [0.0] * len(completions))]
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def reward_total(completions, **kwargs):
|
| 9 |
+
return _values("reward_total", completions, kwargs)
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def reward_security(completions, **kwargs):
|
| 13 |
+
return _values("reward_security", completions, kwargs)
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def reward_regression(completions, **kwargs):
|
| 17 |
+
return _values("reward_regression", completions, kwargs)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def reward_patch_quality(completions, **kwargs):
|
| 21 |
+
return _values("reward_patch_quality", completions, kwargs)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def reward_anti_cheat(completions, **kwargs):
|
| 25 |
+
return _values("reward_anti_cheat", completions, kwargs)
|
training/rollout.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Minimal rollout loop for CyberSecurity_OWASP episodes."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import json
|
| 6 |
+
from typing import Any
|
| 7 |
+
|
| 8 |
+
from CyberSecurity_OWASP import CyberSecurityOWASPAction
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def build_cybersecurity_owasp_prompt(observation, action_trace, observation_trace) -> str:
|
| 12 |
+
return (
|
| 13 |
+
"You are a defensive AppSec repair agent. Output exactly one JSON action.\n"
|
| 14 |
+
f"Phase: {observation.phase}\n"
|
| 15 |
+
f"Task: {observation.task_brief}\n"
|
| 16 |
+
f"Available actions: {observation.available_actions}\n"
|
| 17 |
+
f"Last result: {observation.last_tool_result}\n"
|
| 18 |
+
'Example: {"tool_name":"read_file","arguments":{"path":"app/routes/invoices.py"}}'
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def parse_action_json(text: str) -> CyberSecurityOWASPAction:
|
| 23 |
+
data = json.loads(text)
|
| 24 |
+
return CyberSecurityOWASPAction(**data)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def generate_rollout_completions(trainer, prompts: list[str]) -> list[dict[str, Any]]:
|
| 28 |
+
if hasattr(trainer, "generate_rollout_completions"):
|
| 29 |
+
return trainer.generate_rollout_completions(prompts)
|
| 30 |
+
return [
|
| 31 |
+
{
|
| 32 |
+
"text": '{"tool_name":"noop","arguments":{}}',
|
| 33 |
+
"prompt_ids": [],
|
| 34 |
+
"completion_ids": [],
|
| 35 |
+
"logprobs": [],
|
| 36 |
+
}
|
| 37 |
+
for _ in prompts
|
| 38 |
+
]
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def rollout_once(trainer, env, tokenizer=None, dataset_prompt: str = "", max_steps: int = 40) -> dict:
|
| 42 |
+
result = env.reset()
|
| 43 |
+
observation = result.observation if hasattr(result, "observation") else result
|
| 44 |
+
|
| 45 |
+
prompt_ids = []
|
| 46 |
+
completion_ids = []
|
| 47 |
+
logprobs = []
|
| 48 |
+
reward_trace = []
|
| 49 |
+
action_trace = []
|
| 50 |
+
observation_trace = []
|
| 51 |
+
|
| 52 |
+
for _ in range(max_steps):
|
| 53 |
+
if getattr(observation, "done", False):
|
| 54 |
+
break
|
| 55 |
+
prompt = build_cybersecurity_owasp_prompt(observation, action_trace, observation_trace)
|
| 56 |
+
rollout_output = generate_rollout_completions(trainer, [prompt])[0]
|
| 57 |
+
action = parse_action_json(rollout_output["text"])
|
| 58 |
+
result = env.step(action)
|
| 59 |
+
observation = result.observation if hasattr(result, "observation") else result
|
| 60 |
+
|
| 61 |
+
prompt_ids.extend(rollout_output["prompt_ids"])
|
| 62 |
+
completion_ids.extend(rollout_output["completion_ids"])
|
| 63 |
+
logprobs.extend(rollout_output["logprobs"])
|
| 64 |
+
reward_trace.append(float(getattr(observation, "reward", 0.0) or 0.0))
|
| 65 |
+
action_trace.append(action.model_dump())
|
| 66 |
+
observation_trace.append(observation.model_dump())
|
| 67 |
+
|
| 68 |
+
final_breakdown = getattr(observation, "reward_breakdown", {}) or {}
|
| 69 |
+
state = env.state if not callable(getattr(env, "state", None)) else env.state()
|
| 70 |
+
return {
|
| 71 |
+
"prompt_ids": prompt_ids,
|
| 72 |
+
"completion_ids": completion_ids,
|
| 73 |
+
"logprobs": logprobs,
|
| 74 |
+
"reward_total": float(final_breakdown.get("total", sum(reward_trace))),
|
| 75 |
+
"reward_discovery": float(final_breakdown.get("discovery", 0.0)),
|
| 76 |
+
"reward_security": float(final_breakdown.get("security", 0.0)),
|
| 77 |
+
"reward_regression": float(final_breakdown.get("regression", 0.0)),
|
| 78 |
+
"reward_patch_quality": float(final_breakdown.get("patch_quality", 0.0)),
|
| 79 |
+
"reward_anti_cheat": float(final_breakdown.get("anti_cheat", 0.0)),
|
| 80 |
+
"success": bool(getattr(state, "success", False)),
|
| 81 |
+
"episode_length": len(action_trace),
|
| 82 |
+
"actions": action_trace,
|
| 83 |
+
"observations": observation_trace,
|
| 84 |
+
}
|
training/trackio_utils.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Trackio helpers used by training and evaluation scripts."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
TRAIN_METRICS = [
|
| 9 |
+
"train/reward_total_mean",
|
| 10 |
+
"train/reward_discovery_mean",
|
| 11 |
+
"train/reward_security_mean",
|
| 12 |
+
"train/reward_regression_mean",
|
| 13 |
+
"train/reward_public_routes_mean",
|
| 14 |
+
"train/reward_patch_quality_mean",
|
| 15 |
+
"train/reward_visible_tests_mean",
|
| 16 |
+
"train/reward_safety_mean",
|
| 17 |
+
"train/reward_anti_cheat_mean",
|
| 18 |
+
"train/success_rate",
|
| 19 |
+
"train/exploit_block_rate",
|
| 20 |
+
"train/regression_preservation_rate",
|
| 21 |
+
"train/public_route_preservation_rate",
|
| 22 |
+
"train/invalid_action_rate",
|
| 23 |
+
"train/timeout_rate",
|
| 24 |
+
"train/safety_violation_rate",
|
| 25 |
+
"train/reward_hacking_suspected_rate",
|
| 26 |
+
"train/episode_length_mean",
|
| 27 |
+
"train/episode_length_p95",
|
| 28 |
+
"train/rollouts_per_second",
|
| 29 |
+
"train/tokens_per_second",
|
| 30 |
+
"train/loss",
|
| 31 |
+
"train/learning_rate",
|
| 32 |
+
"train/kl",
|
| 33 |
+
"train/grad_norm",
|
| 34 |
+
]
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def build_run_name(model: str, algo: str, difficulty: int, git_sha: str = "nogit") -> str:
|
| 38 |
+
stamp = datetime.utcnow().strftime("%Y%m%d-%H%M")
|
| 39 |
+
model_slug = model.replace("/", "-")
|
| 40 |
+
return f"CyberSecurity_OWASP-{model_slug}-{algo}-level{difficulty}-{stamp}-{git_sha[:8]}"
|
training/train_grpo.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Minimal GRPO training entrypoint scaffold.
|
| 2 |
+
|
| 3 |
+
This file intentionally does not start training on import. It validates that the
|
| 4 |
+
required TRL/Trackio configuration can be constructed when optional training
|
| 5 |
+
dependencies are installed.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import os
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def build_grpo_config():
|
| 14 |
+
from trl import GRPOConfig
|
| 15 |
+
|
| 16 |
+
output_dir = os.getenv("OUTPUT_DIR", "CyberSecurity_OWASP-qwen3-1.7b-grpo")
|
| 17 |
+
trackio_space_id = os.getenv("TRACKIO_SPACE_ID", output_dir)
|
| 18 |
+
return GRPOConfig(
|
| 19 |
+
output_dir=output_dir,
|
| 20 |
+
report_to="trackio",
|
| 21 |
+
trackio_space_id=trackio_space_id,
|
| 22 |
+
logging_steps=1,
|
| 23 |
+
save_steps=25,
|
| 24 |
+
learning_rate=5e-6,
|
| 25 |
+
num_train_epochs=1,
|
| 26 |
+
per_device_train_batch_size=1,
|
| 27 |
+
gradient_accumulation_steps=32,
|
| 28 |
+
num_generations=2,
|
| 29 |
+
max_prompt_length=4096,
|
| 30 |
+
max_completion_length=768,
|
| 31 |
+
use_vllm=True,
|
| 32 |
+
vllm_mode="colocate",
|
| 33 |
+
vllm_gpu_memory_utilization=0.2,
|
| 34 |
+
gradient_checkpointing=True,
|
| 35 |
+
gradient_checkpointing_kwargs={"use_reentrant": False},
|
| 36 |
+
push_to_hub=False,
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def main():
|
| 41 |
+
config = build_grpo_config()
|
| 42 |
+
print(config)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
if __name__ == "__main__":
|
| 46 |
+
main()
|
uv.lock
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|