diff --git a/maester b/maester new file mode 160000 index 000000000..7ee2cdc63 --- /dev/null +++ b/maester @@ -0,0 +1 @@ +Subproject commit 7ee2cdc636877992b189894effe0b18547d47ecb diff --git a/powershell/Maester.psd1 b/powershell/Maester.psd1 index c23f7ce47..a6100e513 100644 --- a/powershell/Maester.psd1 +++ b/powershell/Maester.psd1 @@ -83,6 +83,7 @@ FunctionsToExport = 'Add-MtTestResultDetail', 'Clear-MtGraphCache', 'Connect-Mae 'Get-MtUser', 'Get-MtRole', 'Get-MtSession', 'Install-MaesterTests', 'Invoke-Maester', 'Invoke-MtGraphRequest', + 'Invoke-MtAzureRequest', 'Send-MtMail', 'Send-MtTeamsMessage', 'Test-MtAppManagementPolicyEnabled', 'Test-MtCaAllAppsExists', 'Test-MtCaApplicationEnforcedRestriction', 'Test-MtCaBlockLegacyExchangeActiveSyncAuthentication', @@ -168,6 +169,7 @@ FunctionsToExport = 'Add-MtTestResultDetail', 'Clear-MtGraphCache', 'Connect-Mae 'Resolve-SpfRecord', 'Clear-MtDnsCache', 'Test-MtTeamsRestrictParticipantGiveRequestControl', 'Test-MtHighRiskAppPermissions', + 'Test-MtUserAccessAdmin', 'Test-ORCA100', 'Test-ORCA101', 'Test-ORCA102', 'Test-ORCA103', 'Test-ORCA104', 'Test-ORCA105', 'Test-ORCA106', diff --git a/powershell/internal/Get-GraphObjectMarkdown.ps1 b/powershell/internal/Get-GraphObjectMarkdown.ps1 index 1bc3fa716..edb774283 100644 --- a/powershell/internal/Get-GraphObjectMarkdown.ps1 +++ b/powershell/internal/Get-GraphObjectMarkdown.ps1 @@ -21,7 +21,8 @@ function Get-GraphObjectMarkdown { [Object[]] $GraphObjects, # The type of graph object, this will be used to show the right deeplink to the test results report. - [Parameter(Mandatory = $true)] + # If not specified, the function will try to determine the type based on the object. + [Parameter(Mandatory = $false)] [ValidateSet('AuthenticationMethod', 'AuthorizationPolicy', 'ConditionalAccess', 'ConsentPolicy', 'Devices', 'Domains', 'Groups', 'IdentityProtection', 'Users', 'UserRole' )] @@ -44,14 +45,38 @@ function Get-GraphObjectMarkdown { UserRole = "https://entra.microsoft.com/#view/Microsoft_AAD_UsersAndTenants/UserProfileMenuBlade/~/AdministrativeRole/userId/{0}" } + $graphObjectTypeMapping = @{ + '#microsoft.graph.user' = 'Users' + '#microsoft.graph.group' = 'Groups' + '#microsoft.graph.device' = 'Devices' + } + # This will work for now, will need to add switch as we add support for complex urls like Applications blade, etc.. $result = "" foreach ($item in $GraphObjects) { - $link = $markdownLinkTemplate[$GraphObjectType] -f $item.id + $displayName = Get-ObjectProperty $item 'displayName' + $currentGraphObjectType = $GraphObjectType + if (-not $currentGraphObjectType) { + $dataType = Get-ObjectProperty $item '@odata.type' + if ($graphObjectTypeMapping.ContainsKey($dataType)) { + $currentGraphObjectType = $graphObjectTypeMapping[$dataType] + } else { + # Unknown type + $displayName = "$displayName ($dataType - $($item.id))" + } + } + + if($currentGraphObjectType) { + $link = $markdownLinkTemplate[$currentGraphObjectType] -f $item.id + } + else { + $link = "#" + } + if ($AsPlainTextLink) { - $result += "[$($item.displayName)]($link)" + $result += "[$displayName]($link)" } else { - $result += " - [$($item.displayName)]($link)`n" + $result += " - [$displayName]($link)`n" } } diff --git a/powershell/internal/Get-MtDirectoryObjects.ps1 b/powershell/internal/Get-MtDirectoryObjects.ps1 new file mode 100644 index 000000000..938e18f4b --- /dev/null +++ b/powershell/internal/Get-MtDirectoryObjects.ps1 @@ -0,0 +1,33 @@ +<# + .SYNOPSIS + Get directory objects by their object id's. + .DESCRIPTION + This function retrieves directory objects from Microsoft Graph by their object id's. + It is a wrapper around the Microsoft Graph API endpoint "directoryObjects/getByIds". + The function takes an array of object id's as input and returns the corresponding directory objects. +#> + +function Get-MtDirectoryObjects { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'This command updates multiple tests')] + [CmdletBinding()] + param( + # The object id's of the directory objects to retrieve. + [Parameter(Mandatory = $true)] + [string[]] $ObjectId, + + # If true, returns the objects as markdown. + [switch] $AsMarkdown = $false + ) + + $postBody = @{ + ids = $ObjectId + } | ConvertTo-Json + + $graphUrl = 'beta/directoryObjects/getByIds?$select=id,displayName' + $result = Invoke-MgGraphRequest -Uri $graphUrl -Method POST -Body $postBody -OutputType PSObject + $values = Get-ObjectProperty $result 'value' + if($AsMarkdown) { + $values = Get-GraphObjectMarkdown -GraphObjects $values + } + return $values +} \ No newline at end of file diff --git a/powershell/public/core/Invoke-MtAzureRequest.ps1 b/powershell/public/core/Invoke-MtAzureRequest.ps1 new file mode 100644 index 000000000..13967402b --- /dev/null +++ b/powershell/public/core/Invoke-MtAzureRequest.ps1 @@ -0,0 +1,47 @@ +<# + .SYNOPSIS + Invoke a REST API request to the Azure Management API. + + .DESCRIPTION + This function allows you to make REST API requests to the Azure Management API. + It is a wrapper around the Invoke-AzRest function, providing a simplified interface. + + .EXAMPLE + Invoke-MtAzureRequest -RelativeUri 'subscriptions' + + + .LINK + https://maester.dev/docs/commands/Invoke-MtAzureRequest +#> + +function Invoke-MtAzureRequest { + [CmdletBinding()] + param( + # Graph endpoint such as "users". + [Parameter(Mandatory = $true)] + [string[]] $RelativeUri, + + # The HTTP method to use. Default is GET. + [Parameter(Mandatory = $false)] + [ValidateSet("GET")] + [string] $Method = "GET", + + # The API version to use. Default is 2024-11-01 + [Parameter(Mandatory = $false)] + $ApiVersion = '2024-11-01', + + # The filter to use. + [Parameter(Mandatory = $false)] + [string] $Filter + ) + + $resourceUrl = (Get-AzContext).Environment.ResourceManagerUrl + $restApi = "$($resourceUrl)$($RelativeUri)?api-version=$($ApiVersion)" + if ($Filter) { + $restApi += '&$Filter=' + $Filter + } + + Write-Verbose "Invoke-AzRest $restApi" + $result = Invoke-AzRest -Method $Method -Uri $restApi + return $result.Content | ConvertFrom-Json +} \ No newline at end of file diff --git a/powershell/public/maester/azure/Test-MtUserAccessAdmin.md b/powershell/public/maester/azure/Test-MtUserAccessAdmin.md new file mode 100644 index 000000000..88d21ed20 --- /dev/null +++ b/powershell/public/maester/azure/Test-MtUserAccessAdmin.md @@ -0,0 +1,28 @@ +Ensure that no person has permanent access to Azure Subscriptions. + +User Access Administrator is a role that allows an Administrator to perform everything on an Azure Subscription. Global Administrators can gain this permission on the Root Scope in Entra ID, in the properties of the Entra ID tenant. These permissions should only be used in case of emergency and should not be assigned permanently. + +Ensure that no User Access Administrator permissions at the Root Scope are applied. + +#### Remediation action: + +To remove all Admins with Root Scope permissions, as a Global Admin: +1. Navigate to Microsoft Azure Portal [https://portal.azure.com](https://portal.azure.com). +2. Search for **Microsoft Entra ID** and select **Microsoft Entra ID**. +3. Expand the **Manage** menu and select **Properties**. +3. On the **Properties** page, go to the **Access management for Azure resources** section. +4. In the information bar, click **Manage elevated access users**. +5. Select all User Access Administrators and click **Remove**. + +To remove the admins through CLI: +```powershell +az role assignment delete --role "User Access Administrator" --assignee adminname@yourdomain.com --scope "/" +``` + +#### Related links + +* [Manage who can create Microsoft 365 Groups](https://learn.microsoft.com/en-us/microsoft-365/solutions/manage-creation-of-groups?view=o365-worldwide) + + + +%TestResult% \ No newline at end of file diff --git a/powershell/public/maester/azure/Test-MtUserAccessAdmin.ps1 b/powershell/public/maester/azure/Test-MtUserAccessAdmin.ps1 new file mode 100644 index 000000000..1a2364763 --- /dev/null +++ b/powershell/public/maester/azure/Test-MtUserAccessAdmin.ps1 @@ -0,0 +1,54 @@ +<# +.SYNOPSIS + Checks if any Global Admins have User Access Control permissions at the Root Scope + +.DESCRIPTION + Ensure that no one has permanent access to all subscriptions through the Root Scope. + +.EXAMPLE + Test-MtUserAccessAdmin + + Returns true if no User Access Control permissions are assigned at the root scope + +.LINK + https://maester.dev/docs/commands/Test-MtUserAccessAdmin +#> +function Test-MtUserAccessAdmin { + [CmdletBinding()] + [OutputType([bool])] + param() + + Write-Verbose "Checking if connected to Graph" + if (!(Test-MtConnection Graph)) { + Add-MtTestResultDetail -SkippedBecause NotConnectedGraph + return $null + } + + if(!(Test-MtConnection Azure)){ + Add-MtTestResultDetail -SkippedBecause NotConnectedAzure + return $null + } + + Write-Verbose "Getting all User Access Administrators at Root Scope" + + $userAccessResult = Invoke-MtAzureRequest -RelativeUri 'providers/Microsoft.Authorization/roleAssignments' -Filter 'atScope()' -ApiVersion '2022-04-01' + $userAccessAdmins = Get-ObjectProperty $userAccessResult 'value' + + # Get the count of role assignments + $roleAssignmentCount = $userAccessAdmins | Measure-Object | Select-Object -ExpandProperty Count + + $testResult = $roleAssignmentCount -eq 0 + + if ($testResult) { + $testResultMarkdown = "Well done. Your tenant has no User Access Administrators." + } + else { + $testResultMarkdown = "Your tenant has $roleAssignmentCount resource(s) with access to manage access to all Azure subscriptions and management groups in this tenant.`n`n" + + $testResultMarkdown += Get-MtDirectoryObjects $userAccessAdmins.properties.principalId -AsMarkdown + } + + Add-MtTestResultDetail -Result $testResultMarkdown + + return $testResult +} diff --git a/tests/Maester/Azure/UserAccessAdmin.Tests.ps1 b/tests/Maester/Azure/UserAccessAdmin.Tests.ps1 new file mode 100644 index 000000000..23d063090 --- /dev/null +++ b/tests/Maester/Azure/UserAccessAdmin.Tests.ps1 @@ -0,0 +1,7 @@ +Describe "AzureConfig" -Tag "Privilege", "Azure" { + It "MT. Check 'User Access Administrators' at root scope" { + + $result = Test-MtUserAccessAdmin + + $result | Should -Be $true -Because "No User Access Administrators at root scope"} +} \ No newline at end of file diff --git a/website/docs/sections/create-entra-app.md b/website/docs/sections/create-entra-app.md index 42e5a43f3..32852ccee 100644 --- a/website/docs/sections/create-entra-app.md +++ b/website/docs/sections/create-entra-app.md @@ -49,10 +49,10 @@ The Azure Role Based Access Control (RBAC) implementation utilizes Uniform Resou > The Azure RBAC permissions are necessary to support tests that validate [Azure configurations](https://maester.dev/docs/installation#installing-azure-and-exchange-online-modules), such as the [CISA tests](https://maester.dev/docs/tests/cisa/entra#:~:text=Test%2DMtCisaDiagnosticSettings). The following PowerShell script will enable you, with a Global Administrator role assignment, to: -- Identify the Service Principal Object ID that will be authorized as a Reader and the Subscription ID to authorize for +- Identify the Service Principal Object ID that will be authorized as a Reader (Enterprise app Object ID) - Install the necessary Az module and prompt for connection - Elevate your account access to the root scope -- Create a role assignment for Reader access over the Subscription and objects within +- Create a role assignment for Reader access over the Root Scope - Create a role assignment for Reader access over the Entra ID (i.e., [aadiam provider](https://learn.microsoft.com/en-us/azure/role-based-access-control/permissions/identity#microsoftaadiam)) - Identify the role assignment authorizing your account access to the root scope - Delete the root scope role assignment for your account @@ -65,8 +65,11 @@ Install-Module Az.Resources -Force Connect-AzAccount #Elevate to root scope access $elevateAccess = Invoke-AzRestMethod -Path "/providers/Microsoft.Authorization/elevateAccess?api-version=2015-07-01" -Method POST -New-AzRoleAssignment -ObjectId $servicePrincipal -Scope "/subscriptions/$subscription" -RoleDefinitionName "Reader" -ObjectType "ServicePrincipal" + +#Assign permissions to Enterprise App +New-AzRoleAssignment -ObjectId $servicePrincipal -Scope "/" -RoleDefinitionName "Reader" -ObjectType "ServicePrincipal" New-AzRoleAssignment -ObjectId $servicePrincipal -Scope "/providers/Microsoft.aadiam" -RoleDefinitionName "Reader" -ObjectType "ServicePrincipal" + #Remove root scope access $assignment = Get-AzRoleAssignment -RoleDefinitionId 18d7d88d-d35e-4fb5-a5c3-7773c20a72d9|?{$_.Scope -eq "/" -and $_.SignInName -eq (Get-AzContext).Account.Id} $deleteAssignment = Invoke-AzRestMethod -Path "$($assignment.RoleAssignmentId)?api-version=2018-07-01" -Method DELETE diff --git a/website/docs/tests/maester/MT.1056.md b/website/docs/tests/maester/MT.1056.md new file mode 100644 index 000000000..e2fef1ac7 --- /dev/null +++ b/website/docs/tests/maester/MT.1056.md @@ -0,0 +1,35 @@ +--- +title: MT.1056 - User Access Administrator permission should not be permanently assigned on the root scope +description: Global Admins should not have permanent access to Azure Subscriptions at the root scope +slug: /tests/MT.1056 +sidebar_class_name: hidden +--- + +# User Access Administrator permission should not be permanently assigned on the root scope + +## Description +Ensure that no person has permanent access to Azure Subscriptions. + +User Access Administrator is a role that allows an Administrator to perform everything on an Azure Subscription. Global Administrators can gain this permission on the Root Scope in Entra ID, in the properties of Entra ID. These permissions should only be used in case of emergency and should not be assigned permanently. + +Ensure that no User Access Administrator permissions at the Root Scope are applied. + +## How to fix + +To remove all Admins with Root Scope permissions, as a Global Admin: +1. Navigate to Microsoft 365 admin center [https://portal.microsoft.com](https://portal.microsoft.com). +2. Search for **Microsoft Entra ID** select **Microsoft Entra ID**. +3. Expand the **Manage** menu, select **Properties** +3. On the **Properties** page, go to the **Access management for Azure resources** section. +4. In the information bar, click: **Manage elevated access users**. +5. Select all User Access Administrators, and click **Remove** + +To remove the admins through CLI: +```powershell +az role assignment delete --role "User Access Administrator" --assignee adminname@yourdomain.com --scope "/" +``` + +## Learn more + +* [Elevate access to manage all Azure subscriptions and management groups](https://learn.microsoft.com/en-us/azure/role-based-access-control/elevate-access-global-admin) +