Private/Format-LogEntry.ps1
function Protect-Message { param ( [string]$Message ) # Refined sensitive data patterns with boundary-aware redaction $patterns = @( '(?i)\b(password|token|secret|apikey|api_key)\b\s*[:=]?\s*\S+' ) foreach ($pattern in $patterns) { $Message = [regex]::Replace($Message, $pattern, '[REDACTED]', 'IgnoreCase') } return $Message } function Format-WindowsEventMessage { [CmdletBinding()] param ( [Parameter(Mandatory)] [System.Object]$EventObject ) try { # If there's already a formatted message, prefer that if ($EventObject.Message -and $EventObject.Message -notmatch '^\s*<Event xmlns') { return $EventObject.Message } # Handle raw XML event data if ($EventObject.PSObject.Properties['RawLine'] -and $EventObject.RawLine -match '<Event xmlns') { try { [xml]$xmlEvent = $EventObject.RawLine # Extract common event information $eventId = $xmlEvent.Event.System.EventID.'#text' $providerName = $xmlEvent.Event.System.Provider.Name $level = $xmlEvent.Event.System.Level $computer = $xmlEvent.Event.System.Computer # Extract event data parameters $eventData = @() if ($xmlEvent.Event.EventData -and $xmlEvent.Event.EventData.Data) { foreach ($data in $xmlEvent.Event.EventData.Data) { if ($data.'#text') { $eventData += $data.'#text' } } } # Create human-readable message based on common event patterns $humanMessage = Format-EventByIdAndProvider -EventId $eventId -Provider $providerName -EventData $eventData -Computer $computer if ($humanMessage) { return $humanMessage } else { # Fallback: create a basic readable message $dataString = if ($eventData.Count -gt 0) { " Data: $($eventData -join ', ')" } else { "" } return "Event $eventId from $providerName on $computer.$dataString" } } catch { Write-Verbose "Failed to parse XML event data: $($_.Exception.Message)" } } # If we can't format it, return the original message or a fallback if ($EventObject.Message) { return $EventObject.Message } else { return "Event from $($EventObject.ProviderName) (ID: $($EventObject.EventId))" } } catch { Write-Verbose "Error formatting Windows event message: $($_.Exception.Message)" return $EventObject.Message -replace '<[^>]+>', '' # Strip XML tags as fallback } } function Format-EventByIdAndProvider { [CmdletBinding()] param ( [string]$EventId, [string]$Provider, [array]$EventData, [string]$Computer ) # Common Windows Event translations $translations = @{ # Service Control Manager events 'Service Control Manager' = @{ '7040' = "Service '{0}' start type changed from '{1}' to '{2}' (Service name: {3})" '7045' = "Service '{0}' was installed with start type '{1}' and runs as '{2}'" '7034' = "Service '{0}' terminated unexpectedly. This has happened {1} time(s)" '7035' = "Service '{0}' was successfully sent a {1} control" '7036' = "Service '{0}' entered the {1} state" '7030' = "Service '{0}' is configured as an interactive service but the system does not support this" '7009' = "Timeout waiting for service '{0}' to connect" '7000' = "Service '{0}' failed to start due to error: {1}" '7001' = "Service '{0}' depends on service '{1}' which failed to start" '7031' = "Service '{0}' terminated and will restart in {1} milliseconds" } # System events 'Microsoft-Windows-Kernel-General' = @{ '1' = "System time was changed from '{0}' to '{1}'" '12' = "Operating system started at {0}" '13' = "Operating system is shutting down at {0}" '5' = "Access to {0} was denied" '16' = "Registry hive cleanup completed for {0} - updated {1} keys" } # User32 events 'User32' = @{ '1074' = "System shutdown initiated by user '{0}' on computer '{1}'. Reason: {2}" } # Logon/Logoff events (Security log) 'Microsoft-Windows-Security-Auditing' = @{ '4624' = "User '{0}' successfully logged on to '{1}' from '{2}' using logon type {3}" '4625' = "Failed logon attempt for user '{0}' on '{1}' from '{2}'. Reason: {3}" '4634' = "User '{0}' logged off from session {1}" '4648' = "User '{0}' attempted to log on using explicit credentials for '{1}'" '4672' = "Special privileges assigned to user '{0}' for new logon session" '4720' = "User account '{0}' was created by '{1}'" '4726' = "User account '{0}' was deleted by '{1}'" '4740' = "User account '{0}' was locked out" } # Winlogon events 'Microsoft-Windows-Winlogon' = @{ '7001' = "User logon notification (Customer Experience Improvement Program)" '7002' = "User logoff notification (Customer Experience Improvement Program)" '6005' = "The winlogon service was started" '6006' = "The winlogon service was stopped" } # Power/Kernel events 'Microsoft-Windows-Kernel-Power' = @{ '1' = "System is entering sleep state {0}. Sleep reason: {1}" '42' = "System is entering sleep state {0}" '107' = "System resumed from sleep" '109' = "Kernel power event occurred: {0}" '41' = "System rebooted without cleanly shutting down first" } # Application errors 'Application Error' = @{ '1000' = "Application '{0}' version {1} crashed. Faulting module: {2}" '1001' = "Fault bucket for application '{0}', type {1}" } # Windows Update Client 'Microsoft-Windows-WindowsUpdateClient' = @{ '19' = "Windows Update installation completed successfully: {0}" '20' = "Windows Update installation failed: {0}" '25' = "Windows Update installation started: {0}" } # DNS Client 'Microsoft-Windows-DNS-Client' = @{ '1014' = "DNS name resolution failed for '{0}'. Error: {1}" } # Disk events 'Microsoft-Windows-Ntfs' = @{ '55' = "File system corruption detected on volume {0}. Check disk recommended" } # Application crashes 'Application Hang' = @{ '1002' = "Application '{0}' stopped responding and was terminated" } # Windows Error Reporting 'Windows Error Reporting' = @{ '1001' = "Error report generated for crashed application '{0}'" } # Terminal Services 'Microsoft-Windows-TerminalServices-LocalSessionManager' = @{ '21' = "Remote Desktop Services session logon succeeded for user '{0}'" '23' = "Remote Desktop Services session logoff succeeded for user '{0}'" '24' = "Remote Desktop Services session disconnected for user '{0}'" '25' = "Remote Desktop Services session reconnected for user '{0}'" } # Group Policy 'Microsoft-Windows-GroupPolicy' = @{ '1502' = "Group Policy processing completed successfully in {0} seconds" '1503' = "Group Policy processing failed. Error: {0}" } } if ($translations.ContainsKey($Provider) -and $translations[$Provider].ContainsKey($EventId)) { $template = $translations[$Provider][$EventId] try { return $template -f $EventData } catch { # If formatting fails, return the template with available data return "$template (Data: $($EventData -join ', '))" } } return $null } function Format-LogEntry { [CmdletBinding()] param ( [Parameter(Mandatory)] [string]$Line, [string[]]$CustomPatterns = @(), [switch]$Redact ) # --- Custom Pattern Matching --- if ($CustomPatterns.Count -gt 0) { foreach ($pattern in $CustomPatterns) { if ($Line -match $pattern) { if ($matches) { $timestamp = $matches['Time'] $level = if ($matches['Level']) { $matches['Level'] } else { 'Info' } $provider = if ($matches['Source']) { $matches['Source'] } else { 'Unknown' } $message = if ($matches['Message']) { $matches['Message'] } else { $Line } if ($Redact) { $message = Protect-Message -Message $message } return [PSCustomObject]@{ Timestamp = $timestamp Level = $level Provider = $provider Message = $message } } } } } # --- SmartLogAnalyzer Default --- if ($Line -match '^(?<Time>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) \[(?<Level>[^\]]+)\] (?<Provider>[^:]+): (?<Message>.+)$') { if ($matches) { $timestamp = $null try { [datetime]::TryParseExact($matches['Time'], 'yyyy-MM-dd HH:mm:ss', $null, 'None', [ref]$timestamp) | Out-Null } catch { $timestamp = $null } $level = $matches['Level'] $provider = $matches['Provider'] $message = $matches['Message'] if ($Redact) { $message = Protect-Message -Message $message } return [PSCustomObject]@{ Timestamp = $timestamp Level = $level Provider = $provider Message = $message } } } # --- Syslog-like --- if ($Line -match '^(?<Month>\w{3}) +(?<Day>\d{1,2}) (?<Time>\d{2}:\d{2}:\d{2}) (?<Host>\S+) (?<Source>[^:]+): (?<Message>.+)$') { if ($matches) { $timestamp = $null try { $year = (Get-Date).Year $datetime = "$($matches['Month']) $($matches['Day']) $year $($matches['Time'])" [datetime]::TryParseExact($datetime, 'MMM dd yyyy HH:mm:ss', $null, 'None', [ref]$timestamp) | Out-Null } catch { $timestamp = $null } $level = 'Info' $provider = $matches['Source'] $message = $matches['Message'] if ($Redact) { $message = Protect-Message -Message $message } return [PSCustomObject]@{ Timestamp = $timestamp Level = $level Provider = $provider Message = $message } } } # --- No match fallback --- return $null } |