Skip to content

Add Sandbox Annotator#848

Open
pmarkowsky wants to merge 4 commits intonorthpolesec:mainfrom
pmarkowsky:plm/sandbox-annotator
Open

Add Sandbox Annotator#848
pmarkowsky wants to merge 4 commits intonorthpolesec:mainfrom
pmarkowsky:plm/sandbox-annotator

Conversation

@pmarkowsky
Copy link
Copy Markdown
Member

This PR adds an annotator to the process tree for sandbox-exec events

Demo video here

https://www.youtube.com/watch?v=HVTLD6qY7AU

Work for SNT-350

Other changes:

  • Makes the authorizer client a tree aware client and moves all annotation propagations to there.

Detect processes launched via sandbox-exec and expose the sandbox
profile path to CEL rules through `process.sandbox_policy`.

- Changes the authorizer to be a TreeAwareClient.
@github-actions github-actions bot added comp/santad Issues or PRs related to the daemon lang/objc++ PRs modifying files in ObjC++ comp/common size/l Size: large labels Mar 16, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 16, 2026

📝 Walkthrough

Walkthrough

Adds sandbox-exec detection and sandbox_policy annotations to the ProcessTree, wires ProcessTree into EndpointSecurity authorizer, passes optional process annotations into CEL Activation, exposes a new "process" CEL variable, and updates build/test targets and annotator registration.

Changes

Cohort / File(s) Summary
CEL Activation Layer
Source/common/cel/Activation.h, Source/common/cel/Activation.mm, Source/common/cel/BUILD, Source/santad/CELActivation.mm
Activation gains an optional process_annotations field and process_accessed_ tracking; exposes a process CEL proto variable and disables result caching if accessed. BUILD deps updated to include the process_tree proto.
Endpoint Security Authorizer & Call Sites
Source/santad/EventProviders/SNTEndpointSecurityAuthorizer.h, Source/santad/EventProviders/SNTEndpointSecurityAuthorizer.mm, Source/santad/EventProviders/SNTEndpointSecurityAuthorizerTest.mm, Source/santad/Santad.mm, Source/santad/SantadTest.mm
Authorizer now subclasses TreeAware client and its initializer accepts a processTree parameter; call sites and tests updated to pass the ProcessTree through.
Sandbox-exec Annotation Support
Source/santad/ProcessTree/annotations/sandbox_exec.h, Source/santad/ProcessTree/annotations/sandbox_exec.cc, Source/santad/ProcessTree/annotations/BUILD, Source/santad/ProcessTree/annotations/sandbox_exec_test.mm
Adds SandboxExecAnnotator, SandboxPolicyInfo/status types, argv parser for sandbox-exec -f <path>, propagation/promotion rules across fork/exec, proto export, and comprehensive unit tests.
ProcessTree Core & Proto
Source/santad/ProcessTree/process_tree.cc, Source/santad/ProcessTree/process_tree.proto
ProcessTree annotation callbacks guarded with a thread-local in_annotator_callback to avoid reentrant mutex deadlocks and enable safe exports; proto extended with SandboxPolicy and Annotations.sandbox_policy.
Integration & Dependencies
Source/santad/BUILD, Source/santad/SantadDeps.mm
Adds process_tree_cc_proto to CEL/targets, registers SandboxExecAnnotator in runtime deps, and wires ProcessTree into Santad initialization and tests.

Sequence Diagram(s)

sequenceDiagram
    participant ES as EndpointSecurityEvent
    participant Auth as SNTEndpointSecurityAuthorizer
    participant PT as ProcessTree
    participant ACT as Activation (CEL)
    participant CEL as CEL Engine

    ES->>Auth: deliver exec/fork event (audit token)
    Auth->>PT: ExportAnnotations(targetPid)
    PT-->>Auth: optional Annotations (sandbox_policy)
    Auth->>ACT: construct Activation(..., process_annotations)
    ACT->>CEL: expose variables (including process proto)
    CEL->>ACT: request "process" value
    ACT-->>CEL: return process proto (sets process_accessed_)
    CEL->>CEL: evaluate policy (cacheability affected by access)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add Sandbox Annotator' directly and clearly summarizes the main change: introducing a sandbox annotator to the process tree, which is the primary objective of this PR.
Description check ✅ Passed The description is related to the changeset, explaining that it adds an annotator for sandbox-exec events, makes the authorizer a tree-aware client, and references the associated work item and demo.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

Important

Merge conflicts detected (Beta)

  • Resolve merge conflict in branch plm/sandbox-annotator
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
📝 Coding Plan
  • Generate coding plan for human review comments

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

Migrating from UI to YAML configuration.

Use the @coderabbitai configuration command in a PR comment to get a dump of all your UI settings in YAML format. You can then edit this YAML file and upload it to the root of your repository to configure CodeRabbit programmatically.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
Source/santad/ProcessTree/annotations/sandbox_exec_test.mm (1)

183-225: Add a regression test for nested sandbox-exec re-sandboxing.

Please add a case where a process with confirmed sandbox annotation execs another /usr/bin/sandbox-exec -f <new_profile> ... and verify the new profile takes effect after the next exec.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/santad/ProcessTree/annotations/sandbox_exec_test.mm` around lines 183
- 225, Add a new regression test that simulates a process already annotated as
confirmed-sandbox that execs another sandbox-exec with a different profile and
then execs a real target, verifying the new profile is applied: reuse the
existing pattern in testAnnotationPropagatesExecToChildren by creating a
process, mark it (or simulate) with a confirmed SandboxExecAnnotator, call
HandleExec with a Program whose .executable is "/usr/bin/sandbox-exec" and
arguments including "-f" and a new profile, then call HandleExec again to a
target binary and assert via tree->GetAnnotation<SandboxExecAnnotator>(...) that
the annotation exists and that (*annotation)->info()->status (and any profile
identifier field you have) reflects the new profile (use HandleFork/HandleExec,
Get, and GetAnnotation<SandboxExecAnnotator> helpers and assert
SandboxPolicyStatus::kConfirmed where appropriate).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Source/santad/ProcessTree/annotations/sandbox_exec.cc`:
- Around line 71-95: AnnotateExec currently checks existing annotations on
orig_process before seeing if new_process is a sandbox-exec, so nested execs
keep the old policy path; change the order so you first check
IsSandboxExec(new_process) and if true call
ParseSandboxExecArgv(new_process.program_->arguments) and, when it returns info,
call tree.AnnotateProcess(new_process,
std::make_shared<SandboxExecAnnotator>(std::move(*info))) and return; only if
new_process is not sandbox-exec continue with the existing logic that calls
tree.GetAnnotation<SandboxExecAnnotator>(orig_process), promotes pending info to
confirmed, or propagates the annotation with tree.AnnotateProcess(new_process,
...) as before.

---

Nitpick comments:
In `@Source/santad/ProcessTree/annotations/sandbox_exec_test.mm`:
- Around line 183-225: Add a new regression test that simulates a process
already annotated as confirmed-sandbox that execs another sandbox-exec with a
different profile and then execs a real target, verifying the new profile is
applied: reuse the existing pattern in testAnnotationPropagatesExecToChildren by
creating a process, mark it (or simulate) with a confirmed SandboxExecAnnotator,
call HandleExec with a Program whose .executable is "/usr/bin/sandbox-exec" and
arguments including "-f" and a new profile, then call HandleExec again to a
target binary and assert via tree->GetAnnotation<SandboxExecAnnotator>(...) that
the annotation exists and that (*annotation)->info()->status (and any profile
identifier field you have) reflects the new profile (use HandleFork/HandleExec,
Get, and GetAnnotation<SandboxExecAnnotator> helpers and assert
SandboxPolicyStatus::kConfirmed where appropriate).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6a8df256-2af7-4816-8e23-ee6eb00e0728

📥 Commits

Reviewing files that changed from the base of the PR and between 81bce37 and 58e6341.

📒 Files selected for processing (17)
  • Source/common/cel/Activation.h
  • Source/common/cel/Activation.mm
  • Source/common/cel/BUILD
  • Source/santad/BUILD
  • Source/santad/CELActivation.mm
  • Source/santad/EventProviders/SNTEndpointSecurityAuthorizer.h
  • Source/santad/EventProviders/SNTEndpointSecurityAuthorizer.mm
  • Source/santad/EventProviders/SNTEndpointSecurityAuthorizerTest.mm
  • Source/santad/ProcessTree/annotations/BUILD
  • Source/santad/ProcessTree/annotations/sandbox_exec.cc
  • Source/santad/ProcessTree/annotations/sandbox_exec.h
  • Source/santad/ProcessTree/annotations/sandbox_exec_test.mm
  • Source/santad/ProcessTree/process_tree.cc
  • Source/santad/ProcessTree/process_tree.proto
  • Source/santad/Santad.mm
  • Source/santad/SantadDeps.mm
  • Source/santad/SantadTest.mm

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
Source/santad/ProcessTree/annotations/sandbox_exec_test.mm (1)

227-253: Add a regression test for malformed sandbox-exec exec-path handling.

Current tests cover pending/confirmed propagation well, but they do not assert behavior when sandbox-exec is exec’d and argv parsing fails. A focused test here would lock in non-propagation of stale policy state from prior annotations (relevant to Line 227 semantics).

🧪 Suggested test shape
+// Test: malformed sandbox-exec argv should not inherit stale annotation on exec.
+- (void)testMalformedSandboxExecExecDoesNotInheritStalePolicy {
+  uint64_t event_id = 1;
+
+  // Build confirmed annotation first: sandbox-exec -f /old.sb -> target
+  const struct Pid fork_pid = {.pid = 2, .pidversion = 2};
+  self.tree->HandleFork(event_id++, *self.initProc, fork_pid);
+  const struct Pid sandbox_pid = {.pid = 2, .pidversion = 3};
+  const struct Program sandbox_prog = {
+      .executable = "/usr/bin/sandbox-exec",
+      .arguments = {"sandbox-exec", "-f", "/old.sb", "/usr/bin/ls"},
+      .code_signing = kSandboxExecCS,
+  };
+  auto forked = *self.tree->Get(fork_pid);
+  self.tree->HandleExec(event_id++, *forked, sandbox_pid, sandbox_prog, cred);
+
+  const struct Pid confirmed_pid = {.pid = 2, .pidversion = 4};
+  const struct Program confirmed_prog = {.executable = "/usr/bin/ls", .arguments = {"/usr/bin/ls"}};
+  auto sandbox_proc = *self.tree->Get(sandbox_pid);
+  self.tree->HandleExec(event_id++, *sandbox_proc, confirmed_pid, confirmed_prog, cred);
+
+  // Exec into sandbox-exec with malformed argv (no -f path).
+  const struct Pid malformed_pid = {.pid = 2, .pidversion = 5};
+  const struct Program malformed_prog = {
+      .executable = "/usr/bin/sandbox-exec",
+      .arguments = {"sandbox-exec", "/usr/bin/id"},
+      .code_signing = kSandboxExecCS,
+  };
+  auto confirmed_proc = *self.tree->Get(confirmed_pid);
+  self.tree->HandleExec(event_id++, *confirmed_proc, malformed_pid, malformed_prog, cred);
+
+  auto malformed_proc = *self.tree->Get(malformed_pid);
+  auto ann = self.tree->GetAnnotation<SandboxExecAnnotator>(*malformed_proc);
+  XCTAssertFalse(ann.has_value());
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/santad/ProcessTree/annotations/sandbox_exec_test.mm` around lines 227
- 253, Add a regression test that simulates a malformed sandbox-exec exec (argv
parsing failure) and asserts that pending sandbox-exec annotations do not
propagate to forked children: reuse the existing test structure in
testPendingDoesNotPropagateToFork by creating a fork via HandleFork, then call
HandleExec on the forked process with a Program whose arguments/executable are
intentionally malformed to force argv parsing to fail, then perform another
HandleFork from the sandbox-exec process and assert via tree->Get and
tree->GetAnnotation<SandboxExecAnnotator> that the child process has no
annotation; ensure you exercise the same symbols (HandleExec, HandleFork, Get,
GetAnnotation<SandboxExecAnnotator>) and check absence with
XCTAssertFalse(child_opt.has_value()).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Source/santad/ProcessTree/annotations/sandbox_exec.cc`:
- Around line 73-96: When a new_process is a sandbox-exec (IsSandboxExec) but
ParseSandboxExecArgv(new_process.program_->arguments) fails, the current code
falls through and can re-attach the previous SandboxExecAnnotator from
orig_process; change the logic so that if IsSandboxExec(new_process) is true and
ParseSandboxExecArgv returns no value, you do NOT propagate the prior
annotation—either return early (skip propagation) or explicitly annotate
new_process with a cleared/neutral SandboxExecAnnotator; update the block around
IsSandboxExec, ParseSandboxExecArgv, SandboxExecAnnotator, and the subsequent
tree.AnnotateProcess / tree.GetAnnotation calls to prevent carrying stale
annotations to new sandbox-exec processes.

---

Nitpick comments:
In `@Source/santad/ProcessTree/annotations/sandbox_exec_test.mm`:
- Around line 227-253: Add a regression test that simulates a malformed
sandbox-exec exec (argv parsing failure) and asserts that pending sandbox-exec
annotations do not propagate to forked children: reuse the existing test
structure in testPendingDoesNotPropagateToFork by creating a fork via
HandleFork, then call HandleExec on the forked process with a Program whose
arguments/executable are intentionally malformed to force argv parsing to fail,
then perform another HandleFork from the sandbox-exec process and assert via
tree->Get and tree->GetAnnotation<SandboxExecAnnotator> that the child process
has no annotation; ensure you exercise the same symbols (HandleExec, HandleFork,
Get, GetAnnotation<SandboxExecAnnotator>) and check absence with
XCTAssertFalse(child_opt.has_value()).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a6945bbd-bd84-48c1-9810-3204883ec50d

📥 Commits

Reviewing files that changed from the base of the PR and between 58e6341 and 477cf99.

📒 Files selected for processing (2)
  • Source/santad/ProcessTree/annotations/sandbox_exec.cc
  • Source/santad/ProcessTree/annotations/sandbox_exec_test.mm

@pmarkowsky pmarkowsky marked this pull request as ready for review March 18, 2026 22:08
@pmarkowsky pmarkowsky requested a review from a team as a code owner March 18, 2026 22:08
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
Source/santad/ProcessTree/annotations/sandbox_exec_test.mm (1)

55-74: Add a regression case for -f in target command args.

Please add a parser test where sandbox-exec options do not include -f, but the target command does (e.g., ... /usr/bin/grep -f pattern). This guards against false policy-path extraction.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/santad/ProcessTree/annotations/sandbox_exec_test.mm` around lines 55 -
74, Add a regression test to ensure ParseSandboxExecArgv does not misinterpret a
target command’s "-f" as the sandbox-exec profile flag: in
Source/santad/ProcessTree/annotations/sandbox_exec_test.mm add a test (e.g.,
testParseDoesNotPickFlagFromTargetCmd) that calls ParseSandboxExecArgv with args
like {"sandbox-exec", "/usr/bin/grep", "-f", "pattern"} (no "-f" option for
sandbox-exec itself) and assert result.has_value() is false or that profile_path
is not set/incorrectly extracted (match the existing tests’ assertion style),
using the existing ParseSandboxExecArgv function and test class patterns to
guard against false policy-path extraction.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Source/santad/ProcessTree/annotations/sandbox_exec.cc`:
- Around line 43-50: ParseSandboxExecArgv currently scans all argv entries and
can pick up a target-command '-f'. Limit the scan to the sandbox-exec option
scope by stopping when the option/args separator is reached (e.g., "--") or when
the sandbox-exec options end; only examine entries before that separator for
"-f". Update ParseSandboxExecArgv to return SandboxPolicyInfo with profile_path
and SandboxPolicyStatus::kPending only when "-f" is found before the separator,
referencing ParseSandboxExecArgv, SandboxPolicyInfo, profile_path, and
SandboxPolicyStatus::kPending to locate the logic to change.

---

Nitpick comments:
In `@Source/santad/ProcessTree/annotations/sandbox_exec_test.mm`:
- Around line 55-74: Add a regression test to ensure ParseSandboxExecArgv does
not misinterpret a target command’s "-f" as the sandbox-exec profile flag: in
Source/santad/ProcessTree/annotations/sandbox_exec_test.mm add a test (e.g.,
testParseDoesNotPickFlagFromTargetCmd) that calls ParseSandboxExecArgv with args
like {"sandbox-exec", "/usr/bin/grep", "-f", "pattern"} (no "-f" option for
sandbox-exec itself) and assert result.has_value() is false or that profile_path
is not set/incorrectly extracted (match the existing tests’ assertion style),
using the existing ParseSandboxExecArgv function and test class patterns to
guard against false policy-path extraction.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 61ffc26e-3a1a-46dc-a7a6-990d55db3aa8

📥 Commits

Reviewing files that changed from the base of the PR and between 477cf99 and 45a774b.

📒 Files selected for processing (2)
  • Source/santad/ProcessTree/annotations/sandbox_exec.cc
  • Source/santad/ProcessTree/annotations/sandbox_exec_test.mm

Comment on lines +43 to +50
for (size_t i = 0; i < argv.size(); i++) {
if (argv[i] == "-f" && i + 1 < argv.size()) {
return SandboxPolicyInfo{
.profile_path = argv[i + 1],
.status = SandboxPolicyStatus::kPending,
};
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Restrict -f parsing to sandbox-exec option scope.

ParseSandboxExecArgv scans the entire argv, so it can incorrectly capture -f from the target command args (e.g., ... /usr/bin/grep -f pattern). That can misattribute sandbox_policy.profile_path.

🔧 Proposed fix
 std::optional<SandboxPolicyInfo> ParseSandboxExecArgv(
     const std::vector<std::string> &argv) {
-  // argv: ["sandbox-exec", "-f", "<path>", ...]
-  // Scan for the -f flag.
-  for (size_t i = 0; i < argv.size(); i++) {
-    if (argv[i] == "-f" && i + 1 < argv.size()) {
+  // argv: ["sandbox-exec", [options...], "<command>", ...]
+  // Only scan sandbox-exec option tokens (before the command).
+  for (size_t i = 1; i < argv.size(); ++i) {
+    const auto &arg = argv[i];
+    if (arg == "--") break;
+    if (!arg.empty() && arg[0] != '-') break;  // command starts
+    if (arg == "-f" && i + 1 < argv.size()) {
       return SandboxPolicyInfo{
           .profile_path = argv[i + 1],
           .status = SandboxPolicyStatus::kPending,
       };
     }
   }
   return std::nullopt;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/santad/ProcessTree/annotations/sandbox_exec.cc` around lines 43 - 50,
ParseSandboxExecArgv currently scans all argv entries and can pick up a
target-command '-f'. Limit the scan to the sandbox-exec option scope by stopping
when the option/args separator is reached (e.g., "--") or when the sandbox-exec
options end; only examine entries before that separator for "-f". Update
ParseSandboxExecArgv to return SandboxPolicyInfo with profile_path and
SandboxPolicyStatus::kPending only when "-f" is found before the separator,
referencing ParseSandboxExecArgv, SandboxPolicyInfo, profile_path, and
SandboxPolicyStatus::kPending to locate the logic to change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/common comp/santad Issues or PRs related to the daemon lang/objc++ PRs modifying files in ObjC++ size/l Size: large

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant