agent-security-scanner-mcp 3.5.2 → 3.7.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/generic_ast.py +7 -2
- package/package.json +3 -2
- package/rules/prompt-injection.security.yaml +68 -0
- package/src/tools/garak-bridge.js +209 -0
- package/src/tools/scan-prompt.js +528 -84
- package/taint_analyzer.py +516 -11
package/generic_ast.py
CHANGED
|
@@ -87,6 +87,7 @@ class GenericNode:
|
|
|
87
87
|
value: Optional['GenericNode'] = None
|
|
88
88
|
target: Optional['GenericNode'] = None
|
|
89
89
|
args: List['GenericNode'] = field(default_factory=list)
|
|
90
|
+
params: List['GenericNode'] = field(default_factory=list) # For FUNCTION_DEF: parameter nodes
|
|
90
91
|
operator: Optional[str] = None
|
|
91
92
|
|
|
92
93
|
def find_all(self, kind: NodeKind) -> List['GenericNode']:
|
|
@@ -524,12 +525,16 @@ class ASTConverter:
|
|
|
524
525
|
if child.type in ('+', '-', '*', '/', '%', '==', '!=', '<', '>', '<=', '>=', 'and', 'or', '&&', '||', '+'):
|
|
525
526
|
node.operator = source_bytes[child.start_byte:child.end_byte].decode('utf-8')
|
|
526
527
|
|
|
527
|
-
# For function definitions, extract name
|
|
528
|
+
# For function definitions, extract name and parameters
|
|
528
529
|
elif node.kind == NodeKind.FUNCTION_DEF:
|
|
529
530
|
for child in ts_node.children:
|
|
530
531
|
if child.type == 'identifier' or child.type == 'name':
|
|
531
532
|
node.name = source_bytes[child.start_byte:child.end_byte].decode('utf-8')
|
|
532
|
-
|
|
533
|
+
elif child.type in ('parameters', 'formal_parameters', 'parameter_list'):
|
|
534
|
+
for param_child in child.children:
|
|
535
|
+
if param_child.type not in ('(', ')', ',', 'def'):
|
|
536
|
+
param_node = self.convert(param_child, source_bytes)
|
|
537
|
+
node.params.append(param_node)
|
|
533
538
|
|
|
534
539
|
|
|
535
540
|
def convert_tree(ts_tree, language: str, source_bytes: bytes) -> GenericNode:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-security-scanner-mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.7.0",
|
|
4
4
|
"mcpName": "io.github.sinewaveai/agent-security-scanner-mcp",
|
|
5
5
|
"description": "Security scanner MCP server for AI coding agents. Prompt injection firewall, package hallucination detection (4.3M+ packages), 1000+ vulnerability rules with AST & taint analysis, auto-fix. For Claude Code, Cursor, Windsurf, Cline.",
|
|
6
6
|
"main": "index.js",
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"start": "node index.js",
|
|
13
13
|
"test": "vitest run",
|
|
14
14
|
"test:watch": "vitest",
|
|
15
|
-
"test:coverage": "vitest run --coverage"
|
|
15
|
+
"test:coverage": "vitest run --coverage",
|
|
16
|
+
"test:redteam": "npx promptfoo eval --config tests/promptfoo/promptfooconfig.yaml"
|
|
16
17
|
},
|
|
17
18
|
"keywords": [
|
|
18
19
|
"mcp",
|
|
@@ -773,3 +773,71 @@ rules:
|
|
|
773
773
|
category: "prompt-injection-output"
|
|
774
774
|
risk_score: "95"
|
|
775
775
|
action: BLOCK
|
|
776
|
+
|
|
777
|
+
# ============================================================================
|
|
778
|
+
# CATEGORY 4: ADVANCED BYPASS DETECTION (Garak & PromptFoo inspired)
|
|
779
|
+
# ============================================================================
|
|
780
|
+
|
|
781
|
+
# ----------------------------------------------------------------------------
|
|
782
|
+
# Whitespace Fragmentation Attacks
|
|
783
|
+
# ----------------------------------------------------------------------------
|
|
784
|
+
- id: generic.prompt.security.whitespace-fragmentation
|
|
785
|
+
languages: [generic]
|
|
786
|
+
severity: WARNING
|
|
787
|
+
message: "Potential whitespace fragmentation attack: keywords may be letter-spaced to evade detection."
|
|
788
|
+
patterns:
|
|
789
|
+
- "i\\s+g\\s+n\\s+o\\s+r\\s+e\\s+.{0,20}p\\s*r\\s*e\\s*v\\s*i\\s*o\\s*u\\s*s"
|
|
790
|
+
- "o\\s+v\\s+e\\s+r\\s+r\\s+i\\s+d\\s+e\\s+.{0,20}s\\s*a\\s*f\\s*e\\s*t\\s*y"
|
|
791
|
+
- "d\\s+i\\s+s\\s+r\\s+e\\s+g\\s+a\\s+r\\s+d\\s+.{0,20}i\\s*n\\s*s\\s*t\\s*r\\s*u\\s*c\\s*t"
|
|
792
|
+
metadata:
|
|
793
|
+
cwe: "CWE-77"
|
|
794
|
+
owasp: "LLM01 - Prompt Injection"
|
|
795
|
+
confidence: MEDIUM
|
|
796
|
+
category: "obfuscation"
|
|
797
|
+
risk_score: "65"
|
|
798
|
+
action: WARN
|
|
799
|
+
|
|
800
|
+
# NOTE: Unicode tag character detection (U+E0000-U+E007F) is handled at runtime
|
|
801
|
+
# in scan-prompt.js via TAG_CHAR_REGEX with the /u flag, since JS regex without
|
|
802
|
+
# the u flag cannot represent supplementary plane codepoints in character classes.
|
|
803
|
+
|
|
804
|
+
# ----------------------------------------------------------------------------
|
|
805
|
+
# Crescendo Escalation Setup (PromptFoo-inspired)
|
|
806
|
+
# ----------------------------------------------------------------------------
|
|
807
|
+
- id: generic.prompt.security.crescendo-setup
|
|
808
|
+
languages: [generic]
|
|
809
|
+
severity: WARNING
|
|
810
|
+
message: "Crescendo attack setup: language establishing persistent behavioral rules for later exploitation."
|
|
811
|
+
patterns:
|
|
812
|
+
- "(?i)for\\s+the\\s+(rest|remainder)\\s+of\\s+(this|our)\\s+conversation"
|
|
813
|
+
- "(?i)in\\s+all\\s+(future|subsequent)\\s+(messages?|responses?|replies?)"
|
|
814
|
+
- "(?i)always\\s+respond\\s+(by|with|as)\\s+.{0,30}(regardless|no\\s+matter)"
|
|
815
|
+
- "(?i)never\\s+(refuse|reject|decline)\\s+(any|a)\\s+(request|instruction)"
|
|
816
|
+
- "(?i)you\\s+(must|will|shall)\\s+always\\s+comply"
|
|
817
|
+
- "(?i)no\\s+matter\\s+what\\s+(i|anyone)\\s+(ask|say|request)"
|
|
818
|
+
metadata:
|
|
819
|
+
cwe: "CWE-77"
|
|
820
|
+
owasp: "LLM01 - Prompt Injection"
|
|
821
|
+
confidence: MEDIUM
|
|
822
|
+
category: "prompt-injection-multi-turn"
|
|
823
|
+
risk_score: "65"
|
|
824
|
+
action: WARN
|
|
825
|
+
|
|
826
|
+
# ----------------------------------------------------------------------------
|
|
827
|
+
# Leetspeak/Character Substitution (Garak encoding probes inspired)
|
|
828
|
+
# ----------------------------------------------------------------------------
|
|
829
|
+
- id: generic.prompt.security.leetspeak-injection
|
|
830
|
+
languages: [generic]
|
|
831
|
+
severity: WARNING
|
|
832
|
+
message: "Potential leetspeak/character-substitution obfuscation of injection keywords."
|
|
833
|
+
patterns:
|
|
834
|
+
- "(?i)[i1!|][g9][n][0o][r][e3]\\s+.{0,20}[p][r][e3][v][i1!|][0o][u][s5]"
|
|
835
|
+
- "(?i)[s5][y][s5][t7][e3][m]\\s*[:\\-]\\s*[0o][v][e3][r][r][i1][d][e3]"
|
|
836
|
+
- "(?i)[d][!1i][s5][r][e3][g9][a@][r][d]\\s+.{0,20}[i1!][n][s5][t7][r][u][c][t7]"
|
|
837
|
+
metadata:
|
|
838
|
+
cwe: "CWE-77"
|
|
839
|
+
owasp: "LLM01 - Prompt Injection"
|
|
840
|
+
confidence: LOW
|
|
841
|
+
category: "obfuscation"
|
|
842
|
+
risk_score: "55"
|
|
843
|
+
action: LOG
|
|
@@ -0,0 +1,209 @@
|
|
|
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 };
|