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.
@@ -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.2.0",
4
- "description": "AI-powered multi-agent security platform. 12 agents scan 50+ attack classes. Red team your code before attackers do.",
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"