bookworm-smart-assistant/hooks/block-sensitive-reads.js

90 lines
3.7 KiB
JavaScript
Raw Permalink Normal View History

#!/usr/bin/env node
const fs = require("fs");
const path = require("path");
const readStdin = require('./lib/read-stdin.js');
const SUFFIX_RULES = [
{ suffix: ".credentials.json", reason: "Claude OAuth 凭证文件" },
{ suffix: ".env", reason: ".env 环境变量文件" },
{ suffix: ".pem", reason: "PEM 证书/密钥文件" },
{ suffix: ".key", reason: "私钥文件" },
{ suffix: ".p12", reason: "PKCS12 证书文件" },
{ suffix: ".pfx", reason: "PFX 证书文件" },
{ suffix: ".netrc", reason: ".netrc 网络凭证" },
{ suffix: ".git-credentials", reason: "Git 凭证" },
{ suffix: ".htpasswd", reason: "HTTP Auth 密码" },
{ suffix: ".npmrc", reason: "npm token" },
{ suffix: ".pypirc", reason: "PyPI token" },
];
const CONTAINS_RULES = [
{ pattern: "id_rsa", reason: "SSH RSA 私钥" },
{ pattern: "id_ed25519", reason: "SSH ED25519 私钥" },
{ pattern: "service_account", reason: "GCP 服务账号" },
{ pattern: "service-account", reason: "GCP 服务账号" },
{ pattern: "firebase-adminsdk", reason: "Firebase SDK" },
{ pattern: "firebase_adminsdk", reason: "Firebase SDK" },
];
function isEnvVariant(fp) {
var b = path.basename(fp).toLowerCase();
return b.startsWith(".env.") && b.length > 5;
}
function isSecretsFile(fp) {
var b = path.basename(fp).toLowerCase();
var X = [".json",".yaml",".yml",".toml",".xml"];
return b.startsWith("secret") && X.some(function(e){return b.endsWith(e);});
}
function isCredFile(fp) {
var b = path.basename(fp).toLowerCase();
var X = [".json",".yaml",".yml",".toml",".xml"];
return b.startsWith("credential") && X.some(function(e){return b.endsWith(e);});
}
function norm(fp) {
if (!fp) return "";
fp = fp.trim(); // P1-8: 剥离尾部空格 (Windows 路径绕过)
var resolved = path.resolve(fp);
// P1-5: 解析符号链接,防止 symlink 绕过
try { resolved = fs.realpathSync(resolved); } catch(e) {}
return resolved.split(path.sep).join("/").toLowerCase()
.replace(/\.+$/, '') // P1-8: 剥离尾部点 (Windows 会忽略)
.replace(/::?\$DATA$/i, ''); // P1-8: 剥离 NTFS ADS 后缀
}
function logEvt(decision,reason,detail) {
try {
let root=require('./lib/root.js'),dd=path.join(root,"debug");
if(!fs.existsSync(dd))fs.mkdirSync(dd,{recursive:true});
let lf=path.join(dd,"security-"+new Date().toISOString().slice(0,10)+".jsonl");
fs.appendFileSync(lf,JSON.stringify({ts:new Date().toISOString(),decision:decision,
hook:"block-sensitive-reads",reason:reason,detail:(detail||"").slice(0,200)
})+"\n");
}catch(e){}
}
function check(raw) {
let fp=norm(raw); if(!fp) return null;
for(let i=0;i<SUFFIX_RULES.length;i++){if(fp.endsWith(SUFFIX_RULES[i].suffix))return SUFFIX_RULES[i].reason;}
if(isEnvVariant(fp))return ".env.* 变体";
if(isSecretsFile(fp))return "密钥文件";
if(isCredFile(fp))return "凭证文件";
for(var j=0;j<CONTAINS_RULES.length;j++){if(fp.includes(CONTAINS_RULES[j].pattern))return CONTAINS_RULES[j].reason;}
return null;
}
function main() {
readStdin({ maxSize: 512 * 1024 }).then(inp => {
var ti=inp.tool_input||{};
var rp=ti.file_path||ti.filePath||ti.path||"";
var reason=check(rp);
if(reason){logEvt("deny",reason,rp);
process.stderr.write(JSON.stringify({hookSpecificOutput:{permissionDecision:"deny"},
systemMessage:"[安全防护] 阻止读取: "+rp+" | "+reason}));
process.exit(2);return;}
process.exit(0);
}).catch((e) => {
process.stderr.write(JSON.stringify({hookSpecificOutput:{permissionDecision:"ask"},
systemMessage:"[安全防护] 异常("+(e.message||"")+")"}));
process.exit(2);
});
}
if(typeof module!=="undefined"){module.exports={check:check};}
if(require.main===module){main();}