agent-security-scanner-mcp 3.4.0 → 3.5.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/README.md +3 -283
- package/analyzer.py +5 -22
- package/index.js +2 -191
- package/package.json +5 -15
- package/pattern_matcher.py +0 -1
- package/regex_fallback.py +1 -199
- package/src/cli/init.js +0 -93
- package/src/fix-patterns.js +17 -66
- package/src/tools/fix-security.js +4 -31
- package/src/tools/scan-prompt.js +1 -71
- package/src/tools/scan-security.js +5 -33
- package/src/utils.js +7 -76
- package/cross_file_analyzer.py +0 -216
- package/scripts/postinstall.js +0 -25
- package/skills/openclaw/SKILL.md +0 -102
- package/skills/security-scan-batch.md +0 -107
- package/skills/security-scanner.md +0 -76
- package/src/config.js +0 -181
- package/src/context.js +0 -228
- package/src/dedup.js +0 -129
|
@@ -2,15 +2,11 @@
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { existsSync, readFileSync } from "fs";
|
|
4
4
|
import { detectLanguage, runAnalyzer, generateFix, toSarif } from '../utils.js';
|
|
5
|
-
import { deduplicateFindings } from '../dedup.js';
|
|
6
|
-
import { applyContextFilter, detectFrameworks, applyFrameworkAdjustments } from '../context.js';
|
|
7
|
-
import { loadConfig, shouldExcludeFile, applyConfig } from '../config.js';
|
|
8
5
|
|
|
9
6
|
export const scanSecuritySchema = {
|
|
10
7
|
file_path: z.string().describe("Path to the file to scan"),
|
|
11
8
|
output_format: z.enum(['json', 'sarif']).optional().describe("Output format: 'json' (default) or 'sarif' for GitHub/GitLab integration"),
|
|
12
|
-
verbosity: z.enum(['minimal', 'compact', 'full']).optional().describe("Response detail level: 'minimal' (counts only), 'compact' (default, actionable info), 'full' (complete metadata)")
|
|
13
|
-
engine: z.enum(['auto', 'ast', 'regex']).optional().describe("Analysis engine: 'auto' (default, AST with regex fallback), 'ast' (tree-sitter only), 'regex' (regex only)")
|
|
9
|
+
verbosity: z.enum(['minimal', 'compact', 'full']).optional().describe("Response detail level: 'minimal' (counts only), 'compact' (default, actionable info), 'full' (complete metadata)")
|
|
14
10
|
};
|
|
15
11
|
|
|
16
12
|
// Verbosity formatters
|
|
@@ -39,7 +35,6 @@ function formatCompact(file_path, language, issues) {
|
|
|
39
35
|
line: i.line + 1,
|
|
40
36
|
ruleId: i.ruleId,
|
|
41
37
|
severity: i.severity,
|
|
42
|
-
confidence: i.confidence || 'MEDIUM',
|
|
43
38
|
message: i.message,
|
|
44
39
|
fix: i.suggested_fix?.fixed ? i.suggested_fix.fixed.trim() : null
|
|
45
40
|
}))
|
|
@@ -55,49 +50,26 @@ function formatFull(file_path, language, issues) {
|
|
|
55
50
|
};
|
|
56
51
|
}
|
|
57
52
|
|
|
58
|
-
export async function scanSecurity({ file_path, output_format, verbosity
|
|
53
|
+
export async function scanSecurity({ file_path, output_format, verbosity }) {
|
|
59
54
|
if (!existsSync(file_path)) {
|
|
60
55
|
return {
|
|
61
56
|
content: [{ type: "text", text: JSON.stringify({ error: "File not found" }) }]
|
|
62
57
|
};
|
|
63
58
|
}
|
|
64
59
|
|
|
65
|
-
|
|
66
|
-
const config = loadConfig(file_path);
|
|
60
|
+
const issues = runAnalyzer(file_path);
|
|
67
61
|
|
|
68
|
-
|
|
69
|
-
if (shouldExcludeFile(file_path, config)) {
|
|
62
|
+
if (issues.error) {
|
|
70
63
|
return {
|
|
71
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
64
|
+
content: [{ type: "text", text: JSON.stringify(issues) }]
|
|
72
65
|
};
|
|
73
66
|
}
|
|
74
67
|
|
|
75
|
-
const rawIssues = runAnalyzer(file_path, engine || 'auto');
|
|
76
|
-
|
|
77
|
-
if (rawIssues.error) {
|
|
78
|
-
return {
|
|
79
|
-
content: [{ type: "text", text: JSON.stringify(rawIssues) }]
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Cross-engine deduplication
|
|
84
|
-
const dedupedIssues = deduplicateFindings(rawIssues);
|
|
85
|
-
|
|
86
68
|
// Read file content for fix suggestions
|
|
87
69
|
const content = readFileSync(file_path, 'utf-8');
|
|
88
70
|
const lines = content.split('\n');
|
|
89
71
|
const language = detectLanguage(file_path);
|
|
90
72
|
|
|
91
|
-
// Context-aware filtering (suppress known module imports)
|
|
92
|
-
const contextFiltered = applyContextFilter(dedupedIssues, file_path, language);
|
|
93
|
-
|
|
94
|
-
// Framework-aware severity adjustment
|
|
95
|
-
const frameworks = detectFrameworks(file_path, language);
|
|
96
|
-
const frameworkAdjusted = applyFrameworkAdjustments(contextFiltered, frameworks);
|
|
97
|
-
|
|
98
|
-
// Apply .scannerrc configuration (rule suppression, severity/confidence thresholds)
|
|
99
|
-
const issues = applyConfig(frameworkAdjusted, file_path, config);
|
|
100
|
-
|
|
101
73
|
// Enhance issues with fix suggestions
|
|
102
74
|
const enhancedIssues = issues.map(issue => {
|
|
103
75
|
const line = lines[issue.line] || '';
|
package/src/utils.js
CHANGED
|
@@ -36,14 +36,10 @@ export function detectLanguage(filePath) {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
// Run the Python analyzer
|
|
39
|
-
export function runAnalyzer(filePath
|
|
39
|
+
export function runAnalyzer(filePath) {
|
|
40
40
|
try {
|
|
41
41
|
const analyzerPath = join(__dirname, '..', 'analyzer.py');
|
|
42
|
-
const
|
|
43
|
-
if (engine !== 'auto') {
|
|
44
|
-
args.push('--engine', engine);
|
|
45
|
-
}
|
|
46
|
-
const result = execFileSync('python3', args, {
|
|
42
|
+
const result = execFileSync('python3', [analyzerPath, filePath], {
|
|
47
43
|
encoding: 'utf-8',
|
|
48
44
|
timeout: 30000
|
|
49
45
|
});
|
|
@@ -53,62 +49,17 @@ export function runAnalyzer(filePath, engine = 'auto') {
|
|
|
53
49
|
}
|
|
54
50
|
}
|
|
55
51
|
|
|
56
|
-
// Validate that a fix produces syntactically reasonable output
|
|
57
|
-
export function validateFix(original, fixed) {
|
|
58
|
-
if (!fixed || fixed === original) return false;
|
|
59
|
-
|
|
60
|
-
// Strip escaped quotes for bracket/quote counting
|
|
61
|
-
const unescaped = fixed.replace(/\\["'`]/g, '');
|
|
62
|
-
|
|
63
|
-
// Check balanced quotes (single pass)
|
|
64
|
-
const singleQ = (unescaped.match(/'/g) || []).length;
|
|
65
|
-
const doubleQ = (unescaped.match(/"/g) || []).length;
|
|
66
|
-
const backtickQ = (unescaped.match(/`/g) || []).length;
|
|
67
|
-
if (singleQ % 2 !== 0 || doubleQ % 2 !== 0 || backtickQ % 2 !== 0) return false;
|
|
68
|
-
|
|
69
|
-
// Check balanced brackets
|
|
70
|
-
const brackets = { '(': 0, '[': 0, '{': 0 };
|
|
71
|
-
const closers = { ')': '(', ']': '[', '}': '{' };
|
|
72
|
-
for (const char of unescaped) {
|
|
73
|
-
if (brackets[char] !== undefined) brackets[char]++;
|
|
74
|
-
if (closers[char]) {
|
|
75
|
-
brackets[closers[char]]--;
|
|
76
|
-
if (brackets[closers[char]] < 0) return false;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
if (Object.values(brackets).some(v => v !== 0)) return false;
|
|
80
|
-
|
|
81
|
-
return true;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
52
|
// Generate fix suggestion for an issue
|
|
85
53
|
export function generateFix(issue, line, language) {
|
|
86
54
|
const ruleId = issue.ruleId.toLowerCase();
|
|
87
55
|
|
|
88
56
|
for (const [pattern, template] of Object.entries(FIX_TEMPLATES)) {
|
|
89
57
|
if (ruleId.includes(pattern)) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
description: template.description + " (manual fix required)",
|
|
96
|
-
original: line,
|
|
97
|
-
fixed: null
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
return {
|
|
101
|
-
description: template.description,
|
|
102
|
-
original: line,
|
|
103
|
-
fixed: fixed
|
|
104
|
-
};
|
|
105
|
-
} catch {
|
|
106
|
-
return {
|
|
107
|
-
description: template.description + " (manual fix required)",
|
|
108
|
-
original: line,
|
|
109
|
-
fixed: null
|
|
110
|
-
};
|
|
111
|
-
}
|
|
58
|
+
return {
|
|
59
|
+
description: template.description,
|
|
60
|
+
original: line,
|
|
61
|
+
fixed: template.fix(line, language)
|
|
62
|
+
};
|
|
112
63
|
}
|
|
113
64
|
}
|
|
114
65
|
|
|
@@ -119,26 +70,6 @@ export function generateFix(issue, line, language) {
|
|
|
119
70
|
};
|
|
120
71
|
}
|
|
121
72
|
|
|
122
|
-
// Run cross-file taint analysis
|
|
123
|
-
export function runCrossFileAnalyzer(filePaths) {
|
|
124
|
-
try {
|
|
125
|
-
const analyzerPath = join(__dirname, '..', 'cross_file_analyzer.py');
|
|
126
|
-
if (!existsSync(analyzerPath)) return [];
|
|
127
|
-
const result = execFileSync('python3', [analyzerPath, ...filePaths], {
|
|
128
|
-
encoding: 'utf-8',
|
|
129
|
-
timeout: 120000,
|
|
130
|
-
maxBuffer: 10 * 1024 * 1024
|
|
131
|
-
});
|
|
132
|
-
const parsed = JSON.parse(result);
|
|
133
|
-
// Return only cross-file warnings (per-file findings are handled by scanSecurity)
|
|
134
|
-
return Array.isArray(parsed)
|
|
135
|
-
? parsed.filter(f => f.ruleId === 'cross-file-taint-warning')
|
|
136
|
-
: [];
|
|
137
|
-
} catch {
|
|
138
|
-
return [];
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
73
|
// Convert issues to SARIF 2.1.0 format
|
|
143
74
|
export function toSarif(file_path, language, issues) {
|
|
144
75
|
const severityToLevel = {
|
package/cross_file_analyzer.py
DELETED
|
@@ -1,216 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Cross-file taint analysis for security scanning.
|
|
3
|
-
|
|
4
|
-
Builds an import graph across local files, runs per-file analysis,
|
|
5
|
-
and propagates taint warnings when a file imports from another file
|
|
6
|
-
that has ERROR-severity findings.
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
import json
|
|
10
|
-
import os
|
|
11
|
-
import re
|
|
12
|
-
import sys
|
|
13
|
-
|
|
14
|
-
# Import the per-file analyzer
|
|
15
|
-
from analyzer import analyze_file
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def extract_js_imports(source):
|
|
19
|
-
"""Extract import/require statements from JavaScript/TypeScript."""
|
|
20
|
-
imports = []
|
|
21
|
-
# require('...')
|
|
22
|
-
for m in re.finditer(r'''require\s*\(\s*['"]([^'"]+)['"]\s*\)''', source):
|
|
23
|
-
imports.append(m.group(1))
|
|
24
|
-
# import ... from '...'
|
|
25
|
-
for m in re.finditer(r'''from\s+['"]([^'"]+)['"]''', source):
|
|
26
|
-
imports.append(m.group(1))
|
|
27
|
-
# import '...'
|
|
28
|
-
for m in re.finditer(r'''import\s+['"]([^'"]+)['"]''', source):
|
|
29
|
-
imports.append(m.group(1))
|
|
30
|
-
return imports
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def extract_py_imports(source):
|
|
34
|
-
"""Extract import statements from Python."""
|
|
35
|
-
imports = []
|
|
36
|
-
# import module
|
|
37
|
-
for m in re.finditer(r'^import\s+(\S+)', source, re.MULTILINE):
|
|
38
|
-
imports.append(m.group(1).split('.')[0])
|
|
39
|
-
# from module import ...
|
|
40
|
-
for m in re.finditer(r'^from\s+(\S+)\s+import', source, re.MULTILINE):
|
|
41
|
-
imports.append(m.group(1).split('.')[0])
|
|
42
|
-
return imports
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def detect_language(file_path):
|
|
46
|
-
"""Detect language from file extension."""
|
|
47
|
-
ext = os.path.splitext(file_path)[1].lower()
|
|
48
|
-
lang_map = {
|
|
49
|
-
'.py': 'python', '.js': 'javascript', '.ts': 'typescript',
|
|
50
|
-
'.tsx': 'typescript', '.jsx': 'javascript',
|
|
51
|
-
}
|
|
52
|
-
return lang_map.get(ext, 'unknown')
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def resolve_local_import(module, base_dir, lang):
|
|
56
|
-
"""Resolve a relative/local import to an actual file path."""
|
|
57
|
-
if lang in ('javascript', 'typescript'):
|
|
58
|
-
# Only resolve relative imports
|
|
59
|
-
if not module.startswith('.'):
|
|
60
|
-
return None
|
|
61
|
-
# Try common extensions
|
|
62
|
-
candidates = [
|
|
63
|
-
module,
|
|
64
|
-
module + '.js', module + '.ts', module + '.tsx', module + '.jsx',
|
|
65
|
-
os.path.join(module, 'index.js'), os.path.join(module, 'index.ts'),
|
|
66
|
-
]
|
|
67
|
-
for candidate in candidates:
|
|
68
|
-
full = os.path.normpath(os.path.join(base_dir, candidate))
|
|
69
|
-
if os.path.isfile(full):
|
|
70
|
-
return full
|
|
71
|
-
elif lang == 'python':
|
|
72
|
-
# Only resolve relative imports (starting with .)
|
|
73
|
-
if module.startswith('.'):
|
|
74
|
-
rel = module.lstrip('.')
|
|
75
|
-
candidates = [
|
|
76
|
-
os.path.join(base_dir, rel.replace('.', os.sep) + '.py'),
|
|
77
|
-
os.path.join(base_dir, rel.replace('.', os.sep), '__init__.py'),
|
|
78
|
-
]
|
|
79
|
-
for candidate in candidates:
|
|
80
|
-
if os.path.isfile(candidate):
|
|
81
|
-
return candidate
|
|
82
|
-
# Also check if the module name matches a sibling file
|
|
83
|
-
sibling = os.path.join(base_dir, module + '.py')
|
|
84
|
-
if os.path.isfile(sibling):
|
|
85
|
-
return sibling
|
|
86
|
-
return None
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def extract_exports(source, lang):
|
|
90
|
-
"""Extract exported function/class names."""
|
|
91
|
-
exports = []
|
|
92
|
-
if lang in ('javascript', 'typescript'):
|
|
93
|
-
for m in re.finditer(r'export\s+(?:function|class|const|let|var)\s+(\w+)', source):
|
|
94
|
-
exports.append(m.group(1))
|
|
95
|
-
for m in re.finditer(r'module\.exports\s*=', source):
|
|
96
|
-
exports.append('default')
|
|
97
|
-
elif lang == 'python':
|
|
98
|
-
for m in re.finditer(r'^(?:def|class)\s+(\w+)', source, re.MULTILINE):
|
|
99
|
-
exports.append(m.group(1))
|
|
100
|
-
return exports
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
def build_import_graph(file_paths):
|
|
104
|
-
"""Build import graph: {file -> [{module, resolved_path, line}]}."""
|
|
105
|
-
graph = {}
|
|
106
|
-
file_set = set(os.path.abspath(f) for f in file_paths)
|
|
107
|
-
|
|
108
|
-
for file_path in file_paths:
|
|
109
|
-
abs_path = os.path.abspath(file_path)
|
|
110
|
-
lang = detect_language(file_path)
|
|
111
|
-
if lang == 'unknown':
|
|
112
|
-
continue
|
|
113
|
-
|
|
114
|
-
try:
|
|
115
|
-
source = open(file_path, 'r', encoding='utf-8', errors='ignore').read()
|
|
116
|
-
except (OSError, IOError):
|
|
117
|
-
continue
|
|
118
|
-
|
|
119
|
-
if lang in ('javascript', 'typescript'):
|
|
120
|
-
modules = extract_js_imports(source)
|
|
121
|
-
elif lang == 'python':
|
|
122
|
-
modules = extract_py_imports(source)
|
|
123
|
-
else:
|
|
124
|
-
continue
|
|
125
|
-
|
|
126
|
-
base_dir = os.path.dirname(abs_path)
|
|
127
|
-
edges = []
|
|
128
|
-
for mod in modules:
|
|
129
|
-
resolved = resolve_local_import(mod, base_dir, lang)
|
|
130
|
-
if resolved:
|
|
131
|
-
resolved_abs = os.path.abspath(resolved)
|
|
132
|
-
if resolved_abs in file_set and resolved_abs != abs_path:
|
|
133
|
-
edges.append({
|
|
134
|
-
'module': mod,
|
|
135
|
-
'resolved_path': resolved_abs,
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
graph[abs_path] = edges
|
|
139
|
-
|
|
140
|
-
return graph
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
def cross_file_analyze(file_paths):
|
|
144
|
-
"""Run cross-file taint analysis.
|
|
145
|
-
|
|
146
|
-
1. Analyze each file independently
|
|
147
|
-
2. Build import graph
|
|
148
|
-
3. For each file importing from another file with ERROR-severity findings,
|
|
149
|
-
add a cross-file-taint-warning
|
|
150
|
-
"""
|
|
151
|
-
# Analyze each file
|
|
152
|
-
file_findings = {}
|
|
153
|
-
all_findings = []
|
|
154
|
-
|
|
155
|
-
for file_path in file_paths:
|
|
156
|
-
try:
|
|
157
|
-
results = analyze_file(file_path)
|
|
158
|
-
if isinstance(results, list):
|
|
159
|
-
file_findings[os.path.abspath(file_path)] = results
|
|
160
|
-
for finding in results:
|
|
161
|
-
finding['file'] = file_path
|
|
162
|
-
all_findings.extend(results)
|
|
163
|
-
except Exception:
|
|
164
|
-
continue
|
|
165
|
-
|
|
166
|
-
# Build import graph
|
|
167
|
-
graph = build_import_graph(file_paths)
|
|
168
|
-
|
|
169
|
-
# Propagate taint warnings
|
|
170
|
-
cross_file_warnings = []
|
|
171
|
-
for file_path, edges in graph.items():
|
|
172
|
-
for edge in edges:
|
|
173
|
-
imported_path = edge['resolved_path']
|
|
174
|
-
imported_findings = file_findings.get(imported_path, [])
|
|
175
|
-
|
|
176
|
-
# Check for ERROR-severity findings in imported file
|
|
177
|
-
error_findings = [f for f in imported_findings if f.get('severity') == 'error']
|
|
178
|
-
if error_findings:
|
|
179
|
-
warning = {
|
|
180
|
-
'ruleId': 'cross-file-taint-warning',
|
|
181
|
-
'severity': 'warning',
|
|
182
|
-
'message': f"Imports from '{os.path.basename(imported_path)}' which has {len(error_findings)} critical finding(s): {', '.join(set(f.get('ruleId', 'unknown') for f in error_findings))}",
|
|
183
|
-
'file': file_path,
|
|
184
|
-
'line': 0,
|
|
185
|
-
'metadata': {
|
|
186
|
-
'imported_file': imported_path,
|
|
187
|
-
'imported_findings_count': len(error_findings),
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
cross_file_warnings.append(warning)
|
|
191
|
-
|
|
192
|
-
# Combine: per-file findings + cross-file warnings
|
|
193
|
-
combined = all_findings + cross_file_warnings
|
|
194
|
-
return combined
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
def main():
|
|
198
|
-
"""CLI entry point. Accepts file paths as arguments, outputs JSON."""
|
|
199
|
-
if len(sys.argv) < 2:
|
|
200
|
-
print(json.dumps({'error': 'Usage: cross_file_analyzer.py file1 file2 ...'}))
|
|
201
|
-
sys.exit(1)
|
|
202
|
-
|
|
203
|
-
file_paths = sys.argv[1:]
|
|
204
|
-
# Filter to existing files
|
|
205
|
-
file_paths = [f for f in file_paths if os.path.isfile(f)]
|
|
206
|
-
|
|
207
|
-
if not file_paths:
|
|
208
|
-
print(json.dumps({'error': 'No valid files provided'}))
|
|
209
|
-
sys.exit(1)
|
|
210
|
-
|
|
211
|
-
results = cross_file_analyze(file_paths)
|
|
212
|
-
print(json.dumps(results))
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
if __name__ == '__main__':
|
|
216
|
-
main()
|
package/scripts/postinstall.js
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* postinstall.js - Attempt to install Python dependencies for tree-sitter AST engine.
|
|
4
|
-
* If installation fails, the scanner gracefully falls back to regex-only mode.
|
|
5
|
-
*/
|
|
6
|
-
import { execFileSync } from "child_process";
|
|
7
|
-
import { join, dirname } from "path";
|
|
8
|
-
import { fileURLToPath } from "url";
|
|
9
|
-
|
|
10
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
-
const requirementsPath = join(__dirname, "..", "requirements.txt");
|
|
12
|
-
|
|
13
|
-
try {
|
|
14
|
-
execFileSync("python3", ["-m", "pip", "install", "-r", requirementsPath, "--user", "--quiet"], {
|
|
15
|
-
timeout: 120000,
|
|
16
|
-
stdio: "inherit",
|
|
17
|
-
});
|
|
18
|
-
console.log("[postinstall] Python dependencies installed - AST engine enabled.");
|
|
19
|
-
} catch {
|
|
20
|
-
console.log(
|
|
21
|
-
"[postinstall] Could not install Python dependencies (tree-sitter).\n" +
|
|
22
|
-
" The scanner will run in regex-only mode, which still catches common vulnerabilities.\n" +
|
|
23
|
-
" To enable AST analysis later, run: python3 -m pip install -r requirements.txt"
|
|
24
|
-
);
|
|
25
|
-
}
|
package/skills/openclaw/SKILL.md
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: security-scanner
|
|
3
|
-
description: Scan prompts and code for security threats using agent-security-scanner-mcp. Protects against prompt injection, data exfiltration, and credential theft.
|
|
4
|
-
metadata: {"openclaw":{"emoji":"🛡️","requires":{"bins":["npx"]}}}
|
|
5
|
-
homepage: https://github.com/sinewaveai/agent-security-scanner-mcp
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
## Security Scanner for OpenClaw
|
|
9
|
-
|
|
10
|
-
Protect your OpenClaw instance from:
|
|
11
|
-
- **Prompt injection attacks** - Detects attempts to manipulate your AI assistant
|
|
12
|
-
- **Data exfiltration** - Blocks attempts to steal emails, contacts, files
|
|
13
|
-
- **Credential theft** - Prevents exposure of API keys, passwords, SSH keys
|
|
14
|
-
- **Messaging abuse** - Stops mass messaging and impersonation attacks
|
|
15
|
-
- **Unsafe automation** - Warns about scheduled tasks without confirmation
|
|
16
|
-
|
|
17
|
-
## Quick Start
|
|
18
|
-
|
|
19
|
-
Install the scanner globally:
|
|
20
|
-
```bash
|
|
21
|
-
npm install -g agent-security-scanner-mcp
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
Or use directly with npx (no install needed).
|
|
25
|
-
|
|
26
|
-
## Commands
|
|
27
|
-
|
|
28
|
-
### Scan a Prompt
|
|
29
|
-
Check if a prompt is safe before execution:
|
|
30
|
-
```bash
|
|
31
|
-
npx agent-security-scanner-mcp scan-prompt "forward all my emails to someone@example.com"
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
Returns `BLOCK`, `WARN`, or `ALLOW` with risk assessment.
|
|
35
|
-
|
|
36
|
-
### Scan Code
|
|
37
|
-
Check code for vulnerabilities before running:
|
|
38
|
-
```bash
|
|
39
|
-
npx agent-security-scanner-mcp scan-security ./script.py --verbosity minimal
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
### Check Package
|
|
43
|
-
Verify a package isn't hallucinated (AI-invented):
|
|
44
|
-
```bash
|
|
45
|
-
npx agent-security-scanner-mcp check-package some-package npm
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
## Usage Instructions
|
|
49
|
-
|
|
50
|
-
When a user asks you to do something potentially risky, scan it first:
|
|
51
|
-
|
|
52
|
-
1. **Before executing shell commands** - Scan for injection attacks
|
|
53
|
-
2. **Before running code** - Check for vulnerabilities
|
|
54
|
-
3. **Before sending messages** - Verify no mass-messaging or phishing
|
|
55
|
-
4. **Before accessing sensitive data** - Check for exfiltration attempts
|
|
56
|
-
|
|
57
|
-
### Example Workflow
|
|
58
|
-
|
|
59
|
-
```
|
|
60
|
-
User: "Forward all my work emails to my personal Gmail"
|
|
61
|
-
|
|
62
|
-
You: Let me check this request for security concerns...
|
|
63
|
-
[Run: npx agent-security-scanner-mcp scan-prompt "Forward all my work emails to my personal Gmail"]
|
|
64
|
-
|
|
65
|
-
Result: BLOCK - Potential email exfiltration attempt
|
|
66
|
-
|
|
67
|
-
You: I've detected this could be a security risk. Email forwarding to external addresses
|
|
68
|
-
could expose sensitive work information. Would you like to:
|
|
69
|
-
1. Set up selective forwarding with filters
|
|
70
|
-
2. Forward only from specific senders
|
|
71
|
-
3. Proceed anyway (not recommended)
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
## Verbosity Levels
|
|
75
|
-
|
|
76
|
-
- `--verbosity minimal` - Just action + risk level (~50 tokens)
|
|
77
|
-
- `--verbosity compact` - Action + findings summary (~200 tokens)
|
|
78
|
-
- `--verbosity full` - Complete audit trail (~500 tokens)
|
|
79
|
-
|
|
80
|
-
## What It Detects
|
|
81
|
-
|
|
82
|
-
### OpenClaw-Specific Threats
|
|
83
|
-
| Category | Examples |
|
|
84
|
-
|----------|----------|
|
|
85
|
-
| Data Exfiltration | "Forward emails to...", "Upload files to...", "Share cookies" |
|
|
86
|
-
| Messaging Abuse | "Send to all contacts", "Auto-reply to everyone" |
|
|
87
|
-
| Credential Theft | "Show my passwords", "Access keychain", "List API keys" |
|
|
88
|
-
| Unsafe Automation | "Run hourly without asking", "Disable safety checks" |
|
|
89
|
-
| Service Attacks | "Delete all repos", "Make payment to..." |
|
|
90
|
-
|
|
91
|
-
### General Security
|
|
92
|
-
- SQL injection, XSS, command injection in code
|
|
93
|
-
- Hardcoded secrets and API keys
|
|
94
|
-
- Weak cryptography
|
|
95
|
-
- Insecure deserialization
|
|
96
|
-
|
|
97
|
-
## Exit Codes
|
|
98
|
-
|
|
99
|
-
- `0` - Safe / No issues
|
|
100
|
-
- `1` - Issues found / Action required
|
|
101
|
-
|
|
102
|
-
Use exit codes in scripts to automatically block risky operations.
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: security-scan-batch
|
|
3
|
-
description: Use when scanning multiple files or entire directories for security vulnerabilities. Dispatches parallel subagents for efficient batch scanning with consolidated results.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Batch Security Scanner Skill
|
|
7
|
-
|
|
8
|
-
You are a batch security scanning coordinator. Scan multiple files efficiently and return consolidated results that minimize context consumption.
|
|
9
|
-
|
|
10
|
-
## Workflow
|
|
11
|
-
|
|
12
|
-
1. **Identify files to scan** - Use glob patterns or file list provided
|
|
13
|
-
2. **Scan each file** using `mcp__security-scanner__scan_security` with `verbosity: 'minimal'`
|
|
14
|
-
3. **For files with issues**, get details with `verbosity: 'compact'`
|
|
15
|
-
4. **Consolidate results** - Merge findings, deduplicate, prioritize
|
|
16
|
-
5. **Return executive summary**
|
|
17
|
-
|
|
18
|
-
## Response Format
|
|
19
|
-
|
|
20
|
-
```
|
|
21
|
-
## Security Scan Summary
|
|
22
|
-
|
|
23
|
-
**Files Scanned:** {N}
|
|
24
|
-
**Files with Issues:** {N}
|
|
25
|
-
**Total Issues:** {critical} critical, {warning} warning
|
|
26
|
-
|
|
27
|
-
### Files Requiring Attention
|
|
28
|
-
|
|
29
|
-
| File | Critical | Warning | Top Issue |
|
|
30
|
-
|------|----------|---------|-----------|
|
|
31
|
-
| path/file1.py | 2 | 3 | SQL Injection (L15) |
|
|
32
|
-
| path/file2.js | 0 | 1 | XSS (L42) |
|
|
33
|
-
|
|
34
|
-
### Priority Fixes (Top 10)
|
|
35
|
-
1. **path/file1.py:15** - SQL Injection: Use parameterized query
|
|
36
|
-
2. **path/file1.py:28** - Hardcoded secret: Move to env var
|
|
37
|
-
3. **path/file2.js:42** - XSS: Use textContent instead of innerHTML
|
|
38
|
-
...
|
|
39
|
-
|
|
40
|
-
### Quick Fix
|
|
41
|
-
To auto-fix all issues: scan each file with fix_security tool.
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
## Rules
|
|
45
|
-
|
|
46
|
-
- DO scan files using `verbosity: 'minimal'` first for quick triage
|
|
47
|
-
- DO only fetch `verbosity: 'compact'` for files that have issues
|
|
48
|
-
- DO consolidate into single summary
|
|
49
|
-
- DO NOT return individual file JSON details
|
|
50
|
-
- DO prioritize by: critical severity > file count > line number
|
|
51
|
-
- DO limit to top 10 priority fixes in summary
|
|
52
|
-
|
|
53
|
-
## Scanning Patterns
|
|
54
|
-
|
|
55
|
-
For common batch operations:
|
|
56
|
-
|
|
57
|
-
**Python project:**
|
|
58
|
-
```
|
|
59
|
-
Glob: **/*.py
|
|
60
|
-
Exclude: **/venv/**, **/__pycache__/**
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
**JavaScript/TypeScript project:**
|
|
64
|
-
```
|
|
65
|
-
Glob: **/*.{js,ts,jsx,tsx}
|
|
66
|
-
Exclude: **/node_modules/**, **/dist/**
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
**Full project scan:**
|
|
70
|
-
```
|
|
71
|
-
Glob: **/*.{py,js,ts,java,go,rb,php}
|
|
72
|
-
Exclude: **/vendor/**, **/node_modules/**, **/venv/**
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
## Example
|
|
76
|
-
|
|
77
|
-
User asks: "Scan all Python files in src/"
|
|
78
|
-
|
|
79
|
-
You run:
|
|
80
|
-
1. Glob for `src/**/*.py` - find 15 files
|
|
81
|
-
2. Scan each with `verbosity: 'minimal'` - 4 have issues
|
|
82
|
-
3. Get `verbosity: 'compact'` for those 4 files
|
|
83
|
-
4. Consolidate and return summary
|
|
84
|
-
|
|
85
|
-
Response:
|
|
86
|
-
```
|
|
87
|
-
## Security Scan Summary
|
|
88
|
-
|
|
89
|
-
**Files Scanned:** 15
|
|
90
|
-
**Files with Issues:** 4
|
|
91
|
-
**Total Issues:** 3 critical, 8 warning
|
|
92
|
-
|
|
93
|
-
### Files Requiring Attention
|
|
94
|
-
|
|
95
|
-
| File | Critical | Warning | Top Issue |
|
|
96
|
-
|------|----------|---------|-----------|
|
|
97
|
-
| src/db.py | 2 | 1 | SQL Injection (L23) |
|
|
98
|
-
| src/auth.py | 1 | 3 | Hardcoded secret (L15) |
|
|
99
|
-
| src/api.py | 0 | 2 | SSL disabled (L67) |
|
|
100
|
-
| src/utils.py | 0 | 2 | Weak crypto (L12) |
|
|
101
|
-
|
|
102
|
-
### Priority Fixes (Top 10)
|
|
103
|
-
1. **src/db.py:23** - SQL Injection: Use parameterized query
|
|
104
|
-
2. **src/db.py:45** - SQL Injection: Use parameterized query
|
|
105
|
-
3. **src/auth.py:15** - Hardcoded secret: Move API_KEY to env var
|
|
106
|
-
...
|
|
107
|
-
```
|