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-запуска.

155 lines 6.4 kB
/** * Regex-паттерны и маскировщики для PII redaction. * * Контракт: все функции — pure (без side effects), принимают строку и * возвращают строку с применённой маской. Поведение детерминировано; * порядок применения — IP → email → JWT → credit card → phone, чтобы более * специфичные совпадения (например IP внутри текста) не были разрушены * последующими более «жадными» regex'ами. */ /** IPv4: четыре октета 0–255, разделены `.`. */ export const IPV4_RE = /\b(?:(?:25[0-5]|2[0-4]\d|1?\d{1,2})\.){3}(?:25[0-5]|2[0-4]\d|1?\d{1,2})\b/g; /** * IPv6 (упрощённо): 2–8 групп hex, разделённых `:`, с поддержкой `::`-сжатия. * Не идеально, но покрывает типичные случаи в логах. False-positives на адресах * вида `aaaa:bbbb` принимаемы — мы перестрахуемся. */ export const IPV6_RE = /\b(?:[0-9a-fA-F]{1,4}:){2,7}[0-9a-fA-F]{1,4}\b|\b(?:[0-9a-fA-F]{1,4}:){1,7}:\b|\b::(?:[0-9a-fA-F]{1,4}:){0,6}[0-9a-fA-F]{1,4}\b/g; /** Email: local-part + @ + domain. */ export const EMAIL_RE = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g; /** * JWT: три base64url-сегмента, разделённые точкой. Начало `eyJ` (header `{"`) * добавлено как anchor для снижения false-positives. */ export const JWT_RE = /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g; /** Bearer/Authorization header value. */ export const BEARER_RE = /\b[Bb]earer\s+[A-Za-z0-9._\-+/=]+/g; /** * Phone: loose — от 8 цифр, **обязательно** с `+` префиксом или хотя бы * одним разделителем (пробел / скобка / дефис). Это исключает совпадения * с длинными числовыми ID и неаутентичными номерами карт. */ export const PHONE_RE = /(?<![\d])(?:\+\d[\d\s()\-]{6,}\d|\d{1,4}[\s\-()][\d\s()\-]{5,}\d)(?![\d])/g; /** * Credit card: 13–19 цифр с опциональными разделителями. После regex применяем * Luhn-check, чтобы не маскировать обычные числовые ID. */ export const CARD_RE = /\b(?:\d[ -]?){13,19}\b/g; /** * Заменить IPv4 на «анонимизированный»: 192.168.1.42 → 192.168.1.0. * IPv6 → /64 prefix (первые 4 группы + ::). */ export function maskIpv4(ip) { const parts = ip.split('.'); if (parts.length !== 4) return ip; return `${parts[0]}.${parts[1]}.${parts[2]}.0`; } export function maskIpv6(ip) { // Раскрыть `::`-сжатие до 8 групп, взять первые 4, склеить обратно с `::`. const expanded = expandIpv6(ip); if (!expanded) return ip; return `${expanded.slice(0, 4).join(':')}::/64`; } function expandIpv6(ip) { if (!ip.includes(':')) return null; const [head, tail] = ip.split('::'); const headParts = head ? head.split(':') : []; const tailParts = tail ? tail.split(':') : []; const missing = 8 - headParts.length - tailParts.length; if (missing < 0) return null; if (ip.includes('::')) { return [...headParts, ...Array(missing).fill('0'), ...tailParts]; } if (headParts.length !== 8) return null; return headParts; } /** Luhn algorithm для credit card validation. */ export function luhnValid(digits) { const clean = digits.replace(/\D/g, ''); if (clean.length < 13 || clean.length > 19) return false; let sum = 0; let alt = false; for (let i = clean.length - 1; i >= 0; i--) { let n = parseInt(clean.charAt(i), 10); if (alt) { n *= 2; if (n > 9) n -= 9; } sum += n; alt = !alt; } return sum % 10 === 0; } export const REDACTION_TOKENS = { ip: '[IP_REDACTED]', email: '[EMAIL_REDACTED]', jwt: '[JWT_REDACTED]', bearer: '[BEARER_REDACTED]', phone: '[PHONE_REDACTED]', card: '[CARD_REDACTED]', }; /** * Применить все базовые правки к строке. Порядок важен (см. §1). * * Возвращает { value, fieldsHit } — массив имён полей, которые сработали * (для метрик / `_meta.redactionFields`). */ export function redactString(input) { if (typeof input !== 'string' || input.length === 0) { return { value: input, fieldsHit: [] }; } const fieldsHit = new Set(); let out = input; // IPv6 первым — он не пересекается с другими ID'шками так часто, как IPv4. out = out.replace(IPV6_RE, (m) => { const masked = maskIpv6(m); if (masked !== m) { fieldsHit.add('ip'); return masked; } return m; }); out = out.replace(IPV4_RE, (m) => { fieldsHit.add('ip'); return maskIpv4(m); }); out = out.replace(JWT_RE, () => { fieldsHit.add('jwt'); return REDACTION_TOKENS.jwt; }); out = out.replace(BEARER_RE, () => { fieldsHit.add('bearer'); return REDACTION_TOKENS.bearer; }); out = out.replace(EMAIL_RE, () => { fieldsHit.add('email'); return REDACTION_TOKENS.email; }); out = out.replace(CARD_RE, (m) => { if (luhnValid(m)) { fieldsHit.add('card'); return REDACTION_TOKENS.card; } return m; }); out = out.replace(PHONE_RE, (m) => { // Игнорим, если в найденной подстроке уже подменили IP/Card/etc. if (m.includes('[') && m.includes('_REDACTED]')) return m; // Минимум 8 цифр. const digits = m.replace(/\D/g, ''); if (digits.length < 8) return m; fieldsHit.add('phone'); return REDACTION_TOKENS.phone; }); return { value: out, fieldsHit: Array.from(fieldsHit) }; } //# sourceMappingURL=fields.js.map