🛡️ ActiveDirectory-Get-UserLoginHistory

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()
  }
}

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert