[Из песочницы] Отслеживаем удаление файлов на PowerShell
Привет, Хабр! Тема моего поста уже поднималась здесь, но мне есть, что добавить.Когда наше файловое хранилище разменяло третий терабайт, все чаще наш отдел стал получать просьбы выяснить, кто удалил важный документ или целую папку с документами. Нередко это происходит по чьему-то злому умыслу. Бэкапы — это хорошо, но страна должна знать своих героев. А молоко вдвойне вкусней, когда мы можем написать его на PowerShell.
Пока разбирался, решил записать для коллег по цеху, а потом подумал, что может пригодиться кому-то еще. Материал получился смешанный. Кто-то найдет для себя готовое решение, кому-то пригодятся несколько неочевидные методы работы с PowerShell или планировщиком задач, а кто-то проверит на быстродействие свои скрипты.
В процессе поиска решения задачи прочитал статью за авторством Deks. Решил взять ее за основу, но некоторые моменты меня не устраивали.
Во-первых, время генерации отчета за четыре часа на 2-терабайтном хранилище, с которым одновременно работает около 200 человек, составило около пяти минут. И это притом, что лишнего у нас в логи не пишется. Это меньше, чем у Deks, но больше, чем хотелосю бы, потому что… Во-вторых, все то же самое нужно было реализовать еще на двадцати серверах, гораздо менее производительных, чем основной. В-третьих, вызывал вопросы график запуска генерации отчетов. И в-четвертых, хотелось исключить себя из процесса доставки собранной информации конечным потребителям (читай: автоматизировать, чтобы мне с этим вопросом больше не звонили). Но ход мыслей Deks мне понравился…Краткий дискурс: При включенном аудите файловой системы в момент удаления файла в журнале безопасности создаются два события, с кодами 4663 и, следом, 4660. Первое записывает попытку запроса доступа на удаление, данные о пользователе и пути к удаляемому файлу, а второе — фиксирует свершившийся факт удаления. У событий есть уникальный идентификатор EventRecordID, который отличается на единицу у этих двух событий.Ниже приведен исходный скрипт, собирающий информацию об удаленных файлах и пользователях, их удаливших.
$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 Многовато, на вторичных ФС будет дольше. Сходу очень не понравился десятиэтажный пайп, поэтому для начала я его структурировал: 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 А вот это уже результат. Ускорение практически в два раза! Автоматизируем Порадовались, и хватит. Три минуты — это лучше, чем пять, но как лучше всего запускать скрипт? Раз в час? Так могут ускользнуть записи, которые появляются одновременно с запуском скрипта. Делать запрос не за час, а за 65 минут? Тогда записи могут повторяться. Да и искать потом запись о нужном файле среди тысячи логов — мутор. Писать раз в сутки? Ротация логов забудет половину. Нужно что-то более надежное. В комментариях к статье Deks кто-то говорил о приложении на дотнете, работающем в режиме службы, но это, знаете, из разряда «There are 14 competing standards»…В планировщике заданий Windows можно создать триггер на событие в системном журнале. Вот так:
Отлично! Скрипт будет запускаться ровно в момент удаления файла, и наш журнал будет создаватья в реальном времени! Но наша радость будет неполной, если мы не сможем определить, какое событие нам нужно записать в момент запуска. Нам нужна хитрость. Их есть у нас! Недолгий гуглинг показал, что по триггеру «Событие» планировщик может передавать исполняемому файлу информацию о событии. Но делается это, мягко говоря, неочевидно. Последовательность действий такая:
Создать задачу с триггером типа «Event»;
Экспортировать задачу в формат XML (через консоль MMC);
Добавить в ветку «EventTrigger» новую ветвь «ValueQueries» с элементами, описывающими переменные:
$Trigger = $taskDefinition.Triggers.Create (0)
$Trigger.Subscription = '
Использованные материалы: — Прекраснейший справочник по регулярным выражениям— Туториал по созданию задачи, привязанной к событию— Описание скриптового API планировщика заданий