admin / Synapse-Sonar
publicAttack Surface Simulation
Synapse-Sonar / synapse-sonar / public / agents / windows-agent / sonar-agent.ps1
4911 B · main
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 | <# Synapse Sonar host agent - Windows. Reads this host's established TCP connections via Get-NetTCPConnection and pushes them as flow records to the Synapse Sonar ingestion endpoint. Uses only built-in Windows PowerShell 5.1 cmdlets - no modules to install. Config is read from a JSON file (default %ProgramData%\SonarAgent\config.json): { "SonarUrl": "https://sonar.corp.internal:3000", "IngestKey": "nsx_sensor_xxxx", "IntervalSeconds": 30, "VerifyTls": true } #> [CmdletBinding()] param( [string]$ConfigPath = "$env:ProgramData\SonarAgent\config.json" ) $ErrorActionPreference = 'Stop' function Write-Log { param([string]$Message) $line = "{0} [sonar-agent] {1}" -f (Get-Date -Format o), $Message Write-Output $line try { Add-Content -Path "$env:ProgramData\SonarAgent\sonar-agent.log" -Value $line } catch {} } if (-not (Test-Path $ConfigPath)) { throw "Config not found: $ConfigPath" } $cfg = Get-Content -Raw -Path $ConfigPath | ConvertFrom-Json if (-not $cfg.SonarUrl -or -not $cfg.IngestKey) { throw "SonarUrl and IngestKey are required in $ConfigPath" } $SonarUrl = $cfg.SonarUrl.TrimEnd('/') $IngestKey = $cfg.IngestKey $Interval = if ($cfg.IntervalSeconds) { [int]$cfg.IntervalSeconds } else { 30 } $VerifyTls = if ($null -ne $cfg.VerifyTls) { [bool]$cfg.VerifyTls } else { $true } $Hostname = $env:COMPUTERNAME [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 if (-not $VerifyTls) { # Self-signed certificate support (skip verification). [System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true } } function Get-HostAddresses { # Primary = the IPv4 on the interface with the default route (the host's real # IP). Related = the host's other IPv4s (Hyper-V / WSL / Docker NAT, secondary # NICs) shown against the same node rather than as separate nodes. $all = @(Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue | Where-Object { $_.IPAddress -ne '127.0.0.1' -and $_.IPAddress -notlike '169.254.*' } | Select-Object -ExpandProperty IPAddress) $primary = $null try { $cfg = Get-NetIPConfiguration -ErrorAction SilentlyContinue | Where-Object { $_.IPv4DefaultGateway } | Select-Object -First 1 if ($cfg) { $primary = @($cfg.IPv4Address.IPAddress)[0] } } catch {} if (-not $primary) { $nonNat = $all | Where-Object { $_ -notlike '172.*' } $primary = if ($nonNat) { @($nonNat)[0] } elseif ($all) { @($all)[0] } else { '127.0.0.1' } } $related = @($all | Where-Object { $_ -ne $primary }) return @{ Primary = $primary; Related = $related } } function Get-Flows { $flows = @{} $addr = Get-HostAddresses $primary = $addr.Primary $related = $addr.Related $conns = Get-NetTCPConnection -State Established -ErrorAction SilentlyContinue foreach ($c in $conns) { $remote = [string]$c.RemoteAddress if ([string]::IsNullOrEmpty($remote)) { continue } if ($remote -eq '0.0.0.0' -or $remote -eq '::' -or $remote -eq '::1') { continue } if ($remote.StartsWith('127.') -or $remote.StartsWith('fe80')) { continue } # Lower port number is treated as the "service" port. $port = [Math]::Min([int]$c.LocalPort, [int]$c.RemotePort) # Source is consolidated onto the primary host IP, so key by peer only. $key = "$remote|$port" if ($flows.ContainsKey($key)) { continue } $flow = [ordered]@{ sourceIp = $primary destIp = $remote sourceHostname = $Hostname protocol = 'tcp' port = $port bytes = 0 metadata = @{ collector = 'windows-agent'; host = $Hostname } } # Omit when empty so PowerShell never emits a null; server tolerates a # single string when there is exactly one related address. if ($related.Count -ge 1) { $flow.sourceRelatedIps = $related } $flows[$key] = $flow } return @($flows.Values) } function Push-Flows { param($Flows) if (-not $Flows -or @($Flows).Count -eq 0) { return } $batch = @($Flows | Select-Object -First 1000) # Build the flows array explicitly so a single flow still serializes as [ ... ]. $items = $batch | ForEach-Object { $_ | ConvertTo-Json -Depth 6 -Compress } $body = '{"flows":[' + ($items -join ',') + ']}' try { Invoke-RestMethod -Uri "$SonarUrl/api/ingest/flow-logs" -Method Post ` -ContentType 'application/json' ` -Headers @{ Authorization = "Bearer $IngestKey" } ` -Body $body -TimeoutSec 20 | Out-Null Write-Log "pushed $($batch.Count) flows" } catch { Write-Log "push error: $($_.Exception.Message)" } } Write-Log "starting; target=$SonarUrl interval=${Interval}s host=$Hostname" while ($true) { try { Push-Flows -Flows (Get-Flows) } catch { Write-Log "loop error: $($_.Exception.Message)" } Start-Sleep -Seconds $Interval } |