LogSlothParser.psm1
Enum LogType { CSV TSV SCCM SCCMSimple MECM W3CExtended Nothing } Enum LogSlothExportType { HTML } [Flags()] Enum SanitizeType { None = 0 guid = 1 hash64 = 2 ipv4 = 4 sid = 8 urlHost = 16 cmDistributionPoint = 32 cmAdvertisementId = 64 cmPackageId = 128 cmProgramId = 256 cmMachineName = 512 cmSiteCode = 1024 cmADSite = 2048 cmServerName = 4096 cmDatabaseName = 8192 cmAll = 16352 all = 2147483647 } Class LogSloth { [LogType]$LogType = [LogType]::Nothing [SanitizeType]$SanitizeType = [SanitizeType]::None [System.Collections.ArrayList]$SanitizedReplacements = @() [System.Collections.ArrayList]$LogData = @() [String]$LogDataUnsanitized = $null [String]$LogDataRaw = $null [System.Collections.ArrayList]$LogFormatting [Boolean] IsSanitized() { Return $($this.SanitizeType -ne [SanitizeType]::None) } } Class LogSlothFormatting { [System.Text.RegularExpressions.RegEx]$Lookup [System.Drawing.Color]$TextColor [System.Drawing.Color]$BackgroundColor } Function SanitizeByMatch { Param( [string]$inputData, [string]$rx, [string]$stub, [switch]$quoted = $false ) Write-Verbose "Private SanitizeByMatch Function is beginning" Write-Verbose "Initalizing RegEx and building Replacement ArrayList" $rxLookup = [regex]::new($rx) Write-Verbose "Getting Matches for Input Data" $matchList = $rxLookup.matches($inputData) Write-Verbose "Reducing to Unique List of Matches" $uniqueMatchList = New-Object -TypeName System.Collections.Generic.HashSet[String] [void]$matchList.ForEach{ $uniqueMatchList.Add($_.Groups[1].Value) } Write-Verbose "Completed Getting Matches for Input Data" $replList = [System.Collections.ArrayList]::New() Write-Verbose "Looping Over Unique Replacement Array to gather list of Text Strings to Replace" $index = 0 ForEach($m in $uniqueMatchList | Where-Object { $_ -ne "" }) { $index++ $replText = "$stub$index" if($quoted) { $replText = "`"$replText`""} Write-Verbose "... Adding '$m' => '$replText' using stub '$stub'" $replList.Add( [PSCustomObject]@{ "OriginalText" = $m "ReplacementText" = $replText "Stub" = $stub } ) | Out-Null } Write-Verbose "Private SanitizeByMatch Function is done" Return $replList } Function Test-FormatRule { [CmdLetBinding()] Param( $Rule ) Write-Verbose "Testing Formatting Rule" If(-Not($rule.Lookup)) { Write-Verbose "Missing Lookup Value, returning False" Return $false } If(-not($Rule.TextColor) -and -not($Rule.BackgroundColor)) { Write-Verbose "One of TextColor or BackgroundColor is required, returning false" Return $false } Try { $x = [regex]$Rule.Lookup Write-Verbose "RegEx Test Passed" } Catch { Write-Verbose "Lookup is not valid regex, returning false" Return $false } If($rule.TextColor) { Try { $x = [System.Drawing.Color]$Rule.TextColor Write-Verbose "TextColor Looks OK" } Catch { Write-Verbose "TextColor is not valid System.Drawing.Color, returning false" Return $false } } If($rule.BackgroundColor) { Try { $x = [System.Drawing.Color]$Rule.BackgroundColor Write-Verbose "BackgroundColor Looks OK" } Catch { Write-Verbose "BackgroundColor is not valid System.Drawing.Color, returning false" Return $false } } Write-Verbose "All checks passed, returning True" Return $true } Function Get-LogSlothType { [Cmdletbinding()] Param( [Parameter(Mandatory=$true, ValueFromPipeline=$true, ParameterSetName = "LogData")] [String]$LogData, [Parameter(Mandatory=$true, ValueFromPipeline=$false, ParameterSetName = "LogFile")] [System.IO.FileInfo]$LogFile, [switch]$SkipWarning ) Write-Verbose "Get-LogSlothType Function is beginning" If(-Not($skipWarning)) { Write-Warning "LogSlothParser 0.2 is Currently in Beta and may not function at 100% (Get-LogSlothType)" } If($logFile) { Try { Write-Verbose "LogFile Parameter Defined, Importing $logFile" $logData = Get-Content $logFile -Raw -ErrorAction Stop } Catch { Throw "Error reading $logFile $($_.Exception.Message)" } } Write-Verbose "Initalizing RegEx Checks" $rxSCCM = [regex]::new('<!\[LOG') $rxSCCMSimple = [regex]::new('(?msi).*? \$\$<.*?><.*?>') $rxW3CExtended = [regex]::new('(?msi)^#Software.*?^#Fields: ') $firstLineOfData = $($logData -split "`n") | Where-Object { $_ -notlike "ROLLOVER*" } | Select-Object -First 1 Write-Verbose "Using RegEx to Determine Log Type" Switch ($logData) { # SCCM { $rxSCCM.IsMatch($firstLineOfData) } { Write-Verbose "RegEx Confirmation that Log is SCCM. Returning." Return [LogType]::SCCM; break } # SCCM Simple { $rxSCCMSimple.IsMatch($firstLineOfData) } { Write-Verbose "RegEx Confirmation that Log is SCCM Simple. Returning." Return [LogType]::SCCMSimple; break } # W3C Extended { $rxW3CExtended.IsMatch($logData) } { Write-Verbose "RegEx Confirmation that Log is W3CExtended. Returning." Return [LogType]::W3CExtended; break } } # Not a pre-defined type, let's make some best guesses Try { Write-Verbose "Converting Log Data to CSV" $csv = $logData | ConvertFrom-csv Write-Verbose "Conversion to CSV was successful" } Catch { Write-Verbose "Failed to Convert Log to CSV $($_.Exception.Message)" $csv = $null } Try { Write-Verbose "Converting Log Data to TSV" $tsv = $logData | ConvertFrom-Csv -Delimiter "`t" Write-Verbose "Conversion to TSV was successful" } Catch { Write-Verbose "Failed to Convert Log to TSV $($_.Exception.Message)" $tsv = $null } if($csv -and $tsv) { $csvItems = $csv[0].PSObject.Members.Where{$_.MemberType -eq "NoteProperty"}.Count $tsvItems = $tsv[0].PSObject.Members.Where{$_.MemberType -eq "NoteProperty"}.Count if($csvItems -gt 1 -and $csvItems -ge $tsvItems) { Write-Verbose "There are equal or more properties in the CSV than TSV, selecting CSV as Winner" Return [LogType]::CSV } elseif($tsvItems -gt 1) { Write-Verbose "There are more properties in the TSV than CSV, selecting TSV as Winner" Return [LogType]::TSV } else { Write-Verbose "There is no clear winner between CSV and TSV, cannot select Winner" } } Write-Verbose "Could not find a match, Returning 'Nothing'" Return [LogType]::Nothing } Function Import-LogSloth { [CmdletBinding()] param ( [Parameter(Mandatory=$true, ValueFromPipeline=$true, ParameterSetName = "LogData")] [String]$LogData, [Parameter(Mandatory=$true, ValueFromPipeline=$false, ParameterSetName = "LogFile")] [System.IO.FileInfo]$LogFile, [Array]$Headers = @(), [System.Collections.ArrayList]$LogFormatting, [Switch]$SkipFormatting, [switch]$SkipWarning ) Write-Verbose "Import-LogSloth Function is beginning" If(-Not($skipWarning)) { Write-Warning "LogSlothParser 0.2 is Currently in Beta and may not function at 100% (Import-LogSloth)" } If($logFile) { Try { Write-Verbose "LogFile Parameter Defined, Importing $logFile" $logData = Get-Content $logFile -Raw -ErrorAction Stop } Catch { Throw "Error reading $logFile. $($_.Exception.Message)" } } $log = [LogSloth]::New() $logData = $logData.Trim() $log.LogDataRaw = $LogData Write-Verbose "Getting Log Type using Get-LogSlothType Function" $log.logType = Get-LogSlothType -LogData $logData -skipWarning Write-Verbose "Detected log type is $($log.logType)" if($log.logType -eq [LogType]::Nothing) { Throw "Cannot determine type of log to import" } $oLog = [System.Collections.ArrayList]::New(@()) Switch ($log.logType) { "SCCM" { Write-Verbose "Importing SCCM Log using Import-LogSCCM Private Function" [System.Collections.ArrayList]$oLog = Import-LogSCCM -logData $logData } "SCCMSimple" { Write-Verbose "Importing SCCM Simple Log using Import-LogSCCMSimple Private Function" [System.Collections.ArrayList]$oLog = Import-LogSCCMSimple -logData $logData } "W3CExtended" { Write-Verbose "Importing W3C Extended Log using Import-LogW3CExtended Private Function" [System.Collections.ArrayList]$oLog = Import-LogW3CExtended -logData $logData } "CSV" { Write-Verbose "Importing CSV using Built-in PowerShell Function" $ConvertParams = @{ InputObject = $logData Delimiter = "," } if($headers) { $ConvertParams.Add("Header",$headers) } [System.Collections.ArrayList]$oLog = ConvertFrom-Csv @ConvertParams } "TSV" { Write-Verbose "Importing TSV using Built-in PowerShell Function" $ConvertParams = @{ InputObject = $logData Delimiter = "`t" } if($headers) { $ConvertParams.Add("Header",$headers) } [System.Collections.ArrayList]$oLog = ConvertFrom-Csv @ConvertParams } default { Throw "No action defined for this log type." } } Write-Verbose "Adding Log Line Numbers to Array" [int]$lineNumber = 0 $oLog.ForEach{ $lineNumber++ Add-Member -InputObject $_ -MemberType NoteProperty -Name "%%LineNo" -Value $lineNumber } $log.logData = $oLog If(-Not($SkipFormatting)) { If($LogFormatting) { $newLogFormatting = [System.Collections.ArrayList]::New() $okToImport = $true ForEach($rule in $LogFormatting) { If(-Not(Test-FormatRule $Rule)) { $okToImport = $false } If($okToImport) { $newRule = [LogSlothFormatting]::New() $newRule.Lookup = [regex]$rule.Lookup if($rule.TextColor) { $newRule.TextColor = [System.Drawing.Color]$rule.TextColor } if($rule.BackgroundCOlor) { $newRule.BackgroundColor = [System.Drawing.Color]$rule.BackgroundColor } [void]$newLogFormatting.Add($newRule) } } if($okToImport) { $log.LogFormatting = $newLogFormatting } else { Write-Error "Invalid rule(s) passed in LogFormatting. Reverting to Default." $LogFormatting = $null } } If(-Not($LogFormatting)) { Write-Verbose "Apply default coloring rules" $FormatList = [System.Collections.ArrayList]::New() $LogFormat = [LogSlothFormatting]::New() $LogFormat.Lookup = "(?i)\bError\b" $LogFormat.TextColor = [System.Drawing.Color]::Red $FormatList.add($LogFormat) | Out-Null $LogFormat = [LogSlothFormatting]::New() $LogFormat.Lookup = "(?i)\bFail(?:ing|ure|)\b" $LogFormat.TextColor = [System.Drawing.Color]::Red $FormatList.add($LogFormat) | Out-Null $log.LogFormatting = $FormatList } } Write-Verbose "Function is complete, Returning." Return $log } Function Import-LogSlothSanitized { Param( [Parameter(Mandatory=$true, ValueFromPipeline=$true, ParameterSetName = "LogClass")] [LogSloth]$LogObject, [Parameter(Mandatory=$true, ValueFromPipeline=$true, ParameterSetName = "LogData")] [String]$LogData, [Parameter(Mandatory=$true, ValueFromPipeline=$false, ParameterSetName = "LogFile")] [System.IO.FileInfo]$LogFile, [Parameter(Mandatory=$false, ParameterSetName = "LogClass")] [Parameter(Mandatory=$false, ParameterSetName = "LogData")] [Parameter(Mandatory=$false, ParameterSetName = "LogFile")] [SanitizeType]$Sanitize = [SanitizeType]::All, [Parameter(Mandatory=$false, ParameterSetName = "LogClass")] [Parameter(Mandatory=$false, ParameterSetName = "LogData")] [Parameter(Mandatory=$false, ParameterSetName = "LogFile")] [ValidateScript({ if($_ -match "(?i)^[a-z]+$") { $true } else { throw "You must use only letters A-Z." } })] [string]$Prefix = "sanitized", [Array]$Headers = @(), [Parameter(Mandatory=$false, ParameterSetName = "LogData")] [Parameter(Mandatory=$false, ParameterSetName = "LogFile")] [System.Collections.ArrayList]$LogFormatting, [Parameter(Mandatory=$false, ParameterSetName = "LogData")] [Parameter(Mandatory=$false, ParameterSetName = "LogFile")] [Switch]$SkipFormatting, [switch]$SkipWarning ) Write-Verbose "Import-LogSlothSanitized Function is beginning" If(-Not($skipWarning)) { Write-Warning "LogSlothParser 0.2 is Currently in Beta and may not function at 100% (Import-LogSlothSanitized)" } If($logFile) { Try { Write-Verbose "LogFile Parameter Defined, Importing $logFile" $logData = Get-Content $logFile -Raw -ErrorAction Stop } Catch { Throw "Error reading $logFile $($_.Exception.Message)" } } elseif ($LogObject) { Write-Verbose "LogClass Passed, Capturing Raw Data" $logData = $LogObject.LogDataRaw } Write-Verbose "Getting Log Type" $logType = Get-LogSlothType -LogData $LogData -SkipWarning If($LogType -eq [LogType]::Nothing) { Throw "Missing LogType" } $LogDataUnsanitized = $LogData $replacementList = [System.Collections.ArrayList]::New() # Build Replacements Table Write-Verbose "Building Replacements Table for Input Data to Sanitize" # -- Configuration Manager Specific -- If($logType -eq [LogType]::SCCM -or $logType -eq [LogType]::SCCMSimple) { Write-Verbose "... Processing Configuration Manager (CM) Sanitization" Switch($sanitize) { { $_ -band [SanitizeType]::cmDistributionPoint } { # Download URLs Write-Verbose "...... Processing CM Distribution Points (Download URLs)" $replacementList.Add([PSCustomObject]@{RegEx="(?msi)Matching DP location found [0-9]{1,} - (http(?:|s):\/\/.*?) "; Stub="$($prefix)dpurl"; Quoted=$false}) | Out-Null # CMG URL Write-Verbose "...... Processing CM Distribution Points (CMG URLs)" $replacementList.Add([PSCustomObject]@{RegEx="(?msi)https:\/\/([^. ]+).cloudapp\.net"; Stub="$($prefix)cmghost"; Quoted=$false}) | Out-Null } { $_ -band [SanitizeType]::cmAdvertisementId } { #Advertisement ID by "ADV:#" Write-Verbose "...... Processing CM Advertisement IDs" $replacementList.Add([PSCustomObject]@{RegEx="Ad(?:vert|):(?: |)([A-Z]{1,3}[0-9A-F]{5,}\b)"; Stub="$($prefix)adv"; Quoted=$false}) | Out-Null } { $_-band [SanitizeType]::cmPackageId} { #Package IDs by "Package:#" Write-Verbose "...... Processing CM Package IDs" $replacementList.Add([PSCustomObject]@{RegEx="(?msi)Package:(?: |)([A-Z]{1,3}[0-9A-F]{5,}\b)"; Stub="$($prefix)pkgid"; Quoted=$false}) | Out-Null $replacementList.Add([PSCustomObject]@{RegEx="(?msi)Download started for content ([A-Z]{1,3}[0-9]{5,})"; Stub="$($prefix)pkgiddl"; Quoted=$false}) | Out-Null } { $_ -band [SanitizeType]::cmProgramId } { # Program Names by "Program:'xxx'>" Write-Verbose "...... Processing CM Program IDs" $replacementList.Add([PSCustomObject]@{RegEx="(?msi)Program:(?: |)(.*?)(?:[\b|\]|,]| \w+ with exit code)"; Stub="$($prefix)prgm"; Quoted=$false}) | Out-Null } { $_ -band [SanitizeType]::cmMachineName } { # Computer Names by "Machine Name = 'xxx'" Write-Verbose "...... Processing CM Machine Name" $replacementList.Add([PSCustomObject]@{RegEx="(?msi)MachineName = `"(.*?)`""; Stub="$($prefix)hostname"; Quoted=$false}) | Out-Null } { $_ -band [SanitizeType]::cmSiteCode} { # Site Code in Quotes Write-Verbose "...... Processing CM Site Codes" $replacementList.Add([PSCustomObject]@{RegEx="(?msi)SiteCode(?: |)=(?: |)`"([A-Z0-9]{1,3})`""; Stub="$($prefix)sitecodeA"; Quoted=$false}) | Out-Null $replacementList.Add([PSCustomObject]@{RegEx="(?msi)Site Code -> ([A-Z0-9]{1,3})"; Stub="$($prefix)sitecodeB"; Quoted=$false}) | Out-Null $replacementList.Add([PSCustomObject]@{RegEx="(?msi)(?:^| )SITE=(.*?)(?:,|\]| )"; Stub="$($prefix)cmsrvC"; Quoted=$false}) | Out-Null } { $_ -band [SanitizeType]::cmADSite} { # AD Site Name Write-Verbose "...... Processing CM AD Sites" $replacementList.Add([PSCustomObject]@{RegEx="(?msi)<ADSite(?:.*?)Name=`"(.*?)`"\/>"; Stub="$($prefix)adsite"; Quoted=$false}) | Out-Null } { $_ -band [SanitizeType]::cmServerName} { # CM SQL Server Name Write-Verbose "...... Processing CM Server Names" $replacementList.Add([PSCustomObject]@{RegEx="(?msi)sqlServerName(?: |)=(?: |)(.*?)(?:,| |\])"; Stub="$($prefix)cmsrvA"; Quoted=$false}) | Out-Null $replacementList.Add([PSCustomObject]@{RegEx="(?msi)Server Name: (.*?)(?:,|\]| )"; Stub="$($prefix)cmsrvB"; Quoted=$false}) | Out-Null $replacementList.Add([PSCustomObject]@{RegEx="(?msi)Setting Site Server -> (.*?)(?:,|\]| )"; Stub="$($prefix)cmsrvC"; Quoted=$false}) | Out-Null $replacementList.Add([PSCustomObject]@{RegEx="(?msi)(?:^| )SYS=(.*?)(?:,|\]| )"; Stub="$($prefix)cmsrvD"; Quoted=$false}) | Out-Null $replacementList.Add([PSCustomObject]@{RegEx="(?msi)(?:^| )Server: (.*?)(?:,|\]| )"; Stub="$($prefix)cmsrvE"; Quoted=$false}) | Out-Null $replacementList.Add([PSCustomObject]@{RegEx="(?msi)(?:^| )MP: (.*?)(?:,|\]| )"; Stub="$($prefix)cmsrvF"; Quoted=$false}) | Out-Null } { $_ -band [SanitizeType]::cmDatabaseName} { # CM Database Name Write-Verbose "...... Processing CM Database Names" $replacementList.Add([PSCustomObject]@{RegEx="(?msi)databaseName(?: |)=(?: |)(.*?)(?:,| |\]|\.)"; Stub="$($prefix)cmdb"; Quoted=$false}) | Out-Null } } # // End of Switch } # // End of Log Type SCCM # -- Generic Replacements -- Write-Verbose "... Processing Generic Sanitization" Switch($Sanitize) { { $_ -band [SanitizeType]::urlHost } { # URL Host Write-Verbose "...... Processing URL Hosts" $replacementList.Add([PSCustomObject]@{RegEx="(?msi)http(?:|s):\/\/(.*?)\/"; Stub="$($prefix)urlhost"; Quoted=$false}) | Out-Null } { $_ -band [SanitizeType]::guid } { # GUIDs in 8-4-4-4-12 Format Write-Verbose "...... Processing GUIDs" $replacementList.Add([PSCustomObject]@{RegEx="(?msi)([0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12})"; Stub="$($prefix)guid"; Quoted=$false}) | Out-Null } { $_ -band [SanitizeType]::hash64 } { # Hashes (64 Character Long Hex) Write-Verbose "...... Processing 64 Character Hashes" $replacementList.Add([PSCustomObject]@{RegEx="(?msi)([A-Z0-9]{64})"; Stub="$($prefix)hash"; Quoted=$false}) | Out-Null } { $_ -band [SanitizeType]::ipv4 } { # IP Addresses Write-Verbose "...... Processing IPv4 Addresses" $replacementList.Add([PSCustomObject]@{RegEx="(?msi)((?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))"; Stub="$($prefix)ip"; Quoted=$false}) | Out-Null } { $_ -band [SanitizeType]::sid } { # SID Format Write-Verbose "...... Processing SIDs" $replacementList.Add([PSCustomObject]@{RegEx="(?msi)(S-1-[0-9]{1,2}-\d{1,2}-(\d{8,10}-){3}\d{8,10})"; Stub="$($prefix)sid"; Quoted=$false}) | Out-Null } } # // End of Switch (Generic) Write-Verbose "Building List of Data to Sanitize" $replacementArray = [System.Collections.ArrayList]::New() ForEach($rule in $replacementList) { $uniqueStringMatches = [System.Collections.Generic.HashSet[string]]::New([StringComparer]::InvariantCultureIgnoreCase) $rxMatches = [regex]::Matches($LogData, $rule.regex) ForEach($m in $rxMatches) { $uniqueStringMatches.Add($m.groups[1].value) | Out-Null } $index = 0 ForEach($find in $uniqueStringMatches) { $index++ $replace = "$($rule.Stub)$index" if($rule.Quoted) { $replace = "`"$replace`"" } $replacementArray.Add( [PSCustomObject]@{ Text = $find Replace = $replace } ) | Out-Null } } Write-Verbose "Starting Sanitization of Data based on Replacement ArrayList" ForEach($replacement in $replacementArray) { $logData = $LogData -replace [RegEx]::Escape($replacement.text), $replacement.Replace } Write-Verbose "Calling Import-LogSloth to Format Data Properly" $log = Import-LogSloth -LogData $LogData -SkipWarning -LogFormatting $LogFormatting -SkipFormatting:$SkipFormatting Write-Verbose "Writing Sanitization Metadata to Log Class" $log.SanitizeType = $Sanitize $log.SanitizedReplacements = $replacementArray $log.LogDataUnsanitized = $LogDataUnsanitized Write-Verbose "Function is complete and returning" Return $log } Function Import-LogSCCM { [CmdLetBinding()] Param( [Parameter(Mandatory=$true)] [string] $LogData ) Write-Verbose "Private Import-LogSCCM Function is beginning" ## Fixes issue where CM adds unnecessary CRs when dumping data from an error report into a log $logData = $logData -replace '(?<!">)\r\n',"`n" ## SCCM breaks log files up by CR (\r). Multiline within a single log line are broken up by NewLine (\n) $cmLogData = $logData -split "`r" $logArray = [System.Collections.ArrayList]::New() Write-Verbose "Building RegEx Variables" $rxLogText = [regex]::new('(?msi)<!\[LOG\[(.*)]LOG]!>') $rxLogTime = [regex]::new('(?msi)<!\[LOG\[.*?]LOG]!><.*?time="(.*?)"') $rxLogDate = [regex]::new('(?msi)<!\[LOG\[.*?]LOG]!><.*?date="(.*?)"') $rxLogComponent = [regex]::new('(?msi)<!\[LOG\[.*?]LOG]!><.*?component="(.*?)"') $rxLogContext = [regex]::new('(?msi)<!\[LOG\[.*?]LOG]!><.*?context="(.*?)"') $rxLogType = [regex]::new('(?msi)<!\[LOG\[.*?]LOG]!><.*?type="(.*?)"') $rxLogThread = [regex]::new('(?msi)<!\[LOG\[.*?]LOG]!><.*?thread="(.*?)"') $rxLogFile = [regex]::new('(?msi)<!\[LOG\[.*?]LOG]!><.*?file="(.*?)"') Write-Verbose "Looping over Lines in Log Data and building custom object" ForEach($item in $cmLogData) { $oLogLine = New-Object -TypeName PSCustomObject # Get Log Text $logText = $rxLogText.Match($item) $logTime = $rxLogTime.Match($item) $logDate = $rxLogDate.Match($item) $logComponent = $rxLogComponent.Match($item) $logContext = $rxLogContext.Match($item) $logType = $rxLogType.Match($item) $logThread = $rxLogThread.Match($item) $logFile = $rxLogFile.Match($item) Add-Member -InputObject $oLogLine -MemberType NoteProperty -Name Text -Value $logText.Groups[1].Value Add-Member -InputObject $oLogLine -MemberType NoteProperty -Name Component -Value $logComponent.Groups[1].Value Add-Member -InputObject $oLogLine -MemberType NoteProperty -Name DateTime -Value "$($logDate.Groups[1].Value) $($logTime.Groups[1].Value)" Add-Member -InputObject $oLogLine -MemberType NoteProperty -Name Thread -Value $logThread.Groups[1].Value Add-Member -InputObject $oLogLine -MemberType NoteProperty -Name Context -Value $logContext.Groups[1].Value Add-Member -InputObject $oLogLine -MemberType NoteProperty -Name Type -Value $logType.Groups[1].Value Add-Member -InputObject $oLogLine -MemberType NoteProperty -Name File -Value $logFile.Groups[1].Value $logArray.add($oLogLine) | Out-Null } Write-Verbose "Completed Looping over Lines in Log Data and building custom object" Write-Verbose "Function returning Log Array" Return $logArray } Function Import-LogSCCMSimple { [CmdLetBinding()] Param( [Parameter(Mandatory=$true)] [string] $LogData ) Write-Verbose "Private Import-LogSCCMSimple Function is beginning" $cmLogData = $logData -split "`r`n" | Where-Object { $_ -ne "" -and $_ -notin ("ROLLOVER")} $logArray = [System.Collections.ArrayList]::New() Write-Verbose "Building RegEx Variables" $rxLogData = [regex]::New('(.*?) \$\$<(.*?)><(.*?)><thread=(.*?)>') Write-Verbose "Looping over Lines in Log Data and building custom object" ForEach($item in $cmLogData) { $oLogLine = New-Object -TypeName PSCustomObject # Get Log Text $logText = $rxLogData.Match($item) if($logText.Success) { Add-Member -InputObject $oLogLine -MemberType NoteProperty -Name Text -Value $logText.Groups[1].Value Add-Member -InputObject $oLogLine -MemberType NoteProperty -Name Component -Value $logText.Groups[2].Value Add-Member -InputObject $oLogLine -MemberType NoteProperty -Name DateTime -Value $logText.Groups[3].Value Add-Member -InputObject $oLogLine -MemberType NoteProperty -Name Thread -Value $logText.Groups[4].Value $logArray.add($oLogLine) | Out-Null } else { Add-Member -InputObject $oLogLine -MemberType NoteProperty -Name Text -Value $item $logArray.add($oLogLine) | Out-Null } } Write-Verbose "Completed Looping over Lines in Log Data and building custom object" Write-Verbose "Function returning Log Array" Return $logArray } Function Import-LogW3CExtended { [CmdLetBinding()] Param( [Parameter(Mandatory=$true)] [string] $LogData ) Write-Verbose "Private Import-LogW3CExtended Function is beginning" $w3cLogData = $logData -split "`r`n" | Where-Object { $_ -ne "" } $headers = $w3cLogData.where{$_ -like "#Fields:*"} | Select-Object -First 1 $headers = $headers -replace "#Fields:[ |]","" -split " " $logContent = $w3cLogData.where{$_ -notlike "#*" } $oLog = $logContent | ConvertFrom-Csv -Delimiter " " -Header $headers Return $oLog } Function Convert-Color2Hex { Param( [System.Drawing.Color]$Color ) $rHex = [Convert]::ToString($color.r, 16).padLeft(2, "0") $gHex = [Convert]::ToString($color.g, 16).padLeft(2, "0") $bHex = [Convert]::ToString($color.b, 16).padLeft(2, "0") Return "#$rHex$gHex$bHex".ToUpper() } Function ConvertTo-LogSlothHTML { Param( [Parameter(Mandatory=$true, ValueFromPipeline=$true)] [LogSloth]$LogObject, [switch]$SkipWarning, [switch]$IncludeRawLog ) Write-Verbose "ConvertTo-LogSlothHTML Function is beginning" If(-Not($skipWarning)) { Write-Warning "LogSlothParser 0.2 is Currently in Beta and may not function at 100% (Export-LogSlothLog)" } Write-Verbose "Excluding Meta Properties from Export" $LogObject.LogData = $LogObject.LogData | Select-Object -Property * -ExcludeProperty "%%*" # Build Collection of Formatting Rules $cssFormatRules = [System.Collections.ArrayList]::New() $cssIndex = 0 ForEach($rule in $LogObject.LogFormatting) { $thisRule = [PSCustomObject]@{ RuleNum = $cssIndex Lookup = $rule.Lookup TextColor = $null BackgroundColor = $null } If($rule.TextColor -ne [System.Drawing.Color]::Empty) { Add-Member -InputObject $thisRule -MemberType NoteProperty -Name "TextColor" -Value (Convert-Color2Hex $rule.TextColor) -Force } If($rule.BackgroundColor -ne [System.Drawing.Color]::Empty) { Add-Member -InputObject $thisRule -MemberType NoteProperty -Name "BackgroundColor" -Value (Convert-Color2Hex $rule.BackgroundColor) -Force } $cssFormatRules.Add($thisRule) | Out-Null $cssIndex++ } [System.Collections.ArrayList]$css = @() [void]$css.Add("#LogTable td { font-family: verdana; font-size: 12px; }") [void]$css.Add("#LogTable th { font-family: verdana; font-size: 12px; font-weight: bold; text-align: left; }") ForEach($rule in $cssFormatRules) { $ruleText = "" If($rule.BackgroundColor) { $ruleText += "background-color: $($rule.BackgroundColor); " } If($rule.TextColor) { $ruleText += "color: $($rule.TextColor); " } [void]$css.Add("#LogTable tr.rxMatch$($rule.RuleNum) { $ruleText }") } if($IncludeRawLog) { [void]$css.Add("#LogRaw { font-family: 'courier new'; font-size: 12px; width: 100%; height: 200px; margin-top: 20px; white-space: nowrap; overflow: auto;") } [System.Collections.ArrayList]$links = @() [void]$links.Add('<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/1.10.24/css/jquery.dataTables.css">') [System.Collections.Specialized.OrderedDictionary]$dataTableOptions = @{} [void]$dataTableOptions.Add("paging", $true) [void]$dataTableOptions.Add("pagingType","full_numbers") [void]$dataTableOptions.Add("ordering", $false) [void]$dataTableOptions.Add("order",@()) [void]$dataTableOptions.Add("lengthMenu",@(25, 50, 100, 250, 500, 1000)) [void]$dataTableOptions.Add("pageLength", 500) [System.Collections.ArrayList]$scripts = @() [void]$scripts.Add('<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>') [void]$scripts.Add('<script src="https://cdn.datatables.net/1.10.24/js/jquery.dataTables.js"></script>') [void]$scripts.Add("<script> `$(document).ready( function () { `$('#LogTable').DataTable( $($dataTableOptions | ConvertTo-Json) ); } );</script>") [System.Collections.ArrayList]$thead = @() [void]$thead.AddRange(@("<thead>","<tr>")) ForEach($prop in $logObject.LogData[0].psobject.Properties.Name) { [void]$thead.Add("<th>$([System.Web.HttpUtility]::HTMLEncode($prop))</th>") } [void]$thead.AddRange(@("</tr>","</thead>")) [System.Collections.ArrayList]$tbody = @() ForEach($entry in $logObject.LogData) { #ForEach Line in the Log File # Determine if we need to apply any style to this row based on RegEx Rules $trClass = "" ForEach($rule in $cssFormatRules) { ForEach($prop in $LogObject.LogData[0].psobject.Properties.Name) { #ForEach Property (Field/Column) if($rule.Lookup.IsMatch($entry.$prop)) { $trClass = "rxMatch$($rule.ruleNum)" } } } if($trClass) { [void]$tbody.Add("<tr class=`"$trClass`">") } else { [void]$tbody.Add("<tr>") } ForEach($prop in $LogObject.LogData[0].psobject.Properties.Name) { #ForEach Property (Field/Column) [void]$tbody.Add("<td>$([System.Web.HttpUtility]::HTMLEncode($entry.$prop))</td>") } [void]$tbody.Add("</tr>") } [System.Collections.ArrayList]$html = @() [void]$html.Add("<!DOCTYPE html>") [void]$html.AddRange(@("<html lang=`"en`">","<head>")) [void]$html.Add("<title>LogSloth Log Export</title>") [void]$html.AddRange($links) [void]$html.AddRange($scripts) [void]$html.AddRange(@("<style>",$css,"</style>")) [void]$html.Add("</head>") [void]$html.Add("<body>") [void]$html.Add('<table id="LogTable">') [void]$html.AddRange($thead) [void]$html.AddRange($tbody) [void]$html.Add("</table>") if($IncludeRawLog) { [void]$html.Add("<textarea id=`"LogRaw`">") [void]$html.Add($logObject.LogDataRaw) [void]$html.Add("</textarea>") } [void]$html.Add("</body>") [void]$html.Add("</html>") Write-Verbose "ConvertTo-LogSlothHTML Function is returning" Return $html } Function Export-LogSlothLog { Param( [Parameter(Mandatory=$true, ValueFromPipeline=$true, Position=1)] [LogSloth]$LogObject, [Parameter(Mandatory=$true, ValueFromPipeline=$false, Position=2)] [System.IO.FileInfo]$Path, [Parameter(Mandatory=$false, ValueFromPipeline=$false, Position=3)] [LogSlothExportType]$Format = [LogSlothExportType]::HTML, [Parameter(Mandatory=$false, ValueFromPipeline=$false)] [Switch]$IncludeRawLog, [Switch]$SkipWarning, [Switch]$Force ) Write-Verbose "Export-LogSlothLog Function is beginning" If(-Not($skipWarning)) { Write-Warning "LogSlothParser 0.2 is Currently in Beta and may not function at 100% (Export-LogSlothLog)" } Switch($Format) { "HTML" { Try { $LogObject | ConvertTo-LogSlothHTML -IncludeRawLog:$includeRawLog -SkipWarning | Out-File $Path -Encoding utf8 -NoClobber:$(-Not $Force) -ErrorAction Stop } Catch { Throw "Unable to export file. $($_.Exception.Message)" } } } Write-Verbose "Export-LogSlothLog Function is returning" Return } # New changes here should be added to the manifest as well. Export-ModuleMember -Function Import-LogSloth,Import-LogSlothSanitized,Get-LogSlothType,Export-LogSlothLog,ConvertTo-LogSlothHTML |