Public/security/Get-AuditPolicy.ps1

#Requires -Version 5.1

function Get-AuditPolicy {
    <#
    .SYNOPSIS
        Report advanced audit policy subcategory settings from auditpol.exe
 
    .DESCRIPTION
        Parses the advanced audit policy returned by 'auditpol.exe /get /category:* /r' (CSV)
        into one object per subcategory, reporting Success and Failure auditing state. It is a
        base building block for CIS/ANSSI compliance auditing and supports local and remote
        targets via Invoke-RemoteOrLocal, with optional filtering to a single category. Each
        Subcategory GUID is mapped to its parent Category using a static, well-known GUID map
        since 'auditpol /r' does not expose Category as a column.
 
    .PARAMETER ComputerName
        One or more computer names to target. Defaults to the local computer.
        Accepts pipeline input by value and by property name.
 
    .PARAMETER Credential
        Optional PSCredential for authenticating to remote machines. Ignored for local
        machine queries.
 
    .PARAMETER Category
        Filter results to a single audit category, e.g. 'Logon/Logoff'. Matched
        case-insensitively against the parsed Category column. When set and no subcategory
        matches on a given machine, that machine yields no rows (no error is raised).
 
    .EXAMPLE
        Get-AuditPolicy
 
        Returns advanced audit policy subcategory settings for the local machine.
 
    .EXAMPLE
        Get-AuditPolicy -Category 'Logon/Logoff'
 
        Returns only the subcategories belonging to the 'Logon/Logoff' category on the
        local machine.
 
    .EXAMPLE
        Get-AuditPolicy -ComputerName SRV01 -Credential (Get-Credential)
 
        Returns advanced audit policy subcategory settings from SRV01 via WinRM, using
        the supplied credential.
 
    .EXAMPLE
        'SRV01','SRV02' | Get-AuditPolicy
 
        Returns advanced audit policy subcategory settings for SRV01 and SRV02 via pipeline.
 
    .OUTPUTS
        PSWinOps.AuditPolicy
        One object per audit subcategory, with Category, Subcategory, SubcategoryGuid,
        AuditSuccess, AuditFailure and the derived Setting string.
 
    .NOTES
        Author: Franck SALLET
        Version: 1.0.0
        Last Modified: 2026-07-05
        Requires: PowerShell 5.1+ / Windows only
        Requires: Elevated (administrator) session for auditpol.exe to return data
 
    .LINK
        https://github.com/k9fr4n/PSWinOps
 
    .LINK
        https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/auditpol
    #>

    [CmdletBinding(SupportsShouldProcess = $false, ConfirmImpact = 'None')]
    [OutputType('PSWinOps.AuditPolicy')]
    param(
        [Parameter(Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [Alias('CN', 'Name', 'DNSHostName')]
        [string[]]$ComputerName = $env:COMPUTERNAME,

        [Parameter(Mandatory = $false)]
        [System.Management.Automation.PSCredential]$Credential,

        [Parameter(Mandatory = $false)]
        [string]$Category
    )

    begin {
        Write-Verbose "[$($MyInvocation.MyCommand)] Starting audit policy query"

        $auditpolPath = Join-Path -Path $env:SystemRoot -ChildPath 'System32\auditpol.exe'

        # Static, well-known Subcategory GUID -> top-level Category map. auditpol /r does not
        # expose Category as a column, so it must be derived from the (stable) subcategory GUID.
        $categoryMap = @{
            # System
            '0CCE9210-69AE-11D9-BED3-505054503030' = 'System'
            '0CCE9211-69AE-11D9-BED3-505054503030' = 'System'
            '0CCE9212-69AE-11D9-BED3-505054503030' = 'System'
            '0CCE9213-69AE-11D9-BED3-505054503030' = 'System'
            '0CCE9214-69AE-11D9-BED3-505054503030' = 'System'
            # Logon/Logoff
            '0CCE9215-69AE-11D9-BED3-505054503030' = 'Logon/Logoff'
            '0CCE9216-69AE-11D9-BED3-505054503030' = 'Logon/Logoff'
            '0CCE9217-69AE-11D9-BED3-505054503030' = 'Logon/Logoff'
            '0CCE9218-69AE-11D9-BED3-505054503030' = 'Logon/Logoff'
            '0CCE9219-69AE-11D9-BED3-505054503030' = 'Logon/Logoff'
            '0CCE921A-69AE-11D9-BED3-505054503030' = 'Logon/Logoff'
            '0CCE921B-69AE-11D9-BED3-505054503030' = 'Logon/Logoff'
            '0CCE921C-69AE-11D9-BED3-505054503030' = 'Logon/Logoff'
            '0CCE9243-69AE-11D9-BED3-505054503030' = 'Logon/Logoff'
            '0CCE9247-69AE-11D9-BED3-505054503030' = 'Logon/Logoff'
            '0CCE9249-69AE-11D9-BED3-505054503030' = 'Logon/Logoff'
            # Object Access
            '0CCE921D-69AE-11D9-BED3-505054503030' = 'Object Access'
            '0CCE921E-69AE-11D9-BED3-505054503030' = 'Object Access'
            '0CCE921F-69AE-11D9-BED3-505054503030' = 'Object Access'
            '0CCE9220-69AE-11D9-BED3-505054503030' = 'Object Access'
            '0CCE9221-69AE-11D9-BED3-505054503030' = 'Object Access'
            '0CCE9222-69AE-11D9-BED3-505054503030' = 'Object Access'
            '0CCE9223-69AE-11D9-BED3-505054503030' = 'Object Access'
            '0CCE9224-69AE-11D9-BED3-505054503030' = 'Object Access'
            '0CCE9225-69AE-11D9-BED3-505054503030' = 'Object Access'
            '0CCE9226-69AE-11D9-BED3-505054503030' = 'Object Access'
            '0CCE9227-69AE-11D9-BED3-505054503030' = 'Object Access'
            '0CCE9244-69AE-11D9-BED3-505054503030' = 'Object Access'
            '0CCE9245-69AE-11D9-BED3-505054503030' = 'Object Access'
            '0CCE9246-69AE-11D9-BED3-505054503030' = 'Object Access'
            # Privilege Use
            '0CCE9228-69AE-11D9-BED3-505054503030' = 'Privilege Use'
            '0CCE9229-69AE-11D9-BED3-505054503030' = 'Privilege Use'
            '0CCE922A-69AE-11D9-BED3-505054503030' = 'Privilege Use'
            # Detailed Tracking
            '0CCE922B-69AE-11D9-BED3-505054503030' = 'Detailed Tracking'
            '0CCE922C-69AE-11D9-BED3-505054503030' = 'Detailed Tracking'
            '0CCE922D-69AE-11D9-BED3-505054503030' = 'Detailed Tracking'
            '0CCE922E-69AE-11D9-BED3-505054503030' = 'Detailed Tracking'
            '0CCE9248-69AE-11D9-BED3-505054503030' = 'Detailed Tracking'
            '0CCE924A-69AE-11D9-BED3-505054503030' = 'Detailed Tracking'
            # Policy Change
            '0CCE922F-69AE-11D9-BED3-505054503030' = 'Policy Change'
            '0CCE9230-69AE-11D9-BED3-505054503030' = 'Policy Change'
            '0CCE9231-69AE-11D9-BED3-505054503030' = 'Policy Change'
            '0CCE9232-69AE-11D9-BED3-505054503030' = 'Policy Change'
            '0CCE9233-69AE-11D9-BED3-505054503030' = 'Policy Change'
            '0CCE9234-69AE-11D9-BED3-505054503030' = 'Policy Change'
            # Account Management
            '0CCE9235-69AE-11D9-BED3-505054503030' = 'Account Management'
            '0CCE9236-69AE-11D9-BED3-505054503030' = 'Account Management'
            '0CCE9237-69AE-11D9-BED3-505054503030' = 'Account Management'
            '0CCE9238-69AE-11D9-BED3-505054503030' = 'Account Management'
            '0CCE9239-69AE-11D9-BED3-505054503030' = 'Account Management'
            '0CCE923A-69AE-11D9-BED3-505054503030' = 'Account Management'
            # DS Access
            '0CCE923B-69AE-11D9-BED3-505054503030' = 'DS Access'
            '0CCE923C-69AE-11D9-BED3-505054503030' = 'DS Access'
            '0CCE923D-69AE-11D9-BED3-505054503030' = 'DS Access'
            '0CCE923E-69AE-11D9-BED3-505054503030' = 'DS Access'
            # Account Logon
            '0CCE923F-69AE-11D9-BED3-505054503030' = 'Account Logon'
            '0CCE9240-69AE-11D9-BED3-505054503030' = 'Account Logon'
            '0CCE9241-69AE-11D9-BED3-505054503030' = 'Account Logon'
            '0CCE9242-69AE-11D9-BED3-505054503030' = 'Account Logon'
        }

        # Scriptblock used for REMOTE execution only (Invoke-Command).
        # Resolves the full auditpol.exe path inside the remote session because remote
        # runspaces do not inherit local mock context, then checks $LASTEXITCODE and
        # throws on non-zero, returning the raw CSV text.
        $auditPolRemoteScriptBlock = {
            $remoteAuditpolPath = Join-Path -Path $env:SystemRoot -ChildPath 'System32\auditpol.exe'
            if (-not (Test-Path -Path $remoteAuditpolPath)) {
                throw "auditpol.exe not found at '$remoteAuditpolPath'"
            }
            $auditOutput = & $remoteAuditpolPath '/get' '/category:*' '/r' 2>&1
            if ($LASTEXITCODE -ne 0) {
                throw "auditpol /get /category:* /r failed (exit code $LASTEXITCODE): $($auditOutput -join ' ')"
            }
            $auditOutput
        }
    }

    process {
        foreach ($targetComputer in $ComputerName) {
            try {
                Write-Verbose "[$($MyInvocation.MyCommand)] Querying audit policy on '$targetComputer'"

                $isLocal = ($targetComputer -eq $env:COMPUTERNAME) -or
                ($targetComputer -eq 'localhost') -or
                ($targetComputer -eq '.')

                if ($isLocal) {
                    # Local execution: use Invoke-NativeCommand for testable auditpol calls
                    $auditResult = Invoke-NativeCommand -FilePath $auditpolPath -ArgumentList @('/get', '/category:*', '/r')
                    if ($auditResult.ExitCode -ne 0) {
                        Write-Error "[$($MyInvocation.MyCommand)] auditpol /get /category:* /r failed (exit code $($auditResult.ExitCode)) on '$targetComputer': $($auditResult.Output)"
                        continue
                    }
                    $rawOutput = $auditResult.Output -split '\r?\n'
                } else {
                    $invokeParams = @{
                        ComputerName = $targetComputer
                        ScriptBlock  = $auditPolRemoteScriptBlock
                        ErrorAction  = 'Stop'
                    }
                    if ($null -ne $Credential) {
                        $invokeParams['Credential'] = $Credential
                    }
                    $rawOutput = Invoke-Command @invokeParams
                }

                # Normalise to a non-empty string array of lines (header + data rows)
                $csvLines = @($rawOutput | ForEach-Object { "$_" } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })

                if ($csvLines.Count -lt 2) {
                    Write-Error "[$($MyInvocation.MyCommand)] No audit policy data returned by auditpol.exe on '$targetComputer'"
                    continue
                }

                $rows = @($csvLines | ConvertFrom-Csv)

                if ($rows.Count -eq 0) {
                    Write-Error "[$($MyInvocation.MyCommand)] Failed to parse auditpol CSV output on '$targetComputer'"
                    continue
                }

                foreach ($row in $rows) {
                    $subcategoryGuid = ($row.'Subcategory GUID' -replace '[{}]', '').Trim().ToUpperInvariant()
                    $resolvedCategory = if ($categoryMap.ContainsKey($subcategoryGuid)) { $categoryMap[$subcategoryGuid] } else { 'Unknown' }

                    if ($Category -and ($resolvedCategory -ne $Category)) {
                        continue
                    }

                    $inclusionSetting = [string]$row.'Inclusion Setting'
                    $auditSuccess = $inclusionSetting -match 'Success'
                    $auditFailure = $inclusionSetting -match 'Failure'

                    $setting = if ($auditSuccess -and $auditFailure) {
                        'Success and Failure'
                    } elseif ($auditSuccess) {
                        'Success'
                    } elseif ($auditFailure) {
                        'Failure'
                    } else {
                        'No Auditing'
                    }

                    [PSCustomObject]@{
                        PSTypeName      = 'PSWinOps.AuditPolicy'
                        ComputerName    = $targetComputer
                        Category        = $resolvedCategory
                        Subcategory     = $row.Subcategory
                        SubcategoryGuid = $subcategoryGuid
                        AuditSuccess    = $auditSuccess
                        AuditFailure    = $auditFailure
                        Setting         = $setting
                        Timestamp       = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
                    }
                }
            } catch {
                Write-Error "[$($MyInvocation.MyCommand)] Failed to query '$targetComputer': $_"
                continue
            }
        }
    }

    end {
        Write-Verbose "[$($MyInvocation.MyCommand)] Completed audit policy query"
    }
}