# Global Constants
$GET_DECIMAL_VALUE = 0
$GET_STRING = 0

$Global:Parsers = @{}

<#
.SYNOPSIS
Provides a function to standardized log entries
.DESCRIPTION
When specified outputs verbose logging in a standardized format
.PARAMETER Message
Descriptive message to write to log
.PARAMETER Operation
Particular operation that trigged this log
.PARAMETER Data
Object containing relevent information to reproduce the operation
.EXAMPLE
Out-Log -Operation $MyInvocation.MyCommand.Name -Data $PSBoundParameters
#>
function Out-Log {
    [CmdletBinding()]
    param(
        [Parameter(Position = 0, ValueFromPipeline)]
        [string] $Message,
        [string] $Operation = '',
        [Hashtable] $Data = @{}
    )
    process {
        $logEnabled = $VerbosePreference -ne 'SilentlyContinue'
        if(-not $logEnabled) {
            return
        }
        $timestamp = (Get-Date).ToUniversalTime().ToString("O")
        $scriptName = "$($self.ScriptName)"
        $operationText = if($Operation) { "${Operation}: " }
        $messageText = if($Message) { "${Message} "  }
        $dataObject = if($Data) {
            $Data.Remove('Verbose')
            $Data.Remove('TenantAppSecret')
            $Data | ConvertTo-Json -Compress
        }
        $logText = "[$timestamp $scriptName] ${operationText}${messageText}${dataObject}"
        Write-Verbose -Message $logText
    }
}

<#
.SYNOPSIS
Searches LDAP server with given filter

.DESCRIPTION
Searches LDAP server identified by given domain name with given parameters using System.DirectoryServices.Protocols

.PARAMETER dnsDomain
DNS name of the domain that will be searched against

.PARAMETER attributes
Attributes to return from the search, should be lowercase as convention

.PARAMETER baseDN
Root distinguished name for the search

.PARAMETER scope
Scope of the search

.PARAMETER includeDeleted
Determine if we search for deleted items

.PARAMETER filter
Filter to be applied to the search

.PARAMETER pageSize
Page size for the search
Default = 1000

.PARAMETER ASQ
Attribute name for Attribute Scope Query

.PARAMETER scriptblockFilter
Scriptblock that gets an entry as parameter, analyze the entry and returns whether the indicator needs this entry or not
This parameter is used to do analysis of the ldap results before building a list of object to reduce memory usage

.EXAMPLE
Get all users with their SamAccountName and UserAccountControl
$DN = Get-DN test.lab
Search-AD -dnsDomain test.lab -attributes "samaccountname", "useraccountcontrol" -baseDN $DN -scope "Subtree" -filter "(&(objectCategory=person)(objectClass=user))"
#>
Function Search-AD
{
    param(
        [parameter(Mandatory = $true)]
        [string]$dnsDomain,

        [parameter(Mandatory = $false)]
        [string[]]$attributes = "",

        [parameter(Mandatory = $false)]
        [string]$baseDN,

        [parameter(Mandatory = $false)]
        [string]$scope = "subtree",

        [parameter(Mandatory = $false)]
        [switch]$includeDeleted,

        [parameter(Mandatory = $true)]
        [string]$filter,

        [parameter(Mandatory = $false)]
        [int]$pageSize = 1000,

        [parameter(Mandatory = $false)]
        [string]$ASQ,

        [parameter(Mandatory = $false)]
        [scriptblock[]]$scriptblockFilter
    )
    Out-Log -Operation $MyInvocation.MyCommand.Name -Data $PSBoundParameters

    Add-Type -Path "$env:windir\Microsoft.NET\Framework64\v4.0.30319\system.directoryservices.protocols.dll"

    # Update filter to support a LDAP filter for a distinguished name which contains an asterisk
    if ($filter -match "distinguishedName=") {
        $filter = $filter.Replace("*","\2A")
    }

    # We don't want the client to timeout in large environments
    $Timeout = [System.TimeSpan]::FromDays(10000)
    $results = New-Object "System.Collections.Generic.List[System.DirectoryServices.Protocols.SearchResultEntry]"
    [System.DirectoryServices.Protocols.LdapConnection] $conn = New-Object System.DirectoryServices.Protocols.LdapConnection($dnsDomain)
    [System.DirectoryServices.Protocols.SearchRequest] $search = New-Object System.DirectoryServices.Protocols.SearchRequest($baseDN,$filter,$scope,$attributes)
    $search.TimeLimit = $Timeout
    [System.DirectoryServices.Protocols.PageResultRequestControl] $pageRequest = New-Object System.DirectoryServices.Protocols.PageResultRequestControl($pageSize)
    [void]$search.Controls.Add($pageRequest)

    [System.DirectoryServices.Protocols.SearchOptionsControl] $searchOptions = New-Object System.DirectoryServices.Protocols.SearchOptionsControl([System.DirectoryServices.Protocols.SearchOption]::DomainScope)
    [void]$search.Controls.Add($searchOptions)

    # Those flags let us query nTSecurityDescriptor when we don't have access to the SACL
    [System.DirectoryServices.Protocols.SecurityDescriptorFlagControl] $searchSecurityFlags = New-Object System.DirectoryServices.Protocols.SecurityDescriptorFlagControl(7)
    [void]$search.Controls.Add($searchSecurityFlags)

    If ($includeDeleted) {
        [System.DirectoryServices.Protocols.ShowDeletedControl] $showDeletedControl = New-Object System.DirectoryServices.Protocols.ShowDeletedControl
        [void]$search.Controls.Add($showDeletedControl)
    }

    # Attribute Scope Query
    if ($ASQ) {
        [System.DirectoryServices.Protocols.AsqRequestControl] $asqControl = New-Object System.DirectoryServices.Protocols.AsqRequestControl($ASQ)
        [void]$search.Controls.Add($asqControl)
    }

    [int] $pageCount = 0

    if ($attributes) {
        $rangeRetrievalNeeded = Test-RangeRetrievalNeed -Attributes $attributes
    }
    while ($true) {
        $pageCount++
        [System.DirectoryServices.Protocols.SearchResponse] $response = [System.DirectoryServices.Protocols.SearchResponse]$conn.SendRequest($search,$Timeout)
        [System.DirectoryServices.Protocols.PageResultResponseControl] $pageResponse = $response.Controls | Where-Object {$_.Type -eq "1.2.840.113556.1.4.319"}
        if ($response.Entries.Count -gt 0) {
            foreach ($entry in $response.Entries) {
                if ($rangeRetrievalNeeded) {
                    $entry = RangeQueryHelper -Attributes $Attributes -Entries $entry
                }
                if ($attributes) {
                    $entry = Bandage -Attributes $Attributes -Entries $entry
                }
                if ($scriptblockFilter) {
                    foreach ($scriptBlock in $scriptblockFilter) {
                        if ($entry) {
                            $entry = & $scriptblock $entry
                        }
                    }
                    if ($entry) {
                        $results.Add($entry)
                    }
                }
                else {
                    $results.Add($entry)
                }
            }
        }
        if ($pageResponse.Cookie.Length -eq 0) {
            break
        }
        $pageRequest.Cookie = $pageResponse.Cookie
    }
    return $results
}

<#
.SYNOPSIS
Gets the distinguished name of a given domain

.PARAMETER dnsDomain
DNS name of the domain whose distinguished name we want

.EXAMPLE
Get-DN "test.lab"
#>
Function Get-DN {
    param(
        [parameter(Mandatory = $true)]
        [string]$dnsDomain
    )
    Out-Log -Operation $MyInvocation.MyCommand.Name -Data $PSBoundParameters

    $domContext = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext([System.DirectoryServices.ActiveDirectory.DirectoryContextType]::Domain, $dnsDomain);
    $selectedDomain = [System.DirectoryServices.ActiveDirectory.Domain]::GetDomain($domContext);
    return $selectedDomain.GetDirectoryEntry().Properties["distinguishedName"][0].ToString();
}

<#
.SYNOPSIS
Gets the SID of a given domain

.DESCRIPTION
Queries a given domain for his SID

.PARAMETER dnsDomain
DNS name of the domain whose SID we want

.EXAMPLE
Get-DomainSID "test.lab"
#>
Function Get-DomainSID{ # Gets the domain SID
    param(
        [parameter(Mandatory = $true)]
        [string]$dnsDomain
    )
    Out-Log -Operation $MyInvocation.MyCommand.Name -Data $PSBoundParameters

    $domContext = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext([System.DirectoryServices.ActiveDirectory.DirectoryContextType]::Domain, $dnsDomain);
    $selectedDomain = [System.DirectoryServices.ActiveDirectory.Domain]::GetDomain($domContext);
    $sid = $selectedDomain.GetDirectoryEntry().Properties["objectSID"][0]
    return (New-Object System.Security.Principal.SecurityIdentifier @($sid,0)).Value
}


<#
.SYNOPSIS
Translate User Account Control value into string

.DESCRIPTION
Translate each of the active flags in a User Account Control value and returns a string of all of them

.PARAMETER uac
User Account Control value to be translated

.EXAMPLE
Get-UACSet 514
#>
Function Get-UACSet{
    param(
        [parameter(Mandatory = $true)]
        [int]$uac
    )
    Out-Log -Operation $MyInvocation.MyCommand.Name -Data $PSBoundParameters

    $uacvalues = @{
        1 = "Script"
        2 = "AccountDisabled"
        8 = "HomeDirectoryRequired"
        16 = "AccountLockedOut"
        32 = "PasswordNotRequired"
        64 = "PasswordCannotChange"
        128 = "EncryptedTextPasswordAllowed"
        256 = "TempDuplicateAccount"
        512 = "NormalAccount"
        2048 = "InterDomainTrustAccount"
        4096 = "WorkstationTrustAccount"
        8192 = "ServerTrustAccount"
        65536 = "PasswordDoesNotExpire"
        131072 = "MnsLogonAccount"
        262144 = "SmartCardRequired"
        524288 = "TrustedForDelegation"
        1048576 = "AccountNotDelegated"
        2097152 = "UseDesKeyOnly"
        4194304 = "DontRequirePreauth"
        8388608 = "PasswordExpired"
        16777216 = "TrustedToAuthenticateForDelegation"
        33554432 = "NoAuthDataRequired"
        67108864 = "PartialSecretsAccount"
    }

    $uaclist = [System.Collections.ArrayList]@()
    foreach($k in $uacvalues.Keys){
        if($uac -band $k){
            $uaclist.Add($uacvalues[$k]) | out-null
        }
    }
    $uaclist
}

<#
.SYNOPSIS
Performs range retrieval when needed

.DESCRIPTION
Checks if the results of Search-AD has partial results for some attributes that need a ranged retrieval

.PARAMETER Attributes
Array of the attributes requested from Search-AD

.PARAMETER Entries
Array of results returned from Search-AD that will be checked to valid full attribute result

.EXAMPLE
RangeQueryHelper -Attributes "SamAccountName" -Entries $Entries

.NOTES
This function is internal and not exported

#>
Function RangeQueryHelper {
    param(
        [parameter(Mandatory = $true)]
        [string[]]$Attributes,

        [parameter(Mandatory = $true)]
        [System.DirectoryServices.Protocols.SearchResultEntry[]]$Entries
    )

    foreach ($entry in $Entries){
        $resRangeResults = $entry.Attributes.AttributeNames.Where({$PSItem -match "^[\w-]+;range=0-\d+$"})
        foreach ($attribute in $resRangeResults){
            $attribute -match "^([\w-]+);range=0-\d+$" | Out-Null
            $attrName = $Matches[1]
            $attrData =  $entry.Attributes[$attribute]
            $inc = $attrData.count # default is 1500
            $start = 0
            $doneSearching = $false
            while (!($doneSearching)) {
                $start += $inc
                $end = ($start + $inc -1)
                $search.Filter = "distinguishedName=$($entry.DistinguishedName)"
                $search.Scope = "base"
                $search.DistinguishedName = $entry.DistinguishedName
                $attr = "$attrName;range=$start-$end"
                $search.Attributes.Clear()
                $search.Attributes.Insert(0,"$attr")
                $response = [System.DirectoryServices.Protocols.SearchResponse]$conn.SendRequest($search)

                # Number of entries should be 1 or something unexpected happened
                foreach ($resEntry in $response.Entries) {
                    if ($resEntry.Attributes[$attr]) {
                        $attrData += $resEntry.Attributes[$attr]
                        continue
                    }

                    # In case we reached the last chunk or we are stuck in the while loop
                    ElseIf ($resEntry.Attributes["$attrName;range=$start-*"]) {
                        $attr = "$attrName;range=$start-*"
                        $attrData += $resEntry.Attributes[$attr]
                    }
                    $doneSearching = $true
                }
            }

            # Done going through the range, now fill the original attribute
            $directoryAttribute = New-Object System.DirectoryServices.Protocols.DirectoryAttribute($attrName,$attrData)
            $entry.Attributes[$attrName] = $directoryAttribute

            # Remove the partial attribute
            $entry.Attributes.Remove($attribute)
        }
    }
    return $Entries
}

# THIS FUNCTION IS TEMPORARY
# This is a temporary work around for cases that the results is missing some attributes
Function Bandage {
    param(
        [parameter(Mandatory = $true)]
        [string[]]$Attributes,

        [parameter(Mandatory = $true)]
        [System.DirectoryServices.Protocols.SearchResultEntry[]]$Entries
    )
    $attributesCount = $Attributes.Count
    foreach ($entry in $Entries) {
        if ($attributesCount -ne ($entry.Attributes.AttributeNames | Where-Object {!($_ -match ";range=")}).Count) {
            foreach ($attribute in $Attributes) {
                if (!($entry.Attributes.contains($attribute))) {
                    $entry.Attributes.Add($attribute,"")
                }
            }
        }
    }
    return $Entries
}

<#
.SYNOPSIS
Translate SearchFlag value into string

.DESCRIPTION
Translate each of the active flags in a SearchFlag value and returns a string of all of them

.PARAMETER searchFlags
SearchFlag value to be translated

.EXAMPLE
Get-ADSearchFlag 904
#>
Function Get-ADSearchFlag {
    param(
        [parameter(Mandatory = $true)]
        [int]$searchFlags
    )
    Out-Log -Operation $MyInvocation.MyCommand.Name -Data $PSBoundParameters

    $searchflagvalues = @{
        1 = "Index"
        2 = "ContainerIndex"
        4 = "ANR"
        8 = "PreserveOnDelete"
        16 = "Copy"
        32 = "TupleIndex"
        64 = "SubtreeIndex"
        128 = "Confidential"
        256 = "NeverValueAudit"
        512 = "RODCFiltered"
        1024 = "ExtendedLinkTracking"
        2048 ="BaseOnly"
        4096 ="PartitionSecret"

    }

    $searchflagslist = [System.Collections.ArrayList]@()
    foreach($k in $searchflagvalues.Keys){
        if($searchFlags -band $k){
            $searchflagslist.Add($searchflagvalues[$k]) | out-null
        }
    }
    $searchflagslist
}

<#
.SYNOPSIS
Trim and replace special characters from a string and convert it to xml

.DESCRIPTION
Trim and replace special characters from a string and convert it to xml

.PARAMETER Xml
Xml in a string format to be converted to xml format

.EXAMPLE
Convert-StringToXml "<pszAttributeName>member</pszAttributeName>"
#>
Function Convert-StringToXml {
    param (
        [parameter(Mandatory = $true)]
        [string]$Xml
    )

    $Xml = $Xml -replace "`0$" -replace "&", "&amp;" -replace "`"", "&quot;" -replace "'", "&apos;" -replace "\\<", "&lt;" -replace "\\>", "&gt;"
    $parsedXML = [xml]$Xml
    return $parsedXML
}

<#
.SYNOPSIS
Translate TrustAttributes value into string

.DESCRIPTION
Translate each of the active flags in a TrustAttributes value and returns a string of all of them

.PARAMETER TrustAttributes
TrustAttributes value to be translated

.EXAMPLE
Get-TrustAttributesSet 64
#>
Function Get-TrustAttributesSet {
    param (
        [parameter(Mandatory = $true)]
        [int]$TrustAttributes
    )
    Out-Log -Operation $MyInvocation.MyCommand.Name -Data $PSBoundParameters

    $trustAttributesValues = @{
        1 = "TRUST_ATTRIBUTE_NON_TRANSITIVE"
        2 = "TRUST_ATTRIBUTE_UPLEVEL_ONLY"
        4 = "TRUST_ATTRIBUTE_QUARANTINED_DOMAIN"
        8 = "TRUST_ATTRIBUTE_FOREST_TRANSITIVE"
        16 = "TRUST_ATTRIBUTE_CROSS_ORGANIZATION"
        32 = "TRUST_ATTRIBUTE_WITHIN_FOREST"
        64 = "TRUST_ATTRIBUTE_TREAT_AS_EXTERNAL"
        128 = "TRUST_ATTRIBUTE_USES_RC4_ENCRYPTION"
        512 = "TRUST_ATTRIBUTE_CROSS_ORGANIZATION_NO_TGT_DELEGATION"
        1024 = "TRUST_ATTRIBUTE_PIM_TRUST"
        2048 = "TRUST_ATTRIBUTE_CROSS_ORGANIZATION_ENABLE_TGT_DELEGATION"
    }

    $attrList = [System.Collections.ArrayList]@()
    foreach($k in $trustAttributesValues.Keys){
        if($TrustAttributes -band $k){
            $attrList.Add($trustAttributesValues[$k]) | out-null
        }
    }
    $attrList
}

<#
.SYNOPSIS
Gets an entry with msds-ReplAttributeMetadata, filter just for the relevant attribute and returns it in a new attribute called attributemetadata

.DESCRIPTION
Gets an entry with msds-ReplAttributeMetadata, filter just for the relevant attribute and returns it in a new attribute called attributemetadata

.PARAMETER Entry
Entry with a msds-ReplAttributeMetadata that needs filter and map

.PARAMETER PropertyFilter
The attribute to filter for.

.PARAMETER StartOriginatingChangeThreshold
Starting time for time filter

.PARAMETER EndOriginatingChangeThreshold
End time for time filter

.PARAMETER MinimumVersionThreshold
Minimum version number that should be retrieved

.PARAMETER MaximumVersionThreshold
Maximum version number that should be retrieved

.EXAMPLE
Update-AttributeMetadata -Entry $entry -PropertyFilter "sidHistory" -MinimumVersion 2 -StartOriginatingChangeThreshold $startOriginatingChangeThreshold

#>
Function Update-AttributeMetadata {
    [CmdletBinding(SupportsShouldProcess)]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "", Justification="False negative")]
    param (
        [parameter(Mandatory = $true)]
        [System.DirectoryServices.Protocols.SearchResultEntry] $Entry,

        [parameter(Mandatory = $true)]
        [string] $PropertyFilter,

        [parameter(Mandatory = $false)]
        [string] $StartOriginatingChangeThreshold = "1000-01-01T00:00:00Z",

        [parameter(Mandatory = $false)]
        [string] $EndOriginatingChangeThreshold = "9999-01-01T00:00:00Z",

        [parameter(Mandatory = $false)]
        [string] $MinimumVersionThreshold = "0",

        [parameter(Mandatory = $false)]
        [string] $MaximumVersionThreshold = "99999"
    )
    $attributesObject = [System.Collections.ArrayList]@()
    if($PSCmdlet.ShouldProcess($Entry)){
        if(-not $Parsers[$PropertyFilter]) { $Parsers[$PropertyFilter] = [System.Text.RegularExpressions.Regex]::new(".*<pszattributename>$($PropertyFilter.ToLower())<\/pszattributename>.*", 'Compiled') }
        if ($Entry.Attributes.'msds-replattributemetadata') {
            $xml = $Entry.Attributes.'msds-replattributemetadata'.GetValues("string")
            if ($parsers[$PropertyFilter].IsMatch($xml.ToLower()))
            {
                foreach ($attr in $xml) {
                    $attrMetaData = ([xml]($attr -replace '\x00','')).DS_REPL_ATTR_META_DATA
                    if ($attrMetaData) {
                        if ($attrMetaData.pszAttributeName.ToLower() -match $propertyFilter.ToLower()) {
                            if ($attrMetaData.ftimeLastOriginatingChange -lt $EndOriginatingChangeThreshold `
                                    -and $attrMetaData.ftimeLastOriginatingChange -gt $StartOriginatingChangeThreshold )
                            {
                                $attributeMetadata = [PSCustomObject][Ordered] @{
                                    LastOriginatingChange = $attrMetaData.ftimeLastOriginatingChange
                                    AttributeName = $attrMetaData.pszAttributeName
                                    Version = $attrMetaData.dwVersion
                                    LastOriginatingDsaInvocationID = $attrMetadata.uuidLastOriginatingDsaInvocationID
                                    USNOriginatingChange = $attrMetadata.usnOriginatingChange
                                    USNLocalChange = $attrMetadata.usnLocalChange
                                    LastOriginatingDsaDN = $attrMetadata.pszLastOriginatingDsaDN
                                }
                                [void]$attributesObject.add($attributeMetadata)
                            }
                        }
                    }
                }
            }
            If ($attributesObject.Count -gt 0) {
                $Entry.Attributes["attributemetadata"] = $attributesObject
                $Entry.Attributes.Remove("msds-replattributemetadata")
                return $Entry
            }
            Else {
                return $null
            }
        }
    }
}

<#
.SYNOPSIS
Gets an entry with msds-ReplValueMetadata, filter just for the relevant Value and returns it in a new attribute called valuemetadata

.DESCRIPTION
Gets an entry with msds-ReplValueMetadata, filter just for the relevant attribute and returns it in a new attribute called valuemetadata

.PARAMETER Entry
Entry with a msds-ReplValueMetadata that needs filter and map

.PARAMETER PropertyFilter
The attribute to filter for.

.PARAMETER StartOriginatingChangeThreshold
Starting time for time filter

.PARAMETER EndOriginatingChangeThreshold
End time for time filter

.PARAMETER MinimumVersionThreshold
Minimum version number that should be retrieved

.PARAMETER MaximumVersionThreshold
Maximum version number that should be retrieved

.EXAMPLE
Update-ValueMetadata -Entry $entry -PropertyFilter "member" -MinimumVersion 2 -StartOriginatingChangeThreshold $startOriginatingChangeThreshold

#>
Function Update-ValueMetadata {
    [CmdletBinding(SupportsShouldProcess)]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "", Justification="False negative")]
    param (
        [parameter(Mandatory = $true)]
        [System.DirectoryServices.Protocols.SearchResultEntry] $Entry,

        [parameter(Mandatory = $true)]
        [string] $PropertyFilter,

        [parameter(Mandatory = $false)]
        [string] $StartOriginatingChangeThreshold = "1000-01-01T00:00:00Z",

        [parameter(Mandatory = $false)]
        [string] $EndOriginatingChangeThreshold = "9999-01-01T00:00:00Z",

        [parameter(Mandatory = $false)]
        [string] $MinimumVersionThreshold = "0",

        [parameter(Mandatory = $false)]
        [string] $MaximumVersionThreshold = "99999"
    )
    if($PSCmdlet.ShouldProcess($Entry)){
        if ($Entry.Attributes.'msds-replvaluemetadata') {
            $valuesObject = [System.Collections.ArrayList]@()
            foreach ($attr in $Entry.Attributes.'msds-replvaluemetadata'.GetValues("string")) {
                $attrMetaData = Convert-StringToXml $attr | Select-Object -ExpandProperty 'DS_REPL_VALUE_META_DATA' -ErrorAction SilentlyContinue
                if ($attrMetaData) {
                    if ($attrMetaData.pszAttributeName.ToLower() -match $propertyFilter.ToLower()) {
                        if ($attrMetaData.ftimeLastOriginatingChange -lt $EndOriginatingChangeThreshold `
                                -and $attrMetaData.ftimeLastOriginatingChange -gt $StartOriginatingChangeThreshold `
                                -and $attrMetaData.dwVersion -ge $MinimumVersionThreshold `
                                -and $attrMetaData.dwVersion -le $MaximumVersionThreshold) {
                            $attributeMetadata = [PSCustomObject][Ordered] @{
                                LastOriginatingChange = $attrMetaData.ftimeLastOriginatingChange
                                AttributeName = $attrMetaData.pszAttributeName
                                TimeDeleted = $attrMetaData.ftimeDeleted
                                TimeCreated = $attrMetaData.ftimeCreated
                                Version = $attrMetaData.dwVersion
                                LastOriginatingDsaInvocationID = $attrMetadata.uuidLastOriginatingDsaInvocationID
                                USNOriginatingChange = $attrMetadata.usnOriginatingChange
                                USNLocalChange = $attrMetadata.usnLocalChange
                                LastOriginatingDsaDN = $attrMetadata.pszLastOriginatingDsaDN
                                ParentDN = $Entry.DistinguishedName
                                ValueDN = $attrMetaData.pszObjectDn
                            }
                            [void]$valuesObject.add($attributeMetadata)
                        }
                    }
                }
            }
            If ($valuesObject.Count -gt 0) {
                $Entry.Attributes["valuemetadata"] = $valuesObject
                $Entry.Attributes.Remove("msds-replvaluemetadata")
                return $Entry
            }
            Else {
                return $null
            }
        }
    }
}

<#
.SYNOPSIS
Gets an entry with ntSecurityDescriptor, filter just the relevant ACEs and returns it in a new attribute

.DESCRIPTION
Gets an entry with ntSecurityDescriptor, filter just the relevant ACEs and returns it in a new attribute

.PARAMETER Entry
Entry with a ntSecurityDescriptor that needs filter and map

.PARAMETER GuidHTFilter
Hashtable of Guids to filter for. Use just if only specific object types are needed.

.PARAMETER PropagationFlagsFilter
Wanted propagation flags as string, divided by '|', to be filtered by

.PARAMETER AccessControlTypeFilter
Wanted Access control type (e.g. Allow)

.PARAMETER ExcludedSIDs
List of SIDs that are allowed and should be excluded

.PARAMETER RightsFilter
Access rights that we care about, divided by "|"
For example - "GenericAll|GenericWrite"

.EXAMPLE
Update-NTSecurityDescriptor -Entry $entry -RightsFilter "GenericAll|GenericWrite" -PropagationFlagsFilter "None|NoPropagateInherit"

#>
$codeUpdateNtSecurityDescriptor = @"
using System.DirectoryServices.Protocols;
using System.DirectoryServices;
using System.Collections.Generic;
using System.Collections;
using System;
using System.Security.Principal;
using System.Security.AccessControl;
using System.Text.RegularExpressions;
namespace UpdateNtSecurityDescriptor
{
    public class NtSecurityDescriptor
    {
        static public List<ActiveDirectoryAccessRule> Update(SearchResultEntry entry, Hashtable guidHTFilter, string propagationFlagsFilter, string accessControlTypeFilter, string []excludedSIDs, string rightsFilter, string inheritanceTypeFilter)
        {
            string identitySID = "";
            List<ActiveDirectoryAccessRule> relevantACEs = new List<ActiveDirectoryAccessRule>();
            byte[][] descriptorBytes= (byte[][])entry.Attributes["ntsecuritydescriptor"].GetValues(typeof(byte[]));
            ActiveDirectorySecurity securityDescriptor = new ActiveDirectorySecurity();
            securityDescriptor.SetSecurityDescriptorBinaryForm(descriptorBytes[0]);
            foreach (AuthorizationRule rule in securityDescriptor.GetAccessRules(true, true, typeof(NTAccount)))

            {
                ActiveDirectoryAccessRule oar = rule as ActiveDirectoryAccessRule;
                if (guidHTFilter.Keys.Count == 0 || guidHTFilter.Contains(oar.ObjectType.ToString()))

                {
                    if (!Regex.IsMatch(oar.PropagationFlags.ToString(),propagationFlagsFilter))
                    {
                        continue;
                    }
                    if (!Regex.IsMatch(oar.InheritanceType.ToString(),inheritanceTypeFilter))
                    {
                        continue;
                    }
                    try
                    {
                        NTAccount identityAccount = new NTAccount(oar.IdentityReference.Value);
                        identitySID = identityAccount.Translate(typeof(SecurityIdentifier)).Value;
                    }
                    catch
                    {
                        identitySID = oar.IdentityReference.Value;
                    }
                    bool containsSID = false;
                    foreach (string sid in excludedSIDs)
                    {
                        if (sid == identitySID)
                        {
                            containsSID = true;
                            break;
                        }
                    }
                    if (!(containsSID))
                    {
                        if (oar.AccessControlType.ToString() == accessControlTypeFilter)
                        {
                            if (Regex.IsMatch(oar.ActiveDirectoryRights.ToString(),rightsFilter))
                            {
                                relevantACEs.Add(oar);
                            }
                      }
                    }
                }
            }
            return relevantACEs;
        }
    }
}
"@
if ($PSVersionTable.PSEdition -eq "Core"){
    $referencingassembliesNtSD = ("System.DirectoryServices.Protocols","System.Collections","System.Security","System.DirectoryServices","System.Security.AccessControl","System.Text.RegularExpressions","System.Collections.NonGeneric","System.IO.FileSystem.AccessControl","System.Security.Principal.Windows")
}
else{
    $referencingassembliesNtSD = ("System.DirectoryServices.Protocols","System.Collections","System.Security","System.DirectoryServices")
}
Add-Type -TypeDefinition $codeUpdateNtSecurityDescriptor -Language CSharp -ReferencedAssemblies $referencingassembliesNtSD


Function Update-NTSecurityDescriptor {
    [CmdletBinding(SupportsShouldProcess)]
    param (
        [parameter(Mandatory = $true)]
        [System.DirectoryServices.Protocols.SearchResultEntry] $Entry,

        [parameter(Mandatory = $false)]
        [Hashtable] $GuidHTFilter = @{},

        [parameter(Mandatory = $false)]
        [string] $PropagationFlagsFilter,

        [parameter(Mandatory = $false)]
        [string] $InheritanceTypeFilter = "None|All|Descendents|Children|SelfAndChildren",

        [parameter(Mandatory = $false)]
        [string] $AccessControlTypeFilter = "Allow",

        [parameter(Mandatory = $false)]
        [string[]] $ExcludedSIDs = @(),

        [parameter(Mandatory = $false)]
        [string] $RightsFilter
    )

    $relevantACEs = [UpdateNtSecurityDescriptor.NtSecurityDescriptor]::Update($Entry,$GuidHTFilter,$PropagationFlagsFilter,$AccessControlTypeFilter,$ExcludedSIDs,$RightsFilter,$InheritanceTypeFilter)
    if ($relevantACEs) {
        $Entry.Attributes["aces"] = $relevantACEs

        # Remove the full ntSecurityDescriptor attribute
        $Entry.Attributes.Remove("ntsecuritydescriptor")
        return $Entry
    }
    else {
        return $null
    }
}
<#
.SYNOPSIS
Check whether a multi valued attribute was requested

.DESCRIPTION
Checking for a need for range retrieval query will require iterating over every entry in the results,
this function should prevent doing this check when no multi valued attributed was requested and therefore no chance for range retrieval need

.PARAMETER Attributes
Array of the attributes requested from Search-AD

.EXAMPLE
Test-RangeRetrievalNeed -Attributes "SamAccountName"

.NOTES
This function is internal and not exported

#>
Function Test-RangeRetrievalNeed {
    param (
        [parameter(Mandatory = $true)]
        [string[]]$Attributes
    )

    # Determine if ranged search might be needed, trying to reduce cost of the check for ranged results
    $rangedSearch = $false
    $rangedAttributes = @("member","memberof","serviceprincipalname", "*", "msds-replattributemetadata", "msds-replvaluemetadata", "serviceprincipalname", "msds-revealedlist") # Attributes that are multi valued and might have more then 1500 entries
    foreach ($attribute in $Attributes) {
        if($rangedAttributes -contains $attribute.ToLower()) {
            $rangedSearch = $true
            break
        }
    }
    return $rangedSearch
}

<#
.SYNOPSIS
Returns a list of SIDs or DNs of every protected group members in each domain
.DESCRIPTION
Returns a list of SIDs or DNs of every protected group members in each domain
.PARAMETER ForestName
Name of the forest in FQDN
.PARAMETER DomainNames
Array of existing domains
.PARAMETER ReturnSIDs
Only specify if SIDs are needed
.EXAMPLE
Get-AdminsInForest $ForestName $DomainNames
#>
Function Get-AdminsInForest {
    param (
        [parameter(Mandatory = $true)]
        [string]$ForestName,

        [parameter(Mandatory = $true)]
        [string[]]$DomainNames,

        [parameter(Mandatory = $false)]
        [switch]$ReturnSIDs
    )
    Out-Log -Operation $MyInvocation.MyCommand.Name -Data $PSBoundParameters

    $forestPrivilegedGroups = @("519","518")

    $forestDN = Get-DN $ForestName
    $privilegedGroupsDN = [System.Collections.ArrayList]@()
    $forestSID = Get-DomainSID $ForestName
    $admins = [System.Collections.ArrayList]@()
    $failedToEnumerate = [System.Collections.ArrayList]@()
    $largeGroups = [System.Collections.ArrayList]@()
    $EnumeratedDN = [System.Collections.ArrayList]@()

    # Get a list of the forest groups DN
    foreach ($groupRID in $forestPrivilegedGroups) {
        $forestGroupObject = Search-AD -dnsDomain $ForestName -baseDN $forestDN -scope "subtree" `
            -filter "(&(objectSid=$forestSID-$groupRID)(objectCategory=group))"
        if ($forestGroupObject) {
            [void]$privilegedGroupsDN.add($forestGroupObject.DistinguishedName)
        }
    }

    # Build filter for primary group ID search
    $privilegedRIDs = [System.Collections.ArrayList]@()
    [void]$privilegedRIDs.AddRange(@("551","552","548","549","550","544","512","516","518","519","521"))

    $primaryGroupFilter = "(|"
    foreach($privilegedRID in $privilegedRIDs) {
        $primaryGroupFilter += "(primaryGroupID=$privilegedRID)"
    }
    $primaryGroupFilter += ")"

    foreach ($domain in $DomainNames) {
        $DN = Get-DN $domain
        $domainSID = Get-DomainSID $domain

        $privilegedGroups = @("S-1-5-32-551","S-1-5-32-552","S-1-5-32-548","S-1-5-32-549","S-1-5-32-550","S-1-5-32-544",
            "$domainSID-512","$domainSID-516","$domainSID-521")

        # Get a list of the groups DN
        foreach ($sid in $privilegedGroups) {
            $groupObject = Search-AD -dnsDomain $domain -baseDN $DN -scope "subtree" `
                -filter "(&(objectSid=$sid)(objectCategory=group))"
            if ($groupObject) {
                [void]$privilegedGroupsDN.add($groupObject.DistinguishedName)
            }
        }

        # Get users who are admins via primaryGroupID
        $primarySearchParams = @{
            dnsDomain = $domain
            attributes = "objectsid"
            baseDN = $DN
            scope = "subtree"
            filter = $primaryGroupFilter
        }

        $primaryResults = Search-AD @primarySearchParams

        foreach ($primaryResult in $primaryResults) {
            if (!($EnumeratedDN.Contains($primaryResult.DistinguishedName))) {
                [void]$EnumeratedDN.Add($primaryResult.DistinguishedName)
            }

            if ($ReturnSIDs) {
                $adminSID = (New-Object System.Security.Principal.SecurityIdentifier @($primaryResult.Attributes."objectsid".GetValues("byte[]")[0],0)).Value
                if (!($admins.Contains($adminSID))) {
                    [void]$admins.Add($adminSID)
                }
            }
        }
    }

    foreach($groupDN in $privilegedGroupsDN) {
        try {
            $groupDomain = $groupDN.Substring($groupDN.IndexOf("DC=")).replace("DC=","").replace(",",".")
            $groupDomainNETBios = Get-NetBIOSName -dnsDomain $groupDomain -forestName $ForestName

            $groupMemberSearchParam = @{
                dnsDomain = $groupDomain
                attributes = @("msds-membertransitive", "objectsid", "samAccountName")
                baseDN = $groupDN
                scope = "base"
                filter = "distinguishedName=$groupDN"
            }

            $groupMembers = Search-AD @groupMemberSearchParam

            if ($ReturnSIDs) {
                $groupSID = (New-Object System.Security.Principal.SecurityIdentifier @($groupMembers.Attributes."objectsid".GetValues("byte[]")[0],0)).Value
                if ($admins.Contains($groupSID)) {
                    continue
                }
                [void]$admins.Add($groupSID)
            }
            if (!$EnumeratedDN.Contains($groupDN)) {
                [void]$EnumeratedDN.Add($groupDN)
            }

            if ($groupMembers.Attributes."msds-membertransitive;range=0-4499") {
                $groupSAM = $groupMembers.Attributes.samaccountname[0]
                [void]$largeGroups.Add("$groupDomainNETBios\$groupSAM")

                $memberAttribute = "msds-membertransitive;range=0-4499"
            }
            else {
                $memberAttribute = "msds-membertransitive"
            }

            if (!($groupMembers.Attributes."msds-membertransitive"[0]) -and !($memberAttribute -match "range")) {
                continue
            }

            foreach ($adminDN in $groupMembers.Attributes.$memberAttribute.GetValues("string")) {
                if ($EnumeratedDN.Contains($adminDN)) {
                    continue
                }
                [void]$EnumeratedDN.Add($adminDN)
                if (!$ReturnSIDs) {
                    continue
                }
                try {
                    $memberDomain = $adminDN.Substring($adminDN.IndexOf("DC=")).replace("DC=","").replace(",",".")

                    $adminSearchParam = @{
                        dnsDomain = $memberDomain
                        attributes = "objectsid"
                        baseDN = $adminDN
                        scope = "base"
                        filter = "distinguishedName=$adminDN"
                    }

                    $adminObject = Search-AD @adminSearchParam

                    $adminSID = (New-Object System.Security.Principal.SecurityIdentifier @($adminObject.Attributes."objectsid".GetValues("byte[]")[0],0)).Value
                    if ($admins.Contains($adminSID)) {
                        continue
                    }
                    [void]$admins.Add($adminSID)
                }
                catch {
                    [void]$failedToEnumerate.Add($adminDN)
                }
            }
        }
        catch {
            [void]$failedToEnumerate.Add($groupDN)
        }
    }
    if (!$ReturnSIDs) {
        $admins = $EnumeratedDN
    }
    return $admins, $failedToEnumerate, $largeGroups
}

<#
.SYNOPSIS
Update output ArrayList to include additional fields
.DESCRIPTION
Returns an updated version of the indicator's output ArrayList if more processing is needed
.PARAMETER OutputObjects
ArrayList of objects to be returned by the indicator
.PARAMETER StartAttackWindow
DateTime specifying the start of the attack
.PARAMETER EndAttackWindow
DateTime specifying the end of the attack
.EXAMPLE
Update-IndicatorOutputAttackWindow -OutputObjects $outputObjects -StartAttackWindow $StartAttackWindow -EndAttackWindow $EndAttackWindow
#>
Function Update-IndicatorOutputAttackWindow {
    [OutputType([System.Collections.ArrayList])]
    [CmdletBinding(SupportsShouldProcess)]
    param (
        $ThisOutput,

        $StartAttackWindow,

        $EndAttackWindow,

        $Metadata,

        [switch]$ValueMetadata,

        [switch]$ExcludeTimestamp
    )
    if($PSCmdlet.ShouldProcess($Metadata.ValueDN)) {
        if(!($ThisOutput)) {
            return $null
        }

        if ($StartAttackWindow -and $EndAttackWindow) {
            if(!($ThisOutput.psobject.Properties.name -contains "EventTimestamp")) {
                if (!($Metadata)) {
                    return $null
                }
                $timestamp = [DateTime]::Parse($Metadata.LastOriginatingChange).ToUniversalTime()
                $ThisOutput | Add-Member -Type NoteProperty -Name "EventTimestamp" -Value $timestamp
            }
            if ($ThisOutput."EventTimestamp" -gt $StartAttackWindow -and $ThisOutput."EventTimestamp" -lt $EndAttackWindow) {
                $ThisOutput | Add-Member -Type NoteProperty -Name "InAttackWindow" -Value "True"
            }
            else {
                $ThisOutput | Add-Member -Type NoteProperty -Name "InAttackWindow" -Value "False"
            }
        }
        else {
            if(!($ThisOutput.psobject.Properties.name -contains "EventTimestamp") -and (!($ExcludeTimestamp))) {
                if (!($Metadata)) {
                    return $null
                }
                $timestamp = [DateTime]::Parse($Metadata.LastOriginatingChange).ToUniversalTime()
                $ThisOutput | Add-Member -Type NoteProperty -Name "EventTimestamp" -Value $timestamp
            }
        }
    }
}

<#
.SYNOPSIS
Proxy to the Search-AD function

.DESCRIPTION
Utilize Search-AD to send ldap queries but applies some logic before calling search-ad to split into several calls

.PARAMETER dnsDomain
DNS name of the domain that will be searched against

.PARAMETER attributes
Attributes to return from the search, should be lowercase as convention

.PARAMETER baseDN
Root distinguished name for the search

.PARAMETER scope
Scope of the search

.PARAMETER filter
Filter to be applied to the search

.PARAMETER pageSize
Page size for the search
Default = 1000

.PARAMETER ASQ
Attribute name for Attribute Scope Query

.PARAMETER scriptblockFilter
Scriptblock that gets an entry as parameter, analyze the entry and returns whether the indicator needs this entry or not
This parameter is used to do analysis of the ldap results before building a list of object to reduce memory usage

.PARAMETER MetaType
For PK-PRE indicators, chose whether we should search on msds-replattributemetadata or msds-replvaluemetadata

.PARAMETER StartAttackWindow
Start time of the time window supplied by PK-PRE

.PARAMETER EndAttackWindow
End time of the time window supplied by PK-PRE

.PARAMETER PropertyFilter
Attribute name of the attribute we are looking for in the metadata (for PK-PRE indicators)

.PARAMETER StartMetaFilter
Start time of the time window supplied by the indicator in his normal case (for indicators that utilized metadata before PK-PRE)

.PARAMETER EndMetaFilter
End time of the time window supplied by the indicator in his normal case (for indicators that utilized metadata before PK-PRE)

.PARAMETER MinimumVersionThreshold
Minimum dwVersion to filter by in the metadata (for indicators that utilized metadata before PK-PRE)

.PARAMETER MaximumVersionThreshold
Maximum dwVersion to filter by in the metadata (for indicators that utilized metadata before PK-PRE)

.EXAMPLE
Get all users with their SamAccountName and UserAccountControl
$DN = Get-DN test.lab
Search-ADHelper -dnsDomain test.lab -attributes "samaccountname", "useraccountcontrol" -baseDN $DN -scope "Subtree" -filter "(&(objectCategory=person)(objectClass=user))"
#>
Function Search-ADHelper
{
    param(
        [parameter(Mandatory)]
        [string] $dnsDomain,

        [string[]] $attributes = "",

        [string] $baseDN,

        [string] $scope = "subtree",

        [parameter(Mandatory)]
        [string] $filter,

        [int] $pageSize = 1000,

        [string] $ASQ,

        [scriptblock[]] $scriptblockFilter = @(),

        [string] $MetaType = "Attribute",

        $StartAttackWindow,

        $EndAttackWindow,

        [string] $PropertyFilter,

        [DateTime] $StartMetaFilter = "Monday, 1 January 0001 0:00:00",

        [DateTime] $EndMetaFilter = "Friday, 31 December 9999 23:59:59",

        [string] $MinimumVersionThreshold = "0",

        [string] $MaximumVersionThreshold = "99999",

        [switch] $ExcludeScriptblock
    )
    Out-Log -Operation $MyInvocation.MyCommand.Name -Data $PSBoundParameters

    $attributes = $attributes | ForEach-Object {$_.ToLower()}
    $searchADParameters = @{
        dnsDomain = $dnsDomain
        attributes = $attributes
        baseDN = $baseDN
        scope = $scope
        filter = $filter
        pageSize = $pageSize
        ASQ = $ASQ
        scriptblockFilter = $scriptblockFilter
    }

    $metaDateFormat = "yyyy-MM-ddTHH:mm:ssZ"

    if ($PropertyFilter) {
        if (!($ExcludeScriptblock) -or ($StartAttackWindow -and $EndAttackWindow)) {
            # Check which is earlier, the indicator default start date or the start of the attack window
            if ($StartAttackWindow -and $StartAttackWindow -lt $StartMetaFilter) {
                $StartMetaFilter = $StartAttackWindow
            }

            # updateMetadataArguments will be used when using $updateMetadata.GetNewClosure() although we are not passing it anywhere
            $updateMetadataArguments = @{
                PropertyFilter = $PropertyFilter
                MinimumVersionThreshold = $MinimumVersionThreshold
                MaximumVersionThreshold = $MaximumVersionThreshold
                StartOriginatingChangeThreshold = get-date $StartMetaFilter -Format $metaDateFormat
                EndOriginatingChangeThreshold = get-date $EndMetaFilter -Format $metaDateFormat
            }

            # This line doesn't really do anything, it is just to bypass the powershell script analyzer saying this variable is assigned but not used
            $updateMetadataArguments.MinimumVersionThreshold = $MinimumVersionThreshold

            # We need to make sure we are getting the metadata
            if ($MetaType -eq "attribute") {
                if(!($attributes.Contains("msds-replattributemetadata"))) {
                    $attributes += "msds-replattributemetadata"
                }
            }
            elseif($MetaType -eq "value") {
                if(!($attributes.Contains("msds-replvaluemetadata"))) {
                    $attributes += "msds-replvaluemetadata"
                }
            }
            $searchADParameters.attributes = $attributes
            $updateMetadata = UpdateMetaHelper -MetaType $MetaType

            $searchADParameters.scriptblockFilter += $updateMetadata.GetNewClosure()
            return Search-AD @searchADParameters
        }
    }
    # We hit this code if we don't have PropertyFilter or we were provided $excludeScriptblockFitler AND were not provided with an attack window
    return Search-AD @searchADParameters
}

<#
.SYNOPSIS
Build scriptblock to be passed by Search-ADHelper to Search-AD base on $MetaType parameter

.DESCRIPTION
Build scriptblock to be passed by Search-ADHelper to Search-AD base on $MetaType parameter

.PARAMETER MetaType
For PK-PRE indicators, chose whether we should search on msds-replattributemetadata or msds-replvaluemetadata

.Example
$updateMetadata = UpdateMetaHelper -MetaType $MetaType
#>
Function UpdateMetaHelper {
    param (
        [string]$MetaType
    )
    if ($MetaType -eq "Attribute") {
        $updateMetaSB = {
            param ([System.DirectoryServices.Protocols.SearchResultEntry] $Entry)
            Update-AttributeMetadata @updateMetadataArguments -entry $Entry
        }
    }
    elseif ($MetaType -eq "Value") {
        $updateMetaSB = {
            param ([System.DirectoryServices.Protocols.SearchResultEntry] $Entry)
            Update-ValueMetadata @updateMetadataArguments -entry $Entry
        }
    }
    return $updateMetaSB
}

Function ConvertTo-ErrorResult {
    <#
    .SYNOPSIS
        Provides unified global exception handling for all indicators
    #>
    param (
        $ErrorObject
    )

    $result = New-Object Semperis.PSSecurityIndicatorResult.SecurityIndicatorResult

    $result.Status = 'Error'
    $result.ResultMessage = $ErrorObject.Exception.Message
    $result.Remediation = 'None'

    Write-Error "$($ErrorObject.Exception.Message) $($ErrorObject.ScriptStackTrace)" -ErrorAction Continue

    return $result
}

<#
.SYNOPSIS
Get a string representation of MsDS-SupportedEncryptionTypes from its decimal value

.DESCRIPTION
Get a string representation of MsDS-SupportedEncryptionTypes from its decimal value

.PARAMETER SupportedEncryptionTypesValue
The decimal value of MsDS-SupportedEncryptionTypes

.EXAMPLE
Get-SupportedEncryptionTypesSet $supportedEncryptionType

#>
Function Get-SupportedEncryptionTypesSet{
    param(
        [parameter(Mandatory = $true)]
        [int]$SupportedEncryptionTypesValue
    )
    Out-Log -Operation $MyInvocation.MyCommand.Name -Data $PSBoundParameters

    $SupportedEncryptionTypesKeys = @{
        0 = "RC4_HMAC_MD5"
        1 = "DES_DES_CBC_CRC"
        2 = "DES_CBC_MD5"
        4 = "RC4_HMAC_MD5"
        8 = "AES 128"
        16 = "AES 256"
    }

    $supportedEncryptionTypeList = [System.Collections.ArrayList]@()

    foreach($k in $SupportedEncryptionTypesKeys.Keys){
        if($SupportedEncryptionTypesValue -band $k){
            $supportedEncryptionTypeList.Add($SupportedEncryptionTypesKeys[$k]) | out-null
        }
    }
    $supportedEncryptionTypeList
}

<#
.SYNOPSIS
Get a string representation of MsDS-SupportedEncryptionTypes from its decimal value

.DESCRIPTION
Get a string representation of MsDS-SupportedEncryptionTypes from its decimal value

.PARAMETER SupportedEncryptionTypesValue
The decimal value of MsDS-SupportedEncryptionTypes

.EXAMPLE
Get-SupportedEncryptionTypesSet $supportedEncryptionType

#>
Function Get-SupportedEncryptionTypesSet{
    param(
        [parameter(Mandatory = $true)]
        [int]$SupportedEncryptionTypesValue
    )
    Out-Log -Operation $MyInvocation.MyCommand.Name -Data $PSBoundParameters

    $SupportedEncryptionTypesKeys = @{
        0 = "RC4_HMAC_MD5"
        1 = "DES_DES_CBC_CRC"
        2 = "DES_CBC_MD5"
        4 = "RC4_HMAC_MD5"
        8 = "AES 128"
        16 = "AES 256"
    }

    $supportedEncryptionTypeList = [System.Collections.ArrayList]@()

    foreach($k in $SupportedEncryptionTypesKeys.Keys){
        if($SupportedEncryptionTypesValue -band $k){
            $supportedEncryptionTypeList.Add($SupportedEncryptionTypesKeys[$k]) | out-null
        }
    }
    $supportedEncryptionTypeList
}

$AAD_PRIVILEGED_ROLES = @{
    "62e90394-69f5-4237-9190-012177145e10" = "Global administrator"
    "9b895d92-2cd3-44c7-9d02-a6ac2d5ea5c3" = "Application administrator"
    "c4e39bd9-1100-46d3-8c65-fb160da0071f" = "Authentication administrator"
    "b0f54661-2d74-4c50-afa3-1ec803f12efe" = "Billing administrator"
    "158c047a-c907-4556-b7ef-446551a6b5f7" = "Cloud application administrator"
    "b1be1c3e-b65d-4f19-8427-f6fa0d97feb9" = "Conditional Access administrator"
    "29232cdf-9323-42fd-ade2-1d097af3e4de" = "Exchange administrator"
    "729827e3-9c14-49f7-bb1b-9608f156bbb8" = "Helpdesk administrator"
    "966707d0-3269-4727-9be2-8c3a10f19b9d" = "Password administrator"
    "7be44c8a-adaf-4e2a-84d6-ab2649e08a13" = "Privileged authentication administrator"
    "194ae4cb-b126-40b2-bd5b-6091b380977d" = "Security administrator"
    "f28a1f50-f6e7-4571-818b-6a12f2af6b6c" = "SharePoint administrator"
    "fe930be7-5e62-47db-91af-98c3a49a38b1" = "User administrator"
    "0526716b-113d-4c15-b2c8-68e3c22b9f80" = "Authentication Policy Administrator"
    "be2f45a1-457d-42af-a067-6ec1fa63bc45" = "External Identity Provider Administrator"
}

$DefinedPermissions = @{
    "Read Administrative Units" = @{
        "appRoleIds" = @(
            "134fd756-38ce-4afd-ba33-e9623dbe66c2",
            "7ab1d382-f21e-4acd-a863-ba3e13f7da61",
            "5eb59dd3-1da2-4329-8733-9dabdc435916",
            "19dbc75e-c2e2-444c-a770-ec69d8559fc7"
        );
        "LeastPrivilege" = "AdministrativeUnit.Read.All"
    }

    "Read Authorization Policy" = @{
        "appRoleIds" = @(
            "246dd0d5-5bd0-4def-940b-0421030a5b68",
            "fb221be6-99f2-473f-bd32-01c6a0e9ca3b"
        );
        "LeastPrivilege" = "Policy.Read.All"
    }

    "Read Policy" = @{
        "appRoleIds" = @(
            "246dd0d5-5bd0-4def-940b-0421030a5b68"
        );
        "LeastPrivilege" = "Policy.Read.All"
    }

    "Read Audit Logs" = @{
        "appRoleIds" = @(
            "b0afded3-3588-46d8-8b3d-9842eff778da"
        );
        "LeastPrivilege" = "AuditLog.Read.All"
    }

    "Read Directory Roles" = @{
        "appRoleIds" = @(
            "483bed4a-2ad3-4361-a73b-c83ccdbdc53c",
            "7ab1d382-f21e-4acd-a863-ba3e13f7da61",
            "9e3f62cf-ca93-4989-b6ce-bf83c28f9fe8",
            "19dbc75e-c2e2-444c-a770-ec69d8559fc7"
        );
        "LeastPrivilege" = "RoleManagement.Read.Directory"
    }

    "Read Directory Objects" = @{
        "appRoleIds" = @(
            "7ab1d382-f21e-4acd-a863-ba3e13f7da61"
        );
        "LeastPrivilege" = "Directory.Read.All"
    }

    "Read Users" = @{
        "appRoleIds" = @(
            "df021288-bdef-4463-88db-98f22de89214",
            "741f803b-c850-494e-b5df-cde7c675a1ca",
            "7ab1d382-f21e-4acd-a863-ba3e13f7da61",
            "19dbc75e-c2e2-444c-a770-ec69d8559fc7"
        );
        "LeastPrivilege" = "User.Read.All"
    }

    "Read Reports" = @{
        "appRoleIds" = @(
            "230c1aed-a721-4c5d-9cb4-a90514e508ef"
        );
        "LeastPrivilege" = "Reports.Read.All"
    }

    "Read Applications" = @{
        "appRoleIds" = @(
            "d8e4ec18-f6c0-4620-8122-c8b1f2bf400e",
            "1bfefb4e-e0b5-418b-a88f-73c46d2cc8e9"
            "7ab1d382-f21e-4acd-a863-ba3e13f7da61"
        );
        "LeastPrivilege" = "Application.Read.All"
    }

    "Read User Authentication Method" = @{
        "appRoleIds" = @(
            "38d9df27-64da-44fd-b7c5-a6fbac20248f",
            "50483e42-d915-4231-9639-7fdb7fd190e5"
        );
        "LeastPrivilege" = "UserAuthenticationMethod.Read.All"
    }

    "Role Eligibility Schedule Instances" = @{
        "appRoleIds" = @(
            "c7fbd983-d9aa-4fa7-84b8-17382c103bc4",
            "483bed4a-2ad3-4361-a73b-c83ccdbdc53c",
            "9e3f62cf-ca93-4989-b6ce-bf83c28f9fe8"
        );
        "LeastPrivilege" = "RoleManagement.Read.All
        "
    }
}

function Get-GraphApiToken {
    <#
    .SYNOPSIS
        Returns access token for specified tenant using security credentials
    #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string] $TenantId,

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

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

        [string] $Scope = 'https://graph.microsoft.com/.default',

        # Unused. Simplify calling syntax by using @PSBoundParameters
        $ForestName, $DomainNames
    )
    $getToken = @{
        Method = 'POST'
        Uri = "https://login.microsoftonline.com/$($TenantId)/oauth2/v2.0/token"
        Body = @{
            client_id = $TenantAppId
            client_secret = $TenantAppSecret
            scope = $Scope
            grant_type = 'client_credentials'
        }
        ContentType = 'application/x-www-form-urlencoded'
    }

    $tokenResult = Invoke-RestMethod @getToken

    return $tokenResult.access_token
}

function Invoke-GraphApiRequest {
    <#
    .SYNOPSIS
        Invokes REST call to Graph API
    #>
    [CmdletBinding()]
    param(
        [Uri] $Uri,
        $Method = 'GET',
        $Body,
        $ContentType = 'application/json',
        [string] $AccessToken,
        [int] $Attempts = 3
    )
    $request = @{
        Method = $Method
        Uri = $Uri
        ContentType = $ContentType
        Headers = @{
            Authorization = "Bearer $AccessToken"
        }
    }

    try {
        return Invoke-RestMethod @request
    }
    catch {
        if ($_.Exception.Response.StatusCode -eq "429") {
            if($Attempts -gt 0){
                Start-Sleep -Seconds 10
                $retry = @{
                    Uri = $Uri
                    Method = $Method
                    Body = $Body
                    ContentType = $ContentType
                    AccessToken = $accessToken
                    Attempts = ($Attempts - 1)
                }
                return Invoke-GraphApiRequest @retry
            }
            else {
                throw "The request has failed due to Azure Graph API throttling. Please try again later."
            }
        }
        throw
    }
}

function ConvertTo-GraphApiErrorResult {
    <#
    .SYNOPSIS
        Provides unified global exception handling for Graph API errors
    #>
    #[CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline)]
        $ErrorObject,

        [string] $AccessToken,
        [string] $TenantAppId,
        [string[]] $RequiredPrivileges
    )
    process {
        if($_.Exception.Message -like '*Forbidden*'){
            $result = New-Object Semperis.PSSecurityIndicatorResult.SecurityIndicatorResult
            $result.Status = 'Error'

            $context = @{
                TenantAppId = $TenantAppId
                AccessToken = $AccessToken
                RequiredPrivileges = $RequiredPrivileges
            }
            $permissions = Get-GraphApiRequiredPermission @context

            $result.ResultMessage = $permissions.Message
            $result.Remediation = $permissions.Remediation

            return $result

        }
        ConvertTo-ErrorResult $ErrorObject
    }
}
function Get-GraphApiRequiredPermission {
    #[CmdletBinding()]
    [OutputType([hashtable])]
    param (
        $TenantAppId,
        [string[]] $RequiredPrivileges,
        $AccessToken
    )
    $getPrincipal = @{
        Method = 'GET'
        Uri = "https://graph.microsoft.com/v1.0/servicePrincipals/?`$filter=appid eq '$TenantAppId'"
        ContentType = 'application/json'
        Headers = @{
            Authorization = "Bearer $accessToken"
        }
    }

    $appIDResult = Invoke-RestMethod @getPrincipal
    $principalId = $appIDResult.value.id

    $getRoleAssignments = @{
        Uri = "https://graph.microsoft.com/v1.0/servicePrincipals/$principalId/appRoleAssignments"
        ContentType = 'application/json'
        Method = 'Get'
        Headers = @{
            Authorization = "Bearer $accessToken"
        }
    }

    $appIDAssignments = @()
    $appAssignmentResult = Invoke-RestMethod @getRoleAssignments
    $appIDAssignments += $appAssignmentResult.value | Where-Object {$_.resourceDisplayName -eq "Microsoft Graph"}

    # The 'nextLink' property will keep being returned if there's another page
    While ($appAssignmentResult.'@odata.nextLink' -ne $null) {
        $getRoleAssignments.Uri = $appAssignmentResult.'@odata.nextLink'
        $appAssignmentResult = Invoke-RestMethod @getRoleAssignments
        $appIDAssignments += $appAssignmentResult.value | Where-Object {$_.resourceDisplayName -eq "Microsoft Graph"}
    }

    $appIDAssignmentRoleIds = @()
    $appIDAssignmentRoleIds += $appIDAssignments.appRoleId

    $neededPrivs = [System.Collections.ArrayList]$RequiredPrivileges
    foreach ($privilege in $RequiredPrivileges) {
        foreach ($roleId in $appIDAssignmentRoleIds) {
            if ($DefinedPermissions[$privilege].appRoleIds -contains $roleId) {
                [void]$neededPrivs.Remove($privilege)
            }
        }
    }

    $missingPrivs = @()
    foreach ($neededPriv in $neededPrivs) {
        $missingPrivs += $DefinedPermissions[$neededPriv].LeastPrivilege
    }

    if ($missingPrivs.Count -gt 0) {
        return @{
            Message = "Insufficient permissions"
            Remediation = "Please ensure your DSP app has the following Graph API permission(s): $($missingPrivs -join ", ")"
        }
    }
    else {
        return @{
            Message = "This indicator requires a P1 or P2 license"
            Remediation = "Acquire appropriate license"
        }
    }
}

<#
.SYNOPSIS
Get the services that map to the HOST SPN

.DESCRIPTION
Get the services that map to the HOST SPN from the Configuration partition

.PARAMETER DomainName
The DNS name of the domain

.PARAMETER ForestName
The DNS name of the forest root

.EXAMPLE
Get-HostSPNMapping -dnsDomain $domain -forestName $forestName

#>
Function Get-HostSPNMapping {
    param(
        [parameter(Mandatory = $true)]
        [string]$dnsDomain,

        [parameter(Mandatory = $false)]
        [string]$forestName
    )
    if (-not $PSBoundParameters['forestName']) {$forestName = $dnsDomain}

    $spnMappings = [System.Collections.ArrayList]@()

    $forestDN = Get-DN -dnsDomain $forestName
    $dsDN = "CN=Directory Service,CN=Windows NT,CN=Services,CN=Configuration,$forestDN"
    $filter = "(sPNMappings=*)"


    $spnMappingObject = Search-AD -dnsDomain $dnsDomain -attributes "sPNMappings" -baseDN $dsDN -scope "base" -filter $filter
    foreach ($mapping in $spnMappingObject.Attributes.'spnmappings'.GetValues("string")) {
        if ($mapping.ToLower().StartsWith("host=")) {
            foreach ($hostMapping in $mapping.Substring(5).Split(',')) {
                [void]$spnMappings.Add($hostMapping.ToLower())
            }
        }
    }

    return $spnMappings
}

<#
.SYNOPSIS
Gets the NetBIOS name of a given domain

.DESCRIPTION
Queries a given domain for its NetBIOS name

.PARAMETER dnsDomain
DNS name of the domain whose NetBIOS name we want

.PARAMETER forestName
DNS name of the forest root

.EXAMPLE
Get-NetBIOSName -dnsDomain "child.test.lab" -forestName "test.lab"
#>
Function Get-NetBIOSName {
    # Gets the domain NetBIOS name
    param(
        [parameter(Mandatory = $true)]
        [string]$dnsDomain,

        [parameter(Mandatory = $false)]
        [string]$forestName
    )
    if (-not $PSBoundParameters['forestName']) {$forestName = $dnsDomain}

    $forestDN = Get-DN -dnsDomain $forestName
    $configDN = "CN=Configuration,$forestDN"
    $netbiosFilter = "(&(netbiosname=*)(dnsroot=$dnsDomain))"


    $netBIOSObject = Search-AD -dnsDomain $dnsDomain -attributes "netbiosname" -baseDN $configDN -scope "subtree" -filter $netbiosFilter
    return $netBIOSObject.Attributes.netbiosname.GetValues("string")[0]
}

<#
.SYNOPSIS
Convert and compare Old and New values from a DSP search

.DESCRIPTION
Convert string values from DSP to arrays and compare

.PARAMETER dspResult
The object returned from a DSP search

.EXAMPLE
Compare-DSPMultiValuedAttribute -dspResult $dsp_result
#>
Function Compare-DSPMultiValuedAttribute {
    param(
        [parameter(Mandatory = $true)]
        [object]$dspResult
    )
    $ret = @{}
    $fromValue = $dspResult.From_StringValue
    $toValue = $dspResult.To_StringValue

    if ($fromValue) { $ret["from"] = $fromValue.TrimStart('[').TrimEnd(']').Split(',') }
    if ($toValue) { $ret["to"] = $toValue.TrimStart('[').TrimEnd(']').Split(',') }

    $ret["removed"] = $ret["from"] | Where-Object {$ret["to"] -notcontains $_}
    $ret["added"] = $ret["to"] | Where-Object {$ret["from"] -notcontains $_}

    return $ret
}
function Get-filepath{
    param(
        [parameter(Mandatory = $true)]
        $productPath,
        
        [parameter(Mandatory = $true)]
        $commandPath,

        [parameter(Mandatory = $true)]
        $script
    )
    if($commandPath -match $script)
    {
        $filepath = $productPath + "\Scripts\" + $script + ".json"
    }
    else
    {
        $filepath = "C:\ProgramData\Semperis\Indicators\" + $script + ".json"
    }
    return $filepath
}
function Get-score{
    param(
        [parameter(Mandatory = $true)]
        $count,

        [parameter(Mandatory = $true)]
        $HighWaterMark

    )
    $maxWeight = 11
    $value = $count * $HighWaterMark
    $score = [Math]::Floor((1.0-(($value / ($maxWeight - $HighWaterMark)) * 0.1))*100)
    if($score -le 0){$score = 0}
    return $score
}
function Initialize-res{
    $res = New-Object Semperis.PSSecurityIndicatorResult.SecurityIndicatorResult
    $res.ResultMessage = "No evidence of exposure"
    $res.Remediation = "None"
    $res.Score = 100
    $res.Status = [Semperis.PSSecurityIndicatorResult.ScriptStatus]"Pass"
    return $res
}

function Get-json{
    param(
        [parameter(Mandatory = $true)]
        [string]$filePath,

        [parameter(Mandatory = $true)]
        [System.Collections.ArrayList]$outputObjects,

        [parameter(Mandatory = $true)]
        [Int]$badCount
    )
    foreach($output in $outputObjects)
    {$output | Add-Member -NotePropertyName Ignored -NotePropertyValue $false}

    $toRemove = [System.Collections.ArrayList]@()
    $jFile = Get-Content $filePath | ConvertFrom-Json
    $excludes = $jFile.exclude.Object.PSObject.Properties

    if($excludes)
    {
        foreach($exclude in $excludes)
        {
            $ignoreName = $exclude.Name
            foreach($output in $outputObjects)
            {
                if($output.$ignoreName)
                {
                    if($exclude.Value.regex)
                    {
                        $ignoreValue = $exclude.Value.regex
                        foreach($ignore in $ignoreValue)
                        {
                            if($output.$ignoreName -Match $ignore)
                            {
                                $output.Ignored = $true
                                if(!$toRemove.Contains($output))
                                {[void]$toRemove.Add($output)}
                            }
                        }
                    }
                    if($exclude.Value.filter)
                    {
                        $ignoreValue = $exclude.Value.filter
                        foreach($ignore in $ignoreValue)
                        {
                            $checkMatch = $ignore -eq $output.$ignoreName
                            if($checkMatch -eq $true)
                            {
                                $output.Ignored = $true
                                if(!$toRemove.Contains($output))
                                {[void]$toRemove.Add($output)}
                            }
                        }
                    }
                }
            }
        }
        $badCount = $badCount -$toRemove.Count
        return $badCount
    }
}
Export-ModuleMember -Function Invoke-GraphApiRequest
Export-ModuleMember -Function ConvertTo-GraphApiErrorResult

Export-ModuleMember -Function Out-Log
Export-ModuleMember -Function Search-AD
Export-ModuleMember -Function Get-DN
Export-ModuleMember -Function Get-DomainSID
Export-ModuleMember -Function Get-UACSet
Export-ModuleMember -Function Get-SupportedEncryptionTypesSet
Export-ModuleMember -Function Get-ADSearchFlag
Export-ModuleMember -Function Convert-StringToXml
Export-ModuleMember -Function Get-TrustAttributesSet
Export-ModuleMember -Function Update-NTSecurityDescriptor
Export-ModuleMember -Function Update-AttributeMetadata
Export-ModuleMember -Function Update-ValueMetadata
Export-ModuleMember -Function Get-AdminsInForest
Export-ModuleMember -Function Update-IndicatorOutputAttackWindow
Export-ModuleMember -Function Search-ADHelper
Export-ModuleMember -Function ConvertTo-ErrorResult
Export-ModuleMember -Function Get-NetBIOSName
Export-ModuleMember -Function Get-HostSPNMapping
Export-ModuleMember -Function Compare-DSPMultiValuedAttribute
Export-ModuleMember -Variable GET_DECIMAL_VALUE
Export-ModuleMember -Variable GET_STRING
Export-ModuleMember -Variable AAD_PRIVILEGED_ROLES

# Ignore list functions
Export-ModuleMember -Function Get-score
Export-ModuleMember -Function Initialize-res
Export-ModuleMember -Function Get-json
Export-ModuleMember -Function Get-filepath

# Graph API
Export-ModuleMember -Function Get-GraphApiToken
Export-ModuleMember -Function Invoke-GraphApiRequest
Export-ModuleMember -Function ConvertTo-GraphApiErrorResult
# SIG # Begin signature block
# MIItlwYJKoZIhvcNAQcCoIItiDCCLYQCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCzppm4rEPT42JF
# uZSa0HCuPuP7FXhTeDzj5g84XUlsEqCCJrcwggWNMIIEdaADAgECAhAOmxiO+dAt
# 5+/bUOIIQBhaMA0GCSqGSIb3DQEBDAUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQK
# EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNV
# BAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0yMjA4MDEwMDAwMDBa
# Fw0zMTExMDkyMzU5NTlaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy
# dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lD
# ZXJ0IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC
# ggIBAL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3E
# MB/zG6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKy
# unWZanMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsF
# xl7sWxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU1
# 5zHL2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJB
# MtfbBHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObUR
# WBf3JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6
# nj3cAORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxB
# YKqxYxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5S
# UUd0viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+x
# q4aLT8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjggE6MIIB
# NjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwP
# TzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzAOBgNVHQ8BAf8EBAMC
# AYYweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdp
# Y2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNv
# bS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwRQYDVR0fBD4wPDA6oDigNoY0
# aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENB
# LmNybDARBgNVHSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQEMBQADggEBAHCgv0Nc
# Vec4X6CjdBs9thbX979XB72arKGHLOyFXqkauyL4hxppVCLtpIh3bb0aFPQTSnov
# Lbc47/T/gLn4offyct4kvFIDyE7QKt76LVbP+fT3rDB6mouyXtTP0UNEm0Mh65Zy
# oUi0mcudT6cGAxN3J0TU53/oWajwvy8LpunyNDzs9wPHh6jSTEAZNUZqaVSwuKFW
# juyk1T3osdz9HNj0d1pcVIxv76FQPfx2CWiEn2/K2yCNNWAcAgPLILCsWKAOQGPF
# mCLBsln1VWvPJ6tsds5vIy30fnFqI2si/xK4VC0nftg62fC2h5b9W9FcrBjDTZ9z
# twGpn1eqXijiuZQwggWiMIIEiqADAgECAhB4AxhCRXCKQc9vAbjutKlUMA0GCSqG
# SIb3DQEBDAUAMEwxIDAeBgNVBAsTF0dsb2JhbFNpZ24gUm9vdCBDQSAtIFIzMRMw
# EQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTIwMDcy
# ODAwMDAwMFoXDTI5MDMxODAwMDAwMFowUzELMAkGA1UEBhMCQkUxGTAXBgNVBAoT
# EEdsb2JhbFNpZ24gbnYtc2ExKTAnBgNVBAMTIEdsb2JhbFNpZ24gQ29kZSBTaWdu
# aW5nIFJvb3QgUjQ1MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAti3F
# MN166KuQPQNysDpLmRZhsuX/pWcdNxzlfuyTg6qE9aNDm5hFirhjV12bAIgEJen4
# aJJLgthLyUoD86h/ao+KYSe9oUTQ/fU/IsKjT5GNswWyKIKRXftZiAULlwbCmPgs
# pzMk7lA6QczwoLB7HU3SqFg4lunf+RuRu4sQLNLHQx2iCXShgK975jMKDFlrjrz0
# q1qXe3+uVfuE8ID+hEzX4rq9xHWhb71hEHREspgH4nSr/2jcbCY+6R/l4ASHrTDT
# DI0DfFW4FnBcJHggJetnZ4iruk40mGtwEd44ytS+ocCc4d8eAgHYO+FnQ4S2z/x0
# ty+Eo7+6CTc9Z2yxRVwZYatBg/WsHet3DUZHc86/vZWV7Z0riBD++ljop1fhs8+o
# WukHJZsSxJ6Acj2T3IyU3ztE5iaA/NLDA/CMDNJF1i7nj5ie5gTuQm5nfkIWcWLn
# BPlgxmShtpyBIU4rxm1olIbGmXRzZzF6kfLUjHlufKa7fkZvTcWFEivPmiJECKiF
# N84HYVcGFxIkwMQxc6GYNVdHfhA6RdktpFGQmKmgBzfEZRqqHGsWd/enl+w/GTCZ
# bzH76kCy59LE+snQ8FB2dFn6jW0XMr746X4D9OeHdZrUSpEshQMTAitCgPKJajbP
# yEygzp74y42tFqfT3tWbGKfGkjrxgmPxLg4kZN8CAwEAAaOCAXcwggFzMA4GA1Ud
# DwEB/wQEAwIBhjATBgNVHSUEDDAKBggrBgEFBQcDAzAPBgNVHRMBAf8EBTADAQH/
# MB0GA1UdDgQWBBQfAL9GgAr8eDm3pbRD2VZQu86WOzAfBgNVHSMEGDAWgBSP8Et/
# qC5FJK5NUPpjmove4t0bvDB6BggrBgEFBQcBAQRuMGwwLQYIKwYBBQUHMAGGIWh0
# dHA6Ly9vY3NwLmdsb2JhbHNpZ24uY29tL3Jvb3RyMzA7BggrBgEFBQcwAoYvaHR0
# cDovL3NlY3VyZS5nbG9iYWxzaWduLmNvbS9jYWNlcnQvcm9vdC1yMy5jcnQwNgYD
# VR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5nbG9iYWxzaWduLmNvbS9yb290LXIz
# LmNybDBHBgNVHSAEQDA+MDwGBFUdIAAwNDAyBggrBgEFBQcCARYmaHR0cHM6Ly93
# d3cuZ2xvYmFsc2lnbi5jb20vcmVwb3NpdG9yeS8wDQYJKoZIhvcNAQEMBQADggEB
# AKz3zBWLMHmoHQsoiBkJ1xx//oa9e1ozbg1nDnti2eEYXLC9E10dI645UHY3qkT9
# XwEjWYZWTMytvGQTFDCkIKjgP+icctx+89gMI7qoLao89uyfhzEHZfU5p1GCdeHy
# L5f20eFlloNk/qEdUfu1JJv10ndpvIUsXPpYd9Gup7EL4tZ3u6m0NEqpbz308w2V
# Xeb5ekWwJRcxLtv3D2jmgx+p9+XUnZiM02FLL8Mofnrekw60faAKbZLEtGY/fadY
# 7qz37MMIAas4/AocqcWXsojICQIZ9lyaGvFNbDDUswarAGBIDXirzxetkpNiIHd1
# bL3IMrTcTevZ38GQlim9wX8wggauMIIElqADAgECAhAHNje3JFR82Ees/ShmKl5b
# MA0GCSqGSIb3DQEBCwUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy
# dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lD
# ZXJ0IFRydXN0ZWQgUm9vdCBHNDAeFw0yMjAzMjMwMDAwMDBaFw0zNzAzMjIyMzU5
# NTlaMGMxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkG
# A1UEAxMyRGlnaUNlcnQgVHJ1c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3Rh
# bXBpbmcgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDGhjUGSbPB
# PXJJUVXHJQPE8pE3qZdRodbSg9GeTKJtoLDMg/la9hGhRBVCX6SI82j6ffOciQt/
# nR+eDzMfUBMLJnOWbfhXqAJ9/UO0hNoR8XOxs+4rgISKIhjf69o9xBd/qxkrPkLc
# Z47qUT3w1lbU5ygt69OxtXXnHwZljZQp09nsad/ZkIdGAHvbREGJ3HxqV3rwN3mf
# XazL6IRktFLydkf3YYMZ3V+0VAshaG43IbtArF+y3kp9zvU5EmfvDqVjbOSmxR3N
# Ng1c1eYbqMFkdECnwHLFuk4fsbVYTXn+149zk6wsOeKlSNbwsDETqVcplicu9Yem
# j052FVUmcJgmf6AaRyBD40NjgHt1biclkJg6OBGz9vae5jtb7IHeIhTZgirHkr+g
# 3uM+onP65x9abJTyUpURK1h0QCirc0PO30qhHGs4xSnzyqqWc0Jon7ZGs506o9UD
# 4L/wojzKQtwYSH8UNM/STKvvmz3+DrhkKvp1KCRB7UK/BZxmSVJQ9FHzNklNiyDS
# LFc1eSuo80VgvCONWPfcYd6T/jnA+bIwpUzX6ZhKWD7TA4j+s4/TXkt2ElGTyYwM
# O1uKIqjBJgj5FBASA31fI7tk42PgpuE+9sJ0sj8eCXbsq11GdeJgo1gJASgADoRU
# 7s7pXcheMBK9Rp6103a50g5rmQzSM7TNsQIDAQABo4IBXTCCAVkwEgYDVR0TAQH/
# BAgwBgEB/wIBADAdBgNVHQ4EFgQUuhbZbU2FL3MpdpovdYxqII+eyG8wHwYDVR0j
# BBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1Ud
# JQQMMAoGCCsGAQUFBwMIMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0
# cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0
# cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8E
# PDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVz
# dGVkUm9vdEc0LmNybDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEw
# DQYJKoZIhvcNAQELBQADggIBAH1ZjsCTtm+YqUQiAX5m1tghQuGwGC4QTRPPMFPO
# vxj7x1Bd4ksp+3CKDaopafxpwc8dB+k+YMjYC+VcW9dth/qEICU0MWfNthKWb8RQ
# TGIdDAiCqBa9qVbPFXONASIlzpVpP0d3+3J0FNf/q0+KLHqrhc1DX+1gtqpPkWae
# LJ7giqzl/Yy8ZCaHbJK9nXzQcAp876i8dU+6WvepELJd6f8oVInw1YpxdmXazPBy
# oyP6wCeCRK6ZJxurJB4mwbfeKuv2nrF5mYGjVoarCkXJ38SNoOeY+/umnXKvxMfB
# wWpx2cYTgAnEtp/Nh4cku0+jSbl3ZpHxcpzpSwJSpzd+k1OsOx0ISQ+UzTl63f8l
# Y5knLD0/a6fxZsNBzU+2QJshIUDQtxMkzdwdeDrknq3lNHGS1yZr5Dhzq6YBT70/
# O3itTK37xJV77QpfMzmHQXh6OOmc4d0j/R0o08f56PGYX/sr2H7yRp11LB4nLCbb
# bxV7HhmLNriT1ObyF5lZynDwN7+YAN8gFk8n+2BnFqFmut1VwDophrCYoCvtlUG3
# OtUVmDG0YgkPCr2B2RP+v6TR81fZvAT6gt4y3wSJ8ADNXcL50CN/AAvkdgIm2fBl
# dkKmKYcJRyvmfxqkhQ/8mJb2VVQrH4D6wPIOK+XW+6kvRBVK5xMOHds3OBqhK/bt
# 1nz8MIIGwDCCBKigAwIBAgIQDE1pckuU+jwqSj0pB4A9WjANBgkqhkiG9w0BAQsF
# ADBjMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xOzA5BgNV
# BAMTMkRpZ2lDZXJ0IFRydXN0ZWQgRzQgUlNBNDA5NiBTSEEyNTYgVGltZVN0YW1w
# aW5nIENBMB4XDTIyMDkyMTAwMDAwMFoXDTMzMTEyMTIzNTk1OVowRjELMAkGA1UE
# BhMCVVMxETAPBgNVBAoTCERpZ2lDZXJ0MSQwIgYDVQQDExtEaWdpQ2VydCBUaW1l
# c3RhbXAgMjAyMiAtIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDP
# 7KUmOsap8mu7jcENmtuh6BSFdDMaJqzQHFUeHjZtvJJVDGH0nQl3PRWWCC9rZKT9
# BoMW15GSOBwxApb7crGXOlWvM+xhiummKNuQY1y9iVPgOi2Mh0KuJqTku3h4uXoW
# 4VbGwLpkU7sqFudQSLuIaQyIxvG+4C99O7HKU41Agx7ny3JJKB5MgB6FVueF7fJh
# vKo6B332q27lZt3iXPUv7Y3UTZWEaOOAy2p50dIQkUYp6z4m8rSMzUy5Zsi7qlA4
# DeWMlF0ZWr/1e0BubxaompyVR4aFeT4MXmaMGgokvpyq0py2909ueMQoP6McD1AG
# N7oI2TWmtR7aeFgdOej4TJEQln5N4d3CraV++C0bH+wrRhijGfY59/XBT3EuiQMR
# oku7mL/6T+R7Nu8GRORV/zbq5Xwx5/PCUsTmFntafqUlc9vAapkhLWPlWfVNL5Af
# J7fSqxTlOGaHUQhr+1NDOdBk+lbP4PQK5hRtZHi7mP2Uw3Mh8y/CLiDXgazT8QfU
# 4b3ZXUtuMZQpi+ZBpGWUwFjl5S4pkKa3YWT62SBsGFFguqaBDwklU/G/O+mrBw5q
# BzliGcnWhX8T2Y15z2LF7OF7ucxnEweawXjtxojIsG4yeccLWYONxu71LHx7jstk
# ifGxxLjnU15fVdJ9GSlZA076XepFcxyEftfO4tQ6dwIDAQABo4IBizCCAYcwDgYD
# VR0PAQH/BAQDAgeAMAwGA1UdEwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUH
# AwgwIAYDVR0gBBkwFzAIBgZngQwBBAIwCwYJYIZIAYb9bAcBMB8GA1UdIwQYMBaA
# FLoW2W1NhS9zKXaaL3WMaiCPnshvMB0GA1UdDgQWBBRiit7QYfyPMRTtlwvNPSqU
# FN9SnDBaBgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsMy5kaWdpY2VydC5jb20v
# RGlnaUNlcnRUcnVzdGVkRzRSU0E0MDk2U0hBMjU2VGltZVN0YW1waW5nQ0EuY3Js
# MIGQBggrBgEFBQcBAQSBgzCBgDAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGln
# aWNlcnQuY29tMFgGCCsGAQUFBzAChkxodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5j
# b20vRGlnaUNlcnRUcnVzdGVkRzRSU0E0MDk2U0hBMjU2VGltZVN0YW1waW5nQ0Eu
# Y3J0MA0GCSqGSIb3DQEBCwUAA4ICAQBVqioa80bzeFc3MPx140/WhSPx/PmVOZsl
# 5vdyipjDd9Rk/BX7NsJJUSx4iGNVCUY5APxp1MqbKfujP8DJAJsTHbCYidx48s18
# hc1Tna9i4mFmoxQqRYdKmEIrUPwbtZ4IMAn65C3XCYl5+QnmiM59G7hqopvBU2AJ
# 6KO4ndetHxy47JhB8PYOgPvk/9+dEKfrALpfSo8aOlK06r8JSRU1NlmaD1TSsht/
# fl4JrXZUinRtytIFZyt26/+YsiaVOBmIRBTlClmia+ciPkQh0j8cwJvtfEiy2JIM
# kU88ZpSvXQJT657inuTTH4YBZJwAwuladHUNPeF5iL8cAZfJGSOA1zZaX5YWsWMM
# xkZAO85dNdRZPkOaGK7DycvD+5sTX2q1x+DzBcNZ3ydiK95ByVO5/zQQZ/YmMph7
# /lxClIGUgp2sCovGSxVK05iQRWAzgOAj3vgDpPZFR+XOuANCR+hBNnF3rf2i6Jd0
# Ti7aHh2MWsgemtXC8MYiqE+bvdgcmlHEL5r2X6cnl7qWLoVXwGDneFZ/au/ClZpL
# EQLIgpzJGgV8unG1TnqZbPTontRamMifv427GFxD9dAq6OJi7ngE273R+1sKqHB+
# 8JeEeOMIA11HLGOoJTiXAdI/Otrl5fbmm9x+LMz/F0xNAKLY1gEOuIvu5uByVYks
# Jxlh9ncBjDCCBuYwggTOoAMCAQICEHe9DgOhtwj4VKsGchDZBEcwDQYJKoZIhvcN
# AQELBQAwUzELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2Ex
# KTAnBgNVBAMTIEdsb2JhbFNpZ24gQ29kZSBTaWduaW5nIFJvb3QgUjQ1MB4XDTIw
# MDcyODAwMDAwMFoXDTMwMDcyODAwMDAwMFowWTELMAkGA1UEBhMCQkUxGTAXBgNV
# BAoTEEdsb2JhbFNpZ24gbnYtc2ExLzAtBgNVBAMTJkdsb2JhbFNpZ24gR0NDIFI0
# NSBDb2RlU2lnbmluZyBDQSAyMDIwMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
# CgKCAgEA1kJN+eNPxiP0bB2BpjD3SD3P0OWN5SAilgdENV0Gzw8dcGDmJlT6UyNg
# AqhfAgL3jsluPal4Bb2O9U8ZJJl8zxEWmx97a9Kje2hld6vYsSw/03IGMlxbrFBn
# LCVNVgY2/MFiTH19hhaVml1UulDQsH+iRBnp1m5sPhPCnxHUXzRbUWgxYwr4W9De
# ullfMa+JaDhAPgjoU2dOY7Yhju/djYVBVZ4cvDfclaDEcacfG6VJbgogWX6Jo1gV
# lwAlad/ewmpQZU5T+2uhnxgeig5fVF694FvP8gwE0t4IoRAm97Lzei7CjpbBP86l
# 2vRZKIw3ZaExlguOpHZ3FUmEZoIl50MKd1KxmVFC/6Gy3ZzS3BjZwYapQB1Bl2KG
# vKj/osdjFwb9Zno2lAEgiXgfkPR7qVJOak9UBiqAr57HUEL6ZQrjAfSxbqwOqOOB
# Gag4yJ4DKIakdKdHlX5yWip7FWocxGnmsL5AGZnL0n1VTiKcEOChW8OzLnqLxN7x
# Sx+MKHkwRX9sE7Y9LP8tSooq7CgPLcrUnJiKSm1aNiwv37rL4kFKCHcYiK01YZQS
# 86Ry6+42nqdRJ5E896IazPyH5ZfhUYdp6SLMg8C3D0VsB+FDT9SMSs7PY7G1pBB6
# +Q0MKLBrNP4haCdv7Pj6JoRbdULNiSZ5WZ1rq2NxYpAlDQgg8f8CAwEAAaOCAa4w
# ggGqMA4GA1UdDwEB/wQEAwIBhjATBgNVHSUEDDAKBggrBgEFBQcDAzASBgNVHRMB
# Af8ECDAGAQH/AgEAMB0GA1UdDgQWBBTas43AJJCja3fTDKBZ3SFnZHYLeDAfBgNV
# HSMEGDAWgBQfAL9GgAr8eDm3pbRD2VZQu86WOzCBkwYIKwYBBQUHAQEEgYYwgYMw
# OQYIKwYBBQUHMAGGLWh0dHA6Ly9vY3NwLmdsb2JhbHNpZ24uY29tL2NvZGVzaWdu
# aW5ncm9vdHI0NTBGBggrBgEFBQcwAoY6aHR0cDovL3NlY3VyZS5nbG9iYWxzaWdu
# LmNvbS9jYWNlcnQvY29kZXNpZ25pbmdyb290cjQ1LmNydDBBBgNVHR8EOjA4MDag
# NKAyhjBodHRwOi8vY3JsLmdsb2JhbHNpZ24uY29tL2NvZGVzaWduaW5ncm9vdHI0
# NS5jcmwwVgYDVR0gBE8wTTBBBgkrBgEEAaAyATIwNDAyBggrBgEFBQcCARYmaHR0
# cHM6Ly93d3cuZ2xvYmFsc2lnbi5jb20vcmVwb3NpdG9yeS8wCAYGZ4EMAQQBMA0G
# CSqGSIb3DQEBCwUAA4ICAQAIiHImxq/6rF8GwKqMkNrQssCil/9uEzIWVP0+9DAR
# n4+Y+ZtS3fKiFu7ZeJWmmnxhuAS1+OvL9GERM/ZlJbcRQovYaW7H/5W0gUOpfq6/
# gtZNzBGjg3FqEF4ZBafnbH9W9Khcw04JrVlruPl+pS64/N4OwqD7sATUExvHJ6m5
# qi0xO89GTJ3rTOy8Lpzxh6N/OGlfQUBn9lN96kHvjj37qdQROEbfPOv2zSK9E83w
# 4eblM6C+POR41RvMIPIwc7AiHPaE1ptcAALhKFJL/xJLQOrusBoGBp6E5ufw24RG
# +3PZK0K2yVc0xxbApushuaoO9/7byuu8F8u4Z+vjPk/bqZSGZFXJCQrA2QRxShFL
# WmTDvHh4rUxHJmUHmdXNNmChM1Oz9nsq1YlAPHGlq/iZWf3jm5JL3QW9Cwx4BivP
# U9i9EppbJ4aFP5G+4HiAc1Tfpx1nK2q2rk2JgCQIUnBQ8wH/RK4vmuDhSQjh4VvX
# ONGeCoqdlCebyqO52+I2auNvuVhi4DZ4NgH6waeJeiZTo1y70rLristjCC/+HvNW
# KeI1m9j/6aW9bUtZLIksL1K7tSmQ2kNHvHLdvNm/gMHcsKu0Sx1YNjdk65vhhRea
# KaL95gjSkv+g+Hzh6afRMI5fJlArx6Lil3eK79hNPibrmUBg8zxnDLYIcik1U4E0
# 3DCCBxwwggUEoAMCAQICDElF7GllQBbooYM+/DANBgkqhkiG9w0BAQsFADBZMQsw
# CQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTEvMC0GA1UEAxMm
# R2xvYmFsU2lnbiBHQ0MgUjQ1IENvZGVTaWduaW5nIENBIDIwMjAwHhcNMjIwMzAy
# MTE0ODM5WhcNMjQwNjE0MTQzMDQ0WjCBiTELMAkGA1UEBhMCVVMxETAPBgNVBAgT
# CE5ldyBZb3JrMREwDwYDVQQHEwhOZXcgWW9yazEWMBQGA1UEChMNU0VNUEVSSVMg
# SU5DLjEWMBQGA1UEAxMNU0VNUEVSSVMgSU5DLjEkMCIGCSqGSIb3DQEJARYVY29k
# ZXNpZ25Ac2VtcGVyaXMuY29tMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC
# AgEAr6tl3SlmfsaiEOfMdtp4zvTErHoof7dk7J3pCnooplIEiChxu0y8qc9lcdPj
# 7pr8RJvSbG110CsfV6GzWevZUVJWmQqvE4mugzLxhFABL4t0r8HZSSa/gKMEndXr
# 3SUhwdbLmg38F+yuzyGoiHY55ecMHPWoz89tBYlezlISI2C0+E/RQC8/c8STflpX
# FCVEpqFwJnY1x5rz+jEDTu8ITyNmrhb66uzogNTdgsl0xasM9TpE5VKrWSLAg37i
# Frj0GRjwd/pzTfO3tXhH4zt728F04HuuPf+3F3ETLhSsrk1lBNwMV+UBVjPJrVro
# 1brvEHn7D88BicGkT/qFh0+R5jUu7O8BJ+/g4IHp4yKOtp/Ockp05Yi6H45fX35c
# QddB6twWIFbGcjRGAadYUSkacUOfGRzAJKQOJYmN3ISOJJT1hoHCFtR9sqjN0pQa
# 2k1bk/bJGmTrmt7uUPJ5Ucnl1qHgZkPD5R9ERXXjZ00cX5mOx5l0gpxvg+01R4cy
# TyxgRzAuBU46q6P8ESh4hiyJM+nnao8+eE8Sr/VN/VC7CCtLEGWl2/Ek1KeniiuZ
# SrdMFnnaI0G4tqqG97mSIBoPuKwBCk+FpvGXQ8u3jd2mU8P4uZo4hH8Mib4L81k/
# 8oSc+vmQzEBiBSY/S9g2j51EnoyzAuyvJMmcCVF1Rp9TUU0CAwEAAaOCAbEwggGt
# MA4GA1UdDwEB/wQEAwIHgDCBmwYIKwYBBQUHAQEEgY4wgYswSgYIKwYBBQUHMAKG
# Pmh0dHA6Ly9zZWN1cmUuZ2xvYmFsc2lnbi5jb20vY2FjZXJ0L2dzZ2NjcjQ1Y29k
# ZXNpZ25jYTIwMjAuY3J0MD0GCCsGAQUFBzABhjFodHRwOi8vb2NzcC5nbG9iYWxz
# aWduLmNvbS9nc2djY3I0NWNvZGVzaWduY2EyMDIwMFYGA1UdIARPME0wQQYJKwYB
# BAGgMgEyMDQwMgYIKwYBBQUHAgEWJmh0dHBzOi8vd3d3Lmdsb2JhbHNpZ24uY29t
# L3JlcG9zaXRvcnkvMAgGBmeBDAEEATAJBgNVHRMEAjAAMEUGA1UdHwQ+MDwwOqA4
# oDaGNGh0dHA6Ly9jcmwuZ2xvYmFsc2lnbi5jb20vZ3NnY2NyNDVjb2Rlc2lnbmNh
# MjAyMC5jcmwwEwYDVR0lBAwwCgYIKwYBBQUHAwMwHwYDVR0jBBgwFoAU2rONwCSQ
# o2t30wygWd0hZ2R2C3gwHQYDVR0OBBYEFHl2JYTkRF1JSD0dmaZEJrMSJ1pZMA0G
# CSqGSIb3DQEBCwUAA4ICAQC1QWVq8P2sWcGYHIPqTtMGIv6YV8RNYVDsS2nkSUsU
# xGGZO0GrGKDNn0epo1M1dXWtbECkivTrVA2x3Ku/yVysHHSliLbcL/QCPkbXT8hr
# 3s71X53tbAQp96iMSt/+RURXWH7bnBOCLfO1BeH9ua0uRYBpy3OYfuDEICowr4SN
# Rf2iY0wSoWZ85QsBlMgLLFymJln9Pjw8SYsVOYkwfMiv1aT7uCxVlkYtKW9bkmyb
# HuKiEduQwSfs5GzegSFM+WcfC4JqBhVXRBxQljX53Y1rb3eA8CVLTHZPVqXXM8di
# 628y1IylTdTzXcriFjCmMYZ0re+FNlVBT6KhJ+nP+pcSmrbVy9qg/BX1fQD5XGT6
# 3VdQp4qfTK25uWHWHm0GlOMR/SyC5HEmpASUkL5tHr+n+9GoRyS8IjQop37RJ1hK
# oBXlH0ayyQWHvLQjCM5Hx+ARSDq3HdeqK3ToAuUDJ+G5qgr0BP3X9Ap4K7fSn8p0
# gafpMdSBWRnmfIhPa0E4XEMA++XQtfy+gA4nr/0zD05uB+vpSli97phr6f3duafm
# NJbH40z7xlg2euTBd7CkUtI3KwFq0pvdgln0Xsjl5NhR4yY7Y9oQIjWd9ooFYWqx
# nphR9JecB3P+CkVA3L7Xwqu5PIXSZ+bPhF5a8yNNThYmm8g20dl3vCaZiX5/RHvt
# 8jGCBjYwggYyAgEBMGkwWTELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNp
# Z24gbnYtc2ExLzAtBgNVBAMTJkdsb2JhbFNpZ24gR0NDIFI0NSBDb2RlU2lnbmlu
# ZyBDQSAyMDIwAgxJRexpZUAW6KGDPvwwDQYJYIZIAWUDBAIBBQCgfDAQBgorBgEE
# AYI3AgEMMQIwADAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgorBgEEAYI3
# AgELMQ4wDAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQgBgrSAFZmSla6vG/E
# 5N+JOpYTAUD5Gj6Hu+XJAtZcR6swDQYJKoZIhvcNAQEBBQAEggIAPM/p+WXLiP7n
# fQDVh7aUTbZhcvOBqRKVXrxa6e4EtuYqcNrgiXoVHBiUhk6g41lzvW/0uYXTqts9
# GOdM+ai8MjNKEvCJFe+6OJS1/vAA+n7Va+gbbrkb5CBSXMRLUInFdbXpFRvcJj9A
# ekwr2offjN9UEsWB/pW9fnyBP9XRetR6/pJ+PiW2+GS0hAGvjnvo8GGYxmkcnLRx
# hCpehDjWkZxj7WLeIYbLaxHNxgmuqXKu922GEzf93UjyGti2j/Kk4MkfvXXMLW+k
# YGO7zvJFjp4Pz05u83b2H4eYedZRQRWEX3zIlA19gNFZsdq5iagHUCeFRlia9BKI
# r9OGYi/RafmAGzKts4+ZXEKFC0qyRugh6m6ur71l3/PAdWC0lw5LDpOa5XiMoAeW
# +g0XI0Cdv5LVkWul9XOUdDqhNTaqEEMgC+6G1B7EVvejr3DooAJDgmIKcIpN2vFf
# 7Rt3pqPA3GPBGxsrZTUlYZKa+D/FZbN90LEfOv24X+HHni0vU5Ujgt+Vcp9PhGbv
# /6UW6bw/FOM0pRXzSdWxStsteJioMLnEhoQd/SwVjXnvB8K8BFi1uWy9wh29MNgI
# ZWiu+wrmBBRp3OkjUNDxPOt/vp5ZOxVpQntEAQDKSvaU3X9c1w6vd1a3BWQqCEjq
# ELvNUh1TFu4jTZI9adbMg6V11IHRM9mhggMgMIIDHAYJKoZIhvcNAQkGMYIDDTCC
# AwkCAQEwdzBjMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4x
# OzA5BgNVBAMTMkRpZ2lDZXJ0IFRydXN0ZWQgRzQgUlNBNDA5NiBTSEEyNTYgVGlt
# ZVN0YW1waW5nIENBAhAMTWlyS5T6PCpKPSkHgD1aMA0GCWCGSAFlAwQCAQUAoGkw
# GAYJKoZIhvcNAQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMjIxMjEx
# MDkyMDA2WjAvBgkqhkiG9w0BCQQxIgQgHUsOJxvSezJpa+aZdEXP4jHAlZ5nvC6A
# rnUmkMwUzPwwDQYJKoZIhvcNAQEBBQAEggIAR0Cq9HaLf8ibA3am1gLsQAiRgtE6
# a90VtNDBhE1YwQDC+/TNh4U/hokw8DIaFI09e1vfUOA1hM1m9OPaWBbcY8Q+JFK9
# 3U3wNbjpoLjv4BejVY1++e3h/Oan8hatl2Z+uUk0y4TDpFhd2E+55xyw9pjcWh7/
# Y5vXh7B71MxQg+8pMWwTJvHrqN0aAf1CiOEfO/RpOm9oABqrbKoh/p2kTzXXyhAK
# 5zdy8qYN5K7BMuZE/EJmNJqbM65aZ1zD+EXZ1M+EShO3dthDjx+uqFq6IQkF3RSt
# qWTz+RMm5Zi1tzQ8nmvIjHXHxIls0lkNzVPEYXvCDnLhd7L7BGu1zy4HtfhRvFTb
# FHz5zEiMrMi4qGYMS+28b/kBjsQe8bZGlD3VKPg/g5YiBh6/a0cR41wROo5stLFk
# 1UzM6NEHwBZd5vanNvmygOQk3nWHSJ1pAu92UcvAVLPS/CMHYg3DaoYHFqT73eBZ
# aeAZOXg4Jag6s/A8vCeXcrXEcoICgGrd9/Ra/1r8WQHQwuN8qPiwg8HH2TxQv3N/
# TdYfIk7CIfuUxMwiYjsg6jTPTExInTGZQvhz/6a4St0qk7w6TkpEMvWHtEPiz/Yy
# pOCegA/Dk92yWu3OQoG67abjhjDBedNS+TNR5KxgRS42W+WVKVSBDY0F5WDy7Scz
# slgJnDuRertYefw=
# SIG # End signature block
