macOS Observer Tiers
Pavri's pavri-endpointd sensor supports three macOS observer tiers,
depending on whether the binary holds Apple's Endpoint Security entitlement.
Bottom line up front: Tier 1 is production-grade. Tier 2 is best-effort and suitable for design-partner deployments only. Tier 3 means process and file observation are off; other subsystems (MCP broker, egress proxy, browser bridge, clipboard observer, git hooks) continue.
Tier 1 — Endpoint Security (production)
Status: active
Coverage: Full
SOC2-graded: Yes
The sensor uses Apple's Endpoint Security framework to subscribe to
ES_EVENT_TYPE_NOTIFY_EXEC (process execution) and
ES_EVENT_TYPE_NOTIFY_OPEN/NOTIFY_WRITE/NOTIFY_CREATE/NOTIFY_RENAME
(file events). ES delivers events synchronously before the kernel allows the
operation to proceed, so there is no race window between event creation and
sensor receipt.
Requirement: The sensor binary must hold the
com.apple.developer.endpoint-security.client entitlement, granted via the
Apple Developer Program after a manual review. Approval is not
instantaneous; the typical turnaround is days to weeks. This is Apple's
standard process — Pavri cannot expedite it.
Build tag: cap_es_entitled
To escalate: Contact your security team to open an Apple Developer Program
entitlement request. Reference the CAP-1.7 roadmap item. Once approved, rebuild
the sensor binary with -tags cap_es_entitled and redeploy.
Tier 2 — kauth/FSEvents fallback (best-effort)
Status: degraded_kauth_fallback
Coverage: Best-effort (see gaps below)
SOC2-graded: No
When the Endpoint Security entitlement is absent, the sensor automatically falls back to two deprecated-but-functional kernel APIs:
-
Process observation: two paths (hardened in S2.3).
- Event-driven fast-path — kqueue(2)
EVFILT_PROCwithNOTE_EXEC | NOTE_FORK | NOTE_EXIT. Low latency; a per-PID start-time "generation" token detects PID reuse and a proactive probe atNOTE_FORKrecovers children that already exec'd, with dedup so nothing double-fires. - Poll-based discovery sweep —
proc_listpidseverykauthSweepInterval(1 s), diffing the live PID set against the watch set to emit REAL (libproc) exec events for newly-seen processes and prune exited ones. This is the load-bearing path on modern macOS: on macOS 26 (Darwin 25)+ an unprivilegedEVFILT_PROCwatcher receives onlyNOTE_EXIT(NOTE_FORK/NOTE_EXECare withheld without ES), andposix_spawn(used by Go'sos/execand much modern software) does not fireNOTE_FORKon the parent — so the fast-path alone would miss most process launches. The sweep is real polling of real processes (not synthetic telemetry).
The sensor enriches live process events with best-effort
libprocancestry, executable path, binary hash, and app bundle identifier when those are readable without special entitlement. - Event-driven fast-path — kqueue(2)
-
File observation: FSEvents (
FSEventStreamCreate,FSEventStreamSetDispatchQueue,FSEventStreamStart). FSEvents is Apple's recommended inotify-equivalent and is not deprecated. No entitlement required for user-launched binaries.
Honest coverage gaps
| Gap | Root cause | Impact |
|---|---|---|
| Short-lived process miss | A process that starts AND exits within one discovery-sweep interval (1 s) whose NOTE_FORK/NOTE_EXEC was not delivered (macOS 26+/posix_spawn) is missed. | Bounded (was unbounded pre-S2.3): sub-second commands that leave no live footprint at a sweep may not appear. Examples: ls, which. |
| Discovery latency | On hosts where the fast-path is inert (macOS 26+/posix_spawn), newly-spawned processes are reported by the poll sweep up to one interval (1 s) late. | Process telemetry is near-real-time, not instantaneous. |
| Exec-in-place miss | A watched process that execs a new image without forking keeps the same PID and start-time, so neither path re-fires when NOTE_EXEC is withheld. | Rare (most launches fork/spawn a child); such in-place replacements are not re-emitted. |
| macOS 26 EVFILT_PROC restriction | Unprivileged EVFILT_PROC on Darwin 25+ delivers only NOTE_EXIT. | Exec/fork detection relies entirely on the discovery sweep on these hosts; PID reuse is handled via NOTE_EXIT + next sweep. |
| File path scope | Only paths matching the configured allowlist are watched. | Events on paths outside ~/.aws, ~/.ssh, /etc/, etc. are not delivered. |
| File writer attribution | FSEvents reports the changed path but not the writer PID/process chain. | File-write telemetry is emitted as file_attribution=path_only; it can be correlated to workspace/config/session context, but it is not treated as assistant-process attribution. |
| FSEvents coalescing | The kernel coalesces rapid successive writes into a single event. | Event count will be lower than actual operation count during heavy I/O bursts. Now surfaced, not silent (S2.3): when the kernel signals event loss (KernelDropped/UserDropped/MustScanSubDirs), the observer increments pavri_endpoint_observer_events_total{observer="file",outcome="kernel_event_loss"} so the telemetry gap is visible to operators. |
| Fork-to-watch race | Inherent in the kqueue(2) design. There is no atomic "watch me and all my future children" operation without ES. | Cannot be closed without Tier 1. |
| Direct socket attribution | macOS connect/DNS observation requires Network Extension entitlement. | net_observer_status=not_entitled; deep model-API visibility is available only through the optional local egress proxy, and web-app visibility is available only through the browser bridge. |
When is Tier 2 acceptable?
- Design-partner-grade deployments where operators understand the gaps.
- Development and staging environments where coverage completeness is not a compliance requirement.
- Incident triage where the missing short-lived-process events are not the primary signal.
Tier 2 is not acceptable for SOC2 Type II evidence collection. Use Tier 1.
Opting out of the fallback
If your security posture requires that an unentitled macOS endpoint report
unavailable (Tier 3) rather than run the fallback, set:
export PAVRI_DARWIN_DISABLE_KAUTH_FALLBACK=1
This is useful for SOC2 environments where a "deprecated kauth API" finding
would be flagged by an auditor. With this flag set, the sensor's process and
file observers surface unavailable on the endpoint detail page, and the
dashboard does not show the kauth fallback banner.
GHA macos-13 / macos-14 runner note
GitHub Actions macos-13 and macos-14 runners are not Endpoint Security
entitled by default. The Tier 2 kauth fallback tests (livemacos build tag)
run on these runners. The test setup helper automatically skips if ES IS
entitled (meaning we're not in fallback mode), so the test suite is
safe to run on both entitled and unentitled macOS hosts.
Tier 3 — Unentitled, fallback disabled
Status: unavailable
Coverage: None (process and file)
SOC2-graded: N/A
When PAVRI_DARWIN_DISABLE_KAUTH_FALLBACK=1 is set and the ES entitlement
is absent, both the process observer and the file observer report Unavailable.
The sensor continues to function: the MCP broker, egress proxy, git hook
scanner, browser bridge, clipboard observer, and heartbeat all remain active.
The direct macOS network observer remains not_entitled until Apple grants
Network Extension approval; it does not emit synthetic socket telemetry.
This is the pre-CAP-2.6-fu behavior. It is appropriate for operators who cannot tolerate any use of deprecated kernel APIs and who have not yet obtained the ES entitlement.
Dashboard indicator
When Tier 2 is active, the endpoint detail page (/endpoints/[id]) shows a
warning banner with the text:
macOS endpoint running in kauth fallback mode — Apple Endpoint Security entitlement not granted. Process/file observation is best-effort; some events may be missed. Contact security team to escalate Apple Developer Program entitlement review.
The banner is read from endpoint.posture.attributes:
process_observer_status = degraded_kauth_fallbackfile_observer_status = degraded_kauth_fallbacknet_observer_status = not_entitlednetwork_visibility = egress_proxy | browser_bridge | none
Either attribute being set to degraded_kauth_fallback triggers the banner.
network_visibility is coverage evidence: egress_proxy means the local
model-API proxy is the active deep network path; browser_bridge means page
interaction/upload/download evidence is available from the browser extension;
none means no no-entitlement network fallback is currently active.
Browser bridge, clipboard observer, and egress proxy events also carry
metadata-only CAP-4 control labels such as
cap4_control_sensitive_upload, cap4_control_sensitive_download,
cap4_control_cross_app_laundering, and
cap4_control_model_api_key_misuse. These labels are correlation evidence
for the existing policy/detection stack; they do not imply direct macOS socket
attribution.
Roadmap
| Sprint | Item |
|---|---|
| CAP-1.2 | macOS Endpoint Security stubs (CGO; production codesigning deferred to CAP-1.7) |
| CAP-1.7 | Production ES entitlement and codesigning (Tier 1) |
| CAP-2.6-fu | kauth/FSEvents fallback (Tier 2); PAVRI_DARWIN_DISABLE_KAUTH_FALLBACK opt-out; dashboard banner; this doc |