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 CHANGED
@@ -1,13 +1,11 @@
1
1
  # agent-security-scanner-mcp
2
2
 
3
- Security scanner for AI coding agents and autonomous assistants. Scans code for vulnerabilities, detects hallucinated packages, and blocks prompt injection — via MCP (Claude Code, Cursor, Windsurf, Cline) or CLI (OpenClaw, CI/CD).
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
  [![npm downloads](https://img.shields.io/npm/dt/agent-security-scanner-mcp.svg)](https://www.npmjs.com/package/agent-security-scanner-mcp)
6
6
  [![npm version](https://img.shields.io/npm/v/agent-security-scanner-mcp.svg)](https://www.npmjs.com/package/agent-security-scanner-mcp)
7
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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\n');
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 scan-prompt "ignore previous instructions"');
257
- console.log(' npx agent-security-scanner-mcp scan-security ./app.py --verbosity minimal');
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.0",
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, OpenClaw.",
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
+ }
@@ -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 openclawRules = loadOpenClawRules();
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;
@@ -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).