@lateos/npm-scan 1.0.0 → 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.
Files changed (125) hide show
  1. package/README.md +864 -861
  2. package/backend/cra.js +113 -21
  3. package/backend/db.js +18 -10
  4. package/backend/detectors/atk-001-lifecycle.js +5 -5
  5. package/backend/detectors/atk-002-obfusc.js +126 -47
  6. package/backend/detectors/atk-003-creds.js +8 -4
  7. package/backend/detectors/atk-004-persist.js +3 -3
  8. package/backend/detectors/atk-005-exfil.js +8 -4
  9. package/backend/detectors/atk-006-depconf.js +3 -3
  10. package/backend/detectors/atk-007-typosquat.js +64 -10
  11. package/backend/detectors/atk-008-tarball-tamper.js +6 -6
  12. package/backend/detectors/atk-009-dormant-trigger.js +9 -5
  13. package/backend/detectors/atk-010-sandbox-evasion.js +25 -10
  14. package/backend/detectors/atk-011-transitive-prop.js +14 -13
  15. package/backend/detectors/axios-poisoning/d1-version-fingerprint.js +4 -4
  16. package/backend/detectors/axios-poisoning/d2-decoy-dep.js +5 -1
  17. package/backend/detectors/axios-poisoning/d3-postinstall-rat.js +64 -19
  18. package/backend/detectors/axios-poisoning/index.js +77 -60
  19. package/backend/detectors/config/thresholds.js +48 -3
  20. package/backend/detectors/cve-2026-48710-badhost/codePattern.js +26 -9
  21. package/backend/detectors/cve-2026-48710-badhost/findings.js +8 -4
  22. package/backend/detectors/cve-2026-48710-badhost/index.js +1 -1
  23. package/backend/detectors/cve-2026-48710-badhost/manifest.js +127 -39
  24. package/backend/detectors/cve-2026-48710-badhost/transitive.js +87 -28
  25. package/backend/detectors/hf-impersonation/index.js +94 -31
  26. package/backend/detectors/hf-impersonation/jaro-winkler.js +33 -12
  27. package/backend/detectors/hf-impersonation/known-orgs.js +15 -3
  28. package/backend/detectors/hf-impersonation/simhash.js +2 -2
  29. package/backend/detectors/index.js +181 -34
  30. package/backend/detectors/lib/ast-patterns.js +4 -1
  31. package/backend/detectors/lib/entropy-analyzer.js +12 -4
  32. package/backend/detectors/megalodon/d1-workflow-scan.js +40 -16
  33. package/backend/detectors/megalodon/d2-credential-harvest.js +12 -5
  34. package/backend/detectors/megalodon/d3-publish-velocity.js +17 -11
  35. package/backend/detectors/megalodon/d4-publisher-drift.js +48 -16
  36. package/backend/detectors/megalodon/d5-bot-commit-identity.js +1 -1
  37. package/backend/detectors/megalodon/d6-date-anachronism.js +1 -1
  38. package/backend/detectors/megalodon/index.js +35 -25
  39. package/backend/detectors/mini-shai-hulud/d1-burst-publish.js +3 -1
  40. package/backend/detectors/mini-shai-hulud/d2-sibling-compromise.js +22 -10
  41. package/backend/detectors/mini-shai-hulud/d3-slsa-mismatch.js +30 -10
  42. package/backend/detectors/mini-shai-hulud/d4-maintainer-anomaly.js +17 -13
  43. package/backend/detectors/mini-shai-hulud/d5-ioc-check.js +12 -4
  44. package/backend/detectors/mini-shai-hulud/d6-token-exfil.js +6 -2
  45. package/backend/detectors/mini-shai-hulud/index.js +63 -26
  46. package/backend/detectors/msh-supplement/d2-persistence.js +30 -12
  47. package/backend/detectors/msh-supplement/d3-geo-killswitch.js +20 -8
  48. package/backend/detectors/msh-supplement/d4-c2-deaddrop.js +19 -5
  49. package/backend/detectors/msh-supplement/index.js +78 -63
  50. package/backend/detectors/node-ipc-compromise/d1-version-blocklist.js +4 -2
  51. package/backend/detectors/node-ipc-compromise/d10-unauthorized-publisher.js +9 -5
  52. package/backend/detectors/node-ipc-compromise/d11-blast-radius.js +7 -3
  53. package/backend/detectors/node-ipc-compromise/d2-tarball-hash.js +9 -4
  54. package/backend/detectors/node-ipc-compromise/d3-cjs-payload-injection.js +7 -5
  55. package/backend/detectors/node-ipc-compromise/d4-injected-payload-hash.js +4 -2
  56. package/backend/detectors/node-ipc-compromise/d5-dns-c2-pattern.js +13 -10
  57. package/backend/detectors/node-ipc-compromise/d7-dns-txt-exfil.js +3 -1
  58. package/backend/detectors/node-ipc-compromise/d8-runtime-trigger.js +5 -2
  59. package/backend/detectors/node-ipc-compromise/index.js +21 -15
  60. package/backend/detectors/tier1-binary-embed.js +109 -41
  61. package/backend/detectors/tier1-cloud-imds.js +57 -37
  62. package/backend/detectors/tier1-encrypted-c2.js +198 -0
  63. package/backend/detectors/tier1-infostealer.js +121 -68
  64. package/backend/detectors/tier1-lifecycle-hook.js +63 -23
  65. package/backend/detectors/tier1-maintainer-compromise.js +157 -0
  66. package/backend/detectors/tier1-metadata-spoof.js +92 -42
  67. package/backend/detectors/tier1-multistage-postinstall.js +46 -19
  68. package/backend/detectors/tier1-obfuscation-heuristics.js +45 -17
  69. package/backend/detectors/tier1-self-propagation.js +115 -0
  70. package/backend/detectors/tier1-slsa-attestation.js +1 -1
  71. package/backend/detectors/tier1-transitive-deps.js +182 -0
  72. package/backend/detectors/tier1-typosquat.js +129 -50
  73. package/backend/detectors/tier1-version-anomaly.js +77 -41
  74. package/backend/detectors/tier1-version-confusion.js +79 -59
  75. package/backend/detectors/trapdoor/d1-campaign-marker.js +3 -1
  76. package/backend/detectors/trapdoor/d2-payload-fingerprint.js +1 -1
  77. package/backend/detectors/trapdoor/d3-publisher-blocklist.js +4 -3
  78. package/backend/detectors/trapdoor/d4-gists-exfil.js +4 -2
  79. package/backend/detectors/trapdoor/d5-ai-poisoning.js +5 -3
  80. package/backend/detectors/trapdoor/d6-lure-name.js +12 -7
  81. package/backend/detectors/trapdoor/d7-crypto-primitives.js +2 -2
  82. package/backend/detectors/trapdoor/d8-xor-key.js +7 -2
  83. package/backend/detectors/trapdoor/d9-cred-validation.js +4 -5
  84. package/backend/detectors/trapdoor/index.js +19 -14
  85. package/backend/detectors/typosquat-vpmdhaj/d1-maintainer.js +32 -8
  86. package/backend/detectors/typosquat-vpmdhaj/d2-preinstall-loader.js +5 -3
  87. package/backend/detectors/typosquat-vpmdhaj/d3-cred-exfil.js +34 -12
  88. package/backend/detectors/typosquat-vpmdhaj/index.js +78 -59
  89. package/backend/detectors.test.js +78 -19
  90. package/backend/fetch.js +37 -29
  91. package/backend/index.js +1 -1
  92. package/backend/license.js +20 -4
  93. package/backend/lockfile.js +60 -36
  94. package/backend/pdf.js +107 -28
  95. package/backend/policy.js +183 -56
  96. package/backend/provenance.js +28 -3
  97. package/backend/report.js +136 -70
  98. package/backend/sbom.js +33 -27
  99. package/backend/scripts/analyze-false-positives.js +14 -8
  100. package/backend/scripts/analyze-validation.js +27 -21
  101. package/backend/scripts/detect-false-positives.js +20 -10
  102. package/backend/scripts/fetch-top-packages.js +197 -49
  103. package/backend/scripts/validate-d10-d13.js +103 -0
  104. package/backend/scripts/validate-detectors.js +26 -17
  105. package/backend/siem/cef.js +23 -21
  106. package/backend/siem/ecs.js +3 -3
  107. package/backend/siem/index.js +1 -1
  108. package/backend/siem/qradar.js +3 -3
  109. package/backend/siem/sentinel.js +2 -2
  110. package/backend/tests-d5-enhanced.test.js +13 -12
  111. package/backend/tests-d6-version-anomaly.test.js +17 -8
  112. package/backend/tests-d6.test.js +24 -14
  113. package/backend/tests-d6c.test.js +27 -14
  114. package/backend/tests-d7-obfuscation.test.js +9 -12
  115. package/backend/tests.test.js +182 -83
  116. package/backend/vsix-scan/detectors/activation-event-risk.js +36 -19
  117. package/backend/vsix-scan/detectors/burst-publish.js +14 -7
  118. package/backend/vsix-scan/detectors/exfil-pattern.js +7 -3
  119. package/backend/vsix-scan/detectors/known-ioc.js +23 -8
  120. package/backend/vsix-scan/detectors/orphan-commit-fetch.js +11 -7
  121. package/backend/vsix-scan/detectors/publisher-anomaly.js +24 -10
  122. package/backend/vsix-scan/index.js +97 -41
  123. package/backend/vsix-scan/marketplace-client.js +29 -13
  124. package/cli/cli.js +154 -64
  125. package/package.json +12 -3
@@ -1,5 +1,12 @@
1
1
  import { transitiveDependencyFinding } from './findings.js';
2
- import { parseRequirementsTxt, parsePyprojectToml, parsePoetryLock, parsePipfile, parseSetupPy, parseSetupCfg } from './manifest.js';
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.replace(/["'\[\]]/g, '').trim().toLowerCase();
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 (allFiles || [])) {
41
+ for (const file of allFiles || []) {
32
42
  const content = typeof file.content === 'string' ? file.content : '';
33
- if (!content) continue;
43
+ if (!content) {
44
+ continue;
45
+ }
34
46
  const path = file.path || '';
35
47
 
36
- let deps = [];
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('-')) continue;
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 = eqIdx >= 0 ? spec.slice(0, eqIdx).trim() : (geIdx >= 0 ? spec.slice(0, geIdx).trim() : spec);
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 = { ...(obj?.tool?.poetry?.dependencies || {}), ...(obj?.dependencies || {}), ...(obj?.['dev-dependencies'] || {}) };
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 (allFiles || [])) {
110
+ for (const file of allFiles || []) {
82
111
  const content = typeof file.content === 'string' ? file.content : '';
83
- if (!content) continue;
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) return false;
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) return true;
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) return null;
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) return 1;
125
- if (!b) return -1;
126
- if (a.major !== b.major) return a.major - b.major;
127
- if (a.minor !== b.minor) return a.minor - b.minor;
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) return findings;
179
+ if (!allFiles || allFiles.length === 0) {
180
+ return findings;
181
+ }
135
182
 
136
183
  const packages = findPackagesInManifests(allFiles);
137
184
 
138
- if (hasStarlettePin(allFiles)) return findings;
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) continue;
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)) continue;
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 (allFiles || [])) {
225
+ for (const file of allFiles || []) {
173
226
  const content = typeof file.content === 'string' ? file.content : '';
174
- if (!content) continue;
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('-')) continue;
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) return trimmed.slice(eqIdx + 2).trim();
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)\/([^\/\s"'>]+)\/([^\/\s"'>]+)/g;
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 SEVERITY_LABELS = ['none', 'low', 'medium', 'high', 'critical'];
15
-
16
- const HF_ARTIFACT_LIBS = new Set(['transformers', 'diffusers', 'sentence-transformers', 'gguf', 'safetensors']);
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 maxSeverity(a, b) {
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) return null;
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) return null;
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') continue;
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)) continue;
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(referencedRepo, org, canonicalOrg, similarityScore, postinstallFetchFlag, tags, hfMeta) {
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 [referencedRepo, { org, canonicalOrg, similarityScore, finding }] of orgsToCheck) {
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 = canonicalOrg.org !== org ? `${API_BASE}/api/models/${canonicalOrg.org}/${referencedRepo.split('/')[1]}` : null;
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 canonicalModel = canonicalUrl ? await fetchWithCache(canonicalUrl) : null;
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(`${API_BASE}/${canonicalOrg.org}/${referencedRepo.split('/')[1]}/resolve/main/README.md`);
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) readmeFinding.hfMeta = 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) artifactFinding.hfMeta = 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) sf.tags = [];
337
+ if (!sf.tags) {
338
+ sf.tags = [];
339
+ }
304
340
  for (const t of tags) {
305
- if (!sf.tags.includes(t)) sf.tags.push(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) nf.tags = [];
353
+ if (!nf.tags) {
354
+ nf.tags = [];
355
+ }
316
356
  for (const t of tags) {
317
- if (!nf.tags.includes(t)) nf.tags.push(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) escalatedRepos.add(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) 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 = [], registryMeta = null, allFiles = null) {
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) return [];
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) continue;
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) continue;
363
- if (org.toLowerCase() === canonicalOrg.org.toLowerCase()) continue;
412
+ if (!canonicalOrg.org) {
413
+ continue;
414
+ }
415
+ if (org.toLowerCase() === canonicalOrg.org.toLowerCase()) {
416
+ continue;
417
+ }
364
418
 
365
- const finding = buildHFOrgSpoofFinding(tuple, org, canonicalOrg, canonicalOrg.score, postinstallFetchFlag, []);
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) return [];
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) return 1;
3
- const len1 = s1.length, len2 = s2.length;
4
- if (len1 === 0 || len2 === 0) return 0;
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]) continue;
16
- if (s1[i] !== s2[j]) continue;
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) return 0;
33
+ if (matches === 0) {
34
+ return 0;
35
+ }
25
36
 
26
- let transpositions = 0, k = 0;
37
+ let transpositions = 0,
38
+ k = 0;
27
39
  for (let i = 0; i < len1; i++) {
28
- if (!matches1[i]) continue;
29
- while (!matches2[k]) k++;
30
- if (s1[i] !== s2[k]) transpositions++;
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]) prefix++;
40
- else break;
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', 'meta-llama', 'mistralai', 'google', 'microsoft',
3
- 'stabilityai', 'EleutherAI', 'huggingface', 'tiiuae', 'cohere',
4
- 'anthropic', 'deepseek-ai', 'Qwen', 'NousResearch', 'teknium',
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 = ((hash << 5) + hash) + str.charCodeAt(i);
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 |= (1n << BigInt(i));
28
+ fingerprint |= 1n << BigInt(i);
29
29
  }
30
30
  }
31
31
  return fingerprint;