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 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
  [![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
+ - ✅ **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
- ## Training
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
- NODE_LOG_PATH=/path/to/access.log npm run train
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
- ## Usage Example
77
+ ## Custom Cache Logic (Advanced)
47
78
 
48
- Here’s a simple Express app that uses `aiwaf-js` with custom settings:
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 express = require('express');
52
- const aiwaf = require('aiwaf-js');
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
- const app = express();
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'], // ← add .php here
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
- ## License
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
- MIT License © 2025 Aayush Gauba
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
- ## Configuration Options
135
+ ---
75
136
 
76
- You can pass an options object to `aiwaf(opts)` or use environment variables.
137
+ ## 📄 License
77
138
 
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. |
139
+ 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,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
- function extractFeatures(req) {
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 pathLen = uri.length;
14
+ const redis = getClient();
7
15
 
8
- const kwHits = STATIC_KW.reduce(
9
- (count, kw) => count + (uri.includes(kw) ? 1 : 0), 0
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
- return [pathLen, kwHits, statusIdx, rt, burst, total404];
55
+ defaultCache.set(uri, features);
56
+ return features;
18
57
  }
19
58
 
20
- module.exports = { extractFeatures };
59
+ module.exports = { extractFeatures, init };
@@ -1,19 +1,63 @@
1
1
  const NodeCache = require('node-cache');
2
2
  const blacklistManager = require('./blacklistManager');
3
- let cache, opts;
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) { opts = o; cache = new NodeCache({ stdTTL: opts.WINDOW_SEC }); },
28
+ async init(o) {
29
+ opts = o;
30
+ cacheBackend = o.cache || fallbackCache;
31
+ },
32
+
6
33
  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');
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
- const recs = cache.get(ip) || [];
16
- const within = recs.filter(t => Date.now() - t < opts.WINDOW_SEC*1000);
17
- return within.length > opts.MAX_REQ;
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 };
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiwaf-js",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
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,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 anomalyDetector = require('../lib/anomalyDetector');
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([429, 403]).toContain(resp.status);
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 = '/secretABC';
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
- .get(segment)
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
- .expect(403, { error: 'blocked' }); // should be flagged as anomalous
145
+ .set('x-response-time', '20')
146
+ .expect(403, { error: 'blocked' });
112
147
  });
113
148
  });
114
149
 
115
- afterAll(() => db.destroy());
150
+ afterAll(async () => {
151
+ if (redisAvailable && redisManager.getClient()) {
152
+ await redisManager.getClient().quit();
153
+ }
154
+ await db.destroy();
155
+ });