Files
portal/tools/enforce-read-path-deny.mjs
T
Дмитрий 37ce771eb1 fix(m5): F-1 — убрать мёртвый контент-скан в enforce-read-path-deny
decide() гейтил по content в ветке, недостижимой в проде: enforce-read-path-deny —
PreToolUse(Read)-хук, main() не передавал content, а контента до чтения нет. Ветка
+ импорт scanSecrets убраны — decide() гейтит строго по пути (path-deny). Реальный
exfil (исходящий payload) закрыт живым enforce-mcp-classification.scanEgress; чтение
секрета само по себе не вынос.

Аудит Машины 5 (объектив sharp-edges + agentic-actions-auditor). TDD RED->GREEN.
Регрессия tools-only 2789 passed + 2 skip.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 16:18:05 +03:00

60 lines
2.6 KiB
JavaScript

/**
* PreToolUse(Read) wrapper — path-deny for Read tool.
* Router-gate v4 emergency fix (Smoke 5 2026-05-30).
*
* Spec §3.1 declared transcript JSONL hard-deny but Read tool had NO
* path-protection — controller could Read ~/.claude/projects/*.jsonl
* (parent context exfil from other sessions). Same for runtime artifacts,
* .env, normative files.
*
* Reuses DEFAULT_PROTECTED_PATTERNS from shell-content-rules.mjs.
* Fail-CLOSE on internal error (security default).
*/
import { fileURLToPath } from 'url';
import {
readStdin,
parseEventJson,
exitDecision,
} from './enforce-hook-helpers.mjs';
import { defaultPathNormalize, isProtectedPath, READ_DENY_PATTERNS } from './shell-content-rules.mjs';
export function decide({ toolName, filePath }) {
if (toolName !== 'Read') return { block: false, reason: null };
const fp = String(filePath || '');
// Path-deny only. Narrow READ_DENY_PATTERNS (not the full DEFAULT_PROTECTED_PATTERNS):
// Read of CLAUDE.md / normative docs / memory has no exfil value and must stay allowed for
// the claude-md-management / memory-sync workflow. Only genuine Read-exfil targets —
// transcripts, runtime, settings, secrets — are blocked. The full protected-list still
// guards Bash/PowerShell read and Write (over-block fix 2026-05-31).
// F-1 (аудит 2026-06-07): контент-скан выдачи убран — был МЁРТВ в проде (PreToolUse(Read)-хук,
// main() не передавал content; контента до чтения нет). Реальный exfil (исходящий payload)
// закрыт живым enforce-mcp-classification.scanEgress. decide() гейтит строго по пути.
if (fp && isProtectedPath(fp, defaultPathNormalize, READ_DENY_PATTERNS)) {
return {
block: true,
reason: `path «${defaultPathNormalize(fp)}» protected against Read (§3.1 transcript/runtime/secrets hard-deny)`,
};
}
return { block: false, reason: null };
}
async function main() {
try {
const raw = await readStdin();
const event = parseEventJson(raw);
const r = decide({
toolName: event.tool_name,
filePath: event.tool_input?.file_path || event.tool_input?.filePath,
});
if (r.block) {
return exitDecision({ block: true, message: `[read-path-deny] ${r.reason}` });
}
return exitDecision({ block: false });
} catch {
return exitDecision({ block: true, message: '[read-path-deny] внутренняя ошибка — fail-CLOSE' });
}
}
const isCli = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
if (isCli) main();