@roboteby/parry 1.1.0-rc.1

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.
Files changed (72) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/LICENSE +21 -0
  3. package/README.md +284 -0
  4. package/config/defaults.js +65 -0
  5. package/constants/patterns.js +77 -0
  6. package/package.json +89 -0
  7. package/src/admin/admin-router.js +106 -0
  8. package/src/admin/auth/admin-auth.js +176 -0
  9. package/src/admin/auth/index.js +13 -0
  10. package/src/admin/auth/strategies/alb-auth.js +49 -0
  11. package/src/admin/auth/strategies/cloudflare-access.js +34 -0
  12. package/src/admin/auth/strategies/combined.js +50 -0
  13. package/src/admin/auth/strategies/ip-allowlist.js +13 -0
  14. package/src/admin/auth/strategies/none.js +20 -0
  15. package/src/admin/auth/strategies/token.js +25 -0
  16. package/src/admin/auth/strategies/trusted-proxy.js +52 -0
  17. package/src/admin/auth/utils/constant-time.js +18 -0
  18. package/src/admin/auth/utils/external-identity.js +156 -0
  19. package/src/admin/auth/utils/header-utils.js +39 -0
  20. package/src/admin/auth/utils/result.js +39 -0
  21. package/src/admin/ban-normalizer.js +98 -0
  22. package/src/admin/index.js +12 -0
  23. package/src/admin/response.js +41 -0
  24. package/src/brute-force/brute-force-guard.js +268 -0
  25. package/src/brute-force/index.js +32 -0
  26. package/src/brute-force/key-builder.js +164 -0
  27. package/src/brute-force/result.js +35 -0
  28. package/src/core/engine.js +264 -0
  29. package/src/core/index.js +7 -0
  30. package/src/core/logger.js +3 -0
  31. package/src/core/rate-limit-result.js +13 -0
  32. package/src/core/rateLimiter.js +3 -0
  33. package/src/core/scoring.js +18 -0
  34. package/src/core/threat-event.js +69 -0
  35. package/src/detectors/hpp.js +30 -0
  36. package/src/detectors/index.js +19 -0
  37. package/src/detectors/nosql.js +53 -0
  38. package/src/detectors/path-traversal.js +72 -0
  39. package/src/detectors/prototype-pollution.js +69 -0
  40. package/src/detectors/request-shape.js +76 -0
  41. package/src/detectors/sql.js +18 -0
  42. package/src/detectors/xss.js +18 -0
  43. package/src/events/event-bus.js +51 -0
  44. package/src/events/index.js +19 -0
  45. package/src/events/memory-event-store.js +64 -0
  46. package/src/events/sanitize-event.js +54 -0
  47. package/src/events/threat-event.js +174 -0
  48. package/src/express/ip-resolver.js +109 -0
  49. package/src/express/middleware.js +379 -0
  50. package/src/express/request-targets.js +35 -0
  51. package/src/express/response.js +14 -0
  52. package/src/index.js +41 -0
  53. package/src/logger/console-reporter.js +75 -0
  54. package/src/middleware/index.js +7 -0
  55. package/src/middleware/parry_ddos.js +3 -0
  56. package/src/observability/index.js +6 -0
  57. package/src/observability/metrics.js +61 -0
  58. package/src/observability/snapshot.js +48 -0
  59. package/src/policies/index.js +15 -0
  60. package/src/policies/matcher.js +48 -0
  61. package/src/policies/normalize-policy.js +94 -0
  62. package/src/policies/presets.js +34 -0
  63. package/src/rate-limit/keys.js +7 -0
  64. package/src/rate-limit/limiter.js +124 -0
  65. package/src/stores/README.md +51 -0
  66. package/src/stores/index.js +6 -0
  67. package/src/stores/memory-store.js +278 -0
  68. package/src/stores/redis-store.js +349 -0
  69. package/src/utils/decode.js +56 -0
  70. package/src/utils/flatten.js +27 -0
  71. package/src/utils/normalize.js +21 -0
  72. package/types/index.d.ts +555 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,19 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ This project follows [Semantic Versioning](https://semver.org/) for public API and
6
+ runtime behavior. The format is based on [Keep a Changelog](https://keepachangelog.com/),
7
+ without adding a changelog generation dependency.
8
+
9
+ ## [Unreleased]
10
+
11
+ ### Added
12
+
13
+ - Release workflow preparation.
14
+ - npm package hardening.
15
+ - Package tarball validation scripts.
16
+
17
+ ### Security
18
+
19
+ - Documented npm publishing and supply-chain hardening with Trusted Publishing/OIDC.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 RobotEby
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,284 @@
1
+ # parry-express-security-middleware
2
+
3
+ Application-layer security middleware for Express.js.
4
+
5
+ ## Overview
6
+
7
+ Parry is a CommonJS security middleware for Express applications. It helps block common application-layer abuse before requests reach route handlers, while keeping the public API small and compatible with standard Express middleware usage.
8
+
9
+ Parry detects and blocks patterns associated with SQL injection, XSS, NoSQL injection, HTTP parameter pollution, prototype pollution, path traversal, rate abuse, and brute-force authentication attempts. It also emits structured Threat Events and metrics that can be inspected through an optional read-only Admin API.
10
+
11
+ Parry is not a complete volumetric DDoS protection product. Network floods, L3/L4 abuse, TLS exhaustion, CDN filtering, and edge rate controls should be handled by CloudFront, AWS WAF, Shield, a CDN, an ALB/load balancer, or equivalent infrastructure controls.
12
+
13
+ ## Why Parry
14
+
15
+ Parry centralizes application-layer security policy in one Express middleware. That gives backend teams a consistent place to tune detector behavior, route-based limits, brute-force protection, event logging, and distributed rate limiting.
16
+
17
+ It is designed for development and production-like deployments:
18
+
19
+ - use `MemoryStore` for local development and single-process services;
20
+ - use `RedisStore` for multiple instances, containers, ECS, Kubernetes, PM2 cluster, or load-balanced services;
21
+ - expose the Admin API only behind authentication, private networking, VPN, or a trusted reverse proxy.
22
+
23
+ ## Features
24
+
25
+ - SQL injection detection
26
+ - XSS detection
27
+ - NoSQL injection detection
28
+ - HTTP parameter pollution checks
29
+ - Prototype pollution checks
30
+ - Path traversal checks
31
+ - Request shape guard
32
+ - Global and route-based rate limiting
33
+ - BruteForceGuard for authentication routes
34
+ - `MemoryStore` and optional `RedisStore`
35
+ - Structured Threat Events
36
+ - Metrics and observability helpers
37
+ - Optional read-only Admin API
38
+ - Route-based policies and presets
39
+ - Docker demo API
40
+ - AWS reference infrastructure
41
+
42
+ ## Installation
43
+
44
+ ```bash
45
+ npm install @roboteby/parry
46
+ ```
47
+
48
+ Parry expects Express to be installed by your application.
49
+
50
+ ## Quick Start
51
+
52
+ ```js
53
+ const express = require('express');
54
+ const { createParry } = require('@roboteby/parry');
55
+
56
+ const app = express();
57
+
58
+ const parry = createParry({
59
+ preset: 'recommended',
60
+ });
61
+
62
+ app.use(express.json());
63
+ app.use(parry.middleware());
64
+
65
+ app.get('/health', (_req, res) => {
66
+ res.json({ ok: true });
67
+ });
68
+
69
+ app.listen(3000);
70
+ ```
71
+
72
+ Parse JSON and URL-encoded bodies before Parry when you want Parry to inspect request bodies. Keep Parry before routes that should be protected.
73
+
74
+ The legacy `Parry_DDoS(options)` export remains available for existing CommonJS integrations:
75
+
76
+ ```js
77
+ const { Parry_DDoS } = require('@roboteby/parry');
78
+
79
+ app.use(express.json());
80
+ app.use(Parry_DDoS({ preset: 'recommended' }));
81
+ ```
82
+
83
+ ## Presets
84
+
85
+ Parry supports conservative presets for common application-layer protection:
86
+
87
+ - `off`: no route-policy preset is added.
88
+ - `recommended`: enables practical defaults for common auth routes and low-noise application-layer checks.
89
+ - `strict`: uses more restrictive brute-force and route rate-limit defaults for sensitive environments.
90
+
91
+ Every option can still be configured explicitly. Prefer starting with `recommended`, reviewing logs and events, then tightening route policies where needed.
92
+
93
+ ## Stores
94
+
95
+ `MemoryStore` is the default store. It is suitable for tests, demos, local development, and single-process deployments.
96
+
97
+ For distributed deployments, use `RedisStore` with a Redis client created by your application:
98
+
99
+ ```js
100
+ const { createClient } = require('redis');
101
+ const { createParry, RedisStore } = require('@roboteby/parry');
102
+
103
+ const redis = createClient({ url: process.env.REDIS_URL });
104
+ await redis.connect();
105
+
106
+ const parry = createParry({
107
+ store: new RedisStore({ client: redis, prefix: 'parry' }),
108
+ rateLimit: {
109
+ enabled: true,
110
+ max: 100,
111
+ windowMs: 60_000,
112
+ headers: true,
113
+ },
114
+ });
115
+ ```
116
+
117
+ If your service runs behind multiple instances, containers, or load balancers, use a shared store. `MemoryStore` protects only the current Node.js process.
118
+
119
+ ## Brute-Force Protection
120
+
121
+ Route policies can protect sensitive authentication endpoints without making the global rate limit too aggressive:
122
+
123
+ ```js
124
+ const parry = createParry({
125
+ policies: [
126
+ {
127
+ name: 'auth-login',
128
+ match: { method: 'POST', path: '/login' },
129
+ rateLimit: {
130
+ enabled: true,
131
+ max: 20,
132
+ windowMs: 60_000,
133
+ key: 'ip',
134
+ },
135
+ bruteForce: {
136
+ enabled: true,
137
+ maxAttempts: 5,
138
+ windowMs: 15 * 60_000,
139
+ blockDurationMs: 10 * 60_000,
140
+ keys: ['ip', 'body.email', 'ip+body.email'],
141
+ failureStatusCodes: [400, 401, 403],
142
+ resetOnSuccess: true,
143
+ },
144
+ },
145
+ ],
146
+ });
147
+ ```
148
+
149
+ Routes can also report authentication outcomes manually:
150
+
151
+ ```js
152
+ app.post('/login', async (req, res) => {
153
+ const user = await authService.validate(req.body.email, req.body.password);
154
+
155
+ if (!user) {
156
+ req.parry?.recordAuthFailure('invalid_credentials');
157
+ return res.status(200).json({ success: false });
158
+ }
159
+
160
+ req.parry?.recordAuthSuccess();
161
+ return res.json({ success: true });
162
+ });
163
+ ```
164
+
165
+ ## Threat Events and Admin API
166
+
167
+ Parry emits structured Threat Events for blocked requests, rate limits, brute-force blocks, store errors, and hook errors. Events are sanitized before reaching logs, hooks, metrics, or the Admin API.
168
+
169
+ The optional Admin API is read-only and is never mounted automatically:
170
+
171
+ ```js
172
+ const { createParry, createParryAdminRouter } = require('@roboteby/parry');
173
+
174
+ const parry = createParry({
175
+ admin: {
176
+ enabled: true,
177
+ auth: {
178
+ mode: 'token',
179
+ token: process.env.PARRY_ADMIN_TOKEN,
180
+ },
181
+ },
182
+ });
183
+
184
+ app.use(parry.middleware());
185
+ app.use('/_parry', createParryAdminRouter(parry));
186
+ ```
187
+
188
+ Main endpoints:
189
+
190
+ - `GET /_parry/health`
191
+ - `GET /_parry/metrics`
192
+ - `GET /_parry/events`
193
+ - `GET /_parry/events/:id`
194
+ - `GET /_parry/bans`
195
+ - `GET /_parry/policies`
196
+
197
+ Protect the Admin API with token auth for local demos, or with VPN, private networking, Cloudflare Access, AWS ALB/Cognito auth, trusted proxy auth, or IP allowlists in production.
198
+
199
+ ## Parry Security Console
200
+
201
+ The separate `parry-security-console` repository provides a read-only dashboard for the Parry Admin API. It displays health, metrics, Threat Events, bans/blocks, and route policies. It does not contain middleware logic, does not execute payloads, and is not a scanner.
202
+
203
+ For local development, the console can use Vite proxying with `VITE_PARRY_API_URL=/api/parry`.
204
+
205
+ ## Docker Demo
206
+
207
+ The repository includes a demo Express API with Redis:
208
+
209
+ ```bash
210
+ docker compose up --build
211
+ ```
212
+
213
+ Useful local checks:
214
+
215
+ ```bash
216
+ curl http://localhost:3000/health
217
+
218
+ curl http://localhost:3000/_parry/health \
219
+ -H "x-parry-admin-token: change-me"
220
+
221
+ curl -X POST http://localhost:3000/echo \
222
+ -H "Content-Type: application/json" \
223
+ -d '{"message":"hello"}'
224
+ ```
225
+
226
+ The `change-me` token is for local demos only.
227
+
228
+ ## Security Model
229
+
230
+ Parry operates inside the Express application. It helps identify and block suspicious application-layer requests, but it does not replace edge and infrastructure controls.
231
+
232
+ Production deployments should account for:
233
+
234
+ - CloudFront, AWS WAF, Shield, CDN, ALB, or equivalent edge protection for volumetric DDoS and network-layer abuse.
235
+ - RedisStore or another shared store for distributed rate limits and brute-force counters.
236
+ - Admin API authentication and network restrictions.
237
+ - Trusted proxy configuration before accepting `x-forwarded-for`, Cloudflare Access, ALB/Cognito, or reverse-proxy identity headers.
238
+ - Generic authentication responses that do not reveal whether a username or email exists.
239
+
240
+ Browser-visible Admin API tokens are appropriate only for local development and demos. Production consoles should be protected by VPN, private networking, Cloudflare Access, ALB/Cognito, reverse proxy auth, or a backend/admin gateway.
241
+
242
+ ## Documentation
243
+
244
+ Additional documentation is available in the repository:
245
+
246
+ - [Admin API](https://github.com/RobotEby/parry-express-security-middleware/blob/main/docs/admin-api.md)
247
+ - [Admin API authentication](https://github.com/RobotEby/parry-express-security-middleware/blob/main/docs/admin-api-auth.md)
248
+ - [AWS Admin API authentication](https://github.com/RobotEby/parry-express-security-middleware/blob/main/docs/aws-admin-auth.md)
249
+ - [Docker demo](https://github.com/RobotEby/parry-express-security-middleware/blob/main/docs/docker-demo.md)
250
+ - [AWS infrastructure notes](https://github.com/RobotEby/parry-express-security-middleware/blob/main/docs/aws-infra.md)
251
+ - [CI/CD](https://github.com/RobotEby/parry-express-security-middleware/blob/main/docs/ci-cd.md)
252
+ - [Release process](https://github.com/RobotEby/parry-express-security-middleware/blob/main/docs/release.md)
253
+ - [Payload regression testing](https://github.com/RobotEby/parry-express-security-middleware/blob/main/docs/testing-payloads.md)
254
+ - [Architecture](https://github.com/RobotEby/parry-express-security-middleware/blob/main/docs/architecture.md)
255
+
256
+ The npm package keeps `docs/`, infrastructure, Docker demo files, and tests out of the published runtime package.
257
+
258
+ ## Development
259
+
260
+ ```bash
261
+ npm ci
262
+ npm test
263
+ npm run test:fixtures
264
+ npm run test:payload-regression
265
+ npm run package:check
266
+ npm pack --dry-run
267
+ ```
268
+
269
+ `npm test` uses local mocks and fake stores. It does not require Redis, AWS, Cloudflare, or external services.
270
+
271
+ ## Roadmap
272
+
273
+ Planned areas for future work:
274
+
275
+ - Redis-backed event persistence
276
+ - OpenTelemetry and Prometheus export
277
+ - Express 4 compatibility matrix
278
+ - Optional hashing/redaction for store keys
279
+ - Additional detector tuning with benign counterexamples
280
+ - Admin API hardening and deployment guides
281
+
282
+ ## License
283
+
284
+ MIT
@@ -0,0 +1,65 @@
1
+ 'use strict';
2
+
3
+ const DEFAULTS = {
4
+ sql: true,
5
+ xss: true,
6
+ nosql: true,
7
+
8
+ hpp: {
9
+ enabled: false,
10
+ allowDuplicateParamsFor: [],
11
+ },
12
+ prototypePollution: {
13
+ enabled: true,
14
+ },
15
+ pathTraversal: {
16
+ enabled: true,
17
+ },
18
+ requestShape: {
19
+ enabled: true,
20
+ maxDepth: 8,
21
+ maxKeys: 500,
22
+ maxArrayLength: 100,
23
+ maxStringLength: 10_000,
24
+ },
25
+
26
+ rateLimit: true,
27
+ maxRequests: 100,
28
+ windowMs: 60_000,
29
+ store: null,
30
+ storeFailureMode: 'fail-open',
31
+ policies: [],
32
+ preset: 'off',
33
+ bruteForce: {
34
+ enabled: false,
35
+ },
36
+ events: {
37
+ maxEvents: 500,
38
+ },
39
+ admin: {
40
+ enabled: false,
41
+ path: '/_parry',
42
+ allowMutations: false,
43
+ allowInsecureAdminApi: false,
44
+ auth: null,
45
+ },
46
+ requestId: {
47
+ enabled: true,
48
+ header: 'x-request-id',
49
+ responseHeader: false,
50
+ },
51
+ trustProxyHeaders: false,
52
+ trustedProxies: [],
53
+ debug: false,
54
+
55
+ suspiciousThreshold: 5,
56
+ banDurationMs: 300_000,
57
+
58
+ logThreats: true,
59
+
60
+ onThreat: null,
61
+
62
+ maxObjectDepth: 5,
63
+ };
64
+
65
+ module.exports = { DEFAULTS };
@@ -0,0 +1,77 @@
1
+ 'use strict';
2
+
3
+ const SQL_PATTERNS = [
4
+ /\b(union)\b.{0,30}\b(select)\b/i,
5
+ /\b(or|and)\b\s+[\w'"]{1,20}\s*=\s*[\w'"]{1,20}/i,
6
+ /(['"`]\s*(--|#|\/\*)|(--|#|\/\*)\s*$)/,
7
+ /;\s*(drop|alter|truncate|delete|insert|update|create|exec|execute)\b/i,
8
+ /\b(sleep|benchmark|pg_sleep|waitfor\s+delay)\s*\(/i,
9
+ /\b(select|insert|update|delete|drop|alter)\b.{0,80}\b(from|into|table|where)\b/i,
10
+ /'?\s*\bor\b\s+'?1'?\s*=\s*'?1/i,
11
+ /'?\s*\band\b\s+'?1'?\s*=\s*'?1/i,
12
+ /information_schema|sys\.tables|sysobjects|pg_catalog/i,
13
+ /\bchar\s*\(\s*\d+/i,
14
+ /0x[0-9a-f]{2,}/i,
15
+ /\b(load_file|into\s+outfile|into\s+dumpfile)\b/i,
16
+ /\b(exec\s*\(|xp_cmdshell|sp_executesql)\b/i,
17
+ ];
18
+
19
+ const XSS_PATTERNS = [
20
+ /<\s*script[\s\S]*?>[\s\S]*?<\s*\/\s*script\s*>/i,
21
+ /<\s*script\b[^>]*>/i,
22
+ /\bon\w+\s*=\s*["']?[^"'>]*/i,
23
+ /javascript\s*:/i,
24
+ /vbscript\s*:/i,
25
+ /data\s*:\s*[^,]*script/i,
26
+ /<\s*(img|iframe|object|embed|svg|video|audio|source|track|input)\b[^>]*\s(src|data|href)\s*=\s*["']?\s*javascript/i,
27
+ /<\s*svg\b[^>]*>[\s\S]*?(script|onload|onerror)/i,
28
+ /expression\s*\(/i,
29
+ /url\s*\(\s*["']?\s*javascript/i,
30
+ /\{\{[\s\S]{0,200}\}\}/,
31
+ /\$\{[\s\S]{0,200}\}/,
32
+ /<\s*(base|link|meta|style)\b[^>]*(http-equiv|href|content)\s*=\s*["']?[^"'>]*script/i,
33
+ /\0|%00/,
34
+ /autofocus.{0,30}onfocus/i,
35
+ ];
36
+
37
+ const NOSQL_DANGEROUS_OPERATORS = new Set(['$where', '$expr', '$function', '$accumulator']);
38
+
39
+ const NOSQL_SUSPICIOUS_OPERATORS = new Set([
40
+ '$gt',
41
+ '$gte',
42
+ '$lt',
43
+ '$lte',
44
+ '$ne',
45
+ '$in',
46
+ '$nin',
47
+ '$or',
48
+ '$and',
49
+ '$not',
50
+ '$nor',
51
+ '$exists',
52
+ '$type',
53
+ '$regex',
54
+ '$options',
55
+ '$elemMatch',
56
+ '$size',
57
+ '$slice',
58
+ '$meta',
59
+ ]);
60
+
61
+ const NOSQL_STRING_PATTERNS = [
62
+ /"\$\w+"?\s*:/,
63
+ /\$where\s*[:=]\s*["'`]?(function|this\.|sleep|db\.)/i,
64
+ /\b(mapReduce|runCommand|eval)\s*\(/i,
65
+ /db\.(getCollection|find|update|insert|remove|drop)\s*\(/i,
66
+ ];
67
+
68
+ const SENSITIVE_HEADERS = ['user-agent', 'referer', 'x-forwarded-for', 'cookie'];
69
+
70
+ module.exports = {
71
+ SQL_PATTERNS,
72
+ XSS_PATTERNS,
73
+ NOSQL_DANGEROUS_OPERATORS,
74
+ NOSQL_SUSPICIOUS_OPERATORS,
75
+ NOSQL_STRING_PATTERNS,
76
+ SENSITIVE_HEADERS,
77
+ };
package/package.json ADDED
@@ -0,0 +1,89 @@
1
+ {
2
+ "name": "@roboteby/parry",
3
+ "version": "1.1.0-rc.1",
4
+ "description": "Application-layer security middleware for Express.js with injection detection, abuse mitigation, brute-force protection and distributed rate limiting.",
5
+ "main": "./src/index.js",
6
+ "types": "./types/index.d.ts",
7
+ "scripts": {
8
+ "test": "node tests/index.js",
9
+ "test:unit": "node -e \"(async()=>{ await require('./tests/unit/detectors.test'); await require('./tests/unit/rateLimiter.test'); await require('./tests/unit/memoryStore.test'); await require('./tests/unit/redisStore.test'); await require('./tests/unit/policyMatcher.test'); await require('./tests/unit/keyBuilder.test'); await require('./tests/unit/bruteForceGuard.test'); await require('./tests/unit/engine.test'); await require('./tests/unit/applicationGuards.test'); await require('./tests/unit/observability.test'); await require('./tests/unit/adminAuth.test'); })().catch((err)=>{ console.error(err); process.exit(1); })\"",
10
+ "test:integ": "node tests/integration/middleware.test.js",
11
+ "test:fixtures": "node scripts/payloads/validate-fixtures.js",
12
+ "test:payload-regression": "node tests/regression/index.js",
13
+ "test:payload-report": "node scripts/payloads/generate-payload-report.js",
14
+ "package:check": "node scripts/package/check-package.js",
15
+ "package:check-tag": "node scripts/package/check-version-tag.js",
16
+ "package:dry-run": "npm pack --dry-run",
17
+ "example": "node examples/express-basic.js",
18
+ "test:http": "node scripts/run-tests.js",
19
+ "start:test": "node scripts/test-server.js",
20
+ "docker:build:demo": "docker build -f docker/demo-api/Dockerfile -t parry-demo-api .",
21
+ "format": "prettier --write \"src/**/*.js\"",
22
+ "format:check": "prettier --check \"src/**/*.js\""
23
+ },
24
+ "exports": {
25
+ ".": {
26
+ "require": "./src/index.js",
27
+ "types": "./types/index.d.ts"
28
+ },
29
+ "./core": "./src/core/index.js",
30
+ "./detectors": "./src/detectors/index.js",
31
+ "./stores": "./src/stores/index.js",
32
+ "./policies": "./src/policies/index.js",
33
+ "./brute-force": "./src/brute-force/index.js",
34
+ "./events": "./src/events/index.js",
35
+ "./observability": "./src/observability/index.js",
36
+ "./admin": "./src/admin/index.js",
37
+ "./package.json": "./package.json"
38
+ },
39
+ "files": [
40
+ "src",
41
+ "config",
42
+ "constants",
43
+ "types",
44
+ "README.md",
45
+ "LICENSE",
46
+ "CHANGELOG.md",
47
+ "package.json"
48
+ ],
49
+ "keywords": [
50
+ "express",
51
+ "middleware",
52
+ "security",
53
+ "application-security",
54
+ "appsec",
55
+ "rate-limit",
56
+ "brute-force",
57
+ "xss",
58
+ "sql-injection",
59
+ "nosql-injection",
60
+ "redis",
61
+ "nodejs"
62
+ ],
63
+ "publishConfig": {
64
+ "access": "public"
65
+ },
66
+ "license": "MIT",
67
+ "repository": {
68
+ "type": "git",
69
+ "url": "git+ssh://git@github.com/RobotEby/parry-express-security-middleware.git"
70
+ },
71
+ "bugs": {
72
+ "url": "https://github.com/RobotEby/parry-express-security-middleware/issues"
73
+ },
74
+ "homepage": "https://github.com/RobotEby/parry-express-security-middleware#readme",
75
+ "peerDependencies": {
76
+ "express": "^5.2.1"
77
+ },
78
+ "devDependencies": {
79
+ "eslint-config-prettier": "^10.1.8",
80
+ "express": "^5.2.1",
81
+ "prettier": "^3.8.1"
82
+ },
83
+ "engines": {
84
+ "node": ">=18"
85
+ },
86
+ "dependencies": {
87
+ "ipaddr.js": "^2.4.0"
88
+ }
89
+ }
@@ -0,0 +1,106 @@
1
+ 'use strict';
2
+
3
+ const express = require('express');
4
+ const pkg = require('../../package.json');
5
+ const { requireAdminAuth } = require('./auth');
6
+ const { ok, notFound } = require('./response');
7
+ const { listAdminBanEntries } = require('./ban-normalizer');
8
+ const { describeStore, sanitizePolicies } = require('../observability');
9
+
10
+ function createParryAdminRouter(parry, options = {}) {
11
+ const context = resolveParryContext(parry);
12
+ if (!context) {
13
+ throw new Error('createParryAdminRouter requires a Parry instance or middleware.');
14
+ }
15
+
16
+ const router = express.Router();
17
+ router.use(requireAdminAuth(options, context));
18
+
19
+ router.get('/health', (_req, res) =>
20
+ ok(res, {
21
+ ok: true,
22
+ name: 'parry',
23
+ version: pkg.version,
24
+ uptimeMs: context.metrics.snapshot().uptimeMs,
25
+ store: describeStore(context.store),
26
+ })
27
+ );
28
+
29
+ router.get(
30
+ '/metrics',
31
+ asyncRoute(async (_req, res) => {
32
+ const activeBans = (await listAdminBanEntries(context.store)).length;
33
+ return ok(res, context.metrics.snapshot({ activeBans }));
34
+ })
35
+ );
36
+
37
+ router.get('/events', (req, res) => {
38
+ const result = context.eventBus.getRecentEvents({
39
+ limit: req.query.limit,
40
+ offset: req.query.offset,
41
+ type: req.query.type,
42
+ severity: req.query.severity,
43
+ action: req.query.action,
44
+ detector: req.query.detector,
45
+ ip: req.query.ip,
46
+ path: req.query.path,
47
+ policyName: req.query.policyName,
48
+ });
49
+ return ok(res, result);
50
+ });
51
+
52
+ router.get('/events/:id', (req, res) => {
53
+ const event = context.eventBus.getEventById(req.params.id);
54
+ if (!event) return notFound(res);
55
+ return ok(res, event);
56
+ });
57
+
58
+ router.get(
59
+ '/bans',
60
+ asyncRoute(async (req, res) => {
61
+ const data = await listAdminBanEntries(context.store, req.query);
62
+ return ok(res, paginateList(data, req.query));
63
+ })
64
+ );
65
+
66
+ router.get('/policies', (req, res) =>
67
+ ok(res, paginateList(sanitizePolicies(context.policies || []), req.query))
68
+ );
69
+
70
+ return router;
71
+ }
72
+
73
+ function resolveParryContext(parry) {
74
+ if (!parry) return null;
75
+ if (typeof parry.getContext === 'function') return parry.getContext();
76
+ if (parry.__parryContext) return parry.__parryContext;
77
+ return null;
78
+ }
79
+
80
+ function paginateList(data, query = {}) {
81
+ const limit = clampNumber(query.limit, 50, 1, 500);
82
+ const offset = clampNumber(query.offset, 0, 0, data.length);
83
+
84
+ return {
85
+ data: data.slice(offset, offset + limit),
86
+ pagination: {
87
+ limit,
88
+ offset,
89
+ total: data.length,
90
+ },
91
+ };
92
+ }
93
+
94
+ function clampNumber(value, fallback, min, max) {
95
+ const parsed = Number(value);
96
+ if (!Number.isFinite(parsed)) return fallback;
97
+ return Math.min(max, Math.max(min, Math.floor(parsed)));
98
+ }
99
+
100
+ function asyncRoute(handler) {
101
+ return function routeHandler(req, res, next) {
102
+ Promise.resolve(handler(req, res, next)).catch(next);
103
+ };
104
+ }
105
+
106
+ module.exports = { createParryAdminRouter, resolveParryContext };