Orchestrator/Show-InteractiveWizard.ps1
|
function Show-InteractiveWizard { <# .SYNOPSIS Presents an interactive menu-driven wizard for configuring the assessment. .DESCRIPTION Walks the user through selecting sections, tenant, auth method, and output folder. Returns a hashtable of parameter values to drive the assessment. #> [CmdletBinding()] param( [string[]]$PreSelectedSections, [string]$PreSelectedOutputFolder ) # Colorblind-friendly palette $cBorder = 'Cyan' $cPrompt = 'Yellow' $cNormal = 'White' $cMuted = 'DarkGray' $cSuccess = 'Cyan' $errorDisplayDelay = 1 # seconds to pause after validation errors $cError = 'Magenta' # Section definitions with default selection state # Use string keys to avoid OrderedDictionary int-key vs ordinal-index ambiguity (GitHub #3) $sections = [ordered]@{ '1' = @{ Name = 'Tenant'; Label = 'Tenant Information'; Selected = $true } '2' = @{ Name = 'Identity'; Label = 'Identity & Access'; Selected = $true } '3' = @{ Name = 'Licensing'; Label = 'Licensing'; Selected = $true } '4' = @{ Name = 'Email'; Label = 'Email & Exchange'; Selected = $true } '5' = @{ Name = 'Intune'; Label = 'Intune Devices'; Selected = $true } '6' = @{ Name = 'Security'; Label = 'Security'; Selected = $true } '7' = @{ Name = 'Collaboration'; Label = 'Collaboration'; Selected = $true } '8' = @{ Name = 'Hybrid'; Label = 'Hybrid Sync'; Selected = $true } '9' = @{ Name = 'PowerBI'; Label = 'Power BI'; Selected = $true } '10' = @{ Name = 'Inventory'; Label = 'M&A Inventory (opt-in)'; Selected = $false } '11' = @{ Name = 'ActiveDirectory'; Label = 'Active Directory (RSAT)'; Selected = $false } '12' = @{ Name = 'SOC2'; Label = 'SOC 2 Readiness (opt-in)'; Selected = $false } } # --- Header --- function Show-Header { Clear-Host Write-Host '' Write-Host ' ███╗ ███╗ ██████╗ ██████╗ ███████╗' -ForegroundColor Cyan Write-Host ' ████╗ ████║ ╚════██╗ ██╔════╝ ██╔════╝' -ForegroundColor Cyan Write-Host ' ██╔████╔██║ █████╔╝ ██████╗ ███████╗' -ForegroundColor Cyan Write-Host ' ██║╚██╔╝██║ ╚═══██╗ ██╔══██╗ ╚════██║' -ForegroundColor Cyan Write-Host ' ██║ ╚═╝ ██║ ██████╔╝ ╚█████╔╝ ███████║' -ForegroundColor Cyan Write-Host ' ╚═╝ ╚═╝ ╚═════╝ ╚════╝ ╚══════╝' -ForegroundColor Cyan Write-Host ' ─────────────────────────────────────────' -ForegroundColor DarkCyan Write-Host ' █████╗ ███████╗███████╗███████╗███████╗███████╗' -ForegroundColor DarkCyan Write-Host ' ██╔══██╗██╔════╝██╔════╝██╔════╝██╔════╝██╔════╝' -ForegroundColor DarkCyan Write-Host ' ███████║███████╗███████╗█████╗ ███████╗███████╗' -ForegroundColor DarkCyan Write-Host ' ██╔══██║╚════██║╚════██║██╔══╝ ╚════██║╚════██║' -ForegroundColor DarkCyan Write-Host ' ██║ ██║███████║███████║███████╗███████║███████║' -ForegroundColor DarkCyan Write-Host ' ╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝╚══════╝╚══════╝' -ForegroundColor DarkCyan Write-Host '' Write-Host ' ░▒▓█ M365 Environment Assessment █▓▒░' -ForegroundColor DarkGray Write-Host ' ░▒▓█ by G A L V N Y Z █▓▒░' -ForegroundColor DarkCyan Write-Host '' } function Show-StepHeader { param([int]$Step, [int]$Total, [string]$Title) Write-Host " STEP $Step of $Total`: $Title" -ForegroundColor $cPrompt Write-Host ' ─────────────────────────────────────────────────────────' -ForegroundColor $cMuted Write-Host '' } # Determine which steps to show and compute dynamic numbering $skipSections = $PreSelectedSections.Count -gt 0 $skipOutput = $PreSelectedOutputFolder -ne '' $totalSteps = 4 # Tenant + Auth + Report Options + Confirmation are always shown if (-not $skipSections) { $totalSteps++ } if (-not $skipOutput) { $totalSteps++ } $currentStep = 0 # ================================================================ # STEP: Select Assessment Sections (skipped when -Section provided) # ================================================================ if ($skipSections) { $selectedSections = $PreSelectedSections } else { $step1Done = $false while (-not $step1Done) { Show-Header $currentStep = 1 Show-StepHeader -Step $currentStep -Total $totalSteps -Title 'Select Assessment Sections' Write-Host ' Toggle sections by number, separated by spaces (e.g. 3 or 1 5 10).' -ForegroundColor $cNormal Write-Host ' Press ENTER when done.' -ForegroundColor $cMuted Write-Host '' foreach ($key in $sections.Keys) { $s = $sections[$key] $marker = if ($s.Selected) { '●' } else { '○' } $color = if ($s.Selected) { $cNormal } else { $cMuted } Write-Host " [$key] $marker $($s.Label)" -ForegroundColor $color } Write-Host '' Write-Host ' [S] Standard [A] Select all [N] Select none' -ForegroundColor $cPrompt Write-Host '' Write-Host ' > ' -ForegroundColor $cPrompt -NoNewline $userChoice = (Read-Host) ?? '' switch ($userChoice.Trim().ToUpper()) { 'S' { $optInSections = @('Inventory', 'ActiveDirectory') $rebuilt = [ordered]@{} foreach ($k in @($sections.Keys)) { $rebuilt["$k"] = @{ Name = $sections[$k].Name; Label = $sections[$k].Label; Selected = ($sections[$k].Name -notin $optInSections) } } $sections = $rebuilt } 'A' { $rebuilt = [ordered]@{} foreach ($k in @($sections.Keys)) { $rebuilt["$k"] = @{ Name = $sections[$k].Name; Label = $sections[$k].Label; Selected = $true } } $sections = $rebuilt } 'N' { $rebuilt = [ordered]@{} foreach ($k in @($sections.Keys)) { $rebuilt["$k"] = @{ Name = $sections[$k].Name; Label = $sections[$k].Label; Selected = $false } } $sections = $rebuilt } '' { $selectedNames = @($sections.Values | Where-Object { $_.Selected } | ForEach-Object { $_.Name }) if ($selectedNames.Count -eq 0) { Write-Host '' Write-Host ' ✗ Please select at least one section.' -ForegroundColor $cError Start-Sleep -Seconds $errorDisplayDelay } else { $step1Done = $true } } default { $tokens = $userChoice.Trim() -split '[,\s]+' foreach ($token in $tokens) { $num = 0 if ($token -ne '' -and [int]::TryParse($token, [ref]$num) -and $sections.Contains("$num")) { $sections["$num"].Selected = -not $sections["$num"].Selected } } } } } $selectedSections = @($sections.Values | Where-Object { $_.Selected } | ForEach-Object { $_.Name }) } # ================================================================ # STEP: Tenant Identity # ================================================================ $currentStep++ Show-Header Show-StepHeader -Step $currentStep -Total $totalSteps -Title 'Tenant Identity' Write-Host ' Enter your tenant ID or domain' -ForegroundColor $cNormal Write-Host ' (e.g., contoso.onmicrosoft.com):' -ForegroundColor $cMuted Write-Host '' Write-Host ' > ' -ForegroundColor $cPrompt -NoNewline $tenantInput = (Read-Host) ?? '' # ================================================================ # STEP: Authentication Method # ================================================================ $currentStep++ $step3Done = $false $authMethod = 'Interactive' $wizClientId = '' $wizCertThumb = '' $wizUpn = '' while (-not $step3Done) { Show-Header Show-StepHeader -Step $currentStep -Total $totalSteps -Title 'Authentication Method' Write-Host ' [1] Interactive login (browser popup)' -ForegroundColor $cNormal Write-Host ' [2] Device code login (choose your browser)' -ForegroundColor $cNormal Write-Host ' [3] Certificate-based (app-only)' -ForegroundColor $cNormal Write-Host ' [4] Skip connection (already connected)' -ForegroundColor $cNormal Write-Host '' Write-Host ' > ' -ForegroundColor $cPrompt -NoNewline $authInput = (Read-Host) ?? '' switch ($authInput.Trim()) { '1' { $authMethod = 'Interactive' Write-Host '' Write-Host ' Enter admin UPN for EXO/Purview (optional, press ENTER to skip):' -ForegroundColor $cNormal Write-Host ' > ' -ForegroundColor $cPrompt -NoNewline $wizUpn = (Read-Host) ?? '' $step3Done = $true } '2' { $authMethod = 'DeviceCode' $step3Done = $true } '3' { $authMethod = 'Certificate' Write-Host '' Write-Host ' Enter Application (Client) ID:' -ForegroundColor $cNormal Write-Host ' > ' -ForegroundColor $cPrompt -NoNewline $wizClientId = (Read-Host) ?? '' Write-Host ' Enter Certificate Thumbprint:' -ForegroundColor $cNormal Write-Host ' > ' -ForegroundColor $cPrompt -NoNewline $wizCertThumb = (Read-Host) ?? '' $step3Done = $true } '4' { $authMethod = 'Skip' $step3Done = $true } default { Write-Host ' ✗ Please enter 1, 2, 3, or 4.' -ForegroundColor $cError Start-Sleep -Seconds $errorDisplayDelay } } } # ================================================================ # STEP: Output Folder (skipped when -OutputFolder provided) # ================================================================ if ($skipOutput) { $wizOutputFolder = $PreSelectedOutputFolder } else { $currentStep++ $defaultOutput = '.\M365-Assessment' Show-Header Show-StepHeader -Step $currentStep -Total $totalSteps -Title 'Output Folder' Write-Host ' Assessment results will be saved to:' -ForegroundColor $cNormal Write-Host " $defaultOutput\" -ForegroundColor $cSuccess Write-Host '' Write-Host ' Press ENTER to accept, or type a custom path:' -ForegroundColor $cMuted do { $outputValid = $true Write-Host ' > ' -ForegroundColor $cPrompt -NoNewline $outputInput = (Read-Host) ?? '' if ($outputInput.Trim()) { if ($outputInput.Trim() -match '@') { Write-Host '' Write-Host ' That looks like an email address or UPN, not a folder path.' -ForegroundColor $cError Write-Host " Press ENTER to use the default ($defaultOutput), or type a valid path:" -ForegroundColor $cMuted $outputValid = $false } elseif ($outputInput.Trim() -match '[<>"|?*]') { Write-Host '' Write-Host ' Path contains invalid characters ( < > " | ? * ).' -ForegroundColor $cError Write-Host " Press ENTER to use the default ($defaultOutput), or type a valid path:" -ForegroundColor $cMuted $outputValid = $false } } } while (-not $outputValid) $wizOutputFolder = if ($outputInput.Trim()) { $outputInput.Trim() } else { $defaultOutput } } # ================================================================ # STEP: Report Options # ================================================================ $currentStep++ $reportOptions = [ordered]@{ '1' = @{ Name = 'ComplianceOverview'; Label = 'Compliance Overview'; Selected = $true } '2' = @{ Name = 'CoverPage'; Label = 'Cover Page'; Selected = $true } '3' = @{ Name = 'ExecutiveSummary'; Label = 'Executive Summary'; Selected = $true } '4' = @{ Name = 'NoBranding'; Label = 'Remove Branding'; Selected = $false } '5' = @{ Name = 'LimitFrameworks'; Label = 'Limit Frameworks'; Selected = $false } } $wizFrameworkFilter = @() # Framework family definitions for the sub-selector $fwFamilies = [ordered]@{ '1' = @{ Family = 'CIS'; Label = 'CIS Benchmarks'; Selected = $true } '2' = @{ Family = 'NIST'; Label = 'NIST 800-53 / CSF'; Selected = $true } '3' = @{ Family = 'ISO'; Label = 'ISO 27001:2022'; Selected = $true } '4' = @{ Family = 'STIG'; Label = 'DISA STIG'; Selected = $true } '5' = @{ Family = 'PCI'; Label = 'PCI DSS v4'; Selected = $true } '6' = @{ Family = 'CMMC'; Label = 'CMMC 2.0'; Selected = $true } '7' = @{ Family = 'HIPAA'; Label = 'HIPAA Security Rule'; Selected = $true } '8' = @{ Family = 'CISA'; Label = 'CISA SCuBA'; Selected = $true } '9' = @{ Family = 'SOC2'; Label = 'SOC 2 TSC'; Selected = $true } '10' = @{ Family = 'FedRAMP'; Label = 'FedRAMP'; Selected = $true } '11' = @{ Family = 'Essential8'; Label = 'Essential Eight'; Selected = $true } '12' = @{ Family = 'MITRE'; Label = 'MITRE ATT&CK'; Selected = $true } '13' = @{ Family = 'CISv8'; Label = 'CIS Controls v8'; Selected = $true } } $fwTotalCount = $fwFamilies.Count $reportStepDone = $false while (-not $reportStepDone) { Show-Header Show-StepHeader -Step $currentStep -Total $totalSteps -Title 'Report Options' Write-Host ' Toggle options by number, separated by spaces.' -ForegroundColor $cNormal Write-Host ' Press ENTER when done.' -ForegroundColor $cMuted Write-Host '' foreach ($key in $reportOptions.Keys) { $opt = $reportOptions[$key] $marker = if ($opt.Selected) { [char]0x25CF } else { [char]0x25CB } $color = if ($opt.Selected) { $cNormal } else { $cMuted } $extra = '' if ($opt.Name -eq 'LimitFrameworks') { if ($opt.Selected) { $selectedCount = @($fwFamilies.Values | Where-Object { $_.Selected }).Count $extra = " ($selectedCount of $fwTotalCount selected)" } else { $extra = " (showing all $fwTotalCount)" } } Write-Host " [$key] $marker $($opt.Label)$extra" -ForegroundColor $color } Write-Host '' Write-Host ' > ' -ForegroundColor $cPrompt -NoNewline $reportChoice = (Read-Host) ?? '' switch ($reportChoice.Trim().ToUpper()) { '' { $reportStepDone = $true } default { $tokens = $reportChoice.Trim() -split '[,\s]+' foreach ($token in $tokens) { $num = 0 if ($token -ne '' -and [int]::TryParse($token, [ref]$num) -and $reportOptions.Contains("$num")) { $reportOptions["$num"].Selected = -not $reportOptions["$num"].Selected # When Limit Frameworks is toggled on, enter the framework sub-selector if ($num -eq 5 -and $reportOptions['5'].Selected) { $fwSubDone = $false while (-not $fwSubDone) { Show-Header Show-StepHeader -Step $currentStep -Total $totalSteps -Title 'Report Options > Frameworks' Write-Host ' Toggle frameworks by number. Press ENTER when done.' -ForegroundColor $cNormal Write-Host '' foreach ($fwKey in $fwFamilies.Keys) { $fw = $fwFamilies[$fwKey] $fwMarker = if ($fw.Selected) { [char]0x25CF } else { [char]0x25CB } $fwColor = if ($fw.Selected) { $cNormal } else { $cMuted } $pad = if ([int]$fwKey -lt 10) { ' ' } else { '' } Write-Host " $pad[$fwKey] $fwMarker $($fw.Label)" -ForegroundColor $fwColor } Write-Host '' Write-Host ' [A] Select all [N] Select none' -ForegroundColor $cPrompt Write-Host '' Write-Host ' > ' -ForegroundColor $cPrompt -NoNewline $fwChoice = (Read-Host) ?? '' switch ($fwChoice.Trim().ToUpper()) { '' { $fwSubDone = $true } 'A' { foreach ($k in @($fwFamilies.Keys)) { $fwFamilies[$k].Selected = $true } } 'N' { foreach ($k in @($fwFamilies.Keys)) { $fwFamilies[$k].Selected = $false } } default { $fwTokens = $fwChoice.Trim() -split '[,\s]+' foreach ($fwToken in $fwTokens) { $fwNum = 0 if ($fwToken -ne '' -and [int]::TryParse($fwToken, [ref]$fwNum) -and $fwFamilies.Contains("$fwNum")) { $fwFamilies["$fwNum"].Selected = -not $fwFamilies["$fwNum"].Selected } } } } } # Build the filter list from selected families $wizFrameworkFilter = @($fwFamilies.Values | Where-Object { $_.Selected } | ForEach-Object { $_.Family }) # If all are selected, clear the filter (no filtering needed) if ($wizFrameworkFilter.Count -eq $fwTotalCount) { $reportOptions['5'].Selected = $false $wizFrameworkFilter = @() } elseif ($wizFrameworkFilter.Count -eq 0) { Write-Host '' Write-Host ' At least one framework must be selected. Filter disabled.' -ForegroundColor $cError $reportOptions['5'].Selected = $false Start-Sleep -Seconds $errorDisplayDelay } } # When toggled off, reset all families to selected elseif ($num -eq 5 -and -not $reportOptions['5'].Selected) { $wizFrameworkFilter = @() foreach ($k in @($fwFamilies.Keys)) { $fwFamilies[$k].Selected = $true } } } } } } } # ================================================================ # Confirmation # ================================================================ Show-Header $sectionDisplay = $selectedSections -join ', ' $tenantDisplay = if ($tenantInput.Trim()) { $tenantInput.Trim() } else { '(not specified)' } $authDisplay = switch ($authMethod) { 'Interactive' { if ($wizUpn.Trim()) { "Interactive login ($($wizUpn.Trim()))" } else { 'Interactive login' } } 'DeviceCode' { 'Device code login' } 'Certificate' { 'Certificate-based (app-only)' } 'Skip' { 'Pre-existing connections' } } Write-Host ' ═══════════════════════════════════════════════════════' -ForegroundColor $cBorder Write-Host '' Write-Host ' Ready to start assessment:' -ForegroundColor $cPrompt Write-Host '' Write-Host " Sections: $sectionDisplay" -ForegroundColor $cNormal Write-Host " Tenant: $tenantDisplay" -ForegroundColor $cNormal Write-Host " Auth: $authDisplay" -ForegroundColor $cNormal if ($M365Environment -ne 'commercial') { Write-Host " Cloud: $M365Environment" -ForegroundColor $cNormal } Write-Host " Output: $wizOutputFolder\" -ForegroundColor $cNormal # Report options summary $reportIncludes = @() if ($reportOptions['1'].Selected) { $reportIncludes += 'Compliance Overview' } if ($reportOptions['2'].Selected) { $reportIncludes += 'Cover Page' } if ($reportOptions['3'].Selected) { $reportIncludes += 'Executive Summary' } $reportExcludes = @() if (-not $reportOptions['1'].Selected) { $reportExcludes += 'Compliance Overview' } if (-not $reportOptions['2'].Selected) { $reportExcludes += 'Cover Page' } if (-not $reportOptions['3'].Selected) { $reportExcludes += 'Executive Summary' } if ($reportOptions['4'].Selected) { $reportExcludes += 'Branding' } $reportDisplay = if ($reportIncludes.Count -eq 3 -and $reportExcludes.Count -eq 0) { 'All sections, branded' } else { $reportIncludes -join ', ' } Write-Host " Report: $reportDisplay" -ForegroundColor $cNormal if ($reportExcludes.Count -gt 0) { Write-Host " Skipping: $($reportExcludes -join ', ')" -ForegroundColor $cMuted } if ($wizFrameworkFilter.Count -gt 0) { Write-Host " Frameworks: $($wizFrameworkFilter -join ', ') ($($wizFrameworkFilter.Count) of $fwTotalCount)" -ForegroundColor $cNormal } Write-Host '' Write-Host ' Press ENTER to begin, or Q to quit.' -ForegroundColor $cPrompt Write-Host ' > ' -ForegroundColor $cPrompt -NoNewline $confirmInput = (Read-Host) ?? '' if ($confirmInput.Trim().ToUpper() -eq 'Q') { Write-Host '' Write-Host ' Assessment cancelled.' -ForegroundColor $cMuted return $null } # Build result hashtable $wizardResult = @{ Section = $selectedSections OutputFolder = $wizOutputFolder } # Report options if (-not $reportOptions['1'].Selected) { $wizardResult['SkipComplianceOverview'] = $true } if (-not $reportOptions['2'].Selected) { $wizardResult['SkipCoverPage'] = $true } if (-not $reportOptions['3'].Selected) { $wizardResult['SkipExecutiveSummary'] = $true } if ($reportOptions['4'].Selected) { $wizardResult['NoBranding'] = $true } if ($wizFrameworkFilter.Count -gt 0) { $wizardResult['FrameworkFilter'] = $wizFrameworkFilter } if ($tenantInput.Trim()) { $wizardResult['TenantId'] = $tenantInput.Trim() } switch ($authMethod) { 'Skip' { $wizardResult['SkipConnection'] = $true } 'Certificate' { if ($wizClientId.Trim()) { $wizardResult['ClientId'] = $wizClientId.Trim() } if ($wizCertThumb.Trim()) { $wizardResult['CertificateThumbprint'] = $wizCertThumb.Trim() } } 'DeviceCode' { $wizardResult['UseDeviceCode'] = $true } 'Interactive' { if ($wizUpn.Trim()) { $wizardResult['UserPrincipalName'] = $wizUpn.Trim() } } } return $wizardResult } |