Add host event rules for bind and capability activity
This commit is contained in:
parent
b40ac4252c
commit
9784033f86
12 changed files with 316 additions and 8 deletions
|
|
@ -136,10 +136,9 @@ Purpose: reduce blind spots and connect related signals.
|
||||||
flags interpreters opening listeners on unusual local ports, with local-port
|
flags interpreters opening listeners on unusual local ports, with local-port
|
||||||
match conditions and a replayable fixture.
|
match conditions and a replayable fixture.
|
||||||
- ✅ Continue generalizing the rule engine across more typed host events:
|
- ✅ Continue generalizing the rule engine across more typed host events:
|
||||||
`file_write`, `chmod/chown`, and `setuid/setgid` now have built-in rules,
|
`file_write`, `chmod/chown`, `setuid/setgid`, `bind`, `accept`, `capset`,
|
||||||
typed event parsing, fixtures, and rule-operation tests.
|
and module-load now have built-in rules, typed event parsing, fixtures, and
|
||||||
- Continue extending typed host-event coverage to:
|
rule-operation tests.
|
||||||
`bind`, `accept`, `capset`, and module-load events.
|
|
||||||
- Keep rules declarative and Snort-like:
|
- Keep rules declarative and Snort-like:
|
||||||
`sid`, `msg`, `severity`, `classtype`, `event`, and match fields such as
|
`sid`, `msg`, `severity`, `classtype`, `event`, and match fields such as
|
||||||
process name, parent process, argv regex, path prefix, peer IP/port, UID/GID,
|
process name, parent process, argv regex, path prefix, peer IP/port, UID/GID,
|
||||||
|
|
|
||||||
|
|
@ -317,3 +317,79 @@ Expected false positives:
|
||||||
- Privileged installer or service-management scripts during maintenance windows.
|
- Privileged installer or service-management scripts during maintenance windows.
|
||||||
|
|
||||||
Drill or fixture: Use `rules test` with `tests/fixtures/sids/100072-interpreter-setgid-root.json`.
|
Drill or fixture: Use `rules test` with `tests/fixtures/sids/100072-interpreter-setgid-root.json`.
|
||||||
|
|
||||||
|
## SID 100073: Interpreter bound an unusual local port
|
||||||
|
|
||||||
|
- Event: `bind`
|
||||||
|
- Signature: `host_rule.suspicious-bind`
|
||||||
|
- Classtype: `suspicious-bind`
|
||||||
|
- Severity: `HIGH`
|
||||||
|
- Origin: `builtin`
|
||||||
|
|
||||||
|
Match fields:
|
||||||
|
- `events`: `bind`
|
||||||
|
- `comm`: `ash`, `bash`, `curl`, `dash`, `fetch`, `ksh`, `lua`, `nc`, `ncat`, `netcat`, `node`, `nodejs`, `perl`, `php`, `python`, `python2`, `python3`, `ruby`, `sh`, `socat`, `wget`, `zsh`
|
||||||
|
- `local_port_exclude`: `22`, `53`, `80`, `123`, `443`, `853`
|
||||||
|
|
||||||
|
Expected false positives:
|
||||||
|
- Developer or diagnostic scripts binding high local ports intentionally.
|
||||||
|
- Short-lived local service wrappers that bind before handing sockets to a supervised process.
|
||||||
|
|
||||||
|
Drill or fixture: Use `rules test` with `tests/fixtures/sids/100073-interpreter-unusual-bind.json`.
|
||||||
|
|
||||||
|
## SID 100076: Interpreter accepted inbound traffic on an unusual local port
|
||||||
|
|
||||||
|
- Event: `accept`
|
||||||
|
- Signature: `host_rule.suspicious-accept`
|
||||||
|
- Classtype: `suspicious-accept`
|
||||||
|
- Severity: `HIGH`
|
||||||
|
- Origin: `builtin`
|
||||||
|
|
||||||
|
Match fields:
|
||||||
|
- `events`: `accept`
|
||||||
|
- `comm`: `ash`, `bash`, `curl`, `dash`, `fetch`, `ksh`, `lua`, `nc`, `ncat`, `netcat`, `node`, `nodejs`, `perl`, `php`, `python`, `python2`, `python3`, `ruby`, `sh`, `socat`, `wget`, `zsh`
|
||||||
|
- `peer_public`: `True`
|
||||||
|
- `local_port_exclude`: `22`, `53`, `80`, `123`, `443`, `853`
|
||||||
|
|
||||||
|
Expected false positives:
|
||||||
|
- Developer servers or test harnesses accepting inbound public traffic from an interpreter.
|
||||||
|
- Administrative troubleshooting with temporary netcat/socat listeners.
|
||||||
|
|
||||||
|
Drill or fixture: Use `rules test` with `tests/fixtures/sids/100076-interpreter-unusual-accept.json`.
|
||||||
|
|
||||||
|
## SID 100077: Interpreter requested sensitive Linux capabilities
|
||||||
|
|
||||||
|
- Event: `capset`
|
||||||
|
- Signature: `host_rule.capability-escalation`
|
||||||
|
- Classtype: `capability-escalation`
|
||||||
|
- Severity: `HIGH`
|
||||||
|
- Origin: `builtin`
|
||||||
|
|
||||||
|
Match fields:
|
||||||
|
- `events`: `capset`
|
||||||
|
- `comm`: `ash`, `bash`, `curl`, `dash`, `fetch`, `ksh`, `lua`, `nc`, `ncat`, `netcat`, `node`, `nodejs`, `perl`, `php`, `python`, `python2`, `python3`, `ruby`, `sh`, `socat`, `wget`, `zsh`
|
||||||
|
- `capabilities_any`: `CAP_DAC_READ_SEARCH`, `CAP_NET_ADMIN`, `CAP_NET_RAW`, `CAP_SYS_ADMIN`, `CAP_SYS_MODULE`, `CAP_SYS_PTRACE`
|
||||||
|
|
||||||
|
Expected false positives:
|
||||||
|
- Privileged maintenance scripts that intentionally adjust capabilities during controlled administration.
|
||||||
|
- Container or network lab setup scripts using interpreters to configure namespaces or packet capture.
|
||||||
|
|
||||||
|
Drill or fixture: Use `rules test` with `tests/fixtures/sids/100077-interpreter-capset-sensitive.json`.
|
||||||
|
|
||||||
|
## SID 100078: Kernel module load requested from a writable runtime path
|
||||||
|
|
||||||
|
- Event: `module_load`
|
||||||
|
- Signature: `host_rule.suspicious-module-load`
|
||||||
|
- Classtype: `suspicious-module-load`
|
||||||
|
- Severity: `CRITICAL`
|
||||||
|
- Origin: `builtin`
|
||||||
|
|
||||||
|
Match fields:
|
||||||
|
- `events`: `module_load`
|
||||||
|
- `path_prefixes`: `/tmp/`, `/var/tmp/`, `/dev/shm/`, `/run/user/`
|
||||||
|
|
||||||
|
Expected false positives:
|
||||||
|
- Kernel development labs loading locally built modules from temporary build directories.
|
||||||
|
- Driver troubleshooting sessions that intentionally test an unsigned module from a writable path.
|
||||||
|
|
||||||
|
Drill or fixture: Use `rules test` with `tests/fixtures/sids/100078-module-load-writable-path.json`.
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ class HostEvent:
|
||||||
target_uid: int = -1
|
target_uid: int = -1
|
||||||
target_gid: int = -1
|
target_gid: int = -1
|
||||||
mode: int = 0
|
mode: int = 0
|
||||||
|
capabilities: tuple[str, ...] = field(default_factory=tuple)
|
||||||
|
module_name: str = ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def argv_str(self) -> str:
|
def argv_str(self) -> str:
|
||||||
|
|
@ -44,4 +46,6 @@ class HostEvent:
|
||||||
"target_uid": self.target_uid,
|
"target_uid": self.target_uid,
|
||||||
"target_gid": self.target_gid,
|
"target_gid": self.target_gid,
|
||||||
"mode": self.mode,
|
"mode": self.mode,
|
||||||
|
"capabilities": list(self.capabilities),
|
||||||
|
"module_name": self.module_name,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,15 @@ _PERSISTENCE_PREFIXES = (
|
||||||
"/etc/ld.so.preload", "/root/.ssh/authorized_keys",
|
"/etc/ld.so.preload", "/root/.ssh/authorized_keys",
|
||||||
"/etc/sudoers", "/etc/sudoers.d/",
|
"/etc/sudoers", "/etc/sudoers.d/",
|
||||||
)
|
)
|
||||||
|
_WRITABLE_RUNTIME_PREFIXES = ("/tmp/", "/var/tmp/", "/dev/shm/", "/run/user/")
|
||||||
|
_SENSITIVE_CAPABILITIES = frozenset({
|
||||||
|
"CAP_SYS_ADMIN",
|
||||||
|
"CAP_SYS_MODULE",
|
||||||
|
"CAP_SYS_PTRACE",
|
||||||
|
"CAP_DAC_READ_SEARCH",
|
||||||
|
"CAP_NET_ADMIN",
|
||||||
|
"CAP_NET_RAW",
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
|
|
@ -40,6 +49,8 @@ class HostRule:
|
||||||
path_prefixes: tuple[str, ...] = ()
|
path_prefixes: tuple[str, ...] = ()
|
||||||
target_uids: frozenset[int] = frozenset()
|
target_uids: frozenset[int] = frozenset()
|
||||||
target_gids: frozenset[int] = frozenset()
|
target_gids: frozenset[int] = frozenset()
|
||||||
|
capabilities_any: frozenset[str] = frozenset()
|
||||||
|
module_names: frozenset[str] = frozenset()
|
||||||
argv_regex: str | None = None
|
argv_regex: str | None = None
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
|
|
@ -49,7 +60,8 @@ class HostRule:
|
||||||
self.comm, self.parent_comm, self.peer_public is not None,
|
self.comm, self.parent_comm, self.peer_public is not None,
|
||||||
self.peer_ports, self.peer_port_exclude, self.local_ports,
|
self.peer_ports, self.peer_port_exclude, self.local_ports,
|
||||||
self.local_port_exclude, self.path_prefixes, self.target_uids,
|
self.local_port_exclude, self.path_prefixes, self.target_uids,
|
||||||
self.target_gids, self.argv_regex,
|
self.target_gids, self.capabilities_any, self.module_names,
|
||||||
|
self.argv_regex,
|
||||||
)):
|
)):
|
||||||
raise ValueError(f"rule sid={self.sid} has no match conditions")
|
raise ValueError(f"rule sid={self.sid} has no match conditions")
|
||||||
if self.argv_regex is not None:
|
if self.argv_regex is not None:
|
||||||
|
|
@ -84,6 +96,12 @@ class HostRule:
|
||||||
return False
|
return False
|
||||||
if self.target_gids and ev.target_gid not in self.target_gids:
|
if self.target_gids and ev.target_gid not in self.target_gids:
|
||||||
return False
|
return False
|
||||||
|
if self.capabilities_any and not (
|
||||||
|
{cap.upper() for cap in ev.capabilities} & self.capabilities_any
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
if self.module_names and ev.module_name not in self.module_names:
|
||||||
|
return False
|
||||||
if self._argv_re is not None and not self._argv_re.search(ev.argv_str):
|
if self._argv_re is not None and not self._argv_re.search(ev.argv_str):
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
@ -95,7 +113,8 @@ class HostRule:
|
||||||
key=(
|
key=(
|
||||||
f"host:{self.sid}:{ev.event}:{ev.pid}:"
|
f"host:{self.sid}:{ev.event}:{ev.pid}:"
|
||||||
f"{ev.peer_ip}:{ev.peer_port}:{ev.local_ip}:{ev.local_port}:"
|
f"{ev.peer_ip}:{ev.peer_port}:{ev.local_ip}:{ev.local_port}:"
|
||||||
f"{ev.path}:{ev.target_uid}:{ev.target_gid}:{ev.mode}"
|
f"{ev.path}:{ev.target_uid}:{ev.target_gid}:{ev.mode}:"
|
||||||
|
f"{','.join(ev.capabilities)}:{ev.module_name}"
|
||||||
),
|
),
|
||||||
detail=(
|
detail=(
|
||||||
f"sid={self.sid} {self.msg} - pid={ev.pid} ppid={ev.ppid} "
|
f"sid={self.sid} {self.msg} - pid={ev.pid} ppid={ev.ppid} "
|
||||||
|
|
@ -103,6 +122,8 @@ class HostRule:
|
||||||
f"local={ev.local_ip}:{ev.local_port} path={ev.path} "
|
f"local={ev.local_ip}:{ev.local_port} path={ev.path} "
|
||||||
f"target_uid={ev.target_uid} target_gid={ev.target_gid} "
|
f"target_uid={ev.target_uid} target_gid={ev.target_gid} "
|
||||||
f"mode={oct(ev.mode) if ev.mode else '-'} "
|
f"mode={oct(ev.mode) if ev.mode else '-'} "
|
||||||
|
f"capabilities={','.join(ev.capabilities) or '-'} "
|
||||||
|
f"module={ev.module_name or '-'}"
|
||||||
),
|
),
|
||||||
pids=(ev.pid,),
|
pids=(ev.pid,),
|
||||||
sid=self.sid,
|
sid=self.sid,
|
||||||
|
|
@ -130,6 +151,15 @@ DEFAULT_HOST_RULES: tuple[HostRule, ...] = (
|
||||||
comm=_INTERPRETERS,
|
comm=_INTERPRETERS,
|
||||||
local_port_exclude=_COMMON_PUBLIC_PORTS,
|
local_port_exclude=_COMMON_PUBLIC_PORTS,
|
||||||
),
|
),
|
||||||
|
HostRule(
|
||||||
|
sid=100073,
|
||||||
|
msg="Interpreter bound an unusual local port",
|
||||||
|
severity=Severity.HIGH,
|
||||||
|
classtype="suspicious-bind",
|
||||||
|
events=frozenset({"bind"}),
|
||||||
|
comm=_INTERPRETERS,
|
||||||
|
local_port_exclude=_COMMON_PUBLIC_PORTS,
|
||||||
|
),
|
||||||
HostRule(
|
HostRule(
|
||||||
sid=100069,
|
sid=100069,
|
||||||
msg="Interpreter wrote to a persistence path",
|
msg="Interpreter wrote to a persistence path",
|
||||||
|
|
@ -165,6 +195,33 @@ DEFAULT_HOST_RULES: tuple[HostRule, ...] = (
|
||||||
comm=_INTERPRETERS,
|
comm=_INTERPRETERS,
|
||||||
target_gids=frozenset({0}),
|
target_gids=frozenset({0}),
|
||||||
),
|
),
|
||||||
|
HostRule(
|
||||||
|
sid=100076,
|
||||||
|
msg="Interpreter accepted inbound traffic on an unusual local port",
|
||||||
|
severity=Severity.HIGH,
|
||||||
|
classtype="suspicious-accept",
|
||||||
|
events=frozenset({"accept"}),
|
||||||
|
comm=_INTERPRETERS,
|
||||||
|
peer_public=True,
|
||||||
|
local_port_exclude=_COMMON_PUBLIC_PORTS,
|
||||||
|
),
|
||||||
|
HostRule(
|
||||||
|
sid=100077,
|
||||||
|
msg="Interpreter requested sensitive Linux capabilities",
|
||||||
|
severity=Severity.HIGH,
|
||||||
|
classtype="capability-escalation",
|
||||||
|
events=frozenset({"capset"}),
|
||||||
|
comm=_INTERPRETERS,
|
||||||
|
capabilities_any=_SENSITIVE_CAPABILITIES,
|
||||||
|
),
|
||||||
|
HostRule(
|
||||||
|
sid=100078,
|
||||||
|
msg="Kernel module load requested from a writable runtime path",
|
||||||
|
severity=Severity.CRITICAL,
|
||||||
|
classtype="suspicious-module-load",
|
||||||
|
events=frozenset({"module_load"}),
|
||||||
|
path_prefixes=_WRITABLE_RUNTIME_PREFIXES,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -174,6 +174,10 @@ def _host_rule_record(rule: HostRule, origin: str) -> dict[str, Any]:
|
||||||
conditions["target_uids"] = sorted(rule.target_uids)
|
conditions["target_uids"] = sorted(rule.target_uids)
|
||||||
if rule.target_gids:
|
if rule.target_gids:
|
||||||
conditions["target_gids"] = sorted(rule.target_gids)
|
conditions["target_gids"] = sorted(rule.target_gids)
|
||||||
|
if rule.capabilities_any:
|
||||||
|
conditions["capabilities_any"] = sorted(rule.capabilities_any)
|
||||||
|
if rule.module_names:
|
||||||
|
conditions["module_names"] = sorted(rule.module_names)
|
||||||
if rule.argv_regex:
|
if rule.argv_regex:
|
||||||
conditions["argv_regex"] = rule.argv_regex
|
conditions["argv_regex"] = rule.argv_regex
|
||||||
return {
|
return {
|
||||||
|
|
@ -280,6 +284,34 @@ _RULE_DOCS: dict[int, dict[str, Any]] = {
|
||||||
],
|
],
|
||||||
"drill": "Use `rules test` with a `listen` event whose `comm` is an interpreter and whose `local_port` is not a common service port.",
|
"drill": "Use `rules test` with a `listen` event whose `comm` is an interpreter and whose `local_port` is not a common service port.",
|
||||||
},
|
},
|
||||||
|
100073: {
|
||||||
|
"false_positives": [
|
||||||
|
"Developer or diagnostic scripts binding high local ports intentionally.",
|
||||||
|
"Short-lived local service wrappers that bind before handing sockets to a supervised process.",
|
||||||
|
],
|
||||||
|
"drill": "Use `rules test` with a `bind` event whose `comm` is an interpreter and whose `local_port` is not a common service port.",
|
||||||
|
},
|
||||||
|
100076: {
|
||||||
|
"false_positives": [
|
||||||
|
"Developer servers or test harnesses accepting inbound public traffic from an interpreter.",
|
||||||
|
"Administrative troubleshooting with temporary netcat/socat listeners.",
|
||||||
|
],
|
||||||
|
"drill": "Use `rules test` with an `accept` event whose `comm` is an interpreter, public `peer_ip`, and unusual `local_port`.",
|
||||||
|
},
|
||||||
|
100077: {
|
||||||
|
"false_positives": [
|
||||||
|
"Privileged maintenance scripts that intentionally adjust capabilities during controlled administration.",
|
||||||
|
"Container or network lab setup scripts using interpreters to configure namespaces or packet capture.",
|
||||||
|
],
|
||||||
|
"drill": "Use `rules test` with a `capset` event containing a sensitive capability such as `CAP_SYS_ADMIN`.",
|
||||||
|
},
|
||||||
|
100078: {
|
||||||
|
"false_positives": [
|
||||||
|
"Kernel development labs loading locally built modules from temporary build directories.",
|
||||||
|
"Driver troubleshooting sessions that intentionally test an unsigned module from a writable path.",
|
||||||
|
],
|
||||||
|
"drill": "Use `rules test` with a `module_load` event whose `path` is under `/tmp`, `/var/tmp`, `/dev/shm`, or `/run/user`.",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -368,6 +400,11 @@ def _host_event(event: dict[str, Any]) -> HostEvent:
|
||||||
target_uid=_to_int(event.get("target_uid", event.get("uid_target", -1))),
|
target_uid=_to_int(event.get("target_uid", event.get("uid_target", -1))),
|
||||||
target_gid=_to_int(event.get("target_gid", event.get("gid_target", -1))),
|
target_gid=_to_int(event.get("target_gid", event.get("gid_target", -1))),
|
||||||
mode=_to_int(event.get("mode", 0)),
|
mode=_to_int(event.get("mode", 0)),
|
||||||
|
capabilities=_to_str_tuple(event.get(
|
||||||
|
"capabilities",
|
||||||
|
event.get("caps", event.get("cap_effective", event.get("capability", ()))),
|
||||||
|
)),
|
||||||
|
module_name=str(event.get("module_name", event.get("name", ""))),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -375,3 +412,11 @@ def _to_int(value: Any) -> int:
|
||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
return int(value, 0)
|
return int(value, 0)
|
||||||
return int(value)
|
return int(value)
|
||||||
|
|
||||||
|
|
||||||
|
def _to_str_tuple(value: Any) -> tuple[str, ...]:
|
||||||
|
if value is None:
|
||||||
|
return ()
|
||||||
|
if isinstance(value, str):
|
||||||
|
return (value.upper(),) if value else ()
|
||||||
|
return tuple(str(item).upper() for item in value)
|
||||||
|
|
|
||||||
9
tests/fixtures/sids/100073-interpreter-unusual-bind.json
vendored
Normal file
9
tests/fixtures/sids/100073-interpreter-unusual-bind.json
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"event": "bind",
|
||||||
|
"pid": 4242,
|
||||||
|
"ppid": 1,
|
||||||
|
"uid": 1000,
|
||||||
|
"comm": "python3",
|
||||||
|
"local_ip": "0.0.0.0",
|
||||||
|
"local_port": 4444
|
||||||
|
}
|
||||||
11
tests/fixtures/sids/100076-interpreter-unusual-accept.json
vendored
Normal file
11
tests/fixtures/sids/100076-interpreter-unusual-accept.json
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"event": "accept",
|
||||||
|
"pid": 4242,
|
||||||
|
"ppid": 1,
|
||||||
|
"uid": 1000,
|
||||||
|
"comm": "python3",
|
||||||
|
"peer_ip": "8.8.8.8",
|
||||||
|
"peer_port": 51515,
|
||||||
|
"local_ip": "0.0.0.0",
|
||||||
|
"local_port": 4444
|
||||||
|
}
|
||||||
8
tests/fixtures/sids/100077-interpreter-capset-sensitive.json
vendored
Normal file
8
tests/fixtures/sids/100077-interpreter-capset-sensitive.json
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"event": "capset",
|
||||||
|
"pid": 4242,
|
||||||
|
"ppid": 1,
|
||||||
|
"uid": 1000,
|
||||||
|
"comm": "python3",
|
||||||
|
"capabilities": ["CAP_SYS_ADMIN"]
|
||||||
|
}
|
||||||
9
tests/fixtures/sids/100078-module-load-writable-path.json
vendored
Normal file
9
tests/fixtures/sids/100078-module-load-writable-path.json
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"event": "module_load",
|
||||||
|
"pid": 4242,
|
||||||
|
"ppid": 1,
|
||||||
|
"uid": 0,
|
||||||
|
"comm": "insmod",
|
||||||
|
"path": "/tmp/evil.ko",
|
||||||
|
"module_name": "evil"
|
||||||
|
}
|
||||||
|
|
@ -124,6 +124,44 @@ class TestRuleEventCompatibility(unittest.TestCase):
|
||||||
"target_uid": "0",
|
"target_uid": "0",
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
def test_host_event_accepts_bind_accept_capset_and_module_shapes(self):
|
||||||
|
self.assertIn(100073, self._sids({
|
||||||
|
"type": "bind",
|
||||||
|
"pid": "4242",
|
||||||
|
"ppid": "1",
|
||||||
|
"uid": "1000",
|
||||||
|
"comm": "python3",
|
||||||
|
"bind_ip": "0.0.0.0",
|
||||||
|
"bind_port": "4444",
|
||||||
|
}))
|
||||||
|
self.assertIn(100076, self._sids({
|
||||||
|
"event": "accept",
|
||||||
|
"pid": 4242,
|
||||||
|
"ppid": 1,
|
||||||
|
"uid": 1000,
|
||||||
|
"comm": "python3",
|
||||||
|
"remote_ip": "8.8.8.8",
|
||||||
|
"remote_port": "51515",
|
||||||
|
"local_port": "4444",
|
||||||
|
}))
|
||||||
|
self.assertIn(100077, self._sids({
|
||||||
|
"event": "capset",
|
||||||
|
"pid": 4242,
|
||||||
|
"ppid": 1,
|
||||||
|
"uid": 1000,
|
||||||
|
"comm": "python3",
|
||||||
|
"capability": "CAP_SYS_ADMIN",
|
||||||
|
}))
|
||||||
|
self.assertIn(100078, self._sids({
|
||||||
|
"event": "module_load",
|
||||||
|
"pid": 4242,
|
||||||
|
"ppid": 1,
|
||||||
|
"uid": 0,
|
||||||
|
"comm": "insmod",
|
||||||
|
"path": "/tmp/evil.ko",
|
||||||
|
"name": "evil",
|
||||||
|
}))
|
||||||
|
|
||||||
def test_unknown_event_shape_still_errors(self):
|
def test_unknown_event_shape_still_errors(self):
|
||||||
with self.assertRaisesRegex(
|
with self.assertRaisesRegex(
|
||||||
ValueError, "event JSON must be an exec, syscall, or host event"
|
ValueError, "event JSON must be an exec, syscall, or host event"
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,10 @@ class TestRuleOps(unittest.TestCase):
|
||||||
self.assertIn(100070, sids)
|
self.assertIn(100070, sids)
|
||||||
self.assertIn(100071, sids)
|
self.assertIn(100071, sids)
|
||||||
self.assertIn(100072, sids)
|
self.assertIn(100072, sids)
|
||||||
|
self.assertIn(100073, sids)
|
||||||
|
self.assertIn(100076, sids)
|
||||||
|
self.assertIn(100077, sids)
|
||||||
|
self.assertIn(100078, sids)
|
||||||
exec_rule = next(r for r in rules if r["sid"] == 100002)
|
exec_rule = next(r for r in rules if r["sid"] == 100002)
|
||||||
self.assertEqual(exec_rule["event"], "exec")
|
self.assertEqual(exec_rule["event"], "exec")
|
||||||
self.assertIn("argv_regex", exec_rule["conditions"])
|
self.assertIn("argv_regex", exec_rule["conditions"])
|
||||||
|
|
@ -36,6 +40,9 @@ class TestRuleOps(unittest.TestCase):
|
||||||
write_rule = next(r for r in rules if r["sid"] == 100069)
|
write_rule = next(r for r in rules if r["sid"] == 100069)
|
||||||
self.assertEqual(write_rule["event"], "file_write")
|
self.assertEqual(write_rule["event"], "file_write")
|
||||||
self.assertIn("path_prefixes", write_rule["conditions"])
|
self.assertIn("path_prefixes", write_rule["conditions"])
|
||||||
|
cap_rule = next(r for r in rules if r["sid"] == 100077)
|
||||||
|
self.assertEqual(cap_rule["event"], "capset")
|
||||||
|
self.assertIn("capabilities_any", cap_rule["conditions"])
|
||||||
|
|
||||||
def test_configured_exec_rule_is_listed(self):
|
def test_configured_exec_rule_is_listed(self):
|
||||||
with tempfile.TemporaryDirectory() as d:
|
with tempfile.TemporaryDirectory() as d:
|
||||||
|
|
@ -135,6 +142,51 @@ exec_comm = ["id"]
|
||||||
}
|
}
|
||||||
self.assertEqual(ruleops.test_event(cfg, event), [])
|
self.assertEqual(ruleops.test_event(cfg, event), [])
|
||||||
|
|
||||||
|
def test_matches_bind_accept_capset_and_module_load_events(self):
|
||||||
|
cfg = Config()
|
||||||
|
cases = (
|
||||||
|
(100073, {
|
||||||
|
"event": "bind",
|
||||||
|
"pid": 42,
|
||||||
|
"ppid": 1,
|
||||||
|
"uid": 1000,
|
||||||
|
"comm": "python3",
|
||||||
|
"local_ip": "0.0.0.0",
|
||||||
|
"local_port": 4444,
|
||||||
|
}),
|
||||||
|
(100076, {
|
||||||
|
"event": "accept",
|
||||||
|
"pid": 42,
|
||||||
|
"ppid": 1,
|
||||||
|
"uid": 1000,
|
||||||
|
"comm": "python3",
|
||||||
|
"peer_ip": "8.8.8.8",
|
||||||
|
"peer_port": 51515,
|
||||||
|
"local_ip": "0.0.0.0",
|
||||||
|
"local_port": 4444,
|
||||||
|
}),
|
||||||
|
(100077, {
|
||||||
|
"event": "capset",
|
||||||
|
"pid": 42,
|
||||||
|
"ppid": 1,
|
||||||
|
"uid": 1000,
|
||||||
|
"comm": "python3",
|
||||||
|
"capabilities": ["CAP_SYS_ADMIN"],
|
||||||
|
}),
|
||||||
|
(100078, {
|
||||||
|
"event": "module_load",
|
||||||
|
"pid": 42,
|
||||||
|
"ppid": 1,
|
||||||
|
"uid": 0,
|
||||||
|
"comm": "insmod",
|
||||||
|
"path": "/tmp/evil.ko",
|
||||||
|
"module_name": "evil",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
for sid, event in cases:
|
||||||
|
with self.subTest(sid=sid):
|
||||||
|
self.assertIn(sid, {a.sid for a in ruleops.test_event(cfg, event)})
|
||||||
|
|
||||||
def test_render_markdown_documents_rules(self):
|
def test_render_markdown_documents_rules(self):
|
||||||
text = ruleops.render_markdown(Config())
|
text = ruleops.render_markdown(Config())
|
||||||
self.assertIn("# Enodia Sentinel Event Rule Reference", text)
|
self.assertIn("# Enodia Sentinel Event Rule Reference", text)
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ class TestRegistry(unittest.TestCase):
|
||||||
self.assertEqual(len(nums), len(set(nums)))
|
self.assertEqual(len(nums), len(set(nums)))
|
||||||
|
|
||||||
def test_count_and_helpers(self):
|
def test_count_and_helpers(self):
|
||||||
self.assertEqual(len(sids.BUILTIN_SIDS), 62)
|
self.assertEqual(len(sids.BUILTIN_SIDS), 66)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
sids.all_sids(), frozenset(s.sid for s in sids.BUILTIN_SIDS)
|
sids.all_sids(), frozenset(s.sid for s in sids.BUILTIN_SIDS)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue