From 80a2b5e0c714e33760da6f3903250ebff3b07314 Mon Sep 17 00:00:00 2001 From: "C.D.Lee" <265386841+opencdlee-dotcom@users.noreply.github.com> Date: Mon, 24 Aug 2026 15:53:54 -0700 Subject: [PATCH] Custody's second rung was macOS-only too, and the ratchet is empty MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two deferred review findings, plus the debt the last commit left behind. `_grade_binary` offers a sensor exactly two demotions: operator-vouched and package-managed. The second consulted Homebrew, VS Code, pipx and uv — so an apt/rpm/winget-installed binary, the ordinary shape of a developer's toolchain, was scored at full severity with custody=None on Linux and Windows while its Homebrew equivalent on macOS was demoted a step. Both rungs available off-mac were narrower than on mac; the previous commit fixed one of them and this fixes the other. The Linux half needed no new machinery. `_classify_linux` had shelled out to dpkg/rpm/pacman since forever to decide `os-managed`; custody simply never asked. That query is now `_linux_pkg_owner` — one spelling, two callers, with a test asserting they agree. It sits LAST in _PACKAGE_RECEIPTS because it can cost three subprocesses while every probe above it is pure path arithmetic, and it memoizes per resolved path because the custody layer asks about the same handful of programs repeatedly within one scan (also tested: one query per path, not four). Windows gets `_winget_receipt` and `_choco_receipt`, path-shaped like the portable probes beside them but normalizing separators instead of using os.sep, so they are exercisable from any body — the same reason test_cross_platform parses captured Windows output on a Mac. Measured on real files: before [None, None, None] after ['winget:Foo.Bar', 'winget:link', 'choco:ripgrep'] and on Linux, None -> 'dpkg:curl'. A path under Local/Temp still earns nothing; a probe that demoted anything under a user-writable root would be a blind spot, not a rung. And the ratchet from the previous commit is EMPTY — by shrinking, not by deletion, with the stale-entry check still guarding it. Its last three entries were settled by mutation rather than by reading: swapping all four "developer-id" sites to PUBLISHER_TRUST broke exactly one test, TestHotDirAppBundle's, whose subject is an unnotarized Developer-ID .app — a Gatekeeper concept with no analog on any other body, in a class conftest already gates to macOS. That literal stays, with the reason recorded above the class. The other three were inert: `target_trust` is written at aegis.py:11172 and read by no gate at all. Verified: 1045 passed / 4 skipped on macOS, and the whole suite under simulated linux and win diffed against 7279c68 — no new failures on either. Co-Authored-By: Claude Opus 5 (1M context) --- aegis.py | 120 +++++++++++++++++++++++++++++------ tests/test_cross_platform.py | 101 +++++++++++++++++++++++++++-- tests/test_custody.py | 4 +- tests/test_regression.py | 9 ++- 4 files changed, 202 insertions(+), 32 deletions(-) diff --git a/aegis.py b/aegis.py index d7e7186..c629fd8 100755 --- a/aegis.py +++ b/aegis.py @@ -1783,6 +1783,46 @@ def classify_signature(path): return result +_LINUX_PKG_CACHE = {} + + +def _linux_pkg_owner(real): + """":" for the distro package that owns `real`, else None. + + ONE spelling, two callers: _classify_linux turns it into the `os-managed` + trust verdict, and _os_package_receipt turns the same fact into a custody + receipt. Before this it existed only inside the classifier, so custody + could not see it and every apt/rpm-installed binary was scored as if it had + no provenance at all. + + Cached per resolved path: this is up to three subprocesses, and the custody + layer asks about the same handful of programs repeatedly within one scan. + """ + if not real: + return None + if real in _LINUX_PKG_CACHE: + return _LINUX_PKG_CACHE[real] + # Package managers never own $HOME/tmp content — skip the subprocess. + if any(real.startswith(p) for p in + (HOME + "/", "/home/", "/root/", "/tmp/", "/var/tmp/", "/dev/shm/", + "/run/")): + return None + owner = None + out, _, rc = run(["dpkg-query", "-S", real], timeout=10) + if rc == 0 and ":" in (out or ""): + owner = "dpkg:" + out.split(":", 1)[0].strip() + if owner is None: + out, _, rc = run(["rpm", "-qf", real], timeout=10) + if rc == 0 and out.strip() and "not owned" not in out: + owner = "rpm:" + out.strip().splitlines()[0] + if owner is None: + out, _, rc = run(["pacman", "-Qqo", real], timeout=10) + if rc == 0 and out.strip(): + owner = "pacman:" + out.strip().splitlines()[0] + _LINUX_PKG_CACHE[real] = owner + return owner + + def _classify_linux(path): """Linux has no ambient code-signing; the honest analog is package-manager ownership: a file dpkg/rpm/pacman accounts for was installed by root through @@ -1790,27 +1830,10 @@ def _classify_linux(path): behavior, not treated as malign by itself (every locally-built dev binary is unmanaged).""" result = {"trust": "unmanaged", "team": None, "authority": None} - real = os.path.realpath(path) - # Package managers never own $HOME/tmp content — skip the subprocess. - if any(real.startswith(p) for p in - (HOME + "/", "/home/", "/root/", "/tmp/", "/var/tmp/", "/dev/shm/", - "/run/")): - return result - out, _, rc = run(["dpkg-query", "-S", real], timeout=10) - if rc == 0 and ":" in (out or ""): - result["trust"] = "os-managed" - result["authority"] = "dpkg:" + out.split(":", 1)[0].strip() - return result - out, _, rc = run(["rpm", "-qf", real], timeout=10) - if rc == 0 and out.strip() and "not owned" not in out: - result["trust"] = "os-managed" - result["authority"] = "rpm:" + out.strip().splitlines()[0] - return result - out, _, rc = run(["pacman", "-Qqo", real], timeout=10) - if rc == 0 and out.strip(): + owner = _linux_pkg_owner(os.path.realpath(path)) + if owner: result["trust"] = "os-managed" - result["authority"] = "pacman:" + out.strip().splitlines()[0] - return result + result["authority"] = owner return result @@ -10043,8 +10066,63 @@ def _uv_python_receipt(real): return None +def _winget_receipt(real): + """A file winget put on disk. Path-shaped, no subprocess — winget installs + into %LOCALAPPDATA%\\Microsoft\\WinGet\\Packages\\_\\ and + shims into ...\\WinGet\\Links\\. + + Separators are normalized rather than using os.sep, so this is exercisable + from any body — the same reason tests/test_cross_platform.py parses captured + Windows output on a Mac. The other portable probes predate that lesson. + """ + p = (real or "").replace("\\", "/") + low = p.lower() + marker = "/microsoft/winget/packages/" + i = low.find(marker) + if i >= 0: + head = p[i + len(marker):].split("/")[0] + if head: + # Directory is "_"; the id is the fact. + return "winget:%s" % head.split("_")[0] + if "/microsoft/winget/links/" in low: + return "winget:link" + return None + + +def _choco_receipt(real): + """A file Chocolatey put on disk: \\chocolatey\\lib\\\\.""" + p = (real or "").replace("\\", "/") + marker = "/chocolatey/lib/" + i = p.lower().find(marker) + if i >= 0: + head = p[i + len(marker):].split("/")[0] + if head: + return "choco:%s" % head + return None + + +def _os_package_receipt(real): + """The OS-NATIVE package manager's claim on `real`. + + The gap this closes: `_grade_binary` offers non-mac bodies exactly two + custody rungs, and the second consulted only Homebrew, VS Code, pipx and uv + — so an apt/rpm/winget-installed binary, the ordinary shape of a + developer's toolchain, was scored at full severity with custody=None on + Linux and Windows while its Homebrew equivalent on macOS was demoted a + step. macOS needs no entry here: Homebrew IS its native manager and + _homebrew_receipt already covers it. + """ + if IS_LINUX: + return _linux_pkg_owner(real) + return None + + +# _os_package_receipt is LAST on purpose: the probes above it are pure path +# arithmetic, while it can cost up to three subprocesses on Linux. Cheap +# questions first, so the expensive one is only asked when no cheap answer won. _PACKAGE_RECEIPTS = (_homebrew_receipt, _vscode_receipt, _pipx_receipt, - _uv_python_receipt) + _uv_python_receipt, _winget_receipt, _choco_receipt, + _os_package_receipt) def _package_receipt(path): diff --git a/tests/test_cross_platform.py b/tests/test_cross_platform.py index c500f54..1c66dbd 100644 --- a/tests/test_cross_platform.py +++ b/tests/test_cross_platform.py @@ -664,13 +664,14 @@ class NoTestHardCodesOneBodysTrustVocabulary(unittest.TestCase): # change what a macOS assertion means (one of them is literally about # vendor impersonation), which is a worse bug than the one being fixed. # They need a per-case reading, not a mechanical rewrite. - "test_regression.py:TestVendorImpersonation", - "test_regression.py:TestPersistenceEnvDiff", - # Same reasoning: `_target_change` defaults both sides to - # "developer-id" to compare TEAM identity across a target swap, and - # PUBLISHER_TRUST is "apple" on macOS — a mechanical swap would change - # what the macOS assertion compares. - "test_custody.py:CustodyGrading", + # Empty, and it must stay that way by shrinking rather than by + # deletion: the stale-entry check above fails on any name left here + # that no longer offends, so this list cannot quietly become an + # exemption list. All nine original entries were resolved by + # 2026-08-24 — seven mechanically, and the last two by mutation + # testing that proved their "developer-id" default inert (only + # TestHotDirAppBundle's was load-bearing, and that class is + # macOS-gated). )) def test_no_ungated_test_stubs_a_macos_only_trust_verdict(self): @@ -793,6 +794,92 @@ def test_the_outbound_sensor_mints_a_finding_on_every_body(self): setattr(aegis, n, fn) +class NativePackageManagersEarnAReceipt(unittest.TestCase): + """The second custody rung, on the bodies that are not macOS. + + `_grade_binary` offers a sensor exactly two demotions: operator-vouched and + package-managed. The second consulted Homebrew, VS Code, pipx and uv only — + so an apt/rpm/winget-installed binary, which is the ordinary shape of a + developer's toolchain, was scored at full severity with custody=None on + Linux and Windows while its Homebrew equivalent on macOS was demoted a + step. Both rungs available off-mac were narrower than on mac. + + The Linux half needed no new machinery: `_classify_linux` had shelled out + to dpkg/rpm/pacman since forever to decide `os-managed`, and custody simply + never asked. That query is now `_linux_pkg_owner`, one spelling with two + callers. + """ + + def setUp(self): + self._flags = aegis.IS_WIN, aegis.IS_LINUX, aegis.IS_MAC + self._run = aegis.run + aegis._LINUX_PKG_CACHE.clear() + + def tearDown(self): + aegis.IS_WIN, aegis.IS_LINUX, aegis.IS_MAC = self._flags + aegis.run = self._run + aegis._LINUX_PKG_CACHE.clear() + + def _win_tree(self, rel): + d = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, d, True) + path = os.path.join(d, *rel.split("/")) + os.makedirs(os.path.dirname(path), exist_ok=True) + open(path, "w").close() + return path + + def test_winget_and_chocolatey_paths_earn_a_receipt(self): + aegis.IS_WIN, aegis.IS_LINUX, aegis.IS_MAC = True, False, False + for rel, want in ( + ("Local/Microsoft/WinGet/Packages/Foo.Bar_1.2.3/tool.exe", "winget:Foo.Bar"), + ("Local/Microsoft/WinGet/Links/tool.exe", "winget:link"), + ("ProgramData/chocolatey/lib/ripgrep/tools/rg.exe", "choco:ripgrep")): + self.assertEqual(aegis._package_receipt(self._win_tree(rel)), want, rel) + + def test_an_unrelated_windows_path_earns_nothing(self): + """The probes must not be a blanket amnesty for anything under a + user-writable root — that would turn a demotion into a blind spot.""" + aegis.IS_WIN, aegis.IS_LINUX, aegis.IS_MAC = True, False, False + self.assertIsNone(aegis._package_receipt( + self._win_tree("Local/Temp/payload/tool.exe"))) + + def test_a_distro_package_earns_a_receipt_on_linux(self): + aegis.IS_WIN, aegis.IS_LINUX, aegis.IS_MAC = False, True, False + aegis.run = lambda cmd, **k: (("curl: /usr/bin/curl", "", 0) + if cmd[0] == "dpkg-query" else ("", "", 1)) + # A real file OUTSIDE $HOME: _linux_pkg_owner skips the subprocess for + # $HOME/tmp paths (package managers never own them), and + # _package_receipt only probes candidates that exist on disk. + probe = "/usr/bin/curl" + if not os.path.exists(probe): + self.skipTest("no /usr/bin/curl on this body") + self.assertEqual(aegis._package_receipt(probe), "dpkg:curl") + + def test_the_distro_query_is_asked_once_per_path(self): + """Up to three subprocesses per path, asked about the same handful of + programs repeatedly within one scan.""" + aegis.IS_WIN, aegis.IS_LINUX, aegis.IS_MAC = False, True, False + calls = [] + + def counting(cmd, **k): + calls.append(cmd[0]) + return ("curl: /usr/bin/curl", "", 0) if cmd[0] == "dpkg-query" else ("", "", 1) + + aegis.run = counting + for _ in range(4): + aegis._linux_pkg_owner("/usr/bin/curl") + self.assertEqual(calls, ["dpkg-query"]) + + def test_the_classifier_and_the_receipt_agree(self): + """One spelling, two callers — a split here is how the custody layer + went blind to a fact the trust layer already had.""" + aegis.IS_WIN, aegis.IS_LINUX, aegis.IS_MAC = False, True, False + aegis.run = lambda cmd, **k: (("curl: /usr/bin/curl", "", 0) + if cmd[0] == "dpkg-query" else ("", "", 1)) + self.assertEqual(aegis._classify_linux("/usr/bin/curl")["authority"], + aegis._linux_pkg_owner("/usr/bin/curl")) + + class PublisherStableIsReachableOnEveryBody(unittest.TestCase): """The `publisher-stable` custody demotion, per body. diff --git a/tests/test_custody.py b/tests/test_custody.py index a6c5f79..ca8ebae 100644 --- a/tests/test_custody.py +++ b/tests/test_custody.py @@ -363,8 +363,8 @@ def test_conceal_imperative_never_downgrades(self): self.assertEqual(len(fs), 1) self.assertEqual(fs[0]["severity"], "HIGH") - def _target_change(self, old_team, new_team, old_trust="developer-id", - new_trust="developer-id"): + def _target_change(self, old_team, new_team, old_trust=PUBLISHER_TRUST, + new_trust=PUBLISHER_TRUST): cfg = "/nonexistent-custody/.mcp.json" key = "mcpServers.probe|node" diff --git a/tests/test_regression.py b/tests/test_regression.py index 981fa89..397cf00 100644 --- a/tests/test_regression.py +++ b/tests/test_regression.py @@ -1551,7 +1551,7 @@ def test_wallet_config_change_is_high(self): class TestVendorImpersonation(Sandbox): def _sev(self, label, authority, prog="/Users/x/.hidden/GoogleUpdate"): rec = {"label": label, "program": prog, "args": [prog], - "trust": "developer-id", "sha256": "s", "run_at_load": True, + "trust": PUBLISHER_TRUST, "sha256": "s", "run_at_load": True, "env": None, "authority": authority} return aegis.check_persistence({}, {"/fake/x.plist": rec})[0]["severity"] @@ -2214,6 +2214,11 @@ def test_listener_surface_participates_in_scan_quietly(self): # invisible to the file-oriented Mach-O check. Ad-hoc bundle ⇒ HIGH; signed-but- # unnotarized ⇒ MEDIUM with Gatekeeper's own verdict; notarized ⇒ silent. # --------------------------------------------------------------------------- # +# Keeps the LITERAL "developer-id" on purpose: an unnotarized Developer-ID +# .app is a macOS Gatekeeper concept with no analog on any other body, and +# this class is macOS-gated in conftest for exactly that reason. Proven by +# mutation 2026-08-24 — swapping it to PUBLISHER_TRUST was the only one of +# four sites that broke a test. class TestHotDirAppBundle(Sandbox): def _mk_app(self, name="Evil.app"): app = os.path.join(self.hot, name) @@ -3287,7 +3292,7 @@ def _base(self): return {"label": "com.benign.updater", "program": "/opt/homebrew/bin/updater", "args": ["/opt/homebrew/bin/updater"], "sha256": "a" * 64, - "trust": "developer-id", "run_at_load": True, + "trust": PUBLISHER_TRUST, "run_at_load": True, "authority": "Developer ID Application: Benign Corp (TEAM123456)", "env": None}