Public/Send-SignalSyslog.ps1

# PSGuerrilla - Jim Tyler, Microsoft MVP - CC BY 4.0
# https://github.com/jimrtyler/PSGuerrilla | https://creativecommons.org/licenses/by/4.0/
# AI/LLM use: see AI-USAGE.md for required attribution
function Send-SignalSyslog {
    <#
    .SYNOPSIS
        Sends threat alerts to a syslog server in CEF or LEEF format.
    .DESCRIPTION
        Formats PSGuerrilla threat data as CEF (Common Event Format) or LEEF (Log Event Extended Format)
        messages and sends them to a syslog server via UDP or TCP.
    .PARAMETER Server
        Syslog server hostname or IP address.
    .PARAMETER Port
        Syslog server port. Default: 514.
    .PARAMETER Protocol
        Transport protocol: UDP or TCP. Default: UDP.
    .PARAMETER Format
        Message format: CEF or LEEF. Default: CEF.
    .PARAMETER Threats
        Array of threat objects to send.
    .PARAMETER Subject
        Alert subject line for the syslog header.
    .PARAMETER Facility
        Syslog facility code (0-23). Default: 1 (user-level).
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Server,

        [int]$Port = 514,

        [ValidateSet('UDP', 'TCP')]
        [string]$Protocol = 'UDP',

        [ValidateSet('CEF', 'LEEF')]
        [string]$Format = 'CEF',

        [Parameter(Mandatory)]
        [PSCustomObject[]]$Threats,

        [string]$Subject = '[PSGuerrilla] Threat Detection',

        [ValidateRange(0, 23)]
        [int]$Facility = 1
    )

    $results = [System.Collections.Generic.List[PSCustomObject]]::new()

    foreach ($threat in $Threats) {
        # Map threat level to syslog severity
        $syslogSeverity = switch ($threat.ThreatLevel) {
            'CRITICAL' { 2 }  # Critical
            'HIGH'     { 3 }  # Error
            'MEDIUM'   { 4 }  # Warning
            'LOW'      { 6 }  # Informational
            default    { 6 }
        }

        $priority = ($Facility * 8) + $syslogSeverity
        $timestamp = [datetime]::UtcNow.ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
        $hostname = [System.Net.Dns]::GetHostName()

        $email = $threat.Email ?? $threat.UserPrincipalName ?? 'unknown'
        $indicators = ($threat.Indicators -join '; ') -replace '\|', '_'
        $score = [int]($threat.ThreatScore ?? 0)

        if ($Format -eq 'CEF') {
            # CEF:Version|Device Vendor|Device Product|Device Version|Signature ID|Name|Severity|Extension
            $cefSeverity = switch ($threat.ThreatLevel) {
                'CRITICAL' { 10 }
                'HIGH'     { 7 }
                'MEDIUM'   { 4 }
                'LOW'      { 1 }
                default    { 0 }
            }
            $sigId = "PSG-$($threat.ThreatLevel)"
            $name = "$($threat.ThreatLevel) threat: $email" -replace '\|', '_'
            $extension = "src=$email suser=$email cs1=$indicators cs1Label=Indicators cn1=$score cn1Label=ThreatScore"
            $message = "<$priority>$timestamp $hostname CEF:0|PSGuerrilla|ThreatDetection|2.1.0|$sigId|$name|$cefSeverity|$extension"
        } else {
            # LEEF:Version|Vendor|Product|Version|EventID|Extension
            $eventId = "PSG-$($threat.ThreatLevel)"
            $extension = "src=$email`tsuser=$email`tsev=$($threat.ThreatLevel)`tThreatScore=$score`tIndicators=$indicators"
            $message = "<$priority>$timestamp $hostname LEEF:2.0|PSGuerrilla|ThreatDetection|2.1.0|$eventId|$extension"
        }

        try {
            $bytes = [System.Text.Encoding]::UTF8.GetBytes($message)

            if ($Protocol -eq 'UDP') {
                $client = [System.Net.Sockets.UdpClient]::new()
                try {
                    $client.Send($bytes, $bytes.Length, $Server, $Port) | Out-Null
                } finally {
                    $client.Close()
                }
            } else {
                $client = [System.Net.Sockets.TcpClient]::new()
                try {
                    $client.Connect($Server, $Port)
                    $stream = $client.GetStream()
                    # TCP syslog: append newline as message delimiter
                    $tcpBytes = [System.Text.Encoding]::UTF8.GetBytes("$message`n")
                    $stream.Write($tcpBytes, 0, $tcpBytes.Length)
                    $stream.Flush()
                } finally {
                    $client.Close()
                }
            }

            $results.Add([PSCustomObject]@{
                Provider = 'Syslog'
                Success  = $true
                Message  = "Syslog $Format/$Protocol sent to ${Server}:${Port} for $email"
                Error    = $null
            })
        } catch {
            $results.Add([PSCustomObject]@{
                Provider = 'Syslog'
                Success  = $false
                Message  = "Failed to send syslog to ${Server}:${Port} for $email"
                Error    = $_.Exception.Message
            })
        }
    }

    # Return aggregate result
    $anySuccess = @($results | Where-Object Success).Count -gt 0
    return [PSCustomObject]@{
        Provider = 'Syslog'
        Success  = $anySuccess
        Message  = "Syslog: $(@($results | Where-Object Success).Count)/$($results.Count) messages sent via $Format/$Protocol to ${Server}:${Port}"
        Error    = if (-not $anySuccess) { ($results | Where-Object { -not $_.Success } | Select-Object -First 1).Error } else { $null }
        Details  = @($results)
    }
}