package ru.pdguard.detect; import java.util.List; import java.util.function.Predicate; import java.util.regex.Pattern; /** * Одно правило детекции персональных данных. * *

Добавление нового типа ПД — это добавление одного {@code Rule} в {@link RuleRegistry}; менять * остальной код не требуется. * * @param type тип ПД, который распознаёт правило * @param pattern регулярное выражение * @param priority приоритет при разрешении перекрытий * @param groups номера групп, которые маскируются; {@code 0} — всё совпадение целиком. Несколько * групп нужны, когда значение разорвано словами: «серия 4509 номер 123456» * @param validator дополнительная проверка значения (контрольная сумма, диапазон дат); {@code null} * — проверка не нужна * @param veto шаблон окружения, при котором совпадение персональными данными не считается: адрес * отделения банка не является ПД, хотя выглядит как адрес * @param context шаблон окружения, который обязан присутствовать рядом. Нужен там, где форма * совпадения сама по себе слишком общая: «Невский проспект» это адрес рядом с домом и индексом * и просто топоним в рассказе о городе * @param anchors строчные подстроки, одна из которых обязана встретиться в тексте. Проверка через * {@code indexOf} на порядок дешевле запуска регулярного выражения и отсекает большинство * правил на коротком запросе. Пустой список — правило запускается всегда */ public record Rule( String type, Pattern pattern, int priority, List groups, Predicate validator, Pattern veto, Pattern context, List anchors) { public Rule { groups = List.copyOf(groups); anchors = List.copyOf(anchors); } /** * Флаги компиляции для всех правил. * *

{@code UNICODE_CHARACTER_CLASS} обязателен: без него {@code \w}, {@code \W} и {@code \b} в * Java охватывают только латиницу, и якорные слова вроде «водительское удостоверение» не * находятся. {@code UNICODE_CASE} делает {@code (?i)} корректным для кириллицы. */ private static final int FLAGS = Pattern.UNICODE_CHARACTER_CLASS | Pattern.UNICODE_CASE; /** * Сколько символов слева и справа от совпадения просматривает вето-шаблон. * *

150, не 80: на реальных адресах отделений из реестра ЦБ (регион, город, улица, дом — в одном * предложении) расстояние от «отделение» до номера дома часто превышает 80 знаков за счёт * длинного названия региона («Ханты-Мансийский автономный округ», «Кабардино-Балкарская * Республика»). Найдено нагрузочным тестом на 60 реальных адресах из официального реестра — с * окном в 80 знаков вето не срабатывало на части из них. */ public static final int VETO_LOOKBEHIND = 150; public static final int VETO_LOOKAHEAD = 40; /** Правило без проверок, маскируется всё совпадение. */ public static Rule of(String type, String regex, int priority) { return new Rule( type, Pattern.compile(regex, FLAGS), priority, List.of(0), null, null, null, List.of()); } /** Маскировать только перечисленные группы, а не всё совпадение. */ public Rule groups(Integer... indexes) { return new Rule(type, pattern, priority, List.of(indexes), validator, veto, context, anchors); } /** Принять совпадение, только если значение прошло проверку. */ public Rule validatedBy(Predicate check) { return new Rule(type, pattern, priority, groups, check, veto, context, anchors); } /** Запускать правило, только если в тексте есть одна из подстрок (в нижнем регистре). */ public Rule anchoredBy(String... required) { return new Rule(type, pattern, priority, groups, validator, veto, context, List.of(required)); } /** Есть ли в тексте хоть один из якорей правила. */ public boolean mayMatch(String lowercasedText) { if (anchors.isEmpty()) { return true; } for (String anchor : anchors) { if (lowercasedText.contains(anchor)) { return true; } } return false; } /** Принять совпадение, только если рядом встретилось указанное слово. */ public Rule requiringNear(String regex) { return new Rule( type, pattern, priority, groups, validator, veto, Pattern.compile(regex, FLAGS), anchors); } /** Отбросить совпадение, если рядом встретилось указанное слово. */ public Rule vetoedBy(String regex) { return new Rule( type, pattern, priority, groups, validator, Pattern.compile(regex, FLAGS), context, anchors); } }