Response/ConvertTo-SiemEvent.ps1
|
function ConvertTo-SiemEvent { <# .SYNOPSIS Converts security findings into SIEM-ready event strings .DESCRIPTION Transforms finding objects (Severity/Category/Finding/Impact/Recommendation) or any PSCustomObject into newline-delimited JSON (NDJSON) or Common Event Format (CEF) strings suitable for ingestion by SIEM platforms (Splunk, Microsoft Sentinel, QRadar, ArcSight). One line is emitted per input object, so the output can be written directly to a .ndjson or .cef file or streamed to a collector. Objects that do not match the expected finding shape degrade to a generic informational event instead of throwing. Technique ID discovery (MITRE ATT&CK) is a heuristic: technique_id is populated only when the input object exposes a recognizable technique property (TechniqueId, Technique, MitreTechnique, etc.). The CEF 'act' field is also heuristic: 'Remediate' for High/Critical severity, 'Review' otherwise. Timestamps reflect the UTC time of conversion, not the time the finding was originally detected. .PARAMETER InputObject Finding object(s) to convert. Accepts pipeline input. .PARAMETER Format Output format: NDJSON (one compact JSON object per line) or CEF (CEF:0 string). .PARAMETER HostName Host value embedded in each event. Defaults to the local machine name. .PARAMETER ProductVersion Product version string embedded in each event. Defaults to '1.4.0'. .EXAMPLE $report.Findings | ConvertTo-SiemEvent -Format NDJSON | Out-File findings.ndjson -Encoding UTF8 .EXAMPLE ConvertTo-SiemEvent -InputObject $finding -Format CEF -HostName "WS01.contoso.com" .OUTPUTS String. One NDJSON line or CEF string per input object. #> [CmdletBinding()] param( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [AllowNull()] [object]$InputObject, [Parameter(Mandatory = $true)] [ValidateSet('NDJSON', 'CEF')] [string]$Format, [Parameter()] [string]$HostName = [Environment]::MachineName, [Parameter()] [string]$ProductVersion = '1.4.0' ) begin { Write-Host "Converting findings to $Format events (host: $HostName)..." -ForegroundColor Cyan if ([string]::IsNullOrWhiteSpace($HostName)) { $HostName = 'unknown-host' } $severityMap = @{ 'Critical' = 10 'High' = 7 'Medium' = 5 'Low' = 3 'Info' = 1 } function Get-ObjectPropertyValue { param( [object]$Object, [string[]]$Names ) if ($null -eq $Object) { return $null } foreach ($name in $Names) { $prop = $Object.PSObject.Properties[$name] if ($null -ne $prop -and $null -ne $prop.Value) { return $prop.Value } } return $null } function Get-SeverityNumber { param( [string]$SeverityName, [hashtable]$Map ) foreach ($key in @($Map.Keys)) { if ($key -ieq $SeverityName) { return $Map[$key] } } return 1 } function ConvertTo-CefHeaderEscaped { param([AllowNull()][object]$Value) if ($null -eq $Value) { return '' } $text = [string]$Value $text = $text -replace '\\', '\\' -replace '\|', '\|' return ($text -replace "[`r`n]", ' ') } function ConvertTo-CefExtensionEscaped { param([AllowNull()][object]$Value) if ($null -eq $Value) { return '' } $text = [string]$Value $text = $text -replace '\\', '\\' -replace '=', '\=' return ($text -replace "`r", '\r' -replace "`n", '\n') } } process { try { $severity = Get-ObjectPropertyValue -Object $InputObject -Names @('Severity') $category = Get-ObjectPropertyValue -Object $InputObject -Names @('Category') $message = Get-ObjectPropertyValue -Object $InputObject -Names @('Finding', 'Message', 'Description', 'Name', 'Title') $impact = Get-ObjectPropertyValue -Object $InputObject -Names @('Impact') $recommendation = Get-ObjectPropertyValue -Object $InputObject -Names @('Recommendation', 'Remediation') $techniqueId = Get-ObjectPropertyValue -Object $InputObject -Names @('TechniqueId', 'TechniqueID', 'Technique', 'MitreTechnique', 'MITRETechnique', 'AttackTechnique') if ($null -eq $InputObject) { Write-Warning "Null input object received; emitting generic event." } if ($null -eq $severity) { $severity = 'Info' } if ($null -eq $category) { $category = 'Generic' } if ($null -eq $message) { if ($null -ne $InputObject) { $message = "$InputObject" if ([string]::IsNullOrWhiteSpace($message)) { $message = ($InputObject | ConvertTo-Json -Compress -Depth 3) } } if ([string]::IsNullOrWhiteSpace([string]$message) -or $message -eq '{}') { $message = 'Unspecified event' } } $severityNumber = Get-SeverityNumber -SeverityName ([string]$severity) -Map $severityMap $action = 'Review' if ($severityNumber -ge 7) { $action = 'Remediate' } $timestampUtc = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') if ($Format -eq 'NDJSON') { $eventData = [ordered]@{ timestamp = $timestampUtc host = $HostName product = 'WindowsSecurityAudit' severity = [string]$severity category = [string]$category message = [string]$message impact = $(if ($null -ne $impact) { [string]$impact } else { '' }) recommendation = $(if ($null -ne $recommendation) { [string]$recommendation } else { '' }) } if ($null -ne $techniqueId) { $eventData['technique_id'] = [string]$techniqueId } Write-Output ($eventData | ConvertTo-Json -Compress) } else { $header = 'CEF:0|{0}|{1}|{2}|{3}|{4}|{5}' -f ` 'okanyildiz', ` 'WindowsSecurityAudit', ` (ConvertTo-CefHeaderEscaped $ProductVersion), ` (ConvertTo-CefHeaderEscaped $category), ` (ConvertTo-CefHeaderEscaped $message), ` $severityNumber $extension = 'rt={0} shost={1} act={2} cs1={3} cs1Label=Impact cs2={4} cs2Label=Recommendation' -f ` $timestampUtc, ` (ConvertTo-CefExtensionEscaped $HostName), ` (ConvertTo-CefExtensionEscaped $action), ` (ConvertTo-CefExtensionEscaped $impact), ` (ConvertTo-CefExtensionEscaped $recommendation) if ($null -ne $techniqueId) { $extension += ' cs3={0} cs3Label=TechniqueId' -f (ConvertTo-CefExtensionEscaped $techniqueId) } Write-Output ($header + '|' + $extension) } } catch { Write-Warning "Failed to convert object to $Format event: $_" } } end { } } |