ownerlens 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +183 -0
- package/README.md +209 -0
- package/bin/ownerlens.js +92 -0
- package/dist/assets/index-B9aAYpVl.css +1 -0
- package/dist/assets/index-BcwLk2bx.js +10 -0
- package/dist/index.html +13 -0
- package/package.json +73 -0
- package/src/App.tsx +18 -0
- package/src/components/azure/AzureComponent.test.tsx +625 -0
- package/src/components/azure/AzureComponent.tsx +189 -0
- package/src/components/azure/AzureRbacComponent.tsx +104 -0
- package/src/components/azure/ClosableAzureTab.tsx +42 -0
- package/src/components/azure/EntraPermissionsComponent.tsx +194 -0
- package/src/components/azure/ManagedIdentityComponent.test.tsx +324 -0
- package/src/components/azure/ManagedIdentityComponent.tsx +141 -0
- package/src/components/azure/ResourceGroupComponent.tsx +157 -0
- package/src/components/azure/ServicePrincipalComponent.test.tsx +457 -0
- package/src/components/azure/ServicePrincipalComponent.tsx +155 -0
- package/src/components/azure/ServicePrincipalFieldRenderers.tsx +140 -0
- package/src/components/azure/ZtaComponent.test.tsx +267 -0
- package/src/components/azure/ZtaComponent.tsx +276 -0
- package/src/components/azure/ZtaRemediationBadge.tsx +70 -0
- package/src/components/azure/api.ts +216 -0
- package/src/components/azure/azureReportConfig.ts +247 -0
- package/src/core/azure/azureRbac.ts +70 -0
- package/src/core/azure/entra/index.ts +1 -0
- package/src/core/azure/entra/managedIdentity.ts +21 -0
- package/src/core/azure/entra/servicePrincipal.ts +34 -0
- package/src/core/azure/entra/types.ts +56 -0
- package/src/core/azure/identityEnrichment.ts +65 -0
- package/src/core/azure/resources.ts +141 -0
- package/src/core/azure/ztaReport.ts +58 -0
- package/src/core/config.ts +39 -0
- package/src/core/ownership/OwnershipTarget.ts +32 -0
- package/src/core/ownership/resolveOwner.ts +5 -0
- package/src/core/ownership/types.ts +14 -0
- package/src/core/risk/types.ts +1 -0
- package/src/core/runtime/index.ts +1 -0
- package/src/core/runtime/localSnapshotFiles.ts +74 -0
- package/src/core/runtime/rest.ts +61 -0
- package/src/lib/searchFilterUtils.ts +17 -0
- package/src/lib/utils.ts +48 -0
- package/src/main.tsx +10 -0
- package/src/providers/azure/identities/azureIdentityTypes.ts +1 -0
- package/src/providers/azure/identities/buildAzureManagedIdentityAssignmentIndex.test.ts +32 -0
- package/src/providers/azure/identities/buildAzureManagedIdentityAssignmentIndex.ts +35 -0
- package/src/providers/azure/identities/userAssignedIdentityAssignments.ts +52 -0
- package/src/providers/azure/inputTransferObject/entra/EntraAppRoleAssignment.ts +10 -0
- package/src/providers/azure/inputTransferObject/entra/EntraApplication.ts +27 -0
- package/src/providers/azure/inputTransferObject/entra/EntraOAuth2PermissionGrant.ts +8 -0
- package/src/providers/azure/inputTransferObject/entra/EntraServicePrincipal.ts +43 -0
- package/src/providers/azure/inputTransferObject/entra/EntraSnapshot.ts +13 -0
- package/src/providers/azure/inputTransferObject/entra/EntraSnapshotMeta.ts +12 -0
- package/src/providers/azure/inputTransferObject/resources/AzureActivityLog.ts +1 -0
- package/src/providers/azure/inputTransferObject/resources/AzureResource.ts +1 -0
- package/src/providers/azure/inputTransferObject/resources/AzureResourceGroup.ts +1 -0
- package/src/providers/azure/inputTransferObject/resources/AzureRoleAssignment.ts +1 -0
- package/src/providers/azure/inputTransferObject/resources/AzureSnapshot.ts +1 -0
- package/src/providers/azure/inputTransferObject/resources/AzureSnapshotMeta.ts +1 -0
- package/src/providers/azure/inputTransferObject/resources/AzureSubscription.ts +1 -0
- package/src/providers/azure/inputTransferObject/resources/AzureUserAssignedManagedIdentity.ts +1 -0
- package/src/providers/azure/ownership/azureActivityOwnershipEvidence.ts +60 -0
- package/src/providers/azure/ownership/azureOwnerReportTypes.ts +13 -0
- package/src/providers/azure/ownership/azureOwnershipConfig.ts +21 -0
- package/src/providers/azure/ownership/azureOwnershipTypes.ts +46 -0
- package/src/providers/azure/ownership/buildAzureOwnershipReport.test.ts +99 -0
- package/src/providers/azure/ownership/buildAzureOwnershipReport.ts +90 -0
- package/src/providers/azure/ownership/buildAzureOwnershipTargets.test.ts +87 -0
- package/src/providers/azure/ownership/buildAzureOwnershipTargets.ts +42 -0
- package/src/providers/azure/ownership/resolveAzureOwner.ts +146 -0
- package/src/providers/azure/runtime/DisabledEvidenceStore.ts +34 -0
- package/src/providers/azure/runtime/EnrichmentService.ts +35 -0
- package/src/providers/azure/runtime/LocalReportRuntime.test.ts +2318 -0
- package/src/providers/azure/runtime/LocalReportRuntime.ts +302 -0
- package/src/providers/azure/runtime/RuntimeHost.ts +60 -0
- package/src/providers/azure/runtime/SnapshotImporter.ts +44 -0
- package/src/providers/azure/runtime/enrichment/azureIdentityEnrichment.ts +523 -0
- package/src/providers/azure/runtime/enrichment/azureScopeClassifier.ts +30 -0
- package/src/providers/azure/runtime/enrichment/evaluateAzureRoleAssignmentRisk.ts +88 -0
- package/src/providers/azure/runtime/entra/EntraCollectionQueryService.ts +307 -0
- package/src/providers/azure/runtime/entra/LocalEntraReportRuntime.ts +227 -0
- package/src/providers/azure/runtime/entra/appRoleAssignmentsTable.ts +52 -0
- package/src/providers/azure/runtime/entra/applicationsTable.ts +175 -0
- package/src/providers/azure/runtime/entra/entraServicePrincipalMapper.ts +63 -0
- package/src/providers/azure/runtime/entra/localReportRuntimeRest.ts +41 -0
- package/src/providers/azure/runtime/entra/oauth2PermissionGrantsTable.ts +48 -0
- package/src/providers/azure/runtime/entra/principalProjection.ts +173 -0
- package/src/providers/azure/runtime/entra/servicePrincipalsTable.ts +149 -0
- package/src/providers/azure/runtime/entra/snapshotMetadataTable.ts +18 -0
- package/src/providers/azure/runtime/entra/snapshotStore.ts +102 -0
- package/src/providers/azure/runtime/localReportCollections.ts +101 -0
- package/src/providers/azure/runtime/localReportRuntimeRest.ts +71 -0
- package/src/providers/azure/runtime/resources/AzureResourcesCollectionQueryService.ts +145 -0
- package/src/providers/azure/runtime/resources/LocalAzureResourcesReportRuntime.ts +114 -0
- package/src/providers/azure/runtime/resources/disabledOwnerEvidenceTable.ts +60 -0
- package/src/providers/azure/runtime/resources/localReportRuntimeRest.ts +81 -0
- package/src/providers/azure/runtime/resources/resourceGroupOwnership.ts +90 -0
- package/src/providers/azure/runtime/resources/snapshotMetadataTable.ts +19 -0
- package/src/providers/azure/runtime/resources/snapshotStore.ts +128 -0
- package/src/providers/azure/runtime/resources/tables.ts +441 -0
- package/src/providers/azure/runtime/runtimeRestQuery.ts +46 -0
- package/src/providers/azure/runtime/runtimeSqlSchema.ts +357 -0
- package/src/providers/azure/runtime/zta/Discovery.ts +141 -0
- package/src/providers/azure/runtime/zta/LocalZeroTrustAssessmentReportRuntime.ts +86 -0
- package/src/providers/azure/runtime/zta/ZeroTrustAssessmentQueryService.ts +124 -0
- package/src/providers/azure/runtime/zta/localReportRuntimeRest.ts +15 -0
- package/src/providers/azure/runtime/zta/snapshotMetadataTable.ts +77 -0
- package/src/providers/azure/runtime/zta/snapshotStore.ts +112 -0
- package/src/providers/azure/runtime/zta/tables.ts +361 -0
- package/src/providers/azure/runtime/zta/types.ts +7 -0
- package/src/providers/azure/runtime/zta/ztaReportMapper.ts +12 -0
- package/src/report/applyCollectionControls.ts +289 -0
- package/src/report/buildCollectionColumns.tsx +38 -0
- package/src/report/components/ConfidenceBadge.tsx +10 -0
- package/src/report/components/EvidenceList.test.ts +25 -0
- package/src/report/components/EvidenceList.tsx +52 -0
- package/src/report/components/GenericTable.tsx +373 -0
- package/src/report/components/PermissionRiskBadge.tsx +19 -0
- package/src/report/components/reportTableControls.test.ts +175 -0
- package/src/report/components/reportTableControls.tsx +483 -0
- package/src/report/components/ui/badge.tsx +35 -0
- package/src/report/components/ui/button.tsx +38 -0
- package/src/report/components/ui/card.tsx +23 -0
- package/src/report/components/ui/input.tsx +15 -0
- package/src/report/components/ui/table.tsx +44 -0
- package/src/report/components/ui/tabs.tsx +29 -0
- package/src/report/export/csv.ts +34 -0
- package/src/report/ownerManualPrecheck.test.ts +137 -0
- package/src/report/ownerManualPrecheck.ts +132 -0
- package/src/report/reportArchitecture.test.ts +125 -0
- package/src/report/reportTypes.ts +54 -0
- package/src/report/reportValueRenderers.tsx +54 -0
- package/src/report/runtimeCollectionQuery.ts +23 -0
- package/src/report/types.ts +14 -0
- package/src/styles.css +43 -0
- package/tools/README.md +108 -0
- package/tools/azure-activity-check.ps1 +164 -0
- package/tools/collect-azure.ps1 +54 -0
- package/tools/collect-entra.ps1 +47 -0
- package/tools/collect-scripts.test.ts +22 -0
- package/tools/prepare-entra-snapshot.ps1 +403 -0
- package/tools/prepare-entra-snapshot.test.ts +14 -0
- package/tools/prepare-resource-snapshot.ps1 +345 -0
- package/vite.config.ts +23 -0
|
@@ -0,0 +1,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,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
|
+
}
|