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,307 @@
|
|
|
1
|
+
import { RuntimeHttpError } from "../../../../core/runtime/localSnapshotFiles";
|
|
2
|
+
import type { ManagedIdentity } from "../../../../core/azure/entra/managedIdentity";
|
|
3
|
+
import type { ServicePrincipal } from "../../../../core/azure/entra/servicePrincipal";
|
|
4
|
+
import type {
|
|
5
|
+
AzureRoleAssignment,
|
|
6
|
+
AzureUserAssignedManagedIdentity,
|
|
7
|
+
ResourceGroupOwnershipRow
|
|
8
|
+
} from "../../../../core/azure/resources";
|
|
9
|
+
import type { OwnerConfidence } from "../../../../core/ownership/types";
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
buildPaginatedCollection,
|
|
13
|
+
type LocalReportCollectionQueryOptions,
|
|
14
|
+
type LocalReportPaginatedCollection
|
|
15
|
+
} from "../localReportCollections";
|
|
16
|
+
import type { AzureResourcesCollectionQueryService } from "../resources/AzureResourcesCollectionQueryService";
|
|
17
|
+
import type { LocalAzureResourcesReportRuntime } from "../resources/LocalAzureResourcesReportRuntime";
|
|
18
|
+
import type { ZeroTrustAssessmentQueryService } from "../zta/ZeroTrustAssessmentQueryService";
|
|
19
|
+
import type { LocalEntraReportRuntime } from "./LocalEntraReportRuntime";
|
|
20
|
+
|
|
21
|
+
export type EntraCollectionQueryServiceOptions = {
|
|
22
|
+
entra: LocalEntraReportRuntime;
|
|
23
|
+
azureResources: LocalAzureResourcesReportRuntime;
|
|
24
|
+
azureResourcesQueries: AzureResourcesCollectionQueryService;
|
|
25
|
+
zeroTrustAssessmentQueries: ZeroTrustAssessmentQueryService;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export class EntraCollectionQueryService {
|
|
29
|
+
private readonly entra: LocalEntraReportRuntime;
|
|
30
|
+
private readonly azureResources: LocalAzureResourcesReportRuntime;
|
|
31
|
+
private readonly azureResourcesQueries: AzureResourcesCollectionQueryService;
|
|
32
|
+
private readonly zeroTrustAssessmentQueries: ZeroTrustAssessmentQueryService;
|
|
33
|
+
|
|
34
|
+
constructor(options: EntraCollectionQueryServiceOptions) {
|
|
35
|
+
this.entra = options.entra;
|
|
36
|
+
this.azureResources = options.azureResources;
|
|
37
|
+
this.azureResourcesQueries = options.azureResourcesQueries;
|
|
38
|
+
this.zeroTrustAssessmentQueries = options.zeroTrustAssessmentQueries;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async queryServicePrincipals(
|
|
42
|
+
options: LocalReportCollectionQueryOptions
|
|
43
|
+
): Promise<LocalReportPaginatedCollection<"entra.servicePrincipals">> {
|
|
44
|
+
return buildPaginatedCollection("entra.servicePrincipals", await this.readServicePrincipalRows(), options);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async queryManagedIdentities(
|
|
48
|
+
options: LocalReportCollectionQueryOptions
|
|
49
|
+
): Promise<LocalReportPaginatedCollection<"entra.managedIdentities">> {
|
|
50
|
+
return buildPaginatedCollection("entra.managedIdentities", await this.readManagedIdentityRows(), options);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async queryOAuth2PermissionGrants(
|
|
54
|
+
options: LocalReportCollectionQueryOptions
|
|
55
|
+
): Promise<LocalReportPaginatedCollection<"entra.oauth2PermissionGrants">> {
|
|
56
|
+
return buildPaginatedCollection(
|
|
57
|
+
"entra.oauth2PermissionGrants",
|
|
58
|
+
(await this.entra.readEntraOAuth2PermissionGrants()) as unknown as Record<string, unknown>[],
|
|
59
|
+
options
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async queryAppRoleAssignments(
|
|
64
|
+
options: LocalReportCollectionQueryOptions
|
|
65
|
+
): Promise<LocalReportPaginatedCollection<"entra.appRoleAssignments">> {
|
|
66
|
+
return buildPaginatedCollection(
|
|
67
|
+
"entra.appRoleAssignments",
|
|
68
|
+
(await this.entra.readEntraAppRoleAssignments()) as unknown as Record<string, unknown>[],
|
|
69
|
+
options
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private async readManagedIdentityRows(): Promise<Record<string, unknown>[]> {
|
|
74
|
+
const managedIdentities = await this.enrichWithZtaRemediationSummaries(await this.entra.readManagedIdentities());
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const [resourceGroupOwnershipRows, userAssignedManagedIdentities] = await Promise.all([
|
|
78
|
+
this.azureResourcesQueries.readResourceGroupOwnershipRows(),
|
|
79
|
+
this.azureResources.readAzureUserAssignedManagedIdentities()
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
return enrichManagedIdentitiesWithResourceGroupOwners(
|
|
83
|
+
managedIdentities,
|
|
84
|
+
resourceGroupOwnershipRows,
|
|
85
|
+
userAssignedManagedIdentities
|
|
86
|
+
) as unknown as Record<string, unknown>[];
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (error instanceof RuntimeHttpError && error.statusCode === 404) {
|
|
89
|
+
return managedIdentities as unknown as Record<string, unknown>[];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private async readServicePrincipalRows(): Promise<Record<string, unknown>[]> {
|
|
97
|
+
const servicePrincipals = await this.enrichWithZtaRemediationSummaries(await this.entra.readServicePrincipals());
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
return enrichServicePrincipalsWithResourceGroupOwners(
|
|
101
|
+
servicePrincipals,
|
|
102
|
+
await this.azureResourcesQueries.readResourceGroupOwnershipRows()
|
|
103
|
+
) as unknown as Record<string, unknown>[];
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if (error instanceof RuntimeHttpError && error.statusCode === 404) {
|
|
106
|
+
return servicePrincipals as unknown as Record<string, unknown>[];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private async enrichWithZtaRemediationSummaries<Row extends ServicePrincipal | ManagedIdentity>(rows: Row[]): Promise<Row[]> {
|
|
114
|
+
const summariesByPrincipalId = await this.zeroTrustAssessmentQueries.readRemediationSummaries();
|
|
115
|
+
|
|
116
|
+
return rows.map((row) => ({
|
|
117
|
+
...row,
|
|
118
|
+
...(summariesByPrincipalId.get(row.id.toLowerCase()) ?? {})
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
type ManagedIdentityOwnerProjection = {
|
|
124
|
+
potentialOwners: string[];
|
|
125
|
+
ownerConfidence: OwnerConfidence;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
type ServicePrincipalOwnerProjection = {
|
|
129
|
+
potentialOwners: string[];
|
|
130
|
+
ownerConfidence: OwnerConfidence;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
type ResourceGroupOwnershipIndex = {
|
|
134
|
+
byResourceGroup: Map<string, ResourceGroupOwnershipRow>;
|
|
135
|
+
bySubscription: Map<string, ResourceGroupOwnershipRow[]>;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
function enrichManagedIdentitiesWithResourceGroupOwners(
|
|
139
|
+
managedIdentities: ManagedIdentity[],
|
|
140
|
+
resourceGroupOwnershipRows: ResourceGroupOwnershipRow[],
|
|
141
|
+
userAssignedManagedIdentities: AzureUserAssignedManagedIdentity[]
|
|
142
|
+
): ManagedIdentity[] {
|
|
143
|
+
const ownershipByResourceGroup = buildResourceGroupOwnershipIndex(resourceGroupOwnershipRows).byResourceGroup;
|
|
144
|
+
const locationsByPrincipal = buildManagedIdentityLocationIndex(userAssignedManagedIdentities);
|
|
145
|
+
|
|
146
|
+
return managedIdentities.map((identity) => {
|
|
147
|
+
const userAssignedIdentity =
|
|
148
|
+
locationsByPrincipal.get(identity.id.toLowerCase()) ?? locationsByPrincipal.get(identity.appId.toLowerCase());
|
|
149
|
+
const projection = projectManagedIdentityOwner(userAssignedIdentity, ownershipByResourceGroup);
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
...identity,
|
|
153
|
+
...projection
|
|
154
|
+
};
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function enrichServicePrincipalsWithResourceGroupOwners(
|
|
159
|
+
servicePrincipals: ServicePrincipal[],
|
|
160
|
+
resourceGroupOwnershipRows: ResourceGroupOwnershipRow[]
|
|
161
|
+
): ServicePrincipal[] {
|
|
162
|
+
const ownershipIndex = buildResourceGroupOwnershipIndex(resourceGroupOwnershipRows);
|
|
163
|
+
|
|
164
|
+
return servicePrincipals.map((servicePrincipal) => ({
|
|
165
|
+
...servicePrincipal,
|
|
166
|
+
...projectServicePrincipalOwners(servicePrincipal.roleAssignments, ownershipIndex)
|
|
167
|
+
}));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function buildResourceGroupOwnershipIndex(rows: ResourceGroupOwnershipRow[]): ResourceGroupOwnershipIndex {
|
|
171
|
+
const byResourceGroup = new Map<string, ResourceGroupOwnershipRow>();
|
|
172
|
+
const bySubscription = new Map<string, ResourceGroupOwnershipRow[]>();
|
|
173
|
+
|
|
174
|
+
for (const row of rows) {
|
|
175
|
+
byResourceGroup.set(getResourceGroupKey(row.subscriptionId, row.resourceGroup), row);
|
|
176
|
+
|
|
177
|
+
const subscriptionKey = row.subscriptionId.toLowerCase();
|
|
178
|
+
const subscriptionRows = bySubscription.get(subscriptionKey) ?? [];
|
|
179
|
+
subscriptionRows.push(row);
|
|
180
|
+
bySubscription.set(subscriptionKey, subscriptionRows);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return { byResourceGroup, bySubscription };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function buildManagedIdentityLocationIndex(
|
|
187
|
+
userAssignedManagedIdentities: AzureUserAssignedManagedIdentity[]
|
|
188
|
+
): Map<string, AzureUserAssignedManagedIdentity> {
|
|
189
|
+
const index = new Map<string, AzureUserAssignedManagedIdentity>();
|
|
190
|
+
|
|
191
|
+
for (const identity of userAssignedManagedIdentities) {
|
|
192
|
+
addManagedIdentityLocation(index, identity.principalId, identity);
|
|
193
|
+
addManagedIdentityLocation(index, identity.clientId, identity);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return index;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function addManagedIdentityLocation(
|
|
200
|
+
index: Map<string, AzureUserAssignedManagedIdentity>,
|
|
201
|
+
key: string,
|
|
202
|
+
identity: AzureUserAssignedManagedIdentity
|
|
203
|
+
): void {
|
|
204
|
+
const normalizedKey = key.trim().toLowerCase();
|
|
205
|
+
if (!normalizedKey) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
index.set(normalizedKey, identity);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function projectManagedIdentityOwner(
|
|
213
|
+
identity: AzureUserAssignedManagedIdentity | undefined,
|
|
214
|
+
ownershipByResourceGroup: Map<string, ResourceGroupOwnershipRow>
|
|
215
|
+
): ManagedIdentityOwnerProjection {
|
|
216
|
+
if (!identity) {
|
|
217
|
+
return {
|
|
218
|
+
potentialOwners: [],
|
|
219
|
+
ownerConfidence: "none"
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const ownership = ownershipByResourceGroup.get(getResourceGroupKey(identity.subscriptionId, identity.resourceGroup));
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
potentialOwners: ownership?.owner ? [ownership.owner] : [],
|
|
227
|
+
ownerConfidence: ownership?.confidence ?? "none"
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function projectServicePrincipalOwners(
|
|
232
|
+
roleAssignments: AzureRoleAssignment[],
|
|
233
|
+
ownershipIndex: ResourceGroupOwnershipIndex
|
|
234
|
+
): ServicePrincipalOwnerProjection {
|
|
235
|
+
const resourceGroups = new Map<string, ResourceGroupOwnershipRow>();
|
|
236
|
+
|
|
237
|
+
for (const assignment of roleAssignments) {
|
|
238
|
+
for (const row of getRoleAssignmentResourceGroupOwners(assignment, ownershipIndex)) {
|
|
239
|
+
resourceGroups.set(getResourceGroupKey(row.subscriptionId, row.resourceGroup), row);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const ownerRows = [...resourceGroups.values()].filter((row) => row.owner);
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
potentialOwners: uniqueSorted(ownerRows.map((row) => row.owner).filter(isString)),
|
|
247
|
+
ownerConfidence: ownerRows.reduce<OwnerConfidence>(
|
|
248
|
+
(confidence, row) => maxOwnerConfidence(confidence, row.confidence),
|
|
249
|
+
"none"
|
|
250
|
+
)
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function getRoleAssignmentResourceGroupOwners(
|
|
255
|
+
assignment: AzureRoleAssignment,
|
|
256
|
+
ownershipIndex: ResourceGroupOwnershipIndex
|
|
257
|
+
): ResourceGroupOwnershipRow[] {
|
|
258
|
+
const scope = assignment.scope;
|
|
259
|
+
const subscriptionId = getScopeSubscriptionId(scope) ?? assignment.subscriptionId;
|
|
260
|
+
const resourceGroup = getScopeResourceGroup(scope);
|
|
261
|
+
|
|
262
|
+
if (subscriptionId && resourceGroup) {
|
|
263
|
+
const row = ownershipIndex.byResourceGroup.get(getResourceGroupKey(subscriptionId, resourceGroup));
|
|
264
|
+
return row ? [row] : [];
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (isSubscriptionScope(scope) && subscriptionId) {
|
|
268
|
+
return ownershipIndex.bySubscription.get(subscriptionId.toLowerCase()) ?? [];
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return [];
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function getScopeSubscriptionId(scope: string): string | null {
|
|
275
|
+
return scope.match(/\/subscriptions\/([^/]+)/i)?.[1] ?? null;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function getScopeResourceGroup(scope: string): string | null {
|
|
279
|
+
return scope.match(/\/resourceGroups\/([^/]+)/i)?.[1] ?? null;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function isSubscriptionScope(scope: string): boolean {
|
|
283
|
+
return /^\/subscriptions\/[^/]+\/?$/i.test(scope);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function getResourceGroupKey(subscriptionId: string, resourceGroup: string): string {
|
|
287
|
+
return `${subscriptionId.toLowerCase()}:${resourceGroup.toLowerCase()}`;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function uniqueSorted(values: string[]): string[] {
|
|
291
|
+
return [...new Set(values)].sort((left, right) => left.localeCompare(right, undefined, { sensitivity: "base" }));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function isString(value: string | null): value is string {
|
|
295
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function maxOwnerConfidence(left: OwnerConfidence, right: OwnerConfidence): OwnerConfidence {
|
|
299
|
+
return OWNER_CONFIDENCE_RANK[left] >= OWNER_CONFIDENCE_RANK[right] ? left : right;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const OWNER_CONFIDENCE_RANK: Record<OwnerConfidence, number> = {
|
|
303
|
+
none: 0,
|
|
304
|
+
low: 1,
|
|
305
|
+
medium: 2,
|
|
306
|
+
high: 3
|
|
307
|
+
};
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import type { DuckDBConnection } from "@duckdb/node-api";
|
|
5
|
+
|
|
6
|
+
import { pathExists, RuntimeHttpError, type LocalSnapshotData } from "../../../../core/runtime/localSnapshotFiles";
|
|
7
|
+
import type { ManagedIdentity } from "../../../../core/azure/entra/managedIdentity";
|
|
8
|
+
import type { EntraPrincipalPermissionSummary, ServicePrincipal } from "../../../../core/azure/entra/servicePrincipal";
|
|
9
|
+
import type { EntraOAuth2PermissionGrant } from "../../../../core/azure/entra/types";
|
|
10
|
+
import type { PermissionRiskLevel } from "../../../../core/risk/types";
|
|
11
|
+
import type { EntraAppRoleAssignment } from "../../inputTransferObject/entra/EntraAppRoleAssignment";
|
|
12
|
+
import type { EntraOAuth2PermissionGrant as InputEntraOAuth2PermissionGrant } from "../../inputTransferObject/entra/EntraOAuth2PermissionGrant";
|
|
13
|
+
import type { EntraServicePrincipal } from "../../inputTransferObject/entra/EntraServicePrincipal";
|
|
14
|
+
import type { EntraSnapshot } from "../../inputTransferObject/entra/EntraSnapshot";
|
|
15
|
+
import { readEntraAppRoleAssignmentRows } from "./appRoleAssignmentsTable";
|
|
16
|
+
import { readEntraOAuth2PermissionGrantRows } from "./oauth2PermissionGrantsTable";
|
|
17
|
+
import { readLatestAzureIdentityEnrichment } from "../enrichment/azureIdentityEnrichment";
|
|
18
|
+
import { readEntraServicePrincipalRows } from "./servicePrincipalsTable";
|
|
19
|
+
import {
|
|
20
|
+
createEmptyEntraImportStatus,
|
|
21
|
+
entraSnapshotFileName,
|
|
22
|
+
importEntraSnapshotToDuckDb,
|
|
23
|
+
readEntraSnapshotFromDuckDb,
|
|
24
|
+
type EntraDuckDbImportStatus
|
|
25
|
+
} from "./snapshotStore";
|
|
26
|
+
import { mapEntraServicePrincipalsToCore } from "./entraServicePrincipalMapper";
|
|
27
|
+
import { toManagedIdentities, toServicePrincipals } from "./principalProjection";
|
|
28
|
+
|
|
29
|
+
export type LocalEntraReportCollectionId =
|
|
30
|
+
| "entra.servicePrincipals"
|
|
31
|
+
| "entra.managedIdentities"
|
|
32
|
+
| "entra.oauth2PermissionGrants"
|
|
33
|
+
| "entra.appRoleAssignments";
|
|
34
|
+
|
|
35
|
+
export type EntraPrincipalPermissions = {
|
|
36
|
+
principalId: string;
|
|
37
|
+
oauth2PermissionGrants: EntraOAuth2PermissionGrant[];
|
|
38
|
+
appRoleAssignments: EntraAppRoleAssignment[];
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type LocalEntraReportRuntimeOptions = {
|
|
42
|
+
dataDir: string;
|
|
43
|
+
getConnection: () => DuckDBConnection;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export class LocalEntraReportRuntime {
|
|
47
|
+
private readonly dataDir: string;
|
|
48
|
+
private readonly getConnection: () => DuckDBConnection;
|
|
49
|
+
private status = createEmptyEntraImportStatus();
|
|
50
|
+
|
|
51
|
+
constructor(options: LocalEntraReportRuntimeOptions) {
|
|
52
|
+
this.dataDir = options.dataDir;
|
|
53
|
+
this.getConnection = options.getConnection;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
getStatus(): EntraDuckDbImportStatus {
|
|
57
|
+
return this.status;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
canReadSnapshot(name: string): boolean {
|
|
61
|
+
return name === entraSnapshotFileName;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async importSnapshot(): Promise<void> {
|
|
65
|
+
const entraSnapshotPath = path.join(this.dataDir, entraSnapshotFileName);
|
|
66
|
+
if (!(await pathExists(entraSnapshotPath))) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const snapshot = JSON.parse(await readFile(entraSnapshotPath, "utf8")) as EntraSnapshot & LocalSnapshotData;
|
|
71
|
+
this.status = await importEntraSnapshotToDuckDb(this.getConnection(), snapshot);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async readSnapshot(): Promise<EntraSnapshot & LocalSnapshotData> {
|
|
75
|
+
this.assertImported();
|
|
76
|
+
return readEntraSnapshotFromDuckDb(this.getConnection());
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async readEntraServicePrincipals(): Promise<EntraServicePrincipal[]> {
|
|
80
|
+
this.assertImported();
|
|
81
|
+
return readEntraServicePrincipalRows(this.getConnection());
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async readServicePrincipals(): Promise<ServicePrincipal[]> {
|
|
85
|
+
this.assertImported();
|
|
86
|
+
const connection = this.getConnection();
|
|
87
|
+
const permissionsByPrincipalId = await this.readPrincipalPermissionSummary(connection);
|
|
88
|
+
|
|
89
|
+
return toServicePrincipals(
|
|
90
|
+
mapEntraServicePrincipalsToCore(await readEntraServicePrincipalRows(connection)),
|
|
91
|
+
await readLatestAzureIdentityEnrichment(connection),
|
|
92
|
+
permissionsByPrincipalId
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async readManagedIdentities(): Promise<ManagedIdentity[]> {
|
|
97
|
+
this.assertImported();
|
|
98
|
+
const connection = this.getConnection();
|
|
99
|
+
const permissionsByPrincipalId = await this.readPrincipalPermissionSummary(connection);
|
|
100
|
+
|
|
101
|
+
return toManagedIdentities(
|
|
102
|
+
mapEntraServicePrincipalsToCore(await readEntraServicePrincipalRows(connection)),
|
|
103
|
+
await readLatestAzureIdentityEnrichment(connection),
|
|
104
|
+
permissionsByPrincipalId
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async readEntraOAuth2PermissionGrants(): Promise<EntraOAuth2PermissionGrant[]> {
|
|
109
|
+
this.assertImported();
|
|
110
|
+
return (await readEntraOAuth2PermissionGrantRows(this.getConnection())).map(toCoreEntraOAuth2PermissionGrant);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async readEntraAppRoleAssignments(): Promise<EntraAppRoleAssignment[]> {
|
|
114
|
+
this.assertImported();
|
|
115
|
+
return readEntraAppRoleAssignmentRows(this.getConnection());
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async readEntraPrincipalPermissions(principalId: string): Promise<EntraPrincipalPermissions> {
|
|
119
|
+
this.assertImported();
|
|
120
|
+
const normalizedPrincipalId = principalId.toLowerCase();
|
|
121
|
+
const [oauth2PermissionGrants, appRoleAssignments] = await Promise.all([
|
|
122
|
+
readEntraOAuth2PermissionGrantRows(this.getConnection()),
|
|
123
|
+
readEntraAppRoleAssignmentRows(this.getConnection())
|
|
124
|
+
]);
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
principalId,
|
|
128
|
+
oauth2PermissionGrants: oauth2PermissionGrants.filter(
|
|
129
|
+
(grant) => grant.clientId.toLowerCase() === normalizedPrincipalId
|
|
130
|
+
).map(toCoreEntraOAuth2PermissionGrant),
|
|
131
|
+
appRoleAssignments: appRoleAssignments.filter(
|
|
132
|
+
(assignment) => assignment.principalId.toLowerCase() === normalizedPrincipalId
|
|
133
|
+
)
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private assertImported(): void {
|
|
138
|
+
if (!this.status.imported) {
|
|
139
|
+
throw new RuntimeHttpError(`Snapshot file ./data/${entraSnapshotFileName} was not found.`, 404);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private async readPrincipalPermissionSummary(
|
|
144
|
+
connection: DuckDBConnection
|
|
145
|
+
): Promise<Map<string, EntraPrincipalPermissionSummary>> {
|
|
146
|
+
const [oauth2PermissionGrants, appRoleAssignments] = await Promise.all([
|
|
147
|
+
readEntraOAuth2PermissionGrantRows(connection),
|
|
148
|
+
readEntraAppRoleAssignmentRows(connection)
|
|
149
|
+
]);
|
|
150
|
+
const permissionsByPrincipalId = new Map<string, EntraPrincipalPermissionSummary>();
|
|
151
|
+
|
|
152
|
+
for (const grant of oauth2PermissionGrants) {
|
|
153
|
+
const summary = getOrCreatePrincipalPermissionSummary(permissionsByPrincipalId, grant.clientId);
|
|
154
|
+
const scopeCount = countOAuthPermissionScopes(grant.scope);
|
|
155
|
+
summary.oauthPemrissionsCount += scopeCount;
|
|
156
|
+
if (scopeCount > 0) {
|
|
157
|
+
summary.entraPermissionRisk = maxPermissionRisk(
|
|
158
|
+
summary.entraPermissionRisk,
|
|
159
|
+
grant.consentType === "AllPrincipals" ? "high" : "medium"
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
for (const assignment of appRoleAssignments) {
|
|
165
|
+
const summary = getOrCreatePrincipalPermissionSummary(permissionsByPrincipalId, assignment.principalId);
|
|
166
|
+
summary.appRolesPermissionCount += 1;
|
|
167
|
+
summary.entraPermissionRisk = maxPermissionRisk(summary.entraPermissionRisk, "medium");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return permissionsByPrincipalId;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function getOrCreatePrincipalPermissionSummary(
|
|
175
|
+
permissionsByPrincipalId: Map<string, EntraPrincipalPermissionSummary>,
|
|
176
|
+
principalId: string
|
|
177
|
+
): EntraPrincipalPermissionSummary {
|
|
178
|
+
const normalizedPrincipalId = principalId.toLowerCase();
|
|
179
|
+
const existing = permissionsByPrincipalId.get(normalizedPrincipalId);
|
|
180
|
+
|
|
181
|
+
if (existing) {
|
|
182
|
+
return existing;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const summary = {
|
|
186
|
+
oauthPemrissionsCount: 0,
|
|
187
|
+
appRolesPermissionCount: 0,
|
|
188
|
+
entraPermissionRisk: "none" as PermissionRiskLevel
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
permissionsByPrincipalId.set(normalizedPrincipalId, summary);
|
|
192
|
+
return summary;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function countOAuthPermissionScopes(scope: string): number {
|
|
196
|
+
return scope.split(/\s+/).filter(Boolean).length;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function toCoreEntraOAuth2PermissionGrant(grant: InputEntraOAuth2PermissionGrant): EntraOAuth2PermissionGrant {
|
|
200
|
+
return {
|
|
201
|
+
...grant,
|
|
202
|
+
risk: getOAuth2PermissionGrantRisk(grant)
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function getOAuth2PermissionGrantRisk(grant: Pick<InputEntraOAuth2PermissionGrant, "consentType">): PermissionRiskLevel {
|
|
207
|
+
if (grant.consentType === "AllPrincipals") {
|
|
208
|
+
return "high";
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (grant.consentType === "Principal") {
|
|
212
|
+
return "low";
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return "medium";
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const permissionRiskRank: Record<PermissionRiskLevel, number> = {
|
|
219
|
+
none: 0,
|
|
220
|
+
low: 1,
|
|
221
|
+
medium: 2,
|
|
222
|
+
high: 3
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
function maxPermissionRisk(left: PermissionRiskLevel, right: PermissionRiskLevel): PermissionRiskLevel {
|
|
226
|
+
return permissionRiskRank[left] >= permissionRiskRank[right] ? left : right;
|
|
227
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { DuckDBConnection } from "@duckdb/node-api";
|
|
2
|
+
|
|
3
|
+
import type { EntraAppRoleAssignment } from "../../inputTransferObject/entra/EntraAppRoleAssignment";
|
|
4
|
+
|
|
5
|
+
export async function insertEntraAppRoleAssignmentRows(
|
|
6
|
+
connection: DuckDBConnection,
|
|
7
|
+
appRoleAssignments: EntraAppRoleAssignment[] = []
|
|
8
|
+
): Promise<void> {
|
|
9
|
+
for (const [ordinal, assignment] of appRoleAssignments.entries()) {
|
|
10
|
+
await connection.run(
|
|
11
|
+
`insert into entra_app_role_assignments values (
|
|
12
|
+
$ordinal,
|
|
13
|
+
$id,
|
|
14
|
+
$appRoleId,
|
|
15
|
+
$appRoleDisplayName,
|
|
16
|
+
$appRoleValue,
|
|
17
|
+
$principalId,
|
|
18
|
+
$principalDisplayName,
|
|
19
|
+
$resourceId,
|
|
20
|
+
$resourceDisplayName
|
|
21
|
+
)`,
|
|
22
|
+
{ ordinal, ...assignment }
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function readEntraAppRoleAssignmentRows(
|
|
28
|
+
connection: DuckDBConnection
|
|
29
|
+
): Promise<EntraAppRoleAssignment[]> {
|
|
30
|
+
return readRows<EntraAppRoleAssignment>(
|
|
31
|
+
connection,
|
|
32
|
+
`select
|
|
33
|
+
id,
|
|
34
|
+
app_role_id as appRoleId,
|
|
35
|
+
app_role_display_name as appRoleDisplayName,
|
|
36
|
+
app_role_value as appRoleValue,
|
|
37
|
+
principal_id as principalId,
|
|
38
|
+
principal_display_name as principalDisplayName,
|
|
39
|
+
resource_id as resourceId,
|
|
40
|
+
resource_display_name as resourceDisplayName
|
|
41
|
+
from entra_app_role_assignments
|
|
42
|
+
order by ordinal`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function readRows<Row extends Record<string, unknown>>(
|
|
47
|
+
connection: DuckDBConnection,
|
|
48
|
+
sql: string
|
|
49
|
+
): Promise<Row[]> {
|
|
50
|
+
const reader = await connection.runAndReadAll(sql);
|
|
51
|
+
return reader.getRowObjectsJson() as Row[];
|
|
52
|
+
}
|