ぼくの話すことは、全部ネットに書いてある。

週末引きこもり族がカメラ片手に外に出ようとするブログ。

PowerShellを使って共有フォルダのアクセスログを取る[Windowsログ][SMB]

サーバーの共有フォルダへのアクセスがどれだけあるのか、統計を取りたいと言われて調べました。

また、PowerShellの勉強がてらコードも書きました。今までの記事と気色が違いますが、せっかくなのでTipsとして残しときます。

要件は、

  • なるべく既存システムに影響を与えない。設定を変えない。
  • Windowsの標準機能で実装する。
  • 同時接続100人以上。

注意! 本記事のコードは、ハードコピーしたものを再度手打ちしてるため誤字ってるかも。

方法1:Windowsログの監査ログを利用する

replication.hatenablog.com

メジャーな方法。フォルダの設定とイベントビューアの設定をいじるだけで、イベントビューアで見れるログを吐いてくれます。

利点

  • ログをとる部分がWindows標準機能。常時ジョブを走らせなくていい。

欠点

  • ログの取得が細かく取捨選択出来ないため、不要なログが貯まる。例えば、1ファイル開くだけでハンドル操作やらオープンやらのログが大量に発生する。
  • セキュリティログが汚れる。

ログが増えるのが嫌だからボツになりました。

実装例

ログ抽出のサンプルコード。XPathで抽出条件を指定してます。該当のログがない場合try-catchしてるのにエラーで落ちます。

$fromDay = -20
$days = 20
$startTime = [DateTime](Get-Date).AddDays($fromDay).ToString("yyyy/MM/dd 00:00:00")
$startUTCTime = [System.TimeZoneInfo]::ConvertTimeToUtc($startTime).ToString("yyyy-MM-ddTHH:mm:ssZ")
$endUTCTime = [System.TimeZoneInfo]::ConvertTimeToUtc($startTime.AddDays($days)).ToString("yyyy-MM-ddTHH:mm:ssZ")

#ログ抽出処理
#対象のEvent=4656
#explorerのアクセスは取得しない
$filter = @"
    Event/System/EventID='4656' and
    Event/EventData[
    Data[@Name='ProcessName']!='C:\Windows\explorer.exe'
    ] and
    Event/EventData[
    Data[@Name='ObjectType']='File'
    ] and
    Event/System/TimeCreated[@SystemTime>='$startUTCTime'] and
    Event/System/TimeCreated[@SystemTime<'$endUTCTime']
"@

#ログ抽出
try{
    Get-WinEvent -LogName security -FilterXPath $filter | % {
        $eventxml = [xml]$_.toXML()
        #名前空間を指定しないと抽出不可能
        $ns = New-Object Xml.XmlNamespaceManager $eventxml.NameTable
        $ns.AddNamespace("e", "http://schemas.microsoft.com/win/2004/08/events/event")

        $prop = @{"Time"=[DateTime]$eventxml.SelectSingleNode('e:Event/e:System/e:TimeCreated', $ns).Attributes.GetNamedItem("SystemTime").Value;
                  "User"=$eventxml.SelectSingleNode('e:Event/e:EventData/e:Data[@Name="SubjectUserName"]', $ns).InnerText;
                  "Domain"=$eventxml.SelectSingleNode('e:Event/e:EventData/e:Data[@Name="SubjectDomainName"]', $ns).InnerText;
                  "Path"=$eventxml.SelectSingleNode('e:Event/e:EventData/e:Data[@Name="ObjectName"]', $ns).InnerText;
                  "Process"=$eventxml.SelectSingleNode('e:Event/e:EventData/e:Data[@Name="ProcessName"]', $ns).InnerText}
                  return New-Object -TypeName PSObject -Property $prop
        }
}
catch {
    Write-Host "該当ログなし"
    $error | Select-Object -Last 1
}

参考にしたサイト様

mtgpowershell.blogspot.jp

方法2:NetEventを用いてSMBを監視する

4sysops.com

docs.microsoft.com

ネットワーク上にある共有フォルダなら、ネットワークを監視すればいいじゃない。ということでプロトコルの監視。ただNetEventはWindowsServer2012 R2(Winodows8.1)以降じゃないと使えない。うちのサーバーは素の2012よってボツ。

利点

  • SMBの通信があったら設定したEventが呼ばれるので無駄が少ない。

欠点

  • WindowsServer2012 R2以降じゃないと使えない。
  • プロトコルの監視なので、サーバー内からのアクセスは把握できない。

実装例

環境がないのでやってません。

方法3:WMIEventを用いてSMBを監視する

Windowsログの監視よりWMIイベント監視するほうが主流かなぁ。

利点

  • 事前の仕掛けが必要ない

欠点

  • 常時ジョブが動きっぱなし。
  • プロトコルの監視なので、サーバー内からのアクセスは把握できない。(場合によっては利点)

実装例その1

Windows8以降じゃないと動かないっぽい。

$ew = New-Object System.Management.ManagementEventWatcher
$ew.Scope = "\\.\root\Microsoft\Windows\SMB"
#MSFT_SmbOpenFileというインスタンスが作成されたときにイベントを発生させる。「WITHIN 1」はチェックする頻度で1秒毎
$ew.Query = "Select * From __InstanceCreationEvent WITHIN 1 Where TargetInstance ISA 'MSFT_SmbOpenFile'"
do
{
    $e = $ew.WaitForNextEvent()
    $log = @{ "Time" = [DateTime]::FromFileTime($e.TIME_CREATED);
              "User" = $e.TargetInstance.ClientUserName;
              "Path" = $e.TargetInstance.Path
            }
    New-Object -TypeName PSObject -Property $log
}while($true)

参考にしたサイト様。C#のコードを書き直しただけ。イベント頻度が高いとこのコードは怖い。

stackoverflow.com

実装例その2

PowerShell2.0以降ならこう書ける。シェルの画面が閉じてセッションが切れると停止してしまうので注意。永続化するにはMOFで書く(powershellでも書けるっぽい)必要があるらしい。

$smb_namespace = "root\Microsoft\Windows\SMB"
$smb_query = "Select * From __InstanceCreationEvent WITHIN 1 Where TargetInstance ISA 'MSFT_SmbOpenFile'"
$fs = New-Object System.IO.StreamWriter("D:\log.txt",$true,[Text.Encoding]::GetEncoding("UTF-8"))
$fs.AutoFlush = $true

#イベントが発生すると実行されるAction
$smb_event = {
    $e = $Event.SourceEventArgs.NewEvent
    $out = [DateTime]::FromFileTime($e.TIME_CREATED).ToString() + "|" + $e.TargetInstance.ClientUserName + "|" + $e.TargetInstance.Path
    $fs.WriteLine($out)
}

#「SMBLog」という名前のイベントを作成
Register-WmiEvent -Namespace $smb_namespace -Query $smb_query -SourceIdentifier "SMBLog" -Action $smb_event

#Get-EventSubscriberで動作中のイベントを確認
#Unregister-eventでイベント停止

参考にしたサイト様。こちらでは、ファイルの出力を「Out-File」へパイプして行っていますが、今回発生頻度が多いイベントなのでStreamWriteで出力ファイルのハンドルをずっと握ってます。

https://blogs.technet.microsoft.com/junichia/2012/03/30/powershellregister-wmiev/