sovr-mcp-proxy 7.0.0 → 7.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.
@@ -0,0 +1,911 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/semanticAnalyzer.ts
21
+ var semanticAnalyzer_exports = {};
22
+ __export(semanticAnalyzer_exports, {
23
+ BUILTIN_RULES: () => BUILTIN_RULES,
24
+ SemanticAnalyzer: () => SemanticAnalyzer,
25
+ analyzeStructure: () => analyzeStructure,
26
+ checkSQLType: () => checkSQLType,
27
+ classifyFormalFinding: () => classifyFormalFinding,
28
+ classifyStructuralFinding: () => classifyStructuralFinding,
29
+ createParanoidAnalyzer: () => createParanoidAnalyzer,
30
+ createSemanticAnalyzer: () => createSemanticAnalyzer,
31
+ findRuleCategoryByName: () => findRuleCategoryByName,
32
+ formalVerify: () => formalVerify,
33
+ runStateMachine: () => runStateMachine,
34
+ structuralRiskAssessment: () => structuralRiskAssessment,
35
+ verifyPathConstraints: () => verifyPathConstraints
36
+ });
37
+ module.exports = __toCommonJS(semanticAnalyzer_exports);
38
+ var BUILTIN_RULES = [
39
+ // ── Data Destruction ──
40
+ {
41
+ id: "destroy-rm-rf",
42
+ name: "Recursive file deletion",
43
+ category: "data_destruction",
44
+ patterns: [
45
+ { type: "regex", value: "rm\\s+(-[a-zA-Z]*r[a-zA-Z]*f|--recursive)\\s", target: "command" },
46
+ { type: "regex", value: "rm\\s+(-[a-zA-Z]*f[a-zA-Z]*r)\\s", target: "command" },
47
+ { type: "keyword", value: "shred", target: "command" },
48
+ { type: "keyword", value: "wipe", target: "command" }
49
+ ],
50
+ riskLevel: "critical",
51
+ priority: 100,
52
+ polarity: "negative"
53
+ },
54
+ {
55
+ id: "destroy-db-drop",
56
+ name: "Database destruction",
57
+ category: "data_destruction",
58
+ patterns: [
59
+ { type: "regex", value: "DROP\\s+(TABLE|DATABASE|SCHEMA|INDEX)", target: "arguments", caseSensitive: false },
60
+ { type: "regex", value: "TRUNCATE\\s+TABLE", target: "arguments", caseSensitive: false },
61
+ { type: "regex", value: "DELETE\\s+FROM\\s+\\w+\\s*$", target: "arguments", caseSensitive: false }
62
+ ],
63
+ riskLevel: "critical",
64
+ priority: 100,
65
+ polarity: "negative"
66
+ },
67
+ {
68
+ id: "destroy-format",
69
+ name: "Disk formatting",
70
+ category: "data_destruction",
71
+ patterns: [
72
+ { type: "regex", value: "mkfs\\.", target: "command" },
73
+ { type: "regex", value: "dd\\s+.*of=/dev/", target: "command" },
74
+ { type: "regex", value: "format\\s+[A-Z]:", target: "command", caseSensitive: false }
75
+ ],
76
+ riskLevel: "critical",
77
+ priority: 100,
78
+ polarity: "negative"
79
+ },
80
+ {
81
+ id: "destroy-find-delete",
82
+ name: "Find and delete (rm -rf equivalent)",
83
+ category: "data_destruction",
84
+ patterns: [
85
+ { type: "regex", value: "find\\s+.*-delete", target: "command" },
86
+ { type: "regex", value: "find\\s+.*-exec\\s+rm", target: "command" },
87
+ { type: "regex", value: "xargs\\s+rm", target: "command" }
88
+ ],
89
+ riskLevel: "dangerous",
90
+ priority: 95,
91
+ polarity: "negative"
92
+ },
93
+ // ── Data Exfiltration ──
94
+ {
95
+ id: "exfil-curl-post",
96
+ name: "Data upload via curl/wget",
97
+ category: "data_exfiltration",
98
+ patterns: [
99
+ { type: "regex", value: "curl\\s+.*(-d|--data|--upload-file|-F|--form)\\s", target: "command" },
100
+ { type: "regex", value: "curl\\s+.*-X\\s*(POST|PUT)", target: "command", caseSensitive: false },
101
+ { type: "regex", value: "wget\\s+.*--post", target: "command" }
102
+ ],
103
+ riskLevel: "suspicious",
104
+ priority: 80,
105
+ polarity: "negative"
106
+ },
107
+ {
108
+ id: "exfil-pipe-network",
109
+ name: "Piping data to network",
110
+ category: "data_exfiltration",
111
+ patterns: [
112
+ { type: "regex", value: "cat\\s+.*\\|\\s*(nc|netcat|curl|wget)", target: "command" },
113
+ { type: "regex", value: "(tar|zip|gzip)\\s+.*\\|\\s*(nc|curl)", target: "command" },
114
+ { type: "regex", value: "base64\\s+.*\\|\\s*curl", target: "command" }
115
+ ],
116
+ riskLevel: "dangerous",
117
+ priority: 90,
118
+ polarity: "negative"
119
+ },
120
+ {
121
+ id: "exfil-dns",
122
+ name: "DNS exfiltration",
123
+ category: "data_exfiltration",
124
+ patterns: [
125
+ { type: "regex", value: "dig\\s+.*\\$\\(", target: "command" },
126
+ { type: "regex", value: "nslookup\\s+.*\\$\\(", target: "command" }
127
+ ],
128
+ riskLevel: "critical",
129
+ priority: 95,
130
+ polarity: "negative"
131
+ },
132
+ // ── Privilege Escalation ──
133
+ {
134
+ id: "privesc-sudo",
135
+ name: "Privilege escalation via sudo",
136
+ category: "privilege_escalation",
137
+ patterns: [
138
+ { type: "regex", value: "sudo\\s+", target: "command" },
139
+ { type: "regex", value: "su\\s+-", target: "command" },
140
+ { type: "keyword", value: "doas", target: "command" }
141
+ ],
142
+ riskLevel: "dangerous",
143
+ priority: 85,
144
+ polarity: "negative"
145
+ },
146
+ {
147
+ id: "privesc-chmod",
148
+ name: "Permission modification",
149
+ category: "privilege_escalation",
150
+ patterns: [
151
+ { type: "regex", value: "chmod\\s+(777|\\+s|u\\+s|g\\+s)", target: "command" },
152
+ { type: "regex", value: "chown\\s+root", target: "command" },
153
+ { type: "regex", value: "setuid", target: "command" }
154
+ ],
155
+ riskLevel: "dangerous",
156
+ priority: 85,
157
+ polarity: "negative"
158
+ },
159
+ // ── Code Execution ──
160
+ {
161
+ id: "exec-remote",
162
+ name: "Remote code execution",
163
+ category: "code_execution",
164
+ patterns: [
165
+ { type: "regex", value: "curl\\s+.*\\|\\s*(sh|bash|python|node|perl|ruby)", target: "command" },
166
+ { type: "regex", value: "wget\\s+.*\\|\\s*(sh|bash)", target: "command" },
167
+ { type: "regex", value: "eval\\s*\\(", target: "arguments" },
168
+ { type: "regex", value: "exec\\s*\\(", target: "arguments" }
169
+ ],
170
+ riskLevel: "critical",
171
+ priority: 100,
172
+ polarity: "negative"
173
+ },
174
+ {
175
+ id: "exec-obfuscated",
176
+ name: "Obfuscated execution",
177
+ category: "code_execution",
178
+ patterns: [
179
+ { type: "regex", value: "\\$\\(.*\\)", target: "command" },
180
+ { type: "regex", value: "`[^`]+`", target: "command" },
181
+ { type: "regex", value: "base64\\s+(-d|--decode)", target: "command" },
182
+ { type: "regex", value: "\\\\x[0-9a-fA-F]{2}", target: "arguments" }
183
+ ],
184
+ riskLevel: "suspicious",
185
+ priority: 75,
186
+ polarity: "negative"
187
+ },
188
+ // ── Credential Access ──
189
+ {
190
+ id: "cred-env",
191
+ name: "Environment variable access",
192
+ category: "credential_access",
193
+ patterns: [
194
+ { type: "regex", value: "\\$\\{?(API_KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL)", target: "command", caseSensitive: false },
195
+ { type: "regex", value: "printenv|env\\s*$", target: "command" },
196
+ { type: "regex", value: "cat\\s+.*\\.(env|secret|key|pem|crt)", target: "command" }
197
+ ],
198
+ riskLevel: "dangerous",
199
+ priority: 90,
200
+ polarity: "negative"
201
+ },
202
+ {
203
+ id: "cred-ssh",
204
+ name: "SSH key access",
205
+ category: "credential_access",
206
+ patterns: [
207
+ { type: "regex", value: "cat\\s+.*\\.ssh/(id_rsa|id_ed25519|authorized_keys)", target: "command" },
208
+ { type: "regex", value: "ssh-keygen", target: "command" }
209
+ ],
210
+ riskLevel: "dangerous",
211
+ priority: 85,
212
+ polarity: "negative"
213
+ },
214
+ // ── Financial Operations ──
215
+ {
216
+ id: "finance-payment",
217
+ name: "Payment/transfer operation",
218
+ category: "financial_operation",
219
+ patterns: [
220
+ { type: "keyword", value: "payment", target: "tool_name" },
221
+ { type: "keyword", value: "transfer", target: "tool_name" },
222
+ { type: "keyword", value: "checkout", target: "tool_name" },
223
+ { type: "keyword", value: "invoice", target: "tool_name" }
224
+ ],
225
+ riskLevel: "dangerous",
226
+ priority: 90,
227
+ polarity: "negative"
228
+ },
229
+ // ── Network Access ──
230
+ {
231
+ id: "net-reverse-shell",
232
+ name: "Reverse shell attempt",
233
+ category: "network_access",
234
+ patterns: [
235
+ { type: "regex", value: "(nc|netcat|ncat)\\s+.*-e\\s", target: "command" },
236
+ { type: "regex", value: "bash\\s+-i\\s+>\\s*&\\s*/dev/tcp/", target: "command" },
237
+ { type: "regex", value: "/dev/(tcp|udp)/", target: "command" }
238
+ ],
239
+ riskLevel: "critical",
240
+ priority: 100,
241
+ polarity: "negative"
242
+ },
243
+ // ── System Modification ──
244
+ {
245
+ id: "sys-service",
246
+ name: "System service modification",
247
+ category: "system_modification",
248
+ patterns: [
249
+ { type: "regex", value: "systemctl\\s+(stop|disable|mask|restart)", target: "command" },
250
+ { type: "regex", value: "service\\s+\\w+\\s+(stop|restart)", target: "command" },
251
+ { type: "regex", value: "crontab\\s+-[er]", target: "command" }
252
+ ],
253
+ riskLevel: "dangerous",
254
+ priority: 80,
255
+ polarity: "negative"
256
+ },
257
+ // ── File Modification ──
258
+ {
259
+ id: "file-write-system",
260
+ name: "System file modification",
261
+ category: "file_modification",
262
+ patterns: [
263
+ { type: "regex", value: "(>|>>)\\s*/etc/", target: "command" },
264
+ { type: "regex", value: "tee\\s+(-a\\s+)?/etc/", target: "command" },
265
+ { type: "regex", value: "sed\\s+-i.*\\s+/etc/", target: "command" }
266
+ ],
267
+ riskLevel: "dangerous",
268
+ priority: 85,
269
+ polarity: "negative"
270
+ },
271
+ // ── Benign / Read-only ──
272
+ {
273
+ id: "safe-read",
274
+ name: "Read-only operation",
275
+ category: "read_only",
276
+ patterns: [
277
+ { type: "regex", value: "^(ls|cat|head|tail|less|more|wc|file|stat|du|df)\\s", target: "command" },
278
+ { type: "regex", value: "^(echo|printf|date|whoami|hostname|uname|pwd)\\s*", target: "command" },
279
+ { type: "regex", value: "^(grep|awk|sed|sort|uniq|cut|tr)\\s", target: "command" },
280
+ { type: "regex", value: "^SELECT\\s", target: "arguments", caseSensitive: false }
281
+ ],
282
+ riskLevel: "safe",
283
+ priority: 10,
284
+ polarity: "positive"
285
+ }
286
+ ];
287
+ function analyzeStructure(command) {
288
+ const pipes = command.split(/(?<![|])\|(?![|])/).map((s) => s.trim()).filter(Boolean);
289
+ const redirections = [...command.matchAll(/(>{1,2})\s*(\S+)/g)].map((m) => m[2]);
290
+ const subshells = [...command.matchAll(/\$\(([^)]+)\)/g)].map((m) => m[1]);
291
+ const backtickSubs = [...command.matchAll(/`([^`]+)`/g)].map((m) => m[1]);
292
+ const filePaths = [...command.matchAll(/(?:^|\s)(\/[\w./-]+)/g)].map((m) => m[1]);
293
+ const networkTargets = [
294
+ ...command.matchAll(/https?:\/\/[\w./:@-]+/g),
295
+ ...command.matchAll(/(\d{1,3}\.){3}\d{1,3}(:\d+)?/g)
296
+ ].map((m) => m[0]);
297
+ const envVars = [...command.matchAll(/\$\{?([A-Z_][A-Z0-9_]*)\}?/g)].map((m) => m[1]);
298
+ let complexity = 0;
299
+ complexity += pipes.length * 10;
300
+ complexity += redirections.length * 8;
301
+ complexity += (subshells.length + backtickSubs.length) * 15;
302
+ complexity += envVars.length * 3;
303
+ complexity += networkTargets.length * 12;
304
+ complexity += command.length > 200 ? 20 : command.length > 100 ? 10 : 0;
305
+ return {
306
+ pipes,
307
+ redirections,
308
+ subshells: [...subshells, ...backtickSubs],
309
+ filePaths,
310
+ networkTargets,
311
+ envVars,
312
+ complexity
313
+ };
314
+ }
315
+ function structuralRiskAssessment(structure) {
316
+ const findings = [];
317
+ let riskScore = 0;
318
+ if (structure.pipes.length > 3) {
319
+ findings.push(`Long pipe chain (${structure.pipes.length} segments) \u2014 potential obfuscation`);
320
+ riskScore += 15;
321
+ }
322
+ if (structure.subshells.length > 0) {
323
+ findings.push(`Command substitution detected (${structure.subshells.length} instances)`);
324
+ riskScore += structure.subshells.length * 10;
325
+ }
326
+ const sensitivePaths = structure.filePaths.filter(
327
+ (p) => /\/(etc|root|\.ssh|\.gnupg|\.aws|\.config|proc|sys|dev)\//.test(p)
328
+ );
329
+ if (sensitivePaths.length > 0) {
330
+ findings.push(`Sensitive paths accessed: ${sensitivePaths.join(", ")}`);
331
+ riskScore += sensitivePaths.length * 15;
332
+ }
333
+ if (structure.networkTargets.length > 0) {
334
+ findings.push(`Network targets: ${structure.networkTargets.join(", ")}`);
335
+ riskScore += 10;
336
+ }
337
+ if (structure.complexity > 50) {
338
+ findings.push(`High command complexity (score: ${structure.complexity})`);
339
+ riskScore += 10;
340
+ }
341
+ if (structure.redirections.length > 1) {
342
+ findings.push(`Multiple redirections (${structure.redirections.length})`);
343
+ riskScore += 5;
344
+ }
345
+ const sensitiveEnvVars = structure.envVars.filter(
346
+ (v) => /(KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL|PRIVATE)/i.test(v)
347
+ );
348
+ if (sensitiveEnvVars.length > 0) {
349
+ findings.push(`Sensitive env vars referenced: ${sensitiveEnvVars.join(", ")}`);
350
+ riskScore += sensitiveEnvVars.length * 12;
351
+ }
352
+ riskScore = Math.min(riskScore, 100);
353
+ let riskLevel;
354
+ if (riskScore < 20) riskLevel = "safe";
355
+ else if (riskScore < 45) riskLevel = "suspicious";
356
+ else if (riskScore < 70) riskLevel = "dangerous";
357
+ else riskLevel = "critical";
358
+ return {
359
+ riskLevel,
360
+ riskScore,
361
+ confidence: 0.7,
362
+ findings
363
+ };
364
+ }
365
+ var STATE_TRANSITIONS = [
366
+ // safe → elevated
367
+ { from: "safe", trigger: "sudo/su detected", to: "elevated", pattern: /\b(sudo|su\s+-|doas)\b/ },
368
+ { from: "safe", trigger: "write operation detected", to: "elevated", pattern: /\b(mv|cp|tee|sed\s+-i|chmod|chown)\b/ },
369
+ { from: "safe", trigger: "network write detected", to: "elevated", pattern: /\bcurl\s+.*(-d|--data|-X\s*(POST|PUT|DELETE))/i },
370
+ // elevated → destructive
371
+ { from: "elevated", trigger: "delete operation detected", to: "destructive", pattern: /\b(rm|unlink|rmdir)\b/ },
372
+ { from: "elevated", trigger: "database write detected", to: "destructive", pattern: /\b(INSERT|UPDATE|DELETE|ALTER)\b/i },
373
+ { from: "elevated", trigger: "service modification detected", to: "destructive", pattern: /\b(systemctl|service)\s+(stop|disable|restart)\b/ },
374
+ // safe → destructive (direct jump for known dangerous patterns)
375
+ { from: "safe", trigger: "direct delete detected", to: "destructive", pattern: /\brm\s+-[a-zA-Z]*r/ },
376
+ { from: "safe", trigger: "database mutation detected", to: "destructive", pattern: /\b(DELETE\s+FROM|UPDATE\s+\w+\s+SET)\b/i },
377
+ // destructive → irreversible
378
+ { from: "destructive", trigger: "recursive force delete", to: "irreversible", pattern: /\brm\s+-[a-zA-Z]*r[a-zA-Z]*f\b/ },
379
+ { from: "destructive", trigger: "DROP/TRUNCATE detected", to: "irreversible", pattern: /\b(DROP|TRUNCATE)\s+(TABLE|DATABASE|SCHEMA)\b/i },
380
+ { from: "destructive", trigger: "disk format detected", to: "irreversible", pattern: /\b(mkfs|dd\s+.*of=\/dev\/)\b/ },
381
+ // elevated → irreversible (direct jump)
382
+ { from: "elevated", trigger: "sudo + recursive delete", to: "irreversible", pattern: /sudo\s+rm\s+-[a-zA-Z]*r[a-zA-Z]*f/ },
383
+ { from: "elevated", trigger: "sudo + DROP", to: "irreversible", pattern: /sudo\s+.*\b(DROP|TRUNCATE)\b/i },
384
+ // safe → irreversible (worst case direct jump)
385
+ { from: "safe", trigger: "rm -rf / detected", to: "irreversible", pattern: /rm\s+-[a-zA-Z]*r[a-zA-Z]*f\s+\// },
386
+ { from: "safe", trigger: "DROP TABLE without conditions", to: "irreversible", pattern: /DROP\s+(TABLE|DATABASE)\b/i }
387
+ ];
388
+ function verifyPathConstraints(command, filePaths, allowedPaths) {
389
+ const results = [];
390
+ const getOperation = (cmd, path) => {
391
+ if (/\b(rm|unlink|rmdir|shred)\b/.test(cmd)) return "delete";
392
+ if (/\b(mv|cp|tee|sed\s+-i|chmod|chown|mkdir|touch)\b/.test(cmd)) return "write";
393
+ if (/\b(>|>>)/.test(cmd)) return "write";
394
+ if (/\b(sh|bash|python|node|perl|ruby|exec)\b/.test(cmd)) return "execute";
395
+ return "read";
396
+ };
397
+ for (const filePath of filePaths) {
398
+ const operation = getOperation(command, filePath);
399
+ const normalizedPath = normalizePath(filePath);
400
+ let withinBounds = false;
401
+ if (allowedPaths.length === 0) {
402
+ withinBounds = operation === "read" || !isSystemPath(normalizedPath);
403
+ } else {
404
+ withinBounds = allowedPaths.some(
405
+ (allowed) => normalizedPath.startsWith(normalizePath(allowed))
406
+ );
407
+ }
408
+ if ((operation === "write" || operation === "delete") && isSystemPath(normalizedPath)) {
409
+ withinBounds = false;
410
+ }
411
+ const constraint = {
412
+ path: filePath,
413
+ operation,
414
+ withinBounds
415
+ };
416
+ if (!withinBounds) {
417
+ constraint.violation = `${operation} operation on "${filePath}" is outside allowed boundaries`;
418
+ }
419
+ results.push(constraint);
420
+ }
421
+ return results;
422
+ }
423
+ function normalizePath(p) {
424
+ const parts = p.split("/").filter(Boolean);
425
+ const resolved = [];
426
+ for (const part of parts) {
427
+ if (part === "..") resolved.pop();
428
+ else if (part !== ".") resolved.push(part);
429
+ }
430
+ return "/" + resolved.join("/");
431
+ }
432
+ function isSystemPath(p) {
433
+ const systemPrefixes = [
434
+ "/etc/",
435
+ "/usr/",
436
+ "/bin/",
437
+ "/sbin/",
438
+ "/boot/",
439
+ "/proc/",
440
+ "/sys/",
441
+ "/dev/",
442
+ "/root/",
443
+ "/var/log/",
444
+ "/var/run/"
445
+ ];
446
+ return systemPrefixes.some((prefix) => p.startsWith(prefix));
447
+ }
448
+ function checkSQLType(sql, safeTables) {
449
+ const trimmed = sql.trim().toUpperCase();
450
+ let statementType = "UNKNOWN";
451
+ if (trimmed.startsWith("SELECT")) statementType = "SELECT";
452
+ else if (trimmed.startsWith("INSERT")) statementType = "INSERT";
453
+ else if (trimmed.startsWith("UPDATE")) statementType = "UPDATE";
454
+ else if (trimmed.startsWith("DELETE")) statementType = "DELETE";
455
+ else if (trimmed.startsWith("DROP")) statementType = "DROP";
456
+ else if (trimmed.startsWith("CREATE")) statementType = "CREATE";
457
+ else if (trimmed.startsWith("ALTER")) statementType = "ALTER";
458
+ else if (trimmed.startsWith("TRUNCATE")) statementType = "TRUNCATE";
459
+ const hasWhereClause = /\bWHERE\b/i.test(sql);
460
+ const tableMatches = [
461
+ ...sql.matchAll(/\bFROM\s+`?(\w+)`?/gi),
462
+ ...sql.matchAll(/\bINTO\s+`?(\w+)`?/gi),
463
+ ...sql.matchAll(/\bUPDATE\s+`?(\w+)`?/gi),
464
+ ...sql.matchAll(/\bTABLE\s+`?(\w+)`?/gi),
465
+ ...sql.matchAll(/\bJOIN\s+`?(\w+)`?/gi)
466
+ ];
467
+ const tables = [...new Set(tableMatches.map((m) => m[1].toLowerCase()))];
468
+ const tablesAreSafe = safeTables.length === 0 || tables.every(
469
+ (t) => safeTables.map((s) => s.toLowerCase()).includes(t)
470
+ );
471
+ const violations = [];
472
+ if ((statementType === "UPDATE" || statementType === "DELETE") && !hasWhereClause) {
473
+ violations.push(`${statementType} without WHERE clause \u2014 will affect ALL rows`);
474
+ }
475
+ if (statementType === "DROP" || statementType === "TRUNCATE") {
476
+ violations.push(`${statementType} is an irreversible operation`);
477
+ }
478
+ if (!tablesAreSafe && tables.length > 0) {
479
+ violations.push(`Tables not in safe list: ${tables.join(", ")}`);
480
+ }
481
+ const statementCount = sql.split(";").filter((s) => s.trim().length > 0).length;
482
+ if (statementCount > 1) {
483
+ violations.push(`Multiple SQL statements detected (${statementCount}) \u2014 potential injection`);
484
+ }
485
+ return {
486
+ statementType,
487
+ hasWhereClause,
488
+ tables,
489
+ tablesAreSafe,
490
+ violations
491
+ };
492
+ }
493
+ function runStateMachine(command) {
494
+ let currentState = "safe";
495
+ const transitions = [];
496
+ const stateOrder = {
497
+ safe: 0,
498
+ elevated: 1,
499
+ destructive: 2,
500
+ irreversible: 3
501
+ };
502
+ for (const transition of STATE_TRANSITIONS) {
503
+ if (transition.pattern.test(command)) {
504
+ if (stateOrder[transition.to] > stateOrder[currentState]) {
505
+ if (transition.from === currentState || stateOrder[transition.from] <= stateOrder[currentState]) {
506
+ transitions.push({
507
+ trigger: transition.trigger,
508
+ from: currentState,
509
+ to: transition.to
510
+ });
511
+ currentState = transition.to;
512
+ }
513
+ }
514
+ }
515
+ }
516
+ return { finalState: currentState, transitions };
517
+ }
518
+ function formalVerify(command, args, structure, config) {
519
+ const findings = [];
520
+ let riskScore = 0;
521
+ const pathViolations = verifyPathConstraints(command, structure.filePaths, config.allowedPaths);
522
+ const violations = pathViolations.filter((p) => !p.withinBounds);
523
+ if (violations.length > 0) {
524
+ for (const v of violations) {
525
+ findings.push(`PATH VIOLATION: ${v.violation}`);
526
+ }
527
+ riskScore += violations.length * 20;
528
+ }
529
+ const { finalState, transitions } = runStateMachine(command);
530
+ const stateScores = {
531
+ safe: 0,
532
+ elevated: 25,
533
+ destructive: 60,
534
+ irreversible: 95
535
+ };
536
+ riskScore = Math.max(riskScore, stateScores[finalState]);
537
+ if (transitions.length > 0) {
538
+ findings.push(`State machine: ${transitions.map((t) => `${t.from}\u2192${t.to} (${t.trigger})`).join(", ")}`);
539
+ }
540
+ if (finalState === "irreversible") {
541
+ findings.push(`FORMAL PROOF: Command reaches IRREVERSIBLE state \u2014 cannot be undone`);
542
+ }
543
+ let sqlCheck;
544
+ const sqlContent = extractSQL(args);
545
+ if (sqlContent) {
546
+ sqlCheck = checkSQLType(sqlContent, config.safeTables);
547
+ if (sqlCheck.violations.length > 0) {
548
+ for (const v of sqlCheck.violations) {
549
+ findings.push(`SQL VIOLATION: ${v}`);
550
+ }
551
+ riskScore += sqlCheck.violations.length * 25;
552
+ }
553
+ }
554
+ const complexityExceeded = structure.complexity > config.maxComplexity;
555
+ if (complexityExceeded) {
556
+ findings.push(`COMPLEXITY EXCEEDED: ${structure.complexity} > ${config.maxComplexity} threshold`);
557
+ riskScore += 15;
558
+ }
559
+ riskScore = Math.min(riskScore, 100);
560
+ return {
561
+ pathViolations,
562
+ finalState,
563
+ transitions,
564
+ sqlCheck,
565
+ complexityScore: structure.complexity,
566
+ complexityExceeded,
567
+ riskScore,
568
+ findings
569
+ };
570
+ }
571
+ function extractSQL(args) {
572
+ for (const key of ["query", "sql", "statement", "command"]) {
573
+ if (typeof args[key] === "string") {
574
+ const val = args[key];
575
+ if (/\b(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|TRUNCATE)\b/i.test(val)) {
576
+ return val;
577
+ }
578
+ }
579
+ }
580
+ return null;
581
+ }
582
+ function findRuleCategoryByName(ruleName, rules) {
583
+ const rule = rules.find((r) => r.name === ruleName);
584
+ return rule?.category ?? "code_execution";
585
+ }
586
+ function classifyStructuralFinding(finding) {
587
+ if (/sensitive\s*path/i.test(finding)) return "file_modification";
588
+ if (/network\s*target/i.test(finding)) return "network_access";
589
+ if (/pipe\s*chain|command\s*substitution/i.test(finding)) return "code_execution";
590
+ if (/redirect/i.test(finding)) return "file_modification";
591
+ if (/complex/i.test(finding)) return "code_execution";
592
+ if (/env\s*var/i.test(finding)) return "credential_access";
593
+ return "code_execution";
594
+ }
595
+ function classifyFormalFinding(finding) {
596
+ if (/PATH\s*VIOLATION/i.test(finding)) return "file_modification";
597
+ if (/SQL\s*VIOLATION/i.test(finding)) return "data_destruction";
598
+ if (/IRREVERSIBLE/i.test(finding)) return "data_destruction";
599
+ if (/COMPLEXITY/i.test(finding)) return "code_execution";
600
+ if (/state\s*machine.*destructive/i.test(finding)) return "data_destruction";
601
+ if (/state\s*machine.*elevated/i.test(finding)) return "privilege_escalation";
602
+ return "system_modification";
603
+ }
604
+ var SemanticAnalyzer = class {
605
+ config;
606
+ rules;
607
+ constructor(config = {}) {
608
+ this.config = {
609
+ customRules: config.customRules ?? [],
610
+ enableStructural: config.enableStructural ?? true,
611
+ enableFormalVerification: config.enableFormalVerification ?? true,
612
+ sensitivity: config.sensitivity ?? "medium",
613
+ allowedPaths: config.allowedPaths ?? [],
614
+ safeTables: config.safeTables ?? [],
615
+ maxComplexity: config.maxComplexity ?? 80
616
+ };
617
+ this.rules = [...BUILTIN_RULES, ...this.config.customRules].sort((a, b) => b.priority - a.priority);
618
+ }
619
+ /**
620
+ * Analyze a tool call for security risks.
621
+ * All three layers are deterministic — zero LLM dependency.
622
+ */
623
+ async analyze(toolName, args) {
624
+ const startTime = Date.now();
625
+ const ruleResult = this.analyzeWithRules(toolName, args);
626
+ let structuralResult = { riskLevel: "safe", riskScore: 0, confidence: 0, findings: [] };
627
+ let structure = null;
628
+ const command = this.extractCommand(toolName, args);
629
+ if (this.config.enableStructural && command) {
630
+ structure = analyzeStructure(command);
631
+ structuralResult = structuralRiskAssessment(structure);
632
+ }
633
+ let formalResult;
634
+ if (this.config.enableFormalVerification && command) {
635
+ const struct = structure ?? analyzeStructure(command);
636
+ const fv = formalVerify(command, args, struct, this.config);
637
+ formalResult = {
638
+ riskLevel: fv.riskScore >= 70 ? "critical" : fv.riskScore >= 45 ? "dangerous" : fv.riskScore >= 20 ? "suspicious" : "safe",
639
+ riskScore: fv.riskScore,
640
+ confidence: 0.95,
641
+ // Formal verification has highest confidence — it's math, not guessing
642
+ findings: fv.findings
643
+ };
644
+ }
645
+ const intents = this.collectIntents(ruleResult, structuralResult, formalResult);
646
+ const finalResult = this.combineResults(ruleResult, structuralResult, formalResult);
647
+ return {
648
+ ...finalResult,
649
+ intents,
650
+ layers: {
651
+ rules: ruleResult,
652
+ structural: structuralResult,
653
+ formal: formalResult
654
+ },
655
+ durationMs: Date.now() - startTime
656
+ };
657
+ }
658
+ /** Quick synchronous check (Layer 1 only, for hot path) */
659
+ quickCheck(toolName, args) {
660
+ const result = this.analyzeWithRules(toolName, args);
661
+ return {
662
+ riskLevel: result.riskLevel,
663
+ riskScore: result.riskScore,
664
+ topFinding: result.findings[0] || "No findings"
665
+ };
666
+ }
667
+ /** Add custom rules at runtime */
668
+ addRule(rule) {
669
+ this.rules.push(rule);
670
+ this.rules.sort((a, b) => b.priority - a.priority);
671
+ }
672
+ /** Get all active rules */
673
+ getRules() {
674
+ return [...this.rules];
675
+ }
676
+ // ─── Private ─────────────────────────────────────────────────────────────
677
+ analyzeWithRules(toolName, args) {
678
+ const findings = [];
679
+ let maxRiskScore = 0;
680
+ let maxRiskLevel = "safe";
681
+ let hasPositiveMatch = false;
682
+ const argsStr = JSON.stringify(args);
683
+ const command = this.extractCommand(toolName, args);
684
+ for (const rule of this.rules) {
685
+ let matched = false;
686
+ for (const pattern of rule.patterns) {
687
+ const target = this.getPatternTarget(pattern.target, toolName, command, argsStr);
688
+ if (!target) continue;
689
+ switch (pattern.type) {
690
+ case "regex": {
691
+ const flags = pattern.caseSensitive === false ? "i" : "";
692
+ const regex = new RegExp(pattern.value, flags);
693
+ if (regex.test(target)) matched = true;
694
+ break;
695
+ }
696
+ case "keyword": {
697
+ const searchIn = pattern.caseSensitive ? target : target.toLowerCase();
698
+ const searchFor = pattern.caseSensitive ? pattern.value : pattern.value.toLowerCase();
699
+ if (searchIn.includes(searchFor)) matched = true;
700
+ break;
701
+ }
702
+ case "sequence": {
703
+ const keywords = pattern.value.split(",").map((k) => k.trim());
704
+ let lastIndex = -1;
705
+ let allFound = true;
706
+ for (const kw of keywords) {
707
+ const idx = target.indexOf(kw, lastIndex + 1);
708
+ if (idx === -1) {
709
+ allFound = false;
710
+ break;
711
+ }
712
+ lastIndex = idx;
713
+ }
714
+ if (allFound) matched = true;
715
+ break;
716
+ }
717
+ }
718
+ if (matched) break;
719
+ }
720
+ if (matched) {
721
+ if (rule.polarity === "positive") {
722
+ hasPositiveMatch = true;
723
+ findings.push(`\u2713 ${rule.name}`);
724
+ } else {
725
+ findings.push(`\u2717 ${rule.name} [${rule.riskLevel}]`);
726
+ const ruleScore = this.riskLevelToScore(rule.riskLevel);
727
+ if (ruleScore > maxRiskScore) {
728
+ maxRiskScore = ruleScore;
729
+ maxRiskLevel = rule.riskLevel;
730
+ }
731
+ }
732
+ }
733
+ }
734
+ maxRiskScore = this.applySensitivity(maxRiskScore);
735
+ if (hasPositiveMatch && maxRiskScore === 0) {
736
+ return { riskLevel: "safe", riskScore: 0, confidence: 0.9, findings };
737
+ }
738
+ return {
739
+ riskLevel: maxRiskLevel,
740
+ riskScore: maxRiskScore,
741
+ confidence: findings.length > 0 ? 0.85 : 0.5,
742
+ findings
743
+ };
744
+ }
745
+ extractCommand(toolName, args) {
746
+ for (const key of ["command", "cmd", "query", "sql", "script", "code", "input"]) {
747
+ if (typeof args[key] === "string") return args[key];
748
+ }
749
+ if (["bash", "shell", "exec", "run_command"].includes(toolName.toLowerCase())) {
750
+ const firstStr = Object.values(args).find((v) => typeof v === "string");
751
+ if (firstStr) return firstStr;
752
+ }
753
+ return null;
754
+ }
755
+ getPatternTarget(target, toolName, command, argsStr) {
756
+ switch (target) {
757
+ case "tool_name":
758
+ return toolName;
759
+ case "command":
760
+ return command;
761
+ case "arguments":
762
+ return argsStr;
763
+ case "full_context":
764
+ return `${toolName} ${command || ""} ${argsStr}`;
765
+ default:
766
+ return null;
767
+ }
768
+ }
769
+ riskLevelToScore(level) {
770
+ switch (level) {
771
+ case "safe":
772
+ return 0;
773
+ case "suspicious":
774
+ return 35;
775
+ case "dangerous":
776
+ return 65;
777
+ case "critical":
778
+ return 90;
779
+ default:
780
+ return 50;
781
+ }
782
+ }
783
+ applySensitivity(score) {
784
+ switch (this.config.sensitivity) {
785
+ case "low":
786
+ return Math.max(0, score - 15);
787
+ case "medium":
788
+ return score;
789
+ case "high":
790
+ return Math.min(100, score + 10);
791
+ case "paranoid":
792
+ return Math.min(100, score + 25);
793
+ }
794
+ }
795
+ /**
796
+ * Collect intents with CORRECT IntentCategory mapping.
797
+ *
798
+ * BUG FIX: Previously all intents were mapped to 'code_execution'.
799
+ * Now we properly extract the category from:
800
+ * - Rule findings → look up the matched rule's category
801
+ * - Structural findings → classify based on finding content
802
+ * - Formal findings → classify based on violation type
803
+ */
804
+ collectIntents(ruleResult, structuralResult, formalResult) {
805
+ const intents = [];
806
+ for (const finding of ruleResult.findings) {
807
+ if (finding.startsWith("\u2717")) {
808
+ const match = finding.match(/✗ (.+) \[(\w+)\]/);
809
+ if (match) {
810
+ const ruleName = match[1];
811
+ const category = findRuleCategoryByName(ruleName, this.rules);
812
+ intents.push({
813
+ category,
814
+ description: ruleName,
815
+ confidence: ruleResult.confidence,
816
+ source: "rule",
817
+ evidence: [finding]
818
+ });
819
+ }
820
+ }
821
+ }
822
+ for (const finding of structuralResult.findings) {
823
+ const category = classifyStructuralFinding(finding);
824
+ intents.push({
825
+ category,
826
+ description: finding,
827
+ confidence: structuralResult.confidence,
828
+ source: "structural",
829
+ evidence: [finding]
830
+ });
831
+ }
832
+ if (formalResult) {
833
+ for (const finding of formalResult.findings) {
834
+ const category = classifyFormalFinding(finding);
835
+ intents.push({
836
+ category,
837
+ description: finding,
838
+ confidence: formalResult.confidence,
839
+ source: "formal",
840
+ evidence: [finding]
841
+ });
842
+ }
843
+ }
844
+ return intents;
845
+ }
846
+ combineResults(ruleResult, structuralResult, formalResult) {
847
+ const ruleWeight = 0.35;
848
+ const structWeight = 0.25;
849
+ const formalWeight = formalResult ? 0.4 : 0;
850
+ const normalizer = ruleWeight + structWeight + formalWeight;
851
+ const combinedScore = Math.round(
852
+ (ruleResult.riskScore * ruleWeight + structuralResult.riskScore * structWeight + (formalResult?.riskScore ?? 0) * formalWeight) / normalizer
853
+ );
854
+ const levels = [ruleResult.riskLevel, structuralResult.riskLevel, formalResult?.riskLevel].filter(Boolean);
855
+ const levelOrder = { safe: 0, suspicious: 1, dangerous: 2, critical: 3 };
856
+ const maxLevel = levels.reduce(
857
+ (max, l) => (levelOrder[l] ?? 0) > (levelOrder[max] ?? 0) ? l : max,
858
+ "safe"
859
+ );
860
+ const confidence = Math.round(
861
+ (ruleResult.confidence * ruleWeight + structuralResult.confidence * structWeight + (formalResult?.confidence ?? 0) * formalWeight) / normalizer * 100
862
+ ) / 100;
863
+ let recommendation;
864
+ if (combinedScore < 20) recommendation = "allow";
865
+ else if (combinedScore < 45) recommendation = "warn";
866
+ else if (combinedScore < 70) recommendation = "require-approval";
867
+ else recommendation = "block";
868
+ const allFindings = [
869
+ ...ruleResult.findings,
870
+ ...structuralResult.findings,
871
+ ...formalResult?.findings ?? []
872
+ ];
873
+ const explanation = allFindings.length > 0 ? `Risk: ${maxLevel} (score: ${combinedScore}). Findings: ${allFindings.slice(0, 3).join("; ")}` : `Risk: ${maxLevel} (score: ${combinedScore}). No specific findings.`;
874
+ return {
875
+ riskLevel: maxLevel,
876
+ riskScore: combinedScore,
877
+ confidence,
878
+ recommendation,
879
+ explanation
880
+ };
881
+ }
882
+ };
883
+ function createSemanticAnalyzer(overrides = {}) {
884
+ return new SemanticAnalyzer(overrides);
885
+ }
886
+ function createParanoidAnalyzer(config) {
887
+ return new SemanticAnalyzer({
888
+ enableStructural: true,
889
+ enableFormalVerification: true,
890
+ sensitivity: "paranoid",
891
+ maxComplexity: 40,
892
+ allowedPaths: config?.allowedPaths ?? [],
893
+ safeTables: config?.safeTables ?? []
894
+ });
895
+ }
896
+ // Annotate the CommonJS export names for ESM import in node:
897
+ 0 && (module.exports = {
898
+ BUILTIN_RULES,
899
+ SemanticAnalyzer,
900
+ analyzeStructure,
901
+ checkSQLType,
902
+ classifyFormalFinding,
903
+ classifyStructuralFinding,
904
+ createParanoidAnalyzer,
905
+ createSemanticAnalyzer,
906
+ findRuleCategoryByName,
907
+ formalVerify,
908
+ runStateMachine,
909
+ structuralRiskAssessment,
910
+ verifyPathConstraints
911
+ });