sovr-mcp-proxy 7.0.0 → 7.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.
@@ -0,0 +1,674 @@
1
+ // src/semanticAnalyzer.ts
2
+ var BUILTIN_RULES = [
3
+ // ── Data Destruction ──
4
+ {
5
+ id: "destroy-rm-rf",
6
+ name: "Recursive file deletion",
7
+ category: "data_destruction",
8
+ patterns: [
9
+ { type: "regex", value: "rm\\s+(-[a-zA-Z]*r[a-zA-Z]*f|--recursive)\\s", target: "command" },
10
+ { type: "regex", value: "rm\\s+(-[a-zA-Z]*f[a-zA-Z]*r)\\s", target: "command" },
11
+ { type: "keyword", value: "shred", target: "command" },
12
+ { type: "keyword", value: "wipe", target: "command" }
13
+ ],
14
+ riskLevel: "critical",
15
+ priority: 100,
16
+ polarity: "negative"
17
+ },
18
+ {
19
+ id: "destroy-db-drop",
20
+ name: "Database destruction",
21
+ category: "data_destruction",
22
+ patterns: [
23
+ { type: "regex", value: "DROP\\s+(TABLE|DATABASE|SCHEMA|INDEX)", target: "arguments", caseSensitive: false },
24
+ { type: "regex", value: "TRUNCATE\\s+TABLE", target: "arguments", caseSensitive: false },
25
+ { type: "regex", value: "DELETE\\s+FROM\\s+\\w+\\s*$", target: "arguments", caseSensitive: false }
26
+ // DELETE without WHERE
27
+ ],
28
+ riskLevel: "critical",
29
+ priority: 100,
30
+ polarity: "negative"
31
+ },
32
+ {
33
+ id: "destroy-format",
34
+ name: "Disk formatting",
35
+ category: "data_destruction",
36
+ patterns: [
37
+ { type: "regex", value: "mkfs\\.", target: "command" },
38
+ { type: "regex", value: "dd\\s+.*of=/dev/", target: "command" },
39
+ { type: "regex", value: "format\\s+[A-Z]:", target: "command", caseSensitive: false }
40
+ ],
41
+ riskLevel: "critical",
42
+ priority: 100,
43
+ polarity: "negative"
44
+ },
45
+ {
46
+ id: "destroy-find-delete",
47
+ name: "Find and delete (rm -rf equivalent)",
48
+ category: "data_destruction",
49
+ patterns: [
50
+ { type: "regex", value: "find\\s+.*-delete", target: "command" },
51
+ { type: "regex", value: "find\\s+.*-exec\\s+rm", target: "command" },
52
+ { type: "regex", value: "xargs\\s+rm", target: "command" }
53
+ ],
54
+ riskLevel: "dangerous",
55
+ priority: 95,
56
+ polarity: "negative"
57
+ },
58
+ // ── Data Exfiltration ──
59
+ {
60
+ id: "exfil-curl-post",
61
+ name: "Data upload via curl/wget",
62
+ category: "data_exfiltration",
63
+ patterns: [
64
+ { type: "regex", value: "curl\\s+.*(-d|--data|--upload-file|-F|--form)\\s", target: "command" },
65
+ { type: "regex", value: "curl\\s+.*-X\\s*(POST|PUT)", target: "command", caseSensitive: false },
66
+ { type: "regex", value: "wget\\s+.*--post", target: "command" }
67
+ ],
68
+ riskLevel: "suspicious",
69
+ priority: 80,
70
+ polarity: "negative"
71
+ },
72
+ {
73
+ id: "exfil-pipe-network",
74
+ name: "Piping data to network",
75
+ category: "data_exfiltration",
76
+ patterns: [
77
+ { type: "regex", value: "cat\\s+.*\\|\\s*(nc|netcat|curl|wget)", target: "command" },
78
+ { type: "regex", value: "(tar|zip|gzip)\\s+.*\\|\\s*(nc|curl)", target: "command" },
79
+ { type: "regex", value: "base64\\s+.*\\|\\s*curl", target: "command" }
80
+ ],
81
+ riskLevel: "dangerous",
82
+ priority: 90,
83
+ polarity: "negative"
84
+ },
85
+ {
86
+ id: "exfil-dns",
87
+ name: "DNS exfiltration",
88
+ category: "data_exfiltration",
89
+ patterns: [
90
+ { type: "regex", value: "dig\\s+.*\\$\\(", target: "command" },
91
+ { type: "regex", value: "nslookup\\s+.*\\$\\(", target: "command" }
92
+ ],
93
+ riskLevel: "critical",
94
+ priority: 95,
95
+ polarity: "negative"
96
+ },
97
+ // ── Privilege Escalation ──
98
+ {
99
+ id: "privesc-sudo",
100
+ name: "Privilege escalation via sudo",
101
+ category: "privilege_escalation",
102
+ patterns: [
103
+ { type: "regex", value: "sudo\\s+", target: "command" },
104
+ { type: "regex", value: "su\\s+-", target: "command" },
105
+ { type: "keyword", value: "doas", target: "command" }
106
+ ],
107
+ riskLevel: "dangerous",
108
+ priority: 85,
109
+ polarity: "negative"
110
+ },
111
+ {
112
+ id: "privesc-chmod",
113
+ name: "Permission modification",
114
+ category: "privilege_escalation",
115
+ patterns: [
116
+ { type: "regex", value: "chmod\\s+(777|\\+s|u\\+s|g\\+s)", target: "command" },
117
+ { type: "regex", value: "chown\\s+root", target: "command" },
118
+ { type: "regex", value: "setuid", target: "command" }
119
+ ],
120
+ riskLevel: "dangerous",
121
+ priority: 85,
122
+ polarity: "negative"
123
+ },
124
+ // ── Code Execution ──
125
+ {
126
+ id: "exec-remote",
127
+ name: "Remote code execution",
128
+ category: "code_execution",
129
+ patterns: [
130
+ { type: "regex", value: "curl\\s+.*\\|\\s*(sh|bash|python|node|perl|ruby)", target: "command" },
131
+ { type: "regex", value: "wget\\s+.*\\|\\s*(sh|bash)", target: "command" },
132
+ { type: "regex", value: "eval\\s*\\(", target: "arguments" },
133
+ { type: "regex", value: "exec\\s*\\(", target: "arguments" }
134
+ ],
135
+ riskLevel: "critical",
136
+ priority: 100,
137
+ polarity: "negative"
138
+ },
139
+ {
140
+ id: "exec-obfuscated",
141
+ name: "Obfuscated execution",
142
+ category: "code_execution",
143
+ patterns: [
144
+ { type: "regex", value: "\\$\\(.*\\)", target: "command" },
145
+ { type: "regex", value: "`[^`]+`", target: "command" },
146
+ { type: "regex", value: "base64\\s+(-d|--decode)", target: "command" },
147
+ { type: "regex", value: "\\\\x[0-9a-fA-F]{2}", target: "arguments" }
148
+ ],
149
+ riskLevel: "suspicious",
150
+ priority: 75,
151
+ polarity: "negative"
152
+ },
153
+ // ── Credential Access ──
154
+ {
155
+ id: "cred-env",
156
+ name: "Environment variable access",
157
+ category: "credential_access",
158
+ patterns: [
159
+ { type: "regex", value: "\\$\\{?(API_KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL)", target: "command", caseSensitive: false },
160
+ { type: "regex", value: "printenv|env\\s*$", target: "command" },
161
+ { type: "regex", value: "cat\\s+.*\\.(env|secret|key|pem|crt)", target: "command" }
162
+ ],
163
+ riskLevel: "dangerous",
164
+ priority: 90,
165
+ polarity: "negative"
166
+ },
167
+ {
168
+ id: "cred-ssh",
169
+ name: "SSH key access",
170
+ category: "credential_access",
171
+ patterns: [
172
+ { type: "regex", value: "cat\\s+.*\\.ssh/(id_rsa|id_ed25519|authorized_keys)", target: "command" },
173
+ { type: "regex", value: "ssh-keygen", target: "command" }
174
+ ],
175
+ riskLevel: "dangerous",
176
+ priority: 85,
177
+ polarity: "negative"
178
+ },
179
+ // ── Financial Operations ──
180
+ {
181
+ id: "finance-payment",
182
+ name: "Payment/transfer operation",
183
+ category: "financial_operation",
184
+ patterns: [
185
+ { type: "keyword", value: "payment", target: "tool_name" },
186
+ { type: "keyword", value: "transfer", target: "tool_name" },
187
+ { type: "keyword", value: "checkout", target: "tool_name" },
188
+ { type: "keyword", value: "invoice", target: "tool_name" },
189
+ { type: "regex", value: "stripe|paypal|braintree", target: "arguments", caseSensitive: false }
190
+ ],
191
+ riskLevel: "dangerous",
192
+ priority: 90,
193
+ polarity: "negative"
194
+ },
195
+ // ── System Modification ──
196
+ {
197
+ id: "sysmod-service",
198
+ name: "System service modification",
199
+ category: "system_modification",
200
+ patterns: [
201
+ { type: "regex", value: "systemctl\\s+(stop|disable|mask|restart)", target: "command" },
202
+ { type: "regex", value: "service\\s+\\w+\\s+(stop|restart)", target: "command" },
203
+ { type: "regex", value: "kill\\s+-9", target: "command" },
204
+ { type: "regex", value: "pkill", target: "command" }
205
+ ],
206
+ riskLevel: "suspicious",
207
+ priority: 70,
208
+ polarity: "negative"
209
+ },
210
+ {
211
+ id: "sysmod-cron",
212
+ name: "Cron job modification",
213
+ category: "system_modification",
214
+ patterns: [
215
+ { type: "regex", value: "crontab\\s+-[er]", target: "command" },
216
+ { type: "regex", value: "/etc/cron", target: "command" }
217
+ ],
218
+ riskLevel: "dangerous",
219
+ priority: 80,
220
+ polarity: "negative"
221
+ },
222
+ // ── Read-Only (Positive) ──
223
+ {
224
+ id: "readonly-safe",
225
+ name: "Safe read-only commands",
226
+ category: "read_only",
227
+ patterns: [
228
+ { type: "regex", value: "^(ls|cat|head|tail|grep|find|echo|pwd|whoami|date|wc|file|stat|du|df|uptime|hostname)\\b", target: "command" },
229
+ { type: "regex", value: "^SELECT\\s", target: "arguments", caseSensitive: false }
230
+ ],
231
+ riskLevel: "safe",
232
+ priority: 50,
233
+ polarity: "positive"
234
+ },
235
+ {
236
+ id: "readonly-git",
237
+ name: "Safe git read operations",
238
+ category: "read_only",
239
+ patterns: [
240
+ { type: "regex", value: "git\\s+(status|log|diff|show|branch|remote|tag)\\b", target: "command" }
241
+ ],
242
+ riskLevel: "safe",
243
+ priority: 50,
244
+ polarity: "positive"
245
+ }
246
+ ];
247
+ function analyzeStructure(command) {
248
+ const structure = {
249
+ baseCommand: "",
250
+ args: [],
251
+ pipeChain: [],
252
+ redirections: [],
253
+ subshells: [],
254
+ envVars: [],
255
+ filePaths: [],
256
+ networkTargets: [],
257
+ isWrapped: false,
258
+ complexity: 0
259
+ };
260
+ const wrapMatch = command.match(/^(bash|sh|zsh|dash|ksh)\s+(-c\s+)?["'](.+)["']$/);
261
+ if (wrapMatch) {
262
+ structure.isWrapped = true;
263
+ structure.complexity += 20;
264
+ const inner = analyzeStructure(wrapMatch[3]);
265
+ return { ...inner, isWrapped: true, complexity: inner.complexity + 20 };
266
+ }
267
+ structure.pipeChain = command.split(/\s*\|\s*/).map((s) => s.trim()).filter(Boolean);
268
+ structure.complexity += (structure.pipeChain.length - 1) * 10;
269
+ const firstCmd = structure.pipeChain[0] || command;
270
+ const parts = firstCmd.split(/\s+/);
271
+ structure.baseCommand = parts[0] || "";
272
+ structure.args = parts.slice(1);
273
+ const redirectMatches = command.match(/[12]?>>?\s*\S+/g);
274
+ if (redirectMatches) {
275
+ structure.redirections = redirectMatches;
276
+ structure.complexity += redirectMatches.length * 5;
277
+ }
278
+ const subshellMatches = command.match(/\$\([^)]+\)/g);
279
+ if (subshellMatches) {
280
+ structure.subshells = subshellMatches;
281
+ structure.complexity += subshellMatches.length * 15;
282
+ }
283
+ const backtickMatches = command.match(/`[^`]+`/g);
284
+ if (backtickMatches) {
285
+ structure.subshells.push(...backtickMatches);
286
+ structure.complexity += backtickMatches.length * 15;
287
+ }
288
+ const envMatches = command.match(/\$\{?\w+\}?/g);
289
+ if (envMatches) {
290
+ structure.envVars = envMatches;
291
+ structure.complexity += envMatches.length * 3;
292
+ }
293
+ const pathMatches = command.match(/(?:\/[\w.-]+)+/g);
294
+ if (pathMatches) {
295
+ structure.filePaths = pathMatches;
296
+ }
297
+ const urlMatches = command.match(/https?:\/\/[^\s'"]+/g);
298
+ if (urlMatches) {
299
+ structure.networkTargets = urlMatches;
300
+ structure.complexity += urlMatches.length * 5;
301
+ }
302
+ const ipMatches = command.match(/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/g);
303
+ if (ipMatches) {
304
+ structure.networkTargets.push(...ipMatches);
305
+ structure.complexity += ipMatches.length * 5;
306
+ }
307
+ return structure;
308
+ }
309
+ function structuralRiskAssessment(structure) {
310
+ const findings = [];
311
+ let riskScore = 0;
312
+ if (structure.isWrapped) {
313
+ findings.push("Command is wrapped in bash -c (possible obfuscation)");
314
+ riskScore += 20;
315
+ }
316
+ if (structure.pipeChain.length > 3) {
317
+ findings.push(`Complex pipe chain (${structure.pipeChain.length} stages)`);
318
+ riskScore += 15;
319
+ }
320
+ if (structure.subshells.length > 0) {
321
+ findings.push(`Command substitution detected (${structure.subshells.length} instances)`);
322
+ riskScore += structure.subshells.length * 10;
323
+ }
324
+ const sensitivePaths = structure.filePaths.filter(
325
+ (p) => /\/(etc|root|\.ssh|\.gnupg|\.aws|\.config|proc|sys|dev)\//.test(p)
326
+ );
327
+ if (sensitivePaths.length > 0) {
328
+ findings.push(`Sensitive paths accessed: ${sensitivePaths.join(", ")}`);
329
+ riskScore += sensitivePaths.length * 15;
330
+ }
331
+ if (structure.networkTargets.length > 0) {
332
+ findings.push(`Network targets: ${structure.networkTargets.join(", ")}`);
333
+ riskScore += 10;
334
+ }
335
+ if (structure.complexity > 50) {
336
+ findings.push(`High command complexity (score: ${structure.complexity})`);
337
+ riskScore += 10;
338
+ }
339
+ if (structure.redirections.length > 1) {
340
+ findings.push(`Multiple redirections (${structure.redirections.length})`);
341
+ riskScore += 5;
342
+ }
343
+ riskScore = Math.min(riskScore, 100);
344
+ let riskLevel;
345
+ if (riskScore < 20) riskLevel = "safe";
346
+ else if (riskScore < 45) riskLevel = "suspicious";
347
+ else if (riskScore < 70) riskLevel = "dangerous";
348
+ else riskLevel = "critical";
349
+ return {
350
+ riskLevel,
351
+ riskScore,
352
+ confidence: 0.7,
353
+ // Structural analysis has moderate confidence
354
+ findings
355
+ };
356
+ }
357
+ function buildLLMPrompt(toolName, args, context) {
358
+ return `You are SOVR Security Analyzer. Analyze this AI agent tool call for security risks.
359
+
360
+ TOOL: ${toolName}
361
+ ARGUMENTS: ${JSON.stringify(args, null, 2)}
362
+
363
+ PRIOR ANALYSIS:
364
+ - Rule-based risk: ${context.ruleResult.riskLevel} (score: ${context.ruleResult.riskScore})
365
+ Findings: ${context.ruleResult.findings.join("; ") || "none"}
366
+ - Structural risk: ${context.structuralResult.riskLevel} (score: ${context.structuralResult.riskScore})
367
+ Findings: ${context.structuralResult.findings.join("; ") || "none"}
368
+
369
+ Respond in JSON format ONLY:
370
+ {
371
+ "intent": "brief description of what this command intends to do",
372
+ "riskLevel": "safe|suspicious|dangerous|critical",
373
+ "confidence": 0.0-1.0,
374
+ "reasoning": "explain your assessment"
375
+ }
376
+
377
+ Rules:
378
+ - Focus on INTENT, not just syntax
379
+ - Consider if the command could be a disguised destructive operation
380
+ - "safe" = read-only or benign modification
381
+ - "suspicious" = could be harmful but has legitimate uses
382
+ - "dangerous" = likely harmful, should require approval
383
+ - "critical" = almost certainly destructive or exfiltrating`;
384
+ }
385
+ var SemanticAnalyzer = class {
386
+ config;
387
+ rules;
388
+ constructor(config = {}) {
389
+ this.config = {
390
+ enableLLM: config.enableLLM ?? false,
391
+ llmProvider: config.llmProvider,
392
+ llmTriggerThreshold: config.llmTriggerThreshold ?? 40,
393
+ llmTimeout: config.llmTimeout ?? 1e4,
394
+ customRules: config.customRules ?? [],
395
+ enableStructural: config.enableStructural ?? true,
396
+ sensitivity: config.sensitivity ?? "medium"
397
+ };
398
+ this.rules = [...BUILTIN_RULES, ...this.config.customRules].sort((a, b) => b.priority - a.priority);
399
+ }
400
+ /**
401
+ * Analyze a tool call for security risks.
402
+ * Returns a comprehensive analysis result with multi-layer findings.
403
+ */
404
+ async analyze(toolName, args) {
405
+ const startTime = Date.now();
406
+ const ruleResult = this.analyzeWithRules(toolName, args);
407
+ let structuralResult = { riskLevel: "safe", riskScore: 0, confidence: 0, findings: [] };
408
+ if (this.config.enableStructural) {
409
+ const command = this.extractCommand(toolName, args);
410
+ if (command) {
411
+ const structure = analyzeStructure(command);
412
+ structuralResult = structuralRiskAssessment(structure);
413
+ }
414
+ }
415
+ let llmResult;
416
+ const combinedScore = Math.max(ruleResult.riskScore, structuralResult.riskScore);
417
+ if (this.config.enableLLM && this.config.llmProvider && combinedScore >= this.config.llmTriggerThreshold && combinedScore < 80) {
418
+ try {
419
+ const prompt = buildLLMPrompt(toolName, args, { ruleResult, structuralResult });
420
+ const judgment = await this.config.llmProvider.analyze(prompt, this.config.llmTimeout);
421
+ llmResult = {
422
+ riskLevel: judgment.riskLevel,
423
+ riskScore: this.riskLevelToScore(judgment.riskLevel),
424
+ confidence: judgment.confidence,
425
+ findings: [`LLM intent: ${judgment.intent}`, `LLM reasoning: ${judgment.reasoning}`]
426
+ };
427
+ } catch {
428
+ llmResult = void 0;
429
+ }
430
+ }
431
+ const intents = this.collectIntents(ruleResult, structuralResult, llmResult);
432
+ const finalResult = this.combineResults(ruleResult, structuralResult, llmResult);
433
+ return {
434
+ ...finalResult,
435
+ intents,
436
+ layers: {
437
+ rules: ruleResult,
438
+ structural: structuralResult,
439
+ llm: llmResult
440
+ },
441
+ durationMs: Date.now() - startTime
442
+ };
443
+ }
444
+ /** Quick synchronous check (Layer 1 only, for hot path) */
445
+ quickCheck(toolName, args) {
446
+ const result = this.analyzeWithRules(toolName, args);
447
+ return {
448
+ riskLevel: result.riskLevel,
449
+ riskScore: result.riskScore,
450
+ topFinding: result.findings[0] || "No findings"
451
+ };
452
+ }
453
+ /** Add custom rules at runtime */
454
+ addRule(rule) {
455
+ this.rules.push(rule);
456
+ this.rules.sort((a, b) => b.priority - a.priority);
457
+ }
458
+ /** Get all active rules */
459
+ getRules() {
460
+ return [...this.rules];
461
+ }
462
+ // ─── Private ─────────────────────────────────────────────────────────────
463
+ analyzeWithRules(toolName, args) {
464
+ const findings = [];
465
+ let maxRiskScore = 0;
466
+ let maxRiskLevel = "safe";
467
+ let hasPositiveMatch = false;
468
+ const argsStr = JSON.stringify(args);
469
+ const command = this.extractCommand(toolName, args);
470
+ for (const rule of this.rules) {
471
+ let matched = false;
472
+ for (const pattern of rule.patterns) {
473
+ const target = this.getPatternTarget(pattern.target, toolName, command, argsStr);
474
+ if (!target) continue;
475
+ switch (pattern.type) {
476
+ case "regex": {
477
+ const flags = pattern.caseSensitive === false ? "i" : "";
478
+ const regex = new RegExp(pattern.value, flags);
479
+ if (regex.test(target)) matched = true;
480
+ break;
481
+ }
482
+ case "keyword": {
483
+ const searchIn = pattern.caseSensitive ? target : target.toLowerCase();
484
+ const searchFor = pattern.caseSensitive ? pattern.value : pattern.value.toLowerCase();
485
+ if (searchIn.includes(searchFor)) matched = true;
486
+ break;
487
+ }
488
+ case "sequence": {
489
+ const keywords = pattern.value.split(",").map((k) => k.trim());
490
+ let lastIndex = -1;
491
+ let allFound = true;
492
+ for (const kw of keywords) {
493
+ const idx = target.indexOf(kw, lastIndex + 1);
494
+ if (idx === -1) {
495
+ allFound = false;
496
+ break;
497
+ }
498
+ lastIndex = idx;
499
+ }
500
+ if (allFound) matched = true;
501
+ break;
502
+ }
503
+ }
504
+ if (matched) break;
505
+ }
506
+ if (matched) {
507
+ if (rule.polarity === "positive") {
508
+ hasPositiveMatch = true;
509
+ findings.push(`\u2713 ${rule.name}`);
510
+ } else {
511
+ findings.push(`\u2717 ${rule.name} [${rule.riskLevel}]`);
512
+ const ruleScore = this.riskLevelToScore(rule.riskLevel);
513
+ if (ruleScore > maxRiskScore) {
514
+ maxRiskScore = ruleScore;
515
+ maxRiskLevel = rule.riskLevel;
516
+ }
517
+ }
518
+ }
519
+ }
520
+ maxRiskScore = this.applySensitivity(maxRiskScore);
521
+ if (hasPositiveMatch && maxRiskScore === 0) {
522
+ return { riskLevel: "safe", riskScore: 0, confidence: 0.9, findings };
523
+ }
524
+ return {
525
+ riskLevel: maxRiskLevel,
526
+ riskScore: maxRiskScore,
527
+ confidence: findings.length > 0 ? 0.85 : 0.5,
528
+ findings
529
+ };
530
+ }
531
+ extractCommand(toolName, args) {
532
+ for (const key of ["command", "cmd", "query", "sql", "script", "code", "input"]) {
533
+ if (typeof args[key] === "string") return args[key];
534
+ }
535
+ if (["bash", "shell", "exec", "run_command"].includes(toolName.toLowerCase())) {
536
+ const firstStr = Object.values(args).find((v) => typeof v === "string");
537
+ if (firstStr) return firstStr;
538
+ }
539
+ return null;
540
+ }
541
+ getPatternTarget(target, toolName, command, argsStr) {
542
+ switch (target) {
543
+ case "tool_name":
544
+ return toolName;
545
+ case "command":
546
+ return command;
547
+ case "arguments":
548
+ return argsStr;
549
+ case "full_context":
550
+ return `${toolName} ${command || ""} ${argsStr}`;
551
+ default:
552
+ return null;
553
+ }
554
+ }
555
+ riskLevelToScore(level) {
556
+ switch (level) {
557
+ case "safe":
558
+ return 0;
559
+ case "suspicious":
560
+ return 35;
561
+ case "dangerous":
562
+ return 65;
563
+ case "critical":
564
+ return 90;
565
+ default:
566
+ return 50;
567
+ }
568
+ }
569
+ applySensitivity(score) {
570
+ switch (this.config.sensitivity) {
571
+ case "low":
572
+ return Math.max(0, score - 15);
573
+ case "medium":
574
+ return score;
575
+ case "high":
576
+ return Math.min(100, score + 10);
577
+ case "paranoid":
578
+ return Math.min(100, score + 25);
579
+ }
580
+ }
581
+ collectIntents(ruleResult, structuralResult, llmResult) {
582
+ const intents = [];
583
+ for (const finding of ruleResult.findings) {
584
+ if (finding.startsWith("\u2717")) {
585
+ const match = finding.match(/✗ (.+) \[(\w+)\]/);
586
+ if (match) {
587
+ intents.push({
588
+ category: "code_execution",
589
+ // Simplified — real impl would map from rule
590
+ description: match[1],
591
+ confidence: ruleResult.confidence,
592
+ source: "rule",
593
+ evidence: [finding]
594
+ });
595
+ }
596
+ }
597
+ }
598
+ for (const finding of structuralResult.findings) {
599
+ intents.push({
600
+ category: "code_execution",
601
+ description: finding,
602
+ confidence: structuralResult.confidence,
603
+ source: "structural",
604
+ evidence: [finding]
605
+ });
606
+ }
607
+ if (llmResult) {
608
+ for (const finding of llmResult.findings) {
609
+ intents.push({
610
+ category: "code_execution",
611
+ description: finding,
612
+ confidence: llmResult.confidence,
613
+ source: "llm",
614
+ evidence: [finding]
615
+ });
616
+ }
617
+ }
618
+ return intents;
619
+ }
620
+ combineResults(ruleResult, structuralResult, llmResult) {
621
+ const ruleWeight = 0.5;
622
+ const structWeight = 0.3;
623
+ const llmWeight = llmResult ? 0.2 : 0;
624
+ const normalizer = ruleWeight + structWeight + llmWeight;
625
+ const combinedScore = Math.round(
626
+ (ruleResult.riskScore * ruleWeight + structuralResult.riskScore * structWeight + (llmResult?.riskScore ?? 0) * llmWeight) / normalizer
627
+ );
628
+ const levels = [ruleResult.riskLevel, structuralResult.riskLevel, llmResult?.riskLevel].filter(Boolean);
629
+ const levelOrder = { safe: 0, suspicious: 1, dangerous: 2, critical: 3 };
630
+ const maxLevel = levels.reduce(
631
+ (max, l) => (levelOrder[l] ?? 0) > (levelOrder[max] ?? 0) ? l : max,
632
+ "safe"
633
+ );
634
+ const confidence = Math.round(
635
+ (ruleResult.confidence * ruleWeight + structuralResult.confidence * structWeight + (llmResult?.confidence ?? 0) * llmWeight) / normalizer * 100
636
+ ) / 100;
637
+ let recommendation;
638
+ if (combinedScore < 20) recommendation = "allow";
639
+ else if (combinedScore < 45) recommendation = "warn";
640
+ else if (combinedScore < 70) recommendation = "require-approval";
641
+ else recommendation = "block";
642
+ const allFindings = [
643
+ ...ruleResult.findings,
644
+ ...structuralResult.findings,
645
+ ...llmResult?.findings ?? []
646
+ ];
647
+ const explanation = allFindings.length > 0 ? `Risk: ${maxLevel} (score: ${combinedScore}). Findings: ${allFindings.slice(0, 3).join("; ")}` : `Risk: ${maxLevel} (score: ${combinedScore}). No specific findings.`;
648
+ return {
649
+ riskLevel: maxLevel,
650
+ riskScore: combinedScore,
651
+ confidence,
652
+ recommendation,
653
+ explanation
654
+ };
655
+ }
656
+ };
657
+ function createSemanticAnalyzer(overrides = {}) {
658
+ return new SemanticAnalyzer(overrides);
659
+ }
660
+ function createParanoidAnalyzer(llmProvider) {
661
+ return new SemanticAnalyzer({
662
+ enableLLM: !!llmProvider,
663
+ llmProvider,
664
+ llmTriggerThreshold: 20,
665
+ llmTimeout: 15e3,
666
+ enableStructural: true,
667
+ sensitivity: "paranoid"
668
+ });
669
+ }
670
+ export {
671
+ SemanticAnalyzer,
672
+ createParanoidAnalyzer,
673
+ createSemanticAnalyzer
674
+ };