hermes: audit every sudo include spelling

A quoted, space-bearing #include path failed the include regex and fell
through to the comment branch, leaving a second authority file
unenumerated. Detect any include directive before the comment rule and
fail closed on every form except the exact bare includedir into the
audited /etc/sudoers.d.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jenkins 2026-08-17 20:37:30 -03:00
parent 6e9e8c6ab5
commit 6c1123201e
2 changed files with 60 additions and 7 deletions

View File

@ -89,13 +89,22 @@ def _active_sudo_policy(text: str) -> str:
stripped = line.lstrip()
if not stripped:
continue
include = re.fullmatch(
r"(?i)([#@]include|[#@]includedir)\s+([^\s]+)", stripped
)
if include:
directive, target = include.groups()
if directive.lower().endswith("includedir") and target == "/etc/sudoers.d":
continue
# Detect any include spelling before the comment rule below. sudo
# honours both #include and @include; a quoted or space-bearing path
# must never fall through the "#" comment branch and leave a second
# authority file unaudited. Only the exact bare includedir into the
# audited /etc/sudoers.d passes; everything else fails closed.
if re.match(r"(?i)[#@]include(?:dir)?(?=\s|$)", stripped):
include = re.fullmatch(
r"(?i)([#@]include(?:dir)?)\s+([^\s\"'\\]+)", stripped
)
if include:
directive, target = include.groups()
if (
directive.lower().endswith("includedir")
and target == "/etc/sudoers.d"
):
continue
raise HardeningError("sudo policy includes an unaudited authority source")
if stripped.startswith("#") and not re.match(r"#\d+(?:\s|$)", stripped):
continue

View File

@ -105,6 +105,50 @@ def test_active_sudo_policy_handles_blank_comments_numeric_and_includes():
module._active_sudo_policy("@include /external/policy\n")
@pytest.mark.parametrize(
"directive",
[
'#include "/etc/evil path"',
"#include '/etc/evil path'",
"#include /etc/evil\\ path",
"#include\t/etc/evil",
"#include /etc/evil",
"#includedir /opt/other",
'#includedir "/etc/sudoers.d"',
"@include /external/policy",
'@include "/etc/evil path"',
"@includedir /opt/other",
"#include",
"@includedir",
],
)
def test_every_include_spelling_outside_the_standard_dir_fails_closed(directive: str):
"""No include form may reach the comment branch and stay unaudited."""
module = _audit_module()
with pytest.raises(module.HardeningError, match="unaudited"):
module._active_sudo_policy(directive + "\n")
@pytest.mark.parametrize(
"directive",
["#includedir /etc/sudoers.d", "@includedir /etc/sudoers.d"],
)
def test_standard_includedir_is_the_only_allowed_include(directive: str):
module = _audit_module()
active = module._active_sudo_policy(directive + "\nroot ALL=(ALL) ALL\n")
assert active == "root ALL=(ALL) ALL"
@pytest.mark.parametrize(
"line",
["#included by an operator note", "#include-guard notes", "#includes history"],
)
def test_comment_lines_that_only_resemble_includes_stay_comments(line: str):
module = _audit_module()
active = module._active_sudo_policy(line + "\nroot ALL=(ALL) ALL\n")
assert active == "root ALL=(ALL) ALL"
def test_identity_matcher_covers_account_numeric_and_unrelated_values():
module = _audit_module()
assert module._mentions_dedicated_identity("hermes-agent ALL", "hermes-agent", 1200)