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,307 @@
1
+ import { RuntimeHttpError } from "../../../../core/runtime/localSnapshotFiles";
2
+ import type { ManagedIdentity } from "../../../../core/azure/entra/managedIdentity";
3
+ import type { ServicePrincipal } from "../../../../core/azure/entra/servicePrincipal";
4
+ import type {
5
+ AzureRoleAssignment,
6
+ AzureUserAssignedManagedIdentity,
7
+ ResourceGroupOwnershipRow
8
+ } from "../../../../core/azure/resources";
9
+ import type { OwnerConfidence } from "../../../../core/ownership/types";
10
+
11
+ import {
12
+ buildPaginatedCollection,
13
+ type LocalReportCollectionQueryOptions,
14
+ type LocalReportPaginatedCollection
15
+ } from "../localReportCollections";
16
+ import type { AzureResourcesCollectionQueryService } from "../resources/AzureResourcesCollectionQueryService";
17
+ import type { LocalAzureResourcesReportRuntime } from "../resources/LocalAzureResourcesReportRuntime";
18
+ import type { ZeroTrustAssessmentQueryService } from "../zta/ZeroTrustAssessmentQueryService";
19
+ import type { LocalEntraReportRuntime } from "./LocalEntraReportRuntime";
20
+
21
+ export type EntraCollectionQueryServiceOptions = {
22
+ entra: LocalEntraReportRuntime;
23
+ azureResources: LocalAzureResourcesReportRuntime;
24
+ azureResourcesQueries: AzureResourcesCollectionQueryService;
25
+ zeroTrustAssessmentQueries: ZeroTrustAssessmentQueryService;
26
+ };
27
+
28
+ export class EntraCollectionQueryService {
29
+ private readonly entra: LocalEntraReportRuntime;
30
+ private readonly azureResources: LocalAzureResourcesReportRuntime;
31
+ private readonly azureResourcesQueries: AzureResourcesCollectionQueryService;
32
+ private readonly zeroTrustAssessmentQueries: ZeroTrustAssessmentQueryService;
33
+
34
+ constructor(options: EntraCollectionQueryServiceOptions) {
35
+ this.entra = options.entra;
36
+ this.azureResources = options.azureResources;
37
+ this.azureResourcesQueries = options.azureResourcesQueries;
38
+ this.zeroTrustAssessmentQueries = options.zeroTrustAssessmentQueries;
39
+ }
40
+
41
+ async queryServicePrincipals(
42
+ options: LocalReportCollectionQueryOptions
43
+ ): Promise<LocalReportPaginatedCollection<"entra.servicePrincipals">> {
44
+ return buildPaginatedCollection("entra.servicePrincipals", await this.readServicePrincipalRows(), options);
45
+ }
46
+
47
+ async queryManagedIdentities(
48
+ options: LocalReportCollectionQueryOptions
49
+ ): Promise<LocalReportPaginatedCollection<"entra.managedIdentities">> {
50
+ return buildPaginatedCollection("entra.managedIdentities", await this.readManagedIdentityRows(), options);
51
+ }
52
+
53
+ async queryOAuth2PermissionGrants(
54
+ options: LocalReportCollectionQueryOptions
55
+ ): Promise<LocalReportPaginatedCollection<"entra.oauth2PermissionGrants">> {
56
+ return buildPaginatedCollection(
57
+ "entra.oauth2PermissionGrants",
58
+ (await this.entra.readEntraOAuth2PermissionGrants()) as unknown as Record<string, unknown>[],
59
+ options
60
+ );
61
+ }
62
+
63
+ async queryAppRoleAssignments(
64
+ options: LocalReportCollectionQueryOptions
65
+ ): Promise<LocalReportPaginatedCollection<"entra.appRoleAssignments">> {
66
+ return buildPaginatedCollection(
67
+ "entra.appRoleAssignments",
68
+ (await this.entra.readEntraAppRoleAssignments()) as unknown as Record<string, unknown>[],
69
+ options
70
+ );
71
+ }
72
+
73
+ private async readManagedIdentityRows(): Promise<Record<string, unknown>[]> {
74
+ const managedIdentities = await this.enrichWithZtaRemediationSummaries(await this.entra.readManagedIdentities());
75
+
76
+ try {
77
+ const [resourceGroupOwnershipRows, userAssignedManagedIdentities] = await Promise.all([
78
+ this.azureResourcesQueries.readResourceGroupOwnershipRows(),
79
+ this.azureResources.readAzureUserAssignedManagedIdentities()
80
+ ]);
81
+
82
+ return enrichManagedIdentitiesWithResourceGroupOwners(
83
+ managedIdentities,
84
+ resourceGroupOwnershipRows,
85
+ userAssignedManagedIdentities
86
+ ) as unknown as Record<string, unknown>[];
87
+ } catch (error) {
88
+ if (error instanceof RuntimeHttpError && error.statusCode === 404) {
89
+ return managedIdentities as unknown as Record<string, unknown>[];
90
+ }
91
+
92
+ throw error;
93
+ }
94
+ }
95
+
96
+ private async readServicePrincipalRows(): Promise<Record<string, unknown>[]> {
97
+ const servicePrincipals = await this.enrichWithZtaRemediationSummaries(await this.entra.readServicePrincipals());
98
+
99
+ try {
100
+ return enrichServicePrincipalsWithResourceGroupOwners(
101
+ servicePrincipals,
102
+ await this.azureResourcesQueries.readResourceGroupOwnershipRows()
103
+ ) as unknown as Record<string, unknown>[];
104
+ } catch (error) {
105
+ if (error instanceof RuntimeHttpError && error.statusCode === 404) {
106
+ return servicePrincipals as unknown as Record<string, unknown>[];
107
+ }
108
+
109
+ throw error;
110
+ }
111
+ }
112
+
113
+ private async enrichWithZtaRemediationSummaries<Row extends ServicePrincipal | ManagedIdentity>(rows: Row[]): Promise<Row[]> {
114
+ const summariesByPrincipalId = await this.zeroTrustAssessmentQueries.readRemediationSummaries();
115
+
116
+ return rows.map((row) => ({
117
+ ...row,
118
+ ...(summariesByPrincipalId.get(row.id.toLowerCase()) ?? {})
119
+ }));
120
+ }
121
+ }
122
+
123
+ type ManagedIdentityOwnerProjection = {
124
+ potentialOwners: string[];
125
+ ownerConfidence: OwnerConfidence;
126
+ };
127
+
128
+ type ServicePrincipalOwnerProjection = {
129
+ potentialOwners: string[];
130
+ ownerConfidence: OwnerConfidence;
131
+ };
132
+
133
+ type ResourceGroupOwnershipIndex = {
134
+ byResourceGroup: Map<string, ResourceGroupOwnershipRow>;
135
+ bySubscription: Map<string, ResourceGroupOwnershipRow[]>;
136
+ };
137
+
138
+ function enrichManagedIdentitiesWithResourceGroupOwners(
139
+ managedIdentities: ManagedIdentity[],
140
+ resourceGroupOwnershipRows: ResourceGroupOwnershipRow[],
141
+ userAssignedManagedIdentities: AzureUserAssignedManagedIdentity[]
142
+ ): ManagedIdentity[] {
143
+ const ownershipByResourceGroup = buildResourceGroupOwnershipIndex(resourceGroupOwnershipRows).byResourceGroup;
144
+ const locationsByPrincipal = buildManagedIdentityLocationIndex(userAssignedManagedIdentities);
145
+
146
+ return managedIdentities.map((identity) => {
147
+ const userAssignedIdentity =
148
+ locationsByPrincipal.get(identity.id.toLowerCase()) ?? locationsByPrincipal.get(identity.appId.toLowerCase());
149
+ const projection = projectManagedIdentityOwner(userAssignedIdentity, ownershipByResourceGroup);
150
+
151
+ return {
152
+ ...identity,
153
+ ...projection
154
+ };
155
+ });
156
+ }
157
+
158
+ function enrichServicePrincipalsWithResourceGroupOwners(
159
+ servicePrincipals: ServicePrincipal[],
160
+ resourceGroupOwnershipRows: ResourceGroupOwnershipRow[]
161
+ ): ServicePrincipal[] {
162
+ const ownershipIndex = buildResourceGroupOwnershipIndex(resourceGroupOwnershipRows);
163
+
164
+ return servicePrincipals.map((servicePrincipal) => ({
165
+ ...servicePrincipal,
166
+ ...projectServicePrincipalOwners(servicePrincipal.roleAssignments, ownershipIndex)
167
+ }));
168
+ }
169
+
170
+ function buildResourceGroupOwnershipIndex(rows: ResourceGroupOwnershipRow[]): ResourceGroupOwnershipIndex {
171
+ const byResourceGroup = new Map<string, ResourceGroupOwnershipRow>();
172
+ const bySubscription = new Map<string, ResourceGroupOwnershipRow[]>();
173
+
174
+ for (const row of rows) {
175
+ byResourceGroup.set(getResourceGroupKey(row.subscriptionId, row.resourceGroup), row);
176
+
177
+ const subscriptionKey = row.subscriptionId.toLowerCase();
178
+ const subscriptionRows = bySubscription.get(subscriptionKey) ?? [];
179
+ subscriptionRows.push(row);
180
+ bySubscription.set(subscriptionKey, subscriptionRows);
181
+ }
182
+
183
+ return { byResourceGroup, bySubscription };
184
+ }
185
+
186
+ function buildManagedIdentityLocationIndex(
187
+ userAssignedManagedIdentities: AzureUserAssignedManagedIdentity[]
188
+ ): Map<string, AzureUserAssignedManagedIdentity> {
189
+ const index = new Map<string, AzureUserAssignedManagedIdentity>();
190
+
191
+ for (const identity of userAssignedManagedIdentities) {
192
+ addManagedIdentityLocation(index, identity.principalId, identity);
193
+ addManagedIdentityLocation(index, identity.clientId, identity);
194
+ }
195
+
196
+ return index;
197
+ }
198
+
199
+ function addManagedIdentityLocation(
200
+ index: Map<string, AzureUserAssignedManagedIdentity>,
201
+ key: string,
202
+ identity: AzureUserAssignedManagedIdentity
203
+ ): void {
204
+ const normalizedKey = key.trim().toLowerCase();
205
+ if (!normalizedKey) {
206
+ return;
207
+ }
208
+
209
+ index.set(normalizedKey, identity);
210
+ }
211
+
212
+ function projectManagedIdentityOwner(
213
+ identity: AzureUserAssignedManagedIdentity | undefined,
214
+ ownershipByResourceGroup: Map<string, ResourceGroupOwnershipRow>
215
+ ): ManagedIdentityOwnerProjection {
216
+ if (!identity) {
217
+ return {
218
+ potentialOwners: [],
219
+ ownerConfidence: "none"
220
+ };
221
+ }
222
+
223
+ const ownership = ownershipByResourceGroup.get(getResourceGroupKey(identity.subscriptionId, identity.resourceGroup));
224
+
225
+ return {
226
+ potentialOwners: ownership?.owner ? [ownership.owner] : [],
227
+ ownerConfidence: ownership?.confidence ?? "none"
228
+ };
229
+ }
230
+
231
+ function projectServicePrincipalOwners(
232
+ roleAssignments: AzureRoleAssignment[],
233
+ ownershipIndex: ResourceGroupOwnershipIndex
234
+ ): ServicePrincipalOwnerProjection {
235
+ const resourceGroups = new Map<string, ResourceGroupOwnershipRow>();
236
+
237
+ for (const assignment of roleAssignments) {
238
+ for (const row of getRoleAssignmentResourceGroupOwners(assignment, ownershipIndex)) {
239
+ resourceGroups.set(getResourceGroupKey(row.subscriptionId, row.resourceGroup), row);
240
+ }
241
+ }
242
+
243
+ const ownerRows = [...resourceGroups.values()].filter((row) => row.owner);
244
+
245
+ return {
246
+ potentialOwners: uniqueSorted(ownerRows.map((row) => row.owner).filter(isString)),
247
+ ownerConfidence: ownerRows.reduce<OwnerConfidence>(
248
+ (confidence, row) => maxOwnerConfidence(confidence, row.confidence),
249
+ "none"
250
+ )
251
+ };
252
+ }
253
+
254
+ function getRoleAssignmentResourceGroupOwners(
255
+ assignment: AzureRoleAssignment,
256
+ ownershipIndex: ResourceGroupOwnershipIndex
257
+ ): ResourceGroupOwnershipRow[] {
258
+ const scope = assignment.scope;
259
+ const subscriptionId = getScopeSubscriptionId(scope) ?? assignment.subscriptionId;
260
+ const resourceGroup = getScopeResourceGroup(scope);
261
+
262
+ if (subscriptionId && resourceGroup) {
263
+ const row = ownershipIndex.byResourceGroup.get(getResourceGroupKey(subscriptionId, resourceGroup));
264
+ return row ? [row] : [];
265
+ }
266
+
267
+ if (isSubscriptionScope(scope) && subscriptionId) {
268
+ return ownershipIndex.bySubscription.get(subscriptionId.toLowerCase()) ?? [];
269
+ }
270
+
271
+ return [];
272
+ }
273
+
274
+ function getScopeSubscriptionId(scope: string): string | null {
275
+ return scope.match(/\/subscriptions\/([^/]+)/i)?.[1] ?? null;
276
+ }
277
+
278
+ function getScopeResourceGroup(scope: string): string | null {
279
+ return scope.match(/\/resourceGroups\/([^/]+)/i)?.[1] ?? null;
280
+ }
281
+
282
+ function isSubscriptionScope(scope: string): boolean {
283
+ return /^\/subscriptions\/[^/]+\/?$/i.test(scope);
284
+ }
285
+
286
+ function getResourceGroupKey(subscriptionId: string, resourceGroup: string): string {
287
+ return `${subscriptionId.toLowerCase()}:${resourceGroup.toLowerCase()}`;
288
+ }
289
+
290
+ function uniqueSorted(values: string[]): string[] {
291
+ return [...new Set(values)].sort((left, right) => left.localeCompare(right, undefined, { sensitivity: "base" }));
292
+ }
293
+
294
+ function isString(value: string | null): value is string {
295
+ return typeof value === "string" && value.trim().length > 0;
296
+ }
297
+
298
+ function maxOwnerConfidence(left: OwnerConfidence, right: OwnerConfidence): OwnerConfidence {
299
+ return OWNER_CONFIDENCE_RANK[left] >= OWNER_CONFIDENCE_RANK[right] ? left : right;
300
+ }
301
+
302
+ const OWNER_CONFIDENCE_RANK: Record<OwnerConfidence, number> = {
303
+ none: 0,
304
+ low: 1,
305
+ medium: 2,
306
+ high: 3
307
+ };
@@ -0,0 +1,227 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ import type { DuckDBConnection } from "@duckdb/node-api";
5
+
6
+ import { pathExists, RuntimeHttpError, type LocalSnapshotData } from "../../../../core/runtime/localSnapshotFiles";
7
+ import type { ManagedIdentity } from "../../../../core/azure/entra/managedIdentity";
8
+ import type { EntraPrincipalPermissionSummary, ServicePrincipal } from "../../../../core/azure/entra/servicePrincipal";
9
+ import type { EntraOAuth2PermissionGrant } from "../../../../core/azure/entra/types";
10
+ import type { PermissionRiskLevel } from "../../../../core/risk/types";
11
+ import type { EntraAppRoleAssignment } from "../../inputTransferObject/entra/EntraAppRoleAssignment";
12
+ import type { EntraOAuth2PermissionGrant as InputEntraOAuth2PermissionGrant } from "../../inputTransferObject/entra/EntraOAuth2PermissionGrant";
13
+ import type { EntraServicePrincipal } from "../../inputTransferObject/entra/EntraServicePrincipal";
14
+ import type { EntraSnapshot } from "../../inputTransferObject/entra/EntraSnapshot";
15
+ import { readEntraAppRoleAssignmentRows } from "./appRoleAssignmentsTable";
16
+ import { readEntraOAuth2PermissionGrantRows } from "./oauth2PermissionGrantsTable";
17
+ import { readLatestAzureIdentityEnrichment } from "../enrichment/azureIdentityEnrichment";
18
+ import { readEntraServicePrincipalRows } from "./servicePrincipalsTable";
19
+ import {
20
+ createEmptyEntraImportStatus,
21
+ entraSnapshotFileName,
22
+ importEntraSnapshotToDuckDb,
23
+ readEntraSnapshotFromDuckDb,
24
+ type EntraDuckDbImportStatus
25
+ } from "./snapshotStore";
26
+ import { mapEntraServicePrincipalsToCore } from "./entraServicePrincipalMapper";
27
+ import { toManagedIdentities, toServicePrincipals } from "./principalProjection";
28
+
29
+ export type LocalEntraReportCollectionId =
30
+ | "entra.servicePrincipals"
31
+ | "entra.managedIdentities"
32
+ | "entra.oauth2PermissionGrants"
33
+ | "entra.appRoleAssignments";
34
+
35
+ export type EntraPrincipalPermissions = {
36
+ principalId: string;
37
+ oauth2PermissionGrants: EntraOAuth2PermissionGrant[];
38
+ appRoleAssignments: EntraAppRoleAssignment[];
39
+ };
40
+
41
+ export type LocalEntraReportRuntimeOptions = {
42
+ dataDir: string;
43
+ getConnection: () => DuckDBConnection;
44
+ };
45
+
46
+ export class LocalEntraReportRuntime {
47
+ private readonly dataDir: string;
48
+ private readonly getConnection: () => DuckDBConnection;
49
+ private status = createEmptyEntraImportStatus();
50
+
51
+ constructor(options: LocalEntraReportRuntimeOptions) {
52
+ this.dataDir = options.dataDir;
53
+ this.getConnection = options.getConnection;
54
+ }
55
+
56
+ getStatus(): EntraDuckDbImportStatus {
57
+ return this.status;
58
+ }
59
+
60
+ canReadSnapshot(name: string): boolean {
61
+ return name === entraSnapshotFileName;
62
+ }
63
+
64
+ async importSnapshot(): Promise<void> {
65
+ const entraSnapshotPath = path.join(this.dataDir, entraSnapshotFileName);
66
+ if (!(await pathExists(entraSnapshotPath))) {
67
+ return;
68
+ }
69
+
70
+ const snapshot = JSON.parse(await readFile(entraSnapshotPath, "utf8")) as EntraSnapshot & LocalSnapshotData;
71
+ this.status = await importEntraSnapshotToDuckDb(this.getConnection(), snapshot);
72
+ }
73
+
74
+ async readSnapshot(): Promise<EntraSnapshot & LocalSnapshotData> {
75
+ this.assertImported();
76
+ return readEntraSnapshotFromDuckDb(this.getConnection());
77
+ }
78
+
79
+ async readEntraServicePrincipals(): Promise<EntraServicePrincipal[]> {
80
+ this.assertImported();
81
+ return readEntraServicePrincipalRows(this.getConnection());
82
+ }
83
+
84
+ async readServicePrincipals(): Promise<ServicePrincipal[]> {
85
+ this.assertImported();
86
+ const connection = this.getConnection();
87
+ const permissionsByPrincipalId = await this.readPrincipalPermissionSummary(connection);
88
+
89
+ return toServicePrincipals(
90
+ mapEntraServicePrincipalsToCore(await readEntraServicePrincipalRows(connection)),
91
+ await readLatestAzureIdentityEnrichment(connection),
92
+ permissionsByPrincipalId
93
+ );
94
+ }
95
+
96
+ async readManagedIdentities(): Promise<ManagedIdentity[]> {
97
+ this.assertImported();
98
+ const connection = this.getConnection();
99
+ const permissionsByPrincipalId = await this.readPrincipalPermissionSummary(connection);
100
+
101
+ return toManagedIdentities(
102
+ mapEntraServicePrincipalsToCore(await readEntraServicePrincipalRows(connection)),
103
+ await readLatestAzureIdentityEnrichment(connection),
104
+ permissionsByPrincipalId
105
+ );
106
+ }
107
+
108
+ async readEntraOAuth2PermissionGrants(): Promise<EntraOAuth2PermissionGrant[]> {
109
+ this.assertImported();
110
+ return (await readEntraOAuth2PermissionGrantRows(this.getConnection())).map(toCoreEntraOAuth2PermissionGrant);
111
+ }
112
+
113
+ async readEntraAppRoleAssignments(): Promise<EntraAppRoleAssignment[]> {
114
+ this.assertImported();
115
+ return readEntraAppRoleAssignmentRows(this.getConnection());
116
+ }
117
+
118
+ async readEntraPrincipalPermissions(principalId: string): Promise<EntraPrincipalPermissions> {
119
+ this.assertImported();
120
+ const normalizedPrincipalId = principalId.toLowerCase();
121
+ const [oauth2PermissionGrants, appRoleAssignments] = await Promise.all([
122
+ readEntraOAuth2PermissionGrantRows(this.getConnection()),
123
+ readEntraAppRoleAssignmentRows(this.getConnection())
124
+ ]);
125
+
126
+ return {
127
+ principalId,
128
+ oauth2PermissionGrants: oauth2PermissionGrants.filter(
129
+ (grant) => grant.clientId.toLowerCase() === normalizedPrincipalId
130
+ ).map(toCoreEntraOAuth2PermissionGrant),
131
+ appRoleAssignments: appRoleAssignments.filter(
132
+ (assignment) => assignment.principalId.toLowerCase() === normalizedPrincipalId
133
+ )
134
+ };
135
+ }
136
+
137
+ private assertImported(): void {
138
+ if (!this.status.imported) {
139
+ throw new RuntimeHttpError(`Snapshot file ./data/${entraSnapshotFileName} was not found.`, 404);
140
+ }
141
+ }
142
+
143
+ private async readPrincipalPermissionSummary(
144
+ connection: DuckDBConnection
145
+ ): Promise<Map<string, EntraPrincipalPermissionSummary>> {
146
+ const [oauth2PermissionGrants, appRoleAssignments] = await Promise.all([
147
+ readEntraOAuth2PermissionGrantRows(connection),
148
+ readEntraAppRoleAssignmentRows(connection)
149
+ ]);
150
+ const permissionsByPrincipalId = new Map<string, EntraPrincipalPermissionSummary>();
151
+
152
+ for (const grant of oauth2PermissionGrants) {
153
+ const summary = getOrCreatePrincipalPermissionSummary(permissionsByPrincipalId, grant.clientId);
154
+ const scopeCount = countOAuthPermissionScopes(grant.scope);
155
+ summary.oauthPemrissionsCount += scopeCount;
156
+ if (scopeCount > 0) {
157
+ summary.entraPermissionRisk = maxPermissionRisk(
158
+ summary.entraPermissionRisk,
159
+ grant.consentType === "AllPrincipals" ? "high" : "medium"
160
+ );
161
+ }
162
+ }
163
+
164
+ for (const assignment of appRoleAssignments) {
165
+ const summary = getOrCreatePrincipalPermissionSummary(permissionsByPrincipalId, assignment.principalId);
166
+ summary.appRolesPermissionCount += 1;
167
+ summary.entraPermissionRisk = maxPermissionRisk(summary.entraPermissionRisk, "medium");
168
+ }
169
+
170
+ return permissionsByPrincipalId;
171
+ }
172
+ }
173
+
174
+ function getOrCreatePrincipalPermissionSummary(
175
+ permissionsByPrincipalId: Map<string, EntraPrincipalPermissionSummary>,
176
+ principalId: string
177
+ ): EntraPrincipalPermissionSummary {
178
+ const normalizedPrincipalId = principalId.toLowerCase();
179
+ const existing = permissionsByPrincipalId.get(normalizedPrincipalId);
180
+
181
+ if (existing) {
182
+ return existing;
183
+ }
184
+
185
+ const summary = {
186
+ oauthPemrissionsCount: 0,
187
+ appRolesPermissionCount: 0,
188
+ entraPermissionRisk: "none" as PermissionRiskLevel
189
+ };
190
+
191
+ permissionsByPrincipalId.set(normalizedPrincipalId, summary);
192
+ return summary;
193
+ }
194
+
195
+ function countOAuthPermissionScopes(scope: string): number {
196
+ return scope.split(/\s+/).filter(Boolean).length;
197
+ }
198
+
199
+ function toCoreEntraOAuth2PermissionGrant(grant: InputEntraOAuth2PermissionGrant): EntraOAuth2PermissionGrant {
200
+ return {
201
+ ...grant,
202
+ risk: getOAuth2PermissionGrantRisk(grant)
203
+ };
204
+ }
205
+
206
+ function getOAuth2PermissionGrantRisk(grant: Pick<InputEntraOAuth2PermissionGrant, "consentType">): PermissionRiskLevel {
207
+ if (grant.consentType === "AllPrincipals") {
208
+ return "high";
209
+ }
210
+
211
+ if (grant.consentType === "Principal") {
212
+ return "low";
213
+ }
214
+
215
+ return "medium";
216
+ }
217
+
218
+ const permissionRiskRank: Record<PermissionRiskLevel, number> = {
219
+ none: 0,
220
+ low: 1,
221
+ medium: 2,
222
+ high: 3
223
+ };
224
+
225
+ function maxPermissionRisk(left: PermissionRiskLevel, right: PermissionRiskLevel): PermissionRiskLevel {
226
+ return permissionRiskRank[left] >= permissionRiskRank[right] ? left : right;
227
+ }
@@ -0,0 +1,52 @@
1
+ import type { DuckDBConnection } from "@duckdb/node-api";
2
+
3
+ import type { EntraAppRoleAssignment } from "../../inputTransferObject/entra/EntraAppRoleAssignment";
4
+
5
+ export async function insertEntraAppRoleAssignmentRows(
6
+ connection: DuckDBConnection,
7
+ appRoleAssignments: EntraAppRoleAssignment[] = []
8
+ ): Promise<void> {
9
+ for (const [ordinal, assignment] of appRoleAssignments.entries()) {
10
+ await connection.run(
11
+ `insert into entra_app_role_assignments values (
12
+ $ordinal,
13
+ $id,
14
+ $appRoleId,
15
+ $appRoleDisplayName,
16
+ $appRoleValue,
17
+ $principalId,
18
+ $principalDisplayName,
19
+ $resourceId,
20
+ $resourceDisplayName
21
+ )`,
22
+ { ordinal, ...assignment }
23
+ );
24
+ }
25
+ }
26
+
27
+ export async function readEntraAppRoleAssignmentRows(
28
+ connection: DuckDBConnection
29
+ ): Promise<EntraAppRoleAssignment[]> {
30
+ return readRows<EntraAppRoleAssignment>(
31
+ connection,
32
+ `select
33
+ id,
34
+ app_role_id as appRoleId,
35
+ app_role_display_name as appRoleDisplayName,
36
+ app_role_value as appRoleValue,
37
+ principal_id as principalId,
38
+ principal_display_name as principalDisplayName,
39
+ resource_id as resourceId,
40
+ resource_display_name as resourceDisplayName
41
+ from entra_app_role_assignments
42
+ order by ordinal`
43
+ );
44
+ }
45
+
46
+ async function readRows<Row extends Record<string, unknown>>(
47
+ connection: DuckDBConnection,
48
+ sql: string
49
+ ): Promise<Row[]> {
50
+ const reader = await connection.runAndReadAll(sql);
51
+ return reader.getRowObjectsJson() as Row[];
52
+ }