poweralto3.psm1

###############################################################################
## Powershell v5 Classes
###############################################################################


###############################################################################
# PaConfigObject
class PaConfigObject {
    # Generic Properties
    [string]$Vsys = 'shared'
    [string]$Device
    [string]$ConfigNode
    hidden [string]$ManualXml
    
    # BaseXPath
    [string] getBaseXPath() {
        $xPath += "/config"
        if ($this.Vsys -eq 'shared') {
            $xPath += '/shared/'
            $xPath += $this.ConfigNode
        } else {
            $xPath += "/devices/entry"
            
            # Add Device
            if ($this.Device) {
                $xPath += "[@name='$($this.Device)']"
            }
            
            # Add Vsys
            $xPath += "/vsys/entry"
            if ($this.Vsys) {
                $xPath += "[@name='$($this.Vsys)']"
            }

            $xPath += '/'
            $xPath += $this.ConfigNode
        }

        return $xPath
    }

    # XPath
    [string] getXPath() {
        return $this.getBaseXPath()
    }

    # Xml
    [System.Xml.Linq.XElement] getXml() {
        if ($this.ManualXml) {
            return [System.Xml.Linq.XElement]$this.ManualXml
        } else {
        # Document Root
        $doc = [System.Xml.Linq.XDocument]::new()

        # Create and add "entry" node
        $entry = [System.Xml.Linq.XElement]::new("entry",$null)

        return $doc.Element("entry")
        }
    }

    # Pretty XMl
    [string] PrintPrettyXml() {
        return $this.getXml().ToString()
    }

    # Plaintext Xml
    [string] PrintPlainXml() {
        return $this.getXml().ToString([System.Xml.Linq.SaveOptions]::DisableFormatting)
    }
}

###############################################################################
# HelperRegex
class HelperRegex {
    static [string]$Ipv4 = '\b((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b'
    static [string]$Fqdn = '(?=^.{1,254}$)(^(?:(?!\d|-)[a-zA-Z0-9\-]{1,63}(?<!-)\.?)+(?:[a-zA-Z]{2,})$)'

    # function for checking regular expressions
    static [string] checkRegex([string]$matchString, [string]$regexString, [string]$errorMessage) {
        $regex = [regex]$regexString
        if ($regex.Match($matchString).Success) {
            return $matchString
        } else {
            Throw $errorMessage
        }
    }

    static [bool] checkRegex([string]$matchString, [string]$regexString, [bool]$returnBool) {
        $regex = [regex]$regexString
        if ($regex.Match($matchString).Success) {
            return $true
        } else {
            return $false
        }
    }

    # Ipv4 Address
    static [string] isIpv4([string]$matchString, [string]$errorMessage) {
        $regexString  = [HelperRegex]::Ipv4
        return [HelperRegex]::checkRegex($matchString,$regexString,$errorMessage)
    }

    static [bool] isIpv4([string]$matchString,  [bool]$returnBool) {
        $regexString  = [HelperRegex]::Ipv4
        return [HelperRegex]::checkRegex($matchString,$regexString,$true)
    }

    # Fqdn
    static [string] isFqdn([string]$matchString, [string]$errorMessage) {
        $regexString  = [HelperRegex]::Fqdn
        return [HelperRegex]::checkRegex($matchString,$regexString,$errorMessage)
    }

    static [bool] isFqdn([string]$matchString,  [bool]$returnBool) {
        $regexString  = [HelperRegex]::Fqdn
        return [HelperRegex]::checkRegex($matchString,$regexString,$true)
    }

    # Fqdn or Ipv4 Address
    static [string] isFqdnOrIpv4([string]$matchString, [string]$errorMessage) {
        $regexString  = [HelperRegex]::Ipv4 + "|" + [HelperRegex]::Fqdn
        return [HelperRegex]::checkRegex($matchString,$regexString,$errorMessage)
    }

    static [bool] isFqdnOrIpv4([string]$matchString,  [bool]$returnBool) {
        $regexString  = [HelperRegex]::Ipv4 + "|" + [HelperRegex]::Fqdn
        return [HelperRegex]::checkRegex($matchString,$regexString,$true)
    }

    # Constructor
    HelperRegex () {
    }
}

###############################################################################
# HelperWeb
class HelperWeb {
    static [string] createQueryString ([hashtable]$hashTable) {
        $i = 0
        $queryString = "?"
        foreach ($hash in $hashTable.GetEnumerator()) {
            $i++
            $queryString += $hash.Name + "=" + $hash.Value
            if ($i -lt $HashTable.Count) {
                $queryString += "&"
            }
        }
        return $queryString
    }
}

###############################################################################
# HelperXml
class HelperXml {
    # Element without Members
    static [System.Xml.Linq.XElement] createXmlWithoutMembers([string]$propertyName, [string]$data) {
        if ($data) {
            return [System.Xml.Linq.XElement]::new($propertyName,$data)
        } else {
            return $null
        }
    }

    # Element with Members
    static [System.Xml.Linq.XElement] createXmlWithMembers([string]$propertyName, [array]$members, [bool] $isRequired) {
        $node = [System.Xml.Linq.XElement]::new($propertyName,$null)
        if ($members) {
            foreach ($member in $members) {
                $node.Add( [System.Xml.Linq.XElement]::new("member",$member) )
            }
        } else {
            if ($isRequired) {
                $node.Add( [System.Xml.Linq.XElement]::new("member","any") )
            } else {
                return $null
            }
        }
        return $node
    }
}

###############################################################################
# PaAddress
class PaAddress : PaConfigObject {
    [string]$Name
    [string]$Description
    [string]$Type
    [string]$Address
    [array]$Tags
    [string]$ConfigNode = "address"

    # XPath
    [string] getXPath() {
        $returnXPath = $this.getBaseXPath()

        # Add Name
        if ($this.Name) {
            $returnXPath += "/entry[@name='"
            $returnXPath += $this.Name
            $returnXPath += "']"
        }

        return $returnXPath
    }

    # Xml
    [System.Xml.Linq.XElement] getXml() {
        # Document Root
        $doc = [System.Xml.Linq.XDocument]::new()

        # Create and add "entry" node
        $entry = [System.Xml.Linq.XElement]::new("entry",$null)
        $entry.SetAttributeValue("name",$this.Name)
        $doc.Add($entry)

        # Add Name
        $doc.Element("entry").Add([HelperXml]::createXmlWithoutMembers($this.Type,$this.Address))

        # Add Description
        $doc.Element("entry").Add([HelperXml]::createXmlWithoutMembers("description",$this.Description))
        
        # Add Tags
        $doc.Element("entry").Add([HelperXml]::createXmlWithMembers("tag",$this.Tags,$false))

        return $doc.Element("entry")
    }
}

###############################################################################
# PaAdmin
class PaAdmin : PaConfigObject {
    [string]$Name
    [string]$AuthProfile
    [bool]$ClientCert
    [bool]$PublicKey
    [string]$AdminType
    [string]$AdminProfile
    [array]$VsysAccess
    [string]$PasswordProfile

    [string]$ConfigNode = "mgt-config/users"

    # BaseXPath
    [string] getBaseXPath() {
        $xPath = "/config"

        $xPath += $this.ConfigNode

        return $xPath
    }

    # XPath
    [string] getXPath() {
        $returnXPath = $this.getBaseXPath()

        # Add Name
        if ($this.Name) {
            $returnXPath += "/entry[@name='"
            $returnXPath += $this.Name
            $returnXPath += "']"
        }

        return $returnXPath
    }

    # Xml
    [System.Xml.Linq.XElement] getXml() {
        # Document Root
        $doc = [System.Xml.Linq.XDocument]::new()

        # Create and add "entry" node
        $entry = [System.Xml.Linq.XElement]::new("entry",$null)

        return $doc.Element("entry")
    }
}

###############################################################################
# PaAuthenticationProfile
class PaAuthenticationProfile : PaConfigObject {
    [string]$Name
    [string]$Type
    [string]$UserDomain
    [array]$AllowList
    [int]$FailedAttempts
    [int]$LockoutTime
    [string]$ServerProfile
    [string]$LoginAttribute
    [string]$PasswordExpiryWarning
    [string]$UsernameModifier
    [bool]$RetrieveGroup
    [string]$KerberosRealm

    [string]$ConfigNode = "authentication-profile"

    # XPath
    [string] getXPath() {
        $returnXPath = $this.getBaseXPath()

        # Add Name
        if ($this.Name) {
            $returnXPath += "/entry[@name='"
            $returnXPath += $this.Name
            $returnXPath += "']"
        }

        return $returnXPath
    }

    # Xml
    [System.Xml.Linq.XElement] getXml() {
        # Document Root
        $doc = [System.Xml.Linq.XDocument]::new()

        # Create and add "entry" node
        $entry = [System.Xml.Linq.XElement]::new("entry",$null)

        return $doc.Element("entry")
    }
}

###############################################################################
# PaAuthServer
class PaAuthServer {
    [string]$Name
    [string]$Server
    [string]$Port
}

###############################################################################
# PaDevice
class PaDevice {
    [ValidateRange(1,65535)]
    [int]$Port = 443
    
    [string]$ApiKey

    [ValidateSet('http','https')] 
    [string]$Protocol = "https"

    [string]$Name
    [string]$IpAddress
    [string]$Model
    [string]$Serial
    [string]$OsVersion
    [string]$GpAgent
    [string]$AppVersion
    [string]$ThreatVersion
    [string]$WildFireVersion
    [string]$UrlVersion

    # DeviceAddress
    hidden [string]$DeviceAddress

    setDeviceAddress([string]$deviceAddress) {
        $this.DeviceAddress = [HelperRegex]::isFqdnOrIpv4($deviceAddress,"DeviceAddress must be a valid FQDN or IPv4 Address.")
    }

    [string] getDeviceAddress() {
        $returnValue = [HelperRegex]::isFqdnOrIpv4($this.DeviceAddress,"DeviceAddress must be a valid FQDN or IPv4 Address.")
        return $returnValue
    }

    # Track usage
    hidden [bool]$Connected
    [array]$UrlHistory
    [array]$RawQueryResultHistory
    [array]$QueryHistory
    $LastError
    $LastResult

    # Error handling
    [bool] checkConnectionStatus([string]$errorPrefix) {
        if ($this.Connected) {
            return $true
        } else {
            throw "$errorPrefix Not Connected, please use Get-PaConfig to connect before using other cmdlets."
        }
    }

    # Function for created the base API Url
    [String] getApiUrl() {
        if ($this.DeviceAddress) {
            $url = $this.Protocol + "://" + $this.getDeviceAddress() + ":" + $this.Port + "/api/"
            return $url
        } else {
            return $null
        }
    }

    ############################################################################################
    # Api Query Functions

    # Base API Query
    [Xml] invokeApiQuery([hashtable]$queryString) {
        if ($queryString.type -ne "keygen") {
            $queryString.key = $this.ApiKey
        }
        $formattedQueryString = [HelperWeb]::createQueryString($queryString)
        $url = $this.getApiUrl() + $formattedQueryString
        if ($queryString.type -ne "keygen") {
            $this.UrlHistory += $url
            $this.QueryHistory += $queryString
        } else {
            $formattedQueryString = [HelperWeb]::createQueryString($queryString)
            $this.UrlHistory += $url.Replace($queryString.password,"PASSWORDREDACTED")
        }
        try {
            $rawResult = Invoke-WebRequest -Uri $url -SkipCertificateCheck
        } catch {
            Throw "$($error[0].ToString()) $($error[0].InvocationInfo.PositionMessage)"
        }
        $this.RawQueryResultHistory += $rawResult
        $result = [xml]($rawResult.Content)
        $this.LastResult = $result

        # Handle Errors
        if ($result.response.status -ne "success") {
            $errorMessage = "PaDevice: " + $result.response.status + " " + $result.response.code + ": "
            if ($result.response.msg.line) {
                if ($result.response.msg.line."#cdata-section") {
                    $errorMessage += "Too Many errors, check `$global:PaDeviceObject.LastError for more details."
                    $this.LastError = $result.response.msg.line."#cdata-section"
                } else {
                    $errorMessage += $result.response.msg.line
                    $this.LastError = $result.response.msg.line
                }
            } else {
                $errorMessage += $result.response.result.msg
                $this.LastError = $result.response.result.msg
            }
            Throw $errorMessage
        }

        return $result
    }

    # Config API Query
    [Xml] invokeConfigQuery([string]$xPath,[string]$action) {
        $queryString = @{}
        $queryString.type = "config"
        $queryString.action = $action
        $queryString.xpath = $xPath
        $result = $this.invokeApiQuery($queryString)
        return $result
    }

    [Xml] invokeConfigQuery([string]$xPath,[string]$action,[string]$element) {
        $queryString         = @{}
        $queryString.type    = "config"
        $queryString.action  = $action
        $queryString.xpath   = $xPath
        $queryString.element = $element

        $result = $this.invokeApiQuery($queryString)
        return $result
    }

    # Keygen API Query
    [xml] invokeKeygenQuery([string]$user,[string]$password) {
        $queryString = @{}
        $queryString.type = "keygen"
        $queryString.user = $user
        $queryString.password = $password
        $result = $this.invokeApiQuery($queryString)
        $this.ApiKey = $result.response.result.key
        return $result
    }

    # Keygen API Query
    [xml] invokeOperationalQuery([string]$cmd) {
        $queryString = @{}
        $queryString.type = "op"
        $queryString.cmd = $cmd
        $result = $this.invokeApiQuery($queryString)
        return $result
    }

    # Test Connection
    [bool] testConnection() {
        $result = $this.invokeOperationalQuery('<show><system><info></info></system></show>')
        $this.Connected       = $true
        $this.Name            = $result.response.result.system.devicename
        $this.IpAddress       = $result.response.result.system.'ip-address'
        $this.Model           = $result.response.result.system.model
        $this.Serial          = $result.response.result.system.serial
        $this.OsVersion       = $result.response.result.system.'sw-version'
        $this.GpAgent         = $result.response.result.system.'global-protect-client-package-version'
        $this.AppVersion      = $result.response.result.system.'app-version'
        $this.ThreatVersion   = $result.response.result.system.'threat-version'
        $this.WildFireVersion = $result.response.result.system.'wildfire-version'
        $this.UrlVersion      = $result.response.result.system.'url-filtering-version'
        return $true
    }
}

###############################################################################
# PaKerberosProfile
class PaKerberosProfile : PaConfigObject {
    [string]$Name
    [bool]$AdminUseOnly
    [array]$Servers

    [string]$ConfigNode = "server-profile/kerberos"

    # XPath
    [string] getXPath() {
        $returnXPath = $this.getBaseXPath()

        # Add Name
        if ($this.Name) {
            $returnXPath += "/entry[@name='"
            $returnXPath += $this.Name
            $returnXPath += "']"
        }

        return $returnXPath
    }

    # Xml
    [System.Xml.Linq.XElement] getXml() {
        # Document Root
        $doc = [System.Xml.Linq.XDocument]::new()

        # Create and add "entry" node
        $entry = [System.Xml.Linq.XElement]::new("entry",$null)

        return $doc.Element("entry")
    }
}

###############################################################################
# PaLdapProfile
class PaLdapProfile : PaConfigObject {
    [string]$Name
    [bool]$AdminUseOnly
    [string]$Type
    [string]$BaseDN
    [string]$BindDN
    
    [int]$BindTimeout
    [int]$SearchTimout
    [int]$RetryInterval
    
    [bool]$RequireSSL
    [bool]$VerifyServerCertificate

    [array]$Servers

    [string]$ConfigNode = "server-profile/ldap"

    # XPath
    [string] getXPath() {
        $returnXPath = $this.getBaseXPath()

        # Add Name
        if ($this.Name) {
            $returnXPath += "/entry[@name='"
            $returnXPath += $this.Name
            $returnXPath += "']"
        }

        return $returnXPath
    }

    # Xml
    [System.Xml.Linq.XElement] getXml() {
        # Document Root
        $doc = [System.Xml.Linq.XDocument]::new()

        # Create and add "entry" node
        $entry = [System.Xml.Linq.XElement]::new("entry",$null)

        return $doc.Element("entry")
    }
}

###############################################################################
# PaPasswordProfile
class PaPasswordProfile : PaConfigObject {
    [string]$Name
    [int]$ExpirationPeriod
    [int]$ExpirationWarningPeriod
    [int]$PostExpirationAdminLoginCount
    [int]$PostExpirationGracePeriod
    [string]$Vsys = 'shared'

    [string]$ConfigNode = "mgt-config/password-profile"

    # BaseXPath
    [string] getBaseXPath() {
        $xPath = "/config"

        $xPath += $this.ConfigNode

        return $xPath
    }

    # XPath
    [string] getXPath() {
        $returnXPath = $this.getBaseXPath()

        # Add Name
        if ($this.Name) {
            $returnXPath += "/entry[@name='"
            $returnXPath += $this.Name
            $returnXPath += "']"
        }

        return $returnXPath
    }

    # Xml
    [System.Xml.Linq.XElement] getXml() {
        # Document Root
        $doc = [System.Xml.Linq.XDocument]::new()

        # Create and add "entry" node
        $entry = [System.Xml.Linq.XElement]::new("entry",$null)

        return $doc.Element("entry")
    }
}

###############################################################################
# PaRadiusProfile
class PaRadiusProfile : PaConfigObject {
    [string]$Name
    [bool]$AdminUseOnly
    [int]$Timeout
    [int]$Retries
    [array]$Servers

    [string]$ConfigNode = "server-profile/radius"

    # XPath
    [string] getXPath() {
        $returnXPath = $this.getBaseXPath()

        # Add Name
        if ($this.Name) {
            $returnXPath += "/entry[@name='"
            $returnXPath += $this.Name
            $returnXPath += "']"
        }

        return $returnXPath
    }

    # Xml
    [System.Xml.Linq.XElement] getXml() {
        # Document Root
        $doc = [System.Xml.Linq.XDocument]::new()

        # Create and add "entry" node
        $entry = [System.Xml.Linq.XElement]::new("entry",$null)

        return $doc.Element("entry")
    }
}

###############################################################################
# PaSession
class PaSession {
    [double]$Id
    [string]$Vsys
    [string]$Application
    [string]$State
    [string]$Type
    [string]$Source
    [string]$SourcePort
    [string]$SourceZone
    [string]$SourceTranslatedIp
    [string]$SourceTranslatedPort
    [string]$Destination
    [string]$DestinationPort
    [string]$DestinationZone
    [string]$DestinationTranslatedIp
    [string]$DestinationTranslatedPort
    [string]$Protocol

    [datetime]$StartTime
}

###############################################################################
# PaTacacsProfile
class PaTacacsProfile : PaConfigObject {
    [string]$Name
    [bool]$AdminUseOnly
    [int]$Timeout
    [bool]$UseSingleConnection
    [array]$Servers

    [string]$ConfigNode = "server-profile/tacplus"

    # XPath
    [string] getXPath() {
        $returnXPath = $this.getBaseXPath()

        # Add Name
        if ($this.Name) {
            $returnXPath += "/entry[@name='"
            $returnXPath += $this.Name
            $returnXPath += "']"
        }

        return $returnXPath
    }

    # Xml
    [System.Xml.Linq.XElement] getXml() {
        # Document Root
        $doc = [System.Xml.Linq.XDocument]::new()

        # Create and add "entry" node
        $entry = [System.Xml.Linq.XElement]::new("entry",$null)

        return $doc.Element("entry")
    }
}

###############################################################################
## Start Powershell Cmdlets
###############################################################################


###############################################################################
# Get-PaAdmin
function Get-PaAdmin {
    Param (
        [Parameter(Mandatory=$False,Position=0)]
        [string]$Name,

        [Parameter(Mandatory=$False,Position=1)]
        [string]$Vsys,

        [Parameter(Mandatory=$False,Position=2)]
        [string]$Device
    )
    
    $VerbosePrefix = "Get-PaAdmin:"

    if ($global:PaDeviceObject.Connected) {
        $InfoObject        = New-Object PaAdmin
        $InfoObject.Name   = $Name
        $InfoObject.Vsys   = $Vsys
        $InfoObject.Device = $Device
        $Response          = Get-PaConfig $InfoObject.GetXpath()

        $ReturnObject = @()
        foreach ($entry in $Response.response.result.users.entry) {
            $NewEntry      = New-Object PaAdmin
            $ReturnObject += $NewEntry

            $NewEntry.Vsys   = $Vsys
            $NewEntry.Device = $Device
            
            # Regular Properties
            $NewEntry.Name            = $entry.name
            $NewEntry.AuthProfile     = $entry.'authentication-profile'
            $NewEntry.VsysAccess      = $entry.permissions.'role-based'.vsysadmin.entry.vsys.member
            $NewEntry.PasswordProfile = $entry.'password-profile'

            # Client Certificate
            if ($entry.'client-certificate-only' -eq 'yes') {
                $NewEntry.ClientCert = $true
            }
            
            # Public Key
            if ($entry.'public-key') {
                $NewEntry.PublicKey = $true
            }

            # AdminProfile
            if ($entry.permissions.'role-based'.custom) {
                $NewEntry.AdminType    = "RoleBased"
                $NewEntry.AdminProfile = $entry.permissions.'role-based'.custom.profile
            } else {
                $NewEntry.AdminType    = "Dynamic"
                $NewEntry.AdminProfile = ($entry.permissions.'role-based' | Get-Member -MemberType Property)[0].Name
            }
        }

        return $ReturnObject
    } else {
        Throw "$VerbosePrefix Not Connected, please use Get-PaConfig to connect before using other cmdlets."
    }
}

###############################################################################
# Get-PaAuthenticationProfile
function Get-PaAuthenticationProfile {
    Param (
        [Parameter(Mandatory=$False,Position=0)]
        [string]$Name,

        [Parameter(Mandatory=$False,Position=1)]
        [string]$Vsys = "shared",

        [Parameter(Mandatory=$False,Position=2)]
        [string]$Device
    )
    
    $VerbosePrefix = "Get-PaAuthenticationProfile:"

    if ($global:PaDeviceObject.Connected) {
        $InfoObject        = New-Object PaAuthenticationProfile
        $InfoObject.Name   = $Name
        $InfoObject.Vsys   = $Vsys
        $InfoObject.Device = $Device
        $Response          = Get-PaConfig $InfoObject.GetXpath()

        $ConfigNode = $InfoObject.ConfigNode

        $ReturnObject = @()
        foreach ($entry in $Response.response.result.$ConfigNode.entry) {
            $NewEntry      = New-Object PaAuthenticationProfile
            $ReturnObject += $NewEntry

            $NewEntry.Vsys   = $Vsys
            $NewEntry.Device = $Device

            $NewEntry.Name             = $entry.name
            $NewEntry.Type             = ($entry.method | Get-Member -MemberType Property)[0].Name
            $NewEntry.UserDomain       = $entry.'user-domain'
            $NewEntry.UsernameModifier = $entry.'username-modifier'
            $NewEntry.AllowList        = $entry.'allow-list'.member
            $NewEntry.FailedAttempts   = $entry.lockout.'failed-attempts'
            $NewEntry.LockoutTime      = $entry.lockout.'lockout-time'

            $NewEntry.ServerProfile         = $entry.method."$($NewEntry.Type)".'server-profile'

            switch ($NewEntry.Type) {
                "ldap" {
                    $NewEntry.LoginAttribute        = $entry.method.ldap.'login-attribute'
                    $NewEntry.PasswordExpiryWarning = $entry.method.ldap.'passwd-exp-days'
                    break
                }
                "radius" {
                    if ($entry.method.radius.checkgroup -eq 'yes') {
                        $NewEntry.RetrieveGroup = $true
                    }
                    break
                }
                "kerberos" {
                    $NewEntry.KerberosRealm = $entry.method.kerberos.realm
                    break
                }
            }
        }
        return $ReturnObject
    } else {
        Throw "$VerbosePrefix Not Connected, please use Get-PaConfig to connect before using other cmdlets."
    }
}

###############################################################################
# Get-PaConfig
function Get-PaConfig {
    Param (
        [Parameter(Mandatory=$False,Position=0)]
        [string]$XPath = "/config",

        [Parameter(Mandatory=$False,Position=3)]
        [ValidateSet("get","show")]
        [string]$Action = "show"
    )
    
    $VerbosePrefix = "Get-PaConfig:"

    if ($global:PaDeviceObject.Connected) {
        $Response = $global:PaDeviceObject.invokeConfigQuery($XPath,$Action)

        return $Response
    } else {
        Throw "$VerbosePrefix Not Connected, please use Get-PaDevice to connect before using other cmdlets."
    }
}

###############################################################################
# Get-PaDevice
function Get-PaDevice {
    [CmdletBinding()]
    <#
    .SYNOPSIS
        Establishes initial connection to Palo Alto API.
        
    .DESCRIPTION
        The Get-PaDevice cmdlet establishes and validates connection parameters to allow further communications to the Palo Alto API. The cmdlet needs at least two parameters:
         - The device IP address or FQDN
         - A valid API key or PSCredential object
        
        The cmdlet returns an object containing details of the connection, but this can be discarded or saved as desired; the returned object is not necessary to provide to further calls to the API.
    
    .EXAMPLE
        Get-PaDevice "pa.example.com" "LUFRPT1asdfPR2JtSDl5M2tjfdsaTktBeTkyaGZMTURasdfTTU9BZm89OGtKN0F"
        
        Connects to Palo Alto Device using the default port (443) over SSL (HTTPS) using an API Key
        
    .PARAMETER DeviceAddress
        Fully-qualified domain name for the Palo Alto Device. Don't include the protocol ("https://" or "http://").

    .PARAMETER ApiKey
        ApiKey used to access Palo Alto Device.
        
    .PARAMETER Credential
        PSCredental object to provide as an alternative to an API Key.
    
    .PARAMETER Port
        The port the Palo Alto Device is using for management communicatins. This defaults to port 443 over HTTPS, and port 80 over HTTP.
    
    .PARAMETER HttpOnly
        When specified, configures the API connection to run over HTTP rather than the default HTTPS. Not recommended!
    
    .PARAMETER SkipCertificateCheck
        When used, all certificate warnings are ignored.

    .PARAMETER Quiet
        When used, the cmdlet returns nothing on success.
    
    #>


    Param (
        [Parameter(Mandatory=$True,Position=0)]
        [ValidatePattern("\d+\.\d+\.\d+\.\d+|(\w\.)+\w")]
        [string]$DeviceAddress,

        [Parameter(ParameterSetName="keyonly",Mandatory=$True,Position=1)]
        [string]$ApiKey,

        [Parameter(ParameterSetName="credential",Mandatory=$True,Position=1)]
        [System.Management.Automation.CredentialAttribute()]$Credential,

        [Parameter(Mandatory=$False,Position=2)]
        [int]$Port = 443,

        [Parameter(Mandatory=$False)]
        [alias('http')]
        [switch]$HttpOnly,
        
        [Parameter(Mandatory=$False)]
        [switch]$SkipCertificateCheck,

        [Parameter(Mandatory=$False)]
        [alias('q')]
        [switch]$Quiet
    )

    BEGIN {
        $VerbosePrefix = "Get-PaDevice:"

        if ($HttpOnly) {
            $Protocol = "http"
            if (!$Port) { $Port = 80 }
        } else {
            $Protocol = "https"
            if (!$Port) { $Port = 443 }
            
            $global:PaDeviceObject = New-Object PaDevice
            $global:PaDeviceObject.SetDeviceAddress($DeviceAddress)
            $global:PaDeviceObject.Protocol = $Protocol
            $global:PaDeviceObject.Port     = $Port

            if ($ApiKey) {
                $global:PaDeviceObject.ApiKey = $ApiKey
            } else {
                $UserName = $Credential.UserName
                $Password = $Credential.getnetworkcredential().password
            }
        }
    }

    PROCESS {
        
        if (!($ApiKey)) {
            Write-Verbose "$VerbosePrefix Attempting to generate API Key."
            $global:PaDeviceObject.invokeKeygenQuery($UserName,$Password)
            Write-Verbose "$VerbosePrefix API Key successfully generated."
        }

        $TestConnection = $global:PaDeviceObject.testConnection()

        if (!($Quiet)) {
            return $global:PaDeviceObject
        }
    }
}

###############################################################################
# Get-PaKerberosProfile
function Get-PaKerberosProfile {
    Param (
        [Parameter(Mandatory=$False,Position=0)]
        [string]$Name,

        [Parameter(Mandatory=$False,Position=1)]
        [string]$Vsys = "shared",

        [Parameter(Mandatory=$False,Position=2)]
        [string]$Device
    )
    
    $VerbosePrefix = "Get-PaKerberosProfile:"

    if ($global:PaDeviceObject.Connected) {
        $InfoObject        = New-Object PaKerberosProfile
        $InfoObject.Name   = $Name
        $InfoObject.Vsys   = $Vsys
        $InfoObject.Device = $Device
        $Response          = Get-PaConfig $InfoObject.GetXpath()

        $ConfigNode = 'kerberos'

        $ReturnObject = @()
        foreach ($entry in $Response.response.result.$ConfigNode.entry) {
            $NewEntry      = New-Object PaKerberosProfile
            $ReturnObject += $NewEntry

            $NewEntry.Vsys   = $Vsys
            $NewEntry.Device = $Device

            $NewEntry.Name         = $entry.name
            $NewEntry.Servers      = @()

            # bool values
            $BoolProperties = @{ 'AdminUseOnly' = 'admin-use-only' }

            foreach ($Bool in $BoolProperties.GetEnumerator()) {
                $PsProp  = $Bool.Name
                $XmlProp = $Bool.Value
                $NewEntry.$PsProp = $entry.$XmlProp
            }

            foreach ($Server in $entry.server.entry) {
                $NewServer         = New-Object PaAuthServer
                $NewServer.Name    = $Server.name
                $NewServer.Server  = $Server.host
                $NewServer.Port    = $Server.port
                $NewEntry.Servers += $NewServer
            }
        }
        return $ReturnObject
    } else {
        Throw "$VerbosePrefix Not Connected, please use Get-PaConfig to connect before using other cmdlets."
    }
}

###############################################################################
# Get-PaLdapProfile
function Get-PaLdapProfile {
    Param (
        [Parameter(Mandatory=$False,Position=0)]
        [string]$Name,

        [Parameter(Mandatory=$False,Position=1)]
        [string]$Vsys = "shared",

        [Parameter(Mandatory=$False,Position=2)]
        [string]$Device
    )
    
    $VerbosePrefix = "Get-PaLdapProfile:"

    if ($global:PaDeviceObject.Connected) {
        $InfoObject        = New-Object PaLdapProfile
        $InfoObject.Name   = $Name
        $InfoObject.Vsys   = $Vsys
        $InfoObject.Device = $Device
        $Response          = Get-PaConfig $InfoObject.GetXpath()

        $ConfigNode = 'ldap'

        $ReturnObject = @()
        foreach ($entry in $Response.response.result.$ConfigNode.entry) {
            $NewEntry      = New-Object PaLdapProfile
            $ReturnObject += $NewEntry

            $NewEntry.Vsys   = $Vsys
            $NewEntry.Device = $Device

            $NewEntry.Name                    = $entry.name
            $NewEntry.Type                    = $entry.'ldap-type'
            $NewEntry.BaseDN                  = $entry.base
            $NewEntry.BindDN                  = $entry.'bind-dn'
            $NewEntry.BindTimeout             = $entry.'bind-timelimit'
            $NewEntry.SearchTimout            = $entry.'timelimit'
            $NewEntry.RetryInterval           = $entry.'retry-interval'
            $NewEntry.Servers                 = @()

            # bool values
            $BoolProperties = @{ 'AdminUseOnly'            = 'admin-use-only'
                                 'RequireSSL'              = 'ssl'
                                 'VerifyServerCertificate' = 'verify-server-certificate' }

            foreach ($Bool in $BoolProperties.GetEnumerator()) {
                $PsProp  = $Bool.Name
                $XmlProp = $Bool.Value
                $NewEntry.$PsProp = $entry.$XmlProp
            }

            foreach ($Server in $entry.server.entry) {
                $NewServer         = New-Object PaAuthServer
                $NewServer.Name    = $Server.name
                $NewServer.Server  = $Server.address
                
                # port will be empty if it's the default (389)
                if ($Server.port) {
                    $NewServer.Port    = $Server.port
                } else {
                    $NewServer.Port = 389
                }

                $NewEntry.Servers += $NewServer
            }
        }
        return $ReturnObject
    } else {
        Throw "$VerbosePrefix Not Connected, please use Get-PaConfig to connect before using other cmdlets."
    }
}

###############################################################################
# Get-PaPasswordProfile
function Get-PaPasswordProfile {
    Param (
        [Parameter(Mandatory=$False,Position=0)]
        [string]$Name,

        [Parameter(Mandatory=$False,Position=1)]
        [string]$Vsys,

        [Parameter(Mandatory=$False,Position=2)]
        [string]$Device
    )
    
    $VerbosePrefix = "Get-PaPasswordProfile:"

    if ($global:PaDeviceObject.Connected) {
        $InfoObject        = New-Object PaPasswordProfile
        $InfoObject.Name   = $Name
        $InfoObject.Vsys   = $Vsys
        $InfoObject.Device = $Device
        $Response          = Get-PaConfig $InfoObject.GetXpath()

        $ConfigNode = $InfoObject.ConfigNode -replace 'mgt-config/'

        $ReturnObject = @()
        foreach ($entry in $Response.response.result.$ConfigNode.entry) {
            $NewEntry      = New-Object PaPasswordProfile
            $ReturnObject += $NewEntry

            $NewEntry.Vsys   = $Vsys
            $NewEntry.Device = $Device
            
            # Regular Properties
            $NewEntry.Name                          = $entry.name
            $NewEntry.ExpirationPeriod              = $entry.'password-change'.'expiration-period'
            $NewEntry.ExpirationWarningPeriod       = $entry.'password-change'.'expiration-warning-period'
            $NewEntry.PostExpirationAdminLoginCount = $entry.'password-change'.'post-expiration-admin-login-count'
            $NewEntry.PostExpirationGracePeriod     = $entry.'password-change'.'post-expiration-grace-period'
        }

        return $ReturnObject
    } else {
        Throw "$VerbosePrefix Not Connected, please use Get-PaConfig to connect before using other cmdlets."
    }
}

###############################################################################
# Get-PaRadiusProfile
function Get-PaRadiusProfile {
    Param (
        [Parameter(Mandatory=$False,Position=0)]
        [string]$Name,

        [Parameter(Mandatory=$False,Position=1)]
        [string]$Vsys = "shared",

        [Parameter(Mandatory=$False,Position=2)]
        [string]$Device
    )
    
    $VerbosePrefix = "Get-PaRadiusProfile:"

    if ($global:PaDeviceObject.Connected) {
        $InfoObject        = New-Object PaRadiusProfile
        $InfoObject.Name   = $Name
        $InfoObject.Vsys   = $Vsys
        $InfoObject.Device = $Device
        $Response          = Get-PaConfig $InfoObject.GetXpath()

        $ConfigNode = 'radius'

        $ReturnObject = @()
        foreach ($entry in $Response.response.result.$ConfigNode.entry) {
            $NewEntry      = New-Object PaRadiusProfile
            $ReturnObject += $NewEntry

            $NewEntry.Vsys   = $Vsys
            $NewEntry.Device = $Device

            $NewEntry.Name         = $entry.name
            $NewEntry.Timeout      = $entry.timeout
            $NewEntry.Retries      = $entry.retries
            $NewEntry.Servers      = @()

            $BoolProperties = @{ 'AdminUseOnly' = 'admin-use-only' }

            foreach ($Bool in $BoolProperties.GetEnumerator()) {
                $PsProp  = $Bool.Name
                $XmlProp = $Bool.Value
                $NewEntry.$PsProp = $entry.$XmlProp
            }

            foreach ($Server in $entry.server.entry) {
                $NewServer         = New-Object PaAuthServer
                $NewServer.Name    = $Server.name
                $NewServer.Server  = $Server.'ip-address'
                $NewServer.Port    = $Server.port
                $NewEntry.Servers += $NewServer
            }
        }
        return $ReturnObject
    } else {
        Throw "$VerbosePrefix Not Connected, please use Get-PaConfig to connect before using other cmdlets."
    }
}

###############################################################################
# Get-PaSession
function Get-PaSession {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory=$False,ParameterSetName="id")]
        [int]$Id,

        [Parameter(Mandatory=$False,ParameterSetName="filter")]
        [string]$Application,

        [Parameter(Mandatory=$False,ParameterSetName="filter")]
        [string]$Destination,

        [Parameter(Mandatory=$False,ParameterSetName="filter")]
        [int]$DestinationPort,

        [Parameter(Mandatory=$False,ParameterSetName="filter")]
        [string]$DestinationUser,

        [Parameter(Mandatory=$False,ParameterSetName="filter")]
        [string]$EgressInterface,

        [Parameter(Mandatory=$False,ParameterSetName="filter")]
        [string]$HwInterface,

        [Parameter(Mandatory=$False,ParameterSetName="filter")]
        [string]$IngressInterface,

        [Parameter(Mandatory=$False,ParameterSetName="filter")]
        [string]$SourceZone,

        [Parameter(Mandatory=$False,ParameterSetName="filter")]
        [string]$DestinationZone,

        [Parameter(Mandatory=$False,ParameterSetName="filter")]
        [string]$MinKb,

        [Parameter(Mandatory=$False,ParameterSetName="filter")]
        [string]$Rule,

        [Parameter(Mandatory=$False,ParameterSetName="filter")]
        [string]$NatRule,

        [Parameter(Mandatory=$False,ParameterSetName="filter")]
        [string]$PbfRule,

        [Parameter(Mandatory=$False,ParameterSetName="filter")]
        [string]$QosRule,

        [Parameter(Mandatory=$False,ParameterSetName="filter")]
        [string]$QosClass,

        [Parameter(Mandatory=$False,ParameterSetName="filter")]
        [string]$QosNodeId,

        [Parameter(Mandatory=$False,ParameterSetName="filter")]
        [string]$Nat,

        [Parameter(Mandatory=$False,ParameterSetName="filter")]
        [string]$Rematch,

        [Parameter(Mandatory=$False,ParameterSetName="filter")]
        [string]$SslDecrypt,

        [Parameter(Mandatory=$False,ParameterSetName="filter")]
        [string]$Type,

        [Parameter(Mandatory=$False,ParameterSetName="filter")]
        [int]$Protocol,

        [Parameter(Mandatory=$False,ParameterSetName="filter")]
        [string]$Source,

        [Parameter(Mandatory=$False,ParameterSetName="filter")]
        [int]$SourcePort,

        [Parameter(Mandatory=$False,ParameterSetName="filter")]
        [string]$SourceUser,

        [Parameter(Mandatory=$False,ParameterSetName="filter")]
        [double]$StartAt,

        [Parameter(Mandatory=$False,ParameterSetName="filter")]
        [string]$State
    )

    $VerbosePrefix = "Get-PaSession:"

    $Filters = @{ "Application"      = "application"
                  "Destination"      = "destination"
                  "DestinationPort"  = "destination-port"
                  "DestinationUser"  = "destination-user"
                  "EgressInterface"  = "egress-interface"
                  "HwInterface"      = "hw-interface"
                  "IngressInterface" = "ingress-interface"
                  "SourceZone"       = "from"
                  "DestinationZone"  = "to"
                  "MinKb"            = "min-kb"
                  "Rule"             = "rule"
                  "NatRule"          = "nat-rule"
                  "PbfRule"          = "pbf-rule"
                  "QosRule"          = "qos-rule"
                  "QosClass"         = "qos-class"
                  "QosNodeId"        = "qos-node-id"
                  "Nat"              = "nat"
                  "Rematch"          = "rematch"
                  "SslDecrypt"       = "ssl-descrypt"
                  "Type"             = "type"
                  "Protocol"         = "protocol"
                  "Source"           = "source"
                  "SourcePort"       = "source-port"
                  "SourceUser"       = "source-user"
                  "StartAt"          = "start-at"
                  "State"            = "state" }

    $Command = "<show><session>"
    
    if ($Id) {
        $Command += "<id>$Id</id>"
    } else {
        $Command += "<all><filter>"
        foreach ($Filter in $Filters.GetEnumerator()) {
            try {
                $FilterValue = Get-Variable -Name $Filter.Name -ValueOnly
                if ($FilterValue) {
                    $Command += "<" + $Filter.Value + ">" + $FilterValue + "</" + $Filter.Value + ">"
                }
            } catch {}
        }
        $Command += "</filter></all>"
    }

    $Command += "</session></show>"
    Write-Verbose "$VerbosePrefix Command: $Command"

    $Results = Invoke-PaOperation $Command
    if ($Id) {
        $Results = $Results.response.result
    } else {
        $Results = $Results.response.result.entry
    }

    $ReturnResults = @()
    foreach ($Result in $Results) {
        $Session = New-Object PaSession

        if ($Id) {
            $Session.Id = $Id
        } else {
            $Session.Id = $Result.idx
        }

        $Session.Vsys        = $Result.vsys
        $Session.Application = $Result.application
        
        # Format Time
        $StartTime = $Result.'start-time' -replace ' ',''
        $Session.StartTime   = [datetime]::ParseExact($StartTime,"dddMMMdHH:mm:ssyyyy",$null)

        if ($Result.c2s) {
            $Session.State                     = $Result.c2s.state
            $Session.Type                      = $Result.c2s.type
            $Session.Source                    = $Result.c2s.source
            $Session.SourcePort                = $Result.c2s.sport
            $Session.SourceZone                = $Result.c2s.'source-zone'
            $Session.Destination               = $Result.c2s.dst
            $Session.DestinationPort           = $Result.c2s.dport
            $Session.Protocol                  = $Result.c2s.proto
            
            $Session.DestinationZone           = $Result.s2c.'source-zone'
            $Session.SourceTranslatedIp        = $Result.s2c.dst
            $Session.SourceTranslatedPort      = $Result.s2c.dport
            $Session.DestinationTranslatedIp   = $Result.s2c.source
            $Session.DestinationTranslatedPort = $Result.s2c.sport
        } else {
            $Session.State                     = $Result.state
            $Session.Type                      = $Result.type
            $Session.Source                    = $Result.source
            $Session.SourcePort                = $Result.sport
            $Session.SourceZone                = $Result.from
            $Session.SourceTranslatedIp        = $Result.xsource
            $Session.SourceTranslatedPort      = $Result.xsport
            $Session.Destination               = $Result.dst
            $Session.DestinationPort           = $Result.dport
            $Session.DestinationZone           = $Result.to
            $Session.DestinationTranslatedIp   = $Result.xdst
            $Session.DestinationTranslatedPort = $Result.xdport
            $Session.Protocol                  = $Result.proto
        }

        $ReturnResults += $Session 
    }

    return $ReturnResults
}

###############################################################################
# Get-PaTacacsProfile
function Get-PaTacacsProfile {
    Param (
        [Parameter(Mandatory=$False,Position=0)]
        [string]$Name,

        [Parameter(Mandatory=$False,Position=1)]
        [string]$Vsys = "shared",

        [Parameter(Mandatory=$False,Position=2)]
        [string]$Device
    )
    
    $VerbosePrefix = "Get-PaTacacsProfile:"

    if ($global:PaDeviceObject.Connected) {
        $InfoObject        = New-Object PaTacacsProfile
        $InfoObject.Name   = $Name
        $InfoObject.Vsys   = $Vsys
        $InfoObject.Device = $Device
        $Response          = Get-PaConfig $InfoObject.GetXpath()

        $ConfigNode = 'tacplus'

        $ReturnObject = @()
        foreach ($entry in $Response.response.result.$ConfigNode.entry) {
            $NewEntry      = New-Object PaTacacsProfile
            $ReturnObject += $NewEntry

            $NewEntry.Vsys   = $Vsys
            $NewEntry.Device = $Device

            $NewEntry.Name                = $entry.name
            $NewEntry.Timeout             = $entry.timeout
            $NewEntry.AdminUseOnly        = $entry.'admin-use-only'
            $NewEntry.UseSingleConnection = $entry.'use-single-connection'
            $NewEntry.Servers             = @()

            foreach ($Server in $entry.server.entry) {
                $NewServer         = New-Object PaAuthServer
                $NewServer.Name    = $Server.name
                $NewServer.Server  = $Server.address
                $NewServer.Port    = $Server.port
                $NewEntry.Servers += $NewServer
            }
        }
        return $ReturnObject
    } else {
        Throw "$VerbosePrefix Not Connected, please use Get-PaConfig to connect before using other cmdlets."
    }
}

###############################################################################
# Get-PaVsys
function Get-PaVsys {
    Param (
    )
    
    $VerbosePrefix = "Get-PaVsys:"

    if ($global:PaDeviceObject.Connected) {
        # Get the data
        $Operation = '<show><system><state><filter-pretty>cfg.dns-vsys</filter-pretty></state></system></show>'
        $Result = Invoke-PaOperation $Operation
        
        # Sanatize it and add it to the array
        $Result = $Result.response.result.'#cdata-section' -replace "cfg.dns-vsys: ",""
        $Rx = [regex] "(.+?):"
        $Matches = $Rx.Matches($Result)
        $ReturnObject = @()
        foreach ($Match in $Matches) {
            $ReturnObject += ($Match.Groups[1].Value).Trim()
        }
        
        return $ReturnObject
    } else {
        Throw "$VerbosePrefix Not Connected, please use Get-PaConfig to connect before using other cmdlets."
    }
}

###############################################################################
# Invoke-PaOperation
function Invoke-PaOperation {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory=$False,Position=0)]
        [string]$Command
    )

    $VerbosePrefix = "Invoke-PaOperation:"

    $CheckConnection = $global:PaDeviceObject.checkConnectionStatus($VerbosePrefix)

    return $global:PaDeviceObject.invokeOperationalQuery($Command)
}

###############################################################################
# Invoke-PaSessionTracker
function Invoke-PaSessionTracker {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory=$False)]
        [int]$Interval = 5,

        [Parameter(Mandatory=$False)]
        [array]$ShowProperties = @("StartTime","Id","Application","Source","Destination","State","DestinationPort"),

        [Parameter(Mandatory=$False)]
        [int]$Count = 40,

        [Parameter(Mandatory=$False)]
        [string]$Application,

        [Parameter(Mandatory=$False)]
        [string]$Destination,

        [Parameter(Mandatory=$False)]
        [int]$DestinationPort,

        [Parameter(Mandatory=$False)]
        [string]$DestinationUser,

        [Parameter(Mandatory=$False)]
        [string]$EgressInterface,

        [Parameter(Mandatory=$False)]
        [string]$HwInterface,

        [Parameter(Mandatory=$False)]
        [string]$IngressInterface,

        [Parameter(Mandatory=$False)]
        [string]$SourceZone,

        [Parameter(Mandatory=$False)]
        [string]$DestinationZone,

        [Parameter(Mandatory=$False)]
        [string]$MinKb,

        [Parameter(Mandatory=$False)]
        [string]$Rule,

        [Parameter(Mandatory=$False)]
        [string]$NatRule,

        [Parameter(Mandatory=$False)]
        [string]$PbfRule,

        [Parameter(Mandatory=$False)]
        [string]$QosRule,

        [Parameter(Mandatory=$False)]
        [string]$QosClass,

        [Parameter(Mandatory=$False)]
        [string]$QosNodeId,

        [Parameter(Mandatory=$False)]
        [string]$Nat,

        [Parameter(Mandatory=$False)]
        [string]$Rematch,

        [Parameter(Mandatory=$False)]
        [string]$SslDecrypt,

        [Parameter(Mandatory=$False)]
        [string]$Type,

        [Parameter(Mandatory=$False)]
        [int]$Protocol,

        [Parameter(Mandatory=$False)]
        [string]$Source,

        [Parameter(Mandatory=$False)]
        [int]$SourcePort,

        [Parameter(Mandatory=$False)]
        [string]$SourceUser,

        [Parameter(Mandatory=$False)]
        [string]$State,

        [Parameter(Mandatory=$False)]
        [switch]$NoClear
    )

    $VerbosePrefix = "Invoke-PaSessionTracker:"
    
    $SessionParameters = $PSBOUNDPARAMETERS
    $SessionParameters.Remove("Interval") | Out-Null
    $SessionParameters.Remove("ShowProperties") | Out-Null
    $SessionParameters.Remove("NoClear") | Out-Null

    $StopWatch  = [System.Diagnostics.Stopwatch]::StartNew() # used by Write-Progress so it doesn't slow the whole function down

    # Header Function
    function WriteHeader([int]$TotalSessionsMatched,[int]$ShownSessions) {
        # Write top block of info.
        Write-Host "Total Sessions Matched: $TotalSessionsMatched"
        Write-Host "Sessions Shown: $ShownSessions"
        Write-Host ""
    }

    # GetColumnLengths
    function GetColumnLengths([array]$PropertiesDisplayed,[array]$Data) {
        $ColumnLengths = @()
        foreach ($Property in $PropertiesDisplayed) {
            $CurrentMaxLength = $Property.Length
            switch ($Property) {
                TickCount {
                    break
                }
                default {
                    foreach ($Datum in $Data) {
                        $CurrentLength = $Datum.$Property.ToString().Length
                        if ($CurrentLength -gt $CurrentMaxLength) {
                            $CurrentMaxLength = $CurrentLength
                        }
                    }
                }
            }

            # Create Object add to array
            $NewObject               = "" | Select-Object PropertyName,MaxLength
            $NewObject.PropertyName  = $Property
            $NewObject.MaxLength     = $CurrentMaxLength
            $ColumnLengths          += $NewObject
        }

        return $ColumnLengths
    }

    # WriteColumnHeaders
    function WriteColumnHeaders([array]$ColumnLengths) {
        # Selection
        Write-Host " " -NoNewline
        # Count
        Write-Host " " -NoNewline
        
        # Headers
        foreach ($Column in $ColumnLengths) {
            $ColumnLabel = $Column.PropertyName
            $Length      = $Column.MaxLength
            $ColumnLabel = $ColumnLabel.PadRight(($Length + 2)," ")
            
            Write-Host $ColumnLabel -NoNewline
        }
        Write-Host
    }



    While ($vkeycode -ne 81) {
        #$press = $host.ui.rawui.readkey("NoEcho,IncludeKeyDown")
        #$vkeycode = $press.virtualkeycode

        $OldSessions = $AllSessions | Select-Object *
        $Sessions = Get-PaSession @SessionParameters | Sort-Object StartTime
        $NewSessions = @()
        foreach ($Session in $Sessions[0..($Count-1)]) {
            $Lookup = $OldSessions | Where-Object { $_.Id -eq $Session.Id }
            if (!($Lookup)) {
                $NewSessions += $Session | Select-Object *,TickCount,Selected
            }
        }

        # Get proper number of OldSessions
        
        if (($NewSessions.Count -lt $Count) -and ($OldSessions.Count -gt 0)) {
            $AvailableCount = $Count - $NewSessions.Count
            $OldSessions = $OldSessions[0..($AvailableCount - 1)]
            $AllSessions = $NewSessions + $OldSessions
        } else {
            $AllSessions = $NewSessions
        }

        
        $AllSessions = $AllSessions | Select-Object * | Sort-Object StartTime -Descending
        $global:test3 = $AllSessions

        $LoopCount = 1
        while ($StopWatch.Elapsed.TotalMilliseconds -lt ($Interval * 1000)) {
                if (!($NoClear)) {
                    Clear-Host
                }

                # Write Header Block
                WriteHeader $Sessions.Count $AllSessions.Count
                
                #Extra Space for line counter
                Write-Host " " -NoNewLine

                # Find Column Length and output Headers
                $LengthValues = @()
                foreach ($p in $ShowProperties) {

                    # Find length of header
                    if ($p -eq "TickCount") {
                        $ValueMaxLength = 0
                    } else {
                        $global:test = $Sessions

                        $ValueMaxLength = 0
                        foreach ($s in $AllSessions) {
                            $CurrentLength = $s.$p.ToString().Length
                            if ($CurrentLength -gt $ValueMaxLength) {
                                $ValueMaxLength = $CurrentLength
                            }
                        }
                    }
                    if ($p.Length -gt $ValueMaxLength) {
                        Write-Verbose "$VerbosePrefix CurrentValue: $ValueMaxLength; NameLength: $($p.Length)"
                        $ValueMaxLength = $p.Length
                    }

                    # Log Column Lengths
                    $New = "" | Select-Object Name,MaxLength
                    $New.Name = $p
                    if ($p -eq "StartTime") {
                        $New.MaxLength = ([string](Get-Date -Format "MM/dd/yy HH:mm:ss")).Length
                    } else {
                        $New.MaxLength = $ValueMaxLength
                    }
                    $LengthValues += $New
                    Write-Verbose "$VerbosePrefix Name: $($New.Name); MaxLength: $($New.MaxLength)"

                    # Write Headers
                    $Header = $p.PadRight(($ValueMaxLength + 2)," ")
                    Write-Host $Header -NoNewline
                }
                
                # Add NewLine after Headers
                Write-Host
            
                $SessionCounter = 0
                foreach ($Session in $AllSessions) {
                    if ($Session.TickCount -gt 0) {
                        $Session.TickCount++
                    } else {
                        $Session.TickCount = 1
                    }
                    
                    if ($Session.Selected) {
                        Write-Host "*" -NoNewline
                    } else {
                        Write-Host " " -NoNewline
                    }
                    
                    $SessionCounter++
                    $SessionCounterString = "$SessionCounter".PadRight(3," ")
                    Write-Host $SessionCounterString -NoNewline

                    $Lookup = $OldSessions | Where-Object { $_.Id -eq $Session.Id }
                    $ReverseLookup = $Sessions | Where-Object { $_.Id -eq $Session.Id }

                    foreach ($p in $LengthValues) {
                        $PropertyName = $p.Name
                        Write-Verbose "$VerbosePrefix PropertyName: $PropertyName"
                        $PropertyValue  = $Session.$PropertyName.ToString()
                        Write-Verbose "$VerbosePrefix PropertyValue: $PropertyValue"
                        $PropertyLength = $p.MaxLength
                        Write-Verbose "$VerbosePrefix PropertyLength: $PropertyLength"
                        $Value = [string]$PropertyValue.PadRight(($PropertyLength + 2)," ")

                        $WriteHostParams = @{}
                        $WriteHostParams.NoNewLine = $true

                        # Format Date, I suspect there's a better way to handle this
                        if ($PropertyName -eq "StartTime") {
                            $WriteHostParams.Object = ([string](Get-Date -Date $Value -Format "MM/dd/yy HH:mm:ss")).PadRight(($PropertyLength + 2)," ")
                        } else {
                            $WriteHostParams.Object = $Value
                        }

                        if (($ReverseLookup) -and (!($Lookup))) {
                            # New Session
                            $WriteHostParams.ForegroundColor = "DarkBlue"
                            switch ($PropertyName) {
                                "State" {
                                    switch ($Value) {
                                        {$_ -match "active"} {
                                            $WriteHostParams.ForegroundColor = "DarkGreen"
                                        }
                                        {$_ -match "discard"} {
                                            $WriteHostParams.ForegroundColor = "DarkRed"
                                        }
                                        default {
                                            # Don't change anything (yet)
                                        }
                                    }
                                    break
                                }
                            }
                        } elseif (($Lookup) -and (!($ReverseLookup))) {
                            # Inactive Session
                            $WriteHostParams.ForegroundColor = "DarkGray"
                        }
                        Write-Host @WriteHostParams
                    }
                    Write-Host
                }

                # Write blank lines
                $BlankLines = $Count - $AllSessions.Count
                for ($b = 0;$b -lt $BlankLines;$b++) {
                    Write-Host
                }

            #Start-Sleep $Interval
            # Show Progress Bar between API Calls
<#
            if ($StopWatch.Elapsed.TotalMilliseconds -ge ($LoopCount * 1000)) {
                $LoopCount++
                $PercentComplete = [math]::truncate($LoopCount / $Interval * 100)
                Write-Progress -Activity "Waiting to refresh sessions: $($Interval - $LoopCount)..." -PercentComplete $PercentComplete
                if ($StopWatch.Elapsed.TotalMilliseconds -ge ($Interval * 1000)) {
                    $StopWatch.Reset()
                    $StopWatch.Start()
                    $LoopCount = 1
                    Write-Progress -Activity "Refreshing..." -PercentComplete 100 -Completed
                }
            }#>

<#
            for ($p = 0;$p -lt $Interval;$p++) {
                $PercentComplete = ($p / $Interval) * 100
                $Activity = "Waiting to refresh sessions: $($Interval - $p)..."
                Write-Progress -Activity $Activity -PercentComplete $PercentComplete
                Start-Sleep 1
            }
            Write-Progress -Activity "Refreshing..." -PercentComplete 100#>

            
        }
    }
}

###############################################################################
# Set-PaConfig
function Set-PaConfig {
    Param (
        [Parameter(Mandatory=$True,Position=0,ParameterSetName="manual")]
        [string]$Xpath = "/config",

        [Parameter(Mandatory=$True,Position=1,ParameterSetName="manual")]
        [string]$ElementAsString,

        [Parameter(Mandatory=$True,Position=1,ParameterSetName="object",ValueFromPipeline=$true)]
        $PaObject
    )
    
    $VerbosePrefix = "Set-PaConfig:"

    if ($global:PaDeviceObject.Connected) {
        if ($PaObject) {
            Write-Verbose "$VerbosePrefix Getting info from Object"

            $ElementAsString = $PaObject.PrintPlainXml()
            $Xpath           = $PaObject.getXPath()
            
            Write-Verbose "$VerbosePrefix Element: $ElementAsString"
            Write-Verbose "$VerbosePrefix Xpath: $Xpath"
        }
        return $global:PaDeviceObject.invokeConfigQuery($Xpath,"set",$ElementAsString)
    } else {
        Throw "$VerbosePrefix Not Connected, please use Get-PaConfig to connect before using other cmdlets."
    }
}

###############################################################################
## Export Cmdlets
###############################################################################

Export-ModuleMember *-*