Public/Get-RBACGuard.ps1

function Get-RBACGuard {
    <#
    .SYNOPSIS
        Identifies the guardrails (gates / blocks) that could prohibit an action or
        a role assignment even when the required RBAC role is held.
 
    .DESCRIPTION
        Holding the required role is necessary but not sufficient. A caller can hold
        Contributor and still be blocked from creating a resource by an Azure Policy
        'deny'; an executor can hold roleAssignments/write and still be blocked from
        granting a role by a deny assignment, a Conditional Access policy, a
        management lock, or a role that is only PIM-eligible (not active).
 
        Get-RBACGuard enumerates those guardrails for a platform at a scope and,
        with -Live, detects several of them against the live control plane:
 
          * Azure - management locks (Get-AzResourceLock), deny assignments
                      (Get-AzDenyAssignment), policy assignments in scope
                      (Get-AzPolicyAssignment), and PIM-eligible resource roles
                      (Get-AzRoleEligibilitySchedule). Plus advisory network guards
                      (Azure Firewall, NSG, resource firewall) and Defender-for-Cloud
                      / Microsoft Sentinel awareness.
          * Graph - Conditional Access policies (Get-MgIdentityConditionalAccessPolicy)
                      and PIM-eligible directory roles, plus advisory guards for
                      restricted-management admin units, admin consent, authentication
                      strength, and Defender for Identity.
          * Fabric - advisory guards for admin tenant settings, capacity state, and
                      Conditional Access.
          * Purview - advisory guards for the metadata-policy gate, collection
                      inheritance, and account network firewall.
 
        Every guard carries what it Affects (Action / RoleAssignment / Both), its
        Category (Preventive / Identity / Network / Detective), a Severity, a Status
        (Potential / Blocking / Clear / Detective), a Detail, a Mitigation, and a
        Microsoft Learn Reference. Detection is read-only and never changes state.
 
        Without -Live the result is the advisory knowledge base for the platform
        (every guard as 'Potential' / 'Detective'), which is fully offline and is
        how CI and the test suite exercise this command.
 
    .PARAMETER Platform
        Platform name or alias (Get-RBACProvider lists them).
 
    .PARAMETER Command
        Optional command / operation the guardrails are being assessed for; stamped
        onto each guard for correlation with a probe.
 
    .PARAMETER CallerId
        Optional identity whose eligible (PIM) roles and identity gates are inspected
        during -Live detection.
 
    .PARAMETER Scope
        Explicit scope. Overrides scope built from the Azure parameters below.
 
    .PARAMETER SubscriptionId / -ResourceGroupName / -ManagementGroupId / -ResourceId
        Parts used to build an ARM scope when -Scope is not supplied.
 
    .PARAMETER Live
        Detect guardrails against the live control plane (read-only). Azure and Graph
        support live detection; other platforms return the advisory set.
 
    .PARAMETER GuardType
        Optional filter: return only the named guard type(s), e.g. 'ResourceLock',
        'DenyAssignment', 'ConditionalAccess', 'PrivilegedIdentityManagement'.
 
    .PARAMETER Affects
        Optional filter: 'Action', 'RoleAssignment', or 'Both'. Guards that affect
        'Both' always match either filter.
 
    .PARAMETER Options
        Provider hashtable (reserved for future per-platform guard options).
 
    .PARAMETER MapPath
        Alternate guardrail knowledge-base path (testing).
 
    .PARAMETER TenantId / -RunAs*
        Probe-identity context (see Get-RBACRequirement).
 
    .EXAMPLE
        Get-RBACGuard -Platform Azure -SubscriptionId SUB1 -ResourceGroupName rg1
 
        Lists the advisory guardrails that could block an operation or a role grant
        in that resource group.
 
    .EXAMPLE
        Get-RBACGuard -Platform Azure -Scope /subscriptions/SUB1/resourceGroups/rg1 `
            -CallerId dev@contoso.com -Live |
            Where-Object Status -eq 'Blocking'
 
        Detects live blockers - locks, deny assignments - at the scope.
 
    .EXAMPLE
        Get-RBACGuard -Platform 'Microsoft Graph' -Affects RoleAssignment
 
        Shows only the Entra guardrails that could block *granting* a role
        (Conditional Access, PIM, restricted-management admin units).
 
    .OUTPUTS
        PSCustomObject (PSAutoRBAC.Guard) per detected / applicable guardrail.
    #>

    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param(
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Platform,

        [Parameter()]
        [string]$Command,

        [Parameter()]
        [string]$CallerId,

        [Parameter()]
        [string]$Scope,

        [Parameter()]
        [string]$SubscriptionId,

        [Parameter()]
        [string]$ResourceGroupName,

        [Parameter()]
        [string]$ManagementGroupId,

        [Parameter()]
        [string]$ResourceId,

        [Parameter()]
        [switch]$Live,

        [Parameter()]
        [string[]]$GuardType,

        [Parameter()]
        [ValidateSet('Action', 'RoleAssignment', 'Both')]
        [string]$Affects,

        [Parameter()]
        [hashtable]$Options,

        [Parameter()]
        [string]$MapPath,

        [Parameter()]
        [string]$TenantId,

        [Parameter()]
        [pscredential]$RunAsCredential,

        [Parameter()]
        [pscredential]$RunAsServicePrincipal,

        [Parameter()]
        [string]$RunAsTenantId,

        [Parameter()]
        [switch]$RunAsManagedIdentity,

        [Parameter()]
        [string]$RunAsManagedIdentityClientId
    )

    $provider = Get-RBACProviderInternal -Platform $Platform
    $context  = Initialize-RBACContext -BoundParameters $PSBoundParameters
    Write-PSFMessage -Level Verbose -Message "Get-RBACGuard: platform '$($provider.Name)', command '$Command', caller '$CallerId', live=$Live." -Tag 'PSAutoRBAC', 'Public', 'Guard'

    try {
        $resolvedScope = Resolve-RBACScope -Scope $Scope -SubscriptionId $SubscriptionId `
            -ResourceGroupName $ResourceGroupName -ManagementGroupId $ManagementGroupId `
            -ResourceId $ResourceId -AllowTenantRoot

        $opts = @{}
        if ($Options) { $opts = $Options.Clone() }
        if ($Live)                                { $opts['Live']      = $true }
        if ($PSBoundParameters.ContainsKey('GuardType')) { $opts['GuardType'] = $GuardType }
        if ($PSBoundParameters.ContainsKey('Affects'))   { $opts['Affects']   = $Affects }
        if ($MapPath)                             { $opts['MapPath']   = $MapPath }

        if ($Live -and -not ($provider.PSObject.Properties.Name -contains 'SupportsGuardProbe' -and $provider.SupportsGuardProbe)) {
            Write-PSFMessage -Level Verbose -Message "Get-RBACGuard: platform '$($provider.Name)' has no live guard detection; returning advisory guardrails." -Tag 'PSAutoRBAC', 'Public', 'Guard'
        }

        $guards = @(Invoke-RBACProviderGuard -Provider $provider -Command $Command -CallerId $CallerId -Scope $resolvedScope -Context $context -Options $opts)
        $blocking = @($guards | Where-Object { $_.Status -eq 'Blocking' }).Count
        Write-PSFMessage -Level Verbose -Message "Get-RBACGuard: $($guards.Count) guardrail(s) for '$($provider.Name)' ($blocking blocking)." -Tag 'PSAutoRBAC', 'Public', 'Guard'
        $guards
    }
    finally {
        if ($context.IsRunAs) { & $context.Disconnect }
    }
}