From 3a378dcf7e801a51fd95cb47c6a4edc02c771f33 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 11 May 2026 22:36:33 +0300 Subject: [PATCH] docs(rules): add per-rule README.md for all 26 rules Each README follows the SOC-style template from the rule-documentation field design: metadata table, Description, Attack Technique, How It Works, Investigation Steps, Remediation, False Positives. Content shipped as the 'documentation' field on the rule's API representation once consumed by the armo-rulelibrary build. Refs SUB-7177. Signed-off-by: Ben --- .../README.md | 59 +++++++++++++++++ .../r0002-unexpected-file-access/README.md | 53 +++++++++++++++ .../r0003-unexpected-system-call/README.md | 49 ++++++++++++++ .../README.md | 49 ++++++++++++++ .../r0005-unexpected-domain-request/README.md | 50 ++++++++++++++ .../README.md | 48 ++++++++++++++ .../README.md | 54 +++++++++++++++ .../README.md | 49 ++++++++++++++ pkg/rules/r0009-ebpf-program-load/README.md | 48 ++++++++++++++ .../README.md | 48 ++++++++++++++ .../README.md | 51 +++++++++++++++ .../README.md | 58 +++++++++++++++++ .../README.md | 47 ++++++++++++++ pkg/rules/r1002-kernel-module-load/README.md | 48 ++++++++++++++ .../r1003-malicious-ssh-connection/README.md | 49 ++++++++++++++ pkg/rules/r1004-exec-from-mount/README.md | 50 ++++++++++++++ pkg/rules/r1005-fileless-execution/README.md | 50 ++++++++++++++ pkg/rules/r1006-unshare-syscall/README.md | 48 ++++++++++++++ pkg/rules/r1007-xmr-crypto-mining/README.md | 46 +++++++++++++ .../README.md | 48 ++++++++++++++ .../README.md | 65 +++++++++++++++++++ .../README.md | 46 +++++++++++++ pkg/rules/r1011-ld-preload-hook/README.md | 54 +++++++++++++++ .../README.md | 46 +++++++++++++ .../r1015-malicious-ptrace-usage/README.md | 49 ++++++++++++++ .../README.md | 48 ++++++++++++++ 26 files changed, 1310 insertions(+) create mode 100644 pkg/rules/r0001-unexpected-process-launched/README.md create mode 100644 pkg/rules/r0002-unexpected-file-access/README.md create mode 100644 pkg/rules/r0003-unexpected-system-call/README.md create mode 100644 pkg/rules/r0004-unexpected-capability-used/README.md create mode 100644 pkg/rules/r0005-unexpected-domain-request/README.md create mode 100644 pkg/rules/r0006-unexpected-service-account-token-access/README.md create mode 100644 pkg/rules/r0007-kubernetes-client-executed/README.md create mode 100644 pkg/rules/r0008-read-environment-variables-procfs/README.md create mode 100644 pkg/rules/r0009-ebpf-program-load/README.md create mode 100644 pkg/rules/r0010-unexpected-sensitive-file-access/README.md create mode 100644 pkg/rules/r0011-unexpected-egress-network-traffic/README.md create mode 100644 pkg/rules/r1000-exec-from-malicious-source/README.md create mode 100644 pkg/rules/r1001-exec-binary-not-in-base-image/README.md create mode 100644 pkg/rules/r1002-kernel-module-load/README.md create mode 100644 pkg/rules/r1003-malicious-ssh-connection/README.md create mode 100644 pkg/rules/r1004-exec-from-mount/README.md create mode 100644 pkg/rules/r1005-fileless-execution/README.md create mode 100644 pkg/rules/r1006-unshare-syscall/README.md create mode 100644 pkg/rules/r1007-xmr-crypto-mining/README.md create mode 100644 pkg/rules/r1008-crypto-mining-domain-communication/README.md create mode 100644 pkg/rules/r1009-crypto-mining-related-port/README.md create mode 100644 pkg/rules/r1010-symlink-created-over-sensitive-file/README.md create mode 100644 pkg/rules/r1011-ld-preload-hook/README.md create mode 100644 pkg/rules/r1012-hardlink-created-over-sensitive-file/README.md create mode 100644 pkg/rules/r1015-malicious-ptrace-usage/README.md create mode 100644 pkg/rules/r1030-unexpected-io_uring-operation/README.md diff --git a/pkg/rules/r0001-unexpected-process-launched/README.md b/pkg/rules/r0001-unexpected-process-launched/README.md new file mode 100644 index 0000000..3cc6ddc --- /dev/null +++ b/pkg/rules/r0001-unexpected-process-launched/README.md @@ -0,0 +1,59 @@ +# R0001 — Unexpected Process Launched + +| Field | Value | +|-------|-------| +| Severity | Low | +| MITRE Tactic | Execution (TA0002) | +| MITRE Technique | Command and Scripting Interpreter (T1059) | +| Platforms | Host/Kubernetes/ECS | +| Requires Application Profile | Yes | + +## Description + +Detects any process executed inside a host or container that was not observed in that host/container's learned application profile. The rule fires on every `exec` event whose executable is not part of the container's baseline. It produces a strong signal in steady-state workloads where the set of legitimate processes is small and predictable, and is one of the broadest catch-alls for execution-based attacks such as command injection, web-shell drops, post-exploitation tooling, and lateral-movement payloads. + +## Attack Technique + +Mapped to **MITRE T1059 — Command and Scripting Interpreter** under tactic **TA0002 — Execution**. Adversaries who land code in a container almost always need to execute *some* unexpected binary or interpreter to make progress: a downloaded shell script, a reverse-shell binary, a privilege-escalation tool, a network utility for reconnaissance, or a cryptominer. Because the baseline is built from real execution traces of the running workload, anything outside that set surfaces here — including techniques the rule library has no signature for. + +## How It Works + +The node agent builds a per-container **application profile** during a learning window, recording every `exec` event observed. After the profile is finalized, every subsequent `exec` event is evaluated against the profile. + +Simplified CEL: + +``` +!ap.was_executed(containerId, parse.get_exec_path(args, comm)) + && (exepath == "" || !ap.was_executed(containerId, exepath)) +``` + +Two paths are checked because `argv[0]` and the kernel-resolved `exepath` can disagree: + +- **Relative `argv[0]`** (e.g. the process invoked itself as `./python`): the profile stores the resolved absolute path, so `argv[0]` would miss. `exepath` (`/usr/bin/python3`) catches it. +- **Empty `argv[0]`** (e.g. via `fexecve()` with `AT_EMPTY_PATH`, common from `sshd → unix_chkpwd`): again, the profile stores the resolved path, and the fallback to `exepath` matches. + +The rule fires when neither lookup succeeds. + +## Investigation Steps + +1. **Identify the process and its parent.** Look at the alert's `event.comm`, `event.exepath`, `event.pid`, and the parent process (`pcomm`/`ppid`). A new shell spawned by a web server (e.g. `bash` from `nginx`) is much more concerning than a known internal tool fired by a known parent. +2. **Confirm the binary was not legitimately added.** Cross-reference the workload's recent deployments — a new image version may legitimately introduce a process the profile never saw. Check container image digests and recent CI/CD activity. +3. **Inspect the executable on disk.** If accessible, hash the binary and look it up against threat-intel sources. Check its path (`/tmp`, `/dev/shm`, container working dir) — non-standard locations strengthen the signal. +4. **Pull surrounding events.** Look for other alerts on the same container in the same time window — file writes, network connections, capability changes, or other R0001 hits. Adversary tooling rarely fires only one rule. +5. **Decide: legitimate change or attack.** If legitimate, retrain the profile (see Remediation). If suspicious, isolate the container and begin incident response. + +## Remediation + +**If the process is malicious:** isolate the container (network policy or seccomp profile), preserve disk/memory for forensics, rotate any credentials accessible from the container (see "blast radius"), and begin standard incident response. The parent process and ingress vector (which previous event allowed this `exec`?) usually reveal the entry point. + +**If the process is legitimate but the profile is stale:** + +- Suppress this specific binary for the workload via a per-rule allowlist policy. + +**Do not blanket-disable R0001 on a workload** — it provides the deepest catch-all coverage in the rule library, and disabling it removes detection for a wide class of execution-based attacks. That said, some workloads are by definition not fit for anomaly detection: software orchestrators, CI/CD tools, runners — anything where the process-invocation cycle is detached from the container or host run cycle. + +## False Positives + +- **Periodic jobs that did not run during learning.** Weekly cron tasks, monthly batch jobs, ad-hoc maintenance scripts, and infrequent administrative tools can be missed by a short learning window. Lengthen the learning window or pre-warm the profile with the expected workload. +- **Runners or execution orchestrators.** Some software, by definition, has a different run cycle than the monitored host or container and may trigger false positives. One example is Apache Spark, where each job can ship its own binaries. + diff --git a/pkg/rules/r0002-unexpected-file-access/README.md b/pkg/rules/r0002-unexpected-file-access/README.md new file mode 100644 index 0000000..80c2a1c --- /dev/null +++ b/pkg/rules/r0002-unexpected-file-access/README.md @@ -0,0 +1,53 @@ +# R0002 — Files Access Anomalies in container + +| Field | Value | +|-------|-------| +| Severity | Low | +| MITRE Tactic | Collection (TA0009) | +| MITRE Technique | Data from Local System (T1005) | +| Platforms | Host, Kubernetes, ECS | +| Requires Application Profile | Yes | + +## Description + +Detects reads of sensitive system locations inside a host or container that were not observed during the learning window. The rule watches a curated set of high-value directories (`/etc/`, `/var/log/`, `/var/run/`, `/run/`, `/var/spool/cron/`, `/var/www/`, `/var/lib/`, `/opt/`, `/usr/local/`, `/app/`, plus the marker files `/.dockerenv` and `/proc/self/environ`) and fires whenever a process opens a path under one of them that the application profile did not record. The rule is disabled by default because steady-state false-positive rate depends heavily on the workload's file-access patterns. + +## Attack Technique + +Mapped to **MITRE T1005 — Data from Local System** under **TA0009 — Collection**. Once an adversary has code execution they typically read configuration, application code, secrets, and logs to understand the environment and to harvest material. This rule surfaces that reconnaissance against the workload's own observed file-access shape, catching reads of files the workload itself never needs. + +## How It Works + +The node agent records every `open` event in the watched prefixes during the learning window. After the profile is finalized, every subsequent `open` in those prefixes is checked against the profile, with three explicit suppressions baked into the rule body: + +``` +event.path is under one of the watched prefixes + AND event.path is NOT under /run/secrets/kubernetes.io/serviceaccount + AND event.path is NOT under /var/run/secrets/kubernetes.io/serviceaccount + AND event.path is NOT under /tmp + AND !ap.was_path_opened(containerId, event.path) +``` + +The `/tmp` and Kubernetes service-account paths are excluded because they have their own dedicated rules; including them here would produce duplicate alerts on the same activity. + +## Investigation Steps + +1. **Identify the process and the file.** Look at `event.comm`, `event.pid`, and `event.path`. A new shell or a network-facing process reading `/etc/passwd`, `/etc/nginx/`, or `/var/lib/postgresql` is much more concerning than a known internal tool opening a known path. +2. **Confirm the path is sensitive in context.** `/etc/` reads can be benign (libc reading `/etc/nsswitch.conf`) or critical (`/etc/shadow`, `/etc/cron.d/*`). Use the path semantics, not just the prefix, to triage. +3. **Look at the parent process and user.** A `nobody`-uid process spawned by a web server reading `/var/www/` configuration is essentially never legitimate. +4. **Pull surrounding events** for the same container in the same window: exec events, network connections, or other R0002 hits in adjacent directories indicate active enumeration. +5. **Decide: legitimate change or attack.** If legitimate, suppress the specific file or workload via a per-rule allowlist. If suspicious, isolate and begin incident response. + +## Remediation + +**If the access is malicious:** isolate the container (network policy or seccomp profile), preserve disk and memory for forensics, rotate any credentials reachable from the file content (see "blast radius"), and begin standard incident response. The opening process's ancestry usually identifies the ingress vector. + +**If the access is legitimate:** suppress the specific path or process for the workload via a per-rule allowlist policy. Do not retrain the profile as a remediation step. + +Some workloads are by definition not fit for application-profile anomaly detection: software orchestrators, CI/CD tools, runners — anything where the process and file-access cycle is detached from the container or host run cycle. R0002 is best disabled on such workloads rather than allowlisted item-by-item. + +## False Positives + +- **Periodic jobs that did not run during learning.** Weekly cron tasks, monthly batch jobs, ad-hoc admin scripts, and infrequent maintenance tools may open files under the watched prefixes for the first time after the profile is finalized. Lengthen the learning window or pre-warm the profile. +- **Self-updating runtimes and package managers.** Some interpreters and language toolchains touch `/etc/` or `/usr/local/` files on first invocation; these may not appear in the recorded baseline. +- **Build orchestrators and runners.** Workloads like Apache Spark or CI runners that ship per-job binaries and config will read configuration files the profile never saw. diff --git a/pkg/rules/r0003-unexpected-system-call/README.md b/pkg/rules/r0003-unexpected-system-call/README.md new file mode 100644 index 0000000..89f949f --- /dev/null +++ b/pkg/rules/r0003-unexpected-system-call/README.md @@ -0,0 +1,49 @@ +# R0003 — Syscalls Anomalies in container + +| Field | Value | +|-------|-------| +| Severity | Low | +| MITRE Tactic | Execution (TA0002) | +| MITRE Technique | Command and Scripting Interpreter (T1059) | +| Platforms | Host, Kubernetes, ECS | +| Requires Application Profile | Yes | + +## Description + +Detects any system call invoked inside a host or container that was not observed during the learning window. The rule fires the first time a given syscall appears that is not part of the application profile's recorded syscall set. It is a very broad anomaly signal: the syscall surface a real workload uses is typically narrow and predictable, so deviations strongly indicate code outside the workload's normal behavior. + +## Attack Technique + +Mapped to **MITRE T1059 — Command and Scripting Interpreter** under **TA0002 — Execution**. Adversaries who land code in a container almost always reach for syscalls the workload itself does not need: namespace manipulation for escape, raw socket operations for tunneling, ptrace for injection, mount syscalls for filesystem games, or kernel-instrumentation primitives. Because the baseline is built from actual workload behavior, anything the workload never demonstrated surfaces here. + +## How It Works + +The node agent records every distinct syscall observed during the learning window. After the profile is finalized, every syscall event is checked against the recorded set: + +``` +!ap.was_syscall_used(containerId, syscallName) +``` + +The rule fires on any miss. Because most workloads use only a small subset of the ~350 available syscalls, the suppression catches steady-state activity while novel syscalls light up. + +## Investigation Steps + +1. **Identify the syscall and the process.** `event.syscallName`, `event.comm`, and `event.pid` are the starting point. Some syscalls (`ptrace`, `mount`, `unshare`, `keyctl`, `bpf`) are diagnostic on their own; others (`fchmod`, `fchown`) need context. +2. **Map the syscall to a technique.** Many security-relevant syscalls have a specific abuse pattern: `ptrace` for process injection, `unshare`/`setns` for container escape, `bpf` for kernel rootkits, `mount` for filesystem masquerade. The technique narrows the investigation immediately. +3. **Look at the process ancestry.** The parent process and its parent often reveal whether the syscall is benign (a new dependency the workload now uses) or hostile (a planted binary). +4. **Pull surrounding events.** Other R0003 hits, exec events, or file/network anomalies on the same container in the same window indicate active tradecraft rather than a quiet workload change. +5. **Decide: legitimate change or attack.** If legitimate, suppress the specific syscall via a per-rule allowlist. If suspicious, isolate and begin incident response. + +## Remediation + +**If the syscall is malicious:** isolate the container (network policy or seccomp profile), preserve disk and memory for forensics, rotate credentials reachable from the workload (see "blast radius"), and begin incident response. The use of an unusual syscall usually indicates the attacker has already executed payload code, so investigate that ingress. + +**If the syscall is legitimate:** allowlist it via a per-rule policy for the affected workload. Prefer a tight allowlist (one syscall, one workload) over broad exceptions. Do not retrain the profile as a remediation step. + +Some workloads are by definition not fit for syscall anomaly detection: software orchestrators, CI/CD tools, runners — anything where the workload runs arbitrary user-supplied code and the syscall surface is unbounded by design. + +## False Positives + +- **Periodic operations that did not run during learning.** A nightly backup that calls `fdatasync` or a weekly maintenance task that calls `setrlimit` may not appear in the baseline if those syscalls were not exercised during the learning window. +- **New library or runtime versions.** A dependency upgrade can introduce syscalls (e.g. `io_uring_*` after a libc bump) that the baseline does not know about. +- **Runners and execution orchestrators.** Workloads like Apache Spark, build runners, or function-as-a-service containers execute user code whose syscall set cannot be predicted from learning. diff --git a/pkg/rules/r0004-unexpected-capability-used/README.md b/pkg/rules/r0004-unexpected-capability-used/README.md new file mode 100644 index 0000000..c569d53 --- /dev/null +++ b/pkg/rules/r0004-unexpected-capability-used/README.md @@ -0,0 +1,49 @@ +# R0004 — Linux Capabilities Anomalies in container + +| Field | Value | +|-------|-------| +| Severity | Low | +| MITRE Tactic | Execution (TA0002) | +| MITRE Technique | Command and Scripting Interpreter (T1059) | +| Platforms | Host, Kubernetes, ECS | +| Requires Application Profile | Yes | + +## Description + +Detects Linux capabilities exercised inside a host or container that were not observed during the learning window. Capabilities are the kernel's way of breaking root privileges into smaller, individually-grantable pieces (`CAP_NET_RAW`, `CAP_SYS_ADMIN`, `CAP_DAC_OVERRIDE`, and ~40 others). A workload's real capability set is usually narrow; an attacker exercising a capability the workload never needed indicates either successful exploitation or a misconfigured container with too many capabilities available to abuse. + +## Attack Technique + +Mapped to **MITRE T1059 — Command and Scripting Interpreter** under **TA0002 — Execution**. Adversaries who land code in a container often reach for capabilities the workload itself doesn't need: `CAP_NET_RAW` for crafted-packet operations, `CAP_SYS_PTRACE` for process injection, `CAP_SYS_MODULE` for kernel modules, `CAP_DAC_READ_SEARCH` for bypassing filesystem permissions. Detecting on the deviation from baseline catches the attacker even when the rule library has no specific signature for the technique. + +## How It Works + +The node agent records every distinct capability exercised during the learning window. After the profile is finalized, every capability check is matched against the recorded set: + +``` +!ap.was_capability_used(containerId, capName) +``` + +The rule fires the first time a capability is exercised that the profile did not see during learning. + +## Investigation Steps + +1. **Identify the capability and the process.** `event.capName`, `event.syscallName`, `event.comm`, and `event.pid` together describe what was attempted. Some capabilities (`CAP_SYS_ADMIN`, `CAP_SYS_PTRACE`, `CAP_SYS_MODULE`) are essentially diagnostic of post-exploitation activity in most workloads. +2. **Check whether the container was even granted the capability.** If the pod spec or container runtime denied the capability, the syscall would have failed and the event indicates an attempted, not successful, use. Either way, the *attempt* is a strong signal. +3. **Map capability to attack pattern.** `CAP_NET_BIND_SERVICE` on a non-privileged port is benign; `CAP_NET_RAW` from a workload that never raw-sockets points at scanning or packet crafting. +4. **Pull surrounding events.** Capability use rarely happens in isolation: an exec event right before, a syscall anomaly right after, or a network anomaly nearby usually points at the broader attack. +5. **Decide: legitimate change or attack.** If legitimate, suppress the specific capability for the workload. If suspicious, isolate and begin incident response. + +## Remediation + +**If the capability use is malicious:** isolate the container (network policy or seccomp profile), preserve disk and memory for forensics, rotate credentials reachable from the workload (see "blast radius"), and begin incident response. As a hardening follow-up, drop the capability from the container spec so a future intruder cannot exercise it at all. + +**If the capability use is legitimate:** allowlist it via a per-rule policy. The better long-term fix is usually to reduce the container's granted capabilities to the actual minimum, not to allow the workload to use anything it pleases. + +Some workloads (orchestrators, CI/CD runners, debugging tools) by design exercise a broad capability set and are unsuited for this anomaly detection. + +## False Positives + +- **Periodic privileged operations.** A monthly admin task that needs `CAP_SYS_TIME` or `CAP_SETPCAP` may not have run during the learning window. +- **Container runtime helpers.** A few sidecar and init-container patterns briefly exercise capabilities not seen in steady state. These are best allowlisted by process name. +- **Newly-deployed features.** A code path added after profile finalization that requires a capability the workload previously did not use. diff --git a/pkg/rules/r0005-unexpected-domain-request/README.md b/pkg/rules/r0005-unexpected-domain-request/README.md new file mode 100644 index 0000000..00e72b4 --- /dev/null +++ b/pkg/rules/r0005-unexpected-domain-request/README.md @@ -0,0 +1,50 @@ +# R0005 — DNS Anomalies in container + +| Field | Value | +|-------|-------| +| Severity | Low | +| MITRE Tactic | Command and Control (TA0011) | +| MITRE Technique | Application Layer Protocol: DNS (T1071.004) | +| Platforms | Host, Kubernetes, ECS | +| Requires Application Profile | Yes (uses Network Neighborhood) | + +## Description + +Detects DNS queries from a host or container to domains that were not observed in the container's Network Neighborhood during the learning window. Cluster-internal lookups (anything ending in `.svc.cluster.local.`) are excluded so the rule focuses on external destinations. A workload's external DNS pattern is usually narrow and predictable; any new domain is either a new dependency rollout or an outbound channel the attacker wants to use. + +## Attack Technique + +Mapped to **MITRE T1071.004 — Application Layer Protocol: DNS** under **TA0011 — Command and Control**. DNS is a near-universally-allowed egress channel, which makes it the first protocol attackers reach for: command-and-control beacons, DNS-tunneled exfiltration, payload-fetch from attacker-controlled domains, and lookups for cloud-metadata or third-party APIs all surface here. Catching them at the DNS layer is cheap and language-agnostic. + +## How It Works + +The node agent records every external (non-cluster-internal) domain queried during the learning window into the container's Network Neighborhood. After the baseline is finalized, every DNS event is matched: + +``` +!event.name.endsWith('.svc.cluster.local.') + AND !nn.is_domain_in_egress(containerId, event.name) +``` + +The first clause filters Kubernetes service discovery noise; the second checks whether the destination domain was seen during learning. + +## Investigation Steps + +1. **Resolve the domain to its purpose.** The domain itself often gives away the intent: a typosquat of a CDN, an attacker-themed name, a dynamic-DNS provider, a Pastebin-like service, or a known C2 framework's default domain are all diagnostic. +2. **Identify the requesting process.** `event.comm` and the container name in `event.containerName` point at the process. A network-facing service suddenly resolving `paste.ee` or a cloud-metadata host is much more interesting than a known cron job hitting a software-update endpoint. +3. **Pull surrounding events.** A DNS anomaly is often followed by a network connection to the resolved address (R0011 may fire), a download (`curl`/`wget` exec), or a fileless execution. Cross-correlation often confirms or refutes the alert quickly. +4. **Check threat-intel feeds.** The domain may already be tagged as known-bad. A clean reputation does not exonerate (newly-registered domains are common attacker tradecraft) but a known-bad tag is a strong escalation signal. +5. **Decide: legitimate change or attack.** If legitimate, allowlist the domain for the workload. If suspicious, isolate the container and begin incident response. + +## Remediation + +**If the lookup is malicious:** apply an egress policy that denies the domain (or denies all DNS to the resolver if the workload should have no external DNS at all), isolate the container's network egress, preserve memory and disk, rotate any credentials reachable from the workload (see "blast radius"), and begin incident response. Look for the upstream activity that brought the domain into the workload's behavior — a freshly-dropped binary or a config change is the usual ingress. + +**If the lookup is legitimate:** allowlist the specific domain (and ideally only that domain) via a per-rule policy. The Network Neighborhood will pick the domain up at the next baseline pass. + +Some workloads (CI runners, orchestrators, scrapers) make DNS queries to a long tail of unpredictable domains by design and are unsuited for DNS anomaly detection. + +## False Positives + +- **Long-tail external dependencies the workload uses only occasionally.** A monthly software-update check, a billing-API call once per cycle, or a vendor health check that was not exercised during learning. +- **CDN domain rotation.** Some services rotate hostnames (e.g. signed-URL hosts under `*.cloudfront.net` or `*.s3.amazonaws.com`) that the baseline did not see verbatim. +- **Workloads with broad external surface by design.** Web scrapers, build orchestrators, and CI runners visit domains that cannot be enumerated up front. diff --git a/pkg/rules/r0006-unexpected-service-account-token-access/README.md b/pkg/rules/r0006-unexpected-service-account-token-access/README.md new file mode 100644 index 0000000..97a661b --- /dev/null +++ b/pkg/rules/r0006-unexpected-service-account-token-access/README.md @@ -0,0 +1,48 @@ +# R0006 — Unexpected Service Account Token Access + +| Field | Value | +|-------|-------| +| Severity | Medium | +| MITRE Tactic | Credential Access (TA0006) | +| MITRE Technique | Steal Application Access Token (T1528) | +| Platforms | Host, Kubernetes, ECS | +| Requires Application Profile | Yes | + +## Description + +Detects reads of a Kubernetes service-account token by a process that was not observed reading the token during the learning window. The rule watches the standard token paths under `/run/secrets/kubernetes.io/serviceaccount/token`, `/var/run/secrets/kubernetes.io/serviceaccount/token`, and the EKS equivalents under `/run/secrets/eks.amazonaws.com/serviceaccount/token`. The token grants whatever RBAC the pod's service account has, so anyone who reads it can act as that pod against the Kubernetes API. + +## Attack Technique + +Mapped to **MITRE T1528 — Steal Application Access Token** under **TA0006 — Credential Access**. Service-account tokens are the most valuable in-cluster credential a compromised pod can yield: they are pre-mounted, pre-authenticated, and tied to whatever roles the cluster operator granted the pod. Adversaries with code execution in a container will reach for the token early to enumerate the cluster, escalate via overly-permissive roles, or exfiltrate the token for use from outside the cluster. + +## How It Works + +The rule fires on any `open` event whose path matches one of the standard token locations and which is not part of the workload's recorded token-access history: + +``` +event.path under /run/secrets/.../serviceaccount/token (or /var/run/.../, or EKS equivalents) + AND !ap.was_path_opened_with_suffix(containerId, '/token') +``` + +The `was_path_opened_with_suffix` check generalizes across the various mount paths so legitimate access from any of them is correctly suppressed. + +## Investigation Steps + +1. **Identify the reading process.** `event.comm`, `event.pid`, and the open flags reveal whether the token was read for normal use (a k8s client library called during request handling) or by a shell, debugger, or unknown binary. +2. **Check the parent process.** A token read from a freshly-launched `bash` or `curl` spawned by a network-facing service is essentially diagnostic of credential theft. +3. **Look for outbound use.** If the token has already been used, you may see network requests to the Kubernetes API server (audit logs are authoritative here) or outbound requests to a non-cluster destination containing the token. +4. **Determine the blast radius.** The compromised service account's RBAC bindings define what the attacker can now do — list secrets across namespaces, create privileged pods, exec into other pods. Cluster role bindings are the worst case. +5. **Decide: legitimate change or attack.** If legitimate, allowlist the reading process. If suspicious, rotate the token and isolate the pod. + +## Remediation + +**If the token read is malicious:** rotate the service account's token (deleting the secret will force recreation, but better: delete and recreate the service account). Audit Kubernetes API server logs for any actions taken with the token between the read and the rotation. Isolate the pod (network policy or seccomp profile), preserve memory and disk, and treat any resources the compromised SA had access to as potentially compromised (see "blast radius"). Tighten the SA's RBAC to least-privilege as a hardening follow-up. + +**If the token read is legitimate:** allowlist the specific process via a per-rule policy. Better long-term, audit whether the workload actually needs Kubernetes API access — many do not, and disabling the token mount (`automountServiceAccountToken: false`) removes the attack surface entirely. + +## False Positives + +- **Workloads with multiple Kubernetes-client containers** where one client was not exercised during learning. +- **Sidecar agents that read the token on-demand** rather than at startup, where the on-demand path didn't run during the learning window. +- **k8s client library version bumps** that change which exact mount path is opened first. diff --git a/pkg/rules/r0007-kubernetes-client-executed/README.md b/pkg/rules/r0007-kubernetes-client-executed/README.md new file mode 100644 index 0000000..ebbadfe --- /dev/null +++ b/pkg/rules/r0007-kubernetes-client-executed/README.md @@ -0,0 +1,54 @@ +# R0007 — Workload uses Kubernetes API unexpectedly + +| Field | Value | +|-------|-------| +| Severity | Medium | +| MITRE Tactic | Lateral Movement (TA0008) | +| MITRE Technique | Exploitation of Remote Services (T1210) | +| Platforms | Host, Kubernetes, ECS | +| Requires Application Profile | Yes | + +## Description + +Detects two related signals that indicate a host or container is communicating with the Kubernetes API server in a way the workload was not observed doing during learning: (1) execution of `kubectl` (or any binary named `kubectl` / ending in `/kubectl`) that was not part of the application profile, and (2) outbound network connections to the cluster's API server address that were not part of the Network Neighborhood baseline. Either signal alone is interesting; together they often indicate cluster reconnaissance from a compromised pod. + +## Attack Technique + +Mapped to **MITRE T1210 — Exploitation of Remote Services** under **TA0008 — Lateral Movement**. The Kubernetes API server is one of the highest-value lateral-movement targets in a cluster: it controls every workload, every secret, and every cluster role binding. An adversary with code execution in a pod that has API access often pivots through the API to escalate, enumerate, or persist. Detecting both the typical tool (`kubectl`) and the typical destination (the API server address) catches both the tooled and the scripted variants. + +## How It Works + +Two event types, evaluated independently: + +``` +exec: + (event.comm == 'kubectl' || event.exepath.endsWith('/kubectl')) + AND !ap.was_executed(containerId, ...) + +network: + event.pktType == 'OUTGOING' + AND k8s.is_api_server_address(event.dstAddr) + AND !nn.was_address_in_egress(containerId, event.dstAddr) +``` + +The exec arm catches the binary by name; the network arm catches any outbound to the cluster's API endpoint(s), so a Go or Python client using the in-cluster config also triggers when the workload was not previously observed talking to the API server. + +## Investigation Steps + +1. **Identify which signal fired.** The exec signal points at a binary; the network signal points at an HTTP/TLS connection. They usually fire together for tooling-based reconnaissance but only the network arm fires for in-process clients. +2. **Look at the originating process and parent.** A `kubectl exec` from a freshly-spawned shell inside a web-facing pod is essentially diagnostic of an intruder. +3. **Pull API server audit logs.** If the connection succeeded, the Kubernetes API server's audit log shows exactly what was requested, by which service account, and against which resources. This determines blast radius. +4. **Check the pod's service account permissions.** A pod whose SA has `cluster-admin` is a very different incident from a pod whose SA has only `get` on its own pod. +5. **Decide: legitimate change or attack.** If legitimate, allowlist the binary and/or destination. If suspicious, rotate the SA token, isolate the pod, and audit API server logs from the alert time forward. + +## Remediation + +**If the activity is malicious:** rotate the service account token immediately, audit the API server logs for actions taken with the compromised SA, and isolate the pod (network policy or seccomp profile). Treat any cluster resources the SA could reach as potentially compromised (see "blast radius"). As hardening, set `automountServiceAccountToken: false` on workloads that do not need API access, and apply NetworkPolicies that deny egress to the API server from pods that should not reach it. + +**If the activity is legitimate:** allowlist the specific binary or destination via a per-rule policy. + +## False Positives + +- **Kubernetes-aware sidecars** added after the baseline was captured. Service-mesh proxies, secret-rotation agents, and metrics scrapers that talk to the API server may not have been present during learning. +- **Application updates that introduce a Kubernetes client.** A code path that begins using the Kubernetes API after a deploy will trigger until either the baseline is replaced or the workload is allowlisted. +- **Operator/admin debug sessions.** A human shell that runs `kubectl` inside a pod for troubleshooting; expected from kubectl-exec entry points and distinguishable by parent process. diff --git a/pkg/rules/r0008-read-environment-variables-procfs/README.md b/pkg/rules/r0008-read-environment-variables-procfs/README.md new file mode 100644 index 0000000..97a28e2 --- /dev/null +++ b/pkg/rules/r0008-read-environment-variables-procfs/README.md @@ -0,0 +1,49 @@ +# R0008 — Read Environment Variables from procfs + +| Field | Value | +|-------|-------| +| Severity | Medium | +| MITRE Tactic | Credential Access (TA0006) | +| MITRE Technique | Unsecured Credentials: Credentials In Files (T1552.001) | +| Platforms | Host, Kubernetes, ECS | +| Requires Application Profile | Yes | + +## Description + +Detects reads of `/proc//environ` by a process that was not observed reading the procfs environ files during learning. The `environ` file exposes the environment variables of a running process, which on most workloads contain API keys, database passwords, cloud credentials, OAuth secrets, and other high-value material that adversaries explicitly target. A read of any process's `environ` from inside a container is rare in normal operation and almost always indicates credential harvesting. + +## Attack Technique + +Mapped to **MITRE T1552.001 — Unsecured Credentials: Credentials In Files** under **TA0006 — Credential Access**. Environment variables are a common but insecure place to put secrets (twelve-factor apps still recommend it, container orchestrators still inject secrets this way), and adversaries know it. Reading `/proc/*/environ` walks every process on the host or container in one shot and harvests whatever was in their environment at exec time — including secrets that were deleted from the filesystem. + +## How It Works + +The rule fires on any `open` event matching the procfs environ path that was not observed during learning: + +``` +event.path.startsWith('/proc/') + AND event.path.endsWith('/environ') + AND !ap.was_path_opened_with_suffix(containerId, '/environ') +``` + +The suffix-based suppression covers both `/proc/self/environ` and `/proc//environ` paths, so legitimate self-reads (an app reading its own environment) are correctly allowlisted by the workload's baseline if they happened during learning. + +## Investigation Steps + +1. **Identify the reading process.** `event.comm`, `event.pid`, and `event.path` together describe the access. Reading another process's environ is much more concerning than reading `/proc/self/environ`. +2. **List which environs were read.** A single self-read may be a debugging library; a sweep across `/proc/[0-9]+/environ` is essentially diagnostic of credential harvesting. +3. **Determine the blast radius.** Whatever was in the read process's environment is now in the attacker's hands. Inventory the secrets injected as environment variables across the affected workload(s) and rotate them. +4. **Look for upstream activity.** A new shell, a fresh binary, or an unexpected `exec` typically precedes a credential-harvest sweep. +5. **Decide: legitimate change or attack.** If legitimate, allowlist the reading process. If suspicious, rotate exposed credentials and isolate. + +## Remediation + +**If the read is malicious:** rotate every credential that was in the environment of any read process, immediately (see "blast radius"). Isolate the source container (network policy or seccomp profile), preserve memory and disk, and audit whether the rotated credentials were used elsewhere between the read and the rotation. Long-term hardening: move secrets out of environment variables and into a secrets manager (Kubernetes Secrets mounted as files, Vault, AWS Secrets Manager) with rotation enabled. + +**If the read is legitimate:** allowlist the specific reading process via a per-rule policy. Self-environ reads from a known library are common; cross-process reads are not, and should be allowlisted with skepticism. + +## False Positives + +- **Self-introspection libraries** that read `/proc/self/environ` for diagnostic purposes. If they ran during learning they are already baselined; if added later, allowlist by process. +- **APM and profiler agents** that enumerate processes for metrics may touch `environ` on each. Allowlist by agent name. +- **In-container debuggers and orchestrators** that walk procfs are inherently broad readers and may be better off having R0008 disabled on the workload. diff --git a/pkg/rules/r0009-ebpf-program-load/README.md b/pkg/rules/r0009-ebpf-program-load/README.md new file mode 100644 index 0000000..e2679ee --- /dev/null +++ b/pkg/rules/r0009-ebpf-program-load/README.md @@ -0,0 +1,48 @@ +# R0009 — eBPF Program Load + +| Field | Value | +|-------|-------| +| Severity | Medium | +| MITRE Tactic | Defense Evasion (TA0005) | +| MITRE Technique | System Binary Proxy Execution (T1218) | +| Platforms | Host, Kubernetes, ECS | +| Requires Application Profile | Optional | + +## Description + +Detects loading of an eBPF program from a host or container. The `BPF_PROG_LOAD` command attaches code to a kernel hook point — kprobes, tracepoints, networking, security hooks — and the kernel runs that code with privileged access to kernel data structures. Legitimate uses (observability agents, runtime security tools, networking systems like Cilium) are well-known and few; an unexpected program load from a workload that never demonstrated this capability is a strong signal of either a rootkit or kernel-instrumentation-based attacker tooling. + +## Attack Technique + +Mapped to **MITRE T1218 — System Binary Proxy Execution** under **TA0005 — Defense Evasion**. eBPF gives an adversary unparalleled hiding capability: an eBPF program can intercept syscalls, hide processes from `ps`, drop network packets that would alert defenders, and rewrite the data returned to other security tools. Modern Linux rootkits increasingly rely on eBPF rather than kernel modules because the loading surface is wider (a process with `CAP_BPF` or `CAP_SYS_ADMIN` does not need to ship a `.ko` file). + +## How It Works + +The rule fires on any `bpf` syscall with `cmd == BPF_PROG_LOAD` (numeric value 5) that was not part of the baseline: + +``` +event.cmd == 5 // BPF_PROG_LOAD + AND !ap.was_syscall_used(containerId, 'bpf') +``` + +If the application profile is available, prior baselined use of the `bpf` syscall suppresses the rule for that workload. The rule fires when no baseline exists or when the workload never demonstrated `bpf` use during learning. + +## Investigation Steps + +1. **Identify the loading process.** `event.comm` and the parent process are the starting point. A known observability or networking agent (Falco, Cilium, datadog-agent, parca, etc.) is benign in context; an unknown binary or a shell-spawned helper is not. +2. **Check whether the container even has `CAP_BPF` or `CAP_SYS_ADMIN`.** If not, the load attempt would have failed and the event indicates an attempted, not successful, attack. Either way, the attempt warrants investigation. +3. **Look at the program type and attach point.** Programs that attach to networking hooks (`SOCK_FILTER`, `XDP`) are common; programs that attach to security hooks (`LSM`, `BPF_PROG_TYPE_KPROBE`) and probe kernel internals are heavily skewed toward hostile use. +4. **Pull surrounding events.** A new binary executed shortly before, a capability anomaly (R0004), or a process that should not have privileged access are usually nearby. +5. **Decide: legitimate change or attack.** If legitimate, allowlist the process. If suspicious, isolate the host (this is a host-level concern given eBPF's kernel scope). + +## Remediation + +**If the load is malicious:** isolate the host the load happened on — eBPF runs in kernel context, not container context, so the blast radius is the host, not just the container. Reboot to clear any loaded program (eBPF programs do not always survive reboot but may be re-pinned by the attacker). Capture loaded program info (`bpftool prog list`) before reboot if forensics tooling allows. Treat any host-level secrets and any data the eBPF program could observe as compromised (see "blast radius"). As a hardening follow-up, drop `CAP_BPF` and `CAP_SYS_ADMIN` from containers that do not need them, and use seccomp to block the `bpf` syscall entirely on workloads that never use it. + +**If the load is legitimate:** allowlist the specific loading process via a per-rule policy. A new observability or networking agent rollout typically only needs to be allowlisted on initial deploy. + +## False Positives + +- **Observability and networking agents** added after the baseline was captured (Falco, Cilium, Tetragon, Pyroscope, datadog-agent, parca). Allowlist by process name on first deploy. +- **Distroless or minimal containers** that legitimately load an eBPF program for filtering. Rare but real, and best allowlisted on a per-workload basis. +- **eBPF-based CNI plugins** that load programs on pod startup; usually run in privileged sidecars and are baseline-able if learning happens after CNI install. diff --git a/pkg/rules/r0010-unexpected-sensitive-file-access/README.md b/pkg/rules/r0010-unexpected-sensitive-file-access/README.md new file mode 100644 index 0000000..729351d --- /dev/null +++ b/pkg/rules/r0010-unexpected-sensitive-file-access/README.md @@ -0,0 +1,48 @@ +# R0010 — Unexpected Sensitive File Access + +| Field | Value | +|-------|-------| +| Severity | Medium | +| MITRE Tactic | Credential Access (TA0006) | +| MITRE Technique | Data from Local System (T1005) | +| Platforms | Host, Kubernetes, ECS | +| Requires Application Profile | Optional | + +## Description + +Detects reads of `/etc/shadow` from a host or container by a process that was not observed reading it during learning. `/etc/shadow` holds hashed passwords for local accounts and is one of the highest-value files on a Linux system. With rare exceptions (PAM-using authentication processes during legitimate user login), no workload code path needs to read this file in steady state, so a hit is a strong indicator of credential theft in progress. + +## Attack Technique + +Mapped to **MITRE T1005 — Data from Local System** under **TA0006 — Credential Access**. Once an adversary has root or `CAP_DAC_READ_SEARCH` they typically reach for `/etc/shadow` immediately: hashed passwords can be cracked offline, used for password-spraying against other systems, or used directly if weak hashing or known passwords are in play. Containers that share the host's `/etc/shadow` via bind mount (a misconfiguration) are particularly dangerous because a container compromise yields host credentials. + +## How It Works + +The rule fires on any `open` of a path starting with `/etc/shadow` that was not part of the baseline: + +``` +event.path.startsWith('/etc/shadow') + AND !ap.was_path_opened(containerId, event.path) +``` + +The prefix match covers `/etc/shadow` proper, `/etc/shadow-` (the backup), and `/etc/shadow.bak`-style variants that some attackers target. + +## Investigation Steps + +1. **Identify the reading process and user.** `event.comm`, `event.pid`, and the open flags reveal who and how. A read by an unfamiliar binary, a shell, or a network-facing service is essentially diagnostic. A read by `sshd`, `login`, `su`, `sudo`, or a known PAM-using process is usually benign. +2. **Check effective UID and capabilities.** Reading `/etc/shadow` requires either root (UID 0) or `CAP_DAC_READ_SEARCH`. The presence of this capability on a process that should not have it is a separate red flag. +3. **Determine the blast radius.** Every account with a hash now in the shadow file should be treated as potentially exfiltrated. Inventory the accounts, prioritize ones with weak or known-leaked passwords, and rotate (see "blast radius"). +4. **Look for upstream activity.** A fresh exec, a privilege escalation event, or an unexpected capability use (R0004) typically precedes this read. +5. **Decide: legitimate change or attack.** If legitimate, allowlist the process. If suspicious, rotate all local account passwords and isolate. + +## Remediation + +**If the read is malicious:** rotate every local-account password whose hash was in the file (see "blast radius"). Isolate the container or host (network policy or seccomp profile), preserve memory and disk, and audit whether any of the local accounts have been used for authentication elsewhere between the read and the rotation. Long-term hardening: do not bind-mount the host's `/etc/shadow` into containers, use SSO/IAM instead of local accounts where possible, and apply seccomp policies that deny `open` of `/etc/shadow` from workloads that never need it. + +**If the read is legitimate:** allowlist the specific reading process. PAM-using processes are the most common legitimate readers; others should be allowlisted with skepticism. + +## False Positives + +- **PAM-using processes that were not exercised during learning.** A workload that supports SSH login but had no logins during the learning window may not have baselined the `sshd → unix_chkpwd` read of `/etc/shadow`. +- **Backup and integrity-monitoring tools** that hash sensitive files. Allowlist by process; ensure the tool itself is not a vector. +- **Configuration management agents** (Ansible, Puppet, Chef) that read `/etc/shadow` for state checks. Rare in containers; common on hosts. diff --git a/pkg/rules/r0011-unexpected-egress-network-traffic/README.md b/pkg/rules/r0011-unexpected-egress-network-traffic/README.md new file mode 100644 index 0000000..e2dff55 --- /dev/null +++ b/pkg/rules/r0011-unexpected-egress-network-traffic/README.md @@ -0,0 +1,51 @@ +# R0011 — Unexpected Egress Network Traffic + +| Field | Value | +|-------|-------| +| Severity | Medium | +| MITRE Tactic | Exfiltration (TA0010) | +| MITRE Technique | Exfiltration Over C2 Channel (T1041) | +| Platforms | Host, Kubernetes, ECS | +| Requires Application Profile | Yes (uses Network Neighborhood) | + +## Description + +Detects outbound network connections from a host or container to public (non-private-IP) destinations that were not observed in the container's Network Neighborhood during the learning window. Private-range destinations (RFC1918, cluster-internal, link-local) are excluded because intra-cluster service-to-service traffic is too varied to baseline reliably at the IP level. A workload's external egress is usually narrow and stable; new public destinations indicate either a feature rollout or an outbound channel the attacker wants to use. The rule is disabled by default because false-positive rate is workload-dependent. + +## Attack Technique + +Mapped to **MITRE T1041 — Exfiltration Over C2 Channel** under **TA0010 — Exfiltration**. Once an adversary has code execution they typically need an outbound channel — to beacon to their C2, to exfiltrate stolen data, to pull additional payloads. Detecting on destinations the workload has not been observed talking to surfaces these channels without requiring signatures of specific C2 frameworks. + +## How It Works + +The rule fires on outbound TCP/UDP connections where the destination is not private and not in the Network Neighborhood: + +``` +event.pktType == 'OUTGOING' + AND !net.is_private_ip(event.dstAddr) + AND !nn.was_address_in_egress(containerId, event.dstAddr) +``` + +`net.is_private_ip` covers RFC1918, RFC4193, and loopback/link-local ranges, so cluster-internal and host-internal traffic is silently filtered. Only the public-Internet destinations the workload never demonstrated talking to surface. + +## Investigation Steps + +1. **Identify the destination.** Reverse-DNS, ASN, and threat-intel lookups on `event.dstAddr` and `event.dstPort` often determine the situation in seconds. A known cloud provider IP block hosting your real dependency is benign; an unknown VPS provider on a non-standard port is not. +2. **Identify the originating process.** `event.comm`, `event.pid`, and the container name point at the caller. A network-facing service connecting outbound to a fresh public IP is much more concerning than a known scheduled job hitting a software-update endpoint. +3. **Inspect the protocol and port.** Standard ports (443, 80) on common destinations are usually less interesting than non-standard ports (4444, 8080, 31337) or known C2 default ports. +4. **Pull surrounding events.** An unexpected egress connection often follows a DNS anomaly (R0005), a freshly-executed binary, or a credential read. Cross-correlation usually confirms or refutes within minutes. +5. **Decide: legitimate change or attack.** If legitimate, allowlist the destination or update the egress policy. If suspicious, block egress to the destination and isolate. + +## Remediation + +**If the connection is malicious:** apply an egress NetworkPolicy that denies the destination IP (and ideally pivots the workload to a default-deny egress posture). Isolate the container's network, preserve memory and disk, rotate any credentials the workload had access to (see "blast radius"), and begin incident response. Trace back to the upstream event that created the new behavior — a freshly-dropped binary or a config change is the usual ingress. + +**If the connection is legitimate:** allowlist the specific destination via a per-rule policy. The Network Neighborhood will pick the destination up at the next baseline pass. + +Some workloads (web scrapers, build runners, orchestrators) have an open-ended set of public destinations by design and are unsuited for egress-IP anomaly detection. + +## False Positives + +- **Long-tail external dependencies the workload uses only occasionally.** A monthly billing API call, a vendor health check, or a license-verification ping that was not exercised during learning. +- **CDN and cloud-provider IP rotation.** Some services rotate the public IPs behind a hostname; a workload connecting to the new IPs of a known hostname will trigger until the baseline picks them up. +- **Multi-region cloud APIs** where the workload occasionally falls back to a region not visited during learning. diff --git a/pkg/rules/r1000-exec-from-malicious-source/README.md b/pkg/rules/r1000-exec-from-malicious-source/README.md new file mode 100644 index 0000000..7f6624a --- /dev/null +++ b/pkg/rules/r1000-exec-from-malicious-source/README.md @@ -0,0 +1,58 @@ +# R1000 — Process Executed from Malicious Source + +| Field | Value | +|-------|-------| +| Severity | High | +| MITRE Tactic | Execution (TA0002) | +| MITRE Technique | Command and Scripting Interpreter (T1059) | +| Platforms | Host, Kubernetes, ECS | +| Requires Application Profile | No | + +## Description + +Detects any process whose executable, current working directory, or invocation path resolves to `/dev/shm`. `/dev/shm` is a `tmpfs` mount that is world-writable, in memory, and persists for the lifetime of the container — exactly the properties an adversary wants for staging payloads while evading on-disk forensics. Legitimate workloads almost never execute binaries from `/dev/shm`, so a hit on this rule is a strong indicator of an in-progress intrusion. + +## Attack Technique + +Mapped to **MITRE T1059 — Command and Scripting Interpreter** under **TA0002 — Execution**. The concrete tradecraft this rule catches is the "fileless" / "in-memory" staging pattern: an attacker who lands code via an exploit, a curl-piped script, or a download writes the payload to `/dev/shm`, then `execve()`s it. This avoids leaving artifacts on the persistent filesystem, evades file-integrity monitors that watch `/usr` and `/bin`, and skips path-based allowlists that focus on canonical locations. Cryptominers, reverse shells, and post-exploitation toolkits routinely use this pattern. + +## How It Works + +The rule is a pure signature — it does not depend on any baseline or learning. On every `exec` event it checks three signals: + +``` +exepath == '/dev/shm' || exepath.startsWith('/dev/shm/') + OR +cwd == '/dev/shm' || cwd.startsWith('/dev/shm/') + OR +parse.get_exec_path(args, comm).startsWith('/dev/shm/') +``` + +The `startsWith('/dev/shm/')` form (with trailing slash) is intentional: it matches `/dev/shm/foo` but **not** `/dev/shm_fake/foo`, avoiding false matches on similarly-named directories. The current-working-directory check catches the variant where a relative path (`./run.sh`) is executed from inside `/dev/shm` even though `exepath` itself resolves elsewhere. + +## Investigation Steps + +1. **Capture the payload before it disappears.** `/dev/shm` is volatile — files can vanish on container restart. If safe, hash and exfiltrate `event.exepath` immediately for static analysis. +2. **Trace the ingress.** Look back from the alert timestamp for the activity that put the file there: a `write()` event into `/dev/shm/*`, a recent `curl` / `wget`, a shell launched from a network-facing process, or an `exec` of a downloader script. +3. **Check the parent process and user.** A `nobody`-uid process spawned by `nginx`/`apache`/`node` executing from `/dev/shm` is essentially never legitimate. The parent chain often points directly at the exploited entry point. +4. **Search for lateral activity.** While `/dev/shm` execution suggests an attacker is mid-flight, also pull DNS lookups, outbound connections, credential file reads, and capability/privilege-escalation alerts on the same container. +5. **Treat as confirmed intrusion until proven otherwise.** Given the severity and the rarity of legitimate hits, default to incident response posture, not triage-and-dismiss. + +## Remediation + +**Active intrusion:** isolate the container immediately (network policy or seccomp profile), preserve `/dev/shm` and process memory if your tooling supports it, identify and revoke any credentials reachable from the workload, and rebuild from a known-good image. Treat all in-cluster lateral targets reachable from this container as potentially compromised until proven otherwise. + +**Hardening to prevent recurrence:** + +- Mount `/dev/shm` with `noexec` where the workload tolerates it. This stops the technique at the kernel level. +- Limit the size of `/dev/shm` so it cannot hold large payloads. +- Drop unnecessary Linux capabilities (`CAP_SYS_ADMIN`, `CAP_DAC_OVERRIDE`) so the attacker cannot remount `/dev/shm` exec. +- Apply read-only root filesystems where possible to remove other staging locations. + +## False Positives + +- **Workloads that legitimately use `/dev/shm` for shared memory IPC.** A small set of applications (some databases, video pipelines, scientific compute) place named files in `/dev/shm` for cross-process communication. They typically `mmap` those files, not `execve` them — execution of `/dev/shm` content is rare even for these workloads. +- **Specialized debug or profiling tooling.** A few APM/profiler agents drop helper binaries in `/dev/shm` at startup. These should be allowlisted explicitly rather than disabling the rule. +- **Test fixtures and CI runners.** Some CI systems stage temporary executables in tmpfs paths. Audit the build pipeline before allowlisting. + +The base rate of legitimate `/dev/shm` execution is low enough that the rule is high-fidelity by default. False positives are best handled by per-workload exceptions, not by disabling the rule globally. diff --git a/pkg/rules/r1001-exec-binary-not-in-base-image/README.md b/pkg/rules/r1001-exec-binary-not-in-base-image/README.md new file mode 100644 index 0000000..4d06302 --- /dev/null +++ b/pkg/rules/r1001-exec-binary-not-in-base-image/README.md @@ -0,0 +1,47 @@ +# R1001 — Drifted Process Executed + +| Field | Value | +|-------|-------| +| Severity | High | +| MITRE Tactic | Defense Evasion (TA0005) | +| MITRE Technique | Masquerading (T1036) | +| Platforms | Host, Kubernetes, ECS | +| Requires Application Profile | Optional | + +## Description + +Detects execution of a binary that was not present in the container's base image — that is, a binary written to the container's writable upper layer after start. The rule fires on `exec` events where either the executable or its parent comes from the upperlayer (the overlayfs writable layer that holds runtime additions on top of the read-only image), and the binary was not part of the application profile. A workload that runs only the binaries from its base image is the expected and ideal posture; a binary appearing in the writable layer and being executed means something added it at runtime, which is the textbook indicator of a dropped attacker payload. + +## Attack Technique + +Mapped to **MITRE T1036 — Masquerading** under **TA0005 — Defense Evasion**. Attackers who compromise a container almost always need to put their tooling somewhere and execute it. The writable upper layer is the path of least resistance: it persists for the container's lifetime, requires no special privileges to write, and is invisible to image scanners that only audit the read-only base image. Dropping a binary into the upperlayer and `execve`'ing it is a near-universal step in container intrusion chains. + +## How It Works + +``` +(event.upperlayer == true || event.pupperlayer == true) + AND !ap.was_executed(containerId, parse.get_exec_path(args, comm)) + AND (event.exepath == "" || !ap.was_executed(containerId, event.exepath)) +``` + +The first clause checks whether the executable (or its parent) lives in the writable upper layer of the container's overlay. The second and third clauses match against the application profile with the same dual-path semantics as R0001 — `argv[0]` and `exepath` are both checked because they can disagree for relative or `fexecve` invocations. + +## Investigation Steps + +1. **Capture the binary before it disappears.** Upperlayer files vanish on container restart. If safe, hash and exfiltrate `event.exepath` immediately for static analysis. +2. **Identify the dropping process.** The binary did not arrive in the image, so something wrote it. Look for `write` events targeting the same path shortly before this exec. The writer's process ancestry typically reveals the ingress vector. +3. **Inspect the parent and user.** A binary in the upperlayer being run by a `nobody`-uid process spawned from a web server is essentially diagnostic of an intrusion in flight. +4. **Look for adjacent activity.** Outbound connections to fresh destinations (R0011), DNS anomalies (R0005), or credential reads (R0010, R0008) frequently follow a dropped-binary exec. +5. **Treat as confirmed intrusion until proven otherwise.** Given the severity and the rarity of legitimate hits, default to incident response posture. + +## Remediation + +**Active intrusion:** isolate the container immediately (network policy or seccomp profile), preserve `/dev/shm`, the upperlayer, and process memory if your tooling supports it. Identify and revoke any credentials reachable from the workload (see "blast radius"), and rebuild the workload from a known-good image rather than patching in place. + +**Hardening:** apply read-only root filesystems where the workload tolerates them (this removes the writable layer entirely as a staging surface). Audit the base image for unnecessary tooling that would let an attacker live-off-the-land instead of dropping binaries. Tighten the container's capability set so the attacker cannot escalate further from a dropped tool. + +## False Positives + +- **Application updates that ship binaries at runtime.** Some legacy deploy patterns push binaries into a running container via a sidecar rather than via image rebuild. These should be considered antipatterns; if they cannot be changed, allowlist the specific binaries. +- **Self-updating runtimes.** A language runtime that downloads helper binaries to the upperlayer on first use. Rare in modern containers; allowlist explicitly. +- **In-container debug tooling** copied in by an operator. Distinguishable by parent process (kubectl-exec entry points). diff --git a/pkg/rules/r1002-kernel-module-load/README.md b/pkg/rules/r1002-kernel-module-load/README.md new file mode 100644 index 0000000..2637e16 --- /dev/null +++ b/pkg/rules/r1002-kernel-module-load/README.md @@ -0,0 +1,48 @@ +# R1002 — Process tries to load a kernel module + +| Field | Value | +|-------|-------| +| Severity | Critical | +| MITRE Tactic | Defense Evasion (TA0005) | +| MITRE Technique | Boot or Logon Autostart Execution: Kernel Modules and Extensions (T1547.006) | +| Platforms | Host, Kubernetes, ECS | +| Requires Application Profile | No | + +## Description + +Detects the loading of a Linux kernel module via the `init_module` or `finit_module` syscalls from a host or container. Loaded kernel modules run with full kernel privilege and can do anything the kernel can: hide processes, intercept syscalls, modify network traffic, install persistent rootkits, and disable other security controls. Almost nothing in a properly-engineered container workload needs to load kernel modules in steady state, so any hit is a strong indicator of either successful host compromise or attempted privilege escalation. + +## Attack Technique + +Mapped to **MITRE T1547.006 — Kernel Modules and Extensions** under **TA0005 — Defense Evasion**. Loading a kernel module is one of the highest-impact actions a process can take: it gives the attacker code execution in ring 0 with no MMU isolation from anything else on the host. Adversary tradecraft includes loading rootkit modules to hide processes and files, loading modules that disable other LKM-based security tools, and loading modules that survive container restart by attaching at the host kernel level. + +## How It Works + +Pure signature on syscall name: + +``` +event.syscallName == 'init_module' || event.syscallName == 'finit_module' +``` + +No baseline check is needed because the legitimate use of these syscalls inside a container is essentially zero — kernel modules are loaded from the host, not from container userland. + +## Investigation Steps + +1. **Identify the loading process and module.** `event.comm`, `event.pid`, and `event.module` describe what was attempted. A known node-level driver bootstrap (e.g. NVIDIA driver install, custom networking driver) is a different situation from an unknown binary loading an unknown module. +2. **Confirm the container had the capability.** Loading requires `CAP_SYS_MODULE`. Containers should almost never have this capability; if they do, that misconfiguration itself is a critical finding even if the load failed. +3. **Capture the module file.** If the load attempt referenced a `.ko` file on disk, exfiltrate it for static analysis before the attacker cleans up. +4. **Check kernel logs.** `dmesg` and the kernel ring buffer show whether the load succeeded. A failed load attempt is still a critical signal — the attacker is now aware they need a different escalation path. +5. **Treat as host-level incident.** Kernel modules run in host kernel context, so the scope of the incident is the host, not just the container. + +## Remediation + +**Active intrusion (loaded module):** isolate the host immediately (not just the container — the module lives in host kernel space). Reboot the host to clear the loaded module; reboots may not be sufficient if the attacker has established persistence in `/etc/modules-load.d/` or `/lib/modules/`, so audit those paths before bringing the host back. Treat all host-level secrets and data as potentially compromised (see "blast radius"). + +**Active intrusion (failed attempt):** isolate the container, drop `CAP_SYS_MODULE` from the container's capability set, and audit the workload for whatever allowed the attacker to even attempt this. + +**Hardening:** drop `CAP_SYS_MODULE` from all containers that do not legitimately load modules (which is nearly all of them). Set the host kernel's `kernel.modules_disabled` sysctl to `1` after all needed modules are loaded at boot, which forbids further module loading until the next reboot. + +## False Positives + +- **Privileged init containers** that legitimately load drivers for hardware passthrough (GPU, FPGA, specialized NICs). Identifiable by process and module name; should be the only legitimate caller in normal operation. +- **CNI plugin initialization** that loads networking modules at host startup. Usually runs in a privileged DaemonSet pod and is identifiable by the loading process. diff --git a/pkg/rules/r1003-malicious-ssh-connection/README.md b/pkg/rules/r1003-malicious-ssh-connection/README.md new file mode 100644 index 0000000..ab2ca3b --- /dev/null +++ b/pkg/rules/r1003-malicious-ssh-connection/README.md @@ -0,0 +1,49 @@ +# R1003 — Disallowed SSH Connection + +| Field | Value | +|-------|-------| +| Severity | Medium | +| MITRE Tactic | Lateral Movement (TA0008) | +| MITRE Technique | Remote Services: SSH (T1021.001) | +| Platforms | Host, Kubernetes, ECS | +| Requires Application Profile | Optional | + +## Description + +Detects SSH connections from a host or container whose source port is in the Linux ephemeral range (32768–60999) and whose destination port is not the standard SSH port (22) or the common alternate (2022). When the destination IP is also not in the container's Network Neighborhood, the connection fires this rule. The pattern matches an SSH client launched from inside the workload to reach an SSH server on an unusual port — a textbook lateral-movement pattern where attackers tunnel SSH through non-standard ports to evade port-based egress filters. The rule is disabled by default; enable it on workloads that should never originate SSH. + +## Attack Technique + +Mapped to **MITRE T1021.001 — Remote Services** under **TA0008 — Lateral Movement**. Once an adversary has code execution in a container, SSH is the natural pivot tool to reach other systems where they have credentials or keys. Using a non-standard destination port (4022, 2222, 31337, etc.) is common because operators often allow outbound 22 only to known bastion hosts, while a wider TCP egress posture might allow arbitrary high ports — making the non-standard port a usable channel. + +## How It Works + +``` +event source port in [32768, 60999] + AND event destination port NOT in [22, 2022] + AND !nn.was_address_in_egress(containerId, event.dstIp) +``` + +The source-port range identifies a connecting (client) socket; servers do not allocate ephemeral source ports. The destination-port exclusion suppresses the legitimate-SSH-to-standard-port case (which a different rule could cover). The Network Neighborhood check suppresses any destination the workload was previously observed talking to, removing internal SSH-to-bastion patterns that were learned. + +## Investigation Steps + +1. **Confirm it is actually SSH.** The event type is `ssh`, derived from packet inspection of the connection. Cross-check the destination port and protocol if your tooling permits. +2. **Identify the originating process.** `event.comm` reveals the SSH client — most often `ssh`, `scp`, `sftp`, or a wrapper. An SSH client running in a workload that should not initiate SSH is essentially diagnostic. +3. **Look up the destination.** Reverse-DNS, ASN, and threat-intel on `event.dstIp:event.dstPort` typically clarify whether the destination is a known internal bastion, a known external resource, or an attacker-controlled host. +4. **Pull surrounding events.** An SSH out is often preceded by a credential read (R0006, R0008, R0010) and a freshly-executed binary (R1001). The credential the attacker stole and the binary they used to mount the pivot are both diagnostic. +5. **Decide the response.** Treat as active lateral movement until disproven; one outbound SSH from an unexpected workload is enough to escalate. + +## Remediation + +**If the connection is malicious:** apply an egress NetworkPolicy that denies the destination, isolate the source container, preserve memory and disk, audit the destination host for activity originating from the source workload, and rotate any credentials or SSH keys reachable from the source workload (see "blast radius"). + +**Hardening:** apply default-deny egress NetworkPolicies and explicitly permit only the destinations the workload needs. For workloads that legitimately use SSH for ops, restrict by destination port and destination address. Audit whether the container even needs SSH client tooling installed — most do not, and removing it reduces the attacker's options post-compromise. + +**If the connection is legitimate:** allowlist the specific destination via a per-rule policy. + +## False Positives + +- **Operator-initiated debug SSH** from within a container during troubleshooting. Distinguishable by entry point (kubectl-exec) and by the operator's username on the destination. +- **Workloads with internal SSH-based deploys** that point at a fleet of hosts on non-standard ports. Typically captured by the Network Neighborhood baseline once it matures. +- **CI runners and orchestrators** that SSH to many destinations as part of their normal job execution. diff --git a/pkg/rules/r1004-exec-from-mount/README.md b/pkg/rules/r1004-exec-from-mount/README.md new file mode 100644 index 0000000..2be0888 --- /dev/null +++ b/pkg/rules/r1004-exec-from-mount/README.md @@ -0,0 +1,50 @@ +# R1004 — Process Executed from Mount + +| Field | Value | +|-------|-------| +| Severity | Medium | +| MITRE Tactic | Execution (TA0002) | +| MITRE Technique | Command and Scripting Interpreter (T1059) | +| Platforms | Host, Kubernetes, ECS | +| Requires Application Profile | Optional | + +## Description + +Detects `exec` calls in a host or container where the executable resides under a Kubernetes-mounted volume path (configMap, secret, emptyDir, hostPath, persistent volume), and the binary was not part of the application profile. Mounted volumes are common attack surfaces because their contents can be modified externally — a compromised configMap, a misconfigured hostPath, or a writable PVC can become a vector for delivering binaries into an otherwise-locked-down container. + +## Attack Technique + +Mapped to **MITRE T1059 — Command and Scripting Interpreter** under **TA0002 — Execution**. Adversaries who cannot directly write into the container's filesystem sometimes pivot through mounted volumes: an attacker who can edit a configMap can drop a script that the workload then executes; an attacker who controls a hostPath gains a write surface that survives container restart. Detecting `exec` from mount paths catches both the "delivery via mounted config" and "delivery via shared storage" variants. + +## How It Works + +The rule combines an application-profile check with a Kubernetes mount-path lookup. For each `exec` event: + +``` +binary not in application profile (same dual-path check as R0001) + AND the binary's path or argv[0] starts with any of the container's mount paths +``` + +`k8s.get_container_mount_paths(namespace, podName, containerName)` returns the list of paths mounted into the container from Kubernetes-managed volumes. The rule fires only when the executed binary's path is under one of those mounts and was not seen during learning. + +## Investigation Steps + +1. **Identify the binary and the mount.** `event.exepath` (or `argv[0]`) together with the matched mount path show which volume delivered the binary. A configMap-mounted binary is a different incident from a hostPath-mounted binary. +2. **Find the source of the binary.** ConfigMap contents come from Kubernetes; a recent change to the configMap (audit logs) often reveals who or what placed the binary. PVC contents come from the underlying storage; trace whoever can write to it. HostPath contents come from the node filesystem and indicate host-level compromise. +3. **Inspect the binary itself.** Hash and analyze it; in many cases it is a benign script that happens to be in a volume, but in attack scenarios it is a downloader, reverse shell, or post-exploitation tool. +4. **Look at the executing process and parent.** A previously-baselined process exec'ing the new binary is interesting (the workload picked up a new code path); a freshly-spawned shell exec'ing it is essentially diagnostic. +5. **Decide: legitimate change or attack.** If legitimate, allowlist. If suspicious, treat as a possible supply-chain or shared-storage compromise. + +## Remediation + +**If malicious:** isolate the container (network policy or seccomp profile), preserve memory and the relevant mount contents, audit the source of the mount (configMap revision history, PVC access logs, hostPath ownership) to understand how the binary arrived, and rotate any credentials the workload had access to (see "blast radius"). If the source is a configMap or PVC shared by multiple workloads, treat all of them as potentially compromised. + +**Hardening:** mark Kubernetes volumes `readOnly: true` where the workload only needs to read them, narrow access to configMaps and PVCs via RBAC, and audit hostPath mounts — most workloads do not need them, and removing them eliminates a major attack surface. + +**If legitimate:** allowlist the specific binary path via a per-rule policy. + +## False Positives + +- **ConfigMap-delivered scripts** that the workload legitimately runs (init scripts, helper utilities). If they were exercised during learning they are already baselined; if added later, allowlist explicitly. +- **Helm chart upgrades** that ship new binaries via configMap rather than image. Common in legacy deploy patterns; consider refactoring to image-based delivery. +- **Operator-controlled mounts** (Operator Framework operators that drop binaries into workloads). Allowlist by operator. diff --git a/pkg/rules/r1005-fileless-execution/README.md b/pkg/rules/r1005-fileless-execution/README.md new file mode 100644 index 0000000..008e3e7 --- /dev/null +++ b/pkg/rules/r1005-fileless-execution/README.md @@ -0,0 +1,50 @@ +# R1005 — Fileless Execution + +| Field | Value | +|-------|-------| +| Severity | High | +| MITRE Tactic | Defense Evasion (TA0005) | +| MITRE Technique | Process Injection (T1055) | +| Platforms | Host, Kubernetes, ECS | +| Requires Application Profile | No | + +## Description + +Detects `exec` calls where the executable backing the process is not a file on disk: a `memfd` (anonymous in-memory file), a `/proc/self/fd/` reference, or a `/proc//fd/` reference to a file descriptor that holds the executable image directly. These execution paths leave no on-disk artifact, defeating file-integrity monitoring, antivirus scanners, and audit pipelines that hash binaries before allowing them to run. They are heavily used by modern Linux malware and post-exploitation frameworks. + +## Attack Technique + +Mapped to **MITRE T1055 — Process Injection** under **TA0005 — Defense Evasion**. The "fileless execution" tradecraft has three common shapes: (1) `memfd_create()` followed by writing the payload and `fexecve()` — the payload exists only in kernel memory tied to a file descriptor; (2) downloading a binary, then `execve('/proc/self/fd/N')` against a still-open file descriptor — the file is unlinked or never written to a stable path; (3) similar techniques against another process's open file descriptors. All three avoid the conventional "write binary to disk, run binary" pattern that most defenders watch. + +## How It Works + +Pure signature on the executable path: + +``` +event.exepath.contains('memfd') + OR event.exepath.startsWith('/proc/self/fd') + OR event.exepath.matches('/proc/[0-9]+/fd/[0-9]+') +``` + +No baseline is needed because legitimate use of these execution paths in a normal workload is essentially nil — these are deliberate evasion primitives. + +## Investigation Steps + +1. **Capture as much of the running process as possible.** The binary is in memory, not on disk. If your tooling supports memory dumping (gcore, criu, or platform equivalent), capture the process image before it exits. +2. **Identify the parent and how the payload arrived.** Look for the syscalls that created the in-memory image: a `memfd_create` immediately before, a downloader process feeding bytes via pipe, or a network connection delivering the payload directly into a file descriptor. +3. **Inspect the process tree.** Fileless execution is rarely a single event — the parent and grandparent processes usually reveal the loader chain (initial exploit, downloader, fileless payload). +4. **Pull surrounding events.** Outbound network connections (R0011), DNS anomalies (R0005), or credential reads frequently surround a fileless execution. +5. **Treat as confirmed intrusion until proven otherwise.** The legitimate base rate for these patterns is near zero. + +## Remediation + +**Active intrusion:** isolate the container immediately (network policy or seccomp profile), preserve process memory if your tooling supports it, identify and revoke any credentials reachable from the workload (see "blast radius"), and rebuild from a known-good image. The fileless payload typically disappears on container restart; capture it before then or accept that you will only have process metadata. + +**Hardening:** apply seccomp policies that deny `memfd_create` and reject `execve` against `/proc/*/fd/*` paths for workloads that never legitimately use them. Drop `CAP_SYS_PTRACE` and `CAP_DAC_READ_SEARCH` so an attacker cannot pivot to another process's file descriptors. Mount the container's root filesystem read-only where the workload tolerates it. + +## False Positives + +- **Just-in-time compilers and dynamic loaders** that legitimately use `memfd_create` to stage generated code. Some language runtimes do this; if so, allowlist by process name and consider whether the workload needs JIT at all. +- **Container init systems** that briefly use `memfd` patterns during setup. Rare and identifiable by process name. + +The base rate of legitimate hits is low enough that false positives should be handled by per-workload exceptions, not by disabling the rule globally. diff --git a/pkg/rules/r1006-unshare-syscall/README.md b/pkg/rules/r1006-unshare-syscall/README.md new file mode 100644 index 0000000..ef0f2df --- /dev/null +++ b/pkg/rules/r1006-unshare-syscall/README.md @@ -0,0 +1,48 @@ +# R1006 — Process tries to escape container + +| Field | Value | +|-------|-------| +| Severity | Medium | +| MITRE Tactic | Privilege Escalation (TA0004) | +| MITRE Technique | Escape to Host (T1611) | +| Platforms | Host, Kubernetes, ECS | +| Requires Application Profile | Optional | + +## Description + +Detects calls to the `unshare` syscall from any process other than `runc` inside a host or container, when the syscall was not part of the application profile's recorded syscall set. `unshare` is the kernel primitive that creates new namespaces, and legitimate use inside an already-namespaced container is exceedingly rare — the container runtime (`runc`) itself uses it once at container setup, after which the workload's processes do not need it. Adversaries use `unshare` as a building block for container-escape techniques that combine namespace manipulation with other primitives (mount syscalls, capability handoffs) to reach the host. + +## Attack Technique + +Mapped to **MITRE T1611 — Escape to Host** under **TA0004 — Privilege Escalation**. Container escape from a less-privileged container typically requires manipulating Linux namespaces: leaving the user namespace to gain real root, leaving the mount namespace to access the host filesystem, leaving the PID namespace to interact with host processes. `unshare` (alongside `setns` and `clone` with namespace flags) is the syscall that performs these manipulations. Detecting on it catches the escape attempt early, before the attacker can complete the chain. + +## How It Works + +``` +event.pcomm != 'runc' + AND !ap.was_syscall_used(containerId, 'unshare') +``` + +The first clause filters out the one legitimate caller (the container runtime during setup). The second suppresses any workload that was observed using `unshare` during learning, which is uncommon but not impossible for some niche workloads. + +## Investigation Steps + +1. **Identify the calling process and its parent.** `event.comm` and `event.pcomm` tell you who and from where. An `unshare` call from a shell, a freshly-dropped binary, or a network-facing process is essentially diagnostic of an escape attempt. +2. **Check the container's capabilities.** `unshare` for most namespace types requires `CAP_SYS_ADMIN`. If the container has it, the call may succeed; if it does not, the call failed but the attempt is still highly suspicious. +3. **Inspect surrounding syscalls.** Escape chains typically combine `unshare` with `setns`, `mount`, `pivot_root`, or `chroot` calls. Looking at the syscall sequence shows whether this is a probe or a complete escape attempt. +4. **Determine whether the escape succeeded.** If the process is no longer in the container's PID namespace (visible from host-level tooling), assume host compromise. +5. **Treat as serious incident.** Even an attempted escape indicates the attacker has root-equivalent privileges inside the container. + +## Remediation + +**Active escape attempt:** isolate the container (network policy or seccomp profile), preserve memory, capture syscall traces if possible, and if the escape may have succeeded, treat the host as compromised and isolate it as well (see "blast radius"). Trace the upstream activity that allowed the attacker to call `unshare` in the first place — usually a freshly-dropped binary and an over-permissive capability set. + +**Hardening:** drop `CAP_SYS_ADMIN` from every container that does not strictly require it (which is almost all of them). Apply seccomp policies that deny the `unshare` syscall entirely on workloads that never use it. Use a less-privileged container runtime where possible (gVisor, kata-containers) to add another isolation layer. + +**Legitimate use:** rare. Some specialized workloads (container-in-container CI runners, sandbox tools) legitimately call `unshare`. Allowlist these explicitly rather than disabling the rule. + +## False Positives + +- **Container-in-container patterns** where the workload itself runs other containers (CI runners, k3d, etc.). These genuinely need `unshare` and are best allowlisted by process. +- **Sandboxing tools** like Firejail, Bubblewrap, or unshare-based test harnesses inside a container. +- **Custom init systems** that re-namespace processes during workload bootstrap. Rare; identifiable by process name and parent. diff --git a/pkg/rules/r1007-xmr-crypto-mining/README.md b/pkg/rules/r1007-xmr-crypto-mining/README.md new file mode 100644 index 0000000..bb388e4 --- /dev/null +++ b/pkg/rules/r1007-xmr-crypto-mining/README.md @@ -0,0 +1,46 @@ +# R1007 — Crypto Miner Launched (XMR / RandomX) + +| Field | Value | +|-------|-------| +| Severity | Critical | +| MITRE Tactic | Impact (TA0040) | +| MITRE Technique | Resource Hijacking (T1496) | +| Platforms | Host, Kubernetes, ECS | +| Requires Application Profile | No | + +## Description + +Detects execution of a process whose CPU instruction profile matches the RandomX algorithm — the proof-of-work function used by Monero (XMR) and several other privacy-coin miners. The node agent observes the process's instruction stream and recognizes the distinctive memory and arithmetic pattern of RandomX hashing. The detection is algorithmic rather than signature-based: it does not rely on the miner binary's name or hash, so renaming `xmrig` to `nginx-worker` will not evade it. + +## Attack Technique + +Mapped to **MITRE T1496 — Resource Hijacking** under **TA0040 — Impact**. Cryptomining is one of the most common post-exploitation monetization paths in cloud environments: once an attacker has code execution they deploy a miner that uses the workload's CPU (or, more lucratively, the cluster's entire CPU pool if they can spread) to mine cryptocurrency. Monero with its RandomX proof-of-work is overwhelmingly the coin of choice because RandomX is CPU-friendly and resistant to GPU/ASIC acceleration, so a stolen CPU cycle is worth more than for most other coins. + +## How It Works + +``` +event type == 'randomx' +``` + +The `randomx` event is emitted by the node agent when its on-CPU profiler detects the characteristic instruction and memory-access pattern of RandomX hashing in a running process. The rule fires unconditionally on any such event — legitimate hits are essentially zero because no real workload incidentally runs RandomX as a side effect. + +## Investigation Steps + +1. **Capture the running process.** `event.exepath` and `event.comm` point at the binary. Hash and exfiltrate it before the attacker cleans up. +2. **Identify how the miner arrived.** Walk back from the alert: a freshly-dropped binary in the upperlayer (R1001), a fileless execution (R1005), an exec from `/dev/shm` (R1000), or a binary delivered via mounted volume (R1004) are the usual entry points. +3. **Determine the scope.** Mining is typically not a single-pod event — attackers spread the miner to as many workloads as their access permits. Check sibling pods in the namespace, other workloads using the same compromised service account, and the broader cluster. +4. **Identify the C2 / pool endpoint.** The miner has to connect somewhere to receive work and submit shares. Outbound connections from the workload (R0011, R1009) usually show the destination pool. +5. **Treat as confirmed intrusion.** RandomX detection has near-zero false positive rate; the legitimate base rate is effectively zero. + +## Remediation + +**Active intrusion:** kill the miner process, isolate the container's egress (network policy denying the destination, or full quarantine), preserve the binary for forensics, and rotate any credentials the compromised workload had access to (see "blast radius"). Rebuild from a known-good image; do not patch in place. Search the rest of the cluster for the same binary or for other workloads connecting to the same pool — mining payloads spread. + +**Hardening:** apply CPU limits to all containers; capping a workload at its real CPU need makes mining economically uninteresting, even if a payload lands. Apply default-deny egress NetworkPolicies — miners must connect outbound to a pool, so denying the connection breaks the operation. Audit the image supply chain; many mining incidents originate from a compromised base image or untrusted third-party image. + +## False Positives + +- **Cryptographic test suites** that exercise RandomX as part of an algorithm correctness check. Rare in production workloads. +- **Cryptocurrency-native applications** (block explorers, payment processors handling XMR) that legitimately compute RandomX as part of their function. These should be allowlisted explicitly by image identity. + +The legitimate base rate is low enough that any hit warrants a real investigation, not a routine triage-and-dismiss. diff --git a/pkg/rules/r1008-crypto-mining-domain-communication/README.md b/pkg/rules/r1008-crypto-mining-domain-communication/README.md new file mode 100644 index 0000000..770d88c --- /dev/null +++ b/pkg/rules/r1008-crypto-mining-domain-communication/README.md @@ -0,0 +1,48 @@ +# R1008 — Crypto Mining Domain Communication + +| Field | Value | +|-------|-------| +| Severity | Critical | +| MITRE Tactic | Command and Control (TA0011) | +| MITRE Technique | Application Layer Protocol: DNS (T1071.004) | +| Platforms | Host, Kubernetes, ECS | +| Requires Application Profile | No | + +## Description + +Detects DNS lookups for any domain in a curated list of well-known cryptomining pool hostnames. The list covers ~90 of the most common Monero, Ethereum-classic, Zcash, and multi-coin mining-pool domains and their regional shards (e.g. `xmr.nanopool.org.`, `eu1.ethermine.org.`, `pool.minexmr.com.`, `supportxmr.com.`). A DNS lookup for any of these from a workload that is not itself a mining-pool client is essentially diagnostic of an in-progress mining incident. + +## Attack Technique + +Mapped to **MITRE T1071.004 — Application Layer Protocol: DNS** under **TA0011 — Command and Control**. Cryptominers in compromised workloads need to reach a mining pool to receive work and submit shares. Most miners are configured with a hostname rather than an IP address (so the pool operator can rebalance load), and DNS is the universally-allowed egress channel. Catching the lookup at the DNS layer is cheap, signature-driven, and independent of whether the connection itself reaches the pool successfully. + +## How It Works + +Pure signature match against a fixed allowlist of mining-pool domains: + +``` +event.name in [] +``` + +The list lives in the rule definition's `ruleExpression.expression` and is maintained centrally. Domains are matched with the trailing dot (`xmr.nanopool.org.`) which is the canonical form delivered by the DNS subsystem. + +## Investigation Steps + +1. **Confirm the lookup and the requesting process.** `event.name` is the domain queried; `event.comm` is the process. A workload that has no business looking up `pool.minexmr.com.` doing so is the entire story. +2. **Check whether the connection succeeded.** A DNS lookup is necessary but not sufficient — surrounding events (an outbound TCP connection to the resolved address, ideally on port 3333 or 45700, R1009) confirm the miner actually reached the pool. +3. **Find the miner binary.** If the lookup happened, a process called the resolver. Identify that process (R0001 may have fired on it) and capture the binary for forensics. +4. **Map the spread.** Mining payloads rarely target a single workload. Check sibling pods, other workloads using the same compromised SA, and the entire cluster for the same domain in DNS logs. +5. **Treat as confirmed intrusion.** The false-positive rate of this rule is essentially nil. + +## Remediation + +**Active intrusion:** apply an egress policy that denies the domain (or denies all DNS to the resolver if the workload should have no external DNS), kill the miner process, isolate the container, preserve the binary, and rotate any credentials the compromised workload had access to (see "blast radius"). Search the cluster for the same domain in DNS or for the same binary across workloads — mining payloads spread. + +**Hardening:** apply default-deny egress NetworkPolicies; the miner cannot mine if it cannot reach a pool. Apply CPU limits to all containers; if the miner does run, it is economically uninteresting. Audit the image supply chain for the original ingress vector. + +## False Positives + +- **Threat-intel and security-research workloads** that legitimately query mining-pool domains for monitoring or research. These should be explicitly allowlisted by namespace or workload. +- **Vulnerability scanners and red-team tooling** running inside the cluster as part of authorized exercises. Coordinate with the responsible team to scope. + +The base rate of legitimate hits is near zero. A hit should escalate to incident response, not be silenced. diff --git a/pkg/rules/r1009-crypto-mining-related-port/README.md b/pkg/rules/r1009-crypto-mining-related-port/README.md new file mode 100644 index 0000000..a2690da --- /dev/null +++ b/pkg/rules/r1009-crypto-mining-related-port/README.md @@ -0,0 +1,65 @@ +# R1009 — Crypto Mining Related Port Communication + +| Field | Value | +|-------|-------| +| Severity | Low | +| MITRE Tactic | Command and Control (TA0011) | +| MITRE Technique | Application Layer Protocol (T1071) | +| Platforms | Host, Kubernetes, ECS | +| Requires Application Profile | No (uses Network Neighborhood) | + +## Description + +Detects outbound TCP connections from a host or container to well-known cryptomining pool ports (`3333`, `45700`). Cryptomining is a common post-exploitation monetization path: once an adversary gains code execution in a container they often deploy a miner that connects to a Stratum pool. The default Stratum ports are a strong network-layer fingerprint. + +This rule does not raise alerts on its own - it is a building block consumed by higher-level detections that correlate it with other signals (e.g. a previously-unseen process making the connection, high CPU, or other mining-adjacent activity). + +## Attack Technique + +Mapped to **MITRE T1071 — Application Layer Protocol** under **TA0011 — Command and Control**. Cryptominers in compromised containers typically connect outbound to a mining pool using the Stratum protocol over TCP. While the protocol itself is benign-looking application-layer traffic, the ports are well-known and rarely match anything legitimate. Detecting on these ports gives a cheap, low-false-positive way to surface mining-adjacent C2 traffic without inspecting payload contents. + +## How It Works + +Pure network-event signature with a network-baseline suppression: + +``` +event.proto == 'TCP' + AND event.pktType == 'OUTGOING' + AND event.dstPort in [3333, 45700] + AND !nn.was_address_in_egress(event.containerId, event.dstAddr) +``` + +Four gates: + +1. **TCP only.** UDP traffic on the same ports does not trigger — Stratum is TCP. +2. **Outgoing only.** Inbound connections to these ports (an unlikely accident) are ignored. +3. **Destination port matches the watchlist** — `3333` and `45700` are the canonical Stratum and known mining-pool ports the rule tracks. The list is in the rule's `state.ports` field for traceability. +4. **Destination IP is not already in the container's Network Neighborhood.** If the workload has been observed talking to this address during the learning window, the connection is treated as part of its baseline and suppressed, this is what filters out, for example, an internal service that happens to listen on `:3333`. + +The baseline reference is **Network Neighborhood**, not Application Profile — they are separate per-container baselines tracking different surfaces. + +## Investigation Steps + +1. **Identify the process making the connection.** `event.comm` and `event.pid` point at the connecting binary. If it is unknown to the workload, a freshly-deployed binary you have no record of, that alone is essentially diagnostic for mining when paired with the destination port. +2. **Look up the destination.** `event.dstAddr` resolved to a known mining pool (or an IP block historically used by one) is a strong confirmation. Threat-intel feeds and public mining-pool IP lists are useful here. +3. **Check resource usage.** Cryptominers are CPU-bound by design. Spike in container CPU around the connection timestamp is corroborating evidence. +4. **Pull the broader picture.** Mining payloads usually arrive via a previous compromise (exploited app, leaked credentials, malicious image). Look for earlier alerts on the same container, file writes to `/tmp` or `/dev/shm`, shells spawned from web servers, unexpected outbound HTTP downloads, to identify the ingress vector. +5. **Decide the response.** Even though this rule is Low severity in isolation, in combination with a positive process-identification step it should escalate to incident-response posture. + +## Remediation + +**Confirmed mining activity:** kill the connecting process, isolate the container's egress (cluster network policy that denies the destination, or a full container quarantine), preserve a binary sample for analysis, and rotate any secrets the container had access to. The container should be redeployed from a known-good image, not patched in place. + +**Hardening:** + +- Apply egress NetworkPolicies that allow only the destinations the workload needs. A default-deny egress posture stops mining payloads from ever phoning home. +- Set CPU limits on containers — even legitimate workloads benefit, and mining payloads become economically uninteresting when capped. +- Audit the image supply chain — many mining incidents trace back to compromised base images or unverified third-party images. + +## False Positives + +- **Workloads that legitimately use port 3333 or 45700.** Some internal services pick these ports without knowing they overlap mining pools. The Network Neighborhood suppression handles this automatically once the baseline observes the destination — so for steady-state workloads the rate should be near zero. For new workloads, allow the baseline to mature before treating R1009 hits as actionable. +- **Penetration tests or red-team exercises** that intentionally simulate mining traffic. Expected; coordinate with the responsible team to scope alerts. +- **Service discovery or health checks scanning a wide port range** that happen to hit `3333` or `45700` outbound TCP. Rare in practice — most discovery is destination-pinned, not arbitrary outbound. + +Because the rule does not directly raise alerts, low-confidence solitary hits are absorbed by the correlation layer. Tune at that layer rather than disabling R1009. diff --git a/pkg/rules/r1010-symlink-created-over-sensitive-file/README.md b/pkg/rules/r1010-symlink-created-over-sensitive-file/README.md new file mode 100644 index 0000000..ec04573 --- /dev/null +++ b/pkg/rules/r1010-symlink-created-over-sensitive-file/README.md @@ -0,0 +1,46 @@ +# R1010 — Symlink Created Over Sensitive File + +| Field | Value | +|-------|-------| +| Severity | Medium | +| MITRE Tactic | Credential Access (TA0006) | +| MITRE Technique | Data from Local System (T1005) | +| Platforms | Host, Kubernetes, ECS | +| Requires Application Profile | Optional | + +## Description + +Detects the creation of a symbolic link whose target is `/etc/shadow` or `/etc/sudoers` (or any path beginning with those prefixes), by a process whose application profile does not include access to that target. Symlink creation against sensitive files is a classic privilege-escalation and credential-access trick: an attacker who can write a symlink into a directory readable by a more privileged process can cause that process to read the sensitive target through the symlinked path, bypassing access controls or audit rules that watch only direct opens. + +## Attack Technique + +Mapped to **MITRE T1005 — Data from Local System** under **TA0006 — Credential Access**. The "symlink games" tradecraft has two main shapes: (1) a less-privileged attacker creates a symlink at a path that a more-privileged process will read, redirecting the privileged read to a sensitive file; (2) an attacker creates the symlink themselves and reads through it to bypass audit rules that match on the target path string rather than the resolved inode. Either way, the symlink creation event is the earliest catch-point before the actual sensitive data is exposed. + +## How It Works + +``` +(event.oldPath.startsWith('/etc/shadow') OR event.oldPath.startsWith('/etc/sudoers')) + AND !ap.was_path_opened(containerId, event.oldPath) +``` + +`event.oldPath` is the symlink's target (what it points at). The rule fires when the target is one of the sensitive prefixes and the workload was not observed legitimately opening that target during learning. The profile check exists so workloads that genuinely read these files during normal operation (like PAM-using authentication code) suppress the rule. + +## Investigation Steps + +1. **Identify the new symlink and its target.** `event.newPath` is where the symlink was placed, `event.oldPath` is what it points at. The location of the symlink often reveals the intent: a symlink at `/tmp/x` pointing to `/etc/shadow` is for the attacker's own use; a symlink at a path the workload's privileged process is about to read is a trap for that process. +2. **Identify the creating process.** `event.comm` and parent process point at who set the trap. An unfamiliar binary creating symlinks into sensitive paths is essentially diagnostic. +3. **Look for the consumer.** A privileged process opening the symlink path shortly after creation completes the attack. Cross-correlate symlink creation with subsequent opens on the symlink path. +4. **Inventory other symlinks.** Attackers rarely set just one. Walk the workload's filesystem for symlinks targeting sensitive paths. +5. **Treat as confirmed credential-access attempt.** The legitimate base rate of symlinks to `/etc/shadow` or `/etc/sudoers` is essentially zero. + +## Remediation + +**Active intrusion:** remove the symlink, identify and audit any reads that may have followed it (was the privileged process tricked into reading the sensitive file?), rotate any credentials whose hashes could have been exposed (see "blast radius"), isolate the workload, and rebuild from a known-good image. + +**Hardening:** apply seccomp policies that deny `symlink`/`symlinkat` syscalls on workloads that never legitimately create symlinks. Use Linux's `fs.protected_symlinks` sysctl (set to 1) on the host to prevent the most common symlink-following attacks. Mount sensitive directories read-only inside containers; do not bind-mount the host's `/etc/shadow` or `/etc/sudoers` into containers. + +## False Positives + +- **Backup and snapshot tools** that legitimately symlink sensitive files for archival. These should be explicitly allowlisted by process. +- **Configuration management tools** (Ansible, Puppet) that create symlinks for `/etc/shadow` rotation patterns. Rare; identifiable by process name. +- **Distribution-specific PAM helpers** that legitimately symlink shadow during early boot of some minimal containers. diff --git a/pkg/rules/r1011-ld-preload-hook/README.md b/pkg/rules/r1011-ld-preload-hook/README.md new file mode 100644 index 0000000..f0d7ad9 --- /dev/null +++ b/pkg/rules/r1011-ld-preload-hook/README.md @@ -0,0 +1,54 @@ +# R1011 — ld_preload Hook Technique + +| Field | Value | +|-------|-------| +| Severity | Medium | +| MITRE Tactic | Defense Evasion (TA0005) | +| MITRE Technique | Hijack Execution Flow: Dynamic Linker Hijacking (T1574.006) | +| Platforms | Host, Kubernetes, ECS | +| Requires Application Profile | Optional | + +## Description + +Detects two related techniques that use the Linux dynamic linker to inject code into processes: (1) running a process with the `LD_PRELOAD` environment variable set (so the linker loads attacker-supplied shared objects before the program's own dependencies), and (2) writes to `/etc/ld.so.preload` (a system-wide file that the linker reads at every exec, achieving the same code injection without needing to set environment variables). The rule is disabled by default because some legitimate runtimes (Java's `LD_PRELOAD`, MATLAB containers) use these mechanisms. + +## Attack Technique + +Mapped to **MITRE T1574.006 — Hijack Execution Flow: Dynamic Linker Hijacking** under **TA0005 — Defense Evasion**. `LD_PRELOAD` is a foundational primitive for Linux userland rootkits: a malicious `.so` loaded into every process can intercept libc calls, hide files from `ls`, hide processes from `ps`, intercept network connections, and redirect logging. The system-wide `/etc/ld.so.preload` variant achieves the same effect with one write to one file, surviving subsequent process launches. + +## How It Works + +Two event types, evaluated independently: + +``` +exec: + event.comm != 'java' + AND event.containerName != 'matlab' + AND process.get_ld_hook_var(event.pid) != '' + +open: + event.path == '/etc/ld.so.preload' + AND event.flagsRaw is set and non-zero +``` + +The exec arm reads the running process's `LD_PRELOAD` variable from procfs; if non-empty (and the process is not one of the well-known legitimate users), it fires. The open arm fires whenever `/etc/ld.so.preload` is opened with any flags set — usually because something is writing it. The two-process allowlist (`java`, `matlab`) covers the most common legitimate users; other legitimate uses should be allowlisted via a per-rule policy rather than expanding the rule's hardcoded list. + +## Investigation Steps + +1. **Identify which arm fired.** The exec arm tells you a process started with `LD_PRELOAD` set; the open arm tells you `/etc/ld.so.preload` was opened (almost certainly written). They sometimes fire together when an attacker is establishing persistence. +2. **For the exec arm: capture the preloaded library.** `process.get_ld_hook_var(event.pid)` returns the path(s) of the preloaded shared object(s). Read or hash these binaries before the attacker cleans up; static analysis of a userland rootkit's `.so` is usually fast and revealing. +3. **For the open arm: read the file content.** `/etc/ld.so.preload` listing one or more paths means every subsequent exec on the host (or container if locally scoped) will load those libraries. The listed paths are the rootkit components. +4. **Identify the persisting process.** Whoever set `LD_PRELOAD` or wrote `/etc/ld.so.preload` is the active loader. Walk the parent chain to find the entry point. +5. **Treat as persistence in flight.** Even if the immediately-affected process is benign, the same vector affects every subsequent exec. + +## Remediation + +**Active intrusion:** remove the offending entry — unset `LD_PRELOAD` from the affected process's environment (often requires killing and respawning), and delete or restore `/etc/ld.so.preload` to empty. Capture the malicious `.so` for analysis. Isolate the workload (network policy or seccomp profile), preserve memory, rotate credentials reachable from the workload (see "blast radius"), and rebuild from a known-good image since rootkit techniques often plant multiple persistence mechanisms. + +**Hardening:** mount `/etc/ld.so.preload` read-only or via an immutable filesystem mount. Apply seccomp policies that deny writes to `/etc/ld.so.preload`. Drop `CAP_DAC_OVERRIDE` from containers so an attacker cannot write to root-owned files. For workloads that need a legitimate `LD_PRELOAD`, allowlist the specific library path rather than disabling the rule. + +## False Positives + +- **Java workloads** using `LD_PRELOAD` for native-library shimming. The rule excludes `java` by name, but Java wrappers or JVM forks with different process names may still trigger. Allowlist by image or by the specific wrapper. +- **APM and profiler agents** that inject via `LD_PRELOAD` (some attach mechanisms work this way). Allowlist by agent name and SHA. +- **Custom build wrappers** in CI that preload sanitizers or instrumentation libraries. diff --git a/pkg/rules/r1012-hardlink-created-over-sensitive-file/README.md b/pkg/rules/r1012-hardlink-created-over-sensitive-file/README.md new file mode 100644 index 0000000..b7b8140 --- /dev/null +++ b/pkg/rules/r1012-hardlink-created-over-sensitive-file/README.md @@ -0,0 +1,46 @@ +# R1012 — Hard Link Created Over Sensitive File + +| Field | Value | +|-------|-------| +| Severity | Medium | +| MITRE Tactic | Credential Access (TA0006) | +| MITRE Technique | Data from Local System (T1005) | +| Platforms | Host, Kubernetes, ECS | +| Requires Application Profile | Optional | + +## Description + +Detects the creation of a hard link whose target is `/etc/shadow` or `/etc/sudoers` (or any path beginning with those prefixes), by a process whose application profile does not include legitimate access to that target. Hard links create a second name for the same inode, sharing data and permissions but living in a directory the attacker controls. Unlike symlinks, hardlinks survive deletion of the original name and do not have a separate file mode, which makes them harder to audit and harder to remove cleanly. + +## Attack Technique + +Mapped to **MITRE T1005 — Data from Local System** under **TA0006 — Credential Access**. The hardlink trick complements symlink games: an attacker who can hardlink `/etc/shadow` into a path they own can read the hash content through their own filename, defeating audit rules that watch only `/etc/shadow` by path string. Some defenses that monitor file access by path will miss the hardlinked read entirely; the inode is the same, but the path differs. + +## How It Works + +``` +(event.oldPath.startsWith('/etc/shadow') OR event.oldPath.startsWith('/etc/sudoers')) + AND !ap.was_path_opened(containerId, event.oldPath) +``` + +`event.oldPath` is the hardlink's target (the existing file the new name will share an inode with). The rule fires when the target is sensitive and the workload was not observed legitimately opening that target during learning. + +## Investigation Steps + +1. **Identify the new hardlink and its target.** `event.newPath` is where the new name was placed, `event.oldPath` is the existing file. The new name's location often reveals intent (a hardlink in `/tmp/` pointing at `/etc/shadow` is for the attacker; a hardlink in a path a privileged process reads is a trap). +2. **Identify the creating process.** `event.comm` and the parent process show who. An unfamiliar binary creating hardlinks into sensitive paths is essentially diagnostic. +3. **Look for the read of the hardlinked path.** A read of `event.newPath` shortly after the link is created completes the attack — the attacker (or a tricked privileged process) reads sensitive content through their controlled name. +4. **Inventory other hardlinks.** `find / -inum ` against the sensitive file's inode reveals every name currently pointing at it; an attacker may have planted more than one. +5. **Treat as confirmed credential-access attempt.** Legitimate hardlinks to `/etc/shadow` or `/etc/sudoers` are essentially nonexistent in normal workloads. + +## Remediation + +**Active intrusion:** remove the hardlink (unlink the new name, which leaves the original intact; verify by checking the link count on the original file). Audit reads that may have followed via the attacker-controlled path. Rotate credentials whose hashes could have been exposed (see "blast radius"), isolate the workload, and rebuild from a known-good image. + +**Hardening:** Linux's `fs.protected_hardlinks` sysctl (set to 1) on the host prevents users from hardlinking files they do not own — apply at the host level. Apply seccomp policies that deny the `link` and `linkat` syscalls on workloads that never legitimately create hardlinks. Do not bind-mount sensitive host files into containers. + +## False Positives + +- **Backup and snapshot tools** that use hardlink-based deduplication (rsnapshot, restic in some configurations). These should be allowlisted by process. +- **Configuration management tools** that hardlink during atomic file replacement patterns. Rare for sensitive files. +- **Container-image extraction tools** that use hardlinks to represent shared image layers. Identifiable by process and namespace. diff --git a/pkg/rules/r1015-malicious-ptrace-usage/README.md b/pkg/rules/r1015-malicious-ptrace-usage/README.md new file mode 100644 index 0000000..8577547 --- /dev/null +++ b/pkg/rules/r1015-malicious-ptrace-usage/README.md @@ -0,0 +1,49 @@ +# R1015 — Malicious Ptrace Usage + +| Field | Value | +|-------|-------| +| Severity | Medium | +| MITRE Tactic | Defense Evasion (TA0005) | +| MITRE Technique | Debugger Evasion (T1622) | +| Platforms | Host, Kubernetes, ECS | +| Requires Application Profile | No | + +## Description + +Detects any use of the `ptrace` syscall by a process inside a host or container. `ptrace` is the kernel primitive that allows one process to inspect and modify another: read memory, write memory, intercept system calls, control execution. Legitimate use in production is rare (mostly debuggers and a handful of language-runtime helpers); use by attacker tooling is common — process injection, credential extraction from running processes, anti-debugging checks against the EDR itself. Because the legitimate base rate is so low, any `ptrace` event is worth investigating. + +## Attack Technique + +Mapped to **MITRE T1622 — Debugger Evasion** under **TA0005 — Defense Evasion**. Adversary uses of `ptrace` include: (1) attaching to a running process to extract secrets from its memory (e.g. SSH keys from `ssh-agent`, decrypted credentials from a long-running daemon); (2) injecting code into another process so the malicious code runs under the target process's identity; (3) detecting whether the attacker's own payload is being traced by a defender's debugger, and refusing to run if so. All three patterns trip this rule. + +## How It Works + +Pure signature on the syscall presence: + +``` +event type == 'ptrace' +``` + +The agent emits a `ptrace` event whenever the syscall is invoked. No baseline check is needed — the legitimate base rate is low enough that allowlisting (per process or per workload) is preferred to baselining. + +## Investigation Steps + +1. **Identify the tracer and the target.** `event.comm` is the process making the `ptrace` call; the syscall arguments name the target PID. A `ptrace` of an unrelated workload process is essentially diagnostic. +2. **Determine the ptrace operation.** `PTRACE_ATTACH` / `PTRACE_SEIZE` start a debugging session; `PTRACE_PEEKDATA` / `PTRACE_POKEDATA` read or write memory; `PTRACE_TRACEME` is benign anti-debugging. The operation narrows the intent immediately. +3. **Check the container's capabilities.** `ptrace` of another process requires the kernel's YAMA LSM allowance (`/proc/sys/kernel/yama/ptrace_scope`), `CAP_SYS_PTRACE`, or being the parent of the target. A container with `CAP_SYS_PTRACE` granted is itself a finding even if the call is benign. +4. **Inspect the target process.** If the target was a credential-holding daemon (`ssh-agent`, `gpg-agent`, the workload's own auth code), treat the secrets in its memory as exposed. +5. **Treat as serious incident.** The combination of low legitimate base rate and high attacker utility makes `ptrace` events high-signal. + +## Remediation + +**If malicious:** isolate the container (network policy or seccomp profile), preserve memory of both tracer and target, treat any in-memory secrets in the target process as exposed and rotate them (see "blast radius"), and rebuild from a known-good image. + +**Hardening:** drop `CAP_SYS_PTRACE` from every container that does not legitimately debug (which is almost all of them). Apply seccomp policies that deny the `ptrace` syscall entirely on workloads that never use it. Set the host's `kernel.yama.ptrace_scope` sysctl to `2` (admin-only ptrace) or `3` (no ptrace at all) where the workload tolerates it. + +**Legitimate uses:** debuggers (gdb, lldb, delve) attached to a running process during a kubectl-exec session; some language runtimes that briefly trace child processes; security tools that use ptrace for sandboxing. Allowlist these explicitly by process name. + +## False Positives + +- **Debug sessions** opened by operators via kubectl-exec or SSH. Expected and identifiable by the entry-point process. +- **Language runtimes that ptrace child processes** for sandboxing (some Python sandboxes, some Ruby debuggers). Allowlist by runtime image. +- **Container init systems** that briefly trace children during setup. Rare and identifiable by `event.pcomm`. diff --git a/pkg/rules/r1030-unexpected-io_uring-operation/README.md b/pkg/rules/r1030-unexpected-io_uring-operation/README.md new file mode 100644 index 0000000..a2a8a55 --- /dev/null +++ b/pkg/rules/r1030-unexpected-io_uring-operation/README.md @@ -0,0 +1,48 @@ +# R1030 — Unexpected io_uring Operation + +| Field | Value | +|-------|-------| +| Severity | Medium | +| MITRE Tactic | Execution (TA0002) | +| MITRE Technique | System Binary Proxy Execution (T1218) | +| Platforms | Host, Kubernetes, ECS | +| Requires Application Profile | Yes | + +## Description + +Detects io_uring operations from a host or container that were not observed during the learning window. `io_uring` is a modern Linux asynchronous-I/O interface that performs system-call-like operations (file reads, network sends, opens, splices) through a shared ring buffer between user space and the kernel — without going through the conventional syscall entry path. This means that defenders watching the syscall table miss io_uring activity entirely. Adversaries have noticed: io_uring is increasingly used as a "syscall bypass" channel for filesystem manipulation, network I/O, and process operations that would otherwise be visible to security tooling. + +## Attack Technique + +Mapped to **MITRE T1218 — System Binary Proxy Execution** under **TA0002 — Execution**. Modern Linux post-exploitation frameworks have added io_uring backends to perform reads, writes, opens, and even some exec-like operations without entering the syscall instruction. EDRs and audit pipelines that hook only the syscall path do not see this activity. Detecting on io_uring opcode use (rather than only on syscalls) closes the gap. + +## How It Works + +``` +event type == 'iouring' + // implicit: not in application profile syscall set +``` + +The node agent emits an `iouring` event for io_uring operations and surfaces the opcode and flags. Because the application profile's `syscalls: all` covers io_uring's pseudo-syscall opcodes for baselining purposes, the workload's recorded io_uring usage suppresses the rule for repeat operations; only novel opcodes trigger. + +## Investigation Steps + +1. **Identify the opcode and the process.** `event.opcode`, `event.flagsRaw`, and `event.comm` describe what was attempted via io_uring. Opcodes for `READ`, `WRITE`, `OPENAT`, `CONNECT`, `SENDMSG` are the typical attacker uses; the opcode list is in the kernel headers. +2. **Map the opcode to its effect.** An `OPENAT` opcode opening `/etc/shadow` is identical in effect to a regular `open` syscall, just invisible to a syscall-only audit. The investigation against the target follows the same path as the corresponding syscall would. +3. **Identify the process and look for io_uring usage history.** Many legitimate workloads use io_uring for performance; new use of io_uring by a workload that previously did not is the signal. +4. **Pull surrounding events.** Adversary chains that use io_uring usually still emit some events the standard path catches (network connections, file events) — the io_uring miss is on the syscalls themselves, not on the events. +5. **Treat as bypass attempt.** The pattern indicates an attacker aware enough of monitoring to choose io_uring as a channel. + +## Remediation + +**If malicious:** isolate the container (network policy or seccomp profile), preserve memory, audit which files were opened and which network destinations contacted via io_uring (the agent surfaces these), rotate any credentials reachable from the workload (see "blast radius"), and rebuild from a known-good image. + +**Hardening:** apply seccomp policies that deny the `io_uring_setup`, `io_uring_register`, and `io_uring_enter` syscalls on workloads that do not need them. Some hardened distributions disable io_uring entirely at the kernel level (`CONFIG_IO_URING=n`); consider this for workloads that never legitimately use it. Where io_uring is needed, restrict it via the IORING_REGISTER_RESTRICTIONS feature to a specific opcode set. + +**If legitimate:** allowlist the specific opcode via a per-rule policy. High-performance database and storage workloads are the most common legitimate users. + +## False Positives + +- **Modern high-performance workloads** that have recently adopted io_uring (recent versions of nginx, ScyllaDB, some databases). If they were exercised during learning, their opcode set is in the baseline; new opcodes after learning will trigger. +- **Workload version upgrades** that introduce io_uring usage where the previous version used regular syscalls. +- **Runners and orchestrators** where workload code paths vary and io_uring use is impossible to predict from learning.