Collaboration/Get-TeamsSecurityConfig.ps1

<#
.SYNOPSIS
    Collects Microsoft Teams security and meeting configuration settings.
.DESCRIPTION
    Queries Microsoft Graph for Teams security-relevant settings including meeting
    policies, external access, messaging policies, and third-party app restrictions.
    Returns a structured inventory of settings with current values and CIS benchmark
    recommendations.
 
    Requires the following Graph API permissions:
    TeamSettings.Read.All, TeamworkAppSettings.Read.All
.PARAMETER OutputPath
    Optional path to export results as CSV. If not specified, results are returned to the pipeline.
.EXAMPLE
    PS> . .\Common\Connect-Service.ps1
    PS> Connect-Service -Service Graph -Scopes 'TeamSettings.Read.All','TeamworkAppSettings.Read.All'
    PS> .\Collaboration\Get-TeamsSecurityConfig.ps1
 
    Displays Teams security configuration settings.
.NOTES
    Author: Daren9m
    Settings checked are aligned with CIS Microsoft 365 Foundations Benchmark v6.0.1 recommendations.
#>

[CmdletBinding()]
param(
    [Parameter()]
    [ValidateNotNullOrEmpty()]
    [string]$OutputPath
)

# Continue on errors: non-critical checks should not block remaining assessments.
$ErrorActionPreference = 'Continue'

# Verify Graph connection
if (-not (Assert-GraphConnection)) { return }
$context = Get-MgContext

# Detect app-only auth — Teams Graph APIs (/v1.0/teamwork/*) do not support
# application-only context and return HTTP 412 "not supported in application-only context".
$isAppOnly = $context.AuthType -eq 'AppOnly' -or (-not $context.Account -and $context.AppName)
if ($isAppOnly) {
    Write-Warning "Teams Graph APIs do not support app-only (certificate) authentication. Teams security checks require delegated (interactive) auth. Skipping Teams collector."
    Write-Output @()
    return
}

# Detect whether the tenant has any Teams-capable licenses.
# If no Teams service plans are assigned, the /teamwork/* Graph endpoints return
# 400/404 errors, producing misleading warnings in the assessment log.
try {
    $subscribedSkus = Get-MgSubscribedSku -ErrorAction Stop
    $teamsServicePlanIds = @(
        '57ff2da0-773e-42df-b2af-ffb7a2317929'  # TEAMS1 (standard Teams service plan)
        '4a51bca5-1eff-43f5-878c-177680f191af'  # TEAMS1 (Gov)
    )
    $hasTeams = $false
    foreach ($sku in $subscribedSkus) {
        if ($sku.ConsumedUnits -gt 0) {
            foreach ($sp in $sku.ServicePlans) {
                if ($sp.ServicePlanId -in $teamsServicePlanIds -and $sp.ProvisioningStatus -ne 'Disabled') {
                    $hasTeams = $true
                    break
                }
            }
        }
        if ($hasTeams) { break }
    }
    if (-not $hasTeams) {
        Write-Warning "No Teams licenses detected in this tenant. Skipping Teams security checks to avoid false errors."
        Write-Output @()
        return
    }
}
catch {
    # If we can't check licenses, proceed with Teams checks and let them fail naturally
    Write-Warning "Could not verify Teams licensing: $($_.Exception.Message). Proceeding with Teams checks."
}

# Load shared security-config helpers
$_scriptDir = if ($MyInvocation.MyCommand.Path) { Split-Path -Parent $MyInvocation.MyCommand.Path } else { $PSScriptRoot }
. (Join-Path -Path $_scriptDir -ChildPath '..\Common\SecurityConfigHelper.ps1')

$ctx = Initialize-SecurityConfig
$settings = $ctx.Settings
$checkIdCounter = $ctx.CheckIdCounter

function Add-Setting {
    param(
        [string]$Category, [string]$Setting, [string]$CurrentValue,
        [string]$RecommendedValue, [string]$Status,
        [string]$CheckId = '', [string]$Remediation = ''
    )
    $p = @{
        Settings         = $settings
        CheckIdCounter   = $checkIdCounter
        Category         = $Category
        Setting          = $Setting
        CurrentValue     = $CurrentValue
        RecommendedValue = $RecommendedValue
        Status           = $Status
        CheckId          = $CheckId
        Remediation      = $Remediation
    }
    Add-SecuritySetting @p
}

# ------------------------------------------------------------------
# 1. Teams Client Configuration (external access)
# ------------------------------------------------------------------
try {
    Write-Verbose "Checking Teams external access settings..."
    $graphParams = @{
        Method      = 'GET'
        Uri         = '/v1.0/teamwork/teamsAppSettings'
        ErrorAction = 'Stop'
    }
    $teamsSettings = Invoke-MgGraphRequest @graphParams

    $isSideloadingAllowed = $teamsSettings['isChatResourceSpecificConsentEnabled']
    $settingParams = @{
        Category         = 'Teams Apps'
        Setting          = 'Chat Resource-Specific Consent'
        CurrentValue     = "$isSideloadingAllowed"
        RecommendedValue = 'False'
        Status           = if (-not $isSideloadingAllowed) { 'Pass' } else { 'Review' }
        CheckId          = 'TEAMS-APPS-001'
        Remediation      = 'Run: Set-CsTeamsAppPermissionPolicy -DefaultCatalogAppsType AllowedAppList. Teams admin center > Teams apps > Permission policies.'
    }
    Add-Setting @settingParams
}
catch {
    Write-Warning "Could not retrieve Teams app settings: $_"
}

# ------------------------------------------------------------------
# 1b. Teams Client Configuration — unmanaged users (CIS 8.2.2, 8.2.3)
# ------------------------------------------------------------------
try {
    Write-Verbose "Checking Teams client configuration for unmanaged users..."
    $graphParams = @{
        Method      = 'GET'
        Uri         = '/beta/teamwork/teamsClientConfiguration'
        ErrorAction = 'Stop'
    }
    $teamsClientConfig = Invoke-MgGraphRequest @graphParams

    if ($teamsClientConfig) {
        $allowConsumer = $teamsClientConfig['allowTeamsConsumer']
        $allowConsumerInbound = $teamsClientConfig['allowTeamsConsumerInbound']

        $settingParams = @{
            Category         = 'External Access'
            Setting          = 'Communication with Unmanaged Teams Users'
            CurrentValue     = "$allowConsumer"
            RecommendedValue = 'False'
            Status           = if (-not $allowConsumer) { 'Pass' } else { 'Fail' }
            CheckId          = 'TEAMS-EXTACCESS-001'
            Remediation      = 'Run: Set-CsTenantFederationConfiguration -AllowTeamsConsumer $false. Teams admin center > Users > External access > Teams accounts not managed by an organization > Off.'
        }
        Add-Setting @settingParams

        $settingParams = @{
            Category         = 'External Access'
            Setting          = 'External Unmanaged Users Can Initiate Conversations'
            CurrentValue     = "$allowConsumerInbound"
            RecommendedValue = 'False'
            Status           = if (-not $allowConsumerInbound) { 'Pass' } else { 'Fail' }
            CheckId          = 'TEAMS-EXTACCESS-002'
            Remediation      = 'Run: Set-CsTenantFederationConfiguration -AllowTeamsConsumerInbound $false. Teams admin center > Users > External access > External users can initiate conversations > Off.'
        }
        Add-Setting @settingParams

        # CIS 8.1.1 — Third-party cloud storage restricted
        $cloudStorageKeys = @('allowDropBox', 'allowBox', 'allowGoogleDrive', 'allowShareFile', 'allowEgnyte')
        $enabledStores = @()
        foreach ($key in $cloudStorageKeys) {
            if ($teamsClientConfig.ContainsKey($key) -and $teamsClientConfig[$key]) {
                $enabledStores += $key -replace '^allow', ''
            }
        }
        $cloudStorageStatus = if ($enabledStores.Count -eq 0) { 'Pass' } else { 'Fail' }
        $settingParams = @{
            Category         = 'Client Configuration'
            Setting          = 'Third-Party Cloud Storage'
            CurrentValue     = if ($enabledStores.Count -eq 0) { 'All disabled' } else { "Enabled: $($enabledStores -join ', ')" }
            RecommendedValue = 'All disabled'
            Status           = $cloudStorageStatus
            CheckId          = 'TEAMS-CLIENT-001'
            Remediation      = 'Run: Set-CsTeamsClientConfiguration -AllowDropBox $false -AllowBox $false -AllowGoogleDrive $false -AllowShareFile $false -AllowEgnyte $false. Teams admin center > Messaging policies > Manage third-party storage.'
        }
        Add-Setting @settingParams

        # CIS 8.1.2 — Channel email disabled
        $allowEmail = $teamsClientConfig['allowEmailIntoChannel']
        $settingParams = @{
            Category         = 'Client Configuration'
            Setting          = 'Email Into Channel'
            CurrentValue     = "$allowEmail"
            RecommendedValue = 'False'
            Status           = if (-not $allowEmail) { 'Pass' } else { 'Fail' }
            CheckId          = 'TEAMS-CLIENT-002'
            Remediation      = 'Run: Set-CsTeamsClientConfiguration -AllowEmailIntoChannel $false. Teams admin center > Teams settings > Email integration > Users can send emails to a channel email address > Off.'
        }
        Add-Setting @settingParams

        # CIS 8.2.1 — External domain access restricted
        $allowFederated = $teamsClientConfig['allowFederatedUsers']
        $allowedDomains = $teamsClientConfig['allowedDomains']
        $domainRestricted = (-not $allowFederated) -or ($allowedDomains -and $allowedDomains.Count -gt 0)
        $settingParams = @{
            Category         = 'External Access'
            Setting          = 'External Domain Access'
            CurrentValue     = if (-not $allowFederated) { 'Disabled' } elseif ($allowedDomains -and $allowedDomains.Count -gt 0) { "Restricted to $($allowedDomains.Count) domains" } else { 'Open to all domains' }
            RecommendedValue = 'Disabled or restricted to specific domains'
            Status           = if ($domainRestricted) { 'Pass' } else { 'Fail' }
            CheckId          = 'TEAMS-EXTACCESS-003'
            Remediation      = 'Run: Set-CsTenantFederationConfiguration -AllowFederatedUsers $false (or restrict with -AllowedDomains). Teams admin center > Users > External access > Choose which external domains your users have access to > Allow only specific external domains.'
        }
        Add-Setting @settingParams

        # CIS 8.2.4 — Skype for Business interop disabled
        $allowPublicUsers = $teamsClientConfig['allowPublicUsers']
        $settingParams = @{
            Category         = 'External Access'
            Setting          = 'Skype for Business/Consumer Interop'
            CurrentValue     = "$allowPublicUsers"
            RecommendedValue = 'False'
            Status           = if (-not $allowPublicUsers) { 'Pass' } else { 'Fail' }
            CheckId          = 'TEAMS-EXTACCESS-004'
            Remediation      = 'Run: Set-CsTenantFederationConfiguration -AllowPublicUsers $false. Teams admin center > Users > External access > Skype users > Off.'
        }
        Add-Setting @settingParams
    }
}
catch {
    Write-Warning "Teams client configuration endpoint unavailable: $($_.Exception.Message)"
}

# ------------------------------------------------------------------
# 2. Teams Meeting Policies (via beta API)
# ------------------------------------------------------------------
try {
    Write-Verbose "Checking Teams meeting policy..."
    $graphParams = @{
        Method      = 'GET'
        Uri         = '/beta/teamwork/teamsMeetingPolicy'
        ErrorAction = 'Stop'
    }
    $meetingPolicy = Invoke-MgGraphRequest @graphParams

    if ($meetingPolicy) {
        $anonymousJoin = $meetingPolicy['allowAnonymousUsersToJoinMeeting']
        $settingParams = @{
            Category         = 'Meeting Policy'
            Setting          = 'Anonymous Users Can Join Meeting'
            CurrentValue     = "$anonymousJoin"
            RecommendedValue = 'False'
            Status           = if (-not $anonymousJoin) { 'Pass' } else { 'Fail' }
            CheckId          = 'TEAMS-MEETING-001'
            Remediation      = 'Run: Set-CsTeamsMeetingPolicy -Identity Global -AllowAnonymousUsersToJoinMeeting $false. Teams admin center > Meetings > Meeting policies > Global > Anonymous users can join a meeting > Off.'
        }
        Add-Setting @settingParams

        # Anonymous/dial-in can't start meeting (CIS 8.5.2)
        $anonStart = $meetingPolicy['allowAnonymousUsersToStartMeeting']
        $settingParams = @{
            Category         = 'Meeting Policy'
            Setting          = 'Anonymous Users Can Start Meeting'
            CurrentValue     = "$anonStart"
            RecommendedValue = 'False'
            Status           = if (-not $anonStart) { 'Pass' } else { 'Fail' }
            CheckId          = 'TEAMS-MEETING-002'
            Remediation      = 'Run: Set-CsTeamsMeetingPolicy -Identity Global -AllowAnonymousUsersToStartMeeting $false. Teams admin center > Meetings > Meeting policies > Global > Anonymous users can start a meeting > Off.'
        }
        Add-Setting @settingParams

        # Auto-admitted users / lobby bypass (CIS 8.5.3)
        $autoAdmit = $meetingPolicy['autoAdmittedUsers']
        $autoAdmitPass = $autoAdmit -eq 'EveryoneInCompanyExcludingGuests' -or $autoAdmit -eq 'EveryoneInSameAndFederatedCompany' -or $autoAdmit -eq 'OrganizerOnly' -or $autoAdmit -eq 'InvitedUsers'
        $settingParams = @{
            Category         = 'Meeting Policy'
            Setting          = 'Auto-Admitted Users (Lobby Bypass)'
            CurrentValue     = "$autoAdmit"
            RecommendedValue = 'EveryoneInCompanyExcludingGuests or stricter'
            Status           = if ($autoAdmitPass) { 'Pass' } else { 'Fail' }
            CheckId          = 'TEAMS-MEETING-003'
            Remediation      = 'Run: Set-CsTeamsMeetingPolicy -Identity Global -AutoAdmittedUsers EveryoneInCompanyExcludingGuests. Teams admin center > Meetings > Meeting policies > Global > Who can bypass the lobby > People in my org.'
        }
        Add-Setting @settingParams

        # Dial-in users can't bypass lobby (CIS 8.5.4)
        $pstnBypass = $meetingPolicy['allowPSTNUsersToBypassLobby']
        $settingParams = @{
            Category         = 'Meeting Policy'
            Setting          = 'Dial-in Users Bypass Lobby'
            CurrentValue     = "$pstnBypass"
            RecommendedValue = 'False'
            Status           = if (-not $pstnBypass) { 'Pass' } else { 'Fail' }
            CheckId          = 'TEAMS-MEETING-004'
            Remediation      = 'Run: Set-CsTeamsMeetingPolicy -Identity Global -AllowPSTNUsersToBypassLobby $false. Teams admin center > Meetings > Meeting policies > Global > Dial-in users can bypass the lobby > Off.'
        }
        Add-Setting @settingParams

        # External participants can't give/request control (CIS 8.5.7)
        $extControl = $meetingPolicy['allowExternalParticipantGiveRequestControl']
        $settingParams = @{
            Category         = 'Meeting Policy'
            Setting          = 'External Participants Can Give/Request Control'
            CurrentValue     = "$extControl"
            RecommendedValue = 'False'
            Status           = if (-not $extControl) { 'Pass' } else { 'Warning' }
            CheckId          = 'TEAMS-MEETING-005'
            Remediation      = 'Run: Set-CsTeamsMeetingPolicy -Identity Global -AllowExternalParticipantGiveRequestControl $false. Teams admin center > Meetings > Meeting policies > Global > External participants can give or request control > Off.'
        }
        Add-Setting @settingParams

        # CIS 8.5.5 — Anonymous meeting chat blocked
        $meetingChat = $meetingPolicy['meetingChatEnabledType']
        $chatPass = $meetingChat -ne 'Enabled'
        $settingParams = @{
            Category         = 'Meeting Policy'
            Setting          = 'Meeting Chat for Anonymous Users'
            CurrentValue     = "$meetingChat"
            RecommendedValue = 'Disabled or EnabledExceptAnonymous'
            Status           = if ($chatPass) { 'Pass' } else { 'Fail' }
            CheckId          = 'TEAMS-MEETING-006'
            Remediation      = 'Run: Set-CsTeamsMeetingPolicy -Identity Global -MeetingChatEnabledType EnabledExceptAnonymous. Teams admin center > Meetings > Meeting policies > Global > Meeting chat > On for everyone except anonymous users.'
        }
        Add-Setting @settingParams

        # CIS 8.5.6 — Only organizers can present
        $presenterRole = $meetingPolicy['designatedPresenterRoleMode']
        $presenterPass = $presenterRole -eq 'OrganizerOnlyUserOverride'
        $settingParams = @{
            Category         = 'Meeting Policy'
            Setting          = 'Default Presenter Role'
            CurrentValue     = "$presenterRole"
            RecommendedValue = 'OrganizerOnlyUserOverride'
            Status           = if ($presenterPass) { 'Pass' } else { 'Fail' }
            CheckId          = 'TEAMS-MEETING-007'
            Remediation      = 'Run: Set-CsTeamsMeetingPolicy -Identity Global -DesignatedPresenterRoleMode OrganizerOnlyUserOverride. Teams admin center > Meetings > Meeting policies > Global > Who can present > Only organizers.'
        }
        Add-Setting @settingParams

        # CIS 8.5.8 — External meeting chat off
        $extMeetingChat = $meetingPolicy['allowExternalNonTrustedMeetingChat']
        $settingParams = @{
            Category         = 'Meeting Policy'
            Setting          = 'External Meeting Chat (Non-Trusted)'
            CurrentValue     = "$extMeetingChat"
            RecommendedValue = 'False'
            Status           = if (-not $extMeetingChat) { 'Pass' } else { 'Fail' }
            CheckId          = 'TEAMS-MEETING-008'
            Remediation      = 'Run: Set-CsTeamsMeetingPolicy -Identity Global -AllowExternalNonTrustedMeetingChat $false. Teams admin center > Meetings > Meeting policies > Global > External meeting chat > Off.'
        }
        Add-Setting @settingParams

        # CIS 8.5.9 — Cloud recording off by default
        $cloudRecording = $meetingPolicy['allowCloudRecording']
        $settingParams = @{
            Category         = 'Meeting Policy'
            Setting          = 'Cloud Recording'
            CurrentValue     = "$cloudRecording"
            RecommendedValue = 'False'
            Status           = if (-not $cloudRecording) { 'Pass' } else { 'Fail' }
            CheckId          = 'TEAMS-MEETING-009'
            Remediation      = 'Run: Set-CsTeamsMeetingPolicy -Identity Global -AllowCloudRecording $false. Teams admin center > Meetings > Meeting policies > Global > Cloud recording > Off.'
        }
        Add-Setting @settingParams
    }
}
catch {
    Write-Warning "Teams meeting policy endpoint unavailable: $($_.Exception.Message)"
}

# ------------------------------------------------------------------
# 3. Teams Settings (tenant-level)
# ------------------------------------------------------------------
try {
    Write-Verbose "Checking tenant-level Teams settings..."
    $graphParams = @{
        Method      = 'GET'
        Uri         = '/v1.0/teamwork'
        ErrorAction = 'Stop'
    }
    $teamSettings = Invoke-MgGraphRequest @graphParams

    if ($teamSettings) {
        $settingParams = @{
            Category         = 'Teams Settings'
            Setting          = 'Teams Workload Active'
            CurrentValue     = 'Active'
            RecommendedValue = 'Active'
            Status           = 'Info'
            CheckId          = 'TEAMS-INFO-001'
            Remediation      = 'Informational — confirms Teams service connectivity.'
        }
        Add-Setting @settingParams
    }
}
catch {
    Write-Warning "Teams settings endpoint unavailable: $($_.Exception.Message)"
}

# ------------------------------------------------------------------
# Teams App Permission Policies (CIS 8.4.1 - Review)
# ------------------------------------------------------------------
$settingParams = @{
    Category         = 'Teams Apps'
    Setting          = 'Third-Party App Permission Policies'
    CurrentValue     = 'Cannot be fully checked via API'
    RecommendedValue = 'Block third-party apps or restrict to approved list'
    Status           = 'Review'
    CheckId          = 'TEAMS-APPS-002'
    Remediation      = 'Teams admin center > Teams apps > Permission policies > Org-wide app settings > Third-party apps > Off (or restrict to approved apps).'
}
Add-Setting @settingParams

# ------------------------------------------------------------------
# Teams Report a Security Concern (CIS 8.6.1 - Review)
# ------------------------------------------------------------------
$settingParams = @{
    Category         = 'Teams Settings'
    Setting          = 'Report a Security Concern Enabled'
    CurrentValue     = 'Cannot be checked via API'
    RecommendedValue = 'Enabled in messaging policies'
    Status           = 'Review'
    CheckId          = 'TEAMS-REPORTING-001'
    Remediation      = 'Teams admin center > Messaging policies > Global (Org-wide default) > Report a security concern > On.'
}
Add-Setting @settingParams

# ------------------------------------------------------------------
# Output
# ------------------------------------------------------------------
Export-SecurityConfigReport -Settings $settings -OutputPath $OutputPath -ServiceLabel 'Teams'