output/20260723_081532/govviz/ALZ_20260723_081601/Enterprise-Scale/src/Alz.Tools/functions/Alz.Tools.ps1

#!/usr/bin/pwsh

using module "../Alz.Enums/"
using module "../Alz.Classes/"

###############################################
# Configure PSScriptAnalyzer rule suppression #
###############################################

# The following SuppressMessageAttribute entries are used to surpress
# PSScriptAnalyzer tests against known exceptions as per:
# https://github.com/powershell/psscriptanalyzer#suppressing-rules
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'Function targets multiple line endings', Scope = 'Function', Target = 'Edit-LineEndings')]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Function does not change system state', Scope = 'Function', Target = 'Remove-Escaping')]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'SleepForSeconds', Justification = 'Used in child process', Scope = 'Function', Target = 'Invoke-RemoveMgHierarchy')]
param ()

#######################################
# Common variables used within module #
#######################################

[Int]$jsonDepth = 100

[Regex]$regex_schema_deploymentParameters = "http[s]?:\/\/schema\.management\.azure\.com\/schemas\/([0-9-]{10})\/deploymentParameters\.json#"
[Regex]$regex_schema_managementGroupDeploymentTemplate = "http[s]?:\/\/schema\.management\.azure\.com\/schemas\/([0-9-]{10})\/managementGroupDeploymentTemplate\.json#"
[Regex]$regex_firstLeftSquareBrace = "(?<=`")(\[)"
[Regex]$regex_escapedLeftSquareBrace = "(?<=`")(\[\[)"
[Regex]$regex_subscriptionAlias = "(?<prefix>[\w-]+?)-(?<scope>\w+)-?(?<secondOctet>[1-2]?[0-9]?[0-9])?$"

[String[]]$allowedResourceTypes = @(
    "Microsoft.Authorization/policyAssignments"
    "Microsoft.Authorization/policyDefinitions"
    "Microsoft.Authorization/policySetDefinitions"
    "Microsoft.Authorization/roleAssignments"
    "Microsoft.Authorization/roleDefinitions"
    "Microsoft.Management/managementGroups"
    "Microsoft.Management/managementGroups/subscriptions"
)

[String[]]$removePolicyEscapingByFormat = @(
    "Terraform"
    "Bicep"
)

[String[]]$removePolicySetEscapingByFormat = @(
    "Terraform"
)

[String[]]$removeResourceEscapingByFormat = @(
    "Terraform"
)

################################
# Functions used within module #
################################

function ProcessObjectByResourceType {
    [CmdletBinding()]
    [OutputType([Object])]
    param (
        [Parameter()][Object]$ResourceObject,
        [Parameter()][String]$ResourceType
    )
    try {
        switch ($ResourceType.ToLower()) {
            "microsoft.authorization/policyassignments" {
                $outputObject = [PolicyAssignment]::new($ResourceObject)
            }
            "microsoft.authorization/policydefinitions" {
                $outputObject = [PolicyDefinition]::new($ResourceObject)
            }
            "microsoft.authorization/policysetdefinitions" {
                $outputObject = [PolicySetDefinition]::new($ResourceObject)
                # Workaround for policySetDefinitions that only have a single policyDefinition. PowerShell tires to convert to an object in that scenario.
                if($outputObject.properties.policyDefinitions.GetType().ToString() -eq "PolicySetDefinitionPropertiesPolicyDefinitions") {
                    $outputObject.properties.policyDefinitions = @($outputObject.properties.policyDefinitions)
                }
            }
            "microsoft.authorization/roleassignments" {
                $outputObject = [RoleAssignment]::new($ResourceObject)
            }
            "microsoft.authorization/roledefinitions" {
                $outputObject = [RoleDefinition]::new($ResourceObject)
            }
            Default {
                Write-Warning "Unsupported resource type: $($ResourceType)"
                $outputObject = $ResourceObject
            }
        }
    }
    catch [System.Management.Automation.RuntimeException] {
        Write-Error $_.Exception.Message
    }

    return $outputObject

}

function Add-Escaping {
    [CmdletBinding()]
    param (
        [Parameter()][Object]$InputObject
    )

    # A number of sources store the required definition in variables
    # which use escaping for ARM functions so they are correctly
    # processed within copy_loops. These may need to be added when
    # converting from a native ARM template.
    $output = $InputObject |
    ConvertTo-Json -Depth $jsonDepth |
    ForEach-Object { $_ -replace $regex_firstLeftSquareBrace, "[[" } |
    ConvertFrom-Json

    return $output
}

function Remove-Escaping {
    [CmdletBinding()]
    param (
        [Parameter()][Object]$InputObject
    )

    # A number of sources store the required definition in variables
    # which use escaping for ARM functions so they are correctly
    # processed within copy_loops. These may need to be removed when
    # converting to a native ARM template.
    $output = $InputObject |
    ConvertTo-Json -Depth $jsonDepth |
    ForEach-Object { $_ -replace $regex_escapedLeftSquareBrace, "[" } |
    ConvertFrom-Json

    return $output
}

function GetObjectByResourceTypeFromJson {
    [CmdletBinding()]
    [OutputType([Object])]
    param (
        [Parameter()][String]$Id,
        [Parameter()][String[]]$InputJSON,
        [Parameter()][ExportFormat]$ExportFormat
    )

    # Try catch is used to gracefully handle type conversion errors when the input contains invalid JSON
    try {
        $objectFromJson = $InputJSON | ConvertFrom-Json -ErrorAction Stop
    }
    catch {
        throw $_.Exception.Message
    }

    # The following block handles processing files in the format generated by the AzOps output
    # e.g. azopsreference/ folder in Azure/Enterprise-Scale repository
    if ($regex_schema_deploymentParameters.IsMatch($objectFromJson."`$schema")) {
        if ($objectFromJson.parameters.input.value.ResourceType) {
            ProcessObjectByResourceType `
                -ResourceObject ($objectFromJson.parameters.input.value) `
                -ResourceType ($objectFromJson.parameters.input.value.ResourceType)
        }
    }
    # The following block handles processing files in the format used by the ALZ reference deployments
    # e.g. eslzArm/managementGroupTemplates/policyDefinitions/ folder in Azure/Enterprise-Scale repository
    elseif ($regex_schema_managementGroupDeploymentTemplate.IsMatch($objectFromJson."`$schema")) {
        foreach ($policyDefinition in $objectFromJson.variables.policies.policyDefinitions) {
            ProcessObjectByResourceType `
                -ResourceObject ($ExportFormat -in $removePolicyEscapingByFormat ? (Remove-Escaping -InputObject $policyDefinition) : $policyDefinition) `
                -ResourceType ("Microsoft.Authorization/policyDefinitions")
        }
        foreach ($policySetDefinition in $objectFromJson.variables.initiatives.policySetDefinitions) {
            ProcessObjectByResourceType `
                -ResourceObject ($ExportFormat -in $removePolicySetEscapingByFormat ? (Remove-Escaping -InputObject $policySetDefinition) : $policySetDefinition) `
                -ResourceType ("Microsoft.Authorization/policySetDefinitions")
        }
        foreach (
            $policyDefinition in $objectFromJson.resources |
            Where-Object { $_.type -eq "Microsoft.Authorization/policyDefinitions" } |
            Where-Object { $_.name -ne "[variables('policies').policyDefinitions[copyIndex()].name]" }
        ) {
            ProcessObjectByResourceType `
                -ResourceObject ($ExportFormat -in $removePolicyEscapingByFormat ? (Remove-Escaping -InputObject $policyDefinition) : $policyDefinition) `
                -ResourceType ("Microsoft.Authorization/policyDefinitions")
        }
        foreach (
            $policySetDefinition in $objectFromJson.resources |
            Where-Object { $_.type -eq "Microsoft.Authorization/policySetDefinitions" } |
            Where-Object { $_.name -ne "[variables('initiatives').policySetDefinitions[copyIndex()].name]" }
        ) {
            ProcessObjectByResourceType `
                -ResourceObject ($ExportFormat -in $removePolicySetEscapingByFormat ? (Remove-Escaping -InputObject $policySetDefinition) : $policySetDefinition) `
                -ResourceType ("Microsoft.Authorization/policySetDefinitions")
        }
    }
    # The following elseif block handles all policy definitions stored in ARM template format
    elseif ($objectFromJson.type -eq "Microsoft.Authorization/policyDefinitions") {
        ProcessObjectByResourceType `
            -ResourceObject ($ExportFormat -in $removePolicyEscapingByFormat ? (Remove-Escaping -InputObject $objectFromJson) : $objectFromJson) `
            -ResourceType $objectFromJson.type
    }
    # The following elseif block handles all policy set definitions stored in ARM template format
    elseif ($objectFromJson.type -eq "Microsoft.Authorization/policySetDefinitions") {
        ProcessObjectByResourceType `
            -ResourceObject ($ExportFormat -in $removePolicySetEscapingByFormat ? (Remove-Escaping -InputObject $objectFromJson) : $objectFromJson) `
            -ResourceType $objectFromJson.type
    }
    # The following elseif block handles all other allowed resource types stored in ARM template format
    elseif ($objectFromJson.type -in $allowedResourceTypes) {
        ProcessObjectByResourceType `
            -ResourceObject ($ExportFormat -in $removeResourceEscapingByFormat ? (Remove-Escaping -InputObject $objectFromJson) : $objectFromJson) `
            -ResourceType $objectFromJson.type
    }
    # The following block handles processing generic files where the source content is unknown
    # High probability of incorrect format if this happens.
    else {
        Write-Warning "Unable to find converter for input object: $Id"
    }

}

function ProcessFile {
    [CmdletBinding()]
    param (
        [Parameter()][String]$FilePath,
        [Parameter()][ExportFormat]$ExportFormat
    )

    $content = Get-Content -Path $FilePath

    $output = GetObjectByResourceTypeFromJson `
        -Id $FilePath `
        -InputJSON $content `
        -ExportFormat $ExportFormat

    return $output
}

function Invoke-UseCacheFromModule {
    param (
        [String]$Directory = "./"
    )
    [ProviderApiVersions]::LoadCacheFromDirectory($Directory)
}

function Invoke-UpdateCacheInModule {
    param (
        [String]$Directory = "./"
    )
    [ProviderApiVersions]::SaveCacheToDirectory($Directory)
}

function Edit-LineEndings {
    [CmdletBinding()]
    [OutputType([String[]])]
    param (
        [Parameter(ValueFromPipeline = $true)]
        [String[]]$InputText,
        [Parameter()][LineEndingTypes]$LineEnding = "Unix"
    )

    Begin {

        Switch ("$LineEnding".ToLower()) {
            "darwin" { $eol = "`r" }
            "unix" { $eol = "`n" }
            "win" { $eol = "`r`n" }
        }

    }

    Process {

        [String[]]$outputText += $InputText |
        ForEach-Object { $_ -replace "`r`n", "`n" } |
        ForEach-Object { $_ -replace "`r", "`n" } |
        ForEach-Object { $_ -replace "`n", "$eol" }

    }

    End {

        return $outputText

    }

}

function ConvertTo-ArmTemplateResource {
    [CmdletBinding()]
    param (
        [Parameter()][String]$FilePath,
        [Parameter()][ExportFormat]$ExportFormat = "Raw",
        [Parameter()][Switch]$AsJson
    )

    $content = ProcessFile `
        -FilePath $FilePath `
        -ExportFormat $ExportFormat

    $output = $content.Format($ExportFormat)

    if ($AsJson) {
        return $output | ConvertTo-Json -Depth $jsonDepth
    }
    else {
        return $output
    }

}

function ConvertTo-LibraryArtifact {
    [CmdletBinding()]
    param (
        [Parameter()][String[]]$InputPath,
        [Parameter()][String]$InputFilter = "*.json",
        [Parameter()][String]$OutputPath = "./",
        [Parameter()][String]$FileNamePrefix = "",
        [Parameter()][String]$FileNameSuffix = ".json",
        [Parameter()][ExportFormat]$ExportFormat = "Raw",
        [Parameter()][Switch]$Recurse
    )
    $inputFiles = foreach ($path in $InputPath) {
        Get-ChildItem -Path $path -Recurse:$Recurse -Filter $InputFilter
    }

    [Object[]]$outputItems = foreach ($inputFile in $inputFiles) {
        $content = ProcessFile `
            -FilePath $inputFile.FullName `
            -ExportFormat $ExportFormat
        foreach ($item in $content | Where-Object { $_ }) {
            [PSCustomObject]@{
                InputFilePath  = $inputFile.FullName
                OutputFilePath = ($OutputPath + "/" + $item.GetFileName($FileNamePrefix, $FileNameSuffix, $ExportFormat)) -replace "//", "/"
                OutputTemplate = $item.Format($ExportFormat)
            }
        }
    }

    return $outputItems

}

function Export-LibraryArtifact {
    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter()][String[]]$InputPath,
        [Parameter()][String]$InputFilter = "*.json",
        [ValidateScript({ $_.foreach({ $_ -in $allowedResourceTypes }) })]
        [Parameter()][String[]]$ResourceTypeFilter = @(),
        [Parameter()][String]$OutputPath = "./",
        [Parameter()][String]$FileNamePrefix = "",
        [Parameter()][String]$FileNameSuffix = ".json",
        [Parameter()][LineEndingTypes]$LineEnding = "Unix",
        [Parameter()][ExportFormat]$ExportFormat = "Raw",
        [Parameter()][Switch]$Recurse
    )

    $libraryArtifacts = ConvertTo-LibraryArtifact `
        -InputPath $InputPath `
        -InputFilter $InputFilter `
        -OutputPath $OutputPath `
        -FileNamePrefix $FileNamePrefix `
        -FileNameSuffix $FileNameSuffix `
        -ExportFormat $ExportFormat `
        -Recurse:$Recurse

    if ($ResourceTypeFilter.Length -eq 0) {
        Write-Verbose "Using default ResourceTypeFilter. Will process all valid resource types."
        $ResourceTypeFilter = [ProviderApiVersions]::ListTypes()
    }
    else {
        Write-Verbose "Using custom ResourceTypeFilter. Will process the following resource types:`n $($ResourceTypeFilter.foreach({" - " + $_ +"`n"}))"
    }

    foreach ($libraryArtifact in $libraryArtifacts) {
        $libraryArtifactMessage = ("Processing file... `n" + `
                " - Input : $($libraryArtifact.InputFilePath) `n" + `
                " - Output : $($libraryArtifact.OutputFilePath)")

        if ($libraryArtifact.OutputTemplate.type -in $ResourceTypeFilter) {
            if ($PSCmdlet.ShouldProcess($libraryArtifact.OutputFilePath)) {
                $libraryArtifactFile = $libraryArtifact.OutputTemplate |
                ConvertTo-Json -Depth $jsonDepth |
                Edit-LineEndings -LineEnding $LineEnding |
                New-Item -Path $libraryArtifact.OutputFilePath -ItemType File -Force
                $libraryArtifactMessage += "`n [COMPLETE]"
                Write-Verbose $libraryArtifactMessage
                Write-Information "Output File : $($libraryArtifactFile.FullName) [COMPLETE]" -InformationAction Continue
            }
        }
        else {
            $libraryArtifactMessage += "`n [SKIPPING] Resource Type not in ResourceTypeFilter."
            Write-Verbose $libraryArtifactMessage
        }
    }
}

function Set-AzureSubscriptionAlias {
    [CmdletBinding(SupportsShouldProcess)]
    [OutputType([Object[]])]
    param (
        [Parameter(Mandatory = $true, ParameterSetName = 'PutAliasWithBillingScope')]
        [Parameter(Mandatory = $true, ParameterSetName = 'PutAliasWithSubscriptionId')]
        [Parameter(Mandatory = $true, ParameterSetName = 'GetAliasOnly')]
        [String[]]$Alias,
        [Parameter(Mandatory = $true, ParameterSetName = 'PutAliasWithBillingScope')]
        [String]$BillingScope,
        [Parameter(Mandatory = $true, ParameterSetName = 'PutAliasWithSubscriptionId')]
        [String]$SubscriptionId,
        [Parameter(Mandatory = $false, ParameterSetName = 'PutAliasWithBillingScope')]
        [String]$Workload = "Production",
        [Parameter(Mandatory = $false, ParameterSetName = 'PutAliasWithBillingScope')]
        [Parameter(Mandatory = $false, ParameterSetName = 'PutAliasWithSubscriptionId')]
        [Parameter(Mandatory = $false, ParameterSetName = 'GetAliasOnly')]
        [Switch]$SetParentManagementGroup,
        [Parameter(Mandatory = $false, ParameterSetName = 'PutAliasWithBillingScope')]
        [Parameter(Mandatory = $false, ParameterSetName = 'PutAliasWithSubscriptionId')]
        [Parameter(Mandatory = $false, ParameterSetName = 'GetAliasOnly')]
        [Switch]$SetAddressPrefix
    )

    # Get the latest stable API version
    $aliasesApiVersion = [ProviderApiVersions]::GetLatestStableByType("Microsoft.Subscription/aliases")
    Write-Information "Using Subscription Alias API Version : $($aliasesApiVersion)" -InformationAction Continue

    # Logic to determine whether to GET an existing Alias or PUT a new one
    $GetExistingAlias = [string]::IsNullOrEmpty($BillingScope) -and [string]::IsNullOrEmpty($SubscriptionId)
    $requestMethod = $GetExistingAlias ? "GET" : "PUT"

    # Process Alias value(s)
    $aliasResponses = @()
    foreach ($subscriptionName in $Alias) {
        Write-Verbose "Microsoft.Subscription/aliases/$($subscriptionName) [$requestMethod]"
        $requestPath = "/providers/Microsoft.Subscription/aliases/$($subscriptionName)?api-version=$($aliasesApiVersion)"
        if (-not [string]::IsNullOrEmpty($BillingScope)) {
            $action = "PutAliasWithBillingScope"
            $requestBodyObject = @{
                properties = @{
                    displayName          = $subscriptionName
                    billingScope         = $BillingScope
                    workload             = $Workload
                    additionalProperties = @{}
                }
            }
        }
        elseif (-not [string]::IsNullOrEmpty($SubscriptionId)) {
            $action = "PutAliasWithSubscriptionId"
            $requestBodyObject = @{
                properties = @{
                    subscriptionId       = $SubscriptionId
                    additionalProperties = @{}
                }
            }
        }
        else {
            $action = "GetAliasOnly"
            $requestBodyObject = @{}
        }
        $requestBody = $requestBodyObject | ConvertTo-Json -Depth $jsonDepth
        if ($PSCmdlet.ShouldProcess("$subscriptionName", "$action")) {
            $aliasResponse = Invoke-AzRestMethod -Method $requestMethod -Path $requestPath -Payload $requestBody
        }
        else {
            $aliasResponse = [ordered]@{
                StatusCode = "200 (WHAT IF)"
                Method     = "GET (WHAT IF)"
                Content    = [ordered]@{
                    id         = "/providers/Microsoft.Subscription/aliases/$subscriptionName"
                    name       = "$subscriptionName"
                    type       = "Microsoft.Subscription/aliases"
                    properties = [ordered]@{
                        subscriptionId    = "00000000-0000-0000-0000-000000000000"
                        provisioningState = "Succeeded"
                    }
                } | ConvertTo-Json -Depth $jsonDepth
            }
        }
        $aliasResponses += $aliasResponse
        Write-Verbose "Microsoft.Subscription/aliases/$($subscriptionName) [$($aliasResponse.StatusCode)]"
    }

    # For newly created Subscriptions, wait until all return StatusCode 200 and Provisioning State Succeeded
    $aliasResponses | Where-Object -Property StatusCode -EQ "201" | ForEach-Object {
        $aliasResponseContent = $_.Content | ConvertFrom-Json
        $retryCount = 0
        $SleepSeconds = 1
        do {
            $retryCount++
            $aliasResponse = Invoke-AzRestMethod -Method GET -Path "$($aliasResponseContent.id)?api-version=$($aliasesApiVersion)"
            $aliasResponseContent = $aliasResponse.Content | ConvertFrom-Json
            $subscriptionId = $aliasResponseContent.properties.subscriptionId
            $provisioningState = $aliasResponseContent.properties.provisioningState
            Write-Verbose "(Retry=$retryCount) $($aliasResponseContent.id) [$($aliasResponse.StatusCode)] [$($subscriptionId)] [$($provisioningState)]"
            if (($aliasResponse.StatusCode -eq "200") -and ($provisioningState -eq "Succeeded")) {
                $endLoop = $true
            }
            else {
                Start-Sleep -Seconds $SleepSeconds
                $SleepSeconds = 2 * $SleepSeconds
            }
            if ($retryCount -eq 10) {
                $endLoop = $true
            }
        } until ($endLoop)
    }

    # Add each subscription to the return object
    $subscriptions = @()
    $aliasResponses | ForEach-Object {
        if ($aliasResponse.StatusCode -eq "201") {
            $status = "NEW"
        }
        elseif ($aliasResponse.StatusCode -eq "200") {
            $status = "EXISTING"
        }
        elseif ($aliasResponse.StatusCode -eq "200 (WHAT IF)") {
            $status = "WHAT IF"
        }
        else {
            $status = "UNKNOWN" # Consider whether to throw an error here
        }
        $subscription = $_.Content | ConvertFrom-Json
        $subscriptions += $subscription
        Write-Information "[$status] Subscription Alias : $($subscription.name) [$($subscription.properties.subscriptionId)]" -InformationAction Continue
    }

    # Determine the parent management group if SetParentManagementGroup is specified
    if ($SetParentManagementGroup) {
        foreach ($subscription in $subscriptions) {
            $scope = $regex_subscriptionAlias.Matches($subscription.name)[0].Groups['scope'].Value
            Write-Information "Set parent management group : $($subscription.name) [$scope]" -InformationAction Continue
            $subscription | Add-Member -Type NoteProperty -Name parentManagementGroup -Value $scope
        }
    }

    # Determine the assigned address prefix if SetAddressPrefix is specified
    if ($SetAddressPrefix) {
        $secondOctetFallback = 100
        $secondOctetLog = @()
        foreach ($subscription in $subscriptions) {
            $secondOctetValue = $regex_subscriptionAlias.Matches($subscription.name)[0].Groups['secondOctet'].Value
            $secondOctet = [string]::IsNullOrEmpty($secondOctetValue) ? $secondOctetFallback : $secondOctetValue
            if ($secondOctet -in $secondOctetLog) {
                throw "Overlapping address space (at secondOctet) detected."
            }
            if ($secondOctet -in $secondOctetFallback) {
                $secondOctetFallback += 10
            }
            $addressPrefix = "10.$secondOctet.0.0/24"
            Write-Information "Set address prefix : $($subscription.name) [$addressPrefix]" -InformationAction Continue
            $subscription | Add-Member -Type NoteProperty -Name addressPrefix -Value $addressPrefix
        }
    }

    return $subscriptions

}

function Invoke-RemoveRsgByPattern {
    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter()][String[]]$SubscriptionId,
        [Parameter()][String]$Like
    )

    $originalCtx = Get-AzContext

    $WhatIfPrefix = ""
    if ($WhatIfPreference) {
        $WhatIfPrefix = "What if: "
    }

    $jobs = @()
    foreach ($subId in $SubscriptionId) {
        Set-AzContext -SubscriptionId $subId -WhatIf:$false | Out-Null

        $resourcesGroups = Get-AzResourceGroup |  Where-Object -Property "ResourceGroupName" -Like $Like

        Write-Information "$($WhatIfPrefix)Deleting [$($resourcesGroups.Length)] Resource Groups for Subscription [$($subId)] matching pattern [$($Like)]" -InformationAction Continue

        if ($resourcesGroups.Length -gt 0) {
            if ($PSCmdlet.ShouldProcess($($resourcesGroups.ResourceGroupName | ConvertTo-Json -Compress), "Remove-AzResourceGroup")) {
                $jobs += $resourcesGroups | Remove-AzResourceGroup -AsJob -Force
            }
        }

    }

    Set-AzContext $originalCtx -WhatIf:$false | Out-Null

    return $jobs

}

function Invoke-RemoveDeploymentByPattern {
    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter()][String[]]$SubscriptionId,
        [Parameter()][String[]]$ManagementGroupId,
        [Parameter()][String]$Like,
        [Parameter()][Switch]$IncludeTenantScope
    )

    $originalCtx = Get-AzContext

    $WhatIfPrefix = ""
    if ($WhatIfPreference) {
        $WhatIfPrefix = "What if: "
    }

    $jobs = @()

    foreach ($subId in $SubscriptionId) {
        Set-AzContext -SubscriptionId $subId -WhatIf:$false | Out-Null

        $deployments = Get-AzSubscriptionDeployment |  Where-Object -Property "DeploymentName" -Like $Like

        Write-Information "$($WhatIfPrefix)Deleting [$($deployments.Length)] Deployments for Subscription [$($subId)] matching pattern [$($Like)]" -InformationAction Continue

        if ($deployments.Length -gt 0) {
            if ($PSCmdlet.ShouldProcess($($deployments.DeploymentName | ConvertTo-Json -Compress), "Remove-AzSubscriptionDeployment")) {
                $jobs += $deployments | Remove-AzSubscriptionDeployment -AsJob
            }
        }

    }

    foreach ($mgId in $ManagementGroupId) {
        $deployments = Get-AzManagementGroupDeployment -ManagementGroupId $mgId |  Where-Object -Property "DeploymentName" -Like $Like

        Write-Information "$($WhatIfPrefix)Deleting [$($deployments.Length)] Deployments for Management Group [$($mgId)] matching pattern [$($Like)]" -InformationAction Continue

        if ($deployments.Length -gt 0) {
            if ($PSCmdlet.ShouldProcess($($deployments.DeploymentName | ConvertTo-Json -Compress), "Remove-AzManagementGroupDeployment")) {
                $jobs += $deployments | Remove-AzManagementGroupDeployment -AsJob
            }
        }

    }

    if ($IncludeTenantScope) {
        $deployments = Get-AzTenantDeployment |  Where-Object -Property "DeploymentName" -Like $Like

        Write-Information "$($WhatIfPrefix)Deleting [$($deployments.Length)] Deployments for Tenant [$($originalCtx.Tenant.Id)] matching pattern [$($Like)]" -InformationAction Continue

        if ($deployments.Length -gt 0) {
            if ($PSCmdlet.ShouldProcess($($deployments.DeploymentName | ConvertTo-Json -Compress), "Remove-AzTenantDeployment")) {
                $jobs += $deployments | Remove-AzTenantDeployment -AsJob
            }
        }

    }

    Set-AzContext $originalCtx -WhatIf:$false | Out-Null

    return $jobs

}

function Invoke-RemoveOrphanedRoleAssignment {
    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter()][String[]]$SubscriptionId
    )

    $originalCtx = Get-AzContext

    $WhatIfPrefix = ""
    if ($WhatIfPreference) {
        $WhatIfPrefix = "What if: "
    }

    # Get the latest stable API version
    $roleAssignmentsApiVersion = [ProviderApiVersions]::GetLatestStableByType("Microsoft.Authorization/roleAssignments")
    Write-Information "Using Role Assignments API Version : $($roleAssignmentsApiVersion)" -InformationAction Continue

    foreach ($subId in $SubscriptionId) {

        # Use Rest API to ensure correct permissions are assigned when looking up
        # whether identity exists, otherwise Get-AzRoleAssignment will always
        # return `objectType : "unknown"` for all assignments with no errors.

        # Get Role Assignments
        $getRequestPath = "/subscriptions/$($subId)/providers/Microsoft.Authorization/roleAssignments?api-version=$($roleAssignmentsApiVersion)"
        $getResponse = Invoke-AzRestMethod -Method "GET" -Path $getRequestPath
        $roleAssignments = ($getResponse.Content | ConvertFrom-Json).value

        # Check for valid response
        if ($getResponse.StatusCode -ne "200") {
            throw $getResponse.Content
        }
        try {
            # If invalid response, $roleAssignments will be null and throw an error
            $roleAssignments.GetType() | Out-Null
        }
        catch {
            throw $getResponse.Content
        }

        # Get a list of assigned principalId values and lookup against Microsoft Entra ID
        $principalsRequestUri = "https://graph.microsoft.com/v1.0/directoryObjects/microsoft.graph.getByIds"
        $principalsRequestBody = @{
            ids = $roleAssignments.properties.principalId
        } | ConvertTo-Json -Depth $jsonDepth
        $principalsResponse = Invoke-AzRestMethod -Method "POST" -Uri $principalsRequestUri -Payload $principalsRequestBody -WhatIf:$false
        $principalIds = ($principalsResponse.Content | ConvertFrom-Json).value.id

        # Find all Role Assignments where the principalId is not found in Microsoft Entra ID
        $orphanedRoleAssignments = $roleAssignments | Where-Object {
            ($_.properties.scope -eq "/subscriptions/$($subId)") -and
            ($_.properties.principalId -notin $principalIds)
        }

        # Delete orphaned Role Assignments
        Write-Information "$($WhatIfPrefix)Deleting [$($orphanedRoleAssignments.Length)] orphaned Role Assignments for Subscription [$($subId)]" -InformationAction Continue
        $orphanedRoleAssignments | ForEach-Object {
            if ($PSCmdlet.ShouldProcess("$($_.id)", "Remove-AzRoleAssignment")) {
                $deleteRequestPath = "$($_.id)?api-version=$($roleAssignmentsApiVersion)"
                $deleteResponse = Invoke-AzRestMethod -Method "DELETE" -Path $deleteRequestPath
                # Check for valid response
                if ($deleteResponse.StatusCode -ne "200") {
                    throw $deleteResponse.Content
                }
            }
        }

    }

    Set-AzContext $originalCtx -WhatIf:$false | Out-Null

}

function Invoke-RemoveMgHierarchy {
    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter()][String[]]$ManagementGroupId
    )

    $InvokeRemoveMgHierarchy = ${function:Invoke-RemoveMgHierarchy}.ToString()

    $ctx = Get-AzContext

    $WhatIfPrefix = ""
    if ($WhatIfPreference) {
        $WhatIfPrefix = "What if: "
    }

    # Get list of existing Management Groups
    $managementGroupIds = (Get-AzManagementGroup).Name

    Write-Information ("$($WhatIfPrefix)Removing Management Group Hierarchy in batch: {0}" -f $($ManagementGroupId | ConvertTo-Json -Compress)) -InformationAction Continue

    # Log warning for non-existing Management Group
    $ManagementGroupId | Where-Object { $_ -notin $managementGroupIds } | ForEach-Object {
        Write-Warning "'/providers/Microsoft.Management/managementGroups/$_' not found"
    }

    # Process existing Management Groups
    $ManagementGroupId | Where-Object { $_ -in $managementGroupIds } | ForEach-Object -Parallel {

        # Set WhatIfPreference from parent session
        $WhatIfPreference = $using:WhatIfPreference

        # Parse functions from parent session
        ${function:Invoke-RemoveMgHierarchy} = $using:InvokeRemoveMgHierarchy

        # Set Azure context from parent session
        Set-AzContext -Context $using:ctx | Out-Null

        # Get expanded properties of current Management Group
        $managementGroup = Get-AzManagementGroup -GroupId $_ -Expand -WarningAction SilentlyContinue

        # Process child Subscriptions under the current Management Group scope
        $childSubs = ($managementGroup.Children | Where-Object { $_.Type -eq "/subscriptions" }).Name
        foreach ($childSub in $childSubs) {
            Remove-AzManagementGroupSubscription -SubscriptionId $childSub -GroupName $managementGroup.Name -WhatIf:$WhatIfPreference -WarningAction SilentlyContinue
            Write-Output "/subscriptions/$childSub"
        }

        # Process child Management Groups under the current Management Group scope
        $childMgs = ($managementGroup.Children | Where-Object { $_.Type -match "^(\/providers\/)?(Microsoft\.Management\/managementGroups)$" }).Name
        if ($childMgs.Length -gt 0) {
            Invoke-RemoveMgHierarchy -ManagementGroupId $childMgs
        }

        Remove-AzManagementGroup -GroupId $_ -WhatIf:$WhatIfPreference -WarningAction SilentlyContinue | Out-Null

        Write-Output "/providers/Microsoft.Management/managementGroups/$_"

    }

}