· last year · Jul 30, 2024, 03:45 PM
1#Requires -modules SqlServer
2
3<#
4.SYNOPSIS
5 Comprehensive Windows Server Update Services (WSUS) configuration and optimization script.
6.DESCRIPTION
7 Comprehensive Windows Server Update Services (WSUS) configuration and optimization script.
8 Features:
9 -Deep cleaning search and removal of unnecessary updates and drives by product title and update title.
10 -IIS Configuration validation and optimization.
11 -Disable device driver syncronization and caching.
12 -WSUS integrated update and computer cleanup
13 -Microsoft best practice WSUS database optimization and re-indexing
14 -Creation of daily and weekly optimization scheduled tasks.
15
16.PARAMETER FirstRun
17 Presents a series of prompts for user to initiate all recommended first run optimization tasks. Additional parameters will be ignored, as they will be redundant.
18
19.PARAMETER DeclineSupersededUpdates
20Declines all updates that have been approved and are superseded by other updates. The update will only be declined if a superseding update has been approved.
21
22.PARAMETER DeepClean
23 Searches through most likely categories for unneeded updates and drivers to free up massive amounts of storage and improve database responsiveness. Prompts user to approve removal before deletion.
24
25.PARAMETER DisableDrivers
26 Disable device driver syncronization and caching.
27
28.PARAMETER CheckConfig
29 Validates current WSUS IIS configuration against recommended settings. Helps prevent frequent WSUS/IIS/SQL service crashes and the "RESET SERVER NODE" error.
30
31.PARAMETER OptimizeServer
32 Runs all of Microsoft's built-in WSUS cleanup processes.
33
34.PARAMETER OptimizeDatabase
35 Runs Microsoft's recommended SQL reindexing script.
36
37.PARAMETER InstallDailyTask
38 Creates a scheduled task to run the OptimizeServer function nightly.
39
40.PARAMETER InstallWeeklyTask
41 Creates a scheduled task to run the OptimizeDatabase function weekly.
42
43.NOTES
44 Version: 1.2.1
45 Author: Austin Warren
46 Creation Date: 2020/07/31
47
48.EXAMPLE
49 Optimize-WsusServer.ps1 -FirstRun
50 Optimize-WsusServer.ps1 -DeepClean
51 Optimize-WsusServer.ps1 -InstallDailyTask -CheckConfig -OptimizeServer
52#>
53
54
55[CmdletBinding()]
56param (
57 [Parameter()]
58 [switch]
59 $FirstRun,
60 [Parameter()]
61 [switch]
62 $DisableDrivers,
63 [Parameter()]
64 [switch]
65 $DeepClean,
66 [Parameter()]
67 [switch]
68 $CheckConfig,
69 [Parameter()]
70 [switch]
71 $InstallDailyTask,
72 [Parameter()]
73 [switch]
74 $InstallWeeklyTask,
75 [Parameter()]
76 [switch]
77 $OptimizeServer,
78 [Parameter()]
79 [switch]
80 $OptimizeDatabase,
81 [switch]
82 $DeclineSupersededUpdates
83)
84#----------------------------------------------------------[Declarations]----------------------------------------------------------
85
86# Recommended IIS settings: https://www.reddit.com/r/sysadmin/comments/996xul/getting_2016_updates_to_work_on_wsus/
87$recommendedIISSettings = @{
88 QueueLength = 25000
89 LoadBalancerCapabilities = 'TcpLevel'
90 CpuResetInterval = 15
91 RecyclingMemory = 0
92 RecyclingPrivateMemory = 0
93 ClientMaxRequestLength = 204800
94 ClientExecutionTimeout = 7200
95}
96
97<#
98DeepClean
99
100To find potentially unneeded updates:
101 1. WSUS management console
102 2. Updates > All Updates
103 3. Approval: Approved, Status: No Status
104 4. Look for unused products
105 5. Add titles to respective arrays below
106
107Get-WsusProduct - Lists all Microsoft WSUS product categories.
108#>
109
110# Common unneeded updates by ProductTitles
111$unneededUpdatesbyProductTitles = @(
112 "Security Only",
113 "Itanium",
114 "ia64",
115 "ARM64",
116 "Technical Preview",
117 "Insider Preview",
118 "Preview Of",
119 "Edge-Beta",
120 "Edge-Dev",
121 "Beta Channel",
122 "Prerelease",
123 "Preview",
124 "Windows 10 Version Next",
125 "Version 1507",
126 "Windows 10 Version 1511",
127 "Windows 10 Version 1607",
128 "Windows 10 Version 1703",
129 "Windows 10 Version 1709",
130 "Windows 10 Version 1803",
131 "Windows 10 Version 1809",
132 "Windows 10 Version 1903",
133 "Windows 10 Version 1909",
134 "ARM64-based",
135 "\(business editions\), version 1709",
136 "\(business editions\), version 1803",
137 "\(business editions\), version 1809",
138 "\(business editions\), version 1903",
139 "\(business editions\), version 1909",
140 "\(consumer editions\), version 1709",
141 "\(consumer editions\), version 1803",
142 "\(consumer editions\), version 1809",
143 "\(consumer editions\), version 1903",
144 "\(consumer editions\), version 1909",
145 "10 Education, version 1607",
146 "10 Education, version 1703",
147 "10 Education, version 1709",
148 "10 Education, version 1803",
149 "10 Education, version 1809",
150 "10 Education, version 1903",
151 "10 Education, version 1909",
152 "10 Education N, version 1607",
153 "10 Education N, version 1703",
154 "10 Education N, version 1709",
155 "10 Education N, version 1803",
156 "10 Education N, version 1809",
157 "10 Education N, version 1903",
158 "10 Education N, version 1909",
159 "10 Enterprise N, version 1607",
160 "10 Enterprise N, version 1703",
161 "10 Enterprise N, version 1709",
162 "10 Enterprise N, version 1803",
163 "10 Enterprise N, version 1809",
164 "10 Enterprise N, version 1903",
165 "10 Enterprise N, version 1909",
166 "10 Enterprise, version 1607",
167 "10 Enterprise, version 1703",
168 "10 Enterprise, version 1709",
169 "10 Enterprise, version 1803",
170 "10 Enterprise, version 1809",
171 "10 Enterprise, version 1903",
172 "10 Enterprise, version 1909",
173 "10 Pro, version 1607",
174 "10 Pro, version 1703",
175 "10 Pro, version 1709",
176 "10 Pro, version 1803",
177 "10 Pro, version 1809",
178 "10 Pro, version 1903",
179 "10 Pro, version 1909",
180 "10 Pro N, version 1607",
181 "10 Pro N, version 1703",
182 "10 Pro N, version 1709",
183 "10 Pro N, version 1803",
184 "10 Pro N, version 1809",
185 "10 Pro N, version 1903",
186 "10 Pro N, version 1909",
187 "10 Team, version 1607",
188 "10 Team, version 1703",
189 "10 Team, version 1709",
190 "10 Team, version 1803",
191 "10 Team, version 1809",
192 "10 Team, version 1903",
193 "10 Team, version 1909",
194 "Windows Server Next"
195)
196
197# Common unneeded updates by Title
198$unneededUpdatesbyTitle = @(
199 "Security Only",
200 "Itanium",
201 "ia64",
202 "ARM64",
203 "Technical Preview",
204 "Insider Preview",
205 "Preview Of",
206 "Edge-Beta",
207 "Edge-Dev",
208 "Beta Channel",
209 "Prerelease",
210 "Preview",
211 "Windows 10 Version Next",
212 "Version 1507",
213 "Windows 10 Version 1511",
214 "Windows 10 Version 1607",
215 "Windows 10 Version 1703",
216 "Windows 10 Version 1709",
217 "Windows 10 Version 1803",
218 "Windows 10 Version 1809",
219 "Windows 10 Version 1903",
220 "Windows 10 Version 1909",
221 "ARM64-based",
222 "\(business editions\), version 1709",
223 "\(business editions\), version 1803",
224 "\(business editions\), version 1809",
225 "\(business editions\), version 1903",
226 "\(business editions\), version 1909",
227 "\(consumer editions\), version 1709",
228 "\(consumer editions\), version 1803",
229 "\(consumer editions\), version 1809",
230 "\(consumer editions\), version 1903",
231 "\(consumer editions\), version 1909",
232 "10 Education, version 1607",
233 "10 Education, version 1703",
234 "10 Education, version 1709",
235 "10 Education, version 1803",
236 "10 Education, version 1809",
237 "10 Education, version 1903",
238 "10 Education, version 1909",
239 "10 Education N, version 1607",
240 "10 Education N, version 1703",
241 "10 Education N, version 1709",
242 "10 Education N, version 1803",
243 "10 Education N, version 1809",
244 "10 Education N, version 1903",
245 "10 Education N, version 1909",
246 "10 Enterprise N, version 1607",
247 "10 Enterprise N, version 1703",
248 "10 Enterprise N, version 1709",
249 "10 Enterprise N, version 1803",
250 "10 Enterprise N, version 1809",
251 "10 Enterprise N, version 1903",
252 "10 Enterprise N, version 1909",
253 "10 Enterprise, version 1607",
254 "10 Enterprise, version 1703",
255 "10 Enterprise, version 1709",
256 "10 Enterprise, version 1803",
257 "10 Enterprise, version 1809",
258 "10 Enterprise, version 1903",
259 "10 Enterprise, version 1909",
260 "10 Pro, version 1607",
261 "10 Pro, version 1703",
262 "10 Pro, version 1709",
263 "10 Pro, version 1803",
264 "10 Pro, version 1809",
265 "10 Pro, version 1903",
266 "10 Pro, version 1909",
267 "10 Pro N, version 1607",
268 "10 Pro N, version 1703",
269 "10 Pro N, version 1709",
270 "10 Pro N, version 1803",
271 "10 Pro N, version 1809",
272 "10 Pro N, version 1903",
273 "10 Pro N, version 1909",
274 "10 Team, version 1607",
275 "10 Team, version 1703",
276 "10 Team, version 1709",
277 "10 Team, version 1803",
278 "10 Team, version 1809",
279 "10 Team, version 1903",
280 "10 Team, version 1909",
281 "Windows Server Next"
282)
283
284<#
285REFERENCES
286 The complete guide to Microsoft WSUS and Configuration Manager SUP maintenance
287 https://support.microsoft.com/en-us/help/4490644/complete-guide-to-microsoft-wsus-and-configuration-manager-sup-maint
288
289 Invoke-WsusServerCleanup
290 https://docs.microsoft.com/en-us/powershell/module/wsus/Invoke-WsusServerCleanup?view=win10-ps
291
292 Reindex the WSUS Database
293 https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2008-R2-and-2008/dd939795(v=ws.10)
294
295 Invoke-Sqlcmd
296 https://docs.microsoft.com/en-us/powershell/module/sqlserver/invoke-sqlcmd?view=sqlserver-ps
297
298 How to Check if an Index Exists on a Table in SQL Server
299 https://littlekendra.com/2016/01/28/how-to-check-if-an-index-exists-on-a-table-in-sql-server/
300#>
301
302<#
303 "[U]sed to create custom indexes in the SUSDB database. This is a one-time process, which is optional but recommended, as doing so will greatly improve performance during subsequent cleanup operations."
304 Modified to check if indexes already exist before creating them.
305#>
306$createCustomIndexesSQLQuery = @"
307USE [SUSDB]
308IF 0 = (SELECT COUNT(*) as index_count
309 FROM sys.indexes
310 WHERE object_id = OBJECT_ID('[dbo].[tbLocalizedPropertyForRevision]')
311 AND name='nclLocalizedPropertyID')
312BEGIN
313-- Create custom index in tbLocalizedPropertyForRevision
314 CREATE NONCLUSTERED INDEX [nclLocalizedPropertyID] ON [dbo].[tbLocalizedPropertyForRevision]
315 (
316 [LocalizedPropertyID] ASC
317 )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
318END
319ELSE
320BEGIN
321 PRINT '[nclLocalizedPropertyID] ON [dbo].[tbLocalizedPropertyForRevision] already exists'
322END ;
323GO
324IF 0 = (SELECT COUNT(*) as index_count
325 FROM sys.indexes
326 WHERE object_id = OBJECT_ID('[dbo].[tbRevisionSupersedesUpdate]')
327 AND name='nclSupercededUpdateID')
328BEGIN
329-- Create custom index in tbRevisionSupersedesUpdate
330 CREATE NONCLUSTERED INDEX [nclSupercededUpdateID] ON [dbo].[tbRevisionSupersedesUpdate]
331 (
332 [SupersededUpdateID] ASC
333 )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY];
334END
335ELSE
336BEGIN
337 PRINT '[nclSupercededUpdateID] ON [dbo].[tbRevisionSupersedesUpdate] already exists'
338END ;
339GO
340"@
341
342<#
343 Microsoft recommended database maintenance script
344
345 "The performance of large Windows Server Update Services (WSUS) deployments will degrade over time if the WSUS database
346 is not maintained properly. The WSUSDBMaintenance script is a T-SQL script that can be run by SQL Server administrators
347 to re-index and defragment WSUS databases. It should not be used on WSUS 2.0 databases.This script contributed by the
348 Microsoft WSUS team."
349
350 Reference: https://support.microsoft.com/en-us/help/4490644/complete-guide-to-microsoft-wsus-and-configuration-manager-sup-maint
351#>
352$wsusDBMaintenanceSQLQuery = @"
353/******************************************************************************
354This sample T-SQL script performs basic maintenance tasks on SUSDB
3551. Identifies indexes that are fragmented and defragments them. For certain
356 tables, a fill-factor is set in order to improve insert performance.
357 Based on MSDN sample at http://msdn2.microsoft.com/en-us/library/ms188917.aspx
358 and tailored for SUSDB requirements
3592. Updates potentially out-of-date table statistics.
360******************************************************************************/
361
362USE SUSDB;
363GO
364SET NOCOUNT ON;
365
366-- Rebuild or reorganize indexes based on their fragmentation levels
367DECLARE @work_to_do TABLE (
368 objectid int
369 , indexid int
370 , pagedensity float
371 , fragmentation float
372 , numrows int
373)
374
375DECLARE @objectid int;
376DECLARE @indexid int;
377DECLARE @schemaname nvarchar(130);
378DECLARE @objectname nvarchar(130);
379DECLARE @indexname nvarchar(130);
380DECLARE @numrows int
381DECLARE @density float;
382DECLARE @fragmentation float;
383DECLARE @command nvarchar(4000);
384DECLARE @fillfactorset bit
385DECLARE @numpages int
386
387-- Select indexes that need to be defragmented based on the following
388-- * Page density is low
389-- * External fragmentation is high in relation to index size
390PRINT 'Estimating fragmentation: Begin. ' + convert(nvarchar, getdate(), 121)
391INSERT @work_to_do
392SELECT
393 f.object_id
394 , index_id
395 , avg_page_space_used_in_percent
396 , avg_fragmentation_in_percent
397 , record_count
398FROM
399 sys.dm_db_index_physical_stats (DB_ID(), NULL, NULL , NULL, 'SAMPLED') AS f
400WHERE
401 (f.avg_page_space_used_in_percent < 85.0 and f.avg_page_space_used_in_percent/100.0 * page_count < page_count - 1)
402 or (f.page_count > 50 and f.avg_fragmentation_in_percent > 15.0)
403 or (f.page_count > 10 and f.avg_fragmentation_in_percent > 80.0)
404
405PRINT 'Number of indexes to rebuild: ' + cast(@@ROWCOUNT as nvarchar(20))
406
407PRINT 'Estimating fragmentation: End. ' + convert(nvarchar, getdate(), 121)
408
409SELECT @numpages = sum(ps.used_page_count)
410FROM
411 @work_to_do AS fi
412 INNER JOIN sys.indexes AS i ON fi.objectid = i.object_id and fi.indexid = i.index_id
413 INNER JOIN sys.dm_db_partition_stats AS ps on i.object_id = ps.object_id and i.index_id = ps.index_id
414
415-- Declare the cursor for the list of indexes to be processed.
416DECLARE curIndexes CURSOR FOR SELECT * FROM @work_to_do
417
418-- Open the cursor.
419OPEN curIndexes
420
421-- Loop through the indexes
422WHILE (1=1)
423BEGIN
424 FETCH NEXT FROM curIndexes
425 INTO @objectid, @indexid, @density, @fragmentation, @numrows;
426 IF @@FETCH_STATUS < 0 BREAK;
427
428 SELECT
429 @objectname = QUOTENAME(o.name)
430 , @schemaname = QUOTENAME(s.name)
431 FROM
432 sys.objects AS o
433 INNER JOIN sys.schemas as s ON s.schema_id = o.schema_id
434 WHERE
435 o.object_id = @objectid;
436
437 SELECT
438 @indexname = QUOTENAME(name)
439 , @fillfactorset = CASE fill_factor WHEN 0 THEN 0 ELSE 1 END
440 FROM
441 sys.indexes
442 WHERE
443 object_id = @objectid AND index_id = @indexid;
444
445 IF ((@density BETWEEN 75.0 AND 85.0) AND @fillfactorset = 1) OR (@fragmentation < 30.0)
446 SET @command = N'ALTER INDEX ' + @indexname + N' ON ' + @schemaname + N'.' + @objectname + N' REORGANIZE';
447 ELSE IF @numrows >= 5000 AND @fillfactorset = 0
448 SET @command = N'ALTER INDEX ' + @indexname + N' ON ' + @schemaname + N'.' + @objectname + N' REBUILD WITH (FILLFACTOR = 90)';
449 ELSE
450 SET @command = N'ALTER INDEX ' + @indexname + N' ON ' + @schemaname + N'.' + @objectname + N' REBUILD';
451 PRINT convert(nvarchar, getdate(), 121) + N' Executing: ' + @command;
452 EXEC (@command);
453 PRINT convert(nvarchar, getdate(), 121) + N' Done.';
454END
455
456-- Close and deallocate the cursor.
457CLOSE curIndexes;
458DEALLOCATE curIndexes;
459
460IF EXISTS (SELECT * FROM @work_to_do)
461BEGIN
462 PRINT 'Estimated number of pages in fragmented indexes: ' + cast(@numpages as nvarchar(20))
463 SELECT @numpages = @numpages - sum(ps.used_page_count)
464 FROM
465 @work_to_do AS fi
466 INNER JOIN sys.indexes AS i ON fi.objectid = i.object_id and fi.indexid = i.index_id
467 INNER JOIN sys.dm_db_partition_stats AS ps on i.object_id = ps.object_id and i.index_id = ps.index_id
468
469 PRINT 'Estimated number of pages freed: ' + cast(@numpages as nvarchar(20))
470END
471GO
472
473--Update all statistics
474PRINT 'Updating all statistics.' + convert(nvarchar, getdate(), 121)
475EXEC sp_updatestats
476PRINT 'Done updating statistics.' + convert(nvarchar, getdate(), 121)
477GO
478"@
479
480#-----------------------------------------------------------[Functions]------------------------------------------------------------
481
482function Confirm-Prompt ($prompt) {
483 <#
484 .SYNOPSIS
485 Y/N confirmation prompt.
486
487 .DESCRIPTION
488 Displays Y/N confirmation prompt and returns true or false.
489
490 .PARAMETER prompt
491 String displayed as prompt
492
493 .EXAMPLE
494 Confirm-Prompt "Is this a question?"
495 #>
496 Write-Host "$prompt Y/N: " -BackgroundColor Blue -ForegroundColor White -NoNewline
497 $confirm = Read-Host
498
499 if ($confirm.ToLower() -eq 'y') {
500 return $true
501 }
502 else {
503 return $false
504 }
505}
506
507function Optimize-WsusUpdates {
508 <#
509 .SYNOPSIS
510 Runs all built-in WSUS cleanup processes.
511
512 .DESCRIPTION
513 Runs all built-in WSUS cleanup processes.
514
515 .LINK
516 https://docs.microsoft.com/en-us/powershell/scripting/developer/help/examples-of-comment-based-help?view=powershell-7
517 #>
518
519 Write-Host "Deleting obsolete computers from WSUS database"
520 Invoke-WsusServerCleanup -CleanupObsoleteComputers
521
522 Write-Host "Deleting obsolete updates"
523 Invoke-WsusServerCleanup -CleanupObsoleteUpdates
524
525 Write-Host "Deleting unneeded content files"
526 Invoke-WsusServerCleanup -CleanupUnneededContentFiles
527
528 Write-Host "Deleting obsolete update revisions"
529 Invoke-WsusServerCleanup -CompressUpdates
530
531 Write-Host "Declining expired updates"
532 Invoke-WsusServerCleanup -DeclineExpiredUpdates
533
534 Write-Host "Declining superceded updates"
535 Invoke-WsusServerCleanup -DeclineSupersededUpdates
536
537 Write-Host "Declining additional superceded updates"
538 Decline-SupersededUpdates $TRUE
539}
540
541function Optimize-WsusDatabase {
542 <#
543 .SYNOPSIS
544 Runs WSUS database optimization.
545
546 .DESCRIPTION
547 Runs Microsoft's recommended WSUS database optimization.
548
549 .LINK
550 https://support.microsoft.com/en-us/help/4490644/complete-guide-to-microsoft-wsus-and-configuration-manager-sup-maint
551
552 .LINK
553 https://devblogs.microsoft.com/scripting/10-tips-for-the-sql-server-powershell-scripter/
554 #>
555
556 # Check registry for WSUS database install type (SQL or WID)
557 $wsusSqlServerName = (get-itemproperty "HKLM:\Software\Microsoft\Update Services\Server\Setup" -Name "SqlServername").SqlServername
558
559 # Set the named pipe to use based on WSUS db type
560 switch -Regex ($wsusSqlServerName) {
561 'SQLEXPRESS' { $serverInstance = 'np:\\.\pipe\MSSQL$SQLEXPRESS\sql\query'; break }
562 '##WID' { $serverInstance = 'np:\\.\pipe\MICROSOFT##WID\tsql\query'; break }
563 '##SSEE' { $serverInstance = 'np:\\.\pipe\MSSQL$MICROSOFT##SSEE\sql\query'; break }
564 default { $serverInstance = $wsusSqlServerName }
565 }
566
567 # Setting query timeout value because both of these scripts are prone to timeout
568 # https://devblogs.microsoft.com/scripting/10-tips-for-the-sql-server-powershell-scripter/
569
570 Write-Host "Creating custom indexes in WSUS index if they don't already exist. This will speed up future database optimizations."
571 #Create custom indexes in the database if they don't already exist
572 Invoke-Sqlcmd -query $createCustomIndexesSQLQuery -ServerInstance $serverInstance -QueryTimeout 120 -EncryptConnection:$false
573
574 Write-Host "Running WSUS SQL database maintenence script. This can take an extremely long time on the first run."
575 #Run the WSUS SQL database maintenance script
576 Invoke-Sqlcmd -query $wsusDBMaintenanceSQLQuery -ServerInstance $serverInstance -QueryTimeout 40000 -EncryptConnection:$false
577}
578
579function New-WsusMaintainenceTask($interval) {
580 <#
581 .SYNOPSIS
582 Creates a new WSUS optimization scheduled tasks.
583
584 .DESCRIPTION
585 Creates or overwrites daily or weekly scheduled tasks for WSUS update and database optimization.
586
587 .PARAMETER interval
588 Specifies "Daily" or "Weekly" tasks
589
590 .LINK
591 https://docs.microsoft.com/en-us/powershell/module/scheduledtasks/?view=win10-ps
592 #>
593
594 $taskName = "Optimize WSUS Server ($interval)"
595 $scriptPath = 'C:\Scripts'
596
597 # Delete scheduled task with the same name if it already exists
598 If (Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue) {
599 Unregister-ScheduledTask -TaskName $taskName -Confirm:$false
600 Write-Host "Unregistered Schedule Task: $taskName"
601 }
602
603 # Change scheduled action based on Daily or Weekly
604 switch ($interval) {
605 'Daily' {
606 $trigger = New-ScheduledTaskTrigger -Daily -At "12pm"
607 $scriptAction = "-OptimizeServer"
608 Break
609 }
610 'Weekly' {
611 $trigger = New-ScheduledTaskTrigger -Weekly -At "2am" -DaysOfWeek Sunday
612 $scriptAction = "-OptimizeDatabase"
613 Break
614 }
615 Default {}
616 }
617
618 $scriptName = Split-Path $MyInvocation.PSCommandPath -Leaf
619
620 #Create "C:\Scripts" to store PS script
621 $null = New-Item -Path "$scriptPath" -ItemType Directory -Force
622 Write-Host "Created Directory: $scriptPath"
623
624 # Copy current script to script
625 Copy-Item -Path $PSCommandPath -Destination $scriptPath -Force
626 Write-Host "Copied Script: $scriptName"
627
628 # Create and register the scheduled task
629 $task = New-ScheduledTaskAction `
630 -Execute "powershell.exe" `
631 -Argument "-Command `"&'$($scriptPath)`\$($scriptName)'$scriptAction`""
632
633 $settings = New-ScheduledTaskSettingsSet
634 $principal = New-ScheduledTaskPrincipal `
635 -UserId "NT AUTHORITY\SYSTEM" `
636 -LogonType ServiceAccount `
637 -RunLevel Highest
638
639 # Sending to $null to supress output
640 $null = Register-ScheduledTask $taskName -Action $task -Trigger $trigger -Settings $settings -Principal $principal
641
642 Write-Host "Registered Scheduled Task: $taskName"
643}
644
645function Get-WsusIISConfig {
646 <#
647 .SYNOPSIS
648 Returns a hash of all WSUS optimization related IIS settings.
649
650 .DESCRIPTION
651 Determines WSUS IIS Site and Pool, and then forms hash of all relevant optimization settings.
652
653 .LINK
654 https://docs.microsoft.com/en-us/powershell/module/webadminstration/get-webapplication?view=winserver2012-ps
655
656 .LINK
657 https://docs.microsoft.com/en-us/powershell/module/iisadministration/get-iissite?view=win10-ps
658 #>
659
660 # Get WSUS IIS Index from registry
661 $iisSiteIndex = Get-ItemPropertyValue "HKLM:\Software\Microsoft\Update Services\Server\Setup" -Name "IISTargetWebSiteIndex"
662
663 # IIS Site
664 $iisSiteName = Get-IISSite | Where-Object -Property "Id" -Eq $iisSiteIndex | Select-Object -ExpandProperty "Name"
665
666 # Site Application Pool
667 $iisAppPool = Get-WebApplication -site $iisSiteName -Name "ClientWebService" | Select-Object -ExpandProperty "applicationPool"
668
669 # Application Pool Config
670 $iisApplicationPoolConfig = Get-IISConfigCollection -ConfigElement (Get-IISConfigSection -SectionPath "system.applicationHost/applicationPools")
671
672 # WSUS Pool Config Root
673 $wsusPoolConfig = Get-IISConfigCollectionElement -ConfigCollection $iisApplicationPoolConfig -ConfigAttribute @{"name" = "$iisAppPool" }
674
675 # Queue Length
676 $queueLength = Get-IISConfigAttributeValue -ConfigElement $wsusPoolConfig -AttributeName "queueLength"
677
678 #Load Balancer Capabilities
679 $wsusPoolFailureConfig = Get-IISConfigElement -ConfigElement $wsusPoolConfig -ChildElementName "failure"
680 $loadBalancerCapabilities = Get-IISConfigAttributeValue -ConfigElement $wsusPoolFailureConfig -AttributeName "loadBalancerCapabilities"
681
682 # CPU Reset Interval
683 $wsusPoolCpuConfig = Get-IISConfigElement -ConfigElement $wsusPoolConfig -ChildElementName "cpu"
684 $cpuResetInterval = (Get-IISConfigAttributeValue -ConfigElement $wsusPoolCpuConfig -AttributeName "resetInterval").TotalMinutes
685
686 # Recycling Config Root
687 $wsusPoolRecyclingConfig = Get-IISConfigElement -ConfigElement $wsusPoolConfig -ChildElementName "recycling" | Get-IISConfigElement -ChildElementName "periodicRestart"
688
689 $recyclingMemory = Get-IISConfigAttributeValue -ConfigElement $wsusPoolRecyclingConfig -AttributeName "memory"
690 $recyclingPrivateMemory = Get-IISConfigAttributeValue -ConfigElement $wsusPoolRecyclingConfig -AttributeName "privateMemory"
691
692 $clientWebServiceConfig = Get-WebConfiguration -PSPath $iisPath -Filter "system.web/httpRuntime"
693
694 $clientMaxRequestLength = $clientWebServiceConfig | select-object -ExpandProperty maxRequestLength
695 $clientExecutionTimeout = ($clientWebServiceConfig | select-object -ExpandProperty executionTimeout).TotalSeconds
696
697 # Return hash of IIS settings
698 @{
699 QueueLength = $queueLength
700 LoadBalancerCapabilities = $loadBalancerCapabilities
701 CpuResetInterval = $cpuResetInterval
702 RecyclingMemory = $recyclingMemory
703 RecyclingPrivateMemory = $recyclingPrivateMemory
704 ClientMaxRequestLength = $clientMaxRequestLength
705 ClientExecutionTimeout = $clientExecutionTimeout
706 }
707}
708
709function Get-WsusIISLocalizedNamespacePath {
710 # Get localized WSUS IIS web site path: https://docs.microsoft.com/fr-fr/security-updates/windowsupdateservices/18127277 - Document is in English but posted in the French docs
711 $iisSitePhysicalPath = Get-ItemPropertyValue -Path 'HKLM:\SOFTWARE\Microsoft\Update Services\Server\Setup\' -Name "TargetDir"
712 $iisLocalizedString = Get-Website | Where-Object { $($_.PhysicalPath).StartsWith($iisSitePhysicalPath) } | Select-Object -ExpandProperty Name
713 $iisLocalizedNamespacePath = "IIS:\Sites\$iisLocalizedString\ClientWebService"
714 return $iisLocalizedNamespacePath
715}
716
717function Test-WsusIISConfig ($settings, $recommended) {
718 <#
719 .SYNOPSIS
720 Compares current WSUS IIS settings to recommended values.
721
722 .DESCRIPTION
723 Compares current WSUS IIS settings to recommended values. Prompts user to commit changes.
724
725 .PARAMETER settings
726 Hash of current WSUS IIS settings.
727
728 .PARAMETER recommended
729 Hash of recommended WSUS IIS settings.
730 #>
731
732 # Delay IIS configuration commits until we're done updating all necessary settings
733 Start-IISCommitDelay
734
735 foreach ($key in $recommended.Keys) {
736 # If the current configuration setting doesn't match the recommended value, prompt the user to update
737 # This could be better designed to match minimum requirements instead of specific values, but it isn't.
738 If ($recommended[$key] -ne $settings[$key]) {
739 Write-Host "$key`n`tCurrent:`t$($settings[$key])`n`tRecommended:`t$($recommended[$key])" -BackgroundColor Black -ForegroundColor Red
740
741 if (Confirm-Prompt "Update $key to recommended value?") {
742 Update-WsusIISConfig $key $recommended[$key]
743 }
744 }
745 else {
746 Write-Host "$key`n`tCurrent:`t$($settings[$key])`n`tRecommended:`t$($recommended[$key])" -BackgroundColor Black -ForegroundColor Green
747 }
748 }
749
750 # Allow IIS config commits again
751 Stop-IISCommitDelay
752}
753
754function Update-WsusIISConfig ($settingKey, $recommendedValue) {
755 <#
756 .SYNOPSIS
757 Modifies IIS configuration for specified setting.
758
759 .DESCRIPTION
760 Modifies specified IIS setting for WSUS IIS Site/App Pool optimization.
761
762 .PARAMETER settingKey
763 String used to reference specific IIS configuration setting.
764
765 .PARAMETER recommendedValue
766 Recommended value for WSUS IIS configuration setting.
767 #>
768
769 # WSUS IIS Index
770 $iisSiteIndex = Get-ItemPropertyValue "HKLM:\Software\Microsoft\Update Services\Server\Setup" -Name "IISTargetWebSiteIndex"
771
772 # IIS Site
773 $iisSiteName = Get-IISSite | Where-Object -Property "Id" -Eq $iisSiteIndex | Select-Object -ExpandProperty "Name"
774
775 # Site Application Pool
776 $iisAppPool = Get-WebApplication -site $iisSiteName -Name "ClientWebService" | Select-Object -ExpandProperty "applicationPool"
777
778 # Application Pool Config
779 $iisApplicationPoolConfig = Get-IISConfigCollection -ConfigElement (Get-IISConfigSection -SectionPath "system.applicationHost/applicationPools")
780
781 # WSUS Pool Config Root
782 $wsusPoolConfig = Get-IISConfigCollectionElement -ConfigCollection $iisApplicationPoolConfig -ConfigAttribute @{"name" = "$iisAppPool" }
783
784 # Recycling Config Root
785 $wsusPoolRecyclingConfig = Get-IISConfigElement -ConfigElement $wsusPoolConfig -ChildElementName "recycling" | Get-IISConfigElement -ChildElementName "periodicRestart"
786
787 switch ($settingKey) {
788 'QueueLength' {
789 # Queue Length
790 Set-IISConfigAttributeValue -ConfigElement $wsusPoolConfig -AttributeName "queueLength" -AttributeValue $recommendedValue
791 Break
792 }
793 'LoadBalancerCapabilities' {
794 # Failure Config Root
795 $wsusPoolFailureConfig = Get-IISConfigElement -ConfigElement $wsusPoolConfig -ChildElementName "failure"
796
797 # Load Balancer Capabilities
798 Set-IISConfigAttributeValue -ConfigElement $wsusPoolFailureConfig -AttributeName "loadBalancerCapabilities" -AttributeValue $recommendedValue
799 Break
800 }
801 'CpuResetInterval' {
802 # CPU Reset Interval
803 $wsusPoolCpuConfig = Get-IISConfigElement -ConfigElement $wsusPoolConfig -ChildElementName "cpu"
804 Set-IISConfigAttributeValue -ConfigElement $wsusPoolCpuConfig -AttributeName "resetInterval" -AttributeValue ([timespan]::FromMinutes($recommendedValue))
805 Break
806 }
807 'RecyclingMemory' {
808 Set-IISConfigAttributeValue -ConfigElement $wsusPoolRecyclingConfig -AttributeName "memory" -AttributeValue $recommendedValue
809 Break
810 }
811 'RecyclingPrivateMemory' {
812 Set-IISConfigAttributeValue -ConfigElement $wsusPoolRecyclingConfig -AttributeName "privateMemory" -AttributeValue $recommendedValue
813 Break
814 }
815 'ClientMaxRequestLength' {
816 # Check if the IIS WSUS Client Web Service web.config is read only and make it RW if so
817 Unblock-WebConfigAcl
818 Set-WebConfigurationProperty -PSPath $iisPath -Filter "system.web/httpRuntime" -Name "maxRequestLength" -Value $recommendedValue
819 Break
820 }
821 'ClientExecutionTimeout' {
822 # Check if the IIS WSUS Client Web Service web.config is read only and make it RW if so
823 Unblock-WebConfigAcl
824 Set-WebConfigurationProperty -PSPath $iisPath -Filter "system.web/httpRuntime" -Name "executionTimeout" -Value ([timespan]::FromSeconds($recommendedValue))
825 Break
826 }
827 Default {}
828 }
829
830 Write-Host "Updated IIS Setting: $settingKey, $recommendedValue" -BackgroundColor Green -ForegroundColor Black
831}
832
833function Remove-Updates ($searchStrings, $updateProp, $force = $false) {
834 [reflection.assembly]::LoadWithPartialName("Microsoft.UpdateServices.Administration") | Out-Null
835 $wsusServer = [Microsoft.UpdateServices.Administration.AdminProxy]::GetUpdateServer();
836 $scope = New-Object Microsoft.UpdateServices.Administration.UpdateScope
837 $updates = $wsusServer.GetUpdates($scope)
838 $declinedCount = 0
839 $searchCount = 0
840 $userMsg = 'Found'
841 $color = 'Yellow'
842
843 if ($force) {
844 $userMsg = 'Declined'
845 $color = 'DarkGreen'
846 }
847
848 Write-Host "Update Property: $updateProp"
849
850 foreach ($searchString in $searchStrings) {
851 $confirm = $false
852 Write-Host " - Update Search: $searchString"
853 $searchCount = 0
854 foreach ($update in $updates) {
855 if ($update.$($updateProp) -match "$searchString") {
856 if ($update.IsDeclined -eq $false) {
857
858 if ($force) {
859 $update.Decline()
860 }
861 $searchCount = $searchCount + 1
862 Write-Host " [*]$($userMsg): $($update.Title), $($update.ProductTitles) ($searchString)" -ForegroundColor $color
863 }
864 }
865 }
866
867 if ($searchCount -gt 0) {
868 Write-Host "$searchCount `"$searchString`" Updates $userMsg!" -ForegroundColor "Blue" -BackgroundColor White
869 }
870 else {
871 Write-Host " $searchCount `"$searchString`" Updates $userMsg" -ForegroundColor "White"
872 }
873
874 #Prompt user to confirm declining updates. Do no prompt if force flag is enable to prevent loop
875 if ((-not $force) -and ($searchCount -ne 0)) {
876 $confirm = Confirm-Prompt "Are you sure you want to decline all ($searchCount) listed ($searchString) updates?"
877
878 if ($confirm) {
879 Remove-Updates @($searchString) $updateProp $true | out-null
880 }
881 }
882
883 if (($confirm) -or $force) {
884 $declinedCount = ($declinedCount + $searchCount)
885 }
886 }
887
888 return $declinedCount
889}
890
891function Invoke-DeepClean ($titles, $productTitles) {
892 <#
893 .SYNOPSIS
894 Checks for unneeded WSUS updates to be deleted.
895
896 .DESCRIPTION
897 Checks for unneeded WSUS updates by product category to be deleted.
898
899 .PARAMETER titles
900 Array of titles of WSUS titles to search and prompt for removal
901
902 .PARAMETER productTitles
903 Array of WSUS product titles to search and prompt for removal
904
905 .EXAMPLE
906 DeepClean $titles $products
907
908 .NOTES
909 WSUS GetUpdates Method
910 https://docs.microsoft.com/en-us/previous-versions/windows/desktop/aa350127(v=vs.85)
911
912 WSUS IUpdate Properties
913 https://docs.microsoft.com/en-us/previous-versions/windows/desktop/ms752741(v=vs.85)
914
915 WSUS Product List
916 Get-WsusProduct
917 https://docs.microsoft.com/en-us/powershell/module/wsus/get-wsusproduct?view=win10-ps
918
919 WSUS Classification List
920 Get-WsusClassification
921 https://docs.microsoft.com/en-us/powershell/module/wsus/get-wsusclassification?view=win10-ps
922 #>
923
924 $declinedTotal = 0
925
926 Write-Host "Make certain to carefully read the listed updates before choosing to remove them!" -BackgroundColor White -ForegroundColor Green
927
928 #Remove updates by Title
929 Write-Host "Searching for unneeded updates by Title. This process can take a long time. Please wait." -BackgroundColor White -ForegroundColor Blue
930 $declinedTotal += Remove-Updates $titles 'Title'
931
932 #Remove updates by ProductTitles
933 Write-Host "Searching for unneeded updates by ProductTitle. This process can take a long time. Please wait." -BackgroundColor White -ForegroundColor Blue
934 $declinedTotal += Remove-Updates $productTitles 'ProductTitles'
935
936 #Remove drivers
937 Write-Host "Searching for drivers to be removed from WSUS. This process can take a long time. Please wait." -BackgroundColor White -ForegroundColor Blue
938 $declinedTotal += Remove-Updates @('Drivers') 'UpdateClassificationTitle'
939
940 Write-Host "Searching for unneeded updates superseded by newer updates. This process can take a long time. Please wait." -BackgroundColor White -ForegroundColor Blue
941 $declinedTotal += Decline-SupersededUpdates
942
943 Write-Host "================DEEPCLEAN COMPLETE==================" -BackgroundColor White -ForegroundColor Blue
944 Write-Host "$declinedTotal Total Updates Declined" -BackgroundColor White -ForegroundColor Blue
945}
946
947function Disable-WsusDriverSync {
948 <#
949 .SYNOPSIS
950 Disable WSUS device driver syncronization and caching.
951
952 .DESCRIPTION
953 Disable WSUS device driver syncronization and caching. Automatic driver sychronization is one of the primary causes of WSUS slowness, crashing, and wasted storage space.
954
955 .LINK
956 https://docs.microsoft.com/en-us/powershell/module/updateservices/set-wsusclassification?view=win10-ps
957 #>
958
959 Get-WsusClassification | Where-Object -FilterScript { $_.Classification.Title -Eq "Drivers" } | Set-WsusClassification -Disable
960 Get-WsusClassification | Where-Object -FilterScript { $_.Classification.Title -Eq "Driver Sets" } | Set-WsusClassification -Disable
961}
962
963
964function Unblock-WebConfigAcl {
965 <#
966 .SYNOPSIS
967 Grants local admins access to web.config
968
969 .DESCRIPTION
970 Grants BUILTIN\Administrators ownership and read write access to ClientWebService web.config. Also removes Read Only flag.
971
972 .LINK
973 https://devblogs.microsoft.com/scripting/use-powershell-to-translate-a-users-sid-to-an-active-directory-account-name/
974 https://docs.microsoft.com/en-us/dotnet/api/system.security.principal.securityidentifier.-ctor?view=windowsdesktop-5.0#System_Security_Principal_SecurityIdentifier__ctor_System_String_
975 #>
976
977 $wsusWebConfigPath = Get-WebConfigFile -PSPath $iisPath | Select-Object -ExpandProperty 'FullName'
978
979 # Get localized BUILTIN\Administrators group
980 $builtinAdminGroup = ([System.Security.Principal.SecurityIdentifier]'S-1-5-32-544').Translate([System.Security.Principal.NTAccount]).Value
981
982 Set-FileAclOwner $wsusWebConfigPath $builtinAdminGroup
983 Set-FileAclPermissions $wsusWebConfigPath $builtinAdminGroup 'FullControl' 'None' 'None' 'Allow'
984 Set-ItemProperty -Path $wsusWebConfigPath -Name IsReadOnly -Value $false
985}
986
987function Set-FileAclOwner ($file, $owner) {
988 <#
989 .SYNOPSIS
990 Sets NTFS file owner
991
992 .DESCRIPTION
993 Sets NTFS file owner
994
995 .PARAMETER file
996 File path as string
997
998 .PARAMETER owner
999 Account as string to set as owner
1000
1001 .LINK
1002 https://stackoverflow.com/questions/22988384/powershell-change-owner-of-files-and-folders
1003 #>
1004
1005 $acl = Get-Acl($file)
1006 $account = New-Object -TypeName System.Security.Principal.NTAccount -ArgumentList $owner
1007 $acl.SetOwner($account)
1008 Set-Acl -Path $file -AclObject $acl
1009}
1010
1011function Set-FileAclPermissions ($file, $accString, $rights, $inheritanceFlags, $propagationFlags, $type) {
1012 <#
1013 .SYNOPSIS
1014 Set NTFS file permissions
1015
1016 .DESCRIPTION
1017 Set NTFS permissions for specified file
1018
1019 .PARAMETER file
1020 File path as string
1021
1022 .PARAMETER accString
1023 Account to set permissions for as string
1024
1025 .PARAMETER rights
1026 Access Rights - https://docs.microsoft.com/en-us/dotnet/api/system.security.accesscontrol.filesystemrights?view=dotnet-plat-ext-3.1
1027
1028 .PARAMETER inheritanceFlags
1029 Inheritence flags - https://docs.microsoft.com/en-us/dotnet/api/system.security.accesscontrol.inheritanceflags?view=dotnet-plat-ext-3.1
1030
1031 .PARAMETER propagationFlags
1032 Propagation flags - https://docs.microsoft.com/en-us/dotnet/api/system.security.accesscontrol.propagationflags?view=dotnet-plat-ext-3.1
1033
1034 .PARAMETER type
1035 Access control type - https://docs.microsoft.com/en-us/dotnet/api/system.security.accesscontrol.accesscontroltype?view=dotnet-plat-ext-3.1
1036
1037 .LINK
1038 https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.security/set-acl?view=powershell-7
1039
1040 .LINK
1041 https://docs.microsoft.com/en-us/dotnet/api/system.security.accesscontrol.filesystemaccessrule.-ctor?view=dotnet-plat-ext-3.1#System_Security_AccessControl_FileSystemAccessRule__ctor_System_String_System_Security_AccessControl_FileSystemRights_System_Security_AccessControl_InheritanceFlags_System_Security_AccessControl_PropagationFlags_System_Security_AccessControl_AccessControlType_
1042 #>
1043
1044 $acl = Get-Acl($file)
1045 $accessRule = New-Object System.Security.AccessControl.FileSystemAccessRule $accString, $rights, $inheritanceFlags, $propagationFlags, $type
1046 $acl.SetAccessRule($accessRule)
1047 Set-Acl -Path $file -AclObject $acl
1048}
1049
1050function Decline-SupersededUpdates ($verbose) {
1051 <#
1052 .SYNOPSIS
1053 Declines approved updates that have been approved and are superseded by other updates.
1054
1055 .DESCRIPTION
1056 Declines all updates that have been approved and are superseded by other updates. The update will only be declined if a superseding update has been approved.
1057
1058 .LINK
1059 ApprovedStates - https://docs.microsoft.com/en-us/previous-versions/windows/desktop/aa354257(v=vs.85)
1060
1061 .LINK
1062 IUpdate - https://docs.microsoft.com/en-us/previous-versions/windows/desktop/bb313429(v=vs.85)
1063
1064 .LINK
1065 UpdateCollection - https://docs.microsoft.com/en-us/previous-versions/windows/desktop/ms752803(v=vs.85)
1066 #>
1067 $declineCount = 0
1068 [reflection.assembly]::LoadWithPartialName("Microsoft.UpdateServices.Administration") | Out-Null
1069 $wsusServer = [Microsoft.UpdateServices.Administration.AdminProxy]::GetUpdateServer();
1070 $scope = New-Object Microsoft.UpdateServices.Administration.UpdateScope
1071
1072 $scope.ApprovedStates = "LatestRevisionApproved"
1073 $updates = $wsusServer.GetUpdates($scope)
1074
1075 foreach ($update in $updates) {
1076 $updatesThatSupersede = $update.GetRelatedUpdates("UpdatesThatSupersedeThisUpdate")
1077 if ($updatesThatSupersede.Count -gt 0) {
1078 foreach ($super in $updatesThatSupersede) {
1079 if ($super.IsApproved) {
1080 $update.Decline()
1081 $declineCount++
1082 break
1083 }
1084 }
1085 }
1086 }
1087
1088 if ($verbose) {
1089 Write-Host "Osbolete Updates Declined: $declineCount"
1090 }
1091 else {
1092 return $declineCount
1093 }
1094}
1095#-----------------------------------------------------------[Execution]------------------------------------------------------------
1096$iisPath = Get-WsusIISLocalizedNamespacePath
1097
1098# Check commandline parameters.
1099switch ($true) {
1100 ($FirstRun) {
1101 Write-Host "All of the following processes are highly recommended!" -ForegroundColor Blue -BackgroundColor White
1102
1103 switch ($true) {
1104 (Confirm-Prompt "Run WSUS IIS configuration optimization?") {
1105 $wsusIISConfig = Get-WsusIISConfig
1106 Test-WsusIISConfig $wsusIISConfig $recommendedIISSettings
1107 }
1108 (Confirm-Prompt "Run WSUS database optimization?") {
1109 Optimize-WsusDatabase
1110 }
1111 (Confirm-Prompt "Run WSUS server optimization?") {
1112 Optimize-WsusUpdates
1113 }
1114 (Confirm-Prompt "Create daily WSUS server optimization scheduled task?") {
1115 New-WsusMaintainenceTask('Daily')
1116 }
1117 (Confirm-Prompt "Create weekly WSUS database optimization scheduled task?") {
1118 New-WsusMaintainenceTask('Weekly')
1119 }
1120 (Confirm-Prompt "Disable device driver synchronization?") {
1121 Disable-WsusDriverSync
1122 }
1123 }
1124 Break
1125 }
1126 ($DisableDrivers) {
1127 Disable-WsusDriverSync
1128 }
1129 ($DeclineSupersededUpdates) {
1130 Decline-SupersededUpdates
1131 }
1132 ($DeepClean) {
1133 Invoke-DeepClean $unneededUpdatesbyTitle $unneededUpdatesbyProductTitles
1134 }
1135 ($InstallDailyTask) {
1136 New-WsusMaintainenceTask('Daily')
1137 }
1138 ($InstallWeeklyTask) {
1139 New-WsusMaintainenceTask('Weekly')
1140 }
1141 ($CheckConfig) {
1142 $wsusIISConfig = Get-WsusIISConfig
1143 Test-WsusIISConfig $wsusIISConfig $recommendedIISSettings
1144 }
1145 ($OptimizeServer) {
1146 Optimize-WsusUpdates
1147 }
1148 ($OptimizeDatabase) {
1149 Optimize-WsusDatabase
1150 }
1151}