security-use 0.1.1__py3-none-any.whl → 0.2.9__py3-none-any.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 (45) hide show
  1. security_use/__init__.py +9 -1
  2. security_use/auth/__init__.py +16 -0
  3. security_use/auth/client.py +223 -0
  4. security_use/auth/config.py +177 -0
  5. security_use/auth/oauth.py +317 -0
  6. security_use/cli.py +699 -34
  7. security_use/compliance/__init__.py +10 -0
  8. security_use/compliance/mapper.py +275 -0
  9. security_use/compliance/models.py +50 -0
  10. security_use/dependency_scanner.py +76 -30
  11. security_use/fixers/iac_fixer.py +173 -95
  12. security_use/iac/rules/azure.py +246 -0
  13. security_use/iac/rules/gcp.py +255 -0
  14. security_use/iac/rules/kubernetes.py +429 -0
  15. security_use/iac/rules/registry.py +56 -0
  16. security_use/parsers/__init__.py +18 -0
  17. security_use/parsers/base.py +2 -0
  18. security_use/parsers/composer.py +101 -0
  19. security_use/parsers/conda.py +97 -0
  20. security_use/parsers/dotnet.py +89 -0
  21. security_use/parsers/gradle.py +90 -0
  22. security_use/parsers/maven.py +108 -0
  23. security_use/parsers/npm.py +196 -0
  24. security_use/parsers/yarn.py +108 -0
  25. security_use/reporter.py +29 -1
  26. security_use/sbom/__init__.py +10 -0
  27. security_use/sbom/generator.py +340 -0
  28. security_use/sbom/models.py +40 -0
  29. security_use/scanner.py +15 -2
  30. security_use/sensor/__init__.py +125 -0
  31. security_use/sensor/alert_queue.py +207 -0
  32. security_use/sensor/config.py +217 -0
  33. security_use/sensor/dashboard_alerter.py +246 -0
  34. security_use/sensor/detector.py +415 -0
  35. security_use/sensor/endpoint_analyzer.py +339 -0
  36. security_use/sensor/middleware.py +521 -0
  37. security_use/sensor/models.py +140 -0
  38. security_use/sensor/webhook.py +227 -0
  39. security_use-0.2.9.dist-info/METADATA +531 -0
  40. security_use-0.2.9.dist-info/RECORD +60 -0
  41. security_use-0.2.9.dist-info/licenses/LICENSE +21 -0
  42. security_use-0.1.1.dist-info/METADATA +0 -92
  43. security_use-0.1.1.dist-info/RECORD +0 -30
  44. {security_use-0.1.1.dist-info → security_use-0.2.9.dist-info}/WHEEL +0 -0
  45. {security_use-0.1.1.dist-info → security_use-0.2.9.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,521 @@
1
+ """Framework middleware adapters for security monitoring."""
2
+
3
+ import asyncio
4
+ import logging
5
+ from io import BytesIO
6
+ from typing import Any, Callable, Optional
7
+ from urllib.parse import parse_qs
8
+
9
+ from .alert_queue import get_alert_queue
10
+ from .config import SensorConfig, create_config
11
+ from .dashboard_alerter import DashboardAlerter
12
+ from .detector import AttackDetector
13
+ from .models import ActionTaken, RequestData
14
+ from .webhook import WebhookAlerter
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class SecurityMiddleware:
20
+ """ASGI middleware for FastAPI/Starlette security monitoring.
21
+
22
+ Usage with dashboard (recommended):
23
+ from fastapi import FastAPI
24
+ from security_use.sensor import SecurityMiddleware
25
+
26
+ app = FastAPI()
27
+ app.add_middleware(
28
+ SecurityMiddleware,
29
+ api_key="su_...", # Or set SECURITY_USE_API_KEY env var
30
+ block_on_detection=True,
31
+ )
32
+
33
+ Usage with auto-detection of vulnerable endpoints:
34
+ app.add_middleware(
35
+ SecurityMiddleware,
36
+ auto_detect_vulnerable=True,
37
+ project_path="./",
38
+ )
39
+
40
+ Legacy usage with webhook:
41
+ app.add_middleware(
42
+ SecurityMiddleware,
43
+ webhook_url="https://your-webhook.com/alerts",
44
+ )
45
+ """
46
+
47
+ def __init__(
48
+ self,
49
+ app: Any,
50
+ api_key: Optional[str] = None,
51
+ webhook_url: Optional[str] = None,
52
+ block_on_detection: bool = True,
53
+ excluded_paths: Optional[list[str]] = None,
54
+ watch_paths: Optional[list[str]] = None,
55
+ auto_detect_vulnerable: bool = False,
56
+ project_path: Optional[str] = None,
57
+ enabled_detectors: Optional[list[str]] = None,
58
+ rate_limit_threshold: int = 100,
59
+ config: Optional[SensorConfig] = None,
60
+ ):
61
+ """Initialize the security middleware.
62
+
63
+ Args:
64
+ app: The ASGI application.
65
+ api_key: SecurityUse API key for dashboard alerting.
66
+ webhook_url: URL to send alerts to (legacy).
67
+ block_on_detection: Return 403 on attack detection.
68
+ excluded_paths: Paths to skip monitoring.
69
+ watch_paths: Only monitor these paths (None = all).
70
+ auto_detect_vulnerable: Auto-detect vulnerable endpoints.
71
+ project_path: Project path for auto-detection.
72
+ enabled_detectors: List of detector types to enable.
73
+ rate_limit_threshold: Requests per minute per IP.
74
+ config: Optional pre-configured SensorConfig.
75
+ """
76
+ self.app = app
77
+
78
+ if config:
79
+ self.config = config
80
+ else:
81
+ self.config = create_config(
82
+ api_key=api_key,
83
+ webhook_url=webhook_url,
84
+ block_on_detection=block_on_detection,
85
+ excluded_paths=excluded_paths,
86
+ watch_paths=watch_paths,
87
+ auto_detect_vulnerable=auto_detect_vulnerable,
88
+ project_path=project_path,
89
+ enabled_detectors=enabled_detectors,
90
+ rate_limit_threshold=rate_limit_threshold,
91
+ )
92
+
93
+ # Auto-detect vulnerable endpoints if requested
94
+ if self.config.auto_detect_vulnerable and self.config.project_path:
95
+ self._detect_vulnerable_endpoints()
96
+
97
+ self.detector = AttackDetector(
98
+ enabled_detectors=self.config.enabled_detectors,
99
+ rate_limit_threshold=self.config.rate_limit_threshold,
100
+ rate_limit_window=self.config.rate_limit_window,
101
+ rate_limit_cleanup_interval=self.config.rate_limit_cleanup_interval,
102
+ rate_limit_max_ips=self.config.rate_limit_max_ips,
103
+ )
104
+
105
+ # Set up alerters based on config
106
+ self.dashboard_alerter: Optional[DashboardAlerter] = None
107
+ self.webhook_alerter: Optional[WebhookAlerter] = None
108
+
109
+ if self.config.alert_mode in ("dashboard", "both"):
110
+ self.dashboard_alerter = DashboardAlerter(
111
+ api_key=self.config.api_key,
112
+ dashboard_url=self.config.dashboard_url,
113
+ timeout=self.config.webhook_timeout,
114
+ retry_count=self.config.webhook_retry_count,
115
+ )
116
+
117
+ if self.config.alert_mode in ("webhook", "both") and self.config.webhook_url:
118
+ self.webhook_alerter = WebhookAlerter(
119
+ webhook_url=self.config.webhook_url,
120
+ retry_count=self.config.webhook_retry_count,
121
+ timeout=self.config.webhook_timeout,
122
+ headers=self.config.webhook_headers,
123
+ )
124
+
125
+ # Log configuration
126
+ if self.config.watch_paths:
127
+ logger.info(f"SecurityMiddleware monitoring {len(self.config.watch_paths)} paths")
128
+ else:
129
+ logger.info("SecurityMiddleware monitoring all paths")
130
+
131
+ def _detect_vulnerable_endpoints(self) -> None:
132
+ """Auto-detect vulnerable endpoints from project scan."""
133
+ try:
134
+ from .endpoint_analyzer import VulnerableEndpointDetector
135
+
136
+ detector = VulnerableEndpointDetector()
137
+ paths = detector.get_watch_paths(self.config.project_path)
138
+
139
+ if paths:
140
+ # Merge with existing watch_paths
141
+ existing = set(self.config.watch_paths or [])
142
+ self.config.watch_paths = list(existing | set(paths))
143
+ logger.info(f"Auto-detected {len(paths)} vulnerable endpoints to monitor")
144
+ else:
145
+ logger.info("No vulnerable endpoints detected")
146
+
147
+ except Exception as e:
148
+ logger.warning(f"Failed to auto-detect vulnerable endpoints: {e}")
149
+
150
+ async def __call__(self, scope: dict, receive: Callable, send: Callable) -> None:
151
+ """ASGI interface."""
152
+ if scope["type"] != "http":
153
+ await self.app(scope, receive, send)
154
+ return
155
+
156
+ # Extract request data
157
+ path = scope.get("path", "/")
158
+
159
+ # Check if path should be monitored
160
+ if not self.config.should_monitor_path(path):
161
+ await self.app(scope, receive, send)
162
+ return
163
+
164
+ # Buffer the body and create a replay receive function
165
+ body, receive_wrapper = await self._buffer_body(receive)
166
+
167
+ # Build request data for analysis
168
+ request_data = self._build_request_data(scope, body)
169
+
170
+ # Analyze for attacks
171
+ events = self.detector.analyze_request(request_data)
172
+
173
+ if events:
174
+ action = (
175
+ ActionTaken.BLOCKED
176
+ if self.config.block_on_detection
177
+ else ActionTaken.LOGGED
178
+ )
179
+
180
+ # Send alerts asynchronously
181
+ for event in events:
182
+ if self.dashboard_alerter:
183
+ asyncio.create_task(self.dashboard_alerter.send_alert(event, action))
184
+ if self.webhook_alerter:
185
+ asyncio.create_task(self.webhook_alerter.send_alert(event, action))
186
+
187
+ if self.config.block_on_detection:
188
+ # Return 403 Forbidden
189
+ await self._send_blocked_response(send, events[0])
190
+ return
191
+
192
+ # Continue to application with replay receive that provides the buffered body
193
+ await self.app(scope, receive_wrapper, send)
194
+
195
+ async def _buffer_body(self, receive: Callable) -> tuple[bytes, Callable]:
196
+ """Buffer the entire request body and create a replay receive function.
197
+
198
+ Returns:
199
+ Tuple of (full_body, receive_wrapper) where receive_wrapper
200
+ can be passed to the app to replay the body.
201
+ """
202
+ body_parts = []
203
+
204
+ # Read all body chunks
205
+ while True:
206
+ message = await receive()
207
+ if message["type"] == "http.request":
208
+ body_parts.append(message.get("body", b""))
209
+ if not message.get("more_body", False):
210
+ break
211
+ elif message["type"] == "http.disconnect":
212
+ break
213
+
214
+ full_body = b"".join(body_parts)
215
+
216
+ # Handle large bodies - truncate for analysis if needed
217
+ max_body_size = getattr(self.config, "max_body_size", 1024 * 1024)
218
+ analysis_body = full_body[:max_body_size] if len(full_body) > max_body_size else full_body
219
+
220
+ # Create a receive function that replays the buffered body
221
+ body_sent = False
222
+
223
+ async def receive_wrapper() -> dict:
224
+ nonlocal body_sent
225
+ if not body_sent:
226
+ body_sent = True
227
+ return {
228
+ "type": "http.request",
229
+ "body": full_body,
230
+ "more_body": False,
231
+ }
232
+ # After body is sent, wait for disconnect or return empty
233
+ return {
234
+ "type": "http.request",
235
+ "body": b"",
236
+ "more_body": False,
237
+ }
238
+
239
+ return analysis_body, receive_wrapper
240
+
241
+ def _build_request_data(self, scope: dict, body: bytes) -> RequestData:
242
+ """Build request data from ASGI scope and buffered body."""
243
+ method = scope.get("method", "GET")
244
+ path = scope.get("path", "/")
245
+ query_string = scope.get("query_string", b"").decode("utf-8")
246
+
247
+ # Parse query params
248
+ query_params = {}
249
+ if query_string:
250
+ parsed = parse_qs(query_string)
251
+ query_params = {k: v[0] if len(v) == 1 else v for k, v in parsed.items()}
252
+
253
+ # Extract headers
254
+ headers = {}
255
+ for key, value in scope.get("headers", []):
256
+ headers[key.decode("utf-8").lower()] = value.decode("utf-8")
257
+
258
+ # Get client IP
259
+ client = scope.get("client")
260
+ source_ip = client[0] if client else "unknown"
261
+
262
+ # Check for forwarded IP
263
+ if "x-forwarded-for" in headers:
264
+ source_ip = headers["x-forwarded-for"].split(",")[0].strip()
265
+ elif "x-real-ip" in headers:
266
+ source_ip = headers["x-real-ip"]
267
+
268
+ return RequestData(
269
+ method=method,
270
+ path=path,
271
+ query_params=query_params,
272
+ headers=headers,
273
+ body=body.decode("utf-8", errors="replace") if body else None,
274
+ source_ip=source_ip,
275
+ )
276
+
277
+ async def _send_blocked_response(self, send: Callable, event: Any) -> None:
278
+ """Send a 403 Forbidden response."""
279
+ body = b'{"error": "Request blocked due to security policy"}'
280
+
281
+ await send(
282
+ {
283
+ "type": "http.response.start",
284
+ "status": 403,
285
+ "headers": [
286
+ (b"content-type", b"application/json"),
287
+ (b"content-length", str(len(body)).encode()),
288
+ ],
289
+ }
290
+ )
291
+ await send(
292
+ {
293
+ "type": "http.response.body",
294
+ "body": body,
295
+ }
296
+ )
297
+
298
+
299
+ class FlaskSecurityMiddleware:
300
+ """WSGI middleware for Flask security monitoring.
301
+
302
+ Usage with dashboard (recommended):
303
+ from flask import Flask
304
+ from security_use.sensor import FlaskSecurityMiddleware
305
+
306
+ app = Flask(__name__)
307
+ app.wsgi_app = FlaskSecurityMiddleware(
308
+ app.wsgi_app,
309
+ api_key="su_...", # Or set SECURITY_USE_API_KEY env var
310
+ )
311
+
312
+ Usage with auto-detection:
313
+ app.wsgi_app = FlaskSecurityMiddleware(
314
+ app.wsgi_app,
315
+ auto_detect_vulnerable=True,
316
+ project_path="./",
317
+ )
318
+ """
319
+
320
+ def __init__(
321
+ self,
322
+ app: Any,
323
+ api_key: Optional[str] = None,
324
+ webhook_url: Optional[str] = None,
325
+ block_on_detection: bool = True,
326
+ excluded_paths: Optional[list[str]] = None,
327
+ watch_paths: Optional[list[str]] = None,
328
+ auto_detect_vulnerable: bool = False,
329
+ project_path: Optional[str] = None,
330
+ enabled_detectors: Optional[list[str]] = None,
331
+ rate_limit_threshold: int = 100,
332
+ config: Optional[SensorConfig] = None,
333
+ ):
334
+ """Initialize the Flask security middleware.
335
+
336
+ Args:
337
+ app: The WSGI application.
338
+ api_key: SecurityUse API key for dashboard alerting.
339
+ webhook_url: URL to send alerts to (legacy).
340
+ block_on_detection: Return 403 on attack detection.
341
+ excluded_paths: Paths to skip monitoring.
342
+ watch_paths: Only monitor these paths (None = all).
343
+ auto_detect_vulnerable: Auto-detect vulnerable endpoints.
344
+ project_path: Project path for auto-detection.
345
+ enabled_detectors: List of detector types to enable.
346
+ rate_limit_threshold: Requests per minute per IP.
347
+ config: Optional pre-configured SensorConfig.
348
+ """
349
+ self.app = app
350
+
351
+ if config:
352
+ self.config = config
353
+ else:
354
+ self.config = create_config(
355
+ api_key=api_key,
356
+ webhook_url=webhook_url,
357
+ block_on_detection=block_on_detection,
358
+ excluded_paths=excluded_paths,
359
+ watch_paths=watch_paths,
360
+ auto_detect_vulnerable=auto_detect_vulnerable,
361
+ project_path=project_path,
362
+ enabled_detectors=enabled_detectors,
363
+ rate_limit_threshold=rate_limit_threshold,
364
+ )
365
+
366
+ # Auto-detect vulnerable endpoints if requested
367
+ if self.config.auto_detect_vulnerable and self.config.project_path:
368
+ self._detect_vulnerable_endpoints()
369
+
370
+ self.detector = AttackDetector(
371
+ enabled_detectors=self.config.enabled_detectors,
372
+ rate_limit_threshold=self.config.rate_limit_threshold,
373
+ rate_limit_window=self.config.rate_limit_window,
374
+ rate_limit_cleanup_interval=self.config.rate_limit_cleanup_interval,
375
+ rate_limit_max_ips=self.config.rate_limit_max_ips,
376
+ )
377
+
378
+ # Set up alerters based on config
379
+ self.dashboard_alerter: Optional[DashboardAlerter] = None
380
+ self.webhook_alerter: Optional[WebhookAlerter] = None
381
+
382
+ if self.config.alert_mode in ("dashboard", "both"):
383
+ self.dashboard_alerter = DashboardAlerter(
384
+ api_key=self.config.api_key,
385
+ dashboard_url=self.config.dashboard_url,
386
+ timeout=self.config.webhook_timeout,
387
+ retry_count=self.config.webhook_retry_count,
388
+ )
389
+
390
+ if self.config.alert_mode in ("webhook", "both") and self.config.webhook_url:
391
+ self.webhook_alerter = WebhookAlerter(
392
+ webhook_url=self.config.webhook_url,
393
+ retry_count=self.config.webhook_retry_count,
394
+ timeout=self.config.webhook_timeout,
395
+ headers=self.config.webhook_headers,
396
+ )
397
+
398
+ # Get shared alert queue for non-blocking alert delivery
399
+ self._alert_queue = get_alert_queue()
400
+
401
+ # Log configuration
402
+ if self.config.watch_paths:
403
+ logger.info(f"FlaskSecurityMiddleware monitoring {len(self.config.watch_paths)} paths")
404
+ else:
405
+ logger.info("FlaskSecurityMiddleware monitoring all paths")
406
+
407
+ def _detect_vulnerable_endpoints(self) -> None:
408
+ """Auto-detect vulnerable endpoints from project scan."""
409
+ try:
410
+ from .endpoint_analyzer import VulnerableEndpointDetector
411
+
412
+ detector = VulnerableEndpointDetector()
413
+ paths = detector.get_watch_paths(self.config.project_path)
414
+
415
+ if paths:
416
+ existing = set(self.config.watch_paths or [])
417
+ self.config.watch_paths = list(existing | set(paths))
418
+ logger.info(f"Auto-detected {len(paths)} vulnerable endpoints to monitor")
419
+ else:
420
+ logger.info("No vulnerable endpoints detected")
421
+
422
+ except Exception as e:
423
+ logger.warning(f"Failed to auto-detect vulnerable endpoints: {e}")
424
+
425
+ def __call__(self, environ: dict, start_response: Callable) -> Any:
426
+ """WSGI interface."""
427
+ path = environ.get("PATH_INFO", "/")
428
+
429
+ # Check if path should be monitored
430
+ if not self.config.should_monitor_path(path):
431
+ return self.app(environ, start_response)
432
+
433
+ request_data = self._extract_request_data(environ)
434
+
435
+ # Analyze for attacks
436
+ events = self.detector.analyze_request(request_data)
437
+
438
+ if events:
439
+ action = (
440
+ ActionTaken.BLOCKED
441
+ if self.config.block_on_detection
442
+ else ActionTaken.LOGGED
443
+ )
444
+
445
+ # Queue alerts for background sending (non-blocking)
446
+ for event in events:
447
+ if self.dashboard_alerter:
448
+ self._alert_queue.enqueue(event, action, self.dashboard_alerter)
449
+ if self.webhook_alerter:
450
+ self._alert_queue.enqueue(event, action, self.webhook_alerter)
451
+
452
+ if self.config.block_on_detection:
453
+ # Return 403 Forbidden
454
+ return self._blocked_response(start_response)
455
+
456
+ return self.app(environ, start_response)
457
+
458
+ def _extract_request_data(self, environ: dict) -> RequestData:
459
+ """Extract request data from WSGI environ."""
460
+ method = environ.get("REQUEST_METHOD", "GET")
461
+ path = environ.get("PATH_INFO", "/")
462
+ query_string = environ.get("QUERY_STRING", "")
463
+
464
+ # Parse query params
465
+ query_params = {}
466
+ if query_string:
467
+ parsed = parse_qs(query_string)
468
+ query_params = {k: v[0] if len(v) == 1 else v for k, v in parsed.items()}
469
+
470
+ # Extract headers
471
+ headers = {}
472
+ for key, value in environ.items():
473
+ if key.startswith("HTTP_"):
474
+ header_name = key[5:].lower().replace("_", "-")
475
+ headers[header_name] = value
476
+ elif key in ("CONTENT_TYPE", "CONTENT_LENGTH"):
477
+ headers[key.lower().replace("_", "-")] = value
478
+
479
+ # Get client IP
480
+ source_ip = environ.get("REMOTE_ADDR", "unknown")
481
+ if "HTTP_X_FORWARDED_FOR" in environ:
482
+ source_ip = environ["HTTP_X_FORWARDED_FOR"].split(",")[0].strip()
483
+ elif "HTTP_X_REAL_IP" in environ:
484
+ source_ip = environ["HTTP_X_REAL_IP"]
485
+
486
+ # Read body
487
+ body = None
488
+ content_length = environ.get("CONTENT_LENGTH")
489
+ if content_length:
490
+ try:
491
+ length = int(content_length)
492
+ if length > 0:
493
+ wsgi_input = environ.get("wsgi.input")
494
+ if wsgi_input:
495
+ body_bytes = wsgi_input.read(length)
496
+ body = body_bytes.decode("utf-8", errors="replace")
497
+ # Reset stream for the application
498
+ environ["wsgi.input"] = BytesIO(body_bytes)
499
+ except (ValueError, TypeError):
500
+ pass
501
+
502
+ return RequestData(
503
+ method=method,
504
+ path=path,
505
+ query_params=query_params,
506
+ headers=headers,
507
+ body=body,
508
+ source_ip=source_ip,
509
+ )
510
+
511
+ def _blocked_response(self, start_response: Callable) -> list[bytes]:
512
+ """Return a 403 Forbidden response."""
513
+ body = b'{"error": "Request blocked due to security policy"}'
514
+ start_response(
515
+ "403 Forbidden",
516
+ [
517
+ ("Content-Type", "application/json"),
518
+ ("Content-Length", str(len(body))),
519
+ ],
520
+ )
521
+ return [body]
@@ -0,0 +1,140 @@
1
+ """Data models for security sensor events and alerts."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from datetime import datetime
5
+ from enum import Enum
6
+ from typing import Optional
7
+ import uuid
8
+
9
+
10
+ class AttackType(Enum):
11
+ """Types of attacks that can be detected."""
12
+
13
+ SQL_INJECTION = "sql_injection"
14
+ XSS = "xss"
15
+ PATH_TRAVERSAL = "path_traversal"
16
+ COMMAND_INJECTION = "command_injection"
17
+ RATE_LIMIT_EXCEEDED = "rate_limit_exceeded"
18
+ SUSPICIOUS_HEADER = "suspicious_header"
19
+
20
+
21
+ class ActionTaken(Enum):
22
+ """Action taken in response to detected threat."""
23
+
24
+ LOGGED = "logged"
25
+ BLOCKED = "blocked"
26
+
27
+
28
+ @dataclass
29
+ class RequestData:
30
+ """Normalized HTTP request data for analysis."""
31
+
32
+ method: str
33
+ path: str
34
+ query_params: dict[str, str] = field(default_factory=dict)
35
+ headers: dict[str, str] = field(default_factory=dict)
36
+ body: Optional[str] = None
37
+ source_ip: str = "unknown"
38
+ request_id: str = field(default_factory=lambda: str(uuid.uuid4()))
39
+
40
+
41
+ @dataclass
42
+ class MatchedPattern:
43
+ """Details about a matched attack pattern."""
44
+
45
+ pattern: str
46
+ location: str # "path", "query", "body", "header"
47
+ field: Optional[str] = None # Specific field name if applicable
48
+ matched_value: Optional[str] = None
49
+
50
+
51
+ @dataclass
52
+ class SecurityEvent:
53
+ """Represents a detected security event."""
54
+
55
+ event_type: AttackType
56
+ severity: str # "CRITICAL", "HIGH", "MEDIUM", "LOW"
57
+ timestamp: datetime
58
+ source_ip: str
59
+ path: str
60
+ method: str
61
+ matched_pattern: MatchedPattern
62
+ request_headers: dict[str, str] = field(default_factory=dict)
63
+ request_body: Optional[str] = None
64
+ request_id: str = field(default_factory=lambda: str(uuid.uuid4()))
65
+ confidence: float = 0.9
66
+ description: str = ""
67
+
68
+ def to_dict(self) -> dict:
69
+ """Convert to dictionary for JSON serialization."""
70
+ return {
71
+ "event_type": self.event_type.value,
72
+ "severity": self.severity,
73
+ "timestamp": self.timestamp.isoformat(),
74
+ "source_ip": self.source_ip,
75
+ "path": self.path,
76
+ "method": self.method,
77
+ "matched_pattern": {
78
+ "pattern": self.matched_pattern.pattern,
79
+ "location": self.matched_pattern.location,
80
+ "field": self.matched_pattern.field,
81
+ "matched_value": self.matched_pattern.matched_value,
82
+ },
83
+ "request_id": self.request_id,
84
+ "confidence": self.confidence,
85
+ "description": self.description,
86
+ }
87
+
88
+
89
+ @dataclass
90
+ class AlertPayload:
91
+ """Webhook alert payload format."""
92
+
93
+ version: str = "1.0"
94
+ event_id: str = field(default_factory=lambda: f"evt_{uuid.uuid4().hex[:12]}")
95
+ event_type: str = "security_alert"
96
+ timestamp: datetime = field(default_factory=datetime.utcnow)
97
+ alert: Optional[SecurityEvent] = None
98
+ action_taken: ActionTaken = ActionTaken.LOGGED
99
+
100
+ def to_dict(self) -> dict:
101
+ """Convert to webhook payload format."""
102
+ if self.alert is None:
103
+ raise ValueError("Alert cannot be None")
104
+
105
+ return {
106
+ "version": self.version,
107
+ "event": {
108
+ "id": self.event_id,
109
+ "type": self.event_type,
110
+ "timestamp": self.timestamp.isoformat() + "Z",
111
+ },
112
+ "alert": {
113
+ "type": self.alert.event_type.value,
114
+ "severity": self.alert.severity,
115
+ "confidence": self.alert.confidence,
116
+ "description": self.alert.description,
117
+ },
118
+ "request": {
119
+ "method": self.alert.method,
120
+ "path": self.alert.path,
121
+ "source_ip": self.alert.source_ip,
122
+ "headers": self.alert.request_headers,
123
+ },
124
+ "matched": {
125
+ "pattern": self.alert.matched_pattern.pattern,
126
+ "location": self.alert.matched_pattern.location,
127
+ "field": self.alert.matched_pattern.field,
128
+ },
129
+ "action_taken": self.action_taken.value,
130
+ }
131
+
132
+
133
+ @dataclass
134
+ class AlertResponse:
135
+ """Response from webhook alert attempt."""
136
+
137
+ success: bool
138
+ webhook_status: int
139
+ retry_count: int
140
+ error_message: Optional[str] = None