AppLockerAdmin.psm1

Function Invoke-ParallellJob
{
    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory)]
        [System.Management.Automation.ScriptBlock]$CodeBlock,
        [Parameter(Mandatory)]
        [String[]]$ComputerName,
        [System.Collections.Hashtable]$Parameters,
        [ValidateRange(1, 30)]
        [Int]$JobLimit = 10
    )

    Begin {
        Function Get-JobState
        {
            Param ($Job)

            $Flag = 'static','nonpublic','instance'

            $_Worker = $Job.PowerShell.GetType().GetField('worker', $Flag)
            $Worker = $_Worker.GetValue($Job.PowerShell)

            $_CRP = $worker.GetType().GetProperty('CurrentlyRunningPipeline',$Flag)
            $CRP = $_CRP.GetValue($Worker)
            If ($Job.handle.IsCompleted -AND -NOT [bool]$CRP) {
                'Completed'
            }
            ElseIf (-NOT $Job.handle.IsCompleted -AND [bool]$CRP) {
                'Running'
            }
            ElseIf (-NOT $Job.handle.IsCompleted -AND -NOT [bool]$CRP) {
                'NotStarted'
            }
        }

        $RunSpacePool = [RunSpaceFactory]::CreateRunspacePool(1, $JobLimit)
        $Posh = [PowerShell]::Create()
        $Posh.RunSpacePool = $RunSpacePool
        $RunSpacePool.Open()

        $Jobs = [System.Collections.ArrayList]::new()
    }

    Process {
        ## Kick off jobs async
        foreach ($Computer in $ComputerName)
        {
            $PoshInstance = [powershell]::Create()
            $PoshInstance.RunSpacePool = $RunSpacePool
            [Void]$PoshInstance.AddScript($CodeBlock)
            [Void]$PoshInstance.AddParameter('ComputerName', $Computer)

            if ($PSBoundParameters.ContainsKey('Parameters'))
            {
                $Parameters.GetEnumerator() | ForEach-Object `
                {
                    [Void]$PoshInstance.AddParameter("$($_.Name)", $_.Value)
                }
            }
            $Handle = $PoshInstance.BeginInvoke()

            $Temp = '' | Select-Object -Property PowerShell, Handle
            $Temp.PowerShell = $PoshInstance
            $Temp.Handle = $Handle

            [Void]$Jobs.Add($Temp)
        }

        ## Monitor jobs
        $TotalJobCount = ($Jobs | Measure-Object).Count
        $ProgressId = Get-Random

        Write-Verbose -Message "Starting monitoring of $TotalJobCount jobs.."

        Do
        {
            $JobStates = Foreach ($Job in $Jobs) { Get-JobState -Job $Job }
            $NotStarted = ($JobStates | Where-Object { $_ -eq 'NotStarted' } | Measure-Object).Count
            $Running = ($JobStates | Where-Object { $_ -eq 'Running' } | Measure-Object).Count
            $Completed = ($JobStates | Where-Object { $_ -eq 'Completed' } | Measure-Object).Count

            Write-Progress -Activity "Getting AppLocker information.." -Status "Total $TotalJobCount | Not Started $($NotStarted) | Running $($Running) | Completed $($Completed)" -Id $ProgressId -PercentComplete ([Math]::Floor(($Completed / $TotalJobCount) * 100))
            Start-Sleep -Seconds 1
        } Until ($Completed -eq $TotalJobCount)

        ## Return data
        Write-Verbose -Message "Fetching data from jobs.."
        $Jobs | ForEach-Object { $_.PowerShell.EndInvoke($_.Handle) }
    }

    End
    {
        ## Cleanup
        Write-Verbose -Message "Cleaning up jobs.."
        $Jobs | ForEach-Object { $_.PowerShell.Dispose() }
        $RunSpacePool.Close()
        $RunSpacePool.Dispose()
    }
}

Function Get-AppLockerEvent
{
<#
.SYNOPSIS
    Fetch AppLocker events from multiple system in parallell
 
.DESCRIPTION
    Fetch AppLocker events from multiple system in parallell.
    Uses PowerShell runspace to perform fast remote code execution in parallell against multiple systems.
    Shows a progressbar to display progress.
 
.EXAMPLE
    $Ev = Get-AppLockerEvent -ComputerName 'Server1', 'Server2' -MaxAgeHours 2 -Type Block -SkipPSScriptPolicyTest -Verbose
    $Ev = Get-AppLockerEvent -OU "OU=Servers,DC=domain,DC=local" -MaxAgeHours 2 -Type Block -SkipPSScriptPolicyTest -Verbose
 
.NOTES
    Author: Martin Norlunn
    Updated: 12.06.2019
 
#>

    [Cmdletbinding(DefaultParameterSetName="ComputerName")]
    Param
    (
        [Parameter(Mandatory, ParameterSetName="ComputerName")]
        [String[]]$ComputerName,
        [Parameter(Mandatory, ParameterSetName="OU")]
        [String]$OU,
        [Parameter(Mandatory, ParameterSetName="ADGroup")]
        [String]$ADGroup,
        [ValidateSet('All', 'Block', 'Audit')]
        [string]$EventType = 'All',
        [ValidateSet('All', 'EXE and DLL', 'MSI and Script', 'Packaged App')]
        [string]$FileType = 'All',
        [ValidateRange(1, 730)]
        [Int]$MaxAgeDays,
        [ValidateRange(1, 17520)]
        [Int]$MaxAgeHours,
        [Switch]$SkipAppDataTemp,
        [Switch]$SkipNormalUsers,
        [Switch]$SkipPSScriptPolicyTest
    )

    $Watch = [System.Diagnostics.Stopwatch]::StartNew()

    if ($PSBoundParameters.ContainsKey('OU'))
    {
        $ComputerName = Get-ADComputer -Filter * -SearchBase $OU | Select-Object -ExpandProperty Name
    }
    elseif ($PSBoundParameters.ContainsKey('ADGroup'))
    {
        $ComputerName = Get-ADGroupMember -Identity "$ADGroup" | Where-Object objectClass -eq 'computer' | Select-Object -ExpandProperty Name
    }

    if ($PSBoundParameters.ContainsKey('MaxAgeDays'))
    {
        $Date = (Get-Date).AddDays(-1 * $MaxAgeDays)
    }
    elseif ($PSBoundParameters.ContainsKey('MaxAgeHours'))
    {
        $Date = (Get-Date).AddHours(-1 * $MaxAgeHours)
    }
    else {
        $Date = (Get-Date).AddYears(-2)
    }

    $EventId = switch ($EventType)
    {
        'All' { 8003, 8004, 8006, 8007, 8024, 8025 }
        'Block' { 8004, 8007, 8025 }
        'Audit' { 8003, 8006, 8024 }
    }

    Write-Verbose -Message "StartDate = $($Date)"
    Write-Verbose -Message "EventId = $($EventId -join ', ')"

    Write-Verbose -Message "Collecting events.."

    $Parameters = @{
        StartDate = $Date
        Id = $EventId
        FileType = $FileType
    }

    $Events = Invoke-ParallellJob -ComputerName $ComputerName -Parameters $Parameters -JobLimit 30 -Verbose:$VerbosePreference -CodeBlock ({
        Param ($ComputerName, $StartDate, $Id)

        $Options = New-PSSessionOption -OpenTimeout (New-TimeSpan -Seconds 20)
        Invoke-Command -ComputerName $ComputerName -SessionOption $Options -ScriptBlock {
            Param($StartDate, $Id, $FileType)

            $Logs = @(
                "Microsoft-Windows-AppLocker/EXE and DLL"
                "Microsoft-Windows-AppLocker/MSI and Script"
                "Microsoft-Windows-AppLocker/Packaged app-Deployment"
                "Microsoft-Windows-AppLocker/Packaged app-Execution"
            )

            if ($FileType -ne 'All')
            {
                $Logs = $Logs | Where-Object { $_ -like "*$FileType*" }
            }

            $Events = @()
            $AppLockerEvents = @()

            foreach ($Log in $Logs)
            {
                $Events += Get-WinEvent -FilterHashtable @{
                    LogName = $Log
                    StartTime = $StartDate
                    ID = @($Id)
                } -ErrorAction SilentlyContinue

                $AppLockerEvents += Get-AppLockerFileInformation -EventLog -LogPath $Log -EventType Audited -Statistics
            }

            $SIDTable = [Ordered]@{}
            $Events.UserId | Select-Object -Unique | ForEach-Object `
            {
                $SIDTable.Add("$_", (New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList "$_").Translate([System.Security.Principal.NTAccount]).Value)
            }

            foreach ($Ev in $Events)
            {
                $AEv = $AppLockerEvents | Where-Object { $Ev.Message -like "*$($_.FilePath.Path)*" } | Select-Object -First 1
                New-Object -TypeName PSObject -Property @{
                    LogName = $Ev.LogName
                    Id = $Ev.Id
                    TimeCreated = $Ev.TimeCreated
                    Message = $Ev.Message
                    FilePublisher = $Aev.FilePublisher
                    FilePath = $Aev.FilePath.Path
                    FileHash = $Aev.FileHash
                    User = $SIDTable[$Ev.UserId.Value]
                    ComputerName = $Ev.MachineName
                }
            }
        } -ArgumentList $StartDate, $Id
    })

    if ($SkipPSScriptPolicyTest.IsPresent)
    {
        Write-Verbose -Message "Skipping events of type __PSScriptPolicyTest_"
        $Events = $Events | Where-Object Message -notlike "*\APPDATA\LOCAL\TEMP\*__PSSCRIPTPOLICYTEST_*"
    }

    if ($SkipAppDataTemp.IsPresent)
    {
        Write-Verbose -Message "Skipping events with path matching C:\users\*\APPDATA\LOCAL\TEMP*"
        $Events = $Events | Where-Object Message -notlike "*C:\users\*\APPDATA\LOCAL\TEMP*"
    }

    if ($SkipNormalUsers.IsPresent)
    {
        Write-Verbose -Message "Including only service account and system events"
        $Events = $Events | Where-Object { ($_.User -split '\\' | Select-Object -Last 1) -like "svc_*" }
    }

    Write-Verbose -Message "Found $(($Events | Measure-Object).Count) events on $ComputerName"

    $Events | Select-Object -Property ComputerName, User, TimeCreated, LogName, Id, Message, FilePath, FilePublisher, FileHash

    $Watch.Stop()
    Write-Verbose -Message "Finished collecting events from $(($ComputerName | Measure-Object).Count) computer(s)"
    Write-Verbose -Message "Total script execution time: $($Watch.Elapsed.ToString())"
}

Function Get-AppLockerConfiguration
{
<#
.SYNOPSIS
    Fetch AppLocker configuration from multiple system in parallell
 
.DESCRIPTION
    Fetch AppLocker configuration from multiple system in parallell.
    Uses PowerShell runspace to perform fast remote code execution in parallell against multiple systems.
    Shows a progressbar to display progress.
 
.EXAMPLE
    $Config = Get-AppLockerConfiguration -ComputerName 'Server1', 'Server2' -Verbose
 
.EXAMPLE
    $Config = Get-AppLockerConfiguration -OU "OU=Servers,DC=domain,DC=local" -Verbose
 
.EXAMPLE
    $Config = Get-AppLockerConfiguration -ADGroup MyComputerGroup -Verbose
 
.NOTES
    Author: Martin Norlunn
    Updated: 12.06.2019
 
#>

    [Cmdletbinding(DefaultParameterSetName="ComputerName")]
    Param
    (
        [Parameter(Mandatory, ParameterSetName="ComputerName")]
        [String[]]$ComputerName,
        [Parameter(Mandatory, ParameterSetName="OU")]
        [String]$OU,
        [Parameter(Mandatory, ParameterSetName="ADGroup")]
        [String]$ADGroup
    )

    $Watch = [System.Diagnostics.Stopwatch]::StartNew()

    if ($PSBoundParameters.ContainsKey('OU'))
    {
        $ComputerName = Get-ADComputer -Filter * -SearchBase $OU | Select-Object -ExpandProperty Name
    }
    elseif ($PSBoundParameters.ContainsKey('ADGroup'))
    {
        $ComputerName = Get-ADGroupMember -Identity "$ADGroup" | Where-Object objectClass -eq 'computer' | Select-Object -ExpandProperty Name
    }

    Invoke-ParallellJob -ComputerName $ComputerName -JobLimit 30 -Verbose:$VerbosePreference -CodeBlock ({
        Param ($ComputerName)

        $Options = New-PSSessionOption -OpenTimeout (New-TimeSpan -Seconds 20)
        Invoke-Command -ComputerName $ComputerName -SessionOption $Options -ScriptBlock {
            $Rules = ForEach ($Rule in ([XML](Get-AppLockerPolicy -Effective -Xml)).AppLockerPolicy.RuleCollection)
            {
                $Rule.FilePublisherRule | ForEach-Object {
                    $_ | Select-Object -Property @{
                        Name = 'Type'
                        Expression = {
                            $Rule.Type
                        }
                    }, @{
                        Name = 'Enforced'
                        Expression = {
                            $Rule.EnforcementMode
                        }
                    }, Action, @{
                        Name = 'Principal'
                        Expression = {
                            (New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList $_.UserOrGroupSid).Translate([System.Security.Principal.NTAccount]).Value
                        }
                    }, Name, Description, @{
                        Name = 'Publisher'
                        Expression = {
                            $_.Conditions.FilePublisherCondition | Select-Object -Property PublisherName, ProductName, BinaryName, @{
                                Name = 'BinaryVersion'
                                Expression = {
                                    if ($_.BinaryVersionRange.LowSection -eq '*' -and $_.BinaryVersionRange.HighSection -eq '*')
                                    { '*' }
                                    elseif ($_.BinaryVersionRange.LowSection -eq '*' -and $_.BinaryVersionRange.HighSection -ne '*')
                                    { "<$($_.BinaryVersionRange.HighSection)" }
                                    elseif ($_.BinaryVersionRange.LowSection -ne '*' -and $_.BinaryVersionRange.HighSection -eq '*')
                                    { ">$($_.BinaryVersionRange.HighSection)" }
                                    else
                                    { "$($_.BinaryVersionRange.LowSection) to $($_.BinaryVersionRange.HighSection)" }
                                }
                            }
                        }
                    }
                }

                $Rule.FilePathRule | ForEach-Object {
                    $_ | Select-Object -Property @{
                        Name = 'Type'
                        Expression = {
                            $Rule.Type
                        }
                    }, @{
                        Name = 'Enforced'
                        Expression = {
                            $Rule.EnforcementMode
                        }
                    }, Action, @{
                        Name = 'Principal'
                        Expression = {
                            (New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList $_.UserOrGroupSid).Translate([System.Security.Principal.NTAccount]).Value
                        }
                    }, Name, Description, @{
                        Name = 'FilePath'
                        Expression = {
                            $_.Conditions.FilePathCondition.Path
                        }
                    }
                }
            }

            New-Object -TypeName PSObject -Property @{
                ComputerName = $env:COMPUTERNAME
                Service = Get-Service -Name AppIdSvc | Select-Object -ExpandProperty Status
                RuleCollections = (Get-AppLockerPolicy -Effective).RuleCollections | Select-Object -Property RuleCollectionType, @{
                    Name = 'Enforced'
                    Expression = {
                        if ($_.EnforcementMode -eq 'Enabled'){ 'Yes' } else { 'No' }
                    }
                }, Count
                Rules = $Rules
                Logs = @{
                    'EXE and DLL' = @{
                        MaxSizeBytes = Get-WinEvent -ListLog 'Microsoft-Windows-AppLocker/EXE and DLL' | Select-Object -ExpandProperty MaximumSizeInBytes
                        EventCount = Get-WinEvent -ListLog 'Microsoft-Windows-AppLocker/EXE and DLL' | Select-Object -ExpandProperty RecordCount
                    }

                    'MSI and Script' = @{
                        MaxSizeBytes = Get-WinEvent -ListLog 'Microsoft-Windows-AppLocker/MSI and Script' | Select-Object -ExpandProperty MaximumSizeInBytes
                        EventCount = Get-WinEvent -ListLog 'Microsoft-Windows-AppLocker/MSI and Script' | Select-Object -ExpandProperty RecordCount
                    }

                    'PackagedAppDeployment' = @{
                        MaxSizeBytes = Get-WinEvent -ListLog 'Microsoft-Windows-AppLocker/Packaged app-Deployment' | Select-Object -ExpandProperty MaximumSizeInBytes
                        EventCount = Get-WinEvent -ListLog 'Microsoft-Windows-AppLocker/Packaged app-Deployment' | Select-Object -ExpandProperty RecordCount
                    }

                    'PackagedAppExecution' = @{
                        MaxSizeBytes = Get-WinEvent -ListLog 'Microsoft-Windows-AppLocker/Packaged app-Execution' | Select-Object -ExpandProperty MaximumSizeInBytes
                        EventCount = Get-WinEvent -ListLog 'Microsoft-Windows-AppLocker/Packaged app-Execution' | Select-Object -ExpandProperty RecordCount
                    }
                }
            }
        } | Select-Object -Property ComputerName, Service, RuleCollections, Rules, Logs
    })

    $Watch.Stop()
    Write-Verbose -Message "Total script execution time: $($Watch.Elapsed.ToString())"
}