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.
Files changed (119) hide show
  1. confiture/__init__.py +48 -0
  2. confiture/_core.cp311-win_amd64.pyd +0 -0
  3. confiture/cli/__init__.py +0 -0
  4. confiture/cli/dry_run.py +116 -0
  5. confiture/cli/lint_formatter.py +193 -0
  6. confiture/cli/main.py +1656 -0
  7. confiture/config/__init__.py +0 -0
  8. confiture/config/environment.py +263 -0
  9. confiture/core/__init__.py +51 -0
  10. confiture/core/anonymization/__init__.py +0 -0
  11. confiture/core/anonymization/audit.py +485 -0
  12. confiture/core/anonymization/benchmarking.py +372 -0
  13. confiture/core/anonymization/breach_notification.py +652 -0
  14. confiture/core/anonymization/compliance.py +617 -0
  15. confiture/core/anonymization/composer.py +298 -0
  16. confiture/core/anonymization/data_subject_rights.py +669 -0
  17. confiture/core/anonymization/factory.py +319 -0
  18. confiture/core/anonymization/governance.py +737 -0
  19. confiture/core/anonymization/performance.py +1092 -0
  20. confiture/core/anonymization/profile.py +284 -0
  21. confiture/core/anonymization/registry.py +195 -0
  22. confiture/core/anonymization/security/kms_manager.py +547 -0
  23. confiture/core/anonymization/security/lineage.py +888 -0
  24. confiture/core/anonymization/security/token_store.py +686 -0
  25. confiture/core/anonymization/strategies/__init__.py +41 -0
  26. confiture/core/anonymization/strategies/address.py +359 -0
  27. confiture/core/anonymization/strategies/credit_card.py +374 -0
  28. confiture/core/anonymization/strategies/custom.py +161 -0
  29. confiture/core/anonymization/strategies/date.py +218 -0
  30. confiture/core/anonymization/strategies/differential_privacy.py +398 -0
  31. confiture/core/anonymization/strategies/email.py +141 -0
  32. confiture/core/anonymization/strategies/format_preserving_encryption.py +310 -0
  33. confiture/core/anonymization/strategies/hash.py +150 -0
  34. confiture/core/anonymization/strategies/ip_address.py +235 -0
  35. confiture/core/anonymization/strategies/masking_retention.py +252 -0
  36. confiture/core/anonymization/strategies/name.py +298 -0
  37. confiture/core/anonymization/strategies/phone.py +119 -0
  38. confiture/core/anonymization/strategies/preserve.py +85 -0
  39. confiture/core/anonymization/strategies/redact.py +101 -0
  40. confiture/core/anonymization/strategies/salted_hashing.py +322 -0
  41. confiture/core/anonymization/strategies/text_redaction.py +183 -0
  42. confiture/core/anonymization/strategies/tokenization.py +334 -0
  43. confiture/core/anonymization/strategy.py +241 -0
  44. confiture/core/anonymization/syncer_audit.py +357 -0
  45. confiture/core/blue_green.py +683 -0
  46. confiture/core/builder.py +500 -0
  47. confiture/core/checksum.py +358 -0
  48. confiture/core/connection.py +132 -0
  49. confiture/core/differ.py +522 -0
  50. confiture/core/drift.py +564 -0
  51. confiture/core/dry_run.py +182 -0
  52. confiture/core/health.py +313 -0
  53. confiture/core/hooks/__init__.py +87 -0
  54. confiture/core/hooks/base.py +232 -0
  55. confiture/core/hooks/context.py +146 -0
  56. confiture/core/hooks/execution_strategies.py +57 -0
  57. confiture/core/hooks/observability.py +220 -0
  58. confiture/core/hooks/phases.py +53 -0
  59. confiture/core/hooks/registry.py +295 -0
  60. confiture/core/large_tables.py +775 -0
  61. confiture/core/linting/__init__.py +70 -0
  62. confiture/core/linting/composer.py +192 -0
  63. confiture/core/linting/libraries/__init__.py +17 -0
  64. confiture/core/linting/libraries/gdpr.py +168 -0
  65. confiture/core/linting/libraries/general.py +184 -0
  66. confiture/core/linting/libraries/hipaa.py +144 -0
  67. confiture/core/linting/libraries/pci_dss.py +104 -0
  68. confiture/core/linting/libraries/sox.py +120 -0
  69. confiture/core/linting/schema_linter.py +491 -0
  70. confiture/core/linting/versioning.py +151 -0
  71. confiture/core/locking.py +389 -0
  72. confiture/core/migration_generator.py +298 -0
  73. confiture/core/migrator.py +793 -0
  74. confiture/core/observability/__init__.py +44 -0
  75. confiture/core/observability/audit.py +323 -0
  76. confiture/core/observability/logging.py +187 -0
  77. confiture/core/observability/metrics.py +174 -0
  78. confiture/core/observability/tracing.py +192 -0
  79. confiture/core/pg_version.py +418 -0
  80. confiture/core/pool.py +406 -0
  81. confiture/core/risk/__init__.py +39 -0
  82. confiture/core/risk/predictor.py +188 -0
  83. confiture/core/risk/scoring.py +248 -0
  84. confiture/core/rollback_generator.py +388 -0
  85. confiture/core/schema_analyzer.py +769 -0
  86. confiture/core/schema_to_schema.py +590 -0
  87. confiture/core/security/__init__.py +32 -0
  88. confiture/core/security/logging.py +201 -0
  89. confiture/core/security/validation.py +416 -0
  90. confiture/core/signals.py +371 -0
  91. confiture/core/syncer.py +540 -0
  92. confiture/exceptions.py +192 -0
  93. confiture/integrations/__init__.py +0 -0
  94. confiture/models/__init__.py +0 -0
  95. confiture/models/lint.py +193 -0
  96. confiture/models/migration.py +180 -0
  97. confiture/models/schema.py +203 -0
  98. confiture/scenarios/__init__.py +36 -0
  99. confiture/scenarios/compliance.py +586 -0
  100. confiture/scenarios/ecommerce.py +199 -0
  101. confiture/scenarios/financial.py +253 -0
  102. confiture/scenarios/healthcare.py +315 -0
  103. confiture/scenarios/multi_tenant.py +340 -0
  104. confiture/scenarios/saas.py +295 -0
  105. confiture/testing/FRAMEWORK_API.md +722 -0
  106. confiture/testing/__init__.py +38 -0
  107. confiture/testing/fixtures/__init__.py +11 -0
  108. confiture/testing/fixtures/data_validator.py +229 -0
  109. confiture/testing/fixtures/migration_runner.py +167 -0
  110. confiture/testing/fixtures/schema_snapshotter.py +352 -0
  111. confiture/testing/frameworks/__init__.py +10 -0
  112. confiture/testing/frameworks/mutation.py +587 -0
  113. confiture/testing/frameworks/performance.py +479 -0
  114. confiture/testing/utils/__init__.py +0 -0
  115. fraiseql_confiture-0.3.4.dist-info/METADATA +438 -0
  116. fraiseql_confiture-0.3.4.dist-info/RECORD +119 -0
  117. fraiseql_confiture-0.3.4.dist-info/WHEEL +4 -0
  118. fraiseql_confiture-0.3.4.dist-info/entry_points.txt +2 -0
  119. fraiseql_confiture-0.3.4.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,485 @@
1
+ """Immutable audit logging for anonymization operations.
2
+
3
+ This module provides an append-only audit trail for compliance with:
4
+ - GDPR Article 30 (Records of Processing Activities)
5
+ - Data protection audit requirements
6
+ - Operational accountability
7
+
8
+ Security Features:
9
+ - Immutable database table (append-only, no UPDATE/DELETE)
10
+ - HMAC signatures prevent tampering
11
+ - Timestamp tracking for all operations
12
+ - JSON serialization for portability
13
+ - User and hostname tracking
14
+ - Verification status recording
15
+
16
+ Example:
17
+ >>> from datetime import datetime, timezone
18
+ >>> from uuid import uuid4
19
+ >>> from confiture.core.anonymization.audit import AuditEntry, AuditLogger
20
+ >>>
21
+ >>> # Create audit entry
22
+ >>> entry = AuditEntry(
23
+ ... id=uuid4(),
24
+ ... timestamp=datetime.now(timezone.utc),
25
+ ... user="admin@example.com",
26
+ ... hostname="sync-server-01",
27
+ ... source_database="prod_main",
28
+ ... target_database="staging_copy",
29
+ ... profile_name="production",
30
+ ... profile_version="1.0",
31
+ ... profile_hash="abc123def456",
32
+ ... tables_synced=["users", "orders"],
33
+ ... rows_anonymized={"users": 1000, "orders": 5000},
34
+ ... strategies_applied={"email": 1000, "phone": 1000},
35
+ ... verification_passed=True,
36
+ ... verification_report="{}",
37
+ ... signature="hmac_sig_here"
38
+ ... )
39
+ >>>
40
+ >>> # Log to database
41
+ >>> logger = AuditLogger(database_connection)
42
+ >>> logger.log_sync(entry)
43
+ >>> print(f"Logged {entry.id}")
44
+ """
45
+
46
+ import hashlib
47
+ import hmac
48
+ import json
49
+ import os
50
+ import socket
51
+ from dataclasses import asdict, dataclass
52
+ from datetime import UTC, datetime
53
+ from uuid import UUID, uuid4
54
+
55
+ import psycopg
56
+
57
+
58
+ @dataclass
59
+ class AuditEntry:
60
+ """Immutable audit log entry for compliance.
61
+
62
+ Each entry records a complete anonymization operation with:
63
+ - Identity: WHO (user, hostname) and WHEN (timestamp)
64
+ - Operation: WHAT (which tables, which strategies)
65
+ - Verification: DID IT WORK (verification_passed, verification_report)
66
+ - Integrity: SIGNATURE (HMAC to prevent tampering)
67
+
68
+ Attributes:
69
+ id: Unique audit entry ID (UUID)
70
+ timestamp: When the operation occurred (ISO 8601)
71
+ user: User who performed the operation (email or system account)
72
+ hostname: Server hostname where operation was performed
73
+ source_database: Source database URL (for reference)
74
+ target_database: Target database URL (for reference)
75
+ profile_name: Anonymization profile name (for identification)
76
+ profile_version: Profile version (for tracking changes)
77
+ profile_hash: SHA256 hash of profile (for integrity check)
78
+ tables_synced: List of table names processed
79
+ rows_anonymized: Dict of table → count anonymized
80
+ strategies_applied: Dict of strategy → count applied
81
+ verification_passed: Whether verification checks passed
82
+ verification_report: Detailed verification results (JSON string)
83
+ signature: HMAC signature for tamper detection
84
+ """
85
+
86
+ id: UUID
87
+ """Unique entry ID (UUID4)."""
88
+
89
+ timestamp: datetime
90
+ """Operation timestamp (UTC, ISO 8601)."""
91
+
92
+ user: str
93
+ """User who performed the operation."""
94
+
95
+ hostname: str
96
+ """Hostname where operation was performed."""
97
+
98
+ source_database: str
99
+ """Source database identifier."""
100
+
101
+ target_database: str
102
+ """Target database identifier."""
103
+
104
+ profile_name: str
105
+ """Anonymization profile name."""
106
+
107
+ profile_version: str
108
+ """Profile version number."""
109
+
110
+ profile_hash: str
111
+ """SHA256 hash of the profile (integrity check)."""
112
+
113
+ tables_synced: list[str]
114
+ """Tables that were anonymized."""
115
+
116
+ rows_anonymized: dict[str, int]
117
+ """Count of rows anonymized per table."""
118
+
119
+ strategies_applied: dict[str, int]
120
+ """Count of strategy applications per type."""
121
+
122
+ verification_passed: bool
123
+ """Whether verification checks passed."""
124
+
125
+ verification_report: str
126
+ """Verification details (JSON string)."""
127
+
128
+ signature: str
129
+ """HMAC signature for tamper detection."""
130
+
131
+ def to_json(self) -> str:
132
+ """Serialize entry to JSON for storage.
133
+
134
+ Returns:
135
+ JSON string representation of the audit entry
136
+ """
137
+ data = asdict(self)
138
+ data["id"] = str(self.id)
139
+ data["timestamp"] = self.timestamp.isoformat()
140
+ return json.dumps(data)
141
+
142
+ @classmethod
143
+ def from_json(cls, json_str: str) -> "AuditEntry":
144
+ """Deserialize entry from JSON.
145
+
146
+ Args:
147
+ json_str: JSON string representation
148
+
149
+ Returns:
150
+ Reconstructed AuditEntry instance
151
+
152
+ Raises:
153
+ ValueError: If JSON is invalid
154
+ """
155
+ try:
156
+ data = json.loads(json_str)
157
+ data["id"] = UUID(data["id"])
158
+ data["timestamp"] = datetime.fromisoformat(data["timestamp"])
159
+ return cls(**data)
160
+ except Exception as e:
161
+ raise ValueError(f"Invalid audit entry JSON: {e}") from e
162
+
163
+
164
+ class AuditLogger:
165
+ """Append-only audit log for anonymization operations.
166
+
167
+ Provides secure logging with:
168
+ - Immutable database table (no UPDATE/DELETE)
169
+ - Automatic table creation
170
+ - Entry signing with HMAC
171
+ - Tamper detection via signatures
172
+
173
+ Example:
174
+ >>> import psycopg
175
+ >>> conn = psycopg.connect("postgresql://localhost/confiture")
176
+ >>> logger = AuditLogger(conn)
177
+ >>> entry = AuditEntry(...)
178
+ >>> logger.log_sync(entry)
179
+ >>> log = logger.get_audit_log(limit=100)
180
+ >>> print(f"Found {len(log)} audit entries")
181
+ """
182
+
183
+ def __init__(self, target_conn: psycopg.Connection):
184
+ """Initialize audit logger with database connection.
185
+
186
+ Args:
187
+ target_conn: PostgreSQL connection for audit table
188
+
189
+ Raises:
190
+ psycopg.OperationalError: If connection fails
191
+ """
192
+ self.conn = target_conn
193
+ self._ensure_audit_table()
194
+
195
+ def _ensure_audit_table(self) -> None:
196
+ """Create audit table if not exists (idempotent).
197
+
198
+ This method creates the confiture_audit_log table with:
199
+ - UUID primary key for entry identification
200
+ - TIMESTAMPTZ for accurate time tracking
201
+ - JSONB for flexible audit data
202
+ - PostgreSQL-enforced append-only constraints
203
+
204
+ Raises:
205
+ psycopg.DatabaseError: If table creation fails
206
+ """
207
+ with self.conn.cursor() as cursor:
208
+ cursor.execute(
209
+ """
210
+ CREATE TABLE IF NOT EXISTS confiture_audit_log (
211
+ id UUID PRIMARY KEY,
212
+ timestamp TIMESTAMPTZ NOT NULL,
213
+ user_name TEXT NOT NULL,
214
+ hostname TEXT NOT NULL,
215
+ source_database TEXT NOT NULL,
216
+ target_database TEXT NOT NULL,
217
+ profile_name TEXT NOT NULL,
218
+ profile_version TEXT NOT NULL,
219
+ profile_hash TEXT NOT NULL,
220
+ tables_synced TEXT[] NOT NULL,
221
+ rows_anonymized JSONB NOT NULL,
222
+ strategies_applied JSONB NOT NULL,
223
+ verification_passed BOOLEAN NOT NULL,
224
+ verification_report TEXT NOT NULL,
225
+ signature TEXT NOT NULL,
226
+ created_at TIMESTAMPTZ DEFAULT NOW()
227
+ );
228
+
229
+ -- Ensure table is append-only by revoking dangerous permissions
230
+ REVOKE UPDATE, DELETE ON confiture_audit_log FROM PUBLIC;
231
+ """
232
+ )
233
+ self.conn.commit()
234
+
235
+ def log_sync(self, entry: AuditEntry) -> None:
236
+ """Append entry to audit log (immutable append-only).
237
+
238
+ This method appends a new audit entry. The entry cannot be
239
+ modified or deleted after insertion due to database constraints.
240
+
241
+ Args:
242
+ entry: AuditEntry to log
243
+
244
+ Raises:
245
+ psycopg.DatabaseError: If insertion fails
246
+ """
247
+ with self.conn.cursor() as cursor:
248
+ # Truncate microseconds to ensure consistent signature verification
249
+ ts = entry.timestamp.replace(microsecond=0)
250
+ cursor.execute(
251
+ """
252
+ INSERT INTO confiture_audit_log (
253
+ id, timestamp, user_name, hostname,
254
+ source_database, target_database,
255
+ profile_name, profile_version, profile_hash,
256
+ tables_synced, rows_anonymized, strategies_applied,
257
+ verification_passed, verification_report, signature
258
+ ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
259
+ """,
260
+ (
261
+ str(entry.id),
262
+ ts,
263
+ entry.user,
264
+ entry.hostname,
265
+ entry.source_database,
266
+ entry.target_database,
267
+ entry.profile_name,
268
+ entry.profile_version,
269
+ entry.profile_hash,
270
+ entry.tables_synced,
271
+ json.dumps(entry.rows_anonymized),
272
+ json.dumps(entry.strategies_applied),
273
+ entry.verification_passed,
274
+ entry.verification_report,
275
+ entry.signature,
276
+ ),
277
+ )
278
+ self.conn.commit()
279
+
280
+ def get_audit_log(self, limit: int = 100) -> list[AuditEntry]:
281
+ """Get recent audit log entries (for reporting).
282
+
283
+ Args:
284
+ limit: Maximum number of entries to return
285
+
286
+ Returns:
287
+ List of recent AuditEntry instances, newest first
288
+
289
+ Raises:
290
+ psycopg.DatabaseError: If query fails
291
+ """
292
+ with self.conn.cursor() as cursor:
293
+ cursor.execute(
294
+ """
295
+ SELECT
296
+ id, timestamp, user_name, hostname,
297
+ source_database, target_database,
298
+ profile_name, profile_version, profile_hash,
299
+ tables_synced, rows_anonymized, strategies_applied,
300
+ verification_passed, verification_report, signature
301
+ FROM confiture_audit_log
302
+ ORDER BY timestamp DESC
303
+ LIMIT %s
304
+ """,
305
+ (limit,),
306
+ )
307
+
308
+ entries = []
309
+ for row in cursor.fetchall():
310
+ # Normalize timestamp to UTC and remove microseconds for consistent signing
311
+ ts = row[1]
312
+ if ts and hasattr(ts, "astimezone"):
313
+ # Convert to UTC and truncate microseconds
314
+ ts = ts.astimezone(UTC).replace(microsecond=0)
315
+ entries.append(
316
+ AuditEntry(
317
+ id=row[0],
318
+ timestamp=ts,
319
+ user=row[2],
320
+ hostname=row[3],
321
+ source_database=row[4],
322
+ target_database=row[5],
323
+ profile_name=row[6],
324
+ profile_version=row[7],
325
+ profile_hash=row[8],
326
+ tables_synced=list(row[9]),
327
+ rows_anonymized=json.loads(row[10])
328
+ if isinstance(row[10], str)
329
+ else row[10],
330
+ strategies_applied=json.loads(row[11])
331
+ if isinstance(row[11], str)
332
+ else row[11],
333
+ verification_passed=row[12],
334
+ verification_report=row[13],
335
+ signature=row[14],
336
+ )
337
+ )
338
+
339
+ return entries
340
+
341
+
342
+ def sign_audit_entry(entry: AuditEntry, secret: str | None = None) -> str:
343
+ """Create HMAC signature for audit entry (prevents tampering).
344
+
345
+ The signature is computed over key fields of the audit entry.
346
+ If the entry is modified after signing, the signature will
347
+ no longer match, indicating tampering.
348
+
349
+ Args:
350
+ entry: AuditEntry to sign
351
+ secret: Secret key for HMAC (or uses AUDIT_LOG_SECRET env var)
352
+
353
+ Returns:
354
+ HMAC-SHA256 signature as hex string
355
+
356
+ Example:
357
+ >>> entry = AuditEntry(...)
358
+ >>> sig = sign_audit_entry(entry, secret="my-secret")
359
+ >>> # Later, verify by recomputing:
360
+ >>> sig2 = sign_audit_entry(modified_entry, secret="my-secret")
361
+ >>> assert sig == sig2 # Should fail if entry was modified
362
+ """
363
+ if secret is None:
364
+ secret = os.getenv("AUDIT_LOG_SECRET", "default-secret")
365
+
366
+ # Create deterministic JSON for signing
367
+ # Include only immutable/important fields
368
+ # Truncate microseconds to avoid precision issues when round-tripping through database
369
+ ts = entry.timestamp.replace(microsecond=0)
370
+ data = {
371
+ "id": str(entry.id),
372
+ "timestamp": ts.isoformat(),
373
+ "user": entry.user,
374
+ "hostname": entry.hostname,
375
+ "source_database": entry.source_database,
376
+ "target_database": entry.target_database,
377
+ "profile_name": entry.profile_name,
378
+ "profile_hash": entry.profile_hash,
379
+ "tables_synced": ",".join(sorted(entry.tables_synced)),
380
+ "rows_anonymized": sum(entry.rows_anonymized.values()),
381
+ "verification_passed": entry.verification_passed,
382
+ }
383
+
384
+ json_str = json.dumps(data, sort_keys=True)
385
+ signature = hmac.new(
386
+ secret.encode(),
387
+ json_str.encode(),
388
+ hashlib.sha256,
389
+ ).hexdigest()
390
+
391
+ return signature
392
+
393
+
394
+ def verify_audit_entry(entry: AuditEntry, secret: str | None = None) -> bool:
395
+ """Verify HMAC signature of audit entry (detect tampering).
396
+
397
+ Args:
398
+ entry: AuditEntry to verify
399
+ secret: Secret key for HMAC (or uses AUDIT_LOG_SECRET env var)
400
+
401
+ Returns:
402
+ True if signature is valid, False otherwise
403
+
404
+ Example:
405
+ >>> entry = logger.get_audit_log()[0]
406
+ >>> if verify_audit_entry(entry, secret="my-secret"):
407
+ ... print("Entry is authentic")
408
+ ... else:
409
+ ... print("Entry may have been tampered with!")
410
+ """
411
+ expected_sig = sign_audit_entry(entry, secret)
412
+ return entry.signature == expected_sig
413
+
414
+
415
+ def create_audit_entry(
416
+ user: str,
417
+ source_db: str,
418
+ target_db: str,
419
+ profile_name: str,
420
+ profile_version: str,
421
+ profile_hash: str,
422
+ tables: list[str],
423
+ rows_by_table: dict[str, int],
424
+ strategies_by_type: dict[str, int],
425
+ verification_passed: bool,
426
+ verification_report: str | None = None,
427
+ secret: str | None = None,
428
+ ) -> AuditEntry:
429
+ """Create and sign an audit entry (convenience function).
430
+
431
+ Args:
432
+ user: User who performed the operation
433
+ source_db: Source database identifier
434
+ target_db: Target database identifier
435
+ profile_name: Anonymization profile name
436
+ profile_version: Profile version
437
+ profile_hash: SHA256 hash of profile
438
+ tables: List of tables processed
439
+ rows_by_table: Dict of table → count anonymized
440
+ strategies_by_type: Dict of strategy → count applied
441
+ verification_passed: Whether verification succeeded
442
+ verification_report: Detailed verification results (JSON)
443
+ secret: Secret key for signature (or AUDIT_LOG_SECRET env var)
444
+
445
+ Returns:
446
+ Signed AuditEntry ready for logging
447
+
448
+ Example:
449
+ >>> entry = create_audit_entry(
450
+ ... user="admin@example.com",
451
+ ... source_db="prod_main",
452
+ ... target_db="staging_copy",
453
+ ... profile_name="production",
454
+ ... profile_version="1.0",
455
+ ... profile_hash="abc123",
456
+ ... tables=["users", "orders"],
457
+ ... rows_by_table={"users": 1000, "orders": 5000},
458
+ ... strategies_by_type={"email": 1000, "phone": 1000},
459
+ ... verification_passed=True,
460
+ ... verification_report="{}"
461
+ ... )
462
+ >>> logger.log_sync(entry)
463
+ """
464
+ entry = AuditEntry(
465
+ id=uuid4(),
466
+ timestamp=datetime.now(UTC),
467
+ user=user,
468
+ hostname=socket.gethostname(),
469
+ source_database=source_db,
470
+ target_database=target_db,
471
+ profile_name=profile_name,
472
+ profile_version=profile_version,
473
+ profile_hash=profile_hash,
474
+ tables_synced=tables,
475
+ rows_anonymized=rows_by_table,
476
+ strategies_applied=strategies_by_type,
477
+ verification_passed=verification_passed,
478
+ verification_report=verification_report or "{}",
479
+ signature="", # Will be computed below
480
+ )
481
+
482
+ # Sign the entry
483
+ entry.signature = sign_audit_entry(entry, secret)
484
+
485
+ return entry