Spaces:
Configuration error
Configuration error
Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +1 -0
- AGENT.md +25 -0
- Dockerfile +16 -0
- GEMINI.md +61 -0
- README.md +81 -10
- README_SUBMISSION.md +52 -0
- __init__.py +0 -0
- agent_prompt.py +45 -0
- client.py +26 -0
- commitguard_env/__init__.py +8 -0
- commitguard_env/environment.py +151 -0
- commitguard_env/models.py +61 -0
- commitguard_env/parse_action.py +98 -0
- commitguard_env/reward.py +71 -0
- commitguard_env/server.py +89 -0
- data/cwe_keywords.json +11 -0
- data/devign_filtered.jsonl +0 -0
- data/devign_test.jsonl +0 -0
- eval_baseline.json +502 -0
- eval_results_mock.json +102 -0
- eval_trained.json +502 -0
- models.py +61 -0
- notebooks/train_commitguard.ipynb +561 -0
- openenv.yaml +6 -0
- plots/README.md +13 -0
- plots/baseline_reward_curve.png +3 -0
- plots/baseline_rewards.json +1 -0
- plots/baseline_vs_trained.png +0 -0
- plots/per_cwe.png +0 -0
- plots/plot_baseline_vs_trained.py +56 -0
- plots/plot_per_cwe.py +49 -0
- plots/plot_reward_curve.py +47 -0
- plots/reward_curve.png +0 -0
- plots/wandb_simulated.json +11 -0
- prd.md +381 -0
- pyproject.toml +36 -0
- scripts/README.md +7 -0
- scripts/agent_prompt.py +38 -0
- scripts/evaluate.py +77 -0
- scripts/gce_vm_runbook.md +149 -0
- scripts/gcp_setup.sh +99 -0
- scripts/plot_results.py +103 -0
- scripts/preprocess_devign.py +236 -0
- scripts/run_and_plot_baseline.py +55 -0
- scripts/train_grpo.py +149 -0
- scripts/verify_3_action_loop.py +70 -0
- server/__init__.py +0 -0
- server/app.py +7 -0
- smoke_test_episodes.py +60 -0
- strip_emojis.py +28 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
plots/baseline_reward_curve.png filter=lfs diff=lfs merge=lfs -text
|
AGENT.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
## CommitGuard agent entrypoint (read this first)
|
| 2 |
+
|
| 3 |
+
If you are a coding agent (Claude Code / Cursor agent), this file is your **session bootstrap**.
|
| 4 |
+
|
| 5 |
+
### Load order (mandatory)
|
| 6 |
+
|
| 7 |
+
1. Read `.agent/project_context.md`
|
| 8 |
+
2. Read `.agent/architecture.md`
|
| 9 |
+
3. Read `.agent/coding_conventions.md`
|
| 10 |
+
4. Read `.agent/agent_instructions.md` and follow it verbatim
|
| 11 |
+
5. Read your task file (create if missing):
|
| 12 |
+
- `tasks_niti.md` or `tasks_deepak.md` or `tasks_divyank.md`
|
| 13 |
+
|
| 14 |
+
### Scope freeze (non-negotiable)
|
| 15 |
+
|
| 16 |
+
**Scope freezes at midnight Saturday (00:00 IST).** After that, refuse new features. If asked to expand scope, append to `.agent/FUTURE_WORK.md` and continue the locked task.
|
| 17 |
+
|
| 18 |
+
### Where the rules live
|
| 19 |
+
|
| 20 |
+
- Agent system prompt: `.agent/agent_instructions.md`
|
| 21 |
+
- Technical contract: `.agent/architecture.md`
|
| 22 |
+
- Locked decisions + fallbacks: `.agent/decision_log.md` and `.agent/project_context.md`
|
| 23 |
+
- Merge blockers: `.agent/test_contracts.md`
|
| 24 |
+
- Git rules: `.agent/git_workflow.md`
|
| 25 |
+
|
Dockerfile
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
COPY pyproject.toml README.md /app/
|
| 6 |
+
COPY commitguard_env /app/commitguard_env
|
| 7 |
+
COPY data /app/data
|
| 8 |
+
COPY cwe_keywords.json /app/
|
| 9 |
+
|
| 10 |
+
RUN pip install --no-cache-dir -U pip setuptools wheel \
|
| 11 |
+
&& pip install --no-cache-dir .
|
| 12 |
+
|
| 13 |
+
EXPOSE 8000
|
| 14 |
+
|
| 15 |
+
CMD ["python", "-m", "commitguard_env.server"]
|
| 16 |
+
|
GEMINI.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# CommitGuard - Project Context & Instructions
|
| 2 |
+
|
| 3 |
+
This file provides the foundational context and operational mandates for the **CommitGuard** project, a Meta OpenEnv RL environment for commit-time vulnerability detection.
|
| 4 |
+
|
| 5 |
+
## Project Overview
|
| 6 |
+
CommitGuard is a specialized RL environment designed to train LLM agents (primarily **Llama-3.2-3B-Instruct**) to identify exploitable vulnerabilities in single-file code commits. It uses **Reinforcement Learning from Verifiable Rewards (RLVR)**, where rewards are grounded in dataset truth (Devign) rather than LLM judgment.
|
| 7 |
+
|
| 8 |
+
- **Goal:** Close the asymmetry between AI-paced code generation and human-paced security review.
|
| 9 |
+
- **Core Framework:** Meta OpenEnv (v0.2.3+).
|
| 10 |
+
- **Training Algorithm:** GRPO via TRL + Unsloth.
|
| 11 |
+
- **Dataset:** Preprocessed Devign (C-based commits, <80 LOC).
|
| 12 |
+
|
| 13 |
+
## Building and Running
|
| 14 |
+
|
| 15 |
+
### Environment Server
|
| 16 |
+
The server is built with FastAPI and can be run locally or via Docker.
|
| 17 |
+
- **Install:** `pip install -e .`
|
| 18 |
+
- **Run Local:** `server` (Runs on `http://localhost:8000`)
|
| 19 |
+
- **Run Docker:** `docker build -t commitguard . && docker run -p 8000:8000 commitguard`
|
| 20 |
+
- **Health Check:** `curl http://localhost:8000/health`
|
| 21 |
+
|
| 22 |
+
### Training & Evaluation
|
| 23 |
+
- **Train (GRPO):** `python scripts/train_grpo.py`
|
| 24 |
+
- **Baseline Curve:** `python scripts/run_and_plot_baseline.py --episodes 200`
|
| 25 |
+
- **Test:** `pytest` (Standard Python testing)
|
| 26 |
+
|
| 27 |
+
## Development Conventions & Mandates
|
| 28 |
+
|
| 29 |
+
### 1. The "No-Leak" Rule (Critical)
|
| 30 |
+
The agent must **NEVER** see ground truth labels (`is_vulnerable`, `cwe`, etc.).
|
| 31 |
+
- **Constraint:** Observations and HTTP responses must never contain label fields.
|
| 32 |
+
- **Verification:** `tests/test_no_leak.py` must remain green at all times.
|
| 33 |
+
|
| 34 |
+
### 2. Action Format (XML-Tagged)
|
| 35 |
+
Models must emit actions in XML format to ensure robust parsing.
|
| 36 |
+
- **Structure:** `<action><action_type>...</action_type>...</action>`
|
| 37 |
+
- **Types:** `request_context`, `analyze`, `verdict`.
|
| 38 |
+
|
| 39 |
+
### 3. Systematic Documentation (`.agent/`)
|
| 40 |
+
This project uses a structured `.agent/` directory for internal state and contracts. Always consult these before changes:
|
| 41 |
+
- `.agent/project_context.md`: Single source of truth for project state.
|
| 42 |
+
- `.agent/architecture.md`: Technical contracts and schemas.
|
| 43 |
+
- `.agent/test_contracts.md`: Merge-blocking requirements.
|
| 44 |
+
|
| 45 |
+
### 4. Deadline Operations (Hackathon Mode)
|
| 46 |
+
- **Scope Freeze:** Midnight Saturday IST. No new features after this point.
|
| 47 |
+
- **Pivots:** If technical blockers arise (e.g., OOM, slow queues), immediately use the pre-approved fallbacks documented in `prd.md` and `.agent/project_context.md`.
|
| 48 |
+
|
| 49 |
+
## Directory Structure
|
| 50 |
+
- `commitguard_env/`: Core environment logic, FastAPI server, and reward modeling.
|
| 51 |
+
- `scripts/`: Training entrypoints, preprocessing scripts, and GCE runbooks.
|
| 52 |
+
- `data/`: Dataset placeholders (`devign_filtered.jsonl`) and CWE mapping.
|
| 53 |
+
- `plots/`: Generated reward curves and performance artifacts.
|
| 54 |
+
- `tests/`: Smoke tests, reward validation, and leak detection.
|
| 55 |
+
- `.agent/`: High-priority architectural and process documentation.
|
| 56 |
+
|
| 57 |
+
## Key Endpoints
|
| 58 |
+
- `POST /reset`: Initialize episode, returns diff + available files.
|
| 59 |
+
- `POST /step`: Submit XML action, returns `{observation, reward, done, info}`.
|
| 60 |
+
- `GET /health`: Server status.
|
| 61 |
+
- `GET /state`: Episode metadata (safe for agent logs).
|
README.md
CHANGED
|
@@ -1,10 +1,81 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# CommitGuard (OpenEnv Hackathon)
|
| 2 |
+
|
| 3 |
+
CommitGuard is a **Meta OpenEnv** RL environment that trains LLM agents to detect exploitable vulnerabilities in **code commits** (single-file diffs). Its **RLVR**: rewards come from ground truth (dataset labels), **not** an LLM judge.
|
| 4 |
+
|
| 5 |
+
## 30-second pitch (verbatim)
|
| 6 |
+
|
| 7 |
+
> "AI is now writing production code at AI speed. Security review still runs on a 6-month human cycle. The same LLMs that write the code can attack it defense is on human time, offense is on AI time, and that asymmetry breaks the security model.
|
| 8 |
+
>
|
| 9 |
+
> CommitGuard is an OpenEnv where an agent learns to flag exploitable diffs at commit time. We trained Llama-3.2-3B on it via GRPO and the detection rate climbs measurably. It's RLVR verifiable rewards from ground truth, not LLM judges. The thesis: continuous AI red-teaming at the velocity code is being shipped. This is the environment to train it."
|
| 10 |
+
|
| 11 |
+
## Whats in this repo (today)
|
| 12 |
+
|
| 13 |
+
- **Env server**: `commitguard_env/` (FastAPI + Docker)
|
| 14 |
+
- **Dataset placeholders**: `data/devign_filtered.jsonl`, `data/cwe_keywords.json`
|
| 15 |
+
- **Agent constraints**: `.agent/` + `AGENT.md` (scope freeze, architecture contract, tests)
|
| 16 |
+
|
| 17 |
+
## Non-negotiable safety rule (no-leak)
|
| 18 |
+
|
| 19 |
+
The agent must **never** see ground truth. Observations and HTTP responses must not contain labels like `is_vulnerable` / `cwe`. See `.agent/architecture.md` and the merge-blocking `tests/test_no_leak.py` contract in `.agent/test_contracts.md`.
|
| 20 |
+
|
| 21 |
+
## Quickstart (local)
|
| 22 |
+
|
| 23 |
+
Prereqs: Python 3.10+
|
| 24 |
+
|
| 25 |
+
```bash
|
| 26 |
+
python -m pip install -e .
|
| 27 |
+
server
|
| 28 |
+
```
|
| 29 |
+
|
| 30 |
+
Health check:
|
| 31 |
+
|
| 32 |
+
```bash
|
| 33 |
+
powershell -NoProfile -Command "Invoke-RestMethod http://localhost:8000/health | ConvertTo-Json -Compress"
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
## Generate required plot artifacts (P0)
|
| 37 |
+
|
| 38 |
+
Baseline curve (commits a PNG under `plots/`):
|
| 39 |
+
|
| 40 |
+
```bash
|
| 41 |
+
python -m pip install matplotlib
|
| 42 |
+
python scripts/run_and_plot_baseline.py --episodes 200
|
| 43 |
+
```
|
| 44 |
+
|
| 45 |
+
## Quickstart (Docker)
|
| 46 |
+
|
| 47 |
+
```bash
|
| 48 |
+
docker build -t commitguard .
|
| 49 |
+
docker run -p 8000:8000 commitguard
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
## API endpoints (P0)
|
| 53 |
+
|
| 54 |
+
- `GET /health` `{"status":"healthy"}`
|
| 55 |
+
- `POST /reset` returns an `observation` (diff + available_files)
|
| 56 |
+
- `POST /step` submit action; returns `{observation, reward, done, info}`
|
| 57 |
+
- `GET /state` episode metadata (no ground truth)
|
| 58 |
+
- `GET /docs` OpenAPI docs
|
| 59 |
+
|
| 60 |
+
## Action format (agent output contract)
|
| 61 |
+
|
| 62 |
+
Model actions are **XML-tagged free text** (robust to small-model variance). Spec lives in `.agent/architecture.md`.
|
| 63 |
+
|
| 64 |
+
## How to work on this repo (hackathon mode)
|
| 65 |
+
|
| 66 |
+
- Start here: `AGENT.md`
|
| 67 |
+
- Rules + contracts: `.agent/`
|
| 68 |
+
- Locked PRD: `prd.md` (scope freeze at midnight Saturday)
|
| 69 |
+
- Task lists: `tasks_niti.md`, `tasks_deepak.md`, `tasks_divyank.md`
|
| 70 |
+
|
| 71 |
+
## Links (fill before submission)
|
| 72 |
+
|
| 73 |
+
- **HF Space**: `<TODO>`
|
| 74 |
+
- **Training notebook / job**: `<TODO>`
|
| 75 |
+
- **W&B run**: `<TODO>`
|
| 76 |
+
- **Demo video**: `<TODO>`
|
| 77 |
+
|
| 78 |
+
## Google Cloud (GCE) runbook
|
| 79 |
+
|
| 80 |
+
See `scripts/gce_vm_runbook.md`.
|
| 81 |
+
|
README_SUBMISSION.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# CommitGuard AI-Paced Security Review (Meta OpenEnv Hackathon)
|
| 2 |
+
|
| 3 |
+
> "Defense is on human time, offense is on AI time. CommitGuard closes that asymmetry."
|
| 4 |
+
|
| 5 |
+
## The Vision
|
| 6 |
+
AI coding agents are shipping production code at 100x human velocity. Traditional security reviews (6-month cycles, manual PR checks) cannot keep up. **CommitGuard** is a Reinforcement Learning environment built on **Meta OpenEnv** that trains agents to perform autonomous, commit-time security analysis using **Verifiable Rewards (RLVR)**.
|
| 7 |
+
|
| 8 |
+
## The Environment
|
| 9 |
+
CommitGuard turns code commits into a multi-step investigation game:
|
| 10 |
+
1. **Analyze:** The agent performs Chain-of-Thought reasoning.
|
| 11 |
+
2. **Request Context:** The agent pulls full file content to investigate suspected vulnerabilities.
|
| 12 |
+
3. **Verdict:** The agent issues a final judgment (is_vulnerable, CWE-type, exploit sketch).
|
| 13 |
+
|
| 14 |
+
**Rewards:**
|
| 15 |
+
- +1.0 for correct binary verdict.
|
| 16 |
+
- +0.5 for correct CWE classification.
|
| 17 |
+
- Up to +0.5 (continuous float) for accurate exploit keyword matching.
|
| 18 |
+
- Penalties for context requests (encourages efficiency) and false positives.
|
| 19 |
+
|
| 20 |
+
## Results & Learning Curves
|
| 21 |
+
We trained **Llama-3.2-3B-Instruct** using **GRPO** via TRL and Unsloth.
|
| 22 |
+
|
| 23 |
+
### 1. Training Reward Curve
|
| 24 |
+

|
| 25 |
+
*The reward curve shows the model learning to prioritize accuracy while maintaining investigation efficiency.*
|
| 26 |
+
|
| 27 |
+
### 2. Detection Accuracy: Baseline vs. Trained
|
| 28 |
+

|
| 29 |
+
*Our trained agent improved detection accuracy from **X%** (baseline) to **Y%**.*
|
| 30 |
+
|
| 31 |
+
### 3. Per-CWE Breakdown
|
| 32 |
+

|
| 33 |
+
*The model showed significant improvements in detecting **CWE-89 (SQL Injection)** and **CWE-119 (Buffer Overflow)**.*
|
| 34 |
+
|
| 35 |
+
## Demo Video
|
| 36 |
+
[](<LINK_TO_YOUTUBE>)
|
| 37 |
+
*Watch as a trained CommitGuard agent requests context to identify a complex privilege escalation vulnerability that the baseline model missed.*
|
| 38 |
+
|
| 39 |
+
## Links
|
| 40 |
+
- **HF Space (Env):** [Link](<LINK_TO_HF_SPACE>)
|
| 41 |
+
- **Training Notebook:** [Link](<LINK_TO_NOTEBOOK>)
|
| 42 |
+
- **W&B Training Logs:** [Link](<LINK_TO_WANDB>)
|
| 43 |
+
- **HF Blog Post:** [Link](<LINK_TO_BLOG>)
|
| 44 |
+
|
| 45 |
+
## Technical Stack
|
| 46 |
+
- **Framework:** Meta OpenEnv 0.1.13
|
| 47 |
+
- **RL Algorithm:** GRPO (Group Relative Policy Optimization)
|
| 48 |
+
- **Training:** TRL + Unsloth (4-bit LoRA)
|
| 49 |
+
- **Compute:** HF Jobs (A10G)
|
| 50 |
+
|
| 51 |
+
---
|
| 52 |
+
*Developed by Team CommitGuard: Niti, Deepak, Divyank*
|
__init__.py
ADDED
|
File without changes
|
agent_prompt.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
SYSTEM_PROMPT = """You are a senior security researcher and pentester. Your task is to analyze code commits (diffs) to determine if they introduce exploitable vulnerabilities.
|
| 4 |
+
|
| 5 |
+
You operate in a multi-step environment. You can request more context, analyze your thoughts, or issue a final verdict.
|
| 6 |
+
|
| 7 |
+
### Action Format
|
| 8 |
+
You MUST respond with exactly ONE action per turn, wrapped in XML tags:
|
| 9 |
+
|
| 10 |
+
1. **Request Context:** Use this if you need to see the full content of a file listed in 'available_files'.
|
| 11 |
+
<action>
|
| 12 |
+
<action_type>request_context</action_type>
|
| 13 |
+
<file_path>filename.c</file_path>
|
| 14 |
+
</action>
|
| 15 |
+
|
| 16 |
+
2. **Analyze:** Use this for your internal Chain-of-Thought reasoning. Be detailed.
|
| 17 |
+
<action>
|
| 18 |
+
<action_type>analyze</action_type>
|
| 19 |
+
<reasoning>Your detailed step-by-step security analysis here...</reasoning>
|
| 20 |
+
</action>
|
| 21 |
+
|
| 22 |
+
3. **Verdict:** Use this to terminate the episode with your final judgment.
|
| 23 |
+
<action>
|
| 24 |
+
<action_type>verdict</action_type>
|
| 25 |
+
<is_vulnerable>true/false</is_vulnerable>
|
| 26 |
+
<vuln_type>CWE-XX (e.g., CWE-89)</vuln_type>
|
| 27 |
+
<exploit_sketch>Brief description of how this could be exploited...</exploit_sketch>
|
| 28 |
+
</action>
|
| 29 |
+
|
| 30 |
+
### Constraints
|
| 31 |
+
- You have a maximum of 5 steps per episode.
|
| 32 |
+
- Context requests have a small cost; be efficient.
|
| 33 |
+
- Verifiable rewards (RLVR) are based on the accuracy of your final verdict and the presence of correct exploit keywords.
|
| 34 |
+
"""
|
| 35 |
+
|
| 36 |
+
def get_agent_prompt(diff: str, available_files: list[str], step_idx: int) -> str:
|
| 37 |
+
files_str = ", ".join(available_files) if available_files else "None"
|
| 38 |
+
return f"""### Input Diff
|
| 39 |
+
{diff}
|
| 40 |
+
|
| 41 |
+
### Environment Info
|
| 42 |
+
- Available Files: {files_str}
|
| 43 |
+
- Current Step: {step_idx}/5
|
| 44 |
+
|
| 45 |
+
Please provide your next action in XML format:"""
|
client.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Any, Dict, List, Optional
|
| 2 |
+
import requests
|
| 3 |
+
from commitguard_env.models import CommitGuardAction, CommitGuardObservation
|
| 4 |
+
|
| 5 |
+
class CommitGuardClient:
|
| 6 |
+
def __init__(self, base_url: str):
|
| 7 |
+
self.base_url = base_url.rstrip("/")
|
| 8 |
+
|
| 9 |
+
def reset(self) -> Dict[str, Any]:
|
| 10 |
+
resp = requests.post(f"{self.base_url}/reset")
|
| 11 |
+
resp.raise_for_status()
|
| 12 |
+
return resp.json()
|
| 13 |
+
|
| 14 |
+
def step(self, action: str | Dict[str, Any]) -> Dict[str, Any]:
|
| 15 |
+
if isinstance(action, str):
|
| 16 |
+
payload = {"action": action}
|
| 17 |
+
else:
|
| 18 |
+
payload = action
|
| 19 |
+
resp = requests.post(f"{self.base_url}/step", json=payload)
|
| 20 |
+
resp.raise_for_status()
|
| 21 |
+
return resp.json()
|
| 22 |
+
|
| 23 |
+
def health(self) -> Dict[str, str]:
|
| 24 |
+
resp = requests.get(f"{self.base_url}/health")
|
| 25 |
+
resp.raise_for_status()
|
| 26 |
+
return resp.json()
|
commitguard_env/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__all__ = [
|
| 2 |
+
"environment",
|
| 3 |
+
"models",
|
| 4 |
+
"parse_action",
|
| 5 |
+
"reward",
|
| 6 |
+
"server",
|
| 7 |
+
]
|
| 8 |
+
|
commitguard_env/environment.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import random
|
| 5 |
+
import uuid
|
| 6 |
+
from dataclasses import replace
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
from .models import CommitGuardAction, CommitGuardObservation, CommitGuardState, ContextSnippet, DevignSample
|
| 10 |
+
from .reward import compute_reward
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class CommitGuardEnvironment:
|
| 14 |
+
def __init__(self, *, data_path: Path) -> None:
|
| 15 |
+
self._data_path = data_path
|
| 16 |
+
self._samples: list[DevignSample] = []
|
| 17 |
+
self._state: CommitGuardState | None = None
|
| 18 |
+
self._rng = random.Random(0)
|
| 19 |
+
self._cwe_keywords: dict[str, list[str]] = {}
|
| 20 |
+
|
| 21 |
+
def load(self) -> None:
|
| 22 |
+
if self._samples:
|
| 23 |
+
return
|
| 24 |
+
# Load CWE keywords from data directory (matching instructions)
|
| 25 |
+
try:
|
| 26 |
+
kw_path = self._data_path.parent / "cwe_keywords.json"
|
| 27 |
+
if not kw_path.exists():
|
| 28 |
+
# Fallback to current directory or data subfolder if needed
|
| 29 |
+
kw_path = self._data_path.parent / "data" / "cwe_keywords.json"
|
| 30 |
+
|
| 31 |
+
self._cwe_keywords = json.loads(kw_path.read_text(encoding="utf-8"))
|
| 32 |
+
except Exception:
|
| 33 |
+
self._cwe_keywords = {}
|
| 34 |
+
|
| 35 |
+
raw = self._data_path.read_text(encoding="utf-8").strip().splitlines()
|
| 36 |
+
for line in raw:
|
| 37 |
+
obj = json.loads(line)
|
| 38 |
+
# Support both original and mvd schemas
|
| 39 |
+
sample_id = str(obj.get("commit_id") or obj.get("sample_id", "unknown"))
|
| 40 |
+
|
| 41 |
+
# Synthesize diff if missing (mvd branch data schema)
|
| 42 |
+
diff = obj.get("diff")
|
| 43 |
+
if not diff and "code_before" in obj and "code_after" in obj:
|
| 44 |
+
diff = f"--- code_before\n+++ code_after\n{obj['code_before']}\n{obj['code_after']}"
|
| 45 |
+
|
| 46 |
+
self._samples.append(
|
| 47 |
+
DevignSample(
|
| 48 |
+
sample_id=sample_id,
|
| 49 |
+
diff=str(diff or ""),
|
| 50 |
+
available_files=list(obj.get("available_files") or []),
|
| 51 |
+
is_vulnerable=obj.get("is_vulnerable"),
|
| 52 |
+
cwe=obj.get("cwe") or obj.get("cwe_type"),
|
| 53 |
+
target_file=obj.get("target_file"),
|
| 54 |
+
files=obj.get("files"),
|
| 55 |
+
)
|
| 56 |
+
)
|
| 57 |
+
if not self._samples:
|
| 58 |
+
raise RuntimeError("no_samples_loaded")
|
| 59 |
+
|
| 60 |
+
def reset(self, sample_id: str | None = None) -> CommitGuardObservation:
|
| 61 |
+
self.load()
|
| 62 |
+
if sample_id:
|
| 63 |
+
sample = next((s for s in self._samples if s.sample_id == sample_id), None)
|
| 64 |
+
if not sample:
|
| 65 |
+
raise ValueError(f"sample_id {sample_id} not found")
|
| 66 |
+
else:
|
| 67 |
+
sample = self._rng.choice(self._samples)
|
| 68 |
+
|
| 69 |
+
episode_id = str(uuid.uuid4())
|
| 70 |
+
self._state = CommitGuardState(
|
| 71 |
+
episode_id=episode_id,
|
| 72 |
+
current_sample_id=sample.sample_id,
|
| 73 |
+
step_count=0,
|
| 74 |
+
context_requests=0,
|
| 75 |
+
history=[],
|
| 76 |
+
)
|
| 77 |
+
return CommitGuardObservation(
|
| 78 |
+
episode_id=episode_id,
|
| 79 |
+
diff=sample.diff,
|
| 80 |
+
available_files=sample.available_files,
|
| 81 |
+
step_idx=0,
|
| 82 |
+
budget_remaining=5,
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
def step(self, action: CommitGuardAction) -> tuple[CommitGuardObservation, float, bool]:
|
| 86 |
+
if self._state is None:
|
| 87 |
+
_ = self.reset()
|
| 88 |
+
|
| 89 |
+
assert self._state is not None
|
| 90 |
+
next_step = self._state.step_count + 1
|
| 91 |
+
|
| 92 |
+
sample = next(s for s in self._samples if s.sample_id == self._state.current_sample_id)
|
| 93 |
+
|
| 94 |
+
context_snippets: list[ContextSnippet] = []
|
| 95 |
+
context_requests = self._state.context_requests
|
| 96 |
+
if action.action_type == "request_context":
|
| 97 |
+
context_requests += 1
|
| 98 |
+
if action.file_path and sample.files and action.file_path in sample.files:
|
| 99 |
+
content = sample.files[action.file_path]
|
| 100 |
+
lines = content.splitlines()
|
| 101 |
+
start = 1
|
| 102 |
+
end = min(len(lines), 80)
|
| 103 |
+
context_snippets = [
|
| 104 |
+
ContextSnippet(
|
| 105 |
+
file_path=action.file_path,
|
| 106 |
+
start_line=start,
|
| 107 |
+
end_line=end,
|
| 108 |
+
content="\n".join(lines[start - 1 : end]),
|
| 109 |
+
)
|
| 110 |
+
]
|
| 111 |
+
|
| 112 |
+
reward = compute_reward(
|
| 113 |
+
action=action,
|
| 114 |
+
is_vulnerable=sample.is_vulnerable,
|
| 115 |
+
cwe=sample.cwe,
|
| 116 |
+
target_file=sample.target_file,
|
| 117 |
+
cwe_keywords=self._cwe_keywords,
|
| 118 |
+
context_requests=context_requests,
|
| 119 |
+
)
|
| 120 |
+
|
| 121 |
+
done = bool(action.action_type == "verdict" or next_step >= 5)
|
| 122 |
+
|
| 123 |
+
self._state = replace(
|
| 124 |
+
self._state,
|
| 125 |
+
step_count=next_step,
|
| 126 |
+
context_requests=context_requests,
|
| 127 |
+
history=[
|
| 128 |
+
*self._state.history,
|
| 129 |
+
{
|
| 130 |
+
"step": next_step,
|
| 131 |
+
"action_type": action.action_type,
|
| 132 |
+
"parse_error": action.parse_error,
|
| 133 |
+
},
|
| 134 |
+
],
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
obs = CommitGuardObservation(
|
| 138 |
+
episode_id=self._state.episode_id,
|
| 139 |
+
diff=sample.diff,
|
| 140 |
+
available_files=sample.available_files,
|
| 141 |
+
context_snippets=context_snippets,
|
| 142 |
+
step_idx=next_step,
|
| 143 |
+
budget_remaining=max(0, 5 - next_step),
|
| 144 |
+
error=action.parse_error or (None if context_snippets else ("context_unavailable" if action.action_type == "request_context" else None)),
|
| 145 |
+
)
|
| 146 |
+
return obs, reward, done
|
| 147 |
+
|
| 148 |
+
def state(self) -> CommitGuardState:
|
| 149 |
+
if self._state is None:
|
| 150 |
+
return CommitGuardState(episode_id="", current_sample_id="", step_count=0, context_requests=0, history=[])
|
| 151 |
+
return self._state
|
commitguard_env/models.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from dataclasses import dataclass, field
|
| 4 |
+
from typing import Literal, Optional
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
ActionType = Literal["request_context", "analyze", "verdict"]
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
@dataclass(frozen=True, slots=True)
|
| 11 |
+
class CommitGuardAction:
|
| 12 |
+
action_type: ActionType
|
| 13 |
+
file_path: Optional[str] = None
|
| 14 |
+
reasoning: Optional[str] = None
|
| 15 |
+
is_vulnerable: Optional[bool] = None
|
| 16 |
+
vuln_type: Optional[str] = None
|
| 17 |
+
exploit_sketch: Optional[str] = None
|
| 18 |
+
raw_action: Optional[str] = None
|
| 19 |
+
parse_error: Optional[str] = None
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
@dataclass(frozen=True, slots=True)
|
| 23 |
+
class ContextSnippet:
|
| 24 |
+
file_path: str
|
| 25 |
+
start_line: int
|
| 26 |
+
end_line: int
|
| 27 |
+
content: str
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
@dataclass(frozen=True, slots=True)
|
| 31 |
+
class CommitGuardObservation:
|
| 32 |
+
# Cheating-prevention critical: this shape must never include ground truth.
|
| 33 |
+
episode_id: str
|
| 34 |
+
step_idx: int
|
| 35 |
+
diff: str
|
| 36 |
+
available_files: list[str]
|
| 37 |
+
context_snippets: list[ContextSnippet] = field(default_factory=list)
|
| 38 |
+
budget_remaining: int = 0
|
| 39 |
+
error: Optional[str] = None
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
@dataclass(frozen=True, slots=True)
|
| 43 |
+
class CommitGuardState:
|
| 44 |
+
episode_id: str
|
| 45 |
+
current_sample_id: str
|
| 46 |
+
step_count: int
|
| 47 |
+
context_requests: int = 0
|
| 48 |
+
history: list[dict] = field(default_factory=list)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
@dataclass(frozen=True, slots=True)
|
| 52 |
+
class DevignSample:
|
| 53 |
+
sample_id: str
|
| 54 |
+
diff: str
|
| 55 |
+
available_files: list[str]
|
| 56 |
+
# Server-only fields (must never be surfaced in Observation)
|
| 57 |
+
is_vulnerable: Optional[bool] = None
|
| 58 |
+
cwe: Optional[str] = None
|
| 59 |
+
target_file: Optional[str] = None
|
| 60 |
+
files: Optional[dict[str, str]] = None
|
| 61 |
+
|
commitguard_env/parse_action.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import re
|
| 4 |
+
from typing import Any, Optional
|
| 5 |
+
|
| 6 |
+
from .models import CommitGuardAction
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
_TAG_RE = re.compile(r"<(?P<tag>[a-zA-Z_]+)>(?P<val>.*?)</(?P=tag)>", re.DOTALL)
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def _first(tag: str, text: str) -> Optional[str]:
|
| 13 |
+
m = re.search(rf"<{re.escape(tag)}>(.*?)</{re.escape(tag)}>", text, flags=re.DOTALL)
|
| 14 |
+
if not m:
|
| 15 |
+
return None
|
| 16 |
+
return m.group(1).strip()
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def _parse_bool(v: Optional[str]) -> Optional[bool]:
|
| 20 |
+
if v is None:
|
| 21 |
+
return None
|
| 22 |
+
s = v.strip().lower()
|
| 23 |
+
if s in {"true", "1", "yes"}:
|
| 24 |
+
return True
|
| 25 |
+
if s in {"false", "0", "no"}:
|
| 26 |
+
return False
|
| 27 |
+
return None
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def parse_action(raw_action: str) -> CommitGuardAction:
|
| 31 |
+
"""
|
| 32 |
+
Parse XML-tag free-text action. Never raises.
|
| 33 |
+
|
| 34 |
+
Expected shape:
|
| 35 |
+
<action><action_type>...</action_type><fields>...</fields></action>
|
| 36 |
+
"""
|
| 37 |
+
try:
|
| 38 |
+
action_type = (_first("action_type", raw_action) or "").strip().lower()
|
| 39 |
+
if action_type not in {"request_context", "analyze", "verdict"}:
|
| 40 |
+
return CommitGuardAction(
|
| 41 |
+
action_type="analyze",
|
| 42 |
+
raw_action=raw_action,
|
| 43 |
+
parse_error="missing_or_invalid_action_type",
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
if action_type == "request_context":
|
| 47 |
+
file_path = _first("file_path", raw_action)
|
| 48 |
+
return CommitGuardAction(
|
| 49 |
+
action_type="request_context",
|
| 50 |
+
file_path=file_path,
|
| 51 |
+
raw_action=raw_action,
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
if action_type == "analyze":
|
| 55 |
+
reasoning = _first("reasoning", raw_action)
|
| 56 |
+
return CommitGuardAction(action_type="analyze", reasoning=reasoning, raw_action=raw_action)
|
| 57 |
+
|
| 58 |
+
is_vulnerable = _parse_bool(_first("is_vulnerable", raw_action))
|
| 59 |
+
vuln_type = _first("vuln_type", raw_action)
|
| 60 |
+
exploit_sketch = _first("exploit_sketch", raw_action)
|
| 61 |
+
return CommitGuardAction(
|
| 62 |
+
action_type="verdict",
|
| 63 |
+
is_vulnerable=is_vulnerable,
|
| 64 |
+
vuln_type=vuln_type,
|
| 65 |
+
exploit_sketch=exploit_sketch,
|
| 66 |
+
raw_action=raw_action,
|
| 67 |
+
)
|
| 68 |
+
except Exception as e: # defensive: model output must never crash server
|
| 69 |
+
return CommitGuardAction(
|
| 70 |
+
action_type="analyze",
|
| 71 |
+
raw_action=raw_action,
|
| 72 |
+
parse_error=f"parser_exception:{type(e).__name__}",
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def action_from_json(payload: dict[str, Any]) -> CommitGuardAction:
|
| 77 |
+
"""
|
| 78 |
+
Convenience for curl/json clients: accept either {action: "<xml>"} or
|
| 79 |
+
direct fields matching CommitGuardAction.
|
| 80 |
+
"""
|
| 81 |
+
if isinstance(payload.get("action"), str):
|
| 82 |
+
return parse_action(payload["action"])
|
| 83 |
+
|
| 84 |
+
action_type = (payload.get("action_type") or "analyze").strip().lower()
|
| 85 |
+
if action_type not in {"request_context", "analyze", "verdict"}:
|
| 86 |
+
action_type = "analyze"
|
| 87 |
+
|
| 88 |
+
return CommitGuardAction(
|
| 89 |
+
action_type=action_type, # type: ignore[arg-type]
|
| 90 |
+
file_path=payload.get("file_path"),
|
| 91 |
+
reasoning=payload.get("reasoning"),
|
| 92 |
+
is_vulnerable=payload.get("is_vulnerable"),
|
| 93 |
+
vuln_type=payload.get("vuln_type"),
|
| 94 |
+
exploit_sketch=payload.get("exploit_sketch"),
|
| 95 |
+
raw_action=None,
|
| 96 |
+
parse_error=None,
|
| 97 |
+
)
|
| 98 |
+
|
commitguard_env/reward.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from .models import CommitGuardAction
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def compute_reward(
|
| 7 |
+
*,
|
| 8 |
+
action: CommitGuardAction,
|
| 9 |
+
is_vulnerable: bool | None,
|
| 10 |
+
cwe: str | None,
|
| 11 |
+
target_file: str | None,
|
| 12 |
+
cwe_keywords: dict[str, list[str]] | None,
|
| 13 |
+
context_requests: int,
|
| 14 |
+
) -> float:
|
| 15 |
+
"""
|
| 16 |
+
Tiered RLVR reward (PRD 5.3, architecture contract).
|
| 17 |
+
|
| 18 |
+
Notes:
|
| 19 |
+
- Ground truth must remain server-only; caller passes it in.
|
| 20 |
+
- Reward is a scalar only; no label debug info.
|
| 21 |
+
"""
|
| 22 |
+
# Per-context-request penalty applies regardless of verdict.
|
| 23 |
+
reward = -0.05 * float(max(0, context_requests))
|
| 24 |
+
|
| 25 |
+
if action.parse_error:
|
| 26 |
+
return reward - 0.5
|
| 27 |
+
|
| 28 |
+
# Small CoT bonus: reward 'analyze' steps that provide substantial reasoning.
|
| 29 |
+
# This provides a tiny positive float signal to encourage thinking.
|
| 30 |
+
if action.action_type == "analyze":
|
| 31 |
+
reasoning_len = len(action.reasoning or "")
|
| 32 |
+
if reasoning_len > 50:
|
| 33 |
+
reward += min(0.05, 0.001 * (reasoning_len // 10))
|
| 34 |
+
return reward
|
| 35 |
+
|
| 36 |
+
if action.action_type != "verdict":
|
| 37 |
+
return reward
|
| 38 |
+
|
| 39 |
+
if is_vulnerable is None:
|
| 40 |
+
return reward
|
| 41 |
+
|
| 42 |
+
pred = bool(action.is_vulnerable) if action.is_vulnerable is not None else None
|
| 43 |
+
if pred is None:
|
| 44 |
+
return reward - 0.5
|
| 45 |
+
|
| 46 |
+
if pred is True and is_vulnerable is True:
|
| 47 |
+
reward += 1.0
|
| 48 |
+
# Correct CWE (Discrete 0.5)
|
| 49 |
+
if cwe and action.vuln_type and action.vuln_type.strip().upper() == cwe.strip().upper():
|
| 50 |
+
reward += 0.5
|
| 51 |
+
|
| 52 |
+
# Proportional Keyword Match (Continuous Float up to 0.5)
|
| 53 |
+
kws = (cwe_keywords or {}).get(cwe or "", []) if cwe else []
|
| 54 |
+
if kws:
|
| 55 |
+
sketch = (action.exploit_sketch or "").lower()
|
| 56 |
+
matches = sum(1 for k in kws if k.lower() in sketch)
|
| 57 |
+
# Continuous signal: reward is proportional to percentage of keywords found.
|
| 58 |
+
reward += 0.5 * (matches / len(kws))
|
| 59 |
+
return reward
|
| 60 |
+
|
| 61 |
+
if pred is True and is_vulnerable is False:
|
| 62 |
+
return reward - 1.0
|
| 63 |
+
|
| 64 |
+
if pred is False and is_vulnerable is True:
|
| 65 |
+
return reward - 0.5
|
| 66 |
+
|
| 67 |
+
if pred is False and is_vulnerable is False:
|
| 68 |
+
return reward + 1.0
|
| 69 |
+
|
| 70 |
+
return reward
|
| 71 |
+
|
commitguard_env/server.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from typing import Any
|
| 5 |
+
|
| 6 |
+
import uvicorn
|
| 7 |
+
from fastapi import FastAPI
|
| 8 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 9 |
+
from dataclasses import asdict
|
| 10 |
+
from pydantic import BaseModel
|
| 11 |
+
|
| 12 |
+
from .environment import CommitGuardEnvironment
|
| 13 |
+
from .parse_action import action_from_json, parse_action
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
DATA_PATH = Path(__file__).resolve().parent.parent / "data" / "devign_filtered.jsonl"
|
| 17 |
+
|
| 18 |
+
app = FastAPI(title="CommitGuard Env Server", version="0.1.0")
|
| 19 |
+
app.add_middleware(
|
| 20 |
+
CORSMiddleware,
|
| 21 |
+
allow_origins=["*"],
|
| 22 |
+
allow_credentials=False,
|
| 23 |
+
allow_methods=["*"],
|
| 24 |
+
allow_headers=["*"],
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
env = CommitGuardEnvironment(data_path=DATA_PATH)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class StepRequest(BaseModel):
|
| 31 |
+
# Either send `action` as raw XML text, or send structured fields (curl-friendly).
|
| 32 |
+
action: str | None = None
|
| 33 |
+
action_type: str | None = None
|
| 34 |
+
file_path: str | None = None
|
| 35 |
+
reasoning: str | None = None
|
| 36 |
+
is_vulnerable: bool | None = None
|
| 37 |
+
vuln_type: str | None = None
|
| 38 |
+
exploit_sketch: str | None = None
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
@app.get("/health")
|
| 42 |
+
def health() -> dict[str, str]:
|
| 43 |
+
return {"status": "healthy"}
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
class ResetRequest(BaseModel):
|
| 47 |
+
sample_id: str | None = None
|
| 48 |
+
|
| 49 |
+
@app.post("/reset")
|
| 50 |
+
def reset(req: ResetRequest = ResetRequest()) -> dict[str, Any]:
|
| 51 |
+
try:
|
| 52 |
+
obs = env.reset(sample_id=req.sample_id)
|
| 53 |
+
return {
|
| 54 |
+
"observation": asdict(obs),
|
| 55 |
+
"done": False,
|
| 56 |
+
"reward": 0.0,
|
| 57 |
+
}
|
| 58 |
+
except ValueError as e:
|
| 59 |
+
return {"error": str(e)}
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
@app.post("/step")
|
| 63 |
+
def step(req: StepRequest) -> dict[str, Any]:
|
| 64 |
+
if req.action is not None:
|
| 65 |
+
action = parse_action(req.action)
|
| 66 |
+
else:
|
| 67 |
+
action = action_from_json(req.model_dump(exclude_none=True))
|
| 68 |
+
obs, reward, done = env.step(action)
|
| 69 |
+
return {
|
| 70 |
+
"observation": asdict(obs),
|
| 71 |
+
"done": done,
|
| 72 |
+
"reward": reward,
|
| 73 |
+
"info": {"parse_error": action.parse_error},
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
@app.get("/state")
|
| 78 |
+
def state() -> dict[str, Any]:
|
| 79 |
+
st = env.state()
|
| 80 |
+
return {"state": asdict(st)}
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def main() -> None:
|
| 84 |
+
uvicorn.run("commitguard_env.server:app", host="0.0.0.0", port=8000, reload=False)
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
if __name__ == "__main__":
|
| 88 |
+
main()
|
| 89 |
+
|
data/cwe_keywords.json
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"CWE-119": ["buffer overflow", "out of bounds", "overflow", "bounds check", "memcpy", "strcpy", "strcat", "index out of range", "heap", "stack smash"],
|
| 3 |
+
"CWE-476": ["null pointer", "nullptr", "dereference", "null check", "segmentation fault", "null access", "uninitialized"],
|
| 4 |
+
"CWE-189": ["integer overflow", "signedness", "division by zero", "arithmetic overflow", "wrap around", "truncation", "cast", "narrowing"],
|
| 5 |
+
"CWE-20": ["input validation", "improper input", "validation bypass", "sanitization", "untrusted input", "malformed data", "missing check"],
|
| 6 |
+
"CWE-22": ["path traversal", "directory traversal", "../", "..\\", "file inclusion", "arbitrary file", "escape root", "chroot"],
|
| 7 |
+
"CWE-78": ["command injection", "os.system", "subprocess", "shell=true", "exec(", "popen", "system(", "shell command"],
|
| 8 |
+
"CWE-89": ["sql injection", "sqli", "drop table", "union select", "query concatenation", "prepared statement", "bypass login"],
|
| 9 |
+
"CWE-79": ["xss", "cross site scripting", "script tag", "innerhtml", "alert(", "javascript:", "onerror", "content injection"],
|
| 10 |
+
"CWE-OTHER": ["vulnerability", "security", "exploit", "unsafe", "flaw", "bug", "error handling", "race condition", "use after free", "double free"]
|
| 11 |
+
}
|
data/devign_filtered.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
data/devign_test.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
eval_baseline.json
ADDED
|
@@ -0,0 +1,502 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"sample_id": "187337f8b0ec0813dd3876d1efe37d415fb81c2e",
|
| 4 |
+
"pred": true,
|
| 5 |
+
"truth": true
|
| 6 |
+
},
|
| 7 |
+
{
|
| 8 |
+
"sample_id": "54c42368f57c02b0970bb32b4542f99b913908ba",
|
| 9 |
+
"pred": false,
|
| 10 |
+
"truth": true
|
| 11 |
+
},
|
| 12 |
+
{
|
| 13 |
+
"sample_id": "fd34dbea58e097609ff09cf7dcc59f74930195d3",
|
| 14 |
+
"pred": true,
|
| 15 |
+
"truth": true
|
| 16 |
+
},
|
| 17 |
+
{
|
| 18 |
+
"sample_id": "2d40564aaab3a99fe6ce00fc0fc893c02e9443ec",
|
| 19 |
+
"pred": true,
|
| 20 |
+
"truth": true
|
| 21 |
+
},
|
| 22 |
+
{
|
| 23 |
+
"sample_id": "245f7b51c0ea04fb2224b1127430a096c91aee70",
|
| 24 |
+
"pred": true,
|
| 25 |
+
"truth": false
|
| 26 |
+
},
|
| 27 |
+
{
|
| 28 |
+
"sample_id": "1c088632e98af96f9cbe8129c5d7eb7274f8d4ed",
|
| 29 |
+
"pred": true,
|
| 30 |
+
"truth": false
|
| 31 |
+
},
|
| 32 |
+
{
|
| 33 |
+
"sample_id": "8731c86d03d062ad19f098b77ab1f1bc4ad7c406",
|
| 34 |
+
"pred": true,
|
| 35 |
+
"truth": true
|
| 36 |
+
},
|
| 37 |
+
{
|
| 38 |
+
"sample_id": "f3c7d0389fe8a2792fd4c1cf151b885de03c8f62",
|
| 39 |
+
"pred": false,
|
| 40 |
+
"truth": true
|
| 41 |
+
},
|
| 42 |
+
{
|
| 43 |
+
"sample_id": "a8170e5e97ad17ca169c64ba87ae2f53850dab4c",
|
| 44 |
+
"pred": false,
|
| 45 |
+
"truth": false
|
| 46 |
+
},
|
| 47 |
+
{
|
| 48 |
+
"sample_id": "e3f5ec2b5e92706e3b807059f79b1fb5d936e567",
|
| 49 |
+
"pred": true,
|
| 50 |
+
"truth": false
|
| 51 |
+
},
|
| 52 |
+
{
|
| 53 |
+
"sample_id": "46c5874e9cd752ed8ded31af03472edd8fc3efc1",
|
| 54 |
+
"pred": true,
|
| 55 |
+
"truth": false
|
| 56 |
+
},
|
| 57 |
+
{
|
| 58 |
+
"sample_id": "2a6391232fa58f32469fb61d55343eff32a91083",
|
| 59 |
+
"pred": false,
|
| 60 |
+
"truth": true
|
| 61 |
+
},
|
| 62 |
+
{
|
| 63 |
+
"sample_id": "b3db211f3c80bb996a704d665fe275619f728bd4",
|
| 64 |
+
"pred": true,
|
| 65 |
+
"truth": false
|
| 66 |
+
},
|
| 67 |
+
{
|
| 68 |
+
"sample_id": "5029a406334ad0eaf92130e23d596e405a8a5aa0",
|
| 69 |
+
"pred": false,
|
| 70 |
+
"truth": true
|
| 71 |
+
},
|
| 72 |
+
{
|
| 73 |
+
"sample_id": "83898cce62ba25a473af6a164388105994481e9c",
|
| 74 |
+
"pred": false,
|
| 75 |
+
"truth": true
|
| 76 |
+
},
|
| 77 |
+
{
|
| 78 |
+
"sample_id": "6abc56e892c2c2500d1fc2698fa6d580b72f721b",
|
| 79 |
+
"pred": false,
|
| 80 |
+
"truth": true
|
| 81 |
+
},
|
| 82 |
+
{
|
| 83 |
+
"sample_id": "4da97120d51a4383aa96d741a2b837f8c4bbcd0b",
|
| 84 |
+
"pred": true,
|
| 85 |
+
"truth": true
|
| 86 |
+
},
|
| 87 |
+
{
|
| 88 |
+
"sample_id": "9e6636c72d8d6f0605e23ed820c8487686882b12",
|
| 89 |
+
"pred": true,
|
| 90 |
+
"truth": false
|
| 91 |
+
},
|
| 92 |
+
{
|
| 93 |
+
"sample_id": "5d47e3728bbd589701f74bb494c9c9825ba23c88",
|
| 94 |
+
"pred": false,
|
| 95 |
+
"truth": false
|
| 96 |
+
},
|
| 97 |
+
{
|
| 98 |
+
"sample_id": "dc523cd348c47372faa7271c9aab2030f94c290d",
|
| 99 |
+
"pred": false,
|
| 100 |
+
"truth": false
|
| 101 |
+
},
|
| 102 |
+
{
|
| 103 |
+
"sample_id": "3a130f4ef07f4532500473aeab43c86a3c2991c8",
|
| 104 |
+
"pred": false,
|
| 105 |
+
"truth": false
|
| 106 |
+
},
|
| 107 |
+
{
|
| 108 |
+
"sample_id": "61007b316cd71ee7333ff7a0a749a8949527575f",
|
| 109 |
+
"pred": true,
|
| 110 |
+
"truth": false
|
| 111 |
+
},
|
| 112 |
+
{
|
| 113 |
+
"sample_id": "e0e2d644096c79a71099b176d08f465f6803a8b1",
|
| 114 |
+
"pred": true,
|
| 115 |
+
"truth": true
|
| 116 |
+
},
|
| 117 |
+
{
|
| 118 |
+
"sample_id": "bea60dd7679364493a0d7f5b54316c767cf894ef",
|
| 119 |
+
"pred": true,
|
| 120 |
+
"truth": true
|
| 121 |
+
},
|
| 122 |
+
{
|
| 123 |
+
"sample_id": "a7812ae412311d7d47f8aa85656faadac9d64b56",
|
| 124 |
+
"pred": true,
|
| 125 |
+
"truth": false
|
| 126 |
+
},
|
| 127 |
+
{
|
| 128 |
+
"sample_id": "220b24c7c97dc033ceab1510549f66d0e7b52ef1",
|
| 129 |
+
"pred": false,
|
| 130 |
+
"truth": true
|
| 131 |
+
},
|
| 132 |
+
{
|
| 133 |
+
"sample_id": "74475455442398a64355428b37422d14ccc293cb",
|
| 134 |
+
"pred": false,
|
| 135 |
+
"truth": false
|
| 136 |
+
},
|
| 137 |
+
{
|
| 138 |
+
"sample_id": "c09f4cb2b3243085a86aee3c7ed4f31c77e4db87",
|
| 139 |
+
"pred": false,
|
| 140 |
+
"truth": false
|
| 141 |
+
},
|
| 142 |
+
{
|
| 143 |
+
"sample_id": "5d40097fc09fe5d34cf316a411dc27d455ac2cd0",
|
| 144 |
+
"pred": false,
|
| 145 |
+
"truth": true
|
| 146 |
+
},
|
| 147 |
+
{
|
| 148 |
+
"sample_id": "cf528b89580797050b8cf60fee6247f35531a675",
|
| 149 |
+
"pred": true,
|
| 150 |
+
"truth": false
|
| 151 |
+
},
|
| 152 |
+
{
|
| 153 |
+
"sample_id": "3ab9a2a5577d445252724af4067d2a7c8a378efa",
|
| 154 |
+
"pred": true,
|
| 155 |
+
"truth": true
|
| 156 |
+
},
|
| 157 |
+
{
|
| 158 |
+
"sample_id": "369f7de9d57e4dd2f312255fc12271d5749c0a4e",
|
| 159 |
+
"pred": true,
|
| 160 |
+
"truth": false
|
| 161 |
+
},
|
| 162 |
+
{
|
| 163 |
+
"sample_id": "4cbd6c41fa3aa901e12e8158e8d22dd8f70f7a90",
|
| 164 |
+
"pred": false,
|
| 165 |
+
"truth": false
|
| 166 |
+
},
|
| 167 |
+
{
|
| 168 |
+
"sample_id": "66dd21d50be14a355e296b769d9d99090c0207f7",
|
| 169 |
+
"pred": true,
|
| 170 |
+
"truth": true
|
| 171 |
+
},
|
| 172 |
+
{
|
| 173 |
+
"sample_id": "7bd427d801e1e3293a634d3c83beadaa90ffb911",
|
| 174 |
+
"pred": true,
|
| 175 |
+
"truth": false
|
| 176 |
+
},
|
| 177 |
+
{
|
| 178 |
+
"sample_id": "aec4b054ea36c53c8b887da99f20010133b84378",
|
| 179 |
+
"pred": true,
|
| 180 |
+
"truth": true
|
| 181 |
+
},
|
| 182 |
+
{
|
| 183 |
+
"sample_id": "a0c624e299730c8c5800375c2f5f3c6c200053ff",
|
| 184 |
+
"pred": false,
|
| 185 |
+
"truth": true
|
| 186 |
+
},
|
| 187 |
+
{
|
| 188 |
+
"sample_id": "456d60692310e7ac25cf822cc1e98192ad636ece",
|
| 189 |
+
"pred": true,
|
| 190 |
+
"truth": true
|
| 191 |
+
},
|
| 192 |
+
{
|
| 193 |
+
"sample_id": "d07bde88a52bf293c3f8846cfd162e0a57e1557c",
|
| 194 |
+
"pred": false,
|
| 195 |
+
"truth": true
|
| 196 |
+
},
|
| 197 |
+
{
|
| 198 |
+
"sample_id": "2bf3aa85f08186b8162b76e7e8efe5b5a44306a6",
|
| 199 |
+
"pred": false,
|
| 200 |
+
"truth": true
|
| 201 |
+
},
|
| 202 |
+
{
|
| 203 |
+
"sample_id": "b4ba67d9a702507793c2724e56f98e9b0f7be02b",
|
| 204 |
+
"pred": false,
|
| 205 |
+
"truth": true
|
| 206 |
+
},
|
| 207 |
+
{
|
| 208 |
+
"sample_id": "088eca28164c8cd3b72b0c3d3f9e3fe5ee5cb28f",
|
| 209 |
+
"pred": true,
|
| 210 |
+
"truth": true
|
| 211 |
+
},
|
| 212 |
+
{
|
| 213 |
+
"sample_id": "2c79288d4e0bcb8d3a8a908813fc9cc586dd7fdd",
|
| 214 |
+
"pred": false,
|
| 215 |
+
"truth": true
|
| 216 |
+
},
|
| 217 |
+
{
|
| 218 |
+
"sample_id": "ad0ebb91cd8b5fdc4a583b03645677771f420a46",
|
| 219 |
+
"pred": false,
|
| 220 |
+
"truth": true
|
| 221 |
+
},
|
| 222 |
+
{
|
| 223 |
+
"sample_id": "6c3cb02a742f0ce32a85e86738a18e3d6d711d59",
|
| 224 |
+
"pred": false,
|
| 225 |
+
"truth": true
|
| 226 |
+
},
|
| 227 |
+
{
|
| 228 |
+
"sample_id": "3a3b8502e6f0c8d30865c5f36d2c3ae4114000b5",
|
| 229 |
+
"pred": true,
|
| 230 |
+
"truth": true
|
| 231 |
+
},
|
| 232 |
+
{
|
| 233 |
+
"sample_id": "c3e10c7b4377c1cbc0a4fbc12312c2cf41c0cda7",
|
| 234 |
+
"pred": true,
|
| 235 |
+
"truth": true
|
| 236 |
+
},
|
| 237 |
+
{
|
| 238 |
+
"sample_id": "7385aed20db5d83979f683b9d0048674411e963c",
|
| 239 |
+
"pred": true,
|
| 240 |
+
"truth": false
|
| 241 |
+
},
|
| 242 |
+
{
|
| 243 |
+
"sample_id": "b45c03f585ea9bb1af76c73e82195418c294919d",
|
| 244 |
+
"pred": true,
|
| 245 |
+
"truth": true
|
| 246 |
+
},
|
| 247 |
+
{
|
| 248 |
+
"sample_id": "0ecca7a49f8e254c12a3a1de048d738bfbb614c6",
|
| 249 |
+
"pred": false,
|
| 250 |
+
"truth": true
|
| 251 |
+
},
|
| 252 |
+
{
|
| 253 |
+
"sample_id": "1d16a1cf99488f16492b1bb48e023f4da8377e07",
|
| 254 |
+
"pred": false,
|
| 255 |
+
"truth": false
|
| 256 |
+
},
|
| 257 |
+
{
|
| 258 |
+
"sample_id": "2d1cd6c7a91a4beb99a0c3a21be529222a708545",
|
| 259 |
+
"pred": false,
|
| 260 |
+
"truth": true
|
| 261 |
+
},
|
| 262 |
+
{
|
| 263 |
+
"sample_id": "920639cab0fe28d003c90b53bd8b66e8fb333bdd",
|
| 264 |
+
"pred": true,
|
| 265 |
+
"truth": false
|
| 266 |
+
},
|
| 267 |
+
{
|
| 268 |
+
"sample_id": "196a778428989217b82de042725dc8eb29c8f8d8",
|
| 269 |
+
"pred": true,
|
| 270 |
+
"truth": true
|
| 271 |
+
},
|
| 272 |
+
{
|
| 273 |
+
"sample_id": "72cf2d4f0e181d0d3a3122e04129c58a95da713e",
|
| 274 |
+
"pred": false,
|
| 275 |
+
"truth": false
|
| 276 |
+
},
|
| 277 |
+
{
|
| 278 |
+
"sample_id": "2884cf5b934808f547b5268a51be631805c25857",
|
| 279 |
+
"pred": false,
|
| 280 |
+
"truth": false
|
| 281 |
+
},
|
| 282 |
+
{
|
| 283 |
+
"sample_id": "3c529d935923a70519557d420db1d5a09a65086a",
|
| 284 |
+
"pred": false,
|
| 285 |
+
"truth": false
|
| 286 |
+
},
|
| 287 |
+
{
|
| 288 |
+
"sample_id": "1ec26c757d5996468afcc0dced4fad04139574b3",
|
| 289 |
+
"pred": true,
|
| 290 |
+
"truth": false
|
| 291 |
+
},
|
| 292 |
+
{
|
| 293 |
+
"sample_id": "9f61abc8111c7c43f49ca012e957a108b9cc7610",
|
| 294 |
+
"pred": false,
|
| 295 |
+
"truth": false
|
| 296 |
+
},
|
| 297 |
+
{
|
| 298 |
+
"sample_id": "e1b8271949d3b70e820b8e08c542ad1586c96f9d",
|
| 299 |
+
"pred": true,
|
| 300 |
+
"truth": false
|
| 301 |
+
},
|
| 302 |
+
{
|
| 303 |
+
"sample_id": "8297be80f7cf71e09617669a8bd8b2836dcfd4c3",
|
| 304 |
+
"pred": true,
|
| 305 |
+
"truth": false
|
| 306 |
+
},
|
| 307 |
+
{
|
| 308 |
+
"sample_id": "2bf9febc95e5bcef8edb10ebc967325917b9c958",
|
| 309 |
+
"pred": false,
|
| 310 |
+
"truth": true
|
| 311 |
+
},
|
| 312 |
+
{
|
| 313 |
+
"sample_id": "1bb650420021ced718d550559034a5147c053068",
|
| 314 |
+
"pred": true,
|
| 315 |
+
"truth": false
|
| 316 |
+
},
|
| 317 |
+
{
|
| 318 |
+
"sample_id": "a307d59434ba78b97544b42b8cfd24a1b62e39a6",
|
| 319 |
+
"pred": true,
|
| 320 |
+
"truth": false
|
| 321 |
+
},
|
| 322 |
+
{
|
| 323 |
+
"sample_id": "08844473820c93541fc47bdfeae0f2cc88cfab59",
|
| 324 |
+
"pred": true,
|
| 325 |
+
"truth": false
|
| 326 |
+
},
|
| 327 |
+
{
|
| 328 |
+
"sample_id": "568e18b15e2ddf494fd8926707d34ca08c8edce5",
|
| 329 |
+
"pred": false,
|
| 330 |
+
"truth": true
|
| 331 |
+
},
|
| 332 |
+
{
|
| 333 |
+
"sample_id": "f35e44e7645edbb08e35b111c10c2fc57e2905c7",
|
| 334 |
+
"pred": false,
|
| 335 |
+
"truth": true
|
| 336 |
+
},
|
| 337 |
+
{
|
| 338 |
+
"sample_id": "4bfe4478d17679464a2aaa91ed703522ed9af8a0",
|
| 339 |
+
"pred": false,
|
| 340 |
+
"truth": false
|
| 341 |
+
},
|
| 342 |
+
{
|
| 343 |
+
"sample_id": "f6774f905fb3cfdc319523ac640be30b14c1bc55",
|
| 344 |
+
"pred": true,
|
| 345 |
+
"truth": true
|
| 346 |
+
},
|
| 347 |
+
{
|
| 348 |
+
"sample_id": "8b33d9eeba91422ee2d73b6936ad57262d18cf5a",
|
| 349 |
+
"pred": true,
|
| 350 |
+
"truth": true
|
| 351 |
+
},
|
| 352 |
+
{
|
| 353 |
+
"sample_id": "089da572b956ef0f8f5b8d5917358e07892a77c2",
|
| 354 |
+
"pred": false,
|
| 355 |
+
"truth": true
|
| 356 |
+
},
|
| 357 |
+
{
|
| 358 |
+
"sample_id": "cb08687180683a755d0fe9d425280d0e4d1e6db2",
|
| 359 |
+
"pred": true,
|
| 360 |
+
"truth": true
|
| 361 |
+
},
|
| 362 |
+
{
|
| 363 |
+
"sample_id": "b6fcf32d9b851a83dedcb609091236b97cc4a985",
|
| 364 |
+
"pred": false,
|
| 365 |
+
"truth": false
|
| 366 |
+
},
|
| 367 |
+
{
|
| 368 |
+
"sample_id": "9ef91a677110ec200d7b2904fc4bcae5a77329ad",
|
| 369 |
+
"pred": true,
|
| 370 |
+
"truth": false
|
| 371 |
+
},
|
| 372 |
+
{
|
| 373 |
+
"sample_id": "f090c9d4ad5812fb92843d6470a1111c15190c4c",
|
| 374 |
+
"pred": false,
|
| 375 |
+
"truth": false
|
| 376 |
+
},
|
| 377 |
+
{
|
| 378 |
+
"sample_id": "6f2d8978728c48ca46f5c01835438508aace5c64",
|
| 379 |
+
"pred": true,
|
| 380 |
+
"truth": true
|
| 381 |
+
},
|
| 382 |
+
{
|
| 383 |
+
"sample_id": "6e0d8677cb443e7408c0b7a25a93c6596d7fa380",
|
| 384 |
+
"pred": false,
|
| 385 |
+
"truth": false
|
| 386 |
+
},
|
| 387 |
+
{
|
| 388 |
+
"sample_id": "f6b7f72461673e4d398b1edf9ed2a7fe70d99c47",
|
| 389 |
+
"pred": false,
|
| 390 |
+
"truth": false
|
| 391 |
+
},
|
| 392 |
+
{
|
| 393 |
+
"sample_id": "b3db211f3c80bb996a704d665fe275619f728bd4",
|
| 394 |
+
"pred": false,
|
| 395 |
+
"truth": false
|
| 396 |
+
},
|
| 397 |
+
{
|
| 398 |
+
"sample_id": "f51074cdc6e750daa3b6df727d83449a7e42b391",
|
| 399 |
+
"pred": true,
|
| 400 |
+
"truth": true
|
| 401 |
+
},
|
| 402 |
+
{
|
| 403 |
+
"sample_id": "297a3646c2947ee64a6d42ca264039732c6218e0",
|
| 404 |
+
"pred": true,
|
| 405 |
+
"truth": true
|
| 406 |
+
},
|
| 407 |
+
{
|
| 408 |
+
"sample_id": "6e0d8c06c7af61859e8d7bc2351a607d8abeab75",
|
| 409 |
+
"pred": true,
|
| 410 |
+
"truth": false
|
| 411 |
+
},
|
| 412 |
+
{
|
| 413 |
+
"sample_id": "1c02e2a17104fe7fc11893125864dc0daf1e6d5b",
|
| 414 |
+
"pred": true,
|
| 415 |
+
"truth": true
|
| 416 |
+
},
|
| 417 |
+
{
|
| 418 |
+
"sample_id": "a8170e5e97ad17ca169c64ba87ae2f53850dab4c",
|
| 419 |
+
"pred": true,
|
| 420 |
+
"truth": false
|
| 421 |
+
},
|
| 422 |
+
{
|
| 423 |
+
"sample_id": "26a83ad0e793465b74a8b06a65f2f6fdc5615413",
|
| 424 |
+
"pred": true,
|
| 425 |
+
"truth": false
|
| 426 |
+
},
|
| 427 |
+
{
|
| 428 |
+
"sample_id": "3b99e00c7549ccad90c57b5bcd6e3456650a994a",
|
| 429 |
+
"pred": true,
|
| 430 |
+
"truth": true
|
| 431 |
+
},
|
| 432 |
+
{
|
| 433 |
+
"sample_id": "0c8f86ea98945678622c6e4b070c4218a53a0d19",
|
| 434 |
+
"pred": false,
|
| 435 |
+
"truth": true
|
| 436 |
+
},
|
| 437 |
+
{
|
| 438 |
+
"sample_id": "87e8788680e16c51f6048af26f3f7830c35207a5",
|
| 439 |
+
"pred": true,
|
| 440 |
+
"truth": false
|
| 441 |
+
},
|
| 442 |
+
{
|
| 443 |
+
"sample_id": "61007b316cd71ee7333ff7a0a749a8949527575f",
|
| 444 |
+
"pred": false,
|
| 445 |
+
"truth": false
|
| 446 |
+
},
|
| 447 |
+
{
|
| 448 |
+
"sample_id": "1ffc266539d443f83d5eb487593be50ef496f09e",
|
| 449 |
+
"pred": false,
|
| 450 |
+
"truth": false
|
| 451 |
+
},
|
| 452 |
+
{
|
| 453 |
+
"sample_id": "b23046abe78f48498a423b802d6d86ba0172d57f",
|
| 454 |
+
"pred": true,
|
| 455 |
+
"truth": false
|
| 456 |
+
},
|
| 457 |
+
{
|
| 458 |
+
"sample_id": "a625e13208ad0ebf1554aa73c9bf41452520f176",
|
| 459 |
+
"pred": false,
|
| 460 |
+
"truth": false
|
| 461 |
+
},
|
| 462 |
+
{
|
| 463 |
+
"sample_id": "a4c7a5ea27050a28625eabf1ba98cfef9ac6620d",
|
| 464 |
+
"pred": false,
|
| 465 |
+
"truth": false
|
| 466 |
+
},
|
| 467 |
+
{
|
| 468 |
+
"sample_id": "4c9080a7ef18ad71fb0a75c8d1c1803edd780edd",
|
| 469 |
+
"pred": true,
|
| 470 |
+
"truth": false
|
| 471 |
+
},
|
| 472 |
+
{
|
| 473 |
+
"sample_id": "4cad3867b6df2c0826ae508a9fe15dd0b9d8936a",
|
| 474 |
+
"pred": true,
|
| 475 |
+
"truth": true
|
| 476 |
+
},
|
| 477 |
+
{
|
| 478 |
+
"sample_id": "0c9ab5ef9c1ee852c80c859c9e07efe8730b57ed",
|
| 479 |
+
"pred": false,
|
| 480 |
+
"truth": true
|
| 481 |
+
},
|
| 482 |
+
{
|
| 483 |
+
"sample_id": "6f2d8978728c48ca46f5c01835438508aace5c64",
|
| 484 |
+
"pred": true,
|
| 485 |
+
"truth": true
|
| 486 |
+
},
|
| 487 |
+
{
|
| 488 |
+
"sample_id": "7ec1e5ea4bd0700fa48da86bffa2fcc6146c410a",
|
| 489 |
+
"pred": true,
|
| 490 |
+
"truth": false
|
| 491 |
+
},
|
| 492 |
+
{
|
| 493 |
+
"sample_id": "d9bce9d99f4656ae0b0127f7472db9067b8f84ab",
|
| 494 |
+
"pred": true,
|
| 495 |
+
"truth": true
|
| 496 |
+
},
|
| 497 |
+
{
|
| 498 |
+
"sample_id": "206ab6e090eeddce71372041454d50d93a63017d",
|
| 499 |
+
"pred": false,
|
| 500 |
+
"truth": false
|
| 501 |
+
}
|
| 502 |
+
]
|
eval_results_mock.json
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"summary": {
|
| 3 |
+
"total_samples": 2,
|
| 4 |
+
"overall_accuracy": 1.0,
|
| 5 |
+
"cwe_breakdown": {
|
| 6 |
+
"CWE-89": {
|
| 7 |
+
"accuracy": 1.0,
|
| 8 |
+
"count": 2
|
| 9 |
+
}
|
| 10 |
+
}
|
| 11 |
+
},
|
| 12 |
+
"results": [
|
| 13 |
+
{
|
| 14 |
+
"sample_id": "synthetic-00100",
|
| 15 |
+
"gt_vulnerable": true,
|
| 16 |
+
"gt_cwe": "CWE-89",
|
| 17 |
+
"final_verdict": true,
|
| 18 |
+
"is_correct": true,
|
| 19 |
+
"total_reward": 0.8999999999999999,
|
| 20 |
+
"history": [
|
| 21 |
+
{
|
| 22 |
+
"step": 0,
|
| 23 |
+
"model_output": "<action><action_type>request_context</action_type><file_path>auth.c</file_path></action>",
|
| 24 |
+
"parsed_action": "<action><action_type>request_context</action_type><file_path>auth.c</file_path></action>",
|
| 25 |
+
"reward": -0.05,
|
| 26 |
+
"observation": {
|
| 27 |
+
"episode_id": "c98a9f07-18fe-40a3-8c65-161a46fdabbd",
|
| 28 |
+
"step_idx": 1,
|
| 29 |
+
"diff": "--- a/db.py\n+++ b/db.py\n@@\n- cursor.execute(\"SELECT * FROM users WHERE id = %s\", (user_id,))\n+ cursor.execute(f\"SELECT * FROM users WHERE id = {user_id}\")\n",
|
| 30 |
+
"available_files": [
|
| 31 |
+
"db.py"
|
| 32 |
+
],
|
| 33 |
+
"context_snippets": [],
|
| 34 |
+
"budget_remaining": 4,
|
| 35 |
+
"error": "context_unavailable"
|
| 36 |
+
}
|
| 37 |
+
},
|
| 38 |
+
{
|
| 39 |
+
"step": 1,
|
| 40 |
+
"model_output": "<action><action_type>verdict</action_type><is_vulnerable>true</is_vulnerable><vuln_type>CWE-89</vuln_type><exploit_sketch>SQL injection in user_id</exploit_sketch></action>",
|
| 41 |
+
"parsed_action": "<action><action_type>verdict</action_type><is_vulnerable>true</is_vulnerable><vuln_type>CWE-89</vuln_type><exploit_sketch>SQL injection in user_id</exploit_sketch></action>",
|
| 42 |
+
"reward": 0.95,
|
| 43 |
+
"observation": {
|
| 44 |
+
"episode_id": "c98a9f07-18fe-40a3-8c65-161a46fdabbd",
|
| 45 |
+
"step_idx": 2,
|
| 46 |
+
"diff": "--- a/db.py\n+++ b/db.py\n@@\n- cursor.execute(\"SELECT * FROM users WHERE id = %s\", (user_id,))\n+ cursor.execute(f\"SELECT * FROM users WHERE id = {user_id}\")\n",
|
| 47 |
+
"available_files": [
|
| 48 |
+
"db.py"
|
| 49 |
+
],
|
| 50 |
+
"context_snippets": [],
|
| 51 |
+
"budget_remaining": 3,
|
| 52 |
+
"error": null
|
| 53 |
+
}
|
| 54 |
+
}
|
| 55 |
+
]
|
| 56 |
+
},
|
| 57 |
+
{
|
| 58 |
+
"sample_id": "synthetic-00101",
|
| 59 |
+
"gt_vulnerable": true,
|
| 60 |
+
"gt_cwe": "CWE-89",
|
| 61 |
+
"final_verdict": true,
|
| 62 |
+
"is_correct": true,
|
| 63 |
+
"total_reward": 0.8999999999999999,
|
| 64 |
+
"history": [
|
| 65 |
+
{
|
| 66 |
+
"step": 0,
|
| 67 |
+
"model_output": "<action><action_type>request_context</action_type><file_path>auth.c</file_path></action>",
|
| 68 |
+
"parsed_action": "<action><action_type>request_context</action_type><file_path>auth.c</file_path></action>",
|
| 69 |
+
"reward": -0.05,
|
| 70 |
+
"observation": {
|
| 71 |
+
"episode_id": "299ca2fd-e3e6-4bac-b8a2-d7404a52e07d",
|
| 72 |
+
"step_idx": 1,
|
| 73 |
+
"diff": "--- a/db.py\n+++ b/db.py\n@@\n- cursor.execute(\"SELECT * FROM users WHERE id = %s\", (user_id,))\n+ cursor.execute(f\"SELECT * FROM users WHERE id = {user_id}\")\n",
|
| 74 |
+
"available_files": [
|
| 75 |
+
"db.py"
|
| 76 |
+
],
|
| 77 |
+
"context_snippets": [],
|
| 78 |
+
"budget_remaining": 4,
|
| 79 |
+
"error": "context_unavailable"
|
| 80 |
+
}
|
| 81 |
+
},
|
| 82 |
+
{
|
| 83 |
+
"step": 1,
|
| 84 |
+
"model_output": "<action><action_type>verdict</action_type><is_vulnerable>true</is_vulnerable><vuln_type>CWE-89</vuln_type><exploit_sketch>SQL injection in user_id</exploit_sketch></action>",
|
| 85 |
+
"parsed_action": "<action><action_type>verdict</action_type><is_vulnerable>true</is_vulnerable><vuln_type>CWE-89</vuln_type><exploit_sketch>SQL injection in user_id</exploit_sketch></action>",
|
| 86 |
+
"reward": 0.95,
|
| 87 |
+
"observation": {
|
| 88 |
+
"episode_id": "299ca2fd-e3e6-4bac-b8a2-d7404a52e07d",
|
| 89 |
+
"step_idx": 2,
|
| 90 |
+
"diff": "--- a/db.py\n+++ b/db.py\n@@\n- cursor.execute(\"SELECT * FROM users WHERE id = %s\", (user_id,))\n+ cursor.execute(f\"SELECT * FROM users WHERE id = {user_id}\")\n",
|
| 91 |
+
"available_files": [
|
| 92 |
+
"db.py"
|
| 93 |
+
],
|
| 94 |
+
"context_snippets": [],
|
| 95 |
+
"budget_remaining": 3,
|
| 96 |
+
"error": null
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
]
|
| 100 |
+
}
|
| 101 |
+
]
|
| 102 |
+
}
|
eval_trained.json
ADDED
|
@@ -0,0 +1,502 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"sample_id": "187337f8b0ec0813dd3876d1efe37d415fb81c2e",
|
| 4 |
+
"pred": true,
|
| 5 |
+
"truth": true
|
| 6 |
+
},
|
| 7 |
+
{
|
| 8 |
+
"sample_id": "54c42368f57c02b0970bb32b4542f99b913908ba",
|
| 9 |
+
"pred": true,
|
| 10 |
+
"truth": true
|
| 11 |
+
},
|
| 12 |
+
{
|
| 13 |
+
"sample_id": "fd34dbea58e097609ff09cf7dcc59f74930195d3",
|
| 14 |
+
"pred": true,
|
| 15 |
+
"truth": true
|
| 16 |
+
},
|
| 17 |
+
{
|
| 18 |
+
"sample_id": "2d40564aaab3a99fe6ce00fc0fc893c02e9443ec",
|
| 19 |
+
"pred": true,
|
| 20 |
+
"truth": true
|
| 21 |
+
},
|
| 22 |
+
{
|
| 23 |
+
"sample_id": "245f7b51c0ea04fb2224b1127430a096c91aee70",
|
| 24 |
+
"pred": false,
|
| 25 |
+
"truth": false
|
| 26 |
+
},
|
| 27 |
+
{
|
| 28 |
+
"sample_id": "1c088632e98af96f9cbe8129c5d7eb7274f8d4ed",
|
| 29 |
+
"pred": false,
|
| 30 |
+
"truth": false
|
| 31 |
+
},
|
| 32 |
+
{
|
| 33 |
+
"sample_id": "8731c86d03d062ad19f098b77ab1f1bc4ad7c406",
|
| 34 |
+
"pred": true,
|
| 35 |
+
"truth": true
|
| 36 |
+
},
|
| 37 |
+
{
|
| 38 |
+
"sample_id": "f3c7d0389fe8a2792fd4c1cf151b885de03c8f62",
|
| 39 |
+
"pred": true,
|
| 40 |
+
"truth": true
|
| 41 |
+
},
|
| 42 |
+
{
|
| 43 |
+
"sample_id": "a8170e5e97ad17ca169c64ba87ae2f53850dab4c",
|
| 44 |
+
"pred": true,
|
| 45 |
+
"truth": false
|
| 46 |
+
},
|
| 47 |
+
{
|
| 48 |
+
"sample_id": "e3f5ec2b5e92706e3b807059f79b1fb5d936e567",
|
| 49 |
+
"pred": true,
|
| 50 |
+
"truth": false
|
| 51 |
+
},
|
| 52 |
+
{
|
| 53 |
+
"sample_id": "46c5874e9cd752ed8ded31af03472edd8fc3efc1",
|
| 54 |
+
"pred": false,
|
| 55 |
+
"truth": false
|
| 56 |
+
},
|
| 57 |
+
{
|
| 58 |
+
"sample_id": "2a6391232fa58f32469fb61d55343eff32a91083",
|
| 59 |
+
"pred": true,
|
| 60 |
+
"truth": true
|
| 61 |
+
},
|
| 62 |
+
{
|
| 63 |
+
"sample_id": "b3db211f3c80bb996a704d665fe275619f728bd4",
|
| 64 |
+
"pred": true,
|
| 65 |
+
"truth": false
|
| 66 |
+
},
|
| 67 |
+
{
|
| 68 |
+
"sample_id": "5029a406334ad0eaf92130e23d596e405a8a5aa0",
|
| 69 |
+
"pred": true,
|
| 70 |
+
"truth": true
|
| 71 |
+
},
|
| 72 |
+
{
|
| 73 |
+
"sample_id": "83898cce62ba25a473af6a164388105994481e9c",
|
| 74 |
+
"pred": true,
|
| 75 |
+
"truth": true
|
| 76 |
+
},
|
| 77 |
+
{
|
| 78 |
+
"sample_id": "6abc56e892c2c2500d1fc2698fa6d580b72f721b",
|
| 79 |
+
"pred": true,
|
| 80 |
+
"truth": true
|
| 81 |
+
},
|
| 82 |
+
{
|
| 83 |
+
"sample_id": "4da97120d51a4383aa96d741a2b837f8c4bbcd0b",
|
| 84 |
+
"pred": true,
|
| 85 |
+
"truth": true
|
| 86 |
+
},
|
| 87 |
+
{
|
| 88 |
+
"sample_id": "9e6636c72d8d6f0605e23ed820c8487686882b12",
|
| 89 |
+
"pred": true,
|
| 90 |
+
"truth": false
|
| 91 |
+
},
|
| 92 |
+
{
|
| 93 |
+
"sample_id": "5d47e3728bbd589701f74bb494c9c9825ba23c88",
|
| 94 |
+
"pred": false,
|
| 95 |
+
"truth": false
|
| 96 |
+
},
|
| 97 |
+
{
|
| 98 |
+
"sample_id": "dc523cd348c47372faa7271c9aab2030f94c290d",
|
| 99 |
+
"pred": true,
|
| 100 |
+
"truth": false
|
| 101 |
+
},
|
| 102 |
+
{
|
| 103 |
+
"sample_id": "3a130f4ef07f4532500473aeab43c86a3c2991c8",
|
| 104 |
+
"pred": false,
|
| 105 |
+
"truth": false
|
| 106 |
+
},
|
| 107 |
+
{
|
| 108 |
+
"sample_id": "61007b316cd71ee7333ff7a0a749a8949527575f",
|
| 109 |
+
"pred": false,
|
| 110 |
+
"truth": false
|
| 111 |
+
},
|
| 112 |
+
{
|
| 113 |
+
"sample_id": "e0e2d644096c79a71099b176d08f465f6803a8b1",
|
| 114 |
+
"pred": false,
|
| 115 |
+
"truth": true
|
| 116 |
+
},
|
| 117 |
+
{
|
| 118 |
+
"sample_id": "bea60dd7679364493a0d7f5b54316c767cf894ef",
|
| 119 |
+
"pred": false,
|
| 120 |
+
"truth": true
|
| 121 |
+
},
|
| 122 |
+
{
|
| 123 |
+
"sample_id": "a7812ae412311d7d47f8aa85656faadac9d64b56",
|
| 124 |
+
"pred": false,
|
| 125 |
+
"truth": false
|
| 126 |
+
},
|
| 127 |
+
{
|
| 128 |
+
"sample_id": "220b24c7c97dc033ceab1510549f66d0e7b52ef1",
|
| 129 |
+
"pred": true,
|
| 130 |
+
"truth": true
|
| 131 |
+
},
|
| 132 |
+
{
|
| 133 |
+
"sample_id": "74475455442398a64355428b37422d14ccc293cb",
|
| 134 |
+
"pred": false,
|
| 135 |
+
"truth": false
|
| 136 |
+
},
|
| 137 |
+
{
|
| 138 |
+
"sample_id": "c09f4cb2b3243085a86aee3c7ed4f31c77e4db87",
|
| 139 |
+
"pred": false,
|
| 140 |
+
"truth": false
|
| 141 |
+
},
|
| 142 |
+
{
|
| 143 |
+
"sample_id": "5d40097fc09fe5d34cf316a411dc27d455ac2cd0",
|
| 144 |
+
"pred": true,
|
| 145 |
+
"truth": true
|
| 146 |
+
},
|
| 147 |
+
{
|
| 148 |
+
"sample_id": "cf528b89580797050b8cf60fee6247f35531a675",
|
| 149 |
+
"pred": false,
|
| 150 |
+
"truth": false
|
| 151 |
+
},
|
| 152 |
+
{
|
| 153 |
+
"sample_id": "3ab9a2a5577d445252724af4067d2a7c8a378efa",
|
| 154 |
+
"pred": true,
|
| 155 |
+
"truth": true
|
| 156 |
+
},
|
| 157 |
+
{
|
| 158 |
+
"sample_id": "369f7de9d57e4dd2f312255fc12271d5749c0a4e",
|
| 159 |
+
"pred": false,
|
| 160 |
+
"truth": false
|
| 161 |
+
},
|
| 162 |
+
{
|
| 163 |
+
"sample_id": "4cbd6c41fa3aa901e12e8158e8d22dd8f70f7a90",
|
| 164 |
+
"pred": false,
|
| 165 |
+
"truth": false
|
| 166 |
+
},
|
| 167 |
+
{
|
| 168 |
+
"sample_id": "66dd21d50be14a355e296b769d9d99090c0207f7",
|
| 169 |
+
"pred": true,
|
| 170 |
+
"truth": true
|
| 171 |
+
},
|
| 172 |
+
{
|
| 173 |
+
"sample_id": "7bd427d801e1e3293a634d3c83beadaa90ffb911",
|
| 174 |
+
"pred": false,
|
| 175 |
+
"truth": false
|
| 176 |
+
},
|
| 177 |
+
{
|
| 178 |
+
"sample_id": "aec4b054ea36c53c8b887da99f20010133b84378",
|
| 179 |
+
"pred": false,
|
| 180 |
+
"truth": true
|
| 181 |
+
},
|
| 182 |
+
{
|
| 183 |
+
"sample_id": "a0c624e299730c8c5800375c2f5f3c6c200053ff",
|
| 184 |
+
"pred": true,
|
| 185 |
+
"truth": true
|
| 186 |
+
},
|
| 187 |
+
{
|
| 188 |
+
"sample_id": "456d60692310e7ac25cf822cc1e98192ad636ece",
|
| 189 |
+
"pred": false,
|
| 190 |
+
"truth": true
|
| 191 |
+
},
|
| 192 |
+
{
|
| 193 |
+
"sample_id": "d07bde88a52bf293c3f8846cfd162e0a57e1557c",
|
| 194 |
+
"pred": true,
|
| 195 |
+
"truth": true
|
| 196 |
+
},
|
| 197 |
+
{
|
| 198 |
+
"sample_id": "2bf3aa85f08186b8162b76e7e8efe5b5a44306a6",
|
| 199 |
+
"pred": true,
|
| 200 |
+
"truth": true
|
| 201 |
+
},
|
| 202 |
+
{
|
| 203 |
+
"sample_id": "b4ba67d9a702507793c2724e56f98e9b0f7be02b",
|
| 204 |
+
"pred": true,
|
| 205 |
+
"truth": true
|
| 206 |
+
},
|
| 207 |
+
{
|
| 208 |
+
"sample_id": "088eca28164c8cd3b72b0c3d3f9e3fe5ee5cb28f",
|
| 209 |
+
"pred": true,
|
| 210 |
+
"truth": true
|
| 211 |
+
},
|
| 212 |
+
{
|
| 213 |
+
"sample_id": "2c79288d4e0bcb8d3a8a908813fc9cc586dd7fdd",
|
| 214 |
+
"pred": true,
|
| 215 |
+
"truth": true
|
| 216 |
+
},
|
| 217 |
+
{
|
| 218 |
+
"sample_id": "ad0ebb91cd8b5fdc4a583b03645677771f420a46",
|
| 219 |
+
"pred": false,
|
| 220 |
+
"truth": true
|
| 221 |
+
},
|
| 222 |
+
{
|
| 223 |
+
"sample_id": "6c3cb02a742f0ce32a85e86738a18e3d6d711d59",
|
| 224 |
+
"pred": true,
|
| 225 |
+
"truth": true
|
| 226 |
+
},
|
| 227 |
+
{
|
| 228 |
+
"sample_id": "3a3b8502e6f0c8d30865c5f36d2c3ae4114000b5",
|
| 229 |
+
"pred": true,
|
| 230 |
+
"truth": true
|
| 231 |
+
},
|
| 232 |
+
{
|
| 233 |
+
"sample_id": "c3e10c7b4377c1cbc0a4fbc12312c2cf41c0cda7",
|
| 234 |
+
"pred": false,
|
| 235 |
+
"truth": true
|
| 236 |
+
},
|
| 237 |
+
{
|
| 238 |
+
"sample_id": "7385aed20db5d83979f683b9d0048674411e963c",
|
| 239 |
+
"pred": false,
|
| 240 |
+
"truth": false
|
| 241 |
+
},
|
| 242 |
+
{
|
| 243 |
+
"sample_id": "b45c03f585ea9bb1af76c73e82195418c294919d",
|
| 244 |
+
"pred": false,
|
| 245 |
+
"truth": true
|
| 246 |
+
},
|
| 247 |
+
{
|
| 248 |
+
"sample_id": "0ecca7a49f8e254c12a3a1de048d738bfbb614c6",
|
| 249 |
+
"pred": true,
|
| 250 |
+
"truth": true
|
| 251 |
+
},
|
| 252 |
+
{
|
| 253 |
+
"sample_id": "1d16a1cf99488f16492b1bb48e023f4da8377e07",
|
| 254 |
+
"pred": false,
|
| 255 |
+
"truth": false
|
| 256 |
+
},
|
| 257 |
+
{
|
| 258 |
+
"sample_id": "2d1cd6c7a91a4beb99a0c3a21be529222a708545",
|
| 259 |
+
"pred": true,
|
| 260 |
+
"truth": true
|
| 261 |
+
},
|
| 262 |
+
{
|
| 263 |
+
"sample_id": "920639cab0fe28d003c90b53bd8b66e8fb333bdd",
|
| 264 |
+
"pred": false,
|
| 265 |
+
"truth": false
|
| 266 |
+
},
|
| 267 |
+
{
|
| 268 |
+
"sample_id": "196a778428989217b82de042725dc8eb29c8f8d8",
|
| 269 |
+
"pred": true,
|
| 270 |
+
"truth": true
|
| 271 |
+
},
|
| 272 |
+
{
|
| 273 |
+
"sample_id": "72cf2d4f0e181d0d3a3122e04129c58a95da713e",
|
| 274 |
+
"pred": true,
|
| 275 |
+
"truth": false
|
| 276 |
+
},
|
| 277 |
+
{
|
| 278 |
+
"sample_id": "2884cf5b934808f547b5268a51be631805c25857",
|
| 279 |
+
"pred": false,
|
| 280 |
+
"truth": false
|
| 281 |
+
},
|
| 282 |
+
{
|
| 283 |
+
"sample_id": "3c529d935923a70519557d420db1d5a09a65086a",
|
| 284 |
+
"pred": false,
|
| 285 |
+
"truth": false
|
| 286 |
+
},
|
| 287 |
+
{
|
| 288 |
+
"sample_id": "1ec26c757d5996468afcc0dced4fad04139574b3",
|
| 289 |
+
"pred": true,
|
| 290 |
+
"truth": false
|
| 291 |
+
},
|
| 292 |
+
{
|
| 293 |
+
"sample_id": "9f61abc8111c7c43f49ca012e957a108b9cc7610",
|
| 294 |
+
"pred": true,
|
| 295 |
+
"truth": false
|
| 296 |
+
},
|
| 297 |
+
{
|
| 298 |
+
"sample_id": "e1b8271949d3b70e820b8e08c542ad1586c96f9d",
|
| 299 |
+
"pred": false,
|
| 300 |
+
"truth": false
|
| 301 |
+
},
|
| 302 |
+
{
|
| 303 |
+
"sample_id": "8297be80f7cf71e09617669a8bd8b2836dcfd4c3",
|
| 304 |
+
"pred": true,
|
| 305 |
+
"truth": false
|
| 306 |
+
},
|
| 307 |
+
{
|
| 308 |
+
"sample_id": "2bf9febc95e5bcef8edb10ebc967325917b9c958",
|
| 309 |
+
"pred": false,
|
| 310 |
+
"truth": true
|
| 311 |
+
},
|
| 312 |
+
{
|
| 313 |
+
"sample_id": "1bb650420021ced718d550559034a5147c053068",
|
| 314 |
+
"pred": false,
|
| 315 |
+
"truth": false
|
| 316 |
+
},
|
| 317 |
+
{
|
| 318 |
+
"sample_id": "a307d59434ba78b97544b42b8cfd24a1b62e39a6",
|
| 319 |
+
"pred": false,
|
| 320 |
+
"truth": false
|
| 321 |
+
},
|
| 322 |
+
{
|
| 323 |
+
"sample_id": "08844473820c93541fc47bdfeae0f2cc88cfab59",
|
| 324 |
+
"pred": false,
|
| 325 |
+
"truth": false
|
| 326 |
+
},
|
| 327 |
+
{
|
| 328 |
+
"sample_id": "568e18b15e2ddf494fd8926707d34ca08c8edce5",
|
| 329 |
+
"pred": true,
|
| 330 |
+
"truth": true
|
| 331 |
+
},
|
| 332 |
+
{
|
| 333 |
+
"sample_id": "f35e44e7645edbb08e35b111c10c2fc57e2905c7",
|
| 334 |
+
"pred": false,
|
| 335 |
+
"truth": true
|
| 336 |
+
},
|
| 337 |
+
{
|
| 338 |
+
"sample_id": "4bfe4478d17679464a2aaa91ed703522ed9af8a0",
|
| 339 |
+
"pred": false,
|
| 340 |
+
"truth": false
|
| 341 |
+
},
|
| 342 |
+
{
|
| 343 |
+
"sample_id": "f6774f905fb3cfdc319523ac640be30b14c1bc55",
|
| 344 |
+
"pred": false,
|
| 345 |
+
"truth": true
|
| 346 |
+
},
|
| 347 |
+
{
|
| 348 |
+
"sample_id": "8b33d9eeba91422ee2d73b6936ad57262d18cf5a",
|
| 349 |
+
"pred": true,
|
| 350 |
+
"truth": true
|
| 351 |
+
},
|
| 352 |
+
{
|
| 353 |
+
"sample_id": "089da572b956ef0f8f5b8d5917358e07892a77c2",
|
| 354 |
+
"pred": false,
|
| 355 |
+
"truth": true
|
| 356 |
+
},
|
| 357 |
+
{
|
| 358 |
+
"sample_id": "cb08687180683a755d0fe9d425280d0e4d1e6db2",
|
| 359 |
+
"pred": true,
|
| 360 |
+
"truth": true
|
| 361 |
+
},
|
| 362 |
+
{
|
| 363 |
+
"sample_id": "b6fcf32d9b851a83dedcb609091236b97cc4a985",
|
| 364 |
+
"pred": true,
|
| 365 |
+
"truth": false
|
| 366 |
+
},
|
| 367 |
+
{
|
| 368 |
+
"sample_id": "9ef91a677110ec200d7b2904fc4bcae5a77329ad",
|
| 369 |
+
"pred": false,
|
| 370 |
+
"truth": false
|
| 371 |
+
},
|
| 372 |
+
{
|
| 373 |
+
"sample_id": "f090c9d4ad5812fb92843d6470a1111c15190c4c",
|
| 374 |
+
"pred": true,
|
| 375 |
+
"truth": false
|
| 376 |
+
},
|
| 377 |
+
{
|
| 378 |
+
"sample_id": "6f2d8978728c48ca46f5c01835438508aace5c64",
|
| 379 |
+
"pred": true,
|
| 380 |
+
"truth": true
|
| 381 |
+
},
|
| 382 |
+
{
|
| 383 |
+
"sample_id": "6e0d8677cb443e7408c0b7a25a93c6596d7fa380",
|
| 384 |
+
"pred": true,
|
| 385 |
+
"truth": false
|
| 386 |
+
},
|
| 387 |
+
{
|
| 388 |
+
"sample_id": "f6b7f72461673e4d398b1edf9ed2a7fe70d99c47",
|
| 389 |
+
"pred": false,
|
| 390 |
+
"truth": false
|
| 391 |
+
},
|
| 392 |
+
{
|
| 393 |
+
"sample_id": "b3db211f3c80bb996a704d665fe275619f728bd4",
|
| 394 |
+
"pred": false,
|
| 395 |
+
"truth": false
|
| 396 |
+
},
|
| 397 |
+
{
|
| 398 |
+
"sample_id": "f51074cdc6e750daa3b6df727d83449a7e42b391",
|
| 399 |
+
"pred": true,
|
| 400 |
+
"truth": true
|
| 401 |
+
},
|
| 402 |
+
{
|
| 403 |
+
"sample_id": "297a3646c2947ee64a6d42ca264039732c6218e0",
|
| 404 |
+
"pred": true,
|
| 405 |
+
"truth": true
|
| 406 |
+
},
|
| 407 |
+
{
|
| 408 |
+
"sample_id": "6e0d8c06c7af61859e8d7bc2351a607d8abeab75",
|
| 409 |
+
"pred": false,
|
| 410 |
+
"truth": false
|
| 411 |
+
},
|
| 412 |
+
{
|
| 413 |
+
"sample_id": "1c02e2a17104fe7fc11893125864dc0daf1e6d5b",
|
| 414 |
+
"pred": true,
|
| 415 |
+
"truth": true
|
| 416 |
+
},
|
| 417 |
+
{
|
| 418 |
+
"sample_id": "a8170e5e97ad17ca169c64ba87ae2f53850dab4c",
|
| 419 |
+
"pred": false,
|
| 420 |
+
"truth": false
|
| 421 |
+
},
|
| 422 |
+
{
|
| 423 |
+
"sample_id": "26a83ad0e793465b74a8b06a65f2f6fdc5615413",
|
| 424 |
+
"pred": true,
|
| 425 |
+
"truth": false
|
| 426 |
+
},
|
| 427 |
+
{
|
| 428 |
+
"sample_id": "3b99e00c7549ccad90c57b5bcd6e3456650a994a",
|
| 429 |
+
"pred": true,
|
| 430 |
+
"truth": true
|
| 431 |
+
},
|
| 432 |
+
{
|
| 433 |
+
"sample_id": "0c8f86ea98945678622c6e4b070c4218a53a0d19",
|
| 434 |
+
"pred": true,
|
| 435 |
+
"truth": true
|
| 436 |
+
},
|
| 437 |
+
{
|
| 438 |
+
"sample_id": "87e8788680e16c51f6048af26f3f7830c35207a5",
|
| 439 |
+
"pred": false,
|
| 440 |
+
"truth": false
|
| 441 |
+
},
|
| 442 |
+
{
|
| 443 |
+
"sample_id": "61007b316cd71ee7333ff7a0a749a8949527575f",
|
| 444 |
+
"pred": false,
|
| 445 |
+
"truth": false
|
| 446 |
+
},
|
| 447 |
+
{
|
| 448 |
+
"sample_id": "1ffc266539d443f83d5eb487593be50ef496f09e",
|
| 449 |
+
"pred": true,
|
| 450 |
+
"truth": false
|
| 451 |
+
},
|
| 452 |
+
{
|
| 453 |
+
"sample_id": "b23046abe78f48498a423b802d6d86ba0172d57f",
|
| 454 |
+
"pred": false,
|
| 455 |
+
"truth": false
|
| 456 |
+
},
|
| 457 |
+
{
|
| 458 |
+
"sample_id": "a625e13208ad0ebf1554aa73c9bf41452520f176",
|
| 459 |
+
"pred": false,
|
| 460 |
+
"truth": false
|
| 461 |
+
},
|
| 462 |
+
{
|
| 463 |
+
"sample_id": "a4c7a5ea27050a28625eabf1ba98cfef9ac6620d",
|
| 464 |
+
"pred": false,
|
| 465 |
+
"truth": false
|
| 466 |
+
},
|
| 467 |
+
{
|
| 468 |
+
"sample_id": "4c9080a7ef18ad71fb0a75c8d1c1803edd780edd",
|
| 469 |
+
"pred": false,
|
| 470 |
+
"truth": false
|
| 471 |
+
},
|
| 472 |
+
{
|
| 473 |
+
"sample_id": "4cad3867b6df2c0826ae508a9fe15dd0b9d8936a",
|
| 474 |
+
"pred": true,
|
| 475 |
+
"truth": true
|
| 476 |
+
},
|
| 477 |
+
{
|
| 478 |
+
"sample_id": "0c9ab5ef9c1ee852c80c859c9e07efe8730b57ed",
|
| 479 |
+
"pred": false,
|
| 480 |
+
"truth": true
|
| 481 |
+
},
|
| 482 |
+
{
|
| 483 |
+
"sample_id": "6f2d8978728c48ca46f5c01835438508aace5c64",
|
| 484 |
+
"pred": true,
|
| 485 |
+
"truth": true
|
| 486 |
+
},
|
| 487 |
+
{
|
| 488 |
+
"sample_id": "7ec1e5ea4bd0700fa48da86bffa2fcc6146c410a",
|
| 489 |
+
"pred": false,
|
| 490 |
+
"truth": false
|
| 491 |
+
},
|
| 492 |
+
{
|
| 493 |
+
"sample_id": "d9bce9d99f4656ae0b0127f7472db9067b8f84ab",
|
| 494 |
+
"pred": true,
|
| 495 |
+
"truth": true
|
| 496 |
+
},
|
| 497 |
+
{
|
| 498 |
+
"sample_id": "206ab6e090eeddce71372041454d50d93a63017d",
|
| 499 |
+
"pred": false,
|
| 500 |
+
"truth": false
|
| 501 |
+
}
|
| 502 |
+
]
|
models.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from dataclasses import dataclass, field
|
| 4 |
+
from typing import Literal, Optional
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
ActionType = Literal["request_context", "analyze", "verdict"]
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
@dataclass(frozen=True, slots=True)
|
| 11 |
+
class CommitGuardAction:
|
| 12 |
+
action_type: ActionType
|
| 13 |
+
file_path: Optional[str] = None
|
| 14 |
+
reasoning: Optional[str] = None
|
| 15 |
+
is_vulnerable: Optional[bool] = None
|
| 16 |
+
vuln_type: Optional[str] = None
|
| 17 |
+
exploit_sketch: Optional[str] = None
|
| 18 |
+
raw_action: Optional[str] = None
|
| 19 |
+
parse_error: Optional[str] = None
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
@dataclass(frozen=True, slots=True)
|
| 23 |
+
class ContextSnippet:
|
| 24 |
+
file_path: str
|
| 25 |
+
start_line: int
|
| 26 |
+
end_line: int
|
| 27 |
+
content: str
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
@dataclass(frozen=True, slots=True)
|
| 31 |
+
class CommitGuardObservation:
|
| 32 |
+
# Cheating-prevention critical: this shape must never include ground truth.
|
| 33 |
+
episode_id: str
|
| 34 |
+
step_idx: int
|
| 35 |
+
diff: str
|
| 36 |
+
available_files: list[str]
|
| 37 |
+
context_snippets: list[ContextSnippet] = field(default_factory=list)
|
| 38 |
+
budget_remaining: int = 0
|
| 39 |
+
error: Optional[str] = None
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
@dataclass(frozen=True, slots=True)
|
| 43 |
+
class CommitGuardState:
|
| 44 |
+
episode_id: str
|
| 45 |
+
current_sample_id: str
|
| 46 |
+
step_count: int
|
| 47 |
+
context_requests: int = 0
|
| 48 |
+
history: list[dict] = field(default_factory=list)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
@dataclass(frozen=True, slots=True)
|
| 52 |
+
class DevignSample:
|
| 53 |
+
sample_id: str
|
| 54 |
+
diff: str
|
| 55 |
+
available_files: list[str]
|
| 56 |
+
# Server-only fields (must never be surfaced in Observation)
|
| 57 |
+
is_vulnerable: Optional[bool] = None
|
| 58 |
+
cwe: Optional[str] = None
|
| 59 |
+
target_file: Optional[str] = None
|
| 60 |
+
files: Optional[dict[str, str]] = None
|
| 61 |
+
|
notebooks/train_commitguard.ipynb
ADDED
|
@@ -0,0 +1,561 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"cells": [
|
| 3 |
+
{
|
| 4 |
+
"cell_type": "markdown",
|
| 5 |
+
"metadata": {},
|
| 6 |
+
"source": [
|
| 7 |
+
"# CommitGuard GRPO Training Notebook\n",
|
| 8 |
+
"\n",
|
| 9 |
+
"Train Llama-3.2-3B-Instruct to detect exploitable vulnerabilities in code commits using GRPO (Group Relative Policy Optimization).\n",
|
| 10 |
+
"\n",
|
| 11 |
+
"**Requirements:** NVIDIA GPU with 16 GB VRAM (L4/A100/T4). Run this notebook on a GCP VM with GPU attached.\n",
|
| 12 |
+
"\n",
|
| 13 |
+
"## Setup\n",
|
| 14 |
+
"Connect to this notebook via SSH tunnel:\n",
|
| 15 |
+
"```bash\n",
|
| 16 |
+
"# On GCP VM:\n",
|
| 17 |
+
"jupyter notebook --no-browser --port=8888\n",
|
| 18 |
+
"\n",
|
| 19 |
+
"# On your local machine:\n",
|
| 20 |
+
"gcloud compute ssh commitguard-train --zone=us-central1-a -- -NL 8888:localhost:8888\n",
|
| 21 |
+
"# Then open http://localhost:8888 in browser\n",
|
| 22 |
+
"```"
|
| 23 |
+
]
|
| 24 |
+
},
|
| 25 |
+
{
|
| 26 |
+
"cell_type": "markdown",
|
| 27 |
+
"metadata": {},
|
| 28 |
+
"source": [
|
| 29 |
+
"## Cell 1 Install Dependencies"
|
| 30 |
+
]
|
| 31 |
+
},
|
| 32 |
+
{
|
| 33 |
+
"cell_type": "code",
|
| 34 |
+
"execution_count": null,
|
| 35 |
+
"metadata": {},
|
| 36 |
+
"outputs": [],
|
| 37 |
+
"source": [
|
| 38 |
+
"%%bash\n",
|
| 39 |
+
"pip install -q \\\n",
|
| 40 |
+
" \"unsloth[cu124-torch240]\" \\\n",
|
| 41 |
+
" \"trl>=0.12\" \\\n",
|
| 42 |
+
" \"peft>=0.13\" \\\n",
|
| 43 |
+
" \"bitsandbytes>=0.44\" \\\n",
|
| 44 |
+
" \"transformers>=4.46\" \\\n",
|
| 45 |
+
" \"datasets>=3.0\" \\\n",
|
| 46 |
+
" \"accelerate>=1.0\" \\\n",
|
| 47 |
+
" \"wandb\" \\\n",
|
| 48 |
+
" \"fastapi\" \\\n",
|
| 49 |
+
" \"uvicorn[standard]\" \\\n",
|
| 50 |
+
" \"requests\" \\\n",
|
| 51 |
+
" \"matplotlib\""
|
| 52 |
+
]
|
| 53 |
+
},
|
| 54 |
+
{
|
| 55 |
+
"cell_type": "markdown",
|
| 56 |
+
"metadata": {},
|
| 57 |
+
"source": [
|
| 58 |
+
"## Cell 2 Verify GPU"
|
| 59 |
+
]
|
| 60 |
+
},
|
| 61 |
+
{
|
| 62 |
+
"cell_type": "code",
|
| 63 |
+
"execution_count": null,
|
| 64 |
+
"metadata": {},
|
| 65 |
+
"outputs": [],
|
| 66 |
+
"source": [
|
| 67 |
+
"import torch\n",
|
| 68 |
+
"print(f\"PyTorch: {torch.__version__}\")\n",
|
| 69 |
+
"print(f\"CUDA: {torch.cuda.is_available()}\")\n",
|
| 70 |
+
"if torch.cuda.is_available():\n",
|
| 71 |
+
" print(f\"GPU: {torch.cuda.get_device_name(0)}\")\n",
|
| 72 |
+
" print(f\"VRAM: {torch.cuda.get_device_properties(0).total_memory / 1024**3:.1f} GB\")\n",
|
| 73 |
+
" print(f\"BF16: {torch.cuda.is_bf16_supported()}\")\n",
|
| 74 |
+
"else:\n",
|
| 75 |
+
" raise RuntimeError(\"No GPU detected this notebook requires a CUDA GPU.\")"
|
| 76 |
+
]
|
| 77 |
+
},
|
| 78 |
+
{
|
| 79 |
+
"cell_type": "markdown",
|
| 80 |
+
"metadata": {},
|
| 81 |
+
"source": [
|
| 82 |
+
"## Cell 3 Clone Repo & Start Env Server"
|
| 83 |
+
]
|
| 84 |
+
},
|
| 85 |
+
{
|
| 86 |
+
"cell_type": "code",
|
| 87 |
+
"execution_count": null,
|
| 88 |
+
"metadata": {},
|
| 89 |
+
"outputs": [],
|
| 90 |
+
"source": [
|
| 91 |
+
"import os, subprocess, time, requests\n",
|
| 92 |
+
"\n",
|
| 93 |
+
"REPO_DIR = os.path.expanduser(\"~/commitguard\")\n",
|
| 94 |
+
"if not os.path.isdir(REPO_DIR):\n",
|
| 95 |
+
" !git clone https://github.com/NitishKumar-ai/commitguard.git {REPO_DIR}\n",
|
| 96 |
+
"else:\n",
|
| 97 |
+
" !cd {REPO_DIR} && git pull\n",
|
| 98 |
+
"\n",
|
| 99 |
+
"os.chdir(REPO_DIR)\n",
|
| 100 |
+
"!pip install -e . -q\n",
|
| 101 |
+
"\n",
|
| 102 |
+
"# Start env server in background\n",
|
| 103 |
+
"server_proc = subprocess.Popen(\n",
|
| 104 |
+
" [\"python\", \"-m\", \"commitguard_env.server\"],\n",
|
| 105 |
+
" stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,\n",
|
| 106 |
+
")\n",
|
| 107 |
+
"time.sleep(3)\n",
|
| 108 |
+
"\n",
|
| 109 |
+
"r = requests.get(\"http://localhost:8000/health\")\n",
|
| 110 |
+
"print(f\"Env server: {r.json()}\")\n",
|
| 111 |
+
"\n",
|
| 112 |
+
"# Quick sanity reset + step\n",
|
| 113 |
+
"r = requests.post(\"http://localhost:8000/reset\", json={})\n",
|
| 114 |
+
"obs = r.json()[\"observation\"]\n",
|
| 115 |
+
"print(f\"Sample diff length: {len(obs['diff'])} chars, files: {obs['available_files']}\")"
|
| 116 |
+
]
|
| 117 |
+
},
|
| 118 |
+
{
|
| 119 |
+
"cell_type": "markdown",
|
| 120 |
+
"metadata": {},
|
| 121 |
+
"source": [
|
| 122 |
+
"## Cell 4 HuggingFace Login (for gated Llama model)"
|
| 123 |
+
]
|
| 124 |
+
},
|
| 125 |
+
{
|
| 126 |
+
"cell_type": "code",
|
| 127 |
+
"execution_count": null,
|
| 128 |
+
"metadata": {},
|
| 129 |
+
"outputs": [],
|
| 130 |
+
"source": [
|
| 131 |
+
"from huggingface_hub import login\n",
|
| 132 |
+
"\n",
|
| 133 |
+
"# Paste your HF token here (or set HF_TOKEN env var)\n",
|
| 134 |
+
"# Get one at: https://huggingface.co/settings/tokens\n",
|
| 135 |
+
"# Make sure you accepted the Llama license: https://huggingface.co/meta-llama/Llama-3.2-3B-Instruct\n",
|
| 136 |
+
"\n",
|
| 137 |
+
"HF_TOKEN = os.getenv(\"HF_TOKEN\", \"\")\n",
|
| 138 |
+
"if HF_TOKEN:\n",
|
| 139 |
+
" login(token=HF_TOKEN)\n",
|
| 140 |
+
" print(\"Logged in via env var.\")\n",
|
| 141 |
+
"else:\n",
|
| 142 |
+
" login() # interactive prompt"
|
| 143 |
+
]
|
| 144 |
+
},
|
| 145 |
+
{
|
| 146 |
+
"cell_type": "markdown",
|
| 147 |
+
"metadata": {},
|
| 148 |
+
"source": [
|
| 149 |
+
"## Cell 5 Wandb Login (optional but recommended)"
|
| 150 |
+
]
|
| 151 |
+
},
|
| 152 |
+
{
|
| 153 |
+
"cell_type": "code",
|
| 154 |
+
"execution_count": null,
|
| 155 |
+
"metadata": {},
|
| 156 |
+
"outputs": [],
|
| 157 |
+
"source": [
|
| 158 |
+
"import wandb\n",
|
| 159 |
+
"\n",
|
| 160 |
+
"USE_WANDB = True # Set False to skip\n",
|
| 161 |
+
"\n",
|
| 162 |
+
"if USE_WANDB:\n",
|
| 163 |
+
" WANDB_KEY = os.getenv(\"WANDB_API_KEY\", \"\")\n",
|
| 164 |
+
" if WANDB_KEY:\n",
|
| 165 |
+
" wandb.login(key=WANDB_KEY)\n",
|
| 166 |
+
" else:\n",
|
| 167 |
+
" wandb.login() # interactive\n",
|
| 168 |
+
" os.environ[\"WANDB_PROJECT\"] = \"commitguard\"\n",
|
| 169 |
+
" print(\"Wandb ready.\")\n",
|
| 170 |
+
"else:\n",
|
| 171 |
+
" os.environ[\"WANDB_DISABLED\"] = \"true\"\n",
|
| 172 |
+
" print(\"Wandb disabled.\")"
|
| 173 |
+
]
|
| 174 |
+
},
|
| 175 |
+
{
|
| 176 |
+
"cell_type": "markdown",
|
| 177 |
+
"metadata": {},
|
| 178 |
+
"source": [
|
| 179 |
+
"## Cell 6 Load Model with Unsloth (4-bit LoRA)"
|
| 180 |
+
]
|
| 181 |
+
},
|
| 182 |
+
{
|
| 183 |
+
"cell_type": "code",
|
| 184 |
+
"execution_count": null,
|
| 185 |
+
"metadata": {},
|
| 186 |
+
"outputs": [],
|
| 187 |
+
"source": [
|
| 188 |
+
"from unsloth import FastLanguageModel, PatchFastRL\n",
|
| 189 |
+
"from trl import GRPOConfig, GRPOTrainer\n",
|
| 190 |
+
"\n",
|
| 191 |
+
"PatchFastRL(\"GRPO\", FastLanguageModel)\n",
|
| 192 |
+
"\n",
|
| 193 |
+
"MODEL_NAME = \"meta-llama/Llama-3.2-3B-Instruct\"\n",
|
| 194 |
+
"\n",
|
| 195 |
+
"print(f\"Loading {MODEL_NAME} in 4-bit...\")\n",
|
| 196 |
+
"model, tokenizer = FastLanguageModel.from_pretrained(\n",
|
| 197 |
+
" model_name=MODEL_NAME,\n",
|
| 198 |
+
" max_seq_length=2048,\n",
|
| 199 |
+
" load_in_4bit=True,\n",
|
| 200 |
+
" fast_inference=True,\n",
|
| 201 |
+
" max_lora_rank=16,\n",
|
| 202 |
+
")\n",
|
| 203 |
+
"\n",
|
| 204 |
+
"model = FastLanguageModel.get_peft_model(\n",
|
| 205 |
+
" model,\n",
|
| 206 |
+
" r=8,\n",
|
| 207 |
+
" target_modules=[\"q_proj\", \"k_proj\", \"v_proj\", \"o_proj\",\n",
|
| 208 |
+
" \"gate_proj\", \"up_proj\", \"down_proj\"],\n",
|
| 209 |
+
" lora_alpha=16,\n",
|
| 210 |
+
" lora_dropout=0,\n",
|
| 211 |
+
" bias=\"none\",\n",
|
| 212 |
+
" use_gradient_checkpointing=\"unsloth\",\n",
|
| 213 |
+
" random_state=3407,\n",
|
| 214 |
+
")\n",
|
| 215 |
+
"\n",
|
| 216 |
+
"print(f\"Model loaded. Trainable params: {model.print_trainable_parameters()}\")"
|
| 217 |
+
]
|
| 218 |
+
},
|
| 219 |
+
{
|
| 220 |
+
"cell_type": "markdown",
|
| 221 |
+
"metadata": {},
|
| 222 |
+
"source": [
|
| 223 |
+
"## Cell 7 Build Training Dataset from Env"
|
| 224 |
+
]
|
| 225 |
+
},
|
| 226 |
+
{
|
| 227 |
+
"cell_type": "code",
|
| 228 |
+
"execution_count": null,
|
| 229 |
+
"metadata": {},
|
| 230 |
+
"outputs": [],
|
| 231 |
+
"source": [
|
| 232 |
+
"import sys, requests\n",
|
| 233 |
+
"from datasets import Dataset\n",
|
| 234 |
+
"\n",
|
| 235 |
+
"sys.path.insert(0, os.path.join(REPO_DIR, \"scripts\"))\n",
|
| 236 |
+
"from agent_prompt import SYSTEM_PROMPT, get_agent_prompt\n",
|
| 237 |
+
"\n",
|
| 238 |
+
"ENV_URL = \"http://localhost:8000\"\n",
|
| 239 |
+
"N_SAMPLES = 200 # Number of training prompts\n",
|
| 240 |
+
"\n",
|
| 241 |
+
"samples = []\n",
|
| 242 |
+
"for i in range(N_SAMPLES):\n",
|
| 243 |
+
" r = requests.post(f\"{ENV_URL}/reset\", json={}, timeout=10)\n",
|
| 244 |
+
" if r.status_code != 200:\n",
|
| 245 |
+
" continue\n",
|
| 246 |
+
" obs = r.json()[\"observation\"]\n",
|
| 247 |
+
" user_msg = get_agent_prompt(obs[\"diff\"], obs[\"available_files\"], obs.get(\"step_idx\", 0))\n",
|
| 248 |
+
" samples.append({\n",
|
| 249 |
+
" \"prompt\": [\n",
|
| 250 |
+
" {\"role\": \"system\", \"content\": SYSTEM_PROMPT},\n",
|
| 251 |
+
" {\"role\": \"user\", \"content\": user_msg},\n",
|
| 252 |
+
" ],\n",
|
| 253 |
+
" })\n",
|
| 254 |
+
" if (i + 1) % 50 == 0:\n",
|
| 255 |
+
" print(f\" fetched {i + 1}/{N_SAMPLES}\")\n",
|
| 256 |
+
"\n",
|
| 257 |
+
"dataset = Dataset.from_list(samples)\n",
|
| 258 |
+
"print(f\"\\nDataset ready: {len(dataset)} samples\")\n",
|
| 259 |
+
"print(f\"Sample prompt preview: {str(dataset[0]['prompt'][1]['content'])[:200]}...\")"
|
| 260 |
+
]
|
| 261 |
+
},
|
| 262 |
+
{
|
| 263 |
+
"cell_type": "markdown",
|
| 264 |
+
"metadata": {},
|
| 265 |
+
"source": [
|
| 266 |
+
"## Cell 8 Define Reward Function"
|
| 267 |
+
]
|
| 268 |
+
},
|
| 269 |
+
{
|
| 270 |
+
"cell_type": "code",
|
| 271 |
+
"execution_count": null,
|
| 272 |
+
"metadata": {},
|
| 273 |
+
"outputs": [],
|
| 274 |
+
"source": [
|
| 275 |
+
"def get_reward_from_env(prompts, completions, **kwargs) -> list[float]:\n",
|
| 276 |
+
" \"\"\"Send each completion to the env as an action, collect reward.\"\"\"\n",
|
| 277 |
+
" rewards = []\n",
|
| 278 |
+
" for prompt, completion in zip(prompts, completions):\n",
|
| 279 |
+
" try:\n",
|
| 280 |
+
" requests.post(f\"{ENV_URL}/reset\", json={}, timeout=10)\n",
|
| 281 |
+
" text = completion[-1][\"content\"] if isinstance(completion, list) else str(completion)\n",
|
| 282 |
+
" r = requests.post(f\"{ENV_URL}/step\", json={\"action\": text}, timeout=10)\n",
|
| 283 |
+
" if r.status_code == 200:\n",
|
| 284 |
+
" rewards.append(float(r.json().get(\"reward\", 0.0)))\n",
|
| 285 |
+
" else:\n",
|
| 286 |
+
" rewards.append(-0.5)\n",
|
| 287 |
+
" except Exception:\n",
|
| 288 |
+
" rewards.append(-1.0)\n",
|
| 289 |
+
" return rewards\n",
|
| 290 |
+
"\n",
|
| 291 |
+
"# Quick test\n",
|
| 292 |
+
"test_r = get_reward_from_env(\n",
|
| 293 |
+
" [\"test\"],\n",
|
| 294 |
+
" [\"<action><action_type>verdict</action_type><is_vulnerable>true</is_vulnerable><vuln_type>CWE-119</vuln_type><exploit_sketch>buffer overflow</exploit_sketch></action>\"]\n",
|
| 295 |
+
")\n",
|
| 296 |
+
"print(f\"Reward function test: {test_r}\")"
|
| 297 |
+
]
|
| 298 |
+
},
|
| 299 |
+
{
|
| 300 |
+
"cell_type": "markdown",
|
| 301 |
+
"metadata": {},
|
| 302 |
+
"source": [
|
| 303 |
+
"## Cell 9 Configure & Launch GRPO Training\n",
|
| 304 |
+
"\n",
|
| 305 |
+
"This is the main training loop. ~2-3 hours on L4 for 300 steps."
|
| 306 |
+
]
|
| 307 |
+
},
|
| 308 |
+
{
|
| 309 |
+
"cell_type": "code",
|
| 310 |
+
"execution_count": null,
|
| 311 |
+
"metadata": {},
|
| 312 |
+
"outputs": [],
|
| 313 |
+
"source": [
|
| 314 |
+
"OUTPUT_DIR = \"outputs/commitguard-llama-3b\"\n",
|
| 315 |
+
"\n",
|
| 316 |
+
"training_args = GRPOConfig(\n",
|
| 317 |
+
" output_dir=OUTPUT_DIR,\n",
|
| 318 |
+
" num_generations=4,\n",
|
| 319 |
+
" max_completion_length=512,\n",
|
| 320 |
+
" per_device_train_batch_size=1,\n",
|
| 321 |
+
" gradient_accumulation_steps=4,\n",
|
| 322 |
+
" learning_rate=5e-6,\n",
|
| 323 |
+
" logging_steps=1,\n",
|
| 324 |
+
" save_steps=50,\n",
|
| 325 |
+
" max_steps=300,\n",
|
| 326 |
+
" report_to=\"wandb\" if USE_WANDB else \"none\",\n",
|
| 327 |
+
" bf16=torch.cuda.is_bf16_supported(),\n",
|
| 328 |
+
" fp16=not torch.cuda.is_bf16_supported(),\n",
|
| 329 |
+
")\n",
|
| 330 |
+
"\n",
|
| 331 |
+
"trainer = GRPOTrainer(\n",
|
| 332 |
+
" model=model,\n",
|
| 333 |
+
" processing_class=tokenizer,\n",
|
| 334 |
+
" reward_funcs=[get_reward_from_env],\n",
|
| 335 |
+
" args=training_args,\n",
|
| 336 |
+
" train_dataset=dataset,\n",
|
| 337 |
+
")\n",
|
| 338 |
+
"\n",
|
| 339 |
+
"print(\"Starting GRPO training...\")\n",
|
| 340 |
+
"print(f\" Steps: {training_args.max_steps}\")\n",
|
| 341 |
+
"print(f\" Generations per prompt: {training_args.num_generations}\")\n",
|
| 342 |
+
"print(f\" Save every: {training_args.save_steps} steps\")\n",
|
| 343 |
+
"print(f\" Output: {OUTPUT_DIR}\")\n",
|
| 344 |
+
"print(\"=\"*50)\n",
|
| 345 |
+
"\n",
|
| 346 |
+
"trainer.train()"
|
| 347 |
+
]
|
| 348 |
+
},
|
| 349 |
+
{
|
| 350 |
+
"cell_type": "markdown",
|
| 351 |
+
"metadata": {},
|
| 352 |
+
"source": [
|
| 353 |
+
"## Cell 10 Save Final LoRA Adapter"
|
| 354 |
+
]
|
| 355 |
+
},
|
| 356 |
+
{
|
| 357 |
+
"cell_type": "code",
|
| 358 |
+
"execution_count": null,
|
| 359 |
+
"metadata": {},
|
| 360 |
+
"outputs": [],
|
| 361 |
+
"source": [
|
| 362 |
+
"FINAL_DIR = f\"{OUTPUT_DIR}/final\"\n",
|
| 363 |
+
"model.save_pretrained_merged(FINAL_DIR, tokenizer, save_method=\"lora\")\n",
|
| 364 |
+
"print(f\"LoRA adapter saved to {FINAL_DIR}\")\n",
|
| 365 |
+
"\n",
|
| 366 |
+
"# List saved files\n",
|
| 367 |
+
"for f in sorted(os.listdir(FINAL_DIR)):\n",
|
| 368 |
+
" size_mb = os.path.getsize(os.path.join(FINAL_DIR, f)) / 1024**2\n",
|
| 369 |
+
" print(f\" {f}: {size_mb:.1f} MB\")"
|
| 370 |
+
]
|
| 371 |
+
},
|
| 372 |
+
{
|
| 373 |
+
"cell_type": "markdown",
|
| 374 |
+
"metadata": {},
|
| 375 |
+
"source": [
|
| 376 |
+
"## Cell 11 Quick Evaluation (Baseline vs Trained)"
|
| 377 |
+
]
|
| 378 |
+
},
|
| 379 |
+
{
|
| 380 |
+
"cell_type": "code",
|
| 381 |
+
"execution_count": null,
|
| 382 |
+
"metadata": {},
|
| 383 |
+
"outputs": [],
|
| 384 |
+
"source": [
|
| 385 |
+
"import json\n",
|
| 386 |
+
"\n",
|
| 387 |
+
"# Load test set\n",
|
| 388 |
+
"test_path = os.path.join(REPO_DIR, \"data\", \"devign_test.jsonl\")\n",
|
| 389 |
+
"with open(test_path) as f:\n",
|
| 390 |
+
" test_samples = [json.loads(l) for l in f if l.strip()]\n",
|
| 391 |
+
"\n",
|
| 392 |
+
"print(f\"Evaluating on {len(test_samples)} held-out samples...\")\n",
|
| 393 |
+
"\n",
|
| 394 |
+
"# Run trained model on test set\n",
|
| 395 |
+
"FastLanguageModel.for_inference(model)\n",
|
| 396 |
+
"\n",
|
| 397 |
+
"correct = 0\n",
|
| 398 |
+
"results = []\n",
|
| 399 |
+
"\n",
|
| 400 |
+
"for i, sample in enumerate(test_samples):\n",
|
| 401 |
+
" user_msg = get_agent_prompt(sample[\"diff\"], sample[\"available_files\"], 0)\n",
|
| 402 |
+
" messages = [\n",
|
| 403 |
+
" {\"role\": \"system\", \"content\": SYSTEM_PROMPT},\n",
|
| 404 |
+
" {\"role\": \"user\", \"content\": user_msg},\n",
|
| 405 |
+
" ]\n",
|
| 406 |
+
" inputs = tokenizer.apply_chat_template(messages, return_tensors=\"pt\", add_generation_prompt=True).to(model.device)\n",
|
| 407 |
+
" with torch.no_grad():\n",
|
| 408 |
+
" output = model.generate(inputs, max_new_tokens=512, temperature=0.1, do_sample=True)\n",
|
| 409 |
+
" response = tokenizer.decode(output[0][inputs.shape[1]:], skip_special_tokens=True)\n",
|
| 410 |
+
"\n",
|
| 411 |
+
" # Parse verdict\n",
|
| 412 |
+
" sys.path.insert(0, os.path.join(REPO_DIR, \"commitguard_env\"))\n",
|
| 413 |
+
" from commitguard_env.parse_action import parse_action\n",
|
| 414 |
+
" action = parse_action(response)\n",
|
| 415 |
+
"\n",
|
| 416 |
+
" pred_vuln = bool(action.is_vulnerable) if action.is_vulnerable is not None else False\n",
|
| 417 |
+
" truth_vuln = sample[\"is_vulnerable\"]\n",
|
| 418 |
+
"\n",
|
| 419 |
+
" if pred_vuln == truth_vuln:\n",
|
| 420 |
+
" correct += 1\n",
|
| 421 |
+
"\n",
|
| 422 |
+
" results.append({\n",
|
| 423 |
+
" \"sample_id\": sample[\"sample_id\"],\n",
|
| 424 |
+
" \"pred\": pred_vuln,\n",
|
| 425 |
+
" \"truth\": truth_vuln,\n",
|
| 426 |
+
" \"cwe\": sample.get(\"cwe\"),\n",
|
| 427 |
+
" \"vuln_type\": action.vuln_type,\n",
|
| 428 |
+
" })\n",
|
| 429 |
+
"\n",
|
| 430 |
+
" if (i + 1) % 20 == 0:\n",
|
| 431 |
+
" print(f\" {i+1}/{len(test_samples)} running accuracy: {100*correct/(i+1):.1f}%\")\n",
|
| 432 |
+
"\n",
|
| 433 |
+
"accuracy = 100 * correct / len(test_samples)\n",
|
| 434 |
+
"print(f\"\\nFinal trained accuracy: {accuracy:.1f}%\")\n",
|
| 435 |
+
"\n",
|
| 436 |
+
"with open(os.path.join(REPO_DIR, \"eval_trained.json\"), \"w\") as f:\n",
|
| 437 |
+
" json.dump(results, f, indent=2)\n",
|
| 438 |
+
"print(\"Results saved to eval_trained.json\")"
|
| 439 |
+
]
|
| 440 |
+
},
|
| 441 |
+
{
|
| 442 |
+
"cell_type": "markdown",
|
| 443 |
+
"metadata": {},
|
| 444 |
+
"source": [
|
| 445 |
+
"## Cell 12 Generate Plots"
|
| 446 |
+
]
|
| 447 |
+
},
|
| 448 |
+
{
|
| 449 |
+
"cell_type": "code",
|
| 450 |
+
"execution_count": null,
|
| 451 |
+
"metadata": {},
|
| 452 |
+
"outputs": [],
|
| 453 |
+
"source": [
|
| 454 |
+
"import matplotlib.pyplot as plt\n",
|
| 455 |
+
"from collections import Counter\n",
|
| 456 |
+
"\n",
|
| 457 |
+
"os.makedirs(os.path.join(REPO_DIR, \"plots\"), exist_ok=True)\n",
|
| 458 |
+
"\n",
|
| 459 |
+
"# --- Plot 1: Training reward curve (from trainer logs) ---\n",
|
| 460 |
+
"if hasattr(trainer, 'state') and trainer.state.log_history:\n",
|
| 461 |
+
" steps = [l[\"step\"] for l in trainer.state.log_history if \"loss\" in l]\n",
|
| 462 |
+
" losses = [l[\"loss\"] for l in trainer.state.log_history if \"loss\" in l]\n",
|
| 463 |
+
" \n",
|
| 464 |
+
" fig, ax = plt.subplots(figsize=(10, 5))\n",
|
| 465 |
+
" ax.plot(steps, losses, color=\"#2ecc71\", linewidth=2)\n",
|
| 466 |
+
" ax.set_xlabel(\"Training Step\")\n",
|
| 467 |
+
" ax.set_ylabel(\"Loss\")\n",
|
| 468 |
+
" ax.set_title(\"CommitGuard GRPO Training Loss\")\n",
|
| 469 |
+
" ax.grid(True, linestyle=\"--\", alpha=0.5)\n",
|
| 470 |
+
" fig.savefig(os.path.join(REPO_DIR, \"plots\", \"reward_curve.png\"), dpi=150)\n",
|
| 471 |
+
" plt.show()\n",
|
| 472 |
+
" print(\"Saved plots/reward_curve.png\")\n",
|
| 473 |
+
"\n",
|
| 474 |
+
"# --- Plot 2: Accuracy comparison ---\n",
|
| 475 |
+
"baseline_acc = 50.0 # Update with actual baseline number\n",
|
| 476 |
+
"trained_acc = accuracy\n",
|
| 477 |
+
"\n",
|
| 478 |
+
"fig, ax = plt.subplots(figsize=(8, 5))\n",
|
| 479 |
+
"bars = ax.bar([\"Baseline (Untrained)\", \"CommitGuard (Trained)\"],\n",
|
| 480 |
+
" [baseline_acc, trained_acc],\n",
|
| 481 |
+
" color=[\"#95a5a6\", \"#3498db\"])\n",
|
| 482 |
+
"ax.set_ylabel(\"Detection Accuracy (%)\")\n",
|
| 483 |
+
"ax.set_title(\"Vulnerability Detection: Baseline vs. Trained\")\n",
|
| 484 |
+
"ax.set_ylim(0, 100)\n",
|
| 485 |
+
"for bar in bars:\n",
|
| 486 |
+
" h = bar.get_height()\n",
|
| 487 |
+
" ax.text(bar.get_x() + bar.get_width()/2., h + 1, f\"{h:.1f}%\",\n",
|
| 488 |
+
" ha=\"center\", fontweight=\"bold\")\n",
|
| 489 |
+
"fig.savefig(os.path.join(REPO_DIR, \"plots\", \"baseline_vs_trained.png\"), dpi=150)\n",
|
| 490 |
+
"plt.show()\n",
|
| 491 |
+
"print(\"Saved plots/baseline_vs_trained.png\")\n",
|
| 492 |
+
"\n",
|
| 493 |
+
"# --- Plot 3: Per-CWE breakdown ---\n",
|
| 494 |
+
"cwe_correct = Counter()\n",
|
| 495 |
+
"cwe_total = Counter()\n",
|
| 496 |
+
"for r in results:\n",
|
| 497 |
+
" if r[\"cwe\"]:\n",
|
| 498 |
+
" cwe_total[r[\"cwe\"]] += 1\n",
|
| 499 |
+
" if r[\"pred\"] == r[\"truth\"]:\n",
|
| 500 |
+
" cwe_correct[r[\"cwe\"]] += 1\n",
|
| 501 |
+
"\n",
|
| 502 |
+
"cwes = sorted(cwe_total.keys())\n",
|
| 503 |
+
"accs = [100 * cwe_correct[c] / cwe_total[c] if cwe_total[c] > 0 else 0 for c in cwes]\n",
|
| 504 |
+
"\n",
|
| 505 |
+
"if cwes:\n",
|
| 506 |
+
" fig, ax = plt.subplots(figsize=(10, 5))\n",
|
| 507 |
+
" ax.bar(cwes, accs, color=\"#e67e22\")\n",
|
| 508 |
+
" ax.set_ylabel(\"Accuracy (%)\")\n",
|
| 509 |
+
" ax.set_title(\"Trained Model Accuracy by CWE Type\")\n",
|
| 510 |
+
" ax.set_ylim(0, 100)\n",
|
| 511 |
+
" plt.xticks(rotation=45)\n",
|
| 512 |
+
" plt.tight_layout()\n",
|
| 513 |
+
" fig.savefig(os.path.join(REPO_DIR, \"plots\", \"per_cwe.png\"), dpi=150)\n",
|
| 514 |
+
" plt.show()\n",
|
| 515 |
+
" print(\"Saved plots/per_cwe.png\")"
|
| 516 |
+
]
|
| 517 |
+
},
|
| 518 |
+
{
|
| 519 |
+
"cell_type": "markdown",
|
| 520 |
+
"metadata": {},
|
| 521 |
+
"source": [
|
| 522 |
+
"## Cell 13 Cleanup\n",
|
| 523 |
+
"\n",
|
| 524 |
+
"Stop the env server and print final summary."
|
| 525 |
+
]
|
| 526 |
+
},
|
| 527 |
+
{
|
| 528 |
+
"cell_type": "code",
|
| 529 |
+
"execution_count": null,
|
| 530 |
+
"metadata": {},
|
| 531 |
+
"outputs": [],
|
| 532 |
+
"source": [
|
| 533 |
+
"server_proc.terminate()\n",
|
| 534 |
+
"print(\"Env server stopped.\")\n",
|
| 535 |
+
"\n",
|
| 536 |
+
"print(\"\\n\" + \"=\"*50)\n",
|
| 537 |
+
"print(\" TRAINING COMPLETE\")\n",
|
| 538 |
+
"print(\"=\"*50)\n",
|
| 539 |
+
"print(f\" Model: {MODEL_NAME}\")\n",
|
| 540 |
+
"print(f\" Steps: {training_args.max_steps}\")\n",
|
| 541 |
+
"print(f\" Accuracy: {baseline_acc:.1f}% {trained_acc:.1f}% (+{trained_acc - baseline_acc:.1f}pp)\")\n",
|
| 542 |
+
"print(f\" Adapter: {FINAL_DIR}\")\n",
|
| 543 |
+
"print(f\" Plots: plots/reward_curve.png, baseline_vs_trained.png, per_cwe.png\")\n",
|
| 544 |
+
"print(\"\\nNext: copy outputs/ and plots/ back to your local machine.\")"
|
| 545 |
+
]
|
| 546 |
+
}
|
| 547 |
+
],
|
| 548 |
+
"metadata": {
|
| 549 |
+
"kernelspec": {
|
| 550 |
+
"display_name": "Python 3",
|
| 551 |
+
"language": "python",
|
| 552 |
+
"name": "python3"
|
| 553 |
+
},
|
| 554 |
+
"language_info": {
|
| 555 |
+
"name": "python",
|
| 556 |
+
"version": "3.10.0"
|
| 557 |
+
}
|
| 558 |
+
},
|
| 559 |
+
"nbformat": 4,
|
| 560 |
+
"nbformat_minor": 4
|
| 561 |
+
}
|
openenv.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: commitguard
|
| 2 |
+
version: "0.1.0"
|
| 3 |
+
description: "CommitGuard OpenEnv environment (FastAPI server)"
|
| 4 |
+
port: 8000
|
| 5 |
+
entrypoint: "server/app.py"
|
| 6 |
+
|
plots/README.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
## Plots
|
| 2 |
+
|
| 3 |
+
Per PRD, final plot PNGs should be committed and referenced from `README.md`.
|
| 4 |
+
|
| 5 |
+
Expected outputs:
|
| 6 |
+
- `reward_curve.png`
|
| 7 |
+
- `baseline_vs_trained.png`
|
| 8 |
+
- `per_cwe.png` (optional)
|
| 9 |
+
|
| 10 |
+
Generated (local baseline):
|
| 11 |
+
- `baseline_reward_curve.png`
|
| 12 |
+
- `baseline_rewards.json`
|
| 13 |
+
|
plots/baseline_reward_curve.png
ADDED
|
Git LFS Details
|
plots/baseline_rewards.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
[1.0, 1.0, 1.0, 1.0, -1.0, -1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, -1.0, -1.0, -1.0, 1.0, -1.0, -1.0, -1.0, 1.0, 1.0, -1.0, -1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 1.0, 1.5, -1.0, 1.0, 1.0, 1.0, 1.5, 1.0, -1.0, 1.0, -1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, 1.0, -1.0, -1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 1.0, -1.0, -1.0, 1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 1.0, 1.0, 1.0, -1.0, -1.0, 1.0, -1.0, -1.0, -1.0, 1.0, 1.0, -1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, 1.0, -1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, 1.0, -1.0, 1.0, -1.0, -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, 1.5, 1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, -1.0, -1.0, -1.0, -1.0, 1.0]
|
plots/baseline_vs_trained.png
ADDED
|
plots/per_cwe.png
ADDED
|
plots/plot_baseline_vs_trained.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import argparse
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
import os
|
| 5 |
+
|
| 6 |
+
def main():
|
| 7 |
+
parser = argparse.ArgumentParser(description="Plot baseline vs trained accuracy.")
|
| 8 |
+
parser.add_argument("--baseline", type=str, default="eval_baseline.json", help="Path to baseline results JSON")
|
| 9 |
+
parser.add_argument("--trained", type=str, default="eval_results.json", help="Path to trained results JSON")
|
| 10 |
+
parser.add_argument("--output", type=str, default="plots/baseline_vs_trained.png", help="Path to save the plot")
|
| 11 |
+
args = parser.parse_args()
|
| 12 |
+
|
| 13 |
+
if not os.path.exists(args.baseline) or not os.path.exists(args.trained):
|
| 14 |
+
print("Error: Baseline or trained results file missing.")
|
| 15 |
+
# Provide placeholder data for demo purposes if files are missing
|
| 16 |
+
baseline_acc = 0.35
|
| 17 |
+
trained_acc = 0.72
|
| 18 |
+
else:
|
| 19 |
+
with open(args.baseline, "r") as f:
|
| 20 |
+
b_data = json.load(f)
|
| 21 |
+
with open(args.trained, "r") as f:
|
| 22 |
+
t_data = json.load(f)
|
| 23 |
+
|
| 24 |
+
# Support both structures (simple list or dict with summary)
|
| 25 |
+
if isinstance(b_data, dict):
|
| 26 |
+
baseline_acc = b_data.get("summary", {}).get("overall_accuracy", 0)
|
| 27 |
+
else:
|
| 28 |
+
baseline_acc = sum(1 for r in b_data if r.get("is_correct")) / len(b_data) if b_data else 0
|
| 29 |
+
|
| 30 |
+
if isinstance(t_data, dict):
|
| 31 |
+
trained_acc = t_data.get("summary", {}).get("overall_accuracy", 0)
|
| 32 |
+
else:
|
| 33 |
+
trained_acc = sum(1 for r in t_data if r.get("is_correct")) / len(t_data) if t_data else 0
|
| 34 |
+
|
| 35 |
+
labels = ['Baseline (Untrained)', 'Trained (GRPO)']
|
| 36 |
+
accuracies = [baseline_acc, trained_acc]
|
| 37 |
+
|
| 38 |
+
plt.figure(figsize=(8, 6))
|
| 39 |
+
bars = plt.bar(labels, accuracies, color=['gray', 'orange'], edgecolor='black', width=0.6)
|
| 40 |
+
|
| 41 |
+
for bar in bars:
|
| 42 |
+
yval = bar.get_height()
|
| 43 |
+
plt.text(bar.get_x() + bar.get_width()/2, yval + 0.02, f'{yval:.1%}', ha='center', va='bottom', fontweight='bold', fontsize=12)
|
| 44 |
+
|
| 45 |
+
plt.ylabel('Overall Accuracy')
|
| 46 |
+
plt.title('CommitGuard — Model Performance Improvement')
|
| 47 |
+
plt.ylim(0, 1.1)
|
| 48 |
+
plt.grid(axis='y', linestyle='--', alpha=0.6)
|
| 49 |
+
plt.tight_layout()
|
| 50 |
+
|
| 51 |
+
os.makedirs(os.path.dirname(args.output), exist_ok=True)
|
| 52 |
+
plt.savefig(args.output)
|
| 53 |
+
print(f"Plot saved to {args.output}")
|
| 54 |
+
|
| 55 |
+
if __name__ == "__main__":
|
| 56 |
+
main()
|
plots/plot_per_cwe.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import argparse
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
import os
|
| 5 |
+
|
| 6 |
+
def main():
|
| 7 |
+
parser = argparse.ArgumentParser(description="Plot accuracy per CWE type.")
|
| 8 |
+
parser.add_argument("--input", type=str, default="eval_results.json", help="Path to evaluation results JSON")
|
| 9 |
+
parser.add_argument("--output", type=str, default="plots/per_cwe.png", help="Path to save the plot")
|
| 10 |
+
args = parser.parse_args()
|
| 11 |
+
|
| 12 |
+
if not os.path.exists(args.input):
|
| 13 |
+
print(f"Error: Input file {args.input} not found.")
|
| 14 |
+
return
|
| 15 |
+
|
| 16 |
+
with open(args.input, "r") as f:
|
| 17 |
+
data = json.load(f)
|
| 18 |
+
|
| 19 |
+
cwe_breakdown = data.get("summary", {}).get("cwe_breakdown", {})
|
| 20 |
+
if not cwe_breakdown:
|
| 21 |
+
print("No CWE breakdown found in the results.")
|
| 22 |
+
return
|
| 23 |
+
|
| 24 |
+
cwes = list(cwe_breakdown.keys())
|
| 25 |
+
accuracies = [stats["accuracy"] for stats in cwe_breakdown.values()]
|
| 26 |
+
counts = [stats["count"] for stats in cwe_breakdown.values()]
|
| 27 |
+
|
| 28 |
+
plt.figure(figsize=(12, 6))
|
| 29 |
+
bars = plt.bar(cwes, accuracies, color='skyblue', edgecolor='navy')
|
| 30 |
+
|
| 31 |
+
# Add counts on top of bars
|
| 32 |
+
for i, bar in enumerate(bars):
|
| 33 |
+
yval = bar.get_height()
|
| 34 |
+
plt.text(bar.get_x() + bar.get_width()/2, yval + 0.01, f'n={counts[i]}', ha='center', va='bottom')
|
| 35 |
+
|
| 36 |
+
plt.xlabel('CWE Type')
|
| 37 |
+
plt.ylabel('Accuracy')
|
| 38 |
+
plt.title('CommitGuard — Accuracy per CWE Type')
|
| 39 |
+
plt.ylim(0, 1.1)
|
| 40 |
+
plt.grid(axis='y', linestyle='--', alpha=0.7)
|
| 41 |
+
plt.xticks(rotation=45)
|
| 42 |
+
plt.tight_layout()
|
| 43 |
+
|
| 44 |
+
os.makedirs(os.path.dirname(args.output), exist_ok=True)
|
| 45 |
+
plt.savefig(args.output)
|
| 46 |
+
print(f"Plot saved to {args.output}")
|
| 47 |
+
|
| 48 |
+
if __name__ == "__main__":
|
| 49 |
+
main()
|
plots/plot_reward_curve.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import argparse
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
import os
|
| 5 |
+
|
| 6 |
+
def main():
|
| 7 |
+
parser = argparse.ArgumentParser(description="Plot reward curve from training/eval history.")
|
| 8 |
+
parser.add_argument("--input", type=str, default="eval_results.json", help="Path to evaluation results JSON")
|
| 9 |
+
parser.add_argument("--output", type=str, default="plots/reward_curve.png", help="Path to save the plot")
|
| 10 |
+
args = parser.parse_args()
|
| 11 |
+
|
| 12 |
+
if not os.path.exists(args.input):
|
| 13 |
+
print(f"Error: Input file {args.input} not found.")
|
| 14 |
+
return
|
| 15 |
+
|
| 16 |
+
with open(args.input, "r") as f:
|
| 17 |
+
data = json.load(f)
|
| 18 |
+
|
| 19 |
+
results = data.get("results", [])
|
| 20 |
+
if not results:
|
| 21 |
+
print("No results found to plot.")
|
| 22 |
+
return
|
| 23 |
+
|
| 24 |
+
rewards = [r["total_reward"] for r in results]
|
| 25 |
+
|
| 26 |
+
plt.figure(figsize=(10, 6))
|
| 27 |
+
plt.plot(rewards, marker='o', linestyle='-', color='green', markersize=4, alpha=0.6)
|
| 28 |
+
|
| 29 |
+
# Calculate moving average
|
| 30 |
+
window = 10
|
| 31 |
+
if len(rewards) >= window:
|
| 32 |
+
moving_avg = [sum(rewards[i:i+window])/window for i in range(len(rewards)-window+1)]
|
| 33 |
+
plt.plot(range(window-1, len(rewards)), moving_avg, color='red', linewidth=2, label=f'{window}-sample Moving Avg')
|
| 34 |
+
|
| 35 |
+
plt.xlabel('Sample Index')
|
| 36 |
+
plt.ylabel('Total Reward')
|
| 37 |
+
plt.title('CommitGuard — Evaluation Reward Distribution')
|
| 38 |
+
plt.legend()
|
| 39 |
+
plt.grid(True, linestyle='--', alpha=0.7)
|
| 40 |
+
plt.tight_layout()
|
| 41 |
+
|
| 42 |
+
os.makedirs(os.path.dirname(args.output), exist_ok=True)
|
| 43 |
+
plt.savefig(args.output)
|
| 44 |
+
print(f"Plot saved to {args.output}")
|
| 45 |
+
|
| 46 |
+
if __name__ == "__main__":
|
| 47 |
+
main()
|
plots/reward_curve.png
ADDED
|
plots/wandb_simulated.json
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{"step": 1, "reward": -0.5},
|
| 3 |
+
{"step": 10, "reward": -0.2},
|
| 4 |
+
{"step": 20, "reward": 0.1},
|
| 5 |
+
{"step": 50, "reward": 0.4},
|
| 6 |
+
{"step": 100, "reward": 0.75},
|
| 7 |
+
{"step": 150, "reward": 1.1},
|
| 8 |
+
{"step": 200, "reward": 1.45},
|
| 9 |
+
{"step": 250, "reward": 1.6},
|
| 10 |
+
{"step": 300, "reward": 1.82}
|
| 11 |
+
]
|
prd.md
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# CommitGuard Product Requirements Document
|
| 2 |
+
|
| 3 |
+
**Project:** CommitGuard
|
| 4 |
+
**Owner:** Niti (Inmodel Labs)
|
| 5 |
+
**Team:** Niti, Deepak, Divyank
|
| 6 |
+
**Submission deadline:** Sunday 5:00 PM IST
|
| 7 |
+
**Hackathon:** Meta OpenEnv Hackathon (PyTorch + Hugging Face + Scaler)
|
| 8 |
+
**Document status:** Locked. Scope freeze at midnight Saturday.
|
| 9 |
+
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
## 1. Executive Summary
|
| 13 |
+
|
| 14 |
+
CommitGuard is a Reinforcement Learning environment built on Meta OpenEnv that trains LLM agents to detect exploitable vulnerabilities in code commits. The submission demonstrates that AI-paced security review is feasible that an agent trained on commit-level reasoning can match the velocity at which AI coding agents are now shipping production code.
|
| 15 |
+
|
| 16 |
+
The deliverable is a runnable HF Space hosting the env, a training notebook that produces a measurable learning curve on Llama-3.2-3B-Instruct, a demo video showing the qualitative shift from untrained to trained behavior, and a README that tells the story.
|
| 17 |
+
|
| 18 |
+
---
|
| 19 |
+
|
| 20 |
+
## 2. Problem Statement
|
| 21 |
+
|
| 22 |
+
### 2.1 The shift in software development
|
| 23 |
+
|
| 24 |
+
Until recently, code was written by humans at human velocity. Security review processes were designed around this assumption periodic pentests every 3 to 6 months, with manual code review at PR time. The cycle worked because the codebase changed slowly enough that periodic deep review caught most issues before they reached production.
|
| 25 |
+
|
| 26 |
+
This assumption has broken. Code is now being written and shipped by AI coding agents Claude Code, Cursor, autonomous coding agents at 10 to 100 times human velocity. Companies push to production daily, sometimes hourly. A pentest report from six months ago describes a codebase that no longer exists.
|
| 27 |
+
|
| 28 |
+
### 2.2 The asymmetry
|
| 29 |
+
|
| 30 |
+
The same class of LLM that writes the code can be weaponized to attack it. An adversary equipped with autonomous coding tooling, given repository access or even just leaked commits, can pentest at the same velocity defenders ship. Defense runs on human time. Offense runs on AI time. **This asymmetry is unsustainable for any organization shipping AI-generated code at scale.**
|
| 31 |
+
|
| 32 |
+
### 2.3 Why this is a frontier problem
|
| 33 |
+
|
| 34 |
+
AI red-teaming today is overwhelmingly a manual, human-bottlenecked discipline. Researchers at Anthropic, OpenAI, and Meta craft attacks one at a time. There is no automated equivalent of Metasploit for AI-generated code. Closing that gap is an open research problem that frontier labs are actively investing in.
|
| 35 |
+
|
| 36 |
+
---
|
| 37 |
+
|
| 38 |
+
## 3. Goals and Non-Goals
|
| 39 |
+
|
| 40 |
+
### 3.1 Goals (in scope for this submission)
|
| 41 |
+
|
| 42 |
+
- Deliver a working OpenEnv environment that takes a code commit as input and rewards an agent for correctly identifying vulnerabilities, the CWE class, and a plausible exploit
|
| 43 |
+
- Train a small Llama variant (Llama-3.2-3B-Instruct) on the env using GRPO via TRL + Unsloth
|
| 44 |
+
- Demonstrate measurable learning baseline vs. trained accuracy with reward curves
|
| 45 |
+
- Ship a complete submission package: HF Space, training notebook, README, demo video, optional HF blog post
|
| 46 |
+
- Frame the work in language a Meta researcher recognizes: RLVR (Reinforcement Learning from Verifiable Rewards), commit-time security, AI-paced defense
|
| 47 |
+
|
| 48 |
+
### 3.2 Non-goals (explicitly out of scope)
|
| 49 |
+
|
| 50 |
+
- Production-ready security tool this is a research environment, not a CI plugin
|
| 51 |
+
- Real-time exploit execution against arbitrary code the v1 reward uses pattern matching, not sandboxed execution
|
| 52 |
+
- Multi-file / repo-level reasoning v1 operates on single-file commits up to 80 lines
|
| 53 |
+
- Multi-agent self-play listed in Future Work
|
| 54 |
+
- Pentesting beyond static code analysis no network attacks, social engineering, or runtime probing
|
| 55 |
+
- Coverage of all CWEs v1 focuses on the top 10 CWEs in Devign
|
| 56 |
+
|
| 57 |
+
### 3.3 Non-goals from the rubric perspective
|
| 58 |
+
|
| 59 |
+
The rubric rewards ambition and storytelling more heavily than engineering polish. Therefore: not pursuing exhaustive test coverage, not optimizing for inference latency, not building a fancy frontend. The HF Space's default web UI is sufficient.
|
| 60 |
+
|
| 61 |
+
---
|
| 62 |
+
|
| 63 |
+
## 4. Target Users and Stakeholders
|
| 64 |
+
|
| 65 |
+
| Stakeholder | Role | What they care about |
|
| 66 |
+
|---|---|---|
|
| 67 |
+
| Hackathon judges (Meta partner engineers) | Primary audience | Innovation, story, training evidence, reward design |
|
| 68 |
+
| Meta Superintelligence Labs researchers | Aspirational audience | Frontier framing, RLVR alignment, paper-worthiness |
|
| 69 |
+
| HF community | Discovery audience | Reproducibility, runnable Space, clean README |
|
| 70 |
+
| Future contributors | Builder audience | Code clarity, extensibility hooks for v2 |
|
| 71 |
+
|
| 72 |
+
---
|
| 73 |
+
|
| 74 |
+
## 5. Solution Overview
|
| 75 |
+
|
| 76 |
+
### 5.1 The environment
|
| 77 |
+
|
| 78 |
+
CommitGuard is an OpenEnv environment where an agent investigates code commits and decides whether they introduce exploitable vulnerabilities. The agent has limited investigation budget (5 steps maximum per episode), forcing it to reason efficiently rather than brute-forcing context.
|
| 79 |
+
|
| 80 |
+
### 5.2 The agent loop
|
| 81 |
+
|
| 82 |
+
1. `reset()` env loads a commit (a `code_before`/`code_after` pair plus metadata) from a preprocessed Devign-derived dataset, returns the diff and the list of available files in the repo
|
| 83 |
+
2. `step(action)` agent emits one of three action types:
|
| 84 |
+
- `request_context(file_path)` pull surrounding code (small reward penalty, encourages efficiency)
|
| 85 |
+
- `analyze(reasoning)` write chain-of-thought, no reward effect, logged for traces
|
| 86 |
+
- `verdict(is_vulnerable, vuln_type, exploit_sketch)` terminate the episode with a judgment
|
| 87 |
+
3. Reward fires on verdict, computed server-side against ground truth the agent never sees
|
| 88 |
+
|
| 89 |
+
### 5.3 Reward design (RLVR philosophy)
|
| 90 |
+
|
| 91 |
+
The reward is tiered and grounded in dataset truth, not in another LLM's opinion. This is deliberate it follows the RLVR tradition (verifiable rewards from ground truth or executable checks) and prevents the reward hacking that plagues LLM-as-judge setups.
|
| 92 |
+
|
| 93 |
+
| Signal | Reward |
|
| 94 |
+
|---|---|
|
| 95 |
+
| Correct binary verdict (vulnerable vs. safe) | +1.0 |
|
| 96 |
+
| Correct CWE classification (when vulnerable) | +0.5 |
|
| 97 |
+
| Plausible exploit sketch (CWE-keyword match) | +0.5 |
|
| 98 |
+
| False positive (safe flagged as vulnerable) | -1.0 |
|
| 99 |
+
| False negative (real vuln missed) | -0.5 |
|
| 100 |
+
| Per-step context request | -0.05 |
|
| 101 |
+
| Episode step cap | 5 steps |
|
| 102 |
+
|
| 103 |
+
The shape is hard to game flagging everything is punished by false positives, never investigating means no exploit sketch bonus.
|
| 104 |
+
|
| 105 |
+
---
|
| 106 |
+
|
| 107 |
+
## 6. Technical Architecture
|
| 108 |
+
|
| 109 |
+
### 6.1 System diagram
|
| 110 |
+
|
| 111 |
+
```
|
| 112 |
+
HTTP/JSON
|
| 113 |
+
TRL + Unsloth HF Space
|
| 114 |
+
Llama-3.2-3B reset/step FastAPI server
|
| 115 |
+
GRPO trainer /state (Docker)
|
| 116 |
+
(HF Jobs A10G)
|
| 117 |
+
|
| 118 |
+
Devign
|
| 119 |
+
JSONL
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
Reward
|
| 123 |
+
function
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
```
|
| 127 |
+
|
| 128 |
+
### 6.2 Component breakdown
|
| 129 |
+
|
| 130 |
+
**Env server** (Python, FastAPI, Docker, OpenEnv 0.2.3+)
|
| 131 |
+
- `models.py` Action, Observation, State dataclasses (extends OpenEnv base classes)
|
| 132 |
+
- `environment.py` `reset()`, `step()`, `state()` methods on the `CommitGuardEnvironment` class
|
| 133 |
+
- `reward.py` pure function `compute_reward(action, ground_truth, cwe_keywords) -> float`
|
| 134 |
+
- `parse_action.py` XML-tag parser, robust to malformed model output
|
| 135 |
+
- `data/devign_filtered.jsonl` preprocessed dataset, shipped in image
|
| 136 |
+
- `data/cwe_keywords.json` top-10 CWE exploit-pattern keyword map
|
| 137 |
+
|
| 138 |
+
**Env client** (auto-generated by OpenEnv CLI)
|
| 139 |
+
- `client.py` `HTTPEnvClient` subclass, used by training notebook
|
| 140 |
+
- Installable via `pip install git+https://huggingface.co/spaces/<user>/commitguard`
|
| 141 |
+
|
| 142 |
+
**Training pipeline** (Python, TRL, Unsloth, PEFT, Wandb)
|
| 143 |
+
- `train_grpo.py` GRPOTrainer config + main loop
|
| 144 |
+
- `agent_prompt.py` system prompt template with XML-tag action format
|
| 145 |
+
- `evaluate.py` runs N samples through a model, returns accuracy stats
|
| 146 |
+
|
| 147 |
+
**Storytelling artifacts**
|
| 148 |
+
- `README.md` pitch + results + links
|
| 149 |
+
- `demo_video.mp4` 60-90 second before/after, hosted on YouTube unlisted
|
| 150 |
+
- `commitguard_hf_blog.md` optional HF Hub blog post (page 26 bonus)
|
| 151 |
+
- `plots/` reward_curve.png, baseline_vs_trained.png, per_cwe.png
|
| 152 |
+
|
| 153 |
+
### 6.3 Data flow
|
| 154 |
+
|
| 155 |
+
1. Preprocess Devign once at build time `data/devign_filtered.jsonl` (~5000 samples, balanced, filtered to <80 LOC)
|
| 156 |
+
2. Build Docker image with JSONL embedded
|
| 157 |
+
3. `openenv push` deploys to HF Space
|
| 158 |
+
4. Training notebook connects to HF Space URL via the OpenEnv HTTP client
|
| 159 |
+
5. Each training step: GRPO generates 4 completions per prompt each runs a full episode in the env rewards collected policy updated via LoRA
|
| 160 |
+
6. Wandb logs reward curves, training loss, checkpoints saved every 50 steps
|
| 161 |
+
7. Final LoRA adapter saved to HF Hub for evaluation and demo
|
| 162 |
+
|
| 163 |
+
### 6.4 Cheating prevention
|
| 164 |
+
|
| 165 |
+
The agent must never see ground truth. Enforced by architecture:
|
| 166 |
+
|
| 167 |
+
- Ground truth lives only on the server, in the JSONL file the env loads from
|
| 168 |
+
- The Observation dataclass schema explicitly excludes `is_vulnerable`, `cwe_type`, and `target_file_with_label`
|
| 169 |
+
- A unit test (`test_no_leak.py`) asserts no observation contains forbidden fields
|
| 170 |
+
- The server returns only `reward` (a scalar) on each step, never the label that produced it
|
| 171 |
+
|
| 172 |
+
---
|
| 173 |
+
|
| 174 |
+
## 7. Stack and Dependencies
|
| 175 |
+
|
| 176 |
+
### 7.1 Locked technical decisions
|
| 177 |
+
|
| 178 |
+
| Decision | Choice | Rationale |
|
| 179 |
+
|---|---|---|
|
| 180 |
+
| Env framework | Meta OpenEnv 0.2.3+ | Mandatory per submission rules |
|
| 181 |
+
| Server runtime | FastAPI in Docker | OpenEnv default, lowest friction |
|
| 182 |
+
| Hosting | HF Space | Mandatory per submission rules, three-in-one (server + repo + registry) |
|
| 183 |
+
| Data source | Devign (DetectBERT subset) | Already on disk, real CWE labels, manageable size |
|
| 184 |
+
| Model | Llama-3.2-3B-Instruct | Meta-branded for the Meta hackathon, fits A10G with GRPO |
|
| 185 |
+
| Training framework | TRL with GRPO | Native OpenEnv integration via `reward_funcs` callback |
|
| 186 |
+
| Training optimization | Unsloth 4-bit + LoRA r=8 | 70% memory reduction, 2x speed (page 75 of opening deck) |
|
| 187 |
+
| Training infra | HF Jobs A10G | $0.40-1.50/hr, runs unattended, integrates with HF ecosystem |
|
| 188 |
+
| Dev infra | GCP VM with T4 | Stable, no Colab disconnects, leverages 24,000 GCP credit |
|
| 189 |
+
| Action serialization | XML-tag free-text | Robust to small-model output variance, easier than JSON-mode |
|
| 190 |
+
| Logging | Wandb | TRL native, judges can view runs |
|
| 191 |
+
|
| 192 |
+
### 7.2 Fallback decisions (pre-approved, no debate when triggered)
|
| 193 |
+
|
| 194 |
+
| If this fails | Fall back to | Trigger |
|
| 195 |
+
|---|---|---|
|
| 196 |
+
| Llama-3.2-3B OOM on A10G | Qwen2.5-1.5B-Instruct | First test step crashes |
|
| 197 |
+
| HF Jobs queue full | GCP A10G on-demand | Job queues for >30 min |
|
| 198 |
+
| 3-action env doesn't ship by midnight | 2-action env (analyze + verdict) | Niti's checkpoint red |
|
| 199 |
+
| Tiered reward buggy | Binary correct/incorrect reward | Deepak's checkpoint red |
|
| 200 |
+
| Training curve flat | Ship with qualitative comparison only | Curve still flat at 10 AM Sunday |
|
| 201 |
+
| Demo video can't be cleanly recorded | Side-by-side text trace in README | Recording fails twice |
|
| 202 |
+
|
| 203 |
+
---
|
| 204 |
+
|
| 205 |
+
## 8. Functional Requirements
|
| 206 |
+
|
| 207 |
+
### 8.1 Environment functional requirements
|
| 208 |
+
|
| 209 |
+
| ID | Requirement | Priority |
|
| 210 |
+
|---|---|---|
|
| 211 |
+
| F-1 | Env exposes `/health`, `/reset`, `/step`, `/state`, `/docs` endpoints | P0 |
|
| 212 |
+
| F-2 | `reset()` returns a random commit observation, never the same one twice in a single episode | P0 |
|
| 213 |
+
| F-3 | `step()` accepts XML-tagged action strings and parses them robustly | P0 |
|
| 214 |
+
| F-4 | `step()` returns reward, observation, and done flag | P0 |
|
| 215 |
+
| F-5 | Episode terminates on `verdict` action OR after 5 steps | P0 |
|
| 216 |
+
| F-6 | Observation never contains ground-truth labels | P0 |
|
| 217 |
+
| F-7 | Env handles malformed actions gracefully (returns -0.5 reward, doesn't crash) | P1 |
|
| 218 |
+
| F-8 | Env supports concurrent episodes (multiple training generations in parallel) | P1 |
|
| 219 |
+
| F-9 | Web UI on HF Space allows manual interaction for demo recording | P2 |
|
| 220 |
+
|
| 221 |
+
### 8.2 Training functional requirements
|
| 222 |
+
|
| 223 |
+
| ID | Requirement | Priority |
|
| 224 |
+
|---|---|---|
|
| 225 |
+
| T-1 | Training notebook runs end-to-end on a single A10G | P0 |
|
| 226 |
+
| T-2 | Reward curve, training loss, and completions logged to Wandb | P0 |
|
| 227 |
+
| T-3 | LoRA adapter saved every 50 steps for resumability | P0 |
|
| 228 |
+
| T-4 | Baseline (untrained) evaluation on 100 held-out samples completes in <10 min | P0 |
|
| 229 |
+
| T-5 | Trained model evaluation produces per-CWE accuracy breakdown | P1 |
|
| 230 |
+
| T-6 | Notebook runnable from Colab via "Open in Colab" badge in README | P1 |
|
| 231 |
+
|
| 232 |
+
### 8.3 Storytelling functional requirements
|
| 233 |
+
|
| 234 |
+
| ID | Requirement | Priority |
|
| 235 |
+
|---|---|---|
|
| 236 |
+
| S-1 | README explains problem, env, results, and motivation in <5 min read | P0 |
|
| 237 |
+
| S-2 | All plot PNGs committed to repo (not Wandb-only) | P0 |
|
| 238 |
+
| S-3 | Demo video 60-90 sec, before/after on a single SQL injection example | P0 |
|
| 239 |
+
| S-4 | Wandb run URL linked in README | P1 |
|
| 240 |
+
| S-5 | HF Hub blog post published and linked | P2 |
|
| 241 |
+
|
| 242 |
+
---
|
| 243 |
+
|
| 244 |
+
## 9. Non-Functional Requirements
|
| 245 |
+
|
| 246 |
+
| Aspect | Requirement |
|
| 247 |
+
|---|---|
|
| 248 |
+
| Performance | Single `step()` call returns in <2 seconds on HF Space free tier |
|
| 249 |
+
| Reliability | Env survives 100 random episodes without crash |
|
| 250 |
+
| Reproducibility | Training notebook produces a measurable learning curve when re-run with same seed |
|
| 251 |
+
| Discoverability | HF Space tagged with `openenv`, `rl`, `security`, `code` |
|
| 252 |
+
| Documentation | README is self-contained judge can understand without reading source |
|
| 253 |
+
| Licensing | Code MIT-licensed, dataset attribution to Devign authors |
|
| 254 |
+
|
| 255 |
+
---
|
| 256 |
+
|
| 257 |
+
## 10. Success Metrics
|
| 258 |
+
|
| 259 |
+
### 10.1 Submission completeness (binary, must-pass)
|
| 260 |
+
|
| 261 |
+
- [ ] HF Space deployed and `/health` returns 200 OK
|
| 262 |
+
- [ ] Training notebook runs without crashes on a fresh Colab/VM
|
| 263 |
+
- [ ] README has all required links (HF Space, notebook, video, GitHub)
|
| 264 |
+
- [ ] At least one reward curve plot committed
|
| 265 |
+
- [ ] Demo video accessible via public URL
|
| 266 |
+
|
| 267 |
+
### 10.2 Quality metrics (graded by rubric)
|
| 268 |
+
|
| 269 |
+
| Metric | Target | Stretch |
|
| 270 |
+
|---|---|---|
|
| 271 |
+
| Innovation framing recognized by mentor | "this is an interesting angle" feedback | "this is paper-worthy" feedback |
|
| 272 |
+
| Baseline accuracy (untrained Llama-3.2-3B) | Establishes a floor (likely 30-45%) | |
|
| 273 |
+
| Trained accuracy (after 300 GRPO steps) | Beats baseline by 10pp absolute | Beats baseline by 20pp |
|
| 274 |
+
| Reward curve | Bends upward visibly | Smooth monotonic increase |
|
| 275 |
+
| Per-CWE breakdown | At least 3 CWEs show improvement | All top-5 CWEs show improvement |
|
| 276 |
+
| Storytelling | Mentor at Round 3 can repeat the pitch back | Mentor offers to share with Meta team |
|
| 277 |
+
|
| 278 |
+
### 10.3 Anti-metrics (things we explicitly don't optimize for)
|
| 279 |
+
|
| 280 |
+
- Number of features
|
| 281 |
+
- Number of CWEs covered (more is not better depth beats breadth here)
|
| 282 |
+
- Lines of code
|
| 283 |
+
- Model size (going larger doesn't make a stronger submission, just slower training)
|
| 284 |
+
|
| 285 |
+
---
|
| 286 |
+
|
| 287 |
+
## 11. Risks and Mitigations
|
| 288 |
+
|
| 289 |
+
| Risk | Likelihood | Impact | Mitigation |
|
| 290 |
+
|---|---|---|---|
|
| 291 |
+
| Training run produces flat curve | Medium | High | Pre-approved pivot to qualitative-comparison narrative; baseline already establishes a contrast |
|
| 292 |
+
| HF Space deployment fails at 4 AM | Low | High | Fallback to Docker image with `docker run` instructions in README |
|
| 293 |
+
| Llama-3.2 license approval delayed | Low | Medium | Submit license request immediately at GCP setup; Qwen-1.5B fallback ready |
|
| 294 |
+
| Devign data has bad CWE labels | Medium | Medium | Filter aggressively; if too noisy, drop to top-5 cleanest CWEs only |
|
| 295 |
+
| One teammate falls behind their phase | Medium | High | Sync points at midnight, 9 AM, 3 PM allow scope cuts; mock-env pattern means training isn't blocked |
|
| 296 |
+
| Niti exhausted at Mentor Round 3 | High if no sleep | High | Mandatory sleep schedule 12:30 AM5:00 AM, non-negotiable |
|
| 297 |
+
| Demo video can't be cleanly recorded | Medium | Medium | Cherry-pick the best example; fall back to text trace if recording fails twice |
|
| 298 |
+
| HF Space rate limits during training | Low | Medium | Run training on local Docker if HF Space hits limits |
|
| 299 |
+
|
| 300 |
+
---
|
| 301 |
+
|
| 302 |
+
## 12. Timeline and Milestones
|
| 303 |
+
|
| 304 |
+
| Time (IST) | Milestone | Owner |
|
| 305 |
+
|---|---|---|
|
| 306 |
+
| Sat 9:30 PM | Phase 1 starts env scaffolding, data prep, training scaffolding in parallel | All |
|
| 307 |
+
| Sat 8:00 PM | Mentor Round 2 pitch validation | Niti |
|
| 308 |
+
| Sat 11:59 PM | Phase 1 checkpoint env runs, data ready, mock training works | All |
|
| 309 |
+
| Sun 12:00 AM | **Scope freeze** no new features after this point | All |
|
| 310 |
+
| Sun 12:30 AM | Niti sleep starts | Niti |
|
| 311 |
+
| Sun 3:00 AM | HF Space live, Deepak sleep starts | Deepak |
|
| 312 |
+
| Sun 5:30 AM | Real training run launched on HF Jobs, Divyank sleep starts | Divyank |
|
| 313 |
+
| Sun 5:00 AM | Niti wakes, watches training | Niti |
|
| 314 |
+
| Sun 9:00 AM | Team sync training results, plot status | All |
|
| 315 |
+
| Sun 10:00 AM | Mentor Round 3 final sharpening | Niti |
|
| 316 |
+
| Sun 11:30 AM | Demo video recorded and uploaded | Divyank |
|
| 317 |
+
| Sun 1:00 PM | README finalized | Niti |
|
| 318 |
+
| Sun 3:00 PM | **Feature freeze** 2-hour reminder, no more changes | All |
|
| 319 |
+
| Sun 4:30 PM | Submission packaged | Niti |
|
| 320 |
+
| Sun 5:00 PM | **Submission deadline** | |
|
| 321 |
+
|
| 322 |
+
---
|
| 323 |
+
|
| 324 |
+
## 13. Open Questions and Assumptions
|
| 325 |
+
|
| 326 |
+
### 13.1 Assumptions
|
| 327 |
+
|
| 328 |
+
- Devign dataset is on disk locally (or downloadable in <30 min) to be verified by Deepak at Phase 1 start
|
| 329 |
+
- HF Space free tier is sufficient for env hosting during the hackathon backup plan: $9/mo upgrade if rate limited
|
| 330 |
+
- Llama-3.2-3B-Instruct license approval lands within 1 hour of request Qwen fallback ready if not
|
| 331 |
+
- HF Jobs A10G availability at 5 AM Sunday GCP A10G fallback if queued
|
| 332 |
+
|
| 333 |
+
### 13.2 Open questions (to resolve during execution)
|
| 334 |
+
|
| 335 |
+
- Exact number of training steps to maximize curve visibility within budget answered empirically by 9 AM Sunday based on observed loss
|
| 336 |
+
- Whether to ship a Colab-runnable notebook AND an HF Jobs notebook, or just one defer to Divyank's call at Phase 2
|
| 337 |
+
- Whether to include a comparison against a non-RL baseline (pure SFT or zero-shot) stretch only
|
| 338 |
+
|
| 339 |
+
---
|
| 340 |
+
|
| 341 |
+
## 14. Future Work (Post-Hackathon)
|
| 342 |
+
|
| 343 |
+
This section becomes part of the README's "What's Next" pitch explicitly signals to judges that we understand the limitations and have a roadmap.
|
| 344 |
+
|
| 345 |
+
- **Sandboxed exploit execution** replace pattern-match reward with actual exploit runs against compiled code in a Docker sandbox
|
| 346 |
+
- **Multi-file commit reasoning** extend the env to support diffs spanning multiple files, with a context budget
|
| 347 |
+
- **Self-play loop** pair CommitGuard with a code-generation agent; defender and attacker train against each other (the AlphaGo pattern for security)
|
| 348 |
+
- **Agentic harness integration** wire into real CI pipelines via the OpenEnv MCP layer, enabling commit-time security review at PR open
|
| 349 |
+
- **Real CVE corpus** extend beyond Devign to recent CVE-tagged commits from major open-source repos
|
| 350 |
+
- **Multi-language support** current env is C-focused via Devign; extend to Python, JavaScript, Go
|
| 351 |
+
- **Reward shape ablations** formal study of how reward composition affects which vulnerability types the model learns fastest
|
| 352 |
+
|
| 353 |
+
---
|
| 354 |
+
|
| 355 |
+
## 15. Appendix
|
| 356 |
+
|
| 357 |
+
### 15.1 Key reference URLs (for the team to bookmark)
|
| 358 |
+
|
| 359 |
+
- OpenEnv repo: https://github.com/meta-pytorch/OpenEnv
|
| 360 |
+
- OpenEnv Scaler intro: https://tinyurl.com/openenv-scaler
|
| 361 |
+
- TRL OpenEnv docs: https://huggingface.co/docs/trl/en/openenv
|
| 362 |
+
- TRL Sudoku GRPO example: https://github.com/huggingface/trl/blob/main/examples/notebooks/openenv_sudoku_grpo.ipynb
|
| 363 |
+
- TRL Wordle GRPO example: https://github.com/huggingface/trl/blob/main/examples/notebooks/openenv_wordle_grpo.ipynb
|
| 364 |
+
- Unsloth 2048 example: https://github.com/meta-pytorch/OpenEnv/blob/main/tutorial/examples/unsloth_2048.ipynb
|
| 365 |
+
- Llama-3.2-3B model card: https://huggingface.co/meta-llama/Llama-3.2-3B-Instruct
|
| 366 |
+
- HF Jobs docs: https://huggingface.co/docs/hub/jobs
|
| 367 |
+
- Cursor credits: https://tinyurl.com/sclr-openenv-dashboard
|
| 368 |
+
- HF $30 credits: https://huggingface.co/coupons/claim/hf-openenv-community
|
| 369 |
+
|
| 370 |
+
### 15.2 Document version
|
| 371 |
+
|
| 372 |
+
- v1.0 Saturday evening, Bangalore venue. Locked at midnight Saturday.
|
| 373 |
+
- Changes after lock require explicit team-wide sign-off and a documented rationale.
|
| 374 |
+
|
| 375 |
+
---
|
| 376 |
+
|
| 377 |
+
## 16. The 30-Second Pitch (For Mentor Rounds, Memorize This)
|
| 378 |
+
|
| 379 |
+
> "AI is now writing production code at AI speed. Security review still runs on a 6-month human cycle. The same LLMs that write the code can attack it defense is on human time, offense is on AI time, and that asymmetry breaks the security model.
|
| 380 |
+
>
|
| 381 |
+
> CommitGuard is an OpenEnv where an agent learns to flag exploitable diffs at commit time. We trained Llama-3.2-3B on it via GRPO and the detection rate climbs measurably. It's RLVR verifiable rewards from ground truth, not LLM judges. The thesis: continuous AI red-teaming at the velocity code is being shipped. This is the environment to train it."
|
pyproject.toml
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[project]
|
| 2 |
+
name = "commitguard"
|
| 3 |
+
version = "0.1.0"
|
| 4 |
+
description = "CommitGuard OpenEnv RL environment for commit-time vuln detection (hackathon submission)"
|
| 5 |
+
readme = "README.md"
|
| 6 |
+
requires-python = ">=3.10"
|
| 7 |
+
dependencies = [
|
| 8 |
+
"fastapi>=0.110",
|
| 9 |
+
"uvicorn[standard]>=0.27",
|
| 10 |
+
"pydantic>=2.6",
|
| 11 |
+
"openenv>=0.1.13",
|
| 12 |
+
]
|
| 13 |
+
|
| 14 |
+
[project.optional-dependencies]
|
| 15 |
+
train = [
|
| 16 |
+
"requests",
|
| 17 |
+
"torch",
|
| 18 |
+
"transformers",
|
| 19 |
+
"trl",
|
| 20 |
+
"accelerate",
|
| 21 |
+
"peft",
|
| 22 |
+
"datasets",
|
| 23 |
+
"wandb",
|
| 24 |
+
"matplotlib",
|
| 25 |
+
]
|
| 26 |
+
|
| 27 |
+
[project.scripts]
|
| 28 |
+
server = "commitguard_env.server:main"
|
| 29 |
+
|
| 30 |
+
[tool.setuptools]
|
| 31 |
+
packages = ["commitguard_env"]
|
| 32 |
+
|
| 33 |
+
[build-system]
|
| 34 |
+
requires = ["setuptools>=68"]
|
| 35 |
+
build-backend = "setuptools.build_meta"
|
| 36 |
+
|
scripts/README.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
## Scripts
|
| 2 |
+
|
| 3 |
+
This directory is for repeatable CLI-first ops (dataset preprocessing, local smoke runs).
|
| 4 |
+
|
| 5 |
+
Primary expected script (Deepak):
|
| 6 |
+
- `preprocess_devign.py` → produces `data/devign_filtered.jsonl`
|
| 7 |
+
|
scripts/agent_prompt.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""System prompt and per-turn prompt for CommitGuard GRPO training."""
|
| 2 |
+
|
| 3 |
+
SYSTEM_PROMPT = """\
|
| 4 |
+
You are a security auditor. You receive code diffs (commits) and must decide \
|
| 5 |
+
whether each commit introduces an exploitable vulnerability.
|
| 6 |
+
|
| 7 |
+
You may take up to 5 actions per episode. Each action must be wrapped in XML tags.
|
| 8 |
+
|
| 9 |
+
Action types:
|
| 10 |
+
|
| 11 |
+
1. Request additional file context:
|
| 12 |
+
<action><action_type>request_context</action_type><file_path>path/to/file.c</file_path></action>
|
| 13 |
+
|
| 14 |
+
2. Analyze / think (chain-of-thought, no reward effect):
|
| 15 |
+
<action><action_type>analyze</action_type><reasoning>your reasoning here</reasoning></action>
|
| 16 |
+
|
| 17 |
+
3. Submit a verdict (terminates the episode):
|
| 18 |
+
<action><action_type>verdict</action_type><is_vulnerable>true|false</is_vulnerable><vuln_type>CWE-XXX</vuln_type><exploit_sketch>describe how to exploit</exploit_sketch></action>
|
| 19 |
+
|
| 20 |
+
Rules:
|
| 21 |
+
- You MUST submit exactly one verdict before running out of budget.
|
| 22 |
+
- If the code is safe, set is_vulnerable to false and vuln_type to NONE.
|
| 23 |
+
- Be specific in exploit_sketch: name the attack vector (e.g., buffer overflow via unchecked memcpy).
|
| 24 |
+
- Common CWE types: CWE-79 (XSS), CWE-89 (SQL injection), CWE-22 (path traversal), \
|
| 25 |
+
CWE-78 (command injection), CWE-20 (input validation), CWE-125 (out-of-bounds read), \
|
| 26 |
+
CWE-787 (buffer overflow), CWE-190 (integer overflow), CWE-476 (null dereference), \
|
| 27 |
+
CWE-400 (resource exhaustion).
|
| 28 |
+
"""
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def get_agent_prompt(diff: str, available_files: list[str], step_idx: int) -> str:
|
| 32 |
+
files_str = ", ".join(available_files) if available_files else "(none)"
|
| 33 |
+
return (
|
| 34 |
+
f"## Commit Diff\n\n```diff\n{diff}\n```\n\n"
|
| 35 |
+
f"Available files: {files_str}\n"
|
| 36 |
+
f"Step: {step_idx}/5\n\n"
|
| 37 |
+
"Analyze this commit and submit your verdict."
|
| 38 |
+
)
|
scripts/evaluate.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import argparse
|
| 3 |
+
import os
|
| 4 |
+
import requests
|
| 5 |
+
from typing import Any
|
| 6 |
+
from commitguard_env.parse_action import parse_action
|
| 7 |
+
|
| 8 |
+
def run_episode(env_url: str, sample_id: str, model_client: Any = None) -> float:
|
| 9 |
+
"""
|
| 10 |
+
Runs a full 5-step episode for a single sample.
|
| 11 |
+
"""
|
| 12 |
+
# 1. Reset
|
| 13 |
+
# In a real evaluate, we'd need a reset_to_id endpoint or just loop reset until ID matches.
|
| 14 |
+
# For now, we assume reset gives us a random sample and we track it.
|
| 15 |
+
r = requests.post(f"{env_url}/reset")
|
| 16 |
+
data = r.json()
|
| 17 |
+
obs = data["observation"]
|
| 18 |
+
|
| 19 |
+
total_reward = 0.0
|
| 20 |
+
done = False
|
| 21 |
+
step_count = 0
|
| 22 |
+
|
| 23 |
+
while not done and step_count < 5:
|
| 24 |
+
# Prompt model (Simplified for script)
|
| 25 |
+
if model_client:
|
| 26 |
+
action_str = model_client.generate(obs['diff'], obs['available_files'])
|
| 27 |
+
else:
|
| 28 |
+
# Mock: straight to verdict for evaluation baseline
|
| 29 |
+
action_str = f"<action><action_type>verdict</action_type><is_vulnerable>true</is_vulnerable></action>"
|
| 30 |
+
|
| 31 |
+
r = requests.post(f"{env_url}/step", json={"action": action_str})
|
| 32 |
+
res = r.json()
|
| 33 |
+
|
| 34 |
+
obs = res["observation"]
|
| 35 |
+
total_reward = res["reward"] # Environment returns cumulative or step reward?
|
| 36 |
+
# In CommitGuard, reward at verdict includes the outcome.
|
| 37 |
+
done = res["done"]
|
| 38 |
+
step_count += 1
|
| 39 |
+
|
| 40 |
+
return total_reward
|
| 41 |
+
|
| 42 |
+
def evaluate(env_url: str, test_file: str, adapter_path: str = None):
|
| 43 |
+
with open(test_file, "r") as f:
|
| 44 |
+
test_samples = [json.loads(line) for line in f]
|
| 45 |
+
|
| 46 |
+
# Loading model if adapter provided
|
| 47 |
+
model_client = None
|
| 48 |
+
if adapter_path:
|
| 49 |
+
print(f"Loading LoRA adapter from {adapter_path}...")
|
| 50 |
+
# (Integration with Unsloth/Peft would go here)
|
| 51 |
+
pass
|
| 52 |
+
|
| 53 |
+
results = []
|
| 54 |
+
print(f"Starting evaluation on {len(test_samples)} samples...")
|
| 55 |
+
|
| 56 |
+
for sample in test_samples:
|
| 57 |
+
reward = run_episode(env_url, sample["commit_id"], model_client)
|
| 58 |
+
results.append({
|
| 59 |
+
"commit_id": sample["commit_id"],
|
| 60 |
+
"reward": reward,
|
| 61 |
+
"cwe": sample.get("cwe_type")
|
| 62 |
+
})
|
| 63 |
+
|
| 64 |
+
avg_reward = sum(r["reward"] for r in results) / len(results)
|
| 65 |
+
print(f"Evaluation Complete. Average Reward: {avg_reward:.4f}")
|
| 66 |
+
|
| 67 |
+
with open("eval_results.json", "w") as f:
|
| 68 |
+
json.dump(results, f, indent=2)
|
| 69 |
+
|
| 70 |
+
if __name__ == "__main__":
|
| 71 |
+
parser = argparse.ArgumentParser()
|
| 72 |
+
parser.add_argument("--env-url", default="http://localhost:8000")
|
| 73 |
+
parser.add_argument("--test-file", default="data/devign_test.jsonl")
|
| 74 |
+
parser.add_argument("--adapter-path", default=None)
|
| 75 |
+
args = parser.parse_args()
|
| 76 |
+
|
| 77 |
+
evaluate(args.env_url, args.test_file, args.adapter_path)
|
scripts/gce_vm_runbook.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
## GCE VM Runbook — CommitGuard GRPO Training
|
| 2 |
+
|
| 3 |
+
### Step 1: Create VM
|
| 4 |
+
|
| 5 |
+
Run from your local machine (or use GCP Console):
|
| 6 |
+
|
| 7 |
+
```bash
|
| 8 |
+
# Option A: L4 (24 GB VRAM, ~$0.70/hr) — RECOMMENDED
|
| 9 |
+
gcloud compute instances create commitguard-train \
|
| 10 |
+
--zone=us-central1-a \
|
| 11 |
+
--machine-type=g2-standard-8 \
|
| 12 |
+
--accelerator=type=nvidia-l4,count=1 \
|
| 13 |
+
--boot-disk-size=100GB \
|
| 14 |
+
--image-family=pytorch-latest-gpu \
|
| 15 |
+
--image-project=deeplearning-platform-release \
|
| 16 |
+
--maintenance-policy=TERMINATE \
|
| 17 |
+
--metadata="install-nvidia-driver=True"
|
| 18 |
+
|
| 19 |
+
# Option B: A100 (40 GB VRAM, ~$2.50/hr) — if L4 unavailable
|
| 20 |
+
gcloud compute instances create commitguard-train \
|
| 21 |
+
--zone=us-central1-a \
|
| 22 |
+
--machine-type=a2-highgpu-1g \
|
| 23 |
+
--accelerator=type=nvidia-tesla-a100,count=1 \
|
| 24 |
+
--boot-disk-size=100GB \
|
| 25 |
+
--image-family=pytorch-latest-gpu \
|
| 26 |
+
--image-project=deeplearning-platform-release \
|
| 27 |
+
--maintenance-policy=TERMINATE \
|
| 28 |
+
--metadata="install-nvidia-driver=True"
|
| 29 |
+
|
| 30 |
+
# Option C: T4 (16 GB VRAM, ~$0.35/hr) — budget fallback
|
| 31 |
+
gcloud compute instances create commitguard-train \
|
| 32 |
+
--zone=us-central1-b \
|
| 33 |
+
--machine-type=n1-standard-8 \
|
| 34 |
+
--accelerator=type=nvidia-tesla-t4,count=1 \
|
| 35 |
+
--boot-disk-size=100GB \
|
| 36 |
+
--image-family=pytorch-latest-gpu \
|
| 37 |
+
--image-project=deeplearning-platform-release \
|
| 38 |
+
--maintenance-policy=TERMINATE \
|
| 39 |
+
--metadata="install-nvidia-driver=True"
|
| 40 |
+
```
|
| 41 |
+
|
| 42 |
+
### Step 2: SSH into VM
|
| 43 |
+
|
| 44 |
+
```bash
|
| 45 |
+
gcloud compute ssh commitguard-train --zone=us-central1-a
|
| 46 |
+
```
|
| 47 |
+
|
| 48 |
+
### Step 3: One-command setup
|
| 49 |
+
|
| 50 |
+
```bash
|
| 51 |
+
curl -sSL https://raw.githubusercontent.com/NitishKumar-ai/commitguard/main/scripts/gcp_setup.sh | bash
|
| 52 |
+
```
|
| 53 |
+
|
| 54 |
+
Or manually:
|
| 55 |
+
|
| 56 |
+
```bash
|
| 57 |
+
git clone https://github.com/NitishKumar-ai/commitguard.git
|
| 58 |
+
cd commitguard
|
| 59 |
+
bash scripts/gcp_setup.sh
|
| 60 |
+
```
|
| 61 |
+
|
| 62 |
+
### Step 4: Start env server (in tmux)
|
| 63 |
+
|
| 64 |
+
```bash
|
| 65 |
+
cd ~/commitguard && source .venv/bin/activate
|
| 66 |
+
tmux new -s server
|
| 67 |
+
server
|
| 68 |
+
# Ctrl-B D to detach
|
| 69 |
+
```
|
| 70 |
+
|
| 71 |
+
Verify:
|
| 72 |
+
|
| 73 |
+
```bash
|
| 74 |
+
curl -s http://localhost:8000/health
|
| 75 |
+
# → {"status":"healthy"}
|
| 76 |
+
```
|
| 77 |
+
|
| 78 |
+
### Step 5: Login to HuggingFace + Wandb
|
| 79 |
+
|
| 80 |
+
```bash
|
| 81 |
+
source ~/commitguard/.venv/bin/activate
|
| 82 |
+
huggingface-cli login # paste your HF token (needed for Llama gated model)
|
| 83 |
+
wandb login # paste your wandb API key
|
| 84 |
+
```
|
| 85 |
+
|
| 86 |
+
### Step 6: Start training
|
| 87 |
+
|
| 88 |
+
```bash
|
| 89 |
+
cd ~/commitguard && source .venv/bin/activate
|
| 90 |
+
export WANDB_PROJECT=commitguard
|
| 91 |
+
|
| 92 |
+
# Full run (~2-3 hours on L4)
|
| 93 |
+
python scripts/train_grpo.py \
|
| 94 |
+
--samples 200 \
|
| 95 |
+
--max-steps 300 \
|
| 96 |
+
--save-steps 50 \
|
| 97 |
+
--num-generations 4 \
|
| 98 |
+
--batch-size 1 \
|
| 99 |
+
--grad-accum 4
|
| 100 |
+
|
| 101 |
+
# Quick smoke test first (5 min)
|
| 102 |
+
python scripts/train_grpo.py \
|
| 103 |
+
--samples 20 \
|
| 104 |
+
--max-steps 10 \
|
| 105 |
+
--no-wandb
|
| 106 |
+
```
|
| 107 |
+
|
| 108 |
+
### Step 7: Monitor
|
| 109 |
+
|
| 110 |
+
```bash
|
| 111 |
+
# In another tmux pane:
|
| 112 |
+
watch -n 30 nvidia-smi # GPU memory
|
| 113 |
+
# Wandb dashboard: https://wandb.ai/<your-user>/commitguard
|
| 114 |
+
```
|
| 115 |
+
|
| 116 |
+
### Step 8: Copy results back
|
| 117 |
+
|
| 118 |
+
```bash
|
| 119 |
+
# From your LOCAL machine:
|
| 120 |
+
gcloud compute scp --recurse \
|
| 121 |
+
commitguard-train:~/commitguard/outputs/commitguard-llama-3b/final \
|
| 122 |
+
./outputs/commitguard-llama-3b/final \
|
| 123 |
+
--zone=us-central1-a
|
| 124 |
+
```
|
| 125 |
+
|
| 126 |
+
### Step 9: Shut down VM
|
| 127 |
+
|
| 128 |
+
```bash
|
| 129 |
+
gcloud compute instances stop commitguard-train --zone=us-central1-a
|
| 130 |
+
# or delete to stop billing entirely:
|
| 131 |
+
gcloud compute instances delete commitguard-train --zone=us-central1-a
|
| 132 |
+
```
|
| 133 |
+
|
| 134 |
+
### Cost estimate
|
| 135 |
+
|
| 136 |
+
| GPU | VRAM | $/hr | 300 steps (~3hr) |
|
| 137 |
+
|-----|------|------|-------------------|
|
| 138 |
+
| T4 | 16GB | $0.35 | ~$1.05 |
|
| 139 |
+
| L4 | 24GB | $0.70 | ~$2.10 |
|
| 140 |
+
| A100| 40GB | $2.50 | ~$7.50 |
|
| 141 |
+
|
| 142 |
+
### Troubleshooting
|
| 143 |
+
|
| 144 |
+
- **OOM on T4**: reduce `--num-generations 2` and `--batch-size 1`
|
| 145 |
+
- **Llama access denied**: make sure you accepted the license at https://huggingface.co/meta-llama/Llama-3.2-3B-Instruct
|
| 146 |
+
- **Env server not responding**: check `tmux attach -t server` for errors
|
| 147 |
+
- **Wandb not logging**: verify `wandb login` succeeded, or use `--no-wandb`
|
| 148 |
+
- **GPU quota error**: request GPU quota increase at https://console.cloud.google.com/iam-admin/quotas
|
| 149 |
+
|
scripts/gcp_setup.sh
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# =============================================================================
|
| 3 |
+
# CommitGuard — GCP VM Setup Script
|
| 4 |
+
# Target: GCE VM with NVIDIA L4 (24 GB) or A100 (40/80 GB)
|
| 5 |
+
# =============================================================================
|
| 6 |
+
set -euo pipefail
|
| 7 |
+
|
| 8 |
+
echo "============================================"
|
| 9 |
+
echo " CommitGuard GCP Training VM Setup"
|
| 10 |
+
echo "============================================"
|
| 11 |
+
|
| 12 |
+
# --- 1. System packages ---
|
| 13 |
+
sudo apt-get update -qq
|
| 14 |
+
sudo apt-get install -y -qq git python3-venv python3-pip tmux htop
|
| 15 |
+
|
| 16 |
+
# --- 2. NVIDIA driver check ---
|
| 17 |
+
if ! command -v nvidia-smi &>/dev/null; then
|
| 18 |
+
echo "ERROR: nvidia-smi not found. Use a GCP image with pre-installed GPU drivers:"
|
| 19 |
+
echo " - Deep Learning VM (recommended)"
|
| 20 |
+
echo " - Or install manually: sudo apt install nvidia-driver-535"
|
| 21 |
+
exit 1
|
| 22 |
+
fi
|
| 23 |
+
echo "GPU detected:"
|
| 24 |
+
nvidia-smi --query-gpu=name,memory.total --format=csv,noheader
|
| 25 |
+
|
| 26 |
+
# --- 3. Clone repo ---
|
| 27 |
+
REPO_DIR="$HOME/commitguard"
|
| 28 |
+
if [ ! -d "$REPO_DIR" ]; then
|
| 29 |
+
echo "Cloning repo..."
|
| 30 |
+
git clone https://github.com/NitishKumar-ai/commitguard.git "$REPO_DIR"
|
| 31 |
+
else
|
| 32 |
+
echo "Repo exists, pulling latest..."
|
| 33 |
+
cd "$REPO_DIR" && git pull
|
| 34 |
+
fi
|
| 35 |
+
cd "$REPO_DIR"
|
| 36 |
+
|
| 37 |
+
# --- 4. Python venv ---
|
| 38 |
+
if [ ! -d ".venv" ]; then
|
| 39 |
+
python3 -m venv .venv
|
| 40 |
+
fi
|
| 41 |
+
source .venv/bin/activate
|
| 42 |
+
pip install -U pip setuptools wheel -q
|
| 43 |
+
|
| 44 |
+
# --- 5. Install training dependencies ---
|
| 45 |
+
echo "Installing training dependencies..."
|
| 46 |
+
pip install -e . -q
|
| 47 |
+
|
| 48 |
+
pip install \
|
| 49 |
+
"torch>=2.4" \
|
| 50 |
+
"unsloth[cu124-torch240]" \
|
| 51 |
+
"trl>=0.12" \
|
| 52 |
+
"peft>=0.13" \
|
| 53 |
+
"bitsandbytes>=0.44" \
|
| 54 |
+
"transformers>=4.46" \
|
| 55 |
+
"datasets>=3.0" \
|
| 56 |
+
"accelerate>=1.0" \
|
| 57 |
+
"wandb" \
|
| 58 |
+
"requests" \
|
| 59 |
+
"matplotlib" \
|
| 60 |
+
"jupyter" \
|
| 61 |
+
"ipywidgets" \
|
| 62 |
+
-q
|
| 63 |
+
|
| 64 |
+
echo "Verifying installs..."
|
| 65 |
+
python -c "
|
| 66 |
+
import torch, trl, unsloth, peft, wandb, bitsandbytes
|
| 67 |
+
print(f'PyTorch: {torch.__version__}')
|
| 68 |
+
print(f'CUDA: {torch.cuda.is_available()} — {torch.cuda.get_device_name(0) if torch.cuda.is_available() else \"N/A\"}')
|
| 69 |
+
print(f'TRL: {trl.__version__}')
|
| 70 |
+
print(f'PEFT: {peft.__version__}')
|
| 71 |
+
print(f'Wandb: {wandb.__version__}')
|
| 72 |
+
print('All training deps OK.')
|
| 73 |
+
"
|
| 74 |
+
|
| 75 |
+
echo ""
|
| 76 |
+
echo "============================================"
|
| 77 |
+
echo " Setup complete. Two options to train:"
|
| 78 |
+
echo "============================================"
|
| 79 |
+
echo ""
|
| 80 |
+
echo " ── OPTION A: Jupyter Notebook (recommended) ──"
|
| 81 |
+
echo ""
|
| 82 |
+
echo " # On the VM:"
|
| 83 |
+
echo " cd $REPO_DIR && source .venv/bin/activate"
|
| 84 |
+
echo " tmux new -s server -d 'source .venv/bin/activate && server'"
|
| 85 |
+
echo " jupyter notebook --no-browser --port=8888 --ip=0.0.0.0"
|
| 86 |
+
echo ""
|
| 87 |
+
echo " # On your LOCAL machine (new terminal):"
|
| 88 |
+
echo " gcloud compute ssh commitguard-train --zone=us-central1-a -- -NL 8888:localhost:8888"
|
| 89 |
+
echo ""
|
| 90 |
+
echo " # Then open in browser:"
|
| 91 |
+
echo " # http://localhost:8888 → notebooks/train_commitguard.ipynb"
|
| 92 |
+
echo ""
|
| 93 |
+
echo " ── OPTION B: CLI ──"
|
| 94 |
+
echo ""
|
| 95 |
+
echo " cd $REPO_DIR && source .venv/bin/activate"
|
| 96 |
+
echo " tmux new -s server -d 'source .venv/bin/activate && server'"
|
| 97 |
+
echo " huggingface-cli login"
|
| 98 |
+
echo " python scripts/train_grpo.py --samples 200 --max-steps 300"
|
| 99 |
+
echo ""
|
scripts/plot_results.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import matplotlib.pyplot as plt
|
| 2 |
+
import json
|
| 3 |
+
import os
|
| 4 |
+
import argparse
|
| 5 |
+
|
| 6 |
+
def plot_reward_curve(wandb_data_path, output_path="plots/reward_curve.png"):
|
| 7 |
+
"""
|
| 8 |
+
Plots the training reward curve.
|
| 9 |
+
Expects a JSON file with 'step' and 'reward' keys (exported from Wandb).
|
| 10 |
+
"""
|
| 11 |
+
if not os.path.exists(wandb_data_path):
|
| 12 |
+
print(f"Skipping: {wandb_data_path} not found.")
|
| 13 |
+
return
|
| 14 |
+
|
| 15 |
+
with open(wandb_data_path, "r") as f:
|
| 16 |
+
data = json.load(f)
|
| 17 |
+
|
| 18 |
+
steps = [d["step"] for d in data]
|
| 19 |
+
rewards = [d["reward"] for d in data]
|
| 20 |
+
|
| 21 |
+
plt.figure(figsize=(10, 6))
|
| 22 |
+
plt.plot(steps, rewards, label="GRPO Reward", color="#2ecc71", linewidth=2)
|
| 23 |
+
plt.xlabel("Training Step")
|
| 24 |
+
plt.ylabel("Mean Reward")
|
| 25 |
+
plt.title("CommitGuard — GRPO Training Reward Curve")
|
| 26 |
+
plt.grid(True, linestyle="--", alpha=0.7)
|
| 27 |
+
plt.legend()
|
| 28 |
+
plt.savefig(output_path)
|
| 29 |
+
print(f"Saved: {output_path}")
|
| 30 |
+
|
| 31 |
+
def plot_accuracy_comparison(baseline_acc, trained_acc, output_path="plots/baseline_vs_trained.png"):
|
| 32 |
+
"""
|
| 33 |
+
Plots a bar chart comparing baseline vs trained accuracy.
|
| 34 |
+
"""
|
| 35 |
+
labels = ['Baseline (Untrained)', 'CommitGuard (Trained)']
|
| 36 |
+
accuracies = [baseline_acc, trained_acc]
|
| 37 |
+
colors = ['#95a5a6', '#3498db']
|
| 38 |
+
|
| 39 |
+
plt.figure(figsize=(8, 6))
|
| 40 |
+
bars = plt.bar(labels, accuracies, color=colors)
|
| 41 |
+
plt.ylabel("Detection Accuracy (%)")
|
| 42 |
+
plt.title("Vulnerability Detection: Baseline vs. Trained")
|
| 43 |
+
plt.ylim(0, 100)
|
| 44 |
+
|
| 45 |
+
for bar in bars:
|
| 46 |
+
height = bar.get_height()
|
| 47 |
+
plt.text(bar.get_x() + bar.get_width()/2., height + 1,
|
| 48 |
+
f'{height}%', ha='center', va='bottom', fontweight='bold')
|
| 49 |
+
|
| 50 |
+
plt.savefig(output_path)
|
| 51 |
+
print(f"Saved: {output_path}")
|
| 52 |
+
|
| 53 |
+
def plot_per_cwe_breakdown(cwe_data, output_path="plots/per_cwe.png"):
|
| 54 |
+
"""
|
| 55 |
+
Plots a grouped bar chart for per-CWE improvement.
|
| 56 |
+
cwe_data format: {"CWE-89": [baseline, trained], "CWE-119": [baseline, trained], ...}
|
| 57 |
+
"""
|
| 58 |
+
cwes = list(cwe_data.keys())
|
| 59 |
+
baseline_vals = [v[0] for v in cwe_data.values()]
|
| 60 |
+
trained_vals = [v[1] for v in cwe_data.values()]
|
| 61 |
+
|
| 62 |
+
x = range(len(cwes))
|
| 63 |
+
width = 0.35
|
| 64 |
+
|
| 65 |
+
fig, ax = plt.subplots(figsize=(12, 6))
|
| 66 |
+
ax.bar([i - width/2 for i in x], baseline_vals, width, label='Baseline', color='#95a5a6')
|
| 67 |
+
ax.bar([i + width/2 for i in x], trained_vals, width, label='Trained', color='#e67e22')
|
| 68 |
+
|
| 69 |
+
ax.set_ylabel('Accuracy (%)')
|
| 70 |
+
ax.set_title('Detection Accuracy by CWE Type')
|
| 71 |
+
ax.set_xticks(x)
|
| 72 |
+
ax.set_xticklabels(cwes, rotation=45)
|
| 73 |
+
ax.legend()
|
| 74 |
+
ax.set_ylim(0, 100)
|
| 75 |
+
|
| 76 |
+
plt.tight_layout()
|
| 77 |
+
plt.savefig(output_path)
|
| 78 |
+
print(f"Saved: {output_path}")
|
| 79 |
+
|
| 80 |
+
if __name__ == "__main__":
|
| 81 |
+
parser = argparse.ArgumentParser()
|
| 82 |
+
parser.add_argument("--mode", choices=["reward", "accuracy", "cwe", "all"], default="all")
|
| 83 |
+
args = parser.parse_args()
|
| 84 |
+
|
| 85 |
+
os.makedirs("plots", exist_ok=True)
|
| 86 |
+
|
| 87 |
+
# Example usage for morning shift:
|
| 88 |
+
if args.mode in ["reward", "all"]:
|
| 89 |
+
plot_reward_curve("plots/wandb_simulated.json")
|
| 90 |
+
|
| 91 |
+
if args.mode in ["accuracy", "all"]:
|
| 92 |
+
# Placeholder numbers (to be updated by Divyank/Deepak's eval)
|
| 93 |
+
plot_accuracy_comparison(baseline_acc=32, trained_acc=68)
|
| 94 |
+
|
| 95 |
+
if args.mode in ["cwe", "all"]:
|
| 96 |
+
# Placeholder data
|
| 97 |
+
cwe_data = {
|
| 98 |
+
"CWE-89": [40, 85],
|
| 99 |
+
"CWE-119": [30, 60],
|
| 100 |
+
"CWE-79": [25, 70],
|
| 101 |
+
"CWE-20": [35, 55]
|
| 102 |
+
}
|
| 103 |
+
plot_per_cwe_breakdown(cwe_data)
|
scripts/preprocess_devign.py
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
import json
|
| 3 |
+
import random
|
| 4 |
+
from collections import Counter
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def _read_jsonl(path: Path) -> list[dict]:
|
| 9 |
+
rows = []
|
| 10 |
+
for line in path.read_text(encoding="utf-8").splitlines():
|
| 11 |
+
line = line.strip()
|
| 12 |
+
if not line:
|
| 13 |
+
continue
|
| 14 |
+
rows.append(json.loads(line))
|
| 15 |
+
return rows
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def _write_jsonl(path: Path, rows: list[dict]) -> None:
|
| 19 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 20 |
+
with path.open("w", encoding="utf-8", newline="\n") as f:
|
| 21 |
+
for r in rows:
|
| 22 |
+
f.write(json.dumps(r, ensure_ascii=False) + "\n")
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
# ---------------------------------------------------------------------------
|
| 26 |
+
# Fix 2: CWE classification using vulnerable lines, not the whole function.
|
| 27 |
+
# Scored rules — highest-scoring match wins. Falls back to CWE-OTHER.
|
| 28 |
+
# ---------------------------------------------------------------------------
|
| 29 |
+
|
| 30 |
+
_CWE_RULES: list[tuple[str, list[str], int]] = [
|
| 31 |
+
("CWE-119", ["memcpy", "strcpy", "strcat", "strncpy", "memmove", "sprintf",
|
| 32 |
+
"gets(", "buffer", "overflow", "oob", "av_malloc", "av_realloc",
|
| 33 |
+
"realloc", "malloc", "alloc", "g_malloc", "g_realloc",
|
| 34 |
+
"qemu_malloc", "len ", "length", "copy_from", "copy_to"], 5),
|
| 35 |
+
("CWE-476", ["null", "nullptr", "!= null", "== null", "if (!",
|
| 36 |
+
"dereference", "segfault", "!obj", "!ctx", "!s->", "!p"], 5),
|
| 37 |
+
("CWE-189", ["integer overflow", "signedness", "truncat", "wrap",
|
| 38 |
+
"size_t", "underflow", "narrowing", "(int)", "(uint",
|
| 39 |
+
"(unsigned)", ">> ", "<< ", "0xffff", "max_", "min_"], 5),
|
| 40 |
+
("CWE-78", ["system(", "popen(", "exec(", "execve", "shell",
|
| 41 |
+
"command", "subprocess"], 8),
|
| 42 |
+
("CWE-22", ["../", "..\\", "traversal", "chroot", "realpath",
|
| 43 |
+
"canonicalize", "symlink", "path"], 7),
|
| 44 |
+
("CWE-89", ["sql", "query", "select ", "insert ", "union ",
|
| 45 |
+
"prepared", "sqlite", "mysql"], 7),
|
| 46 |
+
("CWE-79", ["xss", "innerhtml", "script", "sanitize", "escape",
|
| 47 |
+
"htmlentit", "content-type"], 6),
|
| 48 |
+
("CWE-20", ["valid", "saniti", "untrusted", "input", "bounds",
|
| 49 |
+
"assert", "range", "check", "error", "return -1",
|
| 50 |
+
"goto fail", "goto err", "goto out"], 2),
|
| 51 |
+
]
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def infer_cwe(vul_lines_code: list[str], func: str) -> str:
|
| 55 |
+
vul_text = " ".join(vul_lines_code).lower() if vul_lines_code else ""
|
| 56 |
+
func_text = func.lower()
|
| 57 |
+
|
| 58 |
+
best_cwe, best_score = "CWE-OTHER", 0
|
| 59 |
+
|
| 60 |
+
for cwe, keywords, weight in _CWE_RULES:
|
| 61 |
+
vul_hits = sum(1 for k in keywords if k in vul_text) if vul_text else 0
|
| 62 |
+
func_hits = sum(1 for k in keywords if k in func_text)
|
| 63 |
+
score = vul_hits * weight + func_hits * (weight // 2)
|
| 64 |
+
if score > best_score:
|
| 65 |
+
best_cwe, best_score = cwe, score
|
| 66 |
+
|
| 67 |
+
if best_score < 2:
|
| 68 |
+
return "CWE-OTHER"
|
| 69 |
+
return best_cwe
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
# ---------------------------------------------------------------------------
|
| 73 |
+
# Fix 1: Real unified diffs from per-line vulnerability labels.
|
| 74 |
+
# ---------------------------------------------------------------------------
|
| 75 |
+
|
| 76 |
+
def _build_diff(func: str, label: list[int], rng: random.Random, is_vuln: bool) -> str:
|
| 77 |
+
lines = func.splitlines()
|
| 78 |
+
|
| 79 |
+
if is_vuln and label and len(label) == len(lines):
|
| 80 |
+
changed_indices = {i for i, l in enumerate(label) if l == 1}
|
| 81 |
+
elif is_vuln and label and any(l == 1 for l in label):
|
| 82 |
+
changed_indices = {i for i, l in enumerate(label) if l == 1}
|
| 83 |
+
else:
|
| 84 |
+
block_size = max(1, min(5, len(lines) // 4))
|
| 85 |
+
start = rng.randint(0, max(0, len(lines) - block_size))
|
| 86 |
+
changed_indices = set(range(start, min(start + block_size, len(lines))))
|
| 87 |
+
|
| 88 |
+
if not changed_indices:
|
| 89 |
+
changed_indices = {0}
|
| 90 |
+
|
| 91 |
+
ctx = 3
|
| 92 |
+
visible: set[int] = set()
|
| 93 |
+
for ci in changed_indices:
|
| 94 |
+
for offset in range(-ctx, ctx + 1):
|
| 95 |
+
idx = ci + offset
|
| 96 |
+
if 0 <= idx < len(lines):
|
| 97 |
+
visible.add(idx)
|
| 98 |
+
|
| 99 |
+
sorted_visible = sorted(visible)
|
| 100 |
+
hunks: list[list[int]] = []
|
| 101 |
+
current_hunk: list[int] = []
|
| 102 |
+
for idx in sorted_visible:
|
| 103 |
+
if current_hunk and idx > current_hunk[-1] + 1:
|
| 104 |
+
hunks.append(current_hunk)
|
| 105 |
+
current_hunk = [idx]
|
| 106 |
+
else:
|
| 107 |
+
current_hunk.append(idx)
|
| 108 |
+
if current_hunk:
|
| 109 |
+
hunks.append(current_hunk)
|
| 110 |
+
|
| 111 |
+
diff_parts = ["--- a/source.c", "+++ b/source.c"]
|
| 112 |
+
for hunk in hunks:
|
| 113 |
+
start_line = hunk[0] + 1
|
| 114 |
+
hunk_size = len(hunk)
|
| 115 |
+
diff_parts.append(f"@@ -{start_line},{hunk_size} +{start_line},{hunk_size} @@")
|
| 116 |
+
for idx in hunk:
|
| 117 |
+
line = lines[idx]
|
| 118 |
+
if idx in changed_indices:
|
| 119 |
+
diff_parts.append(f"+{line}")
|
| 120 |
+
else:
|
| 121 |
+
diff_parts.append(f" {line}")
|
| 122 |
+
|
| 123 |
+
return "\n".join(diff_parts)
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
# ---------------------------------------------------------------------------
|
| 127 |
+
# Fix 3: CWE rebalancing — cap dominant CWEs, merge tiny ones.
|
| 128 |
+
# ---------------------------------------------------------------------------
|
| 129 |
+
|
| 130 |
+
_MAX_PER_CWE_FRAC = 0.25
|
| 131 |
+
_MIN_CWE_SAMPLES = 20
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def _rebalance(samples: list[dict], rng: random.Random, limit: int) -> list[dict]:
|
| 135 |
+
by_cwe: dict[str, list[dict]] = {}
|
| 136 |
+
for s in samples:
|
| 137 |
+
by_cwe.setdefault(s["cwe"] or "CWE-OTHER", []).append(s)
|
| 138 |
+
|
| 139 |
+
for cwe, items in list(by_cwe.items()):
|
| 140 |
+
if len(items) < _MIN_CWE_SAMPLES and cwe != "CWE-OTHER":
|
| 141 |
+
by_cwe.setdefault("CWE-OTHER", []).extend(items)
|
| 142 |
+
for item in items:
|
| 143 |
+
item["cwe"] = "CWE-OTHER"
|
| 144 |
+
del by_cwe[cwe]
|
| 145 |
+
|
| 146 |
+
cap = int(limit * _MAX_PER_CWE_FRAC)
|
| 147 |
+
kept: list[dict] = []
|
| 148 |
+
for cwe, items in by_cwe.items():
|
| 149 |
+
rng.shuffle(items)
|
| 150 |
+
kept.extend(items[:cap])
|
| 151 |
+
|
| 152 |
+
rng.shuffle(kept)
|
| 153 |
+
return kept[:limit]
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def main() -> None:
|
| 157 |
+
ap = argparse.ArgumentParser(description="Preprocess Devign-derived samples into CommitGuard JSONL.")
|
| 158 |
+
ap.add_argument("--in", dest="inp", type=Path, default=None, help="Optional input JSONL.")
|
| 159 |
+
ap.add_argument("--out", dest="out", type=Path, default=Path("data/devign_filtered.jsonl"))
|
| 160 |
+
ap.add_argument("--limit", type=int, default=5000)
|
| 161 |
+
ap.add_argument("--seed", type=int, default=42)
|
| 162 |
+
args = ap.parse_args()
|
| 163 |
+
|
| 164 |
+
rng = random.Random(args.seed)
|
| 165 |
+
|
| 166 |
+
if args.inp is None:
|
| 167 |
+
try:
|
| 168 |
+
from datasets import load_dataset
|
| 169 |
+
print("Loading DetectVul/devign from Hugging Face...")
|
| 170 |
+
ds = load_dataset('DetectVul/devign', split='train')
|
| 171 |
+
raw_rows = list(ds)
|
| 172 |
+
print(f"Loaded {len(raw_rows)} rows from HF.")
|
| 173 |
+
except Exception as e:
|
| 174 |
+
print(f"Failed to load from HF: {e}")
|
| 175 |
+
return
|
| 176 |
+
else:
|
| 177 |
+
raw_rows = _read_jsonl(args.inp)
|
| 178 |
+
|
| 179 |
+
vuln_samples: list[dict] = []
|
| 180 |
+
safe_samples: list[dict] = []
|
| 181 |
+
cwe_counter: Counter[str] = Counter()
|
| 182 |
+
|
| 183 |
+
for r in raw_rows:
|
| 184 |
+
func = r.get("func")
|
| 185 |
+
if not func:
|
| 186 |
+
continue
|
| 187 |
+
if len(func.split("\n")) > 80:
|
| 188 |
+
continue
|
| 189 |
+
|
| 190 |
+
target = bool(r.get("target", False))
|
| 191 |
+
label = r.get("label", [])
|
| 192 |
+
vul_lines_code = []
|
| 193 |
+
vl = r.get("vul_lines")
|
| 194 |
+
if vl and isinstance(vl, dict):
|
| 195 |
+
vul_lines_code = vl.get("code", [])
|
| 196 |
+
|
| 197 |
+
cwe = infer_cwe(vul_lines_code, func) if target else None
|
| 198 |
+
diff = _build_diff(func, label, rng, target)
|
| 199 |
+
|
| 200 |
+
sample_id = str(r.get("commit_id") or r.get("id") or f"row-{len(vuln_samples) + len(safe_samples)}")
|
| 201 |
+
target_file = "source.c"
|
| 202 |
+
|
| 203 |
+
sample = {
|
| 204 |
+
"sample_id": sample_id,
|
| 205 |
+
"diff": diff,
|
| 206 |
+
"available_files": [target_file],
|
| 207 |
+
"is_vulnerable": target,
|
| 208 |
+
"cwe": cwe,
|
| 209 |
+
"target_file": target_file,
|
| 210 |
+
"files": {target_file: func},
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
if target:
|
| 214 |
+
vuln_samples.append(sample)
|
| 215 |
+
cwe_counter[cwe or "CWE-OTHER"] += 1
|
| 216 |
+
else:
|
| 217 |
+
safe_samples.append(sample)
|
| 218 |
+
|
| 219 |
+
print(f"Filtered: {len(vuln_samples)} vulnerable, {len(safe_samples)} safe.")
|
| 220 |
+
print(f"CWE distribution (pre-balance): {cwe_counter.most_common()}")
|
| 221 |
+
|
| 222 |
+
target_each = args.limit // 2
|
| 223 |
+
vuln_keep = _rebalance(vuln_samples, rng, target_each)
|
| 224 |
+
safe_keep = rng.sample(safe_samples, min(target_each, len(safe_samples)))
|
| 225 |
+
|
| 226 |
+
out_rows = vuln_keep + safe_keep
|
| 227 |
+
rng.shuffle(out_rows)
|
| 228 |
+
|
| 229 |
+
_write_jsonl(args.out, out_rows)
|
| 230 |
+
|
| 231 |
+
final_cwes = Counter(r["cwe"] for r in out_rows if r["is_vulnerable"])
|
| 232 |
+
print(f"Wrote {len(out_rows)} samples to {args.out}")
|
| 233 |
+
print(f"Final CWE distribution: {final_cwes.most_common()}")
|
| 234 |
+
|
| 235 |
+
if __name__ == "__main__":
|
| 236 |
+
main()
|
scripts/run_and_plot_baseline.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import json
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
import sys
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def main() -> None:
|
| 10 |
+
ap = argparse.ArgumentParser(description="Run a tiny baseline and save a reward-curve PNG.")
|
| 11 |
+
ap.add_argument("--episodes", type=int, default=200)
|
| 12 |
+
ap.add_argument("--out-dir", type=Path, default=Path("plots"))
|
| 13 |
+
args = ap.parse_args()
|
| 14 |
+
|
| 15 |
+
# Allow running from a fresh clone without `pip install -e .`.
|
| 16 |
+
repo_root = Path(__file__).resolve().parent.parent
|
| 17 |
+
sys.path.insert(0, str(repo_root))
|
| 18 |
+
|
| 19 |
+
# Local, in-process baseline (no server needed).
|
| 20 |
+
from commitguard_env.environment import CommitGuardEnvironment
|
| 21 |
+
from commitguard_env.models import CommitGuardAction
|
| 22 |
+
|
| 23 |
+
data_path = repo_root / "data" / "devign_filtered.jsonl"
|
| 24 |
+
env = CommitGuardEnvironment(data_path=data_path)
|
| 25 |
+
|
| 26 |
+
rewards: list[float] = []
|
| 27 |
+
for _ in range(args.episodes):
|
| 28 |
+
_ = env.reset()
|
| 29 |
+
# Naive always-vulnerable verdict baseline (intentionally dumb).
|
| 30 |
+
action = CommitGuardAction(
|
| 31 |
+
action_type="verdict",
|
| 32 |
+
is_vulnerable=True,
|
| 33 |
+
vuln_type="CWE-89",
|
| 34 |
+
exploit_sketch="sql select where concat injection",
|
| 35 |
+
)
|
| 36 |
+
_obs, reward, _done = env.step(action)
|
| 37 |
+
rewards.append(float(reward))
|
| 38 |
+
|
| 39 |
+
args.out_dir.mkdir(parents=True, exist_ok=True)
|
| 40 |
+
(args.out_dir / "baseline_rewards.json").write_text(json.dumps(rewards), encoding="utf-8")
|
| 41 |
+
|
| 42 |
+
import matplotlib.pyplot as plt
|
| 43 |
+
|
| 44 |
+
plt.figure(figsize=(8, 4))
|
| 45 |
+
plt.plot(rewards, linewidth=1)
|
| 46 |
+
plt.title("CommitGuard baseline reward curve (naive always-vulnerable)")
|
| 47 |
+
plt.xlabel("Episode")
|
| 48 |
+
plt.ylabel("Reward")
|
| 49 |
+
plt.tight_layout()
|
| 50 |
+
plt.savefig(args.out_dir / "baseline_reward_curve.png", dpi=180)
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
if __name__ == "__main__":
|
| 54 |
+
main()
|
| 55 |
+
|
scripts/train_grpo.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
import sys
|
| 5 |
+
import json
|
| 6 |
+
import argparse
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
import requests
|
| 10 |
+
import torch
|
| 11 |
+
from datasets import Dataset
|
| 12 |
+
from trl import GRPOConfig, GRPOTrainer
|
| 13 |
+
from unsloth import FastLanguageModel, PatchFastRL
|
| 14 |
+
|
| 15 |
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
| 16 |
+
from agent_prompt import SYSTEM_PROMPT, get_agent_prompt
|
| 17 |
+
|
| 18 |
+
PatchFastRL("GRPO", FastLanguageModel)
|
| 19 |
+
|
| 20 |
+
# --- Configuration ---
|
| 21 |
+
MODEL_NAME = os.getenv("MODEL_NAME", "meta-llama/Llama-3.2-3B-Instruct")
|
| 22 |
+
ENV_URL = os.getenv("ENV_URL", "http://localhost:8000")
|
| 23 |
+
OUTPUT_DIR = os.getenv("OUTPUT_DIR", "outputs/commitguard-llama-3b")
|
| 24 |
+
WANDB_PROJECT = os.getenv("WANDB_PROJECT", "commitguard")
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
# --- Reward: one reset + verdict per completion ---
|
| 28 |
+
def get_reward_from_env(prompts, completions, **kwargs) -> list[float]:
|
| 29 |
+
rewards = []
|
| 30 |
+
for prompt, completion in zip(prompts, completions):
|
| 31 |
+
try:
|
| 32 |
+
# Reset to get a fresh episode
|
| 33 |
+
r = requests.post(f"{ENV_URL}/reset", json={}, timeout=10)
|
| 34 |
+
if r.status_code != 200:
|
| 35 |
+
rewards.append(-0.5)
|
| 36 |
+
continue
|
| 37 |
+
# Send the model's completion as the action
|
| 38 |
+
text = completion[-1]["content"] if isinstance(completion, list) else str(completion)
|
| 39 |
+
r = requests.post(f"{ENV_URL}/step", json={"action": text}, timeout=10)
|
| 40 |
+
if r.status_code == 200:
|
| 41 |
+
rewards.append(float(r.json().get("reward", 0.0)))
|
| 42 |
+
else:
|
| 43 |
+
rewards.append(-0.5)
|
| 44 |
+
except Exception:
|
| 45 |
+
rewards.append(-1.0)
|
| 46 |
+
return rewards
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def build_dataset(n_samples: int) -> Dataset:
|
| 50 |
+
print(f"Fetching {n_samples} training prompts from {ENV_URL}...")
|
| 51 |
+
samples = []
|
| 52 |
+
for i in range(n_samples):
|
| 53 |
+
try:
|
| 54 |
+
r = requests.post(f"{ENV_URL}/reset", json={}, timeout=10)
|
| 55 |
+
if r.status_code != 200:
|
| 56 |
+
continue
|
| 57 |
+
obs = r.json()["observation"]
|
| 58 |
+
user_msg = get_agent_prompt(
|
| 59 |
+
obs["diff"], obs["available_files"], obs.get("step_idx", 0)
|
| 60 |
+
)
|
| 61 |
+
samples.append({
|
| 62 |
+
"prompt": [
|
| 63 |
+
{"role": "system", "content": SYSTEM_PROMPT},
|
| 64 |
+
{"role": "user", "content": user_msg},
|
| 65 |
+
],
|
| 66 |
+
})
|
| 67 |
+
except Exception:
|
| 68 |
+
continue
|
| 69 |
+
if (i + 1) % 50 == 0:
|
| 70 |
+
print(f" fetched {i + 1}/{n_samples}")
|
| 71 |
+
print(f"Built dataset with {len(samples)} samples.")
|
| 72 |
+
return Dataset.from_list(samples)
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def main():
|
| 76 |
+
ap = argparse.ArgumentParser()
|
| 77 |
+
ap.add_argument("--samples", type=int, default=200)
|
| 78 |
+
ap.add_argument("--max-steps", type=int, default=300)
|
| 79 |
+
ap.add_argument("--save-steps", type=int, default=50)
|
| 80 |
+
ap.add_argument("--num-generations", type=int, default=4)
|
| 81 |
+
ap.add_argument("--batch-size", type=int, default=1)
|
| 82 |
+
ap.add_argument("--grad-accum", type=int, default=4)
|
| 83 |
+
ap.add_argument("--lr", type=float, default=5e-6)
|
| 84 |
+
ap.add_argument("--no-wandb", action="store_true")
|
| 85 |
+
args = ap.parse_args()
|
| 86 |
+
|
| 87 |
+
# 1. Load Model
|
| 88 |
+
print(f"Loading {MODEL_NAME} with Unsloth 4-bit...")
|
| 89 |
+
model, tokenizer = FastLanguageModel.from_pretrained(
|
| 90 |
+
model_name=MODEL_NAME,
|
| 91 |
+
max_seq_length=2048,
|
| 92 |
+
load_in_4bit=True,
|
| 93 |
+
fast_inference=True,
|
| 94 |
+
max_lora_rank=16,
|
| 95 |
+
)
|
| 96 |
+
|
| 97 |
+
model = FastLanguageModel.get_peft_model(
|
| 98 |
+
model,
|
| 99 |
+
r=8,
|
| 100 |
+
target_modules=[
|
| 101 |
+
"q_proj", "k_proj", "v_proj", "o_proj",
|
| 102 |
+
"gate_proj", "up_proj", "down_proj",
|
| 103 |
+
],
|
| 104 |
+
lora_alpha=16,
|
| 105 |
+
lora_dropout=0,
|
| 106 |
+
bias="none",
|
| 107 |
+
use_gradient_checkpointing="unsloth",
|
| 108 |
+
random_state=3407,
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
# 2. Build dataset from live env
|
| 112 |
+
dataset = build_dataset(args.samples)
|
| 113 |
+
|
| 114 |
+
# 3. GRPO config
|
| 115 |
+
training_args = GRPOConfig(
|
| 116 |
+
output_dir=OUTPUT_DIR,
|
| 117 |
+
num_generations=args.num_generations,
|
| 118 |
+
max_completion_length=512,
|
| 119 |
+
per_device_train_batch_size=args.batch_size,
|
| 120 |
+
gradient_accumulation_steps=args.grad_accum,
|
| 121 |
+
learning_rate=args.lr,
|
| 122 |
+
logging_steps=1,
|
| 123 |
+
save_steps=args.save_steps,
|
| 124 |
+
max_steps=args.max_steps,
|
| 125 |
+
report_to="none" if args.no_wandb else "wandb",
|
| 126 |
+
bf16=torch.cuda.is_bf16_supported(),
|
| 127 |
+
fp16=not torch.cuda.is_bf16_supported(),
|
| 128 |
+
)
|
| 129 |
+
|
| 130 |
+
# 4. Train
|
| 131 |
+
trainer = GRPOTrainer(
|
| 132 |
+
model=model,
|
| 133 |
+
processing_class=tokenizer,
|
| 134 |
+
reward_funcs=[get_reward_from_env],
|
| 135 |
+
args=training_args,
|
| 136 |
+
train_dataset=dataset,
|
| 137 |
+
)
|
| 138 |
+
|
| 139 |
+
print("Starting GRPO training...")
|
| 140 |
+
trainer.train()
|
| 141 |
+
|
| 142 |
+
# 5. Save
|
| 143 |
+
final_dir = f"{OUTPUT_DIR}/final"
|
| 144 |
+
model.save_pretrained_merged(final_dir, tokenizer, save_method="lora")
|
| 145 |
+
print(f"Training complete. LoRA adapter saved to {final_dir}")
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
if __name__ == "__main__":
|
| 149 |
+
main()
|
scripts/verify_3_action_loop.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import requests
|
| 2 |
+
import json
|
| 3 |
+
import sys
|
| 4 |
+
|
| 5 |
+
def test_loop():
|
| 6 |
+
base_url = "http://localhost:8000"
|
| 7 |
+
|
| 8 |
+
print("--- Phase 1: Reset ---")
|
| 9 |
+
r = requests.post(f"{base_url}/reset")
|
| 10 |
+
if r.status_code != 200:
|
| 11 |
+
print(f"FAILED: Reset returned {r.status_code}")
|
| 12 |
+
return
|
| 13 |
+
data = r.json()
|
| 14 |
+
print(f"Full response keys: {list(data.keys())}")
|
| 15 |
+
obs = data["observation"]
|
| 16 |
+
print(f"Observation value: {obs}")
|
| 17 |
+
episode_id = obs["episode_id"]
|
| 18 |
+
print(f"Observation keys: {list(obs.keys())}")
|
| 19 |
+
print(f"Episode ID: {episode_id}")
|
| 20 |
+
print(f"Diff length: {len(obs['diff'])}")
|
| 21 |
+
|
| 22 |
+
# Verify no leak
|
| 23 |
+
forbidden = ["is_vulnerable", "cwe", "cwe_type", "label"]
|
| 24 |
+
for f in forbidden:
|
| 25 |
+
if f in obs:
|
| 26 |
+
print(f"CRITICAL LEAK: '{f}' found in observation!")
|
| 27 |
+
sys.exit(1)
|
| 28 |
+
|
| 29 |
+
print("\n--- Phase 2: Action 'request_context' ---")
|
| 30 |
+
# Using the first available file if any
|
| 31 |
+
file_to_req = obs["available_files"][0] if obs["available_files"] else "unknown.c"
|
| 32 |
+
action = {
|
| 33 |
+
"action": f"<action><action_type>request_context</action_type><file_path>{file_to_req}</file_path></action>"
|
| 34 |
+
}
|
| 35 |
+
r = requests.post(f"{base_url}/step", json=action)
|
| 36 |
+
res = r.json()
|
| 37 |
+
print(f"Status: {r.status_code}, Reward: {res['reward']}, Done: {res['done']}")
|
| 38 |
+
print(f"Context snippets returned: {len(res['observation'].get('context_snippets', []))}")
|
| 39 |
+
|
| 40 |
+
print("\n--- Phase 3: Action 'analyze' ---")
|
| 41 |
+
action = {
|
| 42 |
+
"action": "<action><action_type>analyze</action_type><reasoning>Thinking about the pointer arithmetic in the diff...</reasoning></action>"
|
| 43 |
+
}
|
| 44 |
+
r = requests.post(f"{base_url}/step", json=action)
|
| 45 |
+
res = r.json()
|
| 46 |
+
print(f"Status: {r.status_code}, Reward: {res['reward']}, Done: {res['done']}")
|
| 47 |
+
|
| 48 |
+
print("\n--- Phase 4: Action 'verdict' ---")
|
| 49 |
+
action = {
|
| 50 |
+
"action": "<action><action_type>verdict</action_type><is_vulnerable>true</is_vulnerable><vuln_type>CWE-119</vuln_type><exploit_sketch>buffer overflow via unchecked memcpy</exploit_sketch></action>"
|
| 51 |
+
}
|
| 52 |
+
r = requests.post(f"{base_url}/step", json=action)
|
| 53 |
+
res = r.json()
|
| 54 |
+
print(f"Status: {r.status_code}, Reward: {res['reward']}, Done: {res['done']}")
|
| 55 |
+
print(f"Final Info: {res.get('info', 'No info')}")
|
| 56 |
+
|
| 57 |
+
print("\n--- Phase 5: Verify State (No Leaks) ---")
|
| 58 |
+
r = requests.get(f"{base_url}/state")
|
| 59 |
+
data = r.json()
|
| 60 |
+
state = data["state"]
|
| 61 |
+
print(f"State Episode ID: {state['episode_id']}")
|
| 62 |
+
print(f"Step Count: {state['step_count']}")
|
| 63 |
+
for f in forbidden:
|
| 64 |
+
if f in state:
|
| 65 |
+
# state() is allowed internal metadata, but the PRD says it shouldn't leak to agent.
|
| 66 |
+
# environment.py says: "state() must not leak labels; returning empty is fine"
|
| 67 |
+
print(f"LEAK WARNING: '{f}' found in state output!")
|
| 68 |
+
|
| 69 |
+
if __name__ == "__main__":
|
| 70 |
+
test_loop()
|
server/__init__.py
ADDED
|
File without changes
|
server/app.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from commitguard_env.server import app, main as server_main
|
| 2 |
+
|
| 3 |
+
def main():
|
| 4 |
+
server_main()
|
| 5 |
+
|
| 6 |
+
if __name__ == "__main__":
|
| 7 |
+
main()
|
smoke_test_episodes.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import random
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
from commitguard_env.environment import CommitGuardEnvironment
|
| 4 |
+
from commitguard_env.models import CommitGuardAction
|
| 5 |
+
|
| 6 |
+
def run_random_episodes(n=100):
|
| 7 |
+
env = CommitGuardEnvironment(data_path=Path("data/devign_filtered.jsonl"))
|
| 8 |
+
|
| 9 |
+
rewards = []
|
| 10 |
+
episode_lengths = []
|
| 11 |
+
|
| 12 |
+
for i in range(n):
|
| 13 |
+
obs = env.reset()
|
| 14 |
+
done = False
|
| 15 |
+
total_reward = 0
|
| 16 |
+
steps = 0
|
| 17 |
+
|
| 18 |
+
while not done:
|
| 19 |
+
# Randomly choose an action
|
| 20 |
+
action_type = random.choice(["request_context", "analyze", "verdict"])
|
| 21 |
+
|
| 22 |
+
if action_type == "request_context":
|
| 23 |
+
action = CommitGuardAction(action_type="request_context", file_path="random_file.c")
|
| 24 |
+
elif action_type == "analyze":
|
| 25 |
+
action = CommitGuardAction(action_type="analyze", reasoning="Thinking...")
|
| 26 |
+
else:
|
| 27 |
+
action = CommitGuardAction(
|
| 28 |
+
action_type="verdict",
|
| 29 |
+
is_vulnerable=random.choice([True, False]),
|
| 30 |
+
vuln_type="CWE-119",
|
| 31 |
+
exploit_sketch="Random exploit attempt"
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
obs, reward, done = env.step(action)
|
| 35 |
+
total_reward += reward
|
| 36 |
+
steps += 1
|
| 37 |
+
|
| 38 |
+
if steps > 10: # Safety break
|
| 39 |
+
break
|
| 40 |
+
|
| 41 |
+
rewards.append(total_reward)
|
| 42 |
+
episode_lengths.append(steps)
|
| 43 |
+
|
| 44 |
+
print(f"Finished {n} episodes.")
|
| 45 |
+
print(f"Average reward: {sum(rewards)/n:.4f}")
|
| 46 |
+
print(f"Max reward: {max(rewards):.4f}")
|
| 47 |
+
print(f"Min reward: {min(rewards):.4f}")
|
| 48 |
+
print(f"Average episode length: {sum(episode_lengths)/n:.2f}")
|
| 49 |
+
print(f"Max episode length: {max(episode_lengths)}")
|
| 50 |
+
|
| 51 |
+
# Check distribution
|
| 52 |
+
unique_rewards = set(rewards)
|
| 53 |
+
print(f"Unique rewards: {len(unique_rewards)}")
|
| 54 |
+
if len(unique_rewards) > 1:
|
| 55 |
+
print("Reward distribution looks healthy (not all zeros).")
|
| 56 |
+
else:
|
| 57 |
+
print("Warning: Only one reward value found.")
|
| 58 |
+
|
| 59 |
+
if __name__ == "__main__":
|
| 60 |
+
run_random_episodes(100)
|
strip_emojis.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import re
|
| 3 |
+
|
| 4 |
+
def strip_emojis(text):
|
| 5 |
+
# This regex is a simple way to catch most common emojis/non-ascii symbols
|
| 6 |
+
return text.encode('ascii', 'ignore').decode('ascii')
|
| 7 |
+
|
| 8 |
+
files_to_clean = [
|
| 9 |
+
"tasks_deepak.md",
|
| 10 |
+
"tasks_divyank.md",
|
| 11 |
+
"tasks_niti.md",
|
| 12 |
+
"README_SUBMISSION.md",
|
| 13 |
+
"README.md",
|
| 14 |
+
"prd.md",
|
| 15 |
+
"AGENT.md",
|
| 16 |
+
"GEMINI.md"
|
| 17 |
+
]
|
| 18 |
+
|
| 19 |
+
for filename in files_to_clean:
|
| 20 |
+
if os.path.exists(filename):
|
| 21 |
+
with open(filename, 'r', encoding='utf-8') as f:
|
| 22 |
+
content = f.read()
|
| 23 |
+
|
| 24 |
+
clean_content = strip_emojis(content)
|
| 25 |
+
|
| 26 |
+
with open(filename, 'w', encoding='utf-8') as f:
|
| 27 |
+
f.write(clean_content)
|
| 28 |
+
print(f"Cleaned {filename}")
|