@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
JavaScript
/**
* 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