All work

Case study · Malware triage

MAAT

A command-line tool that triages suspicious Office documents, Windows executables and PDFs without ever running them, and explains every point of the risk score it assigns.

Role
Solo portfolio project, built with Claude Code
Built
April 2026
Stack
Python · oletools · pefile · pikepdf · YARA · VirusTotal API · Jinja2
Size
About 700 lines of Python, 12 tests

Why I built it

Triaging one suspicious attachment by hand means a handful of tools: something to identify the real file type, a hash, a VirusTotal search, olevba for macros, pefile for executables, a PDF parser for embedded scripts, and then a write-up of what you found. Each step is simple. Doing all of them the same way every time, and recording the result, is where it goes wrong.

I built MAAT as a portfolio project to put that whole routine behind one command, with two constraints I cared about: it had to be safe to run on an ordinary workstation, and its score had to explain itself. A number an analyst cannot question is a number they will either ignore or trust too much.

How it works

One command, python maat.py --file suspicious.docm, runs seven stages in order and writes a report.

  1. 01IdentifyReads the file's contents with libmagic, not its extension, and routes Office (OLE and OOXML), Windows PE and PDF. Anything else stops with exit code 2.
  2. 02HashMD5, SHA-1 and SHA-256 computed in one streamed pass, 64 KB at a time.
  3. 03ReputationLooks up the SHA-256 on VirusTotal. Only the hash leaves the machine. Answers are cached for 7 days, and --no-vt skips the lookup entirely.
  4. 04AnalyzeOne analyzer per file type: macro extraction and keyword scanning for Office, section entropy and imports for PE, and a walk of the object tree for PDF.
  5. 05MatchRuns every YARA rule in rules/ against the file.
  6. 06ScoreAdds up points from each signal into a 0 to 10 score, with one written reason per point.
  7. 07ReportRenders a self-contained HTML report, plus the full result as JSON with --json.

Decisions that shaped it

Never run the sample

Everything MAAT does is static. It parses bytes and never executes, opens or renders the file, so it is safe to run on an analyst's workstation without a sandbox. The tradeoff is that it cannot see what a sample does at runtime, which is why its output is a triage score, not a verdict.

Look up hashes, never upload files

Uploading a file to VirusTotal shares it with every VirusTotal subscriber. That can tip off an attacker who is watching for their sample, or leak a confidential document that turns out to be harmless. A hash lookup reveals nothing new, so MAAT only ever sends the SHA-256. There is no upload code at all.

A score you can argue with

The score is a sum of simple rules, and every rule that fires writes its own line, such as “VBA macros present (+3)”. An analyst who disagrees with a result can see exactly which rule caused it. Caps keep one noisy signal from dominating: YARA can add at most 4 points, and the total stops at 10.

Degrade instead of crashing

Malware is often malformed on purpose. Every analyzer collects errors instead of raising them, and a missing API key or YARA library shows up in the report as “unavailable” rather than stopping the run. The PDF walk is capped at a depth of 12 so a crafted object tree cannot recurse forever.

A report that is safe to open

The HTML report is one file with inline styles, no scripts and no external resources, so it can be attached to a ticket and opened anywhere. Macro source and PDF strings are attacker-controlled, so the template escapes everything it renders.

How the score adds up

SignalPoints
Office: VBA macros present+3
Office: suspicious keyword in the macro source+2
PE: a section with entropy above 7.0 (likely packed)+2
PDF: one or more suspicious objects+2
YARA: each rule match+2, max +4
VirusTotal: 1 to 5 engines flag the hash+1
VirusTotal: more than 5 engines flag the hash+3

The total is capped at 10. 0 to 3 is Low, 4 to 6 is Medium and 7 to 10 is High.

Example: a PDF that runs JavaScript

jsdemo.pdf is a 609-byte synthetic test file whose /OpenAction runs JavaScript when the PDF opens. MAAT found both the /OpenAction and the /JS entry, VirusTotal had never seen the hash, and the file scored 2/10, Low.

MAAT report for jsdemo.pdf

That Low is too low. A PDF that runs code the moment it opens deserves a closer look than a PDF with one embedded link, but both get the same flat +2. Writing this case study is how I noticed, and it is first on the list below.

Testing

Twelve pytest tests run fully offline. VirusTotal is replaced by a fake session, and the test documents are generated at runtime, so no real malware lives in the repository. They cover hashing, file type detection, the Office and PDF analyzers, the three VirusTotal outcomes (no key, not found, found), the YARA fallback, and the scoring bounds. There are no PE analyzer tests yet, and no test that runs the whole command end to end.

Limits

  • YARA is untested on my machine. yara-python has no prebuilt wheel for my Windows Python setup, so every report I have generated says YARA was unavailable. The code path is covered by a fallback test, not by a real scan.
  • PDF findings are scored flat. One embedded link and a PDF that runs JavaScript on open both get +2.
  • PE files top out at 9. Imports are collected but not scored, so a binary that imports process-injection APIs scores the same as one that does not.
  • No size limit. YARA reads the whole file into memory.
  • One file per run, no VirusTotal rate limiting, and only four starter YARA rules.

What I'd improve

  1. Weight PDF findings by kind, so JavaScript that runs on open lands in Medium on its own.
  2. Score risky PE import combinations such as VirtualAlloc, WriteProcessMemory and CreateRemoteThread.
  3. Run YARA for real (WSL or a Linux container), grow the rule set, and add a test fixture per rule.
  4. Add a file size limit and scan large files in chunks.
  5. Add a directory mode with VirusTotal rate limiting for batches.
  6. Add PE analyzer tests and an end-to-end test that renders a report.

How I built it

I built MAAT with Claude Code. I split the work into twelve phases before writing anything: scaffold, hashing, one phase per analyzer, VirusTotal, YARA, scoring, the HTML report, JSON output, tests and the README. Each phase landed as its own commit, so the history reads in the same order as the pipeline.

Working with an AI assistant moves the effort from typing code to deciding what it should do and checking that it does it, which means reading the result closely. Going back through the code for this write-up is how I found the flat PDF score above, and that the README still listed two libraries MAAT never used, which I have since fixed.