Private/ConvertTo-AACAksPolicyCompliance.ps1

function ConvertTo-AACAksPolicyCompliance {
    <#
    .SYNOPSIS
        Consolidates Azure Policy for Kubernetes compliance into one view -
        by namespace, workload, policy and cluster - the way the AKS Policy
        Compliance Toolkit (github.com/sam-cogan/aks-policy-compliance-toolkit)
        does, for Invoke-AACAksAssessment.
    .DESCRIPTION
        In the portal, Kubernetes policy compliance is read one policy at a
        time, then per cluster, then per component. This puts the
        non-compliant components (pods, services, network policies, ...) of
        every cluster side by side:
          Components one row per non-compliant component and policy: its
                       cluster, namespace, workload (inferred from the pod's
                       name: a Deployment's ReplicaSet hash, a CronJob's
                       schedule suffix, a StatefulSet's ordinal, a
                       DaemonSet's or Job's suffix), policy, effect,
                       assignment and initiative
          ByNamespace per namespace (team or tenant): violations, workloads,
                       policies, clusters, how many under Deny
          ByWorkload per cluster, namespace and workload
          ByPolicy per policy: violations, clusters, namespaces, workloads
          ByCluster per cluster and policy - Capped where Azure Policy's
                       limit of 500 non-compliant records per policy and
                       cluster was reached (the in-cluster counts are complete)
          ClusterStates every policy evaluated on each cluster resource
                       itself (Compliant or NonCompliant), with its effect
          Assignments the assignments that reach the clusters: scope,
                       enforcement mode, effect, policies, non-compliant ones
        Effect: 'audit (DoNotEnforce)' when the assignment doesn't enforce,
        else the effect the cluster's policy state reports, else the
        assignment's effect parameter, else 'unresolved'.
        System namespaces (kube-system, gatekeeper-system, azure-arc,
        azure-extensions-usage-system, flux-system, ...) are left out unless
        -IncludeSystemNamespace.
 
        Rows are Resource Graph rows (Get-AACAksQuery); -DefinitionName maps
        a policy or initiative definition ID (lower case) to its display name.
    #>

    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [AllowEmptyCollection()] [object[]] $Component = @(),
        [AllowEmptyCollection()] [object[]] $ClusterState = @(),
        [AllowEmptyCollection()] [object[]] $Assignment = @(),
        [hashtable] $DefinitionName = @{},
        # Cluster ID (lower case) -> name; only these clusters are kept when given.
        [hashtable] $ClusterName = @{},
        [hashtable] $SubscriptionName = @{},
        [switch] $IncludeSystemNamespace,
        [string[]] $SystemNamespace = @('kube-system', 'gatekeeper-system', 'azure-arc', 'azure-extensions-usage-system', 'flux-system', 'kube-public', 'kube-node-lease', 'calico-system', 'tigera-operator', 'aks-command', 'app-routing-system', 'aks-istio-system', 'kube-egress-gateway-system', 'dapr-system')
    )

    $value = { param($Row, [string] $Key) if ($Row -is [System.Collections.IDictionary]) { if ($Row.Contains($Key)) { $Row[$Key] } } elseif ($null -ne $Row) { Get-AACPropertyValue -InputObject $Row -Name $Key } }
    $lower = { param($Text) ([string]$Text).ToLowerInvariant() }
    $leaf = { param($Id) if ($Id) { ([string]$Id).TrimEnd('/') -replace '^.*/', '' } else { '' } }
    $clusterOf = { param([string] $Id) $key = & $lower $Id; if ($ClusterName.Contains($key)) { $ClusterName[$key] } elseif ($Id -match '(?i)/managedclusters/([^/]+)') { $Matches[1] } else { $Id } }
    $keep = { param([string] $Id) -not $ClusterName.Count -or $ClusterName.Contains((& $lower $Id)) }
    $nameOf = { param([string] $Id) $key = & $lower $Id; if ($key -and $DefinitionName.Contains($key) -and $DefinitionName[$key]) { [string]$DefinitionName[$key] } else { '' } }
    $object = { param([string] $TypeName, [System.Collections.IDictionary] $Property) $item = [pscustomobject]$Property; $item.PSObject.TypeNames.Insert(0, $TypeName); $item }

    # --- Assignments and the effect each cluster's policy state reports -----------------------------------------------
    $assignments = @{}
    foreach ($row in $Assignment) { $assignments[(& $lower (& $value $row 'assignmentId'))] = $row }
    $stateEffect = @{}
    foreach ($row in $ClusterState) {
        $key = "$(& $lower (& $value $row 'clusterId'))|$(& $lower (& $value $row 'assignmentId'))|$(& $lower (& $value $row 'refId'))"
        $effect = [string](& $value $row 'effect')
        if ($effect) { $stateEffect[$key] = $effect }
    }
    $effectOf = {
        param([string] $ClusterId, [string] $AssignmentId, [string] $RefId)
        $row = $assignments[(& $lower $AssignmentId)]
        if ($row -and [string](& $value $row 'enforcementMode') -eq 'DoNotEnforce') { return 'audit (DoNotEnforce)' }
        $key = "$(& $lower $ClusterId)|$(& $lower $AssignmentId)|$(& $lower $RefId)"
        if ($stateEffect.Contains($key)) { return ([string]$stateEffect[$key]).ToLowerInvariant() }
        if ($row -and (& $value $row 'effect')) { return ([string](& $value $row 'effect')).ToLowerInvariant() }
        'unresolved'
    }
    $assignmentName = { param([string] $Id) $row = $assignments[(& $lower $Id)]; if ($row -and (& $value $row 'displayName')) { [string](& $value $row 'displayName') } else { & $leaf $Id } }

    # --- The non-compliant components ----------------------------------------------------------------------------
    # Pod names: <deployment>-<replicaset hash>-<pod hash>, <cronjob>-<schedule time>-<hash>,
    # <statefulset>-<ordinal>, <daemonset or job>-<hash>.
    $safe = '[bcdfghjklmnpqrstvwxz2456789]'
    $system = [System.Collections.Generic.HashSet[string]]::new([string[]]@($SystemNamespace), [System.StringComparer]::OrdinalIgnoreCase)
    $components = [System.Collections.Generic.List[object]]::new()
    foreach ($row in $Component) {
        $clusterId = [string](& $value $row 'clusterId')
        if (-not (& $keep $clusterId)) { continue }
        $componentId = [string](& $value $row 'componentId')
        $kind = [string](& $value $row 'componentType')
        $namespace = if ($componentId.Contains('/')) { ($componentId -split '/', 2)[0] } else { '(cluster-scoped)' }
        if (-not $IncludeSystemNamespace -and $system.Contains($namespace)) { continue }
        $name = if ($componentId.Contains('/')) { ($componentId -split '/', 2)[1] } else { $componentId }
        $workload = $name; $workloadKind = if ($kind) { $kind } else { 'Component' }
        if ($kind -eq 'pod') {
            if ($name -match "^(.+)-[0-9]{8,}-$safe{5}$") { $workload = $Matches[1]; $workloadKind = 'CronJob' }
            elseif ($name -match "^(.+)-$safe{5,10}-$safe{5}$") { $workload = $Matches[1]; $workloadKind = 'Deployment' }
            elseif ($name -match '^(.+)-[0-9]{1,3}$') { $workload = $Matches[1]; $workloadKind = 'StatefulSet' }
            elseif ($name -match "^(.+)-$safe{5}$") { $workload = $Matches[1]; $workloadKind = 'DaemonSet / Job' }
            else { $workloadKind = 'Pod' }
        }
        $definitionId = [string](& $value $row 'definitionId')
        $refId = [string](& $value $row 'refId')
        $policy = @((& $nameOf $definitionId), $refId, (& $leaf $definitionId)) | Where-Object { $_ } | Select-Object -First 1
        $setId = [string](& $value $row 'setId')
        $assignmentId = [string](& $value $row 'assignmentId')
        $subscription = if ($clusterId -match '^/subscriptions/([^/]+)') { $Matches[1].ToLowerInvariant() } else { '' }
        $components.Add((& $object 'AAC.AksPolicyViolation' ([ordered]@{
                        Cluster       = & $clusterOf $clusterId
                        Namespace     = $namespace
                        WorkloadKind  = $workloadKind
                        Workload      = $workload
                        ObjectKind    = $kind
                        Component     = $name
                        Policy        = [string]$policy
                        Effect        = & $effectOf $clusterId $assignmentId $refId
                        Assignment    = & $assignmentName $assignmentId
                        Initiative    = $(if ($setId) { @((& $nameOf $setId), (& $leaf $setId)) | Where-Object { $_ } | Select-Object -First 1 } else { '' })
                        LastEvaluated = [string](& $value $row 'evaluated')
                        Subscription  = $(if ($SubscriptionName.Contains($subscription)) { $SubscriptionName[$subscription] } else { $subscription })
                        ClusterId     = $clusterId
                        PolicyId      = $definitionId
                    })))
    }
    $isDeny = { param($Effect) [string]$Effect -eq 'deny' }
    $joinTop = { param([object[]] $Values, [int] $Top) $distinct = @($Values | Where-Object { $_ } | Sort-Object -Unique); ((@($distinct | Select-Object -First $Top)) -join ', ') + $(if ($distinct.Count -gt $Top) { ", +$($distinct.Count - $Top)" } else { '' }) }

    $byNamespace = @(foreach ($group in $components | Group-Object Namespace) {
            & $object 'AAC.AksPolicyNamespace' ([ordered]@{
                    Namespace  = $group.Name
                    Violations = $group.Count
                    Workloads  = @($group.Group | ForEach-Object { "$($_.Cluster)/$($_.Workload)" } | Sort-Object -Unique).Count
                    Policies   = @($group.Group | ForEach-Object Policy | Sort-Object -Unique).Count
                    Deny       = @($group.Group | Where-Object { & $isDeny $_.Effect }).Count
                    Clusters   = & $joinTop @($group.Group | ForEach-Object Cluster) 20
                    PolicyNames = & $joinTop @($group.Group | ForEach-Object Policy) 50
                })
        }) | Sort-Object -Property @{ Expression = 'Violations'; Descending = $true }, Namespace
    $byWorkload = @(foreach ($group in $components | Group-Object Cluster, Namespace, Workload) {
            $first = $group.Group[0]
            & $object 'AAC.AksPolicyWorkload' ([ordered]@{
                    Cluster      = $first.Cluster
                    Namespace    = $first.Namespace
                    WorkloadKind = $first.WorkloadKind
                    Workload     = $first.Workload
                    Violations   = $group.Count
                    Components   = @($group.Group | ForEach-Object Component | Sort-Object -Unique).Count
                    Policies     = @($group.Group | ForEach-Object Policy | Sort-Object -Unique).Count
                    Deny         = @($group.Group | Where-Object { & $isDeny $_.Effect }).Count
                    PolicyNames  = & $joinTop @($group.Group | ForEach-Object Policy) 50
                })
        }) | Sort-Object -Property @{ Expression = 'Violations'; Descending = $true }, Cluster, Namespace, Workload
    $byPolicy = @(foreach ($group in $components | Group-Object Policy) {
            & $object 'AAC.AksPolicySummary' ([ordered]@{
                    Policy     = $group.Name
                    Effect     = & $joinTop @($group.Group | ForEach-Object Effect) 5
                    Violations = $group.Count
                    Clusters   = @($group.Group | ForEach-Object Cluster | Sort-Object -Unique).Count
                    Namespaces = @($group.Group | ForEach-Object Namespace | Sort-Object -Unique).Count
                    Workloads  = @($group.Group | ForEach-Object { "$($_.Cluster)/$($_.Namespace)/$($_.Workload)" } | Sort-Object -Unique).Count
                    Assignment = & $joinTop @($group.Group | ForEach-Object Assignment) 5
                    Initiative = & $joinTop @($group.Group | ForEach-Object Initiative) 5
                })
        }) | Sort-Object -Property @{ Expression = 'Violations'; Descending = $true }, Policy
    $byCluster = @(foreach ($group in $components | Group-Object Cluster, Policy) {
            $first = $group.Group[0]
            & $object 'AAC.AksPolicyCluster' ([ordered]@{
                    Cluster    = $first.Cluster
                    Policy     = $first.Policy
                    Effect     = & $joinTop @($group.Group | ForEach-Object Effect) 5
                    Violations = $group.Count
                    Namespaces = @($group.Group | ForEach-Object Namespace | Sort-Object -Unique).Count
                    Workloads  = @($group.Group | ForEach-Object { "$($_.Namespace)/$($_.Workload)" } | Sort-Object -Unique).Count
                    # Azure Policy keeps 500 non-compliant records per policy and cluster (system namespaces included).
                    Capped     = $group.Count -ge 500
                    ClusterId  = $first.ClusterId
                })
        }) | Sort-Object -Property @{ Expression = 'Violations'; Descending = $true }, Cluster, Policy

    # --- The policies evaluated on each cluster resource ----------------------------------------------------------------
    # A policy with non-compliant components is a Kubernetes (workload) policy.
    $workloadPolicies = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
    foreach ($item in $components) { if ($item.PolicyId) { [void]$workloadPolicies.Add($item.PolicyId) } }
    $clusterStates = @(foreach ($row in $ClusterState) {
            $clusterId = [string](& $value $row 'clusterId')
            if (-not (& $keep $clusterId)) { continue }
            $definitionId = [string](& $value $row 'definitionId')
            $setId = [string](& $value $row 'setId')
            $assignmentId = [string](& $value $row 'assignmentId')
            & $object 'AAC.AksPolicyState' ([ordered]@{
                    Cluster    = & $clusterOf $clusterId
                    State      = [string](& $value $row 'state')
                    Policy     = [string](@((& $nameOf $definitionId), [string](& $value $row 'refId'), (& $leaf $definitionId)) | Where-Object { $_ } | Select-Object -First 1)
                    Effect     = & $effectOf $clusterId $assignmentId ([string](& $value $row 'refId'))
                    Assignment = & $assignmentName $assignmentId
                    Initiative = $(if ($setId) { [string](@((& $nameOf $setId), (& $leaf $setId)) | Where-Object { $_ } | Select-Object -First 1) } else { '' })
                    Scope      = $(if ($workloadPolicies.Contains($definitionId)) { 'Workloads' } else { 'Cluster' })
                    Evaluated  = [string](& $value $row 'evaluated')
                    ClusterId  = $clusterId
                    AssignmentId = $assignmentId
                })
        }) | Sort-Object -Property @{ Expression = { if ($_.State -eq 'NonCompliant') { 0 } else { 1 } } }, Cluster, Policy

    # --- The assignments that reach the clusters ------------------------------------------------------------------------
    $assignmentRows = @(foreach ($group in $clusterStates | Group-Object { & $lower $_.AssignmentId }) {
            $row = $assignments[$group.Name]
            & $object 'AAC.AksPolicyAssignment' ([ordered]@{
                    Assignment      = $group.Group[0].Assignment
                    Scope           = [string](& $value $row 'scope')
                    EnforcementMode = $(if ($row) { [string](& $value $row 'enforcementMode') } else { '' })
                    Effect          = $(if ($row) { [string](& $value $row 'effect') } else { '' })
                    Policies        = @($group.Group | ForEach-Object Policy | Sort-Object -Unique).Count
                    NonCompliant    = @($group.Group | Where-Object State -EQ 'NonCompliant' | ForEach-Object Policy | Sort-Object -Unique).Count
                    Clusters        = @($group.Group | ForEach-Object Cluster | Sort-Object -Unique).Count
                    Violations      = @($components | Where-Object { $_.Assignment -eq $group.Group[0].Assignment }).Count
                    AssignmentId    = $group.Group[0].AssignmentId
                })
        }) | Sort-Object -Property @{ Expression = 'NonCompliant'; Descending = $true }, Assignment

    @{
        Components    = $components.ToArray()
        ByNamespace   = @($byNamespace)
        ByWorkload    = @($byWorkload)
        ByPolicy      = @($byPolicy)
        ByCluster     = @($byCluster)
        ClusterStates = @($clusterStates)
        Assignments   = @($assignmentRows)
        Stats         = [ordered]@{
            Violations            = $components.Count
            Namespaces            = @($byNamespace).Count
            Workloads             = @($byWorkload).Count
            Policies              = @($byPolicy).Count
            Deny                  = @($components | Where-Object { & $isDeny $_.Effect }).Count
            Capped                = @($byCluster | Where-Object Capped).Count
            ClusterPolicies       = @($clusterStates).Count
            NonCompliantPolicies  = @($clusterStates | Where-Object State -EQ 'NonCompliant').Count
            Assignments           = @($assignmentRows).Count
        }
    }
}