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,70 @@
1
+ import type { ZtaRemediationSummary } from "../../core/azure/ztaReport";
2
+ import { cn } from "../../lib/utils";
3
+ import { Badge, type BadgeProps } from "../../report/components/ui/badge";
4
+
5
+ type ZtaRemediationBadgeProps = Pick<
6
+ ZtaRemediationSummary,
7
+ "ztaMaxRisk" | "ztaRemediationCountAll" | "ztaRemediationFailedCount"
8
+ > & {
9
+ onClick?: () => void;
10
+ };
11
+
12
+ export function ZtaRemediationBadge({
13
+ onClick,
14
+ ztaMaxRisk,
15
+ ztaRemediationCountAll,
16
+ ztaRemediationFailedCount
17
+ }: ZtaRemediationBadgeProps) {
18
+ const content = `${ztaRemediationFailedCount}/${ztaRemediationCountAll}`;
19
+ const variant = getZtaRiskVariant(ztaMaxRisk);
20
+
21
+ if (onClick) {
22
+ return (
23
+ <button
24
+ aria-label={`Open ZTA remediations ${content}`}
25
+ className={cn(
26
+ "inline-flex min-w-12 items-center justify-center rounded-full border px-2.5 py-0.5 text-xs font-semibold tabular-nums transition-colors hover:opacity-80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
27
+ getZtaRiskClassName(ztaMaxRisk)
28
+ )}
29
+ type="button"
30
+ onClick={onClick}
31
+ >
32
+ {content}
33
+ </button>
34
+ );
35
+ }
36
+
37
+ return (
38
+ <Badge className="min-w-12 justify-center tabular-nums" variant={variant}>
39
+ {content}
40
+ </Badge>
41
+ );
42
+ }
43
+
44
+ function getZtaRiskClassName(ztaMaxRisk: ZtaRemediationSummary["ztaMaxRisk"]): string {
45
+ switch (ztaMaxRisk) {
46
+ case "high":
47
+ return "border-transparent bg-red-100 text-red-800";
48
+ case "medium":
49
+ return "border-transparent bg-amber-100 text-amber-800";
50
+ case "low":
51
+ return "border-transparent bg-emerald-100 text-emerald-800";
52
+ case "none":
53
+ default:
54
+ return "border-transparent bg-muted text-muted-foreground";
55
+ }
56
+ }
57
+
58
+ function getZtaRiskVariant(ztaMaxRisk: ZtaRemediationSummary["ztaMaxRisk"]): BadgeProps["variant"] {
59
+ switch (ztaMaxRisk) {
60
+ case "high":
61
+ return "riskHigh";
62
+ case "medium":
63
+ return "riskMedium";
64
+ case "low":
65
+ return "riskLow";
66
+ case "none":
67
+ default:
68
+ return "riskNone";
69
+ }
70
+ }
@@ -0,0 +1,216 @@
1
+ import type { ManagedIdentity } from "../../core/azure/entra/managedIdentity";
2
+ import type { ServicePrincipal } from "../../core/azure/entra/servicePrincipal";
3
+ import type { EntraOAuth2PermissionGrant } from "../../core/azure/entra/types";
4
+ import type { AzureRbac } from "../../core/azure/azureRbac";
5
+ import type { ResourceGroupOwnershipRow } from "../../core/azure/resources";
6
+ import type { ZtaReport } from "../../core/azure/ztaReport";
7
+ import type { EntraAppRoleAssignment } from "../../providers/azure/inputTransferObject/entra/EntraAppRoleAssignment";
8
+ import type { ColumnFilters } from "../../report/components/reportTableControls";
9
+ import { appendRuntimeCollectionFilters } from "../../report/runtimeCollectionQuery";
10
+
11
+ type ServicePrincipalRuntimeResponse = {
12
+ collectionId: "entra.servicePrincipals";
13
+ rows: ServicePrincipal[];
14
+ columns: string[];
15
+ page: number;
16
+ pageSize: number;
17
+ count: number;
18
+ };
19
+
20
+ type ManagedIdentityRuntimeResponse = {
21
+ collectionId: "entra.managedIdentities";
22
+ rows: ManagedIdentity[];
23
+ columns: string[];
24
+ page: number;
25
+ pageSize: number;
26
+ count: number;
27
+ };
28
+
29
+ type ResourceGroupRuntimeResponse = {
30
+ collectionId: "azureResources.resourceGroupOwnership";
31
+ rows: ResourceGroupOwnershipRow[];
32
+ columns: string[];
33
+ page: number;
34
+ pageSize: number;
35
+ count: number;
36
+ };
37
+
38
+ type AzureRbacRuntimeResponse = {
39
+ collectionId: "azureRbac";
40
+ rows: AzureRbac[];
41
+ columns: string[];
42
+ page: number;
43
+ pageSize: number;
44
+ count: number;
45
+ };
46
+
47
+ export type EntraPrincipalPermissionsResponse = {
48
+ principalId: string;
49
+ oauth2PermissionGrants: EntraOAuth2PermissionGrant[];
50
+ appRoleAssignments: EntraAppRoleAssignment[];
51
+ };
52
+
53
+ type ZeroTrustAssessmentRuntimeResponse = ZtaReport & {
54
+ collectionId: "zeroTrustAssessment.report";
55
+ rows: ZtaReport["Tests"];
56
+ columns: string[];
57
+ page: number;
58
+ pageSize: number;
59
+ count: number;
60
+ };
61
+
62
+ const remotePageSize = 20;
63
+
64
+ export async function readServicePrincipals({
65
+ filters,
66
+ page,
67
+ signal
68
+ }: {
69
+ filters: ColumnFilters;
70
+ page: number;
71
+ signal: AbortSignal;
72
+ }): Promise<ServicePrincipalRuntimeResponse> {
73
+ const url = new URL("/api/data/entra/servicePrincipals", window.location.origin);
74
+ url.searchParams.set("page", String(page));
75
+ url.searchParams.set("count", String(remotePageSize));
76
+ appendRuntimeCollectionFilters(url, filters);
77
+
78
+ const response = await fetch(`${url.pathname}${url.search}`, { signal });
79
+ if (!response.ok) {
80
+ throw new Error(`Service principals read failed: ${response.status}`);
81
+ }
82
+
83
+ return (await response.json()) as ServicePrincipalRuntimeResponse;
84
+ }
85
+
86
+ export async function readManagedIdentities({
87
+ filters,
88
+ page,
89
+ signal
90
+ }: {
91
+ filters: ColumnFilters;
92
+ page: number;
93
+ signal: AbortSignal;
94
+ }): Promise<ManagedIdentityRuntimeResponse> {
95
+ const url = new URL("/api/data/entra/managedIdentities", window.location.origin);
96
+ url.searchParams.set("page", String(page));
97
+ url.searchParams.set("count", String(remotePageSize));
98
+ appendRuntimeCollectionFilters(url, filters);
99
+
100
+ const response = await fetch(`${url.pathname}${url.search}`, { signal });
101
+ if (!response.ok) {
102
+ throw new Error(`Managed identities read failed: ${response.status}`);
103
+ }
104
+
105
+ return (await response.json()) as ManagedIdentityRuntimeResponse;
106
+ }
107
+
108
+ export async function readResourceGroups({
109
+ filters,
110
+ page,
111
+ signal
112
+ }: {
113
+ filters: ColumnFilters;
114
+ page: number;
115
+ signal: AbortSignal;
116
+ }): Promise<ResourceGroupRuntimeResponse> {
117
+ const url = new URL("/api/data/azureResources/resourceGroupOwnership", window.location.origin);
118
+ url.searchParams.set("page", String(page));
119
+ url.searchParams.set("count", String(remotePageSize));
120
+ appendRuntimeCollectionFilters(url, filters);
121
+
122
+ const response = await fetch(`${url.pathname}${url.search}`, { signal });
123
+ if (!response.ok) {
124
+ throw new Error(`Resource groups read failed: ${response.status}`);
125
+ }
126
+
127
+ return (await response.json()) as ResourceGroupRuntimeResponse;
128
+ }
129
+
130
+ export async function readAzureRbac({
131
+ filters,
132
+ page,
133
+ servicePrincipalId,
134
+ signal
135
+ }: {
136
+ filters: ColumnFilters;
137
+ page: number;
138
+ servicePrincipalId: string;
139
+ signal: AbortSignal;
140
+ }): Promise<AzureRbacRuntimeResponse> {
141
+ const url = new URL("/api/data/azureRbac", window.location.origin);
142
+ url.searchParams.set("servicePrincipalId", servicePrincipalId);
143
+ url.searchParams.set("page", String(page));
144
+ url.searchParams.set("count", String(remotePageSize));
145
+ appendRuntimeCollectionFilters(url, filters);
146
+
147
+ const response = await fetch(`${url.pathname}${url.search}`, { signal });
148
+ if (!response.ok) {
149
+ throw new Error(`Azure RBAC read failed: ${response.status}`);
150
+ }
151
+
152
+ return (await response.json()) as AzureRbacRuntimeResponse;
153
+ }
154
+
155
+ export async function readEntraPermissions({
156
+ principalId,
157
+ signal
158
+ }: {
159
+ principalId: string;
160
+ signal: AbortSignal;
161
+ }): Promise<EntraPrincipalPermissionsResponse> {
162
+ const url = new URL("/api/data/entra/permissions", window.location.origin);
163
+ url.searchParams.set("principalId", principalId);
164
+
165
+ const response = await fetch(`${url.pathname}${url.search}`, { signal });
166
+ if (!response.ok) {
167
+ throw new Error(`Entra permissions read failed: ${response.status}`);
168
+ }
169
+
170
+ return (await response.json()) as EntraPrincipalPermissionsResponse;
171
+ }
172
+
173
+ export async function readZeroTrustAssessmentReport({
174
+ filters = {},
175
+ page,
176
+ pageSize = remotePageSize,
177
+ signal
178
+ }: {
179
+ filters?: ColumnFilters;
180
+ page?: number;
181
+ pageSize?: number;
182
+ signal: AbortSignal;
183
+ }): Promise<ZeroTrustAssessmentRuntimeResponse> {
184
+ const url = new URL("/api/data/zeroTrustAssessment/report", window.location.origin);
185
+ if (page !== undefined) {
186
+ url.searchParams.set("page", String(page));
187
+ }
188
+ if (pageSize !== undefined) {
189
+ url.searchParams.set("count", String(pageSize));
190
+ }
191
+ appendRuntimeCollectionFilters(url, filters);
192
+
193
+ const response = await fetch(`${url.pathname}${url.search}`, { signal });
194
+ if (!response.ok) {
195
+ throw new Error(`Zero Trust Assessment report read failed: ${response.status}`);
196
+ }
197
+
198
+ return (await response.json()) as ZeroTrustAssessmentRuntimeResponse;
199
+ }
200
+
201
+ export async function updateDisabledOwnerEvidence({
202
+ key,
203
+ disabled
204
+ }: {
205
+ key: string;
206
+ disabled: boolean;
207
+ }): Promise<void> {
208
+ const url = new URL("/api/data/azureResources/resourceGroupOwnership/disabledEvidence", window.location.origin);
209
+ url.searchParams.set("key", key);
210
+ url.searchParams.set("disabled", String(disabled));
211
+
212
+ const response = await fetch(`${url.pathname}${url.search}`);
213
+ if (!response.ok) {
214
+ throw new Error(`Owner candidate update failed: ${response.status}`);
215
+ }
216
+ }
@@ -0,0 +1,247 @@
1
+ import type { ReportColumnHelp } from "../../report/reportTypes";
2
+
3
+ export const azureOwnerColumnHelp = {
4
+ target: {
5
+ source: "Computed by app from Azure resource snapshot JSON.",
6
+ logic: [
7
+ "Shows Subscription when the row represents a subscription.",
8
+ "Shows the resource group name when the row represents a resource group."
9
+ ]
10
+ },
11
+ resourceGroup: {
12
+ source: "Computed by app from Azure resource snapshot JSON.",
13
+ logic: ["Shows the resource group name from the owner row built from the Azure resource snapshot."]
14
+ },
15
+ subscription: {
16
+ source: "Direct from Azure resource snapshot JSON.",
17
+ field: "subscriptionName",
18
+ logic: ["Copied from the subscription or resource group record used to build the owner row."]
19
+ },
20
+ subscriptionName: {
21
+ source: "Direct from Azure resource snapshot JSON.",
22
+ field: "subscriptionName",
23
+ logic: ["Copied from the subscription or resource group record used to build the owner row."]
24
+ },
25
+ owner: {
26
+ source: "Computed by app from Azure tags and activity logs.",
27
+ logic: [
28
+ "First checks configured owner tags on the resource group or subscription.",
29
+ "If no tag owner is found, falls back to the most recent write/delete/action caller in Azure activity logs.",
30
+ "CostCenter tag values are mapped through the configured cost center owner map."
31
+ ]
32
+ },
33
+ confidence: {
34
+ source: "Computed by app during owner resolution.",
35
+ logic: [
36
+ "Tag-derived owners use the configured confidence for that tag.",
37
+ "Activity-log fallback is low confidence.",
38
+ "No usable tag or activity caller returns none."
39
+ ]
40
+ },
41
+ ownerConfidence: {
42
+ source: "Computed by app during owner resolution.",
43
+ logic: [
44
+ "Uses the strongest confidence among resource group owner rows targeted by this principal's Azure RBAC scopes.",
45
+ "No usable owner evidence returns none."
46
+ ]
47
+ },
48
+ source: {
49
+ source: "Computed by app during owner resolution.",
50
+ logic: [
51
+ "tag.<name> means the owner came from that Azure tag.",
52
+ "activity.lastModifier means the owner came from resource group activity.",
53
+ "activity.subscriptionLastModifier means the owner came from subscription activity.",
54
+ "none means no owner evidence was found."
55
+ ]
56
+ },
57
+ evidence: {
58
+ source: "Computed by app from Azure tag values or activity logs.",
59
+ logic: [
60
+ "For tag owners, shows the tag value or CostCenter mapping.",
61
+ "For activity fallback, shows recent distinct callers and event timestamps.",
62
+ "Service principal callers are displayed by Entra display name when known."
63
+ ]
64
+ }
65
+ } satisfies Record<string, ReportColumnHelp>;
66
+
67
+ export const azureManagedIdentityColumnHelp = {
68
+ displayName: {
69
+ source: "Direct from Entra JSON.",
70
+ field: "displayName",
71
+ logic: ["Displayed as-is, with empty values shown as a dash."]
72
+ },
73
+ resourceGroup: {
74
+ source: "Computed by app from Azure resource snapshot JSON.",
75
+ logic: [
76
+ "For user-assigned managed identities, uses the managed identity resource group captured in userAssignedManagedIdentities.",
77
+ "For system-assigned managed identities, uses the resource group of the assigned Azure resource.",
78
+ "When the same identity appears in multiple groups, shows each distinct resource group."
79
+ ]
80
+ },
81
+ assignedResourceGroups: {
82
+ source: "Computed by app from Azure resource snapshot JSON.",
83
+ logic: [
84
+ "For user-assigned managed identities, uses the managed identity resource group captured in userAssignedManagedIdentities.",
85
+ "For system-assigned managed identities, uses the resource group of the assigned Azure resource.",
86
+ "When the same identity appears in multiple groups, shows each distinct resource group."
87
+ ]
88
+ },
89
+ potentialOwners: {
90
+ source: "Computed by app from the Owner Report resource group rows.",
91
+ logic: [
92
+ "Looks up the resource group shown for the managed identity in the resolved owner report.",
93
+ "Projects each resource group's owner onto the managed identity.",
94
+ "Shows the distinct resolved owner email addresses separated by commas."
95
+ ]
96
+ },
97
+ ownerConfidence: {
98
+ source: "Computed by app from the matching resource group owner rows.",
99
+ logic: [
100
+ "Uses the strongest confidence among resource group owner rows assigned to this managed identity.",
101
+ "No usable owner evidence returns none."
102
+ ]
103
+ },
104
+ miAssignment: {
105
+ source: "Computed by app from Azure resource snapshot JSON.",
106
+ logic: [
107
+ "Scans Azure resources for system-assigned and user-assigned managed identities.",
108
+ "Matches assignments to this Entra service principal by object ID or client/app ID.",
109
+ "Shows assigned resource name, type, and resource group."
110
+ ]
111
+ },
112
+ permissionRisk: {
113
+ source: "Computed by app from Azure roleAssignments JSON.",
114
+ logic: [
115
+ "Finds Azure RBAC assignments whose principalId matches this Entra object ID, case-insensitively.",
116
+ "Owner, User Access Administrator, Role Based Access Control Administrator, Privileged Role Administrator, and Key Vault Administrator start as high risk.",
117
+ "Reader starts as low risk; missing, custom, unclassified, Contributor, Administrator, Data Owner, Data Contributor, and Operator-style roles start as medium risk.",
118
+ "Management group and subscription scopes are broad: a medium role at a broad scope is raised to high.",
119
+ "Resource scopes are narrow: a high role at a single resource is lowered to medium.",
120
+ "Column shows the highest adjusted risk across all matching assignments; no assignments returns none."
121
+ ]
122
+ },
123
+ azureRbac: {
124
+ source: "Computed by app from Azure roleAssignments JSON.",
125
+ logic: [
126
+ "Lists matching Azure RBAC assignments for this principal.",
127
+ "Adds risk reasons such as privileged role, write-capable role, read-only role, broad scope, or unclassified role.",
128
+ "Shows no Azure RBAC assignments when no assignment matches."
129
+ ]
130
+ },
131
+ oauthPemrissionsCount: {
132
+ source: "Computed by app from Entra OAuth2 permission grants and app role assignments JSON.",
133
+ field: "oauth2PermissionGrants[].scope and appRoleAssignments[].principalId",
134
+ logic: [
135
+ "Finds OAuth2 permission grants whose clientId matches this Entra object ID, case-insensitively.",
136
+ "Counts individual delegated permission scopes split from the grant scope string.",
137
+ "Finds app role assignments whose principalId matches this Entra object ID and counts each matching application permission.",
138
+ "Badge format is delegated/application, for example 0/1 means no delegated scopes and one application app role assignment.",
139
+ "Badge risk is high when any matching OAuth2 permission grant has tenant-wide AllPrincipals consent, medium when any non-tenant-wide delegated or application permission exists, and none when no permissions exist.",
140
+ "For Directory.Read.All on a managed identity, resolve the managed identity service principal by Object ID, resolve Microsoft Graph by appId 00000003-0000-0000-c000-000000000000, select the Directory.Read.All application app role, then create the service principal app role assignment with ServicePrincipalId and PrincipalId set to the managed identity service principal Id."
141
+ ]
142
+ },
143
+ appRolesPermissionCount: {
144
+ source: "Computed by app from Entra app role assignments JSON.",
145
+ field: "appRoleAssignments[].principalId",
146
+ logic: [
147
+ "Finds app role assignments whose principalId matches this Entra object ID, case-insensitively.",
148
+ "Counts each matching app role assignment.",
149
+ "No matching assignments returns zero."
150
+ ]
151
+ },
152
+ entraPermissionRisk: {
153
+ source: "Computed by app from Entra OAuth2 permission grants and app role assignments JSON.",
154
+ field: "oauth2PermissionGrants[].consentType and appRoleAssignments[].principalId",
155
+ logic: [
156
+ "Returns high when any matching OAuth2 permission grant has consentType equal to AllPrincipals.",
157
+ "Returns medium when matching delegated scopes or app role assignments exist without tenant-wide delegated consent.",
158
+ "Returns none when no matching Entra permissions exist."
159
+ ]
160
+ },
161
+ enabled: {
162
+ source: "Direct from Entra JSON.",
163
+ field: "accountEnabled"
164
+ },
165
+ accountEnabled: {
166
+ source: "Direct from Entra JSON.",
167
+ field: "accountEnabled"
168
+ },
169
+ objectId: {
170
+ source: "Direct from Entra JSON.",
171
+ field: "id"
172
+ },
173
+ appId: {
174
+ source: "Direct from Entra JSON.",
175
+ field: "appId"
176
+ },
177
+ appDisplayName: {
178
+ source: "Direct from Entra JSON.",
179
+ field: "appDisplayName",
180
+ logic: ["Displayed as-is, with empty values shown as a dash."]
181
+ },
182
+ servicePrincipalNames: {
183
+ source: "Direct from Entra JSON.",
184
+ field: "servicePrincipalNames",
185
+ logic: ["Array values are joined with commas; empty arrays are shown as a dash."]
186
+ },
187
+ tags: {
188
+ source: "Direct from Entra JSON.",
189
+ field: "tags",
190
+ logic: ["Array values are joined with commas; empty arrays are shown as a dash."]
191
+ }
192
+ } satisfies Record<string, ReportColumnHelp>;
193
+
194
+ export const azureServicePrincipalColumnHelp = {
195
+ ...azureManagedIdentityColumnHelp,
196
+ ownership: {
197
+ source: "Computed by app from Entra JSON.",
198
+ logic: [
199
+ "ManagedIdentity service principals are treated as Tenant owned.",
200
+ "Application service principals are Tenant owned when appOwnerOrganizationId equals the snapshot tenantId.",
201
+ "A different appOwnerOrganizationId is External; a missing value is Unknown."
202
+ ]
203
+ },
204
+ servicePrincipalOwners: {
205
+ source: "Direct from Entra JSON.",
206
+ field: "servicePrincipals[].servicePrincipalOwners",
207
+ logic: [
208
+ "Exported from the Microsoft Graph Service Principal owners relationship.",
209
+ "Owner mail is preferred, then userPrincipalName, displayName, and object ID.",
210
+ "Multiple owners are shown as a comma-separated list."
211
+ ]
212
+ },
213
+ potentialOwners: {
214
+ source: "Computed by app from Service Principal Azure RBAC assignments and Azure owner report rows.",
215
+ logic: [
216
+ "Finds Azure RBAC assignments for this Service Principal.",
217
+ "Collects resource groups targeted by those RBAC scopes.",
218
+ "Subscription-scoped RBAC expands to every resource group in the assigned subscription.",
219
+ "Projects the resolved owner list from those resource group owner rows."
220
+ ]
221
+ },
222
+ ownerConfidence: {
223
+ source: "Computed by app from the matching resource group owner rows.",
224
+ logic: [
225
+ "Uses the strongest confidence among resource group owner rows targeted by this Service Principal's Azure RBAC scopes.",
226
+ "No usable owner evidence returns none."
227
+ ]
228
+ },
229
+ azureRbac: {
230
+ source: "Computed by app from Azure roleAssignments JSON.",
231
+ logic: [
232
+ "Lists matching Azure RBAC assignments for this principal.",
233
+ "For managed identity permission summaries, includes risk reasons such as broad scope or privileged role.",
234
+ "When no permission summary exists, lists direct role assignments by role and formatted scope."
235
+ ]
236
+ },
237
+ type: {
238
+ source: "Direct from Entra JSON.",
239
+ field: "servicePrincipalType",
240
+ logic: ["Displayed as-is, with empty values shown as a dash."]
241
+ },
242
+ servicePrincipalType: {
243
+ source: "Direct from Entra JSON.",
244
+ field: "servicePrincipalType",
245
+ logic: ["Displayed as-is, with empty values shown as a dash."]
246
+ }
247
+ } satisfies Record<string, ReportColumnHelp>;
@@ -0,0 +1,70 @@
1
+ import type { AzureRoleAssignment } from "./resources";
2
+ import type { PermissionRiskLevel } from "../risk/types";
3
+
4
+ export type AzureRbac = AzureRoleAssignment & {
5
+ servicePrincipalId: string;
6
+ accessRisk: PermissionRiskLevel;
7
+ accessScope: string;
8
+ accessScopeType: AzureRoleAssignment["scopeType"];
9
+ accessResourceId: string | null;
10
+ accessResourceGroup: string | null;
11
+ accessSubscriptionId: string | null;
12
+ accessDisplayName: string;
13
+ };
14
+
15
+ export function mapRoleAssignmentToAzureRbac(
16
+ assignment: AzureRoleAssignment,
17
+ permissionRiskLevel: PermissionRiskLevel
18
+ ): AzureRbac {
19
+ return {
20
+ ...assignment,
21
+ servicePrincipalId: assignment.principalId,
22
+ accessRisk: permissionRiskLevel,
23
+ accessScope: assignment.scope,
24
+ accessScopeType: assignment.scopeType ?? "Unknown",
25
+ accessResourceId: getResourceScopeId(assignment),
26
+ accessResourceGroup: assignment.scopeResourceGroup ?? getScopeResourceGroup(assignment.scope),
27
+ accessSubscriptionId: assignment.scopeSubscriptionId ?? getScopeSubscriptionId(assignment.scope),
28
+ accessDisplayName: formatAccessDisplayName(assignment)
29
+ };
30
+ }
31
+
32
+ function formatAccessDisplayName(assignment: AzureRoleAssignment): string {
33
+ const role = assignment.roleDefinitionName ?? "Unknown role";
34
+ const scopeType = assignment.scopeType ?? "Unknown";
35
+ const scope = formatScope(assignment);
36
+
37
+ return `${role} on ${scopeType.toLowerCase()} ${scope}`;
38
+ }
39
+
40
+ function formatScope(assignment: AzureRoleAssignment): string {
41
+ if (assignment.scopeType === "ManagementGroup" && assignment.scopeManagementGroup) {
42
+ return assignment.scopeManagementGroup;
43
+ }
44
+
45
+ if (assignment.scopeType === "Subscription") {
46
+ return assignment.subscriptionName || assignment.scopeSubscriptionId || assignment.subscriptionId;
47
+ }
48
+
49
+ if (assignment.scopeType === "ResourceGroup" && assignment.scopeResourceGroup) {
50
+ return assignment.scopeResourceGroup;
51
+ }
52
+
53
+ if (assignment.scopeType === "Resource" && assignment.scopeResourceName) {
54
+ return assignment.scopeResourceName;
55
+ }
56
+
57
+ return assignment.scope;
58
+ }
59
+
60
+ function getResourceScopeId(assignment: AzureRoleAssignment): string | null {
61
+ return assignment.scopeType === "Resource" ? assignment.scope : null;
62
+ }
63
+
64
+ function getScopeSubscriptionId(scope: string): string | null {
65
+ return scope.match(/\/subscriptions\/([^/]+)/i)?.[1] ?? null;
66
+ }
67
+
68
+ function getScopeResourceGroup(scope: string): string | null {
69
+ return scope.match(/\/resourceGroups\/([^/]+)/i)?.[1] ?? null;
70
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,21 @@
1
+ import type { AzureManagedIdentityResourceAssignment } from "../identityEnrichment";
2
+ import type { OwnerConfidence } from "../../ownership/types";
3
+ import type {
4
+ AzureIdentityRuntimeEnrichment,
5
+ EntraPrincipalPermissionSummary,
6
+ EntraPrincipalRbacSummary
7
+ } from "./servicePrincipal";
8
+ import type { ZtaRemediationSummary } from "../ztaReport";
9
+ import type { EntraServicePrincipal } from "./types";
10
+
11
+ export type ManagedIdentity = EntraServicePrincipal & AzureIdentityRuntimeEnrichment & {
12
+ servicePrincipalType: "ManagedIdentity";
13
+ managedIdentityAssignments: AzureManagedIdentityResourceAssignment[];
14
+ assignedResourceGroups: string[];
15
+ potentialOwners?: string[];
16
+ ownerConfidence?: OwnerConfidence;
17
+ } & EntraPrincipalPermissionSummary & EntraPrincipalRbacSummary & ZtaRemediationSummary;
18
+
19
+ export function isManagedIdentity(servicePrincipal: EntraServicePrincipal): servicePrincipal is ManagedIdentity {
20
+ return servicePrincipal.servicePrincipalType === "ManagedIdentity";
21
+ }
@@ -0,0 +1,34 @@
1
+ import type { ManagedIdentityPermissionRiskLevel } from "../identityEnrichment";
2
+ import type { AzureRoleAssignment } from "../resources";
3
+ import type { ZtaRemediationSummary } from "../ztaReport";
4
+ import type { OwnerConfidence } from "../../ownership/types";
5
+ import type { PermissionRiskLevel } from "../../risk/types";
6
+ import type { EntraServicePrincipal, EntraServicePrincipalType } from "./types";
7
+
8
+ export type AzureIdentityRuntimeEnrichment = {
9
+ permissionRisk: ManagedIdentityPermissionRiskLevel;
10
+ azureRbac: string;
11
+ roleAssignments: AzureRoleAssignment[];
12
+ };
13
+
14
+ export type EntraPrincipalPermissionSummary = {
15
+ oauthPemrissionsCount: number;
16
+ appRolesPermissionCount: number;
17
+ entraPermissionRisk: PermissionRiskLevel;
18
+ };
19
+
20
+ export type EntraPrincipalRbacSummary = {
21
+ rbacRoleAssignmentCount: number;
22
+ rbacRoleLevel: PermissionRiskLevel;
23
+ rbacSubscriptionCount: number;
24
+ };
25
+
26
+ export type ServicePrincipal = EntraServicePrincipal & AzureIdentityRuntimeEnrichment & {
27
+ servicePrincipalType: Exclude<EntraServicePrincipalType, "ManagedIdentity">;
28
+ potentialOwners?: string[];
29
+ ownerConfidence?: OwnerConfidence;
30
+ } & EntraPrincipalPermissionSummary & EntraPrincipalRbacSummary & ZtaRemediationSummary;
31
+
32
+ export function isServicePrincipal(servicePrincipal: EntraServicePrincipal): servicePrincipal is ServicePrincipal {
33
+ return servicePrincipal.servicePrincipalType !== "ManagedIdentity";
34
+ }