ship-safe 6.1.0 → 6.2.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 (48) hide show
  1. package/README.md +735 -594
  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 +40 -4
  10. package/cli/agents/index.js +84 -84
  11. package/cli/agents/injection-tester.js +500 -500
  12. package/cli/agents/llm-redteam.js +251 -251
  13. package/cli/agents/mobile-scanner.js +231 -231
  14. package/cli/agents/orchestrator.js +322 -322
  15. package/cli/agents/pii-compliance-agent.js +301 -301
  16. package/cli/agents/scoring-engine.js +248 -248
  17. package/cli/agents/supabase-rls-agent.js +154 -154
  18. package/cli/agents/supply-chain-agent.js +650 -507
  19. package/cli/bin/ship-safe.js +452 -426
  20. package/cli/commands/agent.js +608 -608
  21. package/cli/commands/audit.js +986 -979
  22. package/cli/commands/baseline.js +193 -193
  23. package/cli/commands/ci.js +342 -342
  24. package/cli/commands/deps.js +516 -516
  25. package/cli/commands/doctor.js +159 -159
  26. package/cli/commands/fix.js +218 -218
  27. package/cli/commands/hooks.js +268 -0
  28. package/cli/commands/init.js +407 -407
  29. package/cli/commands/mcp.js +304 -304
  30. package/cli/commands/red-team.js +7 -1
  31. package/cli/commands/remediate.js +798 -798
  32. package/cli/commands/rotate.js +571 -571
  33. package/cli/commands/scan.js +569 -567
  34. package/cli/commands/score.js +449 -448
  35. package/cli/commands/watch.js +281 -281
  36. package/cli/hooks/patterns.js +313 -0
  37. package/cli/hooks/post-tool-use.js +140 -0
  38. package/cli/hooks/pre-tool-use.js +186 -0
  39. package/cli/index.js +73 -69
  40. package/cli/providers/llm-provider.js +397 -287
  41. package/cli/utils/autofix-rules.js +74 -74
  42. package/cli/utils/cache-manager.js +311 -311
  43. package/cli/utils/output.js +1 -0
  44. package/cli/utils/patterns.js +1121 -1121
  45. package/cli/utils/pdf-generator.js +94 -94
  46. package/package.json +69 -68
  47. package/cli/__tests__/agents.test.js +0 -1301
  48. 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
+ }