优化 Get-WinEvent 以 运行 更快地通过条目

Optimize Get-WinEvent to run through entries faster

所以我有这个循环遍历 Windows 安全日志的脚本,以检查过去 7 天帐户中是否有任何 activity(因为日志上的保留时间为 7 天) - 在脚本中添加一天限制器将是一个奖励)。但是,每个 运行 大约需要 6 个小时(1,200 万个事件)。在事件查看器中列出它们只需要几秒钟的事实让我相信我可以编写更优化的代码。对此有何见解?

代码:

$startTime = (Get-Date)
$filter = @{LogName='Security';ProviderName='Microsoft-Windows-Security-Auditing'}
$i = 0

$entries = Get-WinEvent -FilterHashtable $filter -ComputerName localhost | ForEach-Object{
    $eventXml = ([xml]$_.ToXml()).Event
    $userName = ($eventXml.EventData.Data | Where-Object { $_.Name -eq 'TargetUserName' }).'#text'
    $computer = ($eventXml.EventData.Data | Where-Object { $_.Name -eq 'WorkstationName'}).'#text'

    If($userName -match "Username1" -or $userName -match "Username2" -or $userName -match "Username3")
        {
        [PSCustomObject]@{
            Time     = [DateTime]$eventXml.System.TimeCreated.SystemTime
            UserName = $userName
            Computer = $computer
            }
        }
    $i++
    Write-Progress -activity "Scanning Win Events..." -status "Scanned: $i"
}
$filetime = Get-Date -Format "ddMMyyyyHHmm"
$entries | Out-File "C:\Temp\UsedAccounts$filetime.txt"
$endTime = (Get-Date)
'Duration: {0:mm} min {0:ss} sec' -f ($endTime-$startTime)

编辑 sugestions 之后,代码现在看起来像这样,不幸的是没有重大的时间改进。 请注意,特定事件 ID 也不会更改执行时间,它仍会解析所有条目。 还有什么想法吗?

$startTime = (Get-Date)
$filter = @{
    LogName      ='Security'
    ProviderName ='Microsoft-Windows-Security-Auditing'
    StartTime    = (Get-Date).AddDays(-1).Date
}

$entries = foreach ($entry in (Get-WinEvent -FilterHashtable $filter -ComputerName localhost)){
    $eventXml = ([xml]$entry.ToXml()).Event
    $userName = ($eventXml.EventData.Data | Where-Object { $_.Name -eq 'TargetUserName' }).'#text'
    $computer = ($eventXml.EventData.Data | Where-Object { $_.Name -eq 'WorkstationName' }).'#text'

    If($userName -match 'User1|User2|User3')
        {
        [PSCustomObject]@{
            Time     = [DateTime]$eventXml.System.TimeCreated.SystemTime
            UserName = $userName
            Computer = $computer
            }
        }
}
$filetime = Get-Date -Format "yyyyMMddHHmm"
$entries | Export-Csv -Path "C:\Temp\UsedAccounts$filetime.csv" -UseCulture -NoTypeInformation
$endTime = (Get-Date)
'Duration: {0:hh}h {0:mm}min {0:ss}sec' -f ($endTime-$startTime)

如评论所述,有一些方法可以加快速度:

  1. 向过滤器添加事件 ID,而不是询问 所有 事件类型。此外,并非所有活动都会有 TargetUserName 项目..
  2. ForEach-Object 循环更改为比管道更快的 foreach()
  3. 不要在循环中写出东西或 Write-Progress
# filter on logon, because not all events would have a 'TargetUserName' item..
$filter = @{LogName='Security';ProviderName='Microsoft-Windows-Security-Auditing'; ID=4624}

$entries = foreach ($entry in (Get-WinEvent -FilterHashtable $filter -ComputerName localhost)) {
    $eventXml = ([xml]$entry.ToXml()).Event
    $userName = ($eventXml.EventData.Data | Where-Object { $_.Name -eq 'TargetUserName' }).'#text'
    # if you need 'whole-word' matching, change to
    # $username -match '\b(Username1|Username2|Username3)\b'
    if($userName -match 'Username1|Username2|Username3') {
        $computer = ($eventXml.EventData.Data | Where-Object { $_.Name -eq 'WorkstationName'}).'#text'
        # output an object
        [PSCustomObject]@{
            Time     = [DateTime]$eventXml.System.TimeCreated.SystemTime
            UserName = $userName
            Computer = $computer
        }
    }
    # don't waste time writing unnecessary stuff in the loop with
    # Write-Progress or Write-Host
}

# now output the objects to a structured Csv file you can
# double-click to open in Excel
$filetime = Get-Date -Format "ddMMyyyyHHmm"
$entries | Export-Csv -Path "C:\Temp\UsedAccounts$filetime.csv" -UseCulture -NoTypeInformation

差点忘了.. 您可以通过扩展过滤器将事件限制为最近 7 天的事件:

$filter = @{
    LogName      ='Security'
    ProviderName ='Microsoft-Windows-Security-Auditing'
    ID           = 4624
    StartTime    = (Get-Date).AddDays(-7).Date
}

Theo 比我快,但除了他的建议外,我还建议使用 -FilterXml 如果您改为使用 XML 作为过滤器而不是哈希表,您绝对可以改进过滤器选项,包括说明您最近的结果。

$filter = @'
<QueryList>
  <Query Id="0" Path="Security">
    <Select Path="Security">*[System[TimeCreated[@SystemTime&gt;='2021-09-15T17:39:00.000Z']]]</Select>
  </Query>
</QueryList>
'@

$entries = Get-WinEvent -FilterXml $filter

这将获取从今天上午 10:39 到现在安全日志中的所有内容。您可以将其设置为您想要的任何 date/time。快吗?不,真的不是。加载 10 分钟的日志数据,对我来说刚刚超过 8200 个条目,用了 1 分 18 秒。它比抓取整个日志更快吗?哦,是的,是的!我不允许它加载我的系统拥有的 109k 个条目(我对文件有 100MB 的限制),但我想它会按比例更长。

过去,我真的会考虑 运行 对事件消息进行正则表达式匹配,而不是将所有内容都转换为 XML。不可否认,您的大部分时间只是将事件输入 PowerShell,但处理它们也需要时间,并且只是 运行 对消息进行正则表达式匹配,而不是转换为 XML 和解析转换为 XML 并解析它需要(至少在我的开发箱上)60 倍的时间。当然,这与 1829 相比只有 30 毫秒,但这是为了将 8.2k 事件减少到 8。有了 120 万,这将变得更加重要。由于这在本地计算机上始终是 运行,因此您也可以只使用 $env:COMPUTERNAME 而不是从每个事件中提取计算机名称。所以这是在您获得事件日志条目后我会做的事情:

$filtered=ForEach($entry in $entries){
    if($entry.Message -match "(?ms)Target Subject:.+?   Account Name:\s+(user1|user2|user3)"){
        [pscustomobject]@{
            TimeCreated=$entry.TimeCreated
            User=$Matches[1]
            Computer=$env:COMPUTERNAME
        }
    }
}
$filetime = Get-Date -Format "ddMMyyyyHHmm"
$filtered | Out-File "C:\Temp\UsedAccounts$filetime.txt"

因此,经过大量谷歌搜索后,我将脚本优化为 运行,只用了大约 3 分钟而不是 6 小时... 显然,众所周知 -filterhashtable 非常慢,而我使用的是 -filterxpath。这样做的另一个好处是参数 -logname(-filterhashtable 不可用)可以最大程度地减少时间,因为我没有过滤整个日志,而是只查看我感兴趣的特定日志。

这是最终代码:

$startTime = (Get-Date)
$XPath = "*[EventData[Data[@Name='TargetUserName'] and (Data='User1' or Data='User2' or Data='User3')] and System[TimeCreated[timediff(@SystemTime) <= 86400000]]]"

$entries = foreach ($entry in (Get-WinEvent -LogName 'Security' -FilterXPath $XPath -ComputerName 'localhost')){
    $eventXml = ([xml]$entry.ToXml()).Event
    $userName = ($eventXml.EventData.Data | Where-Object { $_.Name -eq 'TargetUserName' }).'#text'
    $computer = ($eventXml.EventData.Data | Where-Object { $_.Name -eq 'WorkstationName' }).'#text'

        [PSCustomObject]@{
            Time     = [DateTime]$eventXml.System.TimeCreated.SystemTime
            UserName = $userName
            Computer = $computer
            }
}
$filetime = Get-Date -Format "yyyyMMddHHmm"
$entries | Export-Csv -Path "C:\Temp\UsedAccounts$filetime.csv" -UseCulture -NoTypeInformation
$endTime = (Get-Date)
'Duration: {0:hh}h {0:mm}min {0:ss}sec' -f ($endTime-$startTime)