209 lines
7.4 KiB
PowerShell
209 lines
7.4 KiB
PowerShell
[CmdletBinding()]
|
|
param(
|
|
[ValidateSet("Check", "Apply", "Verify")]
|
|
[string]$Mode = "Check",
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$TraceId,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$RunId,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$WorkItemId,
|
|
[string]$AgentRoot = "C:\Wooo\Agent99"
|
|
)
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
$ProgressPreference = "SilentlyContinue"
|
|
|
|
$LegacyTaskName = "Wooo-Agent99-Host188-Performance-Guard"
|
|
$ReplacementTaskName = "Wooo-Agent99-Performance-Guard"
|
|
$ExpectedLegacyActionSha256 = "a2f98560673ebed5aa1f828b4e577c8a34d1c277b2e498044c3526a4ffe15379"
|
|
$EvidenceRoot = Join-Path $AgentRoot "evidence\controlled-task-quarantine"
|
|
$ReceiptPath = Join-Path $EvidenceRoot "$RunId.json"
|
|
$RollbackPath = Join-Path $EvidenceRoot "$RunId.rollback.xml"
|
|
$TemporaryReceiptPath = "$ReceiptPath.tmp"
|
|
$SafeIdentityPattern = "^[A-Za-z0-9](?:[A-Za-z0-9_.:-]{0,158}[A-Za-z0-9])?$"
|
|
|
|
function Get-ActionArgumentSha256 {
|
|
param([Parameter(Mandatory = $true)][object]$Action)
|
|
|
|
$sha256 = [Security.Cryptography.SHA256]::Create()
|
|
try {
|
|
$bytes = [Text.Encoding]::UTF8.GetBytes([string]$Action.Arguments)
|
|
return (($sha256.ComputeHash($bytes) | ForEach-Object { $_.ToString("x2") }) -join "")
|
|
} finally {
|
|
$sha256.Dispose()
|
|
}
|
|
}
|
|
|
|
function Get-ControlledTaskState {
|
|
$legacy = Get-ScheduledTask -TaskName $LegacyTaskName -TaskPath "\" -ErrorAction Stop
|
|
$replacement = Get-ScheduledTask -TaskName $ReplacementTaskName -TaskPath "\" -ErrorAction Stop
|
|
$legacyActions = @($legacy.Actions)
|
|
$replacementActions = @($replacement.Actions)
|
|
|
|
if ($legacyActions.Count -ne 1) { throw "legacy_task_action_count_mismatch" }
|
|
if ((Get-ActionArgumentSha256 $legacyActions[0]) -ne $ExpectedLegacyActionSha256) {
|
|
throw "legacy_task_action_identity_mismatch"
|
|
}
|
|
if ([string]$legacy.State -eq "Running") { throw "legacy_task_running_refuse_apply" }
|
|
if ($replacementActions.Count -ne 1 -or -not [bool]$replacement.Settings.Enabled) {
|
|
throw "canonical_replacement_not_ready"
|
|
}
|
|
|
|
$replacementArguments = [string]$replacementActions[0].Arguments
|
|
if ($replacementArguments -notmatch "agent99-run\.ps1.+-Mode\s+Perf") {
|
|
throw "canonical_replacement_action_mismatch"
|
|
}
|
|
if ($replacementArguments -match "api\.telegram\.org|sendMessage|BotToken|ChatId") {
|
|
throw "canonical_replacement_direct_telegram_marker"
|
|
}
|
|
|
|
return [pscustomobject]@{
|
|
legacyEnabled = [bool]$legacy.Settings.Enabled
|
|
legacyState = [string]$legacy.State
|
|
replacementEnabled = [bool]$replacement.Settings.Enabled
|
|
legacyActionSha256 = $ExpectedLegacyActionSha256
|
|
}
|
|
}
|
|
|
|
function Write-AtomicReceipt {
|
|
param([Parameter(Mandatory = $true)][object]$Receipt)
|
|
|
|
if (Test-Path -LiteralPath $TemporaryReceiptPath) {
|
|
Remove-Item -LiteralPath $TemporaryReceiptPath -Force
|
|
}
|
|
$Receipt | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $TemporaryReceiptPath -Encoding UTF8
|
|
if (Test-Path -LiteralPath $ReceiptPath) { throw "controlled_receipt_identity_already_exists" }
|
|
Move-Item -LiteralPath $TemporaryReceiptPath -Destination $ReceiptPath
|
|
}
|
|
|
|
foreach ($identity in @($TraceId, $RunId, $WorkItemId)) {
|
|
if ($identity -notmatch $SafeIdentityPattern -or $identity -in @(".", "..")) {
|
|
throw "controlled_identity_invalid"
|
|
}
|
|
}
|
|
|
|
$state = Get-ControlledTaskState
|
|
|
|
if ($Mode -eq "Check") {
|
|
[ordered]@{
|
|
schemaVersion = "agent99_legacy_sender_quarantine_check_v1"
|
|
status = "check_ready"
|
|
mode = "check"
|
|
traceId = $TraceId
|
|
runId = $RunId
|
|
workItemId = $WorkItemId
|
|
canonicalAssetId = "windows-vmware:host_99"
|
|
legacyTask = $LegacyTaskName
|
|
replacementTask = $ReplacementTaskName
|
|
legacyEnabled = $state.legacyEnabled
|
|
replacementEnabled = $state.replacementEnabled
|
|
legacyActionSha256 = $state.legacyActionSha256
|
|
wouldDisableLegacySender = $state.legacyEnabled
|
|
remoteWritePerformed = $false
|
|
secretValueRead = $false
|
|
nextSafeAction = "apply_once_with_new_run_identity_then_independent_verify"
|
|
} | ConvertTo-Json -Compress -Depth 6
|
|
exit 0
|
|
}
|
|
|
|
if ($Mode -eq "Verify") {
|
|
$receiptAvailable = Test-Path -LiteralPath $ReceiptPath -PathType Leaf
|
|
$verified = [bool](-not $state.legacyEnabled -and $state.replacementEnabled -and $receiptAvailable)
|
|
[ordered]@{
|
|
schemaVersion = "agent99_legacy_sender_quarantine_verify_v1"
|
|
status = if ($verified) { "quarantine_verified" } else { "quarantine_unverified" }
|
|
mode = "verify"
|
|
traceId = $TraceId
|
|
runId = $RunId
|
|
workItemId = $WorkItemId
|
|
canonicalAssetId = "windows-vmware:host_99"
|
|
legacyEnabled = $state.legacyEnabled
|
|
replacementEnabled = $state.replacementEnabled
|
|
durableReceiptAvailable = $receiptAvailable
|
|
remoteWritePerformed = $false
|
|
secretValueRead = $false
|
|
} | ConvertTo-Json -Compress -Depth 6
|
|
if (-not $verified) { exit 2 }
|
|
exit 0
|
|
}
|
|
|
|
$modified = $false
|
|
$rollbackVerified = $false
|
|
try {
|
|
if (
|
|
(Test-Path -LiteralPath $ReceiptPath) -or
|
|
(Test-Path -LiteralPath $RollbackPath)
|
|
) {
|
|
throw "controlled_receipt_identity_already_exists"
|
|
}
|
|
New-Item -ItemType Directory -Force -Path $EvidenceRoot | Out-Null
|
|
Export-ScheduledTask -TaskName $LegacyTaskName -TaskPath "\" |
|
|
Set-Content -LiteralPath $RollbackPath -Encoding Unicode
|
|
$rollbackSha256 = (Get-FileHash -LiteralPath $RollbackPath -Algorithm SHA256).Hash.ToLowerInvariant()
|
|
|
|
if ($state.legacyEnabled) {
|
|
Disable-ScheduledTask -TaskName $LegacyTaskName -TaskPath "\" | Out-Null
|
|
$modified = $true
|
|
}
|
|
|
|
$after = Get-ControlledTaskState
|
|
if ($after.legacyEnabled) { throw "legacy_task_disable_post_verifier_failed" }
|
|
if (-not $after.replacementEnabled) { throw "canonical_replacement_disabled" }
|
|
|
|
$receipt = [ordered]@{
|
|
schemaVersion = "agent99_legacy_sender_quarantine_receipt_v1"
|
|
status = "quarantined_verified"
|
|
traceId = $TraceId
|
|
runId = $RunId
|
|
workItemId = $WorkItemId
|
|
canonicalAssetId = "windows-vmware:host_99"
|
|
legacyTask = $LegacyTaskName
|
|
replacementTask = $ReplacementTaskName
|
|
legacyActionSha256 = $ExpectedLegacyActionSha256
|
|
rollbackArtifactSha256 = $rollbackSha256
|
|
legacyEnabledBefore = $state.legacyEnabled
|
|
legacyEnabledAfter = $after.legacyEnabled
|
|
replacementEnabledAfter = $after.replacementEnabled
|
|
remoteWritePerformed = $modified
|
|
scheduledTaskModification = $modified
|
|
taskDeleted = $false
|
|
serviceRestart = $false
|
|
hostReboot = $false
|
|
vmPowerChange = $false
|
|
secretValueRead = $false
|
|
privateKeyValueRead = $false
|
|
tokenValueRead = $false
|
|
environmentSecretRead = $false
|
|
rollbackAvailable = $true
|
|
verifierPassed = $true
|
|
nextSafeAction = "persist_exact_legacy_sender_sunset_policy_in_gitea_and_verify_no_new_wooo_bot_receipts"
|
|
}
|
|
Write-AtomicReceipt $receipt
|
|
$receipt | ConvertTo-Json -Compress -Depth 8
|
|
exit 0
|
|
} catch {
|
|
if ($modified) {
|
|
try {
|
|
Enable-ScheduledTask -TaskName $LegacyTaskName -TaskPath "\" | Out-Null
|
|
$rollbackVerified = [bool](Get-ControlledTaskState).legacyEnabled
|
|
} catch {
|
|
$rollbackVerified = $false
|
|
}
|
|
}
|
|
[ordered]@{
|
|
schemaVersion = "agent99_legacy_sender_quarantine_receipt_v1"
|
|
status = if ($rollbackVerified) { "failed_rolled_back_verified" } else { "failed_no_write_or_rollback_unverified" }
|
|
traceId = $TraceId
|
|
runId = $RunId
|
|
workItemId = $WorkItemId
|
|
failureCode = [string]$_.Exception.Message
|
|
remoteWritePerformed = $modified
|
|
rollbackVerified = $rollbackVerified
|
|
secretValueRead = $false
|
|
hostReboot = $false
|
|
vmPowerChange = $false
|
|
} | ConvertTo-Json -Compress -Depth 6
|
|
exit 2
|
|
}
|