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.
- package/LICENSE +63 -0
- package/README.md +474 -0
- package/bin/devfortress-init.js +206 -0
- package/dist/browser.d.ts +61 -0
- package/dist/browser.js +184 -0
- package/dist/circuit-breaker.d.ts +68 -0
- package/dist/circuit-breaker.js +116 -0
- package/dist/client.d.ts +26 -0
- package/dist/client.js +98 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.js +78 -0
- package/dist/middleware/express.d.ts +9 -0
- package/dist/middleware/express.js +236 -0
- package/dist/quick.d.ts +37 -0
- package/dist/quick.js +135 -0
- package/dist/types.d.ts +217 -0
- package/dist/types.js +12 -0
- package/package.json +101 -0
- package/src/middleware/fastapi.py +232 -0
- package/src/middleware/flask.py +213 -0
|
@@ -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
|