<# .SYNOPSIS LogSizer for Windows: measures how much log data this machine produces, for SIEM sizing. .DESCRIPTION Copyright (c) 2026 Cyber Affinity LLC. Free to run; see https://www.cyber-affinity.com/terms What it does: - Reads each event log's record count, size and the time span it covers, and works out events per second and bytes per day. - Reads a sample of recent events to measure average event size and the noisiest event IDs. - Reads the audit policy (which audit categories are on). - Listen mode (-Listen): receives syslog over UDP for a set time to measure a firewall or other network device, then stops. What it does not do: - It changes no settings, installs nothing and leaves nothing running. - It makes no network connections. Listen mode only receives on the port you choose. - The summary file holds counts and sizes only: no log messages, user names or IP addresses. Run from an elevated PowerShell (the Security log needs administrator rights). .EXAMPLE .\LogSizer.ps1 Measures this machine and writes -logsizer.json in the current folder (or on the Desktop when run from a system folder). .EXAMPLE .\LogSizer.ps1 -Role workstation Same, with the machine type set by hand (workstation, server, dc). .EXAMPLE .\LogSizer.ps1 -Listen -Port 5514 -Hours 4 -Source firewall -Name FW01 Receives syslog on UDP 5514 for 4 hours, then writes FW01-logsizer.json. #> [CmdletBinding()] param( [ValidateSet("auto", "workstation", "server", "dc")] [string]$Role = "auto", [ValidateRange(100, 20000)] [int]$Sample = 2000, [switch]$Listen, [ValidateRange(1, 65535)] [int]$Port = 5514, [ValidateRange(0.05, 72)] [double]$Hours = 4, [ValidateSet("firewall", "vpn", "proxy", "switch", "dns", "other")] [string]$Source = "firewall", [ValidateLength(0, 60)] [string]$Name = "", [string]$OutDir = "" ) Set-StrictMode -Version 2 $ErrorActionPreference = "Stop" $version = 1 $utf8 = New-Object System.Text.UTF8Encoding($false) if (-not $Name) { $Name = $env:COMPUTERNAME } if (-not $Name) { $Name = [Environment]::MachineName } $safeName = ($Name -replace '[^A-Za-z0-9._-]', '_') # Default: the current folder, unless that's a system folder (an administrator window opens in System32). if (-not $OutDir) { $here = (Get-Location).ProviderPath $OutDir = $here if (-not $here -or ($env:windir -and $here.StartsWith($env:windir, [StringComparison]::OrdinalIgnoreCase))) { $OutDir = [Environment]::GetFolderPath("Desktop") if (-not $OutDir) { $OutDir = $HOME } } } if (-not (Test-Path -LiteralPath $OutDir -PathType Container)) { throw "Output folder not found: $OutDir" } $outFile = Join-Path (Resolve-Path -LiteralPath $OutDir) "$safeName-logsizer.json" function Write-Summary([object]$obj) { $json = $obj | ConvertTo-Json -Depth 6 [System.IO.File]::WriteAllText($outFile, $json, $utf8) Write-Host "" Write-Host "Saved: $outFile" -ForegroundColor Green Write-Host "Open it to see exactly what it contains, then upload it at https://www.cyber-affinity.com/tools/log-estimator" } function Format-Bytes([double]$b) { if ($b -ge 1GB) { return "{0:N2} GB" -f ($b / 1GB) } if ($b -ge 1MB) { return "{0:N1} MB" -f ($b / 1MB) } if ($b -ge 1KB) { return "{0:N1} KB" -f ($b / 1KB) } return "{0:N0} B" -f $b } # ---------------------------------------------------------------- listen mode if ($Listen) { Write-Host "Listening for syslog on UDP port $Port for $Hours hour(s). Press Ctrl+C to stop early; results are still saved." Write-Host "Point the device's syslog output (or a second syslog destination) at this computer's IP, UDP $Port." Write-Host "If nothing arrives, check that Windows Firewall allows inbound UDP $Port." $client = New-Object System.Net.Sockets.UdpClient($Port) $client.Client.ReceiveTimeout = 1000 $endAt = (Get-Date).AddHours($Hours) $started = Get-Date $events = 0L; $bytes = 0L $perMinute = @{} $remote = New-Object System.Net.IPEndPoint([System.Net.IPAddress]::Any, 0) $lastShow = Get-Date try { while ((Get-Date) -lt $endAt) { try { $data = $client.Receive([ref]$remote) $events++; $bytes += $data.Length $key = (Get-Date).ToString("yyyyMMddHHmm") if ($perMinute.ContainsKey($key)) { $perMinute[$key]++ } else { $perMinute[$key] = 1 } } catch [System.Net.Sockets.SocketException] { } if (((Get-Date) - $lastShow).TotalSeconds -ge 30) { $lastShow = Get-Date $secs = [math]::Max(1, ((Get-Date) - $started).TotalSeconds) Write-Host ("{0:HH:mm} {1:N0} events {2} {3:N1} EPS" -f (Get-Date), $events, (Format-Bytes $bytes), ($events / $secs)) } } } finally { $client.Close() $secs = [math]::Max(1, ((Get-Date) - $started).TotalSeconds) $peak = 0 foreach ($v in $perMinute.Values) { if ($v -gt $peak) { $peak = $v } } $result = [ordered]@{ tool = "cyber-affinity-logsizer"; version = $version; kind = "listener" name = $Name; os = "windows"; role = $Source; collectedAt = (Get-Date).ToUniversalTime().ToString("o") listen = [ordered]@{ port = $Port; seconds = [math]::Round($secs); events = $events; bytes = $bytes eps = [math]::Round($events / $secs, 3) peakEps = [math]::Round($peak / 60, 3) avgEventBytes = $(if ($events) { [math]::Round($bytes / $events) } else { 0 }) bytesPerDay = [math]::Round($bytes / $secs * 86400) } } Write-Host "" Write-Host ("Received {0:N0} events, {1} in {2:N1} hours. Average {3:N1} EPS, busiest minute {4:N1} EPS." -f $events, (Format-Bytes $bytes), ($secs / 3600), ($events / $secs), ($peak / 60)) if (-not $events) { Write-Host "Nothing was received. Check the device's syslog destination and the firewall on this computer." -ForegroundColor Yellow } Write-Summary $result } return } # ---------------------------------------------------------------- this machine $isAdmin = $false try { $isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) } catch { } if (-not $isAdmin) { Write-Host "Not running as administrator: the Security log and audit policy will be skipped. Right-click PowerShell and choose Run as administrator for a full reading." -ForegroundColor Yellow } if ($Role -eq "auto") { $os = Get-CimInstance Win32_OperatingSystem $Role = switch ($os.ProductType) { 1 { "workstation" } 2 { "dc" } default { "server" } } } # Channels a SIEM usually collects. Others are skipped unless they have recent activity and are listed here. $channels = @( "Security", "System", "Application", "Microsoft-Windows-Sysmon/Operational", "Microsoft-Windows-PowerShell/Operational", "Windows PowerShell", "Microsoft-Windows-Windows Defender/Operational", "Microsoft-Windows-TaskScheduler/Operational", "Microsoft-Windows-TerminalServices-LocalSessionManager/Operational", "Microsoft-Windows-Windows Firewall With Advanced Security/Firewall", "Microsoft-Windows-WMI-Activity/Operational", "Microsoft-Windows-Bits-Client/Operational", "Microsoft-Windows-DNS-Client/Operational", "Directory Service", "DNS Server", "DFS Replication", "Microsoft-Windows-DNSServer/Analytical", "ForwardedEvents" ) $sources = @() foreach ($ch in $channels) { try { $log = Get-WinEvent -ListLog $ch -ErrorAction Stop } catch { continue } if (-not $log.IsEnabled -or -not $log.RecordCount) { continue } try { $oldest = Get-WinEvent -LogName $ch -Oldest -MaxEvents 1 -ErrorAction Stop $newest = Get-WinEvent -LogName $ch -MaxEvents 1 -ErrorAction Stop } catch { continue } $span = ($newest.TimeCreated - $oldest.TimeCreated).TotalSeconds # A log that has only just started (under 10 minutes of history) can't give a fair rate. if ($span -lt 600) { continue } # Measure event size from a recent sample, as the XML a SIEM agent forwards. $sizes = 0L; $n = 0 $byId = @{} try { foreach ($e in (Get-WinEvent -LogName $ch -MaxEvents $Sample -ErrorAction Stop)) { $len = $utf8.GetByteCount($e.ToXml()) $sizes += $len; $n++ $id = [int]$e.Id if (-not $byId.ContainsKey($id)) { $byId[$id] = @(0, 0L) } $byId[$id][0]++; $byId[$id][1] += $len } } catch { } if (-not $n) { continue } $avg = $sizes / $n $eps = $log.RecordCount / $span $top = @($byId.GetEnumerator() | Sort-Object { $_.Value[1] } -Descending | Select-Object -First 10 | ForEach-Object { [ordered]@{ id = $_.Key; count = $_.Value[0]; share = [math]::Round($_.Value[1] / $sizes, 4); avgBytes = [math]::Round($_.Value[1] / $_.Value[0]) } }) $sources += [ordered]@{ name = $ch; records = [long]$log.RecordCount; spanHours = [math]::Round($span / 3600, 2) fileBytes = [long]$log.FileSize; maxBytes = [long]$log.MaximumSizeInBytes eps = [math]::Round($eps, 4); avgEventBytes = [math]::Round($avg); bytesPerDay = [math]::Round($eps * $avg * 86400) sampled = $n; topIds = $top } Write-Host ("{0,-48} {1,10:N0} events over {2,6:N1} days {3,8:N2} EPS {4,10}/day" -f $ch, $log.RecordCount, ($span / 86400), $eps, (Format-Bytes ($eps * $avg * 86400))) if ($top.Count) { Write-Host (" Top IDs: " + (($top | Select-Object -First 3 | ForEach-Object { "{0} ({1:P0})" -f $_.id, $_.share }) -join ", ")) -ForegroundColor DarkGray } } # Audit policy: which subcategories are on. Needs administrator rights. $audit = @() if ($isAdmin) { try { $csv = & auditpol.exe /get /category:* /r 2>$null | Where-Object { $_ -and $_ -notmatch '^\s*$' } foreach ($row in ($csv | ConvertFrom-Csv)) { $setting = $row.'Inclusion Setting' if ($setting -and $setting -ne "No Auditing") { $audit += [ordered]@{ subcategory = $row.Subcategory; setting = $setting } } } } catch { } } $total = 0.0; $totalEps = 0.0 foreach ($s in $sources) { $total += $s.bytesPerDay; $totalEps += $s.eps } $result = [ordered]@{ tool = "cyber-affinity-logsizer"; version = $version; kind = "host" name = $Name; os = "windows"; role = $Role; collectedAt = (Get-Date).ToUniversalTime().ToString("o") admin = $isAdmin totals = [ordered]@{ eps = [math]::Round($totalEps, 4); bytesPerDay = [math]::Round($total) } sources = $sources auditPolicy = $audit } Write-Host "" Write-Host ("Total: {0:N2} EPS, {1} per day ({2})" -f $totalEps, (Format-Bytes $total), $Role) if (-not $sources.Count) { Write-Host "No event logs with enough history were found." -ForegroundColor Yellow } Write-Summary $result