atlas-iac/testing/tests/test_hermes_node_polkit_identity.py
jenkins b8f2163af0 hermes: prove sudo and polkit denial closed
Expand sudoers User_Alias chains so aliases, wildcards, netgroups, and
undefined names cannot smuggle authority to the Hermes account, and
require polkit grants to scope through exact literal identity
comparisons: computed strings, bracket lookups, subject aliasing,
operator-built values, and unconditional or wildcard grants fail closed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 15:15:47 -03:00

211 lines
7.1 KiB
Python

"""Literal-identity denial proofs for polkit grants near the Hermes account."""
from __future__ import annotations
import importlib.util
import os
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).parents[2]
SCRIPTS = ROOT / "services/hermes/scripts"
sys.path.insert(0, str(SCRIPTS))
def _load():
spec = importlib.util.spec_from_file_location(
"node_polkit_audit_test", SCRIPTS / "node_polkit_audit.py"
)
assert spec and spec.loader
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
def _rules(module, text: str):
module.audit_polkit_policy(Path("rules.d/50-test.rules"), text)
def test_literal_distribution_rule_passes():
module = _load()
_rules(
module,
"// allow the network daemon\n"
"polkit.addRule(function(action, subject) {\n"
' if (action.id == "org.freedesktop.network1.set-dns" &&\n'
' subject.user == "systemd-network") {\n'
" return polkit.Result.YES;\n"
" }\n"
"});\n",
)
_rules(
module,
"polkit.addRule(function(action, subject) {\n"
' if (subject.isInGroup("wheel") && subject.uid === 4242) {\n'
" return polkit.Result.YES;\n"
" }\n"
"});\n",
)
_rules(
module,
"/* deny-only rules may use constructs the audit cannot prove */\n"
"polkit.addRule(function(action, subject) {\n"
" if (subject.user.indexOf(\"x\") == 0) { return polkit.Result.NO; }\n"
"});\n",
)
def test_reversed_literal_comparisons_pass():
module = _load()
_rules(
module,
"polkit.addRule(function(action, subject) {\n"
' if ("atlas" == subject.user || 4242 === subject.uid) {\n'
" return polkit.Result.YES;\n"
" }\n"
"});\n",
)
def test_unconditional_grant_fails_closed():
module = _load()
with pytest.raises(module.HardeningError, match="explicit identity"):
_rules(module, "polkit.addRule(function() { return polkit.Result.YES; });\n")
@pytest.mark.parametrize(
("snippet", "match"),
[
('subject.user == "herme" + "s-agent"', "operators"),
("subject.uid == 1199 + 1", "operators"),
("subject.uid == 0x4B0", "exact literal"),
("subject.uid >= 1000", "exact literal"),
("subject.uid == -1", "operators"),
("subject.user == name", "exact literal"),
("subject.user.indexOf(\"h\") == 0", "exact literal"),
("subject.isInGroup(group)", "exact literal"),
('subject["user"] == "atlas"', "computed lookup"),
],
)
def test_computed_identity_grants_fail_closed(snippet: str, match: str):
module = _load()
text = (
"polkit.addRule(function(action, subject) {\n"
f" if ({snippet}) {{ return polkit.Result.YES; }}\n"
"});\n"
)
with pytest.raises(module.HardeningError, match=match):
_rules(module, text)
@pytest.mark.parametrize(
("text", "match"),
[
("var x = `hermes`;\n", "template strings"),
('var x = "unterminated\n', "unterminated"),
('var x = "bad\\x2descape";\n', "computed escapes"),
('var x = "dangling\\', "computed escapes"),
("/* never closed\n", "comment is unterminated"),
("var f = eval;\n", "cannot prove"),
('var s = String.fromCharCode(104);\n', "cannot prove"),
("var a = arguments;\n", "cannot prove"),
('var r = polkit.Result["YES"];\n', "computed lookup"),
("var r = polkit.Result;\n", "computed lookup"),
("var s = subject;\n", "computed code"),
("check(subject);\n", "computed code"),
],
)
def test_unprovable_rule_constructs_fail_closed(text: str, match: str):
module = _load()
with pytest.raises(module.HardeningError, match=match):
_rules(module, text)
def test_string_and_comment_handling_keeps_literal_rules_auditable():
module = _load()
code, strings = module._split_strings(
'var a = "with // not a comment"; // trailing\n'
"/* block */ var b = 'quo\\'te\\n';\n"
)
assert strings == ["with // not a comment", "quo'te\n"]
assert "//" not in code and "block" not in code
assert "\x000\x00" in code and "\x001\x00" in code
def test_grant_detection_survives_spacing_and_strings():
module = _load()
with pytest.raises(module.HardeningError, match="explicit identity"):
_rules(module, "polkit.addRule(function() { return polkit.Result . YES; });\n")
# A grant token inside a string is inert and needs no identity scope.
_rules(module, 'var label = "polkit.Result.YES";\n')
@pytest.mark.parametrize(
("text", "match"),
[
(
"[All]\nIdentity=unix-user:*\nAction=org.example\nResultAny=yes\n",
"exact literal",
),
(
"[N]\nIdentity=unix-netgroup:ops\nAction=org.example\nResultActive=yes\n",
"exact literal",
),
("[N]\nAction=org.example\nResultInactive=yes\n", "identity scope"),
("[N]\nIdentity=\nAction=org.example\nResultAny=yes\n", "identity scope"),
],
)
def test_pkla_grants_require_exact_literal_identities(text: str, match: str):
module = _load()
with pytest.raises(module.HardeningError, match=match):
module.audit_polkit_policy(Path("localauthority/50-local.d/a.pkla"), text)
def test_pkla_literal_grants_and_deny_entries_pass():
module = _load()
module.audit_polkit_policy(
Path("localauthority/50-local.d/a.pkla"),
"[Mount]\nIdentity=unix-user:atlas;unix-group:operators\n"
"Action=org.freedesktop.udisks2.filesystem-mount\nResultAny=yes\n",
)
module.audit_polkit_policy(
Path("etc/b.pkla"),
"[Deny]\nIdentity=unix-user:*\nAction=org.example\nResultAny=no\n",
)
def test_integration_rejects_previously_invisible_grants(tmp_path: Path):
spec = importlib.util.spec_from_file_location(
"node_account_audit_polkit_test", SCRIPTS / "node_account_audit.py"
)
assert spec and spec.loader
audit = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = audit
spec.loader.exec_module(audit)
host_etc = tmp_path / "etc"
rules = host_etc / "polkit-1/rules.d"
rules.mkdir(parents=True)
(rules / "90-open.rules").write_text(
"polkit.addRule(function() { return polkit.Result.YES; });\n",
encoding="utf-8",
)
with pytest.raises(audit.HardeningError, match="explicit identity"):
audit.audit_privilege_policies(
"hermes-agent", 1200, os.getuid(), host_etc, tmp_path / "missing"
)
(rules / "90-open.rules").unlink()
local = host_etc / "polkit-1/localauthority/50-local.d"
local.mkdir(parents=True)
(local / "wide.pkla").write_text(
"[W]\nIdentity=unix-user:*\nAction=org.example.benign\nResultAny=yes\n",
encoding="utf-8",
)
with pytest.raises(audit.HardeningError, match="exact literal"):
audit.audit_privilege_policies(
"hermes-agent", 1200, os.getuid(), host_etc, tmp_path / "missing"
)