Skip to content

Commit f248683

Browse files
authored
Add files via upload
1 parent aa32766 commit f248683

1 file changed

Lines changed: 254 additions & 48 deletions

File tree

Lines changed: 254 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,256 @@
1-
# The app for which consent is being granted.
2-
$clientAppId = "de8bc8b5-d9f9-48b1-a8ad-b748da725064" # Microsoft Graph Explorer
3-
4-
# The API to which access will be granted. Microsoft Graph Explorer makes API
5-
# requests to the Microsoft Graph API, so we'll use that here.
6-
$resourceAppId = "00000003-0000-0000-c000-000000000000" # Microsoft Graph API
7-
8-
# The permissions to grant. Here we're including "openid", "profile", "User.Read"
9-
# and "offline_access" (for basic sign-in), as well as "User.ReadBasic.All" (for
10-
# reading other users' basic profile).
11-
$permissions = @("openid", "profile", "offline_access", "User.Read", "User.ReadBasic.All")
12-
13-
# The user on behalf of whom access will be granted. The app will be able to access
14-
# the API on behalf of this user.
15-
$userUpnOrId = "mcontestabile@xxx.yyy"
16-
17-
# Step 0. Connect to Microsoft Graph PowerShell. We need User.ReadBasic.All to get
18-
# users' IDs, Application.ReadWrite.All to list and create service principals,
19-
# DelegatedPermissionGrant.ReadWrite.All to create delegated permission grants,
20-
# and AppRoleAssignment.ReadWrite.All to assign an app role.
21-
# WARNING: These are high-privilege permissions!
22-
Connect-MgGraph -Scopes ("User.ReadBasic.All Application.ReadWrite.All " + "DelegatedPermissionGrant.ReadWrite.All " + "AppRoleAssignment.ReadWrite.All")
23-
24-
# Step 1. Check if a service principal exists for the client application.
25-
# If one doesn't exist, create it.
26-
$clientSp = Get-MgServicePrincipal -Filter "appId eq '$($clientAppId)'"
27-
if (-not $clientSp) {
28-
$clientSp = New-MgServicePrincipal -AppId $clientAppId
29-
}
30-
31-
# Step 2. Create a delegated permission that grants the client app access to the
32-
# API, on behalf of the user. (This example assumes that an existing delegated
33-
# permission grant does not already exist, in which case it would be necessary
34-
# to update the existing grant, rather than create a new one.)
35-
$user = Get-MgUser -UserId $userUpnOrId
36-
$resourceSp = Get-MgServicePrincipal -Filter "appId eq '$($resourceAppId)'"
37-
$scopeToGrant = $permissions -join " "
38-
$grant = New-MgOauth2PermissionGrant -ResourceId $resourceSp.Id -Scope $scopeToGrant -ClientId $clientSp.Id -ConsentType "Principal" -PrincipalId $user.Id
39-
40-
# Step 3. Assign the app to the user. This ensures that the user can sign in if assignment
41-
# is required, and ensures that the app shows up under the user's My Apps portal.
42-
if ($clientSp.AppRoles | ? { $_.AllowedMemberTypes -contains "User" }) {
43-
Write-Warning ("A default app role assignment cannot be created because the " + "client application exposes user-assignable app roles. You must " + "assign the user a specific app role for the app to be listed " + "in the user's My Apps portal.")
44-
} else {
45-
# The app role ID 00000000-0000-0000-0000-000000000000 is the default app role
46-
# indicating that the app is assigned to the user, but not for any specific
47-
# app role.
48-
$assignment = New-MgServicePrincipalAppRoleAssignedTo -ServicePrincipalId $clientSp.Id -ResourceId $clientSp.Id -PrincipalId $user.Id -AppRoleId "00000000-0000-0000-0000-000000000000"
1+
#requires -Version 7.0
2+
<#
3+
.SYNOPSIS
4+
Audits Microsoft Entra delegated OAuth consent grants without modifying the tenant.
495
6+
.DESCRIPTION
7+
Performs a read-only review of delegated permission grants for a specified client
8+
application, user, or both. The script resolves client and resource service
9+
principals, validates granted scope names against the resource API, identifies
10+
user-specific versus tenant-wide consent, and exports structured CSV evidence.
11+
12+
The script does not create applications, service principals, consent grants,
13+
app-role assignments, access tokens, or refresh tokens.
14+
15+
.PARAMETER ClientAppId
16+
Optional application (client) ID used to limit results to one client application.
17+
18+
.PARAMETER UserPrincipalName
19+
Optional Microsoft Entra user principal name used to limit results to grants made
20+
for one user. Tenant-wide grants are excluded when this filter is specified unless
21+
-IncludeTenantWide is also supplied.
22+
23+
.PARAMETER IncludeTenantWide
24+
When UserPrincipalName is specified, also includes grants whose ConsentType is
25+
AllPrincipals.
26+
27+
.PARAMETER OutputCsv
28+
Path for the CSV report.
29+
30+
.EXAMPLE
31+
./Get-EntraDelegatedConsentGrantAudit.ps1 -ClientAppId '11111111-1111-1111-1111-111111111111'
32+
33+
.EXAMPLE
34+
./Get-EntraDelegatedConsentGrantAudit.ps1 -UserPrincipalName 'alice@contoso.com' -IncludeTenantWide
35+
36+
.EXAMPLE
37+
./Get-EntraDelegatedConsentGrantAudit.ps1 `
38+
-ClientAppId '11111111-1111-1111-1111-111111111111' `
39+
-UserPrincipalName 'alice@contoso.com' `
40+
-OutputCsv ./delegated-consent-audit.csv
41+
42+
.NOTES
43+
Required delegated Microsoft Graph scopes:
44+
- DelegatedPermissionGrant.Read.All
45+
- Application.Read.All
46+
- User.Read.All, only when UserPrincipalName is used
47+
48+
The signed-in administrator must also hold an appropriate Microsoft Entra role.
49+
#>
50+
51+
[CmdletBinding()]
52+
param(
53+
[Parameter()]
54+
[ValidatePattern('^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$')]
55+
[string]$ClientAppId,
56+
57+
[Parameter()]
58+
[ValidatePattern('^[^@\s]+@[^@\s]+\.[^@\s]+$')]
59+
[string]$UserPrincipalName,
60+
61+
[Parameter()]
62+
[switch]$IncludeTenantWide,
63+
64+
[Parameter()]
65+
[string]$OutputCsv = (Join-Path $PWD 'EntraDelegatedConsentGrantAudit.csv')
66+
)
67+
68+
Set-StrictMode -Version Latest
69+
$ErrorActionPreference = 'Stop'
70+
71+
$requiredCommands = @(
72+
'Connect-MgGraph',
73+
'Get-MgContext',
74+
'Get-MgServicePrincipal',
75+
'Get-MgOauth2PermissionGrant'
76+
)
77+
78+
if (-not [string]::IsNullOrWhiteSpace($UserPrincipalName)) {
79+
$requiredCommands += 'Get-MgUser'
80+
}
81+
82+
foreach ($commandName in $requiredCommands) {
83+
if (-not (Get-Command $commandName -ErrorAction SilentlyContinue)) {
84+
throw "Required command '$commandName' was not found. Install Microsoft.Graph.Authentication, Microsoft.Graph.Applications, and Microsoft.Graph.Users."
85+
}
86+
}
87+
88+
$requiredScopes = [System.Collections.Generic.List[string]]::new()
89+
$requiredScopes.Add('DelegatedPermissionGrant.Read.All')
90+
$requiredScopes.Add('Application.Read.All')
91+
if (-not [string]::IsNullOrWhiteSpace($UserPrincipalName)) {
92+
$requiredScopes.Add('User.Read.All')
93+
}
94+
95+
$context = Get-MgContext
96+
$mustConnect = $null -eq $context
97+
if (-not $mustConnect) {
98+
foreach ($scope in $requiredScopes) {
99+
if ($scope -notin @($context.Scopes)) {
100+
$mustConnect = $true
101+
break
102+
}
103+
}
104+
}
105+
106+
if ($mustConnect) {
107+
Write-Host 'Connecting to Microsoft Graph with read-only scopes...' -ForegroundColor Cyan
108+
Connect-MgGraph -Scopes $requiredScopes.ToArray() -NoWelcome | Out-Null
109+
}
110+
111+
$user = $null
112+
if (-not [string]::IsNullOrWhiteSpace($UserPrincipalName)) {
113+
$user = Get-MgUser -UserId $UserPrincipalName -Property Id,DisplayName,UserPrincipalName -ErrorAction Stop
114+
Write-Host "User filter: $($user.DisplayName) <$($user.UserPrincipalName)>" -ForegroundColor DarkGray
115+
}
116+
117+
$clientServicePrincipal = $null
118+
if (-not [string]::IsNullOrWhiteSpace($ClientAppId)) {
119+
$escapedClientAppId = $ClientAppId.Replace("'", "''")
120+
$clientMatches = @(Get-MgServicePrincipal -Filter "appId eq '$escapedClientAppId'" -All -Property Id,AppId,DisplayName,PublisherName,ServicePrincipalType)
121+
122+
if ($clientMatches.Count -eq 0) {
123+
throw "No service principal was found for client application ID '$ClientAppId' in the connected tenant."
124+
}
125+
if ($clientMatches.Count -gt 1) {
126+
throw "Multiple service principals were returned for client application ID '$ClientAppId'."
127+
}
128+
129+
$clientServicePrincipal = $clientMatches[0]
130+
Write-Host "Client filter: $($clientServicePrincipal.DisplayName) <$ClientAppId>" -ForegroundColor DarkGray
131+
}
132+
133+
$allServicePrincipals = @(Get-MgServicePrincipal -All -Property Id,AppId,DisplayName,PublisherName,Oauth2PermissionScopes,ServicePrincipalType)
134+
$servicePrincipalById = @{}
135+
foreach ($servicePrincipal in $allServicePrincipals) {
136+
$servicePrincipalById[[string]$servicePrincipal.Id] = $servicePrincipal
137+
}
138+
139+
$grants = @(Get-MgOauth2PermissionGrant -All)
140+
$filteredGrants = @(
141+
foreach ($grant in $grants) {
142+
if ($null -ne $clientServicePrincipal -and [string]$grant.ClientId -ne [string]$clientServicePrincipal.Id) {
143+
continue
144+
}
145+
146+
if ($null -ne $user) {
147+
$isUserGrant = $grant.ConsentType -eq 'Principal' -and [string]$grant.PrincipalId -eq [string]$user.Id
148+
$isTenantWide = $IncludeTenantWide -and $grant.ConsentType -eq 'AllPrincipals'
149+
if (-not $isUserGrant -and -not $isTenantWide) {
150+
continue
151+
}
152+
}
153+
154+
$grant
155+
}
156+
)
157+
158+
$results = [System.Collections.Generic.List[object]]::new()
159+
160+
foreach ($grant in $filteredGrants) {
161+
$clientSp = $servicePrincipalById[[string]$grant.ClientId]
162+
$resourceSp = $servicePrincipalById[[string]$grant.ResourceId]
163+
164+
$grantedScopes = @(
165+
([string]$grant.Scope -split '\s+') |
166+
Where-Object { -not [string]::IsNullOrWhiteSpace($_) } |
167+
Sort-Object -Unique
168+
)
169+
170+
$definedScopes = @{}
171+
if ($null -ne $resourceSp) {
172+
foreach ($scopeDefinition in @($resourceSp.Oauth2PermissionScopes)) {
173+
if (-not [string]::IsNullOrWhiteSpace([string]$scopeDefinition.Value)) {
174+
$definedScopes[[string]$scopeDefinition.Value] = $scopeDefinition
175+
}
176+
}
177+
}
178+
179+
$unknownScopes = @($grantedScopes | Where-Object { -not $definedScopes.ContainsKey($_) })
180+
$adminConsentScopes = @(
181+
foreach ($scopeName in $grantedScopes) {
182+
if ($definedScopes.ContainsKey($scopeName)) {
183+
$definition = $definedScopes[$scopeName]
184+
if ([string]$definition.Type -eq 'Admin') {
185+
$scopeName
186+
}
187+
}
188+
}
189+
)
190+
191+
$riskFlags = [System.Collections.Generic.List[string]]::new()
192+
if ($grant.ConsentType -eq 'AllPrincipals') {
193+
$riskFlags.Add('TenantWideConsent')
194+
}
195+
if ($adminConsentScopes.Count -gt 0) {
196+
$riskFlags.Add('ContainsAdminConsentScope')
197+
}
198+
if ($unknownScopes.Count -gt 0) {
199+
$riskFlags.Add('UnknownOrRetiredScope')
200+
}
201+
if ($null -eq $clientSp) {
202+
$riskFlags.Add('ClientServicePrincipalNotResolved')
203+
}
204+
if ($null -eq $resourceSp) {
205+
$riskFlags.Add('ResourceServicePrincipalNotResolved')
206+
}
207+
208+
$results.Add([pscustomobject]@{
209+
GrantId = $grant.Id
210+
ConsentType = $grant.ConsentType
211+
PrincipalId = $grant.PrincipalId
212+
IsRequestedUser = $null -ne $user -and [string]$grant.PrincipalId -eq [string]$user.Id
213+
ClientServicePrincipalId = $grant.ClientId
214+
ClientAppId = if ($null -ne $clientSp) { $clientSp.AppId } else { $null }
215+
ClientDisplayName = if ($null -ne $clientSp) { $clientSp.DisplayName } else { $null }
216+
ClientPublisher = if ($null -ne $clientSp) { $clientSp.PublisherName } else { $null }
217+
ResourceServicePrincipalId = $grant.ResourceId
218+
ResourceAppId = if ($null -ne $resourceSp) { $resourceSp.AppId } else { $null }
219+
ResourceDisplayName = if ($null -ne $resourceSp) { $resourceSp.DisplayName } else { $null }
220+
GrantedScopes = $grantedScopes -join ' '
221+
GrantedScopeCount = $grantedScopes.Count
222+
AdminConsentScopes = $adminConsentScopes -join ' '
223+
UnknownOrRetiredScopes = $unknownScopes -join ' '
224+
RiskFlags = $riskFlags -join ';'
225+
})
226+
}
227+
228+
$parentDirectory = Split-Path -Parent $OutputCsv
229+
if (-not [string]::IsNullOrWhiteSpace($parentDirectory)) {
230+
New-Item -Path $parentDirectory -ItemType Directory -Force | Out-Null
50231
}
232+
233+
$results |
234+
Sort-Object ConsentType, ClientDisplayName, ResourceDisplayName |
235+
Export-Csv -Path $OutputCsv -NoTypeInformation -Encoding utf8
236+
237+
Write-Host ''
238+
Write-Host '=== Microsoft Entra Delegated Consent Grant Audit ===' -ForegroundColor Cyan
239+
Write-Host "Grants reviewed : $($results.Count)"
240+
Write-Host "Tenant-wide grants : $(@($results | Where-Object ConsentType -eq 'AllPrincipals').Count)"
241+
Write-Host "User-specific grants : $(@($results | Where-Object ConsentType -eq 'Principal').Count)"
242+
Write-Host "Grants with risk flags : $(@($results | Where-Object { -not [string]::IsNullOrWhiteSpace($_.RiskFlags) }).Count)"
243+
Write-Host "CSV report : $OutputCsv" -ForegroundColor Cyan
244+
Write-Host ''
245+
246+
if ($results.Count -gt 0) {
247+
$results |
248+
Select-Object ClientDisplayName, ResourceDisplayName, ConsentType, GrantedScopeCount, RiskFlags |
249+
Format-Table -AutoSize -Wrap
250+
}
251+
else {
252+
Write-Host 'No delegated permission grants matched the supplied filters.' -ForegroundColor Green
253+
}
254+
255+
Write-Host ''
256+
Write-Host 'Read-only audit complete. No applications, consent grants, assignments, or tokens were created or modified.' -ForegroundColor Green

0 commit comments

Comments
 (0)