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. # --- List prices as of 2026-07-01, with Teams, annual commitment: # --- https://www.microsoft.com/en-us/licensing/news/2026-m365-packaging-pricing-updates # --- Existing customers keep their old price until renewal. Keep this table identical to the paid # --- Auditor's Pricing.cs. Rows marked "not re-verified" were last checked before 2026-10. $script:SkuPrice = @{ 'O365_BUSINESS_ESSENTIALS' = 7.00 # Microsoft 365 Business Basic 'O365_BUSINESS_PREMIUM' = 14.00 # Microsoft 365 Business Standard 'SPB' = 22.00 # Microsoft 365 Business Premium 'ENTERPRISEPACK' = 26.00 # Office 365 E3 (was wrongly 36.00, the Microsoft 365 E3 price) 'ENTERPRISEPREMIUM' = 41.00 # Office 365 E5 (was wrongly 57.00, the Microsoft 365 E5 price) 'SPE_E3' = 39.00 # Microsoft 365 E3 'SPE_E5' = 60.00 # Microsoft 365 E5 'SPE_F1' = 10.00 # Microsoft 365 F3 'DESKLESSPACK' = 4.00 # Office 365 F3 (not re-verified) 'EXCHANGESTANDARD' = 4.00 # Exchange Online Plan 1 (not re-verified) 'EXCHANGEENTERPRISE' = 8.00 # Exchange Online Plan 2 (not re-verified) 'MCOEV' = 10.00 # Teams Phone Standard (raised 2025-04-01) 'POWER_BI_PRO' = 14.00 # Power BI Pro (raised 2025-04-01) 'POWER_BI_PREMIUM_PER_USER' = 24.00 # Power BI Premium Per User (raised 2025-04-01) 'PROJECTPROFESSIONAL' = 30.00 # Project Plan 3 (not re-verified) 'PROJECTPREMIUM' = 55.00 # Project Plan 5 (not re-verified) 'VISIOCLIENT' = 15.00 # Visio Plan 2 (not re-verified) '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 # SKUs this tenant isn't paying for: all-trial subscriptions and ones that are disabled, suspended or # expired. Filled in by Resolve-UnbilledSku after sign-in; priced at $0 so they are never counted as waste. $script:UnbilledParts = @{} # The one link to the paid tool, printed only when the scan finds more than the Starter price. # ct= is the campaign code PostHog records on server-bridge.com (issue #23). Change it here only. $script:PaidToolUrl = 'https://server-bridge.com/license-auditor.html?ct=la160_freescan' # --------------------------------------------------------------------------- # Pure, testable helpers (no Graph calls - safe to dot-source) # --------------------------------------------------------------------------- function Get-SkuMonthlyPrice { [CmdletBinding()] param([Parameter(Mandatory)][string]$PartNumber) # A licence on a free trial or an inactive subscription costs nothing, so removing it saves nothing. if ($script:UnbilledParts.ContainsKey($PartNumber)) { return [double]0 } if ($script:SkuPrice.ContainsKey($PartNumber)) { return [double]$script:SkuPrice[$PartNumber] } return [double]$script:DefaultPrice } function Get-UnbilledSkuPart { <# Which SKUs nobody is paying for. Pure - takes the subscribed SKUs and directory subscriptions as plain objects, so it can be tested without Graph. A SKU is billed only when its capabilityStatus is Enabled AND at least one of its enabled seats is not a free trial. Found on a real tenant, 2026-10-23: a dormant shared mailbox and a guest on a Business Standard trial that had been disabled ten days earlier were each priced at $168/year of waste. Same rule as the paid Auditor's SkuInventory.Billable. #> [CmdletBinding()] param( [object[]]$SubscribedSkus = @(), [object[]]$DirectorySubscriptions = @() ) $trialSeats = @{} foreach ($d in $DirectorySubscriptions) { if ($d.isTrial -eq $true -and "$($d.status)" -eq 'Enabled' -and $d.skuId) { $key = "$($d.skuId)" $trialSeats[$key] = [int]$trialSeats[$key] + [int]$d.totalLicenses } } $unbilled = @() foreach ($s in $SubscribedSkus) { $enabled = [int]$s.PrepaidUnits.Enabled $paid = [math]::Max(0, $enabled - [int]$trialSeats["$($s.SkuId)"]) if ("$($s.CapabilityStatus)" -ne 'Enabled' -or $paid -le 0) { $unbilled += $s.SkuPartNumber } } return $unbilled } function Resolve-UnbilledSku { <# Reads directory subscriptions (Organization.Read.All, already granted) and records which SKUs cost nothing. If that read fails, every SKU is treated as billed and the scan says so. #> [CmdletBinding()] param([object[]]$SubscribedSkus = @()) $script:UnbilledParts = @{} try { $subs = (Invoke-MgGraphRequest -Method GET -Uri 'https://graph.microsoft.com/v1.0/directory/subscriptions' -ErrorAction Stop).value } catch { Write-Host " (couldn't check for free trials - trial licences are priced as if paid)" -ForegroundColor DarkGray $subs = @() } foreach ($p in (Get-UnbilledSkuPart -SubscribedSkus $SubscribedSkus -DirectorySubscriptions $subs)) { if ($p) { $script:UnbilledParts[$p] = $true } } if ($script:UnbilledParts.Count -gt 0) { Write-Host ("Not counted (free trial or inactive subscription, so it costs nothing today): {0}" -f (($script:UnbilledParts.Keys | Sort-Object) -join ', ')) -ForegroundColor DarkGray } } 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' } ) } # One line, and no figure: the estimate line above already said what they are paying. Never # "save", never a promise - the number is a list-price estimate, not their invoice. return @( @{ Text = ("The paid License Auditor turns this into a costed PDF you can hand to whoever signs off the spend: {0}" -f $script:PaidToolUrl); 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 = @{} $subscribed = @(Get-MgSubscribedSku -All -ErrorAction Stop) foreach ($s in $subscribed) { $skuMap[$s.SkuId] = $s.SkuPartNumber } Resolve-UnbilledSku -SubscribedSkus $subscribed $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) # One line, same two numbers as before (reclaimable seats and their yearly cost), labelled for # what they are: an estimate at Microsoft's list price, not anyone's invoice. Write-Host (" Estimated unused licenses : {0:N0} (about `${1:N0}/year at Microsoft list price)" -f $dormant.Count, $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 Microsoft list-price estimates (July 2026). Existing customers keep their old' -ForegroundColor DarkGray Write-Host 'price until renewal, so this is not your invoice - 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 # MII2xQYJKoZIhvcNAQcCoII2tjCCNrICAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCASlvVZYDQJ8YTL # VCkFa/Z9odBcwY4fGiDfwgxONTXuNqCCG2MwggXMMIIDtKADAgECAhBUmNLR1FsZ # 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 # 03u4aUoqlmZpxJTG9F9urJh4iIAGXKKy7aIwggbBMIIEqaADAgECAhMzAAjfW/g9 # y8iOPR9FAAAACN9bMA0GCSqGSIb3DQEBDAUAMFoxCzAJBgNVBAYTAlVTMR4wHAYD # VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKzApBgNVBAMTIk1pY3Jvc29mdCBJ # RCBWZXJpZmllZCBDUyBFT0MgQ0EgMDMwHhcNMjYxMDIxMTI1MjI4WhcNMjYxMDI0 # MTI1MjI4WjCBgjELMAkGA1UEBhMCVVMxETAPBgNVBAgTCEtlbnR1Y2t5MRIwEAYD # VQQHEwlMZXhpbmd0b24xJTAjBgNVBAoTHExlZSBDcm93ZSBTb2Z0d2FyZSBTb2x1 # dGlvbnMxJTAjBgNVBAMTHExlZSBDcm93ZSBTb2Z0d2FyZSBTb2x1dGlvbnMwggGi # MA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCnk2tEonuCeIzD0iecDljdNnLr # cM3p/+3/aqJqAqbejrbbzLSG6mMtTR9o0uvD42UjEZGUz1sNzNoqqzGEnisUSXnd # opQUJU9IPNyPVdn262n5Sx2FALU1KfVpE8p+BjkSoiV1wzyBk/F2kUlTtEYVdicK # mmr6Ec6oXZzw86PuYJ5iN9AkmgbRbOHrSVRVVLcREwUMGZkQgnltstx72yK0JEuW # 2WPkV2i+yDFAzf1UlRXzVW7zVKersKWPAWRQixd25SgMWQX5i7vewpGJX5wcconY # +13s9Ufmn5DQ2Fi2GmydUiZgwICtGp6tLxpkZXLD2cQBAZK5n8ZiPHrJY3KhTz8x # zazam4Ov68dyZ6d/t4hHjD+Q9EBFF/+6eTUdtx74+A25AyZ8RuLRic43+yczLLGY # tG3WAlNzWjcj3PKXr7YCSFjSjVjvsS4sPy9oznN/IAPFCjNvNzjOED8ptYxb7grP # kWJn4INIYOxBzzWzGx0PKwtYOzne2xTIxsTl/RsCAwEAAaOCAdUwggHRMAwGA1Ud # EwEB/wQCMAAwDgYDVR0PAQH/BAQDAgeAMDwGA1UdJQQ1MDMGCisGAQQBgjdhAQAG # CCsGAQUFBwMDBhsrBgEEAYI3YfX3xXqCm/n9aIL52tE6goGumFEwHQYDVR0OBBYE # FCI6EUYEPAxc3alu/eYXpaGlrhVVMB8GA1UdIwQYMBaAFGtepTTDBcViSiFajqtz # YGLUrGOFMGcGA1UdHwRgMF4wXKBaoFiGVmh0dHA6Ly93d3cubWljcm9zb2Z0LmNv # bS9wa2lvcHMvY3JsL01pY3Jvc29mdCUyMElEJTIwVmVyaWZpZWQlMjBDUyUyMEVP # QyUyMENBJTIwMDMuY3JsMHQGCCsGAQUFBwEBBGgwZjBkBggrBgEFBQcwAoZYaHR0 # cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNyb3NvZnQlMjBJ # RCUyMFZlcmlmaWVkJTIwQ1MlMjBFT0MlMjBDQSUyMDAzLmNydDBUBgNVHSAETTBL # MEkGBFUdIAAwQTA/BggrBgEFBQcCARYzaHR0cDovL3d3dy5taWNyb3NvZnQuY29t # L3BraW9wcy9Eb2NzL1JlcG9zaXRvcnkuaHRtMA0GCSqGSIb3DQEBDAUAA4ICAQCy # aKvEjF8X1BVqJVirFS0IvUAbRNb7AwlEhULkr6sbwHrtZVmCdjrKR7jbL3OwliF0 # QU1P2MSH0fLJJSAlO/AVDjWJrZfOTeUc3qEnO78ehJXoVaPvS8yEMkrTwINa/VAH # TTyuTIokMoYDkZ2Tb/9j1j/gMO/j8M7mDxJbEl/7hzcwGMHIIx22m5LmMZ8BPcrt # N8FF60rC5DcE5meYMG1rqZE/tnOJAjGcpHS5I7m1vWYOOygMngPs0I9Tgy2ewzde # GBFpJsQTPaVrzfVRDlcQJk00IIddyB7ocFiq8A1oZgwdeZaG5mbxm9K1uYNuPB5F # Ra/IQ8KbMdQiwJUGuKrpx+S+nlar/A6OK1IvvGQj1j6uy4pcYJnH8510CukkjRiJ # XbsA/L2dzXI8XVV0cFBM71ftv7AfwGBcXXoX1uSO2h8RgxfbONZx3N4MLIxlcDQZ # fOzri/FGTr7q8L9umb/Vo+8T/Shzl7eYvu6A5x8hGiBMFmek5aavfHAZ6jxHyHgc # 2tp/bYAinF1fmOcQr1FewAl8XOY6UW5ypIHcNYLJ2xCPSTLKQJFbZ/40M1EFB6Bo # C26zoG4hWW4czBlWhjBO0UIR/PScCa4+6v4NzZt3i9KXTZX4eKfeoFPBTtJp93QJ # 5Q+5ylXb9m7UxTTipIIZOso9YEYmumEZwm22dUjxCjCCBygwggUQoAMCAQICEzMA # AAAVBT5uGY6TKdkAAAAAABUwDQYJKoZIhvcNAQEMBQAwYzELMAkGA1UEBhMCVVMx # HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjE0MDIGA1UEAxMrTWljcm9z # b2Z0IElEIFZlcmlmaWVkIENvZGUgU2lnbmluZyBQQ0EgMjAyMTAeFw0yNjAzMjYx # ODExMjhaFw0zMTAzMjYxODExMjhaMFoxCzAJBgNVBAYTAlVTMR4wHAYDVQQKExVN # aWNyb3NvZnQgQ29ycG9yYXRpb24xKzApBgNVBAMTIk1pY3Jvc29mdCBJRCBWZXJp # ZmllZCBDUyBFT0MgQ0EgMDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC # AQDg9Ms9AqovDnMePvMOe+KybhCd8+lokzYORlS3kBVXseecbyGwBcsenlm5bLtM # GPjiIFLzBQF+ghlVV/U29q5GcdeEEBCHTTGhL2koIrLc4UrliMRcbv9mOMtR/l7/ # xAmv0Fx4BJHn1dHt37fvrBqXmKjKfGf5DpyO/+hnV7TEreMtS19iO+bjZ/9Hnpg3 # PCk0e7YSbRTFkx97FZwRWpC4s3NepRfRXQh/WMAj7JmsYeVZohi4TF5yW2JMrJZq # wHcyzJZYtD2Hlno5ZEJkdiZcEaxHOobmwO06Z1J9c23ps9PGIhGaq1sKLEAz9Doc # 5rLkYWGteDrscKhAp2kIc/oYlH9Ij6BkOqqgWINEkEtC8ZNG1Mak+h3o65aj0iQK # mdxW7IZaHO5cuyoMi+KtYfXeIIg3sVIbS2EL8kUtsDGdEqNqAq/isqTi1jXqLe6i # Kp1ni1SPdvPW9G03CTsYF68b/yuIQRwbdoBCXemMNJCS0dorCRY4b2WAAy4ng7SA # NcEgrBgZf535+QfLU5hGzrKjIpbMabauWb5FKWUKkMsPcXFkXRWO4noKPm4KWlFy # pqOpbJ/KONVReIlxHQRegAOBzIhRB7gr9IDQ1sc2MgOgQ+xVGW4oq4HD0mfAiwiy # LskZrkaQ7JoanYjBNcR9RS26YxAVbcBtLitFTzCIEg5ZdQIDAQABo4IB3DCCAdgw # DgYDVR0PAQH/BAQDAgGGMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRrXqU0 # wwXFYkohWo6rc2Bi1KxjhTBUBgNVHSAETTBLMEkGBFUdIAAwQTA/BggrBgEFBQcC # ARYzaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9Eb2NzL1JlcG9zaXRv # cnkuaHRtMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMBIGA1UdEwEB/wQIMAYB # Af8CAQAwHwYDVR0jBBgwFoAU2UEpsA8PY2zvadf1zSmepEhqMOYwcAYDVR0fBGkw # ZzBloGOgYYZfaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jcmwvTWlj # cm9zb2Z0JTIwSUQlMjBWZXJpZmllZCUyMENvZGUlMjBTaWduaW5nJTIwUENBJTIw # MjAyMS5jcmwwfQYIKwYBBQUHAQEEcTBvMG0GCCsGAQUFBzAChmFodHRwOi8vd3d3 # Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY3Jvc29mdCUyMElEJTIwVmVy # aWZpZWQlMjBDb2RlJTIwU2lnbmluZyUyMFBDQSUyMDIwMjEuY3J0MA0GCSqGSIb3 # DQEBDAUAA4ICAQBdbiI8zwXLX8glJEh/8Q22UMCUhWBO46Z9FPhwOR3mdlqRVLkY # Oon/MczUwrjDhx3X99SPH5PSflkGoTvnO9ZWHM5YFVYpO7NYuB+mfVSGAGZwiGOA # SWk0i2B7vn9nElJJmoiXxugfH5YdBsrUgTt0AFNXkzmqTgk+S1Hxb1u/0HCqEHVZ # Pk2A/6eJXYbtpRM5Fcz00jisUl9BRZgSebODV85bBzOveqyC3f0PnHCxRJNhMb8x # P/sB/VI7pf2rheSV7zqUSv8vn/fIMblXeaVIlpqoq8SP9BJMjE/CoVXJxnkZQRM1 # Fa7kN9yztvReOhxSgPgpZx/Xl/jkwyEFVJTBfBp3sTgfIc/pmqv2ehtakL2AEj78 # EmOPQohxJT3wyX+P78GA25tLpAvzj3RMMHd8z18ZuuVi+60MAzGpOASH1L8Nlr3f # ZRZnQO+pyye2DCvYmHaIfdUgYJqn7noxxGVv89+RaETh1tgCDvwNpFCSG7vl5A4a # ko+2fx409r9TWjXC7Oif1IQ5ZJzB4Rf8GvBiHYjvMmHpledp1FGRLdSRFVpC3/OK # pZY6avIqZp7+8pP/WQP903DdgrvAT6W4xPOBxXPa4tGksN3SuqJaiFYHSNyeBufn # 8iseujW4IbBSbHD4BPqbF3qZ+7nG9d/d/G2/Lx4kH9cCmBfmsZdSkHmukDCCB54w # 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+tHD6n58vhavFIrmcxghq4MIIatAIBATBxMFoxCzAJ # BgNVBAYTAlVTMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKzApBgNV # BAMTIk1pY3Jvc29mdCBJRCBWZXJpZmllZCBDUyBFT0MgQ0EgMDMCEzMACN9b+D3L # yI49H0UAAAAI31swDQYJYIZIAWUDBAIBBQCggYQwGAYKKwYBBAGCNwIBDDEKMAig # AoAAoQKAADAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgorBgEEAYI3AgEL # MQ4wDAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQgY9RKZ2r/Z8mSgDtP5dih # 4P0YpRFPYRy9VBSJ493PbSEwDQYJKoZIhvcNAQEBBQAEggGAVp9P8Z0s1M5KYO0+ # tLCmmstVZRvJfq2eb6BSWxChx7jwNEJT7BvnTjbYPnT3834y2miNnpnAA5hNMN7j # 7Q3s+Upy1IAFzBeNX6q3EkKW0kOD8J0TJ4gs1ZG2cu+aci6NXiqjg1+K1IwvVGBG # XFAF6I3sb+CcFS2by2kxvk/i0IXuxMxPKMIyJQqMISaPsd2OP6JYocm+C8hiU+EA # 0+LmKIkKaET4QJBiXxuDYsrtdNff1DkO1ZkQoKKqTbv2udRKDO7uJfuV/Lk+M92w # Eq4VC1/21e5ObEhEFGvELA6360vqNl4KbzE1qXEbeIzZFgpU6dKbaq8vJt8abzmr # xf+sIHltDUjpVgT/n/jES3sIwnGFc8bIBV/bSzwXR2mgy15Vs4UlmBPompgbkH0X # ielY7vhRGtAMr6JVnxnJgZqBjW3pRJ3PfRiXllM9Cx1OC6nsIz9Hp/pG81I3rOMM # XT/t/1e6p/oDq32hBDwZ26vNxQtMy0AlTzUAeHq6W2Oio0vBoYIYETCCGA0GCisG # AQQBgjcDAwExghf9MIIX+QYJKoZIhvcNAQcCoIIX6jCCF+YCAQMxDzANBglghkgB # ZQMEAgEFADCCAWIGCyqGSIb3DQEJEAEEoIIBUQSCAU0wggFJAgEBBgorBgEEAYRZ # CgMBMDEwDQYJYIZIAWUDBAIBBQAEIEkvI3bVjoIEqU+1Lay8W4NLUj5kDgU/DevX # T9Pg/POyAgZq16hhe30YEzIwMjYxMDIzMTE1MTI4LjAxMlowBIACAfSggeGkgd4w # gdsxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdS # ZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJTAjBgNVBAsT # HE1pY3Jvc29mdCBBbWVyaWNhIE9wZXJhdGlvbnMxJzAlBgNVBAsTHm5TaGllbGQg # VFNTIEVTTjo3QTAwLTA1RTAtRDk0NzE1MDMGA1UEAxMsTWljcm9zb2Z0IFB1Ymxp # 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 # AwIBAgITMwAAAF3GOXhPQH0z/QAAAAAAXTANBgkqhkiG9w0BAQwFADBhMQswCQYD # VQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTIwMAYDVQQD # EylNaWNyb3NvZnQgUHVibGljIFJTQSBUaW1lc3RhbXBpbmcgQ0EgMjAyMDAeFw0y # NjA5MTcxODU3MjFaFw0yNzA5MTQxODU3MjFaMIHbMQswCQYDVQQGEwJVUzETMBEG # A1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWlj # cm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1lcmljYSBP # cGVyYXRpb25zMScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046N0EwMC0wNUUwLUQ5 # NDcxNTAzBgNVBAMTLE1pY3Jvc29mdCBQdWJsaWMgUlNBIFRpbWUgU3RhbXBpbmcg # QXV0aG9yaXR5MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAnfeo3PEU # D4ms6oOxxnr50+xXsYIC89uEGj2MMjXhwsmk31IRN7u7diRWxUr1PfVqZUQSwpJ0 # NlB4Np5ImpQ2wio7MbAEu7GT9fw2VlGJBdanG87gNKKRr91ipU6R+HS2vsmGSAkX # 1uKg/LAuUttZ9HMk4IZPt2jyP/UsUxNB8MEsDb+1jVODSksDGZytN2ImF85UbZ84 # sAGzkIwapHjVjLIOcI07nfunp/8psYD+l4G6TTV+QI9KyBKpHGg945xF5Bju8jo7 # auPPnHvMSj2hzGfnZKw8FlPqmAepuXClZRvpvtqHPLze6bgMLRIy9R/SGyb/T772 # 3i9CavF5VxM11REBvBcNQtX1QvZ3zWqMPpk9u4sKgnacVVrB6V2+z1gl5PiaEbQ6 # k/7sii90m7buyp1nhPxtVaup3up25vs0kAOaOJPOnzG4uZCsB0NH8+EENAWCDP6P # zxLnVmbKJwdydCRW0Q8qL+SyIdJXMrKx85c4MTNr0hmnjmatRWP8lMij6edIYWPL # xIWx5dJOYn4aVEmF5GeFAoZRKxzPQw1jfUQnEA7xJ0+olpHbPfQbxi8k2BOLSlQz # HR5vAa2q4X+X7KPgXV/PRIOYJnUuKSqmk0Sy2LEkfBrfZ9v35qpv5QYFH8DWGvSh # GglZsBuJMIXVLIyT1siEJ89fYmAsMM39jM0CAwEAAaOCAcswggHHMB0GA1UdDgQW # BBRD2wpdmU7wfh5W7f3lqGjIf1rnuTAfBgNVHSMEGDAWgBRraSg6NS9IY0DPe9iv # Sek+2T3bITBsBgNVHR8EZTBjMGGgX6BdhltodHRwOi8vd3d3Lm1pY3Jvc29mdC5j # b20vcGtpb3BzL2NybC9NaWNyb3NvZnQlMjBQdWJsaWMlMjBSU0ElMjBUaW1lc3Rh # bXBpbmclMjBDQSUyMDIwMjAuY3JsMHkGCCsGAQUFBwEBBG0wazBpBggrBgEFBQcw # AoZdaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNyb3Nv # ZnQlMjBQdWJsaWMlMjBSU0ElMjBUaW1lc3RhbXBpbmclMjBDQSUyMDIwMjAuY3J0 # MAwGA1UdEwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwDgYDVR0PAQH/ # BAQDAgeAMGYGA1UdIARfMF0wUQYMKwYBBAGCN0yDfQEBMEEwPwYIKwYBBQUHAgEW # M2h0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvRG9jcy9SZXBvc2l0b3J5 # Lmh0bTAIBgZngQwBBAIwDQYJKoZIhvcNAQEMBQADggIBAELIRPApwy7XbiTh4Wht # Q6gKfJeKC1/pHJUNX+CMvvdgV8Feg7o4paA4m/pCs0+vNxB/LSQLrSzjZgJrhpRa # ATTi2ih2jaUcn12Kaxfa+BjVHMysfIYjGY9GhIlaZR1m+sPaktieppf8rIz/lX8+ # Wx7ozgaFEYnU076R6tCbF3VxO5/BybUrXHMIMocNtQZ7eQtHcwE2wgotoGzmz0uU # 2gJiL7nWvl4fatCUctOWBCYOH90rTvdUON0+VtABpas/mhXurQypBVWbITPirdcH # iEqAe2KcUHMCeazBsUvhHfIsQ0KxHeIPADpYJkVRNMxBCighDoitsHLQX74/4cIk # 8xKPo6CQ5eY1AUIuWY6DiXQ4FmjVOg+gIEMieDyOQiukmMTdMc6YK3kYH/XbyxkS # UKzs6Gr8HkkBZiU3syLke3CadevKdo/hFXSPZuLWWvV5hjo3ad8x4Pg3oWouH9vP # +wmrrDzVKa/pnHRYRkCic+lJKCMc+4IFc8w9g5SCkoU+n4nCxgwCO68qPyacs1OG # xlVq34wfwwdCILMR7ctM5voCfP6r7G67X4PUfXW5DJwo6ZHed1zroRj4jIaxZ5L9 # INnuS9cValdwr+NYIm5nk5sQAJzBp7u6UbvyGbtWXYPNtx2Jvv8RNYTTzPqaFaJ2 # YzmaYOts5AbDvyRH2vNhyZNKMYIHQzCCBz8CAQEweDBhMQswCQYDVQQGEwJVUzEe # MBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3Nv # ZnQgUHVibGljIFJTQSBUaW1lc3RhbXBpbmcgQ0EgMjAyMAITMwAAAF3GOXhPQH0z # /QAAAAAAXTANBglghkgBZQMEAgEFAKCCBJwwEQYLKoZIhvcNAQkQAg8xAgUAMBoG # CSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjYxMDIz # MTE1MTI4WjAvBgkqhkiG9w0BCQQxIgQgZ/kvcrmEd7hxw6ddH2VHIt06Fv5yQbT9 # JQLEmMVrZKYwgbkGCyqGSIb3DQEJEAIvMYGpMIGmMIGjMIGgBCAtNWRLb+nz7kJi # oWWnRqK5hoUAQ15v3MvrF73cxAJKXTB8MGWkYzBhMQswCQYDVQQGEwJVUzEeMBwG # A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQg # UHVibGljIFJTQSBUaW1lc3RhbXBpbmcgQ0EgMjAyMAITMwAAAF3GOXhPQH0z/QAA # AAAAXTCCA14GCyqGSIb3DQEJEAISMYIDTTCCA0mhggNFMIIDQTCCAikCAQEwggEJ # oYHhpIHeMIHbMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4G # A1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUw # IwYDVQQLExxNaWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25zMScwJQYDVQQLEx5u # U2hpZWxkIFRTUyBFU046N0EwMC0wNUUwLUQ5NDcxNTAzBgNVBAMTLE1pY3Jvc29m # dCBQdWJsaWMgUlNBIFRpbWUgU3RhbXBpbmcgQXV0aG9yaXR5oiMKAQEwBwYFKw4D # AhoDFQC9p7q1tbGAzwdowGwPEcSazJaQ9aBnMGWkYzBhMQswCQYDVQQGEwJVUzEe # MBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3Nv # ZnQgUHVibGljIFJTQSBUaW1lc3RhbXBpbmcgQ0EgMjAyMDANBgkqhkiG9w0BAQsF # AAIFAO6Fcp0wIhgPMjAyNjEwMjMwNTQzNTdaGA8yMDI2MTAyNDA1NDM1N1owdDA6 # BgorBgEEAYRZCgQBMSwwKjAKAgUA7oVynQIBADAHAgEAAgIghTAHAgEAAgISlDAK # AgUA7obEHQIBADA2BgorBgEEAYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAowCAIB # AAIDB6EgoQowCAIBAAIDAYagMA0GCSqGSIb3DQEBCwUAA4IBAQAiejpGKMVswo8e # 0JNwof82slEfZj+tLcWq10w0o/IGWDoBWPtSlzCKuiuzDSgHRcDRwiSMuJUG6/Jw # reBAsMpC8fBI87LoVynrTGDBrEbl14G/Im7RcMBebxG1B8X6pZ0hHkS8WrMQHcgI # LnpKZGh3lGaWsxUqWdXWfXgqG/b2GB3AadKtuuiuh2nCeZnVdvLxEFcYZFv/R2Gf # MX9JgVhTYUA/67R6M1RkXTO5wM9Xmmq/lx+1IdrmQVH0u+Nd+v4Pd5Vqvr4xhRA+ # IOJcZ80L9KH4ifU0LTG+RuIuIUTXzhwCI2GgR53tgSxwxiiEnCuC4rUPBVczTYDL # szRBwQJBMA0GCSqGSIb3DQEBAQUABIICAH155zfFsAzeH2pw0k3OK8p++d6zdQfz # ojLJjD7j7q4BiqQQu/vt4VtZQ+xZnBc/UesLPGxpxM9SXykIPgwtHET+yMwfoFAs # kiHk8pEqeCYa5YG2O15rZPBCw+PNt/t/UbFXmimRQAu3haPY671ShBt+B/MTowey # IJPB+epPlm+McGAK6fgXZ9BEK9Hs+c9ukb1dCDfuGYcvpF0MpG/Q6f0YLV1C6PM+ # IFmnVaPt+eCFsZ2ZGVGb0JvLPu3Va4P5yo0p/2JV7FtmYNztABxcdtKFjMrDJYyn # 5nLxKKfH3pteN3C347XEV8pUijuVVOjqO345BYl+E5uL6X5dFzCdzeUGoNFFbvtA # ueXKsN8Gu3L0oNiOz864j/Z6yPpoAd+6+YDM6bJGILNkqXQpzmxJs+VDh0874xw3 # lN2esmq2EFKTO7c+5M1n8xmBDi5anNZmwRZVa+InJDnNPUNhGxpzRFt7FhhTDz2/ # RUOJM7L5r7SmfGcqSPt4sZQ/sB6f+3bpMg0oWV1/WLv8fpzOIUDmGkEATMtmxm3m # 5SabcVjwil7kWTcY97be6OgoUCo/387JfB03jQDtRaeup/up6abc9tYeiPMnoEKi # HLjqEal5g6tWhU7N9bAtONnfpjNJF/AidUdRXElnmVdzNWoBYXAv3gEXmF1FQu9P # nt3RPkPUsSJj # SIG # End signature block |