Content and structure inspection for files. File in, verdict out.
FileTriage recursively unpacks a file and checks each piece of content for
whether it belongs there. A PDF doesn't need JavaScript, a vacation photo
doesn't need an EXE, a .docx doesn't need macros. The tool surfaces exactly
these contradictions and sums them up into a verdict: CLEAN, NOTABLE,
SUSPICIOUS, or MALICIOUS.
Among the things checked:
| Area | What is found |
|---|---|
JavaScript, /OpenAction, /AA, /Launch, embedded files, /ObjStm, obfuscated names (/J#53), data after %%EOF |
|
| Office (OOXML) | VBA macros, Excel 4.0 macros, ActiveX, external templates (template injection), macros under a macro-free extension |
| Office (OLE) | VBA source including olevba analysis, VBA stomping, embedded OLE packages, encrypted documents |
| RTF | embedded objects, \objupdate, Equation Editor objects, control-word obfuscation |
| attachments, mismatched reply address, failed SPF/DMARC checks | |
| Executables (PE / ELF) | suspicious API imports, packed sections, writable+executable sections, TLS callbacks, appended data (overlay), resources |
| Executables (Mach-O) | FAT/universal slices, missing code signature, libraries from writable paths, RPATH hijacking, W+X segments, packed __TEXT, suspicious API references, appended data |
| Images | QR codes (quishing) - the decoded content is checked like any other item |
| Archives | path traversal (Zip Slip), double extensions, encrypted entries, decompression bombs, data after the end of the archive |
| Every item | type mismatch between name and content, embedded files in the byte stream (carving), Base64 payloads, entropy, ~50 content rules (JavaScript, PowerShell, VBA, LOLBins, network, obfuscation) |
On macOS, the quarantine attribute of the scanned file (where it was downloaded from) is recorded as well.
Optional YARA rules can be added, and — with your own API key — the found hashes can be looked up online (see Reputation lookups).
- Python 3.11 or newer
libmagicon the system (Arch:pacman -S file) – optional, only improves the plain-text type descriptionfzf– optional, otherwise the built-in picker is used- QR detection – optional:
zbaron the system (macOS:brew install zbar, Arch:pacman -S zbar) pluspip install pyzbar pillow - Fuzzy hashes (
--fuzzy) – optional:pip install ppdeep py-tlsh
git clone https://github.com/Paspke/filetriage
cd filetriage
./start.sh --help # creates .venv and installs everything on first run
./start.sh --setup # (re)open the optional-dependency dialogOn first run (in a terminal) the script also offers to install everything
optional: QR code detection (pyzbar + Pillow), fuzzy hashing (ppdeep +
py-tlsh), and — via Homebrew/pacman — the zbar, libmagic, and fzf system
packages. ./start.sh --setup brings the dialog back at any time.
Manual setup also works:
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
.venv/bin/python -m core --helpTo invoke it from anywhere:
ln -s "$PWD/filetriage.sh" ~/.local/bin/filetriage./start.sh # select a file via the picker
./start.sh -p invoice.pdf # check a single file
./start.sh -p ~/Downloads # check an entire directory
./start.sh -p file.docx -o report.html # also write a report
./start.sh -p file.bin -o findings.json --no-interactive # for scriptsWithout -p, the file picker appears: fzf if available, otherwise a
built-in fuzzy finder (enter a search term, a number selects, a path is used
directly). Use --root to set the starting directory, --no-fzf to force
the built-in picker.
Without -o, the results stay in the interface.
Directories are scanned with multiple worker processes in parallel (one
process per file, automatic worker count). Use --jobs N to control the
number of processes, --jobs 1 to force the old sequential behavior.
| Flag | Effect |
|---|---|
-p, --path |
File or directory |
-o, --output |
Write a report, format by extension: .json, .html, .md, .txt, .sarif |
-j, --jobs |
Parallel scan processes for directories. Default: automatic (number of cores, capped at 8). --jobs 1 scans sequentially |
-y, --yara |
Also apply a YARA rule file or directory |
--watch DIR |
Watch a directory and check new files as they arrive (see Watch mode) |
--watch-notify |
Desktop notification for suspicious/malicious files in watch mode |
--iocs FILE |
Write extracted IOCs (URLs, domains, IPs, emails) to .json or .csv |
--allowlist FILE |
Extra file with known-good SHA-256 hashes; matches skip analysis |
--fuzzy |
Compute ssdeep/TLSH similarity hashes per item (optional libraries) |
--no-iocs |
Disable IOC extraction |
--no-plugins |
Don't load plugins from ~/.config/filetriage/handlers/ |
--min-severity |
Only show findings at or above this level (info…critical) |
--no-interactive |
Print only, no interactive prompt |
--max-depth, --max-nodes, --max-size |
Limits against bombs and infinite nesting |
--no-carve, --no-base64 |
Disable byte-stream search or Base64 decoding |
--dump-dir |
Target directory for the x command |
--quiet, --no-color |
Leaner output |
| Input | Effect |
|---|---|
<number> |
Finding in detail, with a hexdump around the location |
f / f high |
Findings list, optionally from a given severity |
a |
All findings, without truncation |
/text |
Search findings |
t |
Structure tree |
i |
Extracted IOCs, defanged (i iocs.csv writes them to a file) |
k n3 |
Item in detail (metadata, preview, strings) |
x n3 [path] |
Write an item to disk |
w report.html |
Write a report |
o |
Open a different file |
h / q |
Help / quit |
./start.sh --watch ~/Downloads --watch-notify
./start.sh --watch ~/Downloads -o triage-log.jsonlWatches a directory and triages every new file as soon as it has finished
writing (browser partial files like .crdownload/.download are ignored,
size must be stable across two polls). One line per file with verdict and
top finding; --watch-notify additionally raises a desktop notification for
SUSPICIOUS/MALICIOUS. With -o file, one JSON line per checked file is
appended (findings + IOCs included) - suitable for long-running logs.
Existing files at start are not scanned. Stop with Ctrl-C.
Every scan extracts URLs, domains, IP addresses, and email addresses from
all unpacked items (--no-iocs disables this). Well-known infrastructure
domains (XML namespaces, certificate authorities, …) are filtered out.
Everything is displayed defanged (hxxps://evil[.]example) so nothing
can be clicked by accident; reports include the IOC section, JSON reports
carry both raw and defanged values. --iocs findings.csv (or .json)
writes them separately; in the interface, i shows them.
Known-good files can be skipped: put SHA-256 hashes (one per line, #
comments allowed) into ~/.config/filetriage/allowlist.txt - it is read
automatically - or pass an extra file via --allowlist FILE. Matching
items are marked as allowlisted, skip content analysis, and count as clean.
NSRL-style export lines work as long as the line starts with the hash.
Custom handlers live in ~/.config/filetriage/handlers/*.py and use the
same registration API as the built-in handlers:
# ~/.config/filetriage/handlers/my_format.py
from core.handlers import Extracted, finding, register
from core.model import Severity
@register("sqlite") # kind from the type detection
def handle_sqlite(node, ctx):
if b"suspicious_marker" in node.data[:4096]:
node.add_finding(finding(
Severity.HIGH, "structure", "Marker found", rule="my.marker"))
return [] # or yield Extracted(...) childrenRegistering an existing kind replaces the built-in handler. Plugins are
loaded in every scan process (including parallel workers); --no-plugins
disables them. Plugins are executed as code - only put files you trust
there.
By default, filetriage works purely locally. On explicit request it can look up the SHA-256 hashes of the checked items at VirusTotal and MalwareBazaar. Access runs through your own, free API key (bring your own key) – the project ships no keys.
Only the hash ever leaves the machine, never file content. A hash is a one-way checksum; the file cannot be reconstructed from it. There is deliberately no upload feature.
Free to obtain at virustotal.com (Profile → API Key) and auth.abuse.ch for MalwareBazaar. filetriage looks in this order:
- CLI parameter
--vt-key/--mb-key - Environment variable
FILETRIAGE_VT_KEY/FILETRIAGE_MB_KEY - File
~/.config/filetriage/config.toml
[keys]
virustotal = "your-key"
malwarebazaar = "your-auth-key"One key is enough – the other source is then skipped. Without any key, everything else continues to work unchanged.
./start.sh -p suspicious.doc --reputation # look up after the scan
./start.sh -p suspicious.doc --reputation --reputation-max 3It also works on demand in the interface, without a flag:
| Input | Effect |
|---|---|
v |
Look up all important items |
v n3 |
Look up only this one item |
The results become regular findings (category Reputation), factor into the overall verdict, and appear in every report.
- The free VirusTotal tier allows 4 requests/minute. filetriage throttles
itself and therefore looks up at most 10 items per file by default
(
--reputation-max). - Responses are cached in
~/.cache/filetriage/(hits for 7 days, "unknown" for 1 day), so a second run doesn't need to wait.--no-reputation-cachebypasses the cache. - "Unknown" is not an acquittal. Targeted malware is often not recorded
there. Conversely, one or two hits can be a false positive – filetriage
therefore rates such cases only as
MEDIUM.
| Value | Meaning |
|---|---|
0 |
clean or only notable |
2 |
suspicious |
3 |
malicious |
1 |
error (file not readable, invalid rules, …) |
- Items written out (
x) are unmodified. Do not run them, do not double-click them – best viewed in an isolated environment. - filetriage works purely locally by default. Without the explicitly
requested reputation lookup (
--reputationor thevcommand), nothing leaves the machine. When enabled, only the SHA-256 hash is sent to the services you configured – never file content. - Nothing is executed; unpacking happens only in memory.
- API keys are neither logged nor written into reports.
- A verdict is an assessment, not proof.
CLEANmeans "nothing stood out here", not "guaranteed harmless".
v1.1.0 — parallel directory scans (--jobs), Mach-O
analysis, watch mode (--watch), IOC extraction, SARIF reports, hash
allowlist, QR code detection, fuzzy hashes (--fuzzy), a plugin API, and
an interactive setup dialog (./start.sh --setup).
The PDF structure analysis builds on
pdf-parser.py by
Didier Stevens (public domain). Macro and OLE analysis use
oletools, the PE analysis uses
pefile.