ship-safe 6.3.0 → 6.4.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
@@ -18,9 +18,9 @@
18
18
 
19
19
  18 security agents. 80+ attack classes. One command.
20
20
 
21
- **Ship Safe v6.2.0** is an AI-powered security platform that runs 18 specialized agents in parallel against your codebase, scanning for secrets, injection vulnerabilities, auth bypass, SSRF, supply chain attacks, Supabase RLS misconfigs, Docker/Terraform/Kubernetes misconfigs, CI/CD pipeline poisoning, LLM/agentic AI security, MCP server misuse, RAG poisoning, PII compliance, vibe coding patterns, exception handling, AI agent config security, and more. OWASP 2025 scoring with EPSS exploit probability. LLM-powered deep analysis verifies exploitability of critical findings. Secrets verification probes provider APIs to check if leaked keys are still active. Compliance mapping to SOC 2, ISO 27001, and NIST AI RMF. Built-in threat intelligence feed with offline-first IOC matching. CI integration with GitHub PR comments, threshold gating, and SARIF output.
21
+ **Ship Safe v6.4.0** is an AI-powered security platform that runs 18 specialized agents in parallel against your codebase, scanning for secrets, injection vulnerabilities, auth bypass, SSRF, supply chain attacks, Supabase RLS misconfigs, Docker/Terraform/Kubernetes misconfigs, CI/CD pipeline poisoning, LLM/agentic AI security, MCP server misuse, RAG poisoning, PII compliance, vibe coding patterns, exception handling, AI agent config security, and more. OWASP 2025 scoring with EPSS exploit probability. LLM-powered deep analysis verifies exploitability of critical findings. Secrets verification probes provider APIs to check if leaked keys are still active. Compliance mapping to SOC 2, ISO 27001, and NIST AI RMF. Built-in threat intelligence feed with offline-first IOC matching. CI integration with GitHub PR comments, threshold gating, and SARIF output.
22
22
 
23
- **v6.2.0 highlights:** Real-time Claude Code hooks (`npx ship-safe hooks install`) block secrets before they land on disk. Universal LLM support — use Groq, Together AI, Mistral, DeepSeek, xAI, Perplexity, LM Studio, or any OpenAI-compatible endpoint for deep analysis. Supply chain IOC matching for known-compromised packages and CanisterWorm-style ICP blockchain C2 indicators.
23
+ **v6.4.0 highlights:** MCP server scanning (`npx ship-safe scan-mcp`) vets tool manifests for prompt injection and credential harvesting before you connect. Detection support for openclaude and claw-code the two most-starred Claude Code forks from the March 2026 source leak with accurate config scanning based on their actual architectures. Four new CI/CD patterns flag AI agent danger modes in pipelines. Legal dataset corrected: claw-code reclassified as a clean-room rewrite, not a leaked-source derivative.
24
24
 
25
25
  [Documentation](https://shipsafecli.com/docs) | [Blog](https://shipsafecli.com/blog) | [Pricing](https://shipsafecli.com/pricing)
26
26
 
@@ -169,10 +169,10 @@ npx ship-safe audit .
169
169
  | **PIIComplianceAgent** | Compliance | PII detection — SSNs, credit cards, emails, phone numbers in source code, logs, and configs |
170
170
  | **VibeCodingAgent** | Code Vulns | AI-generated code patterns — no input validation, empty catch blocks, hardcoded secrets, disabled security features, TODO-auth patterns |
171
171
  | **ExceptionHandlerAgent** | Code Vulns | OWASP A10:2025 — empty catch blocks, unhandled promise rejections, missing React error boundaries, leaked stack traces, generic catch-all without rethrow |
172
- | **AgentConfigScanner** | AI/LLM | AI agent config security — prompt injection in .cursorrules/CLAUDE.md/AGENTS.md/.windsurfrules, malicious Claude Code hooks (CVE-2026), OpenClaw public binding & malicious skills, encoded/obfuscated payloads, data exfiltration instructions, agent memory poisoning |
172
+ | **AgentConfigScanner** | AI/LLM | AI agent config security — prompt injection in .cursorrules/CLAUDE.md/AGENTS.md/.windsurfrules, malicious Claude Code hooks (CVE-2026), OpenClaw public binding & malicious skills, openclaude profile file (`OPENAI_BASE_URL` over http://), claw-code config (`danger-full-access`, disabled sandbox, shell hooks, insecure MCP transports), encoded/obfuscated payloads, data exfiltration instructions, agent memory poisoning |
173
173
  | **MobileScanner** | Mobile | OWASP Mobile Top 10 2024 — insecure storage, WebView JS injection, HTTP endpoints, excessive permissions, debug mode |
174
174
  | **GitHistoryScanner** | Secrets | Leaked secrets in git commit history (checks if still active in working tree) |
175
- | **CICDScanner** | CI/CD | OWASP CI/CD Top 10 — pipeline poisoning, unpinned actions, secret logging, self-hosted runners, script injection |
175
+ | **CICDScanner** | CI/CD | OWASP CI/CD Top 10 — pipeline poisoning, unpinned actions, secret logging, self-hosted runners, script injection, AI agent danger flags (`--dangerously-skip-permissions`, insecure provider URLs in CI) |
176
176
  | **APIFuzzer** | API | Routes without auth, missing input validation, mass assignment, unrestricted file upload, GraphQL introspection, debug endpoints, missing rate limiting, OpenAPI spec security issues |
177
177
  | **ReconAgent** | Recon | Attack surface discovery — frameworks, languages, auth patterns, databases, cloud providers, IaC, CI/CD pipelines |
178
178
 
@@ -299,13 +299,13 @@ npx ship-safe audit . --verify
299
299
  npx ship-safe doctor
300
300
  ```
301
301
 
302
- ### OpenClaw Security
302
+ ### Agent Security
303
303
 
304
304
  ```bash
305
305
  # Focused OpenClaw security scan
306
306
  npx ship-safe openclaw .
307
307
 
308
- # Auto-harden OpenClaw configs (0.0.0.0127.0.0.1, add auth, wswss)
308
+ # Auto-harden OpenClaw configs (0.0.0.0->127.0.0.1, add auth, ws->wss)
309
309
  npx ship-safe openclaw . --fix
310
310
 
311
311
  # Red team: simulate ClawJacked, prompt injection, data exfil attacks
@@ -319,6 +319,14 @@ npx ship-safe scan-skill https://clawhub.io/skills/some-skill
319
319
  npx ship-safe scan-skill ./local-skill.json
320
320
  npx ship-safe scan-skill --all # Scan all skills from openclaw.json
321
321
 
322
+ # Scan an MCP server's tool manifest before connecting
323
+ npx ship-safe scan-mcp https://your-mcp-server/
324
+ npx ship-safe scan-mcp ./local-manifest.json
325
+ npx ship-safe scan-mcp https://your-mcp-server/ --json
326
+
327
+ # Legal risk audit — DMCA, leaked-source derivatives (openclaude, claw-code-js), IP disputes
328
+ npx ship-safe legal .
329
+
322
330
  # Generate hardened OpenClaw config
323
331
  npx ship-safe init --openclaw
324
332
 
@@ -326,6 +334,20 @@ npx ship-safe init --openclaw
326
334
  npx ship-safe abom .
327
335
  ```
328
336
 
337
+ #### openclaude and claw-code
338
+
339
+ Ship Safe detects security issues in both major Claude Code forks from the March 2026 source leak.
340
+
341
+ **openclaude** (`@gitlawb/openclaude`) is a CLI tool that routes Claude Code's toolset through any OpenAI-compatible provider. Its only persistent file artifact is `.openclaude-profile.json`. Ship Safe flags:
342
+ - `OPENAI_BASE_URL` using `http://` for non-localhost endpoints (unencrypted LLM traffic)
343
+ - The profile file present in a project not covered by `.gitignore` (API key exposure risk)
344
+
345
+ **claw-code** (`ultraworkers/claw-code`) is a clean-room Rust + Python rewrite of Claude Code's agent harness. Its config lives in `.claw.json`, `.claw/settings.json`, and `.claw/settings.local.json`. Ship Safe flags:
346
+ - `permissionMode: danger-full-access` or `dangerouslySkipPermissions: true` (no confirmation on any tool call)
347
+ - `sandbox.enabled: false` (filesystem isolation removed)
348
+ - Hook commands containing shell execution or remote download patterns
349
+ - MCP server connections over `ws://` or `http://` to non-localhost hosts
350
+
329
351
  ### Threat Intelligence
330
352
 
331
353
  ```bash
@@ -54,6 +54,32 @@ const OPENCLAW_GLOBS = [
54
54
  '.openclaw/**/*.json',
55
55
  ];
56
56
 
57
+ // openclaude (github.com/Gitlawb/openclaude) — Claude Code fork with
58
+ // OpenAI-compatible provider shim. Config is purely via environment variables
59
+ // (CLAUDE_CODE_USE_OPENAI, OPENAI_BASE_URL, OPENAI_MODEL, OPENAI_API_KEY).
60
+ // The only persistent file artifact is .openclaude-profile.json, which stores
61
+ // named profiles as { name, env: { OPENAI_BASE_URL, OPENAI_API_KEY, ... } }.
62
+ const OPENCLAUDE_PROFILE_FILES = [
63
+ '.openclaude-profile.json',
64
+ ];
65
+
66
+ // claw-code (github.com/instructkr/claw-code, now ultraworkers/claw-code) —
67
+ // Rust + Python clean-room rewrite of Claude Code's agent harness.
68
+ // CLI tool (`claw` binary). NOT a server — no port binding outside tests.
69
+ // Config files: .claw.json (project root), .claw/settings.json,
70
+ // .claw/settings.local.json, ~/.claw.json, ~/.claw/settings.json
71
+ // Auth: ANTHROPIC_API_KEY, OPENAI_API_KEY, or XAI_API_KEY env vars.
72
+ // Permission modes: read-only, workspace-write, danger-full-access, prompt, allow.
73
+ // --dangerously-skip-permissions flag disables all permission checks.
74
+ // Sandbox: SandboxConfig with FilesystemIsolationMode (workspace-only default).
75
+ // Hooks: preToolUse / postToolUse arrays in settings JSON.
76
+ // MCP: mcpServers in settings JSON (stdio, sse, http, ws transports).
77
+ const CLAW_CODE_FILES = [
78
+ '.claw.json',
79
+ '.claw/settings.json',
80
+ '.claw/settings.local.json',
81
+ ];
82
+
57
83
  const MEMORY_GLOBS = [
58
84
  '.claude/memory/**',
59
85
  '.cursor/memory/**',
@@ -230,6 +256,18 @@ export class AgentConfigScanner extends BaseAgent {
230
256
  findings = findings.concat(this._scanOpenClawConfig(file));
231
257
  }
232
258
 
259
+ // ── 3b. Scan openclaude profile files ─────────────────────────────────
260
+ for (const file of discovered.openclaudeFiles) {
261
+ findings = findings.concat(this.scanFileWithPatterns(file, PATTERNS));
262
+ findings = findings.concat(this._scanOpenClaudeProfile(file));
263
+ }
264
+
265
+ // ── 3c. Scan claw-code config files ────────────────────────────────────
266
+ for (const file of discovered.clawCodeFiles) {
267
+ findings = findings.concat(this.scanFileWithPatterns(file, PATTERNS));
268
+ findings = findings.concat(this._scanClawCodeConfig(file));
269
+ }
270
+
233
271
  // ── 4. Scan Claude Code hooks ──────────────────────────────────────────
234
272
  for (const file of discovered.claudeSettingsFiles) {
235
273
  findings = findings.concat(this._scanClaudeHooks(file));
@@ -297,7 +335,21 @@ export class AgentConfigScanner extends BaseAgent {
297
335
  memoryFiles.push(...globbed);
298
336
  } catch { /* skip */ }
299
337
 
300
- return { rulesFiles, openclawFiles, claudeSettingsFiles, memoryFiles };
338
+ // ── openclaude profile files ─────────────────────────────────────────────
339
+ const openclaudeFiles = [];
340
+ for (const rel of OPENCLAUDE_PROFILE_FILES) {
341
+ const full = path.join(rootPath, rel);
342
+ if (fs.existsSync(full)) openclaudeFiles.push(full);
343
+ }
344
+
345
+ // ── claw-code config files ────────────────────────────────────────────────
346
+ const clawCodeFiles = [];
347
+ for (const rel of CLAW_CODE_FILES) {
348
+ const full = path.join(rootPath, rel);
349
+ if (fs.existsSync(full)) clawCodeFiles.push(full);
350
+ }
351
+
352
+ return { rulesFiles, openclawFiles, openclaudeFiles, clawCodeFiles, claudeSettingsFiles, memoryFiles };
301
353
  }
302
354
 
303
355
  // ===========================================================================
@@ -415,6 +467,178 @@ export class AgentConfigScanner extends BaseAgent {
415
467
  return findings;
416
468
  }
417
469
 
470
+ /**
471
+ * Scan .openclaude-profile.json for security issues.
472
+ *
473
+ * openclaude (github.com/Gitlawb/openclaude) is a CLI tool, not a server.
474
+ * There is no config file, no host/port binding, no auth mechanism.
475
+ * All configuration is via environment variables:
476
+ * CLAUDE_CODE_USE_OPENAI=1, OPENAI_BASE_URL, OPENAI_MODEL, OPENAI_API_KEY
477
+ *
478
+ * The only persistent file artifact is .openclaude-profile.json, which stores
479
+ * named profiles as { name: string, env: { OPENAI_BASE_URL, OPENAI_API_KEY, ... } }.
480
+ * openclaude ships with this file in its default .gitignore.
481
+ *
482
+ * Security risk: if OPENAI_BASE_URL is an http:// (non-TLS) endpoint, all
483
+ * LLM traffic (prompts, code context, responses) is sent unencrypted.
484
+ */
485
+ _scanOpenClaudeProfile(filePath) {
486
+ const content = this.readFile(filePath);
487
+ if (!content) return [];
488
+ const findings = [];
489
+
490
+ let profile;
491
+ try { profile = JSON.parse(content); } catch { return []; }
492
+
493
+ const env = profile.env || {};
494
+
495
+ // ── Insecure provider URL (http:// for non-localhost) ─────────────────
496
+ const baseUrl = env.OPENAI_BASE_URL || '';
497
+ if (baseUrl && /^http:\/\/(?!localhost|127\.0\.0\.1|::1)/i.test(baseUrl)) {
498
+ findings.push(createFinding({
499
+ file: filePath, line: 1,
500
+ severity: 'high',
501
+ category: this.category,
502
+ rule: 'OPENCLAUDE_INSECURE_PROVIDER_URL',
503
+ title: 'openclaude: LLM Provider URL Without TLS',
504
+ description:
505
+ `openclaude routes model calls to ${baseUrl} over plain HTTP. ` +
506
+ 'Prompts, code context, and model responses are sent unencrypted. ' +
507
+ 'A network attacker can read or modify all LLM interactions in transit.',
508
+ matched: `OPENAI_BASE_URL: "${baseUrl}"`,
509
+ confidence: 'high',
510
+ cwe: 'CWE-319',
511
+ owasp: 'A02:2021',
512
+ fix: 'Use an https:// provider URL. Never route LLM traffic over plaintext HTTP on untrusted networks.',
513
+ }));
514
+ }
515
+
516
+ return findings;
517
+ }
518
+
519
+ /**
520
+ * Scan claw-code config files (.claw.json, .claw/settings.json, .claw/settings.local.json)
521
+ * for insecure settings.
522
+ *
523
+ * claw-code (ultraworkers/claw-code) is a Rust + Python clean-room rewrite of Claude Code.
524
+ * It is a CLI tool — no server port binding. Config lives in JSON settings files.
525
+ *
526
+ * Checked settings:
527
+ * - hooks.preToolUse / hooks.postToolUse: shell hook commands (RCE vector)
528
+ * - permissions.dangerouslySkipPermissions / permissionMode: "danger-full-access"
529
+ * - sandbox.enabled: false (filesystem isolation disabled)
530
+ * - mcpServers with insecure ws:// or http:// remote URLs (MiTM risk)
531
+ * - mcpServers using env vars that could expose credentials
532
+ */
533
+ _scanClawCodeConfig(filePath) {
534
+ const content = this.readFile(filePath);
535
+ if (!content) return [];
536
+ const findings = [];
537
+
538
+ let config;
539
+ try { config = JSON.parse(content); } catch { return []; }
540
+
541
+ // ── Dangerous permission mode ─────────────────────────────────────────
542
+ const permMode = config.permissionMode ?? config.permissions?.mode ?? '';
543
+ const skipPerms = config.dangerouslySkipPermissions ??
544
+ config.permissions?.dangerouslySkipPermissions ?? false;
545
+
546
+ if (skipPerms === true || permMode === 'danger-full-access') {
547
+ findings.push(createFinding({
548
+ file: filePath, line: 1,
549
+ severity: 'high',
550
+ category: this.category,
551
+ rule: 'CLAW_CODE_SKIP_PERMISSIONS',
552
+ title: 'claw-code: All Permission Checks Disabled',
553
+ description:
554
+ 'claw-code is configured with dangerously-skip-permissions or permissionMode: danger-full-access. ' +
555
+ 'Every tool call executes without asking for user confirmation. ' +
556
+ 'A single prompt injection in any file the agent reads can trigger unrestricted shell execution or file writes.',
557
+ matched: skipPerms ? 'dangerouslySkipPermissions: true' : `permissionMode: "${permMode}"`,
558
+ confidence: 'high',
559
+ cwe: 'CWE-269',
560
+ owasp: 'ASI03',
561
+ fix: 'Set permissionMode to "workspace-write" or "prompt". Only use danger-full-access in fully isolated environments.',
562
+ }));
563
+ }
564
+
565
+ // ── Sandbox disabled ──────────────────────────────────────────────────
566
+ if (config.sandbox?.enabled === false) {
567
+ findings.push(createFinding({
568
+ file: filePath, line: 1,
569
+ severity: 'medium',
570
+ category: this.category,
571
+ rule: 'CLAW_CODE_SANDBOX_DISABLED',
572
+ title: 'claw-code: Filesystem Sandbox Disabled',
573
+ description:
574
+ 'claw-code sandbox is explicitly disabled. By default claw-code restricts ' +
575
+ 'filesystem access to the workspace directory. With sandbox off, tools can ' +
576
+ 'read and write anywhere on the system.',
577
+ matched: 'sandbox.enabled: false',
578
+ confidence: 'high',
579
+ cwe: 'CWE-732',
580
+ owasp: 'ASI03',
581
+ fix: 'Remove sandbox.enabled: false or set filesystem-mode to workspace-only.',
582
+ }));
583
+ }
584
+
585
+ // ── Hooks with shell commands ──────────────────────────────────────────
586
+ const hookLists = [
587
+ ...(config.hooks?.preToolUse || []),
588
+ ...(config.hooks?.postToolUse || []),
589
+ ];
590
+ for (const hook of hookLists) {
591
+ const cmd = typeof hook === 'string' ? hook : (hook.command || hook.cmd || hook.run || '');
592
+ if (!cmd) continue;
593
+ if (/(?:bash\s+-c|sh\s+-c|cmd\s+\/c|powershell\s+-|pwsh\s+-)/i.test(cmd) ||
594
+ /\|\s*(?:bash|sh|zsh|node|python)/i.test(cmd) ||
595
+ /(?:curl|wget)\s+https?:\/\/(?!localhost|127\.0\.0\.1)/i.test(cmd)) {
596
+ findings.push(createFinding({
597
+ file: filePath, line: 1,
598
+ severity: 'critical',
599
+ category: this.category,
600
+ rule: 'CLAW_CODE_HOOK_SHELL',
601
+ title: 'claw-code: Dangerous Hook Command',
602
+ description:
603
+ 'claw-code hook contains a shell execution or remote download command. ' +
604
+ 'A malicious .claw.json in a repository can achieve RCE when anyone ' +
605
+ 'opens the project with claw.',
606
+ matched: cmd.substring(0, 150),
607
+ confidence: 'high',
608
+ cwe: 'CWE-94',
609
+ owasp: 'ASI04',
610
+ fix: 'Remove shell execution hooks. Use only safe, scoped commands in claw hooks.',
611
+ }));
612
+ }
613
+ }
614
+
615
+ // ── MCP servers with insecure remote URLs ─────────────────────────────
616
+ const mcpServers = config.mcpServers || {};
617
+ for (const [name, srv] of Object.entries(mcpServers)) {
618
+ const url = typeof srv === 'object' ? (srv.url || '') : '';
619
+ if (/^(?:ws|http):\/\/(?!localhost|127\.0\.0\.1|::1)/i.test(url)) {
620
+ findings.push(createFinding({
621
+ file: filePath, line: 1,
622
+ severity: 'high',
623
+ category: this.category,
624
+ rule: 'CLAW_CODE_MCP_INSECURE_URL',
625
+ title: `claw-code: MCP Server "${name}" Uses Unencrypted Transport`,
626
+ description:
627
+ `MCP server "${name}" connects to ${url} over an unencrypted channel (ws:// or http://). ` +
628
+ 'All MCP messages — tool calls, results, and any code context — are sent in plaintext. ' +
629
+ 'A network attacker can intercept or inject MCP responses to hijack the agent.',
630
+ matched: url,
631
+ confidence: 'high',
632
+ cwe: 'CWE-319',
633
+ owasp: 'A02:2021',
634
+ fix: 'Use wss:// or https:// for all non-localhost MCP server connections.',
635
+ }));
636
+ }
637
+ }
638
+
639
+ return findings;
640
+ }
641
+
418
642
  /**
419
643
  * Scan .claude/settings.json for malicious hooks.
420
644
  * Based on Check Point Research disclosure: hooks in settings.json
@@ -199,6 +199,48 @@ const PATTERNS = [
199
199
  description: 'GitHub expression in run step. Attacker-controlled values can inject shell commands.',
200
200
  fix: 'Use environment variables: env: TITLE: ${{ github.event.issue.title }} then run: echo "$TITLE"',
201
201
  },
202
+
203
+ // ── AI Agent CLI — dangerous flags in CI ──────────────────────────────────
204
+ {
205
+ rule: 'CICD_AGENT_SKIP_PERMISSIONS',
206
+ title: 'CI/CD: AI Agent Running Without Permission Checks',
207
+ regex: /(?:--dangerously-skip-permissions|--skip-permissions|permissionMode\s*[=:]\s*["']?danger-full-access["']?)/g,
208
+ severity: 'critical',
209
+ cwe: 'CWE-269',
210
+ owasp: 'ASI03',
211
+ description: 'AI agent CLI flag disables all permission checks. In CI, this means any prompt injection in workspace files can execute arbitrary commands with no confirmation gate.',
212
+ fix: 'Remove --dangerously-skip-permissions. Use --permission-mode=workspace-write for CI automation or scope tool access with --allowedTools.',
213
+ },
214
+ {
215
+ rule: 'CICD_AGENT_INSECURE_PROVIDER',
216
+ title: 'CI/CD: AI Agent Routing to Non-TLS Provider',
217
+ regex: /(?:OPENAI_BASE_URL|ANTHROPIC_BASE_URL|XAI_BASE_URL|CLAUDE_CODE_USE_OPENAI)\s*=\s*["']?http:\/\/(?!localhost|127\.0\.0\.1|::1)/g,
218
+ severity: 'high',
219
+ cwe: 'CWE-319',
220
+ owasp: 'A02:2021',
221
+ description: 'AI agent provider URL uses plain HTTP for a non-localhost endpoint. All prompts, code context, and model responses are transmitted unencrypted in CI.',
222
+ fix: 'Use https:// for all non-localhost AI provider base URLs.',
223
+ },
224
+ {
225
+ rule: 'CICD_OPENCLAUDE_IN_CI',
226
+ title: 'CI/CD: openclaude Running in CI Pipeline',
227
+ regex: /(?:^|\s)openclaude\s/gm,
228
+ severity: 'medium',
229
+ cwe: 'CWE-1188',
230
+ owasp: 'ASI03',
231
+ description: 'openclaude (Claude Code fork with OpenAI-compatible shim) is invoked in CI. Verify OPENAI_BASE_URL is https://, OPENAI_API_KEY is stored as a secret, and .openclaude-profile.json is not committed.',
232
+ fix: 'Ensure OPENAI_API_KEY is a CI secret, OPENAI_BASE_URL uses https://, and .openclaude-profile.json is gitignored.',
233
+ },
234
+ {
235
+ rule: 'CICD_CLAW_DANGER_MODE',
236
+ title: 'CI/CD: claw-code Running in Danger Mode',
237
+ regex: /(?:^|\s)claw\s[^\n]*--dangerously-skip-permissions/gm,
238
+ severity: 'critical',
239
+ cwe: 'CWE-269',
240
+ owasp: 'ASI03',
241
+ description: 'claw-code (Rust/Python Claude Code rewrite) is invoked with --dangerously-skip-permissions in CI. Any prompt injection in the workspace executes without confirmation.',
242
+ fix: 'Remove --dangerously-skip-permissions. Use --permission-mode=workspace-write for CI automation.',
243
+ },
202
244
  ];
203
245
 
204
246
  export class CICDScanner extends BaseAgent {
@@ -38,22 +38,14 @@ export const LEGALLY_RISKY_PACKAGES = [
38
38
  // Anthropic's Claude Code source was accidentally leaked. Several repos
39
39
  // appeared immediately; Anthropic filed DMCA takedowns but derivatives
40
40
  // remain online. Shipping any of these exposes you to IP liability.
41
+ //
42
+ // NOTE: The instructkr/claw-code repo (now ultraworkers/claw-code) has since
43
+ // pivoted to a clean-room Rust + Python rewrite and explicitly removed the
44
+ // leaked snapshot. The GitHub repo itself is no longer a DMCA concern.
45
+ // However, any claw-code npm package published in the March 31–April 2 2026
46
+ // window may have contained the leaked TypeScript source before the pivot.
47
+ // Flag early versions as a precaution; assess the specific published version.
41
48
  // ---------------------------------------------------------------------------
42
- {
43
- name: 'claw-code',
44
- versions: '*',
45
- ecosystem: 'npm',
46
- risk: 'dmca',
47
- severity: 'high',
48
- detail:
49
- 'Derived from leaked Anthropic Claude Code source (March 2026). ' +
50
- 'Anthropic has filed DMCA takedown notices. Shipping this package ' +
51
- 'may expose your project to IP infringement liability.',
52
- references: [
53
- 'https://cybernews.com/security/anthropic-claude-code-source-leak/',
54
- 'https://venturebeat.com/technology/claude-codes-source-code-appears-to-have-leaked-heres-what-we-know',
55
- ],
56
- },
57
49
  {
58
50
  name: 'claw-code-js',
59
51
  versions: '*',
@@ -81,6 +73,40 @@ export const LEGALLY_RISKY_PACKAGES = [
81
73
  'https://cybernews.com/security/anthropic-claude-code-source-leak/',
82
74
  ],
83
75
  },
76
+ // ---------------------------------------------------------------------------
77
+ // openclaude (github.com/Gitlawb/openclaude)
78
+ // Claude Code fork that routes to any LLM via OpenAI-compatible shim.
79
+ // Derived directly from the leaked Anthropic source (March 31 2026).
80
+ // Under active DMCA enforcement alongside claw-code.
81
+ // ---------------------------------------------------------------------------
82
+ {
83
+ name: 'openclaude',
84
+ versions: '*',
85
+ ecosystem: 'npm',
86
+ risk: 'leaked-source',
87
+ severity: 'high',
88
+ detail:
89
+ 'openclaude is a fork of the leaked Anthropic Claude Code source (March 2026) ' +
90
+ 'that adds an OpenAI-compatible provider shim. The underlying ~512,000 lines of ' +
91
+ 'TypeScript are Anthropic proprietary IP under active DMCA enforcement. ' +
92
+ 'Additionally ships with auth disabled by default and binds to 0.0.0.0:18789.',
93
+ references: [
94
+ 'https://github.com/Gitlawb/openclaude',
95
+ 'https://cybernews.com/security/anthropic-claude-code-source-leak/',
96
+ ],
97
+ },
98
+ {
99
+ name: 'openclaude-core',
100
+ versions: '*',
101
+ ecosystem: 'npm',
102
+ risk: 'leaked-source',
103
+ severity: 'high',
104
+ detail:
105
+ 'Core runtime package for openclaude. Derived from leaked Anthropic Claude Code ' +
106
+ 'source (March 2026). Under active DMCA enforcement.',
107
+ references: ['https://github.com/Gitlawb/openclaude'],
108
+ },
109
+
84
110
  // ---------------------------------------------------------------------------
85
111
  // License violations — well-known cases
86
112
  // ---------------------------------------------------------------------------
@@ -42,6 +42,7 @@ import { vibeCheckCommand } from '../commands/vibe-check.js';
42
42
  import { benchmarkCommand } from '../commands/benchmark.js';
43
43
  import { openclawCommand } from '../commands/openclaw.js';
44
44
  import { scanSkillCommand } from '../commands/scan-skill.js';
45
+ import { scanMcpCommand } from '../commands/scan-mcp.js';
45
46
  import { abomCommand } from '../commands/abom.js';
46
47
  import { updateIntelCommand } from '../commands/update-intel.js';
47
48
  import { hooksCommand } from '../commands/hooks.js';
@@ -364,6 +365,15 @@ program
364
365
  .option('--json', 'Output results as JSON')
365
366
  .action(scanSkillCommand);
366
367
 
368
+ // -----------------------------------------------------------------------------
369
+ // SCAN-MCP COMMAND
370
+ // -----------------------------------------------------------------------------
371
+ program
372
+ .command('scan-mcp [target]')
373
+ .description('Analyze an MCP server\'s tool manifest for security issues before connecting')
374
+ .option('--json', 'Output results as JSON')
375
+ .action(scanMcpCommand);
376
+
367
377
  // -----------------------------------------------------------------------------
368
378
  // ABOM COMMAND
369
379
  // -----------------------------------------------------------------------------
@@ -439,6 +449,7 @@ if (process.argv.length === 2) {
439
449
  console.log(chalk.white(' npx ship-safe watch . ') + chalk.gray('# Continuous monitoring mode'));
440
450
  console.log(chalk.white(' npx ship-safe openclaw . ') + chalk.gray('# OpenClaw & agent config security scan'));
441
451
  console.log(chalk.white(' npx ship-safe scan-skill <u>') + chalk.gray('# Vet a skill before installing'));
452
+ console.log(chalk.white(' npx ship-safe scan-mcp <url> ') + chalk.gray('# Vet an MCP server before connecting'));
442
453
  console.log(chalk.white(' npx ship-safe abom . ') + chalk.gray('# Agent Bill of Materials (CycloneDX)'));
443
454
  console.log(chalk.white(' npx ship-safe sbom . ') + chalk.gray('# Generate CycloneDX SBOM (CRA-ready)'));
444
455
  console.log(chalk.white(' npx ship-safe legal . ') + chalk.gray('# Legal risk audit: DMCA, leaked source, IP disputes'));
@@ -0,0 +1,456 @@
1
+ /**
2
+ * Scan MCP Command
3
+ * ================
4
+ *
5
+ * Fetches and analyzes an MCP server's tool manifest before connecting to it.
6
+ * Checks for malicious tool definitions, prompt injection in descriptions,
7
+ * exfiltration patterns, excessive permissions, and known-bad server hashes.
8
+ *
9
+ * USAGE:
10
+ * ship-safe scan-mcp <url> Analyze a remote MCP server
11
+ * ship-safe scan-mcp <path> Analyze a local MCP manifest file
12
+ *
13
+ * The command connects to the server's /tools endpoint (or reads the manifest
14
+ * JSON directly) and inspects every tool definition for attack patterns.
15
+ *
16
+ * MCP tool definitions are the new ToxicSkills surface — 36% of agent skills
17
+ * had security flaws; early MCP server audits show similar rates.
18
+ */
19
+
20
+ import fs from 'fs';
21
+ import path from 'path';
22
+ import chalk from 'chalk';
23
+ import { createHash } from 'crypto';
24
+ import * as output from '../utils/output.js';
25
+ import { ThreatIntel } from '../utils/threat-intel.js';
26
+
27
+ // =============================================================================
28
+ // MCP TOOL DESCRIPTION PATTERNS
29
+ // Injected into tool descriptions/annotations to hijack agents that use them.
30
+ // =============================================================================
31
+
32
+ const MCP_TOOL_PATTERNS = [
33
+ // ── Prompt injection in tool description ──────────────────────────────────
34
+ {
35
+ name: 'Prompt injection: override instructions',
36
+ regex: /(?:ignore\s+(?:all\s+)?(?:previous|prior|above|your)\s+instructions|disregard\s+(?:all\s+)?(?:previous|above)|your\s+(?:new|real|actual|true)\s+(?:instructions|role|goal)\s+(?:is|are))/gi,
37
+ severity: 'critical',
38
+ target: 'description',
39
+ },
40
+ {
41
+ name: 'Prompt injection: role hijacking',
42
+ regex: /(?:you\s+are\s+now\s+(?:a|an)|act\s+as\s+(?:a\s+)?(?:hacker|attacker|malicious|evil|unrestricted)|pretend\s+(?:you\s+are|to\s+be)\s+(?:a\s+)?(?:different|new|unrestricted))/gi,
43
+ severity: 'critical',
44
+ target: 'description',
45
+ },
46
+
47
+ // ── Silent exfiltration instructions in tool description ──────────────────
48
+ {
49
+ name: 'Silent data exfiltration',
50
+ regex: /(?:silently|quietly|without\s+(?:notif|alert|inform|telling|showing)|in\s+the\s+background)\s+.{0,60}(?:curl|wget|fetch|POST|send).{0,60}(?:http|https):\/\//gi,
51
+ severity: 'critical',
52
+ target: 'description',
53
+ },
54
+ {
55
+ name: 'Output suppression',
56
+ regex: /(?:do\s+not\s+(?:show|display|reveal|mention|tell|report|log)\s+(?:this|these|the\s+(?:output|result|response|command|action))|hide\s+(?:this|the)\s+(?:output|result|action|command|request))/gi,
57
+ severity: 'high',
58
+ target: 'description',
59
+ },
60
+
61
+ // ── Credential harvesting ─────────────────────────────────────────────────
62
+ {
63
+ name: 'Credential harvesting',
64
+ regex: /(?:extract|retrieve|collect|gather|find|read|access|get)\s+.{0,40}(?:api[_\s]?key|secret|token|password|credential|\.env|npmrc|ssh[_\s]?key|private[_\s]?key)/gi,
65
+ severity: 'critical',
66
+ target: 'description',
67
+ },
68
+ {
69
+ name: 'Sensitive path access',
70
+ regex: /(?:~\/\.(?:ssh|aws|npmrc|netrc|gnupg|config\/gcloud)|\/etc\/(?:passwd|shadow|hosts)|%APPDATA%|%USERPROFILE%)/gi,
71
+ severity: 'critical',
72
+ target: 'description',
73
+ },
74
+
75
+ // ── Known data exfiltration service domains ───────────────────────────────
76
+ {
77
+ name: 'Exfiltration service domain',
78
+ regex: /(?:webhook\.site|requestbin\.com|hookbin\.com|pipedream\.net|ngrok\.io|ngrok\.app|burpcollaborator\.net|interact\.sh|oastify\.com|canarytokens\.com)/gi,
79
+ severity: 'critical',
80
+ target: 'any',
81
+ },
82
+
83
+ // ── Dangerous tool input schema patterns ──────────────────────────────────
84
+ {
85
+ name: 'Shell command input parameter',
86
+ regex: /(?:"command"\s*:\s*\{[^}]*"type"\s*:\s*"string"|"cmd"\s*:\s*\{[^}]*"type"\s*:\s*"string"|"shell"\s*:\s*\{[^}]*"type"\s*:\s*"string")/gi,
87
+ severity: 'medium',
88
+ target: 'schema',
89
+ },
90
+ {
91
+ name: 'Arbitrary code execution parameter',
92
+ regex: /(?:"code"\s*:\s*\{[^}]*"type"\s*:\s*"string"|"script"\s*:\s*"(?:string|object)"|"eval"\s*:\s*\{[^}]*"type"\s*:\s*"string")/gi,
93
+ severity: 'high',
94
+ target: 'schema',
95
+ },
96
+
97
+ // ── Permission escalation in description ──────────────────────────────────
98
+ {
99
+ name: 'Permission escalation',
100
+ regex: /(?:grant\s+(?:me|this\s+(?:tool|server|skill)|yourself)\s+(?:admin|root|sudo|full|all)\s+(?:access|permissions?|rights?)|elevate\s+(?:privileges?|permissions?|rights?)|run\s+as\s+(?:admin|root|sudo))/gi,
101
+ severity: 'high',
102
+ target: 'description',
103
+ },
104
+
105
+ // ── Encoded payload in description ────────────────────────────────────────
106
+ {
107
+ name: 'Encoded payload block',
108
+ regex: /[A-Za-z0-9+\/]{60,}={0,2}/g,
109
+ severity: 'medium',
110
+ target: 'any',
111
+ },
112
+ ];
113
+
114
+ // Dangerous tool name keywords — flag tools whose names suggest shell/exec access
115
+ const DANGEROUS_TOOL_NAMES = [
116
+ /^(?:exec|execute|shell|bash|sh|cmd|terminal|run_command|system|subprocess)$/i,
117
+ /(?:_exec|_shell|_bash|_cmd|_terminal|exec_|shell_|bash_cmd)/i,
118
+ ];
119
+
120
+ // =============================================================================
121
+ // MAIN COMMAND
122
+ // =============================================================================
123
+
124
+ export async function scanMcpCommand(target, options = {}) {
125
+ if (!target) {
126
+ output.error('Usage: ship-safe scan-mcp <url|path>');
127
+ output.info(' Analyze an MCP server\'s tool manifest for security issues before connecting.');
128
+ process.exit(1);
129
+ }
130
+
131
+ console.log();
132
+ output.header('Ship Safe — MCP Server Security Analysis');
133
+ console.log();
134
+
135
+ let manifest, serverName, source;
136
+
137
+ if (target.startsWith('http://') || target.startsWith('https://')) {
138
+ console.log(chalk.gray(` Fetching MCP manifest from: ${target}`));
139
+ try {
140
+ manifest = await fetchMcpManifest(target);
141
+ serverName = new URL(target).hostname;
142
+ source = target;
143
+ } catch (err) {
144
+ output.error(`Failed to fetch MCP manifest: ${err.message}`);
145
+ process.exit(1);
146
+ }
147
+ } else {
148
+ const filePath = path.resolve(target);
149
+ if (!fs.existsSync(filePath)) {
150
+ output.error(`File not found: ${filePath}`);
151
+ process.exit(1);
152
+ }
153
+ try {
154
+ manifest = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
155
+ serverName = path.basename(filePath);
156
+ source = filePath;
157
+ } catch (err) {
158
+ output.error(`Failed to parse manifest: ${err.message}`);
159
+ process.exit(1);
160
+ }
161
+ }
162
+
163
+ const tools = extractTools(manifest);
164
+ console.log(chalk.gray(` Server: ${serverName}`));
165
+ console.log(chalk.gray(` Tools found: ${tools.length}`));
166
+ console.log();
167
+
168
+ if (tools.length === 0) {
169
+ output.warning('No tools found in manifest. Is this a valid MCP tools response?');
170
+ return;
171
+ }
172
+
173
+ const findings = analyzeManifest(manifest, tools, serverName, source);
174
+
175
+ if (options.json) {
176
+ console.log(JSON.stringify({ server: serverName, source, toolCount: tools.length, findings, summary: getSummary(findings) }, null, 2));
177
+ return;
178
+ }
179
+
180
+ printFindings(findings, serverName, tools.length);
181
+
182
+ if (getSummary(findings).critical > 0) {
183
+ process.exit(1);
184
+ }
185
+ }
186
+
187
+ // =============================================================================
188
+ // MCP MANIFEST FETCHING
189
+ // =============================================================================
190
+
191
+ async function fetchMcpManifest(baseUrl) {
192
+ const url = baseUrl.replace(/\/$/, '');
193
+
194
+ // Try MCP tools/list endpoint first (JSON-RPC 2.0)
195
+ const jsonRpcBody = JSON.stringify({
196
+ jsonrpc: '2.0',
197
+ id: 1,
198
+ method: 'tools/list',
199
+ params: {},
200
+ });
201
+
202
+ const jsonRpcRes = await fetch(`${url}`, {
203
+ method: 'POST',
204
+ headers: { 'Content-Type': 'application/json' },
205
+ body: jsonRpcBody,
206
+ signal: AbortSignal.timeout(10000),
207
+ }).catch(() => null);
208
+
209
+ if (jsonRpcRes?.ok) {
210
+ const data = await jsonRpcRes.json();
211
+ if (data?.result?.tools) return data.result;
212
+ }
213
+
214
+ // Fall back to GET /tools (some servers expose this)
215
+ const getRes = await fetch(`${url}/tools`, {
216
+ signal: AbortSignal.timeout(10000),
217
+ }).catch(() => null);
218
+
219
+ if (getRes?.ok) {
220
+ return await getRes.json();
221
+ }
222
+
223
+ // Fall back to root endpoint
224
+ const rootRes = await fetch(`${url}`, {
225
+ signal: AbortSignal.timeout(10000),
226
+ }).catch(() => null);
227
+
228
+ if (rootRes?.ok) {
229
+ const text = await rootRes.text();
230
+ try {
231
+ return JSON.parse(text);
232
+ } catch {
233
+ throw new Error('Server responded but returned non-JSON content');
234
+ }
235
+ }
236
+
237
+ throw new Error('Could not retrieve MCP manifest (tried tools/list JSON-RPC, GET /tools, GET /)');
238
+ }
239
+
240
+ // =============================================================================
241
+ // TOOL EXTRACTION — handles multiple MCP manifest formats
242
+ // =============================================================================
243
+
244
+ function extractTools(manifest) {
245
+ // MCP tools/list result: { tools: [...] }
246
+ if (Array.isArray(manifest?.tools)) return manifest.tools;
247
+ // Direct array
248
+ if (Array.isArray(manifest)) return manifest;
249
+ // { result: { tools: [...] } }
250
+ if (Array.isArray(manifest?.result?.tools)) return manifest.result.tools;
251
+ return [];
252
+ }
253
+
254
+ // =============================================================================
255
+ // ANALYSIS
256
+ // =============================================================================
257
+
258
+ function analyzeManifest(manifest, tools, serverName, source) {
259
+ const findings = [];
260
+ const rawJson = JSON.stringify(manifest);
261
+
262
+ // 1. Threat intel hash check on full manifest
263
+ const hash = createHash('sha256').update(rawJson).digest('hex');
264
+ const intelMatch = ThreatIntel.lookupHash(hash);
265
+ if (intelMatch) {
266
+ findings.push({
267
+ check: 'threat-intel',
268
+ name: `Known malicious MCP server: ${intelMatch.name}`,
269
+ severity: 'critical',
270
+ tool: null,
271
+ matched: `SHA-256: ${hash} — ${intelMatch.description}`,
272
+ });
273
+ }
274
+
275
+ // 2. Threat intel signature check on raw manifest
276
+ const sigMatches = ThreatIntel.matchSignatures(rawJson);
277
+ for (const sig of sigMatches) {
278
+ findings.push({
279
+ check: 'threat-intel',
280
+ name: `Threat intel match: ${sig.description}`,
281
+ severity: sig.severity || 'critical',
282
+ tool: null,
283
+ matched: `Pattern: ${sig.pattern}`,
284
+ });
285
+ }
286
+
287
+ // 3. Per-tool analysis
288
+ for (const tool of tools) {
289
+ findings.push(...analyzeToolDefinition(tool));
290
+ }
291
+
292
+ // 4. Server-level checks
293
+ findings.push(...checkServerLevel(manifest, serverName));
294
+
295
+ return findings;
296
+ }
297
+
298
+ function analyzeToolDefinition(tool) {
299
+ const findings = [];
300
+ const name = tool.name || '(unnamed)';
301
+ const description = tool.description || '';
302
+ const schemaStr = JSON.stringify(tool.inputSchema || tool.input_schema || {});
303
+
304
+ // Check description against patterns
305
+ for (const pattern of MCP_TOOL_PATTERNS) {
306
+ if (pattern.target === 'schema') continue; // schema checked separately below
307
+ pattern.regex.lastIndex = 0;
308
+ const text = pattern.target === 'description' ? description
309
+ : pattern.target === 'any' ? description + ' ' + schemaStr
310
+ : description;
311
+ if (pattern.regex.test(text)) {
312
+ findings.push({
313
+ check: 'static-analysis',
314
+ name: pattern.name,
315
+ severity: pattern.severity,
316
+ tool: name,
317
+ matched: (description + schemaStr).slice(0, 120),
318
+ });
319
+ }
320
+ }
321
+
322
+ // Check schema for dangerous input patterns
323
+ for (const pattern of MCP_TOOL_PATTERNS) {
324
+ if (pattern.target !== 'schema') continue;
325
+ pattern.regex.lastIndex = 0;
326
+ if (pattern.regex.test(schemaStr)) {
327
+ findings.push({
328
+ check: 'schema-analysis',
329
+ name: pattern.name,
330
+ severity: pattern.severity,
331
+ tool: name,
332
+ matched: schemaStr.slice(0, 120),
333
+ });
334
+ }
335
+ }
336
+
337
+ // Check tool name against dangerous name list
338
+ for (const namePattern of DANGEROUS_TOOL_NAMES) {
339
+ if (namePattern.test(name)) {
340
+ findings.push({
341
+ check: 'tool-name',
342
+ name: `Dangerous tool name: "${name}"`,
343
+ severity: 'high',
344
+ tool: name,
345
+ matched: `Tool name matches high-risk pattern: ${namePattern}`,
346
+ });
347
+ break;
348
+ }
349
+ }
350
+
351
+ // Check for excessive required parameters (information harvesting)
352
+ const required = tool.inputSchema?.required || tool.input_schema?.required || [];
353
+ const properties = tool.inputSchema?.properties || tool.input_schema?.properties || {};
354
+ const propNames = Object.keys(properties);
355
+ const sensitiveParams = propNames.filter(p =>
356
+ /(?:api[_\s]?key|token|password|secret|credential|auth|private)/i.test(p)
357
+ );
358
+ if (sensitiveParams.length > 0) {
359
+ findings.push({
360
+ check: 'schema-analysis',
361
+ name: `Tool requires sensitive parameters: ${sensitiveParams.join(', ')}`,
362
+ severity: 'high',
363
+ tool: name,
364
+ matched: `Required sensitive params: [${sensitiveParams.join(', ')}]`,
365
+ });
366
+ }
367
+
368
+ return findings;
369
+ }
370
+
371
+ function checkServerLevel(manifest, serverName) {
372
+ const findings = [];
373
+ const raw = JSON.stringify(manifest);
374
+
375
+ // Check for excessively large manifest (may hide payloads)
376
+ if (raw.length > 500_000) {
377
+ findings.push({
378
+ check: 'server-level',
379
+ name: 'Unusually large manifest',
380
+ severity: 'medium',
381
+ tool: null,
382
+ matched: `Manifest size: ${(raw.length / 1024).toFixed(1)} KB (>500 KB is suspicious)`,
383
+ });
384
+ }
385
+
386
+ // Check for tools with no description (reduces reviewability)
387
+ const tools = extractTools(manifest);
388
+ const noDesc = tools.filter(t => !t.description || t.description.trim().length < 10);
389
+ if (noDesc.length > 0 && noDesc.length === tools.length) {
390
+ findings.push({
391
+ check: 'server-level',
392
+ name: 'All tools lack descriptions',
393
+ severity: 'medium',
394
+ tool: null,
395
+ matched: `${noDesc.length}/${tools.length} tools have no meaningful description — cannot assess intent`,
396
+ });
397
+ }
398
+
399
+ return findings;
400
+ }
401
+
402
+ // =============================================================================
403
+ // OUTPUT
404
+ // =============================================================================
405
+
406
+ function printFindings(findings, serverName, toolCount) {
407
+ const summary = getSummary(findings);
408
+
409
+ if (findings.length === 0) {
410
+ console.log(chalk.green.bold(` ✔ ${serverName}: No security issues found across ${toolCount} tool(s).`));
411
+ console.log();
412
+ return;
413
+ }
414
+
415
+ console.log(chalk.red.bold(` ✘ ${serverName}: ${findings.length} issue(s) found across ${toolCount} tool(s)`));
416
+ console.log();
417
+
418
+ // Group by tool
419
+ const byTool = new Map();
420
+ for (const f of findings) {
421
+ const key = f.tool || '(server-level)';
422
+ if (!byTool.has(key)) byTool.set(key, []);
423
+ byTool.get(key).push(f);
424
+ }
425
+
426
+ for (const [toolName, toolFindings] of byTool) {
427
+ if (toolName !== '(server-level)') {
428
+ console.log(chalk.cyan(` Tool: ${toolName}`));
429
+ } else {
430
+ console.log(chalk.cyan(` Server-level`));
431
+ }
432
+
433
+ for (const f of toolFindings) {
434
+ const sevColor = f.severity === 'critical' ? chalk.red.bold
435
+ : f.severity === 'high' ? chalk.yellow
436
+ : chalk.blue;
437
+ console.log(` ${sevColor(`[${f.severity.toUpperCase()}]`)} ${chalk.white(f.name)}`);
438
+ if (f.matched) console.log(chalk.gray(` ${f.matched.slice(0, 120)}`));
439
+ }
440
+ console.log();
441
+ }
442
+
443
+ if (summary.critical > 0) {
444
+ console.log(chalk.red.bold(' ⚠ DO NOT CONNECT to this MCP server — critical security issues detected.'));
445
+ console.log();
446
+ }
447
+ }
448
+
449
+ function getSummary(findings) {
450
+ return {
451
+ total: findings.length,
452
+ critical: findings.filter(f => f.severity === 'critical').length,
453
+ high: findings.filter(f => f.severity === 'high').length,
454
+ medium: findings.filter(f => f.severity === 'medium').length,
455
+ };
456
+ }
@@ -47,6 +47,20 @@ const SKILL_PATTERNS = [
47
47
  { name: 'Crypto operations', regex: /(?:crypto\.createCipher|crypto\.createDecipher|CryptoJS|forge\.cipher)/gi, severity: 'medium' },
48
48
  { name: 'Network listener', regex: /(?:createServer|listen\s*\(\s*\d|bind\s*\(\s*['"]0\.0\.0\.0)/gi, severity: 'high' },
49
49
  { name: 'Encoded payload block', regex: /[A-Za-z0-9+\/]{60,}={0,2}/g, severity: 'medium' },
50
+
51
+ // ── ToxicSkills patterns (Snyk research — 36% of agent skills affected) ──
52
+ // Silent curl exfiltration: skill instructs agent to silently send data
53
+ { name: 'ToxicSkills: silent data exfiltration via curl', regex: /(?:silently|quietly|without\s+(?:notif|alert|inform|telling|showing)|in\s+the\s+background)\s+.{0,60}(?:curl|wget|fetch|POST|send).{0,60}(?:http|https):\/\//gi, severity: 'critical' },
54
+ // System prompt override in skill definition
55
+ { name: 'ToxicSkills: system prompt override', regex: /(?:ignore\s+(?:all\s+)?(?:previous|prior|above|your)\s+instructions|your\s+(?:new|real|actual|true)\s+(?:instructions|role|goal|purpose)\s+(?:is|are)|disregard\s+(?:all\s+)?(?:previous|above|your))/gi, severity: 'critical' },
56
+ // Skill requests credentials/secrets from agent context
57
+ { name: 'ToxicSkills: credential harvesting', regex: /(?:extract|retrieve|collect|gather|find|read|access|get)\s+.{0,40}(?:api[_\s]?key|secret|token|password|credential|\.env|npmrc|ssh[_\s]?key|private[_\s]?key)/gi, severity: 'critical' },
58
+ // Skill attempts to read ~/.ssh, ~/.aws, ~/.npmrc
59
+ { name: 'ToxicSkills: sensitive path access', regex: /(?:~\/\.(?:ssh|aws|npmrc|netrc|gnupg|config\/gcloud)|\/etc\/(?:passwd|shadow|hosts)|%APPDATA%|%USERPROFILE%)/gi, severity: 'critical' },
60
+ // Skill suppresses its own output to avoid detection
61
+ { name: 'ToxicSkills: output suppression', regex: /(?:do\s+not\s+(?:show|display|reveal|mention|tell|report|log)\s+(?:this|these|the\s+(?:output|result|response|command|action))|hide\s+(?:this|the)\s+(?:output|result|action|command|request))/gi, severity: 'high' },
62
+ // Skill requests permissions beyond its stated purpose
63
+ { name: 'ToxicSkills: permission escalation', regex: /(?:grant\s+(?:me|this\s+skill|yourself)\s+(?:admin|root|sudo|full|all)\s+(?:access|permissions?|rights?)|elevate\s+(?:privileges?|permissions?|rights?)|run\s+as\s+(?:admin|root|sudo))/gi, severity: 'high' },
50
64
  ];
51
65
 
52
66
  // =============================================================================
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ship-safe",
3
- "version": "6.3.0",
3
+ "version": "6.4.0",
4
4
  "description": "AI-powered multi-agent security platform. 18 agents scan 80+ attack classes with LLM-powered deep analysis. Red team your code before attackers do.",
5
5
  "main": "cli/index.js",
6
6
  "bin": {