Public/Get-LocalSecurityPolicy.ps1

function Get-LocalSecurityPolicy {
    <#
    .SYNOPSIS
        Reads local security policy settings from a server. Never modifies local policy.
 
    .DESCRIPTION
        Connects to one or more servers and exports the local security policy using
        secedit /export. Parses the resulting .inf file to extract settings from
        System Access, Event Audit, Privilege Rights, and Registry Values sections.
 
        This function is READ-ONLY. It never creates, modifies, or deletes any
        security policy setting on the source server. The only file it creates is a
        temporary .inf export that is removed after parsing.
 
    .PARAMETER ComputerName
        One or more server names to read security policy from. Defaults to localhost.
 
    .PARAMETER ExportPath
        Optional file path to save the structured results as JSON for review.
 
    .EXAMPLE
        Get-LocalSecurityPolicy -ComputerName "SVR-WEB-01" | Where-Object Category -eq 'AuditPolicy'
 
        Reads security policy from SVR-WEB-01 and filters to audit policy settings only.
 
    .EXAMPLE
        Get-LocalSecurityPolicy -ComputerName "SVR-WEB-01" -ExportPath .\secpol-export.json
 
        Reads all local security policy settings and saves them to JSON.
 
    .OUTPUTS
        PSCustomObject with properties: ComputerName, Category, SettingName, SettingValue, Description
    #>

    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [string[]]$ComputerName = @('localhost'),

        [Parameter()]
        [string]$ExportPath
    )

    begin {
        $allResults = [System.Collections.Generic.List[PSObject]]::new()
        Write-Verbose "Get-LocalSecurityPolicy: READ-ONLY operation -- no local policy will be modified."

        # Category mapping from .inf section headers to friendly names
        $sectionMap = @{
            'System Access'   = 'SystemAccess'
            'Event Audit'     = 'AuditPolicy'
            'Privilege Rights' = 'UserRights'
            'Registry Values' = 'SecurityOptions'
        }

        # Friendly descriptions for common security settings
        $settingDescriptions = @{
            # System Access
            'MinimumPasswordAge'          = 'Minimum password age (days)'
            'MaximumPasswordAge'          = 'Maximum password age (days)'
            'MinimumPasswordLength'       = 'Minimum password length (characters)'
            'PasswordComplexity'          = 'Password must meet complexity requirements'
            'PasswordHistorySize'         = 'Enforce password history (passwords remembered)'
            'LockoutBadCount'             = 'Account lockout threshold (invalid attempts)'
            'ResetLockoutCount'           = 'Reset account lockout counter after (minutes)'
            'LockoutDuration'             = 'Account lockout duration (minutes)'
            'ForceLogoffWhenHourExpire'   = 'Force logoff when logon hours expire'
            'NewAdministratorName'        = 'Rename administrator account'
            'NewGuestName'                = 'Rename guest account'
            'ClearTextPassword'           = 'Store passwords using reversible encryption'
            'LSAAnonymousNameLookup'      = 'Allow anonymous SID/Name translation'
            'EnableAdminAccount'          = 'Enable Administrator account'
            'EnableGuestAccount'          = 'Enable Guest account'
            # Audit Policy
            'AuditSystemEvents'           = 'Audit system events'
            'AuditLogonEvents'            = 'Audit logon events'
            'AuditObjectAccess'           = 'Audit object access'
            'AuditPrivilegeUse'           = 'Audit privilege use'
            'AuditPolicyChange'           = 'Audit policy change'
            'AuditAccountManage'          = 'Audit account management'
            'AuditProcessTracking'        = 'Audit process tracking'
            'AuditDSAccess'               = 'Audit directory service access'
            'AuditAccountLogon'           = 'Audit account logon events'
            # User Rights
            'SeNetworkLogonRight'         = 'Access this computer from the network'
            'SeDenyNetworkLogonRight'     = 'Deny access to this computer from the network'
            'SeInteractiveLogonRight'     = 'Allow log on locally'
            'SeDenyInteractiveLogonRight' = 'Deny log on locally'
            'SeRemoteInteractiveLogonRight' = 'Allow log on through Remote Desktop'
            'SeDenyRemoteInteractiveLogonRight' = 'Deny log on through Remote Desktop'
            'SeBackupPrivilege'           = 'Back up files and directories'
            'SeRestorePrivilege'          = 'Restore files and directories'
            'SeShutdownPrivilege'         = 'Shut down the system'
            'SeBatchLogonRight'           = 'Log on as a batch job'
            'SeServiceLogonRight'         = 'Log on as a service'
            'SeDebugPrivilege'            = 'Debug programs'
            'SeTakeOwnershipPrivilege'    = 'Take ownership of files or other objects'
            'SeManageVolumePrivilege'     = 'Perform volume maintenance tasks'
        }

        $scriptBlock = {
            # Export local security policy to a temp .inf file using secedit
            $tempInf = Join-Path $env:TEMP "secpol_export_$(Get-Random).inf"
            try {
                $seceditOutput = & secedit /export /cfg $tempInf 2>&1
                if (-not (Test-Path $tempInf)) {
                    throw "secedit export failed: $seceditOutput"
                }
                # Read and return the raw .inf content
                Get-Content -Path $tempInf -Raw
            }
            finally {
                # Clean up temp file
                if (Test-Path $tempInf) {
                    Remove-Item -Path $tempInf -Force -ErrorAction SilentlyContinue
                }
            }
        }
    }

    process {
        foreach ($computer in $ComputerName) {
            Write-Verbose "Reading local security policy from '$computer'..."

            try {
                if ($computer -eq 'localhost' -or $computer -eq $env:COMPUTERNAME -or $computer -eq '.') {
                    Write-Verbose "Executing locally on '$computer'."
                    $infContent = & $scriptBlock
                }
                else {
                    Write-Verbose "Executing remotely on '$computer' via Invoke-Command."
                    $infContent = Invoke-Command -ComputerName $computer -ScriptBlock $scriptBlock -ErrorAction Stop
                }

                if (-not $infContent) {
                    Write-Warning "No security policy content retrieved from '$computer'."
                    continue
                }

                # Parse the .inf file content
                $currentSection = $null
                $lines = $infContent -split "`r?`n"

                foreach ($line in $lines) {
                    $trimmed = $line.Trim()

                    # Skip empty lines and comments
                    if ([string]::IsNullOrWhiteSpace($trimmed) -or $trimmed.StartsWith(';')) {
                        continue
                    }

                    # Check for section headers
                    if ($trimmed -match '^\[(.+)\]$') {
                        $currentSection = $Matches[1]
                        continue
                    }

                    # Only parse sections we care about
                    if ($currentSection -and $sectionMap.ContainsKey($currentSection)) {
                        $category = $sectionMap[$currentSection]

                        # Parse key = value pairs
                        if ($trimmed -match '^(.+?)\s*=\s*(.*)$') {
                            $settingName  = $Matches[1].Trim()
                            $settingValue = $Matches[2].Trim()

                            # Look up description
                            $description = if ($settingDescriptions.ContainsKey($settingName)) {
                                $settingDescriptions[$settingName]
                            }
                            else {
                                $settingName
                            }

                            $obj = [PSCustomObject]@{
                                ComputerName = $computer
                                Category     = $category
                                SettingName  = $settingName
                                SettingValue = $settingValue
                                Description  = $description
                            }

                            $allResults.Add($obj)
                            $obj
                        }
                    }
                }

                Write-Verbose "Parsed $($allResults.Count) security policy setting(s) from '$computer'."
            }
            catch {
                Write-Error "Failed to read security policy from '$computer': $_"
            }
        }
    }

    end {
        if ($ExportPath -and $allResults.Count -gt 0) {
            try {
                $exportDir = Split-Path -Path $ExportPath -Parent
                if ($exportDir -and -not (Test-Path $exportDir)) {
                    New-Item -ItemType Directory -Path $exportDir -Force | Out-Null
                }
                $allResults | ConvertTo-Json -Depth 10 | Set-Content -Path $ExportPath -Encoding UTF8
                Write-Verbose "Exported $($allResults.Count) setting(s) to '$ExportPath'."
                Write-Output "Export saved to: $ExportPath"
            }
            catch {
                Write-Error "Failed to export results to '$ExportPath': $_"
            }
        }
    }
}