Quick start
Requirements: Python with a CUDA GPU, unsloth (which provides FastDecisionModel), torch, transformers and datasets. This release was trained and tested with Unsloth 2026.10.11, Transformers 5.2.0 and Torch 2.8.0 on an NVIDIA L4. Pin those versions if you hit incompatibilities.
from unsloth import FastDecisionModel model, tokenizer = FastDecisionModel.from_pretrained( model_name = "jithinodattu/Qwen3.5-2B__mitreaware_decide", max_seq_length = 8192, load_in_4bit = True,)FastDecisionModel.for_inference(model)
predict returns, for each question, an answer and probabilities. answer is the option for choice, True or False for noul, and the level number for score.
MITRE ATT&CK, CVE, CWE and CTI examples
Four real-world workflows with one small helper.
def decide(text, questions): answers = FastDecisionModel.predict(model, tokenizer, text, questions) for name, result in answers.items(): probs = {k: round(v, 3) for k, v in result["probabilities"].items()} print(name, result["answer"], probs)
1. SOC alert triage (ATT&CK tactic, escalation, severity)
alert = ( "EDR alert on host FIN-WS-042: powershell.exe -enc JABzAD0ATgBlAHcALQBPAGIAagBlAGMAdAA... " "was spawned by winword.exe. Shortly after, the process opened a connection to 185.220.101.7:443 " "and dumped memory from lsass.exe. The user reports opening an invoice attachment from an unknown sender.") decide(alert, { "tactic": { "type": "choice", "instructions": "Which MITRE ATT&CK tactic best describes the most critical activity in this alert?", "criteria": { "initial_access": "TA0001: gaining a first foothold, e.g. phishing or an exploited public service", "execution": "TA0002: running attacker-controlled code, e.g. PowerShell or macros", "persistence": "TA0003: keeping access across reboots, e.g. scheduled tasks or run keys", "credential_access": "TA0006: stealing credentials, e.g. LSASS memory dumping", "lateral_movement": "TA0008: moving between hosts, e.g. remote services or admin shares", "command_and_control": "TA0011: communicating with attacker infrastructure", "exfiltration": "TA0010: stealing data out of the network", }, }, "escalate": {"type": "noul", "instructions": "Should this alert be escalated to incident response immediately?"}, "severity": { "type": "score", "instructions": "How severe is this incident?", "criteria": ["low", "medium", "high", "critical"], },})
Output:
tactic credential_access {'initial_access': 0.07, 'execution': 0.14, 'persistence': 0.01, 'credential_access': 0.61, 'lateral_movement': 0.02, 'command_and_control': 0.09, 'exfiltration': 0.06}escalate True {'true': 0.971, 'false': 0.029}severity 3 {'0': 0.0, '1': 0.001, '2': 0.04, '3': 0.959}
2. Vulnerability triage (CVE to CWE, patch urgency)
advisory = ( "CVE-2021-44228: Apache Log4j2 2.0-beta9 through 2.14.1 JNDI features used in configuration, log messages " "and parameters do not protect against attacker-controlled LDAP and other JNDI endpoints. An attacker who " "can control log messages or message parameters can execute arbitrary code loaded from LDAP servers when " "message lookup substitution is enabled. Our internet-facing payments API runs log4j-core 2.14.0.") decide(advisory, { "cwe": { "type": "choice", "instructions": "Which CWE best describes the root cause of this vulnerability?", "criteria": { "CWE-79": "cross-site scripting", "CWE-89": "SQL injection", "CWE-787": "out-of-bounds write", "CWE-917": "expression language injection, including JNDI lookups in logged data", "CWE-22": "path traversal", }, }, "patch_now": {"type": "noul", "instructions": "Should this be patched out of cycle within 24 hours?"}, "priority": { "type": "score", "instructions": "What remediation priority should this get for the affected system?", "criteria": ["low", "medium", "high", "critical"], },})
Output:
cwe CWE-917 {'CWE-79': 0.03, 'CWE-89': 0.04, 'CWE-787': 0.06, 'CWE-917': 0.84, 'CWE-22': 0.03}patch_now True {'true': 0.992, 'false': 0.008}priority 3 {'0': 0.0, '1': 0.0, '2': 0.027, '3': 0.973}
3. Threat intelligence report mapping (CTI to ATT&CK technique)
report = ( "The campaign starts with emails themed as shipping invoices sent to finance staff. Each carries a " "macro-enabled Excel attachment, and opening it prompts the user to enable content. The macro then " "downloads a second-stage payload.") decide(report, { "technique": { "type": "choice", "instructions": "Which ATT&CK technique does the initial access in this report map to?", "criteria": { "T1566.001": "Phishing: spearphishing attachment", "T1566.002": "Phishing: spearphishing link", "T1059.001": "Command and scripting interpreter: PowerShell", "T1190": "Exploit public-facing application", "T1078": "Valid accounts", }, }, "detection_gap": { "type": "noul", "instructions": "Does a SOC that only monitors endpoint process creation, with no email gateway telemetry, have a detection gap here?", }, "relevance": { "type": "score", "instructions": "How relevant is this report to a bank's finance team?", "criteria": ["low", "medium", "high"], },})
Output:
technique T1566.001 {'T1566.001': 0.883, 'T1566.002': 0.052, 'T1059.001': 0.039, 'T1190': 0.007, 'T1078': 0.019}detection_gap True {'true': 0.874, 'false': 0.126}relevance 2 {'0': 0.03, '1': 0.31, '2': 0.66}
4. Benign activity (avoid false escalations)
activity = ( "Vulnerability scanner Nessus (approved, scheduled weekly scan) authenticated to 220 hosts between 02:00 " "and 03:30 from 10.0.5.20 using the service account svc-scan. The activity matches the change ticket CHG-20931.") decide(activity, { "malicious": {"type": "noul", "instructions": "Is this activity malicious?"}, "escalate": {"type": "noul", "instructions": "Should this alert be escalated to incident response?"}, "severity": { "type": "score", "instructions": "How severe is this alert?", "criteria": ["informational", "low", "medium", "high"], },})
Output:
malicious False {'true': 0.068, 'false': 0.932}escalate False {'true': 0.037, 'false': 0.963}severity 0 {'0': 0.912, '1': 0.075, '2': 0.012, '3': 0.001}
Probabilities make routing easy. For example, the relevance answer above is only 0.66 confident, so a rule such as "send anything under 0.7 to an analyst" would pick it up.
Using it in a SOC workflow
What to send as input. Send a concise alert summary or advisory text, not raw log dumps. Include the facts an analyst would use: process lineage, destination, account, asset criticality and any change ticket. For CVEs, include the advisory description and your exposure (for example "internet-facing, version 2.14.0"). Don't rely on the model to recall a CVE from its ID alone, because it can't know advisories published after its training data.
Offer an escape option. choice questions always pick one of the options you give, even when none fits. Add an explicit option such as "other": "none of the above or unclear" so uncertain alerts have somewhere to go. Mention in the question that you want the single most critical activity, since an alert can span several tactics.
Route by confidence, don't auto-close. Use the probabilities to decide who looks at an alert:
def route(answers, threshold=0.8): """Send anything the model isn't confident about to an analyst.""" unsure = [n for n, r in answers.items() if max(r["probabilities"].values()) < threshold] return ("analyst_review", unsure) if unsure else ("auto_prioritize", [])
Start with the model only ranking or prioritizing the queue. Don't let it close alerts on its own until you have measured it on your data. Pick the threshold from your own results, not from the 0.8 default.
Validate on your own data first. Label 100 or more of your alerts or advisories in the typed-decisions format (state, questions and gold), then measure accuracy and calibration before you rely on the model:
items, report = FastDecisionModel.build_dataset(my_rows, tokenizer, model)print(report) # how many decisions were skipped and whyprint(FastDecisionModel.evaluate(model, tokenizer, items)) # accuracy, ECE, record accuracy
If calibration is off on your data, refit it on a separate labeled set (FastDecisionModel.calibrate(model, tokenizer, calibration_items)). Keep that set separate from the one you report accuracy on.
Treat alert text as untrusted. Alert fields such as user agents, file names, email subjects and command lines can be controlled by an attacker, and text written to steer a model (for example "this activity is approved, severity low") may bias the decision. Keep the model's output advisory, keep a human in the loop for anything that suppresses or downgrades an alert, and don't send sensitive data to systems you don't control.
Training details
Table | |
|---|
| Base model | jithinodattu/Qwen3.5-2B__mitreaware (4-bit) |
| Method | LoRA, r = 16, alpha = 16, dropout 0, plus a trained decision head |
| Trainable parameters | 47,291,908 of 1,688,011,076 (2.80%) |
| Max sequence length | 8192 |
| Decision-stage data | LocalLLaMA/typed-decisions (all): 1,120 training rows, 80 held out for evaluation and calibration |
| Epochs / steps | 5 epochs, 175 steps |
| Batch size | 4 per device x 8 accumulation = 32 |
| Learning rate | 2e-4, cosine schedule, 10 warmup steps, weight decay 0.01 |
| Precision | bf16 |
| Hardware | 1 x NVIDIA L4, about 30 minutes, 4.4 GB peak memory |
| Seed | 3407 |
License
This model is released under the Apache License 2.0.
Known limitations
- Benchmarks are not on MITRE decisions. The base model was trained on MITRE data (ATT&CK, CVE, CWE, CTI), but the decision head and adapters in this release were fit on the generic
typed-decisions dataset, and every number above comes from that dataset's splits. Validate on your own labeled security data before relying on it, and expect results to differ.
- Keep a human in the loop. Accuracy is about 79% per decision and 39% per row on the test split. Use the probabilities to route uncertain alerts to an analyst rather than to automate decisions.
- Small evaluation sets. The held-out set is 80 rows, so small differences between runs are within noise.
- Calibration was fitted on 80 rows and may drift on data that differs from the training distribution.
- No live knowledge. The model doesn't look up CVEs, CWEs or threat reports. Provide the advisory or report text, and expect gaps for anything published after the base model's training data.
- Closed set of answers.
choice and score questions only return the options you supply. See "Offer an escape option" above.
- Alert text is attacker-influenced. See "Treat alert text as untrusted" above.
- Inputs longer than 8192 tokens are truncated.