Export Critical, Warning and Errors events from Windows Logs - powershell

I'm using most of the script from here.https://kb.webspy.com/s/article/windows-event-logs-and-powershell
However, I was wondering if there is a way only export Critical, Warning and Errors events. I know those events levels are 1-3
Get-WinEvent -FilterHashTable #{LogName = "System"; Level=1,2,3; StartTime=((Get-Date).AddDays(-7))} -ComputerName "server1" #| Out-GridView
I was just wondering where to add the level to this script.
# Logs to extract from server
$logArray = #("System","Security","Application")
# Grabs the server name to append to the log file extraction
$servername = $env:computername
# Provide the path with ending "\" to store the log file extraction.
$destinationpath = "C:\WindowsEventLogs\"
# Checks the last character of the destination path. If it does not end in '\' it adds one.
# '.+?\\$' +? means any character \\ is looking for the backslash $ is the end of the line charater
if ($destinationpath -notmatch '.+?\\$')
{
$destinationpath += '\'
}
# If the destination path does not exist it will create it
if (!(Test-Path -Path $destinationpath))
{
New-Item -ItemType directory -Path $destinationpath
}
# Get the current date in YearMonthDay format
$logdate = Get-Date -format yyyyMMddHHmm
# Start Process Timer
$StopWatch = [system.diagnostics.stopwatch]::startNew()
# Start Code
Clear-Host
Foreach($log in $logArray)
{
# If using Clear and backup
$destination = $destinationpath + $servername + "-" + $log + "-" + $logdate + ".evtx"
Write-Host "Extracting the $log file now."
# Extract each log file listed in $logArray from the local server.
wevtutil epl $log $destination
}
# End Code
# Stop Timer
$StopWatch.Stop()
$TotalTime = $StopWatch.Elapsed.TotalSeconds
$TotalTime = [math]::Round($totalTime, 2)
write-host "The Script took $TotalTime seconds to execute."

It seems like the code is using wevtutil to retrieve information about event logs.
wevtutil epl $log $destination
From the documentation wevtutil also accept different options and one of which is /q:<Query>.
Defines the XPath query to filter the events that are read or
exported. If this option is not specified, all events will be returned
or exported. This option is not available when /sq is true.
So you could create a Xpath query to apply filter based on event levels
wevtutil epl $log $destination /q:"*[System[(Level=1 or Level=2 or Level=3)]]"

Related

Need to find the name of the current logfile being used in IIS

I am needing to write a script to automate moving all of the IIS log files from a system that has been running for months. This system consists of many web servers.
I am successful at getting the log directory moved to the new location, and then my script will move the other log files from the old directory to the new location with the exception of the current file. It cannot move because there is a file in the destination with the same name. (current log file.) I want to rename it, but do not want to rename all files in the directory. I know I could use a wildcard, but would prefer to rename only that file.
What command can I use to find the name of the file, not the directory or path? I have pieced this together from other smaller requests I have found on here and on the web.
Import-Module WebAdministration
$LogPath = "e:\Logs\IISlogs"
foreach($WebSite in $(get-website))
{
$logFile="$($Website.logFile.directory)\w3svc$($website.id)".replace("%SystemDrive%",$env:SystemDrive)
}
New-Item $LogPath -ItemType Directory
Set-ItemProperty “IIS:\Sites\Default Web Site” -name logFile.directory -value $LogPath
$path = $logpath+"\W3SVC1"
$Timeout = 60
$timer = [Diagnostics.Stopwatch]::StartNew()
while (($timer.Elapsed.TotalSeconds -lt $Timeout) -and (-not (Test-Path -Path $path )))
{
Start-Sleep -Seconds 1
$tot = $timer.Elapsed.Seconds
Write-Output -Message ("Still waiting for action to complete after " + $tot + " seconds upto " + $Timeout)
}
$timer.Stop()
Move "$($logfile)\*.*" $path
All files have a time a date stamp. Filter the file name, sort by the time stamp descending, and select the first one.
For example:
$dir = "e:\Logs\IISlogs"
($latest = Get-ChildItem -Path $dir |
Sort-Object LastAccessTime -Descending |
Select-Object -First 1)
"Current log file details: $($latest.name)"
Get-Content $latest
If you are saying the logs can be of a different name, then you need to specify the wildcard match for that in the path specification.

Output results to text file

Thanks to #Theo yesterday I got my script to work and do exactly all the things I wanted. So now have a lovely menu system, options etc.
But I've decided - like a charm I wanted to do a little more!
So part of what is happening is finding files and renaming them. Theo's example was best:
D:\The Old Man and his dog.mp4 → D:\Old Man and his dog (The).mp4
D:\A Nightmare on Elm Street.mp4 → D:\Nightmare on Elm Street (A).mp4
D:\I, Robot.mkv → D:\Robot (I).mkv
What I wanted to do is change from output to screen the results. To a log file of changes made, and preferably update the file and add or make a new log file every time the script runs. Just in case I ever need to find something or check what happened - I've got something to refer back to!
Below is the code I've used but not to happy with to display the results.
Start-Sleep -s 5
cls
Write-Output "
Ok then, $filter and $rename has now been searched and changed.
With a total of
(Get-ChildItem | ? {$_.CreationTime -ge (Get-Date).Addminutes(-10)}).Count
Write-Output "files being changed.
"
pause
cls}
I would like to get a better display result, i.e.
xx Files have been successfully update
xx Files failed.
Please see log for details.
And then look in the log file and see something like:
Log1.txt:
D:\The Old Man and his dog.mp4 successfully Changed to D:\Old Man and his dog (The).mp4
Total of 1 /1 files successfully changed
I'm thinking of Compare-Object and Out-File, so maybe
Compare-Object $(Get-Content c:\user\documents\List1.txt) $(Get-Content c:\user\documents\List2.txt) > c:\user\documents\diff_output.txt
But for some reason I cannot get my head around this idea and figure out where to start.
If I understand the requirements correctly, extending on my previous code, you could do this:
$rootFolder = 'D:\' #'# the root folder where the files and subfolders are
$logFile = 'D:\renamed_{0:yyyy-MM-dd HH_mm_ss}.txt' -f (Get-Date) #'# the file to log to, including the current date
# create an array of words that start the file name and should be moved towards the back
$words = 'The', 'i', 'a'
# create a regex pattern where all possible words are separated by the regex OR (|) sign
$re = '^({0})[ ,]+' -f ($words -join '|')
# create two integer counter variables
[int]$success, [int]$failed = 0
Get-ChildItem -Path $rootFolder | Where-Object { $_.Name -match $re } | ForEach-Object {
$newName = '{0} ({1}){2}' -f ($_.BaseName -replace $re), $matches[1], $_.Extension
try {
$_ | Rename-Item -NewName $newName -ErrorAction Stop -WhatIf
Add-Content -Path $logFile -Value "Successfully renamed '$($_.FullName)' to '$newName'"
$success++
}
catch {
Add-Content -Path $logFile -Value "Rename failed for file '$($_.FullName)'"
$failed++
}
}
# write summary
$total = $success + $failed
$summary = "Total of $success / $total files successfully changed`r`nTotal of $failed / $total files failed"
# output summary to the log file
Add-Content -Path $logFile -Value "`r`n==========================================================="
Add-Content -Path $logFile -Value $summary
# output summary on screen
Write-Host "`r`n$summary`r`nPlease see '$logFile' for details." -ForegroundColor Yellow
Using the same three examples will output this on screen:
Total of 3 / 3 files successfully changed
Total of 0 / 3 files failed
Please see 'D:\renamed_2019-09-18 12_07_01.txt' for details.
and the log file will contain:
Successfully renamed 'D:\A Nightmare on Elm Street.mp4' to 'Nightmare on Elm Street (A).mp4'
Successfully renamed 'D:\I, Robot.mkv' to 'Robot (I).mkv'
Successfully renamed 'D:\The Old Man and his dog.mp4' to 'Old Man and his dog (The).mp4'
===========================================================
Total of 3 / 3 files successfully changed
Total of 0 / 3 files failed

How do I correctly pass parameters to ImageMagick from PowerShell?

The following code works perfectly from the command line to combine two TIFF files.
magick -quiet file1.tif file2.tif -compress JPEG filecombined.tif
However, when I try to use it in PowerShell, I am getting many errors from Magick that indicate that it is not getting the correct parameters. My PowerShell code looks something like the following.
$InputFiles = 'files1.tif file2.tif'
$DestinationFile = 'filecombined.tif'
$magick -quiet $InputFiles -compress JPEG $DestinationFile
This gives me errors stating that it cannot find the input files and the message indicates that it thinks it is one filename instead of two. In PowerShell v4, I was able to get it to work by quoting each of the names. Not sure why this helped, but the names did not have spaces. However, I had to upgrade to v5 and this method broke.
I tried using a temporary file to store the input filenames, but this just caused a different error.
$InputFiles = 'files1.tif file2.tif'
$InputFiles | Out-File list.tmp
$DestinationFile = 'filecombined.tif'
$magick -quiet '#list.tmp' -compress JPEG $DestinationFile
magick.exe: unable to open image '#z:ÿþz
Put all the parameters for Magick into an array and use the call (&) operator to execute the command.
$MagickParameters = #( '-quiet' )
$MagickParameters += 'file1.tif'
$MagickParameters += 'file2.tif'
$MagickParameters += #( '-compress', 'JPEG' )
$MagickParameters += 'filecombined.tif'
&'magick' $MagickParameters
This may not be the most efficient use of arrays, but similar methods are possible if performance is a concern.
I had a large collection of EPS images in several folders that I had to convert to PNG. I tested many Image Conversion programs, but most could not handle recursive conversion of Vector to Raster without choking (most displayed errors after processing a limited number of files. Some could not convert recursively through many subfolders). I created the following Powershell script from various sources that solved my problem and made recursive conversion of many files and folders easier. You can modify the file to perform any ImageMagick functions you need.
Have Fun.
# Recursive-Convert-EPS-to-PNG.ps1
$srcfolder = "C:\Temp"
$destfolder = "C:\Temp"
$im_convert_exe = "convert.exe"
$src_filter = "*.eps"
$dest_ext = "png"
$options = "-depth 8 -colorspace gray -threshold 40% -alpha off"
$logfile = "C:\temp\convert.log"
$fp = New-Item -ItemType file $logfile -force
$count=0
foreach ($srcitem in $(Get-ChildItem $srcfolder -include $src_filter -recurse))
{
$srcname = $srcitem.fullname
# Construct the filename and filepath for the output
$partial = $srcitem.FullName.Substring( $srcfolder.Length )
$destname = $destfolder + $partial
$destname= [System.IO.Path]::ChangeExtension( $destname , $dest_ext )
$destpath = [System.IO.Path]::GetDirectoryName( $destname )
# Create the destination path if it does not exist
if (-not (test-path $destpath))
{
New-Item $destpath -type directory | Out-Null
}
# Perform the conversion by calling an external tool
$cmdline = $im_convert_exe + " `"" + $srcname + " `"" + $options + " `"" + $destname + " `""
#echo $cmdline
invoke-expression -command $cmdline
# Get information about the output file
$destitem = Get-item $destname
# Show and record information comparing the input and output files
$info = [string]::Format( "{0} `t {1} `t {2} `t {3} `t {4} `t {5}", $count, $partial, $srcname, $destname, $srcitem.Length , $destitem.Length)
echo $info
Add-Content $fp $info
$count=$count+1
}

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
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.
I was having a very similar problem and after lots of searching found this article. Whilst handle.exe worked when I first tried I did note the -c carries a warning "Closing handles can cause application or system instability"
I am also using get-winevent and it seems to (sometimes) lock the .evtx file being processed. I have written a loop to wait 5 secs an retry. Sometimes it takes up to 2 minutes or the file to be released, I have had one run overnight and it had hundreds of retries.
When I used handle the first time it worked perfectly. I then implemented it into the script and later found it to be looping an "unexplained error". I ended up having to reboot the server to get things working again so removed the handle.exe from the script and back to waiting for the file to be closed.
I can reliably release the file by stopping the script and closing down the powershell ise. As soon as the ISE is closed the file can be deleted without a problem.
Unfortunately I need this script to keep running and not be held up by the file remaining open. I am surprised that have to resort to sysinternals to release the file and that powershell does not offer an easy way to close the file.
I had the same issue as GTEM where closing the handles would cause corruption when processing hundreds of event log files. Eventually Get-WinEvent would not work properly. It would either freeze or give me the same "unexplained error".
So I opened a premier case with MS. They lead me to the actual variable I was storing the Get-WinEvent events in was what was locking the file. I guess it doesn't actually unlock the file if you are still using that variable. So to resolve this I added some code to my script after I transferred the variable to a new variable. You can see the code I added in the 3rd region listed below.
#***************************************************************************
#region *** Get the log entries.
# clear the log entry for each pass
$LogEntry = #()
# Get the vent from the log file and export it to the logentry variable and output to the screen
Get-WinEvent -Path $NewPath -FilterXPath $XPathFilter -ErrorAction SilentlyContinue | Tee-Object -Variable LogEntry
#endregion *** End get the log entries
#***************************************************************************
#***************************************************************************
#region *** This is where I copy it to the new variable for later output.
# if there are any log entries
if ($LogEntry.Count -gt 0) {
# Add the log entries to the log file
$LogEntries += $LogEntry
} # if there are any log entries
#endregion *** End were I copy to the new variable.
#***************************************************************************
#***************************************************************************
#region *** This is where I added code to allow me to remove the file lock.
# Remove the variable to release the evtx file lock
Remove-Variable -Name LogEntry
# Garbage collect to remove any additional memory tied to the file lock.
[GC]::Collect()
# sleep for 1 seconds
Sleep -Seconds 1
#endregion **** Code to remove the file lock.
#***************************************************************************
After this was done, I no longer have to use Handle.exe to close the file anymore.

Pipe all Write-Output to the same Out-File in PowerShell

As the title suggests, how do you make it so all of the Write-Outputs - no matter where they appear - automatically append to your defined log file? That way the script will be nicer to read and it removes a tiny bit of work!
Little example below, id like to see none of the "| Out-File" if possible, yet have them still output to that file!
$Author = 'Max'
$Time = Get-Date -Format "HH:mm:ss.fff"
$Title = "Illegal Software Removal"
$LogName = "Illegal_Remove_$($env:COMPUTERNAME).log"
$Log = "C:\Windows\Logs\Software" + "\" + $LogName
$RemoteLog = "\\Server\Adobe Illegal Software Removal"
Set-PSBreakpoint -Variable Time -Mode Read -Action { $global:Time = Get-Date -format "HH:mm:ss.fff" } | Out-Null
If((Test-Path $Log) -eq $False){ New-Item $Log -ItemType "File" -Force | Out-Null }
Else { $Null }
"[$Time][Startup] $Title : Created by $Author" | Out-File $Log -Append
"[$Time][Startup] Configuring initial variables required before run..." | Out-File $Log -Append
EDIT: This needs to work on PS v2.0, I don't want the output to appear on screen at all only in the log. So I have the same functionality, but the script would look like so...
"[$Time][Startup] $Title : Created by $Author"
"[$Time][Startup] Configuring initial variables required before run..."
You have two options, one is to do the redirection at the point the script is invoked e.g.:
PowerShell.exe -Command "& {c:\myscript.ps1}" > c:\myscript.log
Or you can use the Start-Transcript command to record everything (except exe output) the shell sees. After the script is done call Stop-Transcript.