@lateos/npm-scan 1.0.0 → 1.1.1
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.de.md +3 -98
- package/README.fr.md +3 -98
- package/README.ja.md +3 -98
- package/README.md +2 -122
- package/README.zh.md +3 -98
- package/backend/cra.js +113 -21
- 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 +48 -3
- 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 +181 -34
- package/backend/detectors/lib/ast-patterns.js +4 -1
- package/backend/detectors/lib/entropy-analyzer.js +12 -4
- 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 +109 -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 +45 -17
- package/backend/detectors/tier1-self-propagation.js +115 -0
- package/backend/detectors/tier1-slsa-attestation.js +1 -1
- package/backend/detectors/tier1-transitive-deps.js +182 -0
- package/backend/detectors/tier1-typosquat.js +129 -50
- package/backend/detectors/tier1-version-anomaly.js +77 -41
- 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 +78 -19
- 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 +14 -8
- package/backend/scripts/analyze-validation.js +27 -21
- package/backend/scripts/detect-false-positives.js +20 -10
- package/backend/scripts/fetch-top-packages.js +197 -49
- package/backend/scripts/validate-d10-d13.js +103 -0
- package/backend/scripts/validate-detectors.js +26 -17
- 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 +13 -12
- package/backend/tests-d6-version-anomaly.test.js +17 -8
- package/backend/tests-d6.test.js +24 -14
- package/backend/tests-d6c.test.js +27 -14
- package/backend/tests-d7-obfuscation.test.js +9 -12
- package/backend/tests.test.js +182 -83
- 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 +12 -3
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { transitiveDependencyFinding } from './findings.js';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
parseRequirementsTxt,
|
|
4
|
+
parsePyprojectToml,
|
|
5
|
+
parsePoetryLock,
|
|
6
|
+
parsePipfile,
|
|
7
|
+
parseSetupPy,
|
|
8
|
+
parseSetupCfg,
|
|
9
|
+
} from './manifest.js';
|
|
3
10
|
|
|
4
11
|
const TIER_1_PACKAGES = [
|
|
5
12
|
'fastapi',
|
|
@@ -22,29 +29,41 @@ const TIER_2_PACKAGES = [
|
|
|
22
29
|
];
|
|
23
30
|
|
|
24
31
|
function normalizePkgName(name) {
|
|
25
|
-
return name
|
|
32
|
+
return name
|
|
33
|
+
.replace(/["'[\]]/g, '')
|
|
34
|
+
.trim()
|
|
35
|
+
.toLowerCase();
|
|
26
36
|
}
|
|
27
37
|
|
|
28
38
|
function findPackagesInManifests(allFiles) {
|
|
29
39
|
const packages = new Set();
|
|
30
40
|
|
|
31
|
-
for (const file of
|
|
41
|
+
for (const file of allFiles || []) {
|
|
32
42
|
const content = typeof file.content === 'string' ? file.content : '';
|
|
33
|
-
if (!content)
|
|
43
|
+
if (!content) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
34
46
|
const path = file.path || '';
|
|
35
47
|
|
|
36
|
-
|
|
48
|
+
const deps = [];
|
|
37
49
|
|
|
38
50
|
if (path === 'requirements.txt' || path.match(/^requirements\/.*\.txt$/)) {
|
|
39
51
|
const lines = content.split('\n');
|
|
40
52
|
for (const line of lines) {
|
|
41
53
|
const trimmed = line.trim();
|
|
42
|
-
if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('-'))
|
|
54
|
+
if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('-')) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
43
57
|
const idx = trimmed.indexOf('#');
|
|
44
58
|
const spec = idx >= 0 ? trimmed.slice(0, idx).trim() : trimmed;
|
|
45
59
|
const eqIdx = spec.indexOf('==');
|
|
46
60
|
const geIdx = spec.indexOf('>=');
|
|
47
|
-
const name =
|
|
61
|
+
const name =
|
|
62
|
+
eqIdx >= 0
|
|
63
|
+
? spec.slice(0, eqIdx).trim()
|
|
64
|
+
: geIdx >= 0
|
|
65
|
+
? spec.slice(0, geIdx).trim()
|
|
66
|
+
: spec;
|
|
48
67
|
if (name && !name.includes('=') && !name.includes('<') && !name.includes('>')) {
|
|
49
68
|
deps.push(normalizePkgName(name));
|
|
50
69
|
}
|
|
@@ -52,11 +71,17 @@ function findPackagesInManifests(allFiles) {
|
|
|
52
71
|
} else if (path === 'pyproject.toml') {
|
|
53
72
|
try {
|
|
54
73
|
const obj = JSON.parse(content);
|
|
55
|
-
const allDeps = {
|
|
74
|
+
const allDeps = {
|
|
75
|
+
...(obj?.tool?.poetry?.dependencies || {}),
|
|
76
|
+
...(obj?.dependencies || {}),
|
|
77
|
+
...(obj?.['dev-dependencies'] || {}),
|
|
78
|
+
};
|
|
56
79
|
for (const key of Object.keys(allDeps)) {
|
|
57
80
|
deps.push(normalizePkgName(key));
|
|
58
81
|
}
|
|
59
|
-
} catch {
|
|
82
|
+
} catch {
|
|
83
|
+
/* ignore parse errors */
|
|
84
|
+
}
|
|
60
85
|
} else if (path === 'poetry.lock') {
|
|
61
86
|
const pattern = /name\s*=\s*["']([^"']+)["']/g;
|
|
62
87
|
let m;
|
|
@@ -69,18 +94,24 @@ function findPackagesInManifests(allFiles) {
|
|
|
69
94
|
for (const key of Object.keys(obj?.packages || {})) {
|
|
70
95
|
deps.push(normalizePkgName(key));
|
|
71
96
|
}
|
|
72
|
-
} catch {
|
|
97
|
+
} catch {
|
|
98
|
+
/* ignore parse errors */
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
for (const dep of deps) {
|
|
102
|
+
packages.add(dep);
|
|
73
103
|
}
|
|
74
|
-
for (const dep of deps) packages.add(dep);
|
|
75
104
|
}
|
|
76
105
|
|
|
77
106
|
return packages;
|
|
78
107
|
}
|
|
79
108
|
|
|
80
109
|
function hasStarlettePin(allFiles) {
|
|
81
|
-
for (const file of
|
|
110
|
+
for (const file of allFiles || []) {
|
|
82
111
|
const content = typeof file.content === 'string' ? file.content : '';
|
|
83
|
-
if (!content)
|
|
112
|
+
if (!content) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
84
115
|
const path = file.path || '';
|
|
85
116
|
|
|
86
117
|
let result = null;
|
|
@@ -99,10 +130,14 @@ function hasStarlettePin(allFiles) {
|
|
|
99
130
|
}
|
|
100
131
|
|
|
101
132
|
if (result) {
|
|
102
|
-
if (result.version === null && result.specifier === null)
|
|
133
|
+
if (result.version === null && result.specifier === null) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
103
136
|
const parsed = parsePEP440(result.version);
|
|
104
137
|
const safe = parsePEP440('1.0.1');
|
|
105
|
-
if (parsed && compareVersions(parsed, safe) >= 0)
|
|
138
|
+
if (parsed && compareVersions(parsed, safe) >= 0) {
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
106
141
|
}
|
|
107
142
|
}
|
|
108
143
|
|
|
@@ -110,7 +145,9 @@ function hasStarlettePin(allFiles) {
|
|
|
110
145
|
}
|
|
111
146
|
|
|
112
147
|
function parsePEP440(versionStr) {
|
|
113
|
-
if (!versionStr)
|
|
148
|
+
if (!versionStr) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
114
151
|
const clean = versionStr.trim().replace(/^v/, '');
|
|
115
152
|
const parts = clean.split('.');
|
|
116
153
|
return {
|
|
@@ -121,21 +158,33 @@ function parsePEP440(versionStr) {
|
|
|
121
158
|
}
|
|
122
159
|
|
|
123
160
|
function compareVersions(a, b) {
|
|
124
|
-
if (!a)
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if (
|
|
161
|
+
if (!a) {
|
|
162
|
+
return 1;
|
|
163
|
+
}
|
|
164
|
+
if (!b) {
|
|
165
|
+
return -1;
|
|
166
|
+
}
|
|
167
|
+
if (a.major !== b.major) {
|
|
168
|
+
return a.major - b.major;
|
|
169
|
+
}
|
|
170
|
+
if (a.minor !== b.minor) {
|
|
171
|
+
return a.minor - b.minor;
|
|
172
|
+
}
|
|
128
173
|
return a.patch - b.patch;
|
|
129
174
|
}
|
|
130
175
|
|
|
131
176
|
export function scanTransitive(allFiles) {
|
|
132
177
|
const findings = [];
|
|
133
178
|
|
|
134
|
-
if (!allFiles || allFiles.length === 0)
|
|
179
|
+
if (!allFiles || allFiles.length === 0) {
|
|
180
|
+
return findings;
|
|
181
|
+
}
|
|
135
182
|
|
|
136
183
|
const packages = findPackagesInManifests(allFiles);
|
|
137
184
|
|
|
138
|
-
if (hasStarlettePin(allFiles))
|
|
185
|
+
if (hasStarlettePin(allFiles)) {
|
|
186
|
+
return findings;
|
|
187
|
+
}
|
|
139
188
|
|
|
140
189
|
const handled = new Set();
|
|
141
190
|
|
|
@@ -147,7 +196,9 @@ export function scanTransitive(allFiles) {
|
|
|
147
196
|
if (version) {
|
|
148
197
|
const parsed = parsePEP440(version);
|
|
149
198
|
const safeFastapi = parsePEP440('0.116.0');
|
|
150
|
-
if (parsed && compareVersions(parsed, safeFastapi) >= 0)
|
|
199
|
+
if (parsed && compareVersions(parsed, safeFastapi) >= 0) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
151
202
|
}
|
|
152
203
|
}
|
|
153
204
|
findings.push(transitiveDependencyFinding(pkg, 1));
|
|
@@ -157,7 +208,9 @@ export function scanTransitive(allFiles) {
|
|
|
157
208
|
|
|
158
209
|
if (findings.length === 0) {
|
|
159
210
|
for (const pkg of packages) {
|
|
160
|
-
if (handled.has(pkg))
|
|
211
|
+
if (handled.has(pkg)) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
161
214
|
if (TIER_2_PACKAGES.includes(pkg)) {
|
|
162
215
|
findings.push(transitiveDependencyFinding(pkg, 2));
|
|
163
216
|
break;
|
|
@@ -169,18 +222,24 @@ export function scanTransitive(allFiles) {
|
|
|
169
222
|
}
|
|
170
223
|
|
|
171
224
|
function findFastapiVersion(allFiles) {
|
|
172
|
-
for (const file of
|
|
225
|
+
for (const file of allFiles || []) {
|
|
173
226
|
const content = typeof file.content === 'string' ? file.content : '';
|
|
174
|
-
if (!content)
|
|
227
|
+
if (!content) {
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
175
230
|
const path = file.path || '';
|
|
176
231
|
if (path === 'requirements.txt' || path.match(/^requirements\/.*\.txt$/)) {
|
|
177
232
|
const lines = content.split('\n');
|
|
178
233
|
for (const line of lines) {
|
|
179
234
|
const trimmed = line.trim();
|
|
180
|
-
if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('-'))
|
|
235
|
+
if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('-')) {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
181
238
|
if (trimmed.startsWith('fastapi')) {
|
|
182
239
|
const eqIdx = trimmed.indexOf('==');
|
|
183
|
-
if (eqIdx >= 0)
|
|
240
|
+
if (eqIdx >= 0) {
|
|
241
|
+
return trimmed.slice(eqIdx + 2).trim();
|
|
242
|
+
}
|
|
184
243
|
}
|
|
185
244
|
}
|
|
186
245
|
}
|
|
@@ -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;
|