<# .SYNOPSIS Deploy the password expiry checker to Windows machines. .DESCRIPTION Downloads password-expiry-check.ps1, installs it to a configurable directory, creates a scheduled task for recurring checks, and optionally copies the script to NETLOGON for GPO deployment. .NOTES Author: Phil Connor License: MIT (https://opensource.org/licenses/MIT) Version: 1.01 #> param( [string]$InstallDir = "C:\Scripts", [int]$WarningDays = 14, [int]$IntervalHours = 4, [switch]$NetlogonCopy, [switch]$CmdPrompt, [switch]$NoProfile, [switch]$Remove, [switch]$DryRun, [Alias("h")] [switch]$Help ) $ScriptUrl = "https://mylinux.work/downloads/password-expiry-check.ps1.zip" $ScriptName = "password-expiry-check.ps1" $TaskName = "PasswordExpiryCheck" # ── Colors ──────────────────────────────────────────────────────────── function Write-OK { param([string]$Msg) Write-Host "[OK] $Msg" -ForegroundColor Green } function Write-Warn { param([string]$Msg) Write-Host "[WARN] $Msg" -ForegroundColor Yellow } function Write-Err { param([string]$Msg) Write-Host "[ERROR] $Msg" -ForegroundColor Red } function Write-Info { param([string]$Msg) Write-Host "[INFO] $Msg" -ForegroundColor Cyan } # ── Help ────────────────────────────────────────────────────────────── if ($Help) { Write-Host @" Usage: .\deploy-password-expiry-checker.ps1 [OPTIONS] Deploy password expiry notifications on Windows machines. Installs: 1. password-expiry-check.ps1 to C:\Scripts\ (configurable) 2. Scheduled task - runs every 4 hours (configurable) under logged-on user 3. Logon-triggered task - fires on every user logon 4. PowerShell profile hook - warning banner in every new PowerShell window 5. Optional cmd.exe AutoRun hook - warning banner in every new cmd window 6. Optional NETLOGON copy for GPO deployment Options: -InstallDir PATH Installation directory (default: C:\Scripts) -WarningDays N Warning threshold in days (default: 14) -IntervalHours N Scheduled task interval in hours (default: 4) -CmdPrompt Also add warning to cmd.exe via AutoRun registry key -NoProfile Skip PowerShell profile hook (scheduled tasks only) -NetlogonCopy Copy script to NETLOGON share for GPO deployment -Remove Remove deployed components -DryRun Show what would be done without making changes -Help Show this help Examples: .\deploy-password-expiry-checker.ps1 # install with defaults .\deploy-password-expiry-checker.ps1 -CmdPrompt # also hook into cmd.exe .\deploy-password-expiry-checker.ps1 -NoProfile # skip profile hook .\deploy-password-expiry-checker.ps1 -WarningDays 30 # 30-day warning threshold .\deploy-password-expiry-checker.ps1 -IntervalHours 8 # check every 8 hours .\deploy-password-expiry-checker.ps1 -NetlogonCopy # also copy to NETLOGON .\deploy-password-expiry-checker.ps1 -DryRun # preview changes .\deploy-password-expiry-checker.ps1 -Remove # uninstall "@ exit 0 } # ── Admin check ─────────────────────────────────────────────────────── $currentUser = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = New-Object Security.Principal.WindowsPrincipal($currentUser) if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { Write-Err "Must run as Administrator" exit 1 } $ScriptPath = Join-Path $InstallDir $ScriptName # ── Remove mode ─────────────────────────────────────────────────────── if ($Remove) { Write-Info "Removing password expiry checker deployment..." Write-Host "" # Remove scheduled tasks foreach ($name in @($TaskName, "${TaskName}Logon")) { $task = Get-ScheduledTask -TaskName $name -ErrorAction SilentlyContinue if ($task) { if ($DryRun) { Write-Info "Would remove scheduled task: $name" } else { Unregister-ScheduledTask -TaskName $name -Confirm:$false Write-OK "Removed scheduled task: $name" } } else { Write-Info "Scheduled task '$name' not found, skipping" } } # Remove script if (Test-Path $ScriptPath) { if ($DryRun) { Write-Info "Would remove: $ScriptPath" } else { Remove-Item -Path $ScriptPath -Force Write-OK "Removed $ScriptPath" } } # Remove PowerShell profile hook $profileMarker = "# PasswordExpiryCheck" $allUsersProfile = $PROFILE.AllUsersAllHosts if ((Test-Path $allUsersProfile) -and (Select-String -Path $allUsersProfile -Pattern $profileMarker -Quiet)) { if ($DryRun) { Write-Info "Would remove profile hook from $allUsersProfile" } else { $content = Get-Content $allUsersProfile | Where-Object { $_ -notmatch $profileMarker } if ($content) { Set-Content -Path $allUsersProfile -Value $content } else { Remove-Item -Path $allUsersProfile -Force } Write-OK "Removed PowerShell profile hook" } } # Remove cmd.exe AutoRun $cmdAutoRun = Get-ItemProperty -Path "HKLM:\Software\Microsoft\Command Processor" -Name "AutoRun" -ErrorAction SilentlyContinue if ($cmdAutoRun -and $cmdAutoRun.AutoRun -match "password-expiry-check") { if ($DryRun) { Write-Info "Would remove cmd.exe AutoRun registry key" } else { $existing = $cmdAutoRun.AutoRun # Remove our command, handle single command or chained with ampersand $cleaned = ($existing -split '\s*&\s*' | Where-Object { $_ -notmatch 'password-expiry-check' }) -join ' & ' if ($cleaned.Trim()) { Set-ItemProperty -Path "HKLM:\Software\Microsoft\Command Processor" -Name "AutoRun" -Value $cleaned.Trim() } else { Remove-ItemProperty -Path "HKLM:\Software\Microsoft\Command Processor" -Name "AutoRun" -ErrorAction SilentlyContinue } Write-OK "Removed cmd.exe AutoRun hook" } } # Remove install dir if empty if ((Test-Path $InstallDir) -and @(Get-ChildItem $InstallDir -Force).Count -eq 0) { if ($DryRun) { Write-Info "Would remove empty directory: $InstallDir" } else { Remove-Item -Path $InstallDir -Force Write-OK "Removed empty directory: $InstallDir" } } Write-Host "" if (-not $DryRun) { Write-OK "Removal complete." } exit 0 } # ── Install mode ────────────────────────────────────────────────────── Write-Info "Deploying password expiry checker..." Write-Host "" # 1. Create install directory if (-not (Test-Path $InstallDir)) { if ($DryRun) { Write-Info "Would create directory: $InstallDir" } else { New-Item -Path $InstallDir -ItemType Directory -Force | Out-Null Write-OK "Created directory: $InstallDir" } } # 2. Download script if (Test-Path $ScriptPath) { Write-Info "Script already exists at $ScriptPath - downloading latest version" } if ($DryRun) { Write-Info "Would download $ScriptUrl and extract to $ScriptPath" } else { $zipPath = Join-Path $env:TEMP "password-expiry-check.ps1.zip" try { Invoke-WebRequest -Uri $ScriptUrl -OutFile $zipPath -UseBasicParsing -ErrorAction Stop Expand-Archive -Path $zipPath -DestinationPath $InstallDir -Force Remove-Item $zipPath -Force -ErrorAction SilentlyContinue if (Test-Path $ScriptPath) { Write-OK "Downloaded and extracted $ScriptPath" } else { Write-Err "Zip extracted but $ScriptName not found in $InstallDir" exit 1 } } catch { Write-Err "Failed to download: $($_.Exception.Message)" exit 1 } } # 3. Scheduled task - recurring interval $taskArgs = "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$ScriptPath`" -Quiet -WarningDays $WarningDays" $existingTask = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue if ($existingTask) { Write-Info "Scheduled task '$TaskName' already exists - recreating" if (-not $DryRun) { Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false } } if ($DryRun) { Write-Info "Would create scheduled task: $TaskName (every ${IntervalHours}h)" } else { $action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument $taskArgs $trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).Date.AddHours(9) ` -RepetitionInterval (New-TimeSpan -Hours $IntervalHours) ` -RepetitionDuration (New-TimeSpan -Days 365) $settings = New-ScheduledTaskSettingsSet ` -AllowStartIfOnBatteries ` -DontStopIfGoingOnBatteries ` -StartWhenAvailable ` -RunOnlyIfNetworkAvailable:$false $principal = New-ScheduledTaskPrincipal -GroupId "S-1-5-32-545" -RunLevel Limited Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger ` -Settings $settings -Principal $principal ` -Description "Check password expiry every $IntervalHours hours (mylinux.work)" | Out-Null Write-OK "Created scheduled task: $TaskName (every ${IntervalHours}h)" } # 4. Logon trigger task $logonTaskName = "${TaskName}Logon" $existingLogon = Get-ScheduledTask -TaskName $logonTaskName -ErrorAction SilentlyContinue if ($existingLogon) { Write-Info "Logon task '$logonTaskName' already exists - recreating" if (-not $DryRun) { Unregister-ScheduledTask -TaskName $logonTaskName -Confirm:$false } } if ($DryRun) { Write-Info "Would create logon trigger task: $logonTaskName" } else { $logonAction = New-ScheduledTaskAction -Execute "powershell.exe" -Argument $taskArgs $logonTrigger = New-ScheduledTaskTrigger -AtLogOn $logonSettings = New-ScheduledTaskSettingsSet ` -AllowStartIfOnBatteries ` -DontStopIfGoingOnBatteries ` -StartWhenAvailable ` -ExecutionTimeLimit (New-TimeSpan -Minutes 5) # Delay 30 seconds after logon to let the desktop load $logonTrigger.Delay = "PT30S" $logonPrincipal = New-ScheduledTaskPrincipal -GroupId "S-1-5-32-545" -RunLevel Limited Register-ScheduledTask -TaskName $logonTaskName -Action $logonAction -Trigger $logonTrigger ` -Settings $logonSettings -Principal $logonPrincipal ` -Description "Check password expiry at logon (mylinux.work)" | Out-Null Write-OK "Created logon trigger task: $logonTaskName" } # 5. NETLOGON copy (optional) if ($NetlogonCopy) { $logonServer = $env:LOGONSERVER if ($logonServer) { $netlogonPath = Join-Path "$logonServer\NETLOGON" $ScriptName if ($DryRun) { Write-Info "Would copy $ScriptPath to $netlogonPath" } else { try { Copy-Item -Path $ScriptPath -Destination $netlogonPath -Force -ErrorAction Stop Write-OK "Copied to $netlogonPath" } catch { Write-Warn "Could not copy to NETLOGON: $($_.Exception.Message)" Write-Warn "Copy manually: Copy-Item '$ScriptPath' '$netlogonPath'" } } } else { Write-Warn "LOGONSERVER not set - machine may not be domain-joined" Write-Warn "Copy manually to \\DC\NETLOGON\$ScriptName" } } # 6. PowerShell profile hook (default, skip with -NoProfile) $profileMarker = "# PasswordExpiryCheck" $profileLine = "& `"$ScriptPath`" -Quiet -WarningDays $WarningDays $profileMarker" if (-not $NoProfile) { $allUsersProfile = $PROFILE.AllUsersAllHosts $profileDir = Split-Path $allUsersProfile -Parent # Check if hook already exists $hookExists = (Test-Path $allUsersProfile) -and (Select-String -Path $allUsersProfile -Pattern $profileMarker -Quiet) if ($hookExists) { Write-Info "PowerShell profile hook already present - updating" if (-not $DryRun) { $content = Get-Content $allUsersProfile | Where-Object { $_ -notmatch $profileMarker } $content += $profileLine Set-Content -Path $allUsersProfile -Value $content } } else { if ($DryRun) { Write-Info "Would add profile hook to $allUsersProfile" } else { if (-not (Test-Path $profileDir)) { New-Item -Path $profileDir -ItemType Directory -Force | Out-Null } Add-Content -Path $allUsersProfile -Value $profileLine } } Write-OK "PowerShell profile hook: $allUsersProfile" } else { Write-Info "Skipping PowerShell profile hook (-NoProfile)" } # 7. cmd.exe AutoRun hook (optional, enable with -CmdPrompt) if ($CmdPrompt) { $cmdCommand = '@powershell.exe -NoProfile -ExecutionPolicy Bypass -File "' + $ScriptPath + '" -Quiet -WarningDays ' + $WarningDays $regPath = "HKLM:\Software\Microsoft\Command Processor" $existing = Get-ItemProperty -Path $regPath -Name "AutoRun" -ErrorAction SilentlyContinue if ($existing -and $existing.AutoRun -match "password-expiry-check") { Write-Info "cmd.exe AutoRun hook already present - updating" if (-not $DryRun) { $cleaned = ($existing.AutoRun -split '\s*&\s*' | Where-Object { $_ -notmatch 'password-expiry-check' }) -join ' & ' if ($cleaned.Trim()) { $newValue = $cleaned.Trim() + " & " + $cmdCommand } else { $newValue = $cmdCommand } Set-ItemProperty -Path $regPath -Name "AutoRun" -Value $newValue } } elseif ($existing -and $existing.AutoRun.Trim()) { if ($DryRun) { Write-Info "Would append to existing cmd.exe AutoRun" } else { $newValue = $existing.AutoRun.Trim() + " & " + $cmdCommand Set-ItemProperty -Path $regPath -Name "AutoRun" -Value $newValue } } else { if ($DryRun) { Write-Info "Would create cmd.exe AutoRun registry key" } else { Set-ItemProperty -Path $regPath -Name "AutoRun" -Value $cmdCommand } } Write-OK "cmd.exe AutoRun hook: $regPath" } # ── Summary ─────────────────────────────────────────────────────────── Write-Host "" Write-Host "Deployment summary:" -ForegroundColor White Write-Host " Script: $ScriptPath" Write-Host " Warning: $WarningDays days" Write-Host " Interval task: $TaskName (every ${IntervalHours}h)" Write-Host " Logon task: $logonTaskName (at user logon, 30s delay)" if (-not $NoProfile) { Write-Host " PS profile: $($PROFILE.AllUsersAllHosts) (all users)" } if ($CmdPrompt) { Write-Host " cmd.exe: AutoRun registry hook (HKLM)" } if ($NetlogonCopy) { Write-Host " NETLOGON: $env:LOGONSERVER\NETLOGON\$ScriptName" } Write-Host "" Write-Host "Users will see warnings via:" -ForegroundColor White Write-Host " MessageBox popup every $IntervalHours hours (scheduled task)" Write-Host " MessageBox popup at logon (logon trigger task)" Write-Host " Terminal banner in new PowerShell windows (profile hook)" if ($CmdPrompt) { Write-Host " Terminal banner in new cmd.exe windows (AutoRun hook)" } Write-Host "" Write-Info "Test with: & '$ScriptPath' -Test" Write-Info "Remove with: .\deploy-password-expiry-checker.ps1 -Remove"