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,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
|