File size: 5,287 Bytes
0b5983c
 
 
 
4dc95b5
0b5983c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
972082f
 
0b5983c
 
4dc95b5
0b5983c
 
 
27dd06b
 
 
 
 
0b5983c
 
 
 
 
 
 
 
972082f
 
0b5983c
27dd06b
 
 
 
 
0b5983c
 
 
 
 
 
4dc95b5
0b5983c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
972082f
 
 
 
 
 
 
 
 
 
8e966fe
 
 
 
 
0b5983c
 
 
 
4dc95b5
972082f
 
0b5983c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
---
license: apache-2.0
base_model: Qwen/Qwen3-8B
datasets:
  - exploitintel/cve-cwe-consensus
language:
  - en
tags:
  - cybersecurity
  - vulnerability
  - cve
  - cwe
  - text-classification
  - qlora
  - unsloth
pipeline_tag: text-generation
library_name: transformers
---

# CVE → CWE Classifier (Qwen3-8B)

A QLoRA fine-tune of **Qwen3-8B** that maps a free-text **CVE description** to the **CWE weakness
ID(s)** it corresponds to. The LoRA adapter is merged into the base and released in 16-bit, so it
loads directly with `transformers`. A higher-quality (larger) variant is at
[`exploitintel/cve-cwe-qwen3-32b`](https://huggingface.co/exploitintel/cve-cwe-qwen3-32b).

Trained only on labels where **NVD and the CNA agree** after roll-up to **CWE View-1003** — see the
[`cve-cwe-consensus`](https://huggingface.co/datasets/exploitintel/cve-cwe-consensus) dataset.

## Results (held-out test split, 6,802 rows)

| Metric | Score |
|---|---|
| Exact-match | **0.676** |
| Micro-F1 | **0.702** |
| Macro-F1 | **0.511** |

By difficulty (does the description *name* the weakness, or must it be inferred?):

| Stratum | n | Exact-match | Micro-F1 |
|---|---|---|---|
| Easy (weakness named) | 2,046 | 0.841 | 0.870 |
| Hard (must infer) | 4,756 | 0.605 | 0.628 |

The macro-F1 reflects a dataset that caps majority CWEs (e.g. CWE-79) so rare weaknesses are
learned rather than drowned out.

**Reading the numbers:**
- **Macro-F1 is computed over the union of gold and predicted labels** (125 = 117 gold + ~8 the model predicted outside the gold set). Those out-of-label predictions score ~0 and pull macro *down*, so 0.511 is a **conservative** figure.
- **Exact-match has an inherent ceiling of ~98.3%:** ~1.74% of the test set (273 groups / 1,205 rows) are identical descriptions mapped to *different* CWEs (e.g. a bare "Windows Kernel Elevation of Privilege Vulnerability"), which a description-only model cannot disambiguate.
- Scores are on the **capped/balanced** test split (~30% "easy" rows), so they are **not** directly comparable to metrics measured on a different (e.g. natural-distribution) split.

## Usage

```python
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer

mid = "exploitintel/cve-cwe-qwen3-8b"
tok = AutoTokenizer.from_pretrained(mid)
model = AutoModelForCausalLM.from_pretrained(mid, torch_dtype="auto", device_map="auto")

messages = [
    {"role": "system", "content": "You are a vulnerability analyst. Given a CVE description, "
                                   "reply with only the CWE ID(s) it maps to, comma-separated."},
    {"role": "user", "content": "A SQL injection vulnerability in the login endpoint allows an "
                                "unauthenticated attacker to execute arbitrary SQL via the username parameter."},
]
inputs = tok.apply_chat_template(messages, add_generation_prompt=True, return_tensors="pt").to(model.device)
out = model.generate(inputs, max_new_tokens=32, do_sample=False)
print(tok.decode(out[0][inputs.shape[-1]:], skip_special_tokens=True))
# -> CWE-89
```

### GGUF / Ollama

A `Q4_K_M` GGUF is included in this repo for local runners:

```bash
ollama run hf.co/exploitintel/cve-cwe-qwen3-8b:Q4_K_M
```

Set the same system prompt (`/set system You are a vulnerability analyst...`) so it returns bare CWE IDs.

> **Note:** This Ollama command has not been verified end-to-end. This is a standard `qwen3`
> model so the embedded template should apply normally — but if `ollama run` ignores the
> system prompt and produces rambling text instead of a bare CWE ID, supply an explicit
> ChatML Modelfile `TEMPLATE` as shown in the [Qwen3.5-4B card](https://huggingface.co/exploitintel/cve-cwe-qwen35-4b).

## Training

- **Base:** `Qwen/Qwen3-8B` (trained 4-bit via `unsloth/qwen3-8b-unsloth-bnb-4bit`)
- **Method:** QLoRA (4-bit) with Unsloth, merged to 16-bit · released checkpoint: **checkpoint-960** (final; eval loss declined monotonically through training)
- **Dataset:** [`exploitintel/cve-cwe-consensus`](https://huggingface.co/datasets/exploitintel/cve-cwe-consensus) — 69,386 rows (55,810 / 6,774 / 6,802), majority CWEs capped at 2,500
- **Settings:** 2 epochs · context 512 · LR 2e-4 · AdamW 8-bit · linear schedule · packing on · train-on-completions-only off · seed 3407
- LoRA fine-tune, adapter merged into the base. Exact per-run **LoRA rank/alpha, batch size, and weight decay were not logged to the repo.**

## Prompt format

ChatML (Qwen3 standard). System prompt fixed; the description is the only user input — never feed the
label or CVE-ID.

- **system:** `You are a vulnerability analyst. Given a CVE description, reply with only the CWE ID(s) it maps to, comma-separated.`
- **user:** the CVE description
- **assistant:** `CWE-79, CWE-80`

## Limitations

- CWEs below the dataset's 50-example floor are not in the label space and won't be predicted.
- Outputs CWE IDs as text and can occasionally emit a malformed/non-existent ID — validate against
  the official CWE list.
- English-only; descriptions only (no code, CVSS, or references).
- A triage/assist aid, not an authoritative CWE assignment — human-review before acting.

## License

Apache-2.0 (inherited from Qwen3-8B). Dataset derives from public upstreams (NVD, MITRE CVE/CWE).