# SPDX-License-Identifier: GPL-3.0-or-later """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 ( credential_access, deleted_exe, egress, first_seen, input_snooper, ld_preload, memory_obfuscation, new_listener, new_suid, persistence, reverse_shell, stealth_network, ) 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("input_snooper", input_snooper.detect), Detector("credential_access", credential_access.detect), Detector("stealth_network", stealth_network.detect), Detector("memory_obfuscation", memory_obfuscation.detect), Detector("egress", egress.detect), Detector("first_seen", first_seen.detect, needs_baseline=True), 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 if det.needs_baseline and state.listener_baseline is None: continue yield from det.detect(state, cfg)