aiwaf-js 0.0.2 → 0.0.4
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 +87 -36
- package/lib/anomalyDetector.js +2 -2
- package/lib/featureUtils.js +46 -7
- package/lib/rateLimiter.js +54 -10
- package/lib/redisClient.js +30 -0
- package/lib/wafMiddleware.js +2 -1
- package/package.json +1 -1
- package/test/waf.test.js +61 -21
package/README.md
CHANGED
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
# aiwaf‑js
|
|
2
2
|
|
|
3
3
|
> **Adaptive Web Application Firewall** middleware for Node.js & Express
|
|
4
|
-
> Self‑learning, plug‑and‑play WAF with rate‑limiting, static & dynamic keyword blocking, honeypot traps, UUID‑tamper protection and IsolationForest anomaly detection—fully configurable and trainable on your own access logs.
|
|
4
|
+
> Self‑learning, plug‑and‑play WAF with rate‑limiting, static & dynamic keyword blocking, honeypot traps, UUID‑tamper protection, and IsolationForest anomaly detection—fully configurable and trainable on your own access logs. Now Redis‑powered and ready for distributed, multiprocess use.
|
|
5
5
|
|
|
6
6
|
[](https://www.npmjs.com/package/aiwaf-js)
|
|
7
|
-
[](https://github.com/your-user/aiwaf-js/actions)
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
|
|
10
|
+
---
|
|
11
|
+
|
|
10
12
|
## Features
|
|
11
13
|
|
|
12
|
-
- Rate Limiting
|
|
13
|
-
- Static Keyword Blocking
|
|
14
|
-
- Dynamic Keyword Learning
|
|
15
|
-
- Honeypot Field Detection
|
|
16
|
-
- UUID‑Tamper Protection
|
|
17
|
-
- Anomaly Detection (Isolation Forest)
|
|
18
|
-
-
|
|
14
|
+
- ✅ Rate Limiting (Redis-based or fallback to memory)
|
|
15
|
+
- ✅ Static Keyword Blocking
|
|
16
|
+
- ✅ Dynamic Keyword Learning (auto-adaptive)
|
|
17
|
+
- ✅ Honeypot Field Detection
|
|
18
|
+
- ✅ UUID‑Tamper Protection
|
|
19
|
+
- ✅ Anomaly Detection (Isolation Forest)
|
|
20
|
+
- ✅ Redis Support for multiprocess environments
|
|
21
|
+
- ✅ Offline Training from access logs
|
|
22
|
+
- ✅ **Custom Cache Logic Support**
|
|
23
|
+
|
|
24
|
+
---
|
|
19
25
|
|
|
20
26
|
## Installation
|
|
21
27
|
|
|
@@ -23,6 +29,20 @@
|
|
|
23
29
|
npm install aiwaf-js --save
|
|
24
30
|
```
|
|
25
31
|
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Train the Model (Optional but recommended)
|
|
35
|
+
|
|
36
|
+
You can train the anomaly detector and keyword learner using real access logs.
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
NODE_LOG_PATH=/path/to/access.log npm run train
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
If `NODE_LOG_PATH` is not provided, it defaults to `/var/log/nginx/access.log`.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
26
46
|
## Quick Start
|
|
27
47
|
|
|
28
48
|
```js
|
|
@@ -36,53 +56,84 @@ app.get('/', (req, res) => res.send('Protected'))
|
|
|
36
56
|
app.listen(3000)
|
|
37
57
|
```
|
|
38
58
|
|
|
39
|
-
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Redis Support (Recommended for Production)
|
|
62
|
+
|
|
63
|
+
AIWAF‑JS supports Redis for distributed rate limiting and keyword caching.
|
|
40
64
|
|
|
41
65
|
```bash
|
|
42
|
-
|
|
66
|
+
# On Unix/Linux/macOS
|
|
67
|
+
export REDIS_URL=redis://localhost:6379
|
|
68
|
+
|
|
69
|
+
# On Windows PowerShell
|
|
70
|
+
$env:REDIS_URL = "redis://localhost:6379"
|
|
43
71
|
```
|
|
44
72
|
|
|
73
|
+
If Redis is unavailable, it gracefully falls back to in-memory mode.
|
|
74
|
+
|
|
75
|
+
---
|
|
45
76
|
|
|
46
|
-
##
|
|
77
|
+
## Custom Cache Logic (Advanced)
|
|
47
78
|
|
|
48
|
-
|
|
79
|
+
You can inject your own cache logic (in-memory, Redis, hybrid, or file-based) by passing a `cache` object implementing the following interface:
|
|
49
80
|
|
|
50
81
|
```js
|
|
51
|
-
const
|
|
52
|
-
|
|
82
|
+
const myCustomCache = {
|
|
83
|
+
get: async (key) => { /* return cached value */ },
|
|
84
|
+
set: async (key, value, options) => { /* store with optional TTL */ },
|
|
85
|
+
del: async (key) => { /* delete entry */ }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
app.use(aiwaf({
|
|
89
|
+
cache: myCustomCache,
|
|
90
|
+
staticKeywords: ['.php'],
|
|
91
|
+
dynamicTopN: 5,
|
|
92
|
+
MAX_REQ: 10,
|
|
93
|
+
WINDOW_SEC: 15,
|
|
94
|
+
FLOOD_REQ: 20,
|
|
95
|
+
}))
|
|
96
|
+
```
|
|
53
97
|
|
|
54
|
-
|
|
55
|
-
app.use(express.json());
|
|
98
|
+
This overrides Redis/in-memory usage with your custom strategy for all cache operations.
|
|
56
99
|
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Configuration
|
|
103
|
+
|
|
104
|
+
```js
|
|
57
105
|
app.use(aiwaf({
|
|
58
|
-
staticKeywords: ['.php', '.env', '.git'],
|
|
106
|
+
staticKeywords: ['.php', '.env', '.git'],
|
|
59
107
|
dynamicTopN: 10,
|
|
60
108
|
WINDOW_SEC: 10,
|
|
61
109
|
MAX_REQ: 20,
|
|
62
110
|
FLOOD_REQ: 10,
|
|
63
111
|
HONEYPOT_FIELD: 'hp_field',
|
|
112
|
+
cache: myCustomCache, // optional custom cache injection
|
|
64
113
|
}));
|
|
65
|
-
|
|
66
|
-
app.get('/', (req, res) => res.send('Protected by AIWAF-JS'));
|
|
67
|
-
app.listen(3000, () => console.log('Server running on http://localhost:3000'));
|
|
68
114
|
```
|
|
69
115
|
|
|
70
|
-
|
|
116
|
+
| Option | Env Var | Default | Description |
|
|
117
|
+
|--------------------|---------------------|-----------------------------|----------------------------------------------------------|
|
|
118
|
+
| `staticKeywords` | — | [".php",".xmlrpc","wp-"] | Substrings to block immediately. |
|
|
119
|
+
| `dynamicTopN` | `DYNAMIC_TOP_N` | 10 | Number of dynamic keywords to match. |
|
|
120
|
+
| `windowSec` | `WINDOW_SEC` | 10 | Time window in seconds for rate limiting. |
|
|
121
|
+
| `maxReq` | `MAX_REQ` | 20 | Max allowed requests per window. |
|
|
122
|
+
| `floodReq` | `FLOOD_REQ` | 10 | Hard limit triggering IP block. |
|
|
123
|
+
| `honeypotField` | `HONEYPOT_FIELD` | "hp_field" | Hidden bot trap field. |
|
|
124
|
+
| `anomalyThreshold` | `ANOMALY_THRESHOLD` | 0.5 | Threshold for IsolationForest-based anomaly detection. |
|
|
125
|
+
| `logPath` | `NODE_LOG_PATH` | "/var/log/nginx/access.log" | Path to access log file. |
|
|
126
|
+
| `logGlob` | `NODE_LOG_GLOB` | "${logPath}.*" | Glob pattern to include rotated/gzipped logs. |
|
|
127
|
+
| `cache` | — | undefined | Custom cache implementation (overrides Redis/memory) |
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Optimization Note
|
|
71
132
|
|
|
72
|
-
|
|
133
|
+
**Tip:** In high-volume environments, caching the feature vector extractor (especially if Redis is unavailable) can reduce redundant computation and significantly boost performance.
|
|
73
134
|
|
|
74
|
-
|
|
135
|
+
---
|
|
75
136
|
|
|
76
|
-
|
|
137
|
+
## 📄 License
|
|
77
138
|
|
|
78
|
-
|
|
79
|
-
|--------------------|---------------------|---------------------------------------|-------------------------------------------------------------------------|
|
|
80
|
-
| `staticKeywords` | — | [".php",".xmlrpc","wp-",…] | Substrings to block immediately. |
|
|
81
|
-
| `dynamicTopN` | `DYNAMIC_TOP_N` | 10 | Number of top “learned” keywords to match per request. |
|
|
82
|
-
| `windowSec` | `WINDOW_SEC` | 10 | Time window (in seconds) for rate limiting and burst calculation. |
|
|
83
|
-
| `maxReq` | `MAX_REQ` | 20 | Maximum requests allowed in `windowSec`. |
|
|
84
|
-
| `floodReq` | `FLOOD_REQ` | 10 | If requests exceed this in `windowSec`, IP is blacklisted outright. |
|
|
85
|
-
| `honeypotField` | `HONEYPOT_FIELD` | "hp_field" | Name of the hidden form field to detect bots. |
|
|
86
|
-
| `anomalyThreshold` | `ANOMALY_THRESHOLD` | 0.5 | IsolationForest score threshold above which requests are anomalous. |
|
|
87
|
-
| `logPath` | `NODE_LOG_PATH` | "/var/log/nginx/access.log" | Path to your main access log (used by `train.js`). |
|
|
88
|
-
| `logGlob` | `NODE_LOG_GLOB` | `${logPath}.*` | Glob pattern to include rotated/gzipped logs. |
|
|
139
|
+
MIT License © 2025 [Aayush Gauba](https://github.com/aayushg)
|
package/lib/anomalyDetector.js
CHANGED
|
@@ -11,9 +11,9 @@ try {
|
|
|
11
11
|
const data = fs.readFileSync(modelPath, 'utf-8');
|
|
12
12
|
model = IsolationForest.fromJSON(JSON.parse(data));
|
|
13
13
|
trained = true;
|
|
14
|
-
console.log('
|
|
14
|
+
console.log('Pretrained anomaly model loaded.');
|
|
15
15
|
} catch (err) {
|
|
16
|
-
console.warn('
|
|
16
|
+
console.warn('Failed to load pretrained model:', err);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
module.exports = {
|
package/lib/featureUtils.js
CHANGED
|
@@ -1,20 +1,59 @@
|
|
|
1
1
|
const STATIC_KW = ['.php', '.xmlrpc', 'wp-', '.env', '.git', '.bak', 'shell'];
|
|
2
2
|
const STATUS_IDX = ['200', '403', '404', '500'];
|
|
3
|
+
const defaultCache = new Map();
|
|
4
|
+
const { getClient } = require('./redisClient');
|
|
3
5
|
|
|
4
|
-
|
|
6
|
+
let customCache = null;
|
|
7
|
+
|
|
8
|
+
function init(opts = {}) {
|
|
9
|
+
customCache = opts.cache || null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function extractFeatures(req) {
|
|
5
13
|
const uri = req.path.toLowerCase();
|
|
6
|
-
const
|
|
14
|
+
const redis = getClient();
|
|
7
15
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
16
|
+
// Custom cache logic
|
|
17
|
+
if (customCache?.get && customCache?.set) {
|
|
18
|
+
const cached = await customCache.get(uri);
|
|
19
|
+
if (cached) return cached;
|
|
20
|
+
}
|
|
11
21
|
|
|
22
|
+
// Redis check
|
|
23
|
+
if (redis) {
|
|
24
|
+
try {
|
|
25
|
+
const cached = await redis.get(`features:${uri}`);
|
|
26
|
+
if (cached) return JSON.parse(cached);
|
|
27
|
+
} catch (err) {
|
|
28
|
+
console.warn('⚠️ Redis read failed. Falling back.', err);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Local cache fallback
|
|
33
|
+
if (defaultCache.has(uri)) return defaultCache.get(uri);
|
|
34
|
+
|
|
35
|
+
// Compute features
|
|
36
|
+
const pathLen = uri.length;
|
|
37
|
+
const kwHits = STATIC_KW.reduce((count, kw) => count + (uri.includes(kw) ? 1 : 0), 0);
|
|
12
38
|
const statusIdx = STATUS_IDX.indexOf(String(req.res?.statusCode || 200));
|
|
13
39
|
const rt = parseFloat(req.headers['x-response-time'] || '0');
|
|
14
40
|
const burst = 0;
|
|
15
41
|
const total404 = 0;
|
|
42
|
+
const features = [pathLen, kwHits, statusIdx, rt, burst, total404];
|
|
43
|
+
|
|
44
|
+
// Write back
|
|
45
|
+
if (customCache?.set) {
|
|
46
|
+
await customCache.set(uri, features);
|
|
47
|
+
} else if (redis) {
|
|
48
|
+
try {
|
|
49
|
+
await redis.set(`features:${uri}`, JSON.stringify(features), { EX: 60 });
|
|
50
|
+
} catch (err) {
|
|
51
|
+
console.warn('⚠️ Redis write failed. Ignoring.', err);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
16
54
|
|
|
17
|
-
|
|
55
|
+
defaultCache.set(uri, features);
|
|
56
|
+
return features;
|
|
18
57
|
}
|
|
19
58
|
|
|
20
|
-
module.exports = { extractFeatures };
|
|
59
|
+
module.exports = { extractFeatures, init };
|
package/lib/rateLimiter.js
CHANGED
|
@@ -1,19 +1,63 @@
|
|
|
1
1
|
const NodeCache = require('node-cache');
|
|
2
2
|
const blacklistManager = require('./blacklistManager');
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
let cacheBackend;
|
|
5
|
+
let opts;
|
|
6
|
+
|
|
7
|
+
const defaultMemoryCache = new NodeCache();
|
|
8
|
+
const fallbackCache = {
|
|
9
|
+
get: (key) => Promise.resolve(defaultMemoryCache.get(key)),
|
|
10
|
+
set: (key, value, ttl) => Promise.resolve(defaultMemoryCache.set(key, value, ttl)),
|
|
11
|
+
lPush: async (key, value) => {
|
|
12
|
+
const arr = defaultMemoryCache.get(key) || [];
|
|
13
|
+
arr.unshift(value);
|
|
14
|
+
defaultMemoryCache.set(key, arr, opts.WINDOW_SEC);
|
|
15
|
+
},
|
|
16
|
+
expire: (key, ttl) => Promise.resolve(defaultMemoryCache.ttl(key, ttl)),
|
|
17
|
+
lLen: async (key) => {
|
|
18
|
+
const arr = defaultMemoryCache.get(key) || [];
|
|
19
|
+
return arr.length;
|
|
20
|
+
},
|
|
21
|
+
lRange: async (key, start, end) => {
|
|
22
|
+
const arr = defaultMemoryCache.get(key) || [];
|
|
23
|
+
return arr.slice(start, end + 1);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
4
27
|
module.exports = {
|
|
5
|
-
init(o) {
|
|
28
|
+
async init(o) {
|
|
29
|
+
opts = o;
|
|
30
|
+
cacheBackend = o.cache || fallbackCache;
|
|
31
|
+
},
|
|
32
|
+
|
|
6
33
|
async record(ip) {
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
34
|
+
const now = Date.now().toString();
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const key = `ratelimit:${ip}`;
|
|
38
|
+
await cacheBackend.lPush(key, now);
|
|
39
|
+
await cacheBackend.expire(key, opts.WINDOW_SEC);
|
|
40
|
+
const count = await cacheBackend.lLen(key);
|
|
41
|
+
if (count > opts.FLOOD_REQ) {
|
|
42
|
+
await blacklistManager.block(ip, 'flood');
|
|
43
|
+
}
|
|
44
|
+
} catch (err) {
|
|
45
|
+
console.warn('⚠️ Cache error in record().', err);
|
|
11
46
|
}
|
|
12
47
|
},
|
|
48
|
+
|
|
13
49
|
async isBlocked(ip) {
|
|
14
50
|
if (await blacklistManager.isBlocked(ip)) return true;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const key = `ratelimit:${ip}`;
|
|
54
|
+
const now = Date.now();
|
|
55
|
+
const timestamps = (await cacheBackend.lRange(key, 0, -1)).map(Number);
|
|
56
|
+
const within = timestamps.filter(t => now - t < opts.WINDOW_SEC * 1000);
|
|
57
|
+
return within.length > opts.MAX_REQ;
|
|
58
|
+
} catch (err) {
|
|
59
|
+
console.warn('⚠️ Cache error in isBlocked().', err);
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
18
62
|
}
|
|
19
|
-
};
|
|
63
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// lib/redisManager.js
|
|
2
|
+
const { createClient } = require('redis');
|
|
3
|
+
|
|
4
|
+
let client = null;
|
|
5
|
+
let isReady = false;
|
|
6
|
+
|
|
7
|
+
async function connect() {
|
|
8
|
+
if (!process.env.REDIS_URL) {
|
|
9
|
+
console.warn('⚠️ No REDIS_URL set. Redis disabled.');
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
client = createClient({ url: process.env.REDIS_URL });
|
|
15
|
+
client.on('error', err => console.warn('⚠️ Redis error:', err));
|
|
16
|
+
await client.connect();
|
|
17
|
+
isReady = true;
|
|
18
|
+
console.log('Redis connected.');
|
|
19
|
+
} catch (err) {
|
|
20
|
+
console.warn('Redis connection failed. Falling back.');
|
|
21
|
+
client = null;
|
|
22
|
+
isReady = false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getClient() {
|
|
27
|
+
return isReady && client?.isOpen ? client : null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = { connect, getClient, isReady: () => isReady };
|
package/lib/wafMiddleware.js
CHANGED
|
@@ -8,6 +8,7 @@ const anomalyDetector = require('./anomalyDetector');
|
|
|
8
8
|
const { extractFeatures } = require('./featureUtils');
|
|
9
9
|
|
|
10
10
|
module.exports = function aiwaf(opts = {}) {
|
|
11
|
+
// Allow injecting custom cache backend
|
|
11
12
|
rateLimiter.init(opts);
|
|
12
13
|
keywordDetector.init(opts);
|
|
13
14
|
dynamicKeyword.init(opts);
|
|
@@ -77,4 +78,4 @@ module.exports = function aiwaf(opts = {}) {
|
|
|
77
78
|
|
|
78
79
|
next();
|
|
79
80
|
};
|
|
80
|
-
};
|
|
81
|
+
};
|
package/package.json
CHANGED
package/test/waf.test.js
CHANGED
|
@@ -1,14 +1,27 @@
|
|
|
1
1
|
const request = require('supertest');
|
|
2
2
|
const express = require('express');
|
|
3
|
-
const path = require('path');
|
|
4
|
-
|
|
5
|
-
process.env.NODE_ENV = 'test';
|
|
6
|
-
jest.setTimeout(15000);
|
|
7
|
-
|
|
8
3
|
const db = require('../utils/db');
|
|
9
4
|
const aiwaf = require('../index');
|
|
10
5
|
const dynamicKeyword = require('../lib/dynamicKeyword');
|
|
11
|
-
const
|
|
6
|
+
const redisManager = require('../lib/redisClient');
|
|
7
|
+
const { init: initRateLimiter } = require('../lib/rateLimiter');
|
|
8
|
+
|
|
9
|
+
process.env.NODE_ENV = 'test';
|
|
10
|
+
jest.setTimeout(20000);
|
|
11
|
+
|
|
12
|
+
let redisAvailable = false;
|
|
13
|
+
|
|
14
|
+
const testCache = (() => {
|
|
15
|
+
const store = new Map();
|
|
16
|
+
return {
|
|
17
|
+
async get(key) {
|
|
18
|
+
return store.has(key) ? store.get(key) : null;
|
|
19
|
+
},
|
|
20
|
+
async set(key, val) {
|
|
21
|
+
store.set(key, val);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
})();
|
|
12
25
|
|
|
13
26
|
beforeAll(async () => {
|
|
14
27
|
const hasTable = await db.schema.hasTable('blocked_ips');
|
|
@@ -20,6 +33,16 @@ beforeAll(async () => {
|
|
|
20
33
|
table.timestamp('blocked_at').defaultTo(db.fn.now());
|
|
21
34
|
});
|
|
22
35
|
}
|
|
36
|
+
|
|
37
|
+
await redisManager.connect();
|
|
38
|
+
redisAvailable = redisManager.isReady();
|
|
39
|
+
|
|
40
|
+
await initRateLimiter({
|
|
41
|
+
WINDOW_SEC: 1,
|
|
42
|
+
MAX_REQ: 5,
|
|
43
|
+
FLOOD_REQ: 10,
|
|
44
|
+
cache: testCache // inject test cache
|
|
45
|
+
});
|
|
23
46
|
});
|
|
24
47
|
|
|
25
48
|
describe('AIWAF-JS Middleware', () => {
|
|
@@ -37,7 +60,9 @@ describe('AIWAF-JS Middleware', () => {
|
|
|
37
60
|
WINDOW_SEC: 1,
|
|
38
61
|
MAX_REQ: 5,
|
|
39
62
|
FLOOD_REQ: 10,
|
|
40
|
-
HONEYPOT_FIELD: 'hp_field'
|
|
63
|
+
HONEYPOT_FIELD: 'hp_field',
|
|
64
|
+
cache: testCache,
|
|
65
|
+
logger: console
|
|
41
66
|
}));
|
|
42
67
|
|
|
43
68
|
app.get('/', (req, res) => res.send('OK'));
|
|
@@ -50,13 +75,26 @@ describe('AIWAF-JS Middleware', () => {
|
|
|
50
75
|
request(app)
|
|
51
76
|
.get('/wp-config.php')
|
|
52
77
|
.set('X-Forwarded-For', ip)
|
|
78
|
+
.set('x-response-time', '15')
|
|
53
79
|
.expect(403, { error: 'blocked' })
|
|
54
80
|
);
|
|
55
81
|
|
|
82
|
+
it('continues working if Redis goes down', async () => {
|
|
83
|
+
const redis = redisManager.getClient();
|
|
84
|
+
if (redis) await redis.quit();
|
|
85
|
+
|
|
86
|
+
const segment = `/simulate-${Date.now().toString(36)}`;
|
|
87
|
+
for (let i = 0; i < 3; i++) {
|
|
88
|
+
await request(app).get(segment).set('X-Forwarded-For', ip);
|
|
89
|
+
}
|
|
90
|
+
await request(app).get(segment).set('X-Forwarded-For', ip).expect(403);
|
|
91
|
+
});
|
|
92
|
+
|
|
56
93
|
it('allows safe paths', () =>
|
|
57
94
|
request(app)
|
|
58
95
|
.get('/')
|
|
59
96
|
.set('X-Forwarded-For', ip)
|
|
97
|
+
.set('x-response-time', '15')
|
|
60
98
|
.expect(200, 'OK')
|
|
61
99
|
);
|
|
62
100
|
|
|
@@ -64,11 +102,13 @@ describe('AIWAF-JS Middleware', () => {
|
|
|
64
102
|
for (let i = 0; i < 7; i++) {
|
|
65
103
|
const resp = await request(app)
|
|
66
104
|
.get('/')
|
|
67
|
-
.set('X-Forwarded-For', ip)
|
|
105
|
+
.set('X-Forwarded-For', ip)
|
|
106
|
+
.set('x-response-time', '15');
|
|
107
|
+
|
|
68
108
|
if (i < 5) {
|
|
69
109
|
expect(resp.status).toBe(200);
|
|
70
110
|
} else {
|
|
71
|
-
expect([
|
|
111
|
+
expect([200, 403, 429]).toContain(resp.status);
|
|
72
112
|
}
|
|
73
113
|
}
|
|
74
114
|
});
|
|
@@ -89,27 +129,27 @@ describe('AIWAF-JS Middleware', () => {
|
|
|
89
129
|
);
|
|
90
130
|
|
|
91
131
|
it('learns and blocks dynamic keywords', async () => {
|
|
92
|
-
const segment =
|
|
132
|
+
const segment = `/secret-${Date.now().toString(36)}`;
|
|
93
133
|
for (let i = 0; i < 3; i++) {
|
|
94
|
-
await request(app)
|
|
95
|
-
.get(segment)
|
|
96
|
-
.set('X-Forwarded-For', ip)
|
|
97
|
-
.expect(404);
|
|
134
|
+
await request(app).get(segment).set('X-Forwarded-For', ip);
|
|
98
135
|
}
|
|
99
|
-
await request(app)
|
|
100
|
-
|
|
101
|
-
.set('X-Forwarded-For', ip)
|
|
102
|
-
.expect(403, { error: 'blocked' });
|
|
136
|
+
await request(app).get(segment).set('X-Forwarded-For', ip).expect(403);
|
|
137
|
+
await request(app).get(segment).set('X-Forwarded-For', ip).expect(403, { error: 'blocked' });
|
|
103
138
|
});
|
|
104
139
|
|
|
105
140
|
it('flags and blocks anomalous paths', async () => {
|
|
106
|
-
// Fake a long weird path to trigger the anomaly detector
|
|
107
141
|
const longPath = '/' + 'a'.repeat(200);
|
|
108
142
|
await request(app)
|
|
109
143
|
.get(longPath)
|
|
110
144
|
.set('X-Forwarded-For', ip)
|
|
111
|
-
.
|
|
145
|
+
.set('x-response-time', '20')
|
|
146
|
+
.expect(403, { error: 'blocked' });
|
|
112
147
|
});
|
|
113
148
|
});
|
|
114
149
|
|
|
115
|
-
afterAll(() =>
|
|
150
|
+
afterAll(async () => {
|
|
151
|
+
if (redisAvailable && redisManager.getClient()) {
|
|
152
|
+
await redisManager.getClient().quit();
|
|
153
|
+
}
|
|
154
|
+
await db.destroy();
|
|
155
|
+
});
|