Some checks failed
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 2m35s
CD Pipeline / build-and-deploy (push) Failing after 7m35s
CD Pipeline / post-deploy-checks (push) Has been skipped
AWOOOI Harbor 110 Local Repair / workflow-shape (push) Successful in 1s
AWOOOI Harbor 110 Local Repair / harbor-110-local-repair (push) Successful in 16s
581 lines
26 KiB
Python
581 lines
26 KiB
Python
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[3]
|
|
READINESS = ROOT / "scripts/reboot-recovery/host112-guest-readiness.sh"
|
|
INSTALLER = ROOT / "scripts/reboot-recovery/install-host112-guest-recovery.sh"
|
|
SERVICE = ROOT / "scripts/reboot-recovery/awoooi-host112-guest-recovery.service"
|
|
CONTROL = ROOT / "agent99-control-plane.ps1"
|
|
BOOTSTRAP = ROOT / "agent99-bootstrap.ps1"
|
|
|
|
|
|
def read(path: Path) -> str:
|
|
return path.read_text(encoding="utf-8")
|
|
|
|
|
|
def isolated_env(tmp_path: Path) -> dict[str, str]:
|
|
env = os.environ.copy()
|
|
env.update(
|
|
{
|
|
"HOST112_RECOVERY_STATE_DIR": str(tmp_path / "state"),
|
|
"HOST112_INDEXER_RETRY_MARKER": str(tmp_path / "indexer.epoch"),
|
|
"HOST112_MANAGER_RETRY_MARKER": str(tmp_path / "manager.epoch"),
|
|
"HOST112_MANAGER_LOCK_FILE": str(tmp_path / "manager.lock"),
|
|
}
|
|
)
|
|
return env
|
|
|
|
|
|
def test_check_and_dry_run_are_real_no_write_execution_paths(tmp_path: Path) -> None:
|
|
identity = [
|
|
"--trace-id",
|
|
"trace:D037-host112-dry-run",
|
|
"--run-id",
|
|
"run:D037-host112-dry-run",
|
|
"--work-item-id",
|
|
"D037-WAZUH-HOST112",
|
|
]
|
|
env = isolated_env(tmp_path)
|
|
|
|
check = subprocess.run(
|
|
["bash", str(READINESS), "--check", *identity],
|
|
text=True,
|
|
capture_output=True,
|
|
env=env,
|
|
check=False,
|
|
timeout=20,
|
|
)
|
|
dry_run = subprocess.run(
|
|
["bash", str(READINESS), "--dry-run", *identity],
|
|
text=True,
|
|
capture_output=True,
|
|
env=env,
|
|
check=False,
|
|
timeout=20,
|
|
)
|
|
|
|
assert check.returncode in {0, 1}
|
|
assert dry_run.returncode in {0, 1}
|
|
for result, mode in ((check, "check"), (dry_run, "dry_run")):
|
|
output = result.stdout + result.stderr
|
|
assert f"mode={mode}" in output
|
|
assert "trace_id=trace:D037-host112-dry-run" in output
|
|
assert "run_id=run:D037-host112-dry-run" in output
|
|
assert "work_item_id=D037-WAZUH-HOST112" in output
|
|
assert "runtime_write_performed=0" in output
|
|
assert "manager_runtime_write_performed=0" in output
|
|
assert "receipt_write_performed=0" in output
|
|
assert "manager_preflight_status=" in output
|
|
assert "apply_phase_timeout_budget_seconds=370" in output
|
|
assert "executor_deadline_seconds=420" in output
|
|
assert "durable_receipt_present=0" in output
|
|
assert not (tmp_path / "state").exists()
|
|
assert not (tmp_path / "indexer.epoch").exists()
|
|
assert not (tmp_path / "manager.epoch").exists()
|
|
assert not (tmp_path / "manager.lock").exists()
|
|
|
|
|
|
def test_unsafe_or_incomplete_identity_fails_before_any_state_path(tmp_path: Path) -> None:
|
|
env = isolated_env(tmp_path)
|
|
unsafe = subprocess.run(
|
|
[
|
|
"bash",
|
|
str(READINESS),
|
|
"--dry-run",
|
|
"--trace-id",
|
|
"unsafe value",
|
|
"--run-id",
|
|
"run-safe",
|
|
"--work-item-id",
|
|
"D037-WAZUH-HOST112",
|
|
],
|
|
text=True,
|
|
capture_output=True,
|
|
env=env,
|
|
check=False,
|
|
timeout=10,
|
|
)
|
|
incomplete = subprocess.run(
|
|
[
|
|
"bash",
|
|
str(READINESS),
|
|
"--dry-run",
|
|
"--trace-id",
|
|
"trace-safe",
|
|
],
|
|
text=True,
|
|
capture_output=True,
|
|
env=env,
|
|
check=False,
|
|
timeout=10,
|
|
)
|
|
|
|
assert unsafe.returncode == 64
|
|
assert "BLOCKER unsafe_control_identity" in (unsafe.stdout + unsafe.stderr)
|
|
assert incomplete.returncode == 64
|
|
assert "BLOCKER incomplete_control_identity" in (
|
|
incomplete.stdout + incomplete.stderr
|
|
)
|
|
assert not any(tmp_path.iterdir())
|
|
|
|
|
|
def test_manager_preflight_verifier_receipt_and_rollback_are_complete() -> None:
|
|
source = read(READINESS)
|
|
|
|
for field in (
|
|
"manager_failed_state_observed",
|
|
"manager_orphan_analysisd",
|
|
"manager_memory_available_kb",
|
|
"manager_disk_available_kb",
|
|
"manager_disk_used_percent",
|
|
"wazuh_analysisd_process_count",
|
|
"wazuh_agent_event_port_1514_tcp",
|
|
"wazuh_agent_event_port_1514_udp",
|
|
"wazuh_agent_enrollment_port_1515_tcp",
|
|
"wazuh_manager_api_port_55000_tcp",
|
|
"manager_independent_verifier_ready",
|
|
):
|
|
assert field in source
|
|
assert 'manager_active_state="$(unit_property wazuh-manager.service ActiveState)"' in source
|
|
assert 'manager_result="$(unit_property wazuh-manager.service Result)"' in source
|
|
assert 'analysisd_count="$(process_count wazuh-analysisd)"' in source
|
|
assert source.count(
|
|
'timeout "$MANAGER_START_TIMEOUT_SECONDS" systemctl start wazuh-manager.service'
|
|
) == 1
|
|
assert "manager_attempt_count=1" in source
|
|
assert 'if [ "$manager_start_exit" = "0" ]; then' in source
|
|
assert 'manager_terminal="manager_start_nonzero_verifier_healthy_unattributed"' in source
|
|
assert 'timeout "$MANAGER_ROLLBACK_TIMEOUT_SECONDS" systemctl stop wazuh-manager.service' in source
|
|
assert 'rollback_terminal="bounded_stop_verified"' in source
|
|
assert "schema_version=host112_wazuh_manager_recovery_receipt_v2" in source
|
|
assert "boot_id=$boot_id" in source
|
|
assert "trace_id=$TRACE_ID run_id=$RUN_ID work_item_id=$WORK_ITEM_ID" in source
|
|
assert 'receipt_status="write_failed"' in source
|
|
assert "prohibited_actions=host_reboot,vm_power_change,vm_reset,firewall_change,secret_read,database_write" in source
|
|
|
|
preflight = source[
|
|
source.index("evaluate_manager_preflight() {") : source.index(
|
|
"wait_for_manager_verifier() {"
|
|
)
|
|
]
|
|
assert 'elif [ "$(unit_active wazuh-indexer.service)" != "active" ]; then' in preflight
|
|
assert 'manager_preflight_status="blocked_indexer_dependency_no_write"' in preflight
|
|
|
|
|
|
def test_cooldown_marker_write_is_atomic_and_fails_closed_before_start() -> None:
|
|
source = read(READINESS)
|
|
marker_function = source[
|
|
source.index("write_epoch_marker() {") : source.index("unit_active() {")
|
|
]
|
|
assert 'tmp="${marker}.tmp"' in marker_function
|
|
assert 'printf \'%s\\n\' "$epoch" >"$tmp"' in marker_function
|
|
assert 'chmod 0644 "$tmp"' in marker_function
|
|
assert 'mv "$tmp" "$marker"' in marker_function
|
|
assert "return 1" in marker_function
|
|
|
|
manager_start = source.index(
|
|
' if write_epoch_marker "$MANAGER_RETRY_MARKER" "$now_epoch"; then'
|
|
)
|
|
manager_end = source.index(" ;;", manager_start)
|
|
manager_branch = source[manager_start:manager_end]
|
|
marker_failure_at = manager_branch.index(
|
|
' else\n record_action "wazuh_manager_marker_write_failed_no_write"'
|
|
)
|
|
success = manager_branch[:marker_failure_at]
|
|
failure = manager_branch[marker_failure_at:]
|
|
marker_success_at = success.index(
|
|
' manager_marker_write_performed=1'
|
|
)
|
|
reset_at = success.index("systemctl reset-failed wazuh-manager.service")
|
|
assert marker_success_at < reset_at
|
|
assert "runtime_write_performed=1" in success[:reset_at]
|
|
assert "state_write_performed=1" in success[:reset_at]
|
|
assert "systemctl reset-failed wazuh-manager.service" in success
|
|
assert "systemctl start wazuh-manager.service" in success
|
|
assert 'bounded_timeout "$MANAGER_RESET_TIMEOUT_SECONDS" systemctl reset-failed wazuh-manager.service' in success
|
|
assert "manager_attempt_count=1" in success
|
|
assert "systemctl reset-failed wazuh-manager.service" not in failure
|
|
assert "systemctl start wazuh-manager.service" not in failure
|
|
assert "wazuh_manager_marker_write_failed_no_write" in failure
|
|
assert 'manager_terminal="no_write_cooldown_marker_write_failed"' in failure
|
|
assert "manager_marker_write_performed=1" not in failure
|
|
|
|
assert "state_write_performed=$state_write_performed" in source
|
|
assert "manager_marker_write_performed=$manager_marker_write_performed" in source
|
|
|
|
indexer_start = source.index(
|
|
' if write_epoch_marker "$INDEXER_RETRY_MARKER" "$now_epoch"; then'
|
|
)
|
|
indexer_end = source.index(" else\n record_action", indexer_start)
|
|
indexer_branch = source[indexer_start:indexer_end]
|
|
indexer_success, indexer_failure = indexer_branch.split(" else\n", maxsplit=1)
|
|
assert "systemctl reset-failed wazuh-indexer.service" in indexer_success
|
|
assert "systemctl start wazuh-indexer.service" in indexer_success
|
|
assert "systemctl reset-failed wazuh-indexer.service" not in indexer_failure
|
|
assert "systemctl start wazuh-indexer.service" not in indexer_failure
|
|
assert "wazuh_indexer_marker_write_failed_no_write" in indexer_failure
|
|
|
|
|
|
def test_phase_budget_and_systemd_outer_timeout_are_aligned() -> None:
|
|
source = read(READINESS)
|
|
service = read(SERVICE)
|
|
control = read(CONTROL)
|
|
|
|
assert 'SERVICE_START_TIMEOUT_SECONDS="${HOST112_SERVICE_START_TIMEOUT_SECONDS:-15}"' in source
|
|
assert 'INDEXER_START_TIMEOUT_SECONDS="${HOST112_INDEXER_START_TIMEOUT_SECONDS:-90}"' in source
|
|
assert 'MANAGER_START_TIMEOUT_SECONDS="${HOST112_MANAGER_START_TIMEOUT_SECONDS:-90}"' in source
|
|
assert 'MANAGER_RESET_TIMEOUT_SECONDS="${HOST112_MANAGER_RESET_TIMEOUT_SECONDS:-5}"' in source
|
|
assert 'MANAGER_VERIFY_TIMEOUT_SECONDS="${HOST112_MANAGER_VERIFY_TIMEOUT_SECONDS:-30}"' in source
|
|
assert 'MANAGER_ROLLBACK_TIMEOUT_SECONDS="${HOST112_MANAGER_ROLLBACK_TIMEOUT_SECONDS:-30}"' in source
|
|
assert 'EXECUTOR_DEADLINE_SECONDS="${HOST112_EXECUTOR_DEADLINE_SECONDS:-420}"' in source
|
|
assert 'if [ "$APPLY_PHASE_TIMEOUT_BUDGET_SECONDS" -gt "$EXECUTOR_DEADLINE_SECONDS" ]; then' in source
|
|
assert 'if [ "$MANAGER_VERIFY_INTERVAL_SECONDS" -gt "$MANAGER_VERIFY_TIMEOUT_SECONDS" ]; then' in source
|
|
assert "TimeoutStartSec=480" in service
|
|
assert "$script:Agent99Host112ExecutorBudgetSeconds = 420" in control
|
|
assert "$script:Agent99Host112ApplyTransportTimeoutSeconds = 510" in control
|
|
assert "Invoke-AgentHost112SshText $applyCommand $script:Agent99Host112ApplyTransportTimeoutSeconds 1" in control
|
|
assert "executorReserveSeconds -ge 60" in control
|
|
assert "queueReserveSeconds -ge 300" in control
|
|
assert "clientReserveSeconds -ge 120" in control
|
|
assert '$process.WaitForExit($script:Agent99QueueOuterDeadlineSeconds * 1000)' in control
|
|
|
|
|
|
def test_receipts_and_readbacks_are_immutable_boot_bound_and_replay_safe() -> None:
|
|
source = read(READINESS)
|
|
|
|
assert "receipt_name_for_identity()" in source
|
|
assert "printf '%s\\0%s\\0%s\\0'" in source
|
|
assert 'boot_id=$boot_id' in source
|
|
assert 'ln "$tmp" "$receipt_path"' in source
|
|
receipt_writer = source[
|
|
source.index("write_public_receipt() {") : source.index(
|
|
"write_signal_readback() {",
|
|
)
|
|
]
|
|
assert 'if [ "$MODE" = "timer_apply" ]' in receipt_writer
|
|
assert 'mv "$tmp" "$receipt_path"' in receipt_writer
|
|
assert 'elif [ "$MODE" != "timer_apply" ]' in receipt_writer
|
|
assert 'ln "$tmp" "$receipt_path"' in receipt_writer
|
|
assert 'receipt_status="reused_immutable"' in source
|
|
assert 'manager_terminal="immutable_receipt_replay"' in source
|
|
assert "immutable_receipt_identity_or_boot_mismatch" in source
|
|
assert "durable_receipt_terminal=$durable_receipt_terminal" in source
|
|
assert 'ln "$tmp" "$durable_readback_path"' in source
|
|
assert "durable_readback_sha256=%s" in source
|
|
assert "latest_readback_pointer_written=%s" in source
|
|
|
|
|
|
def test_check_reads_exact_immutable_receipt_and_rejects_boot_mismatch(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
trace_id = "trace-D037-receipt"
|
|
run_id = "run-D037-receipt"
|
|
work_item_id = "D037-WAZUH-HOST112"
|
|
digest = hashlib.sha256(
|
|
f"{trace_id}\0{run_id}\0{work_item_id}\0".encode()
|
|
).hexdigest()
|
|
receipt_dir = tmp_path / "state" / "receipts"
|
|
receipt_dir.mkdir(parents=True)
|
|
receipt = receipt_dir / f"{run_id}-{digest}.txt"
|
|
receipt.write_text(
|
|
"schema_version=host112_wazuh_manager_recovery_receipt_v2 "
|
|
f"trace_id={trace_id} run_id={run_id} work_item_id={work_item_id} "
|
|
"boot_id=unknown terminal=verified_healthy\n",
|
|
encoding="utf-8",
|
|
)
|
|
readback_dir = tmp_path / "state" / "readbacks"
|
|
readback_dir.mkdir(parents=True)
|
|
durable_readback = readback_dir / f"{run_id}-{digest}.txt"
|
|
receipt_sha = hashlib.sha256(receipt.read_bytes()).hexdigest()
|
|
durable_readback.write_text(
|
|
"schema_version=host112_guest_recovery_v2 "
|
|
f"trace_id={trace_id} run_id={run_id} work_item_id={work_item_id} "
|
|
"boot_id=unknown durable_receipt_terminal=verified_healthy "
|
|
f"receipt_path={receipt} receipt_sha256={receipt_sha}\n",
|
|
encoding="utf-8",
|
|
)
|
|
original = receipt.read_bytes()
|
|
env = isolated_env(tmp_path)
|
|
args = [
|
|
"bash",
|
|
str(READINESS),
|
|
"--check",
|
|
"--trace-id",
|
|
trace_id,
|
|
"--run-id",
|
|
run_id,
|
|
"--work-item-id",
|
|
work_item_id,
|
|
]
|
|
|
|
matched = subprocess.run(
|
|
args, text=True, capture_output=True, env=env, check=False, timeout=20
|
|
)
|
|
matched_output = matched.stdout + matched.stderr
|
|
assert "durable_receipt_present=1" in matched_output
|
|
assert "durable_receipt_identity_match=1" in matched_output
|
|
assert "durable_receipt_boot_match=1" in matched_output
|
|
assert "durable_receipt_terminal=verified_healthy" in matched_output
|
|
assert "durable_readback_present=1" in matched_output
|
|
assert "durable_readback_receipt_link_match=1" in matched_output
|
|
assert "durable_readback_terminal_match=1" in matched_output
|
|
assert receipt.read_bytes() == original
|
|
|
|
receipt.write_text(
|
|
receipt.read_text(encoding="utf-8").replace(
|
|
"boot_id=unknown", "boot_id=previous-boot"
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
mismatched = subprocess.run(
|
|
args, text=True, capture_output=True, env=env, check=False, timeout=20
|
|
)
|
|
assert mismatched.returncode == 65
|
|
assert "immutable_receipt_identity_or_boot_mismatch" in (
|
|
mismatched.stdout + mismatched.stderr
|
|
)
|
|
|
|
|
|
def test_executor_has_absolute_deadline_bounded_reads_and_signal_receipt() -> None:
|
|
source = read(READINESS)
|
|
|
|
assert "remaining_executor_seconds()" in source
|
|
assert "bounded_timeout()" in source
|
|
assert 'timeout --signal=TERM --kill-after=2 "$effective" "$@"' in source
|
|
assert "handle_executor_signal()" in source
|
|
assert "trap 'handle_executor_signal TERM' TERM" in source
|
|
assert "trap 'handle_executor_signal INT' INT" in source
|
|
assert "trap 'handle_executor_signal HUP' HUP" in source
|
|
assert "if write_public_receipt && write_signal_readback; then" in source
|
|
assert "action_count=${#actions[@]} actions=$receipt_action_text" in source
|
|
assert "schema_version=host112_guest_recovery_signal_readback_v1" in source
|
|
assert 'ln "$tmp" "$signal_readback_path"' in source
|
|
assert "signal_readback_status=\"written_immutable\"" in source
|
|
assert 'bounded_timeout "$READ_TIMEOUT_SECONDS" systemctl show' in source
|
|
assert 'bounded_timeout "$READ_TIMEOUT_SECONDS" ss -H -ltn' in source
|
|
assert 'bounded_timeout "$READ_TIMEOUT_SECONDS" df -Pk' in source
|
|
|
|
|
|
def test_timer_noop_uses_one_bounded_aggregate_instead_of_per_minute_artifacts() -> None:
|
|
source = read(READINESS)
|
|
function = source[
|
|
source.index("write_bounded_timer_observation() {") : source.index(
|
|
'boot_id="$(cat /proc/sys/kernel/random/boot_id',
|
|
)
|
|
]
|
|
|
|
assert 'timer_observation_path="$STATE_DIR/timer-observation.latest"' in function
|
|
assert "schema_version=host112_timer_observation_aggregate_v1" in function
|
|
assert "aggregation=fixed_single_atomic_pointer" in function
|
|
assert 'mv "$tmp" "$timer_observation_path"' in function
|
|
assert "repeat_count=$timer_observation_repeat_count" in function
|
|
assert 'if [ "$MODE" = "apply" ] || [ "$runtime_write_performed" -eq 1 ]; then' in source
|
|
assert 'receipt_status="timer_no_runtime_mutation_aggregated"' in source
|
|
assert 'durable_readback_status="skipped_bounded_timer_no_runtime_mutation"' in source
|
|
assert "timer_ledger_capacity=128" in source
|
|
assert "write_bounded_timer_mutation_readback()" in source
|
|
assert "retention=rolling_atomic_overwrite" in source
|
|
assert 'timer_artifact_policy="bounded_ring_128_for_runtime_mutation"' in source
|
|
assert 'timer_artifact_policy="bounded_mutable_observation_no_immutable_artifact"' in source
|
|
|
|
|
|
def test_apply_success_is_bound_to_same_run_durable_terminal() -> None:
|
|
source = read(READINESS)
|
|
terminal_gate = source[source.rindex('if [ "$MODE" = "apply" ]') :]
|
|
|
|
assert 'successful_terminal="$run_terminal"' in terminal_gate
|
|
assert 'successful_terminal="$durable_receipt_terminal"' in terminal_gate
|
|
assert '[ "$successful_terminal" = "verified_healthy" ]' in terminal_gate
|
|
assert '[ "$successful_terminal" = "idempotent_already_verified_healthy" ]' in terminal_gate
|
|
guest_verifier = source.index('guest_ready=0\nif [ "$console_ready" -eq 1 ]')
|
|
receipt_commit = source.index("if ! write_public_receipt; then", guest_verifier)
|
|
assert guest_verifier < receipt_commit
|
|
assert 'run_terminal="guest_verification_failed_${manager_terminal}"' in source
|
|
assert 'run_terminal="execution_failed_or_unattributed_${manager_terminal}"' in source
|
|
assert 'action_failure_observed=1' in source
|
|
assert "manager_terminal=$manager_terminal terminal=$run_terminal" in source
|
|
|
|
|
|
def test_general_guest_repair_includes_recovery_timer_independent_of_manager() -> None:
|
|
source = read(READINESS)
|
|
general_start = source.index(
|
|
' current_default="$(bounded_timeout "$READ_TIMEOUT_SECONDS"'
|
|
)
|
|
manager_start = source.index("\nfi\n\ncapture_manager_state", general_start)
|
|
general = source[general_start:manager_start]
|
|
|
|
assert "unit_loaded awoooi-host112-guest-recovery.timer" in general
|
|
assert (
|
|
"enable_if_disabled awoooi-host112-guest-recovery.timer "
|
|
"enable_recovery_timer"
|
|
) in general
|
|
assert (
|
|
"start_if_inactive awoooi-host112-guest-recovery.timer "
|
|
"start_recovery_timer"
|
|
) in general
|
|
assert 'record_action "recovery_timer_missing"' in general
|
|
|
|
|
|
def test_installer_uses_argument_regex_and_post_install_policy_readback() -> None:
|
|
source = read(INSTALLER)
|
|
|
|
assert "SUDOERS_ARGUMENT_REGEX='^--apply --trace-id " in source
|
|
assert "--run-id [A-Za-z0-9]" in source
|
|
assert "--work-item-id [A-Za-z0-9]" in source
|
|
assert '"$TARGET_USER" "$READINESS_TARGET" "$SUDOERS_ARGUMENT_REGEX"' in source
|
|
assert 'visudo -cf "$sudoers_tmp"' in source
|
|
assert "SUDO_CORRELATED_APPLY_NOPASSWD" in source
|
|
assert "SUDO_LEGACY_UNCORRELATED_APPLY_NOPASSWD" in source
|
|
assert "SUDO_UNSAFE_IDENTITY_NOPASSWD" in source
|
|
assert "--trace-id 'unsafe value'" in source
|
|
assert "--rollback" in source
|
|
|
|
|
|
def test_exact_sudoers_argument_regex_parses_when_visudo_is_available() -> None:
|
|
visudo = shutil.which("visudo")
|
|
if not visudo:
|
|
return
|
|
candidate = (
|
|
"kali ALL=(root) NOPASSWD: "
|
|
"/usr/local/sbin/awoooi-host112-guest-readiness "
|
|
"^--apply --trace-id [A-Za-z0-9][A-Za-z0-9._:@+-]{0,127} "
|
|
"--run-id [A-Za-z0-9][A-Za-z0-9._:@+-]{0,127} "
|
|
"--work-item-id [A-Za-z0-9][A-Za-z0-9._:@+-]{0,127}$\n"
|
|
)
|
|
result = subprocess.run(
|
|
[visudo, "-cf", "-"],
|
|
input=candidate,
|
|
text=True,
|
|
capture_output=True,
|
|
check=False,
|
|
timeout=10,
|
|
)
|
|
assert result.returncode == 0, result.stdout + result.stderr
|
|
|
|
|
|
def test_agent99_propagates_identity_and_requires_independent_receipt() -> None:
|
|
control = read(CONTROL)
|
|
bootstrap = read(BOOTSTRAP)
|
|
function = control[control.index("function Convert-AgentHost112GuestReadback") :]
|
|
function = function[: function.index("function Test-HostReachability")]
|
|
|
|
for parameter in ("AutomationRunId", "TraceId", "WorkItemId"):
|
|
assert f"[string]${parameter} = \"\"" in control
|
|
assert f'[string]`${parameter} = ""' in bootstrap
|
|
assert 'reason = "controlled_dispatch_identity_unsafe"' in control
|
|
assert 'applyBlockedReason = "control_identity_incomplete_or_unsafe"' in function
|
|
assert '$dryRun.managerPreflightStatus -eq "ready"' in function
|
|
assert 'receiptStatus -eq "written_immutable"' in function
|
|
assert '$after.managerVerified' in function
|
|
assert 'name = "wazuh_manager_service_active"' in function
|
|
assert 'name = "wazuh_analysisd_process_present"' in function
|
|
assert 'name = "wazuh_agent_event_1514_ready"' in function
|
|
assert 'name = "wazuh_agent_enrollment_1515_tcp"' in function
|
|
assert 'name = "wazuh_manager_api_55000_tcp"' in function
|
|
assert "host112_readback_transport_unavailable" in function
|
|
assert 'terminal = "no_write_control_identity_incomplete_or_unsafe"' in function
|
|
|
|
|
|
def test_agent99_manager_success_requires_apply_and_immutable_receipt_chain() -> None:
|
|
control = read(CONTROL)
|
|
function = control[control.index("function Invoke-AgentHost112GuestRecovery") :]
|
|
function = function[: function.index("function Test-HostReachability")]
|
|
|
|
assert "$applyTransport -and $applyTransport.ok" in function
|
|
assert '$applyReceipt.durableReceiptTerminal -eq "verified_healthy"' in function
|
|
assert "$applyReceipt.managerIndependentVerifierReady" in function
|
|
assert "$applyReceipt.managerRuntimeWritePerformed" in function
|
|
assert '[string]$applyReceipt.managerAttemptCount -eq "1"' in function
|
|
assert '[string]$applyReceipt.managerStartExit -eq "0"' in function
|
|
assert "$durableReceiptReadback.receiptPath -eq $applyReceipt.receiptPath" in function
|
|
assert "$durableReceiptReadback.receiptSha256 -eq $applyReceipt.receiptSha256" in function
|
|
assert "$durableReceiptReadback.durableReceiptTerminal -eq $applyReceipt.durableReceiptTerminal" in function
|
|
assert '$durableReceiptReadback.durableReceiptTerminal -eq "verified_healthy"' in function
|
|
assert "$applyReceipt.durableReadbackReceiptLinkMatch" in function
|
|
assert "$applyReceipt.durableReadbackTerminalMatch" in function
|
|
assert "$durableReceiptReadback.durableReadbackReceiptLinkMatch" in function
|
|
assert "$durableReceiptReadback.durableReadbackTerminalMatch" in function
|
|
assert "$before.durableReadbackReceiptLinkMatch" in function
|
|
assert "$before.durableReadbackTerminalMatch" in function
|
|
assert "$afterManagerVerifierVerified" in function
|
|
assert 'afterVerifierRole = "independent_second_verifier_only"' in function
|
|
assert "$after.ready -and" in function
|
|
assert "$managerApplyVerified" in function
|
|
assert "existingReceiptTerminalAccepted" in function
|
|
assert "immutable_receipt_replay_not_successful" in function
|
|
assert "host112_failed_apply_cannot_be_washed_green" in read(
|
|
ROOT / "agent99-synthetic-tests.ps1"
|
|
)
|
|
|
|
|
|
def test_agent99_uses_correlated_sudo_dry_run_over_canonical_direct_key() -> None:
|
|
control = read(CONTROL)
|
|
|
|
assert "function Invoke-AgentHost112SshText" in control
|
|
assert 'expectedUser = "kali"' in control
|
|
assert 'route = "direct_host112_kali"' in control
|
|
assert "Invoke-SshText -TargetHost $transportContract.targetHost" in control
|
|
assert "--dry-run --trace-id {0} --run-id {1} --work-item-id {2}" in control
|
|
assert "Invoke-AgentHost112SshText $dryRunCommand" in control
|
|
assert "$dryRun.identityMatches" in control
|
|
assert "$dryRun.managerPreflightPresent" in control
|
|
assert "$dryRun.transportOk" in control
|
|
assert "-not $dryRun.durableReceiptPresent" in control
|
|
|
|
|
|
def test_unhealthy_executor_exit_one_is_delivered_readback_not_transport_failure() -> None:
|
|
control = read(CONTROL)
|
|
function = control[control.index("function Invoke-AgentHost112GuestRecovery") :]
|
|
function = function[: function.index("function Test-HostReachability")]
|
|
synthetic = read(ROOT / "agent99-synthetic-tests.ps1")
|
|
|
|
assert function.count('case "$executor_exit" in 0|1) exit 0') == 3
|
|
assert "$before.executorExitCode -in @(\"0\", \"1\")" in function
|
|
assert "$before.readbackPresent -and" in function
|
|
assert "$before.identityMatches -and" in function
|
|
assert '$beforeTransport = Invoke-AgentHost112SshText $checkCommand' in function
|
|
assert 'New-Host112SyntheticRow "check" 0 0 "none" 0 "not_requested" 0 "unknown" 1' in synthetic
|
|
assert "host112_general_candidate_not_blocked_by_healthy_manager" in synthetic
|
|
|
|
|
|
def test_general_guest_candidate_is_not_blocked_by_manager_candidate() -> None:
|
|
control = read(CONTROL)
|
|
function = control[control.index("function Invoke-AgentHost112GuestRecovery") :]
|
|
function = function[: function.index("function Test-HostReachability")]
|
|
synthetic = read(ROOT / "agent99-synthetic-tests.ps1")
|
|
|
|
assert "$generalCandidateRequired = [bool]($beforeVerified -and -not $before.generalReady)" in function
|
|
assert "$managerCandidateRequired = [bool]($beforeVerified -and -not $before.managerVerified)" in function
|
|
assert "$generalApplyEligible = [bool]($generalCandidateRequired -and $generalDryRunVerified)" in function
|
|
assert "$managerApplyEligible = [bool]($managerCandidateRequired -and $managerDryRunVerified)" in function
|
|
assert "$applyEligible = [bool]($generalApplyEligible -or $managerApplyEligible)" in function
|
|
assert "managerNoAttemptPreserved" in function
|
|
assert "host112_general_candidate_not_blocked_by_healthy_manager" in synthetic
|
|
assert '-not $generalOnly.managerCandidateRequired' in synthetic
|
|
assert '[string]$generalOnly.applyReceipt.managerAttemptCount -eq "0"' in synthetic
|
|
assert '-not $generalOnly.applyReceipt.managerRuntimeWritePerformed' in synthetic
|
|
|
|
|
|
def test_deploy_and_bootstrap_canonicalize_host112_direct_transport() -> None:
|
|
deploy = read(ROOT / "agent99-deploy.ps1")
|
|
bootstrap = read(BOOTSTRAP)
|
|
generic_config = read(ROOT / "agent99.config.example.json")
|
|
host_config = read(ROOT / "agent99.config.99.example.json")
|
|
|
|
assert 'Set-AgentProperty $config.sshUsers "192.168.0.112" "kali"' in deploy
|
|
assert 'name = "host112_canonical_direct_route"' in deploy
|
|
assert 'Host112 canonical direct SSH config post-verifier failed.' in deploy
|
|
assert "Ensure-AgentHost112CanonicalSshConfig" in bootstrap
|
|
assert 'Set-AgentBootstrapProperty $config.sshUsers "192.168.0.112" "kali"' in bootstrap
|
|
assert 'Host112 canonical direct SSH config bootstrap verifier failed.' in bootstrap
|
|
for config in (generic_config, host_config):
|
|
assert '"192.168.0.112": "kali"' in config
|
|
assert '"directHosts"' in config
|
|
assert '"192.168.0.112"' in config
|