Repository navigation
Detection and Analysis
How WinSecHunter turns event streams into verdicts β algorithms, thresholds, tuning, and how to read each result.
- The analysis pipeline
- Test-WinSecBruteForce: patterns & algorithms
- Tuning thresholds
- Session reconstruction
- Timelines & the success-after-failure indicator
- IP attribution β dedicated page: Threat Intel & Attribution
All analysis cmdlets are pure and pipeline-native β they consume
WinSecEvent objects and run on any OS. Collect once, analyze many ways:
flowchart LR
C["Get-WinSecEventByIp<br/>π’ Windows"] --> E["WinSecEvent[]<br/>(typed, decoded, UTC-sorted)"]
E --> BF["Test-WinSecBruteForce"]
E --> TL["Get-WinSecIpTimeline"]
E --> LS["Get-WinSecLogonSession"]
E --> AT["Resolve-WinSecIpAttribution"]
BF --> X["Export-WinSecInvestigation"]
TL --> X
LS --> X
AT --> X
style E fill:#4da3ff,color:#000
style X fill:#a06bff,color:#fff
Because events are typed objects, you can also round-trip them:
Export-Clixml on the collection VM β Import-Clixml on your analysis box
(no Windows needed for anything below).
| Bucket | Event IDs | Rule |
|---|---|---|
| Failures | 4625, 4771, 4776 (non-zero status), RCM 1148, 5632/5633, 4649 | Outcome = Failure |
| Successes | 4624, RCM 1149, LSM 21/25, 4768/4769 (0x0), 4776 (0x0) | Outcome = Success |
| Lockouts | 4740 | by ID |
Note
4776/4768/4769 appear in both lists depending on their status code β the
parser resolves outcome per record (0x0 β Success), which is exactly the
decoding the legacy script lacked.
| Pattern | Grouping key | Trigger (defaults) | Severity | ATT&CK |
|---|---|---|---|---|
BruteForce |
(source IP, account) | β₯ 5 failures inside 10 min sliding window | High | T1110.001/.003 |
PasswordSpraying |
(source IP) | β₯ 5 distinct accounts failed inside window | High | T1110.003 |
CredentialStuffing |
(account) | β₯ 3 distinct sources failed inside window | High | T1110.001 |
LockoutStorm |
global | β₯ 3 lockouts (4740) in scope | Medium | T1110 |
BruteForceWithSuccess |
any of the above | a success for same source/account within 30 min after burst start | Critical | T1078 |
The burst search is a proper sliding window maximum (two-pointer over UTC-sorted events), not a fixed-bucket count β a 5-attempt burst straddling a bucket boundary still triggers.
$findings = $events | Test-WinSecBruteForce
$findings | Format-ListPattern : BruteForceWithSuccess
Severity : Critical
TargetAccount : CORP\svc_backup
SourceIp : 203.0.113.7
SourceHosts : {VPN01}
FailureCount : 47
SuccessCount : 1
DistinctAccounts : 2
AttemptsPerMinute : 6.3
FirstSeenUtc : 2026-10-04 03:12:44Z
LastSeenUtc : 2026-10-04 03:41:02Z
SucceededAfterFailures : True
Description : 47 failed logons from 203.0.113.7 against CORP\svc_backup within 10 min
(6.3/min). SUCCESS observed after failures (VPN01 event 4624 at
2026-10-04 03:41:02Z) β assume compromise: isolate host, reset
credentials, review sessions.
Evidence : {48 Γ WinSecEvent}
Triage playbook for SucceededAfterFailures = True:
-
Get-WinSecLogonSessionβ find the session, itsIsAdminflag and children (4648 pivots). - Re-sweep the account (not the IP):
Get-WinSecEventByIpfinds IPs, but pivot destinations may use new sources β hunt theLogonIdchain and 4648TargetServerName. - Check
DefenseEvasionprofile on the victim host (1102/104/4719) β did they blind it? - Preserve:
Export-WinSecInvestigation -Format All.
# Per-invocation
$events | Test-WinSecBruteForce -FailureThreshold 10 -WindowMinutes 5 -SprayAccountThreshold 8
# Persistent defaults
Set-WinSecHunterOption -Options @{
BruteForceFailureThreshold = 10
BruteForceWindowMinutes = 5
BruteForceSprayAccountThreshold = 8
}| Scenario | Suggested tuning |
|---|---|
| Service accounts with flaky passwords (false positives) | raise FailureThreshold, or exclude the account from collected events upstream |
| Slow-and-low attackers (1 attempt/min) | widen WindowMinutes to 30β60, lower threshold |
| Large-scale spraying from botnets | stuffing rule catches it (β₯3 sources/account); widen window |
| Lockout noise from one broken laptop | raise LockoutThreshold
|
Important
Thresholds trade sensitivity against noise β but never raise
SuccessWindowMinutes reasoning away a success: any success after failures
for the same source/account deserves a human decision, which is why the
finding escalates to Critical and lists the exact success event.
Get-WinSecLogonSession chains events by normalized LogonId per computer:
flowchart LR
A["4624<br/>LogonId 0x12ab<br/>(session opens)"] --> B["4672<br/>same LogonId<br/>β IsAdmin = true"]
B --> C["4648<br/>SubjectLogonId 0x12ab<br/>TargetLogonId 0x9f<br/>β child pivot"]
C --> D["4634 / 4647<br/>same LogonId<br/>β Ended, duration"]
style A fill:#41d99b,color:#000
style B fill:#ffb347,color:#000
style C fill:#4da3ff,color:#000
| Property | Why you care |
|---|---|
IsAdmin |
4672 seen β attacker had an admin token on this host |
Ended = false |
No logoff recorded β the session may still be live. With -ActiveOnly, this is your "still inside" list |
ChildLogonIds |
4648 explicit-credential logons spawned from this session = pivot graph edges |
LogonType |
3 = SMB/network, 10 = RDP, 9 = runas /netonly (token games), 2 = console (physical/KVM) |
AuthPackage |
NTLM where Kerberos is expected β pass-the-hash / downgrade smell |
GetDuration() |
dwell time per session |
LogonId normalization: 0x12AB, 0x0000000012ab, 12ab all collapse to 12ab β
chains never break on formatting.
$events | Get-WinSecLogonSession |
Where-Object IsAdmin |
Sort-Object StartUtc |
Format-Table LogonId, Account, Computer, LogonType, SourceIp, StartUtc, IsAdmin, EndedGet-WinSecIpTimeline emits one summary object first, then typed buckets:
$t = $events | Get-WinSecIpTimeline -AsciiChart
$summary = $t[0] # WinSecTimelineSummary
$buckets = $t | Select-Object -Skip 1 # WinSecTimelineBucket[]
$summary.SuccessAfterFailure # compromise indicators, independent of thresholds
$summary.LogonTypes # {LogonType, Count, Meaning}
$summary.TopAccounts / TopHosts / EventBreakdown / AttackIds
$summary.AsciiChart # console-safe stringsWinSecHunter timeline β 214 events, 2026-10-04 03:00:00Z β 2026-10-04 09:00:00Z (UTC), bucket 01:00:00
10-04 03:00 | ββββββββββββββββββββββββββββββ 118 4625x94, 4771x18, 4740x6
10-04 04:00 | βββββ 12 4624x2, 4672x2, 4648x1
10-04 05:00 | 0
β¦
!! SUCCESS AFTER FAILURE (possible compromise):
CORP\alice on VPN01: 94 failure(s) from 2026-10-04 03:12:44Z, then event 4624 at 2026-10-04 03:41:02Z
Notes:
- Buckets auto-size (1 h / 6 h / 1 d) unless
-BucketSizeis given; all bucket math is UTC. - The success-after-failure indicator uses a 1-hour lookahead per
(computer, account) pair β it is threshold-independent, unlike
Test-WinSecBruteForce; both should agree on real compromises. - Buckets feed the SVG histogram in the exported HTML report automatically when
passed to
Export-WinSecInvestigation -Timeline.
A complete analysis pass in one pipeline block:
$events = Get-WinSecEventByIp 203.0.113.7 -ComputerName $hosts -Credential $cred -LastDays 2 -PassHostStatus
$findings = $events | Test-WinSecBruteForce
$sessions = $events | Get-WinSecLogonSession
$timeline = $events | Get-WinSecIpTimeline
$attrib = $events | ForEach-Object SourceIp | Where-Object { $_ } | Sort-Object -Unique |
Resolve-WinSecIpAttribution
$coverage = Get-WinSecLogCoverage -ComputerName $hosts -Credential $cred -CoveringFrom (Get-Date).AddDays(-2)
Export-WinSecInvestigation -Event $events -Finding $findings -Session $sessions `
-Timeline $timeline -Attribution $attrib -Coverage $coverage -Format AllThe reporting layer raises the case verdict above the individual findings when a success
follows sustained failures from the same source (at least five failures). The finding pattern
BruteForceWithSuccess does the same, but the check does not depend on a detection threshold
being met β a slow attack with a successful logon is still a compromise.
| Situation | Verdict in the report |
|---|---|
| β₯5 failures and a later success from the same IP | Credential compromise β success observed after failures |
finding BruteForce
|
Brute-force attack |
finding PasswordSpraying
|
Password spraying |
| events, no threshold exceeded | Suspicious authentication activity |
Per-channel status. Since 2.1.0 every channel is queried separately. When analysing a
sweep, read WinSecHostResult.LogDetails: a Skipped Security channel means "no visibility",
not "no activity", and must be stated in the report's limitations.
Next: Evidence & Reporting β
WinSecHunter v2.1.0 Β· PolyForm Noncommercial 1.0.0 Β· derivative of Get-SecurityEventsByIP.ps1 by Mikhail Deynekin
Repository Β· Changelog Β· Security policy Β· Contributing
π Home
π― Problem Domain π Getting Started π§ Architecture
π Cmdlet Reference π Detection and Analysis π Remote Collection π°οΈ Threat Intel and Attribution π‘οΈ Audit Policy Guide
π Reporting and Abuse π§Ύ Evidence and Reporting π Reference Tables
π§ Configuration π©Ί Troubleshooting and FAQ π§βπ» Development πΊοΈ Roadmap