Forked from TrimIdeas/Audit changed or deleted files in Windows Server 2008 R2 or newer.ps1
Created
June 14, 2019 02:50
-
-
Save EdwardNavarro/4270b83d220750823a147a69bb04897c to your computer and use it in GitHub Desktop.
Audit changed and deleted files on Server 2008 R2, 2012, and 2012 R2
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Version 1.2 :: This script reads offline event logs, oldest to newest, bottom to top. | |
# See http://www.trimideas.com/2015/04/auditing-changed-deleted-files.html for instructions. | |
$LogPath = "C:\Event_Logs\" | |
$ReportPath = "C:\Audit\File-Audit-Reports\" | |
$Formatted_Date = (Get-Date -UFormat %A-%B-%d-at-%I-%M-%S%p) | |
$ZipName = "Security-Events-for-" + (Get-Date -UFormat %A-%B-%d) + ".zip" | |
$Report_in_CSV = $ReportPath + "Audit of changed files on " + $Formatted_Date + ".csv" | |
$Truncated_Log_Path = $LogPath + "Archive-Security_on_" + $Formatted_Date + ".evtx" | |
$Today_Midnight = (Get-Date -Hour 0 -Minute 0 -Second 0) | |
$Total_Events = 0 | |
$Start_Time = Get-Date | |
$Exclude_Windows_Directory = $TRUE | |
# Hashtable | |
# http://blogs.msdn.com/b/mediaandmicrocode/archive/2008/11/27/microcode-powershell-scripting-tricks-the-joy-of-using-hashtables-with-windows-powershell.aspx | |
$Pending_Delete=@{} | |
# Dynamically expanding array | |
# www.jonathanmedd.net/2014/01/adding-and-removing-items-from-a-powershell-array.html | |
[System.Collections.ArrayList]$Audit_Report=@("User,Action,Source,Destination,Time,DebugNotes") | |
# Files to purge from the Pending_Delete hashtable | |
[System.Collections.ArrayList]$MyGarbage=@() | |
# This allows the Try...Catch error handling to work. | |
# http://blogs.technet.com/b/heyscriptingguy/archive/2014/07/05/weekend-scripter-using-try-catch-finally-blocks-for-powershell-error-handling.aspx | |
$ErrorActionPreference = ‘Stop’ | |
# This function decides if the object is a file or a folder | |
# http://blogs.technet.com/b/heyscriptingguy/archive/2014/08/31/powertip-using-powershell-to-determine-if-path-is-to-file-or-folder.aspx | |
Function Is_a_File | |
{ | |
# If the path still exists, we can know for certain if it's a file or folder | |
Try {If ((Get-Item $Event.ObjectName) -is [System.IO.FileInfo]) {Return $True}} | |
# If the path is gone, we'll assume that it's a file if it contains a period. | |
Catch {If ($Event.ObjectName -like "*.*") {Return $True}} | |
} | |
# This function returns just the filename from a full path. | |
Function Get-FileName ($fullpath) | |
{If ($fullpath -ne $null) {Return $fullpath.Split('\')[-1]}} | |
# This function returns just the foldername from a full path. | |
Function Get-FolderName ($fullpath) | |
{If ($fullpath -ne $null) {Return $fullpath.Substring(0,$fullpath.LastIndexOf("\"))}} | |
# This function checks to see if the file should be ignored. | |
Function Is_Temporary | |
{ | |
$fullpath = $Event.ObjectName | |
# A list of file extensions to ignore | |
$Temp_Files = "tmp","rgt","mta","tlg",".nd",".ps","log",":Zone.Identifier","crdownload",".DS_Store",":AFP_AfpInfo",":AFP_Resource" | |
# Check that the full path is long enough to be applicable, then ask if the file's last few characters match a temporary extension. | |
$Temp_Files | ForEach {If ($fullpath.length -ge $_.length -AND $fullpath.substring($fullpath.length - $_.length, $_.length) -eq $_) {Return $True}} | |
If ((Get-FileName $fullpath).substring(0,1) -eq "~") {Return $True} | |
If ((Get-FileName $fullpath) -eq "thumbs.db") {Return $True} | |
Return $False | |
} | |
# This function empties out the Pending_Delete array, deciding whether the file was created/modified or deleted. | |
Function CleanUp ($Seconds2Wait) | |
{ | |
ForEach ($Key in $Pending_Delete.Keys) | |
{ | |
# Calculate the number of seconds | |
$TimeSpan = -((New-TimeSpan -Start $Event.TimeCreated -End $Pending_Delete[$Key].TimeCreated).TotalSeconds) | |
If ($Pending_Delete[$Key].Alive) | |
{ | |
Audit_Report_Add $Pending_Delete[$Key].User "created/modified" $Key "" $Pending_Delete[$Key].TimeCreated "Single 4663 event" | |
# Mark the item for removal from the list of deleted items | |
$MyGarbage.Add($Key) | |
} | |
# Conclude that the object was deleted if it hasn't been referenced in the past $Seconds2Wait | |
# I was using the .Confirmed tag for this, but PDF printers were triggering false positives. | |
ElseIf ($TimeSpan -ge $Seconds2Wait) | |
{ | |
Audit_Report_Add $Pending_Delete[$Key].User "deleted" $Key "" $Pending_Delete[$Key].TimeCreated "Aged out" | |
# Mark the item for removal from the list of deleted items | |
$MyGarbage.Add($Key) | |
} | |
} | |
# You can't remove items from a hashtable and then continue enumerating it, so this is a work-around. | |
ForEach ($Item in $MyGarbage) {$Pending_Delete.Remove($Item)} | |
# Clear this temporary array now that I'm done with it. | |
$MyGarbage.Clear() | |
# This is not expected to happen, but if it does - I'll know about it! | |
If ($Seconds2Wait -eq 0 -AND $Pending_Delete.Count -ge 1) | |
{$Audit_Report.Add("Orphaned objects") | |
$Pending_Delete.Keys | ForEach-Object {$Audit_Report.Add($_)}} | |
} | |
# This function checks for duplicate lines when writing audit lines to the report. | |
# A default value of " " is assigned to each parameter in case no value was specified when the function was called. | |
Function Audit_Report_Add ($User1 = " ",$Action1 = " ",$Source1 = " ",$Destination1 = " ",$Time1 = " ",$DebugNotes1 = " ") | |
{ | |
# Attempt to show the person's full name instead of just their username | |
$User1 = Try{[string](Get-ADUser $User1).Name} Catch {$User1} | |
# The current line that we'd like to write into the report. | |
$NewLine = $User1 + "," + $Action1 + ",`"" + $Source1 + "`"" | |
# The last line that was written into the report. | |
# On Server 2008 R2 w/ .NET 3.5, I wasn't able to reference the last item in a System.Collections.ArrayList via [-1], so used this way instead. | |
$LastLine = Try{$Audit_Report[$Audit_Report.Count-1].Substring(0,$NewLine.Length)} Catch {" "} | |
# If the new line isn't a duplicate, then put it in the report. | |
If ($LastLine -ne $NewLine) {$Audit_Report.Add($User1 + "," + $Action1 + ",`"" + $Source1 + "`",`"" + $Destination1 + "`"," + $Time1 + "," + $DebugNotes1)} | |
} | |
# This function increases the number of logs you can save by compressing them to save disk space | |
Function Compress_Logs | |
{ | |
Try | |
{ | |
# You'll need the command-line version of 7-zip (www.7-zip.org) in the $LogPath directory | |
Set-Location $LogPath | |
Start-Process -FilePath "7za.exe" -ArgumentList "a $ZipName Archive-Security*" -Wait | |
# Manually trigger .NET garbage collection to close open file handles so security logs can be deleted after compression. | |
[System.GC]::Collect() | |
Sleep 60 | |
# Delete the originals now that they're zipped. | |
Get-ChildItem ($LogPath + "Archive-Security*") | Remove-Item | |
} | |
Catch | |
{ | |
"$Start_Time Unable to zip/delete the logs. Maybe you need to put `"7za.exe`" in $LogPath" | Out-File ($LogPath + "A WARNING.txt") -Append | |
} | |
} | |
# This optional function deletes old logs so they don't accumulate forever | |
Function Prune_Logs ($Days2Retain) | |
{ | |
# Gather all the zip files older than the specified number of days and delete them. | |
Get-ChildItem *.zip | ? {$_.LastWriteTime -lt (Get-Date).AddDays(-$Days2Retain)} | Remove-Item | |
} | |
# Backup and clear the event log | |
# https://4sysops.com/archives/managing-the-event-log-with-powershell-part-2-backup | |
$Security_log = get-wmiobject win32_nteventlogfile -filter "logfilename = 'Security'" | |
$Security_log.BackupEventlog($Truncated_Log_Path) | |
Clear-Eventlog "Security" | |
# Collect all the event logs from today and order them oldest to newest. | |
$Event_Logs = Get-ChildItem ($LogPath + "*.evtx") | ?{$_.LastWriteTime -ge $Today_Midnight} | Sort LastWriteTime | |
ForEach ($Log in $Event_Logs) | |
{ | |
# Define what we want to pull out of the event log | |
$MyFilter = @{Path=($Log).FullName;ID=4656,4659,4660,4663} | |
# Retrieve events; if none are found, immediately go to the next log file. | |
Try {$Events = Get-WinEvent -FilterHashTable $MyFilter -Oldest} Catch {"No events were found in $Log"; Continue} | |
ForEach ($Raw_Event in $Events) | |
{ | |
# Count how many events are processed and display it in the HTML report | |
$Total_Events++ | |
# Convert the event into an XML object | |
# Thanks to http://blogs.technet.com/b/ashleymcglone/archive/2013/08/28/powershell-get-winevent-xml-madness-getting-details-from-event-logs.aspx | |
Try{$EventXML = [xml]$Raw_Event.ToXML()} Catch {Audit_Report_Add "Unable to convert an event to XML"} | |
# Loop through the XML values and turn them into a hashtable for easy access. | |
# http://learn-powershell.net/2010/09/19/custom-powershell-objects-and-performance/ | |
# http://stackoverflow.com/questions/10847573/changing-powershell-pipeline-type-to-hashtable-or-any-other-enumerable-type | |
$Event = @{} | |
ForEach ($object in $EventXML.Event.EventData.Data) {$Event.Add($object.name,$object.'#text')} | |
$Event.Add("ID",$Raw_Event.ID) | |
$Event.Add("TimeCreated",$Raw_Event.TimeCreated) | |
# Check if we're supposed to ignore changes in the C:\Windows directory (e.g. Windows Updates). | |
If ($Exclude_Windows_Directory) | |
{If ($Event.ObjectName.Length -ge 10 -and $Event.ObjectName.Substring(0,10) -eq "C:\Windows") {Continue}} | |
If ($Event.ID -eq "4656" -AND # Event 4656 = a handle was requested. | |
-NOT (Is_Temporary) -AND # Exclude temporary files. | |
$Pending_Delete.ContainsKey($Event.ObjectName)) # It's common for a "Delete" event to be logged right before a file is created/saved. | |
# The file was not deleted, so mark it as created/modified. | |
{If ($Pending_Delete.ContainsKey($Event.ObjectName)) {$Pending_Delete[$Event.ObjectName].Alive = $TRUE}} | |
ElseIf ($Event.ID -eq "4663" -AND # Event 4663 = object access. | |
$Event.AccessMask -eq "0x10000" -AND # 0x10000 = Delete, but this can mean different things - delete, overwrite, rename, move. | |
-NOT (Is_Temporary)) # Exclude temporary files | |
{ | |
# If I've already logged a "Delete" for this file...and am seeing it again... | |
# ...the file wasn't actually deleted, so mark it as created/modified. | |
If ($Pending_Delete.ContainsKey($Event.ObjectName)) | |
{ | |
# Exclude folders | |
If (Is_a_File) {Audit_Report_Add $Event.SubjectUsername "created/modified" $Event.ObjectName "" $Event.TimeCreated "Twin 4663 events"} | |
# The file/folder was not actually deleted, so remove it from this array. | |
$Pending_Delete.Remove($Event.ObjectName) | |
} | |
# Now record the filename, username, handle ID, and time. | |
$Pending_Delete.Add($Event.ObjectName,@{User = $Event.SubjectUsername; HandleID = $Event.HandleId; TimeCreated = $Event.TimeCreated; Alive = $FALSE; Confirmed = $FALSE}) | |
} | |
ElseIf ($Event.ID -eq "4663" -AND # Event 4663 = object access. | |
$Event.AccessMask -eq "0x2" -AND # 0x2 = is a classic "object was modified" signal. | |
-NOT (Is_Temporary) -AND # Exclude temporary files. | |
(Is_a_File)) # Exclude folders | |
{ | |
Audit_Report_Add $Event.SubjectUsername "created/modified" $Event.ObjectName "" $Event.TimeCreated "0x2 AccessMask" | |
# The file was not actually deleted, so remove it from this array. | |
$Pending_Delete.Remove($Event.ObjectName) | |
} | |
ElseIf ($Event.ID -eq "4663" -AND | |
$Event.AccessMask -eq "0x80") # A 4663 event with 0x80 (Read Attributes) is logged | |
{ # with the same handle ID when files/folders are moved or renamed. | |
ForEach ($Key in $Pending_Delete.Keys) | |
{ | |
# If the Handle & User match...and the object wasn't deleted...figure out whether it was moved or renamed. | |
If ($Pending_Delete[$Key].HandleID -eq $Event.HandleID -AND | |
$Pending_Delete[$Key].User -eq $Event.SubjectUsername -AND | |
-NOT $Pending_Delete[$Key].Confirmed -AND | |
-NOT (Is_Temporary)) | |
{ | |
If ((Get-FileName $Event.ObjectName) -ceq (Get-FileName $Key) -AND -NOT $Pending_Delete[$Key].InvalidHandleID) | |
{Audit_Report_Add $Event.SubjectUsername "moved" $Key $Event.ObjectName $Event.TimeCreated | |
$Pending_Delete.Remove($Key)} | |
ElseIf ((Get-FolderName $Event.ObjectName) -ceq (Get-FolderName $Key)) | |
{ | |
If ((Get-Filename $Key) -eq "New Folder") | |
{Audit_Report_Add $Event.SubjectUsername "created" $Event.ObjectName "" $Event.TimeCreated} | |
Else | |
{Audit_Report_Add $Event.SubjectUsername "renamed" $Key $Event.ObjectName $Event.TimeCreated} | |
$Pending_Delete.Remove($Key) | |
} | |
Break | |
} | |
} | |
# If none of those conditions match, at least note that the file still exists (if applicable). | |
If ($Pending_Delete.ContainsKey($Event.ObjectName)) {$Pending_Delete[$Event.ObjectName].Alive = $TRUE} | |
} | |
ElseIf ($Event.ID -eq "4659" -AND # Event 4659 = a handle was requested with intent to delete | |
-NOT (Is_Temporary)) # Exclude temporary files | |
{ | |
# If you use Windows File Explorer on Server 2012 R2 to delete a file: event 4659 is logged on the destination file server. | |
# If you use a command prompt on Server 2012 R2 to delete a file: event 4663 is logged on the destination file server. | |
Audit_Report_Add $Event.SubjectUsername "deleted" $Event.ObjectName "" $Event.TimeCreated "Event 4659" | |
} | |
# This delete confirmation doesn't happen when objects are moved/renamed; it does when files are created/deleted. | |
ElseIf ($Event.ID -eq "4660") | |
{ | |
ForEach ($Key in $Pending_Delete.Keys) | |
{ | |
If ($Event.HandleID -eq $Pending_Delete[$Key].HandleID -AND | |
$Event.SubjectUserName -eq $Pending_Delete[$Key].User) | |
{$Pending_Delete[$Key].Confirmed = $TRUE} | |
} | |
} | |
CleanUp 120 | |
} | |
} | |
# Flush out the list of deleted items (improbable in production at 11:45pm, but was an issue when testing the script). | |
CleanUp 0 | |
# Output the audit results to a CSV file for manipulation in Excel | |
$Audit_Report | Out-File $Report_in_CSV | |
# Zip the event logs to reduce file size | |
Compress_Logs | |
# Delete logs older than 90 days | |
Prune_Logs 90 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment