| title | CVE Triage Env | ||||
|---|---|---|---|---|---|
| emoji | ๐ | ||||
| colorFrom | blue | ||||
| colorTo | green | ||||
| sdk | docker | ||||
| pinned | false | ||||
| tags |
|
An OpenEnv-compliant RL benchmark for training and evaluating AI agents on autonomous security vulnerability triage โ the real task of an on-call security engineer.
Every week, security teams receive dozens of new CVEs. A typical company runs 100+ dependencies. The hard problem is not finding vulnerabilities โ tools like Dependabot do that. The hard problem is knowing which ones to patch first, given CVSS scores, exploitability, attack vectors, and the company's actual business exposure.
This environment trains and evaluates AI agents on exactly that skill. It simulates the full triage workflow: detection โ prioritization โ remediation โ and scores each phase with a mathematically rigorous reward function that provides partial credit at every step.
| Agent | Task 1 โ Detection | Task 2 โ Prioritization | Task 3 โ Remediation | Overall |
|---|---|---|---|---|
| Naive LLM Baseline (Llama 3.3 70B, zero-shot) | 0.4783 | 0.4494 | 0.4641 | 0.4639 |
| Hardened Agentic Pipeline (Deterministic + Reflection) | 1.0000 | 0.9638 | 0.9013 | 0.9550 |
Why the naive LLM scores ~0.47: Pure LLMs hallucinate semantic version ranges. They guess that requests==2.28.0 is not vulnerable to a CVE affecting requests < 2.32.0 โ getting the math wrong consistently. This is a known failure mode, not a prompt engineering problem.
Why Task 3 scores 0.9013, not 1.00: The pipeline shows highly realistic variance on complex dependency graphs. While it correctly identifies the critical fixes, minor variations in the exact wording and specificity of the 3rd-slot fix suggestion correctly trigger partial-credit penalties. This is intentional benchmark design โ the environment tests genuine reasoning and exactness, avoiding the "overfitted 1.00" trap.
Reflection loop effectiveness: On 5 of 8 stacks, the agent's initial ranking was suboptimal. After receiving Kendall-Tau feedback from the OpenEnv grader, the agent utilized its reflection loop to self-correct. The largest single improvement was on Stack 7 (Healthcare SaaS): 0.77 โ 0.91 (+0.14).
Given a company dependency stack (package โ installed version) and a feed of 12 CVEs with affected version ranges, identify which CVEs affect this specific stack.
- Input:
dependenciesdict + full CVE feed - Output:
affected_cves: list[str] - Reward: F1 score (precision ร recall) with an additional โ0.10 penalty per missed critical-severity CVE
Given the CVEs that affect the stack (pre-filtered), rank them from highest to lowest patch priority. The agent must weigh CVSS score, exploitability rating, attack vector (network vs. local), and the company's business profile.
- Input: pre-filtered CVE list + company profile
- Output:
priority_ranking: list[str](ordered highest โ lowest) - Reward: Normalized Kendall-Tau distance + 0.10 bonus for correct #1, + 0.05 bonus for correct #2
End-to-end triage with no scaffolding. The agent must: (1) detect affected CVEs, (2) rank them, and (3) suggest a specific fix action for the top 3 (e.g. "Upgrade flask to 2.3.0 or higher").
- Input: raw dependency stack + full CVE feed + company profile
- Output:
affected_cves+priority_ranking+fix_suggestions: dict[str, str] - Reward: Weighted composite โ 40% detection F1 + 30% ranking Kendall-Tau + 30% fix quality. Fix suggestions are penalized โ0.10 per hallucinated CVE ID.
precision = TP / (TP + FP)
recall = TP / (TP + FN)
f1 = 2 * precision * recall / (precision + recall)
penalty = 0.10 * count(missed critical CVEs)
reward = max(0.0, f1 - penalty)
kendall_tau = concordant_pairs / (concordant_pairs + discordant_pairs)
top1_bonus = 0.10 if predicted[0] == ground_truth[0]
top2_bonus = 0.05 if predicted[1] == ground_truth[1]
reward = min(1.0, kendall_tau + top1_bonus + top2_bonus)
fix_score = sum(0.33 for each top-3 CVE where correct package is mentioned)
- 0.10 * count(hallucinated CVE IDs in fix_suggestions)
fix_score = clamp(fix_score, 0.0, 1.0)
reward = 0.40 * task1_score + 0.30 * task2_score + 0.30 * fix_score
Empty action penalty: Any step with no fields populated receives reward = โ0.30.
{
"task_id": 1,
"stack_id": "stack_001",
"company_profile": "HIPAA-compliant Healthcare SaaS. Patient data processed in-region. External API exposure on Flask endpoints.",
"dependencies": {
"flask": "2.0.1",
"sqlalchemy": "1.4.23",
"cryptography": "3.4.7",
"requests": "2.25.1"
},
"cve_feed": [
{
"cve_id": "CVE-2024-0001",
"affected_package": "flask",
"affected_versions_below": "2.3.0",
"cvss_score": 8.1,
"exploitability": "high",
"attack_vector": "network",
"description": "Remote code execution via crafted request headers.",
"fix": "Upgrade flask to 2.3.0 or higher."
}
],
"task_description": "TASK 1 โ Identify which CVEs affect this dependency stack.",
"step_number": 0,
"done": false,
"reward": 0.0
}{
"task_id": 1,
"affected_cves": ["CVE-2024-0001", "CVE-2024-0005"],
"priority_ranking": ["CVE-2024-0001", "CVE-2024-0005"],
"fix_suggestions": {
"CVE-2024-0001": "Upgrade flask to 2.3.0 or higher.",
"CVE-2024-0005": "Upgrade cryptography to 41.0.0 or higher."
}
}Fields required per task:
- Task 1:
task_id+affected_cves - Task 2:
task_id+affected_cves+priority_ranking - Task 3: all four fields
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ FastAPI Server (port 7860) โ
โ Thread-safe (threading.Lock) โ
โโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโค
โ POST /resetโ POST /step โ GET /state โ
โ task_id โ action: CveTriageAction โ episode_id โ
โ stack_idx โ โ CveTriageObservation โ step_count โ
โโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโ
โผ โผ โผ
grade_task1() grade_task2() grade_task3()
F1 + penalty Kendall-Tau Weighted composite
+ top-2 bonus + hallucination penalty
State management: The environment is fully stateless between episodes. reset() generates a new episode_id (UUID4) and clears all episode state. The threading lock ensures concurrent evaluation requests from automated graders cannot corrupt state mid-episode.
Our pipeline treats CVE triage as a multi-stage agentic workflow, not a single LLM call:
[Tech Stack] + [CVE Feed]
โ
โผ
โ Deterministic Version Matcher
packaging.version (PEP 440) โ mathematically exact,
zero hallucination. Achieves perfect 1.00 on Task 1.
โ
โผ
โก LLM Priority Ranker โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Llama 3.3 70B via Groq. โ
Ranks by CVSS + exploitability + company profile. โ
โ โ
โผ โ
OpenEnv grader returns partial reward โ
+ Kendall-Tau feedback โ
โ โ
โผ โ
score < 1.0? โโโโ Reflection Loop (max 3x) โโโโโโโโ
score = 1.0? โโโโ proceed
โ
โผ
โข LLM Fix Suggester
Context-aware mitigation for top 3 CVEs.
Penalized for hallucinated CVE IDs.
Key insight: Offloading version-range math to a deterministic engine and using the LLM strictly for reasoning tasks eliminates the primary failure mode of naive LLM baselines.
The reflection loop in action โ Stack 7 (Healthcare SaaS, HIPAA compliant), self-correcting after receiving directional feedback (+0.14 improvement):
[START] task=task_2 env=cve_triage_env model=llama-3.3-70b-versatile
[STEP] step=1 action=rank_cves reward=0.77 done=false error=null
[STEP] step=2 action=rank_cves reward=0.91 done=false error=null
[END] success=true steps=2 score=0.91 rewards=0.77,0.91
Full run logs for all 8 stacks available in evaluation_logs.txt.
Full per-stack breakdown available in reports.txt.
The environment ships with 8 company stacks across distinct real-world risk profiles:
| Stack | Profile | Key Packages | Task 3 Score |
|---|---|---|---|
| stack_001 | B2B SaaS Startup โ public REST API, no WAF, 10k users | Flask, SQLAlchemy, Redis, Celery | 1.00 |
| stack_002 | Internal Analytics Tool โ no public exposure, 50 users | Django, Pillow, PyYAML, Pandas | 0.88 |
| stack_003 | Fintech Platform โ WAF enabled, handles payments, 500k users | Flask, Paramiko, PyJWT, aiohttp | 0.88 |
| stack_004 | DevOps Tooling โ SSH automation, internal only | Paramiko, cryptography, PyYAML | 1.00 |
| stack_005 | E-commerce Platform โ image processing, 1M users | Django, Pillow, Celery, Gunicorn | 0.87 |
| stack_006 | ML Platform โ internal Jupyter notebooks, S3 access | Pandas, Jinja2, boto3, cryptography | 0.87 |
| stack_007 | Healthcare SaaS โ HIPAA compliant, patient portal | FastAPI, PyJWT, cryptography, httpx | 0.84 |
| stack_008 | Startup API Gateway โ microservices, no auth layer | FastAPI, aiohttp, PyJWT, psycopg2 | 0.87 |
20 CVEs in the feed spanning CVSS 4.2โ9.8, covering network/local/adjacent attack vectors and high/medium/low exploitability ratings. Ground truth rankings are calculated per-stack based on CVSS score, exploitability weight, and attack vector multiplier.
Extending the dataset: New stacks can be added by appending entries to data/stacks.json
and data/ground_truth.json โ no code changes required. The environment loads all stacks
dynamically at startup. CVEs can be extended in data/cves.json; the graders are fully
data-driven so rankings automatically reflect any new entries. This makes the benchmark
straightforward to scale to larger corpora or domain-specific CVE feeds (e.g., Node.js,
Java, Go ecosystems), and enables curriculum learning by ordering stacks from simple
(few dependencies, low CVSS variance) to complex (many packages, near-equal CVSS scores).
export API_BASE_URL="[https://api.groq.com/openai/v1](https://api.groq.com/openai/v1)" # LLM API endpoint
export MODEL_NAME="llama-3.3-70b-versatile" # Model identifier
export HF_TOKEN="your_api_key_here" # API key (Groq or OpenAI)
export ENV_BASE_URL="http://localhost:8000" # Environment server URL# 1. Install dependencies
pip install -e .
# 2. Start the environment server
uvicorn server.app:app --host 0.0.0.0 --port 8000
# 3. In a second terminal, run evaluation
python inference.py# Build and run
docker build -t cve-triage-env .
docker run -p 7860:7860 \
-e HF_TOKEN=your_key \
-e API_BASE_URL=[https://api.groq.com/openai/v1](https://api.groq.com/openai/v1) \
-e MODEL_NAME=llama-3.3-70b-versatile \
cve-triage-env| Method | Endpoint | Description |
|---|---|---|
POST |
/reset |
Start new episode. Body: {"task_id": 1, "stack_index": 0} |
POST |
/step |
Submit action. Body: {"action": {...}} |
GET |
/state |
Current episode state |
GET |
/health |
Health check |
GET |
/schema |
Action + observation JSON schemas |
GET |
/metadata |
Environment metadata |
The structured observation space is explicitly designed to support future PyTorch research:
Graph Neural Networks (PyG): The dependencies dict naturally forms a directed dependency graph. Researchers can use PyTorch Geometric to model how vulnerability risk propagates through transitive dependencies โ a library like cryptography vulnerably affects every framework that depends on it.
PPO Policy Training: The F1 and Kendall-Tau reward functions are differentiable proxies suitable for training lightweight RL policies (e.g., fine-tuned Llama-3-8B with PPO) to replace the deterministic + LLM pipeline entirely.
Curriculum Learning: The easy โ medium โ hard task progression is designed for curriculum learning โ train on Task 1 until convergence, then transfer to Task 2 using the learned detection representations.