import { describe, it, expect } from 'vitest'; import { decide } from './enforce-mcp-classification.mjs'; describe('enforce-mcp-classification decide()', () => { it('allows non-mcp tools (no-op)', () => { expect(decide({ toolName: 'Bash', toolInput: { command: 'ls' } }).block).toBe(false); }); it('blocks an unknown mcp tool (default-deny)', () => { const r = decide({ toolName: 'mcp__unknown__doSomething', toolInput: {} }); expect(r.block).toBe(true); expect(r.reason).toMatch(/not in gate-config classification/); }); }); // 7.3 (Блок 4.3) — скан ИСХОДЯЩЕГО (egress): даже разрешённый классификатором MCP-вызов не // должен выносить наружу секреты (общий secret-scan) или использовать exfil-схемы // (data:base64 / file:// / IP-литерал URL) или раздутый payload. fail-close на egress. import { scanEgress } from './enforce-mcp-classification.mjs'; describe('scanEgress (7.3): скан исходящего MCP-payload на exfil', () => { it('секрет в аргументах (AWS) → block', () => { const r = scanEgress({ key: 'AKIAIOSFODNN7EXAMPLE' }); expect(r.block).toBe(true); expect(r.reason).toMatch(/секрет/i); }); it('data:base64 URI → block (exfil-вектор)', () => { expect(scanEgress({ url: 'data:text/plain;base64,SGVsbG8=' }).block).toBe(true); }); it('file:// схема → block', () => { expect(scanEgress({ src: 'file:///etc/passwd' }).block).toBe(true); }); it('IP-литерал URL → block (обход DNS/allowlist)', () => { expect(scanEgress({ url: 'http://203.0.113.5/collect' }).block).toBe(true); }); it('раздутый payload → block', () => { expect(scanEgress({ blob: 'x'.repeat(50) }, { maxBytes: 20 }).block).toBe(true); }); it('чистые аргументы → не block', () => { expect(scanEgress({ key: 'user:42:profile' }).block).toBe(false); }); }); describe('decide 7.3: egress-скан даже на разрешённом классификатором MCP-вызове', () => { it('read_only mcp + секрет в аргументах → block (egress)', () => { const r = decide({ toolName: 'mcp__redis__get', toolInput: { key: 'ghp_0123456789abcdefghijklmnopqrstuvwxyz' } }); expect(r.block).toBe(true); expect(r.reason).toMatch(/egress/i); }); it('read_only mcp + чистые аргументы → allow', () => { expect(decide({ toolName: 'mcp__redis__get', toolInput: { key: 'session:abc' } }).block).toBe(false); }); }); // enforce-mcp-classification.mjs — egress/verdict escape (M6 Пакет 6) import { canonicalAction } from './escape-grant.mjs'; describe('decide — egress/verdict escape снимается floor_escape (M6)', () => { const now = 1000; it('egress-блок (read_only + секрет) снимается совпавшим floor_escape', () => { const input = { key: 'ghp_0123456789abcdefghijklmnopqrstuvwxyz' }; const action = canonicalAction('mcp__redis__get', input); const r = decide({ toolName: 'mcp__redis__get', toolInput: input, escapeGrants: [{ action, ts: now - 5 }], escapeConsumed: [], now }); expect(r.block).toBe(false); }); it('egress-блок без пропуска остаётся', () => { const r = decide({ toolName: 'mcp__redis__get', toolInput: { key: 'ghp_0123456789abcdefghijklmnopqrstuvwxyz' }, escapeGrants: [], escapeConsumed: [], now }); expect(r.block).toBe(true); }); it('verdict-блок (unknown mcp) снимается совпавшим floor_escape', () => { const input = {}; const action = canonicalAction('mcp__unknown__do', input); const r = decide({ toolName: 'mcp__unknown__do', toolInput: input, escapeGrants: [{ action, ts: now - 5 }], escapeConsumed: [], now }); expect(r.block).toBe(false); }); it('погашенный пропуск (one-shot) → block остаётся', () => { const input = {}; const action = canonicalAction('mcp__unknown__do', input); const r = decide({ toolName: 'mcp__unknown__do', toolInput: input, escapeGrants: [{ action, ts: now - 5 }], escapeConsumed: [{ action, ts: now - 5 }], now }); expect(r.block).toBe(true); }); }); // enforce-mcp-classification.mjs — G-5 точный токен в блок-сообщении egress/verdict (M6 FIX-1) describe('enforce-mcp-classification G-5 egress/verdict токен (M6 FIX-1)', () => { const now = 1000; it('verdict-блок (unknown mcp) выводит точный FLOOR-ESCAPE токен', () => { const input = {}; const action = canonicalAction('mcp__unknown__do', input); const r = decide({ toolName: 'mcp__unknown__do', toolInput: input, escapeGrants: [], escapeConsumed: [], now }); expect(r.block).toBe(true); expect(r.reason).toContain(`FLOOR-ESCAPE: ${action}`); }); it('egress-блок выводит точный FLOOR-ESCAPE токен', () => { const input = { key: 'ghp_0123456789abcdefghijklmnopqrstuvwxyz' }; const action = canonicalAction('mcp__redis__get', input); const r = decide({ toolName: 'mcp__redis__get', toolInput: input, escapeGrants: [], escapeConsumed: [], now }); expect(r.block).toBe(true); expect(r.reason).toContain(`FLOOR-ESCAPE: ${action}`); }); }); // Task 7 wiring — decide() прокидывает config urlWhitelist в classifyMcpTool (config-seam). describe('decide — config-seam urlWhitelist (Task 7 wiring)', () => { it('пустой urlWhitelist → даже liderra.ru блокируется (fail-CLOSED)', () => { const r = decide({ toolName: 'mcp__playwright__browser_navigate', toolInput: { url: 'https://liderra.ru/x' }, urlWhitelist: [] }); expect(r.block).toBe(true); }); it('urlWhitelist с доменом → тот же домен разрешён', () => { const r = decide({ toolName: 'mcp__playwright__browser_navigate', toolInput: { url: 'https://example.com/x' }, urlWhitelist: ['example.com'] }); expect(r.block).toBe(false); }); });