"""Detector registry. Each detector is a ``Detector`` with a ``name`` and a ``detect(state, cfg)`` callable yielding ``Alert``s. They are pure functions of the injected ``SystemState`` + ``Config``, which is what makes them unit-testable without root or a live system. """ from __future__ import annotations from collections.abc import Callable, Iterable, Iterator from dataclasses import dataclass from ..alert import Alert from ..config import Config from ..system import SystemState from . import ( deleted_exe, egress, ld_preload, new_listener, new_suid, persistence, reverse_shell, ) DetectFn = Callable[[SystemState, Config], Iterable[Alert]] @dataclass(frozen=True) class Detector: name: str detect: DetectFn needs_baseline: bool = False # gated until after the grace window expensive: bool = False # gated to the slow scan cadence # Order here is the order signatures appear in a snapshot. REGISTRY: tuple[Detector, ...] = ( Detector("reverse_shell", reverse_shell.detect), Detector("ld_preload", ld_preload.detect), Detector("deleted_exe", deleted_exe.detect), Detector("egress", egress.detect), Detector("new_listener", new_listener.detect, needs_baseline=True), Detector("persistence", persistence.detect, needs_baseline=True), Detector("new_suid", new_suid.detect, needs_baseline=True, expensive=True), ) def run_all( state: SystemState, cfg: Config, *, include: Iterable[str] | None = None, ) -> Iterator[Alert]: """Run the enabled detectors whose names are in ``include`` (or all).""" wanted = set(include) if include is not None else None for det in REGISTRY: if not cfg.enabled(det.name): continue if wanted is not None and det.name not in wanted: continue yield from det.detect(state, cfg)