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:
parent
6e9e8c6ab5
commit
6c1123201e
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user