Atliso-SecurityLogGuard.ps1

<#PSScriptInfo
.VERSION 1.0.1
.GUID 1d77e939-7b61-49a3-9e29-3ebfb0cc1703
.AUTHOR Atliso
.COMPANYNAME Atliso
.COPYRIGHT (c) 2026 Atliso. MIT License.
.TAGS Intune Security EventLog CrashOnAuditFail Lockout Remediation
.LICENSEURI https://github.com/Atliso-Dev/security-log-guard/blob/main/LICENSE
.PROJECTURI https://atliso.com/blog/windows-11-security-log-full-lockout-fix
.REQUIREDMODULES
.EXTERNALMODULEDEPENDENCIES
.REQUIREDSCRIPTS
.EXTERNALSCRIPTDEPENDENCIES
.RELEASENOTES Never clears the Security log unless the archive succeeded. Every wevtutil call is checked.
#>


<#
.SYNOPSIS
    Atliso-SecurityLogGuard.ps1 - Detect and fix the Windows Security log "only admins can log on" lockout.

.DESCRIPTION
    When the Security event log is set to Do Not Overwrite (LogMode=Retain) and fills
    up, Windows can flip CrashOnAuditFail to 2, which locks out every non-administrator
    until the log is cleared. This script catches the condition early and remediates it:
    it resizes the log, sets it to overwrite as needed, and - if the lockout has already
    tripped - archives and clears the log and resets CrashOnAuditFail from 2 to 1.

    Runs audit-only by default (-WhatIf $true). Set -WhatIf $false to apply changes.

    Intune usage: deploy as a Remediation. The detection half exits 1 when the log is
    on Do Not Overwrite, at/over the warn threshold, full, or the lockout has tripped.
    Run as SYSTEM, 64-bit.

.NOTES
    MIT licensed. Free to use and modify.
    Reconstructed from the published Atliso article spec - lab-test before fleet rollout.
    Tested target: Windows 10/11, PowerShell 5.1+.
#>


[CmdletBinding()]
param(
    # Trigger remediation once the log is at or above this percent full.
    [int]$WarnThresholdPercent = 90,

    # Target max size for the Security log, in bytes. 256MB covers most fleets even
    # with advanced audit policy turned on. Raise it if your baseline logs more.
    [long]$TargetMaxSizeBytes = 256MB,

    # $true reports only. $false resizes the log, sets it to overwrite, and
    # archives + clears it if the lockout has already tripped.
    [bool]$WhatIf = $true,

    [string]$LogPath = (Join-Path $env:LOCALAPPDATA "Atliso\SecurityLogGuard.log")
)

# ── Logging ───────────────────────────────────────────────────────────────────
function Write-Log {
    param([string]$Message, [string]$Level = "INFO")
    $line = "{0} [{1}] {2}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss"), $Level, $Message
    try {
        $dir = Split-Path -Parent $LogPath
        if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null }
        Add-Content -Path $LogPath -Value $line -ErrorAction Stop
    } catch { }
    Write-Output $line
}

$lsaPath = 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa'

# ── Detection ───────────────────────────────────────────────────────────────────
$log = Get-WinEvent -ListLog Security
$crashOnAuditFail = (Get-ItemProperty $lsaPath -Name CrashOnAuditFail -ErrorAction SilentlyContinue).CrashOnAuditFail
$pctFull = if ($log.MaximumSizeInBytes -gt 0) {
    [math]::Round(($log.FileSize / $log.MaximumSizeInBytes) * 100, 1)
} else { 0 }

$needsRemediation = $false
$lockoutTripped   = $false

if ($crashOnAuditFail -eq 2) {
    Write-Log "CrashOnAuditFail=2: the lockout has already tripped. Non-admins are locked out." "ERROR"
    $needsRemediation = $true
    $lockoutTripped   = $true
}
# The root cause. 'Retain' means Do Not Overwrite, so the log stops the machine
# when it fills instead of wrapping. Catch it before it locks anyone out.
elseif ($log.LogMode -eq 'Retain') {
    Write-Log "Security log set to Do Not Overwrite (LogMode=Retain), $pctFull% full." "WARN"
    $needsRemediation = $true
}
elseif ($log.IsLogFull -or $pctFull -ge $WarnThresholdPercent) {
    Write-Log "Security log at $pctFull% full (IsLogFull=$($log.IsLogFull))." "WARN"
    $needsRemediation = $true
}
else {
    Write-Log "Security log healthy: LogMode=$($log.LogMode), $pctFull% full."
}

if (-not $needsRemediation) { exit 0 }

# Audit-only: report non-compliant, which is exactly the shape an Intune
# detection script wants.
if ($WhatIf) {
    Write-Log "WhatIf is on - reporting only, no changes made. Re-run with -WhatIf `$false to fix it."
    exit 1
}

# ── Remediation ─────────────────────────────────────────────────────────────────
if ($lockoutTripped) {
    $stamp   = Get-Date -Format yyyyMMdd-HHmmss
    $archive = Join-Path (Split-Path -Parent $LogPath) "Security-archive-$stamp.evtx"
    wevtutil epl Security "$archive"
    # Never clear what we failed to keep: without the archive, clearing loses the audit trail.
    if ($LASTEXITCODE -ne 0) {
        Write-Log "Archiving the Security log to $archive failed (wevtutil exit $LASTEXITCODE). Not clearing it. Check free disk space and that this runs as SYSTEM." "ERROR"
        exit 1
    }
    wevtutil cl Security
    if ($LASTEXITCODE -ne 0) {
        Write-Log "Clearing the Security log failed (wevtutil exit $LASTEXITCODE). The archive is at $archive. CrashOnAuditFail left at 2." "ERROR"
        exit 1
    }
    Set-ItemProperty $lsaPath -Name CrashOnAuditFail -Value 1
    Write-Log "Lockout cleared: log archived to $archive, cleared, CrashOnAuditFail reset from 2 to 1."
}

# Set the log to overwrite as needed at the target size so this can't recur.
wevtutil sl Security "/ms:$TargetMaxSizeBytes" /rt:false
if ($LASTEXITCODE -ne 0) {
    Write-Log "Setting the Security log to overwrite as needed failed (wevtutil exit $LASTEXITCODE)." "ERROR"
    exit 1
}
Write-Log "Security log set to overwrite as needed, max size $TargetMaxSizeBytes bytes."

exit 0