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:
|
def build_zip(files: List[Tuple[str, bytes]], mapping_csv: str = "") -> bytes:
|
||||||
"""Собрать ZIP-архив с обфусцированными файлами.
|
"""Собрать ZIP-архив с обфусцированными файлами.
|
||||||
|
|
||||||
В архив добавляются:
|
В архив добавляются ТОЛЬКО обфусцированные файлы (с оригинальными именами).
|
||||||
- Все обфусцированные файлы (с оригинальными именами)
|
mapping.csv в архив НЕ кладётся — это таблица соответствия оригинал→замена,
|
||||||
- mapping.csv — таблица соответствия оригинал→замена (если не пустая)
|
т.е. ключ расшифровки (секретные данные). Он хранится/скачивается отдельно
|
||||||
|
(/api/csv), чтобы нельзя было расшифровать архив, получив один ZIP.
|
||||||
|
|
||||||
Имена файлов в архиве — UTF-8 (бит 11 в flag_bits).
|
Имена файлов в архиве — UTF-8 (бит 11 в flag_bits).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
files: [(filename, content_bytes), ...]
|
files: [(filename, content_bytes), ...]
|
||||||
mapping_csv: Строка CSV с таблицей замен (опционально)
|
mapping_csv: Строка CSV с таблицей замен (опционально, НЕ добавляется в архив)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Бинарное содержимое ZIP-архива
|
Бинарное содержимое ZIP-архива
|
||||||
@@ -63,13 +64,6 @@ def build_zip(files: List[Tuple[str, bytes]], mapping_csv: str = "") -> bytes:
|
|||||||
info.flag_bits |= 0x800 # Флаг: имя файла в UTF-8
|
info.flag_bits |= 0x800 # Флаг: имя файла в UTF-8
|
||||||
zf.writestr(info, content)
|
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()
|
return buf.getvalue()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@ if _sys_path_root not in sys.path:
|
|||||||
sys.path.insert(0, _sys_path_root)
|
sys.path.insert(0, _sys_path_root)
|
||||||
|
|
||||||
# Версия приложения (меняется при изменениях)
|
# Версия приложения (меняется при изменениях)
|
||||||
VERSION = "0.0.55"
|
VERSION = "0.0.56"
|
||||||
|
|
||||||
|
|
||||||
def setup_logging():
|
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("mapping.csv нет", "mapping.csv" not in names)
|
||||||
check("договор читается", "Договор" in zf.read("договор_obfuscated.md").decode("utf-8"))
|
check("договор читается", "Договор" in zf.read("договор_obfuscated.md").decode("utf-8"))
|
||||||
|
|
||||||
# 3. ZIP с mapping.csv
|
# 3. ZIP с mapping.csv — mapping НЕ должен попадать в архив (секрет, хранится отдельно)
|
||||||
zip_data = build_zip(
|
zip_data = build_zip(
|
||||||
[("file.md", b"data")],
|
[("file.md", b"data")],
|
||||||
mapping_csv="тип,оригинал,замена\nperson,Иванов,Иванов_0001\n"
|
mapping_csv="тип,оригинал,замена\nperson,Иванов,Иванов_0001\n"
|
||||||
)
|
)
|
||||||
with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
||||||
names = zf.namelist()
|
names = zf.namelist()
|
||||||
check("mapping.csv есть", "mapping.csv" in names)
|
check("mapping.csv НЕ в архиве", "mapping.csv" not in names)
|
||||||
csv_content = zf.read("mapping.csv").decode("utf-8")
|
check("только file.md", len(names) == 1)
|
||||||
check("mapping с BOM", csv_content.startswith('\ufeff'))
|
|
||||||
check("данные в mapping", "Иванов_0001" in csv_content)
|
|
||||||
|
|
||||||
# 4. ZIP с кириллицей в именах
|
# 4. ZIP с кириллицей в именах
|
||||||
zip_data = build_zip([("договор_№123.md", "текст".encode("utf-8"))])
|
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)
|
zip_data = build_zip([("doc.md", "# Договор с Иванов_0001".encode("utf-8"))], mapping_csv=csv)
|
||||||
|
|
||||||
with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
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")
|
doc = zf.read("doc.md").decode("utf-8")
|
||||||
check("doc с заменой", "Иванов_0001" in doc)
|
check("doc с заменой", "Иванов_0001" in doc)
|
||||||
csv_content = zf.read("mapping.csv").decode("utf-8")
|
check("mapping.csv не в архиве", "mapping.csv" not in zf.namelist())
|
||||||
check("CSV с обеими заменами", "Иванов_0001" in csv_content and "ООО_Технология_0001" in csv_content)
|
check("CSV отдаётся отдельной строкой", "Иванов_0001" in csv and "ООО_Технология_0001" in csv)
|
||||||
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════
|
||||||
|
|||||||
Reference in New Issue
Block a user