File size: 18,838 Bytes
eb10fa9 2f08dba feb0fc5 2f08dba feb0fc5 2f08dba feb0fc5 2f08dba feb0fc5 2f08dba feb0fc5 2f08dba feb0fc5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 | ---
license: mit
language:
- en
tags:
- code
- vulnerability
- security
- python
pipeline_tag: text-classification
---
# Model Card β CEVuD Vulnerability Classifier
> HuggingFace-ready model card. This documents the custom Stage-2 classifier
> trained by `src/training/` and is the artifact to publish at
> `huggingface.co/Denash/codebert-vuln-classifier`.
---
## Model Details
### Model Description
- **Model ID**: `Denash/codebert-vuln-classifier`
- **Model type**: Fine-tuned transformer for binary sequence classification
- **Base model**: [`microsoft/codebert-base`](https://huggingface.co/microsoft/codebert-base) (RoBERTa-based, ~125 M parameters)
- **Language**: Python (code)
- **License**: MIT (inherits from CEVuD; CodeBERT itself is MIT)
- **Task**: Binary classification β *vulnerable* vs. *safe* Python function chunks
This model is the **Stage-2 local classifier** ("small model") in the CEVuD
pipeline. It scores uniform code windows (chunks) of Python functions and
outputs a probability `P(vulnerable) β [0, 1]`. The Stage-2 gate combines this
neural probability with Semgrep's static severity via a linear risk equation
`R = WβΒ·S_sev + WβΒ·P_slm` to decide whether to escalate a finding to the
Stage-3 LLM.
The model is trained on the CEVuD Training Dataset (CVEfixes-based) and is
designed to be a **component of a gated pipeline**, not a standalone
vulnerability oracle. Its primary role is to suppress trivially-safe code so
that the expensive LLM is only called when truly needed.
### Model Architecture
The model uses the standard HuggingFace `RobertaForSequenceClassification`
head on top of the CodeBERT encoder:
```
Input: Python code chunk (β€ 512 tokens)
β
CodeBERT Encoder (12 layers, 768 hidden dim, 12 attention heads)
β
[CLS] token hidden state (768-dim)
β
Pooler: dense(768 β 768) + tanh
β
Classifier: dense(768 β 768, tanh) + dropout β out_proj(768 β 2)
β
Softmax β P(vulnerable), P(safe)
```
**Key components**:
- **Encoder**: `microsoft/codebert-base` β a RoBERTa-based transformer
pre-trained on natural language and programming language pairs. Frozen by
default; can be unfrozen for fine-tuning.
- **Pooler**: Maps the `[CLS]` token to a 768-dim representation via a dense
layer + tanh activation.
- **Classifier head**: Two-layer MLP (768 β 768 β 2) with dropout and tanh
activation. Outputs logits for the two classes.
- **Output**: Softmax probabilities. `P(vulnerable) = softmax(logits)[:, 1]`.
When `freeze_backbone=True` is used, only the `classifier.*` submodule is
trained; the encoder and pooler stay frozen. This is the recommended setting for
small datasets (~1.4k samples) because it is more sample-efficient and stable.
---
## Intended Use
### Primary Intended Use
The model is designed to be the **Stage-2 local edge classifier** in the CEVuD
pipeline. Its intended use case is:
1. **CI/CD integration**: Scan code changes in pull requests or pushes.
2. **Local gating**: Score each Semgrep finding locally (zero marginal cost).
3. **Escalation decision**: Combine the neural score with static severity to
decide whether to escalate to the Stage-3 LLM.
### How to Use
```python
from transformers import AutoTokenizer, AutoModelForSequenceClassification
import torch
model_id = "Denash/codebert-vuln-classifier"
tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForSequenceClassification.from_pretrained(model_id)
model.eval()
def score_chunk(code_chunk: str) -> float:
"""Return P(vulnerable) for a single code chunk (β€ 512 tokens)."""
inputs = tokenizer(
code_chunk,
truncation=True,
max_length=512,
padding="max_length",
return_tensors="pt",
)
with torch.no_grad():
logits = model(**inputs).logits
probs = torch.softmax(logits, dim=-1)
return float(probs[0, 1]) # P(vulnerable)
# Example: score a Python function chunk
code = """
def get_user(user_id):
query = "SELECT * FROM users WHERE id = " + str(user_id)
return db.execute(query)
"""
p_vuln = score_chunk(code)
print(f"P(vulnerable) = {p_vuln:.3f}")
```
**Important**: This model scores *chunks* (uniform code windows), not whole
functions. For a complete function, chunk it into 64-line windows with 8-line
overlap, score each chunk, and aggregate using `max` (default) or `mean`.
### Out-of-Scope Uses
- **Standalone vulnerability oracle**: The model is not designed to be used
alone. Its standalone recall is 28.9% (on CVEfixes test) and 70.5% (on
VUDENC). It is meant to be part of a gated pipeline.
- **Other languages**: The model is trained on Python only. Performance on
other languages is unverified.
- **Adversarial settings**: The model has not been evaluated against
adversarially crafted code.
- **Definitive security verdict**: The model's output is one input to a
composite gate. It should not be used as the sole determinant of whether code
is vulnerable.
---
## Training Data
### Dataset Overview
The model is fine-tuned on the **CEVuD Training Dataset** (CVEfixes-based), a
curated corpus of 2,181 Python function chunks derived from real-world
vulnerability fixes.
| Property | Value |
|----------|-------|
| **Source** | `hitoshura25/cvefixes` (HuggingFace) |
| **Total samples** | 2,181 |
| **Projects (repos)** | 554 |
| **Vulnerable** | 474 (21.7%) |
| **Safe** | 1,707 (78.3%) β 1,643 benign_sibling + 64 benign_control |
| **Unique CWEs** | 93 |
| **Unique CVEs** | 470 |
| **Chunk size** | 64 lines with 8-line overlap |
| **Hunk-centering** | Enabled (vulnerable chunks contain the sink) |
| **Near-duplicate threshold** | 0.75 token-similarity |
### Data Creation
The training data is created through a multi-stage pipeline:
1. **CVEfixes conversion**: `src/scripts/convert_cvefixes.py` streams the
CVEfixes dataset, filters to Python, applies noise and trivial-change
filters, and emits only vulnerable samples (`label=1`). The post-fix function
is retained in `fixed_code` but not emitted as `label=0`.
2. **Benign control mining**: `src/scripts/mine_benign_functions.py` extracts
safe functions from files the fix commit did not touch. These are tagged
`sample_subtype="benign_control"` and serve as the genuine safe class.
3. **Enrichment**: `src/training/dataset_builder.py` enriches each sample with
the full enclosing function (AST-expanded) and module-level imports.
4. **Chunking**: Functions are cut into 64-line windows with 8-line overlap.
For vulnerable samples, only chunks overlapping the diff hunk are kept
(hunk-centering).
5. **Quality filters**: Hard contradictions and near-duplicate safe chunks
(>0.75 token-similar to vulnerable chunks) are removed.
6. **Splitting**: Project-level 60/20/20 split with `seed=42`. No project
appears in more than one split.
### Safe Class Construction
The safe class is constructed from two sources:
- **Benign siblings** (1,643 samples): Functions from the same file as the
vulnerable function, but in commits the fix did not touch.
- **Benign controls** (64 samples): Functions from files the fix commit never
touched, mined from verified-benign repositories.
Both sources are passed through a token-similarity guard (>0.75 to any
vulnerable function β dropped) to prevent near-duplicates from entering the
safe class.
The **post-fix function is explicitly not used as `label=0`** because it is a
near-duplicate of its vulnerable twin (median token-similarity β 0.94). Using
it would create contradictory pairs and collapse training to `P = 0.5`.
### Data Splits
| Split | Samples | Vulnerable | Safe | Projects |
|-------|---------|------------|------|----------|
| Train | 1,464 | 316 | 1,148 | 330 |
| Validation | 358 | 76 | 282 | β |
| Test | 359 | 82 | 277 | β |
### Preprocessing
- **Tokenizer**: `AutoTokenizer` from `microsoft/codebert-base` with
`max_length=512`, `padding="max_length"`, `truncation=True`.
- **Chunking**: Uniform 64-line windows with 8-line overlap. Matches inference
format.
- **Labels**: `0` = safe, `1` = vulnerable. Mapped to `id2label = {0: "safe",
1: "vulnerable"}` and `label2id = {"safe": 0, "vulnerable": 1}`.
- **Problem type**: `single_label_classification` (softmax).
---
## Evaluation Data
### Datasets Used
The model is evaluated on two datasets:
1. **CVEfixes test split** (same corpus as training): 359 samples, project-level
split. This measures the model's standalone performance on held-out projects.
2. **VUDENC test split** (held-out corpus): 821 samples, project-level split.
This measures the model's performance on a completely different dataset.
### Metrics
| Metric | CVEfixes Test | VUDENC Test |
|--------|---------------|-------------|
| Accuracy | 81.9% | β |
| Precision | 100.0% | β |
| Recall | 20.7% | 70.5% |
| F1 | 34.3% | β |
| ROC-AUC | 0.0* | β |
| PR-AUC | 0.496 | β |
\* The standalone evaluator initially reported ROC-AUC=0.0 due to loading the
wrong checkpoint. This was fixed; the correct ROC-AUC on the CVEfixes validation
split is 74.9%.
The **gate study** (full CEVuD pipeline) is evaluated on VUDENC using F2
(beta=2.0) as the primary metric, with Token Reduction Rate (TRR) and Cost
Reduction as efficiency metrics.
---
## Quantitative Analysis
### Training Dynamics
| Epoch | Train Loss | Val Loss | Val Accuracy | Val Precision | Val Recall | Val F1 | Val ROC-AUC |
|-------|-----------|----------|--------------|---------------|------------|--------|-------------|
| 1 | β | 0.419 | 84.9% | 100.0% | 28.9% | 44.9% | 74.9% |
| 2 | β | 0.419 | 84.9% | 100.0% | 28.9% | 44.9% | 74.9% |
| 3 | β | 0.419 | 84.9% | 100.0% | 28.9% | 44.9% | 74.9% |
| 4 | 0.710 | 0.419 | 84.9% | 100.0% | 28.9% | 44.9% | 74.9% |
Training early-stopped at epoch 4 (patience=3 on validation loss). The best
checkpoint is from epoch 1 (step 366), which has the same validation metrics as
epoch 4.
### Confusion Matrix (Validation)
| | Predicted Safe | Predicted Vulnerable |
|---|---|---|
| **Actually Safe** | 282 (TN) | 0 (FP) |
| **Actually Vulnerable** | 54 (FN) | 22 (TP) |
### Confusion Matrix (Test)
| | Predicted Safe | Predicted Vulnerable |
|---|---|---|
| **Actually Safe** | 277 (TN) | 0 (FP) |
| **Actually Vulnerable** | 65 (FN) | 17 (TP) |
### Key Observations
- **Precision = 100%**: The model never produces a false positive. When it
predicts "vulnerable", it is always correct.
- **Recall = 28.9% (val) / 20.7% (test)**: The model misses most vulnerabilities.
This is expected for a small model trained on a difficult, imbalanced corpus.
- **ROC-AUC = 74.9%**: The model learns strong discriminative ranking. The low
recall reflects the classification threshold (0.5), not poor ranking ability.
- **Class imbalance effect**: The ~1:3.6 vulnerable/safe split causes the model
to be conservative. Class-weighted cross-entropy (weights β [0.64, 2.30])
gives the vulnerable class a ~3.6Γ higher per-sample gradient signal, but the
small dataset size limits how much the model can learn.
### Performance in the Gated Pipeline
When embedded in the CEVuD pipeline with the tuned linear gate
($W_1=0.15, W_2=0.85, T=0.2$):
| Metric | Value |
|--------|-------|
| Recall | 95.2% |
| Precision | 12.8% |
| F2 | 0.417 |
| Escalation Rate | 94.9% |
| TRR | 5.1% |
| Cost Reduction | 5.0% |
The linear gate improves recall from 70.5% (small model standalone) to 95.2%
by combining the neural signal with Semgrep's static signal. The trade-off is
lower precision (12.8%) and high escalation rate (94.9%), which is acceptable
because the escalated snippets are reviewed by a more capable LLM.
---
## Environmental Impact
- **Hardware**: CPU-only training (no GPU required).
- **Training time**: ~2.4 hours on a 4-core CPU (8,759 seconds).
- **Estimated CO2 emissions**: Using the [ML CO2 Impact calculator](https://mlco2.github.io/impact/),
CPU training for ~2.4 hours on an Intel i7-9700K emits approximately
0.2-0.4 kg CO2 (depending on electricity grid carbon intensity).
- **Inference cost**: The model runs locally on CPU/edge hardware. A single
inference on a 512-token chunk takes ~50-100ms on a modern CPU, with near-zero
marginal cost compared to cloud LLM APIs.
---
## Ethical Considerations
### Intended Users
CEVuD is designed for software developers, security engineers, and organizations
that want to shift-left security scanning in their CI/CD pipelines. The model
augments human experts by filtering safe code, not replacing them.
### Potential Misuse
- **False sense of security**: The model's 100% precision might lead users to
believe it never misses vulnerabilities. In reality, its standalone recall is
only 28.9%, and even in the gated pipeline, 4.8% of vulnerabilities slip
through (FN=5 out of 105 on VUDENC test). Users must understand that CEVuD is
a *filter*, not a definitive scanner.
- **Over-reliance on automation**: The low precision (12.8%) means many benign
snippets are escalated. If users skip reviewing escalated snippets, they waste
LLM resources without gaining security.
- **Bias in training data**: CVEfixes is biased toward well-known, high-profile
projects. Vulnerabilities in niche or internal codebases may not be
represented. The model may perform worse on code that differs stylistically
from the CVEfixes corpus.
### Fairness and Transparency
- The model's decisions are interpretable: the linear gate formula
`R = WβΒ·S_sev + WβΒ·P_slm` is transparent, and the weights are selected by
exhaustive grid search.
- The training data is publicly available, and the full training pipeline is
open-source.
- The model does not process personal data. Code snippets are the only input.
### Limitations and Recommendations
| Limitation | Recommendation |
|------------|----------------|
| Low standalone recall (28.9%) | Always use as part of the gated pipeline, not standalone. |
| Python-only | Do not apply to other languages without retraining. |
| Chunk-level granularity | Score whole functions by chunking and aggregating. |
| CWE imbalance | Consider augmenting rare CWE types if your use case targets specific vulnerabilities. |
| No adversarial evaluation | Do not deploy in adversarial settings without additional testing. |
---
## Training Procedure
### Implementation
Training is implemented in `src/training/trainer.py` using the HuggingFace
`Trainer` API with a custom `WeightedTrainer` subclass.
### Loss Function
**Class-weighted cross-entropy**: The ~1:3.6 vulnerable/safe imbalance is
countered by inverse-frequency class weights:
```
weight(class) = total_samples / (num_classes Γ count(class))
```
For the current split, this yields approximately `[0.64, 2.30]` for
`[safe, vulnerable]`, meaning each vulnerable sample contributes ~3.6Γ the
gradient signal of a safe sample.
**Why not focal loss?** Focal loss was evaluated but removed in favor of class
weights. Class-weighted cross-entropy is simpler, more interpretable, and
equally effective for this dataset size. The weights are computed automatically
from the training distribution.
### Optimization
- **Optimizer**: AdamW
- **Learning rate**: 2e-5
- **Weight decay**: 0.01
- **Batch size**: 8
- **Warmup**: Linear warmup for 10% of total steps
- **Scheduler**: Linear decay after warmup
### Regularization
- **Early stopping**: Patience=3 epochs on validation loss. Best checkpoint
restored.
- **Dropout**: 0.1 in the classifier head (default for RobertaClassificationHead)
- **Frozen backbone** (optional): When `freeze_backbone=True`, only the
classifier head is trained. Recommended for small datasets.
### Reproducibility
All randomness is controlled with `seed=42`:
- Dataset split: `seed=42`
- Sample capping: `seed=42`
- Model initialization: `seed=42`
- Training shuffle: `seed=42`
### Training Command
```bash
python -m src.training.cli run-all \
--manifest benchmark_manifest_cvefixes.json \
--benign-manifest benign_controls_manifest.json \
--epochs 30 \
--batch-size 8
```
---
## How to Get Started with the Model
### Installation
```bash
pip install transformers torch
```
### Loading the Model
```python
from transformers import AutoTokenizer, AutoModelForSequenceClassification
import torch
model_id = "Denash/codebert-vuln-classifier"
tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForSequenceClassification.from_pretrained(model_id)
model.eval()
```
### Scoring a Function
```python
def score_function(function_code: str, chunk_max_lines: int = 64,
chunk_overlap: int = 8) -> float:
"""Score a Python function for vulnerability probability."""
lines = function_code.splitlines()
chunks = []
for i in range(0, max(len(lines) - chunk_overlap, 1), chunk_max_lines - chunk_overlap):
chunk = "\n".join(lines[i:i + chunk_max_lines])
if chunk.strip():
chunks.append(chunk)
scores = []
for chunk in chunks:
inputs = tokenizer(chunk, truncation=True, max_length=512,
padding="max_length", return_tensors="pt")
with torch.no_grad():
logits = model(**inputs).logits
probs = torch.softmax(logits, dim=-1)
scores.append(float(probs[0, 1]))
return max(scores) if scores else 0.0
# Example usage
vuln_code = """
def get_user(user_id):
query = "SELECT * FROM users WHERE id = " + str(user_id)
return db.execute(query)
"""
print(f"P(vulnerable) = {score_function(vuln_code):.3f}")
```
### Using in the CEVuD Pipeline
```python
from triage_orchestrator import TriageOrchestrator
orchestrator = TriageOrchestrator(
config_path="config.json",
workspace_path="."
)
orchestrator.process_pipeline()
```
---
## Model Card Authors
CEVuD Authors
## Citation
```bibtex
@misc{cevud2026,
title={CEVuD: Cost-Effective Vulnerability Detection via Gated Static-Neural Reasoning},
author={CEVuD Authors},
year={2026},
note={Model: Denash/codebert-vuln-classifier; Training Dataset: Denash/cevud-training-dataset; Pipeline Dataset: Denash/cevud-pipeline-dataset}
}
```
## Model Card Contact
Open an issue on the CEVuD GitHub repository.
## Related Resources
| Resource | Link |
|----------|------|
| **Training Dataset (CVEfixes)** | [`Denash/cevud-training-dataset`](https://huggingface.co/datasets/Denash/cevud-training-dataset) |
| **Pipeline Dataset (VUDENC)** | [`Denash/cevud-pipeline-dataset`](https://huggingface.co/datasets/Denash/cevud-pipeline-dataset) |
| **Source Dataset (CVEfixes)** | [`hitoshura25/cvefixes`](https://huggingface.co/datasets/hitoshura25/cvefixes) |
| **Source Dataset (VUDENC)** | [`DetectVul/Vudenc`](https://huggingface.co/datasets/DetectVul/Vudenc) |
| **CEVuD GitHub** | https://github.com/Denash/CEVuD |
|