Antivirus/crates/houndd/rules/hound-builtin.yar
Hound 020a1fa8bd rules: a rule must earn the right to move somebody's file
Quarantine deletes a file from where its owner put it. Until now every
detection did that, so every false positive was destructive rather than
merely wrong — which on this machine cost an 8.5 MB compiler cache and a
4.3 MB session transcript, the latter's history permanently.

Each rule now declares what Hound may do:

    action = "quarantine"   move it to the vault
    action = "alert"        report it, leave it alone

**The default is alert**, and so is an unrecognised value, and so is a
detection name the engine does not know. One misspelt "quarantne" must
not turn an advisory rule into a destructive one across every machine
that updates.

Quarantine has to be earned by an ANCHOR, not by the author's
confidence:

  EICAR-Test-Signature    quarantine  exact 68-byte payload, size-bounded
  Linux.Coinminer.XMRig   quarantine  ELF magic
  Linux.Rootkit.Preload   quarantine  ELF magic
  Linux.Webshell.PHP-Eval ALERT       content-only — PHP has no file
                                      magic, so it can still match a
                                      security write-up, a log or an AI
                                      transcript quoting a webshell

A test asserts that property directly: any rule declaring quarantine
must contain a file-type check or an exact size bound. A future rule
cannot quietly claim the destructive action without one.

Both the execution gate and the inotify fallback consult it, kept in
step deliberately — a fallback more destructive than the primary path is
a trap for whoever ends up running unprivileged.

Verified live on the gated filesystem: a webshell written to disk is
reported and left in place; an ELF miner written beside it is
quarantined. Event text changed to match — "threat detected in X —
reported, not moved" rather than implying something happened.

One process note. The first attempt at this edit silently did nothing:
the replacement did not match because of indentation, the tooling
reported success, and the webshell was still moved. Second time I made
the edit assert its anchor before applying. That is the third silent
no-op edit in this session and the pattern is now obvious enough to
stop assuming an edit landed.

303 tests pass. Gate off.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 08:48:27 -05:00

157 lines
6.2 KiB
Text

/*
* Hound built-in starter pack.
*
* ── the `action` field ──
*
* Every rule declares what Hound may do when it matches:
*
* action = "quarantine" move the file to the vault
* action = "alert" report it and leave it alone
*
* **The default is "alert".** A rule that forgets to declare gets the
* non-destructive behaviour, because the failure mode of guessing wrong
* is somebody's file disappearing.
*
* Only a rule that cannot plausibly match a document deserves
* "quarantine": one anchored to a file type (ELF magic) or pinned to an
* exact, size-bounded payload. A rule matching loose text — a webshell
* pattern, a suspicious string — alerts, however confident it looks,
* because text appears inside logs, transcripts, build caches and
* documentation about the very thing being detected.
*
* This is not hypothetical. Before this field existed, Hound quarantined
* an 8.5 MB compiler cache and a 4.3 MB session transcript on a live
* server, and the transcript's history was lost.
*
* 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"
// Exact 68-byte payload, size-bounded below. It cannot match
// anything that is not deliberately the EICAR file.
action = "quarantine"
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:
// The standard defines the EICAR file as exactly this 68-byte
// string, optionally padded with whitespace to at most 128 bytes.
// Without the size bound this rule matches any file that merely
// CONTAINS the string — and on a live server it quarantined an
// 8.5 MB rustc incremental-compilation cache, because the test
// source being compiled contained the literal. That killed the
// build with a compiler panic.
//
// Anyone whose source, logs or documentation mention EICAR has
// the same problem, which is most security work.
filesize <= 128 and $eicar
}
rule Linux_Coinminer_XMRig
{
meta:
name = "Linux.Coinminer.XMRig"
severity = "critical"
// ELF-anchored: a document about mining cannot match.
action = "quarantine"
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:
// ELF magic is not optional here.
//
// Without it this rule matches any TEXT that mentions mining:
// a blog post, a support ticket, a threat-intelligence report,
// or — as happened on a live server — an AI session transcript
// in which somebody was writing this very rule. Malware is a
// program; a document about malware is not.
uint32(0) == 0x464c457f
and ($pool1 or $pool2) and 2 of ($cfg*) and $name
}
rule Linux_Webshell_PHP_Eval
{
meta:
name = "Linux.Webshell.PHP-Eval"
severity = "critical"
// Content-only. PHP has no file magic, so this can still match a
// document that quotes a webshell — a security write-up, a log,
// an AI transcript. It reports; it does not move anybody's file.
action = "alert"
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:
// A webshell is a PHP file: small, and opening with a PHP tag.
// Without those bounds this matched a 4.3 MB AI session
// transcript on a live server — the conversation happened to
// discuss webshells, so it contained "<?php", the eval pattern
// and "$_POST". The transcript was moved to quarantine and its
// history lost.
filesize < 1MB
and $php in (0..4096)
and $eval1
and 1 of ($src*)
}
rule Linux_Rootkit_Preload
{
meta:
name = "Linux.Rootkit.Preload"
severity = "critical"
// ELF-anchored.
action = "quarantine"
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.
*/