A static document triage tool: it inspects a received file (PDF, Word, Excel, PowerPoint, RTF, archives…) and tells you whether it's reasonable to open it — without ever opening it or executing anything.
Built for machines without antivirus, typically on Linux, where the dubious attachment still shows up anyway.
python3 static_doc_analyzer.py <path_to_document>None. The script only uses the Python standard library and calls no external program.
- Python 3.8 or newer (validated on 3.12)
- No
pip install, no third-party binary, no network access - A single file, copyable as-is onto any machine
# Analyze a document, human-readable report
python3 static_doc_analyzer.py invoice.pdf
# Several files at once, one line per file
python3 static_doc_analyzer.py --quiet ~/Downloads/*
# Machine-readable output, for a script or a SOAR
python3 static_doc_analyzer.py --json document.docx
# Cap analysis time on a large file
python3 static_doc_analyzer.py --timeout 20 large_report.pdf| Option | Effect |
|---|---|
--json |
Structured JSON output (single object, or list if multiple files) |
--quiet |
One line per file: verdict, score, name |
--timeout N |
Time budget per file, in seconds (default: 90) |
--no-color |
Disable color (automatic if output isn't a terminal) |
The report has five sections:
- File identity — size, entropy, actual type inferred from the bytes (not the extension), SHA-256 and MD5 for your pivots and threat-intel lookups.
- Verdict and score — see below.
- Indicators — everything found, grouped by category, each with: its weight, its precise location in the file (
obj 12,word/vbaProject.bin#module1…), the detection context and the excerpt that triggered the rule. Nothing is masked or truncated. - Aggravating correlations — combinations of indicators that, together, characterize an attack chain.
- Extracted IOCs — URLs, IP addresses and UNC paths found in the document, defanged (
hxxp[://]…) so they can be safely copied into a ticket or a search engine.
| Score | Verdict | Return code | What it means |
|---|---|---|---|
| ≥ 85 | MALICIOUS | 3 | Do not open. Isolate the file and treat as an incident. |
| 45 – 84 | SUSPICIOUS | 2 | Do not open outside a sandbox. Detonation or dynamic analysis required. |
| 18 – 44 | REVIEW NEEDED | 1 | Unusual element. Open only in a controlled environment. |
| < 18 | CLEAN | 0 | No active code, macro, embedded object or remote link. The document can be opened. |
| — | ERROR | 4 | Empty, unreadable, too large, or analysis impossible. |
The return codes let you chain the tool into a script:
if python3 static_doc_analyzer.py --quiet "$f"; then
xdg-open "$f"
else
echo "Do not open: $f"
fiAn incomplete analysis is never CLEAN. If a limit is hit (time budget exhausted, truncated archive, file over 64 MB), the verdict is raised to at least "Review needed" and the report states this explicitly.
The score is a risk accumulator, not a probability. It has no absolute meaning: it's calibrated so the thresholds above land in the right place.
| Example indicator | Weight |
|---|---|
Remote template attachedTemplate (remote template injection) |
70 |
Embedded PE/ELF executable, ms-msdt: handler, Equation Editor object |
46 – 50 |
/Launch action in a PDF, VSS shadow copy deletion, AMSI bypass |
45 – 46 |
Excel 4.0 macro (XLM), regsvr32 squiblydoo |
38 – 40 |
.DownloadString(, powershell -enc, certutil -urlcache |
28 – 34 |
Automatic entry point AutoOpen / Document_Open |
26 |
| Presence of a VBA project | 18 |
| Unreadable compressed stream, encrypted PDF | 6 – 8 |
| External hyperlink | 0 (informational) |
This is the core mechanism of the tool. The same string doesn't carry the same weight depending on where it's found:
| Context | Factor |
|---|---|
| Decompressed VBA macro, PDF JavaScript, decoded stream, decrypted base64 | × 1.0 |
| Strings extracted from a binary | × 0.6 |
| XML attributes, container structure | × 0.45 |
| Visible document text | × 0.25 |
+20% per distinct occurrence, capped at 1.6 times the base weight. Ten WScript.Shell occurrences don't carry ten times the risk of a single one.
Its maximum weight across all locations is kept, then distinct indicators are summed.
6 points maximum per rule, 15 points maximum in total, and it never triggers correlations. Direct consequence: an incident report, an internal procedure or training material that quotes powershell -enc, certutil -urlcache and WScript.Shell stays CLEAN. These are the documents you receive most often; they shouldn't trigger an alert.
+14 to +28 points when several behaviors combine: automatic execution + download, macro + embedded executable, PDF JavaScript + known exploitation primitive… It's the combination that makes a maldoc, not the isolated element.
Calibration in one sentence: a single decisive indicator should be enough to condemn (remote template at 70, plus the download/execution correlation at +16, gives 86 → MALICIOUS), while a single mild indicator never goes past "Review needed" (macro present = 18).
The tool identifies the format from the header bytes, never from the extension. A .pdf that is actually an executable gets flagged as such.
PDF — full decoding of filter chains (FlateDecode, ASCIIHexDecode, ASCII85Decode, LZWDecode, RunLengthDecode, PNG predictors), unfolding of compressed object streams /ObjStm (where modern PDF JavaScript hides), detection of hex-obfuscated names (/J#61vaScript), /OpenAction, /AA, /Launch, /JavaScript, /EmbeddedFile, /XFA, /RichMedia triggers, and JavaScript APIs associated with known CVEs.
OOXML (docx, xlsx, pptx, docm, xlsm…) — actual decompression of VBA macros in MS-OVBA format, which a simple string extraction can't do; analysis of external relationships, distinguishing a remote attachedTemplate or oleObject (attack) from a plain hyperlink (benign); DDE/DDEAUTO fields; Excel 4.0 (XLM) macros and _xlnm.Auto_Open; mismatch between extension and the presence of a macro; reassembly of <w:t> runs to reconstruct payloads deliberately fragmented across them.
OLE / legacy formats (doc, xls, ppt) — VBA modules, Equation Editor object (CVE-2017-11882 vector), Ole10Native packages embedding an executable.
RTF — \objdata, \objupdate, structure obfuscation, hex decoding of several successive embedded objects.
Secondary formats frequently abused in phishing — Windows shortcuts (.lnk, target and command line treated as executed code, not as a plain binary); OneNote (.one, a common vector for hiding a .hta/.vbs behind a clickable image); Excel web queries (.iqy) and SYLK files (.slk), two formats that bypass Office's macro warning.
Scripts received directly — a .vbs, .ps1, .js, .sh, .bat… sent as-is (outside a document/archive) is analyzed as executed code, not as text: its visible content IS the payload, with no intermediate rendering layer to discount.
Archives — dangerous extensions (Windows and Linux/macOS: .exe… as much as .sh, .run, .desktop, .AppImage), misleading double extensions, Unicode RTLO character masking the real extension, zip slip, decompression bombs, nested documents and scripts (analyzed recursively up to 3 levels deep).
Cross-cutting — embedded PE/ELF executables, decoded base64 blobs re-analyzed, IOC extraction (URLs, IPs, UNC/SMB paths for NTLM credential leakage).
- Analyzed content is never executed, nor extracted to disk.
- All data originating from the file is neutralized before display: a document can't inject ANSI escape sequences into your terminal to falsify the report.
- Anti-denial-of-service protections: global time budget, size caps, decompression caps, entry-count caps and recursion-depth caps.
- No network access, so no information leakage about the analyzed document.
- This is static analysis. A document whose malicious behavior relies entirely on a remote resource fetched at open time can slip through.
- A CLEAN verdict is not a guarantee of safety, it's the absence of a known indicator. The usual caution about the sender and context still applies.
- Encrypted or password-protected documents are partially opaque: this is flagged in the report.
- A legitimate document containing a macro will come out as "Review needed". This is intentional: a macro always warrants a look.
Everything is configured at the top of the file, without touching the logic:
| Element | Role |
|---|---|
STRUCT |
Weights of structural indicators |
R(...) calls |
Pattern rules: identifier, category, weight, expression, tags |
COMBOS |
Correlations and their bonuses |
CTX_FACTOR |
Per-context weighting factors |
TEXT_RULE_CAP / TEXT_TOTAL_CAP |
Caps applied to visible text |
VERDICTS |
Thresholds and messages |
BENIGN_URI_RE |
Domains to ignore (XML schemas, namespaces) |
Two scripts ship alongside the tool to validate your changes:
python3 make_corpus.py # generates a test corpus in ./corpus
python3 test_docscan.py # replays the full validation suiteThe suite covers edge cases, display safety, return codes, anti-DoS protections, fuzzing on mutated files and verdict non-regression. Run it after every new signature you add: it's what will keep you from reintroducing a false positive.