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 2m29s
CD Pipeline / build-and-deploy (push) Failing after 13m12s
AWOOOI Harbor 110 Local Repair / workflow-shape (push) Successful in 0s
CD Pipeline / post-deploy-checks (push) Has been skipped
AWOOOI Harbor 110 Local Repair / harbor-110-local-repair (push) Successful in 16s
396 lines
17 KiB
PowerShell
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 $runtimeVerified) {
|
|
"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 $runtimeVerified) -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
|