feat: detect account number, BIK, card expiry, OGRN(IP), KPP, income, biometrics
Types the spec asks for beyond the payment card itself: settlement account (20 digits), BIK, card expiry date (kept separate from birth date), OGRN/OGRNIP (negative lookahead so OGRNIP isn't half- swallowed by the OGRN rule), KPP, income/salary amounts, and mentions of biometric enrollment (ЕБС, voice/fingerprint templates). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
e52e9550e6
commit
495cf08a05
@@ -56,6 +56,16 @@ public class RuleRegistry {
|
|||||||
public static final String BIRTH_CERTIFICATE = "BIRTH_CERTIFICATE";
|
public static final String BIRTH_CERTIFICATE = "BIRTH_CERTIFICATE";
|
||||||
public static final String MEDICAL_POLICY = "MEDICAL_POLICY";
|
public static final String MEDICAL_POLICY = "MEDICAL_POLICY";
|
||||||
|
|
||||||
|
/** Банковские реквизиты сверх платёжной карты. */
|
||||||
|
public static final String ACCOUNT_NUMBER = "ACCOUNT_NUMBER";
|
||||||
|
public static final String BIK = "BIK";
|
||||||
|
public static final String CARD_EXPIRY = "CARD_EXPIRY";
|
||||||
|
public static final String INCOME = "INCOME";
|
||||||
|
public static final String OGRN = "OGRN";
|
||||||
|
public static final String OGRNIP = "OGRNIP";
|
||||||
|
public static final String KPP = "KPP";
|
||||||
|
public static final String BIOMETRIC = "BIOMETRIC";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Слово с заглавной буквы; остальные буквы любого регистра, чтобы
|
* Слово с заглавной буквы; остальные буквы любого регистра, чтобы
|
||||||
* «ИВАНОВ» распознавался наравне с «Иванов».
|
* «ИВАНОВ» распознавался наравне с «Иванов».
|
||||||
@@ -181,6 +191,52 @@ public class RuleRegistry {
|
|||||||
.groups(1)
|
.groups(1)
|
||||||
.anchoredBy("подразделени", "к/п"),
|
.anchoredBy("подразделени", "к/п"),
|
||||||
|
|
||||||
|
// --- Банковские реквизиты сверх карты ---
|
||||||
|
|
||||||
|
// Расчётный счёт — ровно 20 цифр после якоря, группировка пробелами не важна.
|
||||||
|
Rule.of(ACCOUNT_NUMBER, "(?iu:р/с|расчетн\\w*\\s+счет|расчётн\\w*\\s+счёт|лицев\\w*\\s+счет|"
|
||||||
|
+ "лицев\\w*\\s+счёт)\\W{0,5}((?:\\d[ ]?){19}\\d)\\b", 83)
|
||||||
|
.groups(1)
|
||||||
|
.anchoredBy("р/с", "расчетн", "расчётн", "лицев"),
|
||||||
|
|
||||||
|
Rule.of(BIK, "(?iu:бик)\\W{0,5}(\\d{9})\\b", 83)
|
||||||
|
.groups(1)
|
||||||
|
.anchoredBy("бик"),
|
||||||
|
|
||||||
|
// «действительна до 09/27», «exp 09/27» — срок действия карты, не дата рождения.
|
||||||
|
Rule.of(CARD_EXPIRY, "(?iu:срок\\s+действия|действительна?\\s+до|\\bexp\\w*)\\W{0,5}"
|
||||||
|
+ "(\\d{2}\\s?/\\s?\\d{2})\\b", 83)
|
||||||
|
.groups(1)
|
||||||
|
.anchoredBy("срок действия", "действительн", "exp"),
|
||||||
|
|
||||||
|
// ОГРНИП раньше ОГРН: без отрицательного просмотра «ОГРНИП» частично ловился бы
|
||||||
|
// ещё и правилом ОГРН.
|
||||||
|
Rule.of(OGRNIP, "(?iu:огрнип)\\W{0,5}(\\d{15})\\b", 83)
|
||||||
|
.groups(1)
|
||||||
|
.anchoredBy("огрнип"),
|
||||||
|
|
||||||
|
Rule.of(OGRN, "(?iu:огрн(?!ип))\\W{0,5}(\\d{13})\\b", 83)
|
||||||
|
.groups(1)
|
||||||
|
.anchoredBy("огрн"),
|
||||||
|
|
||||||
|
Rule.of(KPP, "(?iu:кпп)\\W{0,5}(\\d{9})\\b", 83)
|
||||||
|
.groups(1)
|
||||||
|
.anchoredBy("кпп"),
|
||||||
|
|
||||||
|
// Доход/зарплата: сумма с разделителями тысяч. Между якорем и суммой может
|
||||||
|
// стоять слово («доход клиента», «доход за год») — без этого якорь ловил
|
||||||
|
// бы только «доход 85000», вплотную.
|
||||||
|
Rule.of(INCOME, "(?iu:доход|заработн\\w*\\s+плат\\w*|зарплат\\w*)(?:\\s+\\p{L}+){0,3}?"
|
||||||
|
+ "\\W{0,5}(\\d{1,3}(?:[\\s.]?\\d{3})*(?:,\\d{2})?)\\s?(?iu:руб\\p{L}*|₽)?\\b", 76)
|
||||||
|
.groups(1)
|
||||||
|
.anchoredBy("доход", "заработн", "зарплат"),
|
||||||
|
|
||||||
|
// Биометрия — сама фраза уже говорит, что дальше персональные данные, отдельного
|
||||||
|
// значения для захвата нет: маскируется якорная фраза целиком.
|
||||||
|
Rule.of(BIOMETRIC, "(?iu:биометрическ\\w*\\s+(?:данны\\w*|образц\\w*|шаблон\\w*)"
|
||||||
|
+ "|слепок\\s+голоса|отпечаток\\s+пальца|скан\\s+лица|\\bЕБС\\b)", 81)
|
||||||
|
.anchoredBy("биометри", "слепок голоса", "отпечаток пальца", "скан лица", "ебс"),
|
||||||
|
|
||||||
// --- Даты с явным якорем ---
|
// --- Даты с явным якорем ---
|
||||||
|
|
||||||
Rule.of(BIRTH_DATE, "(?iu:дат\\p{L}*\\s+рождения|дата\\s+рожд\\.)\\W{0,5}(" + DATE_ANY + ")", 87)
|
Rule.of(BIRTH_DATE, "(?iu:дат\\p{L}*\\s+рождения|дата\\s+рожд\\.)\\W{0,5}(" + DATE_ANY + ")", 87)
|
||||||
|
|||||||
@@ -57,7 +57,17 @@ public class Masker {
|
|||||||
Map.entry(RuleRegistry.ADDRESS_HOUSE, Strategies::stars),
|
Map.entry(RuleRegistry.ADDRESS_HOUSE, Strategies::stars),
|
||||||
Map.entry(RuleRegistry.ADDRESS_FLAT, Strategies::stars),
|
Map.entry(RuleRegistry.ADDRESS_FLAT, Strategies::stars),
|
||||||
Map.entry(RuleRegistry.BIRTH_PLACE, Strategies::stars),
|
Map.entry(RuleRegistry.BIRTH_PLACE, Strategies::stars),
|
||||||
Map.entry(RuleRegistry.CITIZENSHIP, Strategies::stars)
|
Map.entry(RuleRegistry.CITIZENSHIP, Strategies::stars),
|
||||||
|
|
||||||
|
Map.entry(RuleRegistry.ACCOUNT_NUMBER, EDGES),
|
||||||
|
Map.entry(RuleRegistry.OGRN, EDGES),
|
||||||
|
Map.entry(RuleRegistry.OGRNIP, EDGES),
|
||||||
|
Map.entry(RuleRegistry.KPP, EDGES),
|
||||||
|
// Срок действия карты — разделитель виден, сам месяц/год нет.
|
||||||
|
Map.entry(RuleRegistry.CARD_EXPIRY, Strategies::starsKeepingPunctuation),
|
||||||
|
Map.entry(RuleRegistry.BIK, Strategies::stars),
|
||||||
|
Map.entry(RuleRegistry.INCOME, Strategies::stars),
|
||||||
|
Map.entry(RuleRegistry.BIOMETRIC, Strategies::stars)
|
||||||
);
|
);
|
||||||
|
|
||||||
public String mask(String type, String value, MaskMode mode, MaskContext context) {
|
public String mask(String type, String value, MaskMode mode, MaskContext context) {
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package ru.pdguard;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import ru.pdguard.config.SystemPolicy;
|
||||||
|
import ru.pdguard.core.PayloadStore;
|
||||||
|
import ru.pdguard.core.Pipeline;
|
||||||
|
import ru.pdguard.detect.RuleRegistry;
|
||||||
|
import ru.pdguard.mask.Masker;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
|
||||||
|
/** Банковские реквизиты сверх платёжной карты: счёт, БИК, ОГРН(ИП), КПП, доход, биометрия. */
|
||||||
|
class BankTypesTest {
|
||||||
|
|
||||||
|
private final Pipeline pipeline =
|
||||||
|
new Pipeline(new RuleRegistry(), new Masker(), new PayloadStore(1_000_000L, 30));
|
||||||
|
|
||||||
|
private void assertHidden(String text, String secret) {
|
||||||
|
String masked = pipeline.process(text, UUID.randomUUID().toString(), SystemPolicy.DEFAULT);
|
||||||
|
assertFalse(masked.contains(secret), "не замаскировано: «" + secret + "» в ответе «" + masked + "»");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void masksAccountNumber() {
|
||||||
|
assertHidden("Расчётный счёт 40702810500000001234 открыт вчера", "40702810500000001234");
|
||||||
|
assertHidden("р/с 4070 2810 5000 0000 1234", "4070 2810 5000 0000 1234");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void masksBik() {
|
||||||
|
assertHidden("БИК 044525593 банка-получателя", "044525593");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void masksCardExpiryButNotCardNumber() {
|
||||||
|
String masked = pipeline.process(
|
||||||
|
"Карта 4111 1111 1111 1111, срок действия 09/27", "expiry-1", SystemPolicy.DEFAULT);
|
||||||
|
assertFalse(masked.contains("09/27"), masked);
|
||||||
|
assertEquals("Карта 41** **** **** **11, срок действия **/**", masked);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void masksOgrnAndOgrnipDifferently() {
|
||||||
|
assertHidden("ОГРН 1027700132195 организации", "1027700132195");
|
||||||
|
assertHidden("ОГРНИП 304500116000157 предпринимателя", "304500116000157");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ОГРНИП (15 цифр) не должен наполовину ловиться правилом ОГРН (13 цифр). */
|
||||||
|
@Test
|
||||||
|
void ogrnDoesNotSwallowOgrnip() {
|
||||||
|
String masked = pipeline.process("ОГРНИП 304500116000157", "ogrnip-1", SystemPolicy.DEFAULT);
|
||||||
|
assertFalse(masked.contains("304500116000157"), masked);
|
||||||
|
assertFalse(masked.matches(".*\\d{15}.*"), "осталась незамаскированная часть номера: " + masked);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void masksKpp() {
|
||||||
|
assertHidden("КПП 770101001 указан в реквизитах", "770101001");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void masksIncomeAmount() {
|
||||||
|
assertHidden("Доход клиента 85 000 руб. в месяц", "85 000");
|
||||||
|
assertHidden("Заработная плата 120000 в месяц", "120000");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void masksBiometricMention() {
|
||||||
|
assertHidden("Клиент сдал биометрические данные в отделении", "биометрические данные");
|
||||||
|
assertHidden("Оформлен слепок голоса для верификации", "слепок голоса");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user