Get-WinEvent Guide: Query Windows Logs in PowerShell

Get-WinEvent is the PowerShell cmdlet that turns Windows event logs into data you can filter, count and export. If you can write one good Get-WinEvent query you can answer questions like “who failed to log on in the last hour” or “which services were installed today” without clicking through Event Viewer. It is also the fastest way to practice the event IDs from Part 3.

This is Part 4 of 30 in the Blue Team Learning Path. We cover the three ways to filter, how to read event fields, and ten queries a defender actually runs. Everything here is meant for machines you own or administer.

Key takeaways

  • Filter at the source with -FilterHashtable or -FilterXPath. Piping everything into Where-Object is far slower on big logs.
  • Reading the Security log needs an elevated PowerShell session.
  • Event fields are easiest to read by converting the event to XML.
  • Use -Path to analyze an exported .evtx file without touching the live system.

Get-WinEvent Basics

Start by seeing what logs exist on the machine and how big they are:

Get-WinEvent -ListLog * | Sort-Object RecordCount -Descending | Select-Object -First 10 LogName, RecordCount, IsEnabled

Then read the newest events from one log. This is the simplest query you will write:

Get-WinEvent -LogName Security -MaxEvents 5

If this fails with an access error, you are not running as administrator. The Security log is protected. The full syntax is in the Get-WinEvent documentation.

Three Ways to Filter Windows Event Logs in PowerShell

Get-WinEvent offers three filtering styles. All three filter before events are returned to PowerShell, which is why they are fast.

FilterHashtable: the everyday choice

A hashtable of keys is readable and covers most needs. Supported keys include LogName, ProviderName, Path, Id, Level, StartTime, EndTime, UserID and Data. Microsoft explains the rules in the FilterHashtable guide.

Get-WinEvent -FilterHashtable @{
    LogName   = 'Security'
    Id        = 4625
    StartTime = (Get-Date).AddHours(-1)
}

Level uses numbers: 1 is Critical, 2 Error, 3 Warning, 4 Informational and 5 Verbose.

FilterXPath: when you need a field value

FilterHashtable cannot match every field, but an XPath query can. This returns only Remote Desktop style logons (logon type 10):

Get-WinEvent -LogName Security -FilterXPath "*[System[(EventID=4624)] and EventData[Data[@Name='LogonType']='10']]" -MaxEvents 20

FilterXml: the format Event Viewer uses

If you build a custom view in Event Viewer you can copy its XML into -FilterXml. You will rarely write it by hand, but it is useful for reusing a view someone else made.

Reading Event Fields

An event’s Message property is a long formatted string, which is slow to generate and awkward to search. For analysis, convert each event to XML and read the named fields:

$events = Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4625} -MaxEvents 200
$events | ForEach-Object {
    $xml = [xml]$_.ToXml()
    $d = @{}
    $xml.Event.EventData.Data | ForEach-Object { $d[$_.Name] = $_.'#text' }
    [pscustomobject]@{
        Time      = $_.TimeCreated
        User      = $d['TargetUserName']
        Source    = $d['IpAddress']
        SubStatus = $d['SubStatus']
    }
}

The result is a normal object list, so you can add Group-Object, Sort-Object or Export-Csv. Remember that TimeCreated is shown in local time. Call .ToUniversalTime() when you build a timeline from several machines.

Ten Get-WinEvent Queries Defenders Use

Run these on a lab machine to see real output. Event IDs are explained in Windows Security Event IDs.

1. Event volume by ID in the last hour. A quick way to see what the machine is busy with:

Get-WinEvent -FilterHashtable @{LogName='Security'; StartTime=(Get-Date).AddHours(-1)} | Group-Object Id | Sort-Object Count -Descending | Select-Object -First 10

2. Failed logons (4625), using the XML parsing block above and then grouping by Source.

3. Remote Desktop logons (4624, type 10), using the FilterXPath example above.

4. New local accounts (4720):

Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4720} -ErrorAction SilentlyContinue

5. Local group membership changes (4732):

Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4732} -ErrorAction SilentlyContinue

6. New services (7045 in the System log):

Get-WinEvent -FilterHashtable @{LogName='System'; ProviderName='Service Control Manager'; Id=7045} -ErrorAction SilentlyContinue

7. Scheduled tasks created (4698):

Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4698} -ErrorAction SilentlyContinue

8. Cleared logs (1102 in Security, 104 in System). These should be rare and are worth an alert:

Get-WinEvent -FilterHashtable @{LogName='Security'; Id=1102} -ErrorAction SilentlyContinue
Get-WinEvent -FilterHashtable @{LogName='System'; Id=104} -ErrorAction SilentlyContinue

9. Process creation mentioning PowerShell (4688). Narrow by ID first, then search the message. This only works if process creation auditing is enabled:

Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4688} -MaxEvents 500 | Where-Object { $_.Message -match 'powershell' }

10. Errors in the last day across the System log:

Get-WinEvent -FilterHashtable @{LogName='System'; Level=2; StartTime=(Get-Date).AddDays(-1)}

The -ErrorAction SilentlyContinue switch matters. When nothing matches, Get-WinEvent writes a “no events were found” error, which is not a failure and would clutter a script.

Analyzing an Exported Log with -Path

During an investigation you often receive logs as .evtx files instead of access to the live host. Get-WinEvent reads them directly:

Get-WinEvent -Path .\Security.evtx -Oldest | Where-Object { $_.Id -eq 4624 } | Select-Object -First 20

The -Oldest switch returns events in chronological order, which is how most timelines are read. Because you analyze a copy, you do not change the original evidence, a habit worth building early. Part 24 on Windows incident response builds on it.

Querying a Remote Machine

Add -ComputerName to read another system’s logs. This needs the right permissions and the remote event log management firewall rules to be allowed:

Get-WinEvent -ComputerName LAB-DC01 -FilterHashtable @{LogName='Security'; Id=4740} -MaxEvents 20

Use it only on hosts you are authorized to query. For anything beyond a handful of machines, send logs to a central SIEM instead. See SOC analyst tools for where that fits.

Turning a Query into a Reusable Function

Once a query works, save it. Wrapping it in a function means you can rerun it during the next incident without rebuilding it from memory. This one packages the failed-logon parsing from earlier and accepts a time window:

function Get-FailedLogons {
    param([int]$Hours = 1)
    Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4625; StartTime=(Get-Date).AddHours(-$Hours)} -ErrorAction SilentlyContinue | ForEach-Object {
        $x = [xml]$_.ToXml()
        $d = @{}
        $x.Event.EventData.Data | ForEach-Object { $d[$_.Name] = $_.'#text' }
        [pscustomobject]@{
            Time      = $_.TimeCreated
            User      = $d['TargetUserName']
            Source    = $d['IpAddress']
            SubStatus = $d['SubStatus']
        }
    }
}

Call it and group the output by source address to see who is generating the most failures, then export it for a ticket:

Get-FailedLogons -Hours 6 | Group-Object Source | Sort-Object Count -Descending | Select-Object Count, Name
Get-FailedLogons -Hours 6 | Export-Csv .\failed-logons.csv -NoTypeInformation

Keep your functions in a text file or a small repository so your team can reuse them. A shared, reviewed query is worth more than a clever one-liner that lives in one analyst’s shell history.

Querying Sysmon with the Same Skills

Sysmon, which Part 11 installs and tunes, writes to its own log named Microsoft-Windows-Sysmon/Operational. Everything you learned here works against it. For example, Sysmon event 1 records process creation, with far more detail than Security event 4688:

Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-Sysmon/Operational'; Id=1} -MaxEvents 5

That is why this Part comes before the Sysmon Parts: the query skills are the same, and only the log and the event IDs change.

Common Pitfalls

  • Filtering after the fact. Get-WinEvent -LogName Security | Where-Object { $_.Id -eq 4625 } reads and builds every event first. Put the ID in the filter instead.
  • Forgetting elevation. A normal PowerShell window cannot read the Security log.
  • Trusting an empty result. It may mean the audit policy is off or the log rolled over, not that nothing happened.
  • Using the Message text for logic. Message wording depends on the language and version. Parse the XML field names.
  • Missing the time zone. Convert to UTC when comparing machines.
  • Pulling the whole log. Without -MaxEvents or a time range, a busy Security log can take minutes and megabytes of memory.

Where This Goes Next

You now have a repeatable way to ask Windows questions. Practice by generating a failed logon, an account creation and a service install in a lab, then find each one with the queries above. The next Part, Linux log analysis, does the same job with grep and journalctl. Later, Part 11 explains how Sysmon enriches these logs through Sysmon configuration. For a gentler introduction to the logs themselves, return to Windows Event Logs 101.

Frequently Asked Questions

What is the difference between Get-WinEvent and Get-EventLog?

Get-EventLog only reads the classic logs such as Application, Security and System in Windows PowerShell. Get-WinEvent reads those plus the newer Applications and Services logs, and supports far richer filtering, so it is the one to learn.

Why is Get-WinEvent slow on my machine?

Usually because the query returns too much data or filters after the fact. Use -FilterHashtable with an ID and time range, add -MaxEvents, and avoid piping the whole log into Where-Object.

How do I read an exported .evtx file in PowerShell?

Use Get-WinEvent -Path with the file path. Add -Oldest if you want chronological order. The file is read-only analysis, so the live system is not affected.

How do I get event field values such as the username or IP address?

Convert the event with [xml]$event.ToXml() and read the Data elements by their Name attribute, for example TargetUserName or IpAddress, as shown in the parsing example above.

Do I need administrator rights to query event logs?

For the Security log, yes. Many other logs can be read by a standard user, but defenders normally work from an elevated session.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top