「BOM for Windows Ver.8.0」(以下、BOM)の「カスタムアクション」を使用すると、BOM標準のアクションにはない、独自の処理を自動化できます。例えば、イベントログ監視やパフォーマンス監視で注意や警告の状態となったときに、直近5分のイベントログを自動収集させることで、異常の原因の調査に役立つ一連のイベントを入手できる場合があります。
カスタムアクションによるイベントログのエクスポート
以下の連載記事の最後「過去5分間のすべてのイベントを画面またはCSV出力する(おまけ)」に、ローカルコンピューターのすべてのログ(WindowsログのApplication、Security、システム、Setupだけでなく、アプリケーションとサービスログのすべて)を対象に、過去5分間のイベントをCSVファイルに出力するPowerShellのサンプルスクリプト「gathereventsinXmin.ps1」が紹介されています。
vol.209 イベントログを使いこなせ(その1)|セイテク・シス管道場(Web)(かつて山市良と呼ばれたおじさんのブログ)
実は、このスクリプトは、ブログ記事の初期公開時にはなく、BOMのカスタムアクションで実行させることを想定して作成したものを後から追加したものです。
BOMのイベントログ監視項目を使用すると、イベントIDやそのイベントの回数などを条件に特定のイベント発生を監視することができます。しかし、イベント発生の原因を調査するには、監視対象の特定のイベントだけではなく、その前後に記録されていたイベントや、別のログファイルに同時期に記録されているイベントに注目することでヒントが得られることがあります。パフォーマンス監視では、特定のパフォーマンスカウンターをしきい値で監視できますが、異常発生時に何かしらイベントログに記録されている可能性があります。
BOMの各種監視設定のアクションとして、このサンプルスクリプト(この記事の改良版)を実行してCSVファイルに出力するカスタムアクションを作成すれば、異常発生時の直近(例えば5分前からの)のイベントを、ローカルコンピューターのすべてのログファイルを対象に自動収集できます(画面1)。さらに、「メール送信」アクションと組み合わせれば、CSVファイルをメールに添付して自動送信させることができます(画面2)。

画面1 イベントログ収集のためのカスタムアクションとして「gathereventsinXmin.ps1」を実行させ、直近5分(-Min 5)のイベントをCSVファイル(-ExportPath ファイルパス)に出力する

画面2 イベントログ収集に続き、メール送信アクションを逐次実行させ、出力されたCSVファイルをメールに添付して送信する
生成AIでの分析を想定した機密情報のマスク
メールを受信した担当者は、CSVファイルをExcelで開いて分析することもできますし(画面3)、生成AIに分析を依頼することもできます。例えば、Windows 11であればエクスプローラーの右クリックメニューからCopilotやMicrosoft 365 Copilotに送信することができます(画面4)。
イベントのメッセージには、メールアドレスやIPアドレス、ユーザー名などセンシティブなデータ(機密情報)が含まれている可能性があるため、外部の生成AIを利用する場合は、たとえそのサービスが入力データを学習に使用しないと約束されているものであっても(Microsoft 365 Copilotなど)、生データではなくマスクした上で送信するべきです。そこで、サンプルスクリプト「gathereventsinXmin.ps1」を少し改良し、センシティブなデータの可能性のあるテキストをマスクする機能(オレンジのコード)を追加したものを用意しました。

画面3 担当者に過去5分間のイベントログを含むCSVファイルがメールで送られてくる(1つは加工なしのイベント、もう1つはセンシティブなテキストをマスク加工したもの)

画面4 CSVファイル(センシティブな情報をマスクしたもの)を生成AI(Microsoft 365 Copilotなど)し、要約や分析を依頼する
スクリプトに追加したMask-EventLogSensitiveData関数は、メールアドレスやユーザープロファイルパス、UNC名、FQDN、ドメイン¥ユーザー名、URLなどのテキストをマスクします(注: マスクは完全ではありません、単独のユーザー名やコンピューター名はマスクず、そのまま出力されます)。 -ExportPathパラメーターに出力先ファイルのパスを指定すると、マスク前のデータを「ファイル名.拡張子」に、マスク後のデータを「ファイル名_masked.拡張子」にCSV形式で出力します。
[gathereventsinXmin.ps](プレーンテキストで表示)
param(
[int]$Min = 5,
[string]$ExportPath
)
$ComputerName = $env:COMPUTERNAME
#$UserName = $env:USERNAME
$Domain = $env:USERDOMAIN
$Fqdn = [System.Net.Dns]::GetHostEntry($ComputerName).HostName
function Mask-EventLogSensitiveData {
param(
[Parameter(ValueFromPipeline)]
[AllowNull()]
[string]$Text
)
process {
if ([string]::IsNullOrEmpty($Text)) {
return $Text
}
$masked = $Text
# Local Computer/User/Domain
if ($Fqdn) {
$masked = $masked.Replace($Fqdn, '__COMPUTER__.__DOMAIN__')
}
$masked = $masked.Replace($ComputerName, '__COMPUTER__')
#$masked = $masked.Replace($UserName, '__USER__')
$masked = $masked.Replace($Domain, '__DOMAIN__')
# mailaddress
$masked = $masked -replace '\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b', '<EMAIL>'
# userprofile path
$masked = $masked -replace 'C:\\Users\\[^\\\s]+', 'C:\Users\<USER>'
# domain user
$masked = $masked -replace '\b[^\\\s]+\\[^\\\s]+\b', '<DOMAIN>\<USER>'
# UNC path
$masked = $masked -replace '\\\\[^\\\s]+\\[^\\\s]+', '\\<SERVER>\<SHARE>'
# IPv4
$masked = $masked -replace '\b(?:\d{1,3}\.){3}\d{1,3}\b', '<IPV4>'
# IPv6
$masked = $masked -replace '\b(?:[0-9A-Fa-f]{1,4}:){2,}[0-9A-Fa-f:]{1,}\b', '<IPV6>'
# MAC address
$masked = $masked -replace '\b[0-9A-Fa-f]{2}([-:])[0-9A-Fa-f]{2}(?:\1[0-9A-Fa-f]{2}){4}\b', '<MAC>'
# SID
$masked = $masked -replace 'S-\d(?:-\d+)+', '<SID>'
# GUID
$masked = $masked -replace '\{?[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}\}?', '<GUID>'
# URL(FQDN)
$masked = $masked -replace '(https?://)(?:[A-Za-z0-9-]+\.)+[A-Za-z]{2,}', '${1}<FQDN>'
# URL(hostname only)
$masked = $masked -replace '(https?://)[A-Za-z0-9-]+(?=[:/]|$)', '${1}<HOST>'
$masked = $masked.Replace('__DOMAIN__', '<LOCALDOMAIN>')
$masked = $masked.Replace('__USER__', '<CURRENTUSER>')
$masked = $masked.Replace('__COMPUTER__', '<LOCALCOMPUTER>')
return $masked
}
}
$logs = (Get-WinEvent -ListLog * | Where-Object {($_.IsEnabled) -and ($_.RecordCount -gt 0)}).LogName 2>$null
$last = (Get-Date).AddMinutes(-$Min)
$allEvents = @()
foreach ($log in $logs) {
$events = Get-WinEvent -FilterHashtable @{
LogName = $log
StartTime = $last
#1 = Critical
#2 = Error
#3 = Warning
#Level = 1,2,3
} -ErrorAction SilentlyContinue
if ($events) {
$allEvents += $events
}
}
$result = $allEvents |
Sort-Object TimeCreated |
Select-Object TimeCreated, LogName, Id, LevelDisplayName, MachineName, Message
if ([string]::IsNullOrWhiteSpace($ExportPath)) {
# 引数なし:画面表示
$result | Format-Table -AutoSize
}
else {
# 引数あり:CSV出力
$result | Export-Csv -Path $ExportPath -NoTypeInformation -Encoding UTF8
# マスク済み
$result = $result | Select-Object TimeCreated, LogName, Id, LevelDisplayName,
@{
Name = 'MachineName'
Expression = { Mask-EventLogSensitiveData $_.MachineName }
},
@{
Name = 'Message'
Expression = { Mask-EventLogSensitiveData $_.Message }
}
$exportpath = $exportPath -Replace [System.IO.Path]::GetExtension($ExportPath), ("_masked"+[System.IO.Path]::GetExtension($ExportPath))
$result | Export-Csv -Path $ExportPath -NoTypeInformation -Encoding UTF8
}