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.
Files changed (124) hide show
  1. confiture/__init__.py +48 -0
  2. confiture/_core.cpython-311-darwin.so +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 +1893 -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 +184 -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 +882 -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 +24 -0
  95. confiture/models/lint.py +193 -0
  96. confiture/models/migration.py +265 -0
  97. confiture/models/schema.py +203 -0
  98. confiture/models/sql_file_migration.py +225 -0
  99. confiture/scenarios/__init__.py +36 -0
  100. confiture/scenarios/compliance.py +586 -0
  101. confiture/scenarios/ecommerce.py +199 -0
  102. confiture/scenarios/financial.py +253 -0
  103. confiture/scenarios/healthcare.py +315 -0
  104. confiture/scenarios/multi_tenant.py +340 -0
  105. confiture/scenarios/saas.py +295 -0
  106. confiture/testing/FRAMEWORK_API.md +722 -0
  107. confiture/testing/__init__.py +100 -0
  108. confiture/testing/fixtures/__init__.py +11 -0
  109. confiture/testing/fixtures/data_validator.py +229 -0
  110. confiture/testing/fixtures/migration_runner.py +167 -0
  111. confiture/testing/fixtures/schema_snapshotter.py +352 -0
  112. confiture/testing/frameworks/__init__.py +10 -0
  113. confiture/testing/frameworks/mutation.py +587 -0
  114. confiture/testing/frameworks/performance.py +479 -0
  115. confiture/testing/loader.py +225 -0
  116. confiture/testing/pytest/__init__.py +38 -0
  117. confiture/testing/pytest_plugin.py +190 -0
  118. confiture/testing/sandbox.py +304 -0
  119. confiture/testing/utils/__init__.py +0 -0
  120. fraiseql_confiture-0.3.7.dist-info/METADATA +438 -0
  121. fraiseql_confiture-0.3.7.dist-info/RECORD +124 -0
  122. fraiseql_confiture-0.3.7.dist-info/WHEEL +4 -0
  123. fraiseql_confiture-0.3.7.dist-info/entry_points.txt +4 -0
  124. fraiseql_confiture-0.3.7.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,652 @@
1
+ """Breach notification and incident management.
2
+
3
+ Provides automated breach detection, notification, and incident tracking
4
+ for compliance with regulations that require breach notification.
5
+
6
+ Supported Regulations:
7
+ - GDPR: Notify authority within 72 hours, notify individuals if high risk
8
+ - CCPA: Notify individuals without undue delay
9
+ - PIPEDA: Notify individuals of breach
10
+ - LGPD: Notify authority and individuals
11
+ - PIPL: Notify individuals and relevant authorities
12
+ - Privacy Act: Notify individuals
13
+ - POPIA: Notify regulator and data subjects
14
+
15
+ Features:
16
+ - Automatic breach detection from events
17
+ - Configurable incident severity levels
18
+ - Notification templates per regulation
19
+ - Audit trail for all notifications sent
20
+ - Escalation procedures
21
+ - Remediation tracking
22
+
23
+ Example:
24
+ >>> from confiture.core.anonymization.breach_notification import (
25
+ ... BreachNotificationManager, IncidentSeverity, NotificationChannel
26
+ ... )
27
+ >>>
28
+ >>> manager = BreachNotificationManager(conn)
29
+ >>> incident = manager.report_incident(
30
+ ... title="Unauthorized access to user table",
31
+ ... description="SQL injection detected in API endpoint",
32
+ ... affected_records=5000,
33
+ ... data_types=["email", "phone", "address"],
34
+ ... severity=IncidentSeverity.CRITICAL,
35
+ ... detected_by="Security Scanner"
36
+ ... )
37
+ >>>
38
+ >>> # Automatically send notifications per regulation
39
+ >>> notifications = manager.notify(
40
+ ... incident,
41
+ ... regulations=[Regulation.GDPR, Regulation.CCPA],
42
+ ... notify_authorities=True,
43
+ ... notify_subjects=True
44
+ ... )
45
+ """
46
+
47
+ import logging
48
+ from dataclasses import dataclass, field
49
+ from datetime import datetime, timedelta
50
+ from enum import Enum
51
+ from uuid import UUID, uuid4
52
+
53
+ import psycopg
54
+
55
+ logger = logging.getLogger(__name__)
56
+
57
+
58
+ class IncidentSeverity(Enum):
59
+ """Incident severity levels."""
60
+
61
+ LOW = "low"
62
+ """Minor security event, no action required."""
63
+
64
+ MEDIUM = "medium"
65
+ """Moderate security event, monitor and log."""
66
+
67
+ HIGH = "high"
68
+ """Serious security event, notify security team."""
69
+
70
+ CRITICAL = "critical"
71
+ """Severe breach, immediate action required."""
72
+
73
+
74
+ class NotificationChannel(Enum):
75
+ """Notification delivery channels."""
76
+
77
+ EMAIL = "email"
78
+ """Send via email."""
79
+
80
+ SMS = "sms"
81
+ """Send via SMS/text."""
82
+
83
+ WEBHOOK = "webhook"
84
+ """Send via webhook to external system."""
85
+
86
+ SYSLOG = "syslog"
87
+ """Send via syslog."""
88
+
89
+ REGULATORY = "regulatory"
90
+ """Send to regulatory authority."""
91
+
92
+
93
+ @dataclass
94
+ class IncidentReport:
95
+ """Security incident report."""
96
+
97
+ id: UUID
98
+ """Unique incident ID."""
99
+
100
+ title: str
101
+ """Brief incident title."""
102
+
103
+ description: str
104
+ """Detailed incident description."""
105
+
106
+ affected_records: int
107
+ """Number of records affected."""
108
+
109
+ data_types: list[str]
110
+ """Types of PII affected (email, phone, SSN, etc.)."""
111
+
112
+ severity: IncidentSeverity
113
+ """Incident severity level."""
114
+
115
+ detected_at: datetime
116
+ """When incident was detected."""
117
+
118
+ reported_by: str
119
+ """Who reported the incident."""
120
+
121
+ incident_category: str = "unauthorized_access"
122
+ """Type of incident (breach, loss, unauthorized_access, etc.)."""
123
+
124
+ root_cause: str | None = None
125
+ """Root cause analysis (if available)."""
126
+
127
+ remediation_plan: str | None = None
128
+ """Planned remediation steps."""
129
+
130
+ estimated_resolution: datetime | None = None
131
+ """Estimated resolution date."""
132
+
133
+ status: str = "open"
134
+ """Incident status (open, investigating, mitigated, resolved)."""
135
+
136
+ notifications_sent: dict[str, list[datetime]] = field(default_factory=dict)
137
+ """Notifications sent per regulation."""
138
+
139
+ affected_individuals: list[str] = field(default_factory=list)
140
+ """Email addresses of affected individuals (if known)."""
141
+
142
+ affected_tables: list[str] = field(default_factory=list)
143
+ """Database tables affected."""
144
+
145
+
146
+ @dataclass
147
+ class BreachNotification:
148
+ """Notification to be sent for a breach."""
149
+
150
+ incident_id: UUID
151
+ """Associated incident ID."""
152
+
153
+ recipient: str
154
+ """Email or identifier of recipient."""
155
+
156
+ recipient_type: str
157
+ """Type of recipient (authority, individual, system)."""
158
+
159
+ notification_channel: NotificationChannel
160
+ """How to deliver notification."""
161
+
162
+ subject: str
163
+ """Email subject or notification title."""
164
+
165
+ body: str
166
+ """Notification content."""
167
+
168
+ regulation: str
169
+ """Which regulation triggered this notification."""
170
+
171
+ deadline: datetime
172
+ """Regulatory deadline for notification."""
173
+
174
+ sent_at: datetime | None = None
175
+ """When notification was actually sent."""
176
+
177
+ delivery_status: str = "pending"
178
+ """Delivery status (pending, sent, failed, delivered)."""
179
+
180
+ confirmation: str | None = None
181
+ """Confirmation of receipt or error message."""
182
+
183
+
184
+ class BreachNotificationManager:
185
+ """Manage security incidents and breach notifications.
186
+
187
+ Tracks security incidents and automatically generates and sends
188
+ breach notifications according to regulatory requirements.
189
+
190
+ Features:
191
+ - Incident tracking and management
192
+ - Automatic notification based on regulations
193
+ - Multi-channel notification support
194
+ - Audit trail of all notifications
195
+ - Deadline tracking
196
+ - Remediation tracking
197
+
198
+ Regulations:
199
+ - GDPR: 72-hour authority notification, individual notification if high risk
200
+ - CCPA: Individual notification without undue delay
201
+ - PIPEDA: Individual notification (no authority requirement)
202
+ - LGPD: Authority and individual notification
203
+ - PIPL: Individual and authority notification
204
+ - Privacy Act: Individual notification
205
+ - POPIA: Individual and regulator notification
206
+ """
207
+
208
+ def __init__(self, conn: psycopg.Connection):
209
+ """Initialize breach notification manager.
210
+
211
+ Args:
212
+ conn: Database connection for storing incidents
213
+ """
214
+ self.conn = conn
215
+ self._ensure_incident_table()
216
+ self._ensure_notification_table()
217
+
218
+ def _ensure_incident_table(self) -> None:
219
+ """Create incident table if not exists."""
220
+ with self.conn.cursor() as cursor:
221
+ cursor.execute(
222
+ """
223
+ CREATE TABLE IF NOT EXISTS confiture_security_incidents (
224
+ id UUID PRIMARY KEY,
225
+ title TEXT NOT NULL,
226
+ description TEXT NOT NULL,
227
+ affected_records INTEGER NOT NULL,
228
+ data_types TEXT[] NOT NULL,
229
+ severity TEXT NOT NULL,
230
+ detected_at TIMESTAMPTZ NOT NULL,
231
+ reported_by TEXT NOT NULL,
232
+ incident_category TEXT NOT NULL,
233
+ root_cause TEXT,
234
+ remediation_plan TEXT,
235
+ estimated_resolution TIMESTAMPTZ,
236
+ status TEXT NOT NULL,
237
+ affected_tables TEXT[],
238
+ created_at TIMESTAMPTZ DEFAULT NOW()
239
+ );
240
+
241
+ CREATE INDEX IF NOT EXISTS idx_incidents_severity
242
+ ON confiture_security_incidents(severity);
243
+ CREATE INDEX IF NOT EXISTS idx_incidents_status
244
+ ON confiture_security_incidents(status);
245
+ CREATE INDEX IF NOT EXISTS idx_incidents_detected_at
246
+ ON confiture_security_incidents(detected_at DESC);
247
+ """
248
+ )
249
+ self.conn.commit()
250
+
251
+ def _ensure_notification_table(self) -> None:
252
+ """Create notification table if not exists (append-only)."""
253
+ with self.conn.cursor() as cursor:
254
+ cursor.execute(
255
+ """
256
+ CREATE TABLE IF NOT EXISTS confiture_breach_notifications (
257
+ id UUID PRIMARY KEY,
258
+ incident_id UUID NOT NULL,
259
+ recipient TEXT NOT NULL,
260
+ recipient_type TEXT NOT NULL,
261
+ notification_channel TEXT NOT NULL,
262
+ subject TEXT NOT NULL,
263
+ body TEXT NOT NULL,
264
+ regulation TEXT NOT NULL,
265
+ deadline TIMESTAMPTZ NOT NULL,
266
+ sent_at TIMESTAMPTZ,
267
+ delivery_status TEXT NOT NULL,
268
+ confirmation TEXT,
269
+ created_at TIMESTAMPTZ DEFAULT NOW(),
270
+ FOREIGN KEY (incident_id) REFERENCES confiture_security_incidents(id)
271
+ );
272
+
273
+ CREATE INDEX IF NOT EXISTS idx_notifications_incident
274
+ ON confiture_breach_notifications(incident_id);
275
+ CREATE INDEX IF NOT EXISTS idx_notifications_status
276
+ ON confiture_breach_notifications(delivery_status);
277
+ CREATE INDEX IF NOT EXISTS idx_notifications_deadline
278
+ ON confiture_breach_notifications(deadline);
279
+
280
+ -- Append-only constraint
281
+ REVOKE UPDATE, DELETE ON confiture_breach_notifications FROM PUBLIC;
282
+ """
283
+ )
284
+ self.conn.commit()
285
+
286
+ def report_incident(
287
+ self,
288
+ title: str,
289
+ description: str,
290
+ affected_records: int,
291
+ data_types: list[str],
292
+ severity: IncidentSeverity,
293
+ reported_by: str,
294
+ incident_category: str = "unauthorized_access",
295
+ root_cause: str | None = None,
296
+ affected_tables: list[str] | None = None,
297
+ ) -> IncidentReport:
298
+ """Report a security incident.
299
+
300
+ Args:
301
+ title: Brief incident title
302
+ description: Detailed description
303
+ affected_records: Number of records affected
304
+ data_types: Types of PII affected
305
+ severity: Incident severity
306
+ reported_by: Who reported the incident
307
+ incident_category: Category of incident
308
+ root_cause: Root cause (if known)
309
+ affected_tables: Database tables affected
310
+
311
+ Returns:
312
+ IncidentReport instance
313
+ """
314
+ incident_id = uuid4()
315
+ now = datetime.now()
316
+
317
+ incident = IncidentReport(
318
+ id=incident_id,
319
+ title=title,
320
+ description=description,
321
+ affected_records=affected_records,
322
+ data_types=data_types,
323
+ severity=severity,
324
+ detected_at=now,
325
+ reported_by=reported_by,
326
+ incident_category=incident_category,
327
+ root_cause=root_cause,
328
+ affected_tables=affected_tables or [],
329
+ )
330
+
331
+ # Store in database
332
+ with self.conn.cursor() as cursor:
333
+ cursor.execute(
334
+ """
335
+ INSERT INTO confiture_security_incidents (
336
+ id, title, description, affected_records, data_types,
337
+ severity, detected_at, reported_by, incident_category,
338
+ root_cause, affected_tables, status
339
+ ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
340
+ """,
341
+ (
342
+ str(incident_id),
343
+ title,
344
+ description,
345
+ affected_records,
346
+ data_types,
347
+ severity.value,
348
+ now,
349
+ reported_by,
350
+ incident_category,
351
+ root_cause,
352
+ affected_tables or [],
353
+ "open",
354
+ ),
355
+ )
356
+ self.conn.commit()
357
+
358
+ logger.error(
359
+ f"Security incident reported: {title} "
360
+ f"({affected_records} records, severity: {severity.value})"
361
+ )
362
+
363
+ return incident
364
+
365
+ def notify(
366
+ self,
367
+ incident: IncidentReport,
368
+ regulations: list[str],
369
+ notify_authorities: bool = True,
370
+ notify_subjects: bool = True,
371
+ ) -> list[BreachNotification]:
372
+ """Generate and send breach notifications.
373
+
374
+ Args:
375
+ incident: Incident to notify about
376
+ regulations: Which regulations to follow
377
+ notify_authorities: Send to regulatory authorities
378
+ notify_subjects: Send to affected individuals
379
+
380
+ Returns:
381
+ List of notifications sent
382
+ """
383
+ notifications = []
384
+
385
+ for regulation in regulations:
386
+ # Generate notifications for each regulation
387
+ regs_notifs = self._generate_notifications(
388
+ incident, regulation, notify_authorities, notify_subjects
389
+ )
390
+ notifications.extend(regs_notifs)
391
+
392
+ logger.info(
393
+ f"Generated {len(notifications)} breach notifications for incident {incident.id}"
394
+ )
395
+
396
+ return notifications
397
+
398
+ def _generate_notifications(
399
+ self,
400
+ incident: IncidentReport,
401
+ regulation: str,
402
+ notify_authorities: bool,
403
+ notify_subjects: bool,
404
+ ) -> list[BreachNotification]:
405
+ """Generate notifications for a specific regulation.
406
+
407
+ Args:
408
+ incident: Incident to notify about
409
+ regulation: Which regulation
410
+ notify_authorities: Notify authorities
411
+ notify_subjects: Notify subjects
412
+
413
+ Returns:
414
+ List of notifications
415
+ """
416
+ notifications = []
417
+
418
+ # Determine notification requirements per regulation
419
+ if regulation == "gdpr":
420
+ # GDPR: 72-hour authority notification
421
+ deadline = incident.detected_at + timedelta(hours=72)
422
+
423
+ if notify_authorities:
424
+ notif = BreachNotification(
425
+ incident_id=incident.id,
426
+ recipient="dpa@authority.eu", # DPA placeholder
427
+ recipient_type="authority",
428
+ notification_channel=NotificationChannel.EMAIL,
429
+ subject=f"GDPR Breach Notification - {incident.title}",
430
+ body=self._generate_gdpr_authority_notice(incident),
431
+ regulation="GDPR",
432
+ deadline=deadline,
433
+ )
434
+ notifications.append(notif)
435
+
436
+ if notify_subjects and incident.affected_individuals:
437
+ for individual in incident.affected_individuals[:100]: # Limit batch
438
+ notif = BreachNotification(
439
+ incident_id=incident.id,
440
+ recipient=individual,
441
+ recipient_type="individual",
442
+ notification_channel=NotificationChannel.EMAIL,
443
+ subject="Important: Your Data Security Notice",
444
+ body=self._generate_gdpr_individual_notice(incident),
445
+ regulation="GDPR",
446
+ deadline=deadline,
447
+ )
448
+ notifications.append(notif)
449
+
450
+ elif regulation == "ccpa":
451
+ # CCPA: Individual notification without undue delay
452
+ deadline = incident.detected_at + timedelta(days=5)
453
+
454
+ if notify_subjects and incident.affected_individuals:
455
+ for individual in incident.affected_individuals[:100]:
456
+ notif = BreachNotification(
457
+ incident_id=incident.id,
458
+ recipient=individual,
459
+ recipient_type="individual",
460
+ notification_channel=NotificationChannel.EMAIL,
461
+ subject="CCPA Data Breach Notification",
462
+ body=self._generate_ccpa_notice(incident),
463
+ regulation="CCPA",
464
+ deadline=deadline,
465
+ )
466
+ notifications.append(notif)
467
+
468
+ # Store notifications in database
469
+ for notif in notifications:
470
+ self._store_notification(notif)
471
+
472
+ return notifications
473
+
474
+ def _generate_gdpr_authority_notice(self, incident: IncidentReport) -> str:
475
+ """Generate GDPR authority breach notice."""
476
+ return f"""
477
+ GDPR DATA BREACH NOTIFICATION
478
+
479
+ Incident ID: {incident.id}
480
+ Title: {incident.title}
481
+ Detected: {incident.detected_at.isoformat()}
482
+
483
+ Description: {incident.description}
484
+
485
+ Affected Records: {incident.affected_records}
486
+ Data Categories: {", ".join(incident.data_types)}
487
+ Severity: {incident.severity.value}
488
+
489
+ Root Cause: {incident.root_cause or "Under investigation"}
490
+ Remediation Plan: {incident.remediation_plan or "To be determined"}
491
+
492
+ All affected individuals will be notified as required under Article 34.
493
+ """
494
+
495
+ def _generate_gdpr_individual_notice(self, incident: IncidentReport) -> str:
496
+ """Generate GDPR individual breach notice."""
497
+ return f"""
498
+ DATA BREACH NOTIFICATION
499
+
500
+ Dear Valued Customer,
501
+
502
+ We are writing to inform you about a security incident that may affect your personal data.
503
+
504
+ Incident: {incident.title}
505
+ Date Discovered: {incident.detected_at.strftime("%B %d, %Y")}
506
+
507
+ What Happened: {incident.description}
508
+
509
+ What Information May Have Been Affected:
510
+ {chr(10).join(f"- {t}" for t in incident.data_types)}
511
+
512
+ What We Are Doing:
513
+ {incident.remediation_plan or "We are investigating this incident and taking appropriate measures to prevent future occurrences."}
514
+
515
+ What You Can Do:
516
+ - Monitor your accounts for suspicious activity
517
+ - Consider changing passwords for important accounts
518
+ - Consider identity protection services
519
+
520
+ For more information, please contact: privacy@example.com
521
+ """
522
+
523
+ def _generate_ccpa_notice(self, incident: IncidentReport) -> str:
524
+ """Generate CCPA breach notice."""
525
+ return f"""
526
+ CCPA DATA BREACH NOTIFICATION
527
+
528
+ A security incident has affected your personal information.
529
+
530
+ Details:
531
+ - Description: {incident.description}
532
+ - Affected Information: {", ".join(incident.data_types)}
533
+ - Records Affected: {incident.affected_records}
534
+
535
+ California law requires us to notify you of this incident.
536
+
537
+ Actions You Can Take:
538
+ 1. Review your credit reports
539
+ 2. Place a fraud alert
540
+ 3. Consider a credit freeze
541
+
542
+ Questions? Contact: privacy@example.com
543
+ """
544
+
545
+ def _store_notification(self, notification: BreachNotification) -> None:
546
+ """Store notification in database."""
547
+ with self.conn.cursor() as cursor:
548
+ cursor.execute(
549
+ """
550
+ INSERT INTO confiture_breach_notifications (
551
+ id, incident_id, recipient, recipient_type,
552
+ notification_channel, subject, body, regulation, deadline,
553
+ delivery_status
554
+ ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
555
+ """,
556
+ (
557
+ str(uuid4()),
558
+ str(notification.incident_id),
559
+ notification.recipient,
560
+ notification.recipient_type,
561
+ notification.notification_channel.value,
562
+ notification.subject,
563
+ notification.body,
564
+ notification.regulation,
565
+ notification.deadline,
566
+ notification.delivery_status,
567
+ ),
568
+ )
569
+ self.conn.commit()
570
+
571
+ def get_incident(self, incident_id: UUID) -> IncidentReport | None:
572
+ """Retrieve an incident by ID.
573
+
574
+ Args:
575
+ incident_id: Incident ID to retrieve
576
+
577
+ Returns:
578
+ IncidentReport or None if not found
579
+ """
580
+ with self.conn.cursor() as cursor:
581
+ cursor.execute(
582
+ """
583
+ SELECT id, title, description, affected_records, data_types,
584
+ severity, detected_at, reported_by, incident_category,
585
+ root_cause, affected_tables, status
586
+ FROM confiture_security_incidents
587
+ WHERE id = %s
588
+ """,
589
+ (str(incident_id),),
590
+ )
591
+ row = cursor.fetchone()
592
+
593
+ if not row:
594
+ return None
595
+
596
+ return IncidentReport(
597
+ id=row[0],
598
+ title=row[1],
599
+ description=row[2],
600
+ affected_records=row[3],
601
+ data_types=row[4],
602
+ severity=IncidentSeverity(row[5]),
603
+ detected_at=row[6],
604
+ reported_by=row[7],
605
+ incident_category=row[8],
606
+ root_cause=row[9],
607
+ affected_tables=row[10],
608
+ status=row[11],
609
+ )
610
+
611
+ def get_notifications_for_incident(self, incident_id: UUID) -> list[BreachNotification]:
612
+ """Get all notifications for an incident.
613
+
614
+ Args:
615
+ incident_id: Incident ID
616
+
617
+ Returns:
618
+ List of notifications
619
+ """
620
+ with self.conn.cursor() as cursor:
621
+ cursor.execute(
622
+ """
623
+ SELECT id, incident_id, recipient, recipient_type,
624
+ notification_channel, subject, body, regulation,
625
+ deadline, sent_at, delivery_status, confirmation
626
+ FROM confiture_breach_notifications
627
+ WHERE incident_id = %s
628
+ ORDER BY created_at DESC
629
+ """,
630
+ (str(incident_id),),
631
+ )
632
+ rows = cursor.fetchall()
633
+
634
+ notifications = []
635
+ for row in rows:
636
+ notifications.append(
637
+ BreachNotification(
638
+ incident_id=row[1],
639
+ recipient=row[2],
640
+ recipient_type=row[3],
641
+ notification_channel=NotificationChannel(row[4]),
642
+ subject=row[5],
643
+ body=row[6],
644
+ regulation=row[7],
645
+ deadline=row[8],
646
+ sent_at=row[9],
647
+ delivery_status=row[10],
648
+ confirmation=row[11],
649
+ )
650
+ )
651
+
652
+ return notifications