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.
270 lines
7.3 KiB
PowerShell
270 lines
7.3 KiB
PowerShell
param(
|
|
[Parameter(Mandatory=$true)]
|
|
[string]$IpAddress,
|
|
|
|
[string]$MinerId = ""
|
|
)
|
|
|
|
$ConfigPath = "C:\MinerOpsAgent\agent-config.json"
|
|
|
|
function Read-AgentConfig {
|
|
if (!(Test-Path $ConfigPath)) {
|
|
throw "Agent config not found: $ConfigPath"
|
|
}
|
|
|
|
return Get-Content $ConfigPath -Raw | ConvertFrom-Json
|
|
}
|
|
|
|
function Invoke-MinerCommandRaw {
|
|
param(
|
|
[string]$IpAddress,
|
|
[string]$Command,
|
|
[int]$Port = 4028,
|
|
[int]$TimeoutMs = 2500
|
|
)
|
|
|
|
# This transport accepts only the exact queries used by this enrichment script.
|
|
# Validate before creating a socket; callers cannot reinterpret a write as a read.
|
|
$readCommands = @("summary", "devs", "edevs", "stats", "pools", "devdetails",
|
|
"version", "get_version", "get_miner_info")
|
|
# PowerShell containment operators use culture comparisons, even with -c.
|
|
# Match ordinally and serialize the canonical literal; never trim/normalize.
|
|
$approvedCommand = $null
|
|
foreach ($candidate in $readCommands) {
|
|
if ([string]::Equals($candidate, $Command, [StringComparison]::Ordinal)) {
|
|
$approvedCommand = $candidate
|
|
break
|
|
}
|
|
}
|
|
if ($Port -ne 4028 -or $null -eq $approvedCommand) {
|
|
throw "unsupported_diagnostic_operation_stage_a"
|
|
}
|
|
$requestJson = @{ cmd = $approvedCommand } | ConvertTo-Json -Compress
|
|
|
|
$client = $null
|
|
|
|
try {
|
|
$client = New-Object System.Net.Sockets.TcpClient
|
|
$async = $client.BeginConnect($IpAddress, $Port, $null, $null)
|
|
|
|
if (-not $async.AsyncWaitHandle.WaitOne($TimeoutMs)) {
|
|
return ""
|
|
}
|
|
|
|
$client.EndConnect($async)
|
|
$client.ReceiveTimeout = $TimeoutMs
|
|
$client.SendTimeout = $TimeoutMs
|
|
|
|
$stream = $client.GetStream()
|
|
$payload = [System.Text.Encoding]::ASCII.GetBytes($requestJson + "`n")
|
|
$stream.Write($payload, 0, $payload.Length)
|
|
|
|
try {
|
|
$client.Client.Shutdown([System.Net.Sockets.SocketShutdown]::Send)
|
|
} catch {
|
|
}
|
|
|
|
$buffer = New-Object byte[] 262144
|
|
$chunks = New-Object System.Collections.ArrayList
|
|
$started = Get-Date
|
|
|
|
while (((Get-Date) - $started).TotalMilliseconds -lt $TimeoutMs) {
|
|
if (-not $stream.DataAvailable) {
|
|
Start-Sleep -Milliseconds 20
|
|
continue
|
|
}
|
|
|
|
$read = $stream.Read($buffer, 0, $buffer.Length)
|
|
|
|
if ($read -le 0) {
|
|
break
|
|
}
|
|
|
|
for ($i = 0; $i -lt $read; $i++) {
|
|
[void]$chunks.Add([byte]$buffer[$i])
|
|
}
|
|
|
|
if ($chunks.Count -gt 2097152) {
|
|
break
|
|
}
|
|
}
|
|
|
|
return [System.Text.Encoding]::UTF8.GetString([byte[]]$chunks.ToArray([byte])).Trim()
|
|
} catch {
|
|
return ""
|
|
} finally {
|
|
if ($client -ne $null) {
|
|
$client.Close()
|
|
}
|
|
}
|
|
}
|
|
|
|
function Try-ParseJson {
|
|
param([string]$Raw)
|
|
|
|
if (!$Raw -or $Raw.Trim().Length -eq 0) {
|
|
return $null
|
|
}
|
|
|
|
try {
|
|
return $Raw | ConvertFrom-Json
|
|
} catch {
|
|
return $null
|
|
}
|
|
}
|
|
|
|
function Find-FirstValue {
|
|
param(
|
|
[object]$Object,
|
|
[string[]]$Names
|
|
)
|
|
|
|
if ($null -eq $Object) {
|
|
return ""
|
|
}
|
|
|
|
$json = $Object | ConvertTo-Json -Depth 80 -Compress
|
|
|
|
foreach ($name in $Names) {
|
|
$escaped = [regex]::Escape($name)
|
|
|
|
$pattern = '"' + $escaped + '"\s*:\s*"([^"]+)"'
|
|
$m = [regex]::Match($json, $pattern, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
|
|
if ($m.Success) {
|
|
return $m.Groups[1].Value
|
|
}
|
|
|
|
$pattern2 = '"' + $escaped + '"\s*:\s*([0-9A-Za-z\.\-_:]+)'
|
|
$m2 = [regex]::Match($json, $pattern2, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
|
|
if ($m2.Success) {
|
|
return $m2.Groups[1].Value
|
|
}
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
function Find-MacAddress {
|
|
param([string]$Text)
|
|
|
|
if (!$Text) {
|
|
return ""
|
|
}
|
|
|
|
$m = [regex]::Match($Text, '([0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}')
|
|
if ($m.Success) {
|
|
return $m.Value.Replace("-", ":").ToUpper()
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
$config = Read-AgentConfig
|
|
$hubUrl = ([string]$config.hub_url).TrimEnd("/")
|
|
$secret = [string]$config.agent_secret
|
|
$agentId = [string]$config.agent_id
|
|
|
|
$commands = @(
|
|
"summary",
|
|
"devs",
|
|
"edevs",
|
|
"stats",
|
|
"pools",
|
|
"devdetails",
|
|
"version",
|
|
"get_version",
|
|
"get_miner_info"
|
|
)
|
|
|
|
$raws = [ordered]@{}
|
|
$parsed = [ordered]@{}
|
|
|
|
foreach ($cmd in $commands) {
|
|
Write-Host "Querying $IpAddress command: $cmd"
|
|
$raw = Invoke-MinerCommandRaw -IpAddress $IpAddress -Command $cmd -TimeoutMs 2500
|
|
$raws[$cmd] = $raw
|
|
$parsed[$cmd] = Try-ParseJson -Raw $raw
|
|
Write-Host " bytes: $($raw.Length)"
|
|
}
|
|
|
|
$allText = ($raws.GetEnumerator() | ForEach-Object { $_.Value }) -join "`n"
|
|
$allParsed = [ordered]@{
|
|
summary = $parsed["summary"]
|
|
devs = $parsed["devs"]
|
|
edevs = $parsed["edevs"]
|
|
stats = $parsed["stats"]
|
|
pools = $parsed["pools"]
|
|
devdetails = $parsed["devdetails"]
|
|
version = $parsed["version"]
|
|
get_version = $parsed["get_version"]
|
|
get_miner_info = $parsed["get_miner_info"]
|
|
}
|
|
|
|
$mac = Find-FirstValue -Object $allParsed -Names @("MAC", "Mac", "mac", "macaddr", "mac_address", "Net MAC", "ethaddr", "Ethernet MAC")
|
|
if (!$mac) {
|
|
$mac = Find-MacAddress -Text $allText
|
|
}
|
|
|
|
$hostname = Find-FirstValue -Object $allParsed -Names @("Hostname", "hostname", "host", "Host")
|
|
$minerSn = Find-FirstValue -Object $allParsed -Names @("Miner SN", "miner_sn", "miner sn", "Serial", "serial", "SN", "sn", "M_Serial")
|
|
$firmware = Find-FirstValue -Object $allParsed -Names @("Firmware", "firmware", "firmware_version", "Soft Version", "Version", "CompileTime")
|
|
$apiVersion = Find-FirstValue -Object $allParsed -Names @("API", "api", "api_version", "Api Version")
|
|
$platform = Find-FirstValue -Object $allParsed -Names @("Platform", "platform", "Machine", "machine")
|
|
$model = Find-FirstValue -Object $allParsed -Names @("Model", "model", "Miner Type", "miner_type", "Type", "type")
|
|
|
|
if (!$model) {
|
|
$model = "WhatsMiner"
|
|
}
|
|
|
|
$payload = [ordered]@{
|
|
ip_address = $IpAddress
|
|
miner_id = $MinerId
|
|
mac_address = $mac
|
|
hostname = $hostname
|
|
miner_sn = $minerSn
|
|
model = $model
|
|
miner_type = $model
|
|
api_version = $apiVersion
|
|
firmware_version = $firmware
|
|
platform = $platform
|
|
raw_version = @{
|
|
version = $raws["version"]
|
|
get_version = $raws["get_version"]
|
|
}
|
|
raw_miner_info = @{
|
|
devs = $raws["devs"]
|
|
edevs = $raws["edevs"]
|
|
stats = $raws["stats"]
|
|
pools = $raws["pools"]
|
|
devdetails = $raws["devdetails"]
|
|
get_miner_info = $raws["get_miner_info"]
|
|
}
|
|
}
|
|
|
|
Write-Host ""
|
|
Write-Host "Detected:"
|
|
Write-Host " MAC: $mac"
|
|
Write-Host " Hostname: $hostname"
|
|
Write-Host " SN: $minerSn"
|
|
Write-Host " Model: $model"
|
|
Write-Host " Firmware: $firmware"
|
|
Write-Host " API: $apiVersion"
|
|
Write-Host " Platform: $platform"
|
|
Write-Host ""
|
|
|
|
$response = Invoke-RestMethod `
|
|
-Method Post `
|
|
-Uri "$hubUrl/api/agent/miner-enrichment-v3" `
|
|
-Headers @{
|
|
"X-MinerOps-Agent-ID" = $agentId
|
|
"X-MinerOps-Agent-Secret" = $secret
|
|
} `
|
|
-ContentType "application/json" `
|
|
-Body ($payload | ConvertTo-Json -Depth 80)
|
|
|
|
$response | ConvertTo-Json -Depth 20
|
|
|
|
|
|
|
|
|