Обнаружение персональных данных в файлах и базах: скрипт поиска и проверка тестовых сред

В реестре у вас написано, что клиентские данные хранятся в базе CRM. А на деле они лежат ещё в «Выгрузка_для_отчёта_финал2.xlsx» на сетевой шаре, в дампе тестовой базы у разработчика и в файле, который кто-то отправил подрядчику три года назад. Эти копии не попадают ни в какие реестры, их не защищает никто, и именно оттуда данные чаще всего утекают. Единственный способ найти такие места — искать. Коммерческие решения класса DCAP делают это централизованно, но небольшая компания может начать с простого скрипта. Ниже как искать данные в файлах и базах, как проверять тестовые среды и разработку, как не получить тысячи ложных срабатываний и что делать с находками.
Куда записывать результаты: реестр защищаемой информации. Тестовые данные и разработка: разработка с ПДн. Если найденное нужно обезличить: обезличивание. Предотвращение утечек по каналам: DLP.
Как это устроено
Поиск не работает как единичное мероприятие: данные расползаются снова. Поэтому его запускают по расписанию (раз в месяц для файловых серверов, при каждом merge request для репозиториев) и каждый раз доводят до решения.
Что искать и как не получить тысячи ложных срабатываний
Регулярные выражения для номеров дают массу ложных срабатываний: любая десятизначная цифра выглядит как ИНН. Поэтому значения проверяют контрольными суммами: у СНИЛС, ИНН и банковских карт они есть, и случайное число проходит проверку лишь в одном случае из ста или десяти.
| Тип данных | Как находим | Надёжность |
|---|---|---|
| СНИЛС | 11 цифр, проверка контрольного числа | Высокая (1 случайное из 100) |
| ИНН | 10 или 12 цифр после слова «ИНН», проверка контрольных цифр | Высокая с контекстом; без контекста слишком шумно |
| Банковская карта | 13–19 цифр, алгоритм Луна, префиксы платёжных систем | Высокая |
| Телефон | +7 или 8 и десять цифр | Средняя: подходит как признак, не как доказательство |
| Паспорт РФ | Слово «паспорт» рядом с серией и номером | Средняя |
| Электронная почта | Шаблон адреса | Высокая, но сама по себе ничего не значит: нужна связка с ФИО |
Шаг 1. Скрипт поиска
Скрипт обходит каталог, читает текстовые файлы, таблицы .xlsx и документы .docx (они сами по себе архивы с XML), ищет значения, проверяет контрольные суммы и выводит только количество находок по типам, а не сами данные. Выводить найденные номера в отчёт нельзя: отчёт сам превращается в хранилище ПДн.
#!/usr/bin/env python3
"""Поиск персональных данных в файлах (без внешних библиотек).
Использование: pdn_scan.py КАТАЛОГ [КАТАЛОГ...] [--max-findings N]
Можно передать '-' для чтения со стандартного ввода (например, вывода pg_dump).
Выводит только количество находок по типам. Код выхода 1, если находок больше порога."""
import argparse
import os
import re
import sys
import zipfile
from collections import Counter
TEXT_EXT = {'.txt', '.csv', '.tsv', '.sql', '.json', '.xml', '.md', '.log', '.yml', '.yaml', '.ini', '.conf', '.html'}
ZIP_EXT = {'.xlsx', '.docx'}
MAX_SIZE = 100 * 1024 * 1024
RE_SNILS = re.compile(r'(?<!\d)(\d{3})[- ]?(\d{3})[- ]?(\d{3})[ -]?(\d{2})(?!\d)')
RE_INN = re.compile(r'(?i)инн\D{0,12}(\d{12}|\d{10})(?!\d)')
RE_CARD = re.compile(r'(?<!\d)(?:\d[ -]?){13,19}(?!\d)')
RE_PHONE = re.compile(r'(?<!\d)(?:\+7|8)[\s(-]*\d{3}[\s)-]*\d{3}[\s-]*\d{2}[\s-]*\d{2}(?!\d)')
RE_PASSPORT = re.compile(r'(?i)паспорт[^\n]{0,30}?(\d{2}\s?\d{2})\s?(\d{6})')
RE_EMAIL = re.compile(r'[\w.+-]+@[\w-]+\.[\w.-]+')
def snils_ok(s):
n, ctrl = s[:9], int(s[9:])
if int(n) <= 1001998:
return False
c = sum(int(d) * (9 - i) for i, d in enumerate(n)) % 101
return (0 if c == 100 else c) == ctrl
def inn_ok(s):
def cs(d, k):
return sum(int(a) * b for a, b in zip(d, k)) % 11 % 10
if len(s) == 10:
return cs(s[:9], [2, 4, 10, 3, 5, 9, 4, 6, 8]) == int(s[9])
return (cs(s[:10], [7, 2, 4, 10, 3, 5, 9, 4, 6, 8]) == int(s[10])
and cs(s[:11], [3, 7, 2, 4, 10, 3, 5, 9, 4, 6, 8]) == int(s[11]))
def luhn_ok(s):
total = 0
for i, ch in enumerate(reversed(s)):
d = int(ch)
if i % 2:
d = d * 2 - 9 if d * 2 > 9 else d * 2
total += d
return total % 10 == 0
def scan_text(text):
found = Counter()
for m in RE_SNILS.finditer(text):
if snils_ok(''.join(m.groups())):
found['СНИЛС'] += 1
for m in RE_INN.finditer(text):
if inn_ok(m.group(1)):
found['ИНН'] += 1
for m in RE_CARD.finditer(text):
digits = re.sub(r'\D', '', m.group(0))
if 13 <= len(digits) <= 19 and digits[0] in '2456' and luhn_ok(digits):
found['Банковская карта'] += 1
found['Телефон'] += len(RE_PHONE.findall(text))
found['Паспорт'] += len(RE_PASSPORT.findall(text))
found['Email'] += len(RE_EMAIL.findall(text))
return found
def read_file(path):
ext = os.path.splitext(path)[1].lower()
if os.path.getsize(path) > MAX_SIZE:
return ''
if ext in TEXT_EXT:
return open(path, encoding='utf-8', errors='ignore').read()
if ext in ZIP_EXT:
try:
with zipfile.ZipFile(path) as z:
parts = [z.read(n).decode('utf-8', 'ignore') for n in z.namelist() if n.endswith('.xml')]
return re.sub(r'<[^>]+>', ' ', ' '.join(parts))
except zipfile.BadZipFile:
return ''
return ''
def main():
ap = argparse.ArgumentParser()
ap.add_argument('paths', nargs='+')
ap.add_argument('--max-findings', type=int, default=0)
args = ap.parse_args()
total = Counter()
if args.paths == ['-']:
total = scan_text(sys.stdin.read())
print('stdin:', dict(total))
else:
for root_dir in args.paths:
for root, _, files in os.walk(root_dir):
for name in files:
p = os.path.join(root, name)
try:
c = scan_text(read_file(p))
except OSError:
continue
strong = sum(v for k, v in c.items() if k in ('СНИЛС', 'ИНН', 'Банковская карта', 'Паспорт'))
if strong or c['Телефон'] >= 5 or c['Email'] >= 5:
print(f'{p}: ' + ', '.join(f'{k} {v}' for k, v in c.items() if v))
total.update(c)
strong_total = sum(v for k, v in total.items() if k in ('СНИЛС', 'ИНН', 'Банковская карта', 'Паспорт'))
print(f'\nИтого значимых находок: {strong_total}', file=sys.stderr)
sys.exit(1 if strong_total > args.max_findings else 0)
if __name__ == '__main__':
main()
Ожидаемо: список файлов с количеством находок по типам и итоговая строка. Телефоны и почта сами по себе в код выхода не входят: они дают шум, а настоящий сигнал — документы с СНИЛС, ИНН, картами и паспортами. Работает на любой системе с Python 3, не требует установки пакетов. Не запускайте скрипт на копии, которую потом отправляете наружу, и сохраняйте отчёт там же, где лежат данные: он содержит пути к файлам с ПДн. Проверьте скрипт на своём тестовом файле с заведомо вымышленными значениями, прежде чем запускать на серверах. Файлы PDF и сканы скрипт не читает: для них нужен отдельный этап с извлечением текста (pdftotext) или распознаванием.
Шаг 2. Где запускать
| Область | Как | Как часто |
|---|---|---|
| Файловые серверы и сетевые шары | Запуск на самом сервере или на смонтированной копии в режиме только чтения: pdn_scan.py /srv/shares/ |
Раз в месяц |
| Рабочие станции критичных отделов | Через Ansible или групповую политику с отчётом на сервер: GPO и Ansible | Раз в квартал |
| Выгрузки и дампы баз | pg_dump --data-only тестовая_база | pdn_scan.py - |
При каждой выгрузке |
| Репозитории и тестовые наборы | В конвейере GitLab CI (ниже) | Каждый merge request |
| Почтовые архивы, облачные хранилища | Экспорт и проверка или встроенные инструменты сервиса | Раз в полгода |
Шаг 3. Базы данных
В базе данных сначала ищут по именам столбцов: часто этого достаточно, чтобы составить карту. Запрос для PostgreSQL:
SELECT table_schema, table_name, column_name, data_type
FROM information_schema.columns
WHERE column_name ~* '(inn|snils|passport|phone|email|birth|fio|surname|address|инн|снилс|паспорт|телефон|фамили)'
AND table_schema NOT IN ('pg_catalog', 'information_schema')
ORDER BY 1, 2, 3;
Ожидаемо: перечень столбцов, где вероятны ПДн. Затем выборочно проверяйте значения. Для тестовых и учебных баз проверка простая: значения должны быть заведомо вымышленными.
-- в тестовой базе не должно быть настоящих телефонов и почты клиентов
SELECT count(*) FROM customers WHERE phone !~ '^\+7000' ;
SELECT count(*) FROM customers WHERE email !~ '@example\.(com|org)$';
Ожидаемо: ноль в обоих запросах для корректно обезличенной базы. Подставьте свои шаблоны замены. Общие принципы подготовки тестовых данных: разработка с ПДн. Не забывайте журналы приложений: ПДн часто лежат в логах отладки, а не в базе.
Шаг 4. Автоматическая проверка в конвейере
Промышленные данные в средах разработки и тестирования — одна из самых частых причин утечек. Правило «не использовать» нужно подкрепить автоматикой: каждый merge request проверяется на наличие ПДн в тестовых наборах и дампах.
pdn-scan:
stage: test
image: python:3.12-slim
script:
- python3 tools/pdn_scan.py tests/ fixtures/ db/ --max-findings 0
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
Ожидаемо: задание зелёное при чистых тестовых данных и падает, если в каталогах найден СНИЛС, ИНН, номер карты или паспорт. Это частичное покрытие технологического стека: проверка видит файлы в репозитории, но не видит данные, которые разработчик загрузил в тестовую базу вручную. Для баз добавьте периодическое задание, выгружающее образцы и пропускающее их через тот же скрипт. Блокирующим сделайте только строгие типы с контрольными суммами, чтобы не заваливать разработчиков шумом: SAST и SCA как процесс.
Что делать с находками
| Находка | Решение |
|---|---|
| Копия данных, которая не нужна | Удалить с подтверждением владельца и записью в журнале: уничтожение ПДн |
| Данные нужны, лежат не там | Перенести в защищённую систему, ограничить права, внести в реестр |
| Данные нужны на месте | Закрыть доступ группой, включить аудит: мониторинг доступа, зашифровать диск |
| Реальные данные в тестовой среде | Заменить обезличенными, расследовать, как они там оказались, проверить доступ |
| Данные у подрядчика без основания | Запрос на удаление и подтверждение, проверка договора: поручение обработки |
Каждая находка получает владельца и срок. Список находок без сроков — это отчёт, а не процесс. Если среди найденных данных оказались сведения, утечка которых влечёт уведомление регулятора, оцените ситуацию: чек-лист готовности к утечке.
Когда нужен готовый продукт
Скрипт хорош, пока данных немного и они лежат в известных форматах. Если объём — терабайты, форматов десятки, а доступы нужно привязывать к конкретным людям, стоит смотреть на решения класса DCAP или функции облачных платформ: они читают больше форматов, ведут индекс, классифицируют по содержанию и интегрируются с каталогом. Прежде чем покупать, прогоните скрипт: он покажет, насколько проблема реальна.
Какие артефакты хранить
| Что хранить | Что показывает |
|---|---|
| Описание используемого решения (скрипт, версия, настройки, расписание) | Автоматизированный поиск применяется |
| Журналы запусков и отчёты о находках с решениями по каждой | Обнаружение даёт действия |
| Конфигурация проверки в конвейере и отчёты о результатах сканирования сред | Реальные данные не попадают в разработку и тестирование |
| Регламент классификации данных, на который ссылается процесс | Находки сопоставляются с категориями |
Диагностика
| Симптом | Причина и что делать |
|---|---|
| Сотни находок телефонов и почты | Шум. Смотрите только файлы со значимыми типами (СНИЛС, ИНН, карты, паспорта), остальное в реестре потоков |
| Скрипт не находит заведомо известные данные | Формат файла не читается (PDF, сканы, архивы). Добавьте извлечение текста или распакуйте архивы отдельно |
| Сканирование нагружает файловый сервер | Запускайте ночью, на копии или с ограничением по каталогам |
| Задание в конвейере падает на вымышленных данных | Тестовые значения случайно проходят контрольную сумму. Измените значения или добавьте исключение по пути с комментарием |
| Находки есть, решений нет | Нет владельца. Привяжите находку к владельцу набора данных из реестра и поставьте срок |
Где это в модели зрелости
Материал относится к направлению «Безопасность данных» (хаб: безопасность данных, карта: карта зрелости). На втором уровне применяются автоматизированные решения для обнаружения и предотвращения использования реальных данных в средах разработки и тестирования, с частичным покрытием технологического стека; на третьем для обнаружения и классификации данных «в покое» используются автоматизированные решения. Скрипт и проверка в конвейере закрывают оба пункта на уровне небольшой компании; для крупных объёмов нужен отдельный продукт.
Как проверить
- Скрипт на тестовом каталоге с вымышленными, но валидными СНИЛС, ИНН и номером карты находит все три значения и не выводит сами числа.
- Файловый сервер просканирован за последний месяц, все находки получили решения и сроки.
- Задание в конвейере падает на тестовом наборе с валидным СНИЛС и проходит на чистом.
- В тестовых базах запросы на реальные адреса и телефоны возвращают ноль.
- Результаты сохранены без самих персональных данных.
Источники
- CIS Control 3, защита данных: cisecurity.org/controls.
- NIST SP 800-122, защита конфиденциальности персональной информации: csrc.nist.gov. Не закон РФ.
- Microsoft Presidio, открытый инструмент поиска персональных данных: microsoft.github.io/presidio.


















