shipready 1.5.1 → 1.5.2
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 +2 -2
- package/dist/checks/secrets.js +90 -32
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
[](https://github.com/formalness/shipready/actions/workflows/ci.yml)
|
|
6
6
|
[](https://www.npmjs.com/package/shipready)
|
|
7
|
-
[](https://github.com/formalness/shipready/tree/main/tests)
|
|
8
7
|
[](./LICENSE)
|
|
9
8
|
[](https://nodejs.org)
|
|
10
9
|
|
|
@@ -215,7 +214,8 @@ The scanner combines provider-specific patterns with **Shannon entropy analysis*
|
|
|
215
214
|
| Stripe (test) | `sk_test_`, `pk_test_` |
|
|
216
215
|
| JWT | `eyJ...` with valid structure |
|
|
217
216
|
| Generic credential | `API_KEY=`, `SECRET=`, `PASSWORD=` assignments — only when the value has high entropy |
|
|
218
|
-
|
|
|
217
|
+
| Generic-pattern findings (opaque assignments, URLs, JWTs) in test/fixture/mock files | automatically downgraded |
|
|
218
|
+
| Provider-prefixed keys (`AKIA...`, `ghp_...`, `sk-ant-...`) in test files | kept at full severity |
|
|
219
219
|
|
|
220
220
|
False-positive protection:
|
|
221
221
|
|
package/dist/checks/secrets.js
CHANGED
|
@@ -76,11 +76,13 @@ const PATTERNS = [
|
|
|
76
76
|
},
|
|
77
77
|
{
|
|
78
78
|
kind: "Twilio credential",
|
|
79
|
+
generic: true,
|
|
79
80
|
re: /\b(?:AC|SK)[a-f0-9]{32}\b/,
|
|
80
81
|
confidence: "high",
|
|
81
82
|
},
|
|
82
83
|
{
|
|
83
84
|
kind: "Telegram bot token",
|
|
85
|
+
generic: true,
|
|
84
86
|
re: /\b\d{8,10}:[A-Za-z0-9_-]{35}\b/,
|
|
85
87
|
confidence: "high",
|
|
86
88
|
},
|
|
@@ -107,11 +109,13 @@ const PATTERNS = [
|
|
|
107
109
|
},
|
|
108
110
|
{
|
|
109
111
|
kind: "Mailchimp key",
|
|
112
|
+
generic: true,
|
|
110
113
|
re: /\b[a-f0-9]{32}-us\d{1,2}\b/,
|
|
111
114
|
confidence: "high",
|
|
112
115
|
},
|
|
113
116
|
{
|
|
114
117
|
kind: "Mailgun key",
|
|
118
|
+
generic: true,
|
|
115
119
|
re: /\bkey-[a-f0-9]{32}\b/,
|
|
116
120
|
confidence: "high",
|
|
117
121
|
},
|
|
@@ -189,6 +193,7 @@ const PATTERNS = [
|
|
|
189
193
|
},
|
|
190
194
|
{
|
|
191
195
|
kind: "Discord bot token",
|
|
196
|
+
generic: true,
|
|
192
197
|
re: /\b[MNO][A-Za-z\d_-]{23,25}\.[A-Za-z\d_-]{6}\.[A-Za-z\d_-]{27,}\b/,
|
|
193
198
|
confidence: "medium",
|
|
194
199
|
minEntropy: 4.0,
|
|
@@ -198,6 +203,7 @@ const PATTERNS = [
|
|
|
198
203
|
// documentation hosts like db.example.com don't suppress real leaks.
|
|
199
204
|
// The host (group 2) downgrades localhost URLs to medium confidence.
|
|
200
205
|
kind: "Database URL with password",
|
|
206
|
+
generic: true,
|
|
201
207
|
re: /\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|rediss|amqp):\/\/[^:\s"'@/]+:([^@\s"']+)@([^\s"'/:?]+)/,
|
|
202
208
|
confidence: "high",
|
|
203
209
|
group: 1,
|
|
@@ -205,6 +211,7 @@ const PATTERNS = [
|
|
|
205
211
|
},
|
|
206
212
|
{
|
|
207
213
|
kind: "Basic auth in URL",
|
|
214
|
+
generic: true,
|
|
208
215
|
re: /https?:\/\/[^:\s"'/@]{3,}:([^@\s"']{8,})@([^\s"'/:?]+)/,
|
|
209
216
|
confidence: "medium",
|
|
210
217
|
group: 1,
|
|
@@ -213,12 +220,14 @@ const PATTERNS = [
|
|
|
213
220
|
},
|
|
214
221
|
{
|
|
215
222
|
kind: "JWT",
|
|
223
|
+
generic: true,
|
|
216
224
|
re: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/,
|
|
217
225
|
confidence: "medium",
|
|
218
226
|
minEntropy: 4.0,
|
|
219
227
|
},
|
|
220
228
|
{
|
|
221
229
|
kind: "Authorization header",
|
|
230
|
+
generic: true,
|
|
222
231
|
re: /\b(?:authorization|x-api-key)["']?\s*[:=]\s*["']?(?:Bearer|Basic|token)\s+([A-Za-z0-9+/_.=-]{20,})\b/i,
|
|
223
232
|
confidence: "medium",
|
|
224
233
|
group: 1,
|
|
@@ -227,6 +236,7 @@ const PATTERNS = [
|
|
|
227
236
|
{
|
|
228
237
|
// Credential-style assignment with a long opaque quoted value
|
|
229
238
|
kind: "Hardcoded credential",
|
|
239
|
+
generic: true,
|
|
230
240
|
re: /\b[A-Z0-9_]*(?:API_?KEY|SECRET(?:_KEY)?|ACCESS_TOKEN|AUTH_TOKEN|PASSWORD|PASSWD|CREDENTIALS?)[A-Z0-9_]*\s*[=:]\s*["']([A-Za-z0-9+/_.=-]{16,})["']/i,
|
|
231
241
|
confidence: "medium",
|
|
232
242
|
group: 1,
|
|
@@ -238,6 +248,7 @@ const PATTERNS = [
|
|
|
238
248
|
// benchmarking against gitleaks. Anchored to line start so ordinary
|
|
239
249
|
// code assignments (always quoted) don't double-match.
|
|
240
250
|
kind: "Hardcoded credential",
|
|
251
|
+
generic: true,
|
|
241
252
|
re: /^[A-Z0-9_]*(?:API_?KEY|SECRET(?:_KEY)?|ACCESS_TOKEN|AUTH_TOKEN|PASSWORD|PASSWD|CREDENTIALS?)[A-Z0-9_]*=([A-Za-z0-9+/_.=-]{16,})\s*$/,
|
|
242
253
|
confidence: "medium",
|
|
243
254
|
group: 1,
|
|
@@ -284,6 +295,15 @@ export function maskSecret(secret) {
|
|
|
284
295
|
const suffix = secret.slice(-4);
|
|
285
296
|
return `${prefix}...${suffix}`;
|
|
286
297
|
}
|
|
298
|
+
/**
|
|
299
|
+
* Global-flag copies of each pattern for matchAll. Compiled once - the
|
|
300
|
+
* originals stay non-global so .test()/.match() callers keep working.
|
|
301
|
+
*/
|
|
302
|
+
const GLOBAL_PATTERN_RES = PATTERNS.map((p) => new RegExp(p.re.source, p.re.flags.includes("g") ? p.re.flags : `${p.re.flags}g`));
|
|
303
|
+
/** Credential-keyword assignment with no value on the same line. */
|
|
304
|
+
const DANGLING_ASSIGNMENT_RE = /\b[A-Z0-9_]*(?:API_?KEY|SECRET(?:_KEY)?|ACCESS_TOKEN|AUTH_TOKEN|PASSWORD|PASSWD|CREDENTIALS?)[A-Z0-9_]*\s*[=:]\s*$/i;
|
|
305
|
+
/** Quoted opaque value at the start of a continuation line. */
|
|
306
|
+
const CONTINUATION_VALUE_RE = /^\s*["']([A-Za-z0-9+/_.=-]{16,})["']/;
|
|
287
307
|
/** Scans a single file's content for potential secrets. */
|
|
288
308
|
export function scanContentForSecrets(content, file, allowlist = []) {
|
|
289
309
|
const found = [];
|
|
@@ -297,43 +317,81 @@ export function scanContentForSecrets(content, file, allowlist = []) {
|
|
|
297
317
|
// Documentation/example lines are not real leaks.
|
|
298
318
|
if (PLACEHOLDER_LINE_RE.test(line))
|
|
299
319
|
continue;
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
320
|
+
// Character spans already claimed by a more specific pattern. Patterns
|
|
321
|
+
// are ordered most specific first, so the first pattern to claim a span
|
|
322
|
+
// wins - but distinct secrets elsewhere on the same line (e.g. two keys
|
|
323
|
+
// in one config object) are still reported.
|
|
324
|
+
const claimed = [];
|
|
325
|
+
const overlapsClaimed = (start, end) => claimed.some(([s, e]) => start < e && end > s);
|
|
326
|
+
for (let pi = 0; pi < PATTERNS.length; pi++) {
|
|
327
|
+
const pattern = PATTERNS[pi];
|
|
328
|
+
for (const match of line.matchAll(GLOBAL_PATTERN_RES[pi])) {
|
|
329
|
+
const start = match.index ?? 0;
|
|
330
|
+
const end = start + match[0].length;
|
|
331
|
+
if (overlapsClaimed(start, end))
|
|
307
332
|
continue;
|
|
308
|
-
|
|
309
|
-
|
|
333
|
+
const value = match[pattern.group ?? 0];
|
|
334
|
+
if (!pattern.skipValueChecks) {
|
|
335
|
+
if (isPlaceholderValue(value))
|
|
336
|
+
continue;
|
|
337
|
+
if (pattern.minEntropy !== undefined &&
|
|
338
|
+
shannonEntropy(value) < pattern.minEntropy) {
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
// User-configured false positives (substring match on the value or line)
|
|
343
|
+
if (allowlist.some((a) => match[0].includes(a) || line.includes(a))) {
|
|
310
344
|
continue;
|
|
311
345
|
}
|
|
346
|
+
let effective = pattern.confidence;
|
|
347
|
+
if (pattern.hostGroup !== undefined) {
|
|
348
|
+
// Scaffolding defaults like root:password@localhost are not leaks.
|
|
349
|
+
if (COMMON_DEV_PASSWORD_RE.test(value))
|
|
350
|
+
continue;
|
|
351
|
+
const host = match[pattern.hostGroup] ?? "";
|
|
352
|
+
if (LOCAL_HOST_RE.test(host))
|
|
353
|
+
effective = "medium";
|
|
354
|
+
}
|
|
355
|
+
// Test paths only downgrade shape-based patterns. A provider-prefixed
|
|
356
|
+
// key that survived the entropy/placeholder gates looks real, and a
|
|
357
|
+
// real key in a test fixture is just as leaked as one in src/.
|
|
358
|
+
const confidence = isTestPath && pattern.generic ? "medium" : effective;
|
|
359
|
+
claimed.push([start, end]);
|
|
360
|
+
found.push({
|
|
361
|
+
kind: pattern.kind,
|
|
362
|
+
file,
|
|
363
|
+
line: i + 1,
|
|
364
|
+
masked: maskSecret(value),
|
|
365
|
+
confidence,
|
|
366
|
+
// Raw value stays in memory only; used by --verify, never printed.
|
|
367
|
+
raw: value,
|
|
368
|
+
});
|
|
312
369
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
370
|
+
}
|
|
371
|
+
// Multiline credential assignment: keyword on one line, quoted value on
|
|
372
|
+
// the next (formatters wrap long lines this way). Only runs when the
|
|
373
|
+
// value line alone matches no pattern - provider-prefixed values are
|
|
374
|
+
// caught by the normal per-line scan on the next iteration.
|
|
375
|
+
if (i + 1 < lines.length && DANGLING_ASSIGNMENT_RE.test(line)) {
|
|
376
|
+
const next = lines[i + 1];
|
|
377
|
+
const valueMatch = next.match(CONTINUATION_VALUE_RE);
|
|
378
|
+
if (valueMatch &&
|
|
379
|
+
!PLACEHOLDER_LINE_RE.test(next) &&
|
|
380
|
+
!PATTERNS.some((p) => p.re.test(valueMatch[1]))) {
|
|
381
|
+
const value = valueMatch[1];
|
|
382
|
+
if (!isPlaceholderValue(value) &&
|
|
383
|
+
shannonEntropy(value) >= 3.8 &&
|
|
384
|
+
!allowlist.some((a) => value.includes(a) || next.includes(a))) {
|
|
385
|
+
found.push({
|
|
386
|
+
kind: "Hardcoded credential",
|
|
387
|
+
file,
|
|
388
|
+
line: i + 2,
|
|
389
|
+
masked: maskSecret(value),
|
|
390
|
+
confidence: "medium",
|
|
391
|
+
raw: value,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
325
394
|
}
|
|
326
|
-
const confidence = isTestPath ? "medium" : effective;
|
|
327
|
-
found.push({
|
|
328
|
-
kind: pattern.kind,
|
|
329
|
-
file,
|
|
330
|
-
line: i + 1,
|
|
331
|
-
masked: maskSecret(value),
|
|
332
|
-
confidence,
|
|
333
|
-
// Raw value stays in memory only; used by --verify, never printed.
|
|
334
|
-
raw: value,
|
|
335
|
-
});
|
|
336
|
-
break; // one finding per line is enough
|
|
337
395
|
}
|
|
338
396
|
}
|
|
339
397
|
return found;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shipready",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.2",
|
|
4
4
|
"description": "Pre-flight check for your repo before shipping. Scans AI-coded projects for secrets, env issues, debug leftovers, and generates AI-agent instruction files.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|