Repository navigation
Remote Collection
Sweeping the estate over WinRM, using CIM sessions, and working acquired EVTX images β including the accountability model that makes fleet sweeps defensible.
- Transport model
- WinRM prerequisites
- Credentials & authentication
- Parallelism & throttling
- Per-host status model
- CimSession transport
- Offline EVTX forensics
- Remote troubleshooting
flowchart TD
subgraph JUMP["Collection host (WinSecHunter)"]
Q["Build QueryList XML once<br/>(IDs + UTC window + IP gate)"]
P["ForEach-Object -Parallel<br/>-ThrottleLimit N"]
end
Q --> P
P -- "Invoke-Command + retry" --> H1["π₯οΈ SRV1<br/>Get-WinEvent -FilterXml"]
P -- "Invoke-Command + retry" --> H2["π₯οΈ SRV2"]
P -- "Invoke-Command + retry" --> H3["π₯οΈ DC01"]
H1 --> R["Hashtable payloads<br/>{Xml, Computer, RecordId, TimeUtc*, LogName}"]
H2 --> R
H3 --> R
R --> N["Normalize locally:<br/>parse β match β dedup β sort"]
Design decisions worth knowing:
- The query is built once and executed identically everywhere β no per-host query drift.
- Remote blocks return plain hashtables, not typed objects: zero module dependencies on targets, CLIXML-safe serialization, parsing happens centrally.
-
TimeUtcis computed on the source machine (TimeCreated.ToUniversalTime()inside the remote block) β correct even when targets span time zones. -
Retry: transport failures retry once (configurable
-RetryCount 0..5, 1.5 s backoff) before a host is markedFailed. -
-CimSessionruns sequentially by design β CIM handles are not safe to fan out across runspaces.
On each target (one-time, elevated):
Enable-PSRemoting -Force
# opens TCP 5985 (HTTP) / 5986 (HTTPS after cert setup),
# creates the firewall rule 'Windows Remote Management (HTTP-In)'| Environment | What works out of the box |
|---|---|
| Same AD domain | Kerberos mutual auth, no extra config |
| Cross-domain / forest trust | Kerberos with proper SPNs, else Negotiate |
| Workgroup / external IPs | HTTPS listener + certificate, or -Authentication Basic over TLS, or CredSSP ( |
| Azure VMs / cloud |
Enable-PSRemoting + NSG allow 5985/5986 from the jump box |
The querying identity needs, on the target: membership in local
Administrators or the SeSecurityPrivilege user right ("Manage auditing
and security log") for the Security channel. Service accounts dedicated to IR
sweeps are the clean answer β see the group-policy route in
Audit Policy Guide.
Warning
CredSSP (-Authentication Credssp) delegates your credentials to the
target β a compromised target harvests them. Enable only for the specific
jumpβtarget pairs that truly need it, prefer Kerberos/HTTPS, and disable
afterwards (Disable-WSManCredSSP).
$cred = Get-Credential # or a gMSA-backed PSCredential from your vault
Get-WinSecEventByIp 203.0.113.7 -ComputerName $hosts -Credential $cred `
-Authentication Negotiate -SessionOption (New-PSSessionOption -NoMachineProfile -OpenTimeout 30000)-
-Credentialis passed toInvoke-Commandand never serialized to disk by WinSecHunter; remote blocks receive no credential material. -
-Authenticationmirrors WinRM values:BasicΒ·NegotiateΒ·NegotiateWithImplicitCredentialΒ·CredsspΒ·DigestΒ·Default. -
-SessionOptionaccepts anyNew-PSSessionOptionobject (timeouts, proxy, SkipCN checks for HTTPS labs).
# 200 hosts, 32 concurrent, unlimited records, whole retained history
Get-WinSecEventByIp 203.0.113.7 -ComputerName (Get-Content hosts.txt) -Credential $cred `
-ThrottleLimit 32 -MaxEvents 0 -PassHostStatus| Knob | Default | Guidance |
|---|---|---|
-ThrottleLimit |
8 (config DefaultThrottleLimit) |
16β64 for LAN jump boxes; each slot holds one WinRM connection + result set |
-MaxEvents |
5000/host (config) |
0 = unlimited β pair with a tight time window, not with -Profile All on DCs |
-RetryCount |
1 |
flaky WAN links: 2β3 |
| Progress | automatic | parallel Write-Progress aggregates: host β status β event count |
Memory rule of thumb: budget ~2 KB per event object (more with
-IncludeRawXml). A 200-host sweep returning 50k events β 100β200 MB β
comfortable; -DeepScan -Profile All -MaxEvents 0 across a DC fleet is how
you find out what swap is for.
With -PassHostStatus, the output stream starts with one WinSecHostResult
per target, then the events:
$out = Get-WinSecEventByIp 203.0.113.7 -ComputerName $hosts -PassHostStatus
$status = $out | Where-Object { $_.GetType().Name -eq 'WinSecHostResult' }
$events = $out | Where-Object { $_.GetType().Name -eq 'WinSecEvent' }
$status | Group-Object Status | Format-Table Name, Count
$status | Where-Object Status -eq 'Failed' | Format-Table Computer, ErrorMessage| Status | Meaning | Typical cause |
|---|---|---|
Completed |
query ran, β₯1 event | β |
NoEvents |
query ran, 0 matches | genuinely quiet host (a result, not a failure) |
Skipped |
channel not installed / not accessible by design | Sysmon absent, RDP logs on a DC, access denied to an optional log |
Failed |
transport or query error | WinRM off, DNS, auth, firewall β full message in ErrorMessage
|
Important
A sweep report should state all four counts. "We queried 200 hosts" is
meaningless without "β¦187 completed, 9 failed (list), 4 skipped channels".
Failed hosts are also emitted as non-terminating errors, so
-ErrorAction Stop turns them into hard stops and
-ErrorVariable ev captures them.
When your environment standardizes on CIM sessions (proxy configs, custom
CimSessionOption, existing session pools):
$sessions = New-CimSession -ComputerName SRV1, SRV2 -Credential $cred
Get-WinSecEventByIp 203.0.113.7 -CimSession $sessions -LastDays 2 -PassHostStatus
$sessions | Remove-CimSessionExecuted via Invoke-Command -CimSession (sequential). Status model identical;
Computer labels come from the session objects.
Acquired images need no WinRM, no admin rights on any remote system β just read access to the files and a Windows analysis host:
# Whole case directory, deep scan, keep raw XML for the evidence bundle
$events = Get-WinSecEventByIp 203.0.113.7 -Path 'E:\case042\*.evtx' `
-DeepScan -IncludeRawXml -MaxEvents 0
# Coverage of the images themselves (retention bounds of what was acquired)
Get-WinSecLogCoverage -Path 'E:\case042\*.evtx' |
Format-Table LogName, RecordCount, OldestRecordUtc, NewestRecordUtcAcquisition tips:
- Copy live
.evtxwithwevtutil epl Security C:\ir\Security-%COMPUTERNAME%.evtxorCopy-ItemfromC:\Windows\System32\winevt\Logs\(VSS for locked files). -
Get-WinSecLogCoverage -CoveringFrom <window>on the live host tells you whether the acquisition even contains the window you care about. - File-mode queries rebuild the XPath with each file's path (
-PathOverrideinternally), so multi-channel QueryLists work per file. - Timestamps: EVTX
SystemTimeis UTC in the XML βTimeUtcis exact;TimeCreatedrenders in the analysis machine's zone.
| Symptom | First check | Fix |
|---|---|---|
Connecting to remote server failed |
Test-WSMan -ComputerName HOST |
Enable-PSRemoting, firewall 5985/5986, DNS |
Access is denied on Security log |
querying identity rights | local Administrators or SeSecurityPrivilege on target |
| Kerberos errors cross-domain | SPNs / trust |
-Authentication Negotiate, or HTTPS listener |
| Timeouts on WAN | default open timeout | -SessionOption (New-PSSessionOption -OpenTimeout 60000) |
| Host has no Sysmon/RDP channels |
Skipped status |
expected β not an error; note it in the report |
| Partial results, log wrapped | Get-WinSecLogCoverage -ComputerName β¦ |
retention gap warning; state the bound in the report |
| CredSSP rejected | both sides must enable |
Enable-WSManCredSSP -Role Client -DelegateComputer HOST (+ Server role on target) β |
A remote host is queried channel by channel. The host result carries LogDetails:
$status = Get-WinSecEventByIp 203.0.113.7 -ComputerName SRV1, SRV2 -LastHours 12 -PassHostStatus
$status | Format-Table Computer, Status, EventsFound, ErrorMessage
$status | ForEach-Object { $_.LogDetails } | Format-Table LogName, Status, EventsFound, Error| Channel status | Meaning | What to do |
|---|---|---|
Completed |
records returned | nothing |
NoEvents |
query ran, no match | widen the window or use -DeepScan
|
Skipped |
channel absent, disabled or access denied | expected on servers without the role; elevate for Security |
Failed |
real error | read Error; check WinRM, permissions, query |
A missing Sysmon or RDP channel no longer aborts the host, and -PassHostStatus is emitted
before any diagnostic message, so statuses are never lost behind an error.
Next: Threat Intel & Attribution β Β· Related: Troubleshooting & FAQ
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