All checks were successful
Quality gates / quality (pull_request) Successful in 2m9s
Strict vertical RED-GREEN TDD across eight review blockers: - drill baseline: requires every required boundary check explicitly passing plus zero alerts of any severity before anything is announced or flipped; unknown, degraded, malformed, missing-boundary, and backlog-warning baselines exit 2 with switch count zero, and stdout stays empty whenever no drill actually ran - manual fallback: deterministic truthful states — app-down, local-journal, none-required, and new app-state-unknown (availability null) so unknown telemetry can never render as proven availability or 'no degradation'; runbook gains a state table with exact wording - exact schema types: schemaVersion must be JSON number 1; counters and latency must be finite bounded integers (0..1e6, 0..3600000); generatedAtUtc must be canonical real UTC surviving round-trip; releaseTag/commit keep exact safe types; coercions reject - control-safety: controlSafe strips LF/CR too, so hostile paths can no longer forge terminal lines (regression test ships a path embedding LF + forged PAGE text); read errors use fixed wording instead of echoing attacker-derived message bytes - malformed checks reject the whole evidence object instead of silently disappearing beside healthy checks; forbidden top-level keys now fail closed rather than being ignored - status/failureClass consistency: pass/unknown checks carrying any failureClass reject the file, so all-pass evidence can never page worker.outage (fail/degraded may stay classless, preserving the closed vocabulary and nullable schema) - sanitization idempotence: omitted latency stays omitted on a second pass; explicit null latency rejects since the schema forbids it - removed the always-false queue-depth tautology in the shared fixture and made the runbook contract test verify each rule's real numeric threshold against DEFAULT_ALERT_RULES Preserved: closed failure-class vocabulary, queue depth 9/10/11 edges, raw loopback-origin validation, app-down fallback, missing-boundary rejection, transition semantics, and every independent drill case.
266 lines
12 KiB
JavaScript
266 lines
12 KiB
JavaScript
#!/usr/bin/env node
|
|
// Local operator surface for release observability (issue #41).
|
|
// Consumes only sanitized evidence files or a loopback drill fixture.
|
|
// Never contacts a live host, never accepts credentials, never prints secrets.
|
|
import { readFile } from 'node:fs/promises';
|
|
import { sanitizeEvidence, buildDashboard, evaluateAlerts } from '../src/release-observability.js';
|
|
|
|
function argValue(args, flag) {
|
|
const index = args.indexOf(flag);
|
|
if (index === -1) return null;
|
|
return args[index + 1] || null;
|
|
}
|
|
|
|
// Terminal safety: strip C0/C1 control characters — newline and carriage
|
|
// return included — from any dynamically produced text before it reaches
|
|
// stdout/stderr. Hostile file paths or fixture payloads can therefore never
|
|
// forge additional terminal lines (e.g. a fake "PAGE worker.outage"): the
|
|
// only line boundaries in CLI output are the ones this program authors.
|
|
function controlSafe(text) {
|
|
return String(text)
|
|
.replace(/[\u0000-\u001F\u007F-\u009F]/g, '');
|
|
}
|
|
|
|
async function loadEvidence(source) {
|
|
if (source.drillOrigin) {
|
|
const response = await fetch(`${source.drillOrigin.replace(/\/$/, '')}/api/drill/checks`, { signal: AbortSignal.timeout(5_000) });
|
|
if (!response.ok) throw new Error(`drill fixture returned ${response.status}`);
|
|
const body = await response.json();
|
|
return {
|
|
schemaVersion: 1,
|
|
releaseTag: controlSafe('daily-2026-08-22.1'),
|
|
commit: 'ca31e6d38bec649407f63880504554c59f2878ae',
|
|
generatedAtUtc: new Date().toISOString().replace(/\.\d{3}Z$/, 'Z'),
|
|
checks: Array.isArray(body.checks) ? body.checks : [],
|
|
};
|
|
}
|
|
const raw = JSON.parse(await readFile(source.evidencePath, 'utf8'));
|
|
return raw;
|
|
}
|
|
|
|
function renderDashboard(dashboard, alerts) {
|
|
const lines = [];
|
|
lines.push('RELEASE OBSERVABILITY DASHBOARD');
|
|
lines.push(`release ${dashboard.identity.releaseTag} · commit ${dashboard.identity.commit.slice(0, 12)} · evidence ${dashboard.identity.generatedAtUtc}`);
|
|
lines.push('');
|
|
for (const boundary of ['app', 'api', 'queue', 'model']) {
|
|
const stats = dashboard.boundaries[boundary];
|
|
const latency = stats.latencyMs == null ? '' : ` · max latency ${stats.latencyMs}ms`;
|
|
const counterBits = Object.keys(stats.counters).sort()
|
|
.map(key => `${key}=${stats.counters[key]}`)
|
|
.join(' ');
|
|
const counterText = counterBits ? ` · ${counterBits}` : '';
|
|
lines.push(`${boundary.padEnd(6)}${stats.pass}/${stats.checks} pass · fail ${stats.fail} · degraded ${stats.degraded}${latency}${counterText}`);
|
|
}
|
|
lines.push('');
|
|
const failureEntries = Object.entries(dashboard.failureClasses);
|
|
if (failureEntries.length === 0) {
|
|
lines.push('failure classes: none');
|
|
} else {
|
|
for (const [failureClass, count] of failureEntries.sort((a, b) => b[1] - a[1])) {
|
|
lines.push(`failure class ${failureClass}: ${count}`);
|
|
}
|
|
}
|
|
lines.push('');
|
|
lines.push(`alerts (${alerts.length})`);
|
|
for (const alert of alerts) {
|
|
lines.push(`[${alert.severity.toUpperCase()}] ${alert.id} — owner ${alert.owner}, observed ${alert.count} >= threshold ${alert.threshold}`);
|
|
lines.push(` ${alert.message}`);
|
|
lines.push(` runbook: ${alert.runbook}`);
|
|
}
|
|
if (alerts.length === 0) lines.push('no alert conditions met');
|
|
return lines.join('\n');
|
|
}
|
|
|
|
async function main() {
|
|
const args = process.argv.slice(2);
|
|
|
|
if (args.includes('--self-check')) {
|
|
console.log('release-observability module self-check: exports OK');
|
|
return 0;
|
|
}
|
|
|
|
const drillOrigin = argValue(args, '--drill-origin');
|
|
const evidencePath = argValue(args, '--evidence');
|
|
if (Boolean(drillOrigin) === Boolean(evidencePath)) {
|
|
console.error('usage: node scripts/release_dashboard.mjs (--evidence <sanitized-evidence.json> | --drill-origin <http://127.0.0.1:port>)');
|
|
return 2;
|
|
}
|
|
|
|
// Gate every network path before any fetch can happen: a drill may only
|
|
// ever address a validated bare loopback origin.
|
|
let validatedOrigin = null;
|
|
if (drillOrigin) {
|
|
try {
|
|
validatedOrigin = validateLoopbackOrigin(drillOrigin);
|
|
} catch (error) {
|
|
console.error(controlSafe(error.message));
|
|
return 2;
|
|
}
|
|
}
|
|
|
|
let rawEvidence;
|
|
try {
|
|
rawEvidence = await loadEvidence({ drillOrigin: validatedOrigin, evidencePath });
|
|
} catch {
|
|
// Fixed wording on purpose: interpolating filesystem error messages would
|
|
// echo attacker-controlled path bytes (even sanitized) into the terminal.
|
|
console.error('release_dashboard: cannot read sanitized evidence (unreadable file or invalid JSON). No contents are echoed.');
|
|
return 2;
|
|
}
|
|
|
|
// Drill mode owns its whole flow: the baseline gate must speak with one
|
|
// voice ("not a genuine drill baseline") whether the fixture served
|
|
// unsanitizable evidence or merely unhealthy checks, and nothing may be
|
|
// rendered or flipped until the baseline proves genuinely healthy.
|
|
if (drillOrigin) return runDrill({ drillOrigin: validatedOrigin });
|
|
|
|
const validated = sanitizeEvidence(rawEvidence);
|
|
if (!validated.ok) {
|
|
console.error('release_dashboard: evidence failed sanitization (schema mismatch, forbidden fields, or unbounded values). Nothing was rendered.');
|
|
return 2;
|
|
}
|
|
|
|
const dashboard = buildDashboard(validated.evidence);
|
|
const alerts = evaluateAlerts(dashboard);
|
|
console.log(renderDashboard(dashboard, alerts));
|
|
console.log('');
|
|
console.log(manualFallbackLine(dashboard.manualFallback));
|
|
|
|
return alerts.some(alert => alert.severity === 'page') ? 1 : 0;
|
|
}
|
|
|
|
// Truthful operator wording: the manual-fallback line is a safety statement,
|
|
// so each deterministic state says exactly what is known. Availability is
|
|
// only ever claimed from explicit passes; an unknown app boundary reads as
|
|
// unknown and demands hand verification — never as quiet health.
|
|
const FALLBACK_PHRASES = {
|
|
'none-required': 'all checks explicitly passing; no degradation observed; fallback not required',
|
|
'local-journal': 'degraded elsewhere; local journal remains usable',
|
|
'app-down': 'app boundary is down; local journal cannot be served',
|
|
'app-state-unknown': 'app state unknown; verify the local journal by hand before relying on it',
|
|
};
|
|
|
|
function manualFallbackLine(fallback, { shout = false } = {}) {
|
|
const label = fallback.available === null
|
|
? (shout ? 'UNKNOWN' : 'unknown')
|
|
: fallback.available
|
|
? (shout ? 'AVAILABLE' : 'available')
|
|
: (shout ? 'UNAVAILABLE' : 'unavailable');
|
|
return `manual fallback: ${label} — ${FALLBACK_PHRASES[fallback.reason] ?? fallback.reason}`;
|
|
}
|
|
|
|
// A drill may only ever talk to a throwaway fixture on 127.0.0.1. The origin
|
|
// must be a bare http URL whose host is exactly the loopback address: no
|
|
// credentials, no DNS names, no alternative IP encodings, no IPv6, no path,
|
|
// query, or fragment, and no scheme other than http.
|
|
const LOOPBACK_ORIGIN_PATTERN = /^http:\/\/127\.0\.0\.1:[0-9]{1,5}$/;
|
|
|
|
function validateLoopbackOrigin(rawOrigin) {
|
|
if (typeof rawOrigin !== 'string') {
|
|
throw new Error('drill origin rejected: expected an http://127.0.0.1:<port> URL');
|
|
}
|
|
// Grammar gate runs on the RAW string first: the WHATWG URL parser
|
|
// canonicalizes hostile host encodings (hex octets, decimal IP integers,
|
|
// percent-encoded dots) into a clean-looking 127.0.0.1, so a normalized
|
|
// string can never be trusted on its own.
|
|
if (!LOOPBACK_ORIGIN_PATTERN.test(rawOrigin)) {
|
|
throw new Error('drill origin rejected: only a bare http://127.0.0.1:<port> loopback origin is permitted');
|
|
}
|
|
let parsed;
|
|
try {
|
|
parsed = new URL(rawOrigin);
|
|
} catch {
|
|
throw new Error('drill origin rejected: not a valid URL');
|
|
}
|
|
// Round-trip defense in depth: the parsed form must be byte-identical to
|
|
// what was supplied, proving no hidden credentials, path, query, fragment,
|
|
// or alternative host encoding was smuggled past the grammar.
|
|
if (parsed.toString().replace(/\/$/, '') !== rawOrigin) {
|
|
throw new Error('drill origin rejected: URL did not round-trip as a bare loopback origin');
|
|
}
|
|
if (parsed.protocol !== 'http:' || parsed.hostname !== '127.0.0.1') {
|
|
throw new Error('drill origin rejected: host must be exactly http://127.0.0.1');
|
|
}
|
|
const port = Number(parsed.port);
|
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
throw new Error('drill origin rejected: port must be between 1 and 65535');
|
|
}
|
|
return rawOrigin;
|
|
}
|
|
|
|
async function runDrill({ drillOrigin }) {
|
|
let origin;
|
|
try {
|
|
origin = validateLoopbackOrigin(drillOrigin);
|
|
} catch (error) {
|
|
console.error(controlSafe(error.message));
|
|
return 2;
|
|
}
|
|
|
|
// Genuine baseline: read the fixture's own current state BEFORE anything is
|
|
// announced or flipped. A drill may only start from proven health — every
|
|
// required boundary check must explicitly pass and zero alerts may fire.
|
|
// Unknown, degraded, malformed, missing-boundary, or warning baselines are
|
|
// refused here, so stdout stays empty whenever no drill actually ran.
|
|
const preRaw = await loadEvidence({ drillOrigin });
|
|
const preValidated = sanitizeEvidence(preRaw);
|
|
if (!preValidated.ok) {
|
|
console.error('release_dashboard: fixture evidence is unsanitizable: not a genuine drill baseline (schema mismatch, forbidden fields, or unbounded values). Nothing will be rendered or flipped.');
|
|
return 2;
|
|
}
|
|
const preDashboard = buildDashboard(preValidated.evidence);
|
|
const preAlerts = evaluateAlerts(preDashboard);
|
|
const allExplicitlyPassing = Object.values(preDashboard.boundaries).every(
|
|
boundary => boundary.checks > 0 && boundary.pass === boundary.checks,
|
|
);
|
|
if (!allExplicitlyPassing || preAlerts.length > 0) {
|
|
console.error('release_dashboard: fixture is not healthy before the drill: not a genuine drill baseline (every required boundary check must explicitly pass with zero alerts). Refusing to flip an already-down or uncertain worker.');
|
|
return 2;
|
|
}
|
|
|
|
console.log('');
|
|
console.log('WORKER OUTAGE DRILL (simulated fixture on loopback; no live host is contacted)');
|
|
console.log('pre-drill baseline: every required boundary check explicitly passing; alerts: none');
|
|
console.log('pre-drill: healthy (0 page alerts)');
|
|
|
|
try {
|
|
const response = await fetch(`${origin}/drill/outage`, { method: 'POST', signal: AbortSignal.timeout(5_000) });
|
|
if (!response.ok) throw new Error(`fixture returned ${response.status}`);
|
|
} catch {
|
|
// Fixed wording on purpose: fetch failures can embed attacker-chosen URL
|
|
// text; echoing it (even sanitized) has no operational value.
|
|
console.error('release_dashboard: drill switch failed. The fixture switch was not verifiably flipped.');
|
|
return 2;
|
|
}
|
|
|
|
const postRaw = await loadEvidence({ drillOrigin });
|
|
const postValidated = sanitizeEvidence(postRaw);
|
|
if (!postValidated.ok) {
|
|
console.error('release_dashboard: post-outage evidence failed sanitization.');
|
|
return 2;
|
|
}
|
|
const postDashboard = buildDashboard(postValidated.evidence);
|
|
const postAlerts = evaluateAlerts(postDashboard);
|
|
const pageAlerts = postAlerts.filter(alert => alert.severity === 'page');
|
|
|
|
if (pageAlerts.length === 0) {
|
|
console.log('no outage observed after flip: the fixture never transitioned from healthy to outage.');
|
|
console.log('DRILL FAIL');
|
|
return 1;
|
|
}
|
|
console.log(`post-outage: exactly 1 alert expected, found ${pageAlerts.length}`);
|
|
for (const alert of pageAlerts) {
|
|
console.log(` ${alert.severity.toUpperCase()} ${alert.message.replace(/^PAGE /, '')}`);
|
|
console.log(` owner ${alert.owner} · threshold ${alert.threshold} · runbook ${alert.runbook}`);
|
|
}
|
|
console.log(manualFallbackLine(postDashboard.manualFallback, { shout: true }));
|
|
const pass = pageAlerts.length === 1
|
|
&& pageAlerts[0].id === 'worker.outage'
|
|
&& postDashboard.manualFallback.available === true;
|
|
console.log(pass ? 'DRILL PASS' : 'DRILL FAIL');
|
|
return pass ? 0 : 1;
|
|
}
|
|
|
|
process.exitCode = await main();
|