masker-db 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- masker_db-0.1.0/.gitignore +50 -0
- masker_db-0.1.0/CHANGELOG.md +67 -0
- masker_db-0.1.0/CONTRIBUTING.md +189 -0
- masker_db-0.1.0/LICENSE +21 -0
- masker_db-0.1.0/PKG-INFO +483 -0
- masker_db-0.1.0/README.md +436 -0
- masker_db-0.1.0/SECURITY.md +78 -0
- masker_db-0.1.0/examples/demo.py +130 -0
- masker_db-0.1.0/examples/demo.sh +161 -0
- masker_db-0.1.0/examples/make_demo_db.py +255 -0
- masker_db-0.1.0/examples/masker.yaml +100 -0
- masker_db-0.1.0/examples/schema.sql +88 -0
- masker_db-0.1.0/pyproject.toml +164 -0
- masker_db-0.1.0/scripts/bench_sqlite.py +191 -0
- masker_db-0.1.0/src/masker/__init__.py +11 -0
- masker_db-0.1.0/src/masker/__main__.py +8 -0
- masker_db-0.1.0/src/masker/cli.py +695 -0
- masker_db-0.1.0/src/masker/config.py +705 -0
- masker_db-0.1.0/src/masker/connect.py +320 -0
- masker_db-0.1.0/src/masker/console.py +58 -0
- masker_db-0.1.0/src/masker/engine.py +679 -0
- masker_db-0.1.0/src/masker/errors.py +104 -0
- masker_db-0.1.0/src/masker/init.py +148 -0
- masker_db-0.1.0/src/masker/keys.py +188 -0
- masker_db-0.1.0/src/masker/py.typed +0 -0
- masker_db-0.1.0/src/masker/report.py +171 -0
- masker_db-0.1.0/src/masker/scan.py +528 -0
- masker_db-0.1.0/src/masker/schema.py +425 -0
- masker_db-0.1.0/src/masker/transformers/__init__.py +47 -0
- masker_db-0.1.0/src/masker/transformers/base.py +286 -0
- masker_db-0.1.0/src/masker/transformers/constant.py +33 -0
- masker_db-0.1.0/src/masker/transformers/date_shift.py +126 -0
- masker_db-0.1.0/src/masker/transformers/email.py +125 -0
- masker_db-0.1.0/src/masker/transformers/fake.py +81 -0
- masker_db-0.1.0/src/masker/transformers/hash.py +89 -0
- masker_db-0.1.0/src/masker/transformers/keep.py +35 -0
- masker_db-0.1.0/src/masker/transformers/null.py +34 -0
- masker_db-0.1.0/src/masker/transformers/number_noise.py +101 -0
- masker_db-0.1.0/src/masker/transformers/partial_mask.py +56 -0
- masker_db-0.1.0/src/masker/transformers/preserve_format.py +56 -0
- masker_db-0.1.0/src/masker/transformers/redact.py +33 -0
- masker_db-0.1.0/src/masker/transformers/regex_replace.py +65 -0
- masker_db-0.1.0/src/masker/transformers/registry.py +20 -0
- masker_db-0.1.0/src/masker/transformers/shuffle.py +105 -0
- masker_db-0.1.0/src/masker/values.py +215 -0
- masker_db-0.1.0/src/masker/verify.py +445 -0
- masker_db-0.1.0/tests/conftest.py +265 -0
- masker_db-0.1.0/tests/test_cli.py +827 -0
- masker_db-0.1.0/tests/test_config.py +364 -0
- masker_db-0.1.0/tests/test_connect.py +229 -0
- masker_db-0.1.0/tests/test_engine.py +577 -0
- masker_db-0.1.0/tests/test_integration_mysql.py +188 -0
- masker_db-0.1.0/tests/test_integration_postgres.py +204 -0
- masker_db-0.1.0/tests/test_integration_sqlite.py +340 -0
- masker_db-0.1.0/tests/test_keys.py +134 -0
- masker_db-0.1.0/tests/test_perf.py +132 -0
- masker_db-0.1.0/tests/test_regressions.py +183 -0
- masker_db-0.1.0/tests/test_report.py +178 -0
- masker_db-0.1.0/tests/test_scan.py +294 -0
- masker_db-0.1.0/tests/test_schema.py +183 -0
- masker_db-0.1.0/tests/test_transformers.py +675 -0
- masker_db-0.1.0/tests/test_values.py +210 -0
- masker_db-0.1.0/tests/test_verify.py +291 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.egg-info/
|
|
6
|
+
.eggs/
|
|
7
|
+
build/
|
|
8
|
+
dist/
|
|
9
|
+
wheels/
|
|
10
|
+
pip-wheel-metadata/
|
|
11
|
+
|
|
12
|
+
# Virtual environments
|
|
13
|
+
.venv/
|
|
14
|
+
venv/
|
|
15
|
+
env/
|
|
16
|
+
|
|
17
|
+
# Tooling caches
|
|
18
|
+
.cache/
|
|
19
|
+
.pytest_cache/
|
|
20
|
+
.mypy_cache/
|
|
21
|
+
.ruff_cache/
|
|
22
|
+
.coverage
|
|
23
|
+
.coverage.*
|
|
24
|
+
coverage.xml
|
|
25
|
+
htmlcov/
|
|
26
|
+
.tox/
|
|
27
|
+
.nox/
|
|
28
|
+
|
|
29
|
+
# Editors / OS
|
|
30
|
+
.idea/
|
|
31
|
+
.vscode/
|
|
32
|
+
*.swp
|
|
33
|
+
.DS_Store
|
|
34
|
+
|
|
35
|
+
# Secrets and local databases -- never commit these
|
|
36
|
+
*.key
|
|
37
|
+
*.pem
|
|
38
|
+
.env
|
|
39
|
+
.env.*
|
|
40
|
+
*.sqlite
|
|
41
|
+
*.sqlite3
|
|
42
|
+
*.db
|
|
43
|
+
|
|
44
|
+
# Masker run artifacts
|
|
45
|
+
.masker/
|
|
46
|
+
|
|
47
|
+
/build-prompt.txt
|
|
48
|
+
/after-build-report.txt
|
|
49
|
+
/PUBLISHING.md
|
|
50
|
+
/VERIFICATION.md
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project
|
|
5
|
+
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [0.1.0] - Unreleased
|
|
8
|
+
|
|
9
|
+
First public release.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **CLI** (`masker`) with the commands `init`, `scan`, `validate`, `run`,
|
|
14
|
+
`verify`, `report` and `transformers`, plus the global flags `--verbose`,
|
|
15
|
+
`--no-color`, `--key-file`, `--ephemeral-key` and `--version`.
|
|
16
|
+
- **Rules file** (`masker.yaml`) with per-table actions (`copy`, `skip`,
|
|
17
|
+
`truncate`, `subset`), per-column transformer rules, and a `settings` block
|
|
18
|
+
for batch size, NULL handling, schema creation, row caps, Faker locale,
|
|
19
|
+
uniqueness retries and sample sizes. Unknown keys are rejected rather than
|
|
20
|
+
ignored.
|
|
21
|
+
- **Thirteen transformers**: `fake`, `hash`, `partial_mask`, `redact`, `null`,
|
|
22
|
+
`constant`, `shuffle`, `regex_replace`, `preserve_format`, `date_shift`,
|
|
23
|
+
`number_noise`, `email` and `keep`, all registered by name and all
|
|
24
|
+
deterministic under a shared key.
|
|
25
|
+
- **Determinism** from `HMAC-SHA256(secret_key, column_scope ‖ value)`, with
|
|
26
|
+
per-column namespacing that is shared across both sides of a foreign key so
|
|
27
|
+
joins survive masking.
|
|
28
|
+
- **Key management**: key from `MASKER_SECRET_KEY` or `--key-file`, never from
|
|
29
|
+
the rules file; refusing to run without one; refusing keys shorter than 12
|
|
30
|
+
characters; warning below 32; `--ephemeral-key` for throwaway output.
|
|
31
|
+
- **Uniqueness handling** with counter-salted retries and a clear error when the
|
|
32
|
+
value space is too small.
|
|
33
|
+
- **Schema reflection** over SQLAlchemy Core for SQLite, PostgreSQL and
|
|
34
|
+
MySQL/MariaDB, including primary keys, unique constraints (including SQLite
|
|
35
|
+
inline `UNIQUE`, which is invisible to the usual inspector API), column
|
|
36
|
+
lengths and nullability.
|
|
37
|
+
- **Foreign-key-aware ordering** using union-find grouping, with cycle detection
|
|
38
|
+
and deferred constraint checks for tables that cannot be ordered.
|
|
39
|
+
- **`masker init`** generating a starter rules file covering every column, with
|
|
40
|
+
PII suggestions marked `# SUGGESTED, review me`.
|
|
41
|
+
- **`masker scan`** combining column-name patterns with value sniffing on a
|
|
42
|
+
sample of rows, listing columns with no rule, `--strict` as a CI gate, and
|
|
43
|
+
`--json` output.
|
|
44
|
+
- **`masker run`** with batched streaming (default 5000 rows), one transaction
|
|
45
|
+
per table, Rich progress, `--dry-run` with a partially hidden before/after
|
|
46
|
+
sample, `--batch-size`, `--tables`, `--exclude`, `--subset-rows`,
|
|
47
|
+
`--create-schema/--no-create-schema`, `--allow-non-empty` and `--yes`.
|
|
48
|
+
- **`masker verify`** sample-checking the target for surviving originals,
|
|
49
|
+
reporting columns with no rule that look like PII, counting orphaned foreign
|
|
50
|
+
keys, and distinguishing genuine leaks from the coincidences that
|
|
51
|
+
domain-preserving transformers (`date_shift`, `number_noise`) legitimately
|
|
52
|
+
produce.
|
|
53
|
+
- **`masker report`** rendering a run summary as Markdown or JSON, from the
|
|
54
|
+
stats file a run writes.
|
|
55
|
+
- **Safety guards**: read-only source connections on all three backends,
|
|
56
|
+
same-database refusal on normalized URLs, non-empty-target refusal, credential
|
|
57
|
+
redaction in every URL and message, no row values in logs, and a per-run list
|
|
58
|
+
of columns copied verbatim.
|
|
59
|
+
- **Test suite** covering every transformer, rules validation, PII heuristics,
|
|
60
|
+
FK ordering, the safety guards and leak detection, plus an end-to-end SQLite
|
|
61
|
+
integration test and opt-in PostgreSQL/MySQL integration tests.
|
|
62
|
+
- **Examples**: a sample schema, a sample `masker.yaml` and a 13-step SQLite
|
|
63
|
+
demo script (`examples/demo.sh`).
|
|
64
|
+
- **Benchmark**: `scripts/bench_sqlite.py`, the source of the numbers in the
|
|
65
|
+
README's Performance section.
|
|
66
|
+
|
|
67
|
+
[0.1.0]: https://github.com/zahidhasann88/masker/releases/tag/v0.1.0
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# Contributing to Masker
|
|
2
|
+
|
|
3
|
+
Thanks for considering it. Masker is a privacy tool, so the bar for correctness
|
|
4
|
+
is higher than usual: a transformer that is subtly wrong leaks data, and a
|
|
5
|
+
safety check that is subtly wrong is worse than no check at all. Please read
|
|
6
|
+
the notes below before opening a pull request.
|
|
7
|
+
|
|
8
|
+
## Getting set up
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
git clone https://github.com/zahidhasann88/masker && cd masker
|
|
12
|
+
python -m venv .venv && source .venv/bin/activate
|
|
13
|
+
pip install -e '.[dev,postgres,mysql]'
|
|
14
|
+
pre-commit install
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`make check` runs everything CI runs:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
make check # ruff + mypy (strict) + pytest with coverage
|
|
21
|
+
make lint # ruff check
|
|
22
|
+
make format # ruff format
|
|
23
|
+
make typecheck # mypy --strict on src/
|
|
24
|
+
make test # pytest
|
|
25
|
+
make demo # end-to-end SQLite demo (examples/demo.sh)
|
|
26
|
+
make bench # 100k-row benchmark
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Running the tests
|
|
30
|
+
|
|
31
|
+
The default suite runs everywhere — it needs no services, because the
|
|
32
|
+
integration tests use SQLite:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pytest
|
|
36
|
+
pytest --cov=masker --cov-report=term-missing
|
|
37
|
+
MASKER_RUN_PERF=1 pytest -m perf # opt-in performance test
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The PostgreSQL and MySQL suites are skipped unless you point them at a
|
|
41
|
+
disposable database. **They drop and recreate databases**, so never point them
|
|
42
|
+
at anything you care about.
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
export MASKER_TEST_POSTGRES_URL="postgresql+psycopg://postgres@localhost:5432/masker_test"
|
|
46
|
+
export MASKER_TEST_MYSQL_URL="mysql+pymysql://root@127.0.0.1:3306/masker_test"
|
|
47
|
+
pytest -m "postgres or mysql"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Both are exercised in CI against service containers.
|
|
51
|
+
|
|
52
|
+
## Code style
|
|
53
|
+
|
|
54
|
+
- Python 3.10+ syntax, `from __future__ import annotations` everywhere.
|
|
55
|
+
- `src/` layout; the package is `masker`.
|
|
56
|
+
- ruff (line length 100) and mypy in `strict` mode on `src/` must pass. The
|
|
57
|
+
config lives in `pyproject.toml`; prefer fixing the code over adding an
|
|
58
|
+
ignore, and if an ignore is genuinely needed, say why in a comment.
|
|
59
|
+
- Tests go in `tests/`, one module per source module where that mapping makes
|
|
60
|
+
sense. Coverage floor is 85%.
|
|
61
|
+
|
|
62
|
+
## Commit messages and pull requests
|
|
63
|
+
|
|
64
|
+
This project uses [Conventional Commits](https://www.conventionalcommits.org/):
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
feat: add a `tokenize` transformer for FPE-style masking
|
|
68
|
+
fix: stop `verify` flagging shifted dates as leaks
|
|
69
|
+
docs: document the cyclic foreign-key limitation
|
|
70
|
+
test: cover the unique-retry path for regex_replace
|
|
71
|
+
chore: bump ruff to 0.6
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Keep one logical change per commit. In the PR description, say what the change
|
|
75
|
+
does, why, and — for anything touching masking or the safety guards — how you
|
|
76
|
+
verified it.
|
|
77
|
+
|
|
78
|
+
## How to add a transformer
|
|
79
|
+
|
|
80
|
+
A transformer is one module in `src/masker/transformers/`, registered by name.
|
|
81
|
+
The interface is deliberately small.
|
|
82
|
+
|
|
83
|
+
**1. Create the module.** For a transformer called `tokenize`:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
# src/masker/transformers/tokenize.py
|
|
87
|
+
"""``tokenize`` -- one-line description, plus a YAML example.
|
|
88
|
+
|
|
89
|
+
```yaml
|
|
90
|
+
columns:
|
|
91
|
+
ssn: {transformer: tokenize, params: {prefix: "TKN"}}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Explain what it guarantees and what it does not. This docstring is the
|
|
95
|
+
documentation, so write it for someone deciding whether to trust it with a
|
|
96
|
+
column of personal data.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
from __future__ import annotations
|
|
100
|
+
|
|
101
|
+
from typing import Any
|
|
102
|
+
|
|
103
|
+
from masker.transformers.base import BaseParams, BaseTransformer
|
|
104
|
+
from masker.transformers.registry import register
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class TokenizeParams(BaseParams):
|
|
108
|
+
"""Validated by pydantic; add constraints here rather than in `_apply`."""
|
|
109
|
+
|
|
110
|
+
prefix: str = ""
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@register
|
|
114
|
+
class TokenizeTransformer(BaseTransformer[TokenizeParams]):
|
|
115
|
+
name = "tokenize"
|
|
116
|
+
summary = "One-line summary shown by `masker transformers`."
|
|
117
|
+
params_model = TokenizeParams
|
|
118
|
+
|
|
119
|
+
# Declare the properties Masker needs to know about. Defaults are in
|
|
120
|
+
# BaseTransformer; only override what differs.
|
|
121
|
+
supports_uniqueness = True # can produce distinct outputs per input?
|
|
122
|
+
tracks_uniqueness = True # should Masker enforce a UNIQUE constraint?
|
|
123
|
+
value_scoped = True # does output depend on the input value?
|
|
124
|
+
requires_prepass = False # does it need the whole column up front?
|
|
125
|
+
preserves_domain = False # can output land on another row's original?
|
|
126
|
+
|
|
127
|
+
def _apply(self, value: Any, attempt: int) -> Any:
|
|
128
|
+
# `value` is never None here unless null_passthrough is disabled;
|
|
129
|
+
# use self.require_not_null(value) to get a clear error otherwise.
|
|
130
|
+
# `attempt` is 0 on the first try and increments on a uniqueness retry,
|
|
131
|
+
# so it must influence the output for retries to make progress.
|
|
132
|
+
# Derive randomness from self.ctx.rng(...) or self.ctx.digest(...) --
|
|
133
|
+
# never from the global random module, or determinism breaks.
|
|
134
|
+
token = self.ctx.hex_digest(value, attempt, length=8)
|
|
135
|
+
return f"{self.params.prefix}{token}"
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**2. Export it.** Add the module to `src/masker/transformers/__init__.py` so
|
|
139
|
+
the `@register` decorator runs at import time.
|
|
140
|
+
|
|
141
|
+
**3. Test it.** `tests/test_transformers.py` has the patterns. At minimum cover:
|
|
142
|
+
|
|
143
|
+
- determinism: the same key and value produce the same output, twice;
|
|
144
|
+
- a different key produces a different output;
|
|
145
|
+
- NULL handling, both with `null_passthrough` on and off;
|
|
146
|
+
- length and type fitting into a narrow column;
|
|
147
|
+
- uniqueness: two distinct inputs give distinct outputs, and the retry path
|
|
148
|
+
(`attempt > 0`) makes progress;
|
|
149
|
+
- anything specific to your guarantee (format preservation, bounds, locale).
|
|
150
|
+
|
|
151
|
+
**4. Document it.** Add a section to the transformer reference in `README.md`
|
|
152
|
+
with a YAML example, and a line to `CHANGELOG.md`.
|
|
153
|
+
|
|
154
|
+
**5. Consider `validate`.** If your transformer only makes sense for certain
|
|
155
|
+
column types — or can provably never satisfy a UNIQUE constraint — say so in
|
|
156
|
+
`masker/config.py` so the mistake is caught by `masker validate` instead of at
|
|
157
|
+
row 9,000,000.
|
|
158
|
+
|
|
159
|
+
### Rules for transformers
|
|
160
|
+
|
|
161
|
+
These are not style preferences; breaking them breaks guarantees users rely on.
|
|
162
|
+
|
|
163
|
+
1. **No unseeded randomness.** Everything random must come from
|
|
164
|
+
`self.ctx.rng(...)` or `self.ctx.digest(...)`, both derived from the key and
|
|
165
|
+
the value. Output must be reproducible.
|
|
166
|
+
2. **Never log or raise with the original value.** Errors name the column, not
|
|
167
|
+
the data. `MaskerError` messages are printed to the console.
|
|
168
|
+
3. **Output must fit the column.** Return something `fit_value` can coerce; if
|
|
169
|
+
your transformer produces strings, they will be truncated to the column
|
|
170
|
+
length, so prefer generating at the right length.
|
|
171
|
+
4. **`attempt` must change the output** if the transformer claims
|
|
172
|
+
`supports_uniqueness`, otherwise a uniqueness retry loops forever.
|
|
173
|
+
5. **Set `preserves_domain = True` only if it is true** — that is, if a masked
|
|
174
|
+
value can legitimately equal another row's original. It controls whether
|
|
175
|
+
`verify` reports a match as a warning or as a leak, so getting it wrong
|
|
176
|
+
either hides a leak or cries wolf.
|
|
177
|
+
|
|
178
|
+
## Reporting issues
|
|
179
|
+
|
|
180
|
+
Use the issue templates. For anything that looks like a security problem, see
|
|
181
|
+
[SECURITY.md](SECURITY.md) instead — please do not open a public issue.
|
|
182
|
+
|
|
183
|
+
A useful bug report includes the Masker version (`masker --version`), the
|
|
184
|
+
Python version, the backend, a `masker.yaml` with the secrets removed, and the
|
|
185
|
+
redacted error output.
|
|
186
|
+
|
|
187
|
+
## Code of conduct
|
|
188
|
+
|
|
189
|
+
This project follows the [Contributor Covenant](CODE_OF_CONDUCT.md).
|
masker_db-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Zahid Hasan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|