90 lines
3.7 KiB
JavaScript
90 lines
3.7 KiB
JavaScript
#!/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();}
|