PowerShellからイベントログの内容をメールで送信する

Windowsのイベントログの内容をメールで送信したい場合、今まではNotifEventLogSecondを使っていました(現在では開発終了)。
NotifEventLogSecond - With nothing better to do

ソースコードは公開されています。
With nothing better to do

 
今後どうしようかと考えましたが、PowerShellでも同様のことができるとの記載があったため、

  • 実現するために使うもの
    • PowerShellGet-WinEventコマンドレットとタスクスケジューラ
  • 対象のイベントログのレベル
    • 重大・エラー・警告の3つ
  • メールの送信タイミング
    • イベントログに出力された時にメール送信
    • 一日分のイベントログ内容をメール送信

という形で試してみることにしました。

 
なお、Get-EventLogでもイベントログを取得できますが、両者の違いは以下が参考になりました。
遥佐保の技術メモ:[PowerShell]不定期イベントログを捕まえる!nmcapとPowerShellで必要なイベントログのみ取得する(2/2) - livedoor Blog(ブログ)

 
なお、Windows Server2008 x64ではFilterHashtableオプションがないため、Get-WinEventでは作りにくいかと思います。
Get-WinEvent -FilterHashtable on Windows 2008 x64

 
目次

 

環境

 

メールの内容について

NotifEventLogSecondを使っていた際は、

  • 日時
  • レベル
  • ログの名前
  • ソース
  • タスクのカテゴリ
  • イベントID
  • キーワード
  • ユーザー
  • コンピューター
  • オペコード
  • メッセージ

を送信していたため、PowerShellでも同じようにしたいと考えました。

 
Get-WinEventコマンドレットの戻り値は System.Diagnostics.Eventing.Reader.EventLogRecordオブジェクトのようでした。
Get-WinEvent - TechNet

MSDNによると、EventLogRecordからほぼ同じような内容が取得できそうでした。
EventLogRecord クラス (System.Diagnostics.Eventing.Reader) - MSDN

 
ただ、

  • ユーザー名
  • メッセージ

についてはそのままでは取得できなかったので、調べてみました。

 

ユーザー名

以下を参考に、UserIdからユーザー名へと変換するのが良さそうでした。
ドメイン名、ユーザ名と関連するSIDを相互変換するPowerShellスクリプト - YOMON8.NET

(New-Object System.Security.Principal.SecurityIdentifier($event.UserId)).Translate([System.Security.Principal.NTAccount]).Value

 

メッセージ

EventLogRecordクラスのプロパティにはメッセージそのものはありませんでした。そのため、Propertiesプロパティに含まれているのかなと考えました。
EventLogRecord.Properties プロパティ (System.Diagnostics.Eventing.Reader) - MSDN

試してみたところ、

# 本来は、「サービス "BITS" (DLL "C:\Windows\System32\bitsperf.dll") の Open プロシージャに失敗しました。...」が欲しい
$log_property = ""
foreach ($p in $event.properties){
    $log_property += $p.value
}

# => BITSC:\Windows\System32\bitsperf.dll82 0 0 0 0 0 0 0

メッセージではありませんでした。

 
PowerShell独自のプロパティの追加があるのかなと考え、以下を参考に.NETのプロパティ以外を表示してみます。

$event | Get-Member -Force | Where-Object {
    ($_.MemberType -ne "Method" -and
     $_.MemberType -ne "Property"
    )
}

# 結果
pstypenames       CodeProperty
psadapted         MemberSet
psbase            MemberSet
psextended        MemberSet
psobject          MemberSet
PSStandardMembers MemberSet
Message           NoteProperty

NotePropertyとしてMessageが追加されていました。

Messageプロパティの中身を見たところ、イベントログのメッセージと同じだったため、これを使うことにします。

 

イベントログに出力された時にメール送信

検知方法について

「イベントログに出力された時」を検知する場合の方法として、

がありました。

個人的な興味として、タスクスケジューラと組み合わせてみたかったため、今回は後者の方法で実装します。

 

Get-WinEventのフィルタについて

Get-WinEventコマンドレットには取得時のフィルタが必要です。フィルタの方法としては、

  • Get-EventLog | Where-Object
  • Get-EventLog -FilterHashTable

がありますが、前者だと全件取得後の絞込によりパフォーマンスが厳しいため、後者を使います。
イベントログから特定のイベントを抽出 at SE の雑記

 
フィルタとして使用するハッシュは、

$filter = @{
    LogName = $log_name;
    Level = 1,2,3;
    StartTime = $start_date;
    EndTime = $end_date;
}

とし、それぞれ

  • LogNameは、PowerShellスクリプトの引数として、ログ名をカンマで結合したものを与える
  • Levelは、1(重大)、2(エラー)、3(警告)の3つ
  • StartTimeは、イベントログで呼ばれた時から30秒前
    • $start_date = (Get-Date).AddSeconds(-30)
      • イベント発生してから30秒以上経過してからの起動はないものと想定
  • EndTimeは、現在時刻 (Get-Date)

とします。

 

既知のイベントログエラーの除外について

Get-WinEventコマンドレットでは、除外フィルターの設定が見当たりませんでした。そのため、以下を参考に除外リストを作り、除外することにしました。
UranuxTech blog: [PowerShell] Get-WinEventで必要なログのみを取得する

# 除外リスト
$exclusions = @(
    @{
        Id = 9999;
        LogName = "Application";
    }
)

# Get-WinEventの戻り値$eventsから、既知のものを除外
foreach($e in $exclusions){
    $events = $events | Where-Object { -not (
        $_.Id -eq $e.Id -and
        $_.LogName -eq $e.LogName
    )}
}

 

文字列中の変数展開について

文字列に変数を埋め込む場合、

$val = "ほげ"
"値 $val"
# => 値 ほげ

としますが、そのままではオブジェクトのプロパティは展開できません。

方法を探したところ、以下にありました。
リテラル - 文字列 | powershell チートシート - Qiita

$val.name = "ほげ"
"値 $($val.name)"
#=> 値 ほげ

変数を$()で囲めば良いようです。

 

全体のコード

上記を踏まえたコードは以下の通りです。

$mail = @{
    from = "example+from@gmail.com";
    to = "example+to@gmail.com";
    smtp_server = "smtp.gmail.com";
    smtp_port = 587;
    user = "example+to@gmail.com";
    password = "1234";
}


function Send-Gmail($mail, $msg){
    $password = ConvertTo-SecureString $mail["password"] -AsPlainText -Force
    $credential = New-Object System.Management.Automation.PSCredential $mail["user"], $password
    $host_name = [Net.Dns]::GetHostName()
    Send-MailMessage -To $mail["to"] `
                     -From $mail["from"] `
                     -SmtpServer $mail["smtp_server"] `
                     -Credential $credential `
                     -Port $mail["smtp_port"] `
                     -Subject "$host_name ログ" `
                     -Body $msg `
                     -Encoding UTF8 `
                     -UseSsl
}


# オブジェクトのプロパティを示すため、カッコでくくる
# 念のため、呼ばれた前30秒のログを取得する
$start_date = (Get-Date).AddSeconds(-30)
$end_date = Get-Date

# 除外するイベントログ情報
$exclusions = @(
    @{
        Id = 9999;
        LogName = "Application";
    }
)

# ログ名は、引数にカンマ区切りでセットする
# 例) System,Application
$log_name = $args[0] -split ","

# FilterHashTable用のフィルタ
$filter = @{
    LogName = $log_name;
    Level = 1,2,3;
    StartTime = $start_date;
    EndTime = $end_date;
}

# FilterHashTableでフィルタした後のイベントを取得
$events = Get-WinEvent -FilterHashTable $filter

# 除外指定されているイベント情報は送信しない
foreach($e in $exclusions){
    $events = $events | Where-Object { -not (
        $_.Id -eq $e.Id -and
        $_.LogName -eq $e.LogName
    )}
}


$msg = @"
対象イベント件数: $($events.Count)
--------------------------------------------------


"@

foreach($event in $events){
    $log_task_category = if ($event.Task){ "$($event.TaskDisplayName) ($($event.Task))" } else { "なし" }
    $log_edited_user_id = if ($event.UserId){ "($($event.UserId))" } else { "" }
    
    $log_user_name = if ($event.UserId) {
        # http://yomon.hatenablog.com/entry/2015/06/19/183522
        (New-Object System.Security.Principal.SecurityIdentifier($event.UserId)).Translate([System.Security.Principal.NTAccount]).Value
    } else { "" }

    $log_property = ""
    foreach ($p in $event.properties){
        $log_property += $p.value
    }

    $msg += @"
日時: $($event.TimeCreated.ToString("yyyy/MM/dd HH:mm:ss"))
レベル: $($event.LevelDisplayName) ($($event.Level))
ログの名前: $($event.LogName)
ソース: $($event.ProviderName) 
タスクのカテゴリ: $log_task_category
イベントID: $($event.Id)
キーワード: $($event.KeywordsDisplayNames)
ユーザー: $($log_user_name) $log_edited_user_id
コンピューター: $($event.MachineName)
オペコード: $($event.OpcodeDisplayName) ($($event.Opcode))
プロパティ:
$log_property


メッセージ:
$($event.Message)

--------------------------------------------------


"@
}

Send-Gmail $mail $msg

 
テストしてみます。Write-EventLogでイベントログへ書き込みます。
PowerShellでイベントログに情報を出力 at SE の雑記

Write-EventLog -LogName Application -EntryType Error -Source Application -EventId 1001 -Message "test event"

 
その後PowerShellスクリプトを実行し、イベントログの内容がメール送信されることを確認します。

 

一日分のイベントログ内容をメール送信

基本はイベント発生ごとと同じです。

変更箇所は、

  • StartTimeを、(Get-Date).AddDays(-1)へと変更
  • LogNameを、イベントビューアーのカスタムビューにある管理イベントで設定されているものへと変更

です。

管理イベントで設定されているイベント名は、管理イベントのプロパティ > フィルターの編集 > XMLタブを選択し、

<QueryList>
  <Query Id="0" Path="Application">
    <Select Path="Application">*[System[(Level=1  or Level=2 or Level=3)]]</Select>
    <Select Path="Security">*[System[(Level=1  or Level=2 or Level=3)]]</Select>
    <Select Path="System">*[System[(Level=1  or Level=2 or Level=3)]]</Select>
...
  </Query>
</QueryList>

を確認します。この中の<Select>タグのPath属性値がログ名になります(例: Application、Security、System など)。

 
そのため、それらを配列にして、LogNameに渡します。

$log_name = @(
    "Application", "Security", "System"
)

$filter = @{
    LogName = $log_name;
# ...
}

 

タスクスケジューラの設定

以下を参考に、PowerShell向けのタスクを追加します。
Tech TIPS:WindowsのタスクスケジューラーでPowerShellのスクリプトを実行する際には「パス」に注意 - @IT

 

イベントログに出力された時にメール送信
タブ名 項目
全般 セキュリティオプション ●ユーザーがログオンしているかどうかにかかわらず実行する を選択
■最上位の特権で実行する にチェック
トリガー タスクの開始 イベント時
設定 カスタム
トリガーのフィルター イベントレベル 重大、エラー、警告
●ログごと 選択
イベントログ Application,システム
操作 操作 プログラムの開始
プログラム/スクリプト C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe
引数の追加 -Command "D:\Sandbox\path\to\send_log_by_event.ps1 System,Application" *1

 

一日分のイベントログ内容をメール送信
タブ名 項目
全般 セキュリティオプション ●ユーザーがログオンしているかどうかにかかわらず実行する を選択
■最上位の特権で実行する にチェック
トリガー タスクの開始 スケジュールに従う
設定 毎日(任意の時間をセット)
〃 - フィルター イベントレベル 重大、エラー、警告
〃 - フィルター ●ログごと 選択
〃 - フィルター イベントログ Application,システム
操作 操作 プログラムの開始
プログラム/スクリプト C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe
引数の追加 -Command "D:\Sandbox\path\to\send_log_per_day"

 

ソースコード

GitHubに上げました。
PowerShell_misc/send_event_log_mail at master · thinkAmi/PowerShell_misc

ディレクトリの中にあるファイルはそれぞれ、

  • send_log_when_handled_event.ps1 (イベントログに出力された時にメール送信)
  • send_daily_log.ps1 (一日分のイベントログ内容をメール送信)

です。

 

その他参考

*1:PowerShellの引数は、「.ps1 + 半角スペース + 引数」として設定します