modules/Devolutions.CIEM.Checks/Public/Get-CIEMRequiredPermission.ps1

function Get-CIEMRequiredPermission {
    <#
    .SYNOPSIS
        Gets the required permissions for running CIEM security checks, discovery endpoints, and Azure remediations.

    .DESCRIPTION
        Aggregates all unique permissions required across all enabled checks and discovery
        endpoints registered in azure_provider_apis. For Azure without service or check
        filters, it also derives remediation permissions from the shared remediation token
        catalog and the attack path remediation templates. Returns permissions grouped by
        type, depending on the provider:
        - Azure: Microsoft Graph API, Azure Resource Manager RBAC, Key Vault data plane, Azure Roles
        - AWS: IAM actions (e.g., iam:ListUsers, s3:GetBucketPolicy)

        Discovery endpoint permissions (e.g., Directory.Read.All for Entra entity collection)
        are always included for the relevant provider, regardless of check filters.

    .PARAMETER Provider
        Filter to permissions for a specific cloud provider (Azure, AWS).

    .PARAMETER Service
        Filter to permissions required for a specific service (e.g., Entra, IAM, KeyVault, Storage, iam, s3).

    .PARAMETER CheckId
        Filter to permissions required for specific check IDs.

    .OUTPUTS
        [PSCustomObject] Object containing provider-appropriate permission properties:
        Azure: Graph, ARM, KeyVaultDataPlane, AzureRoles, CheckCount, DiscoveryEndpointCount, Summary, Discovery, Remediation
        AWS: IAM, CheckCount, Summary

    .EXAMPLE
        Get-CIEMRequiredPermission -Provider Azure
        # Returns all Azure discovery and remediation permissions for Azure

    .EXAMPLE
        Get-CIEMRequiredPermission -Provider AWS
        # Returns all IAM actions required for AWS checks

    .EXAMPLE
        Get-CIEMRequiredPermission -Service Entra
        # Returns permissions required for Entra ID checks and discovery endpoints
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter()]
        [ValidateSet('Azure', 'AWS')]
        [string]$Provider,

        [Parameter()]
        [string]$Service,

        [Parameter()]
        [string[]]$CheckId
    )

    $ErrorActionPreference = 'Stop'

    function GetCIEMRemediationPermissionCatalog {
        $ErrorActionPreference = 'Stop'

        $catalogPath = Join-Path $script:ModuleRoot 'modules' 'Devolutions.CIEM.Checks' 'Data' 'remediation-permissions.json'
        if (-not (Test-Path $catalogPath)) {
            throw "CIEM remediation permission catalog not found: $catalogPath"
        }

        $catalog = Get-Content $catalogPath -Raw | ConvertFrom-Json -AsHashtable
        if ($null -eq $catalog.Azure -or $null -eq $catalog.Azure.RemediationTokens) {
            throw "CIEM remediation permission catalog is missing Azure.RemediationTokens: $catalogPath"
        }

        $catalog
    }

    function GetCIEMAzureRemediationPermissionSection {
        param(
            [Parameter(Mandatory)]
            [hashtable]$Catalog
        )

        $ErrorActionPreference = 'Stop'

        $templateRoot = Join-Path $script:ModuleRoot 'modules' 'Devolutions.CIEM.Graph' 'Data' 'attack_path_remediation_scripts'
        if (-not (Test-Path $templateRoot)) {
            throw "CIEM attack path remediation script folder not found: $templateRoot"
        }

        $tokenMap = $Catalog.Azure.RemediationTokens
        $graphPermissions = [System.Collections.Generic.List[string]]::new()
        $azureRoles = [System.Collections.Generic.List[string]]::new()
        $matchedTemplateCount = 0

        foreach ($template in @(Get-ChildItem $templateRoot -File)) {
            $content = Get-Content $template.FullName -Raw
            $tokens = @(
                [regex]::Matches($content, '{{([A-Z0-9_]+)}}') |
                    ForEach-Object { $_.Groups[1].Value } |
                    Select-Object -Unique
            )

            $matchedTemplate = $false
            foreach ($token in $tokens) {
                if (-not $tokenMap.ContainsKey($token)) {
                    continue
                }

                $matchedTemplate = $true
                $requirements = $tokenMap[$token]

                foreach ($permission in @($requirements.Graph)) {
                    if ([string]::IsNullOrWhiteSpace([string]$permission)) {
                        continue
                    }

                    $graphPermissions.Add([string]$permission)
                }

                foreach ($role in @($requirements.AzureRoles)) {
                    if ([string]::IsNullOrWhiteSpace([string]$role)) {
                        continue
                    }

                    $azureRoles.Add([string]$role)
                }
            }

            if ($matchedTemplate) {
                $matchedTemplateCount++
            }
        }

        [PSCustomObject]@{
            Graph         = @($graphPermissions | Select-Object -Unique | Sort-Object)
            AzureRoles    = @($azureRoles | Select-Object -Unique | Sort-Object)
            TemplateCount = $matchedTemplateCount
        }
    }

    # Get checks based on filters
    $getCheckParams = @{}
    if ($Provider) { $getCheckParams.Provider = $Provider }
    if ($Service) { $getCheckParams.Service = $Service }

    $checks = Get-CIEMCheck @getCheckParams

    if ($CheckId) {
        $checks = $checks | Where-Object { $CheckId -contains $_.Id }
    }

    # Aggregate unique permissions using List for efficient collection
    $graphPermissions = [System.Collections.Generic.List[string]]::new()
    $armPermissions = [System.Collections.Generic.List[string]]::new()
    $kvPermissions = [System.Collections.Generic.List[string]]::new()
    $iamPermissions = [System.Collections.Generic.List[string]]::new()
    $endpointAzureRoles = [System.Collections.Generic.List[string]]::new()
    $remediationSection = [PSCustomObject]@{
        Graph         = @()
        AzureRoles    = @()
        TemplateCount = 0
    }

    # Aggregate from checks
    $checkCount = 0
    if ($checks) {
        $checkCount = @($checks).Count
        foreach ($check in $checks) {
            $perms = $check.Permissions
            if ($perms.Graph) {
                foreach ($p in $perms.Graph) { $graphPermissions.Add($p) }
            }
            if ($perms.ARM) {
                foreach ($p in $perms.ARM) { $armPermissions.Add($p) }
            }
            if ($perms.KeyVaultDataPlane) {
                foreach ($p in $perms.KeyVaultDataPlane) { $kvPermissions.Add($p) }
            }
            if ($perms.IAM) {
                foreach ($p in $perms.IAM) { $iamPermissions.Add($p) }
            }
        }
    }

    # Aggregate discovery endpoint permissions from azure_provider_apis (scoped rows)
    $discoveryEndpointCount = 0
    if ($Provider -ne 'AWS') {
        $endpointParams = @{ HasPermissions = $true }
        if ($Service) { $endpointParams.Service = $Service }
        $endpoints = @(GetCIEMAzureProviderApi @endpointParams)
        $discoveryEndpointCount = $endpoints.Count

        foreach ($endpoint in $endpoints) {
            $perms = $endpoint.Permissions
            if ($perms.Graph) {
                foreach ($p in $perms.Graph) { $graphPermissions.Add($p) }
            }
            if ($perms.ARM) {
                foreach ($p in $perms.ARM) { $armPermissions.Add($p) }
            }
            if ($perms.KeyVaultDataPlane) {
                foreach ($p in $perms.KeyVaultDataPlane) { $kvPermissions.Add($p) }
            }
            if ($perms.AzureRoles) {
                foreach ($p in $perms.AzureRoles) { $endpointAzureRoles.Add($p) }
            }
        }
    }

    $includeAzureRemediation = ($Provider -ne 'AWS' -and -not $PSBoundParameters.ContainsKey('Service') -and -not $PSBoundParameters.ContainsKey('CheckId'))
    if ($includeAzureRemediation) {
        $remediationSection = GetCIEMAzureRemediationPermissionSection -Catalog (GetCIEMRemediationPermissionCatalog)
    }

    $hasRemediationPermissions = ($remediationSection.Graph.Count -gt 0 -or $remediationSection.AzureRoles.Count -gt 0)
    if (-not $checks -and $discoveryEndpointCount -eq 0 -and -not $hasRemediationPermissions) {
        Write-Warning "No checks or discovery endpoints found matching the specified criteria."
        return [PSCustomObject]@{
            Graph                  = @()
            ARM                    = @()
            KeyVaultDataPlane      = @()
            AzureRoles             = @()
            IAM                    = @()
            CheckCount             = 0
            DiscoveryEndpointCount = 0
            Discovery              = [PSCustomObject]@{
                Graph                  = @()
                ARM                    = @()
                KeyVaultDataPlane      = @()
                AzureRoles             = @()
                CheckCount             = 0
                DiscoveryEndpointCount = 0
            }
            Remediation            = $remediationSection
            Summary                = "No checks or discovery endpoints found."
        }
    }

    # Get unique and sort (wrap in @() to ensure arrays)
    $graphPermissions = @($graphPermissions | Select-Object -Unique | Sort-Object)
    $armPermissions = @($armPermissions | Select-Object -Unique | Sort-Object)
    $kvPermissions = @($kvPermissions | Select-Object -Unique | Sort-Object)
    $iamPermissions = @($iamPermissions | Select-Object -Unique | Sort-Object)

    # Determine required Azure RBAC roles based on check permissions + endpoint permissions
    $azureRoles = @()
    if ($graphPermissions.Count -gt 0 -or $armPermissions.Count -gt 0 -or $kvPermissions.Count -gt 0 -or $endpointAzureRoles.Count -gt 0) {
        # Subscription Reader is always required for subscription discovery (ARM REST API)
        $azureRoles = @('Reader')

        # ARM permissions: Reader role covers all */read actions
        if ($armPermissions.Count -gt 0) {
            $nonReadPermissions = @($armPermissions | Where-Object { $_ -notmatch '/read$' })
            if ($nonReadPermissions.Count -gt 0) {
                Write-Warning "Some ARM permissions require write access. Review permissions and assign appropriate roles."
            }
        }

        # Key Vault data plane permissions: Map to specific RBAC roles
        if ($kvPermissions -contains 'secrets/list' -or $kvPermissions -contains 'secrets/get') {
            $azureRoles += 'Key Vault Secrets User'
        }
        if ($kvPermissions -contains 'keys/list' -or $kvPermissions -contains 'keys/get') {
            $azureRoles += 'Key Vault Crypto User'
        }

        # Merge explicit AzureRoles from discovery endpoints
        foreach ($role in $endpointAzureRoles) {
            $azureRoles += $role
        }

        $azureRoles = @($azureRoles | Select-Object -Unique | Sort-Object)
    }

    # Build summary
    $summaryParts = @()
    $summaryParts += "Permissions required for $checkCount check(s) and $discoveryEndpointCount discovery endpoint(s):"

    if ($graphPermissions.Count -gt 0) {
        $summaryParts += ""
        $summaryParts += "Microsoft Graph API Permissions (Application):"
        foreach ($perm in $graphPermissions) {
            $summaryParts += " - $perm"
        }
    }

    if ($armPermissions.Count -gt 0) {
        $summaryParts += ""
        $summaryParts += "Azure Resource Manager RBAC Actions:"
        foreach ($perm in $armPermissions) {
            $summaryParts += " - $perm"
        }
    }

    if ($kvPermissions.Count -gt 0) {
        $summaryParts += ""
        $summaryParts += "Key Vault Data Plane Permissions:"
        foreach ($perm in $kvPermissions) {
            $summaryParts += " - $perm"
        }
    }

    if ($azureRoles.Count -gt 0) {
        $summaryParts += ""
        $summaryParts += "Required Azure RBAC Roles (assign at subscription scope):"
        $summaryParts += " - Reader (required for subscription discovery)"
        foreach ($role in @($azureRoles | Where-Object { $_ -ne 'Reader' })) {
            $summaryParts += " - $role"
        }
    }

    if ($discoveryEndpointCount -gt 0) {
        $summaryParts += ""
        $summaryParts += "Discovery Endpoint Permissions ($discoveryEndpointCount endpoints):"
        $summaryParts += " Permissions above include requirements for data collection endpoints"
        $summaryParts += " registered in azure_provider_apis (Entra, ResourceGraph, IAM)."
    }

    if ($iamPermissions.Count -gt 0) {
        $summaryParts += ""
        $summaryParts += "AWS IAM Actions:"
        foreach ($perm in $iamPermissions) {
            $summaryParts += " - $perm"
        }
    }

    $discoverySection = [PSCustomObject]@{
        Graph                  = @($graphPermissions)
        ARM                    = @($armPermissions)
        KeyVaultDataPlane      = @($kvPermissions)
        AzureRoles             = @($azureRoles)
        CheckCount             = $checkCount
        DiscoveryEndpointCount = $discoveryEndpointCount
    }

    [PSCustomObject]@{
        Graph                  = @($graphPermissions)
        ARM                    = @($armPermissions)
        KeyVaultDataPlane      = @($kvPermissions)
        AzureRoles             = @($azureRoles)
        IAM                    = @($iamPermissions)
        CheckCount             = $checkCount
        DiscoveryEndpointCount = $discoveryEndpointCount
        Discovery              = $discoverySection
        Remediation            = $remediationSection
        Summary                = $summaryParts -join "`n"
    }
}