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,90 @@
1
+ import type { OwnerEvidence } from "../../../../core/ownership/types";
2
+ import type { AzureResourceGroup, ResourceGroupOwnershipRow } from "../../../../core/azure/resources";
3
+ import type { OwnerReportRow } from "../../ownership/azureOwnerReportTypes";
4
+
5
+ export function buildResourceGroupOwnershipRows(
6
+ resourceGroups: AzureResourceGroup[],
7
+ ownerRows: OwnerReportRow[]
8
+ ): ResourceGroupOwnershipRow[] {
9
+ const ownerIndex = buildResourceGroupOwnerIndex(ownerRows);
10
+
11
+ return resourceGroups.map((group) => {
12
+ const ownerRow = ownerIndex.get(getResourceGroupOwnerIndexKey(group.subscriptionId, group.resourceGroup));
13
+
14
+ return {
15
+ ...group,
16
+ targetKey: ownerRow?.targetKey ?? getResourceGroupTargetKey(group.subscriptionId, group.resourceGroup),
17
+ owner: ownerRow?.owner ?? null,
18
+ confidence: ownerRow?.confidence ?? "none",
19
+ source: ownerRow?.source ?? "none",
20
+ evidence: ownerRow?.evidence ?? []
21
+ };
22
+ });
23
+ }
24
+
25
+ export function applyResourceGroupOwnerDisabledEvidence(
26
+ ownerRows: OwnerReportRow[],
27
+ disabledKeys: ReadonlySet<string>
28
+ ): OwnerReportRow[] {
29
+ return ownerRows.map((row) => {
30
+ if (row.kind !== "resourceGroup" || !row.source.startsWith("activity.")) {
31
+ return row;
32
+ }
33
+
34
+ const evidence = row.evidence.map((entry) => ({
35
+ ...entry,
36
+ disabled:
37
+ isDefaultDisabledOwnerEvidence(entry) ||
38
+ disabledKeys.has(getResourceGroupOwnerEvidenceKey(row, entry)) ||
39
+ undefined
40
+ }));
41
+ const activeEvidence = evidence.filter((entry) => !entry.disabled);
42
+
43
+ if (activeEvidence.length === 0) {
44
+ return {
45
+ ...row,
46
+ owner: null,
47
+ confidence: "none",
48
+ evidence
49
+ };
50
+ }
51
+
52
+ return {
53
+ ...row,
54
+ owner: activeEvidence[0].user,
55
+ confidence: "low",
56
+ evidence
57
+ };
58
+ });
59
+ }
60
+
61
+ function buildResourceGroupOwnerIndex(ownerRows: OwnerReportRow[]): Map<string, OwnerReportRow> {
62
+ const index = new Map<string, OwnerReportRow>();
63
+
64
+ for (const row of ownerRows) {
65
+ if (row.kind === "resourceGroup" && row.resourceGroup) {
66
+ index.set(getResourceGroupOwnerIndexKey(row.subscriptionId, row.resourceGroup), row);
67
+ }
68
+ }
69
+
70
+ return index;
71
+ }
72
+
73
+ function getResourceGroupOwnerIndexKey(subscriptionId: string, resourceGroup: string): string {
74
+ return `${subscriptionId.toLowerCase()}:${resourceGroup.toLowerCase()}`;
75
+ }
76
+
77
+ function getResourceGroupTargetKey(subscriptionId: string, resourceGroup: string): string {
78
+ return ["resourceGroup", subscriptionId.toLowerCase(), resourceGroup.toLowerCase()].join(":");
79
+ }
80
+
81
+ function getResourceGroupOwnerEvidenceKey(
82
+ row: Pick<OwnerReportRow, "targetKey">,
83
+ evidence: Pick<OwnerEvidence, "user" | "date">
84
+ ): string {
85
+ return [row.targetKey, evidence.user.trim().toLowerCase(), evidence.date ?? ""].join(":");
86
+ }
87
+
88
+ function isDefaultDisabledOwnerEvidence(evidence: Pick<OwnerEvidence, "user">): boolean {
89
+ return !evidence.user.includes("@");
90
+ }
@@ -0,0 +1,19 @@
1
+ import type { DuckDBConnection } from "@duckdb/node-api";
2
+
3
+ import type { LocalSnapshotData } from "../../../../core/runtime/localSnapshotFiles";
4
+ import type { AzureSnapshot as AzureSnapshotInput } from "../../inputTransferObject/resources/AzureSnapshot";
5
+
6
+ export async function importAzureResourcesSnapshotMetadata(
7
+ connection: DuckDBConnection,
8
+ snapshot: AzureSnapshotInput & LocalSnapshotData
9
+ ): Promise<void> {
10
+ const { meta, subscriptions, resourceGroups, resources, userAssignedManagedIdentities, roleAssignments, activityLogs, ...extra } =
11
+ snapshot;
12
+
13
+ await connection.run("insert into azure_resources_snapshot_meta values ($meta::json)", {
14
+ meta: JSON.stringify(meta ?? {})
15
+ });
16
+ await connection.run("insert into azure_resources_snapshot_extra values ($extra::json)", {
17
+ extra: JSON.stringify(extra)
18
+ });
19
+ }
@@ -0,0 +1,128 @@
1
+ import type { DuckDBConnection } from "@duckdb/node-api";
2
+
3
+ import type { AzureSnapshot } from "../../../../core/azure/resources";
4
+ import type { LocalSnapshotData } from "../../../../core/runtime/localSnapshotFiles";
5
+ import type { AzureSnapshot as AzureSnapshotInput } from "../../inputTransferObject/resources/AzureSnapshot";
6
+ import { importAzureResourcesSnapshotMetadata } from "./snapshotMetadataTable";
7
+ import {
8
+ insertAzureActivityLogRows,
9
+ insertAzureResourceGroupRows,
10
+ insertAzureResourceRows,
11
+ insertAzureRoleAssignmentRows,
12
+ insertAzureSubscriptionRows,
13
+ insertAzureUserAssignedManagedIdentityRows,
14
+ readAzureActivityLogRows,
15
+ readAzureResourceGroupRows,
16
+ readAzureResourceRows,
17
+ readAzureRoleAssignmentRows,
18
+ readAzureSubscriptionRows,
19
+ readAzureUserAssignedManagedIdentityRows
20
+ } from "./tables";
21
+
22
+ export const azureResourcesSnapshotFileName = "snapshot.json";
23
+
24
+ export type AzureResourcesDuckDbImportStatus = {
25
+ imported: boolean;
26
+ fileName: string;
27
+ subscriptionCount: number;
28
+ resourceGroupCount: number;
29
+ resourceCount: number;
30
+ userAssignedManagedIdentityCount: number;
31
+ roleAssignmentCount: number;
32
+ activityLogCount: number;
33
+ importedAt: string | null;
34
+ };
35
+
36
+ export function createEmptyAzureResourcesImportStatus(): AzureResourcesDuckDbImportStatus {
37
+ return {
38
+ imported: false,
39
+ fileName: azureResourcesSnapshotFileName,
40
+ subscriptionCount: 0,
41
+ resourceGroupCount: 0,
42
+ resourceCount: 0,
43
+ userAssignedManagedIdentityCount: 0,
44
+ roleAssignmentCount: 0,
45
+ activityLogCount: 0,
46
+ importedAt: null
47
+ };
48
+ }
49
+
50
+ export async function importAzureResourcesSnapshotToDuckDb(
51
+ connection: DuckDBConnection,
52
+ snapshot: AzureSnapshotInput & LocalSnapshotData
53
+ ): Promise<AzureResourcesDuckDbImportStatus> {
54
+ await connection.run("begin transaction");
55
+ try {
56
+ await connection.run("delete from azure_resources_snapshot_meta");
57
+ await connection.run("delete from azure_resources_snapshot_extra");
58
+ await connection.run("delete from azure_subscriptions");
59
+ await connection.run("delete from azure_resource_groups");
60
+ await connection.run("delete from azure_resources");
61
+ await connection.run("delete from azure_user_assigned_managed_identities");
62
+ await connection.run("delete from azure_role_assignments");
63
+ await connection.run("delete from azure_activity_logs");
64
+
65
+ const {
66
+ subscriptions,
67
+ resourceGroups,
68
+ resources,
69
+ userAssignedManagedIdentities,
70
+ roleAssignments = [],
71
+ activityLogs
72
+ } = snapshot;
73
+
74
+ await importAzureResourcesSnapshotMetadata(connection, snapshot);
75
+ await insertAzureSubscriptionRows(connection, subscriptions);
76
+ await insertAzureResourceGroupRows(connection, resourceGroups);
77
+ await insertAzureResourceRows(connection, resources);
78
+ await insertAzureUserAssignedManagedIdentityRows(connection, userAssignedManagedIdentities);
79
+ await insertAzureRoleAssignmentRows(connection, roleAssignments);
80
+ await insertAzureActivityLogRows(connection, activityLogs);
81
+
82
+ await connection.run("commit");
83
+ return {
84
+ imported: true,
85
+ fileName: azureResourcesSnapshotFileName,
86
+ subscriptionCount: subscriptions.length,
87
+ resourceGroupCount: resourceGroups.length,
88
+ resourceCount: resources.length,
89
+ userAssignedManagedIdentityCount: userAssignedManagedIdentities.length,
90
+ roleAssignmentCount: roleAssignments.length,
91
+ activityLogCount: activityLogs.length,
92
+ importedAt: new Date().toISOString()
93
+ };
94
+ } catch (error) {
95
+ await connection.run("rollback");
96
+ throw error;
97
+ }
98
+ }
99
+
100
+ export async function readAzureResourcesSnapshotFromDuckDb(
101
+ connection: DuckDBConnection
102
+ ): Promise<AzureSnapshot & LocalSnapshotData> {
103
+ const metaRows = await readRows<{ data: string }>(connection, "select data from azure_resources_snapshot_meta limit 1");
104
+ const extraRows = await readRows<{ data: string }>(connection, "select data from azure_resources_snapshot_extra limit 1");
105
+
106
+ return {
107
+ ...parseJsonObject(extraRows[0]?.data),
108
+ meta: parseJsonObject(metaRows[0]?.data) as AzureSnapshot["meta"],
109
+ subscriptions: await readAzureSubscriptionRows(connection),
110
+ resourceGroups: await readAzureResourceGroupRows(connection),
111
+ resources: await readAzureResourceRows(connection),
112
+ userAssignedManagedIdentities: await readAzureUserAssignedManagedIdentityRows(connection),
113
+ roleAssignments: await readAzureRoleAssignmentRows(connection),
114
+ activityLogs: await readAzureActivityLogRows(connection)
115
+ };
116
+ }
117
+
118
+ async function readRows<Row extends Record<string, unknown>>(
119
+ connection: DuckDBConnection,
120
+ sql: string
121
+ ): Promise<Row[]> {
122
+ const reader = await connection.runAndReadAll(sql);
123
+ return reader.getRowObjectsJson() as Row[];
124
+ }
125
+
126
+ function parseJsonObject(value: string | null | undefined): Record<string, unknown> {
127
+ return value ? JSON.parse(value) : {};
128
+ }
@@ -0,0 +1,441 @@
1
+ import type { DuckDBConnection } from "@duckdb/node-api";
2
+
3
+ import type {
4
+ AzureActivityLog,
5
+ AzureResource,
6
+ AzureResourceGroup,
7
+ AzureRoleAssignment,
8
+ AzureSubscription,
9
+ AzureUserAssignedManagedIdentity
10
+ } from "../../../../core/azure/resources";
11
+ import type { AzureActivityLog as AzureActivityLogInput } from "../../inputTransferObject/resources/AzureActivityLog";
12
+ import type { AzureResource as AzureResourceInput } from "../../inputTransferObject/resources/AzureResource";
13
+ import type { AzureResourceGroup as AzureResourceGroupInput } from "../../inputTransferObject/resources/AzureResourceGroup";
14
+ import type { AzureRoleAssignment as AzureRoleAssignmentInput } from "../../inputTransferObject/resources/AzureRoleAssignment";
15
+ import type { AzureSubscription as AzureSubscriptionInput } from "../../inputTransferObject/resources/AzureSubscription";
16
+ import type { AzureUserAssignedManagedIdentity as AzureUserAssignedManagedIdentityInput } from "../../inputTransferObject/resources/AzureUserAssignedManagedIdentity";
17
+
18
+ export async function insertAzureSubscriptionRows(
19
+ connection: DuckDBConnection,
20
+ subscriptions: AzureSubscriptionInput[]
21
+ ): Promise<void> {
22
+ for (const [ordinal, row] of subscriptions.entries()) {
23
+ await connection.run("insert into azure_subscriptions values ($ordinal, $subscriptionId, $subscriptionName, $tenantId, $state, $tags::json)", {
24
+ ordinal,
25
+ subscriptionId: row.subscriptionId,
26
+ subscriptionName: row.subscriptionName,
27
+ tenantId: row.tenantId,
28
+ state: row.state,
29
+ tags: JSON.stringify(row.tags ?? null)
30
+ });
31
+ }
32
+ }
33
+
34
+ export async function insertAzureResourceGroupRows(
35
+ connection: DuckDBConnection,
36
+ resourceGroups: AzureResourceGroupInput[]
37
+ ): Promise<void> {
38
+ for (const [ordinal, row] of resourceGroups.entries()) {
39
+ await connection.run(
40
+ "insert into azure_resource_groups values ($ordinal, $subscriptionId, $subscriptionName, $resourceGroup, $location, $tags::json)",
41
+ {
42
+ ordinal,
43
+ subscriptionId: row.subscriptionId,
44
+ subscriptionName: row.subscriptionName,
45
+ resourceGroup: row.resourceGroup,
46
+ location: row.location,
47
+ tags: JSON.stringify(row.tags ?? null)
48
+ }
49
+ );
50
+ }
51
+ }
52
+
53
+ export async function insertAzureResourceRows(connection: DuckDBConnection, resources: AzureResourceInput[]): Promise<void> {
54
+ for (const [ordinal, row] of resources.entries()) {
55
+ await connection.run(
56
+ `insert into azure_resources values (
57
+ $ordinal, $subscriptionId, $subscriptionName, $resourceId, $resourceName, $resourceGroup, $resourceType,
58
+ $kind, $location, $tags::json, $identityType, $identityPrincipalId, $identityTenantId,
59
+ $userAssignedIdentityResourceIds::json, $userAssignedIdentities::json
60
+ )`,
61
+ {
62
+ ordinal,
63
+ subscriptionId: row.subscriptionId,
64
+ subscriptionName: row.subscriptionName,
65
+ resourceId: row.resourceId,
66
+ resourceName: row.resourceName,
67
+ resourceGroup: row.resourceGroup,
68
+ resourceType: row.resourceType,
69
+ kind: row.kind,
70
+ location: row.location,
71
+ tags: JSON.stringify(row.tags ?? null),
72
+ identityType: row.identityType,
73
+ identityPrincipalId: row.identityPrincipalId,
74
+ identityTenantId: row.identityTenantId,
75
+ userAssignedIdentityResourceIds: JSON.stringify(row.userAssignedIdentityResourceIds ?? []),
76
+ userAssignedIdentities: JSON.stringify(row.userAssignedIdentities ?? null)
77
+ }
78
+ );
79
+ }
80
+ }
81
+
82
+ export async function insertAzureUserAssignedManagedIdentityRows(
83
+ connection: DuckDBConnection,
84
+ identities: AzureUserAssignedManagedIdentityInput[]
85
+ ): Promise<void> {
86
+ for (const [ordinal, row] of identities.entries()) {
87
+ await connection.run(
88
+ `insert into azure_user_assigned_managed_identities values (
89
+ $ordinal, $subscriptionId, $subscriptionName, $resourceId, $name, $resourceGroup, $location,
90
+ $clientId, $principalId, $tenantId, $tags::json
91
+ )`,
92
+ {
93
+ ordinal,
94
+ subscriptionId: row.subscriptionId,
95
+ subscriptionName: row.subscriptionName,
96
+ resourceId: row.resourceId,
97
+ name: row.name,
98
+ resourceGroup: row.resourceGroup,
99
+ location: row.location,
100
+ clientId: row.clientId,
101
+ principalId: row.principalId,
102
+ tenantId: row.tenantId,
103
+ tags: JSON.stringify(row.tags ?? null)
104
+ }
105
+ );
106
+ }
107
+ }
108
+
109
+ export async function insertAzureRoleAssignmentRows(
110
+ connection: DuckDBConnection,
111
+ assignments: AzureRoleAssignmentInput[]
112
+ ): Promise<void> {
113
+ for (const [ordinal, row] of assignments.entries()) {
114
+ await connection.run(
115
+ `insert into azure_role_assignments values (
116
+ $ordinal, $subscriptionId, $subscriptionName, $roleAssignmentId, $scope, $scopeType, $scopeSubscriptionId,
117
+ $scopeResourceGroup, $scopeResourceProvider, $scopeResourceType, $scopeResourceName, $scopeManagementGroup,
118
+ $principalId, $principalType, $principalDisplayName, $signInName, $roleDefinitionId, $roleDefinitionName,
119
+ $canDelegate, $condition, $conditionVersion
120
+ )`,
121
+ {
122
+ ordinal,
123
+ subscriptionId: row.subscriptionId,
124
+ subscriptionName: row.subscriptionName,
125
+ roleAssignmentId: row.roleAssignmentId,
126
+ scope: row.scope,
127
+ scopeType: row.scopeType ?? null,
128
+ scopeSubscriptionId: row.scopeSubscriptionId ?? null,
129
+ scopeResourceGroup: row.scopeResourceGroup ?? null,
130
+ scopeResourceProvider: row.scopeResourceProvider ?? null,
131
+ scopeResourceType: row.scopeResourceType ?? null,
132
+ scopeResourceName: row.scopeResourceName ?? null,
133
+ scopeManagementGroup: row.scopeManagementGroup ?? null,
134
+ principalId: row.principalId,
135
+ principalType: row.principalType,
136
+ principalDisplayName: row.principalDisplayName,
137
+ signInName: row.signInName,
138
+ roleDefinitionId: row.roleDefinitionId,
139
+ roleDefinitionName: row.roleDefinitionName,
140
+ canDelegate: row.canDelegate,
141
+ condition: row.condition,
142
+ conditionVersion: row.conditionVersion
143
+ }
144
+ );
145
+ }
146
+ }
147
+
148
+ export async function insertAzureActivityLogRows(connection: DuckDBConnection, logs: AzureActivityLogInput[]): Promise<void> {
149
+ for (const [ordinal, row] of logs.entries()) {
150
+ await connection.run(
151
+ `insert into azure_activity_logs values (
152
+ $ordinal, $subscriptionId, $subscriptionName, $eventTimestamp, $submissionTimestamp, $caller,
153
+ $callerUserPrincipalName, $callerName, $callerEmail, $callerObjectId, $callerIdentityType, $callerAppId,
154
+ $callerIpAddress, $callerTenantId, $operationName, $operationNameValue, $status, $subStatus, $category,
155
+ $resourceGroupName, $resourceId, $resourceProviderName, $resourceType, $authorizationAction, $authorizationScope
156
+ )`,
157
+ {
158
+ ordinal,
159
+ subscriptionId: row.subscriptionId,
160
+ subscriptionName: row.subscriptionName,
161
+ eventTimestamp: row.eventTimestamp,
162
+ submissionTimestamp: row.submissionTimestamp,
163
+ caller: row.caller,
164
+ callerUserPrincipalName: row.callerUserPrincipalName ?? null,
165
+ callerName: row.callerName ?? null,
166
+ callerEmail: row.callerEmail ?? null,
167
+ callerObjectId: row.callerObjectId ?? null,
168
+ callerIdentityType: row.callerIdentityType ?? null,
169
+ callerAppId: row.callerAppId ?? null,
170
+ callerIpAddress: row.callerIpAddress ?? null,
171
+ callerTenantId: row.callerTenantId ?? null,
172
+ operationName: row.operationName,
173
+ operationNameValue: row.operationNameValue,
174
+ status: row.status,
175
+ subStatus: row.subStatus,
176
+ category: row.category,
177
+ resourceGroupName: row.resourceGroupName,
178
+ resourceId: row.resourceId,
179
+ resourceProviderName: row.resourceProviderName,
180
+ resourceType: row.resourceType,
181
+ authorizationAction: row.authorizationAction,
182
+ authorizationScope: row.authorizationScope
183
+ }
184
+ );
185
+ }
186
+ }
187
+
188
+ export async function readAzureSubscriptionRows(connection: DuckDBConnection): Promise<AzureSubscription[]> {
189
+ return (await readRows<AzureSubscriptionRow>(
190
+ connection,
191
+ "select subscription_id, subscription_name, tenant_id, state, tags from azure_subscriptions order by ordinal"
192
+ )).map((row) => ({
193
+ subscriptionId: row.subscription_id,
194
+ subscriptionName: row.subscription_name,
195
+ tenantId: row.tenant_id,
196
+ state: row.state,
197
+ tags: parseJsonObject(row.tags)
198
+ }));
199
+ }
200
+
201
+ export async function readAzureResourceGroupRows(connection: DuckDBConnection): Promise<AzureResourceGroup[]> {
202
+ return (await readRows<AzureResourceGroupRow>(
203
+ connection,
204
+ "select subscription_id, subscription_name, resource_group, location, tags from azure_resource_groups order by ordinal"
205
+ )).map((row) => ({
206
+ subscriptionId: row.subscription_id,
207
+ subscriptionName: row.subscription_name,
208
+ resourceGroup: row.resource_group,
209
+ location: row.location,
210
+ tags: parseJsonObject(row.tags)
211
+ }));
212
+ }
213
+
214
+ export async function readAzureResourceRows(connection: DuckDBConnection): Promise<AzureResource[]> {
215
+ return (await readRows<AzureResourceRow>(
216
+ connection,
217
+ `select subscription_id, subscription_name, resource_id, resource_name, resource_group, resource_type, kind, location,
218
+ tags, identity_type, identity_principal_id, identity_tenant_id, user_assigned_identity_resource_ids, user_assigned_identities
219
+ from azure_resources order by ordinal`
220
+ )).map((row) => ({
221
+ subscriptionId: row.subscription_id,
222
+ subscriptionName: row.subscription_name,
223
+ resourceId: row.resource_id,
224
+ resourceName: row.resource_name,
225
+ resourceGroup: row.resource_group,
226
+ resourceType: row.resource_type,
227
+ kind: row.kind,
228
+ location: row.location,
229
+ tags: parseJsonObject(row.tags),
230
+ identityType: row.identity_type,
231
+ identityPrincipalId: row.identity_principal_id,
232
+ identityTenantId: row.identity_tenant_id,
233
+ userAssignedIdentityResourceIds: parseJsonArray<string>(row.user_assigned_identity_resource_ids),
234
+ userAssignedIdentities: parseJsonValue(row.user_assigned_identities)
235
+ }));
236
+ }
237
+
238
+ export async function readAzureUserAssignedManagedIdentityRows(
239
+ connection: DuckDBConnection
240
+ ): Promise<AzureUserAssignedManagedIdentity[]> {
241
+ return (await readRows<AzureUserAssignedManagedIdentityRow>(
242
+ connection,
243
+ `select subscription_id, subscription_name, resource_id, name, resource_group, location, client_id, principal_id, tenant_id, tags
244
+ from azure_user_assigned_managed_identities order by ordinal`
245
+ )).map((row) => ({
246
+ subscriptionId: row.subscription_id,
247
+ subscriptionName: row.subscription_name,
248
+ resourceId: row.resource_id,
249
+ name: row.name,
250
+ resourceGroup: row.resource_group,
251
+ location: row.location,
252
+ clientId: row.client_id,
253
+ principalId: row.principal_id,
254
+ tenantId: row.tenant_id,
255
+ tags: parseJsonObject(row.tags)
256
+ }));
257
+ }
258
+
259
+ export async function readAzureRoleAssignmentRows(connection: DuckDBConnection): Promise<AzureRoleAssignment[]> {
260
+ return (await readRows<AzureRoleAssignmentRow>(
261
+ connection,
262
+ `select subscription_id, subscription_name, role_assignment_id, scope, scope_type, scope_subscription_id,
263
+ scope_resource_group, scope_resource_provider, scope_resource_type, scope_resource_name, scope_management_group,
264
+ principal_id, principal_type, principal_display_name, sign_in_name, role_definition_id, role_definition_name,
265
+ can_delegate, condition, condition_version
266
+ from azure_role_assignments order by ordinal`
267
+ )).map((row) => ({
268
+ subscriptionId: row.subscription_id,
269
+ subscriptionName: row.subscription_name,
270
+ roleAssignmentId: row.role_assignment_id,
271
+ scope: row.scope,
272
+ scopeType: row.scope_type,
273
+ scopeSubscriptionId: row.scope_subscription_id,
274
+ scopeResourceGroup: row.scope_resource_group,
275
+ scopeResourceProvider: row.scope_resource_provider,
276
+ scopeResourceType: row.scope_resource_type,
277
+ scopeResourceName: row.scope_resource_name,
278
+ scopeManagementGroup: row.scope_management_group,
279
+ principalId: row.principal_id,
280
+ principalType: row.principal_type,
281
+ principalDisplayName: row.principal_display_name,
282
+ signInName: row.sign_in_name,
283
+ roleDefinitionId: row.role_definition_id,
284
+ roleDefinitionName: row.role_definition_name,
285
+ canDelegate: row.can_delegate,
286
+ condition: row.condition,
287
+ conditionVersion: row.condition_version
288
+ }));
289
+ }
290
+
291
+ export async function readAzureActivityLogRows(connection: DuckDBConnection): Promise<AzureActivityLog[]> {
292
+ return (await readRows<AzureActivityLogRow>(
293
+ connection,
294
+ `select subscription_id, subscription_name, event_timestamp, submission_timestamp, caller, caller_user_principal_name,
295
+ caller_name, caller_email, caller_object_id, caller_identity_type, caller_app_id, caller_ip_address, caller_tenant_id,
296
+ operation_name, operation_name_value, status, sub_status, category, resource_group_name, resource_id,
297
+ resource_provider_name, resource_type, authorization_action, authorization_scope
298
+ from azure_activity_logs order by ordinal`
299
+ )).map((row) => ({
300
+ subscriptionId: row.subscription_id,
301
+ subscriptionName: row.subscription_name,
302
+ eventTimestamp: row.event_timestamp,
303
+ submissionTimestamp: row.submission_timestamp,
304
+ caller: row.caller,
305
+ callerUserPrincipalName: row.caller_user_principal_name,
306
+ callerName: row.caller_name,
307
+ callerEmail: row.caller_email,
308
+ callerObjectId: row.caller_object_id,
309
+ callerIdentityType: row.caller_identity_type,
310
+ callerAppId: row.caller_app_id,
311
+ callerIpAddress: row.caller_ip_address,
312
+ callerTenantId: row.caller_tenant_id,
313
+ operationName: row.operation_name,
314
+ operationNameValue: row.operation_name_value,
315
+ status: row.status,
316
+ subStatus: row.sub_status,
317
+ category: row.category,
318
+ resourceGroupName: row.resource_group_name,
319
+ resourceId: row.resource_id,
320
+ resourceProviderName: row.resource_provider_name,
321
+ resourceType: row.resource_type,
322
+ authorizationAction: row.authorization_action,
323
+ authorizationScope: row.authorization_scope
324
+ }));
325
+ }
326
+
327
+ type AzureSubscriptionRow = {
328
+ subscription_id: string;
329
+ subscription_name: string;
330
+ tenant_id: string;
331
+ state: AzureSubscription["state"];
332
+ tags: string | null;
333
+ };
334
+
335
+ type AzureResourceGroupRow = {
336
+ subscription_id: string;
337
+ subscription_name: string;
338
+ resource_group: string;
339
+ location: string;
340
+ tags: string | null;
341
+ };
342
+
343
+ type AzureResourceRow = {
344
+ subscription_id: string;
345
+ subscription_name: string;
346
+ resource_id: string;
347
+ resource_name: string;
348
+ resource_group: string;
349
+ resource_type: string;
350
+ kind: string | null;
351
+ location: string;
352
+ tags: string | null;
353
+ identity_type: string | null;
354
+ identity_principal_id: string | null;
355
+ identity_tenant_id: string | null;
356
+ user_assigned_identity_resource_ids: string;
357
+ user_assigned_identities: string | null;
358
+ };
359
+
360
+ type AzureUserAssignedManagedIdentityRow = {
361
+ subscription_id: string;
362
+ subscription_name: string;
363
+ resource_id: string;
364
+ name: string;
365
+ resource_group: string;
366
+ location: string;
367
+ client_id: string;
368
+ principal_id: string;
369
+ tenant_id: string;
370
+ tags: string | null;
371
+ };
372
+
373
+ type AzureRoleAssignmentRow = {
374
+ subscription_id: string;
375
+ subscription_name: string;
376
+ role_assignment_id: string | null;
377
+ scope: string;
378
+ scope_type: AzureRoleAssignment["scopeType"];
379
+ scope_subscription_id: string | null;
380
+ scope_resource_group: string | null;
381
+ scope_resource_provider: string | null;
382
+ scope_resource_type: string | null;
383
+ scope_resource_name: string | null;
384
+ scope_management_group: string | null;
385
+ principal_id: string;
386
+ principal_type: string | null;
387
+ principal_display_name: string | null;
388
+ sign_in_name: string | null;
389
+ role_definition_id: string | null;
390
+ role_definition_name: string | null;
391
+ can_delegate: boolean | null;
392
+ condition: string | null;
393
+ condition_version: string | null;
394
+ };
395
+
396
+ type AzureActivityLogRow = {
397
+ subscription_id: string;
398
+ subscription_name: string;
399
+ event_timestamp: string;
400
+ submission_timestamp: string | null;
401
+ caller: string | null;
402
+ caller_user_principal_name: string | null;
403
+ caller_name: string | null;
404
+ caller_email: string | null;
405
+ caller_object_id: string | null;
406
+ caller_identity_type: string | null;
407
+ caller_app_id: string | null;
408
+ caller_ip_address: string | null;
409
+ caller_tenant_id: string | null;
410
+ operation_name: string | null;
411
+ operation_name_value: string | null;
412
+ status: string | null;
413
+ sub_status: string | null;
414
+ category: string | null;
415
+ resource_group_name: string | null;
416
+ resource_id: string | null;
417
+ resource_provider_name: string | null;
418
+ resource_type: string | null;
419
+ authorization_action: string | null;
420
+ authorization_scope: string | null;
421
+ };
422
+
423
+ async function readRows<Row extends Record<string, unknown>>(
424
+ connection: DuckDBConnection,
425
+ sql: string
426
+ ): Promise<Row[]> {
427
+ const reader = await connection.runAndReadAll(sql);
428
+ return reader.getRowObjectsJson() as Row[];
429
+ }
430
+
431
+ function parseJsonArray<T>(value: string | null | undefined): T[] {
432
+ return value ? JSON.parse(value) : [];
433
+ }
434
+
435
+ function parseJsonObject<T extends Record<string, string>>(value: string | null | undefined): T | null {
436
+ return value ? JSON.parse(value) : null;
437
+ }
438
+
439
+ function parseJsonValue(value: string | null | undefined): unknown {
440
+ return value ? JSON.parse(value) : null;
441
+ }