ship-safe 6.0.0 → 6.1.1
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 +157 -23
- package/cli/agents/abom-generator.js +225 -0
- package/cli/agents/agent-config-scanner.js +547 -0
- package/cli/agents/html-reporter.js +568 -511
- package/cli/agents/index.js +5 -1
- package/cli/agents/scoring-engine.js +11 -0
- package/cli/bin/ship-safe.js +57 -4
- package/cli/commands/abom.js +73 -0
- package/cli/commands/audit.js +2 -0
- package/cli/commands/ci.js +1 -1
- package/cli/commands/guard.js +99 -0
- package/cli/commands/init.js +58 -0
- package/cli/commands/openclaw.js +378 -0
- package/cli/commands/scan-skill.js +329 -0
- package/cli/commands/scan.js +2 -0
- package/cli/commands/score.js +1 -0
- package/cli/commands/update-intel.js +55 -0
- package/cli/commands/watch.js +120 -0
- package/cli/data/threat-intel.json +85 -0
- package/cli/index.js +2 -0
- package/cli/utils/compliance-map.js +125 -0
- package/cli/utils/output.js +230 -229
- package/cli/utils/threat-intel.js +167 -0
- package/package.json +3 -2
- package/cli/__tests__/agents.test.js +0 -1301
package/cli/agents/index.js
CHANGED
|
@@ -25,6 +25,8 @@ export { RAGSecurityAgent } from './rag-security-agent.js';
|
|
|
25
25
|
export { PIIComplianceAgent } from './pii-compliance-agent.js';
|
|
26
26
|
export { VibeCodingAgent } from './vibe-coding-agent.js';
|
|
27
27
|
export { ExceptionHandlerAgent } from './exception-handler-agent.js';
|
|
28
|
+
export { AgentConfigScanner } from './agent-config-scanner.js';
|
|
29
|
+
export { ABOMGenerator } from './abom-generator.js';
|
|
28
30
|
export { VerifierAgent } from './verifier-agent.js';
|
|
29
31
|
export { DeepAnalyzer } from './deep-analyzer.js';
|
|
30
32
|
export { ScoringEngine, GRADES, CATEGORIES } from './scoring-engine.js';
|
|
@@ -33,7 +35,7 @@ export { PolicyEngine } from './policy-engine.js';
|
|
|
33
35
|
export { HTMLReporter } from './html-reporter.js';
|
|
34
36
|
|
|
35
37
|
/**
|
|
36
|
-
* Create a fully configured orchestrator with all
|
|
38
|
+
* Create a fully configured orchestrator with all 16 scanning agents.
|
|
37
39
|
* (VerifierAgent and DeepAnalyzer run as post-processors, not in the agent pool.)
|
|
38
40
|
*/
|
|
39
41
|
import { Orchestrator as OrchestratorClass } from './orchestrator.js';
|
|
@@ -54,6 +56,7 @@ import { RAGSecurityAgent as RAGSecurityAgentClass } from './rag-security-agent.
|
|
|
54
56
|
import { PIIComplianceAgent as PIIComplianceAgentClass } from './pii-compliance-agent.js';
|
|
55
57
|
import { VibeCodingAgent as VibeCodingAgentClass } from './vibe-coding-agent.js';
|
|
56
58
|
import { ExceptionHandlerAgent as ExceptionHandlerAgentClass } from './exception-handler-agent.js';
|
|
59
|
+
import { AgentConfigScanner as AgentConfigScannerClass } from './agent-config-scanner.js';
|
|
57
60
|
|
|
58
61
|
export function buildOrchestrator() {
|
|
59
62
|
const orchestrator = new OrchestratorClass();
|
|
@@ -75,6 +78,7 @@ export function buildOrchestrator() {
|
|
|
75
78
|
new PIIComplianceAgentClass(),
|
|
76
79
|
new VibeCodingAgentClass(),
|
|
77
80
|
new ExceptionHandlerAgentClass(),
|
|
81
|
+
new AgentConfigScannerClass(),
|
|
78
82
|
]);
|
|
79
83
|
return orchestrator;
|
|
80
84
|
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import fs from 'fs';
|
|
13
13
|
import path from 'path';
|
|
14
|
+
import { getComplianceSummary } from '../utils/compliance-map.js';
|
|
14
15
|
|
|
15
16
|
// =============================================================================
|
|
16
17
|
// SCORING CONFIGURATION
|
|
@@ -47,6 +48,7 @@ const FALLBACK_CATEGORY_MAP = {
|
|
|
47
48
|
'rag': 'llm',
|
|
48
49
|
'vibe': 'injection', // Vibe coding findings → Code Vulnerabilities
|
|
49
50
|
'exception': 'injection', // OWASP A10:2025 — Mishandling of Exceptional Conditions
|
|
51
|
+
'agent-config': 'llm', // Agent config security → AI/LLM category
|
|
50
52
|
'recon': null, // skip recon findings
|
|
51
53
|
};
|
|
52
54
|
|
|
@@ -136,12 +138,21 @@ export class ScoringEngine {
|
|
|
136
138
|
const score = Math.max(0, 100 - totalDeduction);
|
|
137
139
|
const grade = GRADES.find(g => score >= g.min);
|
|
138
140
|
|
|
141
|
+
// ── Compliance mapping ─────────────────────────────────────────────────
|
|
142
|
+
let compliance;
|
|
143
|
+
try {
|
|
144
|
+
compliance = getComplianceSummary(findings);
|
|
145
|
+
} catch {
|
|
146
|
+
compliance = null;
|
|
147
|
+
}
|
|
148
|
+
|
|
139
149
|
return {
|
|
140
150
|
score,
|
|
141
151
|
grade,
|
|
142
152
|
categories: categoryResults,
|
|
143
153
|
totalFindings: findings.length,
|
|
144
154
|
totalDepVulns: depVulns.length,
|
|
155
|
+
compliance,
|
|
145
156
|
};
|
|
146
157
|
}
|
|
147
158
|
|
package/cli/bin/ship-safe.js
CHANGED
|
@@ -40,6 +40,11 @@ import { ciCommand } from '../commands/ci.js';
|
|
|
40
40
|
import { diffCommand } from '../commands/diff.js';
|
|
41
41
|
import { vibeCheckCommand } from '../commands/vibe-check.js';
|
|
42
42
|
import { benchmarkCommand } from '../commands/benchmark.js';
|
|
43
|
+
import { openclawCommand } from '../commands/openclaw.js';
|
|
44
|
+
import { scanSkillCommand } from '../commands/scan-skill.js';
|
|
45
|
+
import { abomCommand } from '../commands/abom.js';
|
|
46
|
+
import { updateIntelCommand } from '../commands/update-intel.js';
|
|
47
|
+
import { ABOMGenerator } from '../agents/abom-generator.js';
|
|
43
48
|
import { PolicyEngine } from '../agents/policy-engine.js';
|
|
44
49
|
import { SBOMGenerator } from '../agents/sbom-generator.js';
|
|
45
50
|
|
|
@@ -110,6 +115,7 @@ program
|
|
|
110
115
|
.option('--gitignore', 'Only copy .gitignore')
|
|
111
116
|
.option('--headers', 'Only copy security headers config')
|
|
112
117
|
.option('--agents', 'Only add security rules to AI agent instruction files (CLAUDE.md, .cursor/rules/, .windsurfrules, copilot-instructions.md)')
|
|
118
|
+
.option('--openclaw', 'Generate a hardened openclaw.json template')
|
|
113
119
|
.action(initCommand);
|
|
114
120
|
|
|
115
121
|
// -----------------------------------------------------------------------------
|
|
@@ -128,6 +134,7 @@ program
|
|
|
128
134
|
.command('guard [action]')
|
|
129
135
|
.description('Install a git hook to block pushes if secrets are found')
|
|
130
136
|
.option('--pre-commit', 'Install as pre-commit hook instead of pre-push')
|
|
137
|
+
.option('--generate-hooks', 'Generate defensive Claude Code hooks (.claude/settings.json)')
|
|
131
138
|
.action(guardCommand);
|
|
132
139
|
|
|
133
140
|
// -----------------------------------------------------------------------------
|
|
@@ -192,7 +199,7 @@ program
|
|
|
192
199
|
// -----------------------------------------------------------------------------
|
|
193
200
|
program
|
|
194
201
|
.command('audit [path]')
|
|
195
|
-
.description('Full security audit: secrets +
|
|
202
|
+
.description('Full security audit: secrets + 18 agents + deps + score + deep analysis + remediation plan')
|
|
196
203
|
.option('--json', 'Output results as JSON')
|
|
197
204
|
.option('--sarif', 'Output results in SARIF format')
|
|
198
205
|
.option('--csv', 'Output results as CSV')
|
|
@@ -230,7 +237,7 @@ program
|
|
|
230
237
|
// -----------------------------------------------------------------------------
|
|
231
238
|
program
|
|
232
239
|
.command('red-team [path]')
|
|
233
|
-
.description('Multi-agent security audit:
|
|
240
|
+
.description('Multi-agent security audit: 18 agents scan for 80+ attack classes')
|
|
234
241
|
.option('--agents <list>', 'Comma-separated list of agents to run')
|
|
235
242
|
.option('--json', 'Output results as JSON')
|
|
236
243
|
.option('--sarif', 'Output results in SARIF format')
|
|
@@ -252,6 +259,7 @@ program
|
|
|
252
259
|
.command('watch [path]')
|
|
253
260
|
.description('Continuous monitoring: watch files for security issues in real-time')
|
|
254
261
|
.option('--poll', 'Use polling mode (for network drives)')
|
|
262
|
+
.option('--configs', 'Watch only agent config files (openclaw.json, .cursorrules, mcp.json, etc.)')
|
|
255
263
|
.action(watchCommand);
|
|
256
264
|
|
|
257
265
|
// -----------------------------------------------------------------------------
|
|
@@ -327,6 +335,47 @@ program
|
|
|
327
335
|
.option('--json', 'Output results as JSON')
|
|
328
336
|
.action(benchmarkCommand);
|
|
329
337
|
|
|
338
|
+
// -----------------------------------------------------------------------------
|
|
339
|
+
// OPENCLAW COMMAND
|
|
340
|
+
// -----------------------------------------------------------------------------
|
|
341
|
+
program
|
|
342
|
+
.command('openclaw [path]')
|
|
343
|
+
.description('OpenClaw security scan: agent configs, MCP servers, skills, hooks')
|
|
344
|
+
.option('--fix', 'Auto-harden OpenClaw and agent configurations')
|
|
345
|
+
.option('--preflight', 'Exit non-zero on critical findings (for CI)')
|
|
346
|
+
.option('--red-team', 'Simulate adversarial attacks against agent configs')
|
|
347
|
+
.option('--json', 'Output results as JSON')
|
|
348
|
+
.action(openclawCommand);
|
|
349
|
+
|
|
350
|
+
// -----------------------------------------------------------------------------
|
|
351
|
+
// SCAN-SKILL COMMAND
|
|
352
|
+
// -----------------------------------------------------------------------------
|
|
353
|
+
program
|
|
354
|
+
.command('scan-skill [target]')
|
|
355
|
+
.description('Analyze an AI agent skill for security issues before installing it')
|
|
356
|
+
.option('--all', 'Scan all skills defined in openclaw.json')
|
|
357
|
+
.option('--json', 'Output results as JSON')
|
|
358
|
+
.action(scanSkillCommand);
|
|
359
|
+
|
|
360
|
+
// -----------------------------------------------------------------------------
|
|
361
|
+
// ABOM COMMAND
|
|
362
|
+
// -----------------------------------------------------------------------------
|
|
363
|
+
program
|
|
364
|
+
.command('abom [path]')
|
|
365
|
+
.description('Generate Agent Bill of Materials (CycloneDX ABOM) — MCP servers, skills, configs, LLM providers')
|
|
366
|
+
.option('-o, --output <file>', 'Output file path', 'abom.json')
|
|
367
|
+
.option('--json', 'Output to stdout as JSON')
|
|
368
|
+
.action(abomCommand);
|
|
369
|
+
|
|
370
|
+
// -----------------------------------------------------------------------------
|
|
371
|
+
// UPDATE-INTEL COMMAND
|
|
372
|
+
// -----------------------------------------------------------------------------
|
|
373
|
+
program
|
|
374
|
+
.command('update-intel')
|
|
375
|
+
.description('Update threat intelligence feed (malicious skill hashes, compromised MCP servers)')
|
|
376
|
+
.option('--url <url>', 'Custom feed URL')
|
|
377
|
+
.action(updateIntelCommand);
|
|
378
|
+
|
|
330
379
|
// -----------------------------------------------------------------------------
|
|
331
380
|
// DOCTOR COMMAND
|
|
332
381
|
// -----------------------------------------------------------------------------
|
|
@@ -344,15 +393,19 @@ if (process.argv.length === 2) {
|
|
|
344
393
|
console.log(banner);
|
|
345
394
|
console.log(chalk.yellow('\nQuick start:\n'));
|
|
346
395
|
console.log(chalk.cyan.bold(' v6.0 — Full Security Audit'));
|
|
347
|
-
console.log(chalk.white(' npx ship-safe audit . ') + chalk.gray('# Full audit: secrets +
|
|
396
|
+
console.log(chalk.white(' npx ship-safe audit . ') + chalk.gray('# Full audit: secrets + 18 agents + deps + remediation'));
|
|
348
397
|
console.log(chalk.white(' npx ship-safe audit . --deep') + chalk.gray('# LLM-powered taint analysis (Anthropic/Ollama)'));
|
|
349
|
-
console.log(chalk.white(' npx ship-safe red-team . ') + chalk.gray('#
|
|
398
|
+
console.log(chalk.white(' npx ship-safe red-team . ') + chalk.gray('# 18-agent red team scan (80+ attack classes)'));
|
|
350
399
|
console.log(chalk.white(' npx ship-safe vibe-check . ') + chalk.gray('# Fun security check with emoji & shareable badge'));
|
|
351
400
|
console.log(chalk.white(' npx ship-safe benchmark . ') + chalk.gray('# Compare score against industry averages'));
|
|
352
401
|
console.log(chalk.white(' npx ship-safe ci . ') + chalk.gray('# CI/CD mode: scan, score, exit code'));
|
|
353
402
|
console.log(chalk.white(' npx ship-safe diff ') + chalk.gray('# Scan only changed files (fast pre-commit)'));
|
|
354
403
|
console.log(chalk.white(' npx ship-safe watch . ') + chalk.gray('# Continuous monitoring mode'));
|
|
404
|
+
console.log(chalk.white(' npx ship-safe openclaw . ') + chalk.gray('# OpenClaw & agent config security scan'));
|
|
405
|
+
console.log(chalk.white(' npx ship-safe scan-skill <u>') + chalk.gray('# Vet a skill before installing'));
|
|
406
|
+
console.log(chalk.white(' npx ship-safe abom . ') + chalk.gray('# Agent Bill of Materials (CycloneDX)'));
|
|
355
407
|
console.log(chalk.white(' npx ship-safe sbom . ') + chalk.gray('# Generate CycloneDX SBOM (CRA-ready)'));
|
|
408
|
+
console.log(chalk.white(' npx ship-safe update-intel ') + chalk.gray('# Update threat intelligence feed'));
|
|
356
409
|
console.log(chalk.white(' npx ship-safe policy init ') + chalk.gray('# Create security policy template'));
|
|
357
410
|
console.log(chalk.white(' npx ship-safe doctor ') + chalk.gray('# Check environment and configuration'));
|
|
358
411
|
console.log();
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ABOM Command
|
|
3
|
+
* =============
|
|
4
|
+
*
|
|
5
|
+
* Generate an Agent Bill of Materials in CycloneDX format.
|
|
6
|
+
* Lists all AI agent components: MCP servers, skills, configs, LLM providers.
|
|
7
|
+
*
|
|
8
|
+
* USAGE:
|
|
9
|
+
* ship-safe abom [path] Generate ABOM
|
|
10
|
+
* ship-safe abom . -o agent-bom.json Custom output path
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import path from 'path';
|
|
14
|
+
import chalk from 'chalk';
|
|
15
|
+
import { ABOMGenerator } from '../agents/abom-generator.js';
|
|
16
|
+
import * as output from '../utils/output.js';
|
|
17
|
+
|
|
18
|
+
export async function abomCommand(targetPath = '.', options = {}) {
|
|
19
|
+
const absolutePath = path.resolve(targetPath);
|
|
20
|
+
const outputFile = options.output || 'abom.json';
|
|
21
|
+
|
|
22
|
+
console.log();
|
|
23
|
+
output.header('Ship Safe — Agent Bill of Materials');
|
|
24
|
+
console.log();
|
|
25
|
+
|
|
26
|
+
const generator = new ABOMGenerator();
|
|
27
|
+
const bom = generator.generate(absolutePath);
|
|
28
|
+
|
|
29
|
+
if (options.json) {
|
|
30
|
+
console.log(JSON.stringify(bom, null, 2));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
generator.generateToFile(absolutePath, outputFile);
|
|
35
|
+
|
|
36
|
+
const agentComponents = bom.components.filter(c => c.properties?.some(p => p.name?.startsWith('agent:')));
|
|
37
|
+
const mcpServers = agentComponents.filter(c => c.properties?.some(p => p.value === 'mcp-server'));
|
|
38
|
+
const skills = agentComponents.filter(c => c.properties?.some(p => p.value === 'openclaw-skill'));
|
|
39
|
+
const configs = agentComponents.filter(c => c.properties?.some(p => p.value === 'agent-rules' || p.value === 'agent-config'));
|
|
40
|
+
const providers = agentComponents.filter(c => c.properties?.some(p => p.value === 'llm-provider'));
|
|
41
|
+
|
|
42
|
+
console.log(chalk.gray(` Project: ${bom.metadata.component.name}`));
|
|
43
|
+
console.log();
|
|
44
|
+
console.log(` ${chalk.cyan('MCP Servers')}: ${mcpServers.length}`);
|
|
45
|
+
console.log(` ${chalk.cyan('OpenClaw Skills')}: ${skills.length}`);
|
|
46
|
+
console.log(` ${chalk.cyan('Agent Configs')}: ${configs.length}`);
|
|
47
|
+
console.log(` ${chalk.cyan('LLM Providers')}: ${providers.length}`);
|
|
48
|
+
console.log(` ${chalk.cyan('Total Components')}: ${bom.components.length}`);
|
|
49
|
+
console.log();
|
|
50
|
+
|
|
51
|
+
if (mcpServers.length > 0) {
|
|
52
|
+
console.log(chalk.white.bold(' MCP Servers:'));
|
|
53
|
+
for (const s of mcpServers) {
|
|
54
|
+
const cmd = s.properties?.find(p => p.name === 'agent:command')?.value || 'N/A';
|
|
55
|
+
console.log(chalk.gray(` · ${s.name} (${cmd})`));
|
|
56
|
+
}
|
|
57
|
+
console.log();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (skills.length > 0) {
|
|
61
|
+
console.log(chalk.white.bold(' OpenClaw Skills:'));
|
|
62
|
+
for (const s of skills) {
|
|
63
|
+
const verified = s.properties?.find(p => p.name === 'agent:verified')?.value;
|
|
64
|
+
const icon = verified === 'true' ? chalk.green('✔') : chalk.yellow('?');
|
|
65
|
+
console.log(chalk.gray(` ${icon} ${s.name}`));
|
|
66
|
+
}
|
|
67
|
+
console.log();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
console.log(chalk.green(` ✔ ABOM saved to ${outputFile}`));
|
|
71
|
+
console.log(chalk.gray(` Format: CycloneDX ${bom.specVersion}`));
|
|
72
|
+
console.log();
|
|
73
|
+
}
|
package/cli/commands/audit.js
CHANGED
|
@@ -391,6 +391,7 @@ export async function auditCommand(targetPath = '.', options = {}) {
|
|
|
391
391
|
reporter.generateFullReport(scoreResult, filteredFindings, depVulns, recon, remediationPlan, absolutePath, htmlPath);
|
|
392
392
|
console.log();
|
|
393
393
|
console.log(chalk.cyan(` Full report: ${chalk.white.bold(htmlPath)}`));
|
|
394
|
+
console.log(chalk.gray(` Dashboard: `) + chalk.cyan('https://shipsafecli.com/app'));
|
|
394
395
|
|
|
395
396
|
// PDF export
|
|
396
397
|
if (options.pdf) {
|
|
@@ -662,6 +663,7 @@ function outputJSON(scoreResult, findings, depVulns, recon, agentResults, remedi
|
|
|
662
663
|
recon,
|
|
663
664
|
agents: agentResults,
|
|
664
665
|
};
|
|
666
|
+
if (scoreResult.compliance) output.compliance = scoreResult.compliance;
|
|
665
667
|
if (suppressions) output.suppressions = suppressions;
|
|
666
668
|
if (history && history.length >= 2) {
|
|
667
669
|
const prev = history[history.length - 2];
|
package/cli/commands/ci.js
CHANGED
|
@@ -311,7 +311,7 @@ function postPRComment(scoreResult, findings, depVulns, rootPath, duration) {
|
|
|
311
311
|
body += '> No security issues found — looking good! 🎉\n\n';
|
|
312
312
|
}
|
|
313
313
|
|
|
314
|
-
body +=
|
|
314
|
+
body += `\n---\n<sub>Generated by <a href="https://shipsafecli.com">Ship Safe</a> · <a href="https://shipsafecli.com/app">View full report in dashboard</a></sub>`;
|
|
315
315
|
|
|
316
316
|
// Post comment via gh CLI
|
|
317
317
|
execFileSync('gh', ['pr', 'comment', prNumber, '--body', body], { // ship-safe-ignore — execFileSync, not MCP
|
package/cli/commands/guard.js
CHANGED
|
@@ -136,6 +136,10 @@ export async function guardCommand(action, options = {}) {
|
|
|
136
136
|
process.exit(1);
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
if (options.generateHooks) {
|
|
140
|
+
return generateClaudeHooks(cwd);
|
|
141
|
+
}
|
|
142
|
+
|
|
139
143
|
if (action === 'remove') {
|
|
140
144
|
return removeHooks(gitDir, cwd);
|
|
141
145
|
}
|
|
@@ -278,6 +282,101 @@ function removeHooks(gitDir, cwd) {
|
|
|
278
282
|
}
|
|
279
283
|
}
|
|
280
284
|
|
|
285
|
+
// =============================================================================
|
|
286
|
+
// CLAUDE CODE DEFENSIVE HOOKS
|
|
287
|
+
// =============================================================================
|
|
288
|
+
|
|
289
|
+
function generateClaudeHooks(cwd) {
|
|
290
|
+
output.header('Generating Defensive Claude Code Hooks');
|
|
291
|
+
|
|
292
|
+
const claudeDir = path.join(cwd, '.claude');
|
|
293
|
+
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
294
|
+
|
|
295
|
+
// Defensive hooks that block common attack patterns
|
|
296
|
+
const preToolCmd = [
|
|
297
|
+
'node -e "',
|
|
298
|
+
'const c=process.argv[1]||String();',
|
|
299
|
+
'const bad=[/curl.*[|].*(?:bash|sh|node|python)/i,/wget.*[|].*(?:bash|sh)/i,',
|
|
300
|
+
'/rm\\s+-rf\\s+\\//,/webhook[.]site|requestbin|ngrok[.]io|pipedream/i,',
|
|
301
|
+
'/base64.*-d.*[|].*(?:bash|sh)/i];',
|
|
302
|
+
'const m=bad.find(r=>r.test(c));',
|
|
303
|
+
'if(m){console.error(String.fromCharCode(10060)+String.fromCharCode(32)+c.slice(0,80));process.exit(1)}',
|
|
304
|
+
'" "$INPUT"',
|
|
305
|
+
].join('');
|
|
306
|
+
|
|
307
|
+
const postToolCmd = [
|
|
308
|
+
'node -e "',
|
|
309
|
+
'const f=process.argv[1]||String();',
|
|
310
|
+
'if(/[.]env$|[.]env[.]/.test(f)){console.log(String.fromCharCode(9888)+String.fromCharCode(32)+f)}',
|
|
311
|
+
'" "$INPUT"',
|
|
312
|
+
].join('');
|
|
313
|
+
|
|
314
|
+
const defensiveHooks = {
|
|
315
|
+
hooks: {
|
|
316
|
+
PreToolUse: [
|
|
317
|
+
{
|
|
318
|
+
matcher: 'Bash',
|
|
319
|
+
command: preToolCmd,
|
|
320
|
+
description: 'Ship Safe: Block dangerous command patterns (curl|bash, rm -rf /, exfil domains)',
|
|
321
|
+
},
|
|
322
|
+
],
|
|
323
|
+
PostToolUse: [
|
|
324
|
+
{
|
|
325
|
+
matcher: 'Write',
|
|
326
|
+
command: postToolCmd,
|
|
327
|
+
description: 'Ship Safe: Alert when .env files are modified',
|
|
328
|
+
},
|
|
329
|
+
],
|
|
330
|
+
},
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
// Merge with existing settings
|
|
334
|
+
let existing = {};
|
|
335
|
+
if (fs.existsSync(settingsPath)) {
|
|
336
|
+
try {
|
|
337
|
+
existing = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
338
|
+
} catch { /* fresh start */ }
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Merge hooks — don't overwrite existing hooks, append
|
|
342
|
+
if (!existing.hooks) existing.hooks = {};
|
|
343
|
+
if (!existing.hooks.PreToolUse) existing.hooks.PreToolUse = [];
|
|
344
|
+
if (!existing.hooks.PostToolUse) existing.hooks.PostToolUse = [];
|
|
345
|
+
|
|
346
|
+
// Check if ship-safe hooks already present
|
|
347
|
+
const hasPreHook = existing.hooks.PreToolUse.some(h => h.description?.includes('Ship Safe'));
|
|
348
|
+
const hasPostHook = existing.hooks.PostToolUse.some(h => h.description?.includes('Ship Safe'));
|
|
349
|
+
|
|
350
|
+
if (hasPreHook && hasPostHook) {
|
|
351
|
+
output.warning('Ship Safe hooks already installed in .claude/settings.json');
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (!hasPreHook) {
|
|
356
|
+
existing.hooks.PreToolUse.push(...defensiveHooks.hooks.PreToolUse);
|
|
357
|
+
}
|
|
358
|
+
if (!hasPostHook) {
|
|
359
|
+
existing.hooks.PostToolUse.push(...defensiveHooks.hooks.PostToolUse);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Write
|
|
363
|
+
if (!fs.existsSync(claudeDir)) fs.mkdirSync(claudeDir, { recursive: true });
|
|
364
|
+
fs.writeFileSync(settingsPath, JSON.stringify(existing, null, 2) + '\n');
|
|
365
|
+
|
|
366
|
+
output.success('Defensive hooks installed in .claude/settings.json');
|
|
367
|
+
console.log();
|
|
368
|
+
console.log(chalk.gray(' Hooks installed:'));
|
|
369
|
+
console.log(chalk.gray(' PreToolUse → Block curl|bash, rm -rf /, exfil domains'));
|
|
370
|
+
console.log(chalk.gray(' PostToolUse → Alert on .env file modifications'));
|
|
371
|
+
console.log();
|
|
372
|
+
console.log(chalk.gray(' These hooks protect against:'));
|
|
373
|
+
console.log(chalk.gray(' • Remote code execution via piped downloads'));
|
|
374
|
+
console.log(chalk.gray(' • Data exfiltration to webhook.site/ngrok/requestbin'));
|
|
375
|
+
console.log(chalk.gray(' • Destructive filesystem operations'));
|
|
376
|
+
console.log(chalk.gray(' • Unauthorized .env modifications'));
|
|
377
|
+
console.log();
|
|
378
|
+
}
|
|
379
|
+
|
|
281
380
|
// =============================================================================
|
|
282
381
|
// UTILITIES
|
|
283
382
|
// =============================================================================
|
package/cli/commands/init.js
CHANGED
|
@@ -50,6 +50,11 @@ export async function initCommand(options = {}) {
|
|
|
50
50
|
// Determine which files to copy.
|
|
51
51
|
// If a specific flag is set, only run that category.
|
|
52
52
|
// With no flags, run everything.
|
|
53
|
+
// Handle --openclaw flag separately
|
|
54
|
+
if (options.openclaw) {
|
|
55
|
+
return handleOpenClawInit(targetDir, options.force, results);
|
|
56
|
+
}
|
|
57
|
+
|
|
53
58
|
const hasSpecificFlag = options.gitignore || options.headers || options.agents;
|
|
54
59
|
const copyGitignore = hasSpecificFlag ? !!options.gitignore : true;
|
|
55
60
|
const copyHeaders = hasSpecificFlag ? !!options.headers : true;
|
|
@@ -296,6 +301,59 @@ async function handleAgentFiles(targetDir, force, results) {
|
|
|
296
301
|
}
|
|
297
302
|
}
|
|
298
303
|
|
|
304
|
+
// =============================================================================
|
|
305
|
+
// OPENCLAW HARDENED CONFIG
|
|
306
|
+
// =============================================================================
|
|
307
|
+
|
|
308
|
+
const HARDENED_OPENCLAW = `{
|
|
309
|
+
"// SECURITY": "Generated by ship-safe init --openclaw — hardened defaults",
|
|
310
|
+
|
|
311
|
+
"// host": "Bind to localhost only — never 0.0.0.0 (CVE-2026-25253 ClawJacked)",
|
|
312
|
+
"host": "127.0.0.1",
|
|
313
|
+
"port": 3100,
|
|
314
|
+
|
|
315
|
+
"// auth": "Always require authentication — prevents unauthorized agent takeover",
|
|
316
|
+
"auth": {
|
|
317
|
+
"type": "apiKey",
|
|
318
|
+
"key": "\${OPENCLAW_API_KEY}"
|
|
319
|
+
},
|
|
320
|
+
|
|
321
|
+
"// url": "Use wss:// for all non-localhost connections (encrypted WebSocket)",
|
|
322
|
+
"url": "wss://localhost:3100",
|
|
323
|
+
|
|
324
|
+
"// safeBins": "Allowlist of binaries the agent can execute — block everything else",
|
|
325
|
+
"safeBins": ["node", "git", "npx", "npm"],
|
|
326
|
+
|
|
327
|
+
"// skills": "Only add verified skills from trusted sources — ClawHavoc had 1,184 malicious skills",
|
|
328
|
+
"skills": [],
|
|
329
|
+
|
|
330
|
+
"// logging": "Enable audit logging for security monitoring",
|
|
331
|
+
"logging": {
|
|
332
|
+
"level": "info",
|
|
333
|
+
"auditLog": true
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
`;
|
|
337
|
+
|
|
338
|
+
async function handleOpenClawInit(targetDir, force, results) {
|
|
339
|
+
const targetPath = path.join(targetDir, 'openclaw.json');
|
|
340
|
+
|
|
341
|
+
if (fs.existsSync(targetPath) && !force) {
|
|
342
|
+
results.skipped.push('openclaw.json (already exists, use -f to overwrite)');
|
|
343
|
+
} else {
|
|
344
|
+
fs.writeFileSync(targetPath, HARDENED_OPENCLAW.trim() + '\n');
|
|
345
|
+
results.copied.push('openclaw.json (hardened template)');
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
printSummary(results);
|
|
349
|
+
|
|
350
|
+
console.log(chalk.cyan('Important:'));
|
|
351
|
+
console.log(chalk.white(' 1.') + ' Set the OPENCLAW_API_KEY environment variable');
|
|
352
|
+
console.log(chalk.white(' 2.') + ' Only add verified skills from trusted sources');
|
|
353
|
+
console.log(chalk.white(' 3.') + ' Run ' + chalk.cyan('npx ship-safe openclaw .') + ' to verify security');
|
|
354
|
+
console.log();
|
|
355
|
+
}
|
|
356
|
+
|
|
299
357
|
// =============================================================================
|
|
300
358
|
// SUMMARY
|
|
301
359
|
// =============================================================================
|