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,298 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
export const OPERATOR_ACCEPTANCE_PACKET_SCHEMA = 'enigma.operator_acceptance_packet.v1';
|
|
7
|
+
export const OPERATOR_ACCEPTANCE_RESULT_SCHEMA = 'enigma.operator_acceptance_result.v1';
|
|
8
|
+
|
|
9
|
+
export const REQUIRED_PACKET_METADATA = Object.freeze([
|
|
10
|
+
'packet_id',
|
|
11
|
+
'customer_or_tenant',
|
|
12
|
+
'deployment_mode',
|
|
13
|
+
'environment',
|
|
14
|
+
'target_regions',
|
|
15
|
+
'requested_go_live_date',
|
|
16
|
+
'evidence_repository',
|
|
17
|
+
'packet_owner',
|
|
18
|
+
'last_updated',
|
|
19
|
+
'decision',
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
export const REQUIRED_OWNER_ROLES = Object.freeze([
|
|
23
|
+
'business_owner',
|
|
24
|
+
'enigma_operator_owner',
|
|
25
|
+
'customer_infrastructure_owner',
|
|
26
|
+
'security_owner',
|
|
27
|
+
'legal_privacy_owner',
|
|
28
|
+
'incident_commander',
|
|
29
|
+
'support_sla_owner',
|
|
30
|
+
'tenant_policy_owner',
|
|
31
|
+
'backup_restore_owner',
|
|
32
|
+
'kms_secrets_owner',
|
|
33
|
+
'siem_log_owner',
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
export const REQUIRED_EVIDENCE_ITEMS = Object.freeze([
|
|
37
|
+
'infrastructure_readiness_json',
|
|
38
|
+
'deployment_manifests',
|
|
39
|
+
'runtime_auth_and_operator_access',
|
|
40
|
+
'network_access_policy',
|
|
41
|
+
'domain_tls_and_public_endpoint_checks',
|
|
42
|
+
'durable_storage',
|
|
43
|
+
'kms_signer_key_refs',
|
|
44
|
+
'kms_custody',
|
|
45
|
+
'tenant_policy',
|
|
46
|
+
'tenant_policy_approval',
|
|
47
|
+
'runtime_health_and_readiness_json',
|
|
48
|
+
'hosted_backend_live_evidence',
|
|
49
|
+
'gateway_allow_deny_checks',
|
|
50
|
+
'usage_metering',
|
|
51
|
+
'service_settlement',
|
|
52
|
+
'public_site_security',
|
|
53
|
+
'security_threat_model',
|
|
54
|
+
'cloudflare_pages_release_packet',
|
|
55
|
+
'cloudflare_token_policy',
|
|
56
|
+
'production_handoff_packet',
|
|
57
|
+
'goal_completion_audit',
|
|
58
|
+
'relay_plaintext_rejection',
|
|
59
|
+
'witness_checkpoint_minimization',
|
|
60
|
+
'audit_outbox_siem',
|
|
61
|
+
'monitoring_alerting',
|
|
62
|
+
'offline_verification',
|
|
63
|
+
'backup_restore_rehearsal',
|
|
64
|
+
'incident_drill',
|
|
65
|
+
'support_sla',
|
|
66
|
+
'legal_compliance_approval',
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
const GO_DECISIONS = new Set(['go']);
|
|
70
|
+
const ACCEPTED_STATUSES = new Set(['complete', 'go', 'accepted', 'verified', 'approved']);
|
|
71
|
+
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/@]+@|sk-[A-Za-z0-9_-]{16,}|AKIA[0-9A-Z]{16}|raw memory|private prompt|full transcript|decrypted capsule)/iu;
|
|
72
|
+
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;
|
|
73
|
+
const TEMPLATE_PLACEHOLDER_RE = /<[^<>\r\n]+>/u;
|
|
74
|
+
const SAFE_SECRET_NAMED_KEYS = new Set(['kms_secrets_owner', 'relay_plaintext_rejection', 'kms_or_secret_custody', 'cloudflare_token_policy', 'goal_completion_audit', 'production_manifests', 'required_secret_count']);
|
|
75
|
+
|
|
76
|
+
function isPlainObject(value) {
|
|
77
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function nonEmptyString(value) {
|
|
81
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
82
|
+
}
|
|
83
|
+
function assertNoSecretPacketData(value, path = 'packet') {
|
|
84
|
+
if (typeof value === 'string') {
|
|
85
|
+
if (!/\.claim_boundary\[\d+\]$/.test(path) && SECRET_VALUE_RE.test(value)) throw new Error(`${path} contains secret or raw-memory-looking data`);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (Array.isArray(value)) {
|
|
89
|
+
value.forEach((item, index) => assertNoSecretPacketData(item, `${path}[${index}]`));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (!isPlainObject(value)) return;
|
|
93
|
+
for (const [key, child] of Object.entries(value)) {
|
|
94
|
+
if (SECRET_KEY_RE.test(key) && !SAFE_SECRET_NAMED_KEYS.has(key)) throw new Error(`${path}.${key} uses a forbidden evidence field name`);
|
|
95
|
+
assertNoSecretPacketData(child, `${path}.${key}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function collectTemplatePlaceholderBlockers(value, blockers, path = 'packet') {
|
|
100
|
+
if (typeof value === 'string') {
|
|
101
|
+
if (TEMPLATE_PLACEHOLDER_RE.test(value)) blockers.push(blocker(`${path} must not contain unresolved template placeholders`, path));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (Array.isArray(value)) {
|
|
105
|
+
value.forEach((item, index) => collectTemplatePlaceholderBlockers(item, blockers, `${path}[${index}]`));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (!isPlainObject(value)) return;
|
|
109
|
+
for (const [key, child] of Object.entries(value)) collectTemplatePlaceholderBlockers(child, blockers, `${path}.${key}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function statusComplete(value) {
|
|
113
|
+
return typeof value === 'string' && ACCEPTED_STATUSES.has(value.trim().toLowerCase());
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function evidenceRefComplete(item) {
|
|
117
|
+
return isPlainObject(item) && nonEmptyString(item.ref) && statusComplete(item.status);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function ownerComplete(owner) {
|
|
121
|
+
return isPlainObject(owner)
|
|
122
|
+
&& nonEmptyString(owner.name)
|
|
123
|
+
&& nonEmptyString(owner.organization)
|
|
124
|
+
&& nonEmptyString(owner.approval_ref)
|
|
125
|
+
&& statusComplete(owner.approval_status);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function blocker(message, path) {
|
|
129
|
+
return { message, path };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function blockerBreakdown(blockers) {
|
|
133
|
+
const categories = {
|
|
134
|
+
metadata: 0,
|
|
135
|
+
owners: 0,
|
|
136
|
+
evidence: 0,
|
|
137
|
+
readiness: 0,
|
|
138
|
+
manifest: 0,
|
|
139
|
+
storage: 0,
|
|
140
|
+
release_audit: 0,
|
|
141
|
+
production_manifests: 0,
|
|
142
|
+
other: 0,
|
|
143
|
+
};
|
|
144
|
+
for (const item of blockers) {
|
|
145
|
+
const root = String(item?.path ?? '').split('.')[0];
|
|
146
|
+
if (Object.hasOwn(categories, root)) categories[root] += 1;
|
|
147
|
+
else categories.other += 1;
|
|
148
|
+
}
|
|
149
|
+
return Object.fromEntries(Object.entries(categories).filter(([, count]) => count > 0));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function validateOperatorAcceptancePacket(packet, options = {}) {
|
|
153
|
+
if (!isPlainObject(packet)) throw new Error('operator acceptance packet must be an object');
|
|
154
|
+
assertNoSecretPacketData(packet);
|
|
155
|
+
const blockers = [];
|
|
156
|
+
collectTemplatePlaceholderBlockers(packet, blockers);
|
|
157
|
+
const warnings = [];
|
|
158
|
+
if (packet.schema !== OPERATOR_ACCEPTANCE_PACKET_SCHEMA) blockers.push(blocker('packet schema mismatch', 'schema'));
|
|
159
|
+
const metadata = packet.metadata;
|
|
160
|
+
if (!isPlainObject(metadata)) blockers.push(blocker('metadata object is required', 'metadata'));
|
|
161
|
+
else {
|
|
162
|
+
for (const field of REQUIRED_PACKET_METADATA) {
|
|
163
|
+
if (!nonEmptyString(metadata[field])) blockers.push(blocker(`metadata.${field} is required`, `metadata.${field}`));
|
|
164
|
+
}
|
|
165
|
+
if (!GO_DECISIONS.has(String(metadata.decision ?? '').toLowerCase())) blockers.push(blocker('metadata.decision must be go for live acceptance', 'metadata.decision'));
|
|
166
|
+
if (!['hosted', 'byoc', 'on-prem-air-gapped', 'on_prem_air_gapped'].includes(String(metadata.deployment_mode ?? '').toLowerCase())) warnings.push(blocker('deployment_mode is not a recognized hosted/BYOC/on-prem value', 'metadata.deployment_mode'));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const owners = packet.owners;
|
|
170
|
+
if (!isPlainObject(owners)) blockers.push(blocker('owners object is required', 'owners'));
|
|
171
|
+
else {
|
|
172
|
+
for (const role of REQUIRED_OWNER_ROLES) {
|
|
173
|
+
if (!ownerComplete(owners[role])) blockers.push(blocker(`owner ${role} must include name organization approval_ref and complete approval_status`, `owners.${role}`));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const evidence = packet.evidence;
|
|
178
|
+
if (!isPlainObject(evidence)) blockers.push(blocker('evidence object is required', 'evidence'));
|
|
179
|
+
else {
|
|
180
|
+
for (const key of REQUIRED_EVIDENCE_ITEMS) {
|
|
181
|
+
if (!evidenceRefComplete(evidence[key])) blockers.push(blocker(`evidence.${key} must include ref and complete status`, `evidence.${key}`));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const readiness = packet.readiness;
|
|
186
|
+
if (!isPlainObject(readiness)) blockers.push(blocker('readiness object is required', 'readiness'));
|
|
187
|
+
else {
|
|
188
|
+
if (readiness.schema !== 'enigma.infrastructure_readiness.v1') blockers.push(blocker('readiness.schema must be enigma.infrastructure_readiness.v1', 'readiness.schema'));
|
|
189
|
+
if (readiness.ok !== true) blockers.push(blocker('readiness.ok must be true', 'readiness.ok'));
|
|
190
|
+
if (readiness.readiness?.hosted_live_ready !== true) blockers.push(blocker('readiness.hosted_live_ready must be true', 'readiness.readiness.hosted_live_ready'));
|
|
191
|
+
if (!Array.isArray(readiness.external_blockers) || readiness.external_blockers.length !== 0) blockers.push(blocker('readiness.external_blockers must be empty', 'readiness.external_blockers'));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const manifest = packet.manifest;
|
|
195
|
+
if (!isPlainObject(manifest)) blockers.push(blocker('manifest object is required', 'manifest'));
|
|
196
|
+
else {
|
|
197
|
+
if (manifest.schema !== 'enigma.infrastructure_readiness_manifest.v1') blockers.push(blocker('manifest schema mismatch', 'manifest.schema'));
|
|
198
|
+
if (manifest.operator_acceptance?.decision !== 'go') blockers.push(blocker('manifest operator decision must be go', 'manifest.operator_acceptance.decision'));
|
|
199
|
+
if (!Array.isArray(manifest.external_blockers) || manifest.external_blockers.length !== 0) blockers.push(blocker('manifest external_blockers must be empty', 'manifest.external_blockers'));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const storage = packet.storage;
|
|
203
|
+
if (!isPlainObject(storage)) blockers.push(blocker('storage object is required', 'storage'));
|
|
204
|
+
else {
|
|
205
|
+
if (storage.schema !== 'enigma.production_storage_migration_artifact.v1') blockers.push(blocker('storage schema must be enigma.production_storage_migration_artifact.v1', 'storage.schema'));
|
|
206
|
+
if (storage.contract?.engine !== 'postgres') blockers.push(blocker('storage contract engine must be postgres', 'storage.contract.engine'));
|
|
207
|
+
if (!Array.isArray(storage.contract?.tables) || storage.contract.tables.length < 8) blockers.push(blocker('storage contract tables are incomplete', 'storage.contract.tables'));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const releaseAudit = packet.release_audit;
|
|
211
|
+
if (!isPlainObject(releaseAudit)) blockers.push(blocker('release_audit object is required', 'release_audit'));
|
|
212
|
+
else {
|
|
213
|
+
if (releaseAudit.schema !== 'enigma.release_audit.v1') blockers.push(blocker('release_audit schema mismatch', 'release_audit.schema'));
|
|
214
|
+
if (releaseAudit.ok !== true) blockers.push(blocker('release_audit.ok must be true', 'release_audit.ok'));
|
|
215
|
+
if (Array.isArray(releaseAudit.required_failed) && releaseAudit.required_failed.length !== 0) blockers.push(blocker('release_audit.required_failed must be empty', 'release_audit.required_failed'));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const productionManifests = packet.production_manifests;
|
|
219
|
+
if (!isPlainObject(productionManifests)) blockers.push(blocker('production_manifests object is required', 'production_manifests'));
|
|
220
|
+
else {
|
|
221
|
+
if (productionManifests.schema !== 'enigma.production_manifest_result.v1') blockers.push(blocker('production_manifests schema mismatch', 'production_manifests.schema'));
|
|
222
|
+
if (productionManifests.ok !== true) blockers.push(blocker('production_manifests.ok must be true', 'production_manifests.ok'));
|
|
223
|
+
if (productionManifests.status !== 'accepted') blockers.push(blocker('production_manifests.status must be accepted', 'production_manifests.status'));
|
|
224
|
+
if (!Array.isArray(productionManifests.blockers) || productionManifests.blockers.length !== 0) blockers.push(blocker('production_manifests.blockers must be empty', 'production_manifests.blockers'));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const result = {
|
|
228
|
+
schema: OPERATOR_ACCEPTANCE_RESULT_SCHEMA,
|
|
229
|
+
generated_at: options.generated_at ?? options.generatedAt ?? new Date(0).toISOString(),
|
|
230
|
+
ok: blockers.length === 0,
|
|
231
|
+
decision: blockers.length === 0 ? 'go' : 'blocked',
|
|
232
|
+
blockers,
|
|
233
|
+
blocker_breakdown: blockerBreakdown(blockers),
|
|
234
|
+
warnings,
|
|
235
|
+
checked: {
|
|
236
|
+
metadata_fields: REQUIRED_PACKET_METADATA.length,
|
|
237
|
+
owner_roles: REQUIRED_OWNER_ROLES.length,
|
|
238
|
+
evidence_items: REQUIRED_EVIDENCE_ITEMS.length,
|
|
239
|
+
readiness_required: true,
|
|
240
|
+
storage_required: true,
|
|
241
|
+
release_audit_required: true,
|
|
242
|
+
production_manifests_required: true,
|
|
243
|
+
},
|
|
244
|
+
claim_boundary: [
|
|
245
|
+
'Operator acceptance validation checks evidence shape and no-go blockers only; it does not create cloud resources or verify secrets.',
|
|
246
|
+
'A go result requires target-environment readiness evidence with hosted_live_ready true and no external blockers.',
|
|
247
|
+
'Secret values and raw memory must remain outside packets, manifests, logs, docs, and chat.',
|
|
248
|
+
],
|
|
249
|
+
};
|
|
250
|
+
assertNoSecretPacketData(result, 'result');
|
|
251
|
+
return result;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function parseArgs(argv = process.argv.slice(2)) {
|
|
255
|
+
const flags = new Map();
|
|
256
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
257
|
+
const arg = argv[index];
|
|
258
|
+
if (!arg.startsWith('--')) continue;
|
|
259
|
+
const eq = arg.indexOf('=');
|
|
260
|
+
if (eq !== -1) flags.set(arg.slice(2, eq), arg.slice(eq + 1));
|
|
261
|
+
else if (!argv[index + 1] || argv[index + 1].startsWith('--')) flags.set(arg.slice(2), true);
|
|
262
|
+
else {
|
|
263
|
+
flags.set(arg.slice(2), argv[index + 1]);
|
|
264
|
+
index += 1;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return flags;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function getFlag(flags, names) {
|
|
271
|
+
for (const name of names) if (flags.has(name)) return flags.get(name);
|
|
272
|
+
return undefined;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function main() {
|
|
276
|
+
const flags = parseArgs();
|
|
277
|
+
const packetPath = getFlag(flags, ['packet', 'in']);
|
|
278
|
+
if (!nonEmptyString(packetPath)) throw new Error('--packet <path> is required');
|
|
279
|
+
const packet = JSON.parse(await readFile(resolve(packetPath), 'utf8'));
|
|
280
|
+
const result = validateOperatorAcceptancePacket(packet, { generated_at: new Date().toISOString() });
|
|
281
|
+
const out = getFlag(flags, ['out']);
|
|
282
|
+
const text = `${JSON.stringify(result, null, 2)}\n`;
|
|
283
|
+
if (out && out !== true) {
|
|
284
|
+
const outPath = resolve(String(out));
|
|
285
|
+
await mkdir(dirname(outPath), { recursive: true });
|
|
286
|
+
await writeFile(outPath, text, 'utf8');
|
|
287
|
+
}
|
|
288
|
+
process.stdout.write(text);
|
|
289
|
+
process.exitCode = result.ok ? 0 : 1;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const invokedPath = process.argv[1] ? resolve(process.argv[1]) : '';
|
|
293
|
+
if (invokedPath === fileURLToPath(import.meta.url)) {
|
|
294
|
+
main().catch((error) => {
|
|
295
|
+
process.stderr.write(`${error.name ?? 'Error'}: ${error.message}\n`);
|
|
296
|
+
process.exitCode = 1;
|
|
297
|
+
});
|
|
298
|
+
}
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, relative, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
export const PRODUCTION_MANIFEST_RESULT_SCHEMA = 'enigma.production_manifest_result.v1';
|
|
7
|
+
|
|
8
|
+
const REQUIRED_HOSTED_ENV_REF_SPECS = Object.freeze([
|
|
9
|
+
{ id: 'ENIGMA_BACKEND_HOST_REF', pattern: /\bENIGMA_(?:BACKEND_HOST_REF|RELAY_BACKEND_HOST_REF|GATEWAY_BACKEND_HOST_REF|RELAY_DEPLOYMENT_REF|GATEWAY_DEPLOYMENT_REF)\b/ },
|
|
10
|
+
{ id: 'ENIGMA_DNS_TLS_REF', pattern: /\bENIGMA_(?:DNS_TLS_REF|TLS_REF|RELAY_DNS_TLS_REF|GATEWAY_DNS_TLS_REF)\b/ },
|
|
11
|
+
{ id: 'ENIGMA_DURABLE_STORAGE_REF', pattern: /\bENIGMA_(?:DURABLE_STORAGE_REF|EXTERNAL_STORAGE_REF|EXTERNAL_STORAGE_DSN|EXTERNAL_STORAGE_DSN_FILE|RELAY_STORAGE_REF|GATEWAY_STORAGE_REF)\b/ },
|
|
12
|
+
{ id: 'ENIGMA_KMS_KEY_REF', pattern: /\bENIGMA_(?:KMS_KEY_REF|KMS_KEY_REF_FILE|KMS_REF|EXTERNAL_KMS_REF|GATEWAY_SIGNER_REF|GATEWAY_SIGNING_KEY_FILE|RELAY_SIGNING_KEY_FILE)\b/ },
|
|
13
|
+
{ id: 'ENIGMA_MONITORING_REF', pattern: /\bENIGMA_(?:MONITORING_REF|RELAY_MONITORING_REF|GATEWAY_MONITORING_REF)\b/ },
|
|
14
|
+
{ id: 'ENIGMA_BACKUP_TARGET', pattern: /\bENIGMA_BACKUP_TARGET(?:_REF|_URI|_URI_FILE)\b/ },
|
|
15
|
+
{ id: 'ENIGMA_NETWORK_ACCESS_POLICY_REF', pattern: /\bENIGMA_NETWORK_ACCESS_POLICY_REF\b/ },
|
|
16
|
+
{ id: 'ENIGMA_KMS_CUSTODY_REF', pattern: /\bENIGMA_KMS_CUSTODY_REF\b/ },
|
|
17
|
+
{ id: 'ENIGMA_TENANT_POLICY_APPROVAL_REF', pattern: /\bENIGMA_TENANT_POLICY_APPROVAL_REF\b/ },
|
|
18
|
+
{ id: 'ENIGMA_USAGE_METERING_REF', pattern: /\bENIGMA_USAGE_METERING_REF\b/ },
|
|
19
|
+
{ id: 'ENIGMA_SERVICE_SETTLEMENT_REF', pattern: /\bENIGMA_SERVICE_SETTLEMENT_REF\b/ },
|
|
20
|
+
{ id: 'ENIGMA_MONITORING_ALERTING_REF', pattern: /\bENIGMA_MONITORING_ALERTING_REF\b/ },
|
|
21
|
+
{ id: 'ENIGMA_SIEM_REF', pattern: /\bENIGMA_(?:SIEM_REF|AUDIT_SINK_REF|LOG_SINK_REF|SIEM_EXPORT_ENDPOINT(?:_FILE)?)\b/ },
|
|
22
|
+
{ id: 'ENIGMA_PUBLIC_SITE_SECURITY_REF', pattern: /\bENIGMA_PUBLIC_SITE_SECURITY_REF\b/ },
|
|
23
|
+
{ id: 'ENIGMA_SECURITY_THREAT_MODEL_REF', pattern: /\bENIGMA_SECURITY_THREAT_MODEL_REF\b/ },
|
|
24
|
+
{ id: 'ENIGMA_LEGAL_COMPLIANCE_APPROVAL_REF', pattern: /\bENIGMA_LEGAL_COMPLIANCE_APPROVAL_REF\b/ },
|
|
25
|
+
{ id: 'ENIGMA_SUPPORT_SLA_REF', pattern: /\bENIGMA_SUPPORT_SLA_REF\b/ },
|
|
26
|
+
{ id: 'ENIGMA_INCIDENT_DRILL_REF', pattern: /\bENIGMA_INCIDENT_DRILL_REF\b/ },
|
|
27
|
+
{ id: 'ENIGMA_BACKUP_RESTORE_DRILL_REF', pattern: /\bENIGMA_BACKUP_RESTORE_DRILL_REF\b/ },
|
|
28
|
+
{ id: 'ENIGMA_OPERATOR_ACCEPTANCE_DECISION', pattern: /\bENIGMA_OPERATOR_ACCEPTANCE_DECISION\b/ },
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
export const REQUIRED_HOSTED_ENV_REFS = Object.freeze(REQUIRED_HOSTED_ENV_REF_SPECS.map((item) => item.id));
|
|
32
|
+
|
|
33
|
+
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/@]+@|sk-[A-Za-z0-9_-]{16,}|AKIA[0-9A-Z]{16}|raw memory|private prompt|full transcript|decrypted capsule)/iu;
|
|
34
|
+
|
|
35
|
+
const CLAIM_BOUNDARY = Object.freeze([
|
|
36
|
+
'This validator checks production-shaped backend manifests for fail-closed safety gates and required public-safe refs only.',
|
|
37
|
+
'Passing manifests are not hosted backend readiness until deployed endpoints, storage, KMS, SIEM, backup, monitoring, legal, support, and operator acceptance evidence are verified.',
|
|
38
|
+
'Manifest files and validator output must not contain token values, private keys, DSNs with embedded credentials, prompts, transcripts, decrypted capsules, or raw memory.',
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
function publicManifestPath(path, label) {
|
|
43
|
+
const rel = relative(process.cwd(), path).replace(/\\/g, '/');
|
|
44
|
+
if (rel.length > 0 && !rel.startsWith('../') && rel !== '..' && !/^[A-Za-z]:\//u.test(rel)) return rel;
|
|
45
|
+
return `<external-${label}-manifest>`;
|
|
46
|
+
}
|
|
47
|
+
function blocker(message, path) {
|
|
48
|
+
return { message, path };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function include(text, needle) {
|
|
52
|
+
return text.includes(needle);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function requireInclude(text, needle, path, blockers, message) {
|
|
56
|
+
if (!include(text, needle)) blockers.push(blocker(message ?? `missing ${needle}`, path));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function assertNoSecretValues(text, path, blockers) {
|
|
60
|
+
const match = SECRET_VALUE_RE.exec(text);
|
|
61
|
+
if (match) blockers.push(blocker('manifest contains secret-looking or raw-memory-looking literal', path));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function requiredHostedEnvRefCoverage(text) {
|
|
65
|
+
const present = REQUIRED_HOSTED_ENV_REF_SPECS.filter((spec) => spec.pattern.test(text)).map((spec) => spec.id);
|
|
66
|
+
const missing = REQUIRED_HOSTED_ENV_REF_SPECS.filter((spec) => !present.includes(spec.id)).map((spec) => spec.id);
|
|
67
|
+
return { present, missing };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function serviceBlock(text, service) {
|
|
71
|
+
const servicePattern = new RegExp(`\\n ${service}:\\n([\\s\\S]*?)(?=\\n [a-z][a-z0-9_-]*:\\n|\\nsecrets:\\n|$)`);
|
|
72
|
+
return servicePattern.exec(text)?.[1] ?? null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function composePortEntries(block) {
|
|
76
|
+
const match = /ports:\s*\n((?:\s*-\s+"[^"]+"\s*\n?)+)/.exec(block);
|
|
77
|
+
if (!match) return null;
|
|
78
|
+
return [...match[1].matchAll(/-\s+"([^"]+)"/g)].map((item) => item[1]);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isLoopbackPortMapping(value) {
|
|
82
|
+
return /^127\.0\.0\.1:\d+:\d+$/.test(value);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function composeServiceChecks(text, service, blockers) {
|
|
86
|
+
const block = serviceBlock(text, service);
|
|
87
|
+
if (!block) {
|
|
88
|
+
blockers.push(blocker(`compose missing ${service} service`, `compose.services.${service}`));
|
|
89
|
+
return { service, ok: false, hosted_readiness_ref_count: 0 };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
requireInclude(block, 'NODE_ENV: production', `compose.services.${service}.environment.NODE_ENV`, blockers, `${service} must set NODE_ENV production`);
|
|
93
|
+
requireInclude(block, 'ENIGMA_BACKEND_MODE: production', `compose.services.${service}.environment.ENIGMA_BACKEND_MODE`, blockers, `${service} must set backend production mode`);
|
|
94
|
+
requireInclude(block, 'ENIGMA_DISABLE_LOCAL_DEMO_FALLBACK: "true"', `compose.services.${service}.environment.ENIGMA_DISABLE_LOCAL_DEMO_FALLBACK`, blockers, `${service} must disable local demo fallback`);
|
|
95
|
+
requireInclude(block, 'ENIGMA_REQUIRE_OPERATOR_ACCEPTANCE_EVIDENCE: "true"', `compose.services.${service}.environment.ENIGMA_REQUIRE_OPERATOR_ACCEPTANCE_EVIDENCE`, blockers, `${service} must require operator acceptance evidence`);
|
|
96
|
+
requireInclude(block, 'ENIGMA_READINESS_FAIL_CLOSED: "true"', `compose.services.${service}.environment.ENIGMA_READINESS_FAIL_CLOSED`, blockers, `${service} must fail closed for readiness`);
|
|
97
|
+
requireInclude(block, 'read_only: true', `compose.services.${service}.read_only`, blockers, `${service} must use read_only filesystem`);
|
|
98
|
+
requireInclude(block, 'user: "1000:1000"', `compose.services.${service}.user`, blockers, `${service} must run as non-root fixed uid/gid`);
|
|
99
|
+
requireInclude(block, 'cap_drop: ["ALL"]', `compose.services.${service}.cap_drop`, blockers, `${service} must drop all Linux capabilities`);
|
|
100
|
+
requireInclude(block, 'no-new-privileges:true', `compose.services.${service}.security_opt`, blockers, `${service} must set no-new-privileges`);
|
|
101
|
+
requireInclude(block, 'tmpfs:', `compose.services.${service}.tmpfs`, blockers, `${service} must use explicit tmpfs for writable temp`);
|
|
102
|
+
requireInclude(block, '/livez', `compose.services.${service}.healthcheck`, blockers, `${service} healthcheck must probe /livez`);
|
|
103
|
+
requireInclude(block, '/readyz', `compose.services.${service}.healthcheck`, blockers, `${service} healthcheck must probe /readyz`);
|
|
104
|
+
const portEntries = composePortEntries(block);
|
|
105
|
+
if (!portEntries || portEntries.length === 0) {
|
|
106
|
+
blockers.push(blocker(`${service} must declare loopback-only compose port mapping`, `compose.services.${service}.ports`));
|
|
107
|
+
} else {
|
|
108
|
+
portEntries.forEach((entry, index) => {
|
|
109
|
+
if (!isLoopbackPortMapping(entry)) blockers.push(blocker(`${service} port mapping ${entry} must bind to 127.0.0.1 only`, `compose.services.${service}.ports[${index}]`));
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
if (/image:\s*[^\n]*:(?:latest|dev|main)\b/.test(block)) blockers.push(blocker(`${service} image must not use mutable latest/dev/main tag`, `compose.services.${service}.image`));
|
|
113
|
+
const refs = requiredHostedEnvRefCoverage(block);
|
|
114
|
+
for (const name of refs.missing) blockers.push(blocker(`${service} missing hosted readiness ref ${name}`, `compose.services.${service}.environment.${name}`));
|
|
115
|
+
return { service, ok: refs.missing.length === 0, hosted_readiness_ref_count: refs.present.length };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function validateProductionComposeManifestText(text, options = {}) {
|
|
119
|
+
const blockers = [];
|
|
120
|
+
assertNoSecretValues(text, 'compose', blockers);
|
|
121
|
+
const relay = composeServiceChecks(text, 'relay', blockers);
|
|
122
|
+
const gateway = composeServiceChecks(text, 'gateway', blockers);
|
|
123
|
+
requireInclude(text, 'ENIGMA_REQUIRE_EXTERNAL_STORAGE: "true"', 'compose.services.relay.environment.ENIGMA_REQUIRE_EXTERNAL_STORAGE', blockers, 'relay must require external storage');
|
|
124
|
+
requireInclude(text, 'ENIGMA_REQUIRE_EXTERNAL_KMS: "true"', 'compose.environment.ENIGMA_REQUIRE_EXTERNAL_KMS', blockers, 'compose services must require external KMS');
|
|
125
|
+
requireInclude(text, 'ENIGMA_REQUIRE_SIEM_EXPORT: "true"', 'compose.services.gateway.environment.ENIGMA_REQUIRE_SIEM_EXPORT', blockers, 'gateway must require SIEM export');
|
|
126
|
+
for (const secret of ['relay_signing_key', 'gateway_signing_key', 'external_storage_dsn', 'kms_key_ref', 'backup_target_uri', 'siem_export_endpoint', 'operator_acceptance_evidence_uri']) {
|
|
127
|
+
if (!new RegExp(`\\n ${secret}:\\n\\s+file:\\s+\\./operator-secrets/`).test(text)) blockers.push(blocker(`compose secret ${secret} must reference operator-secrets file`, `compose.secrets.${secret}`));
|
|
128
|
+
}
|
|
129
|
+
const allRefs = requiredHostedEnvRefCoverage(text);
|
|
130
|
+
for (const name of allRefs.missing) blockers.push(blocker(`compose manifest missing hosted readiness ref ${name}`, `compose.environment.${name}`));
|
|
131
|
+
return {
|
|
132
|
+
path: options.path ?? null,
|
|
133
|
+
ok: blockers.length === 0,
|
|
134
|
+
blockers,
|
|
135
|
+
relay,
|
|
136
|
+
gateway,
|
|
137
|
+
relay_fail_closed: true,
|
|
138
|
+
gateway_fail_closed: true,
|
|
139
|
+
public_ports_loopback_only: blockers.every((item) => !item.path.startsWith('compose.services.') || !item.path.includes('.ports')),
|
|
140
|
+
hosted_readiness_ref_count: allRefs.present.length,
|
|
141
|
+
required_hosted_ref_count: REQUIRED_HOSTED_ENV_REFS.length,
|
|
142
|
+
required_secret_count: 7,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function documentsOfKind(text, kind) {
|
|
147
|
+
return text.split(/^---\s*$/m).filter((item) => new RegExp(`\\bkind:\\s*${kind}\\b`).test(item));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function documentName(document) {
|
|
151
|
+
return /\bname:\s*([^\s]+)/.exec(document)?.[1] ?? '<unnamed>';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function ingressBlock(text, name) {
|
|
155
|
+
return documentsOfKind(text, 'Ingress').find((item) => documentName(item) === name);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function documentBlock(text, kind, name) {
|
|
159
|
+
return documentsOfKind(text, kind).find((item) => documentName(item) === name);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function deploymentBlock(text, name) {
|
|
163
|
+
return documentBlock(text, 'Deployment', name);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function isPrivateIngress(document) {
|
|
167
|
+
return /operator-selected-private-ingress-class/.test(document);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function ingressPaths(document) {
|
|
171
|
+
return [...document.matchAll(/path:\s*(\/[^\s]+)/g)].map((match) => match[1]);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function validatePublicIngressDocument(document, pathBase, blockers) {
|
|
175
|
+
const paths = ingressPaths(document);
|
|
176
|
+
const pathSet = new Set(paths);
|
|
177
|
+
if (/pathType:\s*Prefix\b/.test(document)) blockers.push(blocker('public ingress must not use Prefix path routing', `${pathBase}.pathType`));
|
|
178
|
+
if (/path:\s*\/(?:$|\s)/.test(document)) blockers.push(blocker('public ingress must not expose root path', `${pathBase}.paths`));
|
|
179
|
+
if (/path:\s*\/(?:relay|witness|pairing|gateway|policy|siem|admin|data)\b/i.test(document)) blockers.push(blocker('public ingress must not expose admin or data-plane paths', `${pathBase}.paths`));
|
|
180
|
+
if (paths.length !== 4 || pathSet.size !== 2 || !pathSet.has('/readyz') || !pathSet.has('/livez')) blockers.push(blocker('public ingress must expose only relay/gateway /readyz and /livez paths', `${pathBase}.paths`));
|
|
181
|
+
const exactPathMatches = [...document.matchAll(/path:\s*(\/[^\s]+)\s*\n\s*pathType:\s*Exact\b/g)].map((match) => match[1]);
|
|
182
|
+
if (exactPathMatches.length !== paths.length) blockers.push(blocker('public ingress health paths must all use Exact pathType', `${pathBase}.pathType`));
|
|
183
|
+
return paths;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function k8sDeploymentChecks(text, name, blockers) {
|
|
187
|
+
const block = deploymentBlock(text, name);
|
|
188
|
+
if (!block) {
|
|
189
|
+
blockers.push(blocker(`kubernetes missing ${name} deployment`, `kubernetes.deployments.${name}`));
|
|
190
|
+
return { name, ok: false };
|
|
191
|
+
}
|
|
192
|
+
requireInclude(block, 'replicas: 2', `kubernetes.deployments.${name}.replicas`, blockers, `${name} must request at least two replicas in the reference manifest`);
|
|
193
|
+
requireInclude(block, 'automountServiceAccountToken: false', `kubernetes.deployments.${name}.automountServiceAccountToken`, blockers, `${name} must disable service-account token automount`);
|
|
194
|
+
requireInclude(block, 'runAsNonRoot: true', `kubernetes.deployments.${name}.securityContext.runAsNonRoot`, blockers, `${name} must run as non-root`);
|
|
195
|
+
requireInclude(block, 'readOnlyRootFilesystem: true', `kubernetes.deployments.${name}.securityContext.readOnlyRootFilesystem`, blockers, `${name} must use a read-only root filesystem`);
|
|
196
|
+
requireInclude(block, 'drop: ["ALL"]', `kubernetes.deployments.${name}.securityContext.capabilities`, blockers, `${name} must drop all capabilities`);
|
|
197
|
+
requireInclude(block, 'readinessProbe:', `kubernetes.deployments.${name}.readinessProbe`, blockers, `${name} must define readiness probe`);
|
|
198
|
+
requireInclude(block, 'livenessProbe:', `kubernetes.deployments.${name}.livenessProbe`, blockers, `${name} must define liveness probe`);
|
|
199
|
+
requireInclude(block, 'optional: false', `kubernetes.deployments.${name}.secrets`, blockers, `${name} secret refs must be required`);
|
|
200
|
+
if (/image:\s*[^\n]*:(?:latest|dev|main)\b/.test(block)) blockers.push(blocker(`${name} image must not use mutable latest/dev/main tag`, `kubernetes.deployments.${name}.image`));
|
|
201
|
+
return { name, ok: true };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function validateKubernetesBackendManifestText(text, options = {}) {
|
|
205
|
+
const blockers = [];
|
|
206
|
+
assertNoSecretValues(text, 'kubernetes', blockers);
|
|
207
|
+
const ingressDocs = documentsOfKind(text, 'Ingress');
|
|
208
|
+
const publicIngress = ingressBlock(text, 'enigma-public');
|
|
209
|
+
const privateIngress = ingressBlock(text, 'enigma-admin');
|
|
210
|
+
if (!publicIngress) blockers.push(blocker('kubernetes missing enigma-public ingress', 'kubernetes.ingress.enigma-public'));
|
|
211
|
+
if (!privateIngress) blockers.push(blocker('kubernetes missing enigma-admin ingress', 'kubernetes.ingress.enigma-admin'));
|
|
212
|
+
|
|
213
|
+
const publicIngressDocs = ingressDocs.filter((document) => !isPrivateIngress(document));
|
|
214
|
+
let publicIngressPaths = [];
|
|
215
|
+
let publicPrefixPaths = 0;
|
|
216
|
+
let publicAdminOrDataPaths = 0;
|
|
217
|
+
for (const document of publicIngressDocs) {
|
|
218
|
+
const name = documentName(document);
|
|
219
|
+
const pathBase = `kubernetes.ingress.${name}`;
|
|
220
|
+
if (name !== 'enigma-public') blockers.push(blocker(`unexpected public ingress ${name}; public ingress must be enigma-public only`, pathBase));
|
|
221
|
+
const paths = validatePublicIngressDocument(document, pathBase, blockers);
|
|
222
|
+
if (name === 'enigma-public') publicIngressPaths = paths;
|
|
223
|
+
if (/pathType:\s*Prefix\b/.test(document)) publicPrefixPaths += 1;
|
|
224
|
+
if (/path:\s*\/(?:relay|witness|pairing|gateway|policy|siem|admin|data)\b/i.test(document)) publicAdminOrDataPaths += 1;
|
|
225
|
+
}
|
|
226
|
+
if (privateIngress && !isPrivateIngress(privateIngress)) blockers.push(blocker('private admin ingress must stay on private ingress class placeholder', 'kubernetes.ingress.enigma-admin.class'));
|
|
227
|
+
|
|
228
|
+
requireInclude(text, 'pod-security.kubernetes.io/enforce: restricted', 'kubernetes.namespace.pod-security', blockers, 'namespace must enforce restricted pod security');
|
|
229
|
+
requireInclude(text, 'kind: NetworkPolicy', 'kubernetes.networkPolicy', blockers, 'manifest must define NetworkPolicy');
|
|
230
|
+
requireInclude(text, 'name: enigma-default-deny', 'kubernetes.networkPolicy.defaultDeny', blockers, 'manifest must include default-deny NetworkPolicy');
|
|
231
|
+
requireInclude(text, 'name: enigma-allow-required-egress-placeholders', 'kubernetes.networkPolicy.egress', blockers, 'manifest must include explicit egress placeholder NetworkPolicy');
|
|
232
|
+
requireInclude(text, 'kind: CronJob', 'kubernetes.backup.cronJob', blockers, 'manifest must include backup CronJob placeholder');
|
|
233
|
+
requireInclude(text, 'echo backup target must be wired by operator; exit 1', 'kubernetes.backup.cronJob.failClosed', blockers, 'backup CronJob placeholder must fail closed until wired');
|
|
234
|
+
requireInclude(text, 'kind: PodDisruptionBudget', 'kubernetes.pdb', blockers, 'manifest must include PodDisruptionBudget');
|
|
235
|
+
const relay = k8sDeploymentChecks(text, 'enigma-relay', blockers);
|
|
236
|
+
const gateway = k8sDeploymentChecks(text, 'enigma-gateway', blockers);
|
|
237
|
+
const refs = requiredHostedEnvRefCoverage(text);
|
|
238
|
+
for (const name of refs.missing) blockers.push(blocker(`kubernetes manifest missing hosted readiness ref ${name}`, `kubernetes.config.${name}`));
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
path: options.path ?? null,
|
|
242
|
+
ok: blockers.length === 0,
|
|
243
|
+
blockers,
|
|
244
|
+
relay,
|
|
245
|
+
gateway,
|
|
246
|
+
public_readyz_paths: publicIngressPaths.filter((path) => path === '/readyz').length,
|
|
247
|
+
public_livez_paths: publicIngressPaths.filter((path) => path === '/livez').length,
|
|
248
|
+
public_prefix_paths: publicPrefixPaths,
|
|
249
|
+
public_admin_or_data_paths: publicAdminOrDataPaths,
|
|
250
|
+
private_admin_ingress_class: Boolean(privateIngress && isPrivateIngress(privateIngress)),
|
|
251
|
+
default_deny_network_policy: include(text, 'name: enigma-default-deny'),
|
|
252
|
+
backup_placeholder_fail_closed: include(text, 'echo backup target must be wired by operator; exit 1'),
|
|
253
|
+
hosted_readiness_ref_count: refs.present.length,
|
|
254
|
+
required_hosted_ref_count: REQUIRED_HOSTED_ENV_REFS.length,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export async function validateProductionManifestFiles(options = {}) {
|
|
259
|
+
const composePath = resolve(options.composePath ?? options.compose ?? 'deploy/docker-compose.production.example.yml');
|
|
260
|
+
const kubernetesPath = resolve(options.kubernetesPath ?? options.kubernetes ?? 'deploy/kubernetes/enigma-backend.example.yaml');
|
|
261
|
+
const [composeText, kubernetesText] = await Promise.all([
|
|
262
|
+
readFile(composePath, 'utf8'),
|
|
263
|
+
readFile(kubernetesPath, 'utf8'),
|
|
264
|
+
]);
|
|
265
|
+
const compose = validateProductionComposeManifestText(composeText, { path: publicManifestPath(composePath, 'compose') });
|
|
266
|
+
const kubernetes = validateKubernetesBackendManifestText(kubernetesText, { path: publicManifestPath(kubernetesPath, 'kubernetes') });
|
|
267
|
+
const blockers = [
|
|
268
|
+
...compose.blockers.map((item) => ({ ...item, source: 'compose' })),
|
|
269
|
+
...kubernetes.blockers.map((item) => ({ ...item, source: 'kubernetes' })),
|
|
270
|
+
];
|
|
271
|
+
return {
|
|
272
|
+
schema: PRODUCTION_MANIFEST_RESULT_SCHEMA,
|
|
273
|
+
generated_at: options.generated_at ?? options.generatedAt ?? new Date().toISOString(),
|
|
274
|
+
ok: blockers.length === 0,
|
|
275
|
+
status: blockers.length === 0 ? 'accepted' : 'blocked',
|
|
276
|
+
compose,
|
|
277
|
+
kubernetes,
|
|
278
|
+
blockers,
|
|
279
|
+
claim_boundary: [...CLAIM_BOUNDARY],
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function parseArgs(argv) {
|
|
284
|
+
const options = {};
|
|
285
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
286
|
+
const arg = argv[index];
|
|
287
|
+
if (arg === '--compose') options.composePath = argv[++index];
|
|
288
|
+
else if (arg === '--kubernetes') options.kubernetesPath = argv[++index];
|
|
289
|
+
else if (arg === '--out') options.out = argv[++index];
|
|
290
|
+
else if (arg === '--generated-at') options.generated_at = argv[++index];
|
|
291
|
+
else if (arg === '--help' || arg === '-h') options.help = true;
|
|
292
|
+
else throw new Error(`Unknown argument: ${arg}`);
|
|
293
|
+
}
|
|
294
|
+
return options;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function help() {
|
|
298
|
+
return `Usage: node scripts/validate-production-manifests.mjs [--compose <path>] [--kubernetes <path>] [--out <path>]\n\nValidates production-shaped backend deploy manifests without reading secrets or contacting providers.\nDefaults:\n --compose deploy/docker-compose.production.example.yml\n --kubernetes deploy/kubernetes/enigma-backend.example.yaml\n`;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function main() {
|
|
302
|
+
const options = parseArgs(process.argv.slice(2));
|
|
303
|
+
if (options.help) {
|
|
304
|
+
process.stdout.write(help());
|
|
305
|
+
return 0;
|
|
306
|
+
}
|
|
307
|
+
const result = await validateProductionManifestFiles(options);
|
|
308
|
+
const json = `${JSON.stringify(result, null, 2)}\n`;
|
|
309
|
+
if (options.out) {
|
|
310
|
+
const out = resolve(options.out);
|
|
311
|
+
await mkdir(dirname(out), { recursive: true });
|
|
312
|
+
await writeFile(out, json, 'utf8');
|
|
313
|
+
}
|
|
314
|
+
process.stdout.write(json);
|
|
315
|
+
return result.ok ? 0 : 1;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) {
|
|
319
|
+
main().then((code) => {
|
|
320
|
+
process.exitCode = code;
|
|
321
|
+
}).catch((error) => {
|
|
322
|
+
process.stderr.write(`${error.stack ?? error.message}\n`);
|
|
323
|
+
process.exitCode = 1;
|
|
324
|
+
});
|
|
325
|
+
}
|