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,2318 @@
1
+ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import path from "node:path";
4
+
5
+ import { DuckDBInstance } from "@duckdb/node-api";
6
+
7
+ import { LocalReportRuntime } from "./LocalReportRuntime";
8
+ import { defineLocalReportRuntimeRestEndpoints } from "./localReportRuntimeRest";
9
+ import type { AzureSnapshot } from "../../../core/azure/resources";
10
+ import type { EntraSnapshot } from "../inputTransferObject/entra/EntraSnapshot";
11
+ import {
12
+ importZeroTrustAssessmentReportToDuckDb,
13
+ readZeroTrustAssessmentReportFromDuckDb
14
+ } from "./zta/snapshotStore";
15
+ import { insertEntraServicePrincipalRows } from "./entra/servicePrincipalsTable";
16
+ import { insertEntraApplicationRows } from "./entra/applicationsTable";
17
+ import { prepareRuntimeSqlSchema } from "./runtimeSqlSchema";
18
+ import type { ZeroTrustAssessmentReport } from "./zta/types";
19
+
20
+ test("imports Zero Trust Assessment report into DuckDB and reads it back through the runtime", async () => {
21
+ const dataDir = await mkdtemp(path.join(tmpdir(), "ownerlens-runtime-"));
22
+ const runtime = new LocalReportRuntime({ dataDir });
23
+ const exportDir = path.join(dataDir, "exports", "nested");
24
+
25
+ const report: ZeroTrustAssessmentReport = {
26
+ Account: "owner@example.test",
27
+ CurrentVersion: "2.4.100",
28
+ Domain: "example.test",
29
+ ExecutedAt: "2026-06-02T16:06:31.3057648+02:00",
30
+ LatestVersion: "2.3.0",
31
+ TenantId: "tenant-1",
32
+ TenantInfo: {
33
+ TenantOverview: {
34
+ UserCount: 4
35
+ }
36
+ },
37
+ TenantName: "Example tenant",
38
+ TestResultSummary: {
39
+ IdentityPassed: 1,
40
+ IdentityTotal: 2
41
+ },
42
+ Tests: [
43
+ {
44
+ TestId: "21791",
45
+ TestTitle: "Guest can't invite other guests",
46
+ TestPillar: "Identity",
47
+ TestImpact: "Medium",
48
+ TestImplementationCost: "Low",
49
+ TestMinimumLicense: "Free",
50
+ TestStatus: "Failed",
51
+ TestResult: "Tenant allows any user to invite guests.",
52
+ TestTags: ["ExternalCollaboration"],
53
+ TestSkipped: "",
54
+ TestDescription: "External collaboration should be restricted.",
55
+ TestCategory: "External collaboration",
56
+ TestRisk: "Medium",
57
+ TestSfiPillar: "Protect tenants and isolate production systems",
58
+ TestAppliesTo: ["Identity"],
59
+ RelatedObjects: [
60
+ {
61
+ object_id: "object-1",
62
+ id: "principal-id-1",
63
+ applicationId: "app-client-1",
64
+ displayName: "Searchable owner app",
65
+ servicePrincipalType: "Application"
66
+ },
67
+ {
68
+ object_id: "object-2",
69
+ id: "principal-id-2",
70
+ applicationId: "app-client-2",
71
+ displayName: "Other owner app",
72
+ servicePrincipalType: "ManagedIdentity"
73
+ }
74
+ ]
75
+ },
76
+ {
77
+ TestId: 21823,
78
+ TestTitle: "Guest self-service sign-up via user flow is disabled",
79
+ TestPillar: "Identity",
80
+ TestStatus: "Passed",
81
+ TestMinimumLicense: ["Free"],
82
+ RelatedObjects: []
83
+ }
84
+ ]
85
+ };
86
+
87
+ try {
88
+ await mkdir(exportDir, { recursive: true });
89
+ await writeFile(path.join(dataDir, "regular.json"), JSON.stringify({ TenantId: "not-zta" }), "utf8");
90
+ await writeFile(
91
+ path.join(exportDir, "older-zta-report.json"),
92
+ JSON.stringify({
93
+ ...report,
94
+ ExecutedAt: "2026-06-01T16:06:31.3057648+02:00",
95
+ Tests: [{ TestId: "old", TestStatus: "Failed" }]
96
+ }),
97
+ "utf8"
98
+ );
99
+ await writeFile(path.join(exportDir, "tenant-zta-report.json"), JSON.stringify(report), "utf8");
100
+ await runtime.initialize();
101
+
102
+ expect(runtime.getStatus().zeroTrustAssessment).toMatchObject({
103
+ imported: true,
104
+ fileName: "exports/nested/tenant-zta-report.json",
105
+ testCount: 2
106
+ });
107
+
108
+ const imported = await runtime.readZeroTrustAssessmentReport();
109
+ expect(imported).toMatchObject({
110
+ Meta: {
111
+ Account: "owner@example.test",
112
+ TenantId: "tenant-1",
113
+ TestResultSummary: {
114
+ IdentityPassed: 1,
115
+ IdentityTotal: 2
116
+ }
117
+ }
118
+ });
119
+ expect(imported.Tests).toHaveLength(2);
120
+ expect(imported.Tests[0]).toMatchObject({
121
+ TestId: "21791",
122
+ TestImpact: "medium",
123
+ TestRisk: "medium",
124
+ TestStatus: "Failed"
125
+ });
126
+ expect(imported.Tests[0].RelatedObjects).toEqual(
127
+ expect.arrayContaining([
128
+ expect.objectContaining({ object_id: "object-1" }),
129
+ expect.objectContaining({ object_id: "object-2" })
130
+ ])
131
+ );
132
+ expect(imported.Tests[1]).toMatchObject({
133
+ TestId: 21823,
134
+ TestMinimumLicense: ["Free"]
135
+ });
136
+
137
+ const endpoints = defineLocalReportRuntimeRestEndpoints(runtime);
138
+ const ztaReportEndpoint = endpoints.find((endpoint) => endpoint.path === "/api/data/zeroTrustAssessment/report");
139
+ await expect(
140
+ ztaReportEndpoint?.handle({
141
+ req: {},
142
+ url: new URL("http://localhost/api/data/zeroTrustAssessment/report")
143
+ })
144
+ ).resolves.toMatchObject({
145
+ collectionId: "zeroTrustAssessment.report",
146
+ rows: [
147
+ expect.objectContaining({
148
+ TestId: "21791"
149
+ }),
150
+ expect.objectContaining({
151
+ TestId: 21823
152
+ })
153
+ ],
154
+ page: 1,
155
+ pageSize: 50,
156
+ count: 2
157
+ });
158
+ await expect(
159
+ ztaReportEndpoint?.handle({
160
+ req: {},
161
+ url: new URL(
162
+ "http://localhost/api/data/zeroTrustAssessment/report?filter[0][column]=RelatedObjects&filter[0][value][0]=app-client-1"
163
+ )
164
+ })
165
+ ).resolves.toEqual(
166
+ expect.objectContaining({
167
+ rows: [
168
+ expect.objectContaining({
169
+ TestId: "21791",
170
+ RelatedObjects: [
171
+ expect.objectContaining({
172
+ object_id: "object-1",
173
+ applicationId: "app-client-1"
174
+ })
175
+ ]
176
+ })
177
+ ],
178
+ count: 1
179
+ })
180
+ );
181
+ await expect(
182
+ ztaReportEndpoint?.handle({
183
+ req: {},
184
+ url: new URL(
185
+ "http://localhost/api/data/zeroTrustAssessment/report?filter[0][column]=RelatedObjects&filter[0][value][0]=principal-id-1"
186
+ )
187
+ })
188
+ ).resolves.toMatchObject({
189
+ rows: [expect.objectContaining({ TestId: "21791" })],
190
+ count: 1
191
+ });
192
+ await expect(
193
+ ztaReportEndpoint?.handle({
194
+ req: {},
195
+ url: new URL(
196
+ "http://localhost/api/data/zeroTrustAssessment/report?filter[0][column]=RelatedObjects&filter[0][value][0]=Searchable%20owner"
197
+ )
198
+ })
199
+ ).resolves.toMatchObject({
200
+ rows: [expect.objectContaining({ TestId: "21791" })],
201
+ count: 1
202
+ });
203
+ await expect(
204
+ ztaReportEndpoint?.handle({
205
+ req: {},
206
+ url: new URL(
207
+ "http://localhost/api/data/zeroTrustAssessment/report?filter[0][column]=RelatedObjects&filter[0][value][0]=object-1"
208
+ )
209
+ })
210
+ ).resolves.toMatchObject({
211
+ rows: [],
212
+ count: 0
213
+ });
214
+ await expect(
215
+ ztaReportEndpoint?.handle({
216
+ req: {},
217
+ url: new URL(
218
+ "http://localhost/api/data/zeroTrustAssessment/report?filter[0][column]=RelatedObjects&filter[0][value][0]=Application"
219
+ )
220
+ })
221
+ ).resolves.toMatchObject({
222
+ rows: [],
223
+ count: 0
224
+ });
225
+ } finally {
226
+ await runtime.close();
227
+ await rm(dataDir, { force: true, recursive: true });
228
+ }
229
+ });
230
+
231
+ test("fills Zero Trust Assessment related object application ids through the REST endpoint", async () => {
232
+ const dataDir = await mkdtemp(path.join(tmpdir(), "ownerlens-runtime-"));
233
+ const runtime = new LocalReportRuntime({ dataDir });
234
+ const payrollServicePrincipal = servicePrincipal("sp-1", "client-app-1", "Payroll API", "Application");
235
+ payrollServicePrincipal.tags = ["WindowsAzureActiveDirectoryIntegratedApp", "HideApp"];
236
+ const entraSnapshot: EntraSnapshot = {
237
+ meta: {
238
+ provider: "entra",
239
+ snapshotVersion: "1",
240
+ createdAt: "2026-06-05T00:00:00.000Z",
241
+ tenantId: "tenant-1",
242
+ account: "owner@example.test",
243
+ scopes: [],
244
+ servicePrincipalCount: 1
245
+ },
246
+ servicePrincipals: [payrollServicePrincipal],
247
+ applications: [application("application-object-1", "client-app-1", "Payroll app registration")],
248
+ oauth2PermissionGrants: [],
249
+ appRoleAssignments: []
250
+ };
251
+ const report: ZeroTrustAssessmentReport = {
252
+ ExecutedAt: "2026-06-03T10:00:00.000Z",
253
+ TenantId: "tenant-1",
254
+ TestResultSummary: { IdentityFailed: 1 },
255
+ Tests: [
256
+ {
257
+ TestId: "sp-test",
258
+ TestStatus: "Failed",
259
+ RelatedObjects: [
260
+ {
261
+ object_id: "sp-1",
262
+ displayName: "Payroll API",
263
+ servicePrincipalType: "Application"
264
+ }
265
+ ]
266
+ }
267
+ ]
268
+ };
269
+
270
+ try {
271
+ await writeFile(path.join(dataDir, "entra-snapshot.json"), JSON.stringify(entraSnapshot), "utf8");
272
+ await writeFile(path.join(dataDir, "zta-report.json"), JSON.stringify(report), "utf8");
273
+ await runtime.initialize();
274
+
275
+ const endpoint = defineLocalReportRuntimeRestEndpoints(runtime).find(
276
+ (candidate) => candidate.path === "/api/data/zeroTrustAssessment/report"
277
+ );
278
+
279
+ await expect(
280
+ endpoint?.handle({
281
+ req: {},
282
+ url: new URL("http://localhost/api/data/zeroTrustAssessment/report")
283
+ })
284
+ ).resolves.toMatchObject({
285
+ rows: [
286
+ expect.objectContaining({
287
+ RelatedObjects: [
288
+ expect.objectContaining({
289
+ object_id: "sp-1",
290
+ servicePrincipalId: "sp-1",
291
+ tags: ["WindowsAzureActiveDirectoryIntegratedApp", "HideApp"],
292
+ applicationId: "application-object-1"
293
+ })
294
+ ]
295
+ })
296
+ ],
297
+ Tests: [
298
+ expect.objectContaining({
299
+ RelatedObjects: [
300
+ expect.objectContaining({
301
+ object_id: "sp-1",
302
+ servicePrincipalId: "sp-1",
303
+ tags: ["WindowsAzureActiveDirectoryIntegratedApp", "HideApp"],
304
+ applicationId: "application-object-1"
305
+ })
306
+ ]
307
+ })
308
+ ]
309
+ });
310
+ await expect(
311
+ endpoint?.handle({
312
+ req: {},
313
+ url: new URL(
314
+ "http://localhost/api/data/zeroTrustAssessment/report?filter[0][column]=RelatedObjects&filter[0][value][0]=sp-1"
315
+ )
316
+ })
317
+ ).resolves.toMatchObject({
318
+ rows: [expect.objectContaining({ TestId: "sp-test" })],
319
+ count: 1
320
+ });
321
+ await expect(
322
+ endpoint?.handle({
323
+ req: {},
324
+ url: new URL(
325
+ "http://localhost/api/data/zeroTrustAssessment/report?filter[0][column]=RelatedObjects&filter[0][value][0]=HideApp"
326
+ )
327
+ })
328
+ ).resolves.toMatchObject({
329
+ rows: [expect.objectContaining({ TestId: "sp-test" })],
330
+ count: 1
331
+ });
332
+ } finally {
333
+ await runtime.close();
334
+ await rm(dataDir, { force: true, recursive: true });
335
+ }
336
+ });
337
+
338
+ test("reads the latest Zero Trust Assessment report from DuckDB by execution time", async () => {
339
+ const instance = await DuckDBInstance.create(":memory:");
340
+ const connection = await instance.connect();
341
+ const olderReport: ZeroTrustAssessmentReport = {
342
+ ExecutedAt: "2026-06-01T10:00:00.000Z",
343
+ TenantId: "tenant-old",
344
+ TestResultSummary: { IdentityPassed: 1 },
345
+ Tests: [{ TestId: "old", TestStatus: "Failed" }]
346
+ };
347
+ const latestReport: ZeroTrustAssessmentReport = {
348
+ ExecutedAt: "2026-06-03T10:00:00.000Z",
349
+ TenantId: "tenant-latest",
350
+ TestResultSummary: { IdentityPassed: 2 },
351
+ CustomTopLevelField: "preserved",
352
+ Tests: [{ TestId: "latest", TestStatus: "Passed" }]
353
+ };
354
+
355
+ try {
356
+ await prepareRuntimeSqlSchema(connection);
357
+ await importZeroTrustAssessmentReportToDuckDb(connection, olderReport, "older-zta-report.json");
358
+ await importZeroTrustAssessmentReportToDuckDb(connection, latestReport, "latest-zta-report.json");
359
+
360
+ await expect(readZeroTrustAssessmentReportFromDuckDb(connection)).resolves.toMatchObject({
361
+ TenantId: "tenant-latest",
362
+ CustomTopLevelField: "preserved",
363
+ Tests: [{ TestId: "latest", TestStatus: "Passed" }]
364
+ });
365
+ } finally {
366
+ connection.disconnectSync();
367
+ instance.closeSync();
368
+ }
369
+ });
370
+
371
+ test("imports Zero Trust Assessment related object ids for service principal joins", async () => {
372
+ const instance = await DuckDBInstance.create(":memory:");
373
+ const connection = await instance.connect();
374
+ const report: ZeroTrustAssessmentReport = {
375
+ ExecutedAt: "2026-06-03T10:00:00.000Z",
376
+ TenantId: "tenant-1",
377
+ TestResultSummary: { IdentityFailed: 1 },
378
+ Tests: [
379
+ {
380
+ TestId: "app-test",
381
+ TestStatus: "Failed",
382
+ RelatedObjects: [
383
+ { object_id: "sp-1", displayName: "Application app", servicePrincipalType: "Application" },
384
+ { id: "sp-2", displayName: "Application app by id", servicePrincipalType: "Application" },
385
+ { object_id: "mi-1", displayName: "Managed identity", servicePrincipalType: "ManagedIdentity" },
386
+ { object_id: "sp-1", displayName: "Duplicate app reference", servicePrincipalType: "Application" },
387
+ { displayName: "No object id" }
388
+ ]
389
+ },
390
+ {
391
+ TestId: "empty-test",
392
+ TestStatus: "Passed",
393
+ RelatedObjects: []
394
+ }
395
+ ]
396
+ };
397
+
398
+ try {
399
+ await prepareRuntimeSqlSchema(connection);
400
+ await insertEntraServicePrincipalRows(connection, [
401
+ servicePrincipal("sp-1", "app-1", "Application app", "Application"),
402
+ servicePrincipal("sp-2", "app-2", "Application app by id", "Application"),
403
+ servicePrincipal("mi-1", "mi-app-1", "Managed identity", "ManagedIdentity")
404
+ ]);
405
+
406
+ const status = await importZeroTrustAssessmentReportToDuckDb(connection, report, "zta-report.json");
407
+
408
+ const relatedRows = await connection.runAndReadAll(
409
+ `
410
+ select report_id, test_ordinal, related_object_id
411
+ from zta_test_related_objects
412
+ order by test_ordinal, related_object_id
413
+ `
414
+ );
415
+ expect(relatedRows.getRowObjectsJson()).toEqual([
416
+ {
417
+ report_id: status.reportId,
418
+ test_ordinal: 0,
419
+ related_object_id: "mi-1"
420
+ },
421
+ {
422
+ report_id: status.reportId,
423
+ test_ordinal: 0,
424
+ related_object_id: "sp-1"
425
+ },
426
+ {
427
+ report_id: status.reportId,
428
+ test_ordinal: 0,
429
+ related_object_id: "sp-2"
430
+ }
431
+ ]);
432
+
433
+ const joinedRows = await connection.runAndReadAll(
434
+ `
435
+ select
436
+ test.test_id,
437
+ related.related_object_id,
438
+ service_principal.service_principal_type
439
+ from zta_test_related_objects related
440
+ join zta_tests test
441
+ on test.report_id = related.report_id
442
+ and test.ordinal = related.test_ordinal
443
+ join entra_service_principals service_principal
444
+ on service_principal.id = related.related_object_id
445
+ order by related.related_object_id
446
+ `
447
+ );
448
+ expect(joinedRows.getRowObjectsJson()).toEqual([
449
+ {
450
+ test_id: "app-test",
451
+ related_object_id: "mi-1",
452
+ service_principal_type: "ManagedIdentity"
453
+ },
454
+ {
455
+ test_id: "app-test",
456
+ related_object_id: "sp-1",
457
+ service_principal_type: "Application"
458
+ },
459
+ {
460
+ test_id: "app-test",
461
+ related_object_id: "sp-2",
462
+ service_principal_type: "Application"
463
+ }
464
+ ]);
465
+ } finally {
466
+ connection.disconnectSync();
467
+ instance.closeSync();
468
+ }
469
+ });
470
+
471
+ test("enriches Zero Trust Assessment related objects with application object ids", async () => {
472
+ const instance = await DuckDBInstance.create(":memory:");
473
+ const connection = await instance.connect();
474
+ const report: ZeroTrustAssessmentReport = {
475
+ ExecutedAt: "2026-06-03T10:00:00.000Z",
476
+ TenantId: "tenant-1",
477
+ Tests: [
478
+ {
479
+ TestId: "sp-test",
480
+ TestStatus: "Failed",
481
+ RelatedObjects: [
482
+ { object_id: "sp-1", displayName: "Application app", servicePrincipalType: "Application" },
483
+ { id: "sp-2", displayName: "Application without registration", servicePrincipalType: "Application" },
484
+ { object_id: "user-1", userPrincipalName: "user@example.test" }
485
+ ]
486
+ }
487
+ ]
488
+ };
489
+
490
+ try {
491
+ await prepareRuntimeSqlSchema(connection);
492
+ const taggedServicePrincipal = servicePrincipal("sp-1", "app-1", "Application app", "Application");
493
+ taggedServicePrincipal.tags = ["WindowsAzureActiveDirectoryIntegratedApp", "HideApp"];
494
+ await insertEntraServicePrincipalRows(connection, [
495
+ taggedServicePrincipal,
496
+ servicePrincipal("sp-2", "app-2", "Application without registration", "Application")
497
+ ]);
498
+ await insertEntraApplicationRows(connection, [
499
+ application("application-object-1", "app-1", "Application app registration")
500
+ ]);
501
+
502
+ await importZeroTrustAssessmentReportToDuckDb(connection, report, "zta-report.json");
503
+
504
+ await expect(readZeroTrustAssessmentReportFromDuckDb(connection)).resolves.toMatchObject({
505
+ Tests: [
506
+ {
507
+ RelatedObjects: [
508
+ expect.objectContaining({
509
+ object_id: "sp-1",
510
+ applicationId: "application-object-1",
511
+ tags: ["WindowsAzureActiveDirectoryIntegratedApp", "HideApp"]
512
+ }),
513
+ expect.objectContaining({
514
+ id: "sp-2",
515
+ applicationId: null
516
+ }),
517
+ expect.not.objectContaining({
518
+ applicationId: expect.anything()
519
+ })
520
+ ]
521
+ }
522
+ ]
523
+ });
524
+
525
+ const relatedRows = await connection.runAndReadAll(
526
+ `
527
+ select related_object_id
528
+ from zta_test_related_objects
529
+ order by related_object_id
530
+ `
531
+ );
532
+ expect(relatedRows.getRowObjectsJson()).toEqual([
533
+ { related_object_id: "application-object-1" },
534
+ { related_object_id: "sp-1" },
535
+ { related_object_id: "sp-2" },
536
+ { related_object_id: "user-1" }
537
+ ]);
538
+ } finally {
539
+ connection.disconnectSync();
540
+ instance.closeSync();
541
+ }
542
+ });
543
+
544
+ test("imports Entra snapshot into DuckDB and reads it back through the runtime", async () => {
545
+ const dataDir = await mkdtemp(path.join(tmpdir(), "ownerlens-runtime-"));
546
+ const runtime = new LocalReportRuntime({ dataDir });
547
+
548
+ const snapshot: EntraSnapshot & { groups: Array<{ id: string }> } = {
549
+ meta: {
550
+ provider: "entra",
551
+ snapshotVersion: "1",
552
+ createdAt: "2026-06-05T00:00:00.000Z",
553
+ tenantId: "tenant-1",
554
+ account: "owner@example.test",
555
+ scopes: ["Application.Read.All"],
556
+ servicePrincipalCount: 2,
557
+ applicationCount: 1,
558
+ oauth2PermissionGrantCount: 1,
559
+ appRoleAssignmentCount: 1
560
+ },
561
+ servicePrincipals: [
562
+ {
563
+ id: "sp-1",
564
+ appId: "app-1",
565
+ displayName: "Example app",
566
+ appDisplayName: "Example app registration",
567
+ servicePrincipalType: "Application",
568
+ publisherName: null,
569
+ accountEnabled: true,
570
+ appOwnerOrganizationId: "tenant-1",
571
+ homepage: null,
572
+ loginUrl: null,
573
+ replyUrls: ["https://example.test/callback"],
574
+ servicePrincipalNames: ["api://example"],
575
+ tags: ["WindowsAzureActiveDirectoryIntegratedApp"],
576
+ appRoles: [
577
+ {
578
+ id: "role-1",
579
+ value: "Read.All",
580
+ displayName: "Read",
581
+ description: null,
582
+ isEnabled: true,
583
+ allowedMemberTypes: ["Application"]
584
+ }
585
+ ],
586
+ owners: [{ id: "owner-1", displayName: "Owner One" }],
587
+ metadata: { source: "test" }
588
+ },
589
+ {
590
+ id: "mi-1",
591
+ appId: "mi-app-1",
592
+ displayName: "Example managed identity",
593
+ appDisplayName: null,
594
+ servicePrincipalType: "ManagedIdentity",
595
+ publisherName: null,
596
+ accountEnabled: true,
597
+ appOwnerOrganizationId: "tenant-1",
598
+ homepage: null,
599
+ loginUrl: null,
600
+ replyUrls: [],
601
+ servicePrincipalNames: [],
602
+ tags: ["WindowsAzureActiveDirectoryManagedIdentity"],
603
+ appRoles: [],
604
+ owners: [],
605
+ metadata: null
606
+ }
607
+ ],
608
+ applications: [
609
+ {
610
+ id: "application-object-1",
611
+ appId: "app-1",
612
+ displayName: "Example app registration",
613
+ signInAudience: "AzureADMyOrg",
614
+ publisherDomain: "example.test",
615
+ identifierUris: ["api://example"],
616
+ tags: ["WindowsAzureActiveDirectoryIntegratedApp"],
617
+ appRoles: [
618
+ {
619
+ id: "role-1",
620
+ value: "Read.All",
621
+ displayName: "Read",
622
+ description: "Read access",
623
+ isEnabled: true,
624
+ allowedMemberTypes: ["Application"]
625
+ }
626
+ ],
627
+ oauth2PermissionScopes: [
628
+ {
629
+ id: "scope-1",
630
+ value: "user_impersonation",
631
+ adminConsentDisplayName: "Access Example API",
632
+ isEnabled: true,
633
+ type: "User"
634
+ }
635
+ ],
636
+ requiredResourceAccess: [
637
+ {
638
+ resourceAppId: "00000003-0000-0000-c000-000000000000",
639
+ resourceAccess: [{ id: "permission-1", type: "Scope" }]
640
+ }
641
+ ],
642
+ web: {
643
+ redirectUris: ["https://example.test/callback"],
644
+ implicitGrantSettings: { enableAccessTokenIssuance: false, enableIdTokenIssuance: true }
645
+ },
646
+ spa: {
647
+ redirectUris: ["https://spa.example.test/callback"]
648
+ },
649
+ publicClient: {
650
+ redirectUris: ["http://localhost"]
651
+ },
652
+ passwordCredentials: [
653
+ {
654
+ keyId: "password-key-1",
655
+ displayName: "client secret",
656
+ hint: "abc",
657
+ startDateTime: "2026-01-01T00:00:00.000Z",
658
+ endDateTime: "2026-12-31T00:00:00.000Z",
659
+ secretText: "must-not-survive"
660
+ }
661
+ ],
662
+ keyCredentials: [
663
+ {
664
+ keyId: "certificate-key-1",
665
+ displayName: "certificate",
666
+ type: "AsymmetricX509Cert",
667
+ usage: "Verify",
668
+ customKeyIdentifier: "AQID",
669
+ startDateTime: "2026-01-01T00:00:00.000Z",
670
+ endDateTime: "2026-12-31T00:00:00.000Z"
671
+ }
672
+ ],
673
+ createdDateTime: "2026-01-01T00:00:00.000Z",
674
+ deletedDateTime: null,
675
+ disabledByMicrosoftStatus: null,
676
+ info: {
677
+ termsOfServiceUrl: "https://example.test/terms",
678
+ supportUrl: "https://example.test/support"
679
+ },
680
+ notes: "Business critical app",
681
+ owners: [{ id: "app-owner-1", mail: "app-owner@example.test", ownerType: "#microsoft.graph.user" }]
682
+ }
683
+ ],
684
+ oauth2PermissionGrants: [
685
+ {
686
+ id: "grant-1",
687
+ clientId: "sp-1",
688
+ consentType: "AllPrincipals",
689
+ principalId: null,
690
+ resourceId: "graph",
691
+ scope: "User.Read"
692
+ },
693
+ {
694
+ id: "grant-2",
695
+ clientId: "mi-1",
696
+ consentType: "Principal",
697
+ principalId: "user-1",
698
+ resourceId: "sharepoint",
699
+ scope: "Sites.Read.All Files.Read.All"
700
+ },
701
+ {
702
+ id: "grant-3",
703
+ clientId: "external-1",
704
+ consentType: "FutureConsentType",
705
+ principalId: null,
706
+ resourceId: "graph",
707
+ scope: "Mail.Read"
708
+ }
709
+ ],
710
+ appRoleAssignments: [
711
+ {
712
+ id: "assignment-1",
713
+ appRoleId: "role-1",
714
+ appRoleDisplayName: "Read",
715
+ appRoleValue: "Read.All",
716
+ principalId: "sp-1",
717
+ principalDisplayName: "Example app",
718
+ resourceId: "graph",
719
+ resourceDisplayName: "Microsoft Graph"
720
+ },
721
+ {
722
+ id: "assignment-2",
723
+ appRoleId: "role-2",
724
+ appRoleDisplayName: null,
725
+ appRoleValue: null,
726
+ principalId: "mi-1",
727
+ principalDisplayName: null,
728
+ resourceId: "sharepoint",
729
+ resourceDisplayName: null
730
+ }
731
+ ],
732
+ groups: [{ id: "group-1" }]
733
+ };
734
+
735
+ try {
736
+ await writeFile(path.join(dataDir, "entra-snapshot.json"), JSON.stringify(snapshot), "utf8");
737
+ await runtime.initialize();
738
+
739
+ expect(runtime.getStatus().entra).toMatchObject({
740
+ imported: true,
741
+ servicePrincipalCount: 2,
742
+ applicationCount: 1,
743
+ oauth2PermissionGrantCount: 3,
744
+ appRoleAssignmentCount: 2
745
+ });
746
+
747
+ const imported = (await runtime.readSnapshot("entra-snapshot.json")) as EntraSnapshot & {
748
+ groups: Array<{ id: string }>;
749
+ };
750
+ const queried = await runtime.queryEntraServicePrincipals({
751
+ filters: [
752
+ { column: "displayName", values: ["Example", "Missing"] },
753
+ { column: "accountEnabled", values: ["true"] }
754
+ ],
755
+ page: 1,
756
+ pageSize: 10
757
+ });
758
+ const queriedManagedIdentities = await runtime.queryEntraManagedIdentities({
759
+ page: 1,
760
+ pageSize: 10
761
+ });
762
+ const principalPermissions = await runtime.readEntraPrincipalPermissions("SP-1");
763
+ const endpoints = defineLocalReportRuntimeRestEndpoints(runtime);
764
+ const servicePrincipalsEndpoint = endpoints.find(
765
+ (endpoint) => endpoint.path === "/api/data/entra/servicePrincipals"
766
+ );
767
+ const managedIdentitiesEndpoint = endpoints.find((endpoint) => endpoint.path === "/api/data/entra/managedIdentities");
768
+ const oauth2PermissionGrantsEndpoint = endpoints.find(
769
+ (endpoint) => endpoint.path === "/api/data/entra/oauth2PermissionGrants"
770
+ );
771
+
772
+ if (!servicePrincipalsEndpoint || !managedIdentitiesEndpoint || !oauth2PermissionGrantsEndpoint) {
773
+ throw new Error("Expected Entra REST endpoints to be registered.");
774
+ }
775
+
776
+ const restServicePrincipals = await servicePrincipalsEndpoint.handle({
777
+ req: {},
778
+ url: new URL("http://localhost/api/data/entra/servicePrincipals?page=1&count=10")
779
+ });
780
+ const restManagedIdentities = await managedIdentitiesEndpoint.handle({
781
+ req: {},
782
+ url: new URL("http://localhost/api/data/entra/managedIdentities?page=1&count=10")
783
+ });
784
+ const restOAuth2PermissionGrants = await oauth2PermissionGrantsEndpoint.handle({
785
+ req: {},
786
+ url: new URL("http://localhost/api/data/entra/oauth2PermissionGrants?page=1&count=10")
787
+ });
788
+
789
+ expect(imported.meta?.provider).toBe("entra");
790
+ expect(imported.servicePrincipals).toHaveLength(2);
791
+ expect(imported.servicePrincipals[0]).toMatchObject({
792
+ id: "sp-1",
793
+ appRoles: [{ id: "role-1" }],
794
+ metadata: { source: "test" },
795
+ owners: [{ id: "owner-1" }]
796
+ });
797
+ expect(imported.applications).toHaveLength(1);
798
+ expect(imported.applications?.[0]).toMatchObject({
799
+ id: "application-object-1",
800
+ appId: "app-1",
801
+ displayName: "Example app registration",
802
+ oauth2PermissionScopes: [{ id: "scope-1", value: "user_impersonation" }],
803
+ requiredResourceAccess: [{ resourceAppId: "00000003-0000-0000-c000-000000000000" }],
804
+ web: { redirectUris: ["https://example.test/callback"] },
805
+ spa: { redirectUris: ["https://spa.example.test/callback"] },
806
+ publicClient: { redirectUris: ["http://localhost"] },
807
+ passwordCredentials: [
808
+ {
809
+ keyId: "password-key-1",
810
+ displayName: "client secret",
811
+ hint: "abc",
812
+ startDateTime: "2026-01-01T00:00:00.000Z",
813
+ endDateTime: "2026-12-31T00:00:00.000Z"
814
+ }
815
+ ],
816
+ keyCredentials: [{ keyId: "certificate-key-1", usage: "Verify" }],
817
+ owners: [{ id: "app-owner-1", mail: "app-owner@example.test" }]
818
+ });
819
+ expect(imported.applications?.[0].passwordCredentials[0]).not.toHaveProperty("secretText");
820
+ expect(imported.oauth2PermissionGrants).toEqual(snapshot.oauth2PermissionGrants);
821
+ expect(imported.oauth2PermissionGrants?.[0]).not.toHaveProperty("risk");
822
+ expect(imported.appRoleAssignments).toEqual(snapshot.appRoleAssignments);
823
+ expect(principalPermissions).toEqual({
824
+ principalId: "SP-1",
825
+ oauth2PermissionGrants: [
826
+ {
827
+ id: "grant-1",
828
+ clientId: "sp-1",
829
+ consentType: "AllPrincipals",
830
+ principalId: null,
831
+ resourceId: "graph",
832
+ risk: "high",
833
+ scope: "User.Read"
834
+ }
835
+ ],
836
+ appRoleAssignments: [
837
+ {
838
+ id: "assignment-1",
839
+ appRoleId: "role-1",
840
+ appRoleDisplayName: "Read",
841
+ appRoleValue: "Read.All",
842
+ principalId: "sp-1",
843
+ principalDisplayName: "Example app",
844
+ resourceId: "graph",
845
+ resourceDisplayName: "Microsoft Graph"
846
+ }
847
+ ]
848
+ });
849
+ expect(imported.groups).toEqual([{ id: "group-1" }]);
850
+ expect(queried).toMatchObject({
851
+ collectionId: "entra.servicePrincipals",
852
+ columns: expect.arrayContaining(["id", "displayName"]),
853
+ count: 1,
854
+ page: 1,
855
+ pageSize: 10,
856
+ rows: [
857
+ expect.objectContaining({
858
+ id: "sp-1",
859
+ displayName: "Example app",
860
+ oauthPemrissionsCount: 1,
861
+ appRolesPermissionCount: 1,
862
+ entraPermissionRisk: "high",
863
+ rbacRoleAssignmentCount: 0,
864
+ rbacRoleLevel: "none",
865
+ rbacSubscriptionCount: 0
866
+ })
867
+ ]
868
+ });
869
+ expect(queriedManagedIdentities).toMatchObject({
870
+ collectionId: "entra.managedIdentities",
871
+ count: 1,
872
+ rows: [
873
+ expect.objectContaining({
874
+ id: "mi-1",
875
+ servicePrincipalType: "ManagedIdentity",
876
+ oauthPemrissionsCount: 2,
877
+ appRolesPermissionCount: 1,
878
+ entraPermissionRisk: "medium",
879
+ rbacRoleAssignmentCount: 0,
880
+ rbacRoleLevel: "none",
881
+ rbacSubscriptionCount: 0
882
+ })
883
+ ]
884
+ });
885
+ expect(restServicePrincipals).toMatchObject({
886
+ collectionId: "entra.servicePrincipals",
887
+ columns: expect.arrayContaining(["oauthPemrissionsCount", "appRolesPermissionCount", "entraPermissionRisk"]),
888
+ rows: [
889
+ expect.objectContaining({
890
+ id: "sp-1",
891
+ oauthPemrissionsCount: 1,
892
+ appRolesPermissionCount: 1,
893
+ entraPermissionRisk: "high"
894
+ })
895
+ ]
896
+ });
897
+ expect(restManagedIdentities).toMatchObject({
898
+ collectionId: "entra.managedIdentities",
899
+ columns: expect.arrayContaining(["oauthPemrissionsCount", "appRolesPermissionCount", "entraPermissionRisk"]),
900
+ rows: [
901
+ expect.objectContaining({
902
+ id: "mi-1",
903
+ oauthPemrissionsCount: 2,
904
+ appRolesPermissionCount: 1,
905
+ entraPermissionRisk: "medium"
906
+ })
907
+ ]
908
+ });
909
+ expect(restOAuth2PermissionGrants).toMatchObject({
910
+ collectionId: "entra.oauth2PermissionGrants",
911
+ columns: expect.arrayContaining(["id", "consentType", "risk"]),
912
+ rows: [
913
+ expect.objectContaining({ id: "grant-1", risk: "high" }),
914
+ expect.objectContaining({ id: "grant-2", risk: "low" }),
915
+ expect.objectContaining({ id: "grant-3", risk: "medium" })
916
+ ]
917
+ });
918
+ } finally {
919
+ await runtime.close();
920
+ await rm(dataDir, { force: true, recursive: true });
921
+ }
922
+ });
923
+
924
+ test("imports legacy Entra snapshots without applications as an empty applications collection", async () => {
925
+ const dataDir = await mkdtemp(path.join(tmpdir(), "ownerlens-runtime-"));
926
+ const runtime = new LocalReportRuntime({ dataDir });
927
+
928
+ const snapshot: EntraSnapshot = {
929
+ meta: {
930
+ provider: "entra",
931
+ snapshotVersion: "0.3",
932
+ createdAt: "2026-06-05T00:00:00.000Z",
933
+ tenantId: "tenant-1",
934
+ account: "owner@example.test",
935
+ scopes: ["Application.Read.All"],
936
+ servicePrincipalCount: 0
937
+ },
938
+ servicePrincipals: [],
939
+ oauth2PermissionGrants: [],
940
+ appRoleAssignments: []
941
+ };
942
+
943
+ try {
944
+ await writeFile(path.join(dataDir, "entra-snapshot.json"), JSON.stringify(snapshot), "utf8");
945
+ await runtime.initialize();
946
+
947
+ expect(runtime.getStatus().entra).toMatchObject({
948
+ imported: true,
949
+ servicePrincipalCount: 0,
950
+ applicationCount: 0
951
+ });
952
+
953
+ const imported = await runtime.readSnapshot("entra-snapshot.json");
954
+
955
+ expect((imported as EntraSnapshot).applications).toEqual([]);
956
+ } finally {
957
+ await runtime.close();
958
+ await rm(dataDir, { force: true, recursive: true });
959
+ }
960
+ });
961
+
962
+ test("enriches Entra runtime collections with latest ZTA remediation summaries", async () => {
963
+ const dataDir = await mkdtemp(path.join(tmpdir(), "ownerlens-runtime-"));
964
+ const runtime = new LocalReportRuntime({ dataDir });
965
+ const entraSnapshot: EntraSnapshot = {
966
+ meta: {
967
+ provider: "entra",
968
+ snapshotVersion: "1",
969
+ createdAt: "2026-06-05T00:00:00.000Z",
970
+ tenantId: "tenant-1",
971
+ account: "owner@example.test",
972
+ scopes: [],
973
+ servicePrincipalCount: 2
974
+ },
975
+ servicePrincipals: [
976
+ servicePrincipal("sp-1", "app-1", "Application app", "Application"),
977
+ servicePrincipal("principal-uami-1", "client-1", "Identity app", "ManagedIdentity")
978
+ ],
979
+ oauth2PermissionGrants: [],
980
+ appRoleAssignments: []
981
+ };
982
+ const olderReport: ZeroTrustAssessmentReport = {
983
+ ExecutedAt: "2026-06-01T10:00:00.000Z",
984
+ TenantId: "tenant-1",
985
+ TestResultSummary: { IdentityFailed: 1 },
986
+ Tests: [
987
+ {
988
+ TestId: "older-sp-test",
989
+ TestStatus: "Failed",
990
+ TestRisk: "High",
991
+ RelatedObjects: [{ object_id: "sp-1" }]
992
+ }
993
+ ]
994
+ };
995
+ const latestReport: ZeroTrustAssessmentReport = {
996
+ ExecutedAt: "2026-06-03T10:00:00.000Z",
997
+ TenantId: "tenant-1",
998
+ TestResultSummary: { IdentityFailed: 2 },
999
+ Tests: [
1000
+ {
1001
+ TestId: "sp-failed",
1002
+ TestStatus: "failed",
1003
+ TestRisk: "High",
1004
+ RelatedObjects: [
1005
+ { object_id: "SP-1", displayName: "Application app" },
1006
+ { id: "sp-1", displayName: "Duplicate application app" }
1007
+ ]
1008
+ },
1009
+ {
1010
+ TestId: "sp-and-mi-passed",
1011
+ TestStatus: "Passed",
1012
+ TestRisk: "Medium",
1013
+ RelatedObjects: [{ id: "sp-1" }, { object_id: "principal-uami-1" }]
1014
+ },
1015
+ {
1016
+ TestId: "mi-failed",
1017
+ TestStatus: "Failed",
1018
+ TestRisk: "Low",
1019
+ RelatedObjects: [{ id: "principal-uami-1" }]
1020
+ }
1021
+ ]
1022
+ };
1023
+
1024
+ try {
1025
+ await writeFile(path.join(dataDir, "entra-snapshot.json"), JSON.stringify(entraSnapshot), "utf8");
1026
+ await writeFile(path.join(dataDir, "older-zta-report.json"), JSON.stringify(olderReport), "utf8");
1027
+ await writeFile(path.join(dataDir, "latest-zta-report.json"), JSON.stringify(latestReport), "utf8");
1028
+ await runtime.initialize();
1029
+
1030
+ const queriedServicePrincipals = await runtime.queryEntraServicePrincipals({
1031
+ page: 1,
1032
+ pageSize: 10
1033
+ });
1034
+ const queriedManagedIdentities = await runtime.queryEntraManagedIdentities({
1035
+ page: 1,
1036
+ pageSize: 10
1037
+ });
1038
+
1039
+ expect(queriedServicePrincipals).toMatchObject({
1040
+ collectionId: "entra.servicePrincipals",
1041
+ columns: expect.arrayContaining(["ztaRemediationCountAll", "ztaRemediationFailedCount", "ztaMaxRisk"]),
1042
+ rows: [
1043
+ expect.objectContaining({
1044
+ id: "sp-1",
1045
+ ztaRemediationCountAll: 2,
1046
+ ztaRemediationFailedCount: 1,
1047
+ ztaMaxRisk: "high"
1048
+ })
1049
+ ]
1050
+ });
1051
+ expect(queriedManagedIdentities).toMatchObject({
1052
+ collectionId: "entra.managedIdentities",
1053
+ columns: expect.arrayContaining(["ztaRemediationCountAll", "ztaRemediationFailedCount", "ztaMaxRisk"]),
1054
+ rows: [
1055
+ expect.objectContaining({
1056
+ id: "principal-uami-1",
1057
+ ztaRemediationCountAll: 2,
1058
+ ztaRemediationFailedCount: 1,
1059
+ ztaMaxRisk: "medium"
1060
+ })
1061
+ ]
1062
+ });
1063
+ } finally {
1064
+ await runtime.close();
1065
+ await rm(dataDir, { force: true, recursive: true });
1066
+ }
1067
+ });
1068
+
1069
+ test("enriches service principals with ZTA remediations related to application object ids", async () => {
1070
+ const dataDir = await mkdtemp(path.join(tmpdir(), "ownerlens-runtime-"));
1071
+ const runtime = new LocalReportRuntime({ dataDir });
1072
+ const entraSnapshot: EntraSnapshot = {
1073
+ meta: {
1074
+ provider: "entra",
1075
+ snapshotVersion: "1",
1076
+ createdAt: "2026-06-05T00:00:00.000Z",
1077
+ tenantId: "tenant-1",
1078
+ account: "owner@example.test",
1079
+ scopes: [],
1080
+ servicePrincipalCount: 2,
1081
+ applicationCount: 1
1082
+ },
1083
+ servicePrincipals: [
1084
+ servicePrincipal("sp-1", "app-1", "Application app", "Application"),
1085
+ servicePrincipal("sp-2", "app-2", "Other application app", "Application")
1086
+ ],
1087
+ applications: [application("application-object-1", "app-1", "Application app registration")],
1088
+ oauth2PermissionGrants: [],
1089
+ appRoleAssignments: []
1090
+ };
1091
+ const report: ZeroTrustAssessmentReport = {
1092
+ ExecutedAt: "2026-06-03T10:00:00.000Z",
1093
+ TenantId: "tenant-1",
1094
+ TestResultSummary: { IdentityFailed: 2 },
1095
+ Tests: [
1096
+ {
1097
+ TestId: "app-object-failed",
1098
+ TestStatus: "Failed",
1099
+ TestRisk: "High",
1100
+ RelatedObjects: [{ id: "application-object-1" }]
1101
+ },
1102
+ {
1103
+ TestId: "app-object-and-sp-deduped",
1104
+ TestStatus: "Passed",
1105
+ TestRisk: "Medium",
1106
+ RelatedObjects: [{ id: "application-object-1" }, { object_id: "sp-1" }]
1107
+ }
1108
+ ]
1109
+ };
1110
+
1111
+ try {
1112
+ await writeFile(path.join(dataDir, "entra-snapshot.json"), JSON.stringify(entraSnapshot), "utf8");
1113
+ await writeFile(path.join(dataDir, "zta-report.json"), JSON.stringify(report), "utf8");
1114
+ await runtime.initialize();
1115
+
1116
+ const queriedServicePrincipals = await runtime.queryEntraServicePrincipals({
1117
+ page: 1,
1118
+ pageSize: 10
1119
+ });
1120
+
1121
+ expect(queriedServicePrincipals).toMatchObject({
1122
+ collectionId: "entra.servicePrincipals",
1123
+ rows: [
1124
+ expect.objectContaining({
1125
+ id: "sp-1",
1126
+ ztaRemediationCountAll: 2,
1127
+ ztaRemediationFailedCount: 1,
1128
+ ztaMaxRisk: "high"
1129
+ }),
1130
+ expect.objectContaining({
1131
+ id: "sp-2",
1132
+ ztaRemediationCountAll: 0,
1133
+ ztaRemediationFailedCount: 0,
1134
+ ztaMaxRisk: "none"
1135
+ })
1136
+ ]
1137
+ });
1138
+ } finally {
1139
+ await runtime.close();
1140
+ await rm(dataDir, { force: true, recursive: true });
1141
+ }
1142
+ });
1143
+
1144
+ test("imports Azure resources snapshot into DuckDB and reads it back through the runtime", async () => {
1145
+ const dataDir = await mkdtemp(path.join(tmpdir(), "ownerlens-runtime-"));
1146
+ const runtime = new LocalReportRuntime({ dataDir });
1147
+
1148
+ const snapshot: AzureSnapshot & { ownershipHints: Array<{ id: string }> } = {
1149
+ meta: {
1150
+ provider: "azure",
1151
+ snapshotVersion: "0.4",
1152
+ createdAt: "2026-06-05T00:00:00.000Z",
1153
+ activityDays: 90,
1154
+ activityStartTime: "2026-03-07T00:00:00.000Z",
1155
+ maxActivityRecords: 10000,
1156
+ requestedSubscriptions: ["sub-1"],
1157
+ subscriptionCount: 1,
1158
+ resourceGroupCount: 1,
1159
+ resourceCount: 1,
1160
+ userAssignedManagedIdentityCount: 1,
1161
+ roleAssignmentCount: 1,
1162
+ activityLogCount: 1
1163
+ },
1164
+ subscriptions: [
1165
+ {
1166
+ subscriptionId: "sub-1",
1167
+ subscriptionName: "Subscription One",
1168
+ tenantId: "tenant-1",
1169
+ state: "Enabled",
1170
+ tags: null
1171
+ }
1172
+ ],
1173
+ resourceGroups: [
1174
+ {
1175
+ subscriptionId: "sub-1",
1176
+ subscriptionName: "Subscription One",
1177
+ resourceGroup: "rg-app",
1178
+ location: "westeurope",
1179
+ tags: { owner: "team-a" }
1180
+ }
1181
+ ],
1182
+ resources: [
1183
+ {
1184
+ subscriptionId: "sub-1",
1185
+ subscriptionName: "Subscription One",
1186
+ resourceId: "/subscriptions/sub-1/resourceGroups/rg-app/providers/Microsoft.Web/sites/app-a",
1187
+ resourceName: "app-a",
1188
+ resourceGroup: "rg-app",
1189
+ resourceType: "Microsoft.Web/sites",
1190
+ kind: "app",
1191
+ location: "westeurope",
1192
+ tags: { env: "test" },
1193
+ identityType: "SystemAssigned",
1194
+ identityPrincipalId: "principal-1",
1195
+ identityTenantId: "tenant-1",
1196
+ userAssignedIdentityResourceIds: ["/subscriptions/sub-1/resourceGroups/rg-app/providers/Microsoft.ManagedIdentity/userAssignedIdentities/uami-a"],
1197
+ userAssignedIdentities: {
1198
+ "/subscriptions/sub-1/resourceGroups/rg-app/providers/Microsoft.ManagedIdentity/userAssignedIdentities/uami-a": {
1199
+ clientId: "client-1",
1200
+ principalId: "principal-uami-1"
1201
+ }
1202
+ }
1203
+ }
1204
+ ],
1205
+ userAssignedManagedIdentities: [
1206
+ {
1207
+ subscriptionId: "sub-1",
1208
+ subscriptionName: "Subscription One",
1209
+ resourceId: "/subscriptions/sub-1/resourceGroups/rg-app/providers/Microsoft.ManagedIdentity/userAssignedIdentities/uami-a",
1210
+ name: "uami-a",
1211
+ resourceGroup: "rg-app",
1212
+ location: "westeurope",
1213
+ clientId: "client-1",
1214
+ principalId: "principal-uami-1",
1215
+ tenantId: "tenant-1",
1216
+ tags: null
1217
+ }
1218
+ ],
1219
+ roleAssignments: [
1220
+ {
1221
+ subscriptionId: "sub-1",
1222
+ subscriptionName: "Subscription One",
1223
+ roleAssignmentId: "ra-1",
1224
+ scope: "/subscriptions/sub-1/resourceGroups/rg-app",
1225
+ scopeType: "ResourceGroup",
1226
+ scopeSubscriptionId: "sub-1",
1227
+ scopeResourceGroup: "rg-app",
1228
+ scopeResourceProvider: null,
1229
+ scopeResourceType: null,
1230
+ scopeResourceName: null,
1231
+ scopeManagementGroup: null,
1232
+ principalId: "principal-1",
1233
+ principalType: "ServicePrincipal",
1234
+ principalDisplayName: "app-a",
1235
+ signInName: null,
1236
+ roleDefinitionId: "role-1",
1237
+ roleDefinitionName: "Contributor",
1238
+ canDelegate: false,
1239
+ condition: null,
1240
+ conditionVersion: null
1241
+ }
1242
+ ],
1243
+ activityLogs: [
1244
+ {
1245
+ subscriptionId: "sub-1",
1246
+ subscriptionName: "Subscription One",
1247
+ eventTimestamp: "2026-06-04T00:00:00.000Z",
1248
+ submissionTimestamp: null,
1249
+ caller: "owner@example.test",
1250
+ callerUserPrincipalName: "owner@example.test",
1251
+ callerName: null,
1252
+ callerEmail: null,
1253
+ callerObjectId: null,
1254
+ callerIdentityType: null,
1255
+ callerAppId: null,
1256
+ callerIpAddress: null,
1257
+ callerTenantId: null,
1258
+ operationName: "Create app",
1259
+ operationNameValue: "Microsoft.Web/sites/write",
1260
+ status: "Succeeded",
1261
+ subStatus: null,
1262
+ category: "Administrative",
1263
+ resourceGroupName: "rg-app",
1264
+ resourceId: "/subscriptions/sub-1/resourceGroups/rg-app/providers/Microsoft.Web/sites/app-a",
1265
+ resourceProviderName: "Microsoft.Web",
1266
+ resourceType: "Microsoft.Web/sites",
1267
+ authorizationAction: "Microsoft.Web/sites/write",
1268
+ authorizationScope: "/subscriptions/sub-1/resourceGroups/rg-app"
1269
+ }
1270
+ ],
1271
+ ownershipHints: [{ id: "hint-1" }]
1272
+ };
1273
+
1274
+ try {
1275
+ await writeFile(path.join(dataDir, "snapshot.json"), JSON.stringify(snapshot), "utf8");
1276
+ await runtime.initialize();
1277
+
1278
+ expect(runtime.getStatus().azureResources).toMatchObject({
1279
+ imported: true,
1280
+ subscriptionCount: 1,
1281
+ resourceGroupCount: 1,
1282
+ resourceCount: 1,
1283
+ userAssignedManagedIdentityCount: 1,
1284
+ roleAssignmentCount: 1,
1285
+ activityLogCount: 1
1286
+ });
1287
+
1288
+ const imported = (await runtime.readSnapshot("snapshot.json")) as AzureSnapshot & {
1289
+ ownershipHints: Array<{ id: string }>;
1290
+ };
1291
+ const queried = await runtime.queryAzureResources({
1292
+ filters: [{ column: "resourceType", values: ["web"] }],
1293
+ page: 1,
1294
+ pageSize: 10
1295
+ });
1296
+
1297
+ expect(imported.meta.provider).toBe("azure");
1298
+ expect(imported.resources[0]).toMatchObject({
1299
+ resourceName: "app-a",
1300
+ tags: { env: "test" },
1301
+ userAssignedIdentityResourceIds: [
1302
+ "/subscriptions/sub-1/resourceGroups/rg-app/providers/Microsoft.ManagedIdentity/userAssignedIdentities/uami-a"
1303
+ ]
1304
+ });
1305
+ expect(imported.roleAssignments).toEqual(snapshot.roleAssignments);
1306
+ expect(imported.activityLogs).toEqual(snapshot.activityLogs);
1307
+ expect(imported.ownershipHints).toEqual([{ id: "hint-1" }]);
1308
+ expect(queried).toMatchObject({
1309
+ collectionId: "azureResources.resources",
1310
+ columns: expect.arrayContaining(["resourceId", "resourceType"]),
1311
+ count: 1,
1312
+ rows: [expect.objectContaining({ resourceName: "app-a", resourceType: "Microsoft.Web/sites" })]
1313
+ });
1314
+ } finally {
1315
+ await runtime.close();
1316
+ await rm(dataDir, { force: true, recursive: true });
1317
+ }
1318
+ });
1319
+
1320
+ test("persists disabled owner evidence keys in DuckDB across runtime restarts", async () => {
1321
+ const dataDir = await mkdtemp(path.join(tmpdir(), "ownerlens-runtime-"));
1322
+ const databasePath = path.join(dataDir, "runtime.duckdb");
1323
+ const disabledKey = "resourceGroup:sub-1:rg-activity:alice@example.test:2026-06-05T10:00:00.000Z";
1324
+ const azureSnapshot: AzureSnapshot = {
1325
+ meta: {
1326
+ provider: "azure",
1327
+ snapshotVersion: "1",
1328
+ createdAt: "2026-06-05T00:00:00.000Z",
1329
+ activityDays: 30,
1330
+ activityStartTime: "2026-05-06T00:00:00.000Z",
1331
+ maxActivityRecords: 1000,
1332
+ requestedSubscriptions: ["sub-1"],
1333
+ subscriptionCount: 0,
1334
+ resourceGroupCount: 1,
1335
+ resourceCount: 0,
1336
+ userAssignedManagedIdentityCount: 0,
1337
+ roleAssignmentCount: 0,
1338
+ activityLogCount: 2
1339
+ },
1340
+ subscriptions: [],
1341
+ resourceGroups: [
1342
+ {
1343
+ subscriptionId: "sub-1",
1344
+ subscriptionName: "Subscription 1",
1345
+ resourceGroup: "rg-activity",
1346
+ location: "westeurope",
1347
+ tags: null
1348
+ }
1349
+ ],
1350
+ resources: [],
1351
+ userAssignedManagedIdentities: [],
1352
+ roleAssignments: [],
1353
+ activityLogs: [
1354
+ {
1355
+ subscriptionId: "sub-1",
1356
+ subscriptionName: "Subscription 1",
1357
+ eventTimestamp: "2026-06-05T10:00:00.000Z",
1358
+ submissionTimestamp: null,
1359
+ caller: "alice@example.test",
1360
+ operationName: "Update resource group",
1361
+ operationNameValue: "Microsoft.Resources/subscriptions/resourcegroups/write",
1362
+ status: "Succeeded",
1363
+ subStatus: null,
1364
+ category: "Administrative",
1365
+ resourceGroupName: "rg-activity",
1366
+ resourceId: null,
1367
+ resourceProviderName: "Microsoft.Resources",
1368
+ resourceType: "Microsoft.Resources/resourceGroups",
1369
+ authorizationAction: "Microsoft.Resources/subscriptions/resourcegroups/write",
1370
+ authorizationScope: null
1371
+ },
1372
+ {
1373
+ subscriptionId: "sub-1",
1374
+ subscriptionName: "Subscription 1",
1375
+ eventTimestamp: "2026-06-04T10:00:00.000Z",
1376
+ submissionTimestamp: null,
1377
+ caller: "bob@example.test",
1378
+ operationName: "Update resource group",
1379
+ operationNameValue: "Microsoft.Resources/subscriptions/resourcegroups/write",
1380
+ status: "Succeeded",
1381
+ subStatus: null,
1382
+ category: "Administrative",
1383
+ resourceGroupName: "rg-activity",
1384
+ resourceId: null,
1385
+ resourceProviderName: "Microsoft.Resources",
1386
+ resourceType: "Microsoft.Resources/resourceGroups",
1387
+ authorizationAction: "Microsoft.Resources/subscriptions/resourcegroups/write",
1388
+ authorizationScope: null
1389
+ }
1390
+ ]
1391
+ };
1392
+ const entraSnapshot: EntraSnapshot = {
1393
+ meta: {
1394
+ provider: "entra",
1395
+ snapshotVersion: "1",
1396
+ createdAt: "2026-06-05T00:00:00.000Z",
1397
+ tenantId: "tenant-1",
1398
+ account: "owner@example.test",
1399
+ scopes: [],
1400
+ servicePrincipalCount: 0
1401
+ },
1402
+ servicePrincipals: [],
1403
+ oauth2PermissionGrants: [],
1404
+ appRoleAssignments: []
1405
+ };
1406
+
1407
+ try {
1408
+ await writeFile(path.join(dataDir, "snapshot.json"), JSON.stringify(azureSnapshot), "utf8");
1409
+ await writeFile(path.join(dataDir, "entra-snapshot.json"), JSON.stringify(entraSnapshot), "utf8");
1410
+
1411
+ const firstRuntime = new LocalReportRuntime({ dataDir, databasePath });
1412
+ await firstRuntime.initialize();
1413
+ let endpoints = defineLocalReportRuntimeRestEndpoints(firstRuntime);
1414
+
1415
+ await expect(
1416
+ endpoints[9].handle({
1417
+ req: {},
1418
+ url: new URL("http://localhost/api/data/azureResources/resourceGroupOwnership?page=1&count=10")
1419
+ })
1420
+ ).resolves.toMatchObject({
1421
+ rows: [
1422
+ expect.objectContaining({
1423
+ resourceGroup: "rg-activity",
1424
+ owner: "alice@example.test",
1425
+ confidence: "low",
1426
+ source: "activity.lastModifier",
1427
+ evidence: [
1428
+ { user: "alice@example.test", date: "2026-06-05T10:00:00.000Z" },
1429
+ { user: "bob@example.test", date: "2026-06-04T10:00:00.000Z" }
1430
+ ]
1431
+ })
1432
+ ]
1433
+ });
1434
+ await expect(
1435
+ endpoints[10].handle({
1436
+ req: {},
1437
+ url: new URL(
1438
+ `http://localhost/api/data/azureResources/resourceGroupOwnership/disabledEvidence?key=${encodeURIComponent(disabledKey)}&disabled=true`
1439
+ )
1440
+ })
1441
+ ).resolves.toEqual({ key: disabledKey, disabled: true, disabledCount: 1 });
1442
+ await expect(
1443
+ endpoints[9].handle({
1444
+ req: {},
1445
+ url: new URL("http://localhost/api/data/azureResources/resourceGroupOwnership?page=1&count=10")
1446
+ })
1447
+ ).resolves.toMatchObject({
1448
+ rows: [
1449
+ expect.objectContaining({
1450
+ resourceGroup: "rg-activity",
1451
+ owner: "bob@example.test",
1452
+ confidence: "low",
1453
+ source: "activity.lastModifier",
1454
+ evidence: [
1455
+ { user: "alice@example.test", date: "2026-06-05T10:00:00.000Z", disabled: true },
1456
+ { user: "bob@example.test", date: "2026-06-04T10:00:00.000Z" }
1457
+ ]
1458
+ })
1459
+ ]
1460
+ });
1461
+
1462
+ await firstRuntime.close();
1463
+
1464
+ const secondRuntime = new LocalReportRuntime({ dataDir, databasePath });
1465
+ await secondRuntime.initialize();
1466
+ endpoints = defineLocalReportRuntimeRestEndpoints(secondRuntime);
1467
+ await expect(
1468
+ endpoints[9].handle({
1469
+ req: {},
1470
+ url: new URL("http://localhost/api/data/azureResources/resourceGroupOwnership?page=1&count=10")
1471
+ })
1472
+ ).resolves.toMatchObject({
1473
+ rows: [
1474
+ expect.objectContaining({
1475
+ resourceGroup: "rg-activity",
1476
+ owner: "bob@example.test",
1477
+ confidence: "low",
1478
+ evidence: [
1479
+ { user: "alice@example.test", date: "2026-06-05T10:00:00.000Z", disabled: true },
1480
+ { user: "bob@example.test", date: "2026-06-04T10:00:00.000Z" }
1481
+ ]
1482
+ })
1483
+ ]
1484
+ });
1485
+ await expect(
1486
+ endpoints[10].handle({
1487
+ req: {},
1488
+ url: new URL(
1489
+ `http://localhost/api/data/azureResources/resourceGroupOwnership/disabledEvidence?key=${encodeURIComponent(disabledKey)}&disabled=false`
1490
+ )
1491
+ })
1492
+ ).resolves.toEqual({ key: disabledKey, disabled: false, disabledCount: 0 });
1493
+ await expect(
1494
+ endpoints[9].handle({
1495
+ req: {},
1496
+ url: new URL("http://localhost/api/data/azureResources/resourceGroupOwnership?page=1&count=10")
1497
+ })
1498
+ ).resolves.toMatchObject({
1499
+ rows: [
1500
+ expect.objectContaining({
1501
+ resourceGroup: "rg-activity",
1502
+ owner: "alice@example.test",
1503
+ confidence: "low",
1504
+ evidence: [
1505
+ { user: "alice@example.test", date: "2026-06-05T10:00:00.000Z" },
1506
+ { user: "bob@example.test", date: "2026-06-04T10:00:00.000Z" }
1507
+ ]
1508
+ })
1509
+ ]
1510
+ });
1511
+
1512
+ await secondRuntime.close();
1513
+ } finally {
1514
+ await rm(dataDir, { force: true, recursive: true });
1515
+ }
1516
+ });
1517
+
1518
+ test("materializes Azure identity enrichment runs and exposes the latest run in runtime output", async () => {
1519
+ const dataDir = await mkdtemp(path.join(tmpdir(), "ownerlens-runtime-"));
1520
+ const databasePath = path.join(dataDir, "runtime.duckdb");
1521
+ const runtime = new LocalReportRuntime({ dataDir, databasePath });
1522
+ const entraSnapshot: EntraSnapshot = {
1523
+ meta: {
1524
+ provider: "entra",
1525
+ snapshotVersion: "1",
1526
+ createdAt: "2026-06-05T00:00:00.000Z",
1527
+ tenantId: "tenant-1",
1528
+ account: "owner@example.test",
1529
+ scopes: [],
1530
+ servicePrincipalCount: 2
1531
+ },
1532
+ servicePrincipals: [
1533
+ servicePrincipal("sp-1", "app-1", "Application app", "Application"),
1534
+ servicePrincipal("principal-uami-1", "client-1", "Identity app", "ManagedIdentity")
1535
+ ],
1536
+ oauth2PermissionGrants: [],
1537
+ appRoleAssignments: []
1538
+ };
1539
+ const azureSnapshot: AzureSnapshot = {
1540
+ meta: {
1541
+ provider: "azure",
1542
+ snapshotVersion: "0.4",
1543
+ createdAt: "2026-06-05T00:00:00.000Z",
1544
+ activityDays: 90,
1545
+ activityStartTime: "2026-03-07T00:00:00.000Z",
1546
+ maxActivityRecords: 10000,
1547
+ requestedSubscriptions: ["sub-1"],
1548
+ subscriptionCount: 1,
1549
+ resourceGroupCount: 1,
1550
+ resourceCount: 1,
1551
+ userAssignedManagedIdentityCount: 1,
1552
+ roleAssignmentCount: 2,
1553
+ activityLogCount: 0
1554
+ },
1555
+ subscriptions: [],
1556
+ resourceGroups: [
1557
+ {
1558
+ subscriptionId: "sub-1",
1559
+ subscriptionName: "Subscription One",
1560
+ resourceGroup: "rg-app",
1561
+ location: "westeurope",
1562
+ tags: { ownerGroup: "alice@example.test" }
1563
+ }
1564
+ ],
1565
+ resources: [
1566
+ {
1567
+ subscriptionId: "sub-1",
1568
+ subscriptionName: "Subscription One",
1569
+ resourceId: "/subscriptions/sub-1/resourceGroups/rg-app/providers/Microsoft.Web/sites/app-a",
1570
+ resourceName: "app-a",
1571
+ resourceGroup: "rg-app",
1572
+ resourceType: "Microsoft.Web/sites",
1573
+ kind: "app",
1574
+ location: "westeurope",
1575
+ tags: null,
1576
+ identityType: "UserAssigned",
1577
+ identityPrincipalId: null,
1578
+ identityTenantId: null,
1579
+ userAssignedIdentityResourceIds: [
1580
+ "/subscriptions/sub-1/resourceGroups/rg-app/providers/Microsoft.ManagedIdentity/userAssignedIdentities/uami-a"
1581
+ ],
1582
+ userAssignedIdentities: {
1583
+ "/subscriptions/sub-1/resourceGroups/rg-app/providers/Microsoft.ManagedIdentity/userAssignedIdentities/uami-a": {
1584
+ clientId: "client-1",
1585
+ principalId: "principal-uami-1"
1586
+ }
1587
+ }
1588
+ }
1589
+ ],
1590
+ userAssignedManagedIdentities: [
1591
+ {
1592
+ subscriptionId: "sub-1",
1593
+ subscriptionName: "Subscription One",
1594
+ resourceId: "/subscriptions/sub-1/resourceGroups/rg-app/providers/Microsoft.ManagedIdentity/userAssignedIdentities/uami-a",
1595
+ name: "uami-a",
1596
+ resourceGroup: "rg-app",
1597
+ location: "westeurope",
1598
+ clientId: "client-1",
1599
+ principalId: "principal-uami-1",
1600
+ tenantId: "tenant-1",
1601
+ tags: null
1602
+ }
1603
+ ],
1604
+ roleAssignments: [
1605
+ roleAssignment("sp-1", "Owner", "/subscriptions/sub-1", "Subscription"),
1606
+ roleAssignment("principal-uami-1", "Reader", "/subscriptions/sub-1/resourceGroups/rg-app", "ResourceGroup")
1607
+ ],
1608
+ activityLogs: []
1609
+ };
1610
+
1611
+ try {
1612
+ await writeFile(path.join(dataDir, "entra-snapshot.json"), JSON.stringify(entraSnapshot), "utf8");
1613
+ await writeFile(path.join(dataDir, "snapshot.json"), JSON.stringify(azureSnapshot), "utf8");
1614
+ await runtime.initialize();
1615
+
1616
+ const firstStatus = runtime.getStatus().enrichment;
1617
+ const servicePrincipals = await runtime.readServicePrincipals();
1618
+ const managedIdentities = await runtime.readManagedIdentities();
1619
+ const queriedServicePrincipals = await runtime.queryEntraServicePrincipals({
1620
+ page: 1,
1621
+ pageSize: 10
1622
+ });
1623
+ const queriedManagedIdentities = await runtime.queryEntraManagedIdentities({
1624
+ page: 1,
1625
+ pageSize: 10
1626
+ });
1627
+ const queriedAzureRbac = await runtime.queryAzureRbac("sp-1", {
1628
+ page: 1,
1629
+ pageSize: 10
1630
+ });
1631
+
1632
+ expect(firstStatus).toMatchObject({
1633
+ calculated: true,
1634
+ identityRoleAssignmentCount: 2,
1635
+ accessRiskIdentityCount: 2,
1636
+ managedIdentityAssignmentCount: 1
1637
+ });
1638
+ expect(servicePrincipals[0]).toMatchObject({
1639
+ id: "sp-1",
1640
+ permissionRisk: "high",
1641
+ azureRbac: expect.stringContaining("Owner on subscription"),
1642
+ roleAssignments: [expect.objectContaining({ roleDefinitionName: "Owner" })],
1643
+ rbacRoleAssignmentCount: 1,
1644
+ rbacRoleLevel: "high",
1645
+ rbacSubscriptionCount: 1
1646
+ });
1647
+ expect(queriedServicePrincipals).toMatchObject({
1648
+ collectionId: "entra.servicePrincipals",
1649
+ rows: [
1650
+ expect.objectContaining({
1651
+ id: "sp-1",
1652
+ potentialOwners: ["alice@example.test"],
1653
+ ownerConfidence: "high"
1654
+ })
1655
+ ]
1656
+ });
1657
+ expect(managedIdentities[0]).toMatchObject({
1658
+ id: "principal-uami-1",
1659
+ permissionRisk: "low",
1660
+ azureRbac: expect.stringContaining("Reader on rg/rg-app"),
1661
+ roleAssignments: [expect.objectContaining({ roleDefinitionName: "Reader" })],
1662
+ rbacRoleAssignmentCount: 1,
1663
+ rbacRoleLevel: "low",
1664
+ rbacSubscriptionCount: 1,
1665
+ assignedResourceGroups: ["rg-app"],
1666
+ managedIdentityAssignments: [expect.objectContaining({ assignedResourceName: "app-a" })]
1667
+ });
1668
+ expect(queriedManagedIdentities).toMatchObject({
1669
+ collectionId: "entra.managedIdentities",
1670
+ rows: [
1671
+ expect.objectContaining({
1672
+ id: "principal-uami-1",
1673
+ potentialOwners: ["alice@example.test"],
1674
+ ownerConfidence: "high"
1675
+ })
1676
+ ]
1677
+ });
1678
+ expect(queriedAzureRbac).toMatchObject({
1679
+ collectionId: "azureRbac",
1680
+ rows: [
1681
+ expect.objectContaining({
1682
+ servicePrincipalId: "sp-1",
1683
+ principalId: "sp-1",
1684
+ roleDefinitionName: "Owner",
1685
+ accessRisk: "high",
1686
+ accessScope: "/subscriptions/sub-1",
1687
+ accessScopeType: "Subscription",
1688
+ accessSubscriptionId: "sub-1",
1689
+ accessDisplayName: "Owner on subscription Subscription One"
1690
+ })
1691
+ ],
1692
+ count: 1
1693
+ });
1694
+
1695
+ await writeFile(path.join(dataDir, "entra-snapshot.json"), "{not-json", "utf8");
1696
+ await writeFile(path.join(dataDir, "snapshot.json"), "{not-json", "utf8");
1697
+ await runtime.recalculateEnrichment();
1698
+
1699
+ const secondStatus = runtime.getStatus().enrichment;
1700
+ expect(secondStatus.calculated).toBe(true);
1701
+ expect(secondStatus.latestRunId).not.toBe(firstStatus.latestRunId);
1702
+ expect(secondStatus.identityRoleAssignmentCount).toBe(2);
1703
+ } finally {
1704
+ await runtime.close();
1705
+ }
1706
+
1707
+ const instance = await DuckDBInstance.create(databasePath);
1708
+ const connection = await instance.connect();
1709
+ try {
1710
+ const rows = await connection.runAndReadAll(
1711
+ "select count(*) as run_count from azure_runtime_enrichment_runs where status = 'completed'"
1712
+ );
1713
+ expect(rows.getRowObjectsJson()[0]).toEqual({ run_count: "2" });
1714
+ } finally {
1715
+ connection.disconnectSync();
1716
+ instance.closeSync();
1717
+ await rm(dataDir, { force: true, recursive: true });
1718
+ }
1719
+ });
1720
+
1721
+ test("defines local report runtime REST endpoints", async () => {
1722
+ const azureSnapshot: AzureSnapshot = {
1723
+ meta: {
1724
+ provider: "azure",
1725
+ snapshotVersion: "1",
1726
+ createdAt: "2026-06-05T00:00:00.000Z",
1727
+ activityDays: 30,
1728
+ activityStartTime: "2026-05-06T00:00:00.000Z",
1729
+ maxActivityRecords: 1000,
1730
+ requestedSubscriptions: ["sub-1"],
1731
+ subscriptionCount: 0,
1732
+ resourceGroupCount: 2,
1733
+ resourceCount: 0,
1734
+ userAssignedManagedIdentityCount: 0,
1735
+ roleAssignmentCount: 0,
1736
+ activityLogCount: 2
1737
+ },
1738
+ subscriptions: [],
1739
+ resourceGroups: [
1740
+ {
1741
+ subscriptionId: "sub-1",
1742
+ subscriptionName: "Subscription 1",
1743
+ resourceGroup: "rg-1",
1744
+ location: "westeurope",
1745
+ tags: { ownerGroup: "alice@example.test" }
1746
+ },
1747
+ {
1748
+ subscriptionId: "sub-1",
1749
+ subscriptionName: "Subscription 1",
1750
+ resourceGroup: "rg-activity",
1751
+ location: "westeurope",
1752
+ tags: null
1753
+ }
1754
+ ],
1755
+ resources: [],
1756
+ userAssignedManagedIdentities: [],
1757
+ roleAssignments: [],
1758
+ activityLogs: [
1759
+ {
1760
+ subscriptionId: "sub-1",
1761
+ subscriptionName: "Subscription 1",
1762
+ eventTimestamp: "2026-06-05T10:00:00.000Z",
1763
+ submissionTimestamp: null,
1764
+ caller: "alice@example.test",
1765
+ operationName: "Update resource group",
1766
+ operationNameValue: "Microsoft.Resources/subscriptions/resourcegroups/write",
1767
+ status: "Succeeded",
1768
+ subStatus: null,
1769
+ category: "Administrative",
1770
+ resourceGroupName: "rg-activity",
1771
+ resourceId: null,
1772
+ resourceProviderName: "Microsoft.Resources",
1773
+ resourceType: "Microsoft.Resources/resourceGroups",
1774
+ authorizationAction: "Microsoft.Resources/subscriptions/resourcegroups/write",
1775
+ authorizationScope: null
1776
+ },
1777
+ {
1778
+ subscriptionId: "sub-1",
1779
+ subscriptionName: "Subscription 1",
1780
+ eventTimestamp: "2026-06-04T10:00:00.000Z",
1781
+ submissionTimestamp: null,
1782
+ caller: "bob@example.test",
1783
+ operationName: "Update resource group",
1784
+ operationNameValue: "Microsoft.Resources/subscriptions/resourcegroups/write",
1785
+ status: "Succeeded",
1786
+ subStatus: null,
1787
+ category: "Administrative",
1788
+ resourceGroupName: "rg-activity",
1789
+ resourceId: null,
1790
+ resourceProviderName: "Microsoft.Resources",
1791
+ resourceType: "Microsoft.Resources/resourceGroups",
1792
+ authorizationAction: "Microsoft.Resources/subscriptions/resourcegroups/write",
1793
+ authorizationScope: null
1794
+ }
1795
+ ]
1796
+ };
1797
+ const entraSnapshot: EntraSnapshot = {
1798
+ meta: {
1799
+ provider: "entra",
1800
+ snapshotVersion: "1",
1801
+ createdAt: "2026-06-05T00:00:00.000Z",
1802
+ tenantId: "tenant-1",
1803
+ account: "owner@example.test",
1804
+ scopes: [],
1805
+ servicePrincipalCount: 0
1806
+ },
1807
+ servicePrincipals: [],
1808
+ oauth2PermissionGrants: [],
1809
+ appRoleAssignments: []
1810
+ };
1811
+ const disabledOwnerKeys = new Set<string>();
1812
+ const disabledAliceKey = "resourceGroup:sub-1:rg-activity:alice@example.test:2026-06-05T10:00:00.000Z";
1813
+ const emptyCollection = (
1814
+ collectionId: string,
1815
+ options: { page?: number; pageSize?: number }
1816
+ ): { collectionId: string; rows: unknown[]; columns: string[]; page: number; pageSize: number; count: number } => ({
1817
+ collectionId,
1818
+ rows: [],
1819
+ columns: [],
1820
+ page: options.page ?? 2,
1821
+ pageSize: options.pageSize ?? 25,
1822
+ count: 0
1823
+ });
1824
+ const runtime = {
1825
+ listSnapshots: jest.fn().mockResolvedValue({ files: [] }),
1826
+ readSnapshot: jest.fn((name: string) => {
1827
+ if (name === "snapshot.json") {
1828
+ return Promise.resolve(azureSnapshot);
1829
+ }
1830
+
1831
+ if (name === "entra-snapshot.json") {
1832
+ return Promise.resolve(entraSnapshot);
1833
+ }
1834
+
1835
+ return Promise.resolve({ meta: { provider: "unknown" } });
1836
+ }),
1837
+ readZeroTrustAssessmentReport: jest.fn().mockResolvedValue({
1838
+ Meta: {
1839
+ TenantId: "tenant-1",
1840
+ ExecutedAt: "2026-06-05T00:00:00.000Z"
1841
+ },
1842
+ Tests: [{ TestId: "zta-1", TestStatus: "Passed" }]
1843
+ }),
1844
+ readEntraServicePrincipals: jest.fn().mockResolvedValue([{ id: "sp-1" }]),
1845
+ readServicePrincipals: jest.fn().mockResolvedValue([{ id: "sp-1" }]),
1846
+ readManagedIdentities: jest.fn().mockResolvedValue([{ id: "mi-1" }]),
1847
+ readEntraOAuth2PermissionGrants: jest.fn().mockResolvedValue([{ id: "grant-1" }]),
1848
+ readEntraAppRoleAssignments: jest.fn().mockResolvedValue([{ id: "assignment-1" }]),
1849
+ readEntraPrincipalPermissions: jest.fn((principalId: string) =>
1850
+ Promise.resolve({
1851
+ principalId,
1852
+ oauth2PermissionGrants: [{ id: "grant-1", clientId: principalId }],
1853
+ appRoleAssignments: [{ id: "assignment-1", principalId }]
1854
+ })
1855
+ ),
1856
+ readAzureSubscriptions: jest.fn().mockResolvedValue([{ subscriptionId: "sub-1" }]),
1857
+ readAzureResourceGroups: jest.fn().mockResolvedValue([{ resourceGroup: "rg-1" }]),
1858
+ readAzureResources: jest.fn().mockResolvedValue([{ resourceId: "res-1" }]),
1859
+ readAzureUserAssignedManagedIdentities: jest.fn().mockResolvedValue([{ resourceId: "uami-1" }]),
1860
+ readAzureRoleAssignments: jest.fn().mockResolvedValue([{ roleAssignmentId: "ra-1" }]),
1861
+ readAzureActivityLogs: jest.fn().mockResolvedValue([{ eventTimestamp: "2026-06-05T00:00:00.000Z" }]),
1862
+ queryEntraServicePrincipals: jest.fn((options) =>
1863
+ Promise.resolve(emptyCollection("entra.servicePrincipals", options))
1864
+ ),
1865
+ queryEntraManagedIdentities: jest.fn((options) =>
1866
+ Promise.resolve(emptyCollection("entra.managedIdentities", options))
1867
+ ),
1868
+ queryEntraOAuth2PermissionGrants: jest.fn((options) =>
1869
+ Promise.resolve(emptyCollection("entra.oauth2PermissionGrants", options))
1870
+ ),
1871
+ queryEntraAppRoleAssignments: jest.fn((options) =>
1872
+ Promise.resolve(emptyCollection("entra.appRoleAssignments", options))
1873
+ ),
1874
+ queryAzureSubscriptions: jest.fn((options) =>
1875
+ Promise.resolve(emptyCollection("azureResources.subscriptions", options))
1876
+ ),
1877
+ queryAzureResourceGroups: jest.fn((options) =>
1878
+ Promise.resolve(emptyCollection("azureResources.resourceGroups", options))
1879
+ ),
1880
+ queryAzureResourceGroupOwnership: jest.fn((options: { page?: number; pageSize?: number }) => {
1881
+ const aliceDisabled = disabledOwnerKeys.has(disabledAliceKey);
1882
+
1883
+ return Promise.resolve({
1884
+ collectionId: "azureResources.resourceGroupOwnership",
1885
+ columns: [],
1886
+ rows: [
1887
+ {
1888
+ resourceGroup: "rg-1",
1889
+ owner: "alice@example.test",
1890
+ confidence: "high",
1891
+ source: "tag.ownerGroup",
1892
+ evidence: [{ user: "ownerGroup=alice@example.test", date: null }]
1893
+ },
1894
+ {
1895
+ resourceGroup: "rg-activity",
1896
+ targetKey: "resourceGroup:sub-1:rg-activity",
1897
+ owner: aliceDisabled ? "bob@example.test" : "alice@example.test",
1898
+ confidence: "low",
1899
+ source: "activity.lastModifier",
1900
+ evidence: [
1901
+ {
1902
+ user: "alice@example.test",
1903
+ date: "2026-06-05T10:00:00.000Z",
1904
+ disabled: aliceDisabled || undefined
1905
+ },
1906
+ { user: "bob@example.test", date: "2026-06-04T10:00:00.000Z" }
1907
+ ]
1908
+ }
1909
+ ],
1910
+ page: options.page ?? 1,
1911
+ pageSize: options.pageSize ?? 10,
1912
+ count: 2
1913
+ });
1914
+ }),
1915
+ queryAzureResources: jest.fn((options) => Promise.resolve(emptyCollection("azureResources.resources", options))),
1916
+ queryAzureUserAssignedManagedIdentities: jest.fn((options) =>
1917
+ Promise.resolve(emptyCollection("azureResources.userAssignedManagedIdentities", options))
1918
+ ),
1919
+ queryAzureRoleAssignments: jest.fn((options) =>
1920
+ Promise.resolve(emptyCollection("azureResources.roleAssignments", options))
1921
+ ),
1922
+ queryAzureRbac: jest.fn((servicePrincipalId: string, options: { page?: number; pageSize?: number }) =>
1923
+ Promise.resolve({
1924
+ collectionId: "azureRbac",
1925
+ rows: [
1926
+ {
1927
+ servicePrincipalId,
1928
+ accessScope: "/subscriptions/sub-1/resourceGroups/rg-1",
1929
+ accessScopeType: "ResourceGroup"
1930
+ }
1931
+ ],
1932
+ columns: ["servicePrincipalId", "accessScope", "accessScopeType"],
1933
+ page: options.page ?? 1,
1934
+ pageSize: options.pageSize ?? 10,
1935
+ count: 1
1936
+ })
1937
+ ),
1938
+ queryAzureActivityLogs: jest.fn((options) =>
1939
+ Promise.resolve(emptyCollection("azureResources.activityLogs", options))
1940
+ ),
1941
+ queryZeroTrustAssessmentReport: jest.fn((options) =>
1942
+ Promise.resolve(emptyCollection("zeroTrustAssessment.report", options))
1943
+ ),
1944
+ readDisabledOwnerEvidenceKeys: jest.fn(() => Promise.resolve(new Set(disabledOwnerKeys))),
1945
+ setOwnerEvidenceDisabled: jest.fn((key: string, disabled: boolean) => {
1946
+ if (disabled) {
1947
+ disabledOwnerKeys.add(key);
1948
+ } else {
1949
+ disabledOwnerKeys.delete(key);
1950
+ }
1951
+
1952
+ return Promise.resolve(disabledOwnerKeys.size);
1953
+ }),
1954
+ recalculateEnrichment: jest.fn().mockResolvedValue(undefined),
1955
+ getStatus: jest.fn().mockReturnValue({ initialized: true })
1956
+ };
1957
+
1958
+ const endpoints = defineLocalReportRuntimeRestEndpoints(runtime as unknown as LocalReportRuntime);
1959
+
1960
+ expect(endpoints.map((endpoint) => endpoint.path)).toEqual([
1961
+ "/api/data",
1962
+ "/api/data/read",
1963
+ "/api/data/entra/servicePrincipals",
1964
+ "/api/data/entra/managedIdentities",
1965
+ "/api/data/entra/permissions",
1966
+ "/api/data/entra/oauth2PermissionGrants",
1967
+ "/api/data/entra/appRoleAssignments",
1968
+ "/api/data/azureResources/subscriptions",
1969
+ "/api/data/azureResources/resourceGroups",
1970
+ "/api/data/azureResources/resourceGroupOwnership",
1971
+ "/api/data/azureResources/resourceGroupOwnership/disabledEvidence",
1972
+ "/api/data/azureResources/resources",
1973
+ "/api/data/azureResources/userAssignedManagedIdentities",
1974
+ "/api/data/azureResources/roleAssignments",
1975
+ "/api/data/azureRbac",
1976
+ "/api/data/azureResources/activityLogs",
1977
+ "/api/data/zeroTrustAssessment/report",
1978
+ "/api/data/runtime/enrichment/recalculate",
1979
+ "/api/data/runtime"
1980
+ ]);
1981
+ await expect(
1982
+ endpoints[1].handle({ req: {}, url: new URL("http://localhost/api/data/read?name=entra-snapshot.json") })
1983
+ ).resolves.toEqual(entraSnapshot);
1984
+ await expect(
1985
+ endpoints[2].handle({
1986
+ req: {},
1987
+ url: new URL(
1988
+ "http://localhost/api/data/entra/servicePrincipals?page=2&count=25&filter[0][column]=displayName&filter[0][value][0]=app&filter[0][value][1]=api&filter[1][column]=accountEnabled&filter[1][value]=true"
1989
+ )
1990
+ })
1991
+ ).resolves.toEqual({
1992
+ collectionId: "entra.servicePrincipals",
1993
+ rows: [],
1994
+ columns: [],
1995
+ page: 2,
1996
+ pageSize: 25,
1997
+ count: 0
1998
+ });
1999
+ await endpoints[3].handle({
2000
+ req: {},
2001
+ url: new URL("http://localhost/api/data/entra/managedIdentities?page=1&count=10")
2002
+ });
2003
+ await expect(
2004
+ endpoints[4].handle({
2005
+ req: {},
2006
+ url: new URL("http://localhost/api/data/entra/permissions?principalId=sp-1")
2007
+ })
2008
+ ).resolves.toEqual({
2009
+ principalId: "sp-1",
2010
+ oauth2PermissionGrants: [{ id: "grant-1", clientId: "sp-1" }],
2011
+ appRoleAssignments: [{ id: "assignment-1", principalId: "sp-1" }]
2012
+ });
2013
+ expect(() =>
2014
+ endpoints[4].handle({
2015
+ req: {},
2016
+ url: new URL("http://localhost/api/data/entra/permissions")
2017
+ })
2018
+ ).toThrow("Missing required query parameter: principalId");
2019
+ await endpoints[5].handle({
2020
+ req: {},
2021
+ url: new URL("http://localhost/api/data/entra/oauth2PermissionGrants?page=1&count=10")
2022
+ });
2023
+ await endpoints[6].handle({
2024
+ req: {},
2025
+ url: new URL("http://localhost/api/data/entra/appRoleAssignments?page=1&count=10")
2026
+ });
2027
+ await endpoints[7].handle({
2028
+ req: {},
2029
+ url: new URL("http://localhost/api/data/azureResources/subscriptions?page=1&count=10")
2030
+ });
2031
+ await endpoints[8].handle({
2032
+ req: {},
2033
+ url: new URL("http://localhost/api/data/azureResources/resourceGroups?page=1&count=10")
2034
+ });
2035
+ await expect(
2036
+ endpoints[9].handle({
2037
+ req: {},
2038
+ url: new URL("http://localhost/api/data/azureResources/resourceGroupOwnership?page=1&count=10")
2039
+ })
2040
+ ).resolves.toMatchObject({
2041
+ collectionId: "azureResources.resourceGroupOwnership",
2042
+ rows: expect.arrayContaining([
2043
+ expect.objectContaining({
2044
+ resourceGroup: "rg-1",
2045
+ owner: "alice@example.test",
2046
+ confidence: "high",
2047
+ source: "tag.ownerGroup",
2048
+ evidence: [{ user: "ownerGroup=alice@example.test", date: null }]
2049
+ }),
2050
+ expect.objectContaining({
2051
+ resourceGroup: "rg-activity",
2052
+ targetKey: "resourceGroup:sub-1:rg-activity",
2053
+ owner: "alice@example.test",
2054
+ confidence: "low",
2055
+ source: "activity.lastModifier",
2056
+ evidence: [
2057
+ { user: "alice@example.test", date: "2026-06-05T10:00:00.000Z" },
2058
+ { user: "bob@example.test", date: "2026-06-04T10:00:00.000Z" }
2059
+ ]
2060
+ })
2061
+ ]),
2062
+ page: 1,
2063
+ pageSize: 10,
2064
+ count: 2
2065
+ });
2066
+ await expect(
2067
+ endpoints[10].handle({
2068
+ req: {},
2069
+ url: new URL(
2070
+ "http://localhost/api/data/azureResources/resourceGroupOwnership/disabledEvidence?key=resourceGroup%3Asub-1%3Arg-activity%3Aalice%40example.test%3A2026-06-05T10%3A00%3A00.000Z&disabled=true"
2071
+ )
2072
+ })
2073
+ ).resolves.toEqual({
2074
+ key: "resourceGroup:sub-1:rg-activity:alice@example.test:2026-06-05T10:00:00.000Z",
2075
+ disabled: true,
2076
+ disabledCount: 1
2077
+ });
2078
+ await expect(
2079
+ endpoints[9].handle({
2080
+ req: {},
2081
+ url: new URL("http://localhost/api/data/azureResources/resourceGroupOwnership?page=1&count=10")
2082
+ })
2083
+ ).resolves.toMatchObject({
2084
+ rows: expect.arrayContaining([
2085
+ expect.objectContaining({
2086
+ resourceGroup: "rg-activity",
2087
+ owner: "bob@example.test",
2088
+ confidence: "low",
2089
+ evidence: [
2090
+ { user: "alice@example.test", date: "2026-06-05T10:00:00.000Z", disabled: true },
2091
+ { user: "bob@example.test", date: "2026-06-04T10:00:00.000Z" }
2092
+ ]
2093
+ })
2094
+ ])
2095
+ });
2096
+ await endpoints[11].handle({
2097
+ req: {},
2098
+ url: new URL("http://localhost/api/data/azureResources/resources?page=1&count=10")
2099
+ });
2100
+ await endpoints[12].handle({
2101
+ req: {},
2102
+ url: new URL("http://localhost/api/data/azureResources/userAssignedManagedIdentities?page=1&count=10")
2103
+ });
2104
+ await endpoints[13].handle({
2105
+ req: {},
2106
+ url: new URL("http://localhost/api/data/azureResources/roleAssignments?page=1&count=10")
2107
+ });
2108
+ await expect(
2109
+ endpoints[14].handle({
2110
+ req: {},
2111
+ url: new URL("http://localhost/api/data/azureRbac?servicePrincipalId=sp-1&page=1&count=10")
2112
+ })
2113
+ ).resolves.toMatchObject({
2114
+ collectionId: "azureRbac",
2115
+ rows: [
2116
+ {
2117
+ servicePrincipalId: "sp-1",
2118
+ accessScope: "/subscriptions/sub-1/resourceGroups/rg-1",
2119
+ accessScopeType: "ResourceGroup"
2120
+ }
2121
+ ],
2122
+ page: 1,
2123
+ pageSize: 10,
2124
+ count: 1
2125
+ });
2126
+ expect(() =>
2127
+ endpoints[14].handle({
2128
+ req: {},
2129
+ url: new URL("http://localhost/api/data/azureRbac?page=1&count=10")
2130
+ })
2131
+ ).toThrow("Missing required query parameter: servicePrincipalId");
2132
+ await endpoints[15].handle({
2133
+ req: {},
2134
+ url: new URL("http://localhost/api/data/azureResources/activityLogs?page=1&count=10")
2135
+ });
2136
+ await expect(
2137
+ endpoints[16].handle({
2138
+ req: {},
2139
+ url: new URL("http://localhost/api/data/zeroTrustAssessment/report?page=1&count=10")
2140
+ })
2141
+ ).resolves.toEqual({
2142
+ collectionId: "zeroTrustAssessment.report",
2143
+ rows: [],
2144
+ columns: [],
2145
+ page: 1,
2146
+ pageSize: 10,
2147
+ count: 0
2148
+ });
2149
+ await expect(
2150
+ endpoints[17].handle({
2151
+ req: {},
2152
+ url: new URL("http://localhost/api/data/runtime/enrichment/recalculate")
2153
+ })
2154
+ ).resolves.toBeUndefined();
2155
+ expect(runtime.recalculateEnrichment).toHaveBeenCalledTimes(1);
2156
+ expect(runtime.readZeroTrustAssessmentReport).not.toHaveBeenCalled();
2157
+ expect(runtime.readSnapshot).toHaveBeenCalledWith("entra-snapshot.json");
2158
+ expect(runtime.queryEntraServicePrincipals).toHaveBeenCalledWith({
2159
+ filters: [
2160
+ { column: "displayName", values: ["app", "api"] },
2161
+ { column: "accountEnabled", values: ["true"] }
2162
+ ],
2163
+ page: 2,
2164
+ pageSize: 25
2165
+ });
2166
+ expect(runtime.queryEntraManagedIdentities).toHaveBeenCalledWith({
2167
+ filters: [],
2168
+ page: 1,
2169
+ pageSize: 10
2170
+ });
2171
+ expect(runtime.readEntraPrincipalPermissions).toHaveBeenCalledWith("sp-1");
2172
+ expect(runtime.queryEntraOAuth2PermissionGrants).toHaveBeenCalledWith({
2173
+ filters: [],
2174
+ page: 1,
2175
+ pageSize: 10
2176
+ });
2177
+ expect(runtime.queryEntraAppRoleAssignments).toHaveBeenCalledWith({
2178
+ filters: [],
2179
+ page: 1,
2180
+ pageSize: 10
2181
+ });
2182
+ expect(runtime.queryAzureSubscriptions).toHaveBeenCalledWith({
2183
+ filters: [],
2184
+ page: 1,
2185
+ pageSize: 10
2186
+ });
2187
+ expect(runtime.queryAzureResourceGroups).toHaveBeenCalledWith({
2188
+ filters: [],
2189
+ page: 1,
2190
+ pageSize: 10
2191
+ });
2192
+ expect(runtime.queryAzureResourceGroupOwnership).toHaveBeenNthCalledWith(1, {
2193
+ filters: [],
2194
+ page: 1,
2195
+ pageSize: 10
2196
+ });
2197
+ expect(runtime.queryAzureResourceGroupOwnership).toHaveBeenNthCalledWith(2, {
2198
+ filters: [],
2199
+ page: 1,
2200
+ pageSize: 10
2201
+ });
2202
+ expect(runtime.queryAzureResources).toHaveBeenCalledWith({
2203
+ filters: [],
2204
+ page: 1,
2205
+ pageSize: 10
2206
+ });
2207
+ expect(runtime.queryAzureUserAssignedManagedIdentities).toHaveBeenCalledWith({
2208
+ filters: [],
2209
+ page: 1,
2210
+ pageSize: 10
2211
+ });
2212
+ expect(runtime.queryAzureRoleAssignments).toHaveBeenCalledWith({
2213
+ filters: [],
2214
+ page: 1,
2215
+ pageSize: 10
2216
+ });
2217
+ expect(runtime.queryAzureRbac).toHaveBeenCalledWith("sp-1", {
2218
+ filters: [],
2219
+ page: 1,
2220
+ pageSize: 10
2221
+ });
2222
+ expect(runtime.queryAzureActivityLogs).toHaveBeenCalledWith({
2223
+ filters: [],
2224
+ page: 1,
2225
+ pageSize: 10
2226
+ });
2227
+ expect(runtime.queryZeroTrustAssessmentReport).toHaveBeenCalledWith({
2228
+ filters: [],
2229
+ page: 1,
2230
+ pageSize: 10
2231
+ });
2232
+ });
2233
+
2234
+ function servicePrincipal(
2235
+ id: string,
2236
+ appId: string,
2237
+ displayName: string,
2238
+ servicePrincipalType: "Application" | "ManagedIdentity"
2239
+ ): EntraSnapshot["servicePrincipals"][number] {
2240
+ return {
2241
+ id,
2242
+ appId,
2243
+ displayName,
2244
+ appDisplayName: null,
2245
+ servicePrincipalType,
2246
+ publisherName: null,
2247
+ accountEnabled: true,
2248
+ appOwnerOrganizationId: "tenant-1",
2249
+ homepage: null,
2250
+ loginUrl: null,
2251
+ replyUrls: [],
2252
+ servicePrincipalNames: [],
2253
+ tags: [],
2254
+ appRoles: [],
2255
+ owners: [],
2256
+ metadata: null
2257
+ };
2258
+ }
2259
+
2260
+ function application(
2261
+ id: string,
2262
+ appId: string,
2263
+ displayName: string
2264
+ ): NonNullable<EntraSnapshot["applications"]>[number] {
2265
+ return {
2266
+ id,
2267
+ appId,
2268
+ displayName,
2269
+ signInAudience: null,
2270
+ publisherDomain: null,
2271
+ identifierUris: [],
2272
+ tags: [],
2273
+ appRoles: [],
2274
+ oauth2PermissionScopes: [],
2275
+ requiredResourceAccess: [],
2276
+ web: null,
2277
+ spa: null,
2278
+ publicClient: null,
2279
+ passwordCredentials: [],
2280
+ keyCredentials: [],
2281
+ createdDateTime: null,
2282
+ deletedDateTime: null,
2283
+ disabledByMicrosoftStatus: null,
2284
+ info: null,
2285
+ notes: null,
2286
+ owners: []
2287
+ };
2288
+ }
2289
+
2290
+ function roleAssignment(
2291
+ principalId: string,
2292
+ roleDefinitionName: string,
2293
+ scope: string,
2294
+ scopeType: NonNullable<AzureSnapshot["roleAssignments"]>[number]["scopeType"]
2295
+ ): NonNullable<AzureSnapshot["roleAssignments"]>[number] {
2296
+ return {
2297
+ subscriptionId: "sub-1",
2298
+ subscriptionName: "Subscription One",
2299
+ roleAssignmentId: `${principalId}-${roleDefinitionName}`,
2300
+ scope,
2301
+ scopeType,
2302
+ scopeSubscriptionId: "sub-1",
2303
+ scopeResourceGroup: scopeType === "ResourceGroup" || scopeType === "Resource" ? "rg-app" : null,
2304
+ scopeResourceProvider: null,
2305
+ scopeResourceType: null,
2306
+ scopeResourceName: null,
2307
+ scopeManagementGroup: null,
2308
+ principalId,
2309
+ principalType: "ServicePrincipal",
2310
+ principalDisplayName: principalId,
2311
+ signInName: null,
2312
+ roleDefinitionId: `${roleDefinitionName}-id`,
2313
+ roleDefinitionName,
2314
+ canDelegate: false,
2315
+ condition: null,
2316
+ conditionVersion: null
2317
+ };
2318
+ }