PowerShell 技能连载 - Windows Update 自动化

适用于 PowerShell 5.1 及以上版本

补丁管理是企业安全运维的基石。每个月的 Patch Tuesday,微软会发布数十个安全更新,涵盖操作系统、浏览器、.NET 运行时等关键组件。如果依赖手动逐台安装更新,不仅效率低下,还容易遗漏关键补丁,给攻击者留下可乘之机。

PowerShell 为 Windows Update 自动化提供了多种手段。从内置的 Microsoft.Update.Session COM 对象完成底层扫描,到社区广泛使用的 PSWindowsUpdate 模块实现批量部署,再到结合 WSUS API 编写审批流程,可以覆盖从单机到数千台服务器的全场景补丁管理需求。

本文将围绕三个核心场景展开:更新扫描与审批、批量安装与重启编排、合规报告与异常处理,帮助你构建一套完整的 Windows Update 自动化体系。

更新扫描与审批

在部署更新之前,首先要扫描可用的更新列表,根据分类(安全更新、关键更新、驱动程序等)和 KB 编号进行筛选,然后决定哪些更新需要审批安装。以下脚本演示了完整的扫描和审批流程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# 使用 PSWindowsUpdate 模块扫描可用更新
# 首次使用需安装模块(需要管理员权限)
# Install-Module -Name PSWindowsUpdate -Force -Scope CurrentUser
Import-Module PSWindowsUpdate

# 扫描当前机器的可用更新
Write-Host "正在扫描可用更新..." -ForegroundColor Cyan
$availableUpdates = Get-WindowsUpdate -Verbose:$false

if ($availableUpdates.Count -eq 0) {
Write-Host "当前系统已是最新,无需更新。" -ForegroundColor Green
return
}

Write-Host "发现 $($availableUpdates.Count) 个可用更新:`n"

# 分类汇总
$updatesByCategory = $availableUpdates | Group-Object -Property {
if ($_.Title -match 'Security') { '安全更新' }
elseif ($_.Title -match 'Cumulative|累计') { '累积更新' }
elseif ($_.Title -match 'Driver|驱动') { '驱动程序' }
else { '其他更新' }
} | Sort-Object Count -Descending

foreach ($group in $updatesByCategory) {
Write-Host (" {0,-12} {1} 个" -f $group.Name, $group.Count) -ForegroundColor Yellow
}

# 过滤安全更新和累积更新
$criticalUpdates = $availableUpdates | Where-Object {
$_.Title -match 'Security|Security Update|安全' -or
$_.Title -match 'Cumulative|累计'
}

Write-Host "`n关键更新列表:"
$criticalUpdates | ForEach-Object {
$kb = if ($_.KB -match 'KB\d+') { $Matches[0] } else { 'N/A' }
Write-Host (" [{0}] {1}" -f $kb, $_.Title)
}

# 审批逻辑:按 KB 编号白名单安装
$approvedKBs = @(
'KB5034441' # 示例:Windows 安全更新
'KB5034440' # 示例:累积更新
)

$toInstall = $criticalUpdates | Where-Object {
$kb = if ($_.KB -match 'KB\d+') { $Matches[0] } else { '' }
$kb -in $approvedKBs
}

Write-Host "`n已审批 $($toInstall.Count) 个更新,准备安装。"
$toInstall | ForEach-Object {
$kb = if ($_.KB -match 'KB\d+') { $Matches[0] } else { 'N/A' }
Write-Host (" [审批通过] {0} - {1}" -f $kb, $_.Title) -ForegroundColor Green
}

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
正在扫描可用更新...
发现 12 个可用更新:

累积更新 4
安全更新 5
其他更新 3

关键更新列表:
[KB5034441] 2026-01 Security Update for Windows Server 2022
[KB5034440] 2026-01 Cumulative Update for Windows 11
[KB5034439] 2026-01 Security Update for .NET Framework 4.8.1
[KB5034438] 2026-01 Security Update for Windows 10
[KB5034437] 2026-01 Cumulative Update for Windows Server 2019
[KB5034436] 2026-01 Cumulative Update for .NET 8.0
[KB5034435] 2026-01 Cumulative Update for Windows 11 (23H2)
[KB5034434] 2026-01 Cumulative Update for Windows Server 2022

已审批 2 个更新,准备安装。
[审批通过] KB5034441 - 2026-01 Security Update for Windows Server 2022
[审批通过] KB5034440 - 2026-01 Cumulative Update for Windows 11

扫描结果按类别分组显示,让运维人员一目了然地看到安全更新和累积更新的数量。审批环节通过 KB 白名单机制控制,只有经过安全团队审核的补丁才会被安装,避免未经测试的更新引发兼容性问题。

批量安装与重启编排

单台机器的更新安装只是起点。在企业环境中,通常需要同时对多台服务器执行滚动更新:按批次安装更新、重启机器、验证服务恢复后再处理下一批。以下脚本实现了一个完整的滚动更新流程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# 批量安装与滚动重启编排
Import-Module PSWindowsUpdate

# 目标服务器列表(可从 AD、CMDB 或文本文件读取)
$servers = @(
'WEB-SVR01',
'WEB-SVR02',
'WEB-SVR03',
'DB-SVR01',
'DB-SVR02'
)

# 分批策略:Web 服务器先更新,数据库后更新
$batches = @(
@{ Name = 'Web层'; Servers = $servers | Where-Object { $_ -match '^WEB' } }
@{ Name = '数据库层'; Servers = $servers | Where-Object { $_ -match '^DB' } }
)

foreach ($batch in $batches) {
Write-Host "`n========== 开始处理:$($batch.Name) ==========" -ForegroundColor Cyan
$batchServers = $batch.Servers
$restartQueue = @()

foreach ($server in $batchServers) {
Write-Host "`n[$server] 扫描并安装更新..." -ForegroundColor Yellow

try {
# 远程安装已审批的安全更新(自动接受并安装)
$result = Invoke-Command -ComputerName $server -ScriptBlock {
Import-Module PSWindowsUpdate
Get-WindowsUpdate -AcceptAll -Install -AutoReboot -Verbose:$false |
Select-Object KB, Title, Result
} -ErrorAction Stop

$installed = $result | Where-Object { $_.Result -eq 'Installed' }
Write-Host " 已安装 $($installed.Count) 个更新"

# 将需要重启的服务器加入队列
if ($result | Where-Object { $_.Result -eq 'InstalledRebootRequired' }) {
$restartQueue += $server
Write-Host " 需要重启,已加入重启队列" -ForegroundColor Magenta
}
}
catch {
Write-Host " 安装失败:$($_.Exception.Message)" -ForegroundColor Red
}
}

# 编排重启:逐台重启并等待恢复
foreach ($server in $restartQueue) {
Write-Host "`n[$server] 正在重启..." -ForegroundColor Yellow
Restart-Computer -ComputerName $server -Force -Wait -For PowerShell `
-Timeout 600 -Delay 15

# 健康检查:验证关键服务已恢复
$health = Invoke-Command -ComputerName $server -ScriptBlock {
$services = @('W3SVC', 'WinRM', 'EventLog')
$results = @{}
foreach ($svc in $services) {
$s = Get-Service -Name $svc -ErrorAction SilentlyContinue
$results[$svc] = if ($s) { $s.Status } else { 'NotFound' }
}
$results
}

$allRunning = $health.Values | Where-Object { $_ -ne 'Running' }
if ($allRunning.Count -eq 0) {
Write-Host " 健康检查通过,所有关键服务已恢复" -ForegroundColor Green
}
else {
Write-Host " 警告:部分服务未恢复 $allRunning" -ForegroundColor Red
}
}

Write-Host "`n$($batch.Name) 批次处理完成" -ForegroundColor Green
}

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
========== 开始处理:Web层 ==========

[WEB-SVR01] 扫描并安装更新...
已安装 3 个更新
需要重启,已加入重启队列

[WEB-SVR02] 扫描并安装更新...
已安装 2 个更新

[WEB-SVR03] 扫描并安装更新...
已安装 3 个更新
需要重启,已加入重启队列

[WEB-SVR01] 正在重启...
健康检查通过,所有关键服务已恢复

[WEB-SVR03] 正在重启...
健康检查通过,所有关键服务已恢复

Web层 批次处理完成

========== 开始处理:数据库层 ==========

[DB-SVR01] 扫描并安装更新...
已安装 4 个更新
需要重启,已加入重启队列

[DB-SVR02] 扫描并安装更新...
已安装 3 个更新

[DB-SVR01] 正在重启...
健康检查通过,所有关键服务已恢复

数据库层 批次处理完成

滚动更新策略按应用层分批执行,确保在更新过程中始终有足够的实例保持服务可用。每台服务器重启后会自动执行健康检查,验证 WinRM、IIS、事件日志等关键服务已正常恢复,发现问题立即告警。

合规报告与异常处理

补丁管理的最后一步是生成合规报告,追踪哪些机器已完成更新、哪些存在缺失补丁、哪些安装失败需要重试。以下脚本构建了一个简易的合规审计仪表板。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# 合规报告与异常处理
Import-Module PSWindowsUpdate

$servers = @(
'WEB-SVR01', 'WEB-SVR02', 'WEB-SVR03',
'DB-SVR01', 'DB-SVR02'
)

$report = @()
$failedServers = @()

foreach ($server in $servers) {
try {
$updateStatus = Invoke-Command -ComputerName $server -ScriptBlock {
Import-Module PSWindowsUpdate

# 获取缺失的更新
$missing = Get-WindowsUpdate -Verbose:$false

# 获取最近 7 天安装的更新
$recentInstalled = Get-WUHistory | Where-Object {
$_.Date -gt (Get-Date).AddDays(-7)
}

# 获取上次重启时间
$lastBoot = (Get-CimInstance -ClassName Win32_OperatingSystem).LastBootUpTime

return @{
MissingCount = $missing.Count
MissingCritical = ($missing | Where-Object {
$_.Title -match 'Security|安全'
}).Count
RecentInstalled = $recentInstalled.Count
LastBootTime = $lastBoot
}
} -ErrorAction Stop

# 计算合规状态
$daysSinceBoot = ((Get-Date) - $updateStatus.LastBootTime).Days
$compliance = if ($updateStatus.MissingCritical -eq 0 -and $daysSinceBoot -lt 30) {
'合规'
}
elseif ($updateStatus.MissingCritical -le 2 -and $daysSinceBoot -lt 60) {
'部分合规'
}
else {
'不合规'
}

$report += [PSCustomObject]@{
Server = $server
Compliance = $compliance
MissingCritical = $updateStatus.MissingCritical
MissingTotal = $updateStatus.MissingCount
RecentInstalled = $updateStatus.RecentInstalled
LastBootDays = $daysSinceBoot
}

# 记录失败的服务器,用于后续重试
if ($compliance -eq '不合规') {
$failedServers += $server
}
}
catch {
$report += [PSCustomObject]@{
Server = $server
Compliance = '无法连接'
MissingCritical = '-'
MissingTotal = '-'
RecentInstalled = '-'
LastBootDays = '-'
}
$failedServers += $server
}
}

# 输出合规仪表板
Write-Host "`n========== Windows Update 合规报告 ==========" -ForegroundColor Cyan
Write-Host "报告时间:$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')`n"

$report | Format-Table -AutoSize

# 合规统计
$compliant = ($report | Where-Object { $_.Compliance -eq '合规' }).Count
$partial = ($report | Where-Object { $_.Compliance -eq '部分合规' }).Count
$nonCompliant = ($report | Where-Object {
$_.Compliance -in '不合规', '无法连接'
}).Count

Write-Host "合规统计:"
Write-Host (" 合规:{0} 台 ({1:P0})" -f $compliant,
($compliant / $servers.Count)) -ForegroundColor Green
Write-Host (" 部分合规:{0} 台 ({1:P0})" -f $partial,
($partial / $servers.Count)) -ForegroundColor Yellow
Write-Host (" 不合规/离线:{0} 台 ({1:P0})" -f $nonCompliant,
($nonCompliant / $servers.Count)) -ForegroundColor Red

# 失败重试:对不合规服务器重新安装
if ($failedServers.Count -gt 0) {
Write-Host "`n不合规服务器,准备重试安装:" -ForegroundColor Magenta
$failedServers | ForEach-Object { Write-Host " - $_" }
}

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
========== Windows Update 合规报告 ==========
报告时间:2026-01-16 14:30:00

Server Compliance MissingCritical MissingTotal RecentInstalled LastBootDays
------ ---------- --------------- ------------ --------------- ------------
WEB-SVR01 合规 0 0 3 2
WEB-SVR02 部分合规 1 2 2 15
WEB-SVR03 合规 0 0 4 2
DB-SVR01 不合规 4 6 0 45
DB-SVR02 无法连接 - - - -

合规统计:
合规:2 台 (40.00%)
部分合规:1 台 (20.00%)
不合规/离线:2 台 (40.00%)

不合规服务器,准备重试安装:
- DB-SVR01
- DB-SVR02

合规报告从三个维度评估补丁状态:缺失关键更新的数量、最近一周已安装更新的数量、以及距上次重启的天数。将三者结合可以全面反映机器的补丁健康度。不合规的服务器会被自动收集,用于触发重试流程。

注意事项

  1. 权限要求:Windows Update 操作需要本地管理员权限。在远程执行时,确保 WinRM 已启用且执行账户具有目标服务器的管理员权限,建议使用 Group Managed Service Account(gMSA)统一管理服务账户。

  2. PSWindowsUpdate 模块来源:该模块托管在 PowerShell Gallery 上,安装前请确认执行策略允许运行(Set-ExecutionPolicy RemoteSigned)。在生产环境中,建议将模块下载到内部 NuGet 仓库,避免直接从公网拉取。

  3. 测试先行:每个 Patch Tuesday 发布的更新应先在测试环境中验证,确认与业务应用无兼容性问题后再推广到生产。建议维护一个延迟 1-2 周的部署窗口。

  4. 重启窗口:某些更新(特别是内核和驱动相关补丁)必须重启才能生效。重启操作应安排在维护窗口内,并提前通知相关团队。对于高可用集群,确保滚动重启不会同时影响所有节点。

  5. WSUS 集成:大规模环境建议通过 WSUS(Windows Server Update Services)或 Configuration Manager 统一管理更新审批和分发。PowerShell 可以通过 Microsoft.UpdateServices 命名空间的 API 编写 WSUS 审批规则。

  6. 日志与回滚:保留每次更新的安装日志(路径 C:\Windows\Logs\CBS\CBS.log),出现问题时可以通过 wusa /uninstall 回滚特定补丁。建议将合规报告定期归档到中央日志系统,方便审计追溯。

PowerShell 技能连载 - Windows 更新自动化管理

适用于 PowerShell 5.1 及以上版本

操作系统补丁管理是企业安全运维的基石。未打补丁的系统是勒索软件和漏洞攻击的首要目标,每一次重大的安全事件背后几乎都有”未及时安装更新”的影子。然而,Windows 更新管理远比点击”检查更新”复杂得多——需要控制更新时机、筛选更新类别、处理重启策略、验证安装结果,并在多台服务器上保持一致性。

PowerShell 通过 Microsoft.Update.Session COM 对象和 PSWindowsUpdate 模块提供了完整的 Windows 更新编程接口。本文将介绍如何使用 PowerShell 实现 Windows 更新的查询、安装、回滚和自动化管理。

查询可用的 Windows 更新

在安装更新之前,先了解有哪些可用更新及其分类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
function Get-WindowsAvailableUpdate {
<#
.SYNOPSIS
查询可用的 Windows 更新
.PARAMETER Category
更新类别过滤(Security、Important、Optional)
#>
[CmdletBinding()]
param(
[string]$Category
)

# 创建更新会话
$updateSession = New-Object -ComObject Microsoft.Update.Session
$updateSearcher = $updateSession.CreateUpdateSearcher()

# 构建搜索条件
$searchCriteria = 'IsInstalled=0 and Type=''Software'''

Write-Verbose "搜索条件:$searchCriteria"
$searchResult = $updateSearcher.Search($searchCriteria)

Write-Verbose "找到 $($searchResult.Updates.Count) 个可用更新"

$updates = foreach ($update in $searchResult.Updates) {
# 确定更新类别
$categories = $update.Categories | ForEach-Object { $_.Name }
$isSecurity = $categories -contains 'Security Updates'
$isImportant = $categories -contains 'Important Updates'

$updateClass = if ($isSecurity) { 'Security' }
elseif ($isImportant) { 'Important' }
else { 'Optional' }

[PSCustomObject]@{
Title = $update.Title
KB = if ($update.KBArticleIDs.Count -gt 0) { $update.KBArticleIDs[0] } else { 'N/A' }
Category = $updateClass
Categories = ($categories -join ', ')
SizeMB = [math]::Round($update.MaxDownloadSize / 1MB, 2)
IsMandatory = $update.IsMandatory
IsDownloaded = $update.IsDownloaded
Description = $update.Description
Identity = $update.Identity.UpdateID
}
}

# 按类别过滤
if ($Category) {
$updates = $updates | Where-Object { $_.Category -eq $Category }
}

$updates | Sort-Object Category, Title
}

# 查询所有可用更新
$available = Get-WindowsAvailableUpdate -Verbose
$available | Format-Table Title, KB, Category, SizeMB -AutoSize

# 按类别统计
$available | Group-Object Category | Format-Table Name, Count -AutoSize

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
详细: 搜索条件:IsInstalled=0 and Type='Software'
详细: 找到 12 个可用更新

Title KB Category SizeMB
----- -- -------- ------
2025-08 Security Update for Windows Server KB5041234 Security 125.5
2025-08 Cumulative Update for .NET Framework KB5041235 Security 68.2
Windows Malicious Software Removal Tool KB890830 Important 45.0
.NET Runtime 8.0.8 Update KB5041236 Optional 32.1

Name Count
---- -----
Security 2
Important 1
Optional 9

下载并安装 Windows 更新

通过 COM 对象控制更新下载和安装过程,可以精确控制安装哪些更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
function Install-WindowsUpdate {
<#
.SYNOPSIS
下载并安装 Windows 更新
.PARAMETER Category
安装指定类别的更新
.PARAMETER KBArticleIDs
安装指定 KB 编号的更新
.PARAMETER AutoReboot
是否允许自动重启
#>
[CmdletBinding()]
param(
[string]$Category,

[string[]]$KBArticleIDs,

[switch]$AutoReboot
)

$updateSession = New-Object -ComObject Microsoft.Update.Session
$updateSearcher = $updateSession.CreateUpdateSearcher()

# 搜索可用更新
$searchResult = $updateSearcher.Search('IsInstalled=0 and Type=''Software''')

# 筛选要安装的更新
$updatesToInstall = New-Object -ComObject Microsoft.Update.UpdateColl

foreach ($update in $searchResult.Updates) {
$shouldInstall = $false

# 按 KB 编号筛选
if ($KBArticleIDs) {
foreach ($kb in $KBArticleIDs) {
if ($update.KBArticleIDs.Count -gt 0 -and $update.KBArticleIDs.Contains($kb)) {
$shouldInstall = $true
break
}
}
}
# 按类别筛选(默认安装安全更新)
elseif ($Category -eq 'Security') {
$cats = $update.Categories | ForEach-Object { $_.Name }
if ($cats -contains 'Security Updates') {
$shouldInstall = $true
}
}
else {
$shouldInstall = $true
}

if ($shouldInstall) {
# 接受 EULA
if ($update.EulaAccepted -eq $false) {
$update.AcceptEula()
}
[void]$updatesToInstall.Add($update)
Write-Verbose "添加更新:$($update.Title)"
}
}

if ($updatesToInstall.Count -eq 0) {
Write-Host '没有需要安装的更新' -ForegroundColor Yellow
return
}

Write-Host "准备安装 $($updatesToInstall.Count) 个更新" -ForegroundColor Cyan

# 下载更新
Write-Host '正在下载更新...' -ForegroundColor Yellow
$downloader = $updateSession.CreateUpdateDownloader()
$downloader.Updates = $updatesToInstall
$downloadResult = $downloader.Download()

$downloadStatus = switch ($downloadResult.ResultCode) {
0 { 'NotStarted' }
1 { 'InProgress' }
2 { 'Succeeded' }
3 { 'SucceededWithErrors' }
4 { 'Failed' }
5 { 'Aborted' }
default { 'Unknown' }
}

Write-Host "下载状态:$downloadStatus"

if ($downloadResult.ResultCode -notin @(2, 3)) {
throw "下载失败,状态码:$($downloadResult.ResultCode)"
}

# 安装更新
Write-Host '正在安装更新...' -ForegroundColor Yellow
$installer = $updateSession.CreateUpdateInstaller()
$installer.Updates = $updatesToInstall

if (-not $AutoReboot) {
# 禁止自动重启
$installer.AllowSourcePrompts = $false
}

$installResult = $installer.Install()

# 输出每个更新的安装结果
$results = for ($i = 0; $i -lt $updatesToInstall.Count; $i++) {
$resultCode = $installResult.GetUpdateResult($i).ResultCode
$status = switch ($resultCode) {
0 { 'NotStarted' }
1 { 'InProgress' }
2 { 'Installed' }
3 { 'InstalledWithErrors' }
4 { 'Failed' }
5 { 'Aborted' }
default { 'Unknown' }
}

[PSCustomObject]@{
Title = $updatesToInstall.Item($i).Title
KB = if ($updatesToInstall.Item($i).KBArticleIDs.Count -gt 0) { $updatesToInstall.Item($i).KBArticleIDs[0] } else { 'N/A' }
Status = $status
}
}

$results | Format-Table -AutoSize

# 检查是否需要重启
if ($installResult.RebootRequired) {
if ($AutoReboot) {
Write-Host '更新安装完成,正在重启系统...' -ForegroundColor Yellow
Restart-Computer -Force
} else {
Write-Host '更新安装完成,需要重启系统' -ForegroundColor Yellow
}
} else {
Write-Host '更新安装完成,无需重启' -ForegroundColor Green
}

return $results
}

# 只安装安全更新
$installResults = Install-WindowsUpdate -Category 'Security' -Verbose

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
详细: 添加更新:2025-08 Security Update for Windows Server
详细: 添加更新:2025-08 Cumulative Update for .NET Framework
准备安装 2 个更新
正在下载更新...
下载状态:Succeeded
正在安装更新...

Title KB Status
----- -- ------
2025-08 Security Update for Windows Server KB5041234 Installed
2025-08 Cumulative Update for .NET Framework KB5041235 Installed
更新安装完成,需要重启系统

使用 PSWindowsUpdate 模块

PSWindowsUpdate 是社区维护的优秀模块,提供了更简洁的 Windows 更新管理接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 安装模块(仅首次需要)
Install-Module -Name PSWindowsUpdate -Force -Scope CurrentUser
Import-Module PSWindowsUpdate

# 查看可用更新
Get-WindowsUpdate -Verbose | Format-Table Title, KB, Size, Status -AutoSize

# 只安装安全更新
Get-WindowsUpdate -Category 'Security Updates' -AcceptAll -Install -AutoReboot -Verbose

# 排除特定 KB
Get-WindowsUpdate -NotKBArticleID 'KB5041234' -AcceptAll -Install -Verbose

# 查看安装历史
Get-WUHistory | Select-Object -First 10 | Format-Table Date, Title, Result -AutoSize

# 远程管理多台服务器的更新
$servers = @('SRV01', 'SRV02', 'SRV03')
Get-WindowsUpdate -ComputerName $servers -Category 'Security Updates' -AcceptAll -Install -Verbose

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Title                                         KB        Size     Status
----- -- ---- ------
2025-08 Security Update for Windows Server KB5041234 125.5 MB Downloaded
2025-08 .NET Framework Cumulative Update KB5041235 68.2 MB Downloaded

ComputerName Result KB Title
------------ ------ -- -----
SRV01 Accepted KB5041234 2025-08 Security Update for Windows Server
SRV01 Accepted KB5041235 2025-08 .NET Framework Cumulative Update
SRV02 Accepted KB5041234 2025-08 Security Update for Windows Server
SRV03 Accepted KB5041234 2025-08 Security Update for Windows Server

Date Title Result
---- ----- ------
2025/8/20 22:00:15 2025-08 Security Update for Windows Server Installed
2025/8/13 22:00:10 2025-08 .NET Framework Cumulative Update Installed
2025/8/6 22:00:08 2025-07 Security Update for Windows Server Installed

查询已安装的更新历史

了解系统上已安装了哪些更新,有助于审计和排查问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
function Get-WindowsUpdateHistory {
<#
.SYNOPSIS
查询 Windows 更新安装历史
.PARAMETER Days
查询最近多少天的记录
.PARAMETER Result
按安装结果过滤(Succeeded、Failed、All)
#>
[CmdletBinding()]
param(
[int]$Days = 30,

[ValidateSet('Succeeded', 'Failed', 'All')]
[string]$Result = 'All'
)

$updateSession = New-Object -ComObject Microsoft.Update.Session
$updateSearcher = $updateSession.CreateUpdateSearcher()

# 查询历史记录
$cutoffDate = (Get-Date).AddDays(-$Days)
$history = $updateSearcher.QueryHistory(0, $updateSearcher.GetTotalHistoryCount())

$results = foreach ($record in $history) {
if ($record.Date -lt $cutoffDate) { continue }

$operation = switch ($record.Operation) {
1 { 'Install' }
2 { 'Uninstall' }
default { 'Unknown' }
}

$resultCode = switch ($record.ResultCode) {
0 { 'NotStarted' }
1 { 'InProgress' }
2 { 'Succeeded' }
3 { 'SucceededWithErrors' }
4 { 'Failed' }
5 { 'Aborted' }
default { 'Unknown' }
}

# 提取 KB 编号
$kb = if ($record.Title -match 'KB(\d+)') { "KB$($Matches[1])" } else { 'N/A' }

[PSCustomObject]@{
Date = $record.Date
Title = $record.Title
KB = $kb
Operation = $operation
Result = $resultCode
HResult = $record.HResult
}
}

# 按结果过滤
if ($Result -eq 'Succeeded') {
$results = $results | Where-Object { $_.Result -eq 'Succeeded' }
} elseif ($Result -eq 'Failed') {
$results = $results | Where-Object { $_.Result -in @('Failed', 'SucceededWithErrors') }
}

$results | Sort-Object Date -Descending
}

# 查询最近 30 天的更新历史
$history = Get-WindowsUpdateHistory -Days 30
$history | Format-Table Date, KB, Operation, Result -AutoSize

# 只看失败的更新
Get-WindowsUpdateHistory -Days 90 -Result Failed | Format-Table Date, Title, Result -AutoSize

执行结果示例:

1
2
3
4
5
6
7
Date                KB        Operation Result
---- -- --------- ------
2025/8/20 22:00:15 KB5041234 Install Succeeded
2025/8/20 22:00:12 KB5041235 Install Succeeded
2025/8/13 22:00:10 KB5040987 Install Succeeded
2025/8/6 22:00:08 KB5040765 Install Succeeded
2025/7/30 22:00:05 KB5040543 Install Failed

卸载 Windows 更新

某些更新可能导致兼容性问题,需要回滚(卸载)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
function Uninstall-WindowsUpdateByKB {
<#
.SYNOPSIS
卸载指定的 Windows 更新
.PARAMETER KBArticleID
要卸载的 KB 编号
.PARAMETER AutoReboot
是否自动重启
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$KBArticleID,

[switch]$AutoReboot
)

# 使用 wusa 卸载更新
$wusaPath = Join-Path $env:SystemRoot 'System32\wusa.exe'

# 先查找已安装更新的 .msu 文件路径
$updateSession = New-Object -ComObject Microsoft.Update.Session
$updateSearcher = $updateSession.CreateUpdateSearcher()
$searchResult = $updateSearcher.Search("IsInstalled=1 and Type='Software'")

$targetUpdate = $null
foreach ($update in $searchResult.Updates) {
if ($update.KBArticleIDs.Count -gt 0 -and $update.KBArticleIDs.Contains($KBArticleID)) {
$targetUpdate = $update
break
}
}

if (-not $targetUpdate) {
Write-Warning "未找到已安装的更新:$KBArticleID"
return
}

Write-Verbose "找到更新:$($targetUpdate.Title)"
Write-Host "正在卸载:$($targetUpdate.Title)" -ForegroundColor Yellow

# 使用 DISM 卸载(更可靠)
$dismArgs = "/Online /Remove-Package /PackageName:$($targetUpdate.Identity.UpdateID) /NoRestart /Quiet"

$process = Start-Process -FilePath 'dism.exe' `
-ArgumentList $dismArgs `
-Wait `
-PassThru `
-NoNewWindow

$status = if ($process.ExitCode -eq 0) { 'Succeeded' } else { "Failed (Exit: $($process.ExitCode))" }

[PSCustomObject]@{
KB = $KBArticleID
Title = $targetUpdate.Title
Status = $status
RebootRequired = $process.ExitCode -eq 3010
}

if ($process.ExitCode -in @(0, 3010) -and $AutoReboot) {
Write-Host '卸载完成,正在重启...' -ForegroundColor Yellow
Restart-Computer -Force
}
}

# 卸载特定更新
$uninstallResult = Uninstall-WindowsUpdateByKB -KBArticleID 'KB5041234' -Verbose
$uninstallResult | Format-List

执行结果示例:

1
2
3
4
5
6
7
详细: 找到更新:2025-08 Security Update for Windows Server
正在卸载:2025-08 Security Update for Windows Server

KB : KB5041234
Title : 2025-08 Security Update for Windows Server
Status : Succeeded
RebootRequired : True

自动化补丁管理计划

将更新管理流程编排为可定期执行的计划任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
function Register-PatchSchedule {
<#
.SYNOPSIS
注册自动化补丁管理计划任务
.PARAMETER ScheduleDay
执行日期(每周几)
.PARAMETER ScheduleTime
执行时间
.PARAMETER Category
更新类别
.PARAMETER AutoReboot
是否自动重启
#>
[CmdletBinding()]
param(
[DayOfWeek]$ScheduleDay = 'Sunday',

[string]$ScheduleTime = '02:00',

[string]$Category = 'Security',

[switch]$AutoReboot
)

# 创建计划任务动作
$scriptBlock = @"
Import-Module PSWindowsUpdate
Get-WindowsUpdate -Category 'Security Updates' -AcceptAll -Install $(if ($AutoReboot) { '-AutoReboot' })
Write-Output "Patch cycle completed at $(Get-Date)" >> C:\Logs\WindowsUpdate\patch-cycle.log
"@

$scriptPath = 'C:\Scripts\Invoke-PatchCycle.ps1'
$scriptDir = Split-Path $scriptPath
if (-not (Test-Path $scriptDir)) {
New-Item -Path $scriptDir -ItemType Directory -Force | Out-Null
}
$scriptBlock | Set-Content $scriptPath -Encoding UTF8

# 注册计划任务
$action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument "-NoProfile -ExecutionPolicy Bypass -File `"$scriptPath`""
$trigger = New-ScheduledTaskTrigger -Weekly -DaysOfWeek $ScheduleDay -At $ScheduleTime
$settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -DontStopOnIdleEnd -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries

$principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest

$taskParams = @{
TaskName = 'AutoPatchCycle'
Action = $action
Trigger = $trigger
Settings = $settings
Principal = $principal
Description = '每周自动安装 Windows 安全更新'
}

Register-ScheduledTask @taskParams -Force

Write-Host "计划任务已注册:AutoPatchCycle" -ForegroundColor Green
Write-Host "执行时间:每周$ScheduleDay $ScheduleTime" -ForegroundColor Cyan
Write-Host "更新类别:$Category" -ForegroundColor Cyan
Write-Host "自动重启:$AutoReboot" -ForegroundColor Cyan
}

# 注册每周日凌晨 2 点执行的安全更新任务
Register-PatchSchedule -ScheduleDay Sunday -ScheduleTime '02:00' -Category Security -AutoReboot

执行结果示例:

1
2
3
4
计划任务已注册:AutoPatchCycle
执行时间:每周Sunday 02:00
更新类别:Security
自动重启:True

注意事项

  • 补丁测试:生产服务器上不要第一时间安装新补丁。建议先在测试环境验证,确认无兼容性问题后再逐步推广到生产环境,形成”测试-预发布-生产”的分阶段部署流程。
  • 重启窗口:安全更新通常需要重启才能生效,应配置在业务低峰期(如凌晨)自动重启,避免影响在线服务。重启前确保所有应用服务已正确停止。
  • WSUS 配置:企业环境中通常使用 WSUS(Windows Server Update Services)或 SCCM 管理更新分发,PowerShell 脚本中的更新源会自动继承系统的 WSUS 配置,无需额外设置。
  • 回滚方案:某些更新可能导致应用程序兼容性问题,安装前应创建系统还原点(Checkpoint-Computer),确保可以快速回滚。
  • 日志审计:每次补丁操作都应记录详细日志,包括安装时间、更新编号、操作结果和操作者信息,以满足安全合规审计要求。
  • 模块兼容性PSWindowsUpdate 模块在不同 Windows 版本上的行为可能有差异,使用前应在目标系统上测试兼容性,必要时回退到 COM 对象方式。