<# .SYNOPSIS Wrapper for running Invoke-RemoteMaintenance.ps1 unattended via Task Scheduler. .DESCRIPTION Stores credentials securely using Windows DPAPI (encrypted to your user account) and runs maintenance tasks without interactive prompts. First run: use -SaveCredential to store your username/password. Subsequent runs (or Task Scheduler): credentials load automatically. .PARAMETER SaveCredential Prompt to save credentials for future unattended use. .PARAMETER Task Maintenance task to run (passed to Invoke-RemoteMaintenance.ps1). .PARAMETER ComputerListFile Path to PC list file (passed to Invoke-RemoteMaintenance.ps1). .PARAMETER CreateScheduledTask Register a Windows Scheduled Task to run this script on a schedule. .PARAMETER TaskTime Time for the scheduled task (default: 03:00 AM). .PARAMETER TaskFrequency How often to run: Daily, Weekly, or Once (default: Weekly). .PARAMETER TaskDay Day of week for Weekly frequency (default: Sunday). .EXAMPLE # Step 1: Save your credentials (one time, interactive) .\Schedule-Maintenance.ps1 -SaveCredential .EXAMPLE # Step 2: Test unattended run .\Schedule-Maintenance.ps1 -ComputerListFile ".\shopfloor-pcs.txt" -Task Reboot .EXAMPLE # Step 3: Create a scheduled task for weekly reboots .\Schedule-Maintenance.ps1 -CreateScheduledTask -ComputerListFile ".\shopfloor-pcs.txt" -Task Reboot -TaskFrequency Weekly -TaskDay Sunday -TaskTime "03:00" #> [CmdletBinding()] param( [switch]$SaveCredential, [string]$Task, [string]$ComputerListFile, [switch]$CreateScheduledTask, [string]$TaskTime = "03:00", [ValidateSet('Daily','Weekly','Once')] [string]$TaskFrequency = "Weekly", [ValidateSet('Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday')] [string]$TaskDay = "Sunday", [string]$TaskDate, [string]$Username, [string]$Password ) $credDir = Join-Path $PSScriptRoot ".creds" $keyFile = Join-Path $credDir "aes.key" $userFile = Join-Path $credDir "username.txt" $passFile = Join-Path $credDir "password.txt" $scriptDir = $PSScriptRoot # --------------------------------------------------------------------------- # Helper: load or create AES key (works across all user contexts on this PC) # --------------------------------------------------------------------------- function Get-AESKey { if (-not (Test-Path $credDir)) { New-Item -Path $credDir -ItemType Directory -Force | Out-Null } if (Test-Path $keyFile) { return [byte[]](Get-Content $keyFile) } $key = New-Object byte[] 32 [System.Security.Cryptography.RNGCryptoServiceProvider]::Create().GetBytes($key) $key | Set-Content $keyFile return $key } function Load-SavedCredential { if (-not (Test-Path $userFile) -or -not (Test-Path $passFile)) { return $null } $key = Get-AESKey $user = Get-Content $userFile $encPass = Get-Content $passFile $secPass = $encPass | ConvertTo-SecureString -Key $key return New-Object System.Management.Automation.PSCredential($user, $secPass) } # --------------------------------------------------------------------------- # Save credentials (AES key file - works from any user context on this PC) # --------------------------------------------------------------------------- if ($SaveCredential) { if ($Username -and $Password) { $secPass = ConvertTo-SecureString $Password -AsPlainText -Force $cred = New-Object System.Management.Automation.PSCredential($Username, $secPass) } else { Write-Host "Enter the credentials used to connect to remote shopfloor PCs:" -ForegroundColor Cyan Write-Host "If no prompt appears, re-run with: -Username 'DOMAIN\user' -Password 'pass'" -ForegroundColor Yellow $cred = Get-Credential -Message "Enter admin credentials for remote shopfloor PCs" } if (-not $cred) { Write-Host "No credentials provided. Exiting." -ForegroundColor Red exit 1 } $key = Get-AESKey $cred.UserName | Set-Content $userFile $cred.Password | ConvertFrom-SecureString -Key $key | Set-Content $passFile Write-Host "Credentials saved to: $credDir" -ForegroundColor Green Write-Host "Encrypted with AES-256 key - works from any user context (normal or admin)." -ForegroundColor Yellow Write-Host "" Write-Host "You can now run tasks unattended:" -ForegroundColor Cyan Write-Host " .\Schedule-Maintenance.ps1 -ComputerListFile '.\shopfloor-pcs.txt' -Task Reboot" exit 0 } # --------------------------------------------------------------------------- # Create Scheduled Task # --------------------------------------------------------------------------- if ($CreateScheduledTask) { if (-not $Task) { Write-Host "ERROR: -Task is required when creating a scheduled task." -ForegroundColor Red exit 1 } if (-not $ComputerListFile) { Write-Host "ERROR: -ComputerListFile is required when creating a scheduled task." -ForegroundColor Red exit 1 } # Resolve to absolute paths $absListFile = (Resolve-Path $ComputerListFile -ErrorAction Stop).Path $absScript = Join-Path $scriptDir "Schedule-Maintenance.ps1" if (-not (Test-Path $passFile)) { Write-Host "ERROR: No saved credentials found. Run with -SaveCredential first." -ForegroundColor Red exit 1 } $taskName = "ShopfloorMaintenance-$Task" $psArgs = "-NoProfile -ExecutionPolicy Bypass -File `"$absScript`" -Task `"$Task`" -ComputerListFile `"$absListFile`"" $action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument $psArgs -WorkingDirectory $scriptDir $triggerTime = if ($TaskDate) { [DateTime]::Parse("$TaskDate $TaskTime") } else { [DateTime]::Parse($TaskTime) } $trigger = switch ($TaskFrequency) { 'Daily' { New-ScheduledTaskTrigger -Daily -At $triggerTime } 'Weekly' { New-ScheduledTaskTrigger -Weekly -DaysOfWeek $TaskDay -At $triggerTime } 'Once' { New-ScheduledTaskTrigger -Once -At $triggerTime } } $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -RunOnlyIfNetworkAvailable # Register as current user (needed for DPAPI credential decryption) $currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name Write-Host "Creating scheduled task '$taskName' as $currentUser..." -ForegroundColor Cyan Write-Host " Schedule: $TaskFrequency at $TaskTime$(if ($TaskFrequency -eq 'Weekly') { " ($TaskDay)" })" -ForegroundColor Gray Write-Host " Task: $Task" -ForegroundColor Gray Write-Host " PC List: $absListFile" -ForegroundColor Gray $existingTask = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue if ($existingTask) { Write-Host "Task '$taskName' already exists. Updating..." -ForegroundColor Yellow Set-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -Settings $settings | Out-Null } else { Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -Settings $settings -User $currentUser -RunLevel Highest -Description "Shopfloor PC Maintenance - $Task (via Invoke-RemoteMaintenance.ps1)" | Out-Null } Write-Host "Scheduled task '$taskName' created successfully." -ForegroundColor Green Write-Host "" Write-Host "Manage in Task Scheduler or with:" -ForegroundColor Cyan Write-Host " Get-ScheduledTask -TaskName '$taskName'" Write-Host " Start-ScheduledTask -TaskName '$taskName' # run now" Write-Host " Unregister-ScheduledTask -TaskName '$taskName' # delete" exit 0 } # --------------------------------------------------------------------------- # Run maintenance (unattended) # --------------------------------------------------------------------------- if (-not $Task) { Write-Host "ERROR: -Task is required. Example: -Task Reboot" -ForegroundColor Red Write-Host " Or use -SaveCredential to store credentials first." -ForegroundColor Yellow Write-Host " Or use -CreateScheduledTask to set up a scheduled run." -ForegroundColor Yellow exit 1 } if (-not $ComputerListFile) { Write-Host "ERROR: -ComputerListFile is required." -ForegroundColor Red exit 1 } # Load saved credentials $cred = Load-SavedCredential if (-not $cred) { Write-Host "ERROR: No saved credentials found in $credDir" -ForegroundColor Red Write-Host "Run with -SaveCredential first to store credentials." -ForegroundColor Yellow exit 1 } Write-Host "Loaded saved credentials for: $($cred.UserName)" -ForegroundColor Green # Log output $logDir = Join-Path $scriptDir "logs" if (-not (Test-Path $logDir)) { New-Item -Path $logDir -ItemType Directory -Force | Out-Null } $logFile = Join-Path $logDir "maintenance-$(Get-Date -Format 'yyyy-MM-dd_HHmmss')-$Task.log" Write-Host "Running: Invoke-RemoteMaintenance.ps1 -Task $Task -ComputerListFile $ComputerListFile" -ForegroundColor Cyan Write-Host "Log: $logFile" -ForegroundColor Gray $mainScript = Join-Path $scriptDir "Invoke-RemoteMaintenance.ps1" Start-Transcript -Path $logFile -Force | Out-Null & $mainScript -ComputerListFile $ComputerListFile -Task $Task -Credential $cred Stop-Transcript | Out-Null # Parse log for results summary $logContent = Get-Content $logFile -ErrorAction SilentlyContinue $okPCs = @($logContent | Select-String '\[OK\]\s+(\S+)' | ForEach-Object { $_.Matches[0].Groups[1].Value }) $failPCs = @($logContent | Select-String '\[FAIL\]\s+(\S+)' | ForEach-Object { $_.Matches[0].Groups[1].Value } | Where-Object { $_ -ne ':' } | Sort-Object -Unique) $summaryFile = Join-Path $logDir "LAST-RUN-SUMMARY.txt" $summary = @() $summary += "============================================" $summary += " MAINTENANCE RESULTS - $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" $summary += " Task: $Task" $summary += "============================================" $summary += "" $summary += "SUCCEEDED: $($okPCs.Count)" foreach ($pc in $okPCs) { $summary += " [OK] $pc" } $summary += "" $summary += "FAILED: $($failPCs.Count)" foreach ($pc in $failPCs) { $summary += " [FAIL] $pc" } $summary += "" $summary += "Full log: $logFile" $summary | Out-File -FilePath $summaryFile -Encoding UTF8 Write-Host "" Write-Host "Complete. Log saved to: $logFile" -ForegroundColor Green Write-Host "" Write-Host "=== RESULTS ===" -ForegroundColor White Write-Host " Succeeded: $($okPCs.Count)" -ForegroundColor Green foreach ($pc in $okPCs) { Write-Host " $pc" -ForegroundColor Green } if ($failPCs.Count -gt 0) { Write-Host " Failed: $($failPCs.Count)" -ForegroundColor Red foreach ($pc in $failPCs) { Write-Host " $pc" -ForegroundColor Red } } Write-Host "" Write-Host "Summary also saved to: $summaryFile" -ForegroundColor Yellow