Public/Configuration/New-EntraOpsConfigFile.ps1

<#
.SYNOPSIS
    Create config file to store required or optional parameters for executing EntraOps without end-user prompts.

.DESCRIPTION
    Creates a configuration file used by other cmdlets, for example to create the required app registration and grant permissions.
    Connect-EntraOps can also import the file so the same settings are available to interactive and automated runs.

.PARAMETER TenantName
    Name of the tenant (e.g. contoso.onmicrosoft.com) to create the configuration for. The tenant ID is resolved
    from the name, and the initial domain of the tenant is written as TenantName to the config file.

.PARAMETER ManagingTenantName
    Name of the governing (managing) tenant of a Tenant Governance relationship, used to classify privileged access
    across tenants. Written as ManagingTenantName. Default is empty.

.PARAMETER ManagingTenantId
    Tenant ID of the governing (managing) tenant of a Tenant Governance relationship. Written as ManagingTenantId.
    Default is empty.

.PARAMETER AuthenticationType
    Defines the authentication method used to request tokens for Microsoft Graph, Azure Resource Manager, and other APIs. Default is FederatedCredentials.

.PARAMETER UseInvokeRestMethodOnly
    Defines if all Invoke-EntraOps*Query cmdlets should use Invoke-RestMethod instead of the Microsoft Graph SDK (Invoke-MgGraphRequest). Default is false.
    In this mode the Microsoft Graph SDK is neither required nor installed; tokens are provided via Connect-EntraOps -MsGraphAccessToken or acquired with Get-AzAccessToken from the Az PowerShell context.
    Written as top-level setting "UseInvokeRestMethodOnly" to the config file and read by Connect-EntraOps; an explicit Connect-EntraOps -UseInvokeRestMethodOnly parameter wins over the config file.
    Note: REST-only parallel object resolution pre-warms a Graph token from the Az PowerShell context. If that token cannot be prepared, resolution falls back to sequential processing.

.PARAMETER IncludeObjectDetails
    Include display names, UPNs, and detailed object-specific errors in console output. Object IDs are always
    shown. Written as ConsoleOutput.IncludeObjectDetails and disabled by default for automation-log privacy.

.PARAMETER DevOpsPlatform
    Defines the platform where the EntraOps repository is hosted. Supported values are GitHub, AzureDevOps and None.
    Default is GitHub.

.PARAMETER AzureDevOpsServiceConnection
    Name of the Azure Resource Manager service connection used by the Azure DevOps pipelines. Written as
    AzureDevOpsServiceConnection and applied to the pipeline definitions by Update-EntraOpsAzureDevOpsSchedules.
    Default is EntraOps-ServiceConnection. Not used with other DevOps platforms.

.PARAMETER ConfigFilePath
    Location of the config file which will be created. Default is ./EntraOpsConfig.json.

.PARAMETER IngestToLogAnalytics
    Defines if the classification data should be ingested to Log Analytics or Microsoft Sentinel Workspace. Default is false.

.PARAMETER IngestToWatchLists
    Defines if the classification data should be ingested to Microsoft Sentinel WatchLists. Default is false.

.PARAMETER ApplyAutomatedControlPlaneScopeUpdate
    Defines whether role-assignment scopes containing Control Plane assets should be updated before privileges are analyzed. Default is false.
    You are able to customize in the config file the data source ("EntraOps", "PrivilegedRolesFromAzGraph", "PrivilegedEdgesFromExposureManagement") and which criticality level or role definition/scope level should be considered.

.PARAMETER ClassificationParameterScope
    Array of RBAC systems whose classification parameter files are (re)generated by Update-EntraOpsClassificationControlPlaneScope, written to the config file as AutomatedControlPlaneScopeUpdate.ClassificationParameterScope. Default is all supported RBAC systems ("Azure", "EntraID", "IdentityGovernance", "ResourceApps", "DeviceManagement", "Defender").
    This is distinct from EntraOpsScopes (RbacSystems parameter/AutomatedControlPlaneScopeUpdate.EntraOpsScopes in the config file), which selects which RBAC systems' already-classified EAM export data is read as input to discover privileged objects. ClassificationParameterScope instead selects which RBAC systems' classification template/parameter files are written as output during that same run.

.PARAMETER ApplyAutomatedClassificationUpdate
    Defines whether classification templates should be updated from "https://github.com/Cloud-Architekt/AzurePrivilegedIAM" before privileges are analyzed. Default is false.
    You are able to define which classification files should be updated in the config file later.

.PARAMETER FailOnContradictoryTierPair
    Defines if generated artifact validation fails when an object's tier level contradicts its tier name
    (e.g. 0 with ManagementPlane). Written as GeneratedArtifactValidation.FailOnContradictoryTierPair.
    Default is false (reported as warning).

.PARAMETER FailOnPrivilegedAssignmentWithoutClassification
    Defines if generated artifact validation fails when an assignment marked as privileged has no classification.
    Written as GeneratedArtifactValidation.FailOnPrivilegedAssignmentWithoutClassification. Default is false
    (reported as warning).

.PARAMETER ApplyAutomatedEntraOpsUpdate
    Defines whether the Update-EntraOps workflow and its weekly schedule are enabled. Default is false.
    Workflow definitions are excluded from the default automated target set because GITHUB_TOKEN cannot
    publish changes below .github/workflows. Update them interactively with Update-EntraOps, or explicitly
    add the workflow target after configuring the documented EntraOps update publisher GitHub App.
    When enabled with the default PullRequest publication mode, the repository setting
    "Allow GitHub Actions to create and approve pull requests" (Settings > Actions > General >
    Workflow permissions) must be turned on, otherwise pull request creation fails.

.PARAMETER UpdateRepository
    Repository name below the Cloud-Architekt organization that serves EntraOps updates. Default is
    "EntraOps" (public release channel, no credentials). "EntraOps-Insiders" is the private preview
    channel and requires the EntraOpsUpdatePat repository secret. Both are declared in the local
    EntraOpsUpdateContract.json; another repository is accepted only when it publishes a matching contract.

.PARAMETER ValidationFrequency
    Controls candidate validation in the Update-EntraOps workflow. OnChange (default) validates new
    source commits and validation-setting changes, Always validates every workflow run, and Never
    applies changed candidates without executing their validation suite.

.PARAMETER RunBrowserTests
    Defines whether update-candidate validation includes the Playwright documentation and report tests.
    Default is true. This setting has no effect when ValidationFrequency is Never.

.PARAMETER UpdatePublicationMode
    Controls how the Update-EntraOps workflow publishes an applied candidate. PullRequest (default)
    creates or updates a review branch and pull request; DirectPush commits to the current branch
    without review. Applies to the GitHub workflow and the Azure DevOps update pipeline.

.PARAMETER ApplyConditionalAccessTargetGroups
    Defines if security groups per tier for targeting Conditional Access policies should be created and updated
    (AutomatedConditionalAccessTargetGroups) as part of the push pipeline. Default is false.

.PARAMETER ApplyAdministrativeUnitAssignments
    Defines if Administrative Units per tier should be created and updated with privileged users and groups
    (AutomatedAdministrativeUnitManagement) as part of the push pipeline. Default is false.

.PARAMETER ApplyRmauAssignmentsForUnprotectedObjects
    Defines if privileged users and groups without restricted management should be added to a Restricted
    Management Administrative Unit (AutomatedRmauAssignmentsForUnprotectedObjects) as part of the push pipeline.
    Default is false.

.PARAMETER ApplyPrivilegedElmCatalogProtection
    Defines if Entitlement Management catalogs with Control Plane scope should be updated to privileged catalogs (Update-EntraOpsPrivilegedUnprotectedElmCatalog) as part of the push pipeline. Default is false.
    Caution: Enabling this setting requires the elevated Microsoft Graph permission "EntitlementManagement.ReadWrite.All" which will be assigned by New-EntraOpsWorkloadIdentity only if this setting is enabled.

.PARAMETER IncludeUnprotectedDevices
    Defines if devices owned by or associated with privileged users should be added to the same tier's unprotected-objects Restricted Management Administrative Unit when they are not already protected by another RMAU. Default is false.

.PARAMETER RemovalSafetyThreshold
    Safety brake for every automated cmdlet that removes protections from classified EAM data (Conditional Access target groups, Administrative Units, Restricted Management AUs including devices, and Entitlement Management catalog privilege levels). The value is the fraction (0..1) of a target's currently protected members/objects that a single run may remove. Each target plans all eligible removals before mutation; when the plan exceeds Ceiling(current count x threshold), no removals are applied to that target, a SafetyAbort is recorded, and the command fails after its summary. Non-destructive additions or protection operations may still proceed. Example with the default 0.5: a CA group with 20 current members permits at most 10 removals; a plan for 11 aborts before group removals begin. Setting the value to 1.0 permits removal of the complete current protected set and therefore disables threshold-based SafetyAbort protection for automated runs. After reviewing the reported plan, run the affected cmdlet manually with -ForceRemovalBeyondSafetyThreshold to apply an intentional oversized reconciliation. The force switch is deliberately not configurable because bypassing the brake must remain an explicit invocation-time decision. Written to all four mutation sections of the config file. Default is 0.5.

.PARAMETER ClassifyConstrainedDelegationAlwaysAsControlPlane
    Defines if Azure RBAC role assignments with an authorization/role assignment ABAC condition (constrained delegation) should always be classified as Control Plane instead of being dynamically downgraded based on the scope and the role definitions allowed by the condition. Default is false (dynamic downgrade).

.PARAMETER UnresolvedRoleDefinitionFallbackTier
    Tier assigned to Azure RBAC role assignments whose role definition could not be resolved from Azure Resource Graph (e.g. ARG replication lag for a new custom role, deleted role definition or a transient ARG failure). Allowed values: "Unclassified" (default, consistent with the Unclassified fallback of the other RBAC systems), "ManagementPlane" or "ControlPlane" (stricter fail-closed tiers) or "None" (skip classification).

.PARAMETER DeletedPrincipalAssignmentHandling
    Controls Azure RBAC assignments whose principal is confirmed deleted by HTTP 404 responses from both Microsoft Graph identity endpoints. "Filter" (default) removes it from Azure EAM output; "Keep" preserves the unresolved assignment. Other resolution failures are never filtered.

.PARAMETER ApplyAutomatedReportingGeneration
    Defines if the EntraOps Reporting static web apps (Classification Explorer, Tier Breach Analyzer, EAM Dashboard and Access Path Map) should be regenerated automatically by the Push-EntraOpsPrivilegedReporting workflow. Default is false.
    The generated report is uploaded as a downloadable workflow artifact; it is not committed back to the repository.
    The trigger for this workflow is controlled by the config file's `WorkflowTrigger` section: by default (`PushReportingAfterPullWorkflowTrigger` set to `true`) it runs right after every successful Pull-EntraOpsPrivilegedEAM run. It can additionally/alternatively be run on a recurring schedule by enabling `PushReportingScheduledTrigger` and customizing the cron expression in `PushReportingScheduledCron` (default is weekly, every Monday at 09:00 UTC). Disable both settings to only allow manual (`workflow_dispatch`) runs.

.PARAMETER PublishReportsAsRelease
    Publishes generated reports as GitHub Releases in addition to the 30-day workflow artifact. Disabled by default because each release contains a full tenant-data snapshot.

.PARAMETER RunGeneratedReportTests
    Installs npm dependencies and runs browser tests against generated tenant reports in reporting automation.
    Disabled by default because third-party dependencies can access tenant report data and the network.

.PARAMETER UploadReportTestDiagnostics
    Uploads Playwright traces of failed report tests as a 1-day artifact (EntraOps-Reporting-TestDiagnostics).
    Disabled by default because the traces contain tenant report data. Used only with RunGeneratedReportTests.

.PARAMETER ReportingReleasesToKeep
    Number of `reporting-*` GitHub Releases to retain when release publishing is enabled. Default is 10.
    Accepts 1 to 1000, so a generated configuration always prunes. Turning pruning off is deliberately not
    offered here because every retained release holds a full tenant-data snapshot; set
    `AutomatedReportingGeneration.ReportingReleasesToKeep` to an empty value in the Configuration Wizard or
    directly in the configuration file if unbounded retention is really intended.

.PARAMETER EnablePrivilegeHistory
    Defines if the Privilege History report data is generated by New-EntraOpsReportingData.
    Written as PrivilegeHistory.EnablePrivilegeHistory. Default is true.

.PARAMETER PrivilegeHistoryTimeRangeInDays
    Number of days the Privilege History samples back into the git history. Written as
    PrivilegeHistory.TimeRangeInDays. Default is empty (full history).

.PARAMETER PrivilegeHistorySnapshotInterval
    ISO 8601 duration of the Privilege History snapshot interval. Written as PrivilegeHistory.SnapshotInterval.
    Default is "P2W" (every 2 weeks).

.PARAMETER AccessPathMapResolveObjectIdsOutsidePrivilegedEAM
    Defines if New-EntraOpsAccessPathMapData should resolve edge endpoints that reference an object id outside the Privileged EAM export (e.g. a standard, non-privileged work account or a group/application owned by a privileged principal - referenced via OwnedObjects, Owners, AssociatedPawDevice or AssociatedWorkAccount) via Microsoft Graph to their display name and object type, instead of showing them as an anonymous "unresolved" placeholder node in Access Path Map. Default is true.
    Requires an active Microsoft Graph connection with at least directory read permissions (User.Read.All, Group.Read.All, Application.Read.All or Directory.Read.All) when generating the Access Path Map data.

.PARAMETER EamDashboardResolveLinkedIdentityObjectIds
    Defines if New-EntraOpsPrivilegedEamDashboardData should resolve linked identity object IDs outside the Privileged EAM export to display names through Microsoft Graph. Default is true.
    Requires an active Microsoft Graph connection with directory read permissions when generating EAM Dashboard data.

.PARAMETER PrivilegedAssetsResolveRelatedObjectIds
    Defines if New-EntraOpsPrivilegedAssetsData should resolve related object IDs outside the Privileged EAM export (owners, sponsors, owned objects, owned devices, identity parent, associated work account and PAW device) to display name and object type through Microsoft Graph. Default is true.
    Requires an active Microsoft Graph connection with directory read permissions when generating Privileged Assets data.

.PARAMETER ClassificationExplorerGenerateChangeHistory
    Defines if New-EntraOpsClassificationExplorerData should derive the classification change history
    from the source repository's git log. Default is false because the derivation requires a full
    AzurePrivilegedIAM checkout and is rarely needed in deployment reporting. Written as
    ClassificationExplorer.GenerateChangeHistory; a missing configuration file or setting also skips
    history generation.

.PARAMETER ResolveGroupMembersForPrivilegedAssets
    Defines if Configuration Analyzer should resolve included Conditional Access and authentication
    method policy target groups through PIM-aware transitive membership. Default is true.

.PARAMETER AllowPartialTenantGovernanceSnapshot
    Defines if the Push-EntraOpsPrivilegedReporting workflow should generate Configuration Analyzer
    data from a partially successful Tenant Governance snapshot. Failed resource types remain marked
    as stale in the report. Default is true.

.PARAMETER MaxTenantGovernanceSnapshotAgeHours
    Maximum age in hours of the published Tenant Governance snapshot before the Configuration Analyzer
    treats it as stale. Default is $null, which derives the limit from TenantGovernanceSnapshotScheduledCron:
    30 hours for a daily, 174 hours for a weekly and 750 hours for a monthly schedule.

.PARAMETER StaleTenantGovernanceSnapshotAction
    Defines what automated reporting does when the Tenant Governance snapshot is older than its maximum
    age. Warn (default) generates the Configuration Analyzer from it and marks the report as outdated,
    Skip leaves the Configuration Analyzer out and publishes the other reports, Fail stops the run before
    any report is generated.

.PARAMETER ConfigurationAnalyzerTimeRangeInDays
    Only Tenant Governance snapshots of the last N days are included in the Configuration Analyzer
    dataset. Default is 365. Use $null for the full history.

.PARAMETER ConfigurationAnalyzerSnapshotInterval
    Minimum spacing between two Configuration Analyzer snapshots as ISO 8601 duration (P1D, P1W, P1M, ...)
    or None to keep every snapshot commit. Default is P1W (weekly), which keeps the dataset size bounded.

.PARAMETER PimRequestFlowExcludedRiskFlags
    Array of PIM Request Flow risk-flag IDs to exclude from the generated report. Default is empty,
    so every supported risk flag is reported. Use the Configuration Wizard to select flags by name.

.PARAMETER AccessPackageFlowExcludedRiskFlags
    Array of Access Package Flow risk-flag IDs to exclude from the generated report. Default is empty, so every
    supported risk flag is reported.

.PARAMETER ConditionalAccessAnalysisExcludedFindings
    Array of Conditional Access Analysis finding IDs to exclude from the generated report. Default is empty, so
    every supported finding is reported.

.PARAMETER EidscaExcludedFindings
    Array of EIDSCA check IDs to exclude from the generated report. Default is empty.

.PARAMETER EnableCustomSecurityAttributeClassification
    Defines if the tier of User, ServicePrincipal and Application objects is read from Custom Security Attributes (CustomSecurityAttributes.Enabled). Can be combined with Alternate Tier Level Attributes and the Object Classification File: Custom Security Attributes win, an object without tier falls back to the filters of its object type, then to the file. Default is true, or false when -EnableObjectClassificationFile is $true and this parameter isn't passed. The PAW device and work account attributes are always read.

.PARAMETER EnableAlternateObjectTierLevelAttributes
    Defines if User and ServicePrincipal objects should be classified by PowerShell filter expressions evaluated against their own resolved EntraOps details (section AlternateObjectTierLevelAttributes) when they have no Custom Security Attribute tier. Sets User.Enabled and ServicePrincipal.Enabled; Group.Enabled is always created as false. Default is false. The config file always includes the section with empty filter expressions so it can be filled in and enabled per object type later; see Docs "Classify by Alternate Tier Level Attributes" for the required syntax.

.PARAMETER EnableObjectClassificationFile
    Defines if the Object Classification File (section ObjectClassificationFile) is used to classify User, Group, ServicePrincipal and Application objects by ObjectId. A file entry applies to objects that neither Custom Security Attributes nor Alternate Tier Level Attributes classify. Default is false. The file can be maintained in the Object Classification view of the Privileged Assets reporting app.

.PARAMETER ObjectClassificationFilePath
    Path of the Object Classification File (.json or .csv), relative to the EntraOps repository root. Default is "./Classification/ObjectClassification.json".

.PARAMETER EnableTenantGovernanceSnapshot
    Defines if the EntraOps Tenant Governance Snapshot feature is enabled. Default is false.
When enabled, run New-EntraOpsWorkloadIdentity afterwards to grant the workload identity the required "ConfigurationMonitoring.ReadWrite.All" Microsoft Graph permission, and to configure the read permissions of the first-party "Microsoft Tenant Configuration Management" (UTCM) service principal for the resources configured in `TenantGovernanceSnapshot.ResourcesToInclude`.
    Snapshots are captured by Save-EntraOpsTenantGovernanceSnapshotJson (and the Pull-EntraOpsTenantGovernance workflow) and stored as per-resource files under `TenantGovernance/Snapshots`; historical versions are tracked via Git history.
    Reference: https://learn.microsoft.com/en-us/graph/utcm-entra-resources

.PARAMETER TenantGovernanceResourcesToInclude
    Array of Microsoft Entra resource types (e.g. "microsoft.entra.conditionalAccessPolicy") to capture in each Tenant Governance snapshot. Defaults to the recommended tenant governance resource set.

.PARAMETER TenantGovernanceSnapshotScheduledCron
    Cron expression for the "start" scheduled trigger of the Pull-EntraOpsTenantGovernance workflow,
    which creates the snapshot job without waiting for it to complete. Default is daily at 06:00 UTC ("0 6 * * *").

.PARAMETER TenantGovernanceSnapshotScheduledCronComplete
    Cron expression for the first "collect" trigger of the Pull-EntraOpsTenantGovernance workflow,
    which checks the pending snapshot job once without polling. Default is daily at 07:00 UTC.

.PARAMETER TenantGovernanceSnapshotScheduledCronCompleteRetry1
    Cron expression for the first collection retry. Default is daily at 07:30 UTC.

.PARAMETER TenantGovernanceSnapshotScheduledCronCompleteRetry2
    Cron expression for the final collection retry. Default is daily at 08:00 UTC.

.PARAMETER TenantGovernanceSnapshotResourceFileNaming
    Controls the file name used for each persisted Tenant Governance snapshot resource file. "ResourceId"
    (default) names files after the immutable resource Id; "DisplayName" produces readable paths but changes
    them on rename. Resource-ID naming falls back to a display-name path for singleton types without an Id.

.PARAMETER RbacSystems
    Array of RBAC systems to collect and classify. Written as RbacSystems and as
    AutomatedControlPlaneScopeUpdate.EntraOpsScopes. Default is Azure, EntraID, IdentityGovernance, ResourceApps
    and DeviceManagement; Defender is optional.

.PARAMETER WatchListTemplates
    Enrichment WatchLists created by Save-EntraOpsPrivilegedEAMEnrichmentToWatchLists ("None", "All", "VIPUsers",
    "HighValueAssets", "IdentityCorrelation"). Written as SentinelWatchLists.WatchListTemplates. Default is "None".

.PARAMETER WatchListWorkloadIdentity
    Workload identity enrichment WatchLists created by Save-EntraOpsWorkloadIdentityEnrichmentWatchLists ("All",
    "ManagedIdentityAssignedResourceId", "WorkloadIdentityAttackPaths", "WorkloadIdentityInfo",
    "WorkloadIdentityRecommendations"). Written as SentinelWatchLists.WatchListWorkloadIdentity. Default is "None".

.EXAMPLE
    Create a configuration file in the default location, update classification files before analyzing privileges, and enable both Log Analytics and Sentinel WatchList ingestion.
    New-EntraOpsConfigFile -TenantName "contoso.onmicrosoft.com" -ApplyAutomatedControlPlaneScopeUpdate $true -IngestToLogAnalytics $true -IngestToWatchLists $true -ApplyAutomatedClassificationUpdate $true

.EXAMPLE
    Create environment file for Azure DevOps deployment with core monitoring only.
    New-EntraOpsConfigFile -TenantName "contoso.onmicrosoft.com" -DevOpsPlatform "AzureDevOps" -ApplyAutomatedClassificationUpdate $true
 #>


function New-EntraOpsConfigFile {

    [CmdletBinding()]
    param (
        [parameter(Mandatory = $true)]
        [string]$TenantName,

        [parameter(Mandatory = $false)]
        [string]$ManagingTenantName = "",        

        [parameter(Mandatory = $false)]
        [string]$ManagingTenantId = "",

        [Parameter(Mandatory = $false)]
        [ValidateSet('UserInteractive', 'SystemAssignedMSI', 'UserAssignedMSI', 'FederatedCredentials', 'AlreadyAuthenticated', 'DeviceAuthentication')]
        [string]$AuthenticationType = "FederatedCredentials",

        [Parameter(Mandatory = $false)]
        [boolean]$UseInvokeRestMethodOnly = $false,

        [Parameter(Mandatory = $false)]
        [boolean]$IncludeObjectDetails = $false,

        [Parameter(Mandatory = $false)]
        [ValidateSet('AzureDevOps', 'GitHub', 'None')]
        [string]$DevOpsPlatform = "GitHub",

        [Parameter(Mandatory = $false)]
        [ValidatePattern('^[^\r\n]+$')]
        [string]$AzureDevOpsServiceConnection = 'EntraOps-ServiceConnection',

        [Parameter(Mandatory = $false)]
        [ValidateScript({ Test-Path (Split-Path $_ -Parent) })]
        [string]$ConfigFilePath = "$EntraOpsBasefolder/EntraOpsConfig.json",

        [Parameter(Mandatory = $false)]
        [boolean]$IngestToLogAnalytics = $false,

        [Parameter(Mandatory = $false)]
        [boolean]$IngestToWatchLists = $false,

        [Parameter(Mandatory = $false)]
        [boolean]$ApplyAutomatedControlPlaneScopeUpdate = $false,

        [Parameter(Mandatory = $false)]
        [boolean]$ApplyAutomatedClassificationUpdate = $false,

        [Parameter(Mandatory = $false)]
        [boolean]$FailOnContradictoryTierPair = $false,

        [Parameter(Mandatory = $false)]
        [boolean]$FailOnPrivilegedAssignmentWithoutClassification = $false,

        [Parameter(Mandatory = $false)]
        [boolean]$ApplyAutomatedEntraOpsUpdate = $false,

        [Parameter(Mandatory = $false)]
        [ValidatePattern('^[A-Za-z0-9_.-]+$')]
        [string]$UpdateRepository = 'EntraOps',

        [Parameter(Mandatory = $false)]
        [ValidateSet('OnChange', 'Always', 'Never')]
        [string]$ValidationFrequency = 'OnChange',

        [Parameter(Mandatory = $false)]
        [boolean]$RunBrowserTests = $true,

        [Parameter(Mandatory = $false)]
        [ValidateSet('PullRequest', 'DirectPush')]
        [string]$UpdatePublicationMode = 'PullRequest',

        [Parameter(Mandatory = $false)]
        [boolean]$ApplyConditionalAccessTargetGroups = $false,

        [Parameter(Mandatory = $false)]
        [boolean]$ApplyAdministrativeUnitAssignments = $false,

        [Parameter(Mandatory = $false)]
        [boolean]$ApplyRmauAssignmentsForUnprotectedObjects = $false,

        [Parameter(Mandatory = $false)]
        [boolean]$IncludeUnprotectedDevices = $false,

        [Parameter(Mandatory = $false)]
        [ValidateRange(0, 1)]
        [double]$RemovalSafetyThreshold = 0.5,

        [Parameter(Mandatory = $false)]
        [boolean]$ApplyPrivilegedElmCatalogProtection = $false,

        [Parameter(Mandatory = $false)]
        [boolean]$ClassifyConstrainedDelegationAlwaysAsControlPlane = $false,

        [Parameter(Mandatory = $false)]
        [ValidateSet("Unclassified", "ManagementPlane", "ControlPlane", "None")]
        [string]$UnresolvedRoleDefinitionFallbackTier = "Unclassified",

        [Parameter(Mandatory = $false)]
        [ValidateSet("Keep", "Filter")]
        [string]$DeletedPrincipalAssignmentHandling = "Filter",

        [Parameter(Mandatory = $false)]
        [boolean]$ApplyAutomatedReportingGeneration = $false,

        [Parameter(Mandatory = $false)]
        [boolean]$PublishReportsAsRelease = $false,

        [Parameter(Mandatory = $false)]
        [boolean]$RunGeneratedReportTests = $false,

        [Parameter(Mandatory = $false)]
        [boolean]$UploadReportTestDiagnostics = $false,

        [Parameter(Mandatory = $false)]
        [ValidateRange(1, 1000)]
        [int]$ReportingReleasesToKeep = 10,

        [Parameter(Mandatory = $false)]
        [boolean]$EnablePrivilegeHistory = $true,

        [Parameter(Mandatory = $false)]
        [Nullable[int]]$PrivilegeHistoryTimeRangeInDays = $null,

        [Parameter(Mandatory = $false)]
        [string]$PrivilegeHistorySnapshotInterval = "P2W",

        [Parameter(Mandatory = $false)]
        [boolean]$AccessPathMapResolveObjectIdsOutsidePrivilegedEAM = $true,

        [Parameter(Mandatory = $false)]
        [boolean]$EamDashboardResolveLinkedIdentityObjectIds = $true,

        [Parameter(Mandatory = $false)]
        [boolean]$PrivilegedAssetsResolveRelatedObjectIds = $true,

        [Parameter(Mandatory = $false)]
        [boolean]$ClassificationExplorerGenerateChangeHistory = $false,

        [Parameter(Mandatory = $false)]
        [boolean]$ResolveGroupMembersForPrivilegedAssets = $true,

        [Parameter(Mandatory = $false)]
        [boolean]$AllowPartialTenantGovernanceSnapshot = $true,

        [Parameter(Mandatory = $false)]
        [System.Nullable[int]]$MaxTenantGovernanceSnapshotAgeHours,

        [Parameter(Mandatory = $false)]
        [ValidateSet('Warn', 'Skip', 'Fail')]
        [string]$StaleTenantGovernanceSnapshotAction = 'Warn',

        [Parameter(Mandatory = $false)]
        [System.Nullable[int]]$ConfigurationAnalyzerTimeRangeInDays = 365,

        [Parameter(Mandatory = $false)]
        [ValidatePattern('^(None|P\d+[DWMY])$')]
        [string]$ConfigurationAnalyzerSnapshotInterval = 'P1W',

        [Parameter(Mandatory = $false)]
        [ValidateSet('noMfaActivation', 'noApprovalActivation', 'approverNotEnforced', 'approverLowerTier', 'authContextMissing', 'permanentEligible', 'permanentActive', 'noMfaActiveAssign')]
        [string[]]$PimRequestFlowExcludedRiskFlags = @(),

        [Parameter(Mandatory = $false)]
        [ValidateSet('noApproval', 'broadRequestor', 'approverLowerTier', 'assignedMorePrivileged')]
        [string[]]$AccessPackageFlowExcludedRiskFlags = @(),

        [Parameter(Mandatory = $false)]
        [ValidateSet('policyNotEnforced', 'legacyAuthentication', 'allUserMfa', 'privilegedRoleControls', 'riskBasedPolicies', 'deviceControls', 'guestCoverage', 'clientAppCoverage', 'exclusionSurface')]
        [string[]]$ConditionalAccessAnalysisExcludedFindings = @(),

        [Parameter(Mandatory = $false)]
        [string[]]$EidscaExcludedFindings = @(),

        [Parameter(Mandatory = $false)]
        [boolean]$EnableCustomSecurityAttributeClassification = $true,

        [Parameter(Mandatory = $false)]
        [boolean]$EnableAlternateObjectTierLevelAttributes = $false,        

        [Parameter(Mandatory = $false)]
        [boolean]$EnableObjectClassificationFile = $false,

        [Parameter(Mandatory = $false)]
        [ValidatePattern('\.(json|csv)$')]
        [string]$ObjectClassificationFilePath = "./Classification/ObjectClassification.json",

        [Parameter(Mandatory = $false)]
        [boolean]$EnableTenantGovernanceSnapshot = $false,

        [Parameter(Mandatory = $false)]
        [Array]$TenantGovernanceResourcesToInclude,

        [Parameter(Mandatory = $false)]
        [string]$TenantGovernanceSnapshotScheduledCron = "0 6 * * *",

        [Parameter(Mandatory = $false)]
        [string]$TenantGovernanceSnapshotScheduledCronComplete = "0 7 * * *",

        [Parameter(Mandatory = $false)]
        [string]$TenantGovernanceSnapshotScheduledCronCompleteRetry1 = "30 7 * * *",

        [Parameter(Mandatory = $false)]
        [string]$TenantGovernanceSnapshotScheduledCronCompleteRetry2 = "0 8 * * *",

        [Parameter(Mandatory = $false)]
        [ValidateSet("DisplayName", "ResourceId")]
        [string]$TenantGovernanceSnapshotResourceFileNaming = "ResourceId",

        [Parameter(Mandatory = $false)]
        [ValidateSet("Azure", "EntraID", "IdentityGovernance", "DeviceManagement", "ResourceApps", "Defender")]
        [Array]$RbacSystems = ("Azure", "EntraID", "IdentityGovernance", "ResourceApps", "DeviceManagement"),

        [Parameter(Mandatory = $false)]
        [ValidateSet("Azure", "EntraID", "IdentityGovernance", "DeviceManagement", "ResourceApps", "Defender")]
        [Array]$ClassificationParameterScope = ("Azure", "EntraID", "IdentityGovernance", "ResourceApps", "DeviceManagement", "Defender"),

        [Parameter(Mandatory = $false)]
        [ValidateSet("None", "All", "VIPUsers", "HighValueAssets", "IdentityCorrelation")]
        [Array]$WatchListTemplates = "None",

        [Parameter(Mandatory = $false)]
        [ValidateSet("ManagedIdentityAssignedResourceId", "All", "WorkloadIdentityAttackPaths", "WorkloadIdentityInfo", "WorkloadIdentityRecommendations")]
        [Array]$WatchListWorkloadIdentity = "None"
    )

    $ErrorActionPreference = "Stop"

    # Validated here instead of [ValidateRange] because that attribute rejects an explicit $null (derive from schedule).
    if ($null -ne $MaxTenantGovernanceSnapshotAgeHours -and ($MaxTenantGovernanceSnapshotAgeHours -lt 1 -or $MaxTenantGovernanceSnapshotAgeHours -gt 8760)) {
        throw "MaxTenantGovernanceSnapshotAgeHours must be between 1 and 8760 (or null to derive it from TenantGovernanceSnapshotScheduledCron), but is $MaxTenantGovernanceSnapshotAgeHours."
    }

    #region Connect to Azure Resource Manager
    # Get TenantId
    try {
        $TenantId = (Invoke-RestMethod -Uri ("https://login.windows.net/$($TenantName)/.well-known/openid-configuration")).token_endpoint.split('/')[3]
    } catch {
        Write-Error "Can't find tenant with name $TenantName. Error: $_"
    }

    Write-Output "Connect to Azure Resource Manager..."
    $AzContext = Get-AzContext
    if ($AzContext.Tenant.Id -ne $TenantId) {
        Write-Verbose "Call Connect-AzAccount to $($TenantId)..."
        Connect-AzAccount -TenantId $TenantId
    } else {
        Write-Verbose "Already connected to $($AzContext.Tenant.Id)"
    }

    try {
        $TenantDetails = Get-AzTenant -TenantId $TenantId
    } catch {
        Write-Error "Failed to get Tenant details for TenantId $TenantId. Error: $_"
    }
    #endregion

    #region Create configuration file schema with default values
    $DefaultUpdateTargets = if ($DevOpsPlatform -eq 'AzureDevOps') {
        @("./.azure-pipelines", "./.github/scripts", "./Docs", "./EntraOps", "./Parsers", "./Queries", "./Reports", "./Samples", "./Tests", "./Workbooks", "./package.json", "./package-lock.json", "./playwright.config.mjs", "./CHANGELOG.md", "./EntraOpsUpdateContract.json")
    } else {
        @("./.github/actions", "./.github/agents", "./.github/scripts", "./Docs", "./EntraOps", "./Parsers", "./Queries", "./Reports", "./Samples", "./Tests", "./Workbooks", "./package.json", "./package-lock.json", "./playwright.config.mjs", "./CHANGELOG.md", "./EntraOpsUpdateContract.json")
    }
    if (-not $PSBoundParameters.ContainsKey('EnableCustomSecurityAttributeClassification')) {
        $EnableCustomSecurityAttributeClassification = -not $EnableObjectClassificationFile
    }
    # GitHub Actions and the Azure DevOps update pipeline both support PullRequest and DirectPush
    $DefaultUpdatePublicationMode = $UpdatePublicationMode

    $EnvConfigSchema = [ordered]@{
        TenantId                                      = $($TenantId)
        TenantName                                    = $($TenantDetails.Domains[0])
        ManagingTenantId                              = $($ManagingTenantId)
        ManagingTenantName                            = $($ManagingTenantName)
        AuthenticationType                            = $($AuthenticationType)
        UseInvokeRestMethodOnly                       = $UseInvokeRestMethodOnly
        ConsoleOutput                                 = [ordered]@{
            IncludeObjectDetails = $IncludeObjectDetails
        }
        ClientId                                      = "Use New-EntraOpsWorkloadIdentity to create a new App Registration or enter here manually"
        DevOpsPlatform                                = $($DevOpsPlatform)
        AzureDevOpsServiceConnection                  = $AzureDevOpsServiceConnection
        RbacSystems                                   = $($RbacSystems)
        AzureRbacClassification                       = [ordered]@{
            ClassifyConstrainedDelegationAlwaysAsControlPlane = $ClassifyConstrainedDelegationAlwaysAsControlPlane
            UnresolvedRoleDefinitionFallbackTier              = $UnresolvedRoleDefinitionFallbackTier
            DeletedPrincipalAssignmentHandling                = $DeletedPrincipalAssignmentHandling
        }
        WorkflowTrigger                               = [ordered]@{
            PullScheduledTrigger                  = $true
            PullScheduledCron                     = "30 9 * * *"
            PushAfterPullWorkflowTrigger          = $true
            PushReportingAfterPullWorkflowTrigger = $true
            PushReportingScheduledTrigger         = $false
            PushReportingScheduledCron            = "0 9 * * 1"
        }
        AutomatedControlPlaneScopeUpdate              = [ordered]@{
            ApplyAutomatedControlPlaneScopeUpdate = $ApplyAutomatedControlPlaneScopeUpdate
            PrivilegedObjectClassificationSource  = ("EntraOps", "PrivilegedRolesFromAzGraph", "PrivilegedEdgesFromExposureManagement")
            EntraOpsScopes                        = @($RbacSystems)
            ClassificationParameterScope          = $($ClassificationParameterScope)
            AzureHighPrivilegedRoles              = ("Owner", "Role Based Access Control Administrator", "User Access Administrator")
            AzureHighPrivilegedScopes             = ("/", "/providers/microsoft.management/managementgroups/$($TenantId)")
            ExposureCriticalityLevel              = "<1"
        }
        AutomatedClassificationUpdate                 = [ordered]@{
            ApplyAutomatedClassificationUpdate = $ApplyAutomatedClassificationUpdate
            Classifications                    = ("All")
        }
        GeneratedArtifactValidation                   = [ordered]@{
            # Contradictory tier pairs come from Custom Security Attribute drift on the source object,
            # so they are reported as warnings by default instead of blocking the whole collection run.
            FailOnContradictoryTierPair                     = $FailOnContradictoryTierPair
            FailOnPrivilegedAssignmentWithoutClassification = $FailOnPrivilegedAssignmentWithoutClassification
        }
        AutomatedEntraOpsUpdate                       = [ordered]@{
            ApplyAutomatedEntraOpsUpdate = $ApplyAutomatedEntraOpsUpdate
            UpdateScheduledTrigger       = $ApplyAutomatedEntraOpsUpdate
            UpdateScheduledCron          = "0 9 * * 3"
            ValidationFrequency          = $ValidationFrequency
            RunBrowserTests              = $RunBrowserTests
            # Distribution repositories are declared once in EntraOpsUpdateContract.json: "EntraOps"
            # (public release channel, no credentials) or "EntraOps-Insiders" (private, requires the
            # EntraOpsUpdatePat secret). A different source is accepted only when it publishes a
            # matching EntraOpsUpdateContract.json.
            Repository                   = $UpdateRepository
            # Track main by default so enabled automated updates require no release-ref maintenance.
            Branch                       = "main"
            # GitHub workflow definitions are intentionally opt-in because GITHUB_TOKEN cannot push
            # them. Azure DevOps configurations instead include their pipeline templates by default.
            TargetUpdateFolders          = $DefaultUpdateTargets
            PublicationMode              = $DefaultUpdatePublicationMode
        }
        LogAnalytics                                  = [ordered]@{
            IngestToLogAnalytics             = $IngestToLogAnalytics
            DataCollectionRuleName           = "Enter Data Collection Rule Name and run New-EntraOpsWorkloadIdentity to assign permissions"
            DataCollectionRuleSubscriptionId = "Enter Subscription Id of Data Collection Rule and run New-EntraOpsWorkloadIdentity to assign permissions"
            DataCollectionResourceGroupName  = "Enter Resource Group Name of Data Collection Rule and run New-EntraOpsWorkloadIdentity to assign permissions"
            TableName                        = "PrivilegedEAM_CL"
        }
        SentinelWatchLists                            = [ordered]@{
            IngestToWatchLists        = $IngestToWatchLists
            WatchListTemplates        = $WatchListTemplates
            WatchListWorkloadIdentity = $WatchListWorkloadIdentity
            SentinelWorkspaceName     = "Enter Log Analytics Workspace Name and run New-EntraOpsWorkloadIdentity to assign permissions"
            SentinelSubscriptionId    = "Enter Subscription Id of Log Analytics Workspace and run New-EntraOpsWorkloadIdentity to assign permissions"
            SentinelResourceGroupName = "Enter Resource Group of Log Analytics Workspace and run New-EntraOpsWorkloadIdentity to assign permissions"
            WatchListPrefix           = "EntraOps_"
        }        
        AutomatedAdministrativeUnitManagement         = [ordered]@{
            ApplyAdministrativeUnitAssignments = $ApplyAdministrativeUnitAssignments
            ApplyToAccessTierLevel             = ("ControlPlane", "ManagementPlane")
            FilterObjectType                   = ("User", "Group")
            RbacSystems                        = ("EntraID", "IdentityGovernance", "ResourceApps", "DeviceManagement")
            RestrictedAuMode                   = "selected"
            RemovalSafetyThreshold             = $RemovalSafetyThreshold
        }
        AutomatedConditionalAccessTargetGroups        = [ordered]@{
            ApplyConditionalAccessTargetGroups = $ApplyConditionalAccessTargetGroups
            AdminUnitName                      = "Tier0-ControlPlane.ConditionalAccess"
            ApplyToAccessTierLevel             = ("ControlPlane", "ManagementPlane")
            FilterObjectType                   = ("User", "Group")
            GroupPrefix                        = "sug_Entra.CA.IncludeUsers.PrivilegedAccounts."
            RbacSystems                        = ("EntraID", "IdentityGovernance", "ResourceApps", "DeviceManagement")
            RemovalSafetyThreshold             = $RemovalSafetyThreshold
        }
        AutomatedRmauAssignmentsForUnprotectedObjects = [ordered]@{
            ApplyRmauAssignmentsForUnprotectedObjects = $ApplyRmauAssignmentsForUnprotectedObjects
            ApplyToAccessTierLevel                    = ("ControlPlane", "ManagementPlane")
            FilterObjectType                          = ("User", "Group")
            RbacSystems                               = ("EntraID", "IdentityGovernance", "DeviceManagement")
            IncludeUnprotectedDevices                 = $IncludeUnprotectedDevices
            RemovalSafetyThreshold                    = $RemovalSafetyThreshold
        }
        AutomatedReportingGeneration                  = [ordered]@{
            ApplyAutomatedReportingGeneration = $ApplyAutomatedReportingGeneration
            PublishReportsAsRelease           = $PublishReportsAsRelease
            RunGeneratedReportTests           = $RunGeneratedReportTests
            UploadReportTestDiagnostics       = $UploadReportTestDiagnostics
            ReportingReleasesToKeep           = $ReportingReleasesToKeep
            GenerateClassificationExplorer    = $true
            GenerateTierBreachAnalyzer        = $true
            GenerateEamDashboard              = $true
            GenerateAccessPathMap             = $true
            GenerateConfigurationAnalyzer     = $true
            GenerateAccessPackageFlow         = $true
            GeneratePrivilegeHistory          = $true
            GeneratePrivilegedAssets          = $true
            ClassificationExplorerRepository  = "Cloud-Architekt/AzurePrivilegedIAM"
        }
        ConfigurationAnalyzer                         = [ordered]@{
            ResolveGroupMembersForPrivilegedAssets    = $ResolveGroupMembersForPrivilegedAssets
            AllowPartialTenantGovernanceSnapshot      = $AllowPartialTenantGovernanceSnapshot
            MaxTenantGovernanceSnapshotAgeHours       = $MaxTenantGovernanceSnapshotAgeHours
            StaleTenantGovernanceSnapshotAction       = $StaleTenantGovernanceSnapshotAction
            TimeRangeInDays                           = $ConfigurationAnalyzerTimeRangeInDays
            SnapshotInterval                          = $ConfigurationAnalyzerSnapshotInterval
            PimRequestFlowExcludedRiskFlags           = @($PimRequestFlowExcludedRiskFlags)
            AccessPackageFlowExcludedRiskFlags        = @($AccessPackageFlowExcludedRiskFlags)
            ConditionalAccessAnalysisExcludedFindings = @($ConditionalAccessAnalysisExcludedFindings)
            EidscaExcludedFindings                    = @($EidscaExcludedFindings)
        }
        AutomatedElmCatalogProtection                 = [ordered]@{
            ApplyPrivilegedElmCatalogProtection = $ApplyPrivilegedElmCatalogProtection
            ApplyToAccessTierLevel              = , "ControlPlane"
            RemovalSafetyThreshold              = $RemovalSafetyThreshold
        }
        CustomSecurityAttributes                      = [ordered]@{
            Enabled                                               = $EnableCustomSecurityAttributeClassification
            PrivilegedUserAttribute                               = "privilegedUser"
            PrivilegedUserPawAttribute                            = "associatedSecureAdminWorkstation"
            PrivilegedServicePrincipalAttribute                   = "privilegedWorkloadIdentity"
            UserWorkAccountAttribute                              = "associatedWorkAccount"
            PrivilegedUserAdminTierLevelAttribute                 = "adminTierLevel"
            PrivilegedUserAdminTierLevelNameAttribute             = "adminTierLevelName"
            PrivilegedServicePrincipalAdminTierLevelAttribute     = "adminTierLevel"
            PrivilegedServicePrincipalAdminTierLevelNameAttribute = "adminTierLevelName"
        }
        AlternateObjectTierLevelAttributes            = [ordered]@{
            User             = [ordered]@{
                Enabled         = $EnableAlternateObjectTierLevelAttributes
                ControlPlane    = ""
                ManagementPlane = ""
                UserAccess      = ""
            }
            ServicePrincipal = [ordered]@{
                Enabled         = $EnableAlternateObjectTierLevelAttributes
                ControlPlane    = ""
                ManagementPlane = ""
                UserAccess      = ""
            }
            Group            = [ordered]@{
                Enabled         = $false
                ControlPlane    = ""
                ManagementPlane = ""
                UserAccess      = ""
            }
        }
        ObjectClassificationFile                      = [ordered]@{
            Enabled  = $EnableObjectClassificationFile
            FilePath = $ObjectClassificationFilePath
        }
        PrivilegeHistory                              = [ordered]@{
            EnablePrivilegeHistory = $EnablePrivilegeHistory
            TimeRangeInDays        = $PrivilegeHistoryTimeRangeInDays
            SnapshotInterval       = $PrivilegeHistorySnapshotInterval
        }
        AccessPathMap                                 = [ordered]@{
            ResolveObjectIdsOutsidePrivilegedEAM = $AccessPathMapResolveObjectIdsOutsidePrivilegedEAM
        }
        EamDashboard                                  = [ordered]@{
            ResolveLinkedIdentityObjectIds = $EamDashboardResolveLinkedIdentityObjectIds
        }
        PrivilegedAssets                              = [ordered]@{
            ResolveRelatedObjectIds = $PrivilegedAssetsResolveRelatedObjectIds
        }
        ClassificationExplorer                        = [ordered]@{
            GenerateChangeHistory = $ClassificationExplorerGenerateChangeHistory
        }
        TenantGovernanceSnapshot                      = [ordered]@{
            EnableTenantGovernanceSnapshot      = $EnableTenantGovernanceSnapshot
            ResourcesToInclude                  = if ($TenantGovernanceResourcesToInclude) { $TenantGovernanceResourcesToInclude } else { (Get-EntraOpsTenantGovernanceResourceDefinition).DefaultResources }
            SnapshotDisplayNamePrefix           = "EntraOps TG"
            SnapshotResourceFileNaming          = $TenantGovernanceSnapshotResourceFileNaming
            SnapshotScheduledTrigger            = $EnableTenantGovernanceSnapshot
            SnapshotScheduledCron               = $TenantGovernanceSnapshotScheduledCron
            SnapshotScheduledCronComplete       = $TenantGovernanceSnapshotScheduledCronComplete
            SnapshotScheduledCronCompleteRetry1 = $TenantGovernanceSnapshotScheduledCronCompleteRetry1
            SnapshotScheduledCronCompleteRetry2 = $TenantGovernanceSnapshotScheduledCronCompleteRetry2
        }
        ServiceEM                                     = [ordered]@{
            GovernanceModel                   = "Centralized"
            ControlPlaneDelegationGroupId     = ""
            ControlPlaneGroupName             = "PRG-Tenant-ControlPlane-IdentityOps"
            ManagementPlaneDelegationGroupId  = ""
            ManagementPlaneGroupName          = "PRG-Tenant-ManagementPlane-PlatformOps"
            AdministratorGroupId              = ""
            DefaultAzureRegion                = ""
            SkipCatalogOwnerAssignment        = $false
            CreateM365Group                   = $false
            AddWorkloadPlaneAdminToUsers      = $false
            GroupPrefix                       = "SG"
            RoleAssignableWorkloadPlaneAdmins = $false
            AdministrativeUnits               = [ordered]@{
                WorkloadPlane = ""
                UserAccess    = ""
            }
            UpdateObjectClassificationFile    = $false
            ConstrainedDelegation             = [ordered]@{
                ManagementPlane = [ordered]@{
                    ExcludedRoleDefinitionIds = @(
                        "8e3af657-a8ff-443c-a75c-2fe8c4bcb635"  # Owner
                        "18d7d88d-d35e-4fb5-a5c3-7773c20a72d9"  # User Access Administrator
                        "f58310d9-a9f6-439a-9e8d-f62e7b41a168"  # Role Based Access Control Administrator
                    )
                    AllowedTargetGroupFilter  = "WorkloadPlane-Admins"
                }
                WorkloadPlane   = [ordered]@{
                    AllowedRoleDefinitionIds = @(
                        # Key Vault roles
                        "00482a5a-887f-4fb3-b363-3b7fe8e74483"  # Key Vault Administrator
                        "a4417e6f-fecd-4de8-b567-7b0420556985"  # Key Vault Certificates Officer
                        "14b46e9e-c2b7-41b4-b07b-48a6ebf60603"  # Key Vault Crypto Officer
                        "12338af0-0e69-4776-bea7-57ae8d297424"  # Key Vault Crypto User
                        "21090545-7ca7-4776-b22c-e363652d74d2"  # Key Vault Reader
                        "b86a8fe4-44ce-4948-aee5-eccb2c155cd7"  # Key Vault Secrets Officer
                        "4633458b-17de-408a-b874-0445c86b69e6"  # Key Vault Secrets User
                        # Storage roles
                        "ba92f5b4-2d11-453d-a403-e96b0029c9fe"  # Storage Blob Data Contributor
                        "b7e6dc6d-f1e8-4753-8033-0f276bb0955b"  # Storage Blob Data Owner
                        "2a2b9908-6ea1-4ae2-8e65-a410df84e7d1"  # Storage Blob Data Reader
                        "0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3"  # Storage Table Data Contributor
                        "76199698-9eea-4c19-bc75-cec21354c6b6"  # Storage Table Data Reader
                        "974c5e8b-45b9-4653-ba55-5f855dd0fb88"  # Storage Queue Data Contributor
                        "19e7f393-937e-4f77-808e-94535e297925"  # Storage Queue Data Reader
                        "8a0f0c08-91a1-4084-bc3d-661d67233fed"  # Storage Queue Data Message Processor
                        "c6a89b2d-59bc-44d0-9896-0f6e12d7b80a"  # Storage Queue Data Message Sender
                    )
                    AllowedTargetGroupFilter = "WorkloadPlane-Users"
                }
            }
            PIMAuthenticationContext          = [ordered]@{
                EnableAuthenticationContext = $false
                ControlPlane                = [ordered]@{
                    AuthenticationContextClassReferenceId = ""
                    AuthenticationContextDisplayName      = ""
                }
                ManagementPlane             = [ordered]@{
                    AuthenticationContextClassReferenceId = ""
                    AuthenticationContextDisplayName      = ""
                }
                WorkloadPlane               = [ordered]@{
                    AuthenticationContextClassReferenceId = ""
                    AuthenticationContextDisplayName      = ""
                }
            }
            PIMForGroups                      = [ordered]@{
                MaximumActivationDuration       = "PT10H"
                MaximumActiveAssignmentDuration = "P15D"
            }
            AssignmentPolicies                = [ordered]@{
                BaselinePolicy            = [ordered]@{ Expiration = "P365D"; ApprovalTimeout = "P2D"; AllowExtension = $true }
                WorkloadPlaneUsers        = [ordered]@{ Expiration = "P365D"; ApprovalTimeout = "P2D"; RequestorScope = "AllMemberUsers"; AllowExtension = $true }
                WorkloadPlaneAdmins       = [ordered]@{ Expiration = "P365D"; ApprovalTimeout = "P2D"; AllowExtension = $true }
                ManagementPlaneAdmins     = [ordered]@{ Expiration = "P365D"; ApprovalTimeout = "P1D"; AllowExtension = $true }
                InitialWorkloadMembership = [ordered]@{ Expiration = "P365D" }
                InitialManagementAdmins   = [ordered]@{ Expiration = "P365D" }
                InitialWorkloadUsers      = [ordered]@{ Expiration = "P365D" }
                InitialWorkloadAdmins     = [ordered]@{ Expiration = "P365D" }
                InitialCatalogMembers     = [ordered]@{ Expiration = "P365D" }
            }
            AccessReviews                     = [ordered]@{
                EnableAccessReviews        = $true
                RecurrenceIntervalInMonths = 3
                StartAfterDays             = 4
                ReviewDuration             = "P25D"
                Policies                   = [ordered]@{
                    BaselinePolicy            = [ordered]@{ ReviewerType = "Group"; Reviewers = @("ManagementPlane-Admins") }
                    WorkloadPlaneUsers        = [ordered]@{ ReviewerType = "Group"; Reviewers = @("WorkloadPlane-Admins") }
                    WorkloadPlaneAdmins       = [ordered]@{ ReviewerType = "Group"; Reviewers = @("ManagementPlane-Admins") }
                    ManagementPlaneAdmins     = [ordered]@{ ReviewerType = "Group"; Reviewers = @("ControlPlane-Admins") }
                    InitialWorkloadMembership = [ordered]@{ ReviewerType = "Group"; Reviewers = @("ManagementPlane-Admins") }
                    InitialManagementAdmins   = [ordered]@{ ReviewerType = "Group"; Reviewers = @("ControlPlane-Admins") }
                    InitialWorkloadUsers      = [ordered]@{ ReviewerType = "Group"; Reviewers = @("WorkloadPlane-Admins") }
                    InitialWorkloadAdmins     = [ordered]@{ ReviewerType = "Group"; Reviewers = @("ManagementPlane-Admins") }
                    InitialCatalogMembers     = [ordered]@{ ReviewerType = "Group"; Reviewers = @("ManagementPlane-Admins") }
                }
            }
        }
    }
    #endregion

    #region Write configuration file to disk
    try {
        Write-Output "Writing configuration file to $($ConfigFilePath)..."
        $EnvConfigSchema | ConvertTo-Json -Depth 10 | Out-File -Path $($ConfigFilePath)
    } catch {
        Write-Error "Failed to write configuration file to $($ConfigFilePath). Error: $_"
    }
    #endregion
}

# SIG # Begin signature block
# MIIniAYJKoZIhvcNAQcCoIIneTCCJ3UCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCAJv431t+okLrE+
# otIGcOZ1nTCbMs7ZXd29TmrRw2IPH6CCIDwwggXJMIIEsaADAgECAhAbtY8lKt8j
# 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
# ARUwLwYJKoZIhvcNAQkEMSIEIO/hmKGHaLJrXOe2VWlYCYVivZK4RZT4PEltoVRr
# XorZMA0GCSqGSIb3DQEBAQUABIIBgCUj7aGRxtS5MlkdNFOZQ03oRqYOBRMIrfok
# 9Y6f+4ujWvDW2GHBqob1gOKRgrYVXTzH8pOsOX5+XXq5K1hhZBVAck30/q526jTW
# REmsZGEIjluT5eaMd3B3khAQWsS8VLONLswGYvsWRkzpuetf/9WslFndcyGsNMdM
# YAslwwZffSXANOl2iMG21/Xe+PnEvpkc++6ipUl/Ay13u0/qjp7vAM7pqWxE+3gY
# LxPbTO8qNdvlh3cPkomrV3UBgmKmCoCOt83MmKnYAoTXuBnIDYyaVudCb6hAExuf
# bTgJwDDzScKPOeZ2Argqg008/SfjXqUFvFNNMulL13mw5Q9BIrT6m4EyT9BfWCgv
# f2A79BCLpQyJGFvdu0xZsTDSCYBi38ihXtbuFqWffB/aWJrE0rQ3Jv2Fudrgy4qf
# MLAym6ZxWXScLOFiZCAgNA0YpkxtiVn6w8nUiWpU4EP7d2UP8itLvc9D46vRkieX
# VmutjTKQKAYMW9Sntz01OUpbz2VTiqGCBAIwggP+BgkqhkiG9w0BCQYxggPvMIID
# 6wIBATBqMFYxCzAJBgNVBAYTAlBMMSEwHwYDVQQKExhBc3NlY28gRGF0YSBTeXN0
# ZW1zIFMuQS4xJDAiBgNVBAMTG0NlcnR1bSBUaW1lc3RhbXBpbmcgMjAyMSBDQQIQ
# KPB3wRw2vf5fdDJHcCcuAzANBglghkgBZQMEAgIFAKCCAVYwGgYJKoZIhvcNAQkD
# MQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNjEwMjIxMTM4NTVaMDcG
# CyqGSIb3DQEJEAIvMSgwJjAkMCIEIIW+kOEK0kONfMkotq9IsJqyCBd87PiwEmxY
# 05EFJcQ8MD8GCSqGSIb3DQEJBDEyBDCv04krO4Suu3F4jnwj/9RKq/2oqk/hiGAZ
# mmUQY0maxcMf+zxHBa82kUB1Ws08HGMwgZ8GCyqGSIb3DQEJEAIMMYGPMIGMMIGJ
# MIGGBBRXFGhBDKha80JO+RZKUTYQ9NONmDBuMFqkWDBWMQswCQYDVQQGEwJQTDEh
# MB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMSQwIgYDVQQDExtDZXJ0
# dW0gVGltZXN0YW1waW5nIDIwMjEgQ0ECECjwd8EcNr3+X3QyR3AnLgMwDQYJKoZI
# hvcNAQEBBQAEggIAiFTSUr/Y2RuXO1MyBYHKvxEk6efQsq8iAoy1/0WLufH1xhRF
# 1FRhEuVw3qtGTmjvb8WwrOFiJWZkAOjfPBaGRheJNHUKQuhb1cYsUfrY0mZx0Fps
# HQCQCfMZdam04+Un5YZl7Mb3HSAa1ZDeJtrxkzfnyFFL9ZBmpkebTSA7Xs5aNb7t
# Js1fFwDLw38md25dTv8btE0MagYuf3fkz3oZxDgEwdZQyIhz5az2wypbXuea7Y76
# rIF/if0u9o7bHWtULXnzGV0uG1U3J0TXmAIkMDIT5JrehqPMy9tG2VUrngMeWzfY
# Sq4VBg6EGjpVvtVN1JFukr3F3Ut1DAayc22hLKiEq7vJg0o/kMuun69ky+1Pz7Kg
# qm1DLZ1QY72lAzfeOQNkCu75qieY4XOm/ca/YHcQinV6hFLmCqEwJahzLYlAU4zH
# fvo/AUGTHubckFT9Y5Ec/DF4YAa2LV1NgYtPybz4C95LhENb2e+oy/f66PPBYj6S
# zElg1HAhCGPrKWh7+MuHpitLwZhUpEXGrea7LGKglagt9FNqX+u+jF8OQtIun1ez
# O5mgcDhw6ZA/2gq/R+iF5UrmItJpRWHX8Z+pplBgauzURuMjOzzslRJYA0eCDMbl
# x9TgMLNfe/0cM7EiV/JENHv3gShnpzN+y3jFfarWb6Ne1l/OGueaSu85Dpc=
# SIG # End signature block