agent-security-scanner-mcp 3.3.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 +1 -59
- package/index.js +3 -92
- package/package.json +4 -7
- package/src/cli/init-hooks.js +164 -0
- package/src/cli/init.js +0 -93
- package/src/tools/scan-diff.js +151 -0
- package/src/tools/scan-project.js +308 -0
- package/src/tools/scan-prompt.js +1 -71
- package/skills/openclaw/SKILL.md +0 -102
- package/skills/security-scan-batch.md +0 -107
- package/skills/security-scanner.md +0 -76
package/README.md
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
# agent-security-scanner-mcp
|
|
2
2
|
|
|
3
|
-
Security scanner for AI coding agents
|
|
3
|
+
Security scanner MCP server for AI coding agents. Scans code for vulnerabilities, detects hallucinated packages, and blocks prompt injection — all in real-time via the Model Context Protocol.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/agent-security-scanner-mcp)
|
|
6
6
|
[](https://www.npmjs.com/package/agent-security-scanner-mcp)
|
|
7
7
|
[](https://opensource.org/licenses/MIT)
|
|
8
8
|
|
|
9
|
-
> **New in v3.3.0:** Full [OpenClaw](https://openclaw.ai) integration with 30+ rules targeting autonomous AI threats — data exfiltration, credential theft, messaging abuse, and unsafe automation. [See OpenClaw setup](#openclaw-integration).
|
|
10
|
-
|
|
11
9
|
## Tools
|
|
12
10
|
|
|
13
11
|
| Tool | Description | When to Use |
|
|
@@ -394,7 +392,6 @@ npx agent-security-scanner-mcp
|
|
|
394
392
|
| Kilo Code | `npx agent-security-scanner-mcp init kilo-code` |
|
|
395
393
|
| OpenCode | `npx agent-security-scanner-mcp init opencode` |
|
|
396
394
|
| Cody | `npx agent-security-scanner-mcp init cody` |
|
|
397
|
-
| **OpenClaw** | `npx agent-security-scanner-mcp init openclaw` |
|
|
398
395
|
| Interactive | `npx agent-security-scanner-mcp init` |
|
|
399
396
|
|
|
400
397
|
The `init` command auto-detects your OS, locates the config file, creates a backup, and adds the MCP server entry. **Restart your client after running init.**
|
|
@@ -454,61 +451,6 @@ Available languages: `js` (default), `py`, `go`, `java`.
|
|
|
454
451
|
|
|
455
452
|
---
|
|
456
453
|
|
|
457
|
-
## CLI Tools
|
|
458
|
-
|
|
459
|
-
Use the scanner directly from command line (for scripts, CI/CD, or OpenClaw):
|
|
460
|
-
|
|
461
|
-
```bash
|
|
462
|
-
# Scan a prompt for injection attacks
|
|
463
|
-
npx agent-security-scanner-mcp scan-prompt "ignore previous instructions"
|
|
464
|
-
|
|
465
|
-
# Scan a file for vulnerabilities
|
|
466
|
-
npx agent-security-scanner-mcp scan-security ./app.py --verbosity minimal
|
|
467
|
-
|
|
468
|
-
# Check if a package is legitimate
|
|
469
|
-
npx agent-security-scanner-mcp check-package flask pypi
|
|
470
|
-
|
|
471
|
-
# Scan file imports for hallucinated packages
|
|
472
|
-
npx agent-security-scanner-mcp scan-packages ./requirements.txt pypi
|
|
473
|
-
```
|
|
474
|
-
|
|
475
|
-
**Exit codes:** `0` = safe, `1` = issues found. Use in scripts to block risky operations.
|
|
476
|
-
|
|
477
|
-
---
|
|
478
|
-
|
|
479
|
-
## OpenClaw Integration
|
|
480
|
-
|
|
481
|
-
[OpenClaw](https://openclaw.ai) is an autonomous AI assistant with broad system access. This scanner provides security guardrails for OpenClaw users.
|
|
482
|
-
|
|
483
|
-
### Install
|
|
484
|
-
|
|
485
|
-
```bash
|
|
486
|
-
npx agent-security-scanner-mcp init openclaw
|
|
487
|
-
```
|
|
488
|
-
|
|
489
|
-
This installs a skill to `~/.openclaw/workspace/skills/security-scanner/`.
|
|
490
|
-
|
|
491
|
-
### OpenClaw-Specific Threats
|
|
492
|
-
|
|
493
|
-
The scanner includes 30+ rules targeting OpenClaw's unique attack surface:
|
|
494
|
-
|
|
495
|
-
| Category | Examples |
|
|
496
|
-
|----------|----------|
|
|
497
|
-
| **Data Exfiltration** | "Forward emails to...", "Upload files to...", "Share browser cookies" |
|
|
498
|
-
| **Messaging Abuse** | "Send to all contacts", "Auto-reply to everyone" |
|
|
499
|
-
| **Credential Theft** | "Show my passwords", "Access keychain", "List API keys" |
|
|
500
|
-
| **Unsafe Automation** | "Run hourly without asking", "Disable safety checks" |
|
|
501
|
-
| **Service Attacks** | "Delete all repos", "Make payment to..." |
|
|
502
|
-
|
|
503
|
-
### Usage in OpenClaw
|
|
504
|
-
|
|
505
|
-
The skill is auto-discovered. Use it by asking:
|
|
506
|
-
- "Scan this prompt for security issues"
|
|
507
|
-
- "Check if this code is safe to run"
|
|
508
|
-
- "Verify these packages aren't hallucinated"
|
|
509
|
-
|
|
510
|
-
---
|
|
511
|
-
|
|
512
454
|
## What This Scanner Detects
|
|
513
455
|
|
|
514
456
|
AI coding agents introduce attack surfaces that traditional security tools weren't designed for:
|
package/index.js
CHANGED
|
@@ -156,106 +156,17 @@ if (cliArgs[0] === 'init') {
|
|
|
156
156
|
console.error(` Error: ${err.message}\n`);
|
|
157
157
|
process.exit(1);
|
|
158
158
|
});
|
|
159
|
-
} else if (cliArgs[0] === 'scan-prompt') {
|
|
160
|
-
// CLI mode: scan-prompt <text> [--verbosity minimal|compact|full]
|
|
161
|
-
const text = cliArgs[1];
|
|
162
|
-
if (!text) {
|
|
163
|
-
console.error('Usage: agent-security-scanner-mcp scan-prompt <text> [--verbosity minimal|compact|full]');
|
|
164
|
-
process.exit(1);
|
|
165
|
-
}
|
|
166
|
-
const verbosityIdx = cliArgs.indexOf('--verbosity');
|
|
167
|
-
const verbosity = verbosityIdx !== -1 ? cliArgs[verbosityIdx + 1] : 'compact';
|
|
168
|
-
|
|
169
|
-
loadPackageLists();
|
|
170
|
-
scanAgentPrompt({ prompt_text: text, verbosity }).then(result => {
|
|
171
|
-
const output = JSON.parse(result.content[0].text);
|
|
172
|
-
console.log(JSON.stringify(output, null, 2));
|
|
173
|
-
process.exit(output.action === 'BLOCK' ? 1 : 0);
|
|
174
|
-
}).catch(err => {
|
|
175
|
-
console.error(JSON.stringify({ error: err.message }));
|
|
176
|
-
process.exit(1);
|
|
177
|
-
});
|
|
178
|
-
} else if (cliArgs[0] === 'scan-security') {
|
|
179
|
-
// CLI mode: scan-security <file> [--verbosity minimal|compact|full] [--format json|sarif]
|
|
180
|
-
const filePath = cliArgs[1];
|
|
181
|
-
if (!filePath) {
|
|
182
|
-
console.error('Usage: agent-security-scanner-mcp scan-security <file> [--verbosity minimal|compact|full] [--format json|sarif]');
|
|
183
|
-
process.exit(1);
|
|
184
|
-
}
|
|
185
|
-
const verbosityIdx = cliArgs.indexOf('--verbosity');
|
|
186
|
-
const verbosity = verbosityIdx !== -1 ? cliArgs[verbosityIdx + 1] : 'compact';
|
|
187
|
-
const formatIdx = cliArgs.indexOf('--format');
|
|
188
|
-
const outputFormat = formatIdx !== -1 ? cliArgs[formatIdx + 1] : 'json';
|
|
189
|
-
|
|
190
|
-
loadPackageLists();
|
|
191
|
-
scanSecurity({ file_path: filePath, verbosity, output_format: outputFormat }).then(result => {
|
|
192
|
-
const output = JSON.parse(result.content[0].text);
|
|
193
|
-
console.log(JSON.stringify(output, null, 2));
|
|
194
|
-
process.exit(output.issues_count > 0 || output.total > 0 ? 1 : 0);
|
|
195
|
-
}).catch(err => {
|
|
196
|
-
console.error(JSON.stringify({ error: err.message }));
|
|
197
|
-
process.exit(1);
|
|
198
|
-
});
|
|
199
|
-
} else if (cliArgs[0] === 'check-package') {
|
|
200
|
-
// CLI mode: check-package <name> <ecosystem>
|
|
201
|
-
const packageName = cliArgs[1];
|
|
202
|
-
const ecosystem = cliArgs[2];
|
|
203
|
-
if (!packageName || !ecosystem) {
|
|
204
|
-
console.error('Usage: agent-security-scanner-mcp check-package <name> <ecosystem>');
|
|
205
|
-
console.error('Ecosystems: npm, pypi, rubygems, crates, dart, perl, raku');
|
|
206
|
-
process.exit(1);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
loadPackageLists();
|
|
210
|
-
checkPackage({ package_name: packageName, ecosystem }).then(result => {
|
|
211
|
-
const output = JSON.parse(result.content[0].text);
|
|
212
|
-
console.log(JSON.stringify(output, null, 2));
|
|
213
|
-
process.exit(output.legitimate ? 0 : 1);
|
|
214
|
-
}).catch(err => {
|
|
215
|
-
console.error(JSON.stringify({ error: err.message }));
|
|
216
|
-
process.exit(1);
|
|
217
|
-
});
|
|
218
|
-
} else if (cliArgs[0] === 'scan-packages') {
|
|
219
|
-
// CLI mode: scan-packages <file> <ecosystem> [--verbosity minimal|compact|full]
|
|
220
|
-
const filePath = cliArgs[1];
|
|
221
|
-
const ecosystem = cliArgs[2];
|
|
222
|
-
if (!filePath || !ecosystem) {
|
|
223
|
-
console.error('Usage: agent-security-scanner-mcp scan-packages <file> <ecosystem> [--verbosity minimal|compact|full]');
|
|
224
|
-
console.error('Ecosystems: npm, pypi, rubygems, crates, dart, perl, raku');
|
|
225
|
-
process.exit(1);
|
|
226
|
-
}
|
|
227
|
-
const verbosityIdx = cliArgs.indexOf('--verbosity');
|
|
228
|
-
const verbosity = verbosityIdx !== -1 ? cliArgs[verbosityIdx + 1] : 'compact';
|
|
229
|
-
|
|
230
|
-
loadPackageLists();
|
|
231
|
-
scanPackages({ file_path: filePath, ecosystem, verbosity }).then(result => {
|
|
232
|
-
const output = JSON.parse(result.content[0].text);
|
|
233
|
-
console.log(JSON.stringify(output, null, 2));
|
|
234
|
-
process.exit(output.hallucinated_count > 0 ? 1 : 0);
|
|
235
|
-
}).catch(err => {
|
|
236
|
-
console.error(JSON.stringify({ error: err.message }));
|
|
237
|
-
process.exit(1);
|
|
238
|
-
});
|
|
239
159
|
} else if (cliArgs[0] === '--help' || cliArgs[0] === '-h' || cliArgs[0] === 'help') {
|
|
240
160
|
console.log('\n agent-security-scanner-mcp\n');
|
|
241
161
|
console.log(' Commands:');
|
|
242
162
|
console.log(' init [client] Set up MCP config for a client');
|
|
243
163
|
console.log(' doctor [--fix] Check environment & client configs');
|
|
244
|
-
console.log(' demo [--lang js] Generate vulnerable file + scan it
|
|
245
|
-
console.log(' CLI Tools (for scripts & OpenClaw):');
|
|
246
|
-
console.log(' scan-prompt <text> Scan prompt for injection attacks');
|
|
247
|
-
console.log(' scan-security <file> Scan file for vulnerabilities');
|
|
248
|
-
console.log(' check-package <n> <e> Check if package exists in ecosystem');
|
|
249
|
-
console.log(' scan-packages <f> <e> Scan file imports for hallucinated packages\n');
|
|
164
|
+
console.log(' demo [--lang js] Generate vulnerable file + scan it');
|
|
250
165
|
console.log(' (no args) Start MCP server on stdio\n');
|
|
251
|
-
console.log(' Options:');
|
|
252
|
-
console.log(' --verbosity <level> minimal|compact|full (default: compact)');
|
|
253
|
-
console.log(' --format <type> json|sarif (scan-security only)\n');
|
|
254
166
|
console.log(' Examples:');
|
|
255
167
|
console.log(' npx agent-security-scanner-mcp init');
|
|
256
|
-
console.log(' npx agent-security-scanner-mcp
|
|
257
|
-
console.log(' npx agent-security-scanner-mcp
|
|
258
|
-
console.log(' npx agent-security-scanner-mcp check-package flask pypi\n');
|
|
168
|
+
console.log(' npx agent-security-scanner-mcp doctor --fix');
|
|
169
|
+
console.log(' npx agent-security-scanner-mcp demo --lang py\n');
|
|
259
170
|
process.exit(0);
|
|
260
171
|
} else {
|
|
261
172
|
// Normal MCP server mode
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-security-scanner-mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.0",
|
|
4
4
|
"mcpName": "io.github.sinewaveai/agent-security-scanner-mcp",
|
|
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
|
|
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",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"bin": {
|
|
@@ -52,9 +52,7 @@
|
|
|
52
52
|
"zed",
|
|
53
53
|
"prompt-firewall",
|
|
54
54
|
"auto-fix",
|
|
55
|
-
"hallucination"
|
|
56
|
-
"openclaw",
|
|
57
|
-
"clawdbot"
|
|
55
|
+
"hallucination"
|
|
58
56
|
],
|
|
59
57
|
"author": "Sinewave AI <divya@sinewave.ai>",
|
|
60
58
|
"license": "MIT",
|
|
@@ -91,8 +89,7 @@
|
|
|
91
89
|
"taint_analyzer.py",
|
|
92
90
|
"requirements.txt",
|
|
93
91
|
"rules/**",
|
|
94
|
-
"packages/**"
|
|
95
|
-
"skills/**"
|
|
92
|
+
"packages/**"
|
|
96
93
|
],
|
|
97
94
|
"devDependencies": {
|
|
98
95
|
"all-the-package-names": "^2.0.2349",
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// src/cli/init-hooks.js
|
|
2
|
+
// CLI command: init-hooks
|
|
3
|
+
// Installs Claude Code hooks for automatic security scanning on file write/edit.
|
|
4
|
+
|
|
5
|
+
import { existsSync, readFileSync, writeFileSync, copyFileSync, mkdirSync } from 'fs';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
|
|
8
|
+
const SCANNER_HOOK_MARKER = 'agent-security-scanner-mcp';
|
|
9
|
+
|
|
10
|
+
function buildHooksConfig(withPromptGuard) {
|
|
11
|
+
const hooks = {
|
|
12
|
+
'post-tool-use': [
|
|
13
|
+
{
|
|
14
|
+
matcher: 'Write|Edit|MultiEdit',
|
|
15
|
+
command: `npx agent-security-scanner-mcp scan-security "$TOOL_INPUT_FILE_PATH" --verbosity minimal`,
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
if (withPromptGuard) {
|
|
21
|
+
hooks['pre-tool-use'] = [
|
|
22
|
+
{
|
|
23
|
+
matcher: 'Bash',
|
|
24
|
+
command: `npx agent-security-scanner-mcp scan-prompt "$TOOL_INPUT_COMMAND" --verbosity minimal`,
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return hooks;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function backupTimestamp() {
|
|
33
|
+
const d = new Date();
|
|
34
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
35
|
+
return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function parseFlags(args) {
|
|
39
|
+
const flags = { dryRun: false, path: null, withPromptGuard: false };
|
|
40
|
+
let i = 0;
|
|
41
|
+
while (i < args.length) {
|
|
42
|
+
const arg = args[i];
|
|
43
|
+
if (arg === '--dry-run') flags.dryRun = true;
|
|
44
|
+
else if (arg === '--path' && i + 1 < args.length) flags.path = args[++i];
|
|
45
|
+
else if (arg === '--with-prompt-guard') flags.withPromptGuard = true;
|
|
46
|
+
i++;
|
|
47
|
+
}
|
|
48
|
+
return flags;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function containsScannerHook(hooksObj) {
|
|
52
|
+
if (!hooksObj || typeof hooksObj !== 'object') return false;
|
|
53
|
+
for (const eventHooks of Object.values(hooksObj)) {
|
|
54
|
+
if (!Array.isArray(eventHooks)) continue;
|
|
55
|
+
if (eventHooks.some(h => h.command && h.command.includes(SCANNER_HOOK_MARKER))) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function mergeHooks(existingHooks, newHooks) {
|
|
63
|
+
const merged = { ...existingHooks };
|
|
64
|
+
|
|
65
|
+
for (const [event, hooks] of Object.entries(newHooks)) {
|
|
66
|
+
if (!merged[event]) {
|
|
67
|
+
merged[event] = hooks;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Filter out existing scanner hooks for this event
|
|
72
|
+
const nonScanner = merged[event].filter(h =>
|
|
73
|
+
!h.command || !h.command.includes(SCANNER_HOOK_MARKER)
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
merged[event] = [...nonScanner, ...hooks];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return merged;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function runInitHooks(args) {
|
|
83
|
+
const flags = parseFlags(args);
|
|
84
|
+
|
|
85
|
+
console.log('\n Agentic Security - Claude Code Hooks Setup\n');
|
|
86
|
+
|
|
87
|
+
const settingsDir = flags.path || join(process.cwd(), '.claude');
|
|
88
|
+
const settingsPath = join(settingsDir, 'settings.json');
|
|
89
|
+
|
|
90
|
+
console.log(` Settings: ${settingsPath}`);
|
|
91
|
+
console.log(` Prompt guard: ${flags.withPromptGuard ? 'enabled' : 'disabled (use --with-prompt-guard to enable)'}`);
|
|
92
|
+
console.log('');
|
|
93
|
+
|
|
94
|
+
const newHooks = buildHooksConfig(flags.withPromptGuard);
|
|
95
|
+
|
|
96
|
+
// Read existing settings
|
|
97
|
+
let existing = {};
|
|
98
|
+
let fileExisted = false;
|
|
99
|
+
if (existsSync(settingsPath)) {
|
|
100
|
+
fileExisted = true;
|
|
101
|
+
try {
|
|
102
|
+
existing = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
103
|
+
} catch (e) {
|
|
104
|
+
console.error(` ERROR: Invalid JSON in ${settingsPath}`);
|
|
105
|
+
console.error(` ${e.message}\n`);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (containsScannerHook(existing.hooks)) {
|
|
111
|
+
console.log(' Scanner hooks already configured. Updating...');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Merge hooks non-destructively
|
|
115
|
+
const mergedHooks = mergeHooks(existing.hooks || {}, newHooks);
|
|
116
|
+
const merged = { ...existing, hooks: mergedHooks };
|
|
117
|
+
const output = JSON.stringify(merged, null, 2) + '\n';
|
|
118
|
+
|
|
119
|
+
if (flags.dryRun) {
|
|
120
|
+
console.log(' [dry-run] Would write:\n');
|
|
121
|
+
console.log(' ' + output.split('\n').join('\n '));
|
|
122
|
+
console.log(' No changes made.\n');
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!existsSync(settingsDir)) {
|
|
127
|
+
mkdirSync(settingsDir, { recursive: true });
|
|
128
|
+
console.log(` Created directory: ${settingsDir}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (fileExisted) {
|
|
132
|
+
const backupPath = `${settingsPath}.bak-${backupTimestamp()}`;
|
|
133
|
+
copyFileSync(settingsPath, backupPath);
|
|
134
|
+
console.log(` Backup: ${backupPath}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
writeFileSync(settingsPath, output);
|
|
138
|
+
console.log(` Wrote: ${settingsPath}\n`);
|
|
139
|
+
|
|
140
|
+
console.log(' Hooks installed:');
|
|
141
|
+
for (const [event, hooks] of Object.entries(newHooks)) {
|
|
142
|
+
for (const hook of hooks) {
|
|
143
|
+
console.log(` - [${event}] Matcher: ${hook.matcher}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
console.log('\n Security scanning is now automatic for file writes and edits.');
|
|
148
|
+
console.log(' Restart Claude Code for hooks to take effect.\n');
|
|
149
|
+
|
|
150
|
+
if (!existsSync(join(process.cwd(), '.scannerrc.yaml')) &&
|
|
151
|
+
!existsSync(join(process.cwd(), '.scannerrc.yml')) &&
|
|
152
|
+
!existsSync(join(process.cwd(), '.scannerrc.json'))) {
|
|
153
|
+
console.log(' Tip: Create a .scannerrc.yaml to customize scanning:');
|
|
154
|
+
console.log('');
|
|
155
|
+
console.log(' version: 1');
|
|
156
|
+
console.log(' suppress:');
|
|
157
|
+
console.log(' - rule: "insecure-random"');
|
|
158
|
+
console.log(' exclude:');
|
|
159
|
+
console.log(' - "node_modules/**"');
|
|
160
|
+
console.log(' - "dist/**"');
|
|
161
|
+
console.log(' severity_threshold: "warning"');
|
|
162
|
+
console.log('');
|
|
163
|
+
}
|
|
164
|
+
}
|
package/src/cli/init.js
CHANGED
|
@@ -73,12 +73,6 @@ const CLIENT_CONFIGS = {
|
|
|
73
73
|
configKey: 'mcpServers',
|
|
74
74
|
configPath: () => join(vscodeBase(), 'Code', 'User', 'globalStorage', 'sourcegraph.cody-ai', 'mcp_settings.json'),
|
|
75
75
|
buildEntry: () => ({ ...MCP_SERVER_ENTRY })
|
|
76
|
-
},
|
|
77
|
-
'openclaw': {
|
|
78
|
-
name: 'OpenClaw',
|
|
79
|
-
isSkillBased: true, // OpenClaw uses skills, not MCP config
|
|
80
|
-
skillPath: () => join(homedir(), '.openclaw', 'workspace', 'skills', 'security-scanner'),
|
|
81
|
-
configPath: () => join(homedir(), '.openclaw', 'workspace', 'skills', 'security-scanner', 'SKILL.md')
|
|
82
76
|
}
|
|
83
77
|
};
|
|
84
78
|
|
|
@@ -156,87 +150,6 @@ function printInitUsage() {
|
|
|
156
150
|
console.log(' npx agent-security-scanner-mcp init cline --force --name my-scanner\n');
|
|
157
151
|
}
|
|
158
152
|
|
|
159
|
-
// Special installer for OpenClaw (skill-based)
|
|
160
|
-
async function installOpenClawSkill(client, flags) {
|
|
161
|
-
const skillDir = client.skillPath();
|
|
162
|
-
const skillFile = client.configPath();
|
|
163
|
-
|
|
164
|
-
// Find the source skill file (bundled with the package)
|
|
165
|
-
const __dirname = dirname(new URL(import.meta.url).pathname);
|
|
166
|
-
const sourceSkill = join(__dirname, '..', '..', 'skills', 'openclaw', 'SKILL.md');
|
|
167
|
-
|
|
168
|
-
console.log(`\n Client: ${client.name}`);
|
|
169
|
-
console.log(` Skill: ${skillDir}`);
|
|
170
|
-
console.log(` OS: ${platform()} (${process.arch})\n`);
|
|
171
|
-
|
|
172
|
-
// Check if OpenClaw workspace exists
|
|
173
|
-
const openclawDir = join(homedir(), '.openclaw');
|
|
174
|
-
if (!existsSync(openclawDir)) {
|
|
175
|
-
console.log(` OpenClaw not found at ${openclawDir}`);
|
|
176
|
-
console.log(` Please install OpenClaw first: https://openclaw.ai\n`);
|
|
177
|
-
process.exit(1);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Check if source skill exists
|
|
181
|
-
if (!existsSync(sourceSkill)) {
|
|
182
|
-
console.error(` ERROR: Skill source not found at ${sourceSkill}`);
|
|
183
|
-
console.error(` This may be a packaging issue. Please reinstall the package.\n`);
|
|
184
|
-
process.exit(1);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Check if skill already exists
|
|
188
|
-
if (existsSync(skillFile)) {
|
|
189
|
-
const existing = readFileSync(skillFile, 'utf-8');
|
|
190
|
-
const source = readFileSync(sourceSkill, 'utf-8');
|
|
191
|
-
if (existing === source) {
|
|
192
|
-
console.log(` Security scanner skill is already installed (identical).`);
|
|
193
|
-
console.log(` Nothing to do.\n`);
|
|
194
|
-
process.exit(0);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
console.log(` Security scanner skill exists but differs.`);
|
|
198
|
-
if (!flags.force) {
|
|
199
|
-
if (flags.yes) {
|
|
200
|
-
console.log(` Skipping (use --force to overwrite).\n`);
|
|
201
|
-
process.exit(0);
|
|
202
|
-
}
|
|
203
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
204
|
-
const answer = await new Promise((resolve) => {
|
|
205
|
-
rl.question(' Overwrite? (y/N): ', (a) => { rl.close(); resolve(a); });
|
|
206
|
-
});
|
|
207
|
-
if (answer.toLowerCase() !== 'y') {
|
|
208
|
-
console.log(' Aborted.\n');
|
|
209
|
-
process.exit(0);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Dry-run mode
|
|
215
|
-
if (flags.dryRun) {
|
|
216
|
-
console.log(` [dry-run] Would create directory: ${skillDir}`);
|
|
217
|
-
console.log(` [dry-run] Would copy skill from: ${sourceSkill}`);
|
|
218
|
-
console.log(` [dry-run] Would write to: ${skillFile}`);
|
|
219
|
-
console.log(` No changes made.\n`);
|
|
220
|
-
process.exit(0);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// Create skill directory
|
|
224
|
-
if (!existsSync(skillDir)) {
|
|
225
|
-
mkdirSync(skillDir, { recursive: true });
|
|
226
|
-
console.log(` Created directory: ${skillDir}`);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// Copy skill file
|
|
230
|
-
copyFileSync(sourceSkill, skillFile);
|
|
231
|
-
console.log(` Installed skill: ${skillFile}`);
|
|
232
|
-
|
|
233
|
-
console.log(`\n OpenClaw security scanner skill installed successfully!`);
|
|
234
|
-
console.log(`\n Usage in OpenClaw:`);
|
|
235
|
-
console.log(` - The skill will be auto-discovered by OpenClaw`);
|
|
236
|
-
console.log(` - Use /security-scanner to invoke it`);
|
|
237
|
-
console.log(` - Or ask: "scan this prompt for security issues"\n`);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
153
|
export async function runInit(args) {
|
|
241
154
|
const flags = parseInitFlags(args);
|
|
242
155
|
let clientName = flags.client;
|
|
@@ -258,12 +171,6 @@ export async function runInit(args) {
|
|
|
258
171
|
process.exit(1);
|
|
259
172
|
}
|
|
260
173
|
|
|
261
|
-
// Special handling for OpenClaw (skill-based, not MCP config)
|
|
262
|
-
if (client.isSkillBased) {
|
|
263
|
-
await installOpenClawSkill(client, flags);
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
174
|
const configPath = flags.path || client.configPath();
|
|
268
175
|
const serverName = flags.name;
|
|
269
176
|
const entry = client.buildEntry();
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// src/tools/scan-diff.js
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { execFileSync } from "child_process";
|
|
4
|
+
import { existsSync } from "fs";
|
|
5
|
+
import { scanSecurity } from './scan-security.js';
|
|
6
|
+
|
|
7
|
+
export const scanDiffSchema = {
|
|
8
|
+
base_ref: z.string().optional().describe("Base git ref (default: HEAD~1)"),
|
|
9
|
+
target_ref: z.string().optional().describe("Target git ref (default: HEAD)"),
|
|
10
|
+
verbosity: z.enum(['minimal', 'compact', 'full']).optional().describe("Response detail level")
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// Parse unified diff output to extract changed files and line ranges
|
|
14
|
+
function parseDiffOutput(diffOutput) {
|
|
15
|
+
const changes = new Map(); // filePath -> Set<lineNumber>
|
|
16
|
+
let currentFile = null;
|
|
17
|
+
|
|
18
|
+
for (const line of diffOutput.split('\n')) {
|
|
19
|
+
// Match diff header: +++ b/path/to/file
|
|
20
|
+
const fileMatch = line.match(/^\+\+\+ b\/(.+)$/);
|
|
21
|
+
if (fileMatch) {
|
|
22
|
+
currentFile = fileMatch[1];
|
|
23
|
+
if (!changes.has(currentFile)) {
|
|
24
|
+
changes.set(currentFile, new Set());
|
|
25
|
+
}
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Match hunk header: @@ -old,count +new,count @@
|
|
30
|
+
const hunkMatch = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/);
|
|
31
|
+
if (hunkMatch && currentFile) {
|
|
32
|
+
const start = parseInt(hunkMatch[1], 10);
|
|
33
|
+
const count = parseInt(hunkMatch[2] || '1', 10);
|
|
34
|
+
const fileChanges = changes.get(currentFile);
|
|
35
|
+
for (let i = start; i < start + count; i++) {
|
|
36
|
+
fileChanges.add(i);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return changes;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function scanDiff({ base_ref, target_ref, verbosity }) {
|
|
45
|
+
const base = base_ref || 'HEAD~1';
|
|
46
|
+
const target = target_ref || 'HEAD';
|
|
47
|
+
|
|
48
|
+
// Get diff output
|
|
49
|
+
let diffOutput;
|
|
50
|
+
try {
|
|
51
|
+
diffOutput = execFileSync('git', ['diff', '--unified=0', `${base}...${target}`], {
|
|
52
|
+
encoding: 'utf-8',
|
|
53
|
+
timeout: 30000,
|
|
54
|
+
maxBuffer: 10 * 1024 * 1024
|
|
55
|
+
});
|
|
56
|
+
} catch (err) {
|
|
57
|
+
// Try without three-dot notation (for uncommitted changes)
|
|
58
|
+
try {
|
|
59
|
+
diffOutput = execFileSync('git', ['diff', '--unified=0', base, target], {
|
|
60
|
+
encoding: 'utf-8',
|
|
61
|
+
timeout: 30000,
|
|
62
|
+
maxBuffer: 10 * 1024 * 1024
|
|
63
|
+
});
|
|
64
|
+
} catch (err2) {
|
|
65
|
+
return {
|
|
66
|
+
content: [{ type: "text", text: JSON.stringify({ error: `Git diff failed: ${err2.message}` }) }]
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!diffOutput.trim()) {
|
|
72
|
+
return {
|
|
73
|
+
content: [{ type: "text", text: JSON.stringify({ message: "No changes found between refs", base, target, issues_count: 0 }) }]
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Parse diff to get changed files and lines
|
|
78
|
+
const changes = parseDiffOutput(diffOutput);
|
|
79
|
+
const allIssues = [];
|
|
80
|
+
const scannedFiles = [];
|
|
81
|
+
|
|
82
|
+
// Scan each changed file
|
|
83
|
+
for (const [filePath, changedLines] of changes) {
|
|
84
|
+
if (!existsSync(filePath)) continue;
|
|
85
|
+
|
|
86
|
+
const result = await scanSecurity({ file_path: filePath, verbosity: 'full' });
|
|
87
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
88
|
+
|
|
89
|
+
if (parsed.issues && Array.isArray(parsed.issues)) {
|
|
90
|
+
// Filter to only issues on changed lines
|
|
91
|
+
const diffIssues = parsed.issues.filter(issue => {
|
|
92
|
+
const issueLine = (issue.line || 0) + 1; // convert 0-indexed to 1-indexed
|
|
93
|
+
return changedLines.has(issueLine);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
for (const issue of diffIssues) {
|
|
97
|
+
allIssues.push({ ...issue, file: filePath });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
scannedFiles.push(filePath);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Format based on verbosity
|
|
104
|
+
const level = verbosity || 'compact';
|
|
105
|
+
|
|
106
|
+
if (level === 'minimal') {
|
|
107
|
+
const bySeverity = { error: 0, warning: 0, info: 0 };
|
|
108
|
+
allIssues.forEach(i => bySeverity[i.severity] = (bySeverity[i.severity] || 0) + 1);
|
|
109
|
+
return {
|
|
110
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
111
|
+
base, target,
|
|
112
|
+
files_scanned: scannedFiles.length,
|
|
113
|
+
total: allIssues.length,
|
|
114
|
+
critical: bySeverity.error,
|
|
115
|
+
warning: bySeverity.warning,
|
|
116
|
+
info: bySeverity.info,
|
|
117
|
+
message: allIssues.length > 0
|
|
118
|
+
? `Found ${allIssues.length} new issue(s) in changed code.`
|
|
119
|
+
: "No new security issues in changed code."
|
|
120
|
+
}) }]
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (level === 'compact') {
|
|
125
|
+
return {
|
|
126
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
127
|
+
base, target,
|
|
128
|
+
files_scanned: scannedFiles.length,
|
|
129
|
+
issues_count: allIssues.length,
|
|
130
|
+
issues: allIssues.map(i => ({
|
|
131
|
+
file: i.file,
|
|
132
|
+
line: (i.line || 0) + 1,
|
|
133
|
+
ruleId: i.ruleId,
|
|
134
|
+
severity: i.severity,
|
|
135
|
+
message: i.message
|
|
136
|
+
}))
|
|
137
|
+
}, null, 2) }]
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// full
|
|
142
|
+
return {
|
|
143
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
144
|
+
base, target,
|
|
145
|
+
files_scanned: scannedFiles.length,
|
|
146
|
+
issues_count: allIssues.length,
|
|
147
|
+
issues: allIssues,
|
|
148
|
+
changed_files: Array.from(changes.keys())
|
|
149
|
+
}, null, 2) }]
|
|
150
|
+
};
|
|
151
|
+
}
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
// src/tools/scan-project.js
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "fs";
|
|
4
|
+
import { join, resolve, relative, extname, basename } from "path";
|
|
5
|
+
import { execFileSync } from "child_process";
|
|
6
|
+
import { scanSecurity } from './scan-security.js';
|
|
7
|
+
import { matchGlob, loadConfig, shouldExcludeFile } from '../config.js';
|
|
8
|
+
import { detectLanguage } from '../utils.js';
|
|
9
|
+
|
|
10
|
+
export const scanProjectSchema = {
|
|
11
|
+
directory_path: z.string().describe("Path to the directory to scan"),
|
|
12
|
+
recursive: z.boolean().optional().describe("Scan subdirectories recursively (default: true)"),
|
|
13
|
+
include_patterns: z.array(z.string()).optional().describe("Glob patterns to include (e.g. ['**/*.py', '**/*.js'])"),
|
|
14
|
+
exclude_patterns: z.array(z.string()).optional().describe("Glob patterns to exclude (e.g. ['*test*', 'vendor/**'])"),
|
|
15
|
+
diff_only: z.boolean().optional().describe("Only scan git-changed files"),
|
|
16
|
+
cross_file: z.boolean().optional().describe("Enable cross-file taint analysis (max 50 files)"),
|
|
17
|
+
verbosity: z.enum(['minimal', 'compact', 'full']).optional().describe("Response detail level")
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Scannable file extensions
|
|
21
|
+
const SCANNABLE_EXTENSIONS = new Set([
|
|
22
|
+
'.py', '.js', '.ts', '.tsx', '.jsx', '.java', '.go', '.rb', '.php',
|
|
23
|
+
'.rs', '.c', '.cpp', '.cc', '.cxx', '.h', '.hpp', '.cs',
|
|
24
|
+
'.tf', '.hcl', '.sql',
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
// Parse .gitignore into patterns
|
|
28
|
+
function parseGitignore(dirPath) {
|
|
29
|
+
const gitignorePath = join(dirPath, '.gitignore');
|
|
30
|
+
if (!existsSync(gitignorePath)) return [];
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const content = readFileSync(gitignorePath, 'utf-8');
|
|
34
|
+
return content.split('\n')
|
|
35
|
+
.map(line => line.trim())
|
|
36
|
+
.filter(line => line && !line.startsWith('#'))
|
|
37
|
+
.map(line => {
|
|
38
|
+
// Normalize: remove trailing slash for directories
|
|
39
|
+
if (line.endsWith('/')) return line.slice(0, -1) + '/**';
|
|
40
|
+
return line;
|
|
41
|
+
});
|
|
42
|
+
} catch {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Check if a file path matches gitignore patterns
|
|
48
|
+
function isGitignored(filePath, patterns) {
|
|
49
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
50
|
+
return patterns.some(pattern => matchGlob(normalized, pattern));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Recursively walk a directory, respecting exclusions
|
|
54
|
+
function walkDirectory(dirPath, options = {}) {
|
|
55
|
+
const { recursive = true, includePatterns = [], excludePatterns = [], gitignorePatterns = [], config } = options;
|
|
56
|
+
const files = [];
|
|
57
|
+
|
|
58
|
+
function walk(currentDir) {
|
|
59
|
+
let entries;
|
|
60
|
+
try {
|
|
61
|
+
entries = readdirSync(currentDir);
|
|
62
|
+
} catch {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
for (const entry of entries) {
|
|
67
|
+
// Skip hidden directories/files
|
|
68
|
+
if (entry.startsWith('.')) continue;
|
|
69
|
+
|
|
70
|
+
const fullPath = join(currentDir, entry);
|
|
71
|
+
const relativePath = relative(dirPath, fullPath);
|
|
72
|
+
|
|
73
|
+
let stat;
|
|
74
|
+
try {
|
|
75
|
+
stat = statSync(fullPath);
|
|
76
|
+
} catch {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (stat.isDirectory()) {
|
|
81
|
+
// Skip common non-source directories
|
|
82
|
+
if (['node_modules', 'vendor', 'dist', 'build', '__pycache__', '.git',
|
|
83
|
+
'venv', 'env', '.venv', 'target', 'coverage'].includes(entry)) continue;
|
|
84
|
+
|
|
85
|
+
// Skip gitignored directories
|
|
86
|
+
if (isGitignored(relativePath, gitignorePatterns)) continue;
|
|
87
|
+
|
|
88
|
+
if (recursive) walk(fullPath);
|
|
89
|
+
} else if (stat.isFile()) {
|
|
90
|
+
const ext = extname(entry).toLowerCase();
|
|
91
|
+
const base = basename(entry).toLowerCase();
|
|
92
|
+
|
|
93
|
+
// Check extension or special filenames
|
|
94
|
+
if (!SCANNABLE_EXTENSIONS.has(ext) && base !== 'dockerfile') continue;
|
|
95
|
+
|
|
96
|
+
// Check gitignore
|
|
97
|
+
if (isGitignored(relativePath, gitignorePatterns)) continue;
|
|
98
|
+
|
|
99
|
+
// Check config exclusions
|
|
100
|
+
if (config && shouldExcludeFile(relativePath, config)) continue;
|
|
101
|
+
|
|
102
|
+
// Check include patterns (if specified, only include matching files)
|
|
103
|
+
if (includePatterns.length > 0) {
|
|
104
|
+
const matches = includePatterns.some(p => matchGlob(relativePath, p));
|
|
105
|
+
if (!matches) continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Check exclude patterns (if specified, skip matching files)
|
|
109
|
+
if (excludePatterns.length > 0) {
|
|
110
|
+
const excluded = excludePatterns.some(p => matchGlob(relativePath, p) || relativePath.includes(p) || entry.includes(p));
|
|
111
|
+
if (excluded) continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
files.push(fullPath);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
walk(dirPath);
|
|
120
|
+
return files;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Get git-changed files in a directory
|
|
124
|
+
function getGitChangedFiles(dirPath) {
|
|
125
|
+
try {
|
|
126
|
+
const output = execFileSync('git', ['diff', '--name-only', 'HEAD'], {
|
|
127
|
+
cwd: dirPath, encoding: 'utf-8', timeout: 10000
|
|
128
|
+
});
|
|
129
|
+
const untrackedOutput = execFileSync('git', ['ls-files', '--others', '--exclude-standard'], {
|
|
130
|
+
cwd: dirPath, encoding: 'utf-8', timeout: 10000
|
|
131
|
+
});
|
|
132
|
+
const allFiles = [...output.trim().split('\n'), ...untrackedOutput.trim().split('\n')]
|
|
133
|
+
.filter(f => f.trim())
|
|
134
|
+
.map(f => resolve(dirPath, f))
|
|
135
|
+
.filter(f => existsSync(f));
|
|
136
|
+
return [...new Set(allFiles)];
|
|
137
|
+
} catch {
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Calculate security grade based on findings
|
|
143
|
+
function calculateGrade(totalIssues, totalFiles, errorCount) {
|
|
144
|
+
if (totalFiles === 0) return 'A';
|
|
145
|
+
const density = totalIssues / totalFiles;
|
|
146
|
+
|
|
147
|
+
if (errorCount === 0 && density === 0) return 'A';
|
|
148
|
+
if (errorCount === 0 && density < 0.5) return 'B';
|
|
149
|
+
if (errorCount <= 2 && density < 1.5) return 'C';
|
|
150
|
+
if (errorCount <= 5 && density < 3) return 'D';
|
|
151
|
+
return 'F';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function scanProject({ directory_path, recursive, include_patterns, exclude_patterns, diff_only, cross_file, verbosity }) {
|
|
155
|
+
const dirPath = resolve(directory_path);
|
|
156
|
+
|
|
157
|
+
if (!existsSync(dirPath)) {
|
|
158
|
+
return {
|
|
159
|
+
content: [{ type: "text", text: JSON.stringify({ error: "Directory not found" }) }]
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Load config from directory
|
|
164
|
+
const config = loadConfig(join(dirPath, 'dummy.js'));
|
|
165
|
+
const gitignorePatterns = parseGitignore(dirPath);
|
|
166
|
+
|
|
167
|
+
// Get files to scan
|
|
168
|
+
let files;
|
|
169
|
+
if (diff_only) {
|
|
170
|
+
files = getGitChangedFiles(dirPath);
|
|
171
|
+
} else {
|
|
172
|
+
files = walkDirectory(dirPath, {
|
|
173
|
+
recursive: recursive !== false,
|
|
174
|
+
includePatterns: include_patterns || [],
|
|
175
|
+
excludePatterns: exclude_patterns || [],
|
|
176
|
+
gitignorePatterns,
|
|
177
|
+
config
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Filter to scannable extensions
|
|
182
|
+
files = files.filter(f => {
|
|
183
|
+
const ext = extname(f).toLowerCase();
|
|
184
|
+
const base = basename(f).toLowerCase();
|
|
185
|
+
return SCANNABLE_EXTENSIONS.has(ext) || base === 'dockerfile';
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
if (files.length === 0) {
|
|
189
|
+
return {
|
|
190
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
191
|
+
directory: dirPath,
|
|
192
|
+
message: "No scannable files found",
|
|
193
|
+
files_scanned: 0,
|
|
194
|
+
grade: 'A'
|
|
195
|
+
}) }]
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Scan each file
|
|
200
|
+
const allIssues = [];
|
|
201
|
+
const byFile = {};
|
|
202
|
+
const bySeverity = { error: 0, warning: 0, info: 0 };
|
|
203
|
+
const byCategory = {};
|
|
204
|
+
|
|
205
|
+
for (const filePath of files) {
|
|
206
|
+
const result = await scanSecurity({ file_path: filePath, verbosity: 'full' });
|
|
207
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
208
|
+
|
|
209
|
+
if (parsed.issues && Array.isArray(parsed.issues)) {
|
|
210
|
+
const relativePath = relative(dirPath, filePath);
|
|
211
|
+
byFile[relativePath] = parsed.issues.length;
|
|
212
|
+
|
|
213
|
+
for (const issue of parsed.issues) {
|
|
214
|
+
allIssues.push({ ...issue, file: relativePath });
|
|
215
|
+
bySeverity[issue.severity] = (bySeverity[issue.severity] || 0) + 1;
|
|
216
|
+
const category = issue.ruleId?.split('.')[0] || 'other';
|
|
217
|
+
byCategory[category] = (byCategory[category] || 0) + 1;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Cross-file taint analysis (opt-in, max 50 files)
|
|
223
|
+
let crossFileIssues = [];
|
|
224
|
+
if (cross_file && files.length <= 50) {
|
|
225
|
+
try {
|
|
226
|
+
const { runCrossFileAnalyzer } = await import('../utils.js');
|
|
227
|
+
if (typeof runCrossFileAnalyzer === 'function') {
|
|
228
|
+
const crossResults = runCrossFileAnalyzer(files);
|
|
229
|
+
if (Array.isArray(crossResults)) {
|
|
230
|
+
crossFileIssues = crossResults;
|
|
231
|
+
for (const issue of crossFileIssues) {
|
|
232
|
+
const relativePath = relative(dirPath, issue.file || '');
|
|
233
|
+
allIssues.push({ ...issue, file: relativePath });
|
|
234
|
+
bySeverity[issue.severity] = (bySeverity[issue.severity] || 0) + 1;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
} catch {
|
|
239
|
+
// Cross-file analysis not available
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const grade = calculateGrade(allIssues.length, files.length, bySeverity.error);
|
|
244
|
+
const level = verbosity || 'compact';
|
|
245
|
+
|
|
246
|
+
if (level === 'minimal') {
|
|
247
|
+
return {
|
|
248
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
249
|
+
directory: dirPath,
|
|
250
|
+
files_scanned: files.length,
|
|
251
|
+
total: allIssues.length,
|
|
252
|
+
critical: bySeverity.error,
|
|
253
|
+
warning: bySeverity.warning,
|
|
254
|
+
info: bySeverity.info,
|
|
255
|
+
grade,
|
|
256
|
+
message: allIssues.length > 0
|
|
257
|
+
? `Found ${allIssues.length} issue(s) across ${files.length} files. Grade: ${grade}`
|
|
258
|
+
: `No issues found in ${files.length} files. Grade: ${grade}`
|
|
259
|
+
}) }]
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (level === 'compact') {
|
|
264
|
+
// Show top issues per file, sorted by severity
|
|
265
|
+
const topIssues = allIssues
|
|
266
|
+
.sort((a, b) => {
|
|
267
|
+
const order = { error: 0, warning: 1, info: 2 };
|
|
268
|
+
return (order[a.severity] || 2) - (order[b.severity] || 2);
|
|
269
|
+
})
|
|
270
|
+
.slice(0, 50)
|
|
271
|
+
.map(i => ({
|
|
272
|
+
file: i.file,
|
|
273
|
+
line: (i.line || 0) + 1,
|
|
274
|
+
ruleId: i.ruleId,
|
|
275
|
+
severity: i.severity,
|
|
276
|
+
message: i.message
|
|
277
|
+
}));
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
281
|
+
directory: dirPath,
|
|
282
|
+
files_scanned: files.length,
|
|
283
|
+
issues_count: allIssues.length,
|
|
284
|
+
grade,
|
|
285
|
+
by_severity: bySeverity,
|
|
286
|
+
by_category: byCategory,
|
|
287
|
+
cross_file_issues: crossFileIssues.length > 0 ? crossFileIssues.length : undefined,
|
|
288
|
+
issues: topIssues
|
|
289
|
+
}, null, 2) }]
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// full
|
|
294
|
+
return {
|
|
295
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
296
|
+
directory: dirPath,
|
|
297
|
+
files_scanned: files.length,
|
|
298
|
+
issues_count: allIssues.length,
|
|
299
|
+
grade,
|
|
300
|
+
by_severity: bySeverity,
|
|
301
|
+
by_category: byCategory,
|
|
302
|
+
by_file: byFile,
|
|
303
|
+
cross_file_issues: crossFileIssues.length > 0 ? crossFileIssues : undefined,
|
|
304
|
+
issues: allIssues,
|
|
305
|
+
scanned_files: files.map(f => relative(dirPath, f))
|
|
306
|
+
}, null, 2) }]
|
|
307
|
+
};
|
|
308
|
+
}
|
package/src/tools/scan-prompt.js
CHANGED
|
@@ -39,12 +39,6 @@ const CATEGORY_WEIGHTS = {
|
|
|
39
39
|
"prompt-injection-privilege": 0.85,
|
|
40
40
|
"prompt-injection-multi-turn": 0.7,
|
|
41
41
|
"prompt-injection-output": 0.9,
|
|
42
|
-
// OpenClaw-specific categories
|
|
43
|
-
"data_exfiltration": 1.0,
|
|
44
|
-
"messaging_abuse": 0.95,
|
|
45
|
-
"credential_theft": 1.0,
|
|
46
|
-
"autonomous_harm": 0.9,
|
|
47
|
-
"service_attack": 0.95,
|
|
48
42
|
"unknown": 0.5
|
|
49
43
|
};
|
|
50
44
|
|
|
@@ -195,69 +189,6 @@ function loadPromptInjectionRules() {
|
|
|
195
189
|
}
|
|
196
190
|
}
|
|
197
191
|
|
|
198
|
-
// Load OpenClaw-specific rules
|
|
199
|
-
function loadOpenClawRules() {
|
|
200
|
-
try {
|
|
201
|
-
const rulesPath = join(__dirname, '..', '..', 'rules', 'openclaw.security.yaml');
|
|
202
|
-
if (!existsSync(rulesPath)) {
|
|
203
|
-
return [];
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const yaml = readFileSync(rulesPath, 'utf-8');
|
|
207
|
-
const rules = [];
|
|
208
|
-
|
|
209
|
-
const ruleBlocks = yaml.split(/^ - id:/m).slice(1);
|
|
210
|
-
|
|
211
|
-
for (const block of ruleBlocks) {
|
|
212
|
-
const lines = (' - id:' + block).split('\n');
|
|
213
|
-
const rule = {
|
|
214
|
-
id: '',
|
|
215
|
-
severity: 'WARNING',
|
|
216
|
-
message: '',
|
|
217
|
-
patterns: [],
|
|
218
|
-
metadata: {}
|
|
219
|
-
};
|
|
220
|
-
|
|
221
|
-
let inPatterns = false;
|
|
222
|
-
|
|
223
|
-
for (const line of lines) {
|
|
224
|
-
if (line.match(/^\s+- id:\s*/)) {
|
|
225
|
-
rule.id = line.replace(/^\s+- id:\s*/, '').trim();
|
|
226
|
-
} else if (line.match(/^\s+severity:\s*/)) {
|
|
227
|
-
rule.severity = line.replace(/^\s+severity:\s*/, '').trim();
|
|
228
|
-
} else if (line.match(/^\s+category:\s*/)) {
|
|
229
|
-
rule.metadata.category = line.replace(/^\s+category:\s*/, '').trim();
|
|
230
|
-
} else if (line.match(/^\s+action:\s*/)) {
|
|
231
|
-
rule.metadata.action = line.replace(/^\s+action:\s*/, '').trim();
|
|
232
|
-
} else if (line.match(/^\s+message:\s*/)) {
|
|
233
|
-
rule.message = line.replace(/^\s+message:\s*["']?/, '').replace(/["']$/, '').trim();
|
|
234
|
-
} else if (line.match(/^\s+patterns:\s*$/)) {
|
|
235
|
-
inPatterns = true;
|
|
236
|
-
} else if (inPatterns && line.match(/^\s+- /)) {
|
|
237
|
-
let pattern = line.replace(/^\s+- /, '').trim();
|
|
238
|
-
pattern = pattern.replace(/^["']|["']$/g, '');
|
|
239
|
-
pattern = pattern.replace(/\\\\/g, '\\');
|
|
240
|
-
if (pattern) rule.patterns.push(pattern);
|
|
241
|
-
} else if (line.match(/^\s+\w+:/) && !line.match(/^\s+- /)) {
|
|
242
|
-
inPatterns = false;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
if (rule.id && rule.patterns.length > 0) {
|
|
247
|
-
// Set confidence and risk score based on severity
|
|
248
|
-
rule.metadata.confidence = rule.severity === 'CRITICAL' ? 'HIGH' : 'MEDIUM';
|
|
249
|
-
rule.metadata.risk_score = rule.severity === 'CRITICAL' ? '90' : '70';
|
|
250
|
-
rules.push(rule);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
return rules;
|
|
255
|
-
} catch (error) {
|
|
256
|
-
console.error("Error loading OpenClaw rules:", error.message);
|
|
257
|
-
return [];
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
192
|
// Calculate risk score from findings
|
|
262
193
|
function calculateRiskScore(findings, context) {
|
|
263
194
|
if (findings.length === 0) return 0;
|
|
@@ -446,8 +377,7 @@ export async function scanAgentPrompt({ prompt_text, context, verbosity }) {
|
|
|
446
377
|
// Load rules
|
|
447
378
|
const agentRules = loadAgentAttackRules();
|
|
448
379
|
const promptRules = loadPromptInjectionRules();
|
|
449
|
-
const
|
|
450
|
-
const allRules = [...agentRules, ...promptRules, ...openclawRules];
|
|
380
|
+
const allRules = [...agentRules, ...promptRules];
|
|
451
381
|
|
|
452
382
|
// 2.7: Extract content from code blocks and append to scan text
|
|
453
383
|
let expandedText = prompt_text;
|
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
|
-
```
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: security-scanner
|
|
3
|
-
description: Use when scanning files for security vulnerabilities. Runs comprehensive security analysis via subagent, returns concise actionable summary to main context.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Security Scanner Skill
|
|
7
|
-
|
|
8
|
-
You are a security scanning subagent. Your job is to run comprehensive security analysis and return a concise, actionable summary that minimizes context consumption in the main conversation.
|
|
9
|
-
|
|
10
|
-
## Workflow
|
|
11
|
-
|
|
12
|
-
1. **Scan the file** using `mcp__security-scanner__scan_security` with `verbosity: 'full'`
|
|
13
|
-
2. **Analyze findings** - group by severity, identify patterns
|
|
14
|
-
3. **If fixes needed**, use `mcp__security-scanner__fix_security` with `verbosity: 'full'`
|
|
15
|
-
4. **Return concise summary** (not the full JSON output)
|
|
16
|
-
|
|
17
|
-
## Response Format
|
|
18
|
-
|
|
19
|
-
Return ONLY this format to the main conversation:
|
|
20
|
-
|
|
21
|
-
```
|
|
22
|
-
## Security Scan: {filename}
|
|
23
|
-
|
|
24
|
-
**Status:** {PASS | WARN | FAIL}
|
|
25
|
-
**Issues:** {critical} critical, {warning} warning, {info} info
|
|
26
|
-
|
|
27
|
-
{If issues found:}
|
|
28
|
-
### Priority Fixes
|
|
29
|
-
1. **Line {N}**: {rule} - {one-line fix description}
|
|
30
|
-
2. **Line {N}**: {rule} - {one-line fix description}
|
|
31
|
-
{limit to top 5}
|
|
32
|
-
|
|
33
|
-
### Auto-Fix Available
|
|
34
|
-
Run `mcp__security-scanner__fix_security` to automatically apply {N} fixes.
|
|
35
|
-
|
|
36
|
-
{If no issues:}
|
|
37
|
-
No security issues detected.
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
## Rules
|
|
41
|
-
|
|
42
|
-
- DO use `verbosity: 'full'` internally for complete analysis
|
|
43
|
-
- DO return only the summary format above to the main conversation
|
|
44
|
-
- DO NOT include raw JSON in your response
|
|
45
|
-
- DO NOT include metadata, CWE references, or verbose explanations
|
|
46
|
-
- DO prioritize fixes by severity (critical > warning > info)
|
|
47
|
-
- DO limit to top 5 issues if more than 5 found
|
|
48
|
-
- DO mention auto-fix availability if fixes can be applied
|
|
49
|
-
|
|
50
|
-
## Example
|
|
51
|
-
|
|
52
|
-
User asks: "Scan app.py for security issues"
|
|
53
|
-
|
|
54
|
-
You run internally:
|
|
55
|
-
```
|
|
56
|
-
mcp__security-scanner__scan_security({ file_path: "app.py", verbosity: "full" })
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
You return:
|
|
60
|
-
```
|
|
61
|
-
## Security Scan: app.py
|
|
62
|
-
|
|
63
|
-
**Status:** WARN
|
|
64
|
-
**Issues:** 1 critical, 3 warning, 0 info
|
|
65
|
-
|
|
66
|
-
### Priority Fixes
|
|
67
|
-
1. **Line 15**: sql-injection - Use parameterized query instead of string concat
|
|
68
|
-
2. **Line 28**: hardcoded-secret - Move API key to environment variable
|
|
69
|
-
3. **Line 42**: weak-crypto-md5 - Replace MD5 with SHA-256
|
|
70
|
-
4. **Line 67**: ssl-verify-disabled - Enable SSL certificate verification
|
|
71
|
-
|
|
72
|
-
### Auto-Fix Available
|
|
73
|
-
Run fix_security to automatically apply 4 fixes.
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
This approach keeps main conversation context minimal (~200 tokens vs 2000+ for raw output).
|