CommandCentral.psm1

<#
.DESCRIPTION
  Provides a easy to use and customizable menu to launch scripts with repetitve parameters (i.e. passwords, hostnames, options), additionally provides automation capabilities for running jobs.

.NOTES
  Version: 1.0
  Author: Admiral-AI
  Portfolio: https://github.com/Admiral-AI
  Creation Date: January 4, 2024
#>


function Start-CCMain {

    # Set script location variable (removes ambiguity and makes testing easier)
    Set-Variable -Name scriptLocation -Scope Script
    $scriptLocation = $PSScriptRoot

    # Set starting location (should'nt be an issue but I wanted peace of mind)
    Set-Location $scriptLocation

    # Import settings file and set the variable to script access (indicates that all scripts run from this one can access the variable when passed)
    Set-Variable -Name settingsFile -Scope Script
    try {
        $settingsFile = Get-Content .\etc\settings.json -ErrorAction Stop | ConvertFrom-Json
    }
    catch {
        Write-Host "Setting file is unreadable, please re-download the script or fix your setting file"
        Start-Sleep 2
        exit
    }

    # Set transcript log path based on json setting file parameter
    Start-Transcript -Path $($settingsFile.CCScriptSettings.Logging.Log_Path) -Append -Force

    # Set the debug prefrence based on the logging setting
    $DebugPreference = $($settingsFile.CCScriptSettings.Logging.DebugPreference)

    # Clear the console
    Clear-Host

    # Start the domino effect of functions:
    $continueChainHop1_Boolean = Update-CCDependencies

    if ($continueChainHop1_Boolean -eq $true) {
        $returnHashTable = Enable-CCVault

        Set-Variable -Name userVaultPassword -Scope Script
        $userVaultPassword = $returnHashTable.userInputPassword
        $continueChainBoolean_Hop2 = $returnHashTable.continueChainBoolean_Hop2
    }

    if ($continueChainBoolean_Hop2 -eq $true) {
        $continueChainBoolean_Hop3 = Start-CCJobs
    }

    if ($continueChainBoolean_Hop3 -eq $true) {
        Out-CCMenu | Out-Null
    }

    # Script will return here when you quit the menu function at the end of the script
    # Reset location before exiting
    Set-Location $scriptLocation

    # Stop Transcript when finished with script
    Stop-Transcript
}

function Update-CCDependencies {
    $restartScriptBoolean = $false

    # Command Central script updater
    if ($settingsFile.CCScriptSettings.Updates.CC_UpdatesOptIn -eq $true) {
        # Initialize some settings into variables for cleanliness
        $selectedRepo = $settingsFile.CCScriptSettings.Updates.CC_UpdateSettings.SelectedRepo
        $repoUriToPullFrom = $settingsFile.CCScriptSettings.Updates.CC_UpdateSettings.Repositories.$($selectedRepo).CCScriptUri
        $repoAccessToken = $settingsFile.CCScriptSettings.Updates.CC_UpdateSettings.Repositories.$($selectedRepo).AuthToken
        $repoUriType = $settingsFile.CCScriptSettings.Updates.CC_UpdateSettings.Repositories.$($selectedRepo).UriType
        $repoAccessAPI = $settingsFile.CCScriptSettings.Updates.CC_UpdateSettings.Repositories.$($selectedRepo).AccessAPI

        if ($true -eq $false) {
            # Catch-22
        } elseif ($repoUriType -match "HTTPS") {
            # Used for matching against HTTP(S) URLs
            if (($repoAccessAPI -match 'GitHub')) {  
                # Actions to take if Repository API is github
                if ($null -ne $repoAccessToken) {
                    $webHeadersForRepo = @{
                        "Accept" = "application/vnd.github.raw+json"
                        "Authorization" = "Bearer $($repoAccessToken)"
                        "X-GitHub-Api-Version" = "2022-11-28"
                    } 
                } else {
                    $webHeadersForRepo = @{
                        "Accept" = "application/vnd.github.raw+json"
                        "X-GitHub-Api-Version" = "2022-11-28"
                    }  
                }

                try {
                    $returnedCCScriptData = Invoke-RestMethod -Method Get -Uri $repoUriToPullFrom -Headers $webHeadersForRepo -ErrorAction Stop
                } catch {
                    Write-Error "Could not access the Url provided, check that the file exists and the token is valid"
                }
            # Do a plain Get request if no supported domain is matched or auth token is found
            } else {
                $returnedCCScriptData = Invoke-RestMethod -Method Get -Uri $repoUriToPullFrom
            }
        } elseif ($repoUriType -match "SMB") {
            # Used for matching against SMB URLs
            try {
                $returnedCCScriptData = Get-Content -Path $repoUriToPullFrom -Raw
            } catch {
                Write-Error "Could not access the Url provided, check that the file and/or share exists"
            }
        }

        # Take the retrieved data and check if different from local copy
        if ($null -ne $returnedCCScriptData) {
            $currentCCScriptData = Get-Content -Path $($PSCommandPath) -Raw
            
            # Use Compare-Object to compare the contents of the two variables
            $comparisonResults = Compare-Object -ReferenceObject ($currentCCScriptData) -DifferenceObject ($returnedCCScriptData)

            if ($null -eq $comparisonResults) {
                Write-Host "No updates found."
            # If the variable is not null then the comparison found differences, the else statement will run updates
            } else {
                Write-Host "Update found! Updating local script."
                try {
                    $returnedCCScriptData | Out-File -FilePath $($PSCommandPath) -NoNewline -ErrorAction Stop
                    $restartScriptBoolean = $true
                } catch {
                    Write-Error "Could not write update to file, check the log for details"
                    $restartScriptBoolean = $false
                }
            }
        } else {
            # Update script variable was empty indicating a problem
            Write-Error "Updates failed, check the log for details."
        }
    } else {
        Write-Debug "Script updates are not enabled..."
    }

    # PowerShell module dependency installer and updater
    if ($settingsFile.CCScriptSettings.Updates.PSModules_UpdatesOptIn -eq $true) {
        # Initialize some settings into variables for cleanliness
        $modulePropertiesFromSettings = $settingsFile.CCScriptSettings.Updates.PSModules_UpdateSettings.Modules

        $modulePropertyNamesFromSettings = $modulePropertiesFromSettings | Get-Member -MemberType Properties

        $modulePropertyNamesFromSettings.Name | ForEach-Object {
            # Make sure to save the version number as a [System.Version] object for proper boolean results
            $moduleUpdateParams = @{
                Name = $($modulePropertiesFromSettings.$($_).Name)
                MinimumVersion = [System.Version]$($modulePropertiesFromSettings.$($_).MinimumVersion)
                MaximumVersion = [System.Version]$($modulePropertiesFromSettings.$($_).MaximumVersion)
                RequiredVersion = [System.Version]$($modulePropertiesFromSettings.$($_).RequiredVersion)
                Repository = $($modulePropertiesFromSettings.$($_).Repository)
                Credential = $($modulePropertiesFromSettings.$($_).Credential)
                Scope = $($modulePropertiesFromSettings.$($_).Scope)
                SkipPublisherCheck = $($modulePropertiesFromSettings.$($_).SkipPublisherCheck)
                Force = $($modulePropertiesFromSettings.$($_).Force)
                AllowPrerelease = $($modulePropertiesFromSettings.$($_).AllowPrerelease)
                AcceptLicense = $($modulePropertiesFromSettings.$($_).AcceptLicense) 
                Confirm = $($modulePropertiesFromSettings.$($_).Confirm)
            }

            # Install-Module does not play nicely with null values when splatting, remove null values
            @($moduleUpdateParams.Keys) | ForEach-Object {
                if ($null -eq  $moduleUpdateParams[$_]) {
                    $moduleUpdateParams.Remove($_)
                }
            }
            
            $currentlyInstalledModule = Get-InstalledModule | Where-Object {($_.Name -eq $($moduleUpdateParams.Name)) -and ($_.Repository -eq $($moduleUpdateParams.Repository))}
            if ($null -ne $currentlyInstalledModule) {
                if ($true -eq $false) {
                } elseif ($null -ne $moduleUpdateParams.RequiredVersion) {
                    if ([System.Version]$currentlyInstalledModule.Version -eq $moduleUpdateParams.RequiredVersion) {
                        Write-Debug "$($moduleUpdateParams.Name) is installed with the correct version, skipping update"
                        $updateCurrentModule = $false
                    } else {
                        Write-Debug "$($moduleUpdateParams.Name) is installed without the correct version, updating module"
                        $updateCurrentModule = $true
                        $removeCurrentModule = $true
                    }
                } elseif (($null -ne $moduleUpdateParams.MinimumVersion) -or ($null -ne $moduleUpdateParams.MaximumVersion)) {
                    if ($true -eq $false) {
                    } elseif (($null -ne $moduleUpdateParams.MinimumVersion) -and ($null -ne $moduleUpdateParams.MaximumVersion)) {
                        $minimumBoolean = ([System.Version]$currentlyInstalledModule.Version -ge $moduleUpdateParams.MinimumVersion)
                        $maximumBoolean = ([System.Version]$currentlyInstalledModule.Version -le $moduleUpdateParams.MaximumVersion)
                        if (($minimumBoolean -eq $true) -and ($maximumBoolean -eq $true)) {
                            $updateCurrentModule = $false
                        } else {
                            $updateCurrentModule = $true
                        }
                    } elseif (($null -ne $moduleUpdateParams.MinimumVersion) -and ($null -eq $moduleUpdateParams.MaximumVersion)) {
                        if ([System.Version]$currentlyInstalledModule.Version -ge $moduleUpdateParams.MinimumVersion) {
                            $updateCurrentModule = $false
                        } else {
                            $updateCurrentModule = $true
                        }
                    } elseif (($null -ne $moduleUpdateParams.MaximumVersion) -and ($null -eq $moduleUpdateParams.MinimumVersion)) {
                        if ([System.Version]$currentlyInstalledModule.Version -le $moduleUpdateParams.MaximumVersion) {
                            $updateCurrentModule = $false
                        } else {
                            $updateCurrentModule = $true
                        }
                    }
                } elseif (($null -eq $moduleUpdateParams.MinimumVersion) -and ($null -eq $moduleUpdateParams.MaximumVersion) -and ($null -eq $moduleUpdateParams.RequiredVersion)) {
                    Write-Debug "Module: $($moduleUpdateParams.Name) is installed as evidenced by Get-InstalledModule but no version is specified, no further work needed"
                    $updateCurrentModule = $false
                } else {
                    Write-Debug "You shouldn't end up here, check the log, `$moduleUpdateParams variable, and Get-InstalledModule cmdlet for issues"
                }
            } else {
                Write-Host "Module: $($moduleUpdateParams.Name) not found, installing..."
                $updateCurrentModule = $true
            }

            if ($updateCurrentModule -eq $true) {
                try {
                    if ($removeCurrentModule -eq $true) {
                        Uninstall-Module -Name $moduleUpdateParams.Name -AllVersions -ErrorAction SilentlyContinue
                    }
                    Install-Module @moduleUpdateParams -ErrorAction Stop
                    Write-Host "Updated/Installed Module: $($moduleUpdateParams.Name)"
                } catch {
                    Write-Error "Updating/Installing module: $($moduleUpdateParams.Name); Failed, check the log for details"
                }
            } else {
                Write-Debug "Module not updated: $($moduleUpdateParams.Name)"
            }
        }
    } else {
        Write-Debug "Powershell module updates are not enabled..."
    }

    # Custom dependency installer and updater (AD, Keepass app, e.t.c.)
    if ($settingsFile.CCScriptSettings.Updates.Custom_UpdatesOptIn -eq $true) {

    } else {
        Write-Debug "App updates are not enabled..."
    }

    # Script restarter (uses $restartScriptBoolean)
    if ($restartScriptBoolean -eq $true) {
        if (($PSVersionTable.PSEdition -like "Core") -and ($IsWindows -eq $true)) {
            Write-Host "Automatic script restart is broken at this time, please restart the script manually."
            Start-Sleep .75
            # Write-Host "The script will now restart to apply updates..."
            # Start-Process pwsh -ArgumentList "-File `"$($PSCommandPath)`""
        } elseif (($PSVersionTable.PSEdition -like "Core") -and ($IsWindows -ne $true)) {
            # Start-Process pwsh is broken in powershell Core on non-windows platforms
            # To-do: Find a workaround
            Write-Host "Automatic script restart is not supported for Linux & MacOS, please restart the script manually."
            Start-Sleep .75
        }
        Write-Host "CommandCentral will now exit"
        exit
    } else {
        # No updates were found, return true in order to head to vault setup
        return $true
    }
}

function Enable-CCVault {
    # Initialize vault reset function
    function Reset-CCVault {
        if ($true -eq $false) {
        # Keepass secret vault provider is selected (default):
        } elseif ($settingsFile.CCScriptSettings.PasswordVault.ExtensionProvider -eq "KeePass") {
            $vaultPath = Join-Path -Path $scriptLocation -ChildPath "var/PasswordVault.kdbx"
            
            if ($settingsFile.CCScriptSettings.PasswordVault.Provider_KeePass.VaultAuthMethod -eq "Password") {
                # Request and validate a password
                $requestPasswordLoopControlBoolean = $true
                while ($requestPasswordLoopControlBoolean -eq $true) {
                    $userInputPassword = Read-Host -Prompt "Enter a password for the database" -MaskInput

                    # To-do: Validate the password based on the setting file requirements
                    if ($true -eq $true) {
                        $userInputPassword = ConvertTo-SecureString -String $userInputPassword -AsPlainText
                        $requestPasswordLoopControlBoolean = $false
                    }
                }
            }

            # If the path test returns true then there is a vault there already, rename it to prevent an error when creating a new one.
            if ((Test-Path -Path $vaultPath) -eq $true) {
                # Format: MM = month; dd = day; yy = 2 digit year; HH = hour; mm = minute; tt = AM/PM
                Move-Item $vaultPath "./var/ResetVault_$(Get-Date -Format MMddyy_HHmmtt).kdbx"
            }

            $installedVaults = Get-SecretVault | Where-Object { $_.Name -eq "CommandCentral_PasswordVault"}
            if ($null -ne $installedVaults) {
                Unregister-SecretVault -Name "CommandCentral_PasswordVault"
            }
            
            if ($settingsFile.CCScriptSettings.PasswordVault.Provider_KeePass.VaultAuthMethod -eq "Password") {
                Register-KeePassSecretVault -Name "CommandCentral_PasswordVault" -Path $vaultPath -UseMasterPassword -MasterPassword $userInputPassword -Create
            } elseif (($settingsFile.CCScriptSettings.PasswordVault.Provider_KeePass.VaultAuthMethod -eq "WindowsAccount")) {
                Register-KeePassSecretVault -Name "CommandCentral_PasswordVault" -Path $vaultPath -UseWindowsAccount -Create
            }

            Write-Host "We will now test the vault functions, please re-input the password if needed"
            $vaultTestBoolean = Test-SecretVault -Name CommandCentral_PasswordVault

            if ($vaultTestBoolean -eq $true) {
                Write-Host "Vault is ready for use"
            } else {
                Write-Error "There shouldn't be an error here"
                Write-Error "Something is critically wrong with the vault and/or module"
                Write-Host "Exiting Script..."
                Start-Sleep 2
                exit
            }
            return $userInputPassword
        }
    }
    
    # Set vault password to script scope
    Set-Variable -Name userInputPassword -Scope Script

    # Validate vault setup
    $installedVaults = Get-SecretVault | Where-Object { $_.Name -eq "CommandCentral_PasswordVault"}
    if ($null -eq $installedVaults) {
        $userInputPassword = Reset-CCVault
    } else {
        $vaultTestBoolean = $true
        while ($vaultTestBoolean -eq $true) {
            # Statement block for keepass vault providers
            if ($settingsFile.CCScriptSettings.PasswordVault.ExtensionProvider -ne "KeePass") {
                $vaultTestBoolean = Test-Path $installedVaults.VaultParameters.Path

                if ($true -eq $false) {    
                } elseif ($settingsFile.CCScriptSettings.PasswordVault.Provider_KeePass.VaultAuthMethod -eq "Password") {
                    Write-Host "We will now test the vault functions, please input your vault password."
                    $userInputPassword = Read-Host -Prompt "-->" -AsSecureString
                    Unlock-SecretVault -Name CommandCentral_PasswordVault -Password $userInputPassword
                } elseif ($settingsFile.CCScriptSettings.PasswordVault.Provider_KeePass.VaultAuthMethod -eq "WindowsAccount") {
                    # No need for testing, windows account auth will unlock the vault automatically
                    Write-Host "We will now test the vault functions using your windows account."
                }
            }

            # All vault providers continue here by testing the vault
            if ($vaultTestBoolean -eq $true) {
                $vaultTestBoolean = Test-SecretVault -Name CommandCentral_PasswordVault
            }

            # If the test is succesful, continue here
            if ($vaultTestBoolean -eq $true) {
                Write-Host "Vault is ready for use"
                $vaultTestBoolean = $false
            } else {
                Start-Sleep 2
                Clear-Host
                Write-Host "Vault failed the function test"
                Write-Host "Do you want to retry your password?"
                Write-Host "Or do you want to reset the vault?"
                Write-Host "Options: Reset or Retry. (Retry is default)"
                Write-Host ""
                $userInputChoice = Read-Host "[retry]-->"
                if ($userInputChoice -like "reset") {
                    $userInputPassword = Reset-CCVault
                    $vaultTestBoolean = $false
                }
            }
        }
    }
    $returnHashTable = @{
        continueChainBoolean_Hop2 = $true
        userInputPassword = $userInputPassword
    }
    return $returnHashTable
}

function Start-CCJobs {
    if ($settingsFile.CCScriptSettings.CCJobs.CCJobs_OptIn -eq $true) {
        # Initialize settings for cleanliness
        $jobListPropertiesFromSettings = $settingsFile.CCScriptSettings.CCJobs.JobsList
        $sessionThreads = $settingsFile.CCScriptSettings.CCJobs.ThrottleLimit

        # Get each job name from the properties
        $jobNamesFromSettings = $jobListPropertiesFromSettings | Get-Member -Type Properties

        # Run each job in the setting file in parallel
        $jobNamesFromSettings.Name | ForEach-Object {

            $currentJobParams = @{
                Name = $jobListPropertiesFromSettings.$($_).Name
                ScriptBlock = [Scriptblock]::Create($jobListPropertiesFromSettings.$($_).ScriptBlock)
                FilePath = $jobListPropertiesFromSettings.$($_).FilePath
                ArgumentList = $jobListPropertiesFromSettings.$($_).ArgumentList
                StreamingHost = $jobListPropertiesFromSettings.$($_).StreamingHost
                Authentication = $jobListPropertiesFromSettings.$($_).Authentication
                CCVault_Credential = $jobListPropertiesFromSettings.$($_).CCVault_Credential
                PSVersion = $jobListPropertiesFromSettings.$($_).PSVersion
                RunAs32 = $jobListPropertiesFromSettings.$($_).RunAs32
                WorkingDirectory = $jobListPropertiesFromSettings.$($_).WorkingDirectory
            }

            # Start-Job does not play nicely with null values when splatting, remove null values
            @($currentJobParams.Keys) | ForEach-Object {
                if ($null -eq  $currentJobParams[$_]) {
                    $currentJobParams.Remove($_)
                }
            }

            try {
                if ($jobListPropertiesFromSettings.$($_).ThreadJob -eq $true) {
                    Start-ThreadJob @currentJobParams -ThrottleLimit $sessionThreads -ErrorAction Stop | Out-Null
                } else {
                    Start-Job @currentJobParams -ErrorAction Stop | Out-Null
                }
            } catch {
                Write-Error "CCJob: $($jobNamesFromSettings.Name) threw an error, please check the job configuration and test before re-running it"
            }
        }
    } else {
        Write-Debug "Jobs are not enabled..."
    }
    return $true
}

function Out-CCMenu {
    if ($settingsFile.CCScriptSettings.Menu.Menu_OptIn -eq $true) {
        ## Initial retrieval & setup of settings ##
        # Check settings if we should use ASCII only when displaying menu options
        if ($settingsFile.CCScriptSettings.Menu.Menu_GridGUIEnable -eq $true) {
            $forceMenuGUI_Enable = $true
        } else {
            $forceMenuGUI_Enable = $false
        }

        # Set the starting directory to the location of the 'bin' directory
        $workingDirectory = Join-Path -Path $scriptLocation -ChildPath $settingsFile.CCScriptSettings.FolderPaths.bin
        $startingDirectory = $workingDirectory
        
        ## Clear screen and display logo from file or simple logo, then wait for input ##
        Clear-Host
        $firstLogo = Get-Content -Path "$($scriptLocation)\etc\logo"
        $logoLength = $firstLogo[0].Length 
        if (($host.UI.RawUI.WindowSize.Width -ge $logoLength)-and ($host.UI.RawUI.WindowSize.Height -ge $firstLogo.Length)) {
            $firstLogo | ForEach-Object {
                Write-Host "$($_)"
            }
        } else {
            Write-Host "*Command*Central*"
        }
        Write-Host -NoNewLine 'Press any key to continue...';
        $null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown');
        Write-Host ""

        $menuLoopControlBoolean = $true
        while ($menuLoopControlBoolean -eq $true) {

            Clear-Host

            # Nullify values so previous menus wont be brought into the next one
            $childItems_Folders = $null
            $childItems_PS1Files = $null
            $childItems_AllItems = $null
            $csvItems_AllItems = $null
            $menuEntriesRecords = $null

            # Set location
            Set-Location $workingDirectory

            ## Get storage contents and menu csv to build menu hashtable ##
            $childItems_Folders = Get-ChildItem -Directory
            $childItems_PS1Files = Get-ChildItem -Filter "*.ps1"
            
            # Combine all storage files into a single array
            $childItems_AllItems = @()

            foreach ($folderEntry in $childItems_Folders) {
                $childItems_AllItems += $folderEntry
            }

            foreach ($ps1FileEntry in $childItems_PS1Files) {
                $childItems_AllItems += $ps1FileEntry
            }

            # Attempt to find menu csv file
            try {
                $csvItems_AllItems = Import-Csv -Path ./MenuStructure.csv -ErrorAction Stop
                
                # Cast the position column to interger format from string
                $csvItems_AllItems | ForEach-Object { $_.Position = [int]$_.Position }

                $csvItems_AllItems = $csvItems_AllItems | Sort-Object -Property Position -Descending
            } catch {
                Write-Debug "Menu structure csv file was not found for the following directory: $($workingDirectory)"
            }

            $menuEntriesRecords = @()
            
            # Build the menu entries from the postive (first) positions in the csv
            foreach ($entry in $csvItems_AllItems) {
                if (($entry.Position) -gt 0) {
                    $pathTest = Test-Path -Path $($entry.FileName)
                    if ($pathTest -eq $true ) {
                        $menuEntriesRecords += [pscustomobject] @{
                            Position = $($entry.Position)
                            Option = $($entry.Option)
                            FileName = $($entry.FileName)
                            Description = $($entry.Description)
                        }
                    } else {
                        Write-Debug ""
                        Write-Error "Menu csv contains invalid entry:"
                        Write-Debug "Path: $($workingDirectory)"
                        Write-Debug "Entry: $($entry)"
                    }
                }
            }

            # Now add entries that arent in the csv from the disk contents
            foreach ($diskEntry in $childItems_AllItems) {
                $skipDiskAdd_Boolean = $false

                foreach ($csvEntry in $csvItems_AllItems) {
                    if ($csvEntry.FileName -clike $diskEntry.Name) {
                        $skipDiskAdd_Boolean = $true
                        break
                    }
                }

                if ($skipDiskAdd_Boolean -eq $false) {
                    $menuEntriesRecords += [pscustomobject] @{
                        Position = 0
                        Option = $($diskEntry.Name)
                        FileName = $($diskEntry.Name)
                        Description = "N/A"
                    }
                }
            }

            # Finally, add the entries with negative (last) positions
            foreach ($entry in $csvItems_AllItems) {
                if (($entry.Position) -lt 0) {
                    $pathTest = Test-Path -Path $($entry.FileName)
                    if ($pathTest -eq $true ) {
                        $menuEntriesRecords += [pscustomobject] @{
                            Position = $($entry.Position)
                            Option = $($entry.Option)
                            FileName = $($entry.FileName)
                            Description = $($entry.Description)
                        }
                    } else {
                        Write-Debug ""
                        Write-Error "Menu csv contains invalid entry:"
                        Write-Debug "Path: $($workingDirectory)"
                        Write-Debug "Entry: $($entry)"
                    }
                }
            }

            # Add in the quit option if the starting and working directories match
            if ($startingDirectory -eq $workingDirectory) {
                $menuEntriesRecords += [pscustomobject] @{
                    Position = $null
                    Option = "Quit"
                    FileName = $null
                    Description = $null
                }

            } else {
                $menuEntriesRecords += [pscustomobject] @{
                    Position = $null
                    Option = "Go Back"
                    FileName = $null
                    Description = $null
                }
            }

            if ($forceMenuGUI_Enable -eq $true) {
                try {
                    $ccMenuSelectedOption = $menuEntriesRecords | Select-Object Option, Description | Out-ConsoleGridView -Title "CommandCentral GUI" -OutputMode Single -ErrorAction Stop

                    if ($null -eq $ccMenuSelectedOption) {
                        $ccMenuSelectedOption = "Refresh"
                    } else {
                        $ccMenuSelectedOption = $menuEntriesRecords | Where-Object {
                            ($_.Option -eq $ccMenuSelectedOption.Option) -and ($_.Description -eq $ccMenuSelectedOption.Description)
                        }
                    }
                } catch {
                    Write-Debug "Current terminal does not support Microsoft.PowerShell.ConsoleGuiTools, or the module is not installed, or it ran into an error"
                    $forceMenuGUI_Enable = $false
                    $ccMenuSelectedOption = "Refresh"
                }
            } else {
                $loopNum = 1
                Write-Host "Key: #) Name, Description"
                Write-Host ""
                foreach ($menuOption in $menuEntriesRecords) {
                    if (($null -eq $($menuOption.Description)) -or ($($menuOption.Description) -like "")) {
                        Write-Host "$($loopNum)) $($menuOption.Option)"
                    } else {
                        Write-Host "$($loopNum)) $($menuOption.Option), $($menuOption.Description)"
                    }
                    $loopNum++
                }
                Write-Host ""
                $ccMenuSelectedOption = Read-Host -Prompt "Select an option"

                # Convert the number/letter back into a option that can be used (same as the console grid view output)
                if ($ccMenuSelectedOption -like "") {
                    $ccMenuSelectedOption = "Refresh"
                } elseif (($ccMenuSelectedOption -like "Quit") -or ($ccMenuSelectedOption -like "Q")) {
                    if ($startingDirectory -eq $workingDirectory) {
                        $ccMenuSelectedOption = $menuEntriesRecords | Where-Object { $_.Option -like "Quit" }   
                    } else {
                        $ccMenuSelectedOption = "Refresh"
                        Write-Host ""
                        Write-Host "Invalid Choice"
                        Start-Sleep 1.5
                    }
                } elseif (($ccMenuSelectedOption -like "Go Back") -or ($ccMenuSelectedOption -like "Back") -or ($ccMenuSelectedOption -like "B")) {
                    if ($startingDirectory -ne $workingDirectory) {
                        $ccMenuSelectedOption = $menuEntriesRecords | Where-Object { $_.Option -like "Go Back" }
                    } else {
                        $ccMenuSelectedOption = "Refresh"
                        Write-Host ""
                        Write-Host "Invalid Choice"
                        Start-Sleep .75
                    }
                } elseif ( ($([int]$ccMenuSelectedOption) -le $($menuEntriesRecords.Count)) -and ([int]$ccMenuSelectedOption -ge 1)) {
                    $ccMenuSelectedOption = $menuEntriesRecords[(($ccMenuSelectedOption) - 1)]
                } else {
                    $ccMenuSelectedOption = "Refresh"
                    Write-Host ""
                    Write-Host "Invalid Choice"
                    Start-Sleep .75
                }
            }

            # Special test for folders
            if ($null -ne $($ccMenuSelectedOption.FileName)) {
                $isFolder_Boolean = Test-Path -Path $($ccMenuSelectedOption.FileName) -PathType Container
            }

            # After menu selection, begin determining what to do (switch directories, execute, or simply do nothing)
            if ($ccMenuSelectedOption -like "Refresh") {
                # Do nothing
            } elseif ($ccMenuSelectedOption.Option -like "Quit") {
                Write-Host ""
                Write-Host "Quiting..."
                Start-Sleep .5
                $menuLoopControlBoolean = $false
            } elseif ($ccMenuSelectedOption.Option -like "Go Back") {
                $workingDirectory = $workingDirectory | Split-Path -Parent
            } elseif ($($isFolder_Boolean) -eq $true) {
                # Matched on folder
                $workingDirectory = Join-Path -Path $workingDirectory -ChildPath $($ccMenuSelectedOption.FileName)
            } elseif ($($ccMenuSelectedOption.FileName) -like "*.ps1") {
                # Matched on ps1 file
                . ".\$($ccMenuSelectedOption.FileName)"
            }
        }
    } else {
        Write-Debug "Menu is not enabled..."
    }
}