CVE to CWE Mapping: How I Built a Retrieval-and-Rank Pipeline to Map Vulnerabilities to Weakness Categories
- Hriday Saha

- Jul 12
- 4 min read

In a previous role, I spent a long stretch of my time mapping audit issues to internal controls. Thousands of loosely worded findings, each one needing to land under the right control category, and a team doing it by hand with keyword search and institutional memory. It worked, but it was slow, and it depended on people who'd been there long enough to know the mapping by instinct.
When I left that project, I kept thinking about the shape of the problem. It wasn't really about audits or controls. It was about matching messy free text to a fixed, controlled taxonomy, at a scale no team of humans can keep up with, in a setting where "trust me" isn't an acceptable answer. So I rebuilt the methodology from scratch on public data, to see if it held up outside the setting where I first found it.
Why CVE to CWE mapping
I landed on mapping CVE descriptions, individual vulnerability reports written by researchers, to CWE categories, the standardized weakness taxonomy MITRE maintains. Something like "Buffer overflow in the parseHeader function of libfoo 2.1 allows remote attackers to execute arbitrary code" has to land against the right entry in a taxonomy of 900+ weakness types. MITRE assigns those labels by hand today. It works, but it doesn't scale, and a lot of findings in downstream security tooling never get a CWE label at all.
Structurally, it's the same problem as the audit work: free text written by someone who wasn't thinking about your taxonomy, landing against one of many categories, some of which sound almost identical. It also had the advantage of being entirely public, which meant I could publish the whole thing, code and results included, instead of describing work I couldn't show.
Why this is harder than "throw a transformer at it"
The obvious framing is text similarity: embed everything, compare, done. That framing isn't wrong, but it hides three things that actually make the problem hard.
The vocabulary gap is real. CVE descriptions use operational language: "buffer overflow allows remote code execution." CWE definitions use structural language: "improper restriction of operations within the bounds of a memory buffer." A model that leans on surface word overlap will confidently produce wrong matches.
The label space is wide and long-tailed. A handful of CWEs account for most CVEs; hundreds are hit rarely. A model optimized on the head will quietly ignore the tail.
Explainability isn't optional. In security triage, every mapping decision needs a reason attached. "The model said so" doesn't fly with a security engineer deciding what to patch first.
The architecture
I ended up with three stages, plus a human validation step in between the unsupervised and supervised parts:

It's the same shape production search and recommendation systems tend to converge on. Not novel. Just right.
The design decisions that actually mattered
Anyone can wire SBERT up to cosine similarity. The interesting part is the handful of choices underneath that shape, and why each one won over the plausible alternative.
Retrieval before semantics, not instead of it. Going straight to dense semantic comparison across the full CWE catalog would work, but it wastes compute and produces noisier top-1 results. BM25 is cheap and right most of the time, so it narrows the full catalog to the top-K candidates in milliseconds, and SBERT reranks only those. The metric that actually matters here is recall at K: if the true match isn't in the candidate set, no amount of reranking downstream can recover it.
Frame the supervised stage as ranking, not classification. The natural instinct is a multi-class classifier: given a CVE, predict its CWE ID directly. That framing fights the data. The class space is wide with thin per-class support, it throws away the retrieval structure by considering all 900+ CWEs from scratch when K of them are already known to be close, and adding a new CWE means retraining the output head. Reframing it as "given this CVE and these K candidates, order them" turns a 900-way classification problem into a K-way ranking problem. A LightGBM ranker trained with lambdarank, using SBERT similarity, BM25 score, token overlap, and metadata as features, beat both a logistic regression baseline and a feed-forward neural alternative.
Don't fine-tune SBERT, at least not yet. Contrastive fine-tuning on the labeled pairs was on the table, but I left it out of the default pipeline for two reasons. With a modest labeled corpus, fine-tuning a large model risks overfitting to that specific label distribution. More importantly, keeping SBERT as a stable, off-the-shelf component and pushing all the learning-from-labels behavior into the downstream ranker makes the system easier to monitor and retrain. When something drifts, you know exactly where to look. Fine-tuning is still there as an optional stage for anyone working with a larger corpus.
Explainability as a first-class output. Every prediction ships with a similarity score as a confidence signal, the matched key terms between the CVE and the suggested CWE, and SHAP feature attributions from the ranker showing which signals actually drove the ranking. Bolting this on after the fact is expensive. Building it in from day one is nearly free.

Results
Trained on NVD-provided CVE-to-CWE mappings, split by CVE publish date rather than randomly, to keep the model from ever seeing the future during training:
Metric | Value |
Top-1 accuracy | 87.4% |
Top-3 accuracy | 96.1% |
Top-5 accuracy | 98.3% |
Mean Reciprocal Rank | 0.921 |
Retrieval recall @ K=20 | 99.4% |
The retrieval recall number is the one I'd point to first. At K=20, the correct CWE is sitting in the candidate set 99.4% of the time, which means almost all of the system's error comes from ranking within an already-good candidate set, not from missing the answer entirely. That's a healthier failure mode than a classifier that occasionally rules out the right answer before it ever gets a chance.
What generalizes
The biggest lesson wasn't about the model. It's that this pattern, mapping unstructured operational text to a controlled taxonomy under limited labels and hard explainability requirements, is more general than it looks from inside any one domain. Compliance mapping, ticket triage, legal clause classification, and the original audit-to-control problem I started with are all the same underlying challenge wearing different clothes. The CVE-to-CWE dataset is just the public, reproducible version of it.
Try it yourself
The full code, configs, and evaluation results are on GitHub.
Comments