PSCloudFlare.psm1

## Pre-Loaded Module code ##

# Used to determine if we are connected to anything
$Script:Headers = $null

# Used to always have a recent set of parameters used to invoke a REST method
$Script:RESTParams = @{
    'Uri' = $null
    'Headers' = $null
    'Body' = @{}
    'Method' = 'Get'
}

# Used for keeping track of the current zone
$Script:ZoneID = $null
$Script:ZoneName = $null

# Just in case the URI changes in the future
$Script:APIURI = 'https://api.cloudflare.com/client/v4'

# Some enumerations that will allow us to do validateset-like operations and include $null values
Enum CFFirewallTarget {
    ip
    ip_range
    country
    asn
}

Enum CFFirewallMode {
    whitelist
    block
    challenge
    js_challenge
}

Enum CFWAFRuleGroupMode {
    on
    off
}

Enum CFWAFRuleMode {
    on
    off
    default
    disable
    simulate
    block
    challenge
}

Enum CFPageRuleStatus {
    active
    disabled
}

Enum CFDNSRecordType {
    A
    AAAA
    CNAME
    TXT
    SRV
    LOC
    MX
    NS
    SPF
}

## PRIVATE MODULE FUNCTIONS AND DATA ##

function Get-CallerPreference {
    <#
    .Synopsis
       Fetches "Preference" variable values from the caller's scope.
    .DESCRIPTION
       Script module functions do not automatically inherit their caller's variables, but they can be
       obtained through the $PSCmdlet variable in Advanced Functions. This function is a helper function
       for any script module Advanced Function; by passing in the values of $ExecutionContext.SessionState
       and $PSCmdlet, Get-CallerPreference will set the caller's preference variables locally.
    .PARAMETER Cmdlet
       The $PSCmdlet object from a script module Advanced Function.
    .PARAMETER SessionState
       The $ExecutionContext.SessionState object from a script module Advanced Function. This is how the
       Get-CallerPreference function sets variables in its callers' scope, even if that caller is in a different
       script module.
    .PARAMETER Name
       Optional array of parameter names to retrieve from the caller's scope. Default is to retrieve all
       Preference variables as defined in the about_Preference_Variables help file (as of PowerShell 4.0)
       This parameter may also specify names of variables that are not in the about_Preference_Variables
       help file, and the function will retrieve and set those as well.
    .EXAMPLE
       Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
 
       Imports the default PowerShell preference variables from the caller into the local scope.
    .EXAMPLE
       Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState -Name 'ErrorActionPreference','SomeOtherVariable'
 
       Imports only the ErrorActionPreference and SomeOtherVariable variables into the local scope.
    .EXAMPLE
       'ErrorActionPreference','SomeOtherVariable' | Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
 
       Same as Example 2, but sends variable names to the Name parameter via pipeline input.
    .INPUTS
       String
    .OUTPUTS
       None. This function does not produce pipeline output.
    .LINK
       about_Preference_Variables
    #>


    [CmdletBinding(DefaultParameterSetName = 'AllVariables')]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateScript({ $_.GetType().FullName -eq 'System.Management.Automation.PSScriptCmdlet' })]
        $Cmdlet,

        [Parameter(Mandatory = $true)]
        [System.Management.Automation.SessionState]$SessionState,

        [Parameter(ParameterSetName = 'Filtered', ValueFromPipeline = $true)]
        [string[]]$Name
    )

    begin {
        $filterHash = @{}
    }
    
    process {
        if ($null -ne $Name)
        {
            foreach ($string in $Name)
            {
                $filterHash[$string] = $true
            }
        }
    }

    end {
        # List of preference variables taken from the about_Preference_Variables help file in PowerShell version 4.0

        $vars = @{
            'ErrorView' = $null
            'FormatEnumerationLimit' = $null
            'LogCommandHealthEvent' = $null
            'LogCommandLifecycleEvent' = $null
            'LogEngineHealthEvent' = $null
            'LogEngineLifecycleEvent' = $null
            'LogProviderHealthEvent' = $null
            'LogProviderLifecycleEvent' = $null
            'MaximumAliasCount' = $null
            'MaximumDriveCount' = $null
            'MaximumErrorCount' = $null
            'MaximumFunctionCount' = $null
            'MaximumHistoryCount' = $null
            'MaximumVariableCount' = $null
            'OFS' = $null
            'OutputEncoding' = $null
            'ProgressPreference' = $null
            'PSDefaultParameterValues' = $null
            'PSEmailServer' = $null
            'PSModuleAutoLoadingPreference' = $null
            'PSSessionApplicationName' = $null
            'PSSessionConfigurationName' = $null
            'PSSessionOption' = $null

            'ErrorActionPreference' = 'ErrorAction'
            'DebugPreference' = 'Debug'
            'ConfirmPreference' = 'Confirm'
            'WhatIfPreference' = 'WhatIf'
            'VerbosePreference' = 'Verbose'
            'WarningPreference' = 'WarningAction'
        }

        foreach ($entry in $vars.GetEnumerator()) {
            if (([string]::IsNullOrEmpty($entry.Value) -or -not $Cmdlet.MyInvocation.BoundParameters.ContainsKey($entry.Value)) -and
                ($PSCmdlet.ParameterSetName -eq 'AllVariables' -or $filterHash.ContainsKey($entry.Name))) {
                
                $variable = $Cmdlet.SessionState.PSVariable.Get($entry.Key)
                
                if ($null -ne $variable) {
                    if ($SessionState -eq $ExecutionContext.SessionState) {
                        Set-Variable -Scope 1 -Name $variable.Name -Value $variable.Value -Force -Confirm:$false -WhatIf:$false
                    }
                    else {
                        $SessionState.PSVariable.Set($variable.Name, $variable.Value)
                    }
                }
            }
        }

        if ($PSCmdlet.ParameterSetName -eq 'Filtered') {
            foreach ($varName in $filterHash.Keys) {
                if (-not $vars.ContainsKey($varName)) {
                    $variable = $Cmdlet.SessionState.PSVariable.Get($varName)
                
                    if ($null -ne $variable)
                    {
                        if ($SessionState -eq $ExecutionContext.SessionState)
                        {
                            Set-Variable -Scope 1 -Name $variable.Name -Value $variable.Value -Force -Confirm:$false -WhatIf:$false
                        }
                        else
                        {
                            $SessionState.PSVariable.Set($variable.Name, $variable.Value)
                        }
                    }
                }
            }
        }
    }
}

Function IsCFID ([string]$ID) {
    if (-not ([string]::IsNullOrEmpty($ID))) {
        return ($ID -match '^[a-f0-9]{32}$')
    }
    else {
        return $false
    }
}

Function IsCountryCode ([string]$Code) {
    return ($Code  -match '^(AF|AX|AL|DZ|AS|AD|AO|AI|AQ|AG|AR|AM|AW|AU|AT|AZ|BS|BH|BD|BB|BY|BE|BZ|BJ|BM|BT|BO|BQ|BA|BW|BV|BR|IO|BN|BG|BF|BI|KH|CM|CA|CV|KY|CF|TD|CL|CN|CX|CC|CO|KM|CG|CD|CK|CR|CI|HR|CU|CW|CY|CZ|DK|DJ|DM|DO|EC|EG|SV|GQ|ER|EE|ET|FK|FO|FJ|FI|FR|GF|PF|TF|GA|GM|GE|DE|GH|GI|GR|GL|GD|GP|GU|GT|GG|GN|GW|GY|HT|HM|VA|HN|HK|HU|IS|IN|ID|IR|IQ|IE|IM|IL|IT|JM|JP|JE|JO|KZ|KE|KI|KP|KR|KW|KG|LA|LV|LB|LS|LR|LY|LI|LT|LU|MO|MK|MG|MW|MY|MV|ML|MT|MH|MQ|MR|MU|YT|MX|FM|MD|MC|MN|ME|MS|MA|MZ|MM|NA|NR|NP|NL|NC|NZ|NI|NE|NG|NU|NF|MP|NO|OM|PK|PW|PS|PA|PG|PY|PE|PH|PN|PL|PT|PR|QA|RE|RO|RU|RW|BL|SH|KN|LC|MF|PM|VC|WS|SM|ST|SA|SN|RS|SC|SL|SG|SX|SK|SI|SB|SO|ZA|GS|SS|ES|LK|SD|SR|SJ|SZ|SE|CH|SY|TW|TJ|TZ|TH|TL|TG|TK|TO|TT|TN|TR|TM|TC|TV|UG|UA|AE|GB|US|UM|UY|UZ|VU|VE|VN|VG|VI|WF|EH|YE|ZM|ZW)$')
}

Function IsIPRange ([string]$Range) {
    return ($Range  -match '^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$')
}

Function IsNullOrCFID ([string]$ID) {
    if (-not ([string]::IsNullOrEmpty($ID))) {
        return ($ID -match '^[a-f0-9]{32}$')
    }
    else {
        return $true
    }
}

## PUBLIC MODULE FUNCTIONS AND DATA ##

Function Add-CFFirewallRule {
<#
    .EXTERNALHELP PSCloudFlare-help.xml
    .LINK
        https://github.com/zloeber/PSCloudFlare/tree/master/release/0.0.5/docs/Functions/Add-CFFirewallRule.md
    #>


    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $True)]
        [ValidateNotNullOrEmpty()]
        [String]$Item,

        [Parameter()]
        [ValidateScript({
            IsNullOrCFID $_
        })]
        [String]$ZoneID,

        [Parameter()]
        [ValidateSet('ip','ip_range','country','asn')]
        [String]$Target = 'ip',

        [Parameter()]
        [ValidateSet('whitelist','block','challenge','js_challenge')]
        [String]$Mode = 'whitelist',

        [Parameter()]
        [String]$Notes = ''
    )
    
    switch ($Target) {
        'ip' { 
            if (-not ($Item -match [IPAddress]$Item)) { 
                throw 'IP address is not valid'
            }
        }
        'ip_range' {
            if ($Item -notmatch '^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$') {
                throw "Subnet address '$Item' does not match the expected CIDR format (example: 192.168.0.0/24)"
            }
        }
        'country' {
            if ($Item -notmatch '^(AF|AX|AL|DZ|AS|AD|AO|AI|AQ|AG|AR|AM|AW|AU|AT|AZ|BS|BH|BD|BB|BY|BE|BZ|BJ|BM|BT|BO|BQ|BA|BW|BV|BR|IO|BN|BG|BF|BI|KH|CM|CA|CV|KY|CF|TD|CL|CN|CX|CC|CO|KM|CG|CD|CK|CR|CI|HR|CU|CW|CY|CZ|DK|DJ|DM|DO|EC|EG|SV|GQ|ER|EE|ET|FK|FO|FJ|FI|FR|GF|PF|TF|GA|GM|GE|DE|GH|GI|GR|GL|GD|GP|GU|GT|GG|GN|GW|GY|HT|HM|VA|HN|HK|HU|IS|IN|ID|IR|IQ|IE|IM|IL|IT|JM|JP|JE|JO|KZ|KE|KI|KP|KR|KW|KG|LA|LV|LB|LS|LR|LY|LI|LT|LU|MO|MK|MG|MW|MY|MV|ML|MT|MH|MQ|MR|MU|YT|MX|FM|MD|MC|MN|ME|MS|MA|MZ|MM|NA|NR|NP|NL|NC|NZ|NI|NE|NG|NU|NF|MP|NO|OM|PK|PW|PS|PA|PG|PY|PE|PH|PN|PL|PT|PR|QA|RE|RO|RU|RW|BL|SH|KN|LC|MF|PM|VC|WS|SM|ST|SA|SN|RS|SC|SL|SG|SX|SK|SI|SB|SO|ZA|GS|SS|ES|LK|SD|SR|SJ|SZ|SE|CH|SY|TW|TJ|TZ|TH|TL|TG|TK|TO|TT|TN|TR|TM|TC|TV|UG|UA|AE|GB|US|UM|UY|UZ|VU|VE|VN|VG|VI|WF|EH|YE|ZM|ZW)$') {
                throw "Country code '$Item' does not match the expected country code format (example: US)"
            }
        }
        'asn' {
            if ($Item -notmatch '^(AS\d+)$') {
                throw "ASN '$Item' does not match the expected ASN format (example: AS1234)"
            }
        }
        default {
            throw 'How did we get here?'
        }
    }

    # If the ZoneID is empty see if we have loaded one earlier in the module and use it instead.
    if ([string]::IsNullOrEmpty($ZoneID) -and ($Script:ZoneID -ne $null)) {
        Write-Verbose "Add-CFFirewallRule: No ZoneID was passed but the current targeted zone was $($Script:ZoneName) so this will be used."
        $ZoneID = $Script:ZoneID
    }

    # If we specified a zone earlier, create the access rules there, else, do it at a user level
    if (-not ([string]::IsNullOrEmpty($ZoneID))) {
        Write-Verbose "Add-CFFirewallRule: Targeting only $($Script:ZoneName) for this firewall rule."
        $Data = @{
            'mode' = $Mode
            'notes' = $Notes
            'configuration' = @{
                'value' = $Item
                'target' = $Target
            }
            'group' = @{
                'id' = 'zone'
            }
        }
        $Uri = $Script:APIURI + ('/zones/{0}/firewall/access_rules/rules' -f $ZoneID)
    }
    else {
        Write-Verbose "Add-CFFirewallRule: Targeting entire organization for this firewall rule."
        $Data = @{
            'mode' = $Mode
            'notes' = $Notes
            'configuration' = @{
                'value' = $Item
                'target' = $Target
            }
            'group' = @{
                'id' = 'owner'
            }
        }
        $Uri = $Script:APIURI + ('/user/firewall/access_rules/rules')
    }
    
    Write-Verbose "Add-CFFirewallRule: URI = '$Uri'"

    try {
        Write-Verbose -Message "Add-CFFirewallRule: Adding '$Item' as '$Target' in '$Mode' mode..."

        Set-CFRequestData -Uri $Uri -Body $Data -Method 'Post'
        $Response = Invoke-CFAPI4Request -ErrorAction Stop
    }
    catch {
        Throw $_
    }
}


Function Connect-CFClientAPI {
<#
    .EXTERNALHELP PSCloudFlare-help.xml
    .LINK
        https://github.com/zloeber/PSCloudFlare/tree/master/release/0.0.5/docs/Functions/Connect-CFClientAPI.md
    #>


    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $True)]
        [ValidateNotNullOrEmpty()]
        [String]$APIToken,

        [Parameter(Mandatory = $True)]
        [ValidateNotNullOrEmpty()]
        [String]$EmailAddress
    )

    # Headers for CloudFlare APIv4
    $Headers = @{
        'X-Auth-Key'   = $APIToken
        'X-Auth-Email' = $EmailAddress
    }

    $Uri = $Script:APIURI + '/user'

    Set-CFRequestData -Uri $Uri -Headers $Headers

    try {
        Write-Verbose  'Connect-CFClientAPI: Attempting to connect'
        $null = Invoke-CFAPI4Request -ErrorAction Stop
        Write-Verbose 'Connect-CFClientAPI: Connected successfully'

        # Make the headers we used available accross the entire script scope
        $Script:Headers = $Headers
    }
    catch {
        Throw $_
    }
}


Function Get-CFCurrentZone {
<#
    .EXTERNALHELP PSCloudFlare-help.xml
    .LINK
        https://github.com/zloeber/PSCloudFlare/tree/master/release/0.0.5/docs/Functions/Get-CFCurrentZone.md
    #>

    return $Script:Zone
}


Function Get-CFCurrentZoneID {
<#
    .EXTERNALHELP PSCloudFlare-help.xml
    .LINK
        https://github.com/zloeber/PSCloudFlare/tree/master/release/0.0.5/docs/Functions/Get-CFCurrentZoneID.md
    #>

    return $Script:ZoneID
}


Function Get-CFDNSRecord {
    <#
    .EXTERNALHELP PSCloudFlare-help.xml
    .LINK
        https://github.com/zloeber/PSCloudFlare/tree/master/release/0.0.5/docs/Functions/Get-CFDNSRecord.md
    #>


    [CmdletBinding()]
    Param (
        [Parameter()]
        [ValidateScript({
            IsNullOrCFID $_
        })]
        [String]$ZoneID,

        [Parameter()]
        [CFDNSRecordType]$RecordType,

        [Parameter()]
        [string]$Name,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [int]$PerPage = 50,

        [Parameter()]
        [ValidateSet('type', 'name', 'content', 'ttl', 'proxied')]
        [string]$Order = 'name',

        [Parameter()]
        [ValidateSet( 'asc', 'dec' )]
        [string]$Direction = 'asc',

        [Parameter()]
        [ValidateSet( 'any', 'all' )]
        [string]$MatchScope = 'all'
    )

    $FunctionName = $MyInvocation.MyCommand.Name

    # If the ZoneID is empty see if we have loaded one earlier in the module and use it instead.
    if ([string]::IsNullOrEmpty($ZoneID) -and ($null -ne $Script:ZoneID)) {
        Write-Verbose "$($FunctionName): No ZoneID was passed but the current targeted zone was $($Script:ZoneName) so this will be used."
        $ZoneID = $Script:ZoneID
    }
    elseif ([string]::IsNullOrEmpty($ZoneID)) {
        throw 'No Zone was set or passed!'
    }

    # If no record type was passed then try to get them all
    if ($null -eq [CFDNSRecordType]::$RecordType) {
        $PassedParams = $PsCmdlet.MyInvocation.BoundParameters
        [enum]::getnames([CFDNSRecordType]) | ForEach {
            Get-CFDNSRecord @PassedParams -RecordType $_
        }
    }
    else {
        $Data = @{
            'direction' = $Direction
            'match' = $MatchScope
            'order' = $Order
            'per_page' = $PerPage
        }
        $Data.type = $RecordType.ToString()
        # Always start at the first page
        $Data.page = 1

        if ($null -ne $Name) {
            $Data.name = $Name
        }

        # Construct the URI for this package
        $Uri = $Script:APIURI + ('/zones/{0}/dns_records' -f $ZoneID)

        # Get the first page, from there we will be able to see the total page numbers
        try {
            Write-Verbose "$($FunctionName): Returning the first result"
            Set-CFRequestData -Uri $Uri -Body $Data
            $LatestPage = Invoke-CFAPI4Request -ErrorAction Stop
            $LatestPage.result
            $TotalPages = $LatestPage.result_info.total_pages
        }
        catch {
            throw $_
        }

        $PageNumber = 2

        # Get any more pages
        while ($PageNumber -le $TotalPages) {
            try {
                Write-Verbose "$($FunctionName): Returning $PageNumber of $TotalPages"
                $Data.page = $PageNumber
                Set-CFRequestData -Uri $Uri -Body $Data
                (Invoke-CFAPI4Request -ErrorAction Stop).result
                $PageNumber++
            }
            catch {
                throw $_
                break
            }
        }
    }

}


Function Get-CFFirewallRule {
<#
    .EXTERNALHELP PSCloudFlare-help.xml
    .LINK
        https://github.com/zloeber/PSCloudFlare/tree/master/release/0.0.5/docs/Functions/Get-CFFirewallRule.md
    #>


    [CmdletBinding()]
    Param (
        [Parameter()]
        [ValidateScript({
            IsNullOrCFID $_
        })]
        [String]$ZoneID,

        [Parameter()]
        [String]$MatchItem = $null,

        [Parameter()]
        [CFFirewallTarget]$MatchTarget,

        [Parameter()]
        [CFFirewallMode]$MatchMode,

        [Parameter()]
        [ValidateSet( 'configuration_target', 'configuration_value', 'mode' )]
        [string]$OrderBy = 'configuration_value',

        [Parameter()]
        [ValidateSet( 'asc', 'dec' )]
        [string]$Direction = 'asc',

        [Parameter()]
        [ValidateSet( 'any', 'all' )]
        [string]$MatchScope = 'all',

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [int]$PageLimit = 50
    )

    $FunctionName = $MyInvocation.MyCommand.Name

    # If the ZoneID is empty see if we have loaded one earlier in the module and use it instead.
    if ([string]::IsNullOrEmpty($ZoneID) -and ($Script:ZoneID -ne $null)) {
        Write-Verbose "$($FunctionName): No ZoneID was passed but the current targeted zone was $($Script:ZoneName) so this will be used."
        $ZoneID = $Script:ZoneID
    }

     # If we specified a zone earlier, then we call the zone page
    if ([string]::IsNullOrEmpty($ZoneID)) {
        $Uri = $Script:APIURI + ('/user/firewall/access_rules/rules/')
    }
    else {
        $Uri = $Script:APIURI + ('/zones/{0}/firewall/access_rules/rules' -f $ZoneID)
    }


    $Data = @{
        'direction' = $Direction
        'match' = $MatchScope
        'order' = $Orderby
        'per_page' = $PageLimit
        'page' = 1
    }

    if ($null -ne [CFFirewallMode]::$MatchMode) {
        $Data.mode = $MatchMode.ToString()
    }

    if ( (-not [string]::IsNullOrEmpty($MatchItem)) -and (-not [string]::IsNullOrEmpty([CFFirewallTarget]::$MatchTarget)))  {
        Write-Verbose "$($FunctionName): A MatchTarget and MatchItem were passed ($($MatchItem) - $([CFFirewallTarget]::$MatchTarget)), adding this to the data request."
        $Data.configuration_value = $MatchItem
        $Data.configuration_target = $MatchTarget.ToString()
    }

    # Get the first page, from there we will be able to see the total page numbers
    try {
        Write-Verbose "$($FunctionName): Returning the first result"
        Set-CFRequestData -Uri $Uri -Body $Data
        $LatestPage = Invoke-CFAPI4Request -ErrorAction Stop
        $LatestPage.result
        $TotalPages = $LatestPage.result_info.total_pages
    }
    catch {
        throw $_
    }

    $PageNumber = 2

    # Get any more pages
    while ($PageNumber -le $TotalPages) {
        try {
             Write-Verbose "$($FunctionName): Returning $PageNumber of $TotalPages"
            $Data.page = $PageNumber
            Set-CFRequestData -Uri $Uri -Body $Data
            (Invoke-CFAPI4Request -ErrorAction Stop).result
            $PageNumber++
        }
        catch {
            throw $_
            break
        }
    }
}


Function Get-CFIP {
<#
    .EXTERNALHELP PSCloudFlare-help.xml
    .LINK
        https://github.com/zloeber/PSCloudFlare/tree/master/release/0.0.5/docs/Functions/Get-CFIP.md
    #>


    [CmdletBinding()]
    Param ()

    $Uri = $Script:APIURI + '/ips'

    Set-CFRequestData -Uri $Uri

    try {
        Write-Verbose "$($FunctionName): Returning Cloudflare IPs"
        (Invoke-CFAPI4Request -ErrorAction Stop).result
    }
    catch {
        Throw $_
    }
}


Function Get-CFPageRule {
<#
    .EXTERNALHELP PSCloudFlare-help.xml
    .LINK
        https://github.com/zloeber/PSCloudFlare/tree/master/release/0.0.5/docs/Functions/Get-CFPageRule.md
    #>


    [CmdletBinding()]
    Param (
        [Parameter()]
        [ValidateScript({
            IsNullOrCFID $_
        })]
        [String]$ZoneID,

        [Parameter()]
        [CFPageRuleStatus]$Status,

        [Parameter()]
        [ValidateSet( 'priority', 'status')]
        [string]$OrderBy = 'priority',

        [Parameter()]
        [ValidateSet( 'asc', 'dec' )]
        [string]$Direction = 'asc',

        [Parameter()]
        [ValidateSet( 'any', 'all' )]
        [string]$MatchScope = 'all',

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [int]$PageLimit = 50
    )  

    $FunctionName = $MyInvocation.MyCommand.Name

    # If the ZoneID is empty see if we have loaded one earlier in the module and use it instead.
    if ([string]::IsNullOrEmpty($ZoneID) -and ($Script:ZoneID -ne $null)) {
        Write-Verbose "$($FunctionName): No ZoneID was passed but the current targeted zone was $($Script:ZoneName) so this will be used."
        $ZoneID = $Script:ZoneID
    }
    elseif ([string]::IsNullOrEmpty($ZoneID)) {
        throw 'No Zone was set or passed!'
    }

    $Data = @{
        'direction' = $Direction
        'match' = $MatchScope
        'order' = $Orderby
        'per_page' = $PageLimit
    }

    if ([CFPageRuleStatus]::$Status -ne $null) {
        $Data.status = [CFPageRuleStatus]::$Status
    }

    # Construct the URI for this package
    $Uri = $Script:APIURI + ('/zones/{0}/pagerules' -f $ZoneID)

    # Always start at the first page
    $Data.page = 1

    # Get the first page, from there we will be able to see the total page numbers
    try {
        Write-Verbose "$($FunctionName): Returning the first result"
        Set-CFRequestData -Uri $Uri -Body $Data
        $LatestPage = Invoke-CFAPI4Request -ErrorAction Stop
        $LatestPage.result
        $TotalPages = $LatestPage.result_info.total_pages
    }
    catch {
        throw $_
    }

    $PageNumber = 2
    
    # Get any more pages
    while ($PageNumber -le $TotalPages) {
        try {
            Write-Verbose "$($FunctionName): Returning $PageNumber of $TotalPages"
            $Data.page = $PageNumber
            Set-CFRequestData -Uri $Uri -Body $Data
            (Invoke-CFAPI4Request -ErrorAction Stop).result
            $PageNumber++
        }
        catch {
            throw $_
            break
        }
    }

}


Function Get-CFRequestData {
<#
    .EXTERNALHELP PSCloudFlare-help.xml
    .LINK
        https://github.com/zloeber/PSCloudFlare/tree/master/release/0.0.5/docs/Functions/Get-CFRequestData.md
    #>

    return $Script:RESTParams
}


Function Get-CFTrafficLog {
<#
    .EXTERNALHELP PSCloudFlare-help.xml
    .LINK
        https://github.com/zloeber/PSCloudFlare/tree/master/release/0.0.5/docs/Functions/Get-CFTrafficLog.md
    #>


    [CmdletBinding()]
    Param (
        [Parameter()]
        [ValidateScript({
            IsNullOrCFID $_
        })]
        [String]$ZoneID,

        [Parameter()]
        [ValidateSet( 'id', 'action', 'host', 'ip', 'country', 'occurred_at')]
        [string]$OrderBy = 'occurred_at',

        [Parameter()]
        [ValidateSet( 'asc', 'dec' )]
        [string]$Direction = 'asc',

        [Parameter()]
        [ValidateRange(1,500)]
        [int]$PageLimit = 50,

        [Parameter()]
        [int]$ResultLimit = 0
    )  
    Begin {
        $FunctionName = $MyInvocation.MyCommand.Name

        # If the ZoneID is empty see if we have loaded one earlier in the module and use it instead.
        if ([string]::IsNullOrEmpty($ZoneID) -and ($Script:ZoneID -ne $null)) {
            Write-Verbose "$($FunctionName): No ZoneID was passed but the current targeted zone was $($Script:ZoneName) so this will be used."
            $ZoneID = $Script:ZoneID
        }

        # If we specified a zone earlier, then we call the zone page
        if ([string]::IsNullOrEmpty($ZoneID)) {
            $Uri = $Script:APIURI + ('/user/firewall/events/')
        }
        else {
            $Uri = $Script:APIURI + ('/zones/{0}/firewall/events' -f $ZoneID)
        }
        $Data = @{
            'direction' = $Direction
            'order' = $Orderby
            'per_page' = $PageLimit
        }
    }

    Process {
        $ResultCount = 0

        Do {
            $ResultCount++
            Write-Verbose "$($FunctionName): Returning result #$($ResultCount)"
            if ($LatestPage.result_info.next_page_id) {
                $Data.page_id = $LatestPage.result_info.next_page_id
            }

            Set-CFRequestData -Uri $Uri -Body $Data

            try {
                $LatestPage = Invoke-CFAPI4Request -ErrorAction Stop

                # Something about the results in this api query requires us to return it as an array
                @($LatestPage.result)
            }
            catch {
                throw $_
                break
            }

            Write-Verbose "$($FunctionName): Next page result ID = $($LatestPage.result_info.next_page_id)"
        } Until ([string]::IsNullOrEmpty($LatestPage.result_info.next_page_id) -or (($ResultCount -ge $ResultLimit) -and ($ResultLimit -ne 0) )  )
    }
}


Function Get-CFWAFRule {
<#
    .EXTERNALHELP PSCloudFlare-help.xml
    .LINK
        https://github.com/zloeber/PSCloudFlare/tree/master/release/0.0.5/docs/Functions/Get-CFWAFRule.md
    #>


    [CmdletBinding()]
    Param (
        [Parameter()]
        [ValidateScript({
            IsNullOrCFID $_
        })]
        [String]$ZoneID,

        [Parameter()]
        [ValidateScript({
            IsNullOrCFID $_
        })]
        [String]$PackageID,

        [Parameter()]
        [ValidateScript({
            IsNullOrCFID $_
        })]
        [String]$GroupID,
        
        [Parameter()]
        [String]$Description,

        [Parameter()]
        [CFWAFRuleMode]$Mode,

        [Parameter()]
        [int]$Priority = $null,

        [Parameter()]
        [ValidateSet( 'priority', 'group_id','description','status' )]
        [string]$OrderBy = 'group_id',

        [Parameter()]
        [ValidateSet( 'asc', 'dec' )]
        [string]$Direction = 'asc',

        [Parameter()]
        [ValidateSet( 'any', 'all' )]
        [string]$MatchScope = 'all',

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [int]$PageLimit = 50
    )  

    $FunctionName = $MyInvocation.MyCommand.Name

    # If the ZoneID is empty see if we have loaded one earlier in the module and use it instead.
    if ([string]::IsNullOrEmpty($ZoneID) -and ($Script:ZoneID -ne $null)) {
        Write-Verbose "$($FunctionName): No ZoneID was passed but the current targeted zone was $($Script:ZoneName) so this will be used."
        $ZoneID = $Script:ZoneID
    }
    elseif ([string]::IsNullOrEmpty($ZoneID)) {
        throw 'No Zone was set or passed!'
    }

    if ([string]::IsNullOrEmpty($PackageID)) {
        Write-Verbose "$($FunctionName): No Package ID passed, pulling all package IDs"
        $PackageIDs = Get-CFWAFRulePackage -ZoneID $ZoneID | Foreach-Object {$_.id}
    }
    else {
        $PackageIDs = @($PackageID)
    }

    $Data = @{
        'direction' = $Direction
        'match' = $MatchScope
        'order' = $Orderby
        'per_page' = $PageLimit
    }
    if (-not ([string]::IsNullOrEmpty($Description))) {
        Write-Verbose "$($FunctionName): Description passed: $Description"
        $Data.description = $Description
    }
    if ([CFWAFRuleMode]::$Mode -ne $null) {
        $Data.mode = [CFWAFRuleMode]::$Mode
    }

    if ($Priority) {
        Write-Verbose "$($FunctionName): Priority passed: $Priority"
        $Data.priority = $Priority
    }
     if (-not ([string]::IsNullOrEmpty($GroupID))) {
        Write-Verbose "$($FunctionName): GroupID passed: $GroupID"
        $Data.group_id = $GroupID
    }

    Foreach ($Package in $PackageIDs) {
        Write-Verbose "$($FunctionName): Listing WAF groups within package ID: $Package"

        # Construct the URI for this package
        $Uri = $Script:APIURI + ('/zones/{0}/firewall/waf/packages/{1}/rules' -f $ZoneID, $Package)

        # Always start at the first page
        $Data.page = 1

        # Get the first page, from there we will be able to see the total page numbers
        try {
            Write-Verbose "$($FunctionName): Returning the first result"
            Set-CFRequestData -Uri $Uri -Body $Data
            $LatestPage = Invoke-CFAPI4Request -ErrorAction Stop
            $LatestPage.result
            $TotalPages = $LatestPage.result_info.total_pages
        }
        catch {
            throw $_
        }

        $PageNumber = 2
        
        # Get any more pages
        while ($PageNumber -le $TotalPages) {
            try {
                Write-Verbose "$($FunctionName): Returning $PageNumber of $TotalPages"
                $Data.page = $PageNumber
                Set-CFRequestData -Uri $Uri -Body $Data
                (Invoke-CFAPI4Request -ErrorAction Stop).result
                $PageNumber++
            }
            catch {
                throw $_
                break
            }
        }
    }
}


Function Get-CFWAFRuleGroup {
<#
    .EXTERNALHELP PSCloudFlare-help.xml
    .LINK
        https://github.com/zloeber/PSCloudFlare/tree/master/release/0.0.5/docs/Functions/Get-CFWAFRuleGroup.md
    #>


    [CmdletBinding()]
    Param (
        [Parameter()]
        [ValidateScript({
            IsNullOrCFID $_
        })]
        [String]$ZoneID,

        [Parameter()]
        [ValidateScript({
            IsNullOrCFID $_
        })]
        [String]$PackageID,
        
        [Parameter()]
        [String]$Name,

        [Parameter()]
        [CFWAFRuleGroupMode]$Mode,

        [Parameter()]
        [ValidateSet( 'mode', 'rules_count' )]
        [string]$OrderBy = 'mode',

        [Parameter()]
        [ValidateSet( 'asc', 'dec' )]
        [string]$Direction = 'asc',

        [Parameter()]
        [ValidateSet( 'any', 'all' )]
        [string]$MatchScope = 'all',

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [int]$PageLimit = 50
    )  

    $FunctionName = $MyInvocation.MyCommand.Name

    # If the ZoneID is empty see if we have loaded one earlier in the module and use it instead.
    if ([string]::IsNullOrEmpty($ZoneID) -and ($Script:ZoneID -ne $null)) {
        Write-Verbose "$($FunctionName): No ZoneID was passed but the current targeted zone was $($Script:ZoneName) so this will be used."
        $ZoneID = $Script:ZoneID
    }
    elseif ([string]::IsNullOrEmpty($ZoneID)) {
        throw 'No Zone was set or passed!'
    }

    if ([string]::IsNullOrEmpty($PackageID)) {
        Write-Verbose "$($FunctionName): No Package ID passed, pulling all package IDs"
        $PackageIDs = Get-CFWAFRulePackage -ZoneID $ZoneID | Foreach {$_.id}
    }
    else {
        $PackageIDs = @($PackageID)
    }

    $Data = @{
        'direction' = $Direction
        'match' = $MatchScope
        'order' = $Orderby
        'per_page' = $PageLimit
    }
    if (-not ([string]::IsNullOrEmpty($Name))) {
        Write-Verbose "$($FunctionName): Name passed: $Name"
        $Data.name = $Name
    }
    if ([CFWAFRuleGroupMode]::$Mode -ne $null) {
        $Data.mode = [CFWAFRuleGroupMode]::$Mode
    }

    Foreach ($Package in $PackageIDs) {
        Write-Verbose "$($FunctionName): Listing WAF groups within package ID: $Package."

        # Construct the URI for this package
        $Uri = $Script:APIURI + ('/zones/{0}/firewall/waf/packages/{1}/groups' -f $ZoneID, $Package)

        # Always start at the first page
        $Data.page = 1

        # Get the first page, from there we will be able to see the total page numbers
        try {
            Write-Verbose "$($FunctionName): Returning the first result"
            Set-CFRequestData -Uri $Uri -Body $Data
            $LatestPage = Invoke-CFAPI4Request -ErrorAction Stop
            $LatestPage.result
            $TotalPages = $LatestPage.result_info.total_pages
        }
        catch {
            throw $_
        }

        $PageNumber = 2
        
        # Get any more pages
        while ($PageNumber -le $TotalPages) {
            try {
                Write-Verbose "$($FunctionName): Returning $PageNumber of $TotalPages"
                $Data.page = $PageNumber
                Set-CFRequestData -Uri $Uri -Body $Data
                (Invoke-CFAPI4Request -ErrorAction Stop).result
                $PageNumber++
            }
            catch {
                throw $_
                break
            }
        }
    }
}


Function Get-CFWAFRulePackage {
<#
    .EXTERNALHELP PSCloudFlare-help.xml
    .LINK
        https://github.com/zloeber/PSCloudFlare/tree/master/release/0.0.5/docs/Functions/Get-CFWAFRulePackage.md
    #>


    [CmdletBinding()]
    Param (
        [Parameter()]
        [ValidateScript({
            IsNullOrCFID $_
        })]
        [String]$ZoneID,
        
        [Parameter()]
        [String]$Name = $null,

        [Parameter()]
        [ValidateSet( 'name', 'status' )]
        [string]$OrderBy = 'name',

        [Parameter()]
        [ValidateSet( 'asc', 'dec' )]
        [string]$Direction = 'asc',

        [Parameter()]
        [ValidateSet( 'any', 'all' )]
        [string]$MatchScope = 'all',

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [int]$PageLimit = 50
    )  

    $FunctionName = $MyInvocation.MyCommand.Name

    # If the ZoneID is empty see if we have loaded one earlier in the module and use it instead.
    if ([string]::IsNullOrEmpty($ZoneID) -and ($Script:ZoneID -ne $null)) {
        Write-Verbose "$($FunctionName): No ZoneID was passed but the current targeted zone was $($Script:ZoneName) so this will be used."
        $ZoneID = $Script:ZoneID
    }
    elseif ([string]::IsNullOrEmpty($ZoneID)) {
        throw 'No Zone was set or passed!'
    }

    $Uri = $Script:APIURI + ('/zones/{0}/firewall/waf/packages' -f $ZoneID)

    $Data = @{
        'direction' = $Direction
        'match' = $MatchScope
        'order' = $Orderby
        'per_page' = $PageLimit
        'page' = 1
    }

    if ($Name -ne $null) {
        $Data.name = $Name
    }

    # Get the first page, from there we will be able to see the total page numbers
    try {
        Write-Verbose "$($FunctionName): Returning the first result"
        Set-CFRequestData -Uri $Uri -Body $Data
        $LatestPage = Invoke-CFAPI4Request -ErrorAction Stop
        $LatestPage.result
        $TotalPages = $LatestPage.result_info.total_pages
    }
    catch {
        throw $_
    }

    $PageNumber = 2
    
    # Get any more pages
    while ($PageNumber -le $TotalPages) {
        try {
             Write-Verbose "$($FunctionName): Returning $PageNumber of $TotalPages"
            $Data.page = $PageNumber
            Set-CFRequestData -Uri $Uri -Body $Data
            (Invoke-CFAPI4Request -ErrorAction Stop).result
            $PageNumber++
        }
        catch {
            throw $_
            break
        }
    }
}


Function Get-CFZoneID {
<#
    .EXTERNALHELP PSCloudFlare-help.xml
    .LINK
        https://github.com/zloeber/PSCloudFlare/tree/master/release/0.0.5/docs/Functions/Get-CFZoneID.md
    #>


    [CmdletBinding()]
    [OutputType([String])]
    Param (
        [Parameter(Mandatory = $True)]
        [ValidateNotNullOrEmpty()]
        [String]$Zone
    )

    $Uri = $Script:APIURI + '/zones'

    Set-CFRequestData -Uri $Uri

    try {
        Write-Verbose -Message 'Getting Zone information'
        $Response = Invoke-CFAPI4Request #-Uri $Uri -Headers $Headers -ErrorAction Stop
    }
    catch {
        Throw $_
    }

    $ZoneData = $Response.Result | Where-Object -FilterScript {
        $_.name -eq $Zone
    }
    
    if ($null -ne $ZoneData) {
        $ZoneData.ID
    }
    else {
        throw 'Zone not found in CloudFlare'
    }
}


Function Invoke-CFAPI4Request {
<#
    .EXTERNALHELP PSCloudFlare-help.xml
    .LINK
        https://github.com/zloeber/PSCloudFlare/tree/master/release/0.0.5/docs/Functions/Invoke-CFAPI4Request.md
    #>


    [CmdletBinding()]
    Param (
        [Parameter()]
        [Uri]$Uri = ($Script:RESTParams).URI,
       
        [Parameter()]
        [HashTable]$Headers = ($Script:RESTParams).Headers,

        [Parameter()]
        [Hashtable]$Body =  ($Script:RESTParams).Body,

        [Parameter()]
        [String]$Method =  ($Script:RESTParams).Method
    )

    $FunctionName = $MyInvocation.MyCommand.Name

    if ($Uri -eq $null) {
        throw "$($FunctionName): Need to run Set-CFRequestData first!"
    }

    # Make null if nothing passed, if the method is 'get' then convert to json, anything else leave as it is.
    $BodyData = if ($Body -eq $null) {$null} else { if ($Method -ne 'Get') {$Body | ConvertTo-Json} else {$Body}}

    try {
        $JSONResponse = Invoke-RestMethod -Uri $Uri -Headers $Headers -ContentType 'application/json' -Method $Method -Body $BodyData -ErrorAction Stop
    }
    catch {
        Write-Debug -Message "$($FunctionName): Error Processing"
        $MyError = $_
        if ($null -ne $MyError.Exception.Response) {
            try {
                # Recieved an error from the API, lets get it out
                $result = $MyError.Exception.Response.GetResponseStream()
                $reader = New-Object -TypeName System.IO.StreamReader -ArgumentList ($result)
                $responseBody = $reader.ReadToEnd()
                $JSONResponse = $responseBody | ConvertFrom-Json
                
                $CloudFlareErrorCode = $JSONResponse.Errors[0].code
                $CloudFlareMessage = $JSONResponse.Errors[0].message

                # Some errors are just plain unfriendly, so I make them more understandable
                switch ($CloudFlareErrorCode) {
                    9103 {
                        throw '[CloudFlare Error 9103] Your Cloudflare API or email address appears to be incorrect.' 
                    }
                    81019  {
                        throw '[CloudFlare Error 81019] Cloudflare access rule quota has been exceeded: You may be trying to add more access rules than your account currently allows. Please check the --rule-limit option.' 
                    }
                    default {
                        throw '[CloudFlare Error {0}] {1}' -f $CloudFlareErrorCode, $CloudFlareMessage 
                    }
                }
            }
            catch {
                # An error has occured whilst processing the error, so we will just throw the original error
                Write-Verbose "$($FunctionName): Unrecognized error connecting with the API."
                Throw $MyError
            }
        }
        else {
            # This wasn't an error from the API, so we need to let the user know directly
            Write-Verbose "$($FunctionName): Non-API related error occurred"
            Throw $MyError
        }
    }
    
    $JSONResponse
}


Function Remove-CFFirewallRule {
<#
    .EXTERNALHELP PSCloudFlare-help.xml
    .LINK
        https://github.com/zloeber/PSCloudFlare/tree/master/release/0.0.5/docs/Functions/Remove-CFFirewallRule.md
    #>

    [CmdletBinding( SupportsShouldProcess = $true )]
    Param (
        [Parameter(Mandatory = $True, ValueFromPipelineByPropertyName=$True, position=0)]
        [ValidateScript({
            ($_ -match '^[a-f0-9]{32}$')
        })]
        [Alias('id')]
        [String]$RuleID,

        [Parameter()]
        [ValidateScript({
            IsNullOrCFID $_
        })]
        [String]$ZoneID
    )   
    
    Begin {
        $FunctionName = $MyInvocation.MyCommand.Name

        # If the ZoneID is empty see if we have loaded one earlier in the module and use it instead.
        if ([string]::IsNullOrEmpty($ZoneID) -and ($Script:ZoneID -ne $null)) {
            Write-Verbose "$($FunctionName): No ZoneID was passed but the current targeted zone was $($Script:ZoneName) so this will be used."
            $ZoneID = $Script:ZoneID
        }
    }

    Process {
        # URI will change if we specified a DNS zone or not
        if (-not ([string]::IsNullOrEmpty($ZoneID))) {
            $Uri = $Script:APIURI +('/zones/{0}/firewall/access_rules/rules/{1}' -f $ZoneID, $RuleID)
        }
        else {
            $Uri = $Script:APIURI + ('/user/firewall/access_rules/rules/{0}' -f $RuleID)
        }

        Set-CFRequestData -Uri $Uri -Method 'Delete'
        
        if ($pscmdlet.ShouldProcess( "Firewall Rule ID $($RuleID)")) {
            try {
                Write-Verbose -Message "$($FunctionName): Attempting to remove firewall rule ID $($RuleID)"
                $Response = Invoke-CFAPI4Request -ErrorAction Stop
            }
            catch {
                Throw $_
            }
        }
    }
}


Function Set-CFCurrentZone {
<#
    .EXTERNALHELP PSCloudFlare-help.xml
    .LINK
        https://github.com/zloeber/PSCloudFlare/tree/master/release/0.0.5/docs/Functions/Set-CFCurrentZone.md
    #>


    [CmdletBinding()]
    Param (
        [Parameter()]
        [String]$Zone
    )

    if ([string]::IsNullOrEmpty($Zone)) {
        Write-Verbose -Message ('Set-CFCurrentZone: Unsetting the targetted DNS zone.')
        $Script:ZoneID = $null
    }
    else {
        try {
            Write-Verbose -Message ('Set-CFCurrentZone: Attempting to get Zone id for {0}' -f $Zone)
            $Script:ZoneID = Get-CFZoneID -Zone $Zone -ErrorAction Stop
            $Script:ZoneName = $Zone
            Write-Verbose -Message ('Set-CFCurrentZone: Zone id is {0}' -f $Script:ZoneID)
        }
        catch {
            throw $_
        }
    }

}



Function Set-CFFirewallRule {
<#
    .EXTERNALHELP PSCloudFlare-help.xml
    .LINK
        https://github.com/zloeber/PSCloudFlare/tree/master/release/0.0.5/docs/Functions/Set-CFFirewallRule.md
    #>


    [CmdletBinding()]
    Param (
        [Parameter()]
        [ValidateScript({ IsNullOrCFID $_ })]
        [String]$ZoneID,
        
        [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
        [ValidateScript({ IsCFID $_ })]
        [String[]]$ID,

        [Parameter()]
        [String]$Item = $null,

        [Parameter()]
        [CFFirewallTarget]$Target,

        [Parameter()]
        [CFFirewallMode]$Mode,

        [Parameter()] 
        [String]$Notes = $null
    )

    Begin {
        $FunctionName = $MyInvocation.MyCommand.Name

        # If the ZoneID is empty see if we have loaded one earlier in the module and use it instead.
        if ([string]::IsNullOrEmpty($ZoneID) -and ($Script:ZoneID -ne $null)) {
            Write-Verbose "$($FunctionName): No ZoneID was passed but the current targeted zone was $($Script:ZoneName) so this will be used."
            $ZoneID = $Script:ZoneID
        }

        $Data = @{}

        if ([CFFirewallMode]::$Mode -ne $null) {
            $Data.mode = [CFFirewallMode]::$Mode
        }
        if ($Notes -ne $null) {
            $Data.notes = $Notes
        }
        Write-Verbose "$($FunctionName): Target parameter = $([CFFirewallTarget]::$Target)"
        switch ([CFFirewallTarget]::$Target) {
            'ip' { 
                if (-not ($Item -match [IPAddress]$Item)) { 
                    throw 'IP address is not valid'
                }
            }
            'ip_range' {
                if (-not (IsIPRange $Item)) {
                    throw "Subnet address '$Item' does not match the expected CIDR format (example: 192.168.0.0/24)"
                }
            }
            'country' {
                if (-not (IsCountryCode $Item)) {
                    throw "Country code '$Item' does not match the expected country code format (example: US)"
                }
            }
            'asn' {
                if ($Item -notmatch '^(AS\d+)$') {
                    throw "ASN '$Item' does not match the expected ASN format (example: AS1234)"
                }
            }
            default {}
        }
        if ( -not [string]::IsNullOrEmpty($Item -ne $null)) {
            Write-Verbose "$($FunctionName): A Target and Item were passed ($($Item) - $([CFFirewallTarget]::$Target)), adding this to the data request."
            $Data.configuration = @{
                'value' = $Item
                'target' = [CFFirewallTarget]::$Target
            }
        }
    }
    Process {
        $ID | Foreach {
            $ThisID = $_
            # If we specified a zone earlier, create the access rules there, else, do it at a user level
            if (-not ([string]::IsNullOrEmpty($ZoneID))) {
                Write-Verbose "$($FunctionName): Targeting only $($Script:ZoneName) for this firewall rule."
                $Uri = $Script:APIURI + ('/zones/{0}/firewall/access_rules/rules/{1}' -f $ZoneID, $ThisID)
                $Data.group = @{
                    'id' = 'zone'
                }
            }
            else {
                Write-Verbose "$($FunctionName): Targeting entire organization for this firewall rule."
                $Uri = $Script:APIURI + ('/user/firewall/access_rules/rules/{0}' -f $ThisID)
                $Data.group = @{
                    'id' = 'owner'
                }
            }
            
            Write-Verbose "$($FunctionName): URI = '$Uri'"

            try {
                Set-CFRequestData -Uri $Uri -Body $Data -Method 'Patch'
                $Response = Invoke-CFAPI4Request -ErrorAction Stop
            }
            catch {
                Throw $_
            }
        }
    }
}


Function Set-CFRequestData {
<#
    .EXTERNALHELP PSCloudFlare-help.xml
    .LINK
        https://github.com/zloeber/PSCloudFlare/tree/master/release/0.0.5/docs/Functions/Set-CFRequestData.md
    #>


    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $True)]
        [ValidateNotNullOrEmpty()]
        [String]$URI,

        [Parameter()]
        [hashtable]$Headers = $Script:Headers,

        [Parameter()]
        [hashtable]$Body = $null,

        [Parameter()]
        [ValidateSet('Head','Get','Put', 'Patch', 'Post', 'Delete')]
        [String]$Method = 'Get'
    )

    $Script:RESTParams = @{
        'Uri' = $URI
        'Headers' = $Headers
        'Body' = $Body
        'Method' = $Method
    }
}


## Post-Load Module code ##

# Use this variable for any path-sepecific actions (like loading dlls and such) to ensure it will work in testing and after being built
$MyModulePath = $(
    Function Get-ScriptPath {
        $Invocation = (Get-Variable MyInvocation -Scope 1).Value
        if($Invocation.PSScriptRoot) {
            $Invocation.PSScriptRoot
        }
        Elseif($Invocation.MyCommand.Path) {
            Split-Path $Invocation.MyCommand.Path
        }
        elseif ($Invocation.InvocationName.Length -eq 0) {
            (Get-Location).Path
        }
        else {
            $Invocation.InvocationName.Substring(0,$Invocation.InvocationName.LastIndexOf("\"));
        }
    }

    Get-ScriptPath
)

# Load any plugins found in the plugins directory
if (Test-Path (Join-Path $MyModulePath 'plugins')) {
    Get-ChildItem (Join-Path $MyModulePath 'plugins') -Directory | ForEach-Object {
        if (Test-Path (Join-Path $_.FullName "Load.ps1")) {
            Invoke-Command -NoNewScope -ScriptBlock ([Scriptblock]::create(".{$(Get-Content -Path (Join-Path $_.FullName "Load.ps1") -Raw)}")) -ErrorVariable errmsg 2>$null
        }
    }
}

$ExecutionContext.SessionState.Module.OnRemove = {
    # Action to take if the module is removed
    # Unload any plugins found in the plugins directory
    if (Test-Path (Join-Path $MyModulePath 'plugins')) {
        Get-ChildItem (Join-Path $MyModulePath 'plugins') -Directory | ForEach-Object {
            if (Test-Path (Join-Path $_.FullName "UnLoad.ps1")) {
                Invoke-Command -NoNewScope -ScriptBlock ([Scriptblock]::create(".{$(Get-Content -Path (Join-Path $_.FullName "UnLoad.ps1") -Raw)}")) -ErrorVariable errmsg 2>$null
            }
        }
    }
}

$null = Register-EngineEvent -SourceIdentifier ( [System.Management.Automation.PsEngineEvent]::Exiting ) -Action {
    # Action to take if the whole pssession is killed
    # Unload any plugins found in the plugins directory
    if (Test-Path (Join-Path $MyModulePath 'plugins')) {
        Get-ChildItem (Join-Path $MyModulePath 'plugins') -Directory | ForEach-Object {
            if (Test-Path (Join-Path $_.FullName "UnLoad.ps1")) {
                Invoke-Command -NoNewScope -ScriptBlock [Scriptblock]::create(".{$(Get-Content -Path (Join-Path $_.FullName "UnLoad.ps1") -Raw)}") -ErrorVariable errmsg 2>$null
            }
        }
    }
}

# Use this in your scripts to check if the function is being called from your module or independantly.
$ThisModuleLoaded = $true

# Non-function exported public module members might go here.
#Export-ModuleMember -Variable SomeVariable -Function *