Dieses Skript dient dazu sämtliche logins aus den Eventlogs aller Domaincontroller zu lesen. Man kann die Domaincontroller, den Benutzer, die Zeit, sowie die Anzahl der Events restriktieren und das Ganze in eine CSV exportieren. Die Ansicht wird per default als Tabelle dargestellt, lässt sich jedoch mit dem Parameter „-ViewType“ leicht auf „Grid-View“ oder „None“ umstellen. „Non“ heißt hier ohne Tabellen Formatierung.
Das Skript zeigt alle Anmeldungen von einem Domaincontroller, also folgende Anmeldeformen:
"Interaktiv"
"Netzwerk"
"Batch"
"Dienst"
"Entsperrt"
"NetworkCleartext"
"NewCredentials"
"RemoteInteraktiv"
"CachedInteraktiv"
PowerShell
# Standardansicht für einen bestimmten Benutzer
Get-UserLoginHistory -Username "Samaaccountname"
# Interaktive GridView-Anzeige
Get-UserLoginHistory -ViewType Grid
# Auf einzelnem DC mit höherem Limit für mehr Ergebnisse
Get-UserLoginHistory -DomainControllers "domaincontroller" -MaxEvents 5000 -ViewType Full
# Schnellerer Export ohne Anzeige
Get-UserLoginHistory -Hours 48 -ExportToCsv -ViewType None
Das Skript wird durch „Runspace Pools“ parallel verarbeitet. Zur Vereinfachung könnte man hier auch mit Powershell-Jobs arbeiten, falls es einem lieber ist.
PowerShell
function Get-UserLoginHistory {
<#
.SYNOPSIS
Ermittelt den Anmeldeverlauf von Benutzern aus den Sicherheitsprotokollen der Domain-Controller.
.DESCRIPTION
Diese Funktion durchsucht die Sicherheitsprotokolle aller oder bestimmter Domain-Controller
nach Anmeldeereignissen (4624, 4625) und zeigt Details zu diesen Anmeldungen in einer übersichtlichen Form an.
Die Funktion wurde für bessere Performance optimiert.
.PARAMETER Username
Optional. Der Benutzername, nach dem gesucht werden soll. Kann im Format "Benutzername",
"Domain\Benutzername" oder "Benutzername@domain.com" angegeben werden.
.PARAMETER DomainControllers
Optional. Eine Liste von Domain-Controllern, die durchsucht werden sollen.
Wenn nicht angegeben, werden alle Domain-Controller der aktuellen Domäne durchsucht.
.PARAMETER Hours
Optional. Die Anzahl der Stunden in der Vergangenheit, die durchsucht werden sollen.
Standard: 24 Stunden
.PARAMETER MaxEvents
Optional. Die maximale Anzahl von Ereignissen, die pro Domain-Controller abgerufen werden sollen.
Standard: 1000 Ereignisse
.PARAMETER ExportToCsv
Optional. Exportiert die Ergebnisse in eine CSV-Datei.
.PARAMETER LogFilePath
Optional. Pfad zur CSV-Datei, wenn ExportToCsv aktiviert ist.
Standard: "C:\Logs\UserLoginHistory.csv"
.PARAMETER ViewType
Optional. Art der Anzeige der Ergebnisse:
- Table: Kompakte Tabellendarstellung (Standard)
- Full: Ausführliche Tabellendarstellung
- Grid: Anzeige im Out-GridView-Dialog (interaktiv)
- None: Keine Anzeige, nur Rückgabe der Objekte
.EXAMPLE
Get-UserLoginHistory -Username "mustermann"
Zeigt die Anmeldungen des Benutzers "mustermann" der letzten 24 Stunden an.
.EXAMPLE
Get-UserLoginHistory -Username "mustermann" -Hours 48 -MaxEvents 5000 -ViewType Grid
Zeigt die Anmeldungen des Benutzers "mustermann" der letzten 48 Stunden im GridView-Dialog an.
.EXAMPLE
Get-UserLoginHistory -DomainControllers "DC01.domain.com" -ExportToCsv
Zeigt Anmeldungen vom angegebenen Domain-Controller und exportiert sie in eine CSV-Datei.
.NOTES
Erfordert entsprechende Berechtigungen zum Lesen der Sicherheitsprotokolle der Domain-Controller.
Dateiname: Get-UserLoginHistory.ps1
Autor: Ismahil Ahmed
Erstelldatum: 12.02.2025
Version: 1.6
#>
[CmdletBinding()]
param (
[Parameter(Mandatory=$false, Position=0)]
[string]$Username,
[Parameter(Mandatory=$false)]
[string[]]$DomainControllers,
[Parameter(Mandatory=$false)]
[int]$Hours = 24,
[Parameter(Mandatory=$false)]
[int]$MaxEvents = 1000,
[Parameter(Mandatory=$false)]
[switch]$ExportToCsv,
[Parameter(Mandatory=$false)]
[string]$LogFilePath = "C:\Logs\UserLoginHistory.csv",
[Parameter(Mandatory=$false)]
[switch]$IncludeFailedLogins = $true,
[Parameter(Mandatory=$false)]
[ValidateSet("Table", "Full", "Grid", "None")]
[string]$ViewType = "Table"
)
begin {
# Optimierung: Verwende RunspacePool für parallele Verarbeitung
$runspacePool = [runspacefactory]::CreateRunspacePool(1, [Environment]::ProcessorCount)
$runspacePool.Open()
$runspaces = @()
$scriptBlock = {
param (
[string]$DomainController,
[datetime]$StartTime,
[datetime]$EndTime,
[int[]]$EventIDs,
[int]$MaxEventsToGet,
[string]$UserFilter,
[string]$UserDomain,
[string]$UserPrincipalName
)
# Ergebnisstruktur
$results = @{
DC = $DomainController
Success = $false
Events = @()
Error = $null
EventCount = 0
ProcessedCount = 0
MatchedCount = 0
}
try {
# Teste Verbindung
if (-not (Test-Connection -ComputerName $DomainController -Count 1 -Quiet)) {
$results.Error = "Domain-Controller nicht erreichbar"
return $results
}
# Erstelle FilterHashtable
$filter = @{
LogName = 'Security'
ID = $EventIDs
StartTime = $StartTime
EndTime = $EndTime
}
# Versuche Zugriff zu testen
try {
$null = Get-WinEvent -ComputerName $DomainController -LogName Security -MaxEvents 1 -ErrorAction Stop
}
catch {
$results.Error = "Zugriff auf Sicherheitsprotokolle nicht möglich: $($_.Exception.Message)"
return $results
}
# Hauptabfrage - OPTIMIERT durch limitierte Feldabfrage
$events = Get-WinEvent -ComputerName $DomainController -FilterHashtable $filter -MaxEvents $MaxEventsToGet -ErrorAction Stop
$results.EventCount = $events.Count
$results.Success = $true
# Verarbeite Ereignisse - OPTIMIERT durch selektive Verarbeitung
foreach ($event in $events) {
$results.ProcessedCount++
try {
$eventXml = [xml]$event.ToXml()
$eventData = $eventXml.Event.EventData.Data
# Extrahiere Benutzernamen
$targetUsername = ($eventData | Where-Object { $_.Name -eq 'TargetUserName' }).'#text'
$targetDomain = ($eventData | Where-Object { $_.Name -eq 'TargetDomainName' }).'#text'
# Benutzernamen vergleichen, wenn ein Benutzerfilter angegeben ist
$matchUser = $true
if ($UserFilter) {
$matchUser = $false
# Vereinfachte Matching-Strategien für bessere Performance
if ($targetUsername -eq $UserFilter) {
$matchUser = $true
}
elseif ($UserDomain -and ($targetDomain -like "*$UserDomain*") -and ($targetUsername -eq $UserFilter)) {
$matchUser = $true
}
elseif ($UserPrincipalName -and ("$targetUsername@$targetDomain" -like "*$UserPrincipalName*")) {
$matchUser = $true
}
}
# Nur Benutzeranmeldungen (keine Systemkonten)
if ($matchUser -and
$targetUsername -notmatch '^\$' -and
$targetUsername -ne 'ANONYMOUS LOGON' -and
$targetUsername -ne 'SYSTEM' -and
$targetUsername -ne 'LOCAL SERVICE' -and
$targetUsername -ne 'NETWORK SERVICE') {
$results.MatchedCount++
# Extrahiere nur die minimal notwendigen Informationen
$ipAddress = ($eventData | Where-Object { $_.Name -eq 'IpAddress' }).'#text'
if ([string]::IsNullOrEmpty($ipAddress) -or $ipAddress -eq '-') {
$ipAddress = "Lokal"
}
$workstation = ($eventData | Where-Object { $_.Name -eq 'WorkstationName' }).'#text'
if ([string]::IsNullOrEmpty($workstation)) {
$workstation = "Unbekannt"
}
$logonType = ($eventData | Where-Object { $_.Name -eq 'LogonType' }).'#text'
$logonTypeDesc = switch ($logonType) {
2 { "Interaktiv" }
3 { "Netzwerk" }
4 { "Batch" }
5 { "Dienst" }
7 { "Entsperrt" }
8 { "NetworkCleartext" }
9 { "NewCredentials" }
10 { "RemoteInteraktiv" }
11 { "CachedInteraktiv" }
default { "Typ $logonType" }
}
$results.Events += [PSCustomObject]@{
Zeitstempel = $event.TimeCreated
DC = $DomainController
Benutzername = "$targetDomain\$targetUsername"
IPAdresse = $ipAddress
Computername = $workstation
EreignisID = $event.Id
Status = if ($event.Id -eq 4624) { "Erfolgreich" } else { "Fehlgeschlagen" }
AnmeldungsTyp = $logonTypeDesc
}
}
}
catch {
# Ignoriere einzelne Fehler bei der Ereignisverarbeitung
continue
}
}
}
catch {
if ($_.Exception.Message -like "*Es wurden keine Ereignisse gefunden*") {
$results.Success = $true
$results.EventCount = 0
}
else {
$results.Error = $_.Exception.Message
}
}
return $results
}
# Erstellen des Log-Verzeichnisses, falls es nicht existiert und Export aktiviert ist
if ($ExportToCsv) {
$logDir = Split-Path $LogFilePath -Parent
if (-not (Test-Path $logDir)) {
New-Item -ItemType Directory -Path $logDir -Force | Out-Null
}
}
# Funktion zum Abrufen von Domain-Controller-Namen
function Get-DCList {
try {
# Verwende Get-ADDomainController falls RSAT-Tools installiert sind
if (Get-Command Get-ADDomainController -ErrorAction SilentlyContinue) {
return (Get-ADDomainController -Filter * | Select-Object -ExpandProperty HostName)
} else {
# Fallback-Methode
$domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()
return $domain.DomainControllers | ForEach-Object { $_.Name }
}
} catch {
Write-Warning "Fehler beim Abrufen der Domain-Controller: $_"
return $null
}
}
# Zeitraum berechnen
$endTime = Get-Date
$startTime = $endTime.AddHours(-$Hours)
Write-Host "Suche nach Anmeldeereignissen zwischen $($startTime.ToString('yyyy-MM-dd HH:mm:ss')) und $($endTime.ToString('yyyy-MM-dd HH:mm:ss'))" -ForegroundColor Cyan
# Ereignis-IDs festlegen
if ($IncludeFailedLogins) {
$eventIDs = @(4624, 4625) # Erfolgreiche und fehlgeschlagene Anmeldungen
} else {
$eventIDs = @(4624) # Nur erfolgreiche Anmeldungen
}
# Wenn keine Domain-Controller angegeben wurden, versuche, sie abzurufen
if (-not $DomainControllers) {
$DomainControllers = Get-DCList
if (-not $DomainControllers) {
Write-Error "Keine Domain-Controller gefunden. Bitte gib explizit Domain-Controller an."
return
}
}
Write-Host "Domain-Controller: $($DomainControllers -join ', ')" -ForegroundColor Cyan
# Benutzernamenformat verarbeiten
$userSamAccountName = $null
$userDomain = $null
$userPrincipalName = $null
if ($Username) {
Write-Host "Suche nach Benutzername: $Username" -ForegroundColor Cyan
# Prüfe verschiedene Formate
if ($Username -match '^(.+)\\(.+)$') {
# Format: DOMAIN\username
$userDomain = $Matches[1]
$userSamAccountName = $Matches[2]
Write-Host " Format erkannt: DOMAIN\username (Domäne: $userDomain, Benutzername: $userSamAccountName)" -ForegroundColor Yellow
}
elseif ($Username -match '^(.+)@(.+)$') {
# Format: username@domain.com
$userSamAccountName = $Matches[1]
$userDomain = $Matches[2]
$userPrincipalName = $Username
Write-Host " Format erkannt: username@domain.com (Benutzername: $userSamAccountName, Domäne: $userDomain)" -ForegroundColor Yellow
}
else {
# Format: username
$userSamAccountName = $Username
Write-Host " Format erkannt: Nur Benutzername (Benutzername: $userSamAccountName)" -ForegroundColor Yellow
# Versuche, den vollständigen Benutzernamen aus Active Directory zu bekommen
if (Get-Command Get-ADUser -ErrorAction SilentlyContinue) {
try {
$adUser = Get-ADUser -Identity $userSamAccountName -Properties UserPrincipalName, DistinguishedName
if ($adUser) {
$userPrincipalName = $adUser.UserPrincipalName
$userDomain = ($adUser.DistinguishedName -split ',DC=')[1]
Write-Host " AD-Benutzerinformationen gefunden: $($adUser.DistinguishedName)" -ForegroundColor Green
}
}
catch {
Write-Warning " Konnte keine AD-Benutzerinformationen für '$userSamAccountName' finden: $_"
}
}
}
}
}
process {
# Starte die parallele Verarbeitung der Domain-Controller
Write-Host "`nStarte parallele Abfrage von $($DomainControllers.Count) Domain-Controllern..." -ForegroundColor Yellow
foreach ($dc in $DomainControllers) {
$runspaceParams = @{
DomainController = $dc
StartTime = $startTime
EndTime = $endTime
EventIDs = $eventIDs
MaxEventsToGet = $MaxEvents
UserFilter = $userSamAccountName
UserDomain = $userDomain
UserPrincipalName = $userPrincipalName
}
$runspace = [powershell]::Create().AddScript($scriptBlock).AddParameters($runspaceParams)
$runspace.RunspacePool = $runspacePool
$runspaces += [PSCustomObject]@{
Runspace = $runspace
Handle = $runspace.BeginInvoke()
DC = $dc
Completed = $false
}
}
# Erstelle eine ArrayList für die Ergebnisse
$allResults = New-Object System.Collections.ArrayList
# Überwache den Fortschritt der Runspaces
$completed = 0
$total = $runspaces.Count
while ($runspaces | Where-Object { -not $_.Completed }) {
foreach ($item in $runspaces | Where-Object { -not $_.Completed }) {
if ($item.Handle.IsCompleted) {
$results = $item.Runspace.EndInvoke($item.Handle)
$item.Completed = $true
$completed++
# Status anzeigen
if ($results.Success) {
Write-Host "Domain-Controller $($item.DC): $($results.MatchedCount) von $($results.EventCount) Ereignissen gefunden" -ForegroundColor Green
} else {
Write-Host "Domain-Controller $($item.DC): Fehler - $($results.Error)" -ForegroundColor Red
}
# Ergebnisse zur Gesamtliste hinzufügen
if ($results.Events.Count -gt 0) {
[void]$allResults.AddRange($results.Events)
}
# Runspace bereinigen
$item.Runspace.Dispose()
}
}
# Fortschrittsanzeige aktualisieren
Write-Progress -Activity "Abfrage von Domain-Controllern" -Status "$completed von $total abgeschlossen" -PercentComplete (($completed / $total) * 100)
# Kurze Pause für CPU-Entlastung
Start-Sleep -Milliseconds 100
}
Write-Progress -Activity "Abfrage von Domain-Controllern" -Completed
# Sortiere die Ergebnisse nach Zeitstempel (neueste zuerst)
$sortedResults = $allResults | Sort-Object -Property Zeitstempel -Descending
# Gib Statistiken aus
$resultCount = $sortedResults.Count
Write-Host "`nGefundene Anmeldungen: $resultCount" -ForegroundColor Cyan
# Exportiere zu CSV, falls gewünscht
if ($ExportToCsv -and $resultCount -gt 0) {
$sortedResults | Export-Csv -Path $LogFilePath -NoTypeInformation -Encoding UTF8
Write-Host "Ergebnisse wurden in $LogFilePath gespeichert." -ForegroundColor Green
}
# Zeige die Ergebnisse je nach ViewType an
if ($resultCount -gt 0) {
switch ($ViewType) {
"Table" {
# Kompakte Tabellendarstellung mit ausgewählten Spalten
$displayCount = [Math]::Min(20, $resultCount)
Write-Host "Zeige die neuesten $displayCount Anmeldungen in Tabellenform:" -ForegroundColor Cyan
# Format-Table mit benutzerdefinierter Formatierung für bessere Lesbarkeit
$sortedResults | Select-Object -First $displayCount |
Format-Table -Property @{
Label = "Zeitpunkt";
Expression = { $_.Zeitstempel.ToString("yyyy-MM-dd HH:mm:ss") };
Width = 19
},
@{
Label = "Benutzer";
Expression = { ($_.Benutzername -split '\\')[-1] };
Width = 15
},
@{
Label = "Status";
Expression = {
if ($_.Status -eq "Erfolgreich") {
"✓"
} else {
"✗"
}
};
Width = 6
},
@{
Label = "Anmeldungstyp";
Expression = { $_.AnmeldungsTyp };
Width = 15
},
@{
Label = "IP-Adresse";
Expression = { $_.IPAdresse };
Width = 15
},
@{
Label = "Computer";
Expression = { $_.Computername };
Width = 15
},
@{
Label = "Domain-Controller";
Expression = { ($_.DC -split '\.')[0] };
Width = 15
}
if ($resultCount -gt 20) {
Write-Host "... und $($resultCount - 20) weitere Anmeldungen." -ForegroundColor Yellow
Write-Host "Vollständige Anzeige mit 'Get-UserLoginHistory -ViewType Grid' oder 'Get-UserLoginHistory -ViewType Full'" -ForegroundColor Yellow
}
# Bei Table-Ansicht keine Objekte zurückgeben
return
}
"Full" {
# Ausführliche Tabellendarstellung mit allen Spalten
Write-Host "Zeige die Anmeldungen in ausführlicher Tabellenform:" -ForegroundColor Cyan
$sortedResults | Format-Table -AutoSize
# Bei Full-Ansicht keine Objekte zurückgeben
return
}
"Grid" {
# GridView-Darstellung (interaktiv)
Write-Host "Öffne interaktives GridView-Fenster mit den Ergebnissen..." -ForegroundColor Cyan
$sortedResults | Out-GridView -Title "Benutzeranmeldungen ($resultCount Einträge)" -PassThru
# Bei Grid-Ansicht keine Objekte zurückgeben
return
}
"None" {
# Keine Anzeige, nur Rückgabe der Objekte
Write-Host "Keine Anzeige ausgewählt. Die Ergebnisse sind als Objekte verfügbar." -ForegroundColor Yellow
}
}
}
else {
Write-Host "Keine Anmeldungen gefunden, die den Filterkriterien entsprechen." -ForegroundColor Yellow
}
# Gib die Ergebnisse nur zurück, wenn ViewType "None" ist oder keine Ergebnisse gefunden wurden
# Bei anderen ViewTypes wurde die Rückgabe bereits mit "return" beendet
return $sortedResults
}
end {
# Runspace-Pool schließen
$runspacePool.Close()
$runspacePool.Dispose()
}
}