<# .SYNOPSIS GUI tool to export all members of an AD group (including nested groups) to a CSV file. .PARAMETER LogPath Optional path for the log file. Defaults to script directory with a timestamp. #> param ( [string]$LogPath = (Join-Path $PSScriptRoot ("export_log_" + (Get-Date -Format 'yyyyMMdd_HHmmss') + ".txt")) ) Add-Type -AssemblyName System.Windows.Forms Add-Type -AssemblyName System.Drawing [System.Windows.Forms.Application]::EnableVisualStyles() # --------------------------------------------------------------------------- # Ensure AD module is loaded — called lazily before first AD operation # --------------------------------------------------------------------------- function Initialize-ADModule { if (Get-Module -Name ActiveDirectory) { return $true } if (-not (Get-Module -Name ActiveDirectory -ListAvailable)) { $isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole( [Security.Principal.WindowsBuiltInRole]::Administrator) if (-not $isAdmin) { [System.Windows.Forms.MessageBox]::Show( "Das ActiveDirectory-Modul wurde nicht gefunden.`nBitte das Skript als Administrator starten.", "Modul fehlt", "OK", "Error") | Out-Null return $false } try { $os = (Get-CimInstance Win32_OperatingSystem).Caption if ($os -match "Server") { Import-Module ServerManager -ErrorAction Stop Add-WindowsFeature RSAT-AD-PowerShell -ErrorAction Stop | Out-Null } else { $feature = Get-WindowsCapability -Name "Rsat.ActiveDirectory*" -Online -ErrorAction Stop | Where-Object State -ne Installed | Select-Object -First 1 if ($feature) { Add-WindowsCapability -Name $feature.Name -Online -ErrorAction Stop | Out-Null } } } catch { [System.Windows.Forms.MessageBox]::Show( "RSAT konnte nicht installiert werden:`n$_", "Fehler", "OK", "Error") | Out-Null return $false } } try { Import-Module ActiveDirectory -ErrorAction Stop -WarningAction SilentlyContinue return $true } catch { [System.Windows.Forms.MessageBox]::Show( "ActiveDirectory-Modul konnte nicht geladen werden:`n$_", "Fehler", "OK", "Error") | Out-Null return $false } } # --------------------------------------------------------------------------- # Script-scope state for the recursive export # --------------------------------------------------------------------------- $script:visitedGroups = $null $script:exportResults = $null $script:exportAdParams = $null # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- function Write-Log { param([string]$Message, [string]$Level = 'INFO') $entry = "[{0}] [{1}] {2}" -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Level, $Message $entry | Out-File -FilePath $LogPath -Append -Encoding UTF8 $color = switch ($Level) { 'ERROR' { [System.Drawing.Color]::Salmon } 'WARN' { [System.Drawing.Color]::Goldenrod } default { [System.Drawing.Color]::White } } Write-OutputLine $entry $color } function Write-OutputLine { param([string]$Text, [System.Drawing.Color]$Color = [System.Drawing.Color]::White) $script:rtbOutput.SelectionStart = $script:rtbOutput.TextLength $script:rtbOutput.SelectionLength = 0 $script:rtbOutput.SelectionColor = $Color $script:rtbOutput.AppendText("$Text`n") $script:rtbOutput.ScrollToCaret() [System.Windows.Forms.Application]::DoEvents() } function Get-NestedMembers { param([string]$Group, [string]$SourceGroup) if (-not $script:visitedGroups.Add($Group)) { return } try { $members = Get-ADGroupMember -Identity $Group -ErrorAction Stop @script:exportAdParams } catch { Write-Log "Gruppe '$Group' konnte nicht abgerufen werden: $_" -Level WARN return } foreach ($member in $members) { switch ($member.objectClass) { 'user' { try { $user = Get-ADUser -Identity $member.distinguishedName -Properties ` DisplayName, EmailAddress, Title, Department, Company, Enabled, LastLogonDate, PasswordLastSet, PasswordNeverExpires, Manager, telephoneNumber, DistinguishedName ` -ErrorAction Stop @script:exportAdParams $managerName = '' if ($user.Manager) { try { $mgr = Get-ADUser -Identity $user.Manager -Properties DisplayName ` -ErrorAction Stop @script:exportAdParams $managerName = $mgr.DisplayName } catch {} } $script:exportResults.Add([PSCustomObject]@{ SourceGroup = $SourceGroup MemberOfGroup = $Group SamAccountName = $user.SamAccountName DisplayName = $user.DisplayName GivenName = $user.GivenName Surname = $user.Surname EmailAddress = $user.EmailAddress Title = $user.Title Department = $user.Department Company = $user.Company TelephoneNumber = $user.telephoneNumber Enabled = $user.Enabled LastLogonDate = $user.LastLogonDate PasswordLastSet = $user.PasswordLastSet PasswordNeverExpires = $user.PasswordNeverExpires Manager = $managerName DistinguishedName = $user.DistinguishedName }) Write-Log " Benutzer: $($user.SamAccountName) ($($user.DisplayName))" } catch { Write-Log "Benutzer '$($member.SamAccountName)' konnte nicht abgerufen werden: $_" -Level WARN } } 'group' { Write-Log "Verschachtelte Gruppe: $($member.Name)" Get-NestedMembers -Group $member.distinguishedName -SourceGroup $SourceGroup } } } } function Start-Export { param([string]$GroupName, [string]$OutputPath, [string]$Server) $script:btnExport.Enabled = $false $script:rtbOutput.Clear() if (-not (Initialize-ADModule)) { $script:btnExport.Enabled = $true return } $script:exportAdParams = @{} if ($Server) { $script:exportAdParams['Server'] = $Server } try { $null = Get-ADGroup -Identity $GroupName -ErrorAction Stop @script:exportAdParams Write-Log "Gruppe gefunden: $GroupName" } catch { Write-Log "Gruppe '$GroupName' nicht gefunden: $_" -Level ERROR $script:btnExport.Enabled = $true return } $script:visitedGroups = [System.Collections.Generic.HashSet[string]]::new( [System.StringComparer]::OrdinalIgnoreCase) $script:exportResults = [System.Collections.Generic.List[PSCustomObject]]::new() Write-Log "Starte Export für '$GroupName' (inkl. verschachtelter Gruppen)..." Get-NestedMembers -Group $GroupName -SourceGroup $GroupName if ($script:exportResults.Count -eq 0) { Write-Log "Keine Benutzer in Gruppe '$GroupName' gefunden." -Level WARN $script:btnExport.Enabled = $true return } $unique = $script:exportResults | Sort-Object SamAccountName -Unique try { $unique | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8 -Delimiter ';' -ErrorAction Stop $msg = "Export abgeschlossen. Benutzer: {0} | Gruppen: {1} | Datei: {2}" -f ` $unique.Count, $script:visitedGroups.Count, $OutputPath Write-Log $msg Write-OutputLine $msg ([System.Drawing.Color]::LightGreen) } catch { Write-Log "Fehler beim Schreiben der CSV: $_" -Level ERROR } $script:btnExport.Enabled = $true } # --------------------------------------------------------------------------- # Form # --------------------------------------------------------------------------- $form = New-Object System.Windows.Forms.Form $form.Text = "AD Group Export" $form.Size = New-Object System.Drawing.Size(680, 600) $form.StartPosition = "CenterScreen" $form.FormBorderStyle = "FixedDialog" $form.MaximizeBox = $false $form.BackColor = [System.Drawing.Color]::FromArgb(30, 30, 30) $form.ForeColor = [System.Drawing.Color]::White $form.Font = New-Object System.Drawing.Font('Segoe UI', 9) $pad = 16 # --- Group row --- $lblGroup = New-Object System.Windows.Forms.Label $lblGroup.Text = "AD Gruppe:" $lblGroup.Location = New-Object System.Drawing.Point($pad, 20) $lblGroup.Size = New-Object System.Drawing.Size(84, 22) $form.Controls.Add($lblGroup) $cmbGroup = New-Object System.Windows.Forms.ComboBox $cmbGroup.Location = New-Object System.Drawing.Point(104, 18) $cmbGroup.Size = New-Object System.Drawing.Size(436, 22) $cmbGroup.BackColor = [System.Drawing.Color]::FromArgb(45, 45, 45) $cmbGroup.ForeColor = [System.Drawing.Color]::White $cmbGroup.FlatStyle = "Flat" $cmbGroup.AutoCompleteMode = "SuggestAppend" $cmbGroup.AutoCompleteSource = "ListItems" $form.Controls.Add($cmbGroup) $btnLoadGroups = New-Object System.Windows.Forms.Button $btnLoadGroups.Text = "Gruppen laden" $btnLoadGroups.Location = New-Object System.Drawing.Point(550, 16) $btnLoadGroups.Size = New-Object System.Drawing.Size(96, 26) $btnLoadGroups.BackColor = [System.Drawing.Color]::FromArgb(0, 122, 204) $btnLoadGroups.ForeColor = [System.Drawing.Color]::White $btnLoadGroups.FlatStyle = "Flat" $btnLoadGroups.FlatAppearance.BorderSize = 0 $form.Controls.Add($btnLoadGroups) $btnLoadGroups.Add_Click({ if (-not (Initialize-ADModule)) { return } $btnLoadGroups.Text = "Lädt..." $btnLoadGroups.Enabled = $false [System.Windows.Forms.Application]::DoEvents() try { $adP = @{} if ($txtServer.Tag) { $adP['Server'] = $txtServer.Tag } $groups = Get-ADGroup -Filter * @adP | Select-Object -ExpandProperty Name | Sort-Object $cmbGroup.Items.Clear() $cmbGroup.Items.AddRange($groups) Write-OutputLine "$($groups.Count) Gruppen geladen." ([System.Drawing.Color]::LightGreen) } catch { Write-OutputLine "Fehler beim Laden der Gruppen: $_" ([System.Drawing.Color]::Salmon) } $btnLoadGroups.Text = "Gruppen laden" $btnLoadGroups.Enabled = $true }) # --- CSV output row --- $lblOut = New-Object System.Windows.Forms.Label $lblOut.Text = "CSV Datei:" $lblOut.Location = New-Object System.Drawing.Point($pad, 58) $lblOut.Size = New-Object System.Drawing.Size(84, 22) $form.Controls.Add($lblOut) $txtOut = New-Object System.Windows.Forms.TextBox $txtOut.Location = New-Object System.Drawing.Point(104, 56) $txtOut.Size = New-Object System.Drawing.Size(436, 22) $txtOut.BackColor = [System.Drawing.Color]::FromArgb(45, 45, 45) $txtOut.ForeColor = [System.Drawing.Color]::White $txtOut.BorderStyle = "FixedSingle" $txtOut.Text = if ($PSScriptRoot) { Join-Path $PSScriptRoot "export.csv" } else { "export.csv" } $form.Controls.Add($txtOut) $btnBrowse = New-Object System.Windows.Forms.Button $btnBrowse.Text = "Speichern..." $btnBrowse.Location = New-Object System.Drawing.Point(550, 54) $btnBrowse.Size = New-Object System.Drawing.Size(96, 26) $btnBrowse.BackColor = [System.Drawing.Color]::FromArgb(0, 122, 204) $btnBrowse.ForeColor = [System.Drawing.Color]::White $btnBrowse.FlatStyle = "Flat" $btnBrowse.FlatAppearance.BorderSize = 0 $form.Controls.Add($btnBrowse) $btnBrowse.Add_Click({ $sfd = New-Object System.Windows.Forms.SaveFileDialog $sfd.Filter = "CSV files (*.csv)|*.csv|All files (*.*)|*.*" $sfd.Title = "CSV-Datei speichern" $sfd.FileName = "export.csv" if ($sfd.ShowDialog() -eq "OK") { $txtOut.Text = $sfd.FileName } }) # --- Server row --- $lblServer = New-Object System.Windows.Forms.Label $lblServer.Text = "DC (optional):" $lblServer.Location = New-Object System.Drawing.Point($pad, 96) $lblServer.Size = New-Object System.Drawing.Size(84, 22) $form.Controls.Add($lblServer) $txtServer = New-Object System.Windows.Forms.TextBox $txtServer.Location = New-Object System.Drawing.Point(104, 94) $txtServer.Size = New-Object System.Drawing.Size(436, 22) $txtServer.BackColor = [System.Drawing.Color]::FromArgb(45, 45, 45) $txtServer.BorderStyle = "FixedSingle" $txtServer.Tag = '' $txtServer.Text = 'Domänencontroller (leer = Standard)' $txtServer.ForeColor = [System.Drawing.Color]::Gray $form.Controls.Add($txtServer) $txtServer.Add_GotFocus({ if ($txtServer.Tag -eq '') { $txtServer.Text = '' $txtServer.ForeColor = [System.Drawing.Color]::White } }) $txtServer.Add_LostFocus({ $txtServer.Tag = $txtServer.Text.Trim() if ($txtServer.Tag -eq '') { $txtServer.Text = 'Domänencontroller (leer = Standard)' $txtServer.ForeColor = [System.Drawing.Color]::Gray } }) # --- Separator --- $sep = New-Object System.Windows.Forms.Panel $sep.Location = New-Object System.Drawing.Point($pad, 130) $sep.Size = New-Object System.Drawing.Size(632, 1) $sep.BackColor = [System.Drawing.Color]::FromArgb(70, 70, 70) $form.Controls.Add($sep) # --- Export button --- $script:btnExport = New-Object System.Windows.Forms.Button $script:btnExport.Text = "Export starten" $script:btnExport.Location = New-Object System.Drawing.Point($pad, 140) $script:btnExport.Size = New-Object System.Drawing.Size(632, 34) $script:btnExport.BackColor = [System.Drawing.Color]::FromArgb(16, 124, 16) $script:btnExport.ForeColor = [System.Drawing.Color]::White $script:btnExport.FlatStyle = "Flat" $script:btnExport.FlatAppearance.BorderSize = 0 $script:btnExport.Font = New-Object System.Drawing.Font('Segoe UI', 10, [System.Drawing.FontStyle]::Bold) $form.Controls.Add($script:btnExport) $script:btnExport.Add_Click({ $groupName = $cmbGroup.Text.Trim() $outputPath = $txtOut.Text.Trim() $server = $txtServer.Tag if (-not $groupName) { [System.Windows.Forms.MessageBox]::Show( "Bitte eine AD-Gruppe eingeben oder auswählen.", "Validierung", "OK", "Warning") | Out-Null return } if (-not $outputPath) { [System.Windows.Forms.MessageBox]::Show( "Bitte einen Speicherpfad für die CSV angeben.", "Validierung", "OK", "Warning") | Out-Null return } Start-Export -GroupName $groupName -OutputPath $outputPath -Server $server }) # --- Output panel --- $lblOutput = New-Object System.Windows.Forms.Label $lblOutput.Text = "Output:" $lblOutput.Location = New-Object System.Drawing.Point($pad, 186) $lblOutput.Size = New-Object System.Drawing.Size(80, 18) $form.Controls.Add($lblOutput) $script:rtbOutput = New-Object System.Windows.Forms.RichTextBox $script:rtbOutput.Location = New-Object System.Drawing.Point($pad, 206) $script:rtbOutput.Size = New-Object System.Drawing.Size(632, 320) $script:rtbOutput.BackColor = [System.Drawing.Color]::FromArgb(12, 12, 12) $script:rtbOutput.ForeColor = [System.Drawing.Color]::White $script:rtbOutput.Font = New-Object System.Drawing.Font('Consolas', 9) $script:rtbOutput.ReadOnly = $true $script:rtbOutput.BorderStyle = "None" $script:rtbOutput.ScrollBars = "Vertical" $form.Controls.Add($script:rtbOutput) # --- Status bar --- $lblStatus = New-Object System.Windows.Forms.Label $lblStatus.Text = "Log: $LogPath" $lblStatus.Location = New-Object System.Drawing.Point($pad, 536) $lblStatus.Size = New-Object System.Drawing.Size(632, 18) $lblStatus.ForeColor = [System.Drawing.Color]::Gray $lblStatus.Font = New-Object System.Drawing.Font('Segoe UI', 8) $form.Controls.Add($lblStatus) # --------------------------------------------------------------------------- $form.ShowDialog() | Out-Null $form.Dispose()