Files
awoooi/agent99-windows-control-baseline.ps1
Your Name f7c7a9e511
All checks were successful
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 2m19s
CD Pipeline / build-and-deploy (push) Successful in 15m8s
CD Pipeline / post-deploy-checks (push) Successful in 2m11s
AI 技術雷達監控 / ai-technology-watch (push) Successful in 43s
fix(agent99): normalize Windows control readback
2026-07-17 07:29:16 +08:00

396 lines
17 KiB
PowerShell

[CmdletBinding()]
param(
[ValidateSet("Check", "Apply")][string]$Mode = "Check",
[Parameter(Mandatory = $true)][ValidatePattern("^[A-Za-z0-9][A-Za-z0-9._:@+-]{0,127}$")][string]$TraceId,
[Parameter(Mandatory = $true)][ValidatePattern("^[0-9a-f-]{36}$")][string]$RunId,
[Parameter(Mandatory = $true)][ValidatePattern("^[A-Za-z0-9][A-Za-z0-9._:@+-]{0,127}$")][string]$WorkItemId,
[Parameter(Mandatory = $true)][ValidatePattern("^[0-9a-f]{40}$")][string]$SourceRevision,
[string]$AgentRoot = "C:\Wooo\Agent99"
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
[Console]::InputEncoding = [System.Text.UTF8Encoding]::new($false)
[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)
$global:OutputEncoding = [System.Text.UTF8Encoding]::new($false)
$SchemaVersion = "agent99_windows_control_baseline_v1"
$CanonicalAsset = "host:192.168.0.99/windows-control-baseline"
$ExpectedComputerName = "WOOO-SUPER"
$ExpectedUserName = "Administrator"
$PreservedCulture = "zh-TW"
$PreservedChineseLanguage = "zh-Hant-TW"
$FallbackLanguage = "en-US"
$FallbackInputTip = "0409:00000409"
$ConsoleRegistryPath = "HKCU:\Console"
$EvidenceRoot = [System.IO.Path]::GetFullPath((Join-Path $AgentRoot "evidence\"))
$EvidencePath = [System.IO.Path]::GetFullPath((Join-Path $EvidenceRoot "agent99-WindowsControlBaseline-$RunId.json"))
$startedAt = Get-Date
$actions = @()
$errorCode = $null
$errorClass = $null
$independentReadback = $null
$rollbackAttempted = $false
$rollbackVerified = $false
$writeAttempted = $false
$before = $null
$after = $null
$beforeLanguageList = $null
$beforeOverride = $null
$beforeConsoleValueExists = $false
$beforeConsoleCodePage = $null
$beforeSshdStartMode = ""
if (-not $EvidencePath.StartsWith($EvidenceRoot, [System.StringComparison]::OrdinalIgnoreCase)) {
throw "evidence_path_outside_agent_root"
}
function Write-AtomicJson {
param([string]$Path, [object]$Value)
[System.IO.Directory]::CreateDirectory((Split-Path -Parent $Path)) | Out-Null
$temporaryPath = "$Path.tmp"
[System.IO.File]::WriteAllText($temporaryPath, ($Value | ConvertTo-Json -Depth 20), [System.Text.UTF8Encoding]::new($false))
Move-Item -LiteralPath $temporaryPath -Destination $Path -Force
}
function Get-ConsoleCodePageState {
if (-not (Test-Path -LiteralPath $ConsoleRegistryPath -PathType Container)) {
return [pscustomobject]@{ valueExists = $false; value = $null }
}
$properties = Get-ItemProperty -LiteralPath $ConsoleRegistryPath -ErrorAction Stop
$property = $properties.PSObject.Properties["CodePage"]
if (-not $property) {
return [pscustomobject]@{ valueExists = $false; value = $null }
}
return [pscustomobject]@{ valueExists = $true; value = [int]$property.Value }
}
function Get-DefaultInputMethodTip {
$override = Get-WinDefaultInputMethodOverride
if (-not $override) { return $null }
$property = $override.PSObject.Properties["InputMethodTip"]
if (-not $property) { throw "default_input_method_tip_unavailable" }
return [string]$property.Value
}
function Get-WindowsControlSnapshot {
$languageList = Get-WinUserLanguageList
$languageRows = @(
foreach ($language in $languageList) {
[pscustomobject]@{
languageTag = [string]$language.LanguageTag
inputMethodTips = @($language.InputMethodTips | ForEach-Object { [string]$_ })
}
}
)
$overrideTip = Get-DefaultInputMethodTip
$consoleState = Get-ConsoleCodePageState
$sshdService = Get-Service -Name "sshd" -ErrorAction Stop
$sshdCim = Get-CimInstance -ClassName Win32_Service -Filter "Name='sshd'" -ErrorAction Stop
[pscustomobject]@{
computerName = [string]$env:COMPUTERNAME
userName = [string]$env:USERNAME
culture = [string](Get-Culture).Name
uiCulture = [string](Get-UICulture).Name
systemLocale = [string](Get-WinSystemLocale).Name
languages = $languageRows
defaultInputMethodOverride = $overrideTip
consoleCodePageValueExists = [bool]$consoleState.valueExists
consoleCodePage = if ($consoleState.valueExists) { [int]$consoleState.value } else { $null }
sshdStatus = [string]$sshdService.Status
sshdStartMode = [string]$sshdCim.StartMode
}
}
function Get-LanguageSignatures {
param([object[]]$Languages)
$signatures = @()
foreach ($language in @($Languages)) {
if (@($language.inputMethodTips).Count -eq 0) {
$signatures += "$([string]$language.languageTag)|"
continue
}
foreach ($tip in @($language.inputMethodTips)) {
$signatures += "$([string]$language.languageTag)|$([string]$tip)"
}
}
return @($signatures | Sort-Object -Unique)
}
function Test-WindowsControlSnapshot {
param(
[object]$Snapshot,
[object]$PreserveFrom,
[switch]$IndependentProcess
)
$languageTags = @($Snapshot.languages | ForEach-Object { [string]$_.languageTag })
$languageSignatures = @(Get-LanguageSignatures $Snapshot.languages)
$preservedSignatures = @(Get-LanguageSignatures $PreserveFrom.languages)
$missingPreserved = @($preservedSignatures | Where-Object { $_ -notin $languageSignatures })
$fallbackSignature = "$FallbackLanguage|$FallbackInputTip"
$checks = [ordered]@{
canonicalComputer = [string]$Snapshot.computerName -eq $ExpectedComputerName
canonicalUser = [string]$Snapshot.userName -ieq $ExpectedUserName
culturePreserved = [string]$Snapshot.culture -eq $PreservedCulture
uiCulturePreserved = if ($IndependentProcess) {
$PreservedChineseLanguage -in $languageTags -and
[string]$Snapshot.systemLocale -eq $PreservedCulture
} else {
[string]$Snapshot.uiCulture -eq $PreservedCulture
}
systemLocalePreserved = [string]$Snapshot.systemLocale -eq $PreservedCulture
chineseLanguagePreserved = $PreservedChineseLanguage -in $languageTags
priorInputMethodsPreserved = $missingPreserved.Count -eq 0
englishFallbackPresent = $fallbackSignature -in $languageSignatures
defaultInputIsUsKeyboard = [string]$Snapshot.defaultInputMethodOverride -eq $FallbackInputTip
consoleUtf8 = [bool]$Snapshot.consoleCodePageValueExists -and [int]$Snapshot.consoleCodePage -eq 65001
sshdRunning = [string]$Snapshot.sshdStatus -eq "Running"
sshdAutomatic = [string]$Snapshot.sshdStartMode -eq "Auto"
}
[pscustomobject]@{
passed = @($checks.GetEnumerator() | Where-Object { -not $_.Value }).Count -eq 0
checks = [pscustomobject]$checks
missingPreservedLanguageSignatures = $missingPreserved
}
}
function Get-SnapshotSignature {
param([object]$Snapshot)
# Process UI culture and user-name casing differ in a no-profile verifier.
$normalized = [ordered]@{
computerName = ([string]$Snapshot.computerName).ToLowerInvariant()
userName = ([string]$Snapshot.userName).ToLowerInvariant()
culture = [string]$Snapshot.culture
systemLocale = [string]$Snapshot.systemLocale
languages = @(Get-LanguageSignatures $Snapshot.languages)
defaultInputMethodOverride = [string]$Snapshot.defaultInputMethodOverride
consoleCodePageValueExists = [bool]$Snapshot.consoleCodePageValueExists
consoleCodePage = if ($null -ne $Snapshot.consoleCodePage) { [int]$Snapshot.consoleCodePage } else { $null }
sshdStatus = [string]$Snapshot.sshdStatus
sshdStartMode = [string]$Snapshot.sshdStartMode
}
return ($normalized | ConvertTo-Json -Depth 8 -Compress)
}
function Invoke-IndependentSnapshot {
$verifierScript = @'
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)
$languageList = Get-WinUserLanguageList
$languages = @(foreach ($language in $languageList) {
[pscustomobject]@{ languageTag = [string]$language.LanguageTag; inputMethodTips = @($language.InputMethodTips | ForEach-Object { [string]$_ }) }
})
$override = Get-WinDefaultInputMethodOverride
$overrideTip = if ($override -and $override.PSObject.Properties["InputMethodTip"]) { [string]$override.InputMethodTip } else { $null }
$consolePath = "HKCU:\Console"
$consoleExists = $false
$consoleCodePage = $null
if (Test-Path -LiteralPath $consolePath -PathType Container) {
$properties = Get-ItemProperty -LiteralPath $consolePath -ErrorAction Stop
$property = $properties.PSObject.Properties["CodePage"]
if ($property) { $consoleExists = $true; $consoleCodePage = [int]$property.Value }
}
$sshd = Get-Service -Name "sshd" -ErrorAction Stop
$sshdCim = Get-CimInstance -ClassName Win32_Service -Filter "Name='sshd'" -ErrorAction Stop
[ordered]@{
processId = $PID
snapshot = [ordered]@{
computerName = [string]$env:COMPUTERNAME
userName = [string]$env:USERNAME
culture = [string](Get-Culture).Name
uiCulture = [string](Get-UICulture).Name
systemLocale = [string](Get-WinSystemLocale).Name
languages = $languages
defaultInputMethodOverride = $overrideTip
consoleCodePageValueExists = $consoleExists
consoleCodePage = $consoleCodePage
sshdStatus = [string]$sshd.Status
sshdStartMode = [string]$sshdCim.StartMode
}
} | ConvertTo-Json -Depth 10 -Compress
'@
$encoded = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($verifierScript))
$token = [Guid]::NewGuid().ToString("N")
$stdoutPath = Join-Path $env:TEMP "agent99-windows-baseline-$token.out"
$stderrPath = Join-Path $env:TEMP "agent99-windows-baseline-$token.err"
try {
$process = Start-Process -FilePath "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" `
-ArgumentList @("-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-EncodedCommand", $encoded) `
-NoNewWindow -RedirectStandardOutput $stdoutPath -RedirectStandardError $stderrPath -PassThru
if (-not $process.WaitForExit(120 * 1000)) {
try { & taskkill.exe /PID $process.Id /T /F 2>&1 | Out-Null } catch {}
throw "independent_verifier_timeout"
}
$process.WaitForExit() | Out-Null
$process.Refresh()
if ([int]$process.ExitCode -ne 0) { throw "independent_verifier_failed" }
try {
return (Get-Content -LiteralPath $stdoutPath -Raw | ConvertFrom-Json)
} catch {
throw "independent_verifier_receipt_invalid"
}
} finally {
Remove-Item -LiteralPath $stdoutPath, $stderrPath -Force -ErrorAction SilentlyContinue
}
}
function Set-WindowsControlBaseline {
$languageList = Get-WinUserLanguageList
$languageTags = @(foreach ($language in $languageList) { [string]$language.LanguageTag })
if ($FallbackLanguage -notin $languageTags) {
$fallback = New-WinUserLanguageList $FallbackLanguage
$languageList.Add($fallback[0]) | Out-Null
Set-WinUserLanguageList -LanguageList $languageList -Force
$script:actions += "added_en_us_keyboard_fallback"
$script:writeAttempted = $true
}
if ([string](Get-DefaultInputMethodTip) -ne $FallbackInputTip) {
Set-WinDefaultInputMethodOverride -InputTip $FallbackInputTip
$script:actions += "set_default_input_method_us_keyboard"
$script:writeAttempted = $true
}
$consoleState = Get-ConsoleCodePageState
if (-not $consoleState.valueExists -or [int]$consoleState.value -ne 65001) {
New-Item -Path $ConsoleRegistryPath -Force | Out-Null
New-ItemProperty -Path $ConsoleRegistryPath -Name "CodePage" -Value 65001 -PropertyType DWord -Force | Out-Null
$script:actions += "set_future_console_code_page_utf8"
$script:writeAttempted = $true
}
$sshd = Get-Service -Name "sshd" -ErrorAction Stop
$sshdCim = Get-CimInstance -ClassName Win32_Service -Filter "Name='sshd'" -ErrorAction Stop
if ([string]$sshdCim.StartMode -ne "Auto") {
Set-Service -Name "sshd" -StartupType Automatic
$script:actions += "set_sshd_automatic"
$script:writeAttempted = $true
}
if ([string]$sshd.Status -ne "Running") {
Start-Service -Name "sshd"
$script:actions += "start_sshd"
$script:writeAttempted = $true
}
}
function Restore-WindowsControlBaseline {
Set-WinUserLanguageList -LanguageList $beforeLanguageList -Force
if ($beforeOverride) {
Set-WinDefaultInputMethodOverride -InputTip ([string]$beforeOverride)
} else {
Set-WinDefaultInputMethodOverride
}
if ($beforeConsoleValueExists) {
New-Item -Path $ConsoleRegistryPath -Force | Out-Null
New-ItemProperty -Path $ConsoleRegistryPath -Name "CodePage" -Value ([int]$beforeConsoleCodePage) -PropertyType DWord -Force | Out-Null
} else {
Remove-ItemProperty -LiteralPath $ConsoleRegistryPath -Name "CodePage" -ErrorAction SilentlyContinue
}
$startupType = switch ($beforeSshdStartMode) {
"Auto" { "Automatic" }
"Manual" { "Manual" }
"Disabled" { "Disabled" }
default { "Automatic" }
}
Set-Service -Name "sshd" -StartupType $startupType
}
try {
$beforeLanguageList = Get-WinUserLanguageList
$beforeOverride = Get-DefaultInputMethodTip
$beforeConsoleState = Get-ConsoleCodePageState
$beforeConsoleValueExists = [bool]$beforeConsoleState.valueExists
$beforeConsoleCodePage = $beforeConsoleState.value
$beforeSshdStartMode = [string](Get-CimInstance -ClassName Win32_Service -Filter "Name='sshd'" -ErrorAction Stop).StartMode
$before = Get-WindowsControlSnapshot
if ([string]$before.computerName -ne $ExpectedComputerName) { throw "canonical_computer_mismatch" }
if ([string]$before.userName -ine $ExpectedUserName) { throw "canonical_user_mismatch" }
if ($Mode -eq "Apply") { Set-WindowsControlBaseline }
$after = Get-WindowsControlSnapshot
$independentReadback = Invoke-IndependentSnapshot
} catch {
$errorClass = $_.Exception.GetType().Name
$candidateCode = [string]$_.Exception.Message
$errorCode = if ($candidateCode -match "^[A-Za-z0-9_:-]{1,96}$") { $candidateCode } else { "windows_control_baseline_failed" }
}
$postCondition = if ($after -and $before) { Test-WindowsControlSnapshot $after $before } else { $null }
$independentPostCondition = if ($independentReadback -and $before) {
Test-WindowsControlSnapshot $independentReadback.snapshot $before -IndependentProcess
} else {
$null
}
$independentAgreement = [bool](
$after -and $independentReadback -and
(Get-SnapshotSignature $after) -eq (Get-SnapshotSignature $independentReadback.snapshot)
)
$runtimeVerified = [bool](
-not $errorCode -and $postCondition -and $independentPostCondition -and
$postCondition.passed -and $independentPostCondition.passed -and $independentAgreement
)
if ($Mode -eq "Apply" -and $writeAttempted -and -not $runtimeVerified) {
$rollbackAttempted = $true
try {
Restore-WindowsControlBaseline
$rollbackSnapshot = Get-WindowsControlSnapshot
$rollbackVerified = (Get-SnapshotSignature $rollbackSnapshot) -eq (Get-SnapshotSignature $before)
} catch {
$rollbackVerified = $false
}
}
$finishedAt = Get-Date
$terminal = if ($Mode -eq "Check" -and $independentAgreement -and -not $errorCode) {
"verified_check_no_write"
} elseif ($runtimeVerified -and $writeAttempted) {
"runtime_verified_controlled_apply"
} elseif ($runtimeVerified) {
"runtime_verified_healthy_no_write"
} elseif ($rollbackAttempted -and $rollbackVerified) {
"rolled_back_verified"
} else {
"degraded_with_safe_next_action"
}
$passed = [bool](
($Mode -eq "Check" -and $independentAgreement -and -not $errorCode) -or
($Mode -eq "Apply" -and $runtimeVerified)
)
$receipt = [ordered]@{
schemaVersion = $SchemaVersion
traceId = $TraceId
runId = $RunId
workItemId = $WorkItemId
generatedAt = $finishedAt.ToString("o")
sourceRevision = $SourceRevision
executor = "host:192.168.0.99"
target = $CanonicalAsset
mode = $Mode
risk = [ordered]@{ level = if ($Mode -eq "Apply") { "medium" } else { "low" }; hostReboot = $false; userLogoff = $false; guiInteraction = $false; clipboardUsed = $false; secretValueRead = $false }
desired = [ordered]@{ preservedCulture = $PreservedCulture; preservedLanguage = $PreservedChineseLanguage; fallbackLanguage = $FallbackLanguage; fallbackInputTip = $FallbackInputTip; consoleCodePage = 65001; sshd = "Running/Auto" }
check = [ordered]@{ before = $before; postCondition = $postCondition }
execution = [ordered]@{ performed = $writeAttempted; actions = @($actions); errorCode = $errorCode; errorClass = $errorClass; elapsedMs = [math]::Round(($finishedAt - $startedAt).TotalMilliseconds) }
verifier = [ordered]@{ independentProcessId = if ($independentReadback) { [int]$independentReadback.processId } else { $null }; readback = if ($independentReadback) { $independentReadback.snapshot } else { $null }; postCondition = $independentPostCondition; exactAgreement = $independentAgreement; passed = $runtimeVerified }
rollback = [ordered]@{ attempted = $rollbackAttempted; verified = $rollbackVerified; sshdStopAllowed = $false }
transport = [ordered]@{ encodedCommandCompatible = $true; interactiveInputRequired = $false; inputMethodIndependent = $true; utf8Output = $true }
telegramNotificationSent = $false
kmRagWritebackPerformed = $false
terminal = $terminal
}
Write-AtomicJson -Path $EvidencePath -Value $receipt
[ordered]@{
evidencePath = $EvidencePath
evidenceSha256 = (Get-FileHash -LiteralPath $EvidencePath -Algorithm SHA256).Hash.ToLowerInvariant()
terminal = $terminal
mode = $Mode
writePerformed = $writeAttempted
ready = if ($postCondition) { [bool]$postCondition.passed } else { $false }
independentVerifier = $independentAgreement
rollbackAttempted = $rollbackAttempted
rollbackVerified = $rollbackVerified
errorCode = $errorCode
} | ConvertTo-Json -Compress -Depth 8
if (-not $passed) { exit 3 }
exit 0