AntiVirus
A dependency-free antivirus in pure Python — signature + behavioural detection, quarantine, PE dissection, ZIP scanning and a Tkinter GUI.
AntiVirus
A dependency-free antivirus in pure Python — signature + behavioural detection, quarantine, PE dissection, ZIP scanning and a Tkinter GUI.
AntiVirus
A small, dependency-free antivirus written in pure Python (3.9+, standard library only). It scans files and directory trees, quarantines threats, watches folders for new/changed files, and writes JSON + text reports.
> ⚠️ Disclaimer — this project is an educational exercise. It is not > a replacement for a real antivirus and must not be relied on for protection. > It ships with a single, clearly-labelled test signature (the standard, > harmless EICAR test string). It contains no > real malware, and none should ever be added to this repository.
(catches renamed/wrapped variants)
feeds) — millions of indicators fromvetted public sources (blocklists, hash sets, rule strings), converted into compact, tiered local indexes: ~39 MB on disk for 4.8 M indicators, matched by binary search. Licence-aware, provenance-tracked, guarded against the false positives aggregated feeds are full of, and built by you rather than shipped — see Public threat-intel feeds.
indicators) — 603 static checks for how a filebehaves, independent of any specific malware: LOLBins, persistence, defence evasion, credential access, C2, ransomware impact, Office macros and file-name tricks. antivirus indicators --category evasion lists them, antivirus matrix shows coverage per ATT&CK tactic.
allowlist (hashes, names, paths,domains) for the things you know are safe, and antivirus fpcheck, which measures the whole indicator library against random data and realistic clean files so a bad check cannot ship unnoticed.
antivirus yara --out rules.yar exports the samedetection content as a YARA ruleset (532 rules, verified with yara-python), for the tooling that speaks nothing else.
eval/exec on non-constant input,subprocess(..., shell=True), os.system, sockets to hardcoded addresses, dynamic imports, base64 payload blobs
crypto-mining C2 endpoints, LOLBin downloaders (certutil/mshta/bitsadmin), persistence (cron / services / shell rc), encoded payloads
.vbs / .vbe / .wsf) —WScript.Shell command execution, XMLHTTP downloads, certutil / bitsadmin / mshta / scrobj LOLBins, hidden or encoded PowerShell launches, WMI process creation, registry writes, chr() obfuscation
download+execute combinations, registry persistence, timestamp tampering) plus per-section entropy and packer section names
.exe / PE images, like adebugger's module view (headers, characteristics, sections + entropy, imports, exports, resources, relocations, TLS, debug directories, .NET marker) with debug-derived indicators: ASLR/DEP disabled, stripped relocations, missing entry point, missing debug info, TLS callbacks, missing/obfuscated import table, huge .rsrc, and script/LOLBin markers hidden in embedded resources
.dynsym (ELF32 and ELF64:process-execution API sets such as system/execve/popen, dynamic library loading dlopen/dlsym, raw socket APIs, ptrace, and file-manipulation sets) plus high-entropy loadable segments
.tar.gz) contentsare inspected in memory (never extracted to disk): every entry runs through the same signature / behaviour / entropy layers and is reported as archive.tar.gz!entry.py; the layer also flags path-traversal entry names (zip-slip / tar-slip, ../x), encrypted ZIP entries, entry caps and an expansion budget that stops bombs
scan --since 2h only scans files modifiedwithin the given duration (30s / 30m / 2h / 1d / 1w / bare seconds); older files are skipped and counted as such (also works with monitor)
hash FILE… prints SHA-256 / MD5 / SHA-1 digests andthe size of each file
report diff [OLD NEW] compares two saved reports(default: the two newest) and lists what is new, what was cleared, and what is unchanged
sig export FILE / sig import FILEmove a signature database to another machine; the import merges and skips duplicate ids
(size + mtime + engine profile) in .av-cache; the next scan of unchanged files skips disk reads entirely, so rescans are ~20× faster (--no-cache to force a full re-read)
--fast runs the hash + pattern layers only (nobehaviour analysis, no entropy): a quick rescan for when you mostly care about known-bad files
--exclude GLOB (repeatable) skips files whosename or relative path matches, e.g. `--exclude '*.log' --exclude 'build/*'`
packed/encrypted files
JSON manifest; list, restore or purge them later
kill action neutralizes a threat in place:its bytes are obfuscated with a one-time stdlib-only keystream cipher so the binary can no longer run or match its own signatures, while the 256-bit key + IV are stored in the AntiVirus registry (registry/kill.json) — kill revive restores the exact original bytes, kill purge destroys file + material. Rescans treat killed files as neutralized (inert, not re-flagged); threats inside archives are neutralized by obfuscating the container
guard start runs the monitor as a **detachedbackground daemon** (no terminal, no console window): it auto-scans every new/changed file as it appears and acts on threats (quarantine / kill / delete). Lightweight by design: a stat-only incremental walk that never re-lists an unchanged directory, no report files (a small rotating guard.log instead), and a single Python process — controlled with guard start | stop | status | log
a file looks like (document-disguised binaries, double extensions, malware-typical name words, random names, suspicious locations, high entropy): suspicious new files are flagged — and the guard quarantines them — with no known signature at all
scored (threat-intel blocklist, URL shorteners, IP hosts, embedded credentials, executable-looking links, backdoor ports); malicious URLs in a file are a high-severity finding; urlcheck / webshield add|show manage it, GET /api/webshield in the console
ports (4444/4431/5555/31337/…), blocklisted remote IPs, risky listeners (telnet/FTP/VNC/RDP/SMB) and odd outbound ports; firewall scan|monitor, GET /api/firewall
benchmark runs the fully-labelled 25-file sample corpusand reports recall + false positives (100% / 0 on the bundled set)
perf measures real throughput: cold vswarm scan (files/s, MB/s), the cache speedup, and per-file latency (p50/p99) on a synthetic corpus
engine FILE gives a VirusTotal-style per-layerverdict table (hash / pattern / behaviour / polyglot / obfuscation / url / risk / entropy / archive) with a "detected by N of M engines" consensus, in one read pass
snapshot|add|remove|list|clear`): every scan compares trusted files against their baseline hashes; a trusted file whose content changed is reported as mutated, and bulk mutation (≥ 5 files in one pass) raises a critical possible ransomware activity alert; mutations checks the baseline on demand
one format**: PDF/PE (MalPDF exploit pattern, critical), image/PE, OLE/PE, ELF/PE implants, text files with an embedded PE or a high-entropy binary blob (steganography) — while a PDF with a photo or a ZIP bundling a document stays clean
base64 / hex payload blobs, double-encoded payloads, UTF-16 encoded blobs, NOP-sled / int3 junk code, and packed PEs with no import table (hidden API surface)
compare prints the honest feature + positioningpicture vs AVG / Avast / Malwarebytes / VirusTotal, with live perf numbers (no inflated "beats commercial AV" claims)
named malware families** (WannaCry, Petya/NotPetya, Ryuk, LockBit, DoppelPaymer, BitPaymer, Stuxnet, Mirai, Cobalt Strike, Metasploit/ Meterpreter, Emotet, TrickBot, Dridex, XMRig, RedLine, Locky, CryptoWall, Cerber, Jigsaw, Alphabet). Every signature carries its public research source; realworld scores the pack on an inert corpus (13/13 pattern families detected, 0 false positives) and says plainly where the commercial engines still lead (unseen variants)
report
pattern)
rescue build packages the engine + signatures into aself-contained kit and a mountable ISO 9660 image (pure-Python writer) for scanning a machine from a live system; rescue run scans an infected volume with quarantine/reports always sent to the live side and the scan cache disabled; rescue verify checks the media's SHA-256 manifest
packages): pick a target, scan with live progress and a stop button, review severity-coloured findings, and manage the quarantine (python3 -m antivirus gui)
http.server (no packages): scan jobs with live findings feed, results table with text filter, quick single-file check (type, digests, mini-scan), integrity baselines, report history, quarantine manager, signature editor and built-in API docs (python3 -m antivirus web --port 8420)
findings list, detail view and scan options (python3 -m antivirus tui)
import antivirus for a one-shotantivirus.scan(target), or from antivirus import Antivirus for a long-lived engine (signatures, quarantine, baselines, file info) in your own programs — see Using it as a module
fileinfo FILE… (and /api/fileinfo in theweb console) reports the file type from magic bytes + extension, size, mtime and SHA-256/MD5
manifest DIR hashes a tree into a JSONbaseline; a later scan DIR --baseline FILE reports every file that changed, appeared or disappeared (classic FIM)
samples [DIR] writes the full inert demo sampletree (EICAR string, scripts, code-less PE/ELF images, sneaky archives) anywhere, for testing the engine
report summary aggregates all saved reports(infected/clean counts, severity breakdown, top indicators)
verify DIR --baseline FILE re-hashes a treeand diffs it against a baseline without running any signature / behaviour / entropy layers, so it is much cheaper than a full scan --baseline — the quick "is anything different?" FIM check (also GET /api/verify and av.verify(...) as a module)
export [REPORT …] flattens the findings of savedreports (default: all of them) into CSV (default) or JSONL with one row per finding: report, target, timestamp, severity, kind, name, file, SHA-256, size, action and message — ready for Excel / SIEM / grep
sig import FILE also accepts plain-text IOC files:64-hex lines become SHA-256 signatures, 32-hex lines MD5 signatures, pattern: … lines are regexes, and anything else is a literal marker. #/; lines are comments; --source tags the category and --severity sets the level. JSON export files still work unchanged
stats (and GET /api/stats) summarises theengine: signature counts by severity and matcher kind, scan-cache size and entry count, quarantine item count, and report totals
[###····]progress bar on stderr (so --json stdout stays a pure JSON stream for piping), throttled to a smooth update rate
monitor --json emits one JSON object perevent (removed / clean / threat / quarantined / deleted / error) instead of coloured human lines, for piping into a log aggregator
and is packaged separately on Linux (sudo apt install python3-tk / sudo dnf install python3-tkinter). Without it, antivirus gui prints a hint and the CLI works unchanged.
# 1. Prove that detection works (uses the harmless EICAR test string)
python3 -m antivirus selftest
# 2. Scan a directory (detect only — nothing is changed)
python3 -m antivirus scan .
# 2b. Scan again — unchanged files are served from the scan cache (~20× faster)
python3 -m antivirus scan .
python3 -m antivirus scan . --fast # quick rescan: hash + pattern only
python3 -m antivirus scan . --exclude '*.log'
# 3. Scan and quarantine threats
python3 -m antivirus scan . --action quarantine
# 4. See what was quarantined, put it back, or destroy it for good
python3 -m antivirus quarantine list
python3 -m antivirus quarantine restore 275a021bbfb6
python3 -m antivirus quarantine purge 275a021bbfb6
# 5. Watch a folder in (near) real time
python3 -m antivirus monitor . --action quarantine
# 5b. …or run the same protection as a lightweight background daemon
python3 -m antivirus guard start . --action quarantine
python3 -m antivirus guard status
python3 -m antivirus guard stop
# 5c. Score how suspicious a file looks / check URLs / audit the network
python3 -m antivirus risk invoice.pdf.exe
python3 -m antivirus urlcheck http://c2node.evilcorp.invalid/x
python3 -m antivirus firewall scan
python3 -m antivirus benchmark # 100% recall, 0 false positives
# 6. Look at the reports
python3 -m antivirus report list
python3 -m antivirus report show
# 7. Inspect / extend the signature database
python3 -m antivirus sig show
python3 -m antivirus sig add --id MY-LAB-1 --name "My lab marker" \
--severity high --pattern "MY-UNIQUE-MARKER-1234"
# 8. Use the graphical interface (needs Tkinter, see Requirements)
python3 -m antivirus gui
Optionally install the console script (same thing, nicer name):
pip install .
antivirus scan .
Exit codes: 0 = clean, 1 = threats found, 2 = error. Add --json to scan for machine-readable output.
samples/ contains a file with the standard EICAR test string (a 68-byte text marker — not malware) and a clean file:
python3 -m antivirus scan samples/ # -> detects eicar-test.txt
python3 -m antivirus scan samples/ --action quarantine
samples/behavior/ contains inert scripts and code-less PE images that demonstrate the behavioural layer and the PE debug report:
python3 -m antivirus scan samples/behavior
python3 -m antivirus behavior analyze samples/behavior/pipe-shell.sh
python3 -m antivirus behavior analyze samples/behavior/suspicious.exe
# full static "debug report" of a PE image (headers, sections, imports,
# exports, resources, relocations, TLS, debug dirs + red flags)
python3 -m antivirus pe analyze samples/behavior/suspicious.exe
python3 -m antivirus pe analyze samples/behavior/packed-upx.exe
python3 -m antivirus pe analyze samples/behavior/clean.exe # -> no findings
Everything runs on Windows (10/11, 32- or 64-bit) with the stock Python from python.org — 3.9+, no packages:
py -3 -m antivirus scan C:\Users\me\Downloads
py -3 -m antivirus scan D:\ --action quarantine --fast
py -3 -m antivirus web :: dashboard + JSON API in the browser
py -3 -m antivirus gui :: Tkinter desktop app
Works on Windows, unchanged from other platforms: signature + behavioural scanning (including the PE dissection and the VBScript layers that matter most there), archives, entropy heuristics, quarantine with restore, reports, scan cache, integrity baselines (manifest / verify / scan --baseline), IOC imports, statistics, the directory monitor, the web console, the GUI, and the rescue kit — on Windows the kit ships a bootstrap.bat next to bootstrap.sh:
bootstrap.bat D:\ --action quarantine
Platform notes:
curses on Windows). antivirus tui explains this and points to the web console / GUI / CLI.
10+ consoles (best effort). On an older terminal set set NO_COLOR=1 to get plain output.
/-style on every OS, so a baseline is readable (and diffable) across platforms.
(the bits do not exist there); everything else is OS-agnostic.
C:\Windows) needs the usual permissions —unreadable files are skipped, like on any other OS.
For every regular file (symlinks, build dirs and VCS metadata are skipped):
In that single pass the engine computes SHA-256 and MD5, runs the pattern signatures (with an overlap window so patterns can't hide at chunk boundaries) and collects a 256 KiB byte histogram for the entropy heuristic. Files larger than max_file_size (512 MiB) are skipped.
threat.
magic), the behavioural layer analyses what it appears to do: Python sources are parsed with ast (never executed), shell/PowerShell/ batch scripts are checked against indicator regexes, PE images are fully dissected (headers, sections, imports, exports, resources, relocations, TLS, debug directories) and checked for dangerous API combinations and debugger-style red flags, ELF binaries have their dynamic import tables (.dynsym, both 32- and 64-bit) walked for dangerous API sets, and binaries are scanned for reverse-shell / C2 byte markers. Files up to 2 MiB are analysed (the content was already buffered during the single read pass). Run pe analyze FILE for the full human-readable "debug report".
entry is read into memory and run through the same layers — a hit is reported as archive.zip!entry / archive.tar.gz!member; suspicious entry names (zip-slip / tar-slip), encrypted entries and total expansion beyond the 64 MiB budget are flagged. Gzip streams are decompressed in memory and, if they contain a TAR, are scanned member by member. Nothing is ever extracted to disk. 5b. Web shield – the file's bytes are also scanned for http(s) / ftp URLs and each one is scored against the threat-intel blocklist + heuristics (shorteners, IP hosts, credentials, executable-looking links, backdoor ports). A malicious URL is a high-severity malicious_url finding, a suspicious one a suspicious_url. 5c. Suspicion engine – if still nothing matched, the file's risk score (document-disguised binaries, double extensions, malware-typical name words, random names, suspicious locations, entropy) is computed; ≥ 45 is reported as suspicious (medium, high at ≥ 70) – the layer that lets the guard act on odd new files with no known signature.
already-collected histogram: ≥ 7.5 bits/byte on files ≥ 256 KiB is reported as "may be packed or encrypted".
Scan cache: before any of the above, a file whose size and mtime are unchanged since the last scan (and whose engine profile – signature DB version, behaviour/entropy/archive/risk/web-shield settings – is the same) reuses its cached verdict without being read at all. The cache lives in .av-cache (JSON, atomically written) and is bypassed with --no-cache; --fast runs layers 1–2 only.
| Severity | Indicator | Layer | | --- | --- | --- | | high | eval/exec on non-constant (fetched/decoded) input | Python AST | | high | socket.connect() to a hardcoded address | Python AST | | high | pipe-to-shell: curl/wget … \| sh, base64 -d \| sh | Shell | | high | reverse shell: /dev/tcp, nc -e, pty.spawn | Shell / Python | | high | PowerShell IEX + download, -ExecutionPolicy Bypass | PowerShell | | high | LOLBin downloaders: certutil -urlcache, mshta http, bitsadmin | Batch / binary IOC | | high | PE process-injection API set (VirtualAllocEx + WriteProcessMemory + CreateRemoteThread) | PE imports | | high | PE imports both download and execute APIs | PE imports | | high | ELF process-execution API set (system/execve/popen) | ELF imports | | medium | ELF dynamic library loading (dlopen/dlsym) or raw socket APIs | ELF imports | | medium | tar-slip entry name (../x, absolute) in a TAR/TAR.GZ | archives | | high | script/LOLBin markers embedded in PE resources (VBScript, PowerShell, cmd.exe, mshta, …) | PE debug | | medium | persistence: cron / systemctl / shell rc / registry (RegSetValue*) | Shell / PE | | medium | crypto-mining pool endpoint stratum+tcp:// | Shell / binary IOC | | medium | setuid executable | file-system | | medium | high-entropy PE section / packer section name (UPX0…) | PE sections | | medium | zip-slip entry name (../x, absolute) in a ZIP | archives | | medium | ZIP total expansion beyond the analysis budget (zip bomb) | archives | | low | password-protected ZIP entry | archives | | medium | PE with ASLR off (no DYNAMIC_BASE) or DEP off (no NX_COMPATIBLE) | PE debug | | medium | PE with relocations stripped / no base-relocation table, no entry point | PE debug | | low | PE without debug information, with TLS callbacks, no import table, or huge .rsrc | PE debug | | low | base64.b64decode/pickle.loads/CryptDecrypt payload handling | Python / PE |
Everything in samples/behavior/ is inert demo material (the sample .exe is a code-less, hand-assembled PE whose import table advertises dangerous APIs – it can never execute).
The engine is built around one rule: touch each file once.
happen while the file is read once (the previous design re-read files 2–3 times).
single compiled alternation, so each 1 MiB buffer is scanned once instead of once per signature. (If patterns can't be merged – e.g. clashing internal group names – it transparently falls back to per-pattern scans.)
lstat per directory entry – the tree walk is a singleos.scandir pass; the stat result is reused by the scanner and by the monitor's snapshot, so nothing is re-stated.
(hashlib releases the GIL while hashing). Use scan --threads auto (default), --threads N, or --threads 1 for sequential.
lstatwalk and scans bursts of changed files in parallel.
only rebuilt when the signature database changes.
Measured on a 2 500-file / 266 MiB tree (2-core box, warm page cache):
| Engine | Wall time | Threats found | | --- | --- | --- | | v1.0 (2–3 passes per file, sequential) | 0.97 s | 1 of 13* | | v1.1 sequential (--threads 1) | 0.95 s | 13 of 13 | | v1.1 auto (--threads auto → 4 workers) | 0.78 s | 13 of 13 |
\* v1.0 had a latent bug: the heuristic layer was gated on the tree-wide findings list, so the first threat silently disabled heuristics for every later file in a directory scan. Fixed in v1.1.
Scan cache (v1.5) — measured on a fresh 1 202-file / 79 MiB tree (2-core box, Python 3.11):
| Scan | Wall time | Files | Verdicts | | --- | --- | --- | --- | | cold full scan (builds the cache) | 4.16 s | 1 202 | 240 | | warm rescan (cache hit on every file) | 0.18 s | 1 202 (1 202 from cache) | 240 | | cold --fast (hash + pattern only) | 3.04 s | 1 202 | 1 |
A warm rescan is a ~23× speedup with identical verdicts: unchanged files are never read from disk (size + mtime + engine-profile check), and the cache is automatically invalidated when a file changes or the signature database / engine settings differ.
Quarantine keeps the file under an id like 275a021bbfb6-20260918-120000-a1b2c3 inside quarantine/files/, with the original path, timestamp and reason recorded in quarantine/manifest.json.
antivirus rescue builds a self-contained rescue kit — the whole engine plus a snapshot of the signature database, shipped as a directory and a standard ISO 9660 image (written by a pure-Python writer — no third-party disk tooling) — for scanning a machine from outside itself:
$ antivirus rescue build --out ~/usb/kit --iso ~/usb/rescue.iso
==============================================================
Rescue kit built: /home/user/usb/kit
==============================================================
Version: AntiVirus 2.0.0
Signatures: 51 (signatures.json)
Files: 29 (SHA-256 manifest)
ISO image: /home/user/usb/rescue.iso (136.0 KiB)
The kit contains the full antivirus package (as antivirus.zip, so it runs from a mounted ISO), its own signatures.json, a run-rescue.py runner, a bootstrap.sh launcher (Linux/macOS) and a bootstrap.bat launcher (Windows), and a rescue-manifest.json holding the SHA-256 of every file — so the media can prove it is not corrupt or tampered with:
$ kit/bootstrap.sh --verify # "Rescue kit OK"
$ kit/bootstrap.sh --selftest # the full self test, from the media
$ kit/bootstrap.sh /mnt/disk --action quarantine
On Windows the same kit works from a second live Windows environment (or Windows PE), using the drive letter of the infected volume:
bootstrap.bat D:\ --action quarantine
Typical flow: copy the kit (or burn the ISO) to a USB stick → boot any live system or VM (Linux or Windows) → make the infected volume visible → run the kit against it.
Rescue runs are deliberately different from normal scans:
--rescue-quarantine), never into the volume being scanned;
--rescue-reports);create, cached state on the suspect machine.
The ISO image is a valid ISO 9660 filesystem (root directory, path tables, PVD/SVD — verified by the self test), so any OS can mount or burn it. It is not a bootable disc image (that needs a platform bootloader — out of scope for a stdlib-only package); use it from any live environment.
$ antivirus rescue build [--out DIR] [--iso FILE | --no-iso]
$ antivirus rescue run TARGET [--action detect|quarantine|kill|delete]
[--fast] [--since FILE] [--exclude SPEC ...]
[--rescue-quarantine DIR] [--rescue-reports DIR]
[--rescue-registry DIR] [--json]
$ antivirus rescue verify KIT
Module API: `antivirus.rescue_build(out_dir=..., iso_path=..., signatures=...), antivirus.run_rescue(target, action=..., ...)` and Antivirus.rescue_build(...); helpers antivirus.rescue.verify_kit and build_iso9660. Web API: POST /api/rescue/build, GET /api/rescue.
Beyond quarantine (move the file away) and delete (destroy it), the kill action neutralizes a threat in place — the file stays exactly where it is, but its bytes are rendered inert:
(SHA-512(key ‖ IV counter), a counter-mode stream cipher built only from the standard library). The original binary becomes unreadable, non-executable garbage that no longer matches its own SHA-256 signature or behavioural patterns.
obfuscation are stored in the AntiVirus registry (registry/kill.json), alongside the original hash, the ciphertext hash, the path, size, time and the reason it was killed. The registry is the single source of recovery material and never touches the scanned volume.
$ antivirus scan C:\downloads --action kill
[CRITICAL] EICAR-Test-File (signature-hash)
file: C:\downloads\x.txt
action: killed in place (registry: 275a021bbfb6-…-a1b2c3)
$ antivirus kill list # what's neutralized + where its key/IV live
$ antivirus kill revive 275a021bbfb6 # restore the exact original bytes
$ antivirus kill purge 275a021bbfb6 # destroy file + registry entry (final)
Because it is built on the same action pipeline as quarantine/delete, kill works everywhere: CLI, module API, web console, TUI, the directory monitor and rescue runs (where the registry lives on the live side, never on the scanned volume).
Scan improvements that come with it:
neutralized and is not re-flagged (no false positive on the inert bytes), so … | rescan goes clean.
(bundle.zip!entry) is neutralized by acting on the container, and one physical file is acted on only once even if several layers flag it.
Top threats by file list (worst severity first) on top of the full per-finding detail.
> Security note. The registry holds the material that revives a > killed file — protect registry/kill.json with the same care you'd give > the key to a safe. Obfuscation here is a neutralization mechanism for a > detected file, not a general-purpose encryption service.
antivirus guard runs continuous protection in the background: a detached daemon watches a directory and scans every file that is created or modified — no full rescan on demand, nothing to babysit.
$ antivirus guard start ~/downloads --action quarantine
Guard started in the background (pid 1234)
target: ~/downloads
action: quarantine interval: 5s
state: guard
antivirus guard status | antivirus guard log | antivirus guard stop
$ drop suspicious.exe into ~/downloads …
$ antivirus guard status
Guard running (pid 1234)
scanned: 3 new/changed file(s), 1 threat(s)
last: quarantined as 9f3c…-20261001-131200-ab12cd
$ antivirus guard log # recent log lines
[2026-10-01T13:12:00Z] [alert] THREAT [CRITICAL] EICAR-Test-File: …
[2026-10-01T13:12:00Z] [ok] quarantined as 9f3c…
$ antivirus guard stop
How it stays lightweight:
them, and a directory whose own timestamp/size is unchanged is *not re-listed at all* (its known entries are only stat'ed). A directory is re-listed at least every 10 s as a safety net against filesystems with coarse or lazily-updated directory timestamps.
baseline; add --initial for one full scan of the tree at startup.
guard.log (rotated at1 MiB) and a one-line guard.json state; quarantine and the kill registry live inside the state dir too, so everything the guard owns is in one place (default ./guard).
/dev/null stdio; Windows: DETACHED_PROCESS | CREATE_NO_WINDOW), soit survives the shell that started it. guard stop sends a graceful stop sentinel (force-kill only as a last resort); guard start refuses while a guard is already running for the same state dir.
| Subcommand | What it does | | --- | --- | | guard start [TARGET] [--action detect\|quarantine\|kill\|delete] [--interval 5] [--initial] [--state-dir DIR] [--signatures FILE] | Start the guard in the background (default target .) | | guard stop [--state-dir DIR] | Stop it gracefully (sentinel, then force-kill if stuck) | | guard status [--state-dir DIR] | Running? pid, target, action, counters, last event | | guard log [--lines N] [--state-dir DIR] | Show recent log lines |
Signatures only catch what you already know. The risk engine scores how suspicious a file looks, independently of any signature match:
| Signal | Weight | | --- | --- | | claims to be a document/image but contains a PE / ZIP / ELF / OLE2 binary | +35 | | double extension (invoice.pdf.exe), +document disguise | +30 (+15) | | executable / script extension | +12 | | malware-typical name words (crack, keygen, update, …) | up to +16 | | random-looking filename (high name entropy) | +8 | | suspicious parent directory (Downloads, temp, …) | +8 | | very high entropy ≥ 7.6 bits/byte (packed / encrypted) | +25 | | executable bit on a "data" file / stub-size .exe | +6 |
A file scoring ≥ 45 (Config.risk_threshold) produces a suspicious finding (medium, or high at ≥ 70) — and since the scanner emits it like any other layer, **the background guard quarantines suspicious drops automatically**, with no known signature involved:
$ antivirus guard start ~/downloads --action quarantine
$ antivirus guard log
[03:09:14Z] [alert] THREAT [HIGH] SuspiciousFile: downloads/invoice.pdf.exe
[03:09:14Z] [ok] quarantined as 7d0d394357eb-…
antivirus risk invoice.pdf.exe
71/100 [SUSPICIOUS] [##############······] invoice.pdf.exe
+30 double extension ('.pdf.exe') - classic disguise
+15 disguised as a common document type ('.pdf')
+12 executable/script extension '.exe'
The file-scanner half of a web filter: **every file we scan is also read for URLs** (scripts, notes, PE resource strings – the regex runs on raw bytes, so it works anywhere), and each URL is scored against a threat-intel database plus heuristics:
ftp://…/update.exe), long random path tokenMalicious URLs in a file produce a malicious_url finding (high), suspicious ones a suspicious_url (medium) — again visible to every front-end, including the guard.
antivirus urlcheck http://c2node.evilcorp.invalid/drop/x.exe
MALICIOUS (100) http://c2node.evilcorp.invalid/drop/x.exe
- domain 'c2node.evilcorp.invalid' is in the threat-intel blocklist
antivirus urlcheck --from notes.txt # extract + check every URL in a file
antivirus webshield add my-bad-host.invalid # extend the blocklist
antivirus webshield show
The bundled data/threat_intel.json is educational: domains use the reserved .invalid / example TLDs (RFC 2606 – they can never resolve) and IPs use reserved documentation ranges. Add real indicators with webshield add (persisted to intel/user_intel.json) or a plain-text IOC import. Web console: GET /api/webshield?url=….
A live network-connection audit — a monitor, not a packet filter. Reading the OS connection table needs no privileges, so it runs as any user (Linux: /proc/net/tcp{,6}; elsewhere: netstat fallback):
6667 (high)
(medium)
$ antivirus firewall scan
Connections: 59 (source: proc)
Intel: 6 domains, 2 IPs, 7 risky ports
Alerts: none – nothing looks wrong
$ antivirus firewall monitor --interval 5
[14:22:31] [ HIGH] backdoor_port: remote peer on port 4444 (classic reverse shell / Metasploit default)
tcp6 10.0.0.5:51234 <-> 192.0.2.10:4444 [ESTABLISHED]
Same intel database as the web shield, so webshield add feeds both. Web console: GET /api/firewall.
benchmark)antivirus benchmark runs the bundled, fully-labelled sample corpus (25 files: EICAR string, behavioural scripts, code-less PE/ELF images, sneaky archives, disguised doubles, packed blobs, malicious links + clean documents/images) through the full engine and reports recall and false positives:
$ antivirus benchmark
15/15 threats detected, 0 false positives (0.014 s)
The honest framing: on this corpus it scores perfectly, which proves the layers work together and gives you a regression check when you add signatures or intel. It is not a claim of commercial parity — real detection coverage comes from a large, continuously updated signature and intelligence base, which this educational engine replaces with your own signatures, IOCs and the heuristic layers above.
perf)antivirus perf measures scan throughput on a synthetic corpus (three size tiers, 2 KiB / 64 KiB text + 1 MiB binary, plus a few known threats). It does a cold pass (fresh empty cache — every file read + analysed + recorded) and a warm pass (second run, unchanged files served from the verdict cache), and samples per-file latency:
$ antivirus perf --files 150
corpus: 455 files / 159.7 MiB
cold scan : 6.289 s 72.3 files/s 25.39 MB/s findings 155
warm scan : 0.037 s 12447.7 files/s 4368.15 MB/s cached 455
cache speedup : 172.1x
per-file latency (single file, cache warm): p50 35.4 ms p99 42.4 ms mean 28.2 ms
These are real numbers, measured on your machine — use them to tune --threads and to catch regressions. They are not a like-for-like claim against optimized native C++ / kernel antivirus engines, which are a different category. The headline is the cache speedup: rescans of unchanged files are nearly free, which is exactly what makes the background guard and repeated verify runs cheap.
engine)antivirus engine FILE gives a VirusTotal-style table for one file: each detection layer runs as its own "engine" and reports its verdict, plus a consensus line —
$ antivirus engine eicar-test.txt
engine verdict severity details
hash FLAG critical matches 'EICAR-STD-2014' (EICAR-Test-File)
pattern FLAG critical pattern of 'EICAR-STD-2014' found
behaviour clean info ...
polyglot clean info ...
obfuscation clean info ...
url clean info ...
risk FLAG high score 71/100
entropy clean info ...
archive clean info ...
consensus: detected by 3 of 9 engines
The nine "engines" are this tool's layers — hash, pattern, behaviour, polyglot (multi-format files), obfuscation (encoding / packing / junk-code techniques), url (web shield), risk (suspicion engine), entropy, archive — shown in the VT style so you can see which layers agree. The file is read once. (They are not 70+ independent third-party AV vendors.)
realworld)This is no longer just an EICAR toy: data/signatures.json ships **29 sourced IOC signatures covering 20 real, named malware families** — WannaCry, Petya/NotPetya, Ryuk, LockBit, DoppelPaymer, BitPaymer, Stuxnet, Mirai, Cobalt Strike, Metasploit/Meterpreter, Emotet, TrickBot, Dridex, XMRig, RedLine, Locky, CryptoWall, Cerber, Jigsaw and Alphabet. Each signature carries its public research source (FBI flash alerts, CISA advisories, ESET/malpedia TLP:WHITE rules, vendor threat-research writeups) in its source field, so every detection is auditable.
$ antivirus realworld
BitPaymer pattern DETECTED bitpaymer-sample.bin
Cobalt Strike pattern+hash DETECTED cobaltstrike-config.txt
...
20 real-world families (13 pattern, 7 hash) - 13/13 pattern families
detected, 0 false positives
text, Cobalt Strike named pipes / TLS fingerprints, the Stuxnet OS-check byte sequence, leaked Dridex build paths, XMRig config strings, ...
sample* SHA-256s — drop a real sample of those and it's caught by hash.
realworld builds an inert corpus (each sample contains only thepublic IOC of its family — no live malware) and scores the engine: 13/13 pattern families detected, 0 false positives on the clean files. Hash-only families (LockBit's documented locker, Locky, CryptoWall, Cerber, Jigsaw, Alphabet, Emotet, TrickBot) are scored as hash-only: the inert corpus cannot match a sample's SHA-256, but a real dropped sample will.
The honest gap, stated plainly: these signatures catch the documented forms of these families. Commercial engines additionally catch **millions of unseen variants** through continuously updated databases, cloud lookups and ML — that is the axis they genuinely lead, and compare/realworld say so. What you get here is real detection of named real malware with auditable provenance, fully offline, plus a first-class way to extend it with your own IOCs (sig import).
The pack was validated against 8 real ransomware binaries pulled from public research collections (never executed — they are Windows PEs on a Linux scanner; only their bytes are read):
| real sample | sha256 (prefix) | result | | --- | --- | --- | | WannaCrypt 2017 dropper (3.5 MB) | ed01ebfbc9eb… | 4 of 9 engines — hash (critical) + .wnry note-path pattern + packed PE + entropy 7.76 | | LockBit Black 3.0 locker (166 KB) | 80e8defa5377… | 3 of 9 engines — hash (critical) + packed PE + risk 45/100 | | Petya 2016/17 (petya2.exe) | 4c1dc737915d… | hash (high) — plus 8 heuristics incl. "PE downloads and executes code" | | Locky 2016 | bc98c8b22461… | hash (high) + packed-PE heuristics | | CryptoWall | 45317968759d… | hash (high) + heuristics | | Cerber | 9ef7fe10bbbb… | hash (high) + heuristics | | Jigsaw | 3ae96f73d805… | hash (high) — "jigsaw" string verified inside the binary | | Alphabet | 4e60f3c8eaa0… | hash (high) — "alphabet" string verified inside the binary |
Two things this validation proved, honestly stated:
LockBit locker keeps Restore-My-Files and the note text XOR-encrypted in memory, so the plaintext pattern correctly misses the binary itself (it still matches the note file the ransomware drops on disk). Verified sample hashes close that gap for the documented samples, and the heuristic layers (packed sections, no ASLR, no relocations, risk score) keep flagging unknown variants for review — that is the closest a local engine gets to "detect the new thing", and it is exactly what the commercial clouds do at scale.
detection mechanics work end-to-end on genuine malware, not on synthetic fixtures. Coverage of all circulating variants remains the commercial engines' axis, as compare says.
Sample provenance: gr33ntii/malware-collection (WannaCrypt, Petya, Locky, CryptoWall, Cerber, Jigsaw, Alphabet — 2017 public research collection) and whichbuffer/Lockbit-Black-3.0 (LockBit locker shipped with its threat-spotlight PDF). The WannaCrypt sample's hash is cross referenced by the public analysis alby0304/WannaCry_Analysis.
manifest/verify give you a one-shot FIM diff; the trust baseline makes mutation detection continuous: register files you trust, and every scan from then on reports any of them whose content changed.
$ antivirus trust snapshot /opt/myapp # baseline a tree (or files)
$ antivirus trust add /etc/ssh/sshd_config # or register single files
$ antivirus scan /opt/myapp # normal scan — mutations included
[HIGH] File mutated since trusted baseline /opt/myapp/bin/worker
trusted sha256 9f1c… changed to 4ab2…
$ antivirus mutations # one-shot check, no full scan
1 mutated, 0 missing
change, no content change) is not a mutation.
critical aggregate finding — *Mass file mutation – possible ransomware activity*. That is the shape ransomware leaves: a wave of content changes, not a single odd file.
data/trust_baseline.json (git-ignored,per-machine state). A missing trusted file is reported by mutations as File missing since baseline.
A polyglot is one file that is simultaneously a valid member of two or more formats. Every format is validated with its real header rules (structural checks, not magic-byte coincidence), and only genuinely suspicious combinations are reported:
| combination | severity | why | | --- | --- | --- | | PDF + PE | critical | the MalPDF exploit pattern — a document that is also a Windows executable | | PDF + ELF, image (PNG/JPEG) + PE/ELF, OLE + PE, PE + ELF | high | implant / document-that-runs patterns | | text file + embedded PE | high | "readme.txt" hiding a payload | | text file + 16 KiB+ high-entropy region | medium | steganography / hidden binary blob | | PDF/PE + ZIP, PE + ZIP/PDF, image + ZIP | low | self-extracting / attachment patterns (also legitimate — informational) |
Deliberately not flagged: a PDF embedding a JPEG photo, a ZIP that bundles a PDF document, a fake JPEG with an inert body — the legitimate use of multi-format containers.
Where the behaviour layer asks what the file does, the obfuscation layer asks whether the file is hiding what it does:
≥ 4.6 (or a 1 KiB+ hex run) in any file: the standard wrapper for stagers, obfuscated PowerShell, malware-in-document payloads.
another base64 layer: a deliberate extra step.
(PowerShell -enc style) inside binary files.
0x90) or int3 (0xCC) bytes in aPE: mutator / packer padding.
packed/encrypted and declares no imports: the API surface is hidden entirely (a normal PE needs imports).
Thresholds are set to stay clean on ordinary code, minified JS, long hashes in config files and random binaries (a 512-char base64 run is astronomically unlikely to appear by chance in non-structured bytes).
compare)antivirus compare prints the concrete feature set, an honest positioning table against **AVG (free), Avast (free), Malwarebytes (free) and VirusTotal**, and live perf numbers. Where this tool genuinely leads: open source & auditable, zero telemetry (nothing leaves the machine), a clean scriptable Python + JSON API, fully offline, and a set of operational features — guard, kill+revive, FIM baselines, rescue disk, SIEM-ready export, in-file URL/web shield, no-root firewall audit — in the free core. Where the commercial engines genuinely lead: **real-world malware coverage** (huge continuously-updated signature + cloud + ML) and scale. antivirus compare says all of this plainly.
A signature list that ships inside a repository can only ever be small. Real coverage comes from the public blocklists, hash sets and rule collections that security teams publish and maintain — so this tool **builds a database from them on your machine** instead of pretending to ship one.
python3 -m antivirus feeds list # 23 vetted sources + licences
python3 -m antivirus feeds build # ~49 s → 4.8 M indicators, 39 MB
python3 -m antivirus feeds build --all # include runtime-only licences
python3 -m antivirus feeds stats # what is in the database
python3 -m antivirus feeds audit # provenance + licence report
python3 -m antivirus feeds verify # indexes consistent + sorted
python3 -m antivirus scan . # the feeds layer now contributes
python3 -m antivirus feeds clear # remove the database
Measured on a full build (2026-10-15): 4,826,602 indicators from 23 sources in 49 seconds, stored as ~39 MB of sorted index files in data/feeds/ (git-ignored — generated data is never committed). A scan that uses it costs about +0.5 s on a full repository scan.
The indexes are tiered, so a feed hit carries the confidence of the source list rather than a flat severity: domain.critical (ransomware C2), domain.high (4.4 M malware/phishing domains), domain.medium, domain.low (adware/telemetry), and ip.critical / ip.high / ip.medium where the tier comes from how many independent blocklists report the address (IPsum). Most-listed-first lookups mean the worst classification wins.
| Source | What it is | Licence | Redistribution | | --- | --- | --- | --- | | blocklistproject-malware | 2.6 M malware distribution domains | MIT | shipped-as-code | | blocklistproject-phishing | phishing domains | MIT | shipped-as-code | | blocklistproject-abuse | abuse / C2 domains | MIT | shipped-as-code | | blocklistproject-ransomware | ransomware C2 domains | MIT | shipped-as-code | | blocklistproject-fraud | fraud domains | MIT | shipped-as-code | | blocklistproject-malware-ip / -abuse-ip | malware / abuse IPs | MIT | shipped-as-code | | mandiant-hashes | red-team tool hashes (MD5/SHA1/SHA256) | BSD-2-Clause | shipped-as-code | | mandiant-yara | red-team tool YARA string literals | BSD-2-Clause | shipped-as-code | | signature-base-hashes | evil hash IOCs | DRL 1.1 | runtime-only (--all) | | signature-base-c2 / -filenames | C2 IOCs, malware file names | DRL 1.1 | runtime-only (--all) | | phishing-database-domains / -links / -ips / -adblock | active phishing domains, links and IPs | MIT | shipped-as-code | | stevenblack-unified | adware / telemetry hosts (low severity) | MIT | shipped-as-code | | ipsum-ips | aggregated malicious IPs with sighting counts | Unlicense | shipped-as-code | | tweetfeed-year / -month / -exclusive | community-reported IOCs (CC0) | CC0-1.0 | shipped-as-code | | hagezi-threat-intel / -fake | threat-intel + fake-domain lists | GPL-3.0 | runtime-only (--all) |
Redistribution is the important column: every source is usable, but sources whose licence does not allow shipping their data inside an MIT project are runtime-only — they are only downloaded when you ask for them (`feeds build --all), and even then they land in your git-ignored data/feeds/`, never in this repository. feeds audit prints the licence mix of whatever you built.
Converting a feed faithfully matters more than converting it completely:
.hdb, .hsb, .ndb, .cvd, .cld) can be importedfrom your own copy with feeds import-clamav FILE — including the official .cvd containers (512-byte header + gzip + tar). ClamAV's own database is licensed for use, not redistribution, so it is never downloaded or shipped here. Logic signatures (.ldb) and PE-section hashes (.mdb) cannot be represented by this engine and are counted as skipped, not mangled.
when the literal is meaningful without the rule's condition: logic. A rule's logic is not implemented here, so a bare ExitProcess from a rule would fire on every Windows binary — such strings are rejected.
Two guard rails keep the database honest:
scanner can find with memchr, otherwise it would be tested against every byte of every file. Ungated conversions are dropped and counted.
bundled known-clean samples; anything that fires on a file known to be innocent is discarded (pattern-benign-match in the build report).
github.com, bit.ly,googleapis.com, blogspot.com, package registries, CDNs, …) are never indexed, however many feeds list a malicious URL on them. Aggregated feeds are full of such entries: the first full-database scan of this repository flagged github.com in its own README. The guard applies at build time and at lookup time, so a database built before the rule cannot produce that finding either (trusted-host=N in the build report).
dropped from IP feeds and URL hosts: a LAN address in a blocklist only creates false positives on ordinary internal traffic.
Feed hits appear as their own engine layer (feeds) with the source and licence in the finding, e.g.:
python3 -m antivirus engine suspicious.py
feeds Public threat-intel feeds flagged high
[high] Feed-listed domain – blocklistproject: 'malware-c2.example.com'
is listed by blocklistproject (MIT)
Domains found in file content are only reported when they are used like a network address (http://…, www.…, host:8080, host/path, a hosts-file line) or are specific enough (≥12 characters with a hyphen or three labels). Public blocklists contain machine-generated junk such as subprocess.run and s.id — .run and .id are real TLDs — and matching those in source code would be a false positive, so context is required.
Every detector is wrong sometimes: a penetration-test script, an in-house admin tool, a domain your company legitimately talks to. A tool that cannot be corrected gets ignored, so v3.0 adds three things that keep the signal trustworthy.
antivirus allowlist)python3 -m antivirus allowlist add hash <sha256> --note "our patch tool"
python3 -m antivirus allowlist add name "*.ps1" --note "internal scripts"
python3 -m antivirus allowlist add path "/srv/legacy/*"
python3 -m antivirus allowlist add domain vpn.corp.example
python3 -m antivirus allowlist check /srv/legacy/old.exe # why would it be hidden?
python3 -m antivirus allowlist from-findings report.json # import a whole report
python3 -m antivirus allowlist audit # entries without an explanation
Entries live in data/allowlist.json, record when and why they were added, and are honoured by every scan — a suppressed finding shows up as Suppressed: N in the summary and as allowlist_suppressed in the JSON report, so nothing is hidden silently. --no-allowlist shows what the allowlist is currently hiding; the allowlist file is part of the scan-cache key, so adding an entry immediately re-scans affected files.
antivirus fpcheck)python3 -m antivirus fpcheck # random blobs + clean corpus (~30 s)
python3 -m antivirus fpcheck --blobs 150 # the full release audit (~90 s)
Runs the entire indicator library against 450 random 300 KiB blobs and a corpus of realistic clean files (Python, shell, batch, PowerShell, docs, JSON) and exits non-zero when anything reaches the chosen severity. Current result: 0 false positives in 457 clean files. This is the gate every new check has to pass; it is how the loopback-URL and packaging-marker false positives in this release were found and fixed.
antivirus/prefilter.py)Every pattern is gated by the literal runs it cannot match without, so the scanner runs regexes only for plausible files. That gate has to be a necessary condition: a token list that is wrong silently deletes detections. It used to be derived by scanning the pattern text for long literal runs, which is wrong as soon as an alternation has a short-literal branch - `gh[psouhr]_[A-Za-z0-9]{20,}|github_pat_...` only yielded `github_pat_, so every ghp_`-style token was gated out, and `id_(?:rsa|ed25519|ecdsa)` only yielded the two long spellings, so `id_rsa` was skipped. The extractor now walks the parsed pattern: a sequence contributes its weakest required member, an alternation contributes the union of its branches only when every branch has a token, optional parts and negative lookarounds contribute nothing, and positive lookaheads do. Six shipped patterns that could previously be skipped were repaired this way, prefilter.sample_strings() builds a matching witness per pattern so the property can be tested, and tests/test_prefilter.py plus a self-test check fail if any check is ever gated out of a buffer it matches.
antivirus matrix)python3 -m antivirus matrix # checks per tactic, severity mix, top tags
python3 -m antivirus matrix --tag lolbin
603 checks across 8 tactics; the table is generated from the library, and the self test asserts that no check is unmapped to a tactic.
antivirus yara)python3 -m antivirus yara --out av-rules.yar # 532 rules
python3 -m antivirus yara --min-severity high --no-feeds
python3 -m antivirus yara --out r.yar --verify # needs yara-python
Turns the signature database, the technique indicators and the feed literals into a portable ruleset for the huge amount of tooling that speaks YARA (sandboxes, SOC pipelines, Velociraptor). The generator is deliberately conservative: constructs YARA cannot express are skipped and counted (11 hash-only signatures and 100 patterns in the current build), because a rule that silently means something else is worse than no rule. Verified against real yara-python 4.5.4: the 532-rule output compiles, matches the EICAR string, and does not match random data.
allowlist add hash|name|path|domain, audited, counted inevery report, --no-allowlist to bypass.
antivirus fpcheck, the release gate fordetection content (457 clean files, 0 false positives).
antivirus matrix groups all 301 checks by tactic.antivirus yara converts this detection content into aportable ruleset (532 rules, compiled with yara-python 4.5.4).
persistence 46, credential 32, exfil 27, lateral 25, impact 25, Office 12), each with a description and tags.
parsers (adblock, URL-hosts, IP-with-count, TweetFeed CSV, wildcard domains), tiered severities, and the trusted-host + private-address guards described above.
new parsers, the allowlist, YARA conversion and the FP harness.
by the duplicate rule (sp_OACreate, the WSL [boot] command, a Word add-in key, and three rows whose patterns were mis-escaped).
ADODB.Stream,Scripting.FileSystemObject), cmd caret obfuscation, runas /savecred, wmic remote XSL stylesheets, database engine abuse (load_extension, INTO DUMPFILE, Oracle CREATE JAVA SOURCE), remote container/CI contexts (docker build <url>, kubectl apply -f <url>), anti-forensic file handling (attrib +h +s, chattr -i, ThreatDefaultAction Allow, Clear-RecycleBin), credential material (PGP private keys, LSA secrets path, SAM hive copies, ImpersonateLoggedOnUser), cloud exfiltration channels (gists, S3 put_object, Azure SAS URLs, presigned S3 URLs), storage destruction (S3 lifecycle expiry, DROP TABLE, Remove-WebSite) and persistence (USB autorun.inf, a PAM pam_permit.so backdoor).
credential 98, evasion 87, exfil 41, lateral 33, impact 41, office 16), 357 unit tests, 86 self-test checks, 187-file FP audit clean, 32 MB of random data with 0 findings.
more candidate rows (rundll32 javascript:, the amsi.dll patch, CryptUnprotectData, and one Azure row that a sibling already covered).
dotnet tool install --add-source,nuget sources add, gem install --source, gradle --init-script, deno run https://…, bun add <url>), webshell primitives (Runtime.getRuntime().exec, Process.Start(Request…)), interpreter escapes (awk system(), find -exec, tar --checkpoint-action), filename evasion (U+202E override, double extension, NTFS alternate data streams), credential APIs (CredRead, an AWS key id literal, PEM private key blocks), the X/Twitter API as a channel, storage destruction (rm --no-preserve-root, zfs destroy -r, lvremove -f, mdadm --zero-superblock, forced reboot) and cloud/Office persistence (lambda layers, Azure webjobs, gcloud functions deploy, IIS applicationInitialization, the Excel OPEN key).
credential 94, evasion 82, exfil 37, lateral 33, impact 38, office 16), 357 unit tests, 86 self-test checks, 32 MB of random data with 0 findings.
13 more candidate rows that a shipped check already caught (URLDownloadToFile, VbaProject.OTM, ssh -R, socat EXEC, mstsc /shadow, netexec, ntlmrelayx, conhost --headless, …).
public drop hosts, IMDS credential endpoints (AWS role keys and Azure managed-identity tokens), Cisco type-7 secrets, AD database copies, destructive cloud/container calls (az group delete, gsutil rm -r, kubectl delete namespace, docker volume rm, rsync --delete), Office marks (Ole10Native payloads, INCLUDEPICTURE, XXE in OOXML), kernel/boot tamper (nointegritychecks on, LsaCfgFlags 0, ETW session stop), Linux execution (gdb call, ptrace, eBPF probes, systemd-run), and developer-environment persistence (docker runtimes, VS Code extensions, Jupyter startup, /var/spool/cron, GitHub Actions workflows).
into a socket" pattern was loose enough to match a random 300 KB blob (which also suppressed the entropy heuristic for that file). The pattern now requires the compressor's stream flag and an actual pipe; 32 MB of random data produces zero findings again.
credential 90, evasion 76, exfil 36, lateral 33, impact 33, office 16).
also repaired the gate for several of them, and the duplicate rule deleted 26 more rows that only restated a shipped check (installutil, pnputil, Set-MpPreference, DDEAUTO, xllAutoOpen, at, .git/hooks, systemd timers, onion addresses, …).
collected from the raw pattern text, so an alternation whose short branch had no long run lost its guarantee: ghp_… tokens, id_rsa, dnscat, ftp -s:, RUN+=, and UNC admin shares could be gated out of a buffer they match. The extractor now walks the parsed pattern and returns a token set that every match must contain (union across alternation branches, and no gate at all when a branch guarantees nothing). Six shipped patterns were repaired, and a new test plus a self-test check fail if any check is ever skipped again.
prefilter.sample_strings() builds a matching witness string for apattern, which is what makes that soundness property testable.
credential 86, evasion 73, exfil 33, lateral 29, impact 28, office 12), 357 unit tests and 86 self-test checks.
the parts of the kill chain the first two rounds under-covered: 67 persistence rows (AppCertDlls, KnownDLLs, App Paths, Winlogon notify, SafeBoot shell, hidden scheduled tasks, shim databases, IIS modules and ISAPI filters, Exchange transport agents, task COM handlers, RDP startup programs, D-Bus/XDG/systemd units, PROMPT_COMMAND, PYTHONSTARTUP, VS Code folder-open tasks, build.rs, cloud-init, k8s DaemonSets and CronJobs, docker restart policies, AWS/GCP/Azure hooks, CI runners, mofcomp, GPO immediate tasks, MSSQL/Oracle/PostgreSQL/MySQL jobs) and 29 credential rows that the shipped checks did not already catch (WDigest, CachedLogonsCount, KeePass, Bitwarden, DPAPI_SYSTEM, DSRM, gMSA, LAPS attributes, autologon, AWS/Azure/GCP key material, cluster and registry secrets, WSL /proc/<pid>/mem, PAM harvesting, WebCache).
tests/test_techniques.py nowrefuses to ship a row whose own trigger sample is also caught by another check — during this round that rule deleted 27 rows that only restated an existing check (xp_cmdshell, @reboot, vaultcmd, MiniDumpWriteDump, GetAsyncKeyState, secretsdump.py, …). The count above is what survived.
.env readercheck flagged a benign app.py because .env is a substring of os.environ; the pattern now needs a word boundary, and the reverse order needs a file-read verb. fpcheck is clean again (457 files, 0 hits).
spans the real multi-line YAML shape of a #cloud-config payload.
credential 72, evasion 66, exfil 27, lateral 25, impact 25, office 12), 351 unit tests and 84 self-test checks.
antivirus/techniques.py) — 55 more checksin the same format as the first 301, covering modern signed-binary abuse (mavinject, xwizard, wsreset, sqlps, wsl bash -c, dllhost, csc, aspnet_compiler, cl_loaddll, makecab, rasphone), the MSDT/search-ms protocol handlers, Linux and container execution (nsenter, unshare, /dev/shm, compile-on-host, mounted docker.sock), sandbox/anti-debug evasion (uptime, cursor, foreground window, hardware breakpoints, self-checksum, hook scanning), hand-written AMSI/ETW patching, and injection primitives (APC queue, section mapping, thread hijack, atom bombing).
must detect; antivirus indicators --show ID prints it, --dump-samples DIR exports the whole corpus, and tests/test_techniques.py fails if a check stops firing on its own sample, matches empty text, or merely restates an existing check.
credential 32, exfil 27, lateral 25, impact 25, office 12).
antivirus indicators, IND-… ids, 8categories) — a detection layer that reports how a file behaves (89 at the time; extended to 301 in v3.0).
antivirus rules add/remove/list/test/import) — YARA-litepatterns and file-name globs, kept separate from the bundled database.
scan --sarif report.sarif (SARIF 2.1.0) andscan --html report.html (self-contained), plus --fail-on/--quiet.
antivirus metrics, --format prometheus) andantivirus history, doctor, strings, entropy, completion, version, explain.
antivirus/prefilter.py)replaces the merged-alternation approach that cost 6.8 s/MiB.
| Command | Description | | --- | --- | | scan TARGET [--action detect\|quarantine\|kill\|delete] [--threads N] [--no-behavior] [--fast] [--no-cache] [--no-archives] [--exclude GLOB] [--since DURATION] [--baseline FILE] [--fail-on LEVEL] [--sarif FILE] [--html FILE] [--quiet] [--no-feeds] [--json] | Scan a file or directory tree (--threads: auto, N, or 1; --fast: hash+pattern only; --exclude repeatable; --since 30m/2h/1d: only recently modified files; --baseline: integrity check vs a manifest; --no-allowlist: show findings the allowlist hides; kill: obfuscate in place, key/IV to the registry) | | monitor TARGET [--action ...] [--interval 2] [--no-behavior] [--no-archives] [--exclude GLOB] [--since DURATION] [--json] | Watch a directory, scan new/changed files (--json: one JSON object per event; actions include kill) | | guard start [TARGET] [--action …] [--interval 5] [--initial] [--state-dir DIR] | Start the background guard: auto-scan new/changed files, detached, lightweight | | guard stop / guard status / guard log [--lines N] | Stop / inspect / tail the background guard | | risk FILE… [--threshold N] [--json] | Score how suspicious files look (0-100 + reasons, no signatures) | | urlcheck URL… [--from FILE] [--json] | Web shield: check URLs – or every URL found inside a file | | webshield show / webshield add DOMAIN\|IP\|PORT [--note …] | View / extend the threat-intel database | | firewall scan [--json] [--quiet] | Live connection audit (no root): backdoor ports, blocklisted IPs, risky listeners | | firewall monitor [--interval 5] | Watch for new suspicious connections | | benchmark [--samples DIR] [--json] | Run the labelled sample corpus: recall + false positives | | perf [--files N] [--keep-corpus] [--json] | Measured throughput: cold vs warm (files/s, MB/s), cache speedup, per-file p50/p99 | | engine FILE [--json] | VirusTotal-style per-layer engine report + "detected by N of M" consensus (11 layers) | | feeds list | Vetted public feed sources with licence + redistribution class | | feeds build [--source ID]… [--all] [--limit N] [--max-patterns N] [--json] | Fetch, convert and index public feeds into data/feeds/ (default: licence-safe sources only) | | feeds import-clamav FILE | Import a local ClamAV database (.hdb/.hsb/.ndb/.cvd/.cld) | | feeds stats / feeds audit / feeds verify / feeds clear | Inspect / provenance + licence report / consistency check / delete the feed database | | indicators [--category C] [--show ID] [--dump-samples DIR] [--json] | List the 603 technique-indicator checks (--show prints the check's trigger sample) | | matrix [--tag TAG] [--json] | ATT&CK coverage view: checks per tactic, severity mix, top tags | | allowlist list\|add\|remove\|clear\|check\|export\|import\|from-findings\|audit | False-positive allowlist (hashes, names, paths, domains), audited and counted | | fpcheck [--blobs N] [--size KiB] [--min-severity S] [--json] | False-positive audit: random blobs + realistic clean corpus; exits non-zero on a hit | | yara [--out FILE] [--min-severity S] [--no-feeds] [--verify] [--json] | Export the detection content as a portable YARA ruleset | | rules list\|add\|remove\|test\|import | Manage user detection rules (patterns + file-name globs) | | sarif -o FILE / html -o FILE | Scan and write a SARIF 2.1.0 / self-contained HTML report | | strings FILE [--min N] [--utf16] [--grep S] / entropy FILE | Extract strings / measure entropy | | metrics [--format prometheus] [--reset] / history [--limit N] | Cumulative counters / past scans | | doctor [--json] / completion bash\|zsh\|fish / version [--json] / explain TOPIC | Health check, shell completion, inventory, glossary | | compare [--no-perf] [--perf-files N] [--json] | Honest feature + performance comparison vs AVG / Avast / Malwarebytes / VirusTotal | | realworld [--corpus DIR] [--rebuild] [--json] | Score the sourced real-world IOC pack (99 named families, inert corpus): per-family detection + false positives | | hash FILE… [--json] | Print SHA-256 / MD5 / SHA-1 digests + size of each file | | behavior analyze FILE [--json] | Show what one file appears to do (static behavioural analysis) | | pe analyze FILE [--json] | Full static PE dissection ("debug report") + red-flag indicators | | gui | Open the graphical user interface (Tkinter) | | quarantine list | Show everything that is quarantined | | quarantine restore ID | Restore a quarantined file (prefix ok) | | quarantine purge ID | Permanently delete a quarantined file | | kill list | List in-place killed files and their registry entries (key/IV) | | kill revive ID | Restore a killed file's original bytes using the registry's key/IV (prefix ok) | | kill purge ID | Permanently delete a killed file and its registry entry (irreversible) | | sig show | List signatures in the database | | sig add --id … --name … [--sha256/--md5/--pattern …] | Add a signature | | sig remove ID | Remove a signature from the database | | sig export FILE | Write the database to a JSON file | | sig import FILE [--source NAME] [--severity LEVEL] | Merge signatures from a JSON file or a plain-text IOC file (hash / pattern: / literal lines; skips duplicate ids) | | selftest | Run the built-in end-to-end self test | | verify TARGET --baseline FILE [--json] | Fast integrity check (hash + diff only, no signature scan) vs a manifest | | export [REPORT …] [--format csv\|jsonl] [--out FILE] | Flatten findings from saved reports (default: all) into CSV / JSONL | | stats [--no-reports] [--json] | Engine statistics: signatures, cache, quarantine, reports | | report list / report show [FILE] | Inspect saved reports | | report diff [OLD NEW] [--json] | Compare two reports (default: the two newest): new / cleared / unchanged | | report summary | Aggregate all saved reports (counts, severities, top indicators) | | manifest TARGET [--out FILE] | Build a file-integrity baseline (SHA-256 of every file) | | trust snapshot\|add\|remove\|list\|clear [PATH…] | Standing trust baseline — continuous mutation detection | | mutations [--json] | One-shot check of the trust baseline (mutated / missing files) | | samples [DIR] | Write the inert demo sample tree (safe test material) | | web [--host 0.0.0.0] [--port 8420] | Open the web console (dashboard + JSON API) | | tui [TARGET] | Terminal UI (curses): live scan, findings, options | | fileinfo FILE… | Identify files: type (magic), size, mtime, SHA-256/MD5 |
Common options (most commands): --signatures FILE, --quarantine-dir DIR, --report-dir DIR, --registry-dir DIR, --max-size BYTES.
Everything is standard-library only, so the package drops straight into another project — no installation, no dependencies:
import antivirus # one-shot, artefacts under ./
result = antivirus.scan("/path/to/scan", fast=True)
print(result.clean, result.worst_severity)
for f in result.findings: # Finding: severity, name, path, kind, message
print(f"[{f.severity}] {f.name} {f.path}")
# --- long-lived engine with its own working area -------------------------
from antivirus import Antivirus
av = Antivirus(base="~/.myapp") # signatures/cache/reports/quarantine
# /baselines live under ~/.myapp
result = av.scan("some/dir", action="quarantine")
print(result.notes) # {path: "quarantined as …"}
av.add_signature(id="AV-MINE-001", name="Mine",
pattern="UNIQUE-MARKER", severity="high")
av.scan_file("suspicious.bin") # -> [Finding, …]
av.file_info("suspicious.bin") # type (magic), size, mtime, sha256, md5
baseline = av.manifest("some/dir") # integrity baseline (dict)
av.save_manifest(baseline, "baseline.json")
av.verify("some/dir", "baseline.json") # fast hash-only diff -> [Finding, …]
av.risk("invoice.pdf.exe") # -> {score, suspicious, reasons}
av.check_url("http://c2node.evilcorp.invalid/x") # web shield verdict
av.extract_urls(data) # every URL found in bytes
av.firewall_scan() # live connections + alerts (no root)
av.threat_intel().add_domain("bad.host") # extend the blocklist
av.quarantine.restore("275a021b-…")
av.remove_signature("AV-MINE-001")
# --- kill engine (neutralize in place, key/IV in the registry) -------------
result = av.scan("some/dir", action="kill")
print(result.notes) # {path: "killed in place (registry: …)"}
for entry in av.kill_list(): # what's neutralized + its key/IV
print(entry.id, entry.original_path)
av.kill_revive("275a021b-…") # restore the exact original bytes
av.kill_purge("275a021b-…") # destroy file + registry entry
# --- rescue disk -----------------------------------------------------------
manifest = antivirus.rescue_build(out_dir="/usb/kit",
iso_path="/usb/rescue.iso")
info = antivirus.run_rescue("/mnt/infected-disk", action="quarantine",
quarantine_dir="/usb/rescue-quarantine",
report_dir="/usb/rescue-reports")
print(info["result"].clean, info["report"])
Plain-text IOC lists import through the same database: antivirus sig import feed.txt --source feed --severity high turns 64-hex lines into SHA-256 signatures, 32-hex lines into MD5 signatures, and any other line into a literal-marker pattern — the very next scan picks them up.
antivirus.scan returns a ScanResult (see antivirus.scanner) and av.scan additionally carries a notes attribute for applied actions. The same components back the CLI, GUI, TUI and web console, so behaviour is identical in every front-end.
python3 -m antivirus web --port 8420
# then open http://localhost:8420
The dashboard (single page, no external assets, dark theme) offers:
fast / since options and an integrity-baseline selector; each scan runs in a background thread and the UI polls **live progress and a running findings feed**. Past jobs stay clickable in the job list.
detail while they stream in.
SHA-256/MD5 plus a single-file mini-scan.
scan against it (changed / missing / new files); the verify (fast) button runs the cheaper hash-only diff instead of a full scan.
signatures; changes apply to the very next scan.
/api/docs.JSON API (useful for scripting / your own front-end):
| Endpoint | Method | Purpose | | --- | --- | --- | | /api/health | GET | engine status + version | | /api/scan | POST | start a job: {"target", "action", "fast", "no_archives", "since", "baseline"} | | /api/jobs | GET | all jobs (summary) | | /api/jobs/<id> | GET | job progress + findings_preview (live) + full result when done | | /api/quarantine | GET | quarantined items | | /api/quarantine/restore / /api/quarantine/purge | POST | {"id"} | | /api/signatures | GET / POST | list / add signatures | | /api/signatures/remove | POST | {"id"} | | /api/baselines | GET / POST | list / create integrity baselines ({"target", "id?"}) | | /api/fileinfo | GET | ?path=… — type, size, mtime, SHA-256/MD5 + single-file findings | | /api/reports / /api/reports/summary | GET | saved reports / aggregate | | /api/verify | GET | ?target=&baseline= — fast integrity check (hash + diff only) | | /api/stats | GET | engine statistics (signatures, cache, quarantine, reports) | | /api/rescue/build | POST | {"out"?, "iso"?} — build the rescue kit + ISO image | | /api/rescue | GET | last rescue build (404 until one exists) | | /api/kill | GET | kill registry — in-place killed files + stored key/IV | | /api/kill/revive | POST | {"id"} — restore a killed file's original bytes | | /api/kill/purge | POST | {"id"} — destroy a killed file and its registry entry | | /api/webshield | GET | ?url=… (repeatable) — URL verdicts; no URL: the threat-intel DB | | /api/firewall | GET | live connection snapshot + security alerts | | /api/docs | GET | in-browser API documentation |
The console binds to 0.0.0.0 by default and has no authentication — it is a local tool, so only expose it on interfaces you trust.
python3 -m antivirus tui # or: tui /path/to/scan
> Windows: CPython does not bundle curses, so there is no TUI on > Windows — antivirus tui tells you so and points to the web console. > Everything else (CLI, GUI, web, rescue) works there.
A curses screen (standard library, Unix-like systems) with a live progress line, a scrollable severity-coloured findings list (findings appear while the scan runs) and per-finding detail. Keys: s scan, e edit target, j/k move, g/G top/bottom, t fast, a cycle action (detect → quarantine → kill → delete), f incremental window, ? help, q quit. The TUI shares the web console's job engine, so every feature (quarantine, the kill engine, signatures, baselines) works identically in all front-ends.
antivirus/
├── __init__.py # package metadata + module API exports
├── __main__.py # python3 -m antivirus
├── api.py # high-level module API (Antivirus, scan, scan_file)
├── cli.py # argparse CLI + console output
├── gui.py # Tkinter graphical interface (python3 -m antivirus gui)
├── tui.py # curses terminal UI (model + renderer)
├── web.py # web console (dashboard + JSON API, http.server)
├── config.py # all tunables in one dataclass
├── models.py # Finding dataclass + entropy helpers
├── cache.py # scan cache (fast rescans of unchanged files)
├── behavior.py # behavioural analysis (Python AST, shell/PS/batch,
│ # PE/ELF import tables, binary IOCs, SUID)
├── pe.py # PE32/PE32+ dissection ("debug report") + indicators
├── fileinfo.py # file identification (magic + digests)
├── integrity.py # file-integrity baselines (manifest + diff + trust
│ # baseline mutation detection + bulk-mutation alert)
├── polyglot.py # polyglot files: one file valid in 2+ formats
├── obfuscation.py # obfuscation techniques (encoded blobs, junk code,
│ # packed PEs without imports)
├── kill.py # kill engine: in-place obfuscation cipher + registry
├── rescue.py # rescue kit + ISO 9660 writer + rescue scan runner
├── samples.py # builder for the inert demo samples (fake PE/ELF,
│ # sneaky ZIP/TAR.GZ, `samples` tree)
├── scanner.py # single-pass hashing, patterns, behaviour, heuristics
├── signatures.py # JSON signature database (load/add/save) + IOC parser
├── quarantine.py # quarantine store with manifest, restore, purge
├── monitor.py # polling directory watcher (structured events,
│ # incremental cached walk for the guard)
├── guard.py # background guard: detached daemon + start/stop/status
├── risk.py # suspicion engine: 0-100 risk score per file
├── webshield.py # web shield: URL extraction + reputation scoring
├── firewall.py # live connection-table audit (no root needed)
├── indicators.py # 301 technique-indicator checks + ATT&CK coverage summary
├── prefilter.py # literal-token gate that makes pattern matching ~100x faster
├── feeds.py # public threat-intel feeds → compact local indexes (v2.9)
├── allowlist.py # false-positive allowlist: hashes/names/paths/domains (v3.0)
├── fpcheck.py # false-positive audit harness: random + clean corpora (v3.0)
├── yararules.py # YARA ruleset export (conservative, skip-and-count) (v3.0)
├── rules.py # user-defined rules (patterns + file-name globs)
├── metrics.py # cumulative counters + Prometheus rendering
├── exporters.py # SARIF 2.1.0 + self-contained HTML report writers
├── extra_cli.py # v2.8–v3.0 commands (indicators, rules, feeds, allowlist, …)
├── benchmark.py # labelled-corpus detection benchmark
├── engine.py # VirusTotal-style per-layer engine report (consensus)
├── perf.py # measured throughput benchmark (cold/warm + latency)
├── compare.py # honest feature + positioning comparison
├── realworld.py # real-world IOC pack scoreboard (20 named families)
├── report.py # JSON + text report writer, diff, summary, export
├── selftest.py # built-in end-to-end self test
├── output.py # tiny ANSI colour helper
└── utils.py # shared helpers
data/
├── signatures.json # bundled signature database (EICAR test string)
├── threat_intel.json # bundled threat intel (blocklist domains/IPs/ports)
├── user_rules.json # your own rules (created by `antivirus rules`)
├── allowlist.json # false positives you have excluded (created by `antivirus allowlist`)
└── feeds/ # built public-feed database (git-ignored, generated)
samples/
├── eicar-test.txt # the standard 68-byte harmless AV test string
├── clean.txt
├── behavior/ # inert behavioural demo samples (scripts + fake PE)
├── suspicious/ # risk-engine + web-shield corpus (inert)
├── clean/ # labelled-clean corpus (images, CSV, notes, script)
└── realworld/ # realworld pack corpus (auto-built, inert IOCs)
tests/
└── test_antivirus.py
python3 -m unittest discover -s tests -v # stdlib only
# or, if you have pytest:
python3 -m pytest -v
SignatureDB is just JSON; write a script thatmerges upstream hashes into data/signatures.json (a "live update" would be a natural next step).
DirectoryWatcher forwatchdog/inotify events; the _handle() logic stays identical.
exec encoded blobs,detect suspicious PE/ELF sections, scan inside 7z archives, or go beyond static ELF/PE analysis (dynamic sandboxing).
MIT
Automated self-audit of your own public digital footprint — 81 sources, breach watch, encrypted watchlist and a local dashboard with an on-machine LLM.
AI-piloted WiFi auditing for Linux — a modern wifite alternative that finds the targets, picks and runs the attack, then explains every decision; Python, web UI, optional local- or cloud-LLM advisor.
Securely routes your network and stays undetectable.