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