Search code examples
powershellevent-viewerwindows-server-2012-r2

How to parse and delete archived event logs in Powershell


I'm trying to parse archived Security logs to track down an issue with changing permissions. This script greps through .evtx files that are +10 days old. It currently outputs what I want, but when it goes to clean up the old logs (About 50GB/daily, uncompressed, each of which are archived into their own daily folder via another script that runs at midnight) it begins complaining that the logs are in use and cannot be deleted. The process that seems to be in use when I try to delete the files through Explorer is alternately DHCP Client or Event Viewer, stopping both of these services works, but clearly I can't run without eventvwr. DHCP client is used for networking niceness but is not needed.

The only thing that touches the .evtx files is this script, they're not backed up, they're not monitored by anything else, they're not automatically parsed by the Event Log service, they're just stored on disk waiting.

The script originally deleted things as it went, but then since that failed all the deletions were moved to the end, then to the KillLogWithFire() function. Even the timer doesn't seem to help. I've also tried moving the files to a Processed subfolder, but that does't work for the same reason.

I assume that there's some way to release any handles that this script opens on any files, but attempting to .close() or .dispose() of the EventLog variable in the loop doesn't work.

$XPath = @'
*[System[Provider/@Name='Microsoft-Windows-Security-Auditing']]
and
*[System/EventID=4670]
'@

$DeletableLogs = @()
$logfile = "L:\PermChanges.txt"
$AdminUsers = ("List","of","Admin","Users")

$today = Get-Date
$marker = "
-------------
$today
-------------
"
write-output $marker >> $logfile 

Function KillLogWithFire($log){
    Try {
        remove-item $log
    }
    Catch [writeerror]{
        $Timer += 1
        sleep $timer
        write-output "Killing log $log in $timer seconds"
        KillLogWithFire($log)
    }
}

Function LogPermissionChange($PermChanges){
    ForEach($PermChange in $PermChanges){
        $Change = @{}
        $Change.ChangedBy = $PermChange.properties[1].value.tostring()

        #Filter out normal non-admin users
        if ($AdminUsers -notcontains $Change.ChangedBy){continue} 
        $Change.FileChanged = $PermChange.properties[6].value.tostring()
        #Ignore temporary files
        if ($Change.FileChanged.EndsWith(".tmp")){continue}
        elseif ($Change.FileChanged.EndsWith(".partial")){continue}

        $Change.MadeOn = $PermChange.TimeCreated.tostring()
        $Change.OriginalPermissions = $PermChange.properties[8].value.tostring()
        $Change.NewPermissions = $PermChange.properties[9].value.tostring()

        write-output "{" >> $logfile
        write-output ("Changed By           : "+ $Change.ChangedBy) >> $logfile
        write-output ("File Changed         : "+ $Change.FileChanged) >> $logfile
        write-output ("Change Made          : "+ $Change.MadeOn) >> $logfile
        write-output ("Original Permissions : 
            "+ $Change.OriginalPermissions) >> $logfile
        write-output ("New Permissions      : 
            "+ $Change.NewPermissions) >> $logfile
        "}
" >> $logfile
    }
}

GCI -include Archive-Security*.evtx -path L:\Security\$Today.AddDays(-10) -recurse | ForEach-Object{
    Try{
        $PermChanges = Get-WinEvent -Path $_ -FilterXPath $XPath -ErrorAction Stop
    }
    Catch [Exception]{
        if ($_.Exception -match "No events were found that match the specified selection criteria."){
        }
        else {
            Throw $_
        }
    }
    LogPermissionChange($PermChanges)
    $PermChanges = $Null 
    $DeletableLogs += $_
}

foreach ($log in $DeletableLogs){
    $Timer = 0
    Try{
        remove-item $log
        }
    Catch [IOException]{
        KillLogWithFire($log)
    }
}

UPDATE

Rather than editing the original code as I've been told not to do, I wanted to post the full code that's now in use as a separate answer. The Initial part, which parses the logs and is run every 30 minutes is mostly the same as above:

$XPath = @'
*[System[Provider/@Name='Microsoft-Windows-Security-Auditing']]
and
*[System/EventID=4670]
'@

$DeletableLogs = @()
$logfile = "L:\PermChanges.txt"
$DeleteList = "L:\DeletableLogs.txt"
$AdminUsers = ("List","Of","Admins")

$today = Get-Date
$marker = "
-------------
$today
-------------
"
write-output $marker >> $logfile 

Function LogPermissionChange($PermChanges){
    ForEach($PermChange in $PermChanges){
        $Change = @{}
        $Change.ChangedBy = $PermChange.properties[1].value.tostring()

        #Filter out normal non-admin users
        if ($AdminUsers -notcontains $Change.ChangedBy){continue} 
        $Change.FileChanged = $PermChange.properties[6].value.tostring()
        #Ignore temporary files
        if ($Change.FileChanged.EndsWith(".tmp")){continue}
        elseif ($Change.FileChanged.EndsWith(".partial")){continue}

        $Change.MadeOn = $PermChange.TimeCreated.tostring()
        $Change.OriginalPermissions = $PermChange.properties[8].value.tostring()
        $Change.NewPermissions = $PermChange.properties[9].value.tostring()

        write-output "{" >> $logfile
        write-output ("Changed By           : "+ $Change.ChangedBy) >> $logfile
        write-output ("File Changed         : "+ $Change.FileChanged) >> $logfile
        write-output ("Change Made          : "+ $Change.MadeOn) >> $logfile
        write-output ("Original Permissions : 
            "+ $Change.OriginalPermissions) >> $logfile
        write-output ("New Permissions      : 
            "+ $Change.NewPermissions) >> $logfile
        "}
" >> $logfile
    }
}

GCI -include Archive-Security*.evtx -path L:\Security\ -recurse | ForEach-Object{
    Try{
        $PermChanges = Get-WinEvent -Path $_ -FilterXPath $XPath -ErrorAction Stop
    }
    Catch [Exception]{
        if ($_.Exception -match "No events were found that match the specified selection criteria."){
        }
        else {
            Throw $_
        }
    }
    LogPermissionChange($PermChanges)
    $PermChanges = $Null 
    $DeletableLogs += $_
}

foreach ($log in $DeletableLogs){
    write-output $log.FullName >> $DeleteList
    }

The second portion does the deletion, including the helper function above graciously provided by TheMadTechnician. The code still loops as the straight delete is faster than the function, but not always successful even ages after the files have not been touched.:

# Log Cleanup script. Works around open log issues caused by PS parsing of 
# saved logs in EventLogParser.ps1

$DeleteList = "L:\DeletableLogs.txt"
$DeletableLogs = get-content $DeleteList

Function Close-LockedFile{
Param(
    [Parameter(Mandatory=$true,ValueFromPipeline=$true)][String[]]$Filename
)
Begin{
    $HandleApp = 'C:\sysinternals\Handle.exe'
    If(!(Test-Path $HandleApp)){Write-Host "Handle.exe not found at $HandleApp`nPlease download it from www.sysinternals.com and save it in the afore mentioned location.";break}
}
Process{
    $HandleOut = Invoke-Expression ($HandleApp+' '+$Filename)
    $Locks = $HandleOut |?{$_ -match "(.+?)\s+pid: (\d+?)\s+type: File\s+(\w+?): (.+)\s*$"}|%{
        [PSCustomObject]@{
            'AppName' = $Matches[1]
            'PID' = $Matches[2]
            'FileHandle' = $Matches[3]
            'FilePath' = $Matches[4]
        }
    }
    ForEach($Lock in $Locks){
        Invoke-Expression ($HandleApp + " -p " + $Lock.PID + " -c " + $Lock.FileHandle + " -y") | Out-Null
    If ( ! $LastexitCode ) { "Successfully closed " + $Lock.AppName + "'s lock on " + $Lock.FilePath}
    }
}
}

Function KillLogWithFire($log){
    Try {
        Close-LockedFile $Log - 
    }
    Catch [System.IO.IOException]{
        $Timer += 1
        sleep $timer
    write-host "Killing $Log in $Timer seconds with fire."
    KillLogWithFire($Log)
    }
}

foreach ($log in $DeletableLogs){
    Try {
        remove-item $log -ErrorAction Stop
        }
    Catch [System.IO.IOException]{
        $Timer = 0
        KillLogWithFire($Log)
        }
    }

remove-item $DeleteList

Solution

  • One solution would be to get HANDLE.EXE and use it to close any open handles. Here's a function that I use roughly based off of this script. It uses handle.exe, finds what has a file locked, and then closes handles locking that file open.

    Function Close-LockedFile{
    Param(
        [Parameter(Mandatory=$true,ValueFromPipeline=$true)][String[]]$Filename
    )
    Begin{
        $HandleApp = 'C:\sysinternals\Handle.exe'
        If(!(Test-Path $HandleApp)){Write-Host "Handle.exe not found at $HandleApp`nPlease download it from www.sysinternals.com and save it in the afore mentioned location.";break}
    }
    Process{
        $HandleOut = Invoke-Expression ($HandleApp+' '+$Filename)
        $Locks = $HandleOut |?{$_ -match "(.+?)\s+pid: (\d+?)\s+type: File\s+(\w+?): (.+)\s*$"}|%{
            [PSCustomObject]@{
                'AppName' = $Matches[1]
                'PID' = $Matches[2]
                'FileHandle' = $Matches[3]
                'FilePath' = $Matches[4]
            }
        }
        ForEach($Lock in $Locks){
            Invoke-Expression ($HandleApp + " -p " + $Lock.PID + " -c " + $Lock.FileHandle + " -y") | Out-Null
        If ( ! $LastexitCode ) { "Successfully closed " + $Lock.AppName + "'s lock on " + $Lock.FilePath}
        }
    }
    }
    

    I have handle.exe saved in C:\Sysinternals, you may want to adjust the path in the function, or save the executable there.