【Powershell】(サーバ保守)サービス登録しないでログオン失敗を検知しメール送信する仕組みをつくる

【ツールの概要】
・タスクスケジューラ上に特定のWindowsイベントログをトリガーとしたタスクを作る。
Powershellで、フィルターされた直近数分間のイベントログのデータをメールで送る。

【環境(使っている環境)】
Windows Server2016(+ Windows10)
Powershell ver5.1

【ソース(AlertLoginError.ps1、メイン処理)】

# クラスファイルを読み込む
using module .\class\Class_CommonFunction.psm1
using module .\class\Class_GetEvents.psm1
using module .\class\Class_Sendmail.psm1

# 各変数をセットアップする、ログの命名などにも使用
$DocRoot = Split-Path (Split-Path $MyInvocation.MyCommand.Path -Parent) -Parent
$YMD = Get-Date -Format "yyyyMMdd"
$yMdHms = Get-Date -Format "yyyyMMddHHmmss"

# xmlにメールの設定を格納しておく
$mailcfgpath = $DocRoot + '\etc\mailcfg.xml'

# クラス[CommonFunction]のインスタンスを作成、ログ実行(ログ用、logfileにWrite-Outputをするメソッドをもつクラス(省略))
$CF = [CommonFunction]::new()
$CF.logfile = $DocRoot + '\log\AlertLoginError_' + $YMD + '.log';
$CF.logwrite("INF", "AlertLoginError process start.")

# クラス[GetEventLog]のインスタンスを作成、メソッド実行(イベントログ取得用)
$GE = [GetEventLog]::new()
$str = $GE.GetEvents()

# 返り値(文字列)のLengthが0じゃなかったらメール送信
if ($str.Length -ne 0) {
    $SM = [SendMail]::new()
    $SM.ConfigMailProperty($str, $mailcfgpath, $logfile)
    $CF.logwrite("INF", "Alert mail sended.")
}

$CF.logwrite("INF", "AlertLoginError process Finished.")

【ソース(class\Class_GetEvents.psm1、イベントログ抽出処理)】

class GetEventLog {
# Get-WinEventで得られるオブジェクトを変数に格納
    [string] GetEvents() {
        $MESSAGES = Get-WinEvent  `
            -logname security  `
            -FilterXPath  `
            "Event [ `
            System [ `
                Provider [@Name='Microsoft-Windows-Security-Auditing'] `
                and ( `
                    EventID='4625'
                ) `
                and ( `
                    TimeCreated[timediff(@SystemTime) <= 300000] `
                ) `
            ] `
        ]"
# オブジェクトの必要な要素のみJsonとして文字列にする(流用しやすいデータにしたいため)
        $str = $MESSAGES | ForEach-Object {
            if ( $_.id -eq "4625" ) {
                $_ | Select-Object `
                @{Name = "TimeCreated"; Expression = { $_.TimeCreated.ToString("yyyy-M-d HH:mm:ss") } }, `
                Id, `
                @{Name = "TargetLogonId"; Expression = { $_.properties[5].value } }, `
                @{Name = "TargetDomainName"; Expression = { $_.properties[6].value } }, `
                @{Name = "TargetUserSid"; Expression = { $_.properties[4].value } }, `
                @{Name = "IpAddress"; Expression = { $_.properties[19].value } }, `
                @{Name = "Message"; Expression = { $_.Message.Substring(0, 18) } }        
            }
        } | ConvertTo-Json
        return $str
    }
}

【ソース(class\Class_Sendmail.psm1、メール送信処理)】

class SendMail{
    [string]ConfigMailProperty($str, $mailcfgpath, $logfile) {
        $Body = "Windows events LoginError catched. `r`n`r`n EventData : `r`n`r`n " + $str

        $Result = $this.SendmailbyPS($Body, $mailcfgpath, $logfile)
        return $Result
    }

    [string]SendmailbyPS($Body, $mailcfgpath, $logfile) {
        $Mailconf_info = [xml](Get-Content $mailcfgpath)
        foreach ($data in $Mailconf_info.config.data) { }
        foreach ($data2 in $Mailconf_info.config.data) { }
        $MailSv = $data.MailSv
        $Port = $data.Port
        $Encode = $data.Encode
        $uid = $data.uid
        $pwd = $data.pwd
        $From = $data.from
        $To = $data.To
        $Cc = $data.Cc
        $Subject = $data.Subject

        #Credential
        $pwd = $pwd | ConvertTo-SecureString -AsPlainText -Force
        $cred = New-Object System.Management.Automation.PSCredential $uid, $pwd

        #Send Mail
        try {
            Send-MailMessage `
                -To $To `
                -Cc $Cc `
                -From $From `
                -SmtpServer $MailSv `
                -Credential $cred `
                -UseSsl `
                -Encoding $Encode `
                -Port $Port `
                -Subject $Subject `
                -Body $Body
            $result = 'OK'
        }
        catch {
            $ErrorMessage = $_.Exception_Message
            $result = $ErrorMessage + $LASTEXITCODE
            Write-Output $LASTEXITCODE | Out-File -Append $logfile -encoding Default
        }

        if ($result -ne 'OK') {
            $MailSv = $data2.MailSv
            $Port = $data2.Port
            $Encode = $data2.Encode
            $uid = $data2.uid
            $pwd = $data2.pwd
            $From = $data.from 
            $pwd = $pwd | ConvertTo-SecureString -AsPlainText -Force
            $cred = New-Object System.Management.Automation.PSCredential $uid, $pwd
            try {
                Send-MailMessage `
                    -To $To `
                    -Cc $Cc `
                    -From $From `
                    -SmtpServer $MailSv `
                    -Credential $cred `
                    -UseSsl `
                    -Encoding $Encode `
                    -Port $Port `
                    -Subject $Subject `
                    -Body $Body
                $result = 'OK'
            }
            catch {
                $ErrorMessage = $_.Exception_Message
                $result = $ErrorMessage + $LASTEXITCODE
                Write-Output $LASTEXITCODE | Out-File -Append $logfile -encoding Default
            }
        }
        return $result
    }
}

f:id:hagure_m:20191217130708p:plain
イベントログトリガ設定


【説明】
・タスクスケジューラでイベントIDで発火するように、上記のようなトリガ設定でPowershellスクリプトを実行します。
PowershellスクリプトではイベントログをFilterXPathでフィルターし、ConvertTo-Jsonでイベントログオブジェクトを文字列にしてメールします。
・イベントログの仕様上、一回のログイン失敗に見えても、数回 ID4625のイベントログが記録されることがあります。

【総評】
Classで分けることである程度わかりやすいスクリプトが書けるようになっていると思います。
FilertXPathやForEach-Objectのところで条件を変えれば、いろんな要件も満たせそうですね。
まあでも、業務用途の場合ちゃんとした商用監査ソフトを使うのが一番でしょうね・・・

【参考文献】
Get-wineventで抽出したログから、IDによって参照するフィールドを分けて表示させる方法