Denash's picture
Add YAML frontmatter to model card
eb10fa9 verified
|
Raw
History Blame Contribute Delete
18.8 kB
---
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 |