enigma-memory 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 +55 -0
- package/README.md +379 -0
- package/apps/cli/bin/enigma.mjs +1117 -0
- package/apps/desktop/src/app.js +1327 -0
- package/apps/desktop/src/index.html +1373 -0
- package/apps/desktop/src/styles.css +386 -0
- package/apps/gateway/bin/enigma-gateway.mjs +52 -0
- package/apps/gateway/src/server.mjs +1403 -0
- package/apps/native-host/README.md +265 -0
- package/apps/native-host/bin/enigma-native-host.mjs +499 -0
- package/apps/native-host/manifests/com.enigma.native_host.chrome.json +9 -0
- package/apps/native-host/manifests/com.enigma.native_host.edge.json +9 -0
- package/apps/native-host/manifests/com.enigma.native_host.firefox.json +9 -0
- package/apps/relay/bin/enigma-relay.mjs +52 -0
- package/apps/relay/src/server.mjs +1571 -0
- package/apps/verifier/bin/enigma-verify.mjs +240 -0
- package/deploy/docker-compose.production.example.yml +150 -0
- package/deploy/kubernetes/enigma-backend.example.yaml +580 -0
- package/package.json +196 -0
- package/packages/adapters/PACKAGE_CONTRACT.md +47 -0
- package/packages/adapters/src/index.js +183 -0
- package/packages/boundary/src/index.js +468 -0
- package/packages/connectors/src/index.js +533 -0
- package/packages/core/src/index.js +766 -0
- package/packages/enterprise/src/index.js +505 -0
- package/packages/importers/src/index.js +1083 -0
- package/packages/mcp-server/bin/enigma-mcp.mjs +19 -0
- package/packages/mcp-server/src/index.js +1185 -0
- package/packages/mesh/src/index.js +531 -0
- package/packages/metering/PACKAGE_CONTRACT.md +20 -0
- package/packages/metering/src/index.js +246 -0
- package/packages/optimizer/src/index.js +707 -0
- package/packages/passport/src/index.js +651 -0
- package/packages/settlement/PACKAGE_CONTRACT.md +28 -0
- package/packages/settlement/src/index.js +369 -0
- package/packages/storage/PACKAGE_CONTRACT.md +23 -0
- package/packages/storage/src/index.js +392 -0
- package/packages/vault/src/index.js +904 -0
- package/scripts/build-ai-orchestration-plan.mjs +248 -0
- package/scripts/build-cloudflare-pages-release-packet.mjs +287 -0
- package/scripts/build-cloudflare-token-policy.mjs +239 -0
- package/scripts/build-cloudflare-token-request.mjs +314 -0
- package/scripts/build-demo-assets.mjs +263 -0
- package/scripts/build-edge-backend-workers.mjs +353 -0
- package/scripts/build-goal-completion-audit.mjs +398 -0
- package/scripts/build-hosted-probe-worker.mjs +191 -0
- package/scripts/build-operator-acceptance-packet.mjs +254 -0
- package/scripts/build-operator-evidence-starter.mjs +525 -0
- package/scripts/build-production-backend-env-kit.mjs +384 -0
- package/scripts/build-production-dependency-report.mjs +369 -0
- package/scripts/build-production-handoff-packet.mjs +475 -0
- package/scripts/build-production-readiness-manifest.mjs +209 -0
- package/scripts/build-production-status-board.mjs +297 -0
- package/scripts/build-production-storage-migration.mjs +86 -0
- package/scripts/build-production-workplan.mjs +371 -0
- package/scripts/build-review-packet.mjs +594 -0
- package/scripts/check.mjs +419 -0
- package/scripts/cloudflare-ops.mjs +1672 -0
- package/scripts/cloudflare-secret-env.mjs +128 -0
- package/scripts/collect-hosted-backend-live-evidence.mjs +263 -0
- package/scripts/infrastructure-readiness.mjs +944 -0
- package/scripts/install-enigma-local.mjs +270 -0
- package/scripts/memory-optimization-benchmark.mjs +242 -0
- package/scripts/monitor-live-endpoints.mjs +358 -0
- package/scripts/release-audit.mjs +5588 -0
- package/scripts/release-provenance.mjs +250 -0
- package/scripts/run-backend-readiness-smoke.mjs +220 -0
- package/scripts/validate-backup-restore-drill.mjs +166 -0
- package/scripts/validate-cloudflare-credentials.mjs +112 -0
- package/scripts/validate-cloudflare-worker-inspect.mjs +179 -0
- package/scripts/validate-domain-tls.mjs +248 -0
- package/scripts/validate-hosted-backend-live.mjs +319 -0
- package/scripts/validate-incident-drill.mjs +221 -0
- package/scripts/validate-kms-custody.mjs +235 -0
- package/scripts/validate-legal-compliance-approval.mjs +274 -0
- package/scripts/validate-monitoring-alerting.mjs +263 -0
- package/scripts/validate-network-access-policy.mjs +285 -0
- package/scripts/validate-operator-acceptance.mjs +298 -0
- package/scripts/validate-production-manifests.mjs +325 -0
- package/scripts/validate-public-site-security.mjs +252 -0
- package/scripts/validate-security-threat-model.mjs +285 -0
- package/scripts/validate-service-settlement.mjs +256 -0
- package/scripts/validate-support-sla.mjs +190 -0
- package/scripts/validate-tenant-policy-approval.mjs +228 -0
- package/scripts/validate-usage-metering.mjs +224 -0
- package/scripts/validate-whitepaper-claims.mjs +166 -0
- package/specs/boundary-manifest-v1.schema.json +35 -0
- package/specs/capsule-v1.schema.json +55 -0
- package/specs/claim-boundary-manifest-v1.schema.json +22 -0
- package/specs/connector-profile-v1.schema.json +62 -0
- package/specs/deletion-tombstone-v1.schema.json +26 -0
- package/specs/enterprise-policy-v1.schema.json +59 -0
- package/specs/gateway-request-v1.schema.json +32 -0
- package/specs/goal-completion-audit-v1.schema.json +70 -0
- package/specs/hosted-backend-live-evidence-v1.schema.json +147 -0
- package/specs/import-report-v1.schema.json +214 -0
- package/specs/infrastructure-readiness-manifest-v1.schema.json +130 -0
- package/specs/memory-event-v1.schema.json +42 -0
- package/specs/mesh-node-v1.schema.json +126 -0
- package/specs/passport-v1.schema.json +50 -0
- package/specs/production-handoff-packet-v1.schema.json +149 -0
- package/specs/receipt-v1.schema.json +61 -0
- package/specs/relay-record-v1.schema.json +43 -0
- package/specs/state-checkpoint-v1.schema.json +37 -0
- package/specs/trust-bundle-v1.schema.json +56 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
export const MONITORING_ALERTING_SCHEMA = 'enigma.monitoring_alerting.v1';
|
|
7
|
+
export const MONITORING_ALERTING_RESULT_SCHEMA = 'enigma.monitoring_alerting_result.v1';
|
|
8
|
+
|
|
9
|
+
export const REQUIRED_ALERT_SIGNALS = Object.freeze([
|
|
10
|
+
'health_probe_failure',
|
|
11
|
+
'latency_slo_breach',
|
|
12
|
+
'http_5xx_rate',
|
|
13
|
+
'signing_failure',
|
|
14
|
+
'policy_load_failure',
|
|
15
|
+
'storage_failure',
|
|
16
|
+
'plaintext_rejection_spike',
|
|
17
|
+
'certificate_expiry',
|
|
18
|
+
'kms_or_secret_access_failure',
|
|
19
|
+
'siem_delivery_failure',
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
export const REQUIRED_SYNTHETIC_CHECKS = Object.freeze([
|
|
23
|
+
'relay_livez',
|
|
24
|
+
'relay_readyz',
|
|
25
|
+
'gateway_livez',
|
|
26
|
+
'gateway_readyz',
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
const ACCEPTED_STATUSES = new Set(['approved', 'accepted', 'go', 'verified']);
|
|
30
|
+
const ALERT_SEVERITIES = new Set(['sev1', 'sev2', 'sev3', 'sev4']);
|
|
31
|
+
const SECRET_VALUE_RE = /(?:Bearer\s+[A-Za-z0-9._~+/=-]{12,}|Basic\s+[A-Za-z0-9+/=-]{12,}|-----BEGIN [A-Z ]*PRIVATE KEY-----|https?:\/\/[^\s/@]+:[^\s/@]+@|raw memory|private prompt|full transcript|decrypted capsule|sk-[A-Za-z0-9_-]{16,}|AKIA[0-9A-Z]{16})/iu;
|
|
32
|
+
const SECRET_KEY_RE = /(?:password|passwd|pwd|token|api[_-]?key|private[_-]?key|secret|raw[_-]?memory|plaintext|plain[_-]?text|prompt|completion|transcript|embedding|provider[_-]?response)/iu;
|
|
33
|
+
const SAFE_FIELD_NAMES = new Set(['kms_or_secret_access_failure', 'plaintext_rejection_spike']);
|
|
34
|
+
const SHA256_PREFIXED_DIGEST = /^sha256:[0-9a-f]{64}$/;
|
|
35
|
+
|
|
36
|
+
function isPlainObject(value) {
|
|
37
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function nonEmptyString(value) {
|
|
41
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function positiveNumber(value) {
|
|
45
|
+
return typeof value === 'number' && Number.isFinite(value) && value > 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function nonNegativeInteger(value) {
|
|
49
|
+
return Number.isSafeInteger(value) && value >= 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isoLike(value) {
|
|
53
|
+
return nonEmptyString(value) && !Number.isNaN(Date.parse(value));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function blocker(message, path) {
|
|
57
|
+
return { message, path };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function assertNoSecrets(value, path = 'monitoring') {
|
|
61
|
+
if (typeof value === 'string') {
|
|
62
|
+
if (!/\.claim_boundary\[\d+\]$/.test(path) && SECRET_VALUE_RE.test(value)) throw new Error(`${path} contains secret or raw-memory-looking data`);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (Array.isArray(value)) {
|
|
66
|
+
value.forEach((item, index) => assertNoSecrets(item, `${path}[${index}]`));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (!isPlainObject(value)) return;
|
|
70
|
+
for (const [key, child] of Object.entries(value)) {
|
|
71
|
+
if (SECRET_KEY_RE.test(key) && !SAFE_FIELD_NAMES.has(key)) throw new Error(`${path}.${key} uses a forbidden field name`);
|
|
72
|
+
assertNoSecrets(child, `${path}.${key}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function statusAccepted(value) {
|
|
77
|
+
return typeof value === 'string' && ACCEPTED_STATUSES.has(value.trim().toLowerCase());
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function validateMetadata(metadata, blockers) {
|
|
81
|
+
if (!isPlainObject(metadata)) {
|
|
82
|
+
blockers.push(blocker('metadata is required', 'metadata'));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
for (const field of ['monitoring_id', 'environment', 'tenant', 'owner', 'approved_at', 'approval_ref', 'status']) {
|
|
86
|
+
if (!nonEmptyString(metadata[field])) blockers.push(blocker(`metadata.${field} is required`, `metadata.${field}`));
|
|
87
|
+
}
|
|
88
|
+
if (!statusAccepted(metadata.status)) blockers.push(blocker('metadata.status must be approved/accepted/go/verified', 'metadata.status'));
|
|
89
|
+
if (!isoLike(metadata.approved_at)) blockers.push(blocker('metadata.approved_at must be ISO time', 'metadata.approved_at'));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function validateObservabilityStack(stack, blockers) {
|
|
93
|
+
if (!isPlainObject(stack)) {
|
|
94
|
+
blockers.push(blocker('observability_stack object is required', 'observability_stack'));
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
for (const field of ['metrics_ref', 'logs_ref', 'dashboard_ref', 'alert_manager_ref', 'owner']) {
|
|
98
|
+
if (!nonEmptyString(stack[field])) blockers.push(blocker(`observability_stack.${field} is required`, `observability_stack.${field}`));
|
|
99
|
+
}
|
|
100
|
+
if (!nonNegativeInteger(stack.retention_days) || stack.retention_days < 7) blockers.push(blocker('observability_stack.retention_days must be integer >= 7', 'observability_stack.retention_days'));
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function validateAlerts(alerts, blockers) {
|
|
105
|
+
if (!Array.isArray(alerts)) {
|
|
106
|
+
blockers.push(blocker('alerts array is required', 'alerts'));
|
|
107
|
+
return { checked: 0, requiredCovered: 0 };
|
|
108
|
+
}
|
|
109
|
+
const names = new Set();
|
|
110
|
+
alerts.forEach((alert, index) => {
|
|
111
|
+
const path = `alerts[${index}]`;
|
|
112
|
+
if (!isPlainObject(alert)) {
|
|
113
|
+
blockers.push(blocker('alert must be object', path));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
for (const field of ['name', 'description', 'query_ref', 'threshold', 'notify_ref', 'runbook_ref', 'owner']) {
|
|
117
|
+
if (!nonEmptyString(alert[field])) blockers.push(blocker(`${path}.${field} is required`, `${path}.${field}`));
|
|
118
|
+
}
|
|
119
|
+
if (nonEmptyString(alert.name)) names.add(alert.name);
|
|
120
|
+
if (!ALERT_SEVERITIES.has(String(alert.severity ?? '').toLowerCase())) blockers.push(blocker(`${path}.severity must be sev1/sev2/sev3/sev4`, `${path}.severity`));
|
|
121
|
+
if (!positiveNumber(alert.window_seconds)) blockers.push(blocker(`${path}.window_seconds must be positive number`, `${path}.window_seconds`));
|
|
122
|
+
if (alert.enabled !== true) blockers.push(blocker(`${path}.enabled must be true`, `${path}.enabled`));
|
|
123
|
+
});
|
|
124
|
+
for (const signal of REQUIRED_ALERT_SIGNALS) {
|
|
125
|
+
if (!names.has(signal)) blockers.push(blocker(`alerts must include ${signal}`, 'alerts'));
|
|
126
|
+
}
|
|
127
|
+
return { checked: alerts.length, requiredCovered: REQUIRED_ALERT_SIGNALS.filter((signal) => names.has(signal)).length };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function validateSynthetics(synthetics, blockers) {
|
|
131
|
+
if (!Array.isArray(synthetics)) {
|
|
132
|
+
blockers.push(blocker('synthetics array is required', 'synthetics'));
|
|
133
|
+
return { checked: 0, requiredCovered: 0 };
|
|
134
|
+
}
|
|
135
|
+
const names = new Set();
|
|
136
|
+
synthetics.forEach((check, index) => {
|
|
137
|
+
const path = `synthetics[${index}]`;
|
|
138
|
+
if (!isPlainObject(check)) {
|
|
139
|
+
blockers.push(blocker('synthetic check must be object', path));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
for (const field of ['name', 'endpoint_ref', 'owner', 'alert_ref']) {
|
|
143
|
+
if (!nonEmptyString(check[field])) blockers.push(blocker(`${path}.${field} is required`, `${path}.${field}`));
|
|
144
|
+
}
|
|
145
|
+
if (nonEmptyString(check.name)) names.add(check.name);
|
|
146
|
+
if (!positiveNumber(check.frequency_seconds)) blockers.push(blocker(`${path}.frequency_seconds must be positive number`, `${path}.frequency_seconds`));
|
|
147
|
+
if (!Number.isSafeInteger(check.expected_status) || check.expected_status < 200 || check.expected_status > 399) blockers.push(blocker(`${path}.expected_status must be 2xx/3xx integer`, `${path}.expected_status`));
|
|
148
|
+
if (check.enabled !== true) blockers.push(blocker(`${path}.enabled must be true`, `${path}.enabled`));
|
|
149
|
+
});
|
|
150
|
+
for (const check of REQUIRED_SYNTHETIC_CHECKS) {
|
|
151
|
+
if (!names.has(check)) blockers.push(blocker(`synthetics must include ${check}`, 'synthetics'));
|
|
152
|
+
}
|
|
153
|
+
return { checked: synthetics.length, requiredCovered: REQUIRED_SYNTHETIC_CHECKS.filter((check) => names.has(check)).length };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function validateRouting(routing, blockers) {
|
|
157
|
+
if (!isPlainObject(routing)) {
|
|
158
|
+
blockers.push(blocker('routing object is required', 'routing'));
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
for (const field of ['paging_policy_ref', 'on_call_ref', 'escalation_ref', 'incident_runbook_ref', 'status_page_ref']) {
|
|
162
|
+
if (!nonEmptyString(routing[field])) blockers.push(blocker(`routing.${field} is required`, `routing.${field}`));
|
|
163
|
+
}
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function validateContentMinimization(contentMinimization, blockers) {
|
|
168
|
+
if (!isPlainObject(contentMinimization)) {
|
|
169
|
+
blockers.push(blocker('content_minimization object is required', 'content_minimization'));
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
for (const field of ['policy_ref', 'sample_export_ref']) {
|
|
173
|
+
if (!nonEmptyString(contentMinimization[field])) blockers.push(blocker(`content_minimization.${field} is required`, `content_minimization.${field}`));
|
|
174
|
+
}
|
|
175
|
+
if (contentMinimization.sensitive_content_in_logs !== false) blockers.push(blocker('content_minimization.sensitive_content_in_logs must be false', 'content_minimization.sensitive_content_in_logs'));
|
|
176
|
+
if (contentMinimization.sample_export_hash && !SHA256_PREFIXED_DIGEST.test(contentMinimization.sample_export_hash)) blockers.push(blocker('content_minimization.sample_export_hash must be sha256-prefixed digest', 'content_minimization.sample_export_hash'));
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function validateMonitoringAlerting(monitoring, options = {}) {
|
|
181
|
+
if (!isPlainObject(monitoring)) throw new Error('monitoring alerting evidence must be an object');
|
|
182
|
+
assertNoSecrets(monitoring);
|
|
183
|
+
const blockers = [];
|
|
184
|
+
if (monitoring.schema !== MONITORING_ALERTING_SCHEMA) blockers.push(blocker('schema mismatch', 'schema'));
|
|
185
|
+
validateMetadata(monitoring.metadata, blockers);
|
|
186
|
+
const stackOk = validateObservabilityStack(monitoring.observability_stack, blockers);
|
|
187
|
+
const alerts = validateAlerts(monitoring.alerts, blockers);
|
|
188
|
+
const synthetics = validateSynthetics(monitoring.synthetics, blockers);
|
|
189
|
+
const routingOk = validateRouting(monitoring.routing, blockers);
|
|
190
|
+
const contentOk = validateContentMinimization(monitoring.content_minimization, blockers);
|
|
191
|
+
|
|
192
|
+
const result = {
|
|
193
|
+
schema: MONITORING_ALERTING_RESULT_SCHEMA,
|
|
194
|
+
generated_at: options.generated_at ?? options.generatedAt ?? new Date(0).toISOString(),
|
|
195
|
+
ok: blockers.length === 0,
|
|
196
|
+
status: blockers.length === 0 ? 'accepted' : 'blocked',
|
|
197
|
+
blockers,
|
|
198
|
+
checked: {
|
|
199
|
+
required_alert_signals: REQUIRED_ALERT_SIGNALS.length,
|
|
200
|
+
alert_signals_covered: alerts.requiredCovered,
|
|
201
|
+
alerts: alerts.checked,
|
|
202
|
+
required_synthetic_checks: REQUIRED_SYNTHETIC_CHECKS.length,
|
|
203
|
+
synthetic_checks_covered: synthetics.requiredCovered,
|
|
204
|
+
synthetics: synthetics.checked,
|
|
205
|
+
observability_stack: stackOk,
|
|
206
|
+
routing: routingOk,
|
|
207
|
+
content_minimization: contentOk,
|
|
208
|
+
},
|
|
209
|
+
claim_boundary: [
|
|
210
|
+
'Monitoring validation checks declared alerting evidence shape only; it does not create dashboards, pagers, metrics, logs, or certificates.',
|
|
211
|
+
'A pass result is evidence for the named monitoring document and does not prove future uptime or incident response performance.',
|
|
212
|
+
'Metrics, logs, dashboards, alert payloads, and sample exports must not include raw memory, prompts, transcripts, provider responses, decrypted content, secrets, or credentials.',
|
|
213
|
+
],
|
|
214
|
+
};
|
|
215
|
+
assertNoSecrets(result, 'result');
|
|
216
|
+
return result;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function parseArgs(argv = process.argv.slice(2)) {
|
|
220
|
+
const flags = new Map();
|
|
221
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
222
|
+
const arg = argv[index];
|
|
223
|
+
if (!arg.startsWith('--')) continue;
|
|
224
|
+
const eq = arg.indexOf('=');
|
|
225
|
+
if (eq !== -1) flags.set(arg.slice(2, eq), arg.slice(eq + 1));
|
|
226
|
+
else if (!argv[index + 1] || argv[index + 1].startsWith('--')) flags.set(arg.slice(2), true);
|
|
227
|
+
else {
|
|
228
|
+
flags.set(arg.slice(2), argv[index + 1]);
|
|
229
|
+
index += 1;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return flags;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function getFlag(flags, names) {
|
|
236
|
+
for (const name of names) if (flags.has(name)) return flags.get(name);
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function main() {
|
|
241
|
+
const flags = parseArgs();
|
|
242
|
+
const monitoringPath = getFlag(flags, ['monitoring', 'in']);
|
|
243
|
+
if (!nonEmptyString(monitoringPath)) throw new Error('--monitoring <path> is required');
|
|
244
|
+
const monitoring = JSON.parse(await readFile(resolve(String(monitoringPath)), 'utf8'));
|
|
245
|
+
const result = validateMonitoringAlerting(monitoring, { generated_at: new Date().toISOString() });
|
|
246
|
+
const out = getFlag(flags, ['out']);
|
|
247
|
+
const text = `${JSON.stringify(result, null, 2)}\n`;
|
|
248
|
+
if (out && out !== true) {
|
|
249
|
+
const outPath = resolve(String(out));
|
|
250
|
+
await mkdir(dirname(outPath), { recursive: true });
|
|
251
|
+
await writeFile(outPath, text, 'utf8');
|
|
252
|
+
}
|
|
253
|
+
process.stdout.write(text);
|
|
254
|
+
process.exitCode = result.ok ? 0 : 1;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const invokedPath = process.argv[1] ? resolve(process.argv[1]) : '';
|
|
258
|
+
if (invokedPath === fileURLToPath(import.meta.url)) {
|
|
259
|
+
main().catch((error) => {
|
|
260
|
+
process.stderr.write(`${error.name ?? 'Error'}: ${error.message}\n`);
|
|
261
|
+
process.exitCode = 1;
|
|
262
|
+
});
|
|
263
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
export const NETWORK_ACCESS_POLICY_SCHEMA = 'enigma.network_access_policy.v1';
|
|
7
|
+
export const NETWORK_ACCESS_POLICY_RESULT_SCHEMA = 'enigma.network_access_policy_result.v1';
|
|
8
|
+
|
|
9
|
+
export const REQUIRED_PUBLIC_PROBES = Object.freeze([
|
|
10
|
+
'relay_livez',
|
|
11
|
+
'relay_readyz',
|
|
12
|
+
'gateway_livez',
|
|
13
|
+
'gateway_readyz',
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
export const REQUIRED_PRIVATE_ROUTES = Object.freeze([
|
|
17
|
+
'relay_records_write',
|
|
18
|
+
'gateway_admin_policy',
|
|
19
|
+
'gateway_data_plane_evaluate',
|
|
20
|
+
'gateway_data_plane_decision',
|
|
21
|
+
'gateway_siem_export',
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
const ACCEPTED_STATUSES = new Set(['approved', 'accepted', 'go', 'verified']);
|
|
25
|
+
const ALLOWED_PUBLIC_PROBE_PATHS = new Set(['/livez', '/readyz']);
|
|
26
|
+
const ALLOWED_PRIVATE_AUTH_MODES = new Set(['bearer_hash', 'mtls', 'private_ingress', 'zero_trust_access']);
|
|
27
|
+
const SECRET_VALUE_RE = /(?:Bearer\s+[A-Za-z0-9._~+/=-]{12,}|Basic\s+[A-Za-z0-9+/=-]{12,}|-----BEGIN [A-Z ]*PRIVATE KEY-----|https?:\/\/[^\s/@]+:[^\s/@]+@|raw memory|private prompt|full transcript|decrypted capsule|sk-[A-Za-z0-9_-]{16,}|AKIA[0-9A-Z]{16})/iu;
|
|
28
|
+
const SECRET_KEY_RE = /(?:password|passwd|pwd|token|api[_-]?key|private[_-]?key|secret|raw[_-]?memory|plaintext|plain[_-]?text|prompt|completion|transcript|embedding|provider[_-]?response)/iu;
|
|
29
|
+
const SAFE_FIELD_NAMES = new Set(['no_token_values_in_policy']);
|
|
30
|
+
|
|
31
|
+
function isPlainObject(value) {
|
|
32
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function nonEmptyString(value) {
|
|
36
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function positiveNumber(value) {
|
|
40
|
+
return typeof value === 'number' && Number.isFinite(value) && value > 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isoLike(value) {
|
|
44
|
+
return nonEmptyString(value) && !Number.isNaN(Date.parse(value));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function blocker(message, path) {
|
|
48
|
+
return { message, path };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function assertNoSecrets(value, path = 'network_policy') {
|
|
52
|
+
if (typeof value === 'string') {
|
|
53
|
+
if (!/\.claim_boundary\[\d+\]$/.test(path) && SECRET_VALUE_RE.test(value)) throw new Error(`${path} contains secret or raw-memory-looking data`);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (Array.isArray(value)) {
|
|
57
|
+
value.forEach((item, index) => assertNoSecrets(item, `${path}[${index}]`));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (!isPlainObject(value)) return;
|
|
61
|
+
for (const [key, child] of Object.entries(value)) {
|
|
62
|
+
if (SECRET_KEY_RE.test(key) && !SAFE_FIELD_NAMES.has(key)) throw new Error(`${path}.${key} uses a forbidden field name`);
|
|
63
|
+
assertNoSecrets(child, `${path}.${key}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function statusAccepted(value) {
|
|
68
|
+
return typeof value === 'string' && ACCEPTED_STATUSES.has(value.trim().toLowerCase());
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function validateMetadata(metadata, blockers) {
|
|
72
|
+
if (!isPlainObject(metadata)) {
|
|
73
|
+
blockers.push(blocker('metadata is required', 'metadata'));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
for (const field of ['policy_id', 'environment', 'tenant', 'owner', 'approved_at', 'approval_ref', 'status']) {
|
|
77
|
+
if (!nonEmptyString(metadata[field])) blockers.push(blocker(`metadata.${field} is required`, `metadata.${field}`));
|
|
78
|
+
}
|
|
79
|
+
if (!statusAccepted(metadata.status)) blockers.push(blocker('metadata.status must be approved/accepted/go/verified', 'metadata.status'));
|
|
80
|
+
if (!isoLike(metadata.approved_at)) blockers.push(blocker('metadata.approved_at must be ISO time', 'metadata.approved_at'));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function validateZones(zones, blockers) {
|
|
84
|
+
if (!isPlainObject(zones)) {
|
|
85
|
+
blockers.push(blocker('network_zones object is required', 'network_zones'));
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
for (const field of ['public_ingress_ref', 'private_admin_network_ref', 'private_data_plane_network_ref', 'egress_policy_ref', 'waf_or_rate_limit_ref', 'tls_policy_ref', 'owner']) {
|
|
89
|
+
if (!nonEmptyString(zones[field])) blockers.push(blocker(`network_zones.${field} is required`, `network_zones.${field}`));
|
|
90
|
+
}
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function validatePublicEndpoints(endpoints, blockers) {
|
|
95
|
+
if (!Array.isArray(endpoints)) {
|
|
96
|
+
blockers.push(blocker('public_endpoints array is required', 'public_endpoints'));
|
|
97
|
+
return { checked: 0, requiredCovered: 0 };
|
|
98
|
+
}
|
|
99
|
+
const names = new Set();
|
|
100
|
+
endpoints.forEach((endpoint, index) => {
|
|
101
|
+
const path = `public_endpoints[${index}]`;
|
|
102
|
+
if (!isPlainObject(endpoint)) {
|
|
103
|
+
blockers.push(blocker('public endpoint must be object', path));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
for (const field of ['name', 'service', 'method', 'path', 'rate_limit_ref', 'owner']) {
|
|
107
|
+
if (!nonEmptyString(endpoint[field])) blockers.push(blocker(`${path}.${field} is required`, `${path}.${field}`));
|
|
108
|
+
}
|
|
109
|
+
if (nonEmptyString(endpoint.name)) names.add(endpoint.name);
|
|
110
|
+
if (endpoint.public !== true) blockers.push(blocker(`${path}.public must be true`, `${path}.public`));
|
|
111
|
+
if (endpoint.tls_required !== true) blockers.push(blocker(`${path}.tls_required must be true`, `${path}.tls_required`));
|
|
112
|
+
if (String(endpoint.method ?? '').toUpperCase() !== 'GET') blockers.push(blocker(`${path}.method must be GET`, `${path}.method`));
|
|
113
|
+
if (!ALLOWED_PUBLIC_PROBE_PATHS.has(endpoint.path)) blockers.push(blocker(`${path}.path must be /livez or /readyz only`, `${path}.path`));
|
|
114
|
+
if (/admin|policy|siem|gateway\/evaluate|gateway\/decision|relay\/records/i.test(String(endpoint.path ?? ''))) blockers.push(blocker(`${path}.path exposes private admin or data-plane route`, `${path}.path`));
|
|
115
|
+
});
|
|
116
|
+
for (const probe of REQUIRED_PUBLIC_PROBES) {
|
|
117
|
+
if (!names.has(probe)) blockers.push(blocker(`public_endpoints must include ${probe}`, 'public_endpoints'));
|
|
118
|
+
}
|
|
119
|
+
return { checked: endpoints.length, requiredCovered: REQUIRED_PUBLIC_PROBES.filter((probe) => names.has(probe)).length };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function validatePrivateRoutes(routes, blockers) {
|
|
123
|
+
if (!Array.isArray(routes)) {
|
|
124
|
+
blockers.push(blocker('private_routes array is required', 'private_routes'));
|
|
125
|
+
return { checked: 0, requiredCovered: 0 };
|
|
126
|
+
}
|
|
127
|
+
const names = new Set();
|
|
128
|
+
routes.forEach((route, index) => {
|
|
129
|
+
const path = `private_routes[${index}]`;
|
|
130
|
+
if (!isPlainObject(route)) {
|
|
131
|
+
blockers.push(blocker('private route must be object', path));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
for (const field of ['name', 'service', 'method', 'path', 'network_ref', 'auth_mode', 'auth_config_ref', 'audit_ref', 'owner']) {
|
|
135
|
+
if (!nonEmptyString(route[field])) blockers.push(blocker(`${path}.${field} is required`, `${path}.${field}`));
|
|
136
|
+
}
|
|
137
|
+
if (nonEmptyString(route.name)) names.add(route.name);
|
|
138
|
+
if (route.public !== false) blockers.push(blocker(`${path}.public must be false`, `${path}.public`));
|
|
139
|
+
if (route.tls_required !== true) blockers.push(blocker(`${path}.tls_required must be true`, `${path}.tls_required`));
|
|
140
|
+
if (!ALLOWED_PRIVATE_AUTH_MODES.has(String(route.auth_mode ?? '').toLowerCase())) blockers.push(blocker(`${path}.auth_mode must be bearer_hash/mtls/private_ingress/zero_trust_access`, `${path}.auth_mode`));
|
|
141
|
+
});
|
|
142
|
+
for (const route of REQUIRED_PRIVATE_ROUTES) {
|
|
143
|
+
if (!names.has(route)) blockers.push(blocker(`private_routes must include ${route}`, 'private_routes'));
|
|
144
|
+
}
|
|
145
|
+
return { checked: routes.length, requiredCovered: REQUIRED_PRIVATE_ROUTES.filter((route) => names.has(route)).length };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function validateLimits(limits, blockers) {
|
|
149
|
+
if (!isPlainObject(limits)) {
|
|
150
|
+
blockers.push(blocker('limits object is required', 'limits'));
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
for (const field of ['max_request_bytes', 'max_requests_per_minute', 'body_timeout_seconds']) {
|
|
154
|
+
if (!positiveNumber(limits[field])) blockers.push(blocker(`limits.${field} must be positive number`, `limits.${field}`));
|
|
155
|
+
}
|
|
156
|
+
if (!nonEmptyString(limits.enforcement_ref)) blockers.push(blocker('limits.enforcement_ref is required', 'limits.enforcement_ref'));
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function validateEgress(egress, blockers) {
|
|
161
|
+
if (!isPlainObject(egress)) {
|
|
162
|
+
blockers.push(blocker('egress object is required', 'egress'));
|
|
163
|
+
return { checked: 0 };
|
|
164
|
+
}
|
|
165
|
+
if (egress.default_denied !== true) blockers.push(blocker('egress.default_denied must be true', 'egress.default_denied'));
|
|
166
|
+
if (!nonEmptyString(egress.policy_ref)) blockers.push(blocker('egress.policy_ref is required', 'egress.policy_ref'));
|
|
167
|
+
const destinations = egress.allowed_destinations;
|
|
168
|
+
if (!Array.isArray(destinations) || destinations.length === 0) {
|
|
169
|
+
blockers.push(blocker('egress.allowed_destinations array is required', 'egress.allowed_destinations'));
|
|
170
|
+
return { checked: 0 };
|
|
171
|
+
}
|
|
172
|
+
destinations.forEach((destination, index) => {
|
|
173
|
+
const path = `egress.allowed_destinations[${index}]`;
|
|
174
|
+
if (!isPlainObject(destination)) {
|
|
175
|
+
blockers.push(blocker('allowed destination must be object', path));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
for (const field of ['name', 'destination_ref', 'purpose', 'owner']) {
|
|
179
|
+
if (!nonEmptyString(destination[field])) blockers.push(blocker(`${path}.${field} is required`, `${path}.${field}`));
|
|
180
|
+
}
|
|
181
|
+
if (destination.sensitive_content_allowed !== false) blockers.push(blocker(`${path}.sensitive_content_allowed must be false`, `${path}.sensitive_content_allowed`));
|
|
182
|
+
});
|
|
183
|
+
return { checked: destinations.length };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function validateBreakGlass(breakGlass, blockers) {
|
|
187
|
+
if (!isPlainObject(breakGlass)) {
|
|
188
|
+
blockers.push(blocker('break_glass object is required', 'break_glass'));
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
for (const field of ['approval_ref', 'audit_ref', 'owner', 'expiry_policy_ref']) {
|
|
192
|
+
if (!nonEmptyString(breakGlass[field])) blockers.push(blocker(`break_glass.${field} is required`, `break_glass.${field}`));
|
|
193
|
+
}
|
|
194
|
+
if (!positiveNumber(breakGlass.max_session_seconds)) blockers.push(blocker('break_glass.max_session_seconds must be positive number', 'break_glass.max_session_seconds'));
|
|
195
|
+
if (breakGlass.enabled_without_approval !== false) blockers.push(blocker('break_glass.enabled_without_approval must be false', 'break_glass.enabled_without_approval'));
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function validateNetworkAccessPolicy(policy, options = {}) {
|
|
200
|
+
if (!isPlainObject(policy)) throw new Error('network access policy must be an object');
|
|
201
|
+
assertNoSecrets(policy);
|
|
202
|
+
const blockers = [];
|
|
203
|
+
if (policy.schema !== NETWORK_ACCESS_POLICY_SCHEMA) blockers.push(blocker('schema mismatch', 'schema'));
|
|
204
|
+
validateMetadata(policy.metadata, blockers);
|
|
205
|
+
const zonesOk = validateZones(policy.network_zones, blockers);
|
|
206
|
+
const publicEndpoints = validatePublicEndpoints(policy.public_endpoints, blockers);
|
|
207
|
+
const privateRoutes = validatePrivateRoutes(policy.private_routes, blockers);
|
|
208
|
+
const limitsOk = validateLimits(policy.limits, blockers);
|
|
209
|
+
const egress = validateEgress(policy.egress, blockers);
|
|
210
|
+
const breakGlassOk = validateBreakGlass(policy.break_glass, blockers);
|
|
211
|
+
if (policy.no_token_values_in_policy !== true) blockers.push(blocker('no_token_values_in_policy must be true', 'no_token_values_in_policy'));
|
|
212
|
+
|
|
213
|
+
const result = {
|
|
214
|
+
schema: NETWORK_ACCESS_POLICY_RESULT_SCHEMA,
|
|
215
|
+
generated_at: options.generated_at ?? options.generatedAt ?? new Date(0).toISOString(),
|
|
216
|
+
ok: blockers.length === 0,
|
|
217
|
+
status: blockers.length === 0 ? 'accepted' : 'blocked',
|
|
218
|
+
blockers,
|
|
219
|
+
checked: {
|
|
220
|
+
zones: zonesOk,
|
|
221
|
+
required_public_probes: REQUIRED_PUBLIC_PROBES.length,
|
|
222
|
+
public_probes_covered: publicEndpoints.requiredCovered,
|
|
223
|
+
public_endpoints: publicEndpoints.checked,
|
|
224
|
+
required_private_routes: REQUIRED_PRIVATE_ROUTES.length,
|
|
225
|
+
private_routes_covered: privateRoutes.requiredCovered,
|
|
226
|
+
private_routes: privateRoutes.checked,
|
|
227
|
+
limits: limitsOk,
|
|
228
|
+
egress_destinations: egress.checked,
|
|
229
|
+
break_glass: breakGlassOk,
|
|
230
|
+
},
|
|
231
|
+
claim_boundary: [
|
|
232
|
+
'Network access validation checks declared network policy shape only; it does not configure firewalls, ingress, DNS, WAF, identity providers, or cloud networks.',
|
|
233
|
+
'Public endpoint acceptance is limited to /livez and /readyz probes; admin and data-plane routes must remain private and authenticated.',
|
|
234
|
+
'Secrets, bearer values, private keys, raw memory, prompts, transcripts, provider responses, decrypted content, and credentials must remain outside network policy artifacts.',
|
|
235
|
+
],
|
|
236
|
+
};
|
|
237
|
+
assertNoSecrets(result, 'result');
|
|
238
|
+
return result;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function parseArgs(argv = process.argv.slice(2)) {
|
|
242
|
+
const flags = new Map();
|
|
243
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
244
|
+
const arg = argv[index];
|
|
245
|
+
if (!arg.startsWith('--')) continue;
|
|
246
|
+
const eq = arg.indexOf('=');
|
|
247
|
+
if (eq !== -1) flags.set(arg.slice(2, eq), arg.slice(eq + 1));
|
|
248
|
+
else if (!argv[index + 1] || argv[index + 1].startsWith('--')) flags.set(arg.slice(2), true);
|
|
249
|
+
else {
|
|
250
|
+
flags.set(arg.slice(2), argv[index + 1]);
|
|
251
|
+
index += 1;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return flags;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function getFlag(flags, names) {
|
|
258
|
+
for (const name of names) if (flags.has(name)) return flags.get(name);
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async function main() {
|
|
263
|
+
const flags = parseArgs();
|
|
264
|
+
const policyPath = getFlag(flags, ['policy', 'in']);
|
|
265
|
+
if (!nonEmptyString(policyPath)) throw new Error('--policy <path> is required');
|
|
266
|
+
const policy = JSON.parse(await readFile(resolve(String(policyPath)), 'utf8'));
|
|
267
|
+
const result = validateNetworkAccessPolicy(policy, { generated_at: new Date().toISOString() });
|
|
268
|
+
const out = getFlag(flags, ['out']);
|
|
269
|
+
const text = `${JSON.stringify(result, null, 2)}\n`;
|
|
270
|
+
if (out && out !== true) {
|
|
271
|
+
const outPath = resolve(String(out));
|
|
272
|
+
await mkdir(dirname(outPath), { recursive: true });
|
|
273
|
+
await writeFile(outPath, text, 'utf8');
|
|
274
|
+
}
|
|
275
|
+
process.stdout.write(text);
|
|
276
|
+
process.exitCode = result.ok ? 0 : 1;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const invokedPath = process.argv[1] ? resolve(process.argv[1]) : '';
|
|
280
|
+
if (invokedPath === fileURLToPath(import.meta.url)) {
|
|
281
|
+
main().catch((error) => {
|
|
282
|
+
process.stderr.write(`${error.name ?? 'Error'}: ${error.message}\n`);
|
|
283
|
+
process.exitCode = 1;
|
|
284
|
+
});
|
|
285
|
+
}
|