Public/Connection.ps1

#
# Copyright 2022, Alexis La Goutte <alexis dot lagoutte at gmail dot com>
#
# SPDX-License-Identifier: Apache-2.0
#

function Connect-FGT {

    <#
      .SYNOPSIS
      Connect to a FortiGate

      .DESCRIPTION
      Connect to a FortiGate

      .EXAMPLE
      Connect-FGT -Server 192.0.2.1

      Connect to a FortiGate with IP 192.0.2.1

      .EXAMPLE
      Connect-FGT -Server 192.0.2.1 -SkipCertificateCheck

      Connect to a FortiGate with IP 192.0.2.1 and disable Certificate (chain) check

      .EXAMPLE
      Connect-FGT -Server 192.0.2.1 -httpOnly

      Connect to a FortiGate using HTTP (unsecure !) with IP 192.0.2.1 using (Get-)credential

      .EXAMPLE
      Connect-FGT -Server 192.0.2.1 -port 4443

      Connect to a FortiGate using HTTPS (with port 4443) with IP 192.0.2.1 using (Get-)credential

      .EXAMPLE
      $cred = get-credential
      Connect-FGT -Server 192.0.2.1 -credential $cred

      Connect to a FortiGate with IP 192.0.2.1 and passing (Get-)credential

      .EXAMPLE
      $mysecpassword = ConvertTo-SecureString mypassword -AsPlainText -Force
      Connect-FGT -Server 192.0.2.1 -Username manager -Password $mysecpassword

      Connect to a FortiGate with IP 192.0.2.1 using Username and Password

      .EXAMPLE
      $fw1 = Connect-FGT -Server 192.0.2.1
      Connect to a FortiGate with IP 192.0.2.1 and store connection info to $fw1 variable

      .EXAMPLE
      $fw2 = Connect-FGT -Server 192.0.2.1 -DefaultConnection:$false

      Connect to a FortiGate with IP 192.0.2.1 and store connection info to $fw2 variable
      and don't store connection on global ($DefaultFGTConnection) variable

      .EXAMPLE
      Connect-FGT -Server 192.0.2.1 -Timeout 15

      Connect to a Fortigate with IP 192.0.2.1 and timeout the operation if it takes longer
      than 15 seconds to form a connection. The Default value "0" will cause the connection to never timeout.

      .EXAMPLE
      $apiToken = Get-Content fortigate_api_token.txt
      Connect-FGT -Server -192.0.2.1 -ApiToken $apiToken

      Connect to a FortiGate with IP 192.0.2.1 and passing api token

      .EXAMPLE
      $mynewpassword = ConvertTo-SecureString mypassword -AsPlainText -Force
      Connect-FGT -Server 192.0.2.1 -new_password $mysecpassword

      Connect to a FortiGate with IP 192.0.2.1 and change the password

      .EXAMPLE
      $mysecpassword = ConvertTo-SecureString mypassword -AsPlainText -Force
      Connect-FGT -Server 192.0.2.1 -Username admin -Password $mysecpassword -token_code XXXXX

      Connect to a FortiGate with IP 192.0.2.1 using Username, Password and (Forti)token code XXXXXX

     .EXAMPLE
      Connect-FGT -Server 192.0.2.1 -token_prompt

      Connect to a FortiGate with IP 192.0.2.1 and it will ask to get (Forti)Token code when connect

     .EXAMPLE
      $lic = Get-Content -Raw license.lic
      $Credential = New-Object System.Management.Automation.PSCredential("admin", (new-object System.Security.SecureString))
      $mynewpassword = ConvertTo-SecureString mypassword -AsPlainText -Force
      Connect-FGT -Server 192.0.2.1 -credentials $credential -New_Password $mynewsecpassword -license $lic -SkipCertificateCheck

      Connect to a FortiGate with IP 192.0.2.1 and upload the new license and change the password
  #>

    [CmdletBinding(DefaultParameterSetName = 'default')]
    [OutputType([System.Collections.Hashtable])]
    Param(
        [Parameter(Mandatory = $true, position = 1)]
        [String]$Server,
        [Parameter(ParameterSetName = "default")]
        [Parameter(Mandatory = $false)]
        [String]$Username,
        [Parameter(ParameterSetName = "default")]
        [Parameter(Mandatory = $false)]
        [SecureString]$Password,
        [Parameter(ParameterSetName = "token")]
        [Parameter(Mandatory = $false)]
        [string]$ApiToken,
        [Parameter(ParameterSetName = "default")]
        [Parameter(Mandatory = $false)]
        [SecureString]$New_Password,
        [Parameter(ParameterSetName = "default")]
        [Parameter(Mandatory = $false)]
        [PSCredential]$Credentials,
        [switch]$httpOnly = $false,
        [Parameter(Mandatory = $false)]
        [switch]$SkipCertificateCheck = $false,
        [Parameter(Mandatory = $false)]
        [ValidateRange(1, 65535)]
        [int]$port,
        [Parameter(Mandatory = $false)]
        [int]$Timeout = 0,
        [Parameter(Mandatory = $false)]
        [string]$token_code,
        [Parameter(Mandatory = $false)]
        [switch]$token_prompt,
        [Parameter(Mandatory = $false)]
        [string]$license,
        [Parameter(Mandatory = $false)]
        [string[]]$vdom,
        [Parameter(Mandatory = $false)]
        [boolean]$DefaultConnection = $true
    )

    Begin {
    }

    Process {

        $connection = @{server = ""; session = ""; httpOnly = $false; port = ""; headers = ""; invokeParams = ""; vdom = ""; version = ""; serial = "" }

        $invokeParams = @{DisableKeepAlive = $false; UseBasicParsing = $true; SkipCertificateCheck = $SkipCertificateCheck; TimeoutSec = $Timeout }

        if (("Desktop" -eq $PSVersionTable.PsEdition) -or ($null -eq $PSVersionTable.PsEdition)) {
            #Remove -SkipCertificateCheck from Invoke Parameter (not supported <= PS 5)
            $invokeParams.remove("SkipCertificateCheck")
        }
        else {
            #Core Edition
            #Remove -UseBasicParsing (Enable by default with PowerShell 6/Core)
            $invokeParams.remove("UseBasicParsing")
        }

        if ($null -eq $PSVersionTable.PsEdition) {
            #Remove -UseBasicParsing from Invoke Parameter (not available on Invoke-RestMethod PS < 5)
            $invokeParams.remove("UseBasicParsing")
        }

        if ($httpOnly) {
            if (!$port) {
                $port = 80
            }
            $connection.httpOnly = $true
            $url = "http://${Server}:${port}/"

        }
        else {
            if (!$port) {
                $port = 443
            }

            #for PowerShell (<=) 5 (Desktop), Enable TLS 1.1, 1.2 and Disable SSL chain trust (needed/recommanded by FortiGate)
            if (("Desktop" -eq $PSVersionTable.PsEdition) -or ($null -eq $PSVersionTable.PsEdition)) {
                #Enable TLS 1.1 and 1.2
                Set-FGTCipherSSL
                if ($SkipCertificateCheck) {
                    #Disable SSL chain trust...
                    Set-FGTuntrustedSSL
                }
            }

            $url = "https://${Server}:${port}/"
        }

        $headers = @{ "content-type" = "application/json" }
        if ($ApiToken) {
            $headers += @{ "Authorization" = "Bearer $ApiToken" }
        }
        else {
            #If there is a password (and a user), create a credentials
            if ($Password) {
                $Credentials = New-Object System.Management.Automation.PSCredential($Username, $Password)
            }
            #Not Credentials (and no password)
            if ($null -eq $Credentials) {
                $Credentials = Get-Credential -Message 'Please enter administrative credentials for your FortiGate'
            }
            $postParams = @{username = $Credentials.username; secretkey = $Credentials.GetNetworkCredential().Password; ajax = 1 }
            $uri = $url + "logincheck"
            $iwrResponse = $null
            try {
                $iwrResponse = Invoke-WebRequest $uri -Method POST -Body $postParams -SessionVariable FGT @invokeParams
            }
            catch {
                Show-FGTException $_
                throw "Unable to connect to FortiGate"
            }

            #check if need token...
            if ( $iwrResponse.Content[0] -eq "3") {
                if ( $PsBoundParameters.ContainsKey('token_code') -or $PsBoundParameters.ContainsKey('token_prompt') ) {
                    if ( $PsBoundParameters.ContainsKey('token_prompt')) {
                        $token_code = Read-Host "Token"
                    }
                    $postParams += @{token_code = $token_code }
                    try {
                        $iwrResponse = Invoke-WebRequest $uri -Method POST -Body $postParams -WebSession $FGT @invokeParams
                    }
                    catch {
                        Show-FGTException $_
                        throw "Unable to connect to FortiGate with a token"
                    }
                }
            }

            #first byte return is a status code
            switch ($iwrResponse.Content[0]) {
                '0' {
                    throw "Log in failure. Most likely an incorrect username/password combo"
                }
                '1' {
                    #no thing, it is good ! continue
                }
                '2' {
                    throw "Admin is now locked out (Please retry in 60 seconds)"
                }
                '3' {
                    throw "Two-factor Authentication is needed (use -token_code XXXXXX or -token_prompt)"
                }
                '4' {
                    if (-not $PsBoundParameters.ContainsKey('new_password')) {
                        #throw if you don't have specify new_password
                        throw "Need to change the password (use -new_password parameter)"
                    }
                }
            }

            #Search crsf cookie and to X-CSRFTOKEN
            $cookies = $FGT.Cookies.GetCookies($uri)
            foreach ($cookie in $cookies) {
                if ($cookie.name -like "ccsrftoken*") {
                    $cookie_csrf = $cookie.value
                }
            }

            # throw if don't found csrf cookie...
            if ($null -eq $cookie_csrf) {
                throw "Unable to found CSRF Cookie"
            }

            #Remove extra "quote"
            $cookie_csrf = $cookie_csrf -replace '["]', ''
            #Add csrf cookie to header (X-CSRFTOKEN)
            $headers += @{"X-CSRFTOKEN" = $cookie_csrf }

            $uri = $url + "logindisclaimer"
            if ($iwrResponse.Content -match '/logindisclaimer') {
                try {
                    Invoke-WebRequest $uri -Method "POST" -WebSession $FGT -Body @{ confirm = 1 ; ajax = 1 } @invokeParams | Out-Null
                }
                catch {
                    throw "Unable to confirm disclaimer"
                }
            }

            if ($PsBoundParameters.ContainsKey('new_password')) {
                $uri = $url + "loginpwd_change"
                $new_pwd = ConvertFrom-SecureString -SecureString $new_password -AsPlainText;
                $postParams = @{
                    CSRF_TOKEN = $cookie_csrf
                    old_pwd    = $Credentials.GetNetworkCredential().Password;
                    pwd1       = $new_pwd
                    pwd2       = $new_pwd
                    ajax       = 1;
                    confirm    = 1
                }
                try {
                    Invoke-WebRequest $uri -Method "POST" -WebSession $FGT -Body $postParams @invokeParams | Out-Null
                }
                catch {
                    throw "Unable to change password"
                }

                #Reconnect...
                Connect-FGT -server $server -port $port -httpOnly:$httpOnly -vdom $vdom -Username $Credentials.username -Password $new_password -license $license -SkipCertificateCheck:$SkipCertificateCheck -DefaultConnection $DefaultConnection
                return
            }

            if ($iwrResponse.Content -match '/system/vm/license') {
                if (-not $PsBoundParameters.ContainsKey('license')) {
                    #throw if you don't have specify license
                    throw "Need to install a license (use -license parameter)"
                }
                $uri = $url + "api/v2/monitor/system/vmlicense/upload"

                #Convert the license to base64 and POST to vmlicense/upload
                $Bytes = [System.Text.Encoding]::UTF8.GetBytes($license)
                $license_b64 = [Convert]::ToBase64String($Bytes)
                $postParams = @{
                    file_content = $license_b64
                }
                try {
                    Invoke-RestMethod $uri -Method "POST" -Header $headers -WebSession $FGT -Body ($postParams | ConvertTo-Json) @invokeParams | Out-Null
                }
                catch {
                    Show-FGTException $_
                    throw "Unable to upload license"
                }
                Write-Warning "Fortigate restart (installing license...)"
                return
            }
        }

        $uri = $url + "api/v2/monitor/system/firmware"
        try {
            $version = Invoke-RestMethod $uri -Method "get" -Header $headers -WebSession $FGT @invokeParams
        }
        catch {
            if ($ApiToken) {
                Show-FGTException $_
                throw "Authentication failure. Wrong token or not a Trusted Host."
            }
            else {
                throw "Unable to find FGT version"
            }
        }

        $connection.server = $server
        $connection.session = $FGT
        $connection.headers = $headers
        $connection.port = $port
        $connection.invokeParams = $invokeParams
        $connection.vdom = $vdom
        $connection.serial = $version.serial
        if ($version.results.current.major) {
            $connection.version = [version]"$($version.results.current.major).$($version.results.current.minor).$($version.results.current.patch)"
        }
        else {
            #Old release like 5.x with no major/minor/patch
            $connection.version = [version]"$($version.results.current.version -replace 'v','')"
            Write-Warning "Old and no longer supported FortiOS with limited API (no filtering...)"
        }

        if ( $DefaultConnection ) {
            set-variable -name DefaultFGTConnection -value $connection -scope Global
        }

        $connection
    }

    End {
    }
}


function Set-FGTConnection {

    <#
        .SYNOPSIS
        Configure FGT connection Setting

        .DESCRIPTION
        Configure FGT connection Setting (Vdom...)

        .EXAMPLE
        Set-FGTConnection -vdom vdomY

        Configure default connection vdom to vdomY

        .EXAMPLE
        Set-FGTConnection -vdom $null

        Restore vdom configuration to default (by default root)
    #>


    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Low')]
    Param(
        [Parameter(Mandatory = $false)]
        [string[]]$vdom,
        [Parameter(Mandatory = $false)]
        [psobject]$connection = $DefaultFGTConnection
    )

    Begin {
    }

    Process {

        if ($PSCmdlet.ShouldProcess($connection.server, 'Set default vdom on connection')) {
            $connection.vdom = $vdom
        }

    }

    End {
    }
}

function Disconnect-FGT {

    <#
        .SYNOPSIS
        Disconnect a FortiGate

        .DESCRIPTION
        Disconnect the connection of FortiGate

        .EXAMPLE
        Disconnect-FGT

        Disconnect the connection

        .EXAMPLE
        Disconnect-FGT -confirm:$false

        Disconnect the connection with no confirmation

    #>


    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
    Param(
        [Parameter(Mandatory = $false)]
        [psobject]$connection = $DefaultFGTConnection
    )

    Begin {
    }

    Process {

        $url = "logout"

        if ($PSCmdlet.ShouldProcess($connection.server, 'Proceed with removal of FortiGate connection ?')) {
            $null = invoke-FGTRestMethod -method "POST" -uri $url -connection $connection
            if (Test-Path variable:global:DefaultFGTConnection) {
                Remove-Variable -name DefaultFGTConnection -scope global
            }
        }
    }

    End {
    }
}