security(discovery): enforce Zone isolation and retire unsafe legacy writers

Bind Agent observations, review, enrichment and telemetry to current Zone
and inventory authority. Preserve immutable MAC ownership and historical
snapshots with stale-result rejection and transactional security audits.

Retire unbound direct/range/scheduled discovery and one/bulk legacy summary
writes in both modes. Require reconciled exact inventory identity or
explicit Zone-scoped IP selection for CSV metadata imports.

Independent Blocker #1 application-security re-audit: PASS on tree
cf863aadc9. Evidence: 141 focused PostgreSQL/HTTP
tests passed; Chromium and PowerShell checks passed. Two documented baseline
source assertions remain unchanged. Production deployment is not approved.
This commit is contained in:
lussierj committed 2026-10-17 00:45:04 +00:00
1 parent e898f283ab
commit 8b0d32cd6b
26 files changed
+3798 -4092

No files matched your search

+15 -1
View File
@@ -2,7 +2,10 @@
[Parameter(Mandatory=$true)]
[string]$IpAddress,
[string]$MinerId = ""
[string]$MinerId = "",
[string]$DiscoveryId = "",
[string]$ObservationVersion = "",
[string]$CommandId = ""
)
$ConfigPath = "C:\MinerOpsAgent\agent-config.json"
@@ -164,6 +167,14 @@ $hubUrl = ([string]$config.hub_url).TrimEnd("/")
$secret = [string]$config.agent_secret
$agentId = [string]$config.agent_id
# Revalidate the claimed command against current Hub authority before miner sockets.
if (!$DiscoveryId -or !$ObservationVersion -or !$CommandId) { throw "discovery_observation_required" }
$target = Invoke-RestMethod -Method Post -Uri "$hubUrl/api/agent/discovery-target" `
-Headers @{ "X-MinerOps-Agent-ID" = $agentId; "X-MinerOps-Agent-Secret" = $secret } `
-ContentType "application/json" -Body (@{ discovery_id=$DiscoveryId; version=$ObservationVersion; command_id=$CommandId } | ConvertTo-Json)
$IpAddress = [string]$target.ip_address
if (!$target.ok -or !$IpAddress) { throw "discovery_target_denied" }
$commands = @(
"summary",
"devs",
@@ -219,6 +230,9 @@ if (!$model) {
$payload = [ordered]@{
ip_address = $IpAddress
miner_id = $MinerId
discovery_id = $DiscoveryId
version = $ObservationVersion
command_id = $CommandId
mac_address = $mac
hostname = $hostname
miner_sn = $minerSn
+23 -2
View File
@@ -58,6 +58,21 @@ function Invoke-AgentApi {
"X-MinerOps-Agent-ID" = [string]$config.agent_id
}
$batchField = $null
if ($Path -eq "/api/agent/miner-snapshots") { $batchField = "miners" }
elseif ($Path -eq "/api/agent/discoveries") { $batchField = "discoveries" }
if ($batchField -and $Body -ne $null -and @($Body[$batchField]).Count -gt 128) {
$items = @($Body[$batchField])
$results = @()
for ($offset = 0; $offset -lt $items.Count; $offset += 128) {
$batch = @{}
foreach ($key in $Body.Keys) { $batch[$key] = $Body[$key] }
$last = [Math]::Min($offset + 127, $items.Count - 1)
$batch[$batchField] = @($items[$offset..$last])
$results += Invoke-AgentApi -Method $Method -Path $Path -Body $batch
}
return @{ ok = $true; batches = $results; processed = $items.Count }
}
if ($Body -ne $null) {
$json = $Body | ConvertTo-Json -Depth 100 -Compress
return Invoke-RestMethod -Method $Method -Uri $uri -Headers $headers -ContentType "application/json" -Body $json -TimeoutSec 180
@@ -252,6 +267,7 @@ function Poll-KnownMiners {
$result = [ordered]@{
miner_id = [string]$Miner.miner_id
miner_version = [string]$Miner.miner_version
ip_address = [string]$Miner.ip_address
mac_address = [string]$Miner.mac_address
room_name = [string]$Miner.room_name
@@ -592,7 +608,7 @@ public static class MinerOpsFastScanner {
}
function Discover-MinersInRanges {
$config = Read-AgentConfig
$config = Invoke-AgentApi -Method "GET" -Path "/api/agent/config"
$ranges = @($config.ip_ranges | Where-Object { $_.enabled -eq $true })
Ensure-FastScannerLoaded
@@ -1015,7 +1031,12 @@ function Process-PendingCommands {
Write-AgentLog "Running Enrich / Match for discovery IP $ipAddress"
$out = powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\MinerOpsAgent\EnrichOneMiner.ps1 -IpAddress $ipAddress 2>&1 | Out-String
$hubConfig = Invoke-AgentApi -Method "GET" -Path "/api/agent/config"
if (-not [string]::Equals([string]$payloadObj.agent_zone_id, [string]$hubConfig.agent_zone_id, [StringComparison]::Ordinal)) {
throw "discovery_command_stale_authority"
}
if (-not $payloadObj.discovery_id -or -not $payloadObj.version) { throw "discovery_observation_required" }
$out = powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\MinerOpsAgent\EnrichOneMiner.ps1 -IpAddress $ipAddress -DiscoveryId ([string]$payloadObj.discovery_id) -ObservationVersion ([string]$payloadObj.version) -CommandId $commandId 2>&1 | Out-String
if ($LASTEXITCODE -ne 0) {
throw "EnrichOneMiner failed for $ipAddress. Output: $out"
+13 -10
View File
@@ -224,7 +224,9 @@ foreach ($miner in $miners) {
}
$items.Add([pscustomobject]@{
ip = $ip
miner_id = [string]$miner.miner_id
miner_version = [string]$miner.miner_version
ip = $ip
mac = $mac
api3_supported = $false
api3_error = $err
@@ -257,6 +259,8 @@ foreach ($miner in $miners) {
-Request @{ cmd = "get.device.custom_data" }
$items.Add([pscustomobject]@{
miner_id = [string]$miner.miner_id
miner_version = [string]$miner.miner_version
ip = $ip
mac = $mac
api3_supported = $true
@@ -275,15 +279,14 @@ $payload = @{
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
$allItems = @($items.ToArray())
$result = @()
for ($offset = 0; $offset -lt $allItems.Count; $offset += 128) {
$last = [Math]::Min($offset + 127, $allItems.Count - 1)
$payload.items = @($allItems[$offset..$last])
$result += Invoke-RestMethod -Uri "$HubUrl/api/agent/api3-enrichment" -Method Post `
-Headers $Headers -ContentType "application/json" -Body ($payload | ConvertTo-Json -Depth 80)
}
Write-Host ""
Write-Host "Local checked: $checked"
+9 -12
View File
@@ -120,6 +120,7 @@ foreach ($miner in $knownMiners) {
if ($pools.Count -gt 0) {
$poolSnapshots.Add([pscustomobject]@{
miner_id = $miner.miner_id
miner_version = $miner.miner_version
ip_address = $ip
mac_address = $miner.mac_address
model = $miner.model
@@ -130,18 +131,14 @@ foreach ($miner in $knownMiners) {
Write-Host "Pool snapshots collected: $($poolSnapshots.Count)"
$body = @{
agent_id = $AgentId
pool_snapshots = $poolSnapshots
} | ConvertTo-Json -Depth 20
$result = Invoke-RestMethod `
-Method Post `
-Uri "$HubUrl/api/agent/pool-snapshots" `
-Headers $Headers `
-Body $body `
-ContentType "application/json" `
-TimeoutSec 180
$allItems = @($poolSnapshots.ToArray())
$result = @()
for ($offset = 0; $offset -lt $allItems.Count; $offset += 128) {
$last = [Math]::Min($offset + 127, $allItems.Count - 1)
$body = @{agent_id=$AgentId; pool_snapshots=@($allItems[$offset..$last])} | ConvertTo-Json -Depth 80
$result += Invoke-RestMethod -Method Post -Uri "$HubUrl/api/agent/pool-snapshots" `
-Headers $Headers -Body $body -ContentType "application/json" -TimeoutSec 180
}
$result | ConvertTo-Json -Depth 10
@@ -0,0 +1,55 @@
param([string]$AgentRoot = (Join-Path $PSScriptRoot '..'))
$ErrorActionPreference = 'Stop'
$checks=0
foreach ($file in @('MinerOpsAgentService.ps1','EnrichOneMiner.ps1','MinerOpsApi3EnrichmentPoller.ps1','MinerOpsPoolPoller.ps1')) {
$tokens=$null; $errors=$null
$ast=[System.Management.Automation.Language.Parser]::ParseFile((Join-Path $AgentRoot $file),[ref]$tokens,[ref]$errors)
if ($errors.Count) { throw "Parse errors in ${file}: $errors" }
$checks++
if ($file -eq 'MinerOpsAgentService.ps1') {
$function=$ast.Find({param($n) $n -is [System.Management.Automation.Language.FunctionDefinitionAst] -and $n.Name -eq 'Invoke-AgentApi'},$true)
Invoke-Expression $function.Extent.Text
}
}
function Read-AgentConfig { return @{hub_url='https://hub.invalid';agent_id='simulated-agent';agent_secret='disposable-only'} }
$script:requests=@()
function Invoke-RestMethod {
param($Method,$Uri,$Headers,$ContentType,$Body,$TimeoutSec)
$parsed=$Body | ConvertFrom-Json
$script:requests+=@{body=$parsed;headers=$Headers}
return @{ok=$true}
}
foreach ($selection in @(@{path='/api/agent/discoveries';field='discoveries'},@{path='/api/agent/miner-snapshots';field='miners'})) {
$script:requests=@();$body=@{agent_id='simulated-agent'}
$body[$selection.field]=@(1..257 | ForEach-Object { @{sample=$_;miner_version='signed-version'} })
$result=Invoke-AgentApi -Method POST -Path $selection.path -Body $body
$sizes=@($script:requests | ForEach-Object { @($_.body.($selection.field)).Count })
if (($sizes -join ',') -ne '128,128,1') { throw "Unsafe batching: $sizes" }
foreach ($request in $script:requests) {
if ($request.headers['X-MinerOps-Agent-ID'] -ne 'simulated-agent') { throw 'Identity lost' }
}
if ($result.processed -ne 257) { throw 'Samples lost' }
$checks+=3
}
# Execute the actual target-validation prefix only; transport functions/loops are never loaded.
$source=Get-Content -LiteralPath (Join-Path $AgentRoot 'EnrichOneMiner.ps1') -Raw
$start=$source.IndexOf('$config = Read-AgentConfig')
$finish=$source.IndexOf('$commands = @(',$start)
$prefix=$source.Substring($start,$finish-$start)
$script:targetRequests=0
function Invoke-RestMethod {
param($Method,$Uri,$Headers,$ContentType,$Body)
$script:targetRequests++
if ($Uri -notlike '*/api/agent/discovery-target') { throw 'Unexpected request' }
return @{ok=$true;ip_address='198.51.100.10'}
}
$DiscoveryId='';$ObservationVersion='';$CommandId='';$IpAddress='203.0.113.99'
$denied=$false
try { Invoke-Expression $prefix } catch { $denied=$_.Exception.Message -eq 'discovery_observation_required' }
if (!$denied -or $script:targetRequests -ne 0) { throw 'Missing identity was not rejected before I/O' }
$checks++
$DiscoveryId='00000000-0000-4000-8000-000000000001';$ObservationVersion='version';$CommandId='command'
Invoke-Expression $prefix
if ($IpAddress -ne '198.51.100.10' -or $script:targetRequests -ne 1) { throw 'Unvalidated target retained' }
$checks++
Write-Output "Discovery PowerShell checks passed: $checks; mocked Hub only; zero miner sockets."