HelpDesk.psm1

#Region $HelpDeskADComputernameArgCompleter
$HelpDeskADComputernameArgCompleter = {
    param ($CommandName, $ParameterName, $StringMatch)
    if ($null -eq $StringMatch) {
        $Filter = "*"
    }
    else {
        $Filter = "*$StringMatch*"
    }
    (Get-ADComputer -filter { Name -like $Filter }).Name
}
Register-ArgumentCompleter -CommandName Get-Computer, Add-LocalAdmin, Remove-LocalAdmin, Get-LocalAdmin, Get-DellTags, Get-UserProfile, Remove-UserProfile -ParameterName ComputerName -ScriptBlock $HelpDeskADComputernameArgCompleter
#EndRegion $HelpDeskADComputernameArgCompleter

#Region $HelpDeskADFirstNameArgCompleter
$HelpDeskADFirstNameArgCompleter = {
    param ($CommandName, $ParameterName, $StringMatch)
    if ($null -eq $StringMatch) {
        $Filter = "*"
    }
    else {
        $Filter = "*$StringMatch*"
    }
    $GivenNames = (Get-ADUser -Filter { GivenName -like $Filter }).GivenName
    $QuotedNames = foreach ($Name in $GivenNames) {
        if ($Name.Contains(" ")) {
            "`"$Name`""
        }
        else {
            $Name
        }
    }
    return $QuotedNames
}
Register-ArgumentCompleter -CommandName Get-UsersWithFirstName -ParameterName FirstName -ScriptBlock $HelpDeskADFirstNameArgCompleter
#EndRegion $HelpDeskADFirstNameArgCompleter
#Region $HelpDeskADLastNameArgCompleter
$HelpDeskADLastNameArgCompleter = {
    param ($CommandName, $ParameterName, $StringMatch)
    if ($null -eq $StringMatch) {
        $Filter = "*"
    }
    else {
        $Filter = "*$StringMatch*"
    }
    $SurNames = (Get-ADUser -Filter { SurName -like $Filter }).SurName
    $QuotedNames = foreach ($Name in $SurNames) {
        if ($Name.Contains(" ")) {
            "`"$Name`""
        }
        else {
            $Name
        }
    }
    return $QuotedNames
}
Register-ArgumentCompleter -CommandName Get-UsersWithLastName -ParameterName LastName -ScriptBlock $HelpDeskADLastNameArgCompleter
#EndRegion $HelpDeskADLastNameArgCompleter
#Region $HelpDeskADUsernameArgCompleter
$HelpDeskADUsernameArgCompleter = {
    param ($CommandName, $ParameterName, $StringMatch)
    if ($null -eq $StringMatch) {
        $Filter = "*"
    }
    else {
        $Filter = "*$StringMatch*"
    }
    $Users = (Get-ADUser -Filter { SamAccountName -like $Filter }).SamAccountName
    $QuotedUsers = foreach ($User in $Users) {
        if ($User[0] -eq "-") {
            "`"$User`""
        }
        else {
            $User
        }
    }
    return $QuotedUsers
}
Register-ArgumentCompleter -CommandName Get-User, Get-OtherADUserAccounts, Reset-Password, Add-LocalAdmin, Remove-LocalAdmin, Get-LocalAdmin, Get-ADGroupsManagedByUser -ParameterName Username -ScriptBlock $HelpDeskADUsernameArgCompleter
#EndRegion $HelpDeskADUsernameArgCompleter
#Region ConvertExpressServiceCodeTo-ServiceTag
Function ConvertExpressServiceCodeTo-ServiceTag() {    
    [CmdletBinding()]
    param ([parameter(valuefrompipeline=$true, HelpMessage="Integer number to convert")][int64]$ExpressServiceCode="")
    $alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    do {
        $remainder = ($ExpressServiceCode % 36)
        $char = $alphabet.substring($remainder,1)
        $ServiceTag = "$char$ServiceTag"
        $ExpressServiceCode = ($ExpressServiceCode - $remainder) / 36
    }
    while ($ExpressServiceCode -gt 0)

    $ServiceTag
}
#EndRegion ConvertExpressServiceCodeTo-ServiceTag
#Region ConvertServiceTagTo-ExpressServiceCode
Function ConvertServiceTagTo-ExpressServiceCode{ 
    [CmdletBinding(DefaultParameterSetName='ServiceTag')]
    param( 
        [Parameter(Mandatory,Position=0,ValueFromPipeline,ValueFromPipelineByPropertyName,ParameterSetName="ServiceTag")]
        [System.String] $ServiceTag
    ) 
        
    begin{}
    
    process{
        try {
            $Range = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
            $ServiceTag_CharacterArray = $ServiceTag.ToUpper().ToCharArray()
            [System.Array]::Reverse($ServiceTag_CharacterArray)
            [System.Int64]$ExpressServiceCode=0
    
            $i = 0
            foreach($Character in $ServiceTag_CharacterArray) {
                $ExpressServiceCode += $Range.IndexOf($Character) * [System.Int64][System.Math]::Pow(36,$i)
                $i+=1
            }

            $ExpressServiceCode
        } catch {
            Write-Error "$($_.Exception.Message)"
        }
    }

    end{}
}
#EndRegion ConvertServiceTagTo-ExpressServiceCode\
#Region Get-PlainText

<#
.SYNOPSIS
    Retrieve the Plain text string from a SecureString object.

.NOTES
    Author: Matthew J. DeGarmo
    GitHub: https://github.com/matthewjdegarmo
#>

Function Get-PlainText() {
    [CmdletBinding()]
    param
    (
        [parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [System.Security.SecureString[]]$SecureString
    )
    begin { }
    process {
        foreach ($String in $SecureString) {
            $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($String);

            try {
                [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr);
            }
            finally {
                [Runtime.InteropServices.Marshal]::FreeBSTR($bstr);
            }
        }
    }
    end { }
}
#EndRegion Get-PlainText
#Region Set-Password

<#
.SYNOPSIS
    Reset domain password for an ActiveDirectory user.

.DESCRIPTION
    Sets a Active Directory Password and provides an option to require a password change on login.

.EXAMPLE
    Specifies the username being set and then prompts for a password.

    Set-Password -username domain\username

.NOTES
    Author: Matthew J. DeGarmo
    GitHub: https://github.com/matthewjdegarmo
#>

Function Set-Password {
    [CmdletBinding()]

    Param (
        [Parameter(Mandatory = $true)]$Username,
        [Parameter()][SecureString]$Password = (Read-Host -Prompt "Password" -AsSecureString),
        [Switch]$RequireReset
    )

    Set-ADAccountPassword -Identity $username -Reset -NewPassword $Password

    if ($RequireReset.IsPresent) {
        Set-ADUser -Identity $Username -ChangePasswordAtLogon $true
    }
}
#EndRegion Set-Password
#Region Add-LocalAdmin

<#
.SYNOPSIS
    Adds user to local admin group.

.DESCRIPTION
    This function adds a local admin to the computer or server it is run from.

.PARAMETER Username
    Specify the SAMAccountName of the user to add.

.PARAMETER ComputerName
    Specify the remote computer to run against.

.PARAMETER Domain
    Specify the domain that the user belongs to.

.EXAMPLE
    PS> Add-LocalAdmin -Username username

    Description
    -----------
    Adds specified domain user to the local administrators group

.EXAMPLE
    PS> Add-LocalAdmin -ComputerName Some-Remote-Computer -Username username

    Description
    -----------
    This will attempt to add the specified user to the local admin group of the specified remote computer. You must be an admin on the remote computer for this to work.

.NOTES
    Author: Matthew J. DeGarmo
    GitHub: https://github.com/matthewjdegarmo

    You can either submit a [PR](https://github.com/matthewjdegarmo/HelpDesk/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/HelpDesk/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/HelpDesk

    Azure AD Joined machines will require the user to first login to a computer with their domain account before adding their domain account as a local admin.
    The user logging in registers their SID so that the command is successful.
#>

Function Add-LocalAdmin() {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory, Position = 0)]
        [string] $Username,

        [Parameter(Position = 1)]
        [string] $ComputerName = $env:COMPUTERNAME,

        [Parameter()]
        [string] $Domain = $env:USERDOMAIN
    )

    begin {
        function Get-UsernameDomainFormat($Username, $Domain) {
            return ("$Domain\$Username")
        }
    }

    process {
        try {
            $FullUsername = Get-UsernameDomainFormat $Username $Domain
            if ($PSBoundParameters.ContainsKey('ComputerName')) {
                Invoke-Command -ComputerName $ComputerName -ScriptBlock { Add-LocalGroupMember -Group 'Administrators' -Member $using:FullUsername }
                #Invoke-Command -ComputerName $ComputerName -ScriptBlock { net.exe Localgroup Administrators $using:FullUsername /add }
            }
            else {
                Add-LocalGroupMember -Group 'Administrators' -Member $FullUsername
                #net.exe Localgroup Administrators $FullUsername /add
            }
        }
        catch {
            Write-Error "$($_.Exception.Message)"
        }
    }

    end {}
}
#EndRegion Add-LocalAdmin
#Region Aliases
Set-Alias FName FirstName
Set-Alias FirstName Get-UsersWithFirstName
Set-Alias LName LastName
Set-Alias LastName Get-UsersWithLastName
Set-Alias GLO Get-LockedOutADUsers
Set-Alias LU Lookup
Set-Alias Lookup Get-User
Set-Alias UL Unlock-LockedOutADUsers
Set-Alias PC PCLookup
Set-Alias PCLookup Get-Computer
Set-Alias RPW Reset-Password
#EndRegion Aliases
#Region Disable-Account

<#
.SYNOPSIS
    Disable an enabled AD Account.
    
.DESCRIPTION
    Disables a specified Active Directory Account

.PARAMETER Username
    Specify the SAMAccountName or DistinguishedName of the user to disable.

.EXAMPLE
    PS> Disable-ADAccount -Username JohnDoe
    
    Description
    -----------
    Use the Samaccountname of the account being disabled
    
.EXAMPLE
    PS> Disable-ADAccount -Username "CN=Matt Degar,OU=AI,OU=UserAccounts,DC=FAKE,DC=COM"
    
    Description
    -----------
    Use the DistinguishedName of the account being disabled
    
.EXAMPLE
    PS> Disable-ADAccount -Username JohnD@Company.com
    
    Description
    -----------
    Use the UserPrincipalName of the account being disabled
    
.NOTES
    Author: Matthew J. DeGarmo
    GitHub: https://github.com/matthewjdegarmo

    You can either submit a [PR](https://github.com/matthewjdegarmo/HelpDesk/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/HelpDesk/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/HelpDesk
#>

Function Disable-Account() {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]$Username
    )

    Disable-ADAccount -Identity $Username -Confirm
}
#EndRegion Disable-Account
#Region Enable-Account

<#
.SYNOPSIS
    Quickly enable a disabled ActiveDirectory user account

.DESCRIPTION
    Enables a specified Active Directory Account

.PARAMETER Username
    Specify the SAMAccountName or DistinguishedName of the user to enable.

.EXAMPLE
    PS> Enable-ADAccount -Username JohnD

    Description
    -----------
    Use the Samaccountname of the account being disabled

.EXAMPLE
    PS> Enable-ADAccount -Username "CN=Matt Degar,OU=AI,OU=UserAccounts,DC=FAKE,DC=COM"

    Description
    -----------
    Use the DistinguishedName of the account being disabled

.EXAMPLE
    PS> Enable-ADAccount -Username PJohnD@Company.com

    Description
    -----------
    Use the UserPrincipalName of the account being disabled

.NOTES
    Author: Matthew J. DeGarmo
    GitHub: https://github.com/matthewjdegarmo

    You can either submit a [PR](https://github.com/matthewjdegarmo/HelpDesk/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/HelpDesk/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/HelpDesk
#>

Function Enable-Account() {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory)]
        [string] $Username
    )

    Enable-ADAccount -Identity $Username -Confirm
}
#EndRegion Enable-Account
#Region Get-ADGroupsManagedByUser

<#
.SYNOPSIS
    Generate a list of group names owned by a specified.
.DESCRIPTION
    This will look up groups owned by the specified user.
.PARAMETER Username
    This is the Identity used by `Get-ADUser` to look up the Distinguished name used by `Get-ADGroup`.
.EXAMPLE
    PS>Get-ADGroupsManagedByUser mjdegar

    Description
    -----------
    This will generate all groups (Security or Distribution) Managed by `mjdegar`.
.NOTES
    Author: Matthew J. DeGarmo
    Handle: @matthewjdegarmo

    You can either submit a [PR](https://github.com/matthewjdegarmo/HelpDesk/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/HelpDesk/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/HelpDesk
#>

Function Get-ADGroupsManagedByUser() {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, Position = 0)]
        [string] $Username
    )
    try {
        $User_DN = (Get-ADUser -Identity $Username).DistinguishedName
        Get-ADGroup -Properties ManagedBy -Filter * | `
            Where-Object { $_.ManagedBy -eq $User_DN } | `
            Select-Object Name
    }
    catch {
        Write-Error "$($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)"
    }
}
#EndRegion Get-ADGroupsManagedByUser
#Region Get-Computer

<#
.SYNOPSIS
    This function performs a quick lookup of an ActiveDirectory computer.
.DESCRIPTION
    This function is a simple Get-ADComputer with certain fields to display.
.PARAMETER ComputerName
    This parameter should specify the ActiveDirectory ComputerName. This parameter can accept multiple values, separated by commas.
.PARAMETER FilePath
    This parameter should specify a .CSV or a .TXT file containing only computername values.

    NOTE: The CSV must have a value title at the top, aka the first line should say ComputerName or something similar.

        CSV files - first row should be column headers
            - The first row should be ComputerName
            EXAMPLE: ComputerName
                        computer1
                        computer2

        TXT files - just input data, no headers necessary.
            EXAMPLE: computer1
                        computer2
.INPUTS
    System.String[]
        This function does not accept pipeline data. The values for all parameters must be specified.
.OUTPUTS
    None
        This function does not produce output except for write-host data
.EXAMPLE
    PS>Get-Computer -ComputerName Computer1

    Name : Computer1
    Description : Some description of this computer
    OperatingSystem : Windows 10 Enterprise
    CanonicalName : some.domain.com/ou/path/to/computer

    Description
    -----------
    This will display information about the computer.
.EXAMPLE
    PS>PCLookup -ComputerName Fake-Computer
    PCLookup : The PC named 'Fake-Computer' does not exist.
    At line:1 char:1
    + PCLookup -ComputerName Fake-Computer
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        + CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
        + FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,PCLookup

    Description
    -----------
    This will write an error to the screen that 'Fake-Computer' is not a computer that exists in ActiveDirectory. This also shows the usage of the alias `PCLookup` which points to `Get-Computer`.
.EXAMPLE
    PS>PC -ComputerName Computer1,Fake-Computer,Computer2

    Name : Computer1
    Description : Some description of this computer
    OperatingSystem : Windows 10 Enterprise
    CanonicalName : some.domain.com/ou/path/to/computer

    PCLookup : The PC named 'Fake-Computer' does not exist.
    At line:1 char:1
    + PCLookup -ComputerName Computer1,Fake-Computer,Computer2
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        + CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
        + FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,PCLookup

    Name : Computer2
    Description : Some description of this computer
    OperatingSystem : Windows 10 Enterprise
    CanonicalName : some.domain.com/ou/path/to/computer

    Description
    -----------
    This will generate results for all three specified ComputerName's, and will generate an error in place of the Fake-Computer, and continue generating results. This also shows the usage of the alias `PC`, which points to the alias `PCLookup`, which points to the command `Get-Computer`.
.EXAMPLE
    PS>Get-Computer -FilePath .\Computers.txt
.NOTES
    Author: Matthew J. DeGarmo
    GitHub: https://github.com/matthewjdegarmo

    You can either submit a [PR](https://github.com/matthewjdegarmo/HelpDesk/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/HelpDesk/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/HelpDesk
    
    Change Log:

    Version: 2.0 - Added the FilePath parameter, and loads error-checking to detect if computername is used or filepath used without having to specify -Computername or -FilePath
                    Example: You can use PCLookup test-pc AND PCLookup C:\temp\file.txt AND PCLookup C:\temp\nonexistentfile.csv
                    This script will now generate results, OR display the correct error message for all these scenarios.
    Version: 1.0 - Function Creation.
#>

Function Get-Computer() {
    [CmdletBinding(DefaultParameterSetName = "Named")]
    param(
        [parameter(Mandatory = $true, Position = 0, ParameterSetName = "Named", ValueFromPipelineByPropertyName = $true, ValueFromPipeline = $true)]
        [ALias("Name")]
        [string[]]$ComputerName,
        
        [parameter(Mandatory = $true, Position = 0, ParameterSetName = "File")]
        [string]$FilePath
    )
    begin {
        if ($ComputerName.Count -le 1) {
            if (([System.IO.File]::Exists($ComputerName)) -or (Test-Path $ComputerName)) {
                $FilePath = (Get-ChildItem $ComputerName).Fullname
                $Computername = $null
            }
            else {
                if (($ComputerName -match "\\") -and (-Not(Test-Path $ComputerName))) {
                    Write-Error "File $ComputerName does not exist. Please provide a correct file path for -FilePath."
                    break
                }
            }
        }

        if ($FilePath) {
            $ImportFileInfo = Get-Item $FilePath -ErrorAction SilentlyContinue
            $ImportFileData = Get-Content $FilePath -ErrorAction SilentlyContinue
            if (!$ImportFileData) {
                Write-Error "$FilePath is null or empty."
                break
            }

            switch ($ImportFileInfo.Extension) {
                '.csv' {
                    $FileData = Import-Csv $FilePath
                }
                '.txt' {
                    $FileData = $ImportFileData
                } #End '.txt'
                Default {
                    Write-Error "Please use an approved file format for the -FilePath parameter."
                    break
                }
            }# End switch ($ImportFile.Extension)
            $ComputerName = $FileData
        }
    }

    process {
        try {
            foreach ($Computer in $ComputerName) {
                try {
                    Get-ADComputer $Computer -Property "Name", "Description", "OperatingSystem", "OperatingSystemVersion", "CanonicalName" -ErrorAction Stop | Select-Object Name, Description, OperatingSystem, OperatingSystemVersion, CanonicalName
                }
                catch {
                    try {
                        $regex = "*$Computer*"
                        $Filter = "Name -like `"$regex`""
                        Get-ADComputer -Filter $Filter -Properties "Name", "Description", "OperatingSystem", "OperatingSystemVersion", "CanonicalName" -ErrorAction Stop -OutVariable ComputerExists | Select-Object Name, Description, OperatingSystem, OperatingSystemVersion, CanonicalName
                        if (!$ComputerExists) { Throw }
                    }
                    catch { Write-Error "The PC named '$Computer' does not exist." }
                }
            }
        }
        catch {
            Write-Error "$($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)"
        }
    }
    end {}
}
#EndRegion Get-Computer
#Region Get-DaysUntil

<#
.SYNOPSIS
    Calculate the number of days between today and any future (or past) date.
.DESCRIPTION
    This function provides the quantity of days between today and any given date.
.PARAMETER Date
    Specifies a date.
    Enter the date in a format that is standard for the system locale.
    Example Date Formats:
        06/06/2020
        6/6
        june6
        june6/2020
        aug12
        october18
.EXAMPLE
    Let's say today is January 1st, 2020.

    PS> Get-DaysUntil 10/18
    291

    Description
    -----------
    This will generate the number of days between today (jan1/2020) and October 18th, 2020.
    See `Get-Help Get-DaysUntil -Parameter Date` for example date formats.
.EXAMPLE
    Let's say today is January 1st, 2020.

    PS> Get-DaysUntil 10/18/2000
    -7014

    Description
    -----------
    This will generate a negative number, for the number of days in the past the specified date is.
    See `Get-Help Get-DaysUntil -Parameter Date` for example date formats.
.NOTES
    Author: Matthew J. DeGarmo
    GitHub: https://github.com/matthewjdegarmo

    You can either submit a [PR](https://github.com/matthewjdegarmo/HelpDesk/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/HelpDesk/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/HelpDesk
#>

Function Get-DaysUntil() {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string] $Date
    )

    $TimeSpan = (New-TimeSpan -Start (Get-Date -Hour 0 -Minute 0 -Second 0) -End (Get-Date -Date $Date))
    if ([math]::Round($TimeSpan.Hours / 24) -eq 1) {
        if ($TimeSpan.Days -ge 0) {$Days = $TimeSpan.Days + 1}
        elseif ($TimeSpan.Days -lt 0) {$Days = $TimeSpan.Days - 1}
    } else {
        $Days = $TimeSpan.Days
    }

    $Days
}
#EndRegion Get-DaysUntil
#Region Get-DellTags

<#
.SYNOPSIS
    Gather Dell Tag information.
.DESCRIPTION
    Generate the ServiceTag and ExpressServiceCode of a dell computer.
.PARAMETER ServiceTag
    Providing the ServiceTag (SerialNumber) will generate the accompanying ExpressServiceCode.
.PARAMETER ExpressServiceCode
    Providing the ExpressServiceCode will generate the accompanying ServiceTag (SerialNumber).
.PARAMETER ComputerName
    Providing the ComputerName will attempt to generate the ServiceTag and ExpressServiceCode.
    The computer specified must be turned on, online, and your account must have remote access rights to it.
    If the Get-CimInstance query finds that the specified computer is not a Dell, your query will fail.
.EXAMPLE
    PS> Get-DellTags -ServiceTag M11P21T

    ServiceTag ExpressServiceCode
    ---------- ------------------
    M11P21T 47952526241

    Description
    -----------
    Providing the -ServiceTag will generate the ExpressServiceCode
.EXAMPLE
    PS> Get-DellTags -ExpressServiceCode 47952526241

    ServiceTag ExpressServiceCode
    ---------- ------------------
    M11P21T 47952526241

    Description
    -----------
    Providing the -ExpressServiceCode will generate the ServiceTag.
.EXAMPLE
    PS> Get-DellTags -ComputerName some-domain-computer

    ServiceTag ExpressServiceCode
    ---------- ------------------
    M11P21T 47952526241

    Description
    -----------
    Providing the -ComputerName will attempt to generate this information if the computer is online, and it is a Dell computer.
.NOTES
    Author: Matthew J. DeGarmo
    GitHub: https://github.com/matthewjdegarmo

    You can either submit a [PR](https://github.com/matthewjdegarmo/HelpDesk/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/HelpDesk/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/HelpDesk
#>

Function Get-DellTags() {
    [cmdletBinding(DefaultParameterSetName='ComputerName')]
    param (
        [Parameter(Mandatory,Position=0,ValueFromPipeline,ValueFromPipelineByPropertyName,ParameterSetName="ServiceTag")]
        [System.String] $ServiceTag,

        [Parameter(Mandatory,Position=0,ValueFromPipeline,ValueFromPipelineByPropertyName,ParameterSetName="ExpressServiceCode")]
        [System.Int64] $ExpressServiceCode,

        [Parameter(Mandatory,Position=0,ValueFromPipeline,ValueFromPipelineByPropertyName,ParameterSetName="ComputerName")]
        [System.String] $ComputerName
    )

    begin {}

    process {
        try {
            switch($PSBoundParameters.Keys) {
                'ServiceTag' {
                    $ExpressServiceCode = ConvertServiceTagTo-ExpressServiceCode -ServiceTag $ServiceTag
                }
                'ExpressServiceCode' {
                    $ServiceTag = ConvertExpressServiceCodeTo-ServiceTag -ExpressServiceCode $ExpressServiceCode
                }
                'ComputerName' {
                    $params = @{
                        ClassName = 'Win32_BIOS'
                        ComputerName = $ComputerName
                    }
                    $ComputerInfo = Get-CimInstance @params
                    if ($ComputerInfo.Manufacturer -like "*DELL*") {
                        $ServiceTag = $ComputerInfo.SerialNumber
                    } else {
                        Throw("Computer: $($ComputerName.ToUpper()) is not a Dell computer. Manufacturer is $($ComputerInfo.Manufacturer)")
                    }
                    $ExpressServiceCode = ConvertServiceTagTo-ExpressServiceCode -ServiceTag $ServiceTag
                }
                DEFAULT {}
            }

            [PSCustomObject]@{
                ServiceTag = $ServiceTag
                ExpressServiceCode = $ExpressServiceCode
            }
        } catch {
            Write-Error "$($_.Exception.Message)"
        }
    }

    end {}
}
#EndRegion Get-DellTags
#Region Get-LocalAdmin

<#
.SYNOPSIS
    Retrieves a list of users in the local Administrators group.

.DESCRIPTION
    This function identifies local admins from the computer or server it is run from.

.PARAMETER ComputerName
    Specify the remote computer to query.

.EXAMPLE
    PS> Get-LocalAdmin

    Description
    -----------
    Generates a list of local admins

.EXAMPLE
    PS> Get-LocalAdmin -ComputerName SomeRemoteComputer-PC

    Description
    -----------
    Generate a list of local admins on a remote PC.

.NOTES
    Author: Matthew J. DeGarmo
    GitHub: https://github.com/matthewjdegarmo

    You can either submit a [PR](https://github.com/matthewjdegarmo/HelpDesk/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/HelpDesk/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/HelpDesk
#>

#Requires -Assembly C:\Windows\system32\net.exe
Function Get-LocalAdmin() {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string] $ComputerName = $env:COMPUTERNAME
    )

    if ($PSBoundParameters.ContainsKey('ComputerName')) {
        $NetAdminObject = Invoke-Command -ComputerName $ComputerName -ScriptBlock { (net.exe localgroup Administrators) -split '\r?\n' }
    }
    else {
        $NetAdminObject = (net.exe localgroup Administrators) -split '\r?\n'
    }
    $NetAdminObject[6..(($NetAdminObject.count) - 3)]
}
#EndRegion Get-LocalAdmin
#Region Get-LockedOutADUsers

<#
.SYNOPSIS
    This function performs a search of users in ActiveDirectory who are currently locked out.
.DESCRIPTION
    This function is a simple Search-ADAccount -Lockedout to generate a list of users who are currently locked out. This function provides the Name, SAMAccountName, and LockoutTime for each user that is locked out.
.INPUTS
    System.String
        This function does not accept pipeline data. The values for all parameters must be specified.
.OUTPUTS
    None
        This function does not produce output except for write-host data
.EXAMPLE
    PS>Get-LockedOutADUsers

    Name SamAccountName LockoutTime
    ---- -------------- -----------
    DeGarmo, Matthew J. matthewjd 6/26/2019 13:32:15

    Description
    -----------
    This will show all users who are currently locked out.
.EXAMPLE
    PS>Get-LockedOutADUsers
    No users are currently locked out.

    Description
    -----------
    This will display the results of the query, in this case there were no results to display.
.NOTES
    Author: Matthew J. DeGarmo
    GitHub: https://github.com/matthewjdegarmo

    You can either submit a [PR](https://github.com/matthewjdegarmo/HelpDesk/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/HelpDesk/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/HelpDesk
#>

Function Get-LockedOutADUsers() {
    [CmdletBinding()]
    param ()

    $Users = Search-ADAccount -LockedOut | Foreach-Object { Get-ADUser $_.SamAccountName -Properties LockoutTime | Where-Object { $_.Name -ne 'Guest' } | Select-Object Name, SAMAccountName, LockoutTime | Sort-Object LockoutTime }
    foreach ($User in $Users) {
        $Date = [DateTime]$User.LockoutTime
        $User.LockoutTime = $Date.AddYears(1600).ToLocalTime()
    }
    $Users
}
#EndRegion Get-LockedOutADUsers
#Region Get-User

<#
.SYNOPSIS
    This function performs a quick lookup of an ActiveDirectory user, group membership, and possible computers belonging to that user.
.DESCRIPTION
    This function is a simple Get-ADUser with certain fields to display. This function also uses the user Surname against ActiveDirectory Computer Descriptions to generate a list of possible PC's used by the user. This assumes that the AD Computer Descriptions are being used and that full last names are used. This function also generates a list (if the parameter -Groups is used) of groups that a user is a member of using Get-ADPrincipalGroupMembership.
.PARAMETER Username
    This parameter should specify the ActiveDirectory account name or SamAccountName.
.PARAMETER Groups
    This is a switch that will have the function include a listing of user group membership.
.PARAMETER Computers
    Specifying this switch will check the desired user's last name (SurName) against all AD Computer Descriptions for a possible match.
    If your organization used the ManagedBy value to tie an AD Computer to an AD User, then you can change the script. I will look into how to do both in the future.
.INPUTS
    System.String
        This function does not accept pipeline data. The values for all parameters must be specified.
.OUTPUTS
    None
        This function does not produce output except for write-host data
.EXAMPLE
    PS>Get-User matthewjd

    DisplayName : DeGarmo, Matthew J.
    Manager : Elon Musk
    Department : AI Relations
    Title : AI Architect
    Office : Jupiter-K12-A
    Mail : jpytr_MailMe@AICorp.com
    OfficePhone : 90acedb3-afb1-4986-99de-378fa797fa58
    IPPhone : a66f0778
    SamAccountName : matthewjd
    Description : Head of AI Relations
    PassLastSet : DATE HERE

    Possible Computers...
    Name : Some-Computer
    Description : Matthew DeGarmo, AI Relations

    Description
    -----------
    This will display generic information about the user as well as attempt to locate AD Computers that may belong to them.
.EXAMPLE
    PS>Lookup -Username user1 -Groups

    Description
    -----------
    Additionally, this will add the group names from `Get-ADPrincipalGroupMembership` to the beginning of the output. This also shows the usage of the previous command `Lookup`. This is an alias.
.EXAMPLE
    PS>lu user1

    Description
    -----------
    This shows the usage of the alias `LU`, which points to the old command name `Lookup`, which now points to `Get-User`
.NOTES
    Author: Matthew J. DeGarmo
    GitHub: https://github.com/matthewjdegarmo

    You can either submit a [PR](https://github.com/matthewjdegarmo/HelpDesk/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/HelpDesk/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/HelpDesk
    
    Change Log:

    Version: 1.2 - Added Password Last Set date to the end of data. This is useful to help troubleshoot lockout issues.
    Version: 1.1 - Added Description to the user data section.
    Version: 1.0 - Function Creation.
#>

Function Get-User() {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('SAMAccountName')]
        [String]$Username,
        [switch]$Groups,
        [switch]$Computers
    )

    begin {
        $Username = $Username.ToUpper()
    }

    process {
        try {
            if ($Groups.IsPresent) {
                Write-Verbose "Groups switch is on. Gathering groups."
                try {
                    Write-Verbose "Generating groups list for Username: $Username"
                    $GroupMembership = Get-ADPrincipalGroupMembership $Username -ErrorAction Stop | Select-Object Name | Sort-Object Name
                }
                catch {
                    Write-Verbose "Failed to generate groups list."
                    Write-Error "Could not retrieve groups for user $($Username.ToUpper())"
                }
            }
            try {
                Write-Verbose "Checking for Username: $Username details."
                $AllUserInfo = Get-ADUser -Identity $Username -Properties "DisplayName", "Department", "Title", "Office", "Mail", "OfficePhone", "IPPhone", "Manager", "Description", "EmployeeID", "pwdLastSet" -ErrorAction Stop
                $User = $AllUserInfo | `
                    Select-Object DisplayName, @{Name = 'Manager'; Expression = { $(Get-ADUser $_.Manager).Name } }, Department, Title, Office, Mail, OfficePhone, IPPhone, EmployeeID, SamAccountName, Description, @{Name = 'PassLastSet'; Expression = { [DateTime]::FromFileTime($_.pwdLastSet) } }
            }
            catch {
                Write-Verbose "Failed to lookup User attributes"
                Throw("The user '$Username' does not exist.")
            }
            if (!$User) {
                Write-Error "The user '$Username' does not exist."
            }
            else {
                if ($Groups.IsPresent) {
                    Write-Verbose "Groups switch is on. Displaying Groups."
                    $GroupMembership.Name
                }
                Write-Verbose "Displaying user attributes."
                $User

                if ($Computers.IsPresent) {
                    $UserDistinguishedName = $AllUserInfo.DistinguishedName
                    $ComputerManagedBy = Get-ADComputer -Filter { ManagedBy -eq $UserDistinguishedName }
                    if ($ComputerManagedBy) {
                        Write-Verbose "Found possible computers. Displaying results."
                        Write-Output "Possible Computers..."
                        $ComputerManagedBy.Name | ForEach-Object {
                            Get-ADComputer -Identity $_ -Properties Description -ErrorAction Stop | Select-Object Name, Description
                        }
                    }
                    else { 
                        $Surname = "*$((Get-ADUser -Identity $Username -Properties SurName).SurName)*"
                        Write-Verbose "Checking for possible computers based on last name: $Surname"
                        try {
                            $Filter = "Description -like `"$Surname`" -and Enabled -eq `"$true`""
                            $PossibleComputers = Get-ADComputer -filter $Filter -Properties Description -ErrorAction Stop | Select-Object Name, Description
                        }
                        catch {}
                        if ($PossibleComputers) {
                            Write-Verbose "Found possible computers. Displaying results."
                            Write-Output "Possible Computers..."
                            $PossibleComputers
                        }
                        else {
                            Write-Verbose "No possible PCs were found."
                            Write-Output "Unable to locate possible Computers for this user..."
                        }
                    }
                }
            }
        }
        catch {
            Write-Error "$($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)"
        }
    }
}
#EndRegion Get-User
#Region Get-UserProfile

<#
.SYNOPSIS
    Quickly generate a list of user profiles for a computer.
.DESCRIPTION
    This will generate a local or a remote list of non-special (SYSTEM, Administrator, etc.) profiles on a computer.
.PARAMETER ComputerName
    Specify the remote machine to query. You must have rights to connect to this computer.
.EXAMPLE
    PS> Get-UserProfile

    Description
    -----------
    This will display CIM Objects for each profile on the local system.
.EXAMPLE
    PS> Get-UserProfile -ComputerName Some-Remote-PC1

    Description
    -----------
    This will retrieve User Profile CIM Objects from the remote PC.
.NOTES
    Author: Matthew J. DeGarmo
    Handle: @matthewjdegarmo

    You can either submit a [PR](https://github.com/matthewjdegarmo/HelpDesk/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/HelpDesk/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/HelpDesk
#>

Function Get-UserProfile() {
    [CmdletBinding()]
    Param (
        [System.String] $ComputerName
    )

    Begin {}

    Process {
        Try {
            $Params = @{
                ClassName = 'Win32_UserProfile'
            }
            if ($PSBoundParameters.ContainsKey('ComputerName')) {
                $Params += @{ComputerName = $ComputerName.ToUpper()}
            }

            $Profiles = Get-CimInstance @Params | Where-Object {$_.Special -ne "Special" -and $_.LocalPath -ne "C:\Users\Administrator"}

            if ($Profiles) {
                $Profiles
            } else {
                Throw("There are no user profiles to display.")
            }
        } Catch {
            Write-Error "$($_.Exception.Message)"
        }
    }

    End {}
}
#EndRegion Get-UserProfile
#Region Get-UsersWithFirstName

<#
.SYNOPSIS
    This function performs a search of users in ActiveDirectory for a first name.
.DESCRIPTION
    This function is a simple Get-ADUser with a filter for GivenName to generate a list of users with the provided first name. This function provides the Name, SAMAccountName, and Office for each user with the specified first name. These are sorted by office first, and then name second.
.PARAMETER FirstName
    This parameter should specify the ActiveDirectory account GivenName (first name). This parameter will only accept alpha characters and will halt the function if it detects numeric characters.
.PARAMETER Properties
    This parameter will declare additional properties to include with the default values generated. To see what other properties can be included, fun this function against a name that is successful and add "| Get-Member" to get a list. The items with a MemberType of NoteProperty can be named in this -Properties parameter separated by commas.

    Example
    -------
    FirstName Matthew | Get-Member
.INPUTS
    System.String
        This function does not accept pipeline data. The values for all parameters must be specified.
.OUTPUTS
    System.Object[]
        Objects generated from the AD query.
.EXAMPLE
    PS>Get-UsersWithFirstName Matt -Properties Title

    Name SamAccountName Office Title
    ---- -------------- ------ -----
    JoeBobJenkins, Matt mjoebob Chicago Position
    Jackson, Matt mjackso Chicago Position
    Williamson-bob, Matt mwillia Boston-North Position
    Zachery, Matt mzacher Boston-East Position
    Zebras, Matt mzebras Chicago Position

    Description
    -----------
    This will find all ActiveDirectory users with the first name of 'Matt' and display the results.
.EXAMPLE
    PS>Get-FirstName FakeName

    FName : There is no user with 'FakeName' as their first name.

    Description
    -----------
    This will display an error indicating there were no users who matched the first name provided.
.EXAMPLE
    PS>fname michael

    Desctiption
    -----------
    This example shows using the alias for this function. The alias is FName.
.NOTES
    Author: Matthew J. DeGarmo
    GitHub: https://github.com/matthewjdegarmo

    You can either submit a [PR](https://github.com/matthewjdegarmo/HelpDesk/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/HelpDesk/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/HelpDesk
#>

Function Get-UsersWithFirstName() {
    [CmdletBinding()]
    param (
        [string[]] $Properties,

        [Parameter(Mandatory = $true, Position = 0)]
        [ValidateLength(1, 33)]
        [ValidateScript( { if ($_ -match '\d') {
                    throw('Parameter -FirstName should only contain alpha characters.')
                }
                else {
                    return $true
                }
            })]
        [string]$FirstName
    )

    $Filter = "GivenName -eq `"$FirstName`""
    $Users = Get-ADUser -Filter $Filter -Properties GivenName, Surname, Manager, Office, OfficePhone, Title
    $i = 0
    $Users | ForEach-Object {
        $i++
    }

    if ($i -eq 1) {
        Lookup -Username $Users.SamAccountName
    } else {
        ##Set up the default display set and create the member set object for use later on
        #Configure a default display set
        if ($Properties -eq '*') {
            $DefaultDisplaySet = 'Name', 'FirstName', 'LastName', 'Manager', 'Office', 'Phone', 'Gender', 'Title', 'SamAccountName'
        }
        elseif ($Properties -and $Properties -ne '*') {
            $DefaultDisplaySet = 'Name', 'SamAccountName', 'Office'
            $DefaultDisplaySet += $Properties
        }
        else {
            $DefaultDisplaySet = 'Name', 'SamAccountName', 'Office'
        }

        #Create the default property display set
        $DefaultDisplayPropertySet = New-Object System.Management.Automation.PSPropertySet('DefaultDisplayPropertySet', [string[]]$DefaultDisplaySet)
        $PSStandardMembers = [System.Management.Automation.PSMemberInfo[]]@($DefaultDisplayPropertySet)

        $Results = $Users | ForEach-Object {
            try {
                $Manager = $(Get-ADUser $_.Manager -ErrorAction Stop ).Name
            }
            catch {
                $Manager = "Unavailable"
            }
            $object = [pscustomobject]@{
                Name           = $_.Name
                FirstName      = $_.GivenName
                LastName       = $_.Surname
                Manager        = $Manager
                Office         = $_.Office
                Phone          = $_.OfficePhone
                Title          = $_.Title
                SamAccountName = $_.SamAccountName
            }

            #Give this object a unique typename
            $object.PSObject.TypeNames.Insert(0, 'User.Information')
            $object | Add-Member MemberSet PSStandardMembers $PSStandardMembers

            $Object
        }
        if ($Results) {
            $Results
        }
        else {
            Write-Error "There is no user with '$FirstName' as their first name."
        }
    }
}
#EndRegion Get-UsersWithFirstName
#Region Get-UsersWithLastName

<#
.SYNOPSIS
    This function performs a search of users in ActiveDirectory for a last name.
.DESCRIPTION
    This function is a simple Get-ADUser with a filter for SurName to generate a list of users with the provided last name. This function provides the Name, SAMAccountName, and Office for each user with the specified last name. These are sorted by office first, and then name second.
.PARAMETER LastName
    This parameter should specify the ActiveDirectory account SurName (last name). This parameter will only accept alpha characters and will halt the function if it detects numeric characters.
.PARAMETER Properties
    This parameter will declare additional properties to include with the default values generated. To see what other properties can be included, fun this function against a name that is successful and add "| Get-Member" to get a list. The items with a MemberType of NoteProperty can be named in this -Properties parameter separated by commas.

    Example
    -------
    Get-UsersWithLastName Matthew | Get-Member
.INPUTS
    System.String
        This function does not accept pipeline data. The values for all parameters must be specified.
.OUTPUTS
    System.Object[]
        Objects generated from the AD query.
.EXAMPLE
    PS>Get-UsersWithLastName Smith -Property Title

    Name SamAccountName Office Title
    ---- -------------- ------ -----
    Smith, Deborah dsmith Chicago Position
    Smith, Kent ksmith Boston-South Position
    Smith, Matthew msmith Chicago Position
    Smith, Trent tsmith Boston-North Position

    Description
    -----------
    This will find all ActiveDirectory users with the last name of 'Smith' and display the results.
.EXAMPLE
    PS>LastName FakeName

    LastName : There is no user with 'FakeName' as their last name.

    Description
    -----------
    This will display an error indicating there were no users who matched the last name provided.
.EXAMPLE
    PS>Lname degarmo

    Desctiption
    -----------
    This example shows using the alias for this function. The alias is LName.
.NOTES
    Author: Matthew J. DeGarmo
    GitHub: https://github.com/matthewjdegarmo

    You can either submit a [PR](https://github.com/matthewjdegarmo/HelpDesk/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/HelpDesk/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/HelpDesk
#>

Function Get-UsersWithLastName() {
    [CmdletBinding()]
    param (
        [string[]]$Properties,
        [Parameter(Mandatory = $true, Position = 0)]
        [ValidateLength(1, 35)]
        [ValidateScript( { if ($_ -match '\d') {
                    throw('Paremeter -LastName should only contain alpha characters.')
                }
                else {
                    return $true
                }
            })]
        [string]$LastName
    )

    $Filter = "Surname -eq `"$LastName`""
    $Users = Get-ADUser -Filter $Filter -Properties GivenName, Surname, Manager, Office, OfficePhone, Title
    $i = 0
    $Users | ForEach-Object {
        $i++
    }

    if ($i -eq 1) {
        Lookup -Username $Users.SamAccountName
    }
    else {

        ##Set up the default display set and create the member set object for use later on
        #Configure a default display set
        if ($Properties -eq '*') {
            $DefaultDisplaySet = 'Name', 'FirstName', 'LastName', 'Manager', 'Office', 'Phone', 'Gender', 'Title', 'SamAccountName'
        }
        elseif ($Properties -and $Properties -ne '*') {
            $DefaultDisplaySet = 'Name', 'SamAccountName', 'Office'
            $DefaultDisplaySet += $Properties
        }
        else {
            $DefaultDisplaySet = 'Name', 'SamAccountName', 'Office'
        }

        #Create the default property display set
        $DefaultDisplayPropertySet = New-Object System.Management.Automation.PSPropertySet('DefaultDisplayPropertySet', [string[]]$DefaultDisplaySet)
        $PSStandardMembers = [System.Management.Automation.PSMemberInfo[]]@($DefaultDisplayPropertySet)
        #$Users = Get-ADUser -Filter {Surname -eq $LastName} -Properties GivenName,Surname,Manager,Office,OfficePhone,Title
        $Results = $Users | ForEach-Object {
            try {
                $Manager = $(Get-ADUser $_.Manager -ErrorAction Stop).Name
            }
            catch {
                $Manager = "Unavailable"
            }
            $object = [pscustomobject]@{
                Name           = $_.Name
                FirstName      = $_.GivenName
                LastName       = $_.Surname
                Manager        = $Manager
                Office         = $_.Office
                Phone          = $_.OfficePhone
                Title          = $_.Title
                SamAccountName = $_.SamAccountName
            }

            #Give this object a unique typename
            $object.PSObject.TypeNames.Insert(0, 'User.Information')
            $object | Add-Member MemberSet PSStandardMembers $PSStandardMembers

            return $Object
        }
        if ($Results) {
            $Results
        }
        else {
            Write-Error "There is no user with '$LastName' as their last name."
        }
    }
}
#EndRegion Get-UsersWithLastName
#Region New-Password

<#
.SYNOPSIS
    Create a random password

.DESCRIPTION
    The function creates a random password using a given set of available characters.
    The password is generated with fixed or random length.
    Passwords will never end with a number, and never start with a special character.

.PARAMETER Admin
    This switch will change all of the password requirements to be a 24-character password with all character types.

.PARAMETER Size
    Specify the fixed length of the password.

.PARAMETER Count
    Number of passwords to generate, default = 1

.PARAMETER CharSets
    Specify the character-type requirements. U=Upper, L=Lower, N=Number, S=Special.

.PARAMETER Exclude
    Specify any characters to exclude from the new password generated.

.EXAMPLE
    PS> New-RandomPassword -Admin

    Description
    -----------
    This generates a random password of 24-character length, and includes all character types.
.EXAMPLE
    PS> New-RandomPassword -Size 20

    Description
    -----------
    Generates a password of 20 characters

.NOTES
    Author: Matthew J. DeGarmo
    GitHub: https://github.com/matthewjdegarmo

    You can either submit a [PR](https://github.com/matthewjdegarmo/HelpDesk/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/HelpDesk/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/HelpDesk
#>

Function New-Password() {
    [CmdletBinding()]
    param (
        [Int]$Size = 12,
        [Char[]]$CharSets = "ULNS",
        [Char[]]$Exclude,
        [int]$Count = 1,
        [switch]$Admin
    )

    if ($Admin.IsPresent) {
        $Size = 24
    }
    for ($i = 0; $i -lt $Count; $i++) {
        $Chars = @(); $TokenSets = @()
        #Creates a disctionary of character arrays for each character set
        If (!$TokenSets) {
            $TokenSets = @{
                U = [Char[]]'ABCDEFGHJKLMNPQRTUVWXYZ'       #Upper case
                L = [Char[]]'abcdefghijkmnrstuvwxyz'        #Lower case
                N = [Char[]]'2346789'                        #Numerals
                S = [Char[]]'_!@#%&'                          #Symbols
            }
        }
        #Cycles through each character set for a random character from each.
        $CharSets | ForEach-Object {
            #Creates a single array from all character and token sets.
            $Tokens = $TokenSets."$_" | ForEach-Object { If ($Exclude -cNotContains $_) { $_ } }
            If ($Tokens) {
                $TokensSet += $Tokens
                If ($_ -cle [Char]"Z") { $Chars += $Tokens | Get-Random }             #Character sets defined in upper case are mandatory
            }
        }
        #Loops through each character and grabs a random one.
        While ($Chars.Count -lt $Size) { $Chars += $TokensSet | Get-Random }
        #Further randomizes the ending password.
        $PW = ($Chars | Sort-Object { Get-Random }) -Join ""     #Mix the (mandatory) characters and output string
        
        #Password must not end with a number, must start with an alpha character, and must have at least 1 of each character type.
        #If password doesn't meet the below regexs, it adds 1 to the amount of password needed, and skips that password.
        if (($PW[-1] -match '\d') -or (-Not ($PW[0] -match '[a-zA-Z]')) -or (-Not($PW -match "^(?=.{8,})(?=.*[a-z])(?=.*[A-Z])(?=.*[_!@#%&]).*$"))) {
            $Count++
            continue
        }
        else {
            $PW
        }
    }
}
#EndRegion New-Password
#Region Remove-LocalAdmin

<#
.SYNOPSIS
    Removes user from local admin group.

.DESCRIPTION
    This function removes a local admin from the computer or server it is run from.

.PARAMETER Username
    Specify the SAMAccountName of the user to remove.

.PARAMETER ComputerName
    Specify a remote computer to run against.

.PARAMETER Domain
    Specify the domain that the user belongs to.

.EXAMPLE
    PS> Remove-LocalAdmin -Username someuser

    Description
    -----------
    Removes specified domain user from the local administrators group

.NOTES
    Author: Matthew J. DeGarmo
    GitHub: https://github.com/matthewjdegarmo

    You can either submit a [PR](https://github.com/matthewjdegarmo/HelpDesk/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/HelpDesk/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/HelpDesk

    Azure AD Joined machines will require the user to first login to a computer with their domain account before adding their domain account as a local admin.
    The user logging in registers their SID so that the command is successful.
#>

Function Remove-LocalAdmin() {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory, Position = 0)]
        [string] $Username,

        [Parameter(Position = 1, ValueFromPipelineByPropertyName)]
        [string] $ComputerName = $env:COMPUTERNAME,

        [Parameter()]
        [string] $Domain = $env:USERDOMAIN
    )

    begin {
        function Get-UsernameDomainFormat($Username, $Domain) {
            return ("$Domain\$Username")
        }
    }

    process {
        try {
            $FullUsername = Get-UsernameDomainFormat $Username $Domain
            if ($PSBoundParameters.ContainsKey('ComputerName')) {
                #Invoke-Command -ComputerName $ComputerName -ScriptBlock { Remove-LocalGroupMember -Group Administrators -Member $using:FullUsername }
                Invoke-Command -ComputerName $ComputerName -ScriptBlock { net.exe Localgroup Administrators $using:FullUsername /delete }
            }
            else {
                #Remove-LocalGroupMember -Group Administrators -Member $FullUsername
                net.exe Localgroup Administrators $FullUsername /delete
            }
        }
        catch {
            Write-Error "$($_.Exception.Message)"
        }
    }

    end {}
}
#EndRegion Remove-LocalAdmin
#Region Remove-UserProfile

<#
.SYNOPSIS
    Quickly delete a user profile from a computer
.DESCRIPTION
    This command will delete user profiles for the local or remote computer.
.PARAMETER Username
    Specify the specific username(s) for the profile(s) to be deleted.
.PARAMETER ComputerName
    Specify a remote computer name to remove user profiles from.
.PARAMETER All
    This switch will query all non-special profiles for deletion.
.PARAMETER Except
    This switch is used to exclude profiles when specifying the `-All` switch.
.EXAMPLE
    PS> Remove-UserProfile test123
    Description
    -----------
    This will delete the local `test123` account on the local computer.
    If this account / profile does not exist, you will get an error.
.EXAMPLE
    PS> Remove-UserProfile -All -ComputerName Some-Remote-PC1
    Description
    -----------
    This will delete all non-special profiles on the remote computer.
.EXAMPLE
    PS> Remove-UserProfile user1,user2,user3 -ComputerName Some-Remote-PC1 -Verbose
    Description
    -----------
    This will attempt to remove all named user accounts (comma-seperated) from the remote computer and display verbose output.
.EXAMPLE
    PS> Remove-UserProfile -ComputerName Some-Remote-PC1 -All -Except user1,specialuser,user4
    
    Description
    -----------
    This will remove all user profiles EXCEPT for the ones listed in the `-Except` parameter.
.NOTES
    Author: Matthew J. DeGarmo
    Handle: @matthewjdegarmo

    You can either submit a [PR](https://github.com/matthewjdegarmo/HelpDesk/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/HelpDesk/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/HelpDesk
#>

Function Remove-UserProfile() {
    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High', DefaultParameterSetName = 'Named')]
    Param (
        [Parameter(Mandatory, ParameterSetName = 'Named')]
        [System.String[]] $UserName,

        [Parameter(ParameterSetName = 'Named')]
        [Parameter(ParameterSetName = 'All')]
        [System.String] $ComputerName,

        [Parameter(Mandatory, ParameterSetName = 'All')]
        [Switch] $All,

        [Parameter(ParameterSetName = 'All')]
        [System.String[]] $Except
    )

    Begin {}

    Process {
        Try {
            if ($PSBoundParameters.ContainsKey('All')) {
                $Params = @{}
                $Confirm = $true
                if ($PSBoundParameters.ContainsKey('ComputerName')) {
                    $Params += @{ ComputerName = $ComputerName.ToUpper() }
                }
                else { $ComputerName = $env:COMPUTERNAME }

                $AllProfiles = Get-UserProfile @Params
                if ($PSBoundParameters.ContainsKey('Except')) {
                    $Except | ForEach-Object {
                        $ExceptUser = $_
                        Write-Output "Removing $ExceptUser from the deletion list. Will not delete user: $ExceptUser"
                        $AllProfiles = $AllProfiles | Where-Object { $_.LocalPath -notlike "*$ExceptUser*" }
                    }
                }
                if ($null -ne $AllProfiles) {
                    if ($PSCmdlet.ShouldProcess($AllProfiles, "Delete all profiles from this computer except for any exceptions listed.")) {
                        Write-Verbose "Removing all user profiles from computer: $ComputerName"
                        $Total = $AllProfiles.Count
                        $Position = 0
                        Write-Progress -Activity "Deleting Profiles from Computer: $ComputerName"
                        $AllProfiles | ForEach-Object {
                            $Percent = [int]$(($Position / $Total) * 100)
                            $Progress = @{
                                Activity         = "Removing profile for user: $(Split-Path $_.LocalPath -Leaf)"
                                Status           = "Removing profile $Position of $Total"
                                PercentComplete  = (($Position / $Total) * 100)
                                CurrentOperation = "$Percent%"
                            }
                            Write-Progress @Progress
                            Remove-CimInstance -InputObject $_
                            $Position++
                        }
                    }
                }
                else {
                    Write-Error "There are no user profiles to remove with the specified criteria. Username: $User Computername: $ComputerName"
                }
            }
            else {
                $Params = @{ }
                if ($PSBoundParameters.ContainsKey('ComputerName')) {
                    $Params += @{ ComputerName = $ComputerName.ToUpper() }
                }
                else { $ComputerName = $env:COMPUTERNAME }
                $Username | ForEach-Object {
                    $User = $_
                    $UserProfile = Get-UserProfile @Params | Where-Object { $_.LocalPath -match $User }
                    if ($UserProfile) {
                        Write-Verbose "Removing user profile $User on $ComputerName"
                        $UserProfile | Remove-CimInstance -Whatif:$Whatif -Confirm:$Confirm
                    }
                    else {
                        Write-Error "There are no user profiles to remove with the specified criteria. Username: $User Computername: $ComputerName"
                    }
                }
            }
        }
        Catch {
            Write-Error "$($_.Exception.Message)"
        }
    }

    End {}
}
#EndRegion Remove-UserProfile

#Region Reset-Password

<#
.SYNOPSIS
    Reset domain password for an ActiveDirectory user.

.DESCRIPTION
    Sets a Active Directory Password and provides an option to require a password change on login.
.PARAMETER Username
    Specify the username to reset the password for.
.PARAMETER Password
    Specify the password to set. By default, a strong, random password is generated, unless a password is provided.
.PARAMETER ComputerName
    Specify the computer name that the user would be sitting at. This will attempt to display a popup on their screen displaying the new password.
.PARAMETER RequireReset
    With this switch present, the user will be forced to reset their password immediately after signing in.
.PARAMETER Admin
    With this switch present, a stronger random password will be auto-generated. #Security
.EXAMPLE
    PS> Reset-Password someuser1

    Username Password
    -------- --------
    someuser1 xh9GKKtN!64v

    Description
    -----------
    Calls `New-Password`

.NOTES
    Author: Matthew J. DeGarmo
    GitHub: https://github.com/matthewjdegarmo

    You can either submit a [PR](https://github.com/matthewjdegarmo/HelpDesk/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/HelpDesk/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/HelpDesk
#>

Function Reset-Password() {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [string] $Username,

        [Parameter()][SecureString] 
        [string] $Password,

        [string] $ComputerName,

        [Switch] $RequireReset = $false,

        [Switch] $Admin
    )

    if ([String]::IsNullOrEmpty($Password)) {
        #Many domain admin accounts do not start with an alpha character. Edit this for your needs.
        if ($Username[0] -notmatch '[a-zA-Z]' -or $Admin.IsPresent) {
            $NewPasswordArgs = @{Admin = $True }
        }
        else { $NewPasswordArgs = @{} }
    
        $SecurePassword = New-Password @NewPasswordArgs | ConvertTo-SecureString -AsPlainText -Force
    }
    else {
        $SecurePassword = $Password | ConvertTo-SecureString -AsPlainText -Force
    }

    $ResetPasswordArgs = @{
        Username     = $Username
        RequireReset = $RequireReset
    }
    try {
        Set-Password @ResetPasswordArgs -Password $SecurePassword

        [pscustomobject] @{
            Username    = $Username
            NewPassword = ($SecurePassword | Get-PlainText)
        }
        #Copies
        $SecurePassword | Get-PlainText | Clip

        if ($PSBoundParameters.ContainsKey('ComputerName')) {
            Send-DesktopPopupMessage -ComputerName $ComputerName -Message "Password: $($SecurePassword | Get-PlainText)"
        }
    }
    catch {
        Write-Error "$($_.Exception.Message)"
    }
}
#EndRegion Reset-Password
#Region Send-DesktopPopupMessage

<#
.SYNOPSIS
    Send individual Pop-up message to remote host.
.DESCRIPTION
    This script sends a custom pop-up message to desired server/host with no options or required action. This script must be run as an Administrator.
.PARAMETER ComputerName
    This parameter is to specify the remote PC to attempt to send a message popup to. This parameter needs to pass a Test-Connection to attempt to send the message.
.PARAMETER Message
    This parameter should be inside quotation marks. This will be the text to be displayed in the popup.
.PARAMETER Seconds
    This parameter specifies the number of seconds that the popup will remain on the screen before auto-closing. This has a default value of 120.
.EXAMPLE
    PS> Send-DesktopPopupMessage -ComputerName computer1,FakeServer -Message "This is a test message. Get back to work!"
    [06/05/2019 09:46:36] : Message to computer1 sent successfully.
    [06/05/2019 09:46:36] : Unable to establish a connection with FAKESERVER.
    ___________________________________
    | Message from 6/5/2019 9:46 AM |X|
    |_________________________________|
    | This is a test message. Get back|
    | to work! |
    | From: Matthew J. DeGarmo |
    | ComputerName: SourceServer __ |
    | |OK||
    |_________________________________|

    Description
    -----------
    This will create a small popup window on the target computer with the message text, and the From: and ComputerName: information at the bottom.
.NOTES
    Author: Matthew J. DeGarmo
    GitHub: https://github.com/matthewjdegarmo

    You can either submit a [PR](https://github.com/matthewjdegarmo/HelpDesk/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/HelpDesk/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/HelpDesk
#>

Function Send-DesktopPopupMessage() {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, Position = 0)]
        [string[]] $ComputerName,

        [Parameter(Mandatory = $true, Position = 1)]
        [string] $Message,

        [Parameter(Position=2)]
        [int] $Seconds = 120
    )

    begin {
        $ComputerName = $ComputerName.ToUpper()
        $Username = (Get-ADUser -Identity $Env:Username | Select-Object Name).Name
        $LocalComputer = $Env:COMPUTERNAME
    }

    process {
        try {
            foreach ($Computer in $ComputerName) {
                if (Test-Connection $Computer -Count 1 -Quiet -ErrorAction Stop) {
                    try {
                        Write-Output "Sending message to Computer: $Computer..."
                        Invoke-Command -ComputerName $Computer -ScriptBlock {
                            C:\windows\system32\msg.exe * /time:$using:Seconds "$using:Message
        
From: $using:Username
Source: $using:LocalComputer"

                        }
                    }
                    catch {
                        Write-Error "Failed to send message to $ComputerName."
                    }
                }
                else {
                    Write-Error "Unable to establish a connection with $Computer."
                }
            }
            ""
        }
        catch {
            Write-Error "$($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)"
        }
    }
}
#EndRegion Send-DesktopPopupMessage
#Region Start-MSRA

<#
.SYNOPSIS
    Starts Microsoft Remote Assistance
.DESCRIPTION
    This function is set to work with the helpdesk mode of Windows Remote Assistance. You can either enter a computer name and the function will attempt to start a connection to the remote computer. If no computer name is specified Windows Remote Assistance will open to the Advanced Help Desk Mode, which is where you can enter a computer name to connect to.
.PARAMETER ComputerName
    This parameter is for the remote computer you wish to connect to. By default if you don’t specify a remote computer Windows Remote Assistant will open up to the helpdesk screen.
.EXAMPLE
    PS C:\> Start-MSRA computer1
    
    Description
    -----------
    This example shows how to call the Start-MSRA function and automatically connect to the remote computer.
.NOTES
    Author: Matthew J. DeGarmo
    GitHub: https://github.com/matthewjdegarmo

    You can either submit a [PR](https://github.com/matthewjdegarmo/HelpDesk/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/HelpDesk/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/HelpDesk
#>

Function Start-MSRA() {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false,
            HelpMessage = 'This is the remote computer you wish to connect to.')]
        [Alias('CN')]
        [string]
        $ComputerName = ""
    )
    
    If ($ComputerName -eq "") {
        Start-Process -FilePath "C:\Windows\System32\msra.exe" -Args "/Offerra"
    }
    Else {
        If (Test-Connection -ComputerName $ComputerName -Count 2 -Quiet) {
            Start-Process -FilePath "C:\Windows\System32\msra.exe" -Args "/Offerra \\$ComputerName"
        }
        Else {
            Return "Failed to Connect to $ComputerName"
        }
    }
}
#EndRegion Start-MSRA
#Region Test-Administrator

<#
.SYNOPSIS
    Test to see if the current user / process is being run as an Administrator.
.DESCRIPTION
    Test if the credentials running this function are elevated on the local system.
.PARAMETER User
    Specify the windows identity to test. This defaults to the current signed-in user running the command.
.EXAMPLE
    PS> Test-Administrator

    Description
    -----------
    This will return a boolean based on if the user is an admin or not.
.NOTES
    Author: Matthew J. DeGarmo
    Handle: @matthewjdegarmo

    You can either submit a [PR](https://github.com/matthewjdegarmo/HelpDesk/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/HelpDesk/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/HelpDesk
#>

Function Test-Administrator() {
    [CmdletBinding()]
    param (
        $User = [Security.Principal.WindowsIdentity]::GetCurrent()
    )
    (New-Object Security.Principal.WindowsPrincipal $user).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)
}
#EndRegion Test-Administrator
#Region Unlock-LockedOutADUsers

<#
.SYNOPSIS
    This function performs an unlock of users in ActiveDirectory who are currently locked out.
.DESCRIPTION
    This function is a simple wrapper for Unlock-ADAccount.
.OUTPUTS
    None
        This function does not produce output except for write-output data
.PARAMETER SAMAccountName
    Specify the SAMAccountName or username of the AD User you want to unlock.
.EXAMPLE
    PS>Unlock-LockedOutADUsers -SAMAccountName matthewjd
    Unlocking: matthewjd

    Description
    -----------
    This will unlock the AD Account for user 'matthewjd'.
.EXAMPLE
    PS>Get-LockedOutADUsers | Unlock-LockedOutADUsers
    Unlocking: User1
    Unlocking: User2

    Description
    -----------
    This gets a list of all current locked out users from Get-LockedOutADUsers and forwards each user to Unlock-LockedOutADUsers, which effectively unlocked all currently locked AD Users
    A short-handed command for this using aliases: PS> glo | ul
.NOTES
    Author: Matthew J. DeGarmo
    GitHub: https://github.com/matthewjdegarmo

    You can either submit a [PR](https://github.com/matthewjdegarmo/HelpDesk/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/HelpDesk/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/HelpDesk
#>

Function Unlock-LockedOutADUsers() {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [Alias('Username')]
        [string[]]$SAMAccountName
    )

    begin {}

    process {
        try {
            foreach ($Username in $SAMAccountName) {
                Write-Output "Unlocking: $Username"
                Unlock-ADAccount -Identity $Username -ErrorAction Stop
            }
        }
        catch {
            Write-Error "$($_.Exception.Message)"
        }
    }
}
#EndRegion Unlock-LockedOutADUsers
#Region Watch-Connection

<#
.SYNOPSIS
    Loops through a test-connection and sends email when connection is made.
.DESCRIPTION
    This script simply is a test-connection that loops until a connection is made.
.PARAMETER ComputerName
    This parameter should specify the computer name you are testing a connections with.
.PARAMETER Message
    This parameter will include text to be included in the success email.
.PARAMETER Recipients
    This parameter specifies additional recipients to receive the successful connection email. These values should be separated by commas and wrapped in quotation marks.
.PARAMETER Email
    This switch allows the function to send an email when a successful connection is made. If this switch is present, an email will be sent.
.PARAMETER From
    This is the email address that this alert will originate from.
.PARAMETER SmtpServer
    This is the SMTP server that you will be using to send the email.
.EXAMPLE
    PS>Watch-Connection -ComputerName SomeServer -Message "A connection to SomeServer has been made. Please remove admin access from user 'SomeUser'" -Recipients "myself@me.com","someoneelse@them.com","OtherEmailsSeperatedByCommas@site.com"
    Connecting to SOMESERVER....
    Sending email to the following recipients: "myself@me.com","someoneelse@them.com","OtherEmailsSeperatedByCommas@site.com"
    Connection Successful.
    Message: A connection to SomeServer has been made. Please remove admin access from user 'SomeUser'

    Description
    -----------
    This will loop through a Test-Connection -Count 1 command against 'SomeServer' until a connection is made. Once it is made, an email is sent to the user running the script and to the additional users specified in the -Recipients Parameter.
.EXAMPLE
    PS>Watch-Connection SomeComputer -Notification "Finally connected to user 'SomeUser' PC."
    Connecting to SOMECOMPUTER....
    Connection Successful.
    Message: Finally connected to user 'SomeUser' PC.

    Description
    -----------
    This will loop through a `Test-Connection -Count 1` command against 'SomeComputer'.
.NOTES
    Author: Matthew J. DeGarmo
    GitHub: https://github.com/matthewjdegarmo

    You can either submit a [PR](https://github.com/matthewjdegarmo/HelpDesk/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/HelpDesk/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/HelpDesk
#>

Function Watch-Connection() {
    [cmdletbinding()]
    param(
        [parameter(Mandatory = $true, Position = 1)]
        [ValidateNotNullOrEmpty()]
        [string] $ComputerName,

        [string ]$Message,

        [string[]] $Recipients,

        [switch] $Email,

        [String] $From,

        [String] $SmtpServer
    )

    begin {
        $ComputerName = $ComputerName.ToUpper()
    }

    process {
        try {
            DO {
                Clear-Host
                Write-Host "Connecting to $ComputerName." -ForegroundColor Yellow
                Start-Sleep -Milliseconds 500
                Clear-Host
                Write-Host "Connecting to $ComputerName.." -ForegroundColor Yellow
                Start-Sleep -Milliseconds 500
                Clear-Host
                Write-Host "Connecting to $ComputerName..." -ForegroundColor Yellow
                Start-Sleep -Milliseconds 500
                Clear-Host
                Write-Host "Connecting to $ComputerName...." -ForegroundColor Yellow
            } while (-not(Test-Connection $ComputerName -count 1 -ErrorAction SilentlyContinue))

            if (Test-Connection $ComputerName -count 1 -ErrorAction SilentlyContinue) {
                if ($Email.IsPresent) {
                    $MessageArgs = @{
                        To         = $Recipients
                        From       = $From
                        Subject    = "$ComputerName Online"
                        SmtpServer = $SmtpServer
                        Priority   = 'High'
                        Body       = "$ComputerName is now online.
                        <p>$Message</p>
                        <p></p><n>From: $env:USERNAME</n>
                        <p></p><n>Source: $env:COMPUTERNAME</n>"

                    }
                    Write-Output "Sending email to the following recipients: $Recipients"
                    Send-MailMessage @MessageArgs -BodyAsHtml
                }
                Write-Host 'Connection Successful.' -ForegroundColor Green
                if ($Message) {
                    Write-Output "Message: $Message"
                }
            }
        }
        catch {
            Write-Error "$($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)"
        }
    }

    end {}
}
#EndRegion Watch-Connection