@lateos/npm-scan 0.18.3 → 1.1.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.
- package/CHANGELOG.md +32 -0
- package/README.md +864 -826
- package/VALIDATION.md +92 -0
- package/backend/cra.js +113 -21
- package/backend/db/pg-schema.sql +155 -0
- package/backend/db.js +18 -10
- package/backend/detectors/atk-001-lifecycle.js +5 -5
- package/backend/detectors/atk-002-obfusc.js +126 -47
- package/backend/detectors/atk-003-creds.js +8 -4
- package/backend/detectors/atk-004-persist.js +3 -3
- package/backend/detectors/atk-005-exfil.js +8 -4
- package/backend/detectors/atk-006-depconf.js +3 -3
- package/backend/detectors/atk-007-typosquat.js +64 -10
- package/backend/detectors/atk-008-tarball-tamper.js +6 -6
- package/backend/detectors/atk-009-dormant-trigger.js +9 -5
- package/backend/detectors/atk-010-sandbox-evasion.js +25 -10
- package/backend/detectors/atk-011-transitive-prop.js +14 -13
- package/backend/detectors/axios-poisoning/d1-version-fingerprint.js +4 -4
- package/backend/detectors/axios-poisoning/d2-decoy-dep.js +5 -1
- package/backend/detectors/axios-poisoning/d3-postinstall-rat.js +64 -19
- package/backend/detectors/axios-poisoning/index.js +77 -60
- package/backend/detectors/config/thresholds.js +111 -0
- package/backend/detectors/config/whitelist.json +74 -0
- package/backend/detectors/cve-2026-48710-badhost/codePattern.js +26 -9
- package/backend/detectors/cve-2026-48710-badhost/findings.js +8 -4
- package/backend/detectors/cve-2026-48710-badhost/index.js +1 -1
- package/backend/detectors/cve-2026-48710-badhost/manifest.js +127 -39
- package/backend/detectors/cve-2026-48710-badhost/transitive.js +87 -28
- package/backend/detectors/hf-impersonation/index.js +94 -31
- package/backend/detectors/hf-impersonation/jaro-winkler.js +33 -12
- package/backend/detectors/hf-impersonation/known-orgs.js +15 -3
- package/backend/detectors/hf-impersonation/simhash.js +2 -2
- package/backend/detectors/index.js +184 -31
- package/backend/detectors/lib/ast-patterns.js +24 -0
- package/backend/detectors/lib/entropy-analyzer.js +32 -0
- package/backend/detectors/megalodon/d1-workflow-scan.js +40 -16
- package/backend/detectors/megalodon/d2-credential-harvest.js +12 -5
- package/backend/detectors/megalodon/d3-publish-velocity.js +17 -11
- package/backend/detectors/megalodon/d4-publisher-drift.js +48 -16
- package/backend/detectors/megalodon/d5-bot-commit-identity.js +1 -1
- package/backend/detectors/megalodon/d6-date-anachronism.js +1 -1
- package/backend/detectors/megalodon/index.js +35 -25
- package/backend/detectors/mini-shai-hulud/d1-burst-publish.js +3 -1
- package/backend/detectors/mini-shai-hulud/d2-sibling-compromise.js +22 -10
- package/backend/detectors/mini-shai-hulud/d3-slsa-mismatch.js +30 -10
- package/backend/detectors/mini-shai-hulud/d4-maintainer-anomaly.js +17 -13
- package/backend/detectors/mini-shai-hulud/d5-ioc-check.js +12 -4
- package/backend/detectors/mini-shai-hulud/d6-token-exfil.js +6 -2
- package/backend/detectors/mini-shai-hulud/index.js +63 -26
- package/backend/detectors/msh-supplement/d2-persistence.js +30 -12
- package/backend/detectors/msh-supplement/d3-geo-killswitch.js +20 -8
- package/backend/detectors/msh-supplement/d4-c2-deaddrop.js +19 -5
- package/backend/detectors/msh-supplement/index.js +78 -63
- package/backend/detectors/node-ipc-compromise/d1-version-blocklist.js +4 -2
- package/backend/detectors/node-ipc-compromise/d10-unauthorized-publisher.js +9 -5
- package/backend/detectors/node-ipc-compromise/d11-blast-radius.js +7 -3
- package/backend/detectors/node-ipc-compromise/d2-tarball-hash.js +9 -4
- package/backend/detectors/node-ipc-compromise/d3-cjs-payload-injection.js +7 -5
- package/backend/detectors/node-ipc-compromise/d4-injected-payload-hash.js +4 -2
- package/backend/detectors/node-ipc-compromise/d5-dns-c2-pattern.js +13 -10
- package/backend/detectors/node-ipc-compromise/d7-dns-txt-exfil.js +3 -1
- package/backend/detectors/node-ipc-compromise/d8-runtime-trigger.js +5 -2
- package/backend/detectors/node-ipc-compromise/index.js +21 -15
- package/backend/detectors/tier1-binary-embed.js +138 -41
- package/backend/detectors/tier1-cloud-imds.js +57 -37
- package/backend/detectors/tier1-encrypted-c2.js +198 -0
- package/backend/detectors/tier1-infostealer.js +121 -68
- package/backend/detectors/tier1-lifecycle-hook.js +63 -23
- package/backend/detectors/tier1-maintainer-compromise.js +157 -0
- package/backend/detectors/tier1-metadata-spoof.js +92 -42
- package/backend/detectors/tier1-multistage-postinstall.js +46 -19
- package/backend/detectors/tier1-obfuscation-heuristics.js +184 -0
- package/backend/detectors/tier1-self-propagation.js +115 -0
- package/backend/detectors/tier1-slsa-attestation.js +12 -0
- package/backend/detectors/tier1-transitive-deps.js +182 -0
- package/backend/detectors/tier1-typosquat.js +129 -50
- package/backend/detectors/tier1-version-anomaly.js +223 -0
- package/backend/detectors/tier1-version-confusion.js +79 -59
- package/backend/detectors/trapdoor/d1-campaign-marker.js +3 -1
- package/backend/detectors/trapdoor/d2-payload-fingerprint.js +1 -1
- package/backend/detectors/trapdoor/d3-publisher-blocklist.js +4 -3
- package/backend/detectors/trapdoor/d4-gists-exfil.js +4 -2
- package/backend/detectors/trapdoor/d5-ai-poisoning.js +5 -3
- package/backend/detectors/trapdoor/d6-lure-name.js +12 -7
- package/backend/detectors/trapdoor/d7-crypto-primitives.js +2 -2
- package/backend/detectors/trapdoor/d8-xor-key.js +7 -2
- package/backend/detectors/trapdoor/d9-cred-validation.js +4 -5
- package/backend/detectors/trapdoor/index.js +19 -14
- package/backend/detectors/typosquat-vpmdhaj/d1-maintainer.js +32 -8
- package/backend/detectors/typosquat-vpmdhaj/d2-preinstall-loader.js +5 -3
- package/backend/detectors/typosquat-vpmdhaj/d3-cred-exfil.js +34 -12
- package/backend/detectors/typosquat-vpmdhaj/index.js +78 -59
- package/backend/detectors.test.js +147 -0
- package/backend/fetch.js +37 -29
- package/backend/index.js +1 -1
- package/backend/license.js +20 -4
- package/backend/lockfile.js +60 -36
- package/backend/pdf.js +107 -28
- package/backend/policy.js +183 -56
- package/backend/provenance.js +28 -3
- package/backend/report.js +136 -70
- package/backend/sbom.js +33 -27
- package/backend/scripts/analyze-false-positives.js +152 -0
- package/backend/scripts/analyze-validation.js +157 -0
- package/backend/scripts/detect-false-positives.js +103 -0
- package/backend/scripts/fetch-top-packages.js +277 -0
- package/backend/scripts/validate-d10-d13.js +103 -0
- package/backend/scripts/validate-detectors.js +151 -0
- package/backend/siem/cef.js +23 -21
- package/backend/siem/ecs.js +3 -3
- package/backend/siem/index.js +1 -1
- package/backend/siem/qradar.js +3 -3
- package/backend/siem/sentinel.js +2 -2
- package/backend/tests-d5-enhanced.test.js +47 -0
- package/backend/tests-d6-version-anomaly.test.js +67 -0
- package/backend/tests-d6.test.js +126 -0
- package/backend/tests-d6c.test.js +119 -0
- package/backend/tests-d7-obfuscation.test.js +88 -0
- package/backend/tests.test.js +997 -0
- package/backend/vsix-scan/detectors/activation-event-risk.js +36 -19
- package/backend/vsix-scan/detectors/burst-publish.js +14 -7
- package/backend/vsix-scan/detectors/exfil-pattern.js +7 -3
- package/backend/vsix-scan/detectors/known-ioc.js +23 -8
- package/backend/vsix-scan/detectors/orphan-commit-fetch.js +11 -7
- package/backend/vsix-scan/detectors/publisher-anomaly.js +24 -10
- package/backend/vsix-scan/index.js +97 -41
- package/backend/vsix-scan/marketplace-client.js +29 -13
- package/cli/cli.js +154 -64
- package/package.json +36 -10
- package/.dockerignore +0 -20
- package/.husky/pre-commit +0 -1
- package/SECURITY.md +0 -73
- package/deploy/helm/npm-scan/Chart.yaml +0 -22
- package/deploy/helm/npm-scan/templates/_helpers.tpl +0 -9
- package/deploy/helm/npm-scan/templates/api.yaml +0 -94
- package/deploy/helm/npm-scan/templates/ingress.yaml +0 -28
- package/deploy/helm/npm-scan/templates/postgresql.yaml +0 -67
- package/deploy/helm/npm-scan/templates/secrets.yaml +0 -19
- package/deploy/helm/npm-scan/templates/worker.yaml +0 -32
- package/deploy/helm/npm-scan/values.byoc.yaml +0 -75
- package/deploy/helm/npm-scan/values.yaml +0 -103
- package/scripts/download-corpus.js +0 -30
- package/scripts/gen-mal-corpus.js +0 -35
- package/scripts/generate-campaign-fixtures.js +0 -170
- package/src/config/top-5000.json +0 -87
- package/test/fixtures/lockfiles/npm-lock.json +0 -69
- package/test/fixtures/lockfiles/pnpm-lock.yaml +0 -118
- package/test/fixtures/lockfiles/yarn.lock +0 -104
- package/test/fixtures/mock-data.js +0 -69
|
@@ -2,7 +2,7 @@ import { KNOWN_HF_ORGS } from './known-orgs.js';
|
|
|
2
2
|
import { jaroWinkler } from './jaro-winkler.js';
|
|
3
3
|
import { simhash, similarity as simhashSimilarity } from './simhash.js';
|
|
4
4
|
|
|
5
|
-
const HF_URL_PATTERN = /(?:huggingface\.co|hf\.co)\/([
|
|
5
|
+
const HF_URL_PATTERN = /(?:huggingface\.co|hf\.co)\/([^/\s"'>]+)\/([^/\s"'>]+)/g;
|
|
6
6
|
const FROM_PRETRAINED_PATTERN = /from_pretrained\(\s*["']([^"']+\/[^"']+)["']/g;
|
|
7
7
|
const HUB_DOWNLOAD_SINGLE = /hub\.download\(\s*["']([^"']+\/[^"']+)["']/g;
|
|
8
8
|
const HUB_DOWNLOAD_DOUBLE = /hub\.download\(\s*["']([^"']+)["']\s*,\s*["']([^"']+)["']/g;
|
|
@@ -11,9 +11,15 @@ const LIFECYCLE_SCRIPTS = new Set(['postinstall', 'prepare', 'install']);
|
|
|
11
11
|
const API_BASE = 'https://huggingface.co';
|
|
12
12
|
|
|
13
13
|
const SEVERITY_SCORE = { none: 0, low: 1, medium: 2, high: 3, critical: 4 };
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
const HF_ARTIFACT_LIBS = new Set([
|
|
14
|
+
const _SEVERITY_LABELS = ['none', 'low', 'medium', 'high', 'critical'];
|
|
15
|
+
|
|
16
|
+
const HF_ARTIFACT_LIBS = new Set([
|
|
17
|
+
'transformers',
|
|
18
|
+
'diffusers',
|
|
19
|
+
'sentence-transformers',
|
|
20
|
+
'gguf',
|
|
21
|
+
'safetensors',
|
|
22
|
+
]);
|
|
17
23
|
const SUSPICIOUS_EXTENSIONS = /\.(exe|msi|bat|ps1|dll)$/i;
|
|
18
24
|
|
|
19
25
|
const _cache = new Map();
|
|
@@ -24,12 +30,12 @@ function severityIndex(sev) {
|
|
|
24
30
|
return SEVERITY_SCORE[sev] || 0;
|
|
25
31
|
}
|
|
26
32
|
|
|
27
|
-
function
|
|
33
|
+
function _maxSeverity(a, b) {
|
|
28
34
|
return severityIndex(a) >= severityIndex(b) ? a : b;
|
|
29
35
|
}
|
|
30
36
|
|
|
31
37
|
function sleep(ms) {
|
|
32
|
-
return new Promise(r => setTimeout(r, ms));
|
|
38
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
33
39
|
}
|
|
34
40
|
|
|
35
41
|
async function fetchWithCache(url) {
|
|
@@ -81,12 +87,16 @@ async function fetchReadme(url) {
|
|
|
81
87
|
const retryAfter = parseInt(res.headers.get('Retry-After') || '5', 10);
|
|
82
88
|
await sleep(retryAfter * 1000);
|
|
83
89
|
const retryRes = await fetch(url);
|
|
84
|
-
if (!retryRes.ok)
|
|
90
|
+
if (!retryRes.ok) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
85
93
|
const text = await retryRes.text();
|
|
86
94
|
_cache.set(url, { data: text, fetchedAt: Date.now() });
|
|
87
95
|
return text;
|
|
88
96
|
}
|
|
89
|
-
if (!res.ok)
|
|
97
|
+
if (!res.ok) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
90
100
|
const text = await res.text();
|
|
91
101
|
_cache.set(url, { data: text, fetchedAt: Date.now() });
|
|
92
102
|
return text;
|
|
@@ -115,7 +125,9 @@ function extractHFTuples(pkgJson, allFiles) {
|
|
|
115
125
|
const scripts = pkgJson?.scripts || {};
|
|
116
126
|
let m;
|
|
117
127
|
for (const [hook, script] of Object.entries(scripts)) {
|
|
118
|
-
if (typeof script !== 'string')
|
|
128
|
+
if (typeof script !== 'string') {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
119
131
|
|
|
120
132
|
HF_URL_PATTERN.lastIndex = 0;
|
|
121
133
|
while ((m = HF_URL_PATTERN.exec(script)) !== null) {
|
|
@@ -152,7 +164,9 @@ function extractHFTuples(pkgJson, allFiles) {
|
|
|
152
164
|
|
|
153
165
|
if (allFiles) {
|
|
154
166
|
for (const file of allFiles) {
|
|
155
|
-
if (!file.path?.match(/\.(js|ts|jsx|tsx|mjs|cjs)$/i))
|
|
167
|
+
if (!file.path?.match(/\.(js|ts|jsx|tsx|mjs|cjs)$/i)) {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
156
170
|
const content = typeof file.content === 'string' ? file.content : '';
|
|
157
171
|
|
|
158
172
|
HF_URL_PATTERN.lastIndex = 0;
|
|
@@ -180,7 +194,15 @@ function extractHFTuples(pkgJson, allFiles) {
|
|
|
180
194
|
return { tuples, postinstallFetchFlag };
|
|
181
195
|
}
|
|
182
196
|
|
|
183
|
-
function buildHFOrgSpoofFinding(
|
|
197
|
+
function buildHFOrgSpoofFinding(
|
|
198
|
+
referencedRepo,
|
|
199
|
+
org,
|
|
200
|
+
canonicalOrg,
|
|
201
|
+
similarityScore,
|
|
202
|
+
postinstallFetchFlag,
|
|
203
|
+
tags,
|
|
204
|
+
hfMeta
|
|
205
|
+
) {
|
|
184
206
|
const finding = {
|
|
185
207
|
id: 'HF_ORG_SPOOF',
|
|
186
208
|
severity: 'high',
|
|
@@ -207,16 +229,22 @@ function buildHFOrgSpoofFinding(referencedRepo, org, canonicalOrg, similaritySco
|
|
|
207
229
|
async function runStage2(spoofFindings, orgsToCheck, postinstallFetchFlag) {
|
|
208
230
|
const newFindings = [];
|
|
209
231
|
|
|
210
|
-
for (const [
|
|
232
|
+
for (const [
|
|
233
|
+
referencedRepo,
|
|
234
|
+
{ org, canonicalOrg, similarityScore: _similarityScore, finding: _finding },
|
|
235
|
+
] of orgsToCheck) {
|
|
211
236
|
const tags = [];
|
|
212
237
|
let hfMeta = null;
|
|
213
238
|
|
|
214
239
|
const modelUrl = `${API_BASE}/api/models/${referencedRepo}`;
|
|
215
|
-
const canonicalUrl =
|
|
240
|
+
const canonicalUrl =
|
|
241
|
+
canonicalOrg.org !== org
|
|
242
|
+
? `${API_BASE}/api/models/${canonicalOrg.org}/${referencedRepo.split('/')[1]}`
|
|
243
|
+
: null;
|
|
216
244
|
const userUrl = `${API_BASE}/api/users/${org}`;
|
|
217
245
|
|
|
218
246
|
const spoofedModel = await fetchWithCache(modelUrl);
|
|
219
|
-
const
|
|
247
|
+
const _canonicalModel = canonicalUrl ? await fetchWithCache(canonicalUrl) : null;
|
|
220
248
|
const userData = await fetchWithCache(userUrl);
|
|
221
249
|
|
|
222
250
|
// Org age check for NEW_ORG tag
|
|
@@ -235,7 +263,9 @@ async function runStage2(spoofFindings, orgsToCheck, postinstallFetchFlag) {
|
|
|
235
263
|
// README clone check
|
|
236
264
|
if (canonicalOrg.org !== org) {
|
|
237
265
|
const readmeSpoof = await fetchReadme(`${API_BASE}/${referencedRepo}/resolve/main/README.md`);
|
|
238
|
-
const readmeCanonical = await fetchReadme(
|
|
266
|
+
const readmeCanonical = await fetchReadme(
|
|
267
|
+
`${API_BASE}/${canonicalOrg.org}/${referencedRepo.split('/')[1]}/resolve/main/README.md`
|
|
268
|
+
);
|
|
239
269
|
|
|
240
270
|
if (readmeSpoof && readmeCanonical) {
|
|
241
271
|
const fp1 = simhash(readmeSpoof);
|
|
@@ -260,7 +290,9 @@ async function runStage2(spoofFindings, orgsToCheck, postinstallFetchFlag) {
|
|
|
260
290
|
tags: [],
|
|
261
291
|
ipiClass: 'SUPPLY_CHAIN',
|
|
262
292
|
};
|
|
263
|
-
if (hfMeta)
|
|
293
|
+
if (hfMeta) {
|
|
294
|
+
readmeFinding.hfMeta = hfMeta;
|
|
295
|
+
}
|
|
264
296
|
newFindings.push(readmeFinding);
|
|
265
297
|
}
|
|
266
298
|
}
|
|
@@ -288,7 +320,9 @@ async function runStage2(spoofFindings, orgsToCheck, postinstallFetchFlag) {
|
|
|
288
320
|
tags: [],
|
|
289
321
|
ipiClass: 'SUPPLY_CHAIN',
|
|
290
322
|
};
|
|
291
|
-
if (hfMeta)
|
|
323
|
+
if (hfMeta) {
|
|
324
|
+
artifactFinding.hfMeta = hfMeta;
|
|
325
|
+
}
|
|
292
326
|
newFindings.push(artifactFinding);
|
|
293
327
|
break;
|
|
294
328
|
}
|
|
@@ -297,12 +331,16 @@ async function runStage2(spoofFindings, orgsToCheck, postinstallFetchFlag) {
|
|
|
297
331
|
}
|
|
298
332
|
|
|
299
333
|
// Apply NEW_ORG and POSTINSTALL_FETCH tags to all findings for this repo
|
|
300
|
-
const repoSpoofFindings = spoofFindings.filter(f => f.referencedRepo === referencedRepo);
|
|
334
|
+
const repoSpoofFindings = spoofFindings.filter((f) => f.referencedRepo === referencedRepo);
|
|
301
335
|
for (const sf of repoSpoofFindings) {
|
|
302
336
|
if (tags.length > 0) {
|
|
303
|
-
if (!sf.tags)
|
|
337
|
+
if (!sf.tags) {
|
|
338
|
+
sf.tags = [];
|
|
339
|
+
}
|
|
304
340
|
for (const t of tags) {
|
|
305
|
-
if (!sf.tags.includes(t))
|
|
341
|
+
if (!sf.tags.includes(t)) {
|
|
342
|
+
sf.tags.push(t);
|
|
343
|
+
}
|
|
306
344
|
}
|
|
307
345
|
}
|
|
308
346
|
if (hfMeta) {
|
|
@@ -312,9 +350,13 @@ async function runStage2(spoofFindings, orgsToCheck, postinstallFetchFlag) {
|
|
|
312
350
|
for (const nf of newFindings) {
|
|
313
351
|
if (nf.referencedRepo === referencedRepo) {
|
|
314
352
|
if (tags.length > 0) {
|
|
315
|
-
if (!nf.tags)
|
|
353
|
+
if (!nf.tags) {
|
|
354
|
+
nf.tags = [];
|
|
355
|
+
}
|
|
316
356
|
for (const t of tags) {
|
|
317
|
-
if (!nf.tags.includes(t))
|
|
357
|
+
if (!nf.tags.includes(t)) {
|
|
358
|
+
nf.tags.push(t);
|
|
359
|
+
}
|
|
318
360
|
}
|
|
319
361
|
}
|
|
320
362
|
}
|
|
@@ -326,14 +368,18 @@ async function runStage2(spoofFindings, orgsToCheck, postinstallFetchFlag) {
|
|
|
326
368
|
const allStage2Findings = [...spoofFindings, ...newFindings];
|
|
327
369
|
const escalatedRepos = new Set();
|
|
328
370
|
for (const f of allStage2Findings) {
|
|
329
|
-
if (f.referencedRepo)
|
|
371
|
+
if (f.referencedRepo) {
|
|
372
|
+
escalatedRepos.add(f.referencedRepo);
|
|
373
|
+
}
|
|
330
374
|
}
|
|
331
375
|
for (const f of allStage2Findings) {
|
|
332
376
|
if (escalatedRepos.has(f.referencedRepo)) {
|
|
333
377
|
if (severityIndex(f.severity) < severityIndex('critical')) {
|
|
334
378
|
f.severity = 'critical';
|
|
335
379
|
}
|
|
336
|
-
if (!f.tags)
|
|
380
|
+
if (!f.tags) {
|
|
381
|
+
f.tags = [];
|
|
382
|
+
}
|
|
337
383
|
if (!f.tags.includes('POSTINSTALL_ESCALATED')) {
|
|
338
384
|
f.tags.push('POSTINSTALL_ESCALATED');
|
|
339
385
|
}
|
|
@@ -344,10 +390,12 @@ async function runStage2(spoofFindings, orgsToCheck, postinstallFetchFlag) {
|
|
|
344
390
|
return newFindings;
|
|
345
391
|
}
|
|
346
392
|
|
|
347
|
-
export async function scan(pkgJson, files = [],
|
|
393
|
+
export async function scan(pkgJson, files = [], _registryMeta = null, allFiles = null) {
|
|
348
394
|
const { tuples, postinstallFetchFlag } = extractHFTuples(pkgJson, allFiles || files);
|
|
349
395
|
|
|
350
|
-
if (tuples.size === 0)
|
|
396
|
+
if (tuples.size === 0) {
|
|
397
|
+
return [];
|
|
398
|
+
}
|
|
351
399
|
|
|
352
400
|
// Stage 1: org spoof detection (local only)
|
|
353
401
|
const spoofFindings = [];
|
|
@@ -355,19 +403,34 @@ export async function scan(pkgJson, files = [], registryMeta = null, allFiles =
|
|
|
355
403
|
|
|
356
404
|
for (const tuple of tuples) {
|
|
357
405
|
const parts = tuple.split('/');
|
|
358
|
-
if (parts.length < 2)
|
|
406
|
+
if (parts.length < 2) {
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
359
409
|
const org = parts[0];
|
|
360
410
|
|
|
361
411
|
const canonicalOrg = findClosestOrg(org);
|
|
362
|
-
if (!canonicalOrg.org)
|
|
363
|
-
|
|
412
|
+
if (!canonicalOrg.org) {
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
if (org.toLowerCase() === canonicalOrg.org.toLowerCase()) {
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
364
418
|
|
|
365
|
-
const finding = buildHFOrgSpoofFinding(
|
|
419
|
+
const finding = buildHFOrgSpoofFinding(
|
|
420
|
+
tuple,
|
|
421
|
+
org,
|
|
422
|
+
canonicalOrg,
|
|
423
|
+
canonicalOrg.score,
|
|
424
|
+
postinstallFetchFlag,
|
|
425
|
+
[]
|
|
426
|
+
);
|
|
366
427
|
spoofFindings.push(finding);
|
|
367
428
|
orgsToCheck.push([tuple, { org, canonicalOrg, similarityScore: canonicalOrg.score, finding }]);
|
|
368
429
|
}
|
|
369
430
|
|
|
370
|
-
if (spoofFindings.length === 0)
|
|
431
|
+
if (spoofFindings.length === 0) {
|
|
432
|
+
return [];
|
|
433
|
+
}
|
|
371
434
|
|
|
372
435
|
// Stage 2: network checks
|
|
373
436
|
const stage2Findings = await runStage2(spoofFindings, orgsToCheck, postinstallFetchFlag);
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
export function jaroWinkler(s1, s2) {
|
|
2
|
-
if (s1 === s2)
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
if (s1 === s2) {
|
|
3
|
+
return 1;
|
|
4
|
+
}
|
|
5
|
+
const len1 = s1.length,
|
|
6
|
+
len2 = s2.length;
|
|
7
|
+
if (len1 === 0 || len2 === 0) {
|
|
8
|
+
return 0;
|
|
9
|
+
}
|
|
5
10
|
|
|
6
11
|
const matchDist = Math.floor(Math.max(len1, len2) / 2) - 1;
|
|
7
12
|
const matches1 = new Array(len1).fill(false);
|
|
@@ -12,8 +17,12 @@ export function jaroWinkler(s1, s2) {
|
|
|
12
17
|
const start = Math.max(0, i - matchDist);
|
|
13
18
|
const end = Math.min(len2, i + matchDist + 1);
|
|
14
19
|
for (let j = start; j < end; j++) {
|
|
15
|
-
if (matches2[j])
|
|
16
|
-
|
|
20
|
+
if (matches2[j]) {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (s1[i] !== s2[j]) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
17
26
|
matches1[i] = true;
|
|
18
27
|
matches2[j] = true;
|
|
19
28
|
matches++;
|
|
@@ -21,13 +30,22 @@ export function jaroWinkler(s1, s2) {
|
|
|
21
30
|
}
|
|
22
31
|
}
|
|
23
32
|
|
|
24
|
-
if (matches === 0)
|
|
33
|
+
if (matches === 0) {
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
25
36
|
|
|
26
|
-
let transpositions = 0,
|
|
37
|
+
let transpositions = 0,
|
|
38
|
+
k = 0;
|
|
27
39
|
for (let i = 0; i < len1; i++) {
|
|
28
|
-
if (!matches1[i])
|
|
29
|
-
|
|
30
|
-
|
|
40
|
+
if (!matches1[i]) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
while (!matches2[k]) {
|
|
44
|
+
k++;
|
|
45
|
+
}
|
|
46
|
+
if (s1[i] !== s2[k]) {
|
|
47
|
+
transpositions++;
|
|
48
|
+
}
|
|
31
49
|
k++;
|
|
32
50
|
}
|
|
33
51
|
|
|
@@ -36,8 +54,11 @@ export function jaroWinkler(s1, s2) {
|
|
|
36
54
|
let prefix = 0;
|
|
37
55
|
const maxPrefix = Math.min(4, len1, len2);
|
|
38
56
|
for (let i = 0; i < maxPrefix; i++) {
|
|
39
|
-
if (s1[i] === s2[i])
|
|
40
|
-
|
|
57
|
+
if (s1[i] === s2[i]) {
|
|
58
|
+
prefix++;
|
|
59
|
+
} else {
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
41
62
|
}
|
|
42
63
|
|
|
43
64
|
return jaro + prefix * 0.1 * (1 - jaro);
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
export const KNOWN_HF_ORGS = [
|
|
2
|
-
'openai',
|
|
3
|
-
'
|
|
4
|
-
'
|
|
2
|
+
'openai',
|
|
3
|
+
'meta-llama',
|
|
4
|
+
'mistralai',
|
|
5
|
+
'google',
|
|
6
|
+
'microsoft',
|
|
7
|
+
'stabilityai',
|
|
8
|
+
'EleutherAI',
|
|
9
|
+
'huggingface',
|
|
10
|
+
'tiiuae',
|
|
11
|
+
'cohere',
|
|
12
|
+
'anthropic',
|
|
13
|
+
'deepseek-ai',
|
|
14
|
+
'Qwen',
|
|
15
|
+
'NousResearch',
|
|
16
|
+
'teknium',
|
|
5
17
|
];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
function hashToken(str) {
|
|
2
2
|
let hash = 5381;
|
|
3
3
|
for (let i = 0; i < str.length; i++) {
|
|
4
|
-
hash = (
|
|
4
|
+
hash = (hash << 5) + hash + str.charCodeAt(i);
|
|
5
5
|
hash = hash & hash;
|
|
6
6
|
}
|
|
7
7
|
return hash >>> 0;
|
|
@@ -25,7 +25,7 @@ export function simhash(text) {
|
|
|
25
25
|
let fingerprint = 0n;
|
|
26
26
|
for (let i = 0; i < 64; i++) {
|
|
27
27
|
if (v[i] > 0) {
|
|
28
|
-
fingerprint |=
|
|
28
|
+
fingerprint |= 1n << BigInt(i);
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
return fingerprint;
|
|
@@ -26,9 +26,18 @@ import { scan as tier1MetadataSpoofScan } from './tier1-metadata-spoof.js';
|
|
|
26
26
|
import { scan as tier1VersionConfusionScan } from './tier1-version-confusion.js';
|
|
27
27
|
import { scan as tier1CloudImdsScan } from './tier1-cloud-imds.js';
|
|
28
28
|
import { scan as tier1MultistagePostinstallScan } from './tier1-multistage-postinstall.js';
|
|
29
|
+
import { scan as tier1VersionAnomalyScan } from './tier1-version-anomaly.js';
|
|
30
|
+
import { scan as tier1ObfuscationHeuristicsScan } from './tier1-obfuscation-heuristics.js';
|
|
31
|
+
import { scan as tier1SlsaAttestationScan } from './tier1-slsa-attestation.js';
|
|
32
|
+
import { scan as tier1SelfPropagationScan } from './tier1-self-propagation.js';
|
|
33
|
+
import { scan as tier1EncryptedC2Scan } from './tier1-encrypted-c2.js';
|
|
34
|
+
import { scan as tier1TransitiveDepsScan } from './tier1-transitive-deps.js';
|
|
35
|
+
import { scan as tier1MaintainerCompromiseScan } from './tier1-maintainer-compromise.js';
|
|
29
36
|
|
|
30
37
|
function timeout(ms) {
|
|
31
|
-
return new Promise((_, reject) =>
|
|
38
|
+
return new Promise((_, reject) =>
|
|
39
|
+
setTimeout(() => reject(new Error(`timeout after ${ms}ms`)), ms)
|
|
40
|
+
);
|
|
32
41
|
}
|
|
33
42
|
|
|
34
43
|
async function runTier1(name, scanFn, pkgJson, files, registryMeta, allFiles) {
|
|
@@ -40,7 +49,9 @@ async function runTier1(name, scanFn, pkgJson, files, registryMeta, allFiles) {
|
|
|
40
49
|
const fileCount = allFiles && allFiles.length > 0 ? allFiles.length : files.length;
|
|
41
50
|
if (fileCount >= 10 && result.length > 0) {
|
|
42
51
|
const hitRate = result.length / fileCount;
|
|
43
|
-
if (hitRate > 0.8)
|
|
52
|
+
if (hitRate > 0.8) {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
44
55
|
}
|
|
45
56
|
return result;
|
|
46
57
|
} catch {
|
|
@@ -50,33 +61,175 @@ async function runTier1(name, scanFn, pkgJson, files, registryMeta, allFiles) {
|
|
|
50
61
|
|
|
51
62
|
export async function runAll(pkgJson, files = [], registryMeta = null, allFiles = null) {
|
|
52
63
|
const findings = [];
|
|
53
|
-
findings.push(...await atk001.scan(pkgJson, files));
|
|
54
|
-
findings.push(...await atk002.scan(pkgJson, files));
|
|
55
|
-
findings.push(...await atk003.scan(pkgJson, files));
|
|
56
|
-
findings.push(...await atk004.scan(pkgJson, files));
|
|
57
|
-
findings.push(...await atk005.scan(pkgJson, files));
|
|
58
|
-
findings.push(...await atk006.scan(pkgJson, files));
|
|
59
|
-
findings.push(...await atk007.scan(pkgJson, files));
|
|
60
|
-
findings.push(...await atk008.scan(pkgJson, files));
|
|
61
|
-
findings.push(...await atk009.scan(pkgJson, files));
|
|
62
|
-
findings.push(...await atk010.scan(pkgJson, files));
|
|
63
|
-
findings.push(...await atk011.scan(pkgJson, files));
|
|
64
|
-
findings.push(...await megalodonScan(pkgJson, allFiles || files, registryMeta));
|
|
65
|
-
findings.push(...await hfScan(pkgJson, files, registryMeta, allFiles || files));
|
|
66
|
-
findings.push(...await miniShaiHuludScan(pkgJson, files, registryMeta, allFiles || files));
|
|
67
|
-
findings.push(...await badhostScan(pkgJson, files, registryMeta, allFiles || files));
|
|
68
|
-
findings.push(...await trapdoorScan(pkgJson, files, registryMeta, allFiles || files));
|
|
69
|
-
findings.push(...await nodeIpcScan(pkgJson, files, registryMeta, allFiles || files));
|
|
70
|
-
findings.push(...await mshSupplementScan(pkgJson, files, registryMeta, allFiles || files));
|
|
71
|
-
findings.push(...await typosquatScan(pkgJson, files, registryMeta, allFiles || files));
|
|
72
|
-
findings.push(...await axiosPoisoningScan(pkgJson, files, registryMeta, allFiles || files));
|
|
73
|
-
findings.push(
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
64
|
+
findings.push(...(await atk001.scan(pkgJson, files)));
|
|
65
|
+
findings.push(...(await atk002.scan(pkgJson, files)));
|
|
66
|
+
findings.push(...(await atk003.scan(pkgJson, files)));
|
|
67
|
+
findings.push(...(await atk004.scan(pkgJson, files)));
|
|
68
|
+
findings.push(...(await atk005.scan(pkgJson, files)));
|
|
69
|
+
findings.push(...(await atk006.scan(pkgJson, files)));
|
|
70
|
+
findings.push(...(await atk007.scan(pkgJson, files)));
|
|
71
|
+
findings.push(...(await atk008.scan(pkgJson, files)));
|
|
72
|
+
findings.push(...(await atk009.scan(pkgJson, files)));
|
|
73
|
+
findings.push(...(await atk010.scan(pkgJson, files)));
|
|
74
|
+
findings.push(...(await atk011.scan(pkgJson, files)));
|
|
75
|
+
findings.push(...(await megalodonScan(pkgJson, allFiles || files, registryMeta)));
|
|
76
|
+
findings.push(...(await hfScan(pkgJson, files, registryMeta, allFiles || files)));
|
|
77
|
+
findings.push(...(await miniShaiHuludScan(pkgJson, files, registryMeta, allFiles || files)));
|
|
78
|
+
findings.push(...(await badhostScan(pkgJson, files, registryMeta, allFiles || files)));
|
|
79
|
+
findings.push(...(await trapdoorScan(pkgJson, files, registryMeta, allFiles || files)));
|
|
80
|
+
findings.push(...(await nodeIpcScan(pkgJson, files, registryMeta, allFiles || files)));
|
|
81
|
+
findings.push(...(await mshSupplementScan(pkgJson, files, registryMeta, allFiles || files)));
|
|
82
|
+
findings.push(...(await typosquatScan(pkgJson, files, registryMeta, allFiles || files)));
|
|
83
|
+
findings.push(...(await axiosPoisoningScan(pkgJson, files, registryMeta, allFiles || files)));
|
|
84
|
+
findings.push(
|
|
85
|
+
...(await runTier1(
|
|
86
|
+
'tier1-typosquat',
|
|
87
|
+
tier1TyposquatScan,
|
|
88
|
+
pkgJson,
|
|
89
|
+
files,
|
|
90
|
+
registryMeta,
|
|
91
|
+
allFiles || files
|
|
92
|
+
))
|
|
93
|
+
);
|
|
94
|
+
findings.push(
|
|
95
|
+
...(await runTier1(
|
|
96
|
+
'tier1-infostealer',
|
|
97
|
+
tier1InfostealerScan,
|
|
98
|
+
pkgJson,
|
|
99
|
+
files,
|
|
100
|
+
registryMeta,
|
|
101
|
+
allFiles || files
|
|
102
|
+
))
|
|
103
|
+
);
|
|
104
|
+
findings.push(
|
|
105
|
+
...(await runTier1(
|
|
106
|
+
'tier1-lifecycle-hook',
|
|
107
|
+
tier1LifecycleHookScan,
|
|
108
|
+
pkgJson,
|
|
109
|
+
files,
|
|
110
|
+
registryMeta,
|
|
111
|
+
allFiles || files
|
|
112
|
+
))
|
|
113
|
+
);
|
|
114
|
+
findings.push(
|
|
115
|
+
...(await runTier1(
|
|
116
|
+
'tier1-binary-embed',
|
|
117
|
+
tier1BinaryEmbedScan,
|
|
118
|
+
pkgJson,
|
|
119
|
+
files,
|
|
120
|
+
registryMeta,
|
|
121
|
+
allFiles || files
|
|
122
|
+
))
|
|
123
|
+
);
|
|
124
|
+
findings.push(
|
|
125
|
+
...(await runTier1(
|
|
126
|
+
'tier1-metadata-spoof',
|
|
127
|
+
tier1MetadataSpoofScan,
|
|
128
|
+
pkgJson,
|
|
129
|
+
files,
|
|
130
|
+
registryMeta,
|
|
131
|
+
allFiles || files
|
|
132
|
+
))
|
|
133
|
+
);
|
|
134
|
+
findings.push(
|
|
135
|
+
...(await runTier1(
|
|
136
|
+
'tier1-version-confusion',
|
|
137
|
+
tier1VersionConfusionScan,
|
|
138
|
+
pkgJson,
|
|
139
|
+
files,
|
|
140
|
+
registryMeta,
|
|
141
|
+
allFiles || files
|
|
142
|
+
))
|
|
143
|
+
);
|
|
144
|
+
findings.push(
|
|
145
|
+
...(await runTier1(
|
|
146
|
+
'tier1-cloud-imds',
|
|
147
|
+
tier1CloudImdsScan,
|
|
148
|
+
pkgJson,
|
|
149
|
+
files,
|
|
150
|
+
registryMeta,
|
|
151
|
+
allFiles || files
|
|
152
|
+
))
|
|
153
|
+
);
|
|
154
|
+
findings.push(
|
|
155
|
+
...(await runTier1(
|
|
156
|
+
'tier1-multistage-postinstall',
|
|
157
|
+
tier1MultistagePostinstallScan,
|
|
158
|
+
pkgJson,
|
|
159
|
+
files,
|
|
160
|
+
registryMeta,
|
|
161
|
+
allFiles || files
|
|
162
|
+
))
|
|
163
|
+
);
|
|
164
|
+
findings.push(
|
|
165
|
+
...(await runTier1(
|
|
166
|
+
'tier1-version-anomaly',
|
|
167
|
+
tier1VersionAnomalyScan,
|
|
168
|
+
pkgJson,
|
|
169
|
+
files,
|
|
170
|
+
registryMeta,
|
|
171
|
+
allFiles || files
|
|
172
|
+
))
|
|
173
|
+
);
|
|
174
|
+
findings.push(
|
|
175
|
+
...(await runTier1(
|
|
176
|
+
'tier1-obfuscation-heuristics',
|
|
177
|
+
tier1ObfuscationHeuristicsScan,
|
|
178
|
+
pkgJson,
|
|
179
|
+
files,
|
|
180
|
+
registryMeta,
|
|
181
|
+
allFiles || files
|
|
182
|
+
))
|
|
183
|
+
);
|
|
184
|
+
findings.push(
|
|
185
|
+
...(await runTier1(
|
|
186
|
+
'tier1-slsa-attestation',
|
|
187
|
+
tier1SlsaAttestationScan,
|
|
188
|
+
pkgJson,
|
|
189
|
+
files,
|
|
190
|
+
registryMeta,
|
|
191
|
+
allFiles || files
|
|
192
|
+
))
|
|
193
|
+
);
|
|
194
|
+
findings.push(
|
|
195
|
+
...(await runTier1(
|
|
196
|
+
'tier1-self-propagation',
|
|
197
|
+
tier1SelfPropagationScan,
|
|
198
|
+
pkgJson,
|
|
199
|
+
files,
|
|
200
|
+
registryMeta,
|
|
201
|
+
allFiles || files
|
|
202
|
+
))
|
|
203
|
+
);
|
|
204
|
+
findings.push(
|
|
205
|
+
...(await runTier1(
|
|
206
|
+
'tier1-encrypted-c2',
|
|
207
|
+
tier1EncryptedC2Scan,
|
|
208
|
+
pkgJson,
|
|
209
|
+
files,
|
|
210
|
+
registryMeta,
|
|
211
|
+
allFiles || files
|
|
212
|
+
))
|
|
213
|
+
);
|
|
214
|
+
findings.push(
|
|
215
|
+
...(await runTier1(
|
|
216
|
+
'tier1-transitive-deps',
|
|
217
|
+
tier1TransitiveDepsScan,
|
|
218
|
+
pkgJson,
|
|
219
|
+
files,
|
|
220
|
+
registryMeta,
|
|
221
|
+
allFiles || files
|
|
222
|
+
))
|
|
223
|
+
);
|
|
224
|
+
findings.push(
|
|
225
|
+
...(await runTier1(
|
|
226
|
+
'tier1-maintainer-compromise',
|
|
227
|
+
tier1MaintainerCompromiseScan,
|
|
228
|
+
pkgJson,
|
|
229
|
+
files,
|
|
230
|
+
registryMeta,
|
|
231
|
+
allFiles || files
|
|
232
|
+
))
|
|
233
|
+
);
|
|
81
234
|
return findings.sort((a, b) => b.severity.localeCompare(a.severity));
|
|
82
|
-
}
|
|
235
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const PATTERNS = [
|
|
2
|
+
{ id: 'EVAL_USAGE', re: /\beval\s*\(/ },
|
|
3
|
+
{ id: 'FUNCTION_CONSTRUCTOR', re: /Function\s*\(/ },
|
|
4
|
+
{
|
|
5
|
+
id: 'STRING_REVERSAL_CHAIN',
|
|
6
|
+
re: /\.split\s*\(\s*['"]\s*['"]\s*\)\s*\.reverse\s*\(\s*\)\s*\.join\s*\(/,
|
|
7
|
+
},
|
|
8
|
+
{ id: 'XOR_CIPHER', re: /charCodeAt\s*\([^)]*\)\s*\^\s*\w+/ },
|
|
9
|
+
{ id: 'BITWISE_LOOP', re: /for\s*\([^;]+;[^;]+\)\s*\{[^}]{20,}\^[^}]*\}/ },
|
|
10
|
+
{ id: 'DYNAMIC_REQUIRE', re: /require\s*\(\s*(?:Buffer\.from|atob|decodeURIComponent)/ },
|
|
11
|
+
{ id: 'BASE64_LITERAL', re: /['"][A-Za-z0-9+/]{60,}={0,2}['"]/ },
|
|
12
|
+
{ id: 'OBFUSCATED_STRING', re: /(?:\\x[0-9a-fA-F]{2}){8,}/ },
|
|
13
|
+
{ id: 'UNICODE_ESCAPE', re: /(?:\\u[0-9a-fA-F]{4}){8,}/ },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
export function detectPatterns(code) {
|
|
17
|
+
const detected = [];
|
|
18
|
+
for (const { id, re } of PATTERNS) {
|
|
19
|
+
if (re.test(code)) {
|
|
20
|
+
detected.push(id);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return detected;
|
|
24
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export function shannonEntropy(str) {
|
|
2
|
+
const len = str.length;
|
|
3
|
+
if (len === 0) {
|
|
4
|
+
return 0;
|
|
5
|
+
}
|
|
6
|
+
const freq = {};
|
|
7
|
+
for (const ch of str) {
|
|
8
|
+
freq[ch] = (freq[ch] || 0) + 1;
|
|
9
|
+
}
|
|
10
|
+
let entropy = 0;
|
|
11
|
+
for (const count of Object.values(freq)) {
|
|
12
|
+
const p = count / len;
|
|
13
|
+
entropy -= p * Math.log2(p);
|
|
14
|
+
}
|
|
15
|
+
return Math.round(entropy * 100) / 100;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function isMinified(code) {
|
|
19
|
+
if (code.length < 100) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
const lines = code.split('\n');
|
|
23
|
+
if (lines.length <= 3 && code.length > 1000) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
const tokens = code.match(/\b[a-zA-Z_$][\w$]*\b/g) || [];
|
|
27
|
+
if (tokens.length < 10) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
const avgLen = tokens.reduce((s, t) => s + t.length, 0) / tokens.length;
|
|
31
|
+
return avgLen < 3;
|
|
32
|
+
}
|