#!/usr/bin/env node /** * patch-p1-3-fail-mode-lib.js * * P1.3 step1: 创建 hooks/lib/fail-mode.js * * 提供 failModeFromFlag(featureName) 决策 API。 * warn 模式(默认 7 天): hook 行为不变,但记录 evolution-log.jsonl violation * enforce 模式: hook 应主动调用 process.exit(1) 拒绝 * * 不直接修改 5 处 fail-open hook(受 tamper protection), * 仅提供基础设施 + warn 模式仅记录能力。enforce 由用户后续决策。 */ 'use strict'; const fs = require('fs'); const path = require('path'); const TARGET = path.join(__dirname, '..', '..', 'hooks', 'lib', 'fail-mode.js'); const SENTINEL = 'P1-FAIL-MODE-V1'; const CONTENT = `'use strict'; /** * fail-mode.js — fail-open/fail-closed 决策 API (${SENTINEL}) * * 红队识别风险: 关键 hook 普遍 fail-open(异常即放行)→ 攻击者构造慢路径或异常即逃逸守卫。 * * 设计: * - feature-flags.json 中读取 features['bookworm.security.failClosed'].mode * - mode='off' / 不存在: 完全无操作(保留原 fail-open 行为) * - mode='warn': 记录 evolution-log violation 但放行 * - mode='enforce': 调用方应据此 process.exit(1) * * Usage: * const { failModeDecide } = require('./lib/fail-mode.js'); * try { ... } catch (e) { * const action = failModeDecide('security-startup-guard', e); * if (action === 'reject') process.exit(1); * // else: 原 fail-open 路径 * } */ const fs = require('fs'); const path = require('path'); const ROOT = path.join(__dirname, '..', '..'); const FLAGS = path.join(ROOT, 'feature-flags.json'); const EVOLUTION_LOG = path.join(ROOT, 'evolution-log.jsonl'); const FLAG_FEATURE = 'bookworm.security.failClosed'; let _cachedFlags = null; let _cacheMtime = 0; function loadFlags() { try { const stat = fs.statSync(FLAGS); if (_cachedFlags && stat.mtimeMs === _cacheMtime) return _cachedFlags; const raw = JSON.parse(fs.readFileSync(FLAGS, 'utf8')); _cachedFlags = raw && raw.features ? raw.features : {}; _cacheMtime = stat.mtimeMs; return _cachedFlags; } catch (_) { return {}; } } /** * 决策 API * @param {string} hookName - 调用方标识,如 'security-startup-guard' * @param {Error|object} ctx - 异常或上下文 * @returns {'noop'|'warn'|'reject'} */ function failModeDecide(hookName, ctx) { const flags = loadFlags(); const cfg = flags[FLAG_FEATURE]; const mode = cfg && cfg.mode ? cfg.mode : 'off'; if (mode === 'off' || !cfg || cfg.enabled === false) return 'noop'; if (mode === 'warn') { // 仅记录,不拒绝 try { const entry = { ts: new Date().toISOString(), type: 'failmode.violation', hook: hookName, ctxMessage: ctx && ctx.message ? String(ctx.message).slice(0, 200) : null, mode: 'warn', }; fs.appendFileSync(EVOLUTION_LOG, JSON.stringify(entry) + '\\n'); } catch (_) { /* best effort */ } return 'warn'; } if (mode === 'enforce') { try { const entry = { ts: new Date().toISOString(), type: 'failmode.rejected', hook: hookName, ctxMessage: ctx && ctx.message ? String(ctx.message).slice(0, 200) : null, mode: 'enforce', }; fs.appendFileSync(EVOLUTION_LOG, JSON.stringify(entry) + '\\n'); } catch (_) {} return 'reject'; } return 'noop'; } /** * 当前 mode 查询 */ function getMode() { const flags = loadFlags(); const cfg = flags[FLAG_FEATURE]; if (!cfg || cfg.enabled === false) return 'off'; return cfg.mode || 'off'; } module.exports = { failModeDecide, getMode, __sentinel: '${SENTINEL}' }; `; function main() { const dir = path.dirname(TARGET); if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); if (fs.existsSync(TARGET)) { const cur = fs.readFileSync(TARGET, 'utf8'); if (cur.includes(SENTINEL)) { process.stdout.write('[SKIP] already deployed\n'); process.exit(0); } const ts = new Date().toISOString().replace(/[:.]/g, '-'); fs.copyFileSync(TARGET, TARGET + '.bak.' + ts); process.stdout.write('[BACKUP] ' + TARGET + '.bak.' + ts + '\n'); } const tmpPath = TARGET + '.tmp.' + process.pid; fs.writeFileSync(tmpPath, CONTENT); try { delete require.cache[require.resolve(tmpPath)]; const mod = require(tmpPath); // 自检 1: getMode 不抛异常(feature-flag 还没加,应返回 'off') const m = mod.getMode(); if (typeof m !== 'string') throw new Error('getMode return wrong type'); // 自检 2: failModeDecide 不抛异常 const action = mod.failModeDecide('self-test', new Error('test')); if (!['noop', 'warn', 'reject'].includes(action)) throw new Error('bad action: ' + action); fs.renameSync(tmpPath, TARGET); process.stdout.write('[OK] hooks/lib/fail-mode.js deployed\n'); process.stdout.write(' current mode: ' + m + ' (will activate after feature-flag registered)\n'); } catch (e) { fs.unlinkSync(tmpPath); process.stderr.write('[ERROR] self-test: ' + e.message + '\n'); process.exit(1); } } if (require.main === module) main();