Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,70 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [2.5.7] - 2026-06

### Security
- Generated encryption passwords are printed to **stderr** (not stdout) with
a "shown once" warning — keeps them out of redirected output, pipes and
CI logs

## [2.5.6] - 2026-06

### Changed
- README updated for the package architecture (`masking/`, `unmasking/`,
`__main__.py`), initials masking documented
- README: new "Модель загроз та обмеження" section — honest statement that
this is pseudonymization (partial digit/letter preservation, deterministic
unsalted hashing, rank shift ±1-2, date shift ±30 days)
- Wrapper docstrings: corrected `python -m` mention (actual invocation is
`python . mask` / `python . unmask` from the repo root)

## [2.5.5] - 2026-06

### Changed
- `modules/rank_data.py` is now a re-export of the root `rank_data.py`
(was a full 636-line copy that could silently diverge)

## [2.5.4] - 2026-06

### Fixed
- `data_masking.MASK_*`, `DEBUG_MODE`, `PRESERVE_CASE`, `HASH_ALGORITHM` are
live again: reads and writes through the wrapper delegate to
`masking.constants` (after the v2.5.0 refactoring writes were silently
ignored — broken backward compatibility)

### Changed
- Removed unused imports in `masking/cli.py` (`SelectiveFilter`,
`apply_filter_to_globals`, `ReMasker`)

## [2.5.3] - 2026-06

### Fixed
- Repeated text dates (`06 жовтня 2025 року` twice in a document) now track
instances `[1, 2, ...]` — previously only the first occurrence was restored
by unmask
- Text date masking inside the engine is now deterministic (the internal copy
of the function never seeded the RNG)

### Changed
- Removed duplicated `_mask_date_text` implementation (~50 lines);
it is now an alias of `mask_date_text`

## [2.5.2] - 2026-06

### Fixed
- **Initials are now reversible**: masked initials (`Іванов П.А.` etc.) are stored
in the mapping under new `initials` category — unmask restores them
- Initials regexes no longer match across line breaks (`П.А.\nСлово` false positive)
- Main PIB parser no longer re-masks surnames already masked by the initials
phase (nested masks broke unmask)
- Initials mapping is written in document order — instance tracking stays
consistent with occurrence order

### Added
- `tests/test_initials.py` — 27 tests covering all formats and mask→unmask roundtrip
- Version asserts in tests are now dynamic (compare against `masking.constants`)

## [2.5.1] - 2026-04

### Fixed
Expand Down
47 changes: 41 additions & 6 deletions data_masking.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
ОНОВЛЕНО В v2.5.1:
- Рефакторинг: розбито на пакет masking/ (constants, helpers, language,
context, mask_personal, mask_military, engine, cli)
- Додано __main__.py для запуску через python -m
- Додано __main__.py: запуск з кореня репо — python . mask / python . unmask
- Зворотна сумісність: всі імпорти з data_masking продовжують працювати

Author: Vladyslav V. Prodan
Expand All @@ -23,14 +23,12 @@
# Re-exports from masking package for backward compatibility
# ============================================================================

__version__ = "2.5.1"
__version__ = "2.5.7"

from masking.constants import (
__version__, __author__, __contact__, __phone__, __license__, __year__,
fake_uk, HASH_ALGORITHM,
MASK_NAMES, MASK_IPN, MASK_PASSPORT, MASK_MILITARY_ID, MASK_RANKS,
MASK_BRIGADES, MASK_UNITS, MASK_ORDERS, MASK_BR_NUMBERS, MASK_DATES,
RANK_SHIFT_OPTIONS, DEBUG_MODE, PRESERVE_CASE,
fake_uk,
RANK_SHIFT_OPTIONS,
ABBREVIATION_WHITELIST, UKRAINIAN_DATE_PATTERN, DATE_TEXT_PATTERN,
GOOD_UKRAINIAN_NAMES_MALE, GOOD_UKRAINIAN_NAMES_FEMALE, PROBLEMATIC_NAMES,
EXCLUDE_WORDS,
Expand Down Expand Up @@ -96,5 +94,42 @@
main,
)

# ============================================================================
# "Живі" конфігураційні прапорці
# ============================================================================
# Рушій читає прапорці з masking.constants. Просте from-import дало б копії:
# старий код `data_masking.MASK_NAMES = False` мовчки не діяв би.
# Тому читання делегуємо через __getattr__ (PEP 562), а запис — через
# підміну класу модуля.

import sys as _sys
from types import ModuleType as _ModuleType

from masking import constants as _cfg

_LIVE_FLAGS = frozenset({
"MASK_NAMES", "MASK_IPN", "MASK_PASSPORT", "MASK_MILITARY_ID",
"MASK_RANKS", "MASK_BRIGADES", "MASK_UNITS", "MASK_ORDERS",
"MASK_BR_NUMBERS", "MASK_DATES",
"DEBUG_MODE", "PRESERVE_CASE", "HASH_ALGORITHM",
})


def __getattr__(name):
if name in _LIVE_FLAGS:
return getattr(_cfg, name)
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")


class _LiveFlagsModule(_ModuleType):
def __setattr__(self, name, value):
if name in _LIVE_FLAGS:
setattr(_cfg, name, value)
else:
super().__setattr__(name, value)


_sys.modules[__name__].__class__ = _LiveFlagsModule

if __name__ == "__main__":
main()
61 changes: 52 additions & 9 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,43 @@
# Data Masking & Unmasking Scripts v2.4.0
# Data Masking & Unmasking Scripts

Скрипти для **узгодженого маскування** конфіденційних даних у військових документах з можливістю точного відновлення.

Актуальна версія — див. [CHANGELOG.md](../CHANGELOG.md) або `python data_masking.py -V`.

---

## 📋 Структура проекту

### Основні скрипти
- **`data_masking.py`** — маскування даних з instance tracking
- **`unmask_data.py`** — розмаскування з точним відновленням
### Точки входу
- **`data_masking.py`** — маскування даних (тонка обгортка над пакетом `masking/`)
- **`unmask_data.py`** — розмаскування (тонка обгортка над пакетом `unmasking/`)
- **`diagnose_mapping.py`** — діагностика, порівняння маппінгів та верифікація відновлення
- **`__main__.py`** — запуск з кореня репозиторію: `python . mask [args]` / `python . unmask [args]`

### Пакет `masking/` (ядро маскування, v2.5.0+)
| Модуль | Опис |
|--------|------|
| `constants.py` | Прапорці `MASK_*`, патерни, словники імен, метадані (єдине джерело версії) |
| `helpers.py` | Seed, mapping, instance tracking, нормалізація |
| `language.py` | Рід, відмінок, відмінювання імен |
| `context.py` | Розпізнавання рядків з ПІБ, парсинг контексту |
| `mask_personal.py` | ІПН, паспорти, прізвища, імена, по батькові |
| `mask_military.py` | Звання, частини, накази, БР, дати |
| `engine.py` | Головний рушій: контекстне маскування тексту/JSON, ініціали |
| `cli.py` | CLI, конфіг, звіти, `main()` |

### Пакет `unmasking/` (ядро розмаскування, v2.5.0+)
| Модуль | Опис |
|--------|------|
| `helpers.py` | Instance map, пошук пар файлів, версії маппінгів |
| `engine.py` | Відновлення тексту/JSON, ланцюги re-mask |
| `io.py` | Завантаження mapping (.json/.enc), валідація схеми |
| `cli.py` | CLI, `main()` |

### Дані
- **`rank_data.py`** — звання ЗСУ (армія, флот, медична/юридична служба) з відмінками
- **`rank_data.py`** — звання ЗСУ (армія, флот, медична/юридична служба) з відмінками; `modules/rank_data.py` — ре-експорт

### Пакет `modules/`
### Пакет `modules/` (опційні можливості)
| Модуль | Опис |
|--------|------|
| `config.py` | YAML + ENV + CLI конфігурація з пріоритетами (CLI > ENV > YAML > Default) |
Expand Down Expand Up @@ -79,6 +102,7 @@ python diagnose_mapping.py --verify input.txt recovered.txt # верифіка

### Персональні дані
- **ПІБ** — з урахуванням відмінків та роду
- **ПІБ з ініціалами** (v2.5.0+) — `Іванов П.А.`, `П. Агранов`, `К.П. Іванов`, `Т. А. Сидоренко`, `КОВАЛЕНКО І.В.`; ініціали зберігаються у mapping і відновлюються при unmask
- **По батькові** — маскуються зі збереженням гендеру та регістру
- **ІПН** — 10 цифр
- **ID-паспорти** — 9 цифр (перші 3 + остання фіксовані, середні 5 — випадкові)
Expand Down Expand Up @@ -436,6 +460,27 @@ ID-паспорт 123947568, видано наказом №59/87/4249/Р від
4. Використовуйте різні mapping для різних документів
5. Використовуйте шифрування mapping файлів (v2.3.0+): `--encrypt --password`
6. Unmask можливий ТІЛЬКИ при наявності mapping файлу
7. Передавайте пароль через `--password-env ІМ'Я_ЗМІННОЇ`, а не `--password`
(аргументи командного рядка видно в історії shell та списку процесів)

### ⚠️ Модель загроз та обмеження

Це **псевдонімізація**, а не повна анонімізація. Враховуйте:

- **Часткове збереження оригіналу.** ІПН зберігає перші 3 та останню цифру
(4 з 10), паспорт — 4 з 9, довгі прізвища — перші 3 та останні 5 літер.
Це навмисно (зберігає формат і частковий контекст), але дозволяє
ре-ідентифікацію перебором за словником кандидатів.
- **Детермінована генерація без секрету.** Маски виводяться з blake2b-хешу
оригіналу без солі/ключа. Хто знає алгоритм і має список можливих
оригіналів, може зіставити маски без mapping-файлу.
- **Звання зсуваються лише на ±1–2 позиції** в ієрархії — приблизний ранг
особи залишається видимим.
- **Дати зсуваються на ±30 днів** — період подій залишається впізнаваним.

**Висновок:** замасковані файли захищають від випадкового розголошення та
підходять для передачі обмеженому колу, але НЕ розраховані на публікацію
проти мотивованого супротивника зі знанням контексту.

---

Expand Down Expand Up @@ -501,6 +546,4 @@ BSD 3-Clause "New" or "Revised" License

## 📅 Версія

**v2.4.0** (квітень 2026)

Детальну історію змін див. у [CHANGELOG.md](../CHANGELOG.md)
Актуальну версію та історію змін див. у [CHANGELOG.md](../CHANGELOG.md)
20 changes: 15 additions & 5 deletions masking/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,14 @@
_opt_logger = _logging.getLogger(__name__)

try:
from modules.selective import SelectiveFilter, apply_filter_to_globals, get_available_types
from modules.selective import get_available_types
SELECTIVE_AVAILABLE = True
except ImportError:
SELECTIVE_AVAILABLE = False
_opt_logger.debug("modules.selective not available — --only/--exclude disabled")

try:
from modules.re_mask import ReMasker, MappingChain, make_empty_masking_dict
from modules.re_mask import MappingChain, make_empty_masking_dict
REMASK_AVAILABLE = True
except ImportError:
REMASK_AVAILABLE = False
Expand Down Expand Up @@ -459,6 +459,15 @@ def _run_single_pass_masking(input_data, is_json: bool, masking_dict: Dict,
return masked_data, total_unique


def _print_generated_password(password: str) -> None:
"""Показує згенерований пароль у stderr (не stdout), щоб він не
потрапляв у перенаправлений вивід, пайпи та логи CI."""
import sys
print(" Generated password (shown once, NOT saved anywhere):",
file=sys.stderr)
print(f" {password}", file=sys.stderr)


def _handle_encryption(args, config, masking_dict: Dict, map_path: Path,
logger) -> None:
"""Encrypt the mapping file if --encrypt is requested."""
Expand All @@ -477,10 +486,10 @@ def _handle_encryption(args, config, masking_dict: Dict, map_path: Path,
if logger:
logger.warning(f"Environment variable '{password_env}' is not set or empty")
password = generate_password_from_config(config)
print(f" Generated password: {password}")
_print_generated_password(password)
else:
password = generate_password_from_config(config)
print(f" Generated password: {password}")
_print_generated_password(password)

manager = MappingSecurityManager()
manager.encrypt_mapping(masking_dict, password, enc_path)
Expand Down Expand Up @@ -689,7 +698,8 @@ def main():
"ipn", "passport_id", "military_id", "surname", "name",
"military_unit", "order_number", "order_number_with_letters",
"br_number", "br_number_slash", "br_number_complex",
"rank", "brigade_number", "date", "date_text", "patronymic"
"rank", "brigade_number", "date", "date_text", "patronymic",
"initials"
]},
"instance_tracking": {}
}
Expand Down
2 changes: 1 addition & 1 deletion masking/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
# ============================================================================
# МЕТАДАНІ
# ============================================================================
__version__ = "2.5.1"
__version__ = "2.5.7"
__author__ = "Vladyslav V. Prodan"
__contact__ = "github.com/click0"
__phone__ = "+38(099)6053340"
Expand Down
Loading
Loading