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,102 @@
|
|
|
1
|
+
import type { DuckDBConnection } from "@duckdb/node-api";
|
|
2
|
+
|
|
3
|
+
import type { EntraSnapshot } from "../../inputTransferObject/entra/EntraSnapshot";
|
|
4
|
+
import type { LocalSnapshotData } from "../../../../core/runtime/localSnapshotFiles";
|
|
5
|
+
import { insertEntraApplicationRows, readEntraApplicationRows } from "./applicationsTable";
|
|
6
|
+
import { insertEntraAppRoleAssignmentRows, readEntraAppRoleAssignmentRows } from "./appRoleAssignmentsTable";
|
|
7
|
+
import { insertEntraOAuth2PermissionGrantRows, readEntraOAuth2PermissionGrantRows } from "./oauth2PermissionGrantsTable";
|
|
8
|
+
import { insertEntraServicePrincipalRows, readEntraServicePrincipalRows } from "./servicePrincipalsTable";
|
|
9
|
+
import { importEntraSnapshotMetadata } from "./snapshotMetadataTable";
|
|
10
|
+
|
|
11
|
+
export const entraSnapshotFileName = "entra-snapshot.json";
|
|
12
|
+
|
|
13
|
+
export type EntraDuckDbImportStatus = {
|
|
14
|
+
imported: boolean;
|
|
15
|
+
fileName: string;
|
|
16
|
+
servicePrincipalCount: number;
|
|
17
|
+
applicationCount: number;
|
|
18
|
+
oauth2PermissionGrantCount: number;
|
|
19
|
+
appRoleAssignmentCount: number;
|
|
20
|
+
importedAt: string | null;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function createEmptyEntraImportStatus(): EntraDuckDbImportStatus {
|
|
24
|
+
return {
|
|
25
|
+
imported: false,
|
|
26
|
+
fileName: entraSnapshotFileName,
|
|
27
|
+
servicePrincipalCount: 0,
|
|
28
|
+
applicationCount: 0,
|
|
29
|
+
oauth2PermissionGrantCount: 0,
|
|
30
|
+
appRoleAssignmentCount: 0,
|
|
31
|
+
importedAt: null
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function importEntraSnapshotToDuckDb(
|
|
36
|
+
connection: DuckDBConnection,
|
|
37
|
+
snapshot: EntraSnapshot & LocalSnapshotData
|
|
38
|
+
): Promise<EntraDuckDbImportStatus> {
|
|
39
|
+
await connection.run("begin transaction");
|
|
40
|
+
try {
|
|
41
|
+
await connection.run("delete from entra_snapshot_meta");
|
|
42
|
+
await connection.run("delete from entra_snapshot_extra");
|
|
43
|
+
await connection.run("delete from entra_service_principals");
|
|
44
|
+
await connection.run("delete from entra_applications");
|
|
45
|
+
await connection.run("delete from entra_oauth2_permission_grants");
|
|
46
|
+
await connection.run("delete from entra_app_role_assignments");
|
|
47
|
+
|
|
48
|
+
const { servicePrincipals, applications, oauth2PermissionGrants, appRoleAssignments } = snapshot;
|
|
49
|
+
await importEntraSnapshotMetadata(connection, snapshot);
|
|
50
|
+
|
|
51
|
+
await insertEntraServicePrincipalRows(connection, servicePrincipals);
|
|
52
|
+
await insertEntraApplicationRows(connection, applications);
|
|
53
|
+
await insertEntraOAuth2PermissionGrantRows(connection, oauth2PermissionGrants);
|
|
54
|
+
await insertEntraAppRoleAssignmentRows(connection, appRoleAssignments);
|
|
55
|
+
|
|
56
|
+
await connection.run("commit");
|
|
57
|
+
return {
|
|
58
|
+
imported: true,
|
|
59
|
+
fileName: entraSnapshotFileName,
|
|
60
|
+
servicePrincipalCount: servicePrincipals.length,
|
|
61
|
+
applicationCount: applications?.length ?? 0,
|
|
62
|
+
oauth2PermissionGrantCount: oauth2PermissionGrants?.length ?? 0,
|
|
63
|
+
appRoleAssignmentCount: appRoleAssignments?.length ?? 0,
|
|
64
|
+
importedAt: new Date().toISOString()
|
|
65
|
+
};
|
|
66
|
+
} catch (error) {
|
|
67
|
+
await connection.run("rollback");
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function readEntraSnapshotFromDuckDb(
|
|
73
|
+
connection: DuckDBConnection
|
|
74
|
+
): Promise<EntraSnapshot & LocalSnapshotData> {
|
|
75
|
+
const metaRows = await readRows<{ data: string }>(connection, "select data from entra_snapshot_meta limit 1");
|
|
76
|
+
const extraRows = await readRows<{ data: string }>(connection, "select data from entra_snapshot_extra limit 1");
|
|
77
|
+
const servicePrincipals = await readEntraServicePrincipalRows(connection);
|
|
78
|
+
const applications = await readEntraApplicationRows(connection);
|
|
79
|
+
const oauth2PermissionGrants = await readEntraOAuth2PermissionGrantRows(connection);
|
|
80
|
+
const appRoleAssignments = await readEntraAppRoleAssignmentRows(connection);
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
...parseJsonObject(extraRows[0]?.data),
|
|
84
|
+
meta: parseJsonObject(metaRows[0]?.data) as EntraSnapshot["meta"],
|
|
85
|
+
servicePrincipals,
|
|
86
|
+
applications,
|
|
87
|
+
oauth2PermissionGrants,
|
|
88
|
+
appRoleAssignments
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function readRows<Row extends Record<string, unknown>>(
|
|
93
|
+
connection: DuckDBConnection,
|
|
94
|
+
sql: string
|
|
95
|
+
): Promise<Row[]> {
|
|
96
|
+
const reader = await connection.runAndReadAll(sql);
|
|
97
|
+
return reader.getRowObjectsJson() as Row[];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function parseJsonObject(value: string | null | undefined): Record<string, unknown> {
|
|
101
|
+
return value ? JSON.parse(value) : {};
|
|
102
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { RuntimeHttpError } from "../../../core/runtime/localSnapshotFiles";
|
|
2
|
+
|
|
3
|
+
export type LocalReportCollectionQueryOptions = {
|
|
4
|
+
page?: number;
|
|
5
|
+
pageSize?: number;
|
|
6
|
+
filters?: LocalReportCollectionFilter[];
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type LocalReportCollectionFilter = {
|
|
10
|
+
column: string;
|
|
11
|
+
values: string[];
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type LocalReportPaginatedCollection<CollectionId extends string = string> = {
|
|
15
|
+
collectionId: CollectionId;
|
|
16
|
+
rows: Record<string, unknown>[];
|
|
17
|
+
columns: string[];
|
|
18
|
+
page: number;
|
|
19
|
+
pageSize: number;
|
|
20
|
+
count: number;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function buildPaginatedCollection<CollectionId extends string>(
|
|
24
|
+
collectionId: CollectionId,
|
|
25
|
+
rows: Record<string, unknown>[],
|
|
26
|
+
query: LocalReportCollectionQueryOptions
|
|
27
|
+
): LocalReportPaginatedCollection<CollectionId> {
|
|
28
|
+
const columns = buildCollectionColumns(rows);
|
|
29
|
+
const filteredRows = applyRuntimeCollectionFilters(rows, columns, query.filters ?? []);
|
|
30
|
+
const pageSize = clampInteger(query.pageSize ?? 50, 1, 500);
|
|
31
|
+
const page = clampInteger(query.page ?? 1, 1, Math.max(1, Math.ceil(filteredRows.length / pageSize)));
|
|
32
|
+
const pageRows = filteredRows.slice((page - 1) * pageSize, page * pageSize);
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
collectionId,
|
|
36
|
+
rows: pageRows,
|
|
37
|
+
columns,
|
|
38
|
+
page,
|
|
39
|
+
pageSize,
|
|
40
|
+
count: filteredRows.length
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function buildCollectionColumns(rows: Record<string, unknown>[]): string[] {
|
|
45
|
+
const columns = new Set<string>();
|
|
46
|
+
|
|
47
|
+
for (const row of rows) {
|
|
48
|
+
for (const column of Object.keys(row)) {
|
|
49
|
+
columns.add(column);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return [...columns];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function applyRuntimeCollectionFilters(
|
|
57
|
+
rows: Record<string, unknown>[],
|
|
58
|
+
columns: string[],
|
|
59
|
+
filters: LocalReportCollectionFilter[]
|
|
60
|
+
): Record<string, unknown>[] {
|
|
61
|
+
const activeFilters = filters
|
|
62
|
+
.map((filter) => ({
|
|
63
|
+
column: filter.column,
|
|
64
|
+
values: filter.values.map((value) => value.trim()).filter(Boolean)
|
|
65
|
+
}))
|
|
66
|
+
.filter((filter) => filter.column && filter.values.length > 0);
|
|
67
|
+
|
|
68
|
+
if (activeFilters.length === 0) {
|
|
69
|
+
return rows;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
for (const filter of activeFilters) {
|
|
73
|
+
if (!columns.includes(filter.column)) {
|
|
74
|
+
throw new RuntimeHttpError(`Unknown collection column: ${filter.column}`, 400);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return rows.filter((row) =>
|
|
79
|
+
activeFilters.every((filter) => {
|
|
80
|
+
const fieldValue = formatRuntimeFilterValue(row[filter.column]).toLocaleLowerCase();
|
|
81
|
+
return filter.values.some((value) => fieldValue.includes(value.toLocaleLowerCase()));
|
|
82
|
+
})
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function formatRuntimeFilterValue(value: unknown): string {
|
|
87
|
+
if (value === null || value === undefined) {
|
|
88
|
+
return "";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (typeof value === "string") {
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return JSON.stringify(value);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function clampInteger(value: number, min: number, max: number): number {
|
|
99
|
+
const integer = Number.isFinite(value) ? Math.trunc(value) : min;
|
|
100
|
+
return Math.min(Math.max(integer, min), max);
|
|
101
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import { RuntimeHttpError } from "../../../core/runtime/localSnapshotFiles";
|
|
4
|
+
import { createRuntimeRestMiddleware, type RuntimeRestEndpoint } from "../../../core/runtime/rest";
|
|
5
|
+
import { defineEntraLocalReportRuntimeRestEndpoints } from "./entra/localReportRuntimeRest";
|
|
6
|
+
import { LocalReportRuntime } from "./LocalReportRuntime";
|
|
7
|
+
import { defineAzureResourcesLocalReportRuntimeRestEndpoints } from "./resources/localReportRuntimeRest";
|
|
8
|
+
import { defineZeroTrustAssessmentLocalReportRuntimeRestEndpoints } from "./zta/localReportRuntimeRest";
|
|
9
|
+
|
|
10
|
+
const restBasePath = "/api/data";
|
|
11
|
+
|
|
12
|
+
export type LocalReportRuntimePluginHost = {
|
|
13
|
+
httpServer?: {
|
|
14
|
+
once(event: "listening" | "close", listener: () => void): void;
|
|
15
|
+
} | null;
|
|
16
|
+
middlewares: {
|
|
17
|
+
use(middleware: ReturnType<typeof createRuntimeRestMiddleware>): void;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function createLocalReportRuntime(dataDir: string): LocalReportRuntime {
|
|
22
|
+
return new LocalReportRuntime({ dataDir, databasePath: path.join(dataDir, "runtime.duckdb") });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function defineLocalReportRuntimeRestEndpoints(runtime: LocalReportRuntime): RuntimeRestEndpoint[] {
|
|
26
|
+
return [
|
|
27
|
+
{
|
|
28
|
+
path: restBasePath,
|
|
29
|
+
handle: () => runtime.listSnapshots()
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
path: `${restBasePath}/read`,
|
|
33
|
+
handle: ({ url }) => runtime.readSnapshot(url.searchParams.get("name") ?? "")
|
|
34
|
+
},
|
|
35
|
+
...defineEntraLocalReportRuntimeRestEndpoints(runtime, restBasePath),
|
|
36
|
+
...defineAzureResourcesLocalReportRuntimeRestEndpoints(runtime, restBasePath),
|
|
37
|
+
...defineZeroTrustAssessmentLocalReportRuntimeRestEndpoints(runtime, restBasePath),
|
|
38
|
+
{
|
|
39
|
+
path: `${restBasePath}/runtime/enrichment/recalculate`,
|
|
40
|
+
handle: async () => {
|
|
41
|
+
await runtime.recalculateEnrichment();
|
|
42
|
+
return runtime.getStatus().enrichment;
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
path: `${restBasePath}/runtime`,
|
|
47
|
+
handle: () => runtime.getStatus()
|
|
48
|
+
}
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function installLocalReportRuntimeRest(host: LocalReportRuntimePluginHost, runtime: LocalReportRuntime): void {
|
|
53
|
+
host.httpServer?.once("listening", () => {
|
|
54
|
+
void runtime.initialize();
|
|
55
|
+
});
|
|
56
|
+
host.httpServer?.once("close", () => {
|
|
57
|
+
void runtime.close();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
host.middlewares.use(
|
|
61
|
+
createRuntimeRestMiddleware({
|
|
62
|
+
basePath: restBasePath,
|
|
63
|
+
endpoints: defineLocalReportRuntimeRestEndpoints(runtime),
|
|
64
|
+
getErrorStatusCode: (error) => (error instanceof RuntimeHttpError ? error.statusCode : 500)
|
|
65
|
+
})
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function createDefaultLocalReportRuntime(root: string): LocalReportRuntime {
|
|
70
|
+
return createLocalReportRuntime(path.join(root, "data"));
|
|
71
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { mapRoleAssignmentToAzureRbac } from "../../../../core/azure/azureRbac";
|
|
2
|
+
import type { AzureRbac } from "../../../../core/azure/azureRbac";
|
|
3
|
+
import type { ResourceGroupOwnershipRow } from "../../../../core/azure/resources";
|
|
4
|
+
|
|
5
|
+
import { evaluateAzureRoleAssignmentRisk } from "../enrichment/evaluateAzureRoleAssignmentRisk";
|
|
6
|
+
import { buildAzureOwnershipReport } from "../../ownership/buildAzureOwnershipReport";
|
|
7
|
+
import {
|
|
8
|
+
buildPaginatedCollection,
|
|
9
|
+
type LocalReportCollectionQueryOptions,
|
|
10
|
+
type LocalReportPaginatedCollection
|
|
11
|
+
} from "../localReportCollections";
|
|
12
|
+
import type { DisabledEvidenceStore } from "../DisabledEvidenceStore";
|
|
13
|
+
import type { LocalEntraReportRuntime } from "../entra/LocalEntraReportRuntime";
|
|
14
|
+
import {
|
|
15
|
+
type LocalAzureResourcesReportCollectionId,
|
|
16
|
+
type LocalAzureResourcesReportRuntime
|
|
17
|
+
} from "./LocalAzureResourcesReportRuntime";
|
|
18
|
+
import {
|
|
19
|
+
applyResourceGroupOwnerDisabledEvidence,
|
|
20
|
+
buildResourceGroupOwnershipRows
|
|
21
|
+
} from "./resourceGroupOwnership";
|
|
22
|
+
|
|
23
|
+
export type LocalAzureResourcesExtendedCollectionId =
|
|
24
|
+
| LocalAzureResourcesReportCollectionId
|
|
25
|
+
| "azureResources.resourceGroupOwnership"
|
|
26
|
+
| "azureRbac";
|
|
27
|
+
|
|
28
|
+
export type AzureResourcesCollectionQueryServiceOptions = {
|
|
29
|
+
entra: LocalEntraReportRuntime;
|
|
30
|
+
azureResources: LocalAzureResourcesReportRuntime;
|
|
31
|
+
disabledEvidenceStore: DisabledEvidenceStore;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export class AzureResourcesCollectionQueryService {
|
|
35
|
+
private readonly entra: LocalEntraReportRuntime;
|
|
36
|
+
private readonly azureResources: LocalAzureResourcesReportRuntime;
|
|
37
|
+
private readonly disabledEvidenceStore: DisabledEvidenceStore;
|
|
38
|
+
|
|
39
|
+
constructor(options: AzureResourcesCollectionQueryServiceOptions) {
|
|
40
|
+
this.entra = options.entra;
|
|
41
|
+
this.azureResources = options.azureResources;
|
|
42
|
+
this.disabledEvidenceStore = options.disabledEvidenceStore;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async querySubscriptions(
|
|
46
|
+
options: LocalReportCollectionQueryOptions
|
|
47
|
+
): Promise<LocalReportPaginatedCollection<"azureResources.subscriptions">> {
|
|
48
|
+
return buildPaginatedCollection(
|
|
49
|
+
"azureResources.subscriptions",
|
|
50
|
+
(await this.azureResources.readAzureSubscriptions()) as unknown as Record<string, unknown>[],
|
|
51
|
+
options
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async queryResourceGroups(
|
|
56
|
+
options: LocalReportCollectionQueryOptions
|
|
57
|
+
): Promise<LocalReportPaginatedCollection<"azureResources.resourceGroups">> {
|
|
58
|
+
return buildPaginatedCollection(
|
|
59
|
+
"azureResources.resourceGroups",
|
|
60
|
+
(await this.azureResources.readAzureResourceGroups()) as unknown as Record<string, unknown>[],
|
|
61
|
+
options
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async queryResourceGroupOwnership(
|
|
66
|
+
options: LocalReportCollectionQueryOptions
|
|
67
|
+
): Promise<LocalReportPaginatedCollection<"azureResources.resourceGroupOwnership">> {
|
|
68
|
+
return buildPaginatedCollection(
|
|
69
|
+
"azureResources.resourceGroupOwnership",
|
|
70
|
+
await this.readResourceGroupOwnershipRows(),
|
|
71
|
+
options
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async queryResources(
|
|
76
|
+
options: LocalReportCollectionQueryOptions
|
|
77
|
+
): Promise<LocalReportPaginatedCollection<"azureResources.resources">> {
|
|
78
|
+
return buildPaginatedCollection(
|
|
79
|
+
"azureResources.resources",
|
|
80
|
+
(await this.azureResources.readAzureResources()) as unknown as Record<string, unknown>[],
|
|
81
|
+
options
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async queryUserAssignedManagedIdentities(
|
|
86
|
+
options: LocalReportCollectionQueryOptions
|
|
87
|
+
): Promise<LocalReportPaginatedCollection<"azureResources.userAssignedManagedIdentities">> {
|
|
88
|
+
return buildPaginatedCollection(
|
|
89
|
+
"azureResources.userAssignedManagedIdentities",
|
|
90
|
+
(await this.azureResources.readAzureUserAssignedManagedIdentities()) as unknown as Record<string, unknown>[],
|
|
91
|
+
options
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async queryRoleAssignments(
|
|
96
|
+
options: LocalReportCollectionQueryOptions
|
|
97
|
+
): Promise<LocalReportPaginatedCollection<"azureResources.roleAssignments">> {
|
|
98
|
+
return buildPaginatedCollection(
|
|
99
|
+
"azureResources.roleAssignments",
|
|
100
|
+
(await this.azureResources.readAzureRoleAssignments()) as unknown as Record<string, unknown>[],
|
|
101
|
+
options
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async queryAzureRbac(
|
|
106
|
+
servicePrincipalId: string,
|
|
107
|
+
options: LocalReportCollectionQueryOptions
|
|
108
|
+
): Promise<LocalReportPaginatedCollection<"azureRbac">> {
|
|
109
|
+
return buildPaginatedCollection(
|
|
110
|
+
"azureRbac",
|
|
111
|
+
(await this.readAzureRbacRows(servicePrincipalId)) as unknown as Record<string, unknown>[],
|
|
112
|
+
options
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async queryActivityLogs(
|
|
117
|
+
options: LocalReportCollectionQueryOptions
|
|
118
|
+
): Promise<LocalReportPaginatedCollection<"azureResources.activityLogs">> {
|
|
119
|
+
return buildPaginatedCollection(
|
|
120
|
+
"azureResources.activityLogs",
|
|
121
|
+
(await this.azureResources.readAzureActivityLogs()) as unknown as Record<string, unknown>[],
|
|
122
|
+
options
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async readResourceGroupOwnershipRows(): Promise<ResourceGroupOwnershipRow[]> {
|
|
127
|
+
const [resourceSnapshot, entraSnapshot, disabledKeys] = await Promise.all([
|
|
128
|
+
this.azureResources.readSnapshot(),
|
|
129
|
+
this.entra.readSnapshot(),
|
|
130
|
+
this.disabledEvidenceStore.readKeys()
|
|
131
|
+
]);
|
|
132
|
+
const ownerReport = buildAzureOwnershipReport(resourceSnapshot, entraSnapshot);
|
|
133
|
+
const ownerRows = applyResourceGroupOwnerDisabledEvidence(ownerReport.owners, disabledKeys);
|
|
134
|
+
|
|
135
|
+
return buildResourceGroupOwnershipRows(resourceSnapshot.resourceGroups, ownerRows);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private async readAzureRbacRows(servicePrincipalId: string): Promise<AzureRbac[]> {
|
|
139
|
+
const normalizedServicePrincipalId = servicePrincipalId.trim().toLowerCase();
|
|
140
|
+
|
|
141
|
+
return (await this.azureResources.readAzureRoleAssignments())
|
|
142
|
+
.filter((assignment) => assignment.principalId.toLowerCase() === normalizedServicePrincipalId)
|
|
143
|
+
.map((assignment) => mapRoleAssignmentToAzureRbac(assignment, evaluateAzureRoleAssignmentRisk(assignment).riskLevel));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
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 type {
|
|
7
|
+
AzureActivityLog,
|
|
8
|
+
AzureResource,
|
|
9
|
+
AzureResourceGroup,
|
|
10
|
+
AzureRoleAssignment,
|
|
11
|
+
AzureSnapshot,
|
|
12
|
+
AzureSubscription,
|
|
13
|
+
AzureUserAssignedManagedIdentity
|
|
14
|
+
} from "../../../../core/azure/resources";
|
|
15
|
+
import { pathExists, RuntimeHttpError, type LocalSnapshotData } from "../../../../core/runtime/localSnapshotFiles";
|
|
16
|
+
import type { AzureSnapshot as AzureSnapshotInput } from "../../inputTransferObject/resources/AzureSnapshot";
|
|
17
|
+
import {
|
|
18
|
+
azureResourcesSnapshotFileName,
|
|
19
|
+
createEmptyAzureResourcesImportStatus,
|
|
20
|
+
importAzureResourcesSnapshotToDuckDb,
|
|
21
|
+
readAzureResourcesSnapshotFromDuckDb,
|
|
22
|
+
type AzureResourcesDuckDbImportStatus
|
|
23
|
+
} from "./snapshotStore";
|
|
24
|
+
import {
|
|
25
|
+
readAzureActivityLogRows,
|
|
26
|
+
readAzureResourceGroupRows,
|
|
27
|
+
readAzureResourceRows,
|
|
28
|
+
readAzureRoleAssignmentRows,
|
|
29
|
+
readAzureSubscriptionRows,
|
|
30
|
+
readAzureUserAssignedManagedIdentityRows
|
|
31
|
+
} from "./tables";
|
|
32
|
+
|
|
33
|
+
export type LocalAzureResourcesReportCollectionId =
|
|
34
|
+
| "azureResources.subscriptions"
|
|
35
|
+
| "azureResources.resourceGroups"
|
|
36
|
+
| "azureResources.resources"
|
|
37
|
+
| "azureResources.userAssignedManagedIdentities"
|
|
38
|
+
| "azureResources.roleAssignments"
|
|
39
|
+
| "azureResources.activityLogs";
|
|
40
|
+
|
|
41
|
+
export type LocalAzureResourcesReportRuntimeOptions = {
|
|
42
|
+
dataDir: string;
|
|
43
|
+
getConnection: () => DuckDBConnection;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export class LocalAzureResourcesReportRuntime {
|
|
47
|
+
private readonly dataDir: string;
|
|
48
|
+
private readonly getConnection: () => DuckDBConnection;
|
|
49
|
+
private status = createEmptyAzureResourcesImportStatus();
|
|
50
|
+
|
|
51
|
+
constructor(options: LocalAzureResourcesReportRuntimeOptions) {
|
|
52
|
+
this.dataDir = options.dataDir;
|
|
53
|
+
this.getConnection = options.getConnection;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
getStatus(): AzureResourcesDuckDbImportStatus {
|
|
57
|
+
return this.status;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
canReadSnapshot(name: string): boolean {
|
|
61
|
+
return name === azureResourcesSnapshotFileName;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async importSnapshot(): Promise<void> {
|
|
65
|
+
const snapshotPath = path.join(this.dataDir, azureResourcesSnapshotFileName);
|
|
66
|
+
if (!(await pathExists(snapshotPath))) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const snapshot = JSON.parse(await readFile(snapshotPath, "utf8")) as AzureSnapshotInput & LocalSnapshotData;
|
|
71
|
+
this.status = await importAzureResourcesSnapshotToDuckDb(this.getConnection(), snapshot);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async readSnapshot(): Promise<AzureSnapshot & LocalSnapshotData> {
|
|
75
|
+
this.assertImported();
|
|
76
|
+
return readAzureResourcesSnapshotFromDuckDb(this.getConnection());
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async readAzureSubscriptions(): Promise<AzureSubscription[]> {
|
|
80
|
+
this.assertImported();
|
|
81
|
+
return readAzureSubscriptionRows(this.getConnection());
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async readAzureResourceGroups(): Promise<AzureResourceGroup[]> {
|
|
85
|
+
this.assertImported();
|
|
86
|
+
return readAzureResourceGroupRows(this.getConnection());
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async readAzureResources(): Promise<AzureResource[]> {
|
|
90
|
+
this.assertImported();
|
|
91
|
+
return readAzureResourceRows(this.getConnection());
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async readAzureUserAssignedManagedIdentities(): Promise<AzureUserAssignedManagedIdentity[]> {
|
|
95
|
+
this.assertImported();
|
|
96
|
+
return readAzureUserAssignedManagedIdentityRows(this.getConnection());
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async readAzureRoleAssignments(): Promise<AzureRoleAssignment[]> {
|
|
100
|
+
this.assertImported();
|
|
101
|
+
return readAzureRoleAssignmentRows(this.getConnection());
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async readAzureActivityLogs(): Promise<AzureActivityLog[]> {
|
|
105
|
+
this.assertImported();
|
|
106
|
+
return readAzureActivityLogRows(this.getConnection());
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private assertImported(): void {
|
|
110
|
+
if (!this.status.imported) {
|
|
111
|
+
throw new RuntimeHttpError(`Snapshot file ./data/${azureResourcesSnapshotFileName} was not found.`, 404);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { DuckDBConnection } from "@duckdb/node-api";
|
|
2
|
+
|
|
3
|
+
export type DisabledOwnerKey = string;
|
|
4
|
+
|
|
5
|
+
export async function readDisabledOwnerEvidenceKeys(
|
|
6
|
+
connection: DuckDBConnection
|
|
7
|
+
): Promise<Set<DisabledOwnerKey>> {
|
|
8
|
+
const rows = await readRows<DisabledOwnerEvidenceDbRow>(
|
|
9
|
+
connection,
|
|
10
|
+
"select owner_key from azure_disabled_owner_evidence_keys order by owner_key"
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
return new Set(rows.map((row) => row.owner_key));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function disableOwnerEvidenceKey(
|
|
17
|
+
connection: DuckDBConnection,
|
|
18
|
+
key: DisabledOwnerKey
|
|
19
|
+
): Promise<void> {
|
|
20
|
+
await connection.run(
|
|
21
|
+
`insert into azure_disabled_owner_evidence_keys values ($key, $disabledAt)
|
|
22
|
+
on conflict(owner_key) do update set disabled_at = excluded.disabled_at`,
|
|
23
|
+
{
|
|
24
|
+
key,
|
|
25
|
+
disabledAt: new Date().toISOString()
|
|
26
|
+
}
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function enableOwnerEvidenceKey(
|
|
31
|
+
connection: DuckDBConnection,
|
|
32
|
+
key: DisabledOwnerKey
|
|
33
|
+
): Promise<void> {
|
|
34
|
+
await connection.run("delete from azure_disabled_owner_evidence_keys where owner_key = $key", { key });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function countDisabledOwnerEvidenceKeys(connection: DuckDBConnection): Promise<number> {
|
|
38
|
+
const rows = await readRows<DisabledOwnerEvidenceKeyCountRow>(
|
|
39
|
+
connection,
|
|
40
|
+
"select count(*) as disabled_count from azure_disabled_owner_evidence_keys"
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
return Number(rows[0]?.disabled_count ?? 0);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type DisabledOwnerEvidenceDbRow = {
|
|
47
|
+
owner_key: DisabledOwnerKey;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
type DisabledOwnerEvidenceKeyCountRow = {
|
|
51
|
+
disabled_count: string | number;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
async function readRows<Row extends Record<string, unknown>>(
|
|
55
|
+
connection: DuckDBConnection,
|
|
56
|
+
sql: string
|
|
57
|
+
): Promise<Row[]> {
|
|
58
|
+
const reader = await connection.runAndReadAll(sql);
|
|
59
|
+
return reader.getRowObjectsJson() as Row[];
|
|
60
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { RuntimeHttpError } from "../../../../core/runtime/localSnapshotFiles";
|
|
2
|
+
import type { RuntimeRestEndpoint } from "../../../../core/runtime/rest";
|
|
3
|
+
import type { LocalReportRuntime } from "../LocalReportRuntime";
|
|
4
|
+
import { parseRuntimeCollectionQueryOptions } from "../runtimeRestQuery";
|
|
5
|
+
|
|
6
|
+
export function defineAzureResourcesLocalReportRuntimeRestEndpoints(
|
|
7
|
+
runtime: LocalReportRuntime,
|
|
8
|
+
restBasePath: string
|
|
9
|
+
): RuntimeRestEndpoint[] {
|
|
10
|
+
return [
|
|
11
|
+
{
|
|
12
|
+
path: `${restBasePath}/azureResources/subscriptions`,
|
|
13
|
+
handle: ({ url }) => runtime.queryAzureSubscriptions(parseRuntimeCollectionQueryOptions(url))
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
path: `${restBasePath}/azureResources/resourceGroups`,
|
|
17
|
+
handle: ({ url }) => runtime.queryAzureResourceGroups(parseRuntimeCollectionQueryOptions(url))
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
path: `${restBasePath}/azureResources/resourceGroupOwnership`,
|
|
21
|
+
handle: ({ url }) => runtime.queryAzureResourceGroupOwnership(parseRuntimeCollectionQueryOptions(url))
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
path: `${restBasePath}/azureResources/resourceGroupOwnership/disabledEvidence`,
|
|
25
|
+
handle: async ({ url }) => {
|
|
26
|
+
const key = readRequiredSearchParam(url, "key");
|
|
27
|
+
const disabled = readBooleanSearchParam(url, "disabled");
|
|
28
|
+
const disabledCount = await runtime.setOwnerEvidenceDisabled(key, disabled);
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
key,
|
|
32
|
+
disabled,
|
|
33
|
+
disabledCount
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
path: `${restBasePath}/azureResources/resources`,
|
|
39
|
+
handle: ({ url }) => runtime.queryAzureResources(parseRuntimeCollectionQueryOptions(url))
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
path: `${restBasePath}/azureResources/userAssignedManagedIdentities`,
|
|
43
|
+
handle: ({ url }) => runtime.queryAzureUserAssignedManagedIdentities(parseRuntimeCollectionQueryOptions(url))
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
path: `${restBasePath}/azureResources/roleAssignments`,
|
|
47
|
+
handle: ({ url }) => runtime.queryAzureRoleAssignments(parseRuntimeCollectionQueryOptions(url))
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
path: `${restBasePath}/azureRbac`,
|
|
51
|
+
handle: ({ url }) =>
|
|
52
|
+
runtime.queryAzureRbac(readRequiredSearchParam(url, "servicePrincipalId"), parseRuntimeCollectionQueryOptions(url))
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
path: `${restBasePath}/azureResources/activityLogs`,
|
|
56
|
+
handle: ({ url }) => runtime.queryAzureActivityLogs(parseRuntimeCollectionQueryOptions(url))
|
|
57
|
+
}
|
|
58
|
+
];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function readRequiredSearchParam(url: URL, name: string): string {
|
|
62
|
+
const value = url.searchParams.get(name)?.trim();
|
|
63
|
+
if (!value) {
|
|
64
|
+
throw new RuntimeHttpError(`Missing required query parameter: ${name}`, 400);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function readBooleanSearchParam(url: URL, name: string): boolean {
|
|
71
|
+
const value = readRequiredSearchParam(url, name).toLowerCase();
|
|
72
|
+
if (value === "true" || value === "1") {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (value === "false" || value === "0") {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
throw new RuntimeHttpError(`Invalid boolean query parameter: ${name}`, 400);
|
|
81
|
+
}
|