fraiseql-confiture 0.3.4__cp311-cp311-win_amd64.whl
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.
- confiture/__init__.py +48 -0
- confiture/_core.cp311-win_amd64.pyd +0 -0
- confiture/cli/__init__.py +0 -0
- confiture/cli/dry_run.py +116 -0
- confiture/cli/lint_formatter.py +193 -0
- confiture/cli/main.py +1656 -0
- confiture/config/__init__.py +0 -0
- confiture/config/environment.py +263 -0
- confiture/core/__init__.py +51 -0
- confiture/core/anonymization/__init__.py +0 -0
- confiture/core/anonymization/audit.py +485 -0
- confiture/core/anonymization/benchmarking.py +372 -0
- confiture/core/anonymization/breach_notification.py +652 -0
- confiture/core/anonymization/compliance.py +617 -0
- confiture/core/anonymization/composer.py +298 -0
- confiture/core/anonymization/data_subject_rights.py +669 -0
- confiture/core/anonymization/factory.py +319 -0
- confiture/core/anonymization/governance.py +737 -0
- confiture/core/anonymization/performance.py +1092 -0
- confiture/core/anonymization/profile.py +284 -0
- confiture/core/anonymization/registry.py +195 -0
- confiture/core/anonymization/security/kms_manager.py +547 -0
- confiture/core/anonymization/security/lineage.py +888 -0
- confiture/core/anonymization/security/token_store.py +686 -0
- confiture/core/anonymization/strategies/__init__.py +41 -0
- confiture/core/anonymization/strategies/address.py +359 -0
- confiture/core/anonymization/strategies/credit_card.py +374 -0
- confiture/core/anonymization/strategies/custom.py +161 -0
- confiture/core/anonymization/strategies/date.py +218 -0
- confiture/core/anonymization/strategies/differential_privacy.py +398 -0
- confiture/core/anonymization/strategies/email.py +141 -0
- confiture/core/anonymization/strategies/format_preserving_encryption.py +310 -0
- confiture/core/anonymization/strategies/hash.py +150 -0
- confiture/core/anonymization/strategies/ip_address.py +235 -0
- confiture/core/anonymization/strategies/masking_retention.py +252 -0
- confiture/core/anonymization/strategies/name.py +298 -0
- confiture/core/anonymization/strategies/phone.py +119 -0
- confiture/core/anonymization/strategies/preserve.py +85 -0
- confiture/core/anonymization/strategies/redact.py +101 -0
- confiture/core/anonymization/strategies/salted_hashing.py +322 -0
- confiture/core/anonymization/strategies/text_redaction.py +183 -0
- confiture/core/anonymization/strategies/tokenization.py +334 -0
- confiture/core/anonymization/strategy.py +241 -0
- confiture/core/anonymization/syncer_audit.py +357 -0
- confiture/core/blue_green.py +683 -0
- confiture/core/builder.py +500 -0
- confiture/core/checksum.py +358 -0
- confiture/core/connection.py +132 -0
- confiture/core/differ.py +522 -0
- confiture/core/drift.py +564 -0
- confiture/core/dry_run.py +182 -0
- confiture/core/health.py +313 -0
- confiture/core/hooks/__init__.py +87 -0
- confiture/core/hooks/base.py +232 -0
- confiture/core/hooks/context.py +146 -0
- confiture/core/hooks/execution_strategies.py +57 -0
- confiture/core/hooks/observability.py +220 -0
- confiture/core/hooks/phases.py +53 -0
- confiture/core/hooks/registry.py +295 -0
- confiture/core/large_tables.py +775 -0
- confiture/core/linting/__init__.py +70 -0
- confiture/core/linting/composer.py +192 -0
- confiture/core/linting/libraries/__init__.py +17 -0
- confiture/core/linting/libraries/gdpr.py +168 -0
- confiture/core/linting/libraries/general.py +184 -0
- confiture/core/linting/libraries/hipaa.py +144 -0
- confiture/core/linting/libraries/pci_dss.py +104 -0
- confiture/core/linting/libraries/sox.py +120 -0
- confiture/core/linting/schema_linter.py +491 -0
- confiture/core/linting/versioning.py +151 -0
- confiture/core/locking.py +389 -0
- confiture/core/migration_generator.py +298 -0
- confiture/core/migrator.py +793 -0
- confiture/core/observability/__init__.py +44 -0
- confiture/core/observability/audit.py +323 -0
- confiture/core/observability/logging.py +187 -0
- confiture/core/observability/metrics.py +174 -0
- confiture/core/observability/tracing.py +192 -0
- confiture/core/pg_version.py +418 -0
- confiture/core/pool.py +406 -0
- confiture/core/risk/__init__.py +39 -0
- confiture/core/risk/predictor.py +188 -0
- confiture/core/risk/scoring.py +248 -0
- confiture/core/rollback_generator.py +388 -0
- confiture/core/schema_analyzer.py +769 -0
- confiture/core/schema_to_schema.py +590 -0
- confiture/core/security/__init__.py +32 -0
- confiture/core/security/logging.py +201 -0
- confiture/core/security/validation.py +416 -0
- confiture/core/signals.py +371 -0
- confiture/core/syncer.py +540 -0
- confiture/exceptions.py +192 -0
- confiture/integrations/__init__.py +0 -0
- confiture/models/__init__.py +0 -0
- confiture/models/lint.py +193 -0
- confiture/models/migration.py +180 -0
- confiture/models/schema.py +203 -0
- confiture/scenarios/__init__.py +36 -0
- confiture/scenarios/compliance.py +586 -0
- confiture/scenarios/ecommerce.py +199 -0
- confiture/scenarios/financial.py +253 -0
- confiture/scenarios/healthcare.py +315 -0
- confiture/scenarios/multi_tenant.py +340 -0
- confiture/scenarios/saas.py +295 -0
- confiture/testing/FRAMEWORK_API.md +722 -0
- confiture/testing/__init__.py +38 -0
- confiture/testing/fixtures/__init__.py +11 -0
- confiture/testing/fixtures/data_validator.py +229 -0
- confiture/testing/fixtures/migration_runner.py +167 -0
- confiture/testing/fixtures/schema_snapshotter.py +352 -0
- confiture/testing/frameworks/__init__.py +10 -0
- confiture/testing/frameworks/mutation.py +587 -0
- confiture/testing/frameworks/performance.py +479 -0
- confiture/testing/utils/__init__.py +0 -0
- fraiseql_confiture-0.3.4.dist-info/METADATA +438 -0
- fraiseql_confiture-0.3.4.dist-info/RECORD +119 -0
- fraiseql_confiture-0.3.4.dist-info/WHEEL +4 -0
- fraiseql_confiture-0.3.4.dist-info/entry_points.txt +2 -0
- fraiseql_confiture-0.3.4.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"""Name masking anonymization strategy.
|
|
2
|
+
|
|
3
|
+
Provides deterministic name masking with multiple format options:
|
|
4
|
+
- Preserve initials only (e.g., "John Doe" → "J.D.")
|
|
5
|
+
- Generate random names (deterministic from seed)
|
|
6
|
+
- Generate name from pools (first names + last names)
|
|
7
|
+
|
|
8
|
+
Uses seeded randomization to ensure same input always produces same output.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import random
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
|
|
14
|
+
from confiture.core.anonymization.strategy import AnonymizationStrategy, StrategyConfig
|
|
15
|
+
|
|
16
|
+
# Common first names (50 names for diversity)
|
|
17
|
+
FIRST_NAMES = [
|
|
18
|
+
"James",
|
|
19
|
+
"Mary",
|
|
20
|
+
"Robert",
|
|
21
|
+
"Patricia",
|
|
22
|
+
"Michael",
|
|
23
|
+
"Jennifer",
|
|
24
|
+
"William",
|
|
25
|
+
"Linda",
|
|
26
|
+
"David",
|
|
27
|
+
"Barbara",
|
|
28
|
+
"Richard",
|
|
29
|
+
"Elizabeth",
|
|
30
|
+
"Joseph",
|
|
31
|
+
"Susan",
|
|
32
|
+
"Charles",
|
|
33
|
+
"Jessica",
|
|
34
|
+
"Christopher",
|
|
35
|
+
"Sarah",
|
|
36
|
+
"Daniel",
|
|
37
|
+
"Karen",
|
|
38
|
+
"Matthew",
|
|
39
|
+
"Nancy",
|
|
40
|
+
"Anthony",
|
|
41
|
+
"Lisa",
|
|
42
|
+
"Mark",
|
|
43
|
+
"Betty",
|
|
44
|
+
"Donald",
|
|
45
|
+
"Margaret",
|
|
46
|
+
"Steven",
|
|
47
|
+
"Sandra",
|
|
48
|
+
"Paul",
|
|
49
|
+
"Ashley",
|
|
50
|
+
"Andrew",
|
|
51
|
+
"Kimberly",
|
|
52
|
+
"Joshua",
|
|
53
|
+
"Emily",
|
|
54
|
+
"Kenneth",
|
|
55
|
+
"Donna",
|
|
56
|
+
"Kevin",
|
|
57
|
+
"Michelle",
|
|
58
|
+
"Brian",
|
|
59
|
+
"Dorothy",
|
|
60
|
+
"George",
|
|
61
|
+
"Carol",
|
|
62
|
+
"Edward",
|
|
63
|
+
"Amanda",
|
|
64
|
+
"Ronald",
|
|
65
|
+
"Melissa",
|
|
66
|
+
"Timothy",
|
|
67
|
+
"Deborah",
|
|
68
|
+
"Jason",
|
|
69
|
+
"Stephanie",
|
|
70
|
+
"Jeffrey",
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
# Common last names (50 names for diversity)
|
|
74
|
+
LAST_NAMES = [
|
|
75
|
+
"Smith",
|
|
76
|
+
"Johnson",
|
|
77
|
+
"Williams",
|
|
78
|
+
"Brown",
|
|
79
|
+
"Jones",
|
|
80
|
+
"Garcia",
|
|
81
|
+
"Miller",
|
|
82
|
+
"Davis",
|
|
83
|
+
"Rodriguez",
|
|
84
|
+
"Martinez",
|
|
85
|
+
"Hernandez",
|
|
86
|
+
"Lopez",
|
|
87
|
+
"Gonzalez",
|
|
88
|
+
"Wilson",
|
|
89
|
+
"Anderson",
|
|
90
|
+
"Thomas",
|
|
91
|
+
"Taylor",
|
|
92
|
+
"Moore",
|
|
93
|
+
"Jackson",
|
|
94
|
+
"Martin",
|
|
95
|
+
"Lee",
|
|
96
|
+
"Perez",
|
|
97
|
+
"Thompson",
|
|
98
|
+
"White",
|
|
99
|
+
"Harris",
|
|
100
|
+
"Sanchez",
|
|
101
|
+
"Clark",
|
|
102
|
+
"Ramirez",
|
|
103
|
+
"Lewis",
|
|
104
|
+
"Robinson",
|
|
105
|
+
"Walker",
|
|
106
|
+
"Young",
|
|
107
|
+
"Allen",
|
|
108
|
+
"King",
|
|
109
|
+
"Wright",
|
|
110
|
+
"Scott",
|
|
111
|
+
"Torres",
|
|
112
|
+
"Peterson",
|
|
113
|
+
"Phillips",
|
|
114
|
+
"Campbell",
|
|
115
|
+
"Parker",
|
|
116
|
+
"Evans",
|
|
117
|
+
"Edwards",
|
|
118
|
+
"Collins",
|
|
119
|
+
"Reyes",
|
|
120
|
+
"Stewart",
|
|
121
|
+
"Morris",
|
|
122
|
+
"Morales",
|
|
123
|
+
"Murphy",
|
|
124
|
+
"Rogers",
|
|
125
|
+
"Morgan",
|
|
126
|
+
"Peterson",
|
|
127
|
+
"Cooper",
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@dataclass
|
|
132
|
+
class NameMaskConfig(StrategyConfig):
|
|
133
|
+
"""Configuration for name masking strategy.
|
|
134
|
+
|
|
135
|
+
Attributes:
|
|
136
|
+
seed: Seed for deterministic randomization
|
|
137
|
+
format_type: Output format:
|
|
138
|
+
- "firstname_lastname": "John Doe" → "Michael Patricia" (from name pools)
|
|
139
|
+
- "initials": "John Doe" → "J.D." (preserve initials)
|
|
140
|
+
- "random": "John Doe" → "XyZ4qW9" (random string)
|
|
141
|
+
preserve_initial: If True, keep original first letter
|
|
142
|
+
case_preserving: If True, preserve original case
|
|
143
|
+
|
|
144
|
+
Example:
|
|
145
|
+
>>> config = NameMaskConfig(seed=12345, format_type="firstname_lastname")
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
format_type: str = "firstname_lastname" # firstname_lastname, initials, random
|
|
149
|
+
preserve_initial: bool = False # Only for firstname_lastname format
|
|
150
|
+
case_preserving: bool = True # Preserve original case
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class NameMaskingStrategy(AnonymizationStrategy):
|
|
154
|
+
"""Anonymization strategy for masking personal names.
|
|
155
|
+
|
|
156
|
+
Provides multiple name masking formats with deterministic output based on seed.
|
|
157
|
+
Same input + same seed = same output (enables foreign key consistency).
|
|
158
|
+
|
|
159
|
+
Features:
|
|
160
|
+
- Format-preserving (maintains name-like structure)
|
|
161
|
+
- Deterministic (seed-based)
|
|
162
|
+
- Configurable output format
|
|
163
|
+
- Handles NULL and edge cases
|
|
164
|
+
|
|
165
|
+
Example:
|
|
166
|
+
>>> config = NameMaskConfig(seed=12345, format_type="firstname_lastname")
|
|
167
|
+
>>> strategy = NameMaskingStrategy(config)
|
|
168
|
+
>>> strategy.anonymize("John Doe")
|
|
169
|
+
'Michael Johnson'
|
|
170
|
+
>>> strategy.anonymize("John Doe") # Same seed = same output
|
|
171
|
+
'Michael Johnson'
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
config_type = NameMaskConfig
|
|
175
|
+
strategy_name = "name"
|
|
176
|
+
|
|
177
|
+
def anonymize(self, value: str | None) -> str | None:
|
|
178
|
+
"""Anonymize a name value.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
value: Name to anonymize
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
Anonymized name in configured format
|
|
185
|
+
|
|
186
|
+
Example:
|
|
187
|
+
>>> strategy.anonymize("John Doe")
|
|
188
|
+
'Michael Johnson'
|
|
189
|
+
"""
|
|
190
|
+
if value is None:
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
if isinstance(value, str) and not value.strip():
|
|
194
|
+
return value
|
|
195
|
+
|
|
196
|
+
config = self.config
|
|
197
|
+
|
|
198
|
+
if config.format_type == "firstname_lastname":
|
|
199
|
+
return self._mask_firstname_lastname(value)
|
|
200
|
+
elif config.format_type == "initials":
|
|
201
|
+
return self._mask_initials(value)
|
|
202
|
+
elif config.format_type == "random":
|
|
203
|
+
return self._mask_random(value)
|
|
204
|
+
else:
|
|
205
|
+
raise ValueError(f"Unknown format_type: {config.format_type}")
|
|
206
|
+
|
|
207
|
+
def _mask_firstname_lastname(self, value: str) -> str:
|
|
208
|
+
"""Mask with random first and last name.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
value: Name to mask
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
Anonymized firstname lastname
|
|
215
|
+
"""
|
|
216
|
+
parts = value.strip().split()
|
|
217
|
+
|
|
218
|
+
if not parts:
|
|
219
|
+
return value
|
|
220
|
+
|
|
221
|
+
# Use seed to generate reproducible random names
|
|
222
|
+
rng = random.Random(f"{self.config.seed}:{value}".encode())
|
|
223
|
+
|
|
224
|
+
# Get random first name
|
|
225
|
+
first_name = rng.choice(FIRST_NAMES)
|
|
226
|
+
|
|
227
|
+
# Get random last name
|
|
228
|
+
last_name = rng.choice(LAST_NAMES)
|
|
229
|
+
|
|
230
|
+
# Apply case preservation if needed
|
|
231
|
+
if self.config.case_preserving and parts:
|
|
232
|
+
# Preserve case of original first name
|
|
233
|
+
if parts[0] and parts[0][0].islower():
|
|
234
|
+
first_name = first_name.lower()
|
|
235
|
+
|
|
236
|
+
# Preserve case of original last name (if exists)
|
|
237
|
+
if len(parts) > 1 and parts[1] and parts[1][0].islower():
|
|
238
|
+
last_name = last_name.lower()
|
|
239
|
+
|
|
240
|
+
return f"{first_name} {last_name}"
|
|
241
|
+
|
|
242
|
+
def _mask_initials(self, value: str) -> str:
|
|
243
|
+
"""Mask with initials only.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
value: Name to mask
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
Initials (e.g., "J.D.")
|
|
250
|
+
"""
|
|
251
|
+
parts = value.strip().split()
|
|
252
|
+
|
|
253
|
+
if not parts:
|
|
254
|
+
return value
|
|
255
|
+
|
|
256
|
+
# Get initials from original name
|
|
257
|
+
initials = [part[0].upper() for part in parts if part]
|
|
258
|
+
|
|
259
|
+
return ".".join(initials) + "."
|
|
260
|
+
|
|
261
|
+
def _mask_random(self, value: str) -> str:
|
|
262
|
+
"""Mask with random string.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
value: Name to mask
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
Random string of same length as original
|
|
269
|
+
"""
|
|
270
|
+
if not value:
|
|
271
|
+
return value
|
|
272
|
+
|
|
273
|
+
# Use seed for reproducibility
|
|
274
|
+
rng = random.Random(f"{self.config.seed}:{value}".encode())
|
|
275
|
+
|
|
276
|
+
# Generate random string of same length
|
|
277
|
+
length = len(value.strip())
|
|
278
|
+
chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
|
279
|
+
return "".join(rng.choices(chars, k=length))
|
|
280
|
+
|
|
281
|
+
def validate(self, value: str) -> bool:
|
|
282
|
+
"""Check if strategy can handle this value type.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
value: Sample value to validate
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
True if value is a string or None
|
|
289
|
+
"""
|
|
290
|
+
return isinstance(value, str) or value is None
|
|
291
|
+
|
|
292
|
+
def short_name(self) -> str:
|
|
293
|
+
"""Return short strategy name for logging.
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
Short name (e.g., "name:initials")
|
|
297
|
+
"""
|
|
298
|
+
return f"{self.strategy_name}:{self.config.format_type}"
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Phone number masking anonymization strategy.
|
|
2
|
+
|
|
3
|
+
Generates deterministic fake phone numbers from real ones, useful for:
|
|
4
|
+
- PII protection in test/staging environments
|
|
5
|
+
- Preserving phone-like format for testing
|
|
6
|
+
- Reproducible anonymization (deterministic with seed)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import hashlib
|
|
10
|
+
import re
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from confiture.core.anonymization.strategy import (
|
|
15
|
+
AnonymizationStrategy,
|
|
16
|
+
StrategyConfig,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class PhoneMaskConfig(StrategyConfig):
|
|
22
|
+
"""Configuration for PhoneMaskingStrategy.
|
|
23
|
+
|
|
24
|
+
Attributes:
|
|
25
|
+
format: Phone number format template (use {number} placeholder)
|
|
26
|
+
preserve_country_code: If True, keep original country code
|
|
27
|
+
seed_env_var: Environment variable containing seed
|
|
28
|
+
seed: Hardcoded seed (testing only)
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
format: str = "+1-555-{number}"
|
|
32
|
+
"""Phone format template with {number} placeholder."""
|
|
33
|
+
|
|
34
|
+
preserve_country_code: bool = False
|
|
35
|
+
"""If True, try to preserve original country code."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class PhoneMaskingStrategy(AnonymizationStrategy):
|
|
39
|
+
"""Generate deterministic fake phone numbers from real ones.
|
|
40
|
+
|
|
41
|
+
Features:
|
|
42
|
+
- Deterministic: Same number + seed = same fake number
|
|
43
|
+
- Format customizable: Template-based generation
|
|
44
|
+
- Format preserving: Output looks like a real phone number
|
|
45
|
+
- Unique: Preserves uniqueness for referential integrity
|
|
46
|
+
|
|
47
|
+
Example:
|
|
48
|
+
>>> config = PhoneMaskConfig(
|
|
49
|
+
... format="+1-555-{number}",
|
|
50
|
+
... seed_env_var='ANONYMIZATION_SEED'
|
|
51
|
+
... )
|
|
52
|
+
>>> strategy = PhoneMaskingStrategy(config)
|
|
53
|
+
>>> result = strategy.anonymize('+1-202-555-0123')
|
|
54
|
+
>>> result # e.g., '+1-555-1234'
|
|
55
|
+
'+1-555-1234'
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
# Basic phone number regex (allows various formats)
|
|
59
|
+
PHONE_REGEX = re.compile(r"[\d\s\-\+\(\)]{10,}")
|
|
60
|
+
|
|
61
|
+
def __init__(self, config: PhoneMaskConfig | None = None):
|
|
62
|
+
"""Initialize phone masking strategy.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
config: PhoneMaskConfig instance
|
|
66
|
+
"""
|
|
67
|
+
config = config or PhoneMaskConfig()
|
|
68
|
+
super().__init__(config)
|
|
69
|
+
self.config: PhoneMaskConfig = config
|
|
70
|
+
|
|
71
|
+
def anonymize(self, value: Any) -> Any:
|
|
72
|
+
"""Generate fake phone number from real number.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
value: Phone number to anonymize
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Fake phone number with same format as original
|
|
79
|
+
|
|
80
|
+
Example:
|
|
81
|
+
>>> strategy = PhoneMaskingStrategy(PhoneMaskConfig(seed=12345))
|
|
82
|
+
>>> strategy.anonymize('+1-202-555-0123')
|
|
83
|
+
'+1-555-1234'
|
|
84
|
+
"""
|
|
85
|
+
# Handle NULL
|
|
86
|
+
if value is None:
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
# Handle empty string
|
|
90
|
+
value_str = str(value).strip()
|
|
91
|
+
if not value_str:
|
|
92
|
+
return ""
|
|
93
|
+
|
|
94
|
+
# Create deterministic hash from phone number
|
|
95
|
+
hash_value = hashlib.sha256(f"{self._seed}:{value_str}".encode()).hexdigest()
|
|
96
|
+
|
|
97
|
+
# Extract digits to generate phone number
|
|
98
|
+
# Use hash to create a 4-digit phone number suffix
|
|
99
|
+
number_suffix = str(int(hash_value[:8], 16) % 10000).zfill(4)
|
|
100
|
+
|
|
101
|
+
# Format output
|
|
102
|
+
output = self.config.format.format(number=number_suffix)
|
|
103
|
+
|
|
104
|
+
return output
|
|
105
|
+
|
|
106
|
+
def validate(self, value: Any) -> bool:
|
|
107
|
+
"""Check if value looks like a phone number.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
value: Value to validate
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
True if value matches basic phone pattern
|
|
114
|
+
"""
|
|
115
|
+
if value is None:
|
|
116
|
+
return False
|
|
117
|
+
|
|
118
|
+
value_str = str(value).strip()
|
|
119
|
+
return bool(self.PHONE_REGEX.match(value_str))
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Preserve (no-op) anonymization strategy.
|
|
2
|
+
|
|
3
|
+
Provides a no-operation strategy that returns values unchanged.
|
|
4
|
+
Useful for:
|
|
5
|
+
- Marking columns that should NOT be anonymized
|
|
6
|
+
- Placeholder in strategy chains
|
|
7
|
+
- Configuration clarity (explicit "don't anonymize" intent)
|
|
8
|
+
- Testing and debugging
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from confiture.core.anonymization.strategy import AnonymizationStrategy, StrategyConfig
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class PreserveConfig(StrategyConfig):
|
|
19
|
+
"""Configuration for preserve strategy.
|
|
20
|
+
|
|
21
|
+
This strategy has no configuration options beyond base StrategyConfig.
|
|
22
|
+
It simply returns values unchanged.
|
|
23
|
+
|
|
24
|
+
Example:
|
|
25
|
+
>>> config = PreserveConfig(seed=12345)
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class PreserveStrategy(AnonymizationStrategy):
|
|
32
|
+
"""No-operation anonymization strategy.
|
|
33
|
+
|
|
34
|
+
Returns values unchanged. Useful for marking columns that should not
|
|
35
|
+
be anonymized or as a placeholder in processing chains.
|
|
36
|
+
|
|
37
|
+
Features:
|
|
38
|
+
- Identity operation (returns input unchanged)
|
|
39
|
+
- Handles all value types
|
|
40
|
+
- NULL-safe
|
|
41
|
+
- Useful for explicit "preserve" intent in configurations
|
|
42
|
+
|
|
43
|
+
Example:
|
|
44
|
+
>>> config = PreserveConfig()
|
|
45
|
+
>>> strategy = PreserveStrategy(config)
|
|
46
|
+
>>> strategy.anonymize("sensitive_data")
|
|
47
|
+
'sensitive_data' # Unchanged
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
config_type = PreserveConfig
|
|
51
|
+
strategy_name = "preserve"
|
|
52
|
+
|
|
53
|
+
def anonymize(self, value):
|
|
54
|
+
"""Return value unchanged.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
value: Any value
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
The same value unchanged
|
|
61
|
+
|
|
62
|
+
Example:
|
|
63
|
+
>>> strategy.anonymize("test@example.com")
|
|
64
|
+
'test@example.com'
|
|
65
|
+
"""
|
|
66
|
+
return value
|
|
67
|
+
|
|
68
|
+
def validate(self, value: Any) -> bool: # noqa: ARG002
|
|
69
|
+
"""Check if strategy can handle this value type.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
value: Sample value to validate (unused, preserve accepts any)
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
True (preserve accepts any value type)
|
|
76
|
+
"""
|
|
77
|
+
return True
|
|
78
|
+
|
|
79
|
+
def short_name(self) -> str:
|
|
80
|
+
"""Return short strategy name for logging.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Short name (e.g., "preserve")
|
|
84
|
+
"""
|
|
85
|
+
return self.strategy_name
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Simple redaction anonymization strategy.
|
|
2
|
+
|
|
3
|
+
One-size-fits-all redaction for sensitive data that should be completely hidden,
|
|
4
|
+
not anonymized to a plausible value.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from confiture.core.anonymization.strategy import (
|
|
11
|
+
AnonymizationStrategy,
|
|
12
|
+
StrategyConfig,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class RedactConfig(StrategyConfig):
|
|
18
|
+
"""Configuration for SimpleRedactStrategy.
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
replacement: Text to replace sensitive values with
|
|
22
|
+
seed_env_var: Environment variable containing seed (unused for redaction)
|
|
23
|
+
seed: Hardcoded seed (unused for redaction)
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
replacement: str = "[REDACTED]"
|
|
27
|
+
"""Text to use for all redacted values."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SimpleRedactStrategy(AnonymizationStrategy):
|
|
31
|
+
"""Simple one-size-fits-all redaction strategy.
|
|
32
|
+
|
|
33
|
+
Features:
|
|
34
|
+
- Fast: No hashing or computation
|
|
35
|
+
- Complete: All data values replaced with same text
|
|
36
|
+
- Safe: Zero information leakage
|
|
37
|
+
- Simple: Easy to understand and audit
|
|
38
|
+
|
|
39
|
+
Use when:
|
|
40
|
+
- PII is highly sensitive (no testing needed with real-like data)
|
|
41
|
+
- You don't need to preserve data format (testing doesn't rely on structure)
|
|
42
|
+
- You want maximum privacy with zero complexity
|
|
43
|
+
|
|
44
|
+
Don't use when:
|
|
45
|
+
- You need to preserve uniqueness for testing (FK constraints)
|
|
46
|
+
- You need format-preserving anonymization (testing email-like values)
|
|
47
|
+
- You need to correlate anonymized data across tables
|
|
48
|
+
|
|
49
|
+
Example:
|
|
50
|
+
>>> config = RedactConfig(replacement="[HIDDEN]")
|
|
51
|
+
>>> strategy = SimpleRedactStrategy(config)
|
|
52
|
+
>>> strategy.anonymize("secret data")
|
|
53
|
+
'[HIDDEN]'
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(self, config: RedactConfig | None = None):
|
|
57
|
+
"""Initialize redaction strategy.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
config: RedactConfig instance
|
|
61
|
+
"""
|
|
62
|
+
config = config or RedactConfig()
|
|
63
|
+
super().__init__(config)
|
|
64
|
+
self.config: RedactConfig = config
|
|
65
|
+
|
|
66
|
+
def anonymize(self, value: Any) -> Any:
|
|
67
|
+
"""Redact value to replacement text.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
value: Value to redact
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Replacement text (same for all values)
|
|
74
|
+
|
|
75
|
+
Example:
|
|
76
|
+
>>> strategy = SimpleRedactStrategy()
|
|
77
|
+
>>> strategy.anonymize("anything")
|
|
78
|
+
'[REDACTED]'
|
|
79
|
+
>>> strategy.anonymize("123")
|
|
80
|
+
'[REDACTED]'
|
|
81
|
+
>>> strategy.anonymize(None) # Special case: NULL stays NULL
|
|
82
|
+
"""
|
|
83
|
+
# Special case: NULL stays NULL
|
|
84
|
+
if value is None:
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
# All other values replaced with redaction text
|
|
88
|
+
return self.config.replacement
|
|
89
|
+
|
|
90
|
+
def validate(self, value: Any) -> bool:
|
|
91
|
+
"""Redaction works for any value type.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
value: Value to validate (not really used)
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Always True (redaction works for all types)
|
|
98
|
+
"""
|
|
99
|
+
# Redaction works for all types
|
|
100
|
+
del value
|
|
101
|
+
return True
|