Public/PrivilegedAccess/Invoke-EntraOpsPrivilegedEAM.ps1
|
<# .SYNOPSIS Run EntraOps Privileged EAM classification with an optional configuration file. .DESCRIPTION Entry point that resolves tenant identity from an already-authenticated AzContext and MgGraph context, sets up all required global variables, downloads the latest classification templates from the AzurePrivilegedIAM repository, computes the Control Plane scope, runs Save-EntraOpsPrivilegedEAMJson, and cleans up every intermediate file so that only the PrivilegedEAM JSON output remains. Reuses the configuration loaded by Connect-EntraOps. Otherwise, looks for EntraOpsConfig.json in the current folder, then the EntraOps working folder. Use -ConfigFilePath to select a file explicitly. Command parameters override configuration values. Without a configuration, defaults apply. When an automatically found configuration contains AlternateObjectTierLevelAttributes filter expressions, a warning is written before the run because these expressions execute as PowerShell code. Pass trusted configuration files with -ConfigFilePath. The Azure and Microsoft Graph contexts must belong to the same tenant, with or without a configuration. Prerequisites: - PowerShell 7.4+ with the EntraOps module loaded. - An active Az context (Connect-AzAccount). - An active Microsoft Graph context (Connect-MgGraph). No EntraOpsConfig.json, no Classification folder, and no tenant-specific files are required beforehand. All temporary artefacts are removed after the run, except for the overwrite files (Classification_RoleActionOverwrites.json, Classification_RoleDefinitionOverwrites.json, Classification_ApiPermissionOverwrites.json and their _<Tag> variants) in the tenant-specific Classification folder: these are tenant customization files that can be authored/maintained outside of this cmdlet (e.g. via the ClassificationExplorer "Customize Overwrites" view, or manually) and are always preserved if present. Use -KeepClassificationFiles to keep all classification files for the reporting apps. This cmdlet disconnects from Azure and Microsoft Graph during teardown. Reconnect before generating reports with live Microsoft Graph enrichment, or disable the reporting object lookups. .PARAMETER RbacSystems RBAC systems to classify. Defaults to Azure, EntraID, IdentityGovernance, DeviceManagement, and ResourceApps when no configuration supplies RbacSystems. .PARAMETER PrivilegedObjectClassificationSource Source for Control Plane scope determination passed to Update-EntraOpsClassificationControlPlaneScope. Uses the configuration value when present. Otherwise defaults to "All", combining any existing EntraOps EAM export with live Azure Resource Graph and Exposure Management sources. .PARAMETER ClassificationParameterScope Which RBAC systems receive a parameterised classification file before the run. Filtered automatically to the intersection with $RbacSystems. Default is EntraID, DeviceManagement, and Azure when no configuration supplies ClassificationParameterScope. .PARAMETER ExportFolder Destination folder for PrivilegedEAM JSON output. Defaults to the session output folder when reusing a loaded configuration, otherwise <EntraOpsBaseFolder>/PrivilegedEAM/. .PARAMETER ConfigFilePath Path to an EntraOps configuration file. Takes precedence over the configuration loaded by Connect-EntraOps. .PARAMETER KeepClassificationFiles Keep the downloaded classification templates and the generated tenant-specific classification files instead of removing them after the run, e.g. so New-EntraOpsReportingData can show the classification that was used for the export in the Classification Explorer. Controls cleanup only, independently of the choice to reuse or refresh templates. .PARAMETER ExistingClassificationAction When tenant classification files exist, Prompt (default) asks whether to Reuse local templates (default choice) or Regenerate using refreshed templates. This prompt is independent of -KeepClassificationFiles. Reuse preserves existing templates and downloads only missing templates. Regenerate refreshes templates. Both choices rebuild tenant classifications and Control Plane scope reasoning for the selected scope through Update-EntraOpsClassificationControlPlaneScope, using the preserved tenant overwrite files. Pass Reuse or Regenerate explicitly for unattended runs to avoid prompting. .EXAMPLE Run EntraOps with all defaults from an already-authenticated session. Invoke-EntraOpsPrivilegedEAM .EXAMPLE Run EntraOps and generate the reporting apps from the export and its classification files. Invoke-EntraOpsPrivilegedEAM -KeepClassificationFiles -ExistingClassificationAction Reuse Connect-EntraOps -AuthenticationType UserInteractive -TenantName 'contoso.onmicrosoft.com' New-EntraOpsReportingData Disconnect-EntraOps .EXAMPLE Run for EntraID and Azure only, sourcing Control Plane objects from Azure Resource Graph. Invoke-EntraOpsPrivilegedEAM -RbacSystems @("EntraID","Azure") -PrivilegedObjectClassificationSource "PrivilegedRolesFromAzGraph" .EXAMPLE Save output to a custom folder. Invoke-EntraOpsPrivilegedEAM -ExportFolder "C:\Output\PrivilegedEAM" .EXAMPLE Use the configuration loaded by Connect-EntraOps and rebuild tenant classifications from existing templates. Connect-EntraOps -TenantName 'contoso.onmicrosoft.com' -ConfigFilePath './EntraOpsConfig.json' Invoke-EntraOpsPrivilegedEAM -KeepClassificationFiles -ExistingClassificationAction Reuse #> function Invoke-EntraOpsPrivilegedEAM { [CmdletBinding()] param ( [Parameter(Mandatory = $false)] [ValidateSet("Azure", "EntraID", "IdentityGovernance", "DeviceManagement", "ResourceApps", "Defender")] [Array]$RbacSystems = @("Azure", "EntraID", "IdentityGovernance", "DeviceManagement", "ResourceApps") , [Parameter(Mandatory = $false)] [ValidateSet("All", "EntraOps", "PrivilegedObjectIds", "PrivilegedRolesFromAzGraph", "PrivilegedEdgesFromExposureManagement")] [string[]]$PrivilegedObjectClassificationSource = "All" , [Parameter(Mandatory = $false)] [ValidateSet("EntraID", "DeviceManagement", "Azure", "Defender", "IdentityGovernance", "ResourceApps")] [Array]$ClassificationParameterScope = @("EntraID", "DeviceManagement", "Azure") , [Parameter(Mandatory = $false)] [System.String]$ExportFolder , [Parameter(Mandatory = $false)] [switch]$KeepClassificationFiles , [Parameter(Mandatory = $false)] [System.String]$ConfigFilePath , [Parameter(Mandatory = $false)] [ValidateSet('Prompt', 'Reuse', 'Regenerate')] [string]$ExistingClassificationAction = 'Prompt' ) $ErrorActionPreference = "Stop" $Activity = "Invoke-EntraOpsPrivilegedEAM" # Tracks which paths were created by this cmdlet so only those are cleaned up. $CreatedPaths = [System.Collections.Generic.List[string]]::new() # Names of classification templates this run actually downloaded into a pre-existing Templates folder. # Stays empty unless the download phase completes, so teardown can never delete user-supplied templates. $DownloadedTemplates = [System.Collections.Generic.List[string]]::new() # Helper: create a directory only if it does not already exist; record it for cleanup. function New-EntraOpsDirectory { param ( [string]$Path, [System.Collections.Generic.List[string]]$Track ) if (-not (Test-Path -LiteralPath $Path)) { New-Item -ItemType Directory -Path $Path -Force | Out-Null $Track.Add($Path) } } try { $SessionConfig = Get-Variable -Name EntraOpsConfig -Scope Global -ValueOnly -ErrorAction SilentlyContinue $UseSessionConfig = -not $ConfigFilePath -and $null -ne $SessionConfig $RunConfig = $null $ResolvedConfigPath = $null if ($UseSessionConfig) { $RunConfig = $SessionConfig $ResolvedConfigPath = $__EntraOpsSession['ConfigFilePath'] } else { $ConfigAutoDiscovered = -not $ConfigFilePath if (-not $ConfigFilePath) { $ConfigFilePath = @( Join-Path (Get-Location -PSProvider FileSystem).ProviderPath 'EntraOpsConfig.json' if (-not [string]::IsNullOrWhiteSpace($EntraOpsBaseFolder)) { Join-Path $EntraOpsBaseFolder 'EntraOpsConfig.json' } ) | Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } | Select-Object -First 1 } if ($ConfigFilePath) { $ResolvedConfigPath = (Resolve-Path -LiteralPath $ConfigFilePath -ErrorAction Stop).ProviderPath $RunConfig = Get-Content -LiteralPath $ResolvedConfigPath -Raw | ConvertFrom-Json -Depth 100 -AsHashtable -NoEnumerate if ($RunConfig -isnot [System.Collections.IDictionary]) { throw "Configuration file '$ResolvedConfigPath' must contain a JSON object." } # Warn before any AlternateObjectTierLevelAttributes filter can run: an automatically # discovered file (e.g. in a cloned or downloaded folder) was not explicitly chosen as trusted. $AlternateConfig = $RunConfig.AlternateObjectTierLevelAttributes $HasAlternateFilters = $null -ne $AlternateConfig -and @(foreach ($ObjectType in 'User', 'ServicePrincipal', 'Group') { if (Test-EntraOpsAlternateObjectTierLevelEnabled -ObjectType $ObjectType -AlternateObjectTierLevelAttributes $AlternateConfig) { foreach ($TierName in 'ControlPlane', 'ManagementPlane', 'WorkloadPlane', 'UserAccess') { if (-not [string]::IsNullOrWhiteSpace([string]$AlternateConfig.$ObjectType.$TierName)) { $TierName } } } }).Count -gt 0 if ($ConfigAutoDiscovered -and $HasAlternateFilters) { Write-Warning "Automatically loaded configuration '$ResolvedConfigPath' contains AlternateObjectTierLevelAttributes filter expressions. These expressions are executed as PowerShell code with your current sessions. Only run with configuration files you trust, and pass trusted files explicitly with -ConfigFilePath." } } } $HasConfiguration = $null -ne $RunConfig if ($HasConfiguration) { if (-not $PSBoundParameters.ContainsKey('RbacSystems') -and $RunConfig.RbacSystems) { $RbacSystems = $RunConfig.RbacSystems } if (-not $PSBoundParameters.ContainsKey('PrivilegedObjectClassificationSource') -and $RunConfig.AutomatedControlPlaneScopeUpdate.PrivilegedObjectClassificationSource) { $PrivilegedObjectClassificationSource = $RunConfig.AutomatedControlPlaneScopeUpdate.PrivilegedObjectClassificationSource } if (-not $PSBoundParameters.ContainsKey('ClassificationParameterScope') -and $null -ne $RunConfig.AutomatedControlPlaneScopeUpdate.ClassificationParameterScope) { $ClassificationParameterScope = $RunConfig.AutomatedControlPlaneScopeUpdate.ClassificationParameterScope } } # ── Banner ──────────────────────────────────────────────────────────────── Write-Host "" Write-Host "═══════════════════════════════════════════════════════════════════════════════" -ForegroundColor Cyan $RunMode = if ($HasConfiguration) { 'Configured Run' } else { 'Zero-Config Run' } Write-Host " Invoke-EntraOpsPrivilegedEAM — $RunMode" -ForegroundColor Cyan if ($HasConfiguration) { $ConfigSource = if ($ResolvedConfigPath) { $ResolvedConfigPath } else { 'In-memory session configuration (source file path unavailable)' } Write-Host " Config File : $ConfigSource" -ForegroundColor Yellow } Write-Host " RBAC Systems : $($RbacSystems -join ', ')" -ForegroundColor Cyan Write-Host " CP Source : $($PrivilegedObjectClassificationSource -join ', ')" -ForegroundColor Cyan Write-Host "═══════════════════════════════════════════════════════════════════════════════" -ForegroundColor Cyan Write-Host "" # ── Phase 1: Validate authentication ───────────────────────────────────── Write-Progress -Activity $Activity -Status "Phase 1/7: Validating authentication contexts..." -PercentComplete 2 Write-Host " [1/7] Validating authentication contexts..." -ForegroundColor Cyan $AzContext = Get-AzContext -ErrorAction SilentlyContinue if (-not $AzContext -or [string]::IsNullOrEmpty($AzContext.Tenant.Id)) { throw "No active Azure context found. Establish an Azure context by running Connect-AzAccount or run Connect-EntraOps before invoking this cmdlet." } $MgContext = Get-MgContext -ErrorAction SilentlyContinue if (-not $MgContext -or [string]::IsNullOrEmpty($MgContext.TenantId)) { throw "No active Microsoft Graph context found. Establish an Microsoft Graph context by running Connect-MgGraph or run Connect-EntraOps before invoking this cmdlet." } Write-Host " Azure : $($AzContext.Account.Id) @ tenant $($AzContext.Tenant.Id)" -ForegroundColor Gray Write-Host " Graph : $($MgContext.Account) @ tenant $($MgContext.TenantId)" -ForegroundColor Gray Write-Host " Status : OK" -ForegroundColor Green # ── Phase 2: Resolve tenant identity ───────────────────────────────────── Write-Progress -Activity $Activity -Status "Phase 2/7: Resolving tenant identity..." -PercentComplete 8 Write-Host "" Write-Host " [2/7] Resolving tenant identity..." -ForegroundColor Cyan $TenantId = $AzContext.Tenant.Id if ([string]::IsNullOrEmpty($TenantId)) { $TenantId = $MgContext.TenantId } if ($HasConfiguration -and $RunConfig.TenantId -and $RunConfig.TenantId -ne $TenantId) { throw "Configuration tenant '$($RunConfig.TenantId)' does not match the active Azure tenant '$TenantId'. Connect-EntraOps to the configured tenant before running." } if ($MgContext.TenantId -ne $TenantId) { $ReconnectTarget = if ($HasConfiguration) { 'the configured tenant' } else { 'the same tenant in both contexts' } throw "Microsoft Graph tenant '$($MgContext.TenantId)' does not match the active Azure tenant '$TenantId'. Reconnect to $ReconnectTarget before running." } # Keep the configured tenant name so an alternate verified domain does not select a different # classification folder. Configurations without an ID must match a verified tenant domain. $TenantName = if ($HasConfiguration) { $RunConfig.TenantName } if ($TenantName -and -not $RunConfig.TenantId) { $TenantDetails = Get-AzTenant -TenantId $TenantId -ErrorAction Stop if ($TenantName -notin $TenantDetails.Domains) { throw "Configuration TenantName '$TenantName' does not match the active Azure tenant '$TenantId'." } } if ([string]::IsNullOrEmpty($TenantName)) { try { $TenantDetails = Get-AzTenant -TenantId $TenantId -ErrorAction Stop $TenantName = $TenantDetails.Domains[0] } catch { Write-Warning "Get-AzTenant failed ($($_.Exception.Message)). Falling back to Microsoft Graph organization endpoint..." } } # Fallback: Microsoft Graph /organization if ([string]::IsNullOrEmpty($TenantName)) { try { $OrgResponse = Invoke-MgGraphRequest -Uri "v1.0/organization?`$select=verifiedDomains" -OutputType PSObject -ErrorAction Stop $TenantName = ($OrgResponse.value[0].verifiedDomains | Where-Object { $_.isDefault -eq $true } | Select-Object -ExpandProperty name -First 1) } catch { throw "Unable to determine TenantName from Azure or Microsoft Graph context: $($_.Exception.Message)" } } if ([string]::IsNullOrEmpty($TenantName)) { throw "TenantName could not be resolved. Check that Get-AzTenant or Microsoft Graph /organization is accessible." } Write-Host " TenantId : $TenantId" -ForegroundColor Gray Write-Host " TenantName : $TenantName" -ForegroundColor Gray Write-Host " Status : OK" -ForegroundColor Green # ── Phase 3: Initialise environment ────────────────────────────────────── Write-Progress -Activity $Activity -Status "Phase 3/7: Initialising environment and global variables..." -PercentComplete 14 Write-Host "" Write-Host " [3/7] Initialising environment and global variables..." -ForegroundColor Cyan if ([string]::IsNullOrEmpty($EntraOpsBaseFolder)) { throw "EntraOpsBaseFolder is not set. Ensure the EntraOps module is properly imported." } # Reuse configured session folders; standalone runs use the single-tenant layout. New-Variable -Name TenantIdContext -Value $TenantId -Scope Global -Force New-Variable -Name TenantNameContext -Value $TenantName -Scope Global -Force # Same "<base>/Classification/" form as Connect-EntraOps; Join-Path would emit '\' on Windows. $ClassificationRoot = if ($UseSessionConfig -and -not [string]::IsNullOrWhiteSpace($DefaultFolderClassification)) { $DefaultFolderClassification } else { "$EntraOpsBaseFolder/Classification/" } New-Variable -Name DefaultFolderClassification -Value $ClassificationRoot -Scope Global -Force New-Variable -Name XdrAvdHuntingAccess -Value (($MgContext.Scopes) -contains "ThreatHunting.Read.All") -Scope Global -Force # Default the query mode only when Connect-EntraOps has not set it yet, so a previously # chosen -UseInvokeRestMethodOnly session setting is preserved if (-not $__EntraOpsSession.ContainsKey('UseInvokeRestMethodOnly')) { $__EntraOpsSession['UseInvokeRestMethodOnly'] = [bool]$RunConfig.UseInvokeRestMethodOnly } $ManagingTenantId = if ($HasConfiguration) { [string]$RunConfig.ManagingTenantId } else { '' } $ManagingTenantName = if ($HasConfiguration) { [string]$RunConfig.ManagingTenantName } else { '' } New-Variable -Name ManagingTenantIdContext -Value $ManagingTenantId -Scope Global -Force New-Variable -Name ManagingTenantNameContext -Value $ManagingTenantName -Scope Global -Force $ResolvedExportFolder = if ($PSBoundParameters.ContainsKey('ExportFolder') -and -not [string]::IsNullOrEmpty($ExportFolder)) { $ExportFolder } elseif ($UseSessionConfig -and -not [string]::IsNullOrWhiteSpace($DefaultFolderClassifiedEam)) { $DefaultFolderClassifiedEam } else { "$EntraOpsBaseFolder/PrivilegedEAM/" } New-Variable -Name DefaultFolderClassifiedEam -Value $ResolvedExportFolder -Scope Global -Force # Phase 5 can consume a previous EAM export, but a zero-config first run has no such files yet. # Remember that state before creating the output directory so Phase 6 can bootstrap scope reasoning # once from the fresh export without imposing a second pass on normal subsequent runs. # ALL selected systems must have a prior export: a partial folder (e.g. only EntraID.json, no # Azure/ResourceApps data driving managed-identity tiering) still needs the bootstrap pass. $HadExistingEamData = @($RbacSystems | Where-Object { Test-Path -LiteralPath (Join-Path $ResolvedExportFolder $_ "$_.json") -PathType Leaf }).Count -eq @($RbacSystems).Count # Build a minimal default EntraOpsConfig in memory (no config file required) $DefaultEntraOpsConfig = @{ TenantId = $TenantId TenantName = $TenantName RbacSystems = $RbacSystems AzureRbacClassification = @{ ClassifyConstrainedDelegationAlwaysAsControlPlane = $false UnresolvedRoleDefinitionFallbackTier = "Unclassified" DeletedPrincipalAssignmentHandling = "Filter" } AutomatedClassificationUpdate = @{ Classifications = @( "AadResources", "AadResources.Param", "ApiPermissions", "Azure", "Azure.Param", "Defender", "DeviceManagement", "DeviceManagement.Param", "IdentityGovernance" ) } AutomatedControlPlaneScopeUpdate = @{ PrivilegedObjectClassificationSource = @($PrivilegedObjectClassificationSource) EntraOpsScopes = $RbacSystems AzureHighPrivilegedRoles = @("Owner", "Role Based Access Control Administrator", "User Access Administrator") # "/" covers elevateAccess-scoped assignments; the management group path covers the # tenant root management group (the real highest hierarchy in ARM). AzureHighPrivilegedScopes = @("/", "/providers/microsoft.management/managementgroups/$TenantId") ExposureCriticalityLevel = "<1" } CustomSecurityAttributes = @{ PrivilegedUserAttribute = "privilegedUser" PrivilegedUserPawAttribute = "associatedSecureAdminWorkstation" PrivilegedServicePrincipalAttribute = "privilegedWorkloadIdentity" UserWorkAccountAttribute = "associatedWorkAccount" } } if (-not $HasConfiguration) { $RunConfig = $DefaultEntraOpsConfig } New-Variable -Name EntraOpsConfig -Value $RunConfig -Scope Global -Force if ($ResolvedConfigPath) { $__EntraOpsSession['ConfigFilePath'] = $ResolvedConfigPath } New-Variable -Name EntraOpsIncludeObjectDetails -Value ([bool]$RunConfig.ConsoleOutput.IncludeObjectDetails) -Scope Global -Force # Construct canonical paths used throughout this run $ClassificationTemplates = Join-Path $ClassificationRoot "Templates" $ClassificationTenant = Join-Path $ClassificationRoot $TenantName $ExistingTenantFiles = @(if (Test-Path -LiteralPath $ClassificationTenant -PathType Container) { Get-ChildItem -LiteralPath $ClassificationTenant -Filter 'Classification_*.json' -File }) $ReuseExistingTemplates = $ExistingClassificationAction -eq 'Reuse' if ($ExistingClassificationAction -eq 'Prompt' -and $ExistingTenantFiles.Count -gt 0) { Write-Host " Found $($ExistingTenantFiles.Count) classification file(s) for '$TenantName' in $ClassificationTenant" -ForegroundColor Yellow Write-Host " Reuse keeps local templates. Regenerate downloads the latest templates. Both choices refresh tenant classifications and Control Plane scopes using the preserved tenant overwrite files." -ForegroundColor Yellow do { try { $Answer = Read-Host 'Reuse local classification templates or regenerate using the latest templates? [R] Reuse (default) / [G] Regenerate' } catch { throw "Unable to prompt for existing classification files. Pass -ExistingClassificationAction Reuse or Regenerate for unattended runs. $($_.Exception.Message)" } $Answer = ([string]$Answer).Trim() $ValidAnswer = $Answer -in @('', 'R', 'Reuse', 'G', 'Regenerate') if (-not $ValidAnswer) { Write-Host ' Enter R to reuse or G to regenerate.' -ForegroundColor Yellow } } until ($ValidAnswer) $ReuseExistingTemplates = $Answer -in @('', 'R', 'Reuse') } # Ensure required directories exist; track newly created ones for cleanup New-EntraOpsDirectory -Path $ClassificationRoot -Track $CreatedPaths New-EntraOpsDirectory -Path $ClassificationTemplates -Track $CreatedPaths New-EntraOpsDirectory -Path $ClassificationTenant -Track $CreatedPaths New-EntraOpsDirectory -Path $ResolvedExportFolder -Track $CreatedPaths Write-Host " Base folder : $EntraOpsBaseFolder" -ForegroundColor Gray Write-Host " Classification : $ClassificationRoot" -ForegroundColor Gray Write-Host " EAM output : $ResolvedExportFolder" -ForegroundColor Gray Write-Host " Paths created : $($CreatedPaths.Count)" -ForegroundColor Gray Write-Host " Status : OK" -ForegroundColor Green # ── Phase 4: Download latest classification files ───────────────────────── Write-Progress -Activity $Activity -Status "Phase 4/7: Preparing classification templates..." -PercentComplete 22 Write-Host "" $TemplateStatus = if ($ReuseExistingTemplates) { 'Reusing existing classification templates and downloading missing files' } else { 'Downloading latest classification templates' } Write-Host " [4/7] $TemplateStatus from AzurePrivilegedIAM..." -ForegroundColor Cyan # Snapshot pre-existing templates so teardown can delete only what THIS run downloaded. The previous # "Classification_*.json" wildcard removed templates the user had downloaded or authored earlier. $PreExistingTemplates = @() if (Test-Path -LiteralPath $ClassificationTemplates -ErrorAction SilentlyContinue) { $PreExistingTemplates = @(Get-ChildItem -LiteralPath $ClassificationTemplates -Filter "Classification_*.json" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Name) } $Classifications = if ($RunConfig.AutomatedClassificationUpdate.Classifications) { $RunConfig.AutomatedClassificationUpdate.Classifications } else { $DefaultEntraOpsConfig.AutomatedClassificationUpdate.Classifications } Update-EntraOpsClassificationFiles ` -FolderClassification $ClassificationTemplates ` -Classifications $Classifications ` -SkipExisting:$ReuseExistingTemplates # Record only the templates that did not exist before this run, so teardown removes exactly those. if (Test-Path -LiteralPath $ClassificationTemplates -ErrorAction SilentlyContinue) { Get-ChildItem -LiteralPath $ClassificationTemplates -Filter "Classification_*.json" -ErrorAction SilentlyContinue | Where-Object { $PreExistingTemplates -notcontains $_.Name } | ForEach-Object { $DownloadedTemplates.Add($_.Name) | Out-Null } } Write-Host " Status : Classification templates updated." -ForegroundColor Green # ── Phase 5: Update Control Plane scope ─────────────────────────────────── Write-Progress -Activity $Activity -Status "Phase 5/7: Updating Control Plane scope classification..." -PercentComplete 38 Write-Host "" Write-Host " [5/7] Updating Control Plane scope classification..." -ForegroundColor Cyan # Always refresh tenant classifications and scope reasoning for selected systems, even when # local templates are reused. This also reapplies current tenant role action overwrites. $EffectiveClassificationScope = @($ClassificationParameterScope | Where-Object { $_ -in $RbacSystems }) $ScopeUpdateParameters = @{} if ($HasConfiguration) { foreach ($Name in @('AzureHighPrivilegedRoles', 'AzureHighPrivilegedScopes', 'ExposureCriticalityLevel', 'PrivilegedObjectIds', 'DeviceMgmtPrivilegedTierScope', 'EntraOpsScopes')) { $Value = $RunConfig.AutomatedControlPlaneScopeUpdate.$Name if ($null -ne $Value) { $ScopeUpdateParameters[$Name] = $Value } } } if (-not $ScopeUpdateParameters.ContainsKey('EntraOpsScopes')) { $ScopeUpdateParameters.EntraOpsScopes = $RbacSystems } $ScopeUpdateParameters.PrivilegedObjectClassificationSource = $PrivilegedObjectClassificationSource $ScopeUpdateParameters.ClassificationParameterScope = $EffectiveClassificationScope if ($EffectiveClassificationScope.Count -eq 0) { Write-Host " No classification files require generation for the selected scope." -ForegroundColor Gray } else { Write-Host " Effective scope : $($EffectiveClassificationScope -join ', ')" -ForegroundColor Gray Update-EntraOpsClassificationControlPlaneScope @ScopeUpdateParameters } Write-Host " Status : Control Plane scope updated." -ForegroundColor Green # ── Phase 6: Save EAM JSON ──────────────────────────────────────────────── Write-Progress -Activity $Activity -Status "Phase 6/7: Running EntraOps classification and saving EAM JSON..." -PercentComplete 62 Write-Host "" Write-Host " [6/7] Running EntraOps classification and saving Privileged EAM JSON..." -ForegroundColor Cyan Write-Host " RbacSystems : $($RbacSystems -join ', ')" -ForegroundColor Gray Write-Host " Output : $ResolvedExportFolder" -ForegroundColor Gray Save-EntraOpsPrivilegedEAMJson -ExportFolder $ResolvedExportFolder -RbacSystems $RbacSystems Write-Host " Status : EAM JSON saved." -ForegroundColor Green if (-not $HadExistingEamData -and $EffectiveClassificationScope.Count -gt 0) { Write-Host "" Write-Host " First-run bootstrap: refreshing Control Plane scope from the fresh EAM export..." -ForegroundColor Yellow # Same source as the first pass: narrowing to 'EntraOps' here would drop live-source-only # objects (e.g. XSPM-critical assets without classified roles) from the Control Plane scope. Update-EntraOpsClassificationControlPlaneScope @ScopeUpdateParameters # The Graph cache was populated moments ago by the first pass - only the classification # files changed, so the re-export can reuse it instead of re-collecting everything. Save-EntraOpsPrivilegedEAMJson -ExportFolder $ResolvedExportFolder -RbacSystems $RbacSystems -UseCache $true Write-Host " Status : Scope reasoning and EAM JSON refreshed from first-run output." -ForegroundColor Green } # ── Summary ─────────────────────────────────────────────────────────────── Write-Progress -Activity $Activity -Status "Complete." -PercentComplete 100 $OutputFiles = Get-ChildItem -Path $ResolvedExportFolder -Filter "*.json" -Recurse -ErrorAction SilentlyContinue $OutputCount = if ($OutputFiles) { @($OutputFiles).Count } else { 0 } Write-Host "" Write-Host "═══════════════════════════════════════════════════════════════════════════════" -ForegroundColor Green Write-Host " Invoke-EntraOpsPrivilegedEAM — Complete" -ForegroundColor Green Write-Host " Tenant : $TenantName ($TenantId)" -ForegroundColor White Write-Host " Output : $ResolvedExportFolder" -ForegroundColor White Write-Host " JSON files : $OutputCount" -ForegroundColor White Write-Host "═══════════════════════════════════════════════════════════════════════════════" -ForegroundColor Green Write-Host "" Start-Sleep -Milliseconds 400 Write-Progress -Activity $Activity -Completed } catch { Write-Progress -Activity $Activity -Completed Write-Host "" Write-Host " [ERROR] Invoke-EntraOpsPrivilegedEAM failed at:" -ForegroundColor Red Write-Host " $($_.InvocationInfo.PositionMessage)" -ForegroundColor DarkRed Write-Host " $($_.Exception.Message)" -ForegroundColor Red Write-Host "" throw } finally { # ── Always: remove classification artefacts and disconnect ──────────────── # Null-guards protect against early failures before path variables were set. Write-Host " [Teardown] Removing classification artefacts and disconnecting..." -ForegroundColor Cyan if ($KeepClassificationFiles) { Write-Host " Kept : classification files in $ClassificationRoot (-KeepClassificationFiles)" -ForegroundColor DarkGray } # Remove the generated tenant-specific classification folder, but preserve the overwrite files # (Classification_RoleActionOverwrites.json, Classification_RoleDefinitionOverwrites.json, # Classification_ApiPermissionOverwrites.json and their _<Tag> variants): # these are tenant customization files that are commonly authored/maintained outside of this # cmdlet (e.g. via the ClassificationExplorer "Customize Overwrites" view, or manually) and # must survive a zero-config run, unlike the classification files generated by this run. # Only clean up a tenant folder THIS run created. Without the $CreatedPaths gate (already used for the # Templates and Classification root below) a zero-config run against an existing configured repo # permanently deleted the user's generated Classification_Azure.json / ScopeReasoning_*.json - on the # success path as well as on failure. if (-not $KeepClassificationFiles -and -not [string]::IsNullOrEmpty($ClassificationTenant) -and ($CreatedPaths -contains $ClassificationTenant) -and (Test-Path -LiteralPath $ClassificationTenant -ErrorAction SilentlyContinue)) { $PreservedTenantFiles = @(Get-ChildItem -LiteralPath $ClassificationTenant -File -Force -ErrorAction SilentlyContinue | Where-Object { Test-EntraOpsClassificationOverwriteFileName -Name $_.Name } | Select-Object -ExpandProperty Name) Get-ChildItem -LiteralPath $ClassificationTenant -Force -ErrorAction SilentlyContinue | Where-Object { $PreservedTenantFiles -notcontains $_.Name } | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue $PreservedTenantFileList = if ($PreservedTenantFiles.Count -gt 0) { $PreservedTenantFiles -join ', ' } else { 'no overwrite files' } Write-Host " Cleaned : $ClassificationTenant (preserved $PreservedTenantFileList)" -ForegroundColor DarkGray # Remove the tenant folder itself only if it ends up empty (i.e. no preserved overwrite files remain). if (-not (Get-ChildItem -LiteralPath $ClassificationTenant -Force -ErrorAction SilentlyContinue)) { Remove-Item -LiteralPath $ClassificationTenant -Recurse -Force -ErrorAction SilentlyContinue Write-Host " Removed : empty folder $ClassificationTenant" -ForegroundColor DarkGray } } # Templates folder: remove whole folder if we created it, otherwise only downloaded files if (-not $KeepClassificationFiles -and -not [string]::IsNullOrEmpty($ClassificationTemplates)) { if (($CreatedPaths -contains $ClassificationTemplates) -and (Test-Path -LiteralPath $ClassificationTemplates -ErrorAction SilentlyContinue)) { Remove-Item -LiteralPath $ClassificationTemplates -Recurse -Force -ErrorAction SilentlyContinue Write-Host " Removed : $ClassificationTemplates" -ForegroundColor DarkGray } elseif (Test-Path -LiteralPath $ClassificationTemplates -ErrorAction SilentlyContinue) { # Pre-existing folder: remove only the templates this run downloaded, never ones that were # already present (they may be user-maintained or downloaded by an earlier configured run). # $DownloadedTemplates stays empty if the run failed before the download phase. $RemovedTemplateCount = 0 foreach ($TemplateName in $DownloadedTemplates) { $TemplatePath = Join-Path -Path $ClassificationTemplates -ChildPath $TemplateName if (Test-Path -LiteralPath $TemplatePath -ErrorAction SilentlyContinue) { Remove-Item -LiteralPath $TemplatePath -Force -ErrorAction SilentlyContinue $RemovedTemplateCount++ } } Write-Host " Cleaned : $RemovedTemplateCount downloaded template(s) from $ClassificationTemplates (pre-existing files preserved)" -ForegroundColor DarkGray } } # Remove Classification root if we created it and it is now empty if (-not $KeepClassificationFiles -and -not [string]::IsNullOrEmpty($ClassificationRoot) -and ($CreatedPaths -contains $ClassificationRoot) -and (Test-Path -LiteralPath $ClassificationRoot -ErrorAction SilentlyContinue)) { $Remaining = Get-ChildItem -LiteralPath $ClassificationRoot -Recurse -ErrorAction SilentlyContinue if (-not $Remaining) { Remove-Item -LiteralPath $ClassificationRoot -Recurse -Force -ErrorAction SilentlyContinue Write-Host " Removed : empty root $ClassificationRoot" -ForegroundColor DarkGray } } # Clear EntraOps cache and disconnect from Azure / Microsoft Graph try { Disconnect-EntraOps -ClearEntraOpsCache All -ErrorAction SilentlyContinue Write-Host " Status : Cache cleared and session disconnected." -ForegroundColor Green } catch { Write-Warning "Disconnect-EntraOps encountered an error during teardown: $($_.Exception.Message)" } Write-Host "" } } # SIG # Begin signature block # MIIniAYJKoZIhvcNAQcCoIIneTCCJ3UCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDJyTR7aI/26TIW # oGx6HLL56Jx8RevEycOgO90ZaHEpS6CCIDwwggXJMIIEsaADAgECAhAbtY8lKt8j # AEkoya49fu0nMA0GCSqGSIb3DQEBDAUAMH4xCzAJBgNVBAYTAlBMMSIwIAYDVQQK # ExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2Vy # dGlmaWNhdGlvbiBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBUcnVzdGVkIE5l # dHdvcmsgQ0EwHhcNMjEwNTMxMDY0MzA2WhcNMjkwOTE3MDY0MzA2WjCBgDELMAkG # A1UEBhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMuQS4xJzAl # BgNVBAsTHkNlcnR1bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIGA1UEAxMb # Q2VydHVtIFRydXN0ZWQgTmV0d29yayBDQSAyMIICIjANBgkqhkiG9w0BAQEFAAOC # Ag8AMIICCgKCAgEAvfl4+ObVgAxknYYblmRnPyI6HnUBfe/7XGeMycxca6mR5rlC # 5SBLm9qbe7mZXdmbgEvXhEArJ9PoujC7Pgkap0mV7ytAJMKXx6fumyXvqAoAl4Va # qp3cKcniNQfrcE1K1sGzVrihQTib0fsxf4/gX+GxPw+OFklg1waNGPmqJhCrKtPQ # 0WeNG0a+RzDVLnLRxWPa52N5RH5LYySJhi40PylMUosqp8DikSiJucBb+R3Z5yet # /5oCl8HGUJKbAiy9qbk0WQq/hEr/3/6zn+vZnuCYI+yma3cWKtvMrTscpIfcRnNe # GWJoRVfkkIJCu0LW8GHgwaM9ZqNd9BjuiMmNF0UpmTJ1AjHuKSbIawLmtWJFfzcV # WiNoidQ+3k4nsPBADLxNF8tNorMe0AZa3faTz1d1mfX6hhpneLO/lv403L3nUlbl # s+V1e9dBkQXcXWnjlQ1DufyDljmVe2yAWk8TcsbXfSl6RLpSpCrVQUYJIP4ioLZb # MI28iQzV13D4h1L92u+sUS4Hs07+0AnacO+Y+lbmbdu1V0vc5SwlFcieLnhO+Nqc # noYsylfzGuXIkosagpZ6w7xQEmnYDlpGizrrJvojybawgb5CAKT41v4wLsfSRvbl # jnX98sy50IdbzAYQYLuDNbdeZ95H7JlI8aShFf6tjGKOOVVPORa5sWOd/7cCAwEA # AaOCAT4wggE6MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFLahVDkCw6A/joq8 # +tT4HKbROg79MB8GA1UdIwQYMBaAFAh2zcsH/yT2xc3tu5C84oQ3RnX3MA4GA1Ud # DwEB/wQEAwIBBjAvBgNVHR8EKDAmMCSgIqAghh5odHRwOi8vY3JsLmNlcnR1bS5w # bC9jdG5jYS5jcmwwawYIKwYBBQUHAQEEXzBdMCgGCCsGAQUFBzABhhxodHRwOi8v # c3ViY2Eub2NzcC1jZXJ0dW0uY29tMDEGCCsGAQUFBzAChiVodHRwOi8vcmVwb3Np # dG9yeS5jZXJ0dW0ucGwvY3RuY2EuY2VyMDkGA1UdIAQyMDAwLgYEVR0gADAmMCQG # CCsGAQUFBwIBFhhodHRwOi8vd3d3LmNlcnR1bS5wbC9DUFMwDQYJKoZIhvcNAQEM # BQADggEBAFHCoVgWIhCL/IYx1MIy01z4S6Ivaj5N+KsIHu3V6PrnCA3st8YeDrJ1 # BXqxC/rXdGoABh+kzqrya33YEcARCNQOTWHFOqj6seHjmOriY/1B9ZN9DbxdkjuR # mmW60F9MvkyNaAMQFtXx0ASKhTP5N+dbLiZpQjy6zbzUeulNndrnQ/tjUoCFBMQl # lVXwfqefAcVbKPjgzoZwpic7Ofs4LphTZSJ1Ldf23SIikZbr3WjtP6MZl9M7JYjs # NhI9qX7OAo0FmpKnJ25FspxihjcNpDOO16hO0EoXQ0zF8ads0h5YbBRRfopUofbv # n3l6XYGaFpAP4bvxSgD5+d2+7arszgowggZrMIIEU6ADAgECAhBX5gfP8LabvnxC # XoItEBa/MA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNVBAYTAlBMMSEwHwYDVQQKExhB # c3NlY28gRGF0YSBTeXN0ZW1zIFMuQS4xJDAiBgNVBAMTG0NlcnR1bSBDb2RlIFNp # Z25pbmcgMjAyMSBDQTAeFw0yNjEwMDYwODI4MjNaFw0yNzEwMDYwODI4MjJaMIGQ # MQswCQYDVQQGEwJERTEdMBsGA1UECAwUUmhpbmVsYW5kLVBhbGF0aW5hdGUxEjAQ # BgNVBAcMCUxhaG5zdGVpbjEeMBwGA1UECgwVT3BlbiBTb3VyY2UgRGV2ZWxvcGVy # MS4wLAYDVQQDDCVPcGVuIFNvdXJjZSBEZXZlbG9wZXIgVGhvbWFzIE5hdW5oZWlt # MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAphp2ljSmVsRB6gvkEq7A # DUMENpKZiDFoAfF1ktD1nCwaVyI+bJ0ROjHwAn4iJgS+vlBDPOUIw2GLHkiqUhOg # HSwzg6gj45GNmt6zxsKP0zPlhMB/fb0zmz7/F7e3yfKCXLfhbHuk0Es9gJ+8Wmet # 9StVwwSE/K0MpW06GKA9nPWyE4N/ADwigDIj9rsTKJR4cve9w8G1b3zohpUc/qMr # onab+HVw5r9b3sOBnnaXC6zvrX4fovRz1ztwfWjRjILLoFG6mW/TuK5F2ICD3Rhl # 9R2rJWWWhzTDMsQlzvX+/SaM74q3OFpIVNdhvtmWU9pKUBRAyYMijcDqKEFGSzrc # 8JFmGMpP5TuroWF+N21d3px2eARvW1mV0DCfFAH+Og20SH/gVW6/ltQQEkH/z/Zt # 49ARyJrgESLOQtlMCIYSoKISdkk4ml4VUS1E/PcbzXEYj3a7m6DOxcd3WuY03UT9 # cnk3p0LMN6LeLYAZgVY+HRloZlH+kW2SkOHNuWgtXvZhAgMBAAGjggF4MIIBdDAM # BgNVHRMBAf8EAjAAMD0GA1UdHwQ2MDQwMqAwoC6GLGh0dHA6Ly9jY3NjYTIwMjEu # Y3JsLmNlcnR1bS5wbC9jY3NjYTIwMjEuY3JsMHMGCCsGAQUFBwEBBGcwZTAsBggr # BgEFBQcwAYYgaHR0cDovL2Njc2NhMjAyMS5vY3NwLWNlcnR1bS5jb20wNQYIKwYB # BQUHMAKGKWh0dHA6Ly9yZXBvc2l0b3J5LmNlcnR1bS5wbC9jY3NjYTIwMjEuY2Vy # MB8GA1UdIwQYMBaAFN10XUwA23ufoHTKsW73PMAywHDNMB0GA1UdDgQWBBTBvGyH # MXj1dMbVnxuof9oP4PY7cDBLBgNVHSAERDBCMAgGBmeBDAEEATA2BgsqhGgBhvZ3 # AgUBBDAnMCUGCCsGAQUFBwIBFhlodHRwczovL3d3dy5jZXJ0dW0ucGwvQ1BTMBMG # A1UdJQQMMAoGCCsGAQUFBwMDMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQsF # AAOCAgEAkC165c7/r6y3VeEEpNXas9bQs6ZpKPdJffHkf9xoPi4kWvAIWGc4ocG1 # DJLb18RXiDYShDmT5sb/6I08FeipKi2DNJkuOq6yOTJ3S7+hNFIZCyd6i7WKO8ea # P5I+FAbyJP4+5coZoMg45bUa9JoocaJCm+5n7WiCfN7/EQpClW5Trm4+ZMbp7EMN # F1uLJgGCMnN0SsoUarp7PHlqupMxMC4Uhhe+lAP2ytcSIU81L97/qjrADbLWqICk # 9il5iGM+aZndguq/cmfH9rJj4GRZHWnG6WwnzYD9+KgYlRI6em+/9dq+IRiXe2Bs # thUuIaFW7U2gWw8tBTEYm1UBLHdJx+L1gjJCLs5KT/wy5wU32wZxNDqPUQejUC1x # GvU3cKqmLgRq+yNf3gFVfzqIoggyM3U9wQvnk7nBtCkpjXS2hdx4yxzcmrlWsDHr # z6+6/jeyQySzbYUlFmKnc4n9Mw2soHOajEua5poFARVRS5KgUBuiiQ21YFNYzWBo # qGuECaaAI/C5rGxapMJ3dAOBnHw6NLqBet+ZXBJEccX5y89MLlOQu+jDmJP8HieR # qDLlwfhVZnw4RY8Rus43NZBwhNlVuOUXqocHKmJhKNlekda3qsJj93eQBQOxKJcc # w5O08q9jukpH2+yzFAj+7ll33DuNUgWptogc4Awtbx3glzcEvJQwggaCMIIEaqAD # AgECAhAo8HfBHDa9/l90MkdwJy4DMA0GCSqGSIb3DQEBDAUAMFYxCzAJBgNVBAYT # AlBMMSEwHwYDVQQKExhBc3NlY28gRGF0YSBTeXN0ZW1zIFMuQS4xJDAiBgNVBAMT # G0NlcnR1bSBUaW1lc3RhbXBpbmcgMjAyMSBDQTAeFw0yNjAzMTEwNzM0NTRaFw0z # NjAyMjcwNzM0NTRaMFAxCzAJBgNVBAYTAlBMMSEwHwYDVQQKDBhBc3NlY28gRGF0 # YSBTeXN0ZW1zIFMuQS4xHjAcBgNVBAMMFUNlcnR1bSBUaW1lc3RhbXAgMjAyNjCC # AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALi9zfBw+xI12hi2F9uNQBNO # STkTIi13fuh3DiWybI5392bBfJw6tO7zDs7UqP/rejVf4xFTU3XNOPsuOFkP5SPF # snWwn3VJL+NhNhsuujvYsQtaQnvNo1wk46reTgws1L8OFEdKaxgyqT4zOQx81mLw # NZSqXNU9Rrb9oAJWJF4Mydz+C1ebu0D0+vgM3tqAQ8TU203fqgKCPqIdHloaFqH9 # eyR+8tnpmy2hnnN59ZOtmsuHeRwf5iTOhlhf0d/qqquLOSIGPgGVL1Hj5XBUox2O # E7tQRr+l/xxNbL6OuwBO4Aa2EbuxYg37cpXv5i2/lTL4NASB/duf9W4fjA3Ro+GP # +bC54n2OYiM3Cq8z92ejx8F5HWO2e4/75eC95B9tMw/eq7109GDyW2+2ut5Bn0o8 # MXJTERkyystWbqcnUkomA97qfiDCwvpWuOMdfTOksXe9S+xDxOJaCbiyTivgPulx # cT+tfQTRXBr5UwtPvc+INnafOEilzVHA11tv+pHcylryxBc08cUihf/ttqkZS0bm # x3VxQay8uohaTp0//ECA9vYGW51SKdJMJhXvKyIdBLy8WYNchTt8yRITv9Adxg9J # qGujh+TnP1bW3aQjFtlEl7SzgNeI8OVA/CJAWWUuRT3Au9ToOpHy/flrrknhN4he # Ew7/6ILMv4HSsj0YZ4otAgMBAAGjggFQMIIBTDB1BggrBgEFBQcBAQRpMGcwOwYI # KwYBBQUHMAKGL2h0dHA6Ly9zdWJjYS5yZXBvc2l0b3J5LmNlcnR1bS5wbC9jdHNj # YTIwMjEuY2VyMCgGCCsGAQUFBzABhhxodHRwOi8vc3ViY2Eub2NzcC1jZXJ0dW0u # Y29tMB8GA1UdIwQYMBaAFL5UAi+/QGxzQ86sCSVOnkNEGu7gMAwGA1UdEwEB/wQC # MAAwOQYDVR0fBDIwMDAuoCygKoYoaHR0cDovL3N1YmNhLmNybC5jZXJ0dW0ucGwv # Y3RzY2EyMDIxLmNybDAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAOBgNVHQ8BAf8E # BAMCB4AwIgYDVR0gBBswGTAIBgZngQwBBAIwDQYLKoRoAYb2dwIFAQswHQYDVR0O # BBYEFCM5aiirmhKnpKfyH/lFmr+N/oIDMA0GCSqGSIb3DQEBDAUAA4ICAQBm/pOb # rAveTqYFPCSthxNVwBHTW4TfuBQY2KGCdMlH6ALjVPYf2XARNGMTXScR0JQ2/c2L # xLpoHHhRwxFf3d933DhFS165rYj6iPSQKm41rc9AMpTCjMdmai1aK9o870pOzTzL # RhWFVIxm+5Qf+t4V5z14tPoUsAWdfRKD1Zzvzfe5AHlE9rxOZcGROiULxs3Edf7Z # J8RZI0jzD9juj2oEX2XS7BVKuNCK1W0h1mjapv028yqo8UtZh5/1Ib48Yyp88BDY # Shh5PSGUUuS5tYXVfcduIPBe3PdNTYRYdT5VNxkwLgjMzIcr/6SH2fejFJDjnCxj # vXLuarErJg/IdI+hXdGIBDVOGv1zrkF4ktoQvFZIp5DIAYh0GwyUhc87qqn5UaYT # j2+iyiBDSNh7aGCrkuAnqg7Qh7frhX+QhMGeJ9SpLpnG76qN85hLyjvYR6gRYiLT # j8G4fxjqV400/buPw0rBo9yJZyq/8cGdCOLPOYf9qpefTcrZKeO9pg+UJHx8UABb # aTqizRxm8sUSEycccCCFhIit3AI/3wGuSOADeiA676NcQ7+dsP04YvzvdN7pTZNy # g672WpBDJdg0eItHl4yomvNeIOWh1qs4QqOW6TI8Ocsi4uSDTLorxLUDnBTwKQzz # pxd1YUctEgRh32R0J9FkFkeEruJU94RljFr1uDCCBrkwggShoAMCAQICEQCZo4AK # JlU7ZavcboSms+o5MA0GCSqGSIb3DQEBDAUAMIGAMQswCQYDVQQGEwJQTDEiMCAG # A1UEChMZVW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVt # IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3Rl # ZCBOZXR3b3JrIENBIDIwHhcNMjEwNTE5MDUzMjE4WhcNMzYwNTE4MDUzMjE4WjBW # MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEu # MSQwIgYDVQQDExtDZXJ0dW0gQ29kZSBTaWduaW5nIDIwMjEgQ0EwggIiMA0GCSqG # SIb3DQEBAQUAA4ICDwAwggIKAoICAQCdI88EMCM7wUYs5zNzPmNdenW6vlxNur3r # Lfi+5OZ+U3iZIB+AspO+CC/bj+taJUbMbFP1gQBJUzDUCPx7BNLgid1TyztVLn52 # NKgxxu8gpyTr6EjWyGzKU/gnIu+bHAse1LCitX3CaOE13rbuHbtrxF2tPU8f253Q # gX6eO8yTbGps1Mg+yda3DcTsOYOhSYNCJiL+5wnjZ9weoGRtvFgMHtJg6i671OPX # IciiHO4Lwo2p9xh/tnj+JmCQEn5QU0NxzrOiRna4kjFaA9ZcwSaG7WAxeC/xoZSx # F1oK1UPZtKVt+yrsGKqWONoK6f5EmBOAVEK2y4ATDSkb34UD7JA32f+Rm0wsr5aj # zftDhA5mBipVZDjHpwzv8bTKzCDUSUuUmPo1govD0RwFcTtMXcfJtm1i+P2UNXad # PyYVKRxKQATHN3imsfBiNRdN5kiVVeqP55piqgxOkyt+HkwIA4gbmSc3hD8ke66t # 9MjlcNg73rZZlrLHsAIV/nJ0mmgSjBI/TthoGJDydekOQ2tQD2Dup/+sKQptalDl # ui59SerVSJg8gAeV7N/ia4mrGoiez+SqV3olVfxyLFt3o/OQOnBmjhKUANoKLYlK # mUpKEFI0PfoT8Q1W/y6s9LTI6ekbi0igEbFUIBE8KDUGfIwnisEkBw5KcBZ3XwnH # mfznwlKo8QIDAQABo4IBVTCCAVEwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU # 3XRdTADbe5+gdMqxbvc8wDLAcM0wHwYDVR0jBBgwFoAUtqFUOQLDoD+Oirz61Pgc # ptE6Dv0wDgYDVR0PAQH/BAQDAgEGMBMGA1UdJQQMMAoGCCsGAQUFBwMDMDAGA1Ud # HwQpMCcwJaAjoCGGH2h0dHA6Ly9jcmwuY2VydHVtLnBsL2N0bmNhMi5jcmwwbAYI # KwYBBQUHAQEEYDBeMCgGCCsGAQUFBzABhhxodHRwOi8vc3ViY2Eub2NzcC1jZXJ0 # dW0uY29tMDIGCCsGAQUFBzAChiZodHRwOi8vcmVwb3NpdG9yeS5jZXJ0dW0ucGwv # Y3RuY2EyLmNlcjA5BgNVHSAEMjAwMC4GBFUdIAAwJjAkBggrBgEFBQcCARYYaHR0 # cDovL3d3dy5jZXJ0dW0ucGwvQ1BTMA0GCSqGSIb3DQEBDAUAA4ICAQB1iFgP5Y9Q # KJpTnxDsQ/z0O23JmoZifZdEOEmQvo/79PQg9nLF/GJe6ZiUBEyDBHMtFRK0mXj3 # Qv3gL0sYXe+PPMfwmreJHvgFGWQ7XwnfMh2YIpBrkvJnjwh8gIlNlUl4KENTK5DL # qsYPEtRQCw7R6p4s2EtWyDDr/M58iY2UBEqfUU/ujR9NuPyKk0bEcEi62JGxauFY # zZ/yld13fHaZskIoq2XazjaD0pQkcQiIueL0HKiohS6XgZuUtCKA7S6CHttZEsOb # QJ1j2s0urIDdqF7xaXFVaTHKtAuMfwi0jXtF3JJphrJfc+FFILgCbX/uYBPBlbBI # P4Ht4xxk2GmfzMn7oxPITpigQFJFWuzTMUUgdRHTxaTSKRJ/6Uh7ki/pFjf9sUAS # WgxT69QF9Ki4JF5nBIujxZ2sOU9e1HSCJwOfK07t5nnzbs1LbHuAIGJsRJiQ6HX/ # DW1XFOlXY1rc9HufFhWU+7Uk+hFkJsfzqBz3pRO+5aI6u5abI4Qws4YaeJH7H7M8 # X/YNoaArZbV4Ql+jarKsE0+8XvC4DJB+IVcvC9Ydqahi09mjQse4fxfef0L7E3hh # o2O3bLDM6v60rIRUCi2fJT2/IRU5ohgyTch4GuYWefSBsp5NPJh4QRTP9DC3gc5Q # EKtbrTY0Ka87Web7/zScvLmvQBm8JDFpDjCCBrkwggShoAMCAQICEQDn/2nHOzXO # S5Em2HR8aKWHMA0GCSqGSIb3DQEBDAUAMIGAMQswCQYDVQQGEwJQTDEiMCAGA1UE # ChMZVW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENl # cnRpZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3RlZCBO # ZXR3b3JrIENBIDIwHhcNMjEwNTE5MDUzMjA3WhcNMzYwNTE4MDUzMjA3WjBWMQsw # CQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMSQw # IgYDVQQDExtDZXJ0dW0gVGltZXN0YW1waW5nIDIwMjEgQ0EwggIiMA0GCSqGSIb3 # DQEBAQUAA4ICDwAwggIKAoICAQDpEh8ENe25XXrFppVBvoplf0530W0lddNmjtv4 # YSh/f7eDQKFaIqc7tHj7ox+u8vIsJZlroakUeMS3i3T8aJRC+eQs4FF0GqvkM6+W # ZO8kmzZfxmZaBYmMLs8FktgFYCzywmXeQ1fEExflee2OpbHVk665eXRHjH7MYZIz # Nnjl2m8Hy8ulB9mR8wL/W0v0pjKNT6G0sfrx1kk+3OGosFUb7yWNnVkWKU4qSxLv # 16kJ6oVJ4BSbZ4xMak6JLeB8szrK9vwGDpvGDnKCUMYL3NuviwH1x4gZG0JAXU3x # 2pOAz91JWKJSAmRy/l0s0l5bEYKolg+DMqVhlOANd8Yh5mkQWaMEvBRE/kAGzIqg # WhwzN2OsKIVtO8mf5sPWSrvyplSABAYa13rMYnzwfg08nljZHghquCJYCa/xHK9a # cev9UD7Y+usr15d7mrszzxhF1JOr1Mpup2chNSBlyOObhlSO16rwrffVrg/SzaKf # SndS5swRhr8bnDqNJY9TNyEYvBYpgF95K7p0g4LguR4A++Z1nFIHWVY5v0fNVZmg # zxD9uVo/gta3onGOQj3JCxgYx0KrCXu4yc9QiVwTFLWbNdHFSjBCt5/8Q9pLuRhV # ocdCunhcHudMS1CGQ/Rn0+7P+fzMgWdRKfEOh/hjLrnQ8BdJiYrZNxvIOhM2aa3z # EDHNwwIDAQABo4IBVTCCAVEwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUvlQC # L79AbHNDzqwJJU6eQ0Qa7uAwHwYDVR0jBBgwFoAUtqFUOQLDoD+Oirz61PgcptE6 # Dv0wDgYDVR0PAQH/BAQDAgEGMBMGA1UdJQQMMAoGCCsGAQUFBwMIMDAGA1UdHwQp # MCcwJaAjoCGGH2h0dHA6Ly9jcmwuY2VydHVtLnBsL2N0bmNhMi5jcmwwbAYIKwYB # BQUHAQEEYDBeMCgGCCsGAQUFBzABhhxodHRwOi8vc3ViY2Eub2NzcC1jZXJ0dW0u # Y29tMDIGCCsGAQUFBzAChiZodHRwOi8vcmVwb3NpdG9yeS5jZXJ0dW0ucGwvY3Ru # Y2EyLmNlcjA5BgNVHSAEMjAwMC4GBFUdIAAwJjAkBggrBgEFBQcCARYYaHR0cDov # L3d3dy5jZXJ0dW0ucGwvQ1BTMA0GCSqGSIb3DQEBDAUAA4ICAQC4k1l3yUwV/ZQH # CKCneqAs8EGTnwEUJLdDpokN/dMhKjK0rR5qX8nIIHzxpQR3TAw2IRw1Uxsr2Pli # G3bCFqSdQTUbfaTq6V3vBzEebDru9QFjqlKnxCF2h1jhLNFFplbPJiW+JSnJTh1f # KEqEdKdxgl9rVTvlxfEJ7exOn25MGbd/wGPwuSmMxRJVO0wnqgS7kmoJjNF9zqee # hFSDDP8ZVkWg4EZ2tIS0M3uZmByRr+1Lkwjjt8AtW83mVnZTyTsOb+FNfwJY7DS4 # FmWhkRbgcHRetreoTirPOr/ozyDKhT8MTSTf6Lttg6s6T/u08mDWw6HK04ZRDfQ9 # sb77QV8mKgO44WGP31vXnVKoWVJpFBjPvjL8/Zck/5wXX2iqjOaLStFOR/IQki+E # hn4zlcgVm22ZVCBPF+l8nAwUUShCtKuSU7GmZLKCmmxQMkSiWILTm8EtVD6AxnJh # oq8EnhjEEyUoflkeRF2WhFiVQOmWTwZRr44IxWGkNJC6tTorW5rl2Zl+2e9JLPYf # 3pStAPMDoPKIjVXd6NW2+fZrNUBeDo2eOa5Fn7Brs/HLQff5Xgris5MeUbdVgDrF # 8uxO6cLPvZPo63j62SsNg55pTWk9fUIF9iPoRbb4QurjoY/woI1RAOKtYtTic6aA # Jq3u83RIPpGXBSJKwx4KJAOZnCDCtTGCBqIwggaeAgEBMGowVjELMAkGA1UEBhMC # UEwxITAfBgNVBAoTGEFzc2VjbyBEYXRhIFN5c3RlbXMgUy5BLjEkMCIGA1UEAxMb # Q2VydHVtIENvZGUgU2lnbmluZyAyMDIxIENBAhBX5gfP8LabvnxCXoItEBa/MA0G # CWCGSAFlAwQCAQUAoIGEMBgGCisGAQQBgjcCAQwxCjAIoAKAAKECgAAwGQYJKoZI # hvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcC # ARUwLwYJKoZIhvcNAQkEMSIEIGqVwqzjcdoQgd+9FPwtzhWh7uFyaikASmxoyFo6 # F9bZMA0GCSqGSIb3DQEBAQUABIIBgJhNP8VWfl7ga8o5m+h0vR+FQfMz16TEyt8H # TgyD8L4hobG478tPJERHv+tEqM1Z+1mPzVxC/vzgf0LLlOAKVcVPZp2IDl7dsDMf # FFpPm0sByOs5nIUTQNkI/LPMCbRMDMUoNut/5+qefZ44A3WLtXjlhZDpj71SjLwp # QK9IeQMRDwYq0ph7ezi61WWmh+4ynqNVCXH/P7eov/NgRAwnph2LX5hy6oCc2BOT # xtW17orfDD/j6HoZ93ZYxBtFCvoIAEe9BdFXal258FGFazjk5nD3rmZuXelRjHa5 # kZwjLxpqBasVYVroqMNiAXgPVz6K9cMEJFLwnabIpKT1wauRmqoJcEMkC1QqPRqL # HIUdiGfsPzDJs+IypgQ+P7t3OyIx9S3ypqetMLqdwlw5ftZcR8BwQnavqfQLK1z4 # f5ITdbm6p0h64/84656UyUAlzQoJ/vY/zE/UU2kD00RNI/csrPcVRN57lSG3ZFR0 # VhWSlo49nclh1yE2pHVL1lmR4nvRjaGCBAIwggP+BgkqhkiG9w0BCQYxggPvMIID # 6wIBATBqMFYxCzAJBgNVBAYTAlBMMSEwHwYDVQQKExhBc3NlY28gRGF0YSBTeXN0 # ZW1zIFMuQS4xJDAiBgNVBAMTG0NlcnR1bSBUaW1lc3RhbXBpbmcgMjAyMSBDQQIQ # KPB3wRw2vf5fdDJHcCcuAzANBglghkgBZQMEAgIFAKCCAVYwGgYJKoZIhvcNAQkD # MQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNjEwMjIxMTM5NTRaMDcG # CyqGSIb3DQEJEAIvMSgwJjAkMCIEIIW+kOEK0kONfMkotq9IsJqyCBd87PiwEmxY # 05EFJcQ8MD8GCSqGSIb3DQEJBDEyBDCx7cMbCembciGiBSNXDV98HRb+NKRbReAI # FaBox0vxAW/JujmMop0lnLMTcYjZjn0wgZ8GCyqGSIb3DQEJEAIMMYGPMIGMMIGJ # MIGGBBRXFGhBDKha80JO+RZKUTYQ9NONmDBuMFqkWDBWMQswCQYDVQQGEwJQTDEh # MB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMSQwIgYDVQQDExtDZXJ0 # dW0gVGltZXN0YW1waW5nIDIwMjEgQ0ECECjwd8EcNr3+X3QyR3AnLgMwDQYJKoZI # hvcNAQEBBQAEggIAZDhrTWNhlndvWGQiBFAxFsLsoBv6rxD40iVVd48A1dVj+8wI # NJjWAlKGxR7PLrPY19STK0XOLKfqUgRTHegeQoV3AXV+edxi7c/Sb/P23rHcIg6I # b/btjJ3Yv6NIK4x3eMa8U5GQsVqupwJwckfPudanIB6bXopzsVmz5Dvy2eE5GgVG # 3lOXCiirGJXx20T7jO/I9R6zWdMSMi2IcS8GShhXrg6FX080iCgd6ZHlusFhF/J2 # x1cZubBI8XeiCSo6RbmFyNq2m5y7PcfRnUOVTlZuSO1+2t7hI49hO8HzLe6d+qm8 # 5/NHJJ+seS2J2nsxAp+DtKLsy+bhw1YO3aE08BgyzbmFB5h2K5sSg4csK/+XGVTL # ppNoUZb7KnHvE2an+zr97OgEzzg/NtHCBwiINL+ZSyzjnaLCUHdvnyDSDbHB/WKe # JGEHiSSfHtCUzdXE3fDAnBQ6E8i43klnaPYMY2BTW4LMmjnLxvvTxWW4qNHYUW2u # GmP6UjM6a8JRj6W89vCGwkNLN/q56xsGN96p9ChMqKYHyMYTVQJmwZ3/X4jKg7zF # 8ceAfucZ7MdDfFuwt7Fa/9+ypMMCKh8Ltm07yiWYNbE/gQpqtbOyY6IdBT5z7/UQ # 17Tc0ToCnXTV+F8xSWFDKasJA5Ku4W+LfbnDfiOHAtFooR1tfva7T445VwY= # SIG # End signature block |