fix(reboot): accept windows99 console verify artifacts
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 1s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Failing after 2m6s
CD Pipeline / build-and-deploy (push) Has been skipped
CD Pipeline / post-deploy-checks (push) Has been skipped

This commit is contained in:
Your Name
2026-07-03 04:08:00 +08:00
parent 101259deb9
commit aeb436ba89
6 changed files with 323 additions and 0 deletions

View File

@@ -1853,6 +1853,11 @@ def _build_windows99_verify_collection_packet(
"no_secret_collector_collect_command": (
"bash scripts/reboot-recovery/collect-windows99-vmware-verify.sh --collect"
),
"no_secret_console_artifact_validate_command": (
"python3.11 scripts/reboot-recovery/"
"validate-windows99-console-verify-artifact.py --input "
"<console-stdout.txt> --output <windows99-vmware-verify.txt>"
),
"post_verifier": (
"rerun_reboot_auto_recovery_slo_scorecard_with_"
"windows99_vmware_file_no_secret_no_reboot"
@@ -1860,6 +1865,7 @@ def _build_windows99_verify_collection_packet(
"collection_blockers": collection_blockers,
"safe_collection_channels": [
"authorized_windows99_console_verify_stdout_only",
"validated_windows99_console_verify_artifact_file",
"existing_management_channel_verify_mode_only",
"no_secret_ssh_batchmode_verify_collector",
"committed_no_secret_artifact_file_then_scorecard_rerun",

View File

@@ -1027,6 +1027,13 @@ def _assert_reboot_slo_payload(payload: dict):
assert collection["no_secret_collector_collect_command"] == (
"bash scripts/reboot-recovery/collect-windows99-vmware-verify.sh --collect"
)
console_validate_command = collection["no_secret_console_artifact_validate_command"]
assert "validate-windows99-console-verify-artifact.py" in console_validate_command
assert "--input <console-stdout.txt>" in console_validate_command
assert "--output <windows99-vmware-verify.txt>" in console_validate_command
assert "validated_windows99_console_verify_artifact_file" in collection[
"safe_collection_channels"
]
assert (
"no_secret_ssh_batchmode_verify_collector"
in collection["safe_collection_channels"]

View File

@@ -967,6 +967,11 @@ def build_windows99_verify_collection_packet(
"no_secret_collector_collect_command": (
"bash scripts/reboot-recovery/collect-windows99-vmware-verify.sh --collect"
),
"no_secret_console_artifact_validate_command": (
"python3.11 scripts/reboot-recovery/"
"validate-windows99-console-verify-artifact.py --input "
"<console-stdout.txt> --output <windows99-vmware-verify.txt>"
),
"post_verifier": (
"rerun_reboot_auto_recovery_slo_scorecard_with_"
"windows99_vmware_file_no_secret_no_reboot"
@@ -974,6 +979,7 @@ def build_windows99_verify_collection_packet(
"collection_blockers": collection_blockers,
"safe_collection_channels": [
"authorized_windows99_console_verify_stdout_only",
"validated_windows99_console_verify_artifact_file",
"existing_management_channel_verify_mode_only",
"no_secret_ssh_batchmode_verify_collector",
"committed_no_secret_artifact_file_then_scorecard_rerun",

View File

@@ -466,6 +466,15 @@ def test_missing_windows99_vmware_readback_fails_closed(tmp_path: Path) -> None:
assert payload["windows99_verify_collection"][
"no_secret_collector_collect_command"
] == "bash scripts/reboot-recovery/collect-windows99-vmware-verify.sh --collect"
console_validate_command = payload["windows99_verify_collection"][
"no_secret_console_artifact_validate_command"
]
assert "validate-windows99-console-verify-artifact.py" in console_validate_command
assert "--input <console-stdout.txt>" in console_validate_command
assert "--output <windows99-vmware-verify.txt>" in console_validate_command
assert "validated_windows99_console_verify_artifact_file" in payload[
"windows99_verify_collection"
]["safe_collection_channels"]
assert "no_secret_ssh_batchmode_verify_collector" in payload[
"windows99_verify_collection"
]["safe_collection_channels"]

View File

@@ -0,0 +1,132 @@
from __future__ import annotations
import subprocess
import textwrap
from pathlib import Path
ROOT = Path(__file__).resolve().parents[3]
SCRIPT = (
ROOT
/ "scripts"
/ "reboot-recovery"
/ "validate-windows99-console-verify-artifact.py"
)
VALID_RED_VERIFY = """\
AWOOOI_WINDOWS99_VMWARE_AUTOSTART=1
MODE=Verify
VMRUN_PRESENT=1
HOST111_VMWARE_TARGET=explicit_or_required
VMX alias=111 path= present=0
VMX alias=188 path=D:\\Documents\\Virtual Machines\\188\\188.vmx present=1
VMX alias=120 path=D:\\Documents\\Virtual Machines\\120\\120.vmx present=1
VMX alias=121 path=D:\\Documents\\Virtual Machines\\121\\121.vmx present=1
VMX alias=112 path=D:\\Downloads\\kali\\kali.vmx present=1
VMWARE_SERVICE name=VMAuthdService present=1 status=Running startup_type=Auto ok=1
VMWARE_SERVICE name=VMnetDHCP present=1 status=Running startup_type=Auto ok=1
VMWARE_AUTOSTART_TASK name=AWOOOI-Start-VMware-VMs present=1 enabled=1 state=Ready trigger_count=1 action_count=1 ok=1
WINDOWS_UPDATE_POLICY name=NoAutoRebootWithLoggedOnUsers value=1 expected=1 ok=1
VM_POWER alias=111 vmx_present=0 running=0
VM_POWER alias=188 vmx_present=1 running=0
VM_POWER alias=120 vmx_present=1 running=0
VM_POWER alias=121 vmx_present=1 running=0
VM_POWER alias=112 vmx_present=1 running=0
VMWARE_AUTOSTART_CONFIG_READY=0
VMWARE_AUTOSTART_POWER_READY=0
WINDOWS_UPDATE_NO_AUTO_REBOOT_READY=1
VMWARE_AUTOSTART_VERIFY_READY=0
MISSING_VMX_ALIASES=111
"""
def _run(input_path: Path, output_path: Path | None = None) -> subprocess.CompletedProcess[str]:
cmd = ["python3.11", str(SCRIPT), "--input", str(input_path)]
if output_path is not None:
cmd.extend(["--output", str(output_path)])
return subprocess.run(
cmd,
cwd=ROOT,
capture_output=True,
text=True,
check=False,
)
def _kv(stdout: str) -> dict[str, str]:
values: dict[str, str] = {}
for line in stdout.splitlines():
if "=" in line:
key, value = line.split("=", 1)
values[key] = value
return values
def test_valid_console_verify_red_artifact_is_accepted_and_normalized(
tmp_path: Path,
) -> None:
raw = tmp_path / "console.txt"
normalized = tmp_path / "windows99-vmware-verify.txt"
raw.write_text(VALID_RED_VERIFY.replace("\n", "\r\n"), encoding="utf-8")
result = _run(raw, normalized)
assert result.returncode == 0
values = _kv(result.stdout)
assert values["schema_version"] == "windows99_console_verify_artifact_validator_v1"
assert values["status"] == "valid_console_verify_artifact_vmware_not_ready"
assert values["missing_required_field_count"] == "0"
assert values["missing_vmx_aliases"] == "111"
assert values["powered_off_aliases"] == "111,188,120,121,112"
assert values["secret_value_read"] == "false"
assert normalized.read_text(encoding="utf-8").startswith(
"AWOOOI_WINDOWS99_VMWARE_AUTOSTART=1\n"
)
def test_collector_envelope_is_extracted(tmp_path: Path) -> None:
raw = tmp_path / "collector.txt"
normalized = tmp_path / "normalized.txt"
raw.write_text(
textwrap.dedent(
f"""
schema_version=windows99_vmware_verify_collector_v1
remote_verify_output_begin
{VALID_RED_VERIFY}
remote_verify_output_end
"""
).lstrip(),
encoding="utf-8",
)
result = _run(raw, normalized)
assert result.returncode == 0
values = _kv(result.stdout)
assert values["status"] == "valid_console_verify_artifact_vmware_not_ready"
assert "remote_verify_output_begin" not in normalized.read_text(encoding="utf-8")
def test_secret_like_console_output_is_rejected(tmp_path: Path) -> None:
raw = tmp_path / "console.txt"
raw.write_text(VALID_RED_VERIFY + "password=not-allowed\n", encoding="utf-8")
result = _run(raw)
assert result.returncode == 76
values = _kv(result.stdout)
assert values["status"] == "blocked_secret_like_console_output"
assert values["secret_like_line_count"] == "1"
def test_missing_required_fields_fail_closed(tmp_path: Path) -> None:
raw = tmp_path / "console.txt"
raw.write_text("AWOOOI_WINDOWS99_VMWARE_AUTOSTART=1\nMODE=Verify\n", encoding="utf-8")
result = _run(raw)
assert result.returncode == 75
values = _kv(result.stdout)
assert values["status"] == "blocked_missing_required_console_verify_fields"
assert int(values["missing_required_field_count"]) > 0

View File

@@ -0,0 +1,163 @@
#!/usr/bin/env python3
"""Validate no-secret Windows99 VMware Verify stdout captured from console."""
from __future__ import annotations
import argparse
import re
import sys
from pathlib import Path
SCHEMA_VERSION = "windows99_console_verify_artifact_validator_v1"
DEFAULT_REQUIRED_ALIASES = ("111", "188", "120", "121", "112")
SENSITIVE_RE = re.compile(
r"(?i)(password|passwd|secret|token|authorization|cookie|private[_ -]?key)\s*[=:]"
)
def _normalize(text: str) -> list[str]:
text = text.replace("\r\n", "\n").replace("\r", "\n")
lines = [line.strip() for line in text.splitlines()]
if "remote_verify_output_begin" in lines and "remote_verify_output_end" in lines:
begin = lines.index("remote_verify_output_begin") + 1
end = lines.index("remote_verify_output_end")
lines = lines[begin:end]
return [line for line in lines if line]
def _value_for(lines: list[str], key: str) -> str:
prefix = f"{key}="
for line in lines:
if line.startswith(prefix):
return line.split("=", 1)[1]
return ""
def _line_matches(lines: list[str], pattern: str) -> bool:
compiled = re.compile(pattern)
return any(compiled.search(line) for line in lines)
def _csv(items: list[str]) -> str:
return ",".join(items)
def validate(lines: list[str], required_aliases: tuple[str, ...]) -> tuple[int, list[str]]:
missing_fields: list[str] = []
secret_like_lines = [line for line in lines if SENSITIVE_RE.search(line)]
required_exact_keys = [
"AWOOOI_WINDOWS99_VMWARE_AUTOSTART",
"MODE",
"VMRUN_PRESENT",
"VMWARE_AUTOSTART_CONFIG_READY",
"VMWARE_AUTOSTART_POWER_READY",
"WINDOWS_UPDATE_NO_AUTO_REBOOT_READY",
"VMWARE_AUTOSTART_VERIFY_READY",
]
for key in required_exact_keys:
if not _value_for(lines, key):
missing_fields.append(key)
if _value_for(lines, "AWOOOI_WINDOWS99_VMWARE_AUTOSTART") != "1":
missing_fields.append("AWOOOI_WINDOWS99_VMWARE_AUTOSTART=1")
if _value_for(lines, "MODE") != "Verify":
missing_fields.append("MODE=Verify")
if not any(line.startswith("VMWARE_SERVICE ") for line in lines):
missing_fields.append("VMWARE_SERVICE")
if not any(line.startswith("VMWARE_AUTOSTART_TASK ") for line in lines):
missing_fields.append("VMWARE_AUTOSTART_TASK")
if not any(line.startswith("WINDOWS_UPDATE_POLICY ") for line in lines):
missing_fields.append("WINDOWS_UPDATE_POLICY")
missing_vmx_aliases: list[str] = []
powered_off_aliases: list[str] = []
for alias in required_aliases:
vmx_pattern = rf"^VMX alias={re.escape(alias)}\s+.*\spresent=([01])$"
power_pattern = rf"^VM_POWER alias={re.escape(alias)}\s+.*\srunning=([01])$"
vmx_line = next((line for line in lines if re.search(vmx_pattern, line)), "")
power_line = next((line for line in lines if re.search(power_pattern, line)), "")
if not vmx_line:
missing_fields.append(f"VMX alias={alias}")
elif not _line_matches([vmx_line], rf"\spresent=1$"):
missing_vmx_aliases.append(alias)
if not power_line:
missing_fields.append(f"VM_POWER alias={alias}")
elif not _line_matches([power_line], rf"\srunning=1$"):
powered_off_aliases.append(alias)
status = "ready_console_verify_artifact"
exit_code = 0
if secret_like_lines:
status = "blocked_secret_like_console_output"
exit_code = 76
elif missing_fields:
status = "blocked_missing_required_console_verify_fields"
exit_code = 75
elif _value_for(lines, "VMWARE_AUTOSTART_VERIFY_READY") != "1":
status = "valid_console_verify_artifact_vmware_not_ready"
summary = [
f"schema_version={SCHEMA_VERSION}",
f"status={status}",
f"required_aliases={_csv(list(required_aliases))}",
f"missing_required_field_count={len(missing_fields)}",
f"missing_required_fields={_csv(missing_fields)}",
f"secret_like_line_count={len(secret_like_lines)}",
f"missing_vmx_aliases={_csv(missing_vmx_aliases)}",
f"powered_off_aliases={_csv(powered_off_aliases)}",
f"vmware_autostart_verify_ready={_value_for(lines, 'VMWARE_AUTOSTART_VERIFY_READY') or 'unknown'}",
"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",
]
return exit_code, summary
def main() -> int:
parser = argparse.ArgumentParser(
description=(
"Validate no-secret stdout from windows99-vmware-autostart.ps1 -Mode Verify "
"captured through RDP / Hyper-V / console."
)
)
parser.add_argument("--input", required=True, help="Raw console stdout file.")
parser.add_argument(
"--output",
help="Optional normalized artifact for --windows99-vmware-file.",
)
parser.add_argument(
"--required-aliases",
default=",".join(DEFAULT_REQUIRED_ALIASES),
help="Comma or space separated required VM aliases.",
)
args = parser.parse_args()
required_aliases = tuple(
item
for item in re.split(r"[\s,]+", args.required_aliases.strip())
if item
) or DEFAULT_REQUIRED_ALIASES
input_path = Path(args.input)
raw_text = input_path.read_text(encoding="utf-8", errors="replace")
lines = _normalize(raw_text)
exit_code, summary = validate(lines, required_aliases)
if exit_code == 0 and args.output:
output_path = Path(args.output)
output_path.write_text("\n".join(lines + [""]) , encoding="utf-8")
for line in summary:
print(line)
if args.output:
print(f"normalized_output={args.output if exit_code == 0 else ''}")
return exit_code
if __name__ == "__main__":
raise SystemExit(main())