devfortress-sdk 4.2.0

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.
@@ -0,0 +1,213 @@
1
+ """
2
+ DevFortress Flask Middleware
3
+
4
+ Automatically monitors Flask applications for security events and sends
5
+ them to the DevFortress surveillance API.
6
+
7
+ Usage:
8
+ from devfortress_middleware import DevFortressFlask
9
+
10
+ app = Flask(__name__)
11
+ devfortress = DevFortressFlask(
12
+ app,
13
+ api_key="your-api-key",
14
+ endpoint="https://www.devfortress.net/api/events/ingest"
15
+ )
16
+ """
17
+
18
+ import time
19
+ import re
20
+ import json
21
+ import threading
22
+ from typing import Optional, List, Dict, Any, Callable
23
+
24
+ try:
25
+ from flask import Flask, request, g
26
+ HAS_FLASK = True
27
+ except ImportError:
28
+ HAS_FLASK = False
29
+
30
+ try:
31
+ import requests as http_requests
32
+ HAS_REQUESTS = True
33
+ except ImportError:
34
+ HAS_REQUESTS = False
35
+
36
+ try:
37
+ from urllib.request import Request as URLRequest, urlopen
38
+ HAS_URLLIB = True
39
+ except ImportError:
40
+ HAS_URLLIB = False
41
+
42
+
43
+ # Detection patterns
44
+ SQL_PATTERNS = [
45
+ re.compile(r"(\bor\b.*=.*)", re.IGNORECASE),
46
+ re.compile(r"(\bunion\b.*\bselect\b)", re.IGNORECASE),
47
+ re.compile(r"(\bdrop\b.*\btable\b)", re.IGNORECASE),
48
+ re.compile(r"('\s*or\s*'1'\s*=\s*'1)", re.IGNORECASE),
49
+ ]
50
+
51
+ XSS_PATTERNS = [
52
+ re.compile(r"<script[^>]*>[\s\S]*?</script>", re.IGNORECASE),
53
+ re.compile(r"javascript:", re.IGNORECASE),
54
+ re.compile(r"onerror\s*=\s*", re.IGNORECASE),
55
+ ]
56
+
57
+ TRAVERSAL_PATTERNS = [
58
+ re.compile(r"\.\.[/\\]"),
59
+ re.compile(r"/etc/passwd", re.IGNORECASE),
60
+ ]
61
+
62
+
63
+ class DevFortressFlask:
64
+ """Flask extension for DevFortress security monitoring."""
65
+
66
+ def __init__(
67
+ self,
68
+ app: Optional["Flask"] = None,
69
+ api_key: str = "",
70
+ endpoint: str = "https://www.devfortress.net/api/events/ingest",
71
+ exclude_paths: Optional[List[str]] = None,
72
+ capture_headers: bool = False,
73
+ debug: bool = False,
74
+ on_error: Optional[Callable[[Exception], None]] = None,
75
+ ):
76
+ self.api_key = api_key
77
+ self.endpoint = endpoint
78
+ self.exclude_paths = exclude_paths or ["/health", "/static"]
79
+ self.capture_headers = capture_headers
80
+ self.debug = debug
81
+ self.on_error = on_error
82
+
83
+ if app is not None:
84
+ self.init_app(app)
85
+
86
+ def init_app(self, app: "Flask"):
87
+ """Initialize Flask app with DevFortress monitoring."""
88
+ if not HAS_FLASK:
89
+ raise ImportError("Flask is required for DevFortressFlask middleware")
90
+
91
+ app.before_request(self._before_request)
92
+ app.after_request(self._after_request)
93
+
94
+ def _before_request(self):
95
+ """Record request start time."""
96
+ g.devfortress_start_time = time.time()
97
+
98
+ def _after_request(self, response):
99
+ """Analyze response and track security events."""
100
+ # Skip excluded paths
101
+ if any(request.path.startswith(p) for p in self.exclude_paths):
102
+ return response
103
+
104
+ start_time = getattr(g, "devfortress_start_time", time.time())
105
+ response_time = time.time() - start_time
106
+ status_code = response.status_code
107
+
108
+ event_type = None
109
+ severity = "LOW"
110
+ reason = None
111
+
112
+ if status_code in (401, 403):
113
+ event_type = "auth_failure"
114
+ severity = "MEDIUM"
115
+ reason = "Authentication or authorization failed"
116
+ elif status_code in (400, 422):
117
+ event_type = "validation_error"
118
+ severity = "LOW"
119
+ reason = "Request validation failed"
120
+ elif status_code == 429:
121
+ event_type = "rate_limit_exceeded"
122
+ severity = "MEDIUM"
123
+ reason = "Rate limit exceeded"
124
+ elif status_code >= 500:
125
+ event_type = "5xx_error"
126
+ severity = "HIGH"
127
+ reason = f"Server error: {status_code}"
128
+ elif status_code >= 400:
129
+ event_type = "4xx_error"
130
+ severity = "LOW"
131
+ reason = f"Client error: {status_code}"
132
+
133
+ # Suspicious pattern detection
134
+ suspicious = self._detect_suspicious()
135
+ if suspicious:
136
+ event_type = "suspicious_pattern"
137
+ severity = "HIGH"
138
+ reason = f"Suspicious patterns: {', '.join(suspicious)}"
139
+
140
+ if event_type:
141
+ ip = (
142
+ request.headers.get("X-Forwarded-For", "").split(",")[0].strip()
143
+ or request.headers.get("X-Real-IP", "")
144
+ or request.remote_addr
145
+ or "0.0.0.0"
146
+ )
147
+
148
+ payload: Dict[str, Any] = {
149
+ "eventType": event_type,
150
+ "ip": ip,
151
+ "method": request.method,
152
+ "path": request.path,
153
+ "userAgent": request.user_agent.string if request.user_agent else None,
154
+ "statusCode": status_code,
155
+ "responseTime": round(response_time * 1000),
156
+ "severity": severity,
157
+ "reason": reason,
158
+ "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
159
+ }
160
+
161
+ if self.capture_headers:
162
+ payload["metadata"] = {"headers": dict(request.headers)}
163
+
164
+ # Send asynchronously to avoid blocking the response
165
+ thread = threading.Thread(
166
+ target=self._send_event, args=(payload,), daemon=True
167
+ )
168
+ thread.start()
169
+
170
+ return response
171
+
172
+ def _send_event(self, payload: Dict[str, Any]):
173
+ """Send event to DevFortress API (runs in background thread)."""
174
+ headers = {
175
+ "Content-Type": "application/json",
176
+ "X-DevFortress-Key": self.api_key,
177
+ }
178
+
179
+ try:
180
+ if HAS_REQUESTS:
181
+ http_requests.post(
182
+ self.endpoint,
183
+ json=payload,
184
+ headers=headers,
185
+ timeout=5,
186
+ )
187
+ elif HAS_URLLIB:
188
+ req = URLRequest(
189
+ self.endpoint,
190
+ data=json.dumps(payload).encode("utf-8"),
191
+ headers=headers,
192
+ method="POST",
193
+ )
194
+ urlopen(req, timeout=5)
195
+ except Exception as exc:
196
+ if self.on_error:
197
+ self.on_error(exc)
198
+ elif self.debug:
199
+ print(f"[DevFortress] Failed to send event: {exc}") # noqa: T201
200
+
201
+ def _detect_suspicious(self) -> List[str]:
202
+ """Detect suspicious patterns in the request."""
203
+ patterns_found = []
204
+ check_str = f"{request.url} {request.query_string.decode('utf-8', errors='ignore')}"
205
+
206
+ if any(p.search(check_str) for p in SQL_PATTERNS):
207
+ patterns_found.append("sql_injection")
208
+ if any(p.search(check_str) for p in XSS_PATTERNS):
209
+ patterns_found.append("xss")
210
+ if any(p.search(check_str) for p in TRAVERSAL_PATTERNS):
211
+ patterns_found.append("path_traversal")
212
+
213
+ return patterns_found