bookworm-smart-assistant/scripts/patches/patch-pipeline-hooks-fix-v1.js
Bookworm Admin b7a8e29d21 release: v6.7.0 - OTA E2E test release
- VERSION file as authoritative version source
- export.mjs reads VERSION with package.json fallback
- bw-ota.ps1 DryRun mode for safe testing
- auto-setup.ps1 bumped to v3.2.0 (Phase 8 OTA)
2026-04-27 17:59:44 +08:00

152 lines
5.8 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
/**
* patch-pipeline-hooks-fix-v1.js · Phase α 冲刺 3 修复 · 2026-04-25
*
* 修 3 个 E2E 发现的 bug:
*
* Bug 1 [HIGH 安全]: staging-validator entropy 阈值 4.5 对 hex 字符串无效
* log2(16)=4.0,阈值 4.5 永远挂不到 hex API key
* 修法: 阈值 4.5 → 3.5 (hex 会命中); 但 alpha+num+symbol 仍需更高区分度
* 额外对 hex32+/base64 64+ 特定形态加专门 regex
*
* Bug 2 [LOW 元数据]: post-edit-snapshot skip-oversize 事件缺 sessionId
* 修法: appendManifest 时附 sessionId
*
* Bug 3 [MED 安全覆盖]: credential-patterns 仅 6 条命令行规则,不够 staging 扫描用
* 修法: 加 4 条常见生产凭证格式 (sk_live_/sk_test_/ghs_/hex32+)
*
* 幂等: 检测 sentinel 跳过; 原子 tmp+rename; .bak 保留
*/
'use strict';
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const ROOT = path.resolve(__dirname, '..', '..');
const SNAPSHOT = path.join(ROOT, 'hooks', 'post-edit-snapshot.js');
const VALIDATOR = path.join(ROOT, 'hooks', 'staging-validator.js');
const CRED_PATTERNS = path.join(ROOT, 'hooks', 'rules', 'credential-patterns.json');
const SENTINEL = '/* fix-v1-applied */';
function backup(p) {
const bak = p + '.bak.fix-v1.' + new Date().toISOString().replace(/[:.]/g, '-');
fs.copyFileSync(p, bak);
return path.basename(bak);
}
function atomicWrite(p, content) {
const tmp = p + '.tmp.' + process.pid;
fs.writeFileSync(tmp, content);
fs.renameSync(tmp, p);
}
// ========= Bug 1 & 3: staging-validator + credential-patterns =========
function fixValidator() {
const src = fs.readFileSync(VALIDATOR, 'utf8');
if (src.includes(SENTINEL)) {
console.log('[fix-v1] validator 已打过补丁, 跳过');
return { changed: false };
}
const newSrc = src
.replace(
/shannonEntropy\(t\) > 4\.5/,
'shannonEntropy(t) > 3.5 ' + SENTINEL
)
.replace(
// 额外加一个硬 regex 覆盖 hex32+
/const highEntropy = \(text\.match\(\/\\b\[A-Za-z0-9_\\-\]\{32,\}\\b\/g\) \|\| \[\]\)/,
'const hexHits = (text.match(/\\b[a-f0-9]{32,}\\b/gi) || []).slice(0, 2);\n' +
' if (hexHits.length > 0) hits.push(\'hex32+-suspicious\');\n' +
' const highEntropy = (text.match(/\\b[A-Za-z0-9_\\-]{32,}\\b/g) || [])'
);
if (newSrc === src) {
console.log('[fix-v1] validator 未找到预期锚点, 跳过 (可能已是不同版本)');
return { changed: false };
}
const bak = backup(VALIDATOR);
atomicWrite(VALIDATOR, newSrc);
console.log('[fix-v1] validator entropy 4.5→3.5 + hex32+ regex 已加 (bak:', bak + ')');
return { changed: true };
}
function fixSnapshot() {
const src = fs.readFileSync(SNAPSHOT, 'utf8');
if (src.includes('skip-oversize') && src.includes("event: 'skip-oversize'") && src.includes('sessionId,\n size: stat.size')) {
// already has sessionId nearby
}
// 检查是否已修
if (src.includes("event: 'skip-oversize', sessionId")) {
console.log('[fix-v1] snapshot 已打过补丁, 跳过');
return { changed: false };
}
// 在 skip-oversize 处插入 sessionId
const oldPattern = /event: 'skip-oversize', originalPath: filePath, size: stat\.size, cap: MAX_FILE_BYTES/;
const newStr = "event: 'skip-oversize', sessionId: getSessionId(input), originalPath: filePath, size: stat.size, cap: MAX_FILE_BYTES";
if (!oldPattern.test(src)) {
console.log('[fix-v1] snapshot 未找到预期锚点, 跳过');
return { changed: false };
}
const newSrc = src.replace(oldPattern, newStr);
const bak = backup(SNAPSHOT);
atomicWrite(SNAPSHOT, newSrc);
console.log('[fix-v1] snapshot skip-oversize 已加 sessionId (bak:', bak + ')');
return { changed: true };
}
function fixCredPatterns() {
const raw = fs.readFileSync(CRED_PATTERNS, 'utf8');
const json = JSON.parse(raw);
const existing = new Set(json.patterns.map(p => p.regex));
const newRules = [
{ regex: 'sk_live_[A-Za-z0-9]{24,}', flags: '', reason: 'Stripe Live Secret Key (文件内容)' },
{ regex: 'sk_test_[A-Za-z0-9]{24,}', flags: '', reason: 'Stripe Test Secret Key (文件内容)' },
{ regex: 'ghp_[A-Za-z0-9]{36,}', flags: '', reason: 'GitHub Personal Access Token (新版)' },
{ regex: 'xox[baprs]-[A-Za-z0-9-]{10,}', flags: '', reason: 'Slack Token' },
];
const toAdd = newRules.filter(r => !existing.has(r.regex));
if (toAdd.length === 0) {
console.log('[fix-v1] credential-patterns 已含新规则, 跳过');
return { changed: false };
}
const bak = backup(CRED_PATTERNS);
json._version = 'v3.9-staging-ext';
json.patterns.push(...toAdd);
atomicWrite(CRED_PATTERNS, JSON.stringify(json, null, 2) + '\n');
console.log('[fix-v1] credential-patterns 追加', toAdd.length, '条 (bak:', bak + ')');
toAdd.forEach(r => console.log(' +', r.reason));
return { changed: true };
}
function main() {
const r1 = fixValidator();
const r2 = fixSnapshot();
const r3 = fixCredPatterns();
// 语法校验
let syntaxOk = true;
for (const f of [SNAPSHOT, VALIDATOR]) {
try { execSync('node --check "' + f + '"', { stdio: 'pipe' }); }
catch (e) {
syntaxOk = false;
console.error('[SYNTAX FAIL]', path.basename(f), String(e.stderr || e.message).split('\n')[0]);
}
}
// 刷新 rules-compiled
if (r3.changed) {
try {
execSync('node "' + path.join(__dirname, '..', 'compile-rules.js') + '"', { stdio: 'pipe' });
console.log('[fix-v1] rules-compiled.json 已刷新');
} catch (e) { console.error('[fix-v1] compile-rules 失败:', e.message); }
}
console.log('\n[fix-v1] done. validator=' + (r1.changed ? '✓' : '-') +
', snapshot=' + (r2.changed ? '✓' : '-') +
', cred-patterns=' + (r3.changed ? '✓' : '-'));
process.exit(syntaxOk ? 0 : 1);
}
main();