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