<# .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