112 lines
3.8 KiB
JavaScript
112 lines
3.8 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* F6 安全启动守卫 — 会话开始时自动检测并修复危险安全配置
|
|
*
|
|
* 检测项:
|
|
* - skipDangerousModePermissionPrompt === true → 自动修复为 false
|
|
*
|
|
* 触发: UserPromptSubmit hook (每次会话首次查询)
|
|
* 耗时: <5ms (仅读写一个 JSON 文件)
|
|
* 冷却: 使用 lockfile 防止同会话多次执行
|
|
*/
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const { logSecurityEvent } = require('./lib/security-log.js');
|
|
|
|
const ROOT = require('./lib/root.js');
|
|
const SETTINGS_FILE = path.join(ROOT, 'settings.json');
|
|
const LOCK_FILE = path.join(ROOT, 'debug', '.security-guard-lock');
|
|
|
|
/**
|
|
* 核心守卫逻辑 (不读 stdin, 不调 process.exit)
|
|
* 供 prompt-dispatcher 内联调用
|
|
* @returns {boolean} true=已执行守卫检查, false=冷却期跳过
|
|
*/
|
|
function runGuard() {
|
|
// 冷却: 同会话只执行一次 (lockfile 10分钟有效)
|
|
try {
|
|
if (fs.existsSync(LOCK_FILE)) {
|
|
const lockAge = Date.now() - fs.statSync(LOCK_FILE).mtimeMs;
|
|
if (lockAge < 10 * 60 * 1000) {
|
|
return false; // 冷却期,跳过
|
|
}
|
|
}
|
|
} catch {}
|
|
|
|
// 读取 settings.json
|
|
if (!fs.existsSync(SETTINGS_FILE)) {
|
|
return false;
|
|
}
|
|
|
|
// P3-3: 备份/恢复机制
|
|
const BACKUP_DIR = path.join(ROOT, 'backups');
|
|
const BACKUP_PREFIX = 'settings-guard-';
|
|
const MAX_BACKUPS = 5;
|
|
|
|
let settings;
|
|
try {
|
|
settings = JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf8'));
|
|
} catch (parseErr) {
|
|
// settings.json 损坏 — 尝试从备份恢复
|
|
logSecurityEvent("critical", "security-startup-guard", "F6-anti-writeback", "settings.json corrupt: " + (parseErr.message || "").slice(0, 80));
|
|
try {
|
|
if (!fs.existsSync(BACKUP_DIR)) return false;
|
|
var backups = fs.readdirSync(BACKUP_DIR)
|
|
.filter(function(f) { return f.startsWith(BACKUP_PREFIX) && f.endsWith('.json'); })
|
|
.sort().reverse();
|
|
if (backups.length === 0) return false;
|
|
var bkContent = fs.readFileSync(path.join(BACKUP_DIR, backups[0]), "utf8");
|
|
JSON.parse(bkContent); // validate
|
|
var tmpR = SETTINGS_FILE + '.tmp.' + process.pid;
|
|
fs.writeFileSync(tmpR, bkContent);
|
|
fs.renameSync(tmpR, SETTINGS_FILE);
|
|
logSecurityEvent("auto-restore", "security-startup-guard", "F6-anti-writeback", "settings.json restored from " + backups[0]);
|
|
settings = JSON.parse(bkContent);
|
|
} catch { return false; }
|
|
}
|
|
|
|
// P3-3: 创建备份 (每次守卫检查时)
|
|
try {
|
|
if (!fs.existsSync(BACKUP_DIR)) fs.mkdirSync(BACKUP_DIR, { recursive: true });
|
|
var bkName = BACKUP_PREFIX + new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19) + '.json';
|
|
fs.writeFileSync(path.join(BACKUP_DIR, bkName), JSON.stringify(settings, null, 2));
|
|
var allBk = fs.readdirSync(BACKUP_DIR)
|
|
.filter(function(f) { return f.startsWith(BACKUP_PREFIX); }).sort();
|
|
while (allBk.length > MAX_BACKUPS) {
|
|
try { fs.unlinkSync(path.join(BACKUP_DIR, allBk.shift())); } catch {}
|
|
}
|
|
} catch {}
|
|
|
|
// 检测: skipDangerousModePermissionPrompt === true → 自动修复为 false
|
|
// 用户确认: skipDangerousModePermissionPrompt=true 是宿主应用需要的,永久豁免
|
|
// 不再自动修复此配置项
|
|
|
|
// 写 lockfile
|
|
try {
|
|
const debugDir = path.join(ROOT, 'debug');
|
|
if (!fs.existsSync(debugDir)) fs.mkdirSync(debugDir, { recursive: true });
|
|
fs.writeFileSync(LOCK_FILE, new Date().toISOString());
|
|
} catch {}
|
|
|
|
return true;
|
|
}
|
|
|
|
function main() {
|
|
// 消费 stdin (hook 协议要求)
|
|
const readStdin = require('./lib/read-stdin.js');
|
|
readStdin({ maxSize: 512 * 1024 }).then(() => {
|
|
runGuard();
|
|
process.exit(0);
|
|
}).catch(() => process.exit(0)); // Fail-open: 守卫异常不阻断用户会话
|
|
}
|
|
|
|
// 模块导出 (供 dispatcher 调用)
|
|
if (typeof module !== 'undefined') {
|
|
module.exports = { main, runGuard };
|
|
}
|
|
|
|
if (require.main === module) {
|
|
main();
|
|
}
|