@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/types/index.d.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { Request, Response, RequestHandler, Router } from 'express';
|
|
2
2
|
|
|
3
|
-
export interface
|
|
3
|
+
export interface ParryOptions {
|
|
4
4
|
/** Enables SQL injection detection. Default: true */
|
|
5
5
|
sql?: boolean;
|
|
6
6
|
/** Enables XSS detection. Default: true */
|
|
7
7
|
xss?: boolean;
|
|
8
|
-
/** Enables NoSQL injection detection. Default: true */
|
|
9
|
-
nosql?:
|
|
8
|
+
/** Enables NoSQL injection detection and exact-path operator allowlists. Default: true */
|
|
9
|
+
nosql?:
|
|
10
|
+
| boolean
|
|
11
|
+
| {
|
|
12
|
+
enabled?: boolean;
|
|
13
|
+
allowedOperators?: Record<string, string[]>;
|
|
14
|
+
};
|
|
10
15
|
/** HTTP Parameter Pollution protection. Default: disabled */
|
|
11
16
|
hpp?: {
|
|
12
17
|
enabled?: boolean;
|
|
@@ -74,6 +79,10 @@ export interface Parry_DDoSOptions {
|
|
|
74
79
|
trustProxyHeaders?: boolean;
|
|
75
80
|
/** Proxy IPs or CIDRs allowed to provide forwarded client IP headers. */
|
|
76
81
|
trustedProxies?: string[];
|
|
82
|
+
/** Scalar request headers scanned by SQLi/XSS/path detectors. Use [] to disable. */
|
|
83
|
+
headers?: {
|
|
84
|
+
scan?: string[];
|
|
85
|
+
};
|
|
77
86
|
/** Emits extra internal observability events where supported. Default: false */
|
|
78
87
|
debug?: boolean;
|
|
79
88
|
/** Suspicious attempts before temporary ban. Default: 5 */
|
|
@@ -90,6 +99,9 @@ export interface Parry_DDoSOptions {
|
|
|
90
99
|
onStoreError?: (error: Error, event: ThreatEvent) => void;
|
|
91
100
|
}
|
|
92
101
|
|
|
102
|
+
/** @deprecated Use ParryOptions instead. */
|
|
103
|
+
export type Parry_DDoSOptions = ParryOptions;
|
|
104
|
+
|
|
93
105
|
export type ThreatSeverity = 'none' | 'low' | 'medium' | 'high' | 'critical';
|
|
94
106
|
export type ThreatAction = 'allowed' | 'blocked' | 'observed' | 'reset' | 'error' | 'created';
|
|
95
107
|
|
|
@@ -186,14 +198,19 @@ export interface PolicyConfig {
|
|
|
186
198
|
max?: number;
|
|
187
199
|
maxRequests?: number;
|
|
188
200
|
windowMs?: number;
|
|
189
|
-
key?:
|
|
201
|
+
key?:
|
|
202
|
+
| 'ip'
|
|
203
|
+
| 'ip+path'
|
|
204
|
+
| ((requestData: unknown) => string | { type?: string; value: string } | null);
|
|
190
205
|
};
|
|
191
206
|
bruteForce?: {
|
|
192
207
|
enabled?: boolean;
|
|
193
208
|
maxAttempts?: number;
|
|
194
209
|
windowMs?: number;
|
|
195
210
|
blockDurationMs?: number;
|
|
196
|
-
keys?: Array<
|
|
211
|
+
keys?: Array<
|
|
212
|
+
string | ((requestData: unknown) => string | { type?: string; value: string } | null)
|
|
213
|
+
>;
|
|
197
214
|
failureStatusCodes?: number[];
|
|
198
215
|
successStatusCodes?: number[];
|
|
199
216
|
blockedStatusCode?: number;
|
|
@@ -334,6 +351,9 @@ export interface ParryAdminContext {
|
|
|
334
351
|
}
|
|
335
352
|
|
|
336
353
|
export interface AdminRouterOptions {
|
|
354
|
+
/** Explicitly allows an unauthenticated Admin API outside production. Insecure. */
|
|
355
|
+
allowInsecureAdminApi?: boolean;
|
|
356
|
+
/** @deprecated Use allowInsecureAdminApi for explicit local-only anonymous access. */
|
|
337
357
|
requireAuth?: boolean;
|
|
338
358
|
auth?: ((req: Request) => boolean | Promise<boolean>) | AdminAuthConfig;
|
|
339
359
|
}
|
|
@@ -372,7 +392,10 @@ export interface StoreBanResult {
|
|
|
372
392
|
}
|
|
373
393
|
|
|
374
394
|
export interface RateLimitStore {
|
|
375
|
-
incrementRateLimit(
|
|
395
|
+
incrementRateLimit(
|
|
396
|
+
key: string,
|
|
397
|
+
windowMs: number
|
|
398
|
+
): StoreCounterResult | Promise<StoreCounterResult>;
|
|
376
399
|
getRateLimit(key: string): StoreCounterResult | Promise<StoreCounterResult>;
|
|
377
400
|
resetRateLimit(key: string): unknown;
|
|
378
401
|
ban(key: string, ttlMs: number, metadata?: unknown): StoreBanResult | Promise<StoreBanResult>;
|
|
@@ -383,10 +406,18 @@ export interface RateLimitStore {
|
|
|
383
406
|
ttlMs: number,
|
|
384
407
|
metadata?: unknown
|
|
385
408
|
): StoreCounterResult | Promise<StoreCounterResult>;
|
|
386
|
-
incrementCounter(
|
|
409
|
+
incrementCounter(
|
|
410
|
+
key: string,
|
|
411
|
+
ttlMs: number,
|
|
412
|
+
metadata?: unknown
|
|
413
|
+
): StoreCounterResult | Promise<StoreCounterResult>;
|
|
387
414
|
getCounter(key: string): StoreCounterResult | Promise<StoreCounterResult>;
|
|
388
415
|
resetCounter(key: string): unknown;
|
|
389
|
-
blockKey(
|
|
416
|
+
blockKey(
|
|
417
|
+
key: string,
|
|
418
|
+
ttlMs: number,
|
|
419
|
+
metadata?: unknown
|
|
420
|
+
): StoreBlockResult | Promise<StoreBlockResult>;
|
|
390
421
|
isBlocked(key: string): StoreBlockResult | Promise<StoreBlockResult>;
|
|
391
422
|
unblockKey(key: string): unknown;
|
|
392
423
|
listBans?(options?: unknown): BanSnapshot[] | Promise<BanSnapshot[]>;
|
|
@@ -430,7 +461,7 @@ export interface BlockSnapshot {
|
|
|
430
461
|
export declare class RateLimiter {
|
|
431
462
|
constructor(
|
|
432
463
|
config: Pick<
|
|
433
|
-
|
|
464
|
+
ParryOptions,
|
|
434
465
|
'rateLimit' | 'maxRequests' | 'windowMs' | 'suspiciousThreshold' | 'banDurationMs' | 'store'
|
|
435
466
|
>,
|
|
436
467
|
store?: RateLimitStore
|
|
@@ -491,12 +522,18 @@ export declare const SQLInjectionDetector: {
|
|
|
491
522
|
scan(value: string): string | null;
|
|
492
523
|
};
|
|
493
524
|
export declare const XSSDetector: { scan(value: string): string | null };
|
|
494
|
-
export declare const NoSQLDetector: {
|
|
495
|
-
export declare const HPPDetector: {
|
|
525
|
+
export declare const NoSQLDetector: {
|
|
496
526
|
scan(
|
|
497
|
-
|
|
498
|
-
options?: {
|
|
499
|
-
):
|
|
527
|
+
value: unknown,
|
|
528
|
+
options?: { rootPath?: string; allowedOperators?: Record<string, string[]> }
|
|
529
|
+
): string | null;
|
|
530
|
+
inspect(
|
|
531
|
+
value: unknown,
|
|
532
|
+
options?: { rootPath?: string; allowedOperators?: Record<string, string[]> }
|
|
533
|
+
): { pattern: string; path: string } | null;
|
|
534
|
+
};
|
|
535
|
+
export declare const HPPDetector: {
|
|
536
|
+
scan(query: unknown, options?: { allowDuplicateParamsFor?: string[] }): ThreatMatch | null;
|
|
500
537
|
};
|
|
501
538
|
export declare const PrototypePollutionDetector: {
|
|
502
539
|
scan(surfaces: unknown): ThreatMatch | null;
|
|
@@ -526,7 +563,10 @@ export declare class MemoryEventStore {
|
|
|
526
563
|
|
|
527
564
|
export declare class EventBus {
|
|
528
565
|
constructor(options?: { eventStore?: MemoryEventStore; maxEvents?: number });
|
|
529
|
-
emitThreat(
|
|
566
|
+
emitThreat(
|
|
567
|
+
event: Partial<ThreatEvent> | ThreatLogEntry,
|
|
568
|
+
context?: { req?: Request; res?: Response }
|
|
569
|
+
): ThreatEvent;
|
|
530
570
|
onThreat(listener: (event: ThreatEvent, req?: Request, res?: Response) => void): () => void;
|
|
531
571
|
getRecentEvents(options?: EventFilters): EventPage;
|
|
532
572
|
getEventById(id: string): ThreatEvent | null;
|
|
@@ -540,8 +580,19 @@ export declare class Metrics {
|
|
|
540
580
|
snapshot(extra?: { activeBans?: number }): MetricsSnapshot;
|
|
541
581
|
}
|
|
542
582
|
|
|
543
|
-
export declare
|
|
544
|
-
|
|
583
|
+
export declare class ThreatLogger {
|
|
584
|
+
constructor(enabled?: boolean);
|
|
585
|
+
log(entry: ThreatLogEntry): void;
|
|
586
|
+
logHookError(error: unknown, entry?: Partial<ThreatLogEntry>): void;
|
|
587
|
+
logStoreError(error: unknown, entry?: Partial<ThreatLogEntry>): void;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
export declare const Policies: typeof import('./policies');
|
|
591
|
+
export declare const BruteForce: typeof import('./brute-force');
|
|
592
|
+
|
|
593
|
+
/** @deprecated Use createParry instead. */
|
|
594
|
+
export declare function Parry_DDoS(options?: ParryOptions): RequestHandler;
|
|
595
|
+
export declare function createParry(options?: ParryOptions): ParryInstance;
|
|
545
596
|
export declare function createParryAdminRouter(
|
|
546
597
|
parry: ParryInstance | RequestHandler,
|
|
547
598
|
options?: AdminRouterOptions
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { Metrics, MetricsSnapshot, EventPage, PolicyConfig } from './index';
|
|
2
|
+
import { EventPage, MetricsSnapshot, PolicyConfig } from './index';
|
|
3
|
+
|
|
4
|
+
export declare function createSnapshot(context: Record<string, unknown>): {
|
|
5
|
+
metrics: MetricsSnapshot;
|
|
6
|
+
policies: PolicyConfig[];
|
|
7
|
+
store: string;
|
|
8
|
+
events: EventPage;
|
|
9
|
+
};
|
|
10
|
+
export declare function describeStore(store: unknown): string;
|
|
11
|
+
export declare function sanitizePolicies(policies: PolicyConfig[]): PolicyConfig[];
|
|
12
|
+
export declare function countActiveBans(store: unknown): number;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ParryOptions, PolicyConfig } from './index';
|
|
2
|
+
|
|
3
|
+
export declare function findMatchingPolicy(
|
|
4
|
+
policies: PolicyConfig[],
|
|
5
|
+
requestData: { method?: string; path?: string; url?: string }
|
|
6
|
+
): PolicyConfig | null;
|
|
7
|
+
export declare function matchesPolicy(
|
|
8
|
+
policy: PolicyConfig,
|
|
9
|
+
requestData: { method?: string; path?: string; url?: string }
|
|
10
|
+
): boolean;
|
|
11
|
+
export declare function matchesMethod(
|
|
12
|
+
expected: string | string[] | undefined,
|
|
13
|
+
method?: string
|
|
14
|
+
): boolean;
|
|
15
|
+
export declare function matchesPath(
|
|
16
|
+
expected: string | string[] | RegExp | undefined,
|
|
17
|
+
path?: string
|
|
18
|
+
): boolean;
|
|
19
|
+
export declare function buildPolicies(options?: ParryOptions): PolicyConfig[];
|
|
20
|
+
export declare function normalizePolicy(policy: PolicyConfig, options?: ParryOptions): PolicyConfig;
|
|
21
|
+
export declare function getPresetPolicies(name?: 'off' | 'recommended' | 'strict'): PolicyConfig[];
|
package/src/core/logger.js
DELETED
package/src/core/rateLimiter.js
DELETED
package/src/middleware/index.js
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const { Parry_DDoS, createParry } = require('./parry_ddos');
|
|
4
|
-
|
|
5
|
-
// Keep the legacy public export name for compatibility while the project
|
|
6
|
-
// prepares for a future package/API name that better reflects its scope.
|
|
7
|
-
module.exports = { Parry_DDoS, createParry };
|
package/src/stores/README.md
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
# Store Contract
|
|
2
|
-
|
|
3
|
-
Stores keep rate limit, temporary ban, and suspicious activity state for Parry.
|
|
4
|
-
Methods may return values directly or return Promises.
|
|
5
|
-
|
|
6
|
-
```js
|
|
7
|
-
store.incrementRateLimit(key, windowMs);
|
|
8
|
-
// -> { key, count, resetAt, ttlMs }
|
|
9
|
-
|
|
10
|
-
store.getRateLimit(key);
|
|
11
|
-
// -> { key, count, resetAt, ttlMs }
|
|
12
|
-
|
|
13
|
-
store.resetRateLimit(key);
|
|
14
|
-
store.ban(key, ttlMs, metadata);
|
|
15
|
-
store.isBanned(key);
|
|
16
|
-
// -> { key, banned, banExpiresAt, metadata }
|
|
17
|
-
|
|
18
|
-
store.unban(key);
|
|
19
|
-
store.recordSuspicious(key, ttlMs, metadata);
|
|
20
|
-
// -> { key, count, resetAt, ttlMs }
|
|
21
|
-
|
|
22
|
-
store.incrementCounter(key, ttlMs, metadata);
|
|
23
|
-
// -> { key, count, resetAt, ttlMs }
|
|
24
|
-
|
|
25
|
-
store.getCounter(key);
|
|
26
|
-
// -> { key, count, resetAt, ttlMs }
|
|
27
|
-
|
|
28
|
-
store.resetCounter(key);
|
|
29
|
-
store.blockKey(key, ttlMs, metadata);
|
|
30
|
-
store.isBlocked(key);
|
|
31
|
-
// -> { key, blocked, blockExpiresAt, metadata }
|
|
32
|
-
|
|
33
|
-
store.unblockKey(key);
|
|
34
|
-
|
|
35
|
-
store.listBans?.({ limit, offset });
|
|
36
|
-
// -> [{ key, createdAt, banExpiresAt, ttlMs, metadata }]
|
|
37
|
-
|
|
38
|
-
store.listBlocks?.({ limit, offset });
|
|
39
|
-
// -> [{ key, createdAt, blockExpiresAt, ttlMs, metadata }]
|
|
40
|
-
|
|
41
|
-
store.getStoreInfo?.();
|
|
42
|
-
// -> { type, supportsAdminListing, ...metadata }
|
|
43
|
-
|
|
44
|
-
store.close?.();
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
`key` is produced by the rate limiter, route policy, or brute force key builder.
|
|
48
|
-
Stores are responsible for TTL handling, cleanup, and any backend-specific
|
|
49
|
-
namespacing. Custom stores should avoid persisting sensitive request data.
|
|
50
|
-
Administrative listing methods are optional, but stores that implement them must
|
|
51
|
-
return only sanitized metadata suitable for a read-only operations dashboard.
|