Skip to content

Check package installs that a macOS session process starts - #85

Merged
ihavespoons merged 2 commits into
mainfrom
macos-package-checks
Nov 3, 2026
Merged

ihavespoons merged 2 commits into
mainfrom
macos-package-checks

Conversation

@ihavespoons

Copy link
Copy Markdown
Contributor

Stacked on #84. Retarget to main after #84 merges.

The problem

Only the command that agentmon exec started was package-checked. An install that an agent ran under agentmon wrap, or that a command started, was not. On build 1678, an agent's pip install django==2.2 (CVE-2022-34265, CVSS 9.8) installed under wrap.

Hardware testing then found four defects that stopped the package check from working as agent-default intends, on the exec API path as well:

  • The OSV provider called /v1/querybatch, which returns only id and modified for each vulnerability. Every finding defaulted to medium, so a rule for critical vulnerabilities never matched.
  • The pip resolver ran without --quiet. pip 26 writes Collecting ... lines to stdout before the JSON report, so the report never parsed.
  • The checker used the package rules of the daemon's default policy for every session, not the session's policy.
  • agent-default had no rule for other findings. An unmatched finding blocks, and the local provider reports "license unknown" for every package, so every install was blocked.

The fix

macOS exec path:

  • PolicyAdapter.CheckExec runs the package check on an exec that the command rules allow. A block or a failed check denies the exec, with rule package-check. An approve verdict denies the exec and opens a dialog, as an approve exec rule does.
  • ES reports a script's interpreter as the executable. The extension now sends the script, and the daemon classifies argv[1], so pip3 install x arrives as pip3.
  • An exec whose arguments hold an install word waits until 1 s before the ES deadline, at most 30 s, on its own socket connection (PolicySocketClient.requestOnce). Every other exec keeps the 2 s limit on the shared connection. If the extension misses an install, the daemon has 1.5 s, and an unfinished check denies.

pkgcheck:

  • python -m pip counts as pip, and pip3.15 as pip3.
  • Checker reuses a verdict for the same install, directory and rules for 30 s. So one agentmon exec pip install does not resolve twice.
  • CheckForPolicy takes the session's package rules, on both paths.
  • ComposeRules applies a policy's final catch-all after the block_on rules, so a stricter block_on still blocks.
  • The OSV provider fetches each record from /v1/vulns/{id}, 16 at a time, and caches it.
  • The pip resolver passes --quiet.

Policy:

  • agent-default ends its package_rules with an allow for every other finding.

Verification

  • scripts/test-macos-package-checks.sh on build 1682 passes 4 of 4. Under wrap, an agent's pip install django==2.2 and python -m pip install django==2.2 are refused with [pip] Django@2.2 -> block (83 finding(s)), and pip install six==1.16.0 installs. On build 1678, both django installs succeeded.
  • Against the live OSV API, Django 2.2 gives 6 critical, 17 high, 46 medium and 6 low findings. Before, all of them were medium.
  • On build 1682 there are no regressions: test-macos-wrap-python.sh 3 of 3, test-macos-wrap-session-reuse.sh 4 of 4, test-macos-socket-signal-rules.sh 13 ok, test-macos-network-proxy.sh 6 of 6.
  • New Go tests cover the adapter verdicts, install command detection, the verdict cache, session rules, rule order, and the OSV record fetch.
  • go test -tags nofuse ./... passes, and golangci-lint reports 0 issues.

Known limits

  • launchd starts the daemon with no Homebrew directory in PATH, so the default resolver command pip is not found. Every check then fails, and the install is denied. The test sets package_checks.resolvers.pip.dry_run_command.
  • The resolver runs pip from the daemon, outside the session. That is the existing design.

Part of SECENG-308.

🤖 Generated with Claude Code

ihavespoons and others added 2 commits November 3, 2026 08:51
Only the command that `agentmon exec` started was package-checked. An
install that an agent ran under `agentmon wrap`, or that a command
started, was not. On build 1678, `pip install django==2.2` (CVE-2022-34265,
CVSS 9.8) installed under wrap, although agent-default blocks a critical
vulnerability.

PolicyAdapter.CheckExec now runs the package check on an exec that the
command rules allow. A block or a failed check denies the exec. An approve
verdict denies it and asks for a dialog, as an approve exec rule does.

ES reports a script's interpreter as the executable, so `pip3 install x`
arrives as python3.15. The kernel puts the script path in argv[1], and
the extension now sends the script, so the daemon classifies argv[1].
pkgcheck also reads `python -m pip` as pip, and a versioned name such as
pip3.15 as pip3.

A check resolves the install and asks the providers, which takes seconds.
An exec whose arguments hold an install word now waits until 1 s before
the ES deadline, at most 30 s, on a socket connection of its own. Every
other exec keeps the 2 s limit on the shared connection.

The exec API and the extension share one Checker, which reuses a verdict
for the same install for 30 s, so one `agentmon exec pip install` does not
resolve twice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Testing the macOS package check on hardware found four defects that
stopped any package check from working as agent-default intends. They
affect the exec API path as well.

- The OSV provider called /v1/querybatch, which returns only the ID of
  each vulnerability. Every finding defaulted to medium, so a rule for
  critical vulnerabilities never matched. The provider now fetches each
  record from /v1/vulns/{id}, 16 at a time, and caches it. Django 2.2
  now gives 6 critical findings.
- The pip resolver ran without --quiet. pip 26 writes "Collecting ..."
  lines to stdout before the JSON report, so the report never parsed.
- The checker used the package rules of the daemon's default policy for
  every session. CheckForPolicy now takes the session's own rules, on
  the exec API path and on the extension path.
- agent-default had no rule for findings other than critical
  vulnerabilities and malware. A finding that no rule matches blocks, and
  the local provider reports "license unknown" for every package, so every
  install was blocked. agent-default now ends with an allow for every
  other finding. ComposeRules applies a policy's final catch-all after the
  block_on rules, so a stricter block_on still blocks.

On build 1682, scripts/test-macos-package-checks.sh passes 4 of 4: an
agent's `pip install django==2.2` and `python -m pip install django==2.2`
under wrap are refused for the critical vulnerability, and
`pip install six==1.16.0` installs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Base automatically changed from macos-wrap-python-reexec to main November 3, 2026 08:08
@ihavespoons
ihavespoons merged commit 4289133 into main Nov 3, 2026
8 checks passed
@ihavespoons
ihavespoons deleted the macos-package-checks branch November 3, 2026 08:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant