Catalyst / admin/Synapse-Sonar 14.8 GB / 57.8 GB 40.0 GB free
Help Sign in

admin / Synapse-Sonar

public

Attack Surface Simulation

Code Issues Pull requests Pipelines Packages Security Insights Wiki Settings
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
}