#requires -Version 7.0 <# .SYNOPSIS Generates a Conditional Access baseline YAML manifest from high-level security requirements. .DESCRIPTION Creates a CIS M365-compatible baseline YAML file covering Conditional Access policies. The output can be reviewed and then deployed with Deploy-CISM365Baseline.ps1. Policy names follow the structured naming convention: ---- Index ranges: CA0xx – User policies CA1xx – Guest policies CA2xx – Application policies CA3xx – Admin policies CA4xx – Threat policies Example: CA001-AllUsers-AllApps-BlockLegacyAuth-Prod .PARAMETER RequireTrustedLocations Enforce that users can only sign in from trusted named locations. - None: No location restriction policy - AllUsers: All users must be on trusted locations - Admins: Only administrative roles must be on trusted locations - All: Both AllUsers and Admins policies .PARAMETER AdminDeviceCompliance Device requirements for administrative roles. - None: No device policy for admins - Required: Admins must use compliant or hybrid-joined devices - RequiredWithMFA: Admins must use compliant/hybrid-joined devices AND MFA .PARAMETER GuestMFA Require MFA for guest and external users. .PARAMETER SessionTimeoutHours Require re-authentication after N hours. 0 disables session timeout policies. .PARAMETER DisablePersistentBrowser Prevent persistent browser sessions (users must re-auth when browser restarts). .PARAMETER TrustedLocationsExemptFromReauth When SessionTimeoutHours is set, do not require re-authentication from trusted locations. This creates an exclusion so users on trusted networks are not nagged. .PARAMETER RequireMFAForAllUsers Require MFA for all member users. .PARAMETER BlockLegacyAuth Block all legacy authentication protocols (Exchange ActiveSync, basic auth, etc.). .PARAMETER BlockHighRiskSignIns Block sign-ins with medium or high risk level (requires Entra ID P2). .PARAMETER RequireMFAForAdminPortals Require MFA when accessing Microsoft admin portals (Azure, M365, Exchange, etc.). .PARAMETER RequireMFAForAdmins Require MFA for all administrative roles across all applications. .PARAMETER RequirePhishingResistantMFAForAdmins Require phishing-resistant MFA (FIDO2, certificate) for administrative roles. .PARAMETER BlockDeviceCodeFlow Block sign-ins using the device code authentication flow. .PARAMETER RequireManagedDeviceForAllUsers Require all users to use compliant or hybrid-joined devices. .PARAMETER OutputPath Path where the generated YAML baseline will be written. .PARAMETER Scope Deployment stage suffix applied to every policy name. - Test, Pilot1, Pilot2, Pilot3, Prod .PARAMETER UseDescriptiveNames Use human-readable descriptive names instead of the structured naming convention. .PARAMETER Prefix Optional prefix applied before the INDEX (e.g. "ACME-" produces ACME-CA001-...). .PARAMETER BreakGlassGroup Name of the break-glass group to auto-exclude from every CA policy. .PARAMETER ReportOnly Default all generated policies to report-only mode (recommended for initial rollout). .EXAMPLE # Minimal baseline: MFA for all + block legacy auth ./Scripts/New-ConditionalAccessBaseline.ps1 ` -RequireMFAForAllUsers ` -BlockLegacyAuth ` -OutputPath ./Baselines/MyCA.yaml .EXAMPLE # Full security baseline with structured names scoped to production ./Scripts/New-ConditionalAccessBaseline.ps1 ` -RequireTrustedLocations AllUsers ` -AdminDeviceCompliance RequiredWithMFA ` -GuestMFA ` -SessionTimeoutHours 8 ` -DisablePersistentBrowser ` -TrustedLocationsExemptFromReauth ` -BlockLegacyAuth ` -BlockHighRiskSignIns ` -OutputPath ./Baselines/SecureTenant-CA.yaml ` -Scope Prod .EXAMPLE # Pilot rollout with descriptive names instead of structured convention ./Scripts/New-ConditionalAccessBaseline.ps1 ` -RequireMFAForAllUsers ` -BlockLegacyAuth ` -OutputPath ./Baselines/Pilot-CA.yaml ` -Scope Pilot1 ` -UseDescriptiveNames #> [CmdletBinding()] param( [Parameter()] [ValidateSet('None','AllUsers','Admins','All')] [string]$RequireTrustedLocations = 'None', [Parameter()] [ValidateSet('None','Required','RequiredWithMFA')] [string]$AdminDeviceCompliance = 'None', [Parameter()] [switch]$GuestMFA, [Parameter()] [ValidateRange(0,24)] [int]$SessionTimeoutHours = 0, [Parameter()] [switch]$DisablePersistentBrowser, [Parameter()] [switch]$TrustedLocationsExemptFromReauth, [Parameter()] [switch]$RequireMFAForAllUsers, [Parameter()] [switch]$BlockLegacyAuth, [Parameter()] [switch]$BlockHighRiskSignIns, [Parameter()] [switch]$RequireMFAForAdminPortals, [Parameter()] [switch]$RequireMFAForAdmins, [Parameter()] [switch]$RequirePhishingResistantMFAForAdmins, [Parameter()] [switch]$BlockDeviceCodeFlow, [Parameter()] [switch]$RequireManagedDeviceForAllUsers, [Parameter(Mandatory = $true)] [string]$OutputPath, [Parameter()] [ValidateSet('Test','Pilot1','Pilot2','Pilot3','Prod')] [string]$Scope = 'Prod', [Parameter()] [switch]$UseDescriptiveNames, [Parameter()] [string]$Prefix = '', [Parameter()] [string]$BreakGlassGroup = 'CIS-BreakGlass', [Parameter()] [switch]$ReportOnly ) $ErrorActionPreference = 'Stop' # ===================================================================== # Naming convention engine # ===================================================================== # Format: CA--- # Area: 0=Threat/Tenant, 1=User, 2=Admin, 3=Guest, 4=Application # Scope: 0=Test, 1=Pilot1, 2=Pilot2, 3=Pilot3, 9=Prod # Seq: auto-increment per area # ===================================================================== $script:AreaDigitMap = @{ 'User' = '1' 'Guest' = '3' 'Application' = '4' 'Admin' = '2' 'Threat' = '0' } $script:ScopeDigitMap = @{ 'Test' = '0' 'Pilot1' = '1' 'Pilot2' = '2' 'Pilot3' = '3' 'Prod' = '9' } $script:NextSeq = @{ '0' = 1 '1' = 1 '2' = 1 '3' = 1 '4' = 1 } function Get-StructuredPolicyName { param( [Parameter(Mandatory)] [ValidateSet('User','Guest','Application','Admin','Threat')] [string]$Category, [Parameter(Mandatory)] [string]$Target, [Parameter(Mandatory)] [string]$AppResource, [Parameter(Mandatory)] [string]$Control ) $area = $script:AreaDigitMap[$Category] $scope = $script:ScopeDigitMap[$Scope] $seq = $script:NextSeq[$area]++ $idx = "$area$scope$($seq.ToString('D2'))" $name = "CA$idx-${Target}-${AppResource}-${Control}" if ($Prefix) { $name = "$Prefix$name" } return $name } function Get-DescriptivePolicyName { param([string]$Name) if ($Prefix) { return "$Prefix$Name" } return $Name } function Get-DefaultState { if ($ReportOnly) { return 'enabledForReportingButNotEnforced' } return 'enabled' } # ===================================================================== # Shared data # ===================================================================== $script:AdminRoles = @( 'Global Administrator', 'Privileged Role Administrator', 'Security Administrator', 'Exchange Administrator', 'SharePoint Administrator', 'Conditional Access Administrator', 'Application Administrator', 'Cloud Application Administrator', 'User Administrator', 'Helpdesk Administrator', 'Billing Administrator', 'Authentication Administrator', 'Password Administrator' ) $script:AdminPortalAppIds = @( '797f4846-ba00-4fd7-ba43-dac1f8f63013', # Azure Management 'c44b4083-3bb0-49c1-b47d-974e53cbdf3c', # Azure AD PowerShell '1b730954-1685-4b74-9bfd-dac224a7b894', # Microsoft Graph PowerShell '00000003-0000-0ff1-ce00-000000000000', # Office 365 Exchange Online '00000003-0000-0000-c000-000000000000', # Microsoft Graph 'de8bc8b5-d9f9-48b1-a8ad-b748da725064', # Microsoft Intune '00000002-0000-0ff1-ce00-000000000000', # Office 365 SharePoint Online '66a88757-258c-4c72-893c-3e8bed4d6899' # Microsoft365DSC ) # ===================================================================== # Policy builders # ===================================================================== function New-PolicyBlockLegacyAuth { $policy = @{ name = if ($UseDescriptiveNames) { Get-DescriptivePolicyName 'Block-Legacy-Authentication' } else { Get-StructuredPolicyName -Category Threat -Target AllUsers -AppResource AllApps -Control BlockLegacyAuth } description = 'Block all legacy authentication protocols (EAS, basic auth, IMAP, POP, etc.)' state = Get-DefaultState conditions = @{ applications = @{ includeApplications = @('All') } users = @{ includeUsers = @('All') } clientAppTypes = @('exchangeActiveSync', 'other') } grantControls = @{ builtInControls = @('block') operator = 'OR' } } return $policy } function New-PolicyRequireMFAAllUsers { $policy = @{ name = if ($UseDescriptiveNames) { Get-DescriptivePolicyName 'Require-MFA-All-Users' } else { Get-StructuredPolicyName -Category User -Target AllUsers -AppResource AllApps -Control RequireMFA } description = 'Require multi-factor authentication for all users' state = Get-DefaultState conditions = @{ applications = @{ includeApplications = @('All') } users = @{ includeUsers = @('All') } } grantControls = @{ builtInControls = @('mfa') operator = 'OR' } } return $policy } function New-PolicyRequireMFAAdmins { $policy = @{ name = if ($UseDescriptiveNames) { Get-DescriptivePolicyName 'Require-MFA-Admins' } else { Get-StructuredPolicyName -Category Admin -Target Admins -AppResource AllApps -Control RequireMFA } description = 'Require multi-factor authentication for all administrative roles' state = Get-DefaultState conditions = @{ applications = @{ includeApplications = @('All') } users = @{ includeRoles = $script:AdminRoles } } grantControls = @{ builtInControls = @('mfa') operator = 'OR' } } return $policy } function New-PolicyRequireMFAAdminPortals { $policy = @{ name = if ($UseDescriptiveNames) { Get-DescriptivePolicyName 'Require-MFA-Admin-Portals' } else { Get-StructuredPolicyName -Category Application -Target AllUsers -AppResource AdminPortals -Control RequireMFA } description = 'Require MFA when accessing Microsoft admin portals' state = Get-DefaultState conditions = @{ applications = @{ includeApplications = $script:AdminPortalAppIds } users = @{ includeUsers = @('All') } } grantControls = @{ builtInControls = @('mfa') operator = 'OR' } } return $policy } function New-PolicyTrustedLocations { param([switch]$ForAdmins) if ($ForAdmins) { $name = if ($UseDescriptiveNames) { Get-DescriptivePolicyName 'Trusted-Locations-Only-Admins' } else { Get-StructuredPolicyName -Category Admin -Target Admins -AppResource AllApps -Control BlockUntrustedLocations } $desc = 'Administrators can only sign in from trusted named locations' $userDef = @{ includeRoles = $script:AdminRoles } } else { $name = if ($UseDescriptiveNames) { Get-DescriptivePolicyName 'Trusted-Locations-Only-All-Users' } else { Get-StructuredPolicyName -Category User -Target AllUsers -AppResource AllApps -Control BlockUntrustedLocations } $desc = 'All users can only sign in from trusted named locations' $userDef = @{ includeUsers = @('All') } } $policy = @{ name = $name description = $desc state = Get-DefaultState conditions = @{ applications = @{ includeApplications = @('All') } users = $userDef locations = @{ includeLocations = @('All') excludeLocations = @('AllTrusted') } } grantControls = @{ builtInControls = @('block') operator = 'OR' } } return $policy } function New-PolicyAdminDeviceCompliance { param([switch]$WithMFA) $controls = @('compliantDevice', 'domainJoinedDevice') $operator = 'OR' $desc = 'Administrators must use compliant or hybrid-joined devices' if ($WithMFA) { $controls = @('compliantDevice', 'domainJoinedDevice', 'mfa') $operator = 'AND' $desc = 'Administrators must use compliant/hybrid-joined devices AND MFA' $name = if ($UseDescriptiveNames) { Get-DescriptivePolicyName 'Require-Compliant-Device-and-MFA-Admins' } else { Get-StructuredPolicyName -Category Admin -Target Admins -AppResource AllApps -Control RequireCompliantDeviceAndMFA } } else { $name = if ($UseDescriptiveNames) { Get-DescriptivePolicyName 'Require-Compliant-Device-Admins' } else { Get-StructuredPolicyName -Category Admin -Target Admins -AppResource AllApps -Control RequireCompliantDevice } } $policy = @{ name = $name description = $desc state = Get-DefaultState conditions = @{ applications = @{ includeApplications = @('All') } users = @{ includeRoles = $script:AdminRoles } } grantControls = @{ builtInControls = $controls operator = $operator } } return $policy } function New-PolicyGuestMFA { $policy = @{ name = if ($UseDescriptiveNames) { Get-DescriptivePolicyName 'Require-MFA-Guests' } else { Get-StructuredPolicyName -Category Guest -Target Guests -AppResource AllApps -Control RequireMFA } description = 'Require multi-factor authentication for guest and external users' state = Get-DefaultState conditions = @{ applications = @{ includeApplications = @('All') } users = @{ includeGuestsOrExternalUsers = @{ guestTypes = @('internalGuest', 'b2bCollaborationGuest', 'b2bCollaborationMember', 'b2bDirectConnectUser') externalTenants = @{ membershipKind = 'all' } } } } grantControls = @{ builtInControls = @('mfa') operator = 'OR' } } return $policy } function New-PolicySessionControls { param( [int]$TimeoutHours = 0, [switch]$DisablePersistent, [switch]$ExemptTrustedLocations ) $sessionControls = @{} $parts = [System.Collections.Generic.List[string]]::new() if ($TimeoutHours -gt 0) { $sessionControls['signInFrequency'] = @{ value = $TimeoutHours type = 'hours' isEnabled = $true } $parts.Add("re-authenticate every $TimeoutHours hours") } if ($DisablePersistent) { $sessionControls['persistentBrowser'] = @{ mode = 'never' isEnabled = $true } $parts.Add('no persistent browser sessions') } $desc = 'Session controls: ' + ($parts -join '; ') if ($ExemptTrustedLocations) { $desc += ' (exempt when on trusted locations)' } $controlTag = if ($TimeoutHours -gt 0 -and $DisablePersistent) { 'SessionControls' } elseif ($TimeoutHours -gt 0) { 'SignInFrequency' } else { 'NoPersistentBrowser' } $name = if ($UseDescriptiveNames) { if ($TimeoutHours -gt 0 -and $DisablePersistent) { Get-DescriptivePolicyName 'Session-Timeout-and-No-Persistent-Browser' } elseif ($TimeoutHours -gt 0) { Get-DescriptivePolicyName "Session-Timeout-${TimeoutHours}h" } else { Get-DescriptivePolicyName 'No-Persistent-Browser' } } else { Get-StructuredPolicyName -Category User -Target AllUsers -AppResource AllApps -Control $controlTag } $conditions = @{ applications = @{ includeApplications = @('All') } users = @{ includeUsers = @('All') } } if ($ExemptTrustedLocations) { $conditions['locations'] = @{ excludeLocations = @('AllTrusted') } } $policy = @{ name = $name description = $desc state = Get-DefaultState conditions = $conditions grantControls = @{ builtInControls = @('mfa') operator = 'OR' } } if ($sessionControls.Count -gt 0) { $policy['sessionControls'] = $sessionControls } return $policy } function New-PolicyBlockHighRisk { $policy = @{ name = if ($UseDescriptiveNames) { Get-DescriptivePolicyName 'Block-High-Risk-SignIns' } else { Get-StructuredPolicyName -Category Threat -Target AllUsers -AppResource AllApps -Control BlockHighRisk } description = 'Block sign-ins with medium or high risk score (requires Entra ID P2)' state = Get-DefaultState conditions = @{ applications = @{ includeApplications = @('All') } users = @{ includeUsers = @('All') } signInRiskLevels = @('medium', 'high') } grantControls = @{ builtInControls = @('block') operator = 'OR' } } return $policy } function New-PolicyPhishingResistantMFAAdmins { $policy = @{ name = if ($UseDescriptiveNames) { Get-DescriptivePolicyName 'Require-PhishingResistant-MFA-Admins' } else { Get-StructuredPolicyName -Category Admin -Target Admins -AppResource AllApps -Control RequirePhishingResistantMFA } description = 'Require phishing-resistant MFA (FIDO2, certificate) for administrative roles' state = Get-DefaultState conditions = @{ applications = @{ includeApplications = @('All') } users = @{ includeRoles = $script:AdminRoles } } grantControls = @{ builtInControls = @('authenticationStrength') authenticationStrength = @{ id = '00000000-0000-0000-0000-000000000004' } operator = 'OR' } } return $policy } function New-PolicyBlockDeviceCodeFlow { $policy = @{ name = if ($UseDescriptiveNames) { Get-DescriptivePolicyName 'Block-Device-Code-Flow' } else { Get-StructuredPolicyName -Category Threat -Target AllUsers -AppResource AllApps -Control BlockDeviceCodeFlow } description = 'Block sign-ins using the device code authentication flow' state = Get-DefaultState conditions = @{ applications = @{ includeApplications = @('All') } users = @{ includeUsers = @('All') } authenticationFlows = @{ deviceCodeFlow = @{ isEnabled = $true } } } grantControls = @{ builtInControls = @('block') operator = 'OR' } } return $policy } function New-PolicyRequireManagedDeviceAllUsers { $policy = @{ name = if ($UseDescriptiveNames) { Get-DescriptivePolicyName 'Require-Managed-Device-All-Users' } else { Get-StructuredPolicyName -Category User -Target AllUsers -AppResource AllApps -Control RequireCompliantDevice } description = 'Require all users to use compliant or hybrid-joined devices' state = Get-DefaultState conditions = @{ applications = @{ includeApplications = @('All') } users = @{ includeUsers = @('All') } } grantControls = @{ builtInControls = @('compliantDevice', 'domainJoinedDevice') operator = 'OR' } } return $policy } # ===================================================================== # Build the policy list based on parameters # ===================================================================== $policies = [System.Collections.Generic.List[hashtable]]::new() if ($BlockLegacyAuth) { $policies.Add((New-PolicyBlockLegacyAuth)) } if ($RequireMFAForAllUsers) { $policies.Add((New-PolicyRequireMFAAllUsers)) } if ($RequireMFAForAdmins) { $policies.Add((New-PolicyRequireMFAAdmins)) } if ($RequireMFAForAdminPortals) { $policies.Add((New-PolicyRequireMFAAdminPortals)) } if ($BlockHighRiskSignIns) { $policies.Add((New-PolicyBlockHighRisk)) } if ($BlockDeviceCodeFlow) { $policies.Add((New-PolicyBlockDeviceCodeFlow)) } if ($RequirePhishingResistantMFAForAdmins) { $policies.Add((New-PolicyPhishingResistantMFAAdmins)) } if ($RequireManagedDeviceForAllUsers) { $policies.Add((New-PolicyRequireManagedDeviceAllUsers)) } switch ($RequireTrustedLocations) { 'AllUsers' { $policies.Add((New-PolicyTrustedLocations)) } 'Admins' { $policies.Add((New-PolicyTrustedLocations -ForAdmins)) } 'All' { $policies.Add((New-PolicyTrustedLocations)); $policies.Add((New-PolicyTrustedLocations -ForAdmins)) } } switch ($AdminDeviceCompliance) { 'Required' { $policies.Add((New-PolicyAdminDeviceCompliance)) } 'RequiredWithMFA' { $policies.Add((New-PolicyAdminDeviceCompliance -WithMFA)) } } if ($GuestMFA) { $policies.Add((New-PolicyGuestMFA)) } if ($SessionTimeoutHours -gt 0 -or $DisablePersistentBrowser) { $policies.Add((New-PolicySessionControls ` -TimeoutHours $SessionTimeoutHours ` -DisablePersistent:$DisablePersistentBrowser ` -ExemptTrustedLocations:$TrustedLocationsExemptFromReauth)) } if ($policies.Count -eq 0) { throw "No policies requested. Specify at least one requirement parameter (e.g. -RequireMFAForAllUsers, -BlockLegacyAuth, etc.)." } # ===================================================================== # Serialize to YAML (requires powershell-yaml) # ===================================================================== function Test-YamlModule { return [bool](Get-Module -ListAvailable -Name powershell-yaml) } if (-not (Test-YamlModule)) { Write-Host "powershell-yaml module is required but not installed." -ForegroundColor Yellow $confirm = Read-Host "Install powershell-yaml from PSGallery now? [Y/n]" if ($confirm -match "^\s*n") { throw "powershell-yaml is required. Install it with: Install-Module powershell-yaml -Scope CurrentUser -Force" } Install-Module powershell-yaml -Scope CurrentUser -Force } Import-Module powershell-yaml -Force # Build the root document $yamlRoot = [ordered]@{ baseline = [ordered]@{ name = 'Generated-ConditionalAccess-Baseline' conflictResolution = 'Skip' whatIf = $false tenantConfig = [ordered]@{ conditionalAccess = [ordered]@{ reportOnly = $true breakGlassGroup = $BreakGlassGroup policies = $policies } } } } $yamlText = ConvertTo-Yaml -Data $yamlRoot # Ensure output directory exists $outDir = Split-Path -Parent $OutputPath if ($outDir -and -not (Test-Path $outDir)) { New-Item -ItemType Directory -Path $outDir -Force | Out-Null } $yamlText | Set-Content -Path $OutputPath -Encoding UTF8 -Force Write-Host "Generated Conditional Access baseline with $($policies.Count) policies." -ForegroundColor Green Write-Host "Output written to: $(Resolve-Path $OutputPath)" -ForegroundColor Green Write-Host "" Write-Host "Review the file, then deploy with:" -ForegroundColor Cyan Write-Host " ./Scripts/Deploy-CISM365Baseline.ps1 -BaselinePath '$OutputPath' -Mode Assess" -ForegroundColor Yellow Write-Host " ./Scripts/Deploy-CISM365Baseline.ps1 -BaselinePath '$OutputPath' -Mode Deploy -Apply" -ForegroundColor Yellow