v0.0.56: КРИТИЧНО — mapping.csv (ключ расшифровки) убран из выходного ZIP, CSV только отдельно
Deploy drhider / validate (push) Canceled after 0s
Deploy drhider / validate (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
# 2026-08-20 — КРИТИЧНО: mapping.csv (ключ расшифровки) убран из выходного ZIP (v0.0.56)
|
||||
|
||||
## Проблема (утечка секретных данных)
|
||||
Выходной ZIP содержал `mapping.csv` — таблицу соответствия «оригинал → замена»,
|
||||
т.е. ключ расшифровки (реальные ФИО/телефоны/ИНН ↔ токены). Если ZIP передать
|
||||
третьему лицу — тот получал и обфусцированные файлы, и таблицу расшифровки.
|
||||
CSV должен храниться/скачиваться ОТДЕЛЬНО от обфусцированных файлов.
|
||||
|
||||
## Фикс (drhider/builder.py `build_zip`)
|
||||
- Убрана запись `mapping.csv` в архив. Теперь в ZIP — ТОЛЬКО обфусцированные файлы.
|
||||
- Параметр `mapping_csv` сохранён в сигнатуре (для совместимости вызовов), но в
|
||||
архив не пишется.
|
||||
- CSV продолжает генерироваться (`build_mapping_csv`) и отдаваться отдельно
|
||||
через `/api/csv/<sid>` (кнопка «Скачать CSV»), как написано на странице.
|
||||
|
||||
## Тесты (tests/test_builder.py)
|
||||
- Блок «ZIP с mapping.csv» переписан: теперь проверяет, что mapping.csv НЕ в архиве.
|
||||
- Интеграционный блок: ZIP содержит только doc.md, CSV проверяется отдельной строкой.
|
||||
- Все 106 тестов OK.
|
||||
|
||||
## Проверка
|
||||
- python3 tests/test_builder.py — 20/20 OK (включая новый сценарий «mapping не в zip»).
|
||||
- Остальные тесты — без изменений поведения, OK.
|
||||
- VERSION 0.0.56.
|
||||
+5
-11
@@ -20,15 +20,16 @@ from .config import ENTITY_PATTERNS, COMPANY_PATTERN
|
||||
def build_zip(files: List[Tuple[str, bytes]], mapping_csv: str = "") -> bytes:
|
||||
"""Собрать ZIP-архив с обфусцированными файлами.
|
||||
|
||||
В архив добавляются:
|
||||
- Все обфусцированные файлы (с оригинальными именами)
|
||||
- mapping.csv — таблица соответствия оригинал→замена (если не пустая)
|
||||
В архив добавляются ТОЛЬКО обфусцированные файлы (с оригинальными именами).
|
||||
mapping.csv в архив НЕ кладётся — это таблица соответствия оригинал→замена,
|
||||
т.е. ключ расшифровки (секретные данные). Он хранится/скачивается отдельно
|
||||
(/api/csv), чтобы нельзя было расшифровать архив, получив один ZIP.
|
||||
|
||||
Имена файлов в архиве — UTF-8 (бит 11 в flag_bits).
|
||||
|
||||
Args:
|
||||
files: [(filename, content_bytes), ...]
|
||||
mapping_csv: Строка CSV с таблицей замен (опционально)
|
||||
mapping_csv: Строка CSV с таблицей замен (опционально, НЕ добавляется в архив)
|
||||
|
||||
Returns:
|
||||
Бинарное содержимое ZIP-архива
|
||||
@@ -63,13 +64,6 @@ def build_zip(files: List[Tuple[str, bytes]], mapping_csv: str = "") -> bytes:
|
||||
info.flag_bits |= 0x800 # Флаг: имя файла в UTF-8
|
||||
zf.writestr(info, content)
|
||||
|
||||
# Добавляем mapping.csv с BOM (для корректного открытия в Excel)
|
||||
if mapping_csv:
|
||||
zf.writestr(
|
||||
"mapping.csv",
|
||||
'\ufeff'.encode('utf-8') + mapping_csv.encode('utf-8'),
|
||||
)
|
||||
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ if _sys_path_root not in sys.path:
|
||||
sys.path.insert(0, _sys_path_root)
|
||||
|
||||
# Версия приложения (меняется при изменениях)
|
||||
VERSION = "0.0.55"
|
||||
VERSION = "0.0.56"
|
||||
|
||||
|
||||
def setup_logging():
|
||||
|
||||
@@ -50,17 +50,15 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
||||
check("mapping.csv нет", "mapping.csv" not in names)
|
||||
check("договор читается", "Договор" in zf.read("договор_obfuscated.md").decode("utf-8"))
|
||||
|
||||
# 3. ZIP с mapping.csv
|
||||
# 3. ZIP с mapping.csv — mapping НЕ должен попадать в архив (секрет, хранится отдельно)
|
||||
zip_data = build_zip(
|
||||
[("file.md", b"data")],
|
||||
mapping_csv="тип,оригинал,замена\nperson,Иванов,Иванов_0001\n"
|
||||
)
|
||||
with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
||||
names = zf.namelist()
|
||||
check("mapping.csv есть", "mapping.csv" in names)
|
||||
csv_content = zf.read("mapping.csv").decode("utf-8")
|
||||
check("mapping с BOM", csv_content.startswith('\ufeff'))
|
||||
check("данные в mapping", "Иванов_0001" in csv_content)
|
||||
check("mapping.csv НЕ в архиве", "mapping.csv" not in names)
|
||||
check("только file.md", len(names) == 1)
|
||||
|
||||
# 4. ZIP с кириллицей в именах
|
||||
zip_data = build_zip([("договор_№123.md", "текст".encode("utf-8"))])
|
||||
@@ -116,11 +114,11 @@ csv = build_mapping_csv(mapping)
|
||||
zip_data = build_zip([("doc.md", "# Договор с Иванов_0001".encode("utf-8"))], mapping_csv=csv)
|
||||
|
||||
with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
||||
check("два файла в ZIP", len(zf.namelist()) == 2)
|
||||
check("только doc.md в ZIP (без mapping)", len(zf.namelist()) == 1)
|
||||
doc = zf.read("doc.md").decode("utf-8")
|
||||
check("doc с заменой", "Иванов_0001" in doc)
|
||||
csv_content = zf.read("mapping.csv").decode("utf-8")
|
||||
check("CSV с обеими заменами", "Иванов_0001" in csv_content and "ООО_Технология_0001" in csv_content)
|
||||
check("mapping.csv не в архиве", "mapping.csv" not in zf.namelist())
|
||||
check("CSV отдаётся отдельной строкой", "Иванов_0001" in csv and "ООО_Технология_0001" in csv)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user