Public/Invoke-PanXApi.ps1

function Invoke-PanXApi {
   <#
   .SYNOPSIS
   Abstracts PAN-OS XML API core modes (actions)
   .DESCRIPTION
   -Keygen is for generating API key (don't use for normal operations, use New-PanDevice)
    
   -Version is an easy way to verify API access is functioning
 
   -Config -Show retrieves ACTIVE configuration.
   -Config -Show only works when provided XPath specifies single node.
   -Config -Show can use relative XPath.
 
   -Config -Get retrieves CANDIDATE, uncommitted configuration.
   -Config -Get works with single and multiple nodes.
   -Config -Get requires absolute XPath.
 
   -Config -Set adds, updates, or merges configuration nodes. -Config -Set actions are non-destructive and are only additive.
   -Config -Edit replaces configuration nodes. -Config -Edit actions can be destructive.
 
   -Import -Category "certificate" is for uploading certificates WITHOUT private key (processes just the certificate)
   -Import -Category "keypair" is for uploading certificates WITH private key (processes both certificate and private key)
      -File is the path or FileInfo object to the supported (PKCS12, Base64 encoded PEM) certificate on local disk
      -CertName parameter is what is used for PAN-OS certificate name, the certificate filename on local disk is ignored
         -Periods (.) are ignored/removed by PAN-OS. Avoid them in CertName
      -CertPassphrase is used when importing the private key
      -CertVsys is optional. If omitted, the API places the certificate in shared
   .NOTES
   .INPUTS
   PanDevice[]
      You can pipe a PanDevice to this cmdlet
   .OUTPUTS
   PanResponse
   .EXAMPLE
   PS> Invoke-PanXApi -Device $Device -Keygen
   .EXAMPLE
   PS> Invoke-PanXApi -Device $Device -Version
   .EXAMPLE
   PS> Invoke-PanXApi -Device $Device -Op -Cmd "<show><system><info></info></system></show>"
   .EXAMPLE
   PS> Invoke-PanXApi -Device $Device -Commit
   .EXAMPLE
   PS> Invoke-PanXApi -Device $Device -Config -Get -XPath "/config/xpath"
   .EXAMPLE
   PS> Invoke-PanXApi -Device $Device -Config -Set -XPath "/config/xpath" -Element "<outer><inner>value</inner></outer>"
   .EXAMPLE
   PS> Invoke-PanXApi -Device $Device -Uid -Cmd "<uid-message>...</uid-message>"
   .EXAMPLE
   Import and process the certificate and private key within, note the -Category keypair
   PS> Invoke-PanXApi -Device $Device -Import -Category keypair -File "C:\path\to\cert.p12" -CertName "gp-portal-acme-com" -CertPassphrase "acme1234"
 
   Import and process just the certificate, ignoring the private key, note the -Category certificate. The -CertPassphrase is ignored by API and is not required.
   PS> Invoke-PanXApi -Device $Device -Import -Category certificate -File "C:\path\to\cert.p12" -CertName "gp-portal-acme-com" -CertPassphrase "acme1234"
   #>

   [CmdletBinding(SupportsShouldProcess,ConfirmImpact='None')]
   param(
      # $Device required in all parameter sets.
      [parameter(Mandatory=$true,Position=0,ValueFromPipeline=$true,HelpMessage='PanDevice against which XML-API will be invoked.')]
      [PanDevice[]] $Device,
      # Begin Keygen parameter set
      [parameter(Mandatory=$true,Position=1,ParameterSetName='Keygen',HelpMessage='Type: keygen')]
      [Switch] $Keygen,
      # Begin Version parameter set
      [parameter(Mandatory=$true,Position=1,ParameterSetName='Version',HelpMessage='Type: version')]
      [Switch] $Version,
      # Begin Op and Uid parameter sets
      [parameter(Mandatory=$true, Position=1,ParameterSetName='Op',HelpMessage='Type: operational commands')]
      [Switch] $Op,
      [parameter(Mandatory=$true,Position=1,ParameterSetName='Uid',HelpMessage='Type: user-id commands')]
      [Switch] $Uid,
      [parameter(Mandatory=$true,Position=2,ParameterSetName='Op',HelpMessage='XML formatted operational command')]
      [parameter(Mandatory=$true,Position=2,ParameterSetName='Uid',HelpMessage='XML formatted user-id payload')]
      [String] $Cmd,
      # Begin Commit parameter set
      [parameter(Mandatory=$true,Position=1,ParameterSetName='Commit',HelpMessage='Type: commit commands')]
      [Switch] $Commit,
      [parameter(Mandatory=$true,Position=2,ParameterSetName='Commit',HelpMessage='Force commit switch parameter')]
      [Switch] $Force,
      # Begin Config-Set and Config-Get parameter sets
      [parameter(Mandatory=$true,Position=1,ParameterSetName='Config-Get',HelpMessage='Type: config ')]
      [parameter(Mandatory=$true,Position=1,ParameterSetName='Config-Show',HelpMessage='Type: config ')]
      [parameter(Mandatory=$true,Position=1,ParameterSetName='Config-Set',HelpMessage='Type: config ')]
      [parameter(Mandatory=$true,Position=1,ParameterSetName='Config-Edit',HelpMessage='Type: config ')]
      [parameter(Mandatory=$true,Position=1,ParameterSetName='Config-Delete',HelpMessage='Type: config ')]
      [Switch] $Config,
      [parameter(Mandatory=$true,Position=2,ParameterSetName='Config-Get',HelpMessage='Retrieve candidate configuration')]
      [Switch] $Get,
      [parameter(Mandatory=$true,Position=2,ParameterSetName='Config-Show',HelpMessage='Retrieve active configuration')]
      [Switch] $Show,
      [parameter(Mandatory=$true,Position=2,ParameterSetName='Config-Set',HelpMessage='Add or create a new object')]
      [Switch] $Set,
      [parameter(Mandatory=$true,Position=2,ParameterSetName='Config-Edit',HelpMessage='Replace existing object (or hierarchy)')]
      [Switch] $Edit,
      [parameter(Mandatory=$true,Position=2,ParameterSetName='Config-Delete',HelpMessage='Delete an object')]
      [Switch] $Delete,
      [parameter(Mandatory=$true,ParameterSetName='Config-Get',HelpMessage='Config XPath')]
      [parameter(Mandatory=$true,ParameterSetName='Config-Show',HelpMessage='Config XPath')]
      [parameter(Mandatory=$true,ParameterSetName='Config-Set',HelpMessage='Config XPath')]
      [parameter(Mandatory=$true,ParameterSetName='Config-Edit',HelpMessage='Config XPath')]
      [parameter(Mandatory=$true,ParameterSetName='Config-Delete',HelpMessage='Config XPath')]
      [String] $XPath,
      [parameter(ParameterSetName='Config-Get',HelpMessage='Config Element')]
      [parameter(ParameterSetName='Config-Show',HelpMessage='Config Element')]
      [parameter(Mandatory=$true,ParameterSetName='Config-Set',HelpMessage='Config Element')]
      [parameter(Mandatory=$true,ParameterSetName='Config-Edit',HelpMessage='Config Element')]
      [parameter(ParameterSetName='Config-Delete',HelpMessage='Config Element')]
      [String] $Element,
      # Begin Import parameter set
      [parameter(Mandatory=$true,Position=1,ParameterSetName='Import-Default',HelpMessage='Type: import')]
      [parameter(Mandatory=$true,Position=1,ParameterSetName='Import-Cert-Key',HelpMessage='Type: import')]
      [Switch] $Import,
      [parameter(Mandatory=$true,Position=2,ParameterSetName='Import-Default',HelpMessage='File path to upload')]
      [parameter(Mandatory=$true,Position=2,ParameterSetName='Import-Cert-Key',HelpMessage='File path to upload')]
      [System.IO.FileInfo] $File,
      [parameter(Mandatory=$true,Position=3,ParameterSetName='Import-Default',HelpMessage='Import category: certificate, software, etc.')]
      [parameter(Mandatory=$true,Position=3,ParameterSetName='Import-Cert-Key',HelpMessage='Import category: certificate, software, etc.')]
      [ValidateSet(
         # Software
         'software',
         # Content
         'anti-virus','content','url-database','signed-url-database',
         # Licenses
         'license',
         # Configuration
         'configuration',
         # Certificates/Keys
         'certificate','high-availability-key','keypair',
         # Response Pages
         'application-block-page','captive-portal-text','file-block-continue-page','file-block-page','global-protect-portal-custom-help-page',
         'global-protect-portal-custom-login-page','global-protect-portal-custom-welcome-page','ssl-cert-status-page','ssl-optout-text',
         'url-block-page','url-coach-text','virus-block-page',
         # Clients
         'global-protect-client',
         # Custom Logo
         'custom-logo'
      )]
      [String] $Category,
      [parameter(Mandatory=$true,ParameterSetName='Import-Cert-Key',HelpMessage='Certificate friendly name')]
      [String] $CertName,
      [parameter(Mandatory=$true,ParameterSetName='Import-Cert-Key',HelpMessage='Certificate format: pkcs12, pem')]
      [ValidateSet('pkcs12','pem')]
      [String] $CertFormat,
      [parameter(ParameterSetName='Import-Cert-Key',HelpMessage='Required when including the certificate private key')]
      [String] $CertPassphrase,
      [parameter(ParameterSetName='Import-Cert-Key',HelpMessage='Optional. If empty, defaults to shared')]
      [String] $CertVsys
   )

   Begin {
      # Propagate -Debug and -Verbose to this module function, https://tinyurl.com/y5dcbb34
      if($PSBoundParameters.Debug) { $DebugPreference = 'Continue' }
      if($PSBoundParameters.Verbose) { $VerbosePreference = 'Continue' }
      # Announce
      Write-Debug ($MyInvocation.MyCommand.Name + ':')
   } # Begin block

   Process {
      foreach($DeviceCur in $PSBoundParameters.Device) {
         # API type=keygen
         if ($PSCmdlet.ParameterSetName -eq 'Keygen') {
            Write-Debug ($MyInvocation.MyCommand.Name + ': type=keygen')
            $PanApiType = 'keygen'
            $InvokeParams = @{
               'Method' = 'Post';
               'Uri' = $('{0}://{1}:{2}/api' -f `
                  $DeviceCur.Protocol,
                  $DeviceCur.Name,
                  $DeviceCur.Port
               ) # Uri sub-expression
               'Body' = @{
                  'type' = $PanApiType;
                  'user' = $DeviceCur.Credential.UserName;
                  'password' = $DeviceCur.Credential.GetNetworkCredential().Password
               } # Body hash table
            } # InvokeParams hash table
         } # End API type=keygen

         # API type=version
         elseif ($PSCmdlet.ParameterSetName -eq 'Version') {
            Write-Debug ($MyInvocation.MyCommand.Name + ': type=version')
            $PanApiType = 'version'
            $InvokeParams = @{
               'Method' = 'Post';
               'Uri' = $('{0}://{1}:{2}/api' -f `
                  $DeviceCur.Protocol,
                  $DeviceCur.Name,
                  $DeviceCur.Port
               ) # Uri sub-expression
               'Body' = @{
                  'type' = $PanApiType;
                  'key' = $(New-Object -TypeName PSCredential -ArgumentList 'user',$DeviceCur.Key).GetNetworkCredential().Password
               } # Body hash table
            } # InvokeParams hash table
         } # End API type=version

         # API type=op
         elseif ($PSCmdlet.ParameterSetName -eq 'Op') {
            Write-Debug ($MyInvocation.MyCommand.Name + ': type=op')
            $PanApiType = 'op'
            $PanApiCmd = $PSBoundParameters.Cmd
            $InvokeParams = @{
               'Method' = 'Post';
               'Uri' = $('{0}://{1}:{2}/api' -f `
                  $DeviceCur.Protocol,
                  $DeviceCur.Name,
                  $DeviceCur.Port
               ) # Uri sub-expression
               'Body' = @{
                  'type' = $PanApiType;
                  'cmd' = $PanApiCmd;
                  'key' = $(New-Object -TypeName PSCredential -ArgumentList 'user',$DeviceCur.Key).GetNetworkCredential().Password
               } # Body hash table
            } # InvokeParams hash table
         } # End API type=op

         # API type=user-id
         elseif ($PSCmdlet.ParameterSetName -eq 'Uid') {
            Write-Debug ($MyInvocation.MyCommand.Name + ': type=user-id')
            $PanApiType = 'user-id'
            $PanApiCmd = $PSBoundParameters.Cmd
            $InvokeParams = @{
               'Method' = 'Post';
               'Uri' = $('{0}://{1}:{2}/api' -f `
                  $DeviceCur.Protocol,
                  $DeviceCur.Name,
                  $DeviceCur.Port
               ); # Uri sub-expression
               'Body' = @{
                  'type' = $PanApiType;
                  'cmd' = $PanApiCmd;
                  'key' = $(New-Object -TypeName PSCredential -ArgumentList 'user',$DeviceCur.Key).GetNetworkCredential().Password
               } # Body hash table
            } # InvokeParams hash table
         } # End API type=user-id

         # API type=commit
         elseif ($PSCmdlet.ParameterSetName -eq 'Commit') {
            Write-Debug ($MyInvocation.MyCommand.Name + ': type=commit')
            $PanApiType = 'commit'
            if ($Force.IsPresent) {
               $PanApiCmd = '<commit><force></force></commit'
            }
            else {
               $PanApiCmd = '<commit></commit>'
            }
            $InvokeParams = @{
               'Method' = 'Post';
               'Uri' = $('{0}://{1}:{2}/api' -f `
                  $DeviceCur.Protocol,
                  $DeviceCur.Name,
                  $DeviceCur.Port
               ) # Uri sub-expression
               'Body' = @{
                  'type' = $PanApiType;
                  'cmd' = $PanApiCmd;
                  'key' = $(New-Object -TypeName PSCredential -ArgumentList 'user',$DeviceCur.Key).GetNetworkCredential().Password
               } # Body hash table
            } # InvokeParams hash table
         } # End API type=commit

         # API type=config
         elseif ($PSCmdlet.ParameterSetName -like 'Config-*') {
            Write-Debug ($MyInvocation.MyCommand.Name + ': type=config')
            $PanApiType = 'config'
            if ($PSBoundParameters.Get.IsPresent) {
               $PanApiAction = 'get'
            }
            elseif ($PSBoundParameters.Show.IsPresent) {
               $PanApiAction = 'show'
            }
            elseif ($PSBoundParameters.Set.IsPresent) {
               $PanApiAction = 'set'
            }
            elseif ($PSBoundParameters.Edit.IsPresent) {
               $PanApiAction = 'edit'
            }
            elseif ($PSBoundParameters.Delete.IsPresent) {
               $PanApiAction = 'delete'
            }
            Write-Debug ($MyInvocation.MyCommand.Name + ": action=$PanApiAction")

            $PanApiXPath = $PSBoundParameters.XPath
            $PanApiElement = $PSBoundParameters.Element

            # Element is optional for several actions. If present, include in the Uri.
            if (-not [String]::IsNullOrEmpty($PanApiElement)) {
               $InvokeParams = @{
                  'Method' = 'Post';
                  'Uri' = $('{0}://{1}:{2}/api' -f `
                     $DeviceCur.Protocol,
                     $DeviceCur.Name,
                     $DeviceCur.Port
                  ) # Uri sub-expression
                  'Body' = @{
                     'type' = $PanApiType;
                     'action' = $PanApiAction;
                     'xpath' = $PanApiXPath;
                     'element' = $PanApiElement;
                     'key' = $(New-Object -TypeName PSCredential -ArgumentList 'user',$DeviceCur.Key).GetNetworkCredential().Password
                  } # Body hash table
               } # InvokeParams hash table
            }
            # Else, do not include element in the Uri
            else {
               $InvokeParams = @{
                  'Method' = 'Post';
                  'Uri' = $('{0}://{1}:{2}/api' -f `
                     $DeviceCur.Protocol,
                     $DeviceCur.Name,
                     $DeviceCur.Port
                  ) # Uri sub-expression
                  'Body' = @{
                     'type' = $PanApiType;
                     'action' = $PanApiAction;
                     'xpath' = $PanApiXPath;
                     'key' = $(New-Object -TypeName PSCredential -ArgumentList 'user',$DeviceCur.Key).GetNetworkCredential().Password
                  } # Body hash table
               } # InvokeParams hash table
            }
         } # End API type=config

         # API type=import
         elseif ($PSCmdlet.ParameterSetName -like 'Import-*') {
            Write-Debug ($MyInvocation.MyCommand.Name + ': type=Import')
            $PanApiType = 'import'
            Write-Debug ($MyInvocation.MyCommand.Name + ': category=' + $Category)

            if($PSCmdlet.ParameterSetName -eq 'Import-Default') {
               Write-Debug ($MyInvocation.MyCommand.Name + ": type=Import (Default)")
               $InvokeParams = @{
                  'Method' = 'Post';
                  'Uri' = $('{0}://{1}:{2}/api?key={3}&type={4}&category={5}' -f `
                     $DeviceCur.Protocol,
                     $DeviceCur.Name,
                     $DeviceCur.Port,
                     $(New-Object -TypeName PSCredential -ArgumentList 'user',$DeviceCur.Key).GetNetworkCredential().Password,
                     $PanApiType,
                     $PSBoundParameters.Category
                  ); # Uri sub-expression
               } # InvokeParams hash table

               # Submit file to New-MultipartFormData for generating Content-Type header and multipart/form-data encoded body
               # Make sure to use -UnquotedBoundary for PAN-OS XML API limitation/workaround. See New-MultipartFormData
               $MPFData = New-MultipartFormData -File $PSBoundParameters.File -UnquotedBoundary
               # Add ContentType header to InvokeParams
               $InvokeParams.Add('ContentType', $MPFData.Header.ContentType)
               $InvokeParams.Add('Body', $MPFData.Body)

            } # end ParameterSetName Import-Default

            elseif($PSCmdlet.ParameterSetName -eq 'Import-Cert-Key') {
               Write-Debug ($MyInvocation.MyCommand.Name + ": type=Import (Cert-Key)")
               $InvokeParams = @{
                  'Method' = 'Post';
                  'Uri' = $('{0}://{1}:{2}/api?key={3}&type={4}&category={5}&certificate-name={6}&format={7}' -f `
                     $DeviceCur.Protocol,
                     $DeviceCur.Name,
                     $DeviceCur.Port,
                     $(New-Object -TypeName PSCredential -ArgumentList 'user',$DeviceCur.Key).GetNetworkCredential().Password,
                     $PanApiType,
                     $PSBoundParameters.Category,
                     $PSBoundParameters.CertName,
                     $PSBoundParameters.CertFormat
                  ); # Uri sub-expression
               } # InvokeParams hash table

               # Submit file to New-MultipartFormData for generating Content-Type header and multipart/form-data encoded body
               # Make sure to use -UnquotedBoundary for PAN-OS XML API limitation/workaround. See New-MultipartFormData
               $MPFData = New-MultipartFormData -File $File -UnquotedBoundary
               # Add ContentType header to InvokeParams
               $InvokeParams.Add('ContentType', $MPFData.Header.ContentType)
               $InvokeParams.Add('Body', $MPFData.Body)

               # If Certificate passphrase is specified, include it in Uri
               if($PSBoundParameters.ContainsKey('CertPassphrase')){
                  $InvokeParams.Uri += '&passphrase={0}' -f $CertPassphrase
               }
               # If Certificate vsys is specified, include it in Uri. If omitted, API defaults to shared
               if($PSBoundParameters.ContainsKey('CertVsys')){
                  $InvokeParams.Uri += '&vsys={0}' -f $CertVsys
               }
            } # end ParameterSetName Import-Cert-Key
         } # End API type=import

         if($PSCmdlet.ShouldProcess($DeviceCur.Name,'PAN-OS XML-API call')) {
            # Invoke-WebRequest is preferred over Invoke-RestMethod. In PowerShell 5.1, Invoke-RestMethod does not make HTTP response
            # *headers* available. Remedied in PowerShell 6+ with -ResponseHeadersVariable, but PowerShell 5.1 compatibility is needed for now
            # PowerShell 7+ x.509 Validation Policy can be set directly on Invoke-WebRequest
            if($PSVersionTable.PSVersion.Major -ge 7) {
               if ($DeviceCur.ValidateCertificate) {
                  $PanResponse = New-PanResponse -WebResponse (Invoke-WebRequest @InvokeParams -UseBasicParsing) -Device $DeviceCur
               }
               else {
                  # Note the addition of -SkipCertificateCheck, supported in PowerShell 6+
                  $PanResponse = New-PanResponse -WebResponse (Invoke-WebRequest @InvokeParams -UseBasicParsing -SkipCertificateCheck) -Device $DeviceCur
               }
            }
            # PowerShell 5 x.509 Validation Policy set using specific helper cmdlet
            else {
               if ($DeviceCur.ValidateCertificate) {
                  Set-X509CertificateValidation -Validate
               }
               else {
                  Set-X509CertificateValidation -NoValidate
               }
               $PanResponse = New-PanResponse -WebResponse (Invoke-WebRequest @InvokeParams -UseBasicParsing) -Device $DeviceCur
            }
            Write-Debug ($MyInvocation.MyCommand.Name + ': PanResponse Status ' + $PanResponse.Status + ', Code ' + $PanResponse.Code)
            return $PanResponse
         }
      } # Process block outermost foreach
   } # Process block

   End {
   } # End block
} # Function