ship-safe 6.1.1 → 6.3.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.
Files changed (49) hide show
  1. package/README.md +748 -641
  2. package/cli/agents/api-fuzzer.js +345 -345
  3. package/cli/agents/auth-bypass-agent.js +348 -348
  4. package/cli/agents/base-agent.js +272 -272
  5. package/cli/agents/cicd-scanner.js +236 -201
  6. package/cli/agents/config-auditor.js +521 -521
  7. package/cli/agents/deep-analyzer.js +6 -2
  8. package/cli/agents/git-history-scanner.js +170 -170
  9. package/cli/agents/html-reporter.js +568 -568
  10. package/cli/agents/index.js +85 -84
  11. package/cli/agents/injection-tester.js +500 -500
  12. package/cli/agents/legal-risk-agent.js +302 -0
  13. package/cli/agents/llm-redteam.js +251 -251
  14. package/cli/agents/mobile-scanner.js +231 -231
  15. package/cli/agents/orchestrator.js +322 -322
  16. package/cli/agents/pii-compliance-agent.js +301 -301
  17. package/cli/agents/scoring-engine.js +248 -248
  18. package/cli/agents/supabase-rls-agent.js +154 -154
  19. package/cli/agents/supply-chain-agent.js +650 -507
  20. package/cli/bin/ship-safe.js +464 -426
  21. package/cli/commands/agent.js +608 -608
  22. package/cli/commands/audit.js +1006 -980
  23. package/cli/commands/baseline.js +193 -193
  24. package/cli/commands/ci.js +342 -342
  25. package/cli/commands/deps.js +516 -516
  26. package/cli/commands/doctor.js +159 -159
  27. package/cli/commands/fix.js +218 -218
  28. package/cli/commands/hooks.js +268 -0
  29. package/cli/commands/init.js +407 -407
  30. package/cli/commands/legal.js +158 -0
  31. package/cli/commands/mcp.js +304 -304
  32. package/cli/commands/red-team.js +7 -1
  33. package/cli/commands/remediate.js +798 -798
  34. package/cli/commands/rotate.js +571 -571
  35. package/cli/commands/scan.js +569 -569
  36. package/cli/commands/score.js +449 -449
  37. package/cli/commands/watch.js +281 -281
  38. package/cli/hooks/patterns.js +313 -0
  39. package/cli/hooks/post-tool-use.js +140 -0
  40. package/cli/hooks/pre-tool-use.js +186 -0
  41. package/cli/index.js +73 -69
  42. package/cli/providers/llm-provider.js +397 -287
  43. package/cli/utils/autofix-rules.js +74 -74
  44. package/cli/utils/cache-manager.js +311 -311
  45. package/cli/utils/output.js +230 -230
  46. package/cli/utils/patterns.js +1121 -1121
  47. package/cli/utils/pdf-generator.js +94 -94
  48. package/package.json +69 -69
  49. package/configs/supabase/rls-templates.sql +0 -242
@@ -0,0 +1,268 @@
1
+ /**
2
+ * Hooks Command
3
+ * =============
4
+ *
5
+ * Installs ship-safe as PreToolUse / PostToolUse hooks in Claude Code
6
+ * (~/.claude/settings.json). Once installed, ship-safe runs automatically
7
+ * on every Write, Edit, and Bash tool call — blocking secrets before they
8
+ * land on disk and feeding advisory findings back into the conversation.
9
+ *
10
+ * USAGE:
11
+ * npx ship-safe hooks install Install hooks into ~/.claude/settings.json
12
+ * npx ship-safe hooks remove Remove ship-safe hooks
13
+ * npx ship-safe hooks status Show whether hooks are installed
14
+ *
15
+ * HOOK BEHAVIOUR:
16
+ * PreToolUse — blocks Write/Edit if critical secrets detected; blocks
17
+ * dangerous Bash patterns (curl|bash, credential exfiltration)
18
+ * PostToolUse — scans the written file and injects advisory findings into
19
+ * Claude's context (never blocks — just informs)
20
+ *
21
+ * STABLE PATH STRATEGY:
22
+ * npx caches packages in a volatile temp directory. Writing that temp path
23
+ * to ~/.claude/settings.json means hooks break silently as soon as npx
24
+ * clears or rotates its cache. Instead, we copy the three hook scripts to
25
+ * ~/.ship-safe/hooks/ (a stable, user-owned directory) and register those
26
+ * paths. They survive npx cache rotations and package updates.
27
+ */
28
+
29
+ import fs from 'fs';
30
+ import path from 'path';
31
+ import os from 'os';
32
+ import chalk from 'chalk';
33
+ import { fileURLToPath } from 'url';
34
+
35
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
36
+
37
+ // Source: hook scripts shipped inside the package
38
+ const PKG_HOOK_DIR = path.resolve(__dirname, '../hooks');
39
+
40
+ // Stable destination: user-owned, survives npx cache rotations
41
+ const STABLE_HOOK_DIR = path.join(os.homedir(), '.ship-safe', 'hooks');
42
+ const PRE_HOOK_SCRIPT = path.join(STABLE_HOOK_DIR, 'pre-tool-use.js');
43
+ const POST_HOOK_SCRIPT = path.join(STABLE_HOOK_DIR, 'post-tool-use.js');
44
+
45
+ // Claude Code settings.json location
46
+ const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json');
47
+
48
+ // The command strings we register (use stable paths)
49
+ const PRE_COMMAND = `node "${PRE_HOOK_SCRIPT}"`;
50
+ const POST_COMMAND = `node "${POST_HOOK_SCRIPT}"`;
51
+
52
+ // =============================================================================
53
+ // Public API
54
+ // =============================================================================
55
+
56
+ export async function hooksCommand(action = 'install', _options = {}) {
57
+ switch (action) {
58
+ case 'install': return install();
59
+ case 'remove': return remove();
60
+ case 'status': return status();
61
+ default:
62
+ console.error(chalk.red(`Unknown action: ${action}. Use: install | remove | status`));
63
+ process.exit(1);
64
+ }
65
+ }
66
+
67
+ // =============================================================================
68
+ // Install
69
+ // =============================================================================
70
+
71
+ function install() {
72
+ // ── 1. Copy hook scripts to stable location ────────────────────────────────
73
+ const scripts = ['pre-tool-use.js', 'post-tool-use.js', 'patterns.js'];
74
+
75
+ for (const script of scripts) {
76
+ const src = path.join(PKG_HOOK_DIR, script);
77
+ if (!fs.existsSync(src)) {
78
+ console.error(chalk.red(`Hook script not found: ${src}. Try reinstalling ship-safe.`));
79
+ process.exit(1);
80
+ }
81
+ }
82
+
83
+ try {
84
+ fs.mkdirSync(STABLE_HOOK_DIR, { recursive: true });
85
+ for (const script of scripts) {
86
+ fs.copyFileSync(
87
+ path.join(PKG_HOOK_DIR, script),
88
+ path.join(STABLE_HOOK_DIR, script)
89
+ );
90
+ }
91
+ } catch (err) {
92
+ console.error(chalk.red(`Failed to copy hook scripts to ${STABLE_HOOK_DIR}: ${err.message}`));
93
+ process.exit(1);
94
+ }
95
+
96
+ // ── 2. Register stable paths in ~/.claude/settings.json ───────────────────
97
+ const settings = readSettings();
98
+ ensureHooksStructure(settings);
99
+
100
+ let changed = false;
101
+
102
+ // PreToolUse: Write / Edit / MultiEdit / Bash
103
+ const preEntry = buildEntry(
104
+ ['Write', 'Edit', 'MultiEdit', 'Bash'],
105
+ PRE_COMMAND,
106
+ 'ship-safe pre-tool-use: block secrets in writes, dangerous bash patterns'
107
+ );
108
+ if (!hasEntry(settings.hooks.PreToolUse, PRE_COMMAND)) {
109
+ settings.hooks.PreToolUse.push(preEntry);
110
+ changed = true;
111
+ }
112
+
113
+ // PostToolUse: Write / Edit / MultiEdit
114
+ const postEntry = buildEntry(
115
+ ['Write', 'Edit', 'MultiEdit'],
116
+ POST_COMMAND,
117
+ 'ship-safe post-tool-use: advisory scan after file writes'
118
+ );
119
+ if (!hasEntry(settings.hooks.PostToolUse, POST_COMMAND)) {
120
+ settings.hooks.PostToolUse.push(postEntry);
121
+ changed = true;
122
+ }
123
+
124
+ if (!changed) {
125
+ console.log(chalk.green('✔ ship-safe hooks are already installed.'));
126
+ printStatus(settings);
127
+ return;
128
+ }
129
+
130
+ writeSettings(settings);
131
+
132
+ console.log(chalk.green.bold('\n✔ ship-safe hooks installed successfully.\n'));
133
+ console.log(chalk.gray(' Hook scripts: ') + chalk.white(STABLE_HOOK_DIR));
134
+ console.log(chalk.gray(' Settings file: ') + chalk.white(CLAUDE_SETTINGS_PATH));
135
+ console.log();
136
+ console.log(chalk.cyan(' What happens now:'));
137
+ console.log(chalk.white(' Write / Edit ') + chalk.gray('→ blocked if critical secrets detected in content'));
138
+ console.log(chalk.white(' Bash ') + chalk.gray('→ blocked on curl|bash, credential exfiltration patterns'));
139
+ console.log(chalk.white(' Write / Edit ') + chalk.gray('→ advisory scan after save (findings injected into context)'));
140
+ console.log();
141
+ console.log(chalk.gray(' To remove: npx ship-safe hooks remove'));
142
+ console.log(chalk.gray(' To verify: npx ship-safe hooks status\n'));
143
+ }
144
+
145
+ // =============================================================================
146
+ // Remove
147
+ // =============================================================================
148
+
149
+ function remove() {
150
+ const settings = readSettings();
151
+
152
+ if (!settings.hooks) {
153
+ console.log(chalk.yellow('No hooks configured in settings.json.'));
154
+ return;
155
+ }
156
+
157
+ let removed = 0;
158
+
159
+ for (const event of ['PreToolUse', 'PostToolUse']) {
160
+ if (!Array.isArray(settings.hooks[event])) continue;
161
+ const before = settings.hooks[event].length;
162
+ settings.hooks[event] = settings.hooks[event].filter(entry => !isOurEntry(entry));
163
+ removed += before - settings.hooks[event].length;
164
+ }
165
+
166
+ if (removed === 0) {
167
+ console.log(chalk.yellow('No ship-safe hooks found in settings.json.'));
168
+ return;
169
+ }
170
+
171
+ writeSettings(settings);
172
+ console.log(chalk.green(`✔ Removed ${removed} ship-safe hook(s) from ${CLAUDE_SETTINGS_PATH}`));
173
+ console.log(chalk.gray(` Hook scripts kept at ${STABLE_HOOK_DIR} (safe to delete manually)`));
174
+ }
175
+
176
+ // =============================================================================
177
+ // Status
178
+ // =============================================================================
179
+
180
+ function status() {
181
+ const settings = readSettings();
182
+ printStatus(settings);
183
+ }
184
+
185
+ function printStatus(settings) {
186
+ const preInstalled = settings.hooks?.PreToolUse && hasEntry(settings.hooks.PreToolUse, PRE_COMMAND);
187
+ const postInstalled = settings.hooks?.PostToolUse && hasEntry(settings.hooks.PostToolUse, POST_COMMAND);
188
+ const scriptsExist = fs.existsSync(PRE_HOOK_SCRIPT) && fs.existsSync(POST_HOOK_SCRIPT);
189
+
190
+ console.log(chalk.bold('\nship-safe Claude Code hooks status:\n'));
191
+ console.log(
192
+ (preInstalled ? chalk.green(' ✔') : chalk.red(' ✗')) +
193
+ chalk.white(' PreToolUse ') +
194
+ chalk.gray('(block secrets in writes, dangerous bash commands)')
195
+ );
196
+ console.log(
197
+ (postInstalled ? chalk.green(' ✔') : chalk.red(' ✗')) +
198
+ chalk.white(' PostToolUse ') +
199
+ chalk.gray('(advisory scan after file writes)')
200
+ );
201
+ console.log(
202
+ (scriptsExist ? chalk.green(' ✔') : chalk.yellow(' ✗')) +
203
+ chalk.white(' Hook scripts') +
204
+ chalk.gray(` (${STABLE_HOOK_DIR})`)
205
+ );
206
+ console.log();
207
+
208
+ if (!preInstalled || !postInstalled) {
209
+ console.log(chalk.gray(' Run: npx ship-safe hooks install'));
210
+ }
211
+ console.log();
212
+ }
213
+
214
+ // =============================================================================
215
+ // Settings helpers
216
+ // =============================================================================
217
+
218
+ function readSettings() {
219
+ try {
220
+ if (fs.existsSync(CLAUDE_SETTINGS_PATH)) {
221
+ return JSON.parse(fs.readFileSync(CLAUDE_SETTINGS_PATH, 'utf8'));
222
+ }
223
+ } catch {
224
+ // If the file exists but is malformed, back it up and start fresh
225
+ const backup = CLAUDE_SETTINGS_PATH + '.bak';
226
+ try { fs.copyFileSync(CLAUDE_SETTINGS_PATH, backup); } catch {}
227
+ console.warn(chalk.yellow(`Warning: could not parse existing settings.json — backed up to ${backup}`));
228
+ }
229
+ return {};
230
+ }
231
+
232
+ function writeSettings(settings) {
233
+ const dir = path.dirname(CLAUDE_SETTINGS_PATH);
234
+ fs.mkdirSync(dir, { recursive: true });
235
+ fs.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2) + '\n', 'utf8');
236
+ }
237
+
238
+ function ensureHooksStructure(settings) {
239
+ if (!settings.hooks) settings.hooks = {};
240
+ if (!Array.isArray(settings.hooks.PreToolUse)) settings.hooks.PreToolUse = [];
241
+ if (!Array.isArray(settings.hooks.PostToolUse)) settings.hooks.PostToolUse = [];
242
+ }
243
+
244
+ function buildEntry(matchers, command, description) {
245
+ return {
246
+ matcher: matchers.join('|'),
247
+ hooks: [
248
+ {
249
+ type: 'command',
250
+ command,
251
+ description,
252
+ },
253
+ ],
254
+ };
255
+ }
256
+
257
+ function hasEntry(list, command) {
258
+ if (!Array.isArray(list)) return false;
259
+ return list.some(entry =>
260
+ Array.isArray(entry.hooks) &&
261
+ entry.hooks.some(h => h.command === command)
262
+ );
263
+ }
264
+
265
+ function isOurEntry(entry) {
266
+ if (!Array.isArray(entry.hooks)) return false;
267
+ return entry.hooks.some(h => h.command === PRE_COMMAND || h.command === POST_COMMAND);
268
+ }