UNPKG

@inso_web/els-mcp

Version:

MCP-сервер поверх INSO Error Logs Service. Read-only tools (search, analytics, fingerprinting, correlations) для подключения Claude Desktop/Code и ChatGPT к логам ошибок. Streamable HTTP transport + stdio для npx-запуска.

160 lines 6.08 kB
/** * Pipeline редакции ErrorLog items перед возвратом из tools. * * Контракт: * - Не мутирует исходный объект, возвращает плоский клон. * - Применяет PII regex ко всем string-полям (включая компактные варианты). * - Анонимизирует `ip` (last octet → 0 для IPv4, /64 для IPv6). * - Усекает `userAgent` до family. * - Strip query из `url` и `referrer`. * - Оборачивает `message`/`stack` в `<untrusted>...</untrusted>` для compact/full форматов. * - Считает `suspiciousContent` по deny-list (см. promptInjection.ts). * * Конфиг управляется ENV (см. src/config.ts): * - MCP_REDACTION_ENABLED (default true) * - MCP_REDACTION_FIELDS (csv override — например `email,phone,jwt`) */ import { redactString } from './fields.js'; import { userAgentFamily } from './userAgent.js'; import { stripUrlQuery } from './url.js'; import { redactValue } from './argsRedactor.js'; import { wrapUntrusted, detectSuspicious } from './promptInjection.js'; export const DEFAULT_REDACTION_CONFIG = { enabled: true, wrapUntrusted: true, }; function shouldApply(field, cfg) { if (!cfg.enabled) return false; if (!cfg.fields || cfg.fields.size === 0) return true; return cfg.fields.has(field); } /** * Список «строковых» полей ErrorLog, которые мы прогоняем через `redactString`. * Стек, message — отдельно (с wrap-обёрткой). */ const STRING_FIELDS_TO_SCAN = [ 'browser', 'urlPath', 'errorCategory', 'appSlug', 'serviceName', 'deploymentEnv', 'language', 'fingerprint', 'sessionId', 'appVersion', 'screenSize', 'viewportSize', ]; /** * Редактирует один ErrorLog (или его compact-вариант). Возвращает новый объект. */ export function redactErrorLog(log, opts = {}) { const cfg = opts.config ?? DEFAULT_REDACTION_CONFIG; const fieldsHit = new Set(); let suspiciousRule; if (!cfg.enabled) { return { value: log, stats: { fieldsHit: [], suspiciousContentBlocked: false }, }; } const out = { ...log }; // 1. IP (отдельная анонимизация). if (shouldApply('ip', cfg) && typeof out.ip === 'string' && out.ip.length > 0) { const { value, fieldsHit: hits } = redactString(out.ip); out.ip = value; for (const h of hits) fieldsHit.add(h); } // 2. userAgent → family. if (shouldApply('userAgent', cfg) && typeof out.userAgent === 'string') { const fam = userAgentFamily(out.userAgent); out.userAgent = fam; if (fam !== out.userAgent) fieldsHit.add('userAgent'); } // 3. URL / referrer — strip query. if (shouldApply('url', cfg)) { if (typeof out.url === 'string') out.url = stripUrlQuery(out.url); if (typeof out.referrer === 'string') out.referrer = stripUrlQuery(out.referrer); } // 4. Простые строковые поля — regex pii. for (const f of STRING_FIELDS_TO_SCAN) { if (!shouldApply(f, cfg)) continue; const v = out[f]; if (typeof v === 'string' && v.length > 0) { const { value, fieldsHit: hits } = redactString(v); out[f] = value; for (const h of hits) fieldsHit.add(h); } } // 5. message + stack — отдельно. Сначала regex, потом suspicious-detect, потом wrap. const processSensitiveText = (key) => { if (!shouldApply(key, cfg)) return; const v = out[key]; if (typeof v !== 'string' || v.length === 0) return; const { value, fieldsHit: hits } = redactString(v); for (const h of hits) fieldsHit.add(h); const suspicious = detectSuspicious(value); if (suspicious && !suspiciousRule) suspiciousRule = suspicious.rule; out[key] = cfg.wrapUntrusted !== false ? wrapUntrusted(value) : value; }; processSensitiveText('message'); processSensitiveText('stack'); processSensitiveText('componentStack'); // 6. aiDiagnosis — если есть, прогоняем через универсальный redactor. if (out.aiDiagnosis !== undefined && out.aiDiagnosis !== null) { const { value, fieldsHit: hits } = redactValue(out.aiDiagnosis); out.aiDiagnosis = value; for (const h of hits) fieldsHit.add(h); } return { value: out, stats: { fieldsHit: Array.from(fieldsHit), suspiciousContentBlocked: !!suspiciousRule, ...(suspiciousRule ? { suspiciousRule } : {}), }, }; } /** * Массовая редакция items. Объединяет stats по всем. */ export function redactErrorLogs(logs, opts = {}) { const fieldsHit = new Set(); let suspiciousRule; const items = logs.map((log) => { const { value, stats } = redactErrorLog(log, opts); for (const f of stats.fieldsHit) fieldsHit.add(f); if (stats.suspiciousContentBlocked && !suspiciousRule) suspiciousRule = stats.suspiciousRule; return value; }); return { items, stats: { fieldsHit: Array.from(fieldsHit), suspiciousContentBlocked: !!suspiciousRule, ...(suspiciousRule ? { suspiciousRule } : {}), }, }; } export { redactValue } from './argsRedactor.js'; export { wrapUntrusted, detectSuspicious, containsSuspicious } from './promptInjection.js'; export { userAgentFamily } from './userAgent.js'; export { stripUrlQuery } from './url.js'; export { redactString } from './fields.js'; //# sourceMappingURL=index.js.map