Src/Private/Gui/Start-AsBuiltReportMSAD.ps1
|
using namespace GliderUI using namespace GliderUI.Avalonia using namespace GliderUI.Avalonia.Controls using namespace GliderUI.Avalonia.Platform.Storage using namespace GliderUI.Avalonia.Media function Start-AsBuiltReportMSAD { <# .SYNOPSIS GUI launcher for AsBuiltReport.Microsoft.AD — runs entirely in PowerShell 7. .DESCRIPTION A PowerShell 7.4+ desktop GUI (GliderUI / Avalonia) that collects connection, output and report options, then generates the Microsoft AD As-Built Report by calling New-AsBuiltReport directly — no child PS5.1 process required. .NOTES Requirements: PowerShell 7.4+ — to run this script GliderUI 0.2.0+ (auto-installed on first run) — Install-PSResource -Name GliderUI -Version 0.2.0 -Scope CurrentUser -TrustRepository AsBuiltReport.Core — Install-PSResource -Name AsBuiltReport.Core AsBuiltReport.Microsoft.AD — Install-PSResource -Name AsBuiltReport.Microsoft.AD #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Scope = 'Function')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '', Scope = 'Function')] [CmdletBinding()] param() if ($PSVersionTable.PSVersion.Major -lt 7 -or ($PSVersionTable.PSVersion.Major -eq 7 -and $PSVersionTable.PSVersion.Minor -lt 4)) { throw "Start-AsBuiltReportMSAD requires PowerShell 7.4+. Current version: $($PSVersionTable.PSVersion)" } $IsAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) if (-not $IsAdmin) { Write-Error -Message 'Please run the report with Run As Administrator priviledges.' break } # ── Bootstrap GliderUI ────────────────────────────────────────────────────── $requiredGliderUIVersion = [version]'0.2.0' if (-not (Get-Module -ListAvailable -Name GliderUI)) { Write-Host 'GliderUI not found — installing from PSGallery…' -ForegroundColor Cyan Install-PSResource -Name GliderUI -Version $requiredGliderUIVersion -Scope CurrentUser -TrustRepository } $gliderMod = Get-Module -ListAvailable -Name GliderUI | Sort-Object Version -Descending | Select-Object -First 1 if ($null -eq $gliderMod -or $gliderMod.Version -lt $requiredGliderUIVersion) { $found = if ($null -eq $gliderMod) { 'not installed' } else { "v$($gliderMod.Version)" } Write-Error ("GliderUI v{0} or later is required (found: {1}).`nInstall with: Install-PSResource -Name GliderUI -Version {0} -Scope CurrentUser -TrustRepository`nThen restart PowerShell." -f $requiredGliderUIVersion, $found) return } Import-Module GliderUI -Force # Thread-safe store shared between the main runspace and the report runspace $syncHash = [Hashtable]::Synchronized(@{ CancelRequested = $false IsBusy = $false }) # ── UI Helper Functions ───────────────────────────────────────────────────── function New-SectionTitle ([string]$Text) { $tb = [TextBlock]::new() $tb.Text = $Text $tb.FontSize = 13 $tb.FontWeight = 'SemiBold' $tb.Margin = '0,18,0,6' return $tb } function New-FormRow ([string]$Label, $Control, [int]$LabelWidth = 185) { $row = [StackPanel]::new() $row.Orientation = 'Horizontal' $row.Spacing = 10 $row.Margin = '0,3,0,3' $lbl = [TextBlock]::new() $lbl.Text = $Label $lbl.Width = $LabelWidth $lbl.VerticalAlignment = 'Center' $lbl.FontSize = 12 $row.Children.Add($lbl) $row.Children.Add($Control) return $row } function New-InlineLabel ([string]$Text) { $tb = [TextBlock]::new() $tb.Text = $Text $tb.VerticalAlignment = 'Center' $tb.Margin = '8,0,0,0' $tb.FontSize = 12 return $tb } # Wraps a password TextBox with an eye-toggle button. function New-PasswordRow ($PasswordTextBox) { $btn = [Button]::new() $btn.Content = '👁' $btn.Padding = '6,2,6,2' $btn.VerticalAlignment = 'Center' $btn.AddClick({ if ($PasswordTextBox.PasswordChar -eq [char]0) { $PasswordTextBox.PasswordChar = [char]'●' } else { $PasswordTextBox.PasswordChar = [char]0 } }.GetNewClosure()) $row = [StackPanel]::new() $row.Orientation = 'Horizontal' $row.Spacing = 6 $row.Children.Add($PasswordTextBox) $row.Children.Add($btn) return $row } function New-DrawerMenuItem ([string]$Title, [string]$IconGeometry, $Page, $NavigationPage) { $icon = [PathIcon]::new() $icon.Data = [Geometry]::Parse($IconGeometry) $textBlock = [TextBlock]::new() $textBlock.Text = $Title $textBlock.VerticalAlignment = 'Center' $panel = [StackPanel]::new() $panel.Orientation = 'Horizontal' $panel.Spacing = 8 $panel.Children.Add($icon) $panel.Children.Add($textBlock) $button = [Button]::new() $button.HorizontalAlignment = 'Stretch' $button.Padding = 12 $button.Background = [SolidColorBrush]::new([Colors]::Transparent, 1) $button.Content = $panel $button.AddClick({ param($argumentList) $targetPage, $navPage = $argumentList $navPage.ReplaceAsync($targetPage) | Out-Null }, @($Page, $NavigationPage)) return $button } # ── Connection Controls ───────────────────────────────────────────────────── $txtServer = [TextBox]::new() $txtServer.Width = 240 $txtServer.Watermark = 'dc01.contoso.com' $txtUser = [TextBox]::new() $txtUser.Width = 200 $txtUser.Watermark = 'DOMAIN\username or user@domain' $txtPass = [TextBox]::new() $txtPass.Width = 200 $txtPass.Watermark = 'Password' try { $txtPass.PasswordChar = [char]'●' } catch { Out-Null } # ── Saved Connections ─────────────────────────────────────────────────────── $savedConnPath = if ($IsWindows) { [System.IO.Path]::Combine($env:USERPROFILE, 'AsBuiltReport', 'MSAD-SavedConnections.json') } else { [System.IO.Path]::Combine($env:HOME, 'AsBuiltReport', 'MSAD-SavedConnections.json') } $loadSavedConns = { if (Test-Path $savedConnPath) { try { $raw = Get-Content -Path $savedConnPath -Raw -Encoding UTF8 | ConvertFrom-Json if ($null -eq $raw) { return @() } return @($raw) } catch { return @() } } return @() }.GetNewClosure() $saveSavedConns = { param ([array]$Connections) $dir = Split-Path $savedConnPath -Parent if (-not (Test-Path $dir)) { New-Item -Path $dir -ItemType Directory -Force | Out-Null } if ($Connections.Count -eq 0) { '[]' | Set-Content -Path $savedConnPath -Encoding UTF8 } else { $Connections | ConvertTo-Json -Depth 3 | Set-Content -Path $savedConnPath -Encoding UTF8 } }.GetNewClosure() $cboSavedConn = [ComboBox]::new() $cboSavedConn.Width = 262 $refreshSavedConnCombo = { $cboSavedConn.Items.Clear() foreach ($c in (& $loadSavedConns)) { $cboSavedConn.Items.Add("$($c.Server) ($($c.Username))") | Out-Null } }.GetNewClosure() & $refreshSavedConnCombo $cboSavedConn.AddSelectionChanged({ $idx = $cboSavedConn.SelectedIndex if ($idx -lt 0) { return } $conns = & $loadSavedConns if ($idx -ge $conns.Count) { return } $sel = $conns[$idx] $txtServer.Text = $sel.Server $txtUser.Text = $sel.Username $txtPass.Text = '' }) $btnSaveConn = [Button]::new() $btnSaveConn.Content = '💾 Save Connection' $btnSaveConn.AddClick({ $srv = $txtServer.Text.Trim() $usr = $txtUser.Text.Trim() if ([string]::IsNullOrWhiteSpace($srv) -or [string]::IsNullOrWhiteSpace($usr)) { $syncHash.lblConfigStatus.Text = '⚠ Enter a Domain Controller FQDN and username before saving.' return } $conns = [System.Collections.ArrayList]@() foreach ($c in (& $loadSavedConns)) { $conns.Add($c) | Out-Null } $dup = $conns | Where-Object { $_.Server -eq $srv -and $_.Username -eq $usr } if (-not $dup) { $conns.Add([PSCustomObject]@{ Server = $srv; Username = $usr }) | Out-Null & $saveSavedConns -Connections @($conns) & $refreshSavedConnCombo $syncHash.lblConfigStatus.Text = "✅ Connection saved: $srv ($usr)" } else { $syncHash.lblConfigStatus.Text = "ℹ Connection already exists: $srv ($usr)" } }) $btnDeleteConn = [Button]::new() $btnDeleteConn.Content = '🗑 Delete' $btnDeleteConn.AddClick({ $idx = $cboSavedConn.SelectedIndex if ($idx -lt 0) { $syncHash.lblConfigStatus.Text = '⚠ Select a saved connection to delete.' return } $conns = [System.Collections.ArrayList]@() foreach ($c in (& $loadSavedConns)) { $conns.Add($c) | Out-Null } if ($idx -ge $conns.Count) { return } $removed = $conns[$idx] $conns.RemoveAt($idx) & $saveSavedConns -Connections @($conns) $cboSavedConn.SelectedIndex = -1 & $refreshSavedConnCombo $syncHash.lblConfigStatus.Text = "🗑 Deleted: $($removed.Server) ($($removed.Username))" }) $savedConnActionsRow = [StackPanel]::new() $savedConnActionsRow.Orientation = 'Horizontal' $savedConnActionsRow.Spacing = 6 $savedConnActionsRow.Children.Add($btnSaveConn) $savedConnActionsRow.Children.Add($btnDeleteConn) # ── Output Controls ───────────────────────────────────────────────────────── $chkHTML = [CheckBox]::new(); $chkHTML.Content = 'HTML'; $chkHTML.IsChecked = $true $chkWord = [CheckBox]::new(); $chkWord.Content = 'Word'; $chkWord.IsChecked = $false $chkText = [CheckBox]::new(); $chkText.Content = 'Text'; $chkText.IsChecked = $false $fmtPanel = [StackPanel]::new() $fmtPanel.Orientation = 'Horizontal' $fmtPanel.Spacing = 20 $fmtPanel.Children.Add($chkHTML) $fmtPanel.Children.Add($chkWord) $fmtPanel.Children.Add($chkText) $txtOutput = [TextBox]::new() $txtOutput.Width = 240 $txtOutput.Text = if ($IsWindows) { [System.IO.Path]::Combine($env:USERPROFILE, 'Documents', 'AsBuiltReport') } else { [System.IO.Path]::Combine($env:HOME, 'AsBuiltReport') } $btnBrowse = [Button]::new() $btnBrowse.Content = 'Browse…' $btnBrowse.AddClick({ try { $btnBrowse.IsEnabled = $false $storageProvider = [Window]::GetTopLevel($btnBrowse).StorageProvider if ($null -eq $storageProvider) { Write-Host 'Storage provider not available.' -ForegroundColor Yellow return } $options = [FolderPickerOpenOptions]::new() $options.Title = 'Select Output Folder Path' $folders = $storageProvider.OpenFolderPickerAsync($options).WaitForCompleted() if ($folders -and $folders.Count -gt 0) { $txtOutput.Text = $folders[0].Path.LocalPath } } catch { Write-Host "Folder picker error: $_" -ForegroundColor Red } finally { $btnBrowse.IsEnabled = $true } }) $outputPathRow = [StackPanel]::new() $outputPathRow.Orientation = 'Horizontal' $outputPathRow.Spacing = 8 $outputPathRow.Children.Add($txtOutput) $outputPathRow.Children.Add($btnBrowse) $cboLang = [ComboBox]::new() $cboLang.Width = 100 $cboLang.Items.Add('en-US') | Out-Null $cboLang.Items.Add('es-ES') | Out-Null $cboLang.SelectedIndex = 0 # ── Report Name ───────────────────────────────────────────────────────────── $txtReportName = [TextBox]::new() $txtReportName.Width = 300 $txtReportName.Text = 'Microsoft Active Directory As Built Report' $txtReportName.Watermark = 'Output filename (without extension)' # ── Options Controls ──────────────────────────────────────────────────────── # Options matching AsBuiltReport.Microsoft.AD.json > Options $swDiagrams = [ToggleSwitch]::new(); $swDiagrams.IsChecked = $true $swExportDiagrams = [ToggleSwitch]::new(); $swExportDiagrams.IsChecked = $true $swTimestamp = [ToggleSwitch]::new(); $swTimestamp.IsChecked = $false $swWinRMSSL = [ToggleSwitch]::new(); $swWinRMSSL.IsChecked = $false $swWinRMFallback = [ToggleSwitch]::new(); $swWinRMFallback.IsChecked = $true $cboDiagramTheme = [ComboBox]::new() $cboDiagramTheme.Width = 120 @('White', 'Black', 'Neon') | ForEach-Object { $cboDiagramTheme.Items.Add($_) | Out-Null } $cboDiagramTheme.SelectedIndex = 0 $cboPSDefaultAuth = [ComboBox]::new() $cboPSDefaultAuth.Width = 160 @('Negotiate', 'Kerberos', 'NTLM', 'Default') | ForEach-Object { $cboPSDefaultAuth.Items.Add($_) | Out-Null } $cboPSDefaultAuth.SelectedIndex = 0 # ── InfoLevel Controls — matching AsBuiltReport.Microsoft.AD.json > InfoLevel ─ function New-LevelCombo { $cbo = [ComboBox]::new() $cbo.Width = 160 @('0 - Off', '1 - Enabled', '2 - Adv Summary', '3 - Detailed') | ForEach-Object { $cbo.Items.Add($_) | Out-Null } $cbo.SelectedIndex = 1 return $cbo } $cboLvlForest = New-LevelCombo; $cboLvlForest.SelectedIndex = 2 # default 2 per JSON $cboLvlDomain = New-LevelCombo; $cboLvlDomain.SelectedIndex = 2 # default 2 per JSON $cboLvlDNS = New-LevelCombo; $cboLvlDNS.SelectedIndex = 1 # default 1 per JSON # ── Progress Bar & Log ────────────────────────────────────────────────────── $progressBar = [ProgressBar]::new() $progressBar.IsIndeterminate = $true $progressBar.IsVisible = $false $progressBar.Margin = '0,8,0,4' $syncHash.progressBar = $progressBar $txtLog = [TextBox]::new() $txtLog.IsReadOnly = $true $txtLog.AcceptsReturn = $true $txtLog.Height = 220 $txtLog.FontSize = 16 $txtLog.TextWrapping = 'Wrap' $txtLog.Watermark = 'Output log will appear here…' try { $txtLog.FontFamily = 'Consolas,Courier New,Monospace' } catch { Out-Null } $syncHash.txtLog = $txtLog $chkVerbose = [CheckBox]::new() $chkVerbose.Content = '🔍Verbose' $chkVerbose.IsChecked = $false $chkVerbose.HorizontalAlignment = 'Right' $chkVerbose.VerticalAlignment = 'Center' $chkVerbose.Margin = '0,0,8,0' $syncHash.chkVerbose = $chkVerbose # ── Action Buttons ────────────────────────────────────────────────────────── $btnCancel = [Button]::new() $btnCancel.Content = '✕ Cancel' $btnCancel.IsVisible = $false $btnCancel.Margin = '0,0,0,0' $btnCancel.AddClick({ $syncHash.CancelRequested = $true $rps = $syncHash.reportPS if ($null -ne $rps) { $rps.Stop() } }) $syncHash.btnCancel = $btnCancel $btnExportLog = [Button]::new() $btnExportLog.Content = '💾 Export Log' $btnExportLog.Margin = '0,0,0,0' $btnExportLog.AddClick({ try { $btnExportLog.IsEnabled = $false $logText = $syncHash.txtLog.Text if ([string]::IsNullOrWhiteSpace($logText)) { $syncHash.lblConfigStatus.Text = '⚠ Log is empty — nothing to export.' return } $storageProvider = [Window]::GetTopLevel($btnExportLog).StorageProvider if ($null -eq $storageProvider) { return } $saveOpts = [FilePickerSaveOptions]::new() $saveOpts.Title = 'Export Output Log' $saveOpts.SuggestedFileName = "MSAD-AsBuiltReport-$(Get-Date -Format 'yyyyMMdd-HHmmss').log" $file = $storageProvider.SaveFilePickerAsync($saveOpts).WaitForCompleted() if ($null -ne $file) { $logText | Set-Content -Path $file.Path.LocalPath -Encoding UTF8 $syncHash.lblConfigStatus.Text = "✅ Log exported: $(Split-Path $file.Path.LocalPath -Leaf)" } } catch { $syncHash.lblConfigStatus.Text = "❌ Log export failed: $_" } finally { $btnExportLog.IsEnabled = $true } }) $btnGenerate = [Button]::new() $btnGenerate.Content = '▶ Generate Report' $btnGenerate.HorizontalAlignment = 'Stretch' $btnGenerate.HorizontalContentAlignment = 'Center' $btnGenerate.FontSize = 14 $btnGenerate.FontWeight = 'SemiBold' $btnGenerate.Margin = '0,22,0,0' $btnGenerate.Classes.Add('accent') $syncHash.btnGenerate = $btnGenerate # ── Generate Callback ──────────────────────────────────────────────────────── $generateCallback = [EventCallback]::new() $generateCallback.RunspaceMode = 'RunspacePoolAsyncUI' $generateCallback.DisabledControlsWhileProcessing = $btnGenerate $generateCallback.ArgumentList = @{ SyncHash = $syncHash Server = $txtServer Username = $txtUser Password = $txtPass ReportName = $txtReportName OutPath = $txtOutput FmtHTML = $chkHTML FmtWord = $chkWord FmtText = $chkText Lang = $cboLang DiagramTheme = $cboDiagramTheme PSDefaultAuth = $cboPSDefaultAuth Diagrams = $swDiagrams ExportDiagrams = $swExportDiagrams Timestamp = $swTimestamp WinRMSSL = $swWinRMSSL WinRMFallback = $swWinRMFallback LvlForest = $cboLvlForest LvlDomain = $cboLvlDomain LvlDNS = $cboLvlDNS Verbose = $chkVerbose # ConfigPath and AbrConfigPath are late-bound below after TextBox creation } $generateCallback.ScriptBlock = { param ($ui) $sh = $ui.SyncHash if ($sh.IsBusy) { $sh.lblConfigStatus.Text = '⚠ Another operation is already running. Please wait.' return } $sh.IsBusy = $true $sh.CancelRequested = $false $sh.progressBar.IsVisible = $true $sh.btnCancel.IsVisible = $true $sh.txtLog.Text = '' $verboseEnabled = $ui.Verbose.IsChecked -eq $true function Write-Logging ([string]$Msg, [string]$Level = '', [bool]$AddTimestamp = $false) { $ts = Get-Date -Format 'HH:mm:ss' if ($Level -eq '') { if ($AddTimestamp) { $sh.txtLog.Text += "[$ts] $Msg`n" } else { $sh.txtLog.Text += "$Msg`n" } } else { if ($AddTimestamp) { $sh.txtLog.Text += "[$ts][$Level] $Msg`n" } else { $sh.txtLog.Text += "[$Level] $Msg`n" } } $sh.txtLog.CaretIndex = $sh.txtLog.Text.Length } function Build-MSADConfigObject { param ( [string]$ReportName, [string]$Lang, [string]$Theme, [bool]$EnableDiagrams, [bool]$ExportDiagrams, [string]$PSDefaultAuthentication, [bool]$WinRMSSL, [bool]$WinRMFallbackToNoSSL, [int]$LvlForest, [int]$LvlDomain, [int]$LvlDNS ) return [ordered]@{ Report = [ordered]@{ Name = $ReportName Version = '1.0' Status = 'Released' Language = $Lang ShowCoverPageImage = $true ShowTableOfContents = $true ShowHeaderFooter = $true ShowTableCaptions = $true } Options = [ordered]@{ ShowExecutionTime = $false ShowDefinitionInfo = $false PSDefaultAuthentication = $PSDefaultAuthentication Exclude = [ordered]@{ Domains = @(); DCs = @() } Include = [ordered]@{ Domains = @() } WinRMSSL = $WinRMSSL WinRMFallbackToNoSSL = $WinRMFallbackToNoSSL WinRMSSLPort = 5986 WinRMPort = 5985 EnableDiagrams = $EnableDiagrams EnableDiagramDebug = $false DiagramTheme = $Theme DiagramObjDebug = $false DiagramWaterMark = '' DiagramType = [ordered]@{ CertificateAuthority = $true Forest = $true Replication = $true Sites = $true SitesInventory = $true Trusts = $true } ExportDiagrams = $ExportDiagrams ExportDiagramsFormat = @('pdf') EnableDiagramSignature = $false SignatureAuthorName = '' SignatureCompanyName = '' JobsTimeOut = 900 DCStatusPingCount = 2 } InfoLevel = [ordered]@{ Forest = $LvlForest Domain = $LvlDomain DNS = $LvlDNS } HealthCheck = [ordered]@{ Domain = [ordered]@{ GMSA = $true GPO = $true Backup = $true DFS = $true SPN = $true DuplicateObject = $true Security = $true BestPractice = $true } DomainController = [ordered]@{ Diagnostic = $true Services = $true Software = $true BestPractice = $true } Site = [ordered]@{ Replication = $true BestPractice = $true } DNS = [ordered]@{ Aging = $true DP = $true Zones = $true BestPractice = $true } CA = [ordered]@{ Status = $true Statistics = $true BestPractice = $true } } } } # ── Collect values ──────────────────────────────────────────────────────── $server = $ui.Server.Text.Trim() $username = $ui.Username.Text.Trim() $password = $ui.Password.Text $reportName = $ui.ReportName.Text.Trim() $outPath = $ui.OutPath.Text.Trim() $lang = [string]$ui.Lang.SelectedItem $configPath = $ui.ConfigPath.Text.Trim() $abrConfigPath = $ui.AbrConfigPath.Text.Trim() $formats = @() if ($ui.FmtHTML.IsChecked -eq $true) { $formats += 'Html' } if ($ui.FmtWord.IsChecked -eq $true) { $formats += 'Word' } if ($ui.FmtText.IsChecked -eq $true) { $formats += 'Text' } if ($formats.Count -eq 0) { $formats = @('Html') } $enableDiagrams = [bool]$ui.Diagrams.IsChecked $exportDiagrams = [bool]$ui.ExportDiagrams.IsChecked $addTimestamp = [bool]$ui.Timestamp.IsChecked $winRMSSL = [bool]$ui.WinRMSSL.IsChecked $winRMFallback = [bool]$ui.WinRMFallback.IsChecked $psDefaultAuth = [string]$ui.PSDefaultAuth.SelectedItem $diagramTheme = [string]$ui.DiagramTheme.SelectedItem # Parse InfoLevel (first char = number) $lvlForest = [int]([string]$ui.LvlForest.SelectedItem).Substring(0, 1) $lvlDomain = [int]([string]$ui.LvlDomain.SelectedItem).Substring(0, 1) $lvlDNS = [int]([string]$ui.LvlDNS.SelectedItem).Substring(0, 1) # ── Validation ──────────────────────────────────────────────────────────── if ([string]::IsNullOrWhiteSpace($server)) { Write-Logging 'Domain Controller FQDN is required.' 'ERROR' $sh.progressBar.IsVisible = $false; $sh.btnCancel.IsVisible = $false; $sh.IsBusy = $false; return } if ([string]::IsNullOrWhiteSpace($username)) { Write-Logging 'Username is required.' 'ERROR' $sh.progressBar.IsVisible = $false; $sh.btnCancel.IsVisible = $false; $sh.IsBusy = $false; return } if ([string]::IsNullOrWhiteSpace($password)) { Write-Logging 'Password is required.' 'ERROR' $sh.progressBar.IsVisible = $false; $sh.btnCancel.IsVisible = $false; $sh.IsBusy = $false; return } if ([string]::IsNullOrWhiteSpace($outPath)) { $outPath = if ($IsWindows) { [System.IO.Path]::Combine($env:USERPROFILE, 'Documents', 'AsBuiltReport') } else { [System.IO.Path]::Combine($env:HOME, 'AsBuiltReport') } } if (-not (Test-Path $outPath)) { New-Item -Path $outPath -ItemType Directory -Force | Out-Null Write-Logging "Created output folder: $outPath" } if ([string]::IsNullOrWhiteSpace($reportName)) { $reportName = 'Microsoft Active Directory As Built Report' } if ([string]::IsNullOrWhiteSpace($abrConfigPath)) { Write-Logging 'AsBuiltReport config file path is required. Use the "⚙️ AsBuiltReport Global Settings" expander to create one.' 'ERROR' $sh.progressBar.IsVisible = $false; $sh.btnCancel.IsVisible = $false; $sh.IsBusy = $false; return } if (-not (Test-Path $abrConfigPath)) { Write-Logging "AsBuiltReport config file not found: $abrConfigPath" 'ERROR' $sh.progressBar.IsVisible = $false; $sh.btnCancel.IsVisible = $false; $sh.IsBusy = $false; return } Write-Logging "Target : $server" Write-Logging "User : $username" Write-Logging "Formats : $($formats -join ', ')" Write-Logging "Output : $outPath" # ── Import modules in this runspace ─────────────────────────────────────── Write-Logging 'Loading AsBuiltReport modules…' try { Import-Module AsBuiltReport.Core, AsBuiltReport.Microsoft.AD -Force -ErrorAction Stop } catch { Write-Logging "Failed to load modules: $_" 'ERROR' $sh.progressBar.IsVisible = $false; $sh.btnCancel.IsVisible = $false; $sh.IsBusy = $false; return } # ── Resolve ReportConfigFilePath ────────────────────────────────────────── # Use the saved config file from Config Management if provided; # otherwise build a temp config from the current UI control values. $tempConfig = $null if (-not [string]::IsNullOrWhiteSpace($configPath) -and (Test-Path $configPath)) { $reportConfigFilePath = $configPath Write-Logging "Using config file: $(Split-Path $configPath -Leaf)" } else { $configObj = Build-MSADConfigObject ` -ReportName $reportName ` -Lang $lang ` -Theme $diagramTheme ` -EnableDiagrams $enableDiagrams ` -ExportDiagrams $exportDiagrams ` -PSDefaultAuthentication $psDefaultAuth ` -WinRMSSL $winRMSSL ` -WinRMFallbackToNoSSL $winRMFallback ` -LvlForest $lvlForest ` -LvlDomain $lvlDomain ` -LvlDNS $lvlDNS $tempConfig = [System.IO.Path]::Combine($env:TEMP, "MSAD_cfg_$(New-Guid).json") $configObj | ConvertTo-Json -Depth 6 | Set-Content -Path $tempConfig -Encoding UTF8 $reportConfigFilePath = $tempConfig Write-Logging 'Using config built from UI controls.' } # ── Invoke New-AsBuiltReport ────────────────────────────────────────────── try { if ($sh.CancelRequested) { Write-Logging 'Cancelled before start.' 'WARN'; return } Write-Logging 'Starting report generation…' $securePassword = ConvertTo-SecureString $password -AsPlainText -Force $credential = [PSCredential]::new($username, $securePassword) $params = @{ Report = 'Microsoft.AD' Target = $server Credential = $credential OutputFolderPath = $outPath Format = $formats ReportConfigFilePath = $reportConfigFilePath AsBuiltConfigFilePath = $abrConfigPath } if ($addTimestamp) { $params['Timestamp'] = $true } if ($verboseEnabled) { $params['Verbose'] = $true } Write-Logging "Using AsBuiltReport config: $(Split-Path $abrConfigPath -Leaf)" New-AsBuiltReport @params *>&1 | ForEach-Object { $line = if ($_ -is [System.Management.Automation.ErrorRecord]) { Write-Logging "$($_.Exception.Message)" 'ERROR' return } elseif ($_ -is [System.Management.Automation.WarningRecord]) { Write-Logging "$($_.Message)" 'WARN' return } elseif ($_ -is [System.Management.Automation.VerboseRecord]) { if ($verboseEnabled) { Write-Logging "$($_.Message)" 'VERBOSE' } return } elseif ($_ -is [System.Management.Automation.InformationRecord]) { "$($_.MessageData)" } else { "$_" } if (-not [string]::IsNullOrWhiteSpace($line)) { Write-Logging $line } } Write-Logging -Msg "✅ Report generation completed. Files saved to: $outPath" -Level '' -AddTimestamp $true } catch { Write-Logging $_.Exception.Message 'ERROR' if ($_.ScriptStackTrace) { Write-Logging $_.ScriptStackTrace 'ERROR' } } finally { if ($null -ne $tempConfig) { Remove-Item -Path $tempConfig -Force -ErrorAction SilentlyContinue } $sh.progressBar.IsVisible = $false $sh.btnCancel.IsVisible = $false $sh.IsBusy = $false } } $btnGenerate.AddClick($generateCallback) # ── Config Management Controls ─────────────────────────────────────────────── $txtConfigPath = [TextBox]::new() $txtConfigPath.Width = 298 $txtConfigPath.Watermark = 'Path to AsBuiltReport.Microsoft.AD.json (optional)' $txtConfigPath.Text = if ($IsWindows) { [System.IO.Path]::Combine($env:USERPROFILE, 'AsBuiltReport', 'AsBuiltReport.Microsoft.AD.json') } else { [System.IO.Path]::Combine($env:HOME, 'AsBuiltReport', 'AsBuiltReport.Microsoft.AD.json') } $btnBrowseConfig = [Button]::new() $btnBrowseConfig.Content = 'Browse…' $btnBrowseConfig.AddClick({ try { $btnBrowseConfig.IsEnabled = $false $storageProvider = [Window]::GetTopLevel($btnBrowseConfig).StorageProvider if ($null -eq $storageProvider) { Write-Host 'Storage provider not available.' -ForegroundColor Yellow return } $options = [FilePickerOpenOptions]::new() $options.Title = 'Select AsBuiltReport.Microsoft.AD JSON Config File' $JsonConfigFile = $storageProvider.OpenFilePickerAsync($options).WaitForCompleted() if ($JsonConfigFile -and $JsonConfigFile.Count -gt 0) { $txtConfigPath.Text = $JsonConfigFile[0].Path.LocalPath } } catch { Write-Host "File picker error: $_" -ForegroundColor Red } finally { $btnBrowseConfig.IsEnabled = $true } }) $configPathRow = [StackPanel]::new() $configPathRow.Orientation = 'Horizontal' $configPathRow.Spacing = 8 $configPathRow.Children.Add($txtConfigPath) $configPathRow.Children.Add($btnBrowseConfig) $lblConfigStatus = [TextBlock]::new() $lblConfigStatus.FontSize = 11 $lblConfigStatus.Margin = '0,4,0,0' $lblConfigStatus.Text = '' $syncHash.lblConfigStatus = $lblConfigStatus # ── AsBuiltReport Global Config (AsBuiltReport.json) ───────────────────────── $txtAbrConfigPath = [TextBox]::new() $txtAbrConfigPath.Width = 298 $txtAbrConfigPath.Watermark = 'Required: path to AsBuiltReport.json' $btnBrowseAbrConfig = [Button]::new() $btnBrowseAbrConfig.Content = 'Browse…' $btnBrowseAbrConfig.AddClick({ try { $btnBrowseAbrConfig.IsEnabled = $false $storageProvider = [Window]::GetTopLevel($btnBrowseAbrConfig).StorageProvider if ($null -eq $storageProvider) { return } $options = [FilePickerOpenOptions]::new() $options.Title = 'Select AsBuiltReport.json' $options.AllowMultiple = $false $picked = $storageProvider.OpenFilePickerAsync($options).WaitForCompleted() if ($picked -and $picked.Count -gt 0) { $txtAbrConfigPath.Text = $picked[0].Path.LocalPath $syncHash.lblConfigStatus.Text = "📄 AsBuiltReport config: $(Split-Path $txtAbrConfigPath.Text -Leaf)" } } catch { $syncHash.lblConfigStatus.Text = "❌ Browse error: $_" } finally { $btnBrowseAbrConfig.IsEnabled = $true } }) $abrConfigPathRow = [StackPanel]::new() $abrConfigPathRow.Orientation = 'Horizontal' $abrConfigPathRow.Spacing = 8 $abrConfigPathRow.Children.Add($txtAbrConfigPath) $abrConfigPathRow.Children.Add($btnBrowseAbrConfig) # Late-bind after TextBox objects exist $generateCallback.ArgumentList['ConfigPath'] = $txtConfigPath $generateCallback.ArgumentList['AbrConfigPath'] = $txtAbrConfigPath # ── AsBuiltReport Global Settings (AsBuiltReport.json editor) ──────────────── $txtAbrCoFullName = [TextBox]::new(); $txtAbrCoFullName.Width = 298; $txtAbrCoFullName.Watermark = 'e.g. Acme Corporation' $txtAbrCoShortName = [TextBox]::new(); $txtAbrCoShortName.Width = 298; $txtAbrCoShortName.Watermark = 'e.g. ACME' $txtAbrCoContact = [TextBox]::new(); $txtAbrCoContact.Width = 298; $txtAbrCoContact.Watermark = 'Contact person' $txtAbrCoPhone = [TextBox]::new(); $txtAbrCoPhone.Width = 298; $txtAbrCoPhone.Watermark = 'e.g. +1-800-555-0100' $txtAbrCoAddress = [TextBox]::new(); $txtAbrCoAddress.Width = 298; $txtAbrCoAddress.Watermark = 'Street, City, Country' $txtAbrCoEmail = [TextBox]::new(); $txtAbrCoEmail.Width = 298; $txtAbrCoEmail.Watermark = 'company@example.com' $txtAbrRptAuthor = [TextBox]::new(); $txtAbrRptAuthor.Width = 298; $txtAbrRptAuthor.Watermark = 'Report author' $txtAbrMailServer = [TextBox]::new(); $txtAbrMailServer.Width = 298; $txtAbrMailServer.Watermark = 'smtp.example.com' $txtAbrMailPort = [TextBox]::new(); $txtAbrMailPort.Width = 298; $txtAbrMailPort.Watermark = '587' $txtAbrMailFrom = [TextBox]::new(); $txtAbrMailFrom.Width = 298; $txtAbrMailFrom.Watermark = 'from@example.com' $txtAbrMailTo = [TextBox]::new(); $txtAbrMailTo.Width = 298; $txtAbrMailTo.Watermark = 'to@example.com, other@example.com' $txtAbrMailBody = [TextBox]::new(); $txtAbrMailBody.Width = 298; $txtAbrMailBody.Watermark = 'Email body text' $swAbrMailUseSSL = [ToggleSwitch]::new(); $swAbrMailUseSSL.IsChecked = $true $swAbrMailCreds = [ToggleSwitch]::new(); $swAbrMailCreds.IsChecked = $true $txtAbrFolderPath = [TextBox]::new(); $txtAbrFolderPath.Width = 298; $txtAbrFolderPath.Watermark = '.\AsBuiltReport' $loadAbrFields = { param ([hashtable]$j) $txtAbrCoFullName.Text = if ($j.Company.FullName) { $j.Company.FullName } else { '' } $txtAbrCoShortName.Text = if ($j.Company.ShortName) { $j.Company.ShortName } else { '' } $txtAbrCoContact.Text = if ($j.Company.Contact) { $j.Company.Contact } else { '' } $txtAbrCoPhone.Text = if ($j.Company.Phone) { $j.Company.Phone } else { '' } $txtAbrCoAddress.Text = if ($j.Company.Address) { $j.Company.Address } else { '' } $txtAbrCoEmail.Text = if ($j.Company.Email) { $j.Company.Email } else { '' } $txtAbrRptAuthor.Text = if ($j.Report.Author) { $j.Report.Author } else { '' } $txtAbrMailServer.Text = if ($j.Email.Server) { $j.Email.Server } else { '' } $txtAbrMailPort.Text = if ($j.Email.Port) { $j.Email.Port } else { '' } $txtAbrMailFrom.Text = if ($j.Email.From) { $j.Email.From } else { '' } $txtAbrMailTo.Text = if ($j.Email.To) { ($j.Email.To -join ', ') } else { '' } $txtAbrMailBody.Text = if ($j.Email.Body) { $j.Email.Body } else { '' } $swAbrMailUseSSL.IsChecked = if ($null -ne $j.Email.UseSSL) { [bool]$j.Email.UseSSL } else { $true } $swAbrMailCreds.IsChecked = if ($null -ne $j.Email.Credentials) { [bool]$j.Email.Credentials } else { $true } $txtAbrFolderPath.Text = if ($j.UserFolder.Path) { $j.UserFolder.Path } else { if ($IsWindows) { [System.IO.Path]::Combine($env:USERPROFILE, 'Documents', 'AsBuiltReport') } else { [System.IO.Path]::Combine($env:HOME, 'AsBuiltReport') } } } $buildAbrConfig = { $toList = ([string]$txtAbrMailTo.Text).Trim() -split '\s*,\s*' | Where-Object { $_ -ne '' } $portRaw = ([string]$txtAbrMailPort.Text).Trim() $portVal = if ($portRaw -match '^\d+$') { [int]$portRaw } else { $null } return [ordered]@{ Company = [ordered]@{ FullName = ([string]$txtAbrCoFullName.Text).Trim() Phone = ([string]$txtAbrCoPhone.Text).Trim() Address = ([string]$txtAbrCoAddress.Text).Trim() ShortName = ([string]$txtAbrCoShortName.Text).Trim() Contact = ([string]$txtAbrCoContact.Text).Trim() Email = ([string]$txtAbrCoEmail.Text).Trim() } Email = [ordered]@{ Credentials = [bool]$swAbrMailCreds.IsChecked Body = ([string]$txtAbrMailBody.Text).Trim() From = ([string]$txtAbrMailFrom.Text).Trim() UseSSL = [bool]$swAbrMailUseSSL.IsChecked Server = ([string]$txtAbrMailServer.Text).Trim() To = if ($toList.Count -gt 0) { @($toList) } else { @() } Port = $portVal } Report = [ordered]@{ Author = ([string]$txtAbrRptAuthor.Text).Trim() } UserFolder = [ordered]@{ Path = ([string]$txtAbrFolderPath.Text).Trim() } } }.GetNewClosure() $syncHash.buildAbrConfig = $buildAbrConfig $validateAbrRequired = { $missing = @() if ([string]::IsNullOrWhiteSpace($txtAbrCoFullName.Text)) { $missing += 'Full Name' } if ([string]::IsNullOrWhiteSpace($txtAbrCoShortName.Text)) { $missing += 'Short Name' } if ([string]::IsNullOrWhiteSpace($txtAbrCoContact.Text)) { $missing += 'Contact' } if ([string]::IsNullOrWhiteSpace($txtAbrCoEmail.Text)) { $missing += 'Email' } if ([string]::IsNullOrWhiteSpace($txtAbrRptAuthor.Text)) { $missing += 'Author' } if ([string]::IsNullOrWhiteSpace($txtAbrFolderPath.Text)) { $missing += 'Path' } if ($missing.Count -gt 0) { return "⚠ Required fields missing: $($missing -join ', ')" } return $null }.GetNewClosure() $syncHash.validateAbrRequired = $validateAbrRequired $btnAbrNew = [Button]::new() $btnAbrNew.Content = '🆕 Create New' $btnAbrNew.Margin = '0,0,8,0' $btnAbrNew.AddClick({ try { $btnAbrNew.IsEnabled = $false $storageProvider = [Window]::GetTopLevel($btnAbrNew).StorageProvider if ($null -eq $storageProvider) { $syncHash.lblConfigStatus.Text = '⚠ Cannot open save dialog.' return } $saveOpts = [FilePickerSaveOptions]::new() $saveOpts.Title = 'Create New AsBuiltReport Config File' $saveOpts.SuggestedFileName = 'AsBuiltReport.json' $saveOpts.DefaultExtension = 'json' $file = $storageProvider.SaveFilePickerAsync($saveOpts).WaitForCompleted() if ($null -eq $file) { return } if ($null -eq $file.Path) { $syncHash.lblConfigStatus.Text = '⚠ Could not resolve file path from dialog.' return } $validationError = & $syncHash.validateAbrRequired if ($null -ne $validationError) { $syncHash.lblConfigStatus.Text = $validationError return } $dest = $file.Path.LocalPath $cfg = & $syncHash.buildAbrConfig $destDir = Split-Path $dest -Parent if (-not (Test-Path $destDir)) { New-Item -Path $destDir -ItemType Directory -Force | Out-Null } $cfg | ConvertTo-Json -Depth 4 | Set-Content -Path $dest -Encoding UTF8 $txtAbrConfigPath.Text = $dest $syncHash.lblConfigStatus.Text = "✅ Created: $(Split-Path $dest -Leaf)" } catch { $syncHash.lblConfigStatus.Text = "❌ Create failed: $_" } finally { $btnAbrNew.IsEnabled = $true } }) $btnAbrLoad = [Button]::new() $btnAbrLoad.Content = '📂 Load from File' $btnAbrLoad.Margin = '0,0,8,0' $btnAbrLoad.AddClick({ try { $btnAbrLoad.IsEnabled = $false $src = if ($txtAbrConfigPath.Text) { $txtAbrConfigPath.Text.Trim() } else { '' } if ([string]::IsNullOrWhiteSpace($src) -or -not (Test-Path $src)) { $syncHash.lblConfigStatus.Text = '⚠ Set a valid AsBuiltReport.json path first.' return } $j = Get-Content -Path $src -Raw | ConvertFrom-Json -AsHashtable & $loadAbrFields $j $syncHash.lblConfigStatus.Text = "✅ Loaded: $(Split-Path $src -Leaf)" } catch { $syncHash.lblConfigStatus.Text = "❌ Load failed: $_" } finally { $btnAbrLoad.IsEnabled = $true } }) $btnAbrSave = [Button]::new() $btnAbrSave.Content = '💾 Save to File' $btnAbrSave.AddClick({ try { $btnAbrSave.IsEnabled = $false $validationError = & $syncHash.validateAbrRequired if ($null -ne $validationError) { $syncHash.lblConfigStatus.Text = $validationError return } if ([string]::IsNullOrWhiteSpace($txtAbrConfigPath.Text)) { $syncHash.lblConfigStatus.Text = '❌ Please provide a config file path before saving.' return } $dest = $txtAbrConfigPath.Text.Trim() $cfg = & $syncHash.buildAbrConfig $destDir = Split-Path $dest -Parent if (-not (Test-Path $destDir)) { New-Item -Path $destDir -ItemType Directory -Force | Out-Null } $cfg | ConvertTo-Json -Depth 4 | Set-Content -Path $dest -Encoding UTF8 $syncHash.lblConfigStatus.Text = "✅ Saved: $(Split-Path $dest -Leaf)" } catch { $syncHash.lblConfigStatus.Text = "❌ Save failed: $_" } finally { $btnAbrSave.IsEnabled = $true } }) $abrActionRow = [StackPanel]::new() $abrActionRow.Orientation = 'Horizontal' $abrActionRow.Margin = '0,10,0,0' $abrActionRow.Children.Add($btnAbrNew) $abrActionRow.Children.Add($btnAbrLoad) $abrActionRow.Children.Add($btnAbrSave) $abrRequiredNote = [TextBlock]::new() $abrRequiredNote.Text = '* Required' $abrRequiredNote.FontSize = 12 $abrRequiredNote.Margin = '0,0,0,8' $abrRequiredNote.TextAlignment = 'Right' $abrInnerPanel = [StackPanel]::new() $abrInnerPanel.Spacing = 2 $abrInnerPanel.Margin = '4,4,4,8' $abrInnerPanel.Children.Add($abrRequiredNote) $abrInnerPanel.Children.Add((New-SectionTitle '🏢 Company')) $abrInnerPanel.Children.Add((New-FormRow -Label '* Full Name' -Control $txtAbrCoFullName)) $abrInnerPanel.Children.Add((New-FormRow -Label '* Short Name' -Control $txtAbrCoShortName)) $abrInnerPanel.Children.Add((New-FormRow -Label '* Contact' -Control $txtAbrCoContact)) $abrInnerPanel.Children.Add((New-FormRow -Label 'Phone' -Control $txtAbrCoPhone)) $abrInnerPanel.Children.Add((New-FormRow -Label 'Address' -Control $txtAbrCoAddress)) $abrInnerPanel.Children.Add((New-FormRow -Label '* Email' -Control $txtAbrCoEmail)) $abrInnerPanel.Children.Add((New-SectionTitle '📝 Report')) $abrInnerPanel.Children.Add((New-FormRow -Label '* Author' -Control $txtAbrRptAuthor)) $abrInnerPanel.Children.Add((New-SectionTitle '📧 Email')) $abrInnerPanel.Children.Add((New-FormRow -Label 'SMTP Server' -Control $txtAbrMailServer)) $abrInnerPanel.Children.Add((New-FormRow -Label 'Port' -Control $txtAbrMailPort)) $abrInnerPanel.Children.Add((New-FormRow -Label 'From' -Control $txtAbrMailFrom)) $abrInnerPanel.Children.Add((New-FormRow -Label 'To (comma-sep.)' -Control $txtAbrMailTo)) $abrInnerPanel.Children.Add((New-FormRow -Label 'Body' -Control $txtAbrMailBody)) $abrInnerPanel.Children.Add((New-FormRow -Label 'Use SSL' -Control $swAbrMailUseSSL)) $abrInnerPanel.Children.Add((New-FormRow -Label 'Credentials' -Control $swAbrMailCreds)) $abrInnerPanel.Children.Add((New-SectionTitle '📁 User Folder')) $abrInnerPanel.Children.Add((New-FormRow -Label '* Path' -Control $txtAbrFolderPath)) $abrInnerPanel.Children.Add($abrActionRow) $abrExpander = [Expander]::new() $abrExpander.Header = '⚙️ AsBuiltReport Global Settings' $abrExpander.IsExpanded = $false $abrExpander.Margin = '0,8,0,0' $abrExpander.Content = $abrInnerPanel # ── Save Config Button ───────────────────────────────────────────────────── function Build-MSADConfigForSave { param ( [string]$ReportName, [string]$Lang, [string]$Theme, [bool]$EnableDiagrams, [bool]$ExportDiagrams, [string]$PSDefaultAuthentication, [bool]$WinRMSSL, [bool]$WinRMFallbackToNoSSL, [int]$LvlForest, [int]$LvlDomain, [int]$LvlDNS ) return [ordered]@{ Report = [ordered]@{ Name = $ReportName Version = '1.0' Status = 'Released' Language = $Lang ShowCoverPageImage = $true ShowTableOfContents = $true ShowHeaderFooter = $true ShowTableCaptions = $true } Options = [ordered]@{ ShowExecutionTime = $false ShowDefinitionInfo = $false PSDefaultAuthentication = $PSDefaultAuthentication Exclude = [ordered]@{ Domains = @(); DCs = @() } Include = [ordered]@{ Domains = @() } WinRMSSL = $WinRMSSL WinRMFallbackToNoSSL = $WinRMFallbackToNoSSL WinRMSSLPort = 5986 WinRMPort = 5985 EnableDiagrams = $EnableDiagrams EnableDiagramDebug = $false DiagramTheme = $Theme DiagramObjDebug = $false DiagramWaterMark = '' DiagramType = [ordered]@{ CertificateAuthority = $true Forest = $true Replication = $true Sites = $true SitesInventory = $true Trusts = $true } ExportDiagrams = $ExportDiagrams ExportDiagramsFormat = @('pdf') EnableDiagramSignature = $false SignatureAuthorName = '' SignatureCompanyName = '' JobsTimeOut = 900 DCStatusPingCount = 2 } InfoLevel = [ordered]@{ Forest = $LvlForest Domain = $LvlDomain DNS = $LvlDNS } HealthCheck = [ordered]@{ Domain = [ordered]@{ GMSA = $true; GPO = $true; Backup = $true; DFS = $true SPN = $true; DuplicateObject = $true; Security = $true; BestPractice = $true } DomainController = [ordered]@{ Diagnostic = $true; Services = $true; Software = $true; BestPractice = $true } Site = [ordered]@{ Replication = $true; BestPractice = $true } DNS = [ordered]@{ Aging = $true; DP = $true; Zones = $true; BestPractice = $true } CA = [ordered]@{ Status = $true; Statistics = $true; BestPractice = $true } } } } $btnSaveConfig = [Button]::new() $btnSaveConfig.Content = '💾 Save Config' $btnSaveConfig.HorizontalAlignment = 'Stretch' $btnSaveConfig.HorizontalContentAlignment = 'Center' $btnSaveConfig.Width = 196 $btnSaveConfig.Margin = '0,0,4,0' $btnSaveConfig.AddClick({ $destPath = $txtConfigPath.Text.Trim() if ([string]::IsNullOrWhiteSpace($destPath)) { $syncHash.lblConfigStatus.Text = '⚠ Please enter a destination path first.' return } try { $parent = Split-Path $destPath -Parent if (-not [string]::IsNullOrEmpty($parent) -and -not (Test-Path $parent)) { New-Item -Path $parent -ItemType Directory -Force | Out-Null } function Get-LevelVal ($cbo) { [int]([string]$cbo.SelectedItem).Substring(0, 1) } $configObj = Build-MSADConfigForSave ` -ReportName ($txtReportName.Text.Trim()) ` -Lang ([string]$cboLang.SelectedItem) ` -Theme ([string]$cboDiagramTheme.SelectedItem) ` -EnableDiagrams ([bool]$swDiagrams.IsChecked) ` -ExportDiagrams ([bool]$swExportDiagrams.IsChecked) ` -PSDefaultAuthentication ([string]$cboPSDefaultAuth.SelectedItem) ` -WinRMSSL ([bool]$swWinRMSSL.IsChecked) ` -WinRMFallbackToNoSSL ([bool]$swWinRMFallback.IsChecked) ` -LvlForest (Get-LevelVal $cboLvlForest) ` -LvlDomain (Get-LevelVal $cboLvlDomain) ` -LvlDNS (Get-LevelVal $cboLvlDNS) $configObj | ConvertTo-Json -Depth 6 | Set-Content -Path $destPath -Encoding UTF8 $syncHash.lblConfigStatus.Text = "✅ Config saved: $(Split-Path $destPath -Leaf)" } catch { $syncHash.lblConfigStatus.Text = "❌ Save failed: $_" } }) $btnLoadConfig = [Button]::new() $btnLoadConfig.Content = '📂 Load Config' $btnLoadConfig.HorizontalAlignment = 'Stretch' $btnLoadConfig.HorizontalContentAlignment = 'Center' $btnLoadConfig.Width = 196 $btnLoadConfig.Margin = '0,0,4,0' $btnLoadConfig.AddClick({ $srcPath = $txtConfigPath.Text.Trim() if ([string]::IsNullOrWhiteSpace($srcPath) -or -not (Test-Path $srcPath)) { $syncHash.lblConfigStatus.Text = '⚠ Config file path not found.' return } try { $j = Get-Content -Path $srcPath -Raw | ConvertFrom-Json if ($j.Report.Name) { $txtReportName.Text = $j.Report.Name } if ($j.Report.Language) { $idx = $cboLang.Items.IndexOf($j.Report.Language); if ($idx -ge 0) { $cboLang.SelectedIndex = $idx } } if ($null -ne $j.Options.EnableDiagrams) { $swDiagrams.IsChecked = [bool]$j.Options.EnableDiagrams } if ($null -ne $j.Options.ExportDiagrams) { $swExportDiagrams.IsChecked = [bool]$j.Options.ExportDiagrams } if ($null -ne $j.Options.ShowExecutionTime) { Out-Null } if ($null -ne $j.Options.ShowDefinitionInfo) { Out-Null } if ($null -ne $j.Options.WinRMSSL) { $swWinRMSSL.IsChecked = [bool]$j.Options.WinRMSSL } if ($null -ne $j.Options.WinRMFallbackToNoSSL) { $swWinRMFallback.IsChecked = [bool]$j.Options.WinRMFallbackToNoSSL } if ($j.Options.DiagramTheme) { $idx = $cboDiagramTheme.Items.IndexOf($j.Options.DiagramTheme); if ($idx -ge 0) { $cboDiagramTheme.SelectedIndex = $idx } } if ($j.Options.PSDefaultAuthentication) { $idx = $cboPSDefaultAuth.Items.IndexOf($j.Options.PSDefaultAuthentication); if ($idx -ge 0) { $cboPSDefaultAuth.SelectedIndex = $idx } } if ($null -ne $j.InfoLevel.Forest) { $cboLvlForest.SelectedIndex = [int]$j.InfoLevel.Forest } if ($null -ne $j.InfoLevel.Domain) { $cboLvlDomain.SelectedIndex = [int]$j.InfoLevel.Domain } if ($null -ne $j.InfoLevel.DNS) { $cboLvlDNS.SelectedIndex = [int]$j.InfoLevel.DNS } $syncHash.lblConfigStatus.Text = "✅ Config loaded: $(Split-Path $srcPath -Leaf)" } catch { $syncHash.lblConfigStatus.Text = "❌ Load failed: $_" } }) $btnOpenConfig = [Button]::new() $btnOpenConfig.Content = '📄 Open File' $btnOpenConfig.HorizontalAlignment = 'Stretch' $btnOpenConfig.HorizontalContentAlignment = 'Center' $btnOpenConfig.Width = 196 $btnOpenConfig.AddClick({ $filePath = $txtConfigPath.Text.Trim() if ([string]::IsNullOrWhiteSpace($filePath) -or -not (Test-Path $filePath)) { $syncHash.lblConfigStatus.Text = '⚠ Config file not found.' return } try { Start-Process $filePath } catch { $syncHash.lblConfigStatus.Text = "❌ Could not open file: $_" } }) $cfgBtnRow = [StackPanel]::new() $cfgBtnRow.Orientation = 'Horizontal' $cfgBtnRow.Margin = '0,4,0,0' $cfgBtnRow.Children.Add($btnSaveConfig) $cfgBtnRow.Children.Add($btnLoadConfig) $cfgBtnRow.Children.Add($btnOpenConfig) # ── Assemble Main Panel (Report Page) ─────────────────────────────────────── $mainPanel = [StackPanel]::new() $mainPanel.Margin = '28,20,28,24' $mainPanel.Spacing = 2 $headerPanel = [StackPanel]::new() $headerPanel.HorizontalAlignment = 'Center' $headerPanel.Spacing = 4 $headerPanel.Margin = '0,0,0,4' $hTitle = [TextBlock]::new() $hTitle.Text = 'Microsoft Active Directory' $hTitle.FontSize = 22 $hTitle.FontWeight = 'Bold' $hTitle.HorizontalAlignment = 'Center' $hSub = [TextBlock]::new() $hSub.Text = 'As-Built Report Generator' $hSub.FontSize = 13 $hSub.HorizontalAlignment = 'Center' $headerPanel.Children.Add($hTitle) $headerPanel.Children.Add($hSub) $mainPanel.Children.Add($headerPanel) # Row 1: Server Connection | Report Output $topGrid = [Grid]::new() $topGrid.ColumnDefinitions = [ColumnDefinitions]::Parse('*, *') $topGrid.ColumnSpacing = 24 $topGrid.Margin = '0,4,0,0' $connPanel = [StackPanel]::new() $connPanel.Spacing = 2 $connPanel.Children.Add((New-SectionTitle '🔌 Server Connection')) $connPanel.Children.Add((New-FormRow -Label 'Saved Connections' -Control $cboSavedConn -LabelWidth 150)) $connPanel.Children.Add((New-FormRow -Label 'Domain Controller' -Control $txtServer -LabelWidth 150)) $connPanel.Children.Add((New-FormRow -Label 'Username' -Control $txtUser -LabelWidth 150)) $connPanel.Children.Add((New-FormRow -Label 'Password' -Control (New-PasswordRow $txtPass) -LabelWidth 150)) $connPanel.Children.Add((New-FormRow -Label '' -Control $savedConnActionsRow -LabelWidth 150)) [Grid]::SetColumn($connPanel, 0) $topGrid.Children.Add($connPanel) $outPanel = [StackPanel]::new() $outPanel.Spacing = 2 $outPanel.Children.Add((New-SectionTitle '📄 Report Output')) $outPanel.Children.Add((New-FormRow -Label 'Report Name' -Control $txtReportName -LabelWidth 130)) $outPanel.Children.Add((New-FormRow -Label 'Format' -Control $fmtPanel -LabelWidth 130)) $outPanel.Children.Add((New-FormRow -Label 'Output Folder' -Control $outputPathRow -LabelWidth 130)) $outPanel.Children.Add((New-FormRow -Label 'Language' -Control $cboLang -LabelWidth 130)) $outPanel.Children.Add((New-FormRow -Label 'Add Timestamp' -Control $swTimestamp -LabelWidth 130)) [Grid]::SetColumn($outPanel, 1) $topGrid.Children.Add($outPanel) $mainPanel.Children.Add($topGrid) # Row 2: Options | Info Level $bottomGrid = [Grid]::new() $bottomGrid.ColumnDefinitions = [ColumnDefinitions]::Parse('*, *') $bottomGrid.ColumnSpacing = 24 $bottomGrid.Margin = '0,4,0,0' $optPanel = [StackPanel]::new() $optPanel.Spacing = 2 $optPanel.Children.Add((New-SectionTitle '⚙️ Options')) $optPanel.Children.Add((New-FormRow -Label 'Enable Diagrams' -Control $swDiagrams -LabelWidth 185)) $optPanel.Children.Add((New-FormRow -Label 'Export Diagrams' -Control $swExportDiagrams -LabelWidth 185)) $optPanel.Children.Add((New-FormRow -Label 'Diagram Theme' -Control $cboDiagramTheme -LabelWidth 185)) $optPanel.Children.Add((New-FormRow -Label 'WinRM SSL' -Control $swWinRMSSL -LabelWidth 185)) $optPanel.Children.Add((New-FormRow -Label 'WinRM Fallback' -Control $swWinRMFallback -LabelWidth 185)) $optPanel.Children.Add((New-FormRow -Label 'PS Authentication' -Control $cboPSDefaultAuth -LabelWidth 185)) [Grid]::SetColumn($optPanel, 0) $bottomGrid.Children.Add($optPanel) $lvlPanel = [StackPanel]::new() $lvlPanel.Spacing = 2 $lvlPanel.Children.Add((New-SectionTitle '📊 Info Level')) $lvlPanel.Children.Add((New-FormRow -Label 'Forest' -Control $cboLvlForest)) $lvlPanel.Children.Add((New-FormRow -Label 'Domain' -Control $cboLvlDomain)) $lvlPanel.Children.Add((New-FormRow -Label 'DNS' -Control $cboLvlDNS)) [Grid]::SetColumn($lvlPanel, 1) $bottomGrid.Children.Add($lvlPanel) $mainPanel.Children.Add($bottomGrid) $mainPanel.Children.Add((New-SectionTitle '🗂️ Config Management')) $mainPanel.Children.Add((New-FormRow -Label '📄 MSAD Config File' -Control $configPathRow)) $mainPanel.Children.Add($cfgBtnRow) $mainPanel.Children.Add((New-FormRow -Label '📄 AsBuiltReport Config File' -Control $abrConfigPathRow)) $mainPanel.Children.Add($abrExpander) $mainPanel.Children.Add($btnGenerate) # Log area header $logTitle = [TextBlock]::new() $logTitle.Text = '📋 Output Log' $logTitle.FontSize = 13 $logTitle.FontWeight = 'SemiBold' $logTitle.VerticalAlignment = 'Center' $logHeaderGrid = [Grid]::new() $logHeaderGrid.Margin = '0,14,0,6' $logHeaderGrid.ColumnDefinitions.Add( [ColumnDefinition]::new([GridLength]::new(1, [GridUnitType]::Star))) $logHeaderGrid.ColumnDefinitions.Add( [ColumnDefinition]::new([GridLength]::new(0, [GridUnitType]::Auto))) $logHeaderGrid.ColumnDefinitions.Add( [ColumnDefinition]::new([GridLength]::new(0, [GridUnitType]::Auto))) [Grid]::SetColumn($logTitle, 0) [Grid]::SetColumn($chkVerbose, 1) [Grid]::SetColumn($btnExportLog, 2) $logHeaderGrid.Children.Add($logTitle) $logHeaderGrid.Children.Add($chkVerbose) $logHeaderGrid.Children.Add($btnExportLog) $btnOpenOutputFolder = [Button]::new() $btnOpenOutputFolder.Content = '📁 Open Output Folder' $btnOpenOutputFolder.Margin = '0,0,8,0' $btnOpenOutputFolder.AddClick({ $path = $txtOutput.Text.Trim() if ([string]::IsNullOrWhiteSpace($path)) { $syncHash.lblConfigStatus.Text = '⚠ No output folder set.' return } if (-not (Test-Path $path)) { $syncHash.lblConfigStatus.Text = "⚠ Output folder not found: $path" return } try { Start-Process $path } catch { $syncHash.lblConfigStatus.Text = "❌ Could not open folder: $_" } }) $logActionsRow = [StackPanel]::new() $logActionsRow.Orientation = 'Horizontal' $logActionsRow.HorizontalAlignment = 'Right' $logActionsRow.Margin = '0,6,0,0' $logActionsRow.Children.Add($btnOpenOutputFolder) $logActionsRow.Children.Add($btnCancel) $scrollView = [ScrollViewer]::new() $scrollView.Content = $mainPanel # ── Drawer Pages ──────────────────────────────────────────────────────────── $reportPage = [ContentPage]::new() $reportPage.Header = 'Report' $reportPage.Content = $scrollView $navigationPage = [NavigationPage]::new() $navigationPage.Content = $reportPage # MDI path geometry for nav icons $reportGeometry = 'M6,2A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2H6M6,4H13V9H18V20H6V4M8,12V14H16V12H8M8,16V18H13V16H8Z' $btnNavReport = New-DrawerMenuItem -Title 'Report' -IconGeometry $reportGeometry -Page $reportPage -NavigationPage $navigationPage $drawerMenuPanel = [StackPanel]::new() $drawerMenuPanel.Margin = 12 $drawerMenuPanel.Children.Add($btnNavReport) $drawerMenu = [ContentPage]::new() $drawerMenu.Content = $drawerMenuPanel $drawerHeader = [TextBlock]::new() $drawerHeader.Text = 'Navigation' $drawerHeader.FontSize = 16 $drawerHeader.FontWeight = 'SemiBold' $drawerHeader.VerticalAlignment = 'Center' $drawerHeader.Padding = '16,10,12,10' $drawerPage = [DrawerPage]::new() $drawerPage.DrawerHeader = $drawerHeader $drawerPage.Drawer = $drawerMenu $drawerPage.Content = $navigationPage # ── Shared bottom strip (log + status — visible from all drawer pages) ──────── $sharedBottomPanel = [StackPanel]::new() $sharedBottomPanel.Margin = '28,4,28,16' $sharedBottomPanel.Children.Add($progressBar) $sharedBottomPanel.Children.Add($logHeaderGrid) $sharedBottomPanel.Children.Add($txtLog) $sharedBottomPanel.Children.Add($logActionsRow) $sharedBottomPanel.Children.Add($lblConfigStatus) # ── Outer grid: drawer (fills space) above shared log strip ────────────────── $outerGrid = [Grid]::new() $outerGrid.RowDefinitions.Add([RowDefinition]::new([GridLength]::new(1, [GridUnitType]::Star))) $outerGrid.RowDefinitions.Add([RowDefinition]::new([GridLength]::new(0, [GridUnitType]::Auto))) [Grid]::SetRow($drawerPage, 0) [Grid]::SetRow($sharedBottomPanel, 1) $outerGrid.Children.Add($drawerPage) $outerGrid.Children.Add($sharedBottomPanel) # ── Window ────────────────────────────────────────────────────────────────── $win = [Window]::new() $win.Title = 'Microsoft AD — As-Built Report Generator' $win.Width = 1050 $win.Height = 920 $win.MinWidth = 880 $win.MinHeight = 500 $win.Content = $outerGrid $win.Show() $win.WaitForClosed() } |