Client/Start-PsFCIV.ps1

function Start-PsFCIV {
<#
.ExternalHelp PSPKI.Help.xml
#>

[CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, Position = 0)]
        [IO.DirectoryInfo]$Path,
        [Parameter(Mandatory = $true, Position = 1, ParameterSetName = '__xml')]
        [string]$XML,
        [Parameter(Position = 2)]
        [string]$Include = "*",
        [Parameter(Position = 3)]
        [string[]]$Exclude,
        [ValidateSet("Rename", "Delete")]
        [string]$Action,
        [ValidateSet("Bad", "Locked", "Missed", "New", "Ok", "Unknown", "All")]
        [String[]]$Show,
        [ValidateSet("MD5", "SHA1", "SHA256", "SHA384", "SHA512")]
        [AllowEmptyCollection()]
        [String[]]$HashAlgorithm = "SHA1",
        [switch]$Recurse,
        [switch]$Rebuild,
        [switch]$Quiet,
        [switch]$NoStatistic,
        [Parameter(ParameterSetName = '__online')]
        [switch]$Online
    )

#region C# wrappers
Add-Type @"
using System;
using System.Collections.Generic;
using System.Xml.Serialization;
namespace PsFCIV {
    public class StatTable {
        public List<String> Total = new List<String>();
        public List<String> New = new List<String>();
        public List<String> Ok = new List<String>();
        public List<String> Bad = new List<String>();
        public List<String> Missed = new List<String>();
        public List<String> Locked = new List<String>();
        public List<String> Unknown = new List<String>();
        public int Del;
    }
    public class IntStatTable {
        public Int32 Total;
        public Int32 New;
        public Int32 Ok;
        public Int32 Bad;
        public Int32 Missed;
        public Int32 Locked;
        public Int32 Unknown;
        public Int32 Del;
    }
    [XmlType(AnonymousType = true)]
    [XmlRoot(Namespace = "", IsNullable = false)]
    public class FCIV {
        public FCIV() { FILE_ENTRY = new List<FCIVFILE_ENTRY>(); }
         
        [XmlElement("FILE_ENTRY")]
        public List<FCIVFILE_ENTRY> FILE_ENTRY { get; set; }
    }
    [XmlType(AnonymousType = true)]
    public class FCIVFILE_ENTRY {
        public FCIVFILE_ENTRY() { }
        public FCIVFILE_ENTRY(string path) { name = path; }
 
        public String name { get; set; }
        public UInt64 Size { get; set; }
        public String TimeStamp { get; set; }
        public String MD5 { get; set; }
        public String SHA1 { get; set; }
        public String SHA256 { get; set; }
        public String SHA384 { get; set; }
        public String SHA512 { get; set; }
 
        public override Int32 GetHashCode() { return name.GetHashCode(); }
        public override Boolean Equals(Object other) {
            if (ReferenceEquals(null, other) || other.GetType() != GetType()) { return false; }
            return other.GetType() == GetType() && String.Equals(name, ((FCIVFILE_ENTRY)other).name);
        }
    }
}
"@
 -Debug:$false -Verbose:$false -ReferencedAssemblies "System.Xml"
Add-Type -AssemblyName System.Xml
#endregion
    
    if ($PSBoundParameters.Verbose) {$VerbosePreference = "continue"}
    if ($PSBoundParameters.Debug) {$DebugPreference = "continue"}
    $oldverb = $host.PrivateData.VerboseForegroundColor
    $olddeb = $host.PrivateData.DebugForegroundColor
    # preserving current path
    $oldpath = $pwd.Path
    $Exclude += $XML

    if (Test-Path -LiteralPath $path) {
        Set-Location -LiteralPath $path
        if ($pwd.Provider.Name -ne "FileSystem") {
            Set-Location $oldpath
            throw "Specified path is not filesystem path. Try again!"
        }
    } else {throw "Specified path not found."}
    
    # statistic variables
    $sum = $new = New-Object PsFCIV.FCIV
    # creating statistics variable with properties. Each property will contain file names (and paths) with corresponding status.
    $global:stats = New-Object PsFCIV.StatTable
    $script:statcount = New-Object PsFCIV.IntStatTable
    
    # lightweight proxy function for Get-ChildItem cmdlet
    function dirx ([string]$Path, [string]$Filter, [string[]]$Exclude, $Recurse, [switch]$Force) {
        Get-ChildItem @PSBoundParameters -ErrorAction SilentlyContinue | Where-Object {!$_.psiscontainer}
    }    
    # internal function that will check whether the file is locked. All locked files are added to a group with 'Unknown' status.
    function __filelock ($file) {
        $locked = $false
        trap {Set-Variable -name locked -value $true -scope 1; continue}
        $inputStream = New-Object IO.StreamReader $file.FullName
        if ($inputStream) {$inputStream.Close()}
        if ($locked) {
            $host.PrivateData.VerboseForegroundColor = "Yellow"
            $host.PrivateData.DebugForegroundColor = "Yellow"
            Write-Verbose "File $($file.Name) is locked. Skipping this file.."
            Write-Debug "File $($file.Name) is locked. Skipping this file.."
            __statcounter $filename Locked
        }
        $locked
    }    
    # internal function to generate UI window with results by using Out-GridView cmdlet.
    function __formatter ($props, $max) {
        $total = @($input)
        foreach ($property in $props) {
            $(for ($n = 0; $n -lt $max; $n++) {
                $total[0] | Select-Object @{n = $property; e = {$_.$property[$n]}}
            }) | Out-GridView -Title "File list by category: $property"
        }
    }
    # internal hasher
    function __hashbytes ($type, $file) {
        $hasher = [Security.Cryptography.HashAlgorithm]::Create($type)
        $inputStream = New-Object IO.StreamReader $file.FullName
        $hashBytes = $hasher.ComputeHash($inputStream.BaseStream)
        $hasher.Clear()
        $inputStream.Close()
        $hashBytes
    }
    # internal function which reads the XML file (if exist).
    function __fromxml ($xml) {
    # reading existing XML file and selecting required properties
        if (!(Test-Path -LiteralPath $XML)) {return New-Object PsFCIV.FCIV}
        try {
            $fs = New-Object IO.FileStream $XML, "Open"
            $xmlser = New-Object System.Xml.Serialization.XmlSerializer ([Type][PsFCIV.FCIV])
            $sum = $xmlser.Deserialize($fs)
            $fs.Close()
            $sum
        } catch {
            Write-Error -Category InvalidData -Message "Input XML file is not valid FCIV XML file."
        } finally {
            if ($fs -ne $null) {$fs.Close()}
        }
        
    }
    # internal xml writer
    function __writexml ($sum) {
        if ($sum.FILE_ENTRY.Count -eq 0) {
            $host.PrivateData.VerboseForegroundColor = "Yellow"
            $host.PrivateData.DebugForegroundColor = "Yellow"
            Write-Verbose "There is no data to write to XML database."
            Write-Debug "There is no data to write to XML database."
        } else {
            $host.PrivateData.DebugForegroundColor = "Cyan"
            Write-Debug "Preparing to DataBase file creation..."
            try {
                $fs = New-Object IO.FileStream $XML, "Create"
                $xmlser = New-Object System.Xml.Serialization.XmlSerializer ([Type][PsFCIV.FCIV])
                $xmlser.Serialize($fs,$sum)
            } finally {
                if ($fs -ne $null) {$fs.Close()}
            }
            Write-Debug "DataBase file created..."
        }
    }
    # internal function to create XML entry object for a file.
    function __makeobject ($file, [switch]$NoHash, [switch]$hex) {
        $host.PrivateData.DebugForegroundColor = "Yellow"
        Write-Debug "Starting object creation for '$($file.FullName)'..."
        $object = New-Object PsFCIV.FCIVFILE_ENTRY
        $object.name = $file.FullName -replace [regex]::Escape($($pwd.ProviderPath + "\"))
        $object.Size = $file.Length
        # use culture-invariant date/time format.
        $object.TimeStamp = "$($file.LastWriteTime.ToUniversalTime())"
        if (!$NoHash) {
        # calculating appropriate hash and convert resulting byte array to a Base64 string
            foreach ($hash in "MD5", "SHA1", "SHA256", "SHA384", "SHA512") {
                if ($HashAlgorithm -contains $hash) {
                    Write-Debug "Calculating '$hash' hash..."
                    $hashBytes = __hashbytes $hash $file
                    if ($hex) {
                        $object.$hash = -join ($hashBytes | Foreach-Object {"{0:X2}" -f $_})
                    } else {
                        Write-Debug ("Calculated hash value: " + (-join ($hashBytes | Foreach-Object {"{0:X2}" -f $_})))
                        $object.$hash = [System.Convert]::ToBase64String($hashBytes)
                    }
                }
            }
        }
        Write-Debug "Object created!"
        $object
    }    
    # internal function that calculates current file hash and formats it to an octet string (for example, B926D7416E8235E6F94F756E9F3AE2F33A92B2C4).
    function __precheck ($entry, $file, $HashAlgorithm) {
        $host.PrivateData.DebugForegroundColor = "Yellow"
        if ($HashAlgorithm.Length -gt 0) {
            $SelectedHash = $HashAlgorithm
        } else {
            :outer foreach ($hash in "SHA512", "SHA384", "SHA256", "SHA1", "MD5") {
                if ($entry.$hash) {$SelectedHash = $hash; break outer}
            }
        }
        $host.PrivateData.DebugForegroundColor = "Green"
        Write-Debug "Selected hash: $hash"
        -join ($(__hashbytes $SelectedHash $file) | ForEach-Object {"{0:X2}" -f $_})
        $SelectedHash
    }
    # process -Action parameter to perform an action against bad file (if actual file properties do not match the record in XML).
    function __takeaction ($file, $Action) {
        switch ($Action) {
            "Rename" {Rename-Item $file $($file.FullName + ".bad")}
            "Delete" {Remove-Item $file -Force}
        }
    }    
    # core file verification function.
    function __checkfiles ($entry, $file, $Action) {
        if (($file.Length -eq $entry.Size) -and ("$($file.LastWriteTime.ToUniversalTime())" -eq $entry.TimeStamp)) {
            $hexhash = __precheck $entry $file $HashAlgorithm
            $ActualHash = -join ([Convert]::FromBase64String($entry.($hexhash[1])) | ForEach-Object {"{0:X2}" -f $_})
            if (!$ActualHash) {
                $host.PrivateData.VerboseForegroundColor = "Red"
                Write-Verbose "XML database entry does not contains '$($hexhash[1])' hash value for the entry '$($entry.name)'."
                __statcounter $entry.name Unknown
                return
            } elseif ($ActualHash -eq $hexhash[0]) {
                $host.PrivateData.VerboseForegroundColor = $Host.PrivateData.DebugForegroundColor = "Green"
                Write-Debug "File hash: $ActualHash"
                Write-Verbose "File '$($file.name)' is ok."
                __statcounter $entry.name Ok
                return
            } else {
                $host.PrivateData.DebugForegroundColor = "Red"
                Write-Debug "File '$($file.name)' failed hash verification.
                    Expected hash: $hexhash
                    Actual hash: $ActualHash"

                __statcounter $entry.name Bad
                if ($Action) {__takeaction $file $Action}
            }
        } else {
            $host.PrivateData.VerboseForegroundColor = $Host.PrivateData.DebugForegroundColor = "Red"
            Write-Verbose "File '$($file.FullName)' size or Modified Date/Time mismatch."
            Write-Debug "Expected file size is: $($entry.Size) byte(s), actual size is: $($file.Length) byte(s)."
            Write-Debug "Expected file modification time is: $($entry.TimeStamp), actual file modification time is: $($file.LastWriteTime.ToUniversalTime())"
            __statcounter $entry.name Bad
            if ($Action) {__takeaction $file $Action}
        }
    }
    # internal function to calculate resulting statistics and show if if necessary.
    function __stats {
    # if -Show parameter is presented we display selected groups (Total, New, Ok, Bad, Missed, Unknown)
        if ($show -and !$NoStatistic) {
            if ($Show -eq "All" -or $Show.Contains("All")) {
                $global:stats | __formatter "Bad", "Locked", "Missed", "New", "Ok", "Unknown" $script:statcount.Total
            } else {
                $global:stats | Select-Object $show | __formatter $show $script:statcount.Total
            }            
        }
        # script work in numbers
        if (!$Quiet) {
            Write-Host ----------------------------------- -ForegroundColor Green
            if ($Rebuild) {
                Write-Host Total entries processed: $script:statcount.Total -ForegroundColor Cyan
                Write-Host Total removed unused entries: $script:statcount.Del -ForegroundColor Yellow
            } else {Write-Host Total files processed: $script:statcount.Total -ForegroundColor Cyan}
            Write-Host Total new added files: $script:statcount.New -ForegroundColor Green
            Write-Host Total good files: $script:statcount.Ok -ForegroundColor Green
            Write-Host Total bad files: $script:statcount.Bad -ForegroundColor Red
            Write-Host Total unknown status files: $script:statcount.Unknown -ForegroundColor Yellow
            Write-Host Total missing files: $script:statcount.Missed -ForegroundColor Yellow
            Write-Host Total locked files: $script:statcount.Locked -ForegroundColor Yellow
            Write-Host ----------------------------------- -ForegroundColor Green
        }
        # restore original variables
        Set-Location -LiteralPath $oldpath
        $host.PrivateData.VerboseForegroundColor = $oldverb
        $host.PrivateData.DebugForegroundColor = $olddeb
        $exit = 0
        # create exit code depending on check status
        if ($Rebuild) {$exit = [int]::MaxValue} else {
            if ($script:statcount.Bad -ne 0) {$exit += 1}
            if ($script:statcount.Missed -ne 0) {$exit += 2}
            if ($script:statcount.Unknown -ne 0) {$exit += 4}
            if ($script:statcount.Locked -ne 0) {$exit += 8}
        }
        if ($Quiet) {exit $exit}
    }
    # internal function to update statistic counters.
    function __statcounter ($filename, $status) {
        $script:statcount.$status++
        $script:statcount.Total++
        if (!$NoStatistic) {
            $global:stats.$status.Add($filename)
        }
    }
    if ($Online) {
        $host.PrivateData.DebugForegroundColor = "White"
        Write-Debug "Online mode ON"
        dirx -Path .\* -Filter $Include -Exclude $Exclude $Recurse -Force | ForEach-Object {
            $host.PrivateData.VerboseForegroundColor = $Host.UI.RawUI.ForegroundColor
            Write-Verbose "Perform file '$($_.fullName)' checking."
            $file = Get-Item -LiteralPath $_.FullName -Force -ErrorAction SilentlyContinue
            if (__filelock $file) {return}
            __makeobject $file -hex
        }
        return
    }

    <#
    in this part we perform XML file update by removing entries for non-exist files and
    adding new entries for files that are not in the database.
    #>

    if ($Rebuild) {
        $host.PrivateData.DebugForegroundColor = "White"
        Write-Debug "Rebuild mode ON"
        if (Test-Path -LiteralPath $xml) {
            $old = __fromxml $xml
        } else {
            Set-Location $oldpath
            throw "Unable to find XML file. Please, run the command without '-Rebuild' switch."
        }
        $interm = New-Object PsFCIV.FCIV
        # use foreach-object instead of where-object to keep original types.
        $host.PrivateData.VerboseForegroundColor = $host.UI.RawUI.ForegroundColor
        Write-Verbose "Perform DB file cleanup from non-existent items."
        $old.FILE_ENTRY | ForEach-Object {
            if ((Test-Path -LiteralPath $_.name)) {
                if ($_.name -eq $xml) {
                    $host.PrivateData.DebugForegroundColor = "Yellow"
                    Write-Debug "File '$($_.name)' is DB file. Removed."
                } else {
                    $interm.FILE_ENTRY.Add($_)
                }
            } else {
                $host.PrivateData.DebugForegroundColor = "Yellow"
                Write-Debug "File '$($_.name)' does not exist. Removed."
            }
        }
        $script:statcount.Del = $interm.Length
        $script:statcount.Total = $old.FILE_ENTRY.Count - $interm.Length
        dirx -Path .\* -Filter $Include -Exclude $Exclude $Recurse -Force | ForEach-Object {
            $host.PrivateData.VerboseForegroundColor = $host.UI.RawUI.ForegroundColor
            Write-Verbose "Perform file '$($_.FullName)' checking."
            $file = Get-Item -LiteralPath $_.FullName -Force
            if (__filelock $file) {return}
            $filename = $file.FullName -replace [regex]::Escape($($pwd.providerpath + "\"))
            $host.PrivateData.VerboseForegroundColor = "Green"
            if ($interm.FILE_ENTRY.Contains((New-Object PsFCIV.FCIVFILE_ENTRY $filename))) {
                Write-Verbose "File '$filename' already exist in XML database. Skipping."
                return
            } else {
                $new.FILE_ENTRY.Add((__makeobject $file))
                Write-Verbose "File '$filename' is added."
                __statcounter $filename New
            }
        }
        $interm.FILE_ENTRY.AddRange($new.FILE_ENTRY)
        __writexml $interm
        __stats
        return
    }
    
    # this part contains main routine
    $sum = __fromxml $xml
    <#
    check XML file format. If Size property of the first element is zero, then the file was generated by
    original FCIV.exe tool. In this case we transform existing XML to a new PsFCIV format by adding new
    properties. Each record is checked against hashes stored in the source XML file. If hash check fails,
    an item is removed from final XML.
    #>

    if ($sum.FILE_ENTRY.Count -gt 0 -and $sum.FILE_ENTRY[0].Size -eq 0) {
        #
        if ($PSBoundParameters.ContainsKey("HashAlgorithm")) {$HashAlgorithm = $HashAlgorithm[0].ToUpper()} else {$HashAlgorithm = @()}
        $host.PrivateData.DebugForegroundColor = "White"
        Write-Debug "FCIV (compatibility) mode ON"
        if ($HashAlgorithm -and $HashAlgorithm -notcontains "sha1" -and $HashAlgorithm -notcontains "md5") {
            throw "Specified hash algorithm (or algorithms) is not supported. For native FCIV source, use MD5 and/or SHA1."
        }
        for ($index = 0; $index -lt $sum.FILE_ENTRY.Count; $index++) {
            $host.PrivateData.VerboseForegroundColor = $host.UI.RawUI.ForegroundColor
            Write-Verbose "Perform file '$($sum.FILE_ENTRY[$index].name)' checking."
            $filename = $sum.FILE_ENTRY[$index].name
            # check if the path is absolute and matches current path. If the path is absolute and does not belong to
            # current path -- skip this entry.
            if ($filename.Contains(":") -and $filename -notmatch [regex]::Escape($pwd.ProviderPath)) {return}
            # if source file name record contains absolute path, and belongs to the current pathe,
            # just strip base path. New XML format uses relative paths only.
            if ($filename.Contains(":")) {$filename = $filename -replace ([regex]::Escape($($pwd.ProviderPath + "\")))}
            # Test if the file exist. If the file does not exist, skip the current entry and process another record.
            if (!(Test-Path -LiteralPath $filename)) {
                $host.PrivateData.VerboseForegroundColor = "Yellow"
                Write-Verbose "File '$filename' not found. Skipping."
                __statcounter $filename Missed
                return
            }
            # get file item and test if it is not locked by another application
            $file = Get-Item -LiteralPath $filename -Force -ErrorAction SilentlyContinue
            if (__filelock $file) {return}
            # create new-style entry record that stores additional data: file length and last modification timestamp.
            $entry = __makeobject $file -NoHash
            $entry.name = $filename
            # process current hash entries and copy required hash values to a new entry object.
            "SHA1", "MD5" | ForEach-Object {$entry.$_ = $sum.FILE_ENTRY[$index].$_}
            $sum.FILE_ENTRY[$index] = $entry
            __checkfiles $newentry $file $Action
        }
        # we are done. Overwrite XML, display stats and exit.
        __writexml $sum
        # display statistics and exit right now.
        __stats
    }
    # if XML file exist, proccess and check all records. XML file will not be modified.
    if ($sum.FILE_ENTRY.Count -gt 0) {
        $host.PrivateData.DebugForegroundColor = "White"
        Write-Debug "Native PsFCIV mode ON"
        # this part is executed only when we want to process certain file. Wildcards are not allowed.
        if ($Include -ne "*") {
            $sum.FILE_ENTRY | Where-Object {$_.name -like $Include} | ForEach-Object {
                $host.PrivateData.VerboseForegroundColor = $host.UI.RawUI.ForegroundColor
                Write-Verbose "Perform file '$($_.name)' checking."
                $entry = $_
                # calculate the hash if the file exist.
                if (Test-Path -LiteralPath $entry.name) {
                    # and check file integrity
                    $file = Get-Item -LiteralPath $entry.name -Force -ErrorAction SilentlyContinue
                    __checkfiles $entry $file $Action
                } else {
                    # if there is no record for the file, skip it and display appropriate message
                    $host.PrivateData.VerboseForegroundColor = "Yellow"
                    Write-Verbose "File '$filename' not found. Skipping."
                    __statcounter $entry.name Missed
                }
            }
        } else {
            $sum.FILE_ENTRY | ForEach-Object {
                <#
                to process files only in the current directory (without subfolders), we remove items
                that contain slashes from the process list and continue regular file checking.
                #>

                if (!$Recurse -and $_.name -match "\\") {return}
                $host.PrivateData.VerboseForegroundColor = $host.UI.RawUI.ForegroundColor
                Write-Verbose "Perform file '$($_.name)' checking."
                $entry = $_
                if (Test-Path -LiteralPath $entry.name) {
                    $file = Get-Item -LiteralPath $entry.name -Force -ErrorAction SilentlyContinue
                    __checkfiles $entry $file $Action
                } else {
                    $host.PrivateData.VerboseForegroundColor = "Yellow"
                    Write-Verbose "File '$($entry.name)' not found. Skipping."
                    __statcounter $entry.name Missed
                }
            }
        }
    } else {
        # if there is no existing XML DB file, start from scratch and create a new one.
        $host.PrivateData.DebugForegroundColor = "White"
        Write-Debug "New XML mode ON"

        dirx -Path .\* -Filter $Include -Exclude $Exclude $Recurse -Force | ForEach-Object {
            $_
            # $host.PrivateData.VerboseForegroundColor = $Host.UI.RawUI.ForegroundColor
            # Write-Verbose "Perform file '$($_.fullName)' checking."
            # $file = Get-Item -LiteralPath $_.FullName -Force -ErrorAction SilentlyContinue
            # if (__filelock $file) {return}
            # $entry = __makeobject $file
            # $sum.FILE_ENTRY.Add($entry)
            # __statcounter $entry.name New
        }
        __writexml $sum
    }
    __stats
}
# SIG # Begin signature block
# MIIcgAYJKoZIhvcNAQcCoIIccTCCHG0CAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCh54G+7BnjUc2T
# VlNgo0WdRcbYHLAtcSrMGdoBXdwbhqCCF4owggUTMIID+6ADAgECAhAJwnVp5a70
# RHscglFEfEqLMA0GCSqGSIb3DQEBCwUAMHIxCzAJBgNVBAYTAlVTMRUwEwYDVQQK
# EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xMTAvBgNV
# BAMTKERpZ2lDZXJ0IFNIQTIgQXNzdXJlZCBJRCBDb2RlIFNpZ25pbmcgQ0EwHhcN
# MTcwNDE3MDAwMDAwWhcNMjAwNDIxMTIwMDAwWjBQMQswCQYDVQQGEwJMVjENMAsG
# A1UEBxMEUmlnYTEYMBYGA1UEChMPU3lzYWRtaW5zIExWIElLMRgwFgYDVQQDEw9T
# eXNhZG1pbnMgTFYgSUswggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCo
# NCCuzEogktL+1+lvPHu7ctNtCD7wA5Nalebh0FaKz3v1944APtg7A5oQfh6c20f7
# xYyTw4wVuo6L6S3dlMUa+bfXvTXIco0ilTIz0uqUKW8WGYwJtbFpu6PvCs0LHDRD
# rD8sEFgGHQhbz+J4gtV8BI7OID+yNfgbUk4JeSBGNzgeqZMdf/xceMoLx+fHi9tU
# OdTtgs/dXQYg3M3J+rGxFdpxOO7JmUZ8nqVALlnU9cHBGKUY4hDvDxfp7EukhnHv
# RpkhacZB1RBw0q8q+ekvLVCZwpG4N1Pnq2ksHiBzqRWQQE89iV+UwgRnLx2igywk
# 2kX+JPSZYsQCbDGo4DqBAgMBAAGjggHFMIIBwTAfBgNVHSMEGDAWgBRaxLl7Kgqj
# pepxA8Bg+S32ZXUOWDAdBgNVHQ4EFgQU9Mh+66y4uf1WQl9FmsWMHdk2HrswDgYD
# VR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMHcGA1UdHwRwMG4wNaAz
# oDGGL2h0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9zaGEyLWFzc3VyZWQtY3MtZzEu
# Y3JsMDWgM6Axhi9odHRwOi8vY3JsNC5kaWdpY2VydC5jb20vc2hhMi1hc3N1cmVk
# LWNzLWcxLmNybDBMBgNVHSAERTBDMDcGCWCGSAGG/WwDATAqMCgGCCsGAQUFBwIB
# FhxodHRwczovL3d3dy5kaWdpY2VydC5jb20vQ1BTMAgGBmeBDAEEATCBhAYIKwYB
# BQUHAQEEeDB2MCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20w
# TgYIKwYBBQUHMAKGQmh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2Vy
# dFNIQTJBc3N1cmVkSURDb2RlU2lnbmluZ0NBLmNydDAMBgNVHRMBAf8EAjAAMA0G
# CSqGSIb3DQEBCwUAA4IBAQCfpLMUerP0WXkcb9+dunMLt3jowZEd8X6ISxxzdsdB
# 8jOZ92L88qKqjWD1I9HBceba4tdJZCLV33S9a3eoxUunIlJH4GmYH/HSrc2qgNxg
# PyobmWf556c7Wd3q6ZUKgos0bw++TtLqb/jvoKN19epTEkwQDIwVFzOAxZ4T+sYr
# jmFhd9KeaRhTLZRBVdKNTKtXaoWFrfNSQTp8NcNYdkEM05cUnEUMDOoeLSmxPnIv
# pl8KbripxtVQ591rCLJN2uMtrtSE1nvjiYfSFQI00EiB33ZoI2T1eCNuP1M6c+ex
# KzQQC8UDp7J+duzl1j605TwSfLS/MJsaiwftNzc3FfgSMIIFMDCCBBigAwIBAgIQ
# BAkYG1/Vu2Z1U0O1b5VQCDANBgkqhkiG9w0BAQsFADBlMQswCQYDVQQGEwJVUzEV
# MBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29t
# MSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwHhcNMTMxMDIy
# MTIwMDAwWhcNMjgxMDIyMTIwMDAwWjByMQswCQYDVQQGEwJVUzEVMBMGA1UEChMM
# RGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMTEwLwYDVQQD
# EyhEaWdpQ2VydCBTSEEyIEFzc3VyZWQgSUQgQ29kZSBTaWduaW5nIENBMIIBIjAN
# BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA+NOzHH8OEa9ndwfTCzFJGc/Q+0WZ
# sTrbRPV/5aid2zLXcep2nQUut4/6kkPApfmJ1DcZ17aq8JyGpdglrA55KDp+6dFn
# 08b7KSfH03sjlOSRI5aQd4L5oYQjZhJUM1B0sSgmuyRpwsJS8hRniolF1C2ho+mI
# LCCVrhxKhwjfDPXiTWAYvqrEsq5wMWYzcT6scKKrzn/pfMuSoeU7MRzP6vIK5Fe7
# SrXpdOYr/mzLfnQ5Ng2Q7+S1TqSp6moKq4TzrGdOtcT3jNEgJSPrCGQ+UpbB8g8S
# 9MWOD8Gi6CxR93O8vYWxYoNzQYIH5DiLanMg0A9kczyen6Yzqf0Z3yWT0QIDAQAB
# o4IBzTCCAckwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwEwYD
# VR0lBAwwCgYIKwYBBQUHAwMweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhho
# dHRwOi8vb2NzcC5kaWdpY2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNl
# cnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwgYEG
# A1UdHwR6MHgwOqA4oDaGNGh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2Vy
# dEFzc3VyZWRJRFJvb3RDQS5jcmwwOqA4oDaGNGh0dHA6Ly9jcmwzLmRpZ2ljZXJ0
# LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcmwwTwYDVR0gBEgwRjA4Bgpg
# hkgBhv1sAAIEMCowKAYIKwYBBQUHAgEWHGh0dHBzOi8vd3d3LmRpZ2ljZXJ0LmNv
# bS9DUFMwCgYIYIZIAYb9bAMwHQYDVR0OBBYEFFrEuXsqCqOl6nEDwGD5LfZldQ5Y
# MB8GA1UdIwQYMBaAFEXroq/0ksuCMS1Ri6enIZ3zbcgPMA0GCSqGSIb3DQEBCwUA
# A4IBAQA+7A1aJLPzItEVyCx8JSl2qB1dHC06GsTvMGHXfgtg/cM9D8Svi/3vKt8g
# VTew4fbRknUPUbRupY5a4l4kgU4QpO4/cY5jDhNLrddfRHnzNhQGivecRk5c/5Cx
# GwcOkRX7uq+1UcKNJK4kxscnKqEpKBo6cSgCPC6Ro8AlEeKcFEehemhor5unXCBc
# 2XGxDI+7qPjFEmifz0DLQESlE/DmZAwlCEIysjaKJAL+L3J+HNdJRZboWR3p+nRk
# a7LrZkPas7CM1ekN3fYBIM6ZMWM9CBoYs4GbT8aTEAb8B4H6i9r5gkn3Ym6hU/oS
# lBiFLpKR6mhsRDKyZqHnGKSaZFHvMIIGajCCBVKgAwIBAgIQAwGaAjr/WLFr1tXq
# 5hfwZjANBgkqhkiG9w0BAQUFADBiMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGln
# aUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhE
# aWdpQ2VydCBBc3N1cmVkIElEIENBLTEwHhcNMTQxMDIyMDAwMDAwWhcNMjQxMDIy
# MDAwMDAwWjBHMQswCQYDVQQGEwJVUzERMA8GA1UEChMIRGlnaUNlcnQxJTAjBgNV
# BAMTHERpZ2lDZXJ0IFRpbWVzdGFtcCBSZXNwb25kZXIwggEiMA0GCSqGSIb3DQEB
# AQUAA4IBDwAwggEKAoIBAQCjZF38fLPggjXg4PbGKuZJdTvMbuBTqZ8fZFnmfGt/
# a4ydVfiS457VWmNbAklQ2YPOb2bu3cuF6V+l+dSHdIhEOxnJ5fWRn8YUOawk6qhL
# LJGJzF4o9GS2ULf1ErNzlgpno75hn67z/RJ4dQ6mWxT9RSOOhkRVfRiGBYxVh3lI
# RvfKDo2n3k5f4qi2LVkCYYhhchhoubh87ubnNC8xd4EwH7s2AY3vJ+P3mvBMMWSN
# 4+v6GYeofs/sjAw2W3rBerh4x8kGLkYQyI3oBGDbvHN0+k7Y/qpA8bLOcEaD6dpA
# oVk62RUJV5lWMJPzyWHM0AjMa+xiQpGsAsDvpPCJEY93AgMBAAGjggM1MIIDMTAO
# BgNVHQ8BAf8EBAMCB4AwDAYDVR0TAQH/BAIwADAWBgNVHSUBAf8EDDAKBggrBgEF
# BQcDCDCCAb8GA1UdIASCAbYwggGyMIIBoQYJYIZIAYb9bAcBMIIBkjAoBggrBgEF
# BQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzCCAWQGCCsGAQUFBwIC
# MIIBVh6CAVIAQQBuAHkAIAB1AHMAZQAgAG8AZgAgAHQAaABpAHMAIABDAGUAcgB0
# AGkAZgBpAGMAYQB0AGUAIABjAG8AbgBzAHQAaQB0AHUAdABlAHMAIABhAGMAYwBl
# AHAAdABhAG4AYwBlACAAbwBmACAAdABoAGUAIABEAGkAZwBpAEMAZQByAHQAIABD
# AFAALwBDAFAAUwAgAGEAbgBkACAAdABoAGUAIABSAGUAbAB5AGkAbgBnACAAUABh
# AHIAdAB5ACAAQQBnAHIAZQBlAG0AZQBuAHQAIAB3AGgAaQBjAGgAIABsAGkAbQBp
# AHQAIABsAGkAYQBiAGkAbABpAHQAeQAgAGEAbgBkACAAYQByAGUAIABpAG4AYwBv
# AHIAcABvAHIAYQB0AGUAZAAgAGgAZQByAGUAaQBuACAAYgB5ACAAcgBlAGYAZQBy
# AGUAbgBjAGUALjALBglghkgBhv1sAxUwHwYDVR0jBBgwFoAUFQASKxOYspkH7R7f
# or5XDStnAs0wHQYDVR0OBBYEFGFaTSS2STKdSip5GoNL9B6Jwcp9MH0GA1UdHwR2
# MHQwOKA2oDSGMmh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3Vy
# ZWRJRENBLTEuY3JsMDigNqA0hjJodHRwOi8vY3JsNC5kaWdpY2VydC5jb20vRGln
# aUNlcnRBc3N1cmVkSURDQS0xLmNybDB3BggrBgEFBQcBAQRrMGkwJAYIKwYBBQUH
# MAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBBBggrBgEFBQcwAoY1aHR0cDov
# L2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEQ0EtMS5jcnQw
# DQYJKoZIhvcNAQEFBQADggEBAJ0lfhszTbImgVybhs4jIA+Ah+WI//+x1GosMe06
# FxlxF82pG7xaFjkAneNshORaQPveBgGMN/qbsZ0kfv4gpFetW7easGAm6mlXIV00
# Lx9xsIOUGQVrNZAQoHuXx/Y/5+IRQaa9YtnwJz04HShvOlIJ8OxwYtNiS7Dgc6aS
# wNOOMdgv420XEwbu5AO2FKvzj0OncZ0h3RTKFV2SQdr5D4HRmXQNJsQOfxu19aDx
# xncGKBXp2JPlVRbwuwqrHNtcSCdmyKOLChzlldquxC5ZoGHd2vNtomHpigtt7BIY
# vfdVVEADkitrwlHCCkivsNRu4PQUCjob4489yq9qjXvc2EQwggbNMIIFtaADAgEC
# AhAG/fkDlgOt6gAK6z8nu7obMA0GCSqGSIb3DQEBBQUAMGUxCzAJBgNVBAYTAlVT
# MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j
# b20xJDAiBgNVBAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0wNjEx
# MTAwMDAwMDBaFw0yMTExMTAwMDAwMDBaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQK
# EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNV
# BAMTGERpZ2lDZXJ0IEFzc3VyZWQgSUQgQ0EtMTCCASIwDQYJKoZIhvcNAQEBBQAD
# ggEPADCCAQoCggEBAOiCLZn5ysJClaWAc0Bw0p5WVFypxNJBBo/JM/xNRZFcgZ/t
# LJz4FlnfnrUkFcKYubR3SdyJxArar8tea+2tsHEx6886QAxGTZPsi3o2CAOrDDT+
# GEmC/sfHMUiAfB6iD5IOUMnGh+s2P9gww/+m9/uizW9zI/6sVgWQ8DIhFonGcIj5
# BZd9o8dD3QLoOz3tsUGj7T++25VIxO4es/K8DCuZ0MZdEkKB4YNugnM/JksUkK5Z
# ZgrEjb7SzgaurYRvSISbT0C58Uzyr5j79s5AXVz2qPEvr+yJIvJrGGWxwXOt1/HY
# zx4KdFxCuGh+t9V3CidWfA9ipD8yFGCV/QcEogkCAwEAAaOCA3owggN2MA4GA1Ud
# DwEB/wQEAwIBhjA7BgNVHSUENDAyBggrBgEFBQcDAQYIKwYBBQUHAwIGCCsGAQUF
# BwMDBggrBgEFBQcDBAYIKwYBBQUHAwgwggHSBgNVHSAEggHJMIIBxTCCAbQGCmCG
# SAGG/WwAAQQwggGkMDoGCCsGAQUFBwIBFi5odHRwOi8vd3d3LmRpZ2ljZXJ0LmNv
# bS9zc2wtY3BzLXJlcG9zaXRvcnkuaHRtMIIBZAYIKwYBBQUHAgIwggFWHoIBUgBB
# AG4AeQAgAHUAcwBlACAAbwBmACAAdABoAGkAcwAgAEMAZQByAHQAaQBmAGkAYwBh
# AHQAZQAgAGMAbwBuAHMAdABpAHQAdQB0AGUAcwAgAGEAYwBjAGUAcAB0AGEAbgBj
# AGUAIABvAGYAIAB0AGgAZQAgAEQAaQBnAGkAQwBlAHIAdAAgAEMAUAAvAEMAUABT
# ACAAYQBuAGQAIAB0AGgAZQAgAFIAZQBsAHkAaQBuAGcAIABQAGEAcgB0AHkAIABB
# AGcAcgBlAGUAbQBlAG4AdAAgAHcAaABpAGMAaAAgAGwAaQBtAGkAdAAgAGwAaQBh
# AGIAaQBsAGkAdAB5ACAAYQBuAGQAIABhAHIAZQAgAGkAbgBjAG8AcgBwAG8AcgBh
# AHQAZQBkACAAaABlAHIAZQBpAG4AIABiAHkAIAByAGUAZgBlAHIAZQBuAGMAZQAu
# MAsGCWCGSAGG/WwDFTASBgNVHRMBAf8ECDAGAQH/AgEAMHkGCCsGAQUFBwEBBG0w
# azAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEMGCCsGAQUF
# BzAChjdodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRBc3N1cmVk
# SURSb290Q0EuY3J0MIGBBgNVHR8EejB4MDqgOKA2hjRodHRwOi8vY3JsMy5kaWdp
# Y2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURSb290Q0EuY3JsMDqgOKA2hjRodHRw
# Oi8vY3JsNC5kaWdpY2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURSb290Q0EuY3Js
# MB0GA1UdDgQWBBQVABIrE5iymQftHt+ivlcNK2cCzTAfBgNVHSMEGDAWgBRF66Kv
# 9JLLgjEtUYunpyGd823IDzANBgkqhkiG9w0BAQUFAAOCAQEARlA+ybcoJKc4HbZb
# Ka9Sz1LpMUerVlx71Q0LQbPv7HUfdDjyslxhopyVw1Dkgrkj0bo6hnKtOHisdV0X
# FzRyR4WUVtHruzaEd8wkpfMEGVWp5+Pnq2LN+4stkMLA0rWUvV5PsQXSDj0aqRRb
# poYxYqioM+SbOafE9c4deHaUJXPkKqvPnHZL7V/CSxbkS3BMAIke/MV5vEwSV/5f
# 4R68Al2o/vsHOE8Nxl2RuQ9nRc3Wg+3nkg2NsWmMT/tZ4CMP0qquAHzunEIOz5HX
# J7cW7g/DvXwKoO4sCFWFIrjrGBpN/CohrUkxg0eVd3HcsRtLSxwQnHcUwZ1PL1qV
# CCkQJjGCBEwwggRIAgEBMIGGMHIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdp
# Q2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xMTAvBgNVBAMTKERp
# Z2lDZXJ0IFNIQTIgQXNzdXJlZCBJRCBDb2RlIFNpZ25pbmcgQ0ECEAnCdWnlrvRE
# exyCUUR8SoswDQYJYIZIAWUDBAIBBQCggYQwGAYKKwYBBAGCNwIBDDEKMAigAoAA
# oQKAADAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgorBgEEAYI3AgELMQ4w
# DAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQgpwLhyhhxKt5syg/Vuubf8G9n
# /9o6uAYWCKV4wLO1ObAwDQYJKoZIhvcNAQEBBQAEggEAJkOTNagKUta2jab51aMX
# vmo8YjbNiGokSoBJ0mX6Xb1UMD463wtyh6J7eQaYmhUgQYtlEqkIPs8+KJlm/PGM
# 9kbrXTZhBKvrkVrD0hTpTSQj7mLfutZrKj1TLQPZytKmvNvG0iMzggG8UpxM9ZIa
# 6+1eESakMulJDZEgHBLXrIotBdKY8K3Ay8GHDaL/jpEWlO2qJpPP2dtpNS3Gr5ht
# 8jKa8shYr7ZRPalpgsD3dpnsDvQ46uspomCUBm0My0oIwAw2bazImfVjpcC0jk5o
# VLycaNnYhSmRADvZdNxMofnColwfObnMsnGZoPHA6Pnb9WIukvG8Sm9moaYmuNc3
# d6GCAg8wggILBgkqhkiG9w0BCQYxggH8MIIB+AIBATB2MGIxCzAJBgNVBAYTAlVT
# MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j
# b20xITAfBgNVBAMTGERpZ2lDZXJ0IEFzc3VyZWQgSUQgQ0EtMQIQAwGaAjr/WLFr
# 1tXq5hfwZjAJBgUrDgMCGgUAoF0wGAYJKoZIhvcNAQkDMQsGCSqGSIb3DQEHATAc
# BgkqhkiG9w0BCQUxDxcNMTgxMDIyMTYyNzI0WjAjBgkqhkiG9w0BCQQxFgQU4vDI
# Cl3NJKgicVVTfMyGjzMKME8wDQYJKoZIhvcNAQEBBQAEggEAYFyFzR8vY6NCJRn/
# u+6fdQQMwFXa4Pecf7bkR4h6/jTiloU3rU1Wv8cbaJ/St/Chhv6l6Z2FqB6iTL25
# Yj6GIeUWZhu1ZoDIVZ3jjyt6S6mHIc4PFHHlugL+pk9OHscCwpvafmIHPfdTvPvB
# tK5KwE5dn4S+zw6wwUP3UAzRhpHOAOUHBk6CHRe/s3QTtguirfIxSToI1C/1rqjZ
# yRqMaTXbjB4tcP3yiDT22oV/2ZS3zuxiP8tojkmnDsNFR632SZHi4np/NjQGR9GY
# oyoDMnqFOCDv0KWS+ywjEtrAtxmVRw/F7lmS/hS7NQRx/v7gN801aFTuMgr7uN/a
# 3z/mwA==
# SIG # End signature block