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,246 @@
1
+ """Dashboard alerter for sending runtime security alerts."""
2
+
3
+ import json
4
+ import logging
5
+ import os
6
+ from datetime import datetime
7
+ from typing import Optional
8
+ from urllib.parse import urljoin
9
+
10
+ import httpx
11
+
12
+ from .models import ActionTaken, AlertPayload, SecurityEvent
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ # Default dashboard API URL
17
+ DEFAULT_DASHBOARD_URL = "https://lhirdknhtzkqynfavdao.supabase.co/functions/v1"
18
+
19
+
20
+ class DashboardAlerter:
21
+ """Send security alerts to the SecurityUse dashboard.
22
+
23
+ Uses API key authentication to send runtime attack alerts directly
24
+ to the dashboard without requiring a custom webhook URL.
25
+
26
+ Usage:
27
+ from security_use.sensor import DashboardAlerter
28
+
29
+ # Uses SECURITY_USE_API_KEY environment variable
30
+ alerter = DashboardAlerter()
31
+
32
+ # Or pass API key directly
33
+ alerter = DashboardAlerter(api_key="su_...")
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ api_key: Optional[str] = None,
39
+ dashboard_url: Optional[str] = None,
40
+ timeout: float = 10.0,
41
+ retry_count: int = 3,
42
+ ):
43
+ """Initialize the dashboard alerter.
44
+
45
+ Args:
46
+ api_key: SecurityUse API key. If not provided, reads from
47
+ SECURITY_USE_API_KEY environment variable.
48
+ dashboard_url: Base URL for the dashboard API. Defaults to
49
+ SecurityUse cloud.
50
+ timeout: Request timeout in seconds.
51
+ retry_count: Number of retry attempts on failure.
52
+ """
53
+ self.api_key = api_key or os.environ.get("SECURITY_USE_API_KEY")
54
+ self.dashboard_url = dashboard_url or os.environ.get(
55
+ "SECURITY_USE_DASHBOARD_URL", DEFAULT_DASHBOARD_URL
56
+ )
57
+ self.timeout = timeout
58
+ self.retry_count = retry_count
59
+
60
+ if not self.api_key:
61
+ logger.warning(
62
+ "No API key provided. Set SECURITY_USE_API_KEY environment variable "
63
+ "or pass api_key parameter to enable dashboard alerting."
64
+ )
65
+
66
+ @property
67
+ def is_configured(self) -> bool:
68
+ """Check if the alerter is properly configured."""
69
+ return bool(self.api_key)
70
+
71
+ def _build_payload(
72
+ self,
73
+ event: SecurityEvent,
74
+ action: ActionTaken,
75
+ ) -> dict:
76
+ """Build the alert payload for the dashboard.
77
+
78
+ Args:
79
+ event: The security event that was detected.
80
+ action: The action taken (blocked or logged).
81
+
82
+ Returns:
83
+ Dictionary payload for the API.
84
+ """
85
+ # Get the attack type value (handle both enum and string)
86
+ attack_type = event.event_type.value if hasattr(event.event_type, 'value') else str(event.event_type)
87
+
88
+ # Get matched pattern info
89
+ pattern_str = ""
90
+ matched_value = ""
91
+ if event.matched_pattern:
92
+ pattern_str = event.matched_pattern.pattern
93
+ matched_value = event.matched_pattern.matched_value or ""
94
+
95
+ return {
96
+ "scan_type": "runtime",
97
+ "status": "completed",
98
+ "findings": [
99
+ {
100
+ "finding_type": "attack",
101
+ "category": "runtime",
102
+ "severity": event.severity.upper() if isinstance(event.severity, str) else event.severity,
103
+ "title": f"{attack_type.replace('_', ' ').title()} attack detected",
104
+ "description": event.description,
105
+ "pattern": pattern_str,
106
+ "payload_preview": matched_value[:500] if matched_value else None,
107
+ "recommendation": self._get_recommendation(attack_type),
108
+ "file_path": event.path,
109
+ "metadata": {
110
+ "source_ip": event.source_ip,
111
+ "method": event.method,
112
+ "user_agent": event.request_headers.get("user-agent") if event.request_headers else None,
113
+ "action_taken": action.value if hasattr(action, 'value') else str(action),
114
+ "confidence": event.confidence,
115
+ "timestamp": event.timestamp.isoformat() if event.timestamp else datetime.utcnow().isoformat(),
116
+ }
117
+ }
118
+ ],
119
+ "metadata": {
120
+ "sensor_version": "0.2.9",
121
+ "alert_type": "runtime_attack",
122
+ }
123
+ }
124
+
125
+ def _get_recommendation(self, attack_type: str) -> str:
126
+ """Get remediation recommendation for attack type."""
127
+ recommendations = {
128
+ "sql_injection": "Review and parameterize database queries. Use ORM or prepared statements.",
129
+ "xss": "Sanitize and escape user input before rendering. Use Content-Security-Policy headers.",
130
+ "path_traversal": "Validate file paths and use allowlists. Never construct paths from user input.",
131
+ "command_injection": "Avoid shell commands with user input. Use subprocess with shell=False.",
132
+ "rate_limit": "Implement proper rate limiting and consider blocking the source IP.",
133
+ "suspicious_headers": "Investigate the source. May indicate automated scanning or attack tools.",
134
+ }
135
+ return recommendations.get(
136
+ attack_type.lower().replace(" ", "_"),
137
+ "Review the attack pattern and implement appropriate input validation."
138
+ )
139
+
140
+ async def send_alert(
141
+ self,
142
+ event: SecurityEvent,
143
+ action: ActionTaken,
144
+ ) -> bool:
145
+ """Send an alert to the dashboard asynchronously.
146
+
147
+ Args:
148
+ event: The security event that was detected.
149
+ action: The action taken (blocked or logged).
150
+
151
+ Returns:
152
+ True if the alert was sent successfully, False otherwise.
153
+ """
154
+ if not self.is_configured:
155
+ logger.debug("Dashboard alerter not configured, skipping alert")
156
+ return False
157
+
158
+ payload = self._build_payload(event, action)
159
+ url = urljoin(self.dashboard_url + "/", "runtime-alert")
160
+
161
+ headers = {
162
+ "Authorization": f"Bearer {self.api_key}",
163
+ "Content-Type": "application/json",
164
+ }
165
+
166
+ for attempt in range(self.retry_count):
167
+ try:
168
+ async with httpx.AsyncClient(timeout=self.timeout) as client:
169
+ response = await client.post(url, json=payload, headers=headers)
170
+
171
+ if response.status_code in (200, 201, 202):
172
+ logger.info(f"Alert sent to dashboard: {event.event_type.value}")
173
+ return True
174
+ elif response.status_code == 401:
175
+ logger.error("Invalid API key for dashboard alerting")
176
+ return False
177
+ elif response.status_code == 404:
178
+ # Fallback to scan-upload endpoint
179
+ url = urljoin(self.dashboard_url + "/", "scan-upload")
180
+ continue
181
+ else:
182
+ logger.warning(
183
+ f"Dashboard alert failed (attempt {attempt + 1}): "
184
+ f"{response.status_code} - {response.text}"
185
+ )
186
+
187
+ except httpx.TimeoutException:
188
+ logger.warning(f"Dashboard alert timeout (attempt {attempt + 1})")
189
+ except Exception as e:
190
+ logger.error(f"Dashboard alert error (attempt {attempt + 1}): {e}")
191
+
192
+ return False
193
+
194
+ def send_alert_sync(
195
+ self,
196
+ event: SecurityEvent,
197
+ action: ActionTaken,
198
+ ) -> bool:
199
+ """Send an alert to the dashboard synchronously.
200
+
201
+ Args:
202
+ event: The security event that was detected.
203
+ action: The action taken (blocked or logged).
204
+
205
+ Returns:
206
+ True if the alert was sent successfully, False otherwise.
207
+ """
208
+ if not self.is_configured:
209
+ logger.debug("Dashboard alerter not configured, skipping alert")
210
+ return False
211
+
212
+ payload = self._build_payload(event, action)
213
+ url = urljoin(self.dashboard_url + "/", "runtime-alert")
214
+
215
+ headers = {
216
+ "Authorization": f"Bearer {self.api_key}",
217
+ "Content-Type": "application/json",
218
+ }
219
+
220
+ for attempt in range(self.retry_count):
221
+ try:
222
+ with httpx.Client(timeout=self.timeout) as client:
223
+ response = client.post(url, json=payload, headers=headers)
224
+
225
+ if response.status_code in (200, 201, 202):
226
+ logger.info(f"Alert sent to dashboard: {event.event_type.value}")
227
+ return True
228
+ elif response.status_code == 401:
229
+ logger.error("Invalid API key for dashboard alerting")
230
+ return False
231
+ elif response.status_code == 404:
232
+ # Fallback to scan-upload endpoint
233
+ url = urljoin(self.dashboard_url + "/", "scan-upload")
234
+ continue
235
+ else:
236
+ logger.warning(
237
+ f"Dashboard alert failed (attempt {attempt + 1}): "
238
+ f"{response.status_code} - {response.text}"
239
+ )
240
+
241
+ except httpx.TimeoutException:
242
+ logger.warning(f"Dashboard alert timeout (attempt {attempt + 1})")
243
+ except Exception as e:
244
+ logger.error(f"Dashboard alert error (attempt {attempt + 1}): {e}")
245
+
246
+ return False
@@ -0,0 +1,415 @@
1
+ """Attack pattern detection engine."""
2
+
3
+ import re
4
+ import threading
5
+ from collections import defaultdict
6
+ from dataclasses import dataclass
7
+ from datetime import datetime
8
+ from time import time
9
+ from typing import Optional
10
+
11
+ from .models import AttackType, MatchedPattern, RequestData, SecurityEvent
12
+
13
+
14
+ @dataclass
15
+ class DetectionPattern:
16
+ """A pattern for detecting attacks."""
17
+
18
+ pattern: str
19
+ compiled: re.Pattern
20
+ attack_type: AttackType
21
+ severity: str
22
+ description: str
23
+
24
+
25
+ # SQL Injection patterns
26
+ SQLI_PATTERNS = [
27
+ (r"(?i)(\%27)|(\')|(\-\-)|(\%23)|(#)", "Basic SQL injection characters"),
28
+ (r"(?i)((\%3D)|(=))[^\n]*((\%27)|(\')|(\-\-)|(\%3B)|(;))", "SQL tautology attempt"),
29
+ (r"(?i)\w*((\%27)|(\'))((\%6F)|o|(\%4F))((\%72)|r|(\%52))", "OR injection"),
30
+ (r"(?i)((\%27)|(\'))union", "UNION injection"),
31
+ (r"(?i)union\s+(all\s+)?select", "UNION SELECT"),
32
+ (r"(?i)(select|insert|update|delete|drop|create|alter)\s+", "SQL keyword"),
33
+ (r"(?i)exec(\s|\+)+(s|x)p\w+", "SQL stored procedure execution"),
34
+ (r"(?i);\s*(drop|delete|truncate|update|insert)", "SQL statement injection"),
35
+ ]
36
+
37
+ # XSS patterns
38
+ XSS_PATTERNS = [
39
+ (r"(?i)<script[^>]*>", "Script tag injection"),
40
+ (r"(?i)javascript\s*:", "JavaScript protocol"),
41
+ (r"(?i)on\w+\s*=", "Event handler injection"),
42
+ (r"(?i)<iframe[^>]*>", "IFrame injection"),
43
+ (r"(?i)<img[^>]+onerror", "Image onerror handler"),
44
+ (r"(?i)<svg[^>]+onload", "SVG onload handler"),
45
+ (r"(?i)expression\s*\(", "CSS expression"),
46
+ (r"(?i)vbscript\s*:", "VBScript protocol"),
47
+ (r"(?i)<embed[^>]*>", "Embed tag injection"),
48
+ (r"(?i)<object[^>]*>", "Object tag injection"),
49
+ ]
50
+
51
+ # Path traversal patterns
52
+ PATH_TRAVERSAL_PATTERNS = [
53
+ (r"\.\./", "Basic path traversal"),
54
+ (r"\.\.\\", "Windows path traversal"),
55
+ (r"%2e%2e%2f", "URL encoded path traversal"),
56
+ (r"%2e%2e/", "Partial URL encoded traversal"),
57
+ (r"\.%2e/", "Mixed encoding traversal"),
58
+ (r"%2e\./", "Mixed encoding traversal variant"),
59
+ (r"%252e%252e%252f", "Double URL encoded traversal"),
60
+ (r"/etc/passwd", "Unix password file access"),
61
+ (r"/etc/shadow", "Unix shadow file access"),
62
+ (r"c:\\windows", "Windows system directory"),
63
+ ]
64
+
65
+ # Command injection patterns
66
+ COMMAND_INJECTION_PATTERNS = [
67
+ (r";\s*(ls|cat|rm|wget|curl|nc|bash|sh|python|perl|ruby)\b", "Unix command injection"),
68
+ (r"\|\s*(ls|cat|rm|wget|curl|nc|bash|sh)\b", "Pipe command injection"),
69
+ (r"`[^`]+`", "Backtick command execution"),
70
+ (r"\$\([^)]+\)", "Command substitution"),
71
+ (r"&\s*(ls|cat|rm|wget|curl|nc|bash|sh)\b", "Background command injection"),
72
+ (r";\s*(dir|type|del|copy|move|net|powershell|cmd)\b", "Windows command injection"),
73
+ (r"\|\s*(dir|type|del|copy|move|net)\b", "Windows pipe injection"),
74
+ ]
75
+
76
+ # Suspicious headers
77
+ SUSPICIOUS_HEADER_PATTERNS = [
78
+ (r"(?i)(sqlmap|nikto|nmap|masscan|dirbuster|gobuster)", "Known attack tool"),
79
+ (r"(?i)(havij|acunetix|nessus|openvas|burp)", "Security scanner"),
80
+ (r"(?i)(\$\{|\%\{)", "Log4j/Template injection attempt"),
81
+ ]
82
+
83
+
84
+ class RateLimiter:
85
+ """Track request frequency per IP with automatic memory cleanup.
86
+
87
+ This rate limiter periodically cleans up stale IPs to prevent unbounded
88
+ memory growth. It also enforces a maximum number of tracked IPs.
89
+ """
90
+
91
+ def __init__(
92
+ self,
93
+ threshold: int = 100,
94
+ window_seconds: int = 60,
95
+ cleanup_interval: int = 300,
96
+ max_tracked_ips: int = 100000,
97
+ ):
98
+ """Initialize the rate limiter.
99
+
100
+ Args:
101
+ threshold: Requests per window before rate limit triggers.
102
+ window_seconds: Time window in seconds for rate limiting.
103
+ cleanup_interval: Seconds between cleanup runs.
104
+ max_tracked_ips: Maximum number of IPs to track.
105
+ """
106
+ self.threshold = threshold
107
+ self.window_seconds = window_seconds
108
+ self.cleanup_interval = cleanup_interval
109
+ self.max_tracked_ips = max_tracked_ips
110
+
111
+ self._requests: dict[str, list[float]] = defaultdict(list)
112
+ self._lock = threading.Lock()
113
+ self._last_cleanup = time()
114
+
115
+ # Stats
116
+ self.total_cleaned = 0
117
+
118
+ def check(self, ip: str) -> bool:
119
+ """Check if IP has exceeded rate limit. Returns True if exceeded."""
120
+ now = time()
121
+
122
+ with self._lock:
123
+ # Trigger cleanup if interval passed
124
+ if now - self._last_cleanup > self.cleanup_interval:
125
+ self._cleanup(now)
126
+
127
+ cutoff = now - self.window_seconds
128
+
129
+ # Clean old entries for this IP
130
+ self._requests[ip] = [t for t in self._requests[ip] if t > cutoff]
131
+
132
+ # Add current request
133
+ self._requests[ip].append(now)
134
+
135
+ return len(self._requests[ip]) > self.threshold
136
+
137
+ def _cleanup(self, now: float) -> None:
138
+ """Remove IPs with no recent requests. Must be called with lock held."""
139
+ cutoff = now - self.window_seconds
140
+
141
+ # Find IPs to remove
142
+ to_remove = []
143
+ for ip, timestamps in self._requests.items():
144
+ # Remove if no recent timestamps
145
+ if not timestamps or all(t <= cutoff for t in timestamps):
146
+ to_remove.append(ip)
147
+
148
+ # Remove stale IPs
149
+ for ip in to_remove:
150
+ del self._requests[ip]
151
+
152
+ self.total_cleaned += len(to_remove)
153
+ self._last_cleanup = now
154
+
155
+ # Emergency cleanup if still too many IPs
156
+ if len(self._requests) > self.max_tracked_ips:
157
+ self._emergency_cleanup(now)
158
+
159
+ def _emergency_cleanup(self, now: float) -> None:
160
+ """Emergency cleanup when max IPs exceeded. Must be called with lock held.
161
+
162
+ Removes least recently active IPs until under limit.
163
+ """
164
+ if len(self._requests) <= self.max_tracked_ips:
165
+ return
166
+
167
+ # Sort IPs by most recent timestamp
168
+ ip_last_seen = []
169
+ for ip, timestamps in self._requests.items():
170
+ last_seen = max(timestamps) if timestamps else 0
171
+ ip_last_seen.append((ip, last_seen))
172
+
173
+ # Sort by last seen (oldest first)
174
+ ip_last_seen.sort(key=lambda x: x[1])
175
+
176
+ # Remove oldest until under limit (with buffer)
177
+ to_remove = len(self._requests) - self.max_tracked_ips + 1000
178
+ for ip, _ in ip_last_seen[:to_remove]:
179
+ del self._requests[ip]
180
+ self.total_cleaned += 1
181
+
182
+ def get_request_count(self, ip: str) -> int:
183
+ """Get current request count for IP."""
184
+ now = time()
185
+ cutoff = now - self.window_seconds
186
+
187
+ with self._lock:
188
+ self._requests[ip] = [t for t in self._requests[ip] if t > cutoff]
189
+ return len(self._requests[ip])
190
+
191
+ def reset(self, ip: Optional[str] = None) -> None:
192
+ """Reset rate limit tracking."""
193
+ with self._lock:
194
+ if ip:
195
+ self._requests.pop(ip, None)
196
+ else:
197
+ self._requests.clear()
198
+
199
+ @property
200
+ def tracked_ip_count(self) -> int:
201
+ """Number of IPs currently being tracked."""
202
+ return len(self._requests)
203
+
204
+ @property
205
+ def stats(self) -> dict:
206
+ """Get rate limiter statistics."""
207
+ return {
208
+ "tracked_ips": len(self._requests),
209
+ "total_cleaned": self.total_cleaned,
210
+ "threshold": self.threshold,
211
+ "window_seconds": self.window_seconds,
212
+ "max_tracked_ips": self.max_tracked_ips,
213
+ }
214
+
215
+
216
+ class AttackDetector:
217
+ """Detects malicious patterns in HTTP requests."""
218
+
219
+ def __init__(
220
+ self,
221
+ enabled_detectors: Optional[list[str]] = None,
222
+ rate_limit_threshold: int = 100,
223
+ rate_limit_window: int = 60,
224
+ rate_limit_cleanup_interval: int = 300,
225
+ rate_limit_max_ips: int = 100000,
226
+ ):
227
+ """Initialize the attack detector.
228
+
229
+ Args:
230
+ enabled_detectors: List of detector types to enable.
231
+ Options: "sqli", "xss", "path_traversal", "command_injection",
232
+ "rate_limit", "suspicious_headers"
233
+ Defaults to all detectors.
234
+ rate_limit_threshold: Requests per window before rate limit triggers.
235
+ rate_limit_window: Time window in seconds for rate limiting.
236
+ rate_limit_cleanup_interval: Seconds between cleanup runs.
237
+ rate_limit_max_ips: Maximum number of IPs to track.
238
+ """
239
+ self.enabled_detectors = enabled_detectors or [
240
+ "sqli",
241
+ "xss",
242
+ "path_traversal",
243
+ "command_injection",
244
+ "rate_limit",
245
+ "suspicious_headers",
246
+ ]
247
+ self.rate_limiter = RateLimiter(
248
+ threshold=rate_limit_threshold,
249
+ window_seconds=rate_limit_window,
250
+ cleanup_interval=rate_limit_cleanup_interval,
251
+ max_tracked_ips=rate_limit_max_ips,
252
+ )
253
+ self._patterns = self._compile_patterns()
254
+
255
+ def _compile_patterns(self) -> dict[AttackType, list[DetectionPattern]]:
256
+ """Compile all detection patterns."""
257
+ patterns: dict[AttackType, list[DetectionPattern]] = {}
258
+
259
+ if "sqli" in self.enabled_detectors:
260
+ patterns[AttackType.SQL_INJECTION] = [
261
+ DetectionPattern(
262
+ pattern=p,
263
+ compiled=re.compile(p),
264
+ attack_type=AttackType.SQL_INJECTION,
265
+ severity="HIGH",
266
+ description=desc,
267
+ )
268
+ for p, desc in SQLI_PATTERNS
269
+ ]
270
+
271
+ if "xss" in self.enabled_detectors:
272
+ patterns[AttackType.XSS] = [
273
+ DetectionPattern(
274
+ pattern=p,
275
+ compiled=re.compile(p),
276
+ attack_type=AttackType.XSS,
277
+ severity="MEDIUM",
278
+ description=desc,
279
+ )
280
+ for p, desc in XSS_PATTERNS
281
+ ]
282
+
283
+ if "path_traversal" in self.enabled_detectors:
284
+ patterns[AttackType.PATH_TRAVERSAL] = [
285
+ DetectionPattern(
286
+ pattern=p,
287
+ compiled=re.compile(p, re.IGNORECASE),
288
+ attack_type=AttackType.PATH_TRAVERSAL,
289
+ severity="HIGH",
290
+ description=desc,
291
+ )
292
+ for p, desc in PATH_TRAVERSAL_PATTERNS
293
+ ]
294
+
295
+ if "command_injection" in self.enabled_detectors:
296
+ patterns[AttackType.COMMAND_INJECTION] = [
297
+ DetectionPattern(
298
+ pattern=p,
299
+ compiled=re.compile(p, re.IGNORECASE),
300
+ attack_type=AttackType.COMMAND_INJECTION,
301
+ severity="CRITICAL",
302
+ description=desc,
303
+ )
304
+ for p, desc in COMMAND_INJECTION_PATTERNS
305
+ ]
306
+
307
+ if "suspicious_headers" in self.enabled_detectors:
308
+ patterns[AttackType.SUSPICIOUS_HEADER] = [
309
+ DetectionPattern(
310
+ pattern=p,
311
+ compiled=re.compile(p),
312
+ attack_type=AttackType.SUSPICIOUS_HEADER,
313
+ severity="MEDIUM",
314
+ description=desc,
315
+ )
316
+ for p, desc in SUSPICIOUS_HEADER_PATTERNS
317
+ ]
318
+
319
+ return patterns
320
+
321
+ def _check_string(
322
+ self,
323
+ value: str,
324
+ location: str,
325
+ field: Optional[str],
326
+ request: RequestData,
327
+ ) -> list[SecurityEvent]:
328
+ """Check a string value against all patterns."""
329
+ events = []
330
+
331
+ for attack_type, patterns in self._patterns.items():
332
+ for pattern in patterns:
333
+ match = pattern.compiled.search(value)
334
+ if match:
335
+ events.append(
336
+ SecurityEvent(
337
+ event_type=attack_type,
338
+ severity=pattern.severity,
339
+ timestamp=datetime.utcnow(),
340
+ source_ip=request.source_ip,
341
+ path=request.path,
342
+ method=request.method,
343
+ matched_pattern=MatchedPattern(
344
+ pattern=pattern.pattern,
345
+ location=location,
346
+ field=field,
347
+ matched_value=match.group(0),
348
+ ),
349
+ request_headers=request.headers,
350
+ request_body=request.body,
351
+ request_id=request.request_id,
352
+ description=pattern.description,
353
+ )
354
+ )
355
+ # Only report first match per attack type per location
356
+ break
357
+
358
+ return events
359
+
360
+ def analyze_request(self, request: RequestData) -> list[SecurityEvent]:
361
+ """Analyze an HTTP request for malicious patterns.
362
+
363
+ Args:
364
+ request: Normalized request data to analyze.
365
+
366
+ Returns:
367
+ List of detected security events.
368
+ """
369
+ events: list[SecurityEvent] = []
370
+
371
+ # Check rate limiting
372
+ if "rate_limit" in self.enabled_detectors:
373
+ if self.rate_limiter.check(request.source_ip):
374
+ events.append(
375
+ SecurityEvent(
376
+ event_type=AttackType.RATE_LIMIT_EXCEEDED,
377
+ severity="MEDIUM",
378
+ timestamp=datetime.utcnow(),
379
+ source_ip=request.source_ip,
380
+ path=request.path,
381
+ method=request.method,
382
+ matched_pattern=MatchedPattern(
383
+ pattern="rate_limit",
384
+ location="request",
385
+ field=None,
386
+ matched_value=str(
387
+ self.rate_limiter.get_request_count(request.source_ip)
388
+ ),
389
+ ),
390
+ request_headers=request.headers,
391
+ request_id=request.request_id,
392
+ description=f"Rate limit exceeded: {self.rate_limiter.get_request_count(request.source_ip)} requests",
393
+ )
394
+ )
395
+
396
+ # Check path
397
+ events.extend(self._check_string(request.path, "path", None, request))
398
+
399
+ # Check query parameters
400
+ for key, value in request.query_params.items():
401
+ events.extend(self._check_string(key, "query", "key", request))
402
+ events.extend(self._check_string(value, "query", key, request))
403
+
404
+ # Check headers
405
+ if "suspicious_headers" in self.enabled_detectors:
406
+ for key, value in request.headers.items():
407
+ # Only check user-agent and other relevant headers for attack tools
408
+ if key.lower() in ("user-agent", "referer", "x-forwarded-for"):
409
+ events.extend(self._check_string(value, "header", key, request))
410
+
411
+ # Check body
412
+ if request.body:
413
+ events.extend(self._check_string(request.body, "body", None, request))
414
+
415
+ return events