diff --git a/ClearOldLogs.ps1 b/ClearOldLogs.ps1 new file mode 100644 index 0000000..e852bcd --- /dev/null +++ b/ClearOldLogs.ps1 @@ -0,0 +1,207 @@ +<# +.SYNOPSIS + Interactive deploy for a per-client "Clear Old Logs" scheduled task. + Grants NETWORK SERVICE Modify on the target dirs, registers a daily + Base64-encoded delete task running as NT AUTHORITY\NETWORK SERVICE, + previews the blast radius, and (optionally) test-fires it. + +.NOTES + ASP 7.1 change-control applies. Any AV exclusion / registry change is + a separate item and gates through Tom. This script only touches ACLs + on the named log dirs and the Task Scheduler. + + Task convention: TaskPath = \\ TaskName = - Clear Old Logs + Principal: NT AUTHORITY\NETWORK SERVICE (*S-1-5-20) RunLevel Limited +#> + +#Requires -Version 5.1 +[CmdletBinding()] +param() + +$ErrorActionPreference = 'Stop' + +function Confirm-YesNo { + param([string]$Prompt, [bool]$Default = $true) + $suffix = if ($Default) { '[Y/n]' } else { '[y/N]' } + while ($true) { + $r = Read-Host "$Prompt $suffix" + if ([string]::IsNullOrWhiteSpace($r)) { return $Default } + switch -Regex ($r.Trim()) { + '^(y|yes)$' { return $true } + '^(n|no)$' { return $false } + default { Write-Host " yes or no, Beltalowda." -ForegroundColor DarkYellow } + } + } +} + +function Read-Default { + param([string]$Prompt, [string]$Default) + $r = Read-Host "$Prompt (default: $Default)" + if ([string]::IsNullOrWhiteSpace($r)) { return $Default } + return $r.Trim() +} + +# ---- 0. Elevation gate --------------------------------------------------- +$id = [Security.Principal.WindowsIdentity]::GetCurrent() +$isAdmin = ([Security.Principal.WindowsPrincipal]$id).IsInRole( + [Security.Principal.WindowsBuiltInRole]::Administrator) +if (-not $isAdmin) { + Write-Host "Not elevated. icacls /T and the NETWORK SERVICE principal need admin." -ForegroundColor Red + Write-Host "Re-launch this in an elevated PowerShell and try again." -ForegroundColor Red + exit 1 +} + +Write-Host "" +Write-Host "=== Log Cleanup Task Deploy ===" -ForegroundColor Cyan +Write-Host "Box: $env:COMPUTERNAME" -ForegroundColor DarkCyan +Write-Host "" + +# ---- 1. Client ----------------------------------------------------------- +$Client = Read-Default -Prompt 'Client / task name prefix' -Default 'SALSL' + +# ---- 2. Paths ------------------------------------------------------------ +# Defaults reflect the SALSL box. The smtpproxy path varies per host, so +# it's prompted explicitly rather than assumed. +$defaultLogPath = "C:\Logging\$Client" +$defaultProxyPath = 'C:\smtpproxy.net\logging' + +$logPath = Read-Default -Prompt 'Application log path' -Default $defaultLogPath +$proxyPath = Read-Default -Prompt 'smtpproxy log path' -Default $defaultProxyPath + +$Paths = New-Object System.Collections.Generic.List[string] +$Paths.Add($logPath) +$Paths.Add($proxyPath) + +# allow extra dirs if this box has more than the two +while (Confirm-YesNo -Prompt 'Add another log directory?' -Default $false) { + $extra = Read-Host ' Path' + if (-not [string]::IsNullOrWhiteSpace($extra)) { $Paths.Add($extra.Trim()) } +} + +# ---- 3. Retention + schedule -------------------------------------------- +$RetentionDays = Read-Default -Prompt 'Retention (days)' -Default '90' +if ($RetentionDays -notmatch '^\d+$' -or [int]$RetentionDays -le 0) { + Write-Host "Retention must be a positive integer. Aborting." -ForegroundColor Red + exit 1 +} +$RetentionDays = [int]$RetentionDays +$RunAt = Read-Default -Prompt 'Run daily at' -Default '3am' + +# ---- 4. Validate paths BEFORE touching anything -------------------------- +# The real silent failure isn't just low privilege - it's a path that +# doesn't exist. icacls and the task will both return 0 against nothing. +Write-Host "" +Write-Host "Validating paths..." -ForegroundColor Cyan +$missing = @() +foreach ($p in $Paths) { + if (Test-Path -LiteralPath $p -PathType Container) { + Write-Host " [ok] $p" -ForegroundColor Green + } else { + Write-Host " [MISSING] $p" -ForegroundColor Red + $missing += $p + } +} +if ($missing.Count -gt 0) { + Write-Host "" + Write-Host "One or more paths don't exist. Fix the typo or create the dir first." -ForegroundColor Red + Write-Host "Refusing to deploy against a phantom directory - that's the exact silent" -ForegroundColor Red + Write-Host "failure we're guarding against." -ForegroundColor Red + exit 1 +} + +# ---- 5. Summary + confirm ------------------------------------------------ +$taskName = "$Client - Clear Old Logs" +$taskPath = "\$Client\" + +Write-Host "" +Write-Host "--- Summary ---" -ForegroundColor Cyan +Write-Host " Client : $Client" +Write-Host " Task : $taskPath$taskName" +Write-Host " Retention : $RetentionDays days" +Write-Host " Run daily at : $RunAt" +Write-Host " Principal : NT AUTHORITY\NETWORK SERVICE (Limited)" +Write-Host " Paths :" +$Paths | ForEach-Object { Write-Host " $_" } +Write-Host "" + +$existing = Get-ScheduledTask -TaskPath $taskPath -TaskName $taskName -ErrorAction SilentlyContinue +if ($existing) { + Write-Host "NOTE: a task named '$taskName' already exists at $taskPath - it will be OVERWRITTEN." -ForegroundColor Yellow +} + +if (-not (Confirm-YesNo -Prompt 'Proceed?' -Default $true)) { + Write-Host "Aborted. Nothing changed." -ForegroundColor Yellow + exit 0 +} + +# ---- 6. Grant NETWORK SERVICE Modify (idempotent) ------------------------ +if (Confirm-YesNo -Prompt 'Grant NETWORK SERVICE Modify on the paths? (safe to re-run)' -Default $true) { + foreach ($p in $Paths) { + Write-Host " icacls grant on $p" -ForegroundColor DarkCyan + & icacls $p /grant '*S-1-5-20:(OI)(CI)M' /T /C | Out-Null + if ($LASTEXITCODE -ne 0) { + Write-Host " icacls returned $LASTEXITCODE on $p - check the path/permissions." -ForegroundColor Yellow + } + } +} else { + Write-Host " Skipping ACL grant (assuming already set)." -ForegroundColor DarkYellow +} + +# ---- 7. Blast-radius preview (deletes NOTHING) --------------------------- +Write-Host "" +Write-Host "Blast radius (files older than $RetentionDays days) - NOTHING deleted yet:" -ForegroundColor Cyan +$cutoff = (Get-Date).AddDays(-$RetentionDays) +$totalDoomed = 0 +foreach ($p in $Paths) { + $doomed = @(Get-ChildItem -LiteralPath $p -Recurse -File -ErrorAction SilentlyContinue | + Where-Object { $_.LastWriteTime -lt $cutoff }) + $totalDoomed += $doomed.Count + Write-Host " $p : $($doomed.Count) file(s)" -ForegroundColor $(if ($doomed.Count) { 'Yellow' } else { 'Green' }) +} +Write-Host " TOTAL would be removed on next run: $totalDoomed" -ForegroundColor Yellow +Write-Host "" +if (-not (Confirm-YesNo -Prompt 'Blast radius look right? Register the task?' -Default $true)) { + Write-Host "Aborted before registration. ACLs may already be set; task NOT created." -ForegroundColor Yellow + exit 0 +} + +# ---- 8. Build encoded delete command + register -------------------------- +# Base64 sidesteps every quoting/pipe issue in the task argument field. +$pathList = ($Paths | ForEach-Object { "'$_'" }) -join ',' +$cmd = "Get-ChildItem $pathList -Recurse -File | Where-Object LastWriteTime -lt (Get-Date).AddDays(-$RetentionDays) | Remove-Item -Force" +$enc = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($cmd)) + +$psExe = "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" +$action = New-ScheduledTaskAction -Execute $psExe -Argument "-NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand $enc" +$trigger = New-ScheduledTaskTrigger -Daily -At $RunAt +$principal = New-ScheduledTaskPrincipal -UserId 'NT AUTHORITY\NETWORK SERVICE' -LogonType ServiceAccount -RunLevel Limited +$settings = New-ScheduledTaskSettingsSet -MultipleInstances IgnoreNew -StartWhenAvailable -ExecutionTimeLimit (New-TimeSpan -Hours 1) + +Register-ScheduledTask -TaskName $taskName -TaskPath $taskPath -Action $action -Trigger $trigger ` + -Principal $principal -Settings $settings ` + -Description "Deletes files older than $RetentionDays days (recursive) under the $Client log dirs. ASP 7.1." -Force | Out-Null + +Write-Host "Task registered: $taskPath$taskName" -ForegroundColor Green + +# ---- 9. Test on demand --------------------------------------------------- +if (Confirm-YesNo -Prompt 'Test-fire the task now and check the result?' -Default $true) { + Start-ScheduledTask -TaskPath $taskPath -TaskName $taskName + Start-Sleep -Seconds 8 + $info = Get-ScheduledTaskInfo -TaskPath $taskPath -TaskName $taskName + Write-Host "" + Write-Host " LastRunTime : $($info.LastRunTime)" + $col = if ($info.LastTaskResult -eq 0) { 'Green' } else { 'Red' } + Write-Host " LastTaskResult : $($info.LastTaskResult)" -ForegroundColor $col + if ($info.LastTaskResult -eq 0) { + Write-Host "" + Write-Host "Result 0. Now eyeball the dirs - old files gone, recent ones untouched." -ForegroundColor Green + Write-Host "Don't write the KB article until you've confirmed that by eye." -ForegroundColor DarkYellow + } else { + Write-Host "" + Write-Host "Non-zero result. Task ran but something's off - check the dirs and" -ForegroundColor Red + Write-Host "the principal's Modify rights before trusting an unattended run." -ForegroundColor Red + } +} + +Write-Host "" +Write-Host "Done." -ForegroundColor Cyan \ No newline at end of file