166 lines
5.0 KiB
JavaScript
166 lines
5.0 KiB
JavaScript
|
|
#!/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();
|