├── .gitignore ├── CAExport-Select.png ├── CaExport-rec.png ├── CaExport-result.png ├── Export-CAPolicyWithRecs.ps1 ├── Export-CaPolicy.ps1 └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | *.json -------------------------------------------------------------------------------- /CAExport-Select.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dougsbaker/CA-Export/4114ba6bc5e31e8821b476047ac32b5f824e868b/CAExport-Select.png -------------------------------------------------------------------------------- /CaExport-rec.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dougsbaker/CA-Export/4114ba6bc5e31e8821b476047ac32b5f824e868b/CaExport-rec.png -------------------------------------------------------------------------------- /CaExport-result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dougsbaker/CA-Export/4114ba6bc5e31e8821b476047ac32b5f824e868b/CaExport-result.png -------------------------------------------------------------------------------- /Export-CAPolicyWithRecs.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Export Conditional Access Policies with Recommendations. 4 | 5 | .DESCRIPTION 6 | This script exports Conditional Access (CA) policies from Azure AD to an HTML file. 7 | It includes recommendations and checks for each policy to enhance security. 8 | 9 | .EXAMPLE 10 | .\Export-CAPolicyWithRecs.ps1 11 | 12 | This example runs the script and exports all Conditional Access policies with recommendations. 13 | 14 | .NOTES 15 | Author: Douglas Baker 16 | @dougsbaker 17 | Version: 3.0 18 | 19 | 20 | Output report uses open source components for HTML formatting 21 | - bootstrap - MIT License - https://getbootstrap.com/docs/4.0/about/license/ 22 | - fontawesome - CC BY 4.0 License - https://fontawesome.com/license/free 23 | 24 | ############################################################################ 25 | This sample script is not supported under any standard support program or service. 26 | This sample script is provided AS IS without warranty of any kind. 27 | This work is licensed under a Creative Commons Attribution 4.0 International License 28 | https://creativecommons.org/licenses/by-nc-sa/4.0/ 29 | ############################################################################ 30 | 31 | #> 32 | 33 | [CmdletBinding()] 34 | param ( 35 | [Parameter()] 36 | [String]$PolicyID 37 | ) 38 | 39 | $ExportLocation = $PSScriptRoot 40 | if (!$ExportLocation) { $ExportLocation = $PWD } 41 | $FileName = "\CAPolicy.html" 42 | $JsonFileName = "\CAPolicy.json" 43 | $HTMLExport = $true 44 | $JsonExport = $false 45 | 46 | 47 | 48 | 49 | try { 50 | Get-MgIdentityConditionalAccessPolicy -ErrorAction Stop > $null 51 | Write-host "Connected: MgGraph" 52 | } 53 | catch { 54 | Write-host "Connecting: MgGraph" 55 | Try { 56 | #Connect-AzureAD 57 | #Select-MgProfile -Name "beta" 58 | Connect-MgGraph -Scopes 'Policy.Read.All', 'Directory.Read.All', 'Application.Read.All', 'Agreement.Read.All' -nowelcome 59 | } 60 | Catch { 61 | Write-host "Error: Please Install MgGraph Module" -ForegroundColor Yellow 62 | Write-Host "Run: Install-Module Microsoft.Graph" -ForegroundColor Yellow 63 | Exit 64 | } 65 | } 66 | 67 | 68 | $TenantData = Get-MgOrganization 69 | $TenantName = $TenantData.DisplayName 70 | $date = Get-Date 71 | Write-Host "Connected: $TenantName tenant" 72 | $LinkURL = "https://portal.azure.com/#view/Microsoft_AAD_ConditionalAccess/PolicyBlade/policyId/" 73 | 74 | #Collect CA Policy 75 | Write-host "Exporting: CA Policy" 76 | if ($PolicyID) { 77 | $CAPolicy = Get-MgIdentityConditionalAccessPolicy -ConditionalAccessPolicyId $PolicyID 78 | } 79 | else { 80 | $CAPolicy = Get-MgIdentityConditionalAccessPolicy -all -ExpandProperty * 81 | 82 | } 83 | 84 | $TenantData = Get-MgOrganization 85 | $TenantName = $TenantData.DisplayName 86 | $date = Get-Date 87 | 88 | 89 | Write-host "Extracting: Names from Guid's" 90 | #Swap User Guid With Names 91 | #Get Name 92 | $ADUsers = $CAPolicy.Conditions.Users.IncludeUsers 93 | $ADUsers += $CAPolicy.Conditions.Users.IncludeGroups 94 | $ADUsers += $CAPolicy.Conditions.Users.IncludeRoles 95 | $ADUsers += $CAPolicy.Conditions.Users.ExcludeUsers 96 | $ADUsers += $CAPolicy.Conditions.Users.ExcludeGroups 97 | $ADUsers += $CAPolicy.Conditions.Users.ExcludeRoles 98 | 99 | 100 | 101 | # Filter the $AdUsers array to include only valid GUIDs 102 | $ADsearch = $AdUsers | Where-Object { 103 | ([Guid]::TryParse($_, [ref] [Guid]::Empty)) 104 | } 105 | 106 | #users Hashtable 107 | $mgobjects = Get-MgDirectoryObjectById -ids $ADsearch 108 | $mgObjectsLookup = @{} 109 | foreach ($obj in $mgobjects) { 110 | $mgObjectsLookup[$obj.Id] = $obj.AdditionalProperties.displayName 111 | } 112 | 113 | 114 | #Change Guid's 115 | foreach ($policy in $caPolicy) { 116 | # Check if the policy has Conditions and Users and ExcludeUsers properties 117 | if ($policy.Conditions -and $policy.Conditions.Users <#-and $policy.Conditions.Users.ExcludeUsers#>) { 118 | # Loop through each user in ExcludeUsers and replace with displayName if found in $mgObjectsLookup 119 | for ($i = 0; $i -lt $policy.Conditions.Users.ExcludeUsers.Count; $i++) { 120 | $userId = $policy.Conditions.Users.ExcludeUsers[$i] 121 | if ($mgObjectsLookup.ContainsKey($userId)) { 122 | $policy.Conditions.Users.ExcludeUsers[$i] = $mgObjectsLookup[$userId] 123 | } 124 | } 125 | 126 | for ($i = 0; $i -lt $policy.Conditions.Users.IncludeUsers.Count; $i++) { 127 | $userId = $policy.Conditions.Users.IncludeUsers[$i] 128 | if ($mgObjectsLookup.ContainsKey($userId)) { 129 | $policy.Conditions.Users.IncludeUsers[$i] = $mgObjectsLookup[$userId] 130 | } 131 | } 132 | for ($i = 0; $i -lt $policy.Conditions.Users.IncludeGroups.Count; $i++) { 133 | $userId = $policy.Conditions.Users.IncludeGroups[$i] 134 | if ($mgObjectsLookup.ContainsKey($userId)) { 135 | $memberCount = 0 136 | $groupMembers = Get-MgGroupMember -GroupId $userId 137 | $memberCount = $groupMembers | Measure-Object | Select-Object -ExpandProperty Count 138 | $policy.Conditions.Users.IncludeGroups[$i] = $mgObjectsLookup[$userId] + " ($memberCount)" 139 | } 140 | } 141 | for ($i = 0; $i -lt $policy.Conditions.Users.ExcludeGroups.Count; $i++) { 142 | $userId = $policy.Conditions.Users.ExcludeGroups[$i] 143 | if ($mgObjectsLookup.ContainsKey($userId)) { 144 | $memberCount = 0 145 | $groupMembers = Get-MgGroupMember -GroupId $userId 146 | $memberCount = $groupMembers | Measure-Object | Select-Object -ExpandProperty Count 147 | $policy.Conditions.Users.ExcludeGroups[$i] = $mgObjectsLookup[$userId] + " ($memberCount)" 148 | } 149 | } 150 | for ($i = 0; $i -lt $policy.Conditions.Users.IncludeRoles.Count; $i++) { 151 | $userId = $policy.Conditions.Users.IncludeRoles[$i] 152 | if ($mgObjectsLookup.ContainsKey($userId)) { 153 | $policy.Conditions.Users.IncludeRoles[$i] = $mgObjectsLookup[$userId] 154 | } 155 | } 156 | for ($i = 0; $i -lt $policy.Conditions.Users.ExcludeRoles.Count; $i++) { 157 | $userId = $policy.Conditions.Users.ExcludeRoles[$i] 158 | if ($mgObjectsLookup.ContainsKey($userId)) { 159 | $policy.Conditions.Users.ExcludeRoles[$i] = $mgObjectsLookup[$userId] 160 | } 161 | } 162 | } 163 | 164 | } 165 | 166 | #Swap App ID with name 167 | $MGApps = Get-MgServicePrincipal -All 168 | #Hash Table 169 | $MGAppsLookup = @{} 170 | foreach ($obj in $MGApps) { 171 | $MGAppsLookup[$obj.AppId] = "
" + $obj.DisplayName + "App Id:" + $obj.AppId + "
" 172 | } 173 | #"
" + $obj.DisplayName +"App Id:"+ $obj.AppId +"
" 174 | 175 | foreach ($policy in $caPolicy) { 176 | if ($policy.Conditions -and $policy.Conditions.Applications) { 177 | 178 | for ($i = 0; $i -lt $policy.Conditions.Applications.ExcludeApplications.Count; $i++) { 179 | $AppId = $policy.Conditions.Applications.ExcludeApplications[$i] 180 | if ($MGAppsLookup.ContainsKey($AppId)) { 181 | $policy.Conditions.Applications.ExcludeApplications[$i] = $MGAppsLookup[$AppId] 182 | } 183 | } 184 | 185 | for ($i = 0; $i -lt $policy.Conditions.Applications.IncludeApplications.Count; $i++) { 186 | $AppId = $policy.Conditions.Applications.IncludeApplications[$i] 187 | if ($MGAppsLookup.ContainsKey($AppId)) { 188 | $policy.Conditions.Applications.IncludeApplications[$i] = $MGAppsLookup[$AppId] 189 | } 190 | } 191 | } 192 | 193 | } 194 | 195 | #Swap Location with Names 196 | $mgLoc = Get-MgIdentityConditionalAccessNamedLocation 197 | $MGLocLookup = @{} 198 | foreach ($obj in $mgLoc) { 199 | $MGLocLookup[$obj.Id] = $obj.DisplayName 200 | } 201 | foreach ($policy in $caPolicy) { 202 | #Set Locations 203 | if ($policy.Conditions -and $policy.Conditions.Locations) { 204 | for ($i = 0; $i -lt $policy.Conditions.Locations.IncludeLocations.Count; $i++) { 205 | $LocId = $policy.Conditions.Locations.IncludeLocations[$i] 206 | if ($MGLocLookup.ContainsKey($LocId)) { 207 | $policy.Conditions.Locations.IncludeLocations[$i] = $MGLocLookup[$LocId] 208 | } 209 | } 210 | for ($i = 0; $i -lt $policy.Conditions.Locations.ExcludeLocations.Count; $i++) { 211 | $LocId = $policy.Conditions.Locations.ExcludeLocations[$i] 212 | if ($MGLocLookup.ContainsKey($LocId)) { 213 | $policy.Conditions.Locations.ExcludeLocations[$i] = $MGLocLookup[$LocId] 214 | } 215 | } 216 | } 217 | 218 | } 219 | 220 | #Switch TOU Id for Name 221 | $mgTou = Get-MgAgreement 222 | $MGTouLookup = @{} 223 | foreach ($obj in $mgTou) { 224 | $MGTouLookup[$obj.Id] = $obj.DisplayName 225 | } 226 | foreach ($policy in $caPolicy) { 227 | if ($policy.GrantControls -and $policy.GrantControls.TermsOfUse) { 228 | 229 | for ($i = 0; $i -lt $policy.GrantControls.TermsOfUse.Count; $i++) { 230 | $TouId = $policy.GrantControls.TermsOfUse[$i] 231 | if ($MGTouLookup.ContainsKey($TouId)) { 232 | $policy.GrantControls.TermsOfUse[$i] = $MGTouLookup[$TouId] 233 | } 234 | } 235 | } 236 | } 237 | #swap Admin Roles 238 | $mgRole = Get-MgDirectoryRoleTemplate 239 | $mgRoleLookup = @{} 240 | foreach ($obj in $mgRole) { 241 | $mgRoleLookup[$obj.Id] = $obj.DisplayName 242 | } 243 | foreach ($policy in $caPolicy) { 244 | if ($policy.Conditions.Users -and $policy.Conditions.Users.IncludeRoles) { 245 | 246 | for ($i = 0; $i -lt $policy.Conditions.Users.IncludeRoles.Count; $i++) { 247 | $RoleId = $policy.Conditions.Users.IncludeRoles[$i] 248 | if ($mgRoleLookup.ContainsKey($RoleId)) { 249 | $policy.Conditions.Users.IncludeRoles[$i] = $mgRoleLookup[$RoleId] 250 | } 251 | } 252 | } 253 | if ($policy.Conditions.Users -and $policy.Conditions.Users.ExcludeRoles) { 254 | 255 | for ($i = 0; $i -lt $policy.Conditions.Users.ExcludeRoles.Count; $i++) { 256 | $RoleId = $policy.Conditions.Users.ExcludeRoles[$i] 257 | if ($mgRoleLookup.ContainsKey($RoleId)) { 258 | $policy.Conditions.Users.ExcludeRoles[$i] = $mgRoleLookup[$RoleId] 259 | } 260 | } 261 | } 262 | } 263 | 264 | # exit 265 | $CAExport = [PSCustomObject]@() 266 | 267 | $AdUsers = @() 268 | $Apps = @() 269 | #Extract Values 270 | Write-host "Extracting: CA Policy Data" 271 | foreach ( $Policy in $CAPolicy) { 272 | 273 | $IncludeUG = $null 274 | $IncludeUG = $Policy.Conditions.Users.IncludeUsers 275 | $IncludeUG += $Policy.Conditions.Users.IncludeGroups 276 | $IncludeUG += $Policy.Conditions.Users.IncludeRoles 277 | $IncludeUG += $policy.conditions.users.IncludeGuestsOrExternalUsers.GuestOrExternalUserTypes -replace ',', ', ' 278 | $DateCreated = $null 279 | $DateCreated = $policy.CreatedDateTime 280 | $DateModified = $null 281 | $DateModified = $Policy.ModifiedDateTime 282 | 283 | 284 | $ExcludeUG = $null 285 | $ExcludeUG = $Policy.Conditions.Users.ExcludeUsers 286 | $ExcludeUG += $Policy.Conditions.Users.ExcludeGroups 287 | $ExcludeUG += $Policy.Conditions.Users.ExcludeRoles 288 | $ExcludeUG += $policy.conditions.users.ExcludeGuestsOrExternalUsers.GuestOrExternalUserTypes -replace ',', ', ' 289 | 290 | 291 | $Apps += $Policy.Conditions.Applications.IncludeApplications 292 | $Apps += $Policy.Conditions.Applications.ExcludeApplications 293 | 294 | 295 | $InclLocation = $Null 296 | $ExclLocation = $Null 297 | $InclLocation = $Policy.Conditions.Locations.includelocations 298 | $ExclLocation = $Policy.Conditions.Locations.Excludelocations 299 | 300 | $InclPlat = $Null 301 | $ExclPlat = $Null 302 | $InclPlat = $Policy.Conditions.Platforms.IncludePlatforms 303 | $ExclPlat = $Policy.Conditions.Platforms.ExcludePlatforms 304 | $InclDev = $null 305 | $ExclDev = $null 306 | $InclDev = $Policy.Conditions.Devices.IncludeDevices 307 | $ExclDev = $Policy.Conditions.Devices.ExcludeDevices 308 | $devFilters = $null 309 | $devFilters = $Policy.Conditions.Devices.DeviceFilter.Rule 310 | 311 | $authenticationFlowsString = $Policy.Conditions.AdditionalProperties.authenticationFlows.Values -join ', ' 312 | 313 | 314 | $CAExport += New-Object PSObject -Property @{ 315 | Name = $Policy.DisplayName; 316 | Status = $Policy.State; 317 | DateModified = $DateModified; 318 | Users = ""; 319 | UsersInclude = ($IncludeUG -join ", `r`n"); 320 | UsersExclude = ($ExcludeUG -join ", `r`n"); 321 | 'Cloud apps or actions' = ""; 322 | ApplicationsIncluded = ($Policy.Conditions.Applications.IncludeApplications -join ", `r`n"); 323 | ApplicationsExcluded = ($Policy.Conditions.Applications.ExcludeApplications -join ", `r`n"); 324 | userActions = ($Policy.Conditions.Applications.IncludeUserActions -join ", `r`n"); 325 | AuthContext = ($Policy.Conditions.Applications.IncludeAuthenticationContextClassReferences -join ", `r`n"); 326 | Conditions = ""; 327 | UserRisk = ($Policy.Conditions.UserRiskLevels -join ", `r`n"); 328 | SignInRisk = ($Policy.Conditions.SignInRiskLevels -join ", `r`n"); 329 | # Platforms = $Policy.Conditions.Platforms; 330 | PlatformsInclude = ($InclPlat -join ", `r`n"); 331 | PlatformsExclude = ($ExclPlat -join ", `r`n"); 332 | # Locations = $Policy.Conditions.Locations; 333 | LocationsIncluded = ($InclLocation -join ", `r`n"); 334 | LocationsExcluded = ($ExclLocation -join ", `r`n"); 335 | ClientApps = ($Policy.Conditions.ClientAppTypes -join ", `r`n"); 336 | # Devices = $Policy.Conditions.Devices; 337 | DevicesIncluded = ($InclDev -join ", `r`n"); 338 | DevicesExcluded = ($ExclDev -join ", `r`n"); 339 | DeviceFilters = ($devFilters -join ", `r`n"); 340 | AuthenticationFlows = $authenticationFlowsString ; 341 | 'Grant Controls' = ""; 342 | # Grant = ($Policy.GrantControls.BuiltInControls -join ", `r`n"); 343 | Block = if ($Policy.GrantControls.BuiltInControls -contains "Block") { "True" } else { "" } 344 | 'Require MFA' = if ($Policy.GrantControls.BuiltInControls -contains "Mfa") { "True" } else { "" } 345 | 'Authentication Strength MFA' = $Policy.GrantControls.AuthenticationStrength.DisplayName 346 | 'CompliantDevice' = if ($Policy.GrantControls.BuiltInControls -contains "CompliantDevice") { "True" } else { "" } 347 | 'DomainJoinedDevice' = if ($Policy.GrantControls.BuiltInControls -contains "DomainJoinedDevice") { "True" } else { "" } 348 | 'CompliantApplication' = if ($Policy.GrantControls.BuiltInControls -contains "CompliantApplication") { "True" } else { "" } 349 | 'ApprovedApplication' = if ($Policy.GrantControls.BuiltInControls -contains "ApprovedApplication") { "True" } else { "" } 350 | 'PasswordChange' = if ($Policy.GrantControls.BuiltInControls -contains "PasswordChange") { "True" } else { "" } 351 | TermsOfUse = ($Policy.GrantControls.TermsOfUse -join ", `r`n"); 352 | CustomControls = ($Policy.GrantControls.CustomAuthenticationFactors -join ", `r`n"); 353 | GrantOperator = $Policy.GrantControls.Operator 354 | # Session = $Policy.SessionControls 355 | 'Session Controls' = ""; 356 | ApplicationEnforcedRestrictions = $Policy.SessionControls.ApplicationEnforcedRestrictions.IsEnabled 357 | CloudAppSecurity = $Policy.SessionControls.CloudAppSecurity.IsEnabled 358 | SignInFrequency = "$($Policy.SessionControls.SignInFrequency.Value) $($conditionalAccessPolicy.SessionControls.SignInFrequency.Type)" 359 | PersistentBrowser = $Policy.SessionControls.PersistentBrowser.Mode 360 | ContinuousAccessEvaluation = $Policy.SessionControls.ContinuousAccessEvaluation.Mode 361 | ResiliantDefaults = $policy.SessionControls.DisableResilienceDefaults 362 | secureSignInSession = $policy.SessionControls.AdditionalProperties.secureSignInSession.Values 363 | } 364 | 365 | } 366 | 367 | 368 | #Export Setup 369 | Write-host "Pivoting: CA to Export Format" 370 | $pivot = @() 371 | $rowItem = New-Object PSObject 372 | $rowitem | Add-Member -type NoteProperty -Name 'CA Item' -Value "row1" 373 | $Pcount = 1 374 | foreach ($CA in $CAExport) { 375 | $rowitem | Add-Member -type NoteProperty -Name "Policy $pcount" -Value "row1" 376 | #$ca.Name 377 | $pcount += 1 378 | } 379 | $pivot += $rowItem 380 | 381 | #Add Data to Report 382 | $Rows = $CAExport | Get-Member | Where-Object { $_.MemberType -eq "NoteProperty" } 383 | $Rows | ForEach-Object { 384 | $rowItem = New-Object PSObject 385 | $rowname = $_.Name 386 | $rowitem | Add-Member -type NoteProperty -Name 'CA Item' -Value $_.Name 387 | $Pcount = 1 388 | foreach ($CA in $CAExport) { 389 | $ca | Get-Member | Where-Object { $_.MemberType -eq "NoteProperty" } | ForEach-Object { 390 | $a = $_.name 391 | $b = $ca.$a 392 | if ($a -eq $rowname) { 393 | $rowitem | Add-Member -type NoteProperty -Name "Policy $pcount" -Value $b 394 | } 395 | 396 | } 397 | # $ca.UsersInclude 398 | $pcount += 1 399 | } 400 | $pivot += $rowItem 401 | } 402 | #Export Setup 403 | Write-host "Analyzing: getting recommendations" 404 | 405 | class Recommendation { 406 | [string]$Control 407 | [string]$Name 408 | [string]$PassText 409 | [string]$FailRecommendation 410 | [string]$Importance 411 | [hashtable]$Links 412 | [bool]$Status 413 | [bool]$SwapStatus 414 | [string]$Note 415 | [string[]]$Excluded 416 | 417 | 418 | Recommendation([string]$Control, [string]$Name, [string]$PassText, [string]$FailRecommendation, [string]$Importance, [hashtable]$Links, [bool]$Status, [bool]$SwapStatus) { 419 | $this.Control = $Control 420 | $this.Name = $Name 421 | $this.PassText = $PassText 422 | $this.FailRecommendation = $FailRecommendation 423 | $this.Importance = $Importance 424 | $this.Links = $Links 425 | $this.Status = $Status 426 | $this.SwapStatus = $SwapStatus 427 | $this.Note = "" 428 | $this.Excluded = @() 429 | 430 | } 431 | } 432 | 433 | 434 | $recommendations = @( 435 | [Recommendation]::new( 436 | "CA-00", 437 | "Legacy Authentication", 438 | "Legacy Authentication is blocked or minimized, targeting Legacy Authentication protocols.", 439 | "Review and update policies to restrict or block Legacy Authentication protocols to ensure security.", 440 | "Legacy Authentication protocols are outdated and less secure. It is recommended to block or minimize their usage to enhance the security of your environment.", 441 | @{"Legacy Authentication Overview" = "https://learn.microsoft.com/en-us/entra/identity/conditional-access/policy-block-legacy-authentication" }, 442 | $false, 443 | $true 444 | ), 445 | [Recommendation]::new( 446 | "CA-01", 447 | "MFA Policy targets All users Group and All Cloud Apps", 448 | "There is at least one policy that targets all users and cloud apps.", 449 | "Review and update MFA policies to ensure they target all users and cloud apps, including any necessary exclusions.", 450 | "Multi-factor Authentication (MFA) should apply to all users and cloud apps as a baseline for security. Policies should include the necessary exclusions if required but should primarily target all users and apps for maximum security.", 451 | @{"The Challenge with Targeted Architecture" = "https://learn.microsoft.com/en-us/azure/architecture/guide/security/conditional-access-architecture#:~:text=The%20challenge%20with%20the,that%20number%20isn%27t%20supported." }, 452 | $false, 453 | $true 454 | ), 455 | [Recommendation]::new( 456 | "CA-02", 457 | "Mobile Device Policy requires MDM or MAM", 458 | "There is at least one policy that requires MDM or MAM for mobile devices.", 459 | "Consider adding policies to check for device management, either through MDM or MAM, to ensure secure mobile access.", 460 | "Mobile Device Management (MDM) or Mobile Application Management (MAM) should be enforced to ensure that mobile devices accessing organizational data are properly managed and secure. Policies should include requirements for MDM or MAM to increase security for mobile devices.", 461 | @{"MAM Overview" = "https://learn.microsoft.com/en-us/mem/intune/apps/app-management#mobile-application-management-mam-basics" 462 | "Protect Data on personally owned devices" = "https://smbtothecloud.com/protecting-company-data-on-personally-owned-devices/" 463 | }, 464 | $false, 465 | $true 466 | ), 467 | [Recommendation]::new( 468 | "CA-03", 469 | "Require Hybrid Join or Intune Compliance on Windows or Mac", 470 | "There is at least one policy that requires Hybrid Join or Intune Compliance for Windows or Mac devices.", 471 | "Consider adding policies to ensure that Windows or Mac devices are either Hybrid Joined or compliant with Intune to enhance security.", 472 | "Hybrid Join or Intune Compliance should be enforced to ensure that Windows or Mac devices accessing organizational data are properly managed and secure. Policies should include requirements for Hybrid Join or Intune Compliance to increase security for these devices.", 473 | @{ 474 | "Hybrid Join Overview" = "https://learn.microsoft.com/en-us/azure/active-directory/devices/hybrid-azuread-join-plan" 475 | "Intune Compliance Overview" = "https://learn.microsoft.com/en-us/mem/intune/protect/compliance-policy-create-windows" 476 | }, 477 | $false, 478 | $true 479 | ), 480 | [Recommendation]::new( 481 | "CA-04", 482 | "Require MFA for Admins", 483 | "There is at least one policy that requires Multi-Factor Authentication (MFA) for administrators.", 484 | "Consider adding policies to ensure that administrators are required to use Multi-Factor Authentication (MFA) to enhance security.", 485 | "Multi-Factor Authentication (MFA) should be enforced for administrators to ensure that access to critical systems and data is secure. Policies should include requirements for MFA to increase security for administrative accounts. Policies should target the folowing roles Global Administrator, Security Administrator, SharePoint Administrator, Exchange Administrator, Conditional Access Administrator, Helpdesk Administrator, Billing Administrator, User Administrator, Authentication Administrator, Application Administrator, Cloud Application Administrator, Password Administrator, Privileged Authentication Administrator, Privileged Role Administrator", 486 | @{ 487 | "MFA Overview" = "https://learn.microsoft.com/en-us/azure/active-directory/authentication/concept-mfa-howitworks" 488 | "MFA for Admins" = "https://learn.microsoft.com/en-us/entra/identity/conditional-access/policy-old-require-mfa-admin" 489 | }, 490 | $false, 491 | $true 492 | ), 493 | [Recommendation]::new( 494 | "CA-05", 495 | "Require Phish-Resistant MFA for Admins", 496 | "There is at least one policy that requires phish-resistant Multi-Factor Authentication (MFA) for administrators.", 497 | "Consider adding policies to ensure that administrators are required to use phish-resistant Multi-Factor Authentication (MFA) to enhance security.", 498 | "Phish-resistant Multi-Factor Authentication (MFA) should be enforced for administrators to ensure that access to critical systems and data is secure. Policies should include requirements for phish-resistant MFA to increase security for administrative accounts. Policies should target the following roles: Global Administrator, Security Administrator, SharePoint Administrator, Exchange Administrator, Conditional Access Administrator, Helpdesk Administrator, Billing Administrator, User Administrator, Authentication Administrator, Application Administrator, Cloud Application Administrator, Password Administrator, Privileged Authentication Administrator, Privileged Role Administrator.", 499 | @{ 500 | "MSFT Authentication Strengths" = "https://learn.microsoft.com/en-us/entra/identity/authentication/concept-authentication-strengths" 501 | "Phish-Resistant MFA for Admins" = "https://learn.microsoft.com/en-us/entra/identity/conditional-access/policy-admin-phish-resistant-mfa" 502 | }, 503 | $false, 504 | $true 505 | ), 506 | [Recommendation]::new( 507 | "CA-06", 508 | "Policy Excludes Same Entities It Includes", 509 | "There is at least one policy that excludes the same entities it includes, resulting in no effective condition being checked.", 510 | "Review and update policies to ensure that they do not exclude the same entities they include, as this results in no effective condition being checked.", 511 | "Policies should be configured to include and exclude distinct sets of entities to ensure that conditions are effectively checked. This helps in maintaining the integrity and effectiveness of the policy.", 512 | @{ 513 | "Policy Configuration Best Practices" = "https://learn.microsoft.com/en-us/azure/active-directory/conditional-access/best-practices" 514 | }, 515 | $true, 516 | $false 517 | ) 518 | [Recommendation]::new( 519 | "CA-07", 520 | "No Users Targeted in Policy", 521 | "There is at least one policy that does not target any users.", 522 | "Review and update policies to ensure that they target specific users, groups, or roles to be effective.", 523 | "Policies should be configured to target specific users, groups, or roles to ensure that they are applied correctly and provide the intended security controls.", 524 | @{ 525 | "Policy Configuration Best Practices" = "https://learn.microsoft.com/en-us/azure/active-directory/conditional-access/best-practices" 526 | }, 527 | $true, 528 | $false 529 | ), 530 | [Recommendation]::new( 531 | "CA-08", 532 | "Direct User Assignment", 533 | "There are no direct user assignments in the policy.", 534 | "Review and update policies to avoid direct user assignments and instead use exclusion groups to manage user access more efficiently.", 535 | "Direct user assignments in policies are not ideal for maintaining flexibility and scalability. Exclusion groups should be used instead to manage policies efficiently without manually adding users to each policy.", 536 | @{}, 537 | $true, 538 | $false 539 | ), 540 | [Recommendation]::new( 541 | "CA-09", 542 | "Implement Risk-Based Policy", 543 | "There is at least 1 policy that addresses risk-based conditional access.", 544 | "Consider implementing risk-based conditional access policies to enhance security by dynamically applying access controls based on the risk level of the sign-in or user.", 545 | "Risk-based policies help in dynamically assessing the risk level of sign-ins and users, and applying appropriate access controls to mitigate potential threats. This ensures that high-risk activities are subject to stricter controls, thereby enhancing the overall security posture.", 546 | @{ 547 | "Risk-Based Conditional Access Overview" = "https://learn.microsoft.com/en-us/entra/id-protection/howto-identity-protection-configure-risk-policies" 548 | "Require MFA for Risky Sign-in" = "https://learn.microsoft.com/en-us/entra/identity/conditional-access/policy-risk-based-sign-in#enable-with-conditional-access-policy" 549 | "Require Passsword Change for Risky USer" = "https://learn.microsoft.com/en-us/entra/identity/conditional-access/policy-risk-based-user#enable-with-conditional-access-policy" 550 | }, 551 | $false, 552 | $true 553 | ), 554 | [Recommendation]::new( 555 | "CA-10", 556 | "Block Device Code Flow", 557 | "There is at least 1 policy that blocks device code flow.", 558 | "Consider implementing a policy to block device code flow to enhance security by preventing unauthorized access through device code authentication.", 559 | "Blocking device code flow helps in preventing unauthorized access through device code authentication, which can be exploited by attackers. Implementing this policy ensures that only secure authentication methods are used.", 560 | @{ 561 | "Block Device Code Flow Overview" = "https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-authentication-flows#device-code-flow" 562 | }, 563 | $false, 564 | $true 565 | ), 566 | [Recommendation]::new( 567 | "CA-11", 568 | "Require MFA to Enroll a Device in Intune", 569 | "There is at least 1 policy that requires Multi-Factor Authentication (MFA) to enroll a device in Intune.", 570 | "Consider implementing a policy to require Multi-Factor Authentication (MFA) for enrolling devices in Intune to enhance security.", 571 | "Requiring MFA for device enrollment in Intune ensures that only authorized users can enroll devices, thereby enhancing the security of your organization's mobile device management.", 572 | @{ 573 | "MFA for Intune Enrollment Overview" = "https://learn.microsoft.com/en-us/mem/intune/enrollment/multi-factor-authentication" 574 | }, 575 | $false, 576 | $true 577 | ), 578 | [Recommendation]::new( 579 | "CA-12", 580 | "Block Unknown/Unsupported Devices", 581 | "There is no policy that blocks unknown or unsupported devices.", 582 | "Consider implementing a policy to block unknown or unsupported devices to enhance security by preventing unauthorized access from devices that do not meet your organization's security standards.", 583 | "Blocking unknown or unsupported devices helps in preventing unauthorized access from devices that may not comply with your organization's security policies. Implementing this policy ensures that only secure and compliant devices can access organizational resources.", 584 | @{ 585 | "Block Unknown/Unsupported Devices Overview" = "https://learn.microsoft.com/en-us/entra/identity/conditional-access/policy-all-users-device-unknown-unsupported" 586 | }, 587 | $false, 588 | $true 589 | ) 590 | ) 591 | 592 | function Update-PolicyStatus { 593 | param ( 594 | [ref]$Recommendation, 595 | $PolicyCheck, 596 | $StatusCheck 597 | ) 598 | 599 | if (&$StatusCheck $PolicyCheck) { 600 | $Recommendation.Value.Status = $Recommendation.Value.SwapStatus 601 | } 602 | 603 | if ($Recommendation.Value.Status -and $PolicyCheck.state -eq "enabled") { 604 | $Status1 = "policy-item success" 605 | $Status2 = "status-icon-large success" 606 | $Status3 = "fas fa-check-circle" 607 | } 608 | else { 609 | $Status1 = "policy-item warning" 610 | $Status2 = "status-icon-large warning" 611 | $Status3 = "fas fa-exclamation-triangle" 612 | } 613 | 614 | $CheckExcUG = $PolicyCheck.Conditions.Users.ExcludeUsers + $PolicyCheck.Conditions.Users.ExcludeGroups + $PolicyCheck.Conditions.Users.ExcludeRoles + $PolicyCheck.conditions.users.ExcludeGuestsOrExternalUsers.GuestOrExternalUserTypes -replace ',', ', ' 615 | $CheckIncUG = $PolicyCheck.Conditions.Users.IncludeUsers + $PolicyCheck.Conditions.Users.IncludeGroups + $PolicyCheck.Conditions.Users.IncludeRoles + $PolicyCheck.conditions.users.IncludeGuestsOrExternalUsers.GuestOrExternalUserTypes -replace ',', ', ' 616 | $CheckIncCond = $PolicyCheck.Conditions.Locations.includelocations + $PolicyCheck.Conditions.Platforms.IncludePlatforms 617 | $CheckExcCond = $PolicyCheck.Conditions.Locations.Excludelocations + $PolicyCheck.Conditions.Platforms.ExcludePlatforms 618 | $CheckGrant = $PolicyCheck.GrantControls.BuiltInControls + $PolicyCheck.GrantControls.AuthenticationStrength.DisplayName + $PolicyCheck.GrantControls.CustomAuthenticationFactors + $PolicyCheck.GrantControls.TermsOfUse 619 | $checkSession = "" 620 | 621 | if ($PolicyCheck.SessionControls.ApplicationEnforcedRestrictions.IsEnabled) { 622 | $checkSession += " ApplicationEnforcedRestrictions: $($PolicyCheck.SessionControls.ApplicationEnforcedRestrictions.IsEnabled)`n" 623 | } 624 | if ($PolicyCheck.SessionControls.CloudAppSecurity.IsEnabled) { 625 | $checkSession += " CloudAppSecurity: $($PolicyCheck.SessionControls.CloudAppSecurity.IsEnabled)`n" 626 | } 627 | if ($PolicyCheck.SessionControls.SignInFrequency.Value -and $conditionalAccessPolicy.SessionControls.SignInFrequency.Type) { 628 | $checkSession += " SignInFrequency: $($PolicyCheck.SessionControls.SignInFrequency.Value) $($conditionalAccessPolicy.SessionControls.SignInFrequency.Type)`n" 629 | } 630 | if ($PolicyCheck.SessionControls.PersistentBrowser.Mode) { 631 | $checkSession += " PersistentBrowser: $($PolicyCheck.SessionControls.PersistentBrowser.Mode)`n" 632 | } 633 | if ($PolicyCheck.SessionControls.ContinuousAccessEvaluation.Mode) { 634 | $checkSession += " ContinuousAccessEvaluation: $($PolicyCheck.SessionControls.ContinuousAccessEvaluation.Mode)`n" 635 | } 636 | if ($PolicyCheck.SessionControls.DisableResilienceDefaults) { 637 | $checkSession += " ResiliantDefaults: $($PolicyCheck.SessionControls.DisableResilienceDefaults)`n" 638 | } 639 | if ($PolicyCheck.SessionControls.AdditionalProperties.secureSignInSession.Values) { 640 | $checkSession += " secureSignInSession: $($PolicyCheck.SessionControls.AdditionalProperties.secureSignInSession.Values)`n" 641 | } 642 | 643 | if (&$StatusCheck $PolicyCheck) { 644 | $Recommendation.Value.Note += " 645 |
646 |
647 |
648 | $($PolicyCheck.DisplayName) 649 |
Status: $($PolicyCheck.state)
650 |
651 |
652 |
653 |
654 |
655 | Include 656 |
657 |
658 | Users: $($CheckIncUG -join ', ') 659 |
660 | Application/Actions: $($PolicyCheck.Conditions.Applications.IncludeApplications -join ', ') $($PolicyCheck.Conditions.Applications.IncludeUserActions -join ', ') 661 |
662 | Conditions: $($CheckIncCond -join ', ') 663 |
664 |
665 |
666 |
667 | Exclude 668 |
669 |
670 | Users: $($CheckExcUG -join ', ') 671 |
672 | Applications: $($PolicyCheck.Conditions.Applications.ExcludeApplications -join ', ') 673 |
674 | Conditions: $($CheckExcCond -join ', ') 675 |
676 |
677 |
678 |
679 | Access 680 |
681 |
682 | Grant: $($CheckGrant -join ', ') :$($PolicyCheck.GrantControls.Operator) 683 |
684 | Session: $($CheckSession -join ', ') 685 |
686 |
687 |
688 |
689 |
" 690 | } 691 | } 692 | 693 | 694 | $CheckFunctions = @{ 695 | "CA-00" = { 696 | param($PolicyCheck) 697 | $PolicyCheck.GrantControls.BuiltInControls -contains "Block" -and 698 | $PolicyCheck.Conditions.ClientAppTypes -contains "exchangeActiveSync" -and 699 | $PolicyCheck.Conditions.ClientAppTypes -contains "other" 700 | } 701 | "CA-01" = { 702 | param($PolicyCheck) 703 | $PolicyCheck.GrantControls.BuiltInControls -contains "Mfa" -and 704 | $PolicyCheck.Conditions.Users.IncludeUsers -eq "all" -and 705 | $PolicyCheck.Conditions.Applications.IncludeApplications -eq "all" 706 | } 707 | "CA-02" = { 708 | param($PolicyCheck) 709 | ($PolicyCheck.Conditions.Platforms.IncludePlatforms -contains "android" -or 710 | $PolicyCheck.Conditions.Platforms.IncludePlatforms -contains "iOS" -or 711 | $PolicyCheck.Conditions.Platforms.IncludePlatforms -contains "windowsPhone") -and 712 | ($PolicyCheck.GrantControls.BuiltInControls -contains "approvedApplication" -or 713 | $PolicyCheck.GrantControls.BuiltInControls -contains "compliantApplication" -or 714 | $PolicyCheck.GrantControls.BuiltInControls -contains "compliantDevice") 715 | } 716 | "CA-03" = { 717 | param($PolicyCheck) 718 | ($PolicyCheck.Conditions.Platforms.IncludePlatforms -contains "windows" -or 719 | $PolicyCheck.Conditions.Platforms.IncludePlatforms -contains "macOS") -and 720 | ($PolicyCheck.GrantControls.BuiltInControls -contains "compliantDevice" -or 721 | $PolicyCheck.GrantControls.BuiltInControls -contains "domainJoinedDevice") 722 | } 723 | 724 | "CA-04" = { 725 | param($PolicyCheck) 726 | ($PolicyCheck.Conditions.Users.IncludeRoles -contains "Privileged Role Administrator" -or 727 | $PolicyCheck.Conditions.Users.IncludeRoles -contains "Global Administrator" -or 728 | $PolicyCheck.Conditions.Users.IncludeRoles -contains "Privileged Authentication Administrator" -or 729 | $PolicyCheck.Conditions.Users.IncludeRoles -contains "Security Administrator" -or 730 | $PolicyCheck.Conditions.Users.IncludeRoles -contains "SharePoint Administrator" -or 731 | $PolicyCheck.Conditions.Users.IncludeRoles -contains "Exchange Administrator" -or 732 | $PolicyCheck.Conditions.Users.IncludeRoles -contains "Conditional Access Administrator" -or 733 | $PolicyCheck.Conditions.Users.IncludeRoles -contains "Helpdesk Administrator" -or 734 | $PolicyCheck.Conditions.Users.IncludeRoles -contains "Billing Administrator" -or 735 | $PolicyCheck.Conditions.Users.IncludeRoles -contains "User Administrator" -or 736 | $PolicyCheck.Conditions.Users.IncludeRoles -contains "Authentication Administrator" -or 737 | $PolicyCheck.Conditions.Users.IncludeRoles -contains "Application Administrator" -or 738 | $PolicyCheck.Conditions.Users.IncludeRoles -contains "Cloud Application Administrator" -or 739 | $PolicyCheck.Conditions.Users.IncludeRoles -contains "Password Administrator") -and 740 | ($PolicyCheck.GrantControls.BuiltInControls -contains "Mfa" -or 741 | $PolicyCheck.GrantControls.AuthenticationStrength.DisplayName -contains "Phishing-resistant MFA" -or 742 | $PolicyCheck.GrantControls.AuthenticationStrength.DisplayName -contains "Passwordless MFA" -or 743 | $PolicyCheck.GrantControls.AuthenticationStrength.DisplayName -contains "Multifactor authentication") 744 | } 745 | "CA-05" = { 746 | param($PolicyCheck) 747 | ($PolicyCheck.Conditions.Users.IncludeRoles -contains "Privileged Role Administrator" -or 748 | $PolicyCheck.Conditions.Users.IncludeRoles -contains "Global Administrator" -or 749 | $PolicyCheck.Conditions.Users.IncludeRoles -contains "Privileged Authentication Administrator" -or 750 | $PolicyCheck.Conditions.Users.IncludeRoles -contains "Security Administrator" -or 751 | $PolicyCheck.Conditions.Users.IncludeRoles -contains "SharePoint Administrator" -or 752 | $PolicyCheck.Conditions.Users.IncludeRoles -contains "Exchange Administrator" -or 753 | $PolicyCheck.Conditions.Users.IncludeRoles -contains "Conditional Access Administrator" -or 754 | $PolicyCheck.Conditions.Users.IncludeRoles -contains "Helpdesk Administrator" -or 755 | $PolicyCheck.Conditions.Users.IncludeRoles -contains "Billing Administrator" -or 756 | $PolicyCheck.Conditions.Users.IncludeRoles -contains "User Administrator" -or 757 | $PolicyCheck.Conditions.Users.IncludeRoles -contains "Authentication Administrator" -or 758 | $PolicyCheck.Conditions.Users.IncludeRoles -contains "Application Administrator" -or 759 | $PolicyCheck.Conditions.Users.IncludeRoles -contains "Cloud Application Administrator" -or 760 | $PolicyCheck.Conditions.Users.IncludeRoles -contains "Password Administrator") -and 761 | ($PolicyCheck.GrantControls.AuthenticationStrength.DisplayName -contains "Phishing-resistant MFA") 762 | } 763 | "CA-06" = { 764 | param($PolicyCheck) 765 | ($PolicyCheck.Conditions.Users.IncludeUsers -ne $null -and $PolicyCheck.Conditions.Users.ExcludeUsers -ne $null -and 766 | ($PolicyCheck.Conditions.Users.IncludeUsers | ForEach-Object { $PolicyCheck.Conditions.Users.ExcludeUsers -contains $_ })) -or 767 | ($PolicyCheck.Conditions.Users.IncludeGroups -ne $null -and $PolicyCheck.Conditions.Users.ExcludeGroups -ne $null -and 768 | ($PolicyCheck.Conditions.Users.IncludeGroups | ForEach-Object { $PolicyCheck.Conditions.Users.ExcludeGroups -contains $_ })) -or 769 | ($PolicyCheck.Conditions.Users.IncludeRoles -ne $null -and $PolicyCheck.Conditions.Users.ExcludeRoles -ne $null -and 770 | ($PolicyCheck.Conditions.Users.IncludeRoles | ForEach-Object { $PolicyCheck.Conditions.Users.ExcludeRoles -contains $_ })) -or 771 | ($PolicyCheck.Conditions.Platforms.IncludePlatforms -ne $null -and $PolicyCheck.Conditions.Platforms.ExcludePlatforms -ne $null -and 772 | ($PolicyCheck.Conditions.Platforms.IncludePlatforms | ForEach-Object { $PolicyCheck.Conditions.Platforms.ExcludePlatforms -contains $_ })) -or 773 | ($PolicyCheck.Conditions.Locations.IncludeLocations -ne $null -and $PolicyCheck.Conditions.Locations.ExcludeLocations -ne $null -and 774 | ($PolicyCheck.Conditions.Locations.IncludeLocations | ForEach-Object { $PolicyCheck.Conditions.Locations.ExcludeLocations -contains $_ })) -or 775 | ($PolicyCheck.Conditions.Applications.IncludeApplications -ne $null -and $PolicyCheck.Conditions.Applications.ExcludeApplications -ne $null -and 776 | ($PolicyCheck.Conditions.Applications.IncludeApplications | ForEach-Object { $PolicyCheck.Conditions.Applications.ExcludeApplications -contains $_ })) 777 | } 778 | "CA-07" = { 779 | param($PolicyCheck) 780 | ($PolicyCheck.Conditions.Users.IncludeUsers -eq $null -or $PolicyCheck.Conditions.Users.IncludeUsers.Count -eq 0 -or $PolicyCheck.Conditions.Users.IncludeUsers -eq "None") -and 781 | ($PolicyCheck.Conditions.Users.IncludeGroups -eq $null -or $PolicyCheck.Conditions.Users.IncludeGroups.Count -eq 0 -or 782 | ($PolicyCheck.Conditions.Users.IncludeGroups | ForEach-Object { $_ -match '\((\d+)\)' -and [int]$matches[1] -eq 0 })) -and 783 | ($PolicyCheck.Conditions.Users.IncludeRoles -eq $null -or $PolicyCheck.Conditions.Users.IncludeRoles.Count -eq 0) -and 784 | ($PolicyCheck.conditions.users.IncludeGuestsOrExternalUsers.GuestOrExternalUserTypes -eq $null) 785 | } 786 | "CA-08" = { 787 | param($PolicyCheck) 788 | $PolicyCheck.Conditions.Users.IncludeUsers -ne "None" -and 789 | $PolicyCheck.Conditions.Users.IncludeUsers -ne $null -and 790 | $PolicyCheck.Conditions.Users.IncludeUsers -ne "All" -and 791 | $PolicyCheck.Conditions.Users.IncludeUsers -ne "GuestsOrExternalUsers" 792 | } 793 | "CA-09" = { 794 | param($PolicyCheck) 795 | ($PolicyCheck.Conditions.SignInRiskLevels -ne $null) -or 796 | ($PolicyCheck.Conditions.UserRiskLevels -ne $null) 797 | } 798 | "CA-10" = { 799 | param($PolicyCheck) 800 | $PolicyCheck.Conditions.AdditionalProperties.authenticationFlows.Values -split ',' -contains "deviceCodeFlow" -and 801 | $PolicyCheck.grantcontrols.BuiltInControls -contains "Block" 802 | } 803 | "CA-11" = { 804 | param($PolicyCheck) 805 | ($PolicyCheck.Conditions.Applications.IncludeUserActions -contains "urn:user:registerdevice") -and 806 | ($PolicyCheck.GrantControls.BuiltInControls -contains "Mfa") 807 | } 808 | "CA-12" = { 809 | param($PolicyCheck) 810 | ($PolicyCheck.GrantControls.BuiltInControls -contains "Block") -and 811 | ($PolicyCheck.Conditions.Platforms.IncludePlatforms -contains "all") -and 812 | ($PolicyCheck.Conditions.Platforms.ExcludePlatforms.Count -gt 0) 813 | } 814 | } 815 | 816 | 817 | foreach ($policy in $CAPolicy) { 818 | foreach ($recommendation in $recommendations) { 819 | Update-PolicyStatus -Recommendation ([ref]$recommendation) -PolicyCheck $policy -StatusCheck $CheckFunctions[$recommendation.Control] 820 | } 821 | } 822 | 823 | 824 | 825 | 826 | 827 | #Set Row Order 828 | $sort = "Name", "Status", "DateModified", "Users", "UsersInclude", "UsersExclude", "Cloud apps or actions", "ApplicationsIncluded", "ApplicationsExcluded", ` 829 | "userActions", "AuthContext", "Conditions", "UserRisk", "SignInRisk", "PlatformsInclude", "PlatformsExclude", "ClientApps", "LocationsIncluded", ` 830 | "LocationsExcluded", "Devices", "DevicesIncluded", "DevicesExcluded", "DeviceFilters", "AuthenticationFlows", "Grant Controls", "Block", "Require MFA", "Authentication Strength MFA", "CompliantDevice", ` 831 | "DomainJoinedDevice", "CompliantApplication", "ApprovedApplication", "PasswordChange", "TermsOfUse", "CustomControls", "GrantOperator", ` 832 | "Session Controls", "ApplicationEnforcedRestrictions", "CloudAppSecurity", "SignInFrequency", "PersistentBrowser", "ContinuousAccessEvaluation", "ResiliantDefaults", "secureSignInSession" 833 | 834 | #Debug 835 | #$pivot | Sort-Object $sort | Out-GridView 836 | function Display-RecommendationsAsHTMLFragment { 837 | param ( 838 | [Parameter(Mandatory = $true)] 839 | [Recommendation[]]$Recommendations 840 | ) 841 | 842 | $htmlFragment = @" 843 |
844 | "@ 845 | 846 | foreach ($rec in $Recommendations) { 847 | $links = "" 848 | foreach ($key in $rec.Links.Keys) { 849 | $links += "
$key
" 850 | } 851 | 852 | $excluded = $rec.Excluded 853 | if ($($rec.Status)) { $RecStatus = "success"; $RecStatusNote = $($rec.PassText) }else { $RecStatus = "warning" ; $RecStatusNote = $($rec.FailRecommendation) } 854 | $htmlFragment += @" 855 |
856 |
857 |
$($rec.Name)
858 |
$($rec.Control)
859 | 860 | 861 |
862 |
$($rec.Importance)
863 | 868 | 869 | 870 |
$RecStatusNote
871 | 872 |
$($rec.Note)
873 | 874 |
875 |
876 | "@ 877 | } 878 | 879 | $htmlFragment += @" 880 |
881 | "@ 882 | 883 | return $htmlFragment 884 | } 885 | 886 | if ($HTMLExport) { 887 | Write-host "Saving to File: HTML" 888 | $jquery = ' 889 | ' 914 | $style = @" 915 | /* General Styles */ 916 | html, body { 917 | font-family: Arial, sans-serif; 918 | } 919 | 920 | .title { 921 | font-size: 1.5em; 922 | font-weight: bold; 923 | top: 0; 924 | right: 0; 925 | left: 0; 926 | } 927 | 928 | .navbar-custom { 929 | background-color: #005494; 930 | color: white; 931 | padding-bottom: 10px; 932 | } 933 | 934 | .navbar-custom .navbar-brand, 935 | .navbar-custom .navbar-text { 936 | color: white; 937 | padding-top: 70px; 938 | padding-bottom: 10px; 939 | } 940 | 941 | .sr-only { 942 | border: 0; 943 | clip: rect(0, 0, 0, 0); 944 | height: 1px; 945 | margin: -1px; 946 | overflow: hidden; 947 | padding: 0; 948 | position: absolute; 949 | width: 1px; 950 | } 951 | 952 | .sr-only-focusable:active, 953 | .sr-only-focusable:focus { 954 | clip: auto; 955 | height: auto; 956 | margin: 0; 957 | overflow: visible; 958 | position: static; 959 | width: auto; 960 | } 961 | 962 | /* Export Policies Styles */ 963 | table { 964 | border-collapse: collapse; 965 | margin-bottom: 30px; 966 | margin-top: 55px; 967 | font-size: 0.9em; 968 | min-width: 400px; 969 | } 970 | 971 | thead tr { 972 | background-color: #009879; 973 | color: #ffffff; 974 | text-align: center; 975 | } 976 | 977 | th, td { 978 | min-width: 250px; 979 | padding: 12px 15px; 980 | border: 1px solid lightgray; 981 | vertical-align: top; 982 | text-align: center; 983 | } 984 | 985 | tbody tr:nth-of-type(even) { 986 | background-color: #f3f3f3; 987 | } 988 | 989 | tbody tr:last-of-type { 990 | border-bottom: 2px solid #009879; 991 | } 992 | 993 | tr:hover { 994 | background-color: #d8d8d8 !important; 995 | } 996 | 997 | .selected:not(th) { 998 | background-color: #eaf7ff !important; 999 | } 1000 | 1001 | th { 1002 | background-color: white; 1003 | } 1004 | 1005 | .colselected { 1006 | width: 10%; 1007 | border: 5px solid #59c7fb; 1008 | } 1009 | 1010 | table tr th:first-child, table tr td:first-child { 1011 | position: sticky; 1012 | inset-inline-start: 0; 1013 | background-color: #005494; 1014 | border: 0px; 1015 | color: #fff; 1016 | font-weight: bolder; 1017 | text-align: center; 1018 | } 1019 | 1020 | tbody tr:nth-of-type(even) td:first-child { 1021 | background-color: #547c9b; 1022 | } 1023 | 1024 | tbody tr:nth-of-type(5), 1025 | tbody tr:nth-of-type(8), 1026 | tbody tr:nth-of-type(13), 1027 | tbody tr:nth-of-type(25), 1028 | tbody tr:nth-of-type(37) { 1029 | background-color: #005494 !important; 1030 | } 1031 | .tooltip-container { 1032 | position: relative; 1033 | display: inline-block; 1034 | } 1035 | 1036 | /* Tooltip text */ 1037 | .tooltip-text { 1038 | visibility: hidden; 1039 | width: 200px; 1040 | background-color: black; 1041 | color: #fff; 1042 | text-align: center; 1043 | border-radius: 6px; 1044 | padding: 5px 0; 1045 | position: absolute; 1046 | z-index: 1; 1047 | top: 115%; /* Position the tooltip below the text */ 1048 | left: 50%; 1049 | margin-left: -100px; 1050 | opacity: 0; 1051 | transition: opacity 0.3s; 1052 | } 1053 | 1054 | .tooltip-container:hover .tooltip-text { 1055 | visibility: visible; 1056 | opacity: 1; 1057 | } 1058 | /* Recommendations Styles */ 1059 | #ca-security-checks { 1060 | padding: 20px; 1061 | background-color: #f8f9fa; 1062 | border: 1px solid #ddd; 1063 | border-radius: 5px; 1064 | margin-top: 55px; 1065 | } 1066 | 1067 | #ca-security-checks h2 { 1068 | margin-top: 0; 1069 | color: #343a40; 1070 | } 1071 | 1072 | #ca-security-checks p { 1073 | color: #6c757d; 1074 | } 1075 | .header { 1076 | display: flex; 1077 | align-items: center; 1078 | } 1079 | .recommendations { 1080 | font-family: Arial, sans-serif; 1081 | } 1082 | .recommendation.success { 1083 | border-left-color: green; 1084 | border-left-width: 7px; 1085 | } 1086 | .recommendation.warning { 1087 | border-left-color: orange; 1088 | border-left-width: 7px; 1089 | } 1090 | .recommendation { 1091 | padding: 10px; 1092 | margin-bottom: 10px; 1093 | border: 1px solid #ddd; 1094 | border-radius: 5px; 1095 | background-color: #f9f9f9; 1096 | } 1097 | 1098 | .recommendation div { 1099 | margin-bottom: 5px; 1100 | } 1101 | .control { 1102 | color: gray; 1103 | } 1104 | 1105 | .links div { 1106 | margin-left: 20px; 1107 | } 1108 | 1109 | .recommendation a { 1110 | color: #0073e6; 1111 | text-decoration: none; 1112 | } 1113 | 1114 | .recommendation a:hover { 1115 | text-decoration: underline; 1116 | } 1117 | 1118 | .recommendation strong { 1119 | display: inline-block; 1120 | } 1121 | 1122 | .status-icon { 1123 | display: inline-block; 1124 | padding-left: 10px; 1125 | } 1126 | 1127 | .status-icon.success { 1128 | color: green; 1129 | } 1130 | 1131 | .status-icon.warning { 1132 | color: orange; 1133 | } 1134 | 1135 | .status-icon.error { 1136 | color: red; 1137 | } 1138 | 1139 | .policy { 1140 | margin-top: 10px; 1141 | } 1142 | 1143 | .policy-item { 1144 | border: 2px solid; 1145 | padding: 10px; 1146 | border-radius: 5px; 1147 | margin-bottom: 10px; 1148 | } 1149 | 1150 | .policy-item.success { 1151 | border-color: green; 1152 | background-color: #e6ffe6; 1153 | } 1154 | 1155 | .policy-item.warning { 1156 | border-color: orange; 1157 | background-color: #fff8e6; 1158 | } 1159 | 1160 | .policy-item.error { 1161 | border-color: red; 1162 | background-color: #ffe6e6; 1163 | } 1164 | 1165 | .policy-item strong { 1166 | display: block; 1167 | margin-bottom: 5px; 1168 | } 1169 | 1170 | .policy-content { 1171 | display: flex; 1172 | flex-direction: column; 1173 | padding-left: 20px; 1174 | margin-top: 5px; 1175 | } 1176 | 1177 | .policy-include, .policy-exclude, .policy-grant { 1178 | display: flex; 1179 | align-items: flex-start; 1180 | margin-top: 5px; 1181 | } 1182 | 1183 | .label-container { 1184 | display: flex; 1185 | align-items: center; 1186 | margin-right: 10px; /* Space between the label and content */ 1187 | } 1188 | 1189 | .include-label, .exclude-label, .grant-label { 1190 | writing-mode: vertical-rl; 1191 | transform: rotate(180deg); /* Optional: Rotate the text to read from bottom to top */ 1192 | border-left: 3px solid darkgrey; 1193 | color: darkgray; 1194 | } 1195 | 1196 | .include-content, .exclude-content, .grant-content { 1197 | margin-left: 10px; /* Space between the label and content */ 1198 | } 1199 | 1200 | .policy-header { 1201 | position: relative; 1202 | padding-right: 40px; 1203 | } 1204 | 1205 | 1206 | .status-icon-large { 1207 | position: absolute; 1208 | top: 0; 1209 | right: 0; 1210 | font-size: 2em; 1211 | } 1212 | 1213 | .status-icon-large.success { 1214 | color: green; 1215 | } 1216 | 1217 | .status-icon-large.warning { 1218 | color: orange; 1219 | } 1220 | 1221 | .status-icon-large.error { 1222 | color: red; 1223 | } 1224 | 1225 | .fa-external-link-alt { 1226 | color: black; 1227 | } 1228 | 1229 | 1230 | "@ 1231 | 1232 | $html = " 1233 | 1234 | 1235 | 1236 | 1237 | 1238 | 1239 | 1240 | 1241 | 1242 | $jquery 1245 | " 1259 | 1260 | $SecurityCheck = Display-RecommendationsAsHTMLFragment -Recommendations $recommendations 1261 | 1262 | Write-host "Launching: Web Browser" 1263 | $Launch = $ExportLocation + $FileName 1264 | $HTML += "" 1267 | $html += $SecurityCheck 1268 | Add-Type -AssemblyName System.Web 1269 | [System.Web.HttpUtility]::HtmlDecode($HTML) | Out-File $Launch 1270 | 1271 | start-process $Launch 1272 | } 1273 | if ($JsonExport) { 1274 | Write-host "Saving to File: JSON" 1275 | $LaunchJson = $ExportLocation + $JsonFileName 1276 | $CAPolicy | ConvertTo-Json -Depth 8 | Out-File $LaunchJson 1277 | start-process $LaunchJson 1278 | } -------------------------------------------------------------------------------- /Export-CaPolicy.ps1: -------------------------------------------------------------------------------- 1 | #Conditional Access Export Utility 2 | 3 | <# 4 | .SYNOPSIS 5 | Conditional Access Export Utility 6 | .DESCRIPTION 7 | Exports CA Policy to HTML Format for auditing/historical purposes. 8 | 9 | .NOTES 10 | Douglas Baker 11 | @dougsbaker 12 | 13 | 14 | 15 | CONTRIBUTORS 16 | Andres Bohren 17 | @andresbohren 18 | 19 | 20 | Output report uses open source components for HTML formatting 21 | - bootstrap - MIT License - https://getbootstrap.com/docs/4.0/about/license/ 22 | - fontawesome - CC BY 4.0 License - https://fontawesome.com/license/free 23 | 24 | ############################################################################ 25 | This sample script is not supported under any standard support program or service. 26 | This sample script is provided AS IS without warranty of any kind. 27 | This work is licensed under a Creative Commons Attribution 4.0 International License 28 | https://creativecommons.org/licenses/by-nc-sa/4.0/ 29 | ############################################################################ 30 | 31 | 32 | 33 | 34 | 35 | #> 36 | 37 | [CmdletBinding()] 38 | param ( 39 | [Parameter()] 40 | [String]$TenantID, 41 | [Parameter()] 42 | [String]$PolicyID 43 | ) 44 | 45 | $ExportLocation = $PSScriptRoot 46 | if (!$ExportLocation) { $ExportLocation = $PWD } 47 | $FileName = "\CAPolicy.html" 48 | $HTMLExport = $true 49 | 50 | try { 51 | Get-MgIdentityConditionalAccessPolicy -ErrorAction Stop > $null 52 | Write-host "Connected: MgGraph" 53 | } 54 | catch { 55 | Write-host "Connecting: MgGraph" 56 | Try { 57 | #Connect-AzureAD 58 | #Select-MgProfile -Name "beta" 59 | Connect-MgGraph -Scopes 'Policy.Read.All', 'Directory.Read.All', 'Application.Read.All', 'Agreement.Read.All' -nowelcome 60 | } 61 | Catch { 62 | Write-host "Error: Please Install MgGraph Module" -ForegroundColor Yellow 63 | Write-Host "Run: Install-Module Microsoft.Graph" -ForegroundColor Yellow 64 | Exit 65 | } 66 | } 67 | 68 | $TenantData = Get-MgOrganization 69 | $TenantName = $TenantData.DisplayName 70 | $date = Get-Date 71 | Write-Host "Connected: $TenantName tenant" 72 | 73 | 74 | #Collect CA Policy 75 | Write-host "Exporting: CA Policy" 76 | if ($PolicyID) { 77 | $CAPolicy = Get-MgIdentityConditionalAccessPolicy -ConditionalAccessPolicyId $PolicyID 78 | } 79 | else { 80 | $CAPolicy = Get-MgIdentityConditionalAccessPolicy -all -ExpandProperty * 81 | 82 | } 83 | 84 | $TenantData = Get-MgOrganization 85 | $TenantName = $TenantData.DisplayName 86 | $date = Get-Date 87 | 88 | 89 | Write-host "Extracting: Names from Guid's" 90 | #Swap User Guid With Names 91 | #Get Name 92 | $ADUsers = $CAPolicy.Conditions.Users.IncludeUsers 93 | $ADUsers += $CAPolicy.Conditions.Users.IncludeGroups 94 | $ADUsers += $CAPolicy.Conditions.Users.IncludeRoles 95 | $ADUsers += $CAPolicy.Conditions.Users.ExcludeUsers 96 | $ADUsers += $CAPolicy.Conditions.Users.ExcludeGroups 97 | $ADUsers += $CAPolicy.Conditions.Users.ExcludeRoles 98 | 99 | 100 | 101 | # Filter the $AdUsers array to include only valid GUIDs 102 | $ADsearch = $AdUsers | Where-Object { 103 | ([Guid]::TryParse($_, [ref] [Guid]::Empty)) 104 | } 105 | 106 | #users Hashtable 107 | $mgobjects = Get-MgDirectoryObjectById -ids $ADsearch 108 | $mgObjectsLookup = @{} 109 | foreach ($obj in $mgobjects) { 110 | $mgObjectsLookup[$obj.Id] = $obj.AdditionalProperties.displayName 111 | } 112 | 113 | 114 | #Change Guid's 115 | foreach ($policy in $caPolicy) { 116 | # Check if the policy has Conditions and Users and ExcludeUsers properties 117 | if ($policy.Conditions -and $policy.Conditions.Users <#-and $policy.Conditions.Users.ExcludeUsers#>) { 118 | # Loop through each user in ExcludeUsers and replace with displayName if found in $mgObjectsLookup 119 | for ($i = 0; $i -lt $policy.Conditions.Users.ExcludeUsers.Count; $i++) { 120 | $userId = $policy.Conditions.Users.ExcludeUsers[$i] 121 | if ($mgObjectsLookup.ContainsKey($userId)) { 122 | $policy.Conditions.Users.ExcludeUsers[$i] = $mgObjectsLookup[$userId] 123 | } 124 | } 125 | 126 | for ($i = 0; $i -lt $policy.Conditions.Users.IncludeUsers.Count; $i++) { 127 | $userId = $policy.Conditions.Users.IncludeUsers[$i] 128 | if ($mgObjectsLookup.ContainsKey($userId)) { 129 | $policy.Conditions.Users.IncludeUsers[$i] = $mgObjectsLookup[$userId] 130 | } 131 | } 132 | for ($i = 0; $i -lt $policy.Conditions.Users.IncludeGroups.Count; $i++) { 133 | $userId = $policy.Conditions.Users.IncludeGroups[$i] 134 | if ($mgObjectsLookup.ContainsKey($userId)) { 135 | $policy.Conditions.Users.IncludeGroups[$i] = $mgObjectsLookup[$userId] 136 | } 137 | } 138 | for ($i = 0; $i -lt $policy.Conditions.Users.ExcludeGroups.Count; $i++) { 139 | $userId = $policy.Conditions.Users.ExcludeGroups[$i] 140 | if ($mgObjectsLookup.ContainsKey($userId)) { 141 | $policy.Conditions.Users.ExcludeGroups[$i] = $mgObjectsLookup[$userId] 142 | } 143 | } 144 | for ($i = 0; $i -lt $policy.Conditions.Users.IncludeRoles.Count; $i++) { 145 | $userId = $policy.Conditions.Users.IncludeRoles[$i] 146 | if ($mgObjectsLookup.ContainsKey($userId)) { 147 | $policy.Conditions.Users.IncludeRoles[$i] = $mgObjectsLookup[$userId] 148 | } 149 | } 150 | for ($i = 0; $i -lt $policy.Conditions.Users.ExcludeRoles.Count; $i++) { 151 | $userId = $policy.Conditions.Users.ExcludeRoles[$i] 152 | if ($mgObjectsLookup.ContainsKey($userId)) { 153 | $policy.Conditions.Users.ExcludeRoles[$i] = $mgObjectsLookup[$userId] 154 | } 155 | } 156 | } 157 | 158 | } 159 | 160 | #Swap App ID with name 161 | $MGApps = Get-MgServicePrincipal -All 162 | #Hash Table 163 | $MGAppsLookup = @{} 164 | foreach ($obj in $MGApps) { 165 | $MGAppsLookup[$obj.AppId] = "
" + $obj.DisplayName + "App Id:" + $obj.AppId + "
" 166 | } 167 | #"
" + $obj.DisplayName +"App Id:"+ $obj.AppId +"
" 168 | 169 | foreach ($policy in $caPolicy) { 170 | if ($policy.Conditions -and $policy.Conditions.Applications) { 171 | 172 | for ($i = 0; $i -lt $policy.Conditions.Applications.ExcludeApplications.Count; $i++) { 173 | $AppId = $policy.Conditions.Applications.ExcludeApplications[$i] 174 | if ($MGAppsLookup.ContainsKey($AppId)) { 175 | $policy.Conditions.Applications.ExcludeApplications[$i] = $MGAppsLookup[$AppId] 176 | } 177 | } 178 | 179 | for ($i = 0; $i -lt $policy.Conditions.Applications.IncludeApplications.Count; $i++) { 180 | $AppId = $policy.Conditions.Applications.IncludeApplications[$i] 181 | if ($MGAppsLookup.ContainsKey($AppId)) { 182 | $policy.Conditions.Applications.IncludeApplications[$i] = $MGAppsLookup[$AppId] 183 | } 184 | } 185 | } 186 | 187 | } 188 | 189 | #Swap Location with Names 190 | $mgLoc = Get-MgIdentityConditionalAccessNamedLocation 191 | $MGLocLookup = @{} 192 | foreach ($obj in $mgLoc) { 193 | $MGLocLookup[$obj.Id] = $obj.DisplayName 194 | } 195 | foreach ($policy in $caPolicy) { 196 | #Set Locations 197 | if ($policy.Conditions -and $policy.Conditions.Locations) { 198 | for ($i = 0; $i -lt $policy.Conditions.Locations.IncludeLocations.Count; $i++) { 199 | $LocId = $policy.Conditions.Locations.IncludeLocations[$i] 200 | if ($MGLocLookup.ContainsKey($LocId)) { 201 | $policy.Conditions.Locations.IncludeLocations[$i] = $MGLocLookup[$LocId] 202 | } 203 | } 204 | for ($i = 0; $i -lt $policy.Conditions.Locations.ExcludeLocations.Count; $i++) { 205 | $LocId = $policy.Conditions.Locations.ExcludeLocations[$i] 206 | if ($MGLocLookup.ContainsKey($LocId)) { 207 | $policy.Conditions.Locations.ExcludeLocations[$i] = $MGLocLookup[$LocId] 208 | } 209 | } 210 | } 211 | 212 | } 213 | 214 | #Switch TOU Id for Name 215 | $mgTou = Get-MgAgreement 216 | $MGTouLookup = @{} 217 | foreach ($obj in $mgTou) { 218 | $MGTouLookup[$obj.Id] = $obj.DisplayName 219 | } 220 | foreach ($policy in $caPolicy) { 221 | if ($policy.GrantControls -and $policy.GrantControls.TermsOfUse) { 222 | 223 | for ($i = 0; $i -lt $policy.GrantControls.TermsOfUse.Count; $i++) { 224 | $TouId = $policy.GrantControls.TermsOfUse[$i] 225 | if ($MGTouLookup.ContainsKey($TouId)) { 226 | $policy.GrantControls.TermsOfUse[$i] = $MGTouLookup[$TouId] 227 | } 228 | } 229 | } 230 | } 231 | #swap Admin Roles 232 | $mgRole = Get-MgDirectoryRoleTemplate 233 | $mgRoleLookup = @{} 234 | foreach ($obj in $mgRole) { 235 | $mgRoleLookup[$obj.Id] = $obj.DisplayName 236 | } 237 | foreach ($policy in $caPolicy) { 238 | if ($policy.Conditions.Users -and $policy.Conditions.Users.IncludeRoles) { 239 | 240 | for ($i = 0; $i -lt $policy.Conditions.Users.IncludeRoles.Count; $i++) { 241 | $RoleId = $policy.Conditions.Users.IncludeRoles[$i] 242 | if ($mgRoleLookup.ContainsKey($RoleId)) { 243 | $policy.Conditions.Users.IncludeRoles[$i] = $mgRoleLookup[$RoleId] 244 | } 245 | } 246 | } 247 | if ($policy.Conditions.Users -and $policy.Conditions.Users.ExcludeRoles) { 248 | 249 | for ($i = 0; $i -lt $policy.Conditions.Users.ExcludeRoles.Count; $i++) { 250 | $RoleId = $policy.Conditions.Users.ExcludeRoles[$i] 251 | if ($mgRoleLookup.ContainsKey($RoleId)) { 252 | $policy.Conditions.Users.ExcludeRoles[$i] = $mgRoleLookup[$RoleId] 253 | } 254 | } 255 | } 256 | } 257 | 258 | # exit 259 | $CAExport = [PSCustomObject]@() 260 | 261 | $AdUsers = @() 262 | $Apps = @() 263 | #Extract Values 264 | Write-host "Extracting: CA Policy Data" 265 | foreach ( $Policy in $CAPolicy) { 266 | 267 | $IncludeUG = $null 268 | $IncludeUG = $Policy.Conditions.Users.IncludeUsers 269 | $IncludeUG += $Policy.Conditions.Users.IncludeGroups 270 | $IncludeUG += $Policy.Conditions.Users.IncludeRoles 271 | $DateCreated = $null 272 | $DateCreated = $policy.CreatedDateTime 273 | $DateModified = $null 274 | $DateModified = $Policy.ModifiedDateTime 275 | 276 | 277 | $ExcludeUG = $null 278 | $ExcludeUG = $Policy.Conditions.Users.ExcludeUsers 279 | $ExcludeUG += $Policy.Conditions.Users.ExcludeGroups 280 | $ExcludeUG += $Policy.Conditions.Users.ExcludeRoles 281 | 282 | 283 | $Apps += $Policy.Conditions.Applications.IncludeApplications 284 | $Apps += $Policy.Conditions.Applications.ExcludeApplications 285 | 286 | 287 | $InclLocation = $Null 288 | $ExclLocation = $Null 289 | $InclLocation = $Policy.Conditions.Locations.includelocations 290 | $ExclLocation = $Policy.Conditions.Locations.Excludelocations 291 | 292 | $InclPlat = $Null 293 | $ExclPlat = $Null 294 | $InclPlat = $Policy.Conditions.Platforms.IncludePlatforms 295 | $ExclPlat = $Policy.Conditions.Platforms.ExcludePlatforms 296 | $InclDev = $null 297 | $ExclDev = $null 298 | $InclDev = $Policy.Conditions.Devices.IncludeDevices 299 | $ExclDev = $Policy.Conditions.Devices.ExcludeDevices 300 | $devFilters = $null 301 | $devFilters = $Policy.Conditions.Devices.DeviceFilter.Rule 302 | 303 | $CAExport += New-Object PSObject -Property @{ 304 | Name = $Policy.DisplayName; 305 | Status = $Policy.State; 306 | DateModified = $DateModified; 307 | Users = ""; 308 | UsersInclude = ($IncludeUG -join ", `r`n"); 309 | UsersExclude = ($ExcludeUG -join ", `r`n"); 310 | 'Cloud apps or actions' = ""; 311 | ApplicationsIncluded = ($Policy.Conditions.Applications.IncludeApplications -join ", `r`n"); 312 | ApplicationsExcluded = ($Policy.Conditions.Applications.ExcludeApplications -join ", `r`n"); 313 | userActions = ($Policy.Conditions.Applications.IncludeUserActions -join ", `r`n"); 314 | AuthContext = ($Policy.Conditions.Applications.IncludeAuthenticationContextClassReferences -join ", `r`n"); 315 | Conditions = ""; 316 | UserRisk = ($Policy.Conditions.UserRiskLevels -join ", `r`n"); 317 | SignInRisk = ($Policy.Conditions.SignInRiskLevels -join ", `r`n"); 318 | # Platforms = $Policy.Conditions.Platforms; 319 | PlatformsInclude = ($InclPlat -join ", `r`n"); 320 | PlatformsExclude = ($ExclPlat -join ", `r`n"); 321 | # Locations = $Policy.Conditions.Locations; 322 | LocationsIncluded = ($InclLocation -join ", `r`n"); 323 | LocationsExcluded = ($ExclLocation -join ", `r`n"); 324 | ClientApps = ($Policy.Conditions.ClientAppTypes -join ", `r`n"); 325 | # Devices = $Policy.Conditions.Devices; 326 | DevicesIncluded = ($InclDev -join ", `r`n"); 327 | DevicesExcluded = ($ExclDev -join ", `r`n"); 328 | DeviceFilters = ($devFilters -join ", `r`n"); 329 | 'Grant Controls' = ""; 330 | # Grant = ($Policy.GrantControls.BuiltInControls -join ", `r`n"); 331 | Block = if ($Policy.GrantControls.BuiltInControls -contains "Block") { "True" } else { "" } 332 | 'Require MFA' = if ($Policy.GrantControls.BuiltInControls -contains "Mfa") { "True" } else { "" } 333 | 'Authentication Strength MFA' = $Policy.GrantControls.AuthenticationStrength.DisplayName 334 | 'CompliantDevice' = if ($Policy.GrantControls.BuiltInControls -contains "CompliantDevice") { "True" } else { "" } 335 | 'DomainJoinedDevice' = if ($Policy.GrantControls.BuiltInControls -contains "DomainJoinedDevice") { "True" } else { "" } 336 | 'CompliantApplication' = if ($Policy.GrantControls.BuiltInControls -contains "CompliantApplication") { "True" } else { "" } 337 | 'ApprovedApplication' = if ($Policy.GrantControls.BuiltInControls -contains "ApprovedApplication") { "True" } else { "" } 338 | 'PasswordChange' = if ($Policy.GrantControls.BuiltInControls -contains "PasswordChange") { "True" } else { "" } 339 | TermsOfUse = ($Policy.GrantControls.TermsOfUse -join ", `r`n"); 340 | CustomControls = ($Policy.GrantControls.CustomAuthenticationFactors -join ", `r`n"); 341 | GrantOperator = $Policy.GrantControls.Operator 342 | # Session = $Policy.SessionControls 343 | 'Session Controls' = ""; 344 | ApplicationEnforcedRestrictions = $Policy.SessionControls.ApplicationEnforcedRestrictions.IsEnabled 345 | CloudAppSecurity = $Policy.SessionControls.CloudAppSecurity.IsEnabled 346 | SignInFrequency = "$($Policy.SessionControls.SignInFrequency.Value) $($conditionalAccessPolicy.SessionControls.SignInFrequency.Type)" 347 | PersistentBrowser = $Policy.SessionControls.PersistentBrowser.Mode 348 | ContinuousAccessEvaluation = $Policy.SessionControls.ContinuousAccessEvaluation.Mode 349 | ResiliantDefaults = $policy.SessionControls.DisableResilienceDefaults 350 | secureSignInSession = $policy.SessionControls.AdditionalProperties.secureSignInSession.Values 351 | } 352 | 353 | } 354 | 355 | 356 | #Export Setup 357 | Write-host "Pivoting: CA to Export Format" 358 | $pivot = @() 359 | $rowItem = New-Object PSObject 360 | $rowitem | Add-Member -type NoteProperty -Name 'CA Item' -Value "row1" 361 | $Pcount = 1 362 | foreach ($CA in $CAExport) { 363 | $rowitem | Add-Member -type NoteProperty -Name "Policy $pcount" -Value "row1" 364 | #$ca.Name 365 | $pcount += 1 366 | } 367 | $pivot += $rowItem 368 | 369 | #Add Data to Report 370 | $Rows = $CAExport | Get-Member | Where-Object { $_.MemberType -eq "NoteProperty" } 371 | $Rows | ForEach-Object { 372 | $rowItem = New-Object PSObject 373 | $rowname = $_.Name 374 | $rowitem | Add-Member -type NoteProperty -Name 'CA Item' -Value $_.Name 375 | $Pcount = 1 376 | foreach ($CA in $CAExport) { 377 | $ca | Get-Member | Where-Object { $_.MemberType -eq "NoteProperty" } | ForEach-Object { 378 | $a = $_.name 379 | $b = $ca.$a 380 | if ($a -eq $rowname) { 381 | $rowitem | Add-Member -type NoteProperty -Name "Policy $pcount" -Value $b 382 | } 383 | 384 | } 385 | # $ca.UsersInclude 386 | $pcount += 1 387 | } 388 | $pivot += $rowItem 389 | } 390 | 391 | 392 | 393 | #Set Row Order 394 | $sort = "Name", "Status", "DateModified", "Users", "UsersInclude", "UsersExclude", "Cloud apps or actions", "ApplicationsIncluded", "ApplicationsExcluded", ` 395 | "userActions", "AuthContext", "Conditions", "UserRisk", "SignInRisk", "PlatformsInclude", "PlatformsExclude", "ClientApps", "LocationsIncluded", ` 396 | "LocationsExcluded", "Devices", "DevicesIncluded", "DevicesExcluded", "DeviceFilters", "Grant Controls", "Block", "Require MFA", "Authentication Strength MFA", "CompliantDevice", ` 397 | "DomainJoinedDevice", "CompliantApplication", "ApprovedApplication", "PasswordChange", "TermsOfUse", "CustomControls", "GrantOperator", ` 398 | "Session Controls", "ApplicationEnforcedRestrictions", "CloudAppSecurity", "SignInFrequency", "PersistentBrowser", "ContinuousAccessEvaluation", "ResiliantDefaults", "secureSignInSession" 399 | 400 | #Debug 401 | #$pivot | Sort-Object $sort | Out-GridView 402 | 403 | 404 | if ($HTMLExport) { 405 | Write-host "Saving to File: HTML" 406 | $jquery = ' 407 | ' 425 | $html = " 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | $jquery " 570 | 571 | 572 | Write-host "Launching: Web Browser" 573 | $Launch = $ExportLocation + $FileName 574 | $HTML += $pivot | Where-Object { $_."CA Item" -ne 'row1' } | Sort-object { $sort.IndexOf($_."CA Item") } | convertto-html -Fragment 575 | Add-Type -AssemblyName System.Web 576 | [System.Web.HttpUtility]::HtmlDecode($HTML) | Out-File $Launch 577 | start-process $Launch 578 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CA-Export 2 | 3 | This tool is designed to help save your CA policies as an HTML format for external review or documentation. 4 | 5 | ## Run the Script using 6 | 7 | ### Export-CaPolicy.ps1 8 | 9 | This script generates a simple table of your CA policies. 10 | 11 | Run the script using: 12 | 13 | ```posh 14 | .\Export-CaPolicy.ps1 15 | ``` 16 | 17 | The script automatically connects to the Graph Module if you are not connected. If you need to change environments, you can manually disconnect using the command below. 18 | 19 | ```posh 20 | Disconnect-MgGraph 21 | ``` 22 | 23 | ### Export-CAPolicyWithRecs.ps1 24 | 25 | This script not only generates a table of your CA policies but also includes recommendations and checks for each policy. It provides detailed insights and suggestions to enhance the security of your Conditional Access policies. 26 | 27 | Run the script using: 28 | 29 | ```posh 30 | .\Export-CAPolicyWithRecs.ps1 31 | ``` 32 | 33 | The script automatically connects to the Graph Module if you are not connected. If you need to change environments, you can manually disconnect using the command below. 34 | 35 | ```posh 36 | Disconnect-MgGraph 37 | ``` 38 | 39 | #### Recommendations and Checks 40 | 41 | The recommendations script checks for the following items: 42 | 43 | 1. Legacy Authentication 44 | 2. MFA Policy targets All Users Group and All Cloud Apps 45 | 3. Mobile Device Policy requires MDM or MAM 46 | 4. Require Hybrid Join or Intune Compliance on Windows or Mac 47 | 5. Require MFA for Admins 48 | 6. Require Phish-Resistant MFA for Admins 49 | 7. Policy Excludes Same Entities It Includes 50 | 8. No Users Targeted in Policy 51 | 9. Direct User Assignment 52 | 10. Implement Risk-Based Policy 53 | 11. Block Device Code Flow 54 | 12. Require MFA to Enroll a Device in Intune 55 | 13. Block Unknown/Unsupported Devices 56 | 57 | Sample output 58 | ![CaExport-rec](CaExport-rec.png) 59 | ![CaExport-result](CaExport-result.png) 60 | 61 | ## What is new? 62 | 63 | This tool has been updated to version 2.0 to maintain support for new features as they are released. 64 | 65 | The other large change in V2.0 is updating the styling of the export so it is easier to read, and now the selects properly target the whole column. 66 | 67 | ![Select](CAExport-Select.png) 68 | 69 | ### License 70 | 71 | It's cool to share stuff to make other people's lives easier, so let's keep doing that. 72 | 73 | Shield: [![CC BY-NC-SA 4.0][cc-by-nc-sa-shield]][cc-by-nc-sa] 74 | 75 | This work is licensed under a 76 | [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License][cc-by-nc-sa]. 77 | 78 | [![CC BY-NC-SA 4.0][cc-by-nc-sa-image]][cc-by-nc-sa] 79 | 80 | [cc-by-nc-sa]: http://creativecommons.org/licenses/by-nc-sa/4.0/ 81 | [cc-by-nc-sa-image]: https://licensebuttons.net/l/by-nc-sa/4.0/88x31.png 82 | [cc-by-nc-sa-shield]: https://img.shields.io/badge/License-CC%20BY--NC--SA%204.0-lightgrey.svg 83 | --------------------------------------------------------------------------------