Atliso-LapsRotationAudit.ps1
|
<#PSScriptInfo .VERSION 1.0.1 .GUID c47f98db-0b99-4aa3-8d8c-3dd2c736700c .AUTHOR Atliso .COMPANYNAME Atliso .COPYRIGHT (c) 2026 Atliso. MIT License. .TAGS Intune LAPS WindowsLAPS Rotation Remediation EntraID .LICENSEURI https://github.com/Atliso-Dev/laps-rotation-audit/blob/main/LICENSE .PROJECTURI https://atliso.com/blog/windows-laps-password-not-rotating-fix .REQUIREDMODULES .EXTERNALMODULEDEPENDENCIES .REQUIREDSCRIPTS .EXTERNALSCRIPTDEPENDENCIES .RELEASENOTES Gallery metadata added. Script logic unchanged from v1.0.0. #> <# .SYNOPSIS Atliso-LapsRotationAudit.ps1 - Audit and force Windows LAPS password rotation. .DESCRIPTION Windows LAPS can report as compliant in Intune while the local admin password hasn't actually rotated in months. This script confirms whether a successful password backup was logged inside the expected window, and if not, forces a rotation with Reset-LapsPassword. Detection signal: event 10029 (Entra ID backup) or 10018 (Active Directory backup) in the Microsoft-Windows-LAPS/Operational channel, logged inside PasswordAgeDays + GraceDays. It also flags event 10005 (policy processing failed), which means rotation is actively failing rather than just late. Runs audit-only by default (-WhatIf $true). Set -WhatIf $false to force rotation. Intune usage: deploy as a Remediation. The detection half exits 1 when no successful backup is confirmed inside the window. 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 with Windows LAPS, PowerShell 5.1+. #> [CmdletBinding()] param( # Where this device backs up its LAPS password. Match your Intune LAPS policy. # "EntraID" for Entra-joined and hybrid devices onboarded through Intune, # "ActiveDirectory" for on-prem devices still managed by the GPO path. [ValidateSet("EntraID", "ActiveDirectory")] [string]$BackupDestination = "EntraID", # Your Intune LAPS policy's Password Age Days value. Detection uses this as # the baseline, not a guess. [int]$PasswordAgeDays = 30, # Days of slack on top of PasswordAgeDays before "hasn't rotated yet" becomes # "rotation is broken". Keep this small. [int]$GraceDays = 2, # $true reports only. $false actually calls Reset-LapsPassword. [bool]$WhatIf = $true, [string]$LogPath = (Join-Path $env:LOCALAPPDATA "Atliso\LapsRotationAudit.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 } # ── Preconditions ─────────────────────────────────────────────────────────────── if (-not (Get-Command Reset-LapsPassword -ErrorAction SilentlyContinue)) { Write-Log "Windows LAPS cmdlets not available on this device. Check the LAPS install/policy." "WARN" exit 1 } $logName = 'Microsoft-Windows-LAPS/Operational' $eventId = if ($BackupDestination -eq 'EntraID') { 10029 } else { 10018 } $cutoff = (Get-Date).AddDays(-($PasswordAgeDays + $GraceDays)) Write-Log "Run start. Destination=$BackupDestination successEventId=$eventId window=$($PasswordAgeDays + $GraceDays)d WhatIf=$WhatIf" # Surface active failures separately from "just late". Event 10005 means policy # processing is failing, which no rotation nudge will fix. try { $failures = Get-WinEvent -FilterHashtable @{ LogName = $logName Id = 10005 StartTime = (Get-Date).AddHours(-24) } -ErrorAction Stop } catch { $failures = $null } if ($failures) { Write-Log "Event 10005 (LAPS policy processing failed) logged $($failures.Count) time(s) in the last 24h. Rotation is actively failing, not just late. Run Get-LapsDiagnostics before trusting any remediation." "WARN" } # ── Detection: was a successful backup logged inside the window? ───────────────── # Get-WinEvent throws instead of returning empty when nothing matches, so wrap it. try { $confirmed = Get-WinEvent -FilterHashtable @{ LogName = $logName Id = $eventId StartTime = $cutoff } -ErrorAction Stop } catch { $confirmed = $null } if ($confirmed) { Write-Log "Backup confirmed by event $eventId within the window. Rotation is healthy." exit 0 } Write-Log "No confirmed LAPS backup (event $eventId) inside the window." "WARN" if ($WhatIf) { Write-Log "WhatIf is on - reporting only, no rotation forced. Re-run with -WhatIf `$false to force it." exit 1 } # ── Remediation: force rotation ───────────────────────────────────────────────── Write-Log "Forcing rotation with Reset-LapsPassword." try { Reset-LapsPassword -ErrorAction Stop } catch { Write-Log "Reset-LapsPassword failed: $($_.Exception.Message)" "ERROR" exit 1 } # Reset-LapsPassword hands off to LAPS' own background processing, it doesn't # rotate the password itself. Windows LAPS wakes up hourly on a fixed, # non-configurable cycle - there's no scheduled task to nudge and no way to # make it faster than that. Write-Log "Reset-LapsPassword called. Expect the new password within the next hourly cycle." exit 0 |