diff --git a/backend/app/services/llm/redaction.py b/backend/app/services/llm/redaction.py index 6d3c7ccd..e2ffcef0 100644 --- a/backend/app/services/llm/redaction.py +++ b/backend/app/services/llm/redaction.py @@ -72,22 +72,42 @@ _SNILS_RE = re.compile(r"\b\d{3}-\d{3}-\d{3}[\s-]\d{2}\b") _PASSPORT_RE = re.compile(r"\b\d{4}[\s-]\d{6}\b") # Телефон РФ: +7/8, опц. скобки/пробелы/дефисы, 10 значащих цифр. _PHONE_RE = re.compile(r"(?:\+7|\b8)[\s\-(]*\d{3}[\s\-)]*\d{3}[\s-]*\d{2}[\s-]*\d{2}\b") +# Телефон РФ «голый»: 11 цифр без разделителей, начинаются с 7 или 8 (#1207). +# Покрывает copy-paste форму «79221234567» / «89221234567», которую _PHONE_RE +# пропускает из-за требования префикса «+7»/«\b8» + хотя бы одного разделителя. +# Ставится РАНЬШЕ _SNILS_BARE_RE (любые 11 цифр), чтобы не путать с СНИЛС. +_PHONE_BARE_RE = re.compile(r"(? None: assert "Иванович" not in out +# ── #1207: regex-дыры в первичном scrub'е (P2-security) ───────────────────────── + + +def test_scrub_phone_bare_11_digits_starting_with_7() -> None: + """Copy-paste форма «79221234567» (11 цифр, без + и без 8) — самая частая дыра. + + _PHONE_RE требует «+7»/«\\b8» + разделитель → этот вариант пропускался и уходил + в OpenAI как есть. _PHONE_BARE_RE закрывает дыру. + """ + out = scrub_text("позвоните 79221234567 договоримся") + assert "[REDACTED:phone]" in out + assert "79221234567" not in out + + +def test_scrub_phone_bare_11_digits_starting_with_8() -> None: + """Аналогично — «89221234567» без разделителей.""" + out = scrub_text("моб 89221234567 в любое время") + assert "[REDACTED:phone]" in out + assert "89221234567" not in out + + +def test_scrub_fullname_caps_format() -> None: + """Стандарт ЕГРН/паспортных выписок «ИВАНОВ ИВАН ИВАНОВИЧ» — старый regex требовал + строчные после первой буквы → каполная форма уходила как есть.""" + out = scrub_text("собственник ИВАНОВ ИВАН ИВАНОВИЧ зарегистрирован") + assert "[REDACTED:name]" in out + assert "ИВАНОВИЧ" not in out + + +def test_scrub_fullname_mixed_caps_and_titlecase() -> None: + """Смешанная форма «ИВАНОВ Иван Иванович» — тоже должна срабатывать.""" + out = scrub_text("по доверенности ИВАНОВ Иван Иванович подписал") + assert "[REDACTED:name]" in out + assert "ИВАНОВ" not in out + + +def test_scrub_snils_bare_11_digits() -> None: + """СНИЛС без разделителей «12345678901» — не покрывался ни phone (нет 7/8 префикса), + ни ИНН (длина не 10/12).""" + out = scrub_text("СНИЛС 12345678901 в личном деле") + assert "[REDACTED:snils]" in out + assert "12345678901" not in out + + +def test_phone_bare_wins_over_snils_bare_for_7_prefix() -> None: + """Конфликт _PHONE_BARE_RE vs _SNILS_BARE_RE: 11 цифр, начинаются с 7 → phone.""" + out = scrub_text("79221234567") + assert "[REDACTED:phone]" in out + assert "[REDACTED:snils]" not in out + + +def test_inn_10_digits_not_caught_as_snils_bare() -> None: + """10-значный ИНН не должен попадать в _SNILS_BARE_RE (тот требует ровно 11).""" + out = scrub_text("ИНН 7707083893 действующий") + assert "[REDACTED:inn]" in out + assert "[REDACTED:snils]" not in out + + def test_scrub_multiple_pii_in_one_text() -> None: out = scrub_text("Иванов Иван Иванович, +7 912 345 67 89, mail@x.ru") assert "[REDACTED:name]" in out