Core/Invoke-WDACSimulation.psm1

Function Invoke-WDACSimulation {
    [CmdletBinding()]
    [OutputType([System.Collections.Generic.List[WDACConfig.SimulationOutput]], [System.Boolean])]
    Param(
        [ArgumentCompleter([WDACConfig.ArgCompleter.XmlFilePathsPicker])]
        [Alias('X')]
        [ValidateScript({ [WDACConfig.CiPolicyTest]::TestCiPolicy($_, $null) })]
        [Parameter(Mandatory = $true)][System.IO.FileInfo]$XmlFilePath,

        [ArgumentCompleter([WDACConfig.ArgCompleter.FolderPicker])]
        [Alias('D')]
        [ValidateScript({ [System.IO.Directory]::Exists($_) }, ErrorMessage = 'The path you selected is not a valid folder path.')]
        [Parameter(Mandatory = $false)][System.IO.DirectoryInfo[]]$FolderPath,

        [ArgumentCompleter([WDACConfig.ArgCompleter.MultipleAnyFilePathsPicker])]
        [Alias('F')]
        [ValidateScript({ [System.IO.File]::Exists($_) }, ErrorMessage = 'The path you selected is not a file path.')]
        [Parameter(Mandatory = $false)][System.IO.FileInfo[]]$FilePath,

        [Alias('B')]
        [Parameter(Mandatory = $false)][System.Management.Automation.SwitchParameter]$BooleanOutput,

        [Alias('C')]
        [Parameter(Mandatory = $false)][System.Management.Automation.SwitchParameter]$CSVOutput,

        [Alias('L')]
        [Parameter(Mandatory = $false)][System.Management.Automation.SwitchParameter]$Log,

        [Alias('N')]
        [Parameter(Mandatory = $false)][System.Management.Automation.SwitchParameter]$NoCatalogScanning,

        [ArgumentCompleter([WDACConfig.ArgCompleter.FolderPicker])]
        [Alias('Cat')]
        [ValidateScript({ [System.IO.Directory]::Exists($_) }, ErrorMessage = 'The path you selected is not a valid folder path.')]
        [Parameter(Mandatory = $false)][System.IO.DirectoryInfo[]]$CatRootPath,

        [Alias('CPU')]
        [Parameter(Mandatory = $false)][System.UInt32]$ThreadsCount = 2,

        [Alias('S')]
        [Parameter(Mandatory = $false)][System.Management.Automation.SwitchParameter]$SkipVersionCheck
    )
    Begin {
        [System.Boolean]$Verbose = $PSBoundParameters.Verbose.IsPresent ? $true : $false
        [System.Boolean]$Debug = $PSBoundParameters.Debug.IsPresent ? $true : $false
        # Only initialize the logger if this function wasn't called from another function, indicating the Verbose Preferences etc. were already set
        if ($(Get-PSCallStack).Count -le 2) {
            [WDACConfig.LoggerInitializer]::Initialize($VerbosePreference, $DebugPreference, $Host)
        }
        else {
            [WDACConfig.LoggerInitializer]::Initialize($null, $null, $Host)
        }
        . "$([WDACConfig.GlobalVars]::ModuleRootPath)\CoreExt\PSDefaultParameterValues.ps1"

        Write-Verbose -Message 'Importing the required sub-modules'
        Import-Module -Force -FullyQualifiedName @(
            "$([WDACConfig.GlobalVars]::ModuleRootPath)\Shared\Update-Self.psm1",
            "$([WDACConfig.GlobalVars]::ModuleRootPath)\WDACSimulation\Get-SignerInfo.psm1"
        )

        # if -SkipVersionCheck wasn't passed, run the updater
        if (-NOT $SkipVersionCheck) { Update-Self -InvocationStatement $MyInvocation.Statement }

        # Validate the $ThreadsCount parameter
        if ($ThreadsCount -lt 1 -or $ThreadsCount -gt [System.Environment]::ProcessorCount) {

            Write-Verbose -Message "The ThreadsCount parameter must be between 1 and $([System.Environment]::ProcessorCount), but you entered $ThreadsCount, setting it to $([System.Environment]::ProcessorCount)"
            $ThreadsCount = [System.Environment]::ProcessorCount
        }

        # The Concurrent Dictionary contains any and all of the Simulation results
        # Keys of it are the fil paths which aren't important, values are the important items needed at the end of the simulation
        $FinalSimulationResults = [System.Collections.Concurrent.ConcurrentDictionary[System.String, WDACConfig.SimulationOutput]]::new()

        # Start the transcript if the -Log switch is used and create a function to stop the transcript and the stopwatch at the end
        if ($Log) {
            Start-Transcript -IncludeInvocationHeader -LiteralPath (Join-Path -Path ([WDACConfig.GlobalVars]::UserConfigDir) -ChildPath "WDAC Simulation Log $(Get-Date -Format "MM-dd-yyyy 'at' HH-mm-ss").txt")

            # Create a new stopwatch object to measure the execution time
            Write-Verbose -Message 'Starting the stopwatch...'
            [System.Diagnostics.Stopwatch]$StopWatch = [Diagnostics.Stopwatch]::StartNew()
            Function Stop-Log {
                <#
                .SYNOPSIS
                    Stops the stopwatch and the transcription when the -Log switch is used with the Invoke-WDACSimulation cmdlet
                .Inputs
                    None
                .Outputs
                    System.Void
                #>

                [CmdletBinding()]
                [OutputType([System.Void])]
                param()

                Write-Verbose -Message 'Stopping the stopwatch'
                $StopWatch.Stop()
                Write-Verbose -Message "WDAC Simulation for $TotalSubSteps files completed in $($StopWatch.Elapsed.Hours) Hours - $($StopWatch.Elapsed.Minutes) Minutes - $($StopWatch.Elapsed.Seconds) Seconds - $($StopWatch.Elapsed.Milliseconds) Milliseconds - $($StopWatch.Elapsed.Microseconds) Microseconds - $($StopWatch.Elapsed.Nanoseconds) Nanoseconds" -Verbose

                Write-Verbose -Message 'Stopping the transcription'
                Stop-Transcript
            }
        }

        # The total number of the main steps for the progress bar to render
        $TotalSteps = 5us
        $CurrentStep = 0us

        # Make sure either -FolderPath or -FilePath is specified
        if (-not ($PSBoundParameters.ContainsKey('FolderPath') -or $PSBoundParameters.ContainsKey('FilePath'))) {
            # Write an error message
            Write-Error -Message 'You must specify either -FolderPath or -FilePath.' -Category InvalidArgument
        }

        # Check if the supplied XML file contains Allow all rule
        [System.Boolean]$HasAllowAllRule = $false

        # Get the content of the XML file
        [System.String]$XMLContent = Get-Content -LiteralPath $XmlFilePath -Raw

        #Region Making Sure No AllowAll Rule Exists

        # Using compiled regex for better performance
        $AllowAllRegex = [System.Text.RegularExpressions.Regex]::new('<Allow ID="ID_ALLOW_.*" FriendlyName=".*" FileName="\*".*/>', 'Compiled')
        if ($AllowAllRegex.IsMatch($XMLContent)) {
            Write-Verbose -Message "The supplied XML file '$($XmlFilePath.Name)' contains a rule that allows all files."


            [System.Void]$FinalSimulationResults.TryAdd([string]$CurrentFilePath, [WDACConfig.SimulationOutput]::New(
                    $null,
                    'AllowAllRule',
                    $true,
                    $null,
                    $null,
                    $null,
                    $null,
                    $null,
                    $null,
                    'Has AllowAll rule',
                    $null,
                    $null,
                    $null,
                    $null,
                    $null,
                    $null
                ))


            # Set a flag to exit the subsequent blocks
            $HasAllowAllRule = $true

            # Exit the Begin block
            Return
        }

        #Endregion Making Sure No AllowAll Rule Exists

        # Get the signer information from the XML
        [WDACConfig.Signer[]]$SignerInfo = Get-SignerInfo -XML ([System.Xml.XmlDocument]$XMLContent)

        # Extensions that are not supported by Authenticode. So if these files are not allowed by hash, they are not allowed at all
        $UnsignedExtensions = [System.Collections.Generic.HashSet[System.String]]::new(
            [System.String[]] ('.ocx', '.bat', '.bin'),
            # Make it case-insensitive
            [System.StringComparer]::InvariantCultureIgnoreCase
        )

        #Region FilePath Rule Checking
        Write-Verbose -Message 'Checking see if the XML policy has any FilePath rules'
        $FilePathRules = [System.Collections.Generic.HashSet[System.String]]@([WDACConfig.XmlFilePathExtractor]::GetFilePaths($XmlFilePath))

        [System.Boolean]$HasFilePathRules = $false
        if ($FilePathRules.Count -gt 0) {
            $HasFilePathRules = $true
        }
        #Endregion FilePath Rule Checking
    }

    process {
        try {
            # Only proceed if the policy doesn't have AllowAll rule
            if (!$HasAllowAllRule) {

                $CurrentStep++
                Write-Progress -Id 0 -Activity 'Parsing the Security Catalogs on the system' -Status "Step $CurrentStep/$TotalSteps" -PercentComplete ($CurrentStep / $TotalSteps * 100)

                if (!$NoCatalogScanning) {

                    # A dictionary where each key is a hash and value is the file path where that hash is found
                    $AllSecurityCatalogHashes = New-Object -TypeName 'System.Collections.Generic.Dictionary[String, String]'

                    # Loop through each .cat security catalog on the system - If user selected custom CatRoot folders then use them instead
                    foreach ($File in ([WDACConfig.FileUtility]::GetFilesFast(($CatRootPath ?? 'C:\Windows\System32\CatRoot'), $null, '.cat'))) {

                        # Get the hashes of the security catalog file
                        $CatHashes = [WDACConfig.MeowParser]::GetHashes($File)

                        # If the security catalog file has hashes, then add them to the dictionary
                        if ($CatHashes.Count -gt 0) {
                            foreach ($Hash in $CatHashes) {
                                [System.Void]$AllSecurityCatalogHashes.TryAdd($Hash, $File)
                            }
                        }
                    }
                }

                # Hash Sha256 values of all the file rules based on hash in the supplied xml policy file
                Write-Verbose -Message 'Getting the Sha256 Hash values of all the file rules based on hash in the supplied xml policy file'

                $CurrentStep++
                Write-Progress -Id 0 -Activity 'Getting the Sha256 Hash values from the XML file' -Status "Step $CurrentStep/$TotalSteps" -PercentComplete ($CurrentStep / $TotalSteps * 100)

                $SHA256HashesFromXML = [System.Collections.Generic.HashSet[System.String]]@(([WDACConfig.GetFileRuleOutput]::Get([System.Xml.XmlDocument]$XMLContent)).HashValue)

                # Get all of the file paths of the files that WDAC supports, from the user provided directory
                Write-Verbose -Message 'Getting all of the file paths of the files that WDAC supports, from the user provided directory'

                $CurrentStep++
                Write-Progress -Id 0 -Activity "Getting the supported files' paths" -Status "Step $CurrentStep/$TotalSteps" -PercentComplete ($CurrentStep / $TotalSteps * 100)
                $CollectedFiles = [System.Collections.Generic.HashSet[System.IO.FileInfo]]@([WDACConfig.FileUtility]::GetFilesFast($FolderPath, $FilePath, $null))

                # Make sure the selected directory contains files with the supported extensions
                if (!$CollectedFiles) { Throw 'There are no files in the selected directory that are supported by the WDAC engine.' }

                # Loop through each file
                Write-Verbose -Message 'Looping through each supported file'

                $CurrentStep++
                Write-Progress -Id 0 -Activity 'Looping through each supported file' -Status "Step $CurrentStep/$TotalSteps" -PercentComplete ($CurrentStep / $TotalSteps * 100)

                # split the file paths by $ThreadsCount which by default is 2
                $SplitArrays = [System.Linq.Enumerable]::Chunk($CollectedFiles, [System.Math]::Ceiling($CollectedFiles.Count / $ThreadsCount))

                # Loop over each split array
                [System.Management.Automation.Job2[]]$Jobs = foreach ($Array in $SplitArrays) {

                    # Create a new ThreadJob for each array of the filepaths to be processed concurrently
                    Start-ThreadJob -ArgumentList @($FinalSimulationResults, $Array, $SignerInfo, $SHA256HashesFromXML, $FilePathRules, $HasFilePathRules, $UnsignedExtensions, $AllSecurityCatalogHashes) -StreamingHost $Host -ScriptBlock {
                        param ($FinalSimulationResults, $Array, $SignerInfo, $SHA256HashesFromXML, $FilePathRules, $HasFilePathRules, $UnsignedExtensions, $AllSecurityCatalogHashes)

                        try {

                            $ErrorActionPreference = 'stop'

                            # only importing the functions that are required in this script block
                            Import-Module -Force -FullyQualifiedName @(
                                "$([WDACConfig.GlobalVars]::ModuleRootPath)\WDACSimulation\Compare-SignerAndCertificate.psm1",
                                "$([WDACConfig.GlobalVars]::ModuleRootPath)\WDACSimulation\Get-CertificateDetails.psm1"
                            )

                            [System.Drawing.Color]$CurrentColor = [System.Drawing.Color]::Violet
                            # Set the progress bar style to use violet color
                            $PSStyle.Progress.Style = "$($PSStyle.Foreground.FromRGB($CurrentColor.R, $CurrentColor.G, $CurrentColor.B))"

                            # The total number of the sub steps for the progress bar to render
                            [System.UInt64]$TotalSubSteps = $Array.Count
                            [System.UInt64]$CurrentSubStep = 0

                            foreach ($CurrentFilePath in $Array) {

                                $CurrentSubStep++
                                Write-Progress -Id ([runspace]::DefaultRunspace.Id) -Activity "Processing file $CurrentSubStep/$TotalSubSteps" -Status "$CurrentFilePath" -PercentComplete ($CurrentSubStep / $TotalSubSteps * 100)

                                # Check see if the file's hash exists in the XML file regardless of whether it's signed or not
                                # This is because WDAC policies sometimes have hash rules for signed files too
                                # So here we prioritize being authorized by file hash over being authorized by Signature

                                # Since Get-AppLockerFileInformation doesn't support special characters such as [ and ], and it doesn't have -LiteralPath parameter, we need to escape them ourselves
                                # [System.String]$CurrentFilePathHash = (Get-AppLockerFileInformation -Path $($CurrentFilePath -match '\[|\]' ? ($CurrentFilePath -replace '(\[|\])', '`$1') : $CurrentFilePath) -ErrorAction Stop).hash -replace 'SHA256 0x', ''

                                if ($HasFilePathRules -and $FilePathRules.Contains($CurrentFilePath)) {

                                    [System.Void]$FinalSimulationResults.TryAdd([string]$CurrentFilePath, [WDACConfig.SimulationOutput]::New(
                                ([System.IO.Path]::GetFileName($CurrentFilePath)),
                                            'FilePath',
                                            $true,
                                            $null,
                                            $null,
                                            $null,
                                            $null,
                                            $null,
                                            $null,
                                            'Allowed By File Path',
                                            $null,
                                            $null,
                                            $null,
                                            $null,
                                            $null,
                                            $CurrentFilePath
                                        ))

                                    Continue
                                }

                                try {
                                    [WDACConfig.AuthenticodePageHashes]$CurrentFileHashResult = [WDACConfig.AuthPageHash]::GetCiFileHashes($CurrentFilePath)
                                    [System.String]$CurrentFilePathHashSHA256 = $CurrentFileHashResult.SHA256Authenticode
                                    [System.String]$CurrentFilePathHashSHA1 = $CurrentFileHashResult.SHA1Authenticode
                                }
                                catch {

                                    [System.Void]$FinalSimulationResults.TryAdd([string]$CurrentFilePath, [WDACConfig.SimulationOutput]::New(
                                ([System.IO.Path]::GetFileName($CurrentFilePath)),
                                            'Signer',
                                            $false,
                                            $null,
                                            $null,
                                            $null,
                                            $null,
                                            $null,
                                            $null,
                                            'Not processed, Inaccessible file',
                                            $null,
                                            $null,
                                            $null,
                                            $null,
                                            $null,
                                            $CurrentFilePath
                                        ))
                                    Continue
                                }

                                # if the file's hash exists in the XML file then add the file's path to the allowed files and do not check anymore that whether the file is signed or not
                                if ($SHA256HashesFromXML.Contains($CurrentFilePathHashSHA256)) {

                                    [System.Void]$FinalSimulationResults.TryAdd([string]$CurrentFilePath, [WDACConfig.SimulationOutput]::New(
                                ([System.IO.Path]::GetFileName($CurrentFilePath)),
                                            'Hash',
                                            $true,
                                            $null,
                                            $null,
                                            $null,
                                            $null,
                                            $null,
                                            $null,
                                            'Hash Level',
                                            $null,
                                            $null,
                                            $null,
                                            $null,
                                            $null,
                                            $CurrentFilePath
                                        ))
                                }
                                # If the file's extension is not supported by Authenticode and it wasn't allowed by file hash then it's not allowed and no reason to check its signature
                                elseif ($UnsignedExtensions.Contains($CurrentFilePath.Extension)) {

                                    [System.Void]$FinalSimulationResults.TryAdd([string]$CurrentFilePath, [WDACConfig.SimulationOutput]::New(
                                ([System.IO.Path]::GetFileName($CurrentFilePath)),
                                            'Unsigned',
                                            $false,
                                            $null,
                                            $null,
                                            $null,
                                            $null,
                                            $null,
                                            $null,
                                            'Not Allowed',
                                            $null,
                                            $null,
                                            $null,
                                            $null,
                                            $null,
                                            $CurrentFilePath
                                        ))
                                }
                                # If the file's hash does not exist in the supplied XML file, then check its signature
                                else {

                                    try {
                                        [WDACConfig.AllCertificatesGrabber.AllFileSigners[]]$FileSignatureResults = [WDACConfig.AllCertificatesGrabber.WinTrust]::GetAllFileSigners($CurrentFilePath)

                                        # If there is no result then check if the file is allowed by a security catalog
                                        if ($FileSignatureResults.Count -eq 0) {

                                            if (!$NoCatalogScanning) {
                                                $MatchedHashResult = $AllSecurityCatalogHashes[$CurrentFilePathHashSHA1] ?? $AllSecurityCatalogHashes[$CurrentFilePathHashSHA256]
                                            }

                                            if (!$NoCatalogScanning -and $MatchedHashResult) {

                                                [WDACConfig.AllCertificatesGrabber.AllFileSigners]$CatalogSignerDits = ([WDACConfig.AllCertificatesGrabber.WinTrust]::GetAllFileSigners($MatchedHashResult))[0]

                                                # The file is authorized by a security catalog on the system
                                                [System.Void]$FinalSimulationResults.TryAdd([string]$CurrentFilePath, [WDACConfig.SimulationOutput]::New(
                                            ([System.IO.Path]::GetFileName($CurrentFilePath)),
                                                        'Catalog Signed',
                                                        $true,
                                                        $null,
                                                        $null,
                                                        $null,
                                                        $null,
                                                        $null,
                                                        $null,
                                                        'Catalog Hash',
                                                        $MatchedHashResult,
                                                        [WDACConfig.CryptoAPI]::GetNameString($CatalogSignerDits.Chain.ChainElements.Certificate[0].Handle, [WDACConfig.CryptoAPI]::CERT_NAME_SIMPLE_DISPLAY_TYPE, $null, $false),
                                                        [WDACConfig.CryptoAPI]::GetNameString($CatalogSignerDits.Chain.ChainElements.Certificate[0].Handle, [WDACConfig.CryptoAPI]::CERT_NAME_SIMPLE_DISPLAY_TYPE, $null, $true),
                                                        $CatalogSignerDits.Chain.ChainElements.Certificate[0].NotAfter,
                                                        [WDACConfig.CertificateHelper]::GetTBSCertificate($CatalogSignerDits.Chain.ChainElements.Certificate[0]),
                                                        $CurrentFilePath
                                                    ))
                                            }
                                            else {

                                                # The file is not signed and is not allowed by hash
                                                [System.Void]$FinalSimulationResults.TryAdd([string]$CurrentFilePath, [WDACConfig.SimulationOutput]::New(
                                            ([System.IO.Path]::GetFileName($CurrentFilePath)),
                                                        'Unsigned',
                                                        $false,
                                                        $null,
                                                        $null,
                                                        $null,
                                                        $null,
                                                        $null,
                                                        $null,
                                                        'Not Allowed',
                                                        $null,
                                                        $null,
                                                        $null,
                                                        $null,
                                                        $null,
                                                        $CurrentFilePath
                                                    ))
                                            }
                                        }
                                        else {
                                            # Use the Compare-SignerAndCertificate function to process it
                                            $ComparisonResult = Compare-SignerAndCertificate -SimulationInput ([WDACConfig.SimulationInput]::New(
                                                    $CurrentFilePath, # Path of the signed file
                                                (Get-CertificateDetails -CompleteSignatureResult $FileSignatureResults), # Get all of the details of all certificates of the signed file
                                                    $SignerInfo, # The entire Signer Info of the WDAC Policy file
                                                    $FileSignatureResults.Signer.SignerInfos.Certificate.EnhancedKeyUsageList.ObjectId # The EKU OIDs of the primary signer of the file, just like the output of the Get-AuthenticodeSignature cmdlet, the ones that WDAC policy uses for EKU-based authorization
                                                ))

                                            [System.Void]$FinalSimulationResults.TryAdd($CurrentFilePath, $ComparisonResult)
                                        }
                                    }
                                    # Handle the HashMismatch situations
                                    catch [WDACConfig.AllCertificatesGrabber.ExceptionHashMismatchInCertificate] {

                                        [System.Void]$FinalSimulationResults.TryAdd([string]$CurrentFilePath, [WDACConfig.SimulationOutput]::New(
                                    ([System.IO.Path]::GetFileName($CurrentFilePath)),
                                                'Signer',
                                                $false,
                                                $null,
                                                $null,
                                                $null,
                                                $null,
                                                $null,
                                                $null,
                                                'Hash Mismatch',
                                                $null,
                                                $null,
                                                $null,
                                                $null,
                                                $null,
                                                $CurrentFilePath
                                            ))
                                    }
                                    # Handle any other error by storing the file path and the reason for the error to display to the user

                                    catch {
                                        # If the file is signed but has unknown signature status
                                        [System.Void]$FinalSimulationResults.TryAdd([string]$CurrentFilePath, [WDACConfig.SimulationOutput]::New(
                                    ([System.IO.Path]::GetFileName($CurrentFilePath)),
                                                'Signer',
                                                $false,
                                                $null,
                                                $null,
                                                $null,
                                                $null,
                                                $null,
                                                $null,
                                                "UnknownError: $_",
                                                $null,
                                                $null,
                                                $null,
                                                $null,
                                                $null,
                                                $CurrentFilePath
                                            ))
                                    }
                                }
                            }
                        }
                        catch {
                            throw $_
                        }
                        finally {
                            # Complete the nested progress bar whether there was an error or not
                            Write-Progress -Id ([runspace]::DefaultRunspace.Id) -Activity 'All of the files have been processed.' -Completed
                        }
                    } -ThrottleLimit $ThreadsCount
                }

                # Wait for all jobs, grab any error that might've ocurred in them and then remove them
                if ($Jobs.count -gt 0) {
                    $null = Wait-Job -Job $Jobs
                    Receive-Job -Job $Jobs
                    Remove-Job -Job $Jobs
                }

            }

            $CurrentStep++
            Write-Progress -Id 0 -Activity 'Preparing the output' -Status "Step $CurrentStep/$TotalSteps" -PercentComplete ($CurrentStep / $TotalSteps * 100)

            # If the user selected the -BooleanOutput switch, then return a boolean value and don't display any more output
            if ($BooleanOutput) {

                # Get all of the allowed files
                $AllAllowedRules = $FinalSimulationResults.Values.Where({ $_.IsAuthorized -eq $true })
                # Get all of the blocked files
                $BlockedRules = $FinalSimulationResults.Values.Where({ $_.IsAuthorized -eq $false })

                Write-Verbose -Message "Allowed files: $($AllAllowedRules.count)"
                Write-Verbose -Message "Blocked files: $($BlockedRules.count)"

                # If the array of allowed files is not empty
                if (-NOT ([System.String]::IsNullOrWhiteSpace($AllAllowedRules))) {

                    # If the array of blocked files is empty
                    if ([System.String]::IsNullOrWhiteSpace($BlockedRules)) {
                        Return $true
                    }
                    else {
                        Return $false
                    }
                }
                else {
                    Return $false
                }
            }

            # Export the output as CSV
            if ($CSVOutput) {
                $FinalSimulationResults.Values | Sort-Object -Property IsAuthorized -Descending | Export-Csv -LiteralPath (Join-Path -Path ([WDACConfig.GlobalVars]::UserConfigDir) -ChildPath "WDAC Simulation Output $(Get-Date -Format "MM-dd-yyyy 'at' HH-mm-ss").csv") -Force
            }

            # Change the color of the Table header to SkyBlue
            $PSStyle.Formatting.TableHeader = "$($PSStyle.Foreground.FromRGB(135,206,235))"

            if ($FinalSimulationResults.Count -gt 10000) {
                # If the result is too big and the user forgot to use CSV Output then output everything to CSV instead of trying to display on the console
                if (!$CSVOutput) {
                    $FinalSimulationResults.Values | Sort-Object -Property IsAuthorized -Descending | Export-Csv -LiteralPath (Join-Path -Path ([WDACConfig.GlobalVars]::UserConfigDir) -ChildPath "WDAC Simulation Output $(Get-Date -Format "MM-dd-yyyy 'at' HH-mm-ss").csv") -Force
                }
                Return "The number of files is too many to display on the console. Saving the results in a CSV file in '$((Join-Path -Path ([WDACConfig.GlobalVars]::UserConfigDir) -ChildPath "WDAC Simulation Output $(Get-Date -Format "MM-dd-yyyy 'at' HH-mm-ss").csv"))'"
            }

            # Return the final main output array as a table
            Return $FinalSimulationResults.Values | Select-Object -Property 'Path',
            @{
                Label      = 'Source'
                Expression =
                { switch ($_.Source) {
                        { $_ -eq 'Signer' } { $Color = "$($PSStyle.Foreground.FromRGB(152,255,152))" }
                        { $_ -eq 'Hash' } { $Color = "$($PSStyle.Foreground.FromRGB(255,255,49))" }
                        { $_ -eq 'Unsigned' } { $Color = "$($PSStyle.Foreground.FromRGB(255,20,147))" }
                    }
                    "$Color$($_.Source)$($PSStyle.Reset)" # Use PSStyle to reset the color
                }
            },
            @{
                Label      = 'IsAuthorized'
                Expression =
                {
                    switch ($_.IsAuthorized) {
                        { $_ -eq $true } { $Color = "$($PSStyle.Foreground.FromRGB(255,0,255))"; break }
                        { $_ -eq $false } { $Color = "$($PSStyle.Foreground.FromRGB(255,165,0))$($PSStyle.Blink)"; break }
                    }
                    "$Color$($_.IsAuthorized)$($PSStyle.Reset)" # Use PSStyle to reset the color
                }
            },
            @{
                Label      = 'MatchCriteria'
                Expression = {
                    # If the MatchCriteria starts with 'UnknownError', truncate it to 50 characters. The full string will be displayed in the CSV output file. If it does not then just display it as it is
                    $_.MatchCriteria -match 'UnknownError' ? $_.MatchCriteria.Substring(0, 50) + '...' : "$($_.MatchCriteria)"
                }
            },
            @{
                Label      = 'SpecificFileName'
                Expression = {
                    $_.SpecificFileNameLevelMatchCriteria
                }
            } | Sort-Object -Property IsAuthorized | Format-Table

        }
        catch {
            throw $_
        }
        finally {
            Write-Progress -Id 0 -Activity 'WDAC Simulation completed.' -Completed
            # If the -Log switch is used, then stop the stopwatch and the transcription
            if ($Log) { Stop-Log }
        }
    }

    <#
.SYNOPSIS
    Simulates the deployment of the WDAC policy. It can produce a very detailed CSV file that contains the output of the simulation.
    On the console, it can display a table that shows the file path, source, MatchCriteria, and whether the file is allowed or not.
    The console results are color coded for easier reading.
 
    Properties explanation:
 
    FilePath: The name of the file gathered from its full path. (the actual long path of the file is not displayed in the console output, only in the CSV file)
    Source: The source of the file's MatchCriteria, e.g., 'Signer' (For signed files only), 'Hash' (For signed and unsigned files), 'Unsigned' (For unsigned files only)
    MatchCriteria: The reason the file is allowed or not. For files authorized by FilePublisher level, it will show the specific file name level that the file is authorized by. (https://learn.microsoft.com/en-us/windows/security/application-security/application-control/windows-defender-application-control/design/select-types-of-rules-to-create#table-3--specificfilenamelevel-options)
    IsAuthorized: A boolean value that indicates whether the file is allowed or not.
.LINK
    https://github.com/HotCakeX/Harden-Windows-Security/wiki/Invoke-WDACSimulation
.DESCRIPTION
    Simulates the deployment of the WDAC policy by analyzing a folder (recursively) or files and checking which of the detected files are allowed by a user selected policy xml file
.COMPONENT
    Windows Defender Application Control
    WDACConfig
.FUNCTIONALITY
    Simulates the deployment of the WDAC policy
.PARAMETER FolderPath
    Provide path to a folders that you want WDAC simulation to run against
 
    Takes the paths of the folders literally as typed including Special characters such as [ and ]
 
    Does not support wildcards
.PARAMETER FilePath
    Provide path to files that you want WDAC simulation to run against
 
    Takes the paths of the files literally as typed including Special characters such as [ and ]
 
    Does not support wildcards
.PARAMETER XmlFilePath
    Provide path to a policy xml file that you want the cmdlet to simulate its deployment and running files against it
 
    Takes the paths of the files literally as typed including Special characters such as [ and ]
 
    Does not support wildcards
.PARAMETER Log
    Use this switch to start a transcript of the WDAC simulation and log everything displayed on the screen.
    Use -Verbose parameter to produce more output on the console during the simulation operation.
    The log file is saved in the WDACConfig folder: C:\Program Files\WDACConfig
.PARAMETER NoCatalogScanning
    Bypass the scanning of the security catalogs on the system
.PARAMETER CatRootPath
    Provide path(s) to directories where security catalog .cat files are located. If not provided, the default path is C:\Windows\System32\CatRoot
.PARAMETER CSVOutput
    Exports the output to a CSV file. The CSV output is saved in the WDACConfig folder: C:\Program Files\WDACConfig
.PARAMETER ThreadsCount
    The number of the concurrent/parallel tasks to use when performing WDAC Simulation.
    By default it uses 2 parallel tasks.
    Max is the number of your system's CPU cores.
    Min is 1.
    If you enter a number higher than your system's CPU cores, it will be set to your system's CPU cores count.
.PARAMETER Verbose
    Shows verbose output
.PARAMETER BooleanOutput
    Returns a boolean value instead of displaying the object output
.PARAMETER SkipVersionCheck
    Bypass the online version check - only to be used in rare cases
.INPUTS
    System.IO.FileInfo[]
    System.IO.DirectoryInfo[]
    System.Management.Automation.SwitchParameter
.OUTPUTS
    System.Collections.Generic.List[WDACConfig.SimulationOutput]
    System.Boolean
.EXAMPLE
    Invoke-WDACSimulation -FolderPath 'C:\Windows\System32' -XmlFilePath 'C:\Users\HotCakeX\Desktop\Policy.xml'
    This example will simulate the deployment of the policy.xml file against the C:\Windows\System32 folder
.NOTES
    WDAC templates such as 'Default Windows' and 'Allow Microsoft' don't have CertPublisher element in their Signers because they don't target a leaf certificate,
    thus they weren't created using FilePublisher level, they were created using Publisher or Root certificate levels to allow Microsoft's wellknown certificates.
#>

}