<# NAME : Install_Security_Updates_OSD.ps1 PURPOSE : Install security-only Windows software updates + MSRT MODE : MDT-ready and standalone-safe AUTHOR : Sakthivel S Behavior: - Searches only non-installed, non-hidden, non-optional SOFTWARE updates - Post-filters to Security category updates + explicit MSRT - Downloads and installs one update at a time - Returns 3010 if reboot is required - Returns 0 only when compliant / successful - Returns 1 on failure #> # -------------------- Configuration -------------------- $UseProxy = $true $ProxyServer = ':8080' $ProxyBypass = '' $LogPath = 'C:\Temp\Windows_Update' $LogFile = Join-Path $LogPath 'WindowsUpdate.log' $PendingFlag = Join-Path $LogPath 'pending.flag' $CompliantFlag = Join-Path $LogPath 'compliant.flag' $FailureFlag = Join-Path $LogPath 'failure.flag' # Keep false by default. Resetting SoftwareDistribution/catroot2 on every clean run # is often unnecessary in OSD and can make troubleshooting noisier. $ResetCacheOnSuccess = $false # -------------------- Bootstrap -------------------- if (-not (Test-Path $LogPath)) { New-Item -Path $LogPath -ItemType Directory -Force | Out-Null } function Write-Log { param([string]$Message) $timestamp = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss') $line = "$timestamp : $Message" $line | Out-File -FilePath $LogFile -Append -Encoding UTF8 Write-Host $line } function Show-ProgressSafe { param( [int]$Percent, [string]$Activity ) try { Write-Progress -Activity $Activity -PercentComplete $Percent } catch { # Can be ignored in non-interactive / TS contexts } } function Reset-ProxyToDirect { try { if ($UseProxy) { Write-Log 'Resetting WinHTTP proxy to DIRECT' & netsh winhttp reset proxy | Out-Null } } catch { Write-Log "Proxy reset error: $($_.Exception.Message)" } } function Invoke-CacheReset { try { Write-Log 'Starting Windows Update cache reset (SoftwareDistribution & catroot2)...' $services = @('wuauserv','bits','cryptsvc','msiserver') foreach ($svc in $services) { try { Stop-Service -Name $svc -Force -ErrorAction Stop } catch { Write-Log "Stop $svc : $($_.Exception.Message)" } } $sd = Join-Path $env:SystemRoot 'SoftwareDistribution' $cr = Join-Path $env:SystemRoot 'System32\catroot2' foreach ($path in @($sd,$cr)) { if (Test-Path $path) { $bak = "$path.bak_$(Get-Date -Format 'yyyyMMddHHmmss')" try { Rename-Item -Path $path -NewName $bak -ErrorAction Stop Write-Log "Renamed $path -> $bak" } catch { Write-Log "Rename failed for $path : $($_.Exception.Message)" } } } foreach ($svc in $services) { try { Start-Service -Name $svc -ErrorAction Stop } catch { Write-Log "Start $svc : $($_.Exception.Message)" } } try { Write-Log 'Triggering USOClient StartScan...' Start-Process -FilePath "$env:SystemRoot\System32\usoclient.exe" ` -ArgumentList 'StartScan' ` -WindowStyle Hidden } catch { Write-Log "USOClient StartScan error: $($_.Exception.Message)" } } catch { Write-Log "Cache reset error: $($_.Exception.Message)" } } function Get-SecurityAndMSRTCandidates { param([Parameter(Mandatory = $true)]$SearchResult) $Candidates = @() foreach ($Update in $SearchResult.Updates) { if ($Update.IsHidden) { continue } # Security classification by category name $isSecurity = ($Update.Categories | Where-Object { $_.Name -like '*Security*' }) -ne $null # Explicit MSRT inclusion $kbMatch = $Update.KBArticleIDs -contains '890830' $titleMatch = $Update.Title -like '*Malicious Software Removal Tool*' $isMSRT = ($kbMatch -or $titleMatch) if ($isSecurity -or $isMSRT) { $Candidates += $Update } } return $Candidates } function Exit-Clean { param([int]$Code) Reset-ProxyToDirect exit $Code } Write-Host '============================================================' -ForegroundColor Cyan Write-Host 'Starting Security-Only Updates (MDT-ready, standalone-safe)' -ForegroundColor Green Write-Host '============================================================' -ForegroundColor Cyan # -------------------- Clear failure marker from prior run -------------------- if (Test-Path $FailureFlag) { Remove-Item $FailureFlag -Force -ErrorAction SilentlyContinue } # -------------------- Proxy -------------------- try { if ($UseProxy) { Write-Log "Configuring WinHTTP proxy (SYSTEM) to $ProxyServer" $json = '{"Proxy":"' + $ProxyServer + '","ProxyBypass":"' + $ProxyBypass + '","AutoconfigUrl":"","AutoDetect":false}' & netsh winhttp set advproxy setting-scope=machine settings=$json | Out-Null } else { Write-Log 'Proxy: DIRECT' & netsh winhttp reset proxy | Out-Null } } catch { Write-Log "Proxy configuration error: $($_.Exception.Message)" } # -------------------- Initialize WUA COM objects -------------------- try { $UpdateSession = New-Object -ComObject Microsoft.Update.Session $UpdateSession.ClientApplicationID = 'OSD-SecurityUpdates' $Searcher = $UpdateSession.CreateUpdateSearcher() $Installer = $UpdateSession.CreateUpdateInstaller() # Windows Update service only, force online scan $Searcher.ServerSelection = 2 $Searcher.Online = $true } catch { Write-Log "Failed to initialize Windows Update COM objects: $($_.Exception.Message)" Set-Content -Path $FailureFlag -Value "init-failed $(Get-Date)" Exit-Clean -Code 1 } # -------------------- Load TS env if present -------------------- $tsenv = $null try { $tsenv = New-Object -ComObject Microsoft.SMS.TSEnvironment Write-Log 'Task Sequence environment detected' } catch { Write-Log 'Task Sequence environment not detected - standalone mode assumed' } # -------------------- Search -------------------- Show-ProgressSafe -Percent 10 -Activity "Searching for security/MSRT updates..." Write-Log "Searching for security/MSRT updates..." # Documented WUA search criteria: # - Type='Software' excludes driver updates # - BrowseOnly=0 excludes optional updates # - IsInstalled=0 and IsHidden=0 keep it applicable/non-hidden $Criteria = "IsInstalled=0 and IsHidden=0 and Type='Software' and BrowseOnly=0" try { $SearchResult = $Searcher.Search($Criteria) } catch { Write-Log "Search failed: $($_.Exception.Message)" Set-Content -Path $FailureFlag -Value "search-failed $(Get-Date)" Exit-Clean -Code 1 } $Candidates = Get-SecurityAndMSRTCandidates -SearchResult $SearchResult if ($Candidates.Count -eq 0) { Show-ProgressSafe -Percent 100 -Activity 'System is compliant (security/MSRT)' Write-Log 'No applicable security/MSRT updates found. Creating compliant.flag.' Set-Content -Path $CompliantFlag -Value "compliant $(Get-Date)" if (Test-Path $PendingFlag) { Remove-Item $PendingFlag -Force -ErrorAction SilentlyContinue } if ($ResetCacheOnSuccess) { Invoke-CacheReset } Write-Log 'Security/MSRT update installation completed successfully (no reboot required).' Exit-Clean -Code 0 } $OrderedUpdates = $Candidates | Sort-Object Title # -------------------- Tracking -------------------- $FailedUpdates = @() $InstalledUpdates = @() # -------------------- Per-update download/install -------------------- foreach ($u in $OrderedUpdates) { Show-ProgressSafe -Percent 25 -Activity "Preparing: $($u.Title)" Write-Log ("Candidate: {0} | KBs: {1}" -f $u.Title, ($u.KBArticleIDs -join ',')) if (-not $u.EulaAccepted) { try { $u.AcceptEula() } catch { Write-Log "EULA accept failed for $($u.Title): $($_.Exception.Message)" } } if (-not $u.IsDownloaded) { Write-Log "Downloading single update..." $dlColl = New-Object -ComObject Microsoft.Update.UpdateColl $null = $dlColl.Add($u) $Downloader = $UpdateSession.CreateUpdateDownloader() $Downloader.Updates = $dlColl try { $dlRes = $Downloader.Download() $uDR = $dlRes.GetUpdateResult(0) Write-Log ("Download: {0} | ResultCode={1} | HResult=0x{2:X8}" -f $u.Title, $uDR.ResultCode, $uDR.HResult) if ($uDR.ResultCode -eq 4) { Write-Log "Download failed; skipping install for: $($u.Title)" $FailedUpdates += $u.Title continue } } catch { Write-Log "Download exception for $($u.Title): $($_.Exception.Message)" $FailedUpdates += $u.Title continue } } else { Write-Log "Content already cached for: $($u.Title)" } Show-ProgressSafe -Percent 70 -Activity "Installing: $($u.Title)" $instColl = New-Object -ComObject Microsoft.Update.UpdateColl $null = $instColl.Add($u) $Installer.Updates = $instColl try { $iRes = $Installer.Install() $uIR = $iRes.GetUpdateResult(0) $rc = $uIR.ResultCode $hr = '0x{0:X8}' -f $uIR.HResult Write-Log ("Install: {0} | ResultCode={1} | HResult={2} | RebootRequired={3}" -f $u.Title, $rc, $hr, $iRes.RebootRequired) switch ($rc) { 2 { Write-Log "Installed successfully: $($u.Title)" $InstalledUpdates += $u.Title } 3 { Write-Log "Installed with errors: $($u.Title)" $FailedUpdates += $u.Title continue } 4 { Write-Log "Failed to install: $($u.Title)" $FailedUpdates += $u.Title continue } 5 { Write-Log "Install aborted: $($u.Title)" $FailedUpdates += $u.Title continue } default { Write-Log "Unexpected install state $rc for $($u.Title)" $FailedUpdates += $u.Title continue } } if ($iRes.RebootRequired) { Write-Log "Reboot required after installing: $($u.Title)" Set-Content -Path $PendingFlag -Value "reboot-required $(Get-Date)" if ($tsenv) { try { $tsenv.Value('SMSTSRebootRequested') = 'true' } catch { Write-Log "Unable to set SMSTSRebootRequested: $($_.Exception.Message)" } } Show-ProgressSafe -Percent 100 -Activity "Reboot required" Exit-Clean -Code 3010 } } catch { Write-Log "Install exception for $($u.Title): $($_.Exception.Message)" $FailedUpdates += $u.Title continue } } # -------------------- Final result -------------------- if ($FailedUpdates.Count -gt 0) { Write-Log ("One or more updates failed: {0}" -f ($FailedUpdates -join '; ')) if (Test-Path $CompliantFlag) { Remove-Item $CompliantFlag -Force -ErrorAction SilentlyContinue } Set-Content -Path $FailureFlag -Value "install-failed $(Get-Date)" Show-ProgressSafe -Percent 100 -Activity 'Updates failed' Exit-Clean -Code 1 } # Verification scan Write-Log 'Performing verification scan after install pass...' try { $VerifyResult = $Searcher.Search($Criteria) $RemainingCandidates = Get-SecurityAndMSRTCandidates -SearchResult $VerifyResult } catch { Write-Log "Verification scan failed: $($_.Exception.Message)" Set-Content -Path $FailureFlag -Value "verify-failed $(Get-Date)" Exit-Clean -Code 1 } if ($RemainingCandidates.Count -gt 0) { Write-Log ("Verification scan found remaining security/MSRT updates: {0}" -f $RemainingCandidates.Count) if (Test-Path $CompliantFlag) { Remove-Item $CompliantFlag -Force -ErrorAction SilentlyContinue } Set-Content -Path $FailureFlag -Value "verify-remaining $(Get-Date)" Exit-Clean -Code 1 } Write-Log 'Completed install pass with no reboot and no remaining updates; creating compliant.flag.' Set-Content -Path $CompliantFlag -Value "compliant $(Get-Date)" if (Test-Path $PendingFlag) { Remove-Item $PendingFlag -Force -ErrorAction SilentlyContinue } if ($ResetCacheOnSuccess) { Invoke-CacheReset } Write-Log 'Security/MSRT update installation completed successfully (no reboot required).' Show-ProgressSafe -Percent 100 -Activity 'Completed successfully' Exit-Clean -Code 0