Antivirus/crates/houndd/rules/hound-builtin.yar
Hound 6746182f18 houndd: replace the clamscan fork with yara-x in process
The old engine shelled out to clamscan for every scan, and clamscan
reloads a 169 MB signature database on every invocation. Measured on a
68-byte EICAR file: 6.5 seconds and ~1.5 GB RSS — paid once per file,
and realtime.rs called it once per inotify event.

Replaces it with HoundEngine: yara-x compiled once at daemon start,
held in memory, one scanner reused across a whole walk, plus a verdict
cache keyed on (dev, ino, mtime, size) so an unchanged file that has
been seen before never reaches the matcher.

Measured after, same machine, same EICAR file:

  single file      6.5 s  ->  4 ms
  400 files cold      --  ->  9 ms
  400 files warm      --  ->  5 ms

Also here:

- rules.rs: hot-swappable rule store. Built-in pack is embedded so a
  fresh install detects something before it has ever reached the
  network; on-disk packs load from $HOUNDD_RULES_DIR, /var/lib/hound
  or the XDG data dir. Reload swaps an Arc, so in-flight scans are
  never torn out from under.
- cache.rs: bounded FIFO verdict cache. Any of the four key fields
  changing means rescan, so edits, truncates and replace-by-rename all
  correctly miss.
- The goodware gate: every rule is scanned against all of /usr/bin,
  /bin and /usr/sbin in CI, and a single hit fails the build. It has
  already earned its keep — it caught a reverse-shell rule that matched
  /usr/bin/sudo, which is now removed rather than tuned. A rule that
  quarantines sudo is worse than no rule at all.
- ScanEngine is Send + Sync and selection stays per-call, so
  HOUNDD_ENGINE=clamav still reaches the legacy path for comparison.
- ScanResult.skipped reports files passed over for size instead of
  quietly counting them as clean.
- Settings gain theme (auto/light/dark), tray_icon_style (color/mono),
  close_to_tray and confirm_quit, normalised daemon-side because
  clients are not trusted to send a theme we can render.

57 tests pass, up from 29.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 22:05:09 -05:00

97 lines
3.2 KiB
Text

/*
* Hound built-in starter pack.
*
* Deliberately tiny and deliberately tight. Every rule here requires
* several independent strings before it fires, because a false positive
* in an antivirus is worse than a miss — one rule that quarantines a
* system binary ends the product.
*
* The real corpus lands in Phase 3 (the signed Hound Linux pack, gated
* behind the goodware CI regression suite). This pack exists so a fresh
* install detects *something* before it has ever contacted the network.
*/
rule EICAR_Test_File
{
meta:
name = "EICAR-Test-Signature"
severity = "info"
desc = "Industry-standard antivirus test file. Harmless."
strings:
$eicar = "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*"
condition:
$eicar
}
rule Linux_Coinminer_XMRig
{
meta:
name = "Linux.Coinminer.XMRig"
severity = "critical"
desc = "XMRig cryptocurrency miner. Requires pool protocol plus two config keys."
strings:
$pool1 = "stratum+tcp://" ascii
$pool2 = "stratum+ssl://" ascii
$cfg1 = "donate-level" ascii
$cfg2 = "rig-id" ascii
$cfg3 = "randomx" ascii nocase
$name = "xmrig" ascii nocase
condition:
($pool1 or $pool2) and 2 of ($cfg*) and $name
}
rule Linux_Webshell_PHP_Eval
{
meta:
name = "Linux.Webshell.PHP-Eval"
severity = "critical"
desc = "PHP webshell: request-driven eval of decoded input."
strings:
$php = "<?php"
$eval1 = /eval\s*\(\s*(base64_decode|gzinflate|str_rot13|gzuncompress)\s*\(/
$src1 = "$_POST"
$src2 = "$_GET"
$src3 = "$_REQUEST"
$src4 = "$_COOKIE"
condition:
$php and $eval1 and 1 of ($src*)
}
rule Linux_Rootkit_Preload
{
meta:
name = "Linux.Rootkit.Preload"
severity = "critical"
desc = "LD_PRELOAD userland rootkit: hooks libc lookup calls and hides itself."
strings:
$dlsym = "dlsym" ascii
$libc = "RTLD_NEXT" ascii
$hook1 = "readdir64" ascii
$hook2 = "readdir" ascii
$hook3 = "lxstat" ascii
$hook4 = "fopen" ascii
$hide1 = "ld.so.preload" ascii
$hide2 = "/proc/net/tcp" ascii
condition:
uint32(0) == 0x464c457f // ELF magic
and $dlsym and $libc
and 2 of ($hook*)
and 1 of ($hide*)
}
/*
* REMOVED: Linux_Backdoor_ReverseShell_ELF
*
* It required an ELF containing "/bin/sh" plus four of
* {dup2, socket, connect, inet_addr, execve}. That is a perfect
* description of a reverse shell and also a perfect description of
* /usr/bin/sudo, which the goodware test caught immediately. Any
* dynamically linked network-capable binary imports those symbols
* legitimately, so no threshold tweak saves this rule — it would only
* move the false positive to a different binary on a different distro.
*
* Catching reverse shells properly needs either ELF structure (statically
* linked, tiny, no libc) or the behaviour itself, which is Phase 7's job.
* Left out rather than shipped loose: a rule that quarantines sudo is
* worse than no rule at all.
*/