ship-safe 7.0.0 → 8.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.
- package/README.md +80 -21
- package/cli/agents/agent-attestation-agent.js +318 -0
- package/cli/agents/agentic-security-agent.js +35 -0
- package/cli/agents/cicd-scanner.js +22 -0
- package/cli/agents/config-auditor.js +235 -0
- package/cli/agents/hermes-security-agent.js +536 -0
- package/cli/agents/index.js +63 -22
- package/cli/agents/managed-agent-scanner.js +333 -0
- package/cli/agents/supply-chain-agent.js +1 -1
- package/cli/bin/ship-safe.js +125 -5
- package/cli/commands/audit.js +116 -2
- package/cli/commands/autofix.js +383 -0
- package/cli/commands/env-audit.js +349 -0
- package/cli/commands/red-team.js +2 -2
- package/cli/commands/scan-mcp.js +78 -0
- package/cli/commands/scan-skill.js +248 -5
- package/cli/index.js +5 -0
- package/cli/utils/hermes-tool-registry.js +252 -0
- package/cli/utils/patterns.js +1 -0
- package/cli/utils/plugin-loader.js +276 -0
- package/cli/utils/scan-playbook.js +312 -0
- package/cli/utils/security-memory.js +296 -0
- package/package.json +2 -2
|
@@ -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;
|
|
@@ -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
|
|
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',
|
package/cli/bin/ship-safe.js
CHANGED
|
@@ -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';
|
|
@@ -203,7 +208,7 @@ program
|
|
|
203
208
|
// -----------------------------------------------------------------------------
|
|
204
209
|
program
|
|
205
210
|
.command('audit [path]')
|
|
206
|
-
.description('Full security audit: secrets +
|
|
211
|
+
.description('Full security audit: secrets + 22 agents + deps + score + deep analysis + remediation plan')
|
|
207
212
|
.option('--json', 'Output results as JSON')
|
|
208
213
|
.option('--sarif', 'Output results in SARIF format')
|
|
209
214
|
.option('--csv', 'Output results as CSV')
|
|
@@ -224,6 +229,8 @@ program
|
|
|
224
229
|
.option('--budget <cents>', 'Max spend in cents for deep analysis (default: 50)', parseInt)
|
|
225
230
|
.option('--verify', 'Check if leaked secrets are still active (probes provider APIs)')
|
|
226
231
|
.option('--include-legal', 'Also run the legal risk scan (DMCA, leaked source, IP disputes)')
|
|
232
|
+
.option('--agentic [iterations]', 'Agentic scan→fix→verify loop (default: 3 iterations, target score: 75)', (v) => v ? parseInt(v) : true)
|
|
233
|
+
.option('--agentic-target <score>', 'Target security score for agentic loop (default: 75)', parseInt)
|
|
227
234
|
.option('-v, --verbose', 'Verbose output')
|
|
228
235
|
.action(auditCommand);
|
|
229
236
|
|
|
@@ -244,7 +251,7 @@ program
|
|
|
244
251
|
// -----------------------------------------------------------------------------
|
|
245
252
|
program
|
|
246
253
|
.command('red-team [path]')
|
|
247
|
-
.description('Multi-agent security audit:
|
|
254
|
+
.description('Multi-agent security audit: 22 agents scan for 80+ attack classes')
|
|
248
255
|
.option('--agents <list>', 'Comma-separated list of agents to run')
|
|
249
256
|
.option('--json', 'Output results as JSON')
|
|
250
257
|
.option('--sarif', 'Output results in SARIF format')
|
|
@@ -273,6 +280,8 @@ program
|
|
|
273
280
|
.option('--status', 'Show current watch status and exit')
|
|
274
281
|
.option('--threshold <score>', 'Alert when score drops below threshold', parseInt)
|
|
275
282
|
.option('--debounce <ms>', 'Debounce interval in ms (default: 1500)', parseInt)
|
|
283
|
+
.option('--slack [webhook]', 'Post findings to Slack webhook URL (or set SHIP_SAFE_SLACK_WEBHOOK env var)')
|
|
284
|
+
.option('--pr-comment', 'Post inline findings as GitHub PR review comments (requires gh CLI)')
|
|
276
285
|
.action(watchCommand);
|
|
277
286
|
|
|
278
287
|
// -----------------------------------------------------------------------------
|
|
@@ -457,6 +466,15 @@ How it works:
|
|
|
457
466
|
`)
|
|
458
467
|
.action(hooksCommand);
|
|
459
468
|
|
|
469
|
+
// -----------------------------------------------------------------------------
|
|
470
|
+
// ENV AUDIT COMMAND
|
|
471
|
+
// -----------------------------------------------------------------------------
|
|
472
|
+
program
|
|
473
|
+
.command('env-audit [path]')
|
|
474
|
+
.description('Credential health check: verify .env coverage, cross-reference source, check git history')
|
|
475
|
+
.option('--json', 'Output results as JSON')
|
|
476
|
+
.action(envAuditCommand);
|
|
477
|
+
|
|
460
478
|
// -----------------------------------------------------------------------------
|
|
461
479
|
// LEGAL COMMAND
|
|
462
480
|
// -----------------------------------------------------------------------------
|
|
@@ -483,6 +501,100 @@ program
|
|
|
483
501
|
.description('Diagnose environment: check Node.js, git, API keys, cache, and dependencies')
|
|
484
502
|
.action(doctorCommand);
|
|
485
503
|
|
|
504
|
+
// -----------------------------------------------------------------------------
|
|
505
|
+
// AUTOFIX COMMAND
|
|
506
|
+
// -----------------------------------------------------------------------------
|
|
507
|
+
program
|
|
508
|
+
.command('autofix [path]')
|
|
509
|
+
.description('Apply LLM-generated security fixes from a deep analysis report and open a GitHub PR')
|
|
510
|
+
.option('--report <file>', 'Path to ship-safe JSON report (default: ship-safe-report.json)')
|
|
511
|
+
.option('--severity <level>', 'Minimum severity to fix: critical, high, medium (default: high)')
|
|
512
|
+
.option('--dry-run', 'Preview fixes without applying them or creating a branch')
|
|
513
|
+
.option('--yes', 'Skip confirmation prompt')
|
|
514
|
+
.action((targetPath, options) => autofixCommand({ ...options, path: targetPath }));
|
|
515
|
+
|
|
516
|
+
// -----------------------------------------------------------------------------
|
|
517
|
+
// MEMORY COMMAND
|
|
518
|
+
// -----------------------------------------------------------------------------
|
|
519
|
+
program
|
|
520
|
+
.command('memory [subcommand]')
|
|
521
|
+
.description('Manage security memory: false-positive learning that auto-suppresses known safe findings')
|
|
522
|
+
.addHelpText('after', `
|
|
523
|
+
Subcommands:
|
|
524
|
+
list Show all suppressed findings in memory (default)
|
|
525
|
+
forget <key> Remove a specific entry by key
|
|
526
|
+
clear Wipe all memory entries
|
|
527
|
+
|
|
528
|
+
How it works:
|
|
529
|
+
When --deep analysis confirms a finding is a false positive, it is added to
|
|
530
|
+
.ship-safe/memory.json and suppressed automatically on all future scans.
|
|
531
|
+
`)
|
|
532
|
+
.argument('[args...]')
|
|
533
|
+
.action((subcommand, args, options) => memoryCommand(subcommand, args, options));
|
|
534
|
+
|
|
535
|
+
// -----------------------------------------------------------------------------
|
|
536
|
+
// PLAYBOOK COMMAND
|
|
537
|
+
// -----------------------------------------------------------------------------
|
|
538
|
+
program
|
|
539
|
+
.command('playbook [subcommand]')
|
|
540
|
+
.description('Manage scan playbooks: repo-specific context injected into every LLM analysis')
|
|
541
|
+
.addHelpText('after', `
|
|
542
|
+
Subcommands:
|
|
543
|
+
show Show the current playbook (default)
|
|
544
|
+
add-note "text" Add a custom note to the playbook
|
|
545
|
+
|
|
546
|
+
How it works:
|
|
547
|
+
After 2+ scans, a playbook is auto-generated in .ship-safe/playbook.md with
|
|
548
|
+
your repo's tech stack, auth patterns, and score history. This is injected
|
|
549
|
+
into the LLM system prompt so deep analysis is more accurate for your project.
|
|
550
|
+
`)
|
|
551
|
+
.argument('[args...]')
|
|
552
|
+
.action((subcommand, args, options) => playbookCommand(subcommand, args, options));
|
|
553
|
+
|
|
554
|
+
// -----------------------------------------------------------------------------
|
|
555
|
+
// PLUGINS COMMAND
|
|
556
|
+
// -----------------------------------------------------------------------------
|
|
557
|
+
program
|
|
558
|
+
.command('plugins [action]')
|
|
559
|
+
.description('Manage custom security agent plugins from .ship-safe/agents/')
|
|
560
|
+
.addHelpText('after', `
|
|
561
|
+
Actions:
|
|
562
|
+
list List loaded plugins (default)
|
|
563
|
+
new <name> Scaffold a new plugin in .ship-safe/agents/<name>.js
|
|
564
|
+
|
|
565
|
+
How it works:
|
|
566
|
+
Drop any .js file into .ship-safe/agents/ that exports a default class
|
|
567
|
+
extending BaseAgent with an analyze() method. It will be loaded automatically
|
|
568
|
+
on every audit or watch --deep run.
|
|
569
|
+
`)
|
|
570
|
+
.action((action, options) => {
|
|
571
|
+
const rootPath = path.resolve(process.cwd());
|
|
572
|
+
if (action === 'new') {
|
|
573
|
+
const pluginName = options.args?.[0] || options._name || 'my-rule';
|
|
574
|
+
try {
|
|
575
|
+
const filePath = scaffoldPlugin(rootPath, pluginName);
|
|
576
|
+
console.log(chalk.green(` ✔ Plugin scaffolded: ${filePath}`));
|
|
577
|
+
console.log(chalk.gray(' Edit the file to implement your custom rule, then run ship-safe audit to activate it.'));
|
|
578
|
+
} catch (err) {
|
|
579
|
+
console.error(chalk.red(` Error: ${err.message}`));
|
|
580
|
+
process.exit(1);
|
|
581
|
+
}
|
|
582
|
+
} else {
|
|
583
|
+
// list
|
|
584
|
+
const plugins = listPluginFiles(rootPath);
|
|
585
|
+
if (plugins.length === 0) {
|
|
586
|
+
console.log('\n No custom plugins found in .ship-safe/agents/');
|
|
587
|
+
console.log(chalk.gray(' Create one with: npx ship-safe plugins new my-rule\n'));
|
|
588
|
+
} else {
|
|
589
|
+
console.log(`\n ${chalk.cyan.bold('Custom Plugins')} — ${plugins.length} found\n`);
|
|
590
|
+
for (const p of plugins) {
|
|
591
|
+
console.log(` ${chalk.white(p.name)} ${chalk.gray(`(${(p.size / 1024).toFixed(1)} KB) ${p.path}`)}`);
|
|
592
|
+
}
|
|
593
|
+
console.log();
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
|
|
486
598
|
// -----------------------------------------------------------------------------
|
|
487
599
|
// PARSE AND RUN
|
|
488
600
|
// -----------------------------------------------------------------------------
|
|
@@ -491,10 +603,10 @@ program
|
|
|
491
603
|
if (process.argv.length === 2) {
|
|
492
604
|
console.log(banner);
|
|
493
605
|
console.log(chalk.yellow('\nQuick start:\n'));
|
|
494
|
-
console.log(chalk.cyan.bold('
|
|
495
|
-
console.log(chalk.white(' npx ship-safe audit . ') + chalk.gray('# Full audit: secrets +
|
|
606
|
+
console.log(chalk.cyan.bold(' v8.0 — Ship Safe × Hermes Agent'));
|
|
607
|
+
console.log(chalk.white(' npx ship-safe audit . ') + chalk.gray('# Full audit: secrets + 22 agents + deps + remediation'));
|
|
496
608
|
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('#
|
|
609
|
+
console.log(chalk.white(' npx ship-safe red-team . ') + chalk.gray('# 22-agent red team scan (80+ attack classes)'));
|
|
498
610
|
console.log(chalk.white(' npx ship-safe vibe-check . ') + chalk.gray('# Fun security check with emoji & shareable badge'));
|
|
499
611
|
console.log(chalk.white(' npx ship-safe benchmark . ') + chalk.gray('# Compare score against industry averages'));
|
|
500
612
|
console.log(chalk.white(' npx ship-safe ci . ') + chalk.gray('# CI/CD mode: scan, score, exit code'));
|
|
@@ -517,9 +629,17 @@ if (process.argv.length === 2) {
|
|
|
517
629
|
console.log(chalk.white(' npx ship-safe rotate . ') + chalk.gray('# Revoke exposed keys (provider guides)'));
|
|
518
630
|
console.log(chalk.white(' npx ship-safe deps . ') + chalk.gray('# Audit dependencies for CVEs'));
|
|
519
631
|
console.log(chalk.white(' npx ship-safe score . ') + chalk.gray('# Security health score (0-100)'));
|
|
632
|
+
console.log(chalk.white(' npx ship-safe env-audit . ') + chalk.gray('# Credential health check (after stripe projects env --pull)'));
|
|
520
633
|
console.log(chalk.white(' npx ship-safe hooks install ') + chalk.gray('# Real-time security gate inside Claude Code (PreToolUse/PostToolUse)'));
|
|
521
634
|
console.log(chalk.white(' npx ship-safe guard ') + chalk.gray('# Block git push if secrets found'));
|
|
522
635
|
console.log(chalk.white(' npx ship-safe init ') + chalk.gray('# Add security configs to your project'));
|
|
636
|
+
console.log();
|
|
637
|
+
console.log(chalk.gray(' Intelligence commands:'));
|
|
638
|
+
console.log(chalk.white(' npx ship-safe autofix . ') + chalk.gray('# Apply LLM fixes from --deep report, open PR'));
|
|
639
|
+
console.log(chalk.white(' npx ship-safe memory list ') + chalk.gray('# View / manage false-positive memory'));
|
|
640
|
+
console.log(chalk.white(' npx ship-safe playbook show ') + chalk.gray('# View repo-specific LLM context playbook'));
|
|
641
|
+
console.log(chalk.white(' npx ship-safe plugins list ') + chalk.gray('# Manage custom agent plugins'));
|
|
642
|
+
console.log(chalk.white(' npx ship-safe watch . --deep --slack ') + chalk.gray('# Guardian mode with Slack alerts + PR comments'));
|
|
523
643
|
console.log(chalk.white('\n npx ship-safe --help ') + chalk.gray('# Show all options'));
|
|
524
644
|
console.log();
|
|
525
645
|
process.exit(0);
|