397777089e
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
101 lines
5.8 KiB
JavaScript
101 lines
5.8 KiB
JavaScript
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);
|
||
});
|
||
});
|