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,189 @@
1
+ import { useState } from "react";
2
+
3
+ import type { ZtaRelatedObject } from "../../core/azure/ztaReport";
4
+ import type { ColumnFilters } from "../../report/components/reportTableControls";
5
+ import { Tabs, TabsList, TabsTrigger } from "../../report/components/ui/tabs";
6
+ import { AzureRbacComponent } from "./AzureRbacComponent";
7
+ import { ClosableAzureTab } from "./ClosableAzureTab";
8
+ import { EntraPermissionsComponent } from "./EntraPermissionsComponent";
9
+ import { ManagedIdentityComponent } from "./ManagedIdentityComponent";
10
+ import { ResourceGroupComponent } from "./ResourceGroupComponent";
11
+ import { ServicePrincipalComponent } from "./ServicePrincipalComponent";
12
+ import type { AzureRbacPrincipalSelection, EntraPermissionsPrincipalSelection } from "./ServicePrincipalFieldRenderers";
13
+ import { ZtaComponent } from "./ZtaComponent";
14
+
15
+ type AzureView =
16
+ | "servicePrincipals"
17
+ | "managedIdentities"
18
+ | "resourceGroups"
19
+ | "zeroTrustAssessment"
20
+ | "azureRbac"
21
+ | "entraPermissions";
22
+
23
+ type PrincipalObjectFilter = {
24
+ objectId: string;
25
+ view: Extract<AzureView, "servicePrincipals" | "managedIdentities">;
26
+ };
27
+
28
+ type AzureRbacTab = AzureRbacPrincipalSelection & {
29
+ returnView: Extract<AzureView, "servicePrincipals" | "managedIdentities">;
30
+ };
31
+
32
+ type EntraPermissionsTab = EntraPermissionsPrincipalSelection & {
33
+ returnView: Extract<AzureView, "servicePrincipals" | "managedIdentities">;
34
+ };
35
+
36
+ export function AzureComponent() {
37
+ const [activeView, setActiveView] = useState<AzureView>("servicePrincipals");
38
+ const [azureRbacTab, setAzureRbacTab] = useState<AzureRbacTab | null>(null);
39
+ const [entraPermissionsTab, setEntraPermissionsTab] = useState<EntraPermissionsTab | null>(null);
40
+ const [principalObjectFilter, setPrincipalObjectFilter] = useState<PrincipalObjectFilter | null>(null);
41
+ const [ztaRelatedObjectFilter, setZtaRelatedObjectFilter] = useState<string | null>(null);
42
+
43
+ function openRelatedPrincipal(relatedObject: ZtaRelatedObject) {
44
+ const objectId = relatedObject.id ?? relatedObject.object_id;
45
+ if (!objectId) {
46
+ return;
47
+ }
48
+
49
+ const view = relatedObject.servicePrincipalType === "ManagedIdentity" ? "managedIdentities" : "servicePrincipals";
50
+ setPrincipalObjectFilter({ objectId, view });
51
+ setActiveView(view);
52
+ }
53
+
54
+ function openZtaRelatedObject(objectId: string) {
55
+ setZtaRelatedObjectFilter(objectId);
56
+ setActiveView("zeroTrustAssessment");
57
+ }
58
+
59
+ function openAzureRbac(
60
+ principal: AzureRbacPrincipalSelection,
61
+ returnView: Extract<AzureView, "servicePrincipals" | "managedIdentities">
62
+ ) {
63
+ setAzureRbacTab({ ...principal, returnView });
64
+ setActiveView("azureRbac");
65
+ }
66
+
67
+ function openEntraPermissions(
68
+ principal: EntraPermissionsPrincipalSelection,
69
+ returnView: Extract<AzureView, "servicePrincipals" | "managedIdentities">
70
+ ) {
71
+ setEntraPermissionsTab({ ...principal, returnView });
72
+ setActiveView("entraPermissions");
73
+ }
74
+
75
+ function closeAzureRbac() {
76
+ const nextView = azureRbacTab?.returnView ?? "servicePrincipals";
77
+ setAzureRbacTab(null);
78
+ if (activeView === "azureRbac") {
79
+ setActiveView(nextView);
80
+ }
81
+ }
82
+
83
+ function closeEntraPermissions() {
84
+ const nextView = entraPermissionsTab?.returnView ?? "servicePrincipals";
85
+ setEntraPermissionsTab(null);
86
+ if (activeView === "entraPermissions") {
87
+ setActiveView(nextView);
88
+ }
89
+ }
90
+
91
+ return (
92
+ <section className="flex flex-col">
93
+ <Tabs className="relative z-10 -mb-px gap-0" value={activeView} onValueChange={(value) => setActiveView(value as AzureView)}>
94
+ <TabsList aria-label="Azure data" className="w-fit max-w-full items-end gap-1 rounded-none bg-transparent p-0 shadow-none">
95
+ <TabsTrigger className={azureTabTriggerClassName} value="resourceGroups">
96
+ Resource groups
97
+ </TabsTrigger>
98
+ <TabsTrigger className={azureTabTriggerClassName} value="servicePrincipals">
99
+ Service principals
100
+ </TabsTrigger>
101
+ <TabsTrigger className={azureTabTriggerClassName} value="managedIdentities">
102
+ Managed identities
103
+ </TabsTrigger>
104
+ <TabsTrigger className={azureTabTriggerClassName} value="zeroTrustAssessment">
105
+ Zero Trust Assessment
106
+ </TabsTrigger>
107
+ {azureRbacTab ? (
108
+ <ClosableAzureTab
109
+ active={activeView === "azureRbac"}
110
+ closeLabel={`Close ${azureRbacTab.displayName} Azure RBAC tab`}
111
+ label={azureRbacTab.displayName}
112
+ onClose={closeAzureRbac}
113
+ value="azureRbac"
114
+ />
115
+ ) : null}
116
+ {entraPermissionsTab ? (
117
+ <ClosableAzureTab
118
+ active={activeView === "entraPermissions"}
119
+ closeLabel={`Close ${entraPermissionsTab.displayName} Entra permissions tab`}
120
+ label={`${entraPermissionsTab.displayName} permissions`}
121
+ onClose={closeEntraPermissions}
122
+ value="entraPermissions"
123
+ />
124
+ ) : null}
125
+ </TabsList>
126
+ </Tabs>
127
+ <div className="relative z-0">
128
+ {activeView === "resourceGroups" ? <ResourceGroupComponent /> : null}
129
+ {activeView === "servicePrincipals" ? (
130
+ <ServicePrincipalComponent
131
+ initialFilters={getPrincipalObjectFilters(principalObjectFilter, "servicePrincipals")}
132
+ onAzureRbacClick={(principal) => openAzureRbac(principal, "servicePrincipals")}
133
+ onEntraPermissionsClick={(principal) => openEntraPermissions(principal, "servicePrincipals")}
134
+ onZtaRemediationsClick={openZtaRelatedObject}
135
+ />
136
+ ) : null}
137
+ {activeView === "managedIdentities" ? (
138
+ <ManagedIdentityComponent
139
+ initialFilters={getPrincipalObjectFilters(principalObjectFilter, "managedIdentities")}
140
+ onAzureRbacClick={(principal) => openAzureRbac(principal, "managedIdentities")}
141
+ onEntraPermissionsClick={(principal) => openEntraPermissions(principal, "managedIdentities")}
142
+ onZtaRemediationsClick={openZtaRelatedObject}
143
+ />
144
+ ) : null}
145
+ {activeView === "zeroTrustAssessment" ? (
146
+ <ZtaComponent initialFilters={getZtaRelatedObjectFilters(ztaRelatedObjectFilter)} onRelatedObjectClick={openRelatedPrincipal} />
147
+ ) : null}
148
+ {activeView === "azureRbac" && azureRbacTab ? (
149
+ <AzureRbacComponent key={azureRbacTab.objectId} servicePrincipalId={azureRbacTab.objectId} />
150
+ ) : null}
151
+ {activeView === "entraPermissions" && entraPermissionsTab ? (
152
+ <EntraPermissionsComponent key={entraPermissionsTab.objectId} principalId={entraPermissionsTab.objectId} />
153
+ ) : null}
154
+ </div>
155
+ </section>
156
+ );
157
+ }
158
+
159
+ const azureTabTriggerClassName =
160
+ "rounded-b-none border border-transparent border-b-border bg-muted/70 shadow-none hover:bg-muted data-[state=active]:border-border data-[state=active]:border-b-card data-[state=active]:bg-card data-[state=active]:shadow-none";
161
+
162
+ function getZtaRelatedObjectFilters(objectId: string | null): ColumnFilters | undefined {
163
+ if (!objectId) {
164
+ return undefined;
165
+ }
166
+
167
+ return {
168
+ RelatedObjects: {
169
+ type: "text",
170
+ value: objectId
171
+ }
172
+ };
173
+ }
174
+
175
+ function getPrincipalObjectFilters(
176
+ principalObjectFilter: PrincipalObjectFilter | null,
177
+ view: PrincipalObjectFilter["view"]
178
+ ): ColumnFilters | undefined {
179
+ if (!principalObjectFilter || principalObjectFilter.view !== view) {
180
+ return undefined;
181
+ }
182
+
183
+ return {
184
+ id: {
185
+ type: "text",
186
+ value: principalObjectFilter.objectId
187
+ }
188
+ };
189
+ }
@@ -0,0 +1,104 @@
1
+ import { useCallback } from "react";
2
+
3
+ import type { AzureRbac } from "../../core/azure/azureRbac";
4
+ import { GenericTable } from "../../report/components/GenericTable";
5
+ import type { ColumnFilters } from "../../report/components/reportTableControls";
6
+ import type { ReportFieldDescriptor } from "../../report/reportTypes";
7
+ import type { PermissionRiskLevel } from "../../core/risk/types";
8
+ import { readAzureRbac } from "./api";
9
+
10
+ const permissionRiskLevelOptions: PermissionRiskLevel[] = ["high", "medium", "low", "none"];
11
+ const azureScopeTypeOptions = ["ManagementGroup", "Subscription", "ResourceGroup", "Resource", "Unknown"];
12
+ const azurePrincipalTypeOptions = ["User", "Group", "ServicePrincipal", "ForeignGroup", "Device", "ManagedIdentity"];
13
+
14
+ const azureRbacFields: ReportFieldDescriptor<AzureRbac>[] = [
15
+ {
16
+ id: "accessDisplayName",
17
+ label: "Access",
18
+ valueType: "text",
19
+ getValue: (assignment) => assignment.accessDisplayName,
20
+ filter: { kind: "text" }
21
+ },
22
+ {
23
+ id: "accessRisk",
24
+ label: "Risk",
25
+ valueType: "riskLevel",
26
+ getValue: (assignment) => assignment.accessRisk,
27
+ filter: { kind: "multiSelect", options: permissionRiskLevelOptions }
28
+ },
29
+ {
30
+ id: "roleDefinitionName",
31
+ label: "Role",
32
+ valueType: "text",
33
+ getValue: (assignment) => assignment.roleDefinitionName,
34
+ filter: { kind: "text" }
35
+ },
36
+ {
37
+ id: "accessScopeType",
38
+ label: "Scope type",
39
+ valueType: "text",
40
+ getValue: (assignment) => assignment.accessScopeType,
41
+ filter: { kind: "multiSelect", options: azureScopeTypeOptions }
42
+ },
43
+ {
44
+ id: "subscriptionName",
45
+ label: "Subscription",
46
+ valueType: "text",
47
+ getValue: (assignment) => assignment.subscriptionName,
48
+ filter: { kind: "text" }
49
+ },
50
+ {
51
+ id: "accessResourceGroup",
52
+ label: "Resource group",
53
+ valueType: "text",
54
+ getValue: (assignment) => assignment.accessResourceGroup,
55
+ filter: { kind: "text" }
56
+ },
57
+ {
58
+ id: "scopeResourceName",
59
+ label: "Resource",
60
+ valueType: "text",
61
+ getValue: (assignment) => assignment.scopeResourceName,
62
+ filter: { kind: "text" }
63
+ },
64
+ {
65
+ id: "principalType",
66
+ label: "Principal type",
67
+ valueType: "text",
68
+ getValue: (assignment) => assignment.principalType,
69
+ filter: { kind: "multiSelect", options: azurePrincipalTypeOptions }
70
+ },
71
+ {
72
+ id: "scope",
73
+ label: "Scope",
74
+ valueType: "text",
75
+ getValue: (assignment) => assignment.scope,
76
+ filter: { kind: "text" }
77
+ },
78
+ {
79
+ id: "roleAssignmentId",
80
+ label: "Assignment ID",
81
+ valueType: "text",
82
+ getValue: (assignment) => assignment.roleAssignmentId,
83
+ filter: { kind: "text" }
84
+ }
85
+ ];
86
+
87
+ export function AzureRbacComponent({ servicePrincipalId }: { servicePrincipalId: string }) {
88
+ const loadPage = useCallback(
89
+ ({ filters, page, signal }: { filters: ColumnFilters; page: number; signal: AbortSignal }) =>
90
+ readAzureRbac({ filters, page, servicePrincipalId, signal }),
91
+ [servicePrincipalId]
92
+ );
93
+
94
+ return (
95
+ <GenericTable
96
+ emptyMessage="No Azure RBAC assignments match the filter."
97
+ fields={azureRbacFields}
98
+ getRowKey={(row) => row.roleAssignmentId ?? `${row.servicePrincipalId}:${row.scope}:${row.roleDefinitionId ?? row.roleDefinitionName ?? ""}`}
99
+ loadPage={loadPage}
100
+ loadingMessage="Loading Azure RBAC assignments..."
101
+ minWidthClassName="min-w-[2200px]"
102
+ />
103
+ );
104
+ }
@@ -0,0 +1,42 @@
1
+ import { X } from "lucide-react";
2
+
3
+ import { cn } from "../../lib/utils";
4
+ import { TabsTrigger } from "../../report/components/ui/tabs";
5
+
6
+ type ClosableAzureTabProps = {
7
+ active: boolean;
8
+ closeLabel: string;
9
+ label: string;
10
+ onClose: () => void;
11
+ value: "azureRbac" | "entraPermissions";
12
+ };
13
+
14
+ export function ClosableAzureTab({ active, closeLabel, label, onClose, value }: ClosableAzureTabProps) {
15
+ return (
16
+ <span
17
+ className={cn(
18
+ "inline-flex min-h-8 max-w-full items-center overflow-hidden rounded-t-sm rounded-b-none border border-transparent border-b-border bg-muted/70 transition-colors hover:bg-muted",
19
+ active && "border-border border-b-card bg-card"
20
+ )}
21
+ >
22
+ <TabsTrigger
23
+ className="min-w-0 max-w-64 flex-1 justify-start overflow-hidden rounded-r-none border-0 bg-transparent pr-2 shadow-none data-[state=active]:bg-transparent data-[state=active]:shadow-none"
24
+ value={value}
25
+ >
26
+ <span className="truncate">{label}</span>
27
+ </TabsTrigger>
28
+ <button
29
+ aria-label={closeLabel}
30
+ className={cn(
31
+ "mr-1 inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-sm text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
32
+ active && "text-foreground"
33
+ )}
34
+ title={closeLabel}
35
+ type="button"
36
+ onClick={onClose}
37
+ >
38
+ <X aria-hidden="true" className="h-3.5 w-3.5" />
39
+ </button>
40
+ </span>
41
+ );
42
+ }
@@ -0,0 +1,194 @@
1
+ import { useEffect, useMemo, useState } from "react";
2
+
3
+ import { GenericTable } from "../../report/components/GenericTable";
4
+ import type { ReportFieldDescriptor } from "../../report/reportTypes";
5
+ import type { PermissionRiskLevel } from "../../core/risk/types";
6
+ import { readEntraPermissions, type EntraPrincipalPermissionsResponse } from "./api";
7
+
8
+ type EntraPermissionRow = {
9
+ id: string;
10
+ permissionType: "OAuth2 permission grant" | "App role assignment";
11
+ resourceDisplayName: string | null;
12
+ resourceId: string;
13
+ permissionDisplayName: string | null;
14
+ permissionValue: string;
15
+ consentType: string | null;
16
+ risk: PermissionRiskLevel | null;
17
+ principalDisplayName: string | null;
18
+ principalId: string | null;
19
+ };
20
+
21
+ type LoadState =
22
+ | {
23
+ status: "loading";
24
+ }
25
+ | {
26
+ status: "ready";
27
+ }
28
+ | {
29
+ status: "error";
30
+ message: string;
31
+ };
32
+
33
+ const permissionTypeOptions: EntraPermissionRow["permissionType"][] = ["OAuth2 permission grant", "App role assignment"];
34
+ const consentTypeOptions = ["AllPrincipals", "Principal"];
35
+ const permissionRiskLevelOptions: PermissionRiskLevel[] = ["high", "medium", "low", "none"];
36
+
37
+ const entraPermissionFields: ReportFieldDescriptor<EntraPermissionRow>[] = [
38
+ {
39
+ id: "permissionType",
40
+ label: "Type",
41
+ valueType: "text",
42
+ getValue: (permission) => permission.permissionType,
43
+ filter: { kind: "multiSelect", options: permissionTypeOptions }
44
+ },
45
+ {
46
+ id: "permissionDisplayName",
47
+ label: "Permission",
48
+ valueType: "text",
49
+ getValue: (permission) => permission.permissionDisplayName,
50
+ filter: { kind: "text" }
51
+ },
52
+ {
53
+ id: "permissionValue",
54
+ label: "Value",
55
+ valueType: "text",
56
+ getValue: (permission) => permission.permissionValue,
57
+ filter: { kind: "text" }
58
+ },
59
+ {
60
+ id: "resourceDisplayName",
61
+ label: "Resource",
62
+ valueType: "text",
63
+ getValue: (permission) => permission.resourceDisplayName,
64
+ filter: { kind: "text" }
65
+ },
66
+ {
67
+ id: "resourceId",
68
+ label: "Resource ID",
69
+ valueType: "text",
70
+ getValue: (permission) => permission.resourceId,
71
+ filter: { kind: "text" }
72
+ },
73
+ {
74
+ id: "consentType",
75
+ label: "Consent",
76
+ valueType: "text",
77
+ getValue: (permission) => permission.consentType,
78
+ filter: { kind: "multiSelect", options: consentTypeOptions }
79
+ },
80
+ {
81
+ id: "risk",
82
+ label: "Risk",
83
+ valueType: "riskLevel",
84
+ getValue: (permission) => permission.risk,
85
+ filter: { kind: "multiSelect", options: permissionRiskLevelOptions }
86
+ },
87
+ {
88
+ id: "principalDisplayName",
89
+ label: "Principal",
90
+ valueType: "text",
91
+ getValue: (permission) => permission.principalDisplayName,
92
+ filter: { kind: "text" }
93
+ },
94
+ {
95
+ id: "principalId",
96
+ label: "Principal ID",
97
+ valueType: "text",
98
+ getValue: (permission) => permission.principalId,
99
+ filter: { kind: "text" }
100
+ },
101
+ {
102
+ id: "id",
103
+ label: "Assignment ID",
104
+ valueType: "text",
105
+ getValue: (permission) => permission.id,
106
+ filter: { kind: "text" }
107
+ }
108
+ ];
109
+
110
+ export function EntraPermissionsComponent({ principalId }: { principalId: string }) {
111
+ const [permissions, setPermissions] = useState<EntraPrincipalPermissionsResponse | null>(null);
112
+ const [loadState, setLoadState] = useState<LoadState>({ status: "loading" });
113
+
114
+ useEffect(() => {
115
+ const controller = new AbortController();
116
+
117
+ async function loadPermissions() {
118
+ setLoadState({ status: "loading" });
119
+
120
+ try {
121
+ setPermissions(await readEntraPermissions({ principalId, signal: controller.signal }));
122
+ setLoadState({ status: "ready" });
123
+ } catch (error) {
124
+ if (error instanceof DOMException && error.name === "AbortError") {
125
+ return;
126
+ }
127
+
128
+ setPermissions(null);
129
+ setLoadState({
130
+ status: "error",
131
+ message: error instanceof Error ? error.message : "Could not load Entra permissions."
132
+ });
133
+ }
134
+ }
135
+
136
+ loadPermissions();
137
+
138
+ return () => controller.abort();
139
+ }, [principalId]);
140
+
141
+ const rows = useMemo(() => (permissions ? mapPermissionsToRows(permissions) : []), [permissions]);
142
+
143
+ if (!permissions && loadState.status === "loading") {
144
+ return <div className="rounded-md border bg-card p-4 text-sm text-muted-foreground">Loading Entra permissions...</div>;
145
+ }
146
+
147
+ if (!permissions && loadState.status === "error") {
148
+ return <div className="rounded-md border border-destructive/40 bg-card p-4 text-sm text-destructive">{loadState.message}</div>;
149
+ }
150
+
151
+ return (
152
+ <>
153
+ {loadState.status === "error" ? (
154
+ <div className="rounded-md border border-destructive/40 bg-card p-4 text-sm text-destructive">{loadState.message}</div>
155
+ ) : null}
156
+ <GenericTable
157
+ emptyMessage="No Entra permissions match the filter."
158
+ fields={entraPermissionFields}
159
+ getRowKey={(row) => `${row.permissionType}:${row.id}`}
160
+ minWidthClassName="min-w-[1800px]"
161
+ rows={rows}
162
+ />
163
+ </>
164
+ );
165
+ }
166
+
167
+ function mapPermissionsToRows(permissions: EntraPrincipalPermissionsResponse): EntraPermissionRow[] {
168
+ return [
169
+ ...permissions.oauth2PermissionGrants.map((grant) => ({
170
+ id: grant.id,
171
+ permissionType: "OAuth2 permission grant" as const,
172
+ resourceDisplayName: null,
173
+ resourceId: grant.resourceId,
174
+ permissionDisplayName: null,
175
+ permissionValue: grant.scope,
176
+ consentType: grant.consentType,
177
+ risk: grant.risk,
178
+ principalDisplayName: null,
179
+ principalId: grant.principalId
180
+ })),
181
+ ...permissions.appRoleAssignments.map((assignment) => ({
182
+ id: assignment.id,
183
+ permissionType: "App role assignment" as const,
184
+ resourceDisplayName: assignment.resourceDisplayName,
185
+ resourceId: assignment.resourceId,
186
+ permissionDisplayName: assignment.appRoleDisplayName,
187
+ permissionValue: assignment.appRoleValue ?? assignment.appRoleId,
188
+ consentType: null,
189
+ risk: null,
190
+ principalDisplayName: assignment.principalDisplayName,
191
+ principalId: assignment.principalId
192
+ }))
193
+ ];
194
+ }