ownerlens 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/LICENSE +183 -0
  2. package/README.md +209 -0
  3. package/bin/ownerlens.js +92 -0
  4. package/dist/assets/index-B9aAYpVl.css +1 -0
  5. package/dist/assets/index-BcwLk2bx.js +10 -0
  6. package/dist/index.html +13 -0
  7. package/package.json +73 -0
  8. package/src/App.tsx +18 -0
  9. package/src/components/azure/AzureComponent.test.tsx +625 -0
  10. package/src/components/azure/AzureComponent.tsx +189 -0
  11. package/src/components/azure/AzureRbacComponent.tsx +104 -0
  12. package/src/components/azure/ClosableAzureTab.tsx +42 -0
  13. package/src/components/azure/EntraPermissionsComponent.tsx +194 -0
  14. package/src/components/azure/ManagedIdentityComponent.test.tsx +324 -0
  15. package/src/components/azure/ManagedIdentityComponent.tsx +141 -0
  16. package/src/components/azure/ResourceGroupComponent.tsx +157 -0
  17. package/src/components/azure/ServicePrincipalComponent.test.tsx +457 -0
  18. package/src/components/azure/ServicePrincipalComponent.tsx +155 -0
  19. package/src/components/azure/ServicePrincipalFieldRenderers.tsx +140 -0
  20. package/src/components/azure/ZtaComponent.test.tsx +267 -0
  21. package/src/components/azure/ZtaComponent.tsx +276 -0
  22. package/src/components/azure/ZtaRemediationBadge.tsx +70 -0
  23. package/src/components/azure/api.ts +216 -0
  24. package/src/components/azure/azureReportConfig.ts +247 -0
  25. package/src/core/azure/azureRbac.ts +70 -0
  26. package/src/core/azure/entra/index.ts +1 -0
  27. package/src/core/azure/entra/managedIdentity.ts +21 -0
  28. package/src/core/azure/entra/servicePrincipal.ts +34 -0
  29. package/src/core/azure/entra/types.ts +56 -0
  30. package/src/core/azure/identityEnrichment.ts +65 -0
  31. package/src/core/azure/resources.ts +141 -0
  32. package/src/core/azure/ztaReport.ts +58 -0
  33. package/src/core/config.ts +39 -0
  34. package/src/core/ownership/OwnershipTarget.ts +32 -0
  35. package/src/core/ownership/resolveOwner.ts +5 -0
  36. package/src/core/ownership/types.ts +14 -0
  37. package/src/core/risk/types.ts +1 -0
  38. package/src/core/runtime/index.ts +1 -0
  39. package/src/core/runtime/localSnapshotFiles.ts +74 -0
  40. package/src/core/runtime/rest.ts +61 -0
  41. package/src/lib/searchFilterUtils.ts +17 -0
  42. package/src/lib/utils.ts +48 -0
  43. package/src/main.tsx +10 -0
  44. package/src/providers/azure/identities/azureIdentityTypes.ts +1 -0
  45. package/src/providers/azure/identities/buildAzureManagedIdentityAssignmentIndex.test.ts +32 -0
  46. package/src/providers/azure/identities/buildAzureManagedIdentityAssignmentIndex.ts +35 -0
  47. package/src/providers/azure/identities/userAssignedIdentityAssignments.ts +52 -0
  48. package/src/providers/azure/inputTransferObject/entra/EntraAppRoleAssignment.ts +10 -0
  49. package/src/providers/azure/inputTransferObject/entra/EntraApplication.ts +27 -0
  50. package/src/providers/azure/inputTransferObject/entra/EntraOAuth2PermissionGrant.ts +8 -0
  51. package/src/providers/azure/inputTransferObject/entra/EntraServicePrincipal.ts +43 -0
  52. package/src/providers/azure/inputTransferObject/entra/EntraSnapshot.ts +13 -0
  53. package/src/providers/azure/inputTransferObject/entra/EntraSnapshotMeta.ts +12 -0
  54. package/src/providers/azure/inputTransferObject/resources/AzureActivityLog.ts +1 -0
  55. package/src/providers/azure/inputTransferObject/resources/AzureResource.ts +1 -0
  56. package/src/providers/azure/inputTransferObject/resources/AzureResourceGroup.ts +1 -0
  57. package/src/providers/azure/inputTransferObject/resources/AzureRoleAssignment.ts +1 -0
  58. package/src/providers/azure/inputTransferObject/resources/AzureSnapshot.ts +1 -0
  59. package/src/providers/azure/inputTransferObject/resources/AzureSnapshotMeta.ts +1 -0
  60. package/src/providers/azure/inputTransferObject/resources/AzureSubscription.ts +1 -0
  61. package/src/providers/azure/inputTransferObject/resources/AzureUserAssignedManagedIdentity.ts +1 -0
  62. package/src/providers/azure/ownership/azureActivityOwnershipEvidence.ts +60 -0
  63. package/src/providers/azure/ownership/azureOwnerReportTypes.ts +13 -0
  64. package/src/providers/azure/ownership/azureOwnershipConfig.ts +21 -0
  65. package/src/providers/azure/ownership/azureOwnershipTypes.ts +46 -0
  66. package/src/providers/azure/ownership/buildAzureOwnershipReport.test.ts +99 -0
  67. package/src/providers/azure/ownership/buildAzureOwnershipReport.ts +90 -0
  68. package/src/providers/azure/ownership/buildAzureOwnershipTargets.test.ts +87 -0
  69. package/src/providers/azure/ownership/buildAzureOwnershipTargets.ts +42 -0
  70. package/src/providers/azure/ownership/resolveAzureOwner.ts +146 -0
  71. package/src/providers/azure/runtime/DisabledEvidenceStore.ts +34 -0
  72. package/src/providers/azure/runtime/EnrichmentService.ts +35 -0
  73. package/src/providers/azure/runtime/LocalReportRuntime.test.ts +2318 -0
  74. package/src/providers/azure/runtime/LocalReportRuntime.ts +302 -0
  75. package/src/providers/azure/runtime/RuntimeHost.ts +60 -0
  76. package/src/providers/azure/runtime/SnapshotImporter.ts +44 -0
  77. package/src/providers/azure/runtime/enrichment/azureIdentityEnrichment.ts +523 -0
  78. package/src/providers/azure/runtime/enrichment/azureScopeClassifier.ts +30 -0
  79. package/src/providers/azure/runtime/enrichment/evaluateAzureRoleAssignmentRisk.ts +88 -0
  80. package/src/providers/azure/runtime/entra/EntraCollectionQueryService.ts +307 -0
  81. package/src/providers/azure/runtime/entra/LocalEntraReportRuntime.ts +227 -0
  82. package/src/providers/azure/runtime/entra/appRoleAssignmentsTable.ts +52 -0
  83. package/src/providers/azure/runtime/entra/applicationsTable.ts +175 -0
  84. package/src/providers/azure/runtime/entra/entraServicePrincipalMapper.ts +63 -0
  85. package/src/providers/azure/runtime/entra/localReportRuntimeRest.ts +41 -0
  86. package/src/providers/azure/runtime/entra/oauth2PermissionGrantsTable.ts +48 -0
  87. package/src/providers/azure/runtime/entra/principalProjection.ts +173 -0
  88. package/src/providers/azure/runtime/entra/servicePrincipalsTable.ts +149 -0
  89. package/src/providers/azure/runtime/entra/snapshotMetadataTable.ts +18 -0
  90. package/src/providers/azure/runtime/entra/snapshotStore.ts +102 -0
  91. package/src/providers/azure/runtime/localReportCollections.ts +101 -0
  92. package/src/providers/azure/runtime/localReportRuntimeRest.ts +71 -0
  93. package/src/providers/azure/runtime/resources/AzureResourcesCollectionQueryService.ts +145 -0
  94. package/src/providers/azure/runtime/resources/LocalAzureResourcesReportRuntime.ts +114 -0
  95. package/src/providers/azure/runtime/resources/disabledOwnerEvidenceTable.ts +60 -0
  96. package/src/providers/azure/runtime/resources/localReportRuntimeRest.ts +81 -0
  97. package/src/providers/azure/runtime/resources/resourceGroupOwnership.ts +90 -0
  98. package/src/providers/azure/runtime/resources/snapshotMetadataTable.ts +19 -0
  99. package/src/providers/azure/runtime/resources/snapshotStore.ts +128 -0
  100. package/src/providers/azure/runtime/resources/tables.ts +441 -0
  101. package/src/providers/azure/runtime/runtimeRestQuery.ts +46 -0
  102. package/src/providers/azure/runtime/runtimeSqlSchema.ts +357 -0
  103. package/src/providers/azure/runtime/zta/Discovery.ts +141 -0
  104. package/src/providers/azure/runtime/zta/LocalZeroTrustAssessmentReportRuntime.ts +86 -0
  105. package/src/providers/azure/runtime/zta/ZeroTrustAssessmentQueryService.ts +124 -0
  106. package/src/providers/azure/runtime/zta/localReportRuntimeRest.ts +15 -0
  107. package/src/providers/azure/runtime/zta/snapshotMetadataTable.ts +77 -0
  108. package/src/providers/azure/runtime/zta/snapshotStore.ts +112 -0
  109. package/src/providers/azure/runtime/zta/tables.ts +361 -0
  110. package/src/providers/azure/runtime/zta/types.ts +7 -0
  111. package/src/providers/azure/runtime/zta/ztaReportMapper.ts +12 -0
  112. package/src/report/applyCollectionControls.ts +289 -0
  113. package/src/report/buildCollectionColumns.tsx +38 -0
  114. package/src/report/components/ConfidenceBadge.tsx +10 -0
  115. package/src/report/components/EvidenceList.test.ts +25 -0
  116. package/src/report/components/EvidenceList.tsx +52 -0
  117. package/src/report/components/GenericTable.tsx +373 -0
  118. package/src/report/components/PermissionRiskBadge.tsx +19 -0
  119. package/src/report/components/reportTableControls.test.ts +175 -0
  120. package/src/report/components/reportTableControls.tsx +483 -0
  121. package/src/report/components/ui/badge.tsx +35 -0
  122. package/src/report/components/ui/button.tsx +38 -0
  123. package/src/report/components/ui/card.tsx +23 -0
  124. package/src/report/components/ui/input.tsx +15 -0
  125. package/src/report/components/ui/table.tsx +44 -0
  126. package/src/report/components/ui/tabs.tsx +29 -0
  127. package/src/report/export/csv.ts +34 -0
  128. package/src/report/ownerManualPrecheck.test.ts +137 -0
  129. package/src/report/ownerManualPrecheck.ts +132 -0
  130. package/src/report/reportArchitecture.test.ts +125 -0
  131. package/src/report/reportTypes.ts +54 -0
  132. package/src/report/reportValueRenderers.tsx +54 -0
  133. package/src/report/runtimeCollectionQuery.ts +23 -0
  134. package/src/report/types.ts +14 -0
  135. package/src/styles.css +43 -0
  136. package/tools/README.md +108 -0
  137. package/tools/azure-activity-check.ps1 +164 -0
  138. package/tools/collect-azure.ps1 +54 -0
  139. package/tools/collect-entra.ps1 +47 -0
  140. package/tools/collect-scripts.test.ts +22 -0
  141. package/tools/prepare-entra-snapshot.ps1 +403 -0
  142. package/tools/prepare-entra-snapshot.test.ts +14 -0
  143. package/tools/prepare-resource-snapshot.ps1 +345 -0
  144. package/vite.config.ts +23 -0
@@ -0,0 +1,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
+ }