Views/ConditionalAccess.ps1

function Show-InTUIConditionalAccessView {
    <#
    .SYNOPSIS
        Displays the Conditional Access view with policies, named locations, and sign-in logs.
    #>

    [CmdletBinding()]
    param()

    $exitView = $false

    while (-not $exitView) {
        Clear-Host
        Show-InTUIHeader
        Show-InTUIBreadcrumb -Path @('Home', 'Conditional Access')

        $choices = @(
            'Policies',
            'Named Locations',
            'Sign-in Logs',
            '─────────────',
            'Back to Home'
        )

        $selection = Show-InTUIMenu -Title "[DeepSkyBlue1]Conditional Access[/]" -Choices $choices

        Write-InTUILog -Message "Conditional Access view selection" -Context @{ Selection = $selection }

        switch ($selection) {
            'Policies' {
                Show-InTUIConditionalAccessPolicyList
            }
            'Named Locations' {
                Show-InTUINamedLocationList
            }
            'Sign-in Logs' {
                Show-InTUISignInLogs
            }
            'Back to Home' {
                $exitView = $true
            }
            default {
                continue
            }
        }
    }
}

function Show-InTUIConditionalAccessPolicyList {
    <#
    .SYNOPSIS
        Displays a list of Conditional Access policies.
    #>

    [CmdletBinding()]
    param()

    $exitList = $false

    while (-not $exitList) {
        Clear-Host
        Show-InTUIHeader
        Show-InTUIBreadcrumb -Path @('Home', 'Conditional Access', 'Policies')

        $params = @{
            Uri      = '/identity/conditionalAccess/policies'
            Beta     = $false
            PageSize = 25
            Select   = 'id,displayName,state,createdDateTime,modifiedDateTime'
        }

        $policies = Show-InTUILoading -Title "[DeepSkyBlue1]Loading Conditional Access policies...[/]" -ScriptBlock {
            Get-InTUIPagedResults @params
        }

        if ($null -eq $policies -or $policies.Results.Count -eq 0) {
            Show-InTUIWarning "No Conditional Access policies found."
            Read-InTUIKey
            $exitList = $true
            continue
        }

        $policyChoices = @()
        foreach ($policy in $policies.Results) {
            $modified = Format-InTUIDate -DateString $policy.modifiedDateTime
            $stateDisplay = switch ($policy.state) {
                'enabled'                              { '[green]enabled[/]' }
                'disabled'                             { '[grey]disabled[/]' }
                'enabledForReportingButNotEnforced'     { '[yellow]report-only[/]' }
                default                                { "[grey]$($policy.state)[/]" }
            }

            $safeName = ConvertTo-InTUISafeMarkup -Text $policy.displayName
            $displayName = "[white]$safeName[/] [grey]| $stateDisplay | $modified[/]"
            $policyChoices += $displayName
        }

        $choiceMap = Get-InTUIChoiceMap -Choices $policyChoices
        $menuChoices = @($choiceMap.Choices + '─────────────' + 'Back')

        Show-InTUIStatusBar -Total $policies.TotalCount -Showing $policies.Results.Count
        $selection = Show-InTUIMenu -Title "[DeepSkyBlue1]Select a policy[/]" -Choices $menuChoices

        if ($selection -eq 'Back') {
            $exitList = $true
        }
        elseif ($selection -ne '─────────────') {
            $idx = $choiceMap.IndexMap[$selection]
            if ($null -ne $idx -and $idx -lt $policies.Results.Count) {
                Show-InTUIConditionalAccessPolicyDetail -PolicyId $policies.Results[$idx].id
            }
        }
    }
}

function Show-InTUIConditionalAccessPolicyDetail {
    <#
    .SYNOPSIS
        Displays detailed information about a specific Conditional Access policy.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$PolicyId
    )

    $exitDetail = $false

    while (-not $exitDetail) {
        Clear-Host
        Show-InTUIHeader

        $policy = Show-InTUILoading -Title "[DeepSkyBlue1]Loading policy details...[/]" -ScriptBlock {
            Invoke-InTUIGraphRequest -Uri "/identity/conditionalAccess/policies/$PolicyId"
        }

        if ($null -eq $policy) {
            Show-InTUIError "Failed to load policy details."
            Read-InTUIKey
            return
        }

        Show-InTUIBreadcrumb -Path @('Home', 'Conditional Access', 'Policies', $policy.displayName)

        # Panel 1: Properties
        $stateDisplay = switch ($policy.state) {
            'enabled'                              { '[green]enabled[/]' }
            'disabled'                             { '[grey]disabled[/]' }
            'enabledForReportingButNotEnforced'     { '[yellow]report-only[/]' }
            default                                { $policy.state }
        }

        $propsContent = @"
[bold white]$(ConvertTo-InTUISafeMarkup -Text $policy.displayName)[/]

[grey]State:[/] $stateDisplay
[grey]Created:[/] $(Format-InTUIDate -DateString $policy.createdDateTime)
[grey]Modified:[/] $(Format-InTUIDate -DateString $policy.modifiedDateTime)
"@


        Show-InTUIPanel -Title "[DeepSkyBlue1]Properties[/]" -Content $propsContent -BorderColor DeepSkyBlue1

        # Panel 2: Conditions summary
        $conditions = $policy.conditions
        $condContent = ""

        # Users
        $includeUsers = $conditions.users.includeUsers
        $excludeUsers = $conditions.users.excludeUsers
        $includeGroups = $conditions.users.includeGroups
        $excludeGroups = $conditions.users.excludeGroups

        $usersInclude = if ($includeUsers -contains 'All') { 'All users' }
                        elseif ($includeUsers) { "$(@($includeUsers).Count) user(s)" }
                        else { 'None' }
        if ($includeGroups -and @($includeGroups).Count -gt 0) {
            $usersInclude += ", $(@($includeGroups).Count) group(s)"
        }

        $usersExclude = if ($excludeUsers -and @($excludeUsers).Count -gt 0) { "$(@($excludeUsers).Count) user(s)" } else { '' }
        if ($excludeGroups -and @($excludeGroups).Count -gt 0) {
            $groupExcl = "$(@($excludeGroups).Count) group(s)"
            $usersExclude = if ($usersExclude) { "$usersExclude, $groupExcl" } else { $groupExcl }
        }
        if (-not $usersExclude) { $usersExclude = 'None' }

        $condContent += "[grey]Users Include:[/] $usersInclude"
        $condContent += "`n[grey]Users Exclude:[/] $usersExclude"

        # Applications
        $includeApps = $conditions.applications.includeApplications
        $excludeApps = $conditions.applications.excludeApplications

        $appsInclude = if ($includeApps -contains 'All') { 'All cloud apps' }
                       elseif ($includeApps) { "$(@($includeApps).Count) app(s)" }
                       else { 'None' }

        $appsExclude = if ($excludeApps -and @($excludeApps).Count -gt 0) { "$(@($excludeApps).Count) app(s)" } else { 'None' }

        $condContent += "`n[grey]Apps Include:[/] $appsInclude"
        $condContent += "`n[grey]Apps Exclude:[/] $appsExclude"

        # Platforms
        if ($conditions.platforms) {
            $platInclude = if ($conditions.platforms.includePlatforms) {
                ($conditions.platforms.includePlatforms -join ', ')
            } else { 'None' }
            $condContent += "`n[grey]Platforms:[/] $platInclude"
        }

        # Locations
        if ($conditions.locations) {
            $locInclude = if ($conditions.locations.includeLocations) {
                ($conditions.locations.includeLocations -join ', ')
            } else { 'None' }
            $locExclude = if ($conditions.locations.excludeLocations) {
                ($conditions.locations.excludeLocations -join ', ')
            } else { 'None' }
            $condContent += "`n[grey]Locations Include:[/] $locInclude"
            $condContent += "`n[grey]Locations Exclude:[/] $locExclude"
        }

        # Client app types
        if ($conditions.clientAppTypes) {
            $condContent += "`n[grey]Client App Types:[/] $($conditions.clientAppTypes -join ', ')"
        }

        # Sign-in risk levels
        if ($conditions.signInRiskLevels -and @($conditions.signInRiskLevels).Count -gt 0) {
            $condContent += "`n[grey]Sign-in Risk:[/] $($conditions.signInRiskLevels -join ', ')"
        }

        Show-InTUIPanel -Title "[DeepSkyBlue1]Conditions[/]" -Content $condContent -BorderColor DeepSkyBlue1

        # Panel 3: Grant Controls
        $grantControls = $policy.grantControls
        if ($grantControls) {
            $builtIn = if ($grantControls.builtInControls) {
                $grantControls.builtInControls -join ', '
            } else { 'None' }
            $operator = $grantControls.operator ?? 'N/A'

            $grantContent = @"
[grey]Built-in Controls:[/] $builtIn
[grey]Operator:[/] $operator
"@

            Show-InTUIPanel -Title "[DeepSkyBlue1]Grant Controls[/]" -Content $grantContent -BorderColor DeepSkyBlue1
        }

        # Panel 4: Session Controls
        $sessionControls = $policy.sessionControls
        if ($sessionControls) {
            $sessionContent = ""
            $hasSession = $false

            if ($sessionControls.signInFrequency) {
                $sif = $sessionControls.signInFrequency
                $sessionContent += "[grey]Sign-in Frequency:[/] $($sif.value) $($sif.type) (enabled: $($sif.isEnabled))"
                $hasSession = $true
            }
            if ($sessionControls.persistentBrowser) {
                $pb = $sessionControls.persistentBrowser
                if ($hasSession) { $sessionContent += "`n" }
                $sessionContent += "[grey]Persistent Browser:[/] $($pb.mode) (enabled: $($pb.isEnabled))"
                $hasSession = $true
            }
            if ($sessionControls.cloudAppSecurity) {
                $cas = $sessionControls.cloudAppSecurity
                if ($hasSession) { $sessionContent += "`n" }
                $sessionContent += "[grey]Cloud App Security:[/] $($cas.cloudAppSecurityType) (enabled: $($cas.isEnabled))"
                $hasSession = $true
            }
            if ($sessionControls.applicationEnforcedRestrictions) {
                $aer = $sessionControls.applicationEnforcedRestrictions
                if ($hasSession) { $sessionContent += "`n" }
                $sessionContent += "[grey]App Enforced Restrictions:[/] enabled: $($aer.isEnabled)"
                $hasSession = $true
            }

            if ($hasSession) {
                Show-InTUIPanel -Title "[DeepSkyBlue1]Session Controls[/]" -Content $sessionContent -BorderColor DeepSkyBlue1
            }
        }

        $actionChoices = @(
            '─────────────',
            'Back to Policies'
        )

        $action = Show-InTUIMenu -Title "[DeepSkyBlue1]Policy Actions[/]" -Choices $actionChoices

        Write-InTUILog -Message "CA policy detail action" -Context @{ PolicyId = $PolicyId; PolicyName = $policy.displayName; Action = $action }

        switch ($action) {
            'Back to Policies' {
                $exitDetail = $true
            }
            default {
                continue
            }
        }
    }
}

function Show-InTUINamedLocationList {
    <#
    .SYNOPSIS
        Displays a list of Named Locations.
    #>

    [CmdletBinding()]
    param()

    $exitList = $false

    while (-not $exitList) {
        Clear-Host
        Show-InTUIHeader
        Show-InTUIBreadcrumb -Path @('Home', 'Conditional Access', 'Named Locations')

        $params = @{
            Uri      = '/identity/conditionalAccess/namedLocations'
            Beta     = $false
            PageSize = 25
            Select   = 'id,displayName,createdDateTime,modifiedDateTime'
        }

        $locations = Show-InTUILoading -Title "[DeepSkyBlue1]Loading named locations...[/]" -ScriptBlock {
            Get-InTUIPagedResults @params
        }

        if ($null -eq $locations -or $locations.Results.Count -eq 0) {
            Show-InTUIWarning "No named locations found."
            Read-InTUIKey
            $exitList = $true
            continue
        }

        $locationChoices = @()
        foreach ($location in $locations.Results) {
            $modified = Format-InTUIDate -DateString $location.modifiedDateTime
            $locType = switch -Wildcard ($location.'@odata.type') {
                '*ipNamedLocation'      { 'IP Ranges' }
                '*countryNamedLocation'  { 'Countries' }
                default                  { 'Unknown' }
            }

            $displayName = "[white]$(ConvertTo-InTUISafeMarkup -Text $location.displayName)[/] [grey]| $locType | $modified[/]"
            $locationChoices += $displayName
        }

        $choiceMap = Get-InTUIChoiceMap -Choices $locationChoices
        $menuChoices = @($choiceMap.Choices + '─────────────' + 'Back')

        Show-InTUIStatusBar -Total $locations.TotalCount -Showing $locations.Results.Count

        $selection = Show-InTUIMenu -Title "[DeepSkyBlue1]Select a named location[/]" -Choices $menuChoices

        if ($selection -eq 'Back') {
            $exitList = $true
        }
        elseif ($selection -ne '─────────────') {
            $idx = $choiceMap.IndexMap[$selection]
            if ($null -ne $idx -and $idx -lt $locations.Results.Count) {
                Show-InTUINamedLocationDetail -LocationId $locations.Results[$idx].id
            }
        }
    }
}

function Show-InTUINamedLocationDetail {
    <#
    .SYNOPSIS
        Displays detailed information about a specific Named Location.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$LocationId
    )

    $exitDetail = $false

    while (-not $exitDetail) {
        Clear-Host
        Show-InTUIHeader

        $location = Show-InTUILoading -Title "[DeepSkyBlue1]Loading named location details...[/]" -ScriptBlock {
            Invoke-InTUIGraphRequest -Uri "/identity/conditionalAccess/namedLocations/$LocationId"
        }

        if ($null -eq $location) {
            Show-InTUIError "Failed to load named location details."
            Read-InTUIKey
            return
        }

        Show-InTUIBreadcrumb -Path @('Home', 'Conditional Access', 'Named Locations', $location.displayName)

        $locType = switch -Wildcard ($location.'@odata.type') {
            '*ipNamedLocation'      { 'IP Ranges' }
            '*countryNamedLocation'  { 'Countries' }
            default                  { 'Unknown' }
        }

        $propsContent = @"
[bold white]$(ConvertTo-InTUISafeMarkup -Text $location.displayName)[/]

[grey]Type:[/] $locType
[grey]Created:[/] $(Format-InTUIDate -DateString $location.createdDateTime)
[grey]Modified:[/] $(Format-InTUIDate -DateString $location.modifiedDateTime)
"@


        # IP Named Location details
        if ($location.'@odata.type' -like '*ipNamedLocation') {
            $propsContent += "`n[grey]Is Trusted:[/] $($location.isTrusted ?? $false)"

            if ($location.ipRanges -and @($location.ipRanges).Count -gt 0) {
                $propsContent += "`n`n[bold white]IP Ranges:[/]"
                foreach ($range in $location.ipRanges) {
                    $propsContent += "`n $($range.cidrAddress)"
                }
            }
        }

        # Country Named Location details
        if ($location.'@odata.type' -like '*countryNamedLocation') {
            $propsContent += "`n[grey]Include Unknown:[/] $($location.includeUnknownCountriesAndRegions ?? $false)"

            if ($location.countriesAndRegions -and @($location.countriesAndRegions).Count -gt 0) {
                $propsContent += "`n`n[bold white]Countries and Regions:[/]"
                $propsContent += "`n $($location.countriesAndRegions -join ', ')"
            }
        }

        Show-InTUIPanel -Title "[DeepSkyBlue1]Named Location Properties[/]" -Content $propsContent -BorderColor DeepSkyBlue1

        $actionChoices = @(
            '─────────────',
            'Back to Named Locations'
        )

        $action = Show-InTUIMenu -Title "[DeepSkyBlue1]Location Actions[/]" -Choices $actionChoices

        Write-InTUILog -Message "Named location detail action" -Context @{ LocationId = $LocationId; LocationName = $location.displayName; Action = $action }

        switch ($action) {
            'Back to Named Locations' {
                $exitDetail = $true
            }
            default {
                continue
            }
        }
    }
}

function Show-InTUISignInLogs {
    <#
    .SYNOPSIS
        Displays sign-in logs with filtering for Conditional Access analysis.
    #>

    [CmdletBinding()]
    param()

    Clear-Host
    Show-InTUIHeader
    Show-InTUIBreadcrumb -Path @('Home', 'Conditional Access', 'Sign-in Logs')

    $filterChoices = @(
        'All Recent',
        'Failures Only',
        'Specific User',
        '─────────────',
        'Back'
    )

    $filterSelection = Show-InTUIMenu -Title "[DeepSkyBlue1]Sign-in Log Filter[/]" -Choices $filterChoices

    Write-InTUILog -Message "Sign-in logs filter selection" -Context @{ Filter = $filterSelection }

    if ($filterSelection -eq 'Back' -or $filterSelection -eq '─────────────') {
        return
    }

    $filter = $null

    switch ($filterSelection) {
        'Failures Only' {
            $filter = 'status/errorCode ne 0'
        }
        'Specific User' {
            $upn = Read-InTUITextInput -Message "[DeepSkyBlue1]Enter user principal name (UPN)[/]"
            if (-not $upn) { return }
            $safeUpn = ConvertTo-InTUISafeFilterValue -Value $upn
            $filter = "userPrincipalName eq '$safeUpn'"
            Write-InTUILog -Message "Sign-in logs filtering by user" -Context @{ UPN = $upn }
        }
    }

    $params = @{
        Uri      = '/auditLogs/signIns'
        Beta     = $false
        PageSize = 25
        Select   = 'id,userDisplayName,userPrincipalName,appDisplayName,ipAddress,status,createdDateTime,conditionalAccessStatus'
    }

    if ($filter) {
        $params['Filter'] = $filter
    }

    $signIns = Show-InTUILoading -Title "[DeepSkyBlue1]Loading sign-in logs...[/]" -ScriptBlock {
        Get-InTUIPagedResults @params
    }

    if ($null -eq $signIns -or $signIns.Results.Count -eq 0) {
        Show-InTUIWarning "No sign-in logs found."
        Read-InTUIKey
        return
    }

    Clear-Host
    Show-InTUIHeader
    Show-InTUIBreadcrumb -Path @('Home', 'Conditional Access', 'Sign-in Logs', $filterSelection)

    $rows = @()
    foreach ($signIn in $signIns.Results) {
        $statusDisplay = if ($signIn.status.errorCode -eq 0) {
            '[green]Success[/]'
        } else {
            "[red]Failed: $($signIn.status.errorCode)[/]"
        }

        $caStatus = switch ($signIn.conditionalAccessStatus) {
            'success'    { '[green]success[/]' }
            'failure'    { '[red]failure[/]' }
            'notApplied' { '[grey]notApplied[/]' }
            default      { "[grey]$($signIn.conditionalAccessStatus)[/]" }
        }

        $time = Format-InTUIDate -DateString $signIn.createdDateTime

        $rows += , @(
            ($signIn.userDisplayName ?? 'N/A'),
            ($signIn.appDisplayName ?? 'N/A'),
            ($signIn.ipAddress ?? 'N/A'),
            $statusDisplay,
            $caStatus,
            $time
        )
    }

    Show-InTUITable -Title "Sign-in Logs ($filterSelection)" -Columns @('User', 'App', 'IP', 'Status', 'CA Status', 'Time') -Rows $rows

    Read-InTUIKey
}