PowerShell 技能连载 - 事件日志深度分析

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

Windows 事件日志是系统运维和安全审计的核心数据源。无论是排查服务崩溃、追踪用户登录行为,还是进行安全取证分析,事件日志都提供了不可替代的线索。然而,面对动辄数十万条日志记录,手动翻阅事件查看器显然效率低下。

PowerShell 内置的 Get-WinEvent cmdlet 拥有强大的过滤和查询能力,配合结构化对象输出,可以大幅提升日志分析效率。与旧版的 Get-EventLog(已在 PowerShell 7 中移除)不同,Get-WinEvent 支持所有日志通道(包括 ETL 诊断日志),并能通过 XPath 和哈希表实现高效的服务端过滤。

本文将从安全审计和故障排查两个场景出发,演示如何用 PowerShell 构建一套实用的事件日志深度分析脚本。重点在于掌握过滤技巧和结果聚合方法,让海量日志真正为你所用。

按时间窗口和级别提取安全日志

在安全审计场景中,最常见的操作是从安全日志中提取特定时间段内的事件。以下脚本演示如何查询最近 24 小时内的登录成功事件(Event ID 4624),并提取关键信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$startTime = (Get-Date).AddHours(-24)
$logonEvents = Get-WinEvent -FilterHashtable @{
LogName = 'Security'
Id = 4624
StartTime = $startTime
} -ErrorAction SilentlyContinue

$results = @()
foreach ($event in $logonEvents) {
$xml = [xml]$event.ToXml()
$data = $xml.Event.EventData.Data
$targetUserName = ($data | Where-Object { $_.Name -eq 'TargetUserName' }).'#text'
$logonType = ($data | Where-Object { $_.Name -eq 'LogonType' }).'#text'
$ipAddress = ($data | Where-Object { $_.Name -eq 'IpAddress' }).'#text'

$results += [PSCustomObject]@{
Time = $event.TimeCreated
User = $targetUserName
LogonType = $logonType
SourceIP = $ipAddress
}
}

$results | Sort-Object Time -Descending | Format-Table -AutoSize

上述代码使用 FilterHashtable 进行服务端过滤,只将符合条件的记录传输到客户端,避免大量无用数据占用内存。通过解析事件的 XML 结构,我们可以提取出事件数据中任意命名的字段。注意这里使用 foreach 循环而非管道,在处理大量记录时性能更稳定。

执行结果示例:

1
2
3
4
5
6
7
Time                  User           LogonType SourceIP
---- ---- --------- --------
2025/10/27 07:52:14 administrator 10 192.168.1.105
2025/10/27 07:48:33 jzhang 2 192.168.1.42
2025/10/27 07:30:19 SYSTEM 5 -
2025/10/27 06:15:42 lwei 10 10.0.0.88
2025/10/27 05:02:07 administrator 7 127.0.0.1

其中 LogonType 的含义尤为关键:类型 2 表示交互式登录(控制台),类型 10 表示远程桌面登录,类型 5 表示服务启动。如果发现异常的远程登录记录,应立即排查来源 IP 是否合法。

聚合高频错误事件并生成统计报告

在故障排查场景中,快速定位哪些错误出现频率最高,往往比逐条分析更有效率。以下脚本从 System 和 Application 两个日志中提取最近 7 天的 Error 和 Critical 级别事件,按事件 ID 分组统计。

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
$startTime = (Get-Date).AddDays(-7)
$logNames = @('System', 'Application')

$allErrors = @()
foreach ($logName in $logNames) {
$events = Get-WinEvent -FilterHashtable @{
LogName = $logName
Level = 1, 2
StartTime = $startTime
} -ErrorAction SilentlyContinue

foreach ($event in $events) {
$allErrors += [PSCustomObject]@{
LogName = $event.LogName
EventId = $event.Id
Level = $event.LevelDisplayName
Message = $event.Message.Substring(0, [Math]::Min(120, $event.Message.Length))
Time = $event.TimeCreated
Provider = $event.ProviderName
}
}
}

$summary = $allErrors | Group-Object LogName, EventId | Sort-Object Count -Descending

foreach ($group in $summary) {
$parts = $group.Name -split ', '
$log = $parts[0]
$eid = $parts[1]
$sampleMessage = $group.Group[0].Message

[PSCustomObject]@{
Count = $group.Count
LogName = $log
EventId = $eid
Provider = $group.Group[0].Provider
Sample = $sampleMessage
}
} | Format-Table -AutoSize -Wrap

这段脚本的核心思路是先用 Level = 1, 2(Critical 和 Error)做服务端过滤,减少数据传输量;然后在客户端用 Group-Object 按日志名和事件 ID 聚合,统计出现次数。最后输出每个高频错误的首次出现样例,方便运维人员快速判断问题根因。

执行结果示例:

1
2
3
4
5
6
7
Count LogName    EventId Provider              Sample
----- ------- ------- -------- ------
147 System 7036 Service Control Manager 服务已在后台启动或停止...
82 Application 1000 Application Error 故障模块名称: ntdll.dll...
45 System 7023 Service Control Manager 服务以特定错误退出...
31 Application 1026 .NET Runtime 进程已因未经处理的异常而终止...
12 System 41 Kernel-Power 系统未先正常关闭...

检测可疑的登录失败和账户锁定

安全取证中,登录失败事件(Event ID 4625)和账户锁定事件(Event ID 4740)是发现暴力破解攻击的关键指标。以下脚本汇总最近 7 天的登录失败记录,按目标账户和来源 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
$startTime = (Get-Date).AddDays(-7)

$failedLogonEvents = Get-WinEvent -FilterHashtable @{
LogName = 'Security'
Id = 4625
StartTime = $startTime
} -ErrorAction SilentlyContinue

$failedLogins = @()
foreach ($event in $failedLogonEvents) {
$xml = [xml]$event.ToXml()
$data = $xml.Event.EventData.Data
$targetUser = ($data | Where-Object { $_.Name -eq 'TargetUserName' }).'#text'
$sourceIP = ($data | Where-Object { $_.Name -eq 'IpAddress' }).'#text'
$failureReason = ($data | Where-Object { $_.Name -eq 'SubStatus' }).'#text'

$failedLogins += [PSCustomObject]@{
Time = $event.TimeCreated
User = $targetUser
SourceIP = $sourceIP
FailureCode = $failureReason
}
}

$suspicious = $failedLogins | Group-Object User, SourceIP | Where-Object { $_.Count -gt 5 } |
Sort-Object Count -Descending

foreach ($entry in $suspicious) {
$parts = $entry.Name -split ', '
[PSCustomObject]@{
FailedCount = $entry.Count
User = $parts[0]
SourceIP = $parts[1]
FirstSeen = $entry.Group[0].Time
LastSeen = $entry.Group[-1].Time
}
} | Format-Table -AutoSize

脚本中通过 Where-Object { $_.Count -gt 5 } 筛选出失败次数超过 5 次的组合,这些通常是暴力破解的特征。FailureCode 字段(SubStatus)可以进一步区分失败原因,例如 0xC000006A 表示密码错误,0xC0000234 表示账户已被锁定。

执行结果示例:

1
2
3
4
5
6
FailedCount User           SourceIP       FirstSeen             LastSeen
----------- ---- -------- --------- --------
89 administrator 203.0.113.42 2025/10/21 02:14:07 2025/10/27 06:58:33
34 backup_svc 198.51.100.7 2025/10/23 11:20:15 2025/10/26 23:47:01
12 testuser 192.168.1.105 2025/10/25 08:30:00 2025/10/27 07:12:44
8 sqladmin 10.0.0.99 2025/10/26 14:05:22 2025/10/27 03:40:18

发现此类模式后,建议立即检查对应账户的锁定策略和网络防火墙规则,并考虑将可疑 IP 加入黑名单。

注意事项

  1. 优先使用 FilterHashtable 而非管道过滤Get-WinEvent 支持 -FilterHashtable 参数做服务端过滤,比先获取全部事件再用 Where-Object 过滤效率高数十倍。在查询安全日志等大型日志时,差异尤为明显。

  2. 处理空结果集:当日志中没有符合条件的记录时,Get-WinEvent 会抛出异常而非返回空集合。务必使用 -ErrorAction SilentlyContinuetry/catch 块来优雅处理这种情况。

  3. Level 参数使用数值FilterHashtable 中的 Level 字段只接受数值:1 = Critical,2 = Error,3 = Warning,4 = Information,5 = Verbose。不要使用字符串,否则过滤不会生效。

  4. XPath 过滤更精细:当 FilterHashtable 无法满足复杂条件时,可以使用 -FilterXPath 参数编写 XPath 表达式。例如按消息内容中的特定字段值过滤,这是最灵活的服务端过滤方式。

  5. 安全日志需要管理员权限:查询 Security 日志需要以管理员身份运行 PowerShell(”以管理员身份运行”),否则会收到权限不足的错误。普通用户只能访问 Application 和 System 等日志。

  6. 大时间范围查询注意性能:如果查询跨度数月的安全日志,即使使用服务端过滤也可能返回海量数据。建议先按天或按小时分段查询,再汇总结果。也可以结合 -MaxEvents 参数限制返回条数做初步探查。

PowerShell 技能连载 - Windows 事件转发

适用于 PowerShell 5.1 及以上版本(Windows),需要管理员权限

在企业环境中,每台 Windows 服务器都在本地记录事件日志。当需要排查跨服务器的问题时,逐台登录查看日志效率极低。Windows 事件转发(WEF,Windows Event Forwarding)可以将多台服务器的事件日志集中转发到收集器服务器,实现集中化日志管理。结合 PowerShell 的日志查询能力,可以构建强大的安全监控和故障排查平台。

本文将讲解如何使用 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
# 查询本地事件日志
$events = Get-WinEvent -LogName "Application" -MaxEvents 10 -ErrorAction SilentlyContinue
$events | Select-Object TimeCreated, LevelDisplayName, ProviderName,
@{N='Message'; E={if ($_.Message.Length -gt 80) { $_.Message.Substring(0, 80) + "..." } else { $_.Message }}} |
Format-Table -AutoSize -Wrap

# 按条件筛选事件
$errors = Get-WinEvent -FilterHashtable @{
LogName = "Application"
Level = 2 # Error
StartTime = (Get-Date).AddHours(-24)
} -ErrorAction SilentlyContinue

Write-Host "过去 24 小时应用错误:$($errors.Count) 条" -ForegroundColor Red

# 查询安全日志(需要管理员权限)
$logons = Get-WinEvent -FilterHashtable @{
LogName = "Security"
Id = 4624 # 登录成功
StartTime = (Get-Date).AddHours(-1)
} -MaxEvents 10 -ErrorAction SilentlyContinue

if ($logons) {
$logonDetails = foreach ($event in $logons) {
$xml = [xml]$event.ToXml()
$targetUser = ($xml.Event.EventData.Data | Where-Object { $_.Name -eq 'TargetUserName' }).'#text'
$logonType = ($xml.Event.EventData.Data | Where-Object { $_.Name -eq 'LogonType' }).'#text'
$sourceIP = ($xml.Event.EventData.Data | Where-Object { $_.Name -eq 'IpAddress' }).'#text'

[PSCustomObject]@{
Time = $event.TimeCreated.ToString('HH:mm:ss')
User = $targetUser
Type = $logonType
SourceIP = $sourceIP
}
}
$logonDetails | Format-Table -AutoSize
}

# 查询系统启动事件
$boots = Get-WinEvent -FilterHashtable @{
LogName = "System"
Id = 12 # 系统启动
} -MaxEvents 5 -ErrorAction SilentlyContinue

Write-Host "`n最近系统启动:" -ForegroundColor Cyan
$boots | ForEach-Object { Write-Host " $($_.TimeCreated.ToString('yyyy-MM-dd HH:mm:ss'))" }

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
过去 24 小时应用错误:12

Time User Type SourceIP
---- ---- ---- --------
08:15:30 admin 2 192.168.1.100
08:20:45 svc_app 3 127.0.0.1
08:35:10 admin 10 10.0.1.50

最近系统启动:
2025-08-14 06:00:00
2025-08-07 06:00:00
2025-08-01 02:30:00

事件转发配置

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
# 配置事件收集器服务器
function Initialize-EventCollector {
<#
.SYNOPSIS
初始化 Windows 事件转发收集器
#>

# 启用 Windows 事件收集器服务
$wecSvc = Get-Service -Name "Wecsvc" -ErrorAction SilentlyContinue
if ($wecSvc.Status -ne 'Running') {
Start-Service -Name "Wecsvc"
Set-Service -Name "Wecsvc" -StartupType Automatic
Write-Host "Windows 事件收集器服务已启动" -ForegroundColor Green
}

# 创建事件订阅
$subscriptionXml = @"
<Subscription xmlns="http://schemas.microsoft.com/2006/03/windows/events/subscription">
<SubscriptionId>SecurityEvents</SubscriptionId>
<SubscriptionType>SourceInitiated</SubscriptionType>
<Description>安全事件集中收集</Description>
<Enabled>true</Enabled>
<Uri>http://schemas.microsoft.com/wbem/wsman/1/windows/EventLog</Uri>
<ConfigurationMode>Custom</ConfigurationMode>
<Delivery Mode="Push">
<Batching>
<MaxItems>20</MaxItems>
<MaxLatencyTime>300000</MaxLatencyTime>
</Batching>
<PushSettings>
<Heartbeat Interval="3600000"/>
</PushSettings>
</Delivery>
<ContentFormat>
<RenderedText/>
</ContentFormat>
<LogFile>ForwardedEvents</LogFile>
<Query>
<![CDATA[
<QueryList>
<Query Path="Security">
<Select>*[System[(EventID=4624 or EventID=4625 or EventID=4648 or EventID=4672)]]</Select>
</Query>
<Query Path="System">
<Select>*[System[Level=1 or Level=2 or Level=3]]</Select>
</Query>
</QueryList>
]]>
</Query>
<ReadExistingEvents>false</ReadExistingEvents>
</Subscription>
"@

$configPath = "$env:TEMP\SecurityEvents.xml"
$subscriptionXml | Set-Content $configPath -Encoding UTF8

# 创建订阅
try {
wecutil cs $configPath 2>&1
Write-Host "安全事件订阅已创建" -ForegroundColor Green
} catch {
Write-Host "创建订阅失败:$($_.Exception.Message)" -ForegroundColor Red
}
}

# Initialize-EventCollector

# 查询转发的事件
function Get-ForwardedEvents {
param(
[int]$Hours = 1,
[int]$MaxEvents = 50
)

$events = Get-WinEvent -FilterHashtable @{
LogName = "ForwardedEvents"
StartTime = (Get-Date).AddHours(-$Hours)
} -MaxEvents $MaxEvents -ErrorAction SilentlyContinue

if (-not $events) {
Write-Host "无转发事件" -ForegroundColor Yellow
return
}

$summary = $events | Group-Object ProviderName | Sort-Object Count -Descending
Write-Host "转发事件概览(过去 $Hours 小时):" -ForegroundColor Cyan
$summary | Select-Object Count, Name | Format-Table -AutoSize

return $events
}

Get-ForwardedEvents -Hours 4 -MaxEvents 100

执行结果示例:

1
2
3
4
5
6
7
安全事件订阅已创建
转发事件概览(过去 4 小时):
Count Name
----- ----
45 Microsoft-Windows-Security-Auditing
23 Service Control Manager
12 Microsoft-Windows-Kernel-General

安全事件分析

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 Get-FailedLogonReport {
param([int]$Hours = 24)

$failedLogons = Get-WinEvent -FilterHashtable @{
LogName = "ForwardedEvents"
Id = 4625
StartTime = (Get-Date).AddHours(-$Hours)
} -ErrorAction SilentlyContinue

if (-not $failedLogons) {
Write-Host "无登录失败事件" -ForegroundColor Green
return
}

$report = foreach ($event in $failedLogons) {
$xml = [xml]$event.ToXml()
$data = @{}
$xml.Event.EventData.Data | ForEach-Object { $data[$_.Name] = $_.'#text' }

[PSCustomObject]@{
Time = $event.TimeCreated.ToString('yyyy-MM-dd HH:mm:ss')
Computer = $event.MachineName
Target = $data['TargetUserName']
SourceIP = $data['IpAddress']
LogonType = $data['LogonType']
Failure = $data['SubStatus']
}
}

Write-Host "登录失败报告(过去 $Hours 小时):" -ForegroundColor Red
Write-Host "总计:$($report.Count) 次失败" -ForegroundColor Yellow

# 按来源 IP 聚合
$byIP = $report | Group-Object SourceIP | Sort-Object Count -Descending | Select-Object -First 10
Write-Host "`n按来源 IP 聚合:" -ForegroundColor Cyan
$byIP | Select-Object Count, Name | Format-Table -AutoSize

# 按目标用户聚合
$byUser = $report | Group-Object Target | Sort-Object Count -Descending | Select-Object -First 5
Write-Host "按目标用户聚合:" -ForegroundColor Cyan
$byUser | Select-Object Count, Name | Format-Table -AutoSize
}

Get-FailedLogonReport -Hours 24

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
登录失败报告(过去 24 小时):
总计:45 次失败

按来源 IP 聚合:
Count Name
----- ----
23 192.168.1.100
12 10.0.1.50
5 203.0.113.42
3 172.16.0.5
2 192.168.1.200

按目标用户聚合:
Count Name
----- ----
30 administrator
8 admin
5 root
2 sa

注意事项

  1. WinRM 依赖:WEF 依赖 WinRM 服务,确保源计算机已启用 WinRM
  2. 防火墙规则:源计算机需要允许 WinRM 出站连接(默认端口 5985/5986)
  3. 订阅类型:SourceInitiated(源发起)适合大规模部署,CollectorInitiated(收集器发起)适合少量服务器
  4. 事件量控制:大量服务器的事件转发可能产生海量数据,使用精确的 XPath 查询过滤
  5. 存储规划:收集器服务器需要足够的磁盘空间存储转发的事件
  6. GPO 部署:企业环境中通过组策略配置 WEF,比手动配置更高效一致

PowerShell 技能连载 - 事件日志与系统监控

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

Windows 事件日志是故障排查的”黑匣子”——系统崩溃、应用异常、安全审计、服务启停,几乎所有重要事件都会被记录到事件日志中。对于运维人员来说,能够高效地查询、筛选和分析事件日志是一项必备技能。PowerShell 提供了 Get-WinEvent 命令,其过滤能力远超传统的事件查看器 GUI。

本文将讲解事件日志的查询技巧、自动化监控脚本、性能计数器采集,以及如何构建系统健康检查工具。

事件日志基础查询

Get-WinEvent 是查询事件日志的主力命令,替代了旧版 Get-EventLog

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 列出所有可用的日志
Get-WinEvent -ListLog * | Where-Object RecordCount -gt 0 |
Select-Object LogName, RecordCount, FileSize,
@{N='大小MB'; E={[math]::Round($_.FileSize/1MB, 2)}} |
Sort-Object RecordCount -Descending |
Select-Object -First 15 |
Format-Table -AutoSize

# 查询最近的系统日志
Get-WinEvent -LogName System -MaxEvents 10 |
Select-Object TimeCreated, Id, LevelDisplayName, Message |
Format-List

# 查询特定事件 ID
Get-WinEvent -LogName System | Where-Object Id -eq 7036 |
Select-Object -First 5 TimeCreated, Message

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
LogName                   RecordCount   大小MB
------- ----------- ------
Security 245812 128.45
Application 87432 42.18
System 45678 18.92
Microsoft-Windows-PowerShell 12345 5.67

TimeCreated Id LevelDisplayName Message
----------- -- ---------------- -------
2025-05-13 08:30:15 7036 信息 服务控制管理器...
2025-05-13 08:30:12 7040 信息 服务控制管理器...
2025-05-13 08:28:45 100 信息 Windows Update...

TimeCreated Message
----------- -------
2025-05-13 08:15:00 服务 Windows Update 已进入运行状态
2025-05-13 08:00:00 服务 WinRM 已进入运行状态

使用 FilterHashtable 高效过滤

Get-WinEventFilterHashtable 参数可以将过滤条件推送到事件日志引擎,比管道 Where-Object 快数倍:

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
# 按时间范围过滤(最近 1 小时的错误和警告)
$oneHourAgo = (Get-Date).AddHours(-1)
Get-WinEvent -FilterHashtable @{
LogName = 'System'
Level = 1, 2, 3
StartTime = $oneHourAgo
} | Select-Object TimeCreated, Id, LevelDisplayName, Message |
Format-Table -AutoSize -Wrap

# 按事件 ID 列表过滤
Get-WinEvent -FilterHashtable @{
LogName = 'System'
Id = 41, 1074, 6008
} -MaxEvents 20 | Select-Object TimeCreated, Id, Message

# 按提供者(来源)过滤
Get-WinEvent -FilterHashtable @{
LogName = 'Application'
ProviderName = 'Application Error'
StartTime = (Get-Date).AddDays(-7)
} | Select-Object TimeCreated, Id,
@{N='应用程序'; E={$_.Properties[0].Value}},
@{N='版本'; E={$_.Properties[1].Value}} |
Format-Table -AutoSize

# 按关键词过滤(安全审计)
Get-WinEvent -FilterHashtable @{
LogName = 'Security'
Id = 4624, 4625
StartTime = (Get-Date).AddHours(-24)
} | Select-Object TimeCreated, Id,
@{N='用户'; E={$_.Properties[5].Value}},
@{N='来源IP'; E={$_.Properties[18].Value}} |
Format-Table -AutoSize

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
TimeCreated          Id  LevelDisplayName Message
----------- -- ---------------- -------
2025-05-13 08:25:31 1014 警告 DNS 解析超时
2025-05-13 08:20:15 1001 错误 Windows 错误报告
2025-05-13 08:15:42 702 警告 服务启动失败

TimeCreated Id 应用程序 版本
----------- -- -------- ----
2025-05-13 02:15:00 1000 chrome.exe 124.0.6367
2025-05-12 18:30:22 1000 Outlook.exe 16.0.17928

TimeCreated Id 用户 来源IP
----------- -- ---- ------
2025-05-13 08:30:01 4624 CONTOSO\admin 192.168.1.100
2025-05-13 07:15:22 4625 unknown_user 10.0.0.55

注意Level 值的含义:1 = Critical,2 = Error,3 = Warning,4 = Information,5 = Verbose。

构建自动化监控脚本

以下是一个实用的系统健康监控脚本,可以定时运行并生成报告:

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
function Get-SystemHealthReport {
<#
.SYNOPSIS
生成系统健康状态报告
#>
[CmdletBinding()]
param(
[int]$CriticalEventHours = 1,
[int]$DiskWarningThreshold = 80,
[int]$CPUWarningThreshold = 85
)

$report = [ordered]@{}
$report['生成时间'] = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
$report['计算机名'] = $env:COMPUTERNAME

# 1. 检查关键事件
$startTime = (Get-Date).AddHours(-$CriticalEventHours)
$criticalEvents = Get-WinEvent -FilterHashtable @{
LogName = 'System', 'Application'
Level = 1, 2
StartTime = $startTime
} -ErrorAction SilentlyContinue

$report['关键事件数'] = $criticalEvents.Count
$report['关键事件'] = $criticalEvents |
Select-Object TimeCreated, LogName, Id, LevelDisplayName,
@{N='消息摘要'; E={$_.Message.Substring(0, [Math]::Min(80, $_.Message.Length))}} |
Format-Table -AutoSize

# 2. 检查磁盘空间
$diskStatus = Get-CimInstance Win32_LogicalDisk -Filter "DriveType=3" |
ForEach-Object {
$usedPct = [math]::Round(($_.Size - $_.FreeSpace) / $_.Size * 100, 1)
[PSCustomObject]@{
驱动器 = $_.DeviceID
已用% = $usedPct
可用GB = [math]::Round($_.FreeSpace / 1GB, 2)
状态 = if ($usedPct -gt $DiskWarningThreshold) { 'WARNING' } else { 'OK' }
}
}
$report['磁盘状态'] = $diskStatus | Format-Table -AutoSize

# 3. 检查自动重启的服务
$autoRestartServices = Get-WinEvent -FilterHashtable @{
LogName = 'System'
Id = 7036, 7045
StartTime = (Get-Date).AddHours(-24)
} -ErrorAction SilentlyContinue |
Select-Object TimeCreated,
@{N='服务'; E={if ($_.Message -match '(.+?) 服务') {$Matches[1]} else {$_.Message}}} |
Group-Object 服务 |
Where-Object Count -gt 3 |
Select-Object Name, Count

$report['频繁重启服务'] = if ($autoRestartServices) {
$autoRestartServices | Format-Table -AutoSize
} else {
"无异常"
}

# 4. 检查 Windows 更新状态
$lastUpdate = Get-CimInstance Win32_QuickFixEngineering |
Sort-Object InstalledOn -Descending | Select-Object -First 1
$report['最新补丁'] = "$($lastUpdate.HotFixID) ($($lastUpdate.InstalledOn.ToString('yyyy-MM-dd')))"

# 5. 系统运行时间
$os = Get-CimInstance Win32_OperatingSystem
$uptime = (Get-Date) - $os.LastBootUpTime
$report['运行时间'] = "$($uptime.Days) 天 $($uptime.Hours) 小时"

# 输出报告
foreach ($key in $report.Keys) {
if ($report[$key] -is [string]) {
Write-Host "${key}: $($report[$key])"
} else {
Write-Host "`n${key}:" -ForegroundColor Cyan
$report[$key]
}
}
}

Get-SystemHealthReport

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
生成时间: 2025-05-13 08:30:00
计算机名: DESKTOP-WORK01

关键事件数: 3

TimeCreated LogName Id LevelDisplayName 消息摘要
----------- ------- -- ---------------- --------
2025-05-13 08:25:31 System 1014 警告 DNS 解析超时: api.example.com
2025-05-13 08:20:15 Application 1001 错误 应用程序错误: chrome.exe

磁盘状态:
驱动器 已用% 可用GB 状态
------ ----- ------ ----
C: 59.8 191.58 OK
D: 68.9 289.33 OK

频繁重启服务: 无异常
最新补丁: KB5036908 (2025-05-10)
运行时间: 9 天 4 小时

性能计数器采集

除了事件日志,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
# 查看可用的性能计数器类别
Get-Counter -ListSet * | Select-Object CounterSetName, Description |
Where-Object CounterSetName -match 'Processor|Memory|Disk|Network' |
Format-Table -AutoSize

# 采集关键性能指标
$counters = @(
'\Processor(_Total)\% Processor Time'
'\Memory\Available MBytes'
'\Memory\% Committed Bytes In Use'
'\PhysicalDisk(_Total)\Disk Read Bytes/sec'
'\PhysicalDisk(_Total)\Disk Write Bytes/sec'
'\Network Interface(*)\Bytes Total/sec'
)

# 采集一次
$sample = Get-Counter -Counter $counters
$sample.CounterSamples | ForEach-Object {
[PSCustomObject]@{
计数器 = $_.Path -replace '^\\\\[^\\]+\\', ''
值 = [math]::Round($_.CookedValue, 2)
}
} | Format-Table -AutoSize

# 持续采集(每 5 秒采样,共 12 次 = 1 分钟)
$results = Get-Counter -Counter '\Processor(_Total)\% Processor Time' `
-SampleInterval 5 -MaxSamples 12

$results.CounterSamples | ForEach-Object {
[PSCustomObject]@{
时间 = $_.Timestamp.ToString('HH:mm:ss')
CPU使用率 = [math]::Round($_.CookedValue, 1)
}
} | Format-Table -AutoSize

# 计算平均值
$avgCpu = ($results.CounterSamples.CookedValue | Measure-Object -Average).Average
Write-Host "1 分钟平均 CPU 使用率:$([math]::Round($avgCpu, 1))%" -ForegroundColor Cyan

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
CounterSetName                 Description
--------------- -----------
Processor 处理器性能计数器
Memory 内存使用情况计数器
PhysicalDisk 物理磁盘性能计数器
Network Interface 网络接口性能计数器

计数器 值
------ --
Processor(_Total)\% Processor Time 12.45
Memory\Available MBytes 8456.00
Memory\% Committed Bytes In Use 65.32
PhysicalDisk(_Total)\Disk Read Bytes/sec 524288.00
PhysicalDisk(_Total)\Disk Write Bytes/sec 262144.00

时间 CPU使用率
---- --------
08:30:00 15.2
08:30:05 22.8
08:30:10 18.5
...
08:30:55 14.3

1 分钟平均 CPU 使用率:17.6%

实时事件监控

通过 Register-WmiEventRegister-CimIndicationEvent,可以对特定事件进行实时监控并触发操作:

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
# 监控新事件的创建
$query = "SELECT * FROM __InstanceCreationEvent WITHIN 5 WHERE TargetInstance ISA 'Win32_NTLogEvent' AND TargetInstance.LogFile = 'System' AND TargetInstance.EventType <= 2"

Register-WmiEvent -Query $query -SourceIdentifier "SystemErrorMonitor" -Action {
$event = $Event.SourceEventArgs.NewEvent.TargetInstance
$body = @"
时间: $($event.TimeGenerated)
事件ID: $($event.EventCode)
类别: $($event.CategoryString)
消息: $($event.Message.Substring(0, [Math]::Min(200, $event.Message.Length)))
"@

# 记录到日志文件
Add-Content -Path "C:\Logs\critical-events.log" -Value $body

# 可以在此处添加邮件通知或 Webhook
Write-Host "检测到关键事件:ID $($event.EventCode)" -ForegroundColor Red
}

# 查看已注册的事件订阅
Get-EventSubscriber

# 手动触发检查
Get-Event -SourceIdentifier "SystemErrorMonitor"

# 取消订阅
Unregister-Event -SourceIdentifier "SystemErrorMonitor"

执行结果示例:

1
2
3
4
5
SubscriptionId   Name                 Enabled
-------------- ---- -------
1 SystemErrorMonitor True

检测到关键事件:ID 1001

注意事项

  1. FilterHashtable 优先:始终使用 -FilterHashtable 而非管道 Where-Object 过滤,前者在日志引擎层面过滤,性能提升可达 10-100 倍
  2. 日志大小管理:定期检查和归档事件日志,避免日志文件过大导致查询变慢。可通过组策略配置日志大小上限和覆盖策略
  3. 安全日志权限:查询安全日志(Security)需要管理员权限
  4. 性能计数器开销:频繁采集性能计数器会消耗 CPU 资源,建议采样间隔不低于 5 秒
  5. 远程查询Get-WinEvent 支持 -ComputerName 参数远程查询,但需要 WinRM 或 RPC 连接
  6. 事件消息截断:某些事件消息很长,使用 $_.Message.Substring(0, N) 截取前 N 个字符用于显示

PowerShell 技能连载 - Windows 事件日志分析

适用于 PowerShell 5.1 及以上版本

Windows 事件日志是排查系统问题、监控安全事件的重要数据源。无论是排查服务崩溃、追踪登录失败,还是审计权限变更,事件日志都记录着关键的操作痕迹。传统的”事件查看器”图形界面虽然直观,但面对大规模日志筛选和批量分析时效率极低。

PowerShell 提供了强大的事件日志查询能力,可以像操作数据库一样对日志进行精确筛选和统计。本文将介绍如何使用 Get-WinEvent 高效查询事件日志,以及如何构建自动化的日志分析脚本。

基础查询

Get-WinEvent 是查询事件日志的核心 cmdlet,比旧版的 Get-EventLog 性能更好且功能更强。

1
2
3
4
# 查询最近 10 条系统日志
Get-WinEvent -LogName 'System' -MaxEvents 10 |
Select-Object TimeCreated, Id, LevelDisplayName, Message |
Format-Table -AutoSize
1
2
3
4
5
6
TimeCreated          Id LevelDisplayName Message
----------- -- ---------------- -------
2025/4/8 9:45:12 7036 信息 Windows Update 服务已停止...
2025/4/8 9:44:30 7036 信息 DNS Client 服务已停止...
2025/4/8 9:43:15 1001 信息 Windows 错误报告...
2025/4/8 9:42:00 7024 错误 服务意外终止...

可以查看系统中所有可用的日志列表。

1
2
3
4
5
6
7
# 列出所有已注册的事件日志
Get-WinEvent -ListLog '*' |
Where-Object { $_.RecordCount -gt 0 } |
Select-Object LogName, RecordCount, FileSize |
Sort-Object RecordCount -Descending |
Select-Object -First 10 |
Format-Table -AutoSize

使用 FilterHashtable 高效筛选

直接通过管道筛选 Get-WinEvent 的输出效率很低,因为 PowerShell 会先获取所有事件再过滤。推荐使用 -FilterHashtable 参数在服务端进行筛选,大幅提升查询速度。

1
2
3
4
5
6
7
8
9
10
# 查询过去 24 小时内的系统错误日志
$startTime = (Get-Date).AddDays(-1)

Get-WinEvent -FilterHashtable @{
LogName = 'System'
Level = 2 # 2=错误,3=警告,4=信息
StartTime = $startTime
} |
Select-Object TimeCreated, Id, Message |
Format-Table -AutoSize -Wrap

Level 的值含义如下:

级别
1 关键 (Critical)
2 错误 (Error)
3 警告 (Warning)
4 信息 (Information)
0 详细 (Verbose)

按 EventId 精确筛选

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 查询所有登录失败事件(EventId 4625)
Get-WinEvent -FilterHashtable @{
LogName = 'Security'
Id = 4625
} -MaxEvents 20 |
ForEach-Object {
# 从 XML 中提取详细信息
$xml = [xml]$_.ToXml()
$ns = New-Object System.Xml.XmlNamespaceManager($xml.NameTable)
$ns.AddNamespace('evt', 'http://schemas.microsoft.com/win/2004/08/events/event')

[PSCustomObject]@{
时间 = $_.TimeCreated
用户 = $xml.SelectSingleNode('//evt:Data[@Name="TargetUserName"]', $ns).InnerText
来源 IP = $xml.SelectSingleNode('//evt:Data[@Name="IpAddress"]', $ns).InnerText
失败原因 = $xml.SelectSingleNode('//evt:Data[@Name="FailureReason"]', $ns).InnerText
}
} | Format-Table -AutoSize
1
2
3
4
5
时间                用户      来源 IP        失败原因
---- ---- ------- --------
2025/4/8 8:15:23 admin 10.0.0.55 %%2313
2025/4/8 8:12:01 testuser 192.168.1.100 %%2310
2025/4/7 22:30:45 backup 172.16.0.10 %%2313

错误模式识别与统计

对于运维人员来说,识别反复出现的错误模式比逐条查看日志更有价值。下面展示如何对事件日志进行统计分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 统计过去 7 天系统错误日志的分布
$startTime = (Get-Date).AddDays(-7)

Get-WinEvent -FilterHashtable @{
LogName = 'System'
Level = 2
StartTime = $startTime
} |
Group-Object -Property Id |
Sort-Object Count -Descending |
Select-Object -First 10 Count, Name |
ForEach-Object {
# 获取该 EventId 的描述
$sampleEvent = Get-WinEvent -FilterHashtable @{
LogName = 'System'
Id = [int]$_.Name
} -MaxEvents 1

[PSCustomObject]@{
出现次数 = $_.Count
事件ID = $_.Name
示例来源 = $sampleEvent.ProviderName
}
} | Format-Table -AutoSize

按时间段统计事件趋势

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 按小时统计错误事件数量
$startTime = (Get-Date).AddDays(-1)

Get-WinEvent -FilterHashtable @{
LogName = 'System'
Level = 2
StartTime = $startTime
} |
Group-Object { $_.TimeCreated.ToString('yyyy-MM-dd HH:00') } |
Sort-Object Name |
ForEach-Object {
[PSCustomObject]@{
时间段 = $_.Name
错误数 = $_.Count
分布 = ('#' * [math]::Min($_.Count, 50))
}
} | Format-Table -AutoSize
1
2
3
4
5
6
7
8
时间段         错误数  分布
------ ------ ----
2025-04-07 10:00 3 ###
2025-04-07 11:00 7 #######
2025-04-07 12:00 2 ##
2025-04-07 14:00 12 ############
2025-04-07 15:00 5 #####
2025-04-08 08:00 4 ####

日志导出与归档

定期归档日志可以防止日志文件过大导致查询变慢,也便于历史回溯。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 导出指定时间范围的安全日志为 CSV
$exportParams = @{
LogName = 'Security'
StartTime = (Get-Date).AddDays(-30)
EndTime = (Get-Date).AddDays(-7)
}

$exportPath = "C:\Logs\Security_Export_$(Get-Date -Format 'yyyyMMdd').csv"

Get-WinEvent @exportParams |
Select-Object TimeCreated, Id, LevelDisplayName, Message |
Export-Csv -Path $exportPath -NoTypeInformation -Encoding UTF8

Write-Host "日志已导出到:$exportPath"
$fileSize = (Get-Item $exportPath).Length / 1MB
Write-Host "文件大小:$([math]::Round($fileSize, 2)) MB"

使用 FilterXML 进行复杂查询

FilterHashtable 无法满足复杂查询需求时,可以使用 XML 精确定义查询条件。在”事件查看器”中可以先手动筛选,然后切换到”XML”选项卡复制查询语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 使用 XML 筛选:查询多个事件源的组合条件
$query = @'
<QueryList>
<Query Id="0" Path="System">
<Select Path="System">
*[System[Provider[@Name='Service Control Manager'] and (Level=2 or Level=3)]]
and
*[System[TimeCreated[@SystemTime&gt;='2025-04-01T00:00:00']]]
</Select>
</Query>
</QueryList>
'@

Get-WinEvent -FilterXml $query |
Select-Object TimeCreated, Id, LevelDisplayName, Message |
Format-Table -AutoSize -Wrap

构建自动化监控脚本

将日志查询整合到监控脚本中,可以实现自动告警。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 监控脚本:检测磁盘相关错误并在超过阈值时发出警告
$threshold = 5
$lookbackMinutes = 30

$diskErrors = Get-WinEvent -FilterHashtable @{
LogName = 'System'
Level = 2
StartTime = (Get-Date).AddMinutes(-$lookbackMinutes)
} | Where-Object { $_.Message -match 'disk|storage|volume' }

if ($diskErrors.Count -ge $threshold) {
$body = "过去 $lookbackMinutes 分钟内检测到 $($diskErrors.Count) 条磁盘相关错误:`n`n"
$diskErrors | ForEach-Object {
$body += "[$($_.TimeCreated)] EventId: $($_.Id) - $($_.Message.Substring(0, [math]::Min(100, $_.Message.Length)))...`n"
}
Write-Warning "磁盘错误数量超过阈值!"
Write-Host $body
# 实际环境中可调用 Send-MailMessage 或其他通知接口
} else {
Write-Host "磁盘状态正常,过去 $lookbackMinutes 分钟内共 $($diskErrors.Count) 条错误。"
}

注意事项

  • 查询安全日志需要管理员权限,请以”以管理员身份运行”启动 PowerShell
  • Get-WinEvent-FilterHashtableWhere-Object 管道筛选性能好数十倍,应优先使用
  • 查询大量日志时,合理使用 -MaxEvents 参数限制返回数量,避免内存占用过高
  • 对于已归档的 .evtx 文件,可以使用 Get-WinEvent -Path 直接查询
  • 在 PowerShell 7 中,Get-WinEvent 跨平台可用(需安装相应模块),但在 Linux/macOS 上功能有限
  • 建议将常用的日志查询封装成函数,便于在日常运维中复用