fix(reboot): surface windows99 collector readback
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 57s
CD Pipeline / post-deploy-checks (push) Has been cancelled
CD Pipeline / build-and-deploy (push) Has been cancelled

This commit is contained in:
Your Name
2026-07-03 07:30:45 +08:00
parent 374d4e491c
commit b338a7ab54
8 changed files with 605 additions and 2 deletions

View File

@@ -306,6 +306,9 @@ fi
if [ -s "$windows99_management_file" ]; then
scorecard_args+=(--windows99-management-file "$windows99_management_file")
fi
if [ -s "$windows99_vmware_file" ]; then
scorecard_args+=(--windows99-vmware-collector-file "$windows99_vmware_file")
fi
if [ -s "$windows99_vmware_file" ] && grep -q '^AWOOOI_WINDOWS99_VMWARE_AUTOSTART=1$' "$windows99_vmware_file"; then
scorecard_args+=(--windows99-vmware-file "$windows99_vmware_file")
fi

View File

@@ -54,6 +54,11 @@ def parse_args() -> argparse.Namespace:
type=Path,
help="Optional windows99-vmware-autostart.ps1 Verify output.",
)
parser.add_argument(
"--windows99-vmware-collector-file",
type=Path,
help="Optional collect-windows99-vmware-verify.sh check/collect output.",
)
parser.add_argument(
"--windows99-management-file",
type=Path,
@@ -351,6 +356,103 @@ def parse_windows99_management_readback(path: Path | None) -> dict[str, Any]:
}
def parse_windows99_vmware_collector_readback(text: str) -> dict[str, Any]:
"""Parse no-secret Windows99 VMware collector check/collect output."""
default = {
"schema_version": "windows99_vmware_verify_collector_v1",
"readback_present": False,
"status": "missing",
"ssh_batchmode_auth_ready": False,
"safe_next_step": "run_no_secret_collector_check_or_console_validator",
"blockers": [],
"forbidden_actions": [
"windows_password_or_secret_collection",
"host_reboot",
"vm_power_change",
"windows_update_policy_apply",
],
}
if not text.strip():
return default
kv = parse_kv(text)
schema = str(kv.get("schema_version") or "")
if schema != "windows99_vmware_verify_collector_v1":
invalid = dict(default)
invalid.update(
{
"readback_present": True,
"schema_version": schema or "unknown",
"status": "blocked_invalid_windows99_vmware_collector_readback",
"blockers": ["windows99_no_secret_collector_readback_invalid"],
}
)
return invalid
status = str(kv.get("verify_collection_status") or "unknown")
ssh_ready = truthy(kv.get("ssh_batchmode_auth_ready"))
collector_blockers_by_status = {
"blocked_ssh_publickey_auth_missing": "windows99_ssh_publickey_auth_missing",
"blocked_ssh_port_closed": "windows99_ssh_port_closed",
"blocked_local_verify_script_missing": "windows99_local_verify_script_missing",
"blocked_remote_verify_output_invalid": "windows99_remote_verify_output_invalid",
"blocked_remote_verify_command_failed": "windows99_remote_verify_command_failed",
}
blockers: list[str] = []
if status.startswith("blocked_"):
blockers.append(
collector_blockers_by_status.get(
status,
"windows99_no_secret_collector_not_ready",
)
)
return {
"schema_version": schema,
"readback_present": True,
"status": status,
"dry_run": str(kv.get("dry_run") or "unknown"),
"target_host": str(kv.get("target_host") or "192.168.0.99"),
"target_host_alias": str(kv.get("target_host_alias") or "99"),
"port_22_open": truthy(kv.get("port_22_open")),
"port_3389_open": truthy(kv.get("port_3389_open")),
"port_5985_open": truthy(kv.get("port_5985_open")),
"port_5986_open": truthy(kv.get("port_5986_open")),
"port_9182_open": truthy(kv.get("port_9182_open")),
"ssh_candidate_users": csv_strings(kv.get("ssh_candidate_users")),
"ssh_auth_probed_users": int_value(kv.get("ssh_auth_probed_users")),
"ssh_batchmode_auth_ready": ssh_ready,
"ssh_authenticated_user": str(kv.get("ssh_authenticated_user") or ""),
"ssh_auth_probe_exit_status": str(
kv.get("ssh_auth_probe_exit_status") or "unknown"
),
"remote_verify_attempted": truthy(kv.get("remote_verify_attempted")),
"remote_verify_exit_status": str(
kv.get("remote_verify_exit_status") or "unknown"
),
"local_verify_script_present": truthy(kv.get("local_verify_script_present")),
"safe_next_step": str(kv.get("safe_next_step") or ""),
"blockers": blockers,
"secret_value_read": truthy(kv.get("secret_value_read")),
"password_prompt_allowed": truthy(kv.get("password_prompt_allowed")),
"remote_write_performed": truthy(kv.get("remote_write_performed")),
"host_reboot_performed": truthy(kv.get("host_reboot_performed")),
"vm_power_change_performed": truthy(kv.get("vm_power_change_performed")),
"windows_update_policy_apply_performed": truthy(
kv.get("windows_update_policy_apply_performed")
),
"forbidden_actions": [
"windows_password_or_secret_collection",
"host_reboot",
"vm_power_change",
"windows_update_policy_apply",
"manual_registry_edit",
"service_restart",
"github_api",
],
}
def read_json_object(path: Path | None) -> dict[str, Any]:
if not path:
return {}
@@ -907,13 +1009,27 @@ def build_windows99_verify_collection_packet(
*,
windows99: dict[str, Any],
host_boot_detection: dict[str, Any],
windows99_management: dict[str, Any] | None = None,
windows99_collector: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""Describe the next no-secret Windows 99 verifier collection step."""
windows99_management = windows99_management or {}
windows99_collector = windows99_collector or {}
host99 = host_row_by_alias(host_boot_detection, "99")
host99_reachable = host99.get("reachable") is True
host99_uptime_known = int_value(host99.get("uptime_seconds"), -1) >= 0
readback_present = windows99.get("readback_present") is True
verify_ready = windows99.get("verify_ready") is True
local_console_reachable = (
windows99_management.get("local_console_channel_reachable") is True
)
console_channels = strings(windows99_management.get("console_collection_channels"))
collector_present = windows99_collector.get("readback_present") is True
collector_status = str(windows99_collector.get("status") or "unknown")
collector_ssh_ready = (
windows99_collector.get("ssh_batchmode_auth_ready") is True
or collector_status == "collected_windows99_vmware_verify_stdout"
)
blockers = strings(windows99.get("blockers"))
collection_blockers: list[str] = []
if not host99_reachable:
@@ -923,6 +1039,11 @@ def build_windows99_verify_collection_packet(
collection_blockers.extend(
blocker for blocker in blockers if blocker not in collection_blockers
)
collection_blockers.extend(
blocker
for blocker in strings(windows99_collector.get("blockers"))
if blocker not in collection_blockers
)
if not host99_uptime_known:
collection_blockers.append("windows99_uptime_unknown")
@@ -933,6 +1054,14 @@ def build_windows99_verify_collection_packet(
if host99_reachable and not readback_present
else "blocked_windows99_verify_collection_not_ready"
)
available_channels: list[str] = []
if local_console_reachable:
available_channels.extend(console_channels or ["local_console"])
if collector_ssh_ready:
available_channels.append("no_secret_ssh_batchmode_verify_collector")
if readback_present:
available_channels.append("committed_no_secret_artifact_file")
available_channels = unique_strings(available_channels)
return {
"schema_version": "windows99_vmware_verify_collection_packet_v1",
"status": status,
@@ -942,7 +1071,29 @@ def build_windows99_verify_collection_packet(
"host99_uptime_known": host99_uptime_known,
"readback_present": readback_present,
"verify_ready": verify_ready,
"can_collect_no_secret_verify": host99_reachable and not verify_ready,
"can_collect_no_secret_verify": (
host99_reachable
and not verify_ready
and (
local_console_reachable
or collector_ssh_ready
or not collector_present
)
),
"available_collection_channels": available_channels,
"no_secret_collector_readback_present": collector_present,
"no_secret_collector_status": collector_status,
"no_secret_collector_safe_next_step": str(
windows99_collector.get("safe_next_step") or ""
),
"no_secret_collector_ssh_batchmode_auth_ready": collector_ssh_ready,
"no_secret_collector_port_22_open": (
windows99_collector.get("port_22_open") is True
),
"no_secret_collector_remote_verify_attempted": (
windows99_collector.get("remote_verify_attempted") is True
),
"no_secret_collector": windows99_collector,
"required_vm_aliases": strings(windows99.get("required_vm_aliases"))
or sorted(WINDOWS99_REQUIRED_VM_ALIASES),
"expected_no_secret_output_fields": [
@@ -1104,6 +1255,9 @@ def build_required_checks(payload: dict[str, Any]) -> dict[str, bool]:
windows99_management = payload.get("windows99_management_channel")
if not isinstance(windows99_management, dict):
windows99_management = {}
windows99_collector = payload.get("windows99_vmware_verify_collector")
if not isinstance(windows99_collector, dict):
windows99_collector = {}
ssh_batch = windows99_management.get("ssh_batch")
if not isinstance(ssh_batch, dict):
ssh_batch = {}
@@ -1548,6 +1702,9 @@ def enrich_machine_readback(payload: dict[str, Any]) -> dict[str, Any]:
windows99_management = payload.get("windows99_management_channel")
if not isinstance(windows99_management, dict):
windows99_management = {}
windows99_collector = payload.get("windows99_vmware_verify_collector")
if not isinstance(windows99_collector, dict):
windows99_collector = {}
ssh_batch = windows99_management.get("ssh_batch")
if not isinstance(ssh_batch, dict):
ssh_batch = {}
@@ -1587,6 +1744,8 @@ def enrich_machine_readback(payload: dict[str, Any]) -> dict[str, Any]:
windows99_verify_collection = build_windows99_verify_collection_packet(
windows99=windows99,
host_boot_detection=host_boot_detection,
windows99_management=windows99_management,
windows99_collector=windows99_collector,
)
rollups = {
@@ -1694,6 +1853,36 @@ def enrich_machine_readback(payload: dict[str, Any]) -> dict[str, Any]:
"windows99_verify_collection_blocker_count": len(
strings(windows99_verify_collection.get("collection_blockers"))
),
"windows99_no_secret_collector_readback_present": (
windows99_verify_collection.get("no_secret_collector_readback_present")
is True
),
"windows99_no_secret_collector_status": str(
windows99_verify_collection.get("no_secret_collector_status") or "unknown"
),
"windows99_no_secret_collector_ssh_batchmode_auth_ready": (
windows99_verify_collection.get(
"no_secret_collector_ssh_batchmode_auth_ready"
)
is True
),
"windows99_no_secret_collector_port_22_open": (
windows99_verify_collection.get("no_secret_collector_port_22_open")
is True
),
"windows99_no_secret_collector_remote_verify_attempted": (
windows99_verify_collection.get(
"no_secret_collector_remote_verify_attempted"
)
is True
),
"windows99_no_secret_collector_safe_next_step": str(
windows99_verify_collection.get("no_secret_collector_safe_next_step")
or ""
),
"windows99_available_collection_channels": strings(
windows99_verify_collection.get("available_collection_channels")
),
"windows99_host99_reachable": (
windows99_verify_collection["host99_reachable"] is True
),
@@ -1764,6 +1953,21 @@ def enrich_machine_readback(payload: dict[str, Any]) -> dict[str, Any]:
"windows99_verify_collection_can_collect_no_secret": rollups[
"windows99_verify_collection_can_collect_no_secret"
],
"windows99_no_secret_collector_readback_present": rollups[
"windows99_no_secret_collector_readback_present"
],
"windows99_no_secret_collector_status": rollups[
"windows99_no_secret_collector_status"
],
"windows99_no_secret_collector_ssh_batchmode_auth_ready": rollups[
"windows99_no_secret_collector_ssh_batchmode_auth_ready"
],
"windows99_no_secret_collector_safe_next_step": rollups[
"windows99_no_secret_collector_safe_next_step"
],
"windows99_available_collection_channels": rollups[
"windows99_available_collection_channels"
],
"windows99_remote_execution_channel_ready": rollups[
"windows99_remote_execution_channel_ready"
],
@@ -1837,6 +2041,12 @@ def enrich_machine_readback(payload: dict[str, Any]) -> dict[str, Any]:
"reboot_auto_recovery_windows99_verify_collection_can_collect_no_secret": (
rollups["windows99_verify_collection_can_collect_no_secret"]
),
"reboot_auto_recovery_windows99_no_secret_collector_status": rollups[
"windows99_no_secret_collector_status"
],
"reboot_auto_recovery_windows99_no_secret_collector_ssh_batchmode_auth_ready": (
rollups["windows99_no_secret_collector_ssh_batchmode_auth_ready"]
),
"secret_values_collected": False,
"github_api_used": False,
"workflow_trigger_performed": False,
@@ -1878,6 +2088,9 @@ def build_scorecard(args: argparse.Namespace) -> dict[str, Any]:
windows99_management = parse_windows99_management_readback(
args.windows99_management_file
)
windows99_collector = parse_windows99_vmware_collector_readback(
read_text(args.windows99_vmware_collector_file)
)
controls = source_controls()
free_gib = disk_free_gib(args.disk_path)
@@ -2051,6 +2264,7 @@ def build_scorecard(args: argparse.Namespace) -> dict[str, Any]:
"public_maintenance_fallback": public_maintenance,
"windows99_vmware_autostart": windows99,
"windows99_management_channel": windows99_management,
"windows99_vmware_verify_collector": windows99_collector,
"capacity": {
"checked": free_gib is not None,
"free_gib": round(free_gib, 3) if free_gib is not None else None,

View File

@@ -172,6 +172,43 @@ WINDOWS99_MANAGEMENT_BLOCKED = {
}
WINDOWS99_COLLECTOR_PUBLICKEY_BLOCKED = """\
schema_version=windows99_vmware_verify_collector_v1
dry_run=true
target_host=192.168.0.99
target_host_alias=99
connect_timeout_seconds=3
ssh_timeout_seconds=3
remote_verify_timeout_seconds=45
port_timeout_wrapper=timeout
ssh_auth_probe_user_limit=2
ssh_timeout_wrapper=timeout
port_22_open=1
port_3389_open=1
port_5985_open=0
port_5986_open=0
port_9182_open=0
ssh_candidate_users=ogt,wooo,ooo,administrator,Administrator
ssh_auth_probed_users=2
ssh_batchmode_auth_ready=0
ssh_authenticated_user=
ssh_auth_probe_exit_status=255
ssh_auth_probe_stdout_present=0
remote_verify_mode=in_memory_stdin_scriptblock
local_verify_script_present=1
remote_verify_attempted=0
remote_verify_exit_status=not_attempted
verify_collection_status=blocked_ssh_publickey_auth_missing
safe_next_step=select_existing_authorized_public_key_user_or_set_WINDOWS99_SSH_USERS_then_rerun_collector_no_password
secret_value_read=false
password_prompt_allowed=false
remote_write_performed=false
host_reboot_performed=false
vm_power_change_performed=false
windows_update_policy_apply_performed=false
"""
def run_scorecard(
tmp_path: Path,
summary: str,
@@ -179,11 +216,13 @@ def run_scorecard(
windows99: str = WINDOWS99_VMWARE_GREEN,
public_maintenance: dict | None = PUBLIC_MAINTENANCE_GREEN,
windows99_management: str | None = None,
windows99_collector: str | None = None,
) -> dict:
summary_path = tmp_path / "summary.txt"
probe_path = tmp_path / "probe.txt"
reboot_event_path = tmp_path / "reboot-event.json"
windows99_path = tmp_path / "windows99-vmware.txt"
windows99_collector_path = tmp_path / "windows99-vmware-collector.txt"
public_maintenance_path = tmp_path / "public-maintenance.json"
summary_path.write_text(summary, encoding="utf-8")
probe_path.write_text(probe, encoding="utf-8")
@@ -213,6 +252,11 @@ def run_scorecard(
if windows99_management is not None:
windows99_management_path.write_text(windows99_management, encoding="utf-8")
args.extend(["--windows99-management-file", str(windows99_management_path)])
if windows99_collector is not None:
windows99_collector_path.write_text(windows99_collector, encoding="utf-8")
args.extend(
["--windows99-vmware-collector-file", str(windows99_collector_path)]
)
result = subprocess.run(
args,
text=True,
@@ -554,6 +598,55 @@ def test_windows99_management_channel_unavailable_is_visible(tmp_path: Path) ->
assert payload["readback"]["windows99_remote_execution_channel_ready"] is False
def test_windows99_no_secret_collector_publickey_blocker_is_visible(
tmp_path: Path,
) -> None:
payload = run_scorecard(
tmp_path,
GREEN_SUMMARY,
windows99="",
windows99_management=json.dumps(WINDOWS99_MANAGEMENT_BLOCKED),
windows99_collector=WINDOWS99_COLLECTOR_PUBLICKEY_BLOCKED,
)
collection = payload["windows99_verify_collection"]
assert collection["no_secret_collector_readback_present"] is True
assert collection["no_secret_collector_status"] == (
"blocked_ssh_publickey_auth_missing"
)
assert collection["no_secret_collector_ssh_batchmode_auth_ready"] is False
assert collection["no_secret_collector_port_22_open"] is True
assert collection["no_secret_collector_remote_verify_attempted"] is False
assert collection["no_secret_collector"]["ssh_auth_probed_users"] == 2
assert collection["no_secret_collector"]["ssh_candidate_users"] == [
"ogt",
"wooo",
"ooo",
"administrator",
"Administrator",
]
assert "windows99_ssh_publickey_auth_missing" in collection[
"collection_blockers"
]
assert collection["can_collect_no_secret_verify"] is True
assert collection["available_collection_channels"] == [
"rdp_console",
"hyperv_vmconnect",
]
assert payload["rollups"]["windows99_no_secret_collector_status"] == (
"blocked_ssh_publickey_auth_missing"
)
assert (
payload["rollups"][
"windows99_no_secret_collector_ssh_batchmode_auth_ready"
]
is False
)
assert payload["readback"]["windows99_no_secret_collector_status"] == (
"blocked_ssh_publickey_auth_missing"
)
def test_degraded_wazuh_and_old_boot_observation_block_slo(tmp_path: Path) -> None:
summary = GREEN_SUMMARY.replace("WAZUH_DASHBOARD_DEGRADED=0", "WAZUH_DASHBOARD_DEGRADED=1")
probe = HOST_PROBE_GREEN.replace("uptime_seconds=150", "uptime_seconds=900")