UMN-Qualys.psm1

#region Intro
###
# Copyright 2017 University of Minnesota, Office of Information Technology

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with Foobar. If not, see <http://www.gnu.org/licenses/>.

# Based off
# https://community.qualys.com/community/developer
# https://www.qualys.com/docs/qualys-api-quick-reference.pdf
# https://www.qualys.com/docs/qualys-api-v2-user-guide.pdf
# https://www.qualys.com/docs/qualys-asset-management-tagging-api-v2-user-guide.pdf

#endregion

#region Connect-Qualys
function Connect-Qualys{
    <#
        .Synopsis
        Connect to Qualys API and get back session $cookie for all other functions

        .DESCRIPTION
            Connect to Qualys API and get back session $cookie for all other functions.

        .PARAMETER qualysCred
            use Get-Credential to create a PSCredential with the username and password of an account that has access to Qualys

        .PARAMETER qualysServer
            FQDN of qualys server, see Qualys documentation, based on wich Qualys Platform you're in.
        
        .PARAMETER assetTagging
            There are two different api endpoints, the new one is Asset Management and Tagging. Use this switch to get a cookie to make calls to Asset Management and Tagging

        .EXAMPLE
            $cookie = Connect-Qualys -qualysCred $qualysCred -qualysServer $qualysServer
        
        .EXAMPLE
            $cookie = Connect-Qualys -qualysCred $qualysCred -qualysServer $qualysServer -assetTagging
            
        .Notes
            Author: Travis Sobeck, Kyle Weeks
    #>

    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory)]
        [System.Management.Automation.PSCredential]$qualysCred,

        [Parameter(Mandatory)]
        [string]$qualysServer,

        [switch]$assetTagging
        
    )

    Begin{}
    Process
    {
        $qualysuser = $qualysCred.UserName
        $qualysPswd = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($qualysCred.Password))
   
        if ($assetTagging)
        {
            $auth = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($qualysuser+':'+$qualysPswd))    
            $header += @{"Authorization" = "Basic $auth"}        
            $response = Invoke-RestMethod -Uri "https://$qualysServer/qps/rest/portal/version" -Method GET -SessionVariable cookie -Headers $header
            return $cookie

        }
        else
        {
            ############# Log in #############
            ## URL for Logging In/OUT
        
            ## Login/out
            $logInBody = @{
                action = "login"
                username = $qualysuser
                password = $qualysPswd
            }

            ## Log in SessionVariable captures the cookie
            $uri = "https://$qualysServer/api/2.0/fo/session/"
            $response = Invoke-RestMethod -Headers @{"X-Requested-With"="powershell"} -Uri $uri -Method Post -Body $logInBody -SessionVariable cookie
            return $cookie
        }
        
    }
    End{}
}
#endregion

#region Disconnect-Qualys
function Disconnect-Qualys{
<#
    .Synopsis
       Disconnect Qaulys API Session, this only works on the old API

    .DESCRIPTION
         Disconnect Qaulys API Session, this only works on the old API

    .PARAMETER qualysServer
            FQDN of qualys server, see Qualys documentation, based on wich Qualys Platform you're in.

    .PARAMETER cookie
        Use Connect-Qualys to get session cookie

    .EXAMPLE
        disconnect-Qualys -uri 'https://qualysapi.qualys.com:443/api/2.0/fo/session/' -header (Get-QualysHeader)
        
    .Notes
        Author: Travis Sobeck
#>

    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory)]
        [string]$qualysServer,

        [Parameter(Mandatory)]
        [Microsoft.PowerShell.Commands.WebRequestSession]$cookie
    )

    Begin{}
    Process
    {
        ## Login/out
        $logInBody = @{action = "logout"}
        $return = (Invoke-RestMethod -Headers @{"X-Requested-With"="powershell"} -Uri "https://$qualysServer/api/2.0/fo/session/" -Method Post -Body $logInBody -WebSession $cookie).SIMPLE_RETURN.RESPONSE.TEXT
        if ($return -eq 'Logged out'){return $true}
        else{Write-Warning "Qualys logout issue" + $return}
    }
    End{}
}
#endregion

#region Get-QualysAssetGrp
function Get-QualysAssetGrp{
    <#
        .Synopsis
            Get a list of AssetGroup IDs or the ID for a specific AssetGroup

        .DESCRIPTION
            Get a list of AssetGroup IDs or the ID for a specific AssetGroup

        .PARAMETER id
            Asset Group ID, use this to get a single Asset Group

        .PARAMETER qualysServer
            FQDN of qualys server, see Qualys documentation, based on wich Qualys Platform you're in.

        .PARAMETER cookie
            Use Connect-Qualys to get session cookie

    #>


    [CmdletBinding()]
    Param
    (
        [string]$id,

        [Parameter(Mandatory)]
        [string]$qualysServer,

        [Parameter(Mandatory)]
        [Microsoft.PowerShell.Commands.WebRequestSession]$cookie
    )

    Begin{}
    Process
    {
        $actionBody = @{action = "list"}
        if($id){$actionBody['ids'] = $id}
        [xml]$returnedXML = Invoke-RestMethod -Headers @{"X-Requested-With"="powershell"} -Uri "https://$qualysServer/api/2.0/fo/asset/group" -Method Get -Body $actionBody -WebSession $cookie
        $data = $returnedXML.ASSET_GROUP_LIST_OUTPUT.RESPONSE.ASSET_GROUP_LIST.ASSET_GROUP        
        if($id){$data;$data.TITLE.'#cdata-section';$data.IP_SET}
        else{foreach ($n in 0..($data.Length -1)){"--------------";"Title: " +$data.Get($n).TITLE.'#cdata-section';$data.Get($n);$data.IP_SET}}
    }
    End{}
}
#endregion

#region Get-QualysHostAsset
function Get-QualysHostAsset{
    <#
        .Synopsis
            Get Host Asset

        .DESCRIPTION
            Get Host Asset
            
        .PARAMETER hostID
            ID of a host

        .PARAMETER searchTerm
            part of the name of Host Asset that will be used in a "Contains" search

        .PARAMETER operator
            operator to apply to searchTerm, options are 'CONTAINS','EQUALS','NOT EQUALS'. NOTE 'EQUALS' IS case sensative!

        .PARAMETER IP
            Get Host Asset by IP address
        
        .PARAMETER filter
            The search section can take a lot of params, see the Qualys Documentation for details. us the filter PARAMETER to create your own custom search
        
        .PARAMETER qualysServer
            FQDN of qualys server, see Qualys documentation, based on wich Qualys Platform you're in.

        .PARAMETER cookie
            Use Connect-Qualys to get session cookie

        .EXAMPLE
            

        .EXAMPLE
            
    #>

    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory,ParameterSetName='ID')]
        [string]$hostID,

        [Parameter(Mandatory,ParameterSetName='Search')]
        [string]$searchTerm,

        [Parameter(ParameterSetName='Search')]
        [ValidateSet('CONTAINS','EQUALS','NOT EQUALS')]
        [string]$operator = 'CONTAINS',

        [Parameter(Mandatory,ParameterSetName='ip')]
        [ValidatePattern("\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}")]
        [string]$ip,

        [Parameter(Mandatory,ParameterSetName='filter')]
        [System.Collections.Hashtable]$filter,

        [Parameter(Mandatory)]
        [string]$qualysServer,

        [Parameter(Mandatory)]
        [Microsoft.PowerShell.Commands.WebRequestSession]$cookie
    )

    Begin{}
    Process
    {
        if ($hostID)
        {
            $response = Invoke-RestMethod -Uri "https://$qualysServer/qps/rest/2.0/get/am/hostasset/$hostID" -Method GET -Headers @{'Content-Type' = 'application/json'} -WebSession $cookie
        }
        elseif ($filter)
        {
            $body = $filter | ConvertTo-Json -Depth 5
            $response = Invoke-RestMethod -Uri "https://$qualysServer/qps/rest/2.0/search/am/hostasset" -Method Post -Headers @{'Content-Type' = 'application/json'} -WebSession $cookie -Body $body
        }
        elseif ($ip)
        {
            $body = @{ServiceRequest = @{filters = @{Criteria = @(@{"field" = "address";"operator" = "EQUALS";"value" = $ip})}}} | ConvertTo-Json -Depth 5
            $response = Invoke-RestMethod -Uri "https://$qualysServer/qps/rest/2.0/search/am/hostasset" -Method Post -Headers @{'Content-Type' = 'application/json'} -WebSession $cookie -Body $body
        }
        else
        {
            $body = @{ServiceRequest = @{filters = @{Criteria = @(@{"field" = "name";"operator" = $operator;"value" = $searchTerm})}}} | ConvertTo-Json -Depth 5
            $response = Invoke-RestMethod -Uri "https://$qualysServer/qps/rest/2.0/search/am/hostasset" -Method Post -Headers @{'Content-Type' = 'application/json'} -WebSession $cookie -Body $body
        }
        return $response.ServiceResponse.data.HostAsset
    }
    End{}
}
#endregion

#region Get-QualysReport
function Get-QualysReport{
<#
    .Synopsis
        Download Qualys Report

    .DESCRIPTION
        Download Qualys Report

    .PARAMETER id
        Report ID, use Get-QualysReportList to find the ID

    .PARAMETER qualysServer
            FQDN of qualys server, see Qualys documentation, based on wich Qualys Platform you're in.

    .PARAMETER cookie
        Use Connect-Qualys to get session cookie

    .EXAMPLE
        

    .EXAMPLE
        
#>


    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory)]
        [string]$id,
        
        [Parameter(Mandatory)]
        [string]$outFilePath,

        [Parameter(Mandatory)]
        [string]$qualysServer,

        [Parameter(Mandatory)]
        [Microsoft.PowerShell.Commands.WebRequestSession]$cookie
    )

    Begin{}
    Process
    {
        ### get the format type
        $format = (Get-QualysReportList -qualysServer $qualysServer -cookie $cookie -id $id).OUTPUT_FORMAT
        $outfile = "$outFilePath\qualysReport$ID.$format"
        ## Create URL, see API docs for path
        $null = Invoke-RestMethod -Headers @{"X-Requested-With"="powershell"} -Uri "https://$qualysServer/api/2.0/fo/report/" -Method get -Body @{action = "fetch";id = "$id"} -WebSession $cookie -OutFile $outfile
    }
    End{return $outfile}
}
#endregion

#region Get-QualysReportList
function Get-QualysReportList{
    <#
        .Synopsis
            Get list of Qualys Reports

        .DESCRIPTION
            Get list of Qualys Reports
            
        .PARAMETER id
            (Optional) Qualys Report ID, use this to get details on a specific ID

        .PARAMETER qualysServer
                FQDN of qualys server, see Qualys documentation, based on wich Qualys Platform you're in.

        .PARAMETER cookie
            Use Connect-Qualys to get session cookie

        .EXAMPLE
            

        .EXAMPLE
            
    #>


    [CmdletBinding()]
    Param
    (
        [string]$id,

        [Parameter(Mandatory)]
        [string]$qualysServer,

        [Parameter(Mandatory)]
        [Microsoft.PowerShell.Commands.WebRequestSession]$cookie
    )

    Begin{}
    Process
    {
        ## Create URL, see API docs for path
        #########################
        $actionBody = @{action = "list"}
        if($id){$actionBody['id'] = $id}
        [xml]$returnedXML = Invoke-RestMethod -Headers @{"X-Requested-With"="powershell"} -Uri "https://$qualysServer/api/2.0/fo/report/" -Method Get -Body $actionBody -WebSession $cookie
        $data = $returnedXML.REPORT_LIST_OUTPUT.RESPONSE.REPORT_LIST.REPORT
        if($id){$data;$data.TITLE.'#cdata-section'}
        else{foreach ($n in 0..($data.Length -1)){"--------------";"Title: " +$data.Get($n).TITLE.'#cdata-section';$data.Get($n)}}
    }
    End{}
}
#endregion

#region Get-QualysScanList
function Get-QualysScanList{
    <#
        .Synopsis
            Get list of Qualys Scans

        .DESCRIPTION
            Get list of Qualys Scans
            
        .PARAMETER scanRef
            (Optional) Qualys Scan Reference, use this to get details on a specific Scan

        .PARAMETER additionalOptions
            See documentation for full list of additional options and pass in as hashtable

        .PARAMETER brief
            Use this switch to get just the title and Ref for faster searching
        
        .PARAMETER qualysServer
        FQDN of qualys server, see Qualys documentation, based on wich Qualys Platform you're in.

        .PARAMETER cookie
            Use Connect-Qualys to get session cookie

        .EXAMPLE
            

        .EXAMPLE
            
    #>


    [CmdletBinding()]
    Param
    (
        [string]$scanRef,

        [System.Collections.Hashtable]$additionalOptions,

        [switch]$brief,

        [Parameter(Mandatory)]
        [string]$qualysServer,

        [Parameter(Mandatory)]
        [Microsoft.PowerShell.Commands.WebRequestSession]$cookie
    )

    Begin{}
    Process
    {
        ## Create URL, see API docs for path
        #########################
        $actionBody = @{action = "list"}
        if($scanRef){$actionBody['scan_ref'] = $scanRef}
        if($additionalOptions){$actionBody += $additionalOptions}
        [xml]$returnedXML = Invoke-RestMethod -Headers @{"X-Requested-With"="powershell"} -Uri "https://$qualysServer/api/2.0/fo/scan/" -Method Get -Body $actionBody -WebSession $cookie
        $data = $returnedXML.SCAN_LIST_OUTPUT.RESPONSE.SCAN_LIST.SCAN
        if ($brief)
        {
            if($scanRef){$data.TITLE.'#cdata-section';$data.REF}
            else
            {
                foreach ($n in 0..($data.Length -1)){"--------------";$data.Get($n).TITLE.'#cdata-section';$data[$n].REF}
            }
        }
        else
        {
            if($scanRef){"`n--------------`n";"Title: " +$data.TITLE.'#cdata-section';($data | Select-Object REF,TYPE,USER_LOGIN,LAUNCH_DATETIME,DURATION,PROCESSING_PRIORITY,PROCESSED);"State: " + $data.STATUS.STATE;"Target: " + $data.TARGET.'#cdata-section'}
            else
            {
                foreach ($n in 0..($data.Length -1)){"`n--------------`n";"Title: " +$data.Get($n).TITLE.'#cdata-section';($data.Get($n) | Select-Object REF,TYPE,USER_LOGIN,LAUNCH_DATETIME,DURATION,PROCESSING_PRIORITY,PROCESSED);"State: " + $data[$n].STATUS.STATE;"Target: " + $data[$n].TARGET.'#cdata-section'}
            }
        }
    }
    End{}
}
#endregion

#region Get-QualysScanResults
function Get-QualysScanResults{
    <#
        .Synopsis
            Get results of Qualys Scan

        .DESCRIPTION
            Get reults of Qualys Scan
            
        .PARAMETER scanRef
            Qualys Scan Reference, use Get-QualysScanList to find the reference

        .PARAMETER additionalOptions
            See documentation for full list of additional options and pass in as hashtable

        .PARAMETER summary
            Use this switch to get just the title and Ref for faster searching
        
        .PARAMETER qualysServer
            FQDN of qualys server, see Qualys documentation, based on wich Qualys Platform you're in.

        .PARAMETER cookie
            Use Connect-Qualys to get session cookie

        .EXAMPLE
            

        .EXAMPLE
            
    #>


    [CmdletBinding()]
    Param
    (
        [string]$scanRef,

        [System.Collections.Hashtable]$additionalOptions,

        [switch]$brief,

        [Parameter(Mandatory)]
        [string]$qualysServer,

        [Parameter(Mandatory)]
        [Microsoft.PowerShell.Commands.WebRequestSession]$cookie
    )

    Begin{}
    Process
    {
        ## Create URL, see API docs for path
        #########################
        $actionBody = @{action = "fetch";scan_ref = $scanRef;output_format='json'}
        if($additionalOptions){$actionBody += $additionalOptions}
        if($brief){$actionBody += @{mode='brief'}}
        Invoke-RestMethod -Headers @{"X-Requested-With"="powershell"} -Uri "https://$qualysServer/api/2.0/fo/scan/" -Method Get -Body $actionBody -WebSession $cookie
        
    }
    End{}
}
#endregion

#region Get-QualysSchedReportList
function Get-QualysSchedReportList{
    <#
        .Synopsis
            Get a list of Reports Scheduled

        .DESCRIPTION
            Get a list of Reports Scheduled

        .PARAMETER id
            (Optional) Report Schedule ID

        .PARAMETER qualysServer
        FQDN of qualys server, see Qualys documentation, based on wich Qualys Platform you're in.

        .PARAMETER cookie
            Use Connect-Qualys to get session cookie
    #>


    [CmdletBinding()]
    Param
    (
        [string]$id,
        
        [Parameter(Mandatory)]
        [string]$qualysServer,

        [Parameter(Mandatory)]
        [Microsoft.PowerShell.Commands.WebRequestSession]$cookie
    )

    Begin{}
    Process
    {
        ## Create URL, see API docs for path
        #########################
        $actionBody = @{action = "list"}
        if($id){$actionBody['id'] = $id}
        [xml]$returnedXML = Invoke-RestMethod -Headers @{"X-Requested-With"="powershell"} -Uri "https://$qualysServer/api/2.0/fo/schedule/report" -Method Get -Body $actionBody -WebSession $cookie
        $data = $returnedXML.SCHEDULE_REPORT_LIST_OUTPUT.RESPONSE.SCHEDULE_REPORT_LIST.REPORT
        if($id){$data;$data.TITLE.'#cdata-section';$data.TEMPLATE_TITLE.'#cdata-section';$data.SCHEDULE}
        else{foreach ($n in 0..($data.Length -1)){"--------------";"Title: " +$data.Get($n).TITLE.'#cdata-section';$data.Get($n);$data.Get($n).TEMPLATE_TITLE.'#cdata-section';$data.Get($n).SCHEDULE}}
    }
    End{}
}
#endregion

#region Get-QualysTagCount
function Get-QualysTagCount{
    <#
        .Synopsis
            Get-QualysTagCount

        .DESCRIPTION
            Get-QualysTagCount
            
        .PARAMETER qualysServer
            FQDN of qualys server, see Qualys documentation, based on wich Qualys Platform you're in.

        .PARAMETER cookie
            Use Connect-Qualys to get session cookie
    #>

    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory)]
        [string]$qualysServer,

        [Parameter(Mandatory)]
        [Microsoft.PowerShell.Commands.WebRequestSession]$cookie
    )

    Begin{}
    Process
    {
        $response = Invoke-RestMethod -Uri "https://$qualysServer/qps/rest/2.0/count/am/tag" -Method Post -Headers @{'Content-Type' = 'application/json'} -WebSession $cookie
        if ($response.ServiceResponse.count){return $response.ServiceResponse.count}
        else{throw "Error $($response.ServiceResponse)"}
    }
    End{}
}
#endregion

#region Get-QualysTag
function Get-QualysTag{
    <#
        .Synopsis
            Get Qualys Tag(s)

        .DESCRIPTION
            Get Qualys Tag(s)
            
        .PARAMETER tagID
            ID of a tag

        .PARAMETER searchTerm
            part of the name of tag that will be used in a "Contains" search

        .PARAMETER operator
            operator to apply to searchTerm, options are 'CONTAINS','EQUALS','NOT EQUALS'. NOTE 'EQUALS' IS case sensative!

        .PARAMETER filter
            The search section can take a lot of params, see the Qualys Documentation for details. us the filter PARAMETER to create your own custom search
        
        .PARAMETER qualysServer
            FQDN of qualys server, see Qualys documentation, based on wich Qualys Platform you're in.

        .PARAMETER cookie
            Use Connect-Qualys to get session cookie
    #>

    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory,ParameterSetName='ID')]
        [string]$tagID,

        [Parameter(Mandatory,ParameterSetName='Search')]
        [string]$searchTerm,

        [Parameter(ParameterSetName='Search')]
        [ValidateSet('CONTAINS','EQUALS','NOT EQUALS')]
        [string]$operator = 'CONTAINS',

        [Parameter(Mandatory,ParameterSetName='filter')]
        [System.Collections.Hashtable]$filter,

        [Parameter(Mandatory)]
        [string]$qualysServer,

        [Parameter(Mandatory)]
        [Microsoft.PowerShell.Commands.WebRequestSession]$cookie
    )

    Begin{}
    Process
    {
        if ($tagID)
        {
            $response = Invoke-RestMethod -Uri "https://$qualysServer/qps/rest/2.0/get/am/tag/$tagID" -Method GET -Headers @{'Content-Type' = 'application/json'} -WebSession $cookie
        }
        elseif ($filter)
        {
            $body = $filter | ConvertTo-Json -Depth 5
            $response = Invoke-RestMethod -Uri "https://$qualysServer/qps/rest/2.0/search/am/tag" -Method Post -Headers @{'Content-Type' = 'application/json'} -WebSession $cookie -Body $body
        }
        else
        {
            $body = @{ServiceRequest = @{filters = @{Criteria = @(@{"field" = "name";"operator" = $operator;"value" = $searchTerm})}}} | ConvertTo-Json -Depth 5
            $response = Invoke-RestMethod -Uri "https://$qualysServer/qps/rest/2.0/search/am/tag" -Method Post -Headers @{'Content-Type' = 'application/json'} -WebSession $cookie -Body $body
        }
        return $response.ServiceResponse.data.Tag 
    }
    End{}
}
#endregion

#region New-QualysHostAsset
function New-QualysHostAsset{
    <#
        .Synopsis
            Create New Qualys Asset

        .DESCRIPTION
            Create New Qualys Host Asset
        .PARAMETER assetName
            Host Asset's FQDN to be added
        .PARAMETER tagID
            ID of tag to add at build time
        .PARAMETER qualysServer
            FQDN of qualys server, see Qualys documentation, based on wich Qualys Platform you're in.

        .PARAMETER cookie
            Use Connect-Qualys to get session cookie
    #>

    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory)]
        [string]$assetName,

        [Parameter(Mandatory)]
        [string]$ip,

        [string]$tagID,

        [Parameter(Mandatory)]
        [string]$qualysServer,

        [Parameter(Mandatory)]
        [Microsoft.PowerShell.Commands.WebRequestSession]$cookie
    )

    Begin{}
    Process
    {
        
        $body = @{ServiceRequest = @{data = @{HostAsset = @{name = $assetName;address=$ip;trackingMethod='IP'}}}}
        if($tagID){$body['ServiceRequest']['data']['HostAsset']['tags'] = @{set=@{TagSimple = @{id = $tagID}}}}
        $body = $body | ConvertTo-Json -Depth 7
        $response = Invoke-RestMethod -Uri "https://$qualysServer/qps/rest/2.0/create/am/hostasset" -Method Post -Headers @{'Content-Type' = 'application/json'} -WebSession $cookie -Body $body
        if ($response.ServiceResponse.responseCode -eq "SUCCESS"){return $response.ServiceResponse.data.HostAsset}
        else{Throw $($response.ServiceResponse.responseErrorDetails.errorMessage)}
    }
    End{}
}
#endregion

#region Invoke-QualysBase
function Invoke-QualysBase{
    <#
        .Synopsis
            Not currently useable
        .DESCRIPTION
            
        .PARAMETER qualysServer
            FQDN of qualys server, see Qualys documentation, based on wich Qualys Platform you're in.

        .PARAMETER cookie
            Use Connect-Qualys to get session cookie

    #>

    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory)]
        [System.Collections.Hashtable]$body,
        
        [Parameter(Mandatory)]
        [string]$method,

        [Parameter(Mandatory)]
        [string]$qualysServer,

        [Parameter(Mandatory)]
        [Microsoft.PowerShell.Commands.WebRequestSession]$cookie
    )

    Begin{}
    Process
    {
        return (Invoke-RestMethod -Headers @{"X-Requested-With"="powershell"} -Uri "https://$qualysServer/api/2.0/fo/" -Method $method -Body $body -WebSession $cookie)
    }
    End{}
}
#endregion

#region New-QualysIP
function New-QualysIP{
    <#
        .Synopsis
            Add an IP to a specific Group

        .DESCRIPTION
            Add an IP to a specific Group

        .PARAMETER qualysServer
            FQDN of qualys server, see Qualys documentation, based on wich Qualys Platform you're in.

        .PARAMETER cookie
            Use Connect-Qualys to get session cookie

    #>

    [CmdletBinding()]
    Param
    (
        [ValidatePattern("\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}")]
        [string]$ip,

        [Parameter(Mandatory)]
        [string]$groupID,

        [Parameter(Mandatory)]
        [string]$qualysServer,

        [Parameter(Mandatory)]
        [Microsoft.PowerShell.Commands.WebRequestSession]$cookie
    )

    Begin{}
    Process
    {
        $actionBody = @{
            action = "list"
            ids = $groupID
        }        
        ## Run your action, WebSession contains the cookie from login
        [xml]$returnedXML = Invoke-RestMethod -Headers @{"X-Requested-With"="powershell"}-Uri "https://$qualysServer/api/2.0/fo/asset/group/" -Method Post -Body $actionBody -WebSession $cookie

        # Single IPs
        [System.Collections.ArrayList]$ips = $returnedXML.ASSET_GROUP_LIST_OUTPUT.RESPONSE.ASSET_GROUP_LIST.ASSET_GROUP.IP_SET.IP
        # IP Ranges, these will take more work to extrapolate
        [System.Collections.ArrayList]$ipRanges = $returnedXML.ASSET_GROUP_LIST_OUTPUT.RESPONSE.ASSET_GROUP_LIST.ASSET_GROUP.IP_SET.IP_RANGE

        ## break up the ip range strings, extract all the ips .. blah blah
        foreach ($range in $ipRanges)
        {
            $a,$b = $range -split "-"
            $a1,$a2,$a3,[int]$a4 = $a -split "\."
            $b1,$b2,$b3,[int]$b4 = $b -split "\."
            foreach ($i in $a4 .. $b4)
            {
                $newIP = $a1 + "." + $a2 + "." + $a3 + "." + [string]$i
                # add to the array of IPs, check for doubles
                if ($ips -notcontains $newIP){$null = $ips.Add($newIP)}
            }
    
        }
        ########################### now we have a full list of IPs to check against
        ### check if IP to be added is is in the list
        if ($ips -notcontains $ip)
        {
            $actionBody = @{
                action = "edit"
                id = $groupID
                add_ips = $ip
            }
            [xml]$response = Invoke-RestMethod -Headers @{"X-Requested-With"="powershell"}-Uri "https://$qualysServer/api/2.0/fo/asset/group/" -Method Post -Body $actionBody -WebSession $cookie
            ## check that it worked
            if (-not ($response.SIMPLE_RETURN.RESPONSE.TEXT -eq 'Asset Group Updated Successfully')){throw "Failed to add IP $ip -- $qualysError"}
            else{return $true}
             
        }
        else{return $true}  
    }
    End
    {
        $returnedXML = $null
    }
}
#endregion

#region New-QualysTag
function New-QualysTag{
    <#
        .Synopsis
            Create New Qualys Tag

        .DESCRIPTION
            Create New Qualys Tag
            
        .PARAMETER tagName
            Name of a tag to create
        
        .PARAMETER qualysServer
            FQDN of qualys server, see Qualys documentation, based on wich Qualys Platform you're in.

        .PARAMETER cookie
            Use Connect-Qualys to get session cookie
    #>

    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory,ParameterSetName='ID')]
        [string]$tagName,

        [Parameter(Mandatory)]
        [string]$qualysServer,

        [Parameter(Mandatory)]
        [Microsoft.PowerShell.Commands.WebRequestSession]$cookie
    )

    Begin{}
    Process
    {
        ## Validate tage does not already exist

        $body = @{ServiceRequest = @{data = @{Tag = @{name = $tagName}}}} | ConvertTo-Json -Depth 5
        $response = Invoke-RestMethod -Uri "https://$qualysServer/qps/rest/2.0/create/am/tag" -Method Post -Headers @{'Content-Type' = 'application/json'} -WebSession $cookie -Body $body
        if ($response.ServiceResponse.responseCode -eq "SUCCESS"){return $true}
        else{throw ($response | Select-Object *)}
    }
    End{}
}
#endregion

#region Remove-QualysHostAsset
function Remove-QualysHostAsset{
    <#
        .Synopsis
            Remove Qualys Host Asset

        .DESCRIPTION
            Remove New Qualys Host Asset
            
        .PARAMETER assetID
            Host Asset's ID
        
        .PARAMETER qualysServer
            FQDN of qualys server, see Qualys documentation, based on wich Qualys Platform you're in.

        .PARAMETER cookie
            Use Connect-Qualys to get session cookie
    #>

    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory)]
        [string]$assetID,

        [Parameter(Mandatory)]
        [string]$qualysServer,

        [Parameter(Mandatory)]
        [Microsoft.PowerShell.Commands.WebRequestSession]$cookie
    )

    Begin{}
    Process
    {
        
        $response = Invoke-RestMethod -Uri "https://$qualysServer/qps/rest/2.0/delete/am/hostasset/$assetID" -Method Post -Headers @{'Content-Type' = 'application/json'} -WebSession $cookie
        if ($response.ServiceResponse.responseCode -eq "SUCCESS"){return $true}
        else{throw ($response | Select-Object *)}
    }
    End{}
}
#endregion

#region Remove-QualysHostAssetTag
function Remove-QualysHostAssetTag{
    <#
        .Synopsis
            Remove tag from a Host Asset

        .DESCRIPTION
            Remove tag from a Host Asset
            
        .PARAMETER hostID
            ID of a host

        .PARAMETER tagID
            ID of tag to apply to Host Asset
                
        .PARAMETER qualysServer
            FQDN of qualys server, see Qualys documentation, based on wich Qualys Platform you're in.

        .PARAMETER cookie
            Use Connect-Qualys to get session cookie
      
    #>

    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory)]
        [string]$hostID,

        [Parameter(Mandatory)]
        [string]$tagID,

        [Parameter(Mandatory)]
        [string]$qualysServer,

        [Parameter(Mandatory)]
        [Microsoft.PowerShell.Commands.WebRequestSession]$cookie
    )

    Begin{}
    Process
    {
        $body = @{ServiceRequest = @{data = @{HostAsset = @{tags = @{remove = @{TagSimple = @{id = $tagID}}}}}}} | ConvertTo-Json -Depth 7
        $response = Invoke-RestMethod -Uri "https://$qualysServer/qps/rest/2.0/update/am/hostasset/$hostID" -Method Post -Headers @{'Content-Type' = 'application/json'} -WebSession $cookie -Body $body        
        ## the quayls api response is junk, to a get to test it actually got added
        if ($response.ServiceResponse.responseCode -eq 'SUCCESS'){return $true}
        else{Write-Warning $response.ServiceResponse.responseErrorDetails;return $false}
    }
    End{}
}
#endregion

#region Remove-QualysIP
function Remove-QualysIP{
    <#
        .Synopsis
            Remove IP from a group by groupId

        .DESCRIPTION
            Remove IP from a group by groupId
            
        .PARAMETER qualysServer
            FQDN of qualys server, see Qualys documentation, based on wich Qualys Platform you're in.

        .PARAMETER cookie
            Use Connect-Qualys to get session cookie
    #>

    [CmdletBinding()]
    Param
    (
        [ValidatePattern("\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}")]
        [string]$ip,

        [Parameter(Mandatory)]
        [string]$groupID,

        [Parameter(Mandatory)]
        [string]$qualysServer,

        [Parameter(Mandatory)]
        [Microsoft.PowerShell.Commands.WebRequestSession]$cookie
    )

    Begin{}
    Process
    {
        ## Look at passinging in Asset Group (High or regular) and set IP
        #########################
        $actionBody = @{
            action = "edit"
            id = $groupID
            remove_ips = $ip
        }
        [xml]$returnedXML = Invoke-RestMethod -Headers @{"X-Requested-With"="powershell"} -Uri "https://$qualysServer/api/2.0/fo/asset/group/" -Method Post -Body $actionBody -WebSession $cookie
        if ($returnedXML.SIMPLE_RETURN.RESPONSE.TEXT -ne "Asset Group Updated Successfully"){throw "Error - $ip - " + $returnedXML.SIMPLE_RETURN.RESPONSE.TEXT}
        else{return $true}
    }
    End{}
}
#endregion

#region Set-QualysHostAssetTag
function Set-QualysHostAssetTag{
    <#
        .Synopsis
            Set tag on a Host Asset

        .DESCRIPTION
            Set tag on a Host Asset
            
        .PARAMETER hostID
            ID of a host

        .PARAMETER tagID
            ID of tag to apply to Host Asset
                
        .PARAMETER qualysServer
            FQDN of qualys server, see Qualys documentation, based on wich Qualys Platform you're in.

        .PARAMETER cookie
            Use Connect-Qualys to get session cookie
      
    #>

    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory)]
        [string]$hostID,

        [Parameter(Mandatory)]
        [string]$tagID,

        [Parameter(Mandatory)]
        [string]$qualysServer,

        [Parameter(Mandatory)]
        [Microsoft.PowerShell.Commands.WebRequestSession]$cookie
    )

    Begin{}
    Process
    {
        $body = @{ServiceRequest = @{data = @{HostAsset = @{tags = @{add = @{TagSimple = @{id = $tagID}}}}}}} | ConvertTo-Json -Depth 7
        $response = Invoke-RestMethod -Uri "https://$qualysServer/qps/rest/2.0/update/am/hostasset/$hostID" -Method Post -Headers @{'Content-Type' = 'application/json'} -WebSession $cookie -Body $body        
        ## the quayls api response is junk, to a get to test it actually got added
        if ($response.ServiceResponse.responseCode -eq 'SUCCESS'){return $true}
        else{return $($response.ServiceResponse.responseErrorDetails.errorMessage)}
    }
    End{}
}
#endregion

#region Update-QualysIP
function Update-QualysIP{
    <#
        .Synopsis
            Update IP asset in Qualys.

        .DESCRIPTION
            Update the FQDN, NetBIOS, and IP tracking info of the asset.

        .PARAMETER cookie
            Use Connect-Qualys to get session cookie

        .PARAMETER fqdn
            Domain validated and tested FQDN of host. something.ad.umn.edu

        .PARAMETER ip
            Valid IP address asset to be updated

        .PARAMETER qualysServer
            FQDN of qualys server, see Qualys documentation, based on wich Qualys Platform you're in.

        .EXAMPLE
            Update-QualysIP -cookie $cookie -ip $ip -fqdn $fqdn -qualysServer $qualysServer

        .NOTES
            Authors: Kyle Weeks



    #>

    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory)]
        [Microsoft.PowerShell.Commands.WebRequestSession]$cookie,

        [ValidatePattern("\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}")]
        [string]$ip,

        [Parameter(Mandatory)]
        [string]$fqdn,

        [Parameter(Mandatory)]
        [string]$qualysServer
    )

    Begin{}
    Process
    {
        $computer = $fqdn.Split('.')[0]
        $body = "action=update&echo_request=1&ips=$ip&tracking_method=IP&host_dns=$fqdn&host_netbios=$computer"

        [xml]$response = Invoke-RestMethod -Headers @{'X-Requested-With'="powershell"} -Uri "https://$qualysServer/api/2.0/fo/asset/ip/" -Method Post -WebSession $cookie -Body $body
        ## check that it worked

        if (-not ($response.SIMPLE_RETURN.RESPONSE.TEXT -eq 'IP successfully updated')){$return = $response.SIMPLE_RETURN.RESPONSE.TEXT; throw "Failed to update IP $ip - $return"}
        else{return $true}

    }
    End
    {
        $response = $null
    }
}
#endregion

#region Update-QualysAssetGroup
function Update-QualysAssetGroup{
    <#
        .Synopsis
            Update Asset Group

        .DESCRIPTION
            Add or remove specific IP address from Asset Group.

        .PARAMETER action
            Action to be performed. Add or delete.

        .PARAMETER cookie
            Use Connect-Qualys to get session cookie

        .PARAMETER ip
            IP address of entry to add/remove

        .PARAMETER groupID
            Asset group ID number to modify

        .PARAMETER qualysServer
            FQDN of qualys server, see Qualys documentation, based on wich Qualys Platform you're in.


    #>

    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory)]
        [ValidateSet('Add','Remove')]
        [string]$action,

        [Parameter(Mandatory)]
        [Microsoft.PowerShell.Commands.WebRequestSession]$cookie,

        [ValidatePattern("\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}")]
        [string]$ip,

        [Parameter(Mandatory)]
        [int]$groupID,

        [Parameter(Mandatory)]
        [string]$qualysServer
    )

    Begin{}
    Process
    {  
        ## Run your action, WebSession contains the cookie from login
        $uri = "https://$qualysServer/api/2.0/fo/asset/group/?action=list&ids=$groupID"
        [xml]$returnedXML = Invoke-RestMethod -Headers @{"X-Requested-With"="powershell"} -Uri $uri -Method Post -WebSession $cookie

        # Single IPs
        [System.Collections.ArrayList]$ips = $returnedXML.ASSET_GROUP_LIST_OUTPUT.RESPONSE.ASSET_GROUP_LIST.ASSET_GROUP.IP_SET.IP
        # IP Ranges, these will take more work to extrapolate
        [System.Collections.ArrayList]$ipRanges = $returnedXML.ASSET_GROUP_LIST_OUTPUT.RESPONSE.ASSET_GROUP_LIST.ASSET_GROUP.IP_SET.IP_RANGE

        ## break up the ip range strings, extract all the ips part of asset group.
        foreach ($range in $ipRanges)
        {
            $a,$b = $range -split "-"
            $a1,$a2,$a3,[int]$a4 = $a -split "\."
            $b1,$b2,$b3,[int]$b4 = $b -split "\."
            foreach ($i in $a4 .. $b4)
            {
                $newIP = $a1 + "." + $a2 + "." + $a3 + "." + [string]$i
                # add to the array of IPs, check for doubles
                if ($ips -notcontains $newIP){$null = $ips.Add($newIP)}
            }
    
        }

        ### check if IP to be added is is in the list
        If ($action -eq 'Remove')
            {
                if ($ips -contains $ip)
                    {
                        $change = 'remove_ips'
                        $uri = "https://$qualysServer/api/2.0/fo/asset/group/?action=edit&id=$groupID&$change=$ip"
                        [xml]$response = Invoke-RestMethod -Headers @{"X-Requested-With"="powershell"}-Uri $uri -Method Post -WebSession $cookie
                        ## check that it worked
                        if (-not ($response.SIMPLE_RETURN.RESPONSE.TEXT -eq 'Asset Group Updated Successfully')){$errorResponse = $response.SIMPLE_RETURN.RESPONSE.TEXT;throw "Failed to update Asset Group IP $ip - $errorResponse"}
                        else{return "Asset Group Updated Successfully"}
                    }
                else{return "IP address $ip was not found in this asset group"} 
            }
        Else
            {
                if ($ips -notcontains $ip)
                                    {
                                        $change = 'add_ips'
                                        $uri = "https://$qualysServer/api/2.0/fo/asset/group/?action=edit&id=$groupID&$change=$ip"
                                        [xml]$response = Invoke-RestMethod -Headers @{"X-Requested-With"="powershell"}-Uri $uri -Method Post -WebSession $cookie
                                        ## check that it worked
                                        if (-not ($response.SIMPLE_RETURN.RESPONSE.TEXT -eq 'Asset Group Updated Successfully')){$errorResponse = $response.SIMPLE_RETURN.RESPONSE.TEXT;throw "Failed to update Asset Group IP $ip - $errorResponse"}
                                        else{return "Asset Group Updated Successfully"}
                                    }
                else{return "IP address $ip was already part of this asset group"} 

            }
    }
    End
    {
        $returnedXML = $null
    }
}
#endregion
# SIG # Begin signature block
# MIIaxgYJKoZIhvcNAQcCoIIatzCCGrMCAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB
# gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR
# AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUsmzno2MtUPB0EvplNmaQx4IL
# HjegghW3MIIEmTCCA4GgAwIBAgIPFojwOSVeY45pFDkH5jMLMA0GCSqGSIb3DQEB
# BQUAMIGVMQswCQYDVQQGEwJVUzELMAkGA1UECBMCVVQxFzAVBgNVBAcTDlNhbHQg
# TGFrZSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxITAfBgNV
# BAsTGGh0dHA6Ly93d3cudXNlcnRydXN0LmNvbTEdMBsGA1UEAxMUVVROLVVTRVJG
# aXJzdC1PYmplY3QwHhcNMTUxMjMxMDAwMDAwWhcNMTkwNzA5MTg0MDM2WjCBhDEL
# MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE
# BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKjAoBgNVBAMT
# IUNPTU9ETyBTSEEtMSBUaW1lIFN0YW1waW5nIFNpZ25lcjCCASIwDQYJKoZIhvcN
# AQEBBQADggEPADCCAQoCggEBAOnpPd/XNwjJHjiyUlNCbSLxscQGBGue/YJ0UEN9
# xqC7H075AnEmse9D2IOMSPznD5d6muuc3qajDjscRBh1jnilF2n+SRik4rtcTv6O
# KlR6UPDV9syR55l51955lNeWM/4Og74iv2MWLKPdKBuvPavql9LxvwQQ5z1IRf0f
# aGXBf1mZacAiMQxibqdcZQEhsGPEIhgn7ub80gA9Ry6ouIZWXQTcExclbhzfRA8V
# zbfbpVd2Qm8AaIKZ0uPB3vCLlFdM7AiQIiHOIiuYDELmQpOUmJPv/QbZP7xbm1Q8
# ILHuatZHesWrgOkwmt7xpD9VTQoJNIp1KdJprZcPUL/4ygkCAwEAAaOB9DCB8TAf
# BgNVHSMEGDAWgBTa7WR0FJwUPKvdmam9WyhNizzJ2DAdBgNVHQ4EFgQUjmstM2v0
# M6eTsxOapeAK9xI1aogwDgYDVR0PAQH/BAQDAgbAMAwGA1UdEwEB/wQCMAAwFgYD
# VR0lAQH/BAwwCgYIKwYBBQUHAwgwQgYDVR0fBDswOTA3oDWgM4YxaHR0cDovL2Ny
# bC51c2VydHJ1c3QuY29tL1VUTi1VU0VSRmlyc3QtT2JqZWN0LmNybDA1BggrBgEF
# BQcBAQQpMCcwJQYIKwYBBQUHMAGGGWh0dHA6Ly9vY3NwLnVzZXJ0cnVzdC5jb20w
# DQYJKoZIhvcNAQEFBQADggEBALozJEBAjHzbWJ+zYJiy9cAx/usfblD2CuDk5oGt
# Joei3/2z2vRz8wD7KRuJGxU+22tSkyvErDmB1zxnV5o5NuAoCJrjOU+biQl/e8Vh
# f1mJMiUKaq4aPvCiJ6i2w7iH9xYESEE9XNjsn00gMQTZZaHtzWkHUxY93TYCCojr
# QOUGMAu4Fkvc77xVCf/GPhIudrPczkLv+XZX4bcKBUCYWJpdcRaTcYxlgepv84n3
# +3OttOe/2Y5vqgtPJfO44dXddZhogfiqwNGAwsTEOYnB9smebNd0+dmX+E/CmgrN
# Xo/4GengpZ/E8JIh5i15Jcki+cPwOoRXrToW9GOUEB1d0MYwggV3MIIEX6ADAgEC
# AhAT6ihwW/Ts7Qw2YwmAYUM2MA0GCSqGSIb3DQEBDAUAMG8xCzAJBgNVBAYTAlNF
# MRQwEgYDVQQKEwtBZGRUcnVzdCBBQjEmMCQGA1UECxMdQWRkVHJ1c3QgRXh0ZXJu
# YWwgVFRQIE5ldHdvcmsxIjAgBgNVBAMTGUFkZFRydXN0IEV4dGVybmFsIENBIFJv
# b3QwHhcNMDAwNTMwMTA0ODM4WhcNMjAwNTMwMTA0ODM4WjCBiDELMAkGA1UEBhMC
# VVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4w
# HAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVz
# dCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUA
# A4ICDwAwggIKAoICAQCAEmUXNg7D2wiz0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzO
# iZ/MPans9s/B3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2jY0K2dvKpOyuR+OJv0OwW
# IJAJPuLodMkYtJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFnRghRy4YU
# VD+8M/5+bJz/Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1da
# t//O+T23LLb2VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+
# UzeQc0PzMsNT79uq/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/z
# JSZrM233bkf6c0Plfg6lZrEpfDKEY1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLa
# qUkL39iAigmTYo61Zs8liM2EuLE/pDkP2QKe6xJMlXzzawWpXhaDzLhn4ugTncxb
# gtNMs+1b/97lc6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8yexDJtC/QV9A
# qURE9JnnV4eeUB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+
# eLf8ZxXhyVeEHg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwID
# AQABo4H0MIHxMB8GA1UdIwQYMBaAFK29mHo0tCb3+sQmVO8DveAky1QaMB0GA1Ud
# DgQWBBRTeb9aqitKz1SA4dibwJ3ysgNmyzAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0T
# AQH/BAUwAwEB/zARBgNVHSAECjAIMAYGBFUdIAAwRAYDVR0fBD0wOzA5oDegNYYz
# aHR0cDovL2NybC51c2VydHJ1c3QuY29tL0FkZFRydXN0RXh0ZXJuYWxDQVJvb3Qu
# Y3JsMDUGCCsGAQUFBwEBBCkwJzAlBggrBgEFBQcwAYYZaHR0cDovL29jc3AudXNl
# cnRydXN0LmNvbTANBgkqhkiG9w0BAQwFAAOCAQEAk2X2N4OVD17Dghwf1nfnPIrA
# qgnw6Qsm8eDCanWhx3nJuVJgyCkSDvCtA9YJxHbf5aaBladG2oJXqZWSxbaPAyJs
# M3fBezIXbgfOWhRBOgUkG/YUBjuoJSQOu8wqdd25cEE/fNBjNiEHH0b/YKSR4We8
# 3h9+GRTJY2eR6mcHa7SPi8BuQ33DoYBssh68U4V93JChpLwt70ZyVzUFv7tGu25t
# N5m2/yOSkcZuQPiPKVbqX9VfFFOs8E9h6vcizKdWC+K4NB8m2XsZBWg/ujzUOAai
# 0+aPDuO0cW1AQsWEtECVK/RloEh59h2BY5adT3Xg+HzkjqnR8q2Ks4zHIc3C7zCC
# BawwggSUoAMCAQICEHJNXiAT1cKRQFXzfFSJVHEwDQYJKoZIhvcNAQELBQAwfDEL
# MAkGA1UEBhMCVVMxCzAJBgNVBAgTAk1JMRIwEAYDVQQHEwlBbm4gQXJib3IxEjAQ
# BgNVBAoTCUludGVybmV0MjERMA8GA1UECxMISW5Db21tb24xJTAjBgNVBAMTHElu
# Q29tbW9uIFJTQSBDb2RlIFNpZ25pbmcgQ0EwHhcNMTcxMjE0MDAwMDAwWhcNMjAx
# MjEzMjM1OTU5WjCByzELMAkGA1UEBhMCVVMxDjAMBgNVBBEMBTU1NDU1MRIwEAYD
# VQQIDAlNaW5uZXNvdGExFDASBgNVBAcMC01pbm5lYXBvbGlzMRgwFgYDVQQJDA8x
# MDAgVW5pb24gU3QgU0UxIDAeBgNVBAoMF1VuaXZlcnNpdHkgb2YgTWlubmVzb3Rh
# MSQwIgYDVQQLDBtDb21wdXRlciBhbmQgRGV2aWNlIFN1cHBvcnQxIDAeBgNVBAMM
# F1VuaXZlcnNpdHkgb2YgTWlubmVzb3RhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
# MIIBCgKCAQEAwk6kLE9u+tWv0JUkIJSn5pWfa09g6cqFLucCXomNj9NYj8t+JfPn
# a3gC6LHv3OQAUDHOoC5H+8N3ea7qVGYIiwPRHzXOGqG/tVaiU5s5hG3vBhfRX8W1
# /2g4/hpgeXUzrxYn/2c5SOGGy0MU1ZJyUSFEdsjXHEV7HXK4qmFGV9RJxtiLZH1q
# UldCglxcj7zw0QnUdG6oAxpwTCeVp057/WXbnIR8a0gPse+y/new5+CBUGTAvrw6
# K2BrJQVsdIIVn/q+BbcZxh9PpeZfTtsi6lgkvy0bUWtl5sSpd75+hvw4Sl3HAaWZ
# toWN7LPmbDbbVRO2Arv4doh4Chod4wJ5xQIDAQABo4IB2DCCAdQwHwYDVR0jBBgw
# FoAUrjUjF///Bj2cUOCMJGUzHnAQiKIwHQYDVR0OBBYEFF4LEhElVUvT8n5txOJS
# NAczooSAMA4GA1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAAMBMGA1UdJQQMMAoG
# CCsGAQUFBwMDMBEGCWCGSAGG+EIBAQQEAwIEEDBmBgNVHSAEXzBdMFsGDCsGAQQB
# riMBBAMCATBLMEkGCCsGAQUFBwIBFj1odHRwczovL3d3dy5pbmNvbW1vbi5vcmcv
# Y2VydC9yZXBvc2l0b3J5L2Nwc19jb2RlX3NpZ25pbmcucGRmMEkGA1UdHwRCMEAw
# PqA8oDqGOGh0dHA6Ly9jcmwuaW5jb21tb24tcnNhLm9yZy9JbkNvbW1vblJTQUNv
# ZGVTaWduaW5nQ0EuY3JsMH4GCCsGAQUFBwEBBHIwcDBEBggrBgEFBQcwAoY4aHR0
# cDovL2NydC5pbmNvbW1vbi1yc2Eub3JnL0luQ29tbW9uUlNBQ29kZVNpZ25pbmdD
# QS5jcnQwKAYIKwYBBQUHMAGGHGh0dHA6Ly9vY3NwLmluY29tbW9uLXJzYS5vcmcw
# GQYDVR0RBBIwEIEOb2l0bXB0QHVtbi5lZHUwDQYJKoZIhvcNAQELBQADggEBAENR
# lesMKmBaZ0g68lttYEMtaPiz+DaNpOlXBs1gH66aghB1aP6iiRJcFVasGLUVFncd
# G1xbw503LTrBUc5PECMVDVF7KKCfHA1OeFV9vOWyvdVgbe3paDy1sj4CADO2D0gn
# xcGiZoFhEZiBkTvSsj4S3GXZEvoFHJxJLw2kvdLnzy0gH/b/b/yblwA1fKXw4loc
# UpDM6qTwM7SiKgkQ5W7/280EYu8BI6c8rpiJmqM1tZLcpswuavB00T52Y+ZZmz3t
# MMVgFHn9pFFltYr3s3bEek7I6pU8unISbiyQzxqhIUKaBi8hy8LgoY5UnGjX5jHs
# IvINzms+JX5Ity02sL0wggXrMIID06ADAgECAhBl4eLj1d5QRYXzJiSABeLUMA0G
# CSqGSIb3DQEBDQUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMKTmV3IEplcnNl
# eTEUMBIGA1UEBxMLSmVyc2V5IENpdHkxHjAcBgNVBAoTFVRoZSBVU0VSVFJVU1Qg
# TmV0d29yazEuMCwGA1UEAxMlVVNFUlRydXN0IFJTQSBDZXJ0aWZpY2F0aW9uIEF1
# dGhvcml0eTAeFw0xNDA5MTkwMDAwMDBaFw0yNDA5MTgyMzU5NTlaMHwxCzAJBgNV
# BAYTAlVTMQswCQYDVQQIEwJNSTESMBAGA1UEBxMJQW5uIEFyYm9yMRIwEAYDVQQK
# EwlJbnRlcm5ldDIxETAPBgNVBAsTCEluQ29tbW9uMSUwIwYDVQQDExxJbkNvbW1v
# biBSU0EgQ29kZSBTaWduaW5nIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
# CgKCAQEAwKAvix56u2p1rPg+3KO6OSLK86N25L99MCfmutOYMlYjXAaGlw2A6O2i
# gTXrC/Zefqk+aHP9ndRnec6q6mi3GdscdjpZh11emcehsriphHMMzKuHRhxqx+85
# Jb6n3dosNXA2HSIuIDvd4xwOPzSf5X3+VYBbBnyCV4RV8zj78gw2qblessWBRyN9
# EoGgwAEoPgP5OJejrQLyAmj91QGr9dVRTVDTFyJG5XMY4DrkN3dRyJ59UopPgNwm
# ucBMyvxR+hAJEXpXKnPE4CEqbMJUvRw+g/hbqSzx+tt4z9mJmm2j/w2nP35MViPW
# Cb7hpR2LB8W/499Yqu+kr4LLBfgKCQIDAQABo4IBWjCCAVYwHwYDVR0jBBgwFoAU
# U3m/WqorSs9UgOHYm8Cd8rIDZsswHQYDVR0OBBYEFK41Ixf//wY9nFDgjCRlMx5w
# EIiiMA4GA1UdDwEB/wQEAwIBhjASBgNVHRMBAf8ECDAGAQH/AgEAMBMGA1UdJQQM
# MAoGCCsGAQUFBwMDMBEGA1UdIAQKMAgwBgYEVR0gADBQBgNVHR8ESTBHMEWgQ6BB
# hj9odHRwOi8vY3JsLnVzZXJ0cnVzdC5jb20vVVNFUlRydXN0UlNBQ2VydGlmaWNh
# dGlvbkF1dGhvcml0eS5jcmwwdgYIKwYBBQUHAQEEajBoMD8GCCsGAQUFBzAChjNo
# dHRwOi8vY3J0LnVzZXJ0cnVzdC5jb20vVVNFUlRydXN0UlNBQWRkVHJ1c3RDQS5j
# cnQwJQYIKwYBBQUHMAGGGWh0dHA6Ly9vY3NwLnVzZXJ0cnVzdC5jb20wDQYJKoZI
# hvcNAQENBQADggIBAEYstn9qTiVmvZxqpqrQnr0Prk41/PA4J8HHnQTJgjTbhuET
# 98GWjTBEE9I17Xn3V1yTphJXbat5l8EmZN/JXMvDNqJtkyOh26owAmvquMCF1pKi
# QWyuDDllxR9MECp6xF4wnH1Mcs4WeLOrQPy+C5kWE5gg/7K6c9G1VNwLkl/po9OR
# PljxKKeFhPg9+Ti3JzHIxW7LdyljffccWiuNFR51/BJHAZIqUDw3LsrdYWzgg4x0
# 6tgMvOEf0nITelpFTxqVvMtJhnOfZbpdXZQ5o1TspxfTEVOQAsp05HUNCXyhznlV
# Lr0JaNkM7edgk59zmdTbSGdMq8Ztuu6VyrivOlMSPWmay5MjvwTzuNorbwBv0DL+
# 7cyZBp7NYZou+DoGd1lFZN0jU5IsQKgm3+00pnnJ67crdFwfz/8bq3MhTiKOWEb0
# 4FT3OZVp+jzvaChHWLQ8gbCORgClaZq1H3aqI7JeRkWEEEp6Tv4WAVsr/i7LoXU7
# 2gOb8CAzPFqwI4Excdrxp0I4OXbECHlDqU4sTInqwlMwofmxeO4u94196qIqJQl+
# 8Sykl06VktqMux84Iw3ZQLH08J8LaJ+WDUycc4OjY61I7FGxCDkbSQf3npXeRFm0
# IBn8GiW+TRDk6J2XJFLWEtVZmhboFlBLoUlqHUCKu0QOhU/+AEOqnY98j2zRMYIE
# eTCCBHUCAQEwgZAwfDELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAk1JMRIwEAYDVQQH
# EwlBbm4gQXJib3IxEjAQBgNVBAoTCUludGVybmV0MjERMA8GA1UECxMISW5Db21t
# b24xJTAjBgNVBAMTHEluQ29tbW9uIFJTQSBDb2RlIFNpZ25pbmcgQ0ECEHJNXiAT
# 1cKRQFXzfFSJVHEwCQYFKw4DAhoFAKB4MBgGCisGAQQBgjcCAQwxCjAIoAKAAKEC
# gAAwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEOMAwG
# CisGAQQBgjcCARUwIwYJKoZIhvcNAQkEMRYEFENXOSJIt1ZafA0H+H9dRDmbrr3U
# MA0GCSqGSIb3DQEBAQUABIIBAI/FMEsZg4oG2hWR0kqEuaOGYw2fiKHYmtb40AKa
# xAkdEB5sQIK30Efaj8TtHcgld65427nlQHqLJXDwoayGmn/NyTOitwrvNEZa49rK
# XCeRQUrpS6mxvWT2h2x4zY8Y89zxgg9Jma0LntbtU9PjBzEY3z8L4zkwePvXMF8T
# YcrjFT8bn2wiEWdKdF26Bpa66jmPoJyesyTEa+tylcijTLxzj075EzNcYYHNolFH
# zSpH84pqFAp+fH3iJQ128dAE4XSE0WZSxgBwU1Cbl+uDiprvPggojiagZesc8I05
# oYRcUF/Z++iFZdM+kpoD2xaRIekUVN7AzQfhlTOgLdpIl1qhggJDMIICPwYJKoZI
# hvcNAQkGMYICMDCCAiwCAQEwgakwgZUxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJV
# VDEXMBUGA1UEBxMOU2FsdCBMYWtlIENpdHkxHjAcBgNVBAoTFVRoZSBVU0VSVFJV
# U1QgTmV0d29yazEhMB8GA1UECxMYaHR0cDovL3d3dy51c2VydHJ1c3QuY29tMR0w
# GwYDVQQDExRVVE4tVVNFUkZpcnN0LU9iamVjdAIPFojwOSVeY45pFDkH5jMLMAkG
# BSsOAwIaBQCgXTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJ
# BTEPFw0xODAzMDExODEwNTJaMCMGCSqGSIb3DQEJBDEWBBR0Kus5GPNdVzDybR5Y
# ksfRPedJSTANBgkqhkiG9w0BAQEFAASCAQCY9eG21F6FQkMaH7bvqIwL16y18KOo
# HbSLpVD3XBupW27shWxlwrUVxs8KvvBXFQPOsBwVRn3iN+kMD14Y0JXVDipHr1JX
# DS/uZUCYRnvP8JYQj0T4ymA1mEOiFzNbuzG/CCSxrHF/yhMcC/Q0/R1HIkOEJkcc
# o2FKKgFlIX+ejZ5q2mYaB6LioW5QUx0WxIqOkgA1di+klnAw/DuVZ1CloTXqDj3s
# TYVu6iA9gveUEKCNf8bTHhE0CighDOz6lC0wbJUoVicmtoZsQaGNlxxGaExPh/TO
# xv8DrM2MDyUshp1lXkefvznKiSM/pUXYsiJvtFXD1q+qPeh5AD+rjc8D
# SIG # End signature block