fraiseql-confiture 0.3.7__cp311-cp311-macosx_11_0_arm64.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.cpython-311-darwin.so +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 +1893 -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 +184 -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 +882 -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 +24 -0
- confiture/models/lint.py +193 -0
- confiture/models/migration.py +265 -0
- confiture/models/schema.py +203 -0
- confiture/models/sql_file_migration.py +225 -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 +100 -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/loader.py +225 -0
- confiture/testing/pytest/__init__.py +38 -0
- confiture/testing/pytest_plugin.py +190 -0
- confiture/testing/sandbox.py +304 -0
- confiture/testing/utils/__init__.py +0 -0
- fraiseql_confiture-0.3.7.dist-info/METADATA +438 -0
- fraiseql_confiture-0.3.7.dist-info/RECORD +124 -0
- fraiseql_confiture-0.3.7.dist-info/WHEEL +4 -0
- fraiseql_confiture-0.3.7.dist-info/entry_points.txt +4 -0
- fraiseql_confiture-0.3.7.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
"""Credit card masking anonymization strategy.
|
|
2
|
+
|
|
3
|
+
Provides PCI-DSS compliant credit card anonymization with:
|
|
4
|
+
- Preserve last 4 digits (identifies card variant)
|
|
5
|
+
- Preserve BIN (Bank Identification Number - first 6 digits)
|
|
6
|
+
- Luhn validation for checksums
|
|
7
|
+
- Card type detection (Visa, Mastercard, Amex, Discover, etc)
|
|
8
|
+
- Deterministic anonymization based on seed
|
|
9
|
+
|
|
10
|
+
Security Note:
|
|
11
|
+
Does NOT mask full PAN in production without proper PCI-DSS controls.
|
|
12
|
+
Use with secure storage and access controls. This is for data masking only.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import random
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
|
|
18
|
+
from confiture.core.anonymization.strategy import AnonymizationStrategy, StrategyConfig
|
|
19
|
+
|
|
20
|
+
# Card type patterns: (name, digit_lengths, prefix_patterns)
|
|
21
|
+
CARD_TYPES = {
|
|
22
|
+
"visa": (16, [4]),
|
|
23
|
+
"mastercard": (16, [51, 52, 53, 54, 55]),
|
|
24
|
+
"amex": (15, [34, 37]),
|
|
25
|
+
"discover": (16, [6011, 622, 644, 645, 646, 647, 648, 649, 65]),
|
|
26
|
+
"diners": (14, [36, 38, 39]),
|
|
27
|
+
"jcb": (16, [35]),
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def luhn_checksum(card_number: str) -> int:
|
|
32
|
+
"""Calculate Luhn checksum for card number.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
card_number: Card number string (digits only, without checksum)
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Luhn checksum digit
|
|
39
|
+
"""
|
|
40
|
+
digits = [int(d) for d in card_number]
|
|
41
|
+
# Double every second digit from right to left (before checksum)
|
|
42
|
+
# Since we're calculating the checksum digit, start from the right and skip first position
|
|
43
|
+
for i in range(len(digits) - 1, -1, -2):
|
|
44
|
+
digits[i] *= 2
|
|
45
|
+
if digits[i] > 9:
|
|
46
|
+
digits[i] -= 9
|
|
47
|
+
|
|
48
|
+
total = sum(digits)
|
|
49
|
+
checksum = (10 - (total % 10)) % 10
|
|
50
|
+
return checksum
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def detect_card_type(card_number: str) -> str:
|
|
54
|
+
"""Detect card type from card number.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
card_number: Card number string (digits only)
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Card type (visa, mastercard, amex, etc) or 'unknown'
|
|
61
|
+
"""
|
|
62
|
+
if not card_number or not card_number.isdigit():
|
|
63
|
+
return "unknown"
|
|
64
|
+
|
|
65
|
+
for card_type, (expected_length, prefixes) in CARD_TYPES.items():
|
|
66
|
+
if len(card_number) == expected_length:
|
|
67
|
+
for prefix in prefixes:
|
|
68
|
+
if card_number.startswith(str(prefix)):
|
|
69
|
+
return card_type
|
|
70
|
+
|
|
71
|
+
return "unknown"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def is_valid_card_number(card_number: str) -> bool:
|
|
75
|
+
"""Validate card number using Luhn algorithm.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
card_number: Card number string (may include spaces/dashes)
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
True if card number passes Luhn validation
|
|
82
|
+
"""
|
|
83
|
+
if not card_number:
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
# Remove spaces/dashes
|
|
87
|
+
digits_str = card_number.replace(" ", "").replace("-", "")
|
|
88
|
+
|
|
89
|
+
if not digits_str.isdigit():
|
|
90
|
+
return False
|
|
91
|
+
|
|
92
|
+
# Check valid length
|
|
93
|
+
if len(digits_str) < 13 or len(digits_str) > 19:
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
# Verify Luhn checksum
|
|
97
|
+
digits = [int(d) for d in digits_str]
|
|
98
|
+
# Double every second digit from right to left
|
|
99
|
+
for i in range(len(digits) - 2, -1, -2):
|
|
100
|
+
digits[i] *= 2
|
|
101
|
+
if digits[i] > 9:
|
|
102
|
+
digits[i] -= 9
|
|
103
|
+
|
|
104
|
+
return sum(digits) % 10 == 0
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass
|
|
108
|
+
class CreditCardConfig(StrategyConfig):
|
|
109
|
+
"""Configuration for credit card masking strategy.
|
|
110
|
+
|
|
111
|
+
Attributes:
|
|
112
|
+
seed: Seed for deterministic randomization
|
|
113
|
+
preserve_last4: If True, preserve last 4 digits (default True)
|
|
114
|
+
preserve_bin: If True, preserve first 6 digits (default False)
|
|
115
|
+
mask_char: Character to use for masking (default '*')
|
|
116
|
+
validate: If True, validate card number with Luhn (default True)
|
|
117
|
+
|
|
118
|
+
Example:
|
|
119
|
+
>>> config = CreditCardConfig(seed=12345, preserve_last4=True)
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
preserve_last4: bool = True
|
|
123
|
+
preserve_bin: bool = False
|
|
124
|
+
mask_char: str = "*"
|
|
125
|
+
validate: bool = True
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class CreditCardStrategy(AnonymizationStrategy):
|
|
129
|
+
"""Anonymization strategy for masking credit card numbers.
|
|
130
|
+
|
|
131
|
+
Provides PCI-DSS compliant card masking with configurable preservation:
|
|
132
|
+
- Preserve last 4 digits (identifies card type for customer)
|
|
133
|
+
- Preserve BIN (first 6 digits, bank identifier)
|
|
134
|
+
- Generate realistic valid card numbers (pass Luhn check)
|
|
135
|
+
- Deterministic output (same seed = same output)
|
|
136
|
+
|
|
137
|
+
Features:
|
|
138
|
+
- Luhn validation
|
|
139
|
+
- Card type detection
|
|
140
|
+
- Format preservation
|
|
141
|
+
- PCI-DSS compliant
|
|
142
|
+
|
|
143
|
+
Example:
|
|
144
|
+
>>> config = CreditCardConfig(seed=12345, preserve_last4=True)
|
|
145
|
+
>>> strategy = CreditCardStrategy(config)
|
|
146
|
+
>>> strategy.anonymize("4532-1111-1111-1234")
|
|
147
|
+
'4532-****-****-1234' # Last 4 preserved
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
config_type = CreditCardConfig
|
|
151
|
+
strategy_name = "credit_card"
|
|
152
|
+
|
|
153
|
+
def anonymize(self, value: str | None) -> str | None:
|
|
154
|
+
"""Anonymize a credit card number.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
value: Card number (with or without separators)
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
Anonymized card number
|
|
161
|
+
|
|
162
|
+
Example:
|
|
163
|
+
>>> strategy.anonymize("4532-1111-1111-1234")
|
|
164
|
+
'4532-****-****-1234'
|
|
165
|
+
"""
|
|
166
|
+
if value is None:
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
if isinstance(value, str) and not value.strip():
|
|
170
|
+
return value
|
|
171
|
+
|
|
172
|
+
# Clean card number
|
|
173
|
+
cleaned = value.replace(" ", "").replace("-", "")
|
|
174
|
+
|
|
175
|
+
# Validate if required
|
|
176
|
+
if self.config.validate and not is_valid_card_number(cleaned):
|
|
177
|
+
# If validation fails, return masked version
|
|
178
|
+
return self._mask_simple(value)
|
|
179
|
+
|
|
180
|
+
# Generate anonymized card
|
|
181
|
+
if self.config.preserve_bin:
|
|
182
|
+
return self._anonymize_preserve_bin(cleaned, value)
|
|
183
|
+
elif self.config.preserve_last4:
|
|
184
|
+
return self._anonymize_preserve_last4(cleaned, value)
|
|
185
|
+
else:
|
|
186
|
+
return self._anonymize_full(cleaned, value)
|
|
187
|
+
|
|
188
|
+
def validate(self, value: str) -> bool:
|
|
189
|
+
"""Check if strategy can handle this value type.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
value: Sample value to validate
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
True if value is a string or None
|
|
196
|
+
"""
|
|
197
|
+
return isinstance(value, str) or value is None
|
|
198
|
+
|
|
199
|
+
def _mask_simple(self, card_number: str) -> str:
|
|
200
|
+
"""Simple masking for invalid cards.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
card_number: Original card number
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
Masked card with same format
|
|
207
|
+
"""
|
|
208
|
+
cleaned = card_number.replace(" ", "").replace("-", "")
|
|
209
|
+
mask_count = max(len(cleaned) - 4, 0)
|
|
210
|
+
|
|
211
|
+
masked = self.config.mask_char * mask_count + cleaned[-4:] if mask_count > 0 else cleaned
|
|
212
|
+
|
|
213
|
+
# Return in original format
|
|
214
|
+
return self._apply_format(card_number, masked)
|
|
215
|
+
|
|
216
|
+
def _anonymize_preserve_last4(self, cleaned: str, original: str) -> str:
|
|
217
|
+
"""Anonymize but preserve last 4 digits.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
cleaned: Card number without separators
|
|
221
|
+
original: Original card number with formatting
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
Anonymized card number in same format as original
|
|
225
|
+
"""
|
|
226
|
+
rng = random.Random(f"{self.config.seed}:{cleaned}".encode())
|
|
227
|
+
|
|
228
|
+
last4 = cleaned[-4:]
|
|
229
|
+
first_part = cleaned[:-4]
|
|
230
|
+
|
|
231
|
+
# Generate random middle digits (excluding the check digit space)
|
|
232
|
+
# We need to preserve last 4, so randomize everything before that
|
|
233
|
+
middle_length = len(first_part) - 6
|
|
234
|
+
if middle_length > 0:
|
|
235
|
+
middle = "".join(str(rng.randint(0, 9)) for _ in range(middle_length))
|
|
236
|
+
else:
|
|
237
|
+
middle = ""
|
|
238
|
+
|
|
239
|
+
# Generate BIN (first 6 digits) - keep valid card type if possible
|
|
240
|
+
card_type = detect_card_type(cleaned)
|
|
241
|
+
bin_digits = self._generate_bin(card_type, rng)
|
|
242
|
+
|
|
243
|
+
# Reconstruct card number WITHOUT checksum first
|
|
244
|
+
# We preserve the last 4, then calculate checksum for the rest
|
|
245
|
+
partial_card = bin_digits + middle + last4[:-1] # First 3 of last4
|
|
246
|
+
checksum = luhn_checksum(partial_card)
|
|
247
|
+
anon_card = partial_card + str(checksum)
|
|
248
|
+
|
|
249
|
+
# Apply original format
|
|
250
|
+
return self._apply_format(original, anon_card)
|
|
251
|
+
|
|
252
|
+
def _anonymize_preserve_bin(self, cleaned: str, original: str) -> str:
|
|
253
|
+
"""Anonymize but preserve BIN (first 6 digits).
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
cleaned: Card number without separators
|
|
257
|
+
original: Original card number with formatting
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
Anonymized card number in same format as original
|
|
261
|
+
"""
|
|
262
|
+
rng = random.Random(f"{self.config.seed}:{cleaned}".encode())
|
|
263
|
+
|
|
264
|
+
bin_digits = cleaned[:6]
|
|
265
|
+
last4 = cleaned[-4:]
|
|
266
|
+
|
|
267
|
+
# Generate random middle digits
|
|
268
|
+
middle_length = len(cleaned) - 10
|
|
269
|
+
if middle_length > 0:
|
|
270
|
+
middle = "".join(str(rng.randint(0, 9)) for _ in range(middle_length))
|
|
271
|
+
else:
|
|
272
|
+
middle = ""
|
|
273
|
+
|
|
274
|
+
# Reconstruct card number
|
|
275
|
+
anon_card = bin_digits + middle + last4
|
|
276
|
+
|
|
277
|
+
# Calculate and append Luhn checksum
|
|
278
|
+
checksum = luhn_checksum(anon_card[:-1])
|
|
279
|
+
anon_card = anon_card[:-1] + str(checksum)
|
|
280
|
+
|
|
281
|
+
# Apply original format
|
|
282
|
+
return self._apply_format(original, anon_card)
|
|
283
|
+
|
|
284
|
+
def _anonymize_full(self, cleaned: str, original: str) -> str:
|
|
285
|
+
"""Fully anonymize card number.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
cleaned: Card number without separators
|
|
289
|
+
original: Original card number with formatting
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
Anonymized card number in same format as original
|
|
293
|
+
"""
|
|
294
|
+
rng = random.Random(f"{self.config.seed}:{cleaned}".encode())
|
|
295
|
+
|
|
296
|
+
card_type = detect_card_type(cleaned)
|
|
297
|
+
bin_digits = self._generate_bin(card_type, rng)
|
|
298
|
+
|
|
299
|
+
# Generate random remaining digits
|
|
300
|
+
remaining_length = len(cleaned) - 7
|
|
301
|
+
remaining = "".join(str(rng.randint(0, 9)) for _ in range(remaining_length))
|
|
302
|
+
|
|
303
|
+
# Reconstruct card number
|
|
304
|
+
anon_card = bin_digits + remaining
|
|
305
|
+
|
|
306
|
+
# Calculate and append Luhn checksum
|
|
307
|
+
checksum = luhn_checksum(anon_card[:-1])
|
|
308
|
+
anon_card = anon_card[:-1] + str(checksum)
|
|
309
|
+
|
|
310
|
+
# Apply original format
|
|
311
|
+
return self._apply_format(original, anon_card)
|
|
312
|
+
|
|
313
|
+
def _generate_bin(self, card_type: str, rng: random.Random) -> str:
|
|
314
|
+
"""Generate valid BIN for card type.
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
card_type: Card type (visa, mastercard, etc)
|
|
318
|
+
rng: Random number generator
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
Valid 6-digit BIN
|
|
322
|
+
"""
|
|
323
|
+
if card_type == "visa":
|
|
324
|
+
return "4" + "".join(str(rng.randint(0, 9)) for _ in range(5))
|
|
325
|
+
elif card_type == "mastercard":
|
|
326
|
+
prefix = rng.choice([51, 52, 53, 54, 55])
|
|
327
|
+
return str(prefix) + "".join(str(rng.randint(0, 9)) for _ in range(4))
|
|
328
|
+
elif card_type == "amex":
|
|
329
|
+
prefix = rng.choice([34, 37])
|
|
330
|
+
return str(prefix) + "".join(str(rng.randint(0, 9)) for _ in range(4))
|
|
331
|
+
elif card_type == "discover":
|
|
332
|
+
prefix = rng.choice([6011, 622, 644, 645, 646, 647, 648, 649, 65])
|
|
333
|
+
prefix_str = str(prefix)
|
|
334
|
+
remaining = 6 - len(prefix_str)
|
|
335
|
+
return prefix_str + "".join(str(rng.randint(0, 9)) for _ in range(remaining))
|
|
336
|
+
else:
|
|
337
|
+
# Default: generate random BIN
|
|
338
|
+
return "".join(str(rng.randint(0, 9)) for _ in range(6))
|
|
339
|
+
|
|
340
|
+
def _apply_format(self, original: str, cleaned: str) -> str:
|
|
341
|
+
"""Apply original formatting to cleaned card number.
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
original: Original card number with formatting
|
|
345
|
+
cleaned: Cleaned anonymized card number
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
Card number with original formatting applied
|
|
349
|
+
"""
|
|
350
|
+
result = []
|
|
351
|
+
cleaned_idx = 0
|
|
352
|
+
|
|
353
|
+
for char in original:
|
|
354
|
+
if char.isdigit():
|
|
355
|
+
if cleaned_idx < len(cleaned):
|
|
356
|
+
result.append(cleaned[cleaned_idx])
|
|
357
|
+
cleaned_idx += 1
|
|
358
|
+
else:
|
|
359
|
+
result.append(char)
|
|
360
|
+
|
|
361
|
+
return "".join(result)
|
|
362
|
+
|
|
363
|
+
def short_name(self) -> str:
|
|
364
|
+
"""Return short strategy name for logging.
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
Short name (e.g., "credit_card:preserve_last4")
|
|
368
|
+
"""
|
|
369
|
+
if self.config.preserve_bin:
|
|
370
|
+
return "credit_card:preserve_bin"
|
|
371
|
+
elif self.config.preserve_last4:
|
|
372
|
+
return "credit_card:preserve_last4"
|
|
373
|
+
else:
|
|
374
|
+
return "credit_card:full"
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Custom anonymization strategy.
|
|
2
|
+
|
|
3
|
+
Provides a framework for implementing custom anonymization logic:
|
|
4
|
+
- Callable-based strategy (use any Python function)
|
|
5
|
+
- Deterministic with seed
|
|
6
|
+
- Type validation
|
|
7
|
+
- Logging-friendly
|
|
8
|
+
|
|
9
|
+
Useful for domain-specific anonymization that doesn't fit into built-in strategies.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from collections.abc import Callable
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from confiture.core.anonymization.strategy import AnonymizationStrategy, StrategyConfig
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class CustomConfig(StrategyConfig):
|
|
21
|
+
"""Configuration for custom anonymization strategy.
|
|
22
|
+
|
|
23
|
+
Attributes:
|
|
24
|
+
seed: Seed for deterministic randomization
|
|
25
|
+
func: Callable that performs anonymization
|
|
26
|
+
name: Human-readable name for custom function
|
|
27
|
+
accepts_seed: If True, func receives (value, seed) else just (value)
|
|
28
|
+
|
|
29
|
+
Example:
|
|
30
|
+
>>> def upper_mask(value):
|
|
31
|
+
... return value.upper() if isinstance(value, str) else value
|
|
32
|
+
>>> config = CustomConfig(seed=12345, func=upper_mask, name="uppercase")
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
func: Callable[[Any], Any] | None = None
|
|
36
|
+
name: str = "custom"
|
|
37
|
+
accepts_seed: bool = False
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class CustomStrategy(AnonymizationStrategy):
|
|
41
|
+
"""Custom anonymization strategy using callable functions.
|
|
42
|
+
|
|
43
|
+
Allows implementing domain-specific anonymization logic without
|
|
44
|
+
creating a full strategy class. Useful for one-off anonymization needs.
|
|
45
|
+
|
|
46
|
+
Features:
|
|
47
|
+
- Function-based anonymization
|
|
48
|
+
- Optional seed parameter
|
|
49
|
+
- Type validation
|
|
50
|
+
- Clear error handling
|
|
51
|
+
|
|
52
|
+
Example:
|
|
53
|
+
>>> def hash_value(value):
|
|
54
|
+
... return f"hash_{hash(str(value))}"
|
|
55
|
+
>>> config = CustomConfig(seed=12345, func=hash_value)
|
|
56
|
+
>>> strategy = CustomStrategy(config)
|
|
57
|
+
>>> strategy.anonymize("secret")
|
|
58
|
+
'hash_...'
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
config_type = CustomConfig
|
|
62
|
+
strategy_name = "custom"
|
|
63
|
+
|
|
64
|
+
def anonymize(self, value: Any) -> Any:
|
|
65
|
+
"""Apply custom anonymization function.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
value: Value to anonymize
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Anonymized value
|
|
72
|
+
|
|
73
|
+
Raises:
|
|
74
|
+
RuntimeError: If custom function is not configured
|
|
75
|
+
Exception: Any exception from custom function
|
|
76
|
+
"""
|
|
77
|
+
if self.config.func is None:
|
|
78
|
+
raise RuntimeError("Custom strategy requires 'func' to be configured")
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
if self.config.accepts_seed:
|
|
82
|
+
return self.config.func(value, self.config.seed)
|
|
83
|
+
else:
|
|
84
|
+
return self.config.func(value)
|
|
85
|
+
except Exception as e:
|
|
86
|
+
raise Exception(
|
|
87
|
+
f"Error in custom anonymization function '{self.config.name}': {e}"
|
|
88
|
+
) from e
|
|
89
|
+
|
|
90
|
+
def validate(self, value: Any) -> bool: # noqa: ARG002
|
|
91
|
+
"""Check if strategy can handle this value type.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
value: Sample value to validate (unused, custom accepts any)
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
True (custom accepts any value type)
|
|
98
|
+
"""
|
|
99
|
+
return True
|
|
100
|
+
|
|
101
|
+
def short_name(self) -> str:
|
|
102
|
+
"""Return short strategy name for logging.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Short name (e.g., "custom:my_function")
|
|
106
|
+
"""
|
|
107
|
+
return f"{self.strategy_name}:{self.config.name}"
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class CustomLambdaStrategy(AnonymizationStrategy):
|
|
111
|
+
"""Custom strategy using inline lambda functions.
|
|
112
|
+
|
|
113
|
+
Simplified version using lambda expressions for very simple
|
|
114
|
+
anonymization logic.
|
|
115
|
+
|
|
116
|
+
Example:
|
|
117
|
+
>>> config = CustomConfig(
|
|
118
|
+
... func=lambda x: f"anon_{hash(x) % 10000}",
|
|
119
|
+
... name="hash_last4"
|
|
120
|
+
... )
|
|
121
|
+
>>> strategy = CustomLambdaStrategy(config)
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
config_type = CustomConfig
|
|
125
|
+
strategy_name = "custom_lambda"
|
|
126
|
+
|
|
127
|
+
def anonymize(self, value: Any) -> Any:
|
|
128
|
+
"""Apply lambda-based anonymization.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
value: Value to anonymize
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Anonymized value
|
|
135
|
+
"""
|
|
136
|
+
if self.config.func is None:
|
|
137
|
+
raise RuntimeError("Lambda strategy requires 'func' to be configured")
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
return self.config.func(value)
|
|
141
|
+
except Exception as e:
|
|
142
|
+
raise Exception(f"Error in lambda anonymization: {e}") from e
|
|
143
|
+
|
|
144
|
+
def validate(self, value: Any) -> bool: # noqa: ARG002
|
|
145
|
+
"""Check if strategy can handle this value type.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
value: Sample value to validate (unused, lambda accepts any)
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
True (lambda accepts any value type)
|
|
152
|
+
"""
|
|
153
|
+
return True
|
|
154
|
+
|
|
155
|
+
def short_name(self) -> str:
|
|
156
|
+
"""Return short strategy name for logging.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
Short name
|
|
160
|
+
"""
|
|
161
|
+
return f"{self.strategy_name}:{self.config.name}"
|