Shared/Receive-CodeIntegrityLogs.psm1

Function Receive-CodeIntegrityLogs {
    <#
    .SYNOPSIS
        A resilient function that:
        Retrieves the Code Integrity Operational logs
        Fixes the paths to the files that are being logged
        Separates events based on their type: Audit or Blocked
        Separates events based on their file paths: existing or deleted
        Finds correlated events with ID 3089 and adds them to the main event (IDs 3076 and 3077)
        Replaces many numbers in the logs with user-friendly strings
        De-duplicates the logs
        Then processes the output based on different criteria
    .PARAMETER Date
        The date from which the logs should be collected. If not specified, all logs will be collected.
        It accepts empty strings, nulls, and whitespace and they are treated as not specified.
    .PARAMETER Type
        The type of logs to be collected. Audit or Blocked. The default value is 'Audit'
    .PARAMETER PostProcessing
        How to process the output for different scenarios
        OnlyExisting: Returns only the logs of files that exist on the disk
        OnlyDeleted: Returns only the hash details of files that do not exist on the disk
        Separate: Returns the file paths of files that exist on the disk and the hash details of files that do not exist on the disk, separately in a nested object
    .PARAMETER PolicyNames
        The names of the policies to filter the logs by
    .INPUTS
        System.String
        System.String[]
    .OUTPUTS
        PSCustomObject
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject[]])]
    param(
        [AllowEmptyString()]
        [AllowNull()]
        [Parameter(Mandatory = $false)]
        [System.String]$Date,

        [ValidateSet('Audit', 'Blocked')]
        [Parameter(Mandatory = $false)]
        [System.String]$Type = 'Audit',

        [ValidateSet('OnlyExisting', 'OnlyDeleted' , 'Separate')]
        [parameter(mandatory = $false)]
        [System.String]$PostProcessing,

        [AllowEmptyString()]
        [AllowNull()]
        [parameter(mandatory = $false)]
        [System.String[]]$PolicyNames
    )

    Begin {
        # Importing the $PSDefaultParameterValues to the current session, prior to everything else
        . "$ModuleRootPath\CoreExt\PSDefaultParameterValues.ps1"

        # Importing the required sub-modules
        Import-Module -FullyQualifiedName "$ModuleRootPath\Shared\Get-GlobalRootDrives.psm1" -Force

        #Region Application Control event tags intelligence

        # Requested and Validated Signing Level Mappings: https://learn.microsoft.com/en-us/windows/security/application-security/application-control/windows-defender-application-control/operations/event-tag-explanations#requested-and-validated-signing-level
        [System.Collections.Hashtable]$ReqValSigningLevels = @{
            [System.UInt16]0  = "Signing level hasn't yet been checked"
            [System.UInt16]1  = 'File is unsigned or has no signature that passes the active policies'
            [System.UInt16]2  = 'Trusted by Windows Defender Application Control policy'
            [System.UInt16]3  = 'Developer signed code'
            [System.UInt16]4  = 'Authenticode signed'
            [System.UInt16]5  = 'Microsoft Store signed app PPL (Protected Process Light)'
            [System.UInt16]6  = 'Microsoft Store-signed'
            [System.UInt16]7  = 'Signed by an Antimalware vendor whose product is using AMPPL'
            [System.UInt16]8  = 'Microsoft signed'
            [System.UInt16]11 = 'Only used for signing of the .NET NGEN compiler'
            [System.UInt16]12 = 'Windows signed'
            [System.UInt16]14 = 'Windows Trusted Computing Base signed'
        }

        # SignatureType Mappings: https://learn.microsoft.com/en-us/windows/security/application-security/application-control/windows-defender-application-control/operations/event-tag-explanations#signaturetype
        [System.Collections.Hashtable]$SignatureTypeTable = @{
            [System.UInt16]0 = "Unsigned or verification hasn't been attempted"
            [System.UInt16]1 = 'Embedded signature'
            [System.UInt16]2 = 'Cached signature; presence of a CI EA means the file was previously verified'
            [System.UInt16]3 = 'Cached catalog verified via Catalog Database or searching catalog directly'
            [System.UInt16]4 = 'Uncached catalog verified via Catalog Database or searching catalog directly'
            [System.UInt16]5 = 'Successfully verified using an EA that informs CI that catalog to try first'
            [System.UInt16]6 = 'AppX / MSIX package catalog verified'
            [System.UInt16]7 = 'File was verified'
        }

        # VerificationError mappings: https://learn.microsoft.com/en-us/windows/security/application-security/application-control/windows-defender-application-control/operations/event-tag-explanations#verificationerror
        [System.Collections.Hashtable]$VerificationErrorTable = @{
            [System.UInt16]0  =    'Successfully verified signature.'
            [System.UInt16]1  =    'File has an invalid hash.'
            [System.UInt16]2  =    'File contains shared writable sections.'
            [System.UInt16]3  =    "File isn't signed."
            [System.UInt16]4  =    'Revoked signature.'
            [System.UInt16]5  =    'Expired signature.'
            [System.UInt16]6  =    "File is signed using a weak hashing algorithm, which doesn't meet the minimum policy."
            [System.UInt16]7  =    'Invalid root certificate.'
            [System.UInt16]8  =    'Signature was unable to be validated; generic error.'
            [System.UInt16]9  =    'Signing time not trusted.'
            [System.UInt16]10 =    'The file must be signed using page hashes for this scenario.'
            [System.UInt16]11 =    'Page hash mismatch.'
            [System.UInt16]12 =    'Not valid for a PPL (Protected Process Light).'
            [System.UInt16]13 =    'Not valid for a PP (Protected Process).'
            [System.UInt16]14 =    'The signature is missing the required ARM processor EKU.'
            [System.UInt16]15 =    'Failed WHQL check.'
            [System.UInt16]16 =    'Default policy signing level not met.'
            [System.UInt16]17 =    "Custom policy signing level not met; returned when signature doesn't validate against an SBCP-defined set of certs."
            [System.UInt16]18 =    'Custom signing level not met; returned if signature fails to match CISigners in UMCI.'
            [System.UInt16]19 =    'Binary is revoked based on its file hash.'
            [System.UInt16]20 =    "SHA1 cert hash's timestamp is missing or after valid cutoff as defined by Weak Crypto Policy."
            [System.UInt16]21 =    'Failed to pass Windows Defender Application Control policy.'
            [System.UInt16]22 =    'Not Isolated User Mode (IUM) signed; indicates an attempt to load a standard Windows binary into a virtualization-based security (VBS) trustlet.'
            [System.UInt16]23 =    "Invalid image hash. This error can indicate file corruption or a problem with the file's signature. Signatures using elliptic curve cryptography (ECC), such as ECDSA, return this VerificationError."
            [System.UInt16]24 =    'Flight root not allowed; indicates trying to run flight-signed code on production OS.'
            [System.UInt16]25 =    'Anti-cheat policy violation.'
            [System.UInt16]26 =    'Explicitly denied by WDAC policy.'
            [System.UInt16]27 =    'The signing chain appears to be tampered / invalid.'
            [System.UInt16]28 =    'Resource page hash mismatch.'
        }

        #EndRegion Application Control event tags intelligence

        # Validate the date provided if it's not null or empty or whitespace
        if (-NOT ([System.String]::IsNullOrWhiteSpace($Date))) {
            if (-NOT ([System.DateTime]::TryParse($Date, [ref]$Date))) {
                Throw 'The date provided is not in a valid DateTime type.'
            }
        }

        Try {
            # Set a flag indicating that the alternative drive letter mapping method is not necessary unless the primary method fails
            [System.Boolean]$AlternativeDriveLetterFix = $false

            # Get the local disks mappings
            [System.Object[]]$DriveLettersGlobalRootFix = Get-GlobalRootDrives
        }
        catch {
            Write-Verbose -Verbose -Message 'Receive-CodeIntegrityLogs: Could not get the drive mappings from the system using the primary method, trying the alternative method now'

            # Set the flag to true indicating the alternative method is being used
            $AlternativeDriveLetterFix = $true

            # Create a hashtable of partition numbers and their associated drive letters
            [System.Collections.Hashtable]$DriveLetterMappings = @{}

            # Get all partitions and filter out the ones that don't have a drive letter and then add them to the hashtable with the partition number as the key and the drive letter as the value
            foreach ($Drive in (Get-Partition | Where-Object -FilterScript { $_.DriveLetter })) {
                $DriveLetterMappings[[System.String]$Drive.PartitionNumber] = [System.String]$Drive.DriveLetter
            }
        }

        Try {
            Write-Verbose -Message 'Receive-CodeIntegrityLogs: Collecting the Code Integrity Operational logs'
            [System.Diagnostics.Eventing.Reader.EventLogRecord[]]$RawEventLogs = Get-WinEvent -FilterHashtable @{LogName = 'Microsoft-Windows-CodeIntegrity/Operational' }
        }
        catch {
            Throw "Receive-CodeIntegrityLogs: Could not collect the Code Integrity Operational logs, the number of logs collected is $($RawEventLogs.Count)"
        }

        [Microsoft.PowerShell.Commands.GroupInfo[]]$GroupedEvents = $RawEventLogs | Group-Object -Property ActivityId
        Write-Verbose -Message "Receive-CodeIntegrityLogs: Grouped the logs by ActivityId. The total number of groups is $($GroupedEvents.Count) and the total number of logs in the groups is $($GroupedEvents.Group.Count)"

        # Create a collection to store the packages of logs
        [PSCustomObject[]]$EventPackageCollections = @()

        # Loop over each group of logs
        Foreach ($RawLogGroup in $GroupedEvents) {

            # Process Audit events
            if ($RawLogGroup.Group.Id -contains '3076') {

                # Finding the main event in the group
                [System.Diagnostics.Eventing.Reader.EventLogRecord]$AuditTemp = $RawLogGroup.Group | Where-Object -FilterScript { $_.Id -eq '3076' }

                # If the main event is older than the specified date, skip it
                if (-NOT ([System.String]::IsNullOrWhiteSpace($Date))) {
                    if ($AuditTemp.TimeCreated -lt $Date) {
                        continue
                    }
                }

                # Add the main event along with the correlated events to the collection
                $EventPackageCollections += [PSCustomObject]@{
                    MainEventData        = $AuditTemp
                    CorrelatedEventsData = $RawLogGroup.Group | Where-Object -FilterScript { $_.Id -eq '3089' }
                    Type                 = 'Audit'
                }
            }

            # Process Blocked events
            if ($RawLogGroup.Group.Id -contains '3077') {

                # Finding the main event in the group
                [System.Diagnostics.Eventing.Reader.EventLogRecord]$BlockedTemp = $RawLogGroup.Group | Where-Object -FilterScript { $_.Id -eq '3077' }

                # If the main event is older than the specified date, skip it
                if (-NOT ([System.String]::IsNullOrWhiteSpace($Date))) {
                    if ($BlockedTemp.TimeCreated -lt $Date) {
                        continue
                    }
                }

                # Add the main event along with the correlated events to the collection
                $EventPackageCollections += [PSCustomObject]@{
                    MainEventData        = $BlockedTemp
                    CorrelatedEventsData = $RawLogGroup.Group | Where-Object -FilterScript { $_.Id -eq '3089' }
                    Type                 = 'Blocked'
                }
            }
        }

        #Region Output objects definition based on type
        # They return all the logs without post-processing
        [PSCustomObject[]]$OutputAudit = @()
        [PSCustomObject[]]$OutputBlocked = @()

        # They only return the logs of files that exist on the disk
        [PSCustomObject[]]$OutputExistingAudit = @()
        [PSCustomObject[]]$OutputExistingBlocked = @()

        # They only return the hash details of files no longer on the disk
        [PSCustomObject[]]$OutputDeletedAudit = @()
        [PSCustomObject[]]$OutputDeletedBlocked = @()

        # They return FilePaths of files on the disk and hash details of files not on the disk
        $OutputSeparatedAudit = [PSCustomObject]@{
            AvailableFilesPaths = [System.IO.FileInfo[]]@()
            DeletedFileHashes   = [PSCustomObject[]]@()
        }
        $OutputSeparatedBlocked = [PSCustomObject]@{
            AvailableFilesPaths = [System.IO.FileInfo[]]@()
            DeletedFileHashes   = [PSCustomObject[]]@()
        }
        #Endregion Output objects definition based on type
    }

    Process {

        # Loop over each event package in the collection
        foreach ($EventPackage in $EventPackageCollections) {

            # Extract the main event data
            [System.Diagnostics.Eventing.Reader.EventLogRecord]$Event = $EventPackage.MainEventData

            # Convert the main event data to XML object
            $Xml = [System.Xml.XmlDocument]$Event.ToXml()

            # Place each event data in a hashtable and repackage it into a custom object at the end for further processing
            [PSCustomObject[]]$ProcessedEvents = $Xml.event.EventData.data | ForEach-Object -Begin { $Hash = @{} } -Process { $Hash[$_.name] = $_.'#text' } -End { [pscustomobject]$Hash }

            # Loop over each event data object
            foreach ($Log in $ProcessedEvents) {

                # Add the TimeCreated property to the $Log object
                $Log | Add-Member -NotePropertyName 'TimeCreated' -NotePropertyValue $Event.TimeCreated
                # Add the ActivityId property to the $Log object
                $Log | Add-Member -NotePropertyName 'ActivityId' -NotePropertyValue $Event.ActivityId
                # Add the UserId property to the $Log object
                $Log | Add-Member -NotePropertyName 'UserId' -NotePropertyValue $Event.UserId

                # Filter the logs based on the policy that generated them
                if (-NOT ([System.String]::IsNullOrWhiteSpace($PolicyNames))) {
                    if ($Log.PolicyName -notin $PolicyNames) {
                        continue
                    }
                }

                # Define the regex pattern for the device path
                [System.Text.RegularExpressions.Regex]$Pattern = '\\Device\\HarddiskVolume(?<HardDiskVolumeNumber>\d+)\\(?<RemainingPath>.*)$'

                # replace the device path with the drive letter if it matches the pattern
                if ($Log.'File Name' -match $Pattern) {

                    # Use the primary method to fix the drive letter mappings
                    if ($AlternativeDriveLetterFix -eq $false) {

                        [System.UInt32]$HardDiskVolumeNumber = $Matches['HardDiskVolumeNumber']
                        [System.String]$RemainingPath = $Matches['RemainingPath']
                        [PSCustomObject]$GetLetter = $DriveLettersGlobalRootFix | Where-Object -FilterScript { $_.DevicePath -eq "\Device\HarddiskVolume$HardDiskVolumeNumber" }
                        [System.IO.FileInfo]$UsablePath = "$($GetLetter.DriveLetter)$RemainingPath"
                        $Log.'File Name' = $Log.'File Name' -replace $Pattern, $UsablePath
                    }
                    # Use the alternative method to fix the drive letter mappings
                    else {
                        $Log.'File Name' = $Log.'File Name' -replace "\\Device\\HarddiskVolume$($Matches['HardDiskVolumeNumber'])", "$($DriveLetterMappings[$Matches['HardDiskVolumeNumber']]):"
                    }
                }
                # sometimes the file name begins with System32 so we prepend the Windows directory to create a full resolvable path
                # https://learn.microsoft.com/en-us/dotnet/api/system.string.startswith
                elseif ($Log.'File Name'.StartsWith('System32', $true, [System.Globalization.CultureInfo]::InvariantCulture)) {
                    $Log.'File Name' = Join-Path -Path $Env:WinDir -ChildPath ($Log.'File Name')
                }

                # Replace these numbers in the logs with user-friendly strings that represent the signature level at which the code was verified
                $Log.'Requested Signing Level' = $ReqValSigningLevels[[System.UInt16]$Log.'Requested Signing Level']
                $Log.'Validated Signing Level' = $ReqValSigningLevels[[System.UInt16]$Log.'Validated Signing Level']

                # Replace the SI Signing Scenario numbers with a user-friendly string
                $Log.'SI Signing Scenario' = $Log.'SI Signing Scenario' -eq '0' ? 'Kernel-Mode' : 'User-Mode'

                # Translate the SID to a UserName
                Try {
                    [System.Security.Principal.SecurityIdentifier]$ObjSID = New-Object -TypeName System.Security.Principal.SecurityIdentifier($Log.UserId)
                    $Log.UserId = [System.String]($ObjSID.Translate([System.Security.Principal.NTAccount])).Value
                }
                Catch {
                    Write-Verbose -Message "Receive-CodeIntegrityLogs: Could not translate the SID $($Log.UserId) to a username."
                }

                # If there are correlated events, then process them
                if ($null -ne $EventPackage.CorrelatedEventsData) {

                    # Store the unique publisher name in an array
                    [System.String[]]$Publishers = @()

                    # Store the correlated logs in an array - these logs are processed into a custom object
                    [PSCustomObject[]]$CorrelatedLogs = @()

                    foreach ($CorrelatedEvent in $EventPackage.CorrelatedEventsData) {

                        # Convert the main event data to XML object
                        $XmlCorrelated = [System.Xml.XmlDocument]$CorrelatedEvent.ToXml()

                        # Place each event data in a hashtable and repackage it into a custom object at the end for further processing
                        [PSCustomObject[]]$ProcessedCorrelatedEvents = $XmlCorrelated.event.EventData.data | ForEach-Object -Begin { $Hash = @{} } -Process { $Hash[$_.name] = $_.'#text' } -End { [pscustomobject]$Hash }

                        # Loop over each event data object
                        foreach ($CorrelatedLog in $ProcessedCorrelatedEvents) {

                            # Replace the properties with their user-friendly strings
                            $CorrelatedLog.SignatureType = $SignatureTypeTable[[System.UInt16]$CorrelatedLog.SignatureType]
                            $CorrelatedLog.ValidatedSigningLevel = $ReqValSigningLevels[[System.UInt16]$CorrelatedLog.ValidatedSigningLevel]
                            $CorrelatedLog.VerificationError = $VerificationErrorTable[[System.UInt16]$CorrelatedLog.VerificationError]

                            # Add the Correlated Log to the array of Correlated Logs
                            $CorrelatedLogs += $CorrelatedLog

                            # Add the unique publisher name to the array of Publishers
                            if ($CorrelatedLog.PublisherName -notin $Publishers) {
                                $Publishers += $CorrelatedLog.PublisherName
                            }
                        }
                    }

                    Write-Debug -Message "Receive-CodeIntegrityLogs: The number of unique publishers in the correlated events is $($Publishers.Count)"
                    $Log | Add-Member -NotePropertyName 'Publishers' -NotePropertyValue $Publishers

                    # De-Duplicate the correlated logs based on specific properties
                    $CorrelatedLogs = $CorrelatedLogs | Group-Object -Property PublisherTBSHash, PublisherName, IssuerTBSHash, IssuerName |
                    ForEach-Object -Process { $_.Group[0] }

                    Write-Debug -Message "Receive-CodeIntegrityLogs: The number of correlated events is $($CorrelatedLogs.Count)"
                    $Log | Add-Member -NotePropertyName 'SignerInfo' -NotePropertyValue $CorrelatedLogs
                }

                # Add the Type property to the log object
                $Log | Add-Member -NotePropertyName 'Type' -NotePropertyValue $EventPackage.Type

                #Region Post-processing for the logs

                if ($Log.Type -eq 'Audit') {

                    # Add the log to the output object if it has Audit type
                    $OutputAudit += $Log

                    # If the file the log is referring to is currently on the disk
                    if (Test-Path -Path $Log.'File Name') {
                        $OutputExistingAudit += $Log
                        $OutputSeparatedAudit.AvailableFilesPaths += $Log.'File Name'
                    }
                    # If the file is not currently on the disk, extract its hashes from the log
                    else {
                        $TempDeletedOutputAudit = $Log | Select-Object -Property FileVersion, 'File Name', PolicyGUID, 'SHA256 Hash', 'SHA256 Flat Hash', 'SHA1 Hash', 'SHA1 Flat Hash'

                        $OutputDeletedAudit += $TempDeletedOutputAudit
                        $OutputSeparatedAudit.DeletedFileHashes += $TempDeletedOutputAudit
                    }
                }
                elseif ($Log.Type -eq 'Blocked') {

                    # Add the log to the output object if it has Blocked type
                    $OutputBlocked += $Log

                    # If the file the log is referring to is currently on the disk
                    if (Test-Path -Path $Log.'File Name') {
                        $OutputExistingBlocked += $Log
                        $OutputSeparatedBlocked.AvailableFilesPaths += $Log.'File Name'
                    }
                    # If the file is not currently on the disk, extract its hashes from the log
                    else {
                        $TempDeletedOutputBlocked = $Log | Select-Object -Property FileVersion, 'File Name', PolicyGUID, 'SHA256 Hash', 'SHA256 Flat Hash', 'SHA1 Hash', 'SHA1 Flat Hash'

                        $OutputDeletedBlocked += $TempDeletedOutputBlocked
                        $OutputSeparatedBlocked.DeletedFileHashes += $TempDeletedOutputBlocked
                    }
                }

                #Endregion Post-processing for the logs
            }
        }
    }

    End {
        Switch ($PostProcessing) {
            'Separate' {
                if ($Type -eq 'Audit') {
                    # De-duplication
                    $OutputSeparatedAudit.AvailableFilesPaths = $OutputSeparatedAudit.AvailableFilesPaths | Select-Object -Unique

                    $OutputSeparatedAudit.DeletedFileHashes = $OutputSeparatedAudit.DeletedFileHashes | Group-Object -Property 'File Name', ProductName, FileVersion, OriginalFileName, FileDescription, InternalName, PackageFamilyName, Publishers, 'SHA256 Hash', 'SHA256 Flat Hash' |
                    ForEach-Object -Process { $_.Group[0] }

                    Write-Verbose -Message "Receive-CodeIntegrityLogs: Returning $($OutputSeparatedAudit.AvailableFilesPaths.Count) Audit Code Integrity logs for files on the disk and $($OutputSeparatedAudit.DeletedFileHashes.Count) for the files not on the disk, in a nested object."
                    Return $OutputSeparatedAudit
                }
                else {
                    # De-duplication
                    $OutputSeparatedBlocked.AvailableFilesPaths = $OutputSeparatedBlocked.AvailableFilesPaths | Select-Object -Unique

                    $OutputSeparatedBlocked.DeletedFileHashes = $OutputSeparatedBlocked.DeletedFileHashes | Group-Object -Property 'File Name', ProductName, FileVersion, OriginalFileName, FileDescription, InternalName, PackageFamilyName, Publishers, 'SHA256 Hash', 'SHA256 Flat Hash' |
                    ForEach-Object -Process { $_.Group[0] }

                    Write-Verbose -Message "Receive-CodeIntegrityLogs: Returning $($OutputSeparatedBlocked.AvailableFilesPaths.Count) Blocked Code Integrity logs for files on the disk and $($OutputSeparatedBlocked.DeletedFileHashes.Count) for the files not on the disk, in a nested object."
                    Return $OutputSeparatedBlocked
                }
            }
            'OnlyExisting' {
                if ($Type -eq 'Audit') {
                    # De-duplication
                    $OutputExistingAudit = $OutputExistingAudit | Group-Object -Property 'File Name', ProductName, FileVersion, OriginalFileName, FileDescription, InternalName, PackageFamilyName, Publishers, 'SHA256 Hash', 'SHA256 Flat Hash' |
                    ForEach-Object -Process { $_.Group[0] }

                    Write-Verbose -Message "Receive-CodeIntegrityLogs: Returning $($OutputExistingAudit.Count) Audit Code Integrity logs for files on the disk."
                    Return $OutputExistingAudit
                }
                else {
                    # De-duplication
                    $OutputExistingBlocked = $OutputExistingBlocked | Group-Object -Property 'File Name', ProductName, FileVersion, OriginalFileName, FileDescription, InternalName, PackageFamilyName, Publishers, 'SHA256 Hash', 'SHA256 Flat Hash' |
                    ForEach-Object -Process { $_.Group[0] }

                    Write-Verbose -Message "Receive-CodeIntegrityLogs: Returning $($OutputExistingBlocked.Count) Blocked Code Integrity logs for files on the disk."
                    Return $OutputExistingBlocked
                }
            }
            'OnlyDeleted' {
                if ($Type -eq 'Audit') {
                    # De-duplication
                    $OutputDeletedAudit = $OutputDeletedAudit | Group-Object -Property 'File Name', ProductName, FileVersion, OriginalFileName, FileDescription, InternalName, PackageFamilyName, Publishers, 'SHA256 Hash', 'SHA256 Flat Hash' |
                    ForEach-Object -Process { $_.Group[0] }

                    Write-Verbose -Message "Receive-CodeIntegrityLogs: Returning $($OutputDeletedAudit.Count) Audit Code Integrity logs for files not on the disk."
                    Return $OutputDeletedAudit
                }
                else {
                    # De-duplication
                    $OutputDeletedBlocked = $OutputDeletedBlocked | Group-Object -Property 'File Name', ProductName, FileVersion, OriginalFileName, FileDescription, InternalName, PackageFamilyName, Publishers, 'SHA256 Hash', 'SHA256 Flat Hash' |
                    ForEach-Object -Process { $_.Group[0] }

                    Write-Verbose -Message "Receive-CodeIntegrityLogs: Returning $($OutputDeletedBlocked.Count) Blocked Code Integrity logs for files not on the disk."
                    Return $OutputDeletedBlocked
                }
            }
            Default {
                if ($Type -eq 'Audit') {
                    # De-duplication
                    $OutputAudit = $OutputAudit | Group-Object -Property 'File Name', ProductName, FileVersion, OriginalFileName, FileDescription, InternalName, PackageFamilyName, Publishers, 'SHA256 Hash', 'SHA256 Flat Hash' |
                    ForEach-Object -Process { $_.Group[0] }

                    Write-Verbose -Message "Receive-CodeIntegrityLogs: Returning $($OutputAudit.Count) Audit Code Integrity logs."
                    Return $OutputAudit
                }
                else {
                    # De-duplication
                    $OutputBlocked = $OutputBlocked | Group-Object -Property 'File Name', ProductName, FileVersion, OriginalFileName, FileDescription, InternalName, PackageFamilyName, Publishers, 'SHA256 Hash', 'SHA256 Flat Hash' |
                    ForEach-Object -Process { $_.Group[0] }

                    Write-Verbose -Message "Receive-CodeIntegrityLogs: Returning $($OutputBlocked.Count) Blocked Code Integrity logs."
                    Return $OutputBlocked
                }
            }
        }
    }
}
Export-ModuleMember -Function 'Receive-CodeIntegrityLogs'

# SIG # Begin signature block
# MIILkgYJKoZIhvcNAQcCoIILgzCCC38CAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCD+k6SyPzW8azo6
# gxH4PNwR1xpSh9r5XlH9lrym/I7OSaCCB9AwggfMMIIFtKADAgECAhMeAAAABI80
# LDQz/68TAAAAAAAEMA0GCSqGSIb3DQEBDQUAME8xEzARBgoJkiaJk/IsZAEZFgNj
# b20xIjAgBgoJkiaJk/IsZAEZFhJIT1RDQUtFWC1DQS1Eb21haW4xFDASBgNVBAMT
# C0hPVENBS0VYLUNBMCAXDTIzMTIyNzExMjkyOVoYDzIyMDgxMTEyMTEyOTI5WjB5
# MQswCQYDVQQGEwJVSzEeMBwGA1UEAxMVSG90Q2FrZVggQ29kZSBTaWduaW5nMSMw
# IQYJKoZIhvcNAQkBFhRob3RjYWtleEBvdXRsb29rLmNvbTElMCMGCSqGSIb3DQEJ
# ARYWU3B5bmV0Z2lybEBvdXRsb29rLmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIP
# ADCCAgoCggIBAKb1BJzTrpu1ERiwr7ivp0UuJ1GmNmmZ65eckLpGSF+2r22+7Tgm
# pEifj9NhPw0X60F9HhdSM+2XeuikmaNMvq8XRDUFoenv9P1ZU1wli5WTKHJ5ayDW
# k2NP22G9IPRnIpizkHkQnCwctx0AFJx1qvvd+EFlG6ihM0fKGG+DwMaFqsKCGh+M
# rb1bKKtY7UEnEVAsVi7KYGkkH+ukhyFUAdUbh/3ZjO0xWPYpkf/1ldvGes6pjK6P
# US2PHbe6ukiupqYYG3I5Ad0e20uQfZbz9vMSTiwslLhmsST0XAesEvi+SJYz2xAQ
# x2O4n/PxMRxZ3m5Q0WQxLTGFGjB2Bl+B+QPBzbpwb9JC77zgA8J2ncP2biEguSRJ
# e56Ezx6YpSoRv4d1jS3tpRL+ZFm8yv6We+hodE++0tLsfpUq42Guy3MrGQ2kTIRo
# 7TGLOLpayR8tYmnF0XEHaBiVl7u/Szr7kmOe/CfRG8IZl6UX+/66OqZeyJ12Q3m2
# fe7ZWnpWT5sVp2sJmiuGb3atFXBWKcwNumNuy4JecjQE+7NF8rfIv94NxbBV/WSM
# pKf6Yv9OgzkjY1nRdIS1FBHa88RR55+7Ikh4FIGPBTAibiCEJMc79+b8cdsQGOo4
# ymgbKjGeoRNjtegZ7XE/3TUywBBFMf8NfcjF8REs/HIl7u2RHwRaUTJdAgMBAAGj
# ggJzMIICbzA8BgkrBgEEAYI3FQcELzAtBiUrBgEEAYI3FQiG7sUghM++I4HxhQSF
# hqV1htyhDXuG5sF2wOlDAgFkAgEIMBMGA1UdJQQMMAoGCCsGAQUFBwMDMA4GA1Ud
# DwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAAMBsGCSsGAQQBgjcVCgQOMAwwCgYIKwYB
# BQUHAwMwHQYDVR0OBBYEFOlnnQDHNUpYoPqECFP6JAqGDFM6MB8GA1UdIwQYMBaA
# FICT0Mhz5MfqMIi7Xax90DRKYJLSMIHUBgNVHR8EgcwwgckwgcaggcOggcCGgb1s
# ZGFwOi8vL0NOPUhPVENBS0VYLUNBLENOPUhvdENha2VYLENOPUNEUCxDTj1QdWJs
# aWMlMjBLZXklMjBTZXJ2aWNlcyxDTj1TZXJ2aWNlcyxDTj1Db25maWd1cmF0aW9u
# LERDPU5vbkV4aXN0ZW50RG9tYWluLERDPWNvbT9jZXJ0aWZpY2F0ZVJldm9jYXRp
# b25MaXN0P2Jhc2U/b2JqZWN0Q2xhc3M9Y1JMRGlzdHJpYnV0aW9uUG9pbnQwgccG
# CCsGAQUFBwEBBIG6MIG3MIG0BggrBgEFBQcwAoaBp2xkYXA6Ly8vQ049SE9UQ0FL
# RVgtQ0EsQ049QUlBLENOPVB1YmxpYyUyMEtleSUyMFNlcnZpY2VzLENOPVNlcnZp
# Y2VzLENOPUNvbmZpZ3VyYXRpb24sREM9Tm9uRXhpc3RlbnREb21haW4sREM9Y29t
# P2NBQ2VydGlmaWNhdGU/YmFzZT9vYmplY3RDbGFzcz1jZXJ0aWZpY2F0aW9uQXV0
# aG9yaXR5MA0GCSqGSIb3DQEBDQUAA4ICAQA7JI76Ixy113wNjiJmJmPKfnn7brVI
# IyA3ZudXCheqWTYPyYnwzhCSzKJLejGNAsMlXwoYgXQBBmMiSI4Zv4UhTNc4Umqx
# pZSpqV+3FRFQHOG/X6NMHuFa2z7T2pdj+QJuH5TgPayKAJc+Kbg4C7edL6YoePRu
# HoEhoRffiabEP/yDtZWMa6WFqBsfgiLMlo7DfuhRJ0eRqvJ6+czOVU2bxvESMQVo
# bvFTNDlEcUzBM7QxbnsDyGpoJZTx6M3cUkEazuliPAw3IW1vJn8SR1jFBukKcjWn
# aau+/BE9w77GFz1RbIfH3hJ/CUA0wCavxWcbAHz1YoPTAz6EKjIc5PcHpDO+n8Fh
# t3ULwVjWPMoZzU589IXi+2Ol0IUWAdoQJr/Llhub3SNKZ3LlMUPNt+tXAs/vcUl0
# 7+Dp5FpUARE2gMYA/XxfU9T6Q3pX3/NRP/ojO9m0JrKv/KMc9sCGmV9sDygCOosU
# 5yGS4Ze/DJw6QR7xT9lMiWsfgL96Qcw4lfu1+5iLr0dnDFsGowGTKPGI0EvzK7H+
# DuFRg+Fyhn40dOUl8fVDqYHuZJRoWJxCsyobVkrX4rA6xUTswl7xYPYWz88WZDoY
# gI8AwuRkzJyUEA07IYtsbFCYrcUzIHME4uf8jsJhCmb0va1G2WrWuyasv3K/G8Nn
# f60MsDbDH1mLtzGCAxgwggMUAgEBMGYwTzETMBEGCgmSJomT8ixkARkWA2NvbTEi
# MCAGCgmSJomT8ixkARkWEkhPVENBS0VYLUNBLURvbWFpbjEUMBIGA1UEAxMLSE9U
# Q0FLRVgtQ0ECEx4AAAAEjzQsNDP/rxMAAAAAAAQwDQYJYIZIAWUDBAIBBQCggYQw
# GAYKKwYBBAGCNwIBDDEKMAigAoAAoQKAADAZBgkqhkiG9w0BCQMxDAYKKwYBBAGC
# NwIBBDAcBgorBgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQx
# IgQgzjFTSjtrwigp2bcZobnKOh0vqaFZ+5GO3XdrLptEylgwDQYJKoZIhvcNAQEB
# BQAEggIAAd/iAOjIxhjMQWV8RgzQcjyWbeuRfmIzGILOxPkAZ8Za39cnjd6enkBo
# IzvHIlI186QvKa4MJyKv46Wov96BF5aMAURTgrTcPqOrG6V7TP2TK8bQ/0IjIqSZ
# W5/7mKdxs8FTljTy/nlxJRg/7AavtyyO73Qh3Z+bejeBODR6A1lMLosBgyNjb10Z
# oFCqPoBouXHmT30dkc1j05NoAcp3lNn78KHjLCMam/mt9eAfedFmYZiaKoABpN1t
# T1GeT9jE+DbipgDj9dKcawudl2gQFM1pSt6kJtvm3orCZediyofZWMKznblQwtTn
# PRWF/mj0yEUJPWXJYU0MUGGYbXpNnMr3QjPoF73cM7ZrbIZzi6CXKqLVYni3fWum
# rQ3MRGyDEqr5aNM73K5NHUKheMSWM8tqR7vRm4wZbZZwAjB17VYsmWXyXOrWwJId
# FL7tbOj6ACCU3Xdo2t6JkD3SyX2cwtdlaLcRJhAexzqtdszfsXT+OLvWDPGHKeek
# OIw/mqcONic5O7yKWwsdRoNWZklfajcHb47QZhKUOgC6uE7qKtSlztWR5dmGYavu
# PLRhCPlfAzQRDWRlq3lwE6jcamruybDMMRAdrohuFPScZbWhyBCFpQmW4vzaInaW
# KCK/0qeex8rzqKZMMBcSyfqhiYfcFFf6CjiZXz0oIFzFIErxiNg=
# SIG # End signature block