diff --git a/powershell/Maester.psd1 b/powershell/Maester.psd1 index 969667d8..5fc274a3 100644 --- a/powershell/Maester.psd1 +++ b/powershell/Maester.psd1 @@ -95,7 +95,7 @@ FunctionsToExport = 'Add-MtTestResultDetail', 'Clear-MtGraphCache', 'Connect-Mae 'Test-MtCaLicenseUtilization', 'Test-MtCaMfaForAdmin', 'Test-MtCaMfaForAdminManagement', 'Test-MtCaMfaForAllUsers', "Test-MtCaGroupsRestricted", - "Test-MtCaGaps", + "Test-MtCaGap", 'Test-MtCaMfaForGuest', 'Test-MtCaMfaForRiskySignIn', 'Test-MtCaRequirePasswordChangeForHighUserRisk', 'Test-MtCaSecureSecurityInfoRegistration', 'Test-MtCisaDiagnosticSettings', @@ -131,6 +131,7 @@ FunctionsToExport = 'Add-MtTestResultDetail', 'Clear-MtGraphCache', 'Connect-Mae 'Test-MtCisaSafeLinkClickTracking', 'Test-MtCisaExoAlert', 'Test-MtCisaExoAlertSiem', 'Test-MtCisaAuditLog', 'Test-MtCisaAuditLogPremium', 'Test-MtCisaAuditLogRetention', 'Get-MtExo', 'Clear-MtExoCache', + 'Test-MtCisCloudAdmin', 'Test-MtConditionalAccessWhatIf', 'Test-MtConnection', 'Test-MtEidscaControl', diff --git a/powershell/public/Test-MtCaGaps.ps1 b/powershell/public/Test-MtCaGap.ps1 similarity index 83% rename from powershell/public/Test-MtCaGaps.ps1 rename to powershell/public/Test-MtCaGap.ps1 index c67dfa6b..247a627f 100644 --- a/powershell/public/Test-MtCaGaps.ps1 +++ b/powershell/public/Test-MtCaGap.ps1 @@ -1,23 +1,19 @@ <# .Synopsis - This function checks if all objects found in policy exclusions are found in policy inclusions. + This function compares to object arrays .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 + Provides the differences in objects between two arrays of objects. .Example - Test-MtCaGaps + Get-ObjectDifference .LINK - https://maester.dev/docs/commands/Test-MtCaGaps + https://maester.dev/docs/commands/Get-ObjectDifference #> -function Get-ObjectDifferences { +function Get-ObjectDifference { [CmdletBinding()] + [OutputType([object[]])] param ( [System.Collections.ArrayList]$excludedObjects, [System.Collections.ArrayList]$includedObjects @@ -38,8 +34,22 @@ function Get-ObjectDifferences { return $objectDifferences } -function Get-RalatedPolicies { +<# +.Synopsis + Provides MarkDown text for specific array of objects + +.Description + Returns a structured MarkDown string resolving objects + +.Example + Get-RelatedPolicy + +.LINK + https://maester.dev/docs/commands/Get-RelatedPolicy +#> +function Get-RelatedPolicy { [CmdletBinding()] + [OutputType([string])] param ( [System.Collections.ArrayList]$Arr, [String]$ObjName @@ -57,7 +67,22 @@ function Get-RalatedPolicies { return $result } -function Test-MtCaGaps { +<# +.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. + +.Example + Test-MtCaGap + +.LINK + https://maester.dev/docs/commands/Test-MtCaGap +#> +function Test-MtCaGap { [CmdletBinding()] [OutputType([bool])] param () @@ -138,14 +163,14 @@ function Test-MtCaGaps { 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." + [System.Collections.ArrayList]$differencesUsers = @(Get-ObjectDifference -excludedObjects $excludedUsers -includedObjects $includedUsers) + [System.Collections.ArrayList]$differencesGroups = @(Get-ObjectDifference -excludedObjects $excludedGroups -includedObjects $includedGroups) + [System.Collections.ArrayList]$differencesRoles = @(Get-ObjectDifference -excludedObjects $excludedRoles -includedObjects $includedRoles) + [System.Collections.ArrayList]$differencesApplications = @(Get-ObjectDifference -excludedObjects $excludedApplications -includedObjects $includedApplications) + [System.Collections.ArrayList]$differencesServicePrincipals = @(Get-ObjectDifference -excludedObjects $excludedServicePrincipals -includedObjects $includedServicePrincipals) + [System.Collections.ArrayList]$differencesLocations = @(Get-ObjectDifference -excludedObjects $excludedLocations -includedObjects $includedLocations) + [System.Collections.ArrayList]$differencesPlatforms = @(Get-ObjectDifference -excludedObjects $excludedPlatforms -includedObjects $includedPlatforms) + Write-Verbose "Finished searching for gaps in policies." # Check if all excluded objects have fallbacks if ( @@ -167,7 +192,7 @@ function Test-MtCaGaps { $testResult = "The following user objects did not have a fallback:`n`n" $differencesUsers | ForEach-Object { $testResult += " - $_`n`n" - $testResult += Get-RalatedPolicies -Arr $mappingArray -ObjName $_ + $testResult += Get-RelatedPolicy -Arr $mappingArray -ObjName $_ } } # Add group objects to results @@ -175,7 +200,7 @@ function Test-MtCaGaps { $testResult += "The following group objects did not have a fallback:`n`n" $differencesGroups | ForEach-Object { $testResult += " - $_`n`n" - $testResult += Get-RalatedPolicies -Arr $mappingArray -ObjName $_ + $testResult += Get-RelatedPolicy -Arr $mappingArray -ObjName $_ } } # Add role objects to results @@ -183,7 +208,7 @@ function Test-MtCaGaps { $testResult += "The following role objects did not have a fallback:`n`n" $differencesRoles | ForEach-Object { $testResult += " - $_`n`n" - $testResult += Get-RalatedPolicies -Arr $mappingArray -ObjName $_ + $testResult += Get-RelatedPolicy -Arr $mappingArray -ObjName $_ } } # Add application objects to results @@ -191,7 +216,7 @@ function Test-MtCaGaps { $testResult += "The following application objects did not have a fallback:`n`n" $differencesApplications | ForEach-Object { $testResult += " - $_`n`n" - $testResult += Get-RalatedPolicies -Arr $mappingArray -ObjName $_ + $testResult += Get-RelatedPolicy -Arr $mappingArray -ObjName $_ } } # Add service principal objects to results @@ -199,7 +224,7 @@ function Test-MtCaGaps { $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 $_ + $testResult += Get-RelatedPolicy -Arr $mappingArray -ObjName $_ } } # Add location objects to results @@ -207,7 +232,7 @@ function Test-MtCaGaps { $testResult += "The following location objects did not have a fallback:`n`n" $differencesLocations | ForEach-Object { $testResult += " - $_`n`n" - $testResult += Get-RalatedPolicies -Arr $mappingArray -ObjName $_ + $testResult += Get-RelatedPolicy -Arr $mappingArray -ObjName $_ } } # Add platform objects to results @@ -215,7 +240,7 @@ function Test-MtCaGaps { $testResult += "The following platform objects did not have a fallback:`n`n" $differencesPlatforms | ForEach-Object { $testResult += " - $_`n`n" - $testResult += Get-RalatedPolicies -Arr $mappingArray -ObjName $_ + $testResult += Get-RelatedPolicy -Arr $mappingArray -ObjName $_ } } } diff --git a/powershell/public/cis/Test-MtCisCloudAdmin.md b/powershell/public/cis/Test-MtCisCloudAdmin.md new file mode 100644 index 00000000..e60b5853 --- /dev/null +++ b/powershell/public/cis/Test-MtCisCloudAdmin.md @@ -0,0 +1,26 @@ +1.1.1 (L1) Ensure Administrative accounts are separate and cloud-only + +Administrative accounts are special privileged accounts that could have varying levels of access to data, users, and settings. Regular user accounts should never be utilized for administrative tasks and care should be taken, in the case of a hybrid environment, to keep Administrative accounts separated from on-prem accounts. Administrative accounts should not have applications assigned so that they have no access to potentially vulnerable services (EX. email, Teams, SharePoint, etc.) and only access to perform tasks as needed for administrative purposes. + +#### Remediation action: + +To created licensed, separate Administrative accounts for Administrative users: + +1. Navigate to **Microsoft 365 admin center**. +2. Click to expand **Users** select **Active users** +3. Click **Add a user**. +4. Fill out the appropriate fields for Name, user, etc. +5. When prompted to assign licenses select as needed **Microsoft Entra ID P1** or +**Microsoft Entra ID P2**, then click **Next**. +6. Under the **Option settings** screen you may choose from several types of +Administrative access roles. Choose **Admin center access** followed by the +appropriate role then click **Next**. +7. Select **Finish adding**. + +#### Related links + +* [Microsoft 365 Admin Center](https://admin.microsoft.com) +* [CIS Microsoft 365 Foundations Benchmark v3.1.0 - Page 16](https://www.cisecurity.org/benchmark/microsoft_365) + + +%TestResult% \ No newline at end of file diff --git a/powershell/public/cis/Test-MtCisCloudAdmin.ps1 b/powershell/public/cis/Test-MtCisCloudAdmin.ps1 new file mode 100644 index 00000000..3de3ef88 --- /dev/null +++ b/powershell/public/cis/Test-MtCisCloudAdmin.ps1 @@ -0,0 +1,78 @@ +<# +.SYNOPSIS + Checks if Global Admins are cloud users + +.DESCRIPTION + Ensure Administrative accounts are separate and cloud-only + CIS Microsoft 365 Foundations Benchmark v3.1.0 + +.EXAMPLE + Test-MtCisCloudAdmin + + Returns true if no global admins are hybrid sync + +.LINK + https://maester.dev/docs/commands/Test-MtCisCloudAdmin +#> +function Test-MtCisCloudAdmin { + [CmdletBinding()] + [OutputType([bool])] + param() + + Write-Verbose "Getting Global Admin role" + $role = Get-MtRole | Where-Object {` + $_.id -eq "62e90394-69f5-4237-9190-012177145e10" } # Global Administrator + + Write-Verbose "Getting role members" + $assignments = Get-MtRoleMember -roleId $role.id + + Write-Verbose "Filtering for users" + $globalAdministrators = $assignments | Where-Object {` + $_.'@odata.type' -eq "#microsoft.graph.user" + } + + $userIds = @($globalAdministrators.Id) + + Write-Verbose "Requesting users onPremisesSyncEnabled property" + $users = Invoke-MtGraphRequest -RelativeUri "users" -UniqueId $userIds -Select id,displayName,onPremisesSyncEnabled + + Write-Verbose "Filtering users for onPremisesSyncEnabled" + $result = $users | Where-Object {` + $_.onPremisesSyncEnabled -eq $true + } + + $testResult = ($result|Measure-Object).Count -eq 0 + + $sortSplat = @{ + Property = @( + @{ + Expression = "onPremisesSyncEnabled" + Descending = $true + }, + @{ + Expression = "displayName" + } + ) + } + + if ($testResult) { + $testResultMarkdown = "Well done. Your tenant has no hybrid Global Administrators:`n`n%TestResult%" + } else { + $testResultMarkdown = "Your tenant has 1 or more hybrid Global Administrators:`n`n%TestResult%" + } + + $resultMd = "| Display Name | Cloud Only |`n" + $resultMd += "| --- | --- |`n" + foreach($item in $users | Sort-Object @sortSplat){ + $itemResult = "❌ Fail" + if($item.id -notin $result.id){ + $itemResult = "✅ Pass" + } + $resultMd += "| $($item.displayName) | $($itemResult) |`n" + } + $testResultMarkdown = $testResultMarkdown -replace "%TestResult%", $resultMd + + Add-MtTestResultDetail -Result $testResultMarkdown + + return $testResult +} \ No newline at end of file diff --git a/tests/Maester/Entra/Test-ConditionalAccessBaseline.Tests.ps1 b/tests/Maester/Entra/Test-ConditionalAccessBaseline.Tests.ps1 index b2f98816..028d7b63 100644 --- a/tests/Maester/Entra/Test-ConditionalAccessBaseline.Tests.ps1 +++ b/tests/Maester/Entra/Test-ConditionalAccessBaseline.Tests.ps1 @@ -60,7 +60,7 @@ 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." + Test-MtCaGap | 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" { diff --git a/tests/cis/Test-MtCisCloudAdmin.Tests.ps1 b/tests/cis/Test-MtCisCloudAdmin.Tests.ps1 new file mode 100644 index 00000000..2d09286b --- /dev/null +++ b/tests/cis/Test-MtCisCloudAdmin.Tests.ps1 @@ -0,0 +1,10 @@ +Describe "CIS" -Tag "CIS 1.1.1", "CIS E3 Level 1", "CIS E3", "CIS", "Security", "All", "CIS M365 v3.1.0" { + It "CIS 1.1.1: Ensure Administrative accounts are separate and cloud-only" { + + $result = Test-MtCisCloudAdmin + + if($null -ne $result) { + $result | Should -Be $true -Because "admin accounts are separate and cloud-only" + } + } +} \ No newline at end of file diff --git a/website/docs/tests/cis/readme.md b/website/docs/tests/cis/readme.md new file mode 100644 index 00000000..267e1bc6 --- /dev/null +++ b/website/docs/tests/cis/readme.md @@ -0,0 +1,26 @@ +--- +id: overview +title: CIS Microsoft 365 Foundations Benchmark Tests +sidebar_label: 🏢 CIS Overview +description: Implementation of CIS Microsoft 365 Foundations Benchmark Controls +--- + +# CIS Microsoft 365 Foundations Benchmark + +## Overview + +The tests in this section verifies that a Micorosft 365 tenant's configuration conforms to the [CIS Microsoft 365 Foundations Benchmark](https://www.cisecurity.org/benchmark/microsoft_365) recommendations (v3.1.0). + +The CIS published material is shared for these tests as it aligns with their licensing of [CC BY-NC-SA 4.0](https://www.cisecurity.org/terms-and-conditions-table-of-contents). + +## Connecting to Azure, Exchange and other services + +In order to run all the CIS tests, you need to install and connect to the Azure and Exchange Online modules. + +See the [Installation guide](/docs/installation#optional-modules-and-permissions) for more information. + +## Tests + +| Cmdlet Name | CIS Recommendation ID | +| - | - | +| Test-MtCisCloudAdmin | CIS 1.1.1: Ensure Administrative accounts are separate and cloud-only | \ No newline at end of file