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,523 @@
1
+ import { randomUUID } from "node:crypto";
2
+
3
+ import type { DuckDBConnection } from "@duckdb/node-api";
4
+
5
+ import {
6
+ AZURE_ACCESS_RISK_RANK,
7
+ type AzureIdentityEnrichmentStatus,
8
+ type AzureManagedIdentityAssignmentEnrichment,
9
+ type AzureRoleAssignmentEnrichment,
10
+ type LatestAzureIdentityEnrichment,
11
+ type ManagedIdentityPermissionRiskAssignment,
12
+ type ManagedIdentityPermissionRiskLevel,
13
+ type ManagedIdentityPermissionRiskSummary
14
+ } from "../../../../core/azure/identityEnrichment";
15
+ import type { AzureResource, AzureRoleAssignment } from "../../../../core/azure/resources";
16
+ import { isBroadAzureScope } from "./azureScopeClassifier";
17
+ import { evaluateAzureRoleAssignmentRisk } from "./evaluateAzureRoleAssignmentRisk";
18
+ import { getResourceManagedIdentityAssignments } from "../../identities/buildAzureManagedIdentityAssignmentIndex";
19
+ import type { AzureManagedIdentityResourceAssignment } from "../../identities/azureIdentityTypes";
20
+ import type { EntraServicePrincipal } from "../../inputTransferObject/entra/EntraServicePrincipal";
21
+ import { readEntraServicePrincipalRows } from "../entra/servicePrincipalsTable";
22
+ import { readAzureResourceRows, readAzureRoleAssignmentRows } from "../resources/tables";
23
+
24
+ export type {
25
+ AzureIdentityEnrichmentStatus,
26
+ AzureManagedIdentityAssignmentEnrichment,
27
+ AzureRoleAssignmentEnrichment,
28
+ LatestAzureIdentityEnrichment
29
+ } from "../../../../core/azure/identityEnrichment";
30
+
31
+ const emptyStatus: AzureIdentityEnrichmentStatus = {
32
+ calculated: false,
33
+ latestRunId: null,
34
+ identityRoleAssignmentCount: 0,
35
+ accessRiskIdentityCount: 0,
36
+ managedIdentityAssignmentCount: 0,
37
+ calculatedAt: null
38
+ };
39
+
40
+ export async function recalculateAzureIdentityEnrichment(
41
+ connection: DuckDBConnection
42
+ ): Promise<AzureIdentityEnrichmentStatus> {
43
+ const runId = randomUUID();
44
+ const startedAt = new Date().toISOString();
45
+
46
+ await connection.run(
47
+ `insert into azure_runtime_enrichment_runs values (
48
+ $runId, $startedAt, null, 'running', 0, 0, 0, null
49
+ )`,
50
+ { runId, startedAt }
51
+ );
52
+
53
+ try {
54
+ const servicePrincipals = await readEntraServicePrincipalRows(connection);
55
+ const roleAssignments = await readOptionalRows(connection, readAzureRoleAssignmentRows);
56
+ const resources = await readOptionalRows(connection, readAzureResourceRows);
57
+ const roleEnrichment = buildRoleAssignmentEnrichment(servicePrincipals, roleAssignments);
58
+ const accessRiskEnrichment = buildAccessRiskEnrichment(servicePrincipals, roleAssignments);
59
+ const managedIdentityEnrichment = buildManagedIdentityAssignmentEnrichment(servicePrincipals, resources);
60
+ const completedAt = new Date().toISOString();
61
+
62
+ await connection.run("begin transaction");
63
+ try {
64
+ await insertRoleAssignmentEnrichmentRows(connection, runId, roleEnrichment);
65
+ await insertAccessRiskEnrichmentRows(connection, runId, accessRiskEnrichment);
66
+ await insertManagedIdentityAssignmentEnrichmentRows(connection, runId, managedIdentityEnrichment);
67
+ await connection.run(
68
+ `update azure_runtime_enrichment_runs
69
+ set completed_at = $completedAt,
70
+ status = 'completed',
71
+ identity_role_assignment_count = $identityRoleAssignmentCount,
72
+ access_risk_identity_count = $accessRiskIdentityCount,
73
+ managed_identity_assignment_count = $managedIdentityAssignmentCount
74
+ where run_id = $runId`,
75
+ {
76
+ runId,
77
+ completedAt,
78
+ identityRoleAssignmentCount: sumRoleAssignments(roleEnrichment),
79
+ accessRiskIdentityCount: accessRiskEnrichment.length,
80
+ managedIdentityAssignmentCount: sumManagedIdentityAssignments(managedIdentityEnrichment)
81
+ }
82
+ );
83
+ await connection.run("commit");
84
+ } catch (error) {
85
+ await connection.run("rollback");
86
+ throw error;
87
+ }
88
+
89
+ return {
90
+ calculated: true,
91
+ latestRunId: runId,
92
+ identityRoleAssignmentCount: sumRoleAssignments(roleEnrichment),
93
+ accessRiskIdentityCount: accessRiskEnrichment.length,
94
+ managedIdentityAssignmentCount: sumManagedIdentityAssignments(managedIdentityEnrichment),
95
+ calculatedAt: completedAt
96
+ };
97
+ } catch (error) {
98
+ await connection.run(
99
+ `update azure_runtime_enrichment_runs
100
+ set completed_at = $completedAt, status = 'failed', error_message = $errorMessage
101
+ where run_id = $runId`,
102
+ {
103
+ runId,
104
+ completedAt: new Date().toISOString(),
105
+ errorMessage: error instanceof Error ? error.message : String(error)
106
+ }
107
+ );
108
+ throw error;
109
+ }
110
+ }
111
+
112
+ export async function readAzureIdentityEnrichmentStatus(
113
+ connection: DuckDBConnection
114
+ ): Promise<AzureIdentityEnrichmentStatus> {
115
+ const rows = await readRows<AzureRuntimeEnrichmentRunRow>(
116
+ connection,
117
+ `select run_id, completed_at, identity_role_assignment_count, access_risk_identity_count,
118
+ managed_identity_assignment_count
119
+ from azure_runtime_enrichment_runs
120
+ where status = 'completed'
121
+ order by completed_at desc
122
+ limit 1`
123
+ );
124
+ const row = rows[0];
125
+
126
+ return row
127
+ ? {
128
+ calculated: true,
129
+ latestRunId: row.run_id,
130
+ identityRoleAssignmentCount: row.identity_role_assignment_count,
131
+ accessRiskIdentityCount: row.access_risk_identity_count,
132
+ managedIdentityAssignmentCount: row.managed_identity_assignment_count,
133
+ calculatedAt: row.completed_at
134
+ }
135
+ : emptyStatus;
136
+ }
137
+
138
+ export async function readLatestAzureIdentityEnrichment(
139
+ connection: DuckDBConnection
140
+ ): Promise<LatestAzureIdentityEnrichment> {
141
+ const status = await readAzureIdentityEnrichmentStatus(connection);
142
+ if (!status.latestRunId) {
143
+ return {
144
+ status,
145
+ roleAssignmentsByPrincipalId: new Map(),
146
+ accessRiskByPrincipalId: new Map(),
147
+ managedIdentityAssignmentsByServicePrincipalId: new Map()
148
+ };
149
+ }
150
+
151
+ const roleRows = await readRows<AzureIdentityRoleAssignmentEnrichmentRow>(
152
+ connection,
153
+ `select principal_id, assignment_count, role_assignments
154
+ from azure_identity_role_assignment_enrichment
155
+ where run_id = '${status.latestRunId}'`
156
+ );
157
+ const riskRows = await readRows<AzureIdentityAccessRiskEnrichmentRow>(
158
+ connection,
159
+ `select principal_id, risk_level, assignment_count, high_risk_assignment_count,
160
+ broad_scope_assignment_count, role_assignments
161
+ from azure_identity_access_risk_enrichment
162
+ where run_id = '${status.latestRunId}'`
163
+ );
164
+ const managedIdentityRows = await readRows<AzureManagedIdentityAssignmentEnrichmentRow>(
165
+ connection,
166
+ `select service_principal_id, principal_id, client_id, assignment_count, assigned_resource_groups,
167
+ managed_identity_assignments
168
+ from azure_managed_identity_assignment_enrichment
169
+ where run_id = '${status.latestRunId}'`
170
+ );
171
+
172
+ return {
173
+ status,
174
+ roleAssignmentsByPrincipalId: new Map(
175
+ roleRows.map((row) => [
176
+ normalizeKey(row.principal_id),
177
+ {
178
+ principalId: row.principal_id,
179
+ assignmentCount: row.assignment_count,
180
+ roleAssignments: parseJsonArray<AzureRoleAssignment>(row.role_assignments)
181
+ }
182
+ ])
183
+ ),
184
+ accessRiskByPrincipalId: new Map(
185
+ riskRows.map((row) => [
186
+ normalizeKey(row.principal_id),
187
+ {
188
+ principalId: row.principal_id,
189
+ riskLevel: row.risk_level,
190
+ assignmentCount: row.assignment_count,
191
+ highRiskAssignmentCount: row.high_risk_assignment_count,
192
+ broadScopeAssignmentCount: row.broad_scope_assignment_count,
193
+ roleAssignments: parseJsonArray<ManagedIdentityPermissionRiskAssignment>(row.role_assignments)
194
+ }
195
+ ])
196
+ ),
197
+ managedIdentityAssignmentsByServicePrincipalId: new Map(
198
+ managedIdentityRows.map((row) => [
199
+ normalizeKey(row.service_principal_id),
200
+ {
201
+ servicePrincipalId: row.service_principal_id,
202
+ principalId: row.principal_id,
203
+ clientId: row.client_id,
204
+ assignmentCount: row.assignment_count,
205
+ assignedResourceGroups: parseJsonArray<string>(row.assigned_resource_groups),
206
+ managedIdentityAssignments: parseJsonArray<AzureManagedIdentityResourceAssignment>(
207
+ row.managed_identity_assignments
208
+ )
209
+ }
210
+ ])
211
+ )
212
+ };
213
+ }
214
+
215
+ function buildRoleAssignmentEnrichment(
216
+ servicePrincipals: EntraServicePrincipal[],
217
+ roleAssignments: AzureRoleAssignment[]
218
+ ): AzureRoleAssignmentEnrichment[] {
219
+ const identityIds = new Set(servicePrincipals.map((servicePrincipal) => normalizeKey(servicePrincipal.id)));
220
+ const assignmentsByPrincipalId = new Map<string, AzureRoleAssignment[]>();
221
+
222
+ for (const assignment of roleAssignments) {
223
+ const principalId = normalizeKey(assignment.principalId);
224
+ if (!identityIds.has(principalId)) {
225
+ continue;
226
+ }
227
+
228
+ const assignments = assignmentsByPrincipalId.get(principalId) ?? [];
229
+ assignments.push(assignment);
230
+ assignmentsByPrincipalId.set(principalId, assignments);
231
+ }
232
+
233
+ return [...assignmentsByPrincipalId.entries()]
234
+ .map(([principalId, assignments]) => ({
235
+ principalId,
236
+ assignmentCount: assignments.length,
237
+ roleAssignments: assignments.sort(compareRoleAssignments)
238
+ }))
239
+ .sort((left, right) => left.principalId.localeCompare(right.principalId));
240
+ }
241
+
242
+ function buildAccessRiskEnrichment(
243
+ servicePrincipals: EntraServicePrincipal[],
244
+ roleAssignments: AzureRoleAssignment[]
245
+ ): ManagedIdentityPermissionRiskSummary[] {
246
+ const summariesByPrincipalId = new Map<string, ManagedIdentityPermissionRiskSummary>();
247
+
248
+ for (const servicePrincipal of servicePrincipals) {
249
+ summariesByPrincipalId.set(normalizeKey(servicePrincipal.id), createRiskSummary(servicePrincipal.id));
250
+ }
251
+
252
+ for (const assignment of roleAssignments) {
253
+ const normalizedPrincipalId = normalizeKey(assignment.principalId);
254
+ const summary = summariesByPrincipalId.get(normalizedPrincipalId);
255
+ if (!summary) {
256
+ continue;
257
+ }
258
+
259
+ const riskAssignment = evaluateAzureRoleAssignmentRisk(assignment);
260
+ summary.roleAssignments.push(riskAssignment);
261
+ summary.assignmentCount += 1;
262
+ summary.riskLevel = maxRisk(summary.riskLevel, riskAssignment.riskLevel);
263
+
264
+ if (riskAssignment.riskLevel === "high") {
265
+ summary.highRiskAssignmentCount += 1;
266
+ }
267
+ if (isBroadAzureScope(assignment)) {
268
+ summary.broadScopeAssignmentCount += 1;
269
+ }
270
+ }
271
+
272
+ for (const summary of summariesByPrincipalId.values()) {
273
+ summary.roleAssignments.sort(compareRiskAssignments);
274
+ }
275
+
276
+ return [...summariesByPrincipalId.values()].sort((left, right) => left.principalId.localeCompare(right.principalId));
277
+ }
278
+
279
+ function buildManagedIdentityAssignmentEnrichment(
280
+ servicePrincipals: EntraServicePrincipal[],
281
+ resources: AzureResource[]
282
+ ): AzureManagedIdentityAssignmentEnrichment[] {
283
+ const assignmentsByKey = new Map<string, AzureManagedIdentityResourceAssignment[]>();
284
+
285
+ for (const resource of resources) {
286
+ for (const assignment of getResourceManagedIdentityAssignments(resource)) {
287
+ addManagedIdentityAssignment(assignmentsByKey, assignment.clientId, assignment);
288
+ addManagedIdentityAssignment(assignmentsByKey, assignment.principalId, assignment);
289
+ }
290
+ }
291
+
292
+ return servicePrincipals
293
+ .filter(isManagedIdentity)
294
+ .map((servicePrincipal) => {
295
+ const assignments = new Map<string, AzureManagedIdentityResourceAssignment>();
296
+ for (const key of [servicePrincipal.id, servicePrincipal.appId]) {
297
+ for (const assignment of assignmentsByKey.get(normalizeKey(key)) ?? []) {
298
+ assignments.set(`${assignment.assignedResourceId}:${assignment.resourceId}`, assignment);
299
+ }
300
+ }
301
+
302
+ const managedIdentityAssignments = [...assignments.values()].sort(compareManagedIdentityAssignments);
303
+ return {
304
+ servicePrincipalId: servicePrincipal.id,
305
+ principalId: servicePrincipal.id,
306
+ clientId: servicePrincipal.appId,
307
+ assignmentCount: managedIdentityAssignments.length,
308
+ assignedResourceGroups: uniqueSorted(managedIdentityAssignments.map((assignment) => assignment.assignedResourceGroup)),
309
+ managedIdentityAssignments
310
+ };
311
+ })
312
+ .sort((left, right) => left.servicePrincipalId.localeCompare(right.servicePrincipalId));
313
+ }
314
+
315
+ async function insertRoleAssignmentEnrichmentRows(
316
+ connection: DuckDBConnection,
317
+ runId: string,
318
+ rows: AzureRoleAssignmentEnrichment[]
319
+ ): Promise<void> {
320
+ for (const row of rows) {
321
+ await connection.run(
322
+ `insert into azure_identity_role_assignment_enrichment values (
323
+ $runId, $principalId, $assignmentCount, $roleAssignments::json
324
+ )`,
325
+ {
326
+ runId,
327
+ principalId: row.principalId,
328
+ assignmentCount: row.assignmentCount,
329
+ roleAssignments: JSON.stringify(row.roleAssignments)
330
+ }
331
+ );
332
+ }
333
+ }
334
+
335
+ async function insertAccessRiskEnrichmentRows(
336
+ connection: DuckDBConnection,
337
+ runId: string,
338
+ rows: ManagedIdentityPermissionRiskSummary[]
339
+ ): Promise<void> {
340
+ for (const row of rows) {
341
+ await connection.run(
342
+ `insert into azure_identity_access_risk_enrichment values (
343
+ $runId, $principalId, $riskLevel, $assignmentCount, $highRiskAssignmentCount,
344
+ $broadScopeAssignmentCount, $roleAssignments::json
345
+ )`,
346
+ {
347
+ runId,
348
+ principalId: row.principalId,
349
+ riskLevel: row.riskLevel,
350
+ assignmentCount: row.assignmentCount,
351
+ highRiskAssignmentCount: row.highRiskAssignmentCount,
352
+ broadScopeAssignmentCount: row.broadScopeAssignmentCount,
353
+ roleAssignments: JSON.stringify(row.roleAssignments)
354
+ }
355
+ );
356
+ }
357
+ }
358
+
359
+ async function insertManagedIdentityAssignmentEnrichmentRows(
360
+ connection: DuckDBConnection,
361
+ runId: string,
362
+ rows: AzureManagedIdentityAssignmentEnrichment[]
363
+ ): Promise<void> {
364
+ for (const row of rows) {
365
+ await connection.run(
366
+ `insert into azure_managed_identity_assignment_enrichment values (
367
+ $runId, $servicePrincipalId, $principalId, $clientId, $assignmentCount,
368
+ $assignedResourceGroups::json, $managedIdentityAssignments::json
369
+ )`,
370
+ {
371
+ runId,
372
+ servicePrincipalId: row.servicePrincipalId,
373
+ principalId: row.principalId,
374
+ clientId: row.clientId,
375
+ assignmentCount: row.assignmentCount,
376
+ assignedResourceGroups: JSON.stringify(row.assignedResourceGroups),
377
+ managedIdentityAssignments: JSON.stringify(row.managedIdentityAssignments)
378
+ }
379
+ );
380
+ }
381
+ }
382
+
383
+ async function readOptionalRows<T>(
384
+ connection: DuckDBConnection,
385
+ read: (connection: DuckDBConnection) => Promise<T[]>
386
+ ): Promise<T[]> {
387
+ try {
388
+ return await read(connection);
389
+ } catch (error) {
390
+ if (error instanceof Error && /does not exist|Catalog Error/i.test(error.message)) {
391
+ return [];
392
+ }
393
+ throw error;
394
+ }
395
+ }
396
+
397
+ function addManagedIdentityAssignment(
398
+ assignmentsByKey: Map<string, AzureManagedIdentityResourceAssignment[]>,
399
+ key: string | null,
400
+ assignment: AzureManagedIdentityResourceAssignment
401
+ ): void {
402
+ if (!key) {
403
+ return;
404
+ }
405
+
406
+ const normalizedKey = normalizeKey(key);
407
+ const assignments = assignmentsByKey.get(normalizedKey) ?? [];
408
+ assignments.push(assignment);
409
+ assignmentsByKey.set(normalizedKey, assignments);
410
+ }
411
+
412
+ function isManagedIdentity(servicePrincipal: EntraServicePrincipal): boolean {
413
+ return servicePrincipal.servicePrincipalType === "ManagedIdentity";
414
+ }
415
+
416
+ function createRiskSummary(principalId: string): ManagedIdentityPermissionRiskSummary {
417
+ return {
418
+ principalId,
419
+ riskLevel: "none",
420
+ assignmentCount: 0,
421
+ highRiskAssignmentCount: 0,
422
+ broadScopeAssignmentCount: 0,
423
+ roleAssignments: []
424
+ };
425
+ }
426
+
427
+ function maxRisk(
428
+ left: ManagedIdentityPermissionRiskLevel,
429
+ right: ManagedIdentityPermissionRiskLevel
430
+ ): ManagedIdentityPermissionRiskLevel {
431
+ return AZURE_ACCESS_RISK_RANK[left] >= AZURE_ACCESS_RISK_RANK[right] ? left : right;
432
+ }
433
+
434
+ function compareRiskAssignments(
435
+ left: ManagedIdentityPermissionRiskAssignment,
436
+ right: ManagedIdentityPermissionRiskAssignment
437
+ ): number {
438
+ return (
439
+ AZURE_ACCESS_RISK_RANK[right.riskLevel] - AZURE_ACCESS_RISK_RANK[left.riskLevel] ||
440
+ left.subscriptionName.localeCompare(right.subscriptionName, undefined, { sensitivity: "base" }) ||
441
+ (left.roleDefinitionName ?? "").localeCompare(right.roleDefinitionName ?? "", undefined, { sensitivity: "base" }) ||
442
+ left.scope.localeCompare(right.scope, undefined, { sensitivity: "base" })
443
+ );
444
+ }
445
+
446
+ function compareRoleAssignments(left: AzureRoleAssignment, right: AzureRoleAssignment): number {
447
+ return (
448
+ left.subscriptionName.localeCompare(right.subscriptionName, undefined, { sensitivity: "base" }) ||
449
+ (left.roleDefinitionName ?? "").localeCompare(right.roleDefinitionName ?? "", undefined, { sensitivity: "base" }) ||
450
+ left.scope.localeCompare(right.scope, undefined, { sensitivity: "base" })
451
+ );
452
+ }
453
+
454
+ function compareManagedIdentityAssignments(
455
+ left: AzureManagedIdentityResourceAssignment,
456
+ right: AzureManagedIdentityResourceAssignment
457
+ ): number {
458
+ return (
459
+ left.subscriptionName.localeCompare(right.subscriptionName, undefined, { sensitivity: "base" }) ||
460
+ left.assignedResourceGroup.localeCompare(right.assignedResourceGroup, undefined, { sensitivity: "base" }) ||
461
+ left.assignedResourceName.localeCompare(right.assignedResourceName, undefined, { sensitivity: "base" })
462
+ );
463
+ }
464
+
465
+ function sumRoleAssignments(rows: AzureRoleAssignmentEnrichment[]): number {
466
+ return rows.reduce((count, row) => count + row.assignmentCount, 0);
467
+ }
468
+
469
+ function sumManagedIdentityAssignments(rows: AzureManagedIdentityAssignmentEnrichment[]): number {
470
+ return rows.reduce((count, row) => count + row.assignmentCount, 0);
471
+ }
472
+
473
+ function uniqueSorted(values: string[]): string[] {
474
+ return [...new Set(values)].sort((left, right) => left.localeCompare(right, undefined, { sensitivity: "base" }));
475
+ }
476
+
477
+ function normalizeKey(value: string): string {
478
+ return value.toLowerCase();
479
+ }
480
+
481
+ async function readRows<Row extends Record<string, unknown>>(
482
+ connection: DuckDBConnection,
483
+ sql: string
484
+ ): Promise<Row[]> {
485
+ const reader = await connection.runAndReadAll(sql);
486
+ return reader.getRowObjectsJson() as Row[];
487
+ }
488
+
489
+ function parseJsonArray<T>(value: string | null | undefined): T[] {
490
+ return value ? JSON.parse(value) : [];
491
+ }
492
+
493
+ type AzureRuntimeEnrichmentRunRow = {
494
+ run_id: string;
495
+ completed_at: string;
496
+ identity_role_assignment_count: number;
497
+ access_risk_identity_count: number;
498
+ managed_identity_assignment_count: number;
499
+ };
500
+
501
+ type AzureIdentityRoleAssignmentEnrichmentRow = {
502
+ principal_id: string;
503
+ assignment_count: number;
504
+ role_assignments: string;
505
+ };
506
+
507
+ type AzureIdentityAccessRiskEnrichmentRow = {
508
+ principal_id: string;
509
+ risk_level: ManagedIdentityPermissionRiskLevel;
510
+ assignment_count: number;
511
+ high_risk_assignment_count: number;
512
+ broad_scope_assignment_count: number;
513
+ role_assignments: string;
514
+ };
515
+
516
+ type AzureManagedIdentityAssignmentEnrichmentRow = {
517
+ service_principal_id: string;
518
+ principal_id: string;
519
+ client_id: string;
520
+ assignment_count: number;
521
+ assigned_resource_groups: string;
522
+ managed_identity_assignments: string;
523
+ };
@@ -0,0 +1,30 @@
1
+ import type { AzureRoleAssignment } from "../../../../core/azure/resources";
2
+
3
+ export function isBroadAzureScope(assignment: AzureRoleAssignment): boolean {
4
+ const scopeType = classifyAzureScope(assignment);
5
+ return scopeType === "ManagementGroup" || scopeType === "Subscription";
6
+ }
7
+
8
+ export function classifyAzureScope(assignment: AzureRoleAssignment): NonNullable<AzureRoleAssignment["scopeType"]> {
9
+ if (assignment.scopeType) {
10
+ return assignment.scopeType;
11
+ }
12
+
13
+ if (/^\/providers\/Microsoft\.Management\/managementGroups\/[^/]+$/i.test(assignment.scope)) {
14
+ return "ManagementGroup";
15
+ }
16
+
17
+ if (/^\/subscriptions\/[^/]+$/i.test(assignment.scope)) {
18
+ return "Subscription";
19
+ }
20
+
21
+ if (/^\/subscriptions\/[^/]+\/resourceGroups\/[^/]+$/i.test(assignment.scope)) {
22
+ return "ResourceGroup";
23
+ }
24
+
25
+ if (/^\/subscriptions\/[^/]+\/resourceGroups\/[^/]+\/providers\/.+/i.test(assignment.scope)) {
26
+ return "Resource";
27
+ }
28
+
29
+ return "Unknown";
30
+ }
@@ -0,0 +1,88 @@
1
+ import type { AzureRoleAssignment } from "../../../../core/azure/resources";
2
+ import type {
3
+ ManagedIdentityPermissionRiskAssignment,
4
+ ManagedIdentityPermissionRiskLevel
5
+ } from "../../../../core/azure/identityEnrichment";
6
+ import { classifyAzureScope, isBroadAzureScope } from "./azureScopeClassifier.ts";
7
+
8
+ const HIGH_RISK_ROLES = new Set([
9
+ "owner",
10
+ "user access administrator",
11
+ "role based access control administrator",
12
+ "privileged role administrator",
13
+ "key vault administrator"
14
+ ]);
15
+
16
+ const MEDIUM_RISK_ROLE_PATTERNS = [
17
+ /(^|\s)contributor$/,
18
+ /(^|\s)administrator$/,
19
+ /(^|\s)data owner$/,
20
+ /(^|\s)data contributor$/,
21
+ /(^|\s)operator$/
22
+ ];
23
+
24
+ export function evaluateAzureRoleAssignmentRisk(
25
+ assignment: AzureRoleAssignment
26
+ ): ManagedIdentityPermissionRiskAssignment {
27
+ const reasons: string[] = [];
28
+ const roleLevel = getAzureRoleRiskLevel(assignment.roleDefinitionName);
29
+ const broadScope = isBroadAzureScope(assignment);
30
+ const resourceScope = classifyAzureScope(assignment) === "Resource";
31
+ let riskLevel = roleLevel;
32
+
33
+ if (broadScope && roleLevel !== "none") {
34
+ reasons.push("broad scope");
35
+ }
36
+
37
+ if (isHighRiskAzureRole(assignment.roleDefinitionName)) {
38
+ reasons.push("privileged role");
39
+ } else if (roleLevel === "medium") {
40
+ reasons.push("write-capable role");
41
+ } else if (roleLevel === "low") {
42
+ reasons.push("read-only role");
43
+ } else if (assignment.roleDefinitionName) {
44
+ reasons.push("custom or unclassified role");
45
+ }
46
+
47
+ if (broadScope && roleLevel === "medium") {
48
+ riskLevel = "high";
49
+ } else if (resourceScope && roleLevel === "high") {
50
+ riskLevel = "medium";
51
+ }
52
+
53
+ return {
54
+ ...assignment,
55
+ riskLevel,
56
+ reasons
57
+ };
58
+ }
59
+
60
+ function getAzureRoleRiskLevel(roleDefinitionName: string | null): ManagedIdentityPermissionRiskLevel {
61
+ const normalizedRole = normalizeAzureRoleName(roleDefinitionName);
62
+
63
+ if (!normalizedRole) {
64
+ return "medium";
65
+ }
66
+
67
+ if (isHighRiskAzureRole(normalizedRole)) {
68
+ return "high";
69
+ }
70
+
71
+ if (normalizedRole === "reader") {
72
+ return "low";
73
+ }
74
+
75
+ if (MEDIUM_RISK_ROLE_PATTERNS.some((pattern) => pattern.test(normalizedRole))) {
76
+ return "medium";
77
+ }
78
+
79
+ return "medium";
80
+ }
81
+
82
+ function isHighRiskAzureRole(roleDefinitionName: string | null): boolean {
83
+ return HIGH_RISK_ROLES.has(normalizeAzureRoleName(roleDefinitionName));
84
+ }
85
+
86
+ function normalizeAzureRoleName(roleDefinitionName: string | null): string {
87
+ return roleDefinitionName?.trim().toLowerCase() ?? "";
88
+ }