🛡️ Security & Tools

AntiVirus

A dependency-free antivirus in pure Python — signature + behavioural detection, quarantine, PE dissection, ZIP scanning and a Tkinter GUI.

AntiVirus

Created Sep 18, 2026 · Last pushed Oct 16, 2026

AntivirusPure PythonOpen source

README

Connecting… Open on GitHub ↗

🛡️ 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.

Features

  • Signature detection
  1. Hash — SHA-256 (plus MD5) of each file vs. the signature database
  2. Pattern — regular expressions matched against raw file bytes
  3. (catches renamed/wrapped variants)

  • Public threat-intel database (feeds) — millions of indicators from
  • vetted 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.

  • Technique indicators (indicators) — 603 static checks for how a file
  • behaves, 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.

  • False-positive control — an auditable 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.

  • Portable rules — antivirus yara --out rules.yar exports the same
  • detection content as a YARA ruleset (532 rules, verified with yara-python), for the tooling that speaks nothing else.

  • Behavioural detection (static — nothing is ever executed):
  • Python — AST analysis: eval/exec on non-constant input,
  • subprocess(..., shell=True), os.system, sockets to hardcoded addresses, dynamic imports, base64 payload blobs

  • Shell / PowerShell / Batch — pipe-to-shell downloads, reverse shells,
  • crypto-mining C2 endpoints, LOLBin downloaders (certutil/mshta/bitsadmin), persistence (cron / services / shell rc), encoded payloads

  • VBScript / Windows Script Host (.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

  • PE binaries — import-table analysis (process-injection API sets,
  • download+execute combinations, registry persistence, timestamp tampering) plus per-section entropy and packer section names

  • PE "debug report" — a static dissection of .exe / PE images, like a
  • debugger'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

  • ELF binaries — import-table analysis of .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

  • File-system — setuid/setgid executables
  • Archive scanning — ZIP, TAR and GZIP (including .tar.gz) contents
  • are 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

  • Incremental scans — scan --since 2h only scans files modified
  • within the given duration (30s / 30m / 2h / 1d / 1w / bare seconds); older files are skipped and counted as such (also works with monitor)

  • Hash command — hash FILE… prints SHA-256 / MD5 / SHA-1 digests and
  • the size of each file

  • Report diff — report diff [OLD NEW] compares two saved reports
  • (default: the two newest) and lists what is new, what was cleared, and what is unchanged

  • Signature import/export — sig export FILE / sig import FILE
  • move a signature database to another machine; the import merges and skips duplicate ids

  • Scan cache — a full scan records each file's verdict
  • (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 mode — --fast runs the hash + pattern layers only (no
  • behaviour analysis, no entropy): a quick rescan for when you mostly care about known-bad files

  • Exclude patterns — --exclude GLOB (repeatable) skips files whose
  • name or relative path matches, e.g. `--exclude '*.log' --exclude 'build/*'`

  • Static heuristic — Shannon-entropy check that flags large
  • packed/encrypted files

  • Quarantine — infected files are moved to a sandboxed directory with a
  • JSON manifest; list, restore or purge them later

  • Kill engine — the 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

  • Directory monitor — polls a tree and scans every new/changed file
  • Background guard — guard start runs the monitor as a **detached
  • background 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

  • Suspicion engine — a signature-independent 0-100 risk score from what
  • 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

  • Web shield — every scanned file is read for URLs and each URL is
  • 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

  • Firewall — a live, no-root connection-table audit that flags backdoor
  • 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 — benchmark runs the fully-labelled 25-file sample corpus
  • and reports recall + false positives (100% / 0 on the bundled set)

  • Performance benchmark — perf measures real throughput: cold vs
  • warm scan (files/s, MB/s), the cache speedup, and per-file latency (p50/p99) on a synthetic corpus

  • Engine report — engine FILE gives a VirusTotal-style per-layer
  • verdict table (hash / pattern / behaviour / polyglot / obfuscation / url / risk / entropy / archive) with a "detected by N of M engines" consensus, in one read pass

  • Mutation detection — a standing trust baseline (`trust
  • 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

  • Polyglot detection — flags one file that is valid in **more than
  • 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

  • Obfuscation detection — static technique analysis: high-entropy
  • 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)

  • Comparison — compare prints the honest feature + positioning
  • picture vs AVG / Avast / Malwarebytes / VirusTotal, with live perf numbers (no inflated "beats commercial AV" claims)

  • Real-world detection pack — 29 sourced IOC signatures for **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, 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)

  • Reports — every scan writes a JSON report and a human-readable text
  • report

  • Signature editor — add your own signatures from the CLI (hash or
  • pattern)

  • Rescue disk — rescue build packages the engine + signatures into a
  • self-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

  • Self test — built-in end-to-end test using the harmless EICAR string
  • Graphical UI — Tkinter desktop app (standard library, no extra
  • packages): pick a target, scan with live progress and a stop button, review severity-coloured findings, and manage the quarantine (python3 -m antivirus gui)

  • Web console — a browser UI + JSON API on the standard-library
  • 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)

  • Terminal UI — a curses TUI with live scan progress, coloured
  • findings list, detail view and scan options (python3 -m antivirus tui)

  • Usable as a module — import antivirus for a one-shot
  • antivirus.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

  • File identification — fileinfo FILE… (and /api/fileinfo in the
  • web console) reports the file type from magic bytes + extension, size, mtime and SHA-256/MD5

  • File-integrity baselines — manifest DIR hashes a tree into a JSON
  • baseline; a later scan DIR --baseline FILE reports every file that changed, appeared or disappeared (classic FIM)

  • Sample generator — samples [DIR] writes the full inert demo sample
  • tree (EICAR string, scripts, code-less PE/ELF images, sneaky archives) anywhere, for testing the engine

  • Report summary — report summary aggregates all saved reports
  • (infected/clean counts, severity breakdown, top indicators)

  • Fast integrity check — verify DIR --baseline FILE re-hashes a tree
  • and 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)

  • Report export — export [REPORT …] flattens the findings of saved
  • reports (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

  • IOC import — 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

  • Engine statistics — stats (and GET /api/stats) summarises the
  • engine: signature counts by severity and matcher kind, scan-cache size and entry count, quarantine item count, and report totals

  • Scan progress bar — directory scans print a live [###····]
  • progress bar on stderr (so --json stdout stays a pure JSON stream for piping), throttled to a smooth update rate

  • Machine-readable monitor — monitor --json emits one JSON object per
  • event (removed / clean / threat / quarantined / deleted / error) instead of coloured human lines, for piping into a log aggregator

Requirements

  • Python 3.9+ — that's it. No third-party packages.
  • For the GUI only: Tkinter, which ships with Python on Windows/macOS
  • 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.

Quick start

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

Try it instantly

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

Windows support

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:

  • TUI — the curses terminal UI is Unix-only (CPython does not bundle
  • curses on Windows). antivirus tui explains this and points to the web console / GUI / CLI.

  • Console colour — ANSI colours are enabled automatically on Windows
  • 10+ consoles (best effort). On an older terminal set set NO_COLOR=1 to get plain output.

  • Baselines — manifests and baselines store relative paths in
  • /-style on every OS, so a baseline is readable (and diffable) across platforms.

  • The setuid/setgid file-system indicator simply never triggers on NTFS
  • (the bits do not exist there); everything else is OS-agnostic.

  • Scanning system areas (e.g. C:\Windows) needs the usual permissions —
  • unreadable files are skipped, like on any other OS.

How detection works

For every regular file (symlinks, build dirs and VCS metadata are skipped):

  1. The file is read from disk exactly once, streamed in 1 MiB chunks.
  2. 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.

  3. If a digest matches a signature, the file is reported as a definite
  4. threat.

  5. Otherwise pattern hits are reported.
  6. If the file looks executable (script extension, shebang, or PE/ELF
  7. 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".

  8. If the file is a ZIP, TAR or GZIP archive (up to 32 MiB), each
  9. 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.

  10. If still nothing matched, the heuristic verdict is made from the
  11. 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.

Behavioural indicators (selection)

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

Efficiency

The engine is built around one rule: touch each file once.

  • Single-pass I/O – hashing, pattern matching and entropy sampling all
  • happen while the file is read once (the previous design re-read files 2–3 times).

  • One regex for all patterns – every pattern signature is merged into a
  • 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.)

  • One lstat per directory entry – the tree walk is a single
  • os.scandir pass; the stat result is reused by the scanner and by the monitor's snapshot, so nothing is re-stated.

  • Parallel directory scans – files are scanned in a thread pool
  • (hashlib releases the GIL while hashing). Use scan --threads auto (default), --threads N, or --threads 1 for sequential.

  • Cheap monitor polling – the watcher reuses the same single-lstat
  • walk and scans bursts of changed files in parallel.

  • Cached signatures – compiled regexes and hash indexes are cached and
  • 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.

Rescue disk

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:

  • quarantine always goes to the rescue/live side (CWD or
  • --rescue-quarantine), never into the volume being scanned;

  • reports always go to the rescue side (--rescue-reports);
  • the scan cache is always disabled — a rescue must not trust, or
  • 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.

Kill engine

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:

  • Obfuscation — the file is XOR-ed with a one-time keystream
  • (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.

  • The registry — the 256-bit key and 128-bit IV that undo the
  • 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:

  • Registry-aware rescans — a file that was already killed is counted as
  • neutralized and is not re-flagged (no false positive on the inert bytes), so … | rescan goes clean.

  • Archive-aware actions — a threat reported inside an archive
  • (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.

  • Per-file threat rollup — the scan summary adds a ranked
  • 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.

Background guard

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:

  • Stat-only incremental walk — each poll stats files instead of reading
  • 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.

  • No initial scan by default — the first poll only establishes a
  • baseline; add --initial for one full scan of the tree at startup.

  • No report files — the daemon keeps a small guard.log (rotated at
  • 1 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).

  • One detached process — spawned without a terminal (POSIX: new session
  • /dev/null stdio; Windows: DETACHED_PROCESS | CREATE_NO_WINDOW), so
  • it 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 |

Suspicion engine

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'

Web shield

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:

  • domain / parent-zone / IP in the blocklist → malicious (decisive)
  • URL shorteners (bit.ly, tinyurl, …) → suspicious
  • IP literal instead of a domain, embedded credentials, ftp://
  • executable-looking link (…/update.exe), long random path token
  • remote port on a known backdoor port (4444, 5555, 31337, …)

Malicious 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=….

Firewall

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

  • backdoor port — remote peer on 4444 / 4431 / 5555 / 31337 / 1337 /
  • 6667 (high)

  • blocklisted IP — remote peer in the threat-intel IP list (high)
  • risky listener — telnet, FTP, VNC, RDP, SMB, MS-RPC listening
  • (medium)

  • odd outbound port — established TCP to a non-standard port (low)
$ 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.

Performance & comparison

Detection benchmark (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.

Throughput benchmark (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.

Per-layer engine report (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.)

Real-world detection pack (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
  • Pattern signatures match documented in-file indicators: ransom-note
  • text, Cobalt Strike named pipes / TLS fingerprints, the Stuxnet OS-check byte sequence, leaked Dridex build paths, XMRig config strings, ...

  • Hash signatures (WannaCry, Emotet, TrickBot) match the *documented
  • sample* SHA-256s — drop a real sample of those and it's caught by hash.

  • realworld builds an inert corpus (each sample contains only the
  • public 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).

Real-sample verification

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:

  1. Note-text patterns do not always live in the locker binary. The
  2. 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.

  3. 8 real samples is a spot check, not a coverage claim. It shows the
  4. 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.

Mutation detection (trust baseline)

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
  • Content-based: only the SHA-256 matters — touching a file (mtime
  • change, no content change) is not a mutation.

  • Bulk-mutation alert: 5+ trusted files mutated in one scan add a
  • 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.

  • The baseline lives in data/trust_baseline.json (git-ignored,
  • per-machine state). A missing trusted file is reported by mutations as File missing since baseline.

Polyglot files

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.

Code obfuscation

Where the behaviour layer asks what the file does, the obfuscation layer asks whether the file is hiding what it does:

  • Encoded payload blobs — a base64 run of 512+ chars with entropy
  • ≥ 4.6 (or a 1 KiB+ hex run) in any file: the standard wrapper for stagers, obfuscated PowerShell, malware-in-document payloads.

  • Double-encoded payloads (high) — a base64 blob that decodes to
  • another base64 layer: a deliberate extra step.

  • UTF-16 encoded blobs — the same wrapper stored as UTF-16LE
  • (PowerShell -enc style) inside binary files.

  • Junk code — runs of 256+ NOP (0x90) or int3 (0xCC) bytes in a
  • PE: mutator / packer padding.

  • Packed PE with no import table (high) — the image is
  • 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).

How does it stack up? (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.

Public threat-intel feeds

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.

Where the data comes from

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

What converts — and what honestly does not

Converting a feed faithfully matters more than converting it completely:

  • Hashes (MD5/SHA1/SHA256) and domains/IPs convert exactly.
  • ClamAV databases (.hdb, .hsb, .ndb, .cvd, .cld) can be imported
  • from 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.

  • YARA rules are converted to their plain string literals only, and only
  • 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:

  1. Prefilter gate — a converted pattern must contain a literal run the
  2. scanner can find with memchr, otherwise it would be tested against every byte of every file. Ungated conversions are dropped and counted.

  3. Benign-corpus gate — every converted pattern is matched against the
  4. bundled known-clean samples; anything that fires on a file known to be innocent is discarded (pattern-benign-match in the build report).

  5. Trusted-host guard — 114 well-known hosts (github.com, bit.ly,
  6. 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).

  7. Private-address guard — RFC1918 / loopback / link-local addresses are
  8. 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.

False positives, and what to do about them

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.

Allowlist (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.

False-positive audit (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.

Prefilter soundness (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.

ATT&CK coverage (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.

YARA export (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.

What's new in v3.0

  • Allowlist — allowlist add hash|name|path|domain, audited, counted in
  • every report, --no-allowlist to bypass.

  • False-positive audit — antivirus fpcheck, the release gate for
  • detection content (457 clean files, 0 false positives).

  • ATT&CK matrix — antivirus matrix groups all 301 checks by tactic.
  • YARA export — antivirus yara converts this detection content into a
  • portable ruleset (532 rules, compiled with yara-python 4.5.4).

  • Indicator library: 89 → 356 checks (execution 68, evasion 66,
  • persistence 46, credential 32, exfil 27, lateral 25, impact 25, Office 12), each with a description and tags.

  • Feed database: 9 → 23 sources, 2.8 M → 4.8 M indicators, five new
  • parsers (adblock, URL-hosts, IP-with-count, TweetFeed CSV, wildcard domains), tiered severities, and the trusted-host + private-address guards described above.

  • Tests: 350 unit tests (was 293) plus 84 self-test checks, covering the
  • new parsers, the allowlist, YARA conversion and the FP harness.

What's new in v3.6

  • 28 further checks (575 → 603); six more candidate rows were deleted
  • by the duplicate rule (sp_OACreate, the WSL [boot] command, a Word add-in key, and three rows whose patterns were mis-escaped).

  • New coverage: script-runtime droppers (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).

  • Library totals: 603 checks (execution 160, persistence 127,
  • 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.

What's new in v3.5

  • 33 further checks (542 → 575) after the duplicate rule deleted five
  • more candidate rows (rundll32 javascript:, the amsi.dll patch, CryptUnprotectData, and one Azure row that a sibling already covered).

  • New coverage: package-manager abuse (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).

  • Library totals: 575 checks (execution 150, persistence 125,
  • 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.

What's new in v3.4

  • 35 further checks (507 → 542) after the same duplicate rule deleted
  • 13 more candidate rows that a shipped check already caught (URLDownloadToFile, VbaProject.OTM, ssh -R, socat EXEC, mstsc /shadow, netexec, ntlmrelayx, conhost --headless, …).

  • New coverage this round: ICMP/SSH/relay tunnels, telegram bot tokens and
  • 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).

  • The unit tests caught a false positive in this round: the "archive piped
  • 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.

  • Library totals: 542 checks (execution 138, persistence 120,
  • credential 90, evasion 76, exfil 36, lateral 33, impact 33, office 16).

What's new in v3.3

  • 55 further checks (452 → 507): the prefilter fix described below
  • 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, …).

  • Silent-miss bug in the literal prefilter, fixed. Long literal runs were
  • 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 a
  • pattern, which is what makes that soundness property testable.

  • Library totals: 507 checks (execution 131, persistence 115,
  • credential 86, evasion 73, exfil 33, lateral 29, impact 28, office 12), 357 unit tests and 86 self-test checks.

What's new in v3.2

  • Technique table, part three — 96 further checks (356 → 452), aimed at
  • 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).

  • Duplicate rows are deleted, not counted. tests/test_techniques.py now
  • refuses 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.

  • False positive found and fixed by the harness. The new .env reader
  • check 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).

  • The cloud-init check used to require its two keywords on one line; it now
  • spans the real multi-line YAML shape of a #cloud-config payload.

  • Library totals: 452 checks (persistence 113, execution 112,
  • credential 72, evasion 66, exfil 27, lateral 25, impact 25, office 12), 351 unit tests and 84 self-test checks.

What's new in v3.1

  • Technique table, part two (antivirus/techniques.py) — 55 more checks
  • in 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).

  • Trigger samples are part of the data. Each row carries the snippet it
  • 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.

  • Library totals: 356 checks (execution 126, evasion 66, persistence 46,
  • credential 32, exfil 27, lateral 25, impact 25, office 12).

What's new in v2.8

  • Technique indicators (antivirus indicators, IND-… ids, 8
  • categories) — a detection layer that reports how a file behaves (89 at the time; extended to 301 in v3.0).

  • User rules (antivirus rules add/remove/list/test/import) — YARA-lite
  • patterns and file-name globs, kept separate from the bundled database.

  • Machine-readable output — scan --sarif report.sarif (SARIF 2.1.0) and
  • scan --html report.html (self-contained), plus --fail-on/--quiet.

  • Cumulative metrics (antivirus metrics, --format prometheus) and
  • antivirus history, doctor, strings, entropy, completion, version, explain.

  • ~100× faster pattern matching — a literal prefilter (antivirus/prefilter.py)
  • replaces the merged-alternation approach that cost 6.8 s/MiB.

Command reference

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

Using it as a module

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.

Web console

python3 -m antivirus web --port 8420
# then open http://localhost:8420

The dashboard (single page, no external assets, dark theme) offers:

  • Scan jobs — target + action (detect / quarantine / kill / delete),
  • 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.

  • Results with text filter — filter findings by name / file / kind /
  • detail while they stream in.

  • Quick file check — type any file path: type (magic), size, mtime,
  • SHA-256/MD5 plus a single-file mini-scan.

  • Integrity baselines — create a manifest from the current target and
  • scan against it (changed / missing / new files); the verify (fast) button runs the cheaper hash-only diff instead of a full scan.

  • Report history — saved-report summary + latest reports.
  • Engine stats — live signature / cache / quarantine / report totals.
  • Quarantine manager — list, restore or purge quarantined files.
  • Signature editor — add (pattern / sha256 / md5), inspect and remove
  • signatures; changes apply to the very next scan.

  • API docs — the JSON API is documented in-browser at /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.

Terminal UI

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.

Project layout

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

Running the tests

python3 -m unittest discover -s tests -v   # stdlib only
# or, if you have pytest:
python3 -m pytest -v

Extending it

  • New signature source — SignatureDB is just JSON; write a script that
  • merges upstream hashes into data/signatures.json (a "live update" would be a natural next step).

  • Real-time monitoring — swap DirectoryWatcher for
  • watchdog/inotify events; the _handle() logic stays identical.

  • Behaviour-based layers — e.g. flag scripts that exec encoded blobs,
  • detect suspicious PE/ELF sections, scan inside 7z archives, or go beyond static ELF/PE analysis (dynamic sandboxing).

License

MIT

More in Security & Tools

D3TA1L3R

Automated self-audit of your own public digital footprint — 81 sources, breach watch, encrypted watchlist and a local dashboard with an on-machine LLM.

OSINTSelf-audit
Open source— DetailsGitHub ↗

WiFi-Pilot

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.

wifipilotWiFi auditing
Open source— DetailsGitHub ↗