A language model drives the triage.
I wanted to see what happens when a language model drives malware triage instead of a fixed script. You upload a Windows .exe or .dll. MalWhere hashes it, checks a cache, and then lets a local model call analysis tools one at a time.
Each result goes back to the model, which decides what to ask for next and finally writes a verdict, a confidence score, the MITRE ATT&CK techniques it saw and the indicators of compromise. Nothing ever runs the sample: every tool reads its bytes.
One investigation, step by step.
- Hash and check the cache
The API reads the upload, takes its SHA-256 and looks it up in SQLite. A hit returns the stored report with no model call.
- Ask the model
On a miss the loop starts. The model replies with one JSON object: a tool name, its arguments and a one-sentence reason.
- Run the tool and stream the step
The app runs that tool and pushes the step to the dashboard straight away, as a server-sent event.
- Trim the result for the model
A trimmed copy goes back into the conversation: long strings cut to 200 characters, lists to 10 items, and the byte image left out. The dashboard keeps the full result.
- Repeat, ten turns at most
If the model has not finished by turn ten, it is told to write its report from what it has.
- Store the report
The final report is saved in SQLite under the file's hash, so the same file never costs a second run.
Nine tools. Seven read the file, two ask the network.
Every tool returns plain JSON. The seven local ones parse the file with pefile, regular expressions, YARA and a LightGBM model; none of them executes it.
get_pe_info- Hashes, sections, compile time and entry point.
analyze_imports- The import table, with 16 risky Windows APIs mapped to ATT&CK techniques.
scan_section_entropy- Shannon entropy per section. Above 7.0 is flagged as likely packed, above 7.5 as encrypted or compressed.
extract_strings- Printable strings sorted into URLs, IP addresses, paths, registry keys and suspicious keywords.
scan_yara- Community YARA rules, compiled one file at a time so a broken rule does not stop the scan.
ml_risk_score- EMBER features through a pre-trained LightGBM model, scored from 0 to 1.
visualize_pe- The file's bytes as a 512-pixel-wide grayscale image, for the person reading the report.
hash_lookup- VirusTotal and MalwareBazaar. A hash counts as known malware only if MalwareBazaar has it, or VirusTotal shows at least 5 detections and at least 10% of engines.
threat_intel_lookup- AlienVault OTX and ThreatFox, called only after the hash lookup found detections.
Where the model decides, and where it doesn't.
The system prompt sets most of the order: file metadata first, then the hash lookup, then every static tool, then the EMBER score. The one real branch is the deeper threat-intel lookup, which depends on what the hash lookup found.
Within that, the model chooses each call, passes the arguments, explains every step in a sentence, and weighs all of the evidence into a verdict. Giving the loop more freedom is the obvious next experiment.
I have not measured detection accuracy, so I make no claim that the verdicts are right. The reports are model output, and the trace is there so a person can check them.
Keeping a small local model usable.
- Local models wrap or mangle their JSON. The loop strips tool-call tokens and code fences before parsing, and asks again when parsing still fails. Each failed turn counts toward the ten.
- An unknown tool name gets a reply listing the valid tools. A tool that crashes becomes an error result the model can read, and the loop carries on.
- EMBER has no fallback. If the model file or package is missing, the tool returns an error rather than a made-up score. Getting the old EMBER package to run on current libraries took small shims for NumPy, LIEF and scikit-learn.
- The VirusTotal gate exists because one or two detections on a clean file are common.
What the analyst sees.
The dashboard is a React app reading the event stream. Steps appear as they finish, so a slow threat-feed call is visible as a slow step rather than a frozen page; if nothing arrives for a minute, the server sends a heartbeat.
When the report arrives it shows the verdict and confidence, the model's summary, ATT&CK tags, indicators you can copy with a click, and the byte image.
The model never sees that image. It is there for the analyst, who can spot packed or encrypted regions in it at a glance.


Limits.
- Static only. Nothing runs the sample and there is no sandbox. Parsing an untrusted file can still carry risk, so the backend belongs in a container or a virtual machine.
- No accuracy numbers. I have not built a labelled test set, so the verdicts are unmeasured.
- It runs locally. The dashboard talks to a backend on
localhost, and the threat feeds need API keys; without them a lookup reports "skipped". - Only
.exeand.dllfiles, up to 10 MB.
