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,523 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import type { DuckDBConnection } from "@duckdb/node-api";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
AZURE_ACCESS_RISK_RANK,
|
|
7
|
+
type AzureIdentityEnrichmentStatus,
|
|
8
|
+
type AzureManagedIdentityAssignmentEnrichment,
|
|
9
|
+
type AzureRoleAssignmentEnrichment,
|
|
10
|
+
type LatestAzureIdentityEnrichment,
|
|
11
|
+
type ManagedIdentityPermissionRiskAssignment,
|
|
12
|
+
type ManagedIdentityPermissionRiskLevel,
|
|
13
|
+
type ManagedIdentityPermissionRiskSummary
|
|
14
|
+
} from "../../../../core/azure/identityEnrichment";
|
|
15
|
+
import type { AzureResource, AzureRoleAssignment } from "../../../../core/azure/resources";
|
|
16
|
+
import { isBroadAzureScope } from "./azureScopeClassifier";
|
|
17
|
+
import { evaluateAzureRoleAssignmentRisk } from "./evaluateAzureRoleAssignmentRisk";
|
|
18
|
+
import { getResourceManagedIdentityAssignments } from "../../identities/buildAzureManagedIdentityAssignmentIndex";
|
|
19
|
+
import type { AzureManagedIdentityResourceAssignment } from "../../identities/azureIdentityTypes";
|
|
20
|
+
import type { EntraServicePrincipal } from "../../inputTransferObject/entra/EntraServicePrincipal";
|
|
21
|
+
import { readEntraServicePrincipalRows } from "../entra/servicePrincipalsTable";
|
|
22
|
+
import { readAzureResourceRows, readAzureRoleAssignmentRows } from "../resources/tables";
|
|
23
|
+
|
|
24
|
+
export type {
|
|
25
|
+
AzureIdentityEnrichmentStatus,
|
|
26
|
+
AzureManagedIdentityAssignmentEnrichment,
|
|
27
|
+
AzureRoleAssignmentEnrichment,
|
|
28
|
+
LatestAzureIdentityEnrichment
|
|
29
|
+
} from "../../../../core/azure/identityEnrichment";
|
|
30
|
+
|
|
31
|
+
const emptyStatus: AzureIdentityEnrichmentStatus = {
|
|
32
|
+
calculated: false,
|
|
33
|
+
latestRunId: null,
|
|
34
|
+
identityRoleAssignmentCount: 0,
|
|
35
|
+
accessRiskIdentityCount: 0,
|
|
36
|
+
managedIdentityAssignmentCount: 0,
|
|
37
|
+
calculatedAt: null
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export async function recalculateAzureIdentityEnrichment(
|
|
41
|
+
connection: DuckDBConnection
|
|
42
|
+
): Promise<AzureIdentityEnrichmentStatus> {
|
|
43
|
+
const runId = randomUUID();
|
|
44
|
+
const startedAt = new Date().toISOString();
|
|
45
|
+
|
|
46
|
+
await connection.run(
|
|
47
|
+
`insert into azure_runtime_enrichment_runs values (
|
|
48
|
+
$runId, $startedAt, null, 'running', 0, 0, 0, null
|
|
49
|
+
)`,
|
|
50
|
+
{ runId, startedAt }
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const servicePrincipals = await readEntraServicePrincipalRows(connection);
|
|
55
|
+
const roleAssignments = await readOptionalRows(connection, readAzureRoleAssignmentRows);
|
|
56
|
+
const resources = await readOptionalRows(connection, readAzureResourceRows);
|
|
57
|
+
const roleEnrichment = buildRoleAssignmentEnrichment(servicePrincipals, roleAssignments);
|
|
58
|
+
const accessRiskEnrichment = buildAccessRiskEnrichment(servicePrincipals, roleAssignments);
|
|
59
|
+
const managedIdentityEnrichment = buildManagedIdentityAssignmentEnrichment(servicePrincipals, resources);
|
|
60
|
+
const completedAt = new Date().toISOString();
|
|
61
|
+
|
|
62
|
+
await connection.run("begin transaction");
|
|
63
|
+
try {
|
|
64
|
+
await insertRoleAssignmentEnrichmentRows(connection, runId, roleEnrichment);
|
|
65
|
+
await insertAccessRiskEnrichmentRows(connection, runId, accessRiskEnrichment);
|
|
66
|
+
await insertManagedIdentityAssignmentEnrichmentRows(connection, runId, managedIdentityEnrichment);
|
|
67
|
+
await connection.run(
|
|
68
|
+
`update azure_runtime_enrichment_runs
|
|
69
|
+
set completed_at = $completedAt,
|
|
70
|
+
status = 'completed',
|
|
71
|
+
identity_role_assignment_count = $identityRoleAssignmentCount,
|
|
72
|
+
access_risk_identity_count = $accessRiskIdentityCount,
|
|
73
|
+
managed_identity_assignment_count = $managedIdentityAssignmentCount
|
|
74
|
+
where run_id = $runId`,
|
|
75
|
+
{
|
|
76
|
+
runId,
|
|
77
|
+
completedAt,
|
|
78
|
+
identityRoleAssignmentCount: sumRoleAssignments(roleEnrichment),
|
|
79
|
+
accessRiskIdentityCount: accessRiskEnrichment.length,
|
|
80
|
+
managedIdentityAssignmentCount: sumManagedIdentityAssignments(managedIdentityEnrichment)
|
|
81
|
+
}
|
|
82
|
+
);
|
|
83
|
+
await connection.run("commit");
|
|
84
|
+
} catch (error) {
|
|
85
|
+
await connection.run("rollback");
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
calculated: true,
|
|
91
|
+
latestRunId: runId,
|
|
92
|
+
identityRoleAssignmentCount: sumRoleAssignments(roleEnrichment),
|
|
93
|
+
accessRiskIdentityCount: accessRiskEnrichment.length,
|
|
94
|
+
managedIdentityAssignmentCount: sumManagedIdentityAssignments(managedIdentityEnrichment),
|
|
95
|
+
calculatedAt: completedAt
|
|
96
|
+
};
|
|
97
|
+
} catch (error) {
|
|
98
|
+
await connection.run(
|
|
99
|
+
`update azure_runtime_enrichment_runs
|
|
100
|
+
set completed_at = $completedAt, status = 'failed', error_message = $errorMessage
|
|
101
|
+
where run_id = $runId`,
|
|
102
|
+
{
|
|
103
|
+
runId,
|
|
104
|
+
completedAt: new Date().toISOString(),
|
|
105
|
+
errorMessage: error instanceof Error ? error.message : String(error)
|
|
106
|
+
}
|
|
107
|
+
);
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function readAzureIdentityEnrichmentStatus(
|
|
113
|
+
connection: DuckDBConnection
|
|
114
|
+
): Promise<AzureIdentityEnrichmentStatus> {
|
|
115
|
+
const rows = await readRows<AzureRuntimeEnrichmentRunRow>(
|
|
116
|
+
connection,
|
|
117
|
+
`select run_id, completed_at, identity_role_assignment_count, access_risk_identity_count,
|
|
118
|
+
managed_identity_assignment_count
|
|
119
|
+
from azure_runtime_enrichment_runs
|
|
120
|
+
where status = 'completed'
|
|
121
|
+
order by completed_at desc
|
|
122
|
+
limit 1`
|
|
123
|
+
);
|
|
124
|
+
const row = rows[0];
|
|
125
|
+
|
|
126
|
+
return row
|
|
127
|
+
? {
|
|
128
|
+
calculated: true,
|
|
129
|
+
latestRunId: row.run_id,
|
|
130
|
+
identityRoleAssignmentCount: row.identity_role_assignment_count,
|
|
131
|
+
accessRiskIdentityCount: row.access_risk_identity_count,
|
|
132
|
+
managedIdentityAssignmentCount: row.managed_identity_assignment_count,
|
|
133
|
+
calculatedAt: row.completed_at
|
|
134
|
+
}
|
|
135
|
+
: emptyStatus;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function readLatestAzureIdentityEnrichment(
|
|
139
|
+
connection: DuckDBConnection
|
|
140
|
+
): Promise<LatestAzureIdentityEnrichment> {
|
|
141
|
+
const status = await readAzureIdentityEnrichmentStatus(connection);
|
|
142
|
+
if (!status.latestRunId) {
|
|
143
|
+
return {
|
|
144
|
+
status,
|
|
145
|
+
roleAssignmentsByPrincipalId: new Map(),
|
|
146
|
+
accessRiskByPrincipalId: new Map(),
|
|
147
|
+
managedIdentityAssignmentsByServicePrincipalId: new Map()
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const roleRows = await readRows<AzureIdentityRoleAssignmentEnrichmentRow>(
|
|
152
|
+
connection,
|
|
153
|
+
`select principal_id, assignment_count, role_assignments
|
|
154
|
+
from azure_identity_role_assignment_enrichment
|
|
155
|
+
where run_id = '${status.latestRunId}'`
|
|
156
|
+
);
|
|
157
|
+
const riskRows = await readRows<AzureIdentityAccessRiskEnrichmentRow>(
|
|
158
|
+
connection,
|
|
159
|
+
`select principal_id, risk_level, assignment_count, high_risk_assignment_count,
|
|
160
|
+
broad_scope_assignment_count, role_assignments
|
|
161
|
+
from azure_identity_access_risk_enrichment
|
|
162
|
+
where run_id = '${status.latestRunId}'`
|
|
163
|
+
);
|
|
164
|
+
const managedIdentityRows = await readRows<AzureManagedIdentityAssignmentEnrichmentRow>(
|
|
165
|
+
connection,
|
|
166
|
+
`select service_principal_id, principal_id, client_id, assignment_count, assigned_resource_groups,
|
|
167
|
+
managed_identity_assignments
|
|
168
|
+
from azure_managed_identity_assignment_enrichment
|
|
169
|
+
where run_id = '${status.latestRunId}'`
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
status,
|
|
174
|
+
roleAssignmentsByPrincipalId: new Map(
|
|
175
|
+
roleRows.map((row) => [
|
|
176
|
+
normalizeKey(row.principal_id),
|
|
177
|
+
{
|
|
178
|
+
principalId: row.principal_id,
|
|
179
|
+
assignmentCount: row.assignment_count,
|
|
180
|
+
roleAssignments: parseJsonArray<AzureRoleAssignment>(row.role_assignments)
|
|
181
|
+
}
|
|
182
|
+
])
|
|
183
|
+
),
|
|
184
|
+
accessRiskByPrincipalId: new Map(
|
|
185
|
+
riskRows.map((row) => [
|
|
186
|
+
normalizeKey(row.principal_id),
|
|
187
|
+
{
|
|
188
|
+
principalId: row.principal_id,
|
|
189
|
+
riskLevel: row.risk_level,
|
|
190
|
+
assignmentCount: row.assignment_count,
|
|
191
|
+
highRiskAssignmentCount: row.high_risk_assignment_count,
|
|
192
|
+
broadScopeAssignmentCount: row.broad_scope_assignment_count,
|
|
193
|
+
roleAssignments: parseJsonArray<ManagedIdentityPermissionRiskAssignment>(row.role_assignments)
|
|
194
|
+
}
|
|
195
|
+
])
|
|
196
|
+
),
|
|
197
|
+
managedIdentityAssignmentsByServicePrincipalId: new Map(
|
|
198
|
+
managedIdentityRows.map((row) => [
|
|
199
|
+
normalizeKey(row.service_principal_id),
|
|
200
|
+
{
|
|
201
|
+
servicePrincipalId: row.service_principal_id,
|
|
202
|
+
principalId: row.principal_id,
|
|
203
|
+
clientId: row.client_id,
|
|
204
|
+
assignmentCount: row.assignment_count,
|
|
205
|
+
assignedResourceGroups: parseJsonArray<string>(row.assigned_resource_groups),
|
|
206
|
+
managedIdentityAssignments: parseJsonArray<AzureManagedIdentityResourceAssignment>(
|
|
207
|
+
row.managed_identity_assignments
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
])
|
|
211
|
+
)
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function buildRoleAssignmentEnrichment(
|
|
216
|
+
servicePrincipals: EntraServicePrincipal[],
|
|
217
|
+
roleAssignments: AzureRoleAssignment[]
|
|
218
|
+
): AzureRoleAssignmentEnrichment[] {
|
|
219
|
+
const identityIds = new Set(servicePrincipals.map((servicePrincipal) => normalizeKey(servicePrincipal.id)));
|
|
220
|
+
const assignmentsByPrincipalId = new Map<string, AzureRoleAssignment[]>();
|
|
221
|
+
|
|
222
|
+
for (const assignment of roleAssignments) {
|
|
223
|
+
const principalId = normalizeKey(assignment.principalId);
|
|
224
|
+
if (!identityIds.has(principalId)) {
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const assignments = assignmentsByPrincipalId.get(principalId) ?? [];
|
|
229
|
+
assignments.push(assignment);
|
|
230
|
+
assignmentsByPrincipalId.set(principalId, assignments);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return [...assignmentsByPrincipalId.entries()]
|
|
234
|
+
.map(([principalId, assignments]) => ({
|
|
235
|
+
principalId,
|
|
236
|
+
assignmentCount: assignments.length,
|
|
237
|
+
roleAssignments: assignments.sort(compareRoleAssignments)
|
|
238
|
+
}))
|
|
239
|
+
.sort((left, right) => left.principalId.localeCompare(right.principalId));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function buildAccessRiskEnrichment(
|
|
243
|
+
servicePrincipals: EntraServicePrincipal[],
|
|
244
|
+
roleAssignments: AzureRoleAssignment[]
|
|
245
|
+
): ManagedIdentityPermissionRiskSummary[] {
|
|
246
|
+
const summariesByPrincipalId = new Map<string, ManagedIdentityPermissionRiskSummary>();
|
|
247
|
+
|
|
248
|
+
for (const servicePrincipal of servicePrincipals) {
|
|
249
|
+
summariesByPrincipalId.set(normalizeKey(servicePrincipal.id), createRiskSummary(servicePrincipal.id));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
for (const assignment of roleAssignments) {
|
|
253
|
+
const normalizedPrincipalId = normalizeKey(assignment.principalId);
|
|
254
|
+
const summary = summariesByPrincipalId.get(normalizedPrincipalId);
|
|
255
|
+
if (!summary) {
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const riskAssignment = evaluateAzureRoleAssignmentRisk(assignment);
|
|
260
|
+
summary.roleAssignments.push(riskAssignment);
|
|
261
|
+
summary.assignmentCount += 1;
|
|
262
|
+
summary.riskLevel = maxRisk(summary.riskLevel, riskAssignment.riskLevel);
|
|
263
|
+
|
|
264
|
+
if (riskAssignment.riskLevel === "high") {
|
|
265
|
+
summary.highRiskAssignmentCount += 1;
|
|
266
|
+
}
|
|
267
|
+
if (isBroadAzureScope(assignment)) {
|
|
268
|
+
summary.broadScopeAssignmentCount += 1;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
for (const summary of summariesByPrincipalId.values()) {
|
|
273
|
+
summary.roleAssignments.sort(compareRiskAssignments);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return [...summariesByPrincipalId.values()].sort((left, right) => left.principalId.localeCompare(right.principalId));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function buildManagedIdentityAssignmentEnrichment(
|
|
280
|
+
servicePrincipals: EntraServicePrincipal[],
|
|
281
|
+
resources: AzureResource[]
|
|
282
|
+
): AzureManagedIdentityAssignmentEnrichment[] {
|
|
283
|
+
const assignmentsByKey = new Map<string, AzureManagedIdentityResourceAssignment[]>();
|
|
284
|
+
|
|
285
|
+
for (const resource of resources) {
|
|
286
|
+
for (const assignment of getResourceManagedIdentityAssignments(resource)) {
|
|
287
|
+
addManagedIdentityAssignment(assignmentsByKey, assignment.clientId, assignment);
|
|
288
|
+
addManagedIdentityAssignment(assignmentsByKey, assignment.principalId, assignment);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return servicePrincipals
|
|
293
|
+
.filter(isManagedIdentity)
|
|
294
|
+
.map((servicePrincipal) => {
|
|
295
|
+
const assignments = new Map<string, AzureManagedIdentityResourceAssignment>();
|
|
296
|
+
for (const key of [servicePrincipal.id, servicePrincipal.appId]) {
|
|
297
|
+
for (const assignment of assignmentsByKey.get(normalizeKey(key)) ?? []) {
|
|
298
|
+
assignments.set(`${assignment.assignedResourceId}:${assignment.resourceId}`, assignment);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const managedIdentityAssignments = [...assignments.values()].sort(compareManagedIdentityAssignments);
|
|
303
|
+
return {
|
|
304
|
+
servicePrincipalId: servicePrincipal.id,
|
|
305
|
+
principalId: servicePrincipal.id,
|
|
306
|
+
clientId: servicePrincipal.appId,
|
|
307
|
+
assignmentCount: managedIdentityAssignments.length,
|
|
308
|
+
assignedResourceGroups: uniqueSorted(managedIdentityAssignments.map((assignment) => assignment.assignedResourceGroup)),
|
|
309
|
+
managedIdentityAssignments
|
|
310
|
+
};
|
|
311
|
+
})
|
|
312
|
+
.sort((left, right) => left.servicePrincipalId.localeCompare(right.servicePrincipalId));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async function insertRoleAssignmentEnrichmentRows(
|
|
316
|
+
connection: DuckDBConnection,
|
|
317
|
+
runId: string,
|
|
318
|
+
rows: AzureRoleAssignmentEnrichment[]
|
|
319
|
+
): Promise<void> {
|
|
320
|
+
for (const row of rows) {
|
|
321
|
+
await connection.run(
|
|
322
|
+
`insert into azure_identity_role_assignment_enrichment values (
|
|
323
|
+
$runId, $principalId, $assignmentCount, $roleAssignments::json
|
|
324
|
+
)`,
|
|
325
|
+
{
|
|
326
|
+
runId,
|
|
327
|
+
principalId: row.principalId,
|
|
328
|
+
assignmentCount: row.assignmentCount,
|
|
329
|
+
roleAssignments: JSON.stringify(row.roleAssignments)
|
|
330
|
+
}
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async function insertAccessRiskEnrichmentRows(
|
|
336
|
+
connection: DuckDBConnection,
|
|
337
|
+
runId: string,
|
|
338
|
+
rows: ManagedIdentityPermissionRiskSummary[]
|
|
339
|
+
): Promise<void> {
|
|
340
|
+
for (const row of rows) {
|
|
341
|
+
await connection.run(
|
|
342
|
+
`insert into azure_identity_access_risk_enrichment values (
|
|
343
|
+
$runId, $principalId, $riskLevel, $assignmentCount, $highRiskAssignmentCount,
|
|
344
|
+
$broadScopeAssignmentCount, $roleAssignments::json
|
|
345
|
+
)`,
|
|
346
|
+
{
|
|
347
|
+
runId,
|
|
348
|
+
principalId: row.principalId,
|
|
349
|
+
riskLevel: row.riskLevel,
|
|
350
|
+
assignmentCount: row.assignmentCount,
|
|
351
|
+
highRiskAssignmentCount: row.highRiskAssignmentCount,
|
|
352
|
+
broadScopeAssignmentCount: row.broadScopeAssignmentCount,
|
|
353
|
+
roleAssignments: JSON.stringify(row.roleAssignments)
|
|
354
|
+
}
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async function insertManagedIdentityAssignmentEnrichmentRows(
|
|
360
|
+
connection: DuckDBConnection,
|
|
361
|
+
runId: string,
|
|
362
|
+
rows: AzureManagedIdentityAssignmentEnrichment[]
|
|
363
|
+
): Promise<void> {
|
|
364
|
+
for (const row of rows) {
|
|
365
|
+
await connection.run(
|
|
366
|
+
`insert into azure_managed_identity_assignment_enrichment values (
|
|
367
|
+
$runId, $servicePrincipalId, $principalId, $clientId, $assignmentCount,
|
|
368
|
+
$assignedResourceGroups::json, $managedIdentityAssignments::json
|
|
369
|
+
)`,
|
|
370
|
+
{
|
|
371
|
+
runId,
|
|
372
|
+
servicePrincipalId: row.servicePrincipalId,
|
|
373
|
+
principalId: row.principalId,
|
|
374
|
+
clientId: row.clientId,
|
|
375
|
+
assignmentCount: row.assignmentCount,
|
|
376
|
+
assignedResourceGroups: JSON.stringify(row.assignedResourceGroups),
|
|
377
|
+
managedIdentityAssignments: JSON.stringify(row.managedIdentityAssignments)
|
|
378
|
+
}
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async function readOptionalRows<T>(
|
|
384
|
+
connection: DuckDBConnection,
|
|
385
|
+
read: (connection: DuckDBConnection) => Promise<T[]>
|
|
386
|
+
): Promise<T[]> {
|
|
387
|
+
try {
|
|
388
|
+
return await read(connection);
|
|
389
|
+
} catch (error) {
|
|
390
|
+
if (error instanceof Error && /does not exist|Catalog Error/i.test(error.message)) {
|
|
391
|
+
return [];
|
|
392
|
+
}
|
|
393
|
+
throw error;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function addManagedIdentityAssignment(
|
|
398
|
+
assignmentsByKey: Map<string, AzureManagedIdentityResourceAssignment[]>,
|
|
399
|
+
key: string | null,
|
|
400
|
+
assignment: AzureManagedIdentityResourceAssignment
|
|
401
|
+
): void {
|
|
402
|
+
if (!key) {
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const normalizedKey = normalizeKey(key);
|
|
407
|
+
const assignments = assignmentsByKey.get(normalizedKey) ?? [];
|
|
408
|
+
assignments.push(assignment);
|
|
409
|
+
assignmentsByKey.set(normalizedKey, assignments);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function isManagedIdentity(servicePrincipal: EntraServicePrincipal): boolean {
|
|
413
|
+
return servicePrincipal.servicePrincipalType === "ManagedIdentity";
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function createRiskSummary(principalId: string): ManagedIdentityPermissionRiskSummary {
|
|
417
|
+
return {
|
|
418
|
+
principalId,
|
|
419
|
+
riskLevel: "none",
|
|
420
|
+
assignmentCount: 0,
|
|
421
|
+
highRiskAssignmentCount: 0,
|
|
422
|
+
broadScopeAssignmentCount: 0,
|
|
423
|
+
roleAssignments: []
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function maxRisk(
|
|
428
|
+
left: ManagedIdentityPermissionRiskLevel,
|
|
429
|
+
right: ManagedIdentityPermissionRiskLevel
|
|
430
|
+
): ManagedIdentityPermissionRiskLevel {
|
|
431
|
+
return AZURE_ACCESS_RISK_RANK[left] >= AZURE_ACCESS_RISK_RANK[right] ? left : right;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function compareRiskAssignments(
|
|
435
|
+
left: ManagedIdentityPermissionRiskAssignment,
|
|
436
|
+
right: ManagedIdentityPermissionRiskAssignment
|
|
437
|
+
): number {
|
|
438
|
+
return (
|
|
439
|
+
AZURE_ACCESS_RISK_RANK[right.riskLevel] - AZURE_ACCESS_RISK_RANK[left.riskLevel] ||
|
|
440
|
+
left.subscriptionName.localeCompare(right.subscriptionName, undefined, { sensitivity: "base" }) ||
|
|
441
|
+
(left.roleDefinitionName ?? "").localeCompare(right.roleDefinitionName ?? "", undefined, { sensitivity: "base" }) ||
|
|
442
|
+
left.scope.localeCompare(right.scope, undefined, { sensitivity: "base" })
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function compareRoleAssignments(left: AzureRoleAssignment, right: AzureRoleAssignment): number {
|
|
447
|
+
return (
|
|
448
|
+
left.subscriptionName.localeCompare(right.subscriptionName, undefined, { sensitivity: "base" }) ||
|
|
449
|
+
(left.roleDefinitionName ?? "").localeCompare(right.roleDefinitionName ?? "", undefined, { sensitivity: "base" }) ||
|
|
450
|
+
left.scope.localeCompare(right.scope, undefined, { sensitivity: "base" })
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function compareManagedIdentityAssignments(
|
|
455
|
+
left: AzureManagedIdentityResourceAssignment,
|
|
456
|
+
right: AzureManagedIdentityResourceAssignment
|
|
457
|
+
): number {
|
|
458
|
+
return (
|
|
459
|
+
left.subscriptionName.localeCompare(right.subscriptionName, undefined, { sensitivity: "base" }) ||
|
|
460
|
+
left.assignedResourceGroup.localeCompare(right.assignedResourceGroup, undefined, { sensitivity: "base" }) ||
|
|
461
|
+
left.assignedResourceName.localeCompare(right.assignedResourceName, undefined, { sensitivity: "base" })
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function sumRoleAssignments(rows: AzureRoleAssignmentEnrichment[]): number {
|
|
466
|
+
return rows.reduce((count, row) => count + row.assignmentCount, 0);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function sumManagedIdentityAssignments(rows: AzureManagedIdentityAssignmentEnrichment[]): number {
|
|
470
|
+
return rows.reduce((count, row) => count + row.assignmentCount, 0);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function uniqueSorted(values: string[]): string[] {
|
|
474
|
+
return [...new Set(values)].sort((left, right) => left.localeCompare(right, undefined, { sensitivity: "base" }));
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function normalizeKey(value: string): string {
|
|
478
|
+
return value.toLowerCase();
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
async function readRows<Row extends Record<string, unknown>>(
|
|
482
|
+
connection: DuckDBConnection,
|
|
483
|
+
sql: string
|
|
484
|
+
): Promise<Row[]> {
|
|
485
|
+
const reader = await connection.runAndReadAll(sql);
|
|
486
|
+
return reader.getRowObjectsJson() as Row[];
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function parseJsonArray<T>(value: string | null | undefined): T[] {
|
|
490
|
+
return value ? JSON.parse(value) : [];
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
type AzureRuntimeEnrichmentRunRow = {
|
|
494
|
+
run_id: string;
|
|
495
|
+
completed_at: string;
|
|
496
|
+
identity_role_assignment_count: number;
|
|
497
|
+
access_risk_identity_count: number;
|
|
498
|
+
managed_identity_assignment_count: number;
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
type AzureIdentityRoleAssignmentEnrichmentRow = {
|
|
502
|
+
principal_id: string;
|
|
503
|
+
assignment_count: number;
|
|
504
|
+
role_assignments: string;
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
type AzureIdentityAccessRiskEnrichmentRow = {
|
|
508
|
+
principal_id: string;
|
|
509
|
+
risk_level: ManagedIdentityPermissionRiskLevel;
|
|
510
|
+
assignment_count: number;
|
|
511
|
+
high_risk_assignment_count: number;
|
|
512
|
+
broad_scope_assignment_count: number;
|
|
513
|
+
role_assignments: string;
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
type AzureManagedIdentityAssignmentEnrichmentRow = {
|
|
517
|
+
service_principal_id: string;
|
|
518
|
+
principal_id: string;
|
|
519
|
+
client_id: string;
|
|
520
|
+
assignment_count: number;
|
|
521
|
+
assigned_resource_groups: string;
|
|
522
|
+
managed_identity_assignments: string;
|
|
523
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { AzureRoleAssignment } from "../../../../core/azure/resources";
|
|
2
|
+
|
|
3
|
+
export function isBroadAzureScope(assignment: AzureRoleAssignment): boolean {
|
|
4
|
+
const scopeType = classifyAzureScope(assignment);
|
|
5
|
+
return scopeType === "ManagementGroup" || scopeType === "Subscription";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function classifyAzureScope(assignment: AzureRoleAssignment): NonNullable<AzureRoleAssignment["scopeType"]> {
|
|
9
|
+
if (assignment.scopeType) {
|
|
10
|
+
return assignment.scopeType;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (/^\/providers\/Microsoft\.Management\/managementGroups\/[^/]+$/i.test(assignment.scope)) {
|
|
14
|
+
return "ManagementGroup";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (/^\/subscriptions\/[^/]+$/i.test(assignment.scope)) {
|
|
18
|
+
return "Subscription";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (/^\/subscriptions\/[^/]+\/resourceGroups\/[^/]+$/i.test(assignment.scope)) {
|
|
22
|
+
return "ResourceGroup";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (/^\/subscriptions\/[^/]+\/resourceGroups\/[^/]+\/providers\/.+/i.test(assignment.scope)) {
|
|
26
|
+
return "Resource";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return "Unknown";
|
|
30
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { AzureRoleAssignment } from "../../../../core/azure/resources";
|
|
2
|
+
import type {
|
|
3
|
+
ManagedIdentityPermissionRiskAssignment,
|
|
4
|
+
ManagedIdentityPermissionRiskLevel
|
|
5
|
+
} from "../../../../core/azure/identityEnrichment";
|
|
6
|
+
import { classifyAzureScope, isBroadAzureScope } from "./azureScopeClassifier.ts";
|
|
7
|
+
|
|
8
|
+
const HIGH_RISK_ROLES = new Set([
|
|
9
|
+
"owner",
|
|
10
|
+
"user access administrator",
|
|
11
|
+
"role based access control administrator",
|
|
12
|
+
"privileged role administrator",
|
|
13
|
+
"key vault administrator"
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
const MEDIUM_RISK_ROLE_PATTERNS = [
|
|
17
|
+
/(^|\s)contributor$/,
|
|
18
|
+
/(^|\s)administrator$/,
|
|
19
|
+
/(^|\s)data owner$/,
|
|
20
|
+
/(^|\s)data contributor$/,
|
|
21
|
+
/(^|\s)operator$/
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
export function evaluateAzureRoleAssignmentRisk(
|
|
25
|
+
assignment: AzureRoleAssignment
|
|
26
|
+
): ManagedIdentityPermissionRiskAssignment {
|
|
27
|
+
const reasons: string[] = [];
|
|
28
|
+
const roleLevel = getAzureRoleRiskLevel(assignment.roleDefinitionName);
|
|
29
|
+
const broadScope = isBroadAzureScope(assignment);
|
|
30
|
+
const resourceScope = classifyAzureScope(assignment) === "Resource";
|
|
31
|
+
let riskLevel = roleLevel;
|
|
32
|
+
|
|
33
|
+
if (broadScope && roleLevel !== "none") {
|
|
34
|
+
reasons.push("broad scope");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (isHighRiskAzureRole(assignment.roleDefinitionName)) {
|
|
38
|
+
reasons.push("privileged role");
|
|
39
|
+
} else if (roleLevel === "medium") {
|
|
40
|
+
reasons.push("write-capable role");
|
|
41
|
+
} else if (roleLevel === "low") {
|
|
42
|
+
reasons.push("read-only role");
|
|
43
|
+
} else if (assignment.roleDefinitionName) {
|
|
44
|
+
reasons.push("custom or unclassified role");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (broadScope && roleLevel === "medium") {
|
|
48
|
+
riskLevel = "high";
|
|
49
|
+
} else if (resourceScope && roleLevel === "high") {
|
|
50
|
+
riskLevel = "medium";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
...assignment,
|
|
55
|
+
riskLevel,
|
|
56
|
+
reasons
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function getAzureRoleRiskLevel(roleDefinitionName: string | null): ManagedIdentityPermissionRiskLevel {
|
|
61
|
+
const normalizedRole = normalizeAzureRoleName(roleDefinitionName);
|
|
62
|
+
|
|
63
|
+
if (!normalizedRole) {
|
|
64
|
+
return "medium";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (isHighRiskAzureRole(normalizedRole)) {
|
|
68
|
+
return "high";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (normalizedRole === "reader") {
|
|
72
|
+
return "low";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (MEDIUM_RISK_ROLE_PATTERNS.some((pattern) => pattern.test(normalizedRole))) {
|
|
76
|
+
return "medium";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return "medium";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function isHighRiskAzureRole(roleDefinitionName: string | null): boolean {
|
|
83
|
+
return HIGH_RISK_ROLES.has(normalizeAzureRoleName(roleDefinitionName));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function normalizeAzureRoleName(roleDefinitionName: string | null): string {
|
|
87
|
+
return roleDefinitionName?.trim().toLowerCase() ?? "";
|
|
88
|
+
}
|