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.
- package/LICENSE +183 -0
- package/README.md +209 -0
- package/bin/ownerlens.js +92 -0
- package/dist/assets/index-B9aAYpVl.css +1 -0
- package/dist/assets/index-BcwLk2bx.js +10 -0
- package/dist/index.html +13 -0
- package/package.json +73 -0
- package/src/App.tsx +18 -0
- package/src/components/azure/AzureComponent.test.tsx +625 -0
- package/src/components/azure/AzureComponent.tsx +189 -0
- package/src/components/azure/AzureRbacComponent.tsx +104 -0
- package/src/components/azure/ClosableAzureTab.tsx +42 -0
- package/src/components/azure/EntraPermissionsComponent.tsx +194 -0
- package/src/components/azure/ManagedIdentityComponent.test.tsx +324 -0
- package/src/components/azure/ManagedIdentityComponent.tsx +141 -0
- package/src/components/azure/ResourceGroupComponent.tsx +157 -0
- package/src/components/azure/ServicePrincipalComponent.test.tsx +457 -0
- package/src/components/azure/ServicePrincipalComponent.tsx +155 -0
- package/src/components/azure/ServicePrincipalFieldRenderers.tsx +140 -0
- package/src/components/azure/ZtaComponent.test.tsx +267 -0
- package/src/components/azure/ZtaComponent.tsx +276 -0
- package/src/components/azure/ZtaRemediationBadge.tsx +70 -0
- package/src/components/azure/api.ts +216 -0
- package/src/components/azure/azureReportConfig.ts +247 -0
- package/src/core/azure/azureRbac.ts +70 -0
- package/src/core/azure/entra/index.ts +1 -0
- package/src/core/azure/entra/managedIdentity.ts +21 -0
- package/src/core/azure/entra/servicePrincipal.ts +34 -0
- package/src/core/azure/entra/types.ts +56 -0
- package/src/core/azure/identityEnrichment.ts +65 -0
- package/src/core/azure/resources.ts +141 -0
- package/src/core/azure/ztaReport.ts +58 -0
- package/src/core/config.ts +39 -0
- package/src/core/ownership/OwnershipTarget.ts +32 -0
- package/src/core/ownership/resolveOwner.ts +5 -0
- package/src/core/ownership/types.ts +14 -0
- package/src/core/risk/types.ts +1 -0
- package/src/core/runtime/index.ts +1 -0
- package/src/core/runtime/localSnapshotFiles.ts +74 -0
- package/src/core/runtime/rest.ts +61 -0
- package/src/lib/searchFilterUtils.ts +17 -0
- package/src/lib/utils.ts +48 -0
- package/src/main.tsx +10 -0
- package/src/providers/azure/identities/azureIdentityTypes.ts +1 -0
- package/src/providers/azure/identities/buildAzureManagedIdentityAssignmentIndex.test.ts +32 -0
- package/src/providers/azure/identities/buildAzureManagedIdentityAssignmentIndex.ts +35 -0
- package/src/providers/azure/identities/userAssignedIdentityAssignments.ts +52 -0
- package/src/providers/azure/inputTransferObject/entra/EntraAppRoleAssignment.ts +10 -0
- package/src/providers/azure/inputTransferObject/entra/EntraApplication.ts +27 -0
- package/src/providers/azure/inputTransferObject/entra/EntraOAuth2PermissionGrant.ts +8 -0
- package/src/providers/azure/inputTransferObject/entra/EntraServicePrincipal.ts +43 -0
- package/src/providers/azure/inputTransferObject/entra/EntraSnapshot.ts +13 -0
- package/src/providers/azure/inputTransferObject/entra/EntraSnapshotMeta.ts +12 -0
- package/src/providers/azure/inputTransferObject/resources/AzureActivityLog.ts +1 -0
- package/src/providers/azure/inputTransferObject/resources/AzureResource.ts +1 -0
- package/src/providers/azure/inputTransferObject/resources/AzureResourceGroup.ts +1 -0
- package/src/providers/azure/inputTransferObject/resources/AzureRoleAssignment.ts +1 -0
- package/src/providers/azure/inputTransferObject/resources/AzureSnapshot.ts +1 -0
- package/src/providers/azure/inputTransferObject/resources/AzureSnapshotMeta.ts +1 -0
- package/src/providers/azure/inputTransferObject/resources/AzureSubscription.ts +1 -0
- package/src/providers/azure/inputTransferObject/resources/AzureUserAssignedManagedIdentity.ts +1 -0
- package/src/providers/azure/ownership/azureActivityOwnershipEvidence.ts +60 -0
- package/src/providers/azure/ownership/azureOwnerReportTypes.ts +13 -0
- package/src/providers/azure/ownership/azureOwnershipConfig.ts +21 -0
- package/src/providers/azure/ownership/azureOwnershipTypes.ts +46 -0
- package/src/providers/azure/ownership/buildAzureOwnershipReport.test.ts +99 -0
- package/src/providers/azure/ownership/buildAzureOwnershipReport.ts +90 -0
- package/src/providers/azure/ownership/buildAzureOwnershipTargets.test.ts +87 -0
- package/src/providers/azure/ownership/buildAzureOwnershipTargets.ts +42 -0
- package/src/providers/azure/ownership/resolveAzureOwner.ts +146 -0
- package/src/providers/azure/runtime/DisabledEvidenceStore.ts +34 -0
- package/src/providers/azure/runtime/EnrichmentService.ts +35 -0
- package/src/providers/azure/runtime/LocalReportRuntime.test.ts +2318 -0
- package/src/providers/azure/runtime/LocalReportRuntime.ts +302 -0
- package/src/providers/azure/runtime/RuntimeHost.ts +60 -0
- package/src/providers/azure/runtime/SnapshotImporter.ts +44 -0
- package/src/providers/azure/runtime/enrichment/azureIdentityEnrichment.ts +523 -0
- package/src/providers/azure/runtime/enrichment/azureScopeClassifier.ts +30 -0
- package/src/providers/azure/runtime/enrichment/evaluateAzureRoleAssignmentRisk.ts +88 -0
- package/src/providers/azure/runtime/entra/EntraCollectionQueryService.ts +307 -0
- package/src/providers/azure/runtime/entra/LocalEntraReportRuntime.ts +227 -0
- package/src/providers/azure/runtime/entra/appRoleAssignmentsTable.ts +52 -0
- package/src/providers/azure/runtime/entra/applicationsTable.ts +175 -0
- package/src/providers/azure/runtime/entra/entraServicePrincipalMapper.ts +63 -0
- package/src/providers/azure/runtime/entra/localReportRuntimeRest.ts +41 -0
- package/src/providers/azure/runtime/entra/oauth2PermissionGrantsTable.ts +48 -0
- package/src/providers/azure/runtime/entra/principalProjection.ts +173 -0
- package/src/providers/azure/runtime/entra/servicePrincipalsTable.ts +149 -0
- package/src/providers/azure/runtime/entra/snapshotMetadataTable.ts +18 -0
- package/src/providers/azure/runtime/entra/snapshotStore.ts +102 -0
- package/src/providers/azure/runtime/localReportCollections.ts +101 -0
- package/src/providers/azure/runtime/localReportRuntimeRest.ts +71 -0
- package/src/providers/azure/runtime/resources/AzureResourcesCollectionQueryService.ts +145 -0
- package/src/providers/azure/runtime/resources/LocalAzureResourcesReportRuntime.ts +114 -0
- package/src/providers/azure/runtime/resources/disabledOwnerEvidenceTable.ts +60 -0
- package/src/providers/azure/runtime/resources/localReportRuntimeRest.ts +81 -0
- package/src/providers/azure/runtime/resources/resourceGroupOwnership.ts +90 -0
- package/src/providers/azure/runtime/resources/snapshotMetadataTable.ts +19 -0
- package/src/providers/azure/runtime/resources/snapshotStore.ts +128 -0
- package/src/providers/azure/runtime/resources/tables.ts +441 -0
- package/src/providers/azure/runtime/runtimeRestQuery.ts +46 -0
- package/src/providers/azure/runtime/runtimeSqlSchema.ts +357 -0
- package/src/providers/azure/runtime/zta/Discovery.ts +141 -0
- package/src/providers/azure/runtime/zta/LocalZeroTrustAssessmentReportRuntime.ts +86 -0
- package/src/providers/azure/runtime/zta/ZeroTrustAssessmentQueryService.ts +124 -0
- package/src/providers/azure/runtime/zta/localReportRuntimeRest.ts +15 -0
- package/src/providers/azure/runtime/zta/snapshotMetadataTable.ts +77 -0
- package/src/providers/azure/runtime/zta/snapshotStore.ts +112 -0
- package/src/providers/azure/runtime/zta/tables.ts +361 -0
- package/src/providers/azure/runtime/zta/types.ts +7 -0
- package/src/providers/azure/runtime/zta/ztaReportMapper.ts +12 -0
- package/src/report/applyCollectionControls.ts +289 -0
- package/src/report/buildCollectionColumns.tsx +38 -0
- package/src/report/components/ConfidenceBadge.tsx +10 -0
- package/src/report/components/EvidenceList.test.ts +25 -0
- package/src/report/components/EvidenceList.tsx +52 -0
- package/src/report/components/GenericTable.tsx +373 -0
- package/src/report/components/PermissionRiskBadge.tsx +19 -0
- package/src/report/components/reportTableControls.test.ts +175 -0
- package/src/report/components/reportTableControls.tsx +483 -0
- package/src/report/components/ui/badge.tsx +35 -0
- package/src/report/components/ui/button.tsx +38 -0
- package/src/report/components/ui/card.tsx +23 -0
- package/src/report/components/ui/input.tsx +15 -0
- package/src/report/components/ui/table.tsx +44 -0
- package/src/report/components/ui/tabs.tsx +29 -0
- package/src/report/export/csv.ts +34 -0
- package/src/report/ownerManualPrecheck.test.ts +137 -0
- package/src/report/ownerManualPrecheck.ts +132 -0
- package/src/report/reportArchitecture.test.ts +125 -0
- package/src/report/reportTypes.ts +54 -0
- package/src/report/reportValueRenderers.tsx +54 -0
- package/src/report/runtimeCollectionQuery.ts +23 -0
- package/src/report/types.ts +14 -0
- package/src/styles.css +43 -0
- package/tools/README.md +108 -0
- package/tools/azure-activity-check.ps1 +164 -0
- package/tools/collect-azure.ps1 +54 -0
- package/tools/collect-entra.ps1 +47 -0
- package/tools/collect-scripts.test.ts +22 -0
- package/tools/prepare-entra-snapshot.ps1 +403 -0
- package/tools/prepare-entra-snapshot.test.ts +14 -0
- package/tools/prepare-resource-snapshot.ps1 +345 -0
- package/vite.config.ts +23 -0
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
import { act } from "react";
|
|
5
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
6
|
+
|
|
7
|
+
import type { ZtaReport } from "../../core/azure/ztaReport";
|
|
8
|
+
import { AzureComponent } from "./AzureComponent";
|
|
9
|
+
|
|
10
|
+
declare global {
|
|
11
|
+
var IS_REACT_ACT_ENVIRONMENT: boolean | undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
beforeAll(() => {
|
|
15
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
delete (globalThis as Partial<typeof globalThis>).fetch;
|
|
20
|
+
document.body.innerHTML = "";
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("opens related managed identity from Zero Trust Assessment with an Object ID filter", async () => {
|
|
24
|
+
const fetchMock = jest.fn<Promise<Response>, Parameters<typeof fetch>>(async (input) => {
|
|
25
|
+
const requestUrl = String(input);
|
|
26
|
+
|
|
27
|
+
if (requestUrl.startsWith("/api/data/zeroTrustAssessment/report")) {
|
|
28
|
+
return zeroTrustAssessmentJsonResponse(ztaReport);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (requestUrl.startsWith("/api/data/entra/managedIdentities")) {
|
|
32
|
+
return jsonResponse({
|
|
33
|
+
collectionId: "entra.managedIdentities",
|
|
34
|
+
columns: [],
|
|
35
|
+
count: 1,
|
|
36
|
+
page: 1,
|
|
37
|
+
pageSize: 20,
|
|
38
|
+
rows: [
|
|
39
|
+
{
|
|
40
|
+
accountEnabled: true,
|
|
41
|
+
appDisplayName: null,
|
|
42
|
+
appId: "mi-client-id",
|
|
43
|
+
appOwnerOrganizationId: null,
|
|
44
|
+
azureRbac: "No Azure RBAC assignments",
|
|
45
|
+
displayName: "uami-prod",
|
|
46
|
+
homepage: null,
|
|
47
|
+
id: "mi-object-id",
|
|
48
|
+
loginUrl: null,
|
|
49
|
+
managedIdentityAssignments: [],
|
|
50
|
+
permissionRisk: "none",
|
|
51
|
+
rbacRoleAssignmentCount: 0,
|
|
52
|
+
rbacRoleLevel: "none",
|
|
53
|
+
rbacSubscriptionCount: 0,
|
|
54
|
+
publisherName: null,
|
|
55
|
+
replyUrls: [],
|
|
56
|
+
roleAssignments: [],
|
|
57
|
+
oauthPemrissionsCount: 0,
|
|
58
|
+
appRolesPermissionCount: 0,
|
|
59
|
+
entraPermissionRisk: "none",
|
|
60
|
+
servicePrincipalNames: [],
|
|
61
|
+
servicePrincipalType: "ManagedIdentity",
|
|
62
|
+
assignedResourceGroups: [],
|
|
63
|
+
potentialOwners: [],
|
|
64
|
+
ownerConfidence: "none",
|
|
65
|
+
tags: []
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return jsonResponse({
|
|
72
|
+
collectionId: "entra.servicePrincipals",
|
|
73
|
+
columns: [],
|
|
74
|
+
count: 0,
|
|
75
|
+
page: 1,
|
|
76
|
+
pageSize: 20,
|
|
77
|
+
rows: []
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
globalThis.fetch = fetchMock;
|
|
81
|
+
|
|
82
|
+
const { container, root } = renderComponent(<AzureComponent />);
|
|
83
|
+
|
|
84
|
+
await clickButton("Zero Trust Assessment");
|
|
85
|
+
await waitForText(container, "Managed identity exposure");
|
|
86
|
+
|
|
87
|
+
await clickButton("Open related object mi-object-id");
|
|
88
|
+
await waitForText(container, "uami-prod");
|
|
89
|
+
|
|
90
|
+
const managedIdentityRequest = fetchMock.mock.calls
|
|
91
|
+
.map(([input]) => String(input))
|
|
92
|
+
.reverse()
|
|
93
|
+
.find((requestUrl) => requestUrl.startsWith("/api/data/entra/managedIdentities"));
|
|
94
|
+
expect(managedIdentityRequest).toBeDefined();
|
|
95
|
+
|
|
96
|
+
const url = new URL(managedIdentityRequest ?? "", window.location.origin);
|
|
97
|
+
expect(url.searchParams.get("filter[0][column]")).toBe("id");
|
|
98
|
+
expect(url.searchParams.get("filter[0][value][0]")).toBe("mi-object-id");
|
|
99
|
+
|
|
100
|
+
act(() => root.unmount());
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("opens Zero Trust Assessment filtered by related object from a principal ZTA badge", async () => {
|
|
104
|
+
const fetchMock = jest.fn<Promise<Response>, Parameters<typeof fetch>>(async (input) => {
|
|
105
|
+
const requestUrl = String(input);
|
|
106
|
+
|
|
107
|
+
if (requestUrl.startsWith("/api/data/zeroTrustAssessment/report")) {
|
|
108
|
+
const tests: ZtaReport["Tests"] = [
|
|
109
|
+
{
|
|
110
|
+
TestId: "zta-sp-1",
|
|
111
|
+
TestStatus: "Failed",
|
|
112
|
+
TestTitle: "Service principal exposure",
|
|
113
|
+
RelatedObjects: [{ id: "sp-object-id", servicePrincipalType: "Application" }]
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
TestId: "zta-other-1",
|
|
117
|
+
TestStatus: "Failed",
|
|
118
|
+
TestTitle: "Unrelated exposure",
|
|
119
|
+
RelatedObjects: [{ id: "other-object-id", servicePrincipalType: "Application" }]
|
|
120
|
+
}
|
|
121
|
+
];
|
|
122
|
+
const url = new URL(requestUrl, window.location.origin);
|
|
123
|
+
const relatedObjectFilter = url.searchParams.get("filter[0][value][0]");
|
|
124
|
+
const filteredTests = relatedObjectFilter
|
|
125
|
+
? tests.filter((test) => JSON.stringify(test).includes(relatedObjectFilter))
|
|
126
|
+
: tests;
|
|
127
|
+
|
|
128
|
+
return zeroTrustAssessmentJsonResponse({
|
|
129
|
+
Meta: {
|
|
130
|
+
TenantId: "tenant-1",
|
|
131
|
+
TenantName: "Example Tenant"
|
|
132
|
+
},
|
|
133
|
+
Tests: filteredTests
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return jsonResponse({
|
|
138
|
+
collectionId: "entra.servicePrincipals",
|
|
139
|
+
columns: [],
|
|
140
|
+
count: 1,
|
|
141
|
+
page: 1,
|
|
142
|
+
pageSize: 20,
|
|
143
|
+
rows: [
|
|
144
|
+
{
|
|
145
|
+
accountEnabled: true,
|
|
146
|
+
appDisplayName: "Service principal app",
|
|
147
|
+
appId: "sp-client-id",
|
|
148
|
+
appOwnerOrganizationId: null,
|
|
149
|
+
azureRbac: "No Azure RBAC assignments",
|
|
150
|
+
displayName: "Service principal app",
|
|
151
|
+
homepage: null,
|
|
152
|
+
id: "sp-object-id",
|
|
153
|
+
loginUrl: null,
|
|
154
|
+
permissionRisk: "none",
|
|
155
|
+
rbacRoleAssignmentCount: 0,
|
|
156
|
+
rbacRoleLevel: "none",
|
|
157
|
+
rbacSubscriptionCount: 0,
|
|
158
|
+
publisherName: null,
|
|
159
|
+
replyUrls: [],
|
|
160
|
+
roleAssignments: [],
|
|
161
|
+
oauthPemrissionsCount: 0,
|
|
162
|
+
appRolesPermissionCount: 0,
|
|
163
|
+
entraPermissionRisk: "none",
|
|
164
|
+
servicePrincipalNames: [],
|
|
165
|
+
servicePrincipalType: "Application",
|
|
166
|
+
potentialOwners: [],
|
|
167
|
+
ownerConfidence: "none",
|
|
168
|
+
tags: [],
|
|
169
|
+
ztaMaxRisk: "high",
|
|
170
|
+
ztaRemediationCountAll: 3,
|
|
171
|
+
ztaRemediationFailedCount: 1
|
|
172
|
+
}
|
|
173
|
+
]
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
globalThis.fetch = fetchMock;
|
|
177
|
+
|
|
178
|
+
const { container, root } = renderComponent(<AzureComponent />);
|
|
179
|
+
|
|
180
|
+
await waitForText(container, "Service principal app");
|
|
181
|
+
await clickButton("Open ZTA remediations 1/3");
|
|
182
|
+
await waitForText(container, "Service principal exposure");
|
|
183
|
+
|
|
184
|
+
expect(getInput("Filter Related objects").value).toBe("sp-object-id");
|
|
185
|
+
expect(container.textContent).toContain("Service principal exposure");
|
|
186
|
+
expect(container.textContent).not.toContain("Unrelated exposure");
|
|
187
|
+
|
|
188
|
+
act(() => root.unmount());
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("opens Azure RBAC tab for the selected service principal from its RBAC badge", async () => {
|
|
192
|
+
const fetchMock = jest.fn<Promise<Response>, Parameters<typeof fetch>>(async (input) => {
|
|
193
|
+
const requestUrl = String(input);
|
|
194
|
+
|
|
195
|
+
if (requestUrl.startsWith("/api/data/azureRbac")) {
|
|
196
|
+
return jsonResponse({
|
|
197
|
+
collectionId: "azureRbac",
|
|
198
|
+
columns: [],
|
|
199
|
+
count: 1,
|
|
200
|
+
page: 1,
|
|
201
|
+
pageSize: 20,
|
|
202
|
+
rows: [
|
|
203
|
+
{
|
|
204
|
+
accessDisplayName: "Owner on subscription Platform",
|
|
205
|
+
accessRisk: "high",
|
|
206
|
+
accessResourceGroup: null,
|
|
207
|
+
accessResourceId: null,
|
|
208
|
+
accessScope: "/subscriptions/sub-1",
|
|
209
|
+
accessScopeType: "Subscription",
|
|
210
|
+
accessSubscriptionId: "sub-1",
|
|
211
|
+
canDelegate: false,
|
|
212
|
+
condition: null,
|
|
213
|
+
conditionVersion: null,
|
|
214
|
+
principalDisplayName: "Service principal app",
|
|
215
|
+
principalId: "sp-object-id",
|
|
216
|
+
principalType: "ServicePrincipal",
|
|
217
|
+
roleAssignmentId: "assignment-1",
|
|
218
|
+
roleDefinitionId: "owner-role-id",
|
|
219
|
+
roleDefinitionName: "Owner",
|
|
220
|
+
scope: "/subscriptions/sub-1",
|
|
221
|
+
scopeSubscriptionId: "sub-1",
|
|
222
|
+
servicePrincipalId: "sp-object-id",
|
|
223
|
+
signInName: null,
|
|
224
|
+
subscriptionId: "sub-1",
|
|
225
|
+
subscriptionName: "Platform"
|
|
226
|
+
}
|
|
227
|
+
]
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return jsonResponse({
|
|
232
|
+
collectionId: "entra.servicePrincipals",
|
|
233
|
+
columns: [],
|
|
234
|
+
count: 1,
|
|
235
|
+
page: 1,
|
|
236
|
+
pageSize: 20,
|
|
237
|
+
rows: [
|
|
238
|
+
{
|
|
239
|
+
accountEnabled: true,
|
|
240
|
+
appDisplayName: "Service principal app",
|
|
241
|
+
appId: "sp-client-id",
|
|
242
|
+
appOwnerOrganizationId: null,
|
|
243
|
+
azureRbac: "Owner on subscription Platform",
|
|
244
|
+
displayName: "Service principal app",
|
|
245
|
+
homepage: null,
|
|
246
|
+
id: "sp-object-id",
|
|
247
|
+
loginUrl: null,
|
|
248
|
+
permissionRisk: "high",
|
|
249
|
+
rbacRoleAssignmentCount: 1,
|
|
250
|
+
rbacRoleLevel: "high",
|
|
251
|
+
rbacSubscriptionCount: 1,
|
|
252
|
+
publisherName: null,
|
|
253
|
+
replyUrls: [],
|
|
254
|
+
roleAssignments: [],
|
|
255
|
+
oauthPemrissionsCount: 0,
|
|
256
|
+
appRolesPermissionCount: 0,
|
|
257
|
+
entraPermissionRisk: "none",
|
|
258
|
+
servicePrincipalNames: [],
|
|
259
|
+
servicePrincipalType: "Application",
|
|
260
|
+
potentialOwners: [],
|
|
261
|
+
ownerConfidence: "none",
|
|
262
|
+
tags: [],
|
|
263
|
+
ztaMaxRisk: "none",
|
|
264
|
+
ztaRemediationCountAll: 0,
|
|
265
|
+
ztaRemediationFailedCount: 0
|
|
266
|
+
}
|
|
267
|
+
]
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
globalThis.fetch = fetchMock;
|
|
271
|
+
|
|
272
|
+
const { container, root } = renderComponent(<AzureComponent />);
|
|
273
|
+
|
|
274
|
+
await waitForText(container, "Service principal app");
|
|
275
|
+
await clickButton("Open Azure RBAC assignments 1/1");
|
|
276
|
+
await waitForText(container, "Owner on subscription Platform");
|
|
277
|
+
|
|
278
|
+
expect(getButton("Service principal app")).toBeDefined();
|
|
279
|
+
expect(container.textContent).toContain("high");
|
|
280
|
+
|
|
281
|
+
const azureRbacRequest = fetchMock.mock.calls
|
|
282
|
+
.map(([input]) => String(input))
|
|
283
|
+
.find((requestUrl) => requestUrl.startsWith("/api/data/azureRbac"));
|
|
284
|
+
expect(azureRbacRequest).toBeDefined();
|
|
285
|
+
|
|
286
|
+
const url = new URL(azureRbacRequest ?? "", window.location.origin);
|
|
287
|
+
expect(url.searchParams.get("servicePrincipalId")).toBe("sp-object-id");
|
|
288
|
+
|
|
289
|
+
await clickButton("Close Service principal app Azure RBAC tab");
|
|
290
|
+
await waitFor(() => {
|
|
291
|
+
expect(queryButton("Close Service principal app Azure RBAC tab")).toBeNull();
|
|
292
|
+
expect(container.textContent).not.toContain("Owner on subscription Platform");
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
act(() => root.unmount());
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test("opens Entra permissions tab for the selected service principal from its permissions badge", async () => {
|
|
299
|
+
const fetchMock = jest.fn<Promise<Response>, Parameters<typeof fetch>>(async (input) => {
|
|
300
|
+
const requestUrl = String(input);
|
|
301
|
+
|
|
302
|
+
if (requestUrl.startsWith("/api/data/entra/permissions")) {
|
|
303
|
+
return jsonResponse({
|
|
304
|
+
principalId: "sp-object-id",
|
|
305
|
+
oauth2PermissionGrants: [
|
|
306
|
+
{
|
|
307
|
+
id: "grant-1",
|
|
308
|
+
clientId: "sp-object-id",
|
|
309
|
+
consentType: "AllPrincipals",
|
|
310
|
+
principalId: null,
|
|
311
|
+
resourceId: "graph-sp-id",
|
|
312
|
+
risk: "high",
|
|
313
|
+
scope: "User.Read Directory.Read.All"
|
|
314
|
+
}
|
|
315
|
+
],
|
|
316
|
+
appRoleAssignments: [
|
|
317
|
+
{
|
|
318
|
+
id: "app-role-assignment-1",
|
|
319
|
+
appRoleId: "role-1",
|
|
320
|
+
appRoleDisplayName: "Read directory data",
|
|
321
|
+
appRoleValue: "Directory.Read.All",
|
|
322
|
+
principalId: "sp-object-id",
|
|
323
|
+
principalDisplayName: "Service principal app",
|
|
324
|
+
resourceId: "graph-sp-id",
|
|
325
|
+
resourceDisplayName: "Microsoft Graph"
|
|
326
|
+
}
|
|
327
|
+
]
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return jsonResponse({
|
|
332
|
+
collectionId: "entra.servicePrincipals",
|
|
333
|
+
columns: [],
|
|
334
|
+
count: 1,
|
|
335
|
+
page: 1,
|
|
336
|
+
pageSize: 20,
|
|
337
|
+
rows: [
|
|
338
|
+
{
|
|
339
|
+
accountEnabled: true,
|
|
340
|
+
appDisplayName: "Service principal app",
|
|
341
|
+
appId: "sp-client-id",
|
|
342
|
+
appOwnerOrganizationId: null,
|
|
343
|
+
azureRbac: "No Azure RBAC assignments",
|
|
344
|
+
displayName: "Service principal app",
|
|
345
|
+
homepage: null,
|
|
346
|
+
id: "sp-object-id",
|
|
347
|
+
loginUrl: null,
|
|
348
|
+
permissionRisk: "high",
|
|
349
|
+
rbacRoleAssignmentCount: 0,
|
|
350
|
+
rbacRoleLevel: "none",
|
|
351
|
+
rbacSubscriptionCount: 0,
|
|
352
|
+
publisherName: null,
|
|
353
|
+
replyUrls: [],
|
|
354
|
+
roleAssignments: [],
|
|
355
|
+
oauthPemrissionsCount: 2,
|
|
356
|
+
appRolesPermissionCount: 1,
|
|
357
|
+
entraPermissionRisk: "high",
|
|
358
|
+
servicePrincipalNames: [],
|
|
359
|
+
servicePrincipalType: "Application",
|
|
360
|
+
potentialOwners: [],
|
|
361
|
+
ownerConfidence: "none",
|
|
362
|
+
tags: [],
|
|
363
|
+
ztaMaxRisk: "none",
|
|
364
|
+
ztaRemediationCountAll: 0,
|
|
365
|
+
ztaRemediationFailedCount: 0
|
|
366
|
+
}
|
|
367
|
+
]
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
globalThis.fetch = fetchMock;
|
|
371
|
+
|
|
372
|
+
const { container, root } = renderComponent(<AzureComponent />);
|
|
373
|
+
|
|
374
|
+
await waitForText(container, "Service principal app");
|
|
375
|
+
await clickButton("Open Entra permissions 2/1");
|
|
376
|
+
await waitForText(container, "User.Read Directory.Read.All");
|
|
377
|
+
await waitForText(container, "Read directory data");
|
|
378
|
+
await waitForText(container, "Risk");
|
|
379
|
+
await waitForText(container, "high");
|
|
380
|
+
|
|
381
|
+
expect(getButton("Service principal app permissions")).toBeDefined();
|
|
382
|
+
|
|
383
|
+
const permissionsRequest = fetchMock.mock.calls
|
|
384
|
+
.map(([input]) => String(input))
|
|
385
|
+
.find((requestUrl) => requestUrl.startsWith("/api/data/entra/permissions"));
|
|
386
|
+
expect(permissionsRequest).toBeDefined();
|
|
387
|
+
|
|
388
|
+
const url = new URL(permissionsRequest ?? "", window.location.origin);
|
|
389
|
+
expect(url.searchParams.get("principalId")).toBe("sp-object-id");
|
|
390
|
+
|
|
391
|
+
await clickButton("Close Service principal app Entra permissions tab");
|
|
392
|
+
await waitFor(() => {
|
|
393
|
+
expect(queryButton("Close Service principal app Entra permissions tab")).toBeNull();
|
|
394
|
+
expect(container.textContent).not.toContain("User.Read Directory.Read.All");
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
act(() => root.unmount());
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
test("opens Azure RBAC tab for the selected managed identity from its RBAC badge", async () => {
|
|
401
|
+
const fetchMock = jest.fn<Promise<Response>, Parameters<typeof fetch>>(async (input) => {
|
|
402
|
+
const requestUrl = String(input);
|
|
403
|
+
|
|
404
|
+
if (requestUrl.startsWith("/api/data/azureRbac")) {
|
|
405
|
+
return jsonResponse({
|
|
406
|
+
collectionId: "azureRbac",
|
|
407
|
+
columns: [],
|
|
408
|
+
count: 1,
|
|
409
|
+
page: 1,
|
|
410
|
+
pageSize: 20,
|
|
411
|
+
rows: [
|
|
412
|
+
{
|
|
413
|
+
accessDisplayName: "Contributor on resource group rg-app",
|
|
414
|
+
accessRisk: "high",
|
|
415
|
+
accessResourceGroup: "rg-app",
|
|
416
|
+
accessResourceId: null,
|
|
417
|
+
accessScope: "/subscriptions/sub-1/resourceGroups/rg-app",
|
|
418
|
+
accessScopeType: "ResourceGroup",
|
|
419
|
+
accessSubscriptionId: "sub-1",
|
|
420
|
+
canDelegate: false,
|
|
421
|
+
condition: null,
|
|
422
|
+
conditionVersion: null,
|
|
423
|
+
principalDisplayName: "uami-prod",
|
|
424
|
+
principalId: "mi-object-id",
|
|
425
|
+
principalType: "ServicePrincipal",
|
|
426
|
+
roleAssignmentId: "assignment-mi-1",
|
|
427
|
+
roleDefinitionId: "contributor-role-id",
|
|
428
|
+
roleDefinitionName: "Contributor",
|
|
429
|
+
scope: "/subscriptions/sub-1/resourceGroups/rg-app",
|
|
430
|
+
scopeResourceGroup: "rg-app",
|
|
431
|
+
scopeSubscriptionId: "sub-1",
|
|
432
|
+
servicePrincipalId: "mi-object-id",
|
|
433
|
+
signInName: null,
|
|
434
|
+
subscriptionId: "sub-1",
|
|
435
|
+
subscriptionName: "Platform"
|
|
436
|
+
}
|
|
437
|
+
]
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (requestUrl.startsWith("/api/data/entra/managedIdentities")) {
|
|
442
|
+
return jsonResponse({
|
|
443
|
+
collectionId: "entra.managedIdentities",
|
|
444
|
+
columns: [],
|
|
445
|
+
count: 1,
|
|
446
|
+
page: 1,
|
|
447
|
+
pageSize: 20,
|
|
448
|
+
rows: [
|
|
449
|
+
{
|
|
450
|
+
accountEnabled: true,
|
|
451
|
+
appDisplayName: null,
|
|
452
|
+
appId: "mi-client-id",
|
|
453
|
+
appOwnerOrganizationId: null,
|
|
454
|
+
assignedResourceGroups: ["rg-app"],
|
|
455
|
+
azureRbac: "Contributor on resource group rg-app",
|
|
456
|
+
displayName: "uami-prod",
|
|
457
|
+
homepage: null,
|
|
458
|
+
id: "mi-object-id",
|
|
459
|
+
entraPermissionRisk: "none",
|
|
460
|
+
loginUrl: null,
|
|
461
|
+
managedIdentityAssignments: [],
|
|
462
|
+
oauthPemrissionsCount: 0,
|
|
463
|
+
appRolesPermissionCount: 0,
|
|
464
|
+
ownerConfidence: "none",
|
|
465
|
+
permissionRisk: "medium",
|
|
466
|
+
potentialOwners: [],
|
|
467
|
+
publisherName: null,
|
|
468
|
+
rbacRoleAssignmentCount: 2,
|
|
469
|
+
rbacRoleLevel: "medium",
|
|
470
|
+
rbacSubscriptionCount: 1,
|
|
471
|
+
replyUrls: [],
|
|
472
|
+
roleAssignments: [],
|
|
473
|
+
servicePrincipalNames: [],
|
|
474
|
+
servicePrincipalType: "ManagedIdentity",
|
|
475
|
+
tags: [],
|
|
476
|
+
ztaMaxRisk: "none",
|
|
477
|
+
ztaRemediationCountAll: 0,
|
|
478
|
+
ztaRemediationFailedCount: 0
|
|
479
|
+
}
|
|
480
|
+
]
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return jsonResponse({
|
|
485
|
+
collectionId: "entra.servicePrincipals",
|
|
486
|
+
columns: [],
|
|
487
|
+
count: 0,
|
|
488
|
+
page: 1,
|
|
489
|
+
pageSize: 20,
|
|
490
|
+
rows: []
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
globalThis.fetch = fetchMock;
|
|
494
|
+
|
|
495
|
+
const { container, root } = renderComponent(<AzureComponent />);
|
|
496
|
+
|
|
497
|
+
await clickButton("Managed identities");
|
|
498
|
+
await waitForText(container, "uami-prod");
|
|
499
|
+
await clickButton("Open Azure RBAC assignments 2/1");
|
|
500
|
+
await waitForText(container, "Contributor on resource group rg-app");
|
|
501
|
+
|
|
502
|
+
expect(getButton("uami-prod")).toBeDefined();
|
|
503
|
+
expect(container.textContent).toContain("high");
|
|
504
|
+
|
|
505
|
+
const azureRbacRequest = fetchMock.mock.calls
|
|
506
|
+
.map(([input]) => String(input))
|
|
507
|
+
.find((requestUrl) => requestUrl.startsWith("/api/data/azureRbac"));
|
|
508
|
+
expect(azureRbacRequest).toBeDefined();
|
|
509
|
+
|
|
510
|
+
const url = new URL(azureRbacRequest ?? "", window.location.origin);
|
|
511
|
+
expect(url.searchParams.get("servicePrincipalId")).toBe("mi-object-id");
|
|
512
|
+
|
|
513
|
+
act(() => root.unmount());
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
const ztaReport: ZtaReport = {
|
|
517
|
+
Meta: {
|
|
518
|
+
TenantId: "tenant-1",
|
|
519
|
+
TenantName: "Example Tenant"
|
|
520
|
+
},
|
|
521
|
+
Tests: [
|
|
522
|
+
{
|
|
523
|
+
TestId: "zta-1",
|
|
524
|
+
TestStatus: "Completed",
|
|
525
|
+
TestTitle: "Managed identity exposure",
|
|
526
|
+
RelatedObjects: [
|
|
527
|
+
{
|
|
528
|
+
id: "mi-object-id",
|
|
529
|
+
servicePrincipalType: "ManagedIdentity"
|
|
530
|
+
}
|
|
531
|
+
]
|
|
532
|
+
}
|
|
533
|
+
]
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
function jsonResponse(body: unknown): Response {
|
|
537
|
+
return {
|
|
538
|
+
json: async () => body,
|
|
539
|
+
ok: true,
|
|
540
|
+
status: 200
|
|
541
|
+
} as Response;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function zeroTrustAssessmentJsonResponse(body: ZtaReport): Response {
|
|
545
|
+
return jsonResponse({
|
|
546
|
+
collectionId: "zeroTrustAssessment.report",
|
|
547
|
+
rows: body.Tests,
|
|
548
|
+
columns: [],
|
|
549
|
+
page: 1,
|
|
550
|
+
pageSize: 20,
|
|
551
|
+
count: body.Tests.length,
|
|
552
|
+
...body
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function renderComponent(component: React.ReactNode): { container: HTMLElement; root: Root } {
|
|
557
|
+
const container = document.createElement("div");
|
|
558
|
+
document.body.appendChild(container);
|
|
559
|
+
const root = createRoot(container);
|
|
560
|
+
|
|
561
|
+
act(() => {
|
|
562
|
+
root.render(component);
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
return { container, root };
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
async function clickButton(label: string) {
|
|
569
|
+
await act(async () => {
|
|
570
|
+
const button = getButton(label);
|
|
571
|
+
button.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, button: 0 }));
|
|
572
|
+
button.click();
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
async function waitForText(container: HTMLElement, text: string) {
|
|
577
|
+
await waitFor(() => {
|
|
578
|
+
expect(container.textContent).toContain(text);
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
async function waitFor(assertion: () => void): Promise<void> {
|
|
583
|
+
const startedAt = Date.now();
|
|
584
|
+
let lastError: unknown;
|
|
585
|
+
|
|
586
|
+
while (Date.now() - startedAt < 1000) {
|
|
587
|
+
try {
|
|
588
|
+
assertion();
|
|
589
|
+
return;
|
|
590
|
+
} catch (error) {
|
|
591
|
+
lastError = error;
|
|
592
|
+
await act(async () => {
|
|
593
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
throw lastError;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function getButton(label: string): HTMLButtonElement {
|
|
602
|
+
const button = queryButton(label);
|
|
603
|
+
if (!(button instanceof HTMLButtonElement)) {
|
|
604
|
+
throw new Error(`Expected button ${label}.`);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
return button;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function queryButton(label: string): HTMLButtonElement | null {
|
|
611
|
+
const button = [...document.querySelectorAll("button")].find(
|
|
612
|
+
(candidate) => candidate.getAttribute("aria-label") === label || candidate.textContent?.trim() === label
|
|
613
|
+
);
|
|
614
|
+
|
|
615
|
+
return button instanceof HTMLButtonElement ? button : null;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function getInput(label: string): HTMLInputElement {
|
|
619
|
+
const input = [...document.querySelectorAll("input")].find((candidate) => candidate.getAttribute("aria-label") === label);
|
|
620
|
+
if (!(input instanceof HTMLInputElement)) {
|
|
621
|
+
throw new Error(`Expected input ${label}.`);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
return input;
|
|
625
|
+
}
|