ship-safe 4.0.0 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -0
- package/cli/__tests__/agents.test.js +496 -0
- package/cli/agents/api-fuzzer.js +234 -224
- package/cli/agents/auth-bypass-agent.js +348 -326
- package/cli/agents/base-agent.js +14 -1
- package/cli/agents/cicd-scanner.js +201 -200
- package/cli/agents/config-auditor.js +458 -413
- package/cli/agents/git-history-scanner.js +170 -167
- package/cli/agents/injection-tester.js +455 -401
- package/cli/agents/llm-redteam.js +251 -251
- package/cli/agents/mobile-scanner.js +225 -225
- package/cli/agents/orchestrator.js +220 -152
- package/cli/agents/scoring-engine.js +225 -207
- package/cli/bin/ship-safe.js +16 -0
- package/cli/commands/audit.js +849 -565
- package/cli/commands/doctor.js +149 -0
- package/cli/commands/remediate.js +7 -3
- package/cli/commands/scan.js +79 -8
- package/cli/index.js +56 -50
- package/cli/providers/llm-provider.js +287 -288
- package/cli/utils/cache-manager.js +311 -0
- package/cli/utils/patterns.js +95 -0
- package/package.json +2 -2
|
@@ -1,152 +1,220 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Agent Orchestrator
|
|
3
|
-
* ==================
|
|
4
|
-
*
|
|
5
|
-
* Coordinates all security agents, deduplicates findings,
|
|
6
|
-
* and produces a unified report.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
*
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Agent Orchestrator
|
|
3
|
+
* ==================
|
|
4
|
+
*
|
|
5
|
+
* Coordinates all security agents, deduplicates findings,
|
|
6
|
+
* and produces a unified report.
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Per-agent timeouts (default 30s, configurable via --timeout)
|
|
10
|
+
* - Parallel execution with configurable concurrency (default 6)
|
|
11
|
+
*
|
|
12
|
+
* USAGE:
|
|
13
|
+
* const orchestrator = new Orchestrator();
|
|
14
|
+
* orchestrator.register(new InjectionTester());
|
|
15
|
+
* const results = await orchestrator.runAll(rootPath, options);
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import path from 'path';
|
|
19
|
+
import ora from 'ora';
|
|
20
|
+
import chalk from 'chalk';
|
|
21
|
+
import { ReconAgent } from './recon-agent.js';
|
|
22
|
+
|
|
23
|
+
// =============================================================================
|
|
24
|
+
// CONSTANTS
|
|
25
|
+
// =============================================================================
|
|
26
|
+
|
|
27
|
+
const DEFAULT_TIMEOUT = 30_000; // 30s per agent
|
|
28
|
+
const DEFAULT_CONCURRENCY = 6;
|
|
29
|
+
|
|
30
|
+
// =============================================================================
|
|
31
|
+
// ORCHESTRATOR
|
|
32
|
+
// =============================================================================
|
|
33
|
+
|
|
34
|
+
export class Orchestrator {
|
|
35
|
+
constructor() {
|
|
36
|
+
/** @type {import('./base-agent.js').BaseAgent[]} */
|
|
37
|
+
this.agents = [];
|
|
38
|
+
this.reconAgent = new ReconAgent();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Register an agent for execution.
|
|
43
|
+
*/
|
|
44
|
+
register(agent) {
|
|
45
|
+
this.agents.push(agent);
|
|
46
|
+
return this;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Register multiple agents at once.
|
|
51
|
+
*/
|
|
52
|
+
registerAll(agents) {
|
|
53
|
+
for (const agent of agents) {
|
|
54
|
+
this.register(agent);
|
|
55
|
+
}
|
|
56
|
+
return this;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Run a single agent with a timeout.
|
|
61
|
+
*/
|
|
62
|
+
async runAgent(agent, context, timeout) {
|
|
63
|
+
return Promise.race([
|
|
64
|
+
agent.analyze(context),
|
|
65
|
+
new Promise((_, reject) => {
|
|
66
|
+
setTimeout(() => reject(new Error(`timed out after ${timeout / 1000}s`)), timeout);
|
|
67
|
+
}),
|
|
68
|
+
]);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Run all registered agents against the codebase.
|
|
73
|
+
*
|
|
74
|
+
* @param {string} rootPath — Absolute path to the project root
|
|
75
|
+
* @param {object} options — { verbose, agents[], categories[], timeout, concurrency }
|
|
76
|
+
* @returns {Promise<object>} — { recon, findings[], agentResults[] }
|
|
77
|
+
*/
|
|
78
|
+
async runAll(rootPath, options = {}) {
|
|
79
|
+
const absolutePath = path.resolve(rootPath);
|
|
80
|
+
const timeout = options.timeout || DEFAULT_TIMEOUT;
|
|
81
|
+
const concurrency = options.concurrency || DEFAULT_CONCURRENCY;
|
|
82
|
+
|
|
83
|
+
// ── 1. Recon — map the attack surface ─────────────────────────────────────
|
|
84
|
+
const quiet = options.quiet || false;
|
|
85
|
+
const reconSpinner = quiet ? null : ora({ text: 'Mapping attack surface...', color: 'cyan' }).start();
|
|
86
|
+
const recon = await this.reconAgent.analyze({ rootPath: absolutePath, options });
|
|
87
|
+
if (reconSpinner) reconSpinner.succeed(chalk.green('Attack surface mapped'));
|
|
88
|
+
|
|
89
|
+
// ── 2. Discover files once (shared across agents) ─────────────────────────
|
|
90
|
+
const files = await this.reconAgent.discoverFiles(absolutePath);
|
|
91
|
+
|
|
92
|
+
// ── 3. Filter agents if specific ones requested ───────────────────────────
|
|
93
|
+
let agentsToRun = this.agents;
|
|
94
|
+
if (options.agents && options.agents.length > 0) {
|
|
95
|
+
const requested = options.agents.map(a => a.toLowerCase());
|
|
96
|
+
agentsToRun = this.agents.filter(a => {
|
|
97
|
+
const name = a.name.toLowerCase();
|
|
98
|
+
const cat = a.category.toLowerCase();
|
|
99
|
+
return requested.some(r => name === r || name.includes(r) || cat === r);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
if (options.categories && options.categories.length > 0) {
|
|
103
|
+
const requested = new Set(options.categories.map(c => c.toLowerCase()));
|
|
104
|
+
agentsToRun = agentsToRun.filter(a => requested.has(a.category.toLowerCase()));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── 4. Build shared context ─────────────────────────────────────────────
|
|
108
|
+
const context = { rootPath: absolutePath, files, recon, options };
|
|
109
|
+
if (options.changedFiles) {
|
|
110
|
+
context.changedFiles = options.changedFiles;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── 5. Run agents in parallel (chunked by concurrency) ──────────────────
|
|
114
|
+
const agentResults = [];
|
|
115
|
+
let allFindings = [];
|
|
116
|
+
|
|
117
|
+
const spinner = quiet ? null : ora({
|
|
118
|
+
text: `Running ${agentsToRun.length} agents in parallel...`,
|
|
119
|
+
color: 'cyan'
|
|
120
|
+
}).start();
|
|
121
|
+
|
|
122
|
+
for (let i = 0; i < agentsToRun.length; i += concurrency) {
|
|
123
|
+
const chunk = agentsToRun.slice(i, i + concurrency);
|
|
124
|
+
const settled = await Promise.allSettled(
|
|
125
|
+
chunk.map(agent => this.runAgent(agent, context, timeout))
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
for (let j = 0; j < chunk.length; j++) {
|
|
129
|
+
const agent = chunk[j];
|
|
130
|
+
const result = settled[j];
|
|
131
|
+
|
|
132
|
+
if (result.status === 'fulfilled') {
|
|
133
|
+
const findings = result.value;
|
|
134
|
+
agentResults.push({
|
|
135
|
+
agent: agent.name,
|
|
136
|
+
category: agent.category,
|
|
137
|
+
findingCount: findings.length,
|
|
138
|
+
success: true,
|
|
139
|
+
});
|
|
140
|
+
allFindings = allFindings.concat(findings);
|
|
141
|
+
} else {
|
|
142
|
+
agentResults.push({
|
|
143
|
+
agent: agent.name,
|
|
144
|
+
category: agent.category,
|
|
145
|
+
findingCount: 0,
|
|
146
|
+
success: false,
|
|
147
|
+
error: result.reason.message,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Show results summary
|
|
154
|
+
if (spinner) {
|
|
155
|
+
const succeeded = agentResults.filter(a => a.success).length;
|
|
156
|
+
const failed = agentResults.filter(a => !a.success).length;
|
|
157
|
+
const totalFindings = allFindings.length;
|
|
158
|
+
|
|
159
|
+
if (failed > 0) {
|
|
160
|
+
spinner.warn(chalk.yellow(
|
|
161
|
+
`${succeeded}/${agentsToRun.length} agents completed, ${failed} failed, ${totalFindings} finding(s)`
|
|
162
|
+
));
|
|
163
|
+
} else {
|
|
164
|
+
spinner.succeed(
|
|
165
|
+
totalFindings === 0
|
|
166
|
+
? chalk.green(`${succeeded} agents: clean`)
|
|
167
|
+
: chalk.yellow(`${succeeded} agents: ${totalFindings} finding(s)`)
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Show per-agent results when not in quiet mode
|
|
173
|
+
if (!quiet) {
|
|
174
|
+
for (const r of agentResults) {
|
|
175
|
+
if (r.success) {
|
|
176
|
+
const icon = r.findingCount === 0 ? chalk.green(' ✔') : chalk.yellow(' ⚠');
|
|
177
|
+
const msg = r.findingCount === 0
|
|
178
|
+
? chalk.green(`${r.agent}: clean`)
|
|
179
|
+
: chalk.yellow(`${r.agent}: ${r.findingCount} finding(s)`);
|
|
180
|
+
console.log(`${icon} ${msg}`);
|
|
181
|
+
} else {
|
|
182
|
+
console.log(chalk.red(` ✗ ${r.agent}: ${r.error}`));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ── 6. Deduplicate ────────────────────────────────────────────────────────
|
|
188
|
+
allFindings = this.deduplicate(allFindings);
|
|
189
|
+
|
|
190
|
+
// ── 7. Sort by severity ───────────────────────────────────────────────────
|
|
191
|
+
const sevOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
192
|
+
allFindings.sort((a, b) =>
|
|
193
|
+
(sevOrder[a.severity] ?? 4) - (sevOrder[b.severity] ?? 4)
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
return { recon, findings: allFindings, agentResults };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Run only agents matching a specific category.
|
|
201
|
+
*/
|
|
202
|
+
async runCategory(category, rootPath, options = {}) {
|
|
203
|
+
return this.runAll(rootPath, { ...options, categories: [category] });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Remove duplicate findings (same file + line + rule).
|
|
208
|
+
*/
|
|
209
|
+
deduplicate(findings) {
|
|
210
|
+
const seen = new Set();
|
|
211
|
+
return findings.filter(f => {
|
|
212
|
+
const key = `${f.file}:${f.line}:${f.rule}`;
|
|
213
|
+
if (seen.has(key)) return false;
|
|
214
|
+
seen.add(key);
|
|
215
|
+
return true;
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export default Orchestrator;
|