GraphEssentials.psm1
function ConvertFrom-DistinguishedName { <# .SYNOPSIS Converts a Distinguished Name to CN, OU, Multiple OUs or DC .DESCRIPTION Converts a Distinguished Name to CN, OU, Multiple OUs or DC .PARAMETER DistinguishedName Distinguished Name to convert .PARAMETER ToOrganizationalUnit Converts DistinguishedName to Organizational Unit .PARAMETER ToDC Converts DistinguishedName to DC .PARAMETER ToDomainCN Converts DistinguishedName to Domain Canonical Name (CN) .PARAMETER ToCanonicalName Converts DistinguishedName to Canonical Name .EXAMPLE $DistinguishedName = 'CN=Przemyslaw Klys,OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz' ConvertFrom-DistinguishedName -DistinguishedName $DistinguishedName -ToOrganizationalUnit Output: OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz .EXAMPLE $DistinguishedName = 'CN=Przemyslaw Klys,OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz' ConvertFrom-DistinguishedName -DistinguishedName $DistinguishedName Output: Przemyslaw Klys .EXAMPLE ConvertFrom-DistinguishedName -DistinguishedName 'OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz' -ToMultipleOrganizationalUnit -IncludeParent Output: OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz OU=Production,DC=ad,DC=evotec,DC=xyz .EXAMPLE ConvertFrom-DistinguishedName -DistinguishedName 'OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz' -ToMultipleOrganizationalUnit Output: OU=Production,DC=ad,DC=evotec,DC=xyz .EXAMPLE $Con = @( 'CN=Windows Authorization Access Group,CN=Builtin,DC=ad,DC=evotec,DC=xyz' 'CN=Mmm,DC=elo,CN=nee,DC=RootDNSServers,CN=MicrosoftDNS,CN=System,DC=ad,DC=evotec,DC=xyz' 'CN=e6d5fd00-385d-4e65-b02d-9da3493ed850,CN=Operations,CN=DomainUpdates,CN=System,DC=ad,DC=evotec,DC=xyz' 'OU=Domain Controllers,DC=ad,DC=evotec,DC=pl' 'OU=Microsoft Exchange Security Groups,DC=ad,DC=evotec,DC=xyz' ) ConvertFrom-DistinguishedName -DistinguishedName $Con -ToLastName Output: Windows Authorization Access Group Mmm e6d5fd00-385d-4e65-b02d-9da3493ed850 Domain Controllers Microsoft Exchange Security Groups .EXAMPLEE ConvertFrom-DistinguishedName -DistinguishedName 'DC=ad,DC=evotec,DC=xyz' -ToCanonicalName ConvertFrom-DistinguishedName -DistinguishedName 'OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz' -ToCanonicalName ConvertFrom-DistinguishedName -DistinguishedName 'CN=test,OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz' -ToCanonicalName Output: ad.evotec.xyz ad.evotec.xyz\Production\Users ad.evotec.xyz\Production\Users\test .NOTES General notes #> [CmdletBinding(DefaultParameterSetName = 'Default')] param([Parameter(ParameterSetName = 'ToOrganizationalUnit')] [Parameter(ParameterSetName = 'ToMultipleOrganizationalUnit')] [Parameter(ParameterSetName = 'ToDC')] [Parameter(ParameterSetName = 'ToDomainCN')] [Parameter(ParameterSetName = 'Default')] [Parameter(ParameterSetName = 'ToLastName')] [Parameter(ParameterSetName = 'ToCanonicalName')] [alias('Identity', 'DN')][Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0)][string[]] $DistinguishedName, [Parameter(ParameterSetName = 'ToOrganizationalUnit')][switch] $ToOrganizationalUnit, [Parameter(ParameterSetName = 'ToMultipleOrganizationalUnit')][alias('ToMultipleOU')][switch] $ToMultipleOrganizationalUnit, [Parameter(ParameterSetName = 'ToMultipleOrganizationalUnit')][switch] $IncludeParent, [Parameter(ParameterSetName = 'ToDC')][switch] $ToDC, [Parameter(ParameterSetName = 'ToDomainCN')][switch] $ToDomainCN, [Parameter(ParameterSetName = 'ToLastName')][switch] $ToLastName, [Parameter(ParameterSetName = 'ToCanonicalName')][switch] $ToCanonicalName) Process { foreach ($Distinguished in $DistinguishedName) { if ($ToDomainCN) { $DN = $Distinguished -replace '.*?((DC=[^=]+,)+DC=[^=]+)$', '$1' $CN = $DN -replace ',DC=', '.' -replace "DC=" if ($CN) { $CN } } elseif ($ToOrganizationalUnit) { $Value = [Regex]::Match($Distinguished, '(?=OU=)(.*\n?)(?<=.)').Value if ($Value) { $Value } } elseif ($ToMultipleOrganizationalUnit) { if ($IncludeParent) { $Distinguished } while ($true) { $Distinguished = $Distinguished -replace '^.+?,(?=..=)' if ($Distinguished -match '^DC=') { break } $Distinguished } } elseif ($ToDC) { $Value = $Distinguished -replace '.*?((DC=[^=]+,)+DC=[^=]+)$', '$1' if ($Value) { $Value } } elseif ($ToLastName) { $NewDN = $Distinguished -split ",DC=" if ($NewDN[0].Contains(",OU=")) { [Array] $ChangedDN = $NewDN[0] -split ",OU=" } elseif ($NewDN[0].Contains(",CN=")) { [Array] $ChangedDN = $NewDN[0] -split ",CN=" } else { [Array] $ChangedDN = $NewDN[0] } if ($ChangedDN[0].StartsWith('CN=')) { $ChangedDN[0] -replace 'CN=', '' } else { $ChangedDN[0] -replace 'OU=', '' } } elseif ($ToCanonicalName) { $Domain = $null $Rest = $null foreach ($O in $Distinguished -split '(?<!\\),') { if ($O -match '^DC=') { $Domain += $O.Substring(3) + '.' } else { $Rest = $O.Substring(3) + '\' + $Rest } } if ($Domain -and $Rest) { $Domain.Trim('.') + '\' + ($Rest.TrimEnd('\') -replace '\\,', ',') } elseif ($Domain) { $Domain.Trim('.') } elseif ($Rest) { $Rest.TrimEnd('\') -replace '\\,', ',' } } else { $Regex = '^CN=(?<cn>.+?)(?<!\\),(?<ou>(?:(?:OU|CN).+?(?<!\\),)+(?<dc>DC.+?))$' $Found = $Distinguished -match $Regex if ($Found) { $Matches.cn } } } } } function Copy-Dictionary { [alias('Copy-Hashtable', 'Copy-OrderedHashtable')] [cmdletbinding()] param([System.Collections.IDictionary] $Dictionary) $ms = [System.IO.MemoryStream]::new() $bf = [System.Runtime.Serialization.Formatters.Binary.BinaryFormatter]::new() $bf.Serialize($ms, $Dictionary) $ms.Position = 0 $clone = $bf.Deserialize($ms) $ms.Close() $clone } function Get-FileName { <# .SYNOPSIS Short description .DESCRIPTION Long description .PARAMETER Extension Parameter description .PARAMETER Temporary Parameter description .PARAMETER TemporaryFileOnly Parameter description .EXAMPLE Get-FileName -Temporary Output: 3ymsxvav.tmp .EXAMPLE Get-FileName -Temporary Output: C:\Users\pklys\AppData\Local\Temp\tmpD74C.tmp .EXAMPLE Get-FileName -Temporary -Extension 'xlsx' Output: C:\Users\pklys\AppData\Local\Temp\tmp45B6.xlsx .NOTES General notes #> [CmdletBinding()] param([string] $Extension = 'tmp', [switch] $Temporary, [switch] $TemporaryFileOnly) if ($Temporary) { return [io.path]::Combine([System.IO.Path]::GetTempPath(), "$($([System.IO.Path]::GetRandomFileName()).Split('.')[0]).$Extension") } if ($TemporaryFileOnly) { return "$($([System.IO.Path]::GetRandomFileName()).Split('.')[0]).$Extension" } } function Get-GitHubLatestRelease { <# .SYNOPSIS Gets one or more releases from GitHub repository .DESCRIPTION Gets one or more releases from GitHub repository .PARAMETER Url Url to github repository .EXAMPLE Get-GitHubLatestRelease -Url "https://api.github.com1/repos/evotecit/Testimo/releases" | Format-Table .NOTES General notes #> [CmdLetBinding()] param([parameter(Mandatory)][alias('ReleasesUrl')][uri] $Url) $ProgressPreference = 'SilentlyContinue' $Responds = Test-Connection -ComputerName $URl.Host -Quiet -Count 1 if ($Responds) { Try { [Array] $JsonOutput = (Invoke-WebRequest -Uri $Url -ErrorAction Stop | ConvertFrom-Json) foreach ($JsonContent in $JsonOutput) { [PSCustomObject] @{PublishDate = [DateTime] $JsonContent.published_at CreatedDate = [DateTime] $JsonContent.created_at PreRelease = [bool] $JsonContent.prerelease Version = [version] ($JsonContent.name -replace 'v', '') Tag = $JsonContent.tag_name Branch = $JsonContent.target_commitish Errors = '' } } } catch { [PSCustomObject] @{PublishDate = $null CreatedDate = $null PreRelease = $null Version = $null Tag = $null Branch = $null Errors = $_.Exception.Message } } } else { [PSCustomObject] @{PublishDate = $null CreatedDate = $null PreRelease = $null Version = $null Tag = $null Branch = $null Errors = "No connection (ping) to $($Url.Host)" } } $ProgressPreference = 'Continue' } function Start-TimeLog { [CmdletBinding()] param() [System.Diagnostics.Stopwatch]::StartNew() } function Stop-TimeLog { [CmdletBinding()] param ([Parameter(ValueFromPipeline = $true)][System.Diagnostics.Stopwatch] $Time, [ValidateSet('OneLiner', 'Array')][string] $Option = 'OneLiner', [switch] $Continue) Begin {} Process { if ($Option -eq 'Array') { $TimeToExecute = "$($Time.Elapsed.Days) days", "$($Time.Elapsed.Hours) hours", "$($Time.Elapsed.Minutes) minutes", "$($Time.Elapsed.Seconds) seconds", "$($Time.Elapsed.Milliseconds) milliseconds" } else { $TimeToExecute = "$($Time.Elapsed.Days) days, $($Time.Elapsed.Hours) hours, $($Time.Elapsed.Minutes) minutes, $($Time.Elapsed.Seconds) seconds, $($Time.Elapsed.Milliseconds) milliseconds" } } End { if (-not $Continue) { $Time.Stop() } return $TimeToExecute } } function Write-Color { <# .SYNOPSIS Write-Color is a wrapper around Write-Host. It provides: - Easy manipulation of colors, - Logging output to file (log) - Nice formatting options out of the box. .DESCRIPTION Author: przemyslaw.klys at evotec.pl Project website: https://evotec.xyz/hub/scripts/write-color-ps1/ Project support: https://github.com/EvotecIT/PSWriteColor Original idea: Josh (https://stackoverflow.com/users/81769/josh) .EXAMPLE Write-Color -Text "Red ", "Green ", "Yellow " -Color Red,Green,Yellow .EXAMPLE Write-Color -Text "This is text in Green ", "followed by red ", "and then we have Magenta... ", "isn't it fun? ", "Here goes DarkCyan" -Color Green,Red,Magenta,White,DarkCyan .EXAMPLE Write-Color -Text "This is text in Green ", "followed by red ", "and then we have Magenta... ", "isn't it fun? ", "Here goes DarkCyan" -Color Green,Red,Magenta,White,DarkCyan -StartTab 3 -LinesBefore 1 -LinesAfter 1 .EXAMPLE Write-Color "1. ", "Option 1" -Color Yellow, Green Write-Color "2. ", "Option 2" -Color Yellow, Green Write-Color "3. ", "Option 3" -Color Yellow, Green Write-Color "4. ", "Option 4" -Color Yellow, Green Write-Color "9. ", "Press 9 to exit" -Color Yellow, Gray -LinesBefore 1 .EXAMPLE Write-Color -LinesBefore 2 -Text "This little ","message is ", "written to log ", "file as well." ` -Color Yellow, White, Green, Red, Red -LogFile "C:\testing.txt" -TimeFormat "yyyy-MM-dd HH:mm:ss" Write-Color -Text "This can get ","handy if ", "want to display things, and log actions to file ", "at the same time." ` -Color Yellow, White, Green, Red, Red -LogFile "C:\testing.txt" .EXAMPLE # Added in 0.5 Write-Color -T "My text", " is ", "all colorful" -C Yellow, Red, Green -B Green, Green, Yellow wc -t "my text" -c yellow -b green wc -text "my text" -c red .NOTES Additional Notes: - TimeFormat https://msdn.microsoft.com/en-us/library/8kb3ddd4.aspx #> [alias('Write-Colour')] [CmdletBinding()] param ([alias ('T')] [String[]]$Text, [alias ('C', 'ForegroundColor', 'FGC')] [ConsoleColor[]]$Color = [ConsoleColor]::White, [alias ('B', 'BGC')] [ConsoleColor[]]$BackGroundColor = $null, [alias ('Indent')][int] $StartTab = 0, [int] $LinesBefore = 0, [int] $LinesAfter = 0, [int] $StartSpaces = 0, [alias ('L')] [string] $LogFile = '', [Alias('DateFormat', 'TimeFormat')][string] $DateTimeFormat = 'yyyy-MM-dd HH:mm:ss', [alias ('LogTimeStamp')][bool] $LogTime = $true, [int] $LogRetry = 2, [ValidateSet('unknown', 'string', 'unicode', 'bigendianunicode', 'utf8', 'utf7', 'utf32', 'ascii', 'default', 'oem')][string]$Encoding = 'Unicode', [switch] $ShowTime, [switch] $NoNewLine) $DefaultColor = $Color[0] if ($null -ne $BackGroundColor -and $BackGroundColor.Count -ne $Color.Count) { Write-Error "Colors, BackGroundColors parameters count doesn't match. Terminated." return } if ($LinesBefore -ne 0) { for ($i = 0; $i -lt $LinesBefore; $i++) { Write-Host -Object "`n" -NoNewline } } if ($StartTab -ne 0) { for ($i = 0; $i -lt $StartTab; $i++) { Write-Host -Object "`t" -NoNewline } } if ($StartSpaces -ne 0) { for ($i = 0; $i -lt $StartSpaces; $i++) { Write-Host -Object ' ' -NoNewline } } if ($ShowTime) { Write-Host -Object "[$([datetime]::Now.ToString($DateTimeFormat))] " -NoNewline } if ($Text.Count -ne 0) { if ($Color.Count -ge $Text.Count) { if ($null -eq $BackGroundColor) { for ($i = 0; $i -lt $Text.Length; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -NoNewline } } else { for ($i = 0; $i -lt $Text.Length; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -BackgroundColor $BackGroundColor[$i] -NoNewline } } } else { if ($null -eq $BackGroundColor) { for ($i = 0; $i -lt $Color.Length; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -NoNewline } for ($i = $Color.Length; $i -lt $Text.Length; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $DefaultColor -NoNewline } } else { for ($i = 0; $i -lt $Color.Length; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -BackgroundColor $BackGroundColor[$i] -NoNewline } for ($i = $Color.Length; $i -lt $Text.Length; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $DefaultColor -BackgroundColor $BackGroundColor[0] -NoNewline } } } } if ($NoNewLine -eq $true) { Write-Host -NoNewline } else { Write-Host } if ($LinesAfter -ne 0) { for ($i = 0; $i -lt $LinesAfter; $i++) { Write-Host -Object "`n" -NoNewline } } if ($Text.Count -and $LogFile) { $TextToFile = "" for ($i = 0; $i -lt $Text.Length; $i++) { $TextToFile += $Text[$i] } $Saved = $false $Retry = 0 Do { $Retry++ try { if ($LogTime) { "[$([datetime]::Now.ToString($DateTimeFormat))] $TextToFile" | Out-File -FilePath $LogFile -Encoding $Encoding -Append -ErrorAction Stop -WhatIf:$false } else { "$TextToFile" | Out-File -FilePath $LogFile -Encoding $Encoding -Append -ErrorAction Stop -WhatIf:$false } $Saved = $true } catch { if ($Saved -eq $false -and $Retry -eq $LogRetry) { $PSCmdlet.WriteError($_) } else { Write-Warning "Write-Color - Couldn't write to log file $($_.Exception.Message). Retrying... ($Retry/$LogRetry)" } } } Until ($Saved -eq $true -or $Retry -ge $LogRetry) } } $Script:Apps = [ordered] @{ Name = 'Azure Active Directory Apps' Enabled = $true Execute = { Get-MyApp } Processing = { } Summary = { } Variables = @{ } Solution = { if ($Script:Reporting['Apps']['Data']) { New-HTMLTable -DataTable $Script:Reporting['Apps']['Data'] -Filtering { New-HTMLTableCondition -Name 'DescriptionWithEmail' -Operator eq -Value $true -ComparisonType string -BackgroundColor SpringGreen -FailBackgroundColor Salmon New-HTMLTableCondition -Name 'KeysCount' -Operator gt -Value 1 -ComparisonType number -BackgroundColor GoldenFizz New-HTMLTableCondition -Name 'KeysCount' -Operator eq -Value 0 -ComparisonType number -BackgroundColor Salmon -Row New-HTMLTableCondition -Name 'KeysCount' -Operator eq -Value 1 -ComparisonType number -BackgroundColor SpringGreen New-HTMLTableCondition -Name 'Expired' -Operator eq -Value "No" -ComparisonType string -BackgroundColor SpringGreen New-HTMLTableCondition -Name 'Expired' -Operator eq -Value "Yes" -ComparisonType string -BackgroundColor Salmon -Row New-HTMLTableCondition -Name 'Expired' -Operator eq -Value "Not available" -ComparisonType string -BackgroundColor Salmon -Row } -ScrollX } } } $Script:AppsCredentials = [ordered] @{ Name = 'Azure Active Directory Apps Credentials' Enabled = $true Execute = { Get-MyAppCredentials } Processing = { } Summary = { } Variables = @{ } Solution = { if ($Script:Reporting['AppsCredentials']['Data']) { New-HTMLTable -DataTable $Script:Reporting['AppsCredentials']['Data'] -Filtering { New-HTMLTableCondition -Name 'Expired' -Operator eq -Value $false -ComparisonType string -BackgroundColor SpringGreen -FailBackgroundColor Salmon New-HTMLTableCondition -Name 'DaysToExpire' -Value 30 -Operator 'ge' -BackgroundColor Conifer -ComparisonType number New-HTMLTableCondition -Name 'DaysToExpire' -Value 30 -Operator 'lt' -BackgroundColor Orange -ComparisonType number New-HTMLTableCondition -Name 'DaysToExpire' -Value 5 -Operator 'lt' -BackgroundColor Red -ComparisonType number } -ScrollX } } } $Script:Roles = [ordered] @{ Name = 'Azure Active Directory Roles' Enabled = $true Execute = { Get-MyRole -OnlyWithMembers } Processing = { } Summary = { } Variables = @{ } Solution = { if ($Script:Reporting['Roles']['Data']) { New-HTMLTable -DataTable $Script:Reporting['Roles']['Data'] -Filtering { New-HTMLTableCondition -Name 'IsEnabled' -Operator eq -Value $true -ComparisonType string -BackgroundColor SpringGreen -FailBackgroundColor Salmon } -ScrollX } } } $Script:RolesUsers = [ordered] @{ Name = 'Azure Active Directory Roles Users' Enabled = $true Execute = { Get-MyRoleUsers -OnlyWithRoles } Processing = { } Summary = { } Variables = @{ } Solution = { if ($Script:Reporting['RolesUsers']['Data']) { New-HTMLTable -DataTable $Script:Reporting['RolesUsers']['Data'] -Filtering { New-HTMLTableCondition -Name 'Enabled' -Operator eq -Value $true -ComparisonType string -BackgroundColor SpringGreen New-HTMLTableCondition -Name 'Enabled' -Operator eq -Value $false -ComparisonType string -BackgroundColor Salmon New-HTMLTableCondition -Name 'Status' -Operator eq -Value 'Synchronized' -ComparisonType string -BackgroundColor SpringGreen New-HTMLTableCondition -Name 'Status' -Operator eq -Value 'Online' -ComparisonType string -BackgroundColor GoldenFizz } -ScrollX } } } $Script:RolesUsersPerColumn = [ordered] @{ Name = 'Azure Active Directory Roles Users Per Column' Enabled = $true Execute = { Get-MyRoleUsers -OnlyWithRoles -RolePerColumn } Processing = { } Summary = { } Variables = @{ } Solution = { if ($Script:Reporting['RolesUsersPerColumn']['Data']) { New-HTMLTable -DataTable $Script:Reporting['RolesUsersPerColumn']['Data'] -Filtering { New-HTMLTableCondition -Name 'Enabled' -Operator eq -Value $true -ComparisonType string -BackgroundColor SpringGreen New-HTMLTableCondition -Name 'Enabled' -Operator eq -Value $false -ComparisonType string -BackgroundColor Salmon foreach ($Name in $Script:Reporting['RolesUsersPerColumn']['Data'][0].PSObject.Properties.Name) { if ($Name -notin 'Name', 'Enabled', 'UserPrincipalName', 'Mail' , 'Status', 'Type', 'Location', 'CreatedDateTime') { New-HTMLTableCondition -Name $Name -Operator eq -Value 'Direct' -ComparisonType string -BackgroundColor GoldenFizz New-HTMLTableCondition -Name $Name -Operator eq -Value 'Eligible' -ComparisonType string -BackgroundColor SpringGreen New-HTMLTableConditionGroup -Conditions { New-HTMLTableCondition -Name $Name -Operator ne -Value 'Eligible' -ComparisonType string New-HTMLTableCondition -Name $Name -Operator ne -Value 'Direct' -ComparisonType string New-HTMLTableCondition -Name $Name -Operator ne -Value '' -ComparisonType string } -Logic AND -BackgroundColor Orange -HighlightHeaders $Name } } New-HTMLTableCondition -Name 'Status' -Operator eq -Value 'Synchronized' -ComparisonType string -BackgroundColor SpringGreen New-HTMLTableCondition -Name 'Status' -Operator eq -Value 'Online' -ComparisonType string -BackgroundColor GoldenFizz } -ScrollX } } } function Get-GitHubVersion { [cmdletBinding()] param( [Parameter(Mandatory)][string] $Cmdlet, [Parameter(Mandatory)][string] $RepositoryOwner, [Parameter(Mandatory)][string] $RepositoryName ) $App = Get-Command -Name $Cmdlet -ErrorAction SilentlyContinue if ($App) { [Array] $GitHubReleases = (Get-GitHubLatestRelease -Url "https://api.github.com/repos/$RepositoryOwner/$RepositoryName/releases" -Verbose:$false) $LatestVersion = $GitHubReleases[0] if (-not $LatestVersion.Errors) { if ($App.Version -eq $LatestVersion.Version) { "Current/Latest: $($LatestVersion.Version) at $($LatestVersion.PublishDate)" } elseif ($App.Version -lt $LatestVersion.Version) { "Current: $($App.Version), Published: $($LatestVersion.Version) at $($LatestVersion.PublishDate). Update?" } elseif ($App.Version -gt $LatestVersion.Version) { "Current: $($App.Version), Published: $($LatestVersion.Version) at $($LatestVersion.PublishDate). Lucky you!" } } else { "Current: $($App.Version)" } } } function New-HTMLReportGraphEssentials { [cmdletBinding()] param( [Array] $Type, [switch] $Online, [switch] $HideHTML, [string] $FilePath ) New-HTML -Author 'Przemysław Kłys' -TitleText 'GraphEssentials Report' { New-HTMLTabStyle -BorderRadius 0px -TextTransform capitalize -BackgroundColorActive SlateGrey New-HTMLSectionStyle -BorderRadius 0px -HeaderBackGroundColor Grey -RemoveShadow New-HTMLPanelStyle -BorderRadius 0px New-HTMLTableOption -DataStore JavaScript -BoolAsString -ArrayJoinString ', ' -ArrayJoin New-HTMLHeader { New-HTMLSection -Invisible { New-HTMLSection { New-HTMLText -Text "Report generated on $(Get-Date)" -Color Blue } -JustifyContent flex-start -Invisible New-HTMLSection { New-HTMLText -Text "GraphEssentials - $($Script:Reporting['Version'])" -Color Blue } -JustifyContent flex-end -Invisible } } if ($Type.Count -eq 1) { foreach ($T in $Script:GraphEssentialsConfiguration.Keys) { if ($Script:GraphEssentialsConfiguration[$T].Enabled -eq $true) { if ($Script:GraphEssentialsConfiguration[$T]['Summary']) { $Script:Reporting[$T]['Summary'] = Invoke-Command -ScriptBlock $Script:GraphEssentialsConfiguration[$T]['Summary'] } & $Script:GraphEssentialsConfiguration[$T]['Solution'] } } } else { foreach ($T in $Script:GraphEssentialsConfiguration.Keys) { if ($Script:GraphEssentialsConfiguration[$T].Enabled -eq $true) { if ($Script:GraphEssentialsConfiguration[$T]['Summary']) { $Script:Reporting[$T]['Summary'] = Invoke-Command -ScriptBlock $Script:GraphEssentialsConfiguration[$T]['Summary'] } New-HTMLTab -Name $Script:GraphEssentialsConfiguration[$T]['Name'] { & $Script:GraphEssentialsConfiguration[$T]['Solution'] } } } } } -Online:$Online.IsPresent -ShowHTML:(-not $HideHTML) -FilePath $FilePath } function New-HTMLReportGraphEssentialsWithSplit { [cmdletBinding()] param( [Array] $Type, [switch] $Online, [switch] $HideHTML, [string] $FilePath, [string] $CurrentReport ) # Split reports into multiple files for easier viewing $DateName = $(Get-Date -f yyyy-MM-dd_HHmmss) $FileName = [io.path]::GetFileNameWithoutExtension($FilePath) $DirectoryName = [io.path]::GetDirectoryName($FilePath) foreach ($T in $Script:GraphEssentialsConfiguration.Keys) { if ($Script:GraphEssentialsConfiguration[$T].Enabled -eq $true -and ((-not $CurrentReport) -or ($CurrentReport -and $CurrentReport -eq $T))) { $NewFileName = $FileName + '_' + $T + "_" + $DateName + '.html' $FilePath = [io.path]::Combine($DirectoryName, $NewFileName) New-HTML -Author 'Przemysław Kłys' -TitleText "GraphEssentials $CurrentReport Report" { New-HTMLTabStyle -BorderRadius 0px -TextTransform capitalize -BackgroundColorActive SlateGrey New-HTMLSectionStyle -BorderRadius 0px -HeaderBackGroundColor Grey -RemoveShadow New-HTMLPanelStyle -BorderRadius 0px New-HTMLTableOption -DataStore JavaScript -BoolAsString -ArrayJoinString ', ' -ArrayJoin New-HTMLHeader { New-HTMLSection -Invisible { New-HTMLSection { New-HTMLText -Text "Report generated on $(Get-Date)" -Color Blue } -JustifyContent flex-start -Invisible New-HTMLSection { New-HTMLText -Text "GraphEssentials - $($Script:Reporting['Version'])" -Color Blue } -JustifyContent flex-end -Invisible } } if ($Script:GraphEssentialsConfiguration[$T]['Summary']) { $Script:Reporting[$T]['Summary'] = Invoke-Command -ScriptBlock $Script:GraphEssentialsConfiguration[$T]['Summary'] } & $Script:GraphEssentialsConfiguration[$T]['Solution'] } -Online:$Online.IsPresent -ShowHTML:(-not $HideHTML) -FilePath $FilePath } } } function Reset-GraphEssentials { [cmdletBinding()] param( ) if (-not $Script:DefaultTypes) { $Script:DefaultTypes = foreach ($T in $Script:GraphEssentialsConfiguration.Keys) { if ($Script:GraphEssentialsConfiguration[$T].Enabled) { $T } } } else { foreach ($T in $Script:GraphEssentialsConfiguration.Keys) { if ($Script:GraphEssentialsConfiguration[$T]) { $Script:GraphEssentialsConfiguration[$T]['Enabled'] = $false } } foreach ($T in $Script:DefaultTypes) { if ($Script:GraphEssentialsConfiguration[$T]) { $Script:GraphEssentialsConfiguration[$T]['Enabled'] = $true } } } } $Script:GraphEssentialsConfiguration = [ordered] @{ Apps = $Script:Apps AppsCredentials = $Script:AppsCredentials Roles = $Script:Roles RolesUsers = $Script:RolesUsers RolesUsersPerColumn = $Script:RolesUsersPerColumn } function Get-MyApp { [cmdletBinding()] param( ) $Application = Get-MgApplication -ConsistencyLevel eventual -All $Applications = foreach ($App in $Application) { [Array] $DatesSorted = $App.PasswordCredentials.StartDateTime | Sort-Object # Lets translate credentials to different format $AppCredentials = Get-MyAppCredentials -Application $App # Lets find if description has email $DescriptionWithEmail = $false foreach ($CredentialName in $AppCredentials.ClientSecretName) { if ($CredentialName -like '*@*') { $DescriptionWithEmail = $true break } } $DaysToExpireOldest = $AppCredentials.DaysToExpire | Sort-Object -Descending | Select-Object -Last 1 $DaysToExpireNewest = $AppCredentials.DaysToExpire | Sort-Object -Descending | Select-Object -First 1 if ($AppCredentials.Expired -contains $false) { $Expired = 'No' } elseif ($AppCredentials.Expired -contains $true) { $Expired = 'Yes' } else { $Expired = 'Not available' } [PSCustomObject] @{ ObjectId = $App.Id ClientID = $App.AppId ApplicationName = $App.DisplayName CreatedDate = $App.CreatedDateTime KeysCount = $App.PasswordCredentials.Count KeysExpired = $Expired DaysToExpireOldest = $DaysToExpireOldest DaysToExpireNewest = $DaysToExpireNewest KeysDateOldest = if ($DatesSorted.Count -gt 0) { $DatesSorted[0] } else { } KeysDateNewest = if ($DatesSorted.Count -gt 0) { $DatesSorted[-1] } else { } KeysDescription = $AppCredentials.ClientSecretName DescriptionWithEmail = $DescriptionWithEmail } } $Applications } function Get-MyAppCredentials { [cmdletBinding()] param( [int] $LessThanDaysToExpire, [switch] $Expired, [Parameter(DontShow)][Array] $Application ) if (-not $Application) { $Application = Get-MgApplication -All } $ApplicationsWithCredentials = foreach ($App in $Application) { if ($App.PasswordCredentials) { foreach ($Credentials in $App.PasswordCredentials) { if ($Credentials.EndDateTime -lt [DateTime]::Now) { $Expired = $true } else { $Expired = $false } if ($null -ne $Credentials.DisplayName) { $DisplayName = $Credentials.DisplayName } elseif ($null -ne $Credentials.CustomKeyIdentifier) { if ($Credentials.CustomKeyIdentifier[0] -eq 255 -and $Credentials.CustomKeyIdentifier[1] -eq 254 -and $Credentials.CustomKeyIdentifier[0] -ne 0 -and $Credentials.CustomKeyIdentifier[0] -ne 0) { $DisplayName = [System.Text.Encoding]::Unicode.GetString($Credentials.CustomKeyIdentifier) } elseif ($Credentials.CustomKeyIdentifier[0] -eq 255 -and $Credentials.CustomKeyIdentifier[1] -eq 254 -and $Credentials.CustomKeyIdentifier[0] -eq 0 -and $Credentials.CustomKeyIdentifier[0] -eq 0) { $DisplayName = [System.Text.Encoding]::UTF32.GetString($Credentials.CustomKeyIdentifier) } elseif ($Credentials.CustomKeyIdentifier[1] -eq 0 -and $Credentials.CustomKeyIdentifier[3] -eq 0) { $DisplayName = [System.Text.Encoding]::Unicode.GetString($Credentials.CustomKeyIdentifier) } else { $DisplayName = [System.Text.Encoding]::UTF8.GetString($Credentials.CustomKeyIdentifier) } } else { $DisplayName = $Null } $Creds = [PSCustomObject] @{ ObjectId = $App.Id ApplicationName = $App.DisplayName ClientID = $App.AppId CreatedDate = $App.CreatedDateTime ClientSecretName = $DisplayName ClientSecretId = $Credentials.KeyId #ClientSecret = $Credentials.SecretTex ClientSecretHint = $Credentials.Hint Expired = $Expired DaysToExpire = ($Credentials.EndDateTime - [DateTime]::Now).Days StartDateTime = $Credentials.StartDateTime EndDateTime = $Credentials.EndDateTime CustomKeyIdentifier = $Credentials.CustomKeyIdentifier } if ($PSBoundParameters.ContainsKey('LessThanDaysToExpire')) { if ($LessThanDaysToExpire -ge $Creds.DaysToExpire) { $Creds } } elseif ($PSBoundParameters.ContainsKey('Expired')) { $Creds } else { $Creds } } } } $ApplicationsWithCredentials } function Get-MyRole { [CmdletBinding()] param( [switch] $OnlyWithMembers ) # $Users = Get-MgUser -All # #$Apps = Get-MgApplication -All # $Groups = Get-MgGroup -All -Filter "IsAssignableToRole eq true" # $ServicePrincipals = Get-MgServicePrincipal -All # #$DirectoryRole = Get-MgDirectoryRole -All # $Roles = Get-MgRoleManagementDirectoryRoleDefinition -All # $RolesAssignement = Get-MgRoleManagementDirectoryRoleAssignment -All #-ExpandProperty "principal" # $EligibilityAssignement = Get-MgRoleManagementDirectoryRoleEligibilitySchedule -All $ErrorsCount = 0 try { $Users = Get-MgUser -ErrorAction Stop -All -Property DisplayName, CreatedDateTime, 'AccountEnabled', 'Mail', 'UserPrincipalName', 'Id', 'UserType', 'OnPremisesDistinguishedName', 'OnPremisesSamAccountName', 'OnPremisesLastSyncDateTime', 'OnPremisesSyncEnabled', 'OnPremisesUserPrincipalName' } catch { Write-Warning -Message "Get-MyRoleUsers - Failed to get users. Error: $($_.Exception.Message)" $ErrorsCount++ } try { $Groups = Get-MgGroup -ErrorAction Stop -All -Filter "IsAssignableToRole eq true" -Property CreatedDateTime, Id, DisplayName, Mail, OnPremisesLastSyncDateTime, SecurityEnabled } catch { Write-Warning -Message "Get-MyRoleUsers - Failed to get groups. Error: $($_.Exception.Message)" $ErrorsCount++ } #$Apps = Get-MgApplication -All try { $ServicePrincipals = Get-MgServicePrincipal -ErrorAction Stop -All -Property CreatedDateTime, 'ServicePrincipalType', 'DisplayName', 'AccountEnabled', 'Id', 'AppID' } catch { Write-Warning -Message "Get-MyRoleUsers - Failed to get service principals. Error: $($_.Exception.Message)" $ErrorsCount++ } #$DirectoryRole = Get-MgDirectoryRole -All try { $Roles = Get-MgRoleManagementDirectoryRoleDefinition -ErrorAction Stop -All } catch { Write-Warning -Message "Get-MyRoleUsers - Failed to get roles. Error: $($_.Exception.Message)" $ErrorsCount++ } try { $RolesAssignement = Get-MgRoleManagementDirectoryRoleAssignment -ErrorAction Stop -All #-ExpandProperty "principal" } catch { Write-Warning -Message "Get-MyRoleUsers - Failed to get roles assignement. Error: $($_.Exception.Message)" $ErrorsCount++ } try { $EligibilityAssignement = Get-MgRoleManagementDirectoryRoleEligibilitySchedule -ErrorAction Stop -All } catch { Write-Warning -Message "Get-MyRoleUsers - Failed to get eligibility assignement. Error: $($_.Exception.Message)" $ErrorsCount++ } if ($ErrorsCount -gt 0) { return } $CacheUsersAndApps = [ordered] @{} foreach ($User in $Users) { $CacheUsersAndApps[$User.Id] = $User } foreach ($ServicePrincipal in $ServicePrincipals) { $CacheUsersAndApps[$ServicePrincipal.Id] = $ServicePrincipal } foreach ($Group in $Groups) { $CacheUsersAndApps[$Group.Id] = $Group } $CacheRoles = [ordered] @{} foreach ($Role in $Roles) { $CacheRoles[$Role.Id] = [ordered] @{ Role = $Role Direct = [System.Collections.Generic.List[object]]::new() Eligible = [System.Collections.Generic.List[object]]::new() Users = [System.Collections.Generic.List[object]]::new() ServicePrincipals = [System.Collections.Generic.List[object]]::new() Groups = [System.Collections.Generic.List[object]]::new() } } foreach ($Role in $RolesAssignement) { if ($CacheRoles[$Role.RoleDefinitionId]) { $CacheRoles[$Role.RoleDefinitionId].Direct.Add($CacheUsersAndApps[$Role.PrincipalId]) if ($CacheUsersAndApps[$Role.PrincipalId].GetType().Name -eq 'MicrosoftGraphUser') { $CacheRoles[$Role.RoleDefinitionId].Users.Add($CacheUsersAndApps[$Role.PrincipalId]) } elseif ($CacheUsersAndApps[$Role.PrincipalId].GetType().Name -eq 'MicrosoftGraphGroup') { $CacheRoles[$Role.RoleDefinitionId].Groups.Add($CacheUsersAndApps[$Role.PrincipalId]) } elseif ($CacheUsersAndApps[$Role.PrincipalId].GetType().Name -eq 'MicrosoftGraphServicePrincipal') { $CacheRoles[$Role.RoleDefinitionId].ServicePrincipals.Add($CacheUsersAndApps[$Role.PrincipalId]) } else { Write-Warning -Message "Unknown type for principal id $($Role.PrincipalId) - not supported yet!" } # MicrosoftGraphServicePrincipal, MicrosoftGraphUser,MicrosoftGraphGroup } else { try { $TemporaryRole = Get-MgRoleManagementDirectoryRoleDefinition -UnifiedRoleDefinitionId $Role.RoleDefinitionId -ErrorAction Stop } catch { Write-Warning -Message "Role $($Role.RoleDefinitionId) was not found. Using direct query failed." } if ($TemporaryRole) { Write-Verbose -Message "Role $($Role.RoleDefinitionId) was not found. Using direct query revealed $($TemporaryRole.DisplayName)." $CacheRoles[$Role.RoleDefinitionId] = [ordered] @{ Role = $TemporaryRole Direct = [System.Collections.Generic.List[object]]::new() Eligible = [System.Collections.Generic.List[object]]::new() Users = [System.Collections.Generic.List[object]]::new() ServicePrincipals = [System.Collections.Generic.List[object]]::new() Groups = [System.Collections.Generic.List[object]]::new() } $CacheRoles[$Role.RoleDefinitionId].Direct.Add($CacheUsersAndApps[$Role.PrincipalId]) if ($CacheUsersAndApps[$Role.PrincipalId].GetType().Name -eq 'MicrosoftGraphUser') { $CacheRoles[$Role.RoleDefinitionId].Users.Add($CacheUsersAndApps[$Role.PrincipalId]) } elseif ($CacheUsersAndApps[$Role.PrincipalId].GetType().Name -eq 'MicrosoftGraphGroup') { $CacheRoles[$Role.RoleDefinitionId].Groups.Add($CacheUsersAndApps[$Role.PrincipalId]) } elseif ($CacheUsersAndApps[$Role.PrincipalId].GetType().Name -eq 'MicrosoftGraphServicePrincipal') { $CacheRoles[$Role.RoleDefinitionId].ServicePrincipals.Add($CacheUsersAndApps[$Role.PrincipalId]) } else { Write-Warning -Message "Unknown type for principal id $($Role.PrincipalId) - not supported yet!" } } } } foreach ($Role in $EligibilityAssignement) { if ($CacheRoles[$Role.RoleDefinitionId]) { $CacheRoles[$Role.RoleDefinitionId].Eligible.Add($CacheUsersAndApps[$Role.PrincipalId]) if ($CacheUsersAndApps[$Role.PrincipalId].GetType().Name -eq 'MicrosoftGraphUser') { $CacheRoles[$Role.RoleDefinitionId].Users.Add($CacheUsersAndApps[$Role.PrincipalId]) } elseif ($CacheUsersAndApps[$Role.PrincipalId].GetType().Name -eq 'MicrosoftGraphGroup') { $CacheRoles[$Role.RoleDefinitionId].Groups.Add($CacheUsersAndApps[$Role.PrincipalId]) } elseif ($CacheUsersAndApps[$Role.PrincipalId].GetType().Name -eq 'MicrosoftGraphServicePrincipal') { $CacheRoles[$Role.RoleDefinitionId].ServicePrincipals.Add($CacheUsersAndApps[$Role.PrincipalId]) } else { Write-Warning -Message "Unknown type for principal id $($Role.PrincipalId) - not supported yet!" } } else { Write-Warning -Message $Role } } # lets get group members of groups we have members in and roles are there too $CacheGroupMembers = [ordered] @{} foreach ($Role in $CacheRoles.Keys) { if ($CacheRoles[$Role].Groups.Count -gt 0) { foreach ($Group in $CacheRoles[$Role].Groups) { if (-not $CacheGroupMembers[$Group.DisplayName]) { $CacheGroupMembers[$Group.DisplayName] = [System.Collections.Generic.List[object]]::new() $GroupMembers = Get-MgGroupMember -GroupId $Group.Id -All #-ErrorAction Stop foreach ($GroupMember in $GroupMembers) { $CacheGroupMembers[$Group.DisplayName].Add($CacheUsersAndApps[$GroupMember.Id]) } } } } } foreach ($Role in $CacheRoles.Keys) { if ($OnlyWithMembers) { if ($CacheRoles[$Role].Direct.Count -eq 0 -and $CacheRoles[$Role].Eligible.Count -eq 0) { continue } } $GroupMembersTotal = 0 foreach ($Group in $CacheRoles[$Role].Groups) { $GroupMembersTotal = + $CacheGroupMembers[$Group.DisplayName].Count } [PSCustomObject] @{ Name = $CacheRoles[$Role].Role.DisplayName Description = $CacheRoles[$Role].Role.Description IsBuiltin = $CacheRoles[$Role].Role.IsBuiltIn IsEnabled = $CacheRoles[$Role].Role.IsEnabled AllowedResourceActions = $CacheRoles[$Role].Role.RolePermissions[0].AllowedResourceActions.Count TotalMembers = $CacheRoles[$Role].Direct.Count + $CacheRoles[$Role].Eligible.Count + $GroupMembersTotal DirectMembers = $CacheRoles[$Role].Direct.Count EligibleMembers = $CacheRoles[$Role].Eligible.Count GroupsMembers = $GroupMembersTotal # here's a split by numbers Users = $CacheRoles[$Role].Users.Count ServicePrincipals = $CacheRoles[$Role].ServicePrincipals.Count Groups = $CacheRoles[$Role].Groups.Count } } } function Get-MyRoleUsers { [CmdletBinding()] param( [switch] $OnlyWithRoles, [switch] $RolePerColumn ) $ErrorsCount = 0 try { $Users = Get-MgUser -ErrorAction Stop -All -Property DisplayName, CreatedDateTime, 'AccountEnabled', 'Mail', 'UserPrincipalName', 'Id', 'UserType', 'OnPremisesDistinguishedName', 'OnPremisesSamAccountName', 'OnPremisesLastSyncDateTime', 'OnPremisesSyncEnabled', 'OnPremisesUserPrincipalName' } catch { Write-Warning -Message "Get-MyRoleUsers - Failed to get users. Error: $($_.Exception.Message)" $ErrorsCount++ } try { $Groups = Get-MgGroup -ErrorAction Stop -All -Filter "IsAssignableToRole eq true" -Property CreatedDateTime, Id, DisplayName, Mail, OnPremisesLastSyncDateTime, SecurityEnabled } catch { Write-Warning -Message "Get-MyRoleUsers - Failed to get groups. Error: $($_.Exception.Message)" $ErrorsCount++ } #$Apps = Get-MgApplication -All try { $ServicePrincipals = Get-MgServicePrincipal -ErrorAction Stop -All -Property CreatedDateTime, 'ServicePrincipalType', 'DisplayName', 'AccountEnabled', 'Id', 'AppID' } catch { Write-Warning -Message "Get-MyRoleUsers - Failed to get service principals. Error: $($_.Exception.Message)" $ErrorsCount++ } #$DirectoryRole = Get-MgDirectoryRole -All try { $Roles = Get-MgRoleManagementDirectoryRoleDefinition -ErrorAction Stop -All } catch { Write-Warning -Message "Get-MyRoleUsers - Failed to get roles. Error: $($_.Exception.Message)" $ErrorsCount++ } try { $RolesAssignement = Get-MgRoleManagementDirectoryRoleAssignment -ErrorAction Stop -All #-ExpandProperty "principal" } catch { Write-Warning -Message "Get-MyRoleUsers - Failed to get roles assignement. Error: $($_.Exception.Message)" $ErrorsCount++ } try { $EligibilityAssignement = Get-MgRoleManagementDirectoryRoleEligibilitySchedule -ErrorAction Stop -All } catch { Write-Warning -Message "Get-MyRoleUsers - Failed to get eligibility assignement. Error: $($_.Exception.Message)" $ErrorsCount++ } if ($ErrorsCount -gt 0) { return } $CacheUsersAndApps = [ordered] @{} foreach ($User in $Users) { $CacheUsersAndApps[$User.Id] = @{ Identity = $User Direct = [System.Collections.Generic.List[object]]::new() Eligible = [System.Collections.Generic.List[object]]::new() } } foreach ($ServicePrincipal in $ServicePrincipals) { $CacheUsersAndApps[$ServicePrincipal.Id] = @{ Identity = $ServicePrincipal Direct = [System.Collections.Generic.List[object]]::new() Eligible = [System.Collections.Generic.List[object]]::new() } } foreach ($Group in $Groups) { $CacheUsersAndApps[$Group.Id] = @{ Identity = $Group Direct = [System.Collections.Generic.List[object]]::new() Eligible = [System.Collections.Generic.List[object]]::new() } } $CacheRoles = [ordered] @{} foreach ($Role in $Roles) { $CacheRoles[$Role.Id] = [ordered] @{ Role = $Role Members = [System.Collections.Generic.List[object]]::new() Users = [System.Collections.Generic.List[object]]::new() ServicePrincipals = [System.Collections.Generic.List[object]]::new() GroupsDirect = [System.Collections.Generic.List[object]]::new() GroupsEligible = [System.Collections.Generic.List[object]]::new() } } foreach ($Role in $RolesAssignement) { if ($CacheRoles[$Role.RoleDefinitionId]) { $CacheUsersAndApps[$Role.PrincipalId].Direct.Add($CacheRoles[$Role.RoleDefinitionId].Role) if ($CacheUsersAndApps[$Role.PrincipalId].Identity.GetType().Name -eq 'MicrosoftGraphGroup') { $CacheRoles[$Role.RoleDefinitionId].GroupsDirect.Add($CacheUsersAndApps[$Role.PrincipalId].Identity) } } else { try { $TemporaryRole = Get-MgRoleManagementDirectoryRoleDefinition -UnifiedRoleDefinitionId $Role.RoleDefinitionId -ErrorAction Stop } catch { Write-Warning -Message "Role $($Role.RoleDefinitionId) was not found. Using direct query failed." } if ($TemporaryRole) { Write-Verbose -Message "Role $($Role.RoleDefinitionId) was not found. Using direct query revealed $($TemporaryRole.DisplayName)." if (-not $CacheRoles[$Role.RoleDefinitionId]) { $CacheRoles[$Role.RoleDefinitionId] = [ordered] @{ Role = $TemporaryRole Direct = [System.Collections.Generic.List[object]]::new() Eligible = [System.Collections.Generic.List[object]]::new() Users = [System.Collections.Generic.List[object]]::new() ServicePrincipals = [System.Collections.Generic.List[object]]::new() } } $CacheUsersAndApps[$Role.PrincipalId].Direct.Add($CacheRoles[$Role.RoleDefinitionId].Role) } } } foreach ($Role in $EligibilityAssignement) { if ($CacheRoles[$Role.RoleDefinitionId]) { $CacheUsersAndApps[$Role.PrincipalId].Eligible.Add($CacheRoles[$Role.RoleDefinitionId].Role) if ($CacheUsersAndApps[$Role.PrincipalId].Identity.GetType().Name -eq 'MicrosoftGraphGroup') { $CacheRoles[$Role.RoleDefinitionId].GroupsEligible.Add($CacheUsersAndApps[$Role.PrincipalId].Identity) } } else { Write-Warning -Message $Role } } $ListActiveRoles = foreach ($Identity in $CacheUsersAndApps.Keys) { if ($OnlyWithRoles) { if ($CacheUsersAndApps[$Identity].Direct.Count -eq 0 -and $CacheUsersAndApps[$Identity].Eligible.Count -eq 0) { continue } $CacheUsersAndApps[$Identity].Direct.DisplayName $CacheUsersAndApps[$Identity].Eligible.DisplayName } } # lets get group members of groups we have members in and roles are there too $CacheGroupMembers = [ordered] @{} $CacheUserMembers = [ordered] @{} foreach ($Role in $CacheRoles.Keys) { if ($CacheRoles[$Role].GroupsDirect.Count -gt 0) { foreach ($Group in $CacheRoles[$Role].GroupsDirect) { if (-not $CacheGroupMembers[$Group.DisplayName]) { $CacheGroupMembers[$Group.DisplayName] = [ordered] @{ Group = $Group Members = Get-MgGroupMember -GroupId $Group.Id -All } } foreach ($GroupMember in $CacheGroupMembers[$Group.DisplayName].Members) { #$CacheGroupMembers[$Group.DisplayName].Add($CacheUsersAndApps[$GroupMember.Id]) if (-not $CacheUserMembers[$GroupMember.Id]) { $CacheUserMembers[$GroupMember.Id] = [ordered] @{ Identity = $GroupMember Role = [ordered] @{} #Direct = [System.Collections.Generic.List[object]]::new() #Eligible = [System.Collections.Generic.List[object]]::new() } } #$CacheUserMembers[$GroupMember.Id].Direct.Add($Group) $RoleDisplayName = $CacheRoles[$Role].Role.DisplayName if (-not $CacheUserMembers[$GroupMember.Id]['Role'][$RoleDisplayName]) { $CacheUserMembers[$GroupMember.Id]['Role'][$RoleDisplayName] = [ordered] @{ Role = $CacheRoles[$Role].Role GroupsDirect = [System.Collections.Generic.List[object]]::new() GroupsEligible = [System.Collections.Generic.List[object]]::new() } } $CacheUserMembers[$GroupMember.Id]['Role'][$RoleDisplayName].GroupsDirect.Add($Group) } } } if ($CacheRoles[$Role].GroupsEligible.Count -gt 0) { foreach ($Group in $CacheRoles[$Role].GroupsEligible) { if (-not $CacheGroupMembers[$Group.DisplayName]) { $CacheGroupMembers[$Group.DisplayName] = [ordered] @{ Group = $Group Members = Get-MgGroupMember -GroupId $Group.Id -All } } foreach ($GroupMember in $CacheGroupMembers[$Group.DisplayName].Members) { if (-not $CacheUserMembers[$GroupMember.Id]) { $CacheUserMembers[$GroupMember.Id] = [ordered] @{ Identity = $GroupMember Role = [ordered] @{} #Direct = [System.Collections.Generic.List[object]]::new() #Eligible = [System.Collections.Generic.List[object]]::new() } } $RoleDisplayName = $CacheRoles[$Role].Role.DisplayName if (-not $CacheUserMembers[$GroupMember.Id]['Role'][$RoleDisplayName]) { $CacheUserMembers[$GroupMember.Id]['Role'][$RoleDisplayName] = [ordered] @{ Role = $CacheRoles[$Role].Role GroupsDirect = [System.Collections.Generic.List[object]]::new() GroupsEligible = [System.Collections.Generic.List[object]]::new() } } $CacheUserMembers[$GroupMember.Id]['Role'][$RoleDisplayName].GroupsEligible.Add($Group) #$CacheUserMembers[$GroupMember.Id].Eligible.Add($Group) } #} } } } foreach ($Identity in $CacheUsersAndApps.Keys) { $Type = if ($CacheUsersAndApps[$Identity].Identity.ServicePrincipalType) { $CacheUsersAndApps[$Identity].Identity.ServicePrincipalType } elseif ($CacheUsersAndApps[$Identity].Identity.UserType) { $CacheUsersAndApps[$Identity].Identity.UserType } elseif ($null -ne $CacheUsersAndApps[$Identity].Identity.SecurityEnabled) { if ($CacheUsersAndApps[$Identity].Identity.SecurityEnabled) { "SecurityGroup" } else { "DistributionGroup" } } else { "Unknown" } $IsSynced = if ($CacheUsersAndApps[$Identity].Identity.OnPremisesLastSyncDateTime) { 'Synchronized' } else { 'Online' } $CanonicalName = if ($CacheUsersAndApps[$Identity].Identity.OnPremisesDistinguishedName) { ConvertFrom-DistinguishedName -DistinguishedName $CacheUsersAndApps[$Identity].Identity.OnPremisesDistinguishedName -ToOrganizationalUnit } else { $null } if (-not $RolePerColumn) { if ($OnlyWithRoles) { if ($CacheUsersAndApps[$Identity].Direct.Count -eq 0 -and $CacheUsersAndApps[$Identity].Eligible.Count -eq 0) { continue } } [PSCustomObject] @{ Name = $CacheUsersAndApps[$Identity].Identity.DisplayName Enabled = $CacheUsersAndApps[$Identity].Identity.AccountEnabled Status = $IsSynced Type = $Type CreatedDateTime = $CacheUsersAndApps[$Identity].Identity.CreatedDateTime Mail = $CacheUsersAndApps[$Identity].Identity.Mail UserPrincipalName = $CacheUsersAndApps[$Identity].Identity.UserPrincipalName AppId = $CacheUsersAndApps[$Identity].Identity.AppID DirectCount = $CacheUsersAndApps[$Identity].Direct.Count EligibleCount = $CacheUsersAndApps[$Identity].Eligible.Count Direct = $CacheUsersAndApps[$Identity].Direct.DisplayName Eligible = $CacheUsersAndApps[$Identity].Eligible.DisplayName Location = $CanonicalName #OnPremisesSamAccountName = $CacheUsersAndApps[$Identity].Identity.OnPremisesSamAccountName #OnPremisesLastSyncDateTime = $CacheUsersAndApps[$Identity].Identity.OnPremisesLastSyncDateTime } } else { # we need to use different way to count roles for each user # this is because we also count the roles of users nested in groups $RolesCount = 0 $GroupNameMember = $CacheUserMembers[$CacheUsersAndApps[$Identity].Identity.Id] if ($GroupNameMember) { # $GroupNameMember['Role'] # $DirectRoles = $CacheUsersAndApps[$GroupNameMember.id].Direct # $EligibleRoles = $CacheUsersAndApps[$GroupNameMember.id].Eligible # $IdentityOfGroup = $CacheUsersAndApps[$GroupNameMember.id].Identity.DisplayName } else { # $DirectRoles = $null # $EligibleRoles = $null # $IdentityOfGroup = $null } $UserIdentity = [ordered] @{ Name = $CacheUsersAndApps[$Identity].Identity.DisplayName Enabled = $CacheUsersAndApps[$Identity].Identity.AccountEnabled Status = $IsSynced Type = $Type CreatedDateTime = $CacheUsersAndApps[$Identity].Identity.CreatedDateTime Mail = $CacheUsersAndApps[$Identity].Identity.Mail UserPrincipalName = $CacheUsersAndApps[$Identity].Identity.UserPrincipalName } foreach ($Role in $ListActiveRoles | Sort-Object -Unique) { $UserIdentity[$Role] = '' } foreach ($Role in $CacheUsersAndApps[$Identity].Eligible) { if (-not $UserIdentity[$Role.DisplayName] ) { $UserIdentity[$Role.DisplayName] = [System.Collections.Generic.List[string]]::new() } $UserIdentity[$Role.DisplayName].Add('Eligible') $RolesCount++ } foreach ($Role in $CacheUsersAndApps[$Identity].Direct) { if (-not $UserIdentity[$Role.DisplayName] ) { $UserIdentity[$Role.DisplayName] = [System.Collections.Generic.List[string]]::new() } $UserIdentity[$Role.DisplayName].Add('Direct') $RolesCount++ } if ($GroupNameMember) { foreach ($Role in $GroupNameMember['Role'].Keys) { foreach ($Group in $GroupNameMember['Role'][$Role].GroupsDirect) { if (-not $UserIdentity[$Role] ) { $UserIdentity[$Role] = [System.Collections.Generic.List[string]]::new() } $UserIdentity[$Role].Add($Group.DisplayName) $RolesCount++ } foreach ($Group in $GroupNameMember['Role'][$Role].GroupsEligible) { if (-not $UserIdentity[$Role] ) { $UserIdentity[$Role] = [System.Collections.Generic.List[string]]::new() } $UserIdentity[$Role].Add($Group.DisplayName) $RolesCount++ } } # foreach ($Role in $DirectRoles) { # if (-not $UserIdentity[$Role.DisplayName] ) { # $UserIdentity[$Role.DisplayName] = [System.Collections.Generic.List[string]]::new() # } # $UserIdentity[$Role.DisplayName].Add($IdentityOfGroup) # } # foreach ($Role in $EligibleRoles) { # if (-not $UserIdentity[$Role.DisplayName]) { # $UserIdentity[$Role.DisplayName] = [System.Collections.Generic.List[string]]::new() # } # $UserIdentity[$Role.DisplayName].Add($IdentityOfGroup) # } } $UserIdentity['Location'] = $CanonicalName if ($OnlyWithRoles) { if ($RolesCount -eq 0) { continue } } [PSCustomObject] $UserIdentity } } } function Invoke-MyGraphEssentials { [cmdletBinding()] param( [string] $FilePath, [Parameter(Position = 0)][string[]] $Type, [switch] $PassThru, [switch] $HideHTML, [switch] $HideSteps, [switch] $ShowError, [switch] $ShowWarning, [switch] $Online, [switch] $SplitReports ) Reset-GraphEssentials #$Script:AllUsers = [ordered] @{} $Script:Cache = [ordered] @{} $Script:Reporting = [ordered] @{} $Script:Reporting['Version'] = Get-GitHubVersion -Cmdlet 'Invoke-MyGraphEssentials' -RepositoryOwner 'evotecit' -RepositoryName 'GraphEssentials' $Script:Reporting['Settings'] = @{ ShowError = $ShowError.IsPresent ShowWarning = $ShowWarning.IsPresent HideSteps = $HideSteps.IsPresent } Write-Color '[i]', "[GraphEssentials] ", 'Version', ' [Informative] ', $Script:Reporting['Version'] -Color Yellow, DarkGray, Yellow, DarkGray, Magenta # Verify requested types are supported $Supported = [System.Collections.Generic.List[string]]::new() [Array] $NotSupported = foreach ($T in $Type) { if ($T -notin $Script:GraphEssentialsConfiguration.Keys ) { $T } else { $Supported.Add($T) } } if ($Supported) { Write-Color '[i]', "[GraphEssentials] ", 'Supported types', ' [Informative] ', "Chosen by user: ", ($Supported -join ', ') -Color Yellow, DarkGray, Yellow, DarkGray, Yellow, Magenta } if ($NotSupported) { Write-Color '[i]', "[GraphEssentials] ", 'Not supported types', ' [Informative] ', "Following types are not supported: ", ($NotSupported -join ', ') -Color Yellow, DarkGray, Yellow, DarkGray, Yellow, Magenta Write-Color '[i]', "[GraphEssentials] ", 'Not supported types', ' [Informative] ', "Please use one/multiple from the list: ", ($Script:GraphEssentialsConfiguration.Keys -join ', ') -Color Yellow, DarkGray, Yellow, DarkGray, Yellow, Magenta return } # Lets make sure we only enable those types which are requestd by user if ($Type) { foreach ($T in $Script:GraphEssentialsConfiguration.Keys) { $Script:GraphEssentialsConfiguration[$T].Enabled = $false } # Lets enable all requested ones foreach ($T in $Type) { $Script:GraphEssentialsConfiguration[$T].Enabled = $true } } # Build data foreach ($T in $Script:GraphEssentialsConfiguration.Keys) { if ($Script:GraphEssentialsConfiguration[$T].Enabled -eq $true) { $Script:Reporting[$T] = [ordered] @{ Name = $Script:GraphEssentialsConfiguration[$T].Name ActionRequired = $null Data = $null Exclusions = $null WarningsAndErrors = $null Time = $null Summary = $null Variables = Copy-Dictionary -Dictionary $Script:GraphEssentialsConfiguration[$T]['Variables'] } if ($Exclusions) { if ($Exclusions -is [scriptblock]) { $Script:Reporting[$T]['ExclusionsCode'] = $Exclusions } if ($Exclusions -is [Array]) { $Script:Reporting[$T]['Exclusions'] = $Exclusions } } $TimeLogGraphEssentials = Start-TimeLog Write-Color -Text '[i]', '[Start] ', $($Script:GraphEssentialsConfiguration[$T]['Name']) -Color Yellow, DarkGray, Yellow $OutputCommand = Invoke-Command -ScriptBlock $Script:GraphEssentialsConfiguration[$T]['Execute'] -WarningVariable CommandWarnings -ErrorVariable CommandErrors #-ArgumentList $Forest, $ExcludeDomains, $IncludeDomains if ($OutputCommand -is [System.Collections.IDictionary]) { # in some cases the return will be wrapped in Hashtable/orderedDictionary and we need to handle this without an array $Script:Reporting[$T]['Data'] = $OutputCommand } else { # since sometimes it can be 0 or 1 objects being returned we force it being an array $Script:Reporting[$T]['Data'] = [Array] $OutputCommand } Invoke-Command -ScriptBlock $Script:GraphEssentialsConfiguration[$T]['Processing'] $Script:Reporting[$T]['WarningsAndErrors'] = @( if ($ShowWarning) { foreach ($War in $CommandWarnings) { [PSCustomObject] @{ Type = 'Warning' Comment = $War Reason = '' TargetName = '' } } } if ($ShowError) { foreach ($Err in $CommandErrors) { [PSCustomObject] @{ Type = 'Error' Comment = $Err Reason = $Err.CategoryInfo.Reason TargetName = $Err.CategoryInfo.TargetName } } } ) $TimeEndGraphEssentials = Stop-TimeLog -Time $TimeLogGraphEssentials -Option OneLiner $Script:Reporting[$T]['Time'] = $TimeEndGraphEssentials Write-Color -Text '[i]', '[End ] ', $($Script:GraphEssentialsConfiguration[$T]['Name']), " [Time to execute: $TimeEndGraphEssentials]" -Color Yellow, DarkGray, Yellow, DarkGray if ($SplitReports) { Write-Color -Text '[i]', '[HTML ] ', 'Generating HTML report for ', $T -Color Yellow, DarkGray, Yellow $TimeLogHTML = Start-TimeLog New-HTMLReportGraphEssentialsWithSplit -FilePath $FilePath -Online:$Online -HideHTML:$HideHTML -CurrentReport $T $TimeLogEndHTML = Stop-TimeLog -Time $TimeLogHTML -Option OneLiner Write-Color -Text '[i]', '[HTML ] ', 'Generating HTML report for', $T, " [Time to execute: $TimeLogEndHTML]" -Color Yellow, DarkGray, Yellow, DarkGray } } } if ( -not $SplitReports) { Write-Color -Text '[i]', '[HTML ] ', 'Generating HTML report' -Color Yellow, DarkGray, Yellow $TimeLogHTML = Start-TimeLog if (-not $FilePath) { $FilePath = Get-FileName -Extension 'html' -Temporary } New-HTMLReportGraphEssentials -Type $Type -Online:$Online.IsPresent -HideHTML:$HideHTML.IsPresent -FilePath $FilePath $TimeLogEndHTML = Stop-TimeLog -Time $TimeLogHTML -Option OneLiner Write-Color -Text '[i]', '[HTML ] ', 'Generating HTML report', " [Time to execute: $TimeLogEndHTML]" -Color Yellow, DarkGray, Yellow, DarkGray } Reset-GraphEssentials } [scriptblock] $SourcesAutoCompleter = { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) $Script:GraphEssentialsConfiguration.Keys | Sort-Object | Where-Object { $_ -like "*$wordToComplete*" } } Register-ArgumentCompleter -CommandName Invoke-MyGraphEssentials -ParameterName Type -ScriptBlock $SourcesAutoCompleter function New-MyApp { [cmdletBinding()] param( [parameter(Mandatory)][string] $ApplicationName, [parameter(Mandatory)][string] $DisplayNameCredentials, [int] $MonthsValid = 12, [switch] $RemoveOldCredentials, [switch] $ServicePrincipal ) $Application = Get-MgApplication -Filter "displayName eq '$ApplicationName'" -All if (-not $Application) { Write-Verbose -Message "New-MyApp - Creating application $ApplicationName" $Application = New-MgApplication -DisplayName $ApplicationName if ($ServicePrincipal) { Write-Verbose -Message "New-MyApp - Creating service principal for $ApplicationName" # not sure if it will help, but lets try it Start-Sleep -Seconds 5 try { $ServicePrincipalData = New-MgServicePrincipal -AppId $App.AppId -AccountEnabled:$true } catch { Write-Warning -Message "New-MyApp - Failed to create service principal for $ApplicationName. Error: $($_.Exception.Message)" } } } else { Write-Verbose -Message "New-MyApp - Application $ApplicationName already exists. Reusing..." } if ($RemoveOldCredentials -and $Application.PasswordCredentials.Count -gt 0) { foreach ($Credential in $Application.PasswordCredentials) { Write-Verbose -Message "New-MyApp - Removing old credential $($Credential.KeyId) / $($Credential.DisplayName)" try { Remove-MgApplicationPassword -ApplicationId $Application.Id -KeyId $Credential.KeyId -ErrorAction Stop } catch { Write-Warning -Message "New-MyApp - Failed to remove old credential $($Credential.KeyId) / $($Credential.DisplayName)" return } } } $Credentials = New-MyAppCredentials -ObjectID $Application.Id -DisplayName $DisplayNameCredentials -MonthsValid $MonthsValid if ($Application -and $Credentials) { [PSCustomObject] @{ ObjectID = $Application.Id ApplicationName = $Application.DisplayName ClientID = $Application.AppId ClientSecretName = $Credentials.DisplayName ClientSecret = $Credentials.SecretText ClientSecretID = $Credentials.KeyID DaysToExpire = ($Credentials.EndDateTime - [DateTime]::Now).Days StartDateTime = $Credentials.StartDateTime EndDateTime = $Credentials.EndDateTime } } else { Write-Warning -Message "New-MyApp - Application or credentials for $ApplicationName was not created." } } function New-MyAppCredentials { [cmdletbinding(DefaultParameterSetName = 'AppName')] param( [parameter(Mandatory, ParameterSetName = 'AppId')][string] $ObjectID, [alias('AppName')] [parameter(Mandatory, ParameterSetName = 'AppName')][string] $ApplicationName, [string] $DisplayName, [int] $MonthsValid = 12 ) if ($AppName) { $Application = Get-MgApplication -Filter "DisplayName eq '$ApplicationName'" -ConsistencyLevel eventual -All if ($Application) { $ID = $Application.Id } else { Write-Warning -Message "Application with name '$ApplicationName' not found" return } } else { $ID = $ObjectID } $PasswordCredential = [Microsoft.Graph.PowerShell.Models.IMicrosoftGraphPasswordCredential] @{ StartDateTime = [datetime]::Now } if ($DisplayName) { $PasswordCredential.DisplayName = $DisplayName } $PasswordCredential.EndDateTime = [datetime]::Now.AddMonths($MonthsValid) try { Add-MgApplicationPassword -ApplicationId $ID -PasswordCredential $PasswordCredential } catch { Write-Warning -Message "Failed to add password credential to application $ID / $ApplicationName" } } function Show-MyApp { [cmdletBinding()] param( [Parameter(Mandatory)][string] $FilePath, [switch] $Online, [switch] $ShowHTML ) $Applications = Get-MyApp $ApplicationsPassword = Get-MyAppCredentials New-HTML { New-HTMLTableOption -DataStore JavaScript -BoolAsString New-HTMLSection -Invisible { New-HTMLSection -HeaderText "Applications" { New-HTMLTable -DataTable $Applications -Filtering { New-TableEvent -ID 'TableAppsCredentials' -SourceColumnID 1 -TargetColumnID 1 } -DataStore JavaScript -DataTableID "TableApps" } New-HTMLSection -HeaderText 'Applications Credentials' { New-HTMLTable -DataTable $ApplicationsPassword -Filtering { New-HTMLTableCondition -Name 'DaysToExpire' -Value 30 -Operator 'ge' -BackgroundColor Conifer -ComparisonType number New-HTMLTableCondition -Name 'DaysToExpire' -Value 30 -Operator 'lt' -BackgroundColor Orange -ComparisonType number New-HTMLTableCondition -Name 'DaysToExpire' -Value 5 -Operator 'lt' -BackgroundColor Red -ComparisonType number New-HTMLTableCondition -Name 'Expired' -Value $true -ComparisonType string -BackgroundColor Salmon -FailBackgroundColor Conifer } -DataStore JavaScript -DataTableID "TableAppsCredentials" } } } -ShowHTML:$ShowHTML.IsPresent -FilePath $FilePath -Online:$Online.IsPresent } # Export functions and aliases as required Export-ModuleMember -Function @('Get-MyApp', 'Get-MyAppCredentials', 'Get-MyRole', 'Get-MyRoleUsers', 'Invoke-MyGraphEssentials', 'New-MyApp', 'New-MyAppCredentials', 'Show-MyApp') -Alias @() # SIG # Begin signature block # MIInPgYJKoZIhvcNAQcCoIInLzCCJysCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDPXNxFDr1f+kEV # oxDCkVOaSajeU/AHttHlWaLkpd0RZaCCITcwggO3MIICn6ADAgECAhAM5+DlF9hG # /o/lYPwb8DA5MA0GCSqGSIb3DQEBBQUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQK # EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNV # BAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0wNjExMTAwMDAwMDBa # Fw0zMTExMTAwMDAwMDBaMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy # dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNVBAMTG0RpZ2lD # ZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC # AQoCggEBAK0OFc7kQ4BcsYfzt2D5cRKlrtwmlIiq9M71IDkoWGAM+IDaqRWVMmE8 # tbEohIqK3J8KDIMXeo+QrIrneVNcMYQq9g+YMjZ2zN7dPKii72r7IfJSYd+fINcf # 4rHZ/hhk0hJbX/lYGDW8R82hNvlrf9SwOD7BG8OMM9nYLxj+KA+zp4PWw25EwGE1 # lhb+WZyLdm3X8aJLDSv/C3LanmDQjpA1xnhVhyChz+VtCshJfDGYM2wi6YfQMlqi # uhOCEe05F52ZOnKh5vqk2dUXMXWuhX0irj8BRob2KHnIsdrkVxfEfhwOsLSSplaz # vbKX7aqn8LfFqD+VFtD/oZbrCF8Yd08CAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGG # MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFEXroq/0ksuCMS1Ri6enIZ3zbcgP # MB8GA1UdIwQYMBaAFEXroq/0ksuCMS1Ri6enIZ3zbcgPMA0GCSqGSIb3DQEBBQUA # A4IBAQCiDrzf4u3w43JzemSUv/dyZtgy5EJ1Yq6H6/LV2d5Ws5/MzhQouQ2XYFwS # TFjk0z2DSUVYlzVpGqhH6lbGeasS2GeBhN9/CTyU5rgmLCC9PbMoifdf/yLil4Qf # 6WXvh+DfwWdJs13rsgkq6ybteL59PyvztyY1bV+JAbZJW58BBZurPSXBzLZ/wvFv # hsb6ZGjrgS2U60K3+owe3WLxvlBnt2y98/Efaww2BxZ/N3ypW2168RJGYIPXJwS+ # S86XvsNnKmgR34DnDDNmvxMNFG7zfx9jEB76jRslbWyPpbdhAbHSoyahEHGdreLD # +cOZUbcrBwjOLuZQsqf6CkUvovDyMIIFMDCCBBigAwIBAgIQBAkYG1/Vu2Z1U0O1 # b5VQCDANBgkqhkiG9w0BAQsFADBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGln # aUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtE # aWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwHhcNMTMxMDIyMTIwMDAwWhcNMjgx # MDIyMTIwMDAwWjByMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5j # MRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMTEwLwYDVQQDEyhEaWdpQ2VydCBT # SEEyIEFzc3VyZWQgSUQgQ29kZSBTaWduaW5nIENBMIIBIjANBgkqhkiG9w0BAQEF # AAOCAQ8AMIIBCgKCAQEA+NOzHH8OEa9ndwfTCzFJGc/Q+0WZsTrbRPV/5aid2zLX # cep2nQUut4/6kkPApfmJ1DcZ17aq8JyGpdglrA55KDp+6dFn08b7KSfH03sjlOSR # I5aQd4L5oYQjZhJUM1B0sSgmuyRpwsJS8hRniolF1C2ho+mILCCVrhxKhwjfDPXi # TWAYvqrEsq5wMWYzcT6scKKrzn/pfMuSoeU7MRzP6vIK5Fe7SrXpdOYr/mzLfnQ5 # Ng2Q7+S1TqSp6moKq4TzrGdOtcT3jNEgJSPrCGQ+UpbB8g8S9MWOD8Gi6CxR93O8 # vYWxYoNzQYIH5DiLanMg0A9kczyen6Yzqf0Z3yWT0QIDAQABo4IBzTCCAckwEgYD # VR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwEwYDVR0lBAwwCgYIKwYB # BQUHAwMweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5k # aWdpY2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0 # LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwgYEGA1UdHwR6MHgwOqA4 # oDaGNGh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJv # b3RDQS5jcmwwOqA4oDaGNGh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2Vy # dEFzc3VyZWRJRFJvb3RDQS5jcmwwTwYDVR0gBEgwRjA4BgpghkgBhv1sAAIEMCow # KAYIKwYBBQUHAgEWHGh0dHBzOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwCgYIYIZI # AYb9bAMwHQYDVR0OBBYEFFrEuXsqCqOl6nEDwGD5LfZldQ5YMB8GA1UdIwQYMBaA # FEXroq/0ksuCMS1Ri6enIZ3zbcgPMA0GCSqGSIb3DQEBCwUAA4IBAQA+7A1aJLPz # ItEVyCx8JSl2qB1dHC06GsTvMGHXfgtg/cM9D8Svi/3vKt8gVTew4fbRknUPUbRu # pY5a4l4kgU4QpO4/cY5jDhNLrddfRHnzNhQGivecRk5c/5CxGwcOkRX7uq+1UcKN # JK4kxscnKqEpKBo6cSgCPC6Ro8AlEeKcFEehemhor5unXCBc2XGxDI+7qPjFEmif # z0DLQESlE/DmZAwlCEIysjaKJAL+L3J+HNdJRZboWR3p+nRka7LrZkPas7CM1ekN # 3fYBIM6ZMWM9CBoYs4GbT8aTEAb8B4H6i9r5gkn3Ym6hU/oSlBiFLpKR6mhsRDKy # ZqHnGKSaZFHvMIIFPTCCBCWgAwIBAgIQBNXcH0jqydhSALrNmpsqpzANBgkqhkiG # 9w0BAQsFADByMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkw # FwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMTEwLwYDVQQDEyhEaWdpQ2VydCBTSEEy # IEFzc3VyZWQgSUQgQ29kZSBTaWduaW5nIENBMB4XDTIwMDYyNjAwMDAwMFoXDTIz # MDcwNzEyMDAwMFowejELMAkGA1UEBhMCUEwxEjAQBgNVBAgMCcWabMSFc2tpZTER # MA8GA1UEBxMIS2F0b3dpY2UxITAfBgNVBAoMGFByemVteXPFgmF3IEvFgnlzIEVW # T1RFQzEhMB8GA1UEAwwYUHJ6ZW15c8WCYXcgS8WCeXMgRVZPVEVDMIIBIjANBgkq # hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv7KB3iyBrhkLUbbFe9qxhKKPBYqDBqln # r3AtpZplkiVjpi9dMZCchSeT5ODsShPuZCIxJp5I86uf8ibo3vi2S9F9AlfFjVye # 3dTz/9TmCuGH8JQt13ozf9niHecwKrstDVhVprgxi5v0XxY51c7zgMA2g1Ub+3ti # i0vi/OpmKXdL2keNqJ2neQ5cYly/GsI8CREUEq9SZijbdA8VrRF3SoDdsWGf3tZZ # zO6nWn3TLYKQ5/bw5U445u/V80QSoykszHRivTj+H4s8ABiforhi0i76beA6Ea41 # zcH4zJuAp48B4UhjgRDNuq8IzLWK4dlvqrqCBHKqsnrF6BmBrv+BXQIDAQABo4IB # xTCCAcEwHwYDVR0jBBgwFoAUWsS5eyoKo6XqcQPAYPkt9mV1DlgwHQYDVR0OBBYE # FBixNSfoHFAgJk4JkDQLFLRNlJRmMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAK # BggrBgEFBQcDAzB3BgNVHR8EcDBuMDWgM6Axhi9odHRwOi8vY3JsMy5kaWdpY2Vy # dC5jb20vc2hhMi1hc3N1cmVkLWNzLWcxLmNybDA1oDOgMYYvaHR0cDovL2NybDQu # ZGlnaWNlcnQuY29tL3NoYTItYXNzdXJlZC1jcy1nMS5jcmwwTAYDVR0gBEUwQzA3 # BglghkgBhv1sAwEwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQu # Y29tL0NQUzAIBgZngQwBBAEwgYQGCCsGAQUFBwEBBHgwdjAkBggrBgEFBQcwAYYY # aHR0cDovL29jc3AuZGlnaWNlcnQuY29tME4GCCsGAQUFBzAChkJodHRwOi8vY2Fj # ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRTSEEyQXNzdXJlZElEQ29kZVNpZ25p # bmdDQS5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAQEAmr1sz4ls # LARi4wG1eg0B8fVJFowtect7SnJUrp6XRnUG0/GI1wXiLIeow1UPiI6uDMsRXPHU # F/+xjJw8SfIbwava2eXu7UoZKNh6dfgshcJmo0QNAJ5PIyy02/3fXjbUREHINrTC # vPVbPmV6kx4Kpd7KJrCo7ED18H/XTqWJHXa8va3MYLrbJetXpaEPpb6zk+l8Rj9y # G4jBVRhenUBUUj3CLaWDSBpOA/+sx8/XB9W9opYfYGb+1TmbCkhUg7TB3gD6o6ES # Jre+fcnZnPVAPESmstwsT17caZ0bn7zETKlNHbc1q+Em9kyBjaQRcEQoQQNpezQu # g9ufqExx6lHYDjCCBY0wggR1oAMCAQICEA6bGI750C3n79tQ4ghAGFowDQYJKoZI # hvcNAQEMBQAwZTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZ # MBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEkMCIGA1UEAxMbRGlnaUNlcnQgQXNz # dXJlZCBJRCBSb290IENBMB4XDTIyMDgwMTAwMDAwMFoXDTMxMTEwOTIzNTk1OVow # YjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQ # d3d3LmRpZ2ljZXJ0LmNvbTEhMB8GA1UEAxMYRGlnaUNlcnQgVHJ1c3RlZCBSb290 # IEc0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAv+aQc2jeu+RdSjww # IjBpM+zCpyUuySE98orYWcLhKac9WKt2ms2uexuEDcQwH/MbpDgW61bGl20dq7J5 # 8soR0uRf1gU8Ug9SH8aeFaV+vp+pVxZZVXKvaJNwwrK6dZlqczKU0RBEEC7fgvMH # hOZ0O21x4i0MG+4g1ckgHWMpLc7sXk7Ik/ghYZs06wXGXuxbGrzryc/NrDRAX7F6 # Zu53yEioZldXn1RYjgwrt0+nMNlW7sp7XeOtyU9e5TXnMcvak17cjo+A2raRmECQ # ecN4x7axxLVqGDgDEI3Y1DekLgV9iPWCPhCRcKtVgkEy19sEcypukQF8IUzUvK4b # A3VdeGbZOjFEmjNAvwjXWkmkwuapoGfdpCe8oU85tRFYF/ckXEaPZPfBaYh2mHY9 # WV1CdoeJl2l6SPDgohIbZpp0yt5LHucOY67m1O+SkjqePdwA5EUlibaaRBkrfsCU # tNJhbesz2cXfSwQAzH0clcOP9yGyshG3u3/y1YxwLEFgqrFjGESVGnZifvaAsPvo # ZKYz0YkH4b235kOkGLimdwHhD5QMIR2yVCkliWzlDlJRR3S+Jqy2QXXeeqxfjT/J # vNNBERJb5RBQ6zHFynIWIgnffEx1P2PsIV/EIFFrb7GrhotPwtZFX50g/KEexcCP # orF+CiaZ9eRpL5gdLfXZqbId5RsCAwEAAaOCATowggE2MA8GA1UdEwEB/wQFMAMB # Af8wHQYDVR0OBBYEFOzX44LScV1kTN8uZz/nupiuHA9PMB8GA1UdIwQYMBaAFEXr # oq/0ksuCMS1Ri6enIZ3zbcgPMA4GA1UdDwEB/wQEAwIBhjB5BggrBgEFBQcBAQRt # MGswJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBDBggrBgEF # BQcwAoY3aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJl # ZElEUm9vdENBLmNydDBFBgNVHR8EPjA8MDqgOKA2hjRodHRwOi8vY3JsMy5kaWdp # Y2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURSb290Q0EuY3JsMBEGA1UdIAQKMAgw # BgYEVR0gADANBgkqhkiG9w0BAQwFAAOCAQEAcKC/Q1xV5zhfoKN0Gz22Ftf3v1cH # vZqsoYcs7IVeqRq7IviHGmlUIu2kiHdtvRoU9BNKei8ttzjv9P+Aufih9/Jy3iS8 # UgPITtAq3votVs/59PesMHqai7Je1M/RQ0SbQyHrlnKhSLSZy51PpwYDE3cnRNTn # f+hZqPC/Lwum6fI0POz3A8eHqNJMQBk1RmppVLC4oVaO7KTVPeix3P0c2PR3WlxU # jG/voVA9/HYJaISfb8rbII01YBwCA8sgsKxYoA5AY8WYIsGyWfVVa88nq2x2zm8j # LfR+cWojayL/ErhULSd+2DrZ8LaHlv1b0VysGMNNn3O3AamfV6peKOK5lDCCBq4w # ggSWoAMCAQICEAc2N7ckVHzYR6z9KGYqXlswDQYJKoZIhvcNAQELBQAwYjELMAkG # A1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRp # Z2ljZXJ0LmNvbTEhMB8GA1UEAxMYRGlnaUNlcnQgVHJ1c3RlZCBSb290IEc0MB4X # DTIyMDMyMzAwMDAwMFoXDTM3MDMyMjIzNTk1OVowYzELMAkGA1UEBhMCVVMxFzAV # BgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMTswOQYDVQQDEzJEaWdpQ2VydCBUcnVzdGVk # IEc0IFJTQTQwOTYgU0hBMjU2IFRpbWVTdGFtcGluZyBDQTCCAiIwDQYJKoZIhvcN # AQEBBQADggIPADCCAgoCggIBAMaGNQZJs8E9cklRVcclA8TykTepl1Gh1tKD0Z5M # om2gsMyD+Vr2EaFEFUJfpIjzaPp985yJC3+dH54PMx9QEwsmc5Zt+FeoAn39Q7SE # 2hHxc7Gz7iuAhIoiGN/r2j3EF3+rGSs+QtxnjupRPfDWVtTnKC3r07G1decfBmWN # lCnT2exp39mQh0YAe9tEQYncfGpXevA3eZ9drMvohGS0UvJ2R/dhgxndX7RUCyFo # bjchu0CsX7LeSn3O9TkSZ+8OpWNs5KbFHc02DVzV5huowWR0QKfAcsW6Th+xtVhN # ef7Xj3OTrCw54qVI1vCwMROpVymWJy71h6aPTnYVVSZwmCZ/oBpHIEPjQ2OAe3Vu # JyWQmDo4EbP29p7mO1vsgd4iFNmCKseSv6De4z6ic/rnH1pslPJSlRErWHRAKKtz # Q87fSqEcazjFKfPKqpZzQmiftkaznTqj1QPgv/CiPMpC3BhIfxQ0z9JMq++bPf4O # uGQq+nUoJEHtQr8FnGZJUlD0UfM2SU2LINIsVzV5K6jzRWC8I41Y99xh3pP+OcD5 # sjClTNfpmEpYPtMDiP6zj9NeS3YSUZPJjAw7W4oiqMEmCPkUEBIDfV8ju2TjY+Cm # 4T72wnSyPx4JduyrXUZ14mCjWAkBKAAOhFTuzuldyF4wEr1GnrXTdrnSDmuZDNIz # tM2xAgMBAAGjggFdMIIBWTASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBS6 # FtltTYUvcyl2mi91jGogj57IbzAfBgNVHSMEGDAWgBTs1+OC0nFdZEzfLmc/57qY # rhwPTzAOBgNVHQ8BAf8EBAMCAYYwEwYDVR0lBAwwCgYIKwYBBQUHAwgwdwYIKwYB # BQUHAQEEazBpMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20w # QQYIKwYBBQUHMAKGNWh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2Vy # dFRydXN0ZWRSb290RzQuY3J0MEMGA1UdHwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwz # LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRSb290RzQuY3JsMCAGA1UdIAQZ # MBcwCAYGZ4EMAQQCMAsGCWCGSAGG/WwHATANBgkqhkiG9w0BAQsFAAOCAgEAfVmO # wJO2b5ipRCIBfmbW2CFC4bAYLhBNE88wU86/GPvHUF3iSyn7cIoNqilp/GnBzx0H # 6T5gyNgL5Vxb122H+oQgJTQxZ822EpZvxFBMYh0MCIKoFr2pVs8Vc40BIiXOlWk/ # R3f7cnQU1/+rT4osequFzUNf7WC2qk+RZp4snuCKrOX9jLxkJodskr2dfNBwCnzv # qLx1T7pa96kQsl3p/yhUifDVinF2ZdrM8HKjI/rAJ4JErpknG6skHibBt94q6/ae # sXmZgaNWhqsKRcnfxI2g55j7+6adcq/Ex8HBanHZxhOACcS2n82HhyS7T6NJuXdm # kfFynOlLAlKnN36TU6w7HQhJD5TNOXrd/yVjmScsPT9rp/Fmw0HNT7ZAmyEhQNC3 # EyTN3B14OuSereU0cZLXJmvkOHOrpgFPvT87eK1MrfvElXvtCl8zOYdBeHo46Zzh # 3SP9HSjTx/no8Zhf+yvYfvJGnXUsHicsJttvFXseGYs2uJPU5vIXmVnKcPA3v5gA # 3yAWTyf7YGcWoWa63VXAOimGsJigK+2VQbc61RWYMbRiCQ8KvYHZE/6/pNHzV9m8 # BPqC3jLfBInwAM1dwvnQI38AC+R2AibZ8GV2QqYphwlHK+Z/GqSFD/yYlvZVVCsf # gPrA8g4r5db7qS9EFUrnEw4d2zc4GqEr9u3WfPwwggbAMIIEqKADAgECAhAMTWly # S5T6PCpKPSkHgD1aMA0GCSqGSIb3DQEBCwUAMGMxCzAJBgNVBAYTAlVTMRcwFQYD # VQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1c3RlZCBH # NCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0EwHhcNMjIwOTIxMDAwMDAw # WhcNMzMxMTIxMjM1OTU5WjBGMQswCQYDVQQGEwJVUzERMA8GA1UEChMIRGlnaUNl # cnQxJDAiBgNVBAMTG0RpZ2lDZXJ0IFRpbWVzdGFtcCAyMDIyIC0gMjCCAiIwDQYJ # KoZIhvcNAQEBBQADggIPADCCAgoCggIBAM/spSY6xqnya7uNwQ2a26HoFIV0Mxom # rNAcVR4eNm28klUMYfSdCXc9FZYIL2tkpP0GgxbXkZI4HDEClvtysZc6Va8z7GGK # 6aYo25BjXL2JU+A6LYyHQq4mpOS7eHi5ehbhVsbAumRTuyoW51BIu4hpDIjG8b7g # L307scpTjUCDHufLckkoHkyAHoVW54Xt8mG8qjoHffarbuVm3eJc9S/tjdRNlYRo # 44DLannR0hCRRinrPibytIzNTLlmyLuqUDgN5YyUXRlav/V7QG5vFqianJVHhoV5 # PgxeZowaCiS+nKrSnLb3T254xCg/oxwPUAY3ugjZNaa1Htp4WB056PhMkRCWfk3h # 3cKtpX74LRsf7CtGGKMZ9jn39cFPcS6JAxGiS7uYv/pP5Hs27wZE5FX/NurlfDHn # 88JSxOYWe1p+pSVz28BqmSEtY+VZ9U0vkB8nt9KrFOU4ZodRCGv7U0M50GT6Vs/g # 9ArmFG1keLuY/ZTDcyHzL8IuINeBrNPxB9ThvdldS24xlCmL5kGkZZTAWOXlLimQ # prdhZPrZIGwYUWC6poEPCSVT8b876asHDmoHOWIZydaFfxPZjXnPYsXs4Xu5zGcT # B5rBeO3GiMiwbjJ5xwtZg43G7vUsfHuOy2SJ8bHEuOdTXl9V0n0ZKVkDTvpd6kVz # HIR+187i1Dp3AgMBAAGjggGLMIIBhzAOBgNVHQ8BAf8EBAMCB4AwDAYDVR0TAQH/ # BAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAgBgNVHSAEGTAXMAgGBmeBDAEE # AjALBglghkgBhv1sBwEwHwYDVR0jBBgwFoAUuhbZbU2FL3MpdpovdYxqII+eyG8w # HQYDVR0OBBYEFGKK3tBh/I8xFO2XC809KpQU31KcMFoGA1UdHwRTMFEwT6BNoEuG # SWh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJTQTQw # OTZTSEEyNTZUaW1lU3RhbXBpbmdDQS5jcmwwgZAGCCsGAQUFBwEBBIGDMIGAMCQG # CCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wWAYIKwYBBQUHMAKG # TGh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJT # QTQwOTZTSEEyNTZUaW1lU3RhbXBpbmdDQS5jcnQwDQYJKoZIhvcNAQELBQADggIB # AFWqKhrzRvN4Vzcw/HXjT9aFI/H8+ZU5myXm93KKmMN31GT8Ffs2wklRLHiIY1UJ # RjkA/GnUypsp+6M/wMkAmxMdsJiJ3HjyzXyFzVOdr2LiYWajFCpFh0qYQitQ/Bu1 # nggwCfrkLdcJiXn5CeaIzn0buGqim8FTYAnoo7id160fHLjsmEHw9g6A++T/350Q # p+sAul9Kjxo6UrTqvwlJFTU2WZoPVNKyG39+XgmtdlSKdG3K0gVnK3br/5iyJpU4 # GYhEFOUKWaJr5yI+RCHSPxzAm+18SLLYkgyRTzxmlK9dAlPrnuKe5NMfhgFknADC # 6Vp0dQ094XmIvxwBl8kZI4DXNlpflhaxYwzGRkA7zl011Fk+Q5oYrsPJy8P7mxNf # arXH4PMFw1nfJ2Ir3kHJU7n/NBBn9iYymHv+XEKUgZSCnawKi8ZLFUrTmJBFYDOA # 4CPe+AOk9kVH5c64A0JH6EE2cXet/aLol3ROLtoeHYxayB6a1cLwxiKoT5u92Bya # UcQvmvZfpyeXupYuhVfAYOd4Vn9q78KVmksRAsiCnMkaBXy6cbVOepls9Oie1FqY # yJ+/jbsYXEP10Cro4mLueATbvdH7WwqocH7wl4R44wgDXUcsY6glOJcB0j862uXl # 9uab3H4szP8XTE0AotjWAQ64i+7m4HJViSwnGWH2dwGMMYIFXTCCBVkCAQEwgYYw # cjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQ # d3d3LmRpZ2ljZXJ0LmNvbTExMC8GA1UEAxMoRGlnaUNlcnQgU0hBMiBBc3N1cmVk # IElEIENvZGUgU2lnbmluZyBDQQIQBNXcH0jqydhSALrNmpsqpzANBglghkgBZQME # AgEFAKCBhDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEM # BgorBgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqG # SIb3DQEJBDEiBCB8bZSz/g44X9q+b7Bhx9LCm1AtoFoeCnFzFnBju2qWvTANBgkq # hkiG9w0BAQEFAASCAQAXTZzaWBe4fDkW9o5M35pQo7KVqK5cT64selkLNWc6oDx7 # kIyf+IFX0pZOzD9shK2X+c/RhzfXhTbVE8m3Knqb9X5IlBsH0Kc6ovJ2jd9vi5bV # mLd2NkiH+knJmn3K5dFk2EysLfCepg3m8uEDkojMQhYjSZAzuvio4+SIQ/7sBwnD # l+iBSTE/f899qmimSB6G9Kyviu+hgr9aiuniZT2WlYuWI38XcSgmg4K3484ykSYv # yn9otBzrjmPU3jiCjUT4QD7Ffn8NdqxLDsf70RO4YiRU34DOJavrNNREtE4DEfUk # mBIFBcNew2+njSXbcOGELZbNpieLxQBg2I61ZrzBoYIDIDCCAxwGCSqGSIb3DQEJ # BjGCAw0wggMJAgEBMHcwYzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0 # LCBJbmMuMTswOQYDVQQDEzJEaWdpQ2VydCBUcnVzdGVkIEc0IFJTQTQwOTYgU0hB # MjU2IFRpbWVTdGFtcGluZyBDQQIQDE1pckuU+jwqSj0pB4A9WjANBglghkgBZQME # AgEFAKBpMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8X # DTIyMTExNDIxMzYxNlowLwYJKoZIhvcNAQkEMSIEIJDYbNDTOO4pWfUKCBGs+lPl # LSsoKOogI+8Y21B3vs4mMA0GCSqGSIb3DQEBAQUABIICAEfLJ1C2ZVu/ufBpoRVS # i3uTQkFRJ6ZIDVRPjWy0tOEnQODm/LxcsYCSD9sXi6M7haTnIZIVom5NP1KRe58u # xwivwpdVgHIAaD8m8HquDWRv8jvMrBdna06U9PciCHuQKyipISIk3SQxXXOy021j # YBKORM6cB7hqknG4+GvRPln1ezX5vhC/CXjmFFkrSCdmQSo8mxli4gf2fAbzUOJq # 5htUUF+TmnoZ+T497fdgUV+3NRpgotnNgUcmFg2icDmmC21QhPkNlvdX8+yhmkbm # gL8w1/1bHcES/94r255mjfhuCXKu/qAUNEQmd5R+iWo056TDsPKcLOhKj2fxQID4 # Tr/01VIA5oRqpR2rhWahN3MEtv7Lqnnzmy1gLisom6xssrPQmJhjHJeAffHU0jDH # rSEtM3PAUK0kMeqpjTr4wz2JdfIbAXzmMIM0ELp1jpWBQ3AMz9kd7T9NfbLK56Ol # brOMLIw7jalbCFbxwmAd333K/lXUwy1SKsFmS8BdyNHEL6yHsJT7A3JBEGKyjoP1 # OldLN6v/vsW90e5nwGiCKC9a3gXhmwnsLqJ2KlO88PUwPDIkUZ5N2ifLf+TkPTr7 # YQ6+PMzxlKk6lvzKrsZlGns9PiUZqLqgwD9igj2RXT+Iy6xrpgmKxFWNWDpMgGJU # kWP0OOEMYlyt7AhxljmPRBDo # SIG # End signature block |