feat: validate OGRN/OGRNIP checksum instead of trusting anchor + digit count
Unlike INN, OGRN and OGRNIP had no checksum check — any 13/15-digit number next to the word was accepted. Both have a public mod-11/mod-13 algorithm (first 12/14 digits, remainder mod divisor mod 10 = the last digit), same shape as the existing INN/SNILS checks. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
c1a1319088
commit
c25ab9c99f
@@ -210,13 +210,16 @@ public class RuleRegistry {
|
|||||||
.anchoredBy("срок действия", "действительн", "exp"),
|
.anchoredBy("срок действия", "действительн", "exp"),
|
||||||
|
|
||||||
// ОГРНИП раньше ОГРН: без отрицательного просмотра «ОГРНИП» частично ловился бы
|
// ОГРНИП раньше ОГРН: без отрицательного просмотра «ОГРНИП» частично ловился бы
|
||||||
// ещё и правилом ОГРН.
|
// ещё и правилом ОГРН. Контрольная сумма отсекает случайные 13/15-значные
|
||||||
|
// числа рядом со словом — раньше якоря было достаточно самого по себе.
|
||||||
Rule.of(OGRNIP, "(?iu:огрнип)\\W{0,5}(\\d{15})\\b", 83)
|
Rule.of(OGRNIP, "(?iu:огрнип)\\W{0,5}(\\d{15})\\b", 83)
|
||||||
.groups(1)
|
.groups(1)
|
||||||
|
.validatedBy(Validators::ogrnip)
|
||||||
.anchoredBy("огрнип"),
|
.anchoredBy("огрнип"),
|
||||||
|
|
||||||
Rule.of(OGRN, "(?iu:огрн(?!ип))\\W{0,5}(\\d{13})\\b", 83)
|
Rule.of(OGRN, "(?iu:огрн(?!ип))\\W{0,5}(\\d{13})\\b", 83)
|
||||||
.groups(1)
|
.groups(1)
|
||||||
|
.validatedBy(Validators::ogrn)
|
||||||
.anchoredBy("огрн"),
|
.anchoredBy("огрн"),
|
||||||
|
|
||||||
Rule.of(KPP, "(?iu:кпп)\\W{0,5}(\\d{9})\\b", 83)
|
Rule.of(KPP, "(?iu:кпп)\\W{0,5}(\\d{9})\\b", 83)
|
||||||
|
|||||||
@@ -63,6 +63,32 @@ public final class Validators {
|
|||||||
return control == d[9] * 10 + d[10];
|
return control == d[9] * 10 + d[10];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Контрольная сумма ОГРН: первые 12 цифр по модулю 11, младший разряд — 13-я цифра. */
|
||||||
|
public static boolean ogrn(String value) {
|
||||||
|
int[] d = digits(value);
|
||||||
|
return d.length == 13 && d[12] == modReduce(d, 12, 11);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Контрольная сумма ОГРНИП: первые 14 цифр по модулю 13, младший разряд — 15-я цифра. */
|
||||||
|
public static boolean ogrnip(String value) {
|
||||||
|
int[] d = digits(value);
|
||||||
|
return d.length == 15 && d[14] == modReduce(d, 14, 13);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Остаток от деления первых {@code count} цифр как одного числа на {@code divisor},
|
||||||
|
* взятый по младшему разряду. Числовое накопление по цифрам, а не парсинг строки
|
||||||
|
* в {@code long}: у ОГРНИП 14 цифр — на грани переполнения {@code int}, и это тот же
|
||||||
|
* приём, что уже применяется к самой длинной последовательности в {@link #luhn}.
|
||||||
|
*/
|
||||||
|
private static int modReduce(int[] d, int count, int divisor) {
|
||||||
|
long remainder = 0;
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
remainder = (remainder * 10 + d[i]) % divisor;
|
||||||
|
}
|
||||||
|
return (int) (remainder % 10);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Дата в числовой записи при любом порядке частей: {@code 12.05.1985},
|
* Дата в числовой записи при любом порядке частей: {@code 12.05.1985},
|
||||||
* {@code 05/12/1985}, {@code 1985-05-12}. Отсекает похожие по форме
|
* {@code 05/12/1985}, {@code 1985-05-12}. Отсекает похожие по форме
|
||||||
|
|||||||
@@ -56,6 +56,26 @@ class BankTypesTest {
|
|||||||
assertFalse(masked.matches(".*\\d{15}.*"), "осталась незамаскированная часть номера: " + masked);
|
assertFalse(masked.matches(".*\\d{15}.*"), "осталась незамаскированная часть номера: " + masked);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Контрольная сумма отсекает случайное 13-значное число рядом со словом «ОГРН».
|
||||||
|
* Число подобрано так, чтобы не проходить заодно и Луна — иначе оно всё равно
|
||||||
|
* маскировалось бы, но уже как номер карты, и тест ничего бы не показывал.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void doesNotMaskOgrnWithBrokenChecksum() {
|
||||||
|
String text = "ОГРН 1027700132190 организации";
|
||||||
|
assertEquals(text, pipeline.process(text, UUID.randomUUID().toString(), SystemPolicy.DEFAULT),
|
||||||
|
"число с неверной контрольной суммой не является настоящим ОГРН");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Та же проверка для ОГРНИП — случайное 15-значное число рядом со словом. */
|
||||||
|
@Test
|
||||||
|
void doesNotMaskOgrnipWithBrokenChecksum() {
|
||||||
|
String text = "ОГРНИП 304500116000150 предпринимателя";
|
||||||
|
assertEquals(text, pipeline.process(text, UUID.randomUUID().toString(), SystemPolicy.DEFAULT),
|
||||||
|
"число с неверной контрольной суммой не является настоящим ОГРНИП");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void masksKpp() {
|
void masksKpp() {
|
||||||
assertHidden("КПП 770101001 указан в реквизитах", "770101001");
|
assertHidden("КПП 770101001 указан в реквизитах", "770101001");
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package ru.pdguard.detect;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
/** Контрольные суммы ОГРН/ОГРНИП: первые 12/14 цифр по модулю 11/13, младший разряд остатка. */
|
||||||
|
class ValidatorsTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void validOgrnPassesChecksum() {
|
||||||
|
assertTrue(Validators.ogrn("1027700123450"));
|
||||||
|
assertTrue(Validators.ogrn("1025000678900"));
|
||||||
|
assertTrue(Validators.ogrn("1045002233440"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void invalidOgrnChecksumFails() {
|
||||||
|
assertFalse(Validators.ogrn("1027700123451"), "последняя цифра изменена — сумма не сходится");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void ogrnWrongLengthFails() {
|
||||||
|
assertFalse(Validators.ogrn("102770012345"), "12 цифр — не хватает контрольной");
|
||||||
|
assertFalse(Validators.ogrn("10277001234500"), "14 цифр — лишняя");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Остаток от деления на 11 может быть 10 — тогда контрольная цифра 0, не 10. */
|
||||||
|
@Test
|
||||||
|
void ogrnRemainderTenMapsToZero() {
|
||||||
|
assertTrue(Validators.ogrn("1000000000000"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void validOgrnipPassesChecksum() {
|
||||||
|
assertTrue(Validators.ogrnip("304500116000157"));
|
||||||
|
assertTrue(Validators.ogrnip("312500000000013"));
|
||||||
|
assertTrue(Validators.ogrnip("305500112233041"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void invalidOgrnipChecksumFails() {
|
||||||
|
assertFalse(Validators.ogrnip("304500116000158"), "последняя цифра изменена — сумма не сходится");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void ogrnipWrongLengthFails() {
|
||||||
|
assertFalse(Validators.ogrnip("30450011600015"), "14 цифр — не хватает контрольной");
|
||||||
|
assertFalse(Validators.ogrnip("30450011600015700"), "18 цифр — лишние");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Остаток от деления на 13 может быть 10 — тогда контрольная цифра 0, не 10. */
|
||||||
|
@Test
|
||||||
|
void ogrnipRemainderTenMapsToZero() {
|
||||||
|
assertTrue(Validators.ogrnip("100000000000000"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void nonDigitCharactersAreIgnored() {
|
||||||
|
assertTrue(Validators.ogrn("10-27700-123450"), "разделители в номере не мешают счёту цифр");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void emptyValueIsNotValid() {
|
||||||
|
assertFalse(Validators.ogrn(""));
|
||||||
|
assertFalse(Validators.ogrnip(""));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user