@mcp-guardian/server 1.0.1 → 1.2.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.
@@ -3,84 +3,256 @@ import { readFileSync } from 'fs';
3
3
  import { resolve, dirname } from 'path';
4
4
  import { fileURLToPath } from 'url';
5
5
  import { Logger } from './logger.js';
6
+ import { DashboardAuth } from '../auth/dashboard-auth.js';
6
7
  const __filename = fileURLToPath(import.meta.url);
7
8
  const __dirname = dirname(__filename);
8
9
  /**
9
10
  * Lightweight dashboard server that serves:
10
- * - / — the dashboard HTML
11
- * - /api/policycurrent policy (JSON)
12
- * - /api/policy/reloadtrigger policy reload
13
- * - /metricsPrometheus metrics
11
+ * - / — the dashboard HTML (requires auth if enabled)
12
+ * - /loginlogin page (when JWT auth is enabled)
13
+ * - /api/loginPOST login endpoint
14
+ * - /api/policycurrent policy (JSON, requires auth)
15
+ * - /api/policy/reload — trigger policy reload (requires auth)
16
+ * - /metrics — Prometheus metrics (can be auth-gated or public via DASHBOARD_METRICS_PUBLIC=true)
17
+ *
18
+ * v1.2: Integrated DashboardAuth for JWT/API key authentication, CSRF protection.
14
19
  */
15
- export async function startDashboardServer(port = 4000, policyWatcher) {
20
+ export async function startDashboardServer(port = 4000, policyWatcher, dashboardAuth) {
16
21
  if (process.env['DASHBOARD_ENABLED'] !== 'true') {
17
22
  Logger.debug('[dashboard] Dashboard server not enabled (set DASHBOARD_ENABLED=true)');
18
- return;
23
+ return { auth: dashboardAuth || new DashboardAuth({ enabled: false }), server: createServer((_req, res) => {
24
+ res.writeHead(200);
25
+ res.end();
26
+ }) };
27
+ }
28
+ const auth = dashboardAuth || new DashboardAuth();
29
+ const authEnabled = auth.isEnabled();
30
+ if (authEnabled) {
31
+ Logger.info('[dashboard] Dashboard authentication enabled');
32
+ }
33
+ else {
34
+ Logger.info('[dashboard] Dashboard running without authentication (set DASHBOARD_AUTH_ENABLED=true)');
19
35
  }
20
36
  const dashboardHtml = readFileSync(resolve(__dirname, '..', '..', 'deploy', 'dashboard.html'), 'utf-8');
37
+ /** Read JSON body from request */
38
+ async function readBody(req) {
39
+ return new Promise((resolve, reject) => {
40
+ let data = '';
41
+ req.on('data', (chunk) => { data += chunk.toString(); });
42
+ req.on('end', () => {
43
+ try {
44
+ resolve(data ? JSON.parse(data) : {});
45
+ }
46
+ catch {
47
+ resolve({});
48
+ }
49
+ });
50
+ req.on('error', reject);
51
+ });
52
+ }
53
+ /** Parse form-encoded body from request */
54
+ async function readFormBody(req) {
55
+ return new Promise((resolve) => {
56
+ let data = '';
57
+ req.on('data', (chunk) => { data += chunk.toString(); });
58
+ req.on('end', () => {
59
+ const result = {};
60
+ if (data) {
61
+ try {
62
+ const params = new URLSearchParams(data);
63
+ for (const [key, value] of params) {
64
+ result[key] = value;
65
+ }
66
+ }
67
+ catch {
68
+ // Ignore parse errors
69
+ }
70
+ }
71
+ resolve(result);
72
+ });
73
+ });
74
+ }
75
+ /** Get client IP for rate limiting */
76
+ function getClientIp(req) {
77
+ const forwarded = req.headers['x-forwarded-for'];
78
+ if (forwarded) {
79
+ const first = Array.isArray(forwarded) ? forwarded[0] : forwarded.split(',')[0];
80
+ return (first || '').trim();
81
+ }
82
+ return req.socket?.remoteAddress || 'unknown';
83
+ }
84
+ /** Write JSON response */
85
+ function writeJson(res, status, data) {
86
+ res.writeHead(status, { 'Content-Type': 'application/json' });
87
+ res.end(JSON.stringify(data));
88
+ }
21
89
  const server = createServer(async (req, res) => {
22
90
  const url = req.url || '/';
91
+ const method = req.method || 'GET';
92
+ // ── CORS preflight ─────────────────────────────────────
93
+ if (method === 'OPTIONS') {
94
+ res.writeHead(204, {
95
+ 'Access-Control-Allow-Origin': '*',
96
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
97
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-API-Key',
98
+ });
99
+ res.end();
100
+ return;
101
+ }
102
+ // ── CORS headers on all responses ─────────────────────
103
+ const setCors = () => {
104
+ res.setHeader('Access-Control-Allow-Origin', '*');
105
+ };
23
106
  try {
24
- // ── Dashboard HTML ──────────────────────────────────────
107
+ // ── Login page (only when JWT auth is enabled, no API key set) ──
108
+ if (url === '/login' && method === 'GET') {
109
+ setCors();
110
+ if (auth.isEnabled() && auth.hasJwtSessionAuth()) {
111
+ res.writeHead(200, { 'Content-Type': 'text/html' });
112
+ res.end(auth.getLoginPageHtml());
113
+ }
114
+ else {
115
+ res.writeHead(302, { 'Location': '/' });
116
+ res.end();
117
+ }
118
+ return;
119
+ }
120
+ // ── Login API endpoint ──────────────────────────────
121
+ if (url === '/api/login' && method === 'POST') {
122
+ setCors();
123
+ const ip = getClientIp(req);
124
+ const contentType = req.headers['content-type'] || '';
125
+ let body;
126
+ if (contentType.includes('application/x-www-form-urlencoded')) {
127
+ body = await readFormBody(req);
128
+ }
129
+ else {
130
+ body = await readBody(req);
131
+ }
132
+ const result = auth.login({
133
+ url,
134
+ headers: req.headers,
135
+ body: {
136
+ username: body.username,
137
+ password: body.password,
138
+ api_key: body.api_key,
139
+ },
140
+ ip,
141
+ });
142
+ if (result.success && req.headers['content-type']?.includes('form')) {
143
+ // Form submission — redirect to dashboard with token
144
+ res.writeHead(302, {
145
+ 'Location': `/?api_key=${encodeURIComponent(result.token)}`,
146
+ 'Set-Cookie': `mcp_guardian_session=${result.token}; Path=/; HttpOnly; SameSite=Strict; Max-Age=3600`,
147
+ });
148
+ res.end();
149
+ return;
150
+ }
151
+ if (result.success) {
152
+ writeJson(res, 200, { success: true, token: result.token });
153
+ }
154
+ else {
155
+ writeJson(res, 401, { success: false, error: result.error });
156
+ }
157
+ return;
158
+ }
159
+ // ── Auth check for all other routes ─────────────────
160
+ const authResult = auth.authenticate({ url, headers: req.headers, method });
161
+ if (!authResult.authenticated) {
162
+ setCors();
163
+ if (req.headers['accept']?.includes('text/html')) {
164
+ // Browser request — redirect to login
165
+ res.writeHead(302, { 'Location': '/login' });
166
+ res.end();
167
+ }
168
+ else {
169
+ writeJson(res, 401, { error: 'Authentication required', reason: authResult.reason });
170
+ }
171
+ return;
172
+ }
173
+ // ── Dashboard HTML ──────────────────────────────────
25
174
  if (url === '/' || url === '/dashboard.html') {
175
+ setCors();
26
176
  res.writeHead(200, { 'Content-Type': 'text/html' });
27
177
  res.end(dashboardHtml);
28
178
  return;
29
179
  }
30
- // ── Policy API ──────────────────────────────────────────
31
- if (url === '/api/policy' && req.method === 'GET') {
180
+ // ── Policy API ──────────────────────────────────────
181
+ if (url === '/api/policy' && method === 'GET') {
182
+ setCors();
32
183
  if (!policyWatcher || !policyWatcher.get()) {
33
- res.writeHead(404, { 'Content-Type': 'application/json' });
34
- res.end(JSON.stringify({ error: 'No active policy. Start proxy with --policy flag.' }));
184
+ writeJson(res, 404, { error: 'No active policy. Start proxy with --policy flag.' });
35
185
  return;
36
186
  }
37
- res.writeHead(200, { 'Content-Type': 'application/json' });
38
- res.end(JSON.stringify({ mode: policyWatcher.get().getMode(), rules: 'Policy engine active (YAML view available on filesystem)' }));
187
+ writeJson(res, 200, { mode: policyWatcher.get().getMode(), rules: 'Policy engine active (YAML view available on filesystem)' });
39
188
  return;
40
189
  }
41
- if (url === '/api/policy/reload' && req.method === 'POST') {
190
+ if (url === '/api/policy/reload' && method === 'POST') {
191
+ setCors();
42
192
  if (!policyWatcher) {
43
- res.writeHead(404, { 'Content-Type': 'application/json' });
44
- res.end(JSON.stringify({ error: 'Policy watcher not configured' }));
193
+ writeJson(res, 404, { error: 'Policy watcher not configured' });
45
194
  return;
46
195
  }
47
- // PolicyWatcher auto-reloads via chokidar no manual reload needed
48
- res.writeHead(200, { 'Content-Type': 'application/json' });
49
- res.end(JSON.stringify({ status: 'ok', message: 'Policy watcher is active. File changes are auto-detected.' }));
196
+ writeJson(res, 200, { status: 'ok', message: 'Policy watcher is active. File changes are auto-detected.' });
50
197
  return;
51
198
  }
52
- // ── Prometheus /metrics proxy ──────────────────────────
199
+ // ── Prometheus /metrics proxy ──────────────────────
53
200
  if (url === '/metrics') {
54
- try {
55
- // Fetch from the metrics server (port 9090 by default)
56
- const metricsPort = process.env['METRICS_PORT'] || '9090';
57
- const metricsRes = await fetch(`http://localhost:${metricsPort}/metrics`);
58
- if (!metricsRes.ok)
59
- throw new Error(`Metrics server returned ${metricsRes.status}`);
60
- const text = await metricsRes.text();
61
- res.writeHead(200, {
62
- 'Content-Type': 'text/plain; version=0.0.4; charset=utf-8',
63
- 'Access-Control-Allow-Origin': '*',
64
- });
65
- res.end(text);
201
+ setCors();
202
+ const metricsPublic = process.env['DASHBOARD_METRICS_PUBLIC'] === 'true';
203
+ // Re-check auth for metrics unless public
204
+ if (metricsPublic || authResult.authenticated) {
205
+ try {
206
+ const metricsPort = process.env['METRICS_PORT'] || '9090';
207
+ const metricsRes = await fetch(`http://localhost:${metricsPort}/metrics`);
208
+ if (!metricsRes.ok)
209
+ throw new Error(`Metrics server returned ${metricsRes.status}`);
210
+ const text = await metricsRes.text();
211
+ res.writeHead(200, {
212
+ 'Content-Type': 'text/plain; version=0.0.4; charset=utf-8',
213
+ 'Access-Control-Allow-Origin': '*',
214
+ });
215
+ res.end(text);
216
+ }
217
+ catch {
218
+ writeJson(res, 200, { error: 'Metrics not available. Ensure METRICS_ENABLED=true and proxy is running.' });
219
+ }
66
220
  }
67
- catch {
68
- res.writeHead(200, { 'Content-Type': 'application/json' });
69
- res.end(JSON.stringify({ error: 'Metrics not available. Ensure METRICS_ENABLED=true and proxy is running.' }));
221
+ else {
222
+ writeJson(res, 401, { error: 'Authentication required for metrics' });
223
+ }
224
+ return;
225
+ }
226
+ // ── Auth status check ───────────────────────────────
227
+ if (url === '/api/auth/status' && method === 'GET') {
228
+ setCors();
229
+ writeJson(res, 200, { authenticated: true, identity: authResult.identity, authEnabled });
230
+ return;
231
+ }
232
+ // ── Logout ──────────────────────────────────────────
233
+ if (url === '/api/logout' && method === 'POST') {
234
+ setCors();
235
+ const authHeader = req.headers['authorization'];
236
+ if (authHeader) {
237
+ const match = authHeader.match(/^Bearer\s+(.+)$/i);
238
+ if (match)
239
+ auth.logout(match[1]);
70
240
  }
241
+ writeJson(res, 200, { status: 'ok', message: 'Logged out' });
71
242
  return;
72
243
  }
73
- // ── 404 ─────────────────────────────────────────────────
74
- res.writeHead(404, { 'Content-Type': 'application/json' });
75
- res.end(JSON.stringify({ error: 'Not found' }));
244
+ // ── 404 ──────────────────────────────────────────────
245
+ setCors();
246
+ writeJson(res, 404, { error: 'Not found' });
76
247
  }
77
248
  catch (err) {
78
- res.writeHead(500, { 'Content-Type': 'application/json' });
79
- res.end(JSON.stringify({ error: err?.message || 'Internal error' }));
249
+ setCors();
250
+ writeJson(res, 500, { error: err?.message || 'Internal error' });
80
251
  }
81
252
  });
82
253
  server.listen(port, () => {
83
- Logger.info(`[dashboard] Dashboard available at http://localhost:${port}`);
254
+ Logger.info(`[dashboard] Dashboard available at http://localhost:${port}${authEnabled ? ' (auth enabled)' : ''}`);
84
255
  });
256
+ return { auth, server };
85
257
  }
86
258
  //# sourceMappingURL=dashboard-server.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"dashboard-server.js","sourceRoot":"","sources":["../../src/utils/dashboard-server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"dashboard-server.js","sourceRoot":"","sources":["../../src/utils/dashboard-server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,62 @@
1
+ export interface NormalizationResult {
2
+ /** The fully normalized string ready for policy evaluation */
3
+ normalized: string;
4
+ /** Whether any normalization was applied */
5
+ wasModified: boolean;
6
+ /** What transformations were applied */
7
+ transformations: string[];
8
+ /** The original raw input */
9
+ original: string;
10
+ }
11
+ /**
12
+ * PayloadNormalizer applies multi-stage normalization to defeat
13
+ * common evasion techniques targeting regex-based policy engines.
14
+ */
15
+ export declare class PayloadNormalizer {
16
+ private readonly maxDepth;
17
+ private readonly maxLength;
18
+ constructor(maxDepth?: number, maxLength?: number);
19
+ /**
20
+ * Full normalization pipeline for policy evaluation input.
21
+ */
22
+ normalize(input: string): NormalizationResult;
23
+ /**
24
+ * URL decode: %XX → character, handles malformed sequences.
25
+ */
26
+ private urlDecode;
27
+ /**
28
+ * Decode hex escapes: \x41 → 'A', \x00 → null byte detection.
29
+ */
30
+ private decodeHexEscapes;
31
+ /**
32
+ * Decode unicode escapes: \u0041 → 'A', \U00000041 → 'A'.
33
+ */
34
+ private decodeUnicodeEscapes;
35
+ /**
36
+ * Decode HTML entities: < -> <, &#60; -> <, &#x3C; -> <.
37
+ * Entity map built at runtime to avoid source-level entity decoding issues.
38
+ */
39
+ private static htmlEntityMap;
40
+ private static getHtmlEntityMap;
41
+ private decodeHtmlEntities;
42
+ /**
43
+ * Unwrap double escapes: \\. → literal character.
44
+ */
45
+ private unwrapDoubleEscapes;
46
+ /**
47
+ * Shell normalize: collapse common shell obfuscation patterns.
48
+ *
49
+ * - $'cmd' → cmd (ANSI-C quoting)
50
+ * - "c"m"d" → cmd (quote splitting)
51
+ * - ''cmd'' → cmd (empty quote pairs)
52
+ * - c\md → cmd (backslash escapes)
53
+ */
54
+ private shellNormalize;
55
+ /**
56
+ * Specifically normalize a JSON string value (tool argument).
57
+ * Handles nested JSON structures recursively.
58
+ */
59
+ normalizeJsonValue(value: unknown, depth?: number): unknown;
60
+ }
61
+ export declare function getNormalizer(): PayloadNormalizer;
62
+ //# sourceMappingURL=payload-normalizer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"payload-normalizer.d.ts","sourceRoot":"","sources":["../../src/utils/payload-normalizer.ts"],"names":[],"mappings":"AAUA,MAAM,WAAW,mBAAmB;IAClC,8DAA8D;IAC9D,UAAU,EAAE,MAAM,CAAC;IACnB,4CAA4C;IAC5C,WAAW,EAAE,OAAO,CAAC;IACrB,wCAAwC;IACxC,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,6BAA6B;IAC7B,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;GAGG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;gBAEvB,QAAQ,SAAI,EAAE,SAAS,SAAY;IAK/C;;OAEG;IACH,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,mBAAmB;IAmE7C;;OAEG;IACH,OAAO,CAAC,SAAS;IAejB;;OAEG;IACH,OAAO,CAAC,gBAAgB;IASxB;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAoB5B;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,aAAa,CAAwC;IAEpE,OAAO,CAAC,MAAM,CAAC,gBAAgB;IAiC/B,OAAO,CAAC,kBAAkB;IAmB1B;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAQ3B;;;;;;;OAOG;IACH,OAAO,CAAC,cAAc;IAkBtB;;;OAGG;IACH,kBAAkB,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,SAAI,GAAG,OAAO;CAqBvD;AAKD,wBAAgB,aAAa,IAAI,iBAAiB,CAKjD"}
@@ -0,0 +1,240 @@
1
+ /**
2
+ * PayloadNormalizer applies multi-stage normalization to defeat
3
+ * common evasion techniques targeting regex-based policy engines.
4
+ */
5
+ export class PayloadNormalizer {
6
+ maxDepth;
7
+ maxLength;
8
+ constructor(maxDepth = 5, maxLength = 1_000_000) {
9
+ this.maxDepth = maxDepth;
10
+ this.maxLength = maxLength;
11
+ }
12
+ /**
13
+ * Full normalization pipeline for policy evaluation input.
14
+ */
15
+ normalize(input) {
16
+ const transformations = [];
17
+ let current = input;
18
+ let depth = 0;
19
+ // ── Step 0: Truncate oversized inputs (memory safety) ──
20
+ if (current.length > this.maxLength) {
21
+ current = current.slice(0, this.maxLength);
22
+ transformations.push('truncated');
23
+ }
24
+ // ── Step 1: Unicode normalization (NFKC) — collapses homoglyphs ──
25
+ const unicodeNormalized = current.normalize('NFKC');
26
+ if (unicodeNormalized !== current) {
27
+ transformations.push('unicode-nfkc');
28
+ current = unicodeNormalized;
29
+ }
30
+ // ── Step 2: Iterative decode loop (URL, hex, HTML entities) ──
31
+ while (depth < this.maxDepth) {
32
+ const before = current;
33
+ // URL decode (handles %20, %00 null bytes, %2F slashes)
34
+ current = this.urlDecode(current);
35
+ // Hex escape decode (\x41, \x00, \x2F)
36
+ current = this.decodeHexEscapes(current);
37
+ // Unicode escape decode (\u0041, \U00000041)
38
+ current = this.decodeUnicodeEscapes(current);
39
+ // HTML entity decode (<, &#60;, &#x3C;)
40
+ current = this.decodeHtmlEntities(current);
41
+ // Double-backslash unwrap (\\. → .)
42
+ current = this.unwrapDoubleEscapes(current);
43
+ if (current === before)
44
+ break;
45
+ depth++;
46
+ }
47
+ if (current !== unicodeNormalized) {
48
+ transformations.push('decode-loop');
49
+ }
50
+ // ── Step 3: Shell normalization ──
51
+ const shellNormalized = this.shellNormalize(current);
52
+ if (shellNormalized !== current) {
53
+ transformations.push('shell-normalize');
54
+ current = shellNormalized;
55
+ }
56
+ // ── Step 4: Whitespace normalization (collapse runs) ──
57
+ const whitespaceNormalized = current.replace(/\s+/g, ' ').trim();
58
+ if (whitespaceNormalized !== current) {
59
+ transformations.push('whitespace');
60
+ current = whitespaceNormalized;
61
+ }
62
+ return {
63
+ normalized: current,
64
+ wasModified: transformations.length > 0,
65
+ transformations,
66
+ original: input,
67
+ };
68
+ }
69
+ /**
70
+ * URL decode: %XX → character, handles malformed sequences.
71
+ */
72
+ urlDecode(input) {
73
+ try {
74
+ return decodeURIComponent(input.replace(/\+/g, ' '));
75
+ }
76
+ catch {
77
+ // Gracefully handle malformed % sequences: replace only valid ones
78
+ return input.replace(/%([0-9A-Fa-f]{2})/g, (_match, hex) => {
79
+ try {
80
+ return String.fromCharCode(parseInt(hex, 16));
81
+ }
82
+ catch {
83
+ return _match;
84
+ }
85
+ });
86
+ }
87
+ }
88
+ /**
89
+ * Decode hex escapes: \x41 → 'A', \x00 → null byte detection.
90
+ */
91
+ decodeHexEscapes(input) {
92
+ return input.replace(/\\x([0-9A-Fa-f]{2})/g, (_match, hex) => {
93
+ const code = parseInt(hex, 16);
94
+ // Preserve null byte as marker for detection
95
+ if (code === 0)
96
+ return '\0';
97
+ return String.fromCharCode(code);
98
+ });
99
+ }
100
+ /**
101
+ * Decode unicode escapes: \u0041 → 'A', \U00000041 → 'A'.
102
+ */
103
+ decodeUnicodeEscapes(input) {
104
+ return input
105
+ .replace(/\\u([0-9A-Fa-f]{4})/g, (_match, hex) => {
106
+ try {
107
+ return String.fromCharCode(parseInt(hex, 16));
108
+ }
109
+ catch {
110
+ return _match;
111
+ }
112
+ })
113
+ .replace(/\\U([0-9A-Fa-f]{8})/g, (_match, hex) => {
114
+ try {
115
+ const code = parseInt(hex, 16);
116
+ if (code > 0x10ffff)
117
+ return _match; // Invalid unicode
118
+ return String.fromCodePoint(code);
119
+ }
120
+ catch {
121
+ return _match;
122
+ }
123
+ });
124
+ }
125
+ /**
126
+ * Decode HTML entities: < -> <, &#60; -> <, &#x3C; -> <.
127
+ * Entity map built at runtime to avoid source-level entity decoding issues.
128
+ */
129
+ static htmlEntityMap = null;
130
+ static getHtmlEntityMap() {
131
+ if (PayloadNormalizer.htmlEntityMap)
132
+ return PayloadNormalizer.htmlEntityMap;
133
+ const a = String.fromCharCode(38); // ampersand char
134
+ const pairs = [
135
+ [a + 'lt;', '<'],
136
+ [a + 'gt;', '>'],
137
+ [a + 'amp;', a],
138
+ [a + 'quot;', '"'],
139
+ [a + '#39;', "'"],
140
+ [a + 'apos;', "'"],
141
+ [a + 'sol;', '/'],
142
+ [a + 'bsol;', '\\'],
143
+ [a + 'colon;', ':'],
144
+ [a + 'semi;', ';'],
145
+ [a + 'verbar;', '|'],
146
+ [a + 'dollar;', '$'],
147
+ [a + 'lpar;', '('],
148
+ [a + 'rpar;', ')'],
149
+ [a + 'lcub;', '{'],
150
+ [a + 'rcub;', '}'],
151
+ [a + 'lbrack;', '['],
152
+ [a + 'rbrack;', ']'],
153
+ ];
154
+ PayloadNormalizer.htmlEntityMap = pairs.map(([entity, ch]) => {
155
+ const escaped = entity.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
156
+ return [new RegExp(escaped, 'g'), ch];
157
+ });
158
+ return PayloadNormalizer.htmlEntityMap;
159
+ }
160
+ decodeHtmlEntities(input) {
161
+ let result = input;
162
+ // Named entities
163
+ for (const [regex, ch] of PayloadNormalizer.getHtmlEntityMap()) {
164
+ result = result.replace(regex, ch);
165
+ }
166
+ // Numeric decimal entities: &#60;
167
+ result = result.replace(/&#(\d+);/g, (_match, dec) => {
168
+ const code = parseInt(dec, 10);
169
+ return (code > 0 && code < 65536) ? String.fromCharCode(code) : _match;
170
+ });
171
+ // Numeric hex entities: &#x3C;
172
+ result = result.replace(/&#x([0-9A-Fa-f]+);/g, (_match, hex) => {
173
+ const code = parseInt(hex, 16);
174
+ return (code > 0 && code < 65536) ? String.fromCharCode(code) : _match;
175
+ });
176
+ return result;
177
+ }
178
+ /**
179
+ * Unwrap double escapes: \\. → literal character.
180
+ */
181
+ unwrapDoubleEscapes(input) {
182
+ return input.replace(/\\(.)/g, (_match, char) => {
183
+ // Only unwrap if the backslash is escaping a non-special char
184
+ if ('\\$`"\''.includes(char))
185
+ return _match;
186
+ return char;
187
+ });
188
+ }
189
+ /**
190
+ * Shell normalize: collapse common shell obfuscation patterns.
191
+ *
192
+ * - $'cmd' → cmd (ANSI-C quoting)
193
+ * - "c"m"d" → cmd (quote splitting)
194
+ * - ''cmd'' → cmd (empty quote pairs)
195
+ * - c\md → cmd (backslash escapes)
196
+ */
197
+ shellNormalize(input) {
198
+ let result = input;
199
+ // ANSI-C quoting: $'command' → command
200
+ result = result.replace(/\$'([^']*)'/g, '$1');
201
+ // Quote splitting: "a""b" → ab, 'a''b' → ab
202
+ result = result.replace(/["']\s*["']/g, '');
203
+ // Shell backslash escapes on non-special chars
204
+ result = result.replace(/\\([^\\$`"'|&;><~#%{}()\[\]])/g, '$1');
205
+ // Null byte detection (normalized → mark as NUL for policy patterns)
206
+ result = result.replace(/\0/g, '\\0');
207
+ return result;
208
+ }
209
+ /**
210
+ * Specifically normalize a JSON string value (tool argument).
211
+ * Handles nested JSON structures recursively.
212
+ */
213
+ normalizeJsonValue(value, depth = 0) {
214
+ if (depth > 10)
215
+ return value; // Recursion guard
216
+ if (typeof value === 'string') {
217
+ return this.normalize(value).normalized;
218
+ }
219
+ if (Array.isArray(value)) {
220
+ return value.map((item) => this.normalizeJsonValue(item, depth + 1));
221
+ }
222
+ if (value !== null && typeof value === 'object') {
223
+ const result = {};
224
+ for (const [key, val] of Object.entries(value)) {
225
+ result[key] = this.normalizeJsonValue(val, depth + 1);
226
+ }
227
+ return result;
228
+ }
229
+ return value;
230
+ }
231
+ }
232
+ /** Singleton instance for policy engine integration */
233
+ let defaultInstance = null;
234
+ export function getNormalizer() {
235
+ if (!defaultInstance) {
236
+ defaultInstance = new PayloadNormalizer();
237
+ }
238
+ return defaultInstance;
239
+ }
240
+ //# sourceMappingURL=payload-normalizer.js.map