Continuous local file analysis for Windows. Watches the folders where untrusted files land, runs every new file through a chain of analyzers, and quarantines what comes back malicious.
What it is: an aggressive multi-engine analysis layer. When a file appears in a watched folder, Customs waits for the write to finish and then runs it through eleven analyzers covering provenance, reputation, static structure, embedded scripts, document macros, archive contents, YARA patterns, and optionally live detonation in a disposable VM. Verdicts land in seconds; you get a toast and the file is vaulted before you would realistically double-click it.
What it is not: a replacement for Microsoft Defender.
This is the important limitation and it is worth being precise about. Customs runs in userland, which means it inspects files after they are written to disk, not before they execute. Genuinely blocking a process at launch requires a kernel minifilter driver — signed, loaded into the kernel, hooked into the I/O path. That is exactly what Defender already does, and it is not something this project can replicate.
So the honest framing is: Defender is the gate, Customs is the inspector. Defender stops what it recognises. Customs catches the categories Defender's signatures routinely miss — fresh droppers with no signature yet, malicious LNK shortcuts, macro-laden documents, remote-template injection, ISO/archive smuggling, obfuscated PowerShell — and it explains why in detail, which Defender does not. Keep Defender enabled. They are complementary.
Validated against a real, everyday Downloads folder (748 files, recursive), with
--no-action so nothing was quarantined during testing:
| Verdict | Count |
|---|---|
| CLEAN | 555 |
| LOW | 169 |
| SUSPICIOUS | 21 |
| MALICIOUS | 3 → 2 after the final tuning pass |
The remaining MALICIOUS verdicts are genuine: an archive and the executable inside it, which Microsoft Defender independently identifies as a potentially unwanted application (PUA).
Runtime cost, measured over 15 minutes of the installed service:
- ~50 MB resident memory
- 246 ms to scan a small text file end-to-end through all 11 analyzers
%TEMP%accounted for 6 scans (1% of activity, 1.4s CPU) — watching it is cheap, contrary to what you might expect
How that number was reached matters. The first working version flagged 743 of 749 files as malicious because the Defender check matched Defender's own "found no threats." message. Every subsequent false-positive class was found the same way — by running against real files, not by reading the code:
- signed installers from two major software vendors scoring 92–100 (fixed: the VM-evasion
YARA rule no longer pairs the C runtime's
IsDebuggerPresentwith a single incidentalQEMUstring, and heuristics alone can no longer reach MALICIOUS for any file — see Quarantine requires corroboration) - duplicate copies of one file ratcheting 43 → 63 → 63 as Customs' own cached verdicts fed back into scoring (fixed: the cache no longer contributes score)
- a signed developer-toolchain installer at 100, because the YARA string
"-enc"matches-encoding - minified JavaScript library bundles at 74, because
re.DOTALLlet one pattern match tokens 40,000 characters apart https://example.commatching "downloadable executable payload", because.comis both an executable extension and a top-level domain
Treat the current false-positive rate as acceptable to start, not solved.
rules/allowlist.txt exists for what still slips through.
Requires Windows 10/11 and Python 3.9 or newer. Run everything from the
repository root (there is no package install; python -m customs resolves
from the current directory):
python -m pip install -r requirements.txtOptional: copy config.example.json to config.json to change settings
(without it, built-in defaults are used); install 7-Zip in its default location
so the archive analyzer can list RAR/7z/ISO contents; VirtualBox 7 for the
detonation sandbox (see below). install.ps1 uses the first python on PATH,
so install the requirements into that interpreter.
Then verify the pipeline:
python -m customs testVerifies the pipeline end-to-end using the EICAR test string (a harmless industry-standard string that every scanner is built to detect).
python -m customs watchRuns the watcher in the foreground. Ctrl+C stops it.
To install it as a background service that starts at every logon:
powershell -ExecutionPolicy Bypass -File install.ps1 -InstallNo administrator rights are needed — it registers a per-user Scheduled Task which runs in your own session, which is precisely the privilege level required to watch your own folders.
The file analyzers answer "is this file dangerous?". The runtime guard answers the question that actually matters for anything packed: "what is this program doing now that it is running?"
This is the answer to a hidden launcher or a payload that only appears at runtime. Static analysis cannot see inside an encrypted section — but the moment that code runs it has to do something observable.
It watches for:
- interpreters spawned with payload-shaped arguments —
powershell -enc,-w hidden,certutil -decode,regsvr32 ... scrobj.dll - new autostart entries — Run / RunOnce registry keys
- files dropped into the Startup folder
- newly installed Windows services
- attempts to disable Defender
- (focused mode) executables running from
%TEMP%, and any shell spawned by an unexpected parent
Always on with the watcher. For something you specifically don't trust, run the focused mode and launch it:
python -m customs guard --app SomeApp.exe SomeAppLauncher.exeReview anything it recorded:
python -m customs events --importantThe background daemon runs quiet: only high-confidence events (payload-shaped
command lines, autostart writes, Startup drops, service installs, AV tampering).
guard --app runs verbose, adding the broader rules.
That split is deliberate. In verbose mode the broader rules fire constantly on
ordinary desktop use — a terminal spawning a shell, an app installed under
AppData, conhost.exe starting alongside every console program. Running that
noise 24/7 would bury the one alert that matters. Verbose is for when you are
deliberately watching one program.
Process state is sampled roughly three times a second, not event-driven. A
process that starts and exits between samples can be missed. Catching every
process start regardless of lifetime requires Win32_ProcessStartTrace, which
returns access denied without elevation — verified, not assumed.
Two things soften it, and one does not:
- Persistence detection is unaffected. Registry keys, Startup files and services are durable state — the guard sees the result, whatever the process's lifetime was.
- Dropped payloads are still caught by the file watcher, which by default
monitors
%TEMP%, Downloads, Desktop and Documents and scans anything written there. Other folders (for example%APPDATA%) can be added towatch_paths. - But a payload that runs entirely in memory, does its work in under a second, and leaves no persistence, can execute unseen. That is a real gap and it is not closeable from userland.
| Command | What it does |
|---|---|
python -m customs watch |
Run the 24/7 watcher |
python -m customs scan <path> |
Scan a file or folder now |
python -m customs scan <path> --detailed --no-action |
Full signal breakdown, never quarantines |
python -m customs status |
Current state, counts, whether the daemon is alive |
python -m customs history -n 40 |
Recent scans |
python -m customs show <id> |
Every signal for one scan |
python -m customs quarantine list |
What is in the vault |
python -m customs quarantine restore --id <n> |
Put a file back |
python -m customs quarantine purge --id <n> |
Delete a vaulted file permanently |
python -m customs config |
Print current configuration |
python -m customs config --set key=value |
Change a setting |
python -m customs guard --app <exe> |
Watch live what a program does while it runs |
python -m customs events --important |
Recorded runtime behaviour events |
python -m customs test |
EICAR self-test |
Service control: install.ps1 -Install | -Uninstall | -Start | -Stop | -Status
Dashboard: http://127.0.0.1:8787 — loopback-only and read-only.
Each analyzer emits signals. A signal has a score contribution, a severity, and a plain-English explanation. The verdict engine then:
- Sums signals per analyzer with diminishing returns (first counts 100%, second 70%, third 50%, rest 30%). One chatty analyzer cannot condemn a file by itself.
- Applies exonerating signals, which are negative — a valid Authenticode signature is −25, a long-clean VirusTotal record is −20, an allowlist hit is −100.
- Checks for decisive signals that override the arithmetic entirely: a
Defender detection, 5+ VirusTotal engines, a
decisiveYARA rule, or confirmed malicious behaviour in the sandbox.
Thresholds (configurable):
| Score | Verdict |
|---|---|
| 0–14 | CLEAN |
| 15–39 | LOW |
| 40–69 | SUSPICIOUS |
| 70+ | MALICIOUS |
Heuristics alone never delete a file. MALICIOUS — the verdict that removes something from disk — requires one of:
- a named detection from Microsoft Defender
- a detection by 5 or more VirusTotal engines
- a YARA rule marked
decisive - a hash listed in
rules/blocklist.txt(or the EICAR test file) - during detonation: shadow-copy deletion or disabling Defender, or two or more other high-severity behaviours actually observed
Heuristic accumulation on its own tops out at SUSPICIOUS, which still raises a toast and, if the detonation sandbox is set up (it is off by default), still triggers detonation.
This rule exists because of a real failure. Before it, a 331 MB game-engine application and two 22 MB debug builds were quarantined out of their own project directories. They imported process-injection, privilege, anti-debug and keyboard APIs, and contained the strings "screenshot" and "webcam" — all true, all meaningless. A large statically-linked binary bundles dozens of libraries, so it imports nearly everything; those same imports in a 40 KB dropper mean something entirely different. No amount of per-rule tuning fixes that, because the signals genuinely are present.
Detection strength is not reduced. A novel sample no engine recognises still scores SUSPICIOUS and still alerts. With the sandbox enabled it is also detonated, and because the sandbox verdict is decisive, it comes back MALICIOUS and is quarantined on the evidence of what it actually did when run. Without the sandbox it is alerted on and left in place for you to judge.
Default response mode is quarantine_malicious: MALICIOUS files are vaulted,
SUSPICIOUS files only alert. Change with:
python -m customs config --set response_mode=alert_onlyValid values: quarantine_malicious, alert_only, quarantine_suspicious.
| Analyzer | What it looks for |
|---|---|
metadata |
Mark-of-the-Web provenance and origin URL, double extensions, right-to-left override tricks, extension/magic-byte mismatch, hidden alternate data streams |
hashes |
Local allow/blocklists (MD5/SHA-1/SHA-256) and the EICAR test hash |
defender |
Microsoft Defender on-demand scan, with the real threat name |
authenticode |
Signature validity. A broken signature scores +50 — no legitimate build modifies a binary after signing |
pe |
Section entropy, packer signatures, writable+executable sections, import-table analysis grouped by capability (injection, credential access, keylogging), TLS callbacks, embedded executables in resources |
scripts |
~50 patterns across PowerShell/batch/VBS/JS/HTA, plus full LNK shortcut parsing to recover the embedded command line |
office |
VBA macros with auto-exec triggers, remote template injection, DDE, embedded executables, PDF /Launch and JavaScript |
archive |
Encrypted archives (unscannable by any engine), LNK files inside archives, double extensions in entries, ISO/IMG containers that strip Mark-of-the-Web |
yara |
The ruleset in rules/*.yar |
virustotal |
Reputation across 70+ engines |
sandbox |
Live detonation in a disposable VM |
Currently: hash-lookup only. Customs sends the SHA-256 digest and nothing else. A hash is a one-way fingerprint — it cannot be reversed into the file and reveals nothing about its contents. Your files do not leave this machine.
To enable it, get a free key from virustotal.com (account → API key) then:
python -m customs config --set vt_api_key=YOUR_KEY_HEREThe free tier allows 4 lookups/minute, which the rate limiter respects, and
results are cached for 30 days so re-downloads cost nothing. To save quota, only
executables downloaded from the internet, or files that already scored at least
vt_min_score (default 20) locally, are looked up.
There is a second mode, vt_upload_unknown, which uploads the actual file when
its hash is unknown to VirusTotal. It meaningfully improves detection of
brand-new malware. It is off by default and you should think carefully before
turning it on: uploaded samples are shared with VirusTotal's security-industry
subscribers and should be considered public. Never enable it on a machine that
handles personal, financial, or work-confidential documents.
Windows Sandbox and Hyper-V exist only on the Pro and Enterprise editions, so detonation runs on VirtualBox, which works on every edition including Home.
Tested with VirtualBox 7.2 (Guest Additions 7.2.2) and a Windows 11
Enterprise Evaluation guest with a clean snapshot. A detonation smoke test
correctly observed both an autostart registry write and a spawned cmd
process, then reverted. One full cycle takes about 3.5 minutes. The sandbox
stage stays off until you build the VM (below) and enable it.
build_sandbox_vm.ps1 builds the VM automatically: unattended Windows install
from the evaluation ISO, Guest Additions, then a clean snapshot. Prerequisites:
VirtualBox 7.0 or newer installed in its default location, and the Windows 11
Enterprise Evaluation ISO downloaded from Microsoft's Evaluation Center. Pass
-Iso (and -BaseFolder for where the VM disk goes); run without -Iso, the
script only prints status.
powershell -ExecutionPolicy Bypass -File build_sandbox_vm.ps1 -Iso "C:\path\to\win11-eval.iso" -BaseFolder "C:\path\to\VMs"If config.json exists the script enables the sandbox in it; otherwise run
python -m customs config --set sandbox_enabled=true and
python -m customs config --set sandbox_vm_name=CustomsSandbox.
The VM is built to be hostile to the sample and harmless to you:
- no shared folders, no clipboard, no drag-and-drop — no bridge back to the host
- network adapter disconnected — a live sample cannot reach your home network or the internet. This costs some detection fidelity for malware that only acts when it can phone home; it is the right trade on a personal machine.
- reverted to the clean snapshot before and after every detonation — nothing persists, and nothing leaks between samples.
Detonation only triggers on files that already scored ≥30 statically, because a full cycle takes minutes. What it observes: new processes (especially interpreters and LOLBins), autostart registry writes, scheduled tasks, new services, files dropped into Startup, shadow-copy deletion, Defender tampering, and attempted network connections.
Manage it with build_sandbox_vm.ps1 -Status and -Destroy.
A caveat worth knowing: advanced malware detects virtual machines and stays
dormant inside them. The Sandbox_Evasion_Checks YARA rule flags samples that
look for VM artefacts, so evasion itself becomes a signal — but a clean sandbox
report is weaker evidence than a dirty one.
Vaulted files are XOR-masked on the way in. That is not encryption and is not meant to be — it exists so the stored bytes are no longer a runnable executable, so nothing can accidentally launch them, and so other scanners don't fight over the vault directory. Restoring reverses it exactly.
Every vaulted file gets a .json sidecar recording where it came from, its
hashes, its verdict, and every signal that contributed.
python -m customs quarantine list
python -m customs quarantine restore --id 3
python -m customs quarantine restore --id 3 --to "C:\somewhere\else.exe"If a file is vaulted but the original could not be deleted (locked by another
process), the action is reported as vaulted_original_locked rather than
quarantined — so you are never told something was removed when it wasn't.
Installers, unsigned developer tools, packed binaries and game modding utilities trip heuristics. That is the cost of catching things Defender misses.
Inspect first:
python -m customs scan "C:\path\to\file" --detailed --no-actionThis prints every signal and its contribution, so you can see exactly what drove
the score. If it's a false positive, add the SHA-256 to rules/allowlist.txt.
A hash pins one exact version of one exact file — modify it at all and the hash
changes and scanning re-arms — so this is safe as long as you verify before
adding.
To get fewer alerts, raise the SUSPICIOUS threshold or the toast level:
python -m customs config --set threshold_suspicious=55
python -m customs config --set notify_min_verdict=MALICIOUSRaising threshold_malicious does not change what is quarantined: MALICIOUS only
ever follows a corroborated detection (see Quarantine requires corroboration).
customs/
__main__.py entry point for `python -m customs`
config.py defaults and config.json loading
store.py SQLite: scans, signals, quarantine ledger, hash cache
engine.py runs the analyzer chain, scores, dispatches response
verdict.py scoring model and thresholds
watcher.py filesystem observers + deduplicating work queue
runtime.py runtime behaviour guard (processes, autostart, services)
responder.py quarantine vault, restore, purge
notify.py Windows toast notifications
dashboard.py loopback status page
util.py hashing, file-type sniffing, safe subprocess helpers
cli.py command line interface
analyzers/ one module per analyzer
rules/
customs.yar YARA ruleset
allowlist.txt hashes to always trust
blocklist.txt hashes to always block
install.ps1 per-user Scheduled Task service control
build_sandbox_vm.ps1 builds the VirtualBox detonation VM
restore_quarantine.py standalone bulk restore of vaulted files
config.example.json sample settings; copy to config.json
requirements.txt
LICENSE
Created at runtime (not in the repository):
config.json your settings
data/
customs.db scan history
customs.log runtime log
quarantine/ the vault
Stated plainly, because a security tool that overstates itself is worse than none:
- Post-write, not pre-execution. Covered above. There is a window between a file landing and Customs finishing analysis. It is seconds, but it is not zero.
- Process monitoring is sampled, not event-driven. The runtime guard polls about three times a second from userland, so a short-lived process can be missed, and it cannot block a process from launching. Network traffic and purely in-memory activity are not monitored; fileless attacks that leave no persistence can go unseen.
- The sandbox is detectable. See above.
- Heuristics generalise, and generalisation costs precision. Expect false positives on unsigned and packed software.
- Archives are inspected, not extracted. Contents are enumerated and judged by name and type; nested files are not individually analysed. Encrypted archives cannot be inspected at all — which is why they score +30 on principle.
- It runs as you. Malware running with your privileges could stop the watcher. Nothing short of a kernel driver or a protected service changes that.
Copyright 2026 the Customs authors. Licensed under the Apache License, Version 2.0.
config.example.json lists the settings most people change. Any key left out
keeps its built-in default (see customs/config.py), and the default
watch_paths already point at your own Downloads, Desktop, Documents and
%TEMP%, so delete that key rather than editing YOU if the defaults suit you.
Your VirusTotal API key goes in config.json, which .gitignore keeps out of
version control.