Text Classification
Transformers
Safetensors
English
modernbert
cyber-threat-intelligence
mitre-attack
multi-label-classification
defensive-security
blue-team
threat-intelligence
text-embeddings-inference
Instructions to use ctokx/cti-attack-mapper-modernbert with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use ctokx/cti-attack-mapper-modernbert with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-classification", model="ctokx/cti-attack-mapper-modernbert")# Load model directly from transformers import AutoTokenizer, AutoModelForSequenceClassification tokenizer = AutoTokenizer.from_pretrained("ctokx/cti-attack-mapper-modernbert") model = AutoModelForSequenceClassification.from_pretrained("ctokx/cti-attack-mapper-modernbert", device_map="auto") - Notebooks
- Google Colab
- Kaggle
| """MITRE ATT&CK technique metadata, used for readable labels and the keyword baseline. | |
| Source: https://github.com/mitre-attack/attack-stix-data (ATT&CK Terms of Use — | |
| free to use with attribution). | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import urllib.request | |
| from . import config | |
| def _download() -> None: | |
| if config.ATTACK_STIX_RAW.exists(): | |
| return | |
| print(f"downloading ATT&CK STIX bundle (~35 MB) …") | |
| urllib.request.urlretrieve(config.ATTACK_STIX_URL, config.ATTACK_STIX_RAW) | |
| def _parse_bundle() -> tuple[dict[str, str], dict[str, str]]: | |
| """Return ``(names, status)`` for every technique ID in the STIX bundle. | |
| Revoked and deprecated techniques are **kept**, not filtered out. The TRAM | |
| corpus was annotated against an older ATT&CK release, and two of its labels | |
| (``T1562.001``, ``T1574.002``) have since been revoked by MITRE. Dropping | |
| them here would silently blank their names in every report and hide a real | |
| provenance issue; instead they are named and flagged. | |
| """ | |
| _download() | |
| with open(config.ATTACK_STIX_RAW, encoding="utf-8") as fh: | |
| bundle = json.load(fh) | |
| names: dict[str, str] = {} | |
| status: dict[str, str] = {} | |
| for obj in bundle.get("objects", []): | |
| if obj.get("type") != "attack-pattern": | |
| continue | |
| ext = next( | |
| (r for r in obj.get("external_references", []) | |
| if r.get("source_name") == "mitre-attack"), | |
| None, | |
| ) | |
| if not (ext and ext.get("external_id")): | |
| continue | |
| tid = ext["external_id"] | |
| if obj.get("revoked"): | |
| state = "revoked" | |
| elif obj.get("x_mitre_deprecated"): | |
| state = "deprecated" | |
| else: | |
| state = "current" | |
| # a current definition always wins over a revoked one sharing the ID | |
| if tid in status and status[tid] == "current" and state != "current": | |
| continue | |
| names[tid] = obj.get("name", "") | |
| status[tid] = state | |
| qualified = {} | |
| for tid, name in names.items(): | |
| if "." in tid: | |
| parent = names.get(tid.split(".")[0]) | |
| qualified[tid] = f"{parent}: {name}" if parent else name | |
| else: | |
| qualified[tid] = name | |
| return qualified, status | |
| def build_technique_names() -> dict[str, str]: | |
| """Map technique ID (``T1027``, ``T1059.003``) -> human-readable name. | |
| Sub-technique names are qualified with their parent, so ``T1059.003`` | |
| becomes ``Command and Scripting Interpreter: Windows Command Shell``. | |
| """ | |
| if config.ATTACK_NAMES_JSON.exists(): | |
| return json.loads(config.ATTACK_NAMES_JSON.read_text(encoding="utf-8")) | |
| qualified, status = _parse_bundle() | |
| config.ATTACK_NAMES_JSON.write_text( | |
| json.dumps(qualified, indent=2, ensure_ascii=False), encoding="utf-8") | |
| config.ATTACK_STATUS_JSON.write_text( | |
| json.dumps(status, indent=2), encoding="utf-8") | |
| return qualified | |
| def build_technique_status() -> dict[str, str]: | |
| """Map technique ID -> ``current`` | ``revoked`` | ``deprecated``.""" | |
| if config.ATTACK_STATUS_JSON.exists(): | |
| return json.loads(config.ATTACK_STATUS_JSON.read_text(encoding="utf-8")) | |
| _, status = _parse_bundle() | |
| config.ATTACK_STATUS_JSON.write_text( | |
| json.dumps(status, indent=2), encoding="utf-8") | |
| return status | |
| def technique_keywords(technique_ids: list[str]) -> dict[str, list[str]]: | |
| """Surface forms to search for in text, per technique — the keyword baseline. | |
| Derived from the technique's own name plus its parent's, lowercased. This is | |
| deliberately naive: it is the zero-training floor a learned model must clear | |
| to justify existing. | |
| """ | |
| names = build_technique_names() | |
| out: dict[str, list[str]] = {} | |
| for tid in technique_ids: | |
| forms = set() | |
| full = names.get(tid, "") | |
| for part in full.split(":"): | |
| part = part.strip().lower() | |
| if len(part) >= 4: | |
| forms.add(part) | |
| out[tid] = sorted(forms) | |
| return out | |