PowerShellでファイルの削除を追跡する

こんにちは、Habr! 私の投稿のトピックはすでにここで取り上げられていますが、何か追加することがあります。



ファイルストレージが3テラバイトを交換したとき、私たちの部門は、重要なドキュメントまたはドキュメントを含むフォルダー全体を削除した人を見つけるためのリクエストを受け取るようになりました。 多くの場合、これは誰かの悪意に応じて起こります。 バックアップは良いが、国はその英雄を知らなければならない。 そして、PowerShellで作成できるミルクは二重に美味です。



私はそれを整理している間に、ワークショップで同僚のためにそれを記録することを決め、それからそれは他の誰かに役立つかもしれないと思いました。 材料が混合されました。 誰かが既製のソリューションを見つけ、PowerShellまたはタスクスケジューラを操作するためのいくつかの非自明な方法を手に入れ、スクリプトの速度を確認します。



問題の解決策を見つける過程で、 Deksの 記事を読みまし 。 私はそれを基礎とすることに決めましたが、いくつかの点は私に合わなかった。



しかし、私はDeksの一連の思考が好きでした...


簡単な説明:ファイルの削除時にファイルシステム監査がオンになると、セキュリティログに2つのイベントが作成され、コード4663、次に4660が記録されます。除去の事実。 イベントには一意のEventRecordIDがありますが、これらはこれら2つのイベントで1つずつ異なります。



以下は、削除されたファイルとそれらを削除したユーザーに関する情報を収集するソーススクリプトです。



$time = (get-date) - (new-timespan -min 240) $Events = Get-WinEvent -FilterHashtable @{LogName="Security";ID=4660;StartTime=$time} | Select TimeCreated,@{n="";e={([xml]$_.ToXml()).Event.System.EventRecordID}} |sort  $BodyL = "" $TimeSpan = new-TimeSpan -sec 1 foreach($event in $events){ $PrevEvent = $Event. $PrevEvent = $PrevEvent - 1 $TimeEvent = $Event.TimeCreated $TimeEventEnd = $TimeEvent+$TimeSpan $TimeEventStart = $TimeEvent- (new-timespan -sec 1) $Body = Get-WinEvent -FilterHashtable @{LogName="Security";ID=4663;StartTime=$TimeEventStart;EndTime=$TimeEventEnd} |where {([xml]$_.ToXml()).Event.System.EventRecordID -match "$PrevEvent"}|where{ ([xml]$_.ToXml()).Event.EventData.Data |where {$_.name -eq "ObjectName"}|where {($_.'#text') -notmatch ".*tmp"} |where {($_.'#text') -notmatch ".*~lock*"}|where {($_.'#text') -notmatch ".*~$*"}} |select TimeCreated, @{n="_";e={([xml]$_.ToXml()).Event.EventData.Data | ? {$_.Name -eq "ObjectName"} | %{$_.'#text'}}},@{n="_";e={([xml]$_.ToXml()).Event.EventData.Data | ? {$_.Name -eq "SubjectUserName"} | %{$_.'#text'}}} if ($Body -match ".*Secret*"){ $BodyL=$BodyL+$Body.TimeCreated+"`t"+$Body._+"`t"+$Body._+"`n" } } $Month = $Time.Month $Year = $Time.Year $name = "DeletedFiles-"+$Month+"-"+$Year+".txt" $Outfile = "\serverServerLogFilesDeletedFilesLog"+$name $BodyL | out-file $Outfile -append
      
      





Measure-Commandを使用すると、次のものが得られました。



 Measure-Command { ... } | Select-Object TotalSeconds | Format-List ... TotalSeconds : 313,6251476
      
      





セカンダリFSでは長すぎます。 降下は実際には10階建てのパイプが好きではなかったので、最初はそれを構造化しました。



 Get-WinEvent -FilterHashtable @{ LogName="Security";ID=4663;StartTime=$TimeEventStart;EndTime=$TimeEventEnd } ` | Where-Object {([xml]$_.ToXml()).Event.System.EventRecordID -match "$PrevEvent"} ` | Where-Object {([xml]$_.ToXml()).Event.EventData.Data ` | Where-Object {$_.name -eq "ObjectName"} ` | Where-Object {($_.'#text') -notmatch ".*tmp"} ` | Where-Object {($_.'#text') -notmatch ".*~lock*"} ` | Where-Object {($_.'#text') -notmatch ".*~$*"} } | Select-Object TimeCreated, @{ n="_"; e={([xml]$_.ToXml()).Event.EventData.Data ` | Where-Object {$_.Name -eq "ObjectName"} ` | ForEach-Object {$_.'#text'} } }, @{ n="_"; e={([xml]$_.ToXml()).Event.EventData.Data ` | Where-Object {$_.Name -eq "SubjectUserName"} ` | ForEach-Object {$_.'#text'} } }
      
      





パイプの階数を減らし、Foreach列挙を削除し、同時にコードを読みやすくすることが判明しましたが、これは大きな効果をもたらさず、差はエラーマージン内です:



 Measure-Command { $time = (Get-Date) - (New-TimeSpan -min 240) $Events = Get-WinEvent -FilterHashtable @{LogName="Security";ID=4660;StartTime=$time}` | Select TimeCreated,@{n="EventID";e={([xml]$_.ToXml()).Event.System.EventRecordID}}` | Sort-Object EventID $DeletedFiles = @() $TimeSpan = new-TimeSpan -sec 1 foreach($Event in $Events){ $PrevEvent = $Event.EventID $PrevEvent = $PrevEvent - 1 $TimeEvent = $Event.TimeCreated $TimeEventEnd = $TimeEvent+$TimeSpan $TimeEventStart = $TimeEvent- (New-TimeSpan -sec 1) $DeletedFiles += Get-WinEvent -FilterHashtable @{LogName="Security";ID=4663;StartTime=$TimeEventStart;EndTime=$TimeEventEnd} ` | Where-Object {` ([xml]$_.ToXml()).Event.System.EventRecordID -match "$PrevEvent" ` -and (([xml]$_.ToXml()).Event.EventData.Data ` | where {$_.name -eq "ObjectName"}).'#text' ` -notmatch ".*tmp$|.*~lock$|.*~$*" } ` | Select-Object TimeCreated, @{n="FilePath";e={ (([xml]$_.ToXml()).Event.EventData.Data ` | Where-Object {$_.Name -eq "ObjectName"}).'#text' } }, @{n="UserName";e={ (([xml]$_.ToXml()).Event.EventData.Data ` | Where-Object {$_.Name -eq "SubjectUserName"}).'#text' } } ` } } | Select-Object TotalSeconds | Format-List $DeletedFiles | Format-Table UserName,FilePath -AutoSize ... TotalSeconds : 302,6915627
      
      





私は頭で少し考えなければなりませんでした。 最も時間がかかる操作は何ですか? さらに数十のMeasure-Commandをつまずかせる可能性がありますが、一般に、この場合、ほとんどの時間がログへのクエリに費やされていること(これはMMCでも最速の手順ではありません)とXMLへの繰り返しの変換に加えて(さらに、 EventRecordIDの場合、これはまったく必要ありません)。 一度に両方を行い、同時に中間変数を除外してみましょう:



 Measure-Command { $time = (Get-Date) - (New-TimeSpan -min 240) $Events = Get-WinEvent -FilterHashtable @{LogName="Security";ID=4660,4663;StartTime=$time}` | Select TimeCreated,ID,RecordID,@{n="EventXML";e={([xml]$_.ToXml()).Event.EventData.Data}}` | Sort-Object RecordID $DeletedFiles = @() foreach($Event in ($Events | Where-Object {$_.Id -EQ 4660})){ $DeletedFiles += $Events ` | Where-Object {` $_.Id -eq 4663 ` -and $_.RecordID -eq ($Event.RecordID - 1) ` -and ($_.EventXML | where Name -eq "ObjectName").'#text'` -notmatch ".*tmp$|.*~lock$|.*~$" } ` | Select-Object ` @{n="RecordID";e={$Event.RecordID}}, TimeCreated, @{n="ObjectName";e={($_.EventXML | where Name -eq "ObjectName").'#text'}}, @{n="UserName";e={($_.EventXML | where Name -eq "SubjectUserName").'#text'}} } } | Select-Object TotalSeconds | Format-List $DeletedFiles | Sort-Object UserName,TimeDeleted | Format-Table -AutoSize -HideTableHeaders ... TotalSeconds : 167,7099384
      
      





しかし、これは結果です。 ほぼ2倍の加速!



自動化



喜んで、それで十分です。 5分より3分の方が優れていますが、スクリプトを実行する最良の方法は何ですか? 一時間に一回? この方法では、スクリプトの開始と同時に表示されるエントリが抜け落ちます。 1時間ではなく、65分で問い合わせますか? その後、エントリを繰り返すことができます。 はい、そして数千のログの中から目的のファイルの記録を検索します-混乱。 1日1回書く? ログローテーションは半分を忘れます。 より信頼性の高いものが必要です。 Deksの記事のコメントで、誰かがサービスモードで実行されているサブセットアプリケーションについて話しましたが、「14の競合する標準があります」というカテゴリから...



Windowsタスクスケジューラで、syslogのイベントのトリガーを作成できます。 このように:







いいね! ファイルが削除されるとスクリプトが正確に起動され、ログがリアルタイムで作成されます! しかし、発売時にどのイベントを記録する必要があるかを判断できない場合、私たちの喜びは不完全になります。 トリックが必要です。 それらがあります! 短いグーグルは、「イベント」トリガーに従って、スケジューラがイベントに関する情報を実行可能ファイルに送信できることを示しました。 しかし、これは控えめに言って、明白ではありません。 アクションのシーケンスは次のとおりです。



  1. タイプ「イベント」のトリガーでタスクを作成します。
  2. タスクをXML形式で(MMCコンソール経由で)エクスポートします。
  3. 変数を記述する要素を持つ新しい「ValueQueries」ブランチを「EventTrigger」ブランチに追加します。



      <EventTrigger> ... <ValueQueries> <Value name="eventRecordID">Event/System/EventRecordID</Value> </ValueQueries> </EventTrigger>
          
          





    ここで、「eventRecordID」はスクリプトに渡すことができる変数の名前であり、「Event / System / EventRecordID」はWindowsログスキーマ要素であり、記事の下部のリンクにあります。 この場合、一意のイベント番号を持つ要素です。
  4. タスクをスケジューラにインポートして戻します。


しかし、20台のサーバーでマウスを使ってこれらすべてをプッシュしたくはありませんか? 自動化する必要があります。 残念ながら、PowerShellは全能ではないため、New-ScheduledTaskTriggerコマンドレットは、イベントなどのトリガーを作成する方法をまだ知りません。 したがって、チートコードを適用し、COMオブジェクトを介してタスクを作成します(現時点では、通常のコマンドレットはPSのすべての新しいバージョンでますます多くのことを実行できますが、COMに頻繁に頼る必要があります)



 $scheduler = New-Object -ComObject "Schedule.Service" $scheduler.Connect("localhost") $rootFolder = $scheduler.GetFolder("\") $taskDefinition = $scheduler.NewTask(0)
      
      





複数のインスタンスの同時起動を許可する必要があります。また、手動起動を禁止し、時間制限を設定することは価値があるようです。



 $taskDefinition.Settings.Enabled = $True $taskDefinition.Settings.Hidden = $False $taskDefinition.Principal.RunLevel = 0 # 0 -  , 1 -   $taskDefinition.Settings.MultipleInstances = $True $taskDefinition.Settings.AllowDemandStart = $False $taskDefinition.Settings.ExecutionTimeLimit = "PT5M"
      
      





タイプ0(イベント)のトリガーを作成します。 次に、XMLクエリを設定して、必要なイベントを取得します。 XML要求コードは、MMCの「イベントログ」コンソールで必要なパラメーターを選択し、「XML」タブに切り替えることで取得できます。







 $Trigger = $taskDefinition.Triggers.Create(0) $Trigger.Subscription = '<QueryList> <Query Id="0" Path="Security"> <Select Path="Security"> *[System[Provider[@Name="Microsoft-Windows-Security-Auditing"] and EventID=4660]] </Select> </Query> </QueryList>'
      
      





主なトリック:スクリプトに渡す変数を指定します。



 $Trigger.ValueQueries.Create("eventRecordID", "Event/System/EventRecordID")
      
      





実際には、実行されているコマンドの説明:



 $Action = $taskDefinition.Actions.Create(0) $Action.Path = 'PowerShell.exe' $Action.WorkingDirectory = 'C:\Temp' $Action.Arguments = '.\ParseDeleted.ps1 $(eventRecordID) C:\Temp\DeletionLog.log'
      
      





そして-離陸!



 $rootFolder.RegisterTaskDefinition("Log Deleted Files", $taskDefinition, 6, 'SYSTEM', $null, 5)
      
      





「コンセプトが変わりました」



ログを記録するためのスクリプトに戻ります。 ここで、すべてのイベントを受け取る必要はありませんが、引数として渡される唯一のものを取得する必要があります。 これを行うには、スクリプトをパラメーター付きのコマンドレットに変換するヘッダーを追加します。 ヒープに-ログへのパスを「オンザフライ」で変更できるようにします。おそらく便利です。



 [CmdletBinding()] Param( [Parameter(Mandatory=$True,Position=1)]$RecordID, [Parameter(Mandatory=$False,Position=2)]$LogPath = "C:\DeletedFiles.log" )
      
      





次に、ニュアンスが発生します。これまでは、Get-WinEventコマンドレットでイベントを受信し、-FilterHashtableパラメーターでフィルター処理しました。 EventRecordIDを含まない属性の限られたセットを理解します。 そのため、-FilterXmlパラメーターを使用してフィルター処理を行いますが、今は可能です!



 $XmlQuery="<QueryList> <Query Id='0' Path='Security'> <Select Path='Security'>*[System[(EventID=4663) and (EventRecordID=$($RecordID - 1))]]</Select> </Query> </QueryList>" $Event = Get-WinEvent -FilterXml $XmlQuery ` | Select TimeCreated,ID,RecordID,@{n="EventXML";e={([xml]$_.ToXml()).Event.EventData.Data}}`
      
      





1つのイベントのみが処理されるため、Foreach-Object列挙は不要になりました。 コード4660のイベントはスクリプトを開始するためだけに使用されるため、2つではなく、それ自体では有用な情報を伝達しません。

覚えておいて、最初はユーザーが参加せずに悪役を認識できるようにしたかったのですか? そのため、ファイルが部門のドキュメントフォルダーで削除された場合、部門フォルダーのルートにもログを書き込みます。



 $EventLine = "" if (($Event.EventXML | where Name -eq "ObjectName").'#text' -notmatch ".*tmp$|.*~lock$|.*~$"){ $EventLine += "$($Event.TimeCreated)`t" $EventLine += "$($Event.RecordID)`t" $EventLine += ($Event.EventXML | where Name -eq "SubjectUserName").'#text' + "`t" $EventLine += ($ObjectName = ($Event.EventXML | where Name -eq "ObjectName").'#text') if ($ObjectName -match "Documents\"){ $OULogPath = $ObjectName ` -replace "(.*Documents\\\\[^\\]*\\)(.*)",'$1\DeletedFiles.log' if (!(Test-Path $OULogPath)){ "DeletionDate`tEventID`tUserName`tObjectPath"| Out-File -FilePath $OULogPath } $EventLine | Out-File -FilePath $OULogPath -Append } if (!(Test-Path $LogPath)){ "DeletionDate`tEventID`tUserName`tObjectPath" | Out-File -FilePath $LogPath } $EventLine | Out-File -FilePath $LogPath -Append }
      
      





要約コマンドレット


さて、ピースはカットされていますが、すべてをまとめて最適化するためにもう少し残っています。 次のようになります。



 [CmdletBinding()] Param( [Parameter(Mandatory=$True,Position=1,ParameterSetName='logEvent')][int]$RecordID, [Parameter(Mandatory=$False,Position=2,ParameterSetName='logEvent')] [string]$LogPath = "$PSScriptRoot\DeletedFiles.log", [Parameter(ParameterSetName='install')][switch]$Install ) if ($Install) { $service = New-Object -ComObject "Schedule.Service" $service.Connect("localhost") $rootFolder = $service.GetFolder("\") $taskDefinition = $service.NewTask(0) $taskDefinition.Settings.Enabled = $True $taskDefinition.Settings.Hidden = $False $taskDefinition.Settings.MultipleInstances = $True $taskDefinition.Settings.AllowDemandStart = $False $taskDefinition.Settings.ExecutionTimeLimit = "PT5M" $taskDefinition.Principal.RunLevel = 0 $trigger = $taskDefinition.Triggers.Create(0) $trigger.Subscription = ' <QueryList> <Query Id="0" Path="Security"> <Select Path="Security"> *[System[Provider[@Name="Microsoft-Windows-Security-Auditing"] and EventID=4660]] </Select> </Query> </QueryList>' $trigger.ValueQueries.Create("eventRecordID", "Event/System/EventRecordID") $Action = $taskDefinition.Actions.Create(0) $Action.Path = 'PowerShell.exe' $Action.WorkingDirectory = $PSScriptRoot $Action.Arguments = '.\' + $MyInvocation.MyCommand.Name + ' $(eventRecordID) ' + $LogPath $rootFolder.RegisterTaskDefinition("Log Deleted Files", $taskDefinition, 6, 'SYSTEM', $null, 5) } else { $XmlQuery="<QueryList> <Query Id='0' Path='Security'> <Select Path='Security'>*[System[(EventID=4663) and (EventRecordID=$($RecordID - 1))]]</Select> </Query> </QueryList>" $Event = Get-WinEvent -FilterXml $XmlQuery ` | Select TimeCreated,ID,RecordID,@{n="EventXML";e={([xml]$_.ToXml()).Event.EventData.Data}} if (($ObjectName = ($Event.EventXML | where Name -eq "ObjectName").'#text') ` -notmatch ".*tmp$|.*~lock$|.*~$"){ $EventLine = "$($Event.TimeCreated)`t" + "$($Event.RecordID)`t" ` + ($Event.EventXML | where Name -eq "SubjectUserName").'#text' + "`t" ` + $ObjectName if ($ObjectName -match ".*Documents\\\\[^\\]*\\"){ $OULogPath = $Matches[0] + '\DeletedFiles.log' if (!(Test-Path $OULogPath)){ "DeletionDate`tEventID`tUserName`tObjectPath"| Out-File -FilePath $OULogPath } $EventLine | Out-File -FilePath $OULogPath -Append } if (!(Test-Path $LogPath)){ "DeletionDate`tEventID`tUserName`tObjectPath" | Out-File -FilePath $LogPath } $EventLine | Out-File -FilePath $LogPath -Append } }
      
      





スクリプトを便利な場所に配置し、-Installスイッチを使用して実行することは変わりません。



現在、どの部門の従業員でも、ディレクトリから誰、何、いつ削除されたかをリアルタイムで確認できます。 ここでは、ログファイルへのアクセス権(悪役がファイルを削除できないようにするため)とローテーションを考慮しなかったことに注意してください。 ファイラー上のディレクトリへの構造とアクセス権は別の記事に取り込まれ、ある程度のローテーションは目的の行の検索を複雑にします。



使用した材料:


- 最高の正規表現ガイド

- イベントバインドタスクの作成に関するチュートリアル

- スクリプトAPIタスクスケジューラの説明



UPD:最終スクリプトにタイプミスがありました。41行目以降に余分な砂利がありました。 発見については、読者のHabr Ruslan Sultanovに感謝します。



All Articles