MacSecLabs
0.4Free preview
DE 00 · Module 0.4 · Beginner

Unified Logging for Detection and Hunting

Unified Logging records messages emitted by macOS components and applications. It can explain how a subsystem reacted to an action, but a log message is not a substitute for a process, file, or network event. The engineer must first establish that a message is emitted on the target build and under the collection settings used in production.

This lesson creates one known message, retrieves it, and then applies the same method to a security-relevant predicate. The known message is the positive control: it confirms the query and time window work before an empty security query is interpreted.

Produce and retrieve a known record

Use the absolute path to make the command unambiguous if the shell has a user-defined alias or function. On the validation VM, type log resolves to /usr/bin/log; stock zsh does not supply a conflicting log builtin.

known Unified Log record
% (
% set -e
% set -o pipefail
% marker="MSL_DE00_LOG_PROBE_$(uuidgen)"
% /usr/bin/logger -t msl_de00 "$marker"
% record='null'
% for attempt in 1 2 3 4 5; do
% record=$(/usr/bin/log show --last 2m --style json --predicate 'process == "logger"' 2>/dev/null | jq -c --arg marker "$marker" '[.[] | select(.eventMessage == $marker) | {processImagePath,messageType}] | .[-1]')
% [[ "$record" != null ]] && break
% sleep 1
% done
% if [[ "$record" != null ]]; then printf 'log_control_found=yes\n%s\n' "$record"; else printf 'log_control_found=no\n'; fi
% )
log_control_found=yes
{"processImagePath":"/usr/bin/logger","messageType":"Default"}

The first command writes a harmless unique marker. The query retries briefly because historical search can lag emission. It filters by the marker in jq, then prints the process path and message type. A prior run cannot satisfy the control because its marker differs. It does not validate a security detection. The Default message type also says nothing about a threat level.

The command uses jq to keep the student-facing result small. If it prints log_control_found=no, investigate logging configuration and query scope before treating a later empty result as evidence of absence. The raw JSON contains timestamps, process identifiers, and other machine-specific fields. Keep those in an evidence capture when reproducing an incident, but omit them from a teaching example where they add no decision value.

Form a predicate before searching

/usr/bin/log show queries retained history. /usr/bin/log stream follows new messages. Both accept predicates using Apple's log-query syntax. Predicates can filter process, subsystem, category, message content, and other fields.

A precise query starts with a behavior. For a sandbox denial, identify the process that attempted access and the resource it tried to use. Then find a local message associated with that attempt. Do not assume a predicate copied from another OS release will return a useful record.

A collection worksheet should contain:

FieldRecord for this lab
Target buildmacOS 27.0.1, 26A434
Trigger/usr/bin/logger marker
QueryProcess plus exact marker text
Expected resultOne or more marker records
Observed resultA Default message from /usr/bin/logger
LimitationThis is a query control, not attack telemetry

Build a source map before writing a predicate

The old habit of collecting every message with a security-sounding word does not produce a detection. Start with the subsystem that could explain the controlled action, then verify that it emitted a usable record on the target build. The following are investigation starting points, not portable predicates or guaranteed event feeds:

Activity to testComponent to investigateIndependent evidence
A quarantined app is assessedGatekeeper and syspolicyd activityQuarantine attribute, signature, launch result
A privacy-sensitive resource is requestedTCC decision activityApp identity, prompt or grant state, resulting access
A sandboxed process is deniedSandbox-related diagnosticsProcess identity, requested resource, failed operation
A job starts or failslaunchd diagnosticsPlist, service state, process execution
A code load is rejectedCode-signing or AMFI-related diagnosticsTarget file, signature, load result

A component name alone is not an alert field. First run a positive control, then reproduce the action, narrow the time window, inspect actual subsystem and category values, and save a representative raw record. If the component does not log the action at the needed level, record that result and choose a different source. Do not ship a rule whose only evidence is a predicate that once returned zero rows.

Once the control works, replace the marker predicate with a candidate subsystem and a controlled action that should cause the subsystem to log. If the candidate query is empty, inspect the action, time window, log level, collection permission, and predicate. Do not label the absence a clean security result.

Read the fields that matter

A Unified Log entry can include a timestamp, process image, process ID, subsystem, category, message type, and formatted message. Some fields may be blank, redacted, or unavailable. <private> in a message is privacy redaction, not an attacker hiding text.

Subsystem and category can narrow a search, but they are not guaranteed portable detection identifiers. Apple can change message wording, category, level, or emission conditions across releases. A rule that matches only a free-text phrase needs a version pin and a regression fixture.

Use an exact message only when you have tested its stability. Prefer records with an actor and an outcome. When possible, correlate a log message with a process event, file artifact, TCC decision, or network record from the same time window.

Example: testing a candidate security signal

Suppose a lab app is denied access to a protected resource. A useful workflow is:

  1. Record the app's identifier and the precise action that was denied.
  2. Run the action once while collecting a narrow time window.
  3. Query the relevant subsystem and inspect the actual fields returned.
  4. Repeat with an approved action as a benign comparison.
  5. Write a rule only if the denied case has a distinguishing record.

The word sandbox in a log message is not itself a denial. A daemon may log setup, teardown, policy loading, or ordinary diagnostics under the same subsystem. Likewise, a TCC message may represent a prompt, a decision, or maintenance activity. Read the result and context.

Collection limits

Unified Logging has retention and privacy controls. Default, info, and debug messages have different availability, and an enterprise collector may export only a subset. A broad historical query can be expensive. A process that never emitted a message cannot be detected through this source. These limits should appear in the rule's source contract.

Do not change global privacy or debug settings on a student's primary Mac to make a lesson work. This course uses controlled messages and read-only queries for the foundation. Later labs that require more instrumentation state the target and cleanup explicitly.

Apple's Unified Logging documentation describes how applications emit structured messages. The log command's local manual and help output describe the query options installed on the target build.

Detection engineering

The positive marker confirms that the query path works on this build. A security detection needs a separate controlled trigger, an observed result, and a benign comparison. Treat the log as one evidence source and record the predicate and collection settings with the finding.

What Comes Next

Module 0.5 adds process, file, network, and identity state. Those sources help determine whether an action described in a log actually occurred.

Continue macOS Detection Engineering

These foundation modules are open as a preview. Full modules, lab downloads, progress tracking, and certificate workflow require active course access.

Get full access