No description
  • Scheme 89.1%
  • Shell 9.9%
  • Makefile 1%
Find a file
2026-07-30 15:47:37 -06:00
.jerboa Harden Virus release evidence privacy 2026-06-26 20:48:51 -06:00
docs Resolve security audit findings 2026-07-11 17:29:30 -06:00
scripts Resolve security audit findings 2026-07-11 17:29:30 -06:00
support Resolve security audit findings 2026-07-11 17:29:30 -06:00
test security: make .jscanignore/.virusignore opt-in (hostile-input bypass) 2026-07-22 12:42:02 -06:00
tools Security hardening and release readiness 2026-06-23 10:51:58 -06:00
virus jpkg: complete binary package setup 2026-07-30 15:16:51 -06:00
.build.yml ci: set umask 022 in package/verify tasks too (jerbuild creates group-writable cache dirs during pkg operations) 2026-07-22 13:10:59 -06:00
.gitignore jpkg: complete binary package setup 2026-07-30 15:16:51 -06:00
.gitsafeignore feat: filesystem walker, worker pool, and 34 modern rules 2026-05-14 16:14:59 -06:00
.jerbuild Resolve security audit findings 2026-07-11 17:29:30 -06:00
AGENTS.md docs: remove jerboa-emacs restriction 2026-07-30 15:47:37 -06:00
clamav-parity.md Resolve security audit findings 2026-07-11 17:29:30 -06:00
dependency-lock.tsv jpkg: complete binary package setup 2026-07-30 15:16:51 -06:00
jpkg.lock jpkg: complete binary package setup 2026-07-30 15:16:51 -06:00
jpkg.policy.sexp Add jpkg package CI 2026-06-18 16:32:16 -06:00
jpkg.sexp Add jpkg package CI 2026-06-18 16:32:16 -06:00
LICENSE Switch to MIT license 2026-07-21 13:42:21 -06:00
Makefile security: make .jscanignore/.virusignore opt-in (hostile-input bypass) 2026-07-22 12:42:02 -06:00
NOTICE Security hardening and release readiness 2026-06-23 10:51:58 -06:00
README.md Resolve security audit findings 2026-07-11 17:29:30 -06:00
SECURITY.md Resolve security audit findings 2026-07-11 17:29:30 -06:00

jerboa-virus

A pure-Jerboa malware scanner: a literal/hex/regex pattern engine with a condition DSL, a parallel filesystem walker, offline hash-intel feeds, and a YARA importer for ingesting third-party rule packs.

Ships as a single self-contained binary — no Chez or Jerboa needed at runtime.

Design

  • Jerboa with a pinned native safety kernel. No shelling out to YARA, ClamAV, or another antivirus on the scan hot path. The matcher, condition evaluator, hashers, and intel loaders are Jerboa; the Rust Thompson-NFA regex engine is statically linked so untrusted regexes cannot downgrade to a backtracking implementation.
  • Rules as data. Malware rules are plain Jerboa hash literals — testable in the REPL, composable, easy for both humans and LLMs to author. A YARA importer is bundled for ingesting existing rule packs (supported subset documented below).
  • Hybrid intel. Offline SHA-256 / SHA-1 feeds (MalwareBazaar, URLhaus, NSRL known-good filter, anything line-oriented) fold into the scanner alongside rules. Online APIs are out of scope.
  • Performance first. Pre-compiled rulesets, immutable for thread-safe sharing across worker pools. A single Aho-Corasick pass per file finds every candidate across all rules; hex wildcards verify by mask on hit, and regex patterns pre-filter through their longest required literal (extracted at compile time) before the engine runs. Binary content is detected by a NUL probe in the first 4 KiB and regex is skipped for it by default — see --regex-mode.

Rule example

(def rule:xmrig
  (make-rule
    (make-hash
      'id          'xmrig
      'name        "Miner.XMRig"
      'description "XMRig cryptocurrency miner indicators."
      'severity    'high
      'tags        '(miner cryptocurrency xmrig)
      'strings     (make-hash
                     'name    "xmrig"
                     'banner  "XMRig"
                     'stratum (make-hash 'regex "stratum\\+tcp://")
                     'algo    (make-hash 'regex "(rx/0|cn/r|kawpow|cn-pico)"))
      'condition   '(and (str name) (any-of stratum algo banner)))))

Patterns are dispatched by shape:

Author writes Pattern type
"xmrig" literal text (UTF-8 encoded)
#vu8(77 90 144 0) literal bytes
(make-hash 'hex "4d 5a ?? 00") hex bytes with ?? wildcards
(make-hash 'regex "stratum.+") guaranteed-linear Rust regex over a Latin-1 byte view

Conditions support: (str ID), (any-of ID...), (all-of ID...), (none-of ID...), (n-of N ID...), (and ...), (or ...), (not ...).

Build & install

Requires jerbuild on PATH, or set JERBUILD=/path/to/jerbuild. Chez and the Jerboa standard library are supplied by jerbuild at build time. The runtime verifier requires the exact executable, bundle, and native library digests in dependency-lock.tsv.

The current lock honestly records the shared Jerboa source as UNCOMMITTED-DIRTY. Functional builds and adversarial tests run against the measured bundle, but make install, source fetching, and release evidence fail closed until those shared changes have an immutable committed revision.

make test              # run the full test suite
make binary            # produces ./jscan (self-contained executable)
make security          # local security/import checks
make sbom              # source/dependency inventory
make reproducibility-report
make archive-regex-stress
                       # inert local archive/regex stress evidence
make soak-evidence     # local parity/soak status
make verify            # security + binary build + tests
make bounded-run ARGS='PATH'
                       # limited worker; enables PCRE2-only ClamAV rules
make release-evidence  # write dist/release-evidence/
make install           # build + install to ~/.local/bin/jscan
make uninstall         # remove the installed binary

Security and release details live in SECURITY.md, docs/threat-model.md, docs/security-hardening.md, and docs/sandbox-policy.md, and docs/release-evidence.md.

CLI

jscan PATH [PATH...]       # recursive scan with worker pool
jscan --severity high ~    # scan your home directory with filters
jscan scan PATH [PATH...]  # equivalent explicit form
jscan match-file PATH      # scan a single file
jscan list-rules           # show bundled rules
jscan list-feeds           # show available threat-intel feeds
jscan update [FEED...]     # download/refresh feeds into the cache

scan flags:

  • -j N, --jobs N — worker thread count (default 4)
  • --max-size MB — skip files larger than MB megabytes (default 100)
  • --follow-symlinks — follow symlinks (off by default). Directory device/inode identities prevent cycles, and resolved targets may not escape the explicitly supplied scan roots.
  • --no-progress — suppress the per-second progress line on stderr
  • --regex-mode MODEauto (default), full, or lit-only. auto runs regex on text files and skips it on binary content (literals + hex still scan via Aho-Corasick). full always runs regex; lit-only never does. Use full if your rules have regex patterns intended to fire inside executables.
  • --intel FILE — load known-malicious hashes from FILE (repeatable). Lines: SHA-256 or SHA-1 hex with an optional label after whitespace or comma. # comments and blank lines ignored.
  • --rules FILE — load custom Jerboa-format rule files (repeatable).
  • --yara-rules FILE — load YARA-format rules (repeatable). Supported subset detailed below.
  • --no-builtin-rules — skip the bundled rule pack; use only --rules / --yara-rules.
  • --tag TAG — keep only rules tagged TAG (repeatable; OR semantics).
  • --severity LEVEL — keep only rules at LEVEL or higher (info|low|medium|high|critical).
  • --output FORMAT — per-finding line format: text (default) or ndjson. NDJSON streams one JSON object per finding so downstream tools can consume incrementally.
  • --exclude GLOB — skip paths matching GLOB (repeatable). The glob matches against both the full path and the basename, so *.log works without **/*.log. **/* recursion is supported.
  • --ignore-file FILE — read one --exclude pattern per line from FILE. If .jscanignore exists in the current directory it is loaded automatically.
  • --color WHEN — colorize text output: auto|always|never. Default auto enables color only when stdout is a TTY. Ignored for ndjson.
  • --no-auto-feeds — skip auto-loading of cached intel + YARA rule packs from ~/.local/share/jscan/. Use this when you want explicit control via --intel and --rules / --yara-rules.
  • --clamav-db PATH — load explicit ClamAV .cvd / .cld files, raw database member files, or one-level directories of raw database files (repeatable). This is useful for no-malware parity fixture testing against clamscan --database=PATH.
  • --clamav-pua — opt into ClamAV PUA signature members (hdu, hsu, mdu, msu, ndu, ldu) from cached CVDs. These are disabled by default because PUA coverage is intentionally higher-noise.

Skipped automatically: .git, node_modules, __pycache__, .cache, .cargo, .rustup, .venv, .tox, .gradle, .idea, .vscode, vendor, the binary itself, plus kernel-virtual filesystems (/proc, /sys, /dev, /run).

Exit code is 0 on a clean scan, 1 when one or more rules fire, and 2 on usage or I/O errors.

Threat-intel feed fetcher

jscan update downloads curated public feeds and caches them so the next scan auto-loads them. No registration or API keys are needed. The fetcher uses HTTPS-only curl for generic feeds. Official ClamAV databases require freshclam; if it is unavailable the update fails closed and never downgrades to a direct CVD download. With no arguments the defaults are fetched (small, recent-window feeds suitable for daily refresh):

Feed Kind Size Source
malwarebazaar-recent intel ~50 KB abuse.ch MalwareBazaar SHA-256 (48h)
threatfox-sha256 intel ~15 KB abuse.ch ThreatFox file-IOC SHA-256s
yara-forge-core rules ~8 MB YARA-Forge curated daily release
clamav-daily clamav ~50 MB ClamAV daily CVD signatures

Heavier optional feeds can be fetched by name:

Feed Size Notes
urlhaus-payloads ~750 MB abuse.ch URLhaus full payload corpus
yara-forge-extended ~tens MB Broader YARA-Forge bundle, more FPs
yara-forge-full ~hundreds MB Everything YARA-Forge ships
clamav-main ~150 MB ClamAV base CVD signatures
clamav-bytecode ~1 MB Accounted .cbc bytecode; hook/unpacker source is not executed
jscan list-feeds                  # show catalog + cache freshness
jscan update                      # fetch all default feeds
jscan update threatfox-sha256     # refresh one feed
jscan update urlhaus-payloads     # opt into a heavy feed
jscan update clamav-official      # daily + main + bytecode
jscan update broad-coverage       # abuse.ch + YARA-Forge full + ClamAV
jscan update clamav-main clamav-bytecode

The cache lives at ~/.jscan/cache/ with this layout. Set an absolute JSCAN_CACHE_DIR to use a different private cache. If HOME is unavailable, an explicit cache is mandatory; there is no shared temporary fallback.

~/.jscan/cache/
├── intel/        # *.txt — hash feeds in our intel format
├── rules/        # *.yar — YARA rule packs
├── clamav/       # *.cvd / *.cld — ClamAV database containers
└── state.txt     # last-fetch timestamp + size per feed

scan auto-loads every intel/*.txt and rules/*.yar it finds, so a freshly run update immediately benefits the next scan. Use --no-auto-feeds to opt out and control loading explicitly via --intel / --rules / --yara-rules.

ClamAV CVD/CLD files in the clamav/ cache are auto-loaded only when the file and sidecar are user-owned mode-0600 regular files, FreshClam authenticated the update, the recorded SHA-256/version/builder/build time still match, the signed header MD5 matches the compressed payload, and freshness/functionality/rollback checks pass. Explicit CVD/CLD paths require the same provenance; explicit raw signature member files remain available for locally trusted fixtures. Current usable formats are hdb, hsb, mdb, msb, fp, sfp, ign, ign2, ndb, and the current official ldb logical-signature corpus. PUA hash/NDB/LDB members can be loaded with --clamav-pua. NDB signatures support literal bytes, byte/nibble wildcards, fixed jumps, ranged/unbounded jumps, simple alternates, empty alternate branches, isolated nibble bodies, absolute offsets, floating offsets, EOF-relative offsets, EP-relative PE offsets, PE/ELF section-relative offsets, PE version-resource VI offsets, and large bounded gaps used by the official corpus. It also applies target-type filters for PE, OLE2, normalized HTML, mail, graphics, ELF, normalized ASCII/script text, Mach-O, PDF, Flash, and Java where those formats can be identified from headers or scan context. LDB support covers plain hex subsignatures, NDB-style subsignature offsets, Target, FileSize, NumberOfSections, boolean &/| expressions, and hit-count modifiers (=, ==, <, >, A,min-max, plus X,Y distinct-signature thresholds). It reuses the NDB body parser for ranged, alternate, and fixed-byte negated subsignatures, and supports literal w/a/f modifiers plus wide/case-insensitive regex-derived subsignatures with fixed gaps and wildcards. LDB target handling includes Container/Intermediates for zip-family, tar, gzip, MIME mail base64 and quoted-printable parts, OLE2/MSOLE2 streams, decoded PDF ASCIIHex/ASCII85/RunLength/LZW streams including /EarlyChange 0, PDF decode predictors, optional libz-backed PDF Flate streams, and OOXML archive contexts already extracted by the scanner, OOXML XML Word/XL member aliases, root PE/RTF/HTML file-type contexts, bounded lowercasing normalized HTML extraction with tag-preserved, tag-stripped, and extracted JavaScript buffers, bounded normalized ASCII text extraction, bounded ASCII script normalization with common JavaScript string-escape decoding, bounded HTML data:image/...;base64,... extraction, audited HandlerType filters for PDF, graphics, zip, and script-like source, PE version-resource VI: offsets, and a conservative slash-form PCRE subset with triggered unanchored fallback for patterns without literal prefilters. When available, optional PCRE2 and libclamav backends add ClamAV-compatible PCRE constructs, anchored A, rolling r, and encompassed e offset-window semantics, plus exact fuzzy_img#...#0 image fuzzy-hash matching. It also supports the current official binary byte-compare subsignatures and legacy logical-expression suffix forms that ClamAV accepts in old signatures. Unsupported ClamAV member formats are counted in the startup log rather than silently treated as supported. ClamAV FTM file-type metadata is loaded and used for root scan-context classification and extracted archive-member aliases, and CDB container metadata is evaluated for ZIP members, including encrypted entries, POSIX tar members, and best-effort bsdtar-listed archive members where names, real sizes, and ordinals are available; ISO9660 members also report uncompressed member size as the CDB compressed-size field. PDB/WDB phishing metadata is loaded for conservative HTML displayed-domain mismatch detection with allow-list suppression, including bare displayed domains, HTML entity, semicolonless numeric and URL-punctuation named entities, percent-host, and scheme-relative URL cleanup, escaped JavaScript/event and CSS URL contexts, plus quoted-printable mail body extraction. CFG dynamic-configuration rows are parsed and accounted as non-signature metadata. CRB certificate trust/block rows are parsed and blocked PE Authenticode certificate entries are enforced for PKCS#7 certificate tables when OpenSSL is available, with trusted code-signing or certificate-signing CRB rows suppressing block hits from the same chain. The .cbc bytecode path accounts source-recovered members from bytecode.cvd when clambc --printsrc is available. The source-only compiler recognizes simple standalone DEFINE_SIGNATURE sources, including literal bytes, masked bytes, fixed jumps, ordered * / {n-m}-style gaps, simple alternates, negated byte groups, and exact, floating, EP-relative, and EOF-relative offsets. It can compile those standalone matchers to WebAssembly and run them with Jerboa's bounded WASM runtime. It also parses simple logical_trigger() boolean expressions over matches(Signatures.name) and count_match(Signatures.name) predicates so supported source signatures can be checked as grouped preconditions instead of loose independent matches. Current official bytecode source members are hook/unpacker programs whose logical_trigger() expressions are only prefilters for later bytecode logic, so they are accounted but not executed as standalone detections. Opaque compiled bytecode is still not executed; source-less .cbc members remain accounted gaps. Hook-only source members that have no source-level signature and no virus-reporting API call are accounted separately from unsupported signature source. See clamav-parity.md for the parity roadmap. tools/clamav-compare.sh runs local corpus comparison against clamscan; the comparison tool writes detail files plus summary.json, auto-uses the jscan ClamAV cache when CLAMSCAN_ARGS is unset, and --fail-on-delta makes it suitable for local CI gates. tools/clamav-classify-deltas.sh layers triage on top of that output and writes delta-classification.tsv / .json using an optional manifest to mark deltas as parity gaps, intentional extra detections, false positives, or policy differences. If a corpus is laid out as CORPUS/samples plus CORPUS/manifest.tsv, the classifier auto-discovers that parent manifest, which lets private local corpora live outside the repository while still enforcing --fail-on-unclassified. tools/clamav-safe-corpus.sh creates a disposable EICAR-based smoke corpus for validating the harness without real malware. tools/clamav-parity-corpus.sh creates an inert synthetic ClamAV database plus archive/mail/PDF extraction samples that both jscan and clamscan should detect identically. tools/jscan-trigger-corpus.sh creates an inert signature-trigger corpus for the bundled jscan-native rules, with a manifest that classifies those jscan-only detections as intentional extras when compared with clamscan. tools/jscan-benign-corpus.sh and tools/jscan-benign-smoke.sh create and enforce clean near-miss fixtures for false-positive regression checks against the bundled rule pack. tools/clamav-official-counts.sh checks the current official-cache load snapshot and fails if NDB/LDB deferrals reopen.

For bytecode source accounting, tools/clamav-cbc-signatures.sh extracts source-recovered DEFINE_SIGNATURE entries from bytecode.cvd/.cld or an unpacked .cbc directory and marks which signatures fit the current source-only WASM signature subset or require hook/unpacker semantics. tools/clamav-ldb-gaps.sh prints a static category breakdown of remaining LDB gaps from cached or unpacked ClamAV databases.

Scan-time archive recursion covers common zip-family containers (.zip, .jar, .apk, .docx, .xlsx, .pptx), tar files, gzip-compressed tar files, and plain gzip streams, including nested archives up to a bounded recursion limit. When bsdtar is available, jscan also performs bounded best-effort member traversal for 7Z, RAR, CAB, ISO9660, DMG, and NSIS containers, and tags root DMG/NSIS/AutoIt/CAB/ ISO/7Z/RAR contexts for ClamAV LDB container rules. Findings inside an archive are reported on the container path, matching clamscan's path-level behavior.

Ordinary rule and YARA regexes are accepted only by Rust regex. Backreferences, look-around, and other unsupported constructs are rejected at rule-load time. ClamAV-only PCRE constructs use PCRE2 match/depth limits and are enabled only inside scripts/jscan-bounded-worker.sh (or an equivalently isolated service worker that deliberately sets JSCAN_ISOLATED_WORKER=1 after applying limits). Archive recursion has per-container and per-file global member, expanded-byte, member-name, and depth budgets. Filesystem traversal streams through a 256-entry work queue, uses bounded descriptor-pinned directory listings, and caps depth, files, entries, and aggregate path bytes.

YARA-Forge rules that use unsupported constructs (modules, wide, filesize, hex jump ranges, etc.) are skipped at parse time with a short stderr summary — the rest of the pack still loads. Expect to keep a few hundred rules out of the several thousand in a YARA-Forge release; the cherry-picked ones are still meaningful detection coverage on top of the bundled rules.

Hash intel feeds

Intel files are plain text. Each non-empty, non-comment line carries a SHA-256 (64 hex chars) or SHA-1 (40 hex chars), optionally followed by a label after whitespace or a comma:

# MalwareBazaar dump 2026-05-01
275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f EICAR
3395856ce81f2b7382dee72602f798b642f14140  Trojan/Generic.x

When --intel FILE is passed, every scanned file is also hashed and looked up in the database. Hits surface as findings with a synthetic rule named after the matched algorithm. The lookup is O(1) per file regardless of feed size.

YARA importer

--yara-rules FILE parses YARA syntax and converts each rule into the internal data model. Supported:

  • meta: description, author, severity (info|low|medium|high|critical)
  • strings: ASCII ($a = "..."), hex ($a = { 4d 5a ?? 90 }), regex ($a = /pat/), with nocase and ascii modifiers
  • condition: $a / not, and, or, (...), N of them, any of them, any of ($prefix*)
  • multiple rules per file, // and /* */ comments

Rejected with a clear error: import statements, hex jump ranges ([N-M]), hex alternation ((A|B)), the wide, xor, fullword, base64, base64wide modifiers, and any module reference (pe.imports, math.entropy, etc.). If you have a YARA rule pack you want to ingest, expect a handful of rules to be rejected — that is by design; the scanner refuses to silently misinterpret what it does not fully support.

Config file

Stash your preferred scan defaults in ~/.jscan/config and they get prepended to every jscan invocation. Each non-comment, non-blank line is one CLI flag-and-value pair; whitespace splits tokens.

# ~/.jscan/config
--jobs 8
--severity medium
--exclude /Volumes/Backups
--exclude **/node_modules
--rules /etc/jscan/site-rules.scm

Resolution order: --config FILE > $JSCAN_CONFIG > ~/.jscan/config

legacy $VIRUS_CONFIG / ~/.virus/config > none. Explicit CLI flags still win for scalar options and accumulate for repeatable ones (--exclude, --intel, --rules, --yara-rules). Use --no-config to skip the file even if one exists.

Ignore files

Line-oriented globs. Blank lines and # comments are skipped. The same syntax as --exclude. Either pass via --ignore-file FILE or drop a .jscanignore at the root of the directory you are scanning. Legacy .virusignore files are still honored.

# .jscanignore
node_modules
*.log
build/**/*.o

Bundled rules

34 hand-written rules covering: EICAR + encoded binary dropper heuristics (base64-encoded ELF/PE); cryptominers (XMRig, generic stratum-pool, Ethereum-family); reverse shells (bash, python, perl, PHP, netcat, ruby); webshells (PHP eval-base64, China Chopper, JSP/Runtime.exec); Linux malware (Mirai, Gafgyt, Diamorphine rootkit, LD_PRELOAD rootkits); credential / data exfil (AWS keys, GitHub tokens, SSH private keys, GCP service-account JSON, browser credential stores, crypto wallets); persistence (suspicious macOS LaunchAgent, suspicious cron fetch-and-pipe); shell-droppers (curl|sh, wget|sh, encoded PowerShell, PowerShell download cradles); and exfil channels (Discord webhooks, Telegram bots, Slack webhooks, Pastebin droppers).

Run jscan list-rules for the full list with one-line descriptions. Rules are pure Jerboa data — see virus/builtin-rules.ss to read or contribute.

License

MIT