Modules/CyberConfigApp/CyberConfigAppHelpers/CyberConfigAppCyberRunHelper.psm1
|
Function Export-ConfigurationToResultsFolder { <# .SYNOPSIS Exports the current configuration YAML to the CyberAssessment results folder. #> param([string]$ResultsFolder) try { if (-not (Test-Path $ResultsFolder)) { Write-DebugOutput -Message "Results folder not found: $ResultsFolder" -Source $MyInvocation.MyCommand -Level "Warning" return } # Generate current YAML configuration New-YamlPreview -NoRedirect $yamlContent = $syncHash.YamlPreview_TextBox.Text if ([string]::IsNullOrWhiteSpace($yamlContent)) { Write-DebugOutput -Message "No YAML content available to export" -Source $MyInvocation.MyCommand -Level "Warning" return } # Create configuration file in results folder $configFileName = "CyberAssessmentConfiguration.yaml" $configFilePath = Join-Path $ResultsFolder $configFileName # Write YAML content to file [System.IO.File]::WriteAllText($configFilePath, $yamlContent, [System.Text.Encoding]::UTF8) Write-DebugOutput -Message "Configuration exported to: $configFilePath" -Source $MyInvocation.MyCommand -Level "Info" # Add to output log $syncHash.CyberRunOutput_TextBox.AppendText("Configuration file saved: $configFilePath`r`n") return $configFilePath } catch { Write-DebugOutput -Message "Error exporting configuration to results folder: $($_.Exception.Message)" -Source $MyInvocation.MyCommand -Level "Error" return $null } } Function Show-ConfigurationViewer { <# .SYNOPSIS Opens a simple window to display the configuration file content. #> param([string]$ConfigFilePath) try { if (-not (Test-Path $ConfigFilePath)) { $syncHash.ShowMessageBox.Invoke($syncHash.UIConfigs.localeErrorMessages.ConfigurationFileNotFound -f $ConfigFilePath, $syncHash.UIConfigs.localeTitles.ConfigurationViewer, "OK", "Error") return } # Read configuration content $configContent = [System.IO.File]::ReadAllText($ConfigFilePath, [System.Text.Encoding]::UTF8) # Create a new window $configWindow = New-Object System.Windows.Window $configWindow.Title = "CyberAssessment Configuration Viewer" $configWindow.Width = 800 $configWindow.Height = 600 $configWindow.WindowStartupLocation = "CenterOwner" $configWindow.Owner = $syncHash.Window $configWindow.Icon = $syncHash.Window.Icon # Create main grid $grid = New-Object System.Windows.Controls.Grid $configWindow.Content = $grid # Define rows $headerRow = New-Object System.Windows.Controls.RowDefinition $headerRow.Height = "Auto" $contentRow = New-Object System.Windows.Controls.RowDefinition $contentRow.Height = "*" $buttonRow = New-Object System.Windows.Controls.RowDefinition $buttonRow.Height = "Auto" $grid.RowDefinitions.Add($headerRow) $grid.RowDefinitions.Add($contentRow) $grid.RowDefinitions.Add($buttonRow) # Header $headerText = New-Object System.Windows.Controls.TextBlock $headerText.Text = "Configuration File: $(Split-Path $ConfigFilePath -Leaf)" $headerText.FontSize = 14 $headerText.FontWeight = "Bold" $headerText.Margin = "10" $headerText.HorizontalAlignment = "Center" [System.Windows.Controls.Grid]::SetRow($headerText, 0) $grid.Children.Add($headerText) # Content TextBox $contentTextBox = New-Object System.Windows.Controls.TextBox $contentTextBox.Text = $configContent $contentTextBox.IsReadOnly = $true $contentTextBox.AcceptsReturn = $true $contentTextBox.TextWrapping = "NoWrap" $contentTextBox.VerticalScrollBarVisibility = "Auto" $contentTextBox.HorizontalScrollBarVisibility = "Auto" $contentTextBox.FontFamily = "Consolas, Courier New, monospace" $contentTextBox.FontSize = 12 $contentTextBox.Margin = "10,0,10,10" $contentTextBox.Background = "#F5F5F5" $contentTextBox.BorderBrush = $syncHash.Window.FindResource("BorderBrush") [System.Windows.Controls.Grid]::SetRow($contentTextBox, 1) $grid.Children.Add($contentTextBox) # Button panel $buttonPanel = New-Object System.Windows.Controls.StackPanel $buttonPanel.Orientation = "Horizontal" $buttonPanel.HorizontalAlignment = "Center" $buttonPanel.Margin = "10" [System.Windows.Controls.Grid]::SetRow($buttonPanel, 2) # Copy button $copyButton = New-Object System.Windows.Controls.Button $copyButton.Content = "Copy to Clipboard" $copyButton.Width = 120 $copyButton.Height = 30 $copyButton.Margin = "0,0,10,0" $copyButton.Add_Click({ try { [System.Windows.Clipboard]::SetText($contentTextBox.Text) $syncHash.ShowMessageBox.Invoke($syncHash.UIConfigs.localeErrorMessages.ConfigurationCopiedToClipboard, $syncHash.UIConfigs.localeTitles.ConfigurationViewer, "OK", "Information") } catch { $syncHash.ShowMessageBox.Invoke($syncHash.UIConfigs.localeErrorMessages.FailedToCopyToClipboard -f $_.Exception.Message, $syncHash.UIConfigs.localeTitles.ConfigurationViewer, "OK", "Error") } }) $buttonPanel.Children.Add($copyButton) # Close button $closeButton = New-Object System.Windows.Controls.Button $closeButton.Content = "Close" $closeButton.Width = 80 $closeButton.Height = 30 $closeButton.Add_Click({ $configWindow.Close() }) $buttonPanel.Children.Add($closeButton) $grid.Children.Add($buttonPanel) # Show window $configWindow.ShowDialog() } catch { $syncHash.ShowMessageBox.Invoke($syncHash.UIConfigs.localeErrorMessages.ConfigurationViewerError -f $_.Exception.Message, $syncHash.UIConfigs.localeTitles.ConfigurationViewer, "OK", "Error") Write-DebugOutput -Message "Error in Show-ConfigurationViewer: $($_.Exception.Message)" -Source $MyInvocation.MyCommand -Level "Error" } } Function New-CyberRunParameterControls { <# .SYNOPSIS Dynamically creates UI controls for CyberRun parameters based on configuration. #> if (-not $syncHash.UIConfigs.CyberRunConfig.powershell.parameters) { Write-DebugOutput -Message "No CyberRun parameters defined in configuration" -Source $MyInvocation.MyCommand -Level "Info" return } # Clear existing dynamic controls $syncHash.CyberRunParametersContainer.Children.Clear() $cyberConfig = $syncHash.UIConfigs.CyberRunConfig $parameters = $cyberConfig.powershell.parameters Write-DebugOutput -Message "Creating dynamic CyberRun parameter controls" -Source $MyInvocation.MyCommand -Level "Info" foreach ($parameterName in $parameters.PSObject.Properties.Name) { $paramConfig = $parameters.$parameterName # Skip hidden parameters (they won't show in UI but will be used in commands) if ($paramConfig.hidden -eq $true) { Write-DebugOutput -Message "Skipping hidden parameter: $parameterName" -Source $MyInvocation.MyCommand -Level "Verbose" continue } Write-DebugOutput -Message "Creating control for parameter: $parameterName" -Source $MyInvocation.MyCommand -Level "Info" # Create container for this parameter $paramContainer = New-Object System.Windows.Controls.StackPanel $paramContainer.Margin = "0,0,0,8" # Create the appropriate control based on parameter type switch ($paramConfig.type) { "boolean" { $control = New-Object System.Windows.Controls.CheckBox $control.Content = $paramConfig.name $control.IsChecked = $paramConfig.defaultValue $control.IsEnabled = -not $paramConfig.readOnly $control.ToolTip = $paramConfig.description # Store control name for later reference $controlName = $parameterName.Replace("CyberRun", "") + "_CheckBox" $control.Name = $controlName $syncHash.$controlName = $control Write-DebugOutput -Message "Created CheckBox: $controlName" -Source $MyInvocation.MyCommand -Level "Verbose" } "string" { # Create label for the parameter $label = New-Object System.Windows.Controls.TextBlock $label.Text = $paramConfig.name $label.FontWeight = "SemiBold" $label.Margin = "0,0,0,4" [void]$paramContainer.Children.Add($label) $control = New-Object System.Windows.Controls.TextBox $control.Text = $paramConfig.defaultValue $control.IsReadOnly = $paramConfig.readOnly $control.ToolTip = $paramConfig.description $control.Height = 36 $control.Padding = "8,6" $control.BorderBrush = $syncHash.Window.FindResource("BorderBrush") $control.Background = if ($paramConfig.readOnly) { "#F5F5F5" } else { "#FFFFFF" } $control.Foreground = if ($paramConfig.readOnly) { $syncHash.Window.FindResource("PrimaryBrush") } else { $syncHash.Window.FindResource("TextBrush") } # Store control name for later reference $controlName = $parameterName.Replace("CyberRun", "") + "_TextBox" $control.Name = $controlName $syncHash.$controlName = $control Write-DebugOutput -Message "Created TextBox: $controlName" -Source $MyInvocation.MyCommand -Level "Verbose" } "dropdown" { # Create label for the parameter $label = New-Object System.Windows.Controls.TextBlock $label.Text = $paramConfig.name $label.FontWeight = "SemiBold" $label.Margin = "0,0,0,4" [void]$paramContainer.Children.Add($label) #get length of items to determine width $maxItemWidth = ($paramConfig.items | ForEach-Object { $_.Length } | Sort-Object -Descending)[0] $control = New-Object System.Windows.Controls.ComboBox $control.ItemsSource = $paramConfig.items $control.SelectedItem = $paramConfig.defaultValue $control.IsEnabled = -not $paramConfig.readOnly $control.ToolTip = $paramConfig.description $control.MaxWidth = $maxItemWidth + 100 $control.HorizontalAlignment = "Left" $control.Padding = "8,6" $control.BorderBrush = $syncHash.Window.FindResource("BorderBrush") $control.Background = if ($paramConfig.readOnly) { "#F5F5F5" } else { "#FFFFFF" } $control.Foreground = if ($paramConfig.readOnly) { $syncHash.Window.FindResource("PrimaryBrush") } else { $syncHash.Window.FindResource("TextBrush") } # Store control name for later reference $controlName = $parameterName.Replace("CyberRun", "") + "_ComboBox" $control.Name = $controlName $syncHash.$controlName = $control Write-DebugOutput -Message "Created ComboBox: $controlName" -Source $MyInvocation.MyCommand -Level "Verbose" } default { Write-DebugOutput -Message "Unknown parameter type: $($paramConfig.type) for $parameterName" -Source $MyInvocation.MyCommand -Level "Error" continue } } # Add control to container [void]$paramContainer.Children.Add($control) # Add the parameter container to the main container [void]$syncHash.CyberRunParametersContainer.Children.Add($paramContainer) } Write-DebugOutput -Message "Dynamic CyberRun parameter controls created successfully" -Source $MyInvocation.MyCommand -Level "Info" } Function Initialize-CyberRunTab { <# .SYNOPSIS Initializes the Cyber Run tab with event handlers and default values. #> # Create dynamic parameter controls New-CyberRunParameterControls # Enable text wrapping for the output textbox to handle long lines if ($syncHash.CyberRunOutput_TextBox) { $syncHash.CyberRunOutput_TextBox.TextWrapping = "Wrap" $syncHash.CyberRunOutput_TextBox.AcceptsReturn = $true $syncHash.CyberRunOutput_TextBox.VerticalScrollBarVisibility = "Auto" $syncHash.CyberRunOutput_TextBox.HorizontalScrollBarVisibility = "Auto" } # Add event handlers - CORRECTED BUTTON NAME $syncHash.CyberRunStart_Button.Add_Click({ Start-CyberAssessmentExecution }) $syncHash.CyberRunStop_Button.Add_Click({ Stop-CyberAssessmentExecution }) $syncHash.CyberRunClearOutput_Button.Add_Click({ $syncHash.CyberRunOutput_TextBox.Clear() $syncHash.CyberRunOutput_TextBox.AppendText("Output cleared...`r`n") }) $syncHash.CyberRunCopyOutput_Button.Add_Click({ try { [System.Windows.Clipboard]::SetText($syncHash.CyberRunOutput_TextBox.Text) Update-CyberRunStatus -Message "Output copied to clipboard" -Level "Info" } catch { Update-CyberRunStatus -Message "Failed to copy output to clipboard" -Level "Error" } }) $syncHash.CyberRunViewConfig_Button.Add_Click({ if ($syncHash.LastCyberRunResultsFolder) { $configFilePath = Join-Path $syncHash.LastCyberRunResultsFolder "CyberAssessmentConfiguration.yaml" if (Test-Path $configFilePath) { Show-ConfigurationViewer -ConfigFilePath $configFilePath } else { $syncHash.ShowMessageBox.Invoke($syncHash.UIConfigs.localeErrorMessages.ConfigurationFileNotFoundInResults -f $configFilePath, $syncHash.UIConfigs.localeTitles.ConfigurationViewer, "OK", "Warning") } } else { $syncHash.ShowMessageBox.Invoke($syncHash.UIConfigs.localeErrorMessages.NoConfigurationFileAvailable, $syncHash.UIConfigs.localeTitles.ConfigurationViewer, "OK", "Information") } }) # Initialize button states and show initial status $syncHash.JustCompletedExecution = $false # Ensure flag is clear on initialization $syncHash.CyberRunViewConfig_Button.IsEnabled = $false # Start with View Configuration button disabled Reset-CyberRunUI Write-DebugOutput -Message "CyberRun tab initialized with correct button event handlers" -Source $MyInvocation.MyCommand -Level "Info" } Function Update-CyberRunStatus { <# .SYNOPSIS Updates the status text and output log. #> param( [string]$Message, [ValidateSet("Info", "Warning", "Error", "Success")] [string]$Level = "Info" ) $timestamp = Get-Date -Format "HH:mm:ss" # Process long messages for better readability $processedMessage = $Message if ($Message.Length -gt 120 -and $Message -match "WARNING:") { # Split long warning messages at logical points $processedMessage = $Message -replace '\s{3,}', "`r`n " # Replace multiple spaces with newlines and indentation $processedMessage = $processedMessage -replace 'WARNING:\s+', "WARNING:`r`n " # Put WARNING on its own line } $logEntry = "[$timestamp] $processedMessage" # Update status text $syncHash.CyberRunStatus_TextBlock.Text = $Message # Set color based on level switch ($Level) { "Info" { $syncHash.CyberRunStatus_TextBlock.Foreground = $syncHash.Window.FindResource("PrimaryBrush") } "Warning" { $syncHash.CyberRunStatus_TextBlock.Foreground = [System.Windows.Media.Brushes]::Orange } "Error" { $syncHash.CyberRunStatus_TextBlock.Foreground = [System.Windows.Media.Brushes]::Red } "Success" { $syncHash.CyberRunStatus_TextBlock.Foreground = [System.Windows.Media.Brushes]::Green } } # Add to output log $syncHash.CyberRunOutput_TextBox.AppendText("$logEntry`r`n") $syncHash.CyberRunOutput_TextBox.ScrollToEnd() # Map "Success" to "Info" for debug output since Write-DebugOutput doesn't accept "Success" $debugLevel = if ($Level -eq "Success") { "Info" } else { $Level } If($Message) { Write-DebugOutput -Message $Message -Source $MyInvocation.MyCommand -Level $debugLevel } } Function Test-CyberRunReadiness { <# .SYNOPSIS Checks if CyberAssessment can be run (valid YAML generated). #> # Check if we have valid configuration data $hasValidConfig = $false # Check if products are selected if ($syncHash.GeneralSettingsData.ProductNames.Count -gt 0) { $hasValidConfig = $true }else{ Write-DebugOutput -Message "CyberRun not allowed. No products are selected." -Source $MyInvocation.MyCommand -Level "Error" } # Check if Organization is set (required) if ([string]::IsNullOrWhiteSpace($syncHash.GeneralSettingsData.Organization)) { $hasValidConfig = $false Write-DebugOutput -Message "CyberRun not allowed. Organization is not set." -Source $MyInvocation.MyCommand -Level "Error" } # Determine run mode based on AppId and CertificateThumbprint # If both AppId and CertificateThumbprint have values, it's non-interactive mode if (![string]::IsNullOrWhiteSpace($syncHash.AdvancedSettingsData.AppId) -and ![string]::IsNullOrWhiteSpace($syncHash.AdvancedSettingsData.CertificateThumbprint)) { $runMode = "[non-interactive mode]" }else{ $runMode = "[interactive mode]" } # Enable/disable run button $syncHash.CyberRunStart_Button.IsEnabled = $hasValidConfig # Only update status if we're not preserving a completion message if (-not $syncHash.JustCompletedExecution) { if ($hasValidConfig) { Update-CyberRunStatus -Message ($syncHash.UIConfigs.localeInfoMessages.CyberRunReady -f $runMode) -Level "Success" } else { Update-CyberRunStatus -Message $syncHash.UIConfigs.localeErrorMessages.CyberRunIncomplete -Level "Error" } } return $hasValidConfig } Function Start-CyberAssessmentExecution { <# .SYNOPSIS Starts CyberAssessment execution in a background job. #> try { # Test readiness if (-not (Test-CyberRunReadiness)) { Update-CyberRunStatus -Message $syncHash.UIConfigs.localeErrorMessages.CyberRunIncomplete -Level "Error" return } # Generate temporary YAML file $tempConfigPath = Export-TempYamlConfiguration if (-not $tempConfigPath) { Update-CyberRunStatus -Message $syncHash.UIConfigs.localeErrorMessages.CyberRunConfigFailed -Level "Error" return } # Update UI state $syncHash.CyberRunStart_Button.IsEnabled = $false $syncHash.CyberRunStop_Button.IsEnabled = $true $syncHash.CyberRunStart_Button.Visibility = "Collapsed" $syncHash.CyberRunStop_Button.Visibility = "Visible" Update-CyberRunStatus -Message "Starting CyberAssessment execution..." -Level "Info" # Build PowerShell command $command = Build-CyberAssessmentCommand -ConfigFilePath $tempConfigPath # Debug: Show commands in output $syncHash.CyberRunOutput_TextBox.AppendText("=== CYBERASSESSMENT EXECUTION STARTING ===`r`n") $syncHash.CyberRunOutput_TextBox.AppendText("Configuration file: $tempConfigPath`r`n") $syncHash.CyberRunOutput_TextBox.AppendText("Commands to execute:`r`n") foreach ($cmd in $command) { $syncHash.CyberRunOutput_TextBox.AppendText(" $cmd`r`n") } $syncHash.CyberRunOutput_TextBox.AppendText("=== EXECUTION OUTPUT ===`r`n") $syncHash.CyberRunOutput_TextBox.ScrollToEnd() # Start background job Start-CyberAssessmentJob -Command $command } catch { Update-CyberRunStatus -Message "Error starting CyberAssessment: $($_.Exception.Message)" -Level "Error" # Reset UI state Reset-CyberRunUI } } Function Export-TempYamlConfiguration { <# .SYNOPSIS Exports current configuration to a temporary YAML file. #> try { # Generate YAML content (reuse existing preview function) New-YamlPreview -NoRedirect # Get the generated YAML content from the UI $yamlContent = $syncHash.YamlPreview_TextBox.Text if ([string]::IsNullOrWhiteSpace($yamlContent)) { throw "No YAML content was generated. Please ensure all required fields are filled." } # Create temp directory if it doesn't exist $tempDir = Join-Path $env:TEMP "CyberConfigRun" if (-not (Test-Path $tempDir)) { New-Item -Path $tempDir -ItemType Directory -Force | Out-Null } # Create temp file $tempFileName = "CyberAssessmentConfig_$(Get-Date -Format 'yyyyMMdd_HHmmss').yaml" $tempFilePath = Join-Path $tempDir $tempFileName # Write YAML content [System.IO.File]::WriteAllText($tempFilePath, $yamlContent, [System.Text.Encoding]::UTF8) Update-CyberRunStatus -Message "Configuration exported to: $tempFilePath" -Level "Info" return $tempFilePath } catch { Update-CyberRunStatus -Message "Failed to export configuration: $($_.Exception.Message)" -Level "Error" return $null } } Function Build-CyberAssessmentCommand { <# .SYNOPSIS Builds the PowerShell command to execute CyberAssessment with required defaults and optional parameters. #> param([string]$ConfigFilePath) # Build command with module import and CyberAssessment execution $cyberConfig = $syncHash.UIConfigs.CyberRunConfig $cmdParts = @() # Add pre-commands from configuration (but skip module installation) if ($cyberConfig.powershell.PreCommands) { foreach ($preCommand in $cyberConfig.powershell.preCommands) { # Skip any Install-Module commands as they're likely to fail if ($preCommand -notlike "*Install-Module*") { $cmdParts += "$preCommand" } } } # Build the main CyberAssessment command with parameters $mainCommand = $cyberConfig.powershell.cmdlets $parameters = @() # REQUIRED DEFAULT PARAMETERS - Always include these $parameters += "-ConfigFilePath '$ConfigFilePath'" $organizationValue = $syncHash.Organization_TextBox.Text $parameters += "-Organization '$organizationValue'" # OPTIONAL PARAMETERS - Only add these if they have values and are not the removed defaults if ($cyberConfig.powershell.parameters) { foreach ($paramName in $cyberConfig.powershell.parameters.PSObject.Properties.Name) { $paramConfig = $cyberConfig.powershell.parameters.$paramName # Skip the removed default parameters (ConfigFilePath and Organization are handled above) if ($paramName -in @("CyberRunConfigFilePath", "CyberRunOrganization")) { continue } # Skip hidden parameters if ($paramConfig.hidden -eq $true) { continue } # Map parameter names to actual Invoke-Cyber parameters $actualParamName = $paramName switch ($paramConfig.type) { "string" { # Get value from UI controls $textBoxName = $paramName + "_TextBox" $textBox = $syncHash.$textBoxName if ($textBox -and ![string]::IsNullOrWhiteSpace($textBox.Text)) { $parameters += "-$actualParamName '$($textBox.Text)'" Write-DebugOutput -Message "Added optional string parameter: -$actualParamName '$($textBox.Text)'" -Source $MyInvocation.MyCommand -Level "Debug" } } "boolean" { $checkboxName = $paramName + "_CheckBox" $checkbox = $syncHash.$checkboxName if ($checkbox -and $checkbox.IsChecked) { $parameters += "-$actualParamName" Write-DebugOutput -Message "Added optional boolean parameter: -$actualParamName" -Source $MyInvocation.MyCommand -Level "Debug" } } "dropdown" { $comboBoxName = $paramName + "_ComboBox" $comboBox = $syncHash.$comboBoxName if ($null -ne $comboBox.SelectedItem) { If ($comboBox.SelectedItem -is [string]) { $parameters += "-$actualParamName '$($comboBox.SelectedItem)'" } else { $parameters += "-$actualParamName $($comboBox.SelectedItem)" } Write-DebugOutput -Message "Added optional dropdown parameter: -$actualParamName '$($comboBox.SelectedItem)'" -Source $MyInvocation.MyCommand -Level "Debug" } } } } } # Combine main command with parameters $fullCommand = "$mainCommand $($parameters -join ' ')" $cmdParts += $fullCommand # Add post-commands from configuration if ($cyberConfig.powershell.PostCommands) { foreach ($postCommand in $cyberConfig.powershell.postCommands) { $cmdParts += $postCommand } } # Log the commands for debugging Write-DebugOutput -Message "Built CyberAssessment commands:" -Source $MyInvocation.MyCommand -Level "Info" foreach ($cmd in $cmdParts) { Write-DebugOutput -Message " Command: $cmd" -Source $MyInvocation.MyCommand -Level "Info" } # Also update the UI to show what will be executed Update-CyberRunStatus -Message "Prepared commands: $($cmdParts.Count) commands ready" -Level "Info" return $cmdParts } Function Start-CyberAssessmentJob { <# .SYNOPSIS Starts CyberAssessment in a background PowerShell process with real-time output capture. #> param([string[]]$Command) # Get what powershell version to run based on configuration $psVersion = $syncHash.UIConfigs.CyberRunConfig.powershell.version if($psVersion -eq "5.1") { $poshPath = "$env:WINDIR\System32\WindowsPowerShell\v1.0\powershell.exe" } else { $poshPath = "$env:ProgramFiles\PowerShell\7\pwsh.exe" } # Create a temporary script file to execute all commands in sequence $tempScriptDir = Join-Path $env:TEMP "CyberConfigRun" if (-not (Test-Path $tempScriptDir)) { New-Item -Path $tempScriptDir -ItemType Directory -Force | Out-Null } $tempScriptPath = Join-Path $tempScriptDir "CyberAssessmentExecution_$(Get-Date -Format 'yyyyMMdd_HHmmss').ps1" # Create enhanced script content with real-time output $scriptContent = @" # Enhanced script for real-time output capture `$ErrorActionPreference = 'Continue' # Function to write timestamped output function Write-TimestampedOutput { param([string]`$Message, [string]`$Type = 'Info') `$timestamp = Get-Date -Format 'HH:mm:ss' Write-Host "[`$timestamp] [`$Type] `$Message" -ForegroundColor `$(if (`$Type -eq 'Error') { 'Red' } elseif (`$Type -eq 'Warning') { 'Yellow' } else { 'Green' }) } Write-TimestampedOutput "Starting CyberAssessment execution script..." "@ # Add each command with proper variable expansion foreach ($cmd in $Command) { $scriptContent += @" Write-TimestampedOutput "Executing: $cmd" -Level "Info" try { # Execute the command directly, allowing variable expansion $cmd Write-TimestampedOutput "Command completed successfully" -Level "Info" } catch { Write-TimestampedOutput "ERROR executing command: `$(`$_.Exception.Message)" -Level "Error" } "@ } $scriptContent += @" Write-TimestampedOutput "CyberAssessment execution script completed." -Level "Info" "@ # Write script to file $scriptContent | Out-File -FilePath $tempScriptPath -Encoding UTF8 # Store execution start time for finding the results folder $syncHash.CyberAssessmentExecutionStartTime = Get-Date # Create a job with real-time output streaming $job = Start-Job -ScriptBlock { # Use the appropriate PowerShell executable $poshExecutable = $using:poshPath if (-not (Test-Path $poshExecutable)) { # Fallback to PowerShell 5.1 if the specified path doesn't exist $poshExecutable = "$env:WINDIR\System32\WindowsPowerShell\v1.0\powershell.exe" } # Execute the script file and capture all output & $poshExecutable -ExecutionPolicy Bypass -File $using:tempScriptPath } # Store job reference and script path for cleanup $syncHash.CyberRunExecutionJob = $job $syncHash.TempScriptPath = $tempScriptPath # Start enhanced monitoring for real-time output Start-CyberAssessmentMonitoringRealTime } Function Find-CyberAssessmentResultFolder { <# .SYNOPSIS Finds the most recently created CyberAssessment results folder. #> param([datetime]$StartTime) try { # Common locations where CyberAssessment creates output folders $searchPaths = @( "$env:USERPROFILE\Documents", ".", "$env:USERPROFILE\Desktop" ) # Get the folder base name - check UI controls first, then fall back to defaults $baseName = "M365BaselineConformance" $reportName = "BaselineReports" # Try to get values from UI controls if they exist and have actual values $ReportPathValue = $syncHash.OutFolderName_TextBox.Text if (![string]::IsNullOrWhiteSpace($ReportPathValue)) { $baseName = $syncHash.UIConfigs.localePlaceholder.OutFolderName_TextBox Write-DebugOutput -Message "Folder placeholder value: '$baseName'" -Source $MyInvocation -Level "Debug" } $reportNameValue = $syncHash.OutReportName_TextBox.Text if (![string]::IsNullOrWhiteSpace($reportNameValue)) { if ($syncHash.UIConfigs.localePlaceholder.OutReportName_TextBox) { $reportName = $syncHash.UIConfigs.localePlaceholder.OutReportName_TextBox Write-DebugOutput -Message "Report placeholder value: '$reportName'" -Source $MyInvocation.MyCommand -Level "Debug" } } Write-DebugOutput -Message "Looking for folders with base name: '$baseName' and report name: '$reportName'" -Source $MyInvocation.MyCommand -Level "Info" $mostRecentFolder = $null $mostRecentTime = [datetime]::MinValue foreach ($searchPath in $searchPaths) { if (Test-Path $searchPath) { # Look for folders with the pattern: BaseName_YYYY_MM_DD_HH_MM_SS $searchPattern = "$baseName*" Write-DebugOutput -Message "Searching in '$searchPath' for pattern: '$searchPattern'" -Source $MyInvocation.MyCommand -Level "Debug" $cyberFolders = Get-ChildItem -Path $searchPath -Directory -Filter $searchPattern -ErrorAction SilentlyContinue | Where-Object { # Check if folder was created after start time (with 2 minute buffer) $_.CreationTime -gt $StartTime.AddMinutes(-2) -and # Additional check: folder name should match the expected pattern $_.Name -like "$baseName*" } | Sort-Object CreationTime -Descending Write-DebugOutput -Message "Found $($cyberFolders.Count) matching folders in '$searchPath'" -Source $MyInvocation.MyCommand -Level "Debug" if ($cyberFolders -and $cyberFolders.Count -gt 0) { $newestInThisPath = $cyberFolders[0] Write-DebugOutput -Message "Newest folder in this path: '$($newestInThisPath.FullName)' (Created: $($newestInThisPath.CreationTime))" -Source $MyInvocation.MyCommand -Level "Debug" if ($newestInThisPath.CreationTime -gt $mostRecentTime) { $mostRecentFolder = $newestInThisPath $mostRecentTime = $newestInThisPath.CreationTime } } } } if ($mostRecentFolder) { Write-DebugOutput -Message "Most recent folder found: '$($mostRecentFolder.FullName)'" -Source $MyInvocation.MyCommand -Level "Info" # Check if the HTML report exists with the expected name $htmlFile = Join-Path $mostRecentFolder.FullName "$reportName.html" Write-DebugOutput -Message "Looking for HTML file: '$htmlFile'" -Source $MyInvocation.MyCommand -Level "Debug" if (Test-Path $htmlFile) { Write-DebugOutput -Message "HTML report found: '$htmlFile'" -Source $MyInvocation.MyCommand -Level "Info" return @{ Type = "HTML" Path = $htmlFile Folder = $mostRecentFolder.FullName } } else { # Try to find any HTML file in the folder as fallback $htmlFiles = Get-ChildItem -Path $mostRecentFolder.FullName -Filter "*.html" -ErrorAction SilentlyContinue if ($htmlFiles -and $htmlFiles.Count -gt 0) { $fallbackHtml = $htmlFiles[0].FullName Write-DebugOutput -Message "Using fallback HTML file: '$fallbackHtml'" -Source $MyInvocation.MyCommand -Level "Info" return @{ Type = "HTML" Path = $fallbackHtml Folder = $mostRecentFolder.FullName } } else { Write-DebugOutput -Message "No HTML files found in folder, returning folder path" -Source $MyInvocation.MyCommand -Level "Info" return @{ Type = "Folder" Path = $mostRecentFolder.FullName Folder = $mostRecentFolder.FullName } } } } else { Write-DebugOutput -Message "No matching CyberAssessment folders found" -Source $MyInvocation.MyCommand -Level "Error" } } catch { Write-DebugOutput -Message "Error finding CyberAssessment results: $($_.Exception.Message)" -Source $MyInvocation.MyCommand -Level "Error" } return $null } Function Start-CyberAssessmentMonitoringRealTime { <# .SYNOPSIS Monitors the CyberAssessment job progress with real-time output capture. #> $timer = New-Object System.Windows.Threading.DispatcherTimer $timer.Interval = [TimeSpan]::FromSeconds(1) # Check more frequently for real-time feel # Track what output we've already processed $script:lastOutputCount = 0 # Fun cyber-themed running messages $script:cyberRunningMessages = @( "Diving deep... CyberAssessment is exploring your settings!", "Checking your dive gear... almost ready to surface!", "Underwater operations in progress... please hold your breath!", "Navigating the reef of configurations... stay tuned!", "CyberAssessment is adjusting your dive computer... one fin stroke at a time!", "Making waves... your CyberAssessment is on the move!", "Sonar ping! CyberAssessment is scanning for updates!", "Bubbles rising... CyberAssessment is bubbling with activity!", "Submerging into configuration depths... please hang tight!", "Gear check complete... CyberAssessment is on the ascent!", "Swimming through policies... current is strong but steady!", "Exploring the coral reef of compliance... beautiful formations ahead!", "Avoiding the sharks of misconfigurations... smooth sailing!", "Tentacles deep in your tenant... mapping every corner!", "Freestyle stroke through your security settings!", "Oxygen levels good... continuing the deep dive!", "Anchored in your environment... collecting treasures of insight!", "Charting the underwater map of your M365 landscape!" ) # Status update tracking (update every 3-4 seconds instead of every second) $script:statusUpdateCounter = 0 $script:statusUpdateInterval = 3 # Update status every 3 timer ticks (3 seconds) $script:currentMessageIndex = 0 $timer.Add_Tick({ if ($syncHash.CyberRunExecutionJob) { $job = $syncHash.CyberRunExecutionJob # Capture any new output that's available try { $newOutput = Receive-Job -Job $job -Keep if ($newOutput -and $newOutput.Count -gt $script:lastOutputCount) { # Process only new output lines $newLines = $newOutput[$script:lastOutputCount..($newOutput.Count - 1)] foreach ($line in $newLines) { if (![string]::IsNullOrWhiteSpace($line)) { # Add to output textbox in real-time $syncHash.CyberRunOutput_TextBox.AppendText("$line`r`n") $syncHash.CyberRunOutput_TextBox.ScrollToEnd() } } $script:lastOutputCount = $newOutput.Count } } catch { Write-DebugOutput -Message "Error receiving job output: $($_.Exception.Message)" -Source $MyInvocation.MyCommand -Level "Error" } # Check job state switch ($job.State) { "Running" { # Only update status message every few seconds with rotating messages $script:statusUpdateCounter++ if ($script:statusUpdateCounter -ge $script:statusUpdateInterval) { # Get next message in rotation #$currentMessage = $script:cyberRunningMessages[$script:currentMessageIndex] #Update-CyberRunStatus -Message $currentMessage -Level "Info" #get random message from list $randomMessage = Get-Random -InputObject $script:cyberRunningMessages Update-CyberRunStatus -Message $randomMessage -Level "Info" # Move to next message (wrap around at end) $script:currentMessageIndex = ($script:currentMessageIndex + 1) % $script:cyberRunningMessages.Count $script:statusUpdateCounter = 0 # Vary the interval slightly for more natural feel (3-5 seconds) $script:statusUpdateInterval = Get-Random -Minimum 3 -Maximum 6 } } "Completed" { # Get any final output $finalOutput = Receive-Job -Job $job if ($finalOutput -and $finalOutput.Count -gt $script:lastOutputCount) { $finalLines = $finalOutput[$script:lastOutputCount..($finalOutput.Count - 1)] foreach ($line in $finalLines) { if (![string]::IsNullOrWhiteSpace($line)) { $syncHash.CyberRunOutput_TextBox.AppendText("$line`r`n") } } } # Now try to find the results folder $resultsInfo = Find-CyberAssessmentResultFolder -StartTime $syncHash.CyberAssessmentExecutionStartTime if ($resultsInfo) { # Export configuration YAML to the results folder Export-ConfigurationToResultsFolder -ResultsFolder $resultsInfo.Folder $syncHash.CyberRunOutput_TextBox.AppendText("`r`n*** EXECUTION COMPLETE! ***`r`n") if ($resultsInfo.Type -eq "HTML") { $syncHash.CyberRunOutput_TextBox.AppendText("Results available at: $($resultsInfo.Path)`r`n") $syncHash.CyberRunOutput_TextBox.AppendText("Tip: Copy this path and paste it into your browser to view the report`r`n") # Update status with the baseline conformance report path Update-CyberRunStatus -Message "CyberAssessment Complete |Report: $($resultsInfo.Path)" -Level "Success" } else { $syncHash.CyberRunOutput_TextBox.AppendText("Results folder: $($resultsInfo.Path)`r`n") # Update status with folder path Update-CyberRunStatus -Message "CyberAssessment Complete | Folder: $($resultsInfo.Path)" -Level "Success" } $syncHash.CyberRunOutput_TextBox.AppendText("Full results folder: $($resultsInfo.Folder)`r`n") # Store the results folder path for the View Configuration button $syncHash.LastCyberRunResultsFolder = $resultsInfo.Folder } else { # Enhanced fallback message with more specific guidance Update-CyberRunStatus -Message "CyberAssessment Complete | Check Documents folder for results" -Level "Success" $syncHash.CyberRunOutput_TextBox.AppendText("`r`n*** EXECUTION COMPLETE! ***`r`n") $syncHash.CyberRunOutput_TextBox.AppendText("Check your Documents folder for M365BaselineConformance_* folders`r`n") } Complete-CyberAssessmentExecution $this.Stop() # Cleanup temp script if ($syncHash.TempScriptPath -and (Test-Path $syncHash.TempScriptPath)) { try { Remove-Item -Path $syncHash.TempScriptPath -Force -ErrorAction SilentlyContinue $syncHash.TempScriptPath = $null } catch { Write-DebugOutput -Message "Error cleaning up temp script file: $($_.Exception.Message)" -Source $MyInvocation.MyCommand -Level "Error" } } } "Failed" { $failureReason = "" try { $jobError = $job.ChildJobs[0].Error if ($jobError) { $failureReason = ": $($jobError[-1].Exception.Message)" } } catch { Write-DebugOutput -Message "Error extracting job error message: $($_.Exception.Message)" -Source $MyInvocation.MyCommand -Level "Error" } Update-CyberRunStatus -Message "CyberAssessment execution failed: $failureReason" -Level "Error" Complete-CyberAssessmentExecution $this.Stop() } "Stopped" { Update-CyberRunStatus -Message "CyberAssessment execution was stopped" -Level "Error" Complete-CyberAssessmentExecution $this.Stop() } } } }) $syncHash.CyberRunExecutionTimer = $timer $timer.Start() } Function Stop-CyberAssessmentExecution { <# .SYNOPSIS Stops the running CyberAssessment job. #> if ($syncHash.CyberRunExecutionJob) { Stop-Job -Job $syncHash.CyberRunExecutionJob -Force Remove-Job -Job $syncHash.CyberRunExecutionJob -Force $syncHash.CyberRunExecutionJob = $null } if ($syncHash.CyberRunExecutionTimer) { $syncHash.CyberRunExecutionTimer.Stop() $syncHash.CyberRunExecutionTimer = $null } # Cleanup temporary script file if ($syncHash.TempScriptPath -and (Test-Path $syncHash.TempScriptPath)) { try { Remove-Item -Path $syncHash.TempScriptPath -Force -ErrorAction SilentlyContinue $syncHash.TempScriptPath = $null } catch { Write-DebugOutput -Message "Error cleaning up temp script file: $($_.Exception.Message)" -Source $MyInvocation.MyCommand -Level "Error" } } Update-CyberRunStatus -Message "Execution stopped by user" -Level "Error" # Clear completion flag since this is a manual stop, not a completion $syncHash.JustCompletedExecution = $false Reset-CyberRunUI } Function Complete-CyberAssessmentExecution { <# .SYNOPSIS Completes CyberAssessment execution and updates UI. #> # Cleanup temporary script file if ($syncHash.TempScriptPath -and (Test-Path $syncHash.TempScriptPath)) { try { Remove-Item -Path $syncHash.TempScriptPath -Force -ErrorAction SilentlyContinue $syncHash.TempScriptPath = $null } catch { Write-DebugOutput -Message "Error cleaning up temp script file: $($_.Exception.Message)" -Source $MyInvocation.MyCommand -Level "Error" } } # Set flag to indicate we just completed execution (preserve status message) $syncHash.JustCompletedExecution = $true # Enable View Configuration button if we have a results folder if ($syncHash.LastCyberRunResultsFolder) { $configFilePath = Join-Path $syncHash.LastCyberRunResultsFolder "CyberAssessmentConfiguration.yaml" if (Test-Path $configFilePath) { $syncHash.CyberRunViewConfig_Button.IsEnabled = $true Write-DebugOutput -Message "Enabled View Configuration button for: $configFilePath" -Source $MyInvocation.MyCommand -Level "Info" } } # Ensure Results tab is enabled and refresh it If($syncHash.UIConfigs.EnableResultReader) { $syncHash.ResultsTab.IsEnabled = $true Update-ResultsTab } Reset-CyberRunUI } Function Start-CyberAssessmentMonitoring { <# .SYNOPSIS Monitors the CyberAssessment job progress. #> $timer = New-Object System.Windows.Threading.DispatcherTimer $timer.Interval = [TimeSpan]::FromSeconds(2) $timer.Add_Tick({ if ($syncHash.CyberRunExecutionJob) { $job = $syncHash.CyberRunExecutionJob # Check job state switch ($job.State) { "Running" { Update-CyberRunStatus -Message "CyberAssessment is running..." -Level "Info" # You could parse output here if available } "Completed" { $results = Receive-Job -Job $job Update-CyberRunStatus -Message "CyberAssessment completed successfully" -Level "Success" $syncHash.CyberRunOutput_TextBox.AppendText("$results`r`n") Complete-CyberAssessmentExecution $this.Stop() } "Failed" { $null = Receive-Job -Job $job -ErrorAction SilentlyContinue Update-CyberRunStatus -Message "CyberAssessment failed: $error" -Level "Error" Complete-CyberAssessmentExecution $this.Stop() } "Stopped" { Update-CyberRunStatus -Message "CyberAssessment execution was stopped" -Level "Error" Complete-CyberAssessmentExecution $this.Stop() } } } }) $syncHash.CyberRunExecutionTimer = $timer $timer.Start() } Function Reset-CyberRunUI { <# .SYNOPSIS Resets the UI to ready state. #> $syncHash.CyberRunStart_Button.IsEnabled = $true $syncHash.CyberRunStop_Button.IsEnabled = $false $syncHash.CyberRunStop_Button.Visibility = "Collapsed" $syncHash.CyberRunStart_Button.Visibility = "Visible" # Only show "Ready to run" status if we haven't just completed execution if (-not $syncHash.JustCompletedExecution) { Test-CyberRunReadiness } else { # Clear the flag for next time $syncHash.JustCompletedExecution = $false } } |