PowerShell 技能连载 - 合规审计自动化

适用于 PowerShell 5.1 及以上版本

在企业 IT 管理中,合规审计是一项持续性工作。无论是为了满足 CIS Benchmarks、ISO 27001 还是行业监管要求,安全团队都需要定期检查系统配置是否符合既定的安全基线。手动逐项检查不仅耗时巨大,而且容易出现遗漏和标准不一致的问题,尤其是在服务器数量较多的环境中。

PowerShell 凭借其对 Windows 系统的深度访问能力,可以从密码策略、账户策略、审核策略到注册表配置、防火墙规则、文件权限等维度全面采集安全配置信息。将审计规则标准化为可执行脚本后,每次审计都能以完全一致的检查项和判定逻辑运行,确保结果的可重复性和可比性。

本文将构建一套完整的合规审计自动化方案,涵盖安全基线检查、系统配置审计和合规报告生成三个核心模块,帮助企业将审计周期从数天缩短到数分钟,并输出结构化的 HTML 报告供管理层审阅。

安全基线检查

安全基线检查是合规审计的基础。我们将密码策略、账户策略、审核策略和用户权限分配四项关键检查整合到一个函数中,每项检查都映射到 CIS Benchmark 的具体控制项,并给出合规/不合规的判定结果。

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
function Invoke-SecurityBaselineCheck {
[CmdletBinding()]
param(
[string]$ComputerName = $env:COMPUTERNAME
)

$results = @()

# 密码策略检查
$passwordPolicy = net accounts |
Select-String -Pattern 'Minimum password length|Maximum password age|Minimum password age|Password history length'

$passwordLengthLine = $passwordPolicy | Where-Object { $_ -match 'Minimum password length' }
$minLength = if ($passwordLengthLine -match '(\d+)') { [int]$Matches[1] } else { 0 }

$results += [PSCustomObject]@{
Category = '密码策略'
CheckItem = '最小密码长度'
Expected = '>= 14 字符 (CIS 1.1.1)'
Actual = "$minLength 字符"
Status = if ($minLength -ge 14) { 'Compliant' } else { 'Non-Compliant' }
Severity = 'High'
}

$maxAgeLine = $passwordPolicy | Where-Object { $_ -match 'Maximum password age' }
$maxAge = if ($maxAgeLine -match '(\d+)') { [int]$Matches[1] } else { 0 }

$results += [PSCustomObject]@{
Category = '密码策略'
CheckItem = '密码最大使用期限'
Expected = '<= 60 天 (CIS 1.1.3)'
Actual = "$maxAge 天"
Status = if ($maxAge -gt 0 -and $maxAge -le 60) { 'Compliant' } else { 'Non-Compliant' }
Severity = 'Medium'
}

# 账户策略检查
$lockoutPolicy = net accounts |
Select-String -Pattern 'Lockout threshold|Lockout duration|Lockout observation'

$lockoutLine = $lockoutPolicy | Where-Object { $_ -match 'Lockout threshold' }
$lockoutThreshold = if ($lockoutLine -match '(\d+)') { [int]$Matches[1] } else { 0 }

$results += [PSCustomObject]@{
Category = '账户策略'
CheckItem = '账户锁定阈值'
Expected = '<= 5 次失败尝试 (CIS 1.2.1)'
Actual = if ($lockoutThreshold -eq 0) { 'Never' } else { "$lockoutThreshold 次" }
Status = if ($lockoutThreshold -gt 0 -and $lockoutThreshold -le 5) { 'Compliant' } else { 'Non-Compliant' }
Severity = 'High'
}

# 审核策略检查
$auditPolicies = auditpol /get /category:* /r 2>$null |
ConvertFrom-Csv |
Where-Object { $_.'Inclusion Setting' -eq 'No Auditing' -and $_.'Subcategory' -ne '' }

$criticalAuditCategories = @(
'Logon',
'Logoff',
'Special Logon',
'User Account Management',
'Security State Change'
)

foreach ($critical in $criticalAuditCategories) {
$found = $auditPolicies | Where-Object { $_.'Subcategory' -eq $critical }
$results += [PSCustomObject]@{
Category = '审核策略'
CheckItem = "审核: $critical"
Expected = 'Success and Failure (CIS 17.x)'
Actual = if ($found) { 'No Auditing' } else { '已配置' }
Status = if ($found) { 'Non-Compliant' } else { 'Compliant' }
Severity = 'High'
}
}

# 用户权限检查 - 检查敏感权限的分配情况
$sensitivePrivileges = @{
'SeDebugPrivilege' = '调试程序'
'SeTakeOwnershipPrivilege' = '取得文件所有权'
'SeLoadDriverPrivilege' = '加载和卸载设备驱动程序'
}

foreach ($privilege in $sensitivePrivileges.GetEnumerator()) {
$privOutput = secedit /export /cfg "$env:TEMP\secedit.cfg" 2>$null
$privLine = Get-Content "$env:TEMP\secedit.cfg" |
Select-String -Pattern $privilege.Key
$assignedTo = if ($privLine) {
($privLine.Line -split '=')[1].Trim()
} else {
'未找到'
}

$isAdminOnly = $assignedTo -match 'Administrators' -and
$assignedTo -notmatch 'Everyone|Users|Authenticated'

$results += [PSCustomObject]@{
Category = '用户权限'
CheckItem = $privilege.Value
Expected = '仅 Administrators (CIS 2.x)'
Actual = $assignedTo
Status = if ($isAdminOnly) { 'Compliant' } else { 'Non-Compliant' }
Severity = 'Critical'
}
}

Remove-Item "$env:TEMP\secedit.cfg" -ErrorAction SilentlyContinue

$results | Sort-Object Severity, Category, CheckItem
}

运行基线检查后,可以看到各项安全策略的合规状态:

1
2
3
4
5
6
7
8
9
Category  CheckItem              Expected                    Actual         Status          Severity
------- ---------- -------- ------ ------ --------
密码策略 最小密码长度 >= 14 字符 (CIS 1.1.1) 8 字符 Non-Compliant High
密码策略 密码最大使用期限 <= 60 天 (CIS 1.1.3) 90 天 Non-Compliant Medium
账户策略 账户锁定阈值 <= 5 次失败尝试 (CIS 1.2.1) 5 次 Compliant High
审核策略 审核: Logon Success and Failure (CIS) 已配置 Compliant High
审核策略 审核: User Account Management Success and Failure (CIS) No Auditing Non-Compliant High
用户权限 调试程序 仅 Administrators (CIS 2.x) Administrators Compliant Critical
用户权限 取得文件所有权 仅 Administrators (CIS 2.x) Administrators Compliant Critical

系统配置审计

安全基线只覆盖了策略层面的检查,实际运行环境中的服务、防火墙、注册表和文件系统同样需要审计。下面这个函数对系统配置进行全面扫描,检查每项配置是否满足 CIS 和 ISO 27001 的要求。

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
function Invoke-SystemConfigAudit {
[CmdletBinding()]
param(
[string[]]$CriticalRegistryKeys = @(
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System',
'HKLM:\SYSTEM\CurrentControlSet\Services\LDAP',
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer'
)
)

$auditResults = @()

# 服务配置检查
$insecureServices = @{
'SysMain' = 'SuperFetch/SysMain 可能导致信息泄露'
'RemoteRegistry' = '远程注册表访问应被禁用'
'XblAuthManager' = 'Xbox 服务不应存在于服务器'
'WSearch' = 'Windows Search 在服务器上应禁用'
}

foreach ($svc in $insecureServices.GetEnumerator()) {
$service = Get-Service -Name $svc.Key -ErrorAction SilentlyContinue
$startType = try {
(Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Services\$($svc.Key)" -ErrorAction Stop).Start
} catch { 4 }

$isDisabled = $service.Status -eq 'Stopped' -and $startType -eq 4

$auditResults += [PSCustomObject]@{
Category = '服务配置'
CheckItem = "服务: $($svc.Key)"
Expected = '已禁用 (CIS 9.x)'
Actual = "状态: $($service.Status), 启动类型: $(if($startType -eq 4){'Disabled'}elseif($startType -eq 3){'Manual'}else{'Auto'})"
Status = if ($isDisabled) { 'Compliant' } else { 'Non-Compliant' }
Severity = 'Medium'
Remediation = "Set-Service -Name $($svc.Key) -StartupType Disabled; Stop-Service -Name $($svc.Key) -Force"
}
}

# 防火墙规则审计
$firewallProfiles = Get-NetFirewallProfile |
Select-Object Name, Enabled

foreach ($profile in $firewallProfiles) {
$auditResults += [PSCustomObject]@{
Category = '防火墙'
CheckItem = "防火墙配置文件: $($profile.Name)"
Expected = '已启用 (CIS 9.1.x)'
Actual = "Enabled: $($profile.Enabled)"
Status = if ($profile.Enabled) { 'Compliant' } else { 'Non-Compliant' }
Severity = 'Critical'
Remediation = "Set-NetFirewallProfile -Name $($profile.Name) -Enabled True"
}
}

# 危险入站规则检查
$dangerousRules = Get-NetFirewallRule |
Where-Object {
$_.Direction -eq 'Inbound' -and
$_.Action -eq 'Allow' -and
$_.Enabled -eq 'True'
} | Get-NetFirewallPortFilter |
Where-Object { $_.LocalPort -in @('3389', '22', '445', '139') }

$rdpExposed = $dangerousRules |
Where-Object { $_.LocalPort -eq '3389' }

if ($rdpExposed) {
$auditResults += [PSCustomObject]@{
Category = '防火墙'
CheckItem = 'RDP (3389) 入站规则'
Expected = '不应公开暴露 (CIS 9.2.x)'
Actual = '存在允许的入站规则'
Status = 'Non-Compliant'
Severity = 'Critical'
Remediation = '限制 RDP 访问源 IP 范围,或通过 VPN 隧道访问'
}
}

# 注册表安全审计
$regChecks = @(
@{
Path = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System'
Name = 'EnableLUA'
Expected = 1
Desc = 'UAC 必须启用 (CIS 2.3.17.3)'
Severity = 'Critical'
},
@{
Path = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System'
Name = 'ConsentPromptBehaviorAdmin'
Expected = 2
Desc = 'UAC 提示级别应为"始终通知" (CIS 2.3.17.5)'
Severity = 'Medium'
},
@{
Path = 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurePipeServers\Winreg'
Name = 'RemoteAllowed'
Expected = 1
Desc = '远程注册表访问应受限制 (CIS 2.3.10.3)'
Severity = 'High'
}
)

foreach ($check in $regChecks) {
$actualValue = try {
(Get-ItemProperty -Path $check.Path -Name $check.Name -ErrorAction Stop).($check.Name)
} catch { $null }

$auditResults += [PSCustomObject]@{
Category = '注册表安全'
CheckItem = $check.Desc
Expected = "值 = $($check.Expected)"
Actual = if ($null -ne $actualValue) { "值 = $actualValue" } else { '键值不存在' }
Status = if ($actualValue -eq $check.Expected) { 'Compliant' } else { 'Non-Compliant' }
Severity = $check.Severity
Remediation = "Set-ItemProperty -Path '$($check.Path)' -Name $($check.Name) -Value $($check.Expected)"
}
}

# 关键目录权限审计
$sensitivePaths = @(
@{ Path = 'C:\Windows\System32\config'; ExpectedOwner = 'BUILTIN\Administrators'; Desc = 'SAM/SYSTEM 文件目录' },
@{ Path = 'C:\ProgramData'; ExpectedOwner = 'BUILTIN\Administrators'; Desc = '公共程序数据目录' }
)

foreach ($sp in $sensitivePaths) {
if (Test-Path $sp.Path) {
$acl = Get-Acl -Path $sp.Path
$owner = $acl.Owner

$auditResults += [PSCustomObject]@{
Category = '文件权限'
CheckItem = "目录所有者: $($sp.Desc)"
Expected = "所有者 = $($sp.ExpectedOwner) (CIS 13.x)"
Actual = "所有者 = $owner"
Status = if ($owner -match 'Administrators') { 'Compliant' } else { 'Non-Compliant' }
Severity = 'High'
Remediation = "手动检查并修复 $sp.Path 的 NTFS 权限"
}
}
}

$auditResults | Sort-Object Severity, Category, CheckItem
}

系统配置审计的输出结果示例如下:

1
2
3
4
5
6
7
8
9
Category  CheckItem                              Expected                              Actual           Status          Severity
-------- ---------- -------- ------ ------ --------
注册表安全 UAC 必须启用 (CIS 2.3.17.3) 值 = 1 值 = 1 Compliant Critical
防火墙 防火墙配置文件: Domain 已启用 (CIS 9.1.x) Enabled: True Compliant Critical
防火墙 RDP (3389) 入站规则 不应公开暴露 (CIS 9.2.x) 存在允许的入站规则 Non-Compliant Critical
注册表安全 远程注册表访问应受限制 (CIS 2.3.10.3) 值 = 1 值 = 1 Compliant High
文件权限 目录所有者: SAM/SYSTEM 文件目录 所有者 = BUILTIN\Administrators (CIS) 所有者 = ... Compliant High
服务配置 服务: RemoteRegistry 已禁用 (CIS 9.x) 状态: Running Non-Compliant Medium
注册表安全 UAC 提示级别应为"始终通知" (CIS 2.3.17.5) 值 = 2 值 = 5 Non-Compliant Medium

合规报告生成

审计数据的最终价值体现在报告上。下面的函数将前面两个模块的检查结果汇总,生成一份结构化的 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
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
function New-ComplianceReport {
[CmdletBinding()]
param(
[string]$OutputPath = "$env:USERPROFILE\Desktop\ComplianceReport_$(Get-Date -Format 'yyyyMMdd_HHmmss').html",
[string]$Organization = 'IT 安全团队'
)

Write-Host "正在执行安全基线检查..." -ForegroundColor Cyan
$baselineResults = Invoke-SecurityBaselineCheck

Write-Host "正在执行系统配置审计..." -ForegroundColor Cyan
$configResults = Invoke-SystemConfigAudit

$allResults = @($baselineResults) + @($configResults)
$totalCount = $allResults.Count
$compliantCount = ($allResults | Where-Object { $_.Status -eq 'Compliant' }).Count
$complianceRate = [math]::Round(($compliantCount / $totalCount) * 100, 1)

# 按严重等级统计
$severityStats = $allResults |
Group-Object Severity |
Select-Object Name, Count

# 按类别统计合规率
$categoryStats = $allResults |
Group-Object Category |
ForEach-Object {
$compliant = ($_.Group | Where-Object { $_.Status -eq 'Compliant' }).Count
[PSCustomObject]@{
Category = $_.Name
Total = $_.Count
Compliant = $compliant
NonCompliant = $_.Count - $compliant
ComplianceRate = [math]::Round(($compliant / $_.Count) * 100, 1)
}
}

# 修复建议汇总
$remediations = $allResults |
Where-Object { $_.Status -eq 'Non-Compliant' -and $_.Remediation } |
Select-Object CheckItem, Severity, Remediation |
Sort-Object Severity

Write-Host "正在生成 HTML 报告..." -ForegroundColor Cyan

$severityColorMap = @{
'Critical' = '#dc3545'
'High' = '#fd7e14'
'Medium' = '#ffc107'
'Low' = '#28a745'
}

# 构建 HTML 报告
$htmlSections = @()
$htmlSections += '<!DOCTYPE html><html lang="zh-CN"><head>'
$htmlSections += '<meta charset="UTF-8">'
$htmlSections += '<title>合规审计报告</title>'
$htmlSections += '<style>'
$htmlSections += 'body { font-family: "Microsoft YaHei", sans-serif; margin: 20px; background: #f5f5f5; }'
$htmlSections += 'h1 { color: #333; border-bottom: 3px solid #007bff; padding-bottom: 10px; }'
$htmlSections += '.summary { display: flex; gap: 20px; margin: 20px 0; }'
$htmlSections += '.card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); flex: 1; text-align: center; }'
$htmlSections += '.rate { font-size: 48px; font-weight: bold; }'
$htmlSections += 'table { width: 100%; border-collapse: collapse; background: white; margin: 10px 0; }'
$htmlSections += 'th, td { border: 1px solid #ddd; padding: 10px; text-align: left; }'
$htmlSections += 'th { background: #007bff; color: white; }'
$htmlSections += '.compliant { color: #28a745; font-weight: bold; }'
$htmlSections += '.non-compliant { color: #dc3545; font-weight: bold; }'
$htmlSections += '</style></head><body>'
$htmlSections += "<h1>合规审计报告</h1>"
$htmlSections += "<p>组织: $Organization | 生成时间: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') | 计算机: $env:COMPUTERNAME</p>"

# 概览卡片
$rateColor = if ($complianceRate -ge 80) { '#28a745' } elseif ($complianceRate -ge 60) { '#ffc107' } else { '#dc3545' }
$htmlSections += '<div class="summary">'
$htmlSections += "<div class='card'><div class='rate' style='color:$rateColor'>$complianceRate%</div><div>总体合规率</div></div>"
$htmlSections += "<div class='card'><div class='rate' style='color:#333'>$compliantCount / $totalCount</div><div>合规项 / 总检查项</div></div>"

$criticalCount = ($severityStats | Where-Object { $_.Name -eq 'Critical' }).Count
$htmlSections += "<div class='card'><div class='rate' style='color:#dc3545'>$criticalCount</div><div>Critical 不合规项</div></div>"
$htmlSections += '</div>'

# 类别合规率表格
$htmlSections += '<h2>分类合规率</h2><table><tr><th>类别</th><th>总数</th><th>合规</th><th>不合规</th><th>合规率</th></tr>'
foreach ($cat in $categoryStats) {
$catRateColor = if ($cat.ComplianceRate -ge 80) { 'compliant' } else { 'non-compliant' }
$htmlSections += "<tr><td>$($cat.Category)</td><td>$($cat.Total)</td><td>$($cat.Compliant)</td><td>$($cat.NonCompliant)</td><td class='$catRateColor'>$($cat.ComplianceRate)%</td></tr>"
}
$htmlSections += '</table>'

# 详细结果表格
$htmlSections += '<h2>详细检查结果</h2><table><tr><th>类别</th><th>检查项</th><th>期望值</th><th>实际值</th><th>状态</th><th>严重等级</th></tr>'
foreach ($item in ($allResults | Sort-Object Severity, Category)) {
$statusClass = if ($item.Status -eq 'Compliant') { 'compliant' } else { 'non-compliant' }
$sevColor = $severityColorMap[$item.Severity]
$htmlSections += "<tr><td>$($item.Category)</td><td>$($item.CheckItem)</td><td>$($item.Expected)</td><td>$($item.Actual)</td><td class='$statusClass'>$($item.Status)</td><td style='color:$sevColor'>$($item.Severity)</td></tr>"
}
$htmlSections += '</table>'

# 修复建议
if ($remediations) {
$htmlSections += '<h2>修复建议</h2><table><tr><th>检查项</th><th>严重等级</th><th>修复命令</th></tr>'
foreach ($rem in $remediations) {
$remSevColor = $severityColorMap[$rem.Severity]
$htmlSections += "<tr><td>$($rem.CheckItem)</td><td style='color:$remSevColor'>$($rem.Severity)</td><td><code>$($rem.Remediation)</code></td></tr>"
}
$htmlSections += '</table>'
}

$htmlSections += '</body></html>'

$htmlSections -join "`n" | Out-File -FilePath $OutputPath -Encoding UTF8

# 控制台输出汇总
[PSCustomObject]@{
ReportPath = $OutputPath
TotalChecks = $totalCount
CompliantItems = $compliantCount
NonCompliantItems = $totalCount - $compliantCount
ComplianceRate = "$complianceRate%"
CriticalIssues = $criticalCount
GeneratedAt = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
}
}

运行报告生成函数后,控制台输出汇总信息,同时在桌面生成 HTML 报告文件:

1
2
3
4
5
6
7
8
9
10
11
正在执行安全基线检查...
正在执行系统配置审计...
正在生成 HTML 报告...

ReportPath : C:\Users\Admin\Desktop\ComplianceReport_20260327_100530.html
TotalChecks : 24
CompliantItems : 17
NonCompliantItems: 7
ComplianceRate : 70.8%
CriticalIssues : 2
GeneratedAt : 2026-03-27 10:05:30

注意事项

  1. 管理员权限要求:安全基线检查和系统配置审计需要以管理员身份运行 PowerShell。账户策略、审核策略和注册表部分操作均涉及系统级配置,普通用户权限无法获取完整信息。

  2. CIS Benchmark 版本对应:本文中的检查项基于 CIS Microsoft Windows Server Benchmark v3.0 编写。不同版本的 Benchmark 可能存在差异,建议根据实际部署的基线版本调整 Expected 字段中的判定标准。

  3. 生产环境慎用修复命令:报告中给出的修复命令(Remediation 字段)仅供参考。直接在生产环境中执行这些命令可能导致服务中断(如禁用 RemoteRegistry 可能影响远程管理工具),务必先在测试环境验证。

  4. 远程审计扩展:上述函数目前仅审计本地计算机。如需批量审计多台服务器,可以结合 Invoke-Command -ComputerName $servers -ScriptBlock { ... } 实现,但需注意 WinRM 服务的连通性和防火墙配置。

  5. HTML 报告编码问题Out-File -Encoding UTF8 在 Windows PowerShell 5.1 中会生成带 BOM 的 UTF-8 文件,在浏览器中通常能正确显示。如果遇到中文乱码,可尝试使用 [System.IO.File]::WriteAllText($OutputPath, $htmlContent, [System.Text.Encoding]::UTF8) 替代。

  6. 定期审计与趋势跟踪:建议将合规审计脚本纳入计划任务(schtasks),每周或每月自动执行一次,并将报告归档保存。通过对比不同时期的合规率变化,可以追踪安全态势的改善趋势,为管理层的合规决策提供数据支撑。

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 技能连载 - 会话录制与回放

适用于 PowerShell 5.1 及以上版本

在安全审计、故障排查、培训演示等场景中,记录操作过程至关重要。PowerShell 的 Start-Transcript 可以记录控制台的完整输入输出,包括命令、结果、错误信息。结合日志轮转和自动化脚本,可以构建完整的操作审计系统,确保所有关键操作可追溯、可回放。

本文将讲解 PowerShell 会话录制的技术和实用方案。

基础会话录制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 开始录制
Start-Transcript -Path "C:\Transcripts\session-$(Get-Date -Format 'yyyyMMdd_HHmmss').txt" -Append
Write-Host "会话录制已开始" -ForegroundColor Green

# 正常操作...
Get-Process | Select-Object -First 3 Name, Id, CPU
Write-Host "当前时间:$(Get-Date)" -ForegroundColor Cyan

# 停止录制
Stop-Transcript
Write-Host "会话录制已停止" -ForegroundColor Yellow

# 查看录制内容
$latestTranscript = Get-ChildItem "C:\Transcripts" -Filter "*.txt" |
Sort-Object LastWriteTime -Descending | Select-Object -First 1

Write-Host "`n最新录制文件:$($latestTranscript.Name) ($([math]::Round($latestTranscript.Length / 1KB, 1)) KB)"

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
会话录制已开始
**********************
Windows PowerShell transcript start
Start time: 20250829103015
Username: CONTOSO\admin
Runspace Id: a1b2c3d4-e5f6-7890-abcd-ef1234567890

Name Id CPU
---- -- ---
chrome 12345 1250.3
node 12346 456.7
pwsh 12347 123.4

当前时间:2025-08-29 10:30:20
Windows PowerShell transcript end
End time: 20250829103021
**********************
会话录制已停止

最新录制文件:session-20250829_103015.txt (2.3 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
# 在 $PROFILE 中自动开始录制
function Install-AutoTranscript {
$profileDir = Split-Path $PROFILE -Parent
if (-not (Test-Path $profileDir)) {
New-Item $profileDir -ItemType Directory -Force | Out-Null
}

$transcriptBlock = @"

# ===== 自动会话录制 =====
`$transcriptDir = "`$env:USERPROFILE\Transcripts"
if (-not (Test-Path `$transcriptDir)) {
New-Item `$transcriptDir -ItemType Directory -Force | Out-Null
}
`$dateStamp = Get-Date -Format 'yyyyMMdd_HHmmss'
`$transcriptFile = Join-Path `$transcriptDir "transcript-`$dateStamp.txt"
Start-Transcript -Path `$transcriptFile -Append
# ===== 自动会话录制结束 =====
"@

if (-not (Test-Path $PROFILE)) {
Set-Content -Path $PROFILE -Value $transcriptBlock -Encoding UTF8
} else {
$content = Get-Content $PROFILE -Raw
if ($content -notmatch 'Start-Transcript') {
Add-Content -Path $PROFILE -Value $transcriptBlock -Encoding UTF8
}
}

Write-Host "自动录制已配置到 $PROFILE" -ForegroundColor Green
Write-Host "下次启动 PowerShell 时自动开始录制" -ForegroundColor Yellow
}

Install-AutoTranscript

# 管理录制文件
function Get-TranscriptSummary {
param([string]$Path = "$env:USERPROFILE\Transcripts")

$transcripts = Get-ChildItem $Path -Filter "transcript-*.txt" -ErrorAction SilentlyContinue

if (-not $transcripts) {
Write-Host "未找到录制文件" -ForegroundColor Yellow
return
}

Write-Host "录制文件概览:" -ForegroundColor Cyan
Write-Host " 总文件数:$($transcripts.Count)"
$totalSize = [math]::Round(($transcripts | Measure-Object Length -Sum).Sum / 1MB, 2)
Write-Host " 总大小:$totalSize MB"

$transcripts | Sort-Object LastWriteTime -Descending | Select-Object -First 5 |
ForEach-Object {
$sizeKB = [math]::Round($_.Length / 1KB, 1)
Write-Host " $($_.Name) ($sizeKB KB) - $($_.LastWriteTime.ToString('yyyy-MM-dd HH:mm'))" -ForegroundColor DarkGray
}
}

Get-TranscriptSummary

执行结果示例:

1
2
3
4
5
6
7
8
9
自动录制已配置到 C:\Users\admin\Documents\PowerShell\Microsoft.PowerShell_profile.ps1
下次启动 PowerShell 时自动开始录制

录制文件概览:
总文件数:45
总大小:12.5 MB
transcript-20250829_103015.txt (2.3 KB) - 2025-08-29 10:30
transcript-20250829_090000.txt (5.6 KB) - 2025-08-29 09:00
transcript-20250828_140530.txt (8.2 KB) - 2025-08-28 14:05

录制文件清理

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
function Clear-OldTranscripts {
param(
[string]$Path = "$env:USERPROFILE\Transcripts",
[int]$KeepDays = 90,
[switch]$WhatIf
)

$cutoff = (Get-Date).AddDays(-$KeepDays)

$oldFiles = Get-ChildItem $Path -Filter "transcript-*.txt" |
Where-Object { $_.LastWriteTime -lt $cutoff }

if (-not $oldFiles) {
Write-Host "没有需要清理的录制文件" -ForegroundColor Green
return
}

$totalSize = [math]::Round(($oldFiles | Measure-Object Length -Sum).Sum / 1MB, 2)

Write-Host "找到 $($oldFiles.Count) 个超过 $KeepDays 天的录制文件($totalSize MB)" -ForegroundColor Yellow

if ($WhatIf) {
Write-Host "[WhatIf] 将删除以上文件" -ForegroundColor Yellow
return
}

$oldFiles | Remove-Item -Force
Write-Host "已清理 $($oldFiles.Count) 个文件,释放 $totalSize MB" -ForegroundColor Green
}

Clear-OldTranscripts -KeepDays 90 -WhatIf

执行结果示例:

1
2
找到 12 个超过 90 天的录制文件(3.45 MB)
[WhatIf] 将删除以上文件

结构化操作日志

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
# 组合 Transcript 和结构化日志
function Invoke-LoggedOperation {
param(
[Parameter(Mandatory)]
[string]$OperationName,

[Parameter(Mandatory)]
[scriptblock]$Operation,

[string]$LogDir = "C:\Logs\Operations"
)

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

$timestamp = Get-Date -Format 'yyyyMMdd_HHmmss'
$transcriptPath = Join-Path $LogDir "$OperationName-$timestamp.txt"

$result = [PSCustomObject]@{
Operation = $OperationName
StartTime = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
Status = "Unknown"
Duration = ""
Transcript = $transcriptPath
Error = ""
}

$sw = [System.Diagnostics.Stopwatch]::StartNew()

try {
Start-Transcript -Path $transcriptPath -Force | Out-Null
& $Operation
Stop-Transcript | Out-Null

$result.Status = "Success"
} catch {
Stop-Transcript | Out-Null
$result.Status = "Failed"
$result.Error = $_.Exception.Message
} finally {
$sw.Stop()
$result.Duration = "$($sw.Elapsed.ToString('mm\:ss'))"
}

# 写入结构化日志
$logEntry = "[$($result.StartTime)] [$($result.Status)] $($result.Operation) ($($result.Duration))"
Add-Content (Join-Path $LogDir "operations.log") -Value $logEntry -Encoding UTF8

$color = if ($result.Status -eq "Success") { "Green" } else { "Red" }
Write-Host $logEntry -ForegroundColor $color

return $result
}

# 使用示例
Invoke-LoggedOperation -OperationName "Deploy-MyApp" -Operation {
Write-Host "停止服务..." -ForegroundColor Cyan
Stop-Service "MyApp" -ErrorAction SilentlyContinue

Write-Host "复制文件..." -ForegroundColor Cyan
Copy-Item "C:\Releases\MyApp\*" "D:\Apps\MyApp\" -Recurse -Force

Write-Host "启动服务..." -ForegroundColor Cyan
Start-Service "MyApp"

Write-Host "验证..." -ForegroundColor Cyan
$svc = Get-Service "MyApp"
Write-Host "服务状态:$($svc.Status)"
}

执行结果示例:

1
2
3
4
5
6
停止服务...
复制文件...
启动服务...
验证...
服务状态:Running
[2025-08-29 10:35:20] [Success] Deploy-MyApp (01:23)

注意事项

  1. 敏感信息:Transcript 会记录所有控制台输出,包括密码输入。敏感操作后及时清理或加密存储
  2. 磁盘空间:长期开启自动录制会积累大量文件,配置定期清理策略
  3. 性能影响:Transcript 的性能开销很小,但在极端高频输出场景中可能有影响
  4. 编码问题:Transcript 文件使用系统默认编码,中文内容可能需要 UTF-8 转换
  5. 嵌套限制:已经在一个 Transcript 中时不能再次 Start-Transcript(除非加 -Append
  6. 远程会话:通过 Enter-PSSession 进入的远程会话需要单独配置 Transcript

PowerShell 技能连载 - 会话录制与审计追踪

适用于 PowerShell 5.1 及以上版本

在企业安全合规中,”谁在什么时候执行了什么操作”是最基本也最重要的审计需求。无论是金融行业的操作审计、医疗行业的 HIPAA 合规,还是日常运维的事故排查,完整的操作记录都不可或缺。PowerShell 内置的 Start-Transcript 可以记录整个会话的输入输出,结合脚本日志(Script Block Logging)和模块日志(Module Logging),可以构建完整的审计追踪体系。

本文将讲解 PowerShell 会话录制的多种方式,以及如何构建满足安全合规要求的审计系统。

Start-Transcript 基础录制

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
# Start-Transcript 记录会话中的所有输入和输出

function Start-SessionRecording {
param(
[string]$LogRoot = "C:\Logs\PowerShell\Transcripts",

[string]$SessionName = "Interactive"
)

# 确保日志目录存在
$datePath = Join-Path $LogRoot (Get-Date -Format "yyyy\\MM")
New-Item -Path $datePath -ItemType Directory -Force | Out-Null

# 生成唯一文件名:日期_主机_用户_会话ID
$timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
$computer = $env:COMPUTERNAME
$user = $env:USERNAME
$sessionId = [System.Diagnostics.Process]::GetCurrentProcess().Id
$fileName = "${timestamp}_${computer}_${user}_${sessionId}.txt"

$transcriptPath = Join-Path $datePath $fileName

# 启动录制
Start-Transcript -Path $transcriptPath -IncludeInvocationHeader -Force | Out-Null

# 在录制文件中写入元数据标记
Write-Host "========================================"
Write-Host "会话录制已启动"
Write-Host " 会话类型:$SessionName"
Write-Host " 操作人员:$user"
Write-Host " 计算机名:$computer"
Write-Host " 进程 ID:$sessionId"
Write-Host " 开始时间:$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
Write-Host " 录制文件:$transcriptPath"
Write-Host "========================================"

return $transcriptPath
}

function Stop-SessionRecording {
param(
[string]$Reason = "正常结束"
)

Write-Host "========================================"
Write-Host "会话录制即将停止"
Write-Host " 结束原因:$Reason"
Write-Host " 结束时间:$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
Write-Host "========================================"

Stop-Transcript | Out-Null
Write-Host "录制已停止并保存" -ForegroundColor Green
}

# 使用示例
$transcriptFile = Start-SessionRecording -SessionName "Maintenance"

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
**************************************
Windows PowerShell transcript start
Start time: 20250808083000
Username: CONTOSO\Admin
RunAs User: CONTOSO\Admin
Machine: SRV-WEB01 (Microsoft Windows NT 10.0.20348.0)
========================================
会话录制已启动
会话类型:Maintenance
操作人员:Admin
计算机名:SRV-WEB01
进程 ID:12345
开始时间:2025-08-08 08:30:00
录制文件:C:\Logs\PowerShell\Transcripts\2025\08\20250808_083000_SRV-WEB01_Admin_12345.txt
========================================

脚本内嵌录制

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
# 在脚本中自动启用录制,确保运维操作可追溯

function Invoke-WithAuditTrail {
param(
[Parameter(Mandatory)]
[scriptblock]$ScriptBlock,

[string]$OperationName = "UnnamedOperation",

[string]$LogRoot = "C:\Logs\PowerShell\Audit",

[hashtable]$Metadata
)

# 构建审计日志路径
$datePath = Join-Path $LogRoot (Get-Date -Format "yyyy\\MM\\dd")
New-Item -Path $datePath -ItemType Directory -Force | Out-Null

$timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
$transcriptPath = Join-Path $datePath "${timestamp}_${OperationName}.log"

# 记录审计元数据
$auditHeader = @(
"=== 审计记录 ==="
" 操作名称:$OperationName"
" 执行用户:$env:USERNAME"
" 计算机名:$env:COMPUTERNAME"
" 开始时间:$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
)

if ($Metadata) {
$auditHeader += " --- 自定义元数据 ---"
foreach ($key in $Metadata.Keys) {
$auditHeader += " ${key}: $($Metadata[$key])"
}
}

# 启动录制
Start-Transcript -Path $transcriptPath -Force | Out-Null
$auditHeader | ForEach-Object { Write-Host $_ }

$success = $false
$errorMessage = ""
$startTime = Get-Date

try {
# 执行目标脚本
$result = & $ScriptBlock
$success = $true
Write-Host "=== 操作完成 ==="
Write-Host " 结果:成功"
} catch {
$success = $false
$errorMessage = $_.Exception.Message
Write-Host "=== 操作失败 ===" -ForegroundColor Red
Write-Host " 错误信息:$errorMessage" -ForegroundColor Red
} finally {
$endTime = Get-Date
$duration = ($endTime - $startTime).ToString("hh\:mm\:ss")

Write-Host " 耗时:$duration"
Write-Host " 结束时间:$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
Stop-Transcript | Out-Null
}

# 返回审计摘要
return [PSCustomObject]@{
Operation = $OperationName
Transcript = $transcriptPath
Success = $success
Error = $errorMessage
StartTime = $startTime
EndTime = $endTime
Duration = $duration
}
}

# 使用示例
$auditResult = Invoke-WithAuditTrail -OperationName "Deploy-MyApp" -Metadata @{
Version = "2.5.0"
Env = "Production"
Approver = "Manager Zhang"
Ticket = "OPS-2025-0808"
} -ScriptBlock {
Write-Host "开始部署 MyApp v2.5.0..."
Start-Sleep -Seconds 2
Write-Host "复制文件到目标目录..."
Write-Host "重启服务..."
Write-Host "验证服务状态..."
"DeployComplete"
}

$auditResult | Format-List

执行结果示例:

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
=== 审计记录 ===
操作名称:Deploy-MyApp
执行用户:Admin
计算机名:SRV-WEB01
开始时间:2025-08-08 08:30:00
--- 自定义元数据 ---
Version: 2.5.0
Env: Production
Approver: Manager Zhang
Ticket: OPS-2025-0808
开始部署 MyApp v2.5.0...
复制文件到目标目录...
重启服务...
验证服务状态...
=== 操作完成 ===
结果:成功
耗时:00:00:02
结束时间:2025-08-08 08:30:02

Operation : Deploy-MyApp
Transcript : C:\Logs\PowerShell\Audit\2025\08\08\20250808_083000_Deploy-MyApp.log
Success : True
Error :
StartTime : 2025-08-08 08:30:00
EndTime : 2025-08-08 08:30:02
Duration : 00:00:02

Script Block Logging 集成

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
# 读取 Windows 事件日志中的 PowerShell 脚本块日志
# 需要提前启用 Script Block Logging(组策略或注册表)

function Enable-ScriptBlockLogging {
param(
[switch]$IncludeWarningLevel
)

$regPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging"

if (-not (Test-Path $regPath)) {
New-Item -Path $regPath -Force | Out-Null
}

# 启用脚本块日志
Set-ItemProperty -Path $regPath -Name "EnableScriptBlockLogging" -Value 1

if ($IncludeWarningLevel) {
# 同时记录警告级别(如混淆代码、可疑命令)
Set-ItemProperty -Path $regPath -Name "EnableScriptBlockInvocationLogging" -Value 1
}

Write-Host "Script Block Logging 已启用" -ForegroundColor Green
}

function Get-ScriptBlockLog {
param(
[int]$MaxEvents = 50,

[datetime]$StartTime,

[string]$OutputPath
)

$params = @{
LogName = "Microsoft-Windows-PowerShell/Operational"
Id = 4104
MaxEvents = $MaxEvents
}

if ($StartTime) {
$params["StartTime"] = $StartTime
}

$events = Get-WinEvent @params -ErrorAction SilentlyContinue

if (-not $events) {
Write-Warning "未找到脚本块日志"
return
}

$logEntries = foreach ($event in $events) {
[PSCustomObject]@{
TimeCreated = $event.TimeCreated
Level = $event.LevelDisplayName
Computer = $event.MachineName
UserId = $event.UserId
ScriptBlock = $event.Properties[0].Value.Substring(
0, [Math]::Min(200, $event.Properties[0].Value.Length)
)
Path = if ($event.Properties.Count -gt 1) {
$event.Properties[1].Value
} else {
"Interactive"
}
}
}

if ($OutputPath) {
$logEntries | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8
Write-Host "已导出 $($logEntries.Count) 条日志到:$OutputPath" -ForegroundColor Green
}

return $logEntries
}

# 启用脚本块日志
Enable-ScriptBlockLogging -IncludeWarningLevel

# 查询最近的脚本块日志
Get-ScriptBlockLog -MaxEvents 10 -StartTime (Get-Date).AddHours(-1) |
Format-Table TimeCreated, Level, Path -AutoSize

执行结果示例:

1
2
3
4
5
6
7
8
Script Block Logging 已启用

TimeCreated Level Path
----------- ----- ----
2025-08-08 08:28:15 Information Interactive
2025-08-08 08:28:14 Information Interactive
2025-08-08 08:25:30 Warning C:\Scripts\deploy.ps1
2025-08-08 08:25:29 Information C:\Scripts\deploy.ps1

审计日志搜索与分析

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
# 审计日志分析工具:从 Transcript 文件中搜索敏感操作

function Search-AuditLog {
param(
[Parameter(Mandatory)]
[string]$LogRoot,

[string[]]$Keywords = @("Remove-", "Stop-", "Delete", "Drop", "Format"),

[datetime]$StartDate,

[datetime]$EndDate,

[string]$UserFilter,

[int]$ContextLines = 3
)

# 查找所有 transcript 文件
$files = Get-ChildItem -Path $LogRoot -Recurse -Filter "*.txt" -File

if ($StartDate) {
$files = $files | Where-Object { $_.LastWriteTime -ge $StartDate }
}
if ($EndDate) {
$files = $files | Where-Object { $_.LastWriteTime -le $EndDate }
}

Write-Host "扫描 $($files.Count) 个审计日志文件..." -ForegroundColor Cyan

$findings = @()
$pattern = ($Keywords | ForEach-Object { [regex]::Escape($_) }) -join "|"

foreach ($file in $files) {
$lines = Get-Content $file.FullName -Encoding UTF8

# 提取用户信息
$userLine = $lines | Where-Object { $_ -match "^Username:" } | Select-Object -First 1
$fileUser = if ($userLine -match "Username:\s*(.+)") {
$Matches[1].Trim()
} else {
"Unknown"
}

# 用户过滤
if ($UserFilter -and $fileUser -notlike "*$UserFilter*") {
continue
}

for ($i = 0; $i -lt $lines.Count; $i++) {
if ($lines[$i] -match $pattern) {
# 收集上下文行
$start = [Math]::Max(0, $i - $ContextLines)
$end = [Math]::Min($lines.Count - 1, $i + $ContextLines)
$context = ($lines[$start..$end] | Where-Object { $_ }) -join "`n"

$findings += [PSCustomObject]@{
File = $file.FullName
User = $fileUser
LineNum = $i + 1
Match = $lines[$i].Trim()
Context = $context
Timestamp = $file.LastWriteTime
}
}
}
}

Write-Host "发现 $($findings.Count) 条敏感操作记录" `
-ForegroundColor $(if ($findings.Count -gt 0) { "Yellow" } else { "Green" })

return $findings
}

# 生成审计摘要报告
function New-AuditSummaryReport {
param(
[Parameter(Mandatory)]
[object[]]$Findings,

[string]$OutputPath = "C:\Reports\AuditSummary.html"
)

$reportDir = Split-Path $OutputPath -Parent
New-Item $reportDir -ItemType Directory -Force | Out-Null

$byUser = $Findings | Group-Object -Property User | Sort-Object Count -Descending
$byKeyword = foreach ($f in $Findings) {
foreach ($kw in @("Remove-", "Stop-", "Delete", "Drop")) {
if ($f.Match -match [regex]::Escape($kw)) {
[PSCustomObject]@{ Keyword = $kw; Match = $f.Match }
}
}
} | Group-Object -Property Keyword | Sort-Object Count -Descending

$html = @"
<!DOCTYPE html>
<html><head><meta charset="UTF-8"><title>审计摘要报告</title>
<style>
body{font-family:'Segoe UI',sans-serif;margin:20px;background:#f5f6fa}
h1{color:#2d3436;border-bottom:2px solid #e74c3c}
h2{color:#2d3436;margin-top:30px}
table{width:100%;border-collapse:collapse;margin:10px 0}
th{background:#0984e3;color:white;padding:10px;text-align:left}
td{padding:8px 10px;border-bottom:1px solid #dfe6e9}
.summary{display:flex;gap:15px;margin:15px 0}
.card{background:white;border-radius:8px;padding:15px;box-shadow:0 2px 4px rgba(0,0,0,0.1)}
.card .value{font-size:24px;font-weight:bold}
</style></head><body>
<h1>审计摘要报告</h1>
<p>生成时间:$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')</p>
<div class="summary">
<div class="card"><div class="value" style="color:#e74c3c">$($Findings.Count)</div><div>敏感操作总数</div></div>
<div class="card"><div class="value" style="color:#0984e3">$($byUser.Count)</div><div>涉及用户</div></div>
</div>
<h2>按用户统计</h2><table><tr><th>用户</th><th>操作次数</th></tr>
$($byUser | ForEach-Object { "<tr><td>$($_.Name)</td><td>$($_.Count)</td></tr>" })
</table>
<h2>按操作类型统计</h2><table><tr><th>关键字</th><th>次数</th></tr>
$($byKeyword | ForEach-Object { "<tr><td>$($_.Name)</td><td>$($_.Count)</td></tr>" })
</table></body></html>
"@

$html | Set-Content $OutputPath -Encoding UTF8
Write-Host "审计报告已生成:$OutputPath" -ForegroundColor Green
}

# 使用示例
$results = Search-AuditLog -LogRoot "C:\Logs\PowerShell\Transcripts" `
-Keywords @("Remove-", "Stop-", "Delete") `
-StartDate (Get-Date).AddDays(-7)

if ($results) {
New-AuditSummaryReport -Findings $results
}

执行结果示例:

1
2
3
4
扫描 42 个审计日志文件...
发现 15 条敏感操作记录

审计报告已生成:C:\Reports\AuditSummary.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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# 通过 Profile 配置所有 PowerShell 会话自动录制

$profileContent = @'
# ===== 全局审计录制 =====
$transcriptRoot = "C:\Logs\PowerShell\Transcripts"
$dateFolder = Join-Path $transcriptRoot (Get-Date -Format "yyyy\\MM")
New-Item -Path $dateFolder -ItemType Directory -Force | Out-Null

$sessionId = [System.Diagnostics.Process]::GetCurrentProcess().Id
$transcriptFile = Join-Path $dateFolder "$(Get-Date -Format 'yyyyMMdd_HHmmss')_${env:COMPUTERNAME}_${env:USERNAME}_${sessionId}.txt"

try {
Start-Transcript -Path $transcriptFile -IncludeInvocationHeader -Force | Out-Null
} catch {
# 录制启动失败不阻止正常工作
Write-Debug "Transcript 启动失败:$($_.Exception.Message)"
}

# 注册会话退出事件,确保录制正常结束
$null = Register-EngineEvent -SourceIdentifier PowerShell.Exiting -Action {
try {
Stop-Transcript | Out-Null
} catch {
# 忽略退出时的错误
}
}
'@

# 显示配置说明
function Set-GlobalTranscript {
param(
[string]$ProfilePath = $PROFILE.AllUsersAllHosts
)

Write-Host "将以下内容添加到 PowerShell Profile:" -ForegroundColor Cyan
Write-Host " 路径:$ProfilePath" -ForegroundColor Yellow
Write-Host ""

if (Test-Path $ProfilePath) {
$existing = Get-Content $ProfilePath -Raw -Encoding UTF8
if ($existing -match "Start-Transcript") {
Write-Warning "Profile 中已存在 Start-Transcript 配置,跳过"
return
}

# 追加到已有 Profile
$profileContent | Add-Content $ProfilePath -Encoding UTF8
Write-Host "已追加到现有 Profile" -ForegroundColor Green
} else {
$profileDir = Split-Path $ProfilePath -Parent
New-Item -Path $profileDir -ItemType Directory -Force | Out-Null
$profileContent | Set-Content $ProfilePath -Encoding UTF8
Write-Host "已创建新 Profile" -ForegroundColor Green
}

Write-Host ""
Write-Host "新会话将自动启动录制。" -ForegroundColor Green
Write-Host "注意:所有用户的所有 PowerShell 会话都将被录制。" -ForegroundColor Yellow
}

# 检查当前录制状态
function Get-TranscriptStatus {
$transcript = try { Get-Transcript } catch { $null }

if ($transcript) {
[PSCustomObject]@{
IsRecording = $true
Path = $transcript.Path
StartTime = $transcript.StartTime
SizeKB = if (Test-Path $transcript.Path) {
[Math]::Round((Get-Item $transcript.Path).Length / 1KB, 1)
} else { "N/A" }
}
} else {
[PSCustomObject]@{
IsRecording = $false
Path = "N/A"
StartTime = "N/A"
SizeKB = "N/A"
}
}
}

Get-TranscriptStatus | Format-List

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
将以下内容添加到 PowerShell Profile:
路径:C:\Windows\System32\WindowsPowerShell\v1.0\profile.ps1
已创建新 Profile

新会话将自动启动录制。
注意:所有用户的所有 PowerShell 会话都将被录制。

IsRecording : True
Path : C:\Logs\PowerShell\Transcripts\2025\08\20250808_083000_SRV-WEB01_Admin_12345.txt
StartTime : 2025-08-08 08:30:00
SizeKB : 12.5

注意事项

  1. 存储空间:Transcript 文件会持续增长,务必配置定期清理策略(如保留 90 天),避免磁盘被填满
  2. 敏感信息:录制会捕获密码输入等敏感内容,生产环境中应在密码提示前暂停录制或使用 Read-Host -AsSecureString
  3. 性能影响:Script Block Logging 在高频脚本环境中可能产生大量事件日志,建议仅在高安全要求的服务器上启用
  4. 日志完整性:有权限的用户可以执行 Stop-Transcript 停止录制,关键系统应结合 Windows 事件转发(WEF)集中收集日志
  5. 合规要求:不同行业对日志保留期限有不同要求(如金融行业通常 7 年),需根据实际合规标准配置保留策略
  6. 编码一致性:Transcript 文件使用系统默认编码,在跨语言环境中读取时可能出现乱码,分析工具应指定正确的编码