How to Track Successful Login Attempts in Batch Script
Monitoring successful logins is essential for tracking user activity, auditing workstation usage, and detecting compromised accounts. If an account is logged in at 3:00 AM from an unusual IP address, it could indicate a security breach. A Batch script can scan the Security log for Event ID 4624 (Successful Logon), allowing you to build automated attendance reports or security alerts that notify you when specific accounts are accessed outside normal patterns.
This guide will explain how to extract successful logon data using PowerShell from a Batch script.
Prerequisites
Successful logon auditing must be enabled in the Windows audit policy. To verify and enable:
:: Check current logon audit policy
auditpol /get /category:"Logon/Logoff"
:: Enable success auditing (requires admin)
auditpol /set /category:"Logon/Logoff" /success:enable
If auditing is not enabled, Event ID 4624 will not be recorded and all methods in this guide will return zero results.
Understanding Logon Types
Windows generates Event ID 4624 for every successful authentication, including hundreds of background service and system logons per hour. Filtering by Logon Type is essential to find actual human logins.
| Type | Name | What It Means | Monitor? |
|---|---|---|---|
| 2 | Interactive | Physical keyboard/console logon | Yes - direct user access |
| 3 | Network | File share, printer, or network resource access | Sometimes - high volume |
| 4 | Batch | Scheduled task execution | Sometimes |
| 5 | Service | Service startup | No - system noise |
| 7 | Unlock | Workstation unlock after lock screen | Yes - user presence |
| 10 | RemoteInteractive | Remote Desktop (RDP) logon | Yes - remote access |
| 11 | CachedInteractive | Domain logon using cached credentials (offline) | Yes - laptop users |
For user activity auditing, focus on Types 2, 7, 10, and 11. Types 3 and 5 generate massive volumes of events for routine system operations and will overwhelm any report.
Method 1: Recent Interactive Logon Report
This method queries Event ID 4624 for human-initiated logons (Types 2, 7, 10, 11) within a configurable time window, displaying the username, source, logon type, and timestamp.
@echo off
setlocal
set "HoursBack=24"
set "MaxResults=25"
:: Verify admin privileges (required for Security log access)
net session >nul 2>&1
if errorlevel 1 (
echo [ERROR] Reading the Security log requires administrator privileges. >&2
echo Right-click and select "Run as administrator." >&2
endlocal
exit /b 1
)
echo [INFO] Searching for interactive logons in the last %HoursBack% hours...
echo --------------------------------------------------
powershell -NoProfile -Command ^
"$events = Get-WinEvent -FilterHashtable @{" ^
" LogName='Security';" ^
" Id=4624;" ^
" StartTime=(Get-Date).AddHours(-%HoursBack%)" ^
"} -MaxEvents 500 -ErrorAction SilentlyContinue;" ^
"if (-not $events) {" ^
" Write-Host 'No logon events found in the last %HoursBack% hours.';" ^
" exit 0" ^
"};" ^
"$interactive = $events | ForEach-Object {" ^
" $xml = [xml]$_.ToXml();" ^
" $data = $xml.Event.EventData.Data;" ^
" $logonType = ($data | Where-Object Name -eq 'LogonType').'#text';" ^
" if ($logonType -in '2','7','10','11') {" ^
" $user = ($data | Where-Object Name -eq 'TargetUserName').'#text';" ^
" $domain = ($data | Where-Object Name -eq 'TargetDomainName').'#text';" ^
" $ip = ($data | Where-Object Name -eq 'IpAddress').'#text';" ^
" if ($user -notmatch '\\$$' -and $user -ne 'SYSTEM' -and $user -ne 'DWM-1' -and $user -ne 'UMFD-0') {" ^
" [PSCustomObject]@{" ^
" Time = $_.TimeCreated.ToString('yyyy-MM-dd HH:mm:ss');" ^
" User = if ($domain -and $domain -ne '-') { \"$domain\\$user\" } else { $user };" ^
" Source = if ($ip -and $ip -ne '-' -and $ip -ne '127.0.0.1') { $ip } else { 'Local' };" ^
" Type = switch ($logonType) {" ^
" '2' { 'Console' }" ^
" '7' { 'Unlock' }" ^
" '10' { 'RDP' }" ^
" '11' { 'Cached' }" ^
" }" ^
" }" ^
" }" ^
" }" ^
"} | Select-Object -First %MaxResults%;" ^
"if ($interactive) {" ^
" Write-Host \"Found $($interactive.Count) interactive logon(s):`n\";" ^
" $interactive | Format-Table -AutoSize -Wrap" ^
"} else {" ^
" Write-Host 'No interactive (human) logons found. Only system logons occurred.'" ^
"}"
echo --------------------------------------------------
endlocal
exit /b 0
Sample output:
Found 6 interactive logon(s):
Time User Source Type
---- ---- ------ ----
2024-05-10 14:32:05 CORP\jsmith 192.168.1.50 RDP
2024-05-10 13:15:22 CORP\jsmith Local Unlock
2024-05-10 09:01:45 CORP\jsmith Local Console
2024-05-10 08:55:30 CORP\admin.backup 10.0.0.100 RDP
2024-05-09 23:47:12 CORP\svc_deploy 192.168.1.200 RDP
2024-05-09 18:30:08 CORP\jsmith Local Console
Why system accounts are filtered out:
Windows constantly logs successful logons for internal accounts like SYSTEM, DWM-1 (Desktop Window Manager), UMFD-0 (User-Mode Font Driver), and computer accounts (ending with $). These are routine system operations, not human activity. The filter $user -notmatch '\\$$' removes computer account logons, and the explicit exclusions remove well-known system accounts.
Why XML parsing instead of Properties[N]:
The original used $_.Properties[5].Value and $_.Properties[8].Value for username and logon type. These numeric indices vary by logon type and Windows version. Parsing the event XML by field name (TargetUserName, LogonType, IpAddress) is reliable regardless of schema version.
Method 2: User-Specific Logon History
When investigating a specific user account, for example, after a suspected compromise, this method shows their complete logon history including the source IP for RDP sessions.
@echo off
setlocal
set "TargetUser=%~1"
set "DaysBack=7"
if "%TargetUser%"=="" (
echo Usage: %~nx0 ^<username^> [days_back]
echo.
echo Examples:
echo %~nx0 jsmith
echo %~nx0 administrator 30
endlocal
exit /b 1
)
if not "%~2"=="" set "DaysBack=%~2"
net session >nul 2>&1
if errorlevel 1 (
echo [ERROR] Administrator privileges required. >&2
endlocal
exit /b 1
)
echo [INFO] Logon history for "%TargetUser%" in the last %DaysBack% days...
echo --------------------------------------------------
powershell -NoProfile -Command ^
"$events = Get-WinEvent -FilterHashtable @{" ^
" LogName='Security';" ^
" Id=4624;" ^
" StartTime=(Get-Date).AddDays(-%DaysBack%)" ^
"} -ErrorAction SilentlyContinue;" ^
"if (-not $events) {" ^
" Write-Host 'No logon events found in the specified period.';" ^
" exit 0" ^
"};" ^
"$userEvents = $events | ForEach-Object {" ^
" $xml = [xml]$_.ToXml();" ^
" $data = $xml.Event.EventData.Data;" ^
" $user = ($data | Where-Object Name -eq 'TargetUserName').'#text';" ^
" if ($user -eq '%TargetUser%') {" ^
" $logonType = ($data | Where-Object Name -eq 'LogonType').'#text';" ^
" $ip = ($data | Where-Object Name -eq 'IpAddress').'#text';" ^
" $workstation = ($data | Where-Object Name -eq 'WorkstationName').'#text';" ^
" [PSCustomObject]@{" ^
" Time = $_.TimeCreated.ToString('yyyy-MM-dd HH:mm:ss');" ^
" LogonType = switch ($logonType) {" ^
" '2' { 'Console' } '3' { 'Network' } '7' { 'Unlock' }" ^
" '10' { 'RDP' } '11' { 'Cached' } default { $logonType }" ^
" };" ^
" SourceIP = if ($ip -and $ip -ne '-') { $ip } else { 'Local' };" ^
" Workstation = if ($workstation) { $workstation } else { '-' }" ^
" }" ^
" }" ^
"};" ^
"if ($userEvents) {" ^
" Write-Host \"Found $($userEvents.Count) logon(s) for '%TargetUser%':`n\";" ^
" $userEvents | Format-Table -AutoSize -Wrap" ^
"} else {" ^
" Write-Host 'No logons found for user ''%TargetUser%'' in the last %DaysBack% days.'" ^
"}"
endlocal
exit /b 0
What to look for:
- Logons at unusual hours (2–5 AM on a weekday): May indicate a compromised account.
- RDP from unexpected IPs: If the user normally connects from
192.168.1.xbut a logon comes from85.214.x.x, investigate immediately. - Multiple logon types in rapid succession: A Console logon followed immediately by Network logons is normal. An RDP logon from an unknown IP followed by Network logons to file shares may indicate lateral movement.
Method 3: Logon Activity CSV Export for Compliance
For compliance auditing (SOX, HIPAA, PCI-DSS), you need a permanent record of who accessed each system. This method exports interactive logons to a CSV file with all relevant fields.
@echo off
setlocal
set "DaysBack=30"
set "OutFile=%~dp0logon_audit_%COMPUTERNAME%.csv"
net session >nul 2>&1
if errorlevel 1 (
echo [ERROR] Administrator privileges required. >&2
endlocal
exit /b 1
)
echo [INFO] Exporting %DaysBack%-day logon history to CSV...
powershell -NoProfile -Command ^
"$events = Get-WinEvent -FilterHashtable @{" ^
" LogName='Security';" ^
" Id=4624;" ^
" StartTime=(Get-Date).AddDays(-%DaysBack%)" ^
"} -ErrorAction SilentlyContinue;" ^
"if (-not $events) {" ^
" Write-Host 'No events found.';" ^
" exit 0" ^
"};" ^
"$report = $events | ForEach-Object {" ^
" $xml = [xml]$_.ToXml();" ^
" $data = $xml.Event.EventData.Data;" ^
" $logonType = ($data | Where-Object Name -eq 'LogonType').'#text';" ^
" if ($logonType -in '2','7','10','11') {" ^
" $user = ($data | Where-Object Name -eq 'TargetUserName').'#text';" ^
" if ($user -notmatch '\\$$' -and $user -ne 'SYSTEM') {" ^
" [PSCustomObject]@{" ^
" Timestamp = $_.TimeCreated.ToString('yyyy-MM-dd HH:mm:ss');" ^
" Computer = $env:COMPUTERNAME;" ^
" User = ($data | Where-Object Name -eq 'TargetDomainName').'#text' + '\\' + $user;" ^
" LogonType = switch ($logonType) {" ^
" '2' { 'Console' } '7' { 'Unlock' } '10' { 'RDP' } '11' { 'Cached' }" ^
" };" ^
" SourceIP = ($data | Where-Object Name -eq 'IpAddress').'#text';" ^
" Workstation = ($data | Where-Object Name -eq 'WorkstationName').'#text'" ^
" }" ^
" }" ^
" }" ^
"};" ^
"if ($report) {" ^
" $report | Export-Csv -Path '%OutFile%' -NoTypeInformation;" ^
" Write-Host \"Exported $($report.Count) interactive logon(s) to: %OutFile%\"" ^
"} else {" ^
" Write-Host 'No interactive logons found in the specified period.'" ^
"}"
endlocal
exit /b 0
Why this matters for compliance:
Event Logs are circular: when they fill up, the oldest events are overwritten. A busy server may overwrite logon events within days. This export creates a permanent record that survives log rotation and can be archived for the retention period required by your compliance framework.
Scheduling for continuous collection:
Run this script weekly or monthly as a scheduled task. Each run exports the period since the last run, and the CSV files accumulate as a permanent audit trail.
Method 4: Startup Logon Recorder
For shared workstations or kiosks, a lightweight script that runs at logon (via Task Scheduler or Group Policy logon script) records each session start to a shared CSV.
@echo off
setlocal
set "LogFile=\\Server\Audit\workstation_logons.csv"
:: Write header if file is new
if not exist "%LogFile%" (
echo "Timestamp","Computer","Username","SessionType" > "%LogFile%" 2>nul
)
:: Determine session type
set "SessionType=Console"
if defined SESSIONNAME (
echo %SESSIONNAME% | findstr /i "RDP" >nul && set "SessionType=RDP"
)
:: Generate locale-independent timestamp
for /f "delims=" %%t in (
'powershell -NoProfile -Command "Get-Date -Format ''yyyy-MM-dd HH:mm:ss''"'
) do set "Timestamp=%%t"
:: Append the logon record
echo "%Timestamp%","%COMPUTERNAME%","%USERNAME%","%SessionType%" >> "%LogFile%" 2>nul
if errorlevel 1 (
echo [WARNING] Could not write to audit log: %LogFile% >&2
)
endlocal
exit /b 0
Why this approach complements Event Log scanning:
- No admin rights required: Runs in the user's own session.
- Works without audit policy configuration: Does not depend on Windows security auditing being enabled.
- Centralized logging: Writes to a network share, consolidating logins from all workstations in one file.
- Limitation: Only records logons when the script runs. It cannot detect past logons or logons where the script failed to execute.
How to Avoid Common Errors
Wrong Way: Counting Every Event ID 4624
Windows generates hundreds of 4624 events per hour for system services, scheduled tasks, and internal authentication. Including all of them in a "user logon" report produces a massive, misleading dataset.
Correct Way: Filter by Logon Type. Only Types 2 (Console), 7 (Unlock), 10 (RDP), and 11 (Cached) represent human-initiated sessions. All methods in this guide apply this filter.
Wrong Way: Extracting Fields by Numeric Index
# FRAGILE: index positions vary by logon type and Windows version
$user = $event.Properties[5].Value
$logonType = $event.Properties[8].Value
Correct Way: Parse the event XML and select by field name:
$xml = [xml]$event.ToXml()
$data = $xml.Event.EventData.Data
$user = ($data | Where-Object Name -eq 'TargetUserName').'#text'
Problem: System Account Noise
Even after filtering by Logon Type, accounts like DWM-1, UMFD-0, and computer accounts (SERVERNAME$) appear in Type 2 events. These are system components, not humans.
Solution: Exclude accounts matching \\$$ (computer accounts) and known system accounts by name, as shown in Method 1.
Problem: Security Log Access Denied
The Security log requires administrator privileges. A standard user will receive zero results without any error message from Get-WinEvent -ErrorAction SilentlyContinue.
Solution: Check admin rights at startup and fail with a clear message:
net session >nul 2>&1
if errorlevel 1 (
echo [ERROR] Administrator privileges required. >&2
exit /b 1
)
Best Practices and Rules
1. Focus on Interactive Logon Types
Filter for Types 2, 7, 10, and 11. Type 3 (Network) and Type 5 (Service) generate enormous volumes and represent routine system operations, not user sessions.
2. Always Use Time Windows
Never query the entire Security log history. Use -StartTime with -FilterHashtable to limit the query to the relevant period. On busy servers, the full log can contain millions of events.
3. Capture the Source IP for RDP Sessions
RDP logons (Type 10) include the source IP address. This is the most critical field for detecting unauthorized remote access. Always include it in your reports.
4. Export to Permanent Storage
Event Logs are circular and overwrite old entries. For compliance and forensic purposes, export logon data to CSV files or a database at regular intervals so the audit trail survives log rotation.
5. Correlate with Failed Logon Data
A successful logon (4624) immediately following a series of failed logons (4625) from the same IP may indicate a brute-force attack that succeeded. Cross-reference your successful logon reports with failed logon data (see our guide on tracking failed login attempts) for a complete security picture.
Conclusions
Tracking successful login attempts provides a clear audit trail of who accessed your systems, when, from where, and how. By filtering for interactive logon types, parsing events with reliable XML extraction, and exporting to permanent storage, you transform raw Security log data into a powerful tool for security monitoring, compliance auditing, and incident investigation. This capability is essential for any administrator maintaining a secure and accountable Windows environment.