Skip to content
nochallengePublic

About

Real-time file inspection for Windows: a second opinion after Microsoft Defender, with verdicts explained signal by signal

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Customs

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 this is, and what it is not

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.


Measured behaviour on a real machine

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 IsDebuggerPresent with a single incidental QEMU string, 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.DOTALL let one pattern match tokens 40,000 characters apart
  • https://example.com matching "downloadable executable payload", because .com is 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.


Quick start

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

Optional: 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 test

Verifies 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 watch

Runs 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 -Install

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


Runtime protection — watching what programs do

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

Review anything it recorded:

python -m customs events --important

Two modes, and why

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

The limitation, stated plainly

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 to watch_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.

Commands

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.


How a verdict is reached

Each analyzer emits signals. A signal has a score contribution, a severity, and a plain-English explanation. The verdict engine then:

  1. 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.
  2. Applies exonerating signals, which are negative — a valid Authenticode signature is −25, a long-clean VirusTotal record is −20, an allowlist hit is −100.
  3. Checks for decisive signals that override the arithmetic entirely: a Defender detection, 5+ VirusTotal engines, a decisive YARA rule, or confirmed malicious behaviour in the sandbox.

Thresholds (configurable):

Score Verdict
0–14 CLEAN
15–39 LOW
40–69 SUSPICIOUS
70+ MALICIOUS

Quarantine requires corroboration

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_only

Valid values: quarantine_malicious, alert_only, quarantine_suspicious.


The analyzers

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

VirusTotal

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_HERE

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


The detonation sandbox

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.


Quarantine

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.


Tuning false positives

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-action

This 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=MALICIOUS

Raising threshold_malicious does not change what is quarantined: MALICIOUS only ever follows a corroborated detection (see Quarantine requires corroboration).


Layout

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

Known limitations

Stated plainly, because a security tool that overstates itself is worse than none:

  1. 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.
  2. 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.
  3. The sandbox is detectable. See above.
  4. Heuristics generalise, and generalisation costs precision. Expect false positives on unsigned and packed software.
  5. 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.
  6. 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.

License

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.

About

Real-time file inspection for Windows: a second opinion after Microsoft Defender, with verdicts explained signal by signal

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages