Skip to content

Commit

Permalink
Merge pull request #416 from RobbeVandenDaele/main
Browse files Browse the repository at this point in the history
Proposal to add conditional access gap test
  • Loading branch information
merill authored Aug 21, 2024
2 parents db089a2 + 77bfb8c commit 3ff6457
Show file tree
Hide file tree
Showing 4 changed files with 245 additions and 0 deletions.
1 change: 1 addition & 0 deletions powershell/Maester.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ FunctionsToExport = 'Add-MtTestResultDetail', 'Clear-MtGraphCache', 'Connect-Mae
'Test-MtCaLicenseUtilization', 'Test-MtCaMfaForAdmin',
'Test-MtCaMfaForAdminManagement', 'Test-MtCaMfaForAllUsers',
"Test-MtCaGroupsRestricted",
"Test-MtCaGaps",
'Test-MtCaMfaForGuest', 'Test-MtCaMfaForRiskySignIn',
'Test-MtCaRequirePasswordChangeForHighUserRisk',
'Test-MtCaSecureSecurityInfoRegistration', 'Test-MtCisaDiagnosticSettings',
Expand Down
225 changes: 225 additions & 0 deletions powershell/public/Test-MtCaGaps.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
<#
.Synopsis
This function checks if all objects found in policy exclusions are found in policy inclusions.
.Description
Checks for gaps in conditional access policies, by looking for excluded objects which are not specifically inlcuded
in another conditional access policy. Instead of looking at the historical sign-ins to find gaps, we try to spot possibly
overlooked exclusions which do not have a fallback.
Reference:
https://learn.microsoft.com/en-us/entra/identity/monitoring-health/workbook-conditional-access-gap-analyzer
.Example
Test-MtCaGaps
.LINK
https://maester.dev/docs/commands/Test-MtCaGaps
#>
function Get-ObjectDifferences {
[CmdletBinding()]
param (
[System.Collections.ArrayList]$excludedObjects,
[System.Collections.ArrayList]$includedObjects
)

# Only get unique values
$excludedObjects = @($excludedObjects | Select-Object -Unique)
$includedObjects = @($includedObjects | Select-Object -Unique)
# Get all the objects that are excluded somewhere but included somewhere else
$excludedObjectsWithFallback = $excludedObjects | Where-Object {
$includedObjects -contains $_
}
# Get the differences between the two Arrays, so we can find which objects did not have a fallback
$objectDifferences = @($excludedObjects | Where-Object {
$excludedObjectsWithFallback -notcontains $_
})

return $objectDifferences
}

function Get-RalatedPolicies {
[CmdletBinding()]
param (
[System.Collections.ArrayList]$Arr,
[String]$ObjName
)
$result = ""

# Check each policy in the array
foreach ($obj in $Arr) {
# Check if the excluded object is present in the policy
if ($obj.ExcludedObjects -contains $ObjName) {
$result += " - Excluded in policy '$($obj.PolicyName)'`n`n"
}
}

return $result
}

function Test-MtCaGaps {
[CmdletBinding()]
[OutputType([bool])]
param ()

$result = $false
$testDescription = "All excluded objects should have a fallback include in another policy"

# Get the enabled conditional access policies
$policies = Get-MtConditionalAccessPolicy | Where-Object { $_.state -eq "enabled" }
Write-Verbose "Retrieved conditional access policies:`n $policies"

# Variabes related to users
[System.Collections.ArrayList]$excludedUsers = @()
[System.Collections.ArrayList]$includedUsers = @()
[System.Collections.ArrayList]$differencesUsers = @()
# Variabes related to groups
[System.Collections.ArrayList]$excludedGroups = @()
[System.Collections.ArrayList]$includedGroups = @()
[System.Collections.ArrayList]$differencesGroups = @()
# Variabes related to Roles
[System.Collections.ArrayList]$excludedRoles = @()
[System.Collections.ArrayList]$includedRoles = @()
[System.Collections.ArrayList]$differencesRoles = @()
# Variabes related to Applications
[System.Collections.ArrayList]$excludedApplications = @()
[System.Collections.ArrayList]$includedApplications = @()
[System.Collections.ArrayList]$differencesApplications = @()
# Variabes related to ServicePrincipals
[System.Collections.ArrayList]$excludedServicePrincipals = @()
[System.Collections.ArrayList]$includedServicePrincipals = @()
[System.Collections.ArrayList]$differencesServicePrincipals = @()
# Variabes related to Locations
[System.Collections.ArrayList]$excludedLocations = @()
[System.Collections.ArrayList]$includedLocations = @()
[System.Collections.ArrayList]$differencesLocations = @()
# Variabes related to Platforms
[System.Collections.ArrayList]$excludedPlatforms = @()
[System.Collections.ArrayList]$includedPlatforms = @()
[System.Collections.ArrayList]$differencesPlatforms = @()
# Mapping array
[System.Collections.ArrayList]$mappingArray = @()

# Get all the objects for all policies
$policies | ForEach-Object {
# Save all interesting objects for later use
$_.Conditions.Users.ExcludeUsers | ForEach-Object { $excludedUsers.Add($_) | Out-Null }
$_.Conditions.Users.IncludeUsers | ForEach-Object { $includedUsers.Add($_) | Out-Null }
$_.Conditions.Users.ExcludeGroups | ForEach-Object { $excludedGroups.Add($_) | Out-Null }
$_.Conditions.Users.IncludeGroups | ForEach-Object { $includedGroups.Add($_) | Out-Null }
$_.Conditions.Users.ExcludeRoles | ForEach-Object { $excludedRoles.Add($_) | Out-Null }
$_.Conditions.Users.IncludeRoles | ForEach-Object { $includedRoles.Add($_) | Out-Null }
$_.Conditions.Applications.ExcludeApplications | ForEach-Object { $excludedApplications.Add($_) | Out-Null }
$_.Conditions.Applications.IncludeApplications | ForEach-Object { $includedApplications.Add($_) | Out-Null }
$_.Conditions.ClientApplications.ExcludeServicePrincipals | ForEach-Object { $excludedServicePrincipals.Add($_) | Out-Null }
$_.Conditions.ClientApplications.IncludeServicePrincipals | ForEach-Object { $includedServicePrincipals.Add($_) | Out-Null }
$_.Conditions.Locations.ExcludeLocations | ForEach-Object { $excludedLocations.Add($_) | Out-Null }
$_.Conditions.Locations.IncludeLocations | ForEach-Object { $includedLocations.Add($_) | Out-Null }
$_.Conditions.Locations.Platforms | ForEach-Object { $excludedPlatforms.Add($_) | Out-Null }
$_.Conditions.Locations.Platforms | ForEach-Object { $includedPlatforms.Add($_) | Out-Null }

# Create a mapping for each policy with excluded objects
[System.Collections.ArrayList]$allExcluded = $_.Conditions.Users.ExcludeUsers + `
$_.Conditions.Users.ExcludeGroups + `
$_.Conditions.Users.ExcludeRoles + `
$_.Conditions.Applications.ExcludeApplications + `
$_.Conditions.ClientApplications.ExcludeServicePrincipals + `
$_.Conditions.Locations.ExcludeLocations + `
$_.Conditions.Locations.Platforms
# Create the mapping
$mapping = [PSCustomObject]@{
PolicyName = $_.DisplayName
ExcludedObjects = $allExcluded
}
# Add the mapping to the array and clear variable
$mappingArray += $mapping
Clear-Variable -Name allExcluded
}
Write-Verbose "Created a mapping with all excluded objects for each policy:`n $mapping"

# Find which objects are excluded without a fallback
[System.Collections.ArrayList]$differencesUsers = @(Get-ObjectDifferences -excludedObjects $excludedUsers -includedObjects $includedUsers)
[System.Collections.ArrayList]$differencesGroups = @(Get-ObjectDifferences -excludedObjects $excludedGroups -includedObjects $includedGroups)
[System.Collections.ArrayList]$differencesRoles = @(Get-ObjectDifferences -excludedObjects $excludedRoles -includedObjects $includedRoles)
[System.Collections.ArrayList]$differencesApplications = @(Get-ObjectDifferences -excludedObjects $excludedApplications -includedObjects $includedApplications)
[System.Collections.ArrayList]$differencesServicePrincipals = @(Get-ObjectDifferences -excludedObjects $excludedServicePrincipals -includedObjects $includedServicePrincipals)
[System.Collections.ArrayList]$differencesLocations = @(Get-ObjectDifferences -excludedObjects $excludedLocations -includedObjects $includedLocations)
[System.Collections.ArrayList]$differencesPlatforms = @(Get-ObjectDifferences -excludedObjects $excludedPlatforms -includedObjects $includedPlatforms)
Write-Host "Finished searching for gaps in policies."

# Check if all excluded objects have fallbacks
if (
$differencesUsers.Count -eq 0 `
-and $differencesGroups.Count -eq 0 `
-and $differencesRoles.Count -eq 0 `
-and $differencesApplications.Count -eq 0 `
-and $differencesServicePrincipals.Count -eq 0 `
-and $differencesLocations.Count -eq 0 `
-and $differencesPlatforms.Count -eq 0 `
) {
$result = $true
$testResult = "All excluded objects seem to have a fallback in other policies."
Write-Verbose "All excluded objects seem to have a fallback in other policies."
} else {
Write-Verbose "Not all excluded objects seem to have a fallback in other policies."
# Add user objects to results
if ($differencesUsers.Count -ne 0) {
$testResult = "The following user objects did not have a fallback:`n`n"
$differencesUsers | ForEach-Object {
$testResult += " - $_`n`n"
$testResult += Get-RalatedPolicies -Arr $mappingArray -ObjName $_
}
}
# Add group objects to results
if ($differencesGroups.Count -ne 0) {
$testResult += "The following group objects did not have a fallback:`n`n"
$differencesGroups | ForEach-Object {
$testResult += " - $_`n`n"
$testResult += Get-RalatedPolicies -Arr $mappingArray -ObjName $_
}
}
# Add role objects to results
if ($differencesRoles.Count -ne 0) {
$testResult += "The following role objects did not have a fallback:`n`n"
$differencesRoles | ForEach-Object {
$testResult += " - $_`n`n"
$testResult += Get-RalatedPolicies -Arr $mappingArray -ObjName $_
}
}
# Add application objects to results
if ($differencesApplications.Count -ne 0) {
$testResult += "The following application objects did not have a fallback:`n`n"
$differencesApplications | ForEach-Object {
$testResult += " - $_`n`n"
$testResult += Get-RalatedPolicies -Arr $mappingArray -ObjName $_
}
}
# Add service principal objects to results
if ($differencesServicePrincipals.Count -ne 0) {
$testResult += "The following service principal objects did not have a fallback:`n`n"
$differencesServicePrincipals | ForEach-Object {
$testResult += " - $_`n`n"
$testResult += Get-RalatedPolicies -Arr $mappingArray -ObjName $_
}
}
# Add location objects to results
if ($differencesLocations.Count -ne 0) {
$testResult += "The following location objects did not have a fallback:`n`n"
$differencesLocations | ForEach-Object {
$testResult += " - $_`n`n"
$testResult += Get-RalatedPolicies -Arr $mappingArray -ObjName $_
}
}
# Add platform objects to results
if ($differencesPlatforms.Count -ne 0) {
$testResult += "The following platform objects did not have a fallback:`n`n"
$differencesPlatforms | ForEach-Object {
$testResult += " - $_`n`n"
$testResult += Get-RalatedPolicies -Arr $mappingArray -ObjName $_
}
}
}
Add-MtTestResultDetail -Description $testDescription -Result $testResult

return $result
}
3 changes: 3 additions & 0 deletions tests/Maester/Entra/Test-ConditionalAccessBaseline.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@
It "MT.1035: All security groups assigned to Conditional Access Policies should be protected by RMAU. See https://maester.dev/docs/tests/MT.1035" -Tag "MT.1035" {
Test-MtCaGroupsRestricted | Should -Be $true -Because "there is one or more policy without protection of included or excluded groups"
}
It "MT.1036: All excluded objects should have a fallback include in another policy. See https://maester.dev/docs/tests/MT.1036" -Tag "MT.1036", "Warning" {
Test-MtCaGaps | Should -Be $true -Because "there is one ore more object excluded without an include fallback in another policy."
}
Context "License utilization" {
It "MT.1022: All users utilizing a P1 license should be licensed. See https://maester.dev/docs/tests/MT.1022" -Tag "MT.1022" {
$LicenseReport = Test-MtCaLicenseUtilization -License "P1"
Expand Down
16 changes: 16 additions & 0 deletions website/docs/tests/maester/MT.1036.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
title: MT.1036 - All excluded objects should have a fallback include in another policy.
description: Checks for gaps in conditional access policies, by looking for excluded objects which are not specifically inlcuded in another conditional access policy. This way we try to spot possibly overlooked exclusions which do not have a fallback.
slug: /tests/MT.1036
sidebar_class_name: hidden
---

# All excluded objects should have a fallback include in another policy

## Description

When exlcuding objects from consitional access policies, you are basically creating gaps into your protection mechanisms. To make sure these gaps are not left open, the excluded objects should be included in another 'fallback' policy. By doing this, you make sure al objects are protected and no gaps exists.

## How to fix

Review the excluded objects and their related policies and try to create a fallback policy for them.

0 comments on commit 3ff6457

Please sign in to comment.