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>
157 lines
6.2 KiB
Text
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.
|
|
*/
|