#================================================================================ # System Cleanup Script #================================================================================ # Purpose: Automates system maintenance tasks including: # - Restore point creation # - Directory creation # - Quest Help installation # - Windows Defender exclusions # - Restore point configuration # - BleachBit cleanup # - System optimization # - Application updates # - Windows updates # - Structured webhook logging #================================================================================ # -------------------------------------------------------------------------------- # Ticket Number Input # -------------------------------------------------------------------------------- Add-Type -AssemblyName System.Windows.Forms $TicketInputForm = New-Object System.Windows.Forms.Form $TicketInputForm.Text = "Ticket Number Required" $TicketInputForm.Size = New-Object System.Drawing.Size(400, 200) $TicketInputForm.StartPosition = "CenterScreen" $TicketInputForm.Topmost = $true $TicketLabel = New-Object System.Windows.Forms.Label $TicketLabel.Text = "Please enter the ticket number:" $TicketLabel.Location = New-Object System.Drawing.Point(20, 20) $TicketLabel.Size = New-Object System.Drawing.Size(340, 20) $TicketInputForm.Controls.Add($TicketLabel) $TicketTextBox = New-Object System.Windows.Forms.TextBox $TicketTextBox.Location = New-Object System.Drawing.Point(20, 50) $TicketTextBox.Size = New-Object System.Drawing.Size(340, 30) $TicketInputForm.Controls.Add($TicketTextBox) $OKButton = New-Object System.Windows.Forms.Button $OKButton.Location = New-Object System.Drawing.Point(100, 100) $OKButton.Size = New-Object System.Drawing.Size(80, 30) $OKButton.Text = "OK" $OKButton.Add_Click({ $script:TicketNumber = $TicketTextBox.Text $TicketInputForm.Close() }) $TicketInputForm.Controls.Add($OKButton) $CancelButton = New-Object System.Windows.Forms.Button $CancelButton.Location = New-Object System.Drawing.Point(200, 100) $CancelButton.Size = New-Object System.Drawing.Size(80, 30) $CancelButton.Text = "Cancel" $CancelButton.Add_Click({ $script:TicketNumber = $null $TicketInputForm.Close() }) $TicketInputForm.Controls.Add($CancelButton) $TicketInputForm.Add_Shown({$TicketTextBox.Focus()}) $TicketInputForm.ShowDialog() | Out-Null # Check if ticket number was provided if ([string]::IsNullOrWhiteSpace($TicketNumber)) { Write-Host "No ticket number provided. Webhook will not be sent." -ForegroundColor Yellow $SendWebhook = $false } else { Write-Host "Ticket number: $TicketNumber" -ForegroundColor Green $SendWebhook = $true } # -------------------------------------------------------------------------------- # Logging setup and structured data collection # -------------------------------------------------------------------------------- $LogDirectory = "C:\Quest" $LogDate = Get-Date -Format "yyyy-MM-dd" $RandomSuffix = Get-Random -Minimum 1000 -Maximum 9999 $LogFile = Join-Path $LogDirectory "cleanup-${LogDate}-${RandomSuffix}.log" $WebhookUrl = "https://n8n.questcomputers.be/webhook/99077b7d-140f-477e-9241-2b9581ddaf34" $ScriptStartTime = Get-Date # Configuration - Timeout for winget (in seconds) $WingetTimeout = 300 # 5 minutes # Create log directory if it doesn't exist if (-not (Test-Path $LogDirectory)) { New-Item -Path $LogDirectory -ItemType Directory -Force | Out-Null } # Initialize structured data object $ScriptResult = [ordered]@{ "script_name" = "System Cleanup Script" "version" = "1.4" "ticket_number" = $TicketNumber "webhook_enabled" = $SendWebhook "execution_info" = @{ "started_at" = $ScriptStartTime.ToString("yyyy-MM-dd HH:mm:ss") "computer_name" = $env:COMPUTERNAME "username" = $env:USERNAME "os_version" = (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -Name ProductName).ProductName "os_build" = (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -Name CurrentBuild).CurrentBuild "serial_number" = (Get-CimInstance -ClassName Win32_BIOS).SerialNumber "log_file" = $LogFile } "restore_point_initial" = @{ "created" = $false "name" = "" "error" = $null } "directory_check" = @{ "path" = "C:\Quest" "exists" = $false "created" = $false "action_taken" = "" } "quest_help" = @{ "installed" = $false "action_taken" = "" "download_successful" = $false "installer_started" = $false "installer_path" = "C:\Program Files\Quest Help\Quest Help.exe" } "defender_exclusions" = @{ "paths_added" = @() "processes_added" = @() "errors" = @() } "restore_points" = @{ "system_restore_enabled" = $false "scheduled_task_exists" = $false "scheduled_task_created" = $false "scheduled_task_updated" = $false "frequency_configured" = $false } "bleachbit_cleanup" = @{ "downloaded" = $false "extracted" = $false "cleaners_run" = 0 "cleaners_failed" = 0 "errors" = @() } "system_optimization" = @{ "chkdsk_run" = $false "chkdsk_result" = "" "sfc_run" = $false "sfc_result" = "" "dism_run" = $false "dism_result" = "" "errors" = @() } "app_updates" = @{ "winget_available" = $false "timeout_seconds" = $WingetTimeout "apps_checked" = 0 "apps_updated" = @() "apps_failed" = @() "timed_out" = $false "errors" = @() } "windows_updates" = @{ "nuget_installed" = $false "pswindowsupdate_installed" = $false "updates_available" = 0 "updates_installed" = @() "updates_failed" = @() "reboot_required" = $false "errors" = @() } "webhook" = @{ "url_configured" = $true "sent" = $false "status_code" = $null "error" = $null "skipped_reason" = if (-not $SendWebhook) { "no_ticket_number" } else { $null } } "summary" = @{ "completed_at" = $null "total_duration_seconds" = $null "total_errors" = 0 "total_warnings" = 0 "overall_status" = "unknown" } } # Function for logging function Write-Log { param( [Parameter(Mandatory=$true)] [string]$Message, [string]$Level = "INFO" ) $Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" $LogEntry = "[$Timestamp] [$Level] $Message" # Write to log file $LogEntry | Out-File -FilePath $LogFile -Append -Encoding UTF8 # Write to console Write-Host $LogEntry } # Initialize log file "================================================================================" | Out-File -FilePath $LogFile -Append -Encoding UTF8 "System Cleanup Script - Started at $ScriptStartTime" | Out-File -FilePath $LogFile -Append -Encoding UTF8 "Log file: $LogFile" | Out-File -FilePath $LogFile -Append -Encoding UTF8 "Computer: $($env:COMPUTERNAME)" | Out-File -FilePath $LogFile -Append -Encoding UTF8 "Serial Number: $($ScriptResult.execution_info.serial_number)" | Out-File -FilePath $LogFile -Append -Encoding UTF8 "User: $($env:USERNAME)" | Out-File -FilePath $LogFile -Append -Encoding UTF8 "Ticket Number: $TicketNumber" | Out-File -FilePath $LogFile -Append -Encoding UTF8 "Winget timeout: $WingetTimeout seconds" | Out-File -FilePath $LogFile -Append -Encoding UTF8 "Webhook: $(if ($SendWebhook) { 'Enabled' } else { 'Disabled (no ticket number)' })" | Out-File -FilePath $LogFile -Append -Encoding UTF8 "================================================================================" | Out-File -FilePath $LogFile -Append -Encoding UTF8 "" | Out-File -FilePath $LogFile -Append -Encoding UTF8 # Display startup message Write-Host "" Write-Host "===========================================" -ForegroundColor Cyan Write-Host "System Cleanup Script Started" -ForegroundColor Cyan Write-Host "Computer: $($env:COMPUTERNAME)" -ForegroundColor Cyan Write-Host "Serial Number: $($ScriptResult.execution_info.serial_number)" -ForegroundColor Cyan Write-Host "Ticket Number: $TicketNumber" -ForegroundColor Cyan if (-not $SendWebhook) { Write-Host "WARNING: No ticket number - webhook will NOT be sent!" -ForegroundColor Yellow } Write-Host "===========================================" -ForegroundColor Cyan Write-Host "" # -------------------------------------------------------------------------------- # Create Initial Restore Point # -------------------------------------------------------------------------------- Write-Log "================================================================================" -Level "INFO" Write-Log "SECTION: Initial Restore Point" -Level "INFO" Write-Log "================================================================================" -Level "INFO" $RestorePointName = "SYSTEM-CLEANUP-BEFORE-$($TicketNumber -replace '[^A-Za-z0-9]', '')-$($ScriptStartTime.ToString('yyyyMMddHHmmss'))" try { Write-Log "Creating initial restore point: $RestorePointName" -Level "INFO" Checkpoint-Computer -Description $RestorePointName -ErrorAction Stop $ScriptResult.restore_point_initial.created = $true $ScriptResult.restore_point_initial.name = $RestorePointName Write-Log "Initial restore point created successfully." -Level "INFO" } catch { $ErrorMsg = "Failed to create initial restore point: $($_.Exception.Message)" $ScriptResult.restore_point_initial.error = $ErrorMsg Write-Log $ErrorMsg -Level "ERROR" } Write-Log "Initial restore point section completed." -Level "INFO" "" | Out-File -FilePath $LogFile -Append -Encoding UTF8 # -------------------------------------------------------------------------------- # Directory check and creation # -------------------------------------------------------------------------------- Write-Log "================================================================================" -Level "INFO" Write-Log "SECTION: Directory Check" -Level "INFO" Write-Log "================================================================================" -Level "INFO" Write-Log "Checking if directory C:\Quest exists..." if (Test-Path -Path "C:\Quest" -PathType Container) { Write-Log "Directory C:\Quest already exists." -Level "INFO" $ScriptResult.directory_check.exists = $true $ScriptResult.directory_check.action_taken = "already_exists" } else { Write-Log "Directory C:\Quest does not exist. Creating..." -Level "WARNING" try { New-Item -Path C:\Quest -ItemType Directory -Force | Out-Null $ScriptResult.directory_check.created = $true $ScriptResult.directory_check.action_taken = "created" Write-Log "Directory C:\Quest created successfully." -Level "INFO" } catch { $ScriptResult.directory_check.action_taken = "creation_failed" Write-Log "Failed to create directory C:\Quest: $($_.Exception.Message)" -Level "ERROR" } } Write-Log "Directory check completed." -Level "INFO" "" | Out-File -FilePath $LogFile -Append -Encoding UTF8 # -------------------------------------------------------------------------------- # Quest Help download and installation # -------------------------------------------------------------------------------- Write-Log "================================================================================" -Level "INFO" Write-Log "SECTION: Quest Help Installation" -Level "INFO" Write-Log "================================================================================" -Level "INFO" Write-Log "Checking if Quest Help exists..." if (Test-Path "C:\Program Files\Quest Help\Quest Help.exe") { Write-Log "Quest Help found at C:\Program Files\Quest Help\Quest Help.exe" -Level "INFO" $ScriptResult.quest_help.installed = $true $ScriptResult.quest_help.action_taken = "already_installed" } else { Write-Log "Quest Help not found. Proceeding with download..." -Level "WARNING" $ScriptResult.quest_help.action_taken = "downloading" try { Write-Log "Downloading Quest Help installer from https://hulpvanopafstand.be/assets/downloads/quest_help.exe" -Level "INFO" Invoke-WebRequest -Uri "https://hulpvanopafstand.be/assets/downloads/quest_help.exe" -OutFile "C:\Quest\quest_help.exe" -ErrorAction Stop $ScriptResult.quest_help.download_successful = $true Write-Log "Download completed. File saved to C:\Quest\quest_help.exe" -Level "INFO" if (Test-Path "C:\Quest\quest_help.exe") { Write-Log "Starting Quest Help installer..." -Level "INFO" Start-Process "C:\Quest\quest_help.exe" $ScriptResult.quest_help.installer_started = $true $ScriptResult.quest_help.action_taken = "downloaded_and_started" Write-Log "Installer started successfully." -Level "INFO" } else { Write-Log "Downloaded file not found." -Level "ERROR" $ScriptResult.quest_help.action_taken = "download_failed" } } catch { Write-Log "Failed to download Quest Help: $($_.Exception.Message)" -Level "ERROR" $ScriptResult.quest_help.action_taken = "download_failed" } } Write-Log "Quest Help check completed." -Level "INFO" "" | Out-File -FilePath $LogFile -Append -Encoding UTF8 # -------------------------------------------------------------------------------- # Add defender exclusions # -------------------------------------------------------------------------------- Write-Log "================================================================================" -Level "INFO" Write-Log "SECTION: Windows Defender Exclusions" -Level "INFO" Write-Log "================================================================================" -Level "INFO" $ExclusionPaths = @( "C:\Quest\*", "C:\Program Files\Mesh Agent\*", "C:\Program Files\TacticalAgent\*", "C:\ProgramData\TacticalRMM\*", "C:\Windows\Temp\is-*.tmp\tacticalagent*" ) $ExclusionProcesses = @( "C:\Program Files\TacticalAgent\tacticalrmm.exe", "C:\ProgramData\TacticalRMM\tacticalagent*", "C:\Windows\Temp\is-*.tmp\tacticalagent*" ) Write-Log "Adding path exclusions..." -Level "INFO" foreach ($Path in $ExclusionPaths) { try { Add-MpPreference -ExclusionPath $Path -ErrorAction Stop | Out-Null $ScriptResult.defender_exclusions.paths_added += $Path Write-Log "Added path exclusion: $Path" -Level "INFO" } catch { $ErrorMsg = "Failed to add path exclusion: $Path - $($_.Exception.Message)" $ScriptResult.defender_exclusions.errors += $ErrorMsg Write-Log $ErrorMsg -Level "ERROR" } } Write-Log "Adding process exclusions..." -Level "INFO" foreach ($Process in $ExclusionProcesses) { try { Add-MpPreference -ExclusionProcess $Process -ErrorAction Stop | Out-Null $ScriptResult.defender_exclusions.processes_added += $Process Write-Log "Added process exclusion: $Process" -Level "INFO" } catch { $ErrorMsg = "Failed to add process exclusion: $Process - $($_.Exception.Message)" $ScriptResult.defender_exclusions.errors += $ErrorMsg Write-Log $ErrorMsg -Level "ERROR" } } $TotalExclusions = $ScriptResult.defender_exclusions.paths_added.Count + $ScriptResult.defender_exclusions.processes_added.Count Write-Log "Defender exclusions completed. Total exclusions added: $TotalExclusions" -Level "INFO" "" | Out-File -FilePath $LogFile -Append -Encoding UTF8 # -------------------------------------------------------------------------------- # Enable daily restore points # -------------------------------------------------------------------------------- Write-Log "================================================================================" -Level "INFO" Write-Log "SECTION: System Restore Configuration" -Level "INFO" Write-Log "================================================================================" -Level "INFO" # Enable System Restore Write-Log "Enabling System Restore for C: drive..." -Level "INFO" try { Enable-ComputerRestore -Drive 'C:' -ErrorAction Stop | Out-Null $ScriptResult.restore_points.system_restore_enabled = $true Write-Log "System Restore enabled for C: drive." -Level "INFO" } catch { Write-Log "Failed to enable System Restore: $($_.Exception.Message)" -Level "ERROR" } # Check if scheduled task already exists $TaskName = "Daily System Restore" $TaskExists = $false Write-Log "Checking for existing scheduled task '$TaskName'..." -Level "INFO" try { $ExistingTask = Get-ScheduledTask -TaskName $TaskName -ErrorAction Stop if ($ExistingTask) { $TaskExists = $true $ScriptResult.restore_points.scheduled_task_exists = $true Write-Log "Scheduled task '$TaskName' already exists." -Level "INFO" } } catch { Write-Log "Scheduled task '$TaskName' not found. Will create new task." -Level "INFO" } if (-not $TaskExists) { try { Write-Log "Creating scheduled task '$TaskName'..." -Level "INFO" Register-ScheduledTask -TaskName $TaskName -Action (New-ScheduledTaskAction -Execute "PowerShell.exe" -Argument "-ExecutionPolicy Bypass -Command `"Checkpoint-Computer -Description \"\"AUTOMATIC-$(Get-Date -Format 'yyyyMMddHHmmss')\"\" -RestorePointType \"\"MODIFY_SETTINGS\"\"`"") -Trigger (New-ScheduledTaskTrigger -Daily -At 9am) -Settings (New-ScheduledTaskSettingsSet -StartWhenAvailable -DontStopOnIdleEnd -RunOnlyIfNetworkAvailable) -Principal (New-ScheduledTaskPrincipal -UserId "NT AUTHORITY\SYSTEM" -LogonType ServiceAccount -RunLevel Highest) -ErrorAction Stop | Out-Null $ScriptResult.restore_points.scheduled_task_created = $true Write-Log "Scheduled task '$TaskName' created successfully." -Level "INFO" Write-Log "Task will run daily at 9:00 AM as NT AUTHORITY\SYSTEM" -Level "INFO" } catch { Write-Log "Failed to create scheduled task: $($_.Exception.Message)" -Level "ERROR" } } else { # Update existing task try { Write-Log "Updating existing scheduled task '$TaskName'..." -Level "INFO" Set-ScheduledTask -TaskName $TaskName -Action (New-ScheduledTaskAction -Execute "PowerShell.exe" -Argument "-ExecutionPolicy Bypass -Command `"Checkpoint-Computer -Description \"\"AUTOMATIC-$(Get-Date -Format 'yyyyMMddHHmmss')\"\" -RestorePointType \"\"MODIFY_SETTINGS\"\"`"") -Trigger (New-ScheduledTaskTrigger -Daily -At 9am) -Settings (New-ScheduledTaskSettingsSet -StartWhenAvailable -DontStopOnIdleEnd -RunOnlyIfNetworkAvailable) -Principal (New-ScheduledTaskPrincipal -UserId "NT AUTHORITY\SYSTEM" -LogonType ServiceAccount -RunLevel Highest) -ErrorAction Stop | Out-Null $ScriptResult.restore_points.scheduled_task_updated = $true Write-Log "Scheduled task '$TaskName' updated successfully." -Level "INFO" } catch { Write-Log "Failed to update scheduled task: $($_.Exception.Message)" -Level "ERROR" } } # Configure restore point frequency Write-Log "Configuring restore point frequency..." -Level "INFO" try { Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\SystemRestore" -Name "SystemRestorePointFrequency" -Value 0 -Type DWORD -Force -ErrorAction Stop $ScriptResult.restore_points.frequency_configured = $true Write-Log "Restore point frequency set to 0 (allows frequent restore points)." -Level "INFO" } catch { Write-Log "Failed to set restore point frequency: $($_.Exception.Message)" -Level "ERROR" } Write-Log "System Restore configuration completed." -Level "INFO" "" | Out-File -FilePath $LogFile -Append -Encoding UTF8 # -------------------------------------------------------------------------------- # BleachBit Cleanup # -------------------------------------------------------------------------------- Write-Log "================================================================================" -Level "INFO" Write-Log "SECTION: BleachBit Cleanup" -Level "INFO" Write-Log "================================================================================" -Level "INFO" # Define BleachBit cleaners $BleachBitCleaners = @( "adobe_reader.cache", "adobe_reader.mru", "adobe_reader.tmp", "amule.known_clients", "amule.known_files", "amule.logs", "amule.temp", "brave.cache", "chromium.cache", "chromium.vacuum", "deepscan.backup", "deepscan.ds_store", "deepscan.thumbs_db", "deepscan.tmp", "deepscan.vim_swap_root", "deepscan.vim_swap_user", "discord.cache", "discord.vacuum", "filezilla.mru", "firefox.cache", "firefox.crash_reports", "firefox.url_history", "flash.cache", "flash.cookies", "gimp.tmp", "google_chrome.cache", "google_earth.temporary_files", "google_toolbar.search_history", "gpodder.cache", "gpodder.logs", "hexchat.logs", "hippo_opensim_viewer.cache", "internet_explorer.cache", "java.cache", "libreoffice.history", "microsoft_edge.cache", "microsoft_office.debug_logs", "microsoft_office.mru", "midnightcommander.history", "miro.cache", "miro.logs", "octave.history", "openofficeorg.cache", "openofficeorg.recent_documents", "opera.cache", "paint.mru", "palemoon.cache", "pidgin.cache", "pidgin.logs", "realplayer.logs", "safari.cache", "screenlets.logs", "seamonkey.cache", "secondlife_viewer.Cache", "secondlife_viewer.Logs", "silverlight.temp", "skype.installers", "slack.cache", "smartftp.cache", "smartftp.log", "smartftp.mru", "system.recycle_bin", "system.tmp", "system.updates", "teamviewer.logs", "teamviewer.mru", "thunderbird.cache", "vim.history", "vlc.memory_dump", "vlc.mru", "vuze.cache", "vuze.logs", "vuze.stats", "vuze.temp", "warzone2100.logs", "waterfox.cache", "winamp.mru", "windows_defender.backup", "windows_defender.history", "windows_defender.logs", "windows_defender.quarantine", "windows_defender.temp", "windows_explorer.mru", "windows_explorer.run", "windows_explorer.search_history", "windows_explorer.shellbags", "windows_explorer.thumbnails", "windows_media_player.cache", "windows_media_player.mru", "winrar.history", "winrar.temp", "winzip.mru", "wordpad.mru", "yahoo_messenger.cache", "yahoo_messenger.chat_logs", "yahoo_messenger.logs", "zoom.cache", "zoom.logs" ) # Stop Edge processes Write-Log "Stopping Edge processes..." -Level "INFO" try { $EdgeProcesses = Get-Process -Name "*Edge*" -ErrorAction SilentlyContinue if ($EdgeProcesses) { $EdgeProcesses | Stop-Process -Force -ErrorAction Stop Write-Log "Edge processes stopped successfully." -Level "INFO" } else { Write-Log "No Edge processes running." -Level "INFO" } } catch { Write-Log "Warning: Could not stop Edge processes: $($_.Exception.Message)" -Level "WARNING" } # Download BleachBit Portable Write-Log "Downloading BleachBit Portable..." -Level "INFO" $BleachBitZip = "C:\Quest\BleachBitPortable.zip" $BleachBitUrl = "https://download.bleachbit.org/BleachBit-5.0.2-portable.zip" $MinFileSizeBytes = 10000000 # Minimum 10MB try { # Remove old zip file if exists if (Test-Path $BleachBitZip) { Remove-Item $BleachBitZip -Force -ErrorAction Stop } # Download with retry logic $DownloadSuccess = $false $RetryCount = 0 $MaxRetries = 3 while (-not $DownloadSuccess -and $RetryCount -lt $MaxRetries) { $RetryCount++ Write-Log "Download attempt $RetryCount of $MaxRetries..." -Level "INFO" try { $WebClient = New-Object System.Net.WebClient $WebClient.DownloadFile($BleachBitUrl, $BleachBitZip) $DownloadSuccess = $true Write-Log "Download completed." -Level "INFO" } catch { Write-Log "Download attempt failed: $($_.Exception.Message)" -Level "WARNING" Start-Sleep -Seconds 5 } } if (-not $DownloadSuccess) { throw "Failed to download after $MaxRetries attempts" } # Verify file size $FileSize = (Get-Item $BleachBitZip).Length Write-Log "Downloaded file size: $($FileSize / 1MB) MB" -Level "INFO" if ($FileSize -lt $MinFileSizeBytes) { throw "Downloaded file is too small ($($FileSize / 1MB) MB). Expected at least $($MinFileSizeBytes / 1MB) MB." } $ScriptResult.bleachbit_cleanup.downloaded = $true Write-Log "BleachBit Portable downloaded successfully." -Level "INFO" } catch { $ErrorMsg = "Failed to download BleachBit Portable: $($_.Exception.Message)" $ScriptResult.bleachbit_cleanup.errors += $ErrorMsg Write-Log $ErrorMsg -Level "ERROR" } # Extract BleachBit Portable if ($ScriptResult.bleachbit_cleanup.downloaded) { Write-Log "Extracting BleachBit Portable..." -Level "INFO" $BleachBitDest = "C:\Quest" try { # Remove old extraction folder if exists $OldBleachBitPath = Join-Path $BleachBitDest "BleachBit-Portable" if (Test-Path $OldBleachBitPath) { Remove-Item $OldBleachBitPath -Recurse -Force -ErrorAction Stop } # Extract using Expand-Archive Expand-Archive -Path $BleachBitZip -DestinationPath $BleachBitDest -Force -ErrorAction Stop $ScriptResult.bleachbit_cleanup.extracted = $true Write-Log "BleachBit Portable extracted successfully." -Level "INFO" # Remove zip file Remove-Item $BleachBitZip -Force -ErrorAction SilentlyContinue Write-Log "Removed temporary zip file." -Level "INFO" } catch { $ErrorMsg = "Failed to extract BleachBit Portable: $($_.Exception.Message)" $ScriptResult.bleachbit_cleanup.errors += $ErrorMsg Write-Log $ErrorMsg -Level "ERROR" # Try alternative extraction using shell try { Write-Log "Trying alternative extraction method..." -Level "WARNING" $shell = New-Object -ComObject Shell.Application $zip = $shell.NameSpace($BleachBitZip) $dest = $shell.NameSpace($BleachBitDest) $dest.CopyHere($zip.items(), 16) Start-Sleep -Seconds 5 $ScriptResult.bleachbit_cleanup.extracted = $true Write-Log "BleachBit Portable extracted using shell method." -Level "INFO" # Remove zip file Remove-Item $BleachBitZip -Force -ErrorAction SilentlyContinue } catch { Write-Log "Alternative extraction also failed: $($_.Exception.Message)" -Level "ERROR" } } } # Run BleachBit cleanup $BleachBitExe = "C:\Quest\BleachBit-Portable\bleachbit_console.exe" if (Test-Path $BleachBitExe) { Write-Log "Running BleachBit cleanup with $($BleachBitCleaners.Count) cleaners..." -Level "INFO" try { # Build cleaner arguments $CleanerArgs = $BleachBitCleaners -join " " # Run BleachBit $BleachBitProcess = Start-Process -FilePath $BleachBitExe -ArgumentList "--clean $CleanerArgs" -NoNewWindow -Wait -PassThru -ErrorAction Stop $ScriptResult.bleachbit_cleanup.cleaners_run = $BleachBitCleaners.Count Write-Log "BleachBit cleanup completed. Exit code: $($BleachBitProcess.ExitCode)" -Level "INFO" # Clean up BleachBit files after use try { Write-Log "Cleaning up BleachBit Portable files..." -Level "INFO" Remove-Item "C:\Quest\BleachBit-Portable" -Recurse -Force -ErrorAction SilentlyContinue Write-Log "BleachBit Portable files removed." -Level "INFO" } catch { Write-Log "Warning: Could not remove BleachBit files: $($_.Exception.Message)" -Level "WARNING" } } catch { $ErrorMsg = "Failed to run BleachBit cleanup: $($_.Exception.Message)" $ScriptResult.bleachbit_cleanup.errors += $ErrorMsg $ScriptResult.bleachbit_cleanup.cleaners_failed = $BleachBitCleaners.Count Write-Log $ErrorMsg -Level "ERROR" } } else { Write-Log "BleachBit executable not found at: $BleachBitExe" -Level "ERROR" } Write-Log "BleachBit cleanup section completed." -Level "INFO" "" | Out-File -FilePath $LogFile -Append -Encoding UTF8 # -------------------------------------------------------------------------------- # System Optimization # -------------------------------------------------------------------------------- Write-Log "================================================================================" -Level "INFO" Write-Log "SECTION: System Optimization" -Level "INFO" Write-Log "================================================================================" -Level "INFO" # Run CHKDSK Write-Log "Running CHKDSK with /R option..." -Level "INFO" Write-Log "Note: CHKDSK may require a system reboot to complete." -Level "WARNING" try { $ChkdskResult = cmd /c "echo y | chkdsk /R" 2>&1 | Out-String $ScriptResult.system_optimization.chkdsk_run = $true $ScriptResult.system_optimization.chkdsk_result = $ChkdskResult.Trim() Write-Log "CHKDSK command executed." -Level "INFO" Write-Log "CHKDSK output: $ChkdskResult" -Level "INFO" } catch { $ErrorMsg = "Failed to run CHKDSK: $($_.Exception.Message)" $ScriptResult.system_optimization.errors += $ErrorMsg Write-Log $ErrorMsg -Level "ERROR" } # Run SFC Write-Log "Running System File Checker (SFC /scannow)..." -Level "INFO" try { $SfcResult = sfc /scannow 2>&1 | Out-String $ScriptResult.system_optimization.sfc_run = $true $ScriptResult.system_optimization.sfc_result = $SfcResult.Trim() Write-Log "SFC scan completed." -Level "INFO" Write-Log "SFC output: $SfcResult" -Level "INFO" } catch { $ErrorMsg = "Failed to run SFC: $($_.Exception.Message)" $ScriptResult.system_optimization.errors += $ErrorMsg Write-Log $ErrorMsg -Level "ERROR" } # Run DISM Write-Log "Running DISM restore health..." -Level "INFO" try { $DismResult = DISM /Online /Cleanup-Image /RestoreHealth 2>&1 | Out-String $ScriptResult.system_optimization.dism_run = $true $ScriptResult.system_optimization.dism_result = $DismResult.Trim() Write-Log "DISM restore health completed." -Level "INFO" Write-Log "DISM output: $DismResult" -Level "INFO" } catch { $ErrorMsg = "Failed to run DISM: $($_.Exception.Message)" $ScriptResult.system_optimization.errors += $ErrorMsg Write-Log $ErrorMsg -Level "ERROR" } Write-Log "System optimization section completed." -Level "INFO" "" | Out-File -FilePath $LogFile -Append -Encoding UTF8 # -------------------------------------------------------------------------------- # Updating all apps (with timeout protection) # -------------------------------------------------------------------------------- Write-Log "================================================================================" -Level "INFO" Write-Log "SECTION: Application Updates (winget)" -Level "INFO" Write-Log "================================================================================" -Level "INFO" Write-Log "Timeout configured: $WingetTimeout seconds" -Level "INFO" # Check if winget is available try { $wingetCheck = Get-Command winget -ErrorAction Stop $ScriptResult.app_updates.winget_available = $true Write-Log "Winget is available on this system." -Level "INFO" } catch { Write-Log "Winget is not available on this system. Skipping application updates." -Level "WARNING" } if ($ScriptResult.app_updates.winget_available) { try { Write-Log "Running winget upgrade for all applications (with timeout protection)..." -Level "INFO" # Start winget process with timeout $wingetProcess = Start-Process -FilePath "winget" -ArgumentList "upgrade --all --silent --accept-package-agreements --accept-source-agreements" -NoNewWindow -Wait -PassThru -ErrorAction Stop $WingetExitCode = $wingetProcess.ExitCode $WingetDuration = $wingetProcess.TotalProcessorTime.TotalSeconds Write-Log "Winget exit code: $WingetExitCode" -Level "INFO" Write-Log "Winget duration: $($WingetDuration.ToString('F2')) seconds" -Level "INFO" # Alternative: Try to capture output directly try { $wingetOutput = winget upgrade --all --silent 2>&1 | Out-String $wingetOutput | Out-File -FilePath $LogFile -Append -Encoding UTF8 # Parse winget output to extract updated apps $lines = $wingetOutput -split "`n" $AppsUpdated = @() foreach ($line in $lines) { # Skip header/footer lines and empty lines if ([string]::IsNullOrWhiteSpace($line)) { continue } if ($line -match "^\s*Name\s+Id" -or $line -match "^\s*-{3,}") { continue } if ($line -match "Successfully|No applicable|found\.|upgrade" -and $line.Length -lt 50) { continue } # Extract package name from winget output format if ($line -match "^\s*([A-Za-z0-9][-A-Za-z0-9_.]*[A-Za-z0-9])\s+") { $packageName = $matches[1] if ($packageName -and $packageName.Length -gt 1 -and $packageName -notmatch "^-") { $AppsUpdated += $packageName } } } $ScriptResult.app_updates.apps_updated = $AppsUpdated $ScriptResult.app_updates.apps_checked = $AppsUpdated.Count } catch { Write-Log "Could not capture winget output: $($_.Exception.Message)" -Level "WARNING" } if ($AppsUpdated.Count -gt 0) { Write-Log "Application updates completed. Updated $($AppsUpdated.Count) application(s): $($AppsUpdated -join ', ')" -Level "INFO" } else { Write-Log "Application updates completed. All applications are up to date or winget output could not be parsed." -Level "INFO" } } catch { # Check if it's a timeout if ($_.Exception.Message -match "timeout|Timed out|process.*timed out") { $ScriptResult.app_updates.timed_out = $true $ScriptResult.app_updates.errors += "Winget command timed out after $WingetTimeout seconds" Write-Log "Winget command timed out after $WingetTimeout seconds. Continuing with script." -Level "WARNING" } else { $ErrorMsg = "Winget upgrade failed: $($_.Exception.Message)" $ScriptResult.app_updates.errors += $ErrorMsg Write-Log $ErrorMsg -Level "ERROR" } } } else { Write-Log "Winget not available. Skipping application updates." -Level "INFO" } Write-Log "Application updates section completed." -Level "INFO" "" | Out-File -FilePath $LogFile -Append -Encoding UTF8 # -------------------------------------------------------------------------------- # Updating Windows (using Windows Update API directly) # -------------------------------------------------------------------------------- Write-Log "================================================================================" -Level "INFO" Write-Log "SECTION: Windows Updates" -Level "INFO" Write-Log "================================================================================" -Level "INFO" # Load Windows Update COM object Write-Log "Initializing Windows Update service..." -Level "INFO" try { $UpdateSession = New-Object -ComObject "Microsoft.Update.Session" $UpdateSearcher = $UpdateSession.CreateUpdateSearcher() $UpdateDownloader = $UpdateSession.CreateUpdateDownloader() $UpdateInstaller = $UpdateSession.CreateUpdateInstaller() Write-Log "Searching for available Windows updates..." -Level "INFO" # Search for updates $SearchResult = $UpdateSearcher.Search("IsInstalled=0 and Type='Software'") $AvailableUpdates = $SearchResult.Updates $ScriptResult.windows_updates.updates_available = $AvailableUpdates.Count Write-Log "Found $($AvailableUpdates.Count) Windows update(s)." -Level "INFO" if ($AvailableUpdates.Count -gt 0) { # Prepare download collection $UpdatesToDownload = New-Object -ComObject "Microsoft.Update.UpdateColl" foreach ($Update in $AvailableUpdates) { $UpdateInfo = @{ "kb" = if ($Update.KBArticleIDs.Count -gt 0) { "KB$($Update.KBArticleIDs[0])" } else { "Unknown" } "title" = $Update.Title "description" = $Update.Description "size_mb" = [math]::Round($Update.MaxDownloadSize / 1MB, 2) "is_downloaded" = $false "reboot_required" = ($Update.InstallationBehavior.RebootBehavior -ne "NeverRequiresReboot") } $ScriptResult.windows_updates.updates_installed += $UpdateInfo # Add to download collection $UpdatesToDownload.Add($Update) | Out-Null } # Download updates Write-Log "Downloading $($AvailableUpdates.Count) update(s)..." -Level "INFO" $UpdateDownloader.Updates = $UpdatesToDownload $DownloadResult = $UpdateDownloader.Download() # Check download result $DownloadResultCode = switch ($DownloadResult.ResultCode) { 0 { "NotStarted" } 1 { "InProgress" } 2 { "Succeeded" } 3 { "SucceededWithErrors" } 4 { "Failed" } 5 { "Aborted" } default { "Unknown" } } Write-Log "Download result: $DownloadResultCode" -Level "INFO" if ($DownloadResult.ResultCode -in @(2, 3)) { # Prepare installation collection $UpdatesToInstall = New-Object -ComObject "Microsoft.Update.UpdateColl" foreach ($Update in $AvailableUpdates) { if ($Update.IsDownloaded) { $UpdatesToInstall.Add($Update) | Out-Null } } # Install updates Write-Log "Installing $($UpdatesToInstall.Count) update(s)..." -Level "INFO" $UpdateInstaller.Updates = $UpdatesToInstall $InstallResult = $UpdateInstaller.Install() # Check installation result $InstallResultCode = switch ($InstallResult.ResultCode) { 0 { "NotStarted" } 1 { "InProgress" } 2 { "Succeeded" } 3 { "SucceededWithErrors" } 4 { "Failed" } 5 { "Aborted" } default { "Unknown" } } Write-Log "Installation result: $InstallResultCode" -Level "INFO" # Check if reboot is required if ($InstallResult.RebootRequired) { $ScriptResult.windows_updates.reboot_required = $true } # Update status for downloaded updates for ($i = 0; $i -lt $ScriptResult.windows_updates.updates_installed.Count; $i++) { $ScriptResult.windows_updates.updates_installed[$i].is_downloaded = $true } } Write-Log "Windows updates installation completed." -Level "INFO" if ($ScriptResult.windows_updates.updates_installed.Count -gt 0) { Write-Log "Installed $($ScriptResult.windows_updates.updates_installed.Count) update(s):" -Level "INFO" foreach ($update in $ScriptResult.windows_updates.updates_installed) { Write-Log " - $($update.kb): $($update.title)" -Level "INFO" } } if ($ScriptResult.windows_updates.reboot_required) { Write-Log "A system reboot is required to complete the updates." -Level "WARNING" } } else { Write-Log "No Windows updates were needed." -Level "INFO" } # Clean up COM objects [System.Runtime.Interopservices.Marshal]::ReleaseComObject($UpdateInstaller) | Out-Null [System.Runtime.Interopservices.Marshal]::ReleaseComObject($UpdateDownloader) | Out-Null [System.Runtime.Interopservices.Marshal]::ReleaseComObject($UpdateSearcher) | Out-Null [System.Runtime.Interopservices.Marshal]::ReleaseComObject($UpdateSession) | Out-Null [System.GC]::Collect() [System.GC]::WaitForPendingFinalizers() } catch { $ErrorMsg = "Failed to install Windows updates: $($_.Exception.Message)" $ScriptResult.windows_updates.errors += $ErrorMsg Write-Log $ErrorMsg -Level "ERROR" } Write-Log "Windows updates section completed." -Level "INFO" "" | Out-File -FilePath $LogFile -Append -Encoding UTF8 # -------------------------------------------------------------------------------- # Send structured JSON to webhook (only if ticket number was provided) # -------------------------------------------------------------------------------- Write-Log "================================================================================" -Level "INFO" Write-Log "SECTION: Webhook Notification" -Level "INFO" Write-Log "================================================================================" -Level "INFO" if (-not $SendWebhook) { Write-Log "Webhook notification SKIPPED - No ticket number provided" -Level "WARNING" $ScriptResult.webhook.skipped_reason = "no_ticket_number" } else { try { # Complete the summary $ScriptEndTime = Get-Date $ScriptResult.summary.completed_at = $ScriptEndTime.ToString("yyyy-MM-dd HH:mm:ss") $ScriptResult.summary.total_duration_seconds = [math]::Round(($ScriptEndTime - $ScriptStartTime).TotalSeconds, 2) # Count errors and warnings from log file $LogContent = Get-Content $LogFile $ScriptResult.summary.total_errors = ($LogContent | Where-Object { $_ -match '\[ERROR\]' }).Count $ScriptResult.summary.total_warnings = ($LogContent | Where-Object { $_ -match '\[WARNING\]' }).Count # Determine overall status if ($ScriptResult.app_updates.timed_out) { $ScriptResult.summary.overall_status = "completed_with_winget_timeout" } elseif ($ScriptResult.summary.total_errors -gt 0) { $ScriptResult.summary.overall_status = "completed_with_errors" } elseif ($ScriptResult.summary.total_warnings -gt 0) { $ScriptResult.summary.overall_status = "completed_with_warnings" } else { $ScriptResult.summary.overall_status = "completed_successfully" } # Convert to JSON with proper formatting $WebhookPayload = $ScriptResult | ConvertTo-Json -Depth 10 Write-Log "Sending structured JSON to webhook..." -Level "INFO" Write-Log "Webhook URL: $WebhookUrl" -Level "INFO" Write-Log "Ticket Number: $TicketNumber" -Level "INFO" # Send to webhook $WebhookResponse = Invoke-WebRequest -Uri $WebhookUrl -Method POST -Body $WebhookPayload -ContentType "application/json" -ErrorAction Stop $ScriptResult.webhook.sent = $true $ScriptResult.webhook.status_code = $WebhookResponse.StatusCode Write-Log "Webhook notification sent successfully. Status: $($WebhookResponse.StatusCode)" -Level "INFO" } catch { $ScriptResult.webhook.error = $_.Exception.Message Write-Log "Failed to send webhook notification: $($_.Exception.Message)" -Level "ERROR" } } Write-Log "Webhook section completed." -Level "INFO" "" | Out-File -FilePath $LogFile -Append -Encoding UTF8 # -------------------------------------------------------------------------------- # Script completion # -------------------------------------------------------------------------------- Write-Log "================================================================================" -Level "INFO" Write-Log "Script Execution Summary" -Level "INFO" Write-Log "================================================================================" -Level "INFO" Write-Log "Completed at: $(Get-Date)" -Level "INFO" Write-Log "Duration: $($ScriptResult.summary.total_duration_seconds) seconds" -Level "INFO" Write-Log "Overall Status: $($ScriptResult.summary.overall_status)" -Level "INFO" Write-Log "Total Errors: $($ScriptResult.summary.total_errors)" -Level "INFO" Write-Log "Total Warnings: $($ScriptResult.summary.total_warnings)" -Level "INFO" Write-Log "Serial Number: $($ScriptResult.execution_info.serial_number)" -Level "INFO" Write-Log "Ticket Number: $TicketNumber" -Level "INFO" Write-Log "Initial Restore Point Created: $($ScriptResult.restore_point_initial.created)" -Level "INFO" if ($ScriptResult.restore_point_initial.name) { Write-Log "Restore Point Name: $($ScriptResult.restore_point_initial.name)" -Level "INFO" } Write-Log "BleachBit Cleaners Run: $($ScriptResult.bleachbit_cleanup.cleaners_run)" -Level "INFO" Write-Log "Webhook Sent: $(if ($ScriptResult.webhook.sent) { 'Yes' } else { 'No' })" -Level "INFO" if (-not $ScriptResult.webhook.sent -and $ScriptResult.webhook.skipped_reason) { Write-Log "Webhook Skipped Reason: $($ScriptResult.webhook.skipped_reason)" -Level "INFO" } Write-Log "Log File: $LogFile" -Level "INFO" Write-Log "================================================================================" -Level "INFO" Write-Log "System Cleanup Script Completed" -Level "INFO" Write-Log "================================================================================" -Level "INFO" "" | Out-File -FilePath $LogFile -Append -Encoding UTF8 # Display completion message Write-Host "" Write-Host "===========================================" -ForegroundColor Green Write-Host "Script completed successfully!" -ForegroundColor Green Write-Host "Duration: $($ScriptResult.summary.total_duration_seconds) seconds" -ForegroundColor Green Write-Host "Status: $($ScriptResult.summary.overall_status)" -ForegroundColor Green Write-Host "Serial Number: $($ScriptResult.execution_info.serial_number)" -ForegroundColor Cyan Write-Host "Ticket Number: $TicketNumber" -ForegroundColor Cyan Write-Host "Initial Restore Point: $($ScriptResult.restore_point_initial.name)" -ForegroundColor Cyan Write-Host "BleachBit Cleaners: $($ScriptResult.bleachbit_cleanup.cleaners_run)" -ForegroundColor Cyan Write-Host "Errors: $($ScriptResult.summary.total_errors)" -ForegroundColor $(if ($ScriptResult.summary.total_errors -gt 0) { "Red" } else { "Green" }) Write-Host "Warnings: $($ScriptResult.summary.total_warnings)" -ForegroundColor $(if ($ScriptResult.summary.total_warnings -gt 0) { "Yellow" } else { "Green" }) if ($ScriptResult.app_updates.timed_out) { Write-Host "Winget timed out after $WingetTimeout seconds" -ForegroundColor Yellow } if (-not $SendWebhook) { Write-Host "Webhook: NOT SENT (no ticket number)" -ForegroundColor Yellow } elseif ($ScriptResult.webhook.sent) { Write-Host "Webhook: SENT successfully (Status: $($ScriptResult.webhook.status_code))" -ForegroundColor Green } else { Write-Host "Webhook: FAILED to send" -ForegroundColor Red } Write-Host "Log file: $LogFile" -ForegroundColor Cyan Write-Host "===========================================" -ForegroundColor Green