LogSlothParser.psm1
Enum LogType { CSV TSV ColonSV SCCM SCCMSimple MECM W3CExtended Nothing } [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 [Boolean] IsSanitized() { Return $($this.SanitizeType -ne [SanitizeType]::None) } } 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 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 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 } 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") | Select -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 } } # Check for W3C Extended by looking at first line of log # 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 } Try { Write-Verbose "Converting Log Data to Colon Delimited" $ColonSV = $logData | ConvertFrom-Csv -Delimiter ";" Write-Verbose "Conversion to Colon Delimited was successful" } Catch { Write-Verbose "Failed to Convert Log to TSV $($_.Exception.Message)" $ColonSV = $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" } } If($ColonSV[0].PSObject.Members.Where{$_.MemberType -eq "NoteProperty"}.Count -gt 1) { Write-Verbose "There are multiple properties returned with colon separated values, selecting ColonSV as Winner" Return [LogType]::ColonSV } 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 = @(), [switch]$SkipWarning ) Write-Verbose "Import-LogSloth Function is beginning" If(-Not($skipWarning)) { Write-Warning "LogSlothParser 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 } 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 } "ColonSV" { Write-Verbose "Importing ColonSV using Built-in PowerShell Function" $ConvertParams = @{ InputObject = $logData Delimiter = ";" } if($headers) { $ConvertParams.Add("Header",$headers) } [System.Collections.ArrayList]$oLog = ConvertFrom-Csv @ConvertParams } default { Throw "No action defined for this log type." } } $log.logData = $oLog 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 = @(), [switch]$SkipWarning ) Write-Verbose "Import-LogSlothSanitized Function is beginning" If(-Not($skipWarning)) { Write-Warning "LogSlothParser 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 } Catch { Throw "Error reading $logFile $($_.Exception.Message)" } } elseif ($LogClass) { Write-Verbose "LogClass Passed, Capturing Raw Data" $logData = $LogClass.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 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" ## 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 Time -Value $logTime.Groups[1].Value Add-Member -InputObject $oLogLine -MemberType NoteProperty -Name Date -Value $logDate.Groups[1].Value Add-Member -InputObject $oLogLine -MemberType NoteProperty -Name Component -Value $logComponent.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 Thread -Value $logThread.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 } Export-ModuleMember -Function Import-LogSloth,Import-LogSlothSanitized,Get-LogSlothType |