Skip to content

Instantly share code, notes, and snippets.

@1mm0rt41PC
Last active March 7, 2025 09:56
Show Gist options
  • Save 1mm0rt41PC/370491b6b312b8b296727cb9e01543c3 to your computer and use it in GitHub Desktop.
Save 1mm0rt41PC/370491b6b312b8b296727cb9e01543c3 to your computer and use it in GitHub Desktop.
#############################################################
# RDP Session Management with LAPS
#
# Author: Claude
# Date: March 6, 2025
# Version: 1.1
#
# Description:
# This script provides a GUI for managing RDP connections using LAPS passwords.
# It retrieves LAPS credentials from Active Directory, establishes RDP connections,
# and supports shadow mode for existing sessions.
#
# Features:
# - Retrieves LAPS passwords without additional modules
# - Manages credentials securely
# - Supports standard RDP and shadow sessions
# - Stays open until RDP session ends
# - Comprehensive logging with command output
# - Proper error handling
#############################################################
# Import required .NET assemblies
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.DirectoryServices
Add-Type -AssemblyName System.Drawing
#region Functions
Function Main-UI {
# Create the main form with appropriate properties
$global:form = New-Object System.Windows.Forms.Form
$global:form.Text = "RDP Session Management with LAPS"
$global:form.Size = New-Object System.Drawing.Size(500, 350)
$global:form.MinimumSize = New-Object System.Drawing.Size(500, 350)
$global:form.StartPosition = "CenterScreen" # Center the window on the screen
$global:form.FormBorderStyle = "Sizable" # Allow resizing
$global:form.MaximizeBox = $true # Allow maximizing the window
$global:form.AcceptButton = $null # Will be set after button is created
# Create a table layout panel for organizing controls
# This enables responsive resizing of the UI
$tableLayoutPanel = New-Object System.Windows.Forms.TableLayoutPanel
$tableLayoutPanel.Dock = [System.Windows.Forms.DockStyle]::Fill # Fill the entire form
$tableLayoutPanel.ColumnCount = 2 # Two columns (labels and inputs)
$tableLayoutPanel.RowCount = 7 # Seven rows for our controls
# Define column widths as percentages
$tableLayoutPanel.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::Percent, 30))) # Labels column (30%)
$tableLayoutPanel.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::Percent, 70))) # Inputs column (70%)
# Configure rows with appropriate sizing
# The layout is divided into 7 rows with different proportions
for ($i = 0; $i -lt 7; $i++) {
if ($i -eq 4) {
# Status message row (larger to accommodate log) - 25% of form height
# This is where the log TextArea will be displayed
$tableLayoutPanel.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Percent, 25)))
}
elseif ($i -eq 5) {
# ListBox row for session selection - 25% of form height
# This will display multiple sessions when found
$tableLayoutPanel.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Percent, 25)))
}
elseif ($i -eq 6) {
# Button row - 10% of form height
# This contains the action buttons
$tableLayoutPanel.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Percent, 10)))
}
else {
# Standard input rows - 8% of form height each
# These contain the form input fields
$tableLayoutPanel.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Percent, 8)))
}
}
# Add the table layout to the form
$global:form.Controls.Add($tableLayoutPanel)
#region Form Controls
# Target Computer Name Label and TextBox
$computerLabel = New-Object System.Windows.Forms.Label
$computerLabel.Dock = [System.Windows.Forms.DockStyle]::Fill
$computerLabel.TextAlign = [System.Drawing.ContentAlignment]::MiddleRight
$computerLabel.Text = "Target Computer Name:"
$tableLayoutPanel.Controls.Add($computerLabel, 0, 0)
$computerTextBox = New-Object System.Windows.Forms.TextBox
$computerTextBox.Dock = [System.Windows.Forms.DockStyle]::Fill
$computerTextBox.Anchor = [System.Windows.Forms.AnchorStyles]::Left -bor [System.Windows.Forms.AnchorStyles]::Right
# Add KeyDown event for Enter key
$computerTextBox.Add_KeyDown({
if ($_.KeyCode -eq [System.Windows.Forms.Keys]::Enter) {
$connectButton.PerformClick()
}
})
$tableLayoutPanel.Controls.Add($computerTextBox, 1, 0)
# AD Username Label and TextBox
$userLabel = New-Object System.Windows.Forms.Label
$userLabel.Dock = [System.Windows.Forms.DockStyle]::Fill
$userLabel.TextAlign = [System.Drawing.ContentAlignment]::MiddleRight
$userLabel.Text = "AD Username:"
$tableLayoutPanel.Controls.Add($userLabel, 0, 1)
$userTextBox = New-Object System.Windows.Forms.TextBox
$userTextBox.Dock = [System.Windows.Forms.DockStyle]::Fill
$userTextBox.Anchor = [System.Windows.Forms.AnchorStyles]::Left -bor [System.Windows.Forms.AnchorStyles]::Right
# Add KeyDown event for Enter key
$userTextBox.Add_KeyDown({
if ($_.KeyCode -eq [System.Windows.Forms.Keys]::Enter) {
$connectButton.PerformClick()
}
})
$tableLayoutPanel.Controls.Add($userTextBox, 1, 1)
# Password Label and TextBox
$passwordLabel = New-Object System.Windows.Forms.Label
$passwordLabel.Dock = [System.Windows.Forms.DockStyle]::Fill
$passwordLabel.TextAlign = [System.Drawing.ContentAlignment]::MiddleRight
$passwordLabel.Text = "Password:"
$tableLayoutPanel.Controls.Add($passwordLabel, 0, 2)
$passwordTextBox = New-Object System.Windows.Forms.MaskedTextBox
$passwordTextBox.Dock = [System.Windows.Forms.DockStyle]::Fill
$passwordTextBox.Anchor = [System.Windows.Forms.AnchorStyles]::Left -bor [System.Windows.Forms.AnchorStyles]::Right
$passwordTextBox.PasswordChar = '*'
# Add KeyDown event for Enter key
$passwordTextBox.Add_KeyDown({
if ($_.KeyCode -eq [System.Windows.Forms.Keys]::Enter) {
$connectButton.PerformClick()
}
})
$tableLayoutPanel.Controls.Add($passwordTextBox, 1, 2)
# Empty row for spacing
$placeholder = New-Object System.Windows.Forms.Label
$placeholder.Text = ""
$tableLayoutPanel.Controls.Add($placeholder, 0, 3)
# Status TextArea (for logging actions)
# This TextBox serves as a log display showing operation history
$global:statusTextBox = New-Object System.Windows.Forms.TextBox
$global:statusTextBox.Dock = [System.Windows.Forms.DockStyle]::Fill
$global:statusTextBox.Multiline = $true # Allow multiple lines
$global:statusTextBox.ReadOnly = $true # Prevent user editing
$global:statusTextBox.ScrollBars = [System.Windows.Forms.ScrollBars]::Vertical # Add scrollbar
$global:statusTextBox.BackColor = [System.Drawing.Color]::FromArgb(240, 240, 240) # Light gray background
$global:statusTextBox.Font = New-Object System.Drawing.Font("Consolas", 9) # Monospaced font for better log readability
# Initialize with first log message
$global:statusTextBox.Text = "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] [INFO] Ready. Enter the information above and click 'Connect'"
# Make the TextBox span both columns
$tableLayoutPanel.SetColumnSpan($global:statusTextBox, 2)
$tableLayoutPanel.Controls.Add($global:statusTextBox, 0, 4)
# Panel for Sessions ListBox
# This panel contains the ListBox and allows it to be shown/hidden as needed
$sessionPanel = New-Object System.Windows.Forms.Panel
$sessionPanel.Dock = [System.Windows.Forms.DockStyle]::Fill
$tableLayoutPanel.SetColumnSpan($sessionPanel, 2) # Make it span both columns
$tableLayoutPanel.Controls.Add($sessionPanel, 0, 5)
# Sessions ListBox
# This ListBox displays available sessions for selection when multiple are found
$sessionListBox = New-Object System.Windows.Forms.ListBox
$sessionListBox.Dock = [System.Windows.Forms.DockStyle]::Fill # Fill the panel
$sessionListBox.Visible = $false # Initially hidden
$sessionListBox.IntegralHeight = $false # Allow for partial items
$sessionListBox.Font = New-Object System.Drawing.Font("Segoe UI", 9)
$sessionListBox.BorderStyle = [System.Windows.Forms.BorderStyle]::FixedSingle
$sessionListBox.SelectionMode = [System.Windows.Forms.SelectionMode]::One
$sessionListBox.HorizontalScrollbar = $true # Enable horizontal scrolling for long entries
$sessionListBox.ItemHeight = 18 # Set consistent item height
$sessionPanel.Controls.Add($sessionListBox)
# Button Panel
# This panel organizes the buttons at the bottom of the form
$buttonPanel = New-Object System.Windows.Forms.FlowLayoutPanel
$buttonPanel.Dock = [System.Windows.Forms.DockStyle]::Fill
$buttonPanel.FlowDirection = [System.Windows.Forms.FlowDirection]::RightToLeft # Buttons align to right
$buttonPanel.WrapContents = $false # No wrapping
$buttonPanel.Padding = New-Object System.Windows.Forms.Padding(10) # Add padding
$tableLayoutPanel.SetColumnSpan($buttonPanel, 2) # Span both columns
$tableLayoutPanel.Controls.Add($buttonPanel, 0, 6)
# Connect Button
$connectButton = New-Object System.Windows.Forms.Button
$connectButton.AutoSize = $true # Size to fit content
$connectButton.MinimumSize = New-Object System.Drawing.Size(120, 30) # Minimum size
$connectButton.Text = "Connect"
$connectButton.BackColor = [System.Drawing.Color]::FromArgb(220, 240, 220) # Light green background
$connectButton.FlatStyle = [System.Windows.Forms.FlatStyle]::System # System-style button
$connectButton.Cursor = [System.Windows.Forms.Cursors]::Hand # Hand cursor on hover
$buttonPanel.Controls.Add($connectButton)
# Cancel Button
$cancelButton = New-Object System.Windows.Forms.Button
$cancelButton.AutoSize = $true # Size to fit content
$cancelButton.MinimumSize = New-Object System.Drawing.Size(120, 30) # Minimum size
$cancelButton.Text = "Cancel"
$cancelButton.BackColor = [System.Drawing.Color]::FromArgb(240, 240, 240) # Light gray background
$cancelButton.FlatStyle = [System.Windows.Forms.FlatStyle]::System # System-style button
$cancelButton.Cursor = [System.Windows.Forms.Cursors]::Hand # Hand cursor on hover
$cancelButton.Add_Click({
# Close the form when cancel is clicked
Add-LogEntry "Operation cancelled by user." -Type "INFO"
$global:form.Close()
})
$buttonPanel.Controls.Add($cancelButton)
#endregion Form Controls
#endregion UI Setup
#region Event Handlers
# Connect Button Click Event Handler
$connectButton.Add_Click({
# Validate computer name
$computerName = $computerTextBox.Text.Trim()
if ([string]::IsNullOrEmpty($computerName)) {
[System.Windows.Forms.MessageBox]::Show("Please enter a valid computer name.", "Error", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error)
return
}
# Validate credentials
$userName = $userTextBox.Text.Trim()
$password = $passwordTextBox.Text
if ([string]::IsNullOrEmpty($userName) -or [string]::IsNullOrEmpty($password)) {
[System.Windows.Forms.MessageBox]::Show("Please enter valid credentials.", "Error", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error)
return
}
# Create secure credential object
$securePassword = ConvertTo-SecureString $password -AsPlainText -Force
$credential = New-Object System.Management.Automation.PSCredential($userName, $securePassword)
# Update status and retrieve LAPS password
Add-LogEntry "Retrieving LAPS password for $computerName..."
$lapsPassword = Get-LAPSPassword -ComputerName $computerName -Credential $credential
# Check if password was retrieved
if ([string]::IsNullOrEmpty($lapsPassword)) {
[System.Windows.Forms.MessageBox]::Show("Unable to retrieve LAPS password for this computer.", "Error", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error)
Add-LogEntry "ERROR: Unable to retrieve LAPS password for $computerName"
return
}
# Update status and set up connection credentials
Add-LogEntry "LAPS password retrieved successfully. Setting up connection credentials..." -Type "INFO"
# Store credentials securely for the RDP connection
# Note: Password is enclosed in quotes to handle special characters
$cmdKeyCommand = "cmdkey /add:$computerName /user:$computerName\administrator /pass:`"$lapsPassword`""
try {
$cmdKeyResult = Invoke-Expression $cmdKeyCommand
Add-CommandResult -Command "cmdkey /add:$computerName /user:.\administrator /pass:********" -Result "$cmdKeyResult"
Add-LogEntry "Stored administrator credentials for $computerName" -Type "SUCCESS"
}
catch {
Add-CommandResult -Command "cmdkey /add:$computerName /user:.\administrator /pass:********" -Result "$_"
Add-LogEntry "Failed to store credentials: $_" -Type "ERROR"
}
# Update status and retrieve Winlogon sessions
Add-LogEntry "Retrieving Winlogon sessions from $computerName..." -Type "INFO"
$sessions = Get-WinlogonSessions -ComputerName $computerName
# Handle different session scenarios
if ($sessions -eq $null -or $sessions.Count -eq 0) {
# No sessions - standard RDP connection
Add-LogEntry "No sessions found. Starting standard RDP connection..."
Start-MstscAndWait -Arguments "/v:$computerName" -ComputerName $computerName
}
elseif ($sessions.Count -eq 1) {
# Single session - direct shadow
$sessionId = $sessions[0].SessionId
Add-LogEntry "One session found (ID: $sessionId). Starting shadow connection..."
Start-MstscAndWait -Arguments "/shadow:$sessionId /control /v:$computerName" -ComputerName $computerName
}
else {
# Multiple sessions - prompt for selection
Add-LogEntry "Multiple sessions found ($($sessions.Count)). Please select a session to target."
$sessionListBox.Visible = $true
$global:form.Refresh()
# Populate session list
$sessionListBox.Items.Clear()
foreach ($session in $sessions) {
$sessionListBox.Items.Add("Session ID: $($session.SessionId) - Process ID: $($session.ProcessId)")
}
# Create shadow session button
$connectSessionButton = New-Object System.Windows.Forms.Button
$connectSessionButton.AutoSize = $true
$connectSessionButton.MinimumSize = New-Object System.Drawing.Size(120, 30)
$connectSessionButton.Text = "Shadow Session"
$connectSessionButton.Add_Click({
# Validate selection
if ($sessionListBox.SelectedIndex -eq -1) {
[System.Windows.Forms.MessageBox]::Show("Please select a session.", "Error", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error)
return
}
# Get selected session ID
$selectedSession = $sessions[$sessionListBox.SelectedIndex]
$sessionId = $selectedSession.SessionId
# Log the selection
Add-LogEntry "Selected session ID: $sessionId - Starting shadow connection..."
# Start shadow session
Start-MstscAndWait -Arguments "/shadow:$sessionId /control /v:$computerName" -ComputerName $computerName
# Note: The nettoyage des identifiants is handled by the Start-MstscAndWait function
})
$buttonPanel.Controls.Add($connectSessionButton)
return
}
# Note: The credentials cleanup is handled by the Start-MstscAndWait function
# The form closing is also handled by that function
})
#endregion Event Handlers
# Set the default button (triggered by Enter key in the form)
$global:form.AcceptButton = $connectButton
# Display the form
$global:form.Add_Shown({$global:form.Activate()})
[void] $global:form.ShowDialog()
}
#region Core Functions
<#
.SYNOPSIS
Retrieves the LAPS password for a specified computer from Active Directory.
.DESCRIPTION
Uses Directory Services to query AD and retrieve the LAPS password
for a specified computer using provided credentials.
The function connects to Active Directory, searches for the computer object,
and extracts the ms-Mcs-AdmPwd attribute which contains the LAPS password.
.PARAMETER ComputerName
The name of the computer to retrieve the LAPS password for.
.PARAMETER Credential
The credentials to use when connecting to Active Directory.
.RETURNS
Returns the LAPS password as a string if successful, or $null if not found or on error.
.EXAMPLE
$cred = Get-Credential
$password = Get-LAPSPassword -ComputerName "PC001" -Credential $cred
#>
function Get-LAPSPassword {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[string]$ComputerName,
[Parameter(Mandatory = $true)]
[System.Management.Automation.PSCredential]$Credential
)
try {
# Connect to the domain controller
# First get the domain information from RootDSE
Write-Verbose "Connecting to domain controller..."
$domain = New-Object System.DirectoryServices.DirectoryEntry("LDAP://RootDSE", $Credential.UserName, $Credential.GetNetworkCredential().Password)
$domainDN = $domain.DefaultNamingContext
Write-Verbose "Connected to domain: $domainDN"
# Prepare the search by creating a searcher object
$root = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$domainDN", $Credential.UserName, $Credential.GetNetworkCredential().Password)
$searcher = New-Object System.DirectoryServices.DirectorySearcher($root)
# Set filter to find the specific computer
$searcher.Filter = "(&(objectClass=computer)(name=$ComputerName))"
Write-Verbose "Searching for computer: $ComputerName"
# Request the ms-Mcs-AdmPwd attribute which contains the LAPS password
# This is the actual AD attribute that stores the LAPS password
$searcher.PropertiesToLoad.Add("ms-Mcs-AdmPwd") | Out-Null
# Perform the search
$computer = $searcher.FindOne()
# Check if the computer was found
if ($null -eq $computer) {
Write-Verbose "Computer $ComputerName not found in AD."
return $null
}
# Extract the password from the result
$password = $computer.Properties["ms-Mcs-AdmPwd"]
# Check if the password attribute exists and has a value
if ($null -eq $password -or $password.Count -eq 0) {
Write-Verbose "No LAPS password found for $ComputerName."
return $null
}
# Return the password value
return $password[0]
}
catch {
# Display error message
[System.Windows.Forms.MessageBox]::Show("Error retrieving LAPS password: $_", "Error", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error)
Write-Error "Error retrieving LAPS password: $_"
# Log the error
if ($null -ne $global:statusTextBox) {
Add-LogEntry "ERROR: Failed to retrieve LAPS password - $($_)"
}
return $null
}
}
<#
.SYNOPSIS
Retrieves active Winlogon sessions from a remote computer.
.DESCRIPTION
Uses WMIC to query a remote computer for Winlogon processes and retrieves
their associated session IDs. This is used to determine which sessions can be
shadowed via RDP.
Winlogon.exe processes are associated with user login sessions on Windows systems.
By identifying these processes, we can find active user sessions for shadowing.
.PARAMETER ComputerName
The name of the remote computer to query.
.RETURNS
An array of custom objects containing ProcessId and SessionId properties.
.EXAMPLE
$sessions = Get-WinlogonSessions -ComputerName "PC001"
foreach ($session in $sessions) {
Write-Host "Session ID: $($session.SessionId)"
}
#>
function Get-WinlogonSessions {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[string]$ComputerName
)
# Execute WMIC command to retrieve Winlogon processes
# Using WMIC to get remote process information (this will be used to identify active sessions)
Add-LogEntry "Executing WMIC query on $ComputerName for winlogon.exe processes..." -Type "INFO"
$wmicOutput = ''
try{
$wmicCommand = "wmic process where name=`"winlogon.exe`" get ProcessId,SessionId /node:$ComputerName"
$wmicOutput = Get-WMIObject Win32_Process -Filter 'Name="explorer.exe"' -ComputerName $ComputerName
Add-CommandResult -Command "$wmicCommand" -Result "$wmicOutput"
}catch{
Add-LogEntry "Failed to Get-WMIObject Win32_Process - $($_)" -Type "ERROR"
return $null
}
try{
$wmicOutput = $wmicOutput | Select SessionId,@{n="User";e={$u=$_.GetOwner(); "$($u.Domain)\$($u.User)"}}
# Log the command and its output
Add-CommandResult -Command "$wmicCommand" -Result "$($wmicOutput | ft | out-string)"
return $wmicOutput
}catch{
Add-LogEntry "Failed to Get-WMIObject Win32_Process WITH the Select - $($_)" -Type "ERROR"
return $null
}
}
<#
.SYNOPSIS
Launches mstsc with specified arguments and waits for it to exit.
.DESCRIPTION
Starts the RDP client (mstsc.exe) with the provided arguments, monitors the process
until it exits, then cleans up the stored credentials.
This function uses a background job to monitor the process without blocking the UI,
and automatically cleans up stored credentials when the RDP session ends.
.PARAMETER Arguments
The command-line arguments to pass to mstsc.exe.
.PARAMETER ComputerName
The name of the computer to connect to. Used for cleaning up credentials.
.EXAMPLE
Start-MstscAndWait -Arguments "/v:SERVER01" -ComputerName "SERVER01"
Start-MstscAndWait -Arguments "/shadow:3 /control /v:SERVER01" -ComputerName "SERVER01"
#>
function Start-MstscAndWait {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$Arguments,
[Parameter(Mandatory = $true)]
[string]$ComputerName
)
try {
# Update status
Add-LogEntry "Launching mstsc with arguments: $Arguments"
# Start mstsc process and get the process object
Write-Verbose "Starting mstsc.exe with arguments: $Arguments"
Add-LogEntry "Starting mstsc.exe with arguments: $Arguments" -Type "CMD"
$process = Start-Process "mstsc.exe" -ArgumentList $Arguments -PassThru
# Update status
Add-LogEntry "MSTSC process started (PID: $($process.Id)). Waiting for session to end..." -Type "INFO"
# Create a background job to monitor the process
# This allows the UI to remain responsive while waiting for mstsc to exit
$job = Start-Job -ScriptBlock {
param($processId, $computerName)
# Wait for the process to exit
try {
Write-Output "Monitoring process ID: $processId"
# Wait for the mstsc process to exit before continuing
Wait-Process -Id $processId -ErrorAction SilentlyContinue
Write-Output "Process has exited"
}
catch {
Write-Output "Error waiting for process: $_"
}
# Clean up credentials
try {
Write-Output "Removing stored credentials for $computerName"
# Remove the credentials that were stored for this session
Invoke-Expression "cmdkey /delete:$computerName"
Write-Output "Credentials removed"
}
catch {
Write-Output "Error removing credentials: $_"
}
return "Completed"
} -ArgumentList $process.Id, $ComputerName
# Set up a timer to periodically check if the job is complete
# This is an event-driven approach to prevent UI blocking
$timer = New-Object System.Windows.Forms.Timer
$timer.Interval = 1000 # Check every second
$timer.Add_Tick({
# Check job status
if ($job.State -eq "Completed") {
# Stop and clean up the timer
$timer.Stop()
$timer.Dispose()
# Get job output and clean up
Write-Verbose "Background job completed"
Receive-Job -Job $job | Write-Verbose
Remove-Job -Job $job -Force
# Update UI and close form
Add-LogEntry "Session ended. Cleaning up credentials..." -Type "INFO"
# Get and log the cmdkey delete command result
try {
$cmdkeyDeleteResult = Invoke-Expression "cmdkey /delete:$ComputerName"
Add-CommandResult -Command "cmdkey /delete:$ComputerName" -Result "$cmdkeyDeleteResult"
}
catch {
Add-LogEntry "Error while deleting credentials: $_" -Type "ERROR"
}
Add-LogEntry "All operations completed successfully." -Type "SUCCESS"
$global:form.Close()
}
})
# Start the timer
$timer.Start()
}
catch {
# Display error message
[System.Windows.Forms.MessageBox]::Show("Error launching mstsc: $_", "Error", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error)
Write-Error "Error launching mstsc: $_"
# Log the error
Add-LogEntry "ERROR: Failed to launch mstsc - $($_)"
# Clean up credentials even if there's an error
try {
$cmdkeyDeleteResult = Invoke-Expression "cmdkey /delete:$computerName"
Add-CommandResult -Command "cmdkey /delete:$computerName" -Result "$cmdkeyDeleteResult"
Add-LogEntry "Cleaned up credentials for $computerName" -Type "SUCCESS"
}
catch {
Write-Error "Error cleaning up credentials: $_"
Add-LogEntry "ERROR: Failed to clean up credentials - $($_)" -Type "ERROR"
}
}
}
#endregion Core Functions
#endregion Functions
# Function to add command result to log
# This function specifically handles command execution results with proper formatting
function Add-CommandResult {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[string]$Command,
[string]$Result
)
# Log the command that was executed
Add-LogEntry "Command executed: $Command" -Type "CMD"
# Log the command output, with each line indented for readability
# Split the result into lines and process each one
$resultLines = $Result -split "`n"
# If there's no output, report that fact
if ($resultLines.Count -eq 0 -or ([string]::IsNullOrWhiteSpace($Result))) {
Add-LogEntry " (No output returned)" -Type "OUTPUT"
return
}
# Process each line of the output
foreach ($line in $resultLines) {
if (![string]::IsNullOrWhiteSpace($line)) {
# Indent output lines with two spaces for readability
Add-LogEntry " $($line.Trim())" -Type "OUTPUT"
}
}
}
# Function to add log entry to status TextBox
# This function provides consistent formatting for all log entries
function Add-LogEntry {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[string]$Message,
[Parameter(Mandatory = $false)]
[ValidateSet("INFO", "ERROR", "SUCCESS", "WARNING", "CMD", "OUTPUT")]
[string]$Type = "INFO"
)
# Format the log entry with ISO-format timestamp for consistency
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
# Format based on message type (provides visual differentiation)
$logEntry = "[$timestamp] [$Type] $Message"
if( $global:statusTextBox -ne $null ){
# Append the new log entry to the TextBox with a newline
$global:statusTextBox.AppendText("`r`n$logEntry")
# Ensure the latest entry is visible by scrolling to it
$global:statusTextBox.SelectionStart = $global:statusTextBox.Text.Length
$global:statusTextBox.ScrollToCaret() | Out-Null
$global:form.Refresh() | Out-Null
}else{
# Also write to console/verbose stream for debugging and transcript logging
Write-Host $logEntry
}
}
Main-UI
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment