muaddib-scanner 2.11.115 → 2.11.117
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 +1 -1
- package/{self-scan-v2.11.115.json → self-scan-v2.11.117.json} +1 -1
- package/src/scanner/ast.js +11 -4
- package/src/scanner/dataflow.js +5 -53
- package/src/scanner/env-var-classification.js +75 -0
- package/src/scanner/module-graph/annotate-tainted.js +18 -0
- package/src/scanner/module-graph/constants.js +5 -1
- package/src/scanner/typosquat.js +28 -8
package/package.json
CHANGED
package/src/scanner/ast.js
CHANGED
|
@@ -17,6 +17,13 @@ const {
|
|
|
17
17
|
// Check if credential keywords appear INSIDE regex literals or new RegExp() patterns.
|
|
18
18
|
// Only true when the keyword is part of the regex pattern itself, not just a string elsewhere in the file.
|
|
19
19
|
const CREDENTIAL_REGEX_KEYWORDS = /bearer|password|secret|token|credential|api.?key/i;
|
|
20
|
+
// axios call shapes — a network call the legacy regexes miss (caught only by ioc_string_match).
|
|
21
|
+
// Covers BOTH the identifier form (axios(...) / axios.get|post|...(...)) and the inline-require
|
|
22
|
+
// form (require('axios').get(...) — the chalk-pro/jsonkeeper staged-loader shape). Call-shaped
|
|
23
|
+
// only, so it never matches a bare `require('axios')` import, `import axios`, `myaxios.get`, or
|
|
24
|
+
// `axios` in a comment/string. Bare instance-var calls (const c = axios.create(); c.get()) are a
|
|
25
|
+
// known follow-up gap; the create() verb catches the create site itself.
|
|
26
|
+
const AXIOS_NETWORK_CALL_RE = /(?:\baxios|require\s*\(\s*['"]axios['"]\s*\))\s*(?:\(|\.\s*(?:get|post|put|patch|delete|request|head|options|create)\s*\()/;
|
|
20
27
|
function hasCredentialInsideRegex(content) {
|
|
21
28
|
// Check regex literals: /...pattern.../flags
|
|
22
29
|
const regexLiteralRe = /\/(?!\*)(?:[^/\\]|\\.)+\/[gimsuy]*/g;
|
|
@@ -172,9 +179,9 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
172
179
|
// SANDWORM_MODE P2: env harvesting co-occurrence
|
|
173
180
|
hasEnvEnumeration: false, // Object.entries/keys/values(process.env)
|
|
174
181
|
hasEnvHarvestPattern: /\b(KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL|NPM|AWS|SSH|WEBHOOK)\b/.test(content),
|
|
175
|
-
hasNetworkCallInFile: /\b(fetch|https?\.request|https?\.get|dns\.resolve)\b/.test(content),
|
|
176
|
-
// C5: Non-fetch network calls indicate independent network channel (NOT WASM loading)
|
|
177
|
-
hasNonFetchNetworkCall: /\bhttps?\.request\b|\bhttps?\.get\b|\bdns\.resolve\b/.test(content),
|
|
182
|
+
hasNetworkCallInFile: /\b(fetch|https?\.request|https?\.get|dns\.resolve)\b/.test(content) || AXIOS_NETWORK_CALL_RE.test(content),
|
|
183
|
+
// C5: Non-fetch network calls indicate independent network channel (NOT WASM loading). axios is non-fetch.
|
|
184
|
+
hasNonFetchNetworkCall: /\bhttps?\.request\b|\bhttps?\.get\b|\bdns\.resolve\b/.test(content) || AXIOS_NETWORK_CALL_RE.test(content),
|
|
178
185
|
// Credential regex harvesting: regex literals or new RegExp() whose PATTERN contains credential keywords
|
|
179
186
|
// Must check that the keyword is inside the regex, not just anywhere in the file
|
|
180
187
|
hasCredentialRegex: hasCredentialInsideRegex(content),
|
|
@@ -197,7 +204,7 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
197
204
|
gitHooksPathVars: new Map(),
|
|
198
205
|
ideConfigPathVars: new Map(),
|
|
199
206
|
// Wave 4: compound detection — fetch + decrypt + eval chain
|
|
200
|
-
hasRemoteFetch: /\bhttps?\.(get|request)\b/.test(content) || /\bfetch\s*\(/.test(content),
|
|
207
|
+
hasRemoteFetch: /\bhttps?\.(get|request)\b/.test(content) || /\bfetch\s*\(/.test(content) || AXIOS_NETWORK_CALL_RE.test(content),
|
|
201
208
|
// Safe domain exclusion: if ALL URLs in file are from known registries, suppress download_exec_binary
|
|
202
209
|
fetchOnlySafeDomains: false, // computed below after URL extraction
|
|
203
210
|
hasCryptoDecipher: /\bcreateDecipher(iv)?\s*\(/.test(content),
|
package/src/scanner/dataflow.js
CHANGED
|
@@ -1149,58 +1149,10 @@ function isCredentialPath(arg, sensitivePathVars) {
|
|
|
1149
1149
|
return false;
|
|
1150
1150
|
}
|
|
1151
1151
|
|
|
1152
|
-
//
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
// Env var prefixes for tool-internal configuration (not external credentials)
|
|
1159
|
-
const SAFE_ENV_PREFIXES = ['MUADDIB_', 'npm_config_', 'npm_lifecycle_', 'npm_package_'];
|
|
1160
|
-
|
|
1161
|
-
// P6: Node.js runtime config env vars that are not credentials.
|
|
1162
|
-
// NODE_TLS_REJECT_UNAUTHORIZED matches "AUTH" in "UNAUTHORIZED" → false positive.
|
|
1163
|
-
// Real credential exfiltration targets API_KEY, TOKEN, SECRET, PASSWORD.
|
|
1164
|
-
const DATAFLOW_SAFE_ENV_VARS = new Set([
|
|
1165
|
-
'NODE_TLS_REJECT_UNAUTHORIZED', 'NODE_OPTIONS', 'NODE_EXTRA_CA_CERTS',
|
|
1166
|
-
'NODE_ENV', 'NODE_PATH', 'NODE_DEBUG',
|
|
1167
|
-
'DEBUG', 'CI', 'HTTPS_PROXY', 'HTTP_PROXY', 'NO_PROXY',
|
|
1168
|
-
'LANG', 'TZ', 'PORT', 'HOST'
|
|
1169
|
-
// Note: HOME, USER, HOSTNAME stay sensitive — fingerprint exfiltration detection.
|
|
1170
|
-
]);
|
|
1171
|
-
|
|
1172
|
-
function isSensitiveEnv(name) {
|
|
1173
|
-
const upper = name.toUpperCase();
|
|
1174
|
-
if (DATAFLOW_SAFE_ENV_VARS.has(upper)) return false;
|
|
1175
|
-
if (SYSTEM_IDENTITY_ENVS.has(upper)) return true;
|
|
1176
|
-
if (SAFE_ENV_PREFIXES.some(p => upper.startsWith(p))) return false;
|
|
1177
|
-
const sensitive = ['TOKEN', 'SECRET', 'KEY', 'PASSWORD', 'CREDENTIAL', 'AUTH', 'NPM', 'AWS', 'AZURE', 'GCP'];
|
|
1178
|
-
return sensitive.some(s => upper.includes(s));
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
// Audit 2026-05 DF-C4: credential-tier env vars distinguished from generic env_read.
|
|
1182
|
-
// These represent authentication material (NPM_TOKEN, GITHUB_TOKEN, AWS_SECRET_ACCESS_KEY,
|
|
1183
|
-
// STRIPE_API_KEY etc.) — strictly narrower than isSensitiveEnv. Sources of this type
|
|
1184
|
-
// participate in hasHighRiskSource so credential exfil patterns are NOT downgraded by the
|
|
1185
|
-
// HIGH→MEDIUM graduation. System identity vars (HOME, USER) remain plain env_read since
|
|
1186
|
-
// they are fingerprinting signals, not credentials.
|
|
1187
|
-
const KNOWN_CREDENTIAL_ENV_VARS = new Set([
|
|
1188
|
-
'NPM_TOKEN', 'GITHUB_TOKEN', 'GH_TOKEN', 'NODE_AUTH_TOKEN',
|
|
1189
|
-
'CIRCLE_TOKEN', 'GITLAB_TOKEN', 'CARGO_REGISTRY_TOKEN', 'PYPI_TOKEN',
|
|
1190
|
-
'GOOGLE_APPLICATION_CREDENTIALS', 'AZURE_CLIENT_SECRET',
|
|
1191
|
-
'SENTRY_AUTH_TOKEN', 'NPM_AUTH_TOKEN', 'NPM_CONFIG_AUTHTOKEN'
|
|
1192
|
-
]);
|
|
1193
|
-
|
|
1194
|
-
const CREDENTIAL_ENV_SUFFIX_RE = /(?:^|_)(?:TOKEN|SECRET|PASSWORD|PASSPHRASE|CREDENTIAL|CREDENTIALS|API_KEY|ACCESS_KEY|ACCESS_KEY_ID|SECRET_KEY|PRIVATE_KEY|SIGNING_KEY|SESSION_TOKEN|REFRESH_TOKEN|AUTH_TOKEN)$/;
|
|
1195
|
-
|
|
1196
|
-
function isCredentialEnv(name) {
|
|
1197
|
-
const upper = name.toUpperCase();
|
|
1198
|
-
// System identity vars are fingerprinting, not credentials
|
|
1199
|
-
if (SYSTEM_IDENTITY_ENVS.has(upper)) return false;
|
|
1200
|
-
// Public keys are not credentials (e.g., SSH_PUBLIC_KEY, GPG_PUBLIC_KEY)
|
|
1201
|
-
if (upper.includes('PUBLIC_KEY') || upper.includes('PUBKEY')) return false;
|
|
1202
|
-
if (KNOWN_CREDENTIAL_ENV_VARS.has(upper)) return true;
|
|
1203
|
-
return CREDENTIAL_ENV_SUFFIX_RE.test(upper);
|
|
1204
|
-
}
|
|
1152
|
+
// Env-var credential classification (isSensitiveEnv / isCredentialEnv + their sets) was
|
|
1153
|
+
// extracted to a shared leaf module so the module-graph cross-file taint can apply the EXACT
|
|
1154
|
+
// same credential-vs-config distinction (it previously tainted any process.env read). Imported
|
|
1155
|
+
// at module load → in scope for the analyzeDataFlow call sites above. Behavior unchanged here.
|
|
1156
|
+
const { isSensitiveEnv, isCredentialEnv } = require('./env-var-classification.js');
|
|
1205
1157
|
|
|
1206
1158
|
module.exports = { analyzeDataFlow };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Environment-variable credential classification (shared leaf module).
|
|
4
|
+
// Extracted verbatim from dataflow.js so the SAME credential-vs-config distinction can
|
|
5
|
+
// gate every taint source — dataflow (scanner/dataflow.js) AND the module-graph cross-file
|
|
6
|
+
// taint (scanner/module-graph/annotate-tainted.js), which previously tainted ANY process.env
|
|
7
|
+
// read indiscriminately (config vars like STORYBOARD_SERVER_URL → false credential_exfil).
|
|
8
|
+
// No project dependencies → safe to require from any scanner, no cycles.
|
|
9
|
+
|
|
10
|
+
// System identity env vars used for fingerprinting/exfiltration
|
|
11
|
+
const SYSTEM_IDENTITY_ENVS = new Set([
|
|
12
|
+
'USER', 'USERNAME', 'LOGNAME', 'HOME', 'HOSTNAME',
|
|
13
|
+
'USERPROFILE', 'COMPUTERNAME', 'WHOAMI'
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
// Env var prefixes for tool-internal configuration (not external credentials)
|
|
17
|
+
const SAFE_ENV_PREFIXES = ['MUADDIB_', 'npm_config_', 'npm_lifecycle_', 'npm_package_'];
|
|
18
|
+
|
|
19
|
+
// P6: Node.js runtime config env vars that are not credentials.
|
|
20
|
+
// NODE_TLS_REJECT_UNAUTHORIZED matches "AUTH" in "UNAUTHORIZED" → false positive.
|
|
21
|
+
// Real credential exfiltration targets API_KEY, TOKEN, SECRET, PASSWORD.
|
|
22
|
+
const DATAFLOW_SAFE_ENV_VARS = new Set([
|
|
23
|
+
'NODE_TLS_REJECT_UNAUTHORIZED', 'NODE_OPTIONS', 'NODE_EXTRA_CA_CERTS',
|
|
24
|
+
'NODE_ENV', 'NODE_PATH', 'NODE_DEBUG',
|
|
25
|
+
'DEBUG', 'CI', 'HTTPS_PROXY', 'HTTP_PROXY', 'NO_PROXY',
|
|
26
|
+
'LANG', 'TZ', 'PORT', 'HOST'
|
|
27
|
+
// Note: HOME, USER, HOSTNAME stay sensitive — fingerprint exfiltration detection.
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
// True when an env var name is a sensitive source (credential material OR system-identity
|
|
31
|
+
// fingerprinting). Config vars (URL/HOST/PORT/NODE_ENV/proxy/...) return false. This is the
|
|
32
|
+
// classification module-graph cross-file taint now shares (it formerly tainted every read).
|
|
33
|
+
function isSensitiveEnv(name) {
|
|
34
|
+
const upper = name.toUpperCase();
|
|
35
|
+
if (DATAFLOW_SAFE_ENV_VARS.has(upper)) return false;
|
|
36
|
+
if (SYSTEM_IDENTITY_ENVS.has(upper)) return true;
|
|
37
|
+
if (SAFE_ENV_PREFIXES.some(p => upper.startsWith(p))) return false;
|
|
38
|
+
const sensitive = ['TOKEN', 'SECRET', 'KEY', 'PASSWORD', 'CREDENTIAL', 'AUTH', 'NPM', 'AWS', 'AZURE', 'GCP'];
|
|
39
|
+
return sensitive.some(s => upper.includes(s));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Audit 2026-05 DF-C4: credential-tier env vars distinguished from generic env_read.
|
|
43
|
+
// These represent authentication material (NPM_TOKEN, GITHUB_TOKEN, AWS_SECRET_ACCESS_KEY,
|
|
44
|
+
// STRIPE_API_KEY etc.) — strictly narrower than isSensitiveEnv. Sources of this type
|
|
45
|
+
// participate in hasHighRiskSource so credential exfil patterns are NOT downgraded by the
|
|
46
|
+
// HIGH→MEDIUM graduation. System identity vars (HOME, USER) remain plain env_read since
|
|
47
|
+
// they are fingerprinting signals, not credentials.
|
|
48
|
+
const KNOWN_CREDENTIAL_ENV_VARS = new Set([
|
|
49
|
+
'NPM_TOKEN', 'GITHUB_TOKEN', 'GH_TOKEN', 'NODE_AUTH_TOKEN',
|
|
50
|
+
'CIRCLE_TOKEN', 'GITLAB_TOKEN', 'CARGO_REGISTRY_TOKEN', 'PYPI_TOKEN',
|
|
51
|
+
'GOOGLE_APPLICATION_CREDENTIALS', 'AZURE_CLIENT_SECRET',
|
|
52
|
+
'SENTRY_AUTH_TOKEN', 'NPM_AUTH_TOKEN', 'NPM_CONFIG_AUTHTOKEN'
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
const CREDENTIAL_ENV_SUFFIX_RE = /(?:^|_)(?:TOKEN|SECRET|PASSWORD|PASSPHRASE|CREDENTIAL|CREDENTIALS|API_KEY|ACCESS_KEY|ACCESS_KEY_ID|SECRET_KEY|PRIVATE_KEY|SIGNING_KEY|SESSION_TOKEN|REFRESH_TOKEN|AUTH_TOKEN)$/;
|
|
56
|
+
|
|
57
|
+
function isCredentialEnv(name) {
|
|
58
|
+
const upper = name.toUpperCase();
|
|
59
|
+
// System identity vars are fingerprinting, not credentials
|
|
60
|
+
if (SYSTEM_IDENTITY_ENVS.has(upper)) return false;
|
|
61
|
+
// Public keys are not credentials (e.g., SSH_PUBLIC_KEY, GPG_PUBLIC_KEY)
|
|
62
|
+
if (upper.includes('PUBLIC_KEY') || upper.includes('PUBKEY')) return false;
|
|
63
|
+
if (KNOWN_CREDENTIAL_ENV_VARS.has(upper)) return true;
|
|
64
|
+
return CREDENTIAL_ENV_SUFFIX_RE.test(upper);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = {
|
|
68
|
+
SYSTEM_IDENTITY_ENVS,
|
|
69
|
+
SAFE_ENV_PREFIXES,
|
|
70
|
+
DATAFLOW_SAFE_ENV_VARS,
|
|
71
|
+
KNOWN_CREDENTIAL_ENV_VARS,
|
|
72
|
+
CREDENTIAL_ENV_SUFFIX_RE,
|
|
73
|
+
isSensitiveEnv,
|
|
74
|
+
isCredentialEnv,
|
|
75
|
+
};
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const { SENSITIVE_MODULES } = require('./constants.js');
|
|
5
|
+
const { isSensitiveEnv } = require('../env-var-classification.js');
|
|
5
6
|
const {
|
|
6
7
|
parseFile, walkAST, isRequireCall, isModuleExportsAssign,
|
|
7
8
|
getExportName, getFunctionBody, getMemberChain, extractLiteralArg
|
|
@@ -272,6 +273,11 @@ function checkNodeTaint(node, moduleVars) {
|
|
|
272
273
|
const chain = getMemberChain(node);
|
|
273
274
|
if (chain.startsWith('process.env')) {
|
|
274
275
|
const detail = chain.length > 'process.env'.length ? chain.slice('process.env.'.length) : '';
|
|
276
|
+
// Source-precision (segment A): a SPECIFIC non-credential config var (URL/HOST/PORT/
|
|
277
|
+
// NODE_ENV/...) is not a credential taint source — consistent with dataflow.js's
|
|
278
|
+
// isSensitiveEnv (DF-C4). Whole-object `process.env` (detail '') stays tainted: an env
|
|
279
|
+
// dump exfiltrates every secret. System-identity vars (HOME/USER) stay tainted too.
|
|
280
|
+
if (detail && !isSensitiveEnv(detail)) return null;
|
|
275
281
|
return { source: 'process.env', detail };
|
|
276
282
|
}
|
|
277
283
|
}
|
|
@@ -332,9 +338,21 @@ function scanBodyForTaint(body, moduleVars, taintedVars) {
|
|
|
332
338
|
// Collect local tainted vars within this function scope too
|
|
333
339
|
const localTainted = Object.assign(Object.create(null), taintedVars);
|
|
334
340
|
|
|
341
|
+
// A `process.env.X` read is matched as a unit by checkNodeTaint (gated on isSensitiveEnv).
|
|
342
|
+
// Record the inner bare `process.env` node of each so the pre-order walker does not RE-match
|
|
343
|
+
// it standalone (detail '' = whole-env dump), which would defeat the per-var gate for config
|
|
344
|
+
// vars. A genuinely standalone `process.env` (a different node) still taints as a dump.
|
|
345
|
+
const innerEnvNodes = new WeakSet();
|
|
346
|
+
|
|
335
347
|
let found = null;
|
|
336
348
|
walkAST({ type: 'Program', body }, (node) => {
|
|
337
349
|
if (found) return;
|
|
350
|
+
if (innerEnvNodes.has(node)) return;
|
|
351
|
+
|
|
352
|
+
if (node.type === 'MemberExpression' && node.object &&
|
|
353
|
+
node.object.type === 'MemberExpression' && getMemberChain(node.object) === 'process.env') {
|
|
354
|
+
innerEnvNodes.add(node.object);
|
|
355
|
+
}
|
|
338
356
|
|
|
339
357
|
// Variable assignment inside function
|
|
340
358
|
if (node.type === 'VariableDeclaration') {
|
|
@@ -18,11 +18,15 @@ const ACORN_OPTIONS = {
|
|
|
18
18
|
};
|
|
19
19
|
|
|
20
20
|
// --- Sink patterns for cross-file detection ---
|
|
21
|
-
const SINK_CALLEE_NAMES = new Set(['fetch', 'eval', 'Function', 'WebSocket', 'XMLHttpRequest']);
|
|
21
|
+
const SINK_CALLEE_NAMES = new Set(['fetch', 'eval', 'Function', 'WebSocket', 'XMLHttpRequest', 'axios']);
|
|
22
22
|
const SINK_MEMBER_METHODS = new Set([
|
|
23
23
|
'https.request', 'https.get', 'http.request', 'http.get',
|
|
24
24
|
'child_process.exec', 'child_process.execSync', 'child_process.spawn',
|
|
25
25
|
'dns.resolveTxt', 'dns.resolve', 'dns.resolve4', 'dns.resolve6',
|
|
26
|
+
// axios as a cross-file network sink (axios.get(taintedData) etc.). Instance form
|
|
27
|
+
// (const c = axios.create(); c.get(...)) is NOT added to SINK_INSTANCE_METHODS — that
|
|
28
|
+
// would match every .get/.post receiver and explode FPs; it's a known follow-up gap.
|
|
29
|
+
'axios.get', 'axios.post', 'axios.put', 'axios.patch', 'axios.delete', 'axios.request',
|
|
26
30
|
]);
|
|
27
31
|
const SINK_INSTANCE_METHODS = new Set(['connect', 'write', 'send']);
|
|
28
32
|
|
package/src/scanner/typosquat.js
CHANGED
|
@@ -73,6 +73,23 @@ const LEGIT_BOUNDARY_TOKENS = new Set([
|
|
|
73
73
|
'v2', 'v3', 'v4', 'next', 'latest', 'stable', 'lts', 'legacy', 'beta', 'alpha'
|
|
74
74
|
]);
|
|
75
75
|
|
|
76
|
+
// RT-C1-FPR (2026-06, n=61 blind adjudication → boundary-squat measured 100% FP): popular
|
|
77
|
+
// packages whose names are GENERIC tech/English words appear as a legitimate TRAILING token
|
|
78
|
+
// in countless real packages — class-validator, graphile-config, ansi-colors, sinon-chai,
|
|
79
|
+
// react-helmet-async, swagger-ui-express, short-uuid, react-router-redux, openapi-typescript,
|
|
80
|
+
// tree-sitter-c-sharp, agent-commander. Suffix boundary-squat on these is unreliable, so they
|
|
81
|
+
// are NOT matched. Distinctive brand names (axios, lodash, chalk, crypto-js — incl. the
|
|
82
|
+
// plain-crypto-js / secure-axios FN-guards) stay matchable. A genuine `<x>-<generic>` squat is
|
|
83
|
+
// caught by its CODE (exfil/RCE + the Track-R malice floor), not by name shape (see below).
|
|
84
|
+
const GENERIC_POPULAR_NAMES = new Set([
|
|
85
|
+
'validator', 'config', 'colors', 'async', 'chai', 'typescript', 'request', 'uuid',
|
|
86
|
+
'redux', 'express', 'sharp', 'commander', 'debug', 'glob', 'yaml', 'cors', 'helmet',
|
|
87
|
+
'canvas', 'immutable', 'classnames',
|
|
88
|
+
// Infra/framework brands that are also common legit trailing tokens (rate-limit-redis,
|
|
89
|
+
// connect-redis, shadcn-svelte, authentikt-svelte) — same measured-FP class, same FN floor.
|
|
90
|
+
'redis', 'svelte',
|
|
91
|
+
]);
|
|
92
|
+
|
|
76
93
|
// Packages legitimes courts ou qui ressemblent a des populaires
|
|
77
94
|
const WHITELIST = new Set([
|
|
78
95
|
// Packages tres courts legitimes
|
|
@@ -456,14 +473,13 @@ function findDependencyBoundarySquat(name) {
|
|
|
456
473
|
if (lower === popular) continue;
|
|
457
474
|
|
|
458
475
|
if (popular.includes('-')) {
|
|
459
|
-
// Multi-token popular (e.g. crypto-js):
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
if (extra === null || extra.length === 0) continue;
|
|
476
|
+
// Multi-token popular (e.g. crypto-js): a squat PREPENDS a deceptive qualifier
|
|
477
|
+
// (plain-crypto-js → endsWith). The reverse `<popular>-<suffix>` (date-fns-tz,
|
|
478
|
+
// aws-sdk-client-mock, core-js-compat) is the popular package's OWN ecosystem extension —
|
|
479
|
+
// never a squat — so the prefix-position match is dropped (2026-06 FPR fix, 100% FP).
|
|
480
|
+
if (!lower.endsWith('-' + popular)) continue;
|
|
481
|
+
const extra = lower.slice(0, lower.length - popular.length - 1);
|
|
482
|
+
if (extra.length === 0) continue;
|
|
467
483
|
// Reject if extra is a legit boundary token (single token only)
|
|
468
484
|
if (!extra.includes('-') && LEGIT_BOUNDARY_TOKENS.has(extra)) continue;
|
|
469
485
|
return { original: POPULAR_PACKAGES[i], type: 'boundary_squat', distance: extra.length, extra };
|
|
@@ -480,6 +496,10 @@ function findDependencyBoundarySquat(name) {
|
|
|
480
496
|
const tokens = lower.split('-');
|
|
481
497
|
if (tokens.length === 1) continue;
|
|
482
498
|
if (tokens[tokens.length - 1] !== popular) continue; // popular must be the trailing token
|
|
499
|
+
// Generic-word popular (validator/config/colors/async/chai/typescript/...) is a common
|
|
500
|
+
// legitimate trailing token (class-validator, graphile-config, ansi-colors) — 100% FP in
|
|
501
|
+
// the 2026-06 measurement. Distinctive brands (axios → secure-axios FN-guard) still match.
|
|
502
|
+
if (GENERIC_POPULAR_NAMES.has(popular)) continue;
|
|
483
503
|
const siblings = tokens.slice(0, -1);
|
|
484
504
|
// Benign ecosystem variant if every prefix token is a legit qualifier (ts-jest, babel-jest).
|
|
485
505
|
if (siblings.every(t => LEGIT_BOUNDARY_TOKENS.has(t) || isLegitimateVariant(t))) continue;
|