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