diff --git a/Active-Directory/export_user_from_Group_to_CSV.ps1 b/Active-Directory/export_user_from_Group_to_CSV.ps1 index 6fe36dc..17f0e2e 100644 --- a/Active-Directory/export_user_from_Group_to_CSV.ps1 +++ b/Active-Directory/export_user_from_Group_to_CSV.ps1 @@ -9,55 +9,61 @@ param ( [string]$LogPath = (Join-Path $PSScriptRoot ("export_log_" + (Get-Date -Format 'yyyyMMdd_HHmmss') + ".txt")) ) -# --------------------------------------------------------------------------- -# Ensure ActiveDirectory module is available -# --------------------------------------------------------------------------- -if (-not (Get-Module -Name ActiveDirectory -ListAvailable)) { - $isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) - if (-not $isAdmin) { - Write-Error "ActiveDirectory module not found. Please run this script as Administrator to install it automatically, or install RSAT manually." - exit 1 - } - - Write-Host "ActiveDirectory module not found. Attempting to install..." -ForegroundColor Yellow - $os = (Get-CimInstance Win32_OperatingSystem).Caption - - if ($os -match "Server") { - try { - Import-Module ServerManager -ErrorAction Stop - Add-WindowsFeature RSAT-AD-PowerShell -ErrorAction Stop | Out-Null - Write-Host "ActiveDirectory module installed via Add-WindowsFeature." -ForegroundColor Green - } catch { - Write-Error "Failed to install ActiveDirectory module on Server: $_" - exit 1 - } - } else { - try { - $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 - Write-Host "ActiveDirectory module installed via Add-WindowsCapability." -ForegroundColor Green - } - } catch { - Write-Error "Failed to install ActiveDirectory module on Windows client: $_" - exit 1 - } - } -} - -try { - Import-Module ActiveDirectory -ErrorAction Stop -} catch { - Write-Error "ActiveDirectory module could not be loaded: $_" - exit 1 -} - 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 # --------------------------------------------------------------------------- @@ -84,18 +90,85 @@ function Write-OutputLine { [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() - $adParams = @{} - if ($Server) { $adParams['Server'] = $Server } + if (-not (Initialize-ADModule)) { + $script:btnExport.Enabled = $true + return + } + + $script:exportAdParams = @{} + if ($Server) { $script:exportAdParams['Server'] = $Server } - # Validate group try { - $null = Get-ADGroup -Identity $GroupName -ErrorAction Stop @adParams + $null = Get-ADGroup -Identity $GroupName -ErrorAction Stop @script:exportAdParams Write-Log "Gruppe gefunden: $GroupName" } catch { Write-Log "Gruppe '$GroupName' nicht gefunden: $_" -Level ERROR @@ -103,88 +176,27 @@ function Start-Export { return } - $visitedGroups = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) - $results = [System.Collections.Generic.List[PSCustomObject]]::new() + $script:visitedGroups = [System.Collections.Generic.HashSet[string]]::new( + [System.StringComparer]::OrdinalIgnoreCase) + $script:exportResults = [System.Collections.Generic.List[PSCustomObject]]::new() - function Get-NestedMembers { - param([string]$Group, [string]$SourceGroup) - - if (-not $visitedGroups.Add($Group)) { return } - - try { - $members = Get-ADGroupMember -Identity $Group -ErrorAction Stop @adParams - } 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 @adParams - - $managerName = '' - if ($user.Manager) { - try { - $mgr = Get-ADUser -Identity $user.Manager -Properties DisplayName -ErrorAction Stop @adParams - $managerName = $mgr.DisplayName - } catch {} - } - - $results.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)" -Level INFO - Get-NestedMembers -Group $member.distinguishedName -SourceGroup $SourceGroup - } - } - } - } - - Write-Log "Starte Export für Gruppe '$GroupName' (inkl. verschachtelter Gruppen)..." + Write-Log "Starte Export für '$GroupName' (inkl. verschachtelter Gruppen)..." Get-NestedMembers -Group $GroupName -SourceGroup $GroupName - if ($results.Count -eq 0) { + if ($script:exportResults.Count -eq 0) { Write-Log "Keine Benutzer in Gruppe '$GroupName' gefunden." -Level WARN $script:btnExport.Enabled = $true return } - $unique = $results | Sort-Object SamAccountName -Unique + $unique = $script:exportResults | Sort-Object SamAccountName -Unique try { $unique | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8 -Delimiter ';' -ErrorAction Stop - $summary = "Export abgeschlossen. Benutzer: {0} | Gruppen durchsucht: {1} | Datei: {2}" -f ` - $unique.Count, $visitedGroups.Count, $OutputPath - Write-Log $summary -Level INFO - Write-OutputLine $summary ([System.Drawing.Color]::LightGreen) + $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 } @@ -224,23 +236,24 @@ $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 = 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.Text.Trim()) { $adP['Server'] = $txtServer.Text.Trim() } + 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) @@ -252,7 +265,7 @@ $btnLoadGroups.Add_Click({ $btnLoadGroups.Enabled = $true }) -# --- Output file row --- +# --- CSV output row --- $lblOut = New-Object System.Windows.Forms.Label $lblOut.Text = "CSV Datei:" $lblOut.Location = New-Object System.Drawing.Point($pad, 58) @@ -265,27 +278,25 @@ $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 = (Join-Path $PSScriptRoot "export.csv") +$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 = 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 - } + $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 --- @@ -299,55 +310,58 @@ $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.ForeColor = [System.Drawing.Color]::White $txtServer.BorderStyle = "FixedSingle" +$txtServer.Tag = '' +$txtServer.Text = 'Domänencontroller (leer = Standard)' $txtServer.ForeColor = [System.Drawing.Color]::Gray -$txtServer.Text = "Domänencontroller (leer = Standard)" $form.Controls.Add($txtServer) $txtServer.Add_GotFocus({ - if ($txtServer.ForeColor -eq [System.Drawing.Color]::Gray) { + if ($txtServer.Tag -eq '') { $txtServer.Text = '' $txtServer.ForeColor = [System.Drawing.Color]::White } }) $txtServer.Add_LostFocus({ - if ([string]::IsNullOrWhiteSpace($txtServer.Text)) { - $txtServer.Text = "Domänencontroller (leer = Standard)" + $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 = 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 = 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) +$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 = if ($txtServer.ForeColor -ne [System.Drawing.Color]::Gray) { $txtServer.Text.Trim() } else { '' } + $server = $txtServer.Tag if (-not $groupName) { - [System.Windows.Forms.MessageBox]::Show("Bitte eine AD-Gruppe eingeben oder auswählen.", "Validierung", "OK", "Warning") | Out-Null + [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 + [System.Windows.Forms.MessageBox]::Show( + "Bitte einen Speicherpfad für die CSV angeben.", "Validierung", "OK", "Warning") | Out-Null return } @@ -361,24 +375,24 @@ $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 = 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" +$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 = 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) +$lblStatus.Font = New-Object System.Drawing.Font('Segoe UI', 8) $form.Controls.Add($lblStatus) # ---------------------------------------------------------------------------