BitBack ← wszystkie skrypty

/logins

Szczegółowy przegląd logowań, udanych i nieudanych, z dziennika zdarzeń Windows.
Przy każdej próbie mówi, co ją wywołało: człowiek przy klawiaturze, człowiek w okienku
programu, usługa albo zadanie ze starym hasłem, czy urządzenie z sieci - i podsumowuje,
czy po serii nieudanych ktoś w końcu wszedł. Zawęża wynik do konkretnego konta, źródła,
typu logowania i okna czasu. Tylko odczyt - niczego nie wysyła.
Zakres i filtry przyjmuje w postaci, którą wypisuje skrypt fla.
Uruchomienie w PowerShell
irm https://dev.bitback.pl/logins | iex
45.8 kB · 1012 linii · zaktualizowano 2026-07-30
Poniżej pełne, niezmienione źródło. Przeczytaj przed uruchomieniem.
  1. # ===
  2. # Szczegółowy przegląd logowań, udanych i nieudanych, z dziennika zdarzeń Windows.
  3. # Przy każdej próbie mówi, co ją wywołało: człowiek przy klawiaturze, człowiek w okienku
  4. # programu, usługa albo zadanie ze starym hasłem, czy urządzenie z sieci - i podsumowuje,
  5. # czy po serii nieudanych ktoś w końcu wszedł. Zawęża wynik do konkretnego konta, źródła,
  6. # typu logowania i okna czasu. Tylko odczyt - niczego nie wysyła.
  7. # Zakres i filtry przyjmuje w postaci, którą wypisuje skrypt fla.
  8. # ===
  9. # ListLogins
  10. # Usage: irm https://dev.bitback.pl/logins | iex
  11. #
  12. # Okno czasu (ten sam zapis co w fla, domyślnie 168h):
  13. # $hback = 24; irm https://dev.bitback.pl/logins | iex
  14. # $hwin = '2026-07-28T13:40:00..2026-07-28T14:10:00'; irm https://dev.bitback.pl/logins | iex
  15. #
  16. # Zawężanie:
  17. # $user = 'DOMENA\User' konto, w pełnej postaci albo sam login
  18. # $source = '127.0.0.1' adres albo nazwa stacji
  19. # $ltype = 2 typ logowania, liczba albo skrót (Kbd, RDP)
  20. # $status = '0xC000006A' kod niepowodzenia, dotyczy tylko zdarzeń nieudanych
  21. # $failedonly = $true pomija logowania udane
  22. #
  23. # Widok:
  24. # $detail = $true dokłada proces logowania, proces wywołujący i konto inicjujące
  25. # $count = 20 ile wpisów wypisać (domyślnie 10)
  26. # $group = $true skleja powtórzenia; $interval steruje progiem w minutach
  27. #
  28. # Przykład celowania w serię nieudanych prób z jednego konta:
  29. # $hwin='2026-07-28T13:46:00..2026-07-28T13:50:00'; $user='DOMENA\User'; $detail=$true; irm https://dev.bitback.pl/logins | iex
  30.  
  31. # ============================================================
  32. # DEFAULTS - override by setting variables before iex
  33. # ============================================================
  34. if (-not (Test-Path variable:count)) { $count = 10 }
  35. if (-not (Test-Path variable:group)) { $group = $false }
  36. if (-not (Test-Path variable:interval)) { $interval = 3 } # minutes - grouping threshold
  37. if (-not (Test-Path variable:failedonly)) { $failedonly = $false }
  38. # Filtry celowania - wypelniane komenda podpowiadana przez fla.
  39. if (-not (Test-Path variable:user)) { $user = $null } # konto: DOMENA\User albo sam User
  40. if (-not (Test-Path variable:source)) { $source = $null } # IP albo nazwa stacji, dokladnie
  41. if (-not (Test-Path variable:ltype)) { $ltype = $null } # typ logowania: 2, 10 albo 'RDP'
  42. if (-not (Test-Path variable:status)) { $status = $null } # SubStatus, np. 0xC000006A
  43. if (-not (Test-Path variable:detail)) { $detail = $false } # pola rozstrzygajace per wpis
  44. # Zmienne sa inicjalizowane jawnie, bo skrypt biegnie w sesji technika: pod
  45. # 'Set-StrictMode -Version 2.0' samo odwolanie do niezdefiniowanej rzuca wyjatkiem.
  46. if (-not (Test-Path variable:hwin)) { $hwin = $null }
  47. if (-not (Test-Path variable:hback)) { $hback = $null }
  48.  
  49. # ============================================================
  50. # OKNO CZASOWE
  51. # ============================================================
  52. # Nazwy zmiennych, format czasu i domkniecie konca sekundy sa tu takie same jak w fla,
  53. # dzieki czemu zakres wypisany przez fla dziala po wklejeniu tutaj bez przerabiania.
  54. # Domyslne okno to 168h zamiast 24h jak w fla, bo logins sluzy do przegladania historii,
  55. # a fla do biezacego triage.
  56. $TimeFmt = 'yyyy-MM-ddTHH:mm:ss'
  57.  
  58. # Wydzielone do funkcji, zeby dalo sie to sprawdzic testem bez uruchamiania calego
  59. # skryptu - w szczegolnosci sprawdzic, ze zakres wypisany przez fla jest tu przyjmowany.
  60. function Resolve-TimeWindow {
  61. param([object]$Win, [object]$Back, [datetime]$Now, [int]$DefaultHours = 168)
  62.  
  63. $TimeFmt = 'yyyy-MM-ddTHH:mm:ss'
  64. if ($null -ne $Win -and "$Win".Trim() -ne '') {
  65. $rawWin = "$Win".Trim().Trim("'", '"')
  66. $winParts = $rawWin -split '\.\.'
  67. $ci = [Globalization.CultureInfo]::InvariantCulture
  68. $winFrom = [datetime]::MinValue
  69. $winTo = [datetime]::MinValue
  70. if ($winParts.Count -eq 2 -and
  71. [datetime]::TryParseExact($winParts[0].Trim(), $TimeFmt, $ci, [Globalization.DateTimeStyles]::None, [ref]$winFrom) -and
  72. [datetime]::TryParseExact($winParts[1].Trim(), $TimeFmt, $ci, [Globalization.DateTimeStyles]::None, [ref]$winTo)) {
  73. if ($winTo -lt $winFrom) { $swap = $winFrom; $winFrom = $winTo; $winTo = $swap }
  74. return [pscustomobject]@{
  75. Start = $winFrom
  76. # Koniec domkniety do pelnej sekundy: wypisywany zakres jest ucinany do
  77. # sekund, wiec zdarzenie o .900 zostaje w oknie, z ktorego je zasugerowano.
  78. End = $winTo.AddSeconds(1).AddTicks(-1)
  79. Label = "$($winFrom.ToString($TimeFmt))..$($winTo.ToString($TimeFmt))"
  80. Explicit = $true
  81. Warning = ''
  82. }
  83. }
  84. $ostrzezenie = "Pomijam `$hwin = '$Win' - oczekiwany zapis 'yyyy-MM-ddTHH:mm:ss..yyyy-MM-ddTHH:mm:ss'."
  85. } else {
  86. $ostrzezenie = ''
  87. }
  88.  
  89. $godziny = $DefaultHours
  90. if ($null -ne $Back -and "$Back".Trim() -ne '') {
  91. $parsedH = 0
  92. if ([int]::TryParse("$Back".Trim(), [ref]$parsedH) -and $parsedH -ge 1 -and $parsedH -le 8760) {
  93. $godziny = $parsedH
  94. } else {
  95. $ostrzezenie = "Pomijam `$hback = '$Back' - oczekiwane pelne godziny z zakresu 1-8760. Wracam do ${DefaultHours}h."
  96. }
  97. }
  98. return [pscustomobject]@{
  99. Start = $Now.AddHours(-$godziny)
  100. End = $Now
  101. Label = "ostatnie ${godziny}h"
  102. Explicit = $false
  103. Warning = $ostrzezenie
  104. }
  105. }
  106.  
  107. $Now = Get-Date
  108. $Window = Resolve-TimeWindow -Win $hwin -Back $hback -Now $Now -DefaultHours 168
  109. if ($Window.Warning) {
  110. Write-Host ''
  111. Write-Host " $($Window.Warning)" -ForegroundColor Yellow
  112. }
  113. $StartTime = $Window.Start
  114. $EndTime = $Window.End
  115. $WindowText = $Window.Label
  116.  
  117. # ============================================================
  118. # ADMIN CHECK
  119. # ============================================================
  120. $identity = [Security.Principal.WindowsIdentity]::GetCurrent()
  121. $principal = New-Object Security.Principal.WindowsPrincipal($identity)
  122. if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
  123. Write-Host ''
  124. Write-Host ' !! ERROR: Administrator privileges required !!' -ForegroundColor Red
  125. Write-Host ' Run PowerShell as Administrator.' -ForegroundColor Yellow
  126. Write-Host ''
  127. return
  128. }
  129.  
  130. # ============================================================
  131. # HELPERS
  132. # ============================================================
  133. $ScriptVersion = '2.1'
  134.  
  135. # Pelne nazwy typow logowania. Skrot w rodzaju "Kbd" mowi, ze proba przyszla z klawiatury,
  136. # a typ 2 powstaje takze wtedy, gdy program wywola LogonUser - i wlasnie ta rozbieznosc
  137. # najczesciej wprowadza w blad przy czytaniu wyniku.
  138. $LogonTypeName = @{
  139. 2 = 'interaktywne'
  140. 3 = 'sieciowe'
  141. 4 = 'zadanie-harmonogramu'
  142. 5 = 'usluga'
  143. 7 = 'odblokowanie'
  144. 8 = 'sieciowe-haslo-jawnym-tekstem'
  145. 9 = 'nowe-poswiadczenia'
  146. 10 = 'pulpit-zdalny'
  147. 11 = 'z-cache'
  148. }
  149.  
  150. function Get-LogonTypeName {
  151. param([int]$lt)
  152. if ($LogonTypeName.ContainsKey($lt)) { return $LogonTypeName[$lt] }
  153. return 'nieznany'
  154. }
  155.  
  156. function Format-Hex {
  157. param([object]$Value)
  158. if ($null -eq $Value) { return '0x00000000' }
  159. if ($Value -is [string]) {
  160. if ($Value -match '^0x') { return $Value.ToUpper().Replace('0X','0x') }
  161. return '0x00000000'
  162. }
  163. # Dziennik oddaje SubStatus jako Int32 z ustawionym bitem znaku: 0xC000006A przychodzi
  164. # jako -1073741718 (zmierzone na zdarzeniu 4625, PowerShell 5.1). Maska na Int64
  165. # obsluguje te postac oraz wariant bez znaku, bo LogonType z tego samego zdarzenia
  166. # przychodzi juz jako UInt32 - typy pol nie sa jednolite i lepiej nie zakladac ktorego.
  167. try {
  168. $u = [uint32]([int64]$Value -band 4294967295L)
  169. return '0x{0:X8}' -f $u
  170. } catch {
  171. return '0x00000000'
  172. }
  173. }
  174.  
  175. $SubStatusMap = @{
  176. '0xC0000064' = 'Account does not exist'
  177. '0xC000006A' = 'Wrong password'
  178. '0xC0000234' = 'Account locked out'
  179. '0xC0000072' = 'Account disabled'
  180. '0xC000006D' = 'Generic failure'
  181. '0xC0000071' = 'Password expired'
  182. '0xC000006F' = 'Outside allowed hours'
  183. '0xC0000070' = 'Unauthorized workstation'
  184. '0xC0000193' = 'Account expired'
  185. '0xC0000224' = 'Password must change'
  186. }
  187.  
  188. # Skroty identyczne jak w fla; typ 2 nie dowodzi klawiatury, stad 'Lokal'.
  189. $LogonTypeShort = @{
  190. 2 = 'Lokal'
  191. 3 = 'SMB'
  192. 4 = 'Batch'
  193. 5 = 'Svc'
  194. 7 = 'Unlock'
  195. 8 = 'NetClr'
  196. 9 = 'RunAs'
  197. 10 = 'RDP'
  198. 11 = 'Cache'
  199. }
  200.  
  201. function Test-PrivateIP {
  202. param([string]$IP)
  203. if ([string]::IsNullOrWhiteSpace($IP) -or $IP -eq '-') { return $true }
  204. try {
  205. $parsed = [System.Net.IPAddress]::Parse($IP)
  206. if ($parsed.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetworkV6) {
  207. if ($parsed.IsIPv6LinkLocal -or $parsed.IsIPv6SiteLocal) { return $true }
  208. if ([System.Net.IPAddress]::IsLoopback($parsed)) { return $true }
  209. $firstByte = $parsed.GetAddressBytes()[0]
  210. return ($firstByte -eq 0xFC -or $firstByte -eq 0xFD)
  211. }
  212. $b = $parsed.GetAddressBytes()
  213. if ($b[0] -eq 10) { return $true }
  214. if ($b[0] -eq 172 -and $b[1] -ge 16 -and $b[1] -le 31) { return $true }
  215. if ($b[0] -eq 192 -and $b[1] -eq 168) { return $true }
  216. if ($b[0] -eq 127) { return $true }
  217. if ($b[0] -eq 169 -and $b[1] -eq 254) { return $true }
  218. return $false
  219. } catch {
  220. # Nazwa stacji nie parsuje sie na IP; nie zglaszamy jej jako zrodla obcego.
  221. return $true
  222. }
  223. }
  224.  
  225. # ============================================================
  226. # OUTPUT HELPERS (header / legend / no-events / footer)
  227. # ============================================================
  228. # Naglowek opisuje wlasny przebieg: wersje, host, faktyczne okno i aktywne filtry.
  229. # Dwa powody. Po pierwsze skrypt biegnie w sesji technika, wiec zmienna zostawiona
  230. # przy poprzednim uruchomieniu dziala dalej i po cichu zawezalaby wynik - tutaj widac
  231. # ja od razu. Po drugie wyjscie bywa wklejane komus innemu albo asystentowi AI do
  232. # analizy, a wtedy sam spis zdarzen bez kontekstu nie mowi, czego dotyczy.
  233. function Show-Header {
  234. $border = '=' * 70
  235. $what = if ($failedonly) { 'tylko nieudane (4625)' } else { 'udane i nieudane (4624+4625)' }
  236. # $filtry opisuje przebieg, $filtrySkad zbiera tylko to, co UKRYWA zdarzenia.
  237. $filtry = @()
  238. $filtrySkad = @()
  239. if ($user) { $filtry += "konto=$user"; $filtrySkad += 'konto' }
  240. if ($source) { $filtry += "zrodlo=$source"; $filtrySkad += 'zrodlo' }
  241. if ($null -ne $ltypeWanted) { $filtry += "typ=$ltypeWanted/$(Get-LogonTypeName $ltypeWanted)"; $filtrySkad += 'typ logowania' }
  242. if ($status) { $filtry += "status=$(Format-Hex $status)"; $filtrySkad += 'kod bledu' }
  243. if ($failedonly) { $filtrySkad += 'pominiete udane logowania' }
  244. if ($detail) { $filtry += 'szczegoly=tak' }
  245.  
  246. Write-Host ''
  247. Write-Host $border -ForegroundColor Cyan
  248. Write-Host " ListLogins v$ScriptVersion | host=$env:COMPUTERNAME | okno=$WindowText" -ForegroundColor Cyan
  249. Write-Host (" zakres={0}..{1}" -f $StartTime.ToString($TimeFmt), $EndTime.ToString($TimeFmt)) -ForegroundColor DarkGray
  250. Write-Host " zawartosc: $what" -ForegroundColor DarkGray
  251. if ($filtry.Count -gt 0) {
  252. Write-Host " filtry: $($filtry -join ' ')" -ForegroundColor Yellow
  253. } else {
  254. Write-Host ' filtry: brak (caly zakres)' -ForegroundColor DarkGray
  255. }
  256. Write-Host $border -ForegroundColor Cyan
  257.  
  258. # Filtry przychodza ze zmiennych w konsoli, wiec moga tam zostac po poprzedniej komendzie.
  259. if ($filtrySkad.Count -gt 0) {
  260. Write-Host ''
  261. Write-Host " UWAGA: wynik jest ZAWEZONY ($($filtrySkad -join ', ')). Widzisz tylko czesc zdarzen." -ForegroundColor Yellow
  262. Write-Host ' Zdjecie wszystkich filtrow (skopiuj cala linie):' -ForegroundColor DarkGray
  263. Write-Host ' $user=$null; $source=$null; $ltype=$null; $status=$null; $failedonly=$false; $hwin=$null; $hback=168; irm https://dev.bitback.pl/logins | iex' -ForegroundColor Gray
  264. }
  265. }
  266.  
  267. function Show-Legend {
  268. Write-Host ' Lista od najnowszych. Kolor: zielony/blekitny = udane, zolty/czerwony = nieudane.' -ForegroundColor DarkGray
  269. Write-Host ' Kody sa podawane razem ze znaczeniem, np. typ=2/interaktywne, status=0xC000006A/zle-haslo.' -ForegroundColor DarkGray
  270. }
  271.  
  272. # GUID Logon subcategory - jezykowo niezalezny od nazwy
  273. $LogonSubcategoryGuid = '{0CCE9215-69AE-11D9-BED3-505054503030}'
  274.  
  275. function Test-AuditLogonEnabled {
  276. # Sprawdza FAKTYCZNE ustawienie polityki przez auditpol /r (CSV).
  277. # Format: Machine Name,Policy Target,Subcategory,Subcategory GUID,Inclusion Setting,Exclusion Setting
  278. # Inclusion Setting wartosci - EN: "No Auditing"/"Success"/"Failure"/"Success and Failure"
  279. # PL: "Brak inspekcji"/"Powodzenie"/"Niepowodzenie"/"Powodzenie i Niepowodzenie"
  280. try {
  281. $csv = auditpol /get /subcategory:$LogonSubcategoryGuid /r 2>&1 | ConvertFrom-Csv -ErrorAction Stop
  282. if ($csv -is [array]) { $csv = $csv[0] }
  283. $setting = ($csv.'Inclusion Setting' -as [string]).Trim()
  284. if ($setting -match '(?i)no auditing|brak inspekcji|bez inspekcji') { return $false }
  285. if ($setting -match '(?i)success|failure|powodzenie|niepowodzenie') { return $true }
  286. return $true # nieznana wartosc - zalozyc enabled (nie blokowac)
  287. } catch {
  288. return $true # nie da sie sprawdzic - zalozyc enabled
  289. }
  290. }
  291.  
  292. function Enable-AuditLogon {
  293. # Wlacz Audit Logon (glowny cel)
  294. $output1 = auditpol /set /subcategory:$LogonSubcategoryGuid /success:enable /failure:enable 2>&1
  295. $exit1 = $LASTEXITCODE
  296. # Tez wlacz Audit Policy Change zeby na PRZYSZLOSC 4719 byly zapisywane.
  297. # Bez tego skrypt nie wie kiedy audit zostal wlaczony (caly powod tego problemu teraz).
  298. $policyChangeGuid = '{0CCE922F-69AE-11D9-BED3-505054503030}'
  299. $output2 = auditpol /set /subcategory:$policyChangeGuid /success:enable 2>&1
  300. $verified = Test-AuditLogonEnabled
  301. return [PSCustomObject]@{
  302. Success = ($exit1 -eq 0 -and $verified)
  303. Verified = $verified
  304. Output = (($output1 + $output2) -join "`n").Trim()
  305. }
  306. }
  307.  
  308. function Get-RecentAuditLogonActivation {
  309. # Szuka Event 4719 (System audit policy was changed) dla Logon subcategory.
  310. # Zwraca DateTime ostatniej zmiany audit Logon (gdy zostalo wlaczone/zmienione) lub $null.
  311. try {
  312. $changes = Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4719; StartTime=(Get-Date).AddDays(-7)} -MaxEvents 50 -ErrorAction Stop
  313. foreach ($evt in $changes) {
  314. if ($evt.Message -match [Regex]::Escape($LogonSubcategoryGuid)) {
  315. return $evt.TimeCreated
  316. }
  317. }
  318. } catch { }
  319. return $null
  320. }
  321.  
  322. function Format-TimeAgo {
  323. param([datetime]$When)
  324. $delta = (Get-Date) - $When
  325. if ($delta.TotalMinutes -lt 1) { return 'less than a minute ago' }
  326. if ($delta.TotalMinutes -lt 60) { return "$([Math]::Round($delta.TotalMinutes)) minutes ago" }
  327. if ($delta.TotalHours -lt 24) { return "$([Math]::Round($delta.TotalHours, 1))h ago" }
  328. return "$([Math]::Round($delta.TotalDays, 1)) days ago"
  329. }
  330.  
  331. function Show-AuditDisabledOffer {
  332. Write-Host ''
  333. Write-Host ' PROBLEM: Audit Logon is DISABLED on this computer.' -ForegroundColor Yellow
  334. Write-Host ' Windows is not recording interactive logins to Security log -' -ForegroundColor Yellow
  335. Write-Host ' this script has nothing to show until auditing is enabled.' -ForegroundColor Yellow
  336. Write-Host ''
  337. Write-Host ' Enabling takes one command (admin required, you already have it):' -ForegroundColor DarkGray
  338. Write-Host " auditpol /set /subcategory:$LogonSubcategoryGuid /success:enable /failure:enable" -ForegroundColor DarkGray
  339. Write-Host ''
  340. $answer = Read-Host ' Enable now? [Y/N]'
  341. if ($answer -match '^[Yy]') {
  342. Write-Host ''
  343. Write-Host ' Enabling Audit Logon...' -ForegroundColor Cyan
  344. $r = Enable-AuditLogon
  345. if ($r.Success) {
  346. Write-Host ' OK. Audit Logon enabled and verified.' -ForegroundColor Green
  347. Write-Host ' Your future logins (from next unlock/login) will be recorded.' -ForegroundColor Green
  348. Write-Host ' Run the script again after the next login to see the list.' -ForegroundColor Green
  349. } elseif (-not $r.Verified) {
  350. Write-Host ' Command executed, but audit is STILL disabled after verification.' -ForegroundColor Red
  351. Write-Host ' Likely GPO override (Group Policy overrides local audit policy).' -ForegroundColor Red
  352. Write-Host ' Check: gpedit.msc -> Computer Config -> Windows Settings -> Security Settings' -ForegroundColor Red
  353. Write-Host ' -> Advanced Audit Policy -> Logon/Logoff -> Audit Logon' -ForegroundColor Red
  354. Write-Host ' Or domain admin must change the domain policy.' -ForegroundColor Red
  355. } else {
  356. Write-Host ' Failed to enable:' -ForegroundColor Red
  357. Write-Host " $($r.Output)" -ForegroundColor Red
  358. Write-Host ' Run the command above manually.' -ForegroundColor Red
  359. }
  360. } else {
  361. Write-Host ' OK. You can enable later manually with the command above.' -ForegroundColor DarkGray
  362. }
  363. }
  364.  
  365. function Show-NoEvents {
  366. param(
  367. [int]$RawCount = 0,
  368. [int]$SkippedCount = 0,
  369. [array]$RawEvents = @()
  370. )
  371. Write-Host ''
  372. $auditOn = Test-AuditLogonEnabled
  373. if (-not $auditOn) {
  374. if ($RawCount -eq 0) {
  375. Write-Host " Dziennik Security nie ma zdarzen 4624/4625 w oknie $WindowText." -ForegroundColor Yellow
  376. } else {
  377. Write-Host " Of $RawCount events in log, all are noise (services LT=5, SYSTEM, machine accounts)." -ForegroundColor Yellow
  378. }
  379. Show-AuditDisabledOffer
  380. } else {
  381. # Audit on, but no data. Two signals whether audit was recently enabled:
  382. # 1. Event 4719 (System audit policy was changed) - precise but requires Audit Policy Change ON
  383. # 2. Heuristic: NO user 4624 in 7 days despite audit ON - likely freshly enabled
  384. $recentActivation = Get-RecentAuditLogonActivation
  385.  
  386. $userLogonTypes = @(2, 3, 7, 10, 11)
  387. $hasUserLogonsInRaw = $false
  388. foreach ($evt in $RawEvents) {
  389. if ($evt.Id -eq 4624) {
  390. $lt = [int]$evt.Properties[8].Value
  391. if ($lt -in $userLogonTypes) { $hasUserLogonsInRaw = $true; break }
  392. }
  393. }
  394.  
  395. if ($recentActivation) {
  396. $ago = Format-TimeAgo $recentActivation
  397. Write-Host " Audit Logon was enabled/changed $ago." -ForegroundColor Cyan
  398. Write-Host " ($($recentActivation.ToString('yyyy-MM-dd HH:mm:ss')))" -ForegroundColor DarkGray
  399. Write-Host ' Windows records logins ONLY from that moment - earlier logins are not visible.' -ForegroundColor Cyan
  400. Write-Host ''
  401. Write-Host ' To see your login:' -ForegroundColor Green
  402. Write-Host ' 1. Win+L (lock) and unlock with password, or log out and log in again' -ForegroundColor Green
  403. Write-Host ' 2. Run the script again - you will see the new entry' -ForegroundColor Green
  404. } elseif (-not $hasUserLogonsInRaw -and $RawCount -gt 0) {
  405. # Audit ON, service events exist but ZERO user logins in 7 days - audit likely freshly enabled
  406. Write-Host " Audyt logowan dziala, ale w oknie $WindowText nie ma zadnych logowan uzytkownikow." -ForegroundColor Cyan
  407. Write-Host ' Audit was likely enabled recently (exact moment unrecorded because Audit Policy' -ForegroundColor Cyan
  408. Write-Host ' Change was also disabled - 4719 not written).' -ForegroundColor DarkGray
  409. Write-Host ''
  410. Write-Host ' To see your login:' -ForegroundColor Green
  411. Write-Host ' 1. Win+L (lock) and unlock with password, or log out and log in again' -ForegroundColor Green
  412. Write-Host ' 2. Run the script again - you will see the new entry' -ForegroundColor Green
  413. } elseif ($RawCount -eq 0) {
  414. Write-Host " Audyt logowan dziala, ale w oknie $WindowText nie ma logowan." -ForegroundColor Green
  415. Write-Host ' (Computer unused / log recently cleared.)' -ForegroundColor DarkGray
  416. } else {
  417. Write-Host " Audit Logon is working. Of $RawCount events, $SkippedCount filtered as noise (services/SYSTEM)." -ForegroundColor Green
  418. Write-Host ' No user logins to show in this time window.' -ForegroundColor DarkGray
  419. }
  420. Write-Host ' Tip: $failedonly=$true shows only failed (Event 4625, no filtering).' -ForegroundColor DarkGray
  421. }
  422. }
  423.  
  424. function Show-Tips {
  425. Write-Host ' Tips - copy whole line and paste in PowerShell:' -ForegroundColor DarkGray
  426. Write-Host ' # show more entries (default 10):' -ForegroundColor DarkGray
  427. Write-Host ' $count=20; irm https://dev.bitback.pl/logins | iex' -ForegroundColor Gray
  428. Write-Host ' # group repeated attempts:' -ForegroundColor DarkGray
  429. Write-Host ' $group=$true; irm https://dev.bitback.pl/logins | iex' -ForegroundColor Gray
  430. Write-Host ' # only failed (Event 4625):' -ForegroundColor DarkGray
  431. Write-Host ' $failedonly=$true; irm https://dev.bitback.pl/logins | iex' -ForegroundColor Gray
  432. }
  433.  
  434. function Show-Footer {
  435. $border = '=' * 70
  436. Write-Host ''
  437. Write-Host $border -ForegroundColor Cyan
  438. Write-Host " ListLogins v$ScriptVersion" -ForegroundColor Cyan
  439. Write-Host $border -ForegroundColor Cyan
  440. Write-Host ''
  441. }
  442.  
  443. # ============================================================
  444. # COLLECT EVENTS
  445. # ============================================================
  446.  
  447. # Filtr typu logowania przyjmuje i liczbe (2), i skrot (Kbd, RDP), bo fla pokazuje skrot,
  448. # a technik czesto zna numer. Normalizacja stoi przed naglowkiem, ktory ten filtr wypisuje.
  449. $ltypeWanted = $null
  450. if ($null -ne $ltype -and "$ltype".Trim() -ne '') {
  451. $rawLt = "$ltype".Trim()
  452. $parsedLt = 0
  453. if ([int]::TryParse($rawLt, [ref]$parsedLt)) {
  454. $ltypeWanted = $parsedLt
  455. } else {
  456. foreach ($k in $LogonTypeShort.Keys) {
  457. if ($LogonTypeShort[$k] -eq $rawLt) { $ltypeWanted = [int]$k; break }
  458. }
  459. if ($null -eq $ltypeWanted) {
  460. Write-Host ''
  461. Write-Host " Pomijam `$ltype = '$ltype' - oczekiwana liczba albo jeden ze skrotow: $(($LogonTypeShort.Values | Sort-Object) -join ', ')" -ForegroundColor Yellow
  462. }
  463. }
  464. }
  465.  
  466. Show-Header
  467.  
  468. Write-Host ''
  469. $modeLabel = if ($failedonly) { 'failed logins (4625)' } else { 'logins (4624+4625)' }
  470. Write-Host " Reading $modeLabel..." -ForegroundColor Cyan
  471.  
  472. $startTimeUtc = $StartTime.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ")
  473. $endTimeUtc = $EndTime.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ")
  474.  
  475. # Build XPath - 4624+4625 or just 4625
  476. if ($failedonly) {
  477. $eventFilter = "EventID=4625"
  478. } else {
  479. $eventFilter = "(EventID=4624 or EventID=4625)"
  480. }
  481.  
  482. # Zakres domkniety z OBU stron. Wczesniej byl tylko poczatek, bo okno zawsze konczylo
  483. # sie "teraz"; przy $hwin z fla koniec jest istotny i bez niego wynik wychodzilby poza
  484. # zakres, ktory technik wlasnie zawezil.
  485. $filterXml = @"
  486. <QueryList>
  487. <Query Id="0" Path="Security">
  488. <Select Path="Security">
  489. *[System[$eventFilter and TimeCreated[@SystemTime&gt;='$startTimeUtc' and @SystemTime&lt;='$endTimeUtc']]]
  490. </Select>
  491. </Query>
  492. </QueryList>
  493. "@
  494.  
  495. try {
  496. $rawEvents = @(Get-WinEvent -FilterXml $filterXml -ErrorAction Stop)
  497. } catch {
  498. if ($_.Exception.Message -match 'No events were found') {
  499. Show-NoEvents -RawCount 0 -SkippedCount 0 -RawEvents @()
  500. Show-Footer
  501. return
  502. }
  503. Write-Host " ERROR: $($_.Exception.Message)" -ForegroundColor Red
  504. Show-Footer
  505. return
  506. }
  507.  
  508. $rawCount = $rawEvents.Count
  509.  
  510. # Odczyt pol PO NAZWIE z XML-a zdarzenia, a nie po numerze pozycji.
  511. # Powody, w kolejnosci waznosci:
  512. # 1. 4624 i 4625 trzymaja te same dane pod ROZNYMI indeksami, co wymuszalo dwie osobne
  513. # tablice i dwie sciezki kodu, ktore mogly sie rozjechac przy kazdej edycji.
  514. # 2. Nazwy pol sa takie same niezaleznie od jezyka Windows (inaczej niz tresc komunikatu).
  515. # 3. Pola diagnostyczne (proces wywolujacy, proces logowania) dostajemy bez dokladania
  516. # kolejnych numerkow do zapamietania.
  517. # Koszt zmierzony: okolo 0,1 ms na zdarzenie zamiast 0,035 ms, czyli 0,5 s na 5000 zdarzen.
  518. # Wobec kosztu samego czytania dziennika - szum.
  519. function ConvertFrom-EventXml {
  520. param([string]$Xml)
  521. $d = @{}
  522. try { $x = [xml]$Xml } catch { return $d }
  523. if (-not $x.Event -or -not $x.Event.EventData) { return $d }
  524. foreach ($n in $x.Event.EventData.Data) {
  525. # InnerText, nie '#text': pole puste w dzienniku to <Data Name='X'/> bez wezla
  526. # tekstowego, na ktorym odwolanie do '#text' zwraca null.
  527. if ($n.Name) { $d[$n.Name] = [string]$n.InnerText }
  528. }
  529. return $d
  530. }
  531.  
  532. $events = @()
  533. $filteredOut = 0
  534. $noiseSkipped = 0
  535. foreach ($evt in $rawEvents) {
  536. $eventId = $evt.Id
  537. $d = ConvertFrom-EventXml -Xml $evt.ToXml()
  538.  
  539. $targetUser = if ($d.ContainsKey('TargetUserName')) { $d['TargetUserName'] } else { '(?)' }
  540. $targetDomain = if ($d.ContainsKey('TargetDomainName')) { $d['TargetDomainName'] } else { '' }
  541. $logonType = 0
  542. if ($d.ContainsKey('LogonType')) { [void][int]::TryParse($d['LogonType'], [ref]$logonType) }
  543. $sourceIP = if ($d.ContainsKey('IpAddress')) { $d['IpAddress'] } else { '-' }
  544. $workstation = if ($d.ContainsKey('WorkstationName')) { $d['WorkstationName'] } else { '' }
  545.  
  546. if ($eventId -eq 4624) {
  547. $subStatus = $null
  548. # Szum systemowy - to samo kryterium co dotychczas.
  549. if ($targetUser -eq 'SYSTEM' -or $targetUser -eq 'NETWORK SERVICE' -or $targetUser -eq 'LOCAL SERVICE') { $noiseSkipped++; continue }
  550. if ($targetUser -match '\$$') { $noiseSkipped++; continue } # konta maszynowe
  551. if ($logonType -eq 5) { $noiseSkipped++; continue } # start uslug
  552. # Konta wirtualne pulpitu, logowane typem 2. Dopasowanie po nazwie konta, bo nazwa
  553. # domeny ('Window Manager') zalezy od jezyka systemu, a wzorzec DWM-n / UMFD-n nie.
  554. if ($targetUser -match '^(DWM|UMFD)-\d+$') { $noiseSkipped++; continue }
  555. } else {
  556. $subStatus = if ($d.ContainsKey('SubStatus')) { Format-Hex $d['SubStatus'] } else { '0x00000000' }
  557. }
  558.  
  559. $srcLabel = $sourceIP
  560. if ($srcLabel -eq '-' -or [string]::IsNullOrWhiteSpace($srcLabel)) {
  561. $srcLabel = if ($workstation) { $workstation } else { '-' }
  562. }
  563. $fullUser = if ($targetDomain -and $targetDomain -ne '-') { "$targetDomain\$targetUser" } else { $targetUser }
  564.  
  565. # --- filtry celowania ---
  566. # Konto dopasowujemy w obu postaciach: pelnej (DOMENA\User) i samej nazwy, bo fla
  567. # wypisuje pelna, a technik czesto pamieta tylko login.
  568. if ($user) {
  569. $wanted = "$user".Trim()
  570. # Trzeci warunek zdejmuje domene z wartosci OCZEKIWANEJ i porownuje z sama nazwa
  571. # konta ze zdarzenia. Dzieki temu 'DOMENA\jkowalski' trafia takze w zdarzenia,
  572. # ktore maja pusta domene - a tak zapisuja sie proby na konto nieistniejace
  573. # w domenie, czyli najciekawszy operacyjnie przypadek.
  574. $wantedBare = $wanted -replace '^.*\\', ''
  575. if (($fullUser -ne $wanted) -and ($targetUser -ne $wanted) -and
  576. ($targetUser -ne $wantedBare)) { $filteredOut++; continue }
  577. }
  578. if ($source) {
  579. $wantedSrc = "$source".Trim()
  580. if (($srcLabel -ne $wantedSrc) -and ($sourceIP -ne $wantedSrc) -and ($workstation -ne $wantedSrc)) { $filteredOut++; continue }
  581. }
  582. # Typ logowania, tak samo jak kod niepowodzenia, zawezamy wylacznie wsrod zdarzen
  583. # nieudanych. Udane wejscie po serii prob bywa zapisane innym typem niz same proby
  584. # (proby jako sieciowe, wejscie jako pulpit zdalny), wiec filtrowanie po typie takze
  585. # zdarzen udanych ukryloby dokladnie ten wpis, ktorego technik szuka.
  586. if ($null -ne $ltypeWanted -and $eventId -eq 4625 -and $logonType -ne $ltypeWanted) { $filteredOut++; continue }
  587. # Status filtruje WYLACZNIE nieudane. Gdyby ucinal tez 4624, ukrylby udane logowanie
  588. # atakujacego - czyli odpowiedz na pytanie, po ktore technik tu przyszedl.
  589. if ($status -and $eventId -eq 4625) {
  590. if ($subStatus -ne (Format-Hex $status)) { $filteredOut++; continue }
  591. }
  592.  
  593. $events += [PSCustomObject]@{
  594. Time = $evt.TimeCreated
  595. EventId = $eventId
  596. User = $fullUser
  597. Source = $srcLabel
  598. LogonType = $logonType
  599. SubStatus = $subStatus
  600. LogonProc = if ($d.ContainsKey('LogonProcessName')) { $d['LogonProcessName'].Trim() } else { '' }
  601. AuthPkg = if ($d.ContainsKey('AuthenticationPackageName')) { $d['AuthenticationPackageName'].Trim() } else { '' }
  602. Process = if ($d.ContainsKey('ProcessName')) { $d['ProcessName'] } else { '' }
  603. Subject = if ($d.ContainsKey('SubjectUserName')) { $d['SubjectUserName'] } else { '' }
  604. }
  605. }
  606.  
  607. $events = @($events | Sort-Object Time -Descending)
  608.  
  609. if ($events.Count -eq 0) {
  610. Show-NoEvents -RawCount $rawCount -SkippedCount ($rawCount - $events.Count) -RawEvents $rawEvents
  611. Show-Footer
  612. return
  613. }
  614.  
  615. Write-Host " $($events.Count) events." -ForegroundColor Cyan
  616. Write-Host ''
  617.  
  618. $border = '=' * 70
  619.  
  620. # ============================================================
  621. # OUTPUT HELPERS
  622. # ============================================================
  623. function Get-StatusTag {
  624. param($EventId, $SubStatus)
  625. if ($EventId -eq 4624) { return 'OK' }
  626. if ($SubStatus -and $SubStatusMap.ContainsKey($SubStatus)) {
  627. return "FAIL: $($SubStatusMap[$SubStatus])"
  628. }
  629. return 'FAIL'
  630. }
  631.  
  632. function Get-EventColor {
  633. param($EventId, $LogonType, $SubStatus, [int]$Count = 1)
  634. if ($EventId -eq 4624) {
  635. if ($LogonType -eq 10) { return 'Cyan' }
  636. return 'Green'
  637. }
  638. if ($LogonType -eq 10) { return 'Red' }
  639. if ($SubStatus -eq '0xC000006A') { return 'Yellow' }
  640. if ($Count -ge 20) { return 'Red' }
  641. return 'DarkYellow'
  642. }
  643.  
  644. function Get-LogonTypeShort {
  645. param([int]$lt)
  646. if ($LogonTypeShort.ContainsKey($lt)) { return $LogonTypeShort[$lt] }
  647. return "T$lt"
  648. }
  649.  
  650. # Wynik proby razem z kodem. Sam kod nic nie mowi czytajacemu, a sam opis nie nadaje sie
  651. # do wklejenia jako filtr - stad obie postaci obok siebie.
  652. function Get-OutcomeText {
  653. param($EventId, $SubStatus)
  654. if ($EventId -eq 4624) { return 'UDANE' }
  655. if ($SubStatus -and $SubStatusMap.ContainsKey($SubStatus)) {
  656. return "NIEUDANE status=$SubStatus/$($SubStatusMap[$SubStatus])"
  657. }
  658. if ($SubStatus) { return "NIEUDANE status=$SubStatus/nieznany-kod" }
  659. return 'NIEUDANE'
  660. }
  661.  
  662. # Proces logowania rozstrzyga, czy proba pochodzila od czlowieka przy konsoli, czy od
  663. # programu. To jedyne pole, ktore odroznia te dwie sytuacje - typ logowania pokazuje
  664. # w obu przypadkach to samo.
  665. function Format-LogonProc {
  666. param([string]$lp)
  667. if (-not $lp) { return '-' }
  668. switch ($lp) {
  669. 'User32' { return 'User32/klawiatura' }
  670. 'Advapi' { return 'Advapi/programowo' }
  671. 'NtLmSsp' { return 'NtLmSsp/sieciowo' }
  672. 'Kerberos' { return 'Kerberos/sieciowo' }
  673. default { return $lp }
  674. }
  675. }
  676.  
  677. function Format-ProcName {
  678. param([string]$p)
  679. if (-not $p -or $p -eq '-') { return '-' }
  680. # Sama nazwa pliku wystarcza do rozpoznania sprawcy, a pelna sciezka ujawnia
  681. # inwentarz oprogramowania w wyjsciu, ktore bywa pokazywane przy kliencie.
  682. try { return (Split-Path $p -Leaf) } catch { return $p }
  683. }
  684.  
  685. # Procesy, ktore wystawiaja okno na haslo - zdarzenie wyglada wtedy jak wywolanie programowe.
  686. $InteractiveProcs = @(
  687. 'consent.exe', # okno UAC "podaj dane administratora"
  688. 'runas.exe',
  689. 'explorer.exe', # "Uruchom jako inny uzytkownik", mapowanie dysku sieciowego
  690. 'mmc.exe', # konsole zarzadzania (AD, DNS, DHCP, uslugi)
  691. 'powershell.exe',
  692. 'pwsh.exe',
  693. 'cmd.exe',
  694. 'credwiz.exe',
  695. 'rundll32.exe', # okno "Menedzer poswiadczen" / keymgr
  696. 'mstsc.exe', # klient pulpitu zdalnego zapisujacy poswiadczenia
  697. 'LogonUI.exe',
  698. 'winlogon.exe'
  699. )
  700.  
  701. # Kolejnosc regul jest znaczaca: User32 rozstrzyga ponad typem logowania i procesem.
  702. function Get-CauseInfo {
  703. param(
  704. [int]$EventId,
  705. [int]$LogonType,
  706. [string]$LogonProc,
  707. [string]$ProcessLeaf,
  708. [string]$Source,
  709. [bool]$SourceExternal = $false
  710. )
  711.  
  712. $lp = ($LogonProc -as [string]).Trim()
  713. $proc = ($ProcessLeaf -as [string]).Trim()
  714. $procTxt = if ($proc -and $proc -ne '-') { $proc } else { '' }
  715.  
  716. if ($lp -eq 'User32') {
  717. return [pscustomobject]@{
  718. Key = 'klawiatura'
  719. Kto = 'CZLOWIEK przy tym komputerze'
  720. Opis = 'haslo wpisano na ekranie logowania Windows, czyli ktos siedzial przy tej maszynie (albo byl na niej zalogowany zdalnie i wywolal ekran logowania)'
  721. Co = 'ustal, kto mial dostep do konsoli w tych godzinach; to nie jest usterka techniczna'
  722. }
  723. }
  724. if ($LogonType -eq 4) {
  725. return [pscustomobject]@{
  726. Key = 'harmonogram'
  727. Kto = 'zadanie harmonogramu'
  728. Opis = "logowanie wywolalo zadanie z Harmonogramu zadan$(if ($procTxt) { " (proces $procTxt)" })"
  729. Co = 'najczestsza przyczyna to haslo zmienione po zapisaniu zadania - popraw haslo w zadaniu (taskschd.msc)'
  730. }
  731. }
  732. if ($LogonType -eq 5) {
  733. return [pscustomobject]@{
  734. Key = 'usluga'
  735. Kto = 'usluga systemowa'
  736. Opis = "logowanie wywolala usluga Windows$(if ($procTxt) { " (proces $procTxt)" })"
  737. Co = 'usterka techniczna, nie atak: usluga ma zapisane stare haslo - popraw je w services.msc na zakladce Logowanie'
  738. }
  739. }
  740. if ($LogonType -eq 10) {
  741. if ($SourceExternal) {
  742. return [pscustomobject]@{
  743. Key = 'rdp-zewnetrzny'
  744. Kto = "pulpit zdalny z adresu SPOZA sieci lokalnej ($Source)"
  745. Opis = 'ktos probuje wejsc przez pulpit zdalny z internetu'
  746. Co = 'to jest powazne: zablokuj ten adres na zaporze i sprawdz, czy port pulpitu zdalnego jest wystawiony do internetu'
  747. }
  748. }
  749. return [pscustomobject]@{
  750. Key = 'rdp-wewnetrzny'
  751. Kto = "pulpit zdalny z sieci lokalnej ($Source)"
  752. Opis = 'ktos lub cos laczy sie pulpitem zdalnym z urzadzenia w sieci'
  753. Co = "ustal, co stoi pod adresem $Source i kto z niego korzysta"
  754. }
  755. }
  756. if ($LogonType -eq 3 -or $LogonType -eq 8) {
  757. if ($SourceExternal) {
  758. return [pscustomobject]@{
  759. Key = 'siec-zewnetrzna'
  760. Kto = "urzadzenie SPOZA sieci lokalnej ($Source)"
  761. Opis = 'proba logowania po sieci z adresu, ktory nie nalezy do sieci lokalnej'
  762. Co = 'zablokuj ten adres na zaporze i sprawdz, co jest z zewnatrz dostepne'
  763. }
  764. }
  765. if ([string]::IsNullOrWhiteSpace($Source) -or $Source -eq '-') {
  766. return [pscustomobject]@{
  767. Key = 'siec-bez-adresu'
  768. Kto = 'zadanie po sieci, dziennik nie zapisal skad'
  769. Opis = 'proba logowania przyszla po sieci, ale zdarzenie nie ma ani adresu, ani nazwy stacji - tak wygladaja najczesciej odrzucone proby uzgodnienia sesji SMB, zwykle szum sieciowy'
  770. Co = 'jesli takich wpisow jest kilka na dobe i nie ma przy nich nazwy konta, mozesz je pominac; przy wiekszej liczbie sprawdz, co skanuje siec'
  771. }
  772. }
  773. return [pscustomobject]@{
  774. Key = 'siec-wewnetrzna'
  775. Kto = "urzadzenie w sieci lokalnej ($Source)"
  776. Opis = 'proba logowania po sieci - typowo zamapowany dysk, drukarka, skaner, kopia zapasowa albo telefon z zapisanym starym haslem'
  777. Co = "ustal, co stoi pod $Source; jesli to sprzet lub program z zapisanym haslem - popraw tam haslo"
  778. }
  779. }
  780. if ($lp -eq 'Advapi') {
  781. if ($procTxt -and ($InteractiveProcs -contains $procTxt)) {
  782. return [pscustomobject]@{
  783. Key = 'okienko'
  784. Kto = "CZLOWIEK, ale w okienku programu $procTxt (nie na ekranie logowania)"
  785. Opis = "haslo wpisano w oknie, ktore wystawil $procTxt - tak wyglada podanie danych administratora w okienku UAC, opcja Uruchom jako inny uzytkownik, konsola zarzadzania albo mapowanie dysku sieciowego"
  786. Co = 'zapytaj osobe pracujaca wtedy na tej maszynie, do czego podawala haslo - z jej strony to nie bylo "logowanie sie", wiec moze zaprzeczac w dobrej wierze'
  787. }
  788. }
  789. if ($procTxt) {
  790. return [pscustomobject]@{
  791. Key = 'program'
  792. Kto = "program $procTxt"
  793. Opis = "logowanie wywolal program $procTxt przez funkcje systemowa, a nie czlowiek przez klawiature"
  794. Co = "sprawdz, czy $procTxt ma gdzies zapisane haslo tego konta (zadanie, usluga, wlasna konfiguracja) i popraw je tam"
  795. }
  796. }
  797. return [pscustomobject]@{
  798. Key = 'program-bez-nazwy'
  799. Kto = 'jakis program na tej maszynie'
  800. Opis = 'logowanie wywolal program przez funkcje systemowa, ale dziennik nie zapisal jego nazwy'
  801. Co = 'poszukaj w tych samych minutach innych zdarzen w dzienniku aplikacji; sprawdz zadania harmonogramu i uslugi dzialajace na tym koncie'
  802. }
  803. }
  804. if ($lp -eq 'NtLmSsp' -or $lp -eq 'Kerberos') {
  805. return [pscustomobject]@{
  806. Key = 'siec-uwierzytelnianie'
  807. Kto = 'zadanie z sieci (uwierzytelnianie domenowe)'
  808. Opis = "proba przyszla przez mechanizm sieciowy $lp - to prosba przekazana przez inny komputer albo usluge, nie klawiatura tej maszyny"
  809. Co = 'ustal, ktore urzadzenie pyta - sprawdz nazwe stacji i adres zrodla w wierszu zdarzenia'
  810. }
  811. }
  812. return [pscustomobject]@{
  813. Key = 'nieustalone'
  814. Kto = 'nie ustalono'
  815. Opis = "dziennik nie zapisal pola rozstrzygajacego (proces logowania: '$(if ($lp) { $lp } else { 'brak' })')"
  816. Co = 'sprawdz to zdarzenie w Podgladzie zdarzen: dziennik Security, ten sam znacznik czasu'
  817. }
  818. }
  819.  
  820. # Tutaj, nie w petli zbierajacej - Get-CauseInfo i Format-ProcName sa zdefiniowane wyzej,
  821. # ale petla zbierajaca stoi przed nimi.
  822. foreach ($e in $events) {
  823. $cause = Get-CauseInfo -EventId $e.EventId -LogonType $e.LogonType -LogonProc $e.LogonProc `
  824. -ProcessLeaf (Format-ProcName $e.Process) -Source $e.Source `
  825. -SourceExternal (-not (Test-PrivateIP $e.Source))
  826. $e | Add-Member -NotePropertyName Cause -NotePropertyValue $cause -Force
  827. }
  828.  
  829. function Show-Meaning {
  830. param([array]$All)
  831.  
  832. $failed = @($All | Where-Object { $_.EventId -eq 4625 })
  833. if ($failed.Count -eq 0) { return }
  834.  
  835. $byCause = @($failed | Group-Object { $_.Cause.Key } | Sort-Object Count -Descending)
  836. $top = $byCause[0]
  837. $topCause = $top.Group[0].Cause
  838.  
  839. Write-Host ''
  840. Write-Host ('=' * 70) -ForegroundColor Cyan
  841. Write-Host ' CO TO ZNACZY' -ForegroundColor Cyan
  842. Write-Host ('=' * 70) -ForegroundColor Cyan
  843. Write-Host (" Zrodlo problemu: {0}" -f $topCause.Kto) -ForegroundColor White
  844. Write-Host (" {0} z {1} nieudanych prob" -f $top.Count, $failed.Count) -ForegroundColor DarkGray
  845. Write-Host (" {0}." -f $topCause.Opis) -ForegroundColor Gray
  846. Write-Host ''
  847. Write-Host (" Co dalej: {0}." -f $topCause.Co) -ForegroundColor Yellow
  848.  
  849. if ($byCause.Count -gt 1) {
  850. Write-Host ''
  851. Write-Host ' W tym samym oknie sa tez inne przyczyny:' -ForegroundColor DarkGray
  852. foreach ($c in $byCause[1..($byCause.Count - 1)]) {
  853. Write-Host (" - {0}x {1}" -f $c.Count, $c.Group[0].Cause.Kto) -ForegroundColor DarkGray
  854. }
  855. }
  856.  
  857. # 4624 zapisuja sie parami o identycznym czasie, wiec grupujemy po znaczniku czasu.
  858. $ostatniaProba = ($failed | Sort-Object Time -Descending | Select-Object -First 1).Time
  859. $udanePo = @($All | Where-Object { $_.EventId -eq 4624 -and $_.Time -ge $ostatniaProba } |
  860. Sort-Object Time | Group-Object { $_.Time.ToString('o') })
  861. Write-Host ''
  862. if ($udanePo.Count -gt 0) {
  863. $pierwsze = $udanePo[0].Group[0]
  864. $ile = [Math]::Round(($pierwsze.Time - $ostatniaProba).TotalSeconds)
  865. Write-Host (" SKUTEK: po ostatniej nieudanej probie logowanie SIE UDALO - {0}, {1} s pozniej, konto {2}." -f `
  866. $pierwsze.Time.ToString($TimeFmt), $ile, $pierwsze.User) -ForegroundColor Yellow
  867. Write-Host (" tamto udane logowanie: {0}" -f $pierwsze.Cause.Kto) -ForegroundColor DarkGray
  868. } elseif ($failedonly) {
  869. Write-Host ' SKUTEK: nie wiadomo, czy ktos w koncu wszedl - $failedonly=$true ukrywa udane logowania.' -ForegroundColor DarkGray
  870. } else {
  871. Write-Host ' SKUTEK: w tym oknie po ostatniej nieudanej probie NIE bylo udanego logowania.' -ForegroundColor Green
  872. }
  873. }
  874.  
  875. # ============================================================
  876. # OUTPUT
  877. # ============================================================
  878.  
  879. Write-Host $border -ForegroundColor Cyan
  880. Write-Host " $env:COMPUTERNAME" -ForegroundColor White
  881. Write-Host $border -ForegroundColor Cyan
  882. Write-Host ''
  883. Show-Legend
  884. Write-Host ''
  885.  
  886. if (-not $group) {
  887. # ============================================================
  888. # SIMPLE MODE
  889. # ============================================================
  890. $shown = @($events | Select-Object -First $count)
  891. $idx = 0
  892.  
  893. foreach ($e in $shown) {
  894. $idx++
  895. # Czas w tym samym formacie, ktorego uzywa $hwin - kazdy znacznik widoczny na
  896. # ekranie da sie wkleic jako granice okna bez przepisywania.
  897. $timeStr = $e.Time.ToString($TimeFmt)
  898. $color = Get-EventColor $e.EventId $e.LogonType $e.SubStatus
  899.  
  900. Write-Host (" {0,2}. {1} typ={2}/{3}" -f $idx, $timeStr, $e.LogonType, (Get-LogonTypeName $e.LogonType)) -ForegroundColor $color
  901. Write-Host (" konto={0} zrodlo={1} {2}" -f $e.User, $e.Source, (Get-OutcomeText $e.EventId $e.SubStatus)) -ForegroundColor DarkGray
  902. Write-Host (" -> {0}" -f $e.Cause.Kto) -ForegroundColor DarkCyan
  903.  
  904. if ($detail) {
  905. Write-Host (" logon={0} proces={1} pakiet={2} inicjator={3}" -f `
  906. (Format-LogonProc $e.LogonProc), (Format-ProcName $e.Process),
  907. $(if ($e.AuthPkg) { $e.AuthPkg } else { '-' }),
  908. $(if ($e.Subject) { $e.Subject } else { '-' })) -ForegroundColor DarkGray
  909. }
  910. }
  911.  
  912. Write-Host ''
  913. Write-Host " Showing $($shown.Count) of $($events.Count)." -ForegroundColor DarkGray
  914. # $events, nie $shown - podsumowanie dotyczy calego okna, nie widocznej czesci listy.
  915. Show-Meaning $events
  916. Write-Host ''
  917. Show-Tips
  918.  
  919. } else {
  920. # ============================================================
  921. # GROUPED MODE
  922. # ============================================================
  923. $sorted = $events | Sort-Object Time
  924.  
  925. $groups = @()
  926. $currentGroup = $null
  927.  
  928. foreach ($e in $sorted) {
  929. $key = "$($e.EventId)|$($e.Source)|$($e.User)|$($e.SubStatus)|$($e.LogonType)"
  930.  
  931. if ($null -eq $currentGroup) {
  932. $currentGroup = @{
  933. Key = $key
  934. EventId = $e.EventId
  935. Source = $e.Source
  936. User = $e.User
  937. LogonType = $e.LogonType
  938. SubStatus = $e.SubStatus
  939. First = $e.Time
  940. Last = $e.Time
  941. Count = 1
  942. }
  943. continue
  944. }
  945.  
  946. $gap = ($e.Time - $currentGroup.Last).TotalMinutes
  947.  
  948. if ($key -eq $currentGroup.Key -and $gap -le $interval) {
  949. $currentGroup.Last = $e.Time
  950. $currentGroup.Count++
  951. } else {
  952. $groups += [PSCustomObject]$currentGroup
  953. $currentGroup = @{
  954. Key = $key
  955. EventId = $e.EventId
  956. Source = $e.Source
  957. User = $e.User
  958. LogonType = $e.LogonType
  959. SubStatus = $e.SubStatus
  960. First = $e.Time
  961. Last = $e.Time
  962. Count = 1
  963. }
  964. }
  965. }
  966. if ($null -ne $currentGroup) {
  967. $groups += [PSCustomObject]$currentGroup
  968. }
  969.  
  970. $groups = @($groups | Sort-Object Last -Descending)
  971. $shown = @($groups | Select-Object -First $count)
  972.  
  973. Write-Host ''
  974. $idx = 0
  975. foreach ($g in $shown) {
  976. $idx++
  977. $ltShort = Get-LogonTypeShort $g.LogonType
  978. $tag = Get-StatusTag $g.EventId $g.SubStatus
  979. $color = Get-EventColor $g.EventId $g.LogonType $g.SubStatus $g.Count
  980.  
  981. if ($g.Count -eq 1) {
  982. $timeStr = $g.First.ToString('yyyy-MM-dd HH:mm:ss')
  983. Write-Host (" {0,2}. {1} {2,-6} {3,-25} <- {4}" -f $idx, $timeStr, $ltShort, $g.User, $g.Source) -ForegroundColor $color
  984. Write-Host (" {0}" -f $tag) -ForegroundColor DarkGray
  985. } else {
  986. $fromStr = $g.First.ToString('yyyy-MM-dd HH:mm:ss')
  987. $toStr = $g.Last.ToString('yyyy-MM-dd HH:mm:ss')
  988. $duration = $g.Last - $g.First
  989. if ($duration.TotalHours -ge 1) {
  990. $durStr = "{0:0}h {1}min" -f [Math]::Floor($duration.TotalHours), $duration.Minutes
  991. } elseif ($duration.TotalMinutes -ge 1) {
  992. $durStr = "{0}min" -f [Math]::Ceiling($duration.TotalMinutes)
  993. } else {
  994. $durStr = "{0}sec" -f [Math]::Ceiling($duration.TotalSeconds)
  995. }
  996.  
  997. Write-Host (" {0,2}. [{1}x] {2,-6} {3,-25} <- {4}" -f $idx, $g.Count, $ltShort, $g.User, $g.Source) -ForegroundColor $color
  998. Write-Host (" {0}" -f $tag) -ForegroundColor DarkGray
  999. Write-Host (" from {0} to {1} ({2})" -f $fromStr, $toStr, $durStr) -ForegroundColor DarkGray
  1000. }
  1001. }
  1002.  
  1003. Write-Host ''
  1004. $totalRaw = ($shown | Measure-Object -Property Count -Sum).Sum
  1005. Write-Host " Showing $($shown.Count) groups ($totalRaw events) of $($events.Count) total. Grouping threshold: ${interval}min." -ForegroundColor DarkGray
  1006. Show-Meaning $events
  1007. Write-Host ''
  1008. Show-Tips
  1009. }
  1010.  
  1011. Show-Footer
  1012.