Files
minerops/agents/windows/MinerOpsApi3EnrichmentPoller.ps1
T
lussierj e898f283ab security: contain physical miner commands in Stage A
Disable physical control through legacy HTTP/M30 producers, typed admission,
queue dispatch and upgraded Windows executors. Restrict diagnostic transports
to exact read-only operations, with ordinal PowerShell validation before sockets.

Retire unsupported pending/claimed work with atomic, idempotent security audits.
Preserve prior delivery evidence and report unknown outcomes after a prior claim,
including explicitly repaired pending rows. Include containment, cancellation,
concurrency and malformed-payload regressions and the old-Agent cutover runbook.

Validation: independent final application-security revalidation PASS. Two unchanged
baseline source-assertion failures are documented as exceptions. Production release
still requires native Windows validation and verified fencing/stop/drain/upgrade;
server cancellation alone cannot revoke a cached command held by an old Agent.
2026-10-15 20:39:45 +00:00

294 lines
8.2 KiB
PowerShell

param(
[int] $Limit = 25,
[int] $TimeoutMs = 2500
)
$ErrorActionPreference = "Stop"
$ConfigPath = "C:\MinerOpsAgent\agent-config.json"
if (-not (Test-Path $ConfigPath)) {
throw "Missing config file: $ConfigPath"
}
$Config = Get-Content $ConfigPath -Raw | ConvertFrom-Json
$HubUrl = [string]$Config.hub_url
$HubUrl = $HubUrl.TrimEnd("/")
$AgentId = [string]$Config.agent_id
$SiteName = [string]$Config.site_name
$Secret = [string]$Config.agent_secret
$Headers = @{
"X-MinerOps-Agent-ID" = $AgentId
"X-MinerOps-Agent-Secret" = $Secret
}
function Invoke-WhatsMinerApi3Read {
param(
[Parameter(Mandatory=$true)] [string] $Ip,
[Parameter(Mandatory=$true)] [hashtable] $Request,
[int] $TimeoutMs = 2500
)
# Exact operation and parameter allowlist at the shared transport boundary.
# No extra keys (including credentials, command aliases, or parameter overrides)
# reach the wire, even when this function is called outside the poller loop.
$readCommands = @("get.device.info", "get.miner.setting", "get.miner.status",
"get.fan.setting", "get.device.custom_data")
$command = $Request["cmd"]
$approvedCommand = $null
if ($command -is [string]) {
foreach ($candidate in $readCommands) {
if ([string]::Equals($candidate, $command, [StringComparison]::Ordinal)) {
$approvedCommand = $candidate
break
}
}
}
if ($null -eq $approvedCommand) {
throw "unsupported_diagnostic_operation_stage_a"
}
$expectedKeys = @("cmd")
$wireRequest = [ordered]@{ cmd = $approvedCommand }
if ([string]::Equals($approvedCommand, "get.miner.status", [StringComparison]::Ordinal)) {
$expectedKeys += "param"
if ($Request["param"] -isnot [string] -or
-not [string]::Equals($Request["param"], "summary+pools+edevs", [StringComparison]::Ordinal)) {
throw "unsupported_diagnostic_operation_stage_a"
}
$wireRequest["param"] = "summary+pools+edevs"
}
if ($Request.Count -ne $expectedKeys.Count) {
throw "unsupported_diagnostic_operation_stage_a"
}
foreach ($key in $Request.Keys) {
$approvedKey = $false
if ($key -is [string]) {
foreach ($expectedKey in $expectedKeys) {
if ([string]::Equals($expectedKey, $key, [StringComparison]::Ordinal)) {
$approvedKey = $true
break
}
}
}
if (-not $approvedKey) {
throw "unsupported_diagnostic_operation_stage_a"
}
}
$client = $null
$stream = $null
try {
$json = ($wireRequest | ConvertTo-Json -Depth 20 -Compress)
$payload = [System.Text.Encoding]::ASCII.GetBytes($json)
$length = [BitConverter]::GetBytes([Int32]$payload.Length)
$client = New-Object System.Net.Sockets.TcpClient
$client.ReceiveTimeout = $TimeoutMs
$client.SendTimeout = $TimeoutMs
$connect = $client.BeginConnect($Ip, 4433, $null, $null)
if (-not $connect.AsyncWaitHandle.WaitOne($TimeoutMs, $false)) {
try { $client.Close() } catch {}
throw "Timed out connecting to $Ip`:4433"
}
$client.EndConnect($connect)
$stream = $client.GetStream()
$stream.ReadTimeout = $TimeoutMs
$stream.WriteTimeout = $TimeoutMs
$stream.Write($length, 0, 4)
$stream.Write($payload, 0, $payload.Length)
$stream.Flush()
$lenBuf = New-Object byte[] 4
$offset = 0
while ($offset -lt 4) {
$read = $stream.Read($lenBuf, $offset, 4 - $offset)
if ($read -le 0) {
throw "No response length received."
}
$offset += $read
}
$responseLength = [BitConverter]::ToInt32($lenBuf, 0)
if ($responseLength -le 0 -or $responseLength -gt 1048576) {
throw "Invalid response length: $responseLength"
}
$buffer = New-Object byte[] $responseLength
$offset = 0
while ($offset -lt $responseLength) {
$read = $stream.Read($buffer, $offset, $responseLength - $offset)
if ($read -le 0) {
throw "Response ended early at $offset of $responseLength bytes."
}
$offset += $read
}
$raw = [System.Text.Encoding]::ASCII.GetString($buffer, 0, $responseLength)
$parsed = $raw | ConvertFrom-Json
return [pscustomobject]@{
ok = $true
parsed = $parsed
error = $null
}
}
catch {
return [pscustomobject]@{
ok = $false
parsed = $null
error = $_.Exception.Message
}
}
finally {
if ($stream) { try { $stream.Close() } catch {} }
if ($client) { try { $client.Close() } catch {} }
}
}
function Invoke-Api3OkCommand {
param(
[string] $Ip,
[hashtable] $Request
)
$result = Invoke-WhatsMinerApi3Read -Ip $Ip -Request $Request -TimeoutMs $TimeoutMs
if (-not $result.ok) {
return $null
}
if ($result.parsed.code -ne 0) {
return $null
}
return $result.parsed
}
Write-Host "Fetching known miners from $HubUrl"
$known = Invoke-RestMethod `
-Uri "$HubUrl/api/agent/known-miners" `
-Headers $Headers `
-Method Get
$miners = @($known.miners)
if ($Limit -gt 0) {
$miners = @($miners | Select-Object -First $Limit)
}
Write-Host "API3 enrichment check count: $($miners.Count)"
$items = New-Object System.Collections.Generic.List[object]
$checked = 0
$api3Ok = 0
$api3Failed = 0
foreach ($miner in $miners) {
$checked += 1
$ip = [string]$miner.ip_address
$mac = [string]$miner.mac_address
if (-not $ip) {
continue
}
Write-Host "[$checked/$($miners.Count)] $ip $mac"
$deviceInfoResult = Invoke-WhatsMinerApi3Read `
-Ip $ip `
-Request @{ cmd = "get.device.info" } `
-TimeoutMs $TimeoutMs
if (-not $deviceInfoResult.ok -or $deviceInfoResult.parsed.code -ne 0) {
$api3Failed += 1
$err = $deviceInfoResult.error
if ($deviceInfoResult.ok) {
$err = "API3 code $($deviceInfoResult.parsed.code): $($deviceInfoResult.parsed.msg)"
}
$items.Add([pscustomobject]@{
ip = $ip
mac = $mac
api3_supported = $false
api3_error = $err
device_info = $null
miner_setting = $null
miner_status = $null
fan_setting = $null
custom_data = $null
})
continue
}
$api3Ok += 1
$minerSetting = Invoke-Api3OkCommand `
-Ip $ip `
-Request @{ cmd = "get.miner.setting" }
$minerStatus = Invoke-Api3OkCommand `
-Ip $ip `
-Request @{ cmd = "get.miner.status"; param = "summary+pools+edevs" }
$fanSetting = Invoke-Api3OkCommand `
-Ip $ip `
-Request @{ cmd = "get.fan.setting" }
$customData = Invoke-Api3OkCommand `
-Ip $ip `
-Request @{ cmd = "get.device.custom_data" }
$items.Add([pscustomobject]@{
ip = $ip
mac = $mac
api3_supported = $true
api3_error = $null
device_info = $deviceInfoResult.parsed
miner_setting = $minerSetting
miner_status = $minerStatus
fan_setting = $fanSetting
custom_data = $customData
})
}
$payload = @{
agent_id = $AgentId
site_name = $SiteName
items = @($items.ToArray())
}
$json = $payload | ConvertTo-Json -Depth 80
Write-Host "Posting API3 enrichment results to hub..."
$result = Invoke-RestMethod `
-Uri "$HubUrl/api/agent/api3-enrichment" `
-Method Post `
-Headers $Headers `
-ContentType "application/json" `
-Body $json
Write-Host ""
Write-Host "Local checked: $checked"
Write-Host "API3 OK: $api3Ok"
Write-Host "API3 failed: $api3Failed"
Write-Host ""
$result | ConvertTo-Json -Depth 20