219 lines
7.4 KiB
PowerShell
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()
|
|
}
|