codaamok.psm1

#region Private functions
#endregion

#region Public functions
Function Add-FileAssociation {
        <#
        .SYNOPSIS
        Set user file associations
        .DESCRIPTION
        Define a program to open a file extension
        .PARAMETER Extension
        The file extension to modify
        .PARAMETER TargetExecutable
        The program to use to open the file extension
        .PARAMETER ftypeName
        Non mandatory parameter used to override the created file type handler value
        .EXAMPLE
        $HT = @{
            Extension = '.txt'
            TargetExecutable = "C:\Program Files\Notepad++\notepad++.exe"
        }
        Add-FileAssociation @HT
        .EXAMPLE
        $HT = @{
            Extension = '.xml'
            TargetExecutable = "C:\Program Files\Microsoft VS Code\Code.exe"
            FtypeName = 'vscode'
        }
        Add-FileAssociation @HT
        .NOTES
        Found here: https://gist.github.com/p0w3rsh3ll/c64d365d15f6f39116dba1a26981dc68#file-add-fileassociation-ps1 https://p0w3rsh3ll.wordpress.com/2018/11/08/about-file-associations/
        #>

        [CmdletBinding()]
        Param(
            [Parameter(Mandatory=$true)]
            [ValidateNotNullOrEmpty()]
            [ValidatePattern('^\.[a-zA-Z0-9]{1,3}')]
            $Extension,
            [Parameter(Mandatory=$true)]
            [ValidateNotNullOrEmpty()]
            [ValidateScript({
                Test-Path -Path $_ -PathType Leaf
            })]
            [string]$TargetExecutable,
            [string]$ftypeName
        )
        Begin {
            $ext = [Management.Automation.Language.CodeGeneration]::EscapeSingleQuotedStringContent($Extension)
            $exec = [Management.Automation.Language.CodeGeneration]::EscapeSingleQuotedStringContent($TargetExecutable)
        
            # 2. Create a ftype
            if (-not($PSBoundParameters['ftypeName'])) {
                $ftypeName = '{0}{1}File'-f $($ext -replace '\.',''),
                $((Get-Item -Path "$($exec)").BaseName)
                $ftypeName = [Management.Automation.Language.CodeGeneration]::EscapeFormatStringContent($ftypeName)
            } else {
                $ftypeName = [Management.Automation.Language.CodeGeneration]::EscapeSingleQuotedStringContent($ftypeName)
            }
            Write-Verbose -Message "Ftype name set to $($ftypeName)"
        }
        Process {
            # 1. remove anti-tampering protection if required
            if (Test-Path -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\$($ext)") {
                $ParentACL = Get-Acl -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\$($ext)"
                if (Test-Path -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\$($ext)\UserChoice") {
                    $k = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey("Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\$($ext)\UserChoice",'ReadWriteSubTree','TakeOwnership')
                    $acl  = $k.GetAccessControl()
                    $null = $acl.SetAccessRuleProtection($false,$true)
                    $rule = New-Object System.Security.AccessControl.RegistryAccessRule ($ParentACL.Owner,'FullControl','Allow')
                    $null = $acl.SetAccessRule($rule)
                    $rule = New-Object System.Security.AccessControl.RegistryAccessRule ($ParentACL.Owner,'SetValue','Deny')
                    $null = $acl.RemoveAccessRule($rule)
                    $null = $k.SetAccessControl($acl)
                    Write-Verbose -Message 'Removed anti-tampering protection'
                }
            }
            # 2. add a ftype
            $null = & (Get-Command "$($env:systemroot)\system32\reg.exe") @(
                'add',
                "HKCU\Software\Classes\$($ftypeName)\shell\open\command"
                '/ve','/d',"$('\"{0}\" \"%1\"'-f $($exec))",
                '/f','/reg:64'
            )
            Write-Verbose -Message "Adding command under HKCU\Software\Classes\$($ftypeName)\shell\open\command"
            # 3. Update user file association

            # Reg2CI (c) 2019 by Roger Zander
            $eap = "SilentlyContinue"
            Remove-Item -LiteralPath ("HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\{0}\OpenWithList" -f $ext) -Force
            if((Test-Path -LiteralPath ("HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\{0}\OpenWithList" -f $ext)) -ne $true) { 
                New-Item ("HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\{0}\OpenWithList" -f $ext) -Force -ErrorAction $eap | Out-Null
            }
            Remove-Item -LiteralPath ("HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\{0}\OpenWithProgids" -f $ext) -Force
            if((Test-Path -LiteralPath ("HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\{0}\OpenWithProgids" -f $ext)) -ne $true) { 
                New-Item ("HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\{0}\OpenWithProgids" -f $ext) -Force -ErrorAction $eap | Out-Null
            }
            if((Test-Path -LiteralPath ("HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\{0}\UserChoice" -f $ext)) -ne $true) {
                New-Item ("HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\{0}\UserChoice" -f $ext) -Force -ErrorAction $eap | Out-Null
            }
            New-ItemProperty -LiteralPath ("HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\{0}\OpenWithList" -f $ext) -Name "MRUList" -Value "a" -PropertyType String -Force -ErrorAction $eap | Out-Null
            New-ItemProperty -LiteralPath ("HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\{0}\OpenWithList" -f $ext) -Name "a" -Value ("{0}" -f (Get-Item -Path $exec | Select-Object -ExpandProperty Name)) -PropertyType String -Force -ErrorAction $eap | Out-Null
            New-ItemProperty -LiteralPath ("HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\{0}\OpenWithProgids" -f $ext) -Name $ftypeName -Value (New-Object Byte[] 0) -PropertyType None -Force -ErrorAction $eap | Out-Null
            Remove-ItemProperty -LiteralPath ("HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\{0}\UserChoice" -f $ext) -Name "Hash" -Force -ErrorAction $eap
            Remove-ItemProperty -LiteralPath ("HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\{0}\UserChoice" -f $ext) -Name "Progid" -Force  -ErrorAction $eap
        }
    }

function Add-FunctionToProfile {
    <#
.SYNOPSIS
    Add a function to your profile
.DESCRIPTION
    This function is used to append a function to your PowerShell profile. You provide a function name, and if it has a script block
    then it will be appended to your PowerShell profile with the function name provided.
.PARAMETER FunctionToAdd
    The name of the function(s) you wish to add to your profile. You can provide multiple.
.EXAMPLE
    PS C:\> Add-FunctionToProfile -FunctionToAdd 'Get-CMClientMaintenanceWindow'
.NOTES
    If a function doesn't have a script block, then it cannot be added to your profile
    Cody is the man: https://github.com/CodyMathis123/CM-Ramblings/blob/master/Add-FunctionToProfile.ps1
#>

    param(
        [Parameter(Mandatory = $True)]
        [string[]]$FunctionToAdd
    )
    foreach ($FunctionName in $FunctionToAdd) {
        try {
            $Function = Get-Command -Name $FunctionName -CommandType Function -ErrorAction Stop
        }
        catch {
            Write-Error "Failed to find the specified function [Name = '$FunctionName']"
            continue
        }    
        $ScriptBlock = $Function | Select-Object -ExpandProperty ScriptBlock
        if ($null -ne $ScriptBlock) {
            $FuncToAdd = [string]::Format("`r`nfunction {0} {{{1}}}", $FunctionName, $ScriptBlock)
            ($FuncToAdd -split "`n") | Add-Content -Path $PROFILE 
        }
        else {
            Write-Error "Function $FunctionName does not have a Script Block and cannot be added to your profile."
        }
    }
}

Function Change-Password ($domain,$samaccountname,$oldPassword,$newpassword){
    ([adsi]"WinNT://$domain/$samaccountname,user").ChangePassword($oldPassword,$newpassword)
}

Function Connect-CMDrive {
    param(
        # SMS provider or site server
        [Parameter(Mandatory=$false, Position = 0)]
        [ValidateScript({
            If(!(Test-Connection -ComputerName $_ -Count 1 -ErrorAction SilentlyContinue)) {
                throw "Host `"$($_)`" is unreachable"
            } Else {
                return $true
            }
        })]
        [string]
        $Server,
        [Parameter(Mandatory=$false, Position = 1)]
        [string]
        $SiteCode,
        [Parameter(Mandatory=$false, Position = 2)]
        [string]
        $Path = (Get-Location | Select-Object -ExpandProperty Path)
    )
    if ([string]::IsNullOrEmpty($Server)) {
        try {
            $Server = Get-ItemProperty "HKLM:\SOFTWARE\WOW6432Node\Microsoft\ConfigMgr10\AdminUI\Connection" -ErrorAction Stop | Select-Object -ExpandProperty Server
        }
        catch [System.Management.Automation.ItemNotFoundException] {
            throw "Console must be installed. If it is installed, then fix your code but for now specify -Server"
        }
    }
    if ([string]::IsNullOrEmpty($SiteCode)) {
        try {
            $SiteCode = Get-WmiObject -Class "SMS_ProviderLocation" -Name "ROOT\SMS" -ComputerName $Server -ErrorAction Stop | Select-Object -ExpandProperty SiteCode
        }
        catch {
            switch -regex ($_.Exception.Message) {
                "Invalid namespace" {
                    throw ("No SMS provider installed on {0}" -f $Server)
                }
                default {
                    throw "Could not determine SiteCode, please pass -SiteCode"
                }
            }
        }
    }

    # Import the ConfigurationManager.psd1 module
    If((Get-Module ConfigurationManager) -eq $null) {
        try {
            Import-Module ("{0}\..\ConfigurationManager.psd1" -f $ENV:SMS_ADMIN_UI_PATH)
        }
        catch {
            throw ("Failed to import ConfigMgr module: {0}" -f $_.Exception.Message)
        }
    }
    try {
        # Connect to the site's drive if it is not already present
        If((Get-PSDrive -Name $SiteCode -PSProvider CMSite -ErrorAction SilentlyContinue) -eq $null) {
            New-PSDrive -Name $SiteCode -PSProvider CMSite -Root $Server -ErrorAction Stop | Out-Null
        }
        # Set the current location to be the site code.
        Set-Location ("{0}:\" -f $SiteCode) -ErrorAction Stop

        # Verify given sitecode
        If((Get-CMSite -SiteCode $SiteCode | Select-Object -ExpandProperty SiteCode) -ne $SiteCode) { throw }

    } 
    catch {
        If((Get-PSDrive -Name $SiteCode -PSProvider CMSite -ErrorAction SilentlyContinue) -ne $null) {
            Set-Location $Path
            Remove-PSDrive -Name $SiteCode -Force
        }
        throw ("Failed to create New-PSDrive with site code `"{0}`" and server `"{1}`"" -f $SiteCode, $Server)
    }
}

function ConvertTo-ByteArrayHex {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ValueFromPipeline)]
        [String]$String
    )
    begin { }
    process {
        [Byte[]]$bytes = for ($i = 0; $i -lt $String.Length; $i += 2) {
            '0x{0}{1}' -f $String[$i], $String[$i + 1]
        }
        $bytes
    }
}

function ConvertTo-ByteArrayString {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ValueFromPipeline)]
        [String]$String
    )
    begin { }
    process {
        [System.Text.Encoding]::UTF8.GetBytes($String)
    }
}

function ConvertTo-HexString {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ValueFromPipeline)]
        [String]$String
    )
    begin { }
    process {
        foreach ($char in $String.ToCharArray()) {
            [System.String]::Format("{0:X}", [System.Convert]::ToUInt32($char))
        }
    }
}

function ConvertTo-Ini {
    param (
        [Object[]]$Content,
        [String]$SectionTitleKeyName
    )
    begin {
        $StringBuilder = [System.Text.StringBuilder]::new()
        $SectionCounter = 0
    }
    process {
        foreach ($ht in $Content) {
            $SectionCounter++

            if ($ht -is [System.Collections.Specialized.OrderedDictionary] -Or $ht -is [hashtable]) {
                if ($ht.Keys -contains $SectionTitleKeyName) {
                    $null = $StringBuilder.AppendFormat("[{0}]", $ht[$SectionTitleKeyName])
                }
                else {
                    $null = $StringBuilder.AppendFormat("[Section {0}]", $SectionCounter)
                }

                $null = $StringBuilder.AppendLine()

                foreach ($key in $ht.Keys) {
                    if ($key -ne $SectionTitleKeyName) {
                        $null = $StringBuilder.AppendFormat("{0}={1}", $key, $ht[$key])
                        $null = $StringBuilder.AppendLine()
                    }
                }

                $null = $StringBuilder.AppendLine()
            }
        }
    }
    end {
        $StringBuilder.ToString(0, $StringBuilder.Length-4)
    }
}

function CreateScriptInstance([string]$ComputerName)
    {
        # Check to see if our custom WMI class already exists
        $classCheck = Get-WmiObject -Class Noxigen_WmiExec -ComputerName $ComputerName -List -Namespace "root\cimv2"
        
        if ($classCheck -eq $null)
        {
            # Create a custom WMI class to store data about the command, including the output.
            Write-Host "Creating WMI class..."
            $newClass = New-Object System.Management.ManagementClass("\\$ComputerName\root\cimv2",[string]::Empty,$null)
            $newClass["__CLASS"] = "Noxigen_WmiExec"
            $newClass.Qualifiers.Add("Static",$true)
            $newClass.Properties.Add("CommandId",[System.Management.CimType]::String,$false)
            $newClass.Properties["CommandId"].Qualifiers.Add("Key",$true)
            $newClass.Properties.Add("CommandOutput",[System.Management.CimType]::String,$false)
            $newClass.Put() | Out-Null
        }
        
        # Create a new instance of the custom class so we can reference it locally and remotely using this key
        $wmiInstance = Set-WmiInstance -Class Noxigen_WmiExec -ComputerName $ComputerName
        $wmiInstance.GetType() | Out-Null
        $commandId = ($wmiInstance | Select-Object -Property CommandId -ExpandProperty CommandId)
        $wmiInstance.Dispose()
        
        # Return the GUID for this instance
        return $CommandId
    }

Function Enable-RemoteRegistry {
    # if disabled, set to manual and start
}

function ExecCommand([string]$ComputerName, [string]$Command)
    {
        #Pass the entire remote command as a base64 encoded string to powershell.exe
        $commandLine = "powershell.exe -NoLogo -NonInteractive -ExecutionPolicy Unrestricted -WindowStyle Hidden -EncodedCommand " + $Command
        $process = Invoke-WmiMethod -ComputerName $ComputerName -Class Win32_Process -Name Create -ArgumentList $commandLine
        
        if ($process.ReturnValue -eq 0)
        {
            $started = Get-Date
            
            Do
            {
                if ($started.AddMinutes(2) -lt (Get-Date))
                {
                    Write-Host "PID: $($process.ProcessId) - Response took too long."
                    break
                }
                
                # TODO: Add timeout
                $watcher = Get-WmiObject -ComputerName $ComputerName -Class Win32_Process -Filter "ProcessId = $($process.ProcessId)"
                
                Write-Host "PID: $($process.ProcessId) - Waiting for remote command to finish..."
                
                Start-Sleep -Seconds 1
            }
            While ($watcher -ne $null)
            
            # Once the remote process is done, retrieve the output
            $scriptOutput = GetScriptOutput $ComputerName $scriptCommandId
            
            return $scriptOutput
        }
    }

function Get-AllTaskSubFolders {
                [cmdletbinding()]
                param (
                    # Set to use $Schedule as default parameter so it automatically list all files
                    # For current schedule object if it exists.
                    $FolderRef = $sch.getfolder("\"),

                    [switch]$recurse
                )

                #No recurse? Return the folder reference
                if (-not $recurse) {
                    $FolderRef
                }
                #Recurse? Build up an array!
                else {
                    Try{
                        #This will fail on older systems...
                        $folders = $folderRef.getfolders(1)

                        #Extract results into array
                        $ArrFolders = @(
                            if($folders) {
                                foreach ($fold in $folders) {
                                    $fold
                                    if($fold.getfolders(1)) {
                                        Get-AllTaskSubFolders -FolderRef $fold
                                    }
                                }
                            }
                        )
                    }
                    Catch{
                        #If we failed and the expected error, return folder ref only!
                        if($_.tostring() -like '*Exception calling "GetFolders" with "1" argument(s): "The request is not supported.*')
                        {
                            $folders = $null
                            Write-Warning "GetFolders failed, returning root folder only: $_"
                            Return $FolderRef
                        }
                        else{
                            Throw $_
                        }
                    }

                    #Return only unique results
                        $Results = @($ArrFolders) + @($FolderRef)
                        $UniquePaths = $Results | select -ExpandProperty path -Unique
                        $Results | ?{$UniquePaths -contains $_.path}
                }
            }

Function Get-Boot {
    Param (
        [Parameter()]
        [String[]]$ComputerName,
        [Parameter()]
        [PSCredential]$Credential
    )
    $HashArguments = @{
        ClassName = "Win32_OperatingSystem"
    }
    if ($PSBoundParameters.ContainsKey("ComputerName")) {
        $HashArguments["ComputerName"] = $ComputerName
    }
    if ($PSBoundParameters.ContainsKey('Credential')) {
        $HashArguments["Credential"] = $Credential
    }
    Get-CimInstance @HashArguments | Select-Object PSComputerName, LastBootUpTime
}

function Get-CommandInfo {
    <#
    .SYNOPSIS
        Get-Command helper.
    .DESCRIPTION
        Get-Command helper.
    .NOTES
        https://github.com/indented-automation/Indented.Profile/blob/master/Indented.Profile/public/Get-CommandInfo.ps1
    #>


    [CmdletBinding()]
    param (
        # The name of a command.
        [Parameter(Mandatory, ParameterSetName = 'ByName')]
        [String]$Name,

        # A CommandInfo object.
        [Parameter(Mandatory, ParameterSetName = 'FromCommandInfo')]
        [System.Management.Automation.CommandInfo]$CommandInfo,

        # If a module name is specified the private / internal scope of the module will be searched.
        [String]$ModuleName,

        # Claims and discards any other supplied arguments.
        [Parameter(ValueFromRemainingArguments, DontShow)]
        $EaterOfArgs
    )

    if ($Name) {
        if ($ModuleName) {
            try {
                if (-not ($moduleInfo = Get-Module $ModuleName)) {
                    $moduleInfo = Import-Module $ModuleName -Global -PassThru
                }
                $CommandInfo = & $moduleInfo ([ScriptBlock]::Create('Get-Command {0}' -f $Name))
            } catch {
                $pscmdlet.ThrowTerminatingError($_)
            }
        } else {
            $CommandInfo = Get-Command -Name $Name
        }
    }

    if ($CommandInfo -is [System.Management.Automation.AliasInfo]) {
        $CommandInfo = $CommandInfo.ResolvedCommand
    }

    return $CommandInfo
}

Function Get-Dns {
    Param( 
        [Parameter()]
        [String[]]$ComputerName,
        [Parameter(Mandatory)]
        [String]$FirstOctet,
        [Parameter()]
        [PSCredential]$Credential
    )
    $InvokeCommandSplat = @{
        ScriptBlock = {
            $InterfaceIndex = Get-NetIPAddress -AddressFamily IPv4 | Where-Object { $_.IPAddress.StartsWith($FirstOctet) } | Select-Object -ExpandProperty InterfaceIndex
            [PSCustomObject]@{
                ComputerName = $env:COMPUTERNAME
                DNSAddresses = [String]::Join(", ", (Get-DnsClientServerAddress -InterfaceIndex $InterfaceIndex -AddressFamily IPv4).ServerAddresses)
            }
        }
    }
    if ($PSBoundParameters.ContainsKey("Credential")) {
        $InvokeCommandSplat["Credential"] = $Credential
    }
    $Jobs = ForEach ($Computer in $ComputerName) {
        if ($PSBoundParameters.ContainsKey("ComputerName")) {
            $InvokeCommandSplat["ComputerName"] = $Computer
        }
        Invoke-Command @InvokeCommandSplat -AsJob
    }
    while (Get-Job -State "Running") {
        $TotalJobs = $Jobs.count
        $NotRunning = $Jobs | Where-Object { $_.State -ne "Running" }
        $Running = $Jobs | Where-Object { $_.State -eq "Running" }
        Write-Progress -Activity "Waiting on results" -Status "$($TotalJobs-$NotRunning.count) Jobs Remaining: $($Running.Location)" -PercentComplete ($NotRunning.count/(0.1+$TotalJobs) * 100)
        Start-Sleep -Seconds 2
    }
    Get-Job | Receive-Job
    Get-Job | Remove-Job    
}

function Get-MyChocoPackages {
    param (
        [String[]]$Exclude,
        [Switch]$IncludeDellUpdate
    )
    $Packages = @(
        "Everything"
        "powertoys"
        "git"
        "vscode"
        "vim"
        "7zip"
        "pwsh"
        "royalts-v5"
        "discord"
        "slack"
        "whatsapp"
        "GoogleChrome"
        "microsoft-edge"
        "microsoft-windows-terminal"
        "ditto"
        "obs-studio"
        "cue"
        "snagit"
        "eddie"
        "altdrag"
    )
    if ((Get-CimInstance -ClassName "Win32_ComputerSystem").Manufacturer -match "dell" -Or $IncludeDellUpdate.IsPresent) {
        $Packages += "dell-update"
    }
    $Packages | Where-Object { $Exclude -notcontains $_ }
}

Function Get-MyCommands {
    [Alias("Get-MyFunctions")]
    param (
        [String]$ProfileFile = $PSCommandPath
    )

    Get-Content -Path $ProfileFile | Select-String -Pattern "^function.+" | ForEach-Object {
        $result = [Regex]::Matches($_, "^function ([a-z.-]+)","IgnoreCase").Groups[1].Value
        if ($result -ne "prompt") { $result }
    } | Sort-Object
}

function Get-MyOS {
    switch -Regex ($PSVersionTable.PSVersion) {
        "^[6-7]" {
            switch ($true) {
                $IsLinux {
                    "Linux"
                }
                $IsWindows {
                    "Windows"
                }
            }
        }
        "^[1-5]" {
            "Windows"
        }
    }
}

function Get-OS {
    Param (
        [Parameter(Mandatory)]
        [String]$ComputerName,
        [Parameter()]
        [PSCredential]$Credential
    )
    $newCimSessionSplat = @{
        ComputerName = $ComputerName
        ErrorAction = "Stop"
    }
    if ($PSBoundParameters.ContainsKey("Credential")) {
        $newCimSessionSplat["Credential"] = $Credential
    }
    $Session = New-CimSession @newCimSessionSplat 
    $getCimInstanceSplat = @{
        Query = "Select Caption from Win32_OperatingSystem"
        CimSession = $Session
    }
    Get-CimInstance @getCimInstanceSplat | Select-Object -ExpandProperty Caption
    Remove-CimSession $Session
}

Function Get-PSTools {
    Add-Type -AssemblyName System.IO.Compression.FileSystem
    Function Unzip {
        Param(
            [string]$zipfile, 
            [string]$outpath
        )
        [System.IO.Compression.ZipFile]::ExtractToDirectory($zipfile, $outpath)
    }
    try {
        $Path = (Join-Path -Path $env:LOCALAPPDATA -ChildPath "Microsoft\WindowsApps")
        (New-Object System.Net.WebClient).DownloadFile("https://download.sysinternals.com/files/PSTools.zip", (Join-Path -Path $Path -ChildPath "PSTools.zip"))
        Unzip -zipfile (Join-Path -Path $Path -ChildPath "PSTools.zip") -outpath $Path
        Rename-Item -LiteralPath (Join-Path -Path $Path -ChildPath "PSexec.exe") -NewName (Join-Path -Path $Path -ChildPath "PSexec_.exe") -ErrorAction Stop
    }
    catch {
        Write-Host "Error: " -ForegroundColor Red -NoNewline
        Write-Host $_.Exception.Message -NoNewline
        Write-Host (" (line {0})" -f $_.InvocationInfo.ScriptLineNumber)
    }
}

Function Get-Reboots {
    Param(
        [Parameter(Mandatory=$false,Position=0)]
        [string]$ComputerName,
        [Parameter(Mandatory=$false)]
        [PSCredential]$Credential
    )
    $HashArguments = @{
        FilterHashtable = @{
            LogName="System"
            ID=1074
        }
    } 
    if ($PSBoundParameters.ContainsKey("ComputerName")) {
        $HashArguments.Add("ComputerName", $ComputerName)
    }
    else {
        $HashArguments.Add("ComputerName", $env:COMPUTERNAME)
    }
    Get-WinEvent @HashArguments | ForEach-Object {
        [PSCustomObject]@{
            Date = $_.TimeCreated
            User = $_.Properties[6].Value
            Process = $_.Properties[0].Value
            Action = $_.Properties[4].Value
            Reason = $_.Properties[2].Value
            ReasonCode = $_.Properties[3].Value
            Comment = $_.Properties[5].Value
        }
    } | Sort-Object Date -Descending
}

Function Get-ScheduledTasks {  
    <#
    .SYNOPSIS
        Get scheduled task information from a system
        https://gallery.technet.microsoft.com/Get-ScheduledTasks-Get-d2207def
    
    .DESCRIPTION
        Get scheduled task information from a system

        Uses Schedule.Service COM object, falls back to SchTasks.exe as needed.
        When we fall back to SchTasks, we add empty properties to match the COM object output.

    .PARAMETER ComputerName
        One or more computers to run this against

    .PARAMETER Folder
        Scheduled tasks folder to query. By default, "\"

    .PARAMETER Recurse
        If specified, recurse through folders below $folder.
        
        Note: We also recurse if we use SchTasks.exe

    .PARAMETER Path
        If specified, path to export XML files
        
        Details:
            Naming scheme is computername-taskname.xml
            Please note that the base filename is used when importing a scheduled task. Rename these as needed prior to importing!

    .PARAMETER Exclude
        If specified, exclude tasks matching this regex (we use -notmatch $exclude)

    .PARAMETER CompatibilityMode
        If specified, pull scheduled tasks only with the schtasks.exe command, which works against older systems.
    
        Notes:
            Export is not possible with this switch.
            Recurse is implied with this switch.
    
    .EXAMPLE
    
        #Get scheduled tasks from the root folder of server1 and c-is-ts-91
        Get-ScheduledTasks server1, c-is-ts-91

    .EXAMPLE

        #Get scheduled tasks from all folders on server1, not in a Microsoft folder
        Get-ScheduledTasks server1 -recurse -Exclude "\\Microsoft\\"

    .EXAMPLE
    
        #Get scheduled tasks from all folders on server1, not in a Microsoft folder, and export in XML format (can be used to import scheduled tasks)
        Get-ScheduledTasks server1 -recurse -Exclude "\\Microsoft\\" -path 'D:\Scheduled Tasks'

    .NOTES
    
        Properties returned : When they will show up
            ComputerName : All queries
            Name : All queries
            Path : COM object queries, added synthetically if we fail back from COM to SchTasks
            Enabled : COM object queries
            Action : All queries. Schtasks.exe queries include both Action and Arguments in this property
            Arguments : COM object queries
            UserId : COM object queries
            LastRunTime : All queries
            NextRunTime : All queries
            Status : All queries
            Author : All queries
            RunLevel : COM object queries
            Description : COM object queries
            NumberOfMissedRuns : COM object queries

        Thanks to help from Brian Wilhite, Jaap Brasser, and Jan Egil's functions:
            http://gallery.technet.microsoft.com/scriptcenter/Get-SchedTasks-Determine-5e04513f
            http://gallery.technet.microsoft.com/scriptcenter/Get-Scheduled-tasks-from-3a377294
            http://blog.crayon.no/blogs/janegil/archive/2012/05/28/working_2D00_with_2D00_scheduled_2D00_tasks_2D00_from_2D00_windows_2D00_powershell.aspx

    .FUNCTIONALITY
        Computers

    #>

    [cmdletbinding(
        DefaultParameterSetName='COM'
    )]
    param(
        [parameter(
            ValueFromPipeline=$true,
            ValueFromPipelineByPropertyName=$true, 
            ValueFromRemainingArguments=$false, 
            Position=0
        )]
        [Alias("host","server","computer")]
        [string[]]$ComputerName = "localhost",

        [parameter()]
        [string]$folder = "\",

        [parameter(ParameterSetName='COM')]
        [switch]$recurse,

        [parameter(ParameterSetName='COM')]
        [validatescript({
            #Test path if provided, otherwise allow $null
            if($_){
                Test-Path -PathType Container -path $_ 
            }
            else {
                $true
            }
        })]
        [string]$Path = $null,

        [parameter()]
        [string]$Exclude = $null,

        [parameter(ParameterSetName='SchTasks')]
        [switch]$CompatibilityMode
    )
    Begin{

        if(-not $CompatibilityMode){
            $sch = New-Object -ComObject Schedule.Service
        
            #thanks to Jaap Brasser - http://gallery.technet.microsoft.com/scriptcenter/Get-Scheduled-tasks-from-3a377294
            function Get-AllTaskSubFolders {
                [cmdletbinding()]
                param (
                    # Set to use $Schedule as default parameter so it automatically list all files
                    # For current schedule object if it exists.
                    $FolderRef = $sch.getfolder("\"),

                    [switch]$recurse
                )

                #No recurse? Return the folder reference
                if (-not $recurse) {
                    $FolderRef
                }
                #Recurse? Build up an array!
                else {
                    Try{
                        #This will fail on older systems...
                        $folders = $folderRef.getfolders(1)

                        #Extract results into array
                        $ArrFolders = @(
                            if($folders) {
                                foreach ($fold in $folders) {
                                    $fold
                                    if($fold.getfolders(1)) {
                                        Get-AllTaskSubFolders -FolderRef $fold
                                    }
                                }
                            }
                        )
                    }
                    Catch{
                        #If we failed and the expected error, return folder ref only!
                        if($_.tostring() -like '*Exception calling "GetFolders" with "1" argument(s): "The request is not supported.*')
                        {
                            $folders = $null
                            Write-Warning "GetFolders failed, returning root folder only: $_"
                            Return $FolderRef
                        }
                        else{
                            Throw $_
                        }
                    }

                    #Return only unique results
                        $Results = @($ArrFolders) + @($FolderRef)
                        $UniquePaths = $Results | select -ExpandProperty path -Unique
                        $Results | ?{$UniquePaths -contains $_.path}
                }
            } #Get-AllTaskSubFolders
        }

        function Get-SchTasks {
            [cmdletbinding()]
            param([string]$computername, [string]$folder, [switch]$CompatibilityMode)
            
            #we format the properties to match those returned from com objects
            $result = @( schtasks.exe /query /v /s $computername /fo csv |
                convertfrom-csv |
                ?{$_.taskname -ne "taskname" -and $_.taskname -match $( $folder.replace("\","\\") ) } |
                select @{ label = "ComputerName"; expression = { $computername } },
                    @{ label = "Name"; expression = { $_.TaskName } },
                    @{ label = "Action"; expression = {$_."Task To Run"} },
                    @{ label = "LastRunTime"; expression = {$_."Last Run Time"} },
                    @{ label = "NextRunTime"; expression = {$_."Next Run Time"} },
                    "Status",
                    "Author"
            )

            if($CompatibilityMode){
                #User requested compat mode, don't add props
                $result    
            }
            else{
                #If this was a failback, we don't want to affect display of props for comps that don't fail... include empty props expected for com object
                #We also extract task name and path to parent for the Name and Path props, respectively
                foreach($item in $result){
                    $name = @( $item.Name -split "\\" )[-1]
                    $taskPath = $item.name
                    $item | select ComputerName, @{ label = "Name"; expression = {$name}}, @{ label = "Path"; Expression = {$taskPath}}, Enabled, Action, Arguments, UserId, LastRunTime, NextRunTime, Status, Author, RunLevel, Description, NumberOfMissedRuns
                }
            }
        } #Get-SchTasks
    }    
    Process{
        #loop through computers
        foreach($computer in $computername){
        
            #bool in case com object fails, fall back to schtasks
            $failed = $false
        
            write-verbose "Running against $computer"
            Try {
            
                #use com object unless in compatibility mode. Set compatibility mode if this fails
                if(-not $compatibilityMode){      

                    Try{
                        #Connect to the computer
                        $sch.Connect($computer)
                        
                        if($recurse)
                        {
                            $AllFolders = Get-AllTaskSubFolders -FolderRef $sch.GetFolder($folder) -recurse -ErrorAction stop
                        }
                        else
                        {
                            $AllFolders = Get-AllTaskSubFolders -FolderRef $sch.GetFolder($folder) -ErrorAction stop
                        }
                        Write-verbose "Looking through $($AllFolders.count) folders on $computer"
                
                        foreach($fold in $AllFolders){
                
                            #Get tasks in this folder
                            $tasks = $fold.GetTasks(0)
                
                            Write-Verbose "Pulling data from $($tasks.count) tasks on $computer in $($fold.name)"
                            foreach($task in $tasks){
                            
                                #extract helpful items from XML
                                $Author = ([regex]::split($task.xml,'<Author>|</Author>'))[1] 
                                $UserId = ([regex]::split($task.xml,'<UserId>|</UserId>'))[1] 
                                $Description =([regex]::split($task.xml,'<Description>|</Description>'))[1]
                                $Action = ([regex]::split($task.xml,'<Command>|</Command>'))[1]
                                $Arguments = ([regex]::split($task.xml,'<Arguments>|</Arguments>'))[1]
                                $RunLevel = ([regex]::split($task.xml,'<RunLevel>|</RunLevel>'))[1]
                                $LogonType = ([regex]::split($task.xml,'<LogonType>|</LogonType>'))[1]
                            
                                #convert state to status
                                Switch ($task.State) { 
                                    0 {$Status = "Unknown"} 
                                    1 {$Status = "Disabled"} 
                                    2 {$Status = "Queued"} 
                                    3 {$Status = "Ready"} 
                                    4 {$Status = "Running"} 
                                }

                                #output the task details
                                if(-not $exclude -or $task.Path -notmatch $Exclude){
                                    $task | select @{ label = "ComputerName"; expression = { $computer } }, 
                                        Name,
                                        Path,
                                        Enabled,
                                        @{ label = "Action"; expression = {$Action} },
                                        @{ label = "Arguments"; expression = {$Arguments} },
                                        @{ label = "UserId"; expression = {$UserId} },
                                        LastRunTime,
                                        NextRunTime,
                                        @{ label = "Status"; expression = {$Status} },
                                        @{ label = "Author"; expression = {$Author} },
                                        @{ label = "RunLevel"; expression = {$RunLevel} },
                                        @{ label = "Description"; expression = {$Description} },
                                        NumberOfMissedRuns
                            
                                    #if specified, output the results in importable XML format
                                    if($path){
                                        $xml = $task.Xml
                                        $taskname = $task.Name
                                        $xml | Out-File $( Join-Path $path "$computer-$taskname.xml" )
                                    }
                                }
                            }
                        }
                    }
                    Catch{
                        Write-Warning "Could not pull scheduled tasks from $computer using COM object, falling back to schtasks.exe"
                        Try{
                            Get-SchTasks -computername $computer -folder $folder -ErrorAction stop
                        }
                        Catch{
                            Write-Error "Could not pull scheduled tasks from $computer using schtasks.exe:`n$_"
                            Continue
                        }
                    }             
                }

                #otherwise, use schtasks
                else{
                
                    Try{
                        Get-SchTasks -computername $computer -folder $folder -CompatibilityMode -ErrorAction stop
                    }
                     Catch{
                        Write-Error "Could not pull scheduled tasks from $computer using schtasks.exe:`n$_"
                        Continue
                     }
                }

            }
            Catch{
                Write-Error "Error pulling Scheduled tasks from $computer`: $_"
                Continue
            }
        }
    }
}

function Get-SchTasks {
            [cmdletbinding()]
            param([string]$computername, [string]$folder, [switch]$CompatibilityMode)
            
            #we format the properties to match those returned from com objects
            $result = @( schtasks.exe /query /v /s $computername /fo csv |
                convertfrom-csv |
                ?{$_.taskname -ne "taskname" -and $_.taskname -match $( $folder.replace("\","\\") ) } |
                select @{ label = "ComputerName"; expression = { $computername } },
                    @{ label = "Name"; expression = { $_.TaskName } },
                    @{ label = "Action"; expression = {$_."Task To Run"} },
                    @{ label = "LastRunTime"; expression = {$_."Last Run Time"} },
                    @{ label = "NextRunTime"; expression = {$_."Next Run Time"} },
                    "Status",
                    "Author"
            )

            if($CompatibilityMode){
                #User requested compat mode, don't add props
                $result    
            }
            else{
                #If this was a failback, we don't want to affect display of props for comps that don't fail... include empty props expected for com object
                #We also extract task name and path to parent for the Name and Path props, respectively
                foreach($item in $result){
                    $name = @( $item.Name -split "\\" )[-1]
                    $taskPath = $item.name
                    $item | select ComputerName, @{ label = "Name"; expression = {$name}}, @{ label = "Path"; Expression = {$taskPath}}, Enabled, Action, Arguments, UserId, LastRunTime, NextRunTime, Status, Author, RunLevel, Description, NumberOfMissedRuns
                }
            }
        }

function Get-Secure {
    <#
    .SYNOPSIS
        Get a stored credential.
    .DESCRIPTION
        Get a stored credential.
        https://github.com/indented-automation/Indented.Profile/blob/master/Indented.Profile/public/Get-Secure.ps1
    #>


    [CmdletBinding(DefaultParameterSetName = 'Get')]
    param (
        # The name which identifies a credential.
        [Parameter(Mandatory, Position = 1, ValueFromPipeline, ParameterSetName = 'Get')]
        [String]$Name,

        # List all available credentials.
        [Parameter(Mandatory, ParameterSetName = 'List')]
        [Switch]$List,

        # Do not copy the password to the clipboard.
        [Switch]$Clipboard,

        # Store the password in an environment variable instead of returning a credential.
        [Switch]$AsEnvironmentVariable
    )

    begin {
        if ($List) {
            Get-ChildItem $home\Documents\Keys | Select-Object @(
                @{n='Name';e={ $_.BaseName }},
                @{n='Created';e={ $_.CreationTime }}
            )
        }
    }

    process {
        if ($pscmdlet.ParameterSetName -eq 'Get') {
            $path = '{0}\Documents\Keys\{1}.xml' -f $home, $Name
            if (Test-Path $path) {
                $credential = Import-CliXml ('{0}\Documents\Keys\{1}.xml' -f $home, $Name)
                if ($AsEnvironmentVariable) {
                    Set-Item env:$Name -Value $credential.GetNetworkCredential().Password
                } else {
                    if ($Clipboard) {
                        $credential.GetNetworkCredential().Password | Set-Clipboard
                    }
                    $credential
                }
            }
        }
    }
}

function Get-Syntax {
    <#
    .SYNOPSIS
        Get the syntax for a command.
    .DESCRIPTION
        Get the syntax for a command. A wrapper for Get-Command -Syntax.
    .NOTES
        https://github.com/indented-automation/Indented.Profile/blob/master/Indented.Profile/public/Get-Syntax.ps1
    #>


    [CmdletBinding()]
    [Alias('synt', 'syntax')]
    param (
        # The name of a command.
        [Parameter(Mandatory, Position = 1, ValueFromPipelineByPropertyName, ParameterSetName = 'ByName')]
        [String]$Name,

        # A CommandInfo object.
        [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'FromCommandInfo')]
        [System.Management.Automation.CommandInfo]$CommandInfo,

        # Write syntax in the short format used by Get-Command.
        [Switch]$Short
    )

    begin {
        $commonParams = @(
            [System.Management.Automation.Internal.CommonParameters].GetProperties().Name
            [System.Management.Automation.Internal.ShouldProcessParameters].GetProperties().Name
            [System.Management.Automation.Internal.TransactionParameters].GetProperties().Name
        )
    }

    process {
        $CommandInfo = Get-CommandInfo @psboundparameters
        foreach ($parameterSet in $CommandInfo.ParameterSets) {
            if ($Short) {
                "`n{0} {1}" -f $CommandInfo.Name, $parameterSet
            } else {
                $stringBuilder = [System.Text.StringBuilder]::new().AppendFormat('{0} ', $commandInfo.Name)

                $null = foreach ($parameter in $parameterSet.Parameters) {
                    if ($parameter.Name -notin $commonParams) {
                        if (-not $parameter.IsMandatory) {
                            $stringBuilder.Append('[')
                        }

                        if ($parameter.Position -gt [Int32]::MinValue) {
                            $stringBuilder.Append('[')
                        }

                        $stringBuilder.AppendFormat('-{0}', $parameter.Name)

                        if ($parameter.Position -gt [Int32]::MinValue) {
                            $stringBuilder.Append(']')
                        }

                        if ($parameter.ParameterType -ne [Switch]) {
                            $stringBuilder.AppendFormat(' <{0}>', $parameter.ParameterType.Name)
                        }

                        if (-not $parameter.IsMandatory) {
                            $stringBuilder.Append(']')
                        }

                        $stringBuilder.AppendLine().Append(' ' * ($commandInfo.Name.Length + 1))
                    }
                }

                $stringBuilder.AppendLine().ToString()
            }
        }
    }
}

function Get-Username {
    param (
        [String]$OS
    )
    if ($OS -eq "Windows") {
        $env:USERNAME
    }
    else {
        $env:USER
    }
}

function Get-WUCOMHistory {
    Param(
        [Parameter()]
        [String]$ComputerName,
        [Parameter()]
        [PSCredential]$Credential,
        [Parameter()]
        [Switch]$ExcludeAV
    )
    switch ($ExcludeAV.IsPresent) {
        $true {
            $ScriptBlock = {
                $Session = New-Object -ComObject Microsoft.Update.Session
                $Searcher = $Session.CreateUpdateSearcher()
                $HistoryCount = $Searcher.GetTotalHistoryCount()
                $Searcher.QueryHistory(0, $HistoryCount) | Where-Object { $_.Title -notmatch "Definition Update" -And $_.Title -notmatch "Antivirus" }
            }
        }
        $false {
            $ScriptBlock = {
                $Session = New-Object -ComObject Microsoft.Update.Session
                $Searcher = $Session.CreateUpdateSearcher()
                $HistoryCount = $Searcher.GetTotalHistoryCount()
                $Searcher.QueryHistory(0, $HistoryCount)
            }
        }
    }
    $InvokeCommandSplat = @{
        ScriptBlock = $ScriptBlock
    }
    if ($PSBoundParameters.ContainsKey("ComputerName")) {
        $InvokeCommandSplat.Add("ComputerName", $ComputerName)
    }
    if ($PSBoundParameters.ContainsKey("Credential")) {
        $InvokeCommandSplat.Add("Credential", $Credential)
    }
    Invoke-Command @InvokeCommandSplat
}

Function Get-WUEventViewerLogs {
    Param (
        [Parameter()]
        [string]$ComputerName,
        [Parameter()]
        [int]$Days,
        [Parameter()]
        [PSCredential]$Credential,
        [Parameter()]
        [Switch]$ErrorOnly,
        [Parameter()]
        [Switch]$Installs,
        [Parameter()]
        [Switch]$Uninstalls,
        [Parameter()]
        [Switch]$ExcludeAV
    )
    $FilterHashtable = @{
        ProviderName = "Microsoft-Windows-WindowsUpdateClient"
    }
    $GetWinEventSplat = @{
        FilterHashTable = $FilterHashtable
    }
    switch ($true) {
        ($Days -gt 0) {
            $FilterHashtable["StartTime"] = (Get-Date).AddDays(-$Days)
        }
        $ErrorOnly.IsPresent {
            $FilterHashtable["Level"] = 2
        }
        $Installs.IsPresent {
            $FilterHashtable["Id"] = $FilterHashtable["Id"] + @(17,18,19,20,21,22, 43)
        }
        $Uninstalls.IsPresent {
            $FilterHashtable["Id"] = $FilterHashtable["Id"] + @(23,24)
        }
        $PSBoundParameters.ContainsKey("ComputerName") {
            $GetWinEventSplat["ComputerName"] = $ComputerName
        }
        $PSBoundParameters.ContainsKey('Credential') {
            $GetWinEventSplat["Credential"] = $Credential
        }
    }
    if ($ExcludeAV.IsPresent) {
        Get-WinEvent @GetWinEventSplat | Where-Object { $_.Message -notmatch "Definition Update" -And $_.Message -notmatch "Antivirus" }
    }
    else {
        Get-WinEvent @GetWinEventSplat
    }
}

Function Get-WUInstalledUpdates {
    Param(
        [Parameter()]
        [string]$ComputerName,
        [Parameter()]
        [PSCredential]$Credential,
        [Parameter()]
        [Switch]$ResolveKB
    )
    if ($ResolveKB.IsPresent -And (-not(Get-Module kbupdate))) {
        Import-Module "kbupdate" -ErrorAction "Stop"
    }
    $getHotFixSplat = @{
        ErrorAction = "Stop"
    }
    if ($PSBoundParameters.ContainsKey('Credential')) {
        $getHotFixSplat['Credential'] = $Credential
    }
    if ($PSBoundParameters.ContainsKey('ComputerName')) {
        $getHotFixSplat['ComputerName'] = $ComputerName
    }
    $Updates = Get-HotFix @getHotFixSplat
    if ($ResolveKB.IsPresent) {
        $Updates | Select-Object @(
            @{l="Title";e={[String]::Join(", ", (Get-KbUpdate -Pattern $_.HotfixId -Simple).Title)}}
            "Description",
            "HotFixId",
            "InstalledBy",
            @{l="InstalledOn";e={[DateTime]::Parse($_.psbase.properties["installedon"].value,$([System.Globalization.CultureInfo]::GetCultureInfo("en-US")))}}
        )
    }
    else {
        $Updates | Select-Object @(
            "Description",
            "HotFixId",
            "InstalledBy",
            @{l="InstalledOn";e={[DateTime]::Parse($_.psbase.properties["installedon"].value,$([System.Globalization.CultureInfo]::GetCultureInfo("en-US")))}}
        )
    }
}

Function Get-WUWSUSRegKeys {
    Param( 
        [Parameter()]
        [String[]]$ComputerName,
        [Parameter()]
        [PSCredential]$Credential
    )
    $InvokeCommandSplat = @{
        ScriptBlock = {
            [PSCustomObject]@{
                WUServer = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" -Name "WUServer" -ErrorAction "SilentlyContinue").WUServer
                WUStatusServer = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" -Name "WUStatusServer" -ErrorAction "SilentlyContinue").WUStatusServer
                UseWUServer = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -Name "UseWUServer" -ErrorAction "SilentlyContinue").UseWUServer
                ComputerName = $env:COMPUTERNAME
            }
        }
    }
    if ($PSBoundParameters.ContainsKey("Credential")) {
        $InvokeCommandSplat["Credential"] = $Credential
    }
    $Jobs = ForEach($Computer in $ComputerName) {
        if ($PSBoundParameters.ContainsKey("ComputerName")) {
            $InvokeCommandSplat["ComputerName"] = $Computer
        }
        Invoke-Command @InvokeCommandSplat -AsJob
    }
    while (Get-Job -State "Running") {
        $TotalJobs = $Jobs.count
        $NotRunning = $Jobs | Where-Object { $_.State -ne "Running" }
        $Running = $Jobs | Where-Object { $_.State -eq "Running" }
        Write-Progress -Activity "Waiting on results" -Status "$($TotalJobs-$NotRunning.count) Jobs Remaining: $($Running.Location)" -PercentComplete ($NotRunning.count/(0.1+$TotalJobs) * 100)
        Start-Sleep -Seconds 2
    }
    Get-Job | Receive-Job
    Get-Job | Remove-Job
}

function GetScriptOutput([string]$ComputerName, [string]$CommandId)
    {
        $wmiInstance = Get-WmiObject -Class Noxigen_WmiExec -ComputerName $ComputerName -Filter "CommandId = '$CommandId'"
        $result = ($wmiInstance | Select-Object CommandOutput -ExpandProperty CommandOutput)
        $wmiInstance | Remove-WmiObject
        return $result
    }

function Import-CMModule {
    Import-Module ("{0}\..\ConfigurationManager.psd1" -f $ENV:SMS_ADMIN_UI_PATH)
}

function Install-Choco {
    Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
}

Function Invoke-CMClientAction {
    Param( 
        [Parameter()]
        [String[]]$ComputerName,
        [Parameter()]
        [PSCredential]$Credential,
        [Parameter(Mandatory = $true)]
        [ValidateSet('MachinePolicy',
            'DiscoveryData',
            'ComplianceEvaluation',
            'AppDeployment', 
            'HardwareInventory',
            'UpdateDeployment',
            'UpdateScan',
            'SoftwareInventory')]
        [String]$Action
    )
    $ScheduleIDMappings = @{
        'MachinePolicy' = '{00000000-0000-0000-0000-000000000021}'
        'DiscoveryData' = '{00000000-0000-0000-0000-000000000003}'
        'ComplianceEvaluation' = '{00000000-0000-0000-0000-000000000071}'
        'AppDeployment' = '{00000000-0000-0000-0000-000000000121}'
        'HardwareInventory' = '{00000000-0000-0000-0000-000000000001}'
        'UpdateDeployment' = '{00000000-0000-0000-0000-000000000108}'
        'UpdateScan' = '{00000000-0000-0000-0000-000000000113}'
        'SoftwareInventory' = '{00000000-0000-0000-0000-000000000002}'
    }
    $ScheduleID = @{ "sScheduleID" = $ScheduleIDMappings[$Action] }
    $InvokeCommandSplat = @{
        ScriptBlock = {
            Param (
                [Parameter(Mandatory = $true)]
                [hashtable]$ScheduleID
            )
            $Result = @{
                ComputerName = $env:COMPUTERNAME
            }
            Invoke-CimMethod -Namespace "ROOT/CCM" -ClassName "SMS_Client" -MethodName "TriggerSchedule" -Arguments $ScheduleID -ErrorAction "Stop"
            $Result["Result"] = $?
            return [PSCustomObject]$Result
        }
        ArgumentList = $ScheduleID
    }
    if ($PSBoundParameters.ContainsKey("Credential")) {
        $InvokeCommandSplat["Credential"] = $Credential
    }
    $Jobs = ForEach($Computer in $ComputerName) {
        if ($PSBoundParameters.ContainsKey("ComputerName")) {
            $InvokeCommandSplat["ComputerName"] = $Computer
        }
        Invoke-Command @InvokeCommandSplat -AsJob
    }
    while (Get-Job -State "Running") {
        $TotalJobs = $Jobs.count
        $NotRunning = $Jobs | Where-Object { $_.State -ne "Running" }
        $Running = $Jobs | Where-Object { $_.State -eq "Running" }
        Write-Progress -Activity "Waiting on results" -Status "$($TotalJobs-$NotRunning.count) Jobs Remaining: $($Running.Location)" -PercentComplete ($NotRunning.count/(0.1+$TotalJobs) * 100)
        Start-Sleep -Seconds 2
    }
    Get-Job | Receive-Job
    Get-Job | Remove-Job
}

function Main()
    {
        $commandString = $Command
        
        # The GUID from our custom WMI class. Used to get only results for this command.
        $scriptCommandId = CreateScriptInstance $ComputerName
        
        if ($scriptCommandId -eq $null)
        {
            Write-Error "Error creating remote instance."
            exit
        }
        
        # Meanwhile, on the remote machine...
        # 1. Execute the command and store the output as a string
        # 2. Get a reference to our current custom WMI class instance and store the output there!
            
        $encodedCommand = "`$result = Invoke-Command -ScriptBlock {$commandString} | Out-String; Get-WmiObject -Class Noxigen_WmiExec -Filter `"CommandId = '$scriptCommandId'`" | Set-WmiInstance -Arguments `@{CommandOutput = `$result} | Out-Null"
        
        $encodedCommand = [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($encodedCommand))
        
        Write-Host "Running the below command on: $ComputerName..."
        Write-Host $commandString
        
        $result = ExecCommand $ComputerName $encodedCommand
        
        Write-Host "Result..."
        Write-Output $result
    }

function Measure-ChildItem {
    <#
    .SYNOPSIS
        Recursively measures the size of a directory.
    .DESCRIPTION
        Recursively measures the size of a directory.

        Measure-ChildItem uses win32 functions, returning a minimal amount of information to gain speed. Once started, the operation cannot be interrupted by using Control and C. The more items present in a directory structure the longer this command will take.

        This command supports paths longer than 260 characters.
    .EXAMPLE
        Measure-ChildItem

        Get the size of all items within the current directory.
    .EXAMPLE
        Get-ChildItem c:\users | Measure-ChildItem -Unit MB

        Get the size of all child items of c:\users.
    .EXAMPLE
        Measure-ChildItem c:\windows -ValueOnly -Unit GB

        Return the size of the c:\windows directory and return only the size in GB.
    .EXAMPLE
        Get-ChildItem \\server\share -Directory | Measure-ChildItem -Unit TB -Digits 5

        Return the size of all items in a share.
    .NOTES
        Thanks Chris Dent! https://gist.github.com/indented-automation
    #>


    [CmdletBinding()]
    param (
        # The path to measure the size of. Accepts pipeline input. By default the size of the current working directory is measured.
        [Parameter(Position = 1, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [Alias('FullName')]
        [String]$Path = $pwd,

        # The units sizes should be displayed in. By default, sizes are displayed in Bytes.
        [ValidateSet('B', 'KB', 'MB', 'GB', 'TB')]
        [String]$Unit = 'B',

        # When rounding, the number of digits to display after a decimal point. By defaut sizes are rounded to two decimal places.
        [ValidateRange(0, 28)]
        [Int32]$Digits = 2,

        # Return the size value only, discards file, and directory counts and path information.
        [Switch]$ValueOnly
    )

    begin {
        if (-not ('SC.IO.FileSearcher' -as [Type])) {
            Add-Type '
                using System;
                using System.Collections.Generic;
                using System.IO;
                using System.Runtime.InteropServices;

                namespace SC.IO
                {
                    [StructLayout(LayoutKind.Sequential)]
                    public struct FILETIME
                    {
                        public uint dwLowDateTime;
                        public uint dwHighDateTime;
                    };

                    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
                    public struct WIN32_FIND_DATA
                    {
                        public FileAttributes dwFileAttributes;
                        public FILETIME ftCreationTime;
                        public FILETIME ftLastAccessTime;
                        public FILETIME ftLastWriteTime;
                        public int nFileSizeHigh;
                        public int nFileSizeLow;
                        public int dwReserved0;
                        public int dwReserved1;
                        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
                        public string cFileName;
                        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)]
                        public string cAlternate;
                    }

                    public class UnsafeNativeMethods
                    {
                        [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
                        public static extern IntPtr FindFirstFile(string lpFileName, out WIN32_FIND_DATA lpFindFileData);

                        [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
                        public static extern IntPtr FindFirstFileExW(
                            string lpFileName,
                            int fInfoLevelId,
                            out WIN32_FIND_DATA lpFindFileData,
                            int fSearchOp,
                            IntPtr lpSearchFilter,
                            int dwAdditionalFlags
                        );

                        [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
                        public static extern bool FindNextFile(IntPtr hFindFile, out WIN32_FIND_DATA lpFindFileData);

                        [DllImport("kernel32.dll", SetLastError = true)]
                        [return: MarshalAs(UnmanagedType.Bool)]
                        public static extern bool FindClose(IntPtr hFindFile);
                    }

                    public class FileSearcher
                    {
                        private static uint convertToUInt(int value)
                        {
                            return BitConverter.ToUInt32(
                                BitConverter.GetBytes(value),
                                0
                            );
                        }

                        private static long convertToLong(int value)
                        {
                            return (long)(convertToUInt(value) << 32);
                        }

                        public static long[] MeasureItem(string path, bool recurse, long[] itemData)
                        {
                            if (itemData == null)
                            {
                                itemData = new long[]{ 0, 0, 0 };
                            }

                            string searchPath;
                            if (path.StartsWith(@"\\"))
                            {
                                searchPath = String.Format(@"\\?\UNC\{0}\*", path.Substring(2));
                            }
                            else
                            {
                                searchPath = String.Format(@"\\?\{0}\*", path);
                            }

                            WIN32_FIND_DATA findData = new WIN32_FIND_DATA();
                            IntPtr findHandle = UnsafeNativeMethods.FindFirstFileExW(searchPath, 1, out findData, 0, IntPtr.Zero, 0);
                            do
                            {
                                if (findData.dwFileAttributes.HasFlag(FileAttributes.Directory))
                                {
                                    if (recurse && findData.cFileName != "." && findData.cFileName != "..")
                                    {
                                        itemData[2]++;
                                        itemData = MeasureItem(
                                            Path.Combine(path, findData.cFileName),
                                            recurse,
                                            itemData
                                        );
                                    }
                                }
                                else
                                {
                                    itemData[0] += convertToLong(findData.nFileSizeHigh) + (long)convertToUInt(findData.nFileSizeLow);
                                    itemData[1]++;
                                }
                            } while (UnsafeNativeMethods.FindNextFile(findHandle, out findData));
                            UnsafeNativeMethods.FindClose(findHandle);

                            return itemData;
                        }
                    }
                }
            '

        }

        $power = ('B', 'KB', 'MB', 'GB', 'TB').IndexOf($Unit.ToUpper())
        $denominator = [Math]::Pow(1024, $power)
    }

    process {
        $Path = $pscmdlet.GetUnresolvedProviderPathFromPSPath($Path).TrimEnd('\')

        $itemData = [SC.IO.FileSearcher]::MeasureItem($Path, $true, $null)

        if ($ValueOnly) {
            [Math]::Round(($itemData[0] / $denominator), $Digits)
        } else {
            [PSCustomObject]@{
                Path           = $Path
                Size           = [Math]::Round(($itemData[0] / $denominator), $Digits)
                FileCount      = $itemData[1]
                DirectoryCount = $itemData[2]
            }
        }
    }
}

Function New-ModuleDirStructure {
    <#
    .NOTES
        http://ramblingcookiemonster.github.io/Building-A-PowerShell-Module/
    #>

    Param (
        [Parameter(Mandatory)]
        [String]$Path,
        [Parameter(Mandatory)]
        [String]$ModuleName,
        [Parameter(Mandatory)]
        [String]$Author,
        [Parameter(Mandatory)]
        [String]$Description,
        [Parameter()]
        [String]$PowerShellVersion = 5.1
    )

    # Create the module and private function directories
    New-Item -Path $Path\$ModuleName -ItemType Directory -Force
    New-Item -Path $Path\$ModuleName\Private -ItemType Directory -Force
    New-Item -Path $Path\$ModuleName\Public -ItemType Directory -Force
    New-Item -Path $Path\$ModuleName\en-US -ItemType Directory -Force # For about_Help files
    #New-Item -Path $Path\Tests -ItemType Directory -Force

    #Create the module and related files
    New-Item "$Path\$ModuleName\$ModuleName.psm1" -ItemType File -Force
    #New-Item "$Path\$ModuleName\$ModuleName.Format.ps1xml" -ItemType File -Force
    New-Item "$Path\$ModuleName\en-US\about_$ModuleName.help.txt" -ItemType File -Force
    #New-Item "$Path\Tests\$ModuleName.Tests.ps1" -ItemType File -Force
    $NewModuleManifestSplat = @{
        Path                = Join-Path -Path $Path -ChildPath $ModuleName | Join-Path -ChildPath "$ModuleName.psd1"
        RootModule          = $ModuleName.psm1
        Description         = $Description
        PowerShellVersion   = $PowerShellVersion
        Author              = $Author
        # FormatsToProcess = "$ModuleName.Format.ps1xml"
    }
    New-ModuleManifest @NewModuleManifestSplat

    # Copy the public/exported functions into the public folder, private functions into private folder
}

Function New-RebootScheduledTask {
    Param(
        [Parameter()]
        [String]$ComputerName,
        [Parameter(Mandatory)]
        [Datetime]$UTCTime,
        [Parameter(Mandatory)]
        [String]$Description,
        [Parameter()]
        [String]$TaskName = "Itergy - Reboot",
        [Parameter()]
        [String]$TaskPath = "\",
        [Parameter()]
        [PSCredential]$Credential,
        [Parameter()]
        [Switch]$Force
    )

    $GetScheduledTaskSplat = @{
        TaskName = $TaskName
        TaskPath = $TaskPath
        ErrorAction = "SilentlyContinue"
    }

    $GetCimInstanceSplat = @{
        ErrorAction = "Stop"
    }

    if ($PSBoundParameters.ContainsKey("ComputerName")) {
        $NewCimSession = @{
            ComputerName = $ComputerName
            ErrorAction = "Stop"
        }
        if ($PSBoundParameters.ContainsKey("Credential")) {
            $NewCimSession["Credential"] = $Credential
        }
        $Session = New-CimSession @NewCimSession
        $GetCimInstanceSplat["CimSession"] = $Session
        $GetScheduledTaskSplat["CimSession"] = $Session
    }

    if (Get-ScheduledTask @GetScheduledTaskSplat) {
        if ($Force.IsPresent) {
            $UnregisterScheduledTaskSplat = @{
                TaskName = $TaskName
                TaskPath = $TaskPath
                Confirm = $false
                ErrorAction = "Stop"
            } 
            if ($PSBoundParameters.ContainsKey("ComputerName")) {
                $UnregisterScheduledTaskSplat["CimSession"] = $Session
            }
            Unregister-ScheduledTask @UnregisterScheduledTaskSplat
        }
        else {
            Write-Warning "Scheduled task already exists, use -Force to recreate"
            return
        }
    }

    try {
        $TimeZone = Get-CimInstance -ClassName "Win32_TimeZone" @GetCimInstanceSplat
    }
    catch {
        Write-Warning "Could not determine target system's timezone"
    }

    try {
        $LocalTime = Get-CimInstance -ClassName "Win32_LocalTime" @GetCimInstanceSplat
        $LocalTime = Get-Date -Year $LocalTime.year -Month $LocalTime.month -day $LocalTime.day -Hour $LocalTime.hour -Minute $LocalTime.minute -Second $LocalTime.Second
    }
    catch {
        Write-Warning "Could not detemrine target system's local time"
    }

    $Y = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Continue with lab build"
    $N = New-Object System.Management.Automation.Host.ChoiceDescription "&No", "Do not continue with lab build"
    $Options = [System.Management.Automation.Host.ChoiceDescription[]]($Y, $N)
    $Message = "Target machine's timezone is '{0}' ('{1}'), would you like to continue with '{2} UTC?'" -f $TimeZone.Caption, $LocalTime, $UTCTime.ToUniversalTime()
    $Result = $host.ui.PromptForChoice($null, $Message, $Options, 1)

    if ($Result -ge 1) {
        return
    }

    $Description = "{0} - created by {1} on {2} (UTC)" -f $Description, $env:USERNAME, $UTCTime.ToUniversalTime()

    $Action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument '-NonInteractive -NoLogo -NoProfile -Command "Restart-Computer -Force"'
    $Trigger = New-ScheduledTaskTrigger -Once -At $UTCTime.ToUniversalTime()
    $Settings = New-ScheduledTaskSettingsSet
    $Task = New-ScheduledTask -Action $Action -Trigger $Trigger -Settings $Settings -Description $Description
    Register-ScheduledTask -TaskName $TaskName -TaskPath $TaskPath -InputObject $Task -User "System" -CimSession $Session

    if ($Session) { Remove-CimSession -CimSession $Session }
}

Function New-Shortcut {
        Param(
            [Parameter(Mandatory=$true)]
            [string]$Target,
            [Parameter(Mandatory=$false)]
            [string]$TargetArguments,
            [Parameter(Mandatory=$true)]
            [string]$ShortcutName
        )
        $Path = Join-Path -Path ([System.Environment]::GetFolderPath("Desktop")) -ChildPath $ShortcutName
        switch ($ShortcutName.EndsWith(".lnk")) {
            $false {
                $ShortcutName = $ShortcutName + ".lnk"
            }
        }
        switch (Test-Path -LiteralPath $Path) {
            $true {
                Write-Warning ("Shortcut already exists: {0}" -f (Split-Path $Path -Leaf))
            }
            $false {
                $WshShell = New-Object -comObject WScript.Shell
                $Shortcut = $WshShell.CreateShortcut($Path)
                $Shortcut.TargetPath = $Target
                If ($null -ne $TargetArguments) {
                    $Shortcut.Arguments = $TargetArguments
                }
                $Shortcut.Save()
            }
        }
    }

function Read-Log {
            param (
                [Parameter(Mandatory = $true)][ValidateSet ('Success', 'Fail')][string]$status
            )        

            Switch ($status) {
                'Success' { $Pattern = 'Win32 Code 0'; $Regex = '\<\!\[LOG.*\((?<Message>\w+|.*)\).*\]LOG]\!\>\<time=\"(?<Time>.{12}).*date=\"(?<Date>.{10})' }
                'Fail' { $Pattern = 'Failed to run the action'; $Regex = '.*:\s(?<Message>.*|.*\n.*)\]\w+\].{3}time\S{2}(?<Time>.{12}).*date\S{2}(?<Date>.{10})' }
            }

            Get-Content $file | Select-String -Pattern $Pattern -Context 1| ForEach-Object {
                $_ -match $Regex | Out-Null

                [PSCustomObject]@{
                    Computer = $Computer
                    Time     = [datetime]::ParseExact($("$($matches.date) $($matches.time)"), "MM-dd-yyyy HH:mm:ss.fff", $null)
                    Message  = $Matches.Message
                    File     = $File
                }
            } | Format-Table -AutoSize
        }

Function Read-SMSTSLog {
    <#
    .DESCRIPTION
    Parses through the SMSTS log and returns the steps which succeeded, steps which failed, comptuer name, timestamp, and log name.
    .PARAMETER Computer
    To view a remote machine's logs use this parameter to define the machine name.
    .PARAMETER Path
    If for some reason the SMSTS log is not located in C:\Windows\CCM\Logs, use this parameter to define the folder path (Accepts UNC Paths)

    .NOTES
    Version: 1.0
    Author: Amar Rathore
    Creation Date: 2019-03-25

    .EXAMPLE
    Read-SMSTSLog.ps1 -Computer PC01

    .EXAMPLE
    Read-SMSTSLog.ps1 -Path X:\Windows\Temp\
    #>


    [CmdletBinding()]

    Param (    
        [string]$Computer = "$env:ComputerName",
        [string]$Path = "\\$computer\c$\Windows\CCM\Logs",
        [pscredential]$Credential
    )

    If (Test-Connection $Computer -Count 1 -ErrorAction  SilentlyContinue) {

        function Read-Log {
            param (
                [Parameter(Mandatory = $true)][ValidateSet ('Success', 'Fail')][string]$status
            )        

            Switch ($status) {
                'Success' { $Pattern = 'Win32 Code 0'; $Regex = '\<\!\[LOG.*\((?<Message>\w+|.*)\).*\]LOG]\!\>\<time=\"(?<Time>.{12}).*date=\"(?<Date>.{10})' }
                'Fail' { $Pattern = 'Failed to run the action'; $Regex = '.*:\s(?<Message>.*|.*\n.*)\]\w+\].{3}time\S{2}(?<Time>.{12}).*date\S{2}(?<Date>.{10})' }
            }

            Get-Content $file | Select-String -Pattern $Pattern -Context 1| ForEach-Object {
                $_ -match $Regex | Out-Null

                [PSCustomObject]@{
                    Computer = $Computer
                    Time     = [datetime]::ParseExact($("$($matches.date) $($matches.time)"), "MM-dd-yyyy HH:mm:ss.fff", $null)
                    Message  = $Matches.Message
                    File     = $File
                }
            } | Format-Table -AutoSize
        }

        If ($PSBoundParameters.ContainsKey('Credential')) {
        New-PSDrive -Name $Computer -PSProvider FileSystem -Root "\\$Computer\c$" -Credential $Credential -ErrorAction Stop | Out-Null
        }

        If (Test-Path $path){
            $smstslog = (Get-ChildItem $path -Recurse -File | Where-Object {$_.Name -match "Smsts"}).FullName
        }
        Else {
        Write-Host "Unable to connect to $Path.`nIf you're attempting to connect to a remote machine try using the UNC path" -ForegroundColor Red -BackgroundColor Black
        return
        }

        $s = ForEach ($file in $smstslog) {Read-log -status 'Success'}
        $f = ForEach ($file in $smstslog) {Read-log -status 'Fail'}

        If ($s) {Write-host "`nCompleted the following steps:" -ForegroundColor Green -BackgroundColor Black; $s}
        If ($f) {Write-host 'Failed the following steps:' -ForegroundColor Red -BackgroundColor Black; $f}

        If (Get-PSDrive -Name $Computer) { 
        try {
            Remove-PSDrive -Name $Computer -ErrorAction Stop
        }
        catch {
            Write-Warning "Failed to remove PS drive `"${Computer}:`""
        }
        }
    }
    Else {
        Write-host "$Computer is offline/unreachable" -ForegroundColor Red -BackgroundColor Black
    }
}

Function Remove-WUWSUSRegKeys {
    [CmdletBinding()]
    Param( 
        [Parameter(ValueFromPipelineByPropertyName)]
        [Alias('PSComputerName')]
        [String[]]$ComputerName,
        [Parameter()]
        [PSCredential]$Credential
    )
    Begin {
        [System.Collections.Generic.List[Object]]$Jobs = @{}
    }
    Process {
        $InvokeCommandSplat = @{
            ScriptBlock = {
                $Result = [Ordered]@{}
                if ((Test-Path -LiteralPath "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate") -ne $true) {
                    throw "WindowsUpdate registry key does not exist"
                }
                if ((Test-Path -LiteralPath "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU") -ne $true) {
                    throw "WindowsUpdate registry key does not exist"
                }
                Remove-ItemProperty -LiteralPath "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" -Name "WUServer" -ErrorAction "SilentlyContinue"
                $Result["WUServer"] = $?
                Remove-ItemProperty -LiteralPath "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" -Name "WUStatusServer" -ErrorAction "SilentlyContinue"
                $Result["WUStatusServer"] = $?
                New-ItemProperty -LiteralPath "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -Name "UseWUServer" -Value 0 -PropertyType "DWord" -Force -ErrorAction "SilentlyContinue"| Out-Null
                $Result["UseWUServer"] = $?
                Get-Service -Name "wuauserv" | Restart-Service -Force -ErrorAction "SilentlyContinue"
                $Result["RestartWU"] = $?
                $Result["ComputerName"] = $env:COMPUTERNAME
                [PSCustomObject]$Result
            }
            ComputerName = $ComputerName
        }
        if ($PSBoundParameters.ContainsKey("Credential")) {
            $InvokeCommandSplat["Credential"] = $Credential
        }
        $Jobs.Add((Invoke-Command @InvokeCommandSplat -AsJob))
    }
    End {
        while (Get-Job -State "Running") {
            $TotalJobs = $Jobs.count
            $NotRunning = $Jobs | Where-Object { $_.State -ne "Running" }
            $Running = $Jobs | Where-Object { $_.State -eq "Running" }
            Write-Progress -Activity "Waiting on results" -Status "$($TotalJobs-$NotRunning.count) Jobs Remaining: $($Running.Location)" -PercentComplete ($NotRunning.count/(0.1+$TotalJobs) * 100)
            Start-Sleep -Seconds 2
        }
        Get-Job | Receive-Job
        Get-Job | Remove-Job
    }    
}

Function Reset-CMClientPolicy {
    param (
        [CimSession]$CimSession
    )

    $InvokeCimMethodSplat = @{
        ClassName    = "SMS_Client"
        Namespace    = "root\ccm"
        Name         = "ResetPolicy"
        ArgumustList = 1
    }   

    if ($PSBoundParameters.ContainsKey("CimSession")) {
        $InvokeCimMethodSplat["CimSession"] = $CimSession
    }

    Invoke-CimMethod @CimSession
}

Function Search-History {
    Param(
        [Parameter(Mandatory=$true)]
        [string]$String
    )
    Get-Content (Get-PSReadlineOption).HistorySavePath | Where-Object { $_ -like ("*{0}*" -f $string) -and $_ -notlike "Search-History*" } | Select-Object -Unique
}

Function Set-CMShortcuts { 
    [Alias("setmeup")]
    param ()

    Function Add-FileAssociation {
        <#
        .SYNOPSIS
        Set user file associations
        .DESCRIPTION
        Define a program to open a file extension
        .PARAMETER Extension
        The file extension to modify
        .PARAMETER TargetExecutable
        The program to use to open the file extension
        .PARAMETER ftypeName
        Non mandatory parameter used to override the created file type handler value
        .EXAMPLE
        $HT = @{
            Extension = '.txt'
            TargetExecutable = "C:\Program Files\Notepad++\notepad++.exe"
        }
        Add-FileAssociation @HT
        .EXAMPLE
        $HT = @{
            Extension = '.xml'
            TargetExecutable = "C:\Program Files\Microsoft VS Code\Code.exe"
            FtypeName = 'vscode'
        }
        Add-FileAssociation @HT
        .NOTES
        Found here: https://gist.github.com/p0w3rsh3ll/c64d365d15f6f39116dba1a26981dc68#file-add-fileassociation-ps1 https://p0w3rsh3ll.wordpress.com/2018/11/08/about-file-associations/
        #>

        [CmdletBinding()]
        Param(
            [Parameter(Mandatory=$true)]
            [ValidateNotNullOrEmpty()]
            [ValidatePattern('^\.[a-zA-Z0-9]{1,3}')]
            $Extension,
            [Parameter(Mandatory=$true)]
            [ValidateNotNullOrEmpty()]
            [ValidateScript({
                Test-Path -Path $_ -PathType Leaf
            })]
            [string]$TargetExecutable,
            [string]$ftypeName
        )
        Begin {
            $ext = [Management.Automation.Language.CodeGeneration]::EscapeSingleQuotedStringContent($Extension)
            $exec = [Management.Automation.Language.CodeGeneration]::EscapeSingleQuotedStringContent($TargetExecutable)
        
            # 2. Create a ftype
            if (-not($PSBoundParameters['ftypeName'])) {
                $ftypeName = '{0}{1}File'-f $($ext -replace '\.',''),
                $((Get-Item -Path "$($exec)").BaseName)
                $ftypeName = [Management.Automation.Language.CodeGeneration]::EscapeFormatStringContent($ftypeName)
            } else {
                $ftypeName = [Management.Automation.Language.CodeGeneration]::EscapeSingleQuotedStringContent($ftypeName)
            }
            Write-Verbose -Message "Ftype name set to $($ftypeName)"
        }
        Process {
            # 1. remove anti-tampering protection if required
            if (Test-Path -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\$($ext)") {
                $ParentACL = Get-Acl -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\$($ext)"
                if (Test-Path -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\$($ext)\UserChoice") {
                    $k = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey("Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\$($ext)\UserChoice",'ReadWriteSubTree','TakeOwnership')
                    $acl  = $k.GetAccessControl()
                    $null = $acl.SetAccessRuleProtection($false,$true)
                    $rule = New-Object System.Security.AccessControl.RegistryAccessRule ($ParentACL.Owner,'FullControl','Allow')
                    $null = $acl.SetAccessRule($rule)
                    $rule = New-Object System.Security.AccessControl.RegistryAccessRule ($ParentACL.Owner,'SetValue','Deny')
                    $null = $acl.RemoveAccessRule($rule)
                    $null = $k.SetAccessControl($acl)
                    Write-Verbose -Message 'Removed anti-tampering protection'
                }
            }
            # 2. add a ftype
            $null = & (Get-Command "$($env:systemroot)\system32\reg.exe") @(
                'add',
                "HKCU\Software\Classes\$($ftypeName)\shell\open\command"
                '/ve','/d',"$('\"{0}\" \"%1\"'-f $($exec))",
                '/f','/reg:64'
            )
            Write-Verbose -Message "Adding command under HKCU\Software\Classes\$($ftypeName)\shell\open\command"
            # 3. Update user file association

            # Reg2CI (c) 2019 by Roger Zander
            $eap = "SilentlyContinue"
            Remove-Item -LiteralPath ("HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\{0}\OpenWithList" -f $ext) -Force
            if((Test-Path -LiteralPath ("HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\{0}\OpenWithList" -f $ext)) -ne $true) { 
                New-Item ("HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\{0}\OpenWithList" -f $ext) -Force -ErrorAction $eap | Out-Null
            }
            Remove-Item -LiteralPath ("HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\{0}\OpenWithProgids" -f $ext) -Force
            if((Test-Path -LiteralPath ("HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\{0}\OpenWithProgids" -f $ext)) -ne $true) { 
                New-Item ("HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\{0}\OpenWithProgids" -f $ext) -Force -ErrorAction $eap | Out-Null
            }
            if((Test-Path -LiteralPath ("HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\{0}\UserChoice" -f $ext)) -ne $true) {
                New-Item ("HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\{0}\UserChoice" -f $ext) -Force -ErrorAction $eap | Out-Null
            }
            New-ItemProperty -LiteralPath ("HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\{0}\OpenWithList" -f $ext) -Name "MRUList" -Value "a" -PropertyType String -Force -ErrorAction $eap | Out-Null
            New-ItemProperty -LiteralPath ("HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\{0}\OpenWithList" -f $ext) -Name "a" -Value ("{0}" -f (Get-Item -Path $exec | Select-Object -ExpandProperty Name)) -PropertyType String -Force -ErrorAction $eap | Out-Null
            New-ItemProperty -LiteralPath ("HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\{0}\OpenWithProgids" -f $ext) -Name $ftypeName -Value (New-Object Byte[] 0) -PropertyType None -Force -ErrorAction $eap | Out-Null
            Remove-ItemProperty -LiteralPath ("HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\{0}\UserChoice" -f $ext) -Name "Hash" -Force -ErrorAction $eap
            Remove-ItemProperty -LiteralPath ("HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\{0}\UserChoice" -f $ext) -Name "Progid" -Force  -ErrorAction $eap
        }
    }

    Function New-Shortcut {
        Param(
            [Parameter(Mandatory=$true)]
            [string]$Target,
            [Parameter(Mandatory=$false)]
            [string]$TargetArguments,
            [Parameter(Mandatory=$true)]
            [string]$ShortcutName
        )
        $Path = Join-Path -Path ([System.Environment]::GetFolderPath("Desktop")) -ChildPath $ShortcutName
        switch ($ShortcutName.EndsWith(".lnk")) {
            $false {
                $ShortcutName = $ShortcutName + ".lnk"
            }
        }
        switch (Test-Path -LiteralPath $Path) {
            $true {
                Write-Warning ("Shortcut already exists: {0}" -f (Split-Path $Path -Leaf))
            }
            $false {
                $WshShell = New-Object -comObject WScript.Shell
                $Shortcut = $WshShell.CreateShortcut($Path)
                $Shortcut.TargetPath = $Target
                If ($null -ne $TargetArguments) {
                    $Shortcut.Arguments = $TargetArguments
                }
                $Shortcut.Save()
            }
        }
    }

    switch (Test-Path -LiteralPath ("{0}\CCM" -f $env:windir)) {
        $true {
            $client = $true
        }
        $false {
            $client = $false
        }
    }
    try {
        switch ($true) {
            (Test-Path -LiteralPath ("{0}\CCM\CMTrace.exe" -f $env:windir)) {
                $CMTracePath = ("{0}\CCM\CMTrace.exe" -f $env:windir)
                break
            }
            (Test-Path -LiteralPath ("{0}\CMTrace.exe" -f [System.Environment]::GetFolderPath("MyDocuments"))) {
                $CMTracePath = ("{0}\CMTrace.exe" -f [System.Environment]::GetFolderPath("MyDocuments"))
                break
            }
            default {
                $CMTracePath = ("{0}\CMTrace.exe" -f [System.Environment]::GetFolderPath("MyDocuments"))
                $Hash = "81F725E8A89A87A1D4A4487905381EE173C2AF54511A47E659ABC2D56DFFB6F9"
                Invoke-WebRequest -Uri "https://www.cookadam.co.uk/scripts/CMTrace.exe" -OutFile $CMTracePath -ErrorAction Stop
                If ((Get-FileHash -LiteralPath $CMTracePath -Algorithm SHA256 | Select-Object -ExpandProperty Hash) -ne $Hash) {
                    Throw "Hash mismatch from download"
                }
            }
        }

        switch ($true) {
            (Test-Path -LiteralPath ("{0}\Programs\Microsoft System Center\Configuration Manager\Software Center.lnk" -f [Environment]::GetFolderPath("CommonStartMenu"))) {
                $SoftwareCenter = "{0}\Programs\Microsoft System Center\Configuration Manager\Software Center.lnk" -f [Environment]::GetFolderPath("CommonStartMenu")
            }
            (Test-Path -LiteralPath ("{0}\Programs\Microsoft Endpoint Manager\Configuration Manager\Software Center.lnk" -f [Environment]::GetFolderPath("CommonStartMenu"))) {
                $SoftwareCenter = "{0}\Programs\Microsoft Endpoint Manager\Configuration Manager\Software Center.lnk" -f [Environment]::GetFolderPath("CommonStartMenu")
            }
        }

        Add-FileAssociation -Extension ".log" -TargetExecutable $CMTracePath
        Add-FileAssociation -Extension ".lo_" -TargetExecutable $CMTracePath
        
        switch ($client) {  
            $true {
                New-Shortcut -Target ("{0}\System32\control.exe" -f $env:windir) -TargetArguments "smscfgrc" -ShortcutName "smscfgrc.lnk"
                New-Shortcut -Target $SoftwareCenter -ShortcutName "Software Center.lnk"
                New-Shortcut -Target ("{0}\CCM" -f $env:windir) -ShortcutName "CCM.lnk"
                New-Shortcut -Target ("{0}\ccmsetup" -f $env:windir) -ShortcutName "ccmsetup.lnk"
                New-Shortcut -Target ("{0}\ccmcache" -f $env:windir) -ShortcutName "ccmcache.lnk"
            }
            $false {
                # tbc
            }
        }

        switch($null -ne $ENV:SMS_ADMIN_UI_PATH) {
            $true {
                New-Shortcut -Target ("{0}\Programs\Microsoft System Center\Configuration Manager\Configuration Manager Console.lnk" -f [Environment]::GetFolderPath("CommonStartMenu")) -ShortcutName "Configuration Manager Console.lnk"
            }
        }

    }
    catch {
        Write-Host "Error: " -ForegroundColor Red -NoNewline
        Write-Host $_.Exception.Message -NoNewline
        Write-Host (" (line {0})" -f $_.InvocationInfo.ScriptLineNumber)
    }
}

function Set-GitConfig {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [String]$WorkEmail,
        [Parameter(Mandatory)]
        [String]$PersonalEmail,
        [Parameter()]
        [String]$gitconfig = "{0}\.gitconfig" -f $HOME,
        [Parameter()]
        [String]$gitconfigwork = "{0}\.gitconfig-work" -f $HOME,
        [Parameter()]
        [String]$gitconfigpersonal = "{0}\.gitconfig-personal" -f $HOME
    )

    $content = @(
        [ordered]@{
            "title" = "includeIf `"gitdir/i:C:/gitpersonal/`""
            "path" = Split-Path $gitconfigpersonal -Leaf
        },
        [ordered]@{
            "title" = "includeIf `"gitdir/i:C:/gitwork/`""       
            "path" = Split-Path $gitconfigwork -Leaf
        }
    )

    ConvertTo-Ini -Content $content -SectionTitleKeyName "title" | Add-Content -Path $gitconfig -Force

    $content = [ordered]@{
        "title" = "user"
        "name" = "Adam Cook"
        "email" = $WorkEmail
    }

    ConvertTo-Ini -Content $content -SectionTitleKeyName "title" | Set-Content -Path $gitconfigwork -Force
    
    $content = [ordered]@{
        "title" = "user"
        "name" = "codaamok"
        "email" = $PersonalEmail
    }

    ConvertTo-Ini -Content $content -SectionTitleKeyName "title" | Set-Content -Path $gitconfigpersonal -Force
}

function Set-Secure {
    <#
    .SYNOPSIS
        Store a credential.
    .DESCRIPTION
        Store a credential in an xml file created
        https://github.com/indented-automation/Indented.Profile/blob/master/Indented.Profile/public/Set-Secure.ps1
    #>


    [CmdletBinding(SupportsShouldProcess)]
    param (
        # The name of the credential to set.
        [Parameter(Mandatory, Position = 1)]
        [String]$Name
    )

    $path = '{0}\Documents\Keys\{1}.xml' -f $home, $Name
    $folder = Split-Path -Path $path -Parent
    if (Test-Path $path) {
        $credential = Get-Credential (Get-Secure $Name).Username
    } else {
        if (-not(Test-Path $folder)) {
            New-Item -Path $folder -ItemType "Directory" -ErrorAction Stop
        }
        $credential = Get-Credential
        if ($null -eq $credential) { return }
    }
    $credential | Export-CliXml $path
}

Function Shamefully-ClearSoftwareDistributionFolder {
    Param( 
        [Parameter()]
        [String[]]$ComputerName,
        [Parameter()]
        [PSCredential]$Credential
    )
    $InvokeCommandSplat = @{
        ScriptBlock = {
            $Result = @{
                ComputerName = $env:COMPUTERNAME
            }
            try {
                Get-Service -Name "bits","Windows Update" -ErrorAction "Stop" | Stop-Service -Force -ErrorAction "Stop"
                Start-Process "ipconfig" -ArgumentList "/flushdns" -ErrorAction "Stop"

                $path = "{0}\Microsoft\Network\Downloader" -f $env:ProgramData
                Get-ChildItem -Path $path | ForEach-Object {
                    if ($_.FullName -notmatch "\.log$|\.old$") {
                        Move-Item -LiteralPath $_.FullName -Destination ($_.FullName + ".old") -Force -ErrorAction "Stop"
                    }
                }

                $path = "{0}\SoftwareDistribution" -f $env:windir
                Move-Item -LiteralPath $path -Destination ("{0}.old" -f $path) -Force -ErrorAction "Stop"

                Get-Service -Name "bits","Windows Update" -ErrorAction "Stop" | Start-Service -ErrorAction "Stop"
                $Result["Result"] = "Success"
            }
            catch {
                $Result["Result"] = $error[0].Exception.Message
            }
            [PSCustomObject]$Result
        }
    }
    if ($PSBoundParameters.ContainsKey("Credential")) {
        $InvokeCommandSplat["Credential"] = $Credential
    }
    $Jobs = ForEach ($Computer in $ComputerName) {
        if ($PSBoundParameters.ContainsKey("ComputerName")) {
            $InvokeCommandSplat["ComputerName"] = $Computer
        }
        Invoke-Command @InvokeCommandSplat -AsJob
    }
    while (Get-Job -State "Running") {
        $TotalJobs = $Jobs.count
        $NotRunning = $Jobs | Where-Object { $_.State -ne "Running" }
        $Running = $Jobs | Where-Object { $_.State -eq "Running" }
        Write-Progress -Activity "Waiting on results" -Status "$($TotalJobs-$NotRunning.count) Jobs Remaining: $($Running.Location)" -PercentComplete ($NotRunning.count/(0.1+$TotalJobs) * 100)
        Start-Sleep -Seconds 2
    }
    Receive-Job -Job $Jobs
    Remove-Job -Job $Jobs 
    
}

Function Shamefully-ResetBITS {
    Param( 
        [Parameter()]
        [String[]]$ComputerName,
        [Parameter()]
        [PSCredential]$Credential
    )
    $InvokeCommandSplat = @{
        ScriptBlock = {
            $Result = @{
                ComputerName = $env:COMPUTERNAME
            }
            try {
                Get-Service -Name "bits" -ErrorAction "Stop" | Stop-Service -Force -ErrorAction "Stop" -WarningAction "SilentlyContinue"
                Start-Process "ipconfig" -ArgumentList "/flushdns" -ErrorAction "Stop"
                $path = "{0}\Microsoft\Network\Downloader" -f $env:ProgramData
                Get-ChildItem -Path $path | ForEach-Object {
                    if ($_.FullName -notmatch "\.log$|\.old$") {
                        Move-Item -LiteralPath $_.FullName -Destination ($_.FullName + ".old") -Force -ErrorAction "Stop"
                    }
                }
                Get-Service -Name "bits" -ErrorAction "Stop" | Start-Service -ErrorAction "Stop"
                $Result["Result"] = "Success"
            }
            catch {
                $Result["Result"] = $error[0].Exception.Message
            }
            [PSCustomObject]$Result
        }
    }
    if ($PSBoundParameters.ContainsKey("Credential")) {
        $InvokeCommandSplat["Credential"] = $Credential
    }
    $Jobs = ForEach ($Computer in $ComputerName) {
        if ($PSBoundParameters.ContainsKey("ComputerName")) {
            $InvokeCommandSplat["ComputerName"] = $Computer
        }
        Invoke-Command @InvokeCommandSplat -AsJob
    }
    while (Get-Job -State "Running") {
        $TotalJobs = $Jobs.count
        $NotRunning = $Jobs | Where-Object { $_.State -ne "Running" }
        $Running = $Jobs | Where-Object { $_.State -eq "Running" }
        Write-Progress -Activity "Waiting on results" -Status "$($TotalJobs-$NotRunning.count) Jobs Remaining: $($Running.Location)" -PercentComplete ($NotRunning.count/(0.1+$TotalJobs) * 100)
        Start-Sleep -Seconds 2
    }
    Receive-Job -Job $Jobs
    Remove-Job -Job $Jobs   
}

function Show-JobProgress {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory,ValueFromPipeline)]
        [ValidateNotNullOrEmpty()]
        [System.Management.Automation.Job[]]
        $Job
        ,
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [scriptblock]
        $FilterScript
    )

    Process {
        $Job.ChildJobs | ForEach-Object {
            if (-not $_.Progress) {
                return
            }

            $LastProgress = $_.Progress
            if ($FilterScript) {
                $LastProgress = $LastProgress | Where-Object -FilterScript $FilterScript
            }

            $LastProgress | Group-Object -Property Activity,StatusDescription | ForEach-Object {
                $_.Group | Select-Object -Last 1

            } | ForEach-Object {
                $ProgressParams = @{}
                if ($_.Activity          -and $_.Activity          -ne $null) { $ProgressParams.Add('Activity',         $_.Activity) }
                if ($_.StatusDescription -and $_.StatusDescription -ne $null) { $ProgressParams.Add('Status',           $_.StatusDescription) }
                if ($_.CurrentOperation  -and $_.CurrentOperation  -ne $null) { $ProgressParams.Add('CurrentOperation', $_.CurrentOperation) }
                if ($_.ActivityId        -and $_.ActivityId        -gt -1)    { $ProgressParams.Add('Id',               $_.ActivityId) }
                if ($_.ParentActivityId  -and $_.ParentActivityId  -gt -1)    { $ProgressParams.Add('ParentId',         $_.ParentActivityId) }
                if ($_.PercentComplete   -and $_.PercentComplete   -gt -1)    { $ProgressParams.Add('PercentComplete',  $_.PercentComplete) }
                if ($_.SecondsRemaining  -and $_.SecondsRemaining  -gt -1)    { $ProgressParams.Add('SecondsRemaining', $_.SecondsRemaining) }

                Write-Progress @ProgressParams
            }
        }
    }
}

Function Start-SetupDiag {
    param (
        [Parameter(Mandatory=$true,ValueFromPipeline=$true)]
        [Alias('Computer', 'PSComputerName', 'Name', 'HostName')]
        [String[]]$ComputerName,
        [Parameter(Mandatory=$false)]
        [PSCredential]$Credential
    )

    $InvokeCommandSplat = @{
        ComputerName    = $ComputerName
        ScriptBlock     = {
            New-Item -ItemType Directory -Path $env:SystemDrive\SetupDiag
            Invoke-WebRequest -Uri "https://go.microsoft.com/fwlink/?linkid=870142" -OutFile $env:SystemDrive\SetupDiag\SetupDiag.exe
            $path = "{0}\SetupDiag" -f $env:SystemDrive; Start-Process -FilePath $path\SetupDiag.exe -ArgumentList @("/Output:$path\Results.log") -Wait
            $path = "{0}\SetupDiag" -f $env:SystemDrive; Get-Content -Path $path\results.log
        }
    }

    if ($PSBoundParameters.ContainsKey('Credential')) {
        $InvokeCommandSplat.Add('Credential', $Credential)
    }

    Invoke-Command @InvokeCommandSplat
}

Function Unzip {
        Param(
            [string]$zipfile, 
            [string]$outpath
        )
        [System.IO.Compression.ZipFile]::ExtractToDirectory($zipfile, $outpath)
    }

Function Update-Profile {
    try {
        $R = Invoke-WebRequest https://www.cookadam.co.uk/profile -OutFile $profile.CurrentUserAllHosts -PassThru -ErrorAction Stop
    }
    catch {
        Write-Host "Error: " -ForegroundColor Red -NoNewline
        Write-Host $Error[0].Exception.Message
    }
    If ($R.StatusCode -eq 200) {
        '. $profile.CurrentUserAllHosts' | clip
        Write-Host "Paste your clipboard"
    }
}

function Update-RoyalTSPortable {
    [CmdletBinding()]
    param (
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [String]$Path = "{0}\RoyalTS" -f [Environment]::GetFolderPath("MyDocuments")
    )

    if (-not (Test-Path $Path)) {
        Write-Warning ("Path '{0}' does not exist" -f $Path)
        $Y = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Create directory and download RoyalTS"
        $N = New-Object System.Management.Automation.Host.ChoiceDescription "&No", "Do not create directory and do not download RoyalTS"
        $Options = [System.Management.Automation.Host.ChoiceDescription[]]($Y, $N)
        $Question = "Would you like to continue?"
        $Message = "Creating directory '{0}'" -f $Path
        $Result = $host.ui.PromptForChoice($Question, $Message, $Options, 1)

        if ($Result -ge 1) {
            return
        }

        New-Item -ItemType "Container" -Path $Path -Force -ErrorAction "Stop"
    }

    $URL = (Invoke-WebRequest -Uri "https://www.royalapps.com/ts/win/download" | Select-Object -ExpandProperty links) -match "\.zip" | Select-Object -ExpandProperty href

    if ($URL) {
        $FileName = ($URL -split "/")[-1]
        $File = ("{0}\{1}" -f $env:temp, $FileName)
        (New-Object System.Net.WebClient).DownloadFile($URL, $File)
    }

    if (Get-Process -Name "RoyalTS" -ErrorAction "SilentlyContinue") {
        Write-Warning "RoyalTS is running"
        $Y = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Stop RoyalTS process"
        $N = New-Object System.Management.Automation.Host.ChoiceDescription "&No", "Do not stop RoyalTS process"
        $Options = [System.Management.Automation.Host.ChoiceDescription[]]($Y, $N)
        $Question = "Would you like to continue?"
        $Message = "Stop RoyalTS process"
        $Result = $host.ui.PromptForChoice($Question, $Message, $Options, 1)

        if ($Result -ge 1) {
            return
        }

        Stop-Process -Name "RoyalTS" -ErrorAction "Stop"
    }

    Expand-Archive -Path $File -DestinationPath $Path -Force

    Remove-Item -Path $File
}

Function Upload-Profile {
    <#
    .NOTES
    https://winscp.net/eng/docs/library_powershell
    #>

    Param(
        [Parameter(Mandatory=$true,Position=0)]
        [string]$user,
        [Parameter(Mandatory=$true,Position=1)]
        [string]$pass
    )

    switch ($true) {
        ((Test-Path -LiteralPath ("{0}\WinSCPnet.dll" -f ${env:ProgramFiles(x86)})) -And (Test-Path -LiteralPath ("{0}\WinSCP.exe" -f ${env:ProgramFiles(x86)}))) {
            $dllPath = ("{0}\WinSCPnet.dll" -f ${env:ProgramFiles(x86)})
            break
        }
        ((Test-Path -LiteralPath ("{0}\WinSCPnet.dll" -f $env:ProgramFiles)) -And (Test-Path -LiteralPath ("{0}\WinSCP.exe" -f $env:ProgramFiles))) {
            # Unlikely as WinSCP only 32bit and I don't think I ever touch 32bit systems
            $dllPath = ("{0}\WinSCPnet.dll" -f $env:ProgramFiles)
            break
        }
        ((Test-Path -LiteralPath ("{0}\WinSCP\WinSCPnet.dll" -f [System.Environment]::GetFolderPath("MyDocuments"))) -And (Test-Path -LiteralPath ("{0}\WinSCP\WinSCP.exe" -f [System.Environment]::GetFolderPath("MyDocuments")))) {
            $dllPath = ("{0}\WinSCP\WinSCPnet.dll" -f [System.Environment]::GetFolderPath("MyDocuments"))
            break
        }
        default {
            try {
                New-Item -Path ("{0}\WinSCP" -f [System.Environment]::GetFolderPath("MyDocuments")) -ItemType Directory -Force -ErrorAction Stop
                $zipPath = ("{0}\WinSCP\WinSCP-5.15.3-Automation.zip" -f [System.Environment]::GetFolderPath("MyDocuments"))
                $zipHash = "6FC1B65724665EF094B2CBFE3F2F8F996BAE627A4569F2C25867C98695ACD288"
                Invoke-WebRequest -Uri "https://www.cookadam.co.uk/scripts/WinSCP-5.15.3-Automation.zip" -OutFile $zipPath -ErrorAction Stop
                switch(Get-FileHash -LiteralPath $zipPath -Algorithm SHA256 | Select-Object -ExpandProperty Hash) {
                    $zipHash {
                        Add-Type -AssemblyName System.IO.Compression.FileSystem
                        [System.IO.Compression.ZipFile]::ExtractToDirectory($zipPath, (Split-Path -Path $zipPath))
                        $dllPath = ("{0}\WinSCP\WinSCPnet.dll" -f [System.Environment]::GetFolderPath("MyDocuments"))
                    }
                    default {
                        Throw "Hash mismatch from download"
                    }
                }
            }
            catch {
                Write-Host "Error: " -ForegroundColor Red -NoNewline
                Write-Host $Error[0].Exception.Message
                $problem = $true
            }
        }
    }

    $hostname = "ftp.cookadam.co.uk"
    $dir = "public_html/scripts/"
    switch ($problem) {
        $true {
            $Title = "Won't be able to upload using SFTP, proceed with FTP?"
            $y = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Proceed to upload via FTP"
            $n = New-Object System.Management.Automation.Host.ChoiceDescription "&No", "Abort upload"
            $options = [System.Management.Automation.Host.ChoiceDescription[]]($y,$n)
            $UseFTP = $host.ui.PromptForChoice($title, $null, $options, 1)
            switch ($UseFTP) {
                1 {
                    Write-Host "Aborting"
                    return
                }
                default {
                    try {
                        $ftp = ("ftp://{0}/{1}" -f $hostname, $dir)
                        $webclient = New-Object System.Net.WebClient 
                        $webclient.Credentials = New-Object System.Net.NetworkCredential($user,$pass)
                        $uri = New-Object -TypeName System.Uri -ArgumentList ($ftp+(Split-Path $profile.CurrentUserAllHosts -Leaf))
                        $webclient.UploadFile($uri, $profile.CurrentUserAllHosts)
                    }
                    catch {
                        Write-Host "Error: " -ForegroundColor Red -NoNewline
                        Write-Host $Error[0].Exception.Message
                    }
                }
            }
        }
        default {
            try {
                # Load WinSCP .NET assembly
                Add-Type -Path $dllPath
            
                # Setup session options
                $sessionOptions = New-Object WinSCP.SessionOptions -Property @{
                    Protocol = [WinSCP.Protocol]::Sftp
                    HostName = $hostname
                    UserName = $user
                    Password = $pass
                    PortNumber = 722
                    SshHostKeyFingerprint = "ssh-ed25519 256 qzr6Ci1g8gxABaGNVI76RYRfPiVMX14a+1f4a7dxczU="
                }
            
                $session = New-Object WinSCP.Session
            
                try {
                    # Connect
                    $session.Open($sessionOptions)
            
                    # Upload files
                    $transferOptions = New-Object WinSCP.TransferOptions
                    $transferOptions.TransferMode = [WinSCP.TransferMode]::Binary
            
                    $transferResult = $session.PutFiles($profile.CurrentUserAllHosts, $dir, $false, $transferOptions)
            
                    # Throw on any error
                    $transferResult.Check()
            
                    # Print results
                    foreach ($transfer in $transferResult.Transfers)
                    {
                        Write-Host "Success: " -ForegroundColor Green -NoNewline
                        Write-Host ("Upload of {0} [ OK ]" -f $transfer.FileName)
                    }
                }
                finally {
                    # Disconnect, clean up
                    $session.Dispose()
                }
            }
            catch {
                Write-Host "Error: " -ForegroundColor Red -NoNewline
                Write-Host $Error[0].Exception.Message
            }
        }
    }
}

Function WmiExec {
    <#
        .SYNOPSIS
        Execute command remotely and capture output, using only WMI.
        Copyright (c) Noxigen LLC. All rights reserved.
        Licensed under GNU GPLv3.

        .DESCRIPTION
        This is proof of concept code. Use at your own risk!
        
        Execute command remotely and capture output, using only WMI.
        Does not reply on PowerShell Remoting, WinRM, PsExec or anything
        else outside of WMI connectivity.
        
        .LINK
        https://github.com/OneScripter/WmiExec
        
        .EXAMPLE
        PS C:\> .\WmiExec.ps1 -ComputerName SFWEB01 -Command "gci c:\; hostname"

        .NOTES
        ========================================================================
            NAME: WmiExec.ps1
            
            AUTHOR: Jay Adams, Noxigen LLC
                        
            DATE: 6/11/2019
            
            Create secure GUIs for PowerShell with System Frontier.
            https://systemfrontier.com/powershell
        ==========================================================================
    #>

    Param(
        [string]$ComputerName,
        [Parameter(ValueFromPipeline=$true)]
        [string]$Command
    )

    function CreateScriptInstance([string]$ComputerName)
    {
        # Check to see if our custom WMI class already exists
        $classCheck = Get-WmiObject -Class Noxigen_WmiExec -ComputerName $ComputerName -List -Namespace "root\cimv2"
        
        if ($classCheck -eq $null)
        {
            # Create a custom WMI class to store data about the command, including the output.
            Write-Host "Creating WMI class..."
            $newClass = New-Object System.Management.ManagementClass("\\$ComputerName\root\cimv2",[string]::Empty,$null)
            $newClass["__CLASS"] = "Noxigen_WmiExec"
            $newClass.Qualifiers.Add("Static",$true)
            $newClass.Properties.Add("CommandId",[System.Management.CimType]::String,$false)
            $newClass.Properties["CommandId"].Qualifiers.Add("Key",$true)
            $newClass.Properties.Add("CommandOutput",[System.Management.CimType]::String,$false)
            $newClass.Put() | Out-Null
        }
        
        # Create a new instance of the custom class so we can reference it locally and remotely using this key
        $wmiInstance = Set-WmiInstance -Class Noxigen_WmiExec -ComputerName $ComputerName
        $wmiInstance.GetType() | Out-Null
        $commandId = ($wmiInstance | Select-Object -Property CommandId -ExpandProperty CommandId)
        $wmiInstance.Dispose()
        
        # Return the GUID for this instance
        return $CommandId
    }

    function GetScriptOutput([string]$ComputerName, [string]$CommandId)
    {
        $wmiInstance = Get-WmiObject -Class Noxigen_WmiExec -ComputerName $ComputerName -Filter "CommandId = '$CommandId'"
        $result = ($wmiInstance | Select-Object CommandOutput -ExpandProperty CommandOutput)
        $wmiInstance | Remove-WmiObject
        return $result
    }

    function ExecCommand([string]$ComputerName, [string]$Command)
    {
        #Pass the entire remote command as a base64 encoded string to powershell.exe
        $commandLine = "powershell.exe -NoLogo -NonInteractive -ExecutionPolicy Unrestricted -WindowStyle Hidden -EncodedCommand " + $Command
        $process = Invoke-WmiMethod -ComputerName $ComputerName -Class Win32_Process -Name Create -ArgumentList $commandLine
        
        if ($process.ReturnValue -eq 0)
        {
            $started = Get-Date
            
            Do
            {
                if ($started.AddMinutes(2) -lt (Get-Date))
                {
                    Write-Host "PID: $($process.ProcessId) - Response took too long."
                    break
                }
                
                # TODO: Add timeout
                $watcher = Get-WmiObject -ComputerName $ComputerName -Class Win32_Process -Filter "ProcessId = $($process.ProcessId)"
                
                Write-Host "PID: $($process.ProcessId) - Waiting for remote command to finish..."
                
                Start-Sleep -Seconds 1
            }
            While ($watcher -ne $null)
            
            # Once the remote process is done, retrieve the output
            $scriptOutput = GetScriptOutput $ComputerName $scriptCommandId
            
            return $scriptOutput
        }
    }

    function Main()
    {
        $commandString = $Command
        
        # The GUID from our custom WMI class. Used to get only results for this command.
        $scriptCommandId = CreateScriptInstance $ComputerName
        
        if ($scriptCommandId -eq $null)
        {
            Write-Error "Error creating remote instance."
            exit
        }
        
        # Meanwhile, on the remote machine...
        # 1. Execute the command and store the output as a string
        # 2. Get a reference to our current custom WMI class instance and store the output there!
            
        $encodedCommand = "`$result = Invoke-Command -ScriptBlock {$commandString} | Out-String; Get-WmiObject -Class Noxigen_WmiExec -Filter `"CommandId = '$scriptCommandId'`" | Set-WmiInstance -Arguments `@{CommandOutput = `$result} | Out-Null"
        
        $encodedCommand = [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($encodedCommand))
        
        Write-Host "Running the below command on: $ComputerName..."
        Write-Host $commandString
        
        $result = ExecCommand $ComputerName $encodedCommand
        
        Write-Host "Result..."
        Write-Output $result
    }

    Main

}
#endregion