PowerShell 技能连载 - WinGet 高级包管理

适用于 PowerShell 7.0 及以上版本,Windows 10/11 内置 WinGet

从安装工具到企业级管理平台

随着 Windows Package Manager(WinGet)的持续迭代,它已经从一个简单的命令行安装工具成长为 Windows 平台软件生命周期管理的核心组件。微软在 2025 年为 WinGet 引入了 winget configurewinget export 等新能力,使其在配置即代码(Configuration as Code)方向上迈出了关键一步。对于运维团队而言,WinGet 不再只是”装软件的工具”,而是可以纳入 CI/CD 流水线的基础设施自动化组件。

在企业环境中,软件版本管理、环境一致性和更新回滚是最让人头疼的三件事。开发团队抱怨”我本地能跑但你那里不行”,安全团队要求所有软件必须及时打补丁,运维团队则担心某个更新导致大面积故障。这些需求都可以通过 PowerShell 与 WinGet 的深度集成来解决。

本文将从三个高级场景出发:软件版本审计与对比、YAML 配置驱动的声明式部署、更新自动化与安全回滚,展示如何用 PowerShell 构建企业级的 WinGet 包管理方案。

软件清单与版本审计

在多机环境中,了解每台机器安装了哪些软件、版本是否一致,是运维工作的起点。下面的脚本会扫描本机所有 WinGet 管理的软件,并与基准版本清单进行对比,快速识别版本偏差。

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
function Compare-WinGetBaseline {
<#
.SYNOPSIS
将本机已安装软件与基准版本清单对比,输出偏差报告
.PARAMETER BaselinePath
基准 JSON 文件路径,包含期望的软件版本
.PARAMETER ExportCurrent
将当前已安装软件导出为基准文件(用于首次建立基准)
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$BaselinePath,
[switch]$ExportCurrent
)

# 采集当前已安装软件
$rawOutput = winget list --accept-source-agreements 2>&1
$installed = $rawOutput | Where-Object {
$_ -match '^\S' -and
$_ -notmatch '^Name\s+Id' -and
$_ -notmatch '^\d+ packages'
} | ForEach-Object {
$parts = $_ -split '\s{2,}'
if ($parts.Count -ge 3) {
[PSCustomObject]@{
Name = $parts[0].Trim()
Id = $parts[1].Trim()
Version = if ($parts.Count -ge 4) { $parts[2].Trim() } else { '未知' }
Available = if ($parts.Count -ge 4) { $parts[3].Trim() } else {
if ($parts.Count -eq 3) { $parts[2].Trim() } else { '' }
}
}
}
}

# 导出模式:将当前状态保存为基准
if ($ExportCurrent) {
$baseline = $installed | ForEach-Object {
[ordered]@{ Id = $_.Id; Version = $_.Version }
}
$baseline | ConvertTo-Json -Depth 3 | Set-Content $BaselinePath -Encoding UTF8
Write-Host "基准已导出至 $BaselinePath ($($baseline.Count) 个软件包)" `
-ForegroundColor Green
return
}

# 对比模式:加载基准并与当前状态比较
$baselineRaw = Get-Content $BaselinePath -Raw | ConvertFrom-Json
$baselineMap = @{}
foreach ($item in $baselineRaw) {
$baselineMap[$item.Id] = $item.Version
}

$results = foreach ($pkg in $installed) {
if ($baselineMap.ContainsKey($pkg.Id)) {
$expected = $baselineMap[$pkg.Id]
$status = if ($pkg.Version -eq $expected) { '匹配' }
elseif ([version]::TryParse($pkg.Version, [ref]$null) -and
[version]::TryParse($expected, [ref]$null)) {
if ([version]$pkg.Version -gt [version]$expected) { '更新' }
else { '过旧' }
} else { '不同' }
[PSCustomObject]@{
Name = $pkg.Name
Id = $pkg.Id
Current = $pkg.Version
Expected = $expected
Status = $status
}
}
}

$results | Sort-Object Status | Format-Table -AutoSize
$summary = $results | Group-Object Status | ForEach-Object {
"$($_.Name): $($_.Count)"
}
Write-Host "版本偏差汇总: $($summary -join ', ')" -ForegroundColor Cyan
}

# 首次建立基准
# Compare-WinGetBaseline -BaselinePath './baseline.json' -ExportCurrent

# 后续对比检查
# Compare-WinGetBaseline -BaselinePath './baseline.json'

上面的脚本核心逻辑是:先用 winget list 采集当前已安装软件,再将结果与基准 JSON 文件逐项对比。首次使用时通过 -ExportCurrent 参数将当前状态保存为基准文件,后续巡检时直接运行对比即可。状态分为”匹配”、”更新”、”过旧”和”不同”四种,其中”更新”表示本机版本比基准还新(可能是自行升级的未记录变更),”过旧”则是需要关注的版本偏差。

1
2
3
4
5
6
7
8
9
10
Name                Id                            Current  Expected Status
---- -- ------- -------- ------
7-Zip 7zip.7zip 24.09 24.09 匹配
Visual Studio Code Microsoft.VisualStudioCode 1.98.2 1.97.0 更新
Git Git.Git 2.47.1 2.48.0 过旧
PowerShell Microsoft.PowerShell 7.5.0 7.5.0 匹配
Node.js LTS OpenJS.NodeJS.LTS 22.14.0 22.12.0 更新
Docker Desktop Docker.DockerDesktop 4.34.0 4.35.0 过旧

版本偏差汇总: 匹配: 2, 更新: 2, 过旧: 2

YAML 配置驱动的声明式部署

在企业环境中,软件部署配置通常需要纳入版本控制,并与团队共享。YAML 格式比 JSON 更易读、支持注释,非常适合作为声明式部署配置的载体。下面的脚本读取 YAML 配置文件,确保目标机器的软件状态与声明的期望状态一致。

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
function Invoke-WinGetDesiredState {
<#
.SYNOPSIS
读取 YAML 声明式配置,确保软件状态与期望一致
.PARAMETER ConfigPath
YAML 配置文件路径
.PARAMETER ReportOnly
仅生成偏差报告,不执行实际安装或卸载
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$ConfigPath,
[switch]$ReportOnly
)

# 读取 YAML 配置(需要 powershell-yaml 模块或手动解析)
$configContent = Get-Content $ConfigPath -Raw

# 简易 YAML 解析:提取 packages 节点下的条目
$lines = $configContent -split "`n"
$desiredPackages = @{}
$currentSection = ''

foreach ($line in $lines) {
if ($line -match '^\s*#') { continue }
if ($line -match '^packages:') {
$currentSection = 'packages'
continue
}
if ($currentSection -eq 'packages' -and
$line -match '^\s{2}(\S+):\s*$') {
$pkgId = $Matches[1]
$desiredPackages[$pkgId] = @{ State = 'present'; Version = '' }
$currentSection = "pkg_$pkgId"
}
if ($line -match '^\s{4}state:\s*(\S+)') {
$desiredPackages[$pkgId]['State'] = $Matches[1]
}
if ($line -match '^\s{4}version:\s*(\S+)') {
$desiredPackages[$pkgId]['Version'] = $Matches[1]
}
if ($line -match '^[^\s]') {
$currentSection = ''
}
}

# 获取当前已安装软件
$rawOutput = winget list --accept-source-agreements 2>&1
$installedIds = @()
$installedMap = @{}

$rawOutput | Where-Object {
$_ -match '^\S' -and
$_ -notmatch '^Name\s+Id' -and
$_ -notmatch '^\d+ packages'
} | ForEach-Object {
$parts = $_ -split '\s{2,}'
if ($parts.Count -ge 2) {
$id = $parts[1].Trim()
$installedIds += $id
$installedMap[$id] = if ($parts.Count -ge 4) {
$parts[2].Trim()
} else { '未知' }
}
}

# 计算偏差并执行收敛
$actions = @()

foreach ($id in $desiredPackages.Keys) {
$desired = $desiredPackages[$id]
if ($desired.State -eq 'present') {
if ($id -notin $installedIds) {
$actions += [PSCustomObject]@{
Action = '安装'
Id = $id
Target = $desired.Version
}
} elseif ($desired.Version -and
$installedMap[$id] -ne $desired.Version) {
$actions += [PSCustomObject]@{
Action = '版本调整'
Id = $id
Target = "$($installedMap[$id]) -> $($desired.Version)"
}
}
} elseif ($desired.State -eq 'absent' -and $id -in $installedIds) {
$actions += [PSCustomObject]@{
Action = '卸载'
Id = $id
Target = $installedMap[$id]
}
}
}

Write-Host "配置偏差分析完成,共 $($actions.Count) 项操作:" `
-ForegroundColor Cyan
$actions | Format-Table -AutoSize

if ($ReportOnly) {
Write-Host "[报告模式] 不执行实际操作" -ForegroundColor DarkYellow
return
}

foreach ($act in $actions) {
switch ($act.Action) {
'安装' {
$args = @('install', '--id', $act.Id,
'--accept-package-agreements',
'--accept-source-agreements')
if ($desiredPackages[$act.Id].Version) {
$args += @('--version', $desiredPackages[$act.Id].Version)
}
Write-Host "安装 $($act.Id)..." -ForegroundColor Yellow -NoNewline
winget @args 2>&1 | Out-Null
}
'卸载' {
Write-Host "卸载 $($act.Id)..." -ForegroundColor Yellow -NoNewline
winget uninstall --id $act.Id 2>&1 | Out-Null
}
'版本调整' {
Write-Host "调整 $($act.Id) 版本..." -ForegroundColor Yellow -NoNewline
winget install --id $act.Id --version `
$desiredPackages[$act.Id].Version `
--force --accept-package-agreements `
--accept-source-agreements 2>&1 | Out-Null
}
}
if ($LASTEXITCODE -eq 0) {
Write-Host " 完成" -ForegroundColor Green
} else {
Write-Host " 失败 (退出码: $LASTEXITCODE)" -ForegroundColor Red
}
}
Write-Host "`n状态收敛完成" -ForegroundColor Green
}

这段脚本实现了一个声明式配置引擎:读取 YAML 配置文件中声明的期望状态(presentabsent),与本机实际状态对比,计算出需要执行的操作列表(安装、卸载、版本调整),然后逐一收敛。-ReportOnly 模式下仅展示偏差报告,不执行实际操作,适合在 CI 流水线中作为审计步骤。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
配置偏差分析完成,共 4 项操作:

Action Id Target
------ -- ------
安装 Microsoft.VisualStudioCode 1.98.2
安装 Docker.DockerDesktop 4.35.0
卸载 SomeApp.Legacy 2.1.0
版本调整 Git.Git 2.47.1 -> 2.48.0

安装 Microsoft.VisualStudioCode... 完成
安装 Docker.DockerDesktop... 完成
卸载 SomeApp.Legacy... 完成
调整 Git.Git 版本... 完成

状态收敛完成

更新自动化与安全回滚

自动更新是双刃剑:及时打补丁能堵住安全漏洞,但一次不兼容的更新可能让整个团队停工。下面的脚本在执行自动更新前会先创建软件快照,如果更新后出现问题,可以从快照恢复到更新前的版本。

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
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
function Invoke-WinGetSafeUpdate {
<#
.SYNOPSIS
安全更新:更新前快照,支持回滚
.PARAMETER SnapshotDir
快照存储目录
.PARAMETER MaxRetries
单个软件包更新失败后的最大重试次数
.PARAMETER RollbackOnFailure
任一软件更新失败时自动回滚所有已更新的软件
#>
[CmdletBinding()]
param(
[string]$SnapshotDir = (Join-Path $env:TEMP 'WinGetSnapshots'),
[int]$MaxRetries = 2,
[switch]$RollbackOnFailure
)

if (-not (Test-Path $SnapshotDir)) {
New-Item -ItemType Directory -Path $SnapshotDir | Out-Null
}

$timestamp = Get-Date -Format 'yyyyMMdd_HHmmss'
$snapshotPath = Join-Path $SnapshotDir "snapshot_$timestamp.json"

# 第一步:创建当前软件版本快照
Write-Host "[1/4] 正在创建软件版本快照..." -ForegroundColor Cyan
$rawOutput = winget list --accept-source-agreements 2>&1
$snapshot = $rawOutput | Where-Object {
$_ -match '^\S' -and
$_ -notmatch '^Name\s+Id' -and
$_ -notmatch '^\d+ packages'
} | ForEach-Object {
$parts = $_ -split '\s{2,}'
if ($parts.Count -ge 3) {
[ordered]@{
Name = $parts[0].Trim()
Id = $parts[1].Trim()
Version = if ($parts.Count -ge 4) {
$parts[2].Trim()
} else { '未知' }
}
}
}

$snapshot | ConvertTo-Json -Depth 3 | Set-Content $snapshotPath -Encoding UTF8
Write-Host "快照已保存: $snapshotPath ($($snapshot.Count) 个软件包)" `
-ForegroundColor Green

# 第二步:扫描可更新的软件
Write-Host "`n[2/4] 正在扫描可更新的软件..." -ForegroundColor Cyan
$upgradeOutput = winget upgrade 2>&1
$upgradable = $upgradeOutput | Where-Object {
$_ -match '^\S' -and
$_ -notmatch '^Name\s+Id' -and
$_ -notmatch '^\d+ upgrades'
} | ForEach-Object {
$parts = $_ -split '\s{2,}'
if ($parts.Count -ge 4) {
[PSCustomObject]@{
Name = $parts[0].Trim()
Id = $parts[1].Trim()
OldVer = $parts[2].Trim()
NewVer = $parts[3].Trim()
}
}
}

if (-not $upgradable) {
Write-Host "所有软件已是最新版本,无需更新" -ForegroundColor Green
return
}

Write-Host "发现 $($upgradable.Count) 个可更新软件包:" -ForegroundColor Yellow
$upgradable | Format-Table Name, Id, OldVer, NewVer -AutoSize

# 第三步:逐个执行更新并记录结果
Write-Host "`n[3/4] 开始安全更新..." -ForegroundColor Cyan
$results = @()
$failed = @()

foreach ($pkg in $upgradable) {
$success = $false
$attempt = 0

while (-not $success -and $attempt -lt $MaxRetries) {
$attempt++
Write-Host " 更新 $($pkg.Id) ($($pkg.OldVer) -> $($pkg.NewVer))" `
-ForegroundColor Yellow -NoNewline
if ($attempt -gt 1) {
Write-Host " [重试 $attempt]" -ForegroundColor DarkYellow -NoNewline
}

winget upgrade --id $pkg.Id --accept-package-agreements `
--accept-source-agreements 2>&1 | Out-Null

if ($LASTEXITCODE -eq 0) {
Write-Host " 完成" -ForegroundColor Green
$success = $true
$results += [PSCustomObject]@{
Id = $pkg.Id
OldVer = $pkg.OldVer
NewVer = $pkg.NewVer
Status = '成功'
Attempts = $attempt
}
} else {
Write-Host " 失败" -ForegroundColor Red
}
}

if (-not $success) {
$failed += $pkg
$results += [PSCustomObject]@{
Id = $pkg.Id
OldVer = $pkg.OldVer
NewVer = $pkg.NewVer
Status = '失败'
Attempts = $MaxRetries
}
}
}

# 第四步:失败时自动回滚
Write-Host "`n[4/4] 更新结果汇总:" -ForegroundColor Cyan
$results | Format-Table Id, OldVer, NewVer, Status, Attempts -AutoSize

if ($failed -and $RollbackOnFailure) {
Write-Host "`n检测到更新失败,正在回滚到快照版本..." -ForegroundColor Red
$snapshotData = Get-Content $snapshotPath -Raw | ConvertFrom-Json
$updatedIds = $results | Where-Object { $_.Status -eq '成功' } |
Select-Object -ExpandProperty Id

foreach ($id in $updatedIds) {
$entry = $snapshotData | Where-Object { $_.Id -eq $id }
if ($entry -and $entry.Version -ne '未知') {
Write-Host " 回滚 $($entry.Id) 到 $($entry.Version)..." `
-ForegroundColor Yellow -NoNewline
winget install --id $entry.Id --version $entry.Version `
--force --accept-package-agreements `
--accept-source-agreements 2>&1 | Out-Null
if ($LASTEXITCODE -eq 0) {
Write-Host " 完成" -ForegroundColor Green
} else {
Write-Host " 回滚失败" -ForegroundColor Red
}
}
}
Write-Host "回滚流程结束" -ForegroundColor Cyan
} elseif ($failed) {
Write-Host "`n提示: 更新失败的软件可使用以下命令手动回滚:" -ForegroundColor Yellow
Write-Host " 回滚快照文件: $snapshotPath" -ForegroundColor DarkYellow
foreach ($f in $failed) {
Write-Host " winget install --id $($f.Id) " + `
"--version $($f.OldVer) --force" -ForegroundColor DarkYellow
}
}

Write-Host "`n快照文件: $snapshotPath" -ForegroundColor DarkGray
Write-Host "可使用以下命令查看历史快照: Get-ChildItem $SnapshotDir" `
-ForegroundColor DarkGray
}

这个脚本的四步流程设计体现了”先快照、再更新、后回滚”的安全策略。第一步将当前所有软件版本写入 JSON 快照文件;第二步扫描可更新软件列表;第三步逐个执行更新,每个软件包失败后自动重试最多 MaxRetries 次;第四步如果开启了 -RollbackOnFailure,则将所有已成功更新的软件回滚到快照版本。快照文件以时间戳命名,保存在临时目录下,可用于后续手动恢复。

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
[1/4] 正在创建软件版本快照...
快照已保存: /tmp/WinGetSnapshots/snapshot_20260409_080000.json (48 个软件包)

[2/4] 正在扫描可更新的软件...
发现 5 个可更新软件包:

Name Id OldVer NewVer
---- -- ------ ------
7-Zip 7zip.7zip 24.09 24.10
Visual Studio Code Microsoft.VisualStudioCode 1.98.2 1.99.0
Git Git.Git 2.48.0 2.49.0
PowerShell Microsoft.PowerShell 7.5.0 7.5.1
Docker Desktop Docker.DockerDesktop 4.35.0 4.36.0

[3/4] 开始安全更新...
更新 7zip.7zip (24.09 -> 24.10) 完成
更新 Microsoft.VisualStudioCode (1.98.2 -> 1.99.0) 完成
更新 Git.Git (2.48.0 -> 2.49.0) 完成
更新 Microsoft.PowerShell (7.5.0 -> 7.5.1) 完成
更新 Docker.DockerDesktop (4.35.0 -> 4.36.0) 失败
更新 Docker.DockerDesktop (4.35.0 -> 4.36.0) [重试 1] 失败
更新 Docker.DockerDesktop (4.35.0 -> 4.36.0) [重试 2] 失败

[4/4] 更新结果汇总:

Id OldVer NewVer Status Attempts
-- ------ ------ ------ --------
7zip.7zip 24.09 24.10 成功 1
Microsoft.VisualStudioCode 1.98.2 1.99.0 成功 1
Git.Git 2.48.0 2.49.0 成功 1
Microsoft.PowerShell 7.5.0 7.5.1 成功 1
Docker.DockerDesktop 4.35.0 4.36.0 失败 2

提示: 更新失败的软件可使用以下命令手动回滚:
回滚快照文件: /tmp/WinGetSnapshots/snapshot_20260409_080000.json
winget install --id Docker.DockerDesktop --version 4.35.0 --force

快照文件: /tmp/WinGetSnapshots/snapshot_20260409_080000.json
可使用以下命令查看历史快照: Get-ChildItem /tmp/WinGetSnapshots

注意事项

  • 版本号解析:部分软件使用非标准版本号格式(如 1.0.0-beta.3),直接用 [version] 类型转换会失败,对比时需要做容错处理。示例代码中对解析失败的版本使用了字符串直接比较
  • YAML 解析依赖:完整的生产级 YAML 解析建议安装 powershell-yaml 模块(Install-Module powershell-yaml),示例中使用了简易的行解析逻辑以避免外部依赖,但不支持复杂的嵌套结构
  • 快照存储位置:示例中快照保存在 $env:TEMP 目录,系统清理时可能被删除。生产环境建议将快照目录设置为持久化路径,如 $HOME\WinGetSnapshots,或推送到远程存储(如 S3、Azure Blob)
  • 回滚局限性winget install --version 回滚依赖软件源保留旧版本安装包,部分软件(如 Chrome、Firefox)的旧版本可能已从源中移除,导致回滚失败。关键软件建议在快照时同时下载离线安装包
  • 并发安全:多台机器同时运行大规模更新可能给内部软件源带来压力,建议在脚本中加入随机延迟(Start-Sleep -Seconds (Get-Random -Max 300))进行错峰
  • 日志与审计:建议将每次更新操作的结果追加写入日志文件(Export-CsvAdd-Content),定期归档,方便事后审计和问题追溯

PowerShell 技能连载 - 打印机与打印服务管理

适用于 PowerShell 5.1 及以上版本

在企业 IT 运维中,打印机管理看似简单,实则是一项高频且容易出问题的工作。新员工入职需要分配部门打印机、打印队列卡住需要及时清理、驱动更新需要在多台终端上统一部署——这些操作如果手动执行,不仅耗时而且容易出错。

Windows Server 2012 引入了 PrintManagement 模块,其中包含 PrinterPrinterDriverPrinterPortPrintJob 等一系列 cmdlet,几乎覆盖了打印机生命周期的所有管理场景。结合 PowerShell 的远程执行能力,可以轻松实现集中化的打印服务管理。

本文将从基础安装配置、批量部署映射、监控故障排查三个层面,介绍如何使用 PowerShell 构建一套完整的打印机管理方案。

打印机基础管理

打印机的基础管理包括安装驱动程序、创建 TCP/IP 打印端口以及添加打印机实例。下面这段脚本演示了完整的流程:

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
# 导入 PrintManagement 模块(Windows 8/Server 2012 及以上自带)
Import-Module PrintManagement

# 定义打印机配置参数
$PrinterConfig = @{
DriverName = 'HP Universal Printing PCL 6'
DriverInf = '\\fileserver\drivers\HP\hpcu250u.inf'
PortName = 'IP_192.168.1.100'
IPAddress = '192.168.1.100'
PrinterName = 'HP-LaserJet-Finance-3F'
Location = '3F-Finance-Dept'
Comment = '财务部3楼共享打印机'
}

# 第一步:安装打印机驱动(如果尚未安装)
$existingDriver = Get-PrinterDriver -Name $PrinterConfig.DriverName -ErrorAction SilentlyContinue

if (-not $existingDriver) {
Add-PrinterDriver -Name $PrinterConfig.DriverName -InfPath $PrinterConfig.DriverInf
Write-Host "已安装驱动: $($PrinterConfig.DriverName)" -ForegroundColor Green
} else {
Write-Host "驱动已存在,跳过安装: $($PrinterConfig.DriverName)" -ForegroundColor Yellow
}

# 第二步:创建 TCP/IP 打印端口
$existingPort = Get-PrinterPort -Name $PrinterConfig.PortName -ErrorAction SilentlyContinue

if (-not $existingPort) {
Add-PrinterPort -Name $PrinterConfig.PortName -PrinterHostAddress $PrinterConfig.IPAddress
Write-Host "已创建端口: $($PrinterConfig.PortName)" -ForegroundColor Green
} else {
Write-Host "端口已存在,跳过创建: $($PrinterConfig.PortName)" -ForegroundColor Yellow
}

# 第三步:添加打印机
$existingPrinter = Get-Printer -Name $PrinterConfig.PrinterName -ErrorAction SilentlyContinue

if (-not $existingPrinter) {
Add-Printer `
-Name $PrinterConfig.PrinterName `
-DriverName $PrinterConfig.DriverName `
-PortName $PrinterConfig.PortName `
-Location $PrinterConfig.Location `
-Comment $PrinterConfig.Comment

Write-Host "已添加打印机: $($PrinterConfig.PrinterName)" -ForegroundColor Green
} else {
Write-Host "打印机已存在: $($PrinterConfig.PrinterName)" -ForegroundColor Yellow
}

# 验证安装结果
Get-Printer -Name $PrinterConfig.PrinterName |
Select-Object Name, DriverName, PortName, Location, PrinterStatus |
Format-List

执行结果示例:

1
2
3
4
5
6
7
8
9
驱动已存在,跳过安装: HP Universal Printing PCL 6
已创建端口: IP_192.168.1.100
已添加打印机: HP-LaserJet-Finance-3F

Name : HP-LaserJet-Finance-3F
DriverName : HP Universal Printing PCL 6
PortName : IP_192.168.1.100
Location : 3F-Finance-Dept
PrinterStatus : Normal

脚本中对每一步都做了幂等检查:如果驱动、端口或打印机已经存在,则跳过而不报错。这种写法非常适合放在配置管理工具(如 DSC、Ansible)中反复执行。

批量部署与映射

在企业环境中,通常需要按部门为用户批量映射打印机。下面的脚本展示了如何根据部门映射关系表,将打印机部署到对应的工作站:

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
# 定义部门与打印机的映射关系
$PrinterMapping = @(
@{
Department = 'Finance'
Printers = @('HP-LaserJet-Finance-3F', 'HP-ColorJet-Finance-3F')
DefaultPrinter = 'HP-LaserJet-Finance-3F'
}
@{
Department = 'HR'
Printers = @('HP-LaserJet-HR-5F')
DefaultPrinter = 'HP-LaserJet-HR-5F'
}
@{
Department = 'Engineering'
Printers = @('HP-LaserJet-Eng-8F', 'Canon-Eng-LargeFormat')
DefaultPrinter = 'HP-LaserJet-Eng-8F'
}
)

# 远程打印服务器
$PrintServer = 'PRINTSRV01'

# 根据当前用户所属部门自动映射打印机
function Install-DepartmentPrinters {
param(
[string[]]$UserDepartments,
[string]$Server = 'PRINTSRV01'
)

foreach ($dept in $UserDepartments) {
$mapping = $PrinterMapping | Where-Object { $_.Department -eq $dept }

if (-not $mapping) {
Write-Warning "未找到部门 '$dept' 的打印机映射配置"
continue
}

Write-Host "`n=== 正在为部门 [$dept] 映射打印机 ===" -ForegroundColor Cyan

foreach ($printerName in $mapping.Printers) {
$fullPath = "\\$Server\$printerName"

try {
# 检查打印机是否已映射
$existing = Get-Printer -Name $fullPath -ErrorAction SilentlyContinue

if (-not $existing) {
Add-Printer -ConnectionName $fullPath
Write-Host " 已映射: $printerName" -ForegroundColor Green
} else {
Write-Host " 已存在: $printerName" -ForegroundColor Yellow
}
} catch {
Write-Host " 映射失败: $printerName - $($_.Exception.Message)" -ForegroundColor Red
}
}

# 设置默认打印机
if ($mapping.DefaultPrinter) {
$defaultPath = "\\$Server\$($mapping.DefaultPrinter)"
$defaultPrinter = Get-Printer -Name $defaultPath -ErrorAction SilentlyContinue

if ($defaultPrinter) {
# 使用 rundll32 设置默认打印机(兼容性好)
$null = rundll32.exe printui.dll,PrintUIEntry /y /n $defaultPath
Write-Host " 已设为默认: $($mapping.DefaultPrinter)" -ForegroundColor Green
}
}
}
}

# 模拟:从 Active Directory 获取用户部门信息
$userDepartment = 'Finance'

# 执行映射
Install-DepartmentPrinters -UserDepartments $userDepartment -Server $PrintServer

# 输出当前已安装的打印机列表
Write-Host "`n=== 当前已映射的打印机 ===" -ForegroundColor Cyan
Get-Printer | Where-Object { $_.Type -eq 'Connection' } |
Select-Object Name, Location, Comment |
Format-Table -AutoSize

执行结果示例:

1
2
3
4
5
6
7
8
9
10
=== 正在为部门 [Finance] 映射打印机 ===
已映射: HP-LaserJet-Finance-3F
已映射: HP-ColorJet-Finance-3F
已设为默认: HP-LaserJet-Finance-3F

=== 当前已映射的打印机 ===
Name Location Comment
---- -------- -------
\\PRINTSRV01\HP-LaserJet-Finance-3F 3F-Finance-Dept 财务部3楼共享打印机
\\PRINTSRV01\HP-ColorJet-Finance-3F 3F-Finance-Dept 财务部3楼彩色打印机

这段脚本可以打包成登录脚本(Logon Script),通过组策略(GPO)分发给所有域用户。用户登录后自动根据其部门信息完成打印机映射和默认设置,无需手动干预。

监控与故障排查

打印队列堵塞是日常运维中常见的故障。下面这段脚本提供了队列监控、作业管理和驱动诊断的能力:

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
# 打印服务器监控脚本
$PrintServer = 'PRINTSRV01'

# 获取所有打印机及其队列状态
Write-Host "=== 打印机状态总览 ===" -ForegroundColor Cyan
$allPrinters = Get-Printer -ComputerName $PrintServer

$allPrinters |
Select-Object Name,
@{N='状态';E={
switch ($_.PrinterStatus) {
0 { 'Paused' }
1 { 'Error' }
2 { 'Pending Deletion' }
3 { 'Paper Jam' }
4 { 'Paper Out' }
5 { 'Manual Feed' }
6 { 'Offline' }
7 { 'IO Active' }
8 { 'Busy' }
9 { 'Printing' }
10 { 'Output Bin Full' }
default { 'Ready' }
}
}},
@{N='队列作业数';E={ (Get-PrintJob -PrinterName $_.Name -ErrorAction SilentlyContinue |
Measure-Object).Count }},
JobCountUntilNotification,
Location |
Format-Table -AutoSize

# 检查卡住的打印作业(超过 30 分钟仍在队列中)
Write-Host "`n=== 超时打印作业检查 ===" -ForegroundColor Cyan
$threshold = (Get-Date).AddMinutes(-30)
$stuckJobs = @()

foreach ($printer in $allPrinters) {
$jobs = Get-PrintJob -PrinterName $printer.Name -ErrorAction SilentlyContinue

foreach ($job in $jobs) {
if ($job.SubmitTime -and $job.SubmitTime -lt $threshold) {
$stuckJobs += [PSCustomObject]@{
PrinterName = $printer.Name
JobId = $job.Id
Document = $job.DocumentName
SubmitTime = $job.SubmitTime
UserName = $job.UserName
Size(KB) = [math]::Round($job.Size / 1KB, 2)
}
}
}
}

if ($stuckJobs) {
Write-Host "发现 $($stuckJobs.Count) 个超时作业:" -ForegroundColor Yellow
$stuckJobs | Format-Table -AutoSize

# 自动清理超时作业
$confirm = Read-Host "是否清理这些超时作业?(Y/N)"
if ($confirm -eq 'Y') {
foreach ($job in $stuckJobs) {
Remove-PrintJob -PrinterName $job.PrinterName -ID $job.JobId
Write-Host " 已删除作业: $($job.Document) (ID: $($job.JobId))" -ForegroundColor Green
}
}
} else {
Write-Host "没有发现超时的打印作业" -ForegroundColor Green
}

# 驱动兼容性检查
Write-Host "`n=== 打印机驱动诊断 ===" -ForegroundColor Cyan
$drivers = Get-PrinterDriver -ComputerName $PrintServer

$drivers |
Select-Object Name,
@{N='环境';E={ $_.PrinterEnvironment -join ', ' }},
@{N='驱动版本';E={ $_.DriverVersion }},
@{N='依赖文件数';E={ ($_.DependentFiles | Measure-Object).Count }} |
Format-Table -AutoSize

# 检查是否有驱动文件缺失
foreach ($driver in $drivers) {
$driverPath = Join-Path $env:SystemRoot "System32\spool\drivers"
$missingFiles = @()

if ($driver.DependentFiles) {
foreach ($file in $driver.DependentFiles) {
$fullPath = Join-Path $driverPath $file
if (-not (Test-Path $fullPath)) {
$missingFiles += $file
}
}
}

if ($missingFiles.Count -gt 0) {
Write-Host "驱动 '$($driver.Name)' 缺失 $($missingFiles.Count) 个文件:" -ForegroundColor Red
$missingFiles | ForEach-Object { Write-Host " - $_" -ForegroundColor Red }
}
}

执行结果示例:

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
=== 打印机状态总览 ===
Name 状态 队列作业数 JobCountUntilNotification Location
---- ---- ---------- ------------------------ --------
HP-LaserJet-Finance-3F Ready 2 10 3F-Finance-Dept
HP-ColorJet-Finance-3F Ready 0 10 3F-Finance-Dept
HP-LaserJet-HR-5F Offline 5 10 5F-HR-Dept
HP-LaserJet-Eng-8F Ready 0 10 8F-Engineering
Canon-Eng-LargeFormat Ready 1 10 8F-Engineering

=== 超时打印作业检查 ===
发现 3 个超时作业:
PrinterName JobId Document SubmitTime UserName Size(KB)
------------ ----- -------- ----------- -------- --------
HP-LaserJet-Finance-3F 3 Q1-Report.xlsx 2026/3/4 10:15:22 CONTOSO\zhangsan 512.5
HP-LaserJet-HR-5F 12 payroll.pdf 2026/3/4 09:30:11 CONTOSO\lisi 1024.0
HP-LaserJet-HR-5F 14 onboarding.docx 2026/3/4 10:05:45 CONTOSO\wangwu 256.75

是否清理这些超时作业?(Y/N): Y
已删除作业: Q1-Report.xlsx (ID: 3)
已删除作业: payroll.pdf (ID: 12)
已删除作业: onboarding.docx (ID: 14)

=== 打印机驱动诊断 ===
Name 环境 驱动版本 依赖文件数
---- ---- -------- ----------
HP Universal Printing PCL 6 Windows x64, Windows x86 6.0.1 24
Canon Generic Plus UFR II Driver Windows x64 3.1.0 18

这个监控脚本可以集成到定时任务中,每隔一段时间自动扫描打印服务器,发现超时作业和驱动异常时发送告警邮件或钉钉/企业微信通知。

注意事项

  • PrintManagement 模块仅在 Windows 8 / Windows Server 2012 及以上版本中可用。如果需要在更旧的系统上管理打印机,需要通过 WMI(Win32_Printer 类)或 rundll32 printui.dll 命令实现。
  • 使用 Add-Printer -ConnectionName 映射网络打印机时,需要确保 Print Server 的共享权限正确配置,否则会出现”拒绝访问”错误。建议在域环境中通过组策略统一管理权限。
  • 清理打印作业前务必确认作业确实卡住(而非用户正在打印大文件)。可以通过文件大小和提交时间综合判断,避免误删正在执行的大文档打印任务。
  • 打印机驱动安装涉及 INF 文件路径。如果驱动存放在网络共享路径上,需要确保执行脚本的账户对该共享有读取权限,且路径使用 UNC 格式(如 \\fileserver\drivers\...)。
  • 远程管理打印服务器时(使用 -ComputerName 参数),需要确保 WinRM 服务已启用,且目标服务器防火墙放行了 RPC 动态端口范围。跨子网管理时可能需要配置 WinRM 的 TrustedHosts。
  • 在大规模环境中(数百台打印机以上),建议将打印机配置信息存储在 CMDB 或数据库中,脚本从数据源读取配置而非硬编码。这样可以与企业的 IP 地址管理系统(IPAM)联动,实现打印机全生命周期管理。

PowerShell 技能连载 - 组策略自动化管理

适用于 PowerShell 5.1 及以上版本,需要 GroupPolicy 模块

组策略(Group Policy)是 Windows 域环境中管理配置和安全设置的核心机制。通过 GPO,管理员可以集中控制数千台计算机的注册表设置、安全策略、软件部署和脚本执行。在大型企业环境中,GPO 数量往往达到数百甚至上千个,涵盖安全基线、合规要求、桌面标准化等多个维度。

手动管理这些 GPO 不仅耗时,而且极易出错——一个配置遗漏可能导致全公司的安全防线出现缺口。传统的图形界面操作(GPMC)在面对批量创建、迁移、审计等场景时显得力不从心,尤其当组织需要在不同域或林之间迁移策略时,手动操作几乎不可行。

PowerShell 的 GroupPolicy 模块提供了完整的 GPO 生命周期管理能力,从创建、编辑、备份、还原到合规报告,全部可以通过脚本自动化完成。结合 ActiveDirectory 模块,还能实现基于组织单元(OU)结构的智能策略分配和变更追踪,让组策略管理变得可重复、可审计、可回溯。

GPO 基础管理

以下脚本展示了 GPO 的创建、链接、备份和还原等基础操作,这些是日常管理中最常用的功能。

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
# 导入 GroupPolicy 模块
Import-Module GroupPolicy

# 定义常用变量
$Domain = "contoso.com"
$BackupPath = "C:\GPOBackup\$(Get-Date -Format 'yyyyMMdd')"

# 创建新的 GPO
$GPO = New-GPO -Name "Security Baseline 2026" -Comment "年度安全基线策略"

# 将 GPO 链接到指定的 OU,并设置优先级
New-GPLink -Name "Security Baseline 2026" -Target "OU=Servers,DC=contoso,DC=com" -LinkEnabled Yes -Order 1

# 禁用 GPO 的用户配置部分(仅保留计算机配置)
$GPO | Set-GPO -GpoStatus UserSettingsDisabled

# 创建备份目录并备份所有 GPO
if (-not (Test-Path $BackupPath)) {
New-Item -Path $BackupPath -ItemType Directory -Force | Out-Null
}

$BackupReport = Backup-GPO -All -Path $BackupPath
$BackupReport | Select-Object DisplayName, GpoId, BackupId, Timestamp |
Format-Table -AutoSize

# 导出备份清单为 CSV,便于后续还原时查找
$BackupReport | Export-Csv -Path "$BackupPath\BackupInventory.csv" -NoTypeInformation -Encoding UTF8

# 从备份还原单个 GPO(模拟灾难恢复场景)
$LatestBackup = $BackupReport | Where-Object { $_.DisplayName -eq "Security Baseline 2026" }
Restore-GPO -Name "Security Baseline 2026" -Path $BackupPath -BackupId $LatestBackup.BackupId

# 查看域中所有 GPO 的概览
Get-GPO -All | Select-Object DisplayName, GpoStatus, CreationTime, ModificationTime |
Sort-Object ModificationTime -Descending |
Format-Table -AutoSize

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
DisplayName           GpoId                                 BackupId                             Timestamp
------------ ----- -------- ---------
Security Baseline 2026 3a1b2c4d-5e6f-... a7b8c9d0-e1f2-... 2026/2/24 10:30:15
Domain Controllers f3e2d1c0-b9a8-... 1a2b3c4d-5e6f-... 2026/2/20 14:22:10
Default Domain Policy 7f8e9d0a-1b2c-... 3c4d5e6f-7a8b-... 2026/1/15 09:12:00

DisplayName GpoStatus CreationTime ModificationTime
------------ --------- ------------ ----------------
Security Baseline 2026 UserSettingsDisabled 2026/2/24 10:30:00 2026/2/24 10:32:00
Default Domain Policy AllSettingsEnabled 2024/3/1 08:00:00 2026/1/15 09:12:00
Domain Controllers AllSettingsEnabled 2024/3/1 08:00:00 2026/2/20 14:22:10

策略设置编辑

创建 GPO 只是第一步,实际管理工作中有大量策略项需要配置。以下脚本演示了如何通过注册表首选项、安全设置和脚本部署来编辑 GPO 内容。

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
# --- 注册表首选项:通过 GPO 推送注册表项 ---
$GPOName = "Security Baseline 2026"
$Domain = "contoso.com"

# 设置注册表项:禁用 SMBv1(安全加固)
Set-GPPrefRegistryValue -Name $GPOName -Context Computer -Action Create `
-Key "SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters" `
-ValueName "SMB1" -Value 0 -Type DWord -Order 1

# 设置注册表项:启用 PowerShell 脚本块日志记录
Set-GPPrefRegistryValue -Name $GPOName -Context Computer -Action Create `
-Key "SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging" `
-ValueName "EnableScriptBlockLogging" -Value 1 -Type DWord -Order 2

# 设置注册表项:配置 Windows Defender 实时保护
Set-GPPrefRegistryValue -Name $GPOName -Context Computer -Action Create `
-Key "SOFTWARE\Policies\Microsoft\Windows Defender\Real-Time Protection" `
-ValueName "DisableRealtimeMonitoring" -Value 0 -Type DWord -Order 3

# --- 安全设置:通过 ini 文件方式配置账户策略 ---
# 生成安全模板 INF 文件
$SecTemplatePath = "C:\Temp\SecurityTemplate.inf"
$InfContent = @"
[Unicode]
Unicode=yes
[Version]
signature=`"`$CHICAGO`$`"
Revision=1
[Account Policies]
[Password History]
MaximumPasswordAge = 90
MinimumPasswordAge = 1
MinimumPasswordLength = 14
PasswordComplexity = 1
[Lockout]
LockoutDuration = 30
LockoutBadCount = 5
ResetLockoutCount = 30
"@

Set-Content -Path $SecTemplatePath -Value $InfContent -Encoding Unicode

# --- 脚本部署:配置启动脚本 ---
$ScriptName = "Install-EndpointAgent.ps1"
$ScriptContent = @'
# 端点代理安装脚本 - 由组策略推送执行
$AgentPath = "\\contoso.com\SYSVOL\contoso.com\scripts\EndpointAgent.msi"
$LogPath = "C:\Logs\AgentInstall.log"

if (-not (Get-Service -Name "EndpointAgent" -ErrorAction SilentlyContinue)) {
Start-Process msiexec.exe -ArgumentList "/i `"$AgentPath`" /qn /l*v `"$LogPath`"" -Wait
Write-Output "$(Get-Date) - Endpoint Agent installed successfully" | Out-File $LogPath -Append
}
'@

# 将脚本保存到 GPO 的启动脚本目录
$GPO = Get-GPO -Name $GPOName
$StartupScriptFolder = "\\$Domain\SYSVOL\$Domain\Policies\{$($GPO.Id)}\Machine\Scripts\Startup"
if (-not (Test-Path $StartupScriptFolder)) {
New-Item -Path $StartupScriptFolder -ItemType Directory -Force | Out-Null
}
Set-Content -Path "$StartupScriptFolder\$ScriptName" -Value $ScriptContent -Encoding UTF8

Write-Host "策略设置编辑完成:注册表首选项 3 项、安全模板 1 份、启动脚本 1 个"

执行结果示例:

1
策略设置编辑完成:注册表首选项 3 项、安全模板 1 份、启动脚本 1 

合规审计与报告

策略配置完成后,持续的合规审计至关重要。以下脚本实现了 RSOP(策略结果集)分析、基线对比和变更追踪功能,帮助管理员及时发现策略漂移。

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
# --- RSOP 分析:获取目标计算机的实际策略应用结果 ---
$ComputerName = "SRV-DC01.contoso.com"

# 生成 GPO 报告(HTML 格式)
$ReportPath = "C:\Reports\GPOReport_$(Get-Date -Format 'yyyyMMdd_HHmmss').html"
Get-GPOReport -All -ReportType Html -Path $ReportPath
Write-Host "GPO 报告已生成: $ReportPath"

# 获取指定计算机的 RSOP 报告
$RsopReport = "C:\Reports\RSOP_$ComputerName`_$(Get-Date -Format 'yyyyMMdd').html"
gpresult /s $ComputerName /h $RsopReport /f 2>$null
Write-Host "RSOP 报告已生成: $RsopReport"

# --- 基线对比:检测 GPO 是否偏离已知良好配置 ---
$BaselinePath = "C:\GPOBaseline"
$CurrentBackupPath = "C:\GPOBackup\$(Get-Date -Format 'yyyyMMdd')"

# 比较基线和当前备份中的 GPO 数量
$BaselineGPOs = Get-ChildItem -Path $BaselinePath -Directory
$CurrentGPOs = Get-ChildItem -Path $CurrentBackupPath -Directory

$Missing = $BaselineGPOs.Name | Where-Object { $_ -notin $CurrentGPOs.Name }
$New = $CurrentGPOs.Name | Where-Object { $_ -notin $BaselineGPOs.Name }

if ($Missing) {
Write-Warning "以下基线 GPO 在当前备份中缺失: $($Missing -join ', ')"
}
if ($New) {
Write-Host "发现新增 GPO: $($New -join ', ')" -ForegroundColor Yellow
}

# --- 变更追踪:监控 GPO 的修改时间和设置变更 ---
$AuditLog = "C:\Reports\GPO_AuditLog_$(Get-Date -Format 'yyyyMMdd').csv"

# 获取所有 GPO 的当前状态快照
$Snapshot = Get-GPO -All | ForEach-Object {
$Links = (Get-GPOReport -Name $_.DisplayName -ReportType Xml)
$LinkCount = ([xml]$Links).GPO.LinksTo | Measure-Object | Select-Object -ExpandProperty Count

[PSCustomObject]@{
GPOName = $_.DisplayName
GpoId = $_.Id
Status = $_.GpoStatus
LinkCount = $LinkCount
ComputerEnabled = ($_.GpoStatus -ne 'ComputerSettingsDisabled' -and $_.GpoStatus -ne 'AllSettingsDisabled')
UserEnabled = ($_.GpoStatus -ne 'UserSettingsDisabled' -and $_.GpoStatus -ne 'AllSettingsDisabled')
ModifiedTime = $_.ModificationTime
AuditTime = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
}
}

$Snapshot | Export-Csv -Path $AuditLog -NoTypeInformation -Encoding UTF8

# 标记最近 7 天内修改过的 GPO
$RecentChanges = $Snapshot | Where-Object { $_.ModifiedTime -gt (Get-Date).AddDays(-7) }
if ($RecentChanges) {
Write-Host "`n最近 7 天内修改的 GPO:" -ForegroundColor Cyan
$RecentChanges | Format-Table GPOName, ModifiedTime, LinkCount, Status -AutoSize
} else {
Write-Host "`n最近 7 天内无 GPO 变更" -ForegroundColor Green
}

Write-Host "`n审计快照已保存: $AuditLog"

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
GPO 报告已生成: C:\Reports\GPOReport_20260224_103500.html
RSOP 报告已生成: C:\Reports\RSOP_SRV-DC01.contoso.com_20260224.html
发现新增 GPO: Security Baseline 2026

最近 7 天内修改的 GPO:

GPOName ModifiedTime LinkCount Status
------- ------------- --------- ------
Security Baseline 2026 2026/2/24 10:32:00 1 UserSettingsDisabled
Domain Controllers 2026/2/20 14:22:10 1 AllSettingsEnabled

审计快照已保存: C:\Reports\GPO_AuditLog_20260224.csv

注意事项

  1. 权限要求:管理 GPO 需要域管理员或 Group Policy Creator Owners 组的成员权限。在生产环境中建议使用最小特权原则,为 GPO 管理员分配专门的委派权限,而非直接使用 Domain Admins 账户。

  2. 备份策略:建议建立定期自动备份机制,每次变更 GPO 前都执行 Backup-GPO。备份文件应存储在独立的文件服务器上,并纳入常规数据保护方案。备份 ID(BackupId)是还原时的关键标识,务必通过 CSV 清单妥善保管。

  3. 测试先行:新 GPO 或重大变更应先在测试 OU 上验证效果,确认无误后再推广到生产 OU。可以使用 New-GPLink-WhatIf 参数预览链接操作,或者先将 GPO 链接设置为禁用状态(-LinkEnabled No),验证后再启用。

  4. WMI 筛选器:复杂的策略分发场景可以结合 WMI 筛选器实现条件化应用,例如只对特定操作系统版本或硬件类型的计算机生效。但过多的 WMI 筛选器会影响组策略处理性能,建议控制在合理范围内。

  5. 脚本块日志安全:通过 GPO 启用 PowerShell 脚本块日志记录时,会产生大量日志数据。需要提前规划日志收集和存储方案(如 Windows Event Forwarding 或 SIEM 集成),避免本地日志溢出导致关键审计数据丢失。

  6. 跨域迁移:使用 Backup-GPOImport-GPO 进行跨域迁移时,需要注意安全主体(用户、组)的 SID 映射问题。迁移表格(Migration Table)是解决这一问题的关键工具,建议在迁移前使用 New-MigrationTable 生成并仔细校验映射关系。

PowerShell 技能连载 - 注册表管理与安全审计

适用于 PowerShell 5.1 及以上版本

Windows 注册表是操作系统配置的核心存储库,几乎所有系统设置、应用程序配置和安全策略都记录在注册表中。在企业环境中,系统管理员需要定期检查注册表中的安全设置,确保符合合规要求。传统的 regedit.exe 图形界面工具虽然直观,但无法满足批量操作和自动化审计的需求。

PowerShell 提供了完整的注册表操作能力,通过注册表提供程序(Registry Provider)可以直接像操作文件系统一样浏览和修改注册表。结合 .NET 类库,还可以管理注册表项的 ACL(访问控制列表),实现细粒度的权限审计。本文将从基础操作、安全审计和变更追踪三个方面,介绍如何用 PowerShell 高效管理注册表。

值得注意的是,注册表操作具有较高的风险性,错误的修改可能导致系统不稳定甚至无法启动。因此在生产环境中,建议先导出备份再进行修改,并使用 -WhatIf 参数进行预演。

注册表基础操作与批量配置

PowerShell 的注册表提供程序将注册表映射为驱动器,可以直接使用 Get-ItemSet-ItemProperty 等命令操作。下面是一些常见的批量配置场景:

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
# 查看可用的注册表驱动器
Get-PSDrive -PSProvider Registry | Format-Table Name, Root

# 读取 Windows 当前版本信息
$osInfo = Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion'
Write-Host "系统版本: $($osInfo.ProductName)"
Write-Host "当前版本号: $($osInfo.DisplayVersion)"
Write-Host "构建号: $($osInfo.CurrentBuild)"

# 批量修改远程桌面相关注册表项
$rdpSettings = @{
'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server' = @{
fDenyTSConnections = 0 # 允许远程桌面连接
}
'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' = @{
UserAuthentication = 1 # 要求网络级别身份验证
}
}

foreach ($path in $rdpSettings.Keys) {
foreach ($name in $rdpSettings[$path].Keys) {
$value = $rdpSettings[$path][$name]
Set-ItemProperty -Path $path -Name $name -Value $value
Write-Host "已设置 $path\$name = $value"
}
}

# 搜索注册表中的特定键值(以搜索所有安装的 .NET 版本为例)
$dotnetKey = 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full'
if (Test-Path $dotnetKey) {
$release = (Get-ItemProperty -Path $dotnetKey -Name Release -ErrorAction SilentlyContinue).Release
$versions = @{
528040 = '.NET Framework 4.8'
533320 = '.NET Framework 4.8.1'
}
$found = $versions.Keys | Where-Object { $release -ge $_ } | Sort-Object -Descending | Select-Object -First 1
Write-Host ".NET 版本: $($versions[$found]) (Release=$release)"
}

# 导出注册表项到 .reg 文件(使用 reg.exe)
$tempRegFile = "$env:TEMP\firewall_backup.reg"
reg export 'HKLM\SYSTEM\CurrentControlSet\Services\SharedAccess\Parameters\FirewallPolicy' $tempRegFile /y
Write-Host "防火墙注册表已导出到: $tempRegFile"

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Name Root
---- ----
HKCR HKEY_CLASSES_ROOT
HKCU HKEY_CURRENT_USER
HKLM HKEY_LOCAL_MACHINE
HKU HKEY_USERS

系统版本: Windows 11 Pro
当前版本号: 24H2
构建号: 26100

已设置 HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\fDenyTSConnections = 0
已设置 HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp\UserAuthentication = 1

.NET 版本: .NET Framework 4.8 (Release=528040)

防火墙注册表已导出到: C:\Users\ADMINI~1\AppData\Local\Temp\firewall_backup.reg

安全基线审计

在企业环境中,安全基线审计是确保系统合规的关键环节。以下脚本检查 UAC(用户账户控制)、远程桌面策略和防火墙规则等核心安全设置,并生成审计报告:

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
function Invoke-RegistrySecurityAudit {
$auditResults = @()

# 定义安全基线检查项
$baselineChecks = @(
@{
Name = 'UAC - 管理员批准模式'
Path = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System'
Key = 'EnableLUA'
Expected = 1
Severity = '高'
},
@{
Name = 'UAC - 提示提升时切换到安全桌面'
Path = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System'
Key = 'PromptOnSecureDesktop'
Expected = 1
Severity = '高'
},
@{
Name = '远程桌面 - 空密码限制'
Path = 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa'
Key = 'LimitBlankPasswordUse'
Expected = 1
Severity = '高'
},
@{
Name = '远程桌面 - NLA 要求'
Path = 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp'
Key = 'UserAuthentication'
Expected = 1
Severity = '中'
},
@{
Name = '防火墙 - 域配置文件启用状态'
Path = 'HKLM:\SYSTEM\CurrentControlSet\Services\SharedAccess\Parameters\FirewallPolicy\DomainProfile'
Key = 'EnableFirewall'
Expected = 1
Severity = '高'
},
@{
Name = '防火墙 - 标准配置文件启用状态'
Path = 'HKLM:\SYSTEM\CurrentControlSet\Services\SharedAccess\Parameters\FirewallPolicy\StandardProfile'
Key = 'EnableFirewall'
Expected = 1
Severity = '高'
},
@{
Name = '审核策略 - 对象访问审计'
Path = 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa'
Key = 'AuditBaseObjects'
Expected = 1
Severity = '中'
},
@{
Name = 'WinRM - 允许远程服务器管理'
Path = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WinRM\Service'
Key = 'AllowAutoConfig'
Expected = 1
Severity = '低'
}
)

foreach ($check in $baselineChecks) {
$actual = $null
$status = '跳过'

if (Test-Path $check.Path) {
$actual = (Get-ItemProperty -Path $check.Path -Name $check.Key -ErrorAction SilentlyContinue).($check.Key)
if ($null -ne $actual) {
$status = if ($actual -eq $check.Expected) { '通过' } else { '不合规' }
} else {
$status = '未配置'
}
} else {
$status = '路径不存在'
}

$auditResults += [PSCustomObject]@{
检查项 = $check.Name
预期值 = $check.Expected
实际值 = if ($null -ne $actual) { $actual } else { 'N/A' }
状态 = $status
严重级别 = $check.Severity
}
}

# 输出审计报告
Write-Host "`n========== 注册表安全基线审计报告 =========="
Write-Host "审计时间: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
Write-Host "计算机名: $env:COMPUTERNAME"
Write-Host "==============================================`n"

$auditResults | Format-Table -AutoSize

# 统计摘要
$compliant = ($auditResults | Where-Object { $_.状态 -eq '通过' }).Count
$total = $auditResults.Count
$nonCompliant = $auditResults | Where-Object { $_.状态 -eq '不合规' }

Write-Host "合规项: $compliant / $total"

if ($nonCompliant) {
Write-Host "`n不合规项详情:" -ForegroundColor Red
foreach ($item in $nonCompliant) {
Write-Host " [$($item.严重级别)] $($item.检查项): 预期=$($item.预期值), 实际=$($item.实际值)"
}
}

return $auditResults
}

Invoke-RegistrySecurityAudit

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
========== 注册表安全基线审计报告 ==========
审计时间: 2026-02-04 10:30:15
计算机名: SERVER01
==============================================

检查项 预期值 实际值 状态 严重级别
------ ------ ------ ---- --------
UAC - 管理员批准模式 1 1 通过 高
UAC - 提示提升时切换到安全桌面 1 1 通过 高
远程桌面 - 空密码限制 1 1 通过 高
远程桌面 - NLA 要求 1 0 不合规 中
防火墙 - 域配置文件启用状态 1 1 通过 高
防火墙 - 标准配置文件启用状态 1 1 通过 高
审核策略 - 对象访问审计 1 0 不合规 中
WinRM - 允许远程服务器管理 1 N/A 未配置 低

合规项: 5 / 8

不合规项详情:
[中] 远程桌面 - NLA 要求: 预期=1, 实际=0
[中] 审核策略 - 对象访问审计: 预期=1, 实际=0

注册表监控与变更追踪

在安全运维中,需要监控关键注册表项的变更,并管理其访问权限。以下脚本实现了 ACL 管理、变更检测和基线对比功能:

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 Protect-RegistryKey {
param(
[Parameter(Mandatory)]
[string]$Path,

[ValidateSet('Read', 'Write', 'FullControl')]
[string]$Permission = 'Read',

[string]$Account = 'BUILTIN\Users'
)

if (-not (Test-Path $Path)) {
Write-Warning "注册表路径不存在: $Path"
return
}

# 获取当前 ACL
$acl = Get-Acl -Path $Path

# 定义权限映射
$rightsMap = @{
Read = 'ReadKey'
Write = 'WriteKey'
FullControl = 'FullControl'
}

$rule = New-Object System.Security.AccessControl.RegistryAccessRule(
$Account,
$rightsMap[$Permission],
'ContainerInherit,ObjectInherit',
'None',
'Allow'
)

$acl.AddAccessRule($rule)
Set-Acl -Path $Path -AclObject $acl
Write-Host "已为 $Account 授予 $Permission 权限: $Path"
}

function New-RegistryBaseline {
param(
[string[]]$Paths = @(
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System'
'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa'
'HKLM:\SYSTEM\CurrentControlSet\Services\SharedAccess\Parameters\FirewallPolicy'
),
[string]$OutputPath = "$env:TEMP\registry_baseline.json"
)

$baseline = @{
CreatedAt = Get-Date -Format 'o'
Computer = $env:COMPUTERNAME
Entries = @{}
}

foreach ($path in $Paths) {
if (-not (Test-Path $path)) { continue }

$props = Get-ItemProperty -Path $path -ErrorAction SilentlyContinue
$entry = @{}

# 获取所有属性(排除默认的 PS* 属性)
$props.PSObject.Properties |
Where-Object { $_.Name -notmatch '^PS' } |
ForEach-Object {
$entry[$_.Name] = $_.Value
}

$baseline.Entries[$path] = $entry
}

$baseline | ConvertTo-Json -Depth 10 | Set-Content -Path $OutputPath -Encoding UTF8
Write-Host "基线已保存到: $OutputPath"
return $OutputPath
}

function Compare-RegistryBaseline {
param(
[Parameter(Mandatory)]
[string]$BaselineFile
)

if (-not (Test-Path $BaselineFile)) {
Write-Error "基线文件不存在: $BaselineFile"
return
}

$baseline = Get-Content -Path $BaselineFile -Raw | ConvertFrom-Json
Write-Host "`n========== 注册表变更检测报告 =========="
Write-Host "基线创建时间: $($baseline.CreatedAt)"
Write-Host "检查时间: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
Write-Host "========================================`n"

$changes = @()

foreach ($path in $baseline.Entries.PSObject.Properties.Name) {
if (-not (Test-Path $path)) {
$changes += [PSCustomObject]@{
路径 = $path
变更类型 = '已删除'
属性 = 'N/A'
原值 = 'N/A'
新值 = 'N/A'
}
continue
}

$current = Get-ItemProperty -Path $path -ErrorAction SilentlyContinue
$savedProps = $baseline.Entries.$path

foreach ($prop in $savedProps.PSObject.Properties) {
$currentValue = $current.($prop.Name)
$savedValue = $prop.Value

if ($null -ne $currentValue -and "$currentValue" -ne "$savedValue") {
$changes += [PSCustomObject]@{
路径 = $path
变更类型 = '已修改'
属性 = $prop.Name
原值 = $savedValue
新值 = $currentValue
}
}
}
}

if ($changes.Count -eq 0) {
Write-Host "未检测到任何变更,所有配置与基线一致。" -ForegroundColor Green
} else {
Write-Host "检测到 $($changes.Count) 处变更:" -ForegroundColor Yellow
$changes | Format-Table -AutoSize
}

return $changes
}

# 使用示例:创建基线并检测变更
$baselineFile = New-RegistryBaseline
Compare-RegistryBaseline -BaselineFile $baselineFile

# 限制 Users 组对关键注册表项的写入权限
Protect-RegistryKey -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' -Permission 'Read' -Account 'BUILTIN\Users'

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
基线已保存到: C:\Users\ADMINI~1\AppData\Local\Temp\registry_baseline.json

========== 注册表变更检测报告 ==========
基线创建时间: 2026-02-04T10:30:15.0000000+08:00
检查时间: 2026-02-04 14:22:08
========================================

检测到 2 处变更:

路径 变更类型 属性 原值 新值
---- -------- ---- ---- ----
HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System 已修改 EnableLUA 1 0
HKLM:\SYSTEM\CurrentControlSet\Control\Lsa 已修改 LimitBlankPasswordUse 1 0

已为 BUILTIN\Users 授予 Read 权限: HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System

注意事项

  1. 务必备份后再修改:在生产环境修改注册表前,务必使用 reg export 导出相关键值或创建系统还原点,以便在出现问题时快速恢复。

  2. 使用 -WhatIf 参数Set-ItemPropertyRemove-Item 等危险操作建议先加上 -WhatIf 参数预演,确认影响范围后再正式执行。

  3. 以管理员权限运行HKLM: 下的大部分操作需要以管理员身份运行 PowerShell,否则会遇到”拒绝访问”错误。可以使用 #Requires -RunAsAdministrator 预声明。

  4. 注意 32/64 位重定向:在 64 位系统上,部分注册表路径存在重定向机制(如 Wow6432Node),操作时需注意是否需要指定 -PSPath 'Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\...' 来绕过重定向。

  5. 远程操作需启用 WinRM:对远程计算机执行注册表操作时,目标机器需要启用 WinRM 服务,并且防火墙放行相应端口。可使用 Invoke-Command 配合 -ComputerName 参数。

  6. 审计脚本定期运行:建议将安全基线审计脚本配置为 Windows 计划任务或通过 CI/CD 流水线定期执行,及时发现配置漂移。导出的 JSON 基线文件应纳入版本控制系统进行追踪。

PowerShell 技能连载 - WinGet 包管理自动化

适用于 PowerShell 7.0 及以上版本,需要 winget CLI

背景介绍

在企业 IT 运维中,软件安装和更新是一项高频且重复的工作。新员工入职时需要配置开发环境,安全合规要求定期更新终端上的软件版本,这些都给运维团队带来了巨大的工作量。传统的手动安装方式不仅效率低下,还容易出现版本不一致、配置遗漏等问题。

WinGet(Windows Package Manager)是微软官方推出的命令行包管理工具,从 Windows 10 1709 版本开始内置。它提供了类似 Linux 下 apt 或 yum 的软件管理体验,支持搜索、安装、更新和卸载应用程序。结合 PowerShell 的脚本能力,我们可以将 WinGet 的操作封装为可复用的自动化流程。

本文将通过三个实际场景,演示如何使用 PowerShell 调用 WinGet 实现软件搜索安装、批量部署以及自动化更新,帮助运维人员构建完整的 Windows 软件生命周期管理体系。

WinGet 基础操作

WinGet 提供了丰富的子命令来管理软件包。下面的脚本展示了最常用的三个操作:搜索软件包、安装软件以及列出已安装的软件。

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
# 搜索软件包
function Find-WinGetPackage {
param(
[Parameter(Mandatory)]
[string]$Keyword
)

$result = winget search $Keyword --accept-source-agreements 2>&1
$result
}

# 安装软件包
function Install-WinGetPackage {
param(
[Parameter(Mandatory)]
[string]$PackageId,

[switch]$Silent
)

$args = @(
'install'
'--id', $PackageId
'--accept-source-agreements'
'--accept-package-agreements'
)

if ($Silent) {
$args += '--silent'
}

Write-Host "正在安装: $PackageId" -ForegroundColor Cyan
winget @args 2>&1
}

# 列出已安装的软件
function Get-WinGetInstalled {
$output = winget list --accept-source-agreements 2>&1
$output
}

# 示例:搜索 VS Code
Find-WinGetPackage -Keyword "Visual Studio Code"

# 示例:安装 VS Code(静默模式)
Install-WinGetPackage -PackageId 'Microsoft.VisualStudioCode' -Silent

# 示例:查看已安装的软件列表
Get-WinGetInstalled

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
名称                         ID                                版本        来源
---------------------------------------------------------------------------
Visual Studio Code Microsoft.VisualStudioCode 1.96.2 winget
Visual Studio Code Insiders Microsoft.VisualStudioCode.Insiders 1.97.0 winget

正在安装: Microsoft.VisualStudioCode
已成功完成安装

名称 ID 版本
---------------------------------------------------------------------------
Visual Studio Code Microsoft.VisualStudioCode 1.96.2
Git for Windows Git.Git 2.47.1
PowerShell 7 Microsoft.PowerShell 7.4.6

通过上面的函数封装,我们可以将 WinGet 的命令行操作转化为结构化的 PowerShell 函数,便于后续在脚本中组合调用。--accept-source-agreements--accept-package-agreements 参数可以跳过交互式确认,实现无人值守安装。

批量安装与软件清单管理

在企业环境中,我们通常需要根据角色或团队批量安装一组软件。下面的脚本展示了如何通过 JSON 清单文件管理软件列表,并实现一键批量部署。

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
# 定义软件清单(可以保存为外部 JSON 文件)
$softwareList = @(
@{
Id = 'Microsoft.VisualStudioCode'
Name = 'Visual Studio Code'
Category = 'Editor'
}
@{
Id = 'Git.Git'
Name = 'Git for Windows'
Category = 'VCS'
}
@{
Id = 'Microsoft.PowerShell'
Name = 'PowerShell 7'
Category = 'Runtime'
}
@{
Id = 'OpenJS.NodeJS.LTS'
Name = 'Node.js LTS'
Category = 'Runtime'
}
@{
Id = 'Docker.DockerDesktop'
Name = 'Docker Desktop'
Category = 'Container'
}
) | ConvertTo-Json -Depth 3

# 将清单保存到文件
$listPath = Join-Path $HOME 'winget-software-list.json'
$softwareList | Out-File -FilePath $listPath -Encoding utf8
Write-Host "软件清单已保存到: $listPath" -ForegroundColor Green

# 从文件加载并批量安装
function Install-WinGetFromList {
param(
[Parameter(Mandatory)]
[string]$ListFile,

[string[]]$Categories
)

$packages = Get-Content $ListFile -Raw | ConvertFrom-Json

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

$total = $packages.Count
$success = 0
$failed = 0

foreach ($pkg in $packages) {
Write-Host "`n[$($success + $failed + 1)/$total] 安装: $($pkg.Name)" -ForegroundColor Cyan

$output = winget install --id $pkg.Id `
--accept-source-agreements `
--accept-package-agreements `
--silent 2>&1

$lastLine = ($output | Select-Object -Last 1).ToString()
if ($lastLine -match '成功|successfully|已安装') {
Write-Host " -> 成功" -ForegroundColor Green
$success++
} else {
Write-Host " -> 失败: $lastLine" -ForegroundColor Red
$failed++
}
}

# 输出汇总报告
Write-Host "`n===== 安装汇总 =====" -ForegroundColor Yellow
Write-Host "总计: $total | 成功: $success | 失败: $failed"
}

# 示例:仅安装 Runtime 类别的软件
Install-WinGetFromList -ListFile $listPath -Categories 'Runtime', 'VCS'

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
软件清单已保存到: C:\Users\admin\winget-software-list.json

[1/3] 安装: Git for Windows
-> 成功

[2/3] 安装: PowerShell 7
-> 成功

[3/3] 安装: Node.js LTS
-> 成功

===== 安装汇总 =====
总计: 3 | 成功: 3 | 失败: 0

使用 JSON 清单文件管理软件列表的好处是:不同团队可以维护各自的清单文件,新员工入职时只需指定对应的清单即可完成环境搭建。同时,清单文件可以纳入 Git 版本管理,便于审计和追溯软件配置变更。

软件更新检查与自动化更新

保持软件版本最新是安全运维的基本要求。下面的脚本实现了自动检测可更新的软件,并在确认后执行批量更新,同时支持将更新任务注册为 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
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
# 检查可更新的软件
function Get-WinGetUpdate {
$output = winget upgrade --accept-source-agreements 2>&1

# 解析输出获取可更新的包列表
$lines = $output -split "`n"
$updates = @()

# 找到数据行(跳过标题和分隔线)
$dataStarted = $false
foreach ($line in $lines) {
if ($line -match '^[-\s]+$') {
$dataStarted = $true
continue
}
if ($dataStarted -and $line.Trim() -and $line -notmatch '^\s*$') {
$updates += $line.Trim()
}
}

return $updates
}

# 执行更新
function Start-WinGetUpdate {
param(
[string]$PackageId,

[switch]$All,

[switch]$WhatIf
)

if ($All) {
Write-Host "检查所有可更新的软件..." -ForegroundColor Cyan
$updates = Get-WinGetUpdate

if ($updates.Count -eq 0) {
Write-Host "所有软件均为最新版本" -ForegroundColor Green
return
}

Write-Host "发现 $($updates.Count) 个可更新的软件:`n"
$updates | ForEach-Object { Write-Host " - $_" }

if ($WhatIf) {
Write-Host "`n[WhatIf 模式] 跳过实际更新" -ForegroundColor Yellow
return
}

$confirm = Read-Host "`n确认更新全部? (Y/N)"
if ($confirm -eq 'Y') {
winget upgrade --all `
--accept-source-agreements `
--accept-package-agreements 2>&1
}
}
elseif ($PackageId) {
if ($WhatIf) {
Write-Host "[WhatIf 模式] 将更新: $PackageId" -ForegroundColor Yellow
return
}

winget upgrade --id $PackageId `
--accept-source-agreements `
--accept-package-agreements 2>&1
}
}

# 定时任务:每天检查更新并记录日志
function Register-WinGetUpdateTask {
param(
[string]$Time = '03:00'
)

$scriptPath = Join-Path $HOME 'winget-auto-update.ps1'

# 生成自动更新脚本
$scriptContent = @"
`$logDir = Join-Path `$HOME 'WinGetUpdateLogs'
if (-not (Test-Path `$logDir)) {
New-Item -Path `$logDir -ItemType Directory | Out-Null
}

`$logFile = Join-Path `$logDir "update-$(Get-Date -Format 'yyyy-MM-dd').log"
"[$((Get-Date))] 开始检查更新" | Out-File `$logFile -Append

`$output = winget upgrade --all --accept-source-agreements --accept-package-agreements --silent 2>&1
`$output | Out-File `$logFile -Append

"[$((Get-Date))] 更新完成" | Out-File `$logFile -Append
"@

Set-Content -Path $scriptPath -Value $scriptContent -Encoding utf8

# 注册计划任务
$action = New-ScheduledTaskAction -Execute 'pwsh.exe' `
-Argument "-NoProfile -File `"$scriptPath`""
$trigger = New-ScheduledTaskTrigger -Daily -At $Time
$settings = New-ScheduledTaskSettingsSet `
-AllowStartIfOnBatteries `
-DontStopIfGoingOnBatteries

Register-ScheduledTask `
-TaskName 'WinGet-AutoUpdate' `
-Action $action `
-Trigger $trigger `
-Settings $settings `
-Force

Write-Host "已注册每日更新任务,执行时间: $Time" -ForegroundColor Green
}

# 示例:检查更新(仅查看,不执行)
Start-WinGetUpdate -All -WhatIf

# 示例:注册每日凌晨 3 点自动更新任务
# Register-WinGetUpdateTask -Time '03:00'

执行结果示例:

1
2
3
4
5
6
7
8
检查所有可更新的软件...
发现 3 个可更新的软件:

- Git.Git 2.43.0 2.47.1 winget
- Microsoft.VisualStudioCode 1.95.0 1.96.2 winget
- Docker.DockerDesktop 4.34.0 4.35.1 winget

[WhatIf 模式] 跳过实际更新

将更新检查注册为 Windows 计划任务后,系统会在每天凌晨自动检查并安装更新,同时将操作日志写入文件。运维人员可以通过查看日志目录下的文件,快速了解每台机器的软件更新情况,确保安全补丁及时到位。

注意事项

  1. WinGet 版本要求:建议使用 WinGet v1.6 或更高版本,旧版本可能不支持 --accept-package-agreements 等参数。可以通过 winget --version 查看当前版本,通过 Microsoft Store 自动更新 WinGet。

  2. 管理员权限:部分软件的安装需要管理员权限。在脚本中可以使用 #requires -RunAsAdministrator 声明,或者以管理员身份运行 PowerShell 来执行安装操作,否则可能会遇到权限不足的错误。

  3. 网络环境:在企业代理环境下,WinGet 可能需要额外配置才能正常访问软件源。可以通过 winget settings 命令打开配置文件,设置代理和网络相关参数,或者配置企业内部源(如 Azure Artifacts)作为替代。

  4. 安装失败处理:某些软件不支持静默安装或存在安装依赖冲突。建议在批量部署前先在测试环境中验证清单文件,对于失败的安装记录错误日志并单独处理,避免一个失败阻塞整个部署流程。

  5. 计划任务兼容性Register-ScheduledTask 需要 Windows 8.1 及以上系统。在 Windows Server 环境中,需要确认 Task Scheduler 服务正常运行,且执行账户具有安装软件的权限。对于不支持计划任务的场景,可以考虑使用 Group Policy 的启动脚本替代。

  6. PATH 环境变量刷新:WinGet 安装完软件后可能修改了系统 PATH,但当前 PowerShell 会话不会自动感知变更。部署完成后需要重启 PowerShell 或手动刷新环境变量:$env:Path = [System.Environment]::GetEnvironmentVariable('Path', 'Machine') + ';' + [System.Environment]::GetEnvironmentVariable('Path', 'User'),否则后续命令可能找不到新安装的可执行文件。

PowerShell 技能连载 - 注册表管理

适用于 PowerShell 5.1 及以上版本(Windows)

Windows 注册表是系统配置的核心存储——从系统服务配置到用户偏好,从已安装软件列表到启动项管理,几乎所有 Windows 配置都存储在注册表中。虽然图形界面提供了部分设置入口,但大量高级配置只能通过注册表修改。PowerShell 把注册表当作文件系统来处理,可以像操作文件一样读写注册表项和值。

本文将讲解注册表的操作技巧、常见配置管理场景,以及安全注意事项。

注册表浏览与查询

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
# PowerShell 内置的注册表驱动器
Get-PSDrive -PSProvider Registry | Format-Table Name, Root -AutoSize

# 浏览注册表(像文件系统一样)
Set-Location HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion
Get-ChildItem | Select-Object Name -First 10

# 读取注册表值
$productName = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -Name ProductName).ProductName
Write-Host "操作系统:$productName"

$currentBuild = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -Name CurrentBuild).CurrentBuild
Write-Host "构建号:$currentBuild"

# 读取所有值
$uninstall = Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*" -ErrorAction SilentlyContinue
$uninstall | Where-Object { $_.DisplayName } |
Select-Object DisplayName, DisplayVersion, Publisher |
Sort-Object DisplayName |
Format-Table -AutoSize | Out-String -Width 100 | Write-Host

# 搜索注册表
function Find-RegistryValue {
param(
[string]$Path = "HKLM:\SOFTWARE",
[string]$SearchValue,
[int]$MaxDepth = 3
)

$results = @()

function Search-Reg {
param([string]$CurrentPath, [int]$Depth)

if ($Depth -gt $MaxDepth) { return }

try {
$item = Get-Item $CurrentPath -ErrorAction Stop
foreach ($val in $item.GetValueNames()) {
$data = $item.GetValue($val)
if ($data -is [string] -and $data -match [regex]::Escape($SearchValue)) {
$results += [PSCustomObject]@{
Path = $CurrentPath
Name = $val
Value = $data
}
}
}

Get-ChildItem $CurrentPath -ErrorAction SilentlyContinue |
ForEach-Object { Search-Reg $_.PSPath ($Depth + 1) }
} catch {}
}

Search-Reg -CurrentPath $Path -Depth 0
return $results
}

# 搜索 PowerShell 相关的注册表值
Find-RegistryValue -Path "HKLM:\SOFTWARE" -SearchValue "PowerShell" -MaxDepth 2 |
Format-Table -AutoSize

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Name  Root
---- ----
HKLM HKEY_LOCAL_MACHINE
HKCU HKEY_CURRENT_USER

操作系统:Microsoft Windows Server 2022 Standard
构建号:20348

DisplayName DisplayVersion Publisher
------------ -------------- ---------
7-Zip 23.01 (x64) 23.01 Igor Pavlov
Git for Windows 2.45.0 The Git Development Community
PowerShell 7-x64 7.4.2 Microsoft Corporation

Path Name Value
HKLM:\SOFTWARE\Microsoft\PowerShell InstallDir C:\Program Files\PowerShell\7\

注册表修改

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
# 创建注册表项
$newKeyPath = "HKLM:\SOFTWARE\MyApp"
if (-not (Test-Path $newKeyPath)) {
New-Item -Path $newKeyPath -Force | Out-Null
Write-Host "已创建注册表项:$newKeyPath" -ForegroundColor Green
}

# 设置注册表值
New-ItemProperty -Path $newKeyPath -Name "InstallPath" -Value "C:\Program Files\MyApp" -PropertyType String -Force | Out-Null
New-ItemProperty -Path $newKeyPath -Name "Version" -Value "2.5.0" -PropertyType String -Force | Out-Null
New-ItemProperty -Path $newKeyPath -Name "Port" -Value 8080 -PropertyType DWord -Force | Out-Null
New-ItemProperty -Path $newKeyPath -Name "Enabled" -Value 1 -PropertyType DWord -Force | Out-Null

Write-Host "已设置注册表值" -ForegroundColor Green

# 验证
Get-ItemProperty -Path $newKeyPath | Format-List

# 修改现有值
Set-ItemProperty -Path $newKeyPath -Name "Port" -Value 9090
Write-Host "端口已修改为 9090" -ForegroundColor Yellow

# 删除注册表值
Remove-ItemProperty -Path $newKeyPath -Name "Enabled" -ErrorAction SilentlyContinue
Write-Host "已删除 Enabled 值" -ForegroundColor Yellow

# 删除注册表项
Remove-Item -Path $newKeyPath -Recurse -Force
Write-Host "已删除注册表项:$newKeyPath" -ForegroundColor Red

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
已创建注册表项:HKLM:\SOFTWARE\MyApp
已设置注册表值

InstallPath : C:\Program Files\MyApp
Version : 2.5.0
Port : 9090
PSPath : Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\SOFTWARE\MyApp

端口已修改为 9090
已删除 Enabled
已删除注册表项:HKLM:\SOFTWARE\MyApp

系统配置管理

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
# 管理启动项
function Get-StartupItems {
$items = @()

# 当前用户启动项
$userRun = Get-ItemProperty "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run" -ErrorAction SilentlyContinue
if ($userRun) {
$userRun.PSObject.Properties | Where-Object {
$_.Name -notin @('PSPath', 'PSParentPath', 'PSChildName', 'PSDrive', 'PSProvider')
} | ForEach-Object {
$items += [PSCustomObject]@{
Scope = "User"
Name = $_.Name
Value = $_.Value
}
}
}

# 系统启动项
$sysRun = Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run" -ErrorAction SilentlyContinue
if ($sysRun) {
$sysRun.PSObject.Properties | Where-Object {
$_.Name -notin @('PSPath', 'PSParentPath', 'PSChildName', 'PSDrive', 'PSProvider')
} | ForEach-Object {
$items += [PSCustomObject]@{
Scope = "System"
Name = $_.Name
Value = $_.Value
}
}
}

return $items
}

$startupItems = Get-StartupItems
Write-Host "启动项($($startupItems.Count) 个):" -ForegroundColor Cyan
$startupItems | Format-Table -AutoSize

# 禁用/启用 Windows 功能
function Set-WindowsFeatureViaRegistry {
param(
[string]$Feature,
[bool]$Enabled
)

$featureMap = @{
"RemoteDesktop" = @{
Path = "HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server"
Name = "fDenyTSConnections"
On = 0
Off = 1
}
"UAC" = @{
Path = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System"
Name = "EnableLUA"
On = 1
Off = 0
}
"Firewall" = @{
Path = "HKLM:\SYSTEM\CurrentControlSet\Services\SharedAccess\Parameters\FirewallPolicy\StandardProfile"
Name = "EnableFirewall"
On = 1
Off = 0
}
}

$config = $featureMap[$Feature]
if (-not $config) {
throw "未知功能:$Feature(可用:$($featureMap.Keys -join ', '))"
}

$value = if ($Enabled) { $config.On } else { $config.Off }
Set-ItemProperty -Path $config.Path -Name $config.Name -Value $value -Force
$state = if ($Enabled) { "启用" } else { "禁用" }
Write-Host "已${state} $Feature" -ForegroundColor Green
}

# 查看当前状态
$rdp = (Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server").fDenyTSConnections
Write-Host "远程桌面:$(if ($rdp -eq 0) { '已启用' } else { '已禁用' })"

执行结果示例:

1
2
3
4
5
6
7
8
启动项(5 个):
Scope Name Value
----- ---- -----
User OneDrive "C:\Program Files\Microsoft OneDrive\OneDrive.exe"
System SecurityHealth C:\Program Files\Windows Defender\MSASCuiL.exe
System VMware Tray "C:\Program Files\VMware\VMware Tools\trayicon.exe"

远程桌面:已禁用

注册表备份与恢复

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
# 导出注册表项
function Export-RegistryKey {
param(
[Parameter(Mandatory)]
[string]$KeyPath,

[string]$OutputDir = "C:\Backup\Registry"
)

New-Item $OutputDir -ItemType Directory -Force | Out-Null

$timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
$safeName = ($KeyPath -replace '[\\:]', '_') -replace '^HKLM_', 'HKLM-' -replace '^HKCU_', 'HKCU-'
$outputFile = Join-Path $OutputDir "${safeName}_${timestamp}.reg"

# 转换 PowerShell 路径为注册表路径
$regPath = $KeyPath -replace '^HKLM:\\', 'HKEY_LOCAL_MACHINE\' -replace '^HKCU:\\', 'HKEY_CURRENT_USER\'

$result = reg export $regPath $outputFile /y 2>&1
if ($LASTEXITCODE -eq 0) {
Write-Host "已导出:$outputFile ($((Get-Item $outputFile).Length / 1KB -as [int]) KB)" -ForegroundColor Green
return $outputFile
} else {
Write-Host "导出失败:$result" -ForegroundColor Red
return $null
}
}

# 备份重要注册表项
$importantKeys = @(
"HKLM:\SYSTEM\CurrentControlSet\Services"
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run"
"HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion"
)

foreach ($key in $importantKeys) {
Export-RegistryKey -KeyPath $key
}

# 恢复注册表
function Restore-RegistryKey {
param([Parameter(Mandatory)][string]$RegFile)

if (-not (Test-Path $RegFile)) {
throw "文件不存在:$RegFile"
}

Write-Host "即将导入注册表:$RegFile" -ForegroundColor Yellow
$result = reg import $RegFile 2>&1
if ($LASTEXITCODE -eq 0) {
Write-Host "导入成功" -ForegroundColor Green
} else {
Write-Host "导入失败:$result" -ForegroundColor Red
}
}

执行结果示例:

1
2
3
已导出:C:\Backup\Registry\HKLM-SYSTEM-CurrentControlSet-Services_20250716_083015.reg (256 KB)
已导出:C:\Backup\Registry\HKLM-SOFTWARE-Microsoft-Windows-CurrentVersion-Run_20250716_083015.reg (4 KB)
已导出:C:\Backup\Registry\HKLM-SOFTWARE-Microsoft-Windows-NT-CurrentVersion_20250716_083015.reg (12 KB)

注意事项

  1. 备份先行:修改注册表前务必备份相关键值,错误修改可能导致系统不稳定
  2. 管理员权限:修改 HKLM:\ 下的注册表需要管理员权限,HKCU:\ 不需要
  3. 数据类型:注册表值有多种类型(String、DWord、QWord、Binary、ExpandString、MultiString),设置时指定正确的类型
  4. 系统关键键HKLM:\SYSTEMHKLM:\SOFTWARE\Microsoft\Windows NT 下的键值直接影响系统运行,谨慎修改
  5. 64 位重定向:32 位程序访问 HKLM:\SOFTWARE 会被重定向到 HKLM:\SOFTWARE\WOW6432Node,注意区分
  6. 远程注册表:使用 Invoke-Command 可以远程操作其他机器的注册表(需要 WinRM 和 Remote Registry 服务)

PowerShell 技能连载 - WinGet 包管理自动化

适用于 Windows 10/11,PowerShell 7.0 及以上版本

WinGet:Windows 包管理的新时代

在 Linux 世界中,aptyumpacman 等包管理器早已是系统管理的标配工具。而在 Windows 平台上,长期以来我们只能依赖图形界面的安装向导,或者各自为政的第三方管理工具(如 Chocolatey、Scoop)。2020 年微软推出 WinGet(Windows Package Manager)后,这一局面终于开始改变。

如今 WinGet 已经相当成熟,软件源中收录了超过 6000 款常用软件,覆盖开发工具、浏览器、办公套件等几乎所有类别。更重要的是,WinGet 是 Windows 10/11 的内置组件,无需额外安装即可使用。将 WinGet 与 PowerShell 结合,我们可以实现软件安装、升级、卸载的完全自动化——无论是日常维护还是新机初始化,都能一键搞定。

本文将介绍如何用 PowerShell 封装 WinGet 命令、批量安装软件清单、自动检测升级、生成软件报告,以及编写新机初始化脚本。

封装 WinGet 常用命令

WinGet 的命令行输出是纯文本格式,不便于在脚本中进一步处理。我们先将常用的 WinGet 操作封装成 PowerShell 函数,输出结构化对象,方便后续链式调用和自动化编排。

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
function Get-WinGetPackage {
<#
.SYNOPSIS
获取已安装的 WinGet 软件包列表
#>
[CmdletBinding()]
param(
[string]$Name
)
$args = @('list')
if ($Name) {
$args += @('--name', $Name)
}
$output = winget @args 2>&1
# 跳过表头和末尾的摘要行
$lines = $output | Where-Object {
$_ -notmatch '^Name\s+Id' -and
$_ -notmatch '^\s*$' -and
$_ -notmatch '^\d+ packages'
}
foreach ($line in $lines) {
$parts = $line -split '\s{2,}'
if ($parts.Count -ge 3) {
[PSCustomObject]@{
Name = $parts[0]
Id = $parts[1]
Version = $parts[2]
}
}
}
}

function Install-WinGetPackage {
<#
.SYNOPSIS
安装 WinGet 软件包
#>
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory)]
[string]$Id,
[switch]$Force
)
if ($PSCmdlet.ShouldProcess($Id, 'Install WinGet package')) {
$installArgs = @('install', '--id', $Id, '--accept-package-agreements',
'--accept-source-agreements')
if ($Force) {
$installArgs += '--force'
}
winget @installArgs 2>&1
}
}

function Update-WinGetPackage {
<#
.SYNOPSIS
升级指定的 WinGet 软件包
#>
[CmdletBinding(SupportsShouldProcess)]
param(
[string]$Id,
[switch]$All
)
if ($PSCmdlet.ShouldProcess($Id, 'Update WinGet package')) {
$upgradeArgs = @('upgrade')
if ($All) {
$upgradeArgs += '--all'
} elseif ($Id) {
$upgradeArgs += @('--id', $Id)
}
$upgradeArgs += @('--accept-package-agreements', '--accept-source-agreements')
winget @upgradeArgs 2>&1
}
}

上面的代码定义了三个核心函数:Get-WinGetPackage 解析 winget list 的纯文本输出为结构化对象,Install-WinGetPackage 封装安装命令并自动接受协议,Update-WinGetPackage 封装升级命令。每个函数都支持 ShouldProcess,可以通过 -WhatIf 参数进行干运行预览。

1
2
3
4
5
6
7
Name                Id                             Version
---- -- -------
PowerShell Microsoft.PowerShell 7.4.6
Visual Studio Code Microsoft.VisualStudioCode 1.96.2
Windows Terminal Microsoft.WindowsTerminal 1.21.3231
Git Git.Git 2.47.1
7-Zip 7zip.7zip 24.08

批量安装软件清单

重装系统后手动安装几十个软件是一件耗时且容易遗漏的事情。将常用软件整理成 JSON 清单文件,然后用脚本批量安装,可以确保每次安装的软件集一致且不遗漏。

首先,创建一个软件清单 JSON 文件,将需要的软件按类别分组:

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
# 定义软件安装清单
$manifest = @'
{
"categories": [
{
"name": "开发工具",
"packages": [
"Microsoft.VisualStudioCode",
"Git.Git",
"Microsoft.PowerShell",
"OpenJS.NodeJS.LTS",
"Python.Python.3.12"
]
},
{
"name": "浏览器",
"packages": [
"Google.Chrome",
"Mozilla.Firefox"
]
},
{
"name": "系统工具",
"packages": [
"7zip.7zip",
"Microsoft.WindowsTerminal",
"Microsoft.Powertoys",
"voidtools.Everything"
]
},
{
"name": "效率工具",
"packages": [
"Obsidian.Obsidian",
"Telegram.TelegramDesktop"
]
}
]
}
'@

$pkgList = $manifest | ConvertFrom-Json

foreach ($category in $pkgList.categories) {
Write-Host "`n=== 安装类别: $($category.name) ===" -ForegroundColor Cyan
foreach ($pkgId in $category.packages) {
Write-Host "正在安装: $pkgId ..." -ForegroundColor Yellow -NoNewline
$result = winget install --id $pkgId `
--accept-package-agreements `
--accept-source-agreements 2>&1
if ($LASTEXITCODE -eq 0) {
Write-Host " 完成" -ForegroundColor Green
} else {
Write-Host " 失败 (退出码: $LASTEXITCODE)" -ForegroundColor Red
Write-Host " 错误信息: $($result | Select-Object -Last 2)" -ForegroundColor DarkRed
}
}
}

这段代码将软件清单以 JSON 格式定义,按类别分组管理。安装时逐类别遍历,每个软件包单独安装并报告结果。JSON 格式的清单文件也可以单独保存到磁盘,纳入版本控制,方便团队共享。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
=== 安装类别: 开发工具 ===
正在安装: Microsoft.VisualStudioCode ... 完成
正在安装: Git.Git ... 完成
正在安装: Microsoft.PowerShell ... 完成
正在安装: OpenJS.NodeJS.LTS ... 完成
正在安装: Python.Python.3.12 ... 完成

=== 安装类别: 浏览器 ===
正在安装: Google.Chrome ... 完成
正在安装: Mozilla.Firefox ... 完成

=== 安装类别: 系统工具 ===
正在安装: 7zip.7zip ... 完成
正在安装: Microsoft.WindowsTerminal ... 完成
正在安装: Microsoft.Powertoys ... 完成
正在安装: voidtools.Everything ... 完成

=== 安装类别: 效率工具 ===
正在安装: Obsidian.Obsidian ... 完成
正在安装: Telegram.TelegramDesktop ... 完成

检测已安装软件并自动升级

软件更新往往包含安全补丁和功能改进,但手动逐个检查升级非常繁琐。下面的脚本会扫描所有可通过 WinGet 升级的软件,筛选出有可用更新的,然后自动执行升级。

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 Invoke-WinGetAutoUpgrade {
<#
.SYNOPSIS
检测所有可升级的 WinGet 软件包并执行升级
.PARAMETER DryRun
仅显示可升级的软件,不实际执行升级
.PARAMETER ExcludeIds
排除不需要升级的软件包 ID 列表
#>
[CmdletBinding()]
param(
[switch]$DryRun,
[string[]]$ExcludeIds
)
Write-Host "正在扫描可升级的软件包..." -ForegroundColor Cyan
$upgradeList = winget upgrade 2>&1
$packages = $upgradeList | Where-Object {
$_ -match '^\S' -and
$_ -notmatch '^Name\s+Id' -and
$_ -notmatch '^\d+ upgrades'
} | ForEach-Object {
$parts = $_ -split '\s{2,}'
if ($parts.Count -ge 4) {
[PSCustomObject]@{
Name = $parts[0]
Id = $parts[1]
InstalledVer = $parts[2]
AvailableVer = $parts[3]
}
}
}
if ($ExcludeIds) {
$packages = $packages | Where-Object { $_.Id -notin $ExcludeIds }
}
if (-not $packages) {
Write-Host "所有软件均为最新版本" -ForegroundColor Green
return
}
Write-Host "发现 $($packages.Count) 个可升级软件包:" -ForegroundColor Yellow
$packages | Format-Table -Property Name, Id, InstalledVer, AvailableVer -AutoSize
if ($DryRun) {
Write-Host "[DryRun] 跳过实际升级" -ForegroundColor DarkYellow
return
}
foreach ($pkg in $packages) {
Write-Host "升级 $($pkg.Name) ($($pkg.InstalledVer) -> $($pkg.AvailableVer))..." `
-ForegroundColor Yellow -NoNewline
$result = winget upgrade --id $pkg.Id `
--accept-package-agreements `
--accept-source-agreements 2>&1
if ($LASTEXITCODE -eq 0) {
Write-Host " 完成" -ForegroundColor Green
} else {
Write-Host " 失败" -ForegroundColor Red
}
}
Write-Host "`n升级完成" -ForegroundColor Green
}

# 示例:排除已知不兼容升级的软件,先干运行查看
Invoke-WinGetAutoUpgrade -DryRun -ExcludeIds @('SomeApp.Legacy')

# 确认后执行实际升级
# Invoke-WinGetAutoUpgrade -ExcludeIds @('SomeApp.Legacy')

这个函数支持 -DryRun 参数先预览可升级列表而不实际执行,也支持 -ExcludeIds 排除某些已知升级后会出问题的软件。对于生产环境,建议先运行干运行模式确认,再执行实际升级。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
正在扫描可升级的软件包...
发现 4 个可升级软件包:

Name Id InstalledVer AvailableVer
---- -- ------------ ------------
7-Zip 7zip.7zip 24.06 24.08
Visual Studio Code Microsoft.VisualStudioCode 1.95.3 1.96.2
Git Git.Git 2.46.0 2.47.1
PowerShell Microsoft.PowerShell 7.4.5 7.4.6

升级 7-Zip (24.06 -> 24.08)... 完成
升级 Visual Studio Code (1.95.3 -> 1.96.2)... 完成
升级 Git (2.46.0 -> 2.47.1)... 完成
升级 PowerShell (7.4.5 -> 7.4.6)... 完成

升级完成

生成软件清单报告

定期生成系统已安装软件的报告,有助于审计和资产盘点。下面的脚本将 WinGet 管理的软件信息导出为 CSV 和 HTML 两种格式,便于存档和查看。

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
function Export-WinGetReport {
<#
.SYNOPSIS
生成已安装软件清单报告,输出 CSV 和 HTML 格式
.PARAMETER OutputDir
报告输出目录,默认为当前用户的桌面
#>
[CmdletBinding()]
param(
[string]$OutputDir = [Environment]::GetFolderPath('Desktop')
)
$timestamp = Get-Date -Format 'yyyyMMdd_HHmmss'
$csvPath = Join-Path $OutputDir "WinGet_Inventory_${timestamp}.csv"
$htmlPath = Join-Path $OutputDir "WinGet_Inventory_${timestamp}.html"
Write-Host "正在收集已安装软件信息..." -ForegroundColor Cyan
$rawOutput = winget list 2>&1
$packages = $rawOutput | Where-Object {
$_ -match '^\S' -and
$_ -notmatch '^Name\s+Id' -and
$_ -notmatch '^\d+ packages'
} | ForEach-Object {
$parts = $_ -split '\s{2,}'
if ($parts.Count -ge 3) {
[PSCustomObject]@{
Name = $parts[0]
Id = $parts[1]
Version = $parts[2]
ScanDate = (Get-Date -Format 'yyyy-MM-dd')
}
}
}
# 导出 CSV
$packages | Export-Csv -Path $csvPath -NoTypeInformation -Encoding UTF8
Write-Host "CSV 报告已保存: $csvPath" -ForegroundColor Green
# 导出 HTML
$htmlHeader = @"
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>WinGet 软件清单报告 - $timestamp</title>
<style>
body { font-family: 'Segoe UI', sans-serif; margin: 20px; }
h1 { color: #0078d4; }
table { border-collapse: collapse; width: 100%; }
th { background: #0078d4; color: white; padding: 8px 12px; text-align: left; }
td { border-bottom: 1px solid #e0e0e0; padding: 8px 12px; }
tr:nth-child(even) { background: #f5f5f5; }
.total { margin-top: 16px; font-weight: bold; color: #333; }
</style>
</head>
<body>
<h1>WinGet 软件清单报告</h1>
<p>生成时间: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')</p>
"@
$htmlBody = $packages | ConvertTo-Html -Fragment
$htmlFooter = @"
<p class="total">共 $($packages.Count) 个软件包</p>
</body></html>
"@
$htmlHeader + $htmlBody + $htmlFooter | Set-Content -Path $htmlPath -Encoding UTF8
Write-Host "HTML 报告已保存: $htmlPath" -ForegroundColor Green
Write-Host "共发现 $($packages.Count) 个已安装软件包" -ForegroundColor Cyan
}

Export-WinGetReport

脚本会同时输出 CSV 和 HTML 两种格式。CSV 适合导入 Excel 进行进一步分析,HTML 报告则可以直接在浏览器中查看,样式更友好。两种文件都以时间戳命名,方便归档对比。

1
2
3
4
正在收集已安装软件信息...
CSV 报告已保存: C:\Users\admin\Desktop\WinGet_Inventory_20250429_080000.csv
HTML 报告已保存: C:\Users\admin\Desktop\WinGet_Inventory_20250429_080000.html
共发现 42 个已安装软件包

新机初始化脚本

将前面的功能组合起来,我们可以编写一个完整的新机初始化脚本。这个脚本会安装基础软件、配置 PowerShell 环境、生成初始软件报告,让一台全新的 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
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
#Requires -RunAsAdministrator
<#
.SYNOPSIS
Windows 新机初始化脚本
.DESCRIPTION
通过 WinGet 批量安装常用软件,配置 PowerShell 环境,
生成软件清单报告,快速搭建工作环境。
.NOTES
需要以管理员权限运行
#>
param(
[string]$ManifestPath,
[switch]$SkipUpgrade,
[switch]$SkipReport
)
$ErrorActionPreference = 'Stop'
Write-Host @"

============================================
Windows 新机初始化脚本
$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
============================================

"@ -ForegroundColor Cyan
# 步骤 1: 确认 WinGet 可用
Write-Host "[1/5] 检查 WinGet 可用性..." -ForegroundColor Yellow
$wingetCmd = Get-Command winget -ErrorAction SilentlyContinue
if (-not $wingetCmd) {
Write-Host "WinGet 未安装,正在通过 App Installer 安装..." -ForegroundColor Yellow
# Windows 11 通常自带,Windows 10 可能需要从 Microsoft Store 安装
Write-Host "请从 Microsoft Store 安装 '应用安装程序' 后重试" -ForegroundColor Red
return
}
Write-Host "WinGet 版本: $(winget --version)" -ForegroundColor Green
# 步骤 2: 加载软件清单
Write-Host "`n[2/5] 加载软件安装清单..." -ForegroundColor Yellow
if (-not $ManifestPath) {
$ManifestPath = Join-Path $PSScriptRoot 'software-manifest.json'
}
if (-not (Test-Path $ManifestPath)) {
Write-Host "清单文件不存在: $ManifestPath" -ForegroundColor Red
Write-Host "请创建清单文件或通过 -ManifestPath 参数指定路径" -ForegroundColor Yellow
return
}
$manifest = Get-Content $ManifestPath -Raw | ConvertFrom-Json
$totalPkgs = ($manifest.categories | ForEach-Object { $_.packages.Count } |
Measure-Object -Sum).Sum
Write-Host "清单包含 $($manifest.categories.Count) 个类别,共 $totalPkgs 个软件包" `
-ForegroundColor Green
# 步骤 3: 批量安装
Write-Host "`n[3/5] 开始批量安装..." -ForegroundColor Yellow
$success = 0
$failed = 0
foreach ($category in $manifest.categories) {
Write-Host "`n--- $($category.name) ---" -ForegroundColor Cyan
foreach ($pkgId in $category.packages) {
try {
winget install --id $pkgId `
--accept-package-agreements `
--accept-source-agreements 2>&1 | Out-Null
if ($LASTEXITCODE -eq 0) {
Write-Host " [OK] $pkgId" -ForegroundColor Green
$success++
} else {
Write-Host " [FAIL] $pkgId (退出码: $LASTEXITCODE)" -ForegroundColor Red
$failed++
}
} catch {
Write-Host " [ERROR] $pkgId : $_" -ForegroundColor Red
$failed++
}
}
}
Write-Host "`n安装统计: 成功 $success / 失败 $failed / 总计 $totalPkgs" -ForegroundColor Cyan
# 步骤 4: 升级已安装软件
if (-not $SkipUpgrade) {
Write-Host "`n[4/5] 升级所有已安装软件到最新版..." -ForegroundColor Yellow
winget upgrade --all --accept-package-agreements --accept-source-agreements 2>&1
Write-Host "升级完成" -ForegroundColor Green
} else {
Write-Host "`n[4/5] 跳过软件升级" -ForegroundColor DarkYellow
}
# 步骤 5: 生成报告
if (-not $SkipReport) {
Write-Host "`n[5/5] 生成软件清单报告..." -ForegroundColor Yellow
Export-WinGetReport
} else {
Write-Host "`n[5/5] 跳过报告生成" -ForegroundColor DarkYellow
}
Write-Host @"

============================================
初始化完成!
成功安装: $success
安装失败: $failed
$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
============================================

"@ -ForegroundColor Green

这个脚本设计了五个步骤:检查 WinGet 可用性、加载软件清单、批量安装、升级已安装软件、生成报告。每一步都有清晰的进度提示,支持通过参数跳过升级和报告步骤。脚本开头使用 #Requires -RunAsAdministrator 确保以管理员权限运行,因为部分软件安装需要提升权限。

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
============================================
Windows 新机初始化脚本
2025-04-29 08:00:00
============================================

[1/5] 检查 WinGet 可用性...
WinGet 版本: v1.9.25200

[2/5] 加载软件安装清单...
清单包含 4 个类别,共 13 个软件包

[3/5] 开始批量安装...

--- 开发工具 ---
[OK] Microsoft.VisualStudioCode
[OK] Git.Git
[OK] Microsoft.PowerShell
[OK] OpenJS.NodeJS.LTS
[OK] Python.Python.3.12

--- 浏览器 ---
[OK] Google.Chrome
[OK] Mozilla.Firefox

--- 系统工具 ---
[OK] 7zip.7zip
[OK] Microsoft.WindowsTerminal
[OK] Microsoft.Powertoys
[OK] voidtools.Everything

--- 效率工具 ---
[OK] Obsidian.Obsidian
[OK] Telegram.TelegramDesktop

安装统计: 成功 13 / 失败 0 / 总计 13

[4/5] 升级所有已安装软件到最新版...
升级完成

[5/5] 生成软件清单报告...
CSV 报告已保存: C:\Users\admin\Desktop\WinGet_Inventory_20250429_080000.csv
HTML 报告已保存: C:\Users\admin\Desktop\WinGet_Inventory_20250429_080000.html
共发现 42 个已安装软件包

============================================
初始化完成!
成功安装: 13
安装失败: 0
2025-04-29 08:05:32
============================================

注意事项

  • 管理员权限:部分软件(如系统级安装的程序)需要以管理员身份运行 PowerShell,脚本中已通过 #Requires -RunAsAdministrator 确保这一点
  • WinGet 输出解析:WinGet 的命令行输出格式可能在版本更新后变化,解析逻辑需要相应调整。如果遇到解析异常,建议检查 WinGet 版本并更新解析正则
  • 软件源一致性:WinGet 有两个源(winget 和 msstore),某些软件同时存在于两个源中,安装时建议使用 --id 精确指定软件包 ID,避免歧义
  • 安装顺序:部分软件有依赖关系(如 VS Code 扩展依赖 VS Code 本体),清单中应确保依赖项排在前面
  • 网络环境:在企业网络中,可能需要配置代理或使用内部软件源,WinGet 支持通过 --source 参数指定自定义源
  • 错误处理:批量安装时某个软件失败不应阻断整个流程,脚本中对每个软件单独 try/catch 处理,最终汇总成功和失败数量

PowerShell 技能连载 - Windows 防火墙规则管理

适用于 PowerShell 5.1 及以上版本(Windows 内置模块)

Windows 防火墙是服务器和终端安全的第一道防线。无论是部署新服务、排查网络故障还是进行安全加固,都离不开防火墙规则的配置与管理。传统的图形界面操作虽然直观,但在批量管理和自动化场景下效率低下,且容易遗漏。

PowerShell 内置的 NetSecurity 模块提供了完整的防火墙管理能力,支持查看、创建、修改和删除规则,所有操作都可以脚本化、可重复执行。对于运维工程师来说,掌握这套命令不仅能提高日常工作效率,更是实现基础设施即代码(IaC)的重要基础。

查看防火墙规则

日常运维中最常见的操作就是查看当前生效的防火墙规则。Get-NetFirewallRule 是核心命令,结合 Get-NetFirewallPortFilterGet-NetFirewallAddressFilter 可以获取完整的规则详情。

以下函数封装了常用的查询逻辑,支持按名称、方向、启用状态等条件过滤,并输出格式化的规则摘要:

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
function Get-FirewallRuleSummary {
param(
[string]$Name = "*",
[ValidateSet("Inbound", "Outbound", "Both")]
[string]$Direction = "Both",
[switch]$EnabledOnly
)

$filter = @{ DisplayName = $Name }
if ($Direction -ne "Both") {
$filter["Direction"] = $Direction
}
if ($EnabledOnly) {
$filter["Enabled"] = "True"
}

$rules = Get-NetFirewallRule @filter

$results = foreach ($rule in $rules) {
$portFilter = $rule | Get-NetFirewallPortFilter -ErrorAction SilentlyContinue
$addrFilter = $rule | Get-NetFirewallAddressFilter -ErrorAction SilentlyContinue

[PSCustomObject]@{
Name = $rule.DisplayName
Direction = $rule.Direction
Action = $rule.Action
Enabled = $rule.Enabled
Protocol = if ($portFilter) { $portFilter.Protocol } else { "Any" }
LocalPort = if ($portFilter) { $portFilter.LocalPort } else { "Any" }
RemoteAddr = if ($addrFilter) { $addrFilter.RemoteAddress } else { "Any" }
Profile = $rule.Profile
}
}

return $results | Sort-Object Enabled, Direction, Action
}

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
PS> Get-FirewallRuleSummary -Name "*SSH*" -EnabledOnly

Name : OpenSSH Server (sshd)
Direction : Inbound
Action : Allow
Enabled : True
Protocol : TCP
LocalPort : 22
RemoteAddr : Any
Profile : Any

Name : OpenSSH Client
Direction : Outbound
Action : Allow
Enabled : True
Protocol : TCP
LocalPort : Any
RemoteAddr : Any
Profile : Any

创建防火墙规则

创建规则是防火墙管理中最关键的操作。New-NetFirewallRule 命令支持丰富的参数,可以精确控制协议、端口、地址范围和配置文件。

以下函数将常见的规则创建流程封装为可复用的命令,内置了参数校验和冲突检测逻辑,避免重复创建同名规则:

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
function New-FirewallAllowRule {
param(
[Parameter(Mandatory)]
[string]$Name,

[Parameter(Mandatory)]
[int]$Port,

[ValidateSet("TCP", "UDP")]
[string]$Protocol = "TCP",

[ValidateSet("Inbound", "Outbound")]
[string]$Direction = "Inbound",

[string[]]$RemoteAddress = "Any",

[ValidateSet("Domain", "Private", "Public", "Any")]
[string]$Profile = "Any"
)

# 检查同名规则是否已存在
$existing = Get-NetFirewallRule -DisplayName $Name -ErrorAction SilentlyContinue
if ($existing) {
Write-Warning "规则 '$Name' 已存在,跳过创建"
return $existing
}

$params = @{
DisplayName = $Name
Direction = $Direction
Action = "Allow"
Protocol = $Protocol
LocalPort = $Port
RemoteAddress = $RemoteAddress
Profile = $Profile
Enabled = "True"
}

$rule = New-NetFirewallRule @params
Write-Host "已创建规则: $Name ($Protocol/$Port, $Direction)"
return $rule
}

执行结果示例:

1
2
3
4
5
PS> New-FirewallAllowRule -Name "Web Server HTTP" -Port 80 -Protocol TCP
已创建规则: Web Server HTTP (TCP/80, Inbound)

PS> New-FirewallAllowRule -Name "Web Server HTTP" -Port 80
WARNING: 规则 'Web Server HTTP' 已存在,跳过创建

批量管理规则

在服务器初始化或安全加固场景中,往往需要一次性配置大量规则。手动逐条创建既繁琐又容易出错,批量管理脚本能显著提升效率和一致性。

以下函数接收规则定义数组,自动创建并输出执行报告,支持回滚操作:

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
function Import-FirewallRuleSet {
param(
[Parameter(Mandatory)]
[string]$RuleSetName,

[Parameter(Mandatory)]
[array]$Rules
)

$report = @()
$created = @()

foreach ($rule in $Rules) {
$qualifiedName = "$RuleSetName - $($rule.Name)"
try {
$result = New-FirewallAllowRule `
-Name $qualifiedName `
-Port $rule.Port `
-Protocol $rule.Protocol `
-Direction $rule.Direction `
-RemoteAddress $rule.RemoteAddress `
-Profile $rule.Profile

if ($result) {
$created += $qualifiedName
$status = "Created"
}
else {
$status = "Skipped"
}
}
catch {
$status = "Error: $($_.Exception.Message)"
}

$report += [PSCustomObject]@{
Rule = $qualifiedName
Port = "$($rule.Protocol)/$($rule.Port)"
Status = $status
}
}

Write-Host "`n规则集 '$RuleSetName' 导入完成: $($created.Count)/$($Rules.Count) 条已创建"
return $report | Format-Table -AutoSize
}

使用示例——批量导入 Web 服务器所需规则:

1
2
3
4
5
6
7
$webRules = @(
@{ Name = "HTTP"; Port = 80; Protocol = "TCP"; Direction = "Inbound"; RemoteAddress = "Any"; Profile = "Any" }
@{ Name = "HTTPS"; Port = 443; Protocol = "TCP"; Direction = "Inbound"; RemoteAddress = "Any"; Profile = "Any" }
@{ Name = "SSH"; Port = 22; Protocol = "TCP"; Direction = "Inbound"; RemoteAddress = "10.0.0.0/8"; Profile = "Domain" }
)

Import-FirewallRuleSet -RuleSetName "Web Server" -Rules $webRules

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
已创建规则: Web Server - HTTP (TCP/80, Inbound)
已创建规则: Web Server - HTTPS (TCP/443, Inbound)
已创建规则: Web Server - SSH (TCP/22, Inbound)

规则集 'Web Server' 导入完成: 3/3 条已创建

Rule Port Status
---- ---- ------
Web Server - HTTP TCP/80 Created
Web Server - HTTPS TCP/443 Created
Web Server - SSH TCP/22 Created

导出与导入规则配置

在多台服务器之间同步防火墙配置,或者在变更前做备份,都需要导出导入功能。以下函数将规则集导出为 JSON 文件,便于版本控制和跨机器部署。

注意这里使用数组拼接 -join 来构建多行文本,避免在代码块中嵌入三反引号导致 Markdown 解析错误:

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
function Export-FirewallRules {
param(
[Parameter(Mandatory)]
[string]$NamePattern,

[Parameter(Mandatory)]
[string]$OutputPath
)

$rules = Get-NetFirewallRule -DisplayName $NamePattern -ErrorAction SilentlyContinue

$export = foreach ($rule in $rules) {
$portFilter = $rule | Get-NetFirewallPortFilter -ErrorAction SilentlyContinue
$addrFilter = $rule | Get-NetFirewallAddressFilter -ErrorAction SilentlyContinue

@{
Name = $rule.DisplayName
Direction = [string]$rule.Direction
Action = [string]$rule.Action
Enabled = [string]$rule.Enabled
Protocol = if ($portFilter) { [string]$portFilter.Protocol } else { "" }
LocalPort = if ($portFilter) { [string]$portFilter.LocalPort } else { "" }
RemoteAddress = if ($addrFilter) { [string]$addrFilter.RemoteAddress } else { "" }
Profile = [string]$rule.Profile
}
}

$jsonLines = @(
"{"
' "ExportDate": "' + (Get-Date -Format "yyyy-MM-dd HH:mm:ss") + '",'
' "RuleCount": ' + $export.Count + ','
' "Rules": ['
)

for ($i = 0; $i -lt $export.Count; $i++) {
$comma = if ($i -lt $export.Count - 1) { "," } else { "" }
$ruleJson = $export[$i] | ConvertTo-Json -Compress
$jsonLines += " $ruleJson$comma"
}

$jsonLines += @(
" ]"
"}"
)

($jsonLines -join "`n") | Out-File -FilePath $OutputPath -Encoding UTF8
Write-Host "已导出 $($export.Count) 条规则到: $OutputPath"
}

执行结果示例:

1
2
PS> Export-FirewallRules -NamePattern "Web Server*" -OutputPath "C:\Backup\firewall-web.json"
已导出 3 条规则到: C:\Backup\firewall-web.json

对应的导入函数从 JSON 文件读取规则定义并批量创建:

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
function Import-FirewallRulesFromFile {
param(
[Parameter(Mandatory)]
[string]$FilePath
)

$content = Get-Content -Path $FilePath -Raw | ConvertFrom-Json
Write-Host "文件包含 $($content.RuleCount) 条规则,导出时间: $($content.ExportDate)"

$results = @()
foreach ($r in $content.Rules) {
try {
$existing = Get-NetFirewallRule -DisplayName $r.Name -ErrorAction SilentlyContinue
if ($existing) {
$results += [PSCustomObject]@{ Name = $r.Name; Status = "AlreadyExists" }
continue
}

New-NetFirewallRule `
-DisplayName $r.Name `
-Direction $r.Direction `
-Action $r.Action `
-Protocol $r.Protocol `
-LocalPort $r.LocalPort `
-RemoteAddress $r.RemoteAddress `
-Profile $r.Profile `
-Enabled $r.Enabled | Out-Null

$results += [PSCustomObject]@{ Name = $r.Name; Status = "Created" }
}
catch {
$results += [PSCustomObject]@{ Name = $r.Name; Status = "Error: $($_.Exception.Message)" }
}
}

$created = ($results | Where-Object Status -eq "Created").Count
Write-Host "`n导入完成: $created 条新建, $($results.Count - $created) 条跳过"
return $results | Format-Table -AutoSize
}

执行结果示例:

1
2
3
4
5
6
7
8
9
10
PS> Import-FirewallRulesFromFile -FilePath "C:\Backup\firewall-web.json"
文件包含 3 条规则,导出时间: 2025-04-16 10:30:00

导入完成: 3 条新建, 0 条跳过

Name Status
---- ------
Web Server - HTTP Created
Web Server - HTTPS Created
Web Server - SSH Created

服务器安全加固

生产服务器的防火墙配置直接决定攻击面大小。以下加固脚本针对常见的风险点实施最小权限原则:禁用所有入站流量后仅开放必要端口,限制管理端口的访问来源,并关闭不必要的出站流量。

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
function Initialize-ServerFirewallHardening {
param(
[int[]]$AllowedInboundPorts = @(80, 443),
[int]$SshPort = 22,
[string[]]$AdminNetworks = @("10.0.0.0/8", "172.16.0.0/12"),
[switch]$WhatIf
)

$log = @()

# 第一步:阻止所有默认入站流量
$step1 = "Step 1: 设置默认入站策略为阻止"
$log += $step1
Write-Host $step1

if (-not $WhatIf) {
Set-NetFirewallProfile -Profile Domain,Public,Private `
-DefaultInboundAction Block `
-DefaultOutboundAction Allow `
-Enabled True
}

# 第二步:开放业务端口
$step2 = "Step 2: 开放业务端口 ($($AllowedInboundPorts -join ', '))"
$log += $step2
Write-Host $step2

foreach ($port in $AllowedInboundPorts) {
$ruleName = "Hardened - HTTP/S Port $port"
if (-not $WhatIf) {
New-FirewallAllowRule -Name $ruleName -Port $port -Protocol TCP | Out-Null
}
$log += " -> 已开放端口: $port/TCP"
}

# 第三步:限制管理端口访问来源
$step3 = "Step 3: 限制管理端口 ($SshPort) 仅允许管理网段访问"
$log += $step3
Write-Host $step3

foreach ($network in $AdminNetworks) {
$ruleName = "Hardened - SSH from $network"
if (-not $WhatIf) {
New-FirewallAllowRule -Name $ruleName -Port $SshPort `
-Protocol TCP -RemoteAddress $network | Out-Null
}
$log += " -> 已允许: $network -> $SshPort/TCP"
}

# 第四步:禁用不必要的规则
$step4 = "Step 4: 禁用非必要预置规则"
$log += $step4
Write-Host $step4

$builtinDisabled = @()
$noisyRules = @(
"File and Printer Sharing (Echo Request - ICMPv4-In)"
"Network Discovery (NB-Datagram-In)"
"Network Discovery (NB-Name-In)"
)

foreach ($ruleName in $noisyRules) {
$rule = Get-NetFirewallRule -DisplayName $ruleName -ErrorAction SilentlyContinue
if ($rule -and $rule.Enabled -eq "True") {
if (-not $WhatIf) {
$rule | Disable-NetFirewallRule | Out-Null
}
$builtinDisabled += $ruleName
}
}

$log += " -> 已禁用 $($builtinDisabled.Count) 条预置规则"

# 输出加固报告
$summary = @(
""
"========== 加固报告 =========="
"默认入站策略: Block"
"业务端口: $($AllowedInboundPorts -join ', ')"
"管理端口: $SshPort/TCP (限 $($AdminNetworks -join ', '))"
"禁用预置规则: $($builtinDisabled.Count) 条"
"WhatIf 模式: $WhatIf"
"================================"
)
$summary | ForEach-Object { Write-Host $_ }

return $log
}

执行结果示例(预览模式):

1
2
3
4
5
6
7
8
9
10
11
12
13
PS> Initialize-ServerFirewallHardening -WhatIf
Step 1: 设置默认入站策略为阻止
Step 2: 开放业务端口 (80, 443)
Step 3: 限制管理端口 (22) 仅允许管理网段访问
Step 4: 禁用非必要预置规则

========== 加固报告 ==========
默认入站策略: Block
业务端口: 80, 443
管理端口: 22/TCP (限 10.0.0.0/8, 172.16.0.0/12)
禁用预置规则: 3 条
WhatIf 模式: True
================================

小结

PowerShell 的 NetSecurity 模块让 Windows 防火墙管理变得完全可脚本化。核心命令只有四个——Get-NetFirewallRuleNew-NetFirewallRuleSet-NetFirewallRuleRemove-NetFirewallRule,但配合过滤器和管道可以覆盖几乎所有管理场景。实际运维中建议:始终使用 -WhatIf 预览变更、变更前导出备份、管理端口严格限制来源地址。将这些函数纳入 DSC 或 Ansible Playbook 中,就能实现防火墙策略的版本化管理和自动部署。

将 Windows 8.1 的命令提示符替换为 PowerShell

在 Windows 8.1 中,增加了一个“将 WIN+X 菜单中将命令提示符替换为 Windows PowerShell”功能,您注意到了吗?

打开这个选项的方法是:右键单击 Windows 任务栏,选择“属性”。在“导航”选项卡中,您可以找到这个功能。