ship-safe 4.2.0 → 5.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 +134 -25
- package/cli/__tests__/agents.test.js +805 -0
- package/cli/agents/agentic-security-agent.js +261 -0
- package/cli/agents/api-fuzzer.js +111 -0
- package/cli/agents/base-agent.js +271 -253
- package/cli/agents/config-auditor.js +71 -0
- package/cli/agents/deep-analyzer.js +333 -0
- package/cli/agents/html-reporter.js +370 -363
- package/cli/agents/index.js +74 -56
- package/cli/agents/injection-tester.js +45 -0
- package/cli/agents/mcp-security-agent.js +358 -0
- package/cli/agents/mobile-scanner.js +6 -0
- package/cli/agents/orchestrator.js +109 -7
- package/cli/agents/pii-compliance-agent.js +301 -0
- package/cli/agents/rag-security-agent.js +204 -0
- package/cli/agents/sbom-generator.js +100 -11
- package/cli/agents/scoring-engine.js +4 -0
- package/cli/agents/supabase-rls-agent.js +154 -0
- package/cli/agents/supply-chain-agent.js +507 -274
- package/cli/agents/verifier-agent.js +292 -0
- package/cli/bin/ship-safe.js +46 -6
- package/cli/commands/audit.js +59 -1
- package/cli/commands/baseline.js +192 -0
- package/cli/commands/ci.js +260 -0
- package/cli/commands/red-team.js +8 -2
- package/cli/index.js +4 -0
- package/cli/utils/autofix-rules.js +74 -0
- package/cli/utils/pdf-generator.js +94 -0
- package/cli/utils/secrets-verifier.js +247 -0
- package/package.json +2 -2
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secrets Verifier
|
|
3
|
+
* =================
|
|
4
|
+
*
|
|
5
|
+
* Checks if leaked secrets are still active by probing provider APIs.
|
|
6
|
+
* Only makes safe, read-only API calls (e.g., account info endpoints).
|
|
7
|
+
*
|
|
8
|
+
* USAGE:
|
|
9
|
+
* const verifier = new SecretsVerifier();
|
|
10
|
+
* const results = await verifier.verify(findings);
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// =============================================================================
|
|
14
|
+
// PROVIDER PROBES
|
|
15
|
+
// =============================================================================
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Each probe defines how to test if a specific type of key is active.
|
|
19
|
+
* All probes use read-only GET endpoints — no side effects.
|
|
20
|
+
*/
|
|
21
|
+
const PROBES = {
|
|
22
|
+
// GitHub tokens
|
|
23
|
+
GITHUB_TOKEN: {
|
|
24
|
+
label: 'GitHub',
|
|
25
|
+
test: async (token) => {
|
|
26
|
+
const res = await safeFetch('https://api.github.com/user', {
|
|
27
|
+
headers: { Authorization: `token ${token}`, 'User-Agent': 'ship-safe-verifier' },
|
|
28
|
+
});
|
|
29
|
+
if (res.status === 200) {
|
|
30
|
+
const data = await res.json();
|
|
31
|
+
return { active: true, info: `Authenticated as: ${data.login}` };
|
|
32
|
+
}
|
|
33
|
+
return { active: false };
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
GITHUB_PAT: { label: 'GitHub PAT', test: (t) => PROBES.GITHUB_TOKEN.test(t) },
|
|
37
|
+
|
|
38
|
+
// OpenAI
|
|
39
|
+
OPENAI_API_KEY: {
|
|
40
|
+
label: 'OpenAI',
|
|
41
|
+
test: async (token) => {
|
|
42
|
+
const res = await safeFetch('https://api.openai.com/v1/models', {
|
|
43
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
44
|
+
});
|
|
45
|
+
return { active: res.status === 200 };
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
// Anthropic
|
|
50
|
+
ANTHROPIC_API_KEY: {
|
|
51
|
+
label: 'Anthropic',
|
|
52
|
+
test: async (token) => {
|
|
53
|
+
const res = await safeFetch('https://api.anthropic.com/v1/messages', {
|
|
54
|
+
method: 'POST',
|
|
55
|
+
headers: {
|
|
56
|
+
'x-api-key': token,
|
|
57
|
+
'anthropic-version': '2023-06-01',
|
|
58
|
+
'content-type': 'application/json',
|
|
59
|
+
},
|
|
60
|
+
// Send minimal invalid request — 400 means key is valid, 401 means invalid
|
|
61
|
+
body: JSON.stringify({ model: 'x', max_tokens: 1, messages: [] }),
|
|
62
|
+
});
|
|
63
|
+
// 400 = valid key, bad request; 401 = invalid key
|
|
64
|
+
return { active: res.status !== 401 && res.status !== 403 };
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
// Stripe
|
|
69
|
+
STRIPE_LIVE_KEY: {
|
|
70
|
+
label: 'Stripe',
|
|
71
|
+
test: async (token) => {
|
|
72
|
+
const res = await safeFetch('https://api.stripe.com/v1/balance', {
|
|
73
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
74
|
+
});
|
|
75
|
+
return { active: res.status === 200 };
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
STRIPE_SECRET_KEY: { label: 'Stripe', test: (t) => PROBES.STRIPE_LIVE_KEY.test(t) },
|
|
79
|
+
|
|
80
|
+
// AWS
|
|
81
|
+
AWS_ACCESS_KEY: {
|
|
82
|
+
label: 'AWS',
|
|
83
|
+
test: async (token) => {
|
|
84
|
+
// AWS keys need both access key and secret — we can only flag that the format is valid
|
|
85
|
+
return { active: null, info: 'Cannot verify AWS keys without secret key pair' };
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
// Slack
|
|
90
|
+
SLACK_TOKEN: {
|
|
91
|
+
label: 'Slack',
|
|
92
|
+
test: async (token) => {
|
|
93
|
+
const res = await safeFetch(`https://slack.com/api/auth.test?token=${encodeURIComponent(token)}`);
|
|
94
|
+
if (res.status === 200) {
|
|
95
|
+
const data = await res.json();
|
|
96
|
+
return { active: data.ok === true, info: data.ok ? `Team: ${data.team}` : undefined };
|
|
97
|
+
}
|
|
98
|
+
return { active: false };
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
SLACK_WEBHOOK: {
|
|
102
|
+
label: 'Slack Webhook',
|
|
103
|
+
test: async () => {
|
|
104
|
+
// Don't probe webhooks — they'd send a message
|
|
105
|
+
return { active: null, info: 'Webhook verification skipped (would send message)' };
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
// Sendgrid
|
|
110
|
+
SENDGRID_API_KEY: {
|
|
111
|
+
label: 'SendGrid',
|
|
112
|
+
test: async (token) => {
|
|
113
|
+
const res = await safeFetch('https://api.sendgrid.com/v3/user/profile', {
|
|
114
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
115
|
+
});
|
|
116
|
+
return { active: res.status === 200 };
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
// Twilio
|
|
121
|
+
TWILIO_AUTH_TOKEN: {
|
|
122
|
+
label: 'Twilio',
|
|
123
|
+
test: async () => {
|
|
124
|
+
return { active: null, info: 'Twilio requires Account SID + Auth Token pair' };
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
// NPM
|
|
129
|
+
NPM_TOKEN: {
|
|
130
|
+
label: 'npm',
|
|
131
|
+
test: async (token) => {
|
|
132
|
+
const res = await safeFetch('https://registry.npmjs.org/-/whoami', {
|
|
133
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
134
|
+
});
|
|
135
|
+
if (res.status === 200) {
|
|
136
|
+
const data = await res.json();
|
|
137
|
+
return { active: true, info: `Authenticated as: ${data.username}` };
|
|
138
|
+
}
|
|
139
|
+
return { active: false };
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// =============================================================================
|
|
145
|
+
// SAFE FETCH
|
|
146
|
+
// =============================================================================
|
|
147
|
+
|
|
148
|
+
async function safeFetch(url, options = {}) {
|
|
149
|
+
const controller = new AbortController();
|
|
150
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
return await fetch(url, { ...options, signal: controller.signal });
|
|
154
|
+
} catch {
|
|
155
|
+
return { status: 0, json: async () => ({}), text: async () => '' };
|
|
156
|
+
} finally {
|
|
157
|
+
clearTimeout(timeout);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// =============================================================================
|
|
162
|
+
// SECRETS VERIFIER
|
|
163
|
+
// =============================================================================
|
|
164
|
+
|
|
165
|
+
export class SecretsVerifier {
|
|
166
|
+
/**
|
|
167
|
+
* Verify an array of secret findings.
|
|
168
|
+
* Only probes findings that have a matching provider probe.
|
|
169
|
+
*
|
|
170
|
+
* @param {object[]} findings — Secret findings with rule and matched fields
|
|
171
|
+
* @returns {Promise<object[]>} — Findings with verifyResult attached
|
|
172
|
+
*/
|
|
173
|
+
async verify(findings) {
|
|
174
|
+
const secretFindings = findings.filter(
|
|
175
|
+
f => f.category === 'secrets' || f.category === 'secret'
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
const results = [];
|
|
179
|
+
|
|
180
|
+
for (const finding of secretFindings) {
|
|
181
|
+
const probe = this._findProbe(finding.rule);
|
|
182
|
+
if (!probe) {
|
|
183
|
+
results.push({ finding, result: { active: null, info: 'No probe available' } });
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Extract the actual secret value from the match
|
|
188
|
+
const secret = this._extractSecret(finding.matched);
|
|
189
|
+
if (!secret) {
|
|
190
|
+
results.push({ finding, result: { active: null, info: 'Could not extract key value' } });
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
const result = await probe.test(secret);
|
|
196
|
+
finding.verifyResult = {
|
|
197
|
+
active: result.active,
|
|
198
|
+
provider: probe.label,
|
|
199
|
+
info: result.info || (result.active ? 'Key is ACTIVE — rotate immediately' : 'Key is inactive or revoked'),
|
|
200
|
+
};
|
|
201
|
+
results.push({ finding, result: finding.verifyResult });
|
|
202
|
+
} catch {
|
|
203
|
+
finding.verifyResult = { active: null, provider: probe.label, info: 'Verification failed' };
|
|
204
|
+
results.push({ finding, result: finding.verifyResult });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return results;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Find the probe for a given rule name.
|
|
213
|
+
*/
|
|
214
|
+
_findProbe(rule) {
|
|
215
|
+
// Direct match
|
|
216
|
+
if (PROBES[rule]) return PROBES[rule];
|
|
217
|
+
|
|
218
|
+
// Partial match (rule may have extra suffixes)
|
|
219
|
+
for (const [key, probe] of Object.entries(PROBES)) {
|
|
220
|
+
if (rule.includes(key) || key.includes(rule)) return probe;
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Extract the secret value from a regex match.
|
|
227
|
+
* Matches typically look like: API_KEY="sk-1234..." or token: 'ghp_...'
|
|
228
|
+
*/
|
|
229
|
+
_extractSecret(matched) {
|
|
230
|
+
if (!matched) return null;
|
|
231
|
+
|
|
232
|
+
// Try to extract quoted value
|
|
233
|
+
const quoted = matched.match(/['"]([^'"]{8,})['"]$/);
|
|
234
|
+
if (quoted) return quoted[1];
|
|
235
|
+
|
|
236
|
+
// Try to extract after = or :
|
|
237
|
+
const assigned = matched.match(/[=:]\s*['"]?([a-zA-Z0-9_\-./+]{8,})['"]?$/);
|
|
238
|
+
if (assigned) return assigned[1];
|
|
239
|
+
|
|
240
|
+
// If the match itself looks like a token, use it
|
|
241
|
+
if (/^[a-zA-Z0-9_\-]{20,}$/.test(matched)) return matched;
|
|
242
|
+
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export default SecretsVerifier;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ship-safe",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "AI-powered multi-agent security platform.
|
|
3
|
+
"version": "5.0.0",
|
|
4
|
+
"description": "AI-powered multi-agent security platform. 16 agents scan 80+ attack classes with LLM-powered deep analysis. Red team your code before attackers do.",
|
|
5
5
|
"main": "cli/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"ship-safe": "cli/bin/ship-safe.js"
|