aiwaf-js 0.0.7 → 0.0.9
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/README.md +191 -0
- package/index.js +15 -2
- package/lib/adonisMiddleware.js +82 -0
- package/lib/anomalyDetector.js +18 -5
- package/lib/fastifyPlugin.js +49 -0
- package/lib/featureUtils.js +11 -1
- package/lib/hapiPlugin.js +92 -0
- package/lib/headerValidation.js +9 -0
- package/lib/koaMiddleware.js +68 -0
- package/lib/nestMiddleware.js +12 -0
- package/lib/nextMiddleware.js +78 -0
- package/lib/rateLimiter.js +3 -0
- package/lib/wafMiddleware.js +160 -12
- package/lib/wasmAdapter.js +187 -0
- package/package.json +34 -2
- package/train.js +15 -2
- package/.dockerignore +0 -6
- package/.github/workflows/node.js.yml +0 -31
- package/.github/workflows/npm-publish.yml +0 -27
- package/aiwaf.sqlite +0 -0
- package/examples/sandbox/README.md +0 -53
- package/examples/sandbox/aiwaf-proxy/Dockerfile +0 -21
- package/examples/sandbox/aiwaf-proxy/package.json +0 -15
- package/examples/sandbox/aiwaf-proxy/server.js +0 -44
- package/examples/sandbox/attack-suite.js +0 -293
- package/examples/sandbox/compare-results.js +0 -86
- package/examples/sandbox/docker-compose.yml +0 -27
- package/examples/sandbox/run-and-compare.js +0 -91
- package/geolock/ipinfo_lite.mmdb +0 -0
- package/knexfile.js +0 -9
- package/migrations/001_create_blocked_ips.js +0 -11
- package/migrations/002_create_dynamic_keywords.js +0 -11
- package/test/anomaly-detector.test.js +0 -36
- package/test/cli.test.js +0 -125
- package/test/csv-fallback.test.js +0 -165
- package/test/dynamic-keyword-integration.test.js +0 -24
- package/test/dynamic-keyword-store.test.js +0 -78
- package/test/exemptions-db.test.js +0 -38
- package/test/geo-mmdb.test.js +0 -77
- package/test/header-validation.test.js +0 -66
- package/test/honeypot-detector.test.js +0 -42
- package/test/isolation-forest.test.js +0 -38
- package/test/middleware-behavior.test.js +0 -75
- package/test/model-store-db.test.js +0 -22
- package/test/model-store.test.js +0 -31
- package/test/redis-client.test.js +0 -35
- package/test/settingsCompat.test.js +0 -95
- package/test/train.test.js +0 -137
- package/test/uuid-detector.test.js +0 -20
- package/test/waf.test.js +0 -327
- package/test-anomaly.js +0 -77
- package/test-complete-waf.js +0 -147
- package/test-simple.js +0 -79
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# aiwaf-js
|
|
2
2
|
|
|
3
3
|
AIWAF-JS is a Node.js/Express Web Application Firewall that combines deterministic protections with anomaly detection and continuous learning. It ships as middleware, a CLI for ops workflows, and an offline trainer for IsolationForest models.
|
|
4
|
+
Supported frameworks: Express (native), Fastify, Hapi, Koa, NestJS (Express/Fastify wrappers), Next.js (API route wrapper), and AdonisJS.
|
|
4
5
|
|
|
5
6
|
## What It Does
|
|
6
7
|
|
|
@@ -62,6 +63,15 @@ AIWAF-JS is a Node.js/Express Web Application Firewall that combines determinist
|
|
|
62
63
|
npm install aiwaf-js
|
|
63
64
|
```
|
|
64
65
|
|
|
66
|
+
### Optional WASM Acceleration
|
|
67
|
+
|
|
68
|
+
AIWAF can use the `aiwaf-wasm` optional dependency for faster IsolationForest scoring and deterministic feature validation.
|
|
69
|
+
If the WASM module fails to load, it automatically falls back to the JS implementation.
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
npm install aiwaf-wasm
|
|
73
|
+
```
|
|
74
|
+
|
|
65
75
|
## Quick Start
|
|
66
76
|
|
|
67
77
|
```js
|
|
@@ -88,6 +98,184 @@ app.get('/', (req, res) => res.send('Protected'));
|
|
|
88
98
|
app.listen(3000);
|
|
89
99
|
```
|
|
90
100
|
|
|
101
|
+
## Fastify Usage
|
|
102
|
+
|
|
103
|
+
```js
|
|
104
|
+
const fastify = require('fastify')({ logger: true });
|
|
105
|
+
const aiwaf = require('aiwaf-js');
|
|
106
|
+
|
|
107
|
+
fastify.register(aiwaf.fastify, {
|
|
108
|
+
staticKeywords: ['.php', '.env', '.git'],
|
|
109
|
+
dynamicTopN: 10,
|
|
110
|
+
WINDOW_SEC: 10,
|
|
111
|
+
MAX_REQ: 20,
|
|
112
|
+
FLOOD_REQ: 40,
|
|
113
|
+
HONEYPOT_FIELD: 'hp_field'
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
fastify.get('/', async () => 'Protected');
|
|
117
|
+
fastify.listen({ port: 3000 });
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Hapi Usage
|
|
121
|
+
|
|
122
|
+
```js
|
|
123
|
+
const Hapi = require('@hapi/hapi');
|
|
124
|
+
const aiwaf = require('aiwaf-js');
|
|
125
|
+
|
|
126
|
+
const server = Hapi.server({ port: 3000 });
|
|
127
|
+
await server.register({
|
|
128
|
+
plugin: aiwaf.hapi,
|
|
129
|
+
options: {
|
|
130
|
+
staticKeywords: ['.php', '.env', '.git'],
|
|
131
|
+
dynamicTopN: 10,
|
|
132
|
+
WINDOW_SEC: 10,
|
|
133
|
+
MAX_REQ: 20,
|
|
134
|
+
FLOOD_REQ: 40,
|
|
135
|
+
HONEYPOT_FIELD: 'hp_field'
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
server.route({ method: 'GET', path: '/', handler: () => 'Protected' });
|
|
140
|
+
await server.start();
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Koa Usage
|
|
144
|
+
|
|
145
|
+
```js
|
|
146
|
+
const Koa = require('koa');
|
|
147
|
+
const bodyParser = require('koa-bodyparser');
|
|
148
|
+
const aiwaf = require('aiwaf-js');
|
|
149
|
+
|
|
150
|
+
const app = new Koa();
|
|
151
|
+
app.use(bodyParser());
|
|
152
|
+
|
|
153
|
+
app.use(aiwaf.koa({
|
|
154
|
+
staticKeywords: ['.php', '.env', '.git'],
|
|
155
|
+
dynamicTopN: 10,
|
|
156
|
+
WINDOW_SEC: 10,
|
|
157
|
+
MAX_REQ: 20,
|
|
158
|
+
FLOOD_REQ: 40,
|
|
159
|
+
HONEYPOT_FIELD: 'hp_field'
|
|
160
|
+
}));
|
|
161
|
+
|
|
162
|
+
app.use(ctx => {
|
|
163
|
+
ctx.body = 'Protected';
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
app.listen(3000);
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## NestJS (Express) Usage
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
import { Module, MiddlewareConsumer, NestModule } from '@nestjs/common';
|
|
173
|
+
import aiwaf from 'aiwaf-js';
|
|
174
|
+
|
|
175
|
+
@Module({})
|
|
176
|
+
export class AppModule implements NestModule {
|
|
177
|
+
configure(consumer: MiddlewareConsumer) {
|
|
178
|
+
consumer
|
|
179
|
+
.apply(aiwaf.nest({
|
|
180
|
+
staticKeywords: ['.php', '.env', '.git'],
|
|
181
|
+
dynamicTopN: 10,
|
|
182
|
+
WINDOW_SEC: 10,
|
|
183
|
+
MAX_REQ: 20,
|
|
184
|
+
FLOOD_REQ: 40,
|
|
185
|
+
HONEYPOT_FIELD: 'hp_field'
|
|
186
|
+
}))
|
|
187
|
+
.forRoutes('*');
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
If you need to guarantee ordering before other middleware/proxies, you can also attach the Express middleware directly in `main.ts`:
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
import { NestFactory } from '@nestjs/core';
|
|
196
|
+
import aiwaf from 'aiwaf-js';
|
|
197
|
+
import { AppModule } from './app.module';
|
|
198
|
+
|
|
199
|
+
async function bootstrap() {
|
|
200
|
+
const app = await NestFactory.create(AppModule);
|
|
201
|
+
app.use(aiwaf({
|
|
202
|
+
staticKeywords: ['.php', '.env', '.git'],
|
|
203
|
+
dynamicTopN: 10,
|
|
204
|
+
WINDOW_SEC: 10,
|
|
205
|
+
MAX_REQ: 20,
|
|
206
|
+
FLOOD_REQ: 40,
|
|
207
|
+
HONEYPOT_FIELD: 'hp_field'
|
|
208
|
+
}));
|
|
209
|
+
await app.listen(3000);
|
|
210
|
+
}
|
|
211
|
+
bootstrap();
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## NestJS (Fastify) Usage
|
|
215
|
+
|
|
216
|
+
Use the Fastify plugin when running Nest with `FastifyAdapter`:
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
import { NestFactory } from '@nestjs/core';
|
|
220
|
+
import { FastifyAdapter } from '@nestjs/platform-fastify';
|
|
221
|
+
import aiwaf from 'aiwaf-js';
|
|
222
|
+
import { AppModule } from './app.module';
|
|
223
|
+
|
|
224
|
+
async function bootstrap() {
|
|
225
|
+
const app = await NestFactory.create(AppModule, new FastifyAdapter());
|
|
226
|
+
await app.register(aiwaf.fastify, {
|
|
227
|
+
staticKeywords: ['.php', '.env', '.git'],
|
|
228
|
+
dynamicTopN: 10,
|
|
229
|
+
WINDOW_SEC: 10,
|
|
230
|
+
MAX_REQ: 20,
|
|
231
|
+
FLOOD_REQ: 40,
|
|
232
|
+
HONEYPOT_FIELD: 'hp_field'
|
|
233
|
+
});
|
|
234
|
+
await app.listen(3000, '0.0.0.0');
|
|
235
|
+
}
|
|
236
|
+
bootstrap();
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## Next.js (API Routes) Usage
|
|
240
|
+
|
|
241
|
+
Use the `aiwaf.next` helper to wrap a Next.js API route handler.
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
import aiwaf from 'aiwaf-js';
|
|
245
|
+
|
|
246
|
+
function handler(req, res) {
|
|
247
|
+
res.status(200).json({ ok: true });
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export default aiwaf.next(handler, {
|
|
251
|
+
staticKeywords: ['.php', '.env', '.git'],
|
|
252
|
+
dynamicTopN: 10,
|
|
253
|
+
WINDOW_SEC: 10,
|
|
254
|
+
MAX_REQ: 20,
|
|
255
|
+
FLOOD_REQ: 40,
|
|
256
|
+
HONEYPOT_FIELD: 'hp_field'
|
|
257
|
+
});
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## AdonisJS Usage
|
|
261
|
+
|
|
262
|
+
Register the middleware in your Adonis middleware stack:
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
import aiwaf from 'aiwaf-js';
|
|
266
|
+
|
|
267
|
+
export const middleware = [
|
|
268
|
+
() => aiwaf.adonis({
|
|
269
|
+
staticKeywords: ['.php', '.env', '.git'],
|
|
270
|
+
dynamicTopN: 10,
|
|
271
|
+
WINDOW_SEC: 10,
|
|
272
|
+
MAX_REQ: 20,
|
|
273
|
+
FLOOD_REQ: 40,
|
|
274
|
+
HONEYPOT_FIELD: 'hp_field'
|
|
275
|
+
})
|
|
276
|
+
];
|
|
277
|
+
```
|
|
278
|
+
|
|
91
279
|
## Configuration
|
|
92
280
|
|
|
93
281
|
### Core Controls
|
|
@@ -105,6 +293,8 @@ app.listen(3000);
|
|
|
105
293
|
| `cache` | fallback memory cache | Custom cache backend used by limiter/features |
|
|
106
294
|
| `nTrees` | `100` | IsolationForest trees when model is initialized in-process |
|
|
107
295
|
| `sampleSize` | `256` | IsolationForest sample size |
|
|
296
|
+
| `AIWAF_WASM_VALIDATION` | `true` | Enable WASM validation when available (headers, URL, content, recent) |
|
|
297
|
+
| `AIWAF_WASM_VALIDATE_RECENT` | `false` | Run WASM recent-behavior validation on recent request logs |
|
|
108
298
|
|
|
109
299
|
### Header Validation
|
|
110
300
|
|
|
@@ -318,6 +508,7 @@ node examples/sandbox/run-and-compare.js http://localhost:3001 http://localhost:
|
|
|
318
508
|
```
|
|
319
509
|
|
|
320
510
|
The comparison output includes per‑attack block rates and total blocked requests.
|
|
511
|
+
Fastify proxy is also available on `http://localhost:3002`.
|
|
321
512
|
|
|
322
513
|
## License
|
|
323
514
|
|
package/index.js
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
1
|
// aiwaf‑js/index.js
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
const createExpressMiddleware = require('./lib/wafMiddleware');
|
|
3
|
+
const createFastifyPlugin = require('./lib/fastifyPlugin');
|
|
4
|
+
const createHapiPlugin = require('./lib/hapiPlugin');
|
|
5
|
+
const createKoaMiddleware = require('./lib/koaMiddleware');
|
|
6
|
+
const createNestMiddleware = require('./lib/nestMiddleware');
|
|
7
|
+
const createNextHandler = require('./lib/nextMiddleware');
|
|
8
|
+
const createAdonisMiddleware = require('./lib/adonisMiddleware');
|
|
9
|
+
|
|
10
|
+
module.exports = createExpressMiddleware;
|
|
11
|
+
module.exports.fastify = createFastifyPlugin;
|
|
12
|
+
module.exports.hapi = createHapiPlugin;
|
|
13
|
+
module.exports.koa = createKoaMiddleware;
|
|
14
|
+
module.exports.nest = createNestMiddleware;
|
|
15
|
+
module.exports.next = createNextHandler;
|
|
16
|
+
module.exports.adonis = createAdonisMiddleware;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const createExpressMiddleware = require('./wafMiddleware');
|
|
2
|
+
|
|
3
|
+
function createExpressLikeResponse(ctx) {
|
|
4
|
+
const rawRes = ctx.response?.response;
|
|
5
|
+
const res = {
|
|
6
|
+
locals: {},
|
|
7
|
+
on: (...args) => rawRes?.on?.(...args),
|
|
8
|
+
get statusCode() {
|
|
9
|
+
return rawRes?.statusCode ?? ctx.response?.statusCode ?? 200;
|
|
10
|
+
},
|
|
11
|
+
set statusCode(code) {
|
|
12
|
+
if (rawRes) rawRes.statusCode = code;
|
|
13
|
+
if (ctx.response) ctx.response.statusCode = code;
|
|
14
|
+
},
|
|
15
|
+
status(code) {
|
|
16
|
+
if (ctx.response?.status) {
|
|
17
|
+
ctx.response.status(code);
|
|
18
|
+
} else {
|
|
19
|
+
res.statusCode = code;
|
|
20
|
+
}
|
|
21
|
+
res._handled = true;
|
|
22
|
+
return res;
|
|
23
|
+
},
|
|
24
|
+
json(payload) {
|
|
25
|
+
if (ctx.response?.json) {
|
|
26
|
+
ctx.response.json(payload);
|
|
27
|
+
} else if (ctx.response?.send) {
|
|
28
|
+
ctx.response.send(payload);
|
|
29
|
+
}
|
|
30
|
+
res._handled = true;
|
|
31
|
+
return res;
|
|
32
|
+
},
|
|
33
|
+
send(payload) {
|
|
34
|
+
if (ctx.response?.send) {
|
|
35
|
+
ctx.response.send(payload);
|
|
36
|
+
} else if (rawRes?.end) {
|
|
37
|
+
rawRes.end(payload);
|
|
38
|
+
}
|
|
39
|
+
res._handled = true;
|
|
40
|
+
return res;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return res;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = function createAdonisMiddleware(opts = {}) {
|
|
48
|
+
const middleware = createExpressMiddleware(opts);
|
|
49
|
+
|
|
50
|
+
return async (ctx, next) => {
|
|
51
|
+
const req = ctx.request?.request || ctx.request || {};
|
|
52
|
+
const res = createExpressLikeResponse(ctx);
|
|
53
|
+
|
|
54
|
+
// Only set path/url/ip if not already available
|
|
55
|
+
if (!req.path) req.path = ctx.request?.url?.() || ctx.request?.url || req.url;
|
|
56
|
+
if (!req.url) req.url = req.path;
|
|
57
|
+
if (!req.ip) req.ip = ctx.request?.ip?.() || ctx.request?.ip;
|
|
58
|
+
|
|
59
|
+
// Don't override headers - the raw request already has them from the HTTP server
|
|
60
|
+
|
|
61
|
+
await new Promise(resolve => {
|
|
62
|
+
let resolved = false;
|
|
63
|
+
const finish = () => {
|
|
64
|
+
if (resolved) return;
|
|
65
|
+
resolved = true;
|
|
66
|
+
resolve();
|
|
67
|
+
};
|
|
68
|
+
const maybePromise = middleware(req, res, finish);
|
|
69
|
+
if (res._handled) {
|
|
70
|
+
finish();
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
Promise.resolve(maybePromise).then(finish).catch(finish);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (res._handled || ctx.response?.response?.writableEnded || ctx.response?.response?.headersSent) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
await next();
|
|
81
|
+
};
|
|
82
|
+
};
|
package/lib/anomalyDetector.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const { IsolationForest } = require('./isolationForest');
|
|
2
|
+
const { createIsolationForest, getWasmStatus } = require('./wasmAdapter');
|
|
2
3
|
const modelStore = require('./modelStore');
|
|
3
4
|
const requestLogStore = require('./requestLogStore');
|
|
4
5
|
const { STATIC_KW } = require('./featureUtils');
|
|
@@ -12,6 +13,7 @@ let loadStarted = false;
|
|
|
12
13
|
let minAiLogs = 0;
|
|
13
14
|
let aiLogsSufficient = true;
|
|
14
15
|
let aiLogCount = null;
|
|
16
|
+
let wasmStatus = { loaded: false, error: null };
|
|
15
17
|
|
|
16
18
|
async function loadModel(opts = {}) {
|
|
17
19
|
if (loadStarted) return;
|
|
@@ -150,20 +152,25 @@ module.exports = {
|
|
|
150
152
|
async init(opts = {}) {
|
|
151
153
|
minAiLogs = Number.isFinite(Number(opts.AIWAF_MIN_AI_LOGS))
|
|
152
154
|
? Number(opts.AIWAF_MIN_AI_LOGS)
|
|
153
|
-
:
|
|
155
|
+
: 10000; // Default: require 10k training samples
|
|
154
156
|
await checkAiLogSufficiency();
|
|
155
157
|
await loadModel(opts);
|
|
156
158
|
if (!model) {
|
|
157
|
-
model =
|
|
159
|
+
model = await createIsolationForest({
|
|
160
|
+
nTrees: opts.nTrees || 100,
|
|
161
|
+
sampleSize: opts.sampleSize || 256,
|
|
162
|
+
threshold: opts.threshold || 0.5
|
|
163
|
+
});
|
|
164
|
+
wasmStatus = getWasmStatus();
|
|
158
165
|
}
|
|
159
166
|
|
|
160
167
|
if (model && !aiLogsSufficient) {
|
|
161
168
|
model = null;
|
|
162
169
|
trained = false;
|
|
163
170
|
if (aiLogCount !== null) {
|
|
164
|
-
console.log(`AIWAF AI model disabled due to insufficient logs (${aiLogCount}/${minAiLogs}).`);
|
|
171
|
+
console.log(`AIWAF AI model disabled due to insufficient logs (${aiLogCount}/${minAiLogs}). Require at least ${minAiLogs} logs before using AI detection.`);
|
|
165
172
|
} else {
|
|
166
|
-
console.log(`AIWAF AI model disabled due to insufficient logs (unknown/${minAiLogs}).`);
|
|
173
|
+
console.log(`AIWAF AI model disabled due to insufficient logs (unknown/${minAiLogs}). Require at least ${minAiLogs} logs before using AI detection.`);
|
|
167
174
|
}
|
|
168
175
|
}
|
|
169
176
|
},
|
|
@@ -177,6 +184,11 @@ module.exports = {
|
|
|
177
184
|
return !!model && trained;
|
|
178
185
|
},
|
|
179
186
|
|
|
187
|
+
isModelSufficientlyTrained() {
|
|
188
|
+
// Model must exist AND have enough training data
|
|
189
|
+
return !!model && trained && aiLogsSufficient;
|
|
190
|
+
},
|
|
191
|
+
|
|
180
192
|
// Expects a feature vector: [pathLen, kwHits, statusIdx, responseTime, burst, total404]
|
|
181
193
|
isAnomalous(features, threshold = 0.5) {
|
|
182
194
|
if (!trained || !model) {
|
|
@@ -222,7 +234,8 @@ module.exports = {
|
|
|
222
234
|
threshold: 0.5,
|
|
223
235
|
minAiLogs,
|
|
224
236
|
aiLogsSufficient,
|
|
225
|
-
aiLogCount
|
|
237
|
+
aiLogCount,
|
|
238
|
+
wasm: wasmStatus
|
|
226
239
|
};
|
|
227
240
|
}
|
|
228
241
|
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const createExpressMiddleware = require('./wafMiddleware');
|
|
2
|
+
|
|
3
|
+
function createExpressLikeResponse(reply) {
|
|
4
|
+
const raw = reply.raw;
|
|
5
|
+
const res = {
|
|
6
|
+
locals: {},
|
|
7
|
+
on: (...args) => raw.on(...args),
|
|
8
|
+
get statusCode() {
|
|
9
|
+
return raw.statusCode;
|
|
10
|
+
},
|
|
11
|
+
set statusCode(code) {
|
|
12
|
+
raw.statusCode = code;
|
|
13
|
+
},
|
|
14
|
+
status(code) {
|
|
15
|
+
reply.code(code);
|
|
16
|
+
return res;
|
|
17
|
+
},
|
|
18
|
+
json(payload) {
|
|
19
|
+
reply.type('application/json').send(payload);
|
|
20
|
+
return res;
|
|
21
|
+
},
|
|
22
|
+
send(payload) {
|
|
23
|
+
reply.send(payload);
|
|
24
|
+
return res;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
return res;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function fastifyPlugin(fastify, opts = {}, done) {
|
|
31
|
+
const middleware = createExpressMiddleware(opts);
|
|
32
|
+
|
|
33
|
+
fastify.addHook('onRequest', async (request, reply) => {
|
|
34
|
+
await new Promise(resolve => {
|
|
35
|
+
const res = createExpressLikeResponse(reply);
|
|
36
|
+
middleware(request.raw, res, resolve);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (reply.sent) {
|
|
40
|
+
return reply;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
done();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
fastifyPlugin[Symbol.for('skip-override')] = true;
|
|
48
|
+
|
|
49
|
+
module.exports = fastifyPlugin;
|
package/lib/featureUtils.js
CHANGED
|
@@ -33,6 +33,15 @@ function init(opts = {}) {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
function cleanup() {
|
|
37
|
+
if (cleanupInterval) {
|
|
38
|
+
clearInterval(cleanupInterval);
|
|
39
|
+
cleanupInterval = null;
|
|
40
|
+
}
|
|
41
|
+
ipRequestHistory.clear();
|
|
42
|
+
ip404Counts.clear();
|
|
43
|
+
}
|
|
44
|
+
|
|
36
45
|
function recordRequest(ip, statusCode) {
|
|
37
46
|
const now = Date.now();
|
|
38
47
|
|
|
@@ -83,7 +92,7 @@ function getResponseTime(req) {
|
|
|
83
92
|
|
|
84
93
|
async function extractFeatures(req, res = null) {
|
|
85
94
|
const uri = req.path || req.url;
|
|
86
|
-
const ip = req.ip || req.
|
|
95
|
+
const ip = req.ip || req.socket?.remoteAddress || req.connection?.remoteAddress || 'unknown';
|
|
87
96
|
|
|
88
97
|
// Get status code from response if available, otherwise default to 200
|
|
89
98
|
let statusCode = 200;
|
|
@@ -168,6 +177,7 @@ module.exports = {
|
|
|
168
177
|
get404Count,
|
|
169
178
|
markRequestStart,
|
|
170
179
|
getResponseTime,
|
|
180
|
+
cleanup,
|
|
171
181
|
STATIC_KW,
|
|
172
182
|
STATUS_IDX
|
|
173
183
|
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
const createExpressMiddleware = require('./wafMiddleware');
|
|
2
|
+
|
|
3
|
+
function createExpressLikeResponse(h, rawRes) {
|
|
4
|
+
const res = {
|
|
5
|
+
locals: {},
|
|
6
|
+
on: (...args) => rawRes.on(...args),
|
|
7
|
+
get statusCode() {
|
|
8
|
+
return rawRes.statusCode;
|
|
9
|
+
},
|
|
10
|
+
set statusCode(code) {
|
|
11
|
+
rawRes.statusCode = code;
|
|
12
|
+
},
|
|
13
|
+
status(code) {
|
|
14
|
+
res._statusCode = code;
|
|
15
|
+
res._handled = true;
|
|
16
|
+
return res;
|
|
17
|
+
},
|
|
18
|
+
json(payload) {
|
|
19
|
+
res._payload = payload;
|
|
20
|
+
res._contentType = 'application/json';
|
|
21
|
+
res._handled = true;
|
|
22
|
+
return res;
|
|
23
|
+
},
|
|
24
|
+
send(payload) {
|
|
25
|
+
res._payload = payload;
|
|
26
|
+
res._handled = true;
|
|
27
|
+
return res;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
res.toResponse = () => {
|
|
32
|
+
const response = h.response(res._payload);
|
|
33
|
+
if (res._contentType) {
|
|
34
|
+
response.type(res._contentType);
|
|
35
|
+
}
|
|
36
|
+
if (res._statusCode) {
|
|
37
|
+
response.code(res._statusCode);
|
|
38
|
+
}
|
|
39
|
+
return response;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return res;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = {
|
|
46
|
+
name: 'aiwaf',
|
|
47
|
+
version: '1.0.0',
|
|
48
|
+
register: async (server, opts = {}) => {
|
|
49
|
+
const middleware = createExpressMiddleware(opts);
|
|
50
|
+
|
|
51
|
+
server.ext('onRequest', async (request, h) => {
|
|
52
|
+
const res = createExpressLikeResponse(h, request.raw.res);
|
|
53
|
+
const req = request.raw.req;
|
|
54
|
+
|
|
55
|
+
// Set path and URL from Hapi's request object
|
|
56
|
+
req.path = request.path || req.path;
|
|
57
|
+
req.url = request.url?.pathname || req.url;
|
|
58
|
+
|
|
59
|
+
// For headers: prefer the framework's parsed headers, but merge them
|
|
60
|
+
// Don't try to replace req.headers entirely as it's read-only on raw requests
|
|
61
|
+
if (request.headers) {
|
|
62
|
+
// Copy headers to the req.headers object (which is read-only but properties can be added)
|
|
63
|
+
try {
|
|
64
|
+
Object.keys(request.headers).forEach(key => {
|
|
65
|
+
if (!req.headers[key]) {
|
|
66
|
+
req.headers[key] = request.headers[key];
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
} catch (err) {
|
|
70
|
+
// If we can't modify headers, continue anyway
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
req.ip = request.info?.remoteAddress || req.ip;
|
|
75
|
+
|
|
76
|
+
return new Promise(resolve => {
|
|
77
|
+
let resolved = false;
|
|
78
|
+
const finish = () => {
|
|
79
|
+
if (resolved) return;
|
|
80
|
+
resolved = true;
|
|
81
|
+
if (res._payload !== undefined || res._statusCode) {
|
|
82
|
+
resolve(res.toResponse().takeover());
|
|
83
|
+
} else {
|
|
84
|
+
resolve(h.continue);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
const maybePromise = middleware(req, res, finish);
|
|
88
|
+
Promise.resolve(maybePromise).then(finish).catch(finish);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
};
|
package/lib/headerValidation.js
CHANGED
|
@@ -203,6 +203,15 @@ module.exports = {
|
|
|
203
203
|
);
|
|
204
204
|
headers['server-protocol'] = req.httpVersion ? `HTTP/${req.httpVersion}` : headers['server-protocol'];
|
|
205
205
|
|
|
206
|
+
if (process.env.AIWAF_DEBUG_HEADERS) {
|
|
207
|
+
console.error(`[HEADER-VALIDATION] headers keys: ${Object.keys(headers).join(', ')}`);
|
|
208
|
+
console.error(`[HEADER-VALIDATION] user-agent: ${headers['user-agent']}`);
|
|
209
|
+
console.error(`[HEADER-VALIDATION] accept: ${headers['accept']}`);
|
|
210
|
+
console.error(`[HEADER-VALIDATION] accept-language: ${headers['accept-language']}`);
|
|
211
|
+
console.error(`[HEADER-VALIDATION] accept-encoding: ${headers['accept-encoding']}`);
|
|
212
|
+
console.error(`[HEADER-VALIDATION] connection: ${headers['connection']}`);
|
|
213
|
+
}
|
|
214
|
+
|
|
206
215
|
if (isStaticRequest(req.path || req.url || '')) return null;
|
|
207
216
|
|
|
208
217
|
const capReason = enforceHeaderCaps(headers);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const createExpressMiddleware = require('./wafMiddleware');
|
|
2
|
+
|
|
3
|
+
function createExpressLikeResponse(ctx) {
|
|
4
|
+
const res = {
|
|
5
|
+
locals: {},
|
|
6
|
+
on: (...args) => ctx.res.on(...args),
|
|
7
|
+
get statusCode() {
|
|
8
|
+
return ctx.status;
|
|
9
|
+
},
|
|
10
|
+
set statusCode(code) {
|
|
11
|
+
ctx.status = code;
|
|
12
|
+
},
|
|
13
|
+
status(code) {
|
|
14
|
+
ctx.status = code;
|
|
15
|
+
res._handled = true;
|
|
16
|
+
return res;
|
|
17
|
+
},
|
|
18
|
+
json(payload) {
|
|
19
|
+
ctx.type = 'application/json';
|
|
20
|
+
ctx.body = payload;
|
|
21
|
+
res._handled = true;
|
|
22
|
+
return res;
|
|
23
|
+
},
|
|
24
|
+
send(payload) {
|
|
25
|
+
ctx.body = payload;
|
|
26
|
+
res._handled = true;
|
|
27
|
+
return res;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
return res;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = function createKoaMiddleware(opts = {}) {
|
|
34
|
+
const middleware = createExpressMiddleware(opts);
|
|
35
|
+
|
|
36
|
+
return async (ctx, next) => {
|
|
37
|
+
const res = createExpressLikeResponse(ctx);
|
|
38
|
+
const req = ctx.req; // Raw Node.js http.IncomingMessage - already has headers
|
|
39
|
+
|
|
40
|
+
// Only set these if not already available
|
|
41
|
+
if (!req.path) req.path = ctx.path;
|
|
42
|
+
if (!req.url) req.url = ctx.url;
|
|
43
|
+
if (!req.ip) req.ip = ctx.ip;
|
|
44
|
+
|
|
45
|
+
// Don't override headers - use what's already on the raw request
|
|
46
|
+
// ctx.headers is Koa's parsed version, but req.headers from Node.js has the originals
|
|
47
|
+
|
|
48
|
+
await new Promise(resolve => {
|
|
49
|
+
let resolved = false;
|
|
50
|
+
const finish = () => {
|
|
51
|
+
if (resolved) return;
|
|
52
|
+
resolved = true;
|
|
53
|
+
resolve();
|
|
54
|
+
};
|
|
55
|
+
const maybePromise = middleware(req, res, finish);
|
|
56
|
+
if (res._handled) {
|
|
57
|
+
finish();
|
|
58
|
+
}
|
|
59
|
+
Promise.resolve(maybePromise).then(finish).catch(finish);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (ctx.body !== undefined) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
await next();
|
|
67
|
+
};
|
|
68
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const createExpressMiddleware = require('./wafMiddleware');
|
|
2
|
+
|
|
3
|
+
module.exports = function createNestMiddleware(opts = {}) {
|
|
4
|
+
const middleware = createExpressMiddleware(opts);
|
|
5
|
+
|
|
6
|
+
return class AIWAFNestMiddleware {
|
|
7
|
+
use(req, res, next) {
|
|
8
|
+
// Express middleware - req/res already have headers set by Express itself
|
|
9
|
+
return middleware(req, res, next);
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
};
|