Files
brain/tools/enforce-read-path-deny.test.mjs
T

101 lines
5.8 KiB
JavaScript
Raw 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.
import { describe, it, expect } from 'vitest';
import { decide } from './enforce-read-path-deny.mjs';
describe('enforce-read-path-deny decide()', () => {
it('allows Read on normal project file', () => {
const r = decide({ toolName: 'Read', filePath: 'docs/observer/STATUS.md' });
expect(r.block).toBe(false);
});
it('blocks Read on ~/.claude/projects/*.jsonl transcript', () => {
const r = decide({ toolName: 'Read', filePath: '~/.claude/projects/abc-session.jsonl' });
expect(r.block).toBe(true);
expect(r.reason).toMatch(/protected/i);
});
it('blocks Read on absolute /c/Users/.../.claude/projects/x.jsonl', () => {
const r = decide({ toolName: 'Read', filePath: '/c/Users/Administrator/.claude/projects/proj/session.jsonl' });
expect(r.block).toBe(true);
});
it('blocks Read on ~/.claude/runtime/*.json (runtime artifacts)', () => {
const r = decide({ toolName: 'Read', filePath: '~/.claude/runtime/router-state-x.json' });
expect(r.block).toBe(true);
});
it('blocks Read on .env', () => {
const r = decide({ toolName: 'Read', filePath: '.env' });
expect(r.block).toBe(true);
});
it('allows non-Read tool calls (no-op)', () => {
const r = decide({ toolName: 'Bash', filePath: 'whatever' });
expect(r.block).toBe(false);
});
});
// Over-block fix (2026-05-31): Smoke 5 added CLAUDE.md + memory/ + normative
// docs to the Read-deny set, which broke the legit claude-md-management /
// memory-sync workflow (Edit requires a prior Read). Read of CLAUDE.md / memory
// / Pravila has no exfil value (public-in-repo / own memory index). The genuine
// Read-exfil targets — cross-session transcripts (.jsonl) and ~/.claude/runtime
// — MUST stay blocked. Bash/PowerShell/Write protections (DEFAULT_PROTECTED_PATTERNS)
// are unchanged.
describe('enforce-read-path-deny — CLAUDE.md / memory readable (over-block fix 2026-05-31)', () => {
it('allows Read on CLAUDE.md (public-in-repo, no exfil value)', () => {
expect(decide({ toolName: 'Read', filePath: 'CLAUDE.md' }).block).toBe(false);
expect(decide({ toolName: 'Read', filePath: '/c/моя/проекты/портал crm/Документация/CLAUDE.md' }).block).toBe(false);
});
it('allows Read on MEMORY.md (own memory index under .claude/projects/<proj>/memory)', () => {
expect(decide({ toolName: 'Read', filePath: '/c/Users/Administrator/.claude/projects/crm/memory/MEMORY.md' }).block).toBe(false);
});
it('allows Read on a memory/*.md feedback file', () => {
expect(decide({ toolName: 'Read', filePath: '/c/Users/Administrator/.claude/projects/crm/memory/feedback_read_path_deny.md' }).block).toBe(false);
});
it('allows Read on a normative doc (Pravila) — needed for claude-md-management', () => {
expect(decide({ toolName: 'Read', filePath: 'docs/Pravila_raboty_Claude_v1_1.md' }).block).toBe(false);
});
it('STILL blocks Read on transcript JSONL under .claude/projects', () => {
expect(decide({ toolName: 'Read', filePath: '/c/Users/Administrator/.claude/projects/crm/session.jsonl' }).block).toBe(true);
expect(decide({ toolName: 'Read', filePath: '~/.claude/projects/abc-session.jsonl' }).block).toBe(true);
});
it('STILL blocks Read on ~/.claude/runtime artifacts', () => {
expect(decide({ toolName: 'Read', filePath: '~/.claude/runtime/router-state-x.json' }).block).toBe(true);
});
});
// Impl completion (2026-05-31, this session): exfil-pattern boundaries.
describe('enforce-read-path-deny — exfil-pattern boundaries (impl completion 2026-05-31)', () => {
it('STILL blocks Read on .env.production (secrets variant)', () => {
expect(decide({ toolName: 'Read', filePath: '.env.production' }).block).toBe(true);
});
it('allows Read on a Tooling normative doc (needed for normative sync)', () => {
expect(decide({ toolName: 'Read', filePath: 'docs/Tooling_v8_3.md' }).block).toBe(false);
});
});
// F-1 (аудит 2026-06-07): контент-скан Read был МЁРТВ в проде — main() в
// enforce-read-path-deny не передавал content, а это PreToolUse(Read)-хук (контента
// до чтения нет). Ветка убрана: decide() гейтит ТОЛЬКО по пути. Реальный exfil
// (исходящий payload) закрыт живым enforce-mcp-classification.scanEgress; чтение
// секрета ≠ вынос. Тесты кодируют новый контракт «content игнорируется».
describe('enforce-read-path-deny — F-1: decide() гейтит только по пути, content игнорируется', () => {
it('Read обычного пути с секрет-контентом → allow (контент-скан убран; exfil ловит egress)', () => {
const r = decide({ toolName: 'Read', filePath: 'app/config.php', content: 'KEY=AKIAIOSFODNN7EXAMPLE' });
expect(r.block).toBe(false);
});
it('Read защищённого пути блокируется независимо от наличия content', () => {
const r = decide({ toolName: 'Read', filePath: '.env', content: 'irrelevant' });
expect(r.block).toBe(true);
});
});
import { canonicalAction } from './escape-grant.mjs';
describe('enforce-read-path-deny — escape-honor (M7 Фаза 2, правило 7в)', () => {
const now = 1_000_000;
it('защищённый путь БЕЗ escape → block (регресс)', () => {
expect(decide({ toolName: 'Read', filePath: '.env' }).block).toBe(true);
});
it('защищённый путь + матч escape-грант владельца → block:false', () => {
const action = canonicalAction('Read', { file_path: '.env' });
const r = decide({ toolName: 'Read', filePath: '.env',
escapeGrants: [{ action, ts: now - 1000 }], escapeConsumed: [], now });
expect(r.block).toBe(false);
});
});