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
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DevFortress SDK v3.0.0 — Type Definitions
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - Event types & severity levels
|
|
6
|
+
* - SDK configuration & enrichment hooks
|
|
7
|
+
* - Threat events & webhook payloads
|
|
8
|
+
* - Token alias interfaces
|
|
9
|
+
* - AbuseIPDB types
|
|
10
|
+
*/
|
|
11
|
+
/** Closed-loop protection mode:
|
|
12
|
+
* - 'external': SDK → DevFortress Platform → webhook → response (default)
|
|
13
|
+
* - 'internal': 3-tier local engine, no platform calls (enterprise/air-gap)
|
|
14
|
+
* - 'hybrid': Internal evaluates first, external enriches asynchronously
|
|
15
|
+
*/
|
|
16
|
+
export type CLMode = 'external' | 'internal' | 'hybrid';
|
|
17
|
+
export type EventType = 'auth_failure' | 'validation_error' | 'rate_limit_exceeded' | '5xx_error' | '4xx_error' | 'suspicious_pattern' | 'sql_injection_attempt' | 'xss_attempt' | 'custom' | 'login_brute_force' | 'credential_stuffing' | 'password_spray' | 'honeypot_triggered' | 'sql_injection' | 'auth_endpoint_scan' | 'recon_scan' | 'bot_signature' | 'brute_force' | 'token_replay' | 'privilege_escalation' | 'suspicious_ip' | 'anomalous_volume' | 'geo_anomaly';
|
|
18
|
+
export type SeverityLevel = 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
|
|
19
|
+
export type ThreatSeverity = 'low' | 'medium' | 'high' | 'critical';
|
|
20
|
+
/** Authentication phase when the event occurred:
|
|
21
|
+
* - 'pre_auth': Before authentication (login attempts, public endpoint attacks)
|
|
22
|
+
* - 'post_auth': After authentication (session-based threats, privilege escalation)
|
|
23
|
+
*/
|
|
24
|
+
export type AuthPhase = 'pre_auth' | 'post_auth';
|
|
25
|
+
/** Action taken by the protection system in response to the threat */
|
|
26
|
+
export type ActionTaken = 'blocked' | 'rate_limited' | 'session_revoked' | 'captcha_challenged' | 'flagged' | 'allowed' | 'monitored';
|
|
27
|
+
export interface LiveThreatEvent {
|
|
28
|
+
eventType: EventType;
|
|
29
|
+
timestamp?: string;
|
|
30
|
+
ip: string;
|
|
31
|
+
method?: string;
|
|
32
|
+
path?: string;
|
|
33
|
+
userAgent?: string | null;
|
|
34
|
+
statusCode?: number | null;
|
|
35
|
+
responseTime?: number | null;
|
|
36
|
+
metadata?: Record<string, unknown>;
|
|
37
|
+
severity?: SeverityLevel;
|
|
38
|
+
reason?: string;
|
|
39
|
+
userId?: string | null;
|
|
40
|
+
sessionId?: string | null;
|
|
41
|
+
appId?: string;
|
|
42
|
+
environment?: string;
|
|
43
|
+
compositeScore?: number;
|
|
44
|
+
abuseipdb?: AbuseIPDBScore;
|
|
45
|
+
/** Closed-loop protection mode active when event was captured */
|
|
46
|
+
clMode?: CLMode;
|
|
47
|
+
/** Whether event occurred pre-auth or post-auth */
|
|
48
|
+
authPhase?: AuthPhase;
|
|
49
|
+
/** Action taken by the protection system */
|
|
50
|
+
actionTaken?: ActionTaken;
|
|
51
|
+
/** Response latency in milliseconds (time from request to SDK event emission) */
|
|
52
|
+
responseLatencyMs?: number;
|
|
53
|
+
/** Whether user session remained active after the action was taken */
|
|
54
|
+
sessionActiveAfterAction?: boolean;
|
|
55
|
+
}
|
|
56
|
+
export interface DevFortressClientOptions {
|
|
57
|
+
apiKey: string;
|
|
58
|
+
endpoint?: string;
|
|
59
|
+
timeout?: number;
|
|
60
|
+
retries?: number;
|
|
61
|
+
debug?: boolean;
|
|
62
|
+
}
|
|
63
|
+
export interface DevFortressConfig {
|
|
64
|
+
apiKey: string;
|
|
65
|
+
appId: string;
|
|
66
|
+
environment?: string;
|
|
67
|
+
debug?: boolean;
|
|
68
|
+
endpoint?: string;
|
|
69
|
+
timeout?: number;
|
|
70
|
+
retries?: number;
|
|
71
|
+
enrichment?: EnrichmentConfig;
|
|
72
|
+
alertThresholds?: Partial<Record<string, ThreatSeverity>>;
|
|
73
|
+
responseWebhook?: {
|
|
74
|
+
url: string;
|
|
75
|
+
secret: string;
|
|
76
|
+
};
|
|
77
|
+
cache?: {
|
|
78
|
+
driver: 'redis' | 'memory';
|
|
79
|
+
url?: string;
|
|
80
|
+
ttl?: number;
|
|
81
|
+
};
|
|
82
|
+
/** Closed-loop protection mode. Default: 'external' */
|
|
83
|
+
mode?: CLMode;
|
|
84
|
+
/** Subscription tier for feature gating */
|
|
85
|
+
tier?: 'starter' | 'pro' | 'enterprise';
|
|
86
|
+
/** Whether the hybrid add-on is enabled (Pro tier only) */
|
|
87
|
+
hybridAddonEnabled?: boolean;
|
|
88
|
+
/** Internal closed-loop engine configuration (for 'internal' or 'hybrid' mode) */
|
|
89
|
+
internalCL?: {
|
|
90
|
+
/** Fail-closed (block on error) or fail-open. Default: 'closed' */
|
|
91
|
+
failMode?: 'closed' | 'open';
|
|
92
|
+
/** Custom Tier 2 scorer */
|
|
93
|
+
tier2Scorer?: (request: unknown) => Promise<number> | number;
|
|
94
|
+
/** Score threshold for auto-block. Default: 85 */
|
|
95
|
+
blockThreshold?: number;
|
|
96
|
+
/** Rate limit: max requests per window. Default: 100 */
|
|
97
|
+
rateLimitMax?: number;
|
|
98
|
+
/** Rate limit window in ms. Default: 60000 */
|
|
99
|
+
rateLimitWindowMs?: number;
|
|
100
|
+
};
|
|
101
|
+
/** Circuit breaker config for hybrid mode fallback */
|
|
102
|
+
circuitBreaker?: {
|
|
103
|
+
/** Failures before opening circuit. Default: 3 */
|
|
104
|
+
failureThreshold?: number;
|
|
105
|
+
/** Time before testing recovery in ms. Default: 60000 */
|
|
106
|
+
recoveryTimeMs?: number;
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
export interface EnrichmentConfig {
|
|
110
|
+
getUserId?: (req: unknown) => string | null | Promise<string | null>;
|
|
111
|
+
getSessionId?: (req: unknown) => string | null | Promise<string | null>;
|
|
112
|
+
getIP?: (req: unknown) => string | null;
|
|
113
|
+
}
|
|
114
|
+
export interface ObserveResult {
|
|
115
|
+
flagged: boolean;
|
|
116
|
+
event_id: string;
|
|
117
|
+
confidence: number;
|
|
118
|
+
}
|
|
119
|
+
export interface ObserveOptions {
|
|
120
|
+
meta?: Record<string, unknown>;
|
|
121
|
+
skipEnrichment?: boolean;
|
|
122
|
+
}
|
|
123
|
+
export interface ThreatEvent {
|
|
124
|
+
event_id: string;
|
|
125
|
+
threat_type: string;
|
|
126
|
+
severity: ThreatSeverity;
|
|
127
|
+
confidence: number;
|
|
128
|
+
composite_score: number;
|
|
129
|
+
ip: string;
|
|
130
|
+
identity: {
|
|
131
|
+
user_id: string | null;
|
|
132
|
+
session_id: string | null;
|
|
133
|
+
};
|
|
134
|
+
geo?: {
|
|
135
|
+
country: string;
|
|
136
|
+
city: string;
|
|
137
|
+
};
|
|
138
|
+
abuseipdb?: AbuseIPDBScore;
|
|
139
|
+
endpoint: string;
|
|
140
|
+
method: string;
|
|
141
|
+
timestamp: string;
|
|
142
|
+
meta: Record<string, unknown>;
|
|
143
|
+
}
|
|
144
|
+
export interface ActionReport {
|
|
145
|
+
success: boolean;
|
|
146
|
+
actions: string[];
|
|
147
|
+
reason?: string;
|
|
148
|
+
meta?: Record<string, unknown>;
|
|
149
|
+
}
|
|
150
|
+
export interface AbuseIPDBScore {
|
|
151
|
+
score: number;
|
|
152
|
+
isTor: boolean;
|
|
153
|
+
isDatacenter: boolean;
|
|
154
|
+
distinctUsers?: number;
|
|
155
|
+
fromCache?: boolean;
|
|
156
|
+
}
|
|
157
|
+
export interface AbuseIPDBCheckResult {
|
|
158
|
+
abuseConfidenceScore: number;
|
|
159
|
+
totalReports: number;
|
|
160
|
+
numDistinctUsers: number;
|
|
161
|
+
isTor: boolean;
|
|
162
|
+
usageType: string;
|
|
163
|
+
countryCode: string;
|
|
164
|
+
lastReportedAt: string | null;
|
|
165
|
+
isp: string;
|
|
166
|
+
}
|
|
167
|
+
export interface CompositeScoreResult {
|
|
168
|
+
score: number;
|
|
169
|
+
severity: ThreatSeverity;
|
|
170
|
+
abuseScore: number;
|
|
171
|
+
isTor: boolean;
|
|
172
|
+
isDatacenter: boolean;
|
|
173
|
+
distinctUsers: number;
|
|
174
|
+
fromCache: boolean;
|
|
175
|
+
}
|
|
176
|
+
export interface TokenAliasData {
|
|
177
|
+
realToken: string;
|
|
178
|
+
userId: string;
|
|
179
|
+
createdAt: number;
|
|
180
|
+
}
|
|
181
|
+
export interface DevFortressMiddlewareOptions extends DevFortressClientOptions {
|
|
182
|
+
captureBody?: boolean;
|
|
183
|
+
captureHeaders?: boolean;
|
|
184
|
+
excludePaths?: string[];
|
|
185
|
+
sanitize?: (data: Record<string, unknown>) => Record<string, unknown>;
|
|
186
|
+
onRequest?: (req: unknown) => Partial<LiveThreatEvent> | null;
|
|
187
|
+
onError?: (error: Error) => void;
|
|
188
|
+
mode?: CLMode;
|
|
189
|
+
}
|
|
190
|
+
export interface ApiResponse {
|
|
191
|
+
success: boolean;
|
|
192
|
+
message?: string;
|
|
193
|
+
eventId?: string;
|
|
194
|
+
}
|
|
195
|
+
export interface WebhookPayload {
|
|
196
|
+
event_id: string;
|
|
197
|
+
action: string;
|
|
198
|
+
threat_type: string;
|
|
199
|
+
severity: ThreatSeverity;
|
|
200
|
+
confidence: number;
|
|
201
|
+
composite_score: number;
|
|
202
|
+
ip: string;
|
|
203
|
+
identity: {
|
|
204
|
+
user_id: string | null;
|
|
205
|
+
session_id: string | null;
|
|
206
|
+
};
|
|
207
|
+
geo?: {
|
|
208
|
+
country: string;
|
|
209
|
+
city: string;
|
|
210
|
+
};
|
|
211
|
+
abuseipdb?: AbuseIPDBScore;
|
|
212
|
+
endpoint: string;
|
|
213
|
+
method: string;
|
|
214
|
+
timestamp: string;
|
|
215
|
+
meta: Record<string, unknown>;
|
|
216
|
+
}
|
|
217
|
+
export type ThreatHandler = (event: ThreatEvent) => Promise<void> | void;
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* DevFortress SDK v3.0.0 — Type Definitions
|
|
4
|
+
*
|
|
5
|
+
* Covers:
|
|
6
|
+
* - Event types & severity levels
|
|
7
|
+
* - SDK configuration & enrichment hooks
|
|
8
|
+
* - Threat events & webhook payloads
|
|
9
|
+
* - Token alias interfaces
|
|
10
|
+
* - AbuseIPDB types
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
package/package.json
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "devfortress-sdk",
|
|
3
|
+
"version": "4.2.0",
|
|
4
|
+
"description": "DevFortress SDK — API and application security with automated threat response, session privacy, and AI agent observability.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"browser": "dist/browser.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"require": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
},
|
|
14
|
+
"./browser": {
|
|
15
|
+
"import": "./dist/browser.js",
|
|
16
|
+
"require": "./dist/browser.js",
|
|
17
|
+
"types": "./dist/browser.d.ts"
|
|
18
|
+
},
|
|
19
|
+
"./quick": {
|
|
20
|
+
"import": "./dist/quick.js",
|
|
21
|
+
"require": "./dist/quick.js",
|
|
22
|
+
"types": "./dist/quick.d.ts"
|
|
23
|
+
},
|
|
24
|
+
"./package.json": "./package.json"
|
|
25
|
+
},
|
|
26
|
+
"bin": {
|
|
27
|
+
"devfortress-init": "./bin/devfortress-init.js"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist/index.js",
|
|
31
|
+
"dist/index.d.ts",
|
|
32
|
+
"dist/client.js",
|
|
33
|
+
"dist/client.d.ts",
|
|
34
|
+
"dist/browser.js",
|
|
35
|
+
"dist/browser.d.ts",
|
|
36
|
+
"dist/quick.js",
|
|
37
|
+
"dist/quick.d.ts",
|
|
38
|
+
"dist/types.js",
|
|
39
|
+
"dist/types.d.ts",
|
|
40
|
+
"dist/circuit-breaker.js",
|
|
41
|
+
"dist/circuit-breaker.d.ts",
|
|
42
|
+
"dist/middleware/express.js",
|
|
43
|
+
"dist/middleware/express.d.ts",
|
|
44
|
+
"bin/devfortress-init.js",
|
|
45
|
+
"src/middleware/fastapi.py",
|
|
46
|
+
"src/middleware/flask.py",
|
|
47
|
+
"README.md",
|
|
48
|
+
"LICENSE"
|
|
49
|
+
],
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "tsc",
|
|
52
|
+
"test": "jest",
|
|
53
|
+
"prepublishOnly": "npm run build"
|
|
54
|
+
},
|
|
55
|
+
"keywords": [
|
|
56
|
+
"devfortress",
|
|
57
|
+
"security",
|
|
58
|
+
"api-security",
|
|
59
|
+
"application-security",
|
|
60
|
+
"threat-detection",
|
|
61
|
+
"monitoring",
|
|
62
|
+
"automated-response",
|
|
63
|
+
"webhook",
|
|
64
|
+
"express",
|
|
65
|
+
"nodejs",
|
|
66
|
+
"agent-security",
|
|
67
|
+
"ai-agents",
|
|
68
|
+
"closed-loop",
|
|
69
|
+
"session-privacy"
|
|
70
|
+
],
|
|
71
|
+
"author": "DevFortress Team",
|
|
72
|
+
"license": "BUSL-1.1",
|
|
73
|
+
"repository": {
|
|
74
|
+
"type": "git",
|
|
75
|
+
"url": "https://github.com/duncan982/devfortress.git",
|
|
76
|
+
"directory": "packages/devfortress-sdk"
|
|
77
|
+
},
|
|
78
|
+
"bugs": {
|
|
79
|
+
"url": "https://github.com/duncan982/devfortress/issues"
|
|
80
|
+
},
|
|
81
|
+
"homepage": "https://devfortress.net",
|
|
82
|
+
"sideEffects": false,
|
|
83
|
+
"dependencies": {
|
|
84
|
+
"axios": "^1.6.0"
|
|
85
|
+
},
|
|
86
|
+
"devDependencies": {
|
|
87
|
+
"@types/node": "^20.0.0",
|
|
88
|
+
"@types/express": "^4.17.21 || ^5.0.0",
|
|
89
|
+
"typescript": "^5.0.0",
|
|
90
|
+
"jest": "^30.2.0",
|
|
91
|
+
"@types/jest": "^30.0.0"
|
|
92
|
+
},
|
|
93
|
+
"peerDependencies": {
|
|
94
|
+
"express": "^4.18.0 || ^5.0.0"
|
|
95
|
+
},
|
|
96
|
+
"peerDependenciesMeta": {
|
|
97
|
+
"express": {
|
|
98
|
+
"optional": true
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DevFortress FastAPI Middleware
|
|
3
|
+
|
|
4
|
+
Automatically monitors FastAPI applications for security events and sends
|
|
5
|
+
them to the DevFortress surveillance API.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from devfortress_middleware import DevFortressMiddleware
|
|
9
|
+
|
|
10
|
+
app = FastAPI()
|
|
11
|
+
app.add_middleware(
|
|
12
|
+
DevFortressMiddleware,
|
|
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 asyncio
|
|
22
|
+
from typing import Optional, List, Dict, Any, Callable
|
|
23
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
24
|
+
from starlette.requests import Request
|
|
25
|
+
from starlette.responses import Response
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
import httpx
|
|
29
|
+
HAS_HTTPX = True
|
|
30
|
+
except ImportError:
|
|
31
|
+
HAS_HTTPX = False
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
from urllib.request import Request as URLRequest, urlopen
|
|
35
|
+
from urllib.error import URLError
|
|
36
|
+
HAS_URLLIB = True
|
|
37
|
+
except ImportError:
|
|
38
|
+
HAS_URLLIB = False
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# SQL Injection patterns
|
|
42
|
+
SQL_PATTERNS = [
|
|
43
|
+
re.compile(r"(\bor\b.*=.*)", re.IGNORECASE),
|
|
44
|
+
re.compile(r"(\bunion\b.*\bselect\b)", re.IGNORECASE),
|
|
45
|
+
re.compile(r"(\bdrop\b.*\btable\b)", re.IGNORECASE),
|
|
46
|
+
re.compile(r"('\s*or\s*'1'\s*=\s*'1)", re.IGNORECASE),
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
# XSS patterns
|
|
50
|
+
XSS_PATTERNS = [
|
|
51
|
+
re.compile(r"<script[^>]*>[\s\S]*?</script>", re.IGNORECASE),
|
|
52
|
+
re.compile(r"javascript:", re.IGNORECASE),
|
|
53
|
+
re.compile(r"onerror\s*=\s*", re.IGNORECASE),
|
|
54
|
+
re.compile(r"onload\s*=\s*", re.IGNORECASE),
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
# Path traversal patterns
|
|
58
|
+
TRAVERSAL_PATTERNS = [
|
|
59
|
+
re.compile(r"\.\.[/\\]"),
|
|
60
|
+
re.compile(r"/etc/passwd", re.IGNORECASE),
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class DevFortressMiddleware(BaseHTTPMiddleware):
|
|
65
|
+
"""FastAPI middleware for DevFortress security monitoring."""
|
|
66
|
+
|
|
67
|
+
def __init__(
|
|
68
|
+
self,
|
|
69
|
+
app,
|
|
70
|
+
api_key: str,
|
|
71
|
+
endpoint: str = "https://www.devfortress.net/api/events/ingest",
|
|
72
|
+
exclude_paths: Optional[List[str]] = None,
|
|
73
|
+
capture_headers: bool = False,
|
|
74
|
+
debug: bool = False,
|
|
75
|
+
on_error: Optional[Callable[[Exception], None]] = None,
|
|
76
|
+
):
|
|
77
|
+
super().__init__(app)
|
|
78
|
+
self.api_key = api_key
|
|
79
|
+
self.endpoint = endpoint
|
|
80
|
+
self.exclude_paths = exclude_paths or ["/health", "/docs", "/openapi.json"]
|
|
81
|
+
self.capture_headers = capture_headers
|
|
82
|
+
self.debug = debug
|
|
83
|
+
self.on_error = on_error
|
|
84
|
+
|
|
85
|
+
async def dispatch(self, request: Request, call_next) -> Response:
|
|
86
|
+
# Skip excluded paths
|
|
87
|
+
if any(request.url.path.startswith(p) for p in self.exclude_paths):
|
|
88
|
+
return await call_next(request)
|
|
89
|
+
|
|
90
|
+
start_time = time.time()
|
|
91
|
+
response: Optional[Response] = None
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
response = await call_next(request)
|
|
95
|
+
except Exception as exc:
|
|
96
|
+
# Track server error
|
|
97
|
+
await self._track_event(
|
|
98
|
+
request=request,
|
|
99
|
+
status_code=500,
|
|
100
|
+
response_time=time.time() - start_time,
|
|
101
|
+
event_type="5xx_error",
|
|
102
|
+
severity="HIGH",
|
|
103
|
+
reason=f"Unhandled exception: {str(exc)}",
|
|
104
|
+
)
|
|
105
|
+
raise
|
|
106
|
+
|
|
107
|
+
response_time = time.time() - start_time
|
|
108
|
+
status_code = response.status_code
|
|
109
|
+
|
|
110
|
+
# Determine event type
|
|
111
|
+
event_type = None
|
|
112
|
+
severity = "LOW"
|
|
113
|
+
reason = None
|
|
114
|
+
|
|
115
|
+
if status_code in (401, 403):
|
|
116
|
+
event_type = "auth_failure"
|
|
117
|
+
severity = "MEDIUM"
|
|
118
|
+
reason = "Authentication or authorization failed"
|
|
119
|
+
elif status_code in (400, 422):
|
|
120
|
+
event_type = "validation_error"
|
|
121
|
+
severity = "LOW"
|
|
122
|
+
reason = "Request validation failed"
|
|
123
|
+
elif status_code == 429:
|
|
124
|
+
event_type = "rate_limit_exceeded"
|
|
125
|
+
severity = "MEDIUM"
|
|
126
|
+
reason = "Rate limit exceeded"
|
|
127
|
+
elif status_code >= 500:
|
|
128
|
+
event_type = "5xx_error"
|
|
129
|
+
severity = "HIGH"
|
|
130
|
+
reason = f"Server error: {status_code}"
|
|
131
|
+
elif status_code >= 400:
|
|
132
|
+
event_type = "4xx_error"
|
|
133
|
+
severity = "LOW"
|
|
134
|
+
reason = f"Client error: {status_code}"
|
|
135
|
+
|
|
136
|
+
# Check suspicious patterns
|
|
137
|
+
suspicious = self._detect_suspicious(request)
|
|
138
|
+
if suspicious:
|
|
139
|
+
event_type = "suspicious_pattern"
|
|
140
|
+
severity = "HIGH"
|
|
141
|
+
reason = f"Suspicious patterns: {', '.join(suspicious)}"
|
|
142
|
+
|
|
143
|
+
if event_type:
|
|
144
|
+
# Fire and forget
|
|
145
|
+
asyncio.create_task(
|
|
146
|
+
self._track_event(
|
|
147
|
+
request=request,
|
|
148
|
+
status_code=status_code,
|
|
149
|
+
response_time=response_time,
|
|
150
|
+
event_type=event_type,
|
|
151
|
+
severity=severity,
|
|
152
|
+
reason=reason,
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
return response
|
|
157
|
+
|
|
158
|
+
async def _track_event(
|
|
159
|
+
self,
|
|
160
|
+
request: Request,
|
|
161
|
+
status_code: int,
|
|
162
|
+
response_time: float,
|
|
163
|
+
event_type: str,
|
|
164
|
+
severity: str,
|
|
165
|
+
reason: Optional[str] = None,
|
|
166
|
+
):
|
|
167
|
+
"""Send event to DevFortress API."""
|
|
168
|
+
ip = (
|
|
169
|
+
request.headers.get("x-forwarded-for", "").split(",")[0].strip()
|
|
170
|
+
or request.headers.get("x-real-ip", "")
|
|
171
|
+
or (request.client.host if request.client else "0.0.0.0")
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
payload: Dict[str, Any] = {
|
|
175
|
+
"eventType": event_type,
|
|
176
|
+
"ip": ip,
|
|
177
|
+
"method": request.method,
|
|
178
|
+
"path": str(request.url.path),
|
|
179
|
+
"userAgent": request.headers.get("user-agent"),
|
|
180
|
+
"statusCode": status_code,
|
|
181
|
+
"responseTime": round(response_time * 1000),
|
|
182
|
+
"severity": severity,
|
|
183
|
+
"reason": reason,
|
|
184
|
+
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if self.capture_headers:
|
|
188
|
+
payload["metadata"] = {"headers": dict(request.headers)}
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
if HAS_HTTPX:
|
|
192
|
+
async with httpx.AsyncClient(timeout=5.0) as client:
|
|
193
|
+
await client.post(
|
|
194
|
+
self.endpoint,
|
|
195
|
+
json=payload,
|
|
196
|
+
headers={
|
|
197
|
+
"Content-Type": "application/json",
|
|
198
|
+
"X-DevFortress-Key": self.api_key,
|
|
199
|
+
},
|
|
200
|
+
)
|
|
201
|
+
elif HAS_URLLIB:
|
|
202
|
+
req = URLRequest(
|
|
203
|
+
self.endpoint,
|
|
204
|
+
data=json.dumps(payload).encode("utf-8"),
|
|
205
|
+
headers={
|
|
206
|
+
"Content-Type": "application/json",
|
|
207
|
+
"X-DevFortress-Key": self.api_key,
|
|
208
|
+
},
|
|
209
|
+
method="POST",
|
|
210
|
+
)
|
|
211
|
+
urlopen(req, timeout=5)
|
|
212
|
+
except Exception as exc:
|
|
213
|
+
if self.on_error:
|
|
214
|
+
self.on_error(exc)
|
|
215
|
+
elif self.debug:
|
|
216
|
+
print(f"[DevFortress] Failed to send event: {exc}") # noqa: T201
|
|
217
|
+
|
|
218
|
+
def _detect_suspicious(self, request: Request) -> List[str]:
|
|
219
|
+
"""Detect suspicious patterns in the request."""
|
|
220
|
+
patterns_found = []
|
|
221
|
+
url_str = str(request.url)
|
|
222
|
+
query_str = str(request.query_params)
|
|
223
|
+
check_str = f"{url_str} {query_str}"
|
|
224
|
+
|
|
225
|
+
if any(p.search(check_str) for p in SQL_PATTERNS):
|
|
226
|
+
patterns_found.append("sql_injection")
|
|
227
|
+
if any(p.search(check_str) for p in XSS_PATTERNS):
|
|
228
|
+
patterns_found.append("xss")
|
|
229
|
+
if any(p.search(check_str) for p in TRAVERSAL_PATTERNS):
|
|
230
|
+
patterns_found.append("path_traversal")
|
|
231
|
+
|
|
232
|
+
return patterns_found
|