From 9ca6dff2ead9cd9ff2b7dd3bab466568e7ff2226 Mon Sep 17 00:00:00 2001 From: ogt Date: Tue, 14 Jul 2026 16:05:24 +0800 Subject: [PATCH] fix(host112): verify exact nopasswd sudo entries --- .../install-host112-guest-recovery.sh | 39 ++++++++++++--- ...install_host112_guest_recovery_contract.py | 48 +++++++++++++++++++ 2 files changed, 80 insertions(+), 7 deletions(-) diff --git a/scripts/reboot-recovery/install-host112-guest-recovery.sh b/scripts/reboot-recovery/install-host112-guest-recovery.sh index 36553244e..d3c328156 100755 --- a/scripts/reboot-recovery/install-host112-guest-recovery.sh +++ b/scripts/reboot-recovery/install-host112-guest-recovery.sh @@ -162,12 +162,37 @@ resolve_target_user() { AUTH_FILE="${AUTH_DIR}/authorized_keys" } -sudo_policy_allows() { +sudo_policy_output_has_nopasswd_match() { + LC_ALL=C awk ' + /^Sudoers entry:/ { + if (entry_nopasswd && entry_matched) found = 1 + entry_nopasswd = 0 + entry_matched = 0 + next + } + /^[[:space:]]*Options:/ && /(^|[ ,])!authenticate([ ,]|$)/ { + entry_nopasswd = 1 + next + } + /^[[:space:]]*Matched:/ { + entry_matched = 1 + next + } + END { + if (entry_nopasswd && entry_matched) found = 1 + exit(found ? 0 : 1) + } + ' +} + +sudo_policy_is_nopasswd() { + local policy_output if [ "${EUID:-$(id -u)}" -eq 0 ]; then - sudo -n -U "$TARGET_USER" -l "$@" >/dev/null 2>&1 + policy_output="$(LC_ALL=C sudo -n -U "$TARGET_USER" -ll "$@" 2>/dev/null)" || return 1 else - sudo -n -l "$@" >/dev/null 2>&1 + policy_output="$(LC_ALL=C sudo -n -ll "$@" 2>/dev/null)" || return 1 fi + printf '%s\n' "$policy_output" | sudo_policy_output_has_nopasswd_match } agent99_direct_authorization_ready() { @@ -246,25 +271,25 @@ refresh_gate_state() { SUDOERS_VALID=1 fi SUDO_CORRELATED_APPLY_NOPASSWD=0 - if sudo_policy_allows "$READINESS_TARGET" --apply \ + if sudo_policy_is_nopasswd "$READINESS_TARGET" --apply \ --trace-id host112-policy-check \ --run-id host112-policy-check \ --work-item-id HOST112-WAZUH-MANAGER-RECOVERY; then SUDO_CORRELATED_APPLY_NOPASSWD=1 fi SUDO_CORRELATED_DRY_RUN_NOPASSWD=0 - if sudo_policy_allows "$READINESS_TARGET" --dry-run \ + if sudo_policy_is_nopasswd "$READINESS_TARGET" --dry-run \ --trace-id host112-policy-check \ --run-id host112-policy-check \ --work-item-id HOST112-WAZUH-MANAGER-RECOVERY; then SUDO_CORRELATED_DRY_RUN_NOPASSWD=1 fi SUDO_LEGACY_UNCORRELATED_APPLY_NOPASSWD=0 - if sudo_policy_allows "$READINESS_TARGET" --apply; then + if sudo_policy_is_nopasswd "$READINESS_TARGET" --apply; then SUDO_LEGACY_UNCORRELATED_APPLY_NOPASSWD=1 fi SUDO_UNSAFE_IDENTITY_NOPASSWD=0 - if sudo_policy_allows "$READINESS_TARGET" --apply \ + if sudo_policy_is_nopasswd "$READINESS_TARGET" --apply \ --trace-id 'unsafe value' \ --run-id host112-policy-check \ --work-item-id HOST112-WAZUH-MANAGER-RECOVERY; then diff --git a/scripts/reboot-recovery/tests/test_install_host112_guest_recovery_contract.py b/scripts/reboot-recovery/tests/test_install_host112_guest_recovery_contract.py index 0da6c077f..b83365082 100644 --- a/scripts/reboot-recovery/tests/test_install_host112_guest_recovery_contract.py +++ b/scripts/reboot-recovery/tests/test_install_host112_guest_recovery_contract.py @@ -49,6 +49,54 @@ def test_sudo_policy_has_exact_correlated_apply_and_dry_run_only() -> None: assert 'SUDO_LEGACY_UNCORRELATED_APPLY_NOPASSWD' in text assert 'SUDO_UNSAFE_IDENTITY_NOPASSWD' in text assert 'SUDO_POLICY_READY' in text + assert 'sudo_policy_is_nopasswd' in text + assert 'Options:' in text + assert '!authenticate' in text + + +def test_sudo_policy_parser_requires_nopasswd_and_match_in_same_entry() -> None: + text = source() + parser = text[ + text.index("sudo_policy_output_has_nopasswd_match() {") : text.index( + "agent99_direct_authorization_ready() {" + ) + ] + broad_password_rule = """Sudoers entry: /etc/sudoers + RunAsUsers: ALL + Commands: + ALL + Matched: /usr/local/sbin/readiness --apply +""" + exact_nopasswd_rule = broad_password_rule + """Sudoers entry: /etc/sudoers.d/agent99 + RunAsUsers: root + Options: !authenticate + Commands: + /usr/local/sbin/readiness ^--apply .*$ + Matched: /usr/local/sbin/readiness --apply --trace-id safe +""" + split_entry_false_positive = """Sudoers entry: /etc/sudoers.d/one + Options: !authenticate + Commands: + /usr/bin/true +Sudoers entry: /etc/sudoers + Commands: + ALL + Matched: /usr/local/sbin/readiness --apply +""" + + def parse(sample: str) -> subprocess.CompletedProcess[str]: + return subprocess.run( + ["bash", "-c", parser + "\nsudo_policy_output_has_nopasswd_match"], + input=sample, + text=True, + capture_output=True, + check=False, + timeout=10, + ) + + assert parse(broad_password_rule).returncode != 0 + assert parse(exact_nopasswd_rule).returncode == 0 + assert parse(split_entry_false_positive).returncode != 0 def test_windows99_direct_route_is_fingerprint_bound_and_not_forced() -> None: