Public/Reportings/New-EntraOpsPrivilegedAssetsData.ps1
|
<# .SYNOPSIS Generate (refresh) the embedded dataset for the EntraOps Privileged Assets static web app. .DESCRIPTION Builds one inventory record per privileged object (users, groups, service principals and applications incl. all object sub types) from the Privileged EAM export and merges the object across all RBAC systems. The inventory focuses on object details that are not visible in the role-assignment centric views: owners, sponsors, owned objects and devices, identity parent, associated work account and PAW device, administrative units, restricted management and the object tier compared with the classification of its role assignments. Related object IDs are resolved against the export first. IDs outside the export are resolved to display name and object type through Microsoft Graph (directoryObjects/getByIds) when -ResolveRelatedObjectIds is enabled; unresolved IDs stay visible as raw IDs. The dataset also embeds the Custom Security Attribute field names (for the PowerShell script generator of the app) and the current Object Classification File entries (so the app can edit the file). Generates data/privileged-assets-data.js exposing `window.ENTRAOPS_PRIVILEGED_ASSETS_DATA`. .PARAMETER RepoRoot Path to the EntraOps repository root that contains the PrivilegedEAM export folder and EntraOpsConfig.json. Defaults to the repository this module lives in. .PARAMETER ImportPath Folder with the Privileged EAM export. Defaults to <RepoRoot>/PrivilegedEAM. .PARAMETER AppRoot Path to the PrivilegedAssets app folder. Defaults to Reports/PrivilegedAssets. .PARAMETER OutFile Output file. Defaults to <AppRoot>/data/privileged-assets-data.js. .PARAMETER ConfigFilePath EntraOpsConfig.json used for the Custom Security Attribute and Object Classification File settings. Defaults to <RepoRoot>/EntraOpsConfig.json. .PARAMETER ResolveRelatedObjectIds Resolve related object IDs outside the export through Microsoft Graph. Defaults to the PrivilegedAssets.ResolveRelatedObjectIds config setting, or $true. .PARAMETER MaesterTestResultPath Location of the Maester test results used to enrich the inventory with failed Maester tests. Accepts a Maester result JSON file, the file path without the .json extension, or a folder; for a folder the latest TestResults-<yyyy-MM-dd-HHmmss>.json is used. Failed tests and the affected objects (AffectedObjects, or AssetInventory in older Maester versions) are matched to the privileged objects by object id, app id of app registrations or user principal name. Optional: without the parameter, or if no result file is found, the Maester integration is skipped (a warning is shown for an explicit path that does not exist). .PARAMETER PassThru Emit the generated payload object to the pipeline. .EXAMPLE New-EntraOpsPrivilegedAssetsData Regenerates the Privileged Assets dataset from the PrivilegedEAM folder of this repository. .EXAMPLE New-EntraOpsPrivilegedAssetsData -ResolveRelatedObjectIds $false -WhatIf Shows what would be generated without Microsoft Graph lookups and without changing any files. #> function New-EntraOpsPrivilegedAssetsData { [CmdletBinding(SupportsShouldProcess = $true)] param ( [Parameter(Mandatory = $false)] [System.String]$RepoRoot, [Parameter(Mandatory = $false)] [System.String]$ImportPath, [Parameter(Mandatory = $false)] [System.String]$AppRoot, [Parameter(Mandatory = $false)] [System.String]$OutFile, [Parameter(Mandatory = $false)] [System.String]$ConfigFilePath, [Parameter(Mandatory = $false)] [System.Boolean]$ResolveRelatedObjectIds = $true, [Parameter(Mandatory = $false)] [AllowEmptyString()] [System.String]$MaesterTestResultPath = '', [Parameter(Mandatory = $false)] [switch]$PassThru ) $ModuleRoot = $MyInvocation.MyCommand.Module.ModuleBase if ([string]::IsNullOrWhiteSpace($ModuleRoot) -and -not [string]::IsNullOrWhiteSpace($PSScriptRoot)) { $ModuleRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) } if ([string]::IsNullOrWhiteSpace($ModuleRoot)) { throw "Unable to resolve the EntraOps module location. Import the module with 'Import-Module <path-to-EntraOps> -Force' and try again." } $RepositoryRoot = if (-not [string]::IsNullOrWhiteSpace($Global:EntraOpsBaseFolder)) { $Global:EntraOpsBaseFolder } else { Split-Path -Parent $ModuleRoot } if ([string]::IsNullOrWhiteSpace($RepoRoot)) { $RepoRoot = $RepositoryRoot } if ([string]::IsNullOrWhiteSpace($AppRoot)) { $AppRoot = Join-Path $RepoRoot 'Reports/PrivilegedAssets' } if (-not (Test-Path -LiteralPath $AppRoot -PathType Container)) { throw "Privileged Assets app folder not found: $AppRoot. Run Initialize-EntraOpsWorkspace -Content Reports -Force or Install-EntraOpsReportingFolder -Force to install or repair the reporting apps in the working folder (use -Ref to match the module version). Generated report data is preserved. If you passed -AppRoot, check that path instead." } if ([string]::IsNullOrWhiteSpace($ImportPath)) { $ImportPath = Join-Path $RepoRoot 'PrivilegedEAM' } if ([string]::IsNullOrWhiteSpace($OutFile)) { $OutFile = Join-Path $AppRoot 'data/privileged-assets-data.js' } if ([string]::IsNullOrWhiteSpace($ConfigFilePath)) { $ConfigFilePath = Join-Path $RepoRoot 'EntraOpsConfig.json' } if (-not (Test-Path -LiteralPath $ImportPath)) { throw "Privileged EAM import path not found: $ImportPath. Run Save-EntraOpsPrivilegedEAMJson first or pass -ImportPath." } $Config = $null if (Test-Path -LiteralPath $ConfigFilePath -PathType Leaf) { try { $Config = Get-Content -LiteralPath $ConfigFilePath -Raw | ConvertFrom-Json } catch { Write-Warning "Failed to read ${ConfigFilePath}: $($_.Exception.Message). Default Custom Security Attribute names are used and the Object Classification File is not loaded." } } if (-not $PSBoundParameters.ContainsKey('ResolveRelatedObjectIds') -and $null -ne $Config.PrivilegedAssets -and $null -ne $Config.PrivilegedAssets.ResolveRelatedObjectIds) { $ResolveRelatedObjectIds = [bool]$Config.PrivilegedAssets.ResolveRelatedObjectIds } function Get-PropValue { param($Object, [string]$Name) if ($null -eq $Object) { return $null } $Property = $Object.PSObject.Properties[$Name] if ($null -ne $Property) { return $Property.Value } return $null } function ConvertTo-StringArray { param($Value) if ($null -eq $Value) { return @() } return @(@($Value) | Where-Object { $null -ne $_ -and "$_" -ne '' } | ForEach-Object { "$_" }) } function Get-ConfigValue { param($Section, [string]$Name, [string]$Default) $Value = "$(Get-PropValue $Section $Name)" if ([string]::IsNullOrWhiteSpace($Value)) { return $Default } return $Value } $TierRank = @{ ControlPlane = 0; ManagementPlane = 1; WorkloadPlane = 2; UserAccess = 3; Unclassified = 4 } $GuidPattern = '^[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}$' $RelationshipProperties = [ordered]@{ owners = 'Owners' sponsors = 'Sponsors' ownedObjects = 'OwnedObjects' ownedDevices = 'OwnedDevices' associatedWorkAccount = 'AssociatedWorkAccount' associatedPawDevice = 'AssociatedPawDevice' } $Files = @(Get-ChildItem -Path $ImportPath -Directory -ErrorAction SilentlyContinue | ForEach-Object { Get-ChildItem -Path $_.FullName -Filter '*.json' -File } | Sort-Object FullName) if ($Files.Count -eq 0) { Write-Warning "No Privileged EAM JSON files found under $ImportPath (expected <RbacSystem>/<RbacSystem>.json). Generating an empty dataset." } $ObjectsById = [ordered]@{} foreach ($File in $Files) { $RoleSystem = Split-Path -Leaf (Split-Path -Parent $File.FullName) try { $Data = [System.IO.File]::ReadAllText($File.FullName) | ConvertFrom-Json } catch { throw "Failed to read or parse Privileged EAM export file '$($File.FullName)' (RBAC system '$RoleSystem'): $($_.Exception.Message)" } foreach ($Source in @($Data)) { if ($null -eq $Source) { continue } $ObjectId = "$(Get-PropValue $Source 'ObjectId')".ToLowerInvariant() if ($ObjectId -notmatch $GuidPattern) { continue } if (-not $ObjectsById.Contains($ObjectId)) { $TierName = "$(Get-PropValue $Source 'ObjectAdminTierLevelName')" if ([string]::IsNullOrWhiteSpace($TierName)) { $TierName = 'Unclassified' } $DisplayName = "$(Get-PropValue $Source 'ObjectDisplayName')" $ObjectsById[$ObjectId] = [ordered]@{ objectId = $ObjectId objectType = "$(Get-PropValue $Source 'ObjectType')".ToLowerInvariant() objectSubType = "$(Get-PropValue $Source 'ObjectSubType')" displayName = $(if ($DisplayName) { $DisplayName } else { $ObjectId }) userPrincipalName = "$(Get-PropValue $Source 'ObjectUserPrincipalName')" objectTenantId = "$(Get-PropValue $Source 'ObjectTenantId')" tierLevel = "$(Get-PropValue $Source 'ObjectAdminTierLevel')" tierName = $TierName onPremSynchronized = [bool](Get-PropValue $Source 'OnPremSynchronized') restrictedManagementByRAG = [bool](Get-PropValue $Source 'RestrictedManagementByRAG') restrictedManagementByAadRole = [bool](Get-PropValue $Source 'RestrictedManagementByAadRole') restrictedManagementByRMAU = [bool](Get-PropValue $Source 'RestrictedManagementByRMAU') identityParent = "$(Get-PropValue $Source 'IdentityParent')" administrativeUnits = [System.Collections.Generic.List[object]]::new() roleSystems = [System.Collections.Generic.List[string]]::new() assignments = [System.Collections.Generic.List[object]]::new() } foreach ($Key in $RelationshipProperties.Keys) { $ObjectsById[$ObjectId][$Key] = [System.Collections.Generic.List[string]]::new() } } $Object = $ObjectsById[$ObjectId] if (-not $Object.roleSystems.Contains($RoleSystem)) { $Object.roleSystems.Add($RoleSystem) } # RBAC systems can carry sparser records for the same object: OR flags and fill empty fields. foreach ($Flag in @('OnPremSynchronized', 'RestrictedManagementByRAG', 'RestrictedManagementByAadRole', 'RestrictedManagementByRMAU')) { $Key = $Flag.Substring(0, 1).ToLowerInvariant() + $Flag.Substring(1) $Object[$Key] = $Object[$Key] -or [bool](Get-PropValue $Source $Flag) } $FieldMap = [ordered]@{ objectSubType = 'ObjectSubType'; userPrincipalName = 'ObjectUserPrincipalName'; objectTenantId = 'ObjectTenantId'; identityParent = 'IdentityParent'; tierLevel = 'ObjectAdminTierLevel' } foreach ($Key in $FieldMap.Keys) { if ([string]::IsNullOrWhiteSpace($Object[$Key])) { $Object[$Key] = "$(Get-PropValue $Source $FieldMap[$Key])" } } if ($Object.displayName -eq $ObjectId -and "$(Get-PropValue $Source 'ObjectDisplayName')") { $Object.displayName = "$(Get-PropValue $Source 'ObjectDisplayName')" } $SourceTierName = "$(Get-PropValue $Source 'ObjectAdminTierLevelName')" if ($Object.tierName -eq 'Unclassified' -and $TierRank.ContainsKey($SourceTierName)) { $Object.tierName = $SourceTierName $Object.tierLevel = "$(Get-PropValue $Source 'ObjectAdminTierLevel')" } foreach ($Key in $RelationshipProperties.Keys) { foreach ($RelatedId in (ConvertTo-StringArray (Get-PropValue $Source $RelationshipProperties[$Key]))) { $NormalizedId = $RelatedId.ToLowerInvariant() if (-not $Object[$Key].Contains($NormalizedId)) { $Object[$Key].Add($NormalizedId) } } } foreach ($AdministrativeUnit in @(Get-PropValue $Source 'AssignedAdministrativeUnits')) { $AdministrativeUnitId = "$(Get-PropValue $AdministrativeUnit 'id')".ToLowerInvariant() if (-not $AdministrativeUnitId -or @($Object.administrativeUnits | Where-Object { $_.id -eq $AdministrativeUnitId }).Count -gt 0) { continue } $Object.administrativeUnits.Add([ordered]@{ id = $AdministrativeUnitId; displayName = "$(Get-PropValue $AdministrativeUnit 'displayName')" }) } foreach ($RoleAssignment in @(Get-PropValue $Source 'RoleAssignments')) { if ($null -eq $RoleAssignment) { continue } $ClassificationTiers = @(@(Get-PropValue $RoleAssignment 'Classification') | ForEach-Object { "$(Get-PropValue $_ 'AdminTierLevelName')" } | Where-Object { $TierRank.ContainsKey($_) }) $AssignmentTier = @($ClassificationTiers | Sort-Object { $TierRank[$_] } | Select-Object -First 1)[0] if (-not $AssignmentTier) { $AssignmentTier = 'Unclassified' } $InstanceId = "$(Get-PropValue $RoleAssignment 'RoleAssignmentInstanceId')" if (-not $InstanceId) { $InstanceId = Get-EntraOpsRoleAssignmentInstanceId -RoleSystem $RoleSystem -RoleAssignment $RoleAssignment } $Object.assignments.Add([ordered]@{ id = $InstanceId roleSystem = $RoleSystem roleDefinitionName = "$(Get-PropValue $RoleAssignment 'RoleDefinitionName')" roleDefinitionId = "$(Get-PropValue $RoleAssignment 'RoleDefinitionId')" scopeName = "$(Get-PropValue $RoleAssignment 'RoleAssignmentScopeName')" scopeId = "$(Get-PropValue $RoleAssignment 'RoleAssignmentScopeId')" assignmentType = "$(Get-PropValue $RoleAssignment 'RoleAssignmentType')" assignmentSubType = "$(Get-PropValue $RoleAssignment 'RoleAssignmentSubType')" pimAssignmentType = "$(Get-PropValue $RoleAssignment 'PIMAssignmentType')" transitiveBy = "$(Get-PropValue $RoleAssignment 'TransitiveByObjectDisplayName')" tierName = $AssignmentTier services = @(@(Get-PropValue $RoleAssignment 'Classification') | ForEach-Object { "$(Get-PropValue $_ 'Service')" } | Where-Object { $_ } | Select-Object -Unique) }) } } } $TenantCounts = @{} foreach ($Object in $ObjectsById.Values) { if (-not $Object.objectTenantId -or ($Object.objectType -eq 'user' -and $Object.objectSubType -eq 'Guest')) { continue } $TenantCounts[$Object.objectTenantId] = [int]$TenantCounts[$Object.objectTenantId] + 1 } $HomeTenantId = ($TenantCounts.GetEnumerator() | Sort-Object -Property Value -Descending | Select-Object -First 1).Key $ObjectsByAppId = @{} foreach ($Object in $ObjectsById.Values) { if ($Object.objectType -in @('serviceprincipal', 'application') -and $Object.userPrincipalName -match $GuidPattern) { $ObjectsByAppId[$Object.userPrincipalName.ToLowerInvariant()] = $Object.objectId } } $Objects = [System.Collections.Generic.List[object]]::new() $RelatedObjects = [ordered]@{} $UnresolvedIds = [System.Collections.Generic.HashSet[string]]::new() foreach ($Object in $ObjectsById.Values) { $Summary = [ordered]@{ total = $Object.assignments.Count; eligible = 0; active = 0; byTier = [ordered]@{}; bySystem = [ordered]@{}; highestTierName = 'Unclassified' } foreach ($TierName in @('ControlPlane', 'ManagementPlane', 'WorkloadPlane', 'UserAccess', 'Unclassified')) { $Summary.byTier[$TierName] = 0 } foreach ($Assignment in $Object.assignments) { $Summary.byTier[$Assignment.tierName]++ $Summary.bySystem[$Assignment.roleSystem] = [int]$Summary.bySystem[$Assignment.roleSystem] + 1 if ($Assignment.pimAssignmentType -eq 'Eligible') { $Summary.eligible++ } else { $Summary.active++ } if ($TierRank[$Assignment.tierName] -lt $TierRank[$Summary.highestTierName]) { $Summary.highestTierName = $Assignment.tierName } } $Object['assignmentSummary'] = $Summary $Object['isForeign'] = [bool]($HomeTenantId -and $Object.objectTenantId -and $Object.objectTenantId -ne $HomeTenantId) $Object['restrictedManagement'] = if ($Object.objectType -in @('serviceprincipal', 'application')) { 'Not available' } elseif ($Object.objectType -eq 'group' -and $Object.objectSubType -eq 'Role-assignable' -and $Object.restrictedManagementByRMAU) { 'Conflict' } elseif ($Object.restrictedManagementByAadRole -or $Object.restrictedManagementByRAG -or $Object.restrictedManagementByRMAU) { 'Applied' } else { 'Not applied' } $RelatedIds = @($RelationshipProperties.Keys | ForEach-Object { $Object[$_] }) if ($Object.identityParent) { $ParentKey = $Object.identityParent.ToLowerInvariant() $Object.identityParent = $ParentKey if ($ObjectsByAppId.ContainsKey($ParentKey) -and -not $RelatedObjects.Contains($ParentKey)) { $ParentObject = $ObjectsById[$ObjectsByAppId[$ParentKey]] $RelatedObjects[$ParentKey] = [ordered]@{ objectId = $ParentObject.objectId; displayName = $ParentObject.displayName; objectType = $ParentObject.objectType; objectSubType = $ParentObject.objectSubType; tierName = $ParentObject.tierName; source = 'PrivilegedEAM' } } else { $RelatedIds += $ParentKey } } foreach ($RelatedId in $RelatedIds) { if ($RelatedObjects.Contains($RelatedId)) { continue } if ($ObjectsById.Contains($RelatedId)) { $RelatedObject = $ObjectsById[$RelatedId] $RelatedObjects[$RelatedId] = [ordered]@{ objectId = $RelatedId; displayName = $RelatedObject.displayName; objectType = $RelatedObject.objectType; objectSubType = $RelatedObject.objectSubType; tierName = $RelatedObject.tierName; source = 'PrivilegedEAM' } } elseif ($RelatedId -match $GuidPattern) { $UnresolvedIds.Add($RelatedId) | Out-Null } } $Objects.Add($Object) } if ($ResolveRelatedObjectIds -and $UnresolvedIds.Count -gt 0) { Write-Verbose "Resolving $($UnresolvedIds.Count) related object id(s) outside the Privileged EAM export via Microsoft Graph..." $IdsToResolve = @($UnresolvedIds | Sort-Object) $GraphBatchSize = 999 # directoryObjects/getByIds accepts up to 1000 IDs per request. for ($Index = 0; $Index -lt $IdsToResolve.Count; $Index += $GraphBatchSize) { $IdBatch = @($IdsToResolve[$Index..([Math]::Min($Index + $GraphBatchSize - 1, $IdsToResolve.Count - 1))]) $Body = @{ ids = $IdBatch } | ConvertTo-Json try { foreach ($Resolved in @(Invoke-EntraOpsMsGraphQuery -Method POST -Uri '/v1.0/directoryObjects/getByIds' -Body $Body -OutputType PSObject)) { $ResolvedId = "$(Get-PropValue $Resolved 'id')".ToLowerInvariant() if (-not $ResolvedId) { continue } $ResolvedType = "$(Get-PropValue $Resolved '@odata.type')" -replace '^#microsoft\.graph\.', '' $RelatedObjects[$ResolvedId] = [ordered]@{ objectId = $ResolvedId displayName = "$(Get-PropValue $Resolved 'displayName')" objectType = $ResolvedType.ToLowerInvariant() userPrincipalName = "$(Get-PropValue $Resolved 'userPrincipalName')" operatingSystem = "$(Get-PropValue $Resolved 'operatingSystem')" isCompliant = Get-PropValue $Resolved 'isCompliant' isManaged = Get-PropValue $Resolved 'isManaged' trustType = "$(Get-PropValue $Resolved 'trustType')" tierName = '' source = 'Microsoft Graph' } } } catch { Write-Warning "Failed to resolve $($IdBatch.Count) related object id(s) via Microsoft Graph: $($_.Exception.Message)" } } } # Maester failed tests: TestResults JSON (tests) joined with its AffectedObjects (assets) per privileged object. $MaesterPayload = $null if (-not [string]::IsNullOrWhiteSpace($MaesterTestResultPath)) { $MaesterFile = $null if (Test-Path -LiteralPath $MaesterTestResultPath -PathType Container) { $MaesterFile = Get-ChildItem -LiteralPath $MaesterTestResultPath -File -Filter 'TestResults-*.json' | Where-Object { $_.Name -match '^TestResults-\d{4}-\d{2}-\d{2}-\d{6}\.json$' } | Sort-Object -Property Name -Descending | Select-Object -First 1 } elseif (Test-Path -LiteralPath $MaesterTestResultPath -PathType Leaf) { $MaesterFile = Get-Item -LiteralPath $MaesterTestResultPath } elseif (Test-Path -LiteralPath "$MaesterTestResultPath.json" -PathType Leaf) { $MaesterFile = Get-Item -LiteralPath "$MaesterTestResultPath.json" } if ($null -eq $MaesterFile) { $MaesterMessage = "No Maester test result found at '$MaesterTestResultPath'. Maester findings are not added to the Privileged Assets dataset." if ($PSBoundParameters.ContainsKey('MaesterTestResultPath')) { Write-Warning $MaesterMessage } else { Write-Verbose $MaesterMessage } } else { Write-Host "Maester test result: $($MaesterFile.FullName)" try { $MaesterResult = [System.IO.File]::ReadAllText($MaesterFile.FullName) | ConvertFrom-Json } catch { throw "Failed to read or parse Maester test result '$($MaesterFile.FullName)': $($_.Exception.Message)" } function Limit-MaesterText { param($Value, [int]$Length = 12000) $Text = "$Value" if ($Text.Length -le $Length) { return $Text } return $Text.Substring(0, $Length) + "`n`n... (truncated, see the Maester report for the full result)" } $MaesterAssets = @(@(Get-PropValue $MaesterResult 'AffectedObjects') + @(Get-PropValue $MaesterResult 'AssetInventory') | Where-Object { $null -ne $_ }) $FailedTests = @{} foreach ($MaesterTest in @(Get-PropValue $MaesterResult 'Tests')) { if ($null -ne $MaesterTest -and "$(Get-PropValue $MaesterTest 'Result')" -eq 'Failed') { $FailedTests["$(Get-PropValue $MaesterTest 'Id')"] = $MaesterTest } } $ObjectIdsByAppId = @{} $ObjectIdsByUpn = @{} foreach ($Object in $ObjectsById.Values) { if ($Object.objectType -eq 'application' -and $Object.userPrincipalName -match $GuidPattern) { $ObjectIdsByAppId[$Object.userPrincipalName.ToLowerInvariant()] = $Object.objectId } if ($Object.objectType -eq 'user' -and $Object.userPrincipalName -like '*@*') { $ObjectIdsByUpn[$Object.userPrincipalName.ToLowerInvariant()] = $Object.objectId } } $UsedTestIds = [System.Collections.Generic.HashSet[string]]::new() $MatchedObjectIds = [System.Collections.Generic.HashSet[string]]::new() foreach ($Asset in $MaesterAssets) { $FailedIds = @(ConvertTo-StringArray (Get-PropValue $Asset 'Tests') | Where-Object { $FailedTests.ContainsKey($_) }) if ($FailedIds.Count -eq 0) { continue } $AssetId = "$(Get-PropValue $Asset 'Id')".ToLowerInvariant() $AssetUpn = "$(Get-PropValue $Asset 'UserPrincipalName')".ToLowerInvariant() $TargetId = $null if ($ObjectsById.Contains($AssetId)) { $TargetId = $AssetId } elseif ("$(Get-PropValue $Asset 'Type')" -eq 'AppRegistration' -and $ObjectIdsByAppId.ContainsKey($AssetId)) { $TargetId = $ObjectIdsByAppId[$AssetId] } elseif ($AssetUpn -and $ObjectIdsByUpn.ContainsKey($AssetUpn)) { $TargetId = $ObjectIdsByUpn[$AssetUpn] } if (-not $TargetId) { continue } if (-not $ObjectsById[$TargetId].Contains('maesterAssets')) { $ObjectsById[$TargetId]['maesterAssets'] = [System.Collections.Generic.List[object]]::new() } $ObjectsById[$TargetId]['maesterAssets'].Add([ordered]@{ system = "$(Get-PropValue $Asset 'System')" anchorKind = "$(Get-PropValue $Asset 'AnchorKind')" type = "$(Get-PropValue $Asset 'Type')" id = "$(Get-PropValue $Asset 'Id')" uniqueId = "$(Get-PropValue $Asset 'UniqueId')" displayName = "$(Get-PropValue $Asset 'DisplayName')" userPrincipalName = "$(Get-PropValue $Asset 'UserPrincipalName')" portalLink = "$(Get-PropValue $Asset 'PortalLink')" sources = @(ConvertTo-StringArray (Get-PropValue $Asset 'Sources')) tests = @($FailedIds) }) $MatchedObjectIds.Add($TargetId) | Out-Null foreach ($FailedId in $FailedIds) { $UsedTestIds.Add($FailedId) | Out-Null } } $MaesterTests = [ordered]@{} foreach ($TestId in @($UsedTestIds | Sort-Object)) { $MaesterTest = $FailedTests[$TestId] $Detail = Get-PropValue $MaesterTest 'ResultDetail' $Severity = "$(Get-PropValue $MaesterTest 'Severity')" if ([string]::IsNullOrWhiteSpace($Severity)) { $Severity = "$(Get-PropValue $Detail 'Severity')" } $MaesterTests[$TestId] = [ordered]@{ index = Get-PropValue $MaesterTest 'Index' id = $TestId title = "$(Get-PropValue $MaesterTest 'Title')" name = "$(Get-PropValue $MaesterTest 'Name')" severity = $Severity result = "$(Get-PropValue $MaesterTest 'Result')" tags = @(ConvertTo-StringArray (Get-PropValue $MaesterTest 'Tag')) helpUrl = "$(Get-PropValue $MaesterTest 'HelpUrl')" block = "$(Get-PropValue $MaesterTest 'Block')" service = "$(Get-PropValue $Detail 'Service')" description = Limit-MaesterText (Get-PropValue $Detail 'TestDescription') testResult = Limit-MaesterText (Get-PropValue $Detail 'TestResult') } } $ExecutedAt = $null try { $ExecutedAt = ([datetime](Get-PropValue $MaesterResult 'ExecutedAt')).ToUniversalTime().ToString('o') } catch { Write-Verbose "Maester ExecutedAt could not be parsed: $($_.Exception.Message)" } $MaesterTenantId = "$(Get-PropValue $MaesterResult 'TenantId')" if ($HomeTenantId -and $MaesterTenantId -and $MaesterTenantId -ne $HomeTenantId) { Write-Warning "The Maester test result belongs to tenant $MaesterTenantId, but the Privileged EAM export belongs to $HomeTenantId." } $MaesterPayload = [ordered]@{ sourceFile = $MaesterFile.Name executedAt = $ExecutedAt tenantId = $MaesterTenantId tenantName = "$(Get-PropValue $MaesterResult 'TenantName')" totals = [ordered]@{ total = [int](Get-PropValue $MaesterResult 'TotalCount') failed = [int](Get-PropValue $MaesterResult 'FailedCount') passed = [int](Get-PropValue $MaesterResult 'PassedCount') skipped = [int](Get-PropValue $MaesterResult 'SkippedCount') } affectedObjectCount = $MaesterAssets.Count matchedObjectCount = $MatchedObjectIds.Count tests = $MaesterTests } Write-Verbose "Maester: $($MaesterTests.Count) failed test(s) on $($MatchedObjectIds.Count) privileged object(s) from $($MaesterFile.FullName)" } } $CsaConfig = Get-PropValue $Config 'CustomSecurityAttributes' $AlternateConfig = Get-PropValue $Config 'AlternateObjectTierLevelAttributes' $FileConfig = Get-PropValue $Config 'ObjectClassificationFile' $FilePath = Get-ConfigValue $FileConfig 'FilePath' './Classification/ObjectClassification.json' $FileEntries = @() $FileError = '' try { $FileEntries = @((Import-EntraOpsObjectClassificationFile -FilePath $FilePath -RootFolder $RepoRoot -WarningAction SilentlyContinue).Values | Sort-Object ObjectId | ForEach-Object { [ordered]@{ objectId = $_.ObjectId objectType = $_.ObjectType objectDisplayName = $_.ObjectDisplayName adminTierLevelName = $_.AdminTierLevelName justification = $_.Justification } }) } catch { $FileError = $_.Exception.Message Write-Warning "Object Classification File could not be loaded for Privileged Assets: $FileError" } $SortedRelatedObjects = [ordered]@{} foreach ($Key in @($RelatedObjects.Keys | Sort-Object)) { $SortedRelatedObjects[$Key] = $RelatedObjects[$Key] } $RepoRootFull = (Resolve-Path -LiteralPath $RepoRoot).Path $Payload = [ordered]@{ tenantName = Get-EntraOpsReportingTenantName -RepoRoot $RepoRoot generatedAt = (Get-Date).ToUniversalTime().ToString('o') generatedFrom = @($Files | ForEach-Object { if ($_.FullName.StartsWith($RepoRootFull)) { $_.FullName.Substring($RepoRootFull.Length).TrimStart('\', '/') -replace '\\', '/' } else { $_.Name } }) homeTenantId = "$HomeTenantId" maester = $MaesterPayload classificationSettings = [ordered]@{ customSecurityAttributes = [ordered]@{ enabledFor = [ordered]@{ user = Test-EntraOpsCustomSecurityAttributeClassificationEnabled -ObjectType User -Enabled (Get-PropValue $CsaConfig 'Enabled') -ObjectClassificationFile $FileConfig -AlternateObjectTierLevelAttributes $AlternateConfig servicePrincipal = Test-EntraOpsCustomSecurityAttributeClassificationEnabled -ObjectType ServicePrincipal -Enabled (Get-PropValue $CsaConfig 'Enabled') -ObjectClassificationFile $FileConfig -AlternateObjectTierLevelAttributes $AlternateConfig application = Test-EntraOpsCustomSecurityAttributeClassificationEnabled -ObjectType Application -Enabled (Get-PropValue $CsaConfig 'Enabled') -ObjectClassificationFile $FileConfig -AlternateObjectTierLevelAttributes $AlternateConfig } userAttributeSet = Get-ConfigValue $CsaConfig 'PrivilegedUserAttribute' 'privilegedUser' userTierLevelAttribute = Get-ConfigValue $CsaConfig 'PrivilegedUserAdminTierLevelAttribute' 'adminTierLevel' userTierNameAttribute = Get-ConfigValue $CsaConfig 'PrivilegedUserAdminTierLevelNameAttribute' 'adminTierLevelName' servicePrincipalAttributeSet = Get-ConfigValue $CsaConfig 'PrivilegedServicePrincipalAttribute' 'privilegedWorkloadIdentity' servicePrincipalTierLevelAttribute = Get-ConfigValue $CsaConfig 'PrivilegedServicePrincipalAdminTierLevelAttribute' 'adminTierLevel' servicePrincipalTierNameAttribute = Get-ConfigValue $CsaConfig 'PrivilegedServicePrincipalAdminTierLevelNameAttribute' 'adminTierLevelName' } alternateObjectTierLevelAttributes = [ordered]@{ user = Test-EntraOpsAlternateObjectTierLevelEnabled -ObjectType User -AlternateObjectTierLevelAttributes $AlternateConfig servicePrincipal = Test-EntraOpsAlternateObjectTierLevelEnabled -ObjectType ServicePrincipal -AlternateObjectTierLevelAttributes $AlternateConfig group = Test-EntraOpsAlternateObjectTierLevelEnabled -ObjectType Group -AlternateObjectTierLevelAttributes $AlternateConfig } objectClassificationFile = [ordered]@{ enabled = [bool](Get-PropValue $FileConfig 'Enabled') filePath = $FilePath error = $FileError entries = @($FileEntries) } } objects = @($Objects | Sort-Object { $_.displayName }) relatedObjects = $SortedRelatedObjects } $Json = $Payload | ConvertTo-Json -Depth 10 -Compress $Content = "// Auto-generated by New-EntraOpsPrivilegedAssetsData - do not edit by hand.`n" + "window.ENTRAOPS_PRIVILEGED_ASSETS_DATA = $Json;`n" if ($PSCmdlet.ShouldProcess($OutFile, 'Write Privileged Assets dataset')) { Save-EntraOpsReportDataFile -Content $Content -LiteralPath $OutFile Write-Host "Privileged objects: $($Objects.Count)" Write-Host "Related objects: $($SortedRelatedObjects.Count) resolved, $([Math]::Max(0, $UnresolvedIds.Count - @($SortedRelatedObjects.Values | Where-Object { $_.source -eq 'Microsoft Graph' }).Count)) unresolved" if ($null -ne $MaesterPayload) { Write-Host "Maester findings: $($MaesterPayload.tests.Count) failed test(s) on $($MaesterPayload.matchedObjectCount) privileged object(s) ($($MaesterPayload.sourceFile))" } Write-Host "Wrote $OutFile" } if ($PassThru) { $Payload } } # SIG # Begin signature block # MIIniAYJKoZIhvcNAQcCoIIneTCCJ3UCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCC6Hu6AfKg1g4SI # M41kmiByWV4QdAmhSH8fMCRg949PNqCCIDwwggXJMIIEsaADAgECAhAbtY8lKt8j # 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 # ARUwLwYJKoZIhvcNAQkEMSIEIEceGkOHGIc4B18S0vGsnRZHspap0kxFhU0Qk0aT # BYcTMA0GCSqGSIb3DQEBAQUABIIBgD4toHNpOzBMP7BiYp4orAsSr2QmYLQd7umj # gpXAMhglb7Vwd6Rx9Cn2Qrh1tAmyJBTEJ225ManX6z/GtkfSpJI4XpmTq1iIYmOZ # QJKCRTzQ1TAdXYE/W6v6BmO5E8ZakzbdNO9D35l+wa93PDhIvZeXhXkWCvfaG8X2 # qXcgwnpHA0n+dq5NsNRYH/LItcibJYefYQEhJonTvR4d7mPZP0B7YzEfrq4zcFid # W6YlMh8OzaGo//ts75SriWYEfO1X9y+mSPgnVYiKUOY2n85Vtd8AiFUaDPvU5a3I # 1fWBKQ8zE8SmXLLIV5efb1af/uRT/VvBJ+VcDoocF73AeMehDaAHP4enfDdgxwox # eXCVFINQpexX+ouXFWqucaL8Jkn9e1GxVuu/IDmc4qOYcSyUxweklZo2kyVBI3LD # Uy6vW43m53o+jetkiem63ZmuvH00Jz496q6zhsmM8AAxpsMtnP4rpwKFF82bUFfF # 7ztD50foIVQL8bnzgiugDWm5BDRR56GCBAIwggP+BgkqhkiG9w0BCQYxggPvMIID # 6wIBATBqMFYxCzAJBgNVBAYTAlBMMSEwHwYDVQQKExhBc3NlY28gRGF0YSBTeXN0 # ZW1zIFMuQS4xJDAiBgNVBAMTG0NlcnR1bSBUaW1lc3RhbXBpbmcgMjAyMSBDQQIQ # KPB3wRw2vf5fdDJHcCcuAzANBglghkgBZQMEAgIFAKCCAVYwGgYJKoZIhvcNAQkD # MQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNjEwMjIxMTQwMTlaMDcG # CyqGSIb3DQEJEAIvMSgwJjAkMCIEIIW+kOEK0kONfMkotq9IsJqyCBd87PiwEmxY # 05EFJcQ8MD8GCSqGSIb3DQEJBDEyBDAd+73T5Z4Dq8ahX9q4gRMz07dxSv/oENEE # ZfLGXALN/NcB0PJ6jTzKPmVXp5qbdyEwgZ8GCyqGSIb3DQEJEAIMMYGPMIGMMIGJ # MIGGBBRXFGhBDKha80JO+RZKUTYQ9NONmDBuMFqkWDBWMQswCQYDVQQGEwJQTDEh # MB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMSQwIgYDVQQDExtDZXJ0 # dW0gVGltZXN0YW1waW5nIDIwMjEgQ0ECECjwd8EcNr3+X3QyR3AnLgMwDQYJKoZI # hvcNAQEBBQAEggIAZTjCUNDsW0Kt8s4HKuZG/uodAkSlevVs9ZpfRY/bW6/2eouw # D38XthC958dSyok7JK8VvCmq8ZHw2mCbR0Wm095hVCGFBk9WJ91IeA+tzLGRLaPT # Z2C93TfNg2+1mXCiCyJI7uuE5fmYF5zpvGeYhRCuLIyya2FvOEL9NRQmikja/qgB # aBlQg1MVfNvyvxWdAOSCrckV6ujNi/vFr5CUKmtQzOINhJK6GCOf8gHNpwKBrgVu # HVOhxfwuf9ZnQI2DlwcFSKtxJwz4mefQcEGIXQ0PPyaxQTCsrhyez0o3d4JmqJV9 # +g+vL1jMQteGgQe8CV0GJIIbMGFBA3Aj0PtIAbpqYfJPKQEC+kLEm5xP0WpHgG2P # kak4IGyz9GIO2eSx/B1G061o/YNiKVEm/DWAAPQAPLnsVBuTthHJPd30ekxRToBY # J3Wn/htWIo9JoOoNjHHcC17DOLJkCCNt8o/UsqoSJkijSkjbLHMxyR9UXzUO0Mck # 7Di5U0peQzeopZj2GZ+inO3ZcocyR9YvYca6Pea5akO+zp335Z1i60gVDzw3uxgU # uzy6onQDQZdKKkqa54AS2bd18KodED2THWXTDTl4veFDmL9x1zl378iqJzc9D+3J # K609bASgVnlM1PuQGU7reWgBlkEOhs5LJ9ktzirfppFgmIZT3R7X+SlS0BU= # SIG # End signature block |