Checks/Teams/MET-Teams006-ExternalAccess.ps1

[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'METCheckInfo',
    Justification = 'Check metadata. Read from the AST by Get-METCheck and never executed.')]
param()

$METCheckInfo = @{
    Name           = 'External Access / Federation Allow-List'
    Severity       = 'High'
    Description    = 'Checks Get-CsTenantFederationConfiguration for open federation (AllowAllKnownDomains), Teams consumer access settings, an empty BlockedDomains deny-list, and a deny-list that does not cover subdomains (BlockAllSubdomains).'
    RequiresModule = @('MicrosoftTeams')
}

$issues = [System.Collections.Generic.List[string]]::new()
$retrievalErrors = [System.Collections.Generic.List[string]]::new()
$subdomainCoverageUnassessed = $false

# Check Teams federation (external access) allow-list scope
try {
    $config = Get-CsTenantFederationConfiguration -ErrorAction Stop

    $allowedDomainsValue = $config.AllowedDomains
    $isAllowAllKnownDomains = $false
    if ($allowedDomainsValue -eq 'AllowAllKnownDomains') {
        $isAllowAllKnownDomains = $true
    }
    elseif ($allowedDomainsValue -and $allowedDomainsValue.ToString() -eq 'AllowAllKnownDomains') {
        $isAllowAllKnownDomains = $true
    }

    if ($config.AllowFederatedUsers -eq $true -and $isAllowAllKnownDomains) {
        $issues.Add('Federation is open to all external domains (AllowAllKnownDomains) - any external Teams user can attempt to chat with your staff, a common vector for Teams-based phishing and vishing')
    }

    if ($config.AllowTeamsConsumer -eq $true) {
        $inboundOpen = $config.AllowTeamsConsumerInbound -ne $false
        if ($inboundOpen) {
            $issues.Add('Teams accounts not managed by any organization (consumer/personal accounts) are allowed to federate, and AllowTeamsConsumerInbound is enabled - unmanaged personal accounts can discover and initiate first contact with your staff, the higher-risk direction for Teams-based phishing and vishing')
        }
        else {
            $issues.Add('Teams accounts not managed by any organization (consumer/personal accounts) are allowed to federate, but AllowTeamsConsumerInbound is disabled - this is partially mitigated: personal/consumer accounts cannot discover or initiate contact with your staff, only your staff can start a conversation outbound')
        }

        $restrictProperty = $config.PSObject.Properties['RestrictTeamsConsumerToExternalUserProfiles']
        if ($restrictProperty -and $config.RestrictTeamsConsumerToExternalUserProfiles -eq $true) {
            $issues.Add('RestrictTeamsConsumerToExternalUserProfiles is enabled, further limiting consumer/personal account interaction to users in the Extended Directory external user profiles rather than any arbitrary personal account')
        }
    }

    if ($config.AllowFederatedUsers -eq $true -and -not $config.BlockedDomains) {
        $issues.Add('No explicit BlockedDomains deny-list is configured - there is no domain-level backstop in place as a defense-in-depth measure if the allow-list scope is ever widened')
    }

    # BlockedDomains is only consulted when AllowedDomains is AllowAllKnownDomains - under a
    # specific-domain allow-list only allow-listed domains can communicate, so subdomain
    # coverage of a dormant deny-list is not a current exposure.
    if ($config.AllowFederatedUsers -eq $true -and $isAllowAllKnownDomains -and $config.BlockedDomains) {
        $blockSubdomainsProperty = $config.PSObject.Properties['BlockAllSubdomains']
        if (-not $blockSubdomainsProperty -or $null -eq $blockSubdomainsProperty.Value) {
            $subdomainCoverageUnassessed = $true
        }
        elseif ($blockSubdomainsProperty.Value -ne $true) {
            $issues.Add('BlockAllSubdomains is disabled, so the BlockedDomains deny-list matches exact domains only - blocking contoso.com does not block marketing.contoso.com, and an attacker who controls a blocked domain can keep reaching your users from any subdomain of it')
        }
    }
}
catch {
    $retrievalErrors.Add("Could not retrieve tenant federation configuration: $($_.Exception.Message)")
    Write-Verbose "Could not retrieve tenant federation configuration: $_"
}

if ($issues.Count -gt 0) {
    $result = if ($issues | Where-Object { $_ -match 'AllowAllKnownDomains' }) { 'Fail' } else { 'Warning' }
    New-METCheckResult -CheckId 'MET-Teams006' -Category Teams -Name 'External Access / Federation Allow-List' `
        -Result $result -Severity High -AffectedObject 'Teams External Access Configuration' `
        -Finding ($issues -join '; ') `
        -Recommendation 'Restrict AllowedDomains to a specific, reviewed allow-list of trusted partner domains instead of AllowAllKnownDomains, and configure a BlockedDomains deny-list as a defense-in-depth backstop, with Set-CsTenantFederationConfiguration -BlockAllSubdomains $true so each blocked domain also covers its subdomains. Disable AllowTeamsConsumer unless there is a specific business need for staff to chat with personal Teams/Skype accounts; if it must stay enabled, run Set-CsTenantFederationConfiguration -AllowTeamsConsumerInbound $false so personal/consumer accounts cannot discover or initiate contact with your organization, and consider -RestrictTeamsConsumerToExternalUserProfiles $true to further narrow exposure. Run: Set-CsTenantFederationConfiguration -AllowedDomains <AllowedDomainsObject> to scope federation, or -AllowFederatedUsers $false to disable entirely.' `
        -ReferenceUrl 'https://learn.microsoft.com/en-us/powershell/module/microsoftteams/set-cstenantfederationconfiguration' `
        -ErrorMessage ($retrievalErrors -join "`n")
}
elseif ($retrievalErrors.Count -gt 0) {
    New-METCheckResult -CheckId 'MET-Teams006' -Category Teams -Name 'External Access / Federation Allow-List' `
        -Result Warning -Severity High -AffectedObject 'Teams External Access Configuration' `
        -Finding 'The Teams tenant federation configuration could not be read, so the federation allow-list scope, consumer-account federation and the blocked-domain deny-list were not assessed.' `
        -Recommendation 'Ensure the MicrosoftTeams module is installed and the session has permission to read the tenant federation configuration, then rerun the assessment.' `
        -ReferenceUrl 'https://learn.microsoft.com/en-us/powershell/module/microsoftteams/set-cstenantfederationconfiguration' `
        -ErrorMessage ($retrievalErrors -join "`n")
}
else {
    New-METCheckResult -CheckId 'MET-Teams006' -Category Teams -Name 'External Access / Federation Allow-List' `
        -Result Pass -Severity High -AffectedObject 'Teams External Access Configuration' `
        -Finding 'Teams external access (federation) is appropriately scoped' `
        -ReferenceUrl 'https://learn.microsoft.com/en-us/powershell/module/microsoftteams/set-cstenantfederationconfiguration'
}

if ($subdomainCoverageUnassessed) {
    New-METCheckResult -CheckId 'MET-Teams006' -Category Teams -Name 'Blocked Domain Subdomain Coverage' `
        -Result NotApplicable -Severity High -AffectedObject 'Teams External Access Configuration' `
        -Finding 'The BlockAllSubdomains property was not returned, so whether subdomains of the BlockedDomains entries are also blocked was not established. By default blocking contoso.com does not block marketing.contoso.com, so an unconfirmed state is reported as unassessed rather than a pass.' `
        -Recommendation 'Confirm directly: Get-CsTenantFederationConfiguration | Format-List BlockedDomains, BlockAllSubdomains. If the property is absent there too, update the MicrosoftTeams module and re-run this check; to cover subdomains run Set-CsTenantFederationConfiguration -BlockAllSubdomains $true.' `
        -ReferenceUrl 'https://learn.microsoft.com/en-us/powershell/module/microsoftteams/set-cstenantfederationconfiguration' `
        -ErrorMessage 'The BlockAllSubdomains property was not returned by Get-CsTenantFederationConfiguration - the installed MicrosoftTeams module version may not expose it.'
}