|
| 1 | +const assert = require('node:assert/strict'); |
| 2 | +const {readFileSync} = require('node:fs'); |
| 3 | +const {resolve} = require('node:path'); |
| 4 | + |
| 5 | +const root = resolve(__dirname, '..'); |
| 6 | +const nginx = readFileSync(resolve(root, 'nginx.conf'), 'utf8'); |
| 7 | +const dockerfile = readFileSync(resolve(root, 'deploy.Dockerfile'), 'utf8'); |
| 8 | +const developmentCompose = readFileSync(resolve(root, 'docker-compose.yml'), 'utf8'); |
| 9 | +const deploymentWorkflows = [ |
| 10 | + ['default', readFileSync(resolve(root, '.github/workflows/deployment.yml'), 'utf8')], |
| 11 | + [ |
| 12 | + 'institution', |
| 13 | + readFileSync(resolve(root, '.github/workflows/deployment-institution.yml'), 'utf8'), |
| 14 | + ], |
| 15 | +]; |
| 16 | +const applicationBootstrap = readFileSync(resolve(root, 'src/main.ts'), 'utf8'); |
| 17 | +const scormPlayerTemplate = readFileSync( |
| 18 | + resolve(root, 'src/app/common/scorm-player/scorm-player.component.html'), |
| 19 | + 'utf8', |
| 20 | +); |
| 21 | +const productionEnvironment = readFileSync( |
| 22 | + resolve(root, 'src/environments/environment.prod.ts'), |
| 23 | + 'utf8', |
| 24 | +); |
| 25 | + |
| 26 | +const controlFiles = [ |
| 27 | + '/index.html', |
| 28 | + '/ngsw.json', |
| 29 | + '/ngsw-worker.js', |
| 30 | + '/safety-worker.js', |
| 31 | + '/worker-basic.min.js', |
| 32 | + '/manifest.webmanifest', |
| 33 | +]; |
| 34 | +const noStorePolicy = 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0'; |
| 35 | + |
| 36 | +const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); |
| 37 | + |
| 38 | +for (const path of controlFiles) { |
| 39 | + const escapedPath = escapeRegExp(path); |
| 40 | + |
| 41 | + assert.match( |
| 42 | + nginx, |
| 43 | + new RegExp(`^\\s*${escapedPath}\\s+"${noStorePolicy}";`, 'm'), |
| 44 | + `${path} must map to the no-store cache policy`, |
| 45 | + ); |
| 46 | + assert.match( |
| 47 | + nginx, |
| 48 | + new RegExp(`location = ${escapedPath} \\{\\s*try_files \\$uri =404;\\s*\\}`, 'm'), |
| 49 | + `${path} must use an exact location and return 404 when absent`, |
| 50 | + ); |
| 51 | +} |
| 52 | + |
| 53 | +assert.match( |
| 54 | + nginx, |
| 55 | + /add_header Cache-Control \$ontrack_control_file_cache_control always;/, |
| 56 | + 'the mapped cache policy must be added to responses', |
| 57 | +); |
| 58 | +assert.match( |
| 59 | + nginx, |
| 60 | + /try_files \$uri \$uri\/ \$uri\/index\.html \/index\.html;/, |
| 61 | + 'SPA routes must continue to fall back to /index.html', |
| 62 | +); |
| 63 | +for (const [name, workflow] of deploymentWorkflows) { |
| 64 | + assert.doesNotMatch( |
| 65 | + workflow, |
| 66 | + /steps\.meta\.outputs\.labels/, |
| 67 | + `${name} deployment metadata labels must reference the declared docker_meta step`, |
| 68 | + ); |
| 69 | + assert.match(workflow, /steps\.docker_meta\.outputs\.labels/); |
| 70 | + |
| 71 | + for (const line of workflow.split('\n')) { |
| 72 | + if (/^\s*uses:/.test(line)) { |
| 73 | + assert.match( |
| 74 | + line, |
| 75 | + /@[0-9a-f]{40}(?:\s+#.*)?$/, |
| 76 | + `${name} production image workflow actions must be pinned to immutable commits`, |
| 77 | + ); |
| 78 | + } |
| 79 | + } |
| 80 | + |
| 81 | + assert.match(workflow, /^\s*sbom:\s*true$/m); |
| 82 | + assert.match(workflow, /^\s*provenance:\s*mode=max$/m); |
| 83 | +} |
| 84 | +assert.match( |
| 85 | + dockerfile, |
| 86 | + /^FROM node:22\.23\.2-bookworm-slim@sha256:[0-9a-f]{64} AS build$/m, |
| 87 | + 'the release build must pin the supported Node image digest', |
| 88 | +); |
| 89 | +assert.match( |
| 90 | + dockerfile, |
| 91 | + /npm install --global npm@11\.19\.1/, |
| 92 | + 'the release build must install the reviewed npm security update', |
| 93 | +); |
| 94 | +assert.match( |
| 95 | + dockerfile, |
| 96 | + /^FROM nginx:1\.30\.4-alpine@sha256:[0-9a-f]{64}$/m, |
| 97 | + 'the release runtime must pin the Nginx image digest', |
| 98 | +); |
| 99 | +assert.match( |
| 100 | + dockerfile, |
| 101 | + /^RUN apk upgrade --no-cache libcrypto3 libssl3$/m, |
| 102 | + 'the release runtime must install the reviewed Alpine OpenSSL security update', |
| 103 | +); |
| 104 | +assert.doesNotMatch( |
| 105 | + dockerfile, |
| 106 | + /npm ci[^\n]*--force/, |
| 107 | + 'the release build must not force npm resolution', |
| 108 | +); |
| 109 | +assert.doesNotMatch( |
| 110 | + dockerfile, |
| 111 | + /chmod\s+777/, |
| 112 | + 'the release build must not create world-writable source', |
| 113 | +); |
| 114 | +assert.match( |
| 115 | + developmentCompose, |
| 116 | + /DF_SECRET_KEY_AAF:\s*\$\{DF_SECRET_KEY_AAF:-\}/, |
| 117 | + 'optional development AAF credentials must come from the ignored environment', |
| 118 | +); |
| 119 | +assert.doesNotMatch( |
| 120 | + developmentCompose, |
| 121 | + /https?:\/\/[^\s$]*(?:aaf\.edu\.au|deakin\.edu\.au)/i, |
| 122 | + 'institution-specific AAF endpoints must not be committed in development Compose', |
| 123 | +); |
| 124 | +assert.doesNotMatch( |
| 125 | + applicationBootstrap, |
| 126 | + /(?:browserTracingIntegration|replayIntegration|tracesSampleRate|replaysSessionSampleRate|replaysOnErrorSampleRate)/, |
| 127 | + 'tracing and replay must stay disabled while SCORM uses credential-bearing paths', |
| 128 | +); |
| 129 | +assert.match( |
| 130 | + applicationBootstrap, |
| 131 | + /enableLogs:\s*false/, |
| 132 | + 'Sentry Logs must stay disabled for the errors-only telemetry policy', |
| 133 | +); |
| 134 | +assert.match( |
| 135 | + scormPlayerTemplate, |
| 136 | + /<iframe[^>]*data-sentry-block[^>]*referrerpolicy="no-referrer"/, |
| 137 | + 'the SCORM iframe must suppress referrers and remain blocked from any future replay integration', |
| 138 | +); |
| 139 | +assert.match( |
| 140 | + productionEnvironment, |
| 141 | + /enableDemoTools:\s*false/, |
| 142 | + 'production must keep demo tools disabled', |
| 143 | +); |
| 144 | + |
| 145 | +console.log('Deployment configuration checks passed.'); |
0 commit comments