@roboteby/parry 1.1.1 → 2.0.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/CHANGELOG.md +51 -20
- package/README.md +88 -239
- package/config/defaults.js +4 -1
- package/config/validate.js +255 -0
- package/constants/patterns.js +3 -4
- package/package.json +55 -24
- package/src/admin/auth/admin-auth.js +81 -17
- package/src/admin/auth/strategies/none.js +6 -2
- package/src/core/engine.js +75 -15
- package/src/core/index.js +2 -2
- package/src/core/scoring.js +5 -1
- package/src/detectors/nosql.js +33 -13
- package/src/detectors/path-traversal.js +3 -1
- package/src/express/ip-resolver.js +16 -2
- package/src/express/middleware.js +13 -4
- package/src/express/request-targets.js +26 -16
- package/src/index.js +1 -1
- package/src/utils/decode.js +3 -1
- package/src/utils/normalize.js +3 -1
- package/types/admin.d.ts +26 -0
- package/types/brute-force.d.ts +44 -0
- package/types/core.d.ts +1 -0
- package/types/detectors.d.ts +9 -0
- package/types/events.d.ts +16 -0
- package/types/index.d.ts +68 -17
- package/types/observability.d.ts +12 -0
- package/types/policies.d.ts +21 -0
- package/types/stores.d.ts +10 -0
- package/src/core/logger.js +0 -3
- package/src/core/rateLimiter.js +0 -3
- package/src/middleware/index.js +0 -7
- package/src/middleware/parry_ddos.js +0 -3
- package/src/stores/README.md +0 -51
package/src/core/engine.js
CHANGED
|
@@ -10,6 +10,7 @@ const {
|
|
|
10
10
|
RequestShapeGuard,
|
|
11
11
|
} = require('../detectors');
|
|
12
12
|
const { safeStringify } = require('../utils/normalize');
|
|
13
|
+
const { collectRequestTargets } = require('../express/request-targets');
|
|
13
14
|
const { severityForThreats } = require('./scoring');
|
|
14
15
|
const {
|
|
15
16
|
createThreatEvent,
|
|
@@ -71,10 +72,17 @@ async function analyzeRequest(requestData, context) {
|
|
|
71
72
|
};
|
|
72
73
|
}
|
|
73
74
|
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
const shapeThreat = scanRequestShape(requestData, config);
|
|
76
|
+
const shapeExceeded = Boolean(shapeThreat);
|
|
77
|
+
const targets = shapeExceeded
|
|
78
|
+
? []
|
|
79
|
+
: requestData.targets || collectRequestTargets(requestData, config);
|
|
80
|
+
const structuredThreats = shapeExceeded
|
|
81
|
+
? [shapeThreat]
|
|
82
|
+
: scanStructuredGuards(requestData, config, targets);
|
|
83
|
+
const threats = deduplicateThreats(
|
|
84
|
+
shapeExceeded ? structuredThreats : [...structuredThreats, ...scanTargets(targets, config)]
|
|
85
|
+
);
|
|
78
86
|
|
|
79
87
|
if (threats.length > 0) {
|
|
80
88
|
if (config.rateLimit && rateLimiter) {
|
|
@@ -189,7 +197,13 @@ function createRequestEventContext(requestData, timestamp) {
|
|
|
189
197
|
}
|
|
190
198
|
|
|
191
199
|
function scanApplicationLayerGuards(requestData, config) {
|
|
192
|
-
const
|
|
200
|
+
const shapeThreat = scanRequestShape(requestData, config);
|
|
201
|
+
if (shapeThreat) return [shapeThreat];
|
|
202
|
+
const targets = requestData.targets || collectRequestTargets(requestData, config);
|
|
203
|
+
return scanStructuredGuards(requestData, config, targets);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function scanRequestShape(requestData, config) {
|
|
193
207
|
const surfaces = {
|
|
194
208
|
query: requestData.query || {},
|
|
195
209
|
params: requestData.params || {},
|
|
@@ -198,8 +212,18 @@ function scanApplicationLayerGuards(requestData, config) {
|
|
|
198
212
|
|
|
199
213
|
if (config.requestShape?.enabled) {
|
|
200
214
|
const hit = RequestShapeGuard.scan(surfaces, config.requestShape);
|
|
201
|
-
if (hit) return
|
|
215
|
+
if (hit) return hit;
|
|
202
216
|
}
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function scanStructuredGuards(requestData, config, targets) {
|
|
221
|
+
const threats = [];
|
|
222
|
+
const surfaces = {
|
|
223
|
+
query: requestData.query || {},
|
|
224
|
+
params: requestData.params || {},
|
|
225
|
+
body: requestData.body,
|
|
226
|
+
};
|
|
203
227
|
|
|
204
228
|
if (config.hpp?.enabled) {
|
|
205
229
|
const hit = HPPDetector.scan(surfaces.query, config.hpp);
|
|
@@ -211,8 +235,25 @@ function scanApplicationLayerGuards(requestData, config) {
|
|
|
211
235
|
if (hit) threats.push(hit);
|
|
212
236
|
}
|
|
213
237
|
|
|
238
|
+
if (config.nosql) {
|
|
239
|
+
for (const [surface, value] of Object.entries(surfaces)) {
|
|
240
|
+
const hit = NoSQLDetector.inspect(value, {
|
|
241
|
+
rootPath: surface,
|
|
242
|
+
allowedOperators: config.nosqlConfig?.allowedOperators,
|
|
243
|
+
});
|
|
244
|
+
if (hit) {
|
|
245
|
+
threats.push({
|
|
246
|
+
detector: 'NOSQL_INJECTION',
|
|
247
|
+
field: hit.path,
|
|
248
|
+
pattern: hit.pattern,
|
|
249
|
+
reason: 'NoSQL operator or expression detected',
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
214
255
|
if (config.pathTraversal?.enabled) {
|
|
215
|
-
const hit = PathTraversalDetector.scan(
|
|
256
|
+
const hit = PathTraversalDetector.scan(targets);
|
|
216
257
|
if (hit) threats.push(hit);
|
|
217
258
|
}
|
|
218
259
|
|
|
@@ -227,23 +268,42 @@ function scanTargets(targets, config) {
|
|
|
227
268
|
|
|
228
269
|
if (config.sql) {
|
|
229
270
|
const hit = SQLInjectionDetector.scan(str);
|
|
230
|
-
if (hit)
|
|
271
|
+
if (hit) {
|
|
272
|
+
threats.push({
|
|
273
|
+
detector: 'SQL_INJECTION',
|
|
274
|
+
field: label,
|
|
275
|
+
pattern: hit,
|
|
276
|
+
reason: 'SQL injection pattern detected',
|
|
277
|
+
});
|
|
278
|
+
}
|
|
231
279
|
}
|
|
232
280
|
|
|
233
281
|
if (config.xss) {
|
|
234
282
|
const hit = XSSDetector.scan(str);
|
|
235
|
-
if (hit)
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
283
|
+
if (hit) {
|
|
284
|
+
threats.push({
|
|
285
|
+
detector: 'XSS',
|
|
286
|
+
field: label,
|
|
287
|
+
pattern: hit,
|
|
288
|
+
reason: 'Cross-site scripting pattern detected',
|
|
289
|
+
});
|
|
290
|
+
}
|
|
241
291
|
}
|
|
242
292
|
}
|
|
243
293
|
|
|
244
294
|
return threats;
|
|
245
295
|
}
|
|
246
296
|
|
|
297
|
+
function deduplicateThreats(threats) {
|
|
298
|
+
const seen = new Set();
|
|
299
|
+
return threats.filter((threat) => {
|
|
300
|
+
const key = [threat.detector, threat.field, threat.pattern, threat.reason].join('\u0000');
|
|
301
|
+
if (seen.has(key)) return false;
|
|
302
|
+
seen.add(key);
|
|
303
|
+
return true;
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
247
307
|
function enrichThreats(threats) {
|
|
248
308
|
return threats.map((threat) => ({
|
|
249
309
|
...threat,
|
|
@@ -261,4 +321,4 @@ function toResponseThreat(threat) {
|
|
|
261
321
|
return responseThreat;
|
|
262
322
|
}
|
|
263
323
|
|
|
264
|
-
module.exports = { analyzeRequest, scanTargets, scanApplicationLayerGuards };
|
|
324
|
+
module.exports = { analyzeRequest, scanTargets, scanApplicationLayerGuards, deduplicateThreats };
|
package/src/core/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { RateLimiter } = require('
|
|
4
|
-
const { ThreatLogger } = require('
|
|
3
|
+
const { RateLimiter } = require('../rate-limit/limiter');
|
|
4
|
+
const { ThreatLogger } = require('../logger/console-reporter');
|
|
5
5
|
const { MemoryStore, RedisStore } = require('../stores');
|
|
6
6
|
|
|
7
7
|
module.exports = { RateLimiter, ThreatLogger, MemoryStore, RedisStore };
|
package/src/core/scoring.js
CHANGED
|
@@ -12,7 +12,11 @@ const THREAT_SEVERITY = {
|
|
|
12
12
|
|
|
13
13
|
function severityForThreats(threats) {
|
|
14
14
|
if (!threats || threats.length === 0) return 'none';
|
|
15
|
-
|
|
15
|
+
const rank = { none: 0, low: 1, medium: 2, high: 3, critical: 4 };
|
|
16
|
+
return threats.reduce((highest, threat) => {
|
|
17
|
+
const severity = threat.severity || THREAT_SEVERITY[threat.detector] || 'medium';
|
|
18
|
+
return rank[severity] > rank[highest] ? severity : highest;
|
|
19
|
+
}, 'none');
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
module.exports = { severityForThreats };
|
package/src/detectors/nosql.js
CHANGED
|
@@ -8,46 +8,66 @@ const {
|
|
|
8
8
|
|
|
9
9
|
const NoSQLDetector = {
|
|
10
10
|
/** @param {*} value @returns {string|null} */
|
|
11
|
-
scan(value) {
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
scan(value, options = {}) {
|
|
12
|
+
return this.inspect(value, options)?.pattern || null;
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
inspect(value, options = {}) {
|
|
16
|
+
const rootPath = options.rootPath || 'value';
|
|
17
|
+
const allowedOperators = options.allowedOperators || {};
|
|
18
|
+
if (value !== null && typeof value === 'object') {
|
|
19
|
+
return _scanObject(value, rootPath, allowedOperators);
|
|
20
|
+
}
|
|
21
|
+
if (typeof value === 'string') return _scanString(value, rootPath, allowedOperators);
|
|
14
22
|
return null;
|
|
15
23
|
},
|
|
16
24
|
};
|
|
17
25
|
|
|
18
|
-
function _scanObject(obj, depth = 0) {
|
|
19
|
-
if (depth >
|
|
26
|
+
function _scanObject(obj, path, allowedOperators, depth = 0, seen = new WeakSet()) {
|
|
27
|
+
if (depth > 8 || seen.has(obj)) return null;
|
|
28
|
+
seen.add(obj);
|
|
20
29
|
for (const key of Object.keys(obj)) {
|
|
21
|
-
if (NOSQL_DANGEROUS_OPERATORS.has(key))
|
|
22
|
-
|
|
30
|
+
if (NOSQL_DANGEROUS_OPERATORS.has(key)) {
|
|
31
|
+
return { pattern: `Operador perigoso: ${key}`, path: `${path}.${key}` };
|
|
32
|
+
}
|
|
33
|
+
if (NOSQL_SUSPICIOUS_OPERATORS.has(key) && !isAllowed(path, key, allowedOperators)) {
|
|
34
|
+
return { pattern: `Operador suspeito: ${key}`, path: `${path}.${key}` };
|
|
35
|
+
}
|
|
23
36
|
const val = obj[key];
|
|
37
|
+
const childPath = Array.isArray(obj) ? `${path}[${key}]` : `${path}.${key}`;
|
|
24
38
|
if (val && typeof val === 'object') {
|
|
25
|
-
const nested = _scanObject(val, depth + 1);
|
|
39
|
+
const nested = _scanObject(val, childPath, allowedOperators, depth + 1, seen);
|
|
26
40
|
if (nested) return nested;
|
|
27
41
|
}
|
|
28
42
|
if (typeof val === 'string') {
|
|
29
|
-
const hit = _scanString(val);
|
|
43
|
+
const hit = _scanString(val, childPath, allowedOperators);
|
|
30
44
|
if (hit) return hit;
|
|
31
45
|
}
|
|
32
46
|
}
|
|
33
47
|
return null;
|
|
34
48
|
}
|
|
35
49
|
|
|
36
|
-
function _scanString(value) {
|
|
50
|
+
function _scanString(value, path, allowedOperators) {
|
|
37
51
|
if (!value || value.trim() === '') return null;
|
|
38
52
|
if (value.trim().startsWith('{') || value.trim().startsWith('[')) {
|
|
39
53
|
try {
|
|
40
54
|
const parsed = JSON.parse(value);
|
|
41
55
|
if (parsed && typeof parsed === 'object') {
|
|
42
|
-
const hit = _scanObject(parsed);
|
|
56
|
+
const hit = _scanObject(parsed, path, allowedOperators);
|
|
43
57
|
if (hit) return hit;
|
|
44
58
|
}
|
|
45
|
-
} catch (_) {
|
|
59
|
+
} catch (_) {
|
|
60
|
+
// Non-JSON strings are checked by the bounded patterns below.
|
|
61
|
+
}
|
|
46
62
|
}
|
|
47
63
|
for (const pattern of NOSQL_STRING_PATTERNS) {
|
|
48
|
-
if (pattern.test(value)) return pattern.toString();
|
|
64
|
+
if (pattern.test(value)) return { pattern: pattern.toString(), path };
|
|
49
65
|
}
|
|
50
66
|
return null;
|
|
51
67
|
}
|
|
52
68
|
|
|
69
|
+
function isAllowed(path, operator, allowedOperators) {
|
|
70
|
+
return Array.isArray(allowedOperators[path]) && allowedOperators[path].includes(operator);
|
|
71
|
+
}
|
|
72
|
+
|
|
53
73
|
module.exports = { NoSQLDetector };
|
|
@@ -60,7 +60,9 @@ function collectStrings(value, label, seen = new WeakSet()) {
|
|
|
60
60
|
for (const [key, child] of Object.entries(value)) {
|
|
61
61
|
strings.push(...collectStrings(child, `${label}.${key}`, seen));
|
|
62
62
|
}
|
|
63
|
-
} catch (_) {
|
|
63
|
+
} catch (_) {
|
|
64
|
+
// Ignore objects that cannot expose enumerable values.
|
|
65
|
+
}
|
|
64
66
|
|
|
65
67
|
return strings;
|
|
66
68
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const ipaddr = require('ipaddr.js');
|
|
4
|
+
const MAX_PROXY_HOPS = 20;
|
|
4
5
|
|
|
5
6
|
function getClientIp(req, options = {}) {
|
|
6
7
|
const headers = req.headers || {};
|
|
@@ -11,9 +12,22 @@ function getClientIp(req, options = {}) {
|
|
|
11
12
|
if (!isTrustedProxy(directIp, options.trustedProxies || [])) return directIp;
|
|
12
13
|
|
|
13
14
|
const forwarded = getHeader(headers, 'x-forwarded-for');
|
|
14
|
-
|
|
15
|
+
if (forwarded) {
|
|
16
|
+
const rawHops = String(forwarded).split(',');
|
|
17
|
+
if (rawHops.length === 0 || rawHops.length > MAX_PROXY_HOPS) return directIp;
|
|
18
|
+
const hops = rawHops.map((hop) => parseIp(hop));
|
|
19
|
+
if (hops.some((hop) => !hop)) return directIp;
|
|
20
|
+
|
|
21
|
+
for (let index = hops.length - 1; index >= 0; index -= 1) {
|
|
22
|
+
const hop = hops[index].toString();
|
|
23
|
+
if (!isTrustedProxy(hop, options.trustedProxies || [])) return hop;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return hops[0]?.toString() || directIp;
|
|
27
|
+
}
|
|
28
|
+
|
|
15
29
|
const realIp = getHeader(headers, 'x-real-ip');
|
|
16
|
-
return
|
|
30
|
+
return realIp && parseIp(realIp) ? normalizeIp(realIp) : directIp;
|
|
17
31
|
}
|
|
18
32
|
|
|
19
33
|
function resolveClientIP(req, options = {}) {
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const crypto = require('node:crypto');
|
|
3
4
|
const { DEFAULTS } = require('../../config/defaults');
|
|
5
|
+
const {
|
|
6
|
+
normalizeHeadersConfig,
|
|
7
|
+
normalizeNoSQLConfig,
|
|
8
|
+
validateParryOptions,
|
|
9
|
+
} = require('../../config/validate');
|
|
4
10
|
const { analyzeRequest } = require('../core/engine');
|
|
5
11
|
const { RateLimiter } = require('../rate-limit/limiter');
|
|
6
12
|
const { ThreatLogger } = require('../logger/console-reporter');
|
|
@@ -16,14 +22,14 @@ const {
|
|
|
16
22
|
observeAuthenticationResult,
|
|
17
23
|
} = require('../brute-force');
|
|
18
24
|
const { resolveClientIP } = require('./ip-resolver');
|
|
19
|
-
const { collectRequestTargets } = require('./request-targets');
|
|
20
25
|
const { setRateLimitHeaders, respond } = require('./response');
|
|
21
26
|
|
|
22
27
|
/**
|
|
23
28
|
* Detects SQL Injection, XSS and NoSQL Injection in real-time.
|
|
24
29
|
* Applies intelligent Rate Limiting with automatic banning for suspicious behavior.
|
|
25
30
|
*
|
|
26
|
-
* @
|
|
31
|
+
* @deprecated Use createParry() instead.
|
|
32
|
+
* @param {import('../../types/index').ParryOptions} options
|
|
27
33
|
* @returns {import('express').RequestHandler}
|
|
28
34
|
*/
|
|
29
35
|
function Parry_DDoS(options = {}) {
|
|
@@ -101,7 +107,6 @@ async function handleRequest(req, res, next, context) {
|
|
|
101
107
|
query: req.query || {},
|
|
102
108
|
params: req.params || {},
|
|
103
109
|
body: req.body,
|
|
104
|
-
targets: collectRequestTargets(req, config.maxObjectDepth),
|
|
105
110
|
requestId,
|
|
106
111
|
userAgent: getHeader(req.headers || {}, 'user-agent'),
|
|
107
112
|
};
|
|
@@ -175,6 +180,7 @@ async function handleRequest(req, res, next, context) {
|
|
|
175
180
|
}
|
|
176
181
|
|
|
177
182
|
function mergeConfig(options) {
|
|
183
|
+
validateParryOptions(options);
|
|
178
184
|
const config = { ...DEFAULTS, ...options };
|
|
179
185
|
|
|
180
186
|
for (const key of ['hpp', 'prototypePollution', 'pathTraversal', 'requestShape']) {
|
|
@@ -198,6 +204,9 @@ function mergeConfig(options) {
|
|
|
198
204
|
config.storeFailureMode =
|
|
199
205
|
options.storeFailureMode === 'fail-closed' ? 'fail-closed' : 'fail-open';
|
|
200
206
|
config.policies = buildPolicies(options);
|
|
207
|
+
config.headers = normalizeHeadersConfig(options.headers);
|
|
208
|
+
config.nosqlConfig = normalizeNoSQLConfig(options.nosql);
|
|
209
|
+
config.nosql = config.nosqlConfig.enabled;
|
|
201
210
|
config.bruteForce =
|
|
202
211
|
options.bruteForce === false
|
|
203
212
|
? false
|
|
@@ -366,7 +375,7 @@ function resolveRequestId(req, res, config) {
|
|
|
366
375
|
}
|
|
367
376
|
|
|
368
377
|
function createRequestId() {
|
|
369
|
-
return `req_${
|
|
378
|
+
return `req_${crypto.randomUUID()}`;
|
|
370
379
|
}
|
|
371
380
|
|
|
372
381
|
function getHeader(headers, name) {
|
|
@@ -1,32 +1,42 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { SENSITIVE_HEADERS } = require('../../constants/patterns');
|
|
4
|
-
const { flattenObject } = require('../utils/flatten');
|
|
5
3
|
const { normalizeTarget } = require('../utils/normalize');
|
|
6
4
|
|
|
7
|
-
function collectRequestTargets(req,
|
|
5
|
+
function collectRequestTargets(req, options = {}) {
|
|
8
6
|
const targets = [];
|
|
9
7
|
const headers = req.headers || {};
|
|
8
|
+
const maxDepth = typeof options === 'number' ? options : (options.maxObjectDepth ?? 8);
|
|
9
|
+
const headersToScan =
|
|
10
|
+
typeof options === 'number' || !options.headers
|
|
11
|
+
? ['user-agent', 'referer', 'x-forwarded-for', 'cookie']
|
|
12
|
+
: options.headers.scan;
|
|
13
|
+
const seen = new WeakSet();
|
|
10
14
|
|
|
11
15
|
const add = (label, value) => {
|
|
12
|
-
if (value
|
|
16
|
+
if (value == null) return;
|
|
17
|
+
const type = typeof value;
|
|
18
|
+
if (type === 'string' || type === 'number' || type === 'boolean' || type === 'bigint') {
|
|
19
|
+
targets.push(normalizeTarget({ label, value }));
|
|
20
|
+
}
|
|
13
21
|
};
|
|
14
22
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
23
|
+
const collect = (value, label, depth) => {
|
|
24
|
+
if (value == null || typeof value !== 'object') return add(label, value);
|
|
25
|
+
if (depth > maxDepth || seen.has(value)) return;
|
|
26
|
+
seen.add(value);
|
|
18
27
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
28
|
+
for (const [key, child] of Object.entries(value)) {
|
|
29
|
+
collect(child, Array.isArray(value) ? `${label}[${key}]` : `${label}.${key}`, depth + 1);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
22
32
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
33
|
+
collect(req.query || {}, 'query', 0);
|
|
34
|
+
collect(req.params || {}, 'params', 0);
|
|
35
|
+
collect(req.body, 'body', 0);
|
|
27
36
|
|
|
28
|
-
for (const header of
|
|
29
|
-
|
|
37
|
+
for (const header of headersToScan || []) {
|
|
38
|
+
const key = Object.keys(headers).find((name) => name.toLowerCase() === header);
|
|
39
|
+
if (key) collect(headers[key], `header.${header}`, 0);
|
|
30
40
|
}
|
|
31
41
|
|
|
32
42
|
return targets;
|
package/src/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { Parry_DDoS, createParry } = require('./middleware');
|
|
3
|
+
const { Parry_DDoS, createParry } = require('./express/middleware');
|
|
4
4
|
const { RateLimiter, ThreatLogger } = require('./core');
|
|
5
5
|
const { MemoryStore, RedisStore } = require('./stores');
|
|
6
6
|
const { EventBus, MemoryEventStore } = require('./events');
|
package/src/utils/decode.js
CHANGED
|
@@ -4,7 +4,9 @@ function decodeSqlValue(input) {
|
|
|
4
4
|
let result = input;
|
|
5
5
|
try {
|
|
6
6
|
result = decodeURIComponent(result.replace(/\+/g, ' '));
|
|
7
|
-
} catch (_) {
|
|
7
|
+
} catch (_) {
|
|
8
|
+
// Preserve malformed encodings for the detector's raw checks.
|
|
9
|
+
}
|
|
8
10
|
return decodeHtmlEntities(result, { hex: false });
|
|
9
11
|
}
|
|
10
12
|
|
package/src/utils/normalize.js
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
function safeStringify(value) {
|
|
4
4
|
if (typeof value === 'string') return value;
|
|
5
|
-
if (typeof value === 'number' || typeof value === 'boolean'
|
|
5
|
+
if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {
|
|
6
|
+
return String(value);
|
|
7
|
+
}
|
|
6
8
|
try {
|
|
7
9
|
return JSON.stringify(value);
|
|
8
10
|
} catch {
|
package/types/admin.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export {
|
|
2
|
+
createParryAdminRouter,
|
|
3
|
+
AdminRouterOptions,
|
|
4
|
+
AdminAuthMode,
|
|
5
|
+
AdminAuthConfig,
|
|
6
|
+
AdminAuthStrategyConfig,
|
|
7
|
+
AdminCombinedAuthConfig,
|
|
8
|
+
ParryAdminContext,
|
|
9
|
+
} from './index';
|
|
10
|
+
import { Request, RequestHandler } from 'express';
|
|
11
|
+
import { AdminAuthConfig, ParryAdminContext } from './index';
|
|
12
|
+
|
|
13
|
+
export declare function resolveParryContext(parry: unknown): Record<string, unknown> | null;
|
|
14
|
+
export declare function createAdminAuthMiddleware(
|
|
15
|
+
config: AdminAuthConfig,
|
|
16
|
+
context?: Record<string, unknown>
|
|
17
|
+
): RequestHandler;
|
|
18
|
+
export declare function authenticateAdminRequest(
|
|
19
|
+
req: Request,
|
|
20
|
+
config: AdminAuthConfig,
|
|
21
|
+
context?: Record<string, unknown>
|
|
22
|
+
): Promise<{ ok: boolean; statusCode?: number; admin?: ParryAdminContext }>;
|
|
23
|
+
export declare function requireAdminAuth(
|
|
24
|
+
options?: Record<string, unknown>,
|
|
25
|
+
context?: Record<string, unknown> | null
|
|
26
|
+
): RequestHandler;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { PolicyConfig, ThreatEvent } from './index';
|
|
2
|
+
|
|
3
|
+
export interface BuiltKey {
|
|
4
|
+
type: string;
|
|
5
|
+
value: string;
|
|
6
|
+
key: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export declare function createBruteForceContext(
|
|
10
|
+
context: Record<string, unknown>
|
|
11
|
+
): Record<string, unknown>;
|
|
12
|
+
export declare function attachParryRequestApi(req: object, context: Record<string, unknown>): void;
|
|
13
|
+
export declare function checkBruteForceBlock(
|
|
14
|
+
context: Record<string, unknown>
|
|
15
|
+
): Promise<Record<string, unknown>>;
|
|
16
|
+
export declare function observeAuthenticationResult(context: Record<string, unknown>): void;
|
|
17
|
+
export declare function finalizeAuthenticationResult(
|
|
18
|
+
context: Record<string, unknown>
|
|
19
|
+
): Promise<unknown>;
|
|
20
|
+
export declare function createBruteForceEvent(context: Record<string, unknown>): ThreatEvent;
|
|
21
|
+
export declare function buildBruteForceKeys(
|
|
22
|
+
policy: PolicyConfig,
|
|
23
|
+
requestData: Record<string, unknown>
|
|
24
|
+
): BuiltKey[];
|
|
25
|
+
export declare function buildRouteRateLimitKey(
|
|
26
|
+
policy: PolicyConfig,
|
|
27
|
+
requestData: Record<string, unknown>
|
|
28
|
+
): BuiltKey | null;
|
|
29
|
+
export declare function buildKey(
|
|
30
|
+
policyName: string,
|
|
31
|
+
spec: string | ((requestData: Record<string, unknown>) => unknown),
|
|
32
|
+
requestData: Record<string, unknown>,
|
|
33
|
+
namespace: string
|
|
34
|
+
): BuiltKey | null;
|
|
35
|
+
export declare function resolveValue(
|
|
36
|
+
path: string,
|
|
37
|
+
requestData: Record<string, unknown>
|
|
38
|
+
): string | null;
|
|
39
|
+
export declare function createBlockedResponse(blocked: Record<string, unknown>): {
|
|
40
|
+
statusCode: number;
|
|
41
|
+
headers: { 'Retry-After': number };
|
|
42
|
+
body: { error: string; code: string; retryAfter: number };
|
|
43
|
+
};
|
|
44
|
+
export declare function retryAfterSeconds(blocked: Record<string, unknown>): number;
|
package/types/core.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { RateLimiter, ThreatLogger, MemoryStore, RedisStore } from './index';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { EventBus, MemoryEventStore } from './index';
|
|
2
|
+
export { ThreatEvent, ThreatLogEntry, EventFilters, EventPage } from './index';
|
|
3
|
+
import { ThreatEvent, ThreatLogEntry } from './index';
|
|
4
|
+
|
|
5
|
+
export declare function createThreatEvent(
|
|
6
|
+
input?: Partial<ThreatEvent> | ThreatLogEntry
|
|
7
|
+
): ThreatEvent;
|
|
8
|
+
export declare function createStoreErrorEvent(
|
|
9
|
+
error: unknown,
|
|
10
|
+
context?: Record<string, unknown>
|
|
11
|
+
): ThreatEvent;
|
|
12
|
+
export declare function createHookErrorEvent(
|
|
13
|
+
error: unknown,
|
|
14
|
+
event?: Partial<ThreatEvent>
|
|
15
|
+
): ThreatEvent;
|
|
16
|
+
export declare function sanitizeEvent<T>(event: T): T;
|