Windows Services Analyzer Tool

<#
.SYNOPSIS
Windows Services Analyzer Tool

.DESCRIPTION
This script analyzes and audits Windows services on local or remote systems,
providing insights into service configurations, startup types, and potential issues.

.NOTES
File Name      : ServicesAnalyzer.ps1
Author         : [Your Name]
Prerequisite   : PowerShell V5.1 or later, administrator rights
Version        : 1.0
Date           : [Current Date]

.EXAMPLE
.\ServicesAnalyzer.ps1
#>

# Global variables
$global:reportPath = "$env:USERPROFILE\Desktop\Services_Analysis_Report_$(Get-Date -Format 'yyyyMMdd_HHmmss').html"
$global:targetComputer = $env:COMPUTERNAME

<#
.SYNOPSIS
Displays the main menu of the tool.
#>
function Show-Menu {
    Clear-Host
    Write-Host "=== Windows Services Analyzer Tool ===" -ForegroundColor Cyan
    Write-Host "Current Target: $global:targetComputer"
    Write-Host "1. Set Target Computer"
    Write-Host "2. Analyze All Services"
    Write-Host "3. Check Automatic Services Not Running"
    Write-Host "4. Identify Manual Services Running"
    Write-Host "5. Analyze Service Dependencies"
    Write-Host "6. Check Services with Non-Standard Accounts"
    Write-Host "7. Identify Services with No Description"
    Write-Host "8. Generate Comprehensive HTML Report"
    Write-Host "9. Exit"
}

<#
.SYNOPSIS
Sets the target computer for analysis.
#>
function Set-TargetComputer {
    $computer = Read-Host "Enter the name of the target computer (or press Enter for local machine)"
    if ([string]::IsNullOrWhiteSpace($computer)) {
        $global:targetComputer = $env:COMPUTERNAME
    } else {
        $global:targetComputer = $computer
    }
    Write-Host "Target computer set to: $global:targetComputer" -ForegroundColor Green
}

<#
.SYNOPSIS
Analyzes all services on the target computer.

.OUTPUTS
Array of PSObjects containing service details.
#>
function Analyze-AllServices {
    Write-Host "`nAnalyzing All Services..." -ForegroundColor Yellow
    try {
        $services = Get-WmiObject -Class Win32_Service -ComputerName $global:targetComputer
        $results = @()
        foreach ($service in $services) {
            $results += [PSCustomObject]@{
                Name = $service.Name
                DisplayName = $service.DisplayName
                State = $service.State
                StartMode = $service.StartMode
                StartName = $service.StartName
            }
        }
        $results | Format-Table -AutoSize
        return $results
    }
    catch {
        Write-Host "Error analyzing services: $_" -ForegroundColor Red
        return $null
    }
}

<#
.SYNOPSIS
Checks for automatic services that are not running.

.OUTPUTS
Array of PSObjects containing details of automatic services not running.
#>
function Check-AutomaticServicesNotRunning {
    Write-Host "`nChecking Automatic Services Not Running..." -ForegroundColor Yellow
    try {
        $services = Get-WmiObject -Class Win32_Service -ComputerName $global:targetComputer |
                    Where-Object { $_.StartMode -eq "Auto" -and $_.State -ne "Running" }
        $results = @()
        foreach ($service in $services) {
            $results += [PSCustomObject]@{
                Name = $service.Name
                DisplayName = $service.DisplayName
                State = $service.State
                StartMode = $service.StartMode
            }
        }
        $results | Format-Table -AutoSize
        return $results
    }
    catch {
        Write-Host "Error checking automatic services: $_" -ForegroundColor Red
        return $null
    }
}

<#
.SYNOPSIS
Identifies manual services that are running.

.OUTPUTS
Array of PSObjects containing details of manual services running.
#>
function Identify-ManualServicesRunning {
    Write-Host "`nIdentifying Manual Services Running..." -ForegroundColor Yellow
    try {
        $services = Get-WmiObject -Class Win32_Service -ComputerName $global:targetComputer |
                    Where-Object { $_.StartMode -eq "Manual" -and $_.State -eq "Running" }
        $results = @()
        foreach ($service in $services) {
            $results += [PSCustomObject]@{
                Name = $service.Name
                DisplayName = $service.DisplayName
                State = $service.State
                StartMode = $service.StartMode
            }
        }
        $results | Format-Table -AutoSize
        return $results
    }
    catch {
        Write-Host "Error identifying manual services: $_" -ForegroundColor Red
        return $null
    }
}

<#
.SYNOPSIS
Analyzes service dependencies.

.OUTPUTS
Array of PSObjects containing service dependency details.
#>
function Analyze-ServiceDependencies {
    Write-Host "`nAnalyzing Service Dependencies..." -ForegroundColor Yellow
    try {
        $services = Get-WmiObject -Class Win32_Service -ComputerName $global:targetComputer
        $results = @()
        foreach ($service in $services) {
            if ($service.DependentServices) {
                $results += [PSCustomObject]@{
                    Name = $service.Name
                    DisplayName = $service.DisplayName
                    DependentServices = $service.DependentServices -join ", "
                }
            }
        }
        $results | Format-Table -AutoSize
        return $results
    }
    catch {
        Write-Host "Error analyzing service dependencies: $_" -ForegroundColor Red
        return $null
    }
}

<#
.SYNOPSIS
Checks for services with non-standard accounts.

.OUTPUTS
Array of PSObjects containing details of services with non-standard accounts.
#>
function Check-ServicesWithNonStandardAccounts {
    Write-Host "`nChecking Services with Non-Standard Accounts..." -ForegroundColor Yellow
    try {
        $standardAccounts = @("LocalSystem", "NT AUTHORITY\LocalService", "NT AUTHORITY\NetworkService")
        $services = Get-WmiObject -Class Win32_Service -ComputerName $global:targetComputer |
                    Where-Object { $standardAccounts -notcontains $_.StartName }
        $results = @()
        foreach ($service in $services) {
            $results += [PSCustomObject]@{
                Name = $service.Name
                DisplayName = $service.DisplayName
                StartName = $service.StartName
                State = $service.State
            }
        }
        $results | Format-Table -AutoSize
        return $results
    }
    catch {
        Write-Host "Error checking services with non-standard accounts: $_" -ForegroundColor Red
        return $null
    }
}

<#
.SYNOPSIS
Identifies services with no description.

.OUTPUTS
Array of PSObjects containing details of services with no description.
#>
function Identify-ServicesWithNoDescription {
    Write-Host "`nIdentifying Services with No Description..." -ForegroundColor Yellow
    try {
        $services = Get-WmiObject -Class Win32_Service -ComputerName $global:targetComputer |
                    Where-Object { [string]::IsNullOrWhiteSpace($_.Description) }
        $results = @()
        foreach ($service in $services) {
            $results += [PSCustomObject]@{
                Name = $service.Name
                DisplayName = $service.DisplayName
                State = $service.State
                StartMode = $service.StartMode
            }
        }
        $results | Format-Table -AutoSize
        return $results
    }
    catch {
        Write-Host "Error identifying services with no description: $_" -ForegroundColor Red
        return $null
    }
}

<#
.SYNOPSIS
Generates a comprehensive HTML report of all analyses.

.PARAMETER AllResults
Hashtable containing all analysis results.

.OUTPUTS
Saves an HTML report to the desktop.
#>
function Generate-HTMLReport {
    param([hashtable]$AllResults)

    Write-Host "`nGenerating Comprehensive HTML Report..." -ForegroundColor Yellow
    $reportContent = @"
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Windows Services Analysis Report</title>
    <style>
        body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 1200px; margin: 0 auto; padding: 20px; }
        h1, h2, h3 { color: #0078D4; }
        table { border-collapse: collapse; width: 100%; margin-bottom: 20px; }
        th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
        th { background-color: #f2f2f2; }
    </style>
</head>
<body>
    <h1>Windows Services Analysis Report</h1>
    <p>Generated on: $(Get-Date)</p>
    <p>Target Computer: $global:targetComputer</p>

    <h2>All Services</h2>
    $($AllResults.AllServices | ConvertTo-Html -Fragment)

    <h2>Automatic Services Not Running</h2>
    $($AllResults.AutomaticNotRunning | ConvertTo-Html -Fragment)

    <h2>Manual Services Running</h2>
    $($AllResults.ManualRunning | ConvertTo-Html -Fragment)

    <h2>Service Dependencies</h2>
    $($AllResults.ServiceDependencies | ConvertTo-Html -Fragment)

    <h2>Services with Non-Standard Accounts</h2>
    $($AllResults.NonStandardAccounts | ConvertTo-Html -Fragment)

    <h2>Services with No Description</h2>
    $($AllResults.NoDescription | ConvertTo-Html -Fragment)
</body>
</html>
"@

    $reportContent | Out-File -FilePath $global:reportPath
    Write-Host "Report generated and saved to: $global:reportPath" -ForegroundColor Green
}

# Main program loop
$allResults = @{}

do {
    Show-Menu
    $choice = Read-Host "`nEnter your choice (1-9)"

    switch ($choice) {
        "1" { Set-TargetComputer }
        "2" { $allResults.AllServices = Analyze-AllServices }
        "3" { $allResults.AutomaticNotRunning = Check-AutomaticServicesNotRunning }
        "4" { $allResults.ManualRunning = Identify-ManualServicesRunning }
        "5" { $allResults.ServiceDependencies = Analyze-ServiceDependencies }
        "6" { $allResults.NonStandardAccounts = Check-ServicesWithNonStandardAccounts }
        "7" { $allResults.NoDescription = Identify-ServicesWithNoDescription }
        "8" { Generate-HTMLReport -AllResults $allResults }
        "9" { Write-Host "Exiting program..." -ForegroundColor Yellow; break }
        default { Write-Host "Invalid choice. Please try again." -ForegroundColor Red }
    }

    if ($choice -ne "9") {
        Read-Host "`nPress Enter to continue..."
    }
} while ($choice -ne "9")

This Windows Services Analyzer Tool includes:

  1. A menu-driven interface for easy navigation.
  2. Functions to analyze various aspects of Windows services:
    • Analysis of all services
    • Check for automatic services not running
    • Identification of manual services running
    • Analysis of service dependencies
    • Check for services with non-standard accounts
    • Identification of services with no description
  3. Ability to set a target computer for remote analysis.
  4. Comprehensive error handling for each analysis function.
  5. A function to generate an HTML report of all collected data.

Key features:

  • Detailed analysis of all services on the target system
  • Identification of potential issues like automatic services not running
  • Detection of manual services that are running (which might be unnecessary)
  • Analysis of service dependencies to understand the impact of service failures
  • Identification of services using non-standard accounts (potential security concern)
  • Detection of services lacking descriptions (which might indicate unauthorized or suspicious services)
  • Comprehensive HTML report generation

This tool is particularly useful for:

  • System administrators managing Windows services
  • Security professionals auditing service configurations
  • IT professionals troubleshooting service-related issues
  • Compliance officers ensuring proper service configurations

To use this script effectively:

  1. Run PowerShell as an administrator
  2. Ensure you have the necessary permissions to query services on the target computer
  3. Be cautious when analyzing services on production systems

This script provides a comprehensive overview of Windows services, making it easier to audit and maintain proper service configurations, identify potential security issues, and ensure the correct setup of services across Windows systems.

Folder Permissions in Windows

In the Windows operating system, folder permissions play a crucial role in controlling access and managing the security of your files and directories. These permissions determine who can perform specific actions, such as reading, writing, or modifying the contents of a folder. Understanding and properly configuring folder permissions is essential for maintaining the integrity and confidentiality of your data.

Understanding Folder Permissions: Folder permissions in Windows are based on the NTFS (New Technology File System) file system. NTFS provides a robust set of permissions that can be applied to folders, files, and other objects within the file system. The main types of permissions include:

  1. Read: Allows users to view the contents of a folder, but not make any changes.
  2. Write: Grants users the ability to create, modify, or delete files and subfolders within the folder.
  3. Execute: Permits users to run executable files or scripts within the folder.
  4. Modify: Combines the Read and Write permissions, allowing users to view, create, modify, and delete files and subfolders.
  5. Full Control: Grants users the highest level of access, enabling them to perform any action on the folder, including changing permissions and ownership.

Configuring Folder Permissions: To configure folder permissions in Windows, follow these steps:

  1. Right-click on the folder you want to manage and select “Properties.”
  2. In the folder’s Properties window, navigate to the “Security” tab.
  3. In the “Security” tab, you will see a list of user or group accounts with their associated permissions.
  4. To modify the permissions, select the user or group you want to manage and click the “Edit” button.
  5. In the “Permissions” window, you can grant or revoke specific permissions for the selected user or group.

Inheritance and Propagation: Folder permissions can be inherited from parent folders or propagated to subfolders and files. By default, new folders and files inherit the permissions from their parent folder. This inheritance can be modified or disabled as needed.

  1. Inheritance: When a new folder or file is created, it automatically inherits the permissions from its parent folder. This ensures consistent security across the file system.
  2. Propagation: Permissions can be propagated from a parent folder to its subfolders and files. This allows you to apply the same set of permissions to an entire directory structure.

Best Practices for Folder Permissions: To effectively manage folder permissions, consider the following best practices:

  1. Principle of Least Privilege: Grant users the minimum permissions required to perform their tasks, reducing the risk of unauthorized access or data breaches.
  2. Periodic Review: Regularly review and audit folder permissions to ensure they align with your organization’s security policies and user requirements.
  3. Separation of Duties: Assign different permissions to different user groups or roles to prevent a single user from having excessive control over sensitive data.
  4. Backup and Restore: Implement a robust backup and recovery strategy to ensure that you can quickly restore folder permissions in the event of a system failure or security incident.

By understanding and properly configuring folder permissions in Windows, you can enhance the security and control of your file system, protecting your valuable data and ensuring that users have the appropriate level of access to perform their tasks effectively.

RDS Login and Logout Logging Script

<#
.SYNOPSIS
RDS Login and Logout Logging Script

.DESCRIPTION
This script automatically logs user login and logout events for Remote Desktop Services.
It runs continuously as a background job, monitoring the Windows Event Log for relevant events.

.NOTES
File Name      : RDSLoginLogoutLogger.ps1
Author         : [Your Name]
Prerequisite   : PowerShell V3 or later, admin rights on the RDS server
Version        : 1.0
Date           : [Current Date]

.EXAMPLE
Start-Job -FilePath .\RDSLoginLogoutLogger.ps1
#>

# Configuration
$logFilePath = "C:\Logs\RDSLoginLogout.log"
$lastRunFile = "C:\Logs\RDSLoginLogoutLastRun.txt"

# Ensure log directory exists
$logDir = Split-Path $logFilePath -Parent
if (-not (Test-Path $logDir)) {
    New-Item -ItemType Directory -Path $logDir | Out-Null
}

# Function to write log entries
function Write-Log {
    param (
        [string]$Message
    )
    
    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    $logEntry = "$timestamp - $Message"
    Add-Content -Path $logFilePath -Value $logEntry
}

# Function to get the last run time
function Get-LastRunTime {
    if (Test-Path $lastRunFile) {
        return Get-Content $lastRunFile
    }
    return (Get-Date).AddDays(-1).ToString("o")  # Default to 1 day ago if no last run time
}

# Function to save the last run time
function Save-LastRunTime {
    param (
        [DateTime]$LastRunTime
    )
    $LastRunTime.ToString("o") | Set-Content $lastRunFile
}

# Main logging loop
try {
    Write-Log "RDS Login/Logout logging started."

    while ($true) {
        $lastRunTime = Get-LastRunTime
        $currentTime = Get-Date

        # Query for login events
        $loginEvents = Get-WinEvent -FilterHashtable @{
            LogName = 'Microsoft-Windows-TerminalServices-LocalSessionManager/Operational'
            ID = 21  # Event ID for session logon
            StartTime = $lastRunTime
        } -ErrorAction SilentlyContinue

        # Query for logout events
        $logoutEvents = Get-WinEvent -FilterHashtable @{
            LogName = 'Microsoft-Windows-TerminalServices-LocalSessionManager/Operational'
            ID = 23  # Event ID for session logoff
            StartTime = $lastRunTime
        } -ErrorAction SilentlyContinue

        # Process login events
        foreach ($event in $loginEvents) {
            $username = $event.Properties[0].Value
            $sessionId = $event.Properties[1].Value
            Write-Log "User logged in: $username (Session ID: $sessionId)"
        }

        # Process logout events
        foreach ($event in $logoutEvents) {
            $username = $event.Properties[0].Value
            $sessionId = $event.Properties[1].Value
            Write-Log "User logged out: $username (Session ID: $sessionId)"
        }

        # Save the current time as the last run time
        Save-LastRunTime $currentTime

        # Wait for a minute before the next check
        Start-Sleep -Seconds 60
    }
}
catch {
    Write-Log "An error occurred: $_"
}
finally {
    Write-Log "RDS Login/Logout logging stopped."
}

To use this script:

  1. Save the script as RDSLoginLogoutLogger.ps1 in a suitable location on your RDS server.
  2. Modify the $logFilePath and $lastRunFile variables at the beginning of the script if you want to change the default log locations.
  3. To run the script as a background job, open PowerShell as an administrator and use the following command:
    Start-Job -FilePath C:\Path\To\RDSLoginLogoutLogger.ps1
    Replace C:\Path\To\ with the actual path where you saved the script.
  4. To check the status of the job: Get-Job
  5. To stop the job when needed: Stop-Job -Id <JobId>
  6. Replace <JobId> with the ID of the job from the Get-Job command.

Key features of this script:

  1. Continuous Monitoring: Runs as a background job, continuously checking for new login and logout events.
  2. Efficient Event Querying: Uses the last run time to query only for new events since the last check.
  3. Separate Log File: Logs events to a dedicated file for easy review and analysis.
  4. Error Handling: Includes basic error handling to log any issues that occur during execution.
  5. Low Resource Usage: Checks for new events every minute, balancing timeliness with system resource usage.

Notes:

  • This script needs to be run with administrator privileges on the RDS server.
  • The script creates a log file and a last run time file. Ensure the specified paths are accessible and writable.
  • For long-term use, consider implementing a log rotation mechanism to manage log file sizes.
  • You may need to adjust the event IDs (21 for login, 23 for logout) if your RDS environment uses different event IDs for these actions.
  • Always test the script in a non-production environment before deploying it to production servers.

This script provides a robust solution for automatically logging RDS login and logout events, which can be valuable for security auditing, user activity tracking, and compliance purposes.

RDS Toolkit – Remote Desktop Services Management Script

<#
.SYNOPSIS
RDS Toolkit - Comprehensive Remote Desktop Services Management Script

.DESCRIPTION
This script provides a set of tools for managing and monitoring Remote Desktop Services environments.
It includes functions for session management, user monitoring, performance analysis, and reporting.

.NOTES
File Name      : RDSToolkit.ps1
Author         : [Your Name]
Prerequisite   : PowerShell V5.1 or later, admin rights on the RDS server
Version        : 1.0
Date           : [Current Date]

.EXAMPLE
.\RDSToolkit.ps1
#>

# Import required modules
Import-Module RemoteDesktop

# Global variables
$global:logFile = "$env:USERPROFILE\Desktop\RDS_Toolkit_Log_$(Get-Date -Format 'yyyyMMdd_HHmmss').log"

function Write-Log {
    param (
        [string]$Message
    )
    
    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    $logEntry = "$timestamp - $Message"
    Add-Content -Path $global:logFile -Value $logEntry
    Write-Host $logEntry
}

function Show-Menu {
    Clear-Host
    Write-Host "=== RDS Toolkit ===" -ForegroundColor Cyan
    Write-Host "1. List Active Sessions"
    Write-Host "2. Disconnect User"
    Write-Host "3. Log Off User"
    Write-Host "4. Send Message to Users"
    Write-Host "5. Monitor Disconnected Sessions"
    Write-Host "6. View RDS Server Performance"
    Write-Host "7. Generate RDS Usage Report"
    Write-Host "8. Manage RDS Collections"
    Write-Host "9. Exit"
}

function Get-ActiveSessions {
    $sessions = quser | Where-Object { $_ -match 'Active' }
    $activeSessions = @()
    foreach ($session in $sessions) {
        $sessionInfo = $session -split '\s+'
        $activeSessions += [PSCustomObject]@{
            Username = $sessionInfo[1]
            SessionID = $sessionInfo[2]
            State = $sessionInfo[3]
            IdleTime = $sessionInfo[4]
            LogonTime = $sessionInfo[5..($sessionInfo.Length-1)] -join ' '
        }
    }
    return $activeSessions
}

function Disconnect-RDSUser {
    $username = Read-Host "Enter username to disconnect"
    $sessions = quser | Where-Object { $_ -match $username }
    if ($sessions) {
        $sessionId = ($sessions -split '\s+')[2]
        tsdiscon $sessionId
        Write-Log "Disconnected user: $username (Session ID: $sessionId)"
    } else {
        Write-Host "User not found or not connected." -ForegroundColor Yellow
    }
}

function LogOff-RDSUser {
    $username = Read-Host "Enter username to log off"
    $sessions = quser | Where-Object { $_ -match $username }
    if ($sessions) {
        $sessionId = ($sessions -split '\s+')[2]
        logoff $sessionId
        Write-Log "Logged off user: $username (Session ID: $sessionId)"
    } else {
        Write-Host "User not found or not connected." -ForegroundColor Yellow
    }
}

function Send-MessageToUsers {
    $message = Read-Host "Enter message to send"
    $users = Get-ActiveSessions
    foreach ($user in $users) {
        msg $user.Username $message
    }
    Write-Log "Sent message to all active users: $message"
}

function Monitor-DisconnectedSessions {
    $duration = Read-Host "Enter monitoring duration in minutes (0 for continuous)"
    $startTime = Get-Date
    Write-Host "Monitoring disconnected sessions. Press Ctrl+C to stop." -ForegroundColor Yellow
    
    try {
        while ($true) {
            $disconnectedSessions = quser | Where-Object { $_ -match 'Disc' }
            foreach ($session in $disconnectedSessions) {
                $sessionInfo = $session -split '\s+'
                Write-Host "Disconnected: $($sessionInfo[1]) (Session ID: $($sessionInfo[2]))" -ForegroundColor Red
                Write-Log "Disconnected session detected: $($sessionInfo[1]) (Session ID: $($sessionInfo[2]))"
            }
            
            if ($duration -ne "0" -and ((Get-Date) - $startTime).TotalMinutes -ge $duration) {
                break
            }
            
            Start-Sleep -Seconds 30
        }
    }
    catch {
        Write-Host "Monitoring stopped." -ForegroundColor Yellow
    }
}

function View-RDSServerPerformance {
    $duration = Read-Host "Enter monitoring duration in minutes"
    $interval = Read-Host "Enter sampling interval in seconds"
    $startTime = Get-Date
    $endTime = $startTime.AddMinutes($duration)
    
    Write-Host "Monitoring RDS server performance. This may take $duration minutes." -ForegroundColor Yellow
    
    $performanceData = @()
    
    while ((Get-Date) -lt $endTime) {
        $cpu = (Get-Counter '\Processor(_Total)\% Processor Time').CounterSamples.CookedValue
        $memory = (Get-Counter '\Memory\% Committed Bytes In Use').CounterSamples.CookedValue
        $disk = (Get-Counter '\PhysicalDisk(_Total)\% Disk Time').CounterSamples.CookedValue
        $network = (Get-Counter '\Network Interface(*)\Bytes Total/sec').CounterSamples.CookedValue | Measure-Object -Sum | Select-Object -ExpandProperty Sum
        
        $performanceData += [PSCustomObject]@{
            Timestamp = Get-Date
            CPU = [math]::Round($cpu, 2)
            Memory = [math]::Round($memory, 2)
            Disk = [math]::Round($disk, 2)
            Network = [math]::Round($network / 1MB, 2)  # Convert to MB/s
        }
        
        Start-Sleep -Seconds $interval
    }
    
    $performanceData | Format-Table -AutoSize
    $performanceData | Export-Csv -Path "$env:USERPROFILE\Desktop\RDS_Performance_$(Get-Date -Format 'yyyyMMdd_HHmmss').csv" -NoTypeInformation
    Write-Host "Performance data exported to desktop." -ForegroundColor Green
}

function Generate-RDSUsageReport {
    $days = Read-Host "Enter number of days for the report"
    $startDate = (Get-Date).AddDays(-$days)
    $events = Get-WinEvent -FilterHashtable @{
        LogName = 'Microsoft-Windows-TerminalServices-LocalSessionManager/Operational'
        ID = 21, 23, 24, 25  # Logon, Logoff, Disconnect, Reconnect
        StartTime = $startDate
    }
    
    $report = @()
    foreach ($event in $events) {
        $report += [PSCustomObject]@{
            Timestamp = $event.TimeCreated
            Username = $event.Properties[0].Value
            EventType = switch ($event.Id) {
                21 { "Logon" }
                23 { "Logoff" }
                24 { "Disconnect" }
                25 { "Reconnect" }
            }
        }
    }
    
    $report | Format-Table -AutoSize
    $report | Export-Csv -Path "$env:USERPROFILE\Desktop\RDS_Usage_Report_$(Get-Date -Format 'yyyyMMdd_HHmmss').csv" -NoTypeInformation
    Write-Host "Usage report exported to desktop." -ForegroundColor Green
}

function Manage-RDSCollections {
    Write-Host "RDS Collections Management" -ForegroundColor Cyan
    Write-Host "1. List Collections"
    Write-Host "2. Create New Collection"
    Write-Host "3. Add Server to Collection"
    Write-Host "4. Remove Server from Collection"
    Write-Host "5. Back to Main Menu"
    
    $choice = Read-Host "Enter your choice"
    
    switch ($choice) {
        "1" {
            Get-RDSessionCollection | Format-Table -AutoSize
        }
        "2" {
            $name = Read-Host "Enter new collection name"
            $sessionHost = Read-Host "Enter session host server name"
            New-RDSessionCollection -CollectionName $name -SessionHost $sessionHost
            Write-Log "Created new RDS collection: $name"
        }
        "3" {
            $collection = Read-Host "Enter collection name"
            $server = Read-Host "Enter server to add"
            Add-RDSessionHost -CollectionName $collection -SessionHost $server
            Write-Log "Added server $server to collection $collection"
        }
        "4" {
            $collection = Read-Host "Enter collection name"
            $server = Read-Host "Enter server to remove"
            Remove-RDSessionHost -CollectionName $collection -SessionHost $server
            Write-Log "Removed server $server from collection $collection"
        }
        "5" {
            return
        }
        default {
            Write-Host "Invalid choice. Please try again." -ForegroundColor Red
        }
    }
    
    Pause
    Manage-RDSCollections
}

# Main program loop
do {
    Show-Menu
    $choice = Read-Host "`nEnter your choice (1-9)"

    switch ($choice) {
        "1" { Get-ActiveSessions | Format-Table -AutoSize }
        "2" { Disconnect-RDSUser }
        "3" { LogOff-RDSUser }
        "4" { Send-MessageToUsers }
        "5" { Monitor-DisconnectedSessions }
        "6" { View-RDSServerPerformance }
        "7" { Generate-RDSUsageReport }
        "8" { Manage-RDSCollections }
        "9" { Write-Host "Exiting program..." -ForegroundColor Yellow; break }
        default { Write-Host "Invalid choice. Please try again." -ForegroundColor Red }
    }

    if ($choice -ne "9") {
        Read-Host "`nPress Enter to continue..."
    }
} while ($choice -ne "9")

This RDS Toolkit script provides a comprehensive set of tools for managing and monitoring Remote Desktop Services environments. Here’s a breakdown of its features:

  1. List Active Sessions: Displays all currently active RDS sessions.
  2. Disconnect User: Allows you to disconnect a specific user.
  3. Log Off User: Allows you to log off a specific user.
  4. Send Message to Users: Sends a broadcast message to all active users.
  5. Monitor Disconnected Sessions: Continuously monitors and reports on disconnected sessions.
  6. View RDS Server Performance: Monitors and reports on server performance metrics (CPU, Memory, Disk, Network).
  7. Generate RDS Usage Report: Creates a report of RDS usage over a specified number of days.
  8. Manage RDS Collections: Provides options to list, create, and modify RDS collections.

To use this script:

  1. Save it as RDSToolkit.ps1.
  2. Open PowerShell as an administrator on the RDS server.
  3. Navigate to the directory containing the script.
  4. Run the script:

.\RDSToolkit.ps1

Notes:

  • This script requires administrative privileges on the RDS server.
  • Some functions may require additional modules or permissions depending on your RDS setup.
  • The script creates log files and exports reports to the desktop by default. You may want to modify these paths for production use.
  • Always test the script in a non-production environment before using it in production.

This toolkit provides a solid foundation for managing RDS environments and can be further customized based on specific organizational needs or more complex RDS setups.

RDS User Disconnected Monitor and Action Script

<#
.SYNOPSIS
RDS User Disconnected Monitor and Action Script

.DESCRIPTION
This script monitors Remote Desktop Services for disconnected user sessions
and performs specified actions when a user disconnects.

.PARAMETER Action
The action to perform when a user disconnects. Options are 'Log', 'Logoff', or 'Both'.

.PARAMETER LogFile
The path of the log file where disconnect events will be recorded.

.EXAMPLE
.\RDSDisconnectMonitor.ps1 -Action "Log" -LogFile "C:\Logs\RDSDisconnects.log"

.NOTES
File Name      : RDSDisconnectMonitor.ps1
Author         : [Your Name]
Prerequisite   : PowerShell V3 or later, admin rights on the RDS server
Version        : 1.0
Date           : [Current Date]
#>

param (
    [Parameter(Mandatory=$true)]
    [ValidateSet("Log", "Logoff", "Both")]
    [string]$Action,

    [Parameter(Mandatory=$true)]
    [string]$LogFile
)

# Function to write log entries
function Write-Log {
    param (
        [string]$Message
    )
    
    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    $logEntry = "$timestamp - $Message"
    Add-Content -Path $LogFile -Value $logEntry
    Write-Host $logEntry
}

# Function to get disconnected sessions
function Get-DisconnectedSessions {
    $sessions = quser | Where-Object { $_ -match 'Disc' }
    $disconnectedUsers = @()
    foreach ($session in $sessions) {
        $sessionInfo = $session -split '\s+'
        $disconnectedUsers += [PSCustomObject]@{
            Username = $sessionInfo[1]
            SessionID = $sessionInfo[2]
        }
    }
    return $disconnectedUsers
}

# Function to perform action on disconnected session
function Perform-Action {
    param (
        [string]$Username,
        [string]$SessionID
    )

    switch ($Action) {
        "Log" {
            Write-Log "User disconnected: $Username (Session ID: $SessionID)"
        }
        "Logoff" {
            logoff $SessionID
            Write-Log "User logged off: $Username (Session ID: $SessionID)"
        }
        "Both" {
            Write-Log "User disconnected: $Username (Session ID: $SessionID)"
            logoff $SessionID
            Write-Log "User logged off: $Username (Session ID: $SessionID)"
        }
    }
}

# Create the log file if it doesn't exist
if (-not (Test-Path $LogFile)) {
    New-Item -Path $LogFile -ItemType File -Force
}

Write-Log "Starting RDS disconnect monitoring..."

# Main monitoring loop
try {
    $previousSessions = @()
    while ($true) {
        $currentSessions = Get-DisconnectedSessions
        
        # Check for new disconnections
        foreach ($session in $currentSessions) {
            if ($session.Username -notin $previousSessions.Username) {
                Perform-Action -Username $session.Username -SessionID $session.SessionID
            }
        }

        $previousSessions = $currentSessions
        Start-Sleep -Seconds 30  # Check every 30 seconds
    }
}
catch {
    Write-Log "An error occurred: $_"
}
finally {
    Write-Log "RDS disconnect monitoring stopped."
}

To use this script:

  1. Save it as RDSDisconnectMonitor.ps1.
  2. Open PowerShell as an administrator on the RDS server.
  3. Navigate to the directory containing the script.
  4. Run the script with the required parameters:

.\RDSDisconnectMonitor.ps1 -Action "Log" -LogFile "C:\Logs\RDSDisconnects.log"

You can replace “Log” with “Logoff” to automatically log off disconnected users, or “Both” to log and then log off.

Key features of this script:

  1. Monitoring: Continuously checks for disconnected RDS sessions.
  2. Flexible Actions: Can log disconnections, automatically log off disconnected users, or both.
  3. Logging: Records all actions and events to a specified log file.
  4. Customizable: Easy to modify for additional actions or different checking intervals.

Notes:

  • This script needs to be run with administrator privileges on the RDS server.
  • The script will continue running until manually stopped (e.g., by pressing Ctrl+C).
  • For production use, consider running this script as a Windows Service or scheduled task.
  • The script checks for disconnections every 30 seconds by default. You can adjust this interval by changing the Start-Sleep -Seconds 30 line.
  • Be cautious when using the “Logoff” action, as it will forcibly close user sessions, which might result in data loss if users have unsaved work.

This script provides a foundation for monitoring and managing disconnected RDS sessions. You can further customize it based on your specific requirements, such as sending notifications, integrating with other systems, or implementing more complex decision logic for when to log off users.

Automated Folder Access Logging Script

<#
.SYNOPSIS
Automated Folder Access Logging Script

.DESCRIPTION
This script monitors a specified folder and its subfolders for file system events
and logs these events to a file. It uses the FileSystemWatcher class to monitor
the folder in real-time.

.PARAMETER FolderPath
The path of the folder to monitor.

.PARAMETER LogFile
The path of the log file where events will be recorded.

.EXAMPLE
.\FolderAccessLogger.ps1 -FolderPath "C:\ImportantFolder" -LogFile "C:\Logs\FolderAccess.log"

.NOTES
File Name      : FolderAccessLogger.ps1
Author         : [Your Name]
Prerequisite   : PowerShell V3 or later
Version        : 1.0
Date           : [Current Date]
#>

param (
    [Parameter(Mandatory=$true)]
    [string]$FolderPath,

    [Parameter(Mandatory=$true)]
    [string]$LogFile
)

# Function to write log entries
function Write-Log {
    param (
        [string]$Message
    )
    
    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    $logEntry = "$timestamp - $Message"
    Add-Content -Path $LogFile -Value $logEntry
    Write-Host $logEntry
}

# Function to handle file system events
function Handle-FileSystemEvent {
    param (
        [System.IO.FileSystemEventArgs]$e
    )

    $eventType = $e.ChangeType
    $fullPath = $e.FullPath
    $message = "Event: $eventType, Path: $fullPath"
    Write-Log $message
}

# Create the log file if it doesn't exist
if (-not (Test-Path $LogFile)) {
    New-Item -Path $LogFile -ItemType File -Force
}

# Create a new FileSystemWatcher
$watcher = New-Object System.IO.FileSystemWatcher
$watcher.Path = $FolderPath
$watcher.IncludeSubdirectories = $true
$watcher.EnableRaisingEvents = $true

# Define the events to watch for
$changeTypes = [System.IO.WatcherChangeTypes]::Created -bor `
               [System.IO.WatcherChangeTypes]::Deleted -bor `
               [System.IO.WatcherChangeTypes]::Changed -bor `
               [System.IO.WatcherChangeTypes]::Renamed

# Set up event handlers
$onChanged = Register-ObjectEvent $watcher "Changed" -Action {
    Handle-FileSystemEvent -e $Event.SourceEventArgs
}
$onCreated = Register-ObjectEvent $watcher "Created" -Action {
    Handle-FileSystemEvent -e $Event.SourceEventArgs
}
$onDeleted = Register-ObjectEvent $watcher "Deleted" -Action {
    Handle-FileSystemEvent -e $Event.SourceEventArgs
}
$onRenamed = Register-ObjectEvent $watcher "Renamed" -Action {
    $oldPath = $Event.SourceEventArgs.OldFullPath
    $newPath = $Event.SourceEventArgs.FullPath
    $message = "Event: Renamed, Old Path: $oldPath, New Path: $newPath"
    Write-Log $message
}

Write-Log "Starting folder access monitoring for: $FolderPath"

try {
    # Keep the script running
    while ($true) {
        Start-Sleep -Seconds 1
    }
}
finally {
    # Clean up event handlers when the script is stopped
    Unregister-Event -SourceIdentifier $onChanged.Name
    Unregister-Event -SourceIdentifier $onCreated.Name
    Unregister-Event -SourceIdentifier $onDeleted.Name
    Unregister-Event -SourceIdentifier $onRenamed.Name
    $watcher.Dispose()
    Write-Log "Folder access monitoring stopped."
}

To use this script:

  1. Save it as FolderAccessLogger.ps1.
  2. Open PowerShell as an administrator.
  3. Navigate to the directory containing the script.
  4. Run the script with the required parameters:

.\FolderAccessLogger.ps1 -FolderPath "C:\PathToMonitor" -LogFile "C:\Logs\FolderAccess.log"

Replace "C:\PathToMonitor" with the path of the folder you want to monitor, and "C:\Logs\FolderAccess.log" with the desired path for your log file.

Key features of this script:

  1. Real-time monitoring: Uses FileSystemWatcher to detect changes as they happen.
  2. Comprehensive logging: Logs creation, deletion, modification, and renaming of files and folders.
  3. Subfolder inclusion: Monitors the specified folder and all its subfolders.
  4. Timestamped logs: Each log entry includes a timestamp for easy tracking.
  5. Continuous operation: The script runs indefinitely until manually stopped.
  6. Clean shutdown: Properly disposes of resources when the script is stopped.

Notes:

  • This script needs to be run with appropriate permissions to access the folder being monitored and to write to the log file.
  • The script will continue running until you manually stop it (e.g., by pressing Ctrl+C).
  • For long-term use, consider running this script as a Windows Service or scheduled task.
  • Be aware that monitoring a very active folder or a folder with many subfolders can generate a large number of events and potentially impact system performance.

This script provides a solid foundation for monitoring folder access and can be further customized based on specific needs, such as filtering certain types of files or events, or integrating with other notification systems.

Dr. Scripto and the Virtual Machine Apocalypse

It was a stormy night at the PowerShell Academy. Lightning flashed across the sky, illuminating the towering servers in the data center. Dr. Scripto, the renowned PowerShell wizard, was burning the midnight oil, putting the finishing touches on his latest creation: a revolutionary VM management system.

“Just one more line of code,” he muttered, his fingers flying across the keyboard. “And… done!”

As he hit the Enter key, a loud crack of thunder shook the building. The lights flickered ominously, and for a moment, everything went dark. When the emergency generators kicked in, Dr. Scripto’s eyes widened in horror as he stared at his screen.

ERROR: CRITICAL FAILURE – ALL VIRTUAL MACHINES UNRESPONSIVE

“Great Scott!” Dr. Scripto exclaimed, adjusting his PowerShell-themed bowtie nervously. “What have I done?”

Just then, his phone began to buzz incessantly. Messages flooded in from panicked students and staff across the academy. Every virtual machine on campus had suddenly failed, bringing all operations to a screeching halt.

Dr. Scripto knew he had to act fast. He cracked his knuckles and dove into the problem, starting with a quick assessment:

$failedVMs = Get-VM | Where-Object {$_.State -eq 'Critical'}
Write-Host "Number of failed VMs: $($failedVMs.Count)"

The result made him gasp. Every single VM was in a critical state. This was worse than he thought.

Determined to get to the bottom of this, Dr. Scripto began his investigation. He started by checking the host servers:

$hostHealth = Get-VMHost | Select-Object Name, MemoryUsageGB, CpuUsageHz
$hostHealth | Format-Table -AutoSize

To his surprise, the hosts seemed fine. Memory and CPU usage were well within normal ranges. “Curiouser and curiouser,” he mused, stroking his PowerShell-blue beard.

Next, he decided to check the storage:

$storageHealth = Get-DataStore | Select-Object Name, FreeSpaceGB, CapacityGB
$storageHealth | Where-Object {$_.FreeSpaceGB -lt 10} | Format-Table -AutoSize

Again, nothing seemed amiss. There was plenty of free space on all datastores.

Dr. Scripto’s brow furrowed in concentration. If it wasn’t the hosts or the storage, what could be causing this mass VM failure? He decided to dig deeper into the VMs themselves:

$vmDetails = $failedVMs | Select-Object Name, NumCpu, MemoryGB, Notes
$vmDetails | Export-Csv -Path "C:\FailedVMs.csv" -NoTypeInformation

As he examined the exported CSV file, a pattern began to emerge. All the failed VMs had been created or modified in the last 24 hours – right around the time he had been working on his new management system.

“Eureka!” Dr. Scripto exclaimed. “My new system must have introduced a bug that affected all these VMs!”

Now that he had identified the problem, Dr. Scripto set about crafting a solution. He quickly wrote a script to revert the changes made by his management system:

foreach ($vm in $failedVMs) {
    try {
        Restore-VMSnapshot -VM $vm -Name "Pre-Management-System" -Confirm:$false
        Start-VM -VM $vm
        Write-Host "Successfully restored and started $($vm.Name)" -ForegroundColor Green
    }
    catch {
        Write-Host "Failed to restore $($vm.Name): $_" -ForegroundColor Red
    }
}

As the script ran, Dr. Scripto watched anxiously. One by one, the VMs began to come back online. The phone calls and messages slowed, then stopped altogether.

But Dr. Scripto knew his work wasn’t done. He needed to prevent this from happening again. He spent the rest of the night implementing safeguards and writing a comprehensive testing suite for his VM management system.

As the sun began to rise, casting a warm glow over the academy, Dr. Scripto put the finishing touches on his improved system. He had added error handling, logging, and even a rollback feature in case of unexpected issues:

function Invoke-VMManagementTask {
    param(
        [Parameter(Mandatory=$true)]
        [string]$VMName,
        [Parameter(Mandatory=$true)]
        [scriptblock]$Task
    )

    $logPath = "C:\Logs\VMManagement.log"
    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"

    try {
        # Create a snapshot before making changes
        Checkpoint-VM -Name $VMName -SnapshotName "Pre-Task-$timestamp"

        # Execute the task
        & $Task

        # Log success
        "$timestamp - Successfully executed task on $VMName" | Out-File -Append -FilePath $logPath
    }
    catch {
        # Log the error
        "$timestamp - Error executing task on $VMName: $_" | Out-File -Append -FilePath $logPath

        # Attempt to revert to the pre-task state
        try {
            Restore-VMSnapshot -VMName $VMName -Name "Pre-Task-$timestamp" -Confirm:$false
            "$timestamp - Successfully reverted $VMName to pre-task state" | Out-File -Append -FilePath $logPath
        }
        catch {
            "$timestamp - Failed to revert $VMName: $_" | Out-File -Append -FilePath $logPath
        }

        # Re-throw the original error
        throw
    }
    finally {
        # Clean up old snapshots
        Get-VMSnapshot -VMName $VMName | Where-Object { $_.CreationTime -lt (Get-Date).AddDays(-7) } | Remove-VMSnapshot
    }
}

Just as he was about to head home for a well-deserved rest, there was a knock at his office door. It was the Dean, looking both relieved and impressed.

“Dr. Scripto,” the Dean said, “I don’t know how you did it, but you saved the entire academy from a complete technological meltdown. The board has decided to award you the prestigious Golden PowerShell Award for your quick thinking and innovative solution.”

Dr. Scripto blushed, his mustache twitching with pride. “Thank you, Dean. But I couldn’t have done it without the power of PowerShell and the support of our wonderful staff and students.”

As he accepted the award, Dr. Scripto couldn’t help but reflect on the night’s events. It had been a close call, but it had also been an invaluable learning experience. He knew that the lessons learned from this virtual machine apocalypse would inform his teaching and scripting for years to come.

“Remember, students,” he announced at the next day’s assembly, “in the world of IT, disasters are just opportunities for growth in disguise. And with PowerShell by your side, there’s no challenge too great to overcome!”

The students cheered, inspired by Dr. Scripto’s words and his heroic actions. And as for Dr. Scripto himself, he was already dreaming up his next big project. After all, in the ever-evolving world of technology, there was always a new adventure waiting just around the corner.

From that day forward, the tale of Dr. Scripto and the Virtual Machine Apocalypse became legend at the PowerShell Academy. It served as a reminder of the importance of thorough testing, robust error handling, and the incredible problem-solving power of PowerShell. And Dr. Scripto? Well, he continued to inspire and educate, always ready with a clever script and a PowerShell pun for whatever challenges the future might bring.

Creating Best Practices for PowerShell Functions

Functions are a fundamental building block in PowerShell, allowing you to encapsulate reusable code and make your scripts more modular and maintainable. However, to ensure your functions are well-designed and follow best practices, there are several guidelines you should consider. In this post, we’ll explore some of the best practices for creating PowerShell functions.

1. Use Meaningful Function Names

The name of your function should be descriptive and convey the purpose of the function. Avoid using generic names like “DoSomething” or “ProcessData”. Instead, use names that clearly indicate what the function does, such as “Get-UserInfo” or “Convert-StringToInt”.

2. Provide Detailed Function Documentation

Every function should have a well-written help comment block that includes a description of the function, its parameters, return values, and any relevant examples. This documentation will not only help other users understand how to use your function, but it will also serve as a reference for you when you need to revisit the function in the future.

3. Use Consistent Parameter Naming

When defining the parameters for your function, use consistent naming conventions. This will make your functions more intuitive and easier to use. For example, if you have a function that takes a file path as input, use a parameter name like “FilePath” rather than “Path” or “FileName”.

4. Validate Input Parameters

Always validate the input parameters to your function to ensure that they are of the expected type and within the expected range of values. This will help prevent errors and unexpected behavior in your functions.

5. Use Appropriate Data Types

Choose the appropriate data types for your function’s parameters and return values. This will help ensure that your function behaves as expected and interoperates well with other PowerShell cmdlets and functions.

6. Provide Meaningful Error Handling

When your function encounters an error, provide meaningful error messages that explain what went wrong and how the user can resolve the issue. Use the throw statement to raise exceptions and provide detailed error information.

7. Implement Robust Error Handling

Use try-catch blocks to handle exceptions that may occur within your function. This will help ensure that your function gracefully handles errors and doesn’t crash unexpectedly.

8. Use Appropriate Cmdlet Verbs

When naming your functions, use the appropriate cmdlet verbs as defined by the PowerShell team. This will help ensure that your functions follow the PowerShell naming conventions and are more easily recognized by other PowerShell users.

9. Provide Examples and Samples

Include examples and sample usage in your function’s help documentation to help users understand how to use your function effectively.

10. Test Your Functions Thoroughly

Before releasing your functions, test them thoroughly to ensure they work as expected and handle edge cases appropriately.

By following these best practices, you can create PowerShell functions that are well-designed, maintainable, and easy to use. This will not only benefit you as the function author, but also the users who will be consuming your functions.

Sample script:

<#
.SYNOPSIS
    Retrieves user information from Active Directory.
.DESCRIPTION
    This function retrieves user information from Active Directory, including the user's name, email address, and department.
.PARAMETER UserPrincipalName
    The user's principal name (e.g., "user@powershellblog.com").
.EXAMPLE
    Get-UserInfo -UserPrincipalName "user@powershellblog.com"
    Retrieves the user information for the user with the principal name "user@powershellblog.com".
.NOTES
    This function requires the ActiveDirectory module to be installed and the user running the script to have the necessary permissions to query Active Directory.
#>
function Get-UserInfo {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true, HelpMessage="Enter the user's principal name.")]
        [ValidateNotNullOrEmpty()]
        [string]$UserPrincipalName
    )

    try {
        # Retrieve the user object from Active Directory
        $user = Get-ADUser -Filter "UserPrincipalName -eq '$UserPrincipalName'" -Properties EmailAddress, Department

        # Create a custom object to hold the user information
        $userInfo = [PSCustomObject]@{
            Name = $user.Name
            EmailAddress = $user.EmailAddress
            Department = $user.Department
        }

        # Return the user information
        return $userInfo
    }
    catch {
        # Handle any errors that occur
        throw "Error retrieving user information: $($_.Exception.Message)"
    }
}

# Example usage
$userInfo = Get-UserInfo -UserPrincipalName "user@powershellblog.com"
Write-Host "Name: $($userInfo.Name)"
Write-Host "Email Address: $($userInfo.EmailAddress)"
Write-Host "Department: $($userInfo.Department)"

Here’s how this script follows the best practices we discussed:

  1. Meaningful Function Name: The function is named Get-UserInfo, which clearly indicates its purpose.
  2. Detailed Function Documentation: The function includes a comprehensive help comment block that provides a synopsis, description, parameter information, an example, and notes.
  3. Consistent Parameter Naming: The function takes a single parameter, UserPrincipalName, which is a clear and consistent name for the user’s principal name.
  4. Input Parameter Validation: The function uses the [Parameter(Mandatory=$true)] attribute to ensure that the UserPrincipalName parameter is provided, and the [ValidateNotNullOrEmpty()] attribute to ensure that the parameter is not null or empty.
  5. Appropriate Data Types: The function uses the [string] data type for the UserPrincipalName parameter, which is the expected data type for a user’s principal name.
  6. Meaningful Error Handling: The function uses a try-catch block to handle any errors that may occur during the execution of the function, and it throws a custom error message with detailed information about the error.
  7. Appropriate Cmdlet Verb: The function uses the Get- verb, which is the appropriate cmdlet verb for a function that retrieves information.
  8. Examples and Samples: The function includes an example usage in the help comment block, which demonstrates how to use the function.
  9. Thorough Testing: While not shown in the script, it’s important to thoroughly test the function to ensure it works as expected and handles edge cases appropriately.

By following these best practices, the Get-UserInfo function is well-designed, maintainable, and easy to use.

Advanced Template Module Generator Tool

<#
.SYNOPSIS
Advanced Template Module Generator Tool

.DESCRIPTION
This script provides an interactive tool to generate a comprehensive structure for a PowerShell module,
including advanced features like dependency management, script analyzer settings, and extensive testing setup.

.NOTES
File Name      : AdvancedTemplateModuleGenerator.ps1
Author         : [Your Name]
Prerequisite   : PowerShell V5.1 or later
Version        : 1.0
Date           : [Current Date]

.EXAMPLE
.\AdvancedTemplateModuleGenerator.ps1
#>

function Show-Menu {
    Clear-Host
    Write-Host "=== Advanced Template Module Generator Tool ===" -ForegroundColor Cyan
    Write-Host "1. Set Module Details"
    Write-Host "2. Add Public Function"
    Write-Host "3. Add Private Function"
    Write-Host "4. Add Module Dependencies"
    Write-Host "5. Configure Build Settings"
    Write-Host "6. Configure Test Settings"
    Write-Host "7. Generate Module Files"
    Write-Host "8. Exit"
}

function Set-ModuleDetails {
    $script:moduleName = Read-Host "Enter the name of the module"
    $script:moduleDescription = Read-Host "Enter a brief description of the module"
    $script:moduleAuthor = Read-Host "Enter the author's name"
    $script:moduleVersion = Read-Host "Enter the module version (e.g., 1.0.0)"
    $script:moduleLicense = Read-Host "Enter the module license (e.g., MIT)"
    $script:moduleProjectUri = Read-Host "Enter the project URI (optional)"
    Write-Host "Module details set successfully." -ForegroundColor Green
}

function Add-PublicFunction {
    $functionName = Read-Host "Enter the name of the public function"
    $script:publicFunctions += $functionName
    Write-Host "Public function '$functionName' added." -ForegroundColor Green
}

function Add-PrivateFunction {
    $functionName = Read-Host "Enter the name of the private function"
    $script:privateFunctions += $functionName
    Write-Host "Private function '$functionName' added." -ForegroundColor Green
}

function Add-ModuleDependencies {
    do {
        $dependencyName = Read-Host "Enter the name of the module dependency (or press Enter to finish)"
        if ($dependencyName -ne "") {
            $dependencyVersion = Read-Host "Enter the required version (leave blank for any version)"
            $script:moduleDependencies += [PSCustomObject]@{
                Name = $dependencyName
                Version = $dependencyVersion
            }
            Write-Host "Dependency added: $dependencyName $dependencyVersion" -ForegroundColor Green
        }
    } while ($dependencyName -ne "")
}

function Set-BuildSettings {
    $script:usePSScriptAnalyzer = (Read-Host "Use PSScriptAnalyzer for code analysis? (Y/N)") -eq 'Y'
    $script:useplatyPS = (Read-Host "Use platyPS for documentation generation? (Y/N)") -eq 'Y'
    Write-Host "Build settings configured." -ForegroundColor Green
}

function Set-TestSettings {
    $script:usePester = (Read-Host "Use Pester for testing? (Y/N)") -eq 'Y'
    $script:useCodeCoverage = (Read-Host "Include code coverage in tests? (Y/N)") -eq 'Y'
    Write-Host "Test settings configured." -ForegroundColor Green
}

function Generate-ModuleFiles {
    if ([string]::IsNullOrWhiteSpace($script:moduleName)) {
        Write-Host "Please set module details first." -ForegroundColor Yellow
        return
    }

    $modulePath = Join-Path -Path $PWD -ChildPath $script:moduleName
    New-Item -Path $modulePath -ItemType Directory -Force | Out-Null

    # Create module manifest
    $manifestPath = Join-Path -Path $modulePath -ChildPath "$($script:moduleName).psd1"
    $manifestParams = @{
        Path = $manifestPath
        RootModule = "$($script:moduleName).psm1"
        ModuleVersion = $script:moduleVersion
        Author = $script:moduleAuthor
        Description = $script:moduleDescription
        FunctionsToExport = $script:publicFunctions
        PowerShellVersion = '5.1'
    }
    if ($script:moduleLicense) { $manifestParams['LicenseUri'] = $script:moduleLicense }
    if ($script:moduleProjectUri) { $manifestParams['ProjectUri'] = $script:moduleProjectUri }
    if ($script:moduleDependencies) {
        $manifestParams['RequiredModules'] = $script:moduleDependencies | ForEach-Object {
            if ($_.Version) { @{ModuleName = $_.Name; ModuleVersion = $_.Version} } else { $_.Name }
        }
    }
    New-ModuleManifest @manifestParams

    # Create module script file
    $modulePSM1Path = Join-Path -Path $modulePath -ChildPath "$($script:moduleName).psm1"
    $modulePSM1Content = @"
# Dot source public/private functions
`$public = @(Get-ChildItem -Path `$PSScriptRoot\Public\*.ps1 -ErrorAction SilentlyContinue)
`$private = @(Get-ChildItem -Path `$PSScriptRoot\Private\*.ps1 -ErrorAction SilentlyContinue)

foreach (`$import in @(`$public + `$private)) {
    try {
        . `$import.FullName
    } catch {
        Write-Error -Message "Failed to import function `$(`$import.FullName): `$_"
    }
}

# Export public functions
Export-ModuleMember -Function `$public.BaseName
"@
    Set-Content -Path $modulePSM1Path -Value $modulePSM1Content

    # Create Public and Private folders
    New-Item -Path (Join-Path -Path $modulePath -ChildPath "Public") -ItemType Directory -Force | Out-Null
    New-Item -Path (Join-Path -Path $modulePath -ChildPath "Private") -ItemType Directory -Force | Out-Null

    # Create function files
    foreach ($function in $script:publicFunctions) {
        $functionPath = Join-Path -Path $modulePath -ChildPath "Public\$function.ps1"
        $functionContent = @"
function $function {
    <#
    .SYNOPSIS
    Brief description of $function

    .DESCRIPTION
    Detailed description of $function

    .PARAMETER Param1
    Description of Param1

    .EXAMPLE
    $function -Param1 Value
    Explanation of the example

    .NOTES
    Additional notes about the function
    #>
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=`$true)]
        [string]`$Param1
    )

    begin {
        # Initialize any prerequisites
    }

    process {
        # Main function logic
    }

    end {
        # Cleanup or final actions
    }
}
"@
        Set-Content -Path $functionPath -Value $functionContent
    }

    foreach ($function in $script:privateFunctions) {
        $functionPath = Join-Path -Path $modulePath -ChildPath "Private\$function.ps1"
        $functionContent = @"
function $function {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=`$true)]
        [string]`$Param1
    )

    # Function logic here
}
"@
        Set-Content -Path $functionPath -Value $functionContent
    }

    # Create tests folder and files
    $testsFolderPath = Join-Path -Path $modulePath -ChildPath "Tests"
    New-Item -Path $testsFolderPath -ItemType Directory -Force | Out-Null

    if ($script:usePester) {
        $testFilePath = Join-Path -Path $testsFolderPath -ChildPath "$($script:moduleName).Tests.ps1"
        $testFileContent = @"
`$ModuleName = '$($script:moduleName)'
`$ModuleManifestPath = "`$PSScriptRoot\..\`$ModuleName.psd1"

Describe "`$ModuleName Module Tests" {
    BeforeAll {
        `$Module = Import-Module `$ModuleManifestPath -PassThru -Force
    }

    It "Module manifest is valid" {
        Test-ModuleManifest -Path `$ModuleManifestPath | Should -Not -BeNullOrEmpty
        `$? | Should -Be `$true
    }

    It "Module can be imported" {
        `$Module | Should -Not -BeNullOrEmpty
    }

    Context "Function Tests" {
        # Add tests for each function
        $(foreach ($function in $script:publicFunctions) {
@"
        It "$function exists" {
            Get-Command $function -Module `$ModuleName | Should -Not -BeNullOrEmpty
        }

"@
        })
    }
}
"@
        Set-Content -Path $testFilePath -Value $testFileContent
    }

    # Create build configuration
    $buildFolderPath = Join-Path -Path $modulePath -ChildPath "Build"
    New-Item -Path $buildFolderPath -ItemType Directory -Force | Out-Null

    $buildFilePath = Join-Path -Path $buildFolderPath -ChildPath "build.ps1"
    $buildFileContent = @"
param (
    [switch]`$Test,
    [switch]`$Analyze,
    [switch]`$Documentation
)

`$ModuleName = '$($script:moduleName)'
`$ModuleRoot = "`$PSScriptRoot\.."
`$OutputPath = "`$ModuleRoot\Output"

# Clean and create output directory
if (Test-Path -Path `$OutputPath) {
    Remove-Item -Path `$OutputPath -Recurse -Force
}
New-Item -Path `$OutputPath -ItemType Directory -Force | Out-Null

# Copy module files to output directory
Copy-Item -Path "`$ModuleRoot\`$ModuleName.psd1" -Destination `$OutputPath
Copy-Item -Path "`$ModuleRoot\`$ModuleName.psm1" -Destination `$OutputPath
Copy-Item -Path "`$ModuleRoot\Public" -Destination `$OutputPath -Recurse
Copy-Item -Path "`$ModuleRoot\Private" -Destination `$OutputPath -Recurse

if (`$Test) {
    # Run Pester tests
    `$testResults = Invoke-Pester -Path "`$ModuleRoot\Tests" -PassThru
    if (`$testResults.FailedCount -gt 0) {
        throw "One or more tests failed."
    }
}

if (`$Analyze) {
    # Run PSScriptAnalyzer
    `$analysisResults = Invoke-ScriptAnalyzer -Path `$OutputPath -Recurse
    if (`$analysisResults) {
        `$analysisResults | Format-Table
        throw "PSScriptAnalyzer found one or more issues."
    }
}

if (`$Documentation) {
    # Generate documentation using platyPS
    Import-Module platyPS
    New-MarkdownHelp -Module `$ModuleName -OutputFolder "`$OutputPath\docs" -Force
}

Write-Host "Build completed successfully." -ForegroundColor Green
"@
    Set-Content -Path $buildFilePath -Value $buildFileContent

    # Create PSScriptAnalyzer settings file
    if ($script:usePSScriptAnalyzer) {
        $analyzerSettingsPath = Join-Path -Path $modulePath -ChildPath "PSScriptAnalyzerSettings.psd1"
        $analyzerSettingsContent = @"
@{
    Severity = @('Error', 'Warning')
    ExcludeRules = @('PSUseShouldProcessForStateChangingFunctions')
    Rules = @{
        PSAvoidUsingCmdletAliases = @{
            Whitelist = @('Where', 'Select')
        }
    }
}
"@
        Set-Content -Path $analyzerSettingsPath -Value $analyzerSettingsContent
    }

    Write-Host "Module files generated successfully at: $modulePath" -ForegroundColor Green
}

# Initialize variables
$script:moduleName = ""
$script:moduleDescription = ""
$script:moduleAuthor = ""
$script:moduleVersion = ""
$script:moduleLicense = ""
$script:moduleProjectUri = ""
$script:publicFunctions = @()
$script:privateFunctions = @()
$script:moduleDependencies = @()
$script:usePSScriptAnalyzer = $false
$script:useplatyPS = $false
$script:usePester = $false
$script:useCodeCoverage = $false

# Main program loop
do {
    Show-Menu
    $choice = Read-Host "`nEnter your choice (1-8)"

    switch ($choice) {
        "1" { Set-ModuleDetails }
        "2" { Add-PublicFunction }
        "3" { Add-PrivateFunction }
        "4" { Add-ModuleDependencies }
        "5" { Set-BuildSettings }
        "6" { Set-TestSettings }
        "7" { Generate-ModuleFiles }
        "8" { Write-Host "Exiting program..." -ForegroundColor Yellow; break }
        default { Write-Host "Invalid choice. Please try again." -ForegroundColor Red }
    }

    if ($choice -ne "8") {
        Read-Host "`nPress Enter to continue..."
    }
} while ($choice -ne "8")

This Advanced Template Module Generator Tool includes:

  1. A menu-driven interface for easy navigation.
  2. Enhanced functions to:
    • Set detailed module information (including license and project URI)
    • Add public and private functions
    • Specify module dependencies
    • Configure build and test settings

Key features:

  1. Advanced Module Manifest Generation:
    • Includes more detailed module information
    • Supports specifying module dependencies
  2. Comprehensive Module Structure:
    • Creates a more robust folder structure including build and test directories
  3. Enhanced Function File Generation:
    • Generates more detailed function templates
  4. Build Script Generation:
    • Creates a build.ps1 script for module building, testing, and documentation generation
  5. Test Configuration:
    • Sets up Pester tests with placeholders for each public function
    • Optionally includes code coverage settings
  6. PSScriptAnalyzer Integration:
    • Generates a PSScriptAnalyzer settings file
    • Includes PSScriptAnalyzer in the build process
  7. Documentation Support:
    • Optionally sets up platyPS for documentation generation
  8. Dependency Management:
    • Allows specifying module dependencies with version requirements

This tool is particularly useful for:

  • Experienced PowerShell module developers
  • Teams working on larger PowerShell projects
  • Developers who want to follow best practices in module development

To use this script effectively:

  1. Run the script in PowerShell
  2. Use the menu options to:
    • Set detailed module information
    • Add public and private functions
    • Specify dependencies and configure build/test settings
    • Generate the module files
  3. Review the generated module structure and customize as needed

This advanced script provides a more comprehensive starting point for PowerShell module development, incorporating best practices and tools commonly used in professional PowerShell module projects.

Template Module Generator Tool

<#
.SYNOPSIS
Template Module Generator Tool

.DESCRIPTION
This script provides an interactive tool to generate a basic structure for a PowerShell module,
including the module manifest, public and private function files, and a basic test file.

.NOTES
File Name      : TemplateModuleGenerator.ps1
Author         : [Your Name]
Prerequisite   : PowerShell V5.1 or later
Version        : 1.0
Date           : [Current Date]

.EXAMPLE
.\TemplateModuleGenerator.ps1
#>

function Show-Menu {
    Clear-Host
    Write-Host "=== Template Module Generator Tool ===" -ForegroundColor Cyan
    Write-Host "1. Set Module Details"
    Write-Host "2. Add Public Function"
    Write-Host "3. Add Private Function"
    Write-Host "4. Generate Module Files"
    Write-Host "5. Exit"
}

function Set-ModuleDetails {
    $script:moduleName = Read-Host "Enter the name of the module"
    $script:moduleDescription = Read-Host "Enter a brief description of the module"
    $script:moduleAuthor = Read-Host "Enter the author's name"
    $script:moduleVersion = Read-Host "Enter the module version (e.g., 1.0.0)"
    Write-Host "Module details set successfully." -ForegroundColor Green
}

function Add-PublicFunction {
    $functionName = Read-Host "Enter the name of the public function"
    $script:publicFunctions += $functionName
    Write-Host "Public function '$functionName' added." -ForegroundColor Green
}

function Add-PrivateFunction {
    $functionName = Read-Host "Enter the name of the private function"
    $script:privateFunctions += $functionName
    Write-Host "Private function '$functionName' added." -ForegroundColor Green
}

function Generate-ModuleFiles {
    if ([string]::IsNullOrWhiteSpace($script:moduleName)) {
        Write-Host "Please set module details first." -ForegroundColor Yellow
        return
    }

    $modulePath = Join-Path -Path $PWD -ChildPath $script:moduleName
    New-Item -Path $modulePath -ItemType Directory -Force | Out-Null

    # Create module manifest
    $manifestPath = Join-Path -Path $modulePath -ChildPath "$($script:moduleName).psd1"
    New-ModuleManifest -Path $manifestPath `
        -RootModule "$($script:moduleName).psm1" `
        -ModuleVersion $script:moduleVersion `
        -Author $script:moduleAuthor `
        -Description $script:moduleDescription `
        -FunctionsToExport $script:publicFunctions

    # Create module script file
    $modulePSM1Path = Join-Path -Path $modulePath -ChildPath "$($script:moduleName).psm1"
    $modulePSM1Content = @"
# Dot source public/private functions
`$public = @(Get-ChildItem -Path `$PSScriptRoot\Public\*.ps1 -ErrorAction SilentlyContinue)
`$private = @(Get-ChildItem -Path `$PSScriptRoot\Private\*.ps1 -ErrorAction SilentlyContinue)

foreach (`$import in @(`$public + `$private)) {
    try {
        . `$import.FullName
    } catch {
        Write-Error -Message "Failed to import function `$(`$import.FullName): `$_"
    }
}

# Export public functions
Export-ModuleMember -Function `$public.BaseName
"@
    Set-Content -Path $modulePSM1Path -Value $modulePSM1Content

    # Create Public and Private folders
    New-Item -Path (Join-Path -Path $modulePath -ChildPath "Public") -ItemType Directory -Force | Out-Null
    New-Item -Path (Join-Path -Path $modulePath -ChildPath "Private") -ItemType Directory -Force | Out-Null

    # Create function files
    foreach ($function in $script:publicFunctions) {
        $functionPath = Join-Path -Path $modulePath -ChildPath "Public\$function.ps1"
        $functionContent = @"
function $function {
    <#
    .SYNOPSIS
    Brief description of $function

    .DESCRIPTION
    Detailed description of $function

    .PARAMETER Param1
    Description of Param1

    .EXAMPLE
    $function -Param1 Value
    Explanation of the example

    .NOTES
    Additional notes about the function
    #>
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=`$true)]
        [string]`$Param1
    )

    begin {
        # Initialize any prerequisites
    }

    process {
        # Main function logic
    }

    end {
        # Cleanup or final actions
    }
}
"@
        Set-Content -Path $functionPath -Value $functionContent
    }

    foreach ($function in $script:privateFunctions) {
        $functionPath = Join-Path -Path $modulePath -ChildPath "Private\$function.ps1"
        $functionContent = @"
function $function {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=`$true)]
        [string]`$Param1
    )

    # Function logic here
}
"@
        Set-Content -Path $functionPath -Value $functionContent
    }

    # Create a basic test file
    $testsFolderPath = Join-Path -Path $modulePath -ChildPath "Tests"
    New-Item -Path $testsFolderPath -ItemType Directory -Force | Out-Null
    $testFilePath = Join-Path -Path $testsFolderPath -ChildPath "$($script:moduleName).Tests.ps1"
    $testFileContent = @"
`$ModuleName = '$($script:moduleName)'
`$ModuleManifestPath = "`$PSScriptRoot\..\`$ModuleName.psd1"

Describe "`$ModuleName Module Tests" {
    It "Module manifest is valid" {
        Test-ModuleManifest -Path `$ModuleManifestPath | Should -Not -BeNullOrEmpty
        `$? | Should -Be `$true
    }

    It "Module can be imported" {
        Import-Module `$ModuleManifestPath
        Get-Module `$ModuleName | Should -Not -BeNullOrEmpty
    }

    # Add more tests for your functions here
}
"@
    Set-Content -Path $testFilePath -Value $testFileContent

    Write-Host "Module files generated successfully at: $modulePath" -ForegroundColor Green
}

# Initialize variables
$script:moduleName = ""
$script:moduleDescription = ""
$script:moduleAuthor = ""
$script:moduleVersion = ""
$script:publicFunctions = @()
$script:privateFunctions = @()

# Main program loop
do {
    Show-Menu
    $choice = Read-Host "`nEnter your choice (1-5)"

    switch ($choice) {
        "1" { Set-ModuleDetails }
        "2" { Add-PublicFunction }
        "3" { Add-PrivateFunction }
        "4" { Generate-ModuleFiles }
        "5" { Write-Host "Exiting program..." -ForegroundColor Yellow; break }
        default { Write-Host "Invalid choice. Please try again." -ForegroundColor Red }
    }

    if ($choice -ne "5") {
        Read-Host "`nPress Enter to continue..."
    }
} while ($choice -ne "5")

This Template Module Generator Tool includes:

  1. A menu-driven interface for easy navigation.
  2. Functions to:
    • Set module details (name, description, author, version)
    • Add public functions
    • Add private functions
    • Generate module files

Key features:

  1. Module Manifest Generation:
    • Creates a .psd1 file with basic module information
  2. Module Script File (.psm1) Generation:
    • Creates a .psm1 file that dot-sources public and private functions
  3. Function File Generation:
    • Creates separate .ps1 files for each public and private function
    • Generates a basic function template with comment-based help for public functions
  4. Directory Structure Creation:
    • Creates a module folder with Public and Private subfolders
  5. Basic Test File Generation:
    • Creates a simple Pester test file for the module

This tool is particularly useful for:

  • PowerShell module developers starting new projects
  • Anyone learning to create PowerShell modules
  • Developers who want to quickly scaffold a new module structure

To use this script effectively:

  1. Run the script in PowerShell
  2. Use the menu options to:
    • Set the module details
    • Add public and private functions as needed
    • Generate the module files
  3. Review the generated module structure in the specified directory

This script provides a quick and easy way to create a basic PowerShell module structure, following best practices for module organization. It saves time in setting up the initial files and folders, allowing developers to focus on writing the actual module functionality.