ship-safe 7.0.0 → 9.0.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.
@@ -0,0 +1,333 @@
1
+ /**
2
+ * ManagedAgentScanner
3
+ * ====================
4
+ *
5
+ * Detect security misconfigurations in Claude Managed Agents definitions.
6
+ *
7
+ * Claude Managed Agents (beta, April 2026) introduces a hosted agent
8
+ * infrastructure with Agents, Environments, Sessions, and Vaults.
9
+ * Misconfigurations in these definitions — unrestricted networking,
10
+ * always_allow permission policies, all tools enabled by default —
11
+ * map directly to OWASP Agentic AI Top 10 risks (ASI-03, ASI-04, ASI-05).
12
+ *
13
+ * Scans: Python, TypeScript, JavaScript, JSON, YAML, shell scripts, and
14
+ * any file containing Managed Agents API calls or SDK usage.
15
+ *
16
+ * Maps to: OWASP Agentic AI ASI-03 (Excessive Agency),
17
+ * ASI-04 (Inadequate Sandboxing), ASI-05 (Improper Tool Use),
18
+ * ASI-07 (Lack of Human Oversight)
19
+ */
20
+
21
+ import path from 'path';
22
+ import { BaseAgent, createFinding } from './base-agent.js';
23
+
24
+ // =============================================================================
25
+ // SINGLE-LINE REGEX PATTERNS
26
+ // =============================================================================
27
+
28
+ const PATTERNS = [
29
+ // ── ASI-03: Excessive Agency — Permission Policies ─────────────────────────
30
+ {
31
+ rule: 'MANAGED_AGENT_ALWAYS_ALLOW',
32
+ title: 'Managed Agent: All Tools Set to always_allow',
33
+ regex: /permission_policy['":\s]*\{?\s*['"]?type['"]?\s*[:=]\s*['"]always_allow['"]/g,
34
+ severity: 'critical',
35
+ cwe: 'CWE-269',
36
+ owasp: 'ASI03',
37
+ description: 'Claude Managed Agent permission policy is set to always_allow. All tools — including bash and file write — execute without human confirmation. Any prompt injection in the session can run arbitrary commands ungateed.',
38
+ fix: 'Set permission_policy to {type: "always_ask"} for the agent toolset, or at minimum require confirmation for bash and write tools using per-tool configs.',
39
+ },
40
+
41
+ // ── ASI-04: Inadequate Sandboxing — Networking ─────────────────────────────
42
+ {
43
+ rule: 'MANAGED_AGENT_UNRESTRICTED_NET',
44
+ title: 'Managed Agent: Unrestricted Network Access',
45
+ regex: /networking['":\s]*\{?\s*['"]?type['"]?\s*[:=]\s*['"]unrestricted['"]/g,
46
+ severity: 'high',
47
+ cwe: 'CWE-284',
48
+ owasp: 'ASI04',
49
+ description: 'Managed Agent environment has unrestricted outbound networking. An agent with bash and web_fetch tools can exfiltrate code, secrets, or PII to any external endpoint.',
50
+ fix: 'Use networking: {type: "limited", allowed_hosts: ["api.example.com"]} with an explicit allowlist. Only grant allow_mcp_servers and allow_package_managers if needed.',
51
+ },
52
+
53
+ // ── ASI-05: Improper Tool Use — Full Toolset Default ───────────────────────
54
+ {
55
+ rule: 'MANAGED_AGENT_ALL_TOOLS_DEFAULT',
56
+ title: 'Managed Agent: Full Toolset Enabled With No Restrictions',
57
+ regex: /['"]?type['"]?\s*[:=]\s*['"]agent_toolset_20260401['"]/g,
58
+ severity: 'high',
59
+ cwe: 'CWE-250',
60
+ owasp: 'ASI05',
61
+ confidence: 'medium',
62
+ description: 'agent_toolset_20260401 enables all 8 built-in tools (bash, read, write, edit, glob, grep, web_fetch, web_search) by default. Without a configs array or default_config, the agent has maximum tool access.',
63
+ fix: 'Use default_config: {enabled: false} and explicitly enable only the tools your agent needs. Disable web_fetch and web_search if the agent does not need internet access.',
64
+ },
65
+
66
+ // ── ASI-05: MCP Toolset Permission Override ────────────────────────────────
67
+ {
68
+ rule: 'MANAGED_AGENT_MCP_ALWAYS_ALLOW',
69
+ title: 'Managed Agent: MCP Toolset Set to always_allow',
70
+ regex: /mcp_toolset['",\s]*[\s\S]{0,200}permission_policy['":\s]*\{?\s*['"]?type['"]?\s*[:=]\s*['"]always_allow['"]/g,
71
+ severity: 'high',
72
+ cwe: 'CWE-269',
73
+ owasp: 'ASI05',
74
+ description: 'MCP toolset permission policy overridden from the safe default (always_ask) to always_allow. Third-party MCP server tools will execute without human confirmation — if the MCP server adds new tools, they auto-execute too.',
75
+ fix: 'Keep MCP toolset at the default always_ask policy, or audit the MCP server tools before setting always_allow.',
76
+ },
77
+
78
+ // ── ASI-04: MCP Server Over HTTP ───────────────────────────────────────────
79
+ {
80
+ rule: 'MANAGED_AGENT_MCP_HTTP',
81
+ title: 'Managed Agent: MCP Server URL Uses Plain HTTP',
82
+ regex: /mcp_server(?:_url|s)?['":\s]*[\s\S]{0,100}['"]http:\/\/(?!localhost|127\.0\.0\.1|::1)/g,
83
+ severity: 'critical',
84
+ cwe: 'CWE-319',
85
+ owasp: 'ASI04',
86
+ description: 'MCP server URL uses unencrypted HTTP for a non-localhost endpoint. All tool calls, results, and credentials are transmitted in cleartext.',
87
+ fix: 'Use https:// for all MCP server URLs. Only http://localhost is acceptable for local development.',
88
+ },
89
+
90
+ // ── ASI-03: Callable Agents (Multi-Agent Escalation) ───────────────────────
91
+ {
92
+ rule: 'MANAGED_AGENT_CALLABLE_AGENTS',
93
+ title: 'Managed Agent: Multi-Agent Orchestration Enabled',
94
+ regex: /callable_agents\s*[:=]/g,
95
+ severity: 'medium',
96
+ cwe: 'CWE-269',
97
+ owasp: 'ASI03',
98
+ confidence: 'medium',
99
+ description: 'Agent has callable_agents configured, enabling multi-agent orchestration. A compromised child agent can escalate privileges through the parent if tool access is not scoped per-agent.',
100
+ fix: 'Apply least-privilege tool access to each callable agent independently. Do not grant child agents broader tool access than their parent.',
101
+ },
102
+
103
+ // ── ASI-07: No System Prompt ───────────────────────────────────────────────
104
+ {
105
+ rule: 'MANAGED_AGENT_NO_SYSTEM_PROMPT',
106
+ title: 'Managed Agent: No System Prompt Defined',
107
+ regex: /(?:agents\.create|\/v1\/agents)\s*\([^)]*\)/g,
108
+ severity: 'low',
109
+ cwe: 'CWE-1188',
110
+ owasp: 'ASI07',
111
+ confidence: 'low',
112
+ description: 'Agent created without a system prompt. Without behavioral constraints, the agent is more susceptible to prompt injection and goal hijacking.',
113
+ fix: 'Add a system prompt that defines the agent\'s role, boundaries, and what it must refuse to do.',
114
+ },
115
+
116
+ // ── Credential Exposure — Hardcoded Tokens ─────────────────────────────────
117
+ {
118
+ rule: 'MANAGED_AGENT_HARDCODED_TOKEN',
119
+ title: 'Managed Agent: Hardcoded Credential in Config',
120
+ regex: /(?:access_token|refresh_token|client_secret)\s*[:=]\s*['"][a-zA-Z0-9_\-/.]{20,}['"]/g,
121
+ severity: 'critical',
122
+ cwe: 'CWE-798',
123
+ owasp: 'ASI04',
124
+ description: 'Vault credential (access_token, refresh_token, or client_secret) appears hardcoded in source code. These should be injected from environment variables or a secrets manager, not committed to the repository.',
125
+ fix: 'Move tokens to environment variables or a secrets manager. Use vault_ids at session creation to inject credentials at runtime.',
126
+ },
127
+
128
+ // ── ASI-04: Static Bearer Token in Source ──────────────────────────────────
129
+ {
130
+ rule: 'MANAGED_AGENT_STATIC_BEARER_INLINE',
131
+ title: 'Managed Agent: Static Bearer Token in Source',
132
+ regex: /['"]?type['"]?\s*[:=]\s*['"]static_bearer['"][\s\S]{0,150}['"]?token['"]?\s*[:=]\s*['"][a-zA-Z0-9_\-/.]{20,}['"]/g,
133
+ severity: 'critical',
134
+ cwe: 'CWE-798',
135
+ owasp: 'ASI04',
136
+ description: 'A static_bearer credential with an inline token is defined in source code. This token is visible to anyone with repository access.',
137
+ fix: 'Store the token in a secrets manager or environment variable. Reference it at runtime: token: process.env.LINEAR_API_KEY.',
138
+ },
139
+ ];
140
+
141
+ // =============================================================================
142
+ // MULTI-LINE / STRUCTURAL PATTERNS (checked via content analysis)
143
+ // =============================================================================
144
+
145
+ /**
146
+ * Check for environment configs missing network restrictions entirely.
147
+ * Pattern: environment creation with no networking field → defaults to unrestricted.
148
+ */
149
+ function checkMissingNetworkConfig(content, filePath, agent) {
150
+ const findings = [];
151
+ // Match environment creation calls that have a config block but no networking field
152
+ const envCreateRe = /(?:environments\.create|\/v1\/environments)\s*\(/g;
153
+ let match;
154
+ while ((match = envCreateRe.exec(content)) !== null) {
155
+ // Look ahead up to 500 chars for a config block
156
+ const snippet = content.slice(match.index, match.index + 500);
157
+ if (snippet.includes('config') && !snippet.includes('networking')) {
158
+ const line = content.slice(0, match.index).split('\n').length;
159
+ findings.push(createFinding({
160
+ file: filePath,
161
+ line,
162
+ severity: 'medium',
163
+ category: agent.category,
164
+ rule: 'MANAGED_AGENT_NO_NETWORK_LIMIT',
165
+ title: 'Managed Agent: Environment Created Without Network Config',
166
+ description: 'Environment created without a networking field. Defaults to unrestricted outbound access. For production, explicitly set networking: {type: "limited"} with an allowed_hosts list.',
167
+ matched: snippet.slice(0, 120),
168
+ confidence: 'medium',
169
+ cwe: 'CWE-284',
170
+ owasp: 'ASI04',
171
+ fix: 'Add networking: {type: "limited", allowed_hosts: ["your-api.example.com"]} to the environment config.',
172
+ }));
173
+ }
174
+ }
175
+ return findings;
176
+ }
177
+
178
+ /**
179
+ * Check for bash + web tools enabled with always_allow — exfiltration combo.
180
+ */
181
+ function checkExfilCombo(content, filePath, agent) {
182
+ const findings = [];
183
+ // Only flag if we see the toolset AND always_allow AND no bash restriction
184
+ const hasToolset = /agent_toolset_20260401/.test(content);
185
+ const hasAlwaysAllow = /always_allow/.test(content);
186
+ const hasBashRestriction = /['"]bash['"][\s\S]{0,100}always_ask/.test(content);
187
+ const disablesBash = /['"]bash['"][\s\S]{0,50}enabled['"]?\s*[:=]\s*false/.test(content);
188
+
189
+ if (hasToolset && hasAlwaysAllow && !hasBashRestriction && !disablesBash) {
190
+ // Find the line of always_allow for positioning
191
+ const idx = content.indexOf('always_allow');
192
+ const line = idx >= 0 ? content.slice(0, idx).split('\n').length : 1;
193
+ findings.push(createFinding({
194
+ file: filePath,
195
+ line,
196
+ severity: 'critical',
197
+ category: agent.category,
198
+ rule: 'MANAGED_AGENT_BASH_NO_CONFIRM',
199
+ title: 'Managed Agent: Bash Executes Without Human Confirmation',
200
+ description: 'Agent toolset uses always_allow and bash is not restricted to always_ask. Any prompt injection can execute shell commands without confirmation. This is equivalent to --dangerously-skip-permissions in Claude Code.',
201
+ matched: 'permission_policy: always_allow (bash unrestricted)',
202
+ confidence: 'high',
203
+ cwe: 'CWE-78',
204
+ owasp: 'ASI03',
205
+ fix: 'Add a per-tool override: configs: [{name: "bash", permission_policy: {type: "always_ask"}}].',
206
+ }));
207
+ }
208
+ return findings;
209
+ }
210
+
211
+ /**
212
+ * Check for unpinned packages in environment config.
213
+ */
214
+ function checkUnpinnedPackages(content, filePath, agent) {
215
+ const findings = [];
216
+ // Look for packages blocks with items that lack version pins
217
+ const packagesRe = /packages['":\s]*\{[\s\S]{0,500}\}/g;
218
+ let match;
219
+ while ((match = packagesRe.exec(content)) !== null) {
220
+ const block = match[0];
221
+ // Check for pip packages without ==, npm without @, etc.
222
+ const unpinnedPip = /pip['":\s]*\[([^\]]+)\]/.exec(block);
223
+ const unpinnedNpm = /npm['":\s]*\[([^\]]+)\]/.exec(block);
224
+
225
+ const checkList = (listMatch, manager) => {
226
+ if (!listMatch) return;
227
+ const items = listMatch[1].match(/['"][^'"]+['"]/g) || [];
228
+ const unpinned = items.filter(item => {
229
+ const clean = item.replace(/['"]/g, '');
230
+ if (manager === 'pip') return !clean.includes('==') && !clean.includes('>=');
231
+ return !clean.includes('@');
232
+ });
233
+ if (unpinned.length > 0) {
234
+ const line = content.slice(0, match.index).split('\n').length;
235
+ findings.push(createFinding({
236
+ file: filePath,
237
+ line,
238
+ severity: 'medium',
239
+ category: agent.category,
240
+ rule: 'MANAGED_AGENT_UNPINNED_PACKAGE',
241
+ title: `Managed Agent: Unpinned ${manager} Packages in Environment`,
242
+ description: `Environment installs ${manager} packages without version pins: ${unpinned.join(', ')}. Unpinned packages can be hijacked via supply chain attacks.`,
243
+ matched: unpinned.join(', '),
244
+ confidence: 'medium',
245
+ cwe: 'CWE-829',
246
+ owasp: 'ASI04',
247
+ fix: `Pin package versions: ${manager === 'pip' ? '"pandas==2.2.0"' : '"express@4.18.0"'}.`,
248
+ }));
249
+ }
250
+ };
251
+
252
+ checkList(unpinnedPip, 'pip');
253
+ checkList(unpinnedNpm, 'npm');
254
+ }
255
+ return findings;
256
+ }
257
+
258
+ // =============================================================================
259
+ // AGENT CLASS
260
+ // =============================================================================
261
+
262
+ export class ManagedAgentScanner extends BaseAgent {
263
+ constructor() {
264
+ super(
265
+ 'ManagedAgentScanner',
266
+ 'Detect security misconfigurations in Claude Managed Agents (environments, tools, permissions, networking)',
267
+ 'agentic',
268
+ );
269
+ }
270
+
271
+ /**
272
+ * Only run if the codebase references Managed Agents API/SDK.
273
+ */
274
+ shouldRun(recon) {
275
+ return true; // Lightweight patterns — always run, regex will short-circuit on non-matching files
276
+ }
277
+
278
+ async analyze(context) {
279
+ const { rootPath, files } = context;
280
+
281
+ // Filter to files likely containing Managed Agent configs
282
+ const targetFiles = files.filter(f => {
283
+ const ext = path.extname(f).toLowerCase();
284
+ const basename = path.basename(f).toLowerCase();
285
+ return (
286
+ ['.js', '.ts', '.mjs', '.mts', '.py', '.json', '.yaml', '.yml', '.sh', '.bash', '.go', '.java', '.cs', '.php', '.rb'].includes(ext) ||
287
+ basename === 'dockerfile' ||
288
+ basename === 'docker-compose.yml' ||
289
+ basename === 'docker-compose.yaml'
290
+ );
291
+ });
292
+
293
+ if (targetFiles.length === 0) return [];
294
+
295
+ let findings = [];
296
+
297
+ for (const file of targetFiles) {
298
+ const content = this.readFile(file);
299
+ if (!content) continue;
300
+
301
+ // Quick relevance check — skip files with no Managed Agent signals
302
+ const hasSignal =
303
+ content.includes('agent_toolset_20260401') ||
304
+ content.includes('managed-agents') ||
305
+ content.includes('/v1/agents') ||
306
+ content.includes('/v1/environments') ||
307
+ content.includes('/v1/sessions') ||
308
+ content.includes('/v1/vaults') ||
309
+ content.includes('beta.agents') ||
310
+ content.includes('beta.environments') ||
311
+ content.includes('beta.sessions') ||
312
+ content.includes('beta.vaults') ||
313
+ content.includes('callable_agents') ||
314
+ content.includes('mcp_toolset') ||
315
+ content.includes('static_bearer') ||
316
+ content.includes('mcp_oauth');
317
+
318
+ if (!hasSignal) continue;
319
+
320
+ // Run single-line patterns
321
+ findings = findings.concat(this.scanFileWithPatterns(file, PATTERNS));
322
+
323
+ // Run structural checks
324
+ findings = findings.concat(checkMissingNetworkConfig(content, file, this));
325
+ findings = findings.concat(checkExfilCombo(content, file, this));
326
+ findings = findings.concat(checkUnpinnedPackages(content, file, this));
327
+ }
328
+
329
+ return findings;
330
+ }
331
+ }
332
+
333
+ export default ManagedAgentScanner;
@@ -234,9 +234,19 @@ export class Orchestrator {
234
234
  allFindings = await analyzer.analyze(allFindings, { rootPath: absolutePath, recon });
235
235
  const stats = analyzer.getStats();
236
236
  if (deepSpinner) {
237
- deepSpinner.succeed(chalk.green(
238
- `Deep analysis: ${stats.analyzedCount} findings analyzed (${stats.spentCents}c spent)`
239
- ));
237
+ if (stats.multiTier) {
238
+ const tierNote = stats.tier3Count > 0
239
+ ? `, ${stats.tier3Count} escalated to Opus`
240
+ : stats.tier2Count > 0 ? `, ${stats.tier2Count} via Sonnet` : '';
241
+ const skipNote = stats.skippedCount > 0 ? `, ${stats.skippedCount} triaged away` : '';
242
+ deepSpinner.succeed(chalk.green(
243
+ `Deep analysis (Haiku→Sonnet→Opus): ${stats.analyzedCount} analyzed${tierNote}${skipNote} (${stats.spentCents}¢)`
244
+ ));
245
+ } else {
246
+ deepSpinner.succeed(chalk.green(
247
+ `Deep analysis: ${stats.analyzedCount} findings analyzed (${stats.spentCents}¢)`
248
+ ));
249
+ }
240
250
  }
241
251
  } catch (err) {
242
252
  if (deepSpinner) deepSpinner.fail(chalk.yellow(`Deep analysis failed: ${err.message}`));
@@ -26,7 +26,7 @@ const COMPROMISED_PACKAGES = [
26
26
  {
27
27
  name: 'axios',
28
28
  badVersions: ['1.8.2'],
29
- note: 'TeamPCP/CanisterWorm campaign (Mar 31 2026). Malicious publish delivered a Remote Access Trojan with persistence.',
29
+ note: 'TeamPCP/CanisterWorm campaign (Mar 31 2026). Attributed to Sapphire Sleet (North Korean state actor) by Microsoft Threat Intelligence. Malicious publish connected to C2 domain delivering a second-stage RAT with persistence.',
30
30
  },
31
31
  {
32
32
  name: 'telnyx',
@@ -48,6 +48,11 @@ import { updateIntelCommand } from '../commands/update-intel.js';
48
48
  import { hooksCommand } from '../commands/hooks.js';
49
49
  import { legalCommand } from '../commands/legal.js';
50
50
  import { runLiveAdvisories } from '../commands/live-advisories.js';
51
+ import { envAuditCommand } from '../commands/env-audit.js';
52
+ import { autofixCommand } from '../commands/autofix.js';
53
+ import { memoryCommand } from '../utils/security-memory.js';
54
+ import { playbookCommand } from '../utils/scan-playbook.js';
55
+ import { listPluginFiles, scaffoldPlugin } from '../utils/plugin-loader.js';
51
56
  import { ABOMGenerator } from '../agents/abom-generator.js';
52
57
  import { PolicyEngine } from '../agents/policy-engine.js';
53
58
  import { SBOMGenerator } from '../agents/sbom-generator.js';
@@ -120,6 +125,8 @@ program
120
125
  .option('--headers', 'Only copy security headers config')
121
126
  .option('--agents', 'Only add security rules to AI agent instruction files (CLAUDE.md, .cursor/rules/, .windsurfrules, copilot-instructions.md)')
122
127
  .option('--openclaw', 'Generate a hardened openclaw.json template')
128
+ .option('--hermes', 'Bootstrap Hermes Agent security config (allowlist, integrity hashes, CI)')
129
+ .option('--from <url>', 'Fetch a pre-built Hermes config bundle from a setup URL (used with --hermes)')
123
130
  .action(initCommand);
124
131
 
125
132
  // -----------------------------------------------------------------------------
@@ -203,7 +210,7 @@ program
203
210
  // -----------------------------------------------------------------------------
204
211
  program
205
212
  .command('audit [path]')
206
- .description('Full security audit: secrets + 18 agents + deps + score + deep analysis + remediation plan')
213
+ .description('Full security audit: secrets + 22 agents + deps + score + deep analysis + remediation plan')
207
214
  .option('--json', 'Output results as JSON')
208
215
  .option('--sarif', 'Output results in SARIF format')
209
216
  .option('--csv', 'Output results as CSV')
@@ -224,6 +231,10 @@ program
224
231
  .option('--budget <cents>', 'Max spend in cents for deep analysis (default: 50)', parseInt)
225
232
  .option('--verify', 'Check if leaked secrets are still active (probes provider APIs)')
226
233
  .option('--include-legal', 'Also run the legal risk scan (DMCA, leaked source, IP disputes)')
234
+ .option('--agentic [iterations]', 'Agentic scan→fix→verify loop (default: 3 iterations, target score: 75)', (v) => v ? parseInt(v) : true)
235
+ .option('--agentic-target <score>', 'Target security score for agentic loop (default: 75)', parseInt)
236
+ .option('--hermes-only', 'Run only Hermes-relevant agents (llm + supply-chain categories) for fast CI')
237
+ .option('--fail-below <threshold>', 'Exit 1 if score is below threshold (number or "baseline")')
227
238
  .option('-v, --verbose', 'Verbose output')
228
239
  .action(auditCommand);
229
240
 
@@ -244,7 +255,7 @@ program
244
255
  // -----------------------------------------------------------------------------
245
256
  program
246
257
  .command('red-team [path]')
247
- .description('Multi-agent security audit: 18 agents scan for 80+ attack classes')
258
+ .description('Multi-agent security audit: 22 agents scan for 80+ attack classes')
248
259
  .option('--agents <list>', 'Comma-separated list of agents to run')
249
260
  .option('--json', 'Output results as JSON')
250
261
  .option('--sarif', 'Output results in SARIF format')
@@ -273,6 +284,8 @@ program
273
284
  .option('--status', 'Show current watch status and exit')
274
285
  .option('--threshold <score>', 'Alert when score drops below threshold', parseInt)
275
286
  .option('--debounce <ms>', 'Debounce interval in ms (default: 1500)', parseInt)
287
+ .option('--slack [webhook]', 'Post findings to Slack webhook URL (or set SHIP_SAFE_SLACK_WEBHOOK env var)')
288
+ .option('--pr-comment', 'Post inline findings as GitHub PR review comments (requires gh CLI)')
276
289
  .action(watchCommand);
277
290
 
278
291
  // -----------------------------------------------------------------------------
@@ -457,6 +470,15 @@ How it works:
457
470
  `)
458
471
  .action(hooksCommand);
459
472
 
473
+ // -----------------------------------------------------------------------------
474
+ // ENV AUDIT COMMAND
475
+ // -----------------------------------------------------------------------------
476
+ program
477
+ .command('env-audit [path]')
478
+ .description('Credential health check: verify .env coverage, cross-reference source, check git history')
479
+ .option('--json', 'Output results as JSON')
480
+ .action(envAuditCommand);
481
+
460
482
  // -----------------------------------------------------------------------------
461
483
  // LEGAL COMMAND
462
484
  // -----------------------------------------------------------------------------
@@ -483,6 +505,100 @@ program
483
505
  .description('Diagnose environment: check Node.js, git, API keys, cache, and dependencies')
484
506
  .action(doctorCommand);
485
507
 
508
+ // -----------------------------------------------------------------------------
509
+ // AUTOFIX COMMAND
510
+ // -----------------------------------------------------------------------------
511
+ program
512
+ .command('autofix [path]')
513
+ .description('Apply LLM-generated security fixes from a deep analysis report and open a GitHub PR')
514
+ .option('--report <file>', 'Path to ship-safe JSON report (default: ship-safe-report.json)')
515
+ .option('--severity <level>', 'Minimum severity to fix: critical, high, medium (default: high)')
516
+ .option('--dry-run', 'Preview fixes without applying them or creating a branch')
517
+ .option('--yes', 'Skip confirmation prompt')
518
+ .action((targetPath, options) => autofixCommand({ ...options, path: targetPath }));
519
+
520
+ // -----------------------------------------------------------------------------
521
+ // MEMORY COMMAND
522
+ // -----------------------------------------------------------------------------
523
+ program
524
+ .command('memory [subcommand]')
525
+ .description('Manage security memory: false-positive learning that auto-suppresses known safe findings')
526
+ .addHelpText('after', `
527
+ Subcommands:
528
+ list Show all suppressed findings in memory (default)
529
+ forget <key> Remove a specific entry by key
530
+ clear Wipe all memory entries
531
+
532
+ How it works:
533
+ When --deep analysis confirms a finding is a false positive, it is added to
534
+ .ship-safe/memory.json and suppressed automatically on all future scans.
535
+ `)
536
+ .argument('[args...]')
537
+ .action((subcommand, args, options) => memoryCommand(subcommand, args, options));
538
+
539
+ // -----------------------------------------------------------------------------
540
+ // PLAYBOOK COMMAND
541
+ // -----------------------------------------------------------------------------
542
+ program
543
+ .command('playbook [subcommand]')
544
+ .description('Manage scan playbooks: repo-specific context injected into every LLM analysis')
545
+ .addHelpText('after', `
546
+ Subcommands:
547
+ show Show the current playbook (default)
548
+ add-note "text" Add a custom note to the playbook
549
+
550
+ How it works:
551
+ After 2+ scans, a playbook is auto-generated in .ship-safe/playbook.md with
552
+ your repo's tech stack, auth patterns, and score history. This is injected
553
+ into the LLM system prompt so deep analysis is more accurate for your project.
554
+ `)
555
+ .argument('[args...]')
556
+ .action((subcommand, args, options) => playbookCommand(subcommand, args, options));
557
+
558
+ // -----------------------------------------------------------------------------
559
+ // PLUGINS COMMAND
560
+ // -----------------------------------------------------------------------------
561
+ program
562
+ .command('plugins [action]')
563
+ .description('Manage custom security agent plugins from .ship-safe/agents/')
564
+ .addHelpText('after', `
565
+ Actions:
566
+ list List loaded plugins (default)
567
+ new <name> Scaffold a new plugin in .ship-safe/agents/<name>.js
568
+
569
+ How it works:
570
+ Drop any .js file into .ship-safe/agents/ that exports a default class
571
+ extending BaseAgent with an analyze() method. It will be loaded automatically
572
+ on every audit or watch --deep run.
573
+ `)
574
+ .action((action, options) => {
575
+ const rootPath = path.resolve(process.cwd());
576
+ if (action === 'new') {
577
+ const pluginName = options.args?.[0] || options._name || 'my-rule';
578
+ try {
579
+ const filePath = scaffoldPlugin(rootPath, pluginName);
580
+ console.log(chalk.green(` ✔ Plugin scaffolded: ${filePath}`));
581
+ console.log(chalk.gray(' Edit the file to implement your custom rule, then run ship-safe audit to activate it.'));
582
+ } catch (err) {
583
+ console.error(chalk.red(` Error: ${err.message}`));
584
+ process.exit(1);
585
+ }
586
+ } else {
587
+ // list
588
+ const plugins = listPluginFiles(rootPath);
589
+ if (plugins.length === 0) {
590
+ console.log('\n No custom plugins found in .ship-safe/agents/');
591
+ console.log(chalk.gray(' Create one with: npx ship-safe plugins new my-rule\n'));
592
+ } else {
593
+ console.log(`\n ${chalk.cyan.bold('Custom Plugins')} — ${plugins.length} found\n`);
594
+ for (const p of plugins) {
595
+ console.log(` ${chalk.white(p.name)} ${chalk.gray(`(${(p.size / 1024).toFixed(1)} KB) ${p.path}`)}`);
596
+ }
597
+ console.log();
598
+ }
599
+ }
600
+ });
601
+
486
602
  // -----------------------------------------------------------------------------
487
603
  // PARSE AND RUN
488
604
  // -----------------------------------------------------------------------------
@@ -491,10 +607,10 @@ program
491
607
  if (process.argv.length === 2) {
492
608
  console.log(banner);
493
609
  console.log(chalk.yellow('\nQuick start:\n'));
494
- console.log(chalk.cyan.bold(' v6.0 — Full Security Audit'));
495
- console.log(chalk.white(' npx ship-safe audit . ') + chalk.gray('# Full audit: secrets + 18 agents + deps + remediation'));
610
+ console.log(chalk.cyan.bold(' v8.0 — Ship Safe × Hermes Agent'));
611
+ console.log(chalk.white(' npx ship-safe audit . ') + chalk.gray('# Full audit: secrets + 22 agents + deps + remediation'));
496
612
  console.log(chalk.white(' npx ship-safe audit . --deep') + chalk.gray('# LLM-powered taint analysis (Anthropic/Ollama)'));
497
- console.log(chalk.white(' npx ship-safe red-team . ') + chalk.gray('# 18-agent red team scan (80+ attack classes)'));
613
+ console.log(chalk.white(' npx ship-safe red-team . ') + chalk.gray('# 22-agent red team scan (80+ attack classes)'));
498
614
  console.log(chalk.white(' npx ship-safe vibe-check . ') + chalk.gray('# Fun security check with emoji & shareable badge'));
499
615
  console.log(chalk.white(' npx ship-safe benchmark . ') + chalk.gray('# Compare score against industry averages'));
500
616
  console.log(chalk.white(' npx ship-safe ci . ') + chalk.gray('# CI/CD mode: scan, score, exit code'));
@@ -517,9 +633,17 @@ if (process.argv.length === 2) {
517
633
  console.log(chalk.white(' npx ship-safe rotate . ') + chalk.gray('# Revoke exposed keys (provider guides)'));
518
634
  console.log(chalk.white(' npx ship-safe deps . ') + chalk.gray('# Audit dependencies for CVEs'));
519
635
  console.log(chalk.white(' npx ship-safe score . ') + chalk.gray('# Security health score (0-100)'));
636
+ console.log(chalk.white(' npx ship-safe env-audit . ') + chalk.gray('# Credential health check (after stripe projects env --pull)'));
520
637
  console.log(chalk.white(' npx ship-safe hooks install ') + chalk.gray('# Real-time security gate inside Claude Code (PreToolUse/PostToolUse)'));
521
638
  console.log(chalk.white(' npx ship-safe guard ') + chalk.gray('# Block git push if secrets found'));
522
639
  console.log(chalk.white(' npx ship-safe init ') + chalk.gray('# Add security configs to your project'));
640
+ console.log();
641
+ console.log(chalk.gray(' Intelligence commands:'));
642
+ console.log(chalk.white(' npx ship-safe autofix . ') + chalk.gray('# Apply LLM fixes from --deep report, open PR'));
643
+ console.log(chalk.white(' npx ship-safe memory list ') + chalk.gray('# View / manage false-positive memory'));
644
+ console.log(chalk.white(' npx ship-safe playbook show ') + chalk.gray('# View repo-specific LLM context playbook'));
645
+ console.log(chalk.white(' npx ship-safe plugins list ') + chalk.gray('# Manage custom agent plugins'));
646
+ console.log(chalk.white(' npx ship-safe watch . --deep --slack ') + chalk.gray('# Guardian mode with Slack alerts + PR comments'));
523
647
  console.log(chalk.white('\n npx ship-safe --help ') + chalk.gray('# Show all options'));
524
648
  console.log();
525
649
  process.exit(0);