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>
97 lines
3.2 KiB
Text
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.
|
|
*/
|