ticketlens 0.38.17 → 0.38.18
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/package.json
CHANGED
|
@@ -13,7 +13,7 @@ const GIT_SHA_RE = /^[0-9a-f]{7,64}$/i;
|
|
|
13
13
|
const TICKET_KEY_RE = /^[A-Z][A-Z0-9]+-\d+$/;
|
|
14
14
|
const GIT_REFERENCE_WORD_RE = /\b(commit|sha\d*|revision|rev|digest|checksum|md5(sum)?|hash|fingerprint)\b/i;
|
|
15
15
|
const HASH_LABEL_PREFIX_RE = /^[a-z0-9]+:/i;
|
|
16
|
-
const EDGE_PUNCTUATION_RE = /^[`'"()
|
|
16
|
+
const EDGE_PUNCTUATION_RE = /^[`'"(),.:]+|[`'"(),.:]+$/g;
|
|
17
17
|
const MIN_RANDOM_TOKEN_LENGTH = 20;
|
|
18
18
|
// Hex-alphabet strings (16 symbols) top out near 4.0 bits/char by definition, so a
|
|
19
19
|
// threshold of 4.0 makes it nearly impossible to ever flag a hex-shaped secret.
|
|
@@ -37,6 +37,32 @@ const HARD_REJECT_PATTERNS = [
|
|
|
37
37
|
|
|
38
38
|
const EMAIL_RE = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/;
|
|
39
39
|
|
|
40
|
+
// A letters-only token ending in a recognized source-file extension reads as
|
|
41
|
+
// high-entropy the same way a real secret does — a class name doubling as its
|
|
42
|
+
// filename is the common case, but a deliberately-renamed or accidentally
|
|
43
|
+
// letters-only secret (no digits, so the hard-reject patterns above don't
|
|
44
|
+
// apply either) would match this shape too and MUST NOT be silently waved
|
|
45
|
+
// through: security review confirmed a full-exemption version of this check
|
|
46
|
+
// was a deterministic bypass (append ".php" to any 20+ char letters-only
|
|
47
|
+
// secret and it passed clean). So this only ever downgrades a match to a
|
|
48
|
+
// warning, never removes it from the reject reasons — see the two
|
|
49
|
+
// independent checks against looksRandomCandidates in scanForSecrets below,
|
|
50
|
+
// not a shortcut inside looksRandom itself. A bare identifier with no
|
|
51
|
+
// extension (a method name, not a filename) isn't covered by this at all —
|
|
52
|
+
// that shape is indistinguishable from a base64 secret fragment either way.
|
|
53
|
+
const CODE_FILENAME_RE = /^[A-Za-z]+\.(php|m?js|tsx?|jsx|py|rb|java|go|rs|vue|s?css|md|json|ya?ml|sh)$/i;
|
|
54
|
+
|
|
55
|
+
function looksLikeCodeFilename(rawToken) {
|
|
56
|
+
const stripped = stripEdgePunctuation(rawToken);
|
|
57
|
+
const match = stripped.match(CODE_FILENAME_RE);
|
|
58
|
+
// Requiring an internal case switch in the stem (the same signal used to
|
|
59
|
+
// detect base64 content elsewhere in this file) means a genuinely random
|
|
60
|
+
// single-case letter run plus a fake extension gets no special treatment
|
|
61
|
+
// at all — only tokens that already look like a real PascalCase/camelCase
|
|
62
|
+
// identifier reach the softer warning path below.
|
|
63
|
+
return match !== null && hasInternalCaseSwitch(match[0]);
|
|
64
|
+
}
|
|
65
|
+
|
|
40
66
|
function shannonEntropy(token) {
|
|
41
67
|
const counts = new Map();
|
|
42
68
|
for (const ch of token) counts.set(ch, (counts.get(ch) ?? 0) + 1);
|
|
@@ -183,8 +209,19 @@ export function scanForSecrets({ title = '', tags = [], body = '' } = {}) {
|
|
|
183
209
|
const reasons = [];
|
|
184
210
|
const warnings = [];
|
|
185
211
|
|
|
186
|
-
|
|
187
|
-
|
|
212
|
+
// Each field is tokenized on its own, and joinedChunkRuns runs separately
|
|
213
|
+
// per field, so a trailing tag word can never glue onto the next tag or
|
|
214
|
+
// onto the body's first word (see Trigger 3 in
|
|
215
|
+
// recall-secret-scanner-false-positives.md — tags echoing a phrase already
|
|
216
|
+
// repeated in the body were false-positiving via exactly this cross-field
|
|
217
|
+
// join). The hard-reject pass below intentionally keeps using the flat,
|
|
218
|
+
// cross-field `tokens`/`combined`/`despacedCombined`: those patterns match
|
|
219
|
+
// an exact literal prefix (AKIA, sk-, ghp_, eyJ, -----BEGIN), so joining
|
|
220
|
+
// across a field boundary can't turn them into a false positive the way
|
|
221
|
+
// entropy can.
|
|
222
|
+
const fieldTokenGroups = [title, ...tags, body].map(field => field.split(/\s+/).filter(Boolean));
|
|
223
|
+
const tokens = fieldTokenGroups.flat();
|
|
224
|
+
const candidates = [...tokens, ...fieldTokenGroups.flatMap(group => joinedChunkRuns(group))];
|
|
188
225
|
|
|
189
226
|
// A known secret shape (AWS key, API key prefix, JWT, PEM block...) is recognized
|
|
190
227
|
// by a specific literal prefix. Checked three ways, each catching what the
|
|
@@ -216,9 +253,20 @@ export function scanForSecrets({ title = '', tags = [], body = '' } = {}) {
|
|
|
216
253
|
// only split secrets), an email joined with an adjacent word can produce a long
|
|
217
254
|
// mixed string with incidentally high entropy. Emails get their own warning
|
|
218
255
|
// below — they're not secrets, so they shouldn't feed the random-string check.
|
|
219
|
-
|
|
256
|
+
//
|
|
257
|
+
// A code-filename-shaped candidate (looksLikeCodeFilename) is split out into
|
|
258
|
+
// its own warning instead of a reject reason: security review found that
|
|
259
|
+
// fully exempting this shape from the entropy check was a deterministic
|
|
260
|
+
// bypass (append ".php" to any letters-only secret and it passed clean).
|
|
261
|
+
// Downgrading to a warning — never silently dropping the signal — matches
|
|
262
|
+
// how an email address is already handled below.
|
|
263
|
+
const randomCandidates = candidates.filter(token => !EMAIL_RE.test(token) && looksRandom(token, combined));
|
|
264
|
+
if (randomCandidates.some(token => !looksLikeCodeFilename(token))) {
|
|
220
265
|
reasons.push('Contains a long, random-looking string that could be a secret.');
|
|
221
266
|
}
|
|
267
|
+
if (randomCandidates.some(token => looksLikeCodeFilename(token))) {
|
|
268
|
+
warnings.push('Contains a code-filename-shaped token that also reads as high-entropy — double-check it is not a credential.');
|
|
269
|
+
}
|
|
222
270
|
|
|
223
271
|
if (EMAIL_RE.test(combined)) {
|
|
224
272
|
warnings.push('Contains an email address.');
|