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.
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:
| Field | Record for this lab |
|---|---|
| Target build | macOS 27.0.1, 26A434 |
| Trigger | /usr/bin/logger marker |
| Query | Process plus exact marker text |
| Expected result | One or more marker records |
| Observed result | A Default message from /usr/bin/logger |
| Limitation | This 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 test | Component to investigate | Independent evidence |
|---|---|---|
| A quarantined app is assessed | Gatekeeper and syspolicyd activity | Quarantine attribute, signature, launch result |
| A privacy-sensitive resource is requested | TCC decision activity | App identity, prompt or grant state, resulting access |
| A sandboxed process is denied | Sandbox-related diagnostics | Process identity, requested resource, failed operation |
| A job starts or fails | launchd diagnostics | Plist, service state, process execution |
| A code load is rejected | Code-signing or AMFI-related diagnostics | Target 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:
- Record the app's identifier and the precise action that was denied.
- Run the action once while collecting a narrow time window.
- Query the relevant subsystem and inspect the actual fields returned.
- Repeat with an approved action as a benign comparison.
- 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.
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