source/Private/AzureResourceOperations.ps1

function Connect-RJAzureContext {

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [string]$SubscriptionId
    )

    Write-Verbose "Checking Azure connection..."

    # Return cached context if it still matches the active Az session
    if ($script:RJAzureContextCache) {
        if (-not $SubscriptionId -or $SubscriptionId -eq $script:RJAzureContextCache.SubscriptionId) {
            $activeContext = Get-AzContext -ErrorAction SilentlyContinue
            if ($activeContext -and
                $activeContext.Tenant.Id -eq $script:RJAzureContextCache.TenantId -and
                $activeContext.Subscription.Id -eq $script:RJAzureContextCache.SubscriptionId) {
                Write-Verbose "Using cached Azure context (Tenant: $($script:RJAzureContextCache.TenantName), Subscription: $($script:RJAzureContextCache.SubscriptionName))"
                return $script:RJAzureContextCache
            }
            Write-Verbose "Active Az context changed — re-evaluating connection."
            $script:RJAzureContextCache = $null
        }
    }

    $context = Get-AzContext -ErrorAction SilentlyContinue
    if (-not $context -or -not $context.Account) {
        Write-Information "Connecting to Azure..."

        $connectParams = @{}
        if ($SubscriptionId) {
            $connectParams.SubscriptionId = $SubscriptionId
        }

        Connect-AzAccount @connectParams | Out-Null

        $context = Get-AzContext -ErrorAction SilentlyContinue
        if (-not $context -or -not $context.Account) {
            throw "Failed to establish Azure connection"
        }

        Write-Verbose "Successfully connected to Azure as $($context.Account.Id)"
    }
    else {
        Write-Verbose "Already connected to Azure as $($context.Account.Id)"
    }

    $allContexts = @(Get-AzContext -ListAvailable)

    # Tenant selection
    $tenantGroups = @($allContexts | Group-Object { $_.Tenant.Id })

    if ($tenantGroups.Count -gt 1) {
        $counter = 0
        $tenantOptions = @($tenantGroups | ForEach-Object {
            $counter++
            $firstContext = $_.Group[0]
            $tenantName = if ($firstContext.Tenant.Name) { $firstContext.Tenant.Name } else { $firstContext.Tenant.Id }
            [PSCustomObject]@{
                Key         = $firstContext.Tenant.Id
                Display     = "[$counter] $tenantName"
                Description = "Tenant ID: $($firstContext.Tenant.Id)"
            }
        })

        $tenantOptions += [PSCustomObject]@{
            Key         = $null
            Display     = "[$($counter + 1)] Cancel"
            Description = "Cancel and return"
        }

        $selectedTenantId = Show-RJGenericMenu -Title "Multiple Azure Tenants Found" -Prompt "Select a tenant:" -Options $tenantOptions
        if ($null -eq $selectedTenantId) {
            return $null
        }
        $tenantContexts = @($allContexts | Where-Object { $_.Tenant.Id -eq $selectedTenantId })
    }
    else {
        $selectedTenantId = $tenantGroups[0].Group[0].Tenant.Id
        $tenantContexts = @($tenantGroups[0].Group)
    }

    # Subscription selection
    if ($SubscriptionId) {
        $targetContext = $tenantContexts | Where-Object { $_.Subscription.Id -eq $SubscriptionId }
        if ($targetContext) {
            $selectedContext = $targetContext
        }
        else {
            Write-Verbose "Switching to subscription $SubscriptionId"
            Set-AzContext -SubscriptionId $SubscriptionId -ErrorAction Stop | Out-Null
            $selectedContext = Get-AzContext
        }
    }
    elseif ($tenantContexts.Count -gt 1) {
        $counter = 0
        $subOptions = @($tenantContexts | ForEach-Object {
            $counter++
            [PSCustomObject]@{
                Key         = $_.Subscription.Id
                Display     = "[$counter] $($_.Subscription.Name)"
                Description = "ID: $($_.Subscription.Id)"
            }
        })

        $subOptions += [PSCustomObject]@{
            Key         = $null
            Display     = "[$($counter + 1)] Cancel"
            Description = "Cancel and return"
        }

        $selectedSubId = Show-RJGenericMenu -Title "Multiple Azure Subscriptions Found" -Prompt "Select a subscription:" -Options $subOptions
        if ($null -eq $selectedSubId) {
            return $null
        }
        $selectedContext = $tenantContexts | Where-Object { $_.Subscription.Id -eq $selectedSubId }
    }
    else {
        $selectedContext = $tenantContexts[0]
    }

    # Set the selected context as active
    if ($selectedContext.Subscription.Id -ne $context.Subscription.Id -or $selectedContext.Tenant.Id -ne $context.Tenant.Id) {
        Set-AzContext -Context $selectedContext -ErrorAction Stop | Out-Null
        Write-Verbose "Switched to context: $($selectedContext.Subscription.Name)"
    }

    $result = [PSCustomObject]@{
        TenantId         = $selectedContext.Tenant.Id
        TenantName       = $selectedContext.Tenant.Name
        SubscriptionId   = $selectedContext.Subscription.Id
        SubscriptionName = $selectedContext.Subscription.Name
    }

    Write-Verbose "Selected tenant: $($result.TenantName) ($($result.TenantId))"
    Write-Verbose "Selected subscription: $($result.SubscriptionName) ($($result.SubscriptionId))"

    $script:RJAzureContextCache = $result
    return $result
}

function Test-ResourceGroup {
    <#
    .SYNOPSIS
    Verifies that a resource group exists.
 
    .PARAMETER ResourceGroupName
    The name of the resource group to check.
 
    .OUTPUTS
    The resource group object if it exists. Throws if not found.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$ResourceGroupName
    )

    Write-Verbose "Verifying resource group '$ResourceGroupName' exists..."

    try {
        $rg = Get-AzResourceGroup -Name $ResourceGroupName -ErrorAction Stop
        Write-Verbose "Resource group found in '$($rg.Location)'"
        return $rg
    }
    catch {
        throw "Resource group '$ResourceGroupName' not found."
    }
}

function Get-ArmTemplatePath {
    <#
    .SYNOPSIS
    Resolves the path to a JSON ARM template bundled with the module.
 
    .PARAMETER RelativePath
    Path relative to the azure-resources directory.
 
    .OUTPUTS
    Absolute path to the template file.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$RelativePath
    )

    # $PSScriptRoot points to source/Private/ when dot-sourced from the module
    # Module root is two levels up: source/Private/ -> source/ -> module root
    $moduleRoot = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent
    $templatePath = Join-Path -Path (Join-Path -Path $moduleRoot -ChildPath "azure-resources") -ChildPath $RelativePath

    if (-not (Test-Path $templatePath)) {
        throw "ARM template not found at: $templatePath"
    }

    return $templatePath
}

function New-RJArmDeployment {
    <#
    .SYNOPSIS
    Deploys a JSON ARM template to a resource group.
 
    .PARAMETER ResourceGroupName
    The target resource group.
 
    .PARAMETER TemplatePath
    Path to the JSON ARM template file.
 
    .PARAMETER DeploymentName
    Name for the deployment (for tracking in Azure portal).
 
    .PARAMETER Parameters
    Optional hashtable of template parameters.
 
    .OUTPUTS
    Hashtable of deployment outputs.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)]
        [string]$ResourceGroupName,

        [Parameter(Mandatory)]
        [string]$TemplatePath,

        [Parameter(Mandatory)]
        [string]$DeploymentName,

        [hashtable]$Parameters = @{}
    )

    Write-Verbose "Deploying '$DeploymentName' from $(Split-Path $TemplatePath -Leaf)..."

    $deployment = New-AzResourceGroupDeployment `
        -ResourceGroupName $ResourceGroupName `
        -Name $DeploymentName `
        -TemplateFile $TemplatePath `
        -TemplateParameterObject $Parameters `
        -ErrorAction Stop `
        -Verbose:($VerbosePreference -eq 'Continue') `
        -Debug:($DebugPreference -eq 'Continue')

    Write-Verbose "Deployment '$DeploymentName' succeeded"
    return $deployment.Outputs
}

function Wait-RJServicePrincipalReplication {
    <#
    .SYNOPSIS
    Waits for a service principal to replicate across Azure AD with exponential backoff.
 
    .PARAMETER ServicePrincipalId
    The object ID of the service principal to wait for.
 
    .OUTPUTS
    [bool] True if replicated, False if timed out.
    #>

    [OutputType([bool])]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$ServicePrincipalId
    )

    Write-Verbose "Waiting for service principal '$ServicePrincipalId' to replicate..."

    for ($attempt = 1; $attempt -le $ReplicationMaxAttempts; $attempt++) {
        if ($attempt -gt 1) {
            $delay = $ReplicationBaseDelaySeconds * [math]::Pow(2, $attempt - 2)  # 8, 16, 32
            Write-Verbose "Attempt $attempt/$ReplicationMaxAttempts - Waiting $delay seconds..."
            Start-Sleep -Seconds $delay
        }

        try {
            $sp = Get-AzADServicePrincipal -ObjectId $ServicePrincipalId -ErrorAction Stop
            if ($sp) {
                Write-Verbose "Service principal replicated successfully"
                return $true
            }
        }
        catch {
            if ($_.FullyQualifiedErrorId -notmatch 'Request_ResourceNotFound') {
                throw  # Unexpected error — don't retry
            }

            if ($attempt -eq $ReplicationMaxAttempts) {
                Write-Warning "Service principal not found after $ReplicationMaxAttempts attempts. Proceeding anyway..."
                return $false
            }
            Write-Verbose "Service principal not yet available. Retrying..."
        }
    }

    return $false
}

function Test-RJRoleAssignmentExist {
    <#
    .SYNOPSIS
    Checks whether a role assignment already exists for a given principal and role on a scope.
 
    .PARAMETER Scope
    The Azure resource ID to check for role assignments.
 
    .PARAMETER PrincipalId
    The object ID of the service principal.
 
    .PARAMETER RoleDefinitionId
    The GUID of the role definition (without the full resource ID prefix).
 
    .OUTPUTS
    [bool] True if the role assignment already exists.
    #>

    [OutputType([bool])]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Scope,

        [Parameter(Mandatory)]
        [string]$PrincipalId,

        [Parameter(Mandatory)]
        [string]$RoleDefinitionId
    )

    try {
        $existing = Get-AzRoleAssignment -ObjectId $PrincipalId -Scope $Scope -RoleDefinitionId $RoleDefinitionId -ErrorAction SilentlyContinue
        if ($existing) {
            Write-Verbose "Role assignment already exists for principal '$PrincipalId' with role '$RoleDefinitionId' on scope '$Scope'"
            return $true
        }
    }
    catch {
        Write-Verbose "Could not check existing role assignments: $($_.Exception.Message). Will attempt assignment."
    }

    return $false
}

function Get-RJLogAnalyticsWorkspaceByCustomerId {
    <#
    .SYNOPSIS
    Resolves a Log Analytics workspace in the selected subscription by its workspace ID (CustomerId GUID).
 
    .PARAMETER CustomerId
    The workspace ID (CustomerId GUID) of the Log Analytics workspace.
 
    .OUTPUTS
    The matching workspace resource (from Get-AzResource). Throws if none or multiple match.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$CustomerId
    )

    Write-Verbose "Resolving Log Analytics workspace with CustomerId '$CustomerId'..."
    $workspaces = @(Get-AzResource -ResourceType 'Microsoft.OperationalInsights/workspaces' -ExpandProperties -ErrorAction Stop |
            Where-Object { $_.Properties.customerId -eq $CustomerId })

    if ($workspaces.Count -eq 0) {
        throw "No Log Analytics workspace with workspace ID (CustomerId) '$CustomerId' found in subscription '$($script:RJAzureContextCache.SubscriptionName)'. Use -SubscriptionId to target a different subscription."
    }
    if ($workspaces.Count -gt 1) {
        throw "Multiple workspaces matched CustomerId '$CustomerId': $($workspaces.ResourceId -join ', ')"
    }

    Write-Verbose "Resolved workspace '$($workspaces[0].Name)' ($($workspaces[0].ResourceId))"
    return $workspaces[0]
}

function Set-RJAutomationAccountResource {
    <#
    .SYNOPSIS
    Orchestrates the full Automation Account deployment (3 steps).
 
    .DESCRIPTION
    Step 1: Configure RealmJoin Service Principal
    Step 2: Configure Automation Account with system-assigned managed identity
    Step 3: Configure permissions (Graph, Exchange, Defender, SharePoint app roles)
 
    .PARAMETER ResourceGroupName
    The target resource group (must exist).
 
    .PARAMETER AutomationAccountName
    Optional name for the automation account. Auto-generated if not provided.
 
    .OUTPUTS
    Hashtable with Success, Outputs (automationAccountId, automationAccountName, automationAccountPrincipalId, permissionCounts).
    #>

    [CmdletBinding(SupportsShouldProcess)]
    [OutputType([hashtable])]
    param(
        [Parameter(Mandatory)]
        [string]$ResourceGroupName,

        [string]$AutomationAccountName
    )

    $result = @{
        Success = $false
        Outputs = @{}
    }

    # Step 1: Configure RealmJoin Service Principal
    Write-Information "Step 1/3: Configuring RealmJoin Service Principal..."
    $spTemplatePath = Get-ArmTemplatePath -RelativePath "realmJoinServicePrincipal.json"
    $step1Outputs = New-RJArmDeployment -ResourceGroupName $ResourceGroupName `
        -TemplatePath $spTemplatePath `
        -DeploymentName "rj-serviceprincipal-$(Get-Date -Format 'yyyyMMddHHmmss')" `
        -Parameters @{ realmJoinAzureResourcesAppId = Get-RJAzureResourcesAppId }

    $rjServicePrincipalId = $step1Outputs["servicePrincipalId"].Value
    Write-Verbose "RJ Service Principal ID: $rjServicePrincipalId"

    # Wait for replication
    $null = Wait-RJServicePrincipalReplication -ServicePrincipalId $rjServicePrincipalId

    # Step 2: Configure Automation Account
    Write-Information "Step 2/3: Configuring Automation Account..."
    $aaTemplatePath = Get-ArmTemplatePath -RelativePath "azure-automation-account/automationAccount.json"
    $step2Params = @{
        rjServicePrincipalId = $rjServicePrincipalId
        contributorRoleId    = $ContributorRoleId
    }
    if ($AutomationAccountName) {
        $step2Params.automationAccountName = $AutomationAccountName
    }

    # Check if role is already assigned (avoids RoleAssignmentExists error from manual portal assignments)
    $assignContributorRole = $true
    if ($AutomationAccountName) {
        $aaScope = "/subscriptions/$($script:RJAzureContextCache.SubscriptionId)/resourceGroups/$ResourceGroupName/providers/Microsoft.Automation/automationAccounts/$AutomationAccountName"
        if (Test-RJRoleAssignmentExist -Scope $aaScope -PrincipalId $rjServicePrincipalId -RoleDefinitionId $ContributorRoleId) {
            $assignContributorRole = $false
            Write-Information " Contributor role already assigned — skipping role assignment."
        }
    }
    $step2Params.assignContributorRole = $assignContributorRole

    $step2Outputs = New-RJArmDeployment -ResourceGroupName $ResourceGroupName `
        -TemplatePath $aaTemplatePath `
        -DeploymentName "rj-automationaccount-$(Get-Date -Format 'yyyyMMddHHmmss')" `
        -Parameters $step2Params

    $automationAccountId = $step2Outputs["automationAccountId"].Value
    $automationAccountPrincipalId = $step2Outputs["automationAccountPrincipalId"].Value
    Write-Verbose "Automation Account ID: $automationAccountId"
    Write-Verbose "Managed Identity Principal ID: $automationAccountPrincipalId"

    # Wait for Managed Identity replication
    $null = Wait-RJServicePrincipalReplication -ServicePrincipalId $automationAccountPrincipalId

    # Step 3: Configure Permissions
    Write-Information "Step 3/3: Configuring permissions..."
    $permTemplatePath = Get-ArmTemplatePath -RelativePath "azure-automation-account/rjAutomationAccountPermissions.json"
    $step3Outputs = New-RJArmDeployment -ResourceGroupName $ResourceGroupName `
        -TemplatePath $permTemplatePath `
        -DeploymentName "rj-permissions-$(Get-Date -Format 'yyyyMMddHHmmss')" `
        -Parameters @{ principalId = $automationAccountPrincipalId }

    $result.Success = $true
    $result.Outputs = @{
        AutomationAccountId          = $automationAccountId
        AutomationAccountName        = $step2Outputs["automationAccountName"].Value
        AutomationAccountPrincipalId = $automationAccountPrincipalId
        ServicePrincipalId           = $rjServicePrincipalId
        GraphPermissionsCount        = $step3Outputs["graphPermissionsCount"].Value
        ExchangePermissionsCount     = $step3Outputs["exchangePermissionsCount"].Value
        DefenderPermissionsCount     = $step3Outputs["defenderPermissionsCount"].Value
        SharePointPermissionsCount   = $step3Outputs["sharePointPermissionsCount"].Value
    }

    return $result
}

function Set-RJLogAnalyticsResource {
    <#
    .SYNOPSIS
    Orchestrates the full Log Analytics Workspace deployment (2 steps).
 
    .DESCRIPTION
    Step 1: Configure RealmJoin Service Principal
    Step 2: Configure Log Analytics Workspace with conditional DCRs and custom tables
 
    .PARAMETER ResourceGroupName
    The target resource group (must exist).
 
    .PARAMETER WorkspaceName
    Optional name for the workspace. Auto-generated if not provided.
 
    .PARAMETER DeployAuditLogsDCR
    Whether to deploy the audit logs DCR. Default: true.
 
    .PARAMETER DeployRunbookLogsDCR
    Whether to deploy the runbook logs DCR. Default: false.
 
    .PARAMETER DeployOperationalLogsDCR
    Whether to deploy the operational logs DCR. Default: true.
 
    .OUTPUTS
    Hashtable with Success, Outputs (workspace info, DCR info).
    #>

    [CmdletBinding(SupportsShouldProcess)]
    [OutputType([hashtable])]
    param(
        [Parameter(Mandatory)]
        [string]$ResourceGroupName,

        [string]$WorkspaceName,

        [bool]$DeployAuditLogsDCR = $DefaultDeployAuditLogsDCR,
        [bool]$DeployRunbookLogsDCR = $DefaultDeployRunbookLogsDCR,
        [bool]$DeployOperationalLogsDCR = $DefaultDeployOperationalLogsDCR
    )

    $result = @{
        Success = $false
        Outputs = @{}
    }

    # Step 1: Configure RealmJoin Service Principal
    Write-Information "Step 1/2: Configuring RealmJoin Service Principal..."
    $spTemplatePath = Get-ArmTemplatePath -RelativePath "realmJoinServicePrincipal.json"
    $step1Outputs = New-RJArmDeployment -ResourceGroupName $ResourceGroupName `
        -TemplatePath $spTemplatePath `
        -DeploymentName "rj-serviceprincipal-$(Get-Date -Format 'yyyyMMddHHmmss')" `
        -Parameters @{ realmJoinAzureResourcesAppId = Get-RJAzureResourcesAppId }

    $rjServicePrincipalId = $step1Outputs["servicePrincipalId"].Value
    Write-Verbose "RJ Service Principal ID: $rjServicePrincipalId"

    # Wait for replication
    $null = Wait-RJServicePrincipalReplication -ServicePrincipalId $rjServicePrincipalId

    # Step 2: Configure Log Analytics Workspace
    Write-Information "Step 2/2: Configuring Log Analytics Workspace..."
    $lawTemplatePath = Get-ArmTemplatePath -RelativePath "log-analytics-workspace/logAnalyticsWorkspace.json"
    $step2Params = @{
        servicePrincipalId                = $rjServicePrincipalId
        deployAuditLogsDCR                = $DeployAuditLogsDCR
        deployRunbookLogsDCR              = $DeployRunbookLogsDCR
        deployOperationalLogsDCR          = $DeployOperationalLogsDCR
        logAnalyticsReaderRoleId          = $LogAnalyticsReaderRoleId
        monitoringMetricsPublisherRoleId  = $MonitoringMetricsPublisherRoleId
    }
    if ($WorkspaceName) {
        $step2Params.workspaceName = $WorkspaceName
    }

    # Check if roles are already assigned (avoids RoleAssignmentExists error from manual portal assignments)
    $subscriptionId = $script:RJAzureContextCache.SubscriptionId

    $assignWorkspaceRole = $true
    if ($WorkspaceName) {
        $lawScope = "/subscriptions/$subscriptionId/resourceGroups/$ResourceGroupName/providers/Microsoft.OperationalInsights/workspaces/$WorkspaceName"
        if (Test-RJRoleAssignmentExist -Scope $lawScope -PrincipalId $rjServicePrincipalId -RoleDefinitionId $LogAnalyticsReaderRoleId) {
            $assignWorkspaceRole = $false
            Write-Information " Log Analytics Reader role already assigned — skipping."
        }
    }
    $step2Params.assignWorkspaceRole = $assignWorkspaceRole

    # Check each DCR role — only for DCRs we're actually deploying
    $assignAuditLogsDcrRole = $true
    $assignRunbookLogsDcrRole = $true
    $assignOperationalLogsDcrRole = $true

    if ($DeployAuditLogsDCR -or $DeployRunbookLogsDCR -or $DeployOperationalLogsDCR) {
        $existingDcrs = Get-AzResource -ResourceGroupName $ResourceGroupName -ResourceType 'Microsoft.Insights/dataCollectionRules' -ErrorAction SilentlyContinue
        foreach ($dcr in $existingDcrs) {
            if ($dcr.Name -like 'dcr-rj-aud-logs-*' -and $DeployAuditLogsDCR) {
                if (Test-RJRoleAssignmentExist -Scope $dcr.ResourceId -PrincipalId $rjServicePrincipalId -RoleDefinitionId $MonitoringMetricsPublisherRoleId) {
                    $assignAuditLogsDcrRole = $false
                    Write-Information " Audit logs DCR role already assigned — skipping."
                }
            }
            elseif ($dcr.Name -like 'dcr-rj-rb-logs-*' -and $DeployRunbookLogsDCR) {
                if (Test-RJRoleAssignmentExist -Scope $dcr.ResourceId -PrincipalId $rjServicePrincipalId -RoleDefinitionId $MonitoringMetricsPublisherRoleId) {
                    $assignRunbookLogsDcrRole = $false
                    Write-Information " Runbook logs DCR role already assigned — skipping."
                }
            }
            elseif ($dcr.Name -like 'dcr-rj-op-logs-*' -and $DeployOperationalLogsDCR) {
                if (Test-RJRoleAssignmentExist -Scope $dcr.ResourceId -PrincipalId $rjServicePrincipalId -RoleDefinitionId $MonitoringMetricsPublisherRoleId) {
                    $assignOperationalLogsDcrRole = $false
                    Write-Information " Operational logs DCR role already assigned — skipping."
                }
            }
        }
    }
    $step2Params.assignAuditLogsDcrRole = $assignAuditLogsDcrRole
    $step2Params.assignRunbookLogsDcrRole = $assignRunbookLogsDcrRole
    $step2Params.assignOperationalLogsDcrRole = $assignOperationalLogsDcrRole

    $step2Outputs = New-RJArmDeployment -ResourceGroupName $ResourceGroupName `
        -TemplatePath $lawTemplatePath `
        -DeploymentName "rj-laworkspace-$(Get-Date -Format 'yyyyMMddHHmmss')" `
        -Parameters $step2Params

    $result.Success = $true
    $result.Outputs = @{
        ServicePrincipalId = $rjServicePrincipalId
        WorkspaceName      = $step2Outputs["workspaceName"].Value
        WorkspaceId        = $step2Outputs["workspaceId"].Value
        CustomerId         = $step2Outputs["customerId"].Value
        # Audit Logs DCR
        AuditLogsTableName            = $step2Outputs["auditLogsTableName"].Value
        AuditLogsStreamName           = $step2Outputs["auditLogsStreamName"].Value
        AuditLogsIngestionEndpoint    = $step2Outputs["auditLogsIngestionEndpoint"].Value
        AuditLogsDcrImmutableId       = $step2Outputs["auditLogsDcrImmutableId"].Value
        AuditLogsDcrId                = $step2Outputs["auditLogsDcrId"].Value
        AuditLogsDcrName              = $step2Outputs["auditLogsDcrName"].Value
        # Runbook Logs DCR
        RunbookLogsTableName          = $step2Outputs["runbookLogsTableName"].Value
        RunbookLogsStreamName         = $step2Outputs["runbookLogsStreamName"].Value
        RunbookLogsIngestionEndpoint  = $step2Outputs["runbookLogsIngestionEndpoint"].Value
        RunbookLogsDcrImmutableId     = $step2Outputs["runbookLogsDcrImmutableId"].Value
        RunbookLogsDcrId              = $step2Outputs["runbookLogsDcrId"].Value
        RunbookLogsDcrName            = $step2Outputs["runbookLogsDcrName"].Value
        # Operational Logs DCR
        OperationalLogsTableName          = $step2Outputs["operationalLogsTableName"].Value
        OperationalLogsStreamName         = $step2Outputs["operationalLogsStreamName"].Value
        OperationalLogsIngestionEndpoint  = $step2Outputs["operationalLogsIngestionEndpoint"].Value
        OperationalLogsDcrImmutableId     = $step2Outputs["operationalLogsDcrImmutableId"].Value
        OperationalLogsDcrId              = $step2Outputs["operationalLogsDcrId"].Value
        OperationalLogsDcrName            = $step2Outputs["operationalLogsDcrName"].Value
    }

    return $result
}