installed-update-network.ps1 5.2 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889
  1. # Operator-owned test-application fault. Status is read-only; Block and Restore require typed confirmation.
  2. param(
  3. [Parameter(Mandatory = $true)][string]$Plan,
  4. [ValidateSet('Status', 'Block', 'Restore')][string]$Action = 'Status'
  5. )
  6. $ErrorActionPreference = 'Stop'
  7. Set-StrictMode -Version Latest
  8. $planPath = (Resolve-Path -LiteralPath $Plan).Path
  9. $spec = Get-Content -LiteralPath $planPath -Raw -Encoding UTF8 | ConvertFrom-Json
  10. if ($spec.schemaVersion -ne 1 -or $spec.runId -cnotmatch '^[a-f0-9]{24}$' -or
  11. $spec.ruleName -cne "DSH-Update-Qualification-$($spec.runId)" -or
  12. $spec.sha512Hex -cnotmatch '^[A-F0-9]{128}$' -or
  13. $spec.executable -notmatch '^[A-Za-z]:\\' -or
  14. [IO.Path]::GetFileName($spec.executable) -cne "DSH Update Test $($spec.runId).exe" -or
  15. [IO.Path]::GetFullPath($spec.executable) -cne $spec.executable) {
  16. throw 'Invalid test-only network plan; no network changes were made.'
  17. }
  18. $owner = "dsh-update-qualification:$($spec.runId):$($spec.sha512Hex)"
  19. $recordParent = Join-Path (Split-Path -Parent $planPath) 'records'
  20. [IO.Directory]::CreateDirectory($recordParent) | Out-Null
  21. $record = Join-Path $recordParent ([Guid]::NewGuid().ToString('N'))
  22. New-Item -ItemType Directory -Path $record | Out-Null
  23. $events = Join-Path $record 'events.jsonl'
  24. function Save-Event([string]$Stage, [object]$Data) {
  25. $line = @{ time = [DateTime]::UtcNow.ToString('o'); action = $Action; stage = $Stage; data = $Data } | ConvertTo-Json -Depth 6 -Compress
  26. $bytes = [Text.Encoding]::UTF8.GetBytes($line + "`n")
  27. $stream = [IO.File]::Open($events, [IO.FileMode]::Append, [IO.FileAccess]::Write, [IO.FileShare]::Read)
  28. try { $stream.Write($bytes, 0, $bytes.Length); $stream.Flush($true) } finally { $stream.Dispose() }
  29. }
  30. function Find-OwnedRule {
  31. $rules = @(Get-NetFirewallRule -PolicyStore PersistentStore | Where-Object { $_.Name -ceq $spec.ruleName })
  32. if ($rules.Count -gt 1) { throw 'Multiple rules match this run; manual inspection is required.' }
  33. if ($rules.Count -eq 0) { return $null }
  34. $rule = $rules[0]
  35. $filters = @($rule | Get-NetFirewallApplicationFilter)
  36. if ($rule.Description -cne $owner -or [string]$rule.Direction -ne 'Outbound' -or [string]$rule.Action -ne 'Block' -or
  37. $filters.Count -ne 1 -or $filters[0].Program -ine $spec.executable) {
  38. throw 'The existing rule is not owned by this exact test plan; it was not modified.'
  39. }
  40. return $rule
  41. }
  42. $success = $false
  43. Write-Output "NETWORK_FAULT_RECORD $record"
  44. try {
  45. Save-Event 'started' @{ runId = $spec.runId; executable = $spec.executable; ruleName = $spec.ruleName }
  46. $existing = Find-OwnedRule
  47. Save-Event 'before' @{ present = ($null -ne $existing) }
  48. if ($Action -eq 'Block') {
  49. if ($null -ne $existing) { throw 'A test rule already exists. Restore it before starting another fault.' }
  50. if ((Get-FileHash -LiteralPath $spec.executable -Algorithm SHA512).Hash -cne $spec.sha512Hex) {
  51. throw 'The test executable changed. No rule was created.'
  52. }
  53. Write-Output "Only this executable will be blocked: $($spec.executable)"
  54. Write-Output 'Keep a second administrator terminal ready to run Restore. Do not disable the adapter, VPN, or proxy.'
  55. if ((Read-Host "Type BLOCK $($spec.runId) after download progress begins") -cne "BLOCK $($spec.runId)") {
  56. throw 'Confirmation declined; no rule was created.'
  57. }
  58. if ($null -ne (Find-OwnedRule)) { throw 'A rule appeared during confirmation. No rule was created.' }
  59. if ((Get-FileHash -LiteralPath $spec.executable -Algorithm SHA512).Hash -cne $spec.sha512Hex) {
  60. throw 'The test executable changed during confirmation. No rule was created.'
  61. }
  62. Save-Event 'creating-rule' @{ name = $spec.ruleName }
  63. New-NetFirewallRule -Name $spec.ruleName -DisplayName $spec.ruleName -Description $owner `
  64. -Direction Outbound -Program $spec.executable -Action Block -Profile Any -Enabled True -PolicyStore PersistentStore | Out-Null
  65. $created = Find-OwnedRule
  66. if ($null -eq $created -or [string]$created.Enabled -ne 'True') { throw 'The rule was not confirmed enabled. Use Restore.' }
  67. Save-Event 'blocked-rule-present' @{ trafficInterruptionVerified = $false }
  68. } elseif ($Action -eq 'Restore') {
  69. if ($null -ne $existing) {
  70. if ((Read-Host "Type RESTORE $($spec.runId)") -cne "RESTORE $($spec.runId)") { throw 'Restoration declined.' }
  71. $existing = Find-OwnedRule
  72. if ($null -ne $existing) {
  73. Save-Event 'removing-rule' @{ name = $spec.ruleName }
  74. $existing | Remove-NetFirewallRule
  75. }
  76. }
  77. if ($null -ne (Find-OwnedRule)) { throw 'The test rule still exists; restoration is incomplete.' }
  78. Save-Event 'rule-absent' @{ downloadRecoveryVerified = $false }
  79. } else {
  80. Save-Event 'status' @{ present = ($null -ne $existing); enabled = $(if ($null -ne $existing) { [string]$existing.Enabled } else { $null }) }
  81. Write-Output "Test rule present: $($null -ne $existing)"
  82. }
  83. $success = $true
  84. } catch {
  85. Save-Event 'failed' @{ message = $_.Exception.Message; automaticRetry = $false }
  86. throw
  87. } finally {
  88. Save-Event 'finished' @{ success = $success; rulePresenceIsNotTrafficEvidence = $true }
  89. }