#
.SYNOPSIS
Windows Host discovery script (twigs equivalent)
.DESCRIPTION
This script helps discover Windows Host(s) as assets in ThreatWorx instance. It is equivalent to twigs.
.PARAMETER mode
Specifies the discovery mode (local or remote).
.PARAMETER remote_hosts_csv
Specifies path for CSV file containing details of remote hosts to be discovered. Optional.
.PARAMETER host_list
Specifies path for CSV file to be secured. Format is same as remote hosts CSV. Optional.
.PARAMETER password
A password used to encrypt / decrypt login information from the host list / remote hosts CSV file. Optional.
.PARAMETER handle
Specifies the handle of the ThreatWorx user. Mandatory.
.PARAMETER token
Specifies the API token of the ThreatWorx user. Optional.
.PARAMETER instance
Specifies the ThreatWorx instance. Optional.
.PARAMETER out
Specifies the output JSON filename to hold discovered asset details. Optional.
.PARAMETER assetid
Specifies the identifier for the asset. Optional.
.PARAMETER assetname
Specifies the name for the asset. Optional.
.PARAMETER tags
Specify tags for the asset. Optional.
.PARAMETER tag_critical
Tag the asset as critical. Optional. Possible values (true or false). Default is false.
.PARAMETER no_scan
Do not initiate a baseline assessment. Optional. Possible values (true or false). Default is false.
.PARAMETER no_host_benchmark
Do not run host benchmark tests. Optional. Possible values (true or false). Default is false.
.PARAMETER email_report
After impact refresh is complete, email scan report to self. Optional. Possible values (true or false). Default is false.
.EXAMPLE
.\twigs.ps1 -handle someuser@company.com -token XXXX -instance ACME.threatworx.io -out asset.json -assetid myassetid -assetname myassetname -tag_critical true -tags 'tag1','tag2' -email_report true
.\twigs.ps1 -mode remote -remote_hosts_csv my_remote_hosts.csv -handle someuser@company.com -token XXXX -instance ACME.threatworx.io
.NOTES
.
#>
# Sample PowerShell based discovery script for Windows
param(
[parameter(Mandatory=$false, HelpMessage='Local or Remote Windows host discovery')]
[ValidateSet('local','remote')]
[String]
$mode='local',
[parameter(Mandatory=$false, HelpMessage='Specify path for CSV file containing details of remote hosts to be discovered')]
[String]
$remote_hosts_csv,
[parameter(Mandatory=$false, HelpMessage='Specifies path for CSV file to be secured. Format is same as remote hosts CSV.')]
[String]
$host_list,
[parameter(Mandatory=$false, HelpMessage='A password used to encrypt / decrypt login information from the host list / remote hosts CSV file.')]
[String]
$password,
[parameter(Mandatory=$true, HelpMessage='Enter the email handle for ThreatWorx instance')]
[String]
$handle,
[parameter(Mandatory=$false, HelpMessage='Enter the API key for the specified email handle for ThreatWorx instance')]
[String]
$token,
[parameter(Mandatory=$false, HelpMessage='Specify the ThreatWorx instance')]
[String]
$instance,
[parameter(Mandatory=$false, HelpMessage='Specify the output JSON filename')]
[String]
$out,
[parameter(Mandatory=$false, HelpMessage='Enter the Asset ID')]
[String]
$assetid,
[parameter(Mandatory=$false, HelpMessage='Enter the Asset Name')]
[String]
$assetname,
[parameter(Mandatory=$false, HelpMessage='Specify tags for the asset')]
[String[]]
$tags,
[parameter(Mandatory=$false, HelpMessage='Tag the asset as critical. Possible values (true or false). Default is false')]
[ValidateSet('true','false')]
[String]
$tag_critical='false',
[parameter(Mandatory=$false, HelpMessage='Do not initiate a baseline assessment. Possible values (true or false). Default is false')]
[ValidateSet('true','false')]
[String]
$no_scan='false',
[parameter(Mandatory=$false, HelpMessage='Do not run host benchmark tests. Possible values (true or false). Default is false')]
[ValidateSet('true','false')]
[String]
$no_host_benchmark='false',
[parameter(Mandatory=$false, HelpMessage='After impact refresh is complete email scan report to self. Possible values (true or false). Default is false')]
[ValidateSet('true','false')]
[String]
$email_report='false'
)
function ql { $Args }
function GetLastIpAddress {
param ([string]$cidr )
$bits = ql 0 128 192 224 240 248 252 254
$net = $cidr.Split("/")
$sn = $net[0]
$octets = $sn.Split(".")
[int]$mask = $net[1]
$activ = $mask % 8
$actval = $bits[$activ]
$fulloctets = [System.Math]::Truncate($mask / 8)
$ao = [int]$octets[$fulloctets]
$mn = 256 - $actval
$x = [System.Math]::Truncate($ao / $mn)
$num = $x * $mn
## calculate active part of broadcast address
$bd = $num + $mn -1
switch ($fulloctets)
{
1
{
$fixed = $octets[0]
$subnet = $fixed + "." + $num.ToString() + ".0.0"
$strmask = "255." + $actval.ToString() + ".0.0"
$broadcast = $fixed + "." + $bd.ToString() + ".255.255"
break
}
2
{
$fixed = $octets[0]+"."+$octets[1]
$subnet = $fixed + "." + $num.ToString() + ".0"
$strmask = "255.255." + $actval.ToString() + ".0"
$broadcast = $fixed + "." + $bd.ToString() + ".255"
break
}
3
{
$fixed = $octets[0]+"."+$octets[1]+"."+$octets[2]
$subnet = $fixed + "." + $num.ToString()
$strmask = "255.255.255." + $actval.ToString()
$broadcast = $fixed + "." + $bd.ToString()
break
}
}
$snoct = $subnet.Split(".")
$snoct[3] = ([int]$snoct[3] + 1).ToString()
$fip = $snoct[0]+"."+$snoct[1]+"."+$snoct[2]+"."+$snoct[3]
$bdoct = $broadcast.Split(".")
$bdoct[3] = ([int]$bdoct[3] - 1).ToString()
$lip = $bdoct[0]+"."+$bdoct[1]+"."+$bdoct[2]+"."+$bdoct[3]
return $broadcast
}
function GetNextIpAddress {
param ([string]$address )
$a = [System.Net.IpAddress]::Parse($address) ## turn the string to IP address
$z = $a.GetAddressBytes() ## and then to an array of bytes
if ($z[3] -eq 255) ## last octet full
{
$z[3] = 0 ## so reset
if ($z[2] -eq 255) ## third octet full
{
$z[2] = 0 ## so reset
$z[1] += 1 ## increment second octet
}
else
{
$z[2] += 1 ## increment third octect
}
}
else
{
$z[3] += 1 ## increment last octet
}
$c = [System.Net.IpAddress]($z) ## recreate IP address
return $c.ToString()
}
function ExpandCidr {
param ([string]$cidr )
$broken=@()
$lastIP = GetLastIpAddress($cidr)
$curradd = $cidr.Split("/")[0]
do {
$addr = GetNextIpAddress($curradd)
#Write-Host $addr
#$ipobj = Add-OneIP $addr
if ($broken -notcontains $addr) {$broken += $addr}
$curradd = $addr
} until ($addr -eq $lastIP)
return $broken
}
function Invoke-RemoteDiscovery {
if (!$remote_hosts_csv -and !$host_list) {
Write-Host "Error missing remote_hosts_csv and host_list argument. Either one must be specified for remote discovery"
exit
}
if ($host_list) {
Write-Host "Securing host list CSV file"
if (!$password) {
$password1 = Read-Host "Enter password: " -AsSecureString
$password2 = Read-Host "Re-enter password: " -AsSecureString
$raw_pwd1 = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($password1))
$raw_pwd2 = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($password2))
if ($raw_pwd1 -ne $raw_pwd2) {
Write-Host "Passwords don't match. Try again."
Exit
}
$password = $raw_pwd1
}
while ($password.length -lt 16) {
$password = $password + $password
}
$password = $password.subString(0,16)
$password = $password | ConvertTo-SecureString -AsPlainText -Force
$hosts = Import-Csv $host_list
foreach ($thost in $hosts) {
if (!$thost.userpwd.StartsWith('__SECURE__:')) {
$host_password = $thost.userpwd | ConvertTo-SecureString -AsPlainText -Force
$enc_pwd = $host_password | ConvertFrom-SecureString -SecureKey $password
$thost.userpwd = "__SECURE__:" + $enc_pwd
}
}
$hosts | Export-Csv $host_list -NoTypeInformation
Write-Host "Host list CSV file secured"
Exit
}
Write-Host "Reading remote hosts CSV file..."
$remote_hosts = Import-Csv $remote_hosts_csv
Write-Host "Starting remote Windows host discovery..."
$secure_password = $null
foreach ($remote_host in $remote_hosts) {
$remotehost = $remote_host.hostname
if ($remote_host.userpwd.StartsWith("__SECURE__:")) {
if (!$secured_password) {
if (!$password) {
$password1 = Read-Host "Enter password: " -AsSecureString
$raw_password = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($password1))
}
else {
$raw_password = $password
}
while ($raw_password.length -lt 16) {
$raw_password = $raw_password + $raw_password
}
$raw_password = $raw_password.subString(0,16)
$secured_password = $raw_password | ConvertTo-SecureString -AsPlainText -Force
}
$etext = $remote_host.userpwd.subString(11, $remote_host.userpwd.Length - 11)
try {
$dtext = $etext | ConvertTo-SecureString -SecureKey $secured_password -ErrorAction Stop
$userPassword = $dtext
}
catch {
Write-Host "Decryption failed, possibly due to incorrect password..."
Exit
}
}
else {
$userPassword = $remote_host.userpwd | ConvertTo-SecureString -AsPlainText -Force
}
$logincredentials = New-Object System.Management.Automation.PSCredential -ArgumentList $remote_host.userlogin, $userPassword
if ($remotehost.indexof('/') -ne -1) {
$remotehosts = ExpandCidr($remotehost)
}
else {
$remotehosts = @()
$remotehosts += $remotehost
}
foreach ($remotehost in $remotehosts) {
Write-Host ''
Write-Host ''
Write-Host "Running remote discovery for: ",$remotehost
$remoteSession = New-PSSession -ComputerName $remotehost -Credential $logincredentials
if ($remoteSession -eq $null) {
Write-Host "Connecting to remote host failed....skipping it"
continue
}
$remote_folder = Invoke-Command -Session $remoteSession -ScriptBlock { $File = New-TemporaryFile; Remove-Item $File -Force; New-Item -Itemtype Directory -Path "$($ENV:Temp)\$($File.Name)"; }
if ($PSScriptRoot) {
Copy-Item $PSScriptRoot -Destination $remote_folder -ToSession $remoteSession -Recurse
$remote_twigs_folder = $remote_folder.ToString() + "\twigs_PS"
$remotescript = ".\twigs.ps1"
}
else {
$FileLocation = Split-Path (Convert-Path -LiteralPath ([Environment]::GetCommandLineArgs()[0]))
Copy-Item $FileLocation -Destination $remote_folder -ToSession $remoteSession -Recurse
$remote_twigs_folder = $remote_folder.ToString() + "\twigs_EXE"
$remotescript = ".\twigs.exe"
}
Invoke-Command -Session $remoteSession { Set-Location $using:remote_twigs_folder }
Invoke-Command -Session $remoteSession -ScriptBlock { & $using:remotescript -mode 'local' -handle $using:handle -token $using:token -instance $using:instance -tags $using:tags -tag_critical $using:tag_critical -no_scan $using:no_scan -no_host_benchmark $using:no_host_benchmark -email_report $using:email_report}
Invoke-Command -Session $remoteSession { Set-Location "..\..\" }
Invoke-Command -Session $remoteSession { Remove-Item $using:remote_folder -Recurse }
Remove-PSSession $remoteSession
Write-Host "Completed remote discovery for: ",$remotehost
}
}
Write-Host "Completed remote Windows host discovery."
}
function Invoke-LocalDiscovery {
if (!$token -and !$instance -and !$out) {
Write-Host "Error missing token, instance and out arguments....nothing to do!"
exit
}
if ($no_scan -eq "true" -and $email_report -eq "true") {
Write-Host "Error conflicting options [no_scan] and [email_report] are specified!"
exit
}
if (!$assetid) {
$assetid = $env:ComputerName
}
if (!$assetname) {
$assetname = $assetid
}
$assetid = $assetid.Replace("/","-")
$assetid = $assetid.Replace(":","-")
$assetname = $assetname.Replace("/","-")
$assetname = $assetname.Replace(":","-")
Write-Host 'Running discovery for Windows asset: ', $assetname
Write-Host ''
$tw_assets_url = 'https://' + $instance+ '/api/v2/assets/'
$tw_scan_url = 'https://' + $instance+ '/api/v1/scans/'
# Check if asset exists
$asset_exists = 1
if ($token -and $instance) {
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$url = $tw_assets_url + $assetid + '/?handle=' + $handle + '&token=' + $token + '&format=json'
$http_method = 'Get'
Write-Host 'Validating ThreatWorx credentials...'
try {
$response = Invoke-RestMethod -Method $http_method -Uri $url -ContentType 'application/json'
}
catch {
if($_.Exception.Response.StatusCode.value__ -eq 404) {
$asset_exists = 0
}
else {
Write-Host 'Encountered fatal error (details below)'
Write-Host "$_"
Write-Host 'Exiting...'
exit
}
}
Write-Host 'ThreatWorx credentials validated.'
}
Write-Host ''
Write-Host 'Extracting OS details...'
$computer_info = Get-ComputerInfo
$base_os = $computer_info.OsName
Write-Host "OS:", $base_os
$os_version = $null
$os_version_ubr = $null
$os_release_id = $null
$os_arch = $null
$os_version = $computer_info.OsVersion
$os_version_ubr = (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion").UBR
if ($os_version_ubr) {
$os_version_tokens = $os_version.ToString().Split(' ')
$os_version = $os_version_tokens[0] + '.' + $os_version_ubr
for ($i=1; $i -lt $os_version_tokens.Length; $i++)
{
$os_version = $os_version + ' ' + $os_version_tokens[$i]
}
}
$temp_str = (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion").ReleaseId
if ($temp_str) {
$os_release_id = $temp_str.ToString().Trim()
}
$mc_arch = $computer_info.CsSystemType
$bit_arch = $computer_info.OsArchitecture
$os_arch = $bit_arch + ' ' + $mc_arch
$os_arch = $os_arch.Trim()
Write-Host ''
Write-Host 'Extracting patch information...'
$hot_fixes = Get-HotFix
$patch_json_array = New-Object System.Collections.Generic.List[System.Object]
foreach ($hot_fix in $hot_fixes) { $patch_entry_json = @{id=$hot_fix.HotFixID}; $patch_json_array.add($patch_entry_json)}
Write-Host 'Number of patches found:', $patch_json_array.Count
$patch_json = $patch_json_array | ConvertTo-Json
$patch_json_str = $patch_json.ToString()
Write-Host ''
Write-Host 'Extracting products (using registry key)...'
$unique_products = New-Object System.Collections.Generic.List[string]
$product_json_array = New-Object System.Collections.Generic.List[string]
$temp_array = Get-ItemProperty HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* | Select-Object Publisher, DisplayName, DisplayVersion
$temp_array | foreach { $var = $_ ; $product = $var.'DisplayName'; $vendor = $var.'Publisher'; $version = $var.'DisplayVersion'; if ($product -and $version) { $product_details = $product.Trim() + ' ' + $version.Trim(); $product_json_array.add($product_details); }}
$temp_products_count = $temp_array.Length
$temp_array = Get-ItemProperty HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\* | Select-Object Publisher, DisplayName, DisplayVersion
$temp_array | foreach { $var = $_ ; $product = $var.'DisplayName'; $vendor = $var.'Publisher'; $version = $var.'DisplayVersion'; if ($product -and $version) { $product_details = $product.Trim() + ' ' + $version.Trim(); $product_json_array.add($product_details); }}
$ie_version = Get-ItemProperty "HKLM:\Software\Microsoft\Internet Explorer" | Select-Object svcVersion, Version
if ($ie_version.svcVersion -ne $null) {
$product_json_array.add('Internet Explorer ' + $ie_version.svcVersion)
$temp_products_count = $temp_products_count + 1
}
else {
if ($ie_version.Version -ne $null) {
$product_json_array.add('Internet Explorer ' + $ie_version.Version)
$temp_products_count = $temp_products_count + 1
}
}
$temp_products_count = $temp_products_count + $temp_array.Length
Write-Host 'Number of products found (using registry key):', $temp_products_count
Write-Host 'Number of products identified till now:', $product_json_array.Count
Write-Host ''
Write-Host 'Extracting products (using WMI)...'
$temp_array = Get-WMIObject -ClassName Win32_Product
foreach ($row in $temp_array) { $product_details = $row.Name.Trim() + ' ' + $row.Version.Trim(); if ($product_json_array -notcontains $product_details) { $product_json_array.Add($product_details)} }
Write-Host 'Number of products found (using wmic):', $temp_array.Count
Write-Host 'Total number of unique products found:', $product_json_array.Count
$tags_json_array = New-Object System.Collections.Generic.List[string]
$tags_json_array.Add($base_os)
$tags_json_array.Add('OS_RELEASE:' + $base_os)
if ($os_version) {
$tags_json_array.Add('OS_VERSION:' + $os_version)
}
if ($os_release_id) {
$tags_json_array.Add('OS_RELEASE_ID:' + $os_release_id)
}
if ($os_arch) {
$tags_json_array.Add('OS_ARCH:' + $os_arch)
}
$tags_json_array.Add('Windows')
if ($tag_critical -eq "true") {
$tags_json_array.Add('CRITICALITY:5')
}
if ($tags) {
foreach($tag in $tags) {
# Special handling for twigs.exe which does not allow multiple tags (as it is requires array of strings)
$temp_tags = $tag.Split(',')
foreach($temp_tag in $temp_tags) {
$tags_json_array.Add($temp_tag)
}
}
}
$misconfigs_json_array = New-Object System.Collections.Generic.List[System.Object]
# Run host benchmark if specified
if ($no_host_benchmark -eq "false") {
$FileLocation =
if ($PSScriptRoot) { # running as .ps1 file
$PSScriptRoot
}
else { # running as .exe
Split-Path (Convert-Path -LiteralPath ([Environment]::GetCommandLineArgs()[0]))
}
Write-Host "Running host benchmarks. This may take some time..."
$hbm_csv_rpt = $FileLocation + '\twigs_hbm.csv'
if (Test-Path $hbm_csv_rpt) { Remove-Item $hbm_csv_rpt }
# If twigs.exe is running, then no need to source HardeningKitty as it is already included
if ($PSScriptRoot) {
# If twigs.ps1 is running, then import HardeningKitty
$hk_script = $FileLocation + '\HardeningKitty.psm1'
Unblock-File $hk_script
Import-Module $hk_script
}
Invoke-HardeningKitty -Mode Audit -Report -ReportFile $hbm_csv_rpt
$misconfigs = Import-Csv $hbm_csv_rpt
$hk_md_file = $FileLocation + "\hk_metadata.json"
$hk_md_json = Get-Content -Path $hk_md_file -Raw -Encoding UTF8
$hk_md = $hk_md_json | ConvertFrom-Json
foreach ($misconfig in $misconfigs) {
if ($misconfig.Severity -ne 'Passed') {
$misconfig_ID = $misconfig.ID
$hk_md_entry = $hk_md.$misconfig_ID
$tmp_title = '[' + $hk_md_entry."category" + '] ' + $misconfig.Name
$mc_rating = $hk_md_entry."rating"
if ($misconfig.Result -eq '') { $cv = 'Not available' }
else { $cv = $misconfig.Result }
$details_msg = "Current value is [" + $cv + "] and recommended value is [" + $misconfig.Recommended + "].
"
$details_msg = $details_msg + $hk_md_entry."description"
$misconfig_entry_json = @{asset_id=$assetid;twc_id=$misconfig.ID;twc_title=$tmp_title;type='Host Benchmark';details=$details_msg;rating=$mc_rating;object_id='';object_meta=''}
$misconfigs_json_array.add($misconfig_entry_json)
}
}
if (Test-Path $hbm_csv_rpt) { Remove-Item $hbm_csv_rpt }
}
$url = ''
$http_method = ''
if ($asset_exists -eq 0) {
# If asset does not exist, then create one
$http_method = 'Post'
$url = $tw_assets_url + '?handle=' + $handle + '&token=' + $token + '&format=json'
}
else {
# If asset exists, then update it
$http_method = 'Put'
$url = $tw_assets_url + $assetid + '/?handle=' + $handle + '&token=' + $token + '&format=json'
}
$current_ts = [long](Get-Date (Get-Date).ToUniversalTime() -UFormat %s)
$payload = @{
id=$assetid
name=$assetname
type='Windows'
attack_surface_label='Corporate::Server::Windows'
timestamp=$current_ts
description=''
owner=$handle
patches=$patch_json_array
products=$product_json_array
config_issues=$misconfigs_json_array
tags=$tags_json_array
}
$body = (ConvertTo-Json -Depth 100 $payload)
# Remove any non-ascii characters
$body = $body -replace '[^ -~]', ''
$scan_type = $null
if ($token -and $instance) {
Write-Host ''
if ($asset_exists -eq 0) {
Write-Host 'Creating asset...'
}
else {
Write-Host 'Updating asset...'
}
$response = Invoke-RestMethod -Method $http_method -Uri $url -ContentType 'application/json' -Body $body
if ($asset_exists -eq 0) {
Write-Host 'Successfully created asset'
}
else {
Write-Host 'Successfully updated asset'
if ($no_scan -eq "false") {
if ($response.status -Match 'No product updates') {
Write-Host 'Asset products are not updated. No need for impact refresh.'
$no_scan = "true"
}
else {
if ($response.status -Match 'Full scan needed') {
$scan_type = 'F'
}
else {
$scan_type = 'Q'
}
}
}
}
if ($no_scan -eq "false") {
$http_method = 'Post'
$url = $tw_scan_url + '?handle=' + $handle + '&token=' + $token + '&format=json'
$assets_array = New-Object System.Collections.Generic.List[string]
$assets_array.Add($assetid)
$payload = @{
assets=$assets_array
}
if ($email_report -eq "true") {
$payload["mode"] = "email"
}
if ($scan_type -eq 'F') {
$payload["scan_type"] = "full"
Write-Host "Starting full impact refresh..."
}
else {
Write-Host "Starting incremental impact refresh..."
}
$temp_body = (ConvertTo-Json -Depth 100 $payload)
$response = Invoke-RestMethod -Method $http_method -Uri $url -ContentType 'application/json' -Body $temp_body
Write-Host 'Started impact refresh.'
}
}
if ($out) {
$temp_body = ($body | ConvertFrom-Json)
$meta = @{
generated_by=$handle
generated_on=$current_ts
tool_name='twigs-ps'
tool_version='1.0.0'
}
$final_body = @{
assets=@($temp_body)
meta=$meta
}
ConvertTo-Json -Depth 100 $final_body | Out-File $out
}
}
$requiredVersion = New-Object System.Version "5.1"
if ($PSVersionTable) {
$requiredVersion = New-Object System.Version "5.1"
if ($PSVersionTable.PSVersion -lt $requiredVersion) {
Write-Host 'Your PowerShell version is:', $PSVersionTable.PSVersion
Write-Host 'This script requires PowerShell version 5.1 and higher...exiting'
exit
}
}
else {
Write-Host "Unable to detect your PowerShell version...exiting"
exit
}
if ($mode -eq "remote") {
Invoke-RemoteDiscovery
}
else {
Invoke-LocalDiscovery
}
# SIG # Begin signature block
# MIIG6AYJKoZIhvcNAQcCoIIG2TCCBtUCAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB
# gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR
# AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUi7oiw2ilKlbMIpnSGTmp2SgY
# uxCgggQKMIIEBjCCAu6gAwIBAgIBATANBgkqhkiG9w0BAQsFADCBoDETMBEGA1UE
# AwwKVGhyZWF0V29yeDEYMBYGA1UECgwPVGhyZWF0V2F0Y2ggSW5jMRQwEgYDVQQL
# DAtFbmdpbmVlcmluZzETMBEGA1UECAwKQ2FsaWZvcm5pYTELMAkGA1UEBhMCVVMx
# EjAQBgNVBAcMCUxvcyBHYXRvczEjMCEGCSqGSIb3DQEJARYUcGFyZXNoQHRocmVh
# dHdvcnguaW8wHhcNMjQwNTA3MTUwNjQ0WhcNMjcwNTA3MTUwNjQ0WjCBoDETMBEG
# A1UEAwwKVGhyZWF0V29yeDEYMBYGA1UECgwPVGhyZWF0V2F0Y2ggSW5jMRQwEgYD
# VQQLDAtFbmdpbmVlcmluZzETMBEGA1UECAwKQ2FsaWZvcm5pYTELMAkGA1UEBhMC
# VVMxEjAQBgNVBAcMCUxvcyBHYXRvczEjMCEGCSqGSIb3DQEJARYUcGFyZXNoQHRo
# cmVhdHdvcnguaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCakSBa
# fxI5dlEt8fOssCOA/0D0N2OSS8vyzbjkLK1ZHAqUawV1+PLqiksc0I+C5OMq8dup
# QbdF6V3NO59TJ4h5DEy1uNBKfYyOZecZ/yZ+/uCcEeaV10kLBkR78D8MRUQoUcqP
# W7jgUzji75dX4+yXEB15zKdjWtoWldxzJ5O1QLmLGfiHgVRgG9AOP/cIMQN3n4sw
# bAwNKzDHTS+vJFcfOWypxsUjRHTouIVZYovMuUmCat+0QFv4yRwS463ISXW9js5q
# FEh2mGKiTCni8da5j0qKKlhjk/xi/vpolmGlwnX6LexrFk9olZdbqufAV3qxskDH
# N7niXpGegbD7NddBAgMBAAGjSTBHMA4GA1UdDwEB/wQEAwIHgDAWBgNVHSUBAf8E
# DDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUyo+KxnxIhm7IgQEKN44vFHfJ6jEwDQYJ
# KoZIhvcNAQELBQADggEBADDIL/zYH7rX1COCDneTEiZ0rEaaCjYeWqAl+bHdYG3i
# bjrwyshp8Z7McXJaKiFPrUHbw2NgdiijlaEoz/SR5JyEcwe5P++er9dPTWt+82hM
# OCs7BOJ/QuqZLhyWFlTJfTDbt0Dx5Ytc5OrUBQoGuwGpRT1LS1ZmqmCnhFnTBVYq
# 35B1qYOTgnE+9TyBc8FVuPKgEiint4fpDU9FYN0CRIZa6cydJ0nFK27jjK3mRRb5
# X7ecHhwhwXRTpEyAk4j/BUoSWIOywD1abInR38B5Gf6dSVu7Ohyj2Z18R8TfS11q
# 6YfNf+L4tirN/qcfzymea2swkxE0lFjKyIxw2sZYyFExggJIMIICRAIBATCBpjCB
# oDETMBEGA1UEAwwKVGhyZWF0V29yeDEYMBYGA1UECgwPVGhyZWF0V2F0Y2ggSW5j
# MRQwEgYDVQQLDAtFbmdpbmVlcmluZzETMBEGA1UECAwKQ2FsaWZvcm5pYTELMAkG
# A1UEBhMCVVMxEjAQBgNVBAcMCUxvcyBHYXRvczEjMCEGCSqGSIb3DQEJARYUcGFy
# ZXNoQHRocmVhdHdvcnguaW8CAQEwCQYFKw4DAhoFAKB4MBgGCisGAQQBgjcCAQwx
# CjAIoAKAAKECgAAwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGC
# NwIBCzEOMAwGCisGAQQBgjcCARUwIwYJKoZIhvcNAQkEMRYEFNTccAa0IK1MevLM
# 4cv//yj2yYwFMA0GCSqGSIb3DQEBAQUABIIBAFsHhxj74whC6lMJd1LtwUC0lGGJ
# F0JeH93zioExUFDqZ9dvhZHCCCRYPjViHSovHSCUbEcDMEpvcHYjBWZ2zUiq/IR2
# 2Oetl/pEbKtMn4wtdEthL1XEsavN5htlr4stzaDK2X+HbKmfESMCIW7qcVoqLQUu
# NNFIKzFqszomXd5boye1S2I6Y3Qksd+HeAtuXwgipCpEPZEwQGxWEDjEQQTBDtfR
# uLyxYh8jhe1H48kRubywdYXg8E2plgpEh35wMkXLNMQ9Bguk8/9vAm/vEU5pOgn1
# rULRuazV1+80dMkIbqnq5a3N7wOaJXMzd3Q1Y3FvaBv+UNdHndTESexvhtw=
# SIG # End signature block