Add false-positive triage via package-ownership provenance
A noisy IDS gets ignored. Adds explicit FP-handling built on provenance — a binary owned by the package manager is almost certainly legitimate (the OSSEC rootcheck / AIDE principle). - provenance.py: package_owner()/is_package_owned() via pacman/dpkg/rpm, cached; framed as confidence-raising, never proof-of-safety - triage.py: triage_alert() labels each detection likely-FP vs review with a reason (package-owned binary, loopback-only listener, allowlisted comm, …); reverse_shell/egress/exec rules are ALWAYS review (provenance can't clear a network shell); unattributable listeners are reviewed, not cleared - cli: `enodia-sentinel triage` summarizes captured alerts and suggests allowlist entries - new_listener: listener_allow_comms + optional suppress_package_owned_listeners gate (the best single knob for a desktop/seedbox running P2P apps) - tests: +10 (provenance injected); README + config documented. 65/65 pass Verified on a live seedbox: 12 detections, 11 auto-cleared as FP (qbittorrent / nicotine / kdeconnectd / the dashboard itself), 1 correctly held for review. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
c00fff224c
commit
07f5261d59
8 changed files with 332 additions and 0 deletions
73
tests/test_triage.py
Normal file
73
tests/test_triage.py
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Tests for false-positive triage (provenance injected, no package manager)."""
|
||||
import unittest
|
||||
|
||||
from enodia_sentinel.config import Config
|
||||
from enodia_sentinel.triage import LIKELY_FP, REVIEW, triage_alert
|
||||
|
||||
|
||||
def alert(sig, detail, pids=(), sid=0):
|
||||
return {"signature": sig, "detail": detail, "pids": list(pids), "sid": sid}
|
||||
|
||||
|
||||
OWNED = lambda p: p in {"/usr/bin/qbittorrent", "/usr/bin/sudo"}
|
||||
NONE = lambda p: False
|
||||
|
||||
|
||||
class TestNewListener(unittest.TestCase):
|
||||
def test_package_owned_is_fp(self):
|
||||
a = alert("new_listener", "new listening socket 1.2.3.4:6881 by comm=qbittorrent",
|
||||
pids=(50,))
|
||||
procs = [{"pid": 50, "exe": "/usr/bin/qbittorrent"}]
|
||||
v = triage_alert(a, procs, Config(), is_owned=OWNED)
|
||||
self.assertEqual(v.label, LIKELY_FP)
|
||||
self.assertIn("qbittorrent", v.suggest)
|
||||
|
||||
def test_loopback_is_fp(self):
|
||||
a = alert("new_listener", "new listening socket 127.0.0.1:9000 by comm=foo")
|
||||
v = triage_alert(a, [], Config(), is_owned=NONE)
|
||||
self.assertEqual(v.label, LIKELY_FP)
|
||||
|
||||
def test_allowlisted_comm_is_fp(self):
|
||||
c = Config()
|
||||
c.listener_allow_comms = frozenset({"nicotine"})
|
||||
a = alert("new_listener", "new listening socket 1.2.3.4:2234 by comm=nicotine")
|
||||
self.assertEqual(triage_alert(a, [], c, is_owned=NONE).label, LIKELY_FP)
|
||||
|
||||
def test_unknown_listener_is_review(self):
|
||||
a = alert("new_listener", "new listening socket 9.9.9.9:31337 by comm=weird",
|
||||
pids=(77,))
|
||||
procs = [{"pid": 77, "exe": "/tmp/weird"}]
|
||||
self.assertEqual(triage_alert(a, procs, Config(), is_owned=NONE).label, REVIEW)
|
||||
|
||||
|
||||
class TestOtherSignatures(unittest.TestCase):
|
||||
def test_reverse_shell_always_review(self):
|
||||
a = alert("reverse_shell", "pid=1 comm=bash stdio=net-socket ...")
|
||||
self.assertEqual(triage_alert(a, [], Config(), is_owned=OWNED).label, REVIEW)
|
||||
|
||||
def test_ld_preload_writable_is_review(self):
|
||||
a = alert("ld_preload", "pid=1 comm=x LD_PRELOAD=[/tmp/evil.so]")
|
||||
self.assertEqual(triage_alert(a, [], Config(), is_owned=NONE).label, REVIEW)
|
||||
|
||||
def test_suid_package_owned_is_fp(self):
|
||||
a = alert("new_suid", "new SUID/SGID binary: /usr/bin/sudo")
|
||||
self.assertEqual(triage_alert(a, [], Config(), is_owned=OWNED).label, LIKELY_FP)
|
||||
|
||||
def test_suid_unowned_is_review(self):
|
||||
a = alert("new_suid", "SUID/SGID binary in writable dir: /tmp/x")
|
||||
self.assertEqual(triage_alert(a, [], Config(), is_owned=NONE).label, REVIEW)
|
||||
|
||||
def test_deleted_exe_package_owned_is_fp(self):
|
||||
a = alert("deleted_exe", "pid=1 comm=foo exe=[/usr/bin/foo (deleted)]", pids=(1,))
|
||||
procs = [{"pid": 1, "exe": "/usr/bin/foo (deleted)"}]
|
||||
owned = lambda p: "foo" in p
|
||||
self.assertEqual(triage_alert(a, procs, Config(), is_owned=owned).label, LIKELY_FP)
|
||||
|
||||
def test_exec_rule_is_review(self):
|
||||
a = alert("exec_rule.fileless-execution", "sid=100001 ...")
|
||||
self.assertEqual(triage_alert(a, [], Config(), is_owned=OWNED).label, REVIEW)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue