iri-shield 1.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/src/threats.js ADDED
@@ -0,0 +1,297 @@
1
+ 'use strict';
2
+
3
+ const { isRuleEnabled } = require('./rules');
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Attack Rule Definitions — each rule has: name, configKey, score, label, category, regex
7
+ // ---------------------------------------------------------------------------
8
+
9
+ const ATTACK_RULES = [
10
+ { name: 'sql_injection', configKey: 'sqlInjection', score: 65, label: 'SQL Injection pattern', category: 'injection',
11
+ regex: /('|%27)\s*(or|and)\s*('|%27)?\s*['"\w]+\s*=\s*['"\w]+|union\s+(all\s+)?select|drop\s+table|insert\s+into|delete\s+from|update\s+\w+\s+set|exec\s*\(|execute\s*\(|xp_cmdshell|--|;\s*waitfor\s+delay|sleep\(\d+\)|benchmark\(\d+/i },
12
+ { name: 'xss_pattern', configKey: 'xss', score: 60, label: 'Cross-Site Scripting (XSS) attempt', category: 'injection',
13
+ regex: /<script[\s>]|javascript\s*:|onerror\s*=|onload\s*=|onfocus\s*=|onclick\s*=|eval\s*\(|document\.cookie|document\.write|innerHTML\s*=|src\s*=\s*["']?javascript|<svg|<body|<iframe/i },
14
+ { name: 'path_traversal', configKey: 'pathTraversal', score: 65, label: 'Path traversal attack', category: 'traversal',
15
+ regex: /\.\.\/|\.\.\\/i },
16
+ { name: 'secret_probe', configKey: 'secretProbe', score: 60, label: 'Secret / config file probe', category: 'recon',
17
+ regex: /\.env|config\.json|wp-config|private[-_]?key|id_rsa|\.git\/|\.htaccess|passwd|shadow|docker-compose|\/debug|\/secrets|\/internal/i },
18
+ { name: 'command_injection', configKey: 'commandInjection', score: 75, label: 'Command injection attempt', category: 'injection',
19
+ regex: /;\s*(ls|cat|wget|curl|bash|sh|nc|netcat|python|perl|ruby|id|whoami|uname|dir)\b|&&\s*(ls|cat|id|whoami|dir)|\|\s*(ls|cat|bash|sh|nc|dir|id|whoami)\b|`[^`]+`|\$\([^)]+\)/i },
20
+ { name: 'ssti_pattern', configKey: 'ssti', score: 65, label: 'Server-Side Template Injection (SSTI)', category: 'injection',
21
+ regex: /\{\{[\s\S]*?\}\}|\$\{[\s\S]*?\}|#\{[\s\S]*?\}|<%[\s\S]*?%>|\{%[\s\S]*?%\}|{{7\*7}|\${7\*7}/i },
22
+ { name: 'nosql_injection', configKey: 'nosqlInjection', score: 60, label: 'NoSQL Injection attempt', category: 'injection',
23
+ regex: /\$where|\$gt\b|"\$gt"|\[\$gt\]|\$ne\b|"\$ne"|\[\$ne\]|\$regex\b|"\$regex"|\[\$regex\]|\$exists\b|"\$exists"|\[\$exists\]|\$in\b|"\$in"|\[\$in\]|\$or\b|"\$or"|\[\$or\]/i },
24
+ { name: 'ldap_injection', configKey: 'ldapInjection', score: 60, label: 'LDAP Injection attempt', category: 'injection',
25
+ regex: /[)(|*\\]{3,}|\(\|[\w=*]+\)|\(&[\w=*]+\)|\)\s*\(&|\)\s*\(\|/ },
26
+ { name: 'xxe_pattern', configKey: 'xxe', score: 70, label: 'XXE / XML Injection', category: 'injection',
27
+ regex: /<!ENTITY|<!DOCTYPE[\s\S]*?SYSTEM|SYSTEM\s+["']https?:|file:\/\/\/|<!ELEMENT|<!ATTLIST/i },
28
+ { name: 'open_redirect', configKey: 'openRedirect', score: 60, label: 'Open redirect attempt', category: 'redirect',
29
+ regex: /[?&](redirect|return|url|next|to|dest|destination|ref|redir|return_url)\s*=\s*https?:\/\//i },
30
+ { name: 'base64_payload', configKey: 'base64Payload', score: 60, label: 'Suspicious base64-encoded payload', category: 'evasion',
31
+ regex: /[?&][a-zA-Z0-9_.-]+=(?:[A-Za-z0-9+/]{24,}={0,2})(?:&|$)/ },
32
+ { name: 'header_injection', configKey: 'headerInjection', score: 60, label: 'HTTP header injection', category: 'injection',
33
+ regex: /(%0d%0a|%0a%0d|\r\n|\n\r).*?:|%0a%0d|%0d%0a|\r\n|\n\r/i }
34
+ ];
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // Scanner / Bot User-Agent patterns
38
+ // ---------------------------------------------------------------------------
39
+
40
+ const SCANNER_UA_PATTERNS = [
41
+ { name: 'sqlmap', score: 65, regex: /sqlmap/i },
42
+ { name: 'nikto', score: 65, regex: /nikto/i },
43
+ { name: 'masscan', score: 65, regex: /masscan/i },
44
+ { name: 'nmap', score: 60, regex: /nmap/i },
45
+ { name: 'zgrab', score: 60, regex: /zgrab/i },
46
+ { name: 'dirbuster', score: 65, regex: /dirbuster/i },
47
+ { name: 'gobuster', score: 65, regex: /gobuster/i },
48
+ { name: 'wfuzz', score: 65, regex: /wfuzz/i },
49
+ { name: 'hydra', score: 65, regex: /hydra/i },
50
+ { name: 'burpsuite', score: 60, regex: /burpsuite|burp\s*suite/i },
51
+ { name: 'metasploit', score: 70, regex: /metasploit/i },
52
+ { name: 'headless_chrome', score: 60, regex: /HeadlessChrome/i },
53
+ { name: 'phantomjs', score: 60, regex: /PhantomJS/i },
54
+ { name: 'puppeteer', score: 50, regex: /puppeteer/i },
55
+ { name: 'python_requests', score: 35, regex: /python-requests\//i },
56
+ { name: 'go_http', score: 35, regex: /^Go-http-client\//i },
57
+ { name: 'curl_automated', score: 25, regex: /^curl\//i },
58
+ { name: 'wget', score: 35, regex: /^Wget\//i },
59
+ { name: 'java_ua', score: 35, regex: /^Java\//i },
60
+ { name: 'libwww', score: 40, regex: /libwww-perl|LWP::/i }
61
+ ];
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // Core Analysis Function
65
+ // Returns: { score, riskLevel, action, threats, reasons, breakdown, confidence, anomalies }
66
+ // ---------------------------------------------------------------------------
67
+
68
+ function analyzeRequest(req, storage, config) {
69
+ const ip = (req.iriShieldClient && req.iriShieldClient.ip) || getClientIp(req);
70
+ const endpoint = req.originalUrl || req.url || '/';
71
+ const userAgent = (req.iriShieldClient && req.iriShieldClient.userAgent) || req.headers['user-agent'] || '';
72
+ const headerSignals = (req.iriShieldClient && req.iriShieldClient.headerSignals) || {};
73
+ const text = collectText(req);
74
+
75
+ const scoreParts = [];
76
+ const threats = [];
77
+ const reasons = [];
78
+ const breakdown = [];
79
+ const anomalies = [];
80
+
81
+ const modeMultiplier = getSecurityMultiplier(config.security);
82
+
83
+ // --- Payload / URL attack pattern scanning ---
84
+ for (const rule of ATTACK_RULES) {
85
+ if (!isRuleEnabled(config, rule.configKey)) continue;
86
+ if (rule.regex.test(text)) {
87
+ scoreParts.push(rule.score);
88
+ threats.push(rule.name);
89
+ reasons.push(rule.name);
90
+ breakdown.push({
91
+ rule: rule.name,
92
+ label: rule.label,
93
+ points: rule.score,
94
+ category: rule.category,
95
+ confidence: 90
96
+ });
97
+ }
98
+ }
99
+
100
+ // --- Scanner / Bot detection ---
101
+ if (isRuleEnabled(config, 'scannerDetection')) {
102
+ for (const rule of SCANNER_UA_PATTERNS) {
103
+ if (rule.regex.test(userAgent)) {
104
+ scoreParts.push(rule.score);
105
+ threats.push('scanner_ua_' + rule.name);
106
+ reasons.push('ua_matches_' + rule.name);
107
+ breakdown.push({
108
+ rule: 'scanner_ua_' + rule.name,
109
+ label: 'Scanner / Bot detected (' + rule.name + ')',
110
+ points: rule.score,
111
+ category: 'scanner',
112
+ confidence: 95
113
+ });
114
+ break;
115
+ }
116
+ }
117
+ }
118
+
119
+ // --- Empty / missing User-Agent ---
120
+ if (!userAgent.trim()) {
121
+ scoreParts.push(20);
122
+ threats.push('missing_user_agent');
123
+ reasons.push('no_user_agent_header');
124
+ breakdown.push({ rule: 'missing_user_agent', label: 'Missing User-Agent header', points: 20, category: 'anomaly', confidence: 80 });
125
+ anomalies.push('missing_user_agent');
126
+ }
127
+
128
+ // --- HTTP Method anomaly ---
129
+ const allowed = (config.anomaly && config.anomaly.allowedMethods) || ['GET','POST','PUT','PATCH','DELETE','OPTIONS'];
130
+ if (allowed.length && !allowed.includes(req.method)) {
131
+ scoreParts.push(20);
132
+ threats.push('unusual_http_method');
133
+ reasons.push('method_' + req.method + '_not_allowed');
134
+ breakdown.push({ rule: 'unusual_http_method', label: 'Unusual HTTP method: ' + req.method, points: 20, category: 'anomaly', confidence: 75 });
135
+ anomalies.push('unusual_http_method');
136
+ }
137
+
138
+ // --- Single endpoint flood ---
139
+ const endpointHits = storage.recordEndpointHit(ip, endpoint);
140
+ if (endpointHits > ((config.anomaly && config.anomaly.singleEndpointMax) || 80)) {
141
+ scoreParts.push(25);
142
+ threats.push('single_endpoint_flood');
143
+ reasons.push('endpoint_hits_' + endpointHits);
144
+ breakdown.push({ rule: 'single_endpoint_flood', label: 'Abnormal request rate to single endpoint (' + endpointHits + ' hits)', points: 25, category: 'anomaly', confidence: 85 });
145
+ anomalies.push('single_endpoint_flood');
146
+ }
147
+
148
+ // --- Sensitive endpoint access ---
149
+ const normalizedEndpoint = endpoint.toLowerCase();
150
+ const sensitivePatterns = (config.anomaly && config.anomaly.sensitiveEndpoints) || ['/admin','/internal','/debug','/.env'];
151
+ if (sensitivePatterns.some(function(item) { return normalizedEndpoint.includes(item.toLowerCase()); })) {
152
+ scoreParts.push(20);
153
+ threats.push('sensitive_endpoint_access');
154
+ reasons.push('sensitive_endpoint_' + endpoint);
155
+ breakdown.push({ rule: 'sensitive_endpoint_access', label: 'Access to sensitive endpoint: ' + endpoint, points: 20, category: 'anomaly', confidence: 80 });
156
+ anomalies.push('sensitive_endpoint_access');
157
+ }
158
+
159
+ // --- Failed auth tracking ---
160
+ if (isFailedAuthRequest(req)) {
161
+ const failedCount = storage.recordFailedAuth(ip);
162
+ if (failedCount >= ((config.anomaly && config.anomaly.failedAuthMax) || 3)) {
163
+ const authScore = 65;
164
+ scoreParts.push(authScore);
165
+ threats.push('repeated_failed_auth');
166
+ reasons.push('failed_auth_count_' + failedCount);
167
+ breakdown.push({ rule: 'repeated_failed_auth', label: 'Brute force authentication (' + failedCount + ' attempts)', points: authScore, category: 'anomaly', confidence: 90 });
168
+ anomalies.push('repeated_failed_auth');
169
+ }
170
+ }
171
+
172
+ // --- Header anomalies ---
173
+ if (isRuleEnabled(config, 'headerAnomaly')) {
174
+ if (headerSignals.missingBrowserHeaders) {
175
+ scoreParts.push(15);
176
+ threats.push('header_anomaly_missing_browser_headers');
177
+ reasons.push('browser_ua_without_standard_headers');
178
+ breakdown.push({ rule: 'header_anomaly_missing_browser_headers', label: 'Browser UA without standard headers', points: 15, category: 'anomaly', confidence: 70 });
179
+ anomalies.push('header_anomaly_missing_browser_headers');
180
+ }
181
+ if (headerSignals.missingModernHeaders) {
182
+ scoreParts.push(10);
183
+ threats.push('header_anomaly_missing_modern_headers');
184
+ reasons.push('chrome_ua_without_sec_fetch_headers');
185
+ breakdown.push({ rule: 'header_anomaly_missing_modern_headers', label: 'Chrome UA without Sec-Fetch headers', points: 10, category: 'anomaly', confidence: 65 });
186
+ anomalies.push('header_anomaly_missing_modern_headers');
187
+ }
188
+ }
189
+
190
+ // --- Compute score ---
191
+ const rawScore = scoreParts.reduce(function(sum, v) { return sum + v; }, 0);
192
+ const score = Math.min(100, Math.round(rawScore * modeMultiplier));
193
+ const riskLevel = riskFromScore(score, config);
194
+ const action = actionFromRisk(riskLevel);
195
+
196
+ // --- Compute confidence ---
197
+ // Confidence = weighted average of individual rule confidences
198
+ const confidence = breakdown.length > 0
199
+ ? Math.round(breakdown.reduce(function(sum, b) { return sum + b.confidence; }, 0) / breakdown.length)
200
+ : 0;
201
+
202
+ return {
203
+ score,
204
+ riskLevel,
205
+ action,
206
+ threats,
207
+ reasons,
208
+ breakdown,
209
+ confidence,
210
+ anomalies
211
+ };
212
+ }
213
+
214
+ // ---------------------------------------------------------------------------
215
+ // Helpers
216
+ // ---------------------------------------------------------------------------
217
+
218
+ function collectText(req) {
219
+ const rawUrl = req.originalUrl || req.url || '';
220
+ let decodedUrl = '';
221
+ try { decodedUrl = decodeURIComponent(rawUrl); } catch (_) { decodedUrl = rawUrl; }
222
+
223
+ const chunks = [rawUrl, decodedUrl, req.headers['user-agent'] || ''];
224
+
225
+ // Auto-decode base64 parameter payloads for deep inspection
226
+ const b64Matches = (decodedUrl + ' ' + (typeof req.body === 'string' ? req.body : '')).match(/[A-Za-z0-9+/]{20,}={0,2}/g) || [];
227
+ for (const b64 of b64Matches) {
228
+ try {
229
+ const decodedB64 = Buffer.from(b64, 'base64').toString('utf8');
230
+ if (decodedB64 && /[a-zA-Z0-9<>'";=]/.test(decodedB64)) chunks.push(decodedB64);
231
+ } catch (_) {}
232
+ }
233
+
234
+ if (req.query) {
235
+ chunks.push(JSON.stringify(req.query));
236
+ try { chunks.push(decodeURIComponent(JSON.stringify(req.query))); } catch (_) {}
237
+ }
238
+ if (req.body && typeof req.body === 'object') {
239
+ const bodyClone = Object.assign({}, req.body);
240
+ delete bodyClone.__iri;
241
+ delete bodyClone.iriShieldTest;
242
+ chunks.push(JSON.stringify(bodyClone));
243
+ }
244
+ if (typeof req.body === 'string') chunks.push(req.body);
245
+ const suspiciousHeaders = ['x-forwarded-for','referer','x-custom-ip'];
246
+ for (const h of suspiciousHeaders) {
247
+ if (req.headers[h]) chunks.push(req.headers[h]);
248
+ }
249
+ return chunks.join(' ');
250
+ }
251
+
252
+ function isFailedAuthRequest(req) {
253
+ const path = String(req.originalUrl || req.url || '').toLowerCase();
254
+ return (
255
+ req.method === 'POST' &&
256
+ (path.includes('/login') || path.includes('/auth') || path.includes('/signin')) &&
257
+ !req.headers.authorization
258
+ );
259
+ }
260
+
261
+ function getClientIp(req) {
262
+ const forwarded = req.headers['x-forwarded-for'];
263
+ if (typeof forwarded === 'string' && forwarded.trim()) return forwarded.split(',')[0].trim();
264
+ return req.ip || (req.socket && req.socket.remoteAddress) || (req.connection && req.connection.remoteAddress) || 'unknown';
265
+ }
266
+
267
+ function getSecurityMultiplier(mode) {
268
+ if (mode === 'high') return 1.3;
269
+ if (mode === 'low') return 0.7;
270
+ return 1.0;
271
+ }
272
+
273
+ function riskFromScore(score, config) {
274
+ const anomaly = (config && config.anomaly) || {};
275
+ if (score >= (anomaly.criticalThreshold || 90)) return 'critical';
276
+ if (score >= (anomaly.highThreshold || 65)) return 'high';
277
+ if (score >= (anomaly.mediumThreshold || 35)) return 'medium';
278
+ if (score > 0) return 'low';
279
+ return 'none';
280
+ }
281
+
282
+ function actionFromRisk(riskLevel) {
283
+ if (riskLevel === 'critical') return 'blocked';
284
+ if (riskLevel === 'high') return 'temporary_block';
285
+ if (riskLevel === 'medium') return 'rate_limited';
286
+ if (riskLevel === 'low') return 'logged';
287
+ return 'none';
288
+ }
289
+
290
+ module.exports = {
291
+ analyzeRequest,
292
+ ATTACK_RULES,
293
+ SCANNER_UA_PATTERNS,
294
+ riskFromScore,
295
+ actionFromRisk,
296
+ getSecurityMultiplier
297
+ };