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,124 @@
1
+ import { RuntimeHttpError } from "../../../../core/runtime/localSnapshotFiles";
2
+ import type {
3
+ ZtaRelatedObject,
4
+ ZtaRemediationSummary,
5
+ ZtaReport,
6
+ ZtaReportTest
7
+ } from "../../../../core/azure/ztaReport";
8
+
9
+ import {
10
+ buildPaginatedCollection,
11
+ type LocalReportCollectionFilter,
12
+ type LocalReportCollectionQueryOptions
13
+ } from "../localReportCollections";
14
+ import type { LocalZeroTrustAssessmentReportRuntime } from "./LocalZeroTrustAssessmentReportRuntime";
15
+
16
+ export type LocalZeroTrustAssessmentReportCollectionId = "zeroTrustAssessment.report";
17
+
18
+ export type ZeroTrustAssessmentQueryServiceOptions = {
19
+ zeroTrustAssessment: LocalZeroTrustAssessmentReportRuntime;
20
+ };
21
+
22
+ export class ZeroTrustAssessmentQueryService {
23
+ private readonly zeroTrustAssessment: LocalZeroTrustAssessmentReportRuntime;
24
+
25
+ constructor(options: ZeroTrustAssessmentQueryServiceOptions) {
26
+ this.zeroTrustAssessment = options.zeroTrustAssessment;
27
+ }
28
+
29
+ async readReport(): Promise<ZtaReport> {
30
+ return this.zeroTrustAssessment.readReport();
31
+ }
32
+
33
+ async queryReport(options: LocalReportCollectionQueryOptions) {
34
+ const report = await this.readReport();
35
+ const { relatedObjectFilters, remainingFilters } = splitRelatedObjectFilters(options.filters ?? []);
36
+ const tests = applyRelatedObjectFilters(report.Tests ?? [], relatedObjectFilters);
37
+ const collection = buildPaginatedCollection(
38
+ "zeroTrustAssessment.report",
39
+ tests as Record<string, unknown>[],
40
+ { ...options, filters: remainingFilters }
41
+ );
42
+
43
+ return {
44
+ ...collection,
45
+ Meta: report.Meta,
46
+ Tests: collection.rows as ZtaReportTest[]
47
+ };
48
+ }
49
+
50
+ async readRemediationSummaries(): Promise<Map<string, ZtaRemediationSummary>> {
51
+ try {
52
+ return await this.zeroTrustAssessment.readRemediationSummaries();
53
+ } catch (error) {
54
+ if (error instanceof RuntimeHttpError && error.statusCode === 404) {
55
+ return new Map();
56
+ }
57
+
58
+ throw error;
59
+ }
60
+ }
61
+ }
62
+
63
+ function splitRelatedObjectFilters(filters: LocalReportCollectionFilter[]): {
64
+ relatedObjectFilters: LocalReportCollectionFilter[];
65
+ remainingFilters: LocalReportCollectionFilter[];
66
+ } {
67
+ return {
68
+ relatedObjectFilters: filters.filter((filter) => filter.column === "RelatedObjects"),
69
+ remainingFilters: filters.filter((filter) => filter.column !== "RelatedObjects")
70
+ };
71
+ }
72
+
73
+ function applyRelatedObjectFilters(
74
+ tests: ZtaReportTest[],
75
+ filters: LocalReportCollectionFilter[]
76
+ ): ZtaReportTest[] {
77
+ const activeFilters = filters
78
+ .map((filter) => filter.values.map((value) => value.trim()).filter(Boolean))
79
+ .filter((values) => values.length > 0);
80
+
81
+ if (activeFilters.length === 0) {
82
+ return tests;
83
+ }
84
+
85
+ return tests.flatMap((test) => {
86
+ const relatedObjects = test.RelatedObjects ?? [];
87
+ const matchingRelatedObjects = relatedObjects.filter((relatedObject) =>
88
+ matchesRelatedObjectFilters(relatedObject, activeFilters)
89
+ );
90
+
91
+ if (matchingRelatedObjects.length === 0) {
92
+ return [];
93
+ }
94
+
95
+ return [
96
+ {
97
+ ...test,
98
+ RelatedObjects: matchingRelatedObjects
99
+ }
100
+ ];
101
+ });
102
+ }
103
+
104
+ function formatRelatedObjectsSearchValue(relatedObjects: ZtaRelatedObject[]): string {
105
+ return relatedObjects
106
+ .flatMap((relatedObject) => [
107
+ relatedObject.servicePrincipalId,
108
+ ...(relatedObject.tags ?? []),
109
+ relatedObject.applicationId,
110
+ relatedObject.id,
111
+ relatedObject.displayName
112
+ ])
113
+ .filter(isNonEmptyString)
114
+ .join(" ");
115
+ }
116
+
117
+ function matchesRelatedObjectFilters(relatedObject: ZtaRelatedObject, filters: string[][]): boolean {
118
+ const searchableValue = formatRelatedObjectsSearchValue([relatedObject]).toLocaleLowerCase();
119
+ return filters.every((values) => values.some((value) => searchableValue.includes(value.toLocaleLowerCase())));
120
+ }
121
+
122
+ function isNonEmptyString(value: unknown): value is string {
123
+ return typeof value === "string" && value.trim().length > 0;
124
+ }
@@ -0,0 +1,15 @@
1
+ import type { RuntimeRestEndpoint } from "../../../../core/runtime/rest";
2
+ import type { LocalReportRuntime } from "../LocalReportRuntime";
3
+ import { parseRuntimeCollectionQueryOptions } from "../runtimeRestQuery";
4
+
5
+ export function defineZeroTrustAssessmentLocalReportRuntimeRestEndpoints(
6
+ runtime: LocalReportRuntime,
7
+ restBasePath: string
8
+ ): RuntimeRestEndpoint[] {
9
+ return [
10
+ {
11
+ path: `${restBasePath}/zeroTrustAssessment/report`,
12
+ handle: ({ url }) => runtime.queryZeroTrustAssessmentReport(parseRuntimeCollectionQueryOptions(url))
13
+ }
14
+ ];
15
+ }
@@ -0,0 +1,77 @@
1
+ import type { DuckDBConnection } from "@duckdb/node-api";
2
+
3
+ import type { ZeroTrustAssessmentReport } from "./types";
4
+
5
+ export async function insertZeroTrustAssessmentReport(
6
+ connection: DuckDBConnection,
7
+ reportId: string,
8
+ report: ZeroTrustAssessmentReport,
9
+ fileName: string,
10
+ importedAt: string
11
+ ): Promise<void> {
12
+ await connection.run(
13
+ `
14
+ insert into zta_report (id, file_name, executed_at, imported_at)
15
+ values ($reportId, $fileName, $executedAt, $importedAt)
16
+ `,
17
+ {
18
+ reportId,
19
+ fileName,
20
+ executedAt: report.ExecutedAt ?? null,
21
+ importedAt
22
+ }
23
+ );
24
+ }
25
+
26
+ export async function importZeroTrustAssessmentMetadata(
27
+ connection: DuckDBConnection,
28
+ reportId: string,
29
+ report: ZeroTrustAssessmentReport
30
+ ): Promise<void> {
31
+ const { Tests, ...metadata } = report;
32
+ const {
33
+ Account,
34
+ CurrentVersion,
35
+ Domain,
36
+ EndOfJson,
37
+ ExecutedAt,
38
+ LatestVersion,
39
+ TenantId,
40
+ TenantInfo,
41
+ TenantName,
42
+ TestResultSummary,
43
+ ...extra
44
+ } = metadata;
45
+
46
+ await connection.run(
47
+ `
48
+ insert into zta_report_meta (report_id, data)
49
+ values ($reportId, $meta::json)
50
+ `,
51
+ {
52
+ reportId,
53
+ meta: JSON.stringify({
54
+ Account,
55
+ CurrentVersion,
56
+ Domain,
57
+ EndOfJson,
58
+ ExecutedAt,
59
+ LatestVersion,
60
+ TenantId,
61
+ TenantInfo,
62
+ TenantName,
63
+ TestResultSummary
64
+ })
65
+ }
66
+ );
67
+ await connection.run(
68
+ `
69
+ insert into zta_report_extra (report_id, data)
70
+ values ($reportId, $extra::json)
71
+ `,
72
+ {
73
+ reportId,
74
+ extra: JSON.stringify(extra)
75
+ }
76
+ );
77
+ }
@@ -0,0 +1,112 @@
1
+ import { randomUUID } from "node:crypto";
2
+
3
+ import type { DuckDBConnection, DuckDBValue } from "@duckdb/node-api";
4
+
5
+ import {
6
+ insertZeroTrustAssessmentReport,
7
+ importZeroTrustAssessmentMetadata
8
+ } from "./snapshotMetadataTable";
9
+ import {
10
+ insertZeroTrustAssessmentRelatedObjectRows,
11
+ insertZeroTrustAssessmentTestRows,
12
+ readZeroTrustAssessmentTestRows
13
+ } from "./tables";
14
+ import type { ZeroTrustAssessmentReport } from "./types";
15
+
16
+ export const zeroTrustAssessmentReportFileName = "ZeroTrustAssessmentReport.json";
17
+
18
+ export type ZeroTrustAssessmentDuckDbImportStatus = {
19
+ imported: boolean;
20
+ fileName: string;
21
+ reportId: string | null;
22
+ testCount: number;
23
+ importedAt: string | null;
24
+ };
25
+
26
+ export function createEmptyZeroTrustAssessmentImportStatus(): ZeroTrustAssessmentDuckDbImportStatus {
27
+ return {
28
+ imported: false,
29
+ fileName: zeroTrustAssessmentReportFileName,
30
+ reportId: null,
31
+ testCount: 0,
32
+ importedAt: null
33
+ };
34
+ }
35
+
36
+ export async function importZeroTrustAssessmentReportToDuckDb(
37
+ connection: DuckDBConnection,
38
+ report: ZeroTrustAssessmentReport,
39
+ fileName = zeroTrustAssessmentReportFileName
40
+ ): Promise<ZeroTrustAssessmentDuckDbImportStatus> {
41
+ await connection.run("begin transaction");
42
+ try {
43
+ const reportId = randomUUID();
44
+ const importedAt = new Date().toISOString();
45
+
46
+ await insertZeroTrustAssessmentReport(connection, reportId, report, fileName, importedAt);
47
+ await importZeroTrustAssessmentMetadata(connection, reportId, report);
48
+ await insertZeroTrustAssessmentTestRows(connection, reportId, report.Tests ?? []);
49
+ await insertZeroTrustAssessmentRelatedObjectRows(connection, reportId, report.Tests ?? []);
50
+
51
+ await connection.run("commit");
52
+ return {
53
+ imported: true,
54
+ fileName,
55
+ reportId,
56
+ testCount: report.Tests?.length ?? 0,
57
+ importedAt
58
+ };
59
+ } catch (error) {
60
+ await connection.run("rollback");
61
+ throw error;
62
+ }
63
+ }
64
+
65
+ export async function readZeroTrustAssessmentReportFromDuckDb(
66
+ connection: DuckDBConnection
67
+ ): Promise<ZeroTrustAssessmentReport> {
68
+ const reportRows = await readRows<{ id: string }>(
69
+ connection,
70
+ `
71
+ select id
72
+ from zta_report
73
+ order by executed_at desc nulls last, imported_at desc
74
+ limit 1
75
+ `
76
+ );
77
+ const reportId = reportRows[0]?.id;
78
+
79
+ if (!reportId) {
80
+ return { Tests: [] };
81
+ }
82
+
83
+ const metaRows = await readRows<{ data: string }>(
84
+ connection,
85
+ "select data from zta_report_meta where report_id = $reportId limit 1",
86
+ { reportId }
87
+ );
88
+ const extraRows = await readRows<{ data: string }>(
89
+ connection,
90
+ "select data from zta_report_extra where report_id = $reportId limit 1",
91
+ { reportId }
92
+ );
93
+
94
+ return {
95
+ ...parseJsonObject(extraRows[0]?.data),
96
+ ...parseJsonObject(metaRows[0]?.data),
97
+ Tests: await readZeroTrustAssessmentTestRows(connection, reportId)
98
+ };
99
+ }
100
+
101
+ async function readRows<Row extends Record<string, unknown>>(
102
+ connection: DuckDBConnection,
103
+ sql: string,
104
+ params?: Record<string, DuckDBValue>
105
+ ): Promise<Row[]> {
106
+ const reader = await connection.runAndReadAll(sql, params);
107
+ return reader.getRowObjectsJson() as Row[];
108
+ }
109
+
110
+ function parseJsonObject(value: string | null | undefined): Record<string, unknown> {
111
+ return value ? JSON.parse(value) : {};
112
+ }
@@ -0,0 +1,361 @@
1
+ import type { DuckDBConnection, DuckDBValue } from "@duckdb/node-api";
2
+
3
+ import type { ZtaRelatedObject, ZtaRemediationSummary } from "../../../../core/azure/ztaReport";
4
+ import type { ZeroTrustAssessmentTest } from "./types";
5
+
6
+ export async function insertZeroTrustAssessmentTestRows(
7
+ connection: DuckDBConnection,
8
+ reportId: string,
9
+ tests: ZeroTrustAssessmentTest[]
10
+ ): Promise<void> {
11
+ const servicePrincipalRelatedObjectIds = await readServicePrincipalRelatedObjectIds(connection);
12
+
13
+ for (const [ordinal, originalTest] of tests.entries()) {
14
+ const enrichedTest = enrichRelatedObjectsWithServicePrincipalIds(originalTest, servicePrincipalRelatedObjectIds);
15
+ const test = normalizeZeroTrustAssessmentRiskFields(enrichedTest);
16
+
17
+ await connection.run(
18
+ `insert into zta_tests values (
19
+ $reportId,
20
+ $ordinal,
21
+ $testId,
22
+ $title,
23
+ $pillar,
24
+ $status,
25
+ $risk,
26
+ $impact,
27
+ $implementationCost,
28
+ $category,
29
+ $sfiPillar,
30
+ $skippedReason,
31
+ $skippedCode,
32
+ $minimumLicense::json,
33
+ $appliesTo::json,
34
+ $tags::json,
35
+ $relatedObjects::json,
36
+ $result,
37
+ $description,
38
+ $data::json
39
+ )`,
40
+ {
41
+ reportId,
42
+ ordinal,
43
+ testId: toNullableString(test.TestId) ?? String(ordinal),
44
+ title: test.TestTitle ?? null,
45
+ pillar: test.TestPillar ?? null,
46
+ status: test.TestStatus ?? null,
47
+ risk: test.TestRisk ?? null,
48
+ impact: test.TestImpact ?? null,
49
+ implementationCost: test.TestImplementationCost ?? null,
50
+ category: test.TestCategory ?? null,
51
+ sfiPillar: test.TestSfiPillar ?? null,
52
+ skippedReason: test.SkippedReason ?? null,
53
+ skippedCode: test.TestSkipped ?? null,
54
+ minimumLicense: JSON.stringify(toJsonArray(test.TestMinimumLicense)),
55
+ appliesTo: JSON.stringify(toJsonArray(test.TestAppliesTo)),
56
+ tags: JSON.stringify(toJsonArray(test.TestTags)),
57
+ relatedObjects: JSON.stringify(test.RelatedObjects ?? []),
58
+ result: test.TestResult ?? null,
59
+ description: test.TestDescription ?? null,
60
+ data: JSON.stringify(test)
61
+ }
62
+ );
63
+ }
64
+ }
65
+
66
+ export async function insertZeroTrustAssessmentRelatedObjectRows(
67
+ connection: DuckDBConnection,
68
+ reportId: string,
69
+ tests: ZeroTrustAssessmentTest[]
70
+ ): Promise<void> {
71
+ const servicePrincipalRelatedObjectIds = await readServicePrincipalRelatedObjectIds(connection);
72
+
73
+ for (const [testOrdinal, test] of tests.entries()) {
74
+ const relatedObjectIds = getRelatedObjectIds(
75
+ enrichRelatedObjectsWithServicePrincipalIds(test, servicePrincipalRelatedObjectIds)
76
+ );
77
+
78
+ for (const relatedObjectId of relatedObjectIds) {
79
+ await connection.run(
80
+ `insert into zta_test_related_objects values (
81
+ $reportId,
82
+ $testOrdinal,
83
+ $relatedObjectId
84
+ )`,
85
+ {
86
+ reportId,
87
+ testOrdinal,
88
+ relatedObjectId
89
+ }
90
+ );
91
+ }
92
+ }
93
+ }
94
+
95
+ export async function readZeroTrustAssessmentTestRows(
96
+ connection: DuckDBConnection,
97
+ reportId: string
98
+ ): Promise<ZeroTrustAssessmentTest[]> {
99
+ const rows = await readRows<{ data: string }>(
100
+ connection,
101
+ "select data from zta_tests where report_id = $reportId order by ordinal",
102
+ { reportId }
103
+ );
104
+
105
+ return rows.map((row) => JSON.parse(row.data) as ZeroTrustAssessmentTest);
106
+ }
107
+
108
+ export async function readZeroTrustAssessmentRemediationSummaries(
109
+ connection: DuckDBConnection
110
+ ): Promise<Map<string, ZtaRemediationSummary>> {
111
+ const rows = await readRows<{
112
+ related_object_id: string;
113
+ remediation_count_all: number;
114
+ remediation_failed_count: number;
115
+ max_risk_rank: number;
116
+ }>(
117
+ connection,
118
+ `
119
+ with latest_report as (
120
+ select id
121
+ from zta_report
122
+ order by executed_at desc nulls last, imported_at desc
123
+ limit 1
124
+ ),
125
+ related_tests as (
126
+ select distinct
127
+ lower(related.related_object_id) as related_object_id,
128
+ related.test_ordinal,
129
+ lower(coalesce(test.status, '')) as status,
130
+ case lower(coalesce(test.risk, ''))
131
+ when 'high' then 3
132
+ when 'medium' then 2
133
+ when 'low' then 1
134
+ else 0
135
+ end as risk_rank
136
+ from zta_test_related_objects related
137
+ join latest_report latest
138
+ on latest.id = related.report_id
139
+ join zta_tests test
140
+ on test.report_id = related.report_id
141
+ and test.ordinal = related.test_ordinal
142
+ ),
143
+ resolved_related_tests as (
144
+ select
145
+ lower(service_principal.id) as principal_id,
146
+ related_tests.test_ordinal,
147
+ related_tests.status,
148
+ related_tests.risk_rank
149
+ from related_tests
150
+ join entra_service_principals service_principal
151
+ on lower(service_principal.id) = related_tests.related_object_id
152
+ union
153
+ select
154
+ lower(service_principal.id) as principal_id,
155
+ related_tests.test_ordinal,
156
+ related_tests.status,
157
+ related_tests.risk_rank
158
+ from related_tests
159
+ join entra_applications application
160
+ on lower(application.id) = related_tests.related_object_id
161
+ join entra_service_principals service_principal
162
+ on lower(service_principal.app_id) = lower(application.app_id)
163
+ )
164
+ select
165
+ principal_id as related_object_id,
166
+ count(*) as remediation_count_all,
167
+ sum(case when status = 'failed' then 1 else 0 end) as remediation_failed_count,
168
+ max(risk_rank) as max_risk_rank
169
+ from resolved_related_tests
170
+ group by principal_id
171
+ `
172
+ );
173
+
174
+ return new Map(
175
+ rows.map((row) => [
176
+ row.related_object_id,
177
+ {
178
+ ztaRemediationCountAll: Number(row.remediation_count_all),
179
+ ztaRemediationFailedCount: Number(row.remediation_failed_count),
180
+ ztaMaxRisk: toRiskLevel(Number(row.max_risk_rank))
181
+ }
182
+ ])
183
+ );
184
+ }
185
+
186
+ async function readRows<Row extends Record<string, unknown>>(
187
+ connection: DuckDBConnection,
188
+ sql: string,
189
+ params?: Record<string, DuckDBValue>
190
+ ): Promise<Row[]> {
191
+ const reader = await connection.runAndReadAll(sql, params);
192
+ return reader.getRowObjectsJson() as Row[];
193
+ }
194
+
195
+ type ServicePrincipalRelatedObjectIds = {
196
+ servicePrincipalId: string;
197
+ applicationId: string | null;
198
+ tags: string[];
199
+ };
200
+
201
+ async function readServicePrincipalRelatedObjectIds(
202
+ connection: DuckDBConnection
203
+ ): Promise<Map<string, ServicePrincipalRelatedObjectIds>> {
204
+ const rows = await readRows<{ service_principal_id: string; application_id: string | null; tags: string | null }>(
205
+ connection,
206
+ `
207
+ select
208
+ lower(service_principal.id) as service_principal_id,
209
+ application.id as application_id,
210
+ service_principal.tags
211
+ from entra_service_principals service_principal
212
+ left join entra_applications application
213
+ on lower(application.app_id) = lower(service_principal.app_id)
214
+ `
215
+ );
216
+
217
+ const relatedObjectIds = new Map<string, ServicePrincipalRelatedObjectIds>();
218
+
219
+ for (const row of rows) {
220
+ const value = {
221
+ servicePrincipalId: row.service_principal_id,
222
+ applicationId: row.application_id,
223
+ tags: parseJsonArray<string>(row.tags)
224
+ };
225
+ relatedObjectIds.set(row.service_principal_id, value);
226
+
227
+ if (row.application_id) {
228
+ relatedObjectIds.set(row.application_id.toLowerCase(), value);
229
+ }
230
+ }
231
+
232
+ return relatedObjectIds;
233
+ }
234
+
235
+ function enrichRelatedObjectsWithServicePrincipalIds(
236
+ test: ZeroTrustAssessmentTest,
237
+ servicePrincipalRelatedObjectIds: Map<string, ServicePrincipalRelatedObjectIds>
238
+ ): ZeroTrustAssessmentTest {
239
+ if (!servicePrincipalRelatedObjectIds.size || !test.RelatedObjects?.length) {
240
+ return test;
241
+ }
242
+
243
+ const relatedObjects = test.RelatedObjects.map((relatedObject) => {
244
+ if (!relatedObject || typeof relatedObject !== "object" || Array.isArray(relatedObject)) {
245
+ return relatedObject;
246
+ }
247
+
248
+ const ids = resolveRelatedObjectIds(relatedObject, servicePrincipalRelatedObjectIds);
249
+ return ids === undefined
250
+ ? relatedObject
251
+ : {
252
+ ...relatedObject,
253
+ servicePrincipalId: ids.servicePrincipalId,
254
+ tags: ids.tags,
255
+ applicationId: ids.applicationId
256
+ };
257
+ });
258
+
259
+ return {
260
+ ...test,
261
+ RelatedObjects: relatedObjects
262
+ };
263
+ }
264
+
265
+ function resolveRelatedObjectIds(
266
+ relatedObject: ZtaRelatedObject,
267
+ servicePrincipalRelatedObjectIds: Map<string, ServicePrincipalRelatedObjectIds>
268
+ ): ServicePrincipalRelatedObjectIds | undefined {
269
+ for (const id of [
270
+ toNullableString(relatedObject.servicePrincipalId),
271
+ toNullableString(relatedObject.object_id),
272
+ toNullableString(relatedObject.id),
273
+ toNullableString(relatedObject.applicationId)
274
+ ]) {
275
+ const ids = id ? servicePrincipalRelatedObjectIds.get(id.toLowerCase()) : undefined;
276
+ if (ids !== undefined) {
277
+ return ids;
278
+ }
279
+ }
280
+
281
+ return undefined;
282
+ }
283
+
284
+ function toJsonArray(value: unknown): unknown[] {
285
+ if (Array.isArray(value)) {
286
+ return value;
287
+ }
288
+
289
+ return value == null || value === "" ? [] : [value];
290
+ }
291
+
292
+ function parseJsonArray<T>(value: string | null | undefined): T[] {
293
+ if (!value) {
294
+ return [];
295
+ }
296
+
297
+ const parsed = JSON.parse(value) as unknown;
298
+ return Array.isArray(parsed) ? (parsed as T[]) : [];
299
+ }
300
+
301
+ function toNullableString(value: unknown): string | null {
302
+ return value == null || value === "" ? null : String(value);
303
+ }
304
+
305
+ function normalizeZeroTrustAssessmentRiskFields(test: ZeroTrustAssessmentTest): ZeroTrustAssessmentTest {
306
+ return {
307
+ ...test,
308
+ TestImpact: normalizeRiskLevel(test.TestImpact),
309
+ TestRisk: normalizeRiskLevel(test.TestRisk)
310
+ };
311
+ }
312
+
313
+ function normalizeRiskLevel(value: string | null | undefined): string | null | undefined {
314
+ if (value == null) {
315
+ return value;
316
+ }
317
+
318
+ const normalized = value.trim().toLowerCase();
319
+ return normalized === "high" || normalized === "medium" || normalized === "low" || normalized === "none"
320
+ ? normalized
321
+ : value;
322
+ }
323
+
324
+ function getRelatedObjectIds(test: ZeroTrustAssessmentTest): string[] {
325
+ const ids = new Set<string>();
326
+
327
+ for (const relatedObject of test.RelatedObjects ?? []) {
328
+ if (!relatedObject || typeof relatedObject !== "object" || Array.isArray(relatedObject)) {
329
+ continue;
330
+ }
331
+
332
+ for (const id of [
333
+ toNullableString(relatedObject.object_id),
334
+ toNullableString(relatedObject.id),
335
+ toNullableString(relatedObject.servicePrincipalId),
336
+ toNullableString(relatedObject.applicationId)
337
+ ]) {
338
+ if (id) {
339
+ ids.add(id);
340
+ }
341
+ }
342
+ }
343
+
344
+ return [...ids];
345
+ }
346
+
347
+ function toRiskLevel(rank: number): ZtaRemediationSummary["ztaMaxRisk"] {
348
+ if (rank >= 3) {
349
+ return "high";
350
+ }
351
+
352
+ if (rank === 2) {
353
+ return "medium";
354
+ }
355
+
356
+ if (rank === 1) {
357
+ return "low";
358
+ }
359
+
360
+ return "none";
361
+ }
@@ -0,0 +1,7 @@
1
+ import type { ZtaReportMeta, ZtaReportTest } from "../../../../core/azure/ztaReport";
2
+
3
+ export type ZeroTrustAssessmentReport = ZtaReportMeta & {
4
+ Tests: ZeroTrustAssessmentTest[];
5
+ };
6
+
7
+ export type ZeroTrustAssessmentTest = ZtaReportTest;
@@ -0,0 +1,12 @@
1
+ import type { ZtaReport } from "../../../../core/azure/ztaReport";
2
+
3
+ import type { ZeroTrustAssessmentReport } from "./types";
4
+
5
+ export function toZtaReport(report: ZeroTrustAssessmentReport): ZtaReport {
6
+ const { Tests, ...Meta } = report;
7
+
8
+ return {
9
+ Meta,
10
+ Tests: Tests ?? []
11
+ };
12
+ }