internal/functions/Get-AzAssignmentsAtScopeRecursive.ps1

#Requires -PSEdition Core

function Add-Assignments {
    param (
        [array]     $assignmentList,
        [string]    $header,
        [string]    $scope,
        [hashtable] $allPolicyDefinitions,
        [hashtable] $allPolicySetDefinitions,
        [bool]      $getAssignments,
        [bool]      $getRemediations,
        [hashtable] $assignments,
        [hashtable] $remediations,
        [bool]      $suppressRoleAssignments
    )

    #region $maybeRemediations
    $maybeRemediations = $false
    $complianceStateSummaryCollection = $null
    if ($getRemediations) {
        foreach ($assignment in $assignmentList) {
            if ($null -ne $assignment.identity -and $null -ne $assignment.identity.principalId) {
                $maybeRemediations = $true
                break
            }
        }
        if ($maybeRemediations) {
            # This call is expensive, only issue if at least one assignment may need a remediation
            # - especially true for subscription-level assignments
            $complianceStateSummary = (Invoke-AzCli policy state summarize -assignmentScopeId $scope `
                    --filter "`"(policyDefinitionAction eq 'deployifnotexists' or policyDefinitionAction eq 'modify')`"" `
                    -AsHashTable)

            if ($null -ne $complianceStateSummary) {
                $complianceStateSummaryCollection = $complianceStateSummary.policyAssignments
                if ($complianceStateSummary.Length -eq 0) {
                    $maybeRemediations = $false
                }
            }
            else {
                $maybeRemediations = $false
            }
        }
    }
    # policyresources
    # | where type == "microsoft.policyinsights/policystates"
    # | where properties.complianceState == "NonCompliant" and properties.policyDefinitionAction in ( "modify", "deployifnotexists" )
    # |

    # policyresources
    # | where type == "microsoft.policyinsights/policystates"
    # | where properties.complianceState == "NonCompliant" and properties.policyDefinitionAction in ( "modify", "deplyifnotecists" )
    # | summarize count() by tostring(properties.policyAssignmentId), tostring(properties.policyDefinitionAction), tostring(properties.policyDefinitionReferenceId)
    # | order by properties_policyAssignmentId asc
    #endregion

    $numberOfAssignmentsWithRemediations = 0 # How many assignment requiring remediation(s) are at this scope
    $headerDisplayed = $false
    foreach ($assignment in $assignmentList) {

        #region Assignment details and Role Assignments
        if ($getAssignments) {
            if (-not $headerDisplayed) {
                Write-Information $header
                $headerDisplayed = $true
            }
            $existingRoleAssignments = @()
            if (-not $suppressRoleAssignments -and $null -ne $assignment.identity -and $null -ne $assignment.identity.principalId) {
                # Collate existing role assignments at Policy assignment scope and at additionalRoleAssignments scope(s)
                $existingRoleAssignments = @() + (Invoke-AzCli role assignment list --scope $scope --assignee $assignment.identity.principalId --only-show-errors)
                $principalId = $assignment.identity.principalId
                $scopesChecked = @{
                    $assignment.scope = $true
                }
                if ($assignment.metadata -and $assignment.metadata.roles) {
                    foreach ($role in $assignment.metadata.roles) {
                        if (-not $scopesChecked.ContainsKey($role.scope)) {
                            $additionalRoleAssignmentsInAzure = @() + (Invoke-AzCli role assignment list --scope $role.scope --assignee $principalId --only-show-errors)
                            $null = $scopesChecked.Add($role.scope, $true)
                            $existingRoleAssignments += $additionalRoleAssignmentsInAzure
                        }
                    }
                }
                Write-Information " `'$($assignment.displayName)`': $($existingRoleAssignments.Length) Role Assignments"
            }
            else {
                Write-Information " `'$($assignment.displayName)`'"
            }

            $value = @{
                assignment      = $assignment
                roleAssignments = $existingRoleAssignments
            }
            $null = $assignments.Add($assignment.id, $value)
        }

        #endregion

        #region Remediations
        if ($maybeRemediations -and $null -ne $assignment.identity -and $null -ne $assignment.identity.principalId) {
            foreach ($summary in $complianceStateSummaryCollection) {
                $id = $summary.policyAssignmentId
                if ($id -eq $assignment.id) {
                    # Match
                    $assignmentResult = $summary.results
                    if ($assignmentResult.nonCompliantResources -gt 0) {
                        if (-not $headerDisplayed) {
                            Write-Information "$header"
                            $headerDisplayed = $true
                        }
                        if ($getAssignments) {
                            Write-Information " NonCompliant Resources=$($assignmentResult.nonCompliantResources)"
                        }
                        else {
                            # Display subheader
                            Write-Information " '$($assignment.displayName)', NonCompliant Resources=$($assignmentResult.nonCompliantResources)"
                        }
                        # Write-Information " NonCompliantResources Total=$($assignmentResultNonCompliantResources)"
                        $numberOfAssignmentsWithRemediations++
                        $remediationTasks = @()
                        $policySetDefinitionId = $summary.policySetDefinitionId
                        $policySetDefinition = $null
                        if ($policySetDefinitionId -ne "") {
                            $policySetDefinition = $allPolicySetDefinitions[$policySetDefinitionId]
                            # Write-Information " Assigned Policy Set '$($policySetDefinition.displayName)'"
                        }
                        foreach ($policyDefinition in $summary.policyDefinitions) {
                            # Check if this PolicyDefinition needs remediation
                            $policyDefinitionResults = $policyDefinition.results
                            if ($policyDefinitionResults.nonCompliantResources -gt 0) {
                                $policyDefinitionId = $policyDefinition.policyDefinitionId
                                $nonCompliantResources = $policyDefinitionResults.nonCompliantResources
                                $policyDefinitionReferenceId = $policyDefinition.policyDefinitionReferenceId
                                $policyDefinitionFull = $allPolicyDefinitions[$policyDefinitionId]
                                $assignmentName = $assignment.name
                                $assignmentDisplayName = $assignment.displayName
                                $taskName = $assignmentName -replace '\s', '-'
                                if ($null -ne $policySetDefinition) {
                                    $taskName = "$($taskName)__$($policyDefinitionReferenceId)"
                                }
                                $remediationTask = @{
                                    assignmentId                = $id
                                    assignmentName              = $assignmentName
                                    assignmentDisplayName       = $assignmentDisplayName
                                    taskName                    = $taskName
                                    policyDefinitionId          = $policyDefinitionId
                                    nonCompliantResources       = $nonCompliantResources
                                    policySetName               = $policySetDefinition.name
                                    policySetDisplayName        = $policySetDefinition.displayName
                                    policyDefinitionReferenceId = $policyDefinitionReferenceId
                                    policyName                  = $policyDefinitionFull.name
                                    policyDisplayName           = $policyDefinitionFull.displayName
                                }
                                $remediationTasks += $remediationTask
                            }
                        }
                        if ($remediations.ContainsKey($scope)) {
                            $remediationsAtScope = $remediations.$scope
                            $null = $remediationsAtScope.Add($id, $assignmentRemediation)
                        }
                        else {
                            $null = $remediations.Add($scope, @{
                                    $id = $assignmentRemediation
                                }
                            )
                        }
                    }
                    break
                }
            }
        }
        #endregion
    }

}

function Get-AzAssignmentsAtSpecificScope {
    [CmdletBinding()]
    param (
        [string]    $scope,
        [bool]      $getAssignments,
        [bool]      $getExemptions,
        [bool]      $getRemediations,
        [hashtable] $allPolicyDefinitions,
        [hashtable] $allPolicySetDefinitions,
        [hashtable] $assignments,
        [hashtable] $exemptions,
        [hashtable] $remediations,
        [bool]      $suppressRoleAssignments
    )

    $splits = $scope.Split('/')
    if ($scope.Contains("/subscriptions/")) {

        # First element is an empty string due to leading forward slash (/)
        # $subscriptionId = $splits[2]
        # $null = (Invoke-AzCli account set --subscription $subscriptionId)

        if ($splits.Length -ge 5) {
            # Resource Group scope
            if ($getAssignments -or $getRemediations) {
                $assignmentList = @() + (Invoke-AzCli policy assignment list --scope $scope)
                if ($assignmentList.Length -gt 0) {
                    $header = "Resource Group $scope with $($assignmentList.Length) Policy Assignments"
                    Add-Assignments `
                        -assignmentList $assignmentList `
                        -header $header `
                        -scope $scope `
                        -allPolicyDefinitions $allPolicyDefinitions `
                        -allPolicySetDefinitions $allPolicySetDefinitions `
                        -getAssignments $getAssignments `
                        -getRemediations $getRemediations `
                        -assignments $assignments `
                        -remediations $remediations `
                        -suppressRoleAssignments $suppressRoleAssignments
                }
            }
        }
        else {
            # Subscription scope
            if ($getAssignments -or $getRemediations) {
                $assignmentList = @() + (Invoke-AzCli policy assignment list --scope $scope)
                if ($assignmentList.Length -gt 0) {
                    $header = "Subscription $subscriptionId with $($assignmentList.Length) Policy Assignments"
                    Add-Assignments `
                        -assignmentList $assignmentList `
                        -header $header `
                        -scope $scope `
                        -allPolicyDefinitions $allPolicyDefinitions `
                        -allPolicySetDefinitions $allPolicySetDefinitions `
                        -getAssignments $getAssignments `
                        -getRemediations $getRemediations `
                        -assignments $assignments `
                        -remediations $remediations `
                        -suppressRoleAssignments $suppressRoleAssignments
                }
            }
            if ($getExemptions) {
                $exemptionList = (Invoke-AzCli policy exemption list --disable-scope-strict-match --scope $scope)
                foreach ($exemptionRaw in $exemptionList) {
                    $exemptionId = $exemptionRaw.id
                    if (-not $exemptions.ContainsKey($exemptionId)) {
                        $name = $exemptionRaw.name

                        [array] $splits = $exemptionId -split "/"
                        $numberOfSplits = $splits.Count
                        $scopeLastIndex = $numberOfSplits - 5
                        $scopeSplits = $splits[0..$scopeLastIndex]
                        $scope = $scopeSplits -join "/"

                        $displayName = $exemptionRaw.displayName
                        $description = $exemptionRaw.description
                        $exemptionCategory = $exemptionRaw.exemptionCategory
                        $expiresOn = $exemptionRaw.expiresOn
                        $policyAssignmentId = $exemptionRaw.policyAssignmentId
                        $policyDefinitionReferenceIds = $exemptionRaw.policyDefinitionReferenceIds
                        $metadata = $exemptionRaw.metadata
                        $exemption = @{
                            name               = $name
                            scope              = $scope
                            policyAssignmentId = $policyAssignmentId
                            exemptionCategory  = $exemptionCategory
                        }
                        if ($displayName -and $displayName -ne "") {
                            $null = $exemption.Add("displayName", $displayName)
                        }
                        if ($description -and $description -ne "") {
                            $null = $exemption.Add("description", $description)
                        }
                        if ($expiresOn) {
                            $expiresOnUtc = $expiresOn.ToUniversalTime()
                            $null = $exemption.Add("expiresOn", $expiresOnUtc)

                        }
                        if ($policyDefinitionReferenceIds -and $policyDefinitionReferenceIds.Count -gt 0) {
                            $null = $exemption.Add("policyDefinitionReferenceIds", $policyDefinitionReferenceIds)
                        }
                        if ($metadata -and $metadata -ne @{} ) {
                            $null = $exemption.Add("metadata", $metadata)
                        }
                        $null = $exemptions.Add($exemptionId, $exemption)
                    }
                }
            }
        }
    }
    else {
        # Management Groups scope
        if ($getAssignments -or $getRemediations) {
            $assignmentList = @() + (Invoke-AzCli policy assignment list --scope $scope)
            $mg = $splits[-1]
            if ($assignmentList.Length -gt 0) {
                $header = "Management Group $mg with $($assignmentList.Length) Policy Assignments"
                Add-Assignments `
                    -assignmentList $assignmentList `
                    -header $header `
                    -scope $scope `
                    -allPolicyDefinitions $allPolicyDefinitions `
                    -allPolicySetDefinitions $allPolicySetDefinitions `
                    -getAssignments $getAssignments `
                    -getRemediations $getRemediations `
                    -assignments $assignments `
                    -remediations $remediations `
                    -suppressRoleAssignments $suppressRoleAssignments
            }
        }
    }
}

function Get-AzAssignmentsAtScopeRecursive {
    [CmdletBinding()]
    param(
        [parameter(Mandatory = $True)] [object]     $scopeTreeInfo,
        [parameter(Mandatory = $false)] [array]     $notScopeIn = @(),
        [parameter(Mandatory = $false)] [bool]      $includeResourceGroups = $false,
        [parameter(Mandatory = $false)] [bool]      $getAssignments = $true,
        [parameter(Mandatory = $false)] [bool]      $getExemptions = $true,
        [Parameter(Mandatory = $false)] [int]       $expiringInDays = 7,
        [parameter(Mandatory = $false)] [bool]      $getRemediations = $false,
        [parameter(Mandatory = $false)] [hashtable] $allPolicyDefinitions = $null,
        [parameter(Mandatory = $false)] [hashtable] $allPolicySetDefinitions = $null,
        [switch] $suppressRoleAssignments
    )

    [array] $subscriptionIds = @()
    [hashtable] $assignmentsInAzure = @{}
    [hashtable] $exemptions = @{}
    [hashtable] $remediations = @{}

    # Check parameters
    if ($getRemediations) {
        if ($null -eq $allPolicyDefinitions -or $null -eq $allPolicySetDefinitions) {
            $errorText = "getRemediations require `$allPolicyDefinitions and `$allPolicySetDefinitions not to be `$null"
            Write-Error $errorText
            Throw $errorText
        }
    }
    if (-not ($getAssignments -or $getRemediations)) {

    }

    Write-Information "==================================================================================================="
    Write-Information "Get Policy and Role Assignments recursively"
    Write-Information "==================================================================================================="

    if ($null -ne $scopeTreeInfo.ScopeTree) {
        # Management Group -> Process Management Groups and Subscriptions
        if ($includeResourceGroups) {
            Write-Information "Management Group $($scopeTreeInfo.ScopeTree.displayName) ($($scopeTreeInfo.ScopeTree.id)), Subscriptions and Resource Groups"
        }
        else {
            Write-Information "Management Group $($scopeTreeInfo.ScopeTree.displayName) ($($scopeTreeInfo.ScopeTree.id)) and Subscriptions"
        }
        $queuedScope = [System.Collections.Queue]::new()
        $null = $queuedScope.Enqueue($scopeTreeInfo.ScopeTree)
        Write-Debug " Enqueue $($scopeTreeInfo.ScopeTree.id)"
        while ($queuedScope.Count -gt 0) {
            $currentMg = $queuedScope.Dequeue()
            Write-Debug "Testing $($currentMg.id)"
            Get-AzAssignmentsAtSpecificScope -scope $currentMg.id `
                -getAssignments $getAssignments `
                -getRemediations $getRemediations `
                -allPolicyDefinitions $allPolicyDefinitions `
                -allPolicySetDefinitions $allPolicySetDefinitions `
                -assignments $assignmentsInAzure `
                -remediations $remediations `
                -suppressRoleAssignments $suppressRoleAssignments
            foreach ($child in $currentMg.children) {
                if (!$notScopeIn -and $notScopeIn.Contains($child.id)) {
                    Write-Information "Skipping notScope $($child.name) ($($child.id))"
                }
                else {
                    if ($child.Id.StartsWith("/providers/Microsoft.Management/managementGroups/")) {
                        $null = $queuedScope.Enqueue($child)
                        Write-Debug " Enqueue child $($child.id)"
                    }
                    else {
                        # Subscription
                        $subscription = $scopeTreeInfo.SubscriptionTable[$child.id]
                        if ($subscription.state -eq "Enabled") {
                            $subscriptionIds += $child.id
                        }
                    }
                }
            }
        }
    }
    else {
        $subscriptionTable = $scopeTreeInfo.SubscriptionTable
        $fullSubscriptionId = $subscriptionTable.Keys[0]
        $subscription = $subscriptionTable.$fullSubscriptionId
        if ($includeResourceGroups) {
            Write-Information "Single Subscription $($subscription.name) ($($subscription.id)) and Resource Groups"
        }
        else {
            Write-Information "Single Subscription $($subscription.name) ($($subscription.id))"
        }
        $subscriptionIds += $fullSubscriptionId
    }

    # Find Resource Groups in all subscriptions in notScope
    if ($subscriptionIds.Length -gt 0) {
        # Find out if we need to process any Resource Groups
        $notScopeResourceGroupIds = $()
        $notScopePatterns = @()
        if ($notScopeIn -and $includeResourceGroups) {
            foreach ($nsi in $notScopeIn) {
                if ($nsi.Contains("/resourceGroups/")) {
                    $notScopeResourceGroupIds += $nsi
                }
                elseif ($nsi.Contains("/resourceGroupPatterns/")) {
                    $nspTrimmed = $nsi.Split("/")[-1]
                    $notScopePatterns += $nspTrimmed
                }
            }
        }

        $subscriptionTable = $scopeTreeInfo.SubscriptionTable
        foreach ($subscriptionId in $subscriptionIds) {
            $subscriptionEntry = $subscriptionTable[$subscriptionId]
            if ($subscriptionEntry.State -eq "Enabled") {
                Get-AzAssignmentsAtSpecificScope -scope $subscriptionId `
                    -getAssignments $getAssignments `
                    -getExemptions $getExemptions `
                    -getRemediations $getRemediations `
                    -allPolicyDefinitions $allPolicyDefinitions `
                    -allPolicySetDefinitions $allPolicySetDefinitions `
                    -assignments $assignmentsInAzure `
                    -exemptions $exemptions `
                    -remediations $remediations `
                    -suppressRoleAssignments $suppressRoleAssignments

                if ($includeResourceGroups) {
                    # Ignore inactive subscriptions
                    $originalSubscriptionResourceGroupIds = $subscriptionEntry.ResourceGroupIds
                    if ($originalSubscriptionResourceGroupIds){$subscriptionResourceGroupIds = $originalSubscriptionResourceGroupIds.clone()}else{$subscriptionResourceGroupIds=@{}}
                    # Write-Information " Checking $($originalSubscriptionResourceGroupIds.Count) RGs in subscription $($subscriptionEntry.Name) ($($subscriptionId))"

                    # Eliminate notScope fully qualified resource Group Ids
                    foreach ($nrg in $notScopeResourceGroupIds) {
                        if ($originalSubscriptionResourceGroupIds.ContainsKey($nrg)) {
                            Write-Debug " Added Resource Group from full resourceId to notScope: $nrg"
                            $null = $subscriptionResourceGroupIds.Remove($nrg)
                        }
                    }

                    # Eliminate notScope patterns
                    foreach ($nsp in $notScopePatterns) {
                        foreach ($rg in $originalSubscriptionResourceGroupIds.Keys) {
                            $rgShort = $rg.Split("/")[-1]
                            if ($rgShort -like $nsp) {
                                Write-Debug " Added Resource Group $rg from pattern $nsp to notScope"
                                $null = $subscriptionResourceGroupIds.Remove($rg)
                            }
                        }
                    }

                    # Find assignments
                    foreach ($rg in $subscriptionResourceGroupIds.Keys) {
                        Write-Debug " Added Resource Group $rg Assignments"
                        Get-AzAssignmentsAtSpecificScope -scope $rg `
                            -getAssignments $getAssignments `
                            -getRemediations $getRemediations `
                            -allPolicyDefinitions $allPolicyDefinitions `
                            -allPolicySetDefinitions $allPolicySetDefinitions `
                            -assignments $assignmentsInAzure `
                            -remediations $remediations `
                            -suppressRoleAssignments $suppressRoleAssignments
                    }
                }
            }
        }
    }
    Write-Information ""
    Write-Information ""


    return $assignmentsInAzure, $remediations, $exemptions

}