299 lines
8.5 KiB
PowerShell
299 lines
8.5 KiB
PowerShell
# End-to-end test for the cloud agent against the local mock worker.
|
|
#
|
|
# powershell -ExecutionPolicy Bypass -File test-cloud.ps1
|
|
#
|
|
# Requests are HMAC-SHA256 signed exactly like the agent does it.
|
|
# Exercises: PING, STATUS, DOWNLOAD, DEPLOY, bad-hash rejection, STOP.
|
|
|
|
param(
|
|
[int]$Port = 8790,
|
|
[string]$Secret = "testsecret",
|
|
[string]$AgentId = "clitest",
|
|
[string]$OperatorKey = "testop",
|
|
[string]$Exe = "$PSScriptRoot\x64\Release\ArDrvLoader.exe"
|
|
)
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
|
|
if (-not (Test-Path -LiteralPath $Exe))
|
|
{
|
|
throw "Agent binary not found: $Exe (build Release first)"
|
|
}
|
|
|
|
$base = "http://127.0.0.1:$Port"
|
|
$payloadDir = Join-Path $env:TEMP "ardrv-test-payloads"
|
|
$downloadDir = Join-Path $env:TEMP "ardrv-test-downloads"
|
|
|
|
Remove-Item -Recurse -Force $payloadDir, $downloadDir -ErrorAction SilentlyContinue
|
|
New-Item -ItemType Directory -Force -Path $payloadDir, $downloadDir | Out-Null
|
|
|
|
$payloadPath = Join-Path $payloadDir "hello.cmd"
|
|
"@echo off`r`necho deployed-by-cloud-agent" | Set-Content -LiteralPath $payloadPath -Encoding ASCII
|
|
|
|
function Get-Sha256Hex([byte[]]$Bytes)
|
|
{
|
|
$sha = [System.Security.Cryptography.SHA256]::Create()
|
|
|
|
try
|
|
{
|
|
if ($null -eq $Bytes -or $Bytes.Length -eq 0)
|
|
{
|
|
$stream = New-Object System.IO.MemoryStream
|
|
}
|
|
else
|
|
{
|
|
$stream = New-Object System.IO.MemoryStream(, $Bytes)
|
|
}
|
|
|
|
return ([BitConverter]::ToString($sha.ComputeHash($stream)) -replace "-", "").ToLower()
|
|
}
|
|
finally { $sha.Dispose() }
|
|
}
|
|
|
|
function Get-HmacHex([string]$Key, [string]$Message)
|
|
{
|
|
$hmac = New-Object System.Security.Cryptography.HMACSHA256
|
|
try
|
|
{
|
|
$hmac.Key = [System.Text.Encoding]::UTF8.GetBytes($Key)
|
|
$hash = $hmac.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($Message))
|
|
return ([BitConverter]::ToString($hash) -replace "-", "").ToLower()
|
|
}
|
|
finally { $hmac.Dispose() }
|
|
}
|
|
|
|
function Invoke-Worker([string]$Path, [string]$Method = "GET", [string]$Body = "")
|
|
{
|
|
$bodyBytes = if ($Body) { [System.Text.Encoding]::UTF8.GetBytes($Body) } else { [byte[]]@() }
|
|
$ts = [string][DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
|
|
$canonicalPath = $Path.TrimStart("/")
|
|
$canonical = "$AgentId`n$ts`n$Method`n$canonicalPath`n$(Get-Sha256Hex $bodyBytes)"
|
|
$sign = Get-HmacHex $Secret $canonical
|
|
|
|
$headers = @{
|
|
"X-Agent-Id" = $AgentId
|
|
"X-Agent-Ts" = $ts
|
|
"X-Agent-Sign" = $sign
|
|
}
|
|
|
|
$uri = "$base$Path"
|
|
|
|
if ($Method -eq "POST")
|
|
{
|
|
return Invoke-WebRequest -Method Post -Uri $uri -Body ([byte[]]$bodyBytes) -ContentType "application/octet-stream" -Headers $headers -UseBasicParsing
|
|
}
|
|
|
|
return Invoke-WebRequest -Method Get -Uri $uri -Headers $headers -UseBasicParsing
|
|
}
|
|
|
|
function Decode-Last([string]$Value)
|
|
{
|
|
if (-not $Value) { return "" }
|
|
|
|
try
|
|
{
|
|
return [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($Value))
|
|
}
|
|
catch
|
|
{
|
|
return $Value
|
|
}
|
|
}
|
|
|
|
function Send-Command([string]$Command, [int]$TimeoutSec = 45)
|
|
{
|
|
# Enqueue through the operator API, then poll that exact command id.
|
|
# Waiting on "last changed" is flaky: identical outputs look like timeouts.
|
|
$bodyBytes = [System.Text.Encoding]::UTF8.GetBytes((@{ agent = $AgentId; text = $Command } | ConvertTo-Json -Compress))
|
|
$res = Invoke-WebRequest -Method Post -Uri "$base/api/enqueue" -Body $bodyBytes `
|
|
-ContentType "application/json" -Headers @{ "X-Operator-Key" = $OperatorKey } -UseBasicParsing
|
|
|
|
$id = ($res.Content | ConvertFrom-Json).id
|
|
|
|
$deadline = (Get-Date).AddSeconds($TimeoutSec)
|
|
|
|
while ((Get-Date) -lt $deadline)
|
|
{
|
|
Start-Sleep -Milliseconds 400
|
|
|
|
$list = (Invoke-WebRequest -Uri "$base/api/commands?agent=$AgentId&limit=30" `
|
|
-Headers @{ "X-Operator-Key" = $OperatorKey } -UseBasicParsing).Content | ConvertFrom-Json
|
|
|
|
$cmd = @($list.commands | Where-Object { $_.id -eq $id }) | Select-Object -First 1
|
|
|
|
if ($cmd -and $cmd.state -eq "done")
|
|
{
|
|
return [string]$cmd.result
|
|
}
|
|
}
|
|
|
|
return "<timeout>"
|
|
}
|
|
|
|
Write-Host "starting mock worker..."
|
|
$mockArgs = @(
|
|
"-ExecutionPolicy", "Bypass",
|
|
"-File", "$PSScriptRoot\cloudflare\mock-worker.ps1",
|
|
"-Port", "$Port",
|
|
"-Secret", $Secret,
|
|
"-FileDir", $payloadDir
|
|
)
|
|
|
|
$mock = Start-Process -FilePath "powershell.exe" -ArgumentList $mockArgs -PassThru -WindowStyle Hidden
|
|
Start-Sleep -Seconds 2
|
|
|
|
Write-Host "starting agent..."
|
|
$agentArgs = @(
|
|
"-cloud",
|
|
"--agent", $AgentId,
|
|
"--url", $base,
|
|
"--secret", $Secret,
|
|
"--interval", "1000",
|
|
"--no-start-driver",
|
|
"--elevated",
|
|
"--allow-vm"
|
|
)
|
|
|
|
$agent = Start-Process -FilePath $Exe -ArgumentList $agentArgs -PassThru -WindowStyle Hidden
|
|
Start-Sleep -Seconds 2
|
|
|
|
$results = [ordered]@{}
|
|
|
|
try
|
|
{
|
|
Write-Host "`nPING ->" -NoNewline
|
|
$results["PING"] = Send-Command "PING"
|
|
Write-Host " $($results['PING'])"
|
|
|
|
Write-Host "STATUS ->" -NoNewline
|
|
$results["STATUS"] = Send-Command "STATUS"
|
|
Write-Host " $($results['STATUS'])"
|
|
|
|
$dlTarget = Join-Path $downloadDir "hello.cmd"
|
|
Write-Host "DOWNLOAD ->" -NoNewline
|
|
$results["DOWNLOAD"] = Send-Command "DOWNLOAD $base/file/hello.cmd `"$dlTarget`""
|
|
Write-Host " $($results['DOWNLOAD'])"
|
|
|
|
Write-Host "BADHASH ->" -NoNewline
|
|
$results["BADHASH"] = Send-Command "DOWNLOAD $base/file/hello.cmd `"$downloadDir\bad.cmd`" 0000000000000000000000000000000000000000000000000000000000000000"
|
|
Write-Host " $($results['BADHASH'])"
|
|
|
|
Write-Host "DEPLOY ->" -NoNewline
|
|
$results["DEPLOY"] = Send-Command "DEPLOY $base/file/hello.cmd"
|
|
Write-Host " $($results['DEPLOY'])"
|
|
|
|
Write-Host "HOST_LIST ->" -NoNewline
|
|
$results["HOST_LIST"] = Send-Command "HOST_LIST"
|
|
Write-Host " $($results['HOST_LIST'].Split("`n")[0])"
|
|
|
|
Write-Host "HOST_TRACK ->" -NoNewline
|
|
$results["HOST_TRACK"] = Send-Command "HOST_TRACK"
|
|
Write-Host " $($results['HOST_TRACK'])"
|
|
|
|
Write-Host "STAGE_STATUS ->" -NoNewline
|
|
$results["STAGE_STATUS"] = Send-Command "STAGE_STATUS"
|
|
Write-Host " $($results['STAGE_STATUS'])"
|
|
|
|
Write-Host "EXCLUSION_LIST ->" -NoNewline
|
|
$results["EXCLUSION_LIST"] = Send-Command "EXCLUSION_LIST"
|
|
Write-Host " $(($results['EXCLUSION_LIST'] -split "`n")[0])"
|
|
|
|
Write-Host "DRIVER_STATUS ->" -NoNewline
|
|
$results["DRIVER_STATUS"] = Send-Command "DRIVER_STATUS"
|
|
Write-Host " $($results['DRIVER_STATUS'])"
|
|
|
|
Write-Host "STOP ->" -NoNewline
|
|
$results["STOP"] = Send-Command "STOP"
|
|
Write-Host " $($results['STOP'])"
|
|
|
|
# Unsigned request must be rejected by the worker.
|
|
try
|
|
{
|
|
$null = Invoke-WebRequest -Method Get -Uri "$base/status" -UseBasicParsing
|
|
$results["UNSIGNED"] = "ACCEPTED (bad)"
|
|
}
|
|
catch
|
|
{
|
|
$results["UNSIGNED"] = "403 rejected"
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
Wait-Process -Id $agent.Id -Timeout 10 -ErrorAction SilentlyContinue
|
|
Stop-Process -Id $agent.Id -Force -ErrorAction SilentlyContinue
|
|
Stop-Process -Id $mock.Id -Force -ErrorAction SilentlyContinue
|
|
}
|
|
|
|
$downloaded = Test-Path -LiteralPath $dlTarget
|
|
$badExists = Test-Path -LiteralPath (Join-Path $downloadDir "bad.cmd")
|
|
|
|
$deployPath = ""
|
|
if ($results["DEPLOY"] -match "^OK (?:SYSTEM )?(.+)$") { $deployPath = $matches[1] }
|
|
$deployed = $deployPath -and (Test-Path -LiteralPath $deployPath)
|
|
|
|
Write-Host "`n--- summary ---"
|
|
$results.GetEnumerator() | ForEach-Object { "{0,-9} {1}" -f $_.Key, $_.Value }
|
|
"download file exists : $downloaded"
|
|
"bad-hash file removed : $(-not $badExists)"
|
|
"deployed file exists : $deployed"
|
|
|
|
$failed = @()
|
|
|
|
foreach ($key in @("PING", "STATUS", "DOWNLOAD", "DEPLOY", "STOP"))
|
|
{
|
|
if ($results[$key] -match "^(ERR|<timeout>)")
|
|
{
|
|
$failed += "$key failed"
|
|
}
|
|
}
|
|
|
|
if (-not ([string]$results["HOST_LIST"]).Contains("hosts="))
|
|
{
|
|
$failed += "host list failed"
|
|
}
|
|
|
|
if (-not (([string]$results["HOST_TRACK"]).Contains("alive=") -or ([string]$results["HOST_TRACK"]).Contains("track=none")))
|
|
{
|
|
$failed += "host track failed"
|
|
}
|
|
|
|
if (-not ([string]$results["STAGE_STATUS"]).Contains("alive="))
|
|
{
|
|
$failed += "stage status failed"
|
|
}
|
|
|
|
if (-not ([string]$results["EXCLUSION_LIST"]).Contains("defender="))
|
|
{
|
|
$failed += "exclusion list failed"
|
|
}
|
|
|
|
if (-not ([string]$results["DRIVER_STATUS"]).Contains("fail="))
|
|
{
|
|
$failed += "driver status failed"
|
|
}
|
|
|
|
if ($results["BADHASH"] -notmatch "^ERR DOWNLOAD hash mismatch")
|
|
{
|
|
$failed += "bad-hash not rejected"
|
|
}
|
|
|
|
if ($results["UNSIGNED"] -notmatch "403")
|
|
{
|
|
$failed += "unsigned request accepted"
|
|
}
|
|
|
|
if ($failed)
|
|
{
|
|
Write-Host "`nFAIL: $($failed -join '; ')"
|
|
exit 1
|
|
}
|
|
|
|
Write-Host "`nPASS"
|
|
exit 0
|
|
|
|
|
|
|
|
|
|
|
|
|