aiwaf-js 0.0.3 → 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
@@ -19,6 +19,7 @@
19
19
  - ✅ Anomaly Detection (Isolation Forest)
20
20
  - ✅ Redis Support for multiprocess environments
21
21
  - ✅ Offline Training from access logs
22
+ - ✅ **Custom Cache Logic Support**
22
23
 
23
24
  ---
24
25
 
@@ -73,6 +74,31 @@ If Redis is unavailable, it gracefully falls back to in-memory mode.
73
74
 
74
75
  ---
75
76
 
77
+ ## Custom Cache Logic (Advanced)
78
+
79
+ You can inject your own cache logic (in-memory, Redis, hybrid, or file-based) by passing a `cache` object implementing the following interface:
80
+
81
+ ```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
+ ```
97
+
98
+ This overrides Redis/in-memory usage with your custom strategy for all cache operations.
99
+
100
+ ---
101
+
76
102
  ## Configuration
77
103
 
78
104
  ```js
@@ -83,6 +109,7 @@ app.use(aiwaf({
83
109
  MAX_REQ: 20,
84
110
  FLOOD_REQ: 10,
85
111
  HONEYPOT_FIELD: 'hp_field',
112
+ cache: myCustomCache, // optional custom cache injection
86
113
  }));
87
114
  ```
88
115
 
@@ -97,6 +124,7 @@ app.use(aiwaf({
97
124
  | `anomalyThreshold` | `ANOMALY_THRESHOLD` | 0.5 | Threshold for IsolationForest-based anomaly detection. |
98
125
  | `logPath` | `NODE_LOG_PATH` | "/var/log/nginx/access.log" | Path to access log file. |
99
126
  | `logGlob` | `NODE_LOG_GLOB` | "${logPath}.*" | Glob pattern to include rotated/gzipped logs. |
127
+ | `cache` | — | undefined | Custom cache implementation (overrides Redis/memory) |
100
128
 
101
129
  ---
102
130
 
@@ -1,23 +1,38 @@
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();
3
+ const defaultCache = new Map();
4
4
  const { getClient } = require('./redisClient');
5
5
 
6
+ let customCache = null;
7
+
8
+ function init(opts = {}) {
9
+ customCache = opts.cache || null;
10
+ }
11
+
6
12
  async function extractFeatures(req) {
7
13
  const uri = req.path.toLowerCase();
8
14
  const redis = getClient();
9
15
 
16
+ // Custom cache logic
17
+ if (customCache?.get && customCache?.set) {
18
+ const cached = await customCache.get(uri);
19
+ if (cached) return cached;
20
+ }
21
+
22
+ // Redis check
10
23
  if (redis) {
11
24
  try {
12
25
  const cached = await redis.get(`features:${uri}`);
13
26
  if (cached) return JSON.parse(cached);
14
27
  } catch (err) {
15
- console.warn('⚠️ Redis read failed. Using fallback.', err);
28
+ console.warn('⚠️ Redis read failed. Falling back.', err);
16
29
  }
17
30
  }
18
31
 
19
- if (localCache.has(uri)) return localCache.get(uri);
32
+ // Local cache fallback
33
+ if (defaultCache.has(uri)) return defaultCache.get(uri);
20
34
 
35
+ // Compute features
21
36
  const pathLen = uri.length;
22
37
  const kwHits = STATIC_KW.reduce((count, kw) => count + (uri.includes(kw) ? 1 : 0), 0);
23
38
  const statusIdx = STATUS_IDX.indexOf(String(req.res?.statusCode || 200));
@@ -26,7 +41,10 @@ async function extractFeatures(req) {
26
41
  const total404 = 0;
27
42
  const features = [pathLen, kwHits, statusIdx, rt, burst, total404];
28
43
 
29
- if (redis) {
44
+ // Write back
45
+ if (customCache?.set) {
46
+ await customCache.set(uri, features);
47
+ } else if (redis) {
30
48
  try {
31
49
  await redis.set(`features:${uri}`, JSON.stringify(features), { EX: 60 });
32
50
  } catch (err) {
@@ -34,8 +52,8 @@ async function extractFeatures(req) {
34
52
  }
35
53
  }
36
54
 
37
- localCache.set(uri, features);
55
+ defaultCache.set(uri, features);
38
56
  return features;
39
57
  }
40
58
 
41
- module.exports = { extractFeatures };
59
+ module.exports = { extractFeatures, init };
@@ -1,56 +1,63 @@
1
1
  const NodeCache = require('node-cache');
2
2
  const blacklistManager = require('./blacklistManager');
3
- const { getClient } = require('./redisClient');
4
3
 
5
- const memoryCache = new NodeCache();
4
+ let cacheBackend;
6
5
  let opts;
7
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
+
8
27
  module.exports = {
9
28
  async init(o) {
10
29
  opts = o;
30
+ cacheBackend = o.cache || fallbackCache;
11
31
  },
12
32
 
13
33
  async record(ip) {
14
34
  const now = Date.now().toString();
15
- const redis = getClient();
16
35
 
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);
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');
27
43
  }
44
+ } catch (err) {
45
+ console.warn('⚠️ Cache error in record().', err);
28
46
  }
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');
34
47
  },
35
48
 
36
49
  async isBlocked(ip) {
37
50
  if (await blacklistManager.isBlocked(ip)) return true;
38
- const now = Date.now();
39
- const redis = getClient();
40
51
 
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
- }
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;
50
61
  }
51
-
52
- const logs = memoryCache.get(ip) || [];
53
- const within = logs.filter(t => now - t < opts.WINDOW_SEC * 1000);
54
- return within.length > opts.MAX_REQ;
55
62
  }
56
- };
63
+ };
@@ -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.3",
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,17 +1,28 @@
1
1
  const request = require('supertest');
2
2
  const express = require('express');
3
-
4
- process.env.NODE_ENV = 'test';
5
- jest.setTimeout(20000);
6
-
7
3
  const db = require('../utils/db');
8
4
  const aiwaf = require('../index');
9
5
  const dynamicKeyword = require('../lib/dynamicKeyword');
10
6
  const redisManager = require('../lib/redisClient');
11
7
  const { init: initRateLimiter } = require('../lib/rateLimiter');
12
8
 
9
+ process.env.NODE_ENV = 'test';
10
+ jest.setTimeout(20000);
11
+
13
12
  let redisAvailable = false;
14
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
+ })();
25
+
15
26
  beforeAll(async () => {
16
27
  const hasTable = await db.schema.hasTable('blocked_ips');
17
28
  if (!hasTable) {
@@ -29,7 +40,8 @@ beforeAll(async () => {
29
40
  await initRateLimiter({
30
41
  WINDOW_SEC: 1,
31
42
  MAX_REQ: 5,
32
- FLOOD_REQ: 10
43
+ FLOOD_REQ: 10,
44
+ cache: testCache // inject test cache
33
45
  });
34
46
  });
35
47
 
@@ -48,7 +60,9 @@ describe('AIWAF-JS Middleware', () => {
48
60
  WINDOW_SEC: 1,
49
61
  MAX_REQ: 5,
50
62
  FLOOD_REQ: 10,
51
- HONEYPOT_FIELD: 'hp_field'
63
+ HONEYPOT_FIELD: 'hp_field',
64
+ cache: testCache,
65
+ logger: console
52
66
  }));
53
67
 
54
68
  app.get('/', (req, res) => res.send('OK'));
@@ -64,17 +78,18 @@ describe('AIWAF-JS Middleware', () => {
64
78
  .set('x-response-time', '15')
65
79
  .expect(403, { error: 'blocked' })
66
80
  );
81
+
67
82
  it('continues working if Redis goes down', async () => {
68
83
  const redis = redisManager.getClient();
69
- if (redis) await redis.quit(); // force disconnect
70
-
84
+ if (redis) await redis.quit();
85
+
71
86
  const segment = `/simulate-${Date.now().toString(36)}`;
72
87
  for (let i = 0; i < 3; i++) {
73
- await request(app).get(segment).set('X-Forwarded-For', ip); // fallback path
88
+ await request(app).get(segment).set('X-Forwarded-For', ip);
74
89
  }
75
90
  await request(app).get(segment).set('X-Forwarded-For', ip).expect(403);
76
91
  });
77
-
92
+
78
93
  it('allows safe paths', () =>
79
94
  request(app)
80
95
  .get('/')
@@ -116,9 +131,9 @@ describe('AIWAF-JS Middleware', () => {
116
131
  it('learns and blocks dynamic keywords', async () => {
117
132
  const segment = `/secret-${Date.now().toString(36)}`;
118
133
  for (let i = 0; i < 3; i++) {
119
- await request(app).get(segment).set('X-Forwarded-For', ip); // allow learning phase
134
+ await request(app).get(segment).set('X-Forwarded-For', ip);
120
135
  }
121
- await request(app).get(segment).set('X-Forwarded-For', ip).expect(403); // block expected
136
+ await request(app).get(segment).set('X-Forwarded-For', ip).expect(403);
122
137
  await request(app).get(segment).set('X-Forwarded-For', ip).expect(403, { error: 'blocked' });
123
138
  });
124
139
 
@@ -137,4 +152,4 @@ afterAll(async () => {
137
152
  await redisManager.getClient().quit();
138
153
  }
139
154
  await db.destroy();
140
- });
155
+ });