Remediation/EntraID/ENT-LEGACY-AUTH.ps1

# UNTESTED: this script has never been run on a real tenant. Read it, try it on a test tenant first, and use it at your own risk.
<#
.SYNOPSIS
    Creates in report-only the policy that blocks legacy authentication, then enables it on decision (procedure ENT-LEGACY-AUTH).
 
.DESCRIPTION
    Depends on the procedure ENT-LEGACY-SIGN-INS. The policy is created in report-only on a decision, tenant by tenant: this mode cuts
    nothing, and with no decision nothing is planned. The activation waits until all the accounts of this tenant have a decision in
    decisions\ENT-LEGACY-SIGN-INS.csv, because the block could cut a printer or an application.
 
    Scope: all users, all resources, clients Exchange ActiveSync and Other clients, access blocked.
    Exclusions: emergency accounts, the sync accounts role and accounts marked Temporary exclusion. Each temporary exclusion stays
    exposed: owner and due date in the decisions file.
 
    decisions\ENT-LEGACY-AUTH.csv: one row per tenant, Report only or Enable. Nothing is created or enabled without a decision.
 
    Guards of the activation, whatever the decisions:
      - each legacy sign-in read has a valid decision, and the collection is complete;
      - the emergency accounts exist, are enabled, have a FIDO2 key in the target and are excluded; otherwise the script refuses;
      - the policy is in the snapshot and reads on the tenant in report-only, unchanged for at least three days;
      - security defaults are off.
    The exclusion is read back after the activation: if it is missing, the policy goes back to report-only.
 
    Limit: the link with EXO-PROTOCOLS stays to follow, authenticated SMTP also closes on the Exchange side.
#>

$c = Open-TcRemediation

$PolicyName = (Get-TcObjectName ' - Block legacy authentication')
$ReportOnlyState = $script:TcReportOnlyState
$UnknownAccount = '(account not resolved)'
$SignInChoices = @('Modern client', 'Other sending method', 'Replacement', 'Drop', 'Temporary exclusion')   # values of ENT-LEGACY-SIGN-INS
$SignInFile = 'ENT-LEGACY-SIGN-INS'





$signIns = if (Test-TcDecisionFile $SignInFile) { @(Read-TcDecisionRow $SignInFile) } else { @() }

$proposals = foreach ($t in $c.Tenants) {
    Write-TcRemediationTenant $t 'Legacy authentication block'
    $caPolicies = @(Get-TcRemediationTable $t 'conditionalAccess')
    if (-not $caPolicies.Count -and -not (Test-TcCollectionComplete $t 'conditionalAccess')) { Write-TcRemediationAlert 'Conditional access: incomplete collection, an empty table proves nothing.' }
    $activePolicies = @($caPolicies | Where-Object { $_['State'] -eq 'enabled' -and (ConvertTo-TcBoolean $_['Blocks legacy authentication']) })
    $accounts = @(Get-TcRemediationTable $t 'legacySignIns').Count
    $ofTenant = @($signIns | Where-Object { (Get-TcMember $_ 'Tenant') -eq $t })
    $pending = @($ofTenant | Where-Object { -not (Get-TcMember $_ 'Decision') }).Count
    Write-TcRemediationNote ("{0} active policy(ies) already block it; {1} legacy sign-in row(s) read, {2} with no decision" -f $activePolicies.Count, $accounts, $(if ($ofTenant.Count) { $pending } else { 'all' }))
    $existingItem = $caPolicies | Where-Object { $_['Name'] -eq $PolicyName } | Select-Object -First 1
    [ordered]@{ Tenant = $t; Policy = $PolicyName; 'State read' = $(if ($existingItem) { $existingItem['State'] } else { 'missing' }); 'Legacy sign-ins' = $accounts; Proposal = 'Report only' }
}
$decisions = @(Get-TcDecision -Name 'ENT-LEGACY-AUTH' -Proposal @($proposals | Where-Object { $_ }) -Key 'Tenant' -Choice 'Report only', 'Enable')

foreach ($t in $c.Tenants) {
    if (ConvertTo-TcBoolean (Get-TcRemediationValue $t 'securitySettings' 'Security defaults enabled')) { Write-TcRemediationNote ("{0}: security defaults already block legacy authentication" -f $t); continue }
    # only a policy on all accounts and all resources makes ours unnecessary: a partial policy would let the rest through
    $blockingOnes = @(Get-TcRemediationTable $t 'conditionalAccess' | Where-Object { $_['State'] -eq 'enabled' -and (ConvertTo-TcBoolean $_['Blocks legacy authentication']) })
    if (@($blockingOnes | Where-Object { $_['Users'] -eq 'All' -and $_['Applications'] -eq 'All' }).Count) { Write-TcRemediationNote ("{0}: an active policy already blocks legacy authentication for everyone: nothing to create" -f $t); continue }
    if ($blockingOnes.Count) { Write-TcRemediationNote ("{0}: {1} active policy(ies) block legacy authentication only for some of the accounts or resources: compare before enabling our own" -f $t, $blockingOnes.Count) }
    $missing = @(Get-TcMissingProtectedAccount $t)
    if ($missing.Count) { Write-TcRemediationAlert ("{0}: emergency accounts missing from the snapshot: {1}; enabling will stay refused." -f $t, ($missing -join ', ')) }
    $ofTenant = @($signIns | Where-Object { (Get-TcMember $_ 'Tenant') -eq $t })
    $temporary = @($ofTenant | Where-Object { (Get-TcMember $_ 'Decision') -eq 'Temporary exclusion' -and (Get-TcMember $_ 'Account') -ne $UnknownAccount } | ForEach-Object { (Get-TcMember $_ 'Account') } | Select-Object -Unique)
    $existingItem = Get-TcRemediationTable $t 'conditionalAccess' | Where-Object { $_['Name'] -eq $PolicyName } | Select-Object -First 1

    if (-not $existingItem -and -not @($decisions | Where-Object { (Get-TcMember $_ 'Tenant') -eq $t }).Count) { Write-TcRemediationNote ("{0}: no policy without a decision" -f $t) }
    elseif (-not $existingItem) {
        Add-TcBlockAction -Tenant $t -Service Graph -Object $PolicyName -Description ("Create policy {0} in report-only mode" -f $PolicyName) -Data @{ Temporary = @($temporary) } `
            -Test { param($a) [bool](Get-TcConditionalAccessPolicy $a.tenant $PolicyName) } `
            -Block {
                param($a)
                $x = Resolve-TcAccountId $a.tenant @(Get-TcProtectedAccount $a.tenant)
                foreach ($u in (Get-TcMember $x 'Missing')) { Write-TcRemediationAlert ("{0} does not exist yet: to exclude before enabling, the script handles it at that time" -f $u) }
                $others = Resolve-TcAccountId $a.tenant @($a.data.Temporary)
                $body = @{ displayName = $PolicyName; state = $ReportOnlyState
                            conditions = @{ users = @{ includeUsers = @('All'); excludeUsers = @(@((Get-TcMember $x 'Ids')) + @((Get-TcMember $others 'Ids'))); excludeRoles = @($script:TcSyncRoleId) }
                                            applications = @{ includeApplications = @('All') }; clientAppTypes = @('exchangeActiveSync', 'other') }
                            grantControls = @{ operator = 'OR'; builtInControls = @('block') } }
                $r = Invoke-TcGraph -Tenant $a.tenant -Method POST -Uri $script:TcPolicyUri -Body $body
                $a.undo = @{ type = 'Graph'; tenant = $a.tenant; method = 'DELETE'; uri = "$script:TcPolicyUri/$((Get-TcMember $r 'id'))" }
            }
    }

    $d = $decisions | Where-Object { (Get-TcMember $_ 'Tenant') -eq $t -and (Get-TcMember $_ 'Decision') -eq 'Enable' } | Select-Object -First 1
    if (-not $d) { continue }
    if (-not $existingItem) { Write-TcRemediationAlert ("{0}: enabling decided, but the policy is not yet in the snapshot: create, observe, refresh the snapshot." -f $t); continue }
    if ($existingItem['State'] -ne $ReportOnlyState) { continue }
    if (-not (Test-TcCollectionComplete $t 'legacySignIns')) { Write-TcRemediationAlert ("{0}: legacy sign-in collection incomplete: block postponed, run Invoke-TcAssessment again." -f $t); continue }
    $readItems = @(Get-TcRemediationTable $t 'legacySignIns' | ForEach-Object { @{ Tenant = $t; Account = $(if ($_['UPN']) { $_['UPN'] } else { $UnknownAccount }); Client = [string]$_['Client'] } })
    $noDecision = @(Get-TcUndecidedRow -Name 'ENT-LEGACY-SIGN-INS' -Key 'Tenant', 'Account', 'Client' -Choice $SignInChoices -Expected $readItems -Tenant $t)
    if ($noDecision.Count) { Write-TcRemediationAlert ("{0}: {1} legacy sign-in row(s) with no valid decision: block postponed so as not to cut a legitimate use (run ENT-LEGACY-SIGN-INS, then decide)." -f $t, $noDecision.Count); continue }
    if ($missing.Count -or -not @(Get-TcProtectedAccount $t).Count) { Write-TcRemediationAlert ("{0}: enabling blocked: emergency accounts missing." -f $t); continue }
    Add-TcPolicyActivation -Tenant $t -Name $PolicyName -Temporary $temporary
}

Invoke-TcRemediationPlan -Verify {
    param($t)
    $p = Get-TcConditionalAccessPolicy $t $PolicyName
    if (-not $p) { New-TcRemediationResult Info "policy missing: $PolicyName"; return }
    New-TcRemediationResult $(if ((Get-TcMember $p 'state') -eq 'enabled') { 'OK' } else { 'Info' }) ("policy: {0}" -f (Get-TcMember $p 'state'))
    if (@((Get-TcMember (Get-TcMember $p 'conditions') 'clientAppTypes')) -notcontains 'other') { New-TcRemediationResult Blocking 'the policy does not target the other clients' }
    if ($script:TcSyncRoleId -notin @((Get-TcMember (Get-TcMember (Get-TcMember $p 'conditions') 'users') 'excludeRoles'))) { New-TcRemediationResult Blocking 'the role of the sync accounts is not excluded' }
    Get-TcExclusionResult -Tenant $t -Policy $p
}
Close-TcRemediation