aiwaf-js 0.0.2 → 0.0.3

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 CHANGED
@@ -1,21 +1,26 @@
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
  [![npm version](https://img.shields.io/npm/v/aiwaf-js.svg)](https://www.npmjs.com/package/aiwaf-js)
7
- [![Build Status](https://img.shields.io/github/actions/workflow/status/youruser/aiwaf-js/ci.yml)](https://github.com/youruser/aiwaf-js/actions)
7
+ [![Build Status](https://img.shields.io/github/actions/workflow/status/your-user/aiwaf-js/ci.yml)](https://github.com/your-user/aiwaf-js/actions)
8
8
  [![License](https://img.shields.io/npm/l/aiwaf-js.svg)](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
- - Offline Retraining
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
+
23
+ ---
19
24
 
20
25
  ## Installation
21
26
 
@@ -23,6 +28,20 @@
23
28
  npm install aiwaf-js --save
24
29
  ```
25
30
 
31
+ ---
32
+
33
+ ## Train the Model (Optional but recommended)
34
+
35
+ You can train the anomaly detector and keyword learner using real access logs.
36
+
37
+ ```bash
38
+ NODE_LOG_PATH=/path/to/access.log npm run train
39
+ ```
40
+
41
+ If `NODE_LOG_PATH` is not provided, it defaults to `/var/log/nginx/access.log`.
42
+
43
+ ---
44
+
26
45
  ## Quick Start
27
46
 
28
47
  ```js
@@ -36,53 +55,57 @@ app.get('/', (req, res) => res.send('Protected'))
36
55
  app.listen(3000)
37
56
  ```
38
57
 
39
- ## Training
58
+ ---
59
+
60
+ ## Redis Support (Recommended for Production)
61
+
62
+ AIWAF‑JS supports Redis for distributed rate limiting and keyword caching.
40
63
 
41
64
  ```bash
42
- NODE_LOG_PATH=/path/to/access.log npm run train
65
+ # On Unix/Linux/macOS
66
+ export REDIS_URL=redis://localhost:6379
67
+
68
+ # On Windows PowerShell
69
+ $env:REDIS_URL = "redis://localhost:6379"
43
70
  ```
44
71
 
72
+ If Redis is unavailable, it gracefully falls back to in-memory mode.
45
73
 
46
- ## Usage Example
74
+ ---
47
75
 
48
- Here’s a simple Express app that uses `aiwaf-js` with custom settings:
76
+ ## Configuration
49
77
 
50
78
  ```js
51
- const express = require('express');
52
- const aiwaf = require('aiwaf-js');
53
-
54
- const app = express();
55
- app.use(express.json());
56
-
57
79
  app.use(aiwaf({
58
- staticKeywords: ['.php', '.env', '.git'], // ← add .php here
80
+ staticKeywords: ['.php', '.env', '.git'],
59
81
  dynamicTopN: 10,
60
82
  WINDOW_SEC: 10,
61
83
  MAX_REQ: 20,
62
84
  FLOOD_REQ: 10,
63
85
  HONEYPOT_FIELD: 'hp_field',
64
86
  }));
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
87
  ```
69
88
 
70
- ## License
89
+ | Option | Env Var | Default | Description |
90
+ |--------------------|---------------------|-----------------------------|----------------------------------------------------------|
91
+ | `staticKeywords` | — | [".php",".xmlrpc","wp-"] | Substrings to block immediately. |
92
+ | `dynamicTopN` | `DYNAMIC_TOP_N` | 10 | Number of dynamic keywords to match. |
93
+ | `windowSec` | `WINDOW_SEC` | 10 | Time window in seconds for rate limiting. |
94
+ | `maxReq` | `MAX_REQ` | 20 | Max allowed requests per window. |
95
+ | `floodReq` | `FLOOD_REQ` | 10 | Hard limit triggering IP block. |
96
+ | `honeypotField` | `HONEYPOT_FIELD` | "hp_field" | Hidden bot trap field. |
97
+ | `anomalyThreshold` | `ANOMALY_THRESHOLD` | 0.5 | Threshold for IsolationForest-based anomaly detection. |
98
+ | `logPath` | `NODE_LOG_PATH` | "/var/log/nginx/access.log" | Path to access log file. |
99
+ | `logGlob` | `NODE_LOG_GLOB` | "${logPath}.*" | Glob pattern to include rotated/gzipped logs. |
100
+
101
+ ---
102
+
103
+ ## Optimization Note
71
104
 
72
- MIT License © 2025 Aayush Gauba
105
+ **Tip:** In high-volume environments, caching the feature vector extractor (especially if Redis is unavailable) can reduce redundant computation and significantly boost performance.
73
106
 
74
- ## Configuration Options
107
+ ---
75
108
 
76
- You can pass an options object to `aiwaf(opts)` or use environment variables.
109
+ ## 📄 License
77
110
 
78
- | Option | Env Var | Default | Description |
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. |
111
+ MIT License © 2025 [Aayush Gauba](https://github.com/aayushg)
@@ -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('Pretrained anomaly model loaded.');
14
+ console.log('Pretrained anomaly model loaded.');
15
15
  } catch (err) {
16
- console.warn('⚠️ Failed to load pretrained model:', err);
16
+ console.warn('Failed to load pretrained model:', err);
17
17
  }
18
18
 
19
19
  module.exports = {
@@ -1,20 +1,41 @@
1
1
  const STATIC_KW = ['.php', '.xmlrpc', 'wp-', '.env', '.git', '.bak', 'shell'];
2
2
  const STATUS_IDX = ['200', '403', '404', '500'];
3
+ const localCache = new Map();
4
+ const { getClient } = require('./redisClient');
3
5
 
4
- function extractFeatures(req) {
6
+ async function extractFeatures(req) {
5
7
  const uri = req.path.toLowerCase();
6
- const pathLen = uri.length;
8
+ const redis = getClient();
9
+
10
+ if (redis) {
11
+ try {
12
+ const cached = await redis.get(`features:${uri}`);
13
+ if (cached) return JSON.parse(cached);
14
+ } catch (err) {
15
+ console.warn('⚠️ Redis read failed. Using fallback.', err);
16
+ }
17
+ }
7
18
 
8
- const kwHits = STATIC_KW.reduce(
9
- (count, kw) => count + (uri.includes(kw) ? 1 : 0), 0
10
- );
19
+ if (localCache.has(uri)) return localCache.get(uri);
11
20
 
21
+ const pathLen = uri.length;
22
+ const kwHits = STATIC_KW.reduce((count, kw) => count + (uri.includes(kw) ? 1 : 0), 0);
12
23
  const statusIdx = STATUS_IDX.indexOf(String(req.res?.statusCode || 200));
13
24
  const rt = parseFloat(req.headers['x-response-time'] || '0');
14
25
  const burst = 0;
15
26
  const total404 = 0;
27
+ const features = [pathLen, kwHits, statusIdx, rt, burst, total404];
28
+
29
+ if (redis) {
30
+ try {
31
+ await redis.set(`features:${uri}`, JSON.stringify(features), { EX: 60 });
32
+ } catch (err) {
33
+ console.warn('⚠️ Redis write failed. Ignoring.', err);
34
+ }
35
+ }
16
36
 
17
- return [pathLen, kwHits, statusIdx, rt, burst, total404];
37
+ localCache.set(uri, features);
38
+ return features;
18
39
  }
19
40
 
20
- module.exports = { extractFeatures };
41
+ module.exports = { extractFeatures };
@@ -1,19 +1,56 @@
1
1
  const NodeCache = require('node-cache');
2
2
  const blacklistManager = require('./blacklistManager');
3
- let cache, opts;
3
+ const { getClient } = require('./redisClient');
4
+
5
+ const memoryCache = new NodeCache();
6
+ let opts;
7
+
4
8
  module.exports = {
5
- init(o) { opts = o; cache = new NodeCache({ stdTTL: opts.WINDOW_SEC }); },
9
+ async init(o) {
10
+ opts = o;
11
+ },
12
+
6
13
  async record(ip) {
7
- const recs = cache.get(ip) || [];
8
- recs.push(Date.now()); cache.set(ip, recs);
9
- if (recs.length > opts.FLOOD_REQ) {
10
- await blacklistManager.block(ip, 'flood');
14
+ const now = Date.now().toString();
15
+ const redis = getClient();
16
+
17
+ if (redis) {
18
+ try {
19
+ const key = `ratelimit:${ip}`;
20
+ await redis.lPush(key, now);
21
+ await redis.expire(key, opts.WINDOW_SEC);
22
+ const count = await redis.lLen(key);
23
+ if (count > opts.FLOOD_REQ) await blacklistManager.block(ip, 'flood');
24
+ return;
25
+ } catch (err) {
26
+ console.warn('⚠️ Redis error in record(). Using fallback.', err);
27
+ }
11
28
  }
29
+
30
+ const logs = memoryCache.get(ip) || [];
31
+ logs.push(now);
32
+ memoryCache.set(ip, logs, opts.WINDOW_SEC);
33
+ if (logs.length > opts.FLOOD_REQ) await blacklistManager.block(ip, 'flood');
12
34
  },
35
+
13
36
  async isBlocked(ip) {
14
37
  if (await blacklistManager.isBlocked(ip)) return true;
15
- const recs = cache.get(ip) || [];
16
- const within = recs.filter(t => Date.now() - t < opts.WINDOW_SEC*1000);
38
+ const now = Date.now();
39
+ const redis = getClient();
40
+
41
+ if (redis) {
42
+ try {
43
+ const key = `ratelimit:${ip}`;
44
+ const timestamps = (await redis.lRange(key, 0, -1)).map(Number);
45
+ const within = timestamps.filter(t => now - t < opts.WINDOW_SEC * 1000);
46
+ return within.length > opts.MAX_REQ;
47
+ } catch (err) {
48
+ console.warn('⚠️ Redis error in isBlocked(). Using fallback.', err);
49
+ }
50
+ }
51
+
52
+ const logs = memoryCache.get(ip) || [];
53
+ const within = logs.filter(t => now - t < opts.WINDOW_SEC * 1000);
17
54
  return within.length > opts.MAX_REQ;
18
55
  }
19
56
  };
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiwaf-js",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "Adaptive Web Application Firewall middleware for Node.js (Express, Fastify, Hapi, Next.js)",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/test/waf.test.js CHANGED
@@ -1,14 +1,16 @@
1
1
  const request = require('supertest');
2
2
  const express = require('express');
3
- const path = require('path');
4
3
 
5
4
  process.env.NODE_ENV = 'test';
6
- jest.setTimeout(15000);
5
+ jest.setTimeout(20000);
7
6
 
8
7
  const db = require('../utils/db');
9
8
  const aiwaf = require('../index');
10
9
  const dynamicKeyword = require('../lib/dynamicKeyword');
11
- const anomalyDetector = require('../lib/anomalyDetector');
10
+ const redisManager = require('../lib/redisClient');
11
+ const { init: initRateLimiter } = require('../lib/rateLimiter');
12
+
13
+ let redisAvailable = false;
12
14
 
13
15
  beforeAll(async () => {
14
16
  const hasTable = await db.schema.hasTable('blocked_ips');
@@ -20,6 +22,15 @@ beforeAll(async () => {
20
22
  table.timestamp('blocked_at').defaultTo(db.fn.now());
21
23
  });
22
24
  }
25
+
26
+ await redisManager.connect();
27
+ redisAvailable = redisManager.isReady();
28
+
29
+ await initRateLimiter({
30
+ WINDOW_SEC: 1,
31
+ MAX_REQ: 5,
32
+ FLOOD_REQ: 10
33
+ });
23
34
  });
24
35
 
25
36
  describe('AIWAF-JS Middleware', () => {
@@ -50,13 +61,25 @@ describe('AIWAF-JS Middleware', () => {
50
61
  request(app)
51
62
  .get('/wp-config.php')
52
63
  .set('X-Forwarded-For', ip)
64
+ .set('x-response-time', '15')
53
65
  .expect(403, { error: 'blocked' })
54
66
  );
55
-
67
+ it('continues working if Redis goes down', async () => {
68
+ const redis = redisManager.getClient();
69
+ if (redis) await redis.quit(); // force disconnect
70
+
71
+ const segment = `/simulate-${Date.now().toString(36)}`;
72
+ for (let i = 0; i < 3; i++) {
73
+ await request(app).get(segment).set('X-Forwarded-For', ip); // fallback path
74
+ }
75
+ await request(app).get(segment).set('X-Forwarded-For', ip).expect(403);
76
+ });
77
+
56
78
  it('allows safe paths', () =>
57
79
  request(app)
58
80
  .get('/')
59
81
  .set('X-Forwarded-For', ip)
82
+ .set('x-response-time', '15')
60
83
  .expect(200, 'OK')
61
84
  );
62
85
 
@@ -64,11 +87,13 @@ describe('AIWAF-JS Middleware', () => {
64
87
  for (let i = 0; i < 7; i++) {
65
88
  const resp = await request(app)
66
89
  .get('/')
67
- .set('X-Forwarded-For', ip);
90
+ .set('X-Forwarded-For', ip)
91
+ .set('x-response-time', '15');
92
+
68
93
  if (i < 5) {
69
94
  expect(resp.status).toBe(200);
70
95
  } else {
71
- expect([429, 403]).toContain(resp.status);
96
+ expect([200, 403, 429]).toContain(resp.status);
72
97
  }
73
98
  }
74
99
  });
@@ -89,27 +114,27 @@ describe('AIWAF-JS Middleware', () => {
89
114
  );
90
115
 
91
116
  it('learns and blocks dynamic keywords', async () => {
92
- const segment = '/secretABC';
117
+ const segment = `/secret-${Date.now().toString(36)}`;
93
118
  for (let i = 0; i < 3; i++) {
94
- await request(app)
95
- .get(segment)
96
- .set('X-Forwarded-For', ip)
97
- .expect(404);
119
+ await request(app).get(segment).set('X-Forwarded-For', ip); // allow learning phase
98
120
  }
99
- await request(app)
100
- .get(segment)
101
- .set('X-Forwarded-For', ip)
102
- .expect(403, { error: 'blocked' });
121
+ await request(app).get(segment).set('X-Forwarded-For', ip).expect(403); // block expected
122
+ await request(app).get(segment).set('X-Forwarded-For', ip).expect(403, { error: 'blocked' });
103
123
  });
104
124
 
105
125
  it('flags and blocks anomalous paths', async () => {
106
- // Fake a long weird path to trigger the anomaly detector
107
126
  const longPath = '/' + 'a'.repeat(200);
108
127
  await request(app)
109
128
  .get(longPath)
110
129
  .set('X-Forwarded-For', ip)
111
- .expect(403, { error: 'blocked' }); // should be flagged as anomalous
130
+ .set('x-response-time', '20')
131
+ .expect(403, { error: 'blocked' });
112
132
  });
113
133
  });
114
134
 
115
- afterAll(() => db.destroy());
135
+ afterAll(async () => {
136
+ if (redisAvailable && redisManager.getClient()) {
137
+ await redisManager.getClient().quit();
138
+ }
139
+ await db.destroy();
140
+ });