agent-security-scanner-mcp 3.11.0 → 3.12.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
@@ -28,7 +28,7 @@ Security scanner for AI coding agents and autonomous assistants. Scans code for
28
28
  | `scan_agent_action` | Pre-execution safety check for agent actions (bash, file ops, HTTP). Returns ALLOW/WARN/BLOCK | Before running any agent-generated shell command or file operation |
29
29
  | `scan_mcp_server` | Scan MCP server source for vulnerabilities: unicode poisoning, name spoofing, rug pull detection, manifest analysis. Returns A-F grade | When auditing or installing an MCP server |
30
30
  | `scan_skill` | Deep security scan of an OpenClaw skill: prompt injection, AST+taint code analysis, ClawHavoc malware signatures, supply chain, rug pull. Returns A-F grade | Before installing any OpenClaw skill |
31
- | `clawproof_health` | Check ClawProof plugin health: engine status, daemon status, package data availability | Diagnostics and plugin status |
31
+ | `scanner_health` | Check plugin health: engine status, daemon status, package data availability | Diagnostics and plugin status |
32
32
  | `list_security_rules` | List available security rules and fix templates | To check rule coverage for a language |
33
33
 
34
34
  ## Quick Start
@@ -1110,7 +1110,7 @@ All MCP tools support a `verbosity` parameter to minimize context window consump
1110
1110
  ### v3.10.0
1111
1111
  - **`scan_skill` Tool** — 6-layer deep security scanner for OpenClaw skills: prompt injection (59+ rules), AST+taint code analysis, ClawHavoc malware signatures, package supply chain verification, and SHA-256 rug pull detection. Returns A-F grade with hard-fail on ClawHavoc/rug pull/critical findings
1112
1112
  - **ClawHavoc Signature Database** (`rules/clawhavoc.yaml`) — 27 rules, 121 regex patterns across 10 threat categories (reverse shells, crypto miners, info stealers, keyloggers, screen capture, DNS exfiltration, C2 beacons, OpenClaw-specific attacks, campaign patterns, exfil endpoints), mapped to MITRE ATT&CK
1113
- - **OpenClaw Plugin Skeleton** — Native plugin manifest (`openclaw.plugin.json`), config loader (`~/.openclaw/scanner-config.json`), and health check endpoint (`clawproof_health` MCP tool)
1113
+ - **OpenClaw Plugin Skeleton** — Native plugin manifest (`openclaw.plugin.json`), config loader (`~/.openclaw/scanner-config.json`), and health check endpoint (`scanner_health` MCP tool)
1114
1114
  - **CLI**: `scan-skill <path>` command with `--baseline` flag; `audit` and `harden` stubs (experimental)
1115
1115
  - **Security fixes**: Path containment uses `realpathSync` to prevent symlink bypass; dedup key includes `source` to prevent ClawHavoc findings from being suppressed by same-named code_analysis findings
1116
1116
  - **Bug fix**: SQL injection concat detection now covers JavaScript (was C#-only) — single-quoted and template literal strings now detected
package/index.js CHANGED
@@ -35,11 +35,18 @@ try {
35
35
  __dirname = process.cwd();
36
36
  }
37
37
 
38
+ // Read version from package.json
39
+ let _pkgVersion = '0.0.0';
40
+ try {
41
+ const pkg = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf-8'));
42
+ _pkgVersion = pkg.version || '0.0.0';
43
+ } catch { /* fallback */ }
44
+
38
45
  // Create MCP Server
39
46
  const server = new McpServer(
40
47
  {
41
- name: "security-scanner",
42
- version: "1.0.0",
48
+ name: "agent-security-scanner-mcp",
49
+ version: _pkgVersion,
43
50
  },
44
51
  {
45
52
  capabilities: {
@@ -163,7 +170,7 @@ server.tool(
163
170
  // Register scan_agent_action tool
164
171
  server.tool(
165
172
  "scan_agent_action",
166
- "Pre-execution security check for agent actions (bash, file_write, file_read, http_request, file_delete). Returns ALLOW/WARN/BLOCK. Lighter than scan_agent_prompt — evaluates concrete actions.",
173
+ "Pre-execution security check for agent actions (bash, file_write, file_read, http_request, file_delete, cron, process_spawn, git, docker). Returns ALLOW/WARN/BLOCK.",
167
174
  scanAgentActionSchema,
168
175
  scanAgentAction
169
176
  );
@@ -202,17 +209,27 @@ server.tool(
202
209
  // PLUGIN HEALTH CHECK
203
210
  // ===========================================
204
211
 
212
+ const _healthHandler = async () => {
213
+ const { getHealthStatus } = await import('./src/plugin-health.js');
214
+ const health = await getHealthStatus();
215
+ return {
216
+ content: [{ type: "text", text: JSON.stringify(health, null, 2) }]
217
+ };
218
+ };
219
+
205
220
  server.tool(
206
- "clawproof_health",
221
+ "scanner_health",
207
222
  "Check plugin health: engine status, daemon status, package data availability",
208
223
  {},
209
- async () => {
210
- const { getHealthStatus } = await import('./src/plugin-health.js');
211
- const health = await getHealthStatus();
212
- return {
213
- content: [{ type: "text", text: JSON.stringify(health, null, 2) }]
214
- };
215
- }
224
+ _healthHandler
225
+ );
226
+
227
+ // Backward-compatible alias (will be removed in a future major version)
228
+ server.tool(
229
+ "clawproof_health",
230
+ "Alias for scanner_health (deprecated, use scanner_health instead)",
231
+ {},
232
+ _healthHandler
216
233
  );
217
234
 
218
235
  // ===========================================
@@ -373,8 +390,9 @@ if (cliArgs[0] === 'init') {
373
390
  });
374
391
  } else if (cliArgs[0] === 'benchmark') {
375
392
  // CLI mode: benchmark [--save] [--json-only] [--compare-latest] [--corpus <path>]
393
+ const { resolvePythonCommand, pythonArgs } = await import('./src/python.js');
376
394
  const benchmarkPath = join(__dirname, 'benchmarks', 'benchmark_runner.py');
377
- const benchArgs = [benchmarkPath];
395
+ const benchArgs = [...pythonArgs(), benchmarkPath];
378
396
 
379
397
  // Pass through supported flags
380
398
  for (let i = 1; i < cliArgs.length; i++) {
@@ -387,7 +405,7 @@ if (cliArgs[0] === 'init') {
387
405
  }
388
406
 
389
407
  try {
390
- execFileSync('python3', benchArgs, { stdio: 'inherit', timeout: 300000 });
408
+ execFileSync(resolvePythonCommand(), benchArgs, { stdio: 'inherit', timeout: 300000 });
391
409
  } catch (err) {
392
410
  if (err.status) process.exit(err.status);
393
411
  console.error(`Benchmark error: ${err.message}`);
@@ -418,7 +436,7 @@ if (cliArgs[0] === 'init') {
418
436
  const actionValue = cliArgs[2];
419
437
  if (!actionType || !actionValue) {
420
438
  console.error('Usage: agent-security-scanner-mcp scan-action <type> <value> [--verbosity minimal|compact|full]');
421
- console.error('Types: bash, file_write, file_read, http_request, file_delete');
439
+ console.error('Types: bash, file_write, file_read, http_request, file_delete, cron, process_spawn, git, docker');
422
440
  process.exit(1);
423
441
  }
424
442
  const verbosityIdx = cliArgs.indexOf('--verbosity');
@@ -1,6 +1,6 @@
1
1
  {
2
- "name": "agent-security-scanner",
3
- "version": "3.9.0",
2
+ "name": "agent-security-scanner-mcp",
3
+ "version": "3.10.3",
4
4
  "description": "Security scanner for OpenClaw: prompt injection firewall, package hallucination detection, code vulnerability scanning, auto-fix",
5
5
  "author": "Sinewave AI",
6
6
  "license": "MIT",
@@ -31,7 +31,7 @@
31
31
  "description": "Scan an OpenClaw skill for security threats"
32
32
  },
33
33
  {
34
- "name": "clawproof_health",
34
+ "name": "scanner_health",
35
35
  "description": "Check plugin health status"
36
36
  }
37
37
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-security-scanner-mcp",
3
- "version": "3.11.0",
3
+ "version": "3.12.0",
4
4
  "mcpName": "io.github.sinewaveai/agent-security-scanner-mcp",
5
5
  "description": "Security scanner MCP server for AI coding agents. Prompt injection firewall, package hallucination detection (4.3M+ packages), 1000+ vulnerability rules with AST & taint analysis, auto-fix. For Claude Code, Cursor, Windsurf, Cline, OpenClaw.",
6
6
  "main": "index.js",
@@ -91,6 +91,7 @@
91
91
  "src/config.js",
92
92
  "src/history.js",
93
93
  "src/typosquat.js",
94
+ "src/python.js",
94
95
  "src/plugin-config.js",
95
96
  "src/plugin-health.js",
96
97
  "openclaw.plugin.json",
package/src/cli/audit.js CHANGED
@@ -1,10 +1,11 @@
1
1
  // src/cli/audit.js — OpenClaw configuration security audit (stub)
2
2
 
3
3
  export async function runAudit(args) {
4
- console.log('\n Security Audit [EXPERIMENTAL STUB]\n');
4
+ const allowStub = args.includes('--allow-stub');
5
+
6
+ console.log('\n Security Audit [NOT YET IMPLEMENTED]\n');
5
7
  console.log(' This command will check your OpenClaw configuration for security issues.');
6
- console.log(' Full implementation coming in Sprint 3.\n');
7
- console.log(' WARNING: This is an experimental stub. No actual checks are performed.\n');
8
+ console.log(' Implementation is in progress.\n');
8
9
  console.log(' Planned checks (60+):');
9
10
  console.log(' - Gateway: bind mode, auth, token strength, HTTPS, CORS');
10
11
  console.log(' - Permissions: config files, credentials, session transcripts');
@@ -15,4 +16,10 @@ export async function runAudit(args) {
15
16
  console.log(' - Plugins: unsigned, permissive, outdated');
16
17
  console.log(' - Credentials: plaintext secrets, exposed API keys\n');
17
18
  console.log(' OWASP ASI Top 10 mapping for all findings.\n');
19
+
20
+ if (!allowStub) {
21
+ console.error(' ERROR: audit is not yet implemented. No checks were performed.');
22
+ console.error(' Pass --allow-stub to suppress this error in CI.\n');
23
+ throw new Error('audit command is not yet implemented');
24
+ }
18
25
  }
package/src/cli/demo.js CHANGED
@@ -4,6 +4,7 @@ import { join } from "path";
4
4
  import { createInterface } from "readline";
5
5
  import { dirname } from "path";
6
6
  import { fileURLToPath } from "url";
7
+ import { resolvePythonCommand, pythonArgs } from "../python.js";
7
8
 
8
9
  // Handle both ESM and CJS bundling (Smithery bundles to CJS)
9
10
  let __dirname;
@@ -178,22 +179,11 @@ export async function runDemo(args) {
178
179
 
179
180
  // Run the analyzer
180
181
  const analyzerPath = join(__dirname, '..', '..', 'analyzer.py');
181
- let pythonCmd = 'python3';
182
- const py3 = checkCommand('python3', ['--version']);
183
- if (!py3.ok) {
184
- const py = checkCommand('python', ['--version']);
185
- if (py.ok && py.output.includes('3.')) {
186
- pythonCmd = 'python';
187
- } else {
188
- console.log(` Error: Python 3 not found. Run "npx agent-security-scanner-mcp doctor" to diagnose.\n`);
189
- unlinkSync(filepath);
190
- process.exit(1);
191
- }
192
- }
182
+ const pythonCmd = resolvePythonCommand();
193
183
 
194
184
  let results;
195
185
  try {
196
- const output = execFileSync(pythonCmd, [analyzerPath, filepath], { timeout: 30000, encoding: 'utf-8' });
186
+ const output = execFileSync(pythonCmd, [...pythonArgs(), analyzerPath, filepath], { timeout: 30000, encoding: 'utf-8' });
197
187
  results = JSON.parse(output);
198
188
  } catch (e) {
199
189
  console.log(` Error running analyzer: ${e.message}\n`);
package/src/cli/doctor.js CHANGED
@@ -4,6 +4,7 @@ import { dirname, join } from "path";
4
4
  import { homedir, platform } from "os";
5
5
  import { fileURLToPath } from "url";
6
6
  import { getDaemonClient } from '../daemon-client.js';
7
+ import { resolvePythonCommand, pythonArgs } from '../python.js';
7
8
 
8
9
  // Handle both ESM and CJS bundling (Smithery bundles to CJS)
9
10
  let __dirname;
@@ -123,22 +124,23 @@ export async function runDoctor(args) {
123
124
  issues++;
124
125
  }
125
126
 
126
- // 2. Python 3
127
+ // 2. Python 3 (uses the same resolver as the runtime)
127
128
  let pythonCmd = null;
128
- const py3 = checkCommand('python3', ['--version']);
129
- if (py3.ok) {
130
- pythonCmd = 'python3';
131
- console.log(` \u2713 ${py3.output}`);
132
- } else {
133
- const py = checkCommand('python', ['--version']);
134
- if (py.ok && py.output.includes('3.')) {
135
- pythonCmd = 'python';
136
- console.log(` \u2713 ${py.output}`);
129
+ try {
130
+ pythonCmd = resolvePythonCommand();
131
+ const pyVer = checkCommand(pythonCmd, [...pythonArgs(), '--version']);
132
+ if (pyVer.ok) {
133
+ console.log(` \u2713 ${pyVer.output} (resolved as '${pythonCmd}${pythonArgs().length ? ' ' + pythonArgs().join(' ') : ''}')`);
137
134
  } else {
135
+ pythonCmd = null;
138
136
  console.log(` \u2717 Python 3 not found`);
139
137
  console.log(` Install: https://python.org/downloads/`);
140
138
  issues++;
141
139
  }
140
+ } catch {
141
+ console.log(` \u2717 Python 3 not found`);
142
+ console.log(` Install: https://python.org/downloads/`);
143
+ issues++;
142
144
  }
143
145
 
144
146
  // 3. analyzer.py reachable
@@ -173,7 +175,7 @@ export async function runDoctor(args) {
173
175
 
174
176
  // 4. Python can import yaml (analyzer dependency check)
175
177
  if (pythonCmd && existsSync(analyzerPath)) {
176
- const yamlCheck = checkCommand(pythonCmd, ['-c', 'import yaml; print("ok")']);
178
+ const yamlCheck = checkCommand(pythonCmd, [...pythonArgs(), '-c', 'import yaml; print("ok")']);
177
179
  if (yamlCheck.ok && yamlCheck.output === 'ok') {
178
180
  console.log(` \u2713 Analyzer engine ready (PyYAML installed)`);
179
181
  } else {
@@ -184,7 +186,7 @@ export async function runDoctor(args) {
184
186
 
185
187
  // 5. tree-sitter AST engine (optional but recommended)
186
188
  if (pythonCmd) {
187
- const tsCheck = checkCommand(pythonCmd, ['-c', 'import tree_sitter; print(tree_sitter.__version__)']);
189
+ const tsCheck = checkCommand(pythonCmd, [...pythonArgs(), '-c', 'import tree_sitter; print(tree_sitter.__version__)']);
188
190
  if (tsCheck.ok && tsCheck.output) {
189
191
  console.log(` \u2713 AST engine ready (tree-sitter ${tsCheck.output})`);
190
192
  } else {
@@ -193,7 +195,7 @@ export async function runDoctor(args) {
193
195
  console.log(` Installing tree-sitter dependencies...`);
194
196
  const requirementsPath = join(__dirname, '..', '..', 'requirements.txt');
195
197
  if (existsSync(requirementsPath)) {
196
- const installResult = checkCommand(pythonCmd, ['-m', 'pip', 'install', '-r', requirementsPath, '--user', '--quiet']);
198
+ const installResult = checkCommand(pythonCmd, [...pythonArgs(), '-m', 'pip', 'install', '-r', requirementsPath, '--user', '--quiet']);
197
199
  if (installResult.ok) {
198
200
  console.log(` \u2713 Fixed: tree-sitter dependencies installed — AST engine enabled`);
199
201
  fixed++;
package/src/cli/harden.js CHANGED
@@ -1,10 +1,11 @@
1
1
  // src/cli/harden.js — OpenClaw auto-hardening (stub)
2
2
 
3
3
  export async function runHarden(args) {
4
- console.log('\n Auto-Hardening [EXPERIMENTAL STUB]\n');
4
+ const allowStub = args.includes('--allow-stub');
5
+
6
+ console.log('\n Auto-Hardening [NOT YET IMPLEMENTED]\n');
5
7
  console.log(' This command will automatically fix security issues in your OpenClaw config.');
6
- console.log(' Full implementation coming in Sprint 3.\n');
7
- console.log(' WARNING: This is an experimental stub. No actions are performed.\n');
8
+ console.log(' Implementation is in progress.\n');
8
9
  console.log(' Planned actions:');
9
10
  console.log(' - Bind gateway to 127.0.0.1');
10
11
  console.log(' - Enable token authentication');
@@ -12,4 +13,10 @@ export async function runHarden(args) {
12
13
  console.log(' - Disable mDNS discovery');
13
14
  console.log(' - Remove plaintext credentials\n');
14
15
  console.log(' Usage: agent-security-scanner-mcp harden --fix [--dry-run]\n');
16
+
17
+ if (!allowStub) {
18
+ console.error(' ERROR: harden is not yet implemented. No actions were performed.');
19
+ console.error(' Pass --allow-stub to suppress this error in CI.\n');
20
+ throw new Error('harden command is not yet implemented');
21
+ }
15
22
  }
package/src/cli/init.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { readFileSync, existsSync, writeFileSync, copyFileSync, mkdirSync } from "fs";
2
2
  import { spawnSync } from "child_process";
3
3
  import { dirname, join } from "path";
4
+ import { fileURLToPath } from "url";
4
5
  import { homedir, platform } from "os";
5
6
  import { createInterface } from "readline";
6
7
 
@@ -167,8 +168,13 @@ async function installOpenClawSkill(client, flags) {
167
168
  const skillFile = client.configPath();
168
169
 
169
170
  // Find the source skill file (bundled with the package)
170
- const __dirname = dirname(new URL(import.meta.url).pathname);
171
- const sourceSkill = join(__dirname, '..', '..', 'skills', 'openclaw', 'SKILL.md');
171
+ let __initDir;
172
+ try {
173
+ __initDir = dirname(fileURLToPath(import.meta.url));
174
+ } catch {
175
+ __initDir = process.cwd();
176
+ }
177
+ const sourceSkill = join(__initDir, '..', '..', 'skills', 'openclaw', 'SKILL.md');
172
178
 
173
179
  console.log(`\n Client: ${client.name}`);
174
180
  console.log(` Skill: ${skillDir}`);
@@ -248,9 +254,9 @@ async function installCodexMCP(flags, serverName) {
248
254
  console.log(` Config: ~/.codex/config.toml (managed by codex CLI)`);
249
255
  console.log(` OS: ${platform()} (${process.arch})\n`);
250
256
 
251
- // Check codex CLI is available
252
- const which = spawnSync('which', ['codex'], { encoding: 'utf-8' });
253
- if (which.status !== 0) {
257
+ // Check codex CLI is available (cross-platform: probe directly instead of using which/where)
258
+ const codexCheck = spawnSync('codex', ['--version'], { encoding: 'utf-8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'] });
259
+ if (codexCheck.status !== 0 || codexCheck.error) {
254
260
  console.error(` ERROR: 'codex' CLI not found in PATH.`);
255
261
  console.error(` Install it first: https://github.com/openai/codex\n`);
256
262
  process.exit(1);
package/src/config.js CHANGED
@@ -4,6 +4,7 @@
4
4
  import { existsSync, readFileSync } from 'fs';
5
5
  import { dirname, join, resolve, sep } from 'path';
6
6
  import { execFileSync } from 'child_process';
7
+ import { resolvePythonCommand, pythonArgs } from './python.js';
7
8
 
8
9
  const DEFAULT_CONFIG = {
9
10
  version: 1,
@@ -86,7 +87,9 @@ function findConfigFile(startPath) {
86
87
 
87
88
  function parseYaml(filePath) {
88
89
  try {
89
- const result = execFileSync('python3', [
90
+ const pyCmd = resolvePythonCommand();
91
+ const result = execFileSync(pyCmd, [
92
+ ...pythonArgs(),
90
93
  '-c',
91
94
  'import yaml,json,sys; print(json.dumps(yaml.safe_load(open(sys.argv[1]))))',
92
95
  filePath,
@@ -3,6 +3,7 @@ import { spawn } from 'child_process';
3
3
  import { createInterface } from 'readline';
4
4
  import { dirname, join } from 'path';
5
5
  import { fileURLToPath } from 'url';
6
+ import { resolvePythonCommand, pythonArgs } from './python.js';
6
7
 
7
8
  let __dirname;
8
9
  try {
@@ -64,7 +65,8 @@ class DaemonClient {
64
65
  // Cleanup any previous process
65
66
  this._cleanup();
66
67
 
67
- const proc = spawn('python3', [DAEMON_SCRIPT], {
68
+ const pyCmd = resolvePythonCommand();
69
+ const proc = spawn(pyCmd, [...pythonArgs(), DAEMON_SCRIPT], {
68
70
  stdio: ['pipe', 'pipe', 'pipe'],
69
71
  env: { ...process.env, PYTHONUNBUFFERED: '1' },
70
72
  });
package/src/python.js ADDED
@@ -0,0 +1,54 @@
1
+ // src/python.js — Cross-platform Python command resolution.
2
+ // Resolves the correct Python 3 command for the current platform:
3
+ // Windows: py -3 → python3 → python
4
+ // Unix: python3 → python
5
+
6
+ import { execFileSync } from 'child_process';
7
+ import { platform } from 'os';
8
+
9
+ let _cached = null;
10
+
11
+ /**
12
+ * Resolve the Python 3 command available on this system.
13
+ * Caches the result for the lifetime of the process.
14
+ * Returns 'python3' as a last-resort default if nothing is found
15
+ * (so callers get a clear "command not found" error rather than silence).
16
+ */
17
+ export function resolvePythonCommand() {
18
+ if (_cached !== null) return _cached;
19
+
20
+ const candidates = platform() === 'win32'
21
+ ? [['py', ['-3', '--version']], ['python3', ['--version']], ['python', ['--version']]]
22
+ : [['python3', ['--version']], ['python', ['--version']]];
23
+
24
+ for (const [cmd, args] of candidates) {
25
+ try {
26
+ const out = execFileSync(cmd, args, {
27
+ encoding: 'utf-8',
28
+ timeout: 5000,
29
+ stdio: ['pipe', 'pipe', 'pipe'],
30
+ });
31
+ if (out.includes('3.')) {
32
+ // For `py -3`, the actual command to use at runtime is ['py', '-3']
33
+ _cached = cmd === 'py' ? 'py' : cmd;
34
+ return _cached;
35
+ }
36
+ } catch {
37
+ // Not available, try next
38
+ }
39
+ }
40
+
41
+ // Fallback — callers will get "command not found" with a clear error
42
+ _cached = 'python3';
43
+ return _cached;
44
+ }
45
+
46
+ /**
47
+ * Returns the argument array prefix for invoking Python.
48
+ * For Windows `py -3`, returns ['-3']; otherwise returns [].
49
+ * Use: execFileSync(resolvePythonCommand(), [...pythonArgs(), scriptPath, ...])
50
+ */
51
+ export function pythonArgs() {
52
+ const cmd = resolvePythonCommand();
53
+ return cmd === 'py' ? ['-3'] : [];
54
+ }