Public/Get-CCMLogFile.ps1

Function Get-CCMLogFile {
    <#
    .SYNOPSIS
        Parse Configuration Manager format logs
    .DESCRIPTION
        This function is used to take Configuration Manager formatted logs and turn them into a PSCustomObject so that it can be
        searched and manipulated easily with PowerShell
    .PARAMETER LogFilePath
        Path to the log file(s) you would like to parse.
    .PARAMETER ParseSMSTS
        Only pulls out the TS actions. This is for parsing an SMSTSLog specifically
    .EXAMPLE
        PS C:\> Get-CCMLogFile -LogFilePath 'c:\windows\ccm\logs\ccmexec.log'
        Returns the CCMExec.log as a PSCustomObject
    .EXAMPLE
        PS C:\> Get-CCMLogFile -LogFilePath 'c:\windows\ccm\logs\AppEnforce.log', 'c:\windows\ccm\logs\AppDiscovery.log'
        Returns the AppEnforce.log and the AppDiscovery.log as a PSCustomObject
    .EXAMPLE
        PS C:\> Get-CCMLogFile -LogFilePath 'c:\windows\ccm\logs\smstslog.log' -ParseSMSTS
        Returns all the actions that ran according to the SMSTSLog provided
    .OUTPUTS
        [pscustomobject]
    .NOTES
        I've done my best to test this against various SCCM log files. They are all generally 'formatted' the same, but do have some
        variance. I had to also balance speed and parsing.
 
        With that said, it can still parse a typical SCCM log VERY quickly. Smaller logs are parsed in milliseconds in my testing.
        Rolled over logs that are 5mb can be parsed in a couple seconds or less.
 
            FileName: Get-CCMLogFile.ps1
            Author: Cody Mathis
            Contact: @CodyMathis123
            Created: 2019-9-19
            Updated: 2020-01-01
    #>

    param (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName)]
        [Alias('Fullname')]
        [string[]]$LogFilePath,
        [Parameter(Mandatory = $false)]
        [switch]$ParseSMSTS
    )
    begin {
        try {
            Add-Type -ErrorAction SilentlyContinue -TypeDefinition @"
            public enum Severity
            {
                None,
                Informational,
                Warning,
                Error
            }
"@

        }
        catch {
            Write-Debug "Type Severity already exists"
        }

        function Get-TimeStampFromLogLine {
            <#
            .SYNOPSIS
                Parses a datetime object from an SCCM log line
            .DESCRIPTION
                This will return a datetime object if it is passed the part of an SCCM log line that contains the date and time
            .PARAMETER LogLinSubArray
                A log line sub array which is everything past "]LOG]!><" split by '"'
                For Example:
                    time=
                    09:58:50.301+300
                    date=
                    11-18-2019
                    component=
                    TSManager
                    context=
 
                    type=
                    1
                    thread=
                    2164
                    file=
                    tsxml.cpp:898
 
                    such that the [3] and [1] are the date and time respectively
            .EXAMPLE
                PS C:\> Get-TimeStampFromLogLine -LogLineSubArray $LogLineSubArray
                return datetime object from the log line that was split into a subarray
            #>

            param (
                [Parameter(Mandatory = $true)]
                [array]$LogLineSubArray
            )
            $DateString = $LogLineSubArray[3]
            $DateStringArray = $DateString -split "-"

            $MonthParser = $DateStringArray[0] -replace '\d', 'M'
            $DayParser = $DateStringArray[1] -replace '\d', 'd'

            $DateTimeFormat = [string]::Format('{0}-{1}-yyyyHH:mm:ss.fff', $MonthParser, $DayParser)
            $TimeString = ($LogLineSubArray[1]).Split("+|-")[0].ToString().Substring(0, 12)
            $DateTimeString = [string]::Format('{0}{1}', $DateString, $TimeString)
            [datetime]::ParseExact($DateTimeString, $DateTimeFormat, $null)
        }
    }
    process {
        Foreach ($LogFile in $LogFilePath) {
            #region ingest log file with StreamReader. Quick, and prevents locks
            $File = [System.IO.File]::Open($LogFile, 'Open', 'Read', 'ReadWrite')
            $StreamReader = New-Object System.IO.StreamReader($File)
            [string]$LogFileRaw = $StreamReader.ReadToEnd()
            $StreamReader.Close()
            $File.Close()
            #endregion ingest log file with StreamReader. Quick, and prevents locks

            #region perform a regex match to determine the 'type' of log we are working with and parse appropriately
            switch ($true) {
                #region parse a 'typical' SCCM log
                (([Regex]::Match($LogFileRaw, "LOG\[(.*?)\]LOG(.*?)time(.*?)date")).Success) {
                    # split on what we know is a line beginning
                    switch -regex ($LogFileRaw -split "<!\[LOG\[") {
                        '^\s*$' {
                            # ignore empty lines
                            continue
                        }
                        default {
                            <#
                                split Log line into an array on what we know is the end of the message section
                                first item contains the message which can be parsed
                                second item contains all the information about the message/line (ie. type, component, datetime, thread) which can be parsed
                            #>

                            $LogLineArray = $PSItem -split "]LOG]!><"

                            # Strip the log message out of our first array index
                            $Message = $LogLineArray[0]

                            # Split LogLineArray into a a sub array based on double quotes to pull log line information
                            $LogLineSubArray = $LogLineArray[1] -split '"'

                            $LogLine = [System.Collections.Specialized.OrderedDictionary]::new()
                            # Rebuild the LogLine into a hash table
                            $LogLine['Message'] = $Message
                            $LogLine['Type'] = [Severity]$LogLineSubArray[9]
                            $LogLine['Component'] = $LogLineSubArray[5]
                            $LogLine['Thread'] = $LogLineSubArray[11]
                            
                            # if we are Parsing SMSTS then we will only pull out messages that match 'win32 code 0|failed to run the action'
                            switch ($ParseSMSTS.IsPresent) {
                                $true {
                                    switch -regex ($Message) {
                                        'win32 code 0|failed to run the action' {
                                            $LogLine.TimeStamp = Get-TimeStampFromLogLine -LogLineSubArray $LogLineSubArray
                                            [pscustomobject]$LogLine
                                        }
                                        default {
                                            continue
                                        }
                                    }
                                }
                                $false {
                                    $LogLine['TimeStamp'] = Get-TimeStampFromLogLine -LogLineSubArray $LogLineSubArray
                                    [pscustomobject]$LogLine
                                }
                            }
                        }
                    }
                }
                #endregion parse a 'typical' SCCM log

                #region parse a 'simple' SCCM log, usually found on site systems
                (([Regex]::Match($LogFileRaw, '\$\$\<(.*?)\>\<thread=')).Success) {
                    switch -regex ($LogFileRaw -split [System.Environment]::NewLine) {
                        '^\s*$' {
                            # ignore empty lines
                            continue
                        }
                        default {
                            <#
                                split Log line into an array
                                first item contains the message which can be parsed
                                second item contains all the information about the message/line (ie. type, component, timestamp, thread) which can be parsed
                            #>

                            $LogLineArray = $PSItem -split '\$\$<'

                            # Strip the log message out of our first array index
                            $Message = $LogLineArray[0]

                            # Split LogLineArray into a a sub array based on double quotes to pull log line information
                            $LogLineSubArray = $LogLineArray[1] -split '><'

                            switch -regex ($Message) {
                                '^\s*$' {
                                    # ignore empty messages
                                    continue
                                }
                                default {
                                    $LogLine = [System.Collections.Specialized.OrderedDictionary]::new()
                                    # Rebuild the LogLine into a hash table
                                    $LogLine['Message'] = $Message
                                    $LogLine['Type'] = [Severity]0
                                    $LogLine['Component'] = $LogLineSubArray[0].Trim()
                                    $LogLine['Thread'] = ($LogLineSubArray[2] -split " ")[0].Substring(7)

                                    #region determine timestamp for log line
                                    $DateTimeString = $LogLineSubArray[1]
                                    $DateTimeStringArray = $DateTimeString -split " "
                                    $DateString = $DateTimeStringArray[0]
                                    $DateStringArray = $DateString -split "-"

                                    $MonthParser = $DateStringArray[0] -replace '\d', 'M'
                                    $DayParser = $DateStringArray[1] -replace '\d', 'd'

                                    $DateTimeFormat = [string]::Format('{0}-{1}-yyyy HH:mm:ss.fff', $MonthParser, $DayParser)
                                    $TimeString = $DateTimeStringArray[1].ToString().Substring(0, 12)
                                    $DateTimeString = [string]::Format('{0} {1}', $DateString, $TimeString)
                                    $LogLine['TimeStamp'] = [datetime]::ParseExact($DateTimeString, $DateTimeFormat, $null)
                                    #endregion determine timestamp for log line

                                    [pscustomobject]$LogLine
                                }
                            }
                        }
                    }
                }
                #endregion parse a 'simple' SCCM log, usually found on site systems
            }
            #endregion perform a regex match to determine the 'type' of log we are working with and parse appropriately
        }
    }
}