Add typed host event egress rule
This commit is contained in:
parent
0b010df514
commit
3b037646d2
15 changed files with 360 additions and 31 deletions
|
|
@ -9,6 +9,8 @@ from typing import Any
|
|||
from .alert import Alert
|
||||
from .config import Config
|
||||
from .events.exec_event import ExecEvent
|
||||
from .events.host_event import HostEvent
|
||||
from .events.host_rules import DEFAULT_HOST_RULES, HostRule, HostRuleEngine
|
||||
from .events.rules import DEFAULT_EXEC_RULES, ExecRule, ExecRuleEngine
|
||||
from .events.syscall_event import SyscallEvent
|
||||
from .events.syscall_rules import (
|
||||
|
|
@ -22,6 +24,7 @@ def list_rules(cfg: Config) -> list[dict[str, Any]]:
|
|||
"""Return built-in and configured event rules as stable dictionaries."""
|
||||
exec_builtin = {r.sid for r in DEFAULT_EXEC_RULES}
|
||||
syscall_builtin = {r.sid for r in DEFAULT_SYSCALL_RULES}
|
||||
host_builtin = {r.sid for r in DEFAULT_HOST_RULES}
|
||||
records = [
|
||||
_exec_rule_record(r, "builtin" if r.sid in exec_builtin else "configured")
|
||||
for r in ExecRuleEngine.load(cfg.exec_rules_file).rules
|
||||
|
|
@ -30,6 +33,10 @@ def list_rules(cfg: Config) -> list[dict[str, Any]]:
|
|||
_syscall_rule_record(r, "builtin" if r.sid in syscall_builtin else "configured")
|
||||
for r in SyscallRuleEngine().rules
|
||||
)
|
||||
records.extend(
|
||||
_host_rule_record(r, "builtin" if r.sid in host_builtin else "configured")
|
||||
for r in HostRuleEngine().rules
|
||||
)
|
||||
return sorted(records, key=lambda r: (int(r["sid"]), str(r["event"])))
|
||||
|
||||
|
||||
|
|
@ -45,8 +52,8 @@ def render_markdown(cfg: Config) -> str:
|
|||
lines = [
|
||||
"# Enodia Sentinel Event Rule Reference",
|
||||
"",
|
||||
"Generated from the active exec/syscall rule defaults plus any configured "
|
||||
"`exec_rules_file` entries.",
|
||||
"Generated from the active exec/syscall/host-event rule defaults plus "
|
||||
"any configured `exec_rules_file` entries.",
|
||||
"",
|
||||
"Use `enodia-sentinel rules list/show/test` to inspect rules and validate "
|
||||
"event fixtures locally.",
|
||||
|
|
@ -98,7 +105,9 @@ def test_event(cfg: Config, event: dict[str, Any]) -> list[Alert]:
|
|||
return list(ExecRuleEngine.load(cfg.exec_rules_file).match(_exec_event(event)))
|
||||
if kind == "syscall":
|
||||
return list(SyscallRuleEngine().match(_syscall_event(event)))
|
||||
raise ValueError("event JSON must be an exec or syscall event")
|
||||
if kind == "host":
|
||||
return list(HostRuleEngine().match(_host_event(event)))
|
||||
raise ValueError("event JSON must be an exec, syscall, or host event")
|
||||
|
||||
|
||||
def _exec_rule_record(rule: ExecRule, origin: str) -> dict[str, Any]:
|
||||
|
|
@ -141,6 +150,34 @@ def _syscall_rule_record(rule: SyscallRule, origin: str) -> dict[str, Any]:
|
|||
}
|
||||
|
||||
|
||||
def _host_rule_record(rule: HostRule, origin: str) -> dict[str, Any]:
|
||||
conditions: dict[str, Any] = {
|
||||
"events": sorted(rule.events),
|
||||
}
|
||||
if rule.comm:
|
||||
conditions["comm"] = sorted(rule.comm)
|
||||
if rule.parent_comm:
|
||||
conditions["parent_comm"] = sorted(rule.parent_comm)
|
||||
if rule.peer_public is not None:
|
||||
conditions["peer_public"] = rule.peer_public
|
||||
if rule.peer_ports:
|
||||
conditions["peer_ports"] = sorted(rule.peer_ports)
|
||||
if rule.peer_port_exclude:
|
||||
conditions["peer_port_exclude"] = sorted(rule.peer_port_exclude)
|
||||
if rule.argv_regex:
|
||||
conditions["argv_regex"] = rule.argv_regex
|
||||
return {
|
||||
"sid": rule.sid,
|
||||
"event": ",".join(sorted(rule.events)),
|
||||
"origin": origin,
|
||||
"severity": str(rule.severity),
|
||||
"classtype": rule.classtype,
|
||||
"signature": f"host_rule.{rule.classtype}",
|
||||
"msg": rule.msg,
|
||||
"conditions": conditions,
|
||||
}
|
||||
|
||||
|
||||
_RULE_DOCS: dict[int, dict[str, Any]] = {
|
||||
100001: {
|
||||
"false_positives": [
|
||||
|
|
@ -219,6 +256,13 @@ _RULE_DOCS: dict[int, dict[str, Any]] = {
|
|||
],
|
||||
"drill": "Use `rules test` with syscall `mlock`, `mlock2`, or `mlockall`.",
|
||||
},
|
||||
100067: {
|
||||
"false_positives": [
|
||||
"Interactive admin scripts that intentionally connect to a non-standard public service.",
|
||||
"Developer tooling using interpreters for custom APIs on high ports.",
|
||||
],
|
||||
"drill": "Use `rules test` with a `tcp_connect` event whose `comm` is an interpreter and whose public `peer_port` is not a common service port.",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -246,6 +290,11 @@ def _event_kind(event: dict[str, Any]) -> str:
|
|||
return "exec"
|
||||
if explicit in {"syscall", "sys"}:
|
||||
return "syscall"
|
||||
if explicit in {
|
||||
"tcp_connect", "bind", "listen", "accept", "file_write",
|
||||
"chmod", "chown", "setuid", "setgid", "capset", "module_load",
|
||||
}:
|
||||
return "host"
|
||||
if "filename" in event or "argv" in event or "parent_comm" in event:
|
||||
return "exec"
|
||||
if "syscall" in event or "args" in event:
|
||||
|
|
@ -283,6 +332,23 @@ def _syscall_event(event: dict[str, Any]) -> SyscallEvent:
|
|||
)
|
||||
|
||||
|
||||
def _host_event(event: dict[str, Any]) -> HostEvent:
|
||||
return HostEvent(
|
||||
event=str(event.get("event") or event.get("type") or ""),
|
||||
pid=_to_int(event.get("pid", 0)),
|
||||
ppid=_to_int(event.get("ppid", 0)),
|
||||
uid=_to_int(event.get("uid", 0)),
|
||||
comm=str(event.get("comm", "")),
|
||||
parent_comm=str(event.get("parent_comm", "")),
|
||||
path=str(event.get("path", "")),
|
||||
argv=tuple(str(a) for a in event.get("argv", ())),
|
||||
peer_ip=str(event.get("peer_ip", event.get("remote_ip", ""))),
|
||||
peer_port=_to_int(event.get("peer_port", event.get("remote_port", 0))),
|
||||
local_ip=str(event.get("local_ip", "")),
|
||||
local_port=_to_int(event.get("local_port", 0)),
|
||||
)
|
||||
|
||||
|
||||
def _to_int(value: Any) -> int:
|
||||
if isinstance(value, str):
|
||||
return int(value, 0)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue