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,175 @@
1
+ import type { DuckDBConnection } from "@duckdb/node-api";
2
+
3
+ import type { EntraApplication } from "../../inputTransferObject/entra/EntraApplication";
4
+
5
+ export async function insertEntraApplicationRows(
6
+ connection: DuckDBConnection,
7
+ applications: EntraApplication[] = []
8
+ ): Promise<void> {
9
+ for (const [ordinal, application] of applications.entries()) {
10
+ await connection.run(
11
+ `insert into entra_applications values (
12
+ $ordinal,
13
+ $id,
14
+ $appId,
15
+ $displayName,
16
+ $signInAudience,
17
+ $publisherDomain,
18
+ $identifierUris::json,
19
+ $tags::json,
20
+ $appRoles::json,
21
+ $oauth2PermissionScopes::json,
22
+ $requiredResourceAccess::json,
23
+ $web::json,
24
+ $spa::json,
25
+ $publicClient::json,
26
+ $passwordCredentials::json,
27
+ $keyCredentials::json,
28
+ $createdDateTime,
29
+ $deletedDateTime,
30
+ $disabledByMicrosoftStatus,
31
+ $info::json,
32
+ $notes,
33
+ $owners::json
34
+ )`,
35
+ {
36
+ ordinal,
37
+ id: application.id,
38
+ appId: application.appId,
39
+ displayName: application.displayName,
40
+ signInAudience: application.signInAudience,
41
+ publisherDomain: application.publisherDomain,
42
+ identifierUris: JSON.stringify(application.identifierUris ?? []),
43
+ tags: JSON.stringify(application.tags ?? []),
44
+ appRoles: JSON.stringify(application.appRoles ?? []),
45
+ oauth2PermissionScopes: JSON.stringify(application.oauth2PermissionScopes ?? []),
46
+ requiredResourceAccess: JSON.stringify(application.requiredResourceAccess ?? []),
47
+ web: JSON.stringify(application.web ?? null),
48
+ spa: JSON.stringify(application.spa ?? null),
49
+ publicClient: JSON.stringify(application.publicClient ?? null),
50
+ passwordCredentials: JSON.stringify(stripSecretText(application.passwordCredentials ?? [])),
51
+ keyCredentials: JSON.stringify(stripSecretText(application.keyCredentials ?? [])),
52
+ createdDateTime: application.createdDateTime,
53
+ deletedDateTime: application.deletedDateTime,
54
+ disabledByMicrosoftStatus: application.disabledByMicrosoftStatus,
55
+ info: JSON.stringify(application.info ?? null),
56
+ notes: application.notes,
57
+ owners: JSON.stringify(application.owners ?? [])
58
+ }
59
+ );
60
+ }
61
+ }
62
+
63
+ export async function readEntraApplicationRows(connection: DuckDBConnection): Promise<EntraApplication[]> {
64
+ const rows = await readRows<EntraApplicationRow>(
65
+ connection,
66
+ `select
67
+ id,
68
+ app_id,
69
+ display_name,
70
+ sign_in_audience,
71
+ publisher_domain,
72
+ identifier_uris,
73
+ tags,
74
+ app_roles,
75
+ oauth2_permission_scopes,
76
+ required_resource_access,
77
+ web,
78
+ spa,
79
+ public_client,
80
+ password_credentials,
81
+ key_credentials,
82
+ created_date_time,
83
+ deleted_date_time,
84
+ disabled_by_microsoft_status,
85
+ info,
86
+ notes,
87
+ owners
88
+ from entra_applications
89
+ order by ordinal`
90
+ );
91
+
92
+ return rows.map(mapApplicationRow);
93
+ }
94
+
95
+ type EntraApplicationRow = {
96
+ id: string;
97
+ app_id: string;
98
+ display_name: string;
99
+ sign_in_audience: string | null;
100
+ publisher_domain: string | null;
101
+ identifier_uris: string;
102
+ tags: string;
103
+ app_roles: string;
104
+ oauth2_permission_scopes: string;
105
+ required_resource_access: string;
106
+ web: string | null;
107
+ spa: string | null;
108
+ public_client: string | null;
109
+ password_credentials: string;
110
+ key_credentials: string;
111
+ created_date_time: string | null;
112
+ deleted_date_time: string | null;
113
+ disabled_by_microsoft_status: string | null;
114
+ info: string | null;
115
+ notes: string | null;
116
+ owners: string;
117
+ };
118
+
119
+ function mapApplicationRow(row: EntraApplicationRow): EntraApplication {
120
+ return {
121
+ id: row.id,
122
+ appId: row.app_id,
123
+ displayName: row.display_name,
124
+ signInAudience: row.sign_in_audience,
125
+ publisherDomain: row.publisher_domain,
126
+ identifierUris: parseJsonArray<string>(row.identifier_uris),
127
+ tags: parseJsonArray<string>(row.tags),
128
+ appRoles: parseJsonArray(row.app_roles),
129
+ oauth2PermissionScopes: parseJsonArray(row.oauth2_permission_scopes),
130
+ requiredResourceAccess: parseJsonArray(row.required_resource_access),
131
+ web: parseNullableJsonObject(row.web),
132
+ spa: parseNullableJsonObject(row.spa),
133
+ publicClient: parseNullableJsonObject(row.public_client),
134
+ passwordCredentials: parseJsonArray(row.password_credentials),
135
+ keyCredentials: parseJsonArray(row.key_credentials),
136
+ createdDateTime: row.created_date_time,
137
+ deletedDateTime: row.deleted_date_time,
138
+ disabledByMicrosoftStatus: row.disabled_by_microsoft_status,
139
+ info: parseNullableJsonObject(row.info),
140
+ notes: row.notes,
141
+ owners: parseJsonArray(row.owners)
142
+ };
143
+ }
144
+
145
+ async function readRows<Row extends Record<string, unknown>>(
146
+ connection: DuckDBConnection,
147
+ sql: string
148
+ ): Promise<Row[]> {
149
+ const reader = await connection.runAndReadAll(sql);
150
+ return reader.getRowObjectsJson() as Row[];
151
+ }
152
+
153
+ function parseJsonArray<T>(value: string | null | undefined): T[] {
154
+ return value ? JSON.parse(value) : [];
155
+ }
156
+
157
+ function parseNullableJsonObject(value: string | null | undefined): Record<string, unknown> | null {
158
+ return value ? JSON.parse(value) : null;
159
+ }
160
+
161
+ function stripSecretText(value: unknown): unknown {
162
+ if (Array.isArray(value)) {
163
+ return value.map(stripSecretText);
164
+ }
165
+
166
+ if (!value || typeof value !== "object") {
167
+ return value;
168
+ }
169
+
170
+ return Object.fromEntries(
171
+ Object.entries(value)
172
+ .filter(([key]) => key.toLowerCase() !== "secrettext")
173
+ .map(([key, entryValue]) => [key, stripSecretText(entryValue)])
174
+ );
175
+ }
@@ -0,0 +1,63 @@
1
+ import type {
2
+ EntraAppRole as CoreEntraAppRole,
3
+ EntraOwner as CoreEntraOwner,
4
+ EntraServicePrincipal as CoreEntraServicePrincipal
5
+ } from "../../../../core/azure/entra/types";
6
+ import type {
7
+ EntraAppRole,
8
+ EntraOwner,
9
+ EntraServicePrincipal
10
+ } from "../../inputTransferObject/entra/EntraServicePrincipal";
11
+
12
+ export function mapEntraServicePrincipalToCore(
13
+ servicePrincipal: EntraServicePrincipal
14
+ ): CoreEntraServicePrincipal {
15
+ return {
16
+ id: servicePrincipal.id,
17
+ appId: servicePrincipal.appId,
18
+ displayName: servicePrincipal.displayName,
19
+ appDisplayName: servicePrincipal.appDisplayName,
20
+ servicePrincipalType: servicePrincipal.servicePrincipalType,
21
+ publisherName: servicePrincipal.publisherName,
22
+ accountEnabled: servicePrincipal.accountEnabled,
23
+ appOwnerOrganizationId: servicePrincipal.appOwnerOrganizationId,
24
+ homepage: servicePrincipal.homepage,
25
+ loginUrl: servicePrincipal.loginUrl,
26
+ replyUrls: [...servicePrincipal.replyUrls],
27
+ servicePrincipalNames: [...servicePrincipal.servicePrincipalNames],
28
+ tags: [...servicePrincipal.tags],
29
+ appRoles: servicePrincipal.appRoles?.map(mapEntraAppRoleToCore),
30
+ owners: servicePrincipal.owners?.map(mapEntraOwnerToCore),
31
+ appOwners: servicePrincipal.appOwners?.map(mapEntraOwnerToCore),
32
+ servicePrincipalOwners: servicePrincipal.servicePrincipalOwners?.map(mapEntraOwnerToCore),
33
+ applicationOwners: servicePrincipal.applicationOwners?.map(mapEntraOwnerToCore),
34
+ metadata: servicePrincipal.metadata ? { ...servicePrincipal.metadata } : servicePrincipal.metadata
35
+ };
36
+ }
37
+
38
+ export function mapEntraServicePrincipalsToCore(
39
+ servicePrincipals: EntraServicePrincipal[]
40
+ ): CoreEntraServicePrincipal[] {
41
+ return servicePrincipals.map(mapEntraServicePrincipalToCore);
42
+ }
43
+
44
+ function mapEntraAppRoleToCore(appRole: EntraAppRole): CoreEntraAppRole {
45
+ return {
46
+ id: appRole.id,
47
+ value: appRole.value,
48
+ displayName: appRole.displayName,
49
+ description: appRole.description,
50
+ isEnabled: appRole.isEnabled,
51
+ allowedMemberTypes: [...appRole.allowedMemberTypes]
52
+ };
53
+ }
54
+
55
+ function mapEntraOwnerToCore(owner: EntraOwner): CoreEntraOwner {
56
+ return {
57
+ id: owner.id,
58
+ displayName: owner.displayName,
59
+ userPrincipalName: owner.userPrincipalName,
60
+ mail: owner.mail,
61
+ ownerType: owner.ownerType
62
+ };
63
+ }
@@ -0,0 +1,41 @@
1
+ import type { RuntimeRestEndpoint } from "../../../../core/runtime/rest";
2
+ import { RuntimeHttpError } from "../../../../core/runtime/localSnapshotFiles";
3
+ import type { LocalReportRuntime } from "../LocalReportRuntime";
4
+ import { parseRuntimeCollectionQueryOptions } from "../runtimeRestQuery";
5
+
6
+ export function defineEntraLocalReportRuntimeRestEndpoints(
7
+ runtime: LocalReportRuntime,
8
+ restBasePath: string
9
+ ): RuntimeRestEndpoint[] {
10
+ return [
11
+ {
12
+ path: `${restBasePath}/entra/servicePrincipals`,
13
+ handle: ({ url }) => runtime.queryEntraServicePrincipals(parseRuntimeCollectionQueryOptions(url))
14
+ },
15
+ {
16
+ path: `${restBasePath}/entra/managedIdentities`,
17
+ handle: ({ url }) => runtime.queryEntraManagedIdentities(parseRuntimeCollectionQueryOptions(url))
18
+ },
19
+ {
20
+ path: `${restBasePath}/entra/permissions`,
21
+ handle: ({ url }) => runtime.readEntraPrincipalPermissions(readRequiredSearchParam(url, "principalId"))
22
+ },
23
+ {
24
+ path: `${restBasePath}/entra/oauth2PermissionGrants`,
25
+ handle: ({ url }) => runtime.queryEntraOAuth2PermissionGrants(parseRuntimeCollectionQueryOptions(url))
26
+ },
27
+ {
28
+ path: `${restBasePath}/entra/appRoleAssignments`,
29
+ handle: ({ url }) => runtime.queryEntraAppRoleAssignments(parseRuntimeCollectionQueryOptions(url))
30
+ }
31
+ ];
32
+ }
33
+
34
+ function readRequiredSearchParam(url: URL, name: string): string {
35
+ const value = url.searchParams.get(name)?.trim();
36
+ if (!value) {
37
+ throw new RuntimeHttpError(`Missing required query parameter: ${name}`, 400);
38
+ }
39
+
40
+ return value;
41
+ }
@@ -0,0 +1,48 @@
1
+ import type { DuckDBConnection } from "@duckdb/node-api";
2
+
3
+ import type { EntraOAuth2PermissionGrant } from "../../inputTransferObject/entra/EntraOAuth2PermissionGrant";
4
+
5
+ export async function insertEntraOAuth2PermissionGrantRows(
6
+ connection: DuckDBConnection,
7
+ oauth2PermissionGrants: EntraOAuth2PermissionGrant[] = []
8
+ ): Promise<void> {
9
+ for (const [ordinal, grant] of oauth2PermissionGrants.entries()) {
10
+ await connection.run(
11
+ `insert into entra_oauth2_permission_grants values (
12
+ $ordinal,
13
+ $id,
14
+ $clientId,
15
+ $consentType,
16
+ $principalId,
17
+ $resourceId,
18
+ $scope
19
+ )`,
20
+ { ordinal, ...grant }
21
+ );
22
+ }
23
+ }
24
+
25
+ export async function readEntraOAuth2PermissionGrantRows(
26
+ connection: DuckDBConnection
27
+ ): Promise<EntraOAuth2PermissionGrant[]> {
28
+ return readRows<EntraOAuth2PermissionGrant>(
29
+ connection,
30
+ `select
31
+ id,
32
+ client_id as clientId,
33
+ consent_type as consentType,
34
+ principal_id as principalId,
35
+ resource_id as resourceId,
36
+ scope
37
+ from entra_oauth2_permission_grants
38
+ order by ordinal`
39
+ );
40
+ }
41
+
42
+ async function readRows<Row extends Record<string, unknown>>(
43
+ connection: DuckDBConnection,
44
+ sql: string
45
+ ): Promise<Row[]> {
46
+ const reader = await connection.runAndReadAll(sql);
47
+ return reader.getRowObjectsJson() as Row[];
48
+ }
@@ -0,0 +1,173 @@
1
+ import type {
2
+ LatestAzureIdentityEnrichment,
3
+ ManagedIdentityPermissionRiskSummary
4
+ } from "../../../../core/azure/identityEnrichment";
5
+ import type { AzureRoleAssignment } from "../../../../core/azure/resources";
6
+ import type { ZtaRemediationSummary } from "../../../../core/azure/ztaReport";
7
+ import { isManagedIdentity, type ManagedIdentity } from "../../../../core/azure/entra/managedIdentity";
8
+ import {
9
+ isServicePrincipal,
10
+ type AzureIdentityRuntimeEnrichment,
11
+ type EntraPrincipalPermissionSummary,
12
+ type EntraPrincipalRbacSummary,
13
+ type ServicePrincipal
14
+ } from "../../../../core/azure/entra/servicePrincipal";
15
+ import type { EntraServicePrincipal } from "../../../../core/azure/entra/types";
16
+
17
+ export function toServicePrincipals(
18
+ servicePrincipals: EntraServicePrincipal[],
19
+ enrichment?: LatestAzureIdentityEnrichment,
20
+ permissionsByPrincipalId: Map<string, EntraPrincipalPermissionSummary> = new Map(),
21
+ ztaSummariesByPrincipalId: Map<string, ZtaRemediationSummary> = new Map()
22
+ ): ServicePrincipal[] {
23
+ return servicePrincipals.filter(isServicePrincipal).map((servicePrincipal) => ({
24
+ ...servicePrincipal,
25
+ ...getAzureIdentityRuntimeEnrichment(servicePrincipal, enrichment),
26
+ ...getEntraPrincipalPermissionSummary(servicePrincipal, permissionsByPrincipalId),
27
+ ...getZtaRemediationSummary(servicePrincipal, ztaSummariesByPrincipalId)
28
+ }));
29
+ }
30
+
31
+ export function toManagedIdentities(
32
+ servicePrincipals: EntraServicePrincipal[],
33
+ enrichment?: LatestAzureIdentityEnrichment,
34
+ permissionsByPrincipalId: Map<string, EntraPrincipalPermissionSummary> = new Map(),
35
+ ztaSummariesByPrincipalId: Map<string, ZtaRemediationSummary> = new Map()
36
+ ): ManagedIdentity[] {
37
+ return servicePrincipals.filter(isManagedIdentity).map((servicePrincipal) => {
38
+ const assignmentEnrichment = enrichment?.managedIdentityAssignmentsByServicePrincipalId.get(
39
+ servicePrincipal.id.toLowerCase()
40
+ );
41
+
42
+ return {
43
+ ...servicePrincipal,
44
+ ...getAzureIdentityRuntimeEnrichment(servicePrincipal, enrichment),
45
+ ...getEntraPrincipalPermissionSummary(servicePrincipal, permissionsByPrincipalId),
46
+ ...getZtaRemediationSummary(servicePrincipal, ztaSummariesByPrincipalId),
47
+ managedIdentityAssignments: assignmentEnrichment?.managedIdentityAssignments ?? [],
48
+ assignedResourceGroups: assignmentEnrichment?.assignedResourceGroups ?? []
49
+ };
50
+ });
51
+ }
52
+
53
+ function getZtaRemediationSummary(
54
+ servicePrincipal: EntraServicePrincipal,
55
+ ztaSummariesByPrincipalId: Map<string, ZtaRemediationSummary>
56
+ ): ZtaRemediationSummary {
57
+ return ztaSummariesByPrincipalId.get(servicePrincipal.id.toLowerCase()) ?? createEmptyZtaRemediationSummary();
58
+ }
59
+
60
+ function getEntraPrincipalPermissionSummary(
61
+ servicePrincipal: EntraServicePrincipal,
62
+ permissionsByPrincipalId: Map<string, EntraPrincipalPermissionSummary>
63
+ ): EntraPrincipalPermissionSummary {
64
+ return permissionsByPrincipalId.get(servicePrincipal.id.toLowerCase()) ?? createEmptyPermissionSummary();
65
+ }
66
+
67
+ function createEmptyPermissionSummary(): EntraPrincipalPermissionSummary {
68
+ return {
69
+ oauthPemrissionsCount: 0,
70
+ appRolesPermissionCount: 0,
71
+ entraPermissionRisk: "none"
72
+ };
73
+ }
74
+
75
+ function createEmptyZtaRemediationSummary(): ZtaRemediationSummary {
76
+ return {
77
+ ztaRemediationCountAll: 0,
78
+ ztaRemediationFailedCount: 0,
79
+ ztaMaxRisk: "none"
80
+ };
81
+ }
82
+
83
+ function getAzureIdentityRuntimeEnrichment(
84
+ servicePrincipal: EntraServicePrincipal,
85
+ enrichment?: LatestAzureIdentityEnrichment
86
+ ): AzureIdentityRuntimeEnrichment {
87
+ const roleAssignments =
88
+ enrichment?.roleAssignmentsByPrincipalId.get(servicePrincipal.id.toLowerCase())?.roleAssignments ?? [];
89
+ const permissionRisk =
90
+ enrichment?.accessRiskByPrincipalId.get(servicePrincipal.id.toLowerCase()) ?? createRiskSummary(servicePrincipal.id);
91
+
92
+ return {
93
+ permissionRisk: permissionRisk.riskLevel,
94
+ azureRbac: formatAzureRbac(permissionRisk, roleAssignments),
95
+ roleAssignments,
96
+ ...createRbacSummary(permissionRisk, roleAssignments)
97
+ };
98
+ }
99
+
100
+ function createRiskSummary(principalId: string): ManagedIdentityPermissionRiskSummary {
101
+ return {
102
+ principalId,
103
+ riskLevel: "none",
104
+ assignmentCount: 0,
105
+ highRiskAssignmentCount: 0,
106
+ broadScopeAssignmentCount: 0,
107
+ roleAssignments: []
108
+ };
109
+ }
110
+
111
+ function formatAzureRbac(
112
+ permissionRisk: ManagedIdentityPermissionRiskSummary,
113
+ roleAssignments: AzureRoleAssignment[]
114
+ ): string {
115
+ if (permissionRisk.assignmentCount > 0) {
116
+ return permissionRisk.roleAssignments
117
+ .map((assignment) => {
118
+ const reasons = assignment.reasons.length > 0 ? ` (${assignment.reasons.join(", ")})` : "";
119
+ return `${assignment.roleDefinitionName ?? "Role"} on ${formatScope(assignment.scope)}${reasons}`;
120
+ })
121
+ .join(", ");
122
+ }
123
+
124
+ if (roleAssignments.length > 0) {
125
+ return roleAssignments
126
+ .map((assignment) => `${assignment.roleDefinitionName ?? "Role"} on ${formatScope(assignment.scope)}`)
127
+ .join(", ");
128
+ }
129
+
130
+ return "No Azure RBAC assignments";
131
+ }
132
+
133
+ function formatScope(scope: string): string {
134
+ const resourceGroupMatch = scope.match(/\/resourceGroups\/([^/]+)/i);
135
+ if (resourceGroupMatch) {
136
+ return `rg/${resourceGroupMatch[1]}`;
137
+ }
138
+
139
+ const subscriptionMatch = scope.match(/^\/subscriptions\/([^/]+)$/i);
140
+ if (subscriptionMatch) {
141
+ return "subscription";
142
+ }
143
+
144
+ return scope.split("/").filter(Boolean).slice(-2).join("/") || scope;
145
+ }
146
+
147
+ function createRbacSummary(
148
+ permissionRisk: ManagedIdentityPermissionRiskSummary,
149
+ roleAssignments: AzureRoleAssignment[]
150
+ ): EntraPrincipalRbacSummary {
151
+ return {
152
+ rbacRoleAssignmentCount: roleAssignments.length,
153
+ rbacRoleLevel: permissionRisk.riskLevel,
154
+ rbacSubscriptionCount: countRbacSubscriptions(roleAssignments)
155
+ };
156
+ }
157
+
158
+ function countRbacSubscriptions(roleAssignments: AzureRoleAssignment[]): number {
159
+ const subscriptionIds = new Set<string>();
160
+
161
+ for (const assignment of roleAssignments) {
162
+ const subscriptionId = getRoleAssignmentSubscriptionId(assignment);
163
+ if (subscriptionId) {
164
+ subscriptionIds.add(subscriptionId.toLowerCase());
165
+ }
166
+ }
167
+
168
+ return subscriptionIds.size;
169
+ }
170
+
171
+ function getRoleAssignmentSubscriptionId(assignment: AzureRoleAssignment): string | null {
172
+ return assignment.scopeSubscriptionId ?? assignment.subscriptionId ?? assignment.scope.match(/\/subscriptions\/([^/]+)/i)?.[1] ?? null;
173
+ }
@@ -0,0 +1,149 @@
1
+ import type { DuckDBConnection } from "@duckdb/node-api";
2
+
3
+ import type { EntraServicePrincipal } from "../../inputTransferObject/entra/EntraServicePrincipal";
4
+
5
+ export async function insertEntraServicePrincipalRows(
6
+ connection: DuckDBConnection,
7
+ servicePrincipals: EntraServicePrincipal[]
8
+ ): Promise<void> {
9
+ for (const [ordinal, servicePrincipal] of servicePrincipals.entries()) {
10
+ await connection.run(
11
+ `insert into entra_service_principals values (
12
+ $ordinal,
13
+ $id,
14
+ $appId,
15
+ $displayName,
16
+ $appDisplayName,
17
+ $servicePrincipalType,
18
+ $publisherName,
19
+ $accountEnabled,
20
+ $appOwnerOrganizationId,
21
+ $homepage,
22
+ $loginUrl,
23
+ $replyUrls::json,
24
+ $servicePrincipalNames::json,
25
+ $tags::json,
26
+ $appRoles::json,
27
+ $owners::json,
28
+ $appOwners::json,
29
+ $servicePrincipalOwners::json,
30
+ $applicationOwners::json,
31
+ $metadata::json
32
+ )`,
33
+ {
34
+ ordinal,
35
+ id: servicePrincipal.id,
36
+ appId: servicePrincipal.appId,
37
+ displayName: servicePrincipal.displayName,
38
+ appDisplayName: servicePrincipal.appDisplayName,
39
+ servicePrincipalType: servicePrincipal.servicePrincipalType,
40
+ publisherName: servicePrincipal.publisherName,
41
+ accountEnabled: servicePrincipal.accountEnabled,
42
+ appOwnerOrganizationId: servicePrincipal.appOwnerOrganizationId,
43
+ homepage: servicePrincipal.homepage,
44
+ loginUrl: servicePrincipal.loginUrl,
45
+ replyUrls: JSON.stringify(servicePrincipal.replyUrls),
46
+ servicePrincipalNames: JSON.stringify(servicePrincipal.servicePrincipalNames),
47
+ tags: JSON.stringify(servicePrincipal.tags),
48
+ appRoles: JSON.stringify(servicePrincipal.appRoles ?? []),
49
+ owners: JSON.stringify(servicePrincipal.owners ?? []),
50
+ appOwners: JSON.stringify(servicePrincipal.appOwners ?? []),
51
+ servicePrincipalOwners: JSON.stringify(servicePrincipal.servicePrincipalOwners ?? []),
52
+ applicationOwners: JSON.stringify(servicePrincipal.applicationOwners ?? []),
53
+ metadata: JSON.stringify(servicePrincipal.metadata ?? null)
54
+ }
55
+ );
56
+ }
57
+ }
58
+
59
+ export async function readEntraServicePrincipalRows(connection: DuckDBConnection): Promise<EntraServicePrincipal[]> {
60
+ const rows = await readRows<EntraServicePrincipalRow>(
61
+ connection,
62
+ `select
63
+ id,
64
+ app_id,
65
+ display_name,
66
+ app_display_name,
67
+ service_principal_type,
68
+ publisher_name,
69
+ account_enabled,
70
+ app_owner_organization_id,
71
+ homepage,
72
+ login_url,
73
+ reply_urls,
74
+ service_principal_names,
75
+ tags,
76
+ app_roles,
77
+ owners,
78
+ app_owners,
79
+ service_principal_owners,
80
+ application_owners,
81
+ metadata
82
+ from entra_service_principals
83
+ order by ordinal`
84
+ );
85
+
86
+ return rows.map(mapServicePrincipalRow);
87
+ }
88
+
89
+ type EntraServicePrincipalRow = {
90
+ id: string;
91
+ app_id: string;
92
+ display_name: string;
93
+ app_display_name: string | null;
94
+ service_principal_type: EntraServicePrincipal["servicePrincipalType"];
95
+ publisher_name: string | null;
96
+ account_enabled: boolean;
97
+ app_owner_organization_id: string | null;
98
+ homepage: string | null;
99
+ login_url: string | null;
100
+ reply_urls: string;
101
+ service_principal_names: string;
102
+ tags: string;
103
+ app_roles: string;
104
+ owners: string;
105
+ app_owners: string;
106
+ service_principal_owners: string;
107
+ application_owners: string;
108
+ metadata: string | null;
109
+ };
110
+
111
+ function mapServicePrincipalRow(row: EntraServicePrincipalRow): EntraServicePrincipal {
112
+ return {
113
+ id: row.id,
114
+ appId: row.app_id,
115
+ displayName: row.display_name,
116
+ appDisplayName: row.app_display_name,
117
+ servicePrincipalType: row.service_principal_type,
118
+ publisherName: row.publisher_name,
119
+ accountEnabled: row.account_enabled,
120
+ appOwnerOrganizationId: row.app_owner_organization_id,
121
+ homepage: row.homepage,
122
+ loginUrl: row.login_url,
123
+ replyUrls: parseJsonArray<string>(row.reply_urls),
124
+ servicePrincipalNames: parseJsonArray<string>(row.service_principal_names),
125
+ tags: parseJsonArray<string>(row.tags),
126
+ appRoles: parseJsonArray(row.app_roles),
127
+ owners: parseJsonArray(row.owners),
128
+ appOwners: parseJsonArray(row.app_owners),
129
+ servicePrincipalOwners: parseJsonArray(row.service_principal_owners),
130
+ applicationOwners: parseJsonArray(row.application_owners),
131
+ metadata: row.metadata ? parseJsonObject(row.metadata) : null
132
+ };
133
+ }
134
+
135
+ async function readRows<Row extends Record<string, unknown>>(
136
+ connection: DuckDBConnection,
137
+ sql: string
138
+ ): Promise<Row[]> {
139
+ const reader = await connection.runAndReadAll(sql);
140
+ return reader.getRowObjectsJson() as Row[];
141
+ }
142
+
143
+ function parseJsonArray<T>(value: string | null | undefined): T[] {
144
+ return value ? JSON.parse(value) : [];
145
+ }
146
+
147
+ function parseJsonObject(value: string | null | undefined): Record<string, unknown> {
148
+ return value ? JSON.parse(value) : {};
149
+ }
@@ -0,0 +1,18 @@
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
+
6
+ export async function importEntraSnapshotMetadata(
7
+ connection: DuckDBConnection,
8
+ snapshot: EntraSnapshot & LocalSnapshotData
9
+ ): Promise<void> {
10
+ const { meta, servicePrincipals, applications, oauth2PermissionGrants, appRoleAssignments, ...extra } = snapshot;
11
+ void servicePrincipals;
12
+ void applications;
13
+ void oauth2PermissionGrants;
14
+ void appRoleAssignments;
15
+
16
+ await connection.run("insert into entra_snapshot_meta values ($data::json)", { data: JSON.stringify(meta) });
17
+ await connection.run("insert into entra_snapshot_extra values ($data::json)", { data: JSON.stringify(extra) });
18
+ }