Files
brain/tools/enforce-mcp-classification.test.mjs
T
2026-06-15 19:21:13 +03:00

116 lines
6.0 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-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);
});
});