@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
@@ -0,0 +1,182 @@
1
+ import { KNOWN_REPUTABLE_PACKAGES } from '../policy.js';
2
+
3
+ const THRESHOLDS = {
4
+ flag_threshold: 80,
5
+ warn_threshold: 50,
6
+ new_package_days: 7,
7
+ unknown_depth_weight: 45,
8
+ typosquat_depth_weight: 50,
9
+ different_maintainer_weight: 35,
10
+ };
11
+
12
+ const SUSPICIOUS_NAMES = /(?:plain-crypto|crypto-js|secure-crypto|crypto-lib|cryptography)/i;
13
+
14
+ function levenshtein(a, b) {
15
+ if (Math.abs(a.length - b.length) > 2) return 3;
16
+ const m = a.length,
17
+ n = b.length;
18
+ if (m === 0) return n;
19
+ if (n === 0) return m;
20
+ let prev = new Int32Array(n + 1);
21
+ let curr = new Int32Array(n + 1);
22
+ for (let j = 0; j <= n; j++) prev[j] = j;
23
+ for (let i = 1; i <= m; i++) {
24
+ curr[0] = i;
25
+ let rowMin = curr[0];
26
+ for (let j = 1; j <= n; j++) {
27
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
28
+ curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
29
+ if (curr[j] < rowMin) rowMin = curr[j];
30
+ }
31
+ if (rowMin > 2) return 3;
32
+ const tmp = prev;
33
+ prev = curr;
34
+ curr = tmp;
35
+ }
36
+ return prev[n];
37
+ }
38
+
39
+ function isTyposquat(name, popularNames) {
40
+ for (const popular of popularNames) {
41
+ if (Math.abs(name.length - popular.length) > 2) continue;
42
+ const dist = levenshtein(name, popular);
43
+ if (dist <= 2 && name !== popular) return popular;
44
+ }
45
+ return null;
46
+ }
47
+
48
+ const POPULAR_PACKAGES = [
49
+ 'crypto-js',
50
+ 'crypto',
51
+ 'bcrypt',
52
+ 'jsonwebtoken',
53
+ 'json5',
54
+ 'lodash',
55
+ 'axios',
56
+ 'express',
57
+ 'moment',
58
+ 'chalk',
59
+ 'react',
60
+ 'vue',
61
+ 'angular',
62
+ 'next',
63
+ 'nuxt',
64
+ 'typescript',
65
+ 'eslint',
66
+ 'prettier',
67
+ 'webpack',
68
+ 'babel',
69
+ 'mongoose',
70
+ 'redis',
71
+ 'mysql',
72
+ 'postgres',
73
+ 'passport',
74
+ ];
75
+
76
+ function collectDependencies(pkgJson) {
77
+ const deps = {};
78
+ const allDeps = {
79
+ ...(pkgJson?.dependencies || {}),
80
+ ...(pkgJson?.devDependencies || {}),
81
+ };
82
+ for (const [name, version] of Object.entries(allDeps)) {
83
+ deps[name] = { version, depth: 0, isDirect: true };
84
+ }
85
+ return deps;
86
+ }
87
+
88
+ function computeConfidence(findings) {
89
+ if (findings.length === 0) return 0;
90
+ const maxScore = Math.max(...findings.map((f) => f.weight));
91
+ let base = maxScore;
92
+ if (findings.length > 1) base += 15;
93
+ return Math.min(100, Math.max(0, base));
94
+ }
95
+
96
+ function severityLabel(score) {
97
+ if (score >= 80) return 'high';
98
+ if (score >= 60) return 'medium';
99
+ return 'low';
100
+ }
101
+
102
+ function confidenceLabel(score) {
103
+ if (score >= 80) return 'HIGH';
104
+ if (score >= 60) return 'MEDIUM';
105
+ return 'LOW';
106
+ }
107
+
108
+ export const name = 'tier1-transitive-deps';
109
+
110
+ export async function scan(pkgJson, _jsFiles, _registryMeta, _allFiles) {
111
+ const pkgName = pkgJson?.name;
112
+ if (pkgName && KNOWN_REPUTABLE_PACKAGES.has(pkgName)) return [];
113
+
114
+ const deps = collectDependencies(pkgJson);
115
+ const depNames = Object.keys(deps);
116
+ if (depNames.length === 0) return [];
117
+
118
+ const findings = [];
119
+
120
+ for (const [depName, depInfo] of Object.entries(deps)) {
121
+ let weight = 0;
122
+ const reasons = [];
123
+
124
+ if (SUSPICIOUS_NAMES.test(depName)) {
125
+ weight += 55;
126
+ reasons.push('suspicious_naming: matches known malicious pattern');
127
+ }
128
+
129
+ const typosquatTarget = isTyposquat(depName, POPULAR_PACKAGES);
130
+ if (typosquatTarget) {
131
+ weight += THRESHOLDS.typosquat_depth_weight;
132
+ reasons.push(`typosquat: "${depName}" similar to "${typosquatTarget}"`);
133
+ }
134
+
135
+ if (depInfo.isDirect) {
136
+ const depVersion = depInfo.version || '';
137
+ if (depVersion.includes('x') || depVersion === '*' || /^\d+\.\d+\.\d+$/.test(depVersion)) {
138
+ const parts = depVersion.split('.');
139
+ if (parts.length === 3) {
140
+ const major = parseInt(parts[0], 10);
141
+ if (major >= 99) {
142
+ weight += 55;
143
+ reasons.push(`version_anomaly: suspicious version ${depVersion}`);
144
+ }
145
+ }
146
+ }
147
+ }
148
+
149
+ if (weight > 0) {
150
+ findings.push({
151
+ package: depName,
152
+ depth: depInfo.depth,
153
+ isDirect: depInfo.isDirect,
154
+ weight,
155
+ reasons,
156
+ });
157
+ }
158
+ }
159
+
160
+ if (findings.length === 0) return [];
161
+
162
+ const confidenceScore = computeConfidence(findings);
163
+ if (confidenceScore < THRESHOLDS.warn_threshold) return [];
164
+
165
+ const topFindings = findings.sort((a, b) => b.weight - a.weight).slice(0, 5);
166
+
167
+ return [
168
+ {
169
+ detector: 'tier1-transitive-deps',
170
+ id: 'TIER1-TRANSITIVE-DEPS',
171
+ severity: severityLabel(confidenceScore),
172
+ confidence: confidenceLabel(confidenceScore),
173
+ confidenceScore,
174
+ subtype: 'transitive_injection',
175
+ message: `${findings.length} suspicious transitive dependenc${findings.length > 1 ? 'ies' : 'y'} detected`,
176
+ evidence: topFindings.map((f) => `${f.package} (depth ${f.depth}): ${f.reasons.join('; ')}`),
177
+ locations: [{ file: 'package.json', line: 1, column: 1 }],
178
+ crossFiles: topFindings.map((f) => f.package),
179
+ reference: 'D12: Axios backdoor transitive injection',
180
+ },
181
+ ];
182
+ }
@@ -3,38 +3,54 @@ import { KNOWN_REPUTABLE_PACKAGES } from '../policy.js';
3
3
 
4
4
  const require = createRequire(import.meta.url);
5
5
  const TOP_PACKAGES = require('../../src/config/top-5000.json');
6
- const TOP_SET = new Set(TOP_PACKAGES);
6
+ const _TOP_SET = new Set(TOP_PACKAGES);
7
7
 
8
8
  function levenshtein(a, b) {
9
- if (Math.abs(a.length - b.length) > 2) return 3;
10
- const m = a.length, n = b.length;
11
- if (m === 0) return n;
12
- if (n === 0) return m;
9
+ if (Math.abs(a.length - b.length) > 2) {
10
+ return 3;
11
+ }
12
+ const m = a.length,
13
+ n = b.length;
14
+ if (m === 0) {
15
+ return n;
16
+ }
17
+ if (n === 0) {
18
+ return m;
19
+ }
13
20
  let prev = new Int32Array(n + 1);
14
21
  let curr = new Int32Array(n + 1);
15
- for (let j = 0; j <= n; j++) prev[j] = j;
22
+ for (let j = 0; j <= n; j++) {
23
+ prev[j] = j;
24
+ }
16
25
  for (let i = 1; i <= m; i++) {
17
26
  curr[0] = i;
18
27
  let rowMin = curr[0];
19
28
  for (let j = 1; j <= n; j++) {
20
29
  const cost = a[i - 1] === b[j - 1] ? 0 : 1;
21
- curr[j] = Math.min(
22
- prev[j] + 1,
23
- curr[j - 1] + 1,
24
- prev[j - 1] + cost
25
- );
26
- if (curr[j] < rowMin) rowMin = curr[j];
30
+ curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
31
+ if (curr[j] < rowMin) {
32
+ rowMin = curr[j];
33
+ }
27
34
  }
28
- if (rowMin > 2) return 3;
29
- const tmp = prev; prev = curr; curr = tmp;
35
+ if (rowMin > 2) {
36
+ return 3;
37
+ }
38
+ const tmp = prev;
39
+ prev = curr;
40
+ curr = tmp;
30
41
  }
31
42
  return prev[n];
32
43
  }
33
44
 
34
45
  function jaroWinkler(a, b) {
35
- if (a === b) return 1;
36
- const m = a.length, n = b.length;
37
- if (m === 0 || n === 0) return 0;
46
+ if (a === b) {
47
+ return 1;
48
+ }
49
+ const m = a.length,
50
+ n = b.length;
51
+ if (m === 0 || n === 0) {
52
+ return 0;
53
+ }
38
54
  const matchDist = Math.floor(Math.max(m, n) / 2) - 1;
39
55
  const aMatch = new Array(m).fill(false);
40
56
  const bMatch = new Array(n).fill(false);
@@ -43,36 +59,53 @@ function jaroWinkler(a, b) {
43
59
  const start = Math.max(0, i - matchDist);
44
60
  const end = Math.min(n, i + matchDist + 1);
45
61
  for (let j = start; j < end; j++) {
46
- if (bMatch[j] || a[i] !== b[j]) continue;
62
+ if (bMatch[j] || a[i] !== b[j]) {
63
+ continue;
64
+ }
47
65
  aMatch[i] = true;
48
66
  bMatch[j] = true;
49
67
  matches++;
50
68
  break;
51
69
  }
52
70
  }
53
- if (matches === 0) return 0;
54
- let t = 0, k = 0;
71
+ if (matches === 0) {
72
+ return 0;
73
+ }
74
+ let t = 0,
75
+ k = 0;
55
76
  for (let i = 0; i < m; i++) {
56
- if (!aMatch[i]) continue;
57
- while (!bMatch[k]) k++;
58
- if (a[i] !== b[k]) t++;
77
+ if (!aMatch[i]) {
78
+ continue;
79
+ }
80
+ while (!bMatch[k]) {
81
+ k++;
82
+ }
83
+ if (a[i] !== b[k]) {
84
+ t++;
85
+ }
59
86
  k++;
60
87
  }
61
88
  const jaro = (matches / m + matches / n + (matches - t / 2) / matches) / 3;
62
89
  let prefix = 0;
63
90
  const limit = Math.min(4, m, n);
64
91
  for (let i = 0; i < limit; i++) {
65
- if (a[i] === b[i]) prefix++;
66
- else break;
92
+ if (a[i] === b[i]) {
93
+ prefix++;
94
+ } else {
95
+ break;
96
+ }
67
97
  }
68
98
  return jaro + prefix * 0.1 * (1 - jaro);
69
99
  }
70
100
 
71
- function soundex(s) {
72
- if (!s) return '';
101
+ function _soundex(s) {
102
+ if (!s) {
103
+ return '';
104
+ }
73
105
  s = s.toLowerCase();
74
106
  const first = s[0];
75
- const rest = s.slice(1)
107
+ const rest = s
108
+ .slice(1)
76
109
  .replace(/[aeiouyhw]/g, '')
77
110
  .replace(/[bfpv]/g, '1')
78
111
  .replace(/[cgjkqsxz]/g, '2')
@@ -85,15 +118,22 @@ function soundex(s) {
85
118
  }
86
119
 
87
120
  function homoglyphScore(a, b) {
88
- if (a.length !== b.length) return 0;
89
- const map = { '0': 'o', '1': 'l', '3': 'e', '4': 'a', '5': 's', '6': 'g', '7': 't', '8': 'b', '@': 'a' };
121
+ if (a.length !== b.length) {
122
+ return 0;
123
+ }
124
+ const map = { 0: 'o', 1: 'l', 3: 'e', 4: 'a', 5: 's', 6: 'g', 7: 't', 8: 'b', '@': 'a' };
90
125
  let swaps = 0;
91
126
  for (let i = 0; i < a.length; i++) {
92
- if (a[i] === b[i]) continue;
127
+ if (a[i] === b[i]) {
128
+ continue;
129
+ }
93
130
  const na = map[a[i]] || a[i];
94
131
  const nb = map[b[i]] || b[i];
95
- if (na === b[i] || a[i] === nb || na === nb) swaps++;
96
- else return 0;
132
+ if (na === b[i] || a[i] === nb || na === nb) {
133
+ swaps++;
134
+ } else {
135
+ return 0;
136
+ }
97
137
  }
98
138
  return swaps > 0 ? 1 - swaps / a.length : 0;
99
139
  }
@@ -119,34 +159,52 @@ function computeConfidence(dist, phonetic, homoglyph, registryMeta) {
119
159
  if (registryMeta) {
120
160
  const age = registryMeta.age || 0;
121
161
  const downloads = registryMeta.weeklyDownloads || 0;
122
- if (age < 30) score += 15;
123
- if (downloads < 1000) score += 10;
124
- if (age > 365 && downloads > 100000) score -= 30;
162
+ if (age < 30) {
163
+ score += 15;
164
+ }
165
+ if (downloads < 1000) {
166
+ score += 10;
167
+ }
168
+ if (age > 365 && downloads > 100000) {
169
+ score -= 30;
170
+ }
125
171
  }
126
172
  score = Math.max(50, Math.min(100, score));
127
173
  return { subtype, score };
128
174
  }
129
175
 
130
176
  function severityLabel(score) {
131
- if (score >= 80) return 'high';
132
- if (score >= 60) return 'medium';
177
+ if (score >= 80) {
178
+ return 'high';
179
+ }
180
+ if (score >= 60) {
181
+ return 'medium';
182
+ }
133
183
  return 'low';
134
184
  }
135
185
 
136
186
  function confidenceLabel(score) {
137
- if (score >= 80) return 'HIGH';
138
- if (score >= 60) return 'MEDIUM';
187
+ if (score >= 80) {
188
+ return 'HIGH';
189
+ }
190
+ if (score >= 60) {
191
+ return 'MEDIUM';
192
+ }
139
193
  return 'LOW';
140
194
  }
141
195
 
142
196
  export const name = 'tier1-typosquat';
143
197
 
144
- export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
198
+ export async function scan(pkgJson, jsFiles, registryMeta, _allFiles) {
145
199
  const findings = [];
146
200
  const pkgName = pkgJson?.name;
147
- if (!pkgName) return findings;
201
+ if (!pkgName) {
202
+ return findings;
203
+ }
148
204
 
149
- if (KNOWN_REPUTABLE_PACKAGES.has(pkgName)) return findings;
205
+ if (KNOWN_REPUTABLE_PACKAGES.has(pkgName)) {
206
+ return findings;
207
+ }
150
208
 
151
209
  const namesToCheck = [];
152
210
  let scopedName = null;
@@ -167,27 +225,48 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
167
225
  for (const checkName of namesToCheck) {
168
226
  for (const target of TOP_PACKAGES) {
169
227
  if (checkName === target) {
170
- if (pkgName.startsWith('@') && checkName === scopedName && !KNOWN_REPUTABLE_PACKAGES.has(scopedName)) {
228
+ if (
229
+ pkgName.startsWith('@') &&
230
+ checkName === scopedName &&
231
+ !KNOWN_REPUTABLE_PACKAGES.has(scopedName)
232
+ ) {
171
233
  let score = 80;
172
234
  if (registryMeta) {
173
- if ((registryMeta.age || 0) < 30) score += 15;
174
- if ((registryMeta.weeklyDownloads || 0) < 1000) score += 10;
235
+ if ((registryMeta.age || 0) < 30) {
236
+ score += 15;
237
+ }
238
+ if ((registryMeta.weeklyDownloads || 0) < 1000) {
239
+ score += 10;
240
+ }
175
241
  }
176
242
  score = Math.max(50, Math.min(100, score));
177
243
  if (score > bestScore) {
178
244
  bestScore = score;
179
- best = { target, dist: 0, phonetic: 1, homoglyph: 0, subtype: 'edit_distance_1', isScopeSquat: true };
245
+ best = {
246
+ target,
247
+ dist: 0,
248
+ phonetic: 1,
249
+ homoglyph: 0,
250
+ subtype: 'edit_distance_1',
251
+ isScopeSquat: true,
252
+ };
180
253
  }
181
254
  }
182
255
  continue;
183
256
  }
184
- if (Math.abs(checkName.length - target.length) > 2) continue;
257
+ if (Math.abs(checkName.length - target.length) > 2) {
258
+ continue;
259
+ }
185
260
  const dist = levenshtein(checkName, target);
186
- if (dist > 2) continue;
261
+ if (dist > 2) {
262
+ continue;
263
+ }
187
264
  const phonetic = jaroWinkler(checkName, target);
188
265
  const homoglyph = homoglyphScore(checkName, target);
189
266
  const conf = computeConfidence(dist, phonetic, homoglyph, registryMeta);
190
- if (!conf || conf.score <= bestScore) continue;
267
+ if (!conf || conf.score <= bestScore) {
268
+ continue;
269
+ }
191
270
  bestScore = conf.score;
192
271
  best = { target, dist, phonetic, homoglyph, subtype: conf.subtype, isScopeSquat: false };
193
272
  }
@@ -3,11 +3,17 @@ import { KNOWN_REPUTABLE_PACKAGES } from '../policy.js';
3
3
  const SENTINEL_PATTERNS = new Set(['99.99.99', '11.11.11', '10.10.10']);
4
4
 
5
5
  function parseVersion(v) {
6
- if (!v || typeof v !== 'string') return null;
6
+ if (!v || typeof v !== 'string') {
7
+ return null;
8
+ }
7
9
  const parts = v.split('.');
8
- if (parts.length !== 3) return null;
10
+ if (parts.length !== 3) {
11
+ return null;
12
+ }
9
13
  const [major, minor, patch] = parts.map(Number);
10
- if (isNaN(major) || isNaN(minor) || isNaN(patch)) return null;
14
+ if (isNaN(major) || isNaN(minor) || isNaN(patch)) {
15
+ return null;
16
+ }
11
17
  return { major, minor, patch, full: v };
12
18
  }
13
19
 
@@ -17,19 +23,23 @@ function versionScore(v) {
17
23
 
18
24
  function extractVersions(registryMeta) {
19
25
  if (Array.isArray(registryMeta)) {
20
- return registryMeta.map(v => parseVersion(v)).filter(Boolean);
26
+ return registryMeta.map((v) => parseVersion(v)).filter(Boolean);
21
27
  }
22
28
  if (registryMeta && typeof registryMeta === 'object') {
23
29
  const versions = registryMeta.versions || registryMeta.time;
24
30
  if (versions && typeof versions === 'object') {
25
- return Object.keys(versions).map(v => parseVersion(v)).filter(Boolean);
31
+ return Object.keys(versions)
32
+ .map((v) => parseVersion(v))
33
+ .filter(Boolean);
26
34
  }
27
35
  }
28
36
  return [];
29
37
  }
30
38
 
31
39
  function computeStats(scores) {
32
- if (scores.length < 2) return null;
40
+ if (scores.length < 2) {
41
+ return null;
42
+ }
33
43
  const mean = scores.reduce((s, v) => s + v, 0) / scores.length;
34
44
  const variance = scores.reduce((s, v) => s + (v - mean) ** 2, 0) / scores.length;
35
45
  const stddev = Math.sqrt(variance);
@@ -38,7 +48,9 @@ function computeStats(scores) {
38
48
 
39
49
  export function analyzeAnomaly(packageName, versionStr, versionHistory) {
40
50
  const current = parseVersion(versionStr);
41
- if (!current) return null;
51
+ if (!current) {
52
+ return null;
53
+ }
42
54
 
43
55
  const historical = extractVersions(versionHistory);
44
56
  const currentScore = versionScore(current);
@@ -79,14 +91,18 @@ export function analyzeAnomaly(packageName, versionStr, versionHistory) {
79
91
  }
80
92
 
81
93
  const stats = computeStats(recentScores);
82
- if (!stats) return null;
94
+ if (!stats) {
95
+ return null;
96
+ }
83
97
 
84
98
  const zScore = stats.stddev > 0 ? (currentScore - stats.mean) / stats.stddev : 0;
85
- const baselineMaxVer = historical.find(v => versionScore(v) === stats.max)?.full || 'unknown';
99
+ const baselineMaxVer = historical.find((v) => versionScore(v) === stats.max)?.full || 'unknown';
86
100
  const baselineMeanVal = (stats.mean / 10000).toFixed(1);
87
101
  const prevMaxMajor = Math.floor(stats.max / 10000);
88
- const isNormalMajorBump = current.major === prevMaxMajor + 1 && current.minor === 0 && current.patch === 0;
89
- const isReasonableVersion = current.major <= prevMaxMajor + 2 && current.major >= Math.floor(stats.min / 10000);
102
+ const isNormalMajorBump =
103
+ current.major === prevMaxMajor + 1 && current.minor === 0 && current.patch === 0;
104
+ const isReasonableVersion =
105
+ current.major <= prevMaxMajor + 2 && current.major >= Math.floor(stats.min / 10000);
90
106
  const ratio = stats.max > 0 ? currentScore / stats.max : 0;
91
107
 
92
108
  let flagged = false;
@@ -126,7 +142,9 @@ export function analyzeAnomaly(packageName, versionStr, versionHistory) {
126
142
  reason = `Version ${versionStr} has z-score ${zScore.toFixed(1)} and is outside expected version range`;
127
143
  }
128
144
 
129
- if (!flagged) return null;
145
+ if (!flagged) {
146
+ return null;
147
+ }
130
148
 
131
149
  return {
132
150
  flagged,
@@ -141,47 +159,65 @@ export function analyzeAnomaly(packageName, versionStr, versionHistory) {
141
159
  }
142
160
 
143
161
  function severityLabel(sc) {
144
- if (sc >= 90) return 'critical';
145
- if (sc >= 70) return 'high';
146
- if (sc >= 50) return 'medium';
162
+ if (sc >= 90) {
163
+ return 'critical';
164
+ }
165
+ if (sc >= 70) {
166
+ return 'high';
167
+ }
168
+ if (sc >= 50) {
169
+ return 'medium';
170
+ }
147
171
  return 'low';
148
172
  }
149
173
 
150
174
  function confidenceLabel(sc) {
151
- if (sc >= 80) return 'HIGH';
152
- if (sc >= 60) return 'MEDIUM';
175
+ if (sc >= 80) {
176
+ return 'HIGH';
177
+ }
178
+ if (sc >= 60) {
179
+ return 'MEDIUM';
180
+ }
153
181
  return 'LOW';
154
182
  }
155
183
 
156
184
  export const name = 'tier1-version-anomaly';
157
185
 
158
- export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
186
+ export async function scan(pkgJson, jsFiles, registryMeta, _allFiles) {
159
187
  const pkgName = pkgJson?.name;
160
188
  const version = pkgJson?.version;
161
189
 
162
- if (!pkgName || !version) return [];
163
- if (pkgName && KNOWN_REPUTABLE_PACKAGES.has(pkgName)) return [];
190
+ if (!pkgName || !version) {
191
+ return [];
192
+ }
193
+ if (pkgName && KNOWN_REPUTABLE_PACKAGES.has(pkgName)) {
194
+ return [];
195
+ }
164
196
 
165
197
  const result = analyzeAnomaly(pkgName, version, registryMeta);
166
- if (!result) return [];
167
-
168
- return [{
169
- detector: 'tier1-version-anomaly',
170
- id: 'TIER1-VERSION-ANOMALY',
171
- severity: severityLabel(result.confidenceScore),
172
- confidence: confidenceLabel(result.confidenceScore),
173
- confidenceScore: result.confidenceScore,
174
- subtype: result.attackPattern.toLowerCase(),
175
- message: `Version anomaly detected in "${pkgName}": ${result.reason}`,
176
- evidence: [
177
- `version: ${version}`,
178
- `baseline_max: ${result.baselineMax}`,
179
- `baseline_mean: ${result.baselineMean}`,
180
- `z_score: ${result.zScore ?? 'N/A'}`,
181
- `attack_pattern: ${result.attackPattern}`,
182
- ],
183
- crossFiles: [],
184
- locations: [{ file: 'package.json', line: 3, column: 10 }],
185
- reference: '176-package dependency confusion campaign',
186
- }];
198
+ if (!result) {
199
+ return [];
200
+ }
201
+
202
+ return [
203
+ {
204
+ detector: 'tier1-version-anomaly',
205
+ id: 'TIER1-VERSION-ANOMALY',
206
+ severity: severityLabel(result.confidenceScore),
207
+ confidence: confidenceLabel(result.confidenceScore),
208
+ confidenceScore: result.confidenceScore,
209
+ subtype: result.attackPattern.toLowerCase(),
210
+ message: `Version anomaly detected in "${pkgName}": ${result.reason}`,
211
+ evidence: [
212
+ `version: ${version}`,
213
+ `baseline_max: ${result.baselineMax}`,
214
+ `baseline_mean: ${result.baselineMean}`,
215
+ `z_score: ${result.zScore ?? 'N/A'}`,
216
+ `attack_pattern: ${result.attackPattern}`,
217
+ ],
218
+ crossFiles: [],
219
+ locations: [{ file: 'package.json', line: 3, column: 10 }],
220
+ reference: '176-package dependency confusion campaign',
221
+ },
222
+ ];
187
223
  }