backup materials and knowledge-base docs
This commit is contained in:
169
文档润色流和知识库构建流/claude-scholar/hooks/security-guard.js
Normal file
169
文档润色流和知识库构建流/claude-scholar/hooks/security-guard.js
Normal file
@@ -0,0 +1,169 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* PreToolUse Hook: Security guard layer (cross-platform version)
|
||||
*
|
||||
* Event: PreToolUse
|
||||
* Two-tier security: Block (exit 2) | Confirm (exit 0 + ask user)
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const common = require('./hook-common');
|
||||
|
||||
// Read stdin input
|
||||
let input = {};
|
||||
try {
|
||||
const stdinData = require('fs').readFileSync(0, 'utf8');
|
||||
if (stdinData.trim()) {
|
||||
input = JSON.parse(stdinData);
|
||||
}
|
||||
} catch {
|
||||
// Use default empty object
|
||||
}
|
||||
|
||||
const toolName = input.tool_name || '';
|
||||
const cwd = input.cwd || process.cwd();
|
||||
|
||||
let decision = 'allow';
|
||||
let reason = '';
|
||||
let confirmLabel = '';
|
||||
|
||||
function isPathInside(basePath, targetPath) {
|
||||
const relative = path.relative(basePath, targetPath);
|
||||
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
||||
}
|
||||
|
||||
// === Bash command security check ===
|
||||
if (toolName === 'Bash') {
|
||||
const command = input.tool_input?.command || '';
|
||||
|
||||
// Tier 1: Block — catastrophic, no recovery
|
||||
const blockPatterns = [
|
||||
/rm\s+-rf\s+\/(\s|$)/, // rm -rf /
|
||||
/rm\s+--no-preserve-root/, // rm --no-preserve-root
|
||||
/dd\s+if=\/dev\/(zero|random)/, // dd from /dev/zero or /dev/random
|
||||
/>\s*\/dev\/(sd|nvme|vda)/, // Write to block devices
|
||||
/mkfs\./, // Format filesystem
|
||||
/rm\s+-rf?\s+\/(etc|usr|bin|sbin)(\/|\s|$)/, // Remove system dirs
|
||||
/rm\s+-rf\s+\/home\/[^\/\s]*\/?(\s|$)/, // Remove user home dir
|
||||
/rm\s+-rf\s+\/Users\/[^\/\s]*\/?(\s|$)/, // Remove macOS user dir
|
||||
];
|
||||
|
||||
for (const pattern of blockPatterns) {
|
||||
if (pattern.test(command)) {
|
||||
decision = 'deny';
|
||||
reason = 'Catastrophic command detected';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Tier 2: Confirm — dangerous but sometimes legitimate
|
||||
if (decision === 'allow') {
|
||||
const confirmPatterns = [
|
||||
{ pattern: /git\s+push\s+.*(-f|--force)/, label: 'git push --force (overwrites remote history)' },
|
||||
{ pattern: /git\s+reset\s+--hard/, label: 'git reset --hard (discards all uncommitted changes)' },
|
||||
{ pattern: /git\s+clean\s+-[a-z]*f/, label: 'git clean -f (permanently deletes untracked files)' },
|
||||
{ pattern: /git\s+(checkout|restore)\s+\./, label: 'git checkout/restore . (discards all working tree changes)' },
|
||||
{ pattern: /rm\s+-[rf]/, label: 'rm -rf (recursive/force delete)' },
|
||||
{ pattern: /chmod\s+(-R\s+)?777/, label: 'chmod 777 (world-writable permissions)' },
|
||||
{ pattern: /npm\s+publish/, label: 'npm publish (publishes package to registry)' },
|
||||
{ pattern: /pip\s+upload|twine\s+upload/, label: 'pip/twine upload (publishes package to PyPI)' },
|
||||
{ pattern: /docker\s+system\s+prune/, label: 'docker system prune (removes all unused resources)' },
|
||||
{ pattern: /DROP\s+(DATABASE|TABLE)/i, label: 'SQL DROP (destroys database/table)' },
|
||||
{ pattern: /DELETE\s+FROM\s+(?!.*WHERE)/i, label: 'DELETE without WHERE (deletes all rows)' },
|
||||
{ pattern: /UPDATE\s+\S+\s+SET\s+(?!.*WHERE)/i, label: 'UPDATE without WHERE (updates all rows)' },
|
||||
{ pattern: /TRUNCATE\s+TABLE/i, label: 'SQL TRUNCATE (empties entire table)' },
|
||||
];
|
||||
|
||||
for (const { pattern, label } of confirmPatterns) {
|
||||
if (pattern.test(command)) {
|
||||
confirmLabel = label;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === File write security check ===
|
||||
} else if (toolName === 'Write' || toolName === 'Edit') {
|
||||
const filePath = input.tool_input?.file_path || '';
|
||||
const homeDir = os.homedir();
|
||||
const repoRoot = common.getRepoRoot(cwd);
|
||||
const resolved = path.resolve(cwd, filePath);
|
||||
|
||||
// Tier 1: Block — system paths
|
||||
const sensitivePaths = [
|
||||
'/etc/', '/usr/bin/', '/usr/sbin/',
|
||||
'/bin/', '/sbin/', '/System/',
|
||||
'/dev/', '/proc/', '/sys/'
|
||||
];
|
||||
|
||||
for (const sp of sensitivePaths) {
|
||||
if (filePath.startsWith(sp)) {
|
||||
decision = 'deny';
|
||||
reason = `Writing to system path denied: ${sp}`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Block — outside repo and home
|
||||
if (decision === 'allow') {
|
||||
const insideRepo = isPathInside(repoRoot, resolved);
|
||||
const insideHome = isPathInside(homeDir, resolved);
|
||||
|
||||
if (!insideRepo && !insideHome) {
|
||||
decision = 'deny';
|
||||
reason = 'Path traversal attack detected: resolved path is outside the repository and home directory';
|
||||
} else if (!insideRepo && insideHome) {
|
||||
const relativeHomePath = path.relative(homeDir, resolved) || path.basename(resolved);
|
||||
confirmLabel = `home directory path outside repo (~/${relativeHomePath})`;
|
||||
}
|
||||
}
|
||||
|
||||
// Tier 2: Confirm — sensitive files
|
||||
if (decision === 'allow') {
|
||||
const fileName = path.basename(filePath);
|
||||
|
||||
if (fileName.startsWith('.env')) {
|
||||
confirmLabel = `.env file (${fileName})`;
|
||||
}
|
||||
|
||||
if (!confirmLabel) {
|
||||
const sensitiveFiles = ['credentials.json', 'key.pem', 'key.json', 'id_rsa'];
|
||||
for (const sf of sensitiveFiles) {
|
||||
if (fileName === sf) {
|
||||
confirmLabel = `sensitive file (${sf})`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!confirmLabel) {
|
||||
const sensitivePathPatterns = ['.aws/credentials', '.npmrc'];
|
||||
for (const sp of sensitivePathPatterns) {
|
||||
if (filePath.includes(sp)) {
|
||||
confirmLabel = `sensitive path (${sp})`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === Build output ===
|
||||
if (decision === 'deny') {
|
||||
const errorOutput = {
|
||||
hookSpecificOutput: { permissionDecision: 'deny' },
|
||||
systemMessage: `🛑 Blocked: ${reason}\n\nTo perform this operation, run it manually in the terminal.`
|
||||
};
|
||||
console.error(JSON.stringify(errorOutput));
|
||||
process.exit(2);
|
||||
} else {
|
||||
const result = { continue: true };
|
||||
|
||||
if (confirmLabel) {
|
||||
result.systemMessage = `⚠️ CONFIRM REQUIRED: ${confirmLabel}\n\nYou MUST ask the user for explicit confirmation before executing this operation. Do NOT proceed without user approval.`;
|
||||
}
|
||||
|
||||
console.log(JSON.stringify(result));
|
||||
process.exit(0);
|
||||
}
|
||||
Reference in New Issue
Block a user