Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 99 additions & 21 deletions aegis.py
Original file line number Diff line number Diff line change
Expand Up @@ -1783,34 +1783,57 @@ def classify_signature(path):
return result


_LINUX_PKG_CACHE = {}


def _linux_pkg_owner(real):
""""<manager>:<package>" 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
the distro pipeline. Everything else is 'unmanaged' — scored by location and
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


Expand Down Expand Up @@ -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\\<Package.Id>_<hash>\\ 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 "<Package.Id>_<install hash>"; 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: <ProgramData>\\chocolatey\\lib\\<pkg>\\."""
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):
Expand Down
101 changes: 94 additions & 7 deletions tests/test_cross_platform.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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.

Expand Down
4 changes: 2 additions & 2 deletions tests/test_custody.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
9 changes: 7 additions & 2 deletions tests/test_regression.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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}

Expand Down