From 5fc121e488c1de3399d8b14dfc56cafd3f81bf7e Mon Sep 17 00:00:00 2001 From: Erik Oppedijk Date: Mon, 14 Apr 2025 10:57:14 +0200 Subject: [PATCH 01/12] Add Azure test for User Access Administrator --- maester | 1 + tests/Maester/Azure/Test-MtUserAccessAdmin.md | 28 ++++++++ .../Maester/Azure/Test-MtUserAccessAdmin.ps1 | 72 +++++++++++++++++++ tests/Maester/Azure/UserAccessAdmin.Tests.ps1 | 10 +++ website/docs/sections/create-entra-app.md | 9 ++- website/docs/tests/maester/MT.1056.md | 35 +++++++++ 6 files changed, 152 insertions(+), 3 deletions(-) create mode 160000 maester create mode 100644 tests/Maester/Azure/Test-MtUserAccessAdmin.md create mode 100644 tests/Maester/Azure/Test-MtUserAccessAdmin.ps1 create mode 100644 tests/Maester/Azure/UserAccessAdmin.Tests.ps1 create mode 100644 website/docs/tests/maester/MT.1056.md 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/tests/Maester/Azure/Test-MtUserAccessAdmin.md b/tests/Maester/Azure/Test-MtUserAccessAdmin.md new file mode 100644 index 000000000..573d93b49 --- /dev/null +++ b/tests/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 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. + +#### Remediation action: + +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 "/" +``` + +#### Related links + +* [Microsoft Azure Portal](https://portal.azure.com) + + + +%TestResult% \ No newline at end of file diff --git a/tests/Maester/Azure/Test-MtUserAccessAdmin.ps1 b/tests/Maester/Azure/Test-MtUserAccessAdmin.ps1 new file mode 100644 index 000000000..1d8817f21 --- /dev/null +++ b/tests/Maester/Azure/Test-MtUserAccessAdmin.ps1 @@ -0,0 +1,72 @@ +<# +.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 { + + 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" + $result = az role assignment list --role 'User Access Administrator' --scope '/' + if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to retrieve role assignments" + Add-MtTestResultDetail -SkippedBecause NotConnectedAzure + return $null + } + + $resultObject = $result | ConvertFrom-Json + + # Get the count of role assignments + $roleAssignmentCount = $resultObject.Count + + $testResult = $roleAssignmentCount -eq 0 + + if ($testResult) { + $testResultMarkdown = "Well done. Your tenant has no User Access Administrators:`n`n%TestResult%" + } + else { + $testResultMarkdown = "Your tenant has $roleAssignmentCount User Access Administrators:`n`n%TestResult%" + } + # $itemCount is used to limit the number of returned results shown in the table + $itemCount = 0 + $resultMd = "| Display Name | User Access |`n" + $resultMd += "| --- | --- |`n" + foreach ($item in $resultObject) { + $itemCount += 1 + $itemResult = "❌ Fail" + # We are restricting the table output to 50 below as it could be extremely large + if ($itemCount -lt 51) { + $resultMd += "| $($item.principalName) | $($itemResult) |`n" + } + } + # Add a limited results message if more than 6 results are returned + if ($itemCount -gt 50) { + $resultMd += "Results limited to 50`n" + } + + $testResultMarkdown = $testResultMarkdown -replace "%TestResult%", $resultMd + + 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..f3ccd4e5b --- /dev/null +++ b/tests/Maester/Azure/UserAccessAdmin.Tests.ps1 @@ -0,0 +1,10 @@ +BeforeAll { + . $PSScriptRoot/Test-MtUserAccessAdmin.ps1 +} +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) + From ede397fa30f7c04d3a1f6923aba46201707a57f7 Mon Sep 17 00:00:00 2001 From: Erik Oppedijk Date: Wed, 16 Apr 2025 10:06:51 +0200 Subject: [PATCH 02/12] enhance wording --- tests/Maester/Azure/Test-MtUserAccessAdmin.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/Maester/Azure/Test-MtUserAccessAdmin.md b/tests/Maester/Azure/Test-MtUserAccessAdmin.md index 573d93b49..92bd40641 100644 --- a/tests/Maester/Azure/Test-MtUserAccessAdmin.md +++ b/tests/Maester/Azure/Test-MtUserAccessAdmin.md @@ -8,11 +8,11 @@ Ensure that no User Access Administrator permissions at the Root Scope are appli 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** +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** +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 @@ -21,7 +21,7 @@ az role assignment delete --role "User Access Administrator" --assignee adminnam #### Related links -* [Microsoft Azure Portal](https://portal.azure.com) +* [Manage who can create Microsoft 365 Groups](https://learn.microsoft.com/en-us/microsoft-365/solutions/manage-creation-of-groups?view=o365-worldwide) From 872c32cd83a80d3ef6de4b5bf22ffb60cf1aaa82 Mon Sep 17 00:00:00 2001 From: Erik Oppedijk Date: Wed, 16 Apr 2025 10:13:58 +0200 Subject: [PATCH 03/12] move files to correct location --- .../public/maester/azure}/Test-MtUserAccessAdmin.md | 0 .../public/maester/azure}/Test-MtUserAccessAdmin.ps1 | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename {tests/Maester/Azure => powershell/public/maester/azure}/Test-MtUserAccessAdmin.md (100%) rename {tests/Maester/Azure => powershell/public/maester/azure}/Test-MtUserAccessAdmin.ps1 (100%) diff --git a/tests/Maester/Azure/Test-MtUserAccessAdmin.md b/powershell/public/maester/azure/Test-MtUserAccessAdmin.md similarity index 100% rename from tests/Maester/Azure/Test-MtUserAccessAdmin.md rename to powershell/public/maester/azure/Test-MtUserAccessAdmin.md diff --git a/tests/Maester/Azure/Test-MtUserAccessAdmin.ps1 b/powershell/public/maester/azure/Test-MtUserAccessAdmin.ps1 similarity index 100% rename from tests/Maester/Azure/Test-MtUserAccessAdmin.ps1 rename to powershell/public/maester/azure/Test-MtUserAccessAdmin.ps1 From 2035ee6fa091ad81b4cb4b1a1f0264acf839d107 Mon Sep 17 00:00:00 2001 From: Erik Oppedijk Date: Wed, 16 Apr 2025 13:22:19 +0200 Subject: [PATCH 04/12] Change to Get-AzRoleAssignment --- .../public/maester/azure/Test-MtUserAccessAdmin.ps1 | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/powershell/public/maester/azure/Test-MtUserAccessAdmin.ps1 b/powershell/public/maester/azure/Test-MtUserAccessAdmin.ps1 index 1d8817f21..96c57db46 100644 --- a/powershell/public/maester/azure/Test-MtUserAccessAdmin.ps1 +++ b/powershell/public/maester/azure/Test-MtUserAccessAdmin.ps1 @@ -27,14 +27,15 @@ function Test-MtUserAccessAdmin { } Write-Verbose "Getting all User Access Administrators at Root Scope" - $result = az role assignment list --role 'User Access Administrator' --scope '/' - if ($LASTEXITCODE -ne 0) { - Write-Error "Failed to retrieve role assignments" + try { + $result = Get-AzRoleAssignment -Scope "/" -RoleDefinitionName 'User Access Administrator' -ErrorAction Stop + } catch { + Write-Error "Failed to retrieve role assignments at root scope" Add-MtTestResultDetail -SkippedBecause NotConnectedAzure return $null } - $resultObject = $result | ConvertFrom-Json + $resultObject = $result # Get the count of role assignments $roleAssignmentCount = $resultObject.Count @@ -56,7 +57,7 @@ function Test-MtUserAccessAdmin { $itemResult = "❌ Fail" # We are restricting the table output to 50 below as it could be extremely large if ($itemCount -lt 51) { - $resultMd += "| $($item.principalName) | $($itemResult) |`n" + $resultMd += "| $($item.SignInName) | $($itemResult) |`n" } } # Add a limited results message if more than 6 results are returned From b2508e9190ac19baf41219ec8f94b0e059797636 Mon Sep 17 00:00:00 2001 From: Erik Oppedijk Date: Wed, 16 Apr 2025 13:29:50 +0200 Subject: [PATCH 05/12] add BOM --- powershell/public/maester/azure/Test-MtUserAccessAdmin.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/powershell/public/maester/azure/Test-MtUserAccessAdmin.ps1 b/powershell/public/maester/azure/Test-MtUserAccessAdmin.ps1 index 96c57db46..2c647e394 100644 --- a/powershell/public/maester/azure/Test-MtUserAccessAdmin.ps1 +++ b/powershell/public/maester/azure/Test-MtUserAccessAdmin.ps1 @@ -1,4 +1,4 @@ -<# +<# .SYNOPSIS Checks if any Global Admins have User Access Control permissions at the Root Scope From 4f47e9250ea3246654f398ab694d91f223cd07ac Mon Sep 17 00:00:00 2001 From: Erik Oppedijk Date: Wed, 16 Apr 2025 13:31:35 +0200 Subject: [PATCH 06/12] export public function Test-MtUserAccessAdmin --- powershell/Maester.psd1 | 1 + 1 file changed, 1 insertion(+) diff --git a/powershell/Maester.psd1 b/powershell/Maester.psd1 index 1558d10aa..9d2109425 100644 --- a/powershell/Maester.psd1 +++ b/powershell/Maester.psd1 @@ -165,6 +165,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', From 309c6a58f61dc7b7566f8c108b5e30c9daad6106 Mon Sep 17 00:00:00 2001 From: Erik Oppedijk Date: Wed, 16 Apr 2025 13:39:43 +0200 Subject: [PATCH 07/12] Add Advanced Function support for PS --- powershell/public/maester/azure/Test-MtUserAccessAdmin.ps1 | 3 +++ 1 file changed, 3 insertions(+) diff --git a/powershell/public/maester/azure/Test-MtUserAccessAdmin.ps1 b/powershell/public/maester/azure/Test-MtUserAccessAdmin.ps1 index 2c647e394..76f92b178 100644 --- a/powershell/public/maester/azure/Test-MtUserAccessAdmin.ps1 +++ b/powershell/public/maester/azure/Test-MtUserAccessAdmin.ps1 @@ -14,6 +14,9 @@ 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)) { From a88fb42da8c2b36f12deefd7565bcb673bba91fe Mon Sep 17 00:00:00 2001 From: Erik Oppedijk Date: Wed, 16 Apr 2025 13:45:58 +0200 Subject: [PATCH 08/12] cleanup results --- powershell/public/maester/azure/Test-MtUserAccessAdmin.ps1 | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/powershell/public/maester/azure/Test-MtUserAccessAdmin.ps1 b/powershell/public/maester/azure/Test-MtUserAccessAdmin.ps1 index 76f92b178..995ea6034 100644 --- a/powershell/public/maester/azure/Test-MtUserAccessAdmin.ps1 +++ b/powershell/public/maester/azure/Test-MtUserAccessAdmin.ps1 @@ -31,17 +31,15 @@ function Test-MtUserAccessAdmin { Write-Verbose "Getting all User Access Administrators at Root Scope" try { - $result = Get-AzRoleAssignment -Scope "/" -RoleDefinitionName 'User Access Administrator' -ErrorAction Stop + $roles = Get-AzRoleAssignment -Scope "/" -RoleDefinitionName 'User Access Administrator' -ErrorAction Stop } catch { Write-Error "Failed to retrieve role assignments at root scope" Add-MtTestResultDetail -SkippedBecause NotConnectedAzure return $null } - $resultObject = $result - # Get the count of role assignments - $roleAssignmentCount = $resultObject.Count + $roleAssignmentCount = $roles.Count $testResult = $roleAssignmentCount -eq 0 @@ -51,6 +49,7 @@ function Test-MtUserAccessAdmin { else { $testResultMarkdown = "Your tenant has $roleAssignmentCount User Access Administrators:`n`n%TestResult%" } + # $itemCount is used to limit the number of returned results shown in the table $itemCount = 0 $resultMd = "| Display Name | User Access |`n" From e73bb0ddd2a3b75773c6b05b1429c57982388a32 Mon Sep 17 00:00:00 2001 From: Erik Oppedijk Date: Fri, 2 May 2025 14:53:03 +0200 Subject: [PATCH 09/12] Fix texts and links --- powershell/public/maester/azure/Test-MtUserAccessAdmin.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/powershell/public/maester/azure/Test-MtUserAccessAdmin.md b/powershell/public/maester/azure/Test-MtUserAccessAdmin.md index 92bd40641..88d21ed20 100644 --- a/powershell/public/maester/azure/Test-MtUserAccessAdmin.md +++ b/powershell/public/maester/azure/Test-MtUserAccessAdmin.md @@ -1,13 +1,13 @@ 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. +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 365 admin center [https://portal.microsoft.com](https://portal.microsoft.com). +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. From 0c90df79d37fc71ad0a64a7d911497b49bcfb2d9 Mon Sep 17 00:00:00 2001 From: merill Date: Sun, 4 May 2025 08:07:36 +1000 Subject: [PATCH 10/12] Switched to rest api --- powershell/Maester.psd1 | 1 + .../internal/Get-GraphObjectMarkdown.ps1 | 33 ++++++++++++-- .../internal/Get-MtDirectoryObjects.ps1 | 32 ++++++++++++++ .../public/core/Invoke-MtAzureRequest.ps1 | 43 +++++++++++++++++++ .../maester/azure/Test-MtUserAccessAdmin.ps1 | 35 +++------------ tests/Maester/Azure/UserAccessAdmin.Tests.ps1 | 3 -- 6 files changed, 112 insertions(+), 35 deletions(-) create mode 100644 powershell/internal/Get-MtDirectoryObjects.ps1 create mode 100644 powershell/public/core/Invoke-MtAzureRequest.ps1 diff --git a/powershell/Maester.psd1 b/powershell/Maester.psd1 index 9d2109425..d11c5051c 100644 --- a/powershell/Maester.psd1 +++ b/powershell/Maester.psd1 @@ -82,6 +82,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', 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..092dd7bf8 --- /dev/null +++ b/powershell/internal/Get-MtDirectoryObjects.ps1 @@ -0,0 +1,32 @@ +<# + .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 { + [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..020c9805d --- /dev/null +++ b/powershell/public/core/Invoke-MtAzureRequest.ps1 @@ -0,0 +1,43 @@ +<# +.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. +#> + +function Invoke-MtAzureRequest { + [CmdletBinding()] + param( + # Graph endpoint such as "users". + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $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 2022-04-01 + [Parameter(Mandatory = $false)] + $ApiVersion = '2022-04-01', + + # The filter to use. + [Parameter(Mandatory = $false)] + [string] $Filter, + + [Parameter(Mandatory = $false)] + [ValidateSet('PSObject', 'PSCustomObject', 'Hashtable')] + [string] $OutputType = 'PSObject' + ) + + $resourceUrl = (Get-AzContext).Environment.ResourceManagerUrl + $restApi = "$($resourceUrl)$($RelativeUri)?api-version=$($ApiVersion)" + if ($Filter) { + $restApi += '&$Filter=' + $Filter + } + + $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.ps1 b/powershell/public/maester/azure/Test-MtUserAccessAdmin.ps1 index 995ea6034..e2b2aac06 100644 --- a/powershell/public/maester/azure/Test-MtUserAccessAdmin.ps1 +++ b/powershell/public/maester/azure/Test-MtUserAccessAdmin.ps1 @@ -30,45 +30,24 @@ function Test-MtUserAccessAdmin { } Write-Verbose "Getting all User Access Administrators at Root Scope" - try { - $roles = Get-AzRoleAssignment -Scope "/" -RoleDefinitionName 'User Access Administrator' -ErrorAction Stop - } catch { - Write-Error "Failed to retrieve role assignments at root scope" - Add-MtTestResultDetail -SkippedBecause NotConnectedAzure - return $null - } + + $userAccessResult = Invoke-MtAzureRequest -RelativeUri 'providers/Microsoft.Authorization/roleAssignments' -Filter 'atScope()' + $userAccessAdmins = Get-ObjectProperty $userAccessResult 'value' # Get the count of role assignments - $roleAssignmentCount = $roles.Count + $roleAssignmentCount = $userAccessAdmins | Measure-Object | Select-Object -ExpandProperty Count $testResult = $roleAssignmentCount -eq 0 if ($testResult) { - $testResultMarkdown = "Well done. Your tenant has no User Access Administrators:`n`n%TestResult%" + $testResultMarkdown = "Well done. Your tenant has no User Access Administrators." } else { - $testResultMarkdown = "Your tenant has $roleAssignmentCount User Access Administrators:`n`n%TestResult%" - } + $testResultMarkdown = "Your tenant has $roleAssignmentCount resource(s) with access to manage access to all Azure subscriptions and management groups in this tenant.`n`n" - # $itemCount is used to limit the number of returned results shown in the table - $itemCount = 0 - $resultMd = "| Display Name | User Access |`n" - $resultMd += "| --- | --- |`n" - foreach ($item in $resultObject) { - $itemCount += 1 - $itemResult = "❌ Fail" - # We are restricting the table output to 50 below as it could be extremely large - if ($itemCount -lt 51) { - $resultMd += "| $($item.SignInName) | $($itemResult) |`n" - } - } - # Add a limited results message if more than 6 results are returned - if ($itemCount -gt 50) { - $resultMd += "Results limited to 50`n" + $testResultMarkdown += Get-MtDirectoryObjects $userAccessAdmins.properties.principalId -AsMarkdown } - $testResultMarkdown = $testResultMarkdown -replace "%TestResult%", $resultMd - Add-MtTestResultDetail -Result $testResultMarkdown return $testResult diff --git a/tests/Maester/Azure/UserAccessAdmin.Tests.ps1 b/tests/Maester/Azure/UserAccessAdmin.Tests.ps1 index f3ccd4e5b..23d063090 100644 --- a/tests/Maester/Azure/UserAccessAdmin.Tests.ps1 +++ b/tests/Maester/Azure/UserAccessAdmin.Tests.ps1 @@ -1,6 +1,3 @@ -BeforeAll { - . $PSScriptRoot/Test-MtUserAccessAdmin.ps1 -} Describe "AzureConfig" -Tag "Privilege", "Azure" { It "MT. Check 'User Access Administrators' at root scope" { From 665efb32d8d11fea5045d9fc6fc1e8149e651600 Mon Sep 17 00:00:00 2001 From: merill Date: Sun, 4 May 2025 08:21:07 +1000 Subject: [PATCH 11/12] Switched to use Rest api for Azure --- .../public/core/Invoke-MtAzureRequest.ps1 | 17 +++++++++++++---- .../maester/azure/Test-MtUserAccessAdmin.ps1 | 2 +- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/powershell/public/core/Invoke-MtAzureRequest.ps1 b/powershell/public/core/Invoke-MtAzureRequest.ps1 index 020c9805d..330ef6efa 100644 --- a/powershell/public/core/Invoke-MtAzureRequest.ps1 +++ b/powershell/public/core/Invoke-MtAzureRequest.ps1 @@ -1,10 +1,17 @@ <# -.SYNOPSIS + .SYNOPSIS Invoke a REST API request to the Azure Management API. -.DESCRIPTION + .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 { @@ -19,14 +26,15 @@ function Invoke-MtAzureRequest { [ValidateSet("GET")] [string] $Method = "GET", - # The API version to use. Default is 2022-04-01 + # The API version to use. Default is 2024-11-01 [Parameter(Mandatory = $false)] - $ApiVersion = '2022-04-01', + $ApiVersion = '2024-11-01', # The filter to use. [Parameter(Mandatory = $false)] [string] $Filter, + # The type of object to be returned [Parameter(Mandatory = $false)] [ValidateSet('PSObject', 'PSCustomObject', 'Hashtable')] [string] $OutputType = 'PSObject' @@ -38,6 +46,7 @@ function Invoke-MtAzureRequest { $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.ps1 b/powershell/public/maester/azure/Test-MtUserAccessAdmin.ps1 index e2b2aac06..1a2364763 100644 --- a/powershell/public/maester/azure/Test-MtUserAccessAdmin.ps1 +++ b/powershell/public/maester/azure/Test-MtUserAccessAdmin.ps1 @@ -31,7 +31,7 @@ function Test-MtUserAccessAdmin { Write-Verbose "Getting all User Access Administrators at Root Scope" - $userAccessResult = Invoke-MtAzureRequest -RelativeUri 'providers/Microsoft.Authorization/roleAssignments' -Filter 'atScope()' + $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 From fcdafb8fe7d675d3cfb53b3f200258ced5a8f0e8 Mon Sep 17 00:00:00 2001 From: merill Date: Sun, 4 May 2025 08:27:48 +1000 Subject: [PATCH 12/12] Fixed pester issues --- powershell/internal/Get-MtDirectoryObjects.ps1 | 1 + powershell/public/core/Invoke-MtAzureRequest.ps1 | 9 ++------- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/powershell/internal/Get-MtDirectoryObjects.ps1 b/powershell/internal/Get-MtDirectoryObjects.ps1 index 092dd7bf8..938e18f4b 100644 --- a/powershell/internal/Get-MtDirectoryObjects.ps1 +++ b/powershell/internal/Get-MtDirectoryObjects.ps1 @@ -8,6 +8,7 @@ #> 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. diff --git a/powershell/public/core/Invoke-MtAzureRequest.ps1 b/powershell/public/core/Invoke-MtAzureRequest.ps1 index 330ef6efa..13967402b 100644 --- a/powershell/public/core/Invoke-MtAzureRequest.ps1 +++ b/powershell/public/core/Invoke-MtAzureRequest.ps1 @@ -18,7 +18,7 @@ function Invoke-MtAzureRequest { [CmdletBinding()] param( # Graph endpoint such as "users". - [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] + [Parameter(Mandatory = $true)] [string[]] $RelativeUri, # The HTTP method to use. Default is GET. @@ -32,12 +32,7 @@ function Invoke-MtAzureRequest { # The filter to use. [Parameter(Mandatory = $false)] - [string] $Filter, - - # The type of object to be returned - [Parameter(Mandatory = $false)] - [ValidateSet('PSObject', 'PSCustomObject', 'Hashtable')] - [string] $OutputType = 'PSObject' + [string] $Filter ) $resourceUrl = (Get-AzContext).Environment.ResourceManagerUrl