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:
1 parent
e898f283ab
commit
8b0d32cd6b
26 files changed
+3798
-4092
No files matched your search
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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."
|
||||
Reference in new issue
Block a user