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.
- package/README.md +735 -594
- package/cli/agents/api-fuzzer.js +345 -345
- package/cli/agents/auth-bypass-agent.js +348 -348
- package/cli/agents/base-agent.js +272 -272
- package/cli/agents/cicd-scanner.js +236 -201
- package/cli/agents/config-auditor.js +521 -521
- package/cli/agents/deep-analyzer.js +6 -2
- package/cli/agents/git-history-scanner.js +170 -170
- package/cli/agents/html-reporter.js +40 -4
- package/cli/agents/index.js +84 -84
- package/cli/agents/injection-tester.js +500 -500
- package/cli/agents/llm-redteam.js +251 -251
- package/cli/agents/mobile-scanner.js +231 -231
- package/cli/agents/orchestrator.js +322 -322
- package/cli/agents/pii-compliance-agent.js +301 -301
- package/cli/agents/scoring-engine.js +248 -248
- package/cli/agents/supabase-rls-agent.js +154 -154
- package/cli/agents/supply-chain-agent.js +650 -507
- package/cli/bin/ship-safe.js +452 -426
- package/cli/commands/agent.js +608 -608
- package/cli/commands/audit.js +986 -979
- package/cli/commands/baseline.js +193 -193
- package/cli/commands/ci.js +342 -342
- package/cli/commands/deps.js +516 -516
- package/cli/commands/doctor.js +159 -159
- package/cli/commands/fix.js +218 -218
- package/cli/commands/hooks.js +268 -0
- package/cli/commands/init.js +407 -407
- package/cli/commands/mcp.js +304 -304
- package/cli/commands/red-team.js +7 -1
- package/cli/commands/remediate.js +798 -798
- package/cli/commands/rotate.js +571 -571
- package/cli/commands/scan.js +569 -567
- package/cli/commands/score.js +449 -448
- package/cli/commands/watch.js +281 -281
- package/cli/hooks/patterns.js +313 -0
- package/cli/hooks/post-tool-use.js +140 -0
- package/cli/hooks/pre-tool-use.js +186 -0
- package/cli/index.js +73 -69
- package/cli/providers/llm-provider.js +397 -287
- package/cli/utils/autofix-rules.js +74 -74
- package/cli/utils/cache-manager.js +311 -311
- package/cli/utils/output.js +1 -0
- package/cli/utils/patterns.js +1121 -1121
- package/cli/utils/pdf-generator.js +94 -94
- package/package.json +69 -68
- package/cli/__tests__/agents.test.js +0 -1301
- 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
|
+
}
|