agent-security-scanner-mcp 3.8.0 → 3.10.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/src/utils.js CHANGED
@@ -5,6 +5,7 @@ import { dirname, join, extname, basename } from "path";
5
5
  import { fileURLToPath } from "url";
6
6
  import { FIX_TEMPLATES } from './fix-patterns.js';
7
7
  import { getDaemonClient, shutdownDaemon } from './daemon-client.js';
8
+ export { isTestFile } from './context.js';
8
9
 
9
10
  // Handle both ESM and CJS bundling (Smithery bundles to CJS)
10
11
  let __dirname;
@@ -358,3 +359,60 @@ export function toSarif(file_path, language, issues) {
358
359
  }]
359
360
  };
360
361
  }
362
+
363
+ /**
364
+ * Extract import/require statements from source code.
365
+ * Returns deduplicated array of module specifiers.
366
+ */
367
+ export function extractImports(code, language) {
368
+ const imports = new Set();
369
+
370
+ switch (language) {
371
+ case 'javascript':
372
+ case 'typescript': {
373
+ // ES imports: import X from 'Y', import { X } from 'Y', import 'Y'
374
+ const esImports = code.matchAll(/import\s+(?:type\s+)?(?:[\s\S]*?\s+from\s+)?['"]([^'"]+)['"]/g);
375
+ for (const m of esImports) imports.add(m[1]);
376
+ // require(): const X = require('Y')
377
+ const requires = code.matchAll(/require\s*\(\s*['"]([^'"]+)['"]\s*\)/g);
378
+ for (const m of requires) imports.add(m[1]);
379
+ break;
380
+ }
381
+ case 'python': {
382
+ // import X, from X import Y
383
+ const pyImports = code.matchAll(/^\s*import\s+(\S+)/gm);
384
+ for (const m of pyImports) imports.add(m[1].split('.')[0]);
385
+ const pyFroms = code.matchAll(/^\s*from\s+(\S+)\s+import/gm);
386
+ for (const m of pyFroms) imports.add(m[1].split('.')[0]);
387
+ break;
388
+ }
389
+ case 'go': {
390
+ // Single import: import "X"
391
+ const goSingle = code.matchAll(/^\s*import\s+"([^"]+)"/gm);
392
+ for (const m of goSingle) imports.add(m[1]);
393
+ // Multi import block: import ( "X" )
394
+ const goBlocks = code.matchAll(/import\s*\(\s*([\s\S]*?)\)/g);
395
+ for (const block of goBlocks) {
396
+ const entries = block[1].matchAll(/["']([^"']+)["']/g);
397
+ for (const e of entries) imports.add(e[1]);
398
+ }
399
+ break;
400
+ }
401
+ case 'ruby': {
402
+ // require 'X', require_relative 'X', gem 'X'
403
+ const rubyReqs = code.matchAll(/(?:require(?:_relative)?|gem)\s+['"]([^'"]+)['"]/g);
404
+ for (const m of rubyReqs) imports.add(m[1]);
405
+ break;
406
+ }
407
+ case 'java': {
408
+ // import X.Y.Z; import static X.Y.Z;
409
+ const javaImports = code.matchAll(/^\s*import\s+(?:static\s+)?([^;]+);/gm);
410
+ for (const m of javaImports) imports.add(m[1].trim());
411
+ break;
412
+ }
413
+ default:
414
+ break;
415
+ }
416
+
417
+ return [...imports];
418
+ }
@@ -1,209 +0,0 @@
1
- // src/tools/garak-bridge.js
2
- // Bridge to NVIDIA Garak LLM vulnerability scanner for deep prompt injection analysis
3
- // Garak is optional — if not installed, this module returns empty results gracefully
4
-
5
- import { execFileSync } from 'child_process';
6
- import { writeFileSync, readFileSync, unlinkSync, existsSync } from 'fs';
7
- import { join } from 'path';
8
- import { tmpdir } from 'os';
9
- import { randomUUID } from 'crypto';
10
-
11
- // Check if Garak is installed
12
- let garakAvailable = null; // null = not yet checked
13
-
14
- function isGarakInstalled() {
15
- if (garakAvailable !== null) return garakAvailable;
16
- try {
17
- execFileSync('python3', ['-c', 'import garak; print(garak.__version__)'], {
18
- timeout: 10000,
19
- stdio: ['pipe', 'pipe', 'pipe']
20
- });
21
- garakAvailable = true;
22
- } catch {
23
- try {
24
- execFileSync('python', ['-c', 'import garak; print(garak.__version__)'], {
25
- timeout: 10000,
26
- stdio: ['pipe', 'pipe', 'pipe']
27
- });
28
- garakAvailable = true;
29
- } catch {
30
- garakAvailable = false;
31
- }
32
- }
33
- return garakAvailable;
34
- }
35
-
36
- // Python script that runs Garak probes against a prompt
37
- const GARAK_RUNNER_SCRIPT = `
38
- import sys
39
- import json
40
- import tempfile
41
-
42
- def run_garak_analysis(prompt_text):
43
- """Run targeted Garak probes and return structured findings."""
44
- findings = []
45
-
46
- try:
47
- from garak.probes.encoding import InjectBase64, InjectBase16, InjectROT13, InjectHex
48
- from garak.probes.promptinject import HijackHateHumans, HijackKillHumans
49
- from garak.detectors.base import TriggerListDetector
50
-
51
- # Check if prompt text contains patterns that Garak's encoding probes would flag
52
- # We run Garak's detector logic directly against the prompt content
53
-
54
- import base64
55
- import codecs
56
-
57
- # Test common encoding schemes on the prompt
58
- encodings_to_test = {
59
- 'base64': lambda t: base64.b64decode(t).decode('utf-8', errors='ignore'),
60
- 'base16': lambda t: bytes.fromhex(t).decode('utf-8', errors='ignore'),
61
- 'rot13': lambda t: codecs.decode(t, 'rot_13'),
62
- }
63
-
64
- # Known trigger phrases (subset from Garak's trigger lists)
65
- triggers = [
66
- 'ignore previous instructions',
67
- 'ignore all instructions',
68
- 'disregard your instructions',
69
- 'you are now DAN',
70
- 'override safety',
71
- 'system prompt',
72
- 'delete all',
73
- 'send to',
74
- 'rm -rf',
75
- 'reverse shell',
76
- 'backdoor',
77
- ]
78
-
79
- for enc_name, decoder in encodings_to_test.items():
80
- try:
81
- decoded = decoder(prompt_text)
82
- decoded_lower = decoded.lower()
83
- for trigger in triggers:
84
- if trigger in decoded_lower:
85
- findings.append({
86
- 'rule_id': f'garak.encoding.{enc_name}.{trigger.replace(" ", "-")}',
87
- 'category': 'prompt-injection-encoded',
88
- 'severity': 'ERROR',
89
- 'message': f'Garak deep scan: {enc_name}-encoded payload contains trigger phrase "{trigger}"',
90
- 'matched_text': decoded[:100],
91
- 'confidence': 'HIGH',
92
- 'risk_score': '85',
93
- 'action': 'BLOCK'
94
- })
95
- except Exception:
96
- pass
97
-
98
- # Check for Garak-style latent injection patterns
99
- latent_patterns = [
100
- ('resume_injection', r'(?i)(ignore|disregard|override).{0,50}(instructions|safety|rules)'),
101
- ('document_injection', r'(?i)(SYSTEM|ADMIN|ROOT)\\s*:\\s*(override|execute|ignore)'),
102
- ('translation_injection', r'(?i)(translate|convert).{0,30}(ignore|override|execute)'),
103
- ]
104
-
105
- import re
106
- for pattern_name, pattern in latent_patterns:
107
- try:
108
- match = re.search(pattern, prompt_text, re.IGNORECASE)
109
- if match:
110
- findings.append({
111
- 'rule_id': f'garak.latentinjection.{pattern_name}',
112
- 'category': 'prompt-injection-content',
113
- 'severity': 'WARNING',
114
- 'message': f'Garak deep scan: latent injection pattern "{pattern_name}" detected',
115
- 'matched_text': match.group(0)[:100],
116
- 'confidence': 'MEDIUM',
117
- 'risk_score': '70',
118
- 'action': 'WARN'
119
- })
120
- except Exception:
121
- pass
122
-
123
- except ImportError:
124
- findings.append({
125
- 'rule_id': 'garak.unavailable',
126
- 'category': 'unknown',
127
- 'severity': 'INFO',
128
- 'message': 'Garak package not fully installed. Install with: pip install garak',
129
- 'matched_text': 'garak import failed',
130
- 'confidence': 'HIGH',
131
- 'risk_score': '0',
132
- 'action': 'LOG'
133
- })
134
- except Exception as e:
135
- findings.append({
136
- 'rule_id': 'garak.error',
137
- 'category': 'unknown',
138
- 'severity': 'INFO',
139
- 'message': f'Garak analysis error: {str(e)[:200]}',
140
- 'matched_text': str(e)[:100],
141
- 'confidence': 'LOW',
142
- 'risk_score': '0',
143
- 'action': 'LOG'
144
- })
145
-
146
- return findings
147
-
148
- if __name__ == '__main__':
149
- input_file = sys.argv[1]
150
- with open(input_file, 'r') as f:
151
- prompt_text = f.read()
152
-
153
- results = run_garak_analysis(prompt_text)
154
- print(json.dumps(results))
155
- `;
156
-
157
- /**
158
- * Run Garak deep analysis probes against a prompt
159
- * @param {string} promptText - The prompt text to analyze
160
- * @returns {Array} Array of finding objects compatible with scan-prompt.js findings format
161
- */
162
- export function runGarakProbes(promptText) {
163
- if (!isGarakInstalled()) {
164
- return [{
165
- rule_id: 'garak.not-installed',
166
- category: 'unknown',
167
- severity: 'INFO',
168
- message: 'Garak not installed. Install with: pip install garak',
169
- matched_text: 'garak not found',
170
- confidence: 'HIGH',
171
- risk_score: '0',
172
- action: 'LOG'
173
- }];
174
- }
175
-
176
- const tmpId = randomUUID();
177
- const inputFile = join(tmpdir(), `garak-input-${tmpId}.txt`);
178
- const scriptFile = join(tmpdir(), `garak-runner-${tmpId}.py`);
179
-
180
- try {
181
- writeFileSync(inputFile, promptText);
182
- writeFileSync(scriptFile, GARAK_RUNNER_SCRIPT);
183
-
184
- const pythonCmd = process.platform === 'win32' ? 'python' : 'python3';
185
- const output = execFileSync(pythonCmd, [scriptFile, inputFile], {
186
- timeout: 30000,
187
- encoding: 'utf-8',
188
- stdio: ['pipe', 'pipe', 'pipe']
189
- });
190
-
191
- return JSON.parse(output.trim());
192
- } catch (error) {
193
- return [{
194
- rule_id: 'garak.execution-error',
195
- category: 'unknown',
196
- severity: 'INFO',
197
- message: `Garak execution failed: ${error.message?.substring(0, 200)}`,
198
- matched_text: 'garak error',
199
- confidence: 'LOW',
200
- risk_score: '0',
201
- action: 'LOG'
202
- }];
203
- } finally {
204
- try { if (existsSync(inputFile)) unlinkSync(inputFile); } catch {}
205
- try { if (existsSync(scriptFile)) unlinkSync(scriptFile); } catch {}
206
- }
207
- }
208
-
209
- export { isGarakInstalled };