PSGubbins.psm1

# This PSM1 compiled with psake 2020/06/04 22:55:32

# PSGubbins\Source\Functions\Get-CCMLog.ps1
function Get-CCMLog
{
    <#
    .SYNOPSIS
        Retrieves entries from Configuration Manager's logs.
    .DESCRIPTION
        Retrieves entries from Configuration Manager's log files (defaulting to the last 2000 lines per file) from a local or remote computer and filters using pre-set patterns.
    .PARAMETER LogName
        The name of the log to be retrieved.
    .PARAMETER Path
        The directory the logs are stored in. UNC path's are assumed for use against remote machine but conversion from local drives is attempted.
    .PARAMETER ComputerName
        The computer(s) whose logs will queried.
    .PARAMETER AllMessages
        Returns all log entries rather than using the default pattern-matching for filtering.
    .EXAMPLE
        Get-CCMLog -ComputerName "lt-12345" -Count 25
 
        Retrieves the AppEnforce log of lt-12345
    .EXAMPLE
        Get-CCMLog -LogName "AppDiscovery"
 
        Retrieves the AppDiscovery log of the local machine
    .EXAMPLE
        Get-CCMLog -LogName "AppIntentEval", "AppDiscovery", "AppEnforce" | Out-GridView
 
        Retrieves the 'AppIntentEval', 'AppDiscovery' and 'AppEnforce' log entries and outputs to Out-GridView for interactive search and manipulation.
    .EXAMPLE
        Get-CCMLog -LogName PolicyAgent, AppDiscovery, AppIntentEval, CAS, ContentTransferManager, DataTransferService, AppEnforce | Out-GridView
 
        Retrieves logs allowing for the tracing of a deployment from machine policy to app enforcement and outputs to Out-GridView again.
    .INPUTS
        String, Integer, Switch
    .OUTPUTS
        PSCustomObject
    #>

    [CmdletBinding(ConfirmImpact = "Low", SupportsShouldProcess = $True)]
    [OutputType("PSCustomObject")]
    param
    (
        [Parameter(Position = 0, HelpMessage = "The log(s) to be retrieved")]
        [alias("Name")]
        [ValidateSet("AlternateHandler", "AppDiscovery", "AppEnforce", "AppIntentEval", "AssetAdvisor", "CAS", "Ccm32BitLauncher", "CcmCloud",
            "CcmEval", "CcmEvalTask", "CcmExec", "CcmMessaging", "CcmNotificationAgent", "CcmRepair", "CcmRestart", "CCMSDKProvider",
            "CcmSqlCE", "CCMVDIProvider", "CertEnrollAgent", "CertificateMaintenance", "CIAgent", "CIDownloader", "CIStateStore",
            "CIStore", "CITaskMgr", "ClientIDManagerStartup", "ClientLocation", "ClientServicing", "CMBITSManager", "CmRcService",
            "CoManagementHandler", "ComplRelayAgent", "ContentTransferManager", "DataTransferService", "DCMAgent", "DCMReporting",
            "DcmWmiProvider", "DdrProvider", "DeltaDownload", "EndpointProtectionAgent", "execmgr", "ExpressionSolver",
            "ExternalEventAgent", "FileBITS", "FSPStateMessage", "InternetProxy", "InventoryAgent", "InventoryProvider",
            "LocationServices", "MaintenanceCoordinator", "ManagedProvider", "mtrmgr", "oobmgmt", "PeerDPAgent", "PolicyAgent",
            "PolicyAgentProvider", "PolicyEvaluator", "pwrmgmt", "PwrProvider", "RebootCoordinator", "ScanAgent", "Scheduler",
            "ServiceWindowManager", "SettingsAgent", "setuppolicyevaluator", "smscliui", "SoftwareCatalogUpdateEndpoint",
            "SoftwareCenterSystemTasks", "SrcUpdateMgr", "StateMessage", "StateMessageProvider", "StatusAgent", "SWMTRReportGen",
            "UpdatesDeployment", "UpdatesHandler", "UpdatesStore", "UpdateTrustedSites", "UserAffinity", "UserAffinityProvider",
            "VirtualApp", "wedmtrace", "WindowsAnalytics", "WUAHandler")]
        [string[]] $LogName = "AppEnforce",

        [Parameter(Position = 1, HelpMessage = "The path to the directory containing the logs")]
        [string] $Path = "C:\Windows\CCM\Logs",

        [Parameter(Position = 3, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True, HelpMessage = "The computer whose logs will be parsed")]
        [alias("PSComputerName", "__SERVER", "CN", "IPAddress")]
        [string[]] $ComputerName = "localhost",

        [Parameter(HelpMessage = "Returns all log entries rather than using the default pattern-matching for filtering")]
        [alias("NoPattern")]
        [switch] $AllMessages
    )

    BEGIN
    {
        if ($PSBoundParameters.ContainsKey("Debug"))
        {
            $DebugPreference = "Continue"
        }

        $PatternContents = if ($AllMessages)
        {
            "(.+)"
        }
        else
        {
            "(a?)sync(hronous?)"
            "(Prepared|Executing) command line"
            "App enforcement completed"
            "Application not discovered"
            "authority"
            "cache"
            "complete(d?)"
            "criteria"
            "detect(ed?)"
            "exclude"
            "exit code"
            "fail(ure|s|ed)"
            "include"
            "Install enforcement"
            "missing"
            "Performing detection of app deployment type"
            "policy"
            "Post install behavior"
            "Prepared working directory"
            "registration"
            "releas(e|ed|ing)"
            "request(ed|ing)?"
            "revok(e|ed|ing)"
            "S-1-5"
            "search"
            "set"
            "size"
            "source"
            "state"
            "status"
            "succe(eded|ss)"
            "tombstone"
            "update(d?)"
            "user logged"
            "Waiting for process"
        }
        # Convert the array to a regex pattern
        $Pattern = $PatternContents -join "|"
    }

    PROCESS
    {
        forEach ($Computer in $ComputerName)
        {
            if (($Computer -like "localhost") -or ($Computer -like "127.0.0.1"))
            {
                Write-Verbose -Message "Converting localhost to hostname"
                $Computer = [Environment]::MachineName
            }
            Write-Verbose -Message "Processing '$Computer'"

            try
            {
                $LogRoot = $Null
                if ($Null -ne $Path)
                {
                    $JoinSplat = @{
                        Path        = "\\$Computer\"
                        ChildPath   = ($Path -replace ":", "$")
                        ErrorAction = "Stop"
                    }

                    $UNCPath = Join-Path @JoinSplat
                    if (Test-Path -Path $UNCPath -ErrorAction "Stop")
                    {
                        $LogRoot = Resolve-Path -Path $UNCPath -ErrorAction "Stop"
                    }
                }
                else
                {
                    $LogRoot = Resolve-Path "\\$Computer\C$\Windows\CCM\Logs\" -ErrorAction "Stop"
                }

                if ($LogRoot -match "^Microsoft.Powershell")
                {
                    $LogRoot = $LogRoot -Split "::" | Select-Object -Last 1
                }
            }
            catch
            {
                Write-Warning -Message "Problems were encountered resolving '$LogRoot'"
                $PSCmdlet.ThrowTerminatingError($_)
            }

            if (-not (Test-Path -Path $LogRoot))
            {
                Write-Warning -Message "Unable to resolve/access '$LogRoot'"
                Continue
            }

            Write-Verbose -Message "Testing for connectivity to '$Computer'"
            if (($Computer -eq [Environment]::MachineName) -or
                (Test-Connection -ComputerName $Computer -Quiet -Count 2))
            {
                forEach ($Log in $LogName)
                {
                    if ($PSCmdlet.ShouldProcess($LogRoot, "Retrieve '$Log' log entries"))
                    {
                        try
                        {
                            Write-Verbose -Message "Locating '$Log' log(s)."
                            $LogSearchSplat = $Null
                            $LogSearchSplat = @{
                                Path   = $LogRoot
                                Filter = "$Log*.log"
                                File   = $True
                            }

                            $LogPaths = $Null
                            $LogPaths = @(Get-ChildItem @LogSearchSplat  | Select-Object -ExpandProperty "FullName")
                            Write-Verbose -Message "'$($LogPaths.Count)' '$Log' logs found."
                        }
                        catch
                        {
                            Write-Warning -Message "Problems were encountered '$Log' logs from '$LogRoot'"
                            $PSCmdlet.ThrowTerminatingError($_)
                        }

                        forEach ($LogPath in $LogPaths)
                        {
                            if (Test-Path -Path $LogPath -ErrorAction "Stop")
                            {
                                try
                                {
                                    $Parameters = @{
                                        Path        = $LogPath
                                        Tail        = 2000     # I'm reluctant to read the entirety of the files by default and this seems likely to read most logs.
                                        ErrorAction = "Stop"
                                    }

                                    Write-Verbose -Message "Reading log ($LogPath)."
                                    $LogContents = Get-Content @Parameters |
                                        Where-Object { $_ -match $Pattern }
                                }
                                catch [System.UnauthorizedAccessException]
                                {
                                    Write-Warning -Message "Unable due to retrieve details to an 'Unauthorized Access' exception. Ensure you have the required permissions."
                                    $PSCmdlet.ThrowTerminatingError($_)
                                }
                                catch
                                {
                                    $PSCmdlet.ThrowTerminatingError($_)
                                }

                                forEach ($LogEntry in $LogContents)
                                {
                                    Write-Debug -Message "Processing log entry: '$LogEntry'"
                                    try
                                    {
                                        Write-Debug -Message "Processing message property."
                                        [string] $Message = ($LogEntry | Select-String -Pattern "\[LOG\[((.| )+)\]LOG\]").Matches.Value
                                        # Identifies the message sections of each line using the [LOG[] tags and removes them for us.
                                        if ($Log -like "AppIntentEval")
                                        {
                                            Write-Debug -Message "Parsing AppIntentEval message."
                                            $Message = $Message -replace ":- |, ", "`n"
                                            # Lazy reformatting to key:value statements instead of in a line.
                                        }
                                        $Message = ($Message -replace "\[LOG\[|\]LOG\]").Trim()
                                    }
                                    catch
                                    {
                                        $PSCmdlet.ThrowTerminatingError($_)
                                    }

                                    if (-not ($Message))
                                    {
                                        Write-Verbose -Message "Unable to read blank message - skipping to next."
                                        Continue
                                    }

                                    try
                                    {
                                        Write-Debug -Message "Processing metadata block."
                                        [string] $Metadata = ((($LogEntry | Select-String -Pattern "<time((.| )+)>").Matches.Value -replace "<|>") -split " ")
                                        # Identifies and isolates the metadata section
                                        # Includes the time and date which we want, but also other entries which we'll need to get rid of.
                                    }
                                    catch
                                    {
                                        $PSCmdlet.ThrowTerminatingError($_)
                                    }


                                    try
                                    {
                                        Write-Debug -Message "Processing TimeStub block."
                                        [string] $TimeStub = ((($Metadata -split " ")[0] -replace 'time|=|"') -split "\.")[0]
                                        # To find the time, we remove 'time', '=', and '"'
                                        # Split the remainder in two based on '.' and keep the first part.
                                        # This is a bit awkward but the casting is tricky without the split.
                                    }
                                    catch
                                    {
                                        $PSCmdlet.ThrowTerminatingError($_)
                                    }

                                    try
                                    {
                                        Write-Debug -Message "Processing DateStub block."
                                        [string] $DateStub = ((($Metadata -split " ")[1] -replace 'date|=|"'))
                                        # Finding the date is similar but simpler.
                                        # We only need to remove 'date', '=', and '"'.
                                    }
                                    catch
                                    {
                                        $PSCmdlet.ThrowTerminatingError($_)
                                    }


                                    try
                                    {
                                        Write-Debug -Message "Generating timestamp."
                                        [datetime] $TimeStamp = "$DateStub $TimeStub"
                                        # At the end we add the two stubs together, and cast them as a [datetime] object
                                    }
                                    catch
                                    {
                                        $PSCmdlet.ThrowTerminatingError($_)
                                    }

                                    [PSCustomObject]@{
                                        ComputerName = $Computer
                                        Source       = $Log
                                        Timestamp    = $TimeStamp
                                        Message      = $Message
                                        Path         = $LogPath
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    END
    {
    }
}

# PSGubbins\Source\Functions\Get-Cpl.ps1
function Get-Cpl
{
    <#
    .SYNOPSIS
        Finds any .cpl files in a given location.
    .DESCRIPTION
        Performs a recursive search to find all unique .cpl files in a given location, and return the name and the associated file description.
    .PARAMETER Path
        The path which will be searched. By default this is 'C:\Windows\System32'
    .EXAMPLE
        Get-Cpl -Path "C:\"
 
        Name Description
        ---- -----------
        appwiz.cpl Shell Application Manager
        bthprops.cpl Bluetooth Control Panel Applet
        CmiCnfgp.cpl ConfigPanel DLL
        desk.cpl Desktop Settings Control Panel
        Firewall.cpl Windows Defender Firewall Control Panel DLL Launching Stub
        FlashPlayerCPLApp.cpl
        hdwwiz.cpl Add Hardware Control Panel Applet
        igfxcpl.cpl igfxcpl Module
        inetcpl.cpl
        intl.cpl Control Panel DLL
        irprops.cpl Infrared Control Panel Applet
        joy.cpl Game Controllers Control Panel Applet
        main.cpl Mouse and Keyboard Control Panel Applets
        MLCFG32.CPL Microsoft Mail Configuration Library
        mmsys.cpl Audio Control Panel
        ncpa.cpl Network Connections Control-Panel Stub
        powercfg.cpl Power Management Configuration Control Panel Applet
        sapi.cpl Speech UX Control Panel
        sysdm.cpl
        TabletPC.cpl Tablet PC Control Panel
        telephon.cpl Telephony Control Panel
        timedate.cpl Time Date Control Panel Applet
        wscui.cpl Security and Maintenance
    .INPUTS
        String
    .OUTPUTS
        PSCUstomObject
    #>

    [CmdletBinding()]
    [Alias("Get-ControlPanelApplet")]
    [OutputType([PSCustomObject])]
    param
    (
        [Parameter(Mandatory = $False, Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True, HelpMessage = "The file path to search for CPL files.")]
        [ValidateScript( {
                if (Test-Path $_)
                {
                    $True
                }
                else
                {
                    throw "The specified location could not be found."
                }
            } )]
        [alias("FullName", "FilePath")]
        [string] $Path = "C:\Windows\System32"
    )

    try
    {
        if (Test-Path -Path $Path)
        {
            $SearchSplat = @{
                Path        = $Path
                Include     = "*.cpl"
                Recurse     = $True
                Force       = $True
                ErrorAction = "SilentlyContinue"
            }

            $Files = Get-ChildItem @SearchSplat | Sort-Object -Property "Name" -Unique

            forEach ($File in $Files)
            {
                [PSCustomObject]@{
                    Name        = $File.Name
                    Description = $File.VersionInfo.FileDescription
                    Path        = $File.FullName
                }
            }
        }
        else
        {
            Write-Warning -Message "The provided path is inaccessible ($Path)"
        }
    }
    catch
    {
        $PSCmdlet.ThrowTerminatingError($_)
    }
}

# PSGubbins\Source\Functions\Get-Msc.ps1
function Get-Msc
{
    <#
    .SYNOPSIS
        Finds any .msc files (MMC snap-ins) in a specified location.
    .DESCRIPTION
        Performs a recursive search to find all unique .msc files in a given location, and return the name and the associated file description.
    .PARAMETER Path
        The path to be searched for snap-in files.
    .EXAMPLE
        Get-Msc -Path "C:\"
    .INPUTS
        String
    .OUTPUTS
        PSCUstomObject
    #>

    [CmdletBinding()]
    [Alias("Get-MMCSnapIn")]
    [OutputType([PSCustomObject])]
    param
    (
        [Parameter(Mandatory = $False, Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True, HelpMessage = "The file path to search for CPL files.")]
        [ValidateScript( {
                if (Test-Path $_)
                {
                    $True
                }
                else
                {
                    throw "The specified location could not be found."
                }
            } )]
        [alias("FullName", "FilePath")]
        [string] $Path = "C:\Windows\System32"
    )

    try
    {
        if (Test-Path -Path $Path)
        {
            $SearchSplat = @{
                Path        = $Path
                Include     = "*.msc"
                Recurse     = $True
                ErrorAction = "SilentlyContinue"
            }

            $Files = Get-ChildItem @SearchSplat | Sort-Object -Property "Name" -Unique

            forEach ($File in $Files)
            {
                [PSCustomObject]@{
                    Name = $File.Name
                    Path = $File.FullName
                }
            }
        }
        else
        {
            Write-Warning -Message "The provided path is inaccessible ($Path)"
        }
    }
    catch
    {
        $PSCmdlet.ThrowTerminatingError($_)
    }
}

# PSGubbins\Source\Functions\New-Password.ps1
function New-Password
{
    <#
    .SYNOPSIS
        Creates one or more password/passphrase from custom or pre-defined lists of words.
    .DESCRIPTION
        Creates one or more password/passphrase from custom or pre-defined lists of words, with the option of specifying minimum or maximum lengths.
    .PARAMETER Count
        The number of passwords to generate.
    .PARAMETER Long
        Determines whether longer passwords will be generated by using a third list of words.
    .PARAMETER MinimumLength
        Determines the minimum accepted length of the generated password(s).
    .PARAMETER MaximumLength
        Determines the maximum accepted length of the generated password(s).
    .PARAMETER FirstDictionary
        The first group of words to be used when generating the password.
    .PARAMETER SecondDictionary
        The second group of words to be used when generating the password.
    .PARAMETER ExtraDictionary
        The third group of words to be used when generating the password.
    .EXAMPLE
        New-Password
 
        Generates a new password.
    .EXAMPLE
        New-Password -Count 10 -Long
 
        Generates 10 long passwords.
    .EXAMPLE
        New-Password -Count 10 -MinimumLength 16
 
        Generates 10 passwords at least 16 characters long.
    .INPUTS
        String, Integer
    .OUTPUTS
        String
    #>

    [CmdletBinding(ConfirmImpact = "Low", SupportsShouldProcess = $False)]
    [Alias("New-Passphrase")]
    [OutputType([String])]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions",
        "",
        Justification = "This does generate something new but it does not change state.")]
    param
    (
        [Parameter(Mandatory = $False, Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True, HelpMessage = "The number of passwords to generate")]
        [Alias('Number')]
        [int] $Count = 1,

        [Parameter(Mandatory = $False, HelpMessage = "Determines the complexity of the password")]
        [Alias('Extended', 'Complex')]
        [switch] $Long = $False,

        [Parameter(Mandatory = $False, HelpMessage = "Determines the minimum accepted length of the password")]
        [int] $MinimumLength = $Null,

        [Parameter(Mandatory = $False, HelpMessage = "Determines the maximum accepted length of the password")]
        [int] $MaximumLength = $Null,

        [Parameter(Mandatory = $False, HelpMessage = "The first group of words to be used when generating the password - adjectives by default")]
        [string[]] $FirstDictionary = @("Angry", "Annoying", "Awesome", "Basic", "Bitter", "Clever", "Correct",
            "Divine", "Enraged", "Fast", "Fat", "Great", "Guilty", "Handy", "Huge", "Intelligent", "Irritated",
            "Just", "Massive", "Moral", "Morose", "Psyched", "Rapid", "Resourceful", "Righteous", "Sad", "Scary",
            "Sinful", "Smart", "Strong", "Tiny", "Upright", "Vengeful", "Vexed", "Violent", "Wrathful"),

        [Parameter(Mandatory = $False, HelpMessage = "The second group of words to be used when generating the password - animal names by default")]
        [string[]] $SecondDictionary = @("Alligators", "Apes", "Baboons", "Badgers", "Beavers", "Cats",
            "Chipmunks", "Coyotes", "Crocodiles", "Dinsoaurs", "Dogs", "Dolphins", "Ducks", "Ferrets", "Fish",
            "Fleas", "Foxes", "Geese", "Giraffes", "Hares", "Honey Badgers", "Horses", "Leopards", "Lions",
            "Meerkats", "Mice", "Monkeys", "Rabbits", "Raccoons", "Rats", "Sharks", "Squirrels", "Swans",
            "Tigers", "Tortoises", "Turtles", "Weasels", "Wolves", "Zebras"),

        [Parameter(Mandatory = $False, HelpMessage = "The third group of words to be used when generating the password - modifiers by default")]
        [string[]] $ExtraDictionary = @("Abhorrently", "Abnormally", "Absurdly", "Acceptably", "Accordingly",
            "Adorably", "Amazingly", "Artfully", "Creatively", "Extremely", "Incredibly", "Infinitely", "Mad",
            "Moderately", "Particularly", "Pleasingly", "Proper", "Really", "Reasonably", "Somewhat",
            "Strikingly", "Sufficiently", "Super", "Supremely", "Totally", "Tremendously", "Truly", "Very",
            "Well", "Wicked")
    )

    BEGIN
    {
        if ($PSBoundParameters.ContainsKey("Debug"))
        {
            $DebugPreference = "Continue"
        }
        Write-Debug -Message "BEGIN Block"

        if ($Long)
        {
            Write-Verbose -Message "The 'Long' parameter has been used. All dictionaries will be used when generating the password(s)."
        }
        if ($MinimumLength)
        {
            Write-Verbose -Message "The minimum allowed length has been set as $MinimumLength."
        }
        if ($MaximumLength)
        {
            Write-Verbose -Message "The maximum allowed length has been set as $MaximumLength."
        }
    }

    PROCESS
    {
        Write-Debug -Message "PROCESS Block"
        for ($i = 0; $i -lt $Count)
        {
            $Random = (Get-Random -Minimum 1 -Maximum 10000)
            # Define the separator to appear between words.
            if ($Random % 2 -eq 0)
            {
                [string] $Separator = "-"
            }
            else
            {
                [string] $Separator = "_"
            }

            if (($Long -eq $True) -or ($Random -ge 4999))
            {
                $RequiredDictionaries = @($ExtraDictionary, $FirstDictionary, $SecondDictionary)
                Write-Verbose -Message "Creating 'long' password."
            }
            else
            {
                $RequiredDictionaries = @($FirstDictionary, $SecondDictionary)
            }

            # Start generating the actual password.
            try
            {
                $PassPhrase = $Null
                Write-Debug -Message "Generating number to start password."
                [string] $PassPhrase = Get-Random -Minimum 2 -Maximum 999 -ErrorAction "Stop"
                [string] $Password = $PassPhrase + $Separator
            }
            catch
            {
                $PSCmdlet.ThrowTerminatingError($_)
            }

            # Start adding words!
            forEach ($Dictionary in $RequiredDictionaries)
            {
                $PassPhrase = $Null
                try
                {
                    Write-Verbose -Message "Retrieving word."
                    $DictionaryLength = ($Dictionary).Length
                    $PassPhrase = $Dictionary[(Get-Random -Minimum 0 -Maximum $DictionaryLength -ErrorAction "Stop")]

                    Write-Debug -Message "Determining case."
                    [string] $PassPhrase = if ((Get-Random -Minimum 0 -Maximum 10) -ge 5)
                    {
                        $PassPhrase.ToUpper()
                    }
                    else
                    {
                        $PassPhrase.ToLower()
                    }

                    Write-Debug -Message "Adding word ($PassPhrase) to password."
                    [string] $Password += $PassPhrase + $Separator
                }
                catch
                {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
            }

            if ($Null -ne $Password)
            {
                Write-Debug -Message "Cleaning up final separator."
                $Password = $Password -replace "$Separator$", ""
                # Removes final 'separator' added to the end of the password.

                if ((-not ($MinimumLength)) -and (-not ($MaximumLength)))
                {
                    # If we're not checking the length, tick over the counter and output the password.
                    Write-Debug -Message "No length validation required."
                    $i ++
                    $Password
                }
                else
                {
                    if (($MinimumLength) -and
                        (-not ($MaximumLength)))
                    {
                        Write-Debug -Message "Validating against minimum length only."
                        if ($Password.Length -ge $MinimumLength)
                        {
                            Write-Verbose -Message "The password is $($Password.Length) characters long."
                            $i ++
                            $Password
                        }
                    }
                    elseif (($MaximumLength) -and
                        (-not ($MinimumLength)))
                    {
                        Write-Debug -Message "Validating against maximum length only."
                        if ($Password.Length -le $MaximumLength)
                        {
                            Write-Verbose -Message "The password is $($Password.Length) characters long."
                            $i ++
                            $Password
                        }
                    }
                    elseif (($MinimumLength) -and
                        ($MaximumLength))
                    {
                        Write-Debug -Message "Validating against minimum and maximum length."
                        if (($Password.Length -ge $MinimumLength) -and
                            ($Password.Length -le $MaximumLength))
                        {
                            Write-Verbose -Message "The password is $($Password.Length) characters long."
                            $i ++
                            $Password
                        }
                    }
                }
            }
            else
            {
                Write-Warning -Message "No password was generated - that shouldn't have happened!"
            }
        }
    }

    END
    {
        Write-Debug -Message "END Block"
    }
}

# PSGubbins\Source\Functions\New-RDPSession.ps1
function New-RdpSession
{
    <#
    .SYNOPSIS
        Launches a Remote Desktop connection to specified computer(s).
    .DESCRIPTION
        Uses mstsc.exe to launch a Remote Desktop connection to specified computer(s) subject a successful ping.
    .PARAMETER ComputerName
        The computer(s) to be connected to.
    .PARAMETER PassThru
        Whether or not an object is output to represent each session.
    .PARAMETER SkipPing
        Dictates that a successful ping should not be required to attempt connection.
    .EXAMPLE
        rdp -ComputerName "dc01", "file01"
    .EXAMPLE
        rdp dc01
    .EXAMPLE
        rdp file01 -SkipTest
    .EXAMPLE
        rdp 10.0.10.50
    .INPUTS
        Strings
    .OUTPUTS
        PSCustomObject (optional)
    #>

    [CmdletBinding(ConfirmImpact = "Low", SupportsShouldProcess = $True)]
    [OutputType("PSCustomObject")]
    [Alias("rdp")]
    param
    (
        [Parameter(Mandatory = $True, Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True, HelpMessage = "The computer(s) to RDP to")]
        [string[]] $ComputerName,

        [Parameter(Mandatory = $False, Position = 1, HelpMessage = "Whether or not an object is output to represent each session.")]
        [switch] $PassThru,

        [Parameter(Mandatory = $False, Position = 2, HelpMessage = "Dictates that a successful ping should not be required to attempt connection.")]
        [switch] $SkipPing
    )

    BEGIN
    {
        if ($PSBoundParameters.ContainsKey("Debug"))
        {
            $DebugPreference = "Continue"
        }
        Write-Debug -Message "BEGIN Block"
    }

    PROCESS
    {
        Write-Debug -Message "PROCESS Block"
        forEach ($Computer in $ComputerName)
        {
            Write-Verbose -Message "Pinging '$Computer'"
            $PingTest = $Null
            $PingTest = Test-Connection -ComputerName $Computer -Count 2 -ErrorAction "SilentlyContinue"

            if ($PingTest)
            {
                if ($PSCmdlet.ShouldProcess($Computer, "Create new RDP session"))
                {
                    $RdpSplat = @{
                        FilePath     = "mstsc.exe"
                        ArgumentList = "/v:$Computer"
                        PassThru     = $True
                        ErrorAction  = "Stop"
                    }

                    try
                    {
                        $Process = Start-Process @RdpSplat
                        if ($PassThru)
                        {
                            [PSCustomObject]@{
                                ComputerName = $Computer
                                IPv4Address  = $PingTest.IPv4Address | Sort-Object -Unique
                                PID          = $Process.Id
                            }
                        }
                    }
                    catch
                    {
                        Write-Warning -Message "Problems starting RDP session with '$Computer'"
                    }
                }
            }
            else
            {
                Write-Verbose -Message "Ping to '$Computer' failed. Skipping RDP connection."
            }
        }
    }

    END
    {
        Write-Debug -Message "END Block"
    }
}