Files
awoooi/agent99-sre-alert-relay.ps1

219 lines
7.4 KiB
PowerShell

param(
[string]$AgentRoot = "C:\Wooo\Agent99",
[string]$Prefix = "http://+:8787/agent99/sre-alert/",
[string]$TokenEnv = "AGENT99_SRE_RELAY_TOKEN",
[string[]]$AllowedRemotePrefixes = @("127.", "::1", "192.168.0."),
[int]$MaxBodyBytes = 262144,
[switch]$RunOnce
)
$ErrorActionPreference = "Stop"
$EvidenceDir = Join-Path $AgentRoot "evidence"
$AlertRoot = Join-Path $AgentRoot "alerts"
$IncomingDir = Join-Path $AlertRoot "incoming"
$LogDir = Join-Path $AgentRoot "logs"
$BinDir = Join-Path $AgentRoot "bin"
$SreAlertInboxScript = Join-Path $BinDir "agent99-sre-alert-inbox.ps1"
New-Item -ItemType Directory -Force -Path $EvidenceDir, $IncomingDir, $LogDir | Out-Null
function Convert-AgentSafeFileToken {
param([string]$Value)
$safe = [regex]::Replace($Value, "[^A-Za-z0-9_.-]", "_")
if (-not $safe) { return "relay-alert" }
if ($safe.Length -gt 96) { return $safe.Substring(0, 96) }
return $safe
}
function Get-AgentField {
param(
[object]$Object,
[string]$Name,
[object]$Default = $null
)
if ($Object -and $Object.PSObject.Properties[$Name]) {
return $Object.PSObject.Properties[$Name].Value
}
return $Default
}
function Get-AgentEnvValue {
param([string]$Name)
foreach ($target in @("Process", "User", "Machine")) {
$value = [Environment]::GetEnvironmentVariable($Name, $target)
if ($value) { return $value }
}
return ""
}
function Write-AgentRelayEvent {
param([hashtable]$Event)
$path = Join-Path $LogDir "agent99-sre-alert-relay.jsonl"
$Event["timestamp"] = (Get-Date).ToString("o")
($Event | ConvertTo-Json -Compress -Depth 8) | Add-Content -Encoding UTF8 $path
}
function Send-AgentRelayResponse {
param(
[System.Net.HttpListenerContext]$Context,
[int]$StatusCode,
[hashtable]$Body
)
$json = $Body | ConvertTo-Json -Compress -Depth 8
$bytes = [System.Text.Encoding]::UTF8.GetBytes($json)
$Context.Response.StatusCode = $StatusCode
$Context.Response.ContentType = "application/json; charset=utf-8"
$Context.Response.ContentLength64 = $bytes.Length
$Context.Response.OutputStream.Write($bytes, 0, $bytes.Length)
$Context.Response.Close()
}
function Test-AgentRelayRemoteAllowed {
param([System.Net.HttpListenerContext]$Context)
$remote = [string]$Context.Request.RemoteEndPoint.Address
foreach ($prefix in $AllowedRemotePrefixes) {
if ($remote.StartsWith($prefix)) { return $true }
}
return $false
}
function Start-AgentSreAlertInbox {
if (-not (Test-Path $SreAlertInboxScript)) {
return [pscustomobject]@{ started = $false; reason = "sre_alert_inbox_script_missing" }
}
$ps = Join-Path $env:SystemRoot "System32\WindowsPowerShell\v1.0\powershell.exe"
$stamp = Get-Date -Format "yyyyMMdd-HHmmss"
$stdout = Join-Path $LogDir "agent99-sre-alert-relay-trigger-$stamp.out"
$stderr = Join-Path $LogDir "agent99-sre-alert-relay-trigger-$stamp.err"
$args = @(
"-NoProfile",
"-ExecutionPolicy", "Bypass",
"-File", $SreAlertInboxScript,
"-AgentRoot", $AgentRoot,
"-RunNow"
)
$proc = Start-Process -FilePath $ps -ArgumentList $args -PassThru -WindowStyle Hidden -RedirectStandardOutput $stdout -RedirectStandardError $stderr
return [pscustomobject]@{
started = $true
processId = $proc.Id
stdout = $stdout
stderr = $stderr
}
}
if (-not $Prefix.EndsWith("/")) {
$Prefix = "$Prefix/"
}
$listener = [System.Net.HttpListener]::new()
$listener.Prefixes.Add($Prefix)
$startupStamp = Get-Date -Format "yyyyMMdd-HHmmss"
$startupEvidence = Join-Path $EvidenceDir "agent99-SreAlertRelay-start-$startupStamp.json"
@{
ok = $true
prefix = $Prefix
tokenEnv = $TokenEnv
tokenConfigured = [bool](Get-AgentEnvValue $TokenEnv)
allowedRemotePrefixes = $AllowedRemotePrefixes
maxBodyBytes = $MaxBodyBytes
runOnce = [bool]$RunOnce
startedAt = (Get-Date).ToString("o")
} | ConvertTo-Json -Depth 6 | Set-Content -Encoding UTF8 $startupEvidence
try {
$listener.Start()
Write-AgentRelayEvent @{
event = "relay_started"
ok = $true
prefix = $Prefix
tokenConfigured = [bool](Get-AgentEnvValue $TokenEnv)
evidence = $startupEvidence
}
do {
$context = $listener.GetContext()
$remote = [string]$context.Request.RemoteEndPoint.Address
try {
if ($context.Request.HttpMethod -ne "POST") {
Send-AgentRelayResponse $context 405 @{ ok = $false; error = "method_not_allowed" }
Write-AgentRelayEvent @{ event = "relay_rejected"; ok = $false; remote = $remote; reason = "method_not_allowed" }
continue
}
if (-not (Test-AgentRelayRemoteAllowed $context)) {
Send-AgentRelayResponse $context 403 @{ ok = $false; error = "remote_not_allowed" }
Write-AgentRelayEvent @{ event = "relay_rejected"; ok = $false; remote = $remote; reason = "remote_not_allowed" }
continue
}
$expectedToken = Get-AgentEnvValue $TokenEnv
if ($expectedToken) {
$actualToken = [string]$context.Request.Headers["X-Agent99-Relay-Token"]
if ($actualToken -ne $expectedToken) {
Send-AgentRelayResponse $context 401 @{ ok = $false; error = "invalid_relay_token" }
Write-AgentRelayEvent @{ event = "relay_rejected"; ok = $false; remote = $remote; reason = "invalid_relay_token" }
continue
}
}
if ($context.Request.ContentLength64 -gt $MaxBodyBytes) {
Send-AgentRelayResponse $context 413 @{ ok = $false; error = "payload_too_large" }
Write-AgentRelayEvent @{ event = "relay_rejected"; ok = $false; remote = $remote; reason = "payload_too_large" }
continue
}
$reader = [System.IO.StreamReader]::new($context.Request.InputStream, $context.Request.ContentEncoding)
$body = $reader.ReadToEnd()
if ($body.Length -gt $MaxBodyBytes) {
Send-AgentRelayResponse $context 413 @{ ok = $false; error = "payload_too_large" }
Write-AgentRelayEvent @{ event = "relay_rejected"; ok = $false; remote = $remote; reason = "payload_too_large_readback" }
continue
}
$alert = $body | ConvertFrom-Json
$alertId = [string](Get-AgentField $alert "id" ("relay-" + [guid]::NewGuid().ToString("N")))
$safeId = Convert-AgentSafeFileToken $alertId
$fileStamp = Get-Date -Format "yyyyMMdd-HHmmss"
$tmpPath = Join-Path $IncomingDir ".$safeId-$fileStamp-$([guid]::NewGuid().ToString("N")).tmp"
$finalPath = Join-Path $IncomingDir "$safeId-$fileStamp.json"
$alert | ConvertTo-Json -Depth 20 | Set-Content -Encoding UTF8 $tmpPath
Move-Item -Force $tmpPath $finalPath
$trigger = Start-AgentSreAlertInbox
Send-AgentRelayResponse $context 202 @{
ok = $true
alertId = $alertId
incomingFile = $finalPath
inboxTriggered = [bool]$trigger.started
inboxProcessId = $trigger.processId
}
Write-AgentRelayEvent @{
event = "relay_accepted"
ok = $true
remote = $remote
alertId = $alertId
incomingFile = $finalPath
inboxTriggered = [bool]$trigger.started
inboxProcessId = $trigger.processId
}
} catch {
try {
Send-AgentRelayResponse $context 500 @{ ok = $false; error = "relay_exception"; errorType = $_.Exception.GetType().Name }
} catch {
}
Write-AgentRelayEvent @{
event = "relay_exception"
ok = $false
remote = $remote
errorType = $_.Exception.GetType().Name
error = $_.Exception.Message
}
}
} while (-not $RunOnce)
} finally {
if ($listener.IsListening) { $listener.Stop() }
$listener.Close()
}