ownerlens 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/LICENSE +183 -0
  2. package/README.md +209 -0
  3. package/bin/ownerlens.js +92 -0
  4. package/dist/assets/index-B9aAYpVl.css +1 -0
  5. package/dist/assets/index-BcwLk2bx.js +10 -0
  6. package/dist/index.html +13 -0
  7. package/package.json +73 -0
  8. package/src/App.tsx +18 -0
  9. package/src/components/azure/AzureComponent.test.tsx +625 -0
  10. package/src/components/azure/AzureComponent.tsx +189 -0
  11. package/src/components/azure/AzureRbacComponent.tsx +104 -0
  12. package/src/components/azure/ClosableAzureTab.tsx +42 -0
  13. package/src/components/azure/EntraPermissionsComponent.tsx +194 -0
  14. package/src/components/azure/ManagedIdentityComponent.test.tsx +324 -0
  15. package/src/components/azure/ManagedIdentityComponent.tsx +141 -0
  16. package/src/components/azure/ResourceGroupComponent.tsx +157 -0
  17. package/src/components/azure/ServicePrincipalComponent.test.tsx +457 -0
  18. package/src/components/azure/ServicePrincipalComponent.tsx +155 -0
  19. package/src/components/azure/ServicePrincipalFieldRenderers.tsx +140 -0
  20. package/src/components/azure/ZtaComponent.test.tsx +267 -0
  21. package/src/components/azure/ZtaComponent.tsx +276 -0
  22. package/src/components/azure/ZtaRemediationBadge.tsx +70 -0
  23. package/src/components/azure/api.ts +216 -0
  24. package/src/components/azure/azureReportConfig.ts +247 -0
  25. package/src/core/azure/azureRbac.ts +70 -0
  26. package/src/core/azure/entra/index.ts +1 -0
  27. package/src/core/azure/entra/managedIdentity.ts +21 -0
  28. package/src/core/azure/entra/servicePrincipal.ts +34 -0
  29. package/src/core/azure/entra/types.ts +56 -0
  30. package/src/core/azure/identityEnrichment.ts +65 -0
  31. package/src/core/azure/resources.ts +141 -0
  32. package/src/core/azure/ztaReport.ts +58 -0
  33. package/src/core/config.ts +39 -0
  34. package/src/core/ownership/OwnershipTarget.ts +32 -0
  35. package/src/core/ownership/resolveOwner.ts +5 -0
  36. package/src/core/ownership/types.ts +14 -0
  37. package/src/core/risk/types.ts +1 -0
  38. package/src/core/runtime/index.ts +1 -0
  39. package/src/core/runtime/localSnapshotFiles.ts +74 -0
  40. package/src/core/runtime/rest.ts +61 -0
  41. package/src/lib/searchFilterUtils.ts +17 -0
  42. package/src/lib/utils.ts +48 -0
  43. package/src/main.tsx +10 -0
  44. package/src/providers/azure/identities/azureIdentityTypes.ts +1 -0
  45. package/src/providers/azure/identities/buildAzureManagedIdentityAssignmentIndex.test.ts +32 -0
  46. package/src/providers/azure/identities/buildAzureManagedIdentityAssignmentIndex.ts +35 -0
  47. package/src/providers/azure/identities/userAssignedIdentityAssignments.ts +52 -0
  48. package/src/providers/azure/inputTransferObject/entra/EntraAppRoleAssignment.ts +10 -0
  49. package/src/providers/azure/inputTransferObject/entra/EntraApplication.ts +27 -0
  50. package/src/providers/azure/inputTransferObject/entra/EntraOAuth2PermissionGrant.ts +8 -0
  51. package/src/providers/azure/inputTransferObject/entra/EntraServicePrincipal.ts +43 -0
  52. package/src/providers/azure/inputTransferObject/entra/EntraSnapshot.ts +13 -0
  53. package/src/providers/azure/inputTransferObject/entra/EntraSnapshotMeta.ts +12 -0
  54. package/src/providers/azure/inputTransferObject/resources/AzureActivityLog.ts +1 -0
  55. package/src/providers/azure/inputTransferObject/resources/AzureResource.ts +1 -0
  56. package/src/providers/azure/inputTransferObject/resources/AzureResourceGroup.ts +1 -0
  57. package/src/providers/azure/inputTransferObject/resources/AzureRoleAssignment.ts +1 -0
  58. package/src/providers/azure/inputTransferObject/resources/AzureSnapshot.ts +1 -0
  59. package/src/providers/azure/inputTransferObject/resources/AzureSnapshotMeta.ts +1 -0
  60. package/src/providers/azure/inputTransferObject/resources/AzureSubscription.ts +1 -0
  61. package/src/providers/azure/inputTransferObject/resources/AzureUserAssignedManagedIdentity.ts +1 -0
  62. package/src/providers/azure/ownership/azureActivityOwnershipEvidence.ts +60 -0
  63. package/src/providers/azure/ownership/azureOwnerReportTypes.ts +13 -0
  64. package/src/providers/azure/ownership/azureOwnershipConfig.ts +21 -0
  65. package/src/providers/azure/ownership/azureOwnershipTypes.ts +46 -0
  66. package/src/providers/azure/ownership/buildAzureOwnershipReport.test.ts +99 -0
  67. package/src/providers/azure/ownership/buildAzureOwnershipReport.ts +90 -0
  68. package/src/providers/azure/ownership/buildAzureOwnershipTargets.test.ts +87 -0
  69. package/src/providers/azure/ownership/buildAzureOwnershipTargets.ts +42 -0
  70. package/src/providers/azure/ownership/resolveAzureOwner.ts +146 -0
  71. package/src/providers/azure/runtime/DisabledEvidenceStore.ts +34 -0
  72. package/src/providers/azure/runtime/EnrichmentService.ts +35 -0
  73. package/src/providers/azure/runtime/LocalReportRuntime.test.ts +2318 -0
  74. package/src/providers/azure/runtime/LocalReportRuntime.ts +302 -0
  75. package/src/providers/azure/runtime/RuntimeHost.ts +60 -0
  76. package/src/providers/azure/runtime/SnapshotImporter.ts +44 -0
  77. package/src/providers/azure/runtime/enrichment/azureIdentityEnrichment.ts +523 -0
  78. package/src/providers/azure/runtime/enrichment/azureScopeClassifier.ts +30 -0
  79. package/src/providers/azure/runtime/enrichment/evaluateAzureRoleAssignmentRisk.ts +88 -0
  80. package/src/providers/azure/runtime/entra/EntraCollectionQueryService.ts +307 -0
  81. package/src/providers/azure/runtime/entra/LocalEntraReportRuntime.ts +227 -0
  82. package/src/providers/azure/runtime/entra/appRoleAssignmentsTable.ts +52 -0
  83. package/src/providers/azure/runtime/entra/applicationsTable.ts +175 -0
  84. package/src/providers/azure/runtime/entra/entraServicePrincipalMapper.ts +63 -0
  85. package/src/providers/azure/runtime/entra/localReportRuntimeRest.ts +41 -0
  86. package/src/providers/azure/runtime/entra/oauth2PermissionGrantsTable.ts +48 -0
  87. package/src/providers/azure/runtime/entra/principalProjection.ts +173 -0
  88. package/src/providers/azure/runtime/entra/servicePrincipalsTable.ts +149 -0
  89. package/src/providers/azure/runtime/entra/snapshotMetadataTable.ts +18 -0
  90. package/src/providers/azure/runtime/entra/snapshotStore.ts +102 -0
  91. package/src/providers/azure/runtime/localReportCollections.ts +101 -0
  92. package/src/providers/azure/runtime/localReportRuntimeRest.ts +71 -0
  93. package/src/providers/azure/runtime/resources/AzureResourcesCollectionQueryService.ts +145 -0
  94. package/src/providers/azure/runtime/resources/LocalAzureResourcesReportRuntime.ts +114 -0
  95. package/src/providers/azure/runtime/resources/disabledOwnerEvidenceTable.ts +60 -0
  96. package/src/providers/azure/runtime/resources/localReportRuntimeRest.ts +81 -0
  97. package/src/providers/azure/runtime/resources/resourceGroupOwnership.ts +90 -0
  98. package/src/providers/azure/runtime/resources/snapshotMetadataTable.ts +19 -0
  99. package/src/providers/azure/runtime/resources/snapshotStore.ts +128 -0
  100. package/src/providers/azure/runtime/resources/tables.ts +441 -0
  101. package/src/providers/azure/runtime/runtimeRestQuery.ts +46 -0
  102. package/src/providers/azure/runtime/runtimeSqlSchema.ts +357 -0
  103. package/src/providers/azure/runtime/zta/Discovery.ts +141 -0
  104. package/src/providers/azure/runtime/zta/LocalZeroTrustAssessmentReportRuntime.ts +86 -0
  105. package/src/providers/azure/runtime/zta/ZeroTrustAssessmentQueryService.ts +124 -0
  106. package/src/providers/azure/runtime/zta/localReportRuntimeRest.ts +15 -0
  107. package/src/providers/azure/runtime/zta/snapshotMetadataTable.ts +77 -0
  108. package/src/providers/azure/runtime/zta/snapshotStore.ts +112 -0
  109. package/src/providers/azure/runtime/zta/tables.ts +361 -0
  110. package/src/providers/azure/runtime/zta/types.ts +7 -0
  111. package/src/providers/azure/runtime/zta/ztaReportMapper.ts +12 -0
  112. package/src/report/applyCollectionControls.ts +289 -0
  113. package/src/report/buildCollectionColumns.tsx +38 -0
  114. package/src/report/components/ConfidenceBadge.tsx +10 -0
  115. package/src/report/components/EvidenceList.test.ts +25 -0
  116. package/src/report/components/EvidenceList.tsx +52 -0
  117. package/src/report/components/GenericTable.tsx +373 -0
  118. package/src/report/components/PermissionRiskBadge.tsx +19 -0
  119. package/src/report/components/reportTableControls.test.ts +175 -0
  120. package/src/report/components/reportTableControls.tsx +483 -0
  121. package/src/report/components/ui/badge.tsx +35 -0
  122. package/src/report/components/ui/button.tsx +38 -0
  123. package/src/report/components/ui/card.tsx +23 -0
  124. package/src/report/components/ui/input.tsx +15 -0
  125. package/src/report/components/ui/table.tsx +44 -0
  126. package/src/report/components/ui/tabs.tsx +29 -0
  127. package/src/report/export/csv.ts +34 -0
  128. package/src/report/ownerManualPrecheck.test.ts +137 -0
  129. package/src/report/ownerManualPrecheck.ts +132 -0
  130. package/src/report/reportArchitecture.test.ts +125 -0
  131. package/src/report/reportTypes.ts +54 -0
  132. package/src/report/reportValueRenderers.tsx +54 -0
  133. package/src/report/runtimeCollectionQuery.ts +23 -0
  134. package/src/report/types.ts +14 -0
  135. package/src/styles.css +43 -0
  136. package/tools/README.md +108 -0
  137. package/tools/azure-activity-check.ps1 +164 -0
  138. package/tools/collect-azure.ps1 +54 -0
  139. package/tools/collect-entra.ps1 +47 -0
  140. package/tools/collect-scripts.test.ts +22 -0
  141. package/tools/prepare-entra-snapshot.ps1 +403 -0
  142. package/tools/prepare-entra-snapshot.test.ts +14 -0
  143. package/tools/prepare-resource-snapshot.ps1 +345 -0
  144. package/vite.config.ts +23 -0
@@ -0,0 +1,403 @@
1
+ param(
2
+ [string]$OutputPath = ".\data\entra-snapshot.json"
3
+ )
4
+
5
+ $requiredGraphModules = @(
6
+ "Microsoft.Graph.Authentication",
7
+ "Microsoft.Graph.Applications"
8
+ )
9
+
10
+ foreach ($moduleName in $requiredGraphModules) {
11
+ try {
12
+ Import-Module $moduleName -ErrorAction Stop
13
+ } catch {
14
+ throw "Microsoft Graph PowerShell module missing: $moduleName. Install: Install-Module Microsoft.Graph -Scope CurrentUser"
15
+ }
16
+ }
17
+
18
+ $context = Get-MgContext
19
+
20
+ if (-not $context) {
21
+ throw 'Not connected. Run: Connect-MgGraph -TenantId "<tenant-id>" -Scopes "Application.Read.All","Group.Read.All","Directory.Read.All"'
22
+ }
23
+
24
+ $snapshot = [ordered]@{
25
+ meta = [ordered]@{
26
+ provider = "entra"
27
+ snapshotVersion = "0.4"
28
+ createdAt = (Get-Date).ToUniversalTime().ToString("o")
29
+ tenantId = $context.TenantId
30
+ account = $context.Account
31
+ scopes = $context.Scopes
32
+ }
33
+ servicePrincipals = @()
34
+ applications = @()
35
+ oauth2PermissionGrants = @()
36
+ appRoleAssignments = @()
37
+ groups = @()
38
+ }
39
+
40
+ function Get-DirectoryObjectSnapshotValue {
41
+ param(
42
+ [Parameter(Mandatory = $true)]
43
+ $DirectoryObject,
44
+
45
+ [Parameter(Mandatory = $true)]
46
+ [string]$Name
47
+ )
48
+
49
+ $value = $DirectoryObject.$Name
50
+
51
+ if ($null -ne $value) {
52
+ return $value
53
+ }
54
+
55
+ $camelName = $Name.Substring(0, 1).ToLowerInvariant() + $Name.Substring(1)
56
+ if (-not $DirectoryObject.AdditionalProperties) {
57
+ return $null
58
+ }
59
+
60
+ return $DirectoryObject.AdditionalProperties[$camelName]
61
+ }
62
+
63
+ function ConvertTo-OwnerSnapshot {
64
+ param(
65
+ [Parameter(Mandatory = $true)]
66
+ $Owner
67
+ )
68
+
69
+ [pscustomobject]@{
70
+ id = Get-DirectoryObjectSnapshotValue -DirectoryObject $Owner -Name "Id"
71
+ displayName = Get-DirectoryObjectSnapshotValue -DirectoryObject $Owner -Name "DisplayName"
72
+ userPrincipalName = Get-DirectoryObjectSnapshotValue -DirectoryObject $Owner -Name "UserPrincipalName"
73
+ mail = Get-DirectoryObjectSnapshotValue -DirectoryObject $Owner -Name "Mail"
74
+ ownerType = if ($Owner.AdditionalProperties) { $Owner.AdditionalProperties["@odata.type"] } else { $null }
75
+ }
76
+ }
77
+
78
+ function ConvertTo-SnapshotPropertyName {
79
+ param(
80
+ [Parameter(Mandatory = $true)]
81
+ [string]$Name
82
+ )
83
+
84
+ if ($Name.Length -le 1) {
85
+ return $Name.ToLowerInvariant()
86
+ }
87
+
88
+ return $Name.Substring(0, 1).ToLowerInvariant() + $Name.Substring(1)
89
+ }
90
+
91
+ function ConvertTo-SafeSnapshotValue {
92
+ param(
93
+ $Value
94
+ )
95
+
96
+ if ($null -eq $Value) {
97
+ return $null
98
+ }
99
+
100
+ if (
101
+ $Value -is [string] -or
102
+ $Value -is [bool] -or
103
+ $Value -is [byte] -or
104
+ $Value -is [int] -or
105
+ $Value -is [long] -or
106
+ $Value -is [decimal] -or
107
+ $Value -is [double] -or
108
+ $Value -is [single]
109
+ ) {
110
+ return $Value
111
+ }
112
+
113
+ if ($Value -is [datetime]) {
114
+ return $Value.ToUniversalTime().ToString("o")
115
+ }
116
+
117
+ if ($Value -is [datetimeoffset]) {
118
+ return $Value.ToUniversalTime().ToString("o")
119
+ }
120
+
121
+ if ($Value -is [guid]) {
122
+ return $Value.ToString()
123
+ }
124
+
125
+ if ($Value -is [System.Collections.IDictionary]) {
126
+ $converted = [ordered]@{}
127
+
128
+ foreach ($key in $Value.Keys) {
129
+ if ([string]$key -ieq "secretText") {
130
+ continue
131
+ }
132
+
133
+ $converted[$key] = ConvertTo-SafeSnapshotValue -Value $Value[$key]
134
+ }
135
+
136
+ return [pscustomobject]$converted
137
+ }
138
+
139
+ if ($Value -is [System.Collections.IEnumerable] -and -not ($Value -is [string])) {
140
+ return @(
141
+ foreach ($item in $Value) {
142
+ ConvertTo-SafeSnapshotValue -Value $item
143
+ }
144
+ )
145
+ }
146
+
147
+ $convertedObject = [ordered]@{}
148
+
149
+ foreach ($property in $Value.PSObject.Properties) {
150
+ if ($property.Name -ieq "secretText") {
151
+ continue
152
+ }
153
+
154
+ $convertedObject[(ConvertTo-SnapshotPropertyName -Name $property.Name)] = ConvertTo-SafeSnapshotValue -Value $property.Value
155
+ }
156
+
157
+ return [pscustomobject]$convertedObject
158
+ }
159
+
160
+ function ConvertTo-ApplicationAppRoleSnapshot {
161
+ param(
162
+ [Parameter(Mandatory = $true)]
163
+ $AppRole
164
+ )
165
+
166
+ [pscustomobject]@{
167
+ id = $AppRole.Id
168
+ value = $AppRole.Value
169
+ displayName = $AppRole.DisplayName
170
+ description = $AppRole.Description
171
+ isEnabled = $AppRole.IsEnabled
172
+ allowedMemberTypes = $AppRole.AllowedMemberTypes
173
+ }
174
+ }
175
+
176
+ $oauth2PermissionGrantIds = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
177
+
178
+ function Add-OAuth2PermissionGrantSnapshot {
179
+ param(
180
+ [Parameter(Mandatory = $true)]
181
+ $Grant
182
+ )
183
+
184
+ if ([string]::IsNullOrWhiteSpace([string]$Grant.Id)) {
185
+ return
186
+ }
187
+
188
+ if (-not $oauth2PermissionGrantIds.Add([string]$Grant.Id)) {
189
+ return
190
+ }
191
+
192
+ $snapshot.oauth2PermissionGrants += [pscustomobject]@{
193
+ id = $Grant.Id
194
+ clientId = $Grant.ClientId
195
+ consentType = $Grant.ConsentType
196
+ principalId = $Grant.PrincipalId
197
+ resourceId = $Grant.ResourceId
198
+ scope = $Grant.Scope
199
+ }
200
+ }
201
+
202
+ $servicePrincipals = Get-MgServicePrincipal `
203
+ -All `
204
+ -Property Id,AppId,DisplayName,ServicePrincipalType,PublisherName,AccountEnabled,AppOwnerOrganizationId,AppDisplayName,Homepage,LoginUrl,ReplyUrls,ServicePrincipalNames,Tags,AppRoles
205
+
206
+ $servicePrincipalById = @{}
207
+
208
+ foreach ($sp in $servicePrincipals) {
209
+ $servicePrincipalById[$sp.Id] = $sp
210
+ }
211
+
212
+ foreach ($sp in $servicePrincipals) {
213
+ $servicePrincipalOwners = @(
214
+ Get-MgServicePrincipalOwner `
215
+ -ServicePrincipalId $sp.Id `
216
+ -All `
217
+ -Property Id,DisplayName,UserPrincipalName,Mail |
218
+ ForEach-Object { ConvertTo-OwnerSnapshot -Owner $_ }
219
+ )
220
+
221
+ $snapshot.servicePrincipals += [pscustomobject]@{
222
+ id = $sp.Id
223
+ appId = $sp.AppId
224
+ displayName = $sp.DisplayName
225
+ appDisplayName = $sp.AppDisplayName
226
+ servicePrincipalType = $sp.ServicePrincipalType
227
+ publisherName = $sp.PublisherName
228
+ accountEnabled = $sp.AccountEnabled
229
+ appOwnerOrganizationId = $sp.AppOwnerOrganizationId
230
+ homepage = $sp.Homepage
231
+ loginUrl = $sp.LoginUrl
232
+ replyUrls = $sp.ReplyUrls
233
+ servicePrincipalNames = $sp.ServicePrincipalNames
234
+ tags = $sp.Tags
235
+ servicePrincipalOwners = $servicePrincipalOwners
236
+ appRoles = @(
237
+ $sp.AppRoles | ForEach-Object {
238
+ [pscustomobject]@{
239
+ id = $_.Id
240
+ value = $_.Value
241
+ displayName = $_.DisplayName
242
+ description = $_.Description
243
+ isEnabled = $_.IsEnabled
244
+ allowedMemberTypes = $_.AllowedMemberTypes
245
+ }
246
+ }
247
+ )
248
+ }
249
+ }
250
+
251
+ $applications = Get-MgApplication `
252
+ -All `
253
+ -Property Id,AppId,DisplayName,SignInAudience,PublisherDomain,IdentifierUris,Tags,AppRoles,Api,RequiredResourceAccess,Web,Spa,PublicClient,PasswordCredentials,KeyCredentials,CreatedDateTime,DeletedDateTime,DisabledByMicrosoftStatus,Info,Notes
254
+
255
+ foreach ($app in $applications) {
256
+ $applicationOwners = @(
257
+ Get-MgApplicationOwner `
258
+ -ApplicationId $app.Id `
259
+ -All `
260
+ -Property Id,DisplayName,UserPrincipalName,Mail |
261
+ ForEach-Object { ConvertTo-OwnerSnapshot -Owner $_ }
262
+ )
263
+
264
+ $snapshot.applications += [pscustomobject]@{
265
+ id = $app.Id
266
+ appId = $app.AppId
267
+ displayName = $app.DisplayName
268
+ signInAudience = $app.SignInAudience
269
+ publisherDomain = $app.PublisherDomain
270
+ identifierUris = $app.IdentifierUris
271
+ tags = $app.Tags
272
+ appRoles = @($app.AppRoles | ForEach-Object { ConvertTo-ApplicationAppRoleSnapshot -AppRole $_ })
273
+ oauth2PermissionScopes = @($app.Api.Oauth2PermissionScopes | ForEach-Object { ConvertTo-SafeSnapshotValue -Value $_ })
274
+ requiredResourceAccess = @($app.RequiredResourceAccess | ForEach-Object { ConvertTo-SafeSnapshotValue -Value $_ })
275
+ web = ConvertTo-SafeSnapshotValue -Value $app.Web
276
+ spa = ConvertTo-SafeSnapshotValue -Value $app.Spa
277
+ publicClient = ConvertTo-SafeSnapshotValue -Value $app.PublicClient
278
+ passwordCredentials = @($app.PasswordCredentials | ForEach-Object { ConvertTo-SafeSnapshotValue -Value $_ })
279
+ keyCredentials = @($app.KeyCredentials | ForEach-Object { ConvertTo-SafeSnapshotValue -Value $_ })
280
+ createdDateTime = if ($app.CreatedDateTime) { $app.CreatedDateTime.ToUniversalTime().ToString("o") } else { $null }
281
+ deletedDateTime = if ($app.DeletedDateTime) { $app.DeletedDateTime.ToUniversalTime().ToString("o") } else { $null }
282
+ disabledByMicrosoftStatus = $app.DisabledByMicrosoftStatus
283
+ info = ConvertTo-SafeSnapshotValue -Value $app.Info
284
+ notes = $app.Notes
285
+ owners = $applicationOwners
286
+ }
287
+ }
288
+
289
+ $globalOAuth2PermissionGrantCommand = Get-Command Get-MgOauth2PermissionGrant -ErrorAction SilentlyContinue
290
+
291
+ if ($globalOAuth2PermissionGrantCommand) {
292
+ try {
293
+ $oauth2PermissionGrants = Get-MgOauth2PermissionGrant `
294
+ -All `
295
+ -Property Id,ClientId,ConsentType,PrincipalId,ResourceId,Scope `
296
+ -ErrorAction Stop
297
+
298
+ foreach ($grant in $oauth2PermissionGrants) {
299
+ Add-OAuth2PermissionGrantSnapshot -Grant $grant
300
+ }
301
+ } catch {
302
+ Write-Warning "Global OAuth2 permission grant query failed. Falling back to per-service-principal queries. $($_.Exception.Message)"
303
+ }
304
+ }
305
+
306
+ foreach ($sp in $servicePrincipals) {
307
+ $servicePrincipalOauth2PermissionGrants = Get-MgServicePrincipalOauth2PermissionGrant `
308
+ -ServicePrincipalId $sp.Id `
309
+ -All `
310
+ -Property Id,ClientId,ConsentType,PrincipalId,ResourceId,Scope `
311
+ -ErrorAction Stop
312
+
313
+ foreach ($grant in $servicePrincipalOauth2PermissionGrants) {
314
+ Add-OAuth2PermissionGrantSnapshot -Grant $grant
315
+ }
316
+ }
317
+
318
+ foreach ($sp in $servicePrincipals) {
319
+ $assignments = Get-MgServicePrincipalAppRoleAssignment `
320
+ -ServicePrincipalId $sp.Id `
321
+ -All
322
+
323
+ foreach ($assignment in $assignments) {
324
+ $resourceServicePrincipal = $servicePrincipalById[$assignment.ResourceId]
325
+ $appRole = $null
326
+
327
+ if ($resourceServicePrincipal -and $resourceServicePrincipal.AppRoles) {
328
+ $appRole = $resourceServicePrincipal.AppRoles | Where-Object { [string]$_.Id -eq [string]$assignment.AppRoleId } | Select-Object -First 1
329
+ }
330
+
331
+ $snapshot.appRoleAssignments += [pscustomobject]@{
332
+ id = $assignment.Id
333
+ appRoleId = $assignment.AppRoleId
334
+ appRoleDisplayName = if ($appRole) { $appRole.DisplayName } else { $null }
335
+ appRoleValue = if ($appRole) { $appRole.Value } else { $null }
336
+ principalId = $assignment.PrincipalId
337
+ principalDisplayName = $assignment.PrincipalDisplayName
338
+ resourceId = $assignment.ResourceId
339
+ resourceDisplayName = $assignment.ResourceDisplayName
340
+ }
341
+ }
342
+ }
343
+
344
+ $canReadGroups = $false
345
+
346
+ try {
347
+ Import-Module Microsoft.Graph.Groups -ErrorAction Stop
348
+ $canReadGroups = $true
349
+ } catch {
350
+ Write-Warning "Microsoft.Graph.Groups could not be loaded. Skipping group snapshot. $($_.Exception.Message)"
351
+ }
352
+
353
+ if ($canReadGroups) {
354
+ $groups = Get-MgGroup `
355
+ -All `
356
+ -Property Id,DisplayName,Description,Mail,MailEnabled,SecurityEnabled,GroupTypes,ProxyAddresses,Visibility
357
+
358
+ foreach ($group in $groups) {
359
+ $members = Get-MgGroupMember `
360
+ -GroupId $group.Id `
361
+ -All
362
+
363
+ $memberEmails = @(
364
+ $members | ForEach-Object {
365
+ $mail = $_.AdditionalProperties["mail"]
366
+ $userPrincipalName = $_.AdditionalProperties["userPrincipalName"]
367
+
368
+ if ($mail) {
369
+ $mail
370
+ } elseif ($userPrincipalName) {
371
+ $userPrincipalName
372
+ }
373
+ } | Where-Object { $_ } | Select-Object -Unique
374
+ )
375
+
376
+ $snapshot.groups += [pscustomobject]@{
377
+ id = $group.Id
378
+ displayName = $group.DisplayName
379
+ description = $group.Description
380
+ mail = $group.Mail
381
+ mailEnabled = $group.MailEnabled
382
+ securityEnabled = $group.SecurityEnabled
383
+ groupTypes = $group.GroupTypes
384
+ proxyAddresses = $group.ProxyAddresses
385
+ visibility = $group.Visibility
386
+ memberEmails = $memberEmails
387
+ memberEmailCount = $memberEmails.Count
388
+ }
389
+ }
390
+ }
391
+
392
+ $snapshot.meta.servicePrincipalCount = $snapshot.servicePrincipals.Count
393
+ $snapshot.meta.applicationCount = $snapshot.applications.Count
394
+ $snapshot.meta.oauth2PermissionGrantCount = $snapshot.oauth2PermissionGrants.Count
395
+ $snapshot.meta.appRoleAssignmentCount = $snapshot.appRoleAssignments.Count
396
+ $snapshot.meta.groupCount = $snapshot.groups.Count
397
+
398
+ $outputDirectory = Split-Path -Parent $OutputPath
399
+ if (-not [string]::IsNullOrWhiteSpace($outputDirectory) -and -not (Test-Path $outputDirectory)) {
400
+ New-Item -ItemType Directory -Path $outputDirectory -Force | Out-Null
401
+ }
402
+
403
+ $snapshot | ConvertTo-Json -Depth 20 | Out-File $OutputPath -Encoding utf8
@@ -0,0 +1,14 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ const script = readFileSync(join(process.cwd(), "tools/prepare-entra-snapshot.ps1"), "utf8");
5
+
6
+ test("Entra snapshot preparation imports Graph modules used for permission grants", () => {
7
+ expect(script).toContain('"Microsoft.Graph.Applications"');
8
+ expect(script).toContain("Import-Module $moduleName -ErrorAction Stop");
9
+ });
10
+
11
+ test("Entra snapshot preparation reads delegated grants per service principal", () => {
12
+ expect(script).toContain("Get-MgServicePrincipalOauth2PermissionGrant");
13
+ expect(script).toContain("Add-OAuth2PermissionGrantSnapshot");
14
+ });