Invoke-LicenseScan.ps1

<#
.SYNOPSIS
    Free, read-only Microsoft 365 unused-license scan.
    Cross-checks every assigned license against real activity and reports the
    seats you're paying for but nobody is using.
 
.DESCRIPTION
    Community edition of the ServerBridge License Auditor. Connects to Microsoft
    Graph with delegated, READ-ONLY scopes via device-code sign-in (nothing to
    register, no app, no secrets). It never writes, removes, or changes anything
    in your tenant, and it stores nothing.
 
    Activity signal (chosen automatically):
      * If the tenant has Microsoft Entra ID P1/P2, it uses directory
        sign-in activity (signInActivity) - the cleanest signal.
      * Otherwise it falls back to the Microsoft 365 usage reports
        (getOffice365ActiveUserDetail), which need no premium license.
      * Disabled accounts that still hold paid licenses are always flagged -
        that works on every tenant regardless of the above.
 
    Outputs:
      * A console summary of wasted spend by SKU
      * A CSV of every dormant licensed user (for your own follow-up)
 
    What it deliberately does NOT do (that's the paid ServerBridge License Auditor):
      * A formatted PDF report, costed recommended actions, scheduled re-audits
        with drift tracking, multi-tenant roll-up, and support.
    See https://server-bridge.com/license-auditor.html
 
.PARAMETER InactiveDays
    A licensed user with no activity in this many days is counted as dormant.
    Default 90. Range 1-3650. (The usage-report fallback resolves to the nearest
    supported window: 30, 90 or 180 days.)
 
.PARAMETER OutputCsv
    Path for the CSV export. Default: .\license-scan_<tenant>_<date>.csv
 
.PARAMETER IncludeGuests
    Include guest (external) users. Off by default.
 
.PARAMETER PassThru
    Emit the dormant-seat objects to the pipeline in addition to the CSV.
 
.EXAMPLE
    .\Invoke-LicenseScan.ps1
 
.EXAMPLE
    .\Invoke-LicenseScan.ps1 -InactiveDays 60 -OutputCsv C:\reports\waste.csv
 
.NOTES
    Requires the Microsoft Graph PowerShell SDK:
        Install-Module Microsoft.Graph -Scope CurrentUser
    Delegated scopes requested (all read-only):
        User.Read.All, Organization.Read.All, AuditLog.Read.All, Reports.Read.All
    You must be able to consent to these (Global Reader is sufficient).
    Prices are public list-price ESTIMATES (USD/user/month) - adjust the price
    table below to match your actual contract for exact figures.
 
    Project: https://github.com/lbcrowe-del/ServerBridge-LicenseScan
    License: MIT
#>


[CmdletBinding()]
param(
    [ValidateRange(1, 3650)]
    [int]$InactiveDays = 90,

    [string]$OutputCsv,

    [switch]$IncludeGuests,

    [switch]$PassThru
)

$script:RequiredScopes = @('User.Read.All', 'Organization.Read.All', 'AuditLog.Read.All', 'Reports.Read.All')
# Our own publisher-verified app, "ServerBridge License Auditor" (Lee Crowe Software Solutions LLC),
# so the consent screen shows who is asking. Until 2026-10-03 this was Microsoft's Graph Command Line
# Tools app. The paid tool signs in with the same app.
$script:SignInClientId = '931c3cab-1458-4cb8-b0cd-f2bd4be4482c'

# --- Approximate public list prices, USD / user / month, keyed by SkuPartNumber.
# --- ESTIMATES so the tool can put a dollar figure on waste out of the box.
# --- Edit to match your real per-seat cost for precise numbers.
$script:SkuPrice = @{
    'O365_BUSINESS_ESSENTIALS' = 6.00    # Microsoft 365 Business Basic
    'O365_BUSINESS_PREMIUM'    = 12.50   # Microsoft 365 Business Standard
    'SPB'                      = 22.00   # Microsoft 365 Business Premium
    'ENTERPRISEPACK'           = 36.00   # Office 365 E3
    'ENTERPRISEPREMIUM'        = 57.00   # Office 365 E5
    'SPE_E3'                   = 36.00   # Microsoft 365 E3
    'SPE_E5'                   = 57.00   # Microsoft 365 E5
    'SPE_F1'                   = 8.00    # Microsoft 365 F3
    'DESKLESSPACK'             = 4.00    # Office 365 F3
    'EXCHANGESTANDARD'         = 4.00    # Exchange Online Plan 1
    'EXCHANGEENTERPRISE'       = 8.00    # Exchange Online Plan 2
    'MCOEV'                    = 8.00    # Teams Phone Standard
    'POWER_BI_PRO'             = 10.00   # Power BI Pro
    'POWER_BI_PREMIUM_PER_USER' = 20.00  # Power BI Premium Per User
    'PROJECTPROFESSIONAL'      = 30.00   # Project Plan 3
    'PROJECTPREMIUM'           = 55.00   # Project Plan 5
    'VISIOCLIENT'              = 15.00   # Visio Plan 2
    'Microsoft_365_Copilot'    = 30.00   # Microsoft 365 Copilot
    'FLOW_FREE'                = 0.00    # Power Automate (free)
    'TEAMS_EXPLORATORY'        = 0.00    # Teams Exploratory (free)
    'POWERAPPS_VIRAL'          = 0.00    # Power Apps (free/viral)
}
$script:DefaultPrice = 20.00  # fallback for SKUs not in the table

# ---------------------------------------------------------------------------
# Pure, testable helpers (no Graph calls - safe to dot-source)
# ---------------------------------------------------------------------------

function Get-SkuMonthlyPrice {
    [CmdletBinding()]
    param([Parameter(Mandatory)][string]$PartNumber)
    if ($script:SkuPrice.ContainsKey($PartNumber)) { return [double]$script:SkuPrice[$PartNumber] }
    return [double]$script:DefaultPrice
}

function Test-UserDormant {
    <# $true when a user has no activity since $Cutoff (never-active counts as dormant). #>
    [CmdletBinding()]
    param(
        [Parameter()][Nullable[datetime]]$LastActivity,
        [Parameter(Mandatory)][datetime]$Cutoff
    )
    if ($null -eq $LastActivity) { return $true }
    return ($LastActivity -lt $Cutoff)
}

function Get-DormantLicenseRow {
    <#
    Pure projection: given normalized user objects (DisplayName, UserPrincipalName,
    UserType, AccountEnabled, LastActivity, AssignedLicenses[].SkuId), a
    SkuId->PartNumber map and a cutoff, emit one row per dormant user per priced
    license. No Graph, no I/O - unit-testable.
 
    A user is dormant when disabled, OR (when an activity signal is available) when
    last activity is older than the cutoff or unknown. With no activity signal only
    disabled accounts are flagged.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][AllowNull()][AllowEmptyCollection()][object[]]$Users,
        [Parameter(Mandatory)][hashtable]$SkuMap,
        [Parameter(Mandatory)][datetime]$Cutoff,
        [bool]$ActivitySignalAvailable = $true,
        [switch]$IncludeGuests
    )

    foreach ($u in @($Users | Where-Object { $null -ne $_ })) {
        if (-not $IncludeGuests -and $u.UserType -eq 'Guest') { continue }

        $dormant = $false
        $reason = $null
        if (-not $u.AccountEnabled) {
            $dormant = $true; $reason = 'disabled'
        } elseif ($ActivitySignalAvailable -and (Test-UserDormant -LastActivity $u.LastActivity -Cutoff $Cutoff)) {
            $dormant = $true; $reason = 'inactive'
        }
        if (-not $dormant) { continue }

        foreach ($lic in $u.AssignedLicenses) {
            $part = $SkuMap[$lic.SkuId]
            if (-not $part) { continue }
            $price = Get-SkuMonthlyPrice -PartNumber $part
            if ($price -le 0) { continue }  # skip free SKUs

            [pscustomobject]@{
                DisplayName       = $u.DisplayName
                UserPrincipalName = $u.UserPrincipalName
                UserType          = $u.UserType
                AccountEnabled    = $u.AccountEnabled
                Reason            = $reason
                Sku               = $part
                LastActivity      = if ($u.LastActivity) { ([datetime]$u.LastActivity).ToString('yyyy-MM-dd') } else { 'never/unknown' }
                MonthlyCost       = $price
                AnnualCost        = [math]::Round($price * 12, 2)
            }
        }
    }
}

function Get-NewestSignIn {
    <#
    The most recent of a user's sign-in timestamps, or $null when none are set.
 
    Microsoft splits sign-in activity across three properties and reading only the first one
    (lastSignInDateTime) makes people who live in Outlook or Teams on a phone look dormant, because
    those clients sign in non-interactively. Reported by robofski on r/PowerShell, 2026-09-17.
 
      * lastSuccessfulSignInDateTime - "the account was truly accessed", interactive OR
        non-interactive. Best signal, but Microsoft only started populating it in Dec 2023 and did
        not backfill, so older tenants can have it empty.
      * lastSignInDateTime - INTERACTIVE attempts only, successful or not.
      * lastNonInteractiveSignInDateTime - client sign-ins on the user's behalf (mobile mail,
        Teams). Microsoft's own inactive-user guidance says to use it alongside the interactive one.
 
    Taking the newest of the three deliberately errs toward "active": under-flagging costs a missed
    saving, over-flagging tells someone to remove a license from a person who is still working.
    #>

    [CmdletBinding()]
    param(
        [Parameter()][AllowNull()][object]$Interactive,
        [Parameter()][AllowNull()][object]$NonInteractive,
        [Parameter()][AllowNull()][object]$Successful
    )

    $dates = @($Successful, $Interactive, $NonInteractive) |
        ForEach-Object { if ($_) { $_ -as [datetime] } } |
        Where-Object { $_ }
    if (-not $dates) { return $null }
    return (@($dates) | Sort-Object -Descending)[0]
}

function Test-TenantPopulatesSuccessfulSignIn {
    <#
    $true when at least one account in the tenant has lastSuccessfulSignInDateTime set.
 
    That one populated value is what proves Microsoft is filling the property in this tenant. An
    empty tenant returns $false: with nothing to go on, fall back to the wider reading rather than
    assume.
    #>

    [CmdletBinding()]
    param([Parameter(Mandatory)][AllowNull()][AllowEmptyCollection()][object[]]$Accounts)

    foreach ($a in @($Accounts | Where-Object { $null -ne $_ })) {
        if ($a.Successful) { return $true }
    }
    return $false
}

function Get-TenantSignInMap {
    <#
    Last activity per account (keyed by lower-case UPN), with the tenant-wide guard applied.
 
    Takes objects with Upn, Interactive, NonInteractive and Successful. Two passes on purpose:
    whether lastSuccessfulSignInDateTime can be trusted is a fact about the TENANT, so it cannot be
    decided one user at a time.
 
    2026-09-23: lastSignInDateTime is no longer read when it can be avoided. It logs FAILED
    attempts, so someone password-spraying a dormant account keeps its timestamp fresh - the account
    looks active, keeps its licence, and never gets reviewed. The account most worth surfacing is
    the one that gets hidden. Raised by iRyan23 on r/entra.
 
    THE GUARD: dropping a timestamp can only make accounts look LESS active, which errs toward
    telling someone to strip access from a person still working - the expensive mistake, and
    robofski's bug from the other direction. That risk only exists if lastSuccessfulSignInDateTime
    is not populated at all, so the stricter reading is used only when some account has it set.
    #>

    [CmdletBinding()]
    param([Parameter(Mandatory)][AllowNull()][AllowEmptyCollection()][object[]]$Accounts)

    $accountList = @($Accounts | Where-Object { $null -ne $_ })
    $trustSuccessful = Test-TenantPopulatesSuccessfulSignIn -Accounts $accountList

    $map = @{}
    foreach ($a in $accountList) {
        if ([string]::IsNullOrWhiteSpace($a.Upn)) { continue }
        $map[$a.Upn.ToLower()] = if ($trustSuccessful) {
            Get-NewestSignIn -Interactive $null -NonInteractive $a.NonInteractive -Successful $a.Successful
        } else {
            Get-NewestSignIn -Interactive $a.Interactive -NonInteractive $a.NonInteractive -Successful $a.Successful
        }
    }
    return $map
}

# ---------------------------------------------------------------------------
# Graph I/O (kept in functions so tests can dot-source without a tenant)
# ---------------------------------------------------------------------------

function Get-SignInActivityMap {
    <#
    Returns @{ ok=$bool; map=@{ upn(lower) -> [datetime] } }. ok=$false when the
    tenant lacks Entra ID P1 (signInActivity is premium-only) so the caller can
    fall back. Other errors are rethrown.
    #>

    [CmdletBinding()] param()
    $map = @{}
    try {
        $users = Get-MgUser -All -Property 'userPrincipalName,signInActivity' `
            -Filter 'assignedLicenses/$count ne 0' -ConsistencyLevel eventual `
            -CountVariable siaCount -ErrorAction Stop
        # Collect the raw timestamps first and judge afterwards: whether lastSuccessfulSignInDateTime
        # can be trusted is a fact about the TENANT - see Get-TenantSignInMap.
        $accounts = foreach ($u in $users) {
            if ($u.UserPrincipalName) {
                [pscustomobject]@{
                    Upn            = $u.UserPrincipalName
                    Interactive    = $u.SignInActivity.LastSignInDateTime
                    NonInteractive = $u.SignInActivity.LastNonInteractiveSignInDateTime
                    Successful     = $u.SignInActivity.LastSuccessfulSignInDateTime
                }
            }
        }
        $map = Get-TenantSignInMap -Accounts @($accounts)
        return @{ ok = $true; map = $map }
    } catch {
        $msg = "$($_.Exception.Message)"
        if ($msg -match 'NonPremium' -or $msg -match 'premium') { return @{ ok = $false; map = $map } }
        throw
    }
}

function Get-UsageReportActivityMap {
    <#
    Fallback that needs no Entra premium: pulls the Microsoft 365 active-user
    detail report and returns @{ ok=$bool; map=@{ upn(lower) -> [datetime] } }.
    ok=$false when the tenant de-identifies report data (UPNs masked) so activity
    can't be matched to users.
    #>

    [CmdletBinding()] param([int]$InactiveDays = 90)

    # NB: the Graph reports API keeps the legacy "Office365" name for this function.
    $period = if ($InactiveDays -le 30) { 'D30' } elseif ($InactiveDays -le 90) { 'D90' } else { 'D180' }
    $uri = "https://graph.microsoft.com/v1.0/reports/getOffice365ActiveUserDetail(period='$period')"
    $resp = Invoke-MgGraphRequest -Method GET -Uri $uri -OutputType HttpResponseMessage -ErrorAction Stop
    $csv = $resp.Content.ReadAsStringAsync().Result
    $rows = @($csv | ConvertFrom-Csv)

    $map = @{}
    $sawUpn = $false
    foreach ($r in $rows) {
        $upn = $r.'User Principal Name'
        if (-not $upn) { continue }
        if ($upn -like '*@*') { $sawUpn = $true }
        # Take the most recent activity across every per-service "* Last Activity Date" column.
        $maxDate = $null
        foreach ($p in $r.PSObject.Properties) {
            if ($p.Name -like '*Last Activity Date' -and $p.Value) {
                $d = $p.Value -as [datetime]
                if ($d -and ($null -eq $maxDate -or $d -gt $maxDate)) { $maxDate = $d }
            }
        }
        $map[$upn.ToLower()] = $maxDate
    }
    if (-not $sawUpn -and $rows.Count -gt 0) { return @{ ok = $false; map = @{} } }
    return @{ ok = $true; map = $map }
}

function Test-SignInTimedOut {
    <#
    $true when Connect-MgGraph gave up because the device code wasn't used in time. Microsoft's
    Graph PowerShell module allows 2 minutes and the limit can't be changed, so the scan offers a
    new code instead of making the admin start over.
    #>

    [CmdletBinding()]
    param([Parameter(Mandatory)][AllowEmptyString()][string]$Message)
    return ($Message -match 'timed out after \d+ seconds')
}

function ConvertTo-SafeCsvValue {
    <#
    Spreadsheet apps run a cell as a formula when it starts with = + - @ (or tab/CR).
    Directory text such as a guest's display name is attacker-controllable, so prefix a
    single quote to keep it as plain text when the CSV is opened in Excel.
    #>

    [CmdletBinding()]
    param([Parameter(Mandatory)][AllowNull()][AllowEmptyString()][object]$Value)
    if ($Value -is [string] -and $Value -match '^[=+\-@\t\r]') { return "'" + $Value }
    return $Value
}

function Get-ConcealedNamesHelpText {
    <# The one-minute fix for de-identified usage reports, as lines of text (testable). #>
    [CmdletBinding()] param()
    @(
        'Microsoft is hiding user names in your usage reports, so this scan cannot tell who is inactive.'
        'Fix it in about a minute (needs a Global Administrator):'
        ' 1. Open https://admin.microsoft.com'
        ' 2. Go to Settings > Org settings > Services > Reports'
        ' 3. Untick "Conceal user, group, and site names in all reports", then click Save'
        'This only changes what admins see in Microsoft''s own reports. You can turn it back on after the audit.'
    )
}

function Test-GraphSdkPresent {
    <#
    $true when the Microsoft Graph PowerShell SDK is installed; otherwise prints the install hint
    and returns $false. Shared so every command in this module gives the same first-run message.
    #>

    [CmdletBinding()] param()
    if (Get-Module -ListAvailable -Name Microsoft.Graph.Authentication) { return $true }
    Write-Host 'Microsoft Graph PowerShell SDK not found.' -ForegroundColor Yellow
    Write-Host 'Install it with: Install-Module Microsoft.Graph -Scope CurrentUser' -ForegroundColor Yellow
    return $false
}

function Get-PaidToolNote {
    <#
    The line shown after a scan about the paid Auditor, chosen from what the scan actually found.
    Pure - returns the lines, prints nothing, so it can be tested.
 
    Why this exists: 43 free installs and 0 paid sales (METRICS 2026-09-29). The old line was a
    generic feature list shown to everyone regardless of their numbers. This says something true
    about THEIR tenant at the moment they have just seen the figure.
 
    The important half is the negative case. Below the Starter price the honest answer is "don't
    buy it", and saying so is worth more than a sale that gets refunded. A tool that tells you when
    it is not worth paying for is one you trust the next time it says something IS worth doing.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][double]$AnnualRecoverable,
        [double]$StarterPrice = 149
    )

    if ($AnnualRecoverable -le 0) {
        return @(
            @{ Text = 'Nothing recoverable today, so there is nothing here worth paying for.'; Color = 'DarkGray' }
            @{ Text = 'Worth re-running after your next round of leavers.'; Color = 'DarkGray' }
        )
    }

    if ($AnnualRecoverable -lt $StarterPrice) {
        return @(
            @{ Text = ("At `${0:N0}/year recoverable, the paid Auditor (from `${1:N0}/year) would not pay for itself." -f $AnnualRecoverable, $StarterPrice); Color = 'DarkGray' }
            @{ Text = 'Use the CSV above and keep the money.'; Color = 'DarkGray' }
        )
    }

    return @(
        @{ Text = ("You are paying `${0:N0}/year for seats nobody is using." -f $AnnualRecoverable); Color = 'White' }
        @{ Text = 'The paid Auditor puts that in a PDF you can hand to whoever signs off the spend,'; Color = 'DarkGray' }
        @{ Text = 'with what to do about each account - and re-runs it on a schedule so it stays fixed.'; Color = 'DarkGray' }
        @{ Text = ' https://server-bridge.com/license-auditor.html'; Color = 'Cyan' }
    )
}

function Show-PaidToolNote {
    <# Prints Get-PaidToolNote's lines. Separated so the decision itself stays testable. #>
    [CmdletBinding()]
    param([Parameter(Mandatory)][double]$AnnualRecoverable)

    Write-Host ''
    foreach ($line in Get-PaidToolNote -AnnualRecoverable $AnnualRecoverable) {
        Write-Host $line.Text -ForegroundColor $line.Color
    }
}

function Show-GraphReadFailure {
    <#
    One place for "the tenant read failed", so both commands say the same thing.
 
    Splits out the personal-account case. Microsoft returns "This API is not supported for MSA
    accounts" when someone signs in with an outlook.com/hotmail.com/personal account: there is no
    directory behind it, so every directory call fails. Telling that person to check their consented
    scopes sends them somewhere useless - the permissions are fine, the account is wrong.
 
    Easy mistake rather than a careless one: the sign-in page reuses whatever Microsoft session the
    browser already has, so a personal account gets picked silently. Hit 2026-09-23.
    #>

    [CmdletBinding()]
    param([Parameter(Mandatory)][AllowEmptyString()][string]$Message)

    if ($Message -match 'MSA account') {
        Write-Host ''
        Write-Host "You're signed in with a personal Microsoft account, which has no organization behind it." -ForegroundColor Yellow
        Write-Host 'Sign in again with your work or school account (the one on your company domain).' -ForegroundColor Yellow
        Write-Host 'If the sign-in page skips the account picker it is reusing a session you already had -' -ForegroundColor DarkGray
        Write-Host 'choose "Use another account", or sign in from a private browser window.' -ForegroundColor DarkGray
        return
    }

    Write-Host "Graph read failed: $Message" -ForegroundColor Red
    Write-Host 'Confirm you consented to User.Read.All, Organization.Read.All, AuditLog.Read.All and Reports.Read.All.' -ForegroundColor Yellow
}

function Connect-ScanGraph {
    <#
    Device-code sign-in with the read-only scopes, offering a fresh code when Microsoft's
    2-minute limit expires. Returns $true once connected, $false if the user quit or it failed.
 
    Shared by every command in this module: this retry behaviour was a real bug fix (1.1.1) and
    must not be reimplemented per command.
    #>

    [CmdletBinding()] param()
    $signedIn = $false
    while (-not $signedIn) {
        try {
            Write-Host 'Signing you in. A device code will appear below - enter it within 2 minutes.' -ForegroundColor Cyan
            # Out-Host, not a bare call. Connect-MgGraph -UseDeviceCode writes the code to the
            # SUCCESS stream, and both commands invoke this function as `if (-not (Connect-ScanGraph))`
            # - which captures that stream. The code then lands in this function's return value
            # instead of on screen, so the user sees "a device code will appear below", no code, and
            # a 2-minute timeout. Sign-in was impossible. Found 2026-09-22 on Graph SDK 2.38.0.
            Connect-MgGraph -ClientId $script:SignInClientId -Scopes $script:RequiredScopes -UseDeviceCode -NoWelcome -ContextScope Process -ErrorAction Stop |
                Out-Host
            $signedIn = $true
        } catch {
            $message = "$($_.Exception.Message)"
            if ((Test-SignInTimedOut -Message $message) -and -not [Console]::IsInputRedirected) {
                Write-Host 'The sign-in code expired (Microsoft allows 2 minutes).' -ForegroundColor Yellow
                $answer = Read-Host 'Press Enter for a new code, or type Q to quit'
                if ($answer -match '^\s*[qQ]') { return $false }
                continue
            }
            Write-Host "Sign-in failed: $message" -ForegroundColor Red
            return $false
        }
    }
    if (-not (Get-MgContext)) { Write-Host 'Sign-in cancelled.' -ForegroundColor Red; return $false }
    return $true
}

function Resolve-ActivityMap {
    <#
    Picks the best available activity signal and returns
    @{ available=$bool; map=@{ upn(lower) -> [datetime] }; signalName=[string] }.
 
    Prefers directory sign-in activity (needs Entra ID P1), else the Microsoft 365 usage reports.
    When the tenant conceals user names in those reports it shows the one-minute fix and re-checks
    on Enter without a new sign-in. available=$false means the admin skipped, so only disabled
    accounts can be judged.
 
    Shared by every command in this module.
    #>

    [CmdletBinding()] param([int]$InactiveDays = 90)

    $sia = Get-SignInActivityMap
    if ($sia.ok) {
        return @{ available = $true; map = $sia.map; signalName = 'directory sign-in activity' }
    }

    Write-Host 'Sign-in activity needs Entra ID P1; falling back to Microsoft 365 usage reports...' -ForegroundColor Yellow
    $rep = $null
    $attempt = 0
    while ($true) {
        try {
            $rep = Get-UsageReportActivityMap -InactiveDays $InactiveDays
        } catch {
            $rep = $null
            Write-Host "Usage reports unavailable: $($_.Exception.Message)" -ForegroundColor Yellow
            break
        }
        if ($rep.ok) { break }

        $attempt++
        Write-Host ''
        if ($attempt -gt 1) {
            Write-Host 'Names are still hidden. Microsoft can take a few minutes to apply the change.' -ForegroundColor Yellow
        } else {
            Get-ConcealedNamesHelpText | ForEach-Object { Write-Host $_ -ForegroundColor Yellow }
        }
        if ([Console]::IsInputRedirected) { break }
        $answer = Read-Host 'Press Enter when done to check again (no new sign-in), or type S to skip'
        if ($answer -match '^\s*[sS]') { break }
    }

    if ($rep -and $rep.ok) {
        return @{ available = $true; map = $rep.map; signalName = 'Microsoft 365 usage reports' }
    }
    return @{ available = $false; map = @{}; signalName = $null }
}

function Invoke-LicenseScanMain {
    [CmdletBinding()]
    param(
        [int]$InactiveDays,
        [string]$OutputCsv,
        [switch]$IncludeGuests,
        [switch]$PassThru
    )

    Write-Host ''
    Write-Host 'ServerBridge - Free M365 License Scan (read-only)' -ForegroundColor Green
    Write-Host '-------------------------------------------------' -ForegroundColor DarkGray

    if (-not (Test-GraphSdkPresent)) { return }
    if (-not (Connect-ScanGraph)) { return }

    try {
        $org = Get-MgOrganization -ErrorAction Stop
        $tenantName = ($org.DisplayName | Select-Object -First 1)
        Write-Host "Connected to: $tenantName (signed in as $((Get-MgContext).Account))" -ForegroundColor Green

        $skuMap = @{}
        foreach ($s in (Get-MgSubscribedSku -All -ErrorAction Stop)) { $skuMap[$s.SkuId] = $s.SkuPartNumber }

        $select = 'id,displayName,userPrincipalName,userType,accountEnabled,assignedLicenses'
        $rawUsers = Get-MgUser -All -Property $select -Filter 'assignedLicenses/$count ne 0' `
            -ConsistencyLevel eventual -CountVariable userCount -ErrorAction Stop
    } catch {
        Show-GraphReadFailure -Message "$($_.Exception.Message)"
        Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
        return
    }

    # Resolve an activity signal: prefer sign-in activity (premium), fall back to usage reports.
    $signal = Resolve-ActivityMap -InactiveDays $InactiveDays
    $activity = $signal.map
    $signalAvailable = $signal.available
    $signalName = $signal.signalName
    if (-not $signalAvailable) {
        Write-Host ''
        Write-Host 'PARTIAL AUDIT: only disabled accounts that still hold licenses can be checked.' -ForegroundColor Yellow
    }

    if ($signalAvailable) {
        Write-Host "Flagging licensed users dormant $InactiveDays+ days (via $signalName)..." -ForegroundColor Cyan
    }

    $normalized = foreach ($u in $rawUsers) {
        [pscustomobject]@{
            DisplayName       = $u.DisplayName
            UserPrincipalName = $u.UserPrincipalName
            UserType          = $u.UserType
            AccountEnabled    = $u.AccountEnabled
            AssignedLicenses  = $u.AssignedLicenses
            LastActivity      = if ($u.UserPrincipalName) { $activity[$u.UserPrincipalName.ToLower()] } else { $null }
        }
    }

    if ($signalAvailable) {
        $resolved = @($normalized | Where-Object { $_.LastActivity }).Count
        Write-Host (" activity resolved for {0} of {1} licensed users" -f $resolved, @($normalized).Count) -ForegroundColor DarkGray
    }

    $cutoff = (Get-Date).AddDays(-1 * $InactiveDays)
    $dormant = @(Get-DormantLicenseRow -Users $normalized -SkuMap $skuMap -Cutoff $cutoff `
            -ActivitySignalAvailable $signalAvailable -IncludeGuests:$IncludeGuests)

    Write-Host ''
    Write-Host 'Wasted spend by license' -ForegroundColor Green
    Write-Host '-----------------------' -ForegroundColor DarkGray
    if ($dormant.Count -eq 0) {
        Write-Host ' Nothing obvious to reclaim.' -ForegroundColor Green
    } else {
        $dormant | Group-Object Sku | ForEach-Object {
            [pscustomobject]@{
                License      = $_.Name
                DormantSeats = $_.Count
                'Annual $'   = [int][math]::Round((($_.Group | Measure-Object AnnualCost -Sum).Sum), 0)
            }
        } | Sort-Object 'Annual $' -Descending | Format-Table -AutoSize
    }

    $totalAnnual = [math]::Round((($dormant | Measure-Object AnnualCost -Sum).Sum), 0)
    Write-Host (" Reclaimable seats : {0:N0}" -f $dormant.Count) -ForegroundColor White
    Write-Host (" Wasted spend : `${0:N0} / year" -f $totalAnnual) -ForegroundColor Green
    Write-Host ''

    if (-not $OutputCsv) {
        $safeTenant = ($tenantName -replace '[^\w]', '')
        $OutputCsv = Join-Path (Get-Location) ("license-scan_{0}_{1}.csv" -f $safeTenant, (Get-Date -Format 'yyyyMMdd'))
    }
    if ($dormant.Count -gt 0) {
        $dormant | Sort-Object AnnualCost -Descending | ForEach-Object {
            $row = $_.PSObject.Copy()
            foreach ($name in 'DisplayName', 'UserPrincipalName', 'UserType', 'Sku') {
                $row.$name = ConvertTo-SafeCsvValue -Value $row.$name
            }
            $row
        } | Export-Csv -Path $OutputCsv -NoTypeInformation -Encoding UTF8
        Write-Host "Full per-user list saved to:" -ForegroundColor Cyan
        Write-Host " $OutputCsv" -ForegroundColor White
        Write-Host ''
    }

    Write-Host 'Prices are list-price estimates - edit the price table for exact figures.' -ForegroundColor DarkGray
    Write-Host 'Got a number that looks wrong, or something confusing? Tell me:' -ForegroundColor DarkGray
    Write-Host ' https://github.com/lbcrowe-del/ServerBridge-LicenseScan/issues' -ForegroundColor Cyan

    Show-PaidToolNote -AnnualRecoverable $totalAnnual
    Write-Host ''

    Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
    if ($PassThru) { $dormant }
}

# Run unless dot-sourced (e.g. by Pester tests, where InvocationName is '.').
if ($MyInvocation.InvocationName -ne '.') {
    Invoke-LicenseScanMain -InactiveDays $InactiveDays -OutputCsv $OutputCsv `
        -IncludeGuests:$IncludeGuests -PassThru:$PassThru
}

# SIG # Begin signature block
# MII2yAYJKoZIhvcNAQcCoII2uTCCNrUCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCC7kdCWMMfft+L9
# +wLT68nPh2qa3HS26pCk+vaPHIPS36CCG2MwggXMMIIDtKADAgECAhBUmNLR1FsZ
# lUgTecgRwIeZMA0GCSqGSIb3DQEBDAUAMHcxCzAJBgNVBAYTAlVTMR4wHAYDVQQK
# ExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xSDBGBgNVBAMTP01pY3Jvc29mdCBJZGVu
# dGl0eSBWZXJpZmljYXRpb24gUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAy
# MDAeFw0yMDA0MTYxODM2MTZaFw00NTA0MTYxODQ0NDBaMHcxCzAJBgNVBAYTAlVT
# MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xSDBGBgNVBAMTP01pY3Jv
# c29mdCBJZGVudGl0eSBWZXJpZmljYXRpb24gUm9vdCBDZXJ0aWZpY2F0ZSBBdXRo
# b3JpdHkgMjAyMDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALORKgeD
# Bmf9np3gx8C3pOZCBH8Ppttf+9Va10Wg+3cL8IDzpm1aTXlT2KCGhFdFIMeiVPvH
# or+Kx24186IVxC9O40qFlkkN/76Z2BT2vCcH7kKbK/ULkgbk/WkTZaiRcvKYhOuD
# PQ7k13ESSCHLDe32R0m3m/nJxxe2hE//uKya13NnSYXjhr03QNAlhtTetcJtYmrV
# qXi8LW9J+eVsFBT9FMfTZRY33stuvF4pjf1imxUs1gXmuYkyM6Nix9fWUmcIxC70
# ViueC4fM7Ke0pqrrBc0ZV6U6CwQnHJFnni1iLS8evtrAIMsEGcoz+4m+mOJyoHI1
# vnnhnINv5G0Xb5DzPQCGdTiO0OBJmrvb0/gwytVXiGhNctO/bX9x2P29Da6SZEi3
# W295JrXNm5UhhNHvDzI9e1eM80UHTHzgXhgONXaLbZ7LNnSrBfjgc10yVpRnlyUK
# xjU9lJfnwUSLgP3B+PR0GeUw9gb7IVc+BhyLaxWGJ0l7gpPKWeh1R+g/OPTHU3mg
# trTiXFHvvV84wRPmeAyVWi7FQFkozA8kwOy6CXcjmTimthzax7ogttc32H83rwjj
# O3HbbnMbfZlysOSGM1l0tRYAe1BtxoYT2v3EOYI9JACaYNq6lMAFUSw0rFCZE4e7
# swWAsk0wAly4JoNdtGNz764jlU9gKL431VulAgMBAAGjVDBSMA4GA1UdDwEB/wQE
# AwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTIftJqhSobyhmYBAcnz1AQ
# T2ioojAQBgkrBgEEAYI3FQEEAwIBADANBgkqhkiG9w0BAQwFAAOCAgEAr2rd5hnn
# LZRDGU7L6VCVZKUDkQKL4jaAOxWiUsIWGbZqWl10QzD0m/9gdAmxIR6QFm3FJI9c
# Zohj9E/MffISTEAQiwGf2qnIrvKVG8+dBetJPnSgaFvlVixlHIJ+U9pW2UYXeZJF
# xBA2CFIpF8svpvJ+1Gkkih6PsHMNzBxKq7Kq7aeRYwFkIqgyuH4yKLNncy2RtNwx
# AQv3Rwqm8ddK7VZgxCwIo3tAsLx0J1KH1r6I3TeKiW5niB31yV2g/rarOoDXGpc8
# FzYiQR6sTdWD5jw4vU8w6VSp07YEwzJ2YbuwGMUrGLPAgNW3lbBeUU0i/OxYqujY
# lLSlLu2S3ucYfCFX3VVj979tzR/SpncocMfiWzpbCNJbTsgAlrPhgzavhgplXHT2
# 6ux6anSg8Evu75SjrFDyh+3XOjCDyft9V77l4/hByuVkrrOj7FjshZrM77nq81YY
# uVxzmq/FdxeDWds3GhhyVKVB0rYjdaNDmuV3fJZ5t0GNv+zcgKCf0Xd1WF81E+Al
# GmcLfc4l+gcK5GEh2NQc5QfGNpn0ltDGFf5Ozdeui53bFv0ExpK91IjmqaOqu/dk
# ODtfzAzQNb50GQOmxapMomE2gj4d8yu8l13bS3g7LfU772Aj6PXsCyM2la+YZr9T
# 03u4aUoqlmZpxJTG9F9urJh4iIAGXKKy7aIwggbBMIIEqaADAgECAhMzAAdmgLOh
# qWn9zsb1AAAAB2aAMA0GCSqGSIb3DQEBDAUAMFoxCzAJBgNVBAYTAlVTMR4wHAYD
# VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKzApBgNVBAMTIk1pY3Jvc29mdCBJ
# RCBWZXJpZmllZCBDUyBFT0MgQ0EgMDQwHhcNMjYxMDAxMTQxOTEzWhcNMjYxMDA0
# MTQxOTEzWjCBgjELMAkGA1UEBhMCVVMxETAPBgNVBAgTCEtlbnR1Y2t5MRIwEAYD
# VQQHEwlMZXhpbmd0b24xJTAjBgNVBAoTHExlZSBDcm93ZSBTb2Z0d2FyZSBTb2x1
# dGlvbnMxJTAjBgNVBAMTHExlZSBDcm93ZSBTb2Z0d2FyZSBTb2x1dGlvbnMwggGi
# MA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCtBmpgXpaS1pmrkYwhe2KaYFI8
# 4WWTUWzev+aeBFxO5AHRl+HOfYP/nE7GuneuTe8wydQk8EWiBptLJQaRKHc9dr/N
# 2cEImV8XY7kCjTThHlUSKUknU0fSIU29jTlSIe9yjg95EXl2quN+ySyrAITZoeDW
# CDMUHBmupGveKX/Kd9otXPxYQg6u4e7vY6NiOziJ8XD0gocOSHUoYEbMQ91yNQx2
# PnRV25f4h7b9PSD0jRq4Turdt7Rn249Is5QhGsYjCqIVCK+M2+0yIF/2RIDJE187
# g82xl99wHBYpM0agsw/us8NY2/QVTwWmnWl01tBPlHMCJJE8OU4BnD14o2jlgahm
# kXoqA6k7EQnmwQ/deSL+2snLChjZtEqghRMi3Xo28Fo17nS2eEnH93ZvdQLDPLjf
# 3r9uI2dp8UFACE1rzYI12BMdHGmffm+kvgtvccReY1rzFQIomk66E9saGnq/nJz9
# gFAMZXvF1+hFIoxj5wwSpMocLUR56Wsdn15zeXUCAwEAAaOCAdUwggHRMAwGA1Ud
# EwEB/wQCMAAwDgYDVR0PAQH/BAQDAgeAMDwGA1UdJQQ1MDMGCisGAQQBgjdhAQAG
# CCsGAQUFBwMDBhsrBgEEAYI3YfX3xXqCm/n9aIL52tE6goGumFEwHQYDVR0OBBYE
# FKcc1Wwp/dW+ZxL7FMsYuTwrsB3mMB8GA1UdIwQYMBaAFJrxVHd1DIcWN0agrN55
# +fR/wXjpMGcGA1UdHwRgMF4wXKBaoFiGVmh0dHA6Ly93d3cubWljcm9zb2Z0LmNv
# bS9wa2lvcHMvY3JsL01pY3Jvc29mdCUyMElEJTIwVmVyaWZpZWQlMjBDUyUyMEVP
# QyUyMENBJTIwMDQuY3JsMHQGCCsGAQUFBwEBBGgwZjBkBggrBgEFBQcwAoZYaHR0
# cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNyb3NvZnQlMjBJ
# RCUyMFZlcmlmaWVkJTIwQ1MlMjBFT0MlMjBDQSUyMDA0LmNydDBUBgNVHSAETTBL
# MEkGBFUdIAAwQTA/BggrBgEFBQcCARYzaHR0cDovL3d3dy5taWNyb3NvZnQuY29t
# L3BraW9wcy9Eb2NzL1JlcG9zaXRvcnkuaHRtMA0GCSqGSIb3DQEBDAUAA4ICAQB6
# aQKa9QMQNOfbXEyjnlqhTW1kCXyzFn75ptujryqfy42UVFt9jf4DwjWYBKxoe/vQ
# OGei5GMP6ucSqOwGdFvUn89V58BqVyvZ6BIMH6c6EgtU9CK4IAO+ow3NmsFODY+F
# Ug65tpG2tPITNKeeGb1wvIlMKmLVqrVVMTWAMTf/UDX9InVDVabbi6BZYJBhhFQ6
# TiWSMzsCoX11T+9z8NiZ9hh/xMlldzRZw/ZBPIXv6B/Y3Ggxc0ryzpfA/h5kWdy5
# ahMPU6HfgNv9Luszzi/mIsp35tci685iB0h8sI3oTDU0AbWkPBUC90K28kNQJheR
# 9CHQlz7+cIYHUPonNm4K+KK8WaEqI2xrRGziXGFpSpY7HHqGN6gW4KH4m8Fl9qtl
# uQE+rNbnLo8ICUhhj2adRwKmZpzkVkMpdCgwUaDYhQit8UIDSlnP/Td7JhbXTAHr
# NMsFpBMciPmGFFtXtphZk5R77M9mM3wTeg1QPfoC/rgPL1kCTKnHK00wEvWbIIMn
# wujoPJnCE/IZuOdxzd/3ObBL3KneCWZ86gRAtBwoMNYCmh4TFwUssq1k0JfyHH1v
# lldkcSqqpo+4aXJwhpihoDGYke0PywECP0Ppq+nairXWH9RwBFvJpyMcms4PILZu
# 9eis+lDgNP79+dZWuUh7eWCe3MPNa03Fp6gbkkoMZjCCBygwggUQoAMCAQICEzMA
# AAAXJ0UJC4uHr8YAAAAAABcwDQYJKoZIhvcNAQEMBQAwYzELMAkGA1UEBhMCVVMx
# HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjE0MDIGA1UEAxMrTWljcm9z
# b2Z0IElEIFZlcmlmaWVkIENvZGUgU2lnbmluZyBQQ0EgMjAyMTAeFw0yNjAzMjYx
# ODExMzFaFw0zMTAzMjYxODExMzFaMFoxCzAJBgNVBAYTAlVTMR4wHAYDVQQKExVN
# aWNyb3NvZnQgQ29ycG9yYXRpb24xKzApBgNVBAMTIk1pY3Jvc29mdCBJRCBWZXJp
# ZmllZCBDUyBFT0MgQ0EgMDQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC
# AQCCx2T+Aw9mKgGVzJ+Tq0PMn49G3itIsYpbx7ClLSRHFe1RELdPcZ1sIqWOhsSf
# y6yyqEapClGH9Je9FXA1cQgZvvpQbkg+QInVLr/0EPrVBCwrM96lbRI2PxNeCwXG
# 9LsyW2hG6KQgintDmNCBo4zpDIr377plVdSliZm6UB7rHwmvBnR02QT6tnrqWq2i
# hzB6lRJVTEzuh0OafzIMeMnYM0+x+ve5EOLHdfiq+HXiMf9Jb7YLHtYgyHIiJA7b
# TWLqFSLGaTh7ZlbxbsLXA91OOroEpv7OjzFuu3tkpC9FflA4Dp2Euq4+qPmxUqfG
# p+TX0gLRJp9NJOzzILjcTD3rkFFFbxUv1xyg6avivFDLtoKBhM2Td138umE1pNOa
# canuSYtPHIeQHmB6haFi64avLBLwTTAm/Rbit860cFXR72wq+5Qh4hSmezHqKXER
# WPpVBe+APrJ4Iqc+aPeMmIkoCWZQO22HnLNFUFSXjiwyIbgvlH/LIAJEqTafTzxD
# ZgKhlLU7zr6gwsq3WNpcYQI6NuxWnwh3VVDDyF7onQqKs5Ll7bleVN0Y8VvqgE45
# ppyBbvwqN/Run5fMCCRz3aYMY0kZhKO92eP7t4zHqZ5bQMAgZ0tE2Pz/jb0wiykU
# F/PcoOqqk3vVLiRDYst6vd3GEMNzMpUUvQcvBG46+COIbwIDAQABo4IB3DCCAdgw
# DgYDVR0PAQH/BAQDAgGGMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBSa8VR3
# dQyHFjdGoKzeefn0f8F46TBUBgNVHSAETTBLMEkGBFUdIAAwQTA/BggrBgEFBQcC
# ARYzaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9Eb2NzL1JlcG9zaXRv
# cnkuaHRtMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMBIGA1UdEwEB/wQIMAYB
# Af8CAQAwHwYDVR0jBBgwFoAU2UEpsA8PY2zvadf1zSmepEhqMOYwcAYDVR0fBGkw
# ZzBloGOgYYZfaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jcmwvTWlj
# cm9zb2Z0JTIwSUQlMjBWZXJpZmllZCUyMENvZGUlMjBTaWduaW5nJTIwUENBJTIw
# MjAyMS5jcmwwfQYIKwYBBQUHAQEEcTBvMG0GCCsGAQUFBzAChmFodHRwOi8vd3d3
# Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY3Jvc29mdCUyMElEJTIwVmVy
# aWZpZWQlMjBDb2RlJTIwU2lnbmluZyUyMFBDQSUyMDIwMjEuY3J0MA0GCSqGSIb3
# DQEBDAUAA4ICAQCQdVoZ/U0m38l2iKaZFlsxavptpoOLyaR1a9ZK2TSF1kOnFJhM
# Dse6KkCgsveoiEjXTVc6Xt86IKHn76Nk5qZB0BXv2iMRQ2giAJmYvZcmstoZqfB2
# M3Kd5wnJhUJOtF/b6HsqSelY6nhrF06zor1lDmDQixBZcLB9zR1+RKQso1jekNxY
# uUk+HaN3k1S57qk0O//YbkwU0mELCW04N5vICMZx5T5c7Nq/7uLvbVhCdD7f2bZp
# A4U7vOkB1ooB4AaER3pjoJ0Mad5LFyi6Na9p9Zu/hrLeOjU5FItS5YxsqvlfXxAT
# hJ176CmkYstKRmytSHZ7JhKRfV6e9Zftk/ODb/CK4pGVAVqsOf4337bQGrOHHCQ3
# IvN9gmnUuDh8JdvbheoWPHxIN1GB5sUiY584tXN7xdD8LCSsRqJvQ8e7a3gZWTgV
# iugRs1QWq+N0G9Nje6JHlN1CjJehge+H5PGktJja+juGEr0P+ukSkcL6qaZxFQTh
# 3SDI71lvW++3bl/Ezd6SO8N9Udw+reoyvRHCyTiSsplZQSBTVJdPmo3qCpGuyHFt
# Po5CBn3/FPTiqJd3M9BHoqKd0G9Kmg6fGcAvFwnLNXA2kov727wRljL3ypfqL7iA
# T/Ynpxul6RwHRlcOf9dDGg1RRvr92NP/CWVXIb68geR2rvU/NsfmtjF1wDCCB54w
# ggWGoAMCAQICEzMAAAAHh6M0o3uljhwAAAAAAAcwDQYJKoZIhvcNAQEMBQAwdzEL
# MAkGA1UEBhMCVVMxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjFIMEYG
# A1UEAxM/TWljcm9zb2Z0IElkZW50aXR5IFZlcmlmaWNhdGlvbiBSb290IENlcnRp
# ZmljYXRlIEF1dGhvcml0eSAyMDIwMB4XDTIxMDQwMTIwMDUyMFoXDTM2MDQwMTIw
# MTUyMFowYzELMAkGA1UEBhMCVVMxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3Jh
# dGlvbjE0MDIGA1UEAxMrTWljcm9zb2Z0IElEIFZlcmlmaWVkIENvZGUgU2lnbmlu
# ZyBQQ0EgMjAyMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALLwwK8Z
# iCji3VR6TElsaQhVCbRS/3pK+MHrJSj3Zxd3KU3rlfL3qrZilYKJNqztA9OQacr1
# AwoNcHbKBLbsQAhBnIB34zxf52bDpIO3NJlfIaTE/xrweLoQ71lzCHkD7A4As1Bs
# 076Iu+mA6cQzsYYH/Cbl1icwQ6C65rU4V9NQhNUwgrx9rGQ//h890Q8JdjLLw0nV
# +ayQ2Fbkd242o9kH82RZsH3HEyqjAB5a8+Ae2nPIPc8sZU6ZE7iRrRZywRmrKDp5
# +TcmJX9MRff241UaOBs4NmHOyke8oU1TYrkxh+YeHgfWo5tTgkoSMoayqoDpHOLJ
# s+qG8Tvh8SnifW2Jj3+ii11TS8/FGngEaNAWrbyfNrC69oKpRQXY9bGH6jn9NEJv
# 9weFxhTwyvx9OJLXmRGbAUXN1U9nf4lXezky6Uh/cgjkVd6CGUAf0K+Jw+GE/5Vp
# IVbcNr9rNE50Sbmy/4RTCEGvOq3GhjITbCa4crCzTTHgYYjHs1NbOc6brH+eKpWL
# tr+bGecy9CrwQyx7S/BfYJ+ozst7+yZtG2wR461uckFu0t+gCwLdN0A6cFtSRtR8
# bvxVFyWwTtgMMFRuBa3vmUOTnfKLsLefRaQcVTgRnzeLzdpt32cdYKp+dhr2ogc+
# qM6K4CBI5/j4VFyC4QFeUP2YAidLtvpXRRo3AgMBAAGjggI1MIICMTAOBgNVHQ8B
# Af8EBAMCAYYwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFNlBKbAPD2Ns72nX
# 9c0pnqRIajDmMFQGA1UdIARNMEswSQYEVR0gADBBMD8GCCsGAQUFBwIBFjNodHRw
# Oi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL0RvY3MvUmVwb3NpdG9yeS5odG0w
# GQYJKwYBBAGCNxQCBAweCgBTAHUAYgBDAEEwDwYDVR0TAQH/BAUwAwEB/zAfBgNV
# HSMEGDAWgBTIftJqhSobyhmYBAcnz1AQT2ioojCBhAYDVR0fBH0wezB5oHegdYZz
# aHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jcmwvTWljcm9zb2Z0JTIw
# SWRlbnRpdHklMjBWZXJpZmljYXRpb24lMjBSb290JTIwQ2VydGlmaWNhdGUlMjBB
# dXRob3JpdHklMjAyMDIwLmNybDCBwwYIKwYBBQUHAQEEgbYwgbMwgYEGCCsGAQUF
# BzAChnVodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY3Jv
# c29mdCUyMElkZW50aXR5JTIwVmVyaWZpY2F0aW9uJTIwUm9vdCUyMENlcnRpZmlj
# YXRlJTIwQXV0aG9yaXR5JTIwMjAyMC5jcnQwLQYIKwYBBQUHMAGGIWh0dHA6Ly9v
# bmVvY3NwLm1pY3Jvc29mdC5jb20vb2NzcDANBgkqhkiG9w0BAQwFAAOCAgEAfyUq
# nv7Uq+rdZgrbVyNMul5skONbhls5fccPlmIbzi+OwVdPQ4H55v7VOInnmezQEeW4
# LqK0wja+fBznANbXLB0KrdMCbHQpbLvG6UA/Xv2pfpVIE1CRFfNF4XKO8XYEa3oW
# 8oVH+KZHgIQRIwAbyFKQ9iyj4aOWeAzwk+f9E5StNp5T8FG7/VEURIVWArbAzPt9
# ThVN3w1fAZkF7+YU9kbq1bCR2YD+MtunSQ1Rft6XG7b4e0ejRA7mB2IoX5hNh3UE
# auY0byxNRG+fT2MCEhQl9g2i2fs6VOG19CNep7SquKaBjhWmirYyANb0RJSLWjin
# MLXNOAga10n8i9jqeprzSMU5ODmrMCJE12xS/NWShg/tuLjAsKP6SzYZ+1Ry358Z
# TFcx0FS/mx2vSoU8s8HRvy+rnXqyUJ9HBqS0DErVLjQwK8VtsBdekBmdTbQVoCgP
# Cqr+PDPB3xajYnzevs7eidBsM71PINK2BoE2UfMwxCCX3mccFgx6UsQeRSdVVVNS
# yALQe6PT12418xon2iDGE81OGCreLzDcMAZnrUAx4XQLUz6ZTl65yPUiOh3k7Yww
# 94lDf+8oG2oZmDh5O1Qe38E+M3vhKwmzIeoB1dVLlz4i3IpaDcR+iuGjH2TdaC1Z
# OmBXiCRKJLj4DT2uhJ04ji+tHD6n58vhavFIrmcxghq7MIIatwIBATBxMFoxCzAJ
# BgNVBAYTAlVTMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKzApBgNV
# BAMTIk1pY3Jvc29mdCBJRCBWZXJpZmllZCBDUyBFT0MgQ0EgMDQCEzMAB2aAs6Gp
# af3OxvUAAAAHZoAwDQYJYIZIAWUDBAIBBQCggYQwGAYKKwYBBAGCNwIBDDEKMAig
# AoAAoQKAADAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgorBgEEAYI3AgEL
# MQ4wDAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQgYEETGtuCfJU3cgakTjU6
# 8Hq1fV1/OmFXFBZaIKn2RHwwDQYJKoZIhvcNAQEBBQAEggGAW+wg45HHOAPT16Aa
# FjoOOVhCfgrXCeDAQEfmG7RJLxaYmj+Tos43KtT4d5lSZ7Q72+lxqnkteLSXvBB1
# VhsNc0fsqZ0QuZQ+jPsgcsz7kMY4LeX/JiglpAD8f/0ukPL0tiN8fg/sXmmXNm++
# TkaHkDZvfYDxEw5f58xdQX4dMCyZ6An3cob3vYQnlK3Dlfk9gcqjapbZYkKEFyB7
# bqnJO7xYabqy5fnq6fuyNg9I+P0ItPb74JOUeT7FY1KmAG9yNwqEXWHzgxzcYY9C
# 8MEXQh9PlppGXEEc759H34AMoQ3FWfWsdfVbjLgao+U47CDY3+bYHS5aq8Wd+OaT
# QrI2awRnnDCAriE9rHhoiFTFYyermeIgWVC2nthYLOM2zfieSuAPSeAJn8w5/pfb
# tTgZuXn9T2BuDRNOtqPneE5oI6aTQ5xAPTD8yC6loEvQC0mWoFASPanOjt9++BFJ
# M9IrJ8LU5t5k2PeJUPGKa/6Ov1yNHIy1k1bIBOUra6iZ9hU0oYIYFDCCGBAGCisG
# AQQBgjcDAwExghgAMIIX/AYJKoZIhvcNAQcCoIIX7TCCF+kCAQMxDzANBglghkgB
# ZQMEAgEFADCCAWIGCyqGSIb3DQEJEAEEoIIBUQSCAU0wggFJAgEBBgorBgEEAYRZ
# CgMBMDEwDQYJYIZIAWUDBAIBBQAEIDI3Gn6ijM4N9Se4mlHFCxN9NUbxtQLSx8tU
# /W1t3AwqAgZqqXgplmgYEzIwMjYxMDAzMTMwMTA0LjIxOFowBIACAfSggeGkgd4w
# gdsxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdS
# ZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJTAjBgNVBAsT
# HE1pY3Jvc29mdCBBbWVyaWNhIE9wZXJhdGlvbnMxJzAlBgNVBAsTHm5TaGllbGQg
# VFNTIEVTTjo3RDAwLTA1RTAtRDk0NzE1MDMGA1UEAxMsTWljcm9zb2Z0IFB1Ymxp
# YyBSU0EgVGltZSBTdGFtcGluZyBBdXRob3JpdHmggg8hMIIHgjCCBWqgAwIBAgIT
# MwAAAAXlzw//Zi7JhwAAAAAABTANBgkqhkiG9w0BAQwFADB3MQswCQYDVQQGEwJV
# UzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMUgwRgYDVQQDEz9NaWNy
# b3NvZnQgSWRlbnRpdHkgVmVyaWZpY2F0aW9uIFJvb3QgQ2VydGlmaWNhdGUgQXV0
# aG9yaXR5IDIwMjAwHhcNMjAxMTE5MjAzMjMxWhcNMzUxMTE5MjA0MjMxWjBhMQsw
# CQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTIwMAYD
# VQQDEylNaWNyb3NvZnQgUHVibGljIFJTQSBUaW1lc3RhbXBpbmcgQ0EgMjAyMDCC
# AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJ5851Jj/eDFnwV9Y7UGIqMc
# HtfnlzPREwW9ZUZHd5HBXXBvf7KrQ5cMSqFSHGqg2/qJhYqOQxwuEQXG8kB41wsD
# JP5d0zmLYKAY8Zxv3lYkuLDsfMuIEqvGYOPURAH+Ybl4SJEESnt0MbPEoKdNihwM
# 5xGv0rGofJ1qOYSTNcc55EbBT7uq3wx3mXhtVmtcCEr5ZKTkKKE1CxZvNPWdGWJU
# PC6e4uRfWHIhZcgCsJ+sozf5EeH5KrlFnxpjKKTavwfFP6XaGZGWUG8TZaiTogRo
# AlqcevbiqioUz1Yt4FRK53P6ovnUfANjIgM9JDdJ4e0qiDRm5sOTiEQtBLGd9Vhd
# 1MadxoGcHrRCsS5rO9yhv2fjJHrmlQ0EIXmp4DhDBieKUGR+eZ4CNE3ctW4uvSDQ
# VeSp9h1SaPV8UWEfyTxgGjOsRpeexIveR1MPTVf7gt8hY64XNPO6iyUGsEgt8c2P
# xF87E+CO7A28TpjNq5eLiiunhKbq0XbjkNoU5JhtYUrlmAbpxRjb9tSreDdtACpm
# 3rkpxp7AQndnI0Shu/fk1/rE3oWsDqMX3jjv40e8KN5YsJBnczyWB4JyeeFMW3JB
# fdeAKhzohFe8U5w9WuvcP1E8cIxLoKSDzCCBOu0hWdjzKNu8Y5SwB1lt5dQhABYy
# zR3dxEO/T1K/BVF3rV69AgMBAAGjggIbMIICFzAOBgNVHQ8BAf8EBAMCAYYwEAYJ
# KwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFGtpKDo1L0hjQM972K9J6T7ZPdshMFQG
# A1UdIARNMEswSQYEVR0gADBBMD8GCCsGAQUFBwIBFjNodHRwOi8vd3d3Lm1pY3Jv
# c29mdC5jb20vcGtpb3BzL0RvY3MvUmVwb3NpdG9yeS5odG0wEwYDVR0lBAwwCgYI
# KwYBBQUHAwgwGQYJKwYBBAGCNxQCBAweCgBTAHUAYgBDAEEwDwYDVR0TAQH/BAUw
# AwEB/zAfBgNVHSMEGDAWgBTIftJqhSobyhmYBAcnz1AQT2ioojCBhAYDVR0fBH0w
# ezB5oHegdYZzaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jcmwvTWlj
# cm9zb2Z0JTIwSWRlbnRpdHklMjBWZXJpZmljYXRpb24lMjBSb290JTIwQ2VydGlm
# aWNhdGUlMjBBdXRob3JpdHklMjAyMDIwLmNybDCBlAYIKwYBBQUHAQEEgYcwgYQw
# gYEGCCsGAQUFBzAChnVodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2Nl
# cnRzL01pY3Jvc29mdCUyMElkZW50aXR5JTIwVmVyaWZpY2F0aW9uJTIwUm9vdCUy
# MENlcnRpZmljYXRlJTIwQXV0aG9yaXR5JTIwMjAyMC5jcnQwDQYJKoZIhvcNAQEM
# BQADggIBAF+Idsd+bbVaFXXnTHho+k7h2ESZJRWluLE0Oa/pO+4ge/XEizXvhs0Y
# 7+KVYyb4nHlugBesnFqBGEdC2IWmtKMyS1OWIviwpnK3aL5JedwzbeBF7POyg6IG
# G/XhhJ3UqWeWTO+Czb1c2NP5zyEh89F72u9UIw+IfvM9lzDmc2O2END7MPnrcjWd
# QnrLn1Ntday7JSyrDvBdmgbNnCKNZPmhzoa8PccOiQljjTW6GePe5sGFuRHzdFt8
# y+bN2neF7Zu8hTO1I64XNGqst8S+w+RUdie8fXC1jKu3m9KGIqF4aldrYBamyh3g
# 4nJPj/LR2CBaLyD+2BuGZCVmoNR/dSpRCxlot0i79dKOChmoONqbMI8m04uLaEHA
# v4qwKHQ1vBzbV/nG89LDKbRSSvijmwJwxRxLLpMQ/u4xXxFfR4f/gksSkbJp7oqL
# wliDm/h+w0aJ/U5ccnYhYb7vPKNMN+SZDWycU5ODIRfyoGl59BsXR/HpRGtiJquO
# YGmvA/pk5vC1lcnbeMrcWD/26ozePQ/TWfNXKBOmkFpvPE8CH+EeGGWzqTCjdAsn
# o2jzTeNSxlx3glDGJgcdz5D/AAxw9Sdgq/+rY7jjgs7X6fqPTXPmaCAJKVHAP19o
# EjJIBwD1LyHbaEgBxFCogYSOiUIr0Xqcr1nJfiWG2GwYe6ZoAF1bMIIHlzCCBX+g
# AwIBAgITMwAAAFXZ3WkmKPn44gAAAAAAVTANBgkqhkiG9w0BAQwFADBhMQswCQYD
# VQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTIwMAYDVQQD
# EylNaWNyb3NvZnQgUHVibGljIFJTQSBUaW1lc3RhbXBpbmcgQ0EgMjAyMDAeFw0y
# NTEwMjMyMDQ2NDlaFw0yNjEwMjIyMDQ2NDlaMIHbMQswCQYDVQQGEwJVUzETMBEG
# A1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWlj
# cm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1lcmljYSBP
# cGVyYXRpb25zMScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046N0QwMC0wNUUwLUQ5
# NDcxNTAzBgNVBAMTLE1pY3Jvc29mdCBQdWJsaWMgUlNBIFRpbWUgU3RhbXBpbmcg
# QXV0aG9yaXR5MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvbkfkh5Z
# SLP0MCUWafaw/KZoVZu9iQx8r5JwhZvdrUi86UjCCFQONjQanrIxGF9hRGIZLQZ5
# 0gHrLC+4fpUEJff5t04VwByWC2/bWOuk6NmaTh9JpPZDcGzNR95QlryjfEjtl+gx
# j12zNPEdADPplVfzt8cYRWFBx/Fbfch08k6P9p7jX2q1jFPbUxWYJ+xOyGC1aKhD
# GY5b+8wL39v6qC0HFIx/v3y+bep+aEXooK8VoeWK+szfaFjXo8YTcvQ8UL4szu9H
# FTuZNv6vvoJ7Ju+o5aTj51sph+0+FXW38TlL/rDBd5ia79jskLtOeHbDjkbljilw
# zegcxv9i49F05ZrS/5ELZCCY1VaqO7EOLKVaxxdAO5oy1vb0Bx0ZRVX1mxFjYzay
# 2EC051k6yGJHm58y1oe2IKRa/SM1+BTGse6vHNi5Q2d5ZnoR9AOAUDDwJIIqRI4r
# Zz2MSinh11WrXTG9urF2uoyd5Ve+8hxes9ABeP2PYQKlXYTAxvdaeanDTQ/vwmnM
# +yTcWzrVm84Z38XVFw4G7p/ZNZ2nscvv6uru2AevXcyV1t8ha7iWmhhgTWBNBrVi
# uDlc3iPvOz2SVPbPeqhyY/NXwNZCAgc2H5pOztu6MwQxDIjte3XM/FkKBxHofS2a
# bNT/0HG+xZtFqUJDaxgbJa6lN1zh7spjuQ8CAwEAAaOCAcswggHHMB0GA1UdDgQW
# BBRWBF8QbdwIA/DIv6nJFsrB16xltjAfBgNVHSMEGDAWgBRraSg6NS9IY0DPe9iv
# Sek+2T3bITBsBgNVHR8EZTBjMGGgX6BdhltodHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20vcGtpb3BzL2NybC9NaWNyb3NvZnQlMjBQdWJsaWMlMjBSU0ElMjBUaW1lc3Rh
# bXBpbmclMjBDQSUyMDIwMjAuY3JsMHkGCCsGAQUFBwEBBG0wazBpBggrBgEFBQcw
# AoZdaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNyb3Nv
# ZnQlMjBQdWJsaWMlMjBSU0ElMjBUaW1lc3RhbXBpbmclMjBDQSUyMDIwMjAuY3J0
# MAwGA1UdEwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwDgYDVR0PAQH/
# BAQDAgeAMGYGA1UdIARfMF0wUQYMKwYBBAGCN0yDfQEBMEEwPwYIKwYBBQUHAgEW
# M2h0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvRG9jcy9SZXBvc2l0b3J5
# Lmh0bTAIBgZngQwBBAIwDQYJKoZIhvcNAQEMBQADggIBAFIe4ZJUe9qUKcWeWypc
# hB58fXE/ZIWv2D5XP5/k/tB7LCN9BvmNSVKZ3VeclQM978wfEvuvdMQSUv6Y20bo
# IM8DK1K1IU9cP21MG0ExiHxaqjrikf2qbfrXIip4Ef3v2bNYKQxCxN3Sczp1SX0H
# 7uqK2L5OhfDEiXf15iou5hh+EPaaqp49czNQpJDOR/vfJghUc/qcslDPhoCZpZx8
# b2ODvywGQNXwqlbsmCS24uGmEkQ3UH5JUeN6c91yasVchS78riMrm6R9ZpAiO5pf
# NKMGU2MLm1A3pp098DcbFTAc95Hh6Qvkh//28F/Xe2bMFb6DL7Sw0ZO95v0gv0ZT
# yJfxS/LCxfraeEII9FSFOKAMEp1zNFSs2ue0GGjBt9yEEMUwvxq9ExFz0aZzYm8i
# vJfffpIVDnX/+rVRTYcxIkQyFYslIhYlWF9SjCw5r49qakjMRNh8W9O7aaoolSVZ
# leQZjGt0K8JzMlyp6hp2lbW6XqRx2cOHbbxJDxmENzohGUziI13lI2g2Bf5qibfC
# 4bKNRpJo9lbE8HUbY0qJiE8u3SU8eDQaySPXOEhJjxRCQwwOvejYmBG5P7CckQNB
# Snnl12+FKRKgPoj0Mv+z5OMhj9z2MtpbnHLAkep0odQClEyyCG/uR5tK5rW6mZH5
# Oq56UWS0NI6NV1JGS7Jri6jFMYIHRjCCB0ICAQEweDBhMQswCQYDVQQGEwJVUzEe
# MBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3Nv
# ZnQgUHVibGljIFJTQSBUaW1lc3RhbXBpbmcgQ0EgMjAyMAITMwAAAFXZ3WkmKPn4
# 4gAAAAAAVTANBglghkgBZQMEAgEFAKCCBJ8wEQYLKoZIhvcNAQkQAg8xAgUAMBoG
# CSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjYxMDAz
# MTMwMTA0WjAvBgkqhkiG9w0BCQQxIgQgz4SeRdI7fRUuDLYTE8RRQEzTZOHniJIj
# 1w26VkgU5UcwgbkGCyqGSIb3DQEJEAIvMYGpMIGmMIGjMIGgBCDYuTyXZIZiu799
# /v4PaqsmeSzBxh0rqkYq7sYYavj+zTB8MGWkYzBhMQswCQYDVQQGEwJVUzEeMBwG
# A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQg
# UHVibGljIFJTQSBUaW1lc3RhbXBpbmcgQ0EgMjAyMAITMwAAAFXZ3WkmKPn44gAA
# AAAAVTCCA2EGCyqGSIb3DQEJEAISMYIDUDCCA0yhggNIMIIDRDCCAiwCAQEwggEJ
# oYHhpIHeMIHbMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4G
# A1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUw
# IwYDVQQLExxNaWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25zMScwJQYDVQQLEx5u
# U2hpZWxkIFRTUyBFU046N0QwMC0wNUUwLUQ5NDcxNTAzBgNVBAMTLE1pY3Jvc29m
# dCBQdWJsaWMgUlNBIFRpbWUgU3RhbXBpbmcgQXV0aG9yaXR5oiMKAQEwBwYFKw4D
# AhoDFQAdO1QBgmW/tuBZV5EGjhfsV4cN6qBnMGWkYzBhMQswCQYDVQQGEwJVUzEe
# MBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3Nv
# ZnQgUHVibGljIFJTQSBUaW1lc3RhbXBpbmcgQ0EgMjAyMDANBgkqhkiG9w0BAQsF
# AAIFAO5rCMIwIhgPMjAyNjEwMDMwNDUzMjJaGA8yMDI2MTAwNDA0NTMyMlowdzA9
# BgorBgEEAYRZCgQBMS8wLTAKAgUA7msIwgIBADAKAgEAAgIBCAIB/zAHAgEAAgIS
# YjAKAgUA7mxaQgIBADA2BgorBgEEAYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAow
# CAIBAAIDB6EgoQowCAIBAAIDAYagMA0GCSqGSIb3DQEBCwUAA4IBAQCA1iEGz0zM
# Y1g0svw944ktKzBzkKB5Rvp4mHV5bCn6z/XHQNoY4d6pbjJdCA6VXm703oxXid2L
# GzwHZbTswWRPeSNCAmNEki9wS/vz4/qyaoq8/xwCmPU0iNvu456bvRocEK72MUkS
# /5zqqe38PQFGx+mkEbPLkS/BPxAQWoPN0ye35R4bytADrb0u+299J5DMZVwrPGhL
# moyElI6hBc5HZrmuYm3MPW9zzTZevwRA3atxHELCHwYabMXCCgzqKNZuOuYt+S92
# 5ps8nB1+VSOSw24GO7hF6mT8CZgf3f892sa42P8gTcSsoJCiAm/4LD5NK69nlejM
# VtcOfNOMhVFGMA0GCSqGSIb3DQEBAQUABIICAKwxJXAUCnZdCmzQEyy9PpO+DaLR
# E+vC9VfbgN3ObQeWjauh4vpsEZ+LcJUkIFpTBVEOWYhOhFX+s5gAoqGJbtyai6uV
# Lnk4VHc5gTylJmvvY9KpfCyvCbyJey3zRzYV6Rv47rK7NLWWamBFRarKj1EFw3AS
# ok6Ox4yPWmaRC+/D+YQDS0rcq5Fh5qLRPxe9LafpvOlXRX93aPAX8X/k48kmt/tJ
# zPZlLyp+FfBj1X3bdzrMbTnw3VN7bwRU/PF21fEJhpFt+cvO6tFkVTRsv1CA27jT
# WqV10numGwXChLlG6A7Lx9VWl/lY9AU7aQBr8sWCx0nHLaCyLo9hs9bxnHxHAHRo
# wehJMmVMRmrJQKREqNPlPV/VuHij2YrI0QIA7oG/trdbm+udJwrRss0afxmWaO4s
# NVv4lYEtC5J14KasiE7Mkcb28psZ3poSoT3iYGqLDunCBqkCJlwV7Cq03C0g6nf1
# ykpkBg9zQ6U7QIV61QFyEC2j+mn4Kx+gmaxxEnoHcTlucv3QKW/GlVPZZEI3CMMa
# jiDeUUY52aLq9yWz7az7QDs2/neM7xczkNi6VgyLAULXYHrlhCktamgAvNRMyh41
# /++GlCT7dgJRQEDnLWv/R/plKjjF9QydbVdPaRRiXvbvGs6vLaPkzbcUg2oShnjq
# ZnkENEohSjKDC7/k
# SIG # End signature block