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,46 @@
|
|
|
1
|
+
import type { LocalReportCollectionQueryOptions } from "./localReportCollections";
|
|
2
|
+
|
|
3
|
+
export function parseRuntimeCollectionQueryOptions(url: URL): LocalReportCollectionQueryOptions {
|
|
4
|
+
return {
|
|
5
|
+
filters: parseRuntimeCollectionFilters(url),
|
|
6
|
+
page: parseOptionalInteger(url.searchParams.get("page")),
|
|
7
|
+
pageSize: parseOptionalInteger(url.searchParams.get("pageSize") ?? url.searchParams.get("count"))
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function parseRuntimeCollectionFilters(url: URL): Array<{ column: string; values: string[] }> {
|
|
12
|
+
const filters = new Map<number, { column: string; values: string[] }>();
|
|
13
|
+
|
|
14
|
+
for (const [key, value] of url.searchParams) {
|
|
15
|
+
const match = /^filter\[(\d+)\]\[(column|value|values)\](?:\[(\d+)\])?$/.exec(key);
|
|
16
|
+
if (!match) {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const index = Number(match[1]);
|
|
21
|
+
const property = match[2];
|
|
22
|
+
const filter = filters.get(index) ?? { column: "", values: [] };
|
|
23
|
+
|
|
24
|
+
if (property === "column") {
|
|
25
|
+
filter.column = value;
|
|
26
|
+
} else {
|
|
27
|
+
filter.values.push(value);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
filters.set(index, filter);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return [...filters.entries()]
|
|
34
|
+
.sort(([left], [right]) => left - right)
|
|
35
|
+
.map(([, filter]) => filter)
|
|
36
|
+
.filter((filter) => filter.column && filter.values.length > 0);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function parseOptionalInteger(value: string | null): number | undefined {
|
|
40
|
+
if (value === null || value.trim() === "") {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const parsed = Number(value);
|
|
45
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
46
|
+
}
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import type { DuckDBConnection } from "@duckdb/node-api";
|
|
2
|
+
|
|
3
|
+
const entraSnapshotSchemaSql = [
|
|
4
|
+
`
|
|
5
|
+
create table if not exists entra_snapshot_meta (
|
|
6
|
+
data json not null
|
|
7
|
+
)
|
|
8
|
+
`,
|
|
9
|
+
`
|
|
10
|
+
create table if not exists entra_snapshot_extra (
|
|
11
|
+
data json not null
|
|
12
|
+
)
|
|
13
|
+
`
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const entraServicePrincipalSchemaSql = [
|
|
17
|
+
`
|
|
18
|
+
create table if not exists entra_service_principals (
|
|
19
|
+
ordinal integer not null,
|
|
20
|
+
id varchar primary key,
|
|
21
|
+
app_id varchar not null,
|
|
22
|
+
display_name varchar not null,
|
|
23
|
+
app_display_name varchar,
|
|
24
|
+
service_principal_type varchar not null,
|
|
25
|
+
publisher_name varchar,
|
|
26
|
+
account_enabled boolean not null,
|
|
27
|
+
app_owner_organization_id varchar,
|
|
28
|
+
homepage varchar,
|
|
29
|
+
login_url varchar,
|
|
30
|
+
reply_urls json not null,
|
|
31
|
+
service_principal_names json not null,
|
|
32
|
+
tags json not null,
|
|
33
|
+
app_roles json not null,
|
|
34
|
+
owners json not null,
|
|
35
|
+
app_owners json not null,
|
|
36
|
+
service_principal_owners json not null,
|
|
37
|
+
application_owners json not null,
|
|
38
|
+
metadata json
|
|
39
|
+
)
|
|
40
|
+
`
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const entraApplicationSchemaSql = [
|
|
44
|
+
`
|
|
45
|
+
create table if not exists entra_applications (
|
|
46
|
+
ordinal integer not null,
|
|
47
|
+
id varchar primary key,
|
|
48
|
+
app_id varchar not null,
|
|
49
|
+
display_name varchar not null,
|
|
50
|
+
sign_in_audience varchar,
|
|
51
|
+
publisher_domain varchar,
|
|
52
|
+
identifier_uris json not null,
|
|
53
|
+
tags json not null,
|
|
54
|
+
app_roles json not null,
|
|
55
|
+
oauth2_permission_scopes json not null,
|
|
56
|
+
required_resource_access json not null,
|
|
57
|
+
web json,
|
|
58
|
+
spa json,
|
|
59
|
+
public_client json,
|
|
60
|
+
password_credentials json not null,
|
|
61
|
+
key_credentials json not null,
|
|
62
|
+
created_date_time varchar,
|
|
63
|
+
deleted_date_time varchar,
|
|
64
|
+
disabled_by_microsoft_status varchar,
|
|
65
|
+
info json,
|
|
66
|
+
notes varchar,
|
|
67
|
+
owners json not null
|
|
68
|
+
)
|
|
69
|
+
`
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
const entraOAuth2PermissionGrantSchemaSql = [
|
|
73
|
+
`
|
|
74
|
+
create table if not exists entra_oauth2_permission_grants (
|
|
75
|
+
ordinal integer not null,
|
|
76
|
+
id varchar primary key,
|
|
77
|
+
client_id varchar not null,
|
|
78
|
+
consent_type varchar not null,
|
|
79
|
+
principal_id varchar,
|
|
80
|
+
resource_id varchar not null,
|
|
81
|
+
scope varchar not null
|
|
82
|
+
)
|
|
83
|
+
`
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
const entraAppRoleAssignmentSchemaSql = [
|
|
87
|
+
`
|
|
88
|
+
create table if not exists entra_app_role_assignments (
|
|
89
|
+
ordinal integer not null,
|
|
90
|
+
id varchar primary key,
|
|
91
|
+
app_role_id varchar not null,
|
|
92
|
+
app_role_display_name varchar,
|
|
93
|
+
app_role_value varchar,
|
|
94
|
+
principal_id varchar not null,
|
|
95
|
+
principal_display_name varchar,
|
|
96
|
+
resource_id varchar not null,
|
|
97
|
+
resource_display_name varchar
|
|
98
|
+
)
|
|
99
|
+
`
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
const azureResourcesSnapshotSchemaSql = [
|
|
103
|
+
"create table if not exists azure_resources_snapshot_meta (data json not null)",
|
|
104
|
+
"create table if not exists azure_resources_snapshot_extra (data json not null)"
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
const azureResourcesSchemaSql = [
|
|
108
|
+
`
|
|
109
|
+
create table if not exists azure_subscriptions (
|
|
110
|
+
ordinal integer not null,
|
|
111
|
+
subscription_id varchar primary key,
|
|
112
|
+
subscription_name varchar not null,
|
|
113
|
+
tenant_id varchar not null,
|
|
114
|
+
state varchar not null,
|
|
115
|
+
tags json
|
|
116
|
+
)
|
|
117
|
+
`,
|
|
118
|
+
`
|
|
119
|
+
create table if not exists azure_resource_groups (
|
|
120
|
+
ordinal integer not null,
|
|
121
|
+
subscription_id varchar not null,
|
|
122
|
+
subscription_name varchar not null,
|
|
123
|
+
resource_group varchar not null,
|
|
124
|
+
location varchar not null,
|
|
125
|
+
tags json
|
|
126
|
+
)
|
|
127
|
+
`,
|
|
128
|
+
`
|
|
129
|
+
create table if not exists azure_resources (
|
|
130
|
+
ordinal integer not null,
|
|
131
|
+
subscription_id varchar not null,
|
|
132
|
+
subscription_name varchar not null,
|
|
133
|
+
resource_id varchar primary key,
|
|
134
|
+
resource_name varchar not null,
|
|
135
|
+
resource_group varchar not null,
|
|
136
|
+
resource_type varchar not null,
|
|
137
|
+
kind varchar,
|
|
138
|
+
location varchar not null,
|
|
139
|
+
tags json,
|
|
140
|
+
identity_type varchar,
|
|
141
|
+
identity_principal_id varchar,
|
|
142
|
+
identity_tenant_id varchar,
|
|
143
|
+
user_assigned_identity_resource_ids json not null,
|
|
144
|
+
user_assigned_identities json
|
|
145
|
+
)
|
|
146
|
+
`,
|
|
147
|
+
`
|
|
148
|
+
create table if not exists azure_user_assigned_managed_identities (
|
|
149
|
+
ordinal integer not null,
|
|
150
|
+
subscription_id varchar not null,
|
|
151
|
+
subscription_name varchar not null,
|
|
152
|
+
resource_id varchar primary key,
|
|
153
|
+
name varchar not null,
|
|
154
|
+
resource_group varchar not null,
|
|
155
|
+
location varchar not null,
|
|
156
|
+
client_id varchar not null,
|
|
157
|
+
principal_id varchar not null,
|
|
158
|
+
tenant_id varchar not null,
|
|
159
|
+
tags json
|
|
160
|
+
)
|
|
161
|
+
`,
|
|
162
|
+
`
|
|
163
|
+
create table if not exists azure_role_assignments (
|
|
164
|
+
ordinal integer not null,
|
|
165
|
+
subscription_id varchar not null,
|
|
166
|
+
subscription_name varchar not null,
|
|
167
|
+
role_assignment_id varchar,
|
|
168
|
+
scope varchar not null,
|
|
169
|
+
scope_type varchar,
|
|
170
|
+
scope_subscription_id varchar,
|
|
171
|
+
scope_resource_group varchar,
|
|
172
|
+
scope_resource_provider varchar,
|
|
173
|
+
scope_resource_type varchar,
|
|
174
|
+
scope_resource_name varchar,
|
|
175
|
+
scope_management_group varchar,
|
|
176
|
+
principal_id varchar not null,
|
|
177
|
+
principal_type varchar,
|
|
178
|
+
principal_display_name varchar,
|
|
179
|
+
sign_in_name varchar,
|
|
180
|
+
role_definition_id varchar,
|
|
181
|
+
role_definition_name varchar,
|
|
182
|
+
can_delegate boolean,
|
|
183
|
+
condition varchar,
|
|
184
|
+
condition_version varchar
|
|
185
|
+
)
|
|
186
|
+
`,
|
|
187
|
+
`
|
|
188
|
+
create table if not exists azure_activity_logs (
|
|
189
|
+
ordinal integer not null,
|
|
190
|
+
subscription_id varchar not null,
|
|
191
|
+
subscription_name varchar not null,
|
|
192
|
+
event_timestamp varchar not null,
|
|
193
|
+
submission_timestamp varchar,
|
|
194
|
+
caller varchar,
|
|
195
|
+
caller_user_principal_name varchar,
|
|
196
|
+
caller_name varchar,
|
|
197
|
+
caller_email varchar,
|
|
198
|
+
caller_object_id varchar,
|
|
199
|
+
caller_identity_type varchar,
|
|
200
|
+
caller_app_id varchar,
|
|
201
|
+
caller_ip_address varchar,
|
|
202
|
+
caller_tenant_id varchar,
|
|
203
|
+
operation_name varchar,
|
|
204
|
+
operation_name_value varchar,
|
|
205
|
+
status varchar,
|
|
206
|
+
sub_status varchar,
|
|
207
|
+
category varchar,
|
|
208
|
+
resource_group_name varchar,
|
|
209
|
+
resource_id varchar,
|
|
210
|
+
resource_provider_name varchar,
|
|
211
|
+
resource_type varchar,
|
|
212
|
+
authorization_action varchar,
|
|
213
|
+
authorization_scope varchar
|
|
214
|
+
)
|
|
215
|
+
`
|
|
216
|
+
];
|
|
217
|
+
|
|
218
|
+
const disabledOwnerEvidenceSchemaSql = [
|
|
219
|
+
`
|
|
220
|
+
create table if not exists azure_disabled_owner_evidence_keys (
|
|
221
|
+
owner_key varchar primary key,
|
|
222
|
+
disabled_at varchar not null
|
|
223
|
+
)
|
|
224
|
+
`
|
|
225
|
+
];
|
|
226
|
+
|
|
227
|
+
const zeroTrustAssessmentMetadataSchemaSql = [
|
|
228
|
+
`
|
|
229
|
+
create table if not exists zta_report (
|
|
230
|
+
id varchar not null primary key,
|
|
231
|
+
file_name varchar not null,
|
|
232
|
+
executed_at varchar,
|
|
233
|
+
imported_at varchar not null
|
|
234
|
+
)
|
|
235
|
+
`,
|
|
236
|
+
`
|
|
237
|
+
create table if not exists zta_report_meta (
|
|
238
|
+
report_id varchar not null primary key,
|
|
239
|
+
data json not null
|
|
240
|
+
)
|
|
241
|
+
`,
|
|
242
|
+
`
|
|
243
|
+
create table if not exists zta_report_extra (
|
|
244
|
+
report_id varchar not null primary key,
|
|
245
|
+
data json not null
|
|
246
|
+
)
|
|
247
|
+
`
|
|
248
|
+
];
|
|
249
|
+
|
|
250
|
+
const zeroTrustAssessmentTestSchemaSql = [
|
|
251
|
+
`
|
|
252
|
+
create table if not exists zta_tests (
|
|
253
|
+
report_id varchar not null,
|
|
254
|
+
ordinal integer not null,
|
|
255
|
+
test_id varchar not null,
|
|
256
|
+
title varchar,
|
|
257
|
+
pillar varchar,
|
|
258
|
+
status varchar,
|
|
259
|
+
risk varchar,
|
|
260
|
+
impact varchar,
|
|
261
|
+
implementation_cost varchar,
|
|
262
|
+
category varchar,
|
|
263
|
+
sfi_pillar varchar,
|
|
264
|
+
skipped_reason varchar,
|
|
265
|
+
skipped_code varchar,
|
|
266
|
+
minimum_license json,
|
|
267
|
+
applies_to json,
|
|
268
|
+
tags json,
|
|
269
|
+
related_objects json,
|
|
270
|
+
result varchar,
|
|
271
|
+
description varchar,
|
|
272
|
+
data json not null,
|
|
273
|
+
primary key (report_id, ordinal)
|
|
274
|
+
)
|
|
275
|
+
`,
|
|
276
|
+
`
|
|
277
|
+
create table if not exists zta_test_related_objects (
|
|
278
|
+
report_id varchar not null,
|
|
279
|
+
test_ordinal integer not null,
|
|
280
|
+
related_object_id varchar not null,
|
|
281
|
+
primary key (report_id, test_ordinal, related_object_id)
|
|
282
|
+
)
|
|
283
|
+
`
|
|
284
|
+
];
|
|
285
|
+
|
|
286
|
+
const zeroTrustAssessmentSchemaSql = [
|
|
287
|
+
...zeroTrustAssessmentMetadataSchemaSql,
|
|
288
|
+
...zeroTrustAssessmentTestSchemaSql
|
|
289
|
+
];
|
|
290
|
+
|
|
291
|
+
const azureIdentityEnrichmentSchemaSql = [
|
|
292
|
+
`
|
|
293
|
+
create table if not exists azure_runtime_enrichment_runs (
|
|
294
|
+
run_id varchar primary key,
|
|
295
|
+
started_at varchar not null,
|
|
296
|
+
completed_at varchar,
|
|
297
|
+
status varchar not null,
|
|
298
|
+
identity_role_assignment_count integer not null,
|
|
299
|
+
access_risk_identity_count integer not null,
|
|
300
|
+
managed_identity_assignment_count integer not null,
|
|
301
|
+
error_message varchar
|
|
302
|
+
)
|
|
303
|
+
`,
|
|
304
|
+
`
|
|
305
|
+
create table if not exists azure_identity_role_assignment_enrichment (
|
|
306
|
+
run_id varchar not null,
|
|
307
|
+
principal_id varchar not null,
|
|
308
|
+
assignment_count integer not null,
|
|
309
|
+
role_assignments json not null
|
|
310
|
+
)
|
|
311
|
+
`,
|
|
312
|
+
`
|
|
313
|
+
create table if not exists azure_identity_access_risk_enrichment (
|
|
314
|
+
run_id varchar not null,
|
|
315
|
+
principal_id varchar not null,
|
|
316
|
+
risk_level varchar not null,
|
|
317
|
+
assignment_count integer not null,
|
|
318
|
+
high_risk_assignment_count integer not null,
|
|
319
|
+
broad_scope_assignment_count integer not null,
|
|
320
|
+
role_assignments json not null
|
|
321
|
+
)
|
|
322
|
+
`,
|
|
323
|
+
`
|
|
324
|
+
create table if not exists azure_managed_identity_assignment_enrichment (
|
|
325
|
+
run_id varchar not null,
|
|
326
|
+
service_principal_id varchar not null,
|
|
327
|
+
principal_id varchar not null,
|
|
328
|
+
client_id varchar not null,
|
|
329
|
+
assignment_count integer not null,
|
|
330
|
+
assigned_resource_groups json not null,
|
|
331
|
+
managed_identity_assignments json not null
|
|
332
|
+
)
|
|
333
|
+
`
|
|
334
|
+
];
|
|
335
|
+
|
|
336
|
+
const runtimeSchemaSql = [
|
|
337
|
+
...entraSnapshotSchemaSql,
|
|
338
|
+
...entraServicePrincipalSchemaSql,
|
|
339
|
+
...entraApplicationSchemaSql,
|
|
340
|
+
...entraOAuth2PermissionGrantSchemaSql,
|
|
341
|
+
...entraAppRoleAssignmentSchemaSql,
|
|
342
|
+
...azureResourcesSnapshotSchemaSql,
|
|
343
|
+
...azureResourcesSchemaSql,
|
|
344
|
+
...disabledOwnerEvidenceSchemaSql,
|
|
345
|
+
...zeroTrustAssessmentSchemaSql,
|
|
346
|
+
...azureIdentityEnrichmentSchemaSql
|
|
347
|
+
];
|
|
348
|
+
|
|
349
|
+
export async function prepareRuntimeSqlSchema(connection: DuckDBConnection): Promise<void> {
|
|
350
|
+
await runSqlSchema(connection, runtimeSchemaSql);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async function runSqlSchema(connection: DuckDBConnection, statements: string[]): Promise<void> {
|
|
354
|
+
for (const statement of statements) {
|
|
355
|
+
await connection.run(statement);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type { Dirent } from "node:fs";
|
|
2
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
export type JsonDiscoveryCandidate<T extends Record<string, unknown>> = {
|
|
6
|
+
absolutePath: string;
|
|
7
|
+
relativePath: string;
|
|
8
|
+
data: T;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type JsonDiscoveryDescription<T extends Record<string, unknown>> = {
|
|
12
|
+
requiredTopLevelKeys: string[];
|
|
13
|
+
validate?: (value: Record<string, unknown>) => value is T;
|
|
14
|
+
compareCandidates?: (left: JsonDiscoveryCandidate<T>, right: JsonDiscoveryCandidate<T>) => number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export async function discoverJsonFile<T extends Record<string, unknown>>(
|
|
18
|
+
rootDir: string,
|
|
19
|
+
description: JsonDiscoveryDescription<T>
|
|
20
|
+
): Promise<JsonDiscoveryCandidate<T> | null> {
|
|
21
|
+
const jsonPaths = await listJsonFiles(rootDir);
|
|
22
|
+
const candidates: JsonDiscoveryCandidate<T>[] = [];
|
|
23
|
+
|
|
24
|
+
for (const filePath of jsonPaths) {
|
|
25
|
+
const data = await readJsonDiscoveryCandidate(filePath, description);
|
|
26
|
+
if (!data) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
candidates.push({
|
|
31
|
+
absolutePath: filePath,
|
|
32
|
+
relativePath: toPosixRelativePath(rootDir, filePath),
|
|
33
|
+
data
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return candidates.sort(description.compareCandidates)[0] ?? null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function compareByNewestDateField<T extends Record<string, unknown>>(
|
|
41
|
+
fieldName: keyof T
|
|
42
|
+
): (left: JsonDiscoveryCandidate<T>, right: JsonDiscoveryCandidate<T>) => number {
|
|
43
|
+
return (left, right) => {
|
|
44
|
+
const dateComparison = compareNullableDateStrings(
|
|
45
|
+
toNullableString(right.data[fieldName]),
|
|
46
|
+
toNullableString(left.data[fieldName])
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
return dateComparison === 0 ? left.relativePath.localeCompare(right.relativePath) : dateComparison;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function listJsonFiles(rootDir: string): Promise<string[]> {
|
|
54
|
+
let entries: Dirent[];
|
|
55
|
+
try {
|
|
56
|
+
entries = await readdir(rootDir, { recursive: true, withFileTypes: true });
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return entries
|
|
66
|
+
.filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".json"))
|
|
67
|
+
.map((entry) => path.join(entry.parentPath, entry.name))
|
|
68
|
+
.sort((left, right) => left.localeCompare(right));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function readJsonDiscoveryCandidate<T extends Record<string, unknown>>(
|
|
72
|
+
filePath: string,
|
|
73
|
+
description: JsonDiscoveryDescription<T>
|
|
74
|
+
): Promise<T | null> {
|
|
75
|
+
try {
|
|
76
|
+
const rawJson = stripJsonByteOrderMark(await readFile(filePath, "utf8"));
|
|
77
|
+
if (!containsRequiredJsonKeyNames(rawJson, description.requiredTopLevelKeys)) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const value = JSON.parse(rawJson) as unknown;
|
|
82
|
+
if (!isPlainObject(value) || !hasRequiredTopLevelKeys(value, description.requiredTopLevelKeys)) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (description.validate && !description.validate(value)) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return value as T;
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function hasRequiredTopLevelKeys(value: Record<string, unknown>, keys: string[]): boolean {
|
|
97
|
+
return keys.every((key) => Object.prototype.hasOwnProperty.call(value, key));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function containsRequiredJsonKeyNames(value: string, keys: string[]): boolean {
|
|
101
|
+
return keys.every((key) => value.includes(JSON.stringify(key)));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
105
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function compareNullableDateStrings(left: string | null, right: string | null): number {
|
|
109
|
+
const leftTime = left ? Date.parse(left) : Number.NaN;
|
|
110
|
+
const rightTime = right ? Date.parse(right) : Number.NaN;
|
|
111
|
+
|
|
112
|
+
if (Number.isNaN(leftTime) && Number.isNaN(rightTime)) {
|
|
113
|
+
return 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (Number.isNaN(leftTime)) {
|
|
117
|
+
return 1;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (Number.isNaN(rightTime)) {
|
|
121
|
+
return -1;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return leftTime - rightTime;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function toNullableString(value: unknown): string | null {
|
|
128
|
+
return typeof value === "string" ? value : null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function toPosixRelativePath(rootDir: string, filePath: string): string {
|
|
132
|
+
return path.relative(rootDir, filePath).split(path.sep).join("/");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function stripJsonByteOrderMark(value: string): string {
|
|
136
|
+
return value.charCodeAt(0) === 0xfeff ? value.slice(1) : value;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function isNodeError(error: unknown): error is NodeJS.ErrnoException {
|
|
140
|
+
return error instanceof Error;
|
|
141
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { DuckDBConnection } from "@duckdb/node-api";
|
|
2
|
+
|
|
3
|
+
import { RuntimeHttpError } from "../../../../core/runtime/localSnapshotFiles";
|
|
4
|
+
import type { ZtaRemediationSummary, ZtaReport } from "../../../../core/azure/ztaReport";
|
|
5
|
+
import { compareByNewestDateField, discoverJsonFile, type JsonDiscoveryDescription } from "./Discovery";
|
|
6
|
+
import {
|
|
7
|
+
createEmptyZeroTrustAssessmentImportStatus,
|
|
8
|
+
importZeroTrustAssessmentReportToDuckDb,
|
|
9
|
+
readZeroTrustAssessmentReportFromDuckDb,
|
|
10
|
+
type ZeroTrustAssessmentDuckDbImportStatus
|
|
11
|
+
} from "./snapshotStore";
|
|
12
|
+
import { readZeroTrustAssessmentRemediationSummaries } from "./tables";
|
|
13
|
+
import type { ZeroTrustAssessmentReport } from "./types";
|
|
14
|
+
import { toZtaReport } from "./ztaReportMapper";
|
|
15
|
+
|
|
16
|
+
const zeroTrustAssessmentReportDiscovery: JsonDiscoveryDescription<ZeroTrustAssessmentReport> = {
|
|
17
|
+
requiredTopLevelKeys: ["TestResultSummary", "ExecutedAt", "TenantId"],
|
|
18
|
+
validate: isZeroTrustAssessmentReport,
|
|
19
|
+
compareCandidates: compareByNewestDateField<ZeroTrustAssessmentReport>("ExecutedAt")
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type LocalZeroTrustAssessmentReportRuntimeOptions = {
|
|
23
|
+
dataDir: string;
|
|
24
|
+
getConnection: () => DuckDBConnection;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export class LocalZeroTrustAssessmentReportRuntime {
|
|
28
|
+
private readonly dataDir: string;
|
|
29
|
+
private readonly getConnection: () => DuckDBConnection;
|
|
30
|
+
private status = createEmptyZeroTrustAssessmentImportStatus();
|
|
31
|
+
|
|
32
|
+
constructor(options: LocalZeroTrustAssessmentReportRuntimeOptions) {
|
|
33
|
+
this.dataDir = options.dataDir;
|
|
34
|
+
this.getConnection = options.getConnection;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
getStatus(): ZeroTrustAssessmentDuckDbImportStatus {
|
|
38
|
+
return this.status;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async importSnapshot(): Promise<void> {
|
|
42
|
+
const candidate = await discoverJsonFile(this.dataDir, zeroTrustAssessmentReportDiscovery);
|
|
43
|
+
if (!candidate) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
this.status = await importZeroTrustAssessmentReportToDuckDb(
|
|
48
|
+
this.getConnection(),
|
|
49
|
+
candidate.data,
|
|
50
|
+
candidate.relativePath
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async readReport(): Promise<ZtaReport> {
|
|
55
|
+
this.assertImported();
|
|
56
|
+
return toZtaReport(await readZeroTrustAssessmentReportFromDuckDb(this.getConnection()));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async readRemediationSummaries(): Promise<Map<string, ZtaRemediationSummary>> {
|
|
60
|
+
this.assertImported();
|
|
61
|
+
return readZeroTrustAssessmentRemediationSummaries(this.getConnection());
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private assertImported(): void {
|
|
65
|
+
if (!this.status.imported) {
|
|
66
|
+
throw new RuntimeHttpError(
|
|
67
|
+
"ZTA report JSON with TestResultSummary, ExecutedAt, and TenantId was not found under ./data.",
|
|
68
|
+
404
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isZeroTrustAssessmentReport(value: unknown): value is ZeroTrustAssessmentReport {
|
|
75
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const report = value as Record<string, unknown>;
|
|
80
|
+
return (
|
|
81
|
+
Object.prototype.hasOwnProperty.call(report, "TestResultSummary") &&
|
|
82
|
+
Object.prototype.hasOwnProperty.call(report, "ExecutedAt") &&
|
|
83
|
+
Object.prototype.hasOwnProperty.call(report, "TenantId") &&
|
|
84
|
+
Array.isArray(report.Tests)
|
|
85
|
+
);
|
|
86
|
+
}
|