##################################################################### # PsFCIV.ps1 # Version 0.85 # # File Checksum Integrity Verifier - PowerShell compatible version # # Note: requires PowerShell V2, not compatible with PowerShell 1.0 # Reference links: # http://www.sysadmins.lv/PermaLink,guid,22a8186c-615b-41fa-8062-d74e1a2a28e0.aspx # http://www.sysadmins.lv/PermaLink,guid,29898a6c-aceb-4738-82d8-318bc89272cc.aspx # # Vadims Podans (c) 2009 # http://www.sysadmins.lv/ ##################################################################### #requires -Version 2.0 param ( # объявляем нужные переменные [string]$path = $(throw "You must specify target folder!"), [string]$xml = $(throw "You must specify XML traget path"), # переменная $Include может проверить либо все файлы, либо один конкретный файл [string]$Include = "*.*", # этим параметром задаём уровень вывода Brief или Full [string]$Verbose, # ключом Force мы можем задавать действие для проблемных файлов: переименовать или удалить [string]$Force, # переменная $show должна содержать имена всех файлов, разбитых по категориям, чтобы # иметь возможность посмотреть, например, пропущенные, удалённые, добавленные, # с нессоответствющим хешем файлы [string[]]$show, # проверяем либо текущую папку, либо подпапки тоже [switch]$Recurse, # задаёт особый режим работы, когда файл БД очищается от устаревших записей и в неё # добавляются новые файлы, которые были созданы после создания БД [switch]$Rebuild, [switch]$sha1, [switch]$md5 ) # первым делом проверяем, что указан хотя бы один алгоритм хеширования. Если не указано # ни одного, то начинаем очень ругаться и останавливаем работу скрипта if (!$sha1 -and !$md5) { throw "You must specify at least one hash algorythm." } # резервируем текущий путь, чтобы на него вернуться после окончания работы $oldpath = $pwd.Path # проверяем, что нам задали валидный путь для проверки. Если путь существует, то # устанавливаем текущий путь на $path, в противном случае начинаем ругаться if (Test-Path -LiteralPath $path) { Set-Location -LiteralPath $path } else { throw "Specified path is invalid. Try again!" } # добавлены в отладочных целях $sfile = $null $sum = $new = @() # создаём объект статистики, который в каждом свойстве будет хранить файлы # соответствующей категории $stats = "" | select "Total", "New", "Ok", "Bad", "Miss" # поскольку в свойствах будут объекты, то их объявляем массивами $stats | gm -MemberType NoteProperty | %{$stats.$($_.name) = @()} $stats | Add-Member NoteProperty Del ([int]0) # функция враппер командлета Get-ChildItem function dirx ([string]$Filter, [string[]]$Exclude, $Recurse) { dir @PsBoundParameters | ?{!$_.psiscontainer} } # основная функция подсчёта файлов. На выходе даст массив байтов, который мы преобразуем # в base64 строку function _hashbytes_ ($type, $file) { switch ($type) { "sha1" {$hasher = [System.Security.Cryptography.SHA1]::Create()} "md5" {$hasher = [System.Security.Cryptography.MD5]::Create()} } $inputStream = New-Object System.IO.StreamReader ($file) $hashBytes = $hasher.ComputeHash($inputStream.BaseStream) $inputStream.Close() $hashBytes } function _toxml_ { # в Begin создаём заголовок XML и открывающийся тег Begin { $xmlstring = New-Object System.Text.StringBuilder [void]$xmlstring.Append("`n `n") } # в Process будут по одному поступать объекты, которые описывают файл. Process { # для каждого файла у нас будет один тег [void]$xmlstring.Append(" `n") # чтобы вручную не создавать и не заполнять вложенные теги мы простым foreach # перечисляем теги, какие у нас будут в XML и за счёт переменных автоматом # создаём их в XML и заполняем их данными. Для этого свойства объектов должны # называться так же, как и теги. foreach ($child in ("name", "Size", "TimeStamp", "SHA1", "MD5")) { [void]$xmlstring.Append(" <$child>$($_.$child)`n") } # когда текущий объект обработан, закрываем тег и ждём следующий объект файла [void]$xmlstring.Append(" `n") } # когда объекты закончились, закрываем первый тег и подаём полученный XML дальше End { [void]$xmlstring.Append("`n") [string]$xmlstring.ToString() } } function _fromxml_ ($xml) { # читаем XML файл и собираем из него массив объектов $sum = ([xml]$(gc -LiteralPath $xml)).FCIV.FILE_ENTRY | %{$_ | Select Name, Size, TimeStamp, SHA1, MD5} $sum } function _makeobject_ ($sha1, $md5, $filename, $file) { # вот тут собственно происходит формирование объекта, который будет описывать конкретный файл $object = "" | Select Name, Size, TimeStamp, SHA1, MD5 $object.name = [Security.SecurityElement]::Escape($filename) $object.Size = $file.length $object.TimeStamp = ($file.lastwritetime).ToString() # здесь автоматом считаем нужные хеши и на лету конвертируем их в base64 строку if ($sha1) {$object.SHA1 = [System.Convert]::ToBase64String($(_hashbytes_ "sha1" $(Join-Path $pwd.providerpath $filename)))} if ($md5) {$object.MD5 = [System.Convert]::ToBase64String($(_hashbytes_ "md5" $(Join-Path $pwd.providerpath $filename)))} $object } # простая функция, которая будет подсчитывать хеш для проверямого файла и собирать его # в hex-строку вида B926D7416E8235E6F94F756E9F3AE2F33A92B2C4. function _precheck_ ($sha1, $md5, $filename) { if ($sha1) { -join ($(_hashbytes_ "sha1" $(Join-Path $pwd.providerpath $filename)) | %{"{0:X2}" -f $_}) } elseif ($md5) { -join ($(_hashbytes_ "md5" $(Join-Path $pwd.providerpath $filename)) | %{"{0:X2}" -f $_}) } } # стержень скрипта - основная рутинная проверка соответствия файла записям в XML файле function _checkfiles_ ($filename, $hash, $sum, $Verbose, $force) { # из полученных аргументов берём имя, собираем путь и убеждаемся, что такой файл существует if (Test-Path -LiteralPath $(Join-Path $pwd.providerpath $filename)) { # через Get-Item выбираем объект файла и снова выбираем запись из XML файла для сравнения $current = $sum | ?{$_.name -eq $filename} $file = gi -LiteralPath $filename if ($current) { # если соответствующая запись найдена, то сравниваем размер и дату/время последней модификации # файла с записью в XML файле if (($file.length -eq $current.size) -and ($file.lastwritetime.ToString() -eq $current.TimeStamp)) { # если эти параметры совпали, то проверяем хеш. При этом убеждаемся, что в XML записи # хранится тот тип хеша, который указан в аргументах вызова скрипта if ($current.SHA1 -and $sha1) { # если совпадение есть то конвертируем base64 строку в hex строку $ActualHash = -join ([system.convert]::FromBase64String($current.SHA1) | %{"{0:X2}" -f $_}) } elseif ($current.MD5 -and $md5) { $ActualHash = -join ([system.convert]::FromBase64String($current.MD5) | %{"{0:X2}" -f $_}) } else { # если совпадений не обнаружено, т.е. в аргументах скрипта указан -sha1 ключ, а # XML запись хранит только MD5 хеш, то мы выдаём ошибку, что для данного файла # нету сохранённого подходящего хеша Write-Warning "Cannot bind -sha1 or -md5 switch to containing hash for file: $file.name" return } # если же у нас есть нужные записи, то сверяем посчитанный хеш с записанным в XML хешем if ($ActualHash -eq $hash) { # в зависимости от указанного уровня вывода мы можем выводить информацию на экран # и добавляем в счётчики сведения о проверке данного файла if ($Verbose) {Write-Host $filename - ok -ForegroundColor Green} $script:stats.Ok += $filename $script:stats.Total += $filename return } else { # если размер и дата последней модификации файла и данные из XML записи совпадают, # но не совпал хеш, то пополняем нужный счётчик сведениями о проверке и выводим # на экран информацию, что хеш не совпал. При этом в зависимости от уровня вывода на экран # (ключ -Verbose) мы можем просто информировать или выводить актуальный хеш файла # и тот хеш, что записан в XML для данного файла $script:stats.Bad += $filename Write-Host $filename - bad hash -ForegroundColor Red switch ($Verbose) { "full" {Write-Host $filename - bad hash -ForegroundColor Red Write-Host "Expected hash: " $hash -ForegroundColor Green Write-Host "Actual hash: " $ActualHash -ForegroundColor Yellow} } # мы можем задать на уровне скрипта что делать с такими файлами. Действует только # при указании ключа -Force. Мы можем переименовывать такие файлы или удалять автоматически switch ($force) { "Rename" {Ren $filename $($filename + ".bad")} "Delete" {Del $filename -Force} } } } else { # если проверка на размер или дату/время модификации файла, то выводим сообщение на экран # и пополняем счётчик $script:stats.Bad += $filename Write-Host File $file.name size or Modified Date/Time mismatch! -ForegroundColor Red } } } else { # если для соответствующей записи в XML не найден реальный файл, то так же выводим сообщение, # что файл удалён (хотя он может быть просто переименован) if ($verbose) {Write-Host $filename - is missing -ForegroundColor Yellow} $script:stats.Miss += $filename $script:sum return } # подбиваем статистику и переходим к следующей итерации проверки файла $script:stats.Total += $filename $script:sum } function stats ($sum, $xml, $stats) { # если указан ключ -Show, то мы можем посмотреть список файлов, которые подпали # под одну из категорий (Total, New, Ok, Bad, Miss. К сожалению пока можно за раз # указывать только какую-то одну категорию if ($show) { $stats | select $show | ft -AutoSize } # ну и сама итоговая статистка работы скрипта в цифрах Write-Host ---------------------------------- -ForegroundColor Green if ($Rebuild) { Write-Host Total entries processed: $stats.Total.Count -ForegroundColor Cyan } else {Write-Host Total files processed: $stats.Total.Count -ForegroundColor Cyan} Write-Host Total new added files: $stats.New.Count -ForegroundColor Green Write-Host Total removed unused files: $stats.Del -ForegroundColor Yellow Write-Host Total good files: $stats.Ok.Count -ForegroundColor Green Write-Host Total bad files: $stats.Bad.Count -ForegroundColor Red Write-Host Total missing files: $stats.Miss.Count -ForegroundColor Yellow Write-Host ---------------------------------- -ForegroundColor Green # возвращаемся на исходный путь Set-Location -LiteralPath $oldpath } # для освежения файла БД, т.е. удаления устаревших файлов и добавления записей для новых # файлов в папках, которые не отражены в БД, введён специальный режим Rebuild, который # включается по соответствующему ключу -Rebuild. При этом режиме работы производится только # проверка, что файл из записи существует на диске. Если такого файла нету, то запись удаляется # из XML. После чего производится поиск новых файлов и для них создаются соответствующие объекты # которые в последствии записываются в обновлённый XML файл. if ($rebuild) { if (Test-Path -LiteralPath $xml) { $old += _fromxml_ $xml } else { throw "Unable to find XML file. Please, run script without '-Rebuild' switch" } [void]($interm = $old | ?{[bool](Test-Path -LiteralPath $_.name) -and $_.name -ne $xml}) $stats.Del = $old.count - $interm.count dirx -filter $Include -exclude $xml $Recurse | %{ $file = gi -LiteralPath $_.fullname $filename = [regex]::Escape($($pwd.providerpath + "\")) $filename = $file.FullName -replace $filename if ($interm | ?{$_.name -eq $filename}) { if ($Verbose) {Write-Host File $filename is ok -ForegroundColor Green} return } else { if ($Verbose) {Write-Host File $filename added -ForegroundColor Cyan} $new += _makeobject_ $sha1 $md5 $filename $file $stats.New += $filename $stats.Total += $filename } } $interm = $interm | %{$_.name = [Security.SecurityElement]::Escape($_.name); $_} $newest = $interm + $new $newest | _toxml_ > $xml stats $newest $xml $stats Set-Location $oldpath return } # все необходимые функции готовы, теперь можно приступать к самой рутине, т.е. # начать с чтения XML файла if (Test-Path -LiteralPath $xml) {$sum += _fromxml_ $xml} # эта часть кода пока отключена, т.к. предназначена для интеграции с другим форматом # БД, на основании которой этот скрипт будет проверять файлы #if (Test-Path $($Include + ".sha1.txt")) { # dirx $path $Include.sha1.txt | %{ # [void]($_.name -match "(.*).sha1.txt") # $filename = $matches[1] # [void]($(gc $($matches[0])) -match "..(.*)`t") # $hash = $matches[1] # [void](_checkfiles_ $filename $hash $sum $Verbose $force) # stats $sum $xml $stats # } #} else { # если XML файл есть, то новые записи в него вноситься не будут, а будет происходить только # сверка БД с действительными файлами if ($sum) { # эта часть выполняется только если мы хотим указать или проверить только конкретный файл if ($Include -ne "*.*") { $sfile = $sum | ?{$_.name -eq $Include -and !$_.psiscontainer} $filename = $Include # если указанный файл существует. то считаем для него хеш if (Test-Path -LiteralPath $(Join-Path $pwd.providerpath $filename)) { $hash = _precheck_ $sha1 $md5 $filename # и отправляем на стандартную рутину проверки файла. [void](_checkfiles_ $filename $hash $sum $Verbose $force) # и готовим итоговый вывод на экран консоли stats $sum $xml $stats return } else { # если файл для этой записи не существует, то пропускаем его, выводя соответствующее сообщение на экран Write-Host File $filename not found. Skipped -ForegroundColor Yellow $stats.Miss += $filename } } else { # мы можем проверять файлы как только в текущей папке (по умолчанию), так и во вложенных # папках тоже. Т.к. все имена файлов во вложенных папках будут содержать слеши, то # при отсутствии ключа -Recurse мы исключаем такие файлы. И дальше повторяем проверку\ # как и в случае с одиночным файлом. В принципе, код одинаковый и их надо как-то совместить # но об этом я буду думать чуть позже :) if ($Recurse) {$xsum = $sum} else {$xsum = $sum | ?{$_.name -notmatch "\\"}} $xsum | %{$filename = $_.name if (Test-Path -LiteralPath $(Join-Path $pwd.providerpath $filename)) { $hash = _precheck_ $sha1 $md5 $filename [void](_checkfiles_ $filename $hash $sum $Verbose $force) } else { Write-Host File $filename not found. Skipped -ForegroundColor Yellow $stats.Miss += $filename } } } } else { # если XML файла нету, то мы будем создавать новую БД с записями dirx -filter $Include -exclude $xml $Recurse | %{ if ($Verbose) {Write-Host Perform file $filename checking} $file = gi -LiteralPath $_.fullname # у имени файла отрезаем текущий путь и оставляем только хвост $filename = [regex]::Escape($($pwd.providerpath + "\")) $filename = $file.FullName -replace $filename # собираем объект XML записи для файла и добавляем её в массив $sum += _makeobject_ $sha1 $md5 $filename $file $stats.New += $filename $stats.Total += $filename } # когда массив файлов будет собран - подаём его на запись в файл $sum | _toxml_ > $xml } stats $sum $xml $stats #} # SIG # Begin signature block # MIIQNAYJKoZIhvcNAQcCoIIQJTCCECECAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB # gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR # AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUHVm3ALCz2SxHoumzuhh3rW2y # F42gggykMIIDejCCAmKgAwIBAgIQOCXX+vhhr570kOcmtdZa1TANBgkqhkiG9w0B # AQUFADBTMQswCQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xKzAp # BgNVBAMTIlZlcmlTaWduIFRpbWUgU3RhbXBpbmcgU2VydmljZXMgQ0EwHhcNMDcw # NjE1MDAwMDAwWhcNMTIwNjE0MjM1OTU5WjBcMQswCQYDVQQGEwJVUzEXMBUGA1UE # ChMOVmVyaVNpZ24sIEluYy4xNDAyBgNVBAMTK1ZlcmlTaWduIFRpbWUgU3RhbXBp # bmcgU2VydmljZXMgU2lnbmVyIC0gRzIwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ # AoGBAMS18lIVvIiGYCkWSlsvS5Frh5HzNVRYNerRNl5iTVJRNHHCe2YdicjdKsRq # CvY32Zh0kfaSrrC1dpbxqUpjRUcuawuSTksrjO5YSovUB+QaLPiCqljZzULzLcB1 # 3o2rx44dmmxMCJUe3tvvZ+FywknCnmA84eK+FqNjeGkUe60tAgMBAAGjgcQwgcEw # NAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC52ZXJpc2ln # bi5jb20wDAYDVR0TAQH/BAIwADAzBgNVHR8ELDAqMCigJqAkhiJodHRwOi8vY3Js # LnZlcmlzaWduLmNvbS90c3MtY2EuY3JsMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMI # MA4GA1UdDwEB/wQEAwIGwDAeBgNVHREEFzAVpBMwETEPMA0GA1UEAxMGVFNBMS0y # MA0GCSqGSIb3DQEBBQUAA4IBAQBQxUvIJIDf5A0kwt4asaECoaaCLQyDFYE3CoIO # LLBaF2G12AX+iNvxkZGzVhpApuuSvjg5sHU2dDqYT+Q3upmJypVCHbC5x6CNV+D6 # 1WQEQjVOAdEzohfITaonx/LhhkwCOE2DeMb8U+Dr4AaH3aSWnl4MmOKlvr+ChcNg # 4d+tKNjHpUtk2scbW72sOQjVOCKhM4sviprrvAchP0RBCQe1ZRwkvEjTRIDroc/J # ArQUz1THFqOAXPl5Pl1yfYgXnixDospTzn099io6uE+UAKVtCoNd+V5T9BizVw9w # w/v1rZWgDhfexBaAYMkPK26GBPHr9Hgn0QXF7jRbXrlJMvIzMIIDxDCCAy2gAwIB # AgIQR78Zld+NUkZD99ttSA0xpDANBgkqhkiG9w0BAQUFADCBizELMAkGA1UEBhMC # WkExFTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTEUMBIGA1UEBxMLRHVyYmFudmlsbGUx # DzANBgNVBAoTBlRoYXd0ZTEdMBsGA1UECxMUVGhhd3RlIENlcnRpZmljYXRpb24x # HzAdBgNVBAMTFlRoYXd0ZSBUaW1lc3RhbXBpbmcgQ0EwHhcNMDMxMjA0MDAwMDAw # WhcNMTMxMjAzMjM1OTU5WjBTMQswCQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNp # Z24sIEluYy4xKzApBgNVBAMTIlZlcmlTaWduIFRpbWUgU3RhbXBpbmcgU2Vydmlj # ZXMgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCpyrKkzM0grwp9 # iayHdfC0TvHfwQ+/Z2G9o2Qc2rv5yjOrhDCJWH6M22vdNp4Pv9HsePJ3pn5vPL+T # rw26aPRslMq9Ui2rSD31ttVdXxsCn/ovax6k96OaphrIAuF/TFLjDmDsQBx+uQ3e # P8e034e9X3pqMS4DmYETqEcgzjFzDVctzXg0M5USmRK53mgvqubjwoqMKsOLIYdm # vYNYV291vzyqJoddyhAVPJ+E6lTBCm7E/sVK3bkHEZcifNs+J9EeeOyfMcnx5iIZ # 28SzR0OaGl+gHpDkXvXufPF9q2IBj/VNC97QIlaolc2uiHau7roN8+RN2aD7aKCu # FDuzh8G7AgMBAAGjgdswgdgwNAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUFBzABhhho # dHRwOi8vb2NzcC52ZXJpc2lnbi5jb20wEgYDVR0TAQH/BAgwBgEB/wIBADBBBgNV # HR8EOjA4MDagNKAyhjBodHRwOi8vY3JsLnZlcmlzaWduLmNvbS9UaGF3dGVUaW1l # c3RhbXBpbmdDQS5jcmwwEwYDVR0lBAwwCgYIKwYBBQUHAwgwDgYDVR0PAQH/BAQD # AgEGMCQGA1UdEQQdMBukGTAXMRUwEwYDVQQDEwxUU0EyMDQ4LTEtNTMwDQYJKoZI # hvcNAQEFBQADgYEASmv56ljCRBwxiXmZK5a/gqwB1hxMzbCKWG7fCCmjXsjKkxPn # BFIN70cnLwA4sOTJk06a1CJiFfc/NyFPcDGA8Ys4h7Po6JcA/s9Vlk4k0qknTnqu # t2FB8yrO58nZXt27K4U+tZ212eFX/760xX71zwye8Jf+K9M7UhsbOCf3P0owggVa # MIIDQqADAgECAgoWkwfVAAAAAAATMA0GCSqGSIb3DQEBBQUAMEkxEjAQBgoJkiaJ # k/IsZAEZFgJsdjEZMBcGCgmSJomT8ixkARkWCXN5c2FkbWluczEYMBYGA1UEAxMP # c3lzYWRtaW5zLUxWLUNBMB4XDTA5MDgwNzEzMzMyOVoXDTEwMDgwNzEzMzMyOVow # VzESMBAGCgmSJomT8ixkARkWAmx2MRkwFwYKCZImiZPyLGQBGRYJc3lzYWRtaW5z # MQ4wDAYDVQQDEwVVc2VyczEWMBQGA1UEAxMNQWRtaW5pc3RyYXRvcjCBnzANBgkq # hkiG9w0BAQEFAAOBjQAwgYkCgYEAloTUoXflQDFR9ZS5sAdOT1QKQQ5IKCpaY2Vv # c9Lxlymo2VI0T3f8lBpzVm2C6ZeKtSyHs+GjbklFUDFDgT8wJLHgE5aFhFukh7nh # wJQsfNKyg10zE/mebfIZIGZ9IPFGDvMVJW+eV+skUjBn58tDFR+3IgnjkOGqbd6R # 8Dsir28CAwEAAaOCAbgwggG0MA4GA1UdDwEB/wQEAwIHgDA7BgkrBgEEAYI3FQcE # LjAsBiQrBgEEAYI3FQiGmb0x4rBigu2DJtLefoO13VFhhLz2C4T0qhkCAWQCAQIw # HQYDVR0OBBYEFFVXJ/UEPfMmoCnu5WNbREsdwlerMB8GA1UdIwQYMBaAFHrmiNrp # +Fw+BwTeEaI4Iql1oxOrMDsGA1UdHwQ0MDIwMKAuoCyGKmh0dHA6Ly9jYS5zeXNh # ZG1pbnMubHYvc3lzYWRtaW5zLUxWLUNBLmNybDB/BggrBgEFBQcBAQRzMHEwJwYI # KwYBBQUHMAGGG2h0dHA6Ly9jYS5zeXNhZG1pbnMubHYvb2NzcDBGBggrBgEFBQcw # AoY6aHR0cDovL2NhLnN5c2FkbWlucy5sdi9jYS5zeXNhZG1pbnMubHZfc3lzYWRt # aW5zLUxWLUNBLmNydDATBgNVHSUEDDAKBggrBgEFBQcDAzAbBgkrBgEEAYI3FQoE # DjAMMAoGCCsGAQUFBwMDMDUGA1UdEQQuMCygKgYKKwYBBAGCNxQCA6AcDBpBZG1p # bmlzdHJhdG9yQHN5c2FkbWlucy5sdjANBgkqhkiG9w0BAQUFAAOCAgEAiAoSViW5 # /n4EJZmLBGrFPoTOCPcxkuCeCm8aLDjvvKZZWl0ylZFrCf099NmjdsBZJjk7Gvd6 # NhiCX80QJaZvoGv/dTK9EvqlFKO46aQzIPrEVCX7FmEA5yDSvmyO/lFsa5m5oM5x # Q6otTM6ZooGNLBMYmaFkJ6N9OsgR1x9A+2f6htAH5BwdmacnvYwMTl3SNmR9rvxJ # HALAErVBWFkwOlU7FUnLQSmlKVhXKaDhrp/mm3ETloRWPVm7jkqLwOnFCys1dTBx # dIDGNHjboWFvUF4zJSJ/vsQhw0aF8bTSP0UIkh0689bKw+ae0+Z62gtVq4usOJMF # zANhgtLcNYUlsB46AIgenneeK0aZAgQ6jGXV0BTWrzMntF6jv7/cA/lhp3AG5Bg8 # hjR890YrrLk6l65zUnJlesnW45hg0wmgb5DDceaXiVJN1Gb9GhbdQfd9nHqCXQnl # hU4eTVyGGfvwaO0qGlklf/aCYKwTThrWLYTrXCbggGkZJqldj8uTAz5fwRgjZ6fV # 9JbMz6bgFdWiOpJUEd30R56i2QNVnu0+EtueTuV26NLbqis2Wi4pbS9jqO44pFMa # usPpPqsu5xoQ8Ad8Wb5JOMA/PClSf7OFXPeqyHgWHZHwMKXY5GdUbLLU2ZfifK1u # c55wnZ+kPGm5r3BUY2CVhvltZIJw6r7nTOsxggL6MIIC9gIBATBXMEkxEjAQBgoJ # kiaJk/IsZAEZFgJsdjEZMBcGCgmSJomT8ixkARkWCXN5c2FkbWluczEYMBYGA1UE # AxMPc3lzYWRtaW5zLUxWLUNBAgoWkwfVAAAAAAATMAkGBSsOAwIaBQCgeDAYBgor # BgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEMBgorBgEEAYI3AgEE # MBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMCMGCSqGSIb3DQEJBDEWBBQP # PVG54pd9thQP8j+kbnGZzSF6pzANBgkqhkiG9w0BAQEFAASBgCdxQSdk2e7xLs6b # h8Mr6402wCEMtBIQpBeQmF4zcTm7YOOmB5aPqSyDGE6dAmdQXcQQo/cLXWIMePgU # GaO78p3zk6IRydob3tbHLN5vzZtY9MJmUJgYUNoZ8Leknpzx52HeOd3nG3ZvjFK2 # 1N/wHvB0WAsaZ4aFv3EZs2wRVO5hoYIBfzCCAXsGCSqGSIb3DQEJBjGCAWwwggFo # AgEBMGcwUzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMSsw # KQYDVQQDEyJWZXJpU2lnbiBUaW1lIFN0YW1waW5nIFNlcnZpY2VzIENBAhA4Jdf6 # +GGvnvSQ5ya11lrVMAkGBSsOAwIaBQCgXTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcN # AQcBMBwGCSqGSIb3DQEJBTEPFw0wOTA4MTkxOTE1NTZaMCMGCSqGSIb3DQEJBDEW # BBRJXWvWM/mJ+Hcu8wj7Odi9S4u8PTANBgkqhkiG9w0BAQEFAASBgJ1/CijgrEHs # NvVCV62btfltoNXi/I1qjDpDZddB1iaDov1E/0Y/C65A2BUsjgqnH3Q6s1NBNfbA # iI8CBqQo9Jt/EY3a816Qd77mh46rBc/PUuoMTHOromLuIhn+5nWs6oQoTUA078wb # qFARKscTMiwyMKqzxU+nYksUDzhkf07s # SIG # End signature block