Skip to content

Latest commit

 

History

6 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

example

Content and structure inspection for files. File in, verdict out.

What it does

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
PDF 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
Email 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).

Requirements

  • Python 3.11 or newer
  • libmagic on the system (Arch: pacman -S file) – optional, only improves the plain-text type description
  • fzf – optional, otherwise the built-in picker is used
  • QR detection – optional: zbar on the system (macOS: brew install zbar, Arch: pacman -S zbar) plus pip install pyzbar pillow
  • Fuzzy hashes (--fuzzy) – optional: pip install ppdeep py-tlsh

Setup

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 dialog

On 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 --help

To invoke it from anywhere:

ln -s "$PWD/filetriage.sh" ~/.local/bin/filetriage

Usage

./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 scripts

Without -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.

Important flags

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 (infocritical)
--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

Commands in the interface

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

Watch mode

./start.sh --watch ~/Downloads --watch-notify
./start.sh --watch ~/Downloads -o triage-log.jsonl

Watches 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.

IOCs

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.

Hash allowlist

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.

Plugins

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(...) children

Registering 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.

Reputation lookups (your own key)

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.

Setting up a key

Free to obtain at virustotal.com (Profile → API Key) and auth.abuse.ch for MalwareBazaar. filetriage looks in this order:

  1. CLI parameter --vt-key / --mb-key
  2. Environment variable FILETRIAGE_VT_KEY / FILETRIAGE_MB_KEY
  3. 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.

Usage

./start.sh -p suspicious.doc --reputation          # look up after the scan
./start.sh -p suspicious.doc --reputation --reputation-max 3

It 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.

Limits

  • 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-cache bypasses 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.

Exit codes

Value Meaning
0 clean or only notable
2 suspicious
3 malicious
1 error (file not readable, invalid rules, …)

Security notes

  • 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 (--reputation or the v command), 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. CLEAN means "nothing stood out here", not "guaranteed harmless".

Release notes

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).

Credits

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.

License

MIT

About

Recursive file triage tool that inspects content and structure (PDF, Office, PE/ELF, archives, email) for suspicious contradictions

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages