ship-safe 7.0.0 → 9.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/deep-analyzer.js +473 -133
- 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/orchestrator.js +13 -3
- package/cli/agents/supply-chain-agent.js +1 -1
- package/cli/bin/ship-safe.js +129 -5
- package/cli/commands/audit.js +149 -3
- package/cli/commands/autofix.js +383 -0
- package/cli/commands/env-audit.js +349 -0
- package/cli/commands/init.js +104 -0
- package/cli/commands/mcp.js +270 -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/commands/watch.js +142 -5
- package/cli/index.js +5 -0
- package/cli/providers/llm-provider.js +50 -2
- 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;
|
|
@@ -234,9 +234,19 @@ export class Orchestrator {
|
|
|
234
234
|
allFindings = await analyzer.analyze(allFindings, { rootPath: absolutePath, recon });
|
|
235
235
|
const stats = analyzer.getStats();
|
|
236
236
|
if (deepSpinner) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
237
|
+
if (stats.multiTier) {
|
|
238
|
+
const tierNote = stats.tier3Count > 0
|
|
239
|
+
? `, ${stats.tier3Count} escalated to Opus`
|
|
240
|
+
: stats.tier2Count > 0 ? `, ${stats.tier2Count} via Sonnet` : '';
|
|
241
|
+
const skipNote = stats.skippedCount > 0 ? `, ${stats.skippedCount} triaged away` : '';
|
|
242
|
+
deepSpinner.succeed(chalk.green(
|
|
243
|
+
`Deep analysis (Haiku→Sonnet→Opus): ${stats.analyzedCount} analyzed${tierNote}${skipNote} (${stats.spentCents}¢)`
|
|
244
|
+
));
|
|
245
|
+
} else {
|
|
246
|
+
deepSpinner.succeed(chalk.green(
|
|
247
|
+
`Deep analysis: ${stats.analyzedCount} findings analyzed (${stats.spentCents}¢)`
|
|
248
|
+
));
|
|
249
|
+
}
|
|
240
250
|
}
|
|
241
251
|
} catch (err) {
|
|
242
252
|
if (deepSpinner) deepSpinner.fail(chalk.yellow(`Deep analysis failed: ${err.message}`));
|
|
@@ -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';
|
|
@@ -120,6 +125,8 @@ program
|
|
|
120
125
|
.option('--headers', 'Only copy security headers config')
|
|
121
126
|
.option('--agents', 'Only add security rules to AI agent instruction files (CLAUDE.md, .cursor/rules/, .windsurfrules, copilot-instructions.md)')
|
|
122
127
|
.option('--openclaw', 'Generate a hardened openclaw.json template')
|
|
128
|
+
.option('--hermes', 'Bootstrap Hermes Agent security config (allowlist, integrity hashes, CI)')
|
|
129
|
+
.option('--from <url>', 'Fetch a pre-built Hermes config bundle from a setup URL (used with --hermes)')
|
|
123
130
|
.action(initCommand);
|
|
124
131
|
|
|
125
132
|
// -----------------------------------------------------------------------------
|
|
@@ -203,7 +210,7 @@ program
|
|
|
203
210
|
// -----------------------------------------------------------------------------
|
|
204
211
|
program
|
|
205
212
|
.command('audit [path]')
|
|
206
|
-
.description('Full security audit: secrets +
|
|
213
|
+
.description('Full security audit: secrets + 22 agents + deps + score + deep analysis + remediation plan')
|
|
207
214
|
.option('--json', 'Output results as JSON')
|
|
208
215
|
.option('--sarif', 'Output results in SARIF format')
|
|
209
216
|
.option('--csv', 'Output results as CSV')
|
|
@@ -224,6 +231,10 @@ program
|
|
|
224
231
|
.option('--budget <cents>', 'Max spend in cents for deep analysis (default: 50)', parseInt)
|
|
225
232
|
.option('--verify', 'Check if leaked secrets are still active (probes provider APIs)')
|
|
226
233
|
.option('--include-legal', 'Also run the legal risk scan (DMCA, leaked source, IP disputes)')
|
|
234
|
+
.option('--agentic [iterations]', 'Agentic scan→fix→verify loop (default: 3 iterations, target score: 75)', (v) => v ? parseInt(v) : true)
|
|
235
|
+
.option('--agentic-target <score>', 'Target security score for agentic loop (default: 75)', parseInt)
|
|
236
|
+
.option('--hermes-only', 'Run only Hermes-relevant agents (llm + supply-chain categories) for fast CI')
|
|
237
|
+
.option('--fail-below <threshold>', 'Exit 1 if score is below threshold (number or "baseline")')
|
|
227
238
|
.option('-v, --verbose', 'Verbose output')
|
|
228
239
|
.action(auditCommand);
|
|
229
240
|
|
|
@@ -244,7 +255,7 @@ program
|
|
|
244
255
|
// -----------------------------------------------------------------------------
|
|
245
256
|
program
|
|
246
257
|
.command('red-team [path]')
|
|
247
|
-
.description('Multi-agent security audit:
|
|
258
|
+
.description('Multi-agent security audit: 22 agents scan for 80+ attack classes')
|
|
248
259
|
.option('--agents <list>', 'Comma-separated list of agents to run')
|
|
249
260
|
.option('--json', 'Output results as JSON')
|
|
250
261
|
.option('--sarif', 'Output results in SARIF format')
|
|
@@ -273,6 +284,8 @@ program
|
|
|
273
284
|
.option('--status', 'Show current watch status and exit')
|
|
274
285
|
.option('--threshold <score>', 'Alert when score drops below threshold', parseInt)
|
|
275
286
|
.option('--debounce <ms>', 'Debounce interval in ms (default: 1500)', parseInt)
|
|
287
|
+
.option('--slack [webhook]', 'Post findings to Slack webhook URL (or set SHIP_SAFE_SLACK_WEBHOOK env var)')
|
|
288
|
+
.option('--pr-comment', 'Post inline findings as GitHub PR review comments (requires gh CLI)')
|
|
276
289
|
.action(watchCommand);
|
|
277
290
|
|
|
278
291
|
// -----------------------------------------------------------------------------
|
|
@@ -457,6 +470,15 @@ How it works:
|
|
|
457
470
|
`)
|
|
458
471
|
.action(hooksCommand);
|
|
459
472
|
|
|
473
|
+
// -----------------------------------------------------------------------------
|
|
474
|
+
// ENV AUDIT COMMAND
|
|
475
|
+
// -----------------------------------------------------------------------------
|
|
476
|
+
program
|
|
477
|
+
.command('env-audit [path]')
|
|
478
|
+
.description('Credential health check: verify .env coverage, cross-reference source, check git history')
|
|
479
|
+
.option('--json', 'Output results as JSON')
|
|
480
|
+
.action(envAuditCommand);
|
|
481
|
+
|
|
460
482
|
// -----------------------------------------------------------------------------
|
|
461
483
|
// LEGAL COMMAND
|
|
462
484
|
// -----------------------------------------------------------------------------
|
|
@@ -483,6 +505,100 @@ program
|
|
|
483
505
|
.description('Diagnose environment: check Node.js, git, API keys, cache, and dependencies')
|
|
484
506
|
.action(doctorCommand);
|
|
485
507
|
|
|
508
|
+
// -----------------------------------------------------------------------------
|
|
509
|
+
// AUTOFIX COMMAND
|
|
510
|
+
// -----------------------------------------------------------------------------
|
|
511
|
+
program
|
|
512
|
+
.command('autofix [path]')
|
|
513
|
+
.description('Apply LLM-generated security fixes from a deep analysis report and open a GitHub PR')
|
|
514
|
+
.option('--report <file>', 'Path to ship-safe JSON report (default: ship-safe-report.json)')
|
|
515
|
+
.option('--severity <level>', 'Minimum severity to fix: critical, high, medium (default: high)')
|
|
516
|
+
.option('--dry-run', 'Preview fixes without applying them or creating a branch')
|
|
517
|
+
.option('--yes', 'Skip confirmation prompt')
|
|
518
|
+
.action((targetPath, options) => autofixCommand({ ...options, path: targetPath }));
|
|
519
|
+
|
|
520
|
+
// -----------------------------------------------------------------------------
|
|
521
|
+
// MEMORY COMMAND
|
|
522
|
+
// -----------------------------------------------------------------------------
|
|
523
|
+
program
|
|
524
|
+
.command('memory [subcommand]')
|
|
525
|
+
.description('Manage security memory: false-positive learning that auto-suppresses known safe findings')
|
|
526
|
+
.addHelpText('after', `
|
|
527
|
+
Subcommands:
|
|
528
|
+
list Show all suppressed findings in memory (default)
|
|
529
|
+
forget <key> Remove a specific entry by key
|
|
530
|
+
clear Wipe all memory entries
|
|
531
|
+
|
|
532
|
+
How it works:
|
|
533
|
+
When --deep analysis confirms a finding is a false positive, it is added to
|
|
534
|
+
.ship-safe/memory.json and suppressed automatically on all future scans.
|
|
535
|
+
`)
|
|
536
|
+
.argument('[args...]')
|
|
537
|
+
.action((subcommand, args, options) => memoryCommand(subcommand, args, options));
|
|
538
|
+
|
|
539
|
+
// -----------------------------------------------------------------------------
|
|
540
|
+
// PLAYBOOK COMMAND
|
|
541
|
+
// -----------------------------------------------------------------------------
|
|
542
|
+
program
|
|
543
|
+
.command('playbook [subcommand]')
|
|
544
|
+
.description('Manage scan playbooks: repo-specific context injected into every LLM analysis')
|
|
545
|
+
.addHelpText('after', `
|
|
546
|
+
Subcommands:
|
|
547
|
+
show Show the current playbook (default)
|
|
548
|
+
add-note "text" Add a custom note to the playbook
|
|
549
|
+
|
|
550
|
+
How it works:
|
|
551
|
+
After 2+ scans, a playbook is auto-generated in .ship-safe/playbook.md with
|
|
552
|
+
your repo's tech stack, auth patterns, and score history. This is injected
|
|
553
|
+
into the LLM system prompt so deep analysis is more accurate for your project.
|
|
554
|
+
`)
|
|
555
|
+
.argument('[args...]')
|
|
556
|
+
.action((subcommand, args, options) => playbookCommand(subcommand, args, options));
|
|
557
|
+
|
|
558
|
+
// -----------------------------------------------------------------------------
|
|
559
|
+
// PLUGINS COMMAND
|
|
560
|
+
// -----------------------------------------------------------------------------
|
|
561
|
+
program
|
|
562
|
+
.command('plugins [action]')
|
|
563
|
+
.description('Manage custom security agent plugins from .ship-safe/agents/')
|
|
564
|
+
.addHelpText('after', `
|
|
565
|
+
Actions:
|
|
566
|
+
list List loaded plugins (default)
|
|
567
|
+
new <name> Scaffold a new plugin in .ship-safe/agents/<name>.js
|
|
568
|
+
|
|
569
|
+
How it works:
|
|
570
|
+
Drop any .js file into .ship-safe/agents/ that exports a default class
|
|
571
|
+
extending BaseAgent with an analyze() method. It will be loaded automatically
|
|
572
|
+
on every audit or watch --deep run.
|
|
573
|
+
`)
|
|
574
|
+
.action((action, options) => {
|
|
575
|
+
const rootPath = path.resolve(process.cwd());
|
|
576
|
+
if (action === 'new') {
|
|
577
|
+
const pluginName = options.args?.[0] || options._name || 'my-rule';
|
|
578
|
+
try {
|
|
579
|
+
const filePath = scaffoldPlugin(rootPath, pluginName);
|
|
580
|
+
console.log(chalk.green(` ✔ Plugin scaffolded: ${filePath}`));
|
|
581
|
+
console.log(chalk.gray(' Edit the file to implement your custom rule, then run ship-safe audit to activate it.'));
|
|
582
|
+
} catch (err) {
|
|
583
|
+
console.error(chalk.red(` Error: ${err.message}`));
|
|
584
|
+
process.exit(1);
|
|
585
|
+
}
|
|
586
|
+
} else {
|
|
587
|
+
// list
|
|
588
|
+
const plugins = listPluginFiles(rootPath);
|
|
589
|
+
if (plugins.length === 0) {
|
|
590
|
+
console.log('\n No custom plugins found in .ship-safe/agents/');
|
|
591
|
+
console.log(chalk.gray(' Create one with: npx ship-safe plugins new my-rule\n'));
|
|
592
|
+
} else {
|
|
593
|
+
console.log(`\n ${chalk.cyan.bold('Custom Plugins')} — ${plugins.length} found\n`);
|
|
594
|
+
for (const p of plugins) {
|
|
595
|
+
console.log(` ${chalk.white(p.name)} ${chalk.gray(`(${(p.size / 1024).toFixed(1)} KB) ${p.path}`)}`);
|
|
596
|
+
}
|
|
597
|
+
console.log();
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
|
|
486
602
|
// -----------------------------------------------------------------------------
|
|
487
603
|
// PARSE AND RUN
|
|
488
604
|
// -----------------------------------------------------------------------------
|
|
@@ -491,10 +607,10 @@ program
|
|
|
491
607
|
if (process.argv.length === 2) {
|
|
492
608
|
console.log(banner);
|
|
493
609
|
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 +
|
|
610
|
+
console.log(chalk.cyan.bold(' v8.0 — Ship Safe × Hermes Agent'));
|
|
611
|
+
console.log(chalk.white(' npx ship-safe audit . ') + chalk.gray('# Full audit: secrets + 22 agents + deps + remediation'));
|
|
496
612
|
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('#
|
|
613
|
+
console.log(chalk.white(' npx ship-safe red-team . ') + chalk.gray('# 22-agent red team scan (80+ attack classes)'));
|
|
498
614
|
console.log(chalk.white(' npx ship-safe vibe-check . ') + chalk.gray('# Fun security check with emoji & shareable badge'));
|
|
499
615
|
console.log(chalk.white(' npx ship-safe benchmark . ') + chalk.gray('# Compare score against industry averages'));
|
|
500
616
|
console.log(chalk.white(' npx ship-safe ci . ') + chalk.gray('# CI/CD mode: scan, score, exit code'));
|
|
@@ -517,9 +633,17 @@ if (process.argv.length === 2) {
|
|
|
517
633
|
console.log(chalk.white(' npx ship-safe rotate . ') + chalk.gray('# Revoke exposed keys (provider guides)'));
|
|
518
634
|
console.log(chalk.white(' npx ship-safe deps . ') + chalk.gray('# Audit dependencies for CVEs'));
|
|
519
635
|
console.log(chalk.white(' npx ship-safe score . ') + chalk.gray('# Security health score (0-100)'));
|
|
636
|
+
console.log(chalk.white(' npx ship-safe env-audit . ') + chalk.gray('# Credential health check (after stripe projects env --pull)'));
|
|
520
637
|
console.log(chalk.white(' npx ship-safe hooks install ') + chalk.gray('# Real-time security gate inside Claude Code (PreToolUse/PostToolUse)'));
|
|
521
638
|
console.log(chalk.white(' npx ship-safe guard ') + chalk.gray('# Block git push if secrets found'));
|
|
522
639
|
console.log(chalk.white(' npx ship-safe init ') + chalk.gray('# Add security configs to your project'));
|
|
640
|
+
console.log();
|
|
641
|
+
console.log(chalk.gray(' Intelligence commands:'));
|
|
642
|
+
console.log(chalk.white(' npx ship-safe autofix . ') + chalk.gray('# Apply LLM fixes from --deep report, open PR'));
|
|
643
|
+
console.log(chalk.white(' npx ship-safe memory list ') + chalk.gray('# View / manage false-positive memory'));
|
|
644
|
+
console.log(chalk.white(' npx ship-safe playbook show ') + chalk.gray('# View repo-specific LLM context playbook'));
|
|
645
|
+
console.log(chalk.white(' npx ship-safe plugins list ') + chalk.gray('# Manage custom agent plugins'));
|
|
646
|
+
console.log(chalk.white(' npx ship-safe watch . --deep --slack ') + chalk.gray('# Guardian mode with Slack alerts + PR comments'));
|
|
523
647
|
console.log(chalk.white('\n npx ship-safe --help ') + chalk.gray('# Show all options'));
|
|
524
648
|
console.log();
|
|
525
649
|
process.exit(0);
|