test(preprod): two-host routing simulation, drill suite, CI security scanning

- infra/scripts/simulate-two-host/: real two-Docker-network simulation
  (Gateway genuinely isolated from any Lab bridge network) proving the
  Lab Host Router fix - 13/13 real assertions (HTTP+WebSocket through the
  Router, Alice/Bob isolation, garbage/destroyed tokens, mTLS-only Router
  access).
- infra/scripts/drills/: Lab Host loss/rebuild (9/9), deployment rollback,
  CI-fails-closed (6 required failure modes), fake cohort dress rehearsal
  (14/14, one continuous narrative through the real app).
- infra/ci/gitea-workflows/build-and-test.yml: new security-scan job
  (npm audit, gitleaks, hadolint, shellcheck, trivy - all free/open-source),
  severity policy in docs/SECURITY_SCANNING.md (CRITICAL/HIGH blocks,
  MEDIUM/LOW reported only). Gates build-images alongside regression.

Found and fixed two real pre-existing bugs via the CI-fails-closed drill:
staging-smoke-test.sh and production-smoke-test.sh both had a classic
set -e gotcha (curl ... && echo "OK" as one statement - a failing command
that isn't last in an && chain doesn't trigger -e) that let an
unreachable/broken endpoint fall through silently instead of failing the
smoke test.

platform/test/e2e-phase3a.js updated for the new paginated
/api/admin/audit-events response shape (Phase 4.1 pagination work).
This commit is contained in:
2026-08-24 20:16:00 -05:00
parent f5110d3c1b
commit f3c356951c
15 changed files with 1067 additions and 9 deletions
+6 -2
View File
@@ -160,12 +160,16 @@ async function main() {
});
await test('audit log: security events were actually recorded', async () => {
// Phase 4.1 §16 — /api/admin/audit-events is now paginated (see platform/api/src/pagination.js);
// the events land on the most recent page, but this test has produced fewer than one
// page's worth by this point, so page 1's items are still every event so far.
const { body } = await owner.request('GET', '/api/admin/audit-events');
const actions = body.map((e) => e.action);
const items = body.items;
const actions = items.map((e) => e.action);
assert.ok(actions.includes('login'));
assert.ok(actions.includes('authz_denied'));
assert.ok(actions.includes('instructor_update'));
assert.ok(body.every((e) => !JSON.stringify(e.metadata || {}).match(/password/i)), 'audit metadata must never contain password material');
assert.ok(items.every((e) => !JSON.stringify(e.metadata || {}).match(/password/i)), 'audit metadata must never contain password material');
});
// ── Student session hardening ──────────────────────────────────────────────────────────