Private/Get-RBACGuardDefinition.ps1

function New-RBACGuardObject {
    <#
    .SYNOPSIS
        Builds a normalized PSAutoRBAC.Guard object.
    .DESCRIPTION
        Internal factory shared by Get-RBACGuardDefinition (advisory, offline) and
        the provider ResolveGuard scriptblocks (live detection). Centralizing the
        shape keeps every guard - whether surfaced from the knowledge base or from
        a live control - identical, so callers can filter and format them uniformly.
    .OUTPUTS
        PSCustomObject (PSAutoRBAC.Guard)
    #>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '',
        Justification = 'Factory; constructs and returns a guard object, makes no state change.')]
    [OutputType([psobject])]
    param(
        [Parameter(Mandatory)] [string]$Platform,
        [Parameter(Mandatory)] [string]$GuardType,
        [Parameter()] [string]$Command,
        [Parameter()] [string]$CallerId,
        [Parameter()] [string]$Scope,
        [Parameter()] [ValidateSet('Action', 'RoleAssignment', 'Both')] [string]$Affects = 'Both',
        [Parameter()] [ValidateSet('Preventive', 'Identity', 'Network', 'Detective')] [string]$Category = 'Preventive',
        [Parameter()] [ValidateSet('High', 'Medium', 'Info')] [string]$Severity = 'Medium',
        [Parameter()] [ValidateSet('Blocking', 'Potential', 'Clear', 'Detective', 'Unknown')] [string]$Status = 'Potential',
        [Parameter()] [string]$Detail,
        [Parameter()] [string]$Mitigation,
        [Parameter()] [string]$Reference,
        [Parameter()] [ValidateSet('KnowledgeBase', 'Live')] [string]$Source = 'KnowledgeBase',
        [Parameter()] [object[]]$Evidence
    )

    [pscustomobject]@{
        PSTypeName = 'PSAutoRBAC.Guard'
        Platform   = $Platform
        Command    = $Command
        CallerId   = $CallerId
        Scope      = $Scope
        GuardType  = $GuardType
        Affects    = $Affects
        Category   = $Category
        Severity   = $Severity
        Status     = $Status
        Detail     = $Detail
        Mitigation = $Mitigation
        Reference  = $Reference
        Source     = $Source
        Evidence   = @($Evidence)
    }
}

function Get-RBACGuardDefinition {
    <#
    .SYNOPSIS
        Returns the advisory guardrail definitions for a platform from the knowledge base.
    .DESCRIPTION
        Internal. Reads Data/GuardrailMap.psd1 and projects each guard definition
        for the requested platform into a PSAutoRBAC.Guard object with
        Status = 'Potential' (a gate that *may* apply) or 'Detective' (a control
        that watches rather than blocks). Provider ResolveGuard scriptblocks call
        this for their offline baseline, then optionally promote individual guards
        to 'Blocking'/'Clear' from live detection.
 
        This is the pure-data layer: every guard the framework knows about is
        declared in GuardrailMap.psd1, so extending coverage is a data edit, not a
        code change - mirroring the CommandRoleMap knowledge base.
    .PARAMETER Platform
        Canonical platform name (e.g. 'Azure', 'Microsoft Graph').
    .PARAMETER GuardType
        Optional filter: return only the named guard type(s).
    .PARAMETER Affects
        Optional filter: 'Action', 'RoleAssignment', or 'Both'. 'Both' entries
        always match either filter because they affect both paths.
    .PARAMETER Command / -CallerId / -Scope
        Context stamped onto each returned guard.
    .PARAMETER MapPath
        Optional explicit GuardrailMap path (testing).
    .OUTPUTS
        PSCustomObject (PSAutoRBAC.Guard)
    #>

    [CmdletBinding()]
    [OutputType([psobject])]
    param(
        [Parameter(Mandatory)] [string]$Platform,
        [Parameter()] [string[]]$GuardType,
        [Parameter()] [ValidateSet('Action', 'RoleAssignment', 'Both')] [string]$Affects,
        [Parameter()] [string]$Command,
        [Parameter()] [string]$CallerId,
        [Parameter()] [string]$Scope,
        [Parameter()] [string]$MapPath
    )

    $map = if ($MapPath) { Get-RBACKnowledgeBase -Path $MapPath } else { Get-RBACKnowledgeBase -Name 'GuardrailMap' }

    $platformKey = $map.Keys | Where-Object { $_ -eq $Platform } | Select-Object -First 1
    if (-not $platformKey) {
        Write-PSFMessage -Level Debug -Message "Guardrail map has no entry for platform '$Platform'." -Tag 'PSAutoRBAC', 'Guard'
        return
    }
    $guards = $map[$platformKey]

    foreach ($type in ($guards.Keys | Sort-Object)) {
        if ($GuardType -and ($GuardType -notcontains $type)) { continue }
        $def = $guards[$type]

        # 'Both'-affecting guards are relevant to either an Action or a RoleAssignment filter.
        if ($Affects -and $def.Affects -ne 'Both' -and $def.Affects -ne $Affects) { continue }

        $status = if ($def.Category -eq 'Detective') { 'Detective' } else { 'Potential' }

        New-RBACGuardObject -Platform $Platform -GuardType $type `
            -Command $Command -CallerId $CallerId -Scope $Scope `
            -Affects $def.Affects -Category $def.Category -Severity $def.Severity `
            -Status $status -Detail $def.Detail -Mitigation $def.Mitigation `
            -Reference $def.Reference -Source 'KnowledgeBase'
    }
}

function Invoke-RBACProviderGuard {
    <#
    .SYNOPSIS
        Dispatches guardrail evaluation to a provider (or the advisory fallback).
    .DESCRIPTION
        Internal. If the provider implements the optional ResolveGuard member, it
        is invoked and owns both the offline advisory baseline and any live
        detection. Providers that do not implement it still yield the knowledge-base
        advisory guards via Get-RBACGuardDefinition, so guard analysis degrades
        gracefully for third-party providers. Returned guards are sorted by
        severity (High -> Medium -> Info) for stable, human-friendly output.
    .OUTPUTS
        PSCustomObject (PSAutoRBAC.Guard)
    #>

    [CmdletBinding()]
    [OutputType([psobject])]
    param(
        [Parameter(Mandatory)] [psobject]$Provider,
        [Parameter()] [string]$Command,
        [Parameter()] [string]$CallerId,
        [Parameter()] [string]$Scope,
        [Parameter()] [psobject]$Context,
        [Parameter()] [hashtable]$Options
    )

    $opts = @{}
    if ($Options) { $opts = $Options.Clone() }

    $guards = @()
    if ($Provider.PSObject.Properties.Name -contains 'ResolveGuard' -and $Provider.ResolveGuard) {
        $isLive = [bool]($opts.ContainsKey('Live') -and $opts['Live'])
        Write-PSFMessage -Level Debug -Message "Guard: dispatching to '$($Provider.Name)' ResolveGuard (live=$isLive)." -Tag 'PSAutoRBAC', 'Guard'
        $guards = @(& $Provider.ResolveGuard $Command $CallerId $Scope $Context $opts)
    }
    else {
        Write-PSFMessage -Level Debug -Message "Guard: '$($Provider.Name)' has no ResolveGuard; using advisory knowledge base." -Tag 'PSAutoRBAC', 'Guard'
        $defParams = @{ Platform = $Provider.Name; Command = $Command; CallerId = $CallerId; Scope = $Scope }
        if ($opts.ContainsKey('GuardType') -and $opts['GuardType']) { $defParams['GuardType'] = $opts['GuardType'] }
        if ($opts.ContainsKey('Affects')   -and $opts['Affects'])   { $defParams['Affects']   = $opts['Affects'] }
        if ($opts.ContainsKey('MapPath')   -and $opts['MapPath'])   { $defParams['MapPath']   = $opts['MapPath'] }
        $guards = @(Get-RBACGuardDefinition @defParams)
    }

    $severityRank = @{ 'High' = 0; 'Medium' = 1; 'Info' = 2 }
    $guards | Sort-Object -Property @{ Expression = { $severityRank[$_.Severity] } }, GuardType
}