enodia-sentinal/enodia_sentinel/detectors/__init__.py

69 lines
2.1 KiB
Python

# 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,
input_snooper,
ld_preload,
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("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)