From e34c0dd864ccea218eb4e259d382da71a9b1630c Mon Sep 17 00:00:00 2001 From: Jan Bakker <38911727+BakkerJan@users.noreply.github.com> Date: Fri, 24 Jul 2026 07:18:16 +0200 Subject: [PATCH 1/4] Add MT.1186: Require explicit assignment for high-privilege first-party Entra apps Checks appRoleAssignmentRequired for 9 high-privilege, pre-consented first-party Microsoft service principals (Azure PowerShell, Azure CLI, Microsoft Graph Command Line Tools, Graph Explorer, Azure AD PowerShell, Teams PowerShell, Exchange Online PowerShell, SharePoint Online Management Shell, Power Platform CLI). These carry broad delegated permissions and are commonly used to enumerate or exfiltrate tenant data once an attacker has a foothold on any single user account, but MT.1075 explicitly excludes Microsoft-owned apps from its equivalent check, leaving this gap uncovered. Reserved via https://github.com/maester365/maester/issues/697#issuecomment-5066179224. --- powershell/Maester.psd1 | 2 +- ...ghPrivilegeServicePrincipalsForAllUsers.md | 29 ++++ ...hPrivilegeServicePrincipalsForAllUsers.ps1 | 125 ++++++++++++++++++ ...legeServicePrincipalsForAllUsers.Tests.ps1 | 5 + tests/maester-config.json | 5 + 5 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 powershell/public/maester/entra/Test-MtHighPrivilegeServicePrincipalsForAllUsers.md create mode 100644 powershell/public/maester/entra/Test-MtHighPrivilegeServicePrincipalsForAllUsers.ps1 create mode 100644 tests/Maester/Entra/Test-MtHighPrivilegeServicePrincipalsForAllUsers.Tests.ps1 diff --git a/powershell/Maester.psd1 b/powershell/Maester.psd1 index 3bedbe72d..45c0ab6dd 100644 --- a/powershell/Maester.psd1 +++ b/powershell/Maester.psd1 @@ -266,7 +266,7 @@ 'Test-MtKrbtgtAzureADNotSynced', 'Test-MtExoDelicensingResiliency', 'Test-MtExoMailTip', 'Test-MtExoModernAuth', 'Test-MtExoMoeraMailActivity', 'Test-MtExoOutlookAddin', 'Test-MtExoRejectDirectSend', 'Test-MtExoSetScl', 'Test-MtFeatureUpdatePolicy', 'Test-MtGroupCreationRestricted', - 'Test-MtHighRiskAppPermissions', 'Test-MtIntuneAppControl', 'Test-MtIntuneASRRules', 'Test-MtIntuneDiagnosticSettings', + 'Test-MtHighPrivilegeServicePrincipalsForAllUsers', 'Test-MtHighRiskAppPermissions', 'Test-MtIntuneAppControl', 'Test-MtIntuneASRRules', 'Test-MtIntuneDiagnosticSettings', 'Test-MtIntuneLAPSConfiguration', 'Test-MtIntuneManagedInstallerRules', 'Test-MtIntuneRbacGroupsProtected', 'Test-MtLimitOnMicrosoftDomainUsage', 'Test-MtManagedDeviceCleanupSettings', 'Test-MtManagementGroupWriteRequirement', 'Test-MtMdmAuthority', 'Test-MtMobileThreatDefenseConnectors', 'Test-MtMdeArchiveScanning', diff --git a/powershell/public/maester/entra/Test-MtHighPrivilegeServicePrincipalsForAllUsers.md b/powershell/public/maester/entra/Test-MtHighPrivilegeServicePrincipalsForAllUsers.md new file mode 100644 index 000000000..dae6fa753 --- /dev/null +++ b/powershell/public/maester/entra/Test-MtHighPrivilegeServicePrincipalsForAllUsers.md @@ -0,0 +1,29 @@ +This test checks if any of the following high-privilege, first-party Microsoft service principals allow sign-in from any user instead of requiring explicit assignment: + +- Microsoft Azure PowerShell +- Microsoft Azure CLI +- Microsoft Graph Command Line Tools (used by the Microsoft Graph PowerShell SDK and Microsoft Graph CLI) +- Graph Explorer +- Azure Active Directory PowerShell (legacy) +- Microsoft Teams PowerShell Cmdlets (used by the `MicrosoftTeams` module) +- Microsoft Exchange Online PowerShell (used by the `ExchangeOnlineManagement` module) +- Microsoft SharePoint Online Management Shell +- Power Platform CLI (`pac`) + +These apps are pre-consented in most tenants and carry broad delegated permissions to Microsoft Graph, Azure Resource Manager, or their respective workload (Exchange Online, SharePoint Online, Teams, Power Platform). Left open to all users, any compromised or malicious account can use them, without any further consent prompt, to enumerate directory data and resources, or to administer that workload directly - a common first step in privilege escalation and data exfiltration. It is recommended to set 'Assignment required?' to Yes for these applications and explicitly assign only the users or groups who need them. + +#### Remediation action + +1. Open each flagged application below in the Microsoft Entra admin center and set 'Assignment required?' to Yes under **Properties**. +2. Assign the users or groups who need access under **Users and groups**. +3. Alternatively, use Microsoft Graph PowerShell: +```powershell +Connect-MgGraph -Scopes 'Application.ReadWrite.All' +$sp = Get-MgServicePrincipal -Filter "appId eq ''" +Update-MgServicePrincipal -ServicePrincipalId $sp.Id -AppRoleAssignmentRequired:$true +``` +4. If desired, review the application's sign-in logs first to identify who was using it before locking it down. + + + +%TestResult% diff --git a/powershell/public/maester/entra/Test-MtHighPrivilegeServicePrincipalsForAllUsers.ps1 b/powershell/public/maester/entra/Test-MtHighPrivilegeServicePrincipalsForAllUsers.ps1 new file mode 100644 index 000000000..eb8d76335 --- /dev/null +++ b/powershell/public/maester/entra/Test-MtHighPrivilegeServicePrincipalsForAllUsers.ps1 @@ -0,0 +1,125 @@ +function Test-MtHighPrivilegeServicePrincipalsForAllUsers { + <# + .SYNOPSIS + Checks if any high-privilege first-party Microsoft service principals (e.g. Azure PowerShell, Azure CLI, + Microsoft Graph Command Line Tools, Graph Explorer, Azure AD PowerShell, Exchange Online PowerShell, + SharePoint Online Management Shell, Teams PowerShell, Power Platform CLI) are open to all users instead of + requiring explicit assignment. + + .DESCRIPTION + A small set of Microsoft first-party applications carry broad, pre-consented delegated permissions to Azure + Resource Manager and Microsoft Graph and are commonly used to enumerate or exfiltrate tenant data once an + attacker has a foothold on any single user account. Unlike third-party apps, these service principals are + automatically provisioned in every tenant and easy to overlook when locking down 'Assignment required?'. + + .EXAMPLE + Test-MtHighPrivilegeServicePrincipalsForAllUsers + + Returns true if none of the monitored high-privilege service principals can be used by any user. + + .LINK + https://maester.dev/docs/commands/Test-MtHighPrivilegeServicePrincipalsForAllUsers + #> + [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'This test checks multiple service principals.')] + [OutputType([bool])] + param( + + ) + + if (-not (Test-MtConnection Graph)) { + Add-MtTestResultDetail -SkippedBecause NotConnectedGraph + return $null + } + + Write-Verbose 'Test-MtHighPrivilegeServicePrincipalsForAllUsers: Checking high-privilege first-party service principals for open assignment' + + try { + + $highPrivilegeApps = @( + [pscustomobject]@{ AppId = '1950a258-227b-4e31-a9cf-717495945fc2'; Name = 'Microsoft Azure PowerShell'; Reason = 'Holds Directory.AccessAsUser.All and Application.ReadWrite.All - full directory and Azure Resource Manager control.' } + [pscustomobject]@{ AppId = '04b07795-8ddb-461a-bbee-02f9e1bf7b46'; Name = 'Microsoft Azure CLI'; Reason = 'Holds Directory.AccessAsUser.All and Application.ReadWrite.All - full directory and Azure Resource Manager control.' } + [pscustomobject]@{ AppId = '14d82eec-204b-4c2f-b7e8-296a70dab67e'; Name = 'Microsoft Graph Command Line Tools'; Reason = 'Backs Connect-MgGraph and the Microsoft Graph CLI - can be consented with any Graph scope, including tenant-wide admin permissions.' } + [pscustomobject]@{ AppId = 'de8bc8b5-d9f9-48b1-a8ad-b748da725064'; Name = 'Graph Explorer'; Reason = 'Browser-based, no install required - lowest-friction way to query Microsoft Graph with a signed-in user''s consented scopes.' } + [pscustomobject]@{ AppId = '1b730954-1685-4b74-9bfd-dac224a7b894'; Name = 'Azure Active Directory PowerShell (legacy)'; Reason = 'Holds Directory.ReadWrite.All - full directory control via the deprecated AzureAD module.' } + [pscustomobject]@{ AppId = '12128f48-ec9e-42f0-b203-ea49fb6af367'; Name = 'Microsoft Teams PowerShell Cmdlets'; Reason = 'Holds Group.ReadWrite.All and TeamSettings.ReadWrite.All - can modify any Team, channel, or Microsoft 365 group.' } + [pscustomobject]@{ AppId = 'fb78d390-0c51-40cd-8e17-fdbfab77341b'; Name = 'Microsoft Exchange Online PowerShell'; Reason = 'Grants full Exchange Online admin impersonation - mailbox permissions, transport rules, connectors.' } + [pscustomobject]@{ AppId = '9bc3ab49-b65d-410a-85ad-de819febfddc'; Name = 'Microsoft SharePoint Online Management Shell'; Reason = 'Grants full SharePoint Online tenant admin impersonation - site collections, sharing policy.' } + [pscustomobject]@{ AppId = '9cee029c-6210-4654-90bb-17e6e9d36617'; Name = 'Power Platform CLI'; Reason = 'Holds Application.ReadWrite.All - can impersonate other app registrations to escalate privileges.' } + ) + + $appIdFilter = ($highPrivilegeApps.AppId | ForEach-Object { "appId eq '$_'" }) -join ' or ' + + $params = @{ + 'RelativeUri' = 'serviceprincipals' + 'Select' = 'id,displayName,appId,appRoleAssignmentRequired,accountEnabled' + 'Filter' = "($appIdFilter)" + } + + $spns = Invoke-MtGraphRequest @params + + # No dedicated properties blade to deep-link to for apps that aren't provisioned in this tenant, so + # fall back to Microsoft's own first-party app reference. The hover title still explains the risk either way. + $referenceLink = 'https://learn.microsoft.com/en-us/troubleshoot/entra/entra-id/governance/verify-first-party-apps-sign-in' + + $appRows = foreach ($app in $highPrivilegeApps) { + $spn = $spns | Where-Object { $_.appId -eq $app.AppId } | Select-Object -First 1 + + if (-not $spn) { + Write-Verbose "Test-MtHighPrivilegeServicePrincipalsForAllUsers: $($app.Name) ($($app.AppId)) is not provisioned in this tenant" + [pscustomobject]@{ + Name = $app.Name + AppId = $app.AppId + Status = 'Not present in tenant' + IsOpen = $false + SpnLink = $referenceLink + Reason = $app.Reason + } + } else { + $isOpen = $spn.accountEnabled -eq $true -and $spn.appRoleAssignmentRequired -ne $true + Write-Verbose "Test-MtHighPrivilegeServicePrincipalsForAllUsers: $($spn.displayName) ($($spn.appId)) open to all users: $isOpen" + [pscustomobject]@{ + Name = $spn.displayName + AppId = $spn.appId + Status = if ($isOpen) { 'Open to all users' } else { 'Assignment required' } + IsOpen = $isOpen + SpnLink = "https://entra.microsoft.com/#view/Microsoft_AAD_IAM/ManagedAppMenuBlade/~/Properties/objectId/$($spn.id)/appId/$($spn.appId)" + Reason = $app.Reason + } + } + } + + $buildAppTable = { + param($Rows) + $table = "| Application | Application Id | Status |`n" + $table += "| --- | --- | --- |`n" + foreach ($row in $Rows) { + $nameCell = "[$($row.Name)]($($row.SpnLink) `"$($row.Reason)`")" + $table += "| $nameCell | $($row.AppId) | $($row.Status) |`n" + } + return $table + } + + $openRows = $appRows | Where-Object { $_.IsOpen } + $openCount = ($openRows | Measure-Object).Count + $return = $openCount -eq 0 + + if ($return) { + $testResultMarkdown = "Well done. All monitored high-privilege first-party service principals present in this tenant require explicit user assignment.`n`n" + $testResultMarkdown += & $buildAppTable $appRows + } else { + $otherRows = $appRows | Where-Object { -not $_.IsOpen } + $testResultMarkdown = "You have $openCount high-privilege first-party service principals that can be used by any user.`n`n" + $testResultMarkdown += "**Open to all users**`n`n" + $testResultMarkdown += & $buildAppTable $openRows + $testResultMarkdown += "`n**Other monitored apps**`n`n" + $testResultMarkdown += & $buildAppTable $otherRows + } + + Add-MtTestResultDetail -Result $testResultMarkdown + return $return + } catch { + Add-MtTestResultDetail -SkippedBecause Error -SkippedError $_ + return $null + } +} diff --git a/tests/Maester/Entra/Test-MtHighPrivilegeServicePrincipalsForAllUsers.Tests.ps1 b/tests/Maester/Entra/Test-MtHighPrivilegeServicePrincipalsForAllUsers.Tests.ps1 new file mode 100644 index 000000000..ea93b2924 --- /dev/null +++ b/tests/Maester/Entra/Test-MtHighPrivilegeServicePrincipalsForAllUsers.Tests.ps1 @@ -0,0 +1,5 @@ +Describe 'Maester/Entra' -Tag 'Maester', 'Entra', 'App', 'Graph' { + It 'MT.1186: Require explicit assignment of high-privilege first-party Entra Apps. See https://maester.dev/docs/tests/MT.1186' -Tag 'MT.1186' { + Test-MtHighPrivilegeServicePrincipalsForAllUsers | Should -Be $true -Because 'high-privilege first-party service principals such as Azure PowerShell, Azure CLI, Microsoft Graph Command Line Tools, Graph Explorer, and Azure AD PowerShell should require explicit assignment to users' + } +} diff --git a/tests/maester-config.json b/tests/maester-config.json index 0599fadcc..67efa5fc0 100644 --- a/tests/maester-config.json +++ b/tests/maester-config.json @@ -1543,6 +1543,11 @@ "Severity": "High", "Title": "Block legacy MSOnline (MSOL) PowerShell module" }, + { + "Id": "MT.1186", + "Severity": "High", + "Title": "High-privilege first-party Entra Apps should only have explicitly assigned users instead of All Users." + }, { "Id": "MT.1178", "Severity": "High", From 3ed9f1a0295d9d2dfb03c65f46359ad4846bcc11 Mon Sep 17 00:00:00 2001 From: Jan Bakker <38911727+BakkerJan@users.noreply.github.com> Date: Fri, 24 Jul 2026 07:32:59 +0200 Subject: [PATCH 2/4] Fix status for disabled service principals without assignment required appRoleAssignmentRequired was only evaluated when accountEnabled was also true, so a disabled app with assignment not configured was mislabeled 'Assignment required' and the test passed despite the misconfiguration - it would silently become open to all users the moment the app is re-enabled. Evaluate appRoleAssignmentRequired independently and report disabled-but-unassigned apps under a distinct, non-compliant status. Addresses CodeRabbit review comment on maester365/maester#1985. --- ...hPrivilegeServicePrincipalsForAllUsers.ps1 | 51 +++++++++++-------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/powershell/public/maester/entra/Test-MtHighPrivilegeServicePrincipalsForAllUsers.ps1 b/powershell/public/maester/entra/Test-MtHighPrivilegeServicePrincipalsForAllUsers.ps1 index eb8d76335..0780b3b01 100644 --- a/powershell/public/maester/entra/Test-MtHighPrivilegeServicePrincipalsForAllUsers.ps1 +++ b/powershell/public/maester/entra/Test-MtHighPrivilegeServicePrincipalsForAllUsers.ps1 @@ -68,23 +68,32 @@ if (-not $spn) { Write-Verbose "Test-MtHighPrivilegeServicePrincipalsForAllUsers: $($app.Name) ($($app.AppId)) is not provisioned in this tenant" [pscustomobject]@{ - Name = $app.Name - AppId = $app.AppId - Status = 'Not present in tenant' - IsOpen = $false - SpnLink = $referenceLink - Reason = $app.Reason + Name = $app.Name + AppId = $app.AppId + Status = 'Not present in tenant' + NeedsAttention = $false + SpnLink = $referenceLink + Reason = $app.Reason } } else { - $isOpen = $spn.accountEnabled -eq $true -and $spn.appRoleAssignmentRequired -ne $true - Write-Verbose "Test-MtHighPrivilegeServicePrincipalsForAllUsers: $($spn.displayName) ($($spn.appId)) open to all users: $isOpen" + # Evaluate appRoleAssignmentRequired on its own so a disabled service principal without assignment + # configured is never reported as compliant - it would silently become open the moment it's re-enabled. + $assignmentRequired = $spn.appRoleAssignmentRequired -eq $true + $status = if ($assignmentRequired) { + 'Assignment required' + } elseif ($spn.accountEnabled -eq $true) { + 'Open to all users' + } else { + 'Disabled (assignment not required)' + } + Write-Verbose "Test-MtHighPrivilegeServicePrincipalsForAllUsers: $($spn.displayName) ($($spn.appId)) status: $status" [pscustomobject]@{ - Name = $spn.displayName - AppId = $spn.appId - Status = if ($isOpen) { 'Open to all users' } else { 'Assignment required' } - IsOpen = $isOpen - SpnLink = "https://entra.microsoft.com/#view/Microsoft_AAD_IAM/ManagedAppMenuBlade/~/Properties/objectId/$($spn.id)/appId/$($spn.appId)" - Reason = $app.Reason + Name = $spn.displayName + AppId = $spn.appId + Status = $status + NeedsAttention = -not $assignmentRequired + SpnLink = "https://entra.microsoft.com/#view/Microsoft_AAD_IAM/ManagedAppMenuBlade/~/Properties/objectId/$($spn.id)/appId/$($spn.appId)" + Reason = $app.Reason } } } @@ -100,18 +109,18 @@ return $table } - $openRows = $appRows | Where-Object { $_.IsOpen } - $openCount = ($openRows | Measure-Object).Count - $return = $openCount -eq 0 + $needsAttentionRows = $appRows | Where-Object { $_.NeedsAttention } + $needsAttentionCount = ($needsAttentionRows | Measure-Object).Count + $return = $needsAttentionCount -eq 0 if ($return) { $testResultMarkdown = "Well done. All monitored high-privilege first-party service principals present in this tenant require explicit user assignment.`n`n" $testResultMarkdown += & $buildAppTable $appRows } else { - $otherRows = $appRows | Where-Object { -not $_.IsOpen } - $testResultMarkdown = "You have $openCount high-privilege first-party service principals that can be used by any user.`n`n" - $testResultMarkdown += "**Open to all users**`n`n" - $testResultMarkdown += & $buildAppTable $openRows + $otherRows = $appRows | Where-Object { -not $_.NeedsAttention } + $testResultMarkdown = "You have $needsAttentionCount high-privilege first-party service principals that do not require explicit user assignment.`n`n" + $testResultMarkdown += "**Needs attention**`n`n" + $testResultMarkdown += & $buildAppTable $needsAttentionRows $testResultMarkdown += "`n**Other monitored apps**`n`n" $testResultMarkdown += & $buildAppTable $otherRows } From 29706b815062c8cb3e1ebf0558a731ca0fee2dae Mon Sep 17 00:00:00 2001 From: Merill Fernando Date: Sat, 25 Jul 2026 15:39:13 +1000 Subject: [PATCH 3/4] Fix MT.1186 assignment enforcement --- ...ghPrivilegeServicePrincipalsForAllUsers.md | 31 ++++--- ...hPrivilegeServicePrincipalsForAllUsers.ps1 | 30 ++++--- ...legeServicePrincipalsForAllUsers.Tests.ps1 | 87 +++++++++++++++++++ ...legeServicePrincipalsForAllUsers.Tests.ps1 | 2 +- 4 files changed, 125 insertions(+), 25 deletions(-) create mode 100644 powershell/tests/functions/Test-MtHighPrivilegeServicePrincipalsForAllUsers.Tests.ps1 diff --git a/powershell/public/maester/entra/Test-MtHighPrivilegeServicePrincipalsForAllUsers.md b/powershell/public/maester/entra/Test-MtHighPrivilegeServicePrincipalsForAllUsers.md index dae6fa753..501a290f1 100644 --- a/powershell/public/maester/entra/Test-MtHighPrivilegeServicePrincipalsForAllUsers.md +++ b/powershell/public/maester/entra/Test-MtHighPrivilegeServicePrincipalsForAllUsers.md @@ -1,7 +1,7 @@ This test checks if any of the following high-privilege, first-party Microsoft service principals allow sign-in from any user instead of requiring explicit assignment: - Microsoft Azure PowerShell -- Microsoft Azure CLI +- Microsoft Azure CLI and Azure Developer CLI (`azd`) - both currently use the same application ID - Microsoft Graph Command Line Tools (used by the Microsoft Graph PowerShell SDK and Microsoft Graph CLI) - Graph Explorer - Azure Active Directory PowerShell (legacy) @@ -12,17 +12,28 @@ This test checks if any of the following high-privilege, first-party Microsoft s These apps are pre-consented in most tenants and carry broad delegated permissions to Microsoft Graph, Azure Resource Manager, or their respective workload (Exchange Online, SharePoint Online, Teams, Power Platform). Left open to all users, any compromised or malicious account can use them, without any further consent prompt, to enumerate directory data and resources, or to administer that workload directly - a common first step in privilege escalation and data exfiltration. It is recommended to set 'Assignment required?' to Yes for these applications and explicitly assign only the users or groups who need them. +A first-party application can still be used even when its service principal is not visible in the tenant. A missing service principal is therefore reported as needing attention because the assignment requirement cannot be enforced until the service principal is created. + #### Remediation action -1. Open each flagged application below in the Microsoft Entra admin center and set 'Assignment required?' to Yes under **Properties**. -2. Assign the users or groups who need access under **Users and groups**. -3. Alternatively, use Microsoft Graph PowerShell: -```powershell -Connect-MgGraph -Scopes 'Application.ReadWrite.All' -$sp = Get-MgServicePrincipal -Filter "appId eq ''" -Update-MgServicePrincipal -ServicePrincipalId $sp.Id -AppRoleAssignmentRequired:$true -``` -4. If desired, review the application's sign-in logs first to identify who was using it before locking it down. +1. Review the application's sign-in logs to identify the users who need access. +2. If a flagged application is reported as **Service principal missing**, create it with Microsoft Graph PowerShell: + + ```powershell + Connect-MgGraph -Scopes 'Application.ReadWrite.All' + $appId = '' + $sp = Get-MgServicePrincipal -Filter "appId eq '$appId'" + if (-not $sp) { + $sp = New-MgServicePrincipal -AppId $appId + } + ``` + +3. Assign the approved users or groups under **Users and groups**. +4. Set **Assignment required?** to **Yes** under **Properties**, or use Microsoft Graph PowerShell: + + ```powershell + Update-MgServicePrincipal -ServicePrincipalId $sp.Id -AppRoleAssignmentRequired:$true + ``` diff --git a/powershell/public/maester/entra/Test-MtHighPrivilegeServicePrincipalsForAllUsers.ps1 b/powershell/public/maester/entra/Test-MtHighPrivilegeServicePrincipalsForAllUsers.ps1 index 0780b3b01..3be83b0da 100644 --- a/powershell/public/maester/entra/Test-MtHighPrivilegeServicePrincipalsForAllUsers.ps1 +++ b/powershell/public/maester/entra/Test-MtHighPrivilegeServicePrincipalsForAllUsers.ps1 @@ -1,16 +1,17 @@ function Test-MtHighPrivilegeServicePrincipalsForAllUsers { <# .SYNOPSIS - Checks if any high-privilege first-party Microsoft service principals (e.g. Azure PowerShell, Azure CLI, - Microsoft Graph Command Line Tools, Graph Explorer, Azure AD PowerShell, Exchange Online PowerShell, - SharePoint Online Management Shell, Teams PowerShell, Power Platform CLI) are open to all users instead of - requiring explicit assignment. + Checks if any high-privilege first-party Microsoft service principals (e.g. Azure PowerShell, Azure CLI and + Azure Developer CLI, Microsoft Graph Command Line Tools, Graph Explorer, Azure AD PowerShell, Exchange Online + PowerShell, SharePoint Online Management Shell, Teams PowerShell, Power Platform CLI) are open to all users + instead of requiring explicit assignment. .DESCRIPTION A small set of Microsoft first-party applications carry broad, pre-consented delegated permissions to Azure Resource Manager and Microsoft Graph and are commonly used to enumerate or exfiltrate tenant data once an - attacker has a foothold on any single user account. Unlike third-party apps, these service principals are - automatically provisioned in every tenant and easy to overlook when locking down 'Assignment required?'. + attacker has a foothold on any single user account. Unlike third-party apps, these Microsoft-owned apps can + remain available without a visible service principal in the tenant and are easy to overlook when locking down + 'Assignment required?'. .EXAMPLE Test-MtHighPrivilegeServicePrincipalsForAllUsers @@ -38,7 +39,7 @@ $highPrivilegeApps = @( [pscustomobject]@{ AppId = '1950a258-227b-4e31-a9cf-717495945fc2'; Name = 'Microsoft Azure PowerShell'; Reason = 'Holds Directory.AccessAsUser.All and Application.ReadWrite.All - full directory and Azure Resource Manager control.' } - [pscustomobject]@{ AppId = '04b07795-8ddb-461a-bbee-02f9e1bf7b46'; Name = 'Microsoft Azure CLI'; Reason = 'Holds Directory.AccessAsUser.All and Application.ReadWrite.All - full directory and Azure Resource Manager control.' } + [pscustomobject]@{ AppId = '04b07795-8ddb-461a-bbee-02f9e1bf7b46'; Name = 'Microsoft Azure CLI / Azure Developer CLI'; Reason = 'Used by both Azure CLI (az) and Azure Developer CLI (azd); holds Directory.AccessAsUser.All and Application.ReadWrite.All - full directory and Azure Resource Manager control.' } [pscustomobject]@{ AppId = '14d82eec-204b-4c2f-b7e8-296a70dab67e'; Name = 'Microsoft Graph Command Line Tools'; Reason = 'Backs Connect-MgGraph and the Microsoft Graph CLI - can be consented with any Graph scope, including tenant-wide admin permissions.' } [pscustomobject]@{ AppId = 'de8bc8b5-d9f9-48b1-a8ad-b748da725064'; Name = 'Graph Explorer'; Reason = 'Browser-based, no install required - lowest-friction way to query Microsoft Graph with a signed-in user''s consented scopes.' } [pscustomobject]@{ AppId = '1b730954-1685-4b74-9bfd-dac224a7b894'; Name = 'Azure Active Directory PowerShell (legacy)'; Reason = 'Holds Directory.ReadWrite.All - full directory control via the deprecated AzureAD module.' } @@ -58,9 +59,10 @@ $spns = Invoke-MtGraphRequest @params - # No dedicated properties blade to deep-link to for apps that aren't provisioned in this tenant, so - # fall back to Microsoft's own first-party app reference. The hover title still explains the risk either way. - $referenceLink = 'https://learn.microsoft.com/en-us/troubleshoot/entra/entra-id/governance/verify-first-party-apps-sign-in' + # Apps without a local service principal can still appear in sign-in logs. Creating the service principal + # is required before user assignment can be enforced, so absence is a non-compliant state. + $referenceLink = 'https://learn.microsoft.com/en-us/entra/identity-platform/howto-restrict-your-app-to-a-set-of-users' + $entraPortalUrl = $__MtSession.AdminPortalUrl.Entra $appRows = foreach ($app in $highPrivilegeApps) { $spn = $spns | Where-Object { $_.appId -eq $app.AppId } | Select-Object -First 1 @@ -70,8 +72,8 @@ [pscustomobject]@{ Name = $app.Name AppId = $app.AppId - Status = 'Not present in tenant' - NeedsAttention = $false + Status = 'Service principal missing (assignment not enforced)' + NeedsAttention = $true SpnLink = $referenceLink Reason = $app.Reason } @@ -92,7 +94,7 @@ AppId = $spn.appId Status = $status NeedsAttention = -not $assignmentRequired - SpnLink = "https://entra.microsoft.com/#view/Microsoft_AAD_IAM/ManagedAppMenuBlade/~/Properties/objectId/$($spn.id)/appId/$($spn.appId)" + SpnLink = "$($entraPortalUrl)#view/Microsoft_AAD_IAM/ManagedAppMenuBlade/~/Properties/objectId/$($spn.id)/appId/$($spn.appId)" Reason = $app.Reason } } @@ -118,7 +120,7 @@ $testResultMarkdown += & $buildAppTable $appRows } else { $otherRows = $appRows | Where-Object { -not $_.NeedsAttention } - $testResultMarkdown = "You have $needsAttentionCount high-privilege first-party service principals that do not require explicit user assignment.`n`n" + $testResultMarkdown = "You have $needsAttentionCount high-privilege first-party applications that are not configured to require explicit user assignment.`n`n" $testResultMarkdown += "**Needs attention**`n`n" $testResultMarkdown += & $buildAppTable $needsAttentionRows $testResultMarkdown += "`n**Other monitored apps**`n`n" diff --git a/powershell/tests/functions/Test-MtHighPrivilegeServicePrincipalsForAllUsers.Tests.ps1 b/powershell/tests/functions/Test-MtHighPrivilegeServicePrincipalsForAllUsers.Tests.ps1 new file mode 100644 index 000000000..0c18531c1 --- /dev/null +++ b/powershell/tests/functions/Test-MtHighPrivilegeServicePrincipalsForAllUsers.Tests.ps1 @@ -0,0 +1,87 @@ +Describe 'Test-MtHighPrivilegeServicePrincipalsForAllUsers' { + BeforeAll { + $script:monitoredAppIds = @( + '1950a258-227b-4e31-a9cf-717495945fc2' + '04b07795-8ddb-461a-bbee-02f9e1bf7b46' + '14d82eec-204b-4c2f-b7e8-296a70dab67e' + 'de8bc8b5-d9f9-48b1-a8ad-b748da725064' + '1b730954-1685-4b74-9bfd-dac224a7b894' + '12128f48-ec9e-42f0-b203-ea49fb6af367' + 'fb78d390-0c51-40cd-8e17-fdbfab77341b' + '9bc3ab49-b65d-410a-85ad-de819febfddc' + '9cee029c-6210-4654-90bb-17e6e9d36617' + ) + + function New-TestServicePrincipal { + param( + [string] $AppId, + [bool] $AssignmentRequired = $true, + [bool] $AccountEnabled = $true + ) + + return [pscustomobject]@{ + id = "sp-$AppId" + displayName = "App $AppId" + appId = $AppId + appRoleAssignmentRequired = $AssignmentRequired + accountEnabled = $AccountEnabled + } + } + } + + BeforeEach { + $script:testResultMarkdown = $null + + Mock -ModuleName Maester Test-MtConnection { return $true } + Mock -ModuleName Maester Add-MtTestResultDetail { + param($Result) + + $script:testResultMarkdown = $Result + } + + InModuleScope Maester { + $__MtSession.AdminPortalUrl = @{ + Entra = 'https://entra.microsoft.us/' + } + } + } + + It 'fails when a monitored first-party service principal is missing' { + $servicePrincipals = $script:monitoredAppIds | + Select-Object -Skip 1 | + ForEach-Object { New-TestServicePrincipal -AppId $_ } + Mock -ModuleName Maester Invoke-MtGraphRequest { return $servicePrincipals } + + Test-MtHighPrivilegeServicePrincipalsForAllUsers | Should -BeFalse + $script:testResultMarkdown | Should -Match 'Service principal missing \(assignment not enforced\)' + $script:testResultMarkdown | Should -Match 'Microsoft Azure PowerShell' + } + + It 'passes when every monitored service principal requires assignment' { + $servicePrincipals = $script:monitoredAppIds | + ForEach-Object { New-TestServicePrincipal -AppId $_ } + Mock -ModuleName Maester Invoke-MtGraphRequest { return $servicePrincipals } + + Test-MtHighPrivilegeServicePrincipalsForAllUsers | Should -BeTrue + $script:testResultMarkdown | Should -Match 'Well done' + $script:testResultMarkdown | Should -Match 'https://entra\.microsoft\.us/' + } + + It 'fails when a disabled service principal does not require assignment' { + $servicePrincipals = $script:monitoredAppIds | + ForEach-Object { New-TestServicePrincipal -AppId $_ } + $servicePrincipals[0].appRoleAssignmentRequired = $false + $servicePrincipals[0].accountEnabled = $false + Mock -ModuleName Maester Invoke-MtGraphRequest { return $servicePrincipals } + + Test-MtHighPrivilegeServicePrincipalsForAllUsers | Should -BeFalse + $script:testResultMarkdown | Should -Match 'Disabled \(assignment not required\)' + } + + It 'documents that the Azure CLI app also covers Azure Developer CLI' { + Mock -ModuleName Maester Invoke-MtGraphRequest { return @() } + + Test-MtHighPrivilegeServicePrincipalsForAllUsers | Should -BeFalse + $script:testResultMarkdown | Should -Match 'Azure Developer CLI' + } +} diff --git a/tests/Maester/Entra/Test-MtHighPrivilegeServicePrincipalsForAllUsers.Tests.ps1 b/tests/Maester/Entra/Test-MtHighPrivilegeServicePrincipalsForAllUsers.Tests.ps1 index ea93b2924..0385cbff3 100644 --- a/tests/Maester/Entra/Test-MtHighPrivilegeServicePrincipalsForAllUsers.Tests.ps1 +++ b/tests/Maester/Entra/Test-MtHighPrivilegeServicePrincipalsForAllUsers.Tests.ps1 @@ -1,5 +1,5 @@ Describe 'Maester/Entra' -Tag 'Maester', 'Entra', 'App', 'Graph' { It 'MT.1186: Require explicit assignment of high-privilege first-party Entra Apps. See https://maester.dev/docs/tests/MT.1186' -Tag 'MT.1186' { - Test-MtHighPrivilegeServicePrincipalsForAllUsers | Should -Be $true -Because 'high-privilege first-party service principals such as Azure PowerShell, Azure CLI, Microsoft Graph Command Line Tools, Graph Explorer, and Azure AD PowerShell should require explicit assignment to users' + Test-MtHighPrivilegeServicePrincipalsForAllUsers | Should -Be $true -Because 'high-privilege first-party service principals such as Azure PowerShell, Azure CLI and Azure Developer CLI, Microsoft Graph Command Line Tools, Graph Explorer, and Azure AD PowerShell should require explicit assignment to users' } } From 0c470e3b61fbd7eb8f65c998bda5aa2ff7507b07 Mon Sep 17 00:00:00 2001 From: Merill Fernando Date: Sat, 25 Jul 2026 15:54:35 +1000 Subject: [PATCH 4/4] Fix MT.1186 test analyzer finding --- ...-MtHighPrivilegeServicePrincipalsForAllUsers.Tests.ps1 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/powershell/tests/functions/Test-MtHighPrivilegeServicePrincipalsForAllUsers.Tests.ps1 b/powershell/tests/functions/Test-MtHighPrivilegeServicePrincipalsForAllUsers.Tests.ps1 index 0c18531c1..a621fd9e6 100644 --- a/powershell/tests/functions/Test-MtHighPrivilegeServicePrincipalsForAllUsers.Tests.ps1 +++ b/powershell/tests/functions/Test-MtHighPrivilegeServicePrincipalsForAllUsers.Tests.ps1 @@ -12,7 +12,7 @@ '9cee029c-6210-4654-90bb-17e6e9d36617' ) - function New-TestServicePrincipal { + function Get-TestServicePrincipal { param( [string] $AppId, [bool] $AssignmentRequired = $true, @@ -49,7 +49,7 @@ It 'fails when a monitored first-party service principal is missing' { $servicePrincipals = $script:monitoredAppIds | Select-Object -Skip 1 | - ForEach-Object { New-TestServicePrincipal -AppId $_ } + ForEach-Object { Get-TestServicePrincipal -AppId $_ } Mock -ModuleName Maester Invoke-MtGraphRequest { return $servicePrincipals } Test-MtHighPrivilegeServicePrincipalsForAllUsers | Should -BeFalse @@ -59,7 +59,7 @@ It 'passes when every monitored service principal requires assignment' { $servicePrincipals = $script:monitoredAppIds | - ForEach-Object { New-TestServicePrincipal -AppId $_ } + ForEach-Object { Get-TestServicePrincipal -AppId $_ } Mock -ModuleName Maester Invoke-MtGraphRequest { return $servicePrincipals } Test-MtHighPrivilegeServicePrincipalsForAllUsers | Should -BeTrue @@ -69,7 +69,7 @@ It 'fails when a disabled service principal does not require assignment' { $servicePrincipals = $script:monitoredAppIds | - ForEach-Object { New-TestServicePrincipal -AppId $_ } + ForEach-Object { Get-TestServicePrincipal -AppId $_ } $servicePrincipals[0].appRoleAssignmentRequired = $false $servicePrincipals[0].accountEnabled = $false Mock -ModuleName Maester Invoke-MtGraphRequest { return $servicePrincipals }