Skip to content

Latest commit

 

History

History
375 lines (242 loc) · 23.3 KB

File metadata and controls

375 lines (242 loc) · 23.3 KB

Detection rules

This page is generated from tools/gen-rule-docs by reading the rulesapi.RuleMetadata.Doc field on every rule registered in server/cmd/fleet-edr-server/main.go. To refresh after changing a rule's documentation, run:

go run ./tools/gen-rule-docs

Hand-edits to this file get overwritten on the next regeneration.

Index

Rule ID Title Severity ATT&CK
suspicious_exec Suspicious exec chain high T1059, T1105
persistence_launchagent LaunchAgent persistence high T1543.001
dyld_insert DYLD injection on exec high T1574.006
shell_from_office Shell spawned by Microsoft Office high T1566.001, T1059.004
osascript_network_exec AppleScript dropper critical T1059.002, T1105
credential_keychain_dump Keychain credential dump high T1555.001
privilege_launchd_plist_write LaunchDaemon persistence high T1543.004
sudoers_tamper Sudoers tamper high T1548.003
application_control_block Application control block medium
dns_c2_beacon DNS C2 beacon high T1071.004, T1568.002
sensor_tamper EDR sensor disabled high T1562.001
sensor_recovery_failed EDR sensor could not be restored critical T1562.001

suspicious_exec

Suspicious exec chain
Flags a non-shell process that spawns a shell which, within 30 seconds, execs from /tmp or makes an outbound network connection.

Rule ID suspicious_exec
Severity high
ATT&CK T1059, T1105
Event types exec, network_connect

Description

Detects two related chain shapes that share a single attribution chain:

  1. non-shell parent → shell child → temp-directory exec (e.g. /tmp/payload)
  2. non-shell parent → shell child → outbound network_connect

The rule fires on the LAST link of the chain (the temp-exec or the network_connect) rather than the shell's exec. That makes it race-immune across the agent's flush boundaries: a chain that completes in ~150ms but straddles a 1-second flush boundary still resolves cleanly because the entire ancestor chain has already been ingested by the time the trigger event lands.

Both arms find the shell whether it forked the payload or replaced itself with it. A shell invoked as zsh -c '<cmd>' commonly execs the payload at its own PID rather than forking, which leaves no shell in the payload's parent chain; the rule looks at that PID's own exec history as well, so the shell is still attributed. This matters because the shells behave differently on macOS: zsh replaces itself, while bash and sh fork, so an identical command would otherwise be detected under one shell and missed under another.

30 seconds is the temporal cap between the shell exec and the trigger event.

Known false-positive sources

  • Interactive SSH where an admin runs a script from /tmp and/or curls a tool. Add a parent-path-glob exclusion for /usr/libexec/sshd-session via the detection-config surface if that's a routine workflow on the host class.
  • Developer tooling that shells out and connects (Claude Code, lefthook git hooks, git, IDEs). These install under version-stamped paths, so add a parent-path-glob exclusion such as */claude/versions/* or */lefthook_* that survives upgrades.
  • Some Apple-signed installer-postflight scripts shell out to /tmp/ during package install.

Limitations

  • 30s window is hard-coded; long-tail post-shell activity is missed by design.
  • A parent-path-glob exclusion silences BOTH arms of the rule for that parent.
  • An outbound DNS lookup (port 53) to a local-resolver-class address (loopback, RFC1918, link-local, CGNAT 100.64.0.0/10, IPv6 ULA/link-local) is treated as name resolution and does not trigger the network arm; a DNS lookup to a publicly routable resolver still fires.
  • The 30-second window applies to the shell that ran the payload, including one found on a PID's exec history, so a payload run from a long-idle interactive shell is still out of scope.

persistence_launchagent

LaunchAgent persistence
Flags launchctl load / launchctl bootstrap of a plist under ~/Library/LaunchAgents or /Library/LaunchAgents.

Rule ID persistence_launchagent
Severity high
ATT&CK T1543.001
Event types exec

Description

Detects the canonical user-domain persistence step on macOS: an attacker drops a plist into a LaunchAgents directory and then activates it via launchctl load <plist> or launchctl bootstrap gui/<uid> <plist>. We catch the activation rather than the file write so the alert ties to the moment the persistence becomes effective.

Argument parsing handles launch-domain specifiers (gui/501) preceding the plist path and tolerates flag-like args between load and the plist (-w, -F, etc.).

Known false-positive sources

  • MDM- or installer-provisioned LaunchAgents (Munki, Kandji, JumpCloud) loaded at deploy time. Add a path-glob exclusion for their plist paths via the detection-config surface.
  • Developer tools that register helper agents (Docker Desktop, Backblaze, etc.) on first launch.

Limitations

  • Does not cover launchctl bootout or launchctl unload: those undo persistence rather than create it.
  • Does not catch direct plist writes that never get activated; pair with the privilege_launchd_plist_write rule for system-domain coverage.

dyld_insert

DYLD injection on exec
Flags exec where DYLD_INSERT_LIBRARIES or DYLD_LIBRARY_PATH is set in argv (shell-style or via env(1)).

Rule ID dyld_insert
Severity high
ATT&CK T1574.006
Event types exec

Description

Detects the classic macOS code-injection primitive: launching a process with DYLD_INSERT_LIBRARIES=… or DYLD_LIBRARY_PATH=… set so dyld loads attacker-supplied dylibs into the new process before main(). The rule fires on the leading argv slot only (the VAR=value /path/to/bin shell form, or the env VAR=value /path/to/bin invocation), so substring noise (curl POST data, echo, etc.) does not false-positive.

The matching dylib path is redacted in alert text (a sensitive payload location) but kept in the raw event payload for responders.

Known false-positive sources

  • Local development of code that itself uses DYLD_INSERT_LIBRARIES (rare; usually scoped to non-managed dev hosts).
  • Apple-signed binaries are immune to DYLD_INSERT_LIBRARIES under SIP, but the rule still fires on the launch: investigate why an admin script is setting these vars at all.

Limitations

  • Inherited environment variables (set by a parent shell, not on the exec line) are invisible: ESF does not yet hand the agent the full env map. Tracked as future work.
  • DYLD_FRAMEWORK_PATH and DYLD_FALLBACK_* are intentionally NOT matched: higher-FP, lower-signal. Extend dyldPrefixes if a pilot surfaces real abuse.

shell_from_office

Shell spawned by Microsoft Office
Flags any /bin/sh, /bin/bash, /bin/zsh (etc.) whose parent is Word, Excel, PowerPoint, or Outlook.

Rule ID shell_from_office
Severity high
ATT&CK T1566.001, T1059.004
Event types exec

Description

Detects the textbook post-phishing execution step: a macro-laden Office document opens, the macro shells out, and the second stage takes off from there. The match is on the parent process being one of the four standard macOS Office binaries (full path, not substring) and the child being a known shell.

Office apps almost never need to shell out in normal use; when they do, it's an admin-side automation that's worth surfacing anyway.

Known false-positive sources

  • Office's internal Get Started first-run flow has historically shelled out to fetch help content. Confirm by inspecting argv on the alert.
  • Admin-driven user-environment scripts that template Office settings via shell.

Limitations

  • Does not catch non-shell payloads (osascript, python, ruby) launched directly from Office. Pair with osascript_network_exec for the AppleScript variant.
  • Office binary path matching is exact: /Applications/Microsoft Word.app/Contents/MacOS/Microsoft Word. Apps installed elsewhere (e.g. on an external volume) are missed by design.

osascript_network_exec

AppleScript dropper
Critical-severity catch on the canonical macOS commodity-dropper chain: osascript fetches a stage-2 over the network and runs it from /tmp.

Rule ID osascript_network_exec
Severity critical
ATT&CK T1059.002, T1105
Event types exec

Description

Fires on the LAST link of the chain: an exec from a temp directory whose process tree has both an osascript ancestor and a curl/wget sibling within the osascript's 30-second descendant window. This shape is the recognisable signature of macOS commodity malware staged via AppleScript.

Reverse-direction triggering is deliberate: by the time the temp-exec event lands, the entire ancestor chain has already been ingested and materialised by earlier batches, so the rule is race-immune. Forward triggering (fire on the osascript exec, look for descendants) misses chains that complete across an agent flush boundary.

The rule requires both halves of the chain to be present, so download-only or temp-exec-only flows do not fire here: those overlap with suspicious_exec.

Known false-positive sources

  • Internal automation that bootstraps tooling by scripting curl … | sh from osascript: extremely rare in managed fleets.

Limitations

  • 30-second descendant window is hard-coded; longer-running chains are missed by design.
  • Does not cover Python URL fetches or AppleScript built-in URL access: only flags the explicit curl/wget shape.

credential_keychain_dump

Keychain credential dump
Flags exec of /usr/bin/security dump-keychain: the canonical macOS Keychain export command.

Rule ID credential_keychain_dump
Severity high
ATT&CK T1555.001
Event types exec

Description

Fires when a process invokes /usr/bin/security with the dump-keychain subcommand. That command exports Keychain entries (saved passwords, private keys) and is the macOS-native equivalent of credential-dumping tooling on Windows. Admin scripts virtually never invoke it; offensive playbooks do.

Match shape is exact-path + exact-subcommand to keep the rule high-precision. A shell wrapper (sh -c "security dump-keychain") still surfaces because ESF emits a NOTIFY_EXEC for each execve(), so the security binary always shows up as its own exec event regardless of parent.

Known false-positive sources

  • An IT admin running a one-off keychain audit. Rare in managed fleets; confirm with the user before treating as benign.

Limitations

  • Does not cover Keychain reads via the Security framework (SecItemCopyMatching, etc.) or raw SQLite scrapes of login.keychain-db. Those paths are tracked for a future file-integrity rule.
  • Does not cover adjacent enumerative subcommands (find-internet-password -w, find-generic-password -w); left out for precision; add them to dumpKeychainArgTokens if a pilot fleet surfaces real abuse.

privilege_launchd_plist_write

LaunchDaemon persistence
Flags a system-domain LaunchDaemon whose registered executable is not an Apple platform binary and not allowlisted.

Rule ID privilege_launchd_plist_write
Severity high
ATT&CK T1543.004
Event types btm_launch_item_add

Description

Detects the canonical system-domain persistence vector (T1543.004): a LaunchDaemon being registered with macOS Background Task Management. Once registered, the next launchctl bootstrap system/<name> (or a reboot) gives the attacker root-running persistence.

Keyed on the high-level BTM_LAUNCH_ITEM_ADD event (item_type=daemon) rather than a raw file write, so the registration is caught no matter how the plist landed on disk (direct write, atomic temp-file+rename, copy), which a file-write rule can miss.

The decision keys on the REGISTERED EXECUTABLE's code-signing, not on who registered it: a launchctl bootstrap is always instigated by Apple's smd, so the instigator cannot discriminate. A daemon whose executable is an Apple platform binary, MDM-managed, or signed by an allowlisted vendor team ID is skipped; an ad-hoc, unsigned, or unknown-vendor executable fires. Paired with persistence_launchagent (user-domain LaunchAgents).

Notarization is deliberately NOT a trust signal: it is an automated Apple scan, not an endorsement (Apple has notarized malware), and is not checkable network-free on the ES callback thread. Trust is the operator's team-ID allowlist; notarization, if ever used, belongs in a server-side reputation layer off the hot path.

Known false-positive sources

  • Non-Apple vendor app installing its own LaunchDaemon (a niche VPN, an in-house agent). Add a team_id exclusion for the vendor's signing team ID via the detection-config surface.
  • Custom in-house pkg installers signed by an unexcluded developer team: add a team_id exclusion for that team ID.

Limitations

  • BTM fires at item registration, not at the raw file-drop moment. A plist dropped on disk but never registered/loaded does not surface until registration (often deferred to reboot).
  • Registrations whose executable code-signing cannot be read (executable absent or unreadable at registration) are skipped to stay high-precision.

sudoers_tamper

Sudoers tamper
Flags any non-allowlisted writer that opens /etc/sudoers or /etc/sudoers.d/* in write mode.

Rule ID sudoers_tamper
Severity high
ATT&CK T1548.003
Event types open

Description

Detects an instant escalation primitive: writing to /etc/sudoers or any direct child of /etc/sudoers.d/. A successful tamper grants future shell sessions arbitrary command execution as root.

Unlike the persistence rules, this one deliberately does NOT key on Apple-signed platform binaries: the canonical attacker tools for sudoers tampering ARE platform binaries (cp, tee, redirected shells, even sudo vi /etc/sudoers), so a platform-binary filter would silence every realistic attack while admitting almost nothing of value. Operators tune with a path-glob exclusion via the detection-config surface instead.

visudo and sudoedit use atomic-rename semantics and never open /etc/sudoers in write mode, so the rule does not see them at all.

Known false-positive sources

  • Configuration-management agents (Ansible, Chef, Puppet, MDM-driven scripts) that drop a sudoers fragment under /etc/sudoers.d. Add a path-glob exclusion for their absolute writer paths.

Limitations

  • Atomic-rename writes (write a temp file, rename onto /etc/sudoers) are missed: ESF NOTIFY_OPEN doesn't fire on rename, and the extension does not subscribe to NOTIFY_RENAME today. Tracked as future work.

application_control_block

Application control block
Surfaces every AUTH_EXEC denial from the extension as an alert in the unified view.

Rule ID application_control_block
Severity medium
Event types application_control_block

Description

The extension's AUTH_EXEC decision walker denies execs that match an admin-defined application-control rule. Every such denial emits an application_control_block event that this built-in rule maps to an alert with source='application_control'. The alert carries the matched rule's identifier, severity, and operator-supplied custom message. The dedup key (source, host_id, rule_id, subject), where an app-control alert's subject is its process id, means repeated blocks of the same binary by the same rule on the same process collapse into one alert row.

dns_c2_beacon

DNS C2 beacon
Flags a program that looks up a domain name and then connects to the address that lookup returned, when that program was launched from a suspicious location such as a temporary or world-writable folder. This is the classic "malware phoning home" shape, and the alert ties three normally separate signals into one finding: the program launch, the DNS lookup, and the outbound connection.

Rule ID dns_c2_beacon
Severity high
ATT&CK T1071.004, T1568.002
Event types network_connect, dns_query, exec

Description

Fires on the last link of the chain: an outbound network connection from a program that was launched out of a temporary or world-writable path and had earlier looked up a domain whose resolved addresses include the one now being connected to. The single finding cites both the DNS lookup and the outbound connection, and is attributed to the program that launched, so an analyst sees the whole launch -> lookup -> connection chain in one alert.

Triggering on the connection (rather than on the lookup) is deliberate and avoids races: by the time the connection lands, the program's DNS lookups are already recorded, so the rule keeps no state between event batches. The DNS lookup and the connection are reported by the same network extension and share its clock, so the lookup-then-connect window is measured directly on their timestamps.

A high-entropy, algorithmically generated domain name (the kind produced by a domain-generation algorithm) raises the finding to Critical and adds the DGA technique. Ordinary browser traffic does not fire this rule: it only considers programs launched from a suspicious location, which a browser is not.

Known false-positive sources

  • A legitimate tool staged in a temporary path that looks up a hostname and connects to it. This is rare on managed fleets; allowlist the path if it recurs.

Limitations

  • Sees plain UDP/TCP DNS only. Encrypted DNS (DoH/DoT) bypasses the proxy and is not correlated.
  • The current detection requires the program to have been launched from a temporary or world-writable path. Detecting beacons started by a scripting interpreter that is running non-interactively (for example, a shell script with no terminal) is a planned addition.
  • The lookup-then-connect window is bounded (currently 30 seconds). A beacon that looks up its domain far in advance of connecting is missed by design.

sensor_tamper

EDR sensor disabled
Flags one of the EDR's own capture providers stopping without coming back within a few seconds.

Rule ID sensor_tamper
Severity high
ATT&CK T1562.001
Event types sensor_provider_transition

Description

Detects tampering with the EDR itself. A capture provider (the content filter or the DNS proxy) stopping means the host stops reporting the telemetry that provider carries, so an attacker who can switch it off can work unobserved.

The agent restores a stopped provider automatically, in about 35 seconds. That repair is why the rule exists rather than why it is unnecessary: agent health only reports what is true now, so once the provider is back the health view reads healthy and nothing records that it was ever off. The alert is the durable account.

An agent upgrade also stops providers, as part of replacing the system extension, and the platform reports the same stop reason for that as for somebody switching capture off. The rule separates them by how fast capture resumes: an upgrade's replacement provider runs about a second later, while a stop that needed the automatic repair takes tens of seconds. A provider that resumes within a few seconds is therefore not reported.

A provider an operator has deliberately turned off (the DNS proxy is optional) is reported as absent rather than stopped and never reaches this rule.

Known false-positive sources

  • An upgrade whose replacement provider takes more than a few seconds to start. The upgrade cutover measured on a live host resumed capture in about a second; a host slow enough to exceed the window would also be a host that was genuinely not capturing for that long.

Limitations

  • Reports that capture stopped, not whether it was restored. The repair (or its failure) is carried by the following transition events on the host's timeline rather than by the alert.
  • An attacker who stops a provider and prevents the agent from reporting it at all (killing the agent, or blocking upload) produces no transition event and so no alert. That absence is covered by host health going stale, not by this rule.

sensor_recovery_failed

EDR sensor could not be restored
Flags a stopped capture provider that the agent tried and failed to restore, so the host is not capturing until someone intervenes.

Rule ID sensor_recovery_failed
Severity critical
ATT&CK T1562.001
Event types sensor_recovery_failed

Description

The agent repairs a stopped capture provider by itself, and usually succeeds within about half a minute. This fires when it does not: every attempt in its budget has been used and the provider is still stopped.

The practical difference from the sensor-disabled alert is what the host is doing now. That alert is raised seconds after capture stops, before anyone can know whether the repair will work, so most of the time it describes a host that has already fixed itself. This alert only exists for hosts that have not: the telemetry that provider carries is not being collected, and will not be until an operator restores it, usually by re-activating the extension on the host.

The reported outcome says which kind of failure it was, because they point at different causes. If the repair command kept failing, suspect the host application or the system configuration daemon. If every repair reported success and the provider stayed stopped, re-enabling is not what the fault needs; that shape has been seen when the extension is running but its sessions are wedged.

A provider an operator has deliberately turned off never reaches this rule: the agent does not try to repair a provider it was told to leave alone.

Known false-positive sources

  • None known. The event is only emitted after the agent has attempted and failed a bounded number of repairs, so there is no benign path that produces it; a host that reaches this state genuinely is not capturing.

Limitations

  • Reports that automatic recovery gave up, not why the provider stopped in the first place. The stop itself, and whether it looked like tampering, is carried by the sensor-disabled alert that precedes it.
  • An attacker who stops a provider AND prevents the agent from reporting at all produces no event and so no alert. That absence is covered by host health going stale, not by this rule.