mdefender-pro 1.1.0

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 ADDED
@@ -0,0 +1,194 @@
1
+ # MDefender Pro
2
+
3
+ [![npm version](https://img.shields.io/npm/v/mdefender.svg)](https://www.npmjs.com/package/mdefender)
4
+ [![license](https://img.shields.io/npm/l/mdefender.svg)](https://github.com/mdefender/mdefender/blob/main/LICENSE)
5
+
6
+ **MDefender Pro** is a Web Application Firewall (WAF) middleware for Node.js/Express. It intercepts incoming HTTP requests and sends them to the MDefender Pro API for real-time threat analysis, blocking malicious traffic such as XSS, SQLi, CSRF, and other OWASP Top 10 attacks.
7
+
8
+ ## Installation
9
+
10
+ ```bash
11
+ npm install mdefender
12
+ ```
13
+
14
+ ## Quick Start
15
+
16
+ ```js
17
+ const express = require('express');
18
+ const mdefender = require('mdefender');
19
+
20
+ const app = express();
21
+
22
+ // Protect all routes
23
+ app.use(mdefender({
24
+ apiKey: 'your-api-key-here',
25
+ domain: 'example.com',
26
+ }));
27
+
28
+ app.get('/', (req, res) => {
29
+ res.send('Hello, protected world!');
30
+ });
31
+
32
+ app.listen(3000, () => {
33
+ console.log('Server running on port 3000');
34
+ });
35
+ ```
36
+
37
+ ## Configuration
38
+
39
+ You can configure MDefender in three ways:
40
+
41
+ 1. **Inline** - Pass options directly to the middleware
42
+ 2. **Config file** - Use `mdefender.config.js` or `mdefender.json`
43
+ 3. **package.json** - Add a `"mdefender"` key to your `package.json`
44
+
45
+ Priority order: **Inline options > Config file > package.json**
46
+
47
+ ### Options
48
+
49
+ | Option | Type | Default | Description |
50
+ |---|---|---|---|
51
+ | `apiKey` | `string` | `''` | **Required.** Your MDefender Pro API key |
52
+ | `domain` | `string` | `''` | **Required.** The domain to protect |
53
+ | `apiEndpoint` | `string` | `'https://mdefender-pro.onrender.com'` | MDefender API base URL |
54
+ | `mode` | `'block' \| 'monitor' \| 'off'` | `'block'` | `block` = block threats, `monitor` = log only, `off` = disabled |
55
+ | `blockStatusCode` | `number` | `403` | HTTP status code for blocked requests |
56
+ | `timeout` | `number` | `5000` | API request timeout in milliseconds |
57
+ | `maxBodySize` | `number` | `1048576` | Max request body size in bytes (1MB) |
58
+ | `logBlocked` | `boolean` | `true` | Log blocked requests to console |
59
+ | `customBlockPage` | `string \| null` | `null` | Path to a custom HTML file for block page |
60
+ | `skipPaths` | `string[]` | `['/health', '/favicon.ico']` | Paths to skip WAF checking |
61
+ | `skipUserAgents` | `string[]` | `[]` | User agents to skip (substring match) |
62
+ | `skipMethods` | `string[]` | `[]` | HTTP methods to skip entirely |
63
+ | `headers` | `boolean` | `true` | Forward original request headers to API |
64
+ | `onError` | `'allow' \| 'block'` | `'allow'` | Behavior when API is unreachable |
65
+
66
+ ### Config File: `mdefender.config.js`
67
+
68
+ ```js
69
+ module.exports = {
70
+ apiKey: process.env.MDEFENDER_API_KEY,
71
+ domain: 'example.com',
72
+ mode: 'block',
73
+ skipPaths: ['/health', '/ping', '/favicon.ico'],
74
+ logBlocked: true,
75
+ };
76
+ ```
77
+
78
+ ### Config File: `mdefender.json`
79
+
80
+ ```json
81
+ {
82
+ "apiKey": "your-api-key-here",
83
+ "domain": "example.com",
84
+ "mode": "block",
85
+ "skipPaths": ["/health", "/ping"],
86
+ "onError": "allow"
87
+ }
88
+ ```
89
+
90
+ ### `package.json`
91
+
92
+ ```json
93
+ {
94
+ "name": "my-app",
95
+ "mdefender": {
96
+ "apiKey": "your-api-key-here",
97
+ "domain": "example.com"
98
+ }
99
+ }
100
+ ```
101
+
102
+ ## Advanced Usage
103
+
104
+ ### Custom Block Page
105
+
106
+ Provide a path to your own HTML file:
107
+
108
+ ```js
109
+ app.use(mdefender({
110
+ apiKey: 'your-key',
111
+ domain: 'example.com',
112
+ customBlockPage: path.join(__dirname, 'views', 'block.html'),
113
+ }));
114
+ ```
115
+
116
+ ### Monitor Mode (Log Only)
117
+
118
+ Run in monitoring mode to analyze requests without blocking:
119
+
120
+ ```js
121
+ app.use(mdefender({
122
+ apiKey: 'your-key',
123
+ domain: 'example.com',
124
+ mode: 'monitor',
125
+ }));
126
+ ```
127
+
128
+ ### Skip Specific Paths
129
+
130
+ ```js
131
+ app.use(mdefender({
132
+ apiKey: 'your-key',
133
+ domain: 'example.com',
134
+ skipPaths: ['/health', '/api/webhook', '/static/'],
135
+ }));
136
+ ```
137
+
138
+ ### Block on API Error
139
+
140
+ By default, requests are allowed if the WAF API is unreachable. To block instead:
141
+
142
+ ```js
143
+ app.use(mdefender({
144
+ apiKey: 'your-key',
145
+ domain: 'example.com',
146
+ onError: 'block',
147
+ }));
148
+ ```
149
+
150
+ ### Accessing Analysis Results
151
+
152
+ After the middleware processes a request, analysis data is attached to `req.mdefender`:
153
+
154
+ ```js
155
+ app.get('/dashboard', (req, res) => {
156
+ if (req.mdefender) {
157
+ console.log('Threat score:', req.mdefender.threat_score);
158
+ console.log('Request ID:', req.mdefender.request_id);
159
+ }
160
+ res.send('Dashboard');
161
+ });
162
+ ```
163
+
164
+ ## API Reference
165
+
166
+ ### `mdefender(config?)`
167
+
168
+ Returns an Express middleware function.
169
+
170
+ **Parameters:**
171
+ - `config` *(optional)* - `MDefenderConfig` object. Options merge with file-based config and defaults.
172
+
173
+ **Returns:** `Express middleware function`
174
+
175
+ ### `mdefender.loadConfig(overrides?)`
176
+
177
+ Utility to load configuration from file sources with optional overrides.
178
+
179
+ ### `mdefender.DEFAULT_CONFIG`
180
+
181
+ The default configuration object.
182
+
183
+ ## How It Works
184
+
185
+ 1. A request hits your Express app
186
+ 2. MDefender intercepts it and builds a payload (method, URL, headers, body, IP, etc.)
187
+ 3. The payload is sent to the MDefender Pro API for analysis
188
+ 4. If the API detects a threat, it returns a `blocked` status with attack details
189
+ 5. MDefender renders a block page and responds with the configured status code
190
+ 6. If safe, the request continues to your route handler
191
+
192
+ ## License
193
+
194
+ MIT
@@ -0,0 +1,35 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+
6
+ function findConfig(startDir) {
7
+ const configs = [
8
+ 'mdefender.config.js',
9
+ 'mdefender.config.cjs',
10
+ 'mdefender.json',
11
+ ];
12
+
13
+ let dir = startDir || process.cwd();
14
+
15
+ while (dir !== path.dirname(dir)) {
16
+ for (const name of configs) {
17
+ const fp = path.join(dir, name);
18
+ if (fs.existsSync(fp)) return fp;
19
+ }
20
+ dir = path.dirname(dir);
21
+ }
22
+ return null;
23
+ }
24
+
25
+ function load(startDir) {
26
+ const configPath = findConfig(startDir);
27
+ if (!configPath) return null;
28
+
29
+ if (configPath.endsWith('.json')) {
30
+ return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
31
+ }
32
+ return require(configPath);
33
+ }
34
+
35
+ module.exports = { findConfig, load };
package/index.d.ts ADDED
@@ -0,0 +1,37 @@
1
+ import { Request, Response, NextFunction } from 'express';
2
+
3
+ interface MDefenderConfig {
4
+ apiKey?: string;
5
+ domain?: string;
6
+ apiEndpoint?: string;
7
+ mode?: 'block' | 'monitor' | 'off';
8
+ blockStatusCode?: number;
9
+ timeout?: number;
10
+ maxBodySize?: number;
11
+ logBlocked?: boolean;
12
+ customBlockPage?: string | null;
13
+ skipPaths?: string[];
14
+ skipUserAgents?: string[];
15
+ skipMethods?: string[];
16
+ headers?: boolean;
17
+ onError?: 'allow' | 'block';
18
+ }
19
+
20
+ interface MDefenderRequest extends Request {
21
+ mdefender?: {
22
+ status: string;
23
+ threat_score: number;
24
+ request_id: string | null;
25
+ };
26
+ }
27
+
28
+ type MDefenderMiddleware = (req: MDefenderRequest, res: Response, next: NextFunction) => void;
29
+
30
+ declare function mdefender(config?: MDefenderConfig): MDefenderMiddleware;
31
+
32
+ declare namespace mdefender {
33
+ export function loadConfig(overrides?: Partial<MDefenderConfig>): MDefenderConfig;
34
+ export const DEFAULT_CONFIG: MDefenderConfig;
35
+ }
36
+
37
+ export = mdefender;
package/index.js ADDED
@@ -0,0 +1,284 @@
1
+ 'use strict';
2
+
3
+ const axios = require('axios');
4
+ const path = require('path');
5
+ const fs = require('fs');
6
+ const crypto = require('crypto');
7
+
8
+ const DEFAULT_CONFIG = {
9
+ apiKey: '',
10
+ domain: '',
11
+ apiEndpoint: 'https://mdefender-pro.onrender.com',
12
+ mode: 'block', // 'block' | 'monitor' | 'off'
13
+ blockStatusCode: 403,
14
+ timeout: 5000,
15
+ maxBodySize: 1024 * 1024, // 1MB
16
+ logBlocked: true,
17
+ customBlockPage: null, // path to custom HTML file
18
+ skipPaths: ['/health', '/favicon.ico'],
19
+ skipUserAgents: [],
20
+ skipMethods: [],
21
+ headers: true, // forward original headers
22
+ onError: 'allow', // 'allow' | 'block' - what to do if API is unreachable
23
+ };
24
+
25
+ function loadConfig(overrides = {}) {
26
+ let fileConfig = {};
27
+
28
+ // Try mdefender.config.js
29
+ const jsPath = path.resolve(process.cwd(), 'mdefender.config.js');
30
+ if (fs.existsSync(jsPath)) {
31
+ fileConfig = require(jsPath);
32
+ }
33
+
34
+ // Try mdefender.json
35
+ const jsonPath = path.resolve(process.cwd(), 'mdefender.json');
36
+ if (!fileConfig.apiKey && fs.existsSync(jsonPath)) {
37
+ fileConfig = JSON.parse(fs.readFileSync(jsonPath, 'utf-8'));
38
+ }
39
+
40
+ // Try package.json "mdefender" key
41
+ if (!fileConfig.apiKey) {
42
+ try {
43
+ const pkg = require(path.resolve(process.cwd(), 'package.json'));
44
+ if (pkg.mdefender) fileConfig = pkg.mdefender;
45
+ } catch (e) {}
46
+ }
47
+
48
+ return { ...DEFAULT_CONFIG, ...fileConfig, ...overrides };
49
+ }
50
+
51
+ function getClientIP(req) {
52
+ return req.headers['x-forwarded-for']?.split(',')[0]?.trim()
53
+ || req.headers['x-real-ip']
54
+ || req.connection?.remoteAddress
55
+ || req.socket?.remoteAddress
56
+ || 'unknown';
57
+ }
58
+
59
+ function buildPayload(req, config) {
60
+ const ip = getClientIP(req);
61
+ const originalUrl = req.originalUrl || req.url;
62
+ const parsedUrl = new URL(originalUrl, `http://${req.headers.host || 'localhost'}`);
63
+
64
+ const payload = {
65
+ domain: config.domain,
66
+ method: req.method,
67
+ url: parsedUrl.pathname,
68
+ query_string: parsedUrl.search || '',
69
+ query_params: Object.fromEntries(parsedUrl.searchParams),
70
+ ip: ip,
71
+ headers: config.headers ? req.headers : {},
72
+ user_agent: req.headers['user-agent'] || '',
73
+ referer: req.headers['referer'] || req.headers['referrer'] || '',
74
+ content_type: req.headers['content-type'] || '',
75
+ body: '',
76
+ body_fields: {},
77
+ body_field_values: '',
78
+ timestamp: new Date().toISOString(),
79
+ };
80
+
81
+ return payload;
82
+ }
83
+
84
+ function readBody(req) {
85
+ return new Promise((resolve) => {
86
+ if (req.body) {
87
+ if (typeof req.body === 'string') return resolve(req.body);
88
+ if (typeof req.body === 'object') return resolve(JSON.stringify(req.body));
89
+ return resolve(String(req.body));
90
+ }
91
+
92
+ let body = '';
93
+ const maxBytes = 1024 * 1024;
94
+ let bytesRead = 0;
95
+
96
+ const onData = (chunk) => {
97
+ bytesRead += chunk.length;
98
+ if (bytesRead > maxBytes) {
99
+ req.removeListener('data', onData);
100
+ req.removeListener('end', onEnd);
101
+ resolve(body);
102
+ return;
103
+ }
104
+ body += chunk.toString();
105
+ };
106
+
107
+ const onEnd = () => {
108
+ req.removeListener('data', onData);
109
+ resolve(body);
110
+ };
111
+
112
+ req.on('data', onData);
113
+ req.on('end', onEnd);
114
+
115
+ // If stream already ended
116
+ if (req.readableEnded || req.complete) {
117
+ req.removeListener('data', onData);
118
+ req.removeListener('end', onEnd);
119
+ resolve(body);
120
+ }
121
+ });
122
+ }
123
+
124
+ function parseBody(body, contentType) {
125
+ if (!body) return { fields: {}, values: '' };
126
+
127
+ try {
128
+ if (contentType && contentType.includes('application/json')) {
129
+ const parsed = JSON.parse(body);
130
+ return { fields: parsed, values: Object.values(parsed).join(' ') };
131
+ }
132
+ if (contentType && contentType.includes('application/x-www-form-urlencoded')) {
133
+ const params = new URLSearchParams(body);
134
+ const fields = Object.fromEntries(params);
135
+ return { fields, values: Object.values(fields).join(' ') };
136
+ }
137
+ } catch (e) {}
138
+
139
+ return { fields: {}, values: body };
140
+ }
141
+
142
+ function renderBlockPage(config, result) {
143
+ if (config.customBlockPage && fs.existsSync(config.customBlockPage)) {
144
+ return fs.readFileSync(config.customBlockPage, 'utf-8');
145
+ }
146
+
147
+ const attackType = result.attack_type || 'Unknown';
148
+ const confidence = result.confidence || 0;
149
+ const referenceId = result.reference_id || crypto.randomUUID();
150
+
151
+ return `<!DOCTYPE html>
152
+ <html lang="en">
153
+ <head>
154
+ <meta charset="UTF-8">
155
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
156
+ <title>Access Blocked - MDefender Pro</title>
157
+ <style>
158
+ * { margin: 0; padding: 0; box-sizing: border-box; }
159
+ body { min-height: 100vh; display: flex; align-items: center; justify-content: center;
160
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
161
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
162
+ color: #fff; }
163
+ .container { text-align: center; padding: 40px; max-width: 600px; }
164
+ .icon { font-size: 80px; margin-bottom: 20px; opacity: 0.9; }
165
+ h1 { font-size: 28px; margin-bottom: 12px; font-weight: 700; }
166
+ .subtitle { font-size: 16px; color: rgba(255,255,255,0.7); margin-bottom: 30px; }
167
+ .details { background: rgba(255,255,255,0.08); border-radius: 12px; padding: 20px;
168
+ text-align: left; margin-bottom: 30px; backdrop-filter: blur(10px); }
169
+ .details p { font-size: 14px; color: rgba(255,255,255,0.8); margin: 8px 0; }
170
+ .details strong { color: #e74c3c; }
171
+ .footer { font-size: 12px; color: rgba(255,255,255,0.4); }
172
+ </style>
173
+ </head>
174
+ <body>
175
+ <div class="container">
176
+ <div class="icon">&#x1F6AB;</div>
177
+ <h1>Access Blocked</h1>
178
+ <p class="subtitle">This request has been blocked by MDefender Pro WAF</p>
179
+ <div class="details">
180
+ <p><strong>Attack Type:</strong> ${attackType}</p>
181
+ <p><strong>Confidence:</strong> ${(confidence * 100).toFixed(1)}%</p>
182
+ <p><strong>Reference ID:</strong> ${referenceId}</p>
183
+ <p><strong>Time:</strong> ${new Date().toISOString()}</p>
184
+ </div>
185
+ <p class="footer">MDefender Pro - Web Application Firewall</p>
186
+ </div>
187
+ </body>
188
+ </html>`;
189
+ }
190
+
191
+ function mdefender(overrides = {}) {
192
+ const config = loadConfig(overrides);
193
+
194
+ if (!config.apiKey) {
195
+ console.error('[MDefender] ERROR: No API key provided. Set it in mdefender.config.js or pass it as an option.');
196
+ return (req, res, next) => next();
197
+ }
198
+
199
+ if (config.mode === 'off') {
200
+ return (req, res, next) => next();
201
+ }
202
+
203
+ const client = axios.create({
204
+ baseURL: config.apiEndpoint,
205
+ timeout: config.timeout,
206
+ headers: {
207
+ 'Authorization': `Bearer ${config.apiKey}`,
208
+ 'Content-Type': 'application/json',
209
+ 'X-MDefender-Version': '1.0.0',
210
+ },
211
+ });
212
+
213
+ return async function mdefenderMiddleware(req, res, next) {
214
+ // Skip certain paths
215
+ if (config.skipPaths.some(p => req.url.startsWith(p))) return next();
216
+
217
+ // Skip certain methods
218
+ if (config.skipMethods.includes(req.method)) return next();
219
+
220
+ // Skip certain user agents
221
+ const ua = req.headers['user-agent'] || '';
222
+ if (config.skipUserAgents.some(s => ua.toLowerCase().includes(s.toLowerCase()))) return next();
223
+
224
+ try {
225
+ // Read and parse request body
226
+ const rawBody = await readBody(req);
227
+ const contentType = req.headers['content-type'] || '';
228
+ const { fields, values } = parseBody(rawBody, contentType);
229
+
230
+ // Build payload
231
+ const payload = buildPayload(req, config);
232
+ payload.body = rawBody;
233
+ payload.body_fields = fields;
234
+ payload.body_field_values = values;
235
+
236
+ // Send to MDefender API
237
+ const response = await client.post('/api/analyze', {
238
+ request: payload,
239
+ domain: config.domain,
240
+ });
241
+
242
+ const result = response.data;
243
+
244
+ if (result.status === 'blocked') {
245
+ if (config.logBlocked) {
246
+ console.log(`[MDefender] BLOCKED ${req.method} ${req.url} - ${result.attack_type} (${(result.confidence * 100).toFixed(1)}%)`);
247
+ }
248
+
249
+ const blockPage = result.block_page || renderBlockPage(config, result);
250
+ res.writeHead(config.blockStatusCode, {
251
+ 'Content-Type': 'text/html; charset=utf-8',
252
+ 'X-MDefender-Status': 'blocked',
253
+ 'X-MDefender-Attack-Type': result.attack_type || 'unknown',
254
+ });
255
+ return res.end(blockPage);
256
+ }
257
+
258
+ // Request is safe - attach result to request for downstream use
259
+ req.mdefender = {
260
+ status: 'allowed',
261
+ threat_score: result.threat_score || 0,
262
+ request_id: result.request_id || null,
263
+ };
264
+
265
+ return next();
266
+
267
+ } catch (error) {
268
+ console.error(`[MDefender] API Error: ${error.message}`);
269
+
270
+ if (config.onError === 'block') {
271
+ res.writeHead(503, { 'Content-Type': 'text/html' });
272
+ return res.end('<html><body><h1>Service Temporarily Unavailable</h1><p>WAF service is currently unreachable. Please try again later.</p></body></html>');
273
+ }
274
+
275
+ // Default: allow request on error
276
+ return next();
277
+ }
278
+ };
279
+ }
280
+
281
+ mdefender.loadConfig = loadConfig;
282
+ mdefender.DEFAULT_CONFIG = DEFAULT_CONFIG;
283
+
284
+ module.exports = mdefender;
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "mdefender-pro",
3
+ "version": "1.1.0",
4
+ "description": "MDefender Pro - Web Application Firewall middleware for Node.js/Express. Protects against XSS, SQLi, CSRF, and other OWASP Top 10 attacks.",
5
+ "main": "index.js",
6
+ "types": "index.d.ts",
7
+ "keywords": [
8
+ "waf",
9
+ "web-application-firewall",
10
+ "security",
11
+ "firewall",
12
+ "xss",
13
+ "sqli",
14
+ "csrf",
15
+ "protection",
16
+ "middleware",
17
+ "express",
18
+ "nodejs",
19
+ "owasp"
20
+ ],
21
+ "author": "MDefender Pro",
22
+ "license": "MIT",
23
+ "engines": {
24
+ "node": ">=14.0.0"
25
+ },
26
+ "dependencies": {
27
+ "axios": "^1.6.0"
28
+ },
29
+ "peerDependencies": {
30
+ "express": ">=4.0.0"
31
+ },
32
+ "files": [
33
+ "index.js",
34
+ "index.d.ts",
35
+ "README.md",
36
+ "config-loader.js"
37
+ ],
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "https://github.com/mdefender/mdefender"
41
+ },
42
+ "homepage": "https://mdefender-pro-6e3r.onrender.com",
43
+ "bugs": {
44
+ "url": "https://github.com/mdefender/mdefender/issues"
45
+ }
46
+ }