Skip to content

Commit fab0a9e

Browse files
new: [trainers] Never train the CWE classifier on discouraged CWEs (ref #17)
The CWE classifier was trained to predict deep ancestor CWEs, which are mostly Pillars and Classes whose MITRE mapping usage is Discouraged (CWE-664, CWE-707, CWE-703, CWE-284, ...), so its suggestions were almost always CWEs that should not be used for mapping. The knowledge base is now refreshed from the Vulnerability-Lookup API by tools/cwe/update_cwe_knowledge_base.py, which also stores each CWE's Mapping_Notes usage (Allowed, Allowed-with-Review, Discouraged, Prohibited) in cwe_usage.json. tools/cwe/build_child_to_ancestor.py then regenerates vulntrain/data/deep_child_to_ancestor.json, mapping each CWE to its highest ancestor with an Allowed or Allowed-with-Review usage. The label set goes from 26 discouraged ancestors to 303 allowed CWEs; CWEs with no allowed entry on their ancestor path (e.g. CWE-20, CWE-200) are left out so the trainer drops those examples. The trainer itself is unchanged apart from loading its mapping relative to the package. Also finalize the path fixes in the CWE mapping scripts that were left unstaged by the previous restructuring commit.
1 parent 5927e1c commit fab0a9e

9 files changed

Lines changed: 37540 additions & 944 deletions

tools/cwe/README.md

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,30 @@ regenerated.
77
## Pipeline
88

99
```
10-
vulnerability.circl.lu.json # raw CWE export from https://vulnerability.circl.lu
10+
python tools/cwe/update_cwe_knowledge_base.py # fetches from the Vulnerability-Lookup API
1111
12-
▼ python tools/cwe/mapping_child_parent_cwe.py
13-
parent_to_children_mapping.json # parent CWE → direct children
12+
├── vulnerability.circl.lu.json # raw CWE records
13+
└── cwe_usage.json # per-CWE mapping usage (Allowed, Discouraged, ...)
1414
15-
▼ python tools/cwe/hierarchy.py
16-
cwe_hierarchy.json # CWEs grouped by hierarchy depth level
15+
▼ python tools/cwe/build_child_to_ancestor.py
16+
vulntrain/data/deep_child_to_ancestor.json # CWE → highest allowed ancestor (trainer input)
1717
```
1818

19+
Each CWE is mapped to its highest ancestor whose MITRE mapping usage is
20+
*Allowed* or *Allowed-with-Review*, so the CWE classifier is never trained to
21+
predict a Discouraged or Prohibited CWE (issue #17). CWEs without any allowed
22+
entry on their ancestor path (e.g. CWE-20, CWE-200) are excluded: the trainer
23+
drops the corresponding examples.
24+
1925
## Files
2026

2127
| File | Role |
2228
|------|------|
23-
| `vulnerability.circl.lu.json` | Raw CWE data snapshot (input). |
29+
| `update_cwe_knowledge_base.py` | Refreshes the knowledge base from `https://vulnerability.circl.lu/api/cwe/`. |
30+
| `vulnerability.circl.lu.json` | Generated: raw CWE data snapshot. |
31+
| `cwe_usage.json` | Generated: per-CWE name, abstraction, and mapping usage. |
32+
| `build_child_to_ancestor.py` | Builds `vulntrain/data/deep_child_to_ancestor.json` (shipped with the package, consumed by `vulntrain-train-cwe-classification`). |
2433
| `mapping_child_parent_cwe.py` | Builds `parent_to_children_mapping.json` from the raw snapshot. |
2534
| `parent_to_children_mapping.json` | Generated: parent → children mapping. |
2635
| `hierarchy.py` | Builds `cwe_hierarchy.json` (hierarchy levels) from the parent/children mapping. |
2736
| `cwe_hierarchy.json` | Generated: CWE IDs grouped by depth level. |
28-
29-
The mapping consumed at training time by
30-
`vulntrain-train-cwe-classification` lives in
31-
`vulntrain/data/deep_child_to_ancestor.json` (CWE child → top ancestor); it is
32-
maintained by hand and shipped with the package.
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
"""Build the child-to-ancestor CWE mapping used by the CWE classifier.
2+
3+
Regenerates ``vulntrain/data/deep_child_to_ancestor.json`` from the knowledge
4+
base produced by ``update_cwe_knowledge_base.py``. Each CWE is mapped to its
5+
highest ancestor whose MITRE mapping usage is *Allowed* or
6+
*Allowed-with-Review*, so the classifier is never trained to predict a
7+
Discouraged or Prohibited CWE (e.g. the Pillars CWE-664, CWE-707...).
8+
See https://github.com/vulnerability-lookup/VulnTrain/issues/17
9+
10+
CWEs with no allowed entry anywhere on their ancestor path are left out of
11+
the mapping: the trainer then drops the corresponding training examples.
12+
"""
13+
14+
import json
15+
from pathlib import Path
16+
17+
ALLOWED_USAGES = {"Allowed", "Allowed-with-Review"}
18+
19+
HERE = Path(__file__).parent
20+
OUTPUT = HERE.parent.parent / "vulntrain" / "data" / "deep_child_to_ancestor.json"
21+
22+
23+
def primary_parent(record: dict) -> str | None:
24+
related = (record.get("Related_Weaknesses") or {}).get("Related_Weakness")
25+
if not related:
26+
return None
27+
if isinstance(related, dict):
28+
related = [related]
29+
child_of = [rel for rel in related if rel.get("@Nature") == "ChildOf"]
30+
if not child_of:
31+
return None
32+
for rel in child_of:
33+
if rel.get("@Ordinal") == "Primary":
34+
return rel.get("@CWE_ID")
35+
return child_of[0].get("@CWE_ID")
36+
37+
38+
def main() -> None:
39+
with open(HERE / "vulnerability.circl.lu.json") as f:
40+
records = json.load(f)["data"]
41+
with open(HERE / "cwe_usage.json") as f:
42+
usage = json.load(f)
43+
44+
parent = {
45+
record["@ID"]: primary_parent(record)
46+
for record in records
47+
if primary_parent(record)
48+
}
49+
50+
def highest_allowed_ancestor(cwe_id: str) -> str | None:
51+
best = None
52+
node: str | None = cwe_id
53+
visited = set()
54+
while node and node not in visited:
55+
visited.add(node)
56+
if usage.get(node, {}).get("usage") in ALLOWED_USAGES:
57+
best = node
58+
node = parent.get(node)
59+
return best
60+
61+
mapping = {}
62+
unmappable = []
63+
for record in records:
64+
cwe_id = record["@ID"]
65+
ancestor = highest_allowed_ancestor(cwe_id)
66+
if ancestor:
67+
mapping[cwe_id] = ancestor
68+
else:
69+
unmappable.append(cwe_id)
70+
71+
with open(OUTPUT, "w") as f:
72+
json.dump(dict(sorted(mapping.items(), key=lambda kv: int(kv[0]))), f, indent=2)
73+
74+
labels = sorted(set(mapping.values()), key=int)
75+
discouraged = [
76+
cwe for cwe in labels if usage.get(cwe, {}).get("usage") not in ALLOWED_USAGES
77+
]
78+
print(f"{len(mapping)} CWEs mapped to {len(labels)} labels -> {OUTPUT}")
79+
print(f"Labels with non-allowed usage: {discouraged or 'none'}")
80+
if unmappable:
81+
print(
82+
f"{len(unmappable)} CWEs without any allowed ancestor "
83+
f"(excluded): {sorted(unmappable, key=int)}"
84+
)
85+
86+
87+
if __name__ == "__main__":
88+
main()

0 commit comments

Comments
 (0)