ticketlens 0.38.39 → 0.38.41
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
|
@@ -150,6 +150,89 @@ const CODE_FILENAME_RE = new RegExp(`^([A-Za-z][A-Za-z-]*)\\.(${CODE_EXTENSION_A
|
|
|
150
150
|
// text regardless of any token's label-word status.
|
|
151
151
|
const FILENAME_REFERENCE_RE = new RegExp(`^[A-Za-z][A-Za-z-]*\\.(${CODE_EXTENSION_ALTERNATION})('s)?$`, 'i');
|
|
152
152
|
|
|
153
|
+
// Underscore-delimited segments (Zend-1-style PHP class names — a common
|
|
154
|
+
// legacy naming convention, e.g. "Acme_Http_ClientFactory",
|
|
155
|
+
// "Zend_Http_Client") are letters-only per segment, no digit support (same
|
|
156
|
+
// convention as CODE_FILENAME_RE's stem). Deliberately does NOT require
|
|
157
|
+
// isHyphenatedWordCompound's "no internal case switch" guard: that guard
|
|
158
|
+
// exists there to keep base64-shaped content from posing as a lowercase-
|
|
159
|
+
// English hyphenated compound, but PascalCase segments ARE the real,
|
|
160
|
+
// expected positive signal for a class name ("ClientFactory",
|
|
161
|
+
// "SaleSystem") — requiring their absence would reject the exact
|
|
162
|
+
// legitimate shape this exists to recognize. See
|
|
163
|
+
// looksLikeCodeIdentifierOrPath's own comment for how this stays
|
|
164
|
+
// downgrade-only despite that asymmetry.
|
|
165
|
+
const UNDERSCORE_COMPOUND_RE = /^[A-Za-z]+(_[A-Za-z]+)+$/;
|
|
166
|
+
|
|
167
|
+
// Slash-delimited path segments (a namespace-shaped filesystem/class path —
|
|
168
|
+
// "acme/library/Billing/Client/Http/Adapter/Fetch/", optionally trailing-
|
|
169
|
+
// slash-terminated). Allows digits per segment (real
|
|
170
|
+
// directory/namespace segments routinely do — "v2", "api2"), unlike the
|
|
171
|
+
// underscore shape above — a deliberate, narrower convention than mixing
|
|
172
|
+
// delimiters within one candidate would need.
|
|
173
|
+
const SLASH_PATH_RE = /^[A-Za-z0-9]+(\/[A-Za-z0-9]+)+\/?$/;
|
|
174
|
+
|
|
175
|
+
// PHP static method/property/const access (Class_Name::method,
|
|
176
|
+
// Class_Name::CONST), optionally prefixed by a leading backslash (PHP's
|
|
177
|
+
// fully-qualified global-namespace form — stripEdgePunctuation does not
|
|
178
|
+
// strip a LEADING backslash, only the trailing/leading punctuation in
|
|
179
|
+
// EDGE_PUNCTUATION_RE's class). The class part reuses the same shape as
|
|
180
|
+
// UNDERSCORE_COMPOUND_RE but allows a bare (non-underscore) class name too
|
|
181
|
+
// — real code has both ("Zend_Http_Client::GET" and "Router::dispatch").
|
|
182
|
+
// Found live: backlog #17's ORIGINAL bug report repro included this exact
|
|
183
|
+
// class+member shape (`Advent_Http_ClientFactory::clientForJson`,
|
|
184
|
+
// `Zend_Http_Client::GET`), but the first fix only covered the bare class
|
|
185
|
+
// name in isolation — the embedded "::member" broke UNDERSCORE_COMPOUND_RE's
|
|
186
|
+
// match (colons aren't letters/underscores) and no other exemption applied.
|
|
187
|
+
const STATIC_REFERENCE_RE = /^\\?([A-Za-z]+(?:_[A-Za-z]+)*)::([A-Za-z_][A-Za-z0-9_]*)$/;
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Downgrade-only, same never-exempt treatment as looksLikeCodeFilename and
|
|
191
|
+
* looksLikeCodeSyntax below: true for a candidate shaped like a multi-segment
|
|
192
|
+
* code identifier (underscore-delimited PHP/Zend-1-style class name), a
|
|
193
|
+
* namespace/filesystem path (slash-delimited), or a PHP static method/const
|
|
194
|
+
* reference (Class_Name::member, see STATIC_REFERENCE_RE), each segment
|
|
195
|
+
* capped at MAX_COMPOUND_SEGMENT_LENGTH so a long random run can't pose as
|
|
196
|
+
* one "segment" — same cap isHyphenatedWordCompound already uses. Backlog
|
|
197
|
+
* #17: isLabelWord's ordinary-word branch only matches letters-only tokens
|
|
198
|
+
* with no separator at all, and neither existing downgrade path below
|
|
199
|
+
* requires a file extension or bracket/paren — so a standalone identifier,
|
|
200
|
+
* path, or static reference like the three shapes above had no exemption
|
|
201
|
+
* path whatsoever, unlike a hyphenated compound or a dotted filename.
|
|
202
|
+
*
|
|
203
|
+
* A disguised real secret matching any of the three shapes still surfaces
|
|
204
|
+
* as a warning, never a silent pass — see the security regression tests
|
|
205
|
+
* alongside this function's own tests for all three shapes.
|
|
206
|
+
*
|
|
207
|
+
* Known accepted gap (security review, backlog #17): unlike
|
|
208
|
+
* isHyphenatedWordCompound, this has no whole-token case-switch guard (see
|
|
209
|
+
* UNDERSCORE_COMPOUND_RE's own comment for why one can't be added without
|
|
210
|
+
* rejecting the real Zend_Http_Client-shaped identifiers this exists to
|
|
211
|
+
* recognize) — so a uniform-case (all-upper or all-lower) letters-only
|
|
212
|
+
* secret needs only ONE underscore, slash, or "::" inserted to downgrade
|
|
213
|
+
* from a full reject to a warning, no camouflage (fake extension, case
|
|
214
|
+
* pattern) required. Still never a silent pass — always a warning — so
|
|
215
|
+
* this is the same class of trade-off as CODE_SYNTAX_RE's even less-
|
|
216
|
+
* constrained downgrade above (any bracket/paren, no shape requirement at
|
|
217
|
+
* all), not a new exposure.
|
|
218
|
+
*/
|
|
219
|
+
function looksLikeCodeIdentifierOrPath(rawToken) {
|
|
220
|
+
const stripped = stripEdgePunctuation(rawToken);
|
|
221
|
+
if (UNDERSCORE_COMPOUND_RE.test(stripped)) {
|
|
222
|
+
return stripped.split('_').every(segment => segment.length <= MAX_COMPOUND_SEGMENT_LENGTH);
|
|
223
|
+
}
|
|
224
|
+
if (SLASH_PATH_RE.test(stripped)) {
|
|
225
|
+
return stripped.split('/').filter(Boolean).every(segment => segment.length <= MAX_COMPOUND_SEGMENT_LENGTH);
|
|
226
|
+
}
|
|
227
|
+
const staticRefMatch = stripped.match(STATIC_REFERENCE_RE);
|
|
228
|
+
if (staticRefMatch) {
|
|
229
|
+
const classSegmentsWithinCap = staticRefMatch[1].split('_').every(segment => segment.length <= MAX_COMPOUND_SEGMENT_LENGTH);
|
|
230
|
+
const memberWithinCap = staticRefMatch[2].length <= MAX_COMPOUND_SEGMENT_LENGTH;
|
|
231
|
+
return classSegmentsWithinCap && memberWithinCap;
|
|
232
|
+
}
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
|
|
153
236
|
function looksLikeCodeFilename(rawToken) {
|
|
154
237
|
const stripped = stripEdgePunctuation(rawToken);
|
|
155
238
|
const match = stripped.match(CODE_FILENAME_RE);
|
|
@@ -443,7 +526,11 @@ export function scanForSecrets({ title = '', tags = [], body = '' } = {}) {
|
|
|
443
526
|
// Downgrading to a warning — never silently dropping the signal — matches
|
|
444
527
|
// how an email address is already handled below.
|
|
445
528
|
const randomCandidates = candidates.filter(token => !EMAIL_RE.test(token) && looksRandom(token, combined));
|
|
446
|
-
if (
|
|
529
|
+
if (
|
|
530
|
+
randomCandidates.some(
|
|
531
|
+
token => !looksLikeCodeFilename(token) && !looksLikeCodeSyntax(token) && !looksLikeCodeIdentifierOrPath(token),
|
|
532
|
+
)
|
|
533
|
+
) {
|
|
447
534
|
reasons.push('Contains a long, random-looking string that could be a secret.');
|
|
448
535
|
}
|
|
449
536
|
if (randomCandidates.some(token => looksLikeCodeFilename(token))) {
|
|
@@ -452,6 +539,9 @@ export function scanForSecrets({ title = '', tags = [], body = '' } = {}) {
|
|
|
452
539
|
if (randomCandidates.some(token => looksLikeCodeSyntax(token))) {
|
|
453
540
|
warnings.push('Contains a code-syntax-shaped token (brackets or parentheses) that also reads as high-entropy — double-check it is not a credential.');
|
|
454
541
|
}
|
|
542
|
+
if (randomCandidates.some(token => looksLikeCodeIdentifierOrPath(token))) {
|
|
543
|
+
warnings.push('Contains a code-identifier-or-path-shaped token that also reads as high-entropy — double-check it is not a credential.');
|
|
544
|
+
}
|
|
455
545
|
|
|
456
546
|
if (EMAIL_RE.test(combined)) {
|
|
457
547
|
warnings.push('Contains an email address.');
|