@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.
Files changed (149) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +864 -826
  3. package/VALIDATION.md +92 -0
  4. package/backend/cra.js +113 -21
  5. package/backend/db/pg-schema.sql +155 -0
  6. package/backend/db.js +18 -10
  7. package/backend/detectors/atk-001-lifecycle.js +5 -5
  8. package/backend/detectors/atk-002-obfusc.js +126 -47
  9. package/backend/detectors/atk-003-creds.js +8 -4
  10. package/backend/detectors/atk-004-persist.js +3 -3
  11. package/backend/detectors/atk-005-exfil.js +8 -4
  12. package/backend/detectors/atk-006-depconf.js +3 -3
  13. package/backend/detectors/atk-007-typosquat.js +64 -10
  14. package/backend/detectors/atk-008-tarball-tamper.js +6 -6
  15. package/backend/detectors/atk-009-dormant-trigger.js +9 -5
  16. package/backend/detectors/atk-010-sandbox-evasion.js +25 -10
  17. package/backend/detectors/atk-011-transitive-prop.js +14 -13
  18. package/backend/detectors/axios-poisoning/d1-version-fingerprint.js +4 -4
  19. package/backend/detectors/axios-poisoning/d2-decoy-dep.js +5 -1
  20. package/backend/detectors/axios-poisoning/d3-postinstall-rat.js +64 -19
  21. package/backend/detectors/axios-poisoning/index.js +77 -60
  22. package/backend/detectors/config/thresholds.js +111 -0
  23. package/backend/detectors/config/whitelist.json +74 -0
  24. package/backend/detectors/cve-2026-48710-badhost/codePattern.js +26 -9
  25. package/backend/detectors/cve-2026-48710-badhost/findings.js +8 -4
  26. package/backend/detectors/cve-2026-48710-badhost/index.js +1 -1
  27. package/backend/detectors/cve-2026-48710-badhost/manifest.js +127 -39
  28. package/backend/detectors/cve-2026-48710-badhost/transitive.js +87 -28
  29. package/backend/detectors/hf-impersonation/index.js +94 -31
  30. package/backend/detectors/hf-impersonation/jaro-winkler.js +33 -12
  31. package/backend/detectors/hf-impersonation/known-orgs.js +15 -3
  32. package/backend/detectors/hf-impersonation/simhash.js +2 -2
  33. package/backend/detectors/index.js +184 -31
  34. package/backend/detectors/lib/ast-patterns.js +24 -0
  35. package/backend/detectors/lib/entropy-analyzer.js +32 -0
  36. package/backend/detectors/megalodon/d1-workflow-scan.js +40 -16
  37. package/backend/detectors/megalodon/d2-credential-harvest.js +12 -5
  38. package/backend/detectors/megalodon/d3-publish-velocity.js +17 -11
  39. package/backend/detectors/megalodon/d4-publisher-drift.js +48 -16
  40. package/backend/detectors/megalodon/d5-bot-commit-identity.js +1 -1
  41. package/backend/detectors/megalodon/d6-date-anachronism.js +1 -1
  42. package/backend/detectors/megalodon/index.js +35 -25
  43. package/backend/detectors/mini-shai-hulud/d1-burst-publish.js +3 -1
  44. package/backend/detectors/mini-shai-hulud/d2-sibling-compromise.js +22 -10
  45. package/backend/detectors/mini-shai-hulud/d3-slsa-mismatch.js +30 -10
  46. package/backend/detectors/mini-shai-hulud/d4-maintainer-anomaly.js +17 -13
  47. package/backend/detectors/mini-shai-hulud/d5-ioc-check.js +12 -4
  48. package/backend/detectors/mini-shai-hulud/d6-token-exfil.js +6 -2
  49. package/backend/detectors/mini-shai-hulud/index.js +63 -26
  50. package/backend/detectors/msh-supplement/d2-persistence.js +30 -12
  51. package/backend/detectors/msh-supplement/d3-geo-killswitch.js +20 -8
  52. package/backend/detectors/msh-supplement/d4-c2-deaddrop.js +19 -5
  53. package/backend/detectors/msh-supplement/index.js +78 -63
  54. package/backend/detectors/node-ipc-compromise/d1-version-blocklist.js +4 -2
  55. package/backend/detectors/node-ipc-compromise/d10-unauthorized-publisher.js +9 -5
  56. package/backend/detectors/node-ipc-compromise/d11-blast-radius.js +7 -3
  57. package/backend/detectors/node-ipc-compromise/d2-tarball-hash.js +9 -4
  58. package/backend/detectors/node-ipc-compromise/d3-cjs-payload-injection.js +7 -5
  59. package/backend/detectors/node-ipc-compromise/d4-injected-payload-hash.js +4 -2
  60. package/backend/detectors/node-ipc-compromise/d5-dns-c2-pattern.js +13 -10
  61. package/backend/detectors/node-ipc-compromise/d7-dns-txt-exfil.js +3 -1
  62. package/backend/detectors/node-ipc-compromise/d8-runtime-trigger.js +5 -2
  63. package/backend/detectors/node-ipc-compromise/index.js +21 -15
  64. package/backend/detectors/tier1-binary-embed.js +138 -41
  65. package/backend/detectors/tier1-cloud-imds.js +57 -37
  66. package/backend/detectors/tier1-encrypted-c2.js +198 -0
  67. package/backend/detectors/tier1-infostealer.js +121 -68
  68. package/backend/detectors/tier1-lifecycle-hook.js +63 -23
  69. package/backend/detectors/tier1-maintainer-compromise.js +157 -0
  70. package/backend/detectors/tier1-metadata-spoof.js +92 -42
  71. package/backend/detectors/tier1-multistage-postinstall.js +46 -19
  72. package/backend/detectors/tier1-obfuscation-heuristics.js +184 -0
  73. package/backend/detectors/tier1-self-propagation.js +115 -0
  74. package/backend/detectors/tier1-slsa-attestation.js +12 -0
  75. package/backend/detectors/tier1-transitive-deps.js +182 -0
  76. package/backend/detectors/tier1-typosquat.js +129 -50
  77. package/backend/detectors/tier1-version-anomaly.js +223 -0
  78. package/backend/detectors/tier1-version-confusion.js +79 -59
  79. package/backend/detectors/trapdoor/d1-campaign-marker.js +3 -1
  80. package/backend/detectors/trapdoor/d2-payload-fingerprint.js +1 -1
  81. package/backend/detectors/trapdoor/d3-publisher-blocklist.js +4 -3
  82. package/backend/detectors/trapdoor/d4-gists-exfil.js +4 -2
  83. package/backend/detectors/trapdoor/d5-ai-poisoning.js +5 -3
  84. package/backend/detectors/trapdoor/d6-lure-name.js +12 -7
  85. package/backend/detectors/trapdoor/d7-crypto-primitives.js +2 -2
  86. package/backend/detectors/trapdoor/d8-xor-key.js +7 -2
  87. package/backend/detectors/trapdoor/d9-cred-validation.js +4 -5
  88. package/backend/detectors/trapdoor/index.js +19 -14
  89. package/backend/detectors/typosquat-vpmdhaj/d1-maintainer.js +32 -8
  90. package/backend/detectors/typosquat-vpmdhaj/d2-preinstall-loader.js +5 -3
  91. package/backend/detectors/typosquat-vpmdhaj/d3-cred-exfil.js +34 -12
  92. package/backend/detectors/typosquat-vpmdhaj/index.js +78 -59
  93. package/backend/detectors.test.js +147 -0
  94. package/backend/fetch.js +37 -29
  95. package/backend/index.js +1 -1
  96. package/backend/license.js +20 -4
  97. package/backend/lockfile.js +60 -36
  98. package/backend/pdf.js +107 -28
  99. package/backend/policy.js +183 -56
  100. package/backend/provenance.js +28 -3
  101. package/backend/report.js +136 -70
  102. package/backend/sbom.js +33 -27
  103. package/backend/scripts/analyze-false-positives.js +152 -0
  104. package/backend/scripts/analyze-validation.js +157 -0
  105. package/backend/scripts/detect-false-positives.js +103 -0
  106. package/backend/scripts/fetch-top-packages.js +277 -0
  107. package/backend/scripts/validate-d10-d13.js +103 -0
  108. package/backend/scripts/validate-detectors.js +151 -0
  109. package/backend/siem/cef.js +23 -21
  110. package/backend/siem/ecs.js +3 -3
  111. package/backend/siem/index.js +1 -1
  112. package/backend/siem/qradar.js +3 -3
  113. package/backend/siem/sentinel.js +2 -2
  114. package/backend/tests-d5-enhanced.test.js +47 -0
  115. package/backend/tests-d6-version-anomaly.test.js +67 -0
  116. package/backend/tests-d6.test.js +126 -0
  117. package/backend/tests-d6c.test.js +119 -0
  118. package/backend/tests-d7-obfuscation.test.js +88 -0
  119. package/backend/tests.test.js +997 -0
  120. package/backend/vsix-scan/detectors/activation-event-risk.js +36 -19
  121. package/backend/vsix-scan/detectors/burst-publish.js +14 -7
  122. package/backend/vsix-scan/detectors/exfil-pattern.js +7 -3
  123. package/backend/vsix-scan/detectors/known-ioc.js +23 -8
  124. package/backend/vsix-scan/detectors/orphan-commit-fetch.js +11 -7
  125. package/backend/vsix-scan/detectors/publisher-anomaly.js +24 -10
  126. package/backend/vsix-scan/index.js +97 -41
  127. package/backend/vsix-scan/marketplace-client.js +29 -13
  128. package/cli/cli.js +154 -64
  129. package/package.json +36 -10
  130. package/.dockerignore +0 -20
  131. package/.husky/pre-commit +0 -1
  132. package/SECURITY.md +0 -73
  133. package/deploy/helm/npm-scan/Chart.yaml +0 -22
  134. package/deploy/helm/npm-scan/templates/_helpers.tpl +0 -9
  135. package/deploy/helm/npm-scan/templates/api.yaml +0 -94
  136. package/deploy/helm/npm-scan/templates/ingress.yaml +0 -28
  137. package/deploy/helm/npm-scan/templates/postgresql.yaml +0 -67
  138. package/deploy/helm/npm-scan/templates/secrets.yaml +0 -19
  139. package/deploy/helm/npm-scan/templates/worker.yaml +0 -32
  140. package/deploy/helm/npm-scan/values.byoc.yaml +0 -75
  141. package/deploy/helm/npm-scan/values.yaml +0 -103
  142. package/scripts/download-corpus.js +0 -30
  143. package/scripts/gen-mal-corpus.js +0 -35
  144. package/scripts/generate-campaign-fixtures.js +0 -170
  145. package/src/config/top-5000.json +0 -87
  146. package/test/fixtures/lockfiles/npm-lock.json +0 -69
  147. package/test/fixtures/lockfiles/pnpm-lock.yaml +0 -118
  148. package/test/fixtures/lockfiles/yarn.lock +0 -104
  149. package/test/fixtures/mock-data.js +0 -69
@@ -0,0 +1,184 @@
1
+ import { KNOWN_REPUTABLE_PACKAGES } from '../policy.js';
2
+ import { shannonEntropy, isMinified } from './lib/entropy-analyzer.js';
3
+ import { detectPatterns } from './lib/ast-patterns.js';
4
+
5
+ const LIFECYCLE_SCRIPTS = ['preinstall', 'install', 'postinstall', 'prepare'];
6
+ const ENTROPY_THRESHOLD = 5.3;
7
+ const PAYLOAD_SIZE_THRESHOLD = 100000;
8
+
9
+ function analyze(code, _label) {
10
+ if (!code || code.length < 20) {
11
+ return null;
12
+ }
13
+
14
+ const entropy = shannonEntropy(code);
15
+ const patterns = detectPatterns(code);
16
+ const minified = isMinified(code);
17
+ const payloadSize = code.length;
18
+
19
+ let score = 0;
20
+ const flags = [];
21
+
22
+ if (entropy > ENTROPY_THRESHOLD) {
23
+ score += 35;
24
+ flags.push(`Entropy ${entropy} exceeds threshold ${ENTROPY_THRESHOLD}`);
25
+ }
26
+
27
+ if (entropy > 5.8) {
28
+ score += 15;
29
+ flags.push(`High entropy ${entropy} — strong obfuscation indicator`);
30
+ }
31
+
32
+ const patternCount = patterns.length;
33
+ if (patternCount >= 3) {
34
+ score += 30;
35
+ flags.push(`${patternCount} obfuscation patterns detected`);
36
+ } else if (patternCount >= 1) {
37
+ score += 20;
38
+ flags.push(`${patternCount} obfuscation patterns detected`);
39
+ }
40
+
41
+ if (minified) {
42
+ score += 10;
43
+ flags.push('Minified code — reduced readability');
44
+ }
45
+
46
+ if (payloadSize > PAYLOAD_SIZE_THRESHOLD) {
47
+ score += 15;
48
+ flags.push(
49
+ `Payload size ${payloadSize} bytes exceeds ${PAYLOAD_SIZE_THRESHOLD} byte threshold`
50
+ );
51
+ }
52
+
53
+ if (patterns.includes('XOR_CIPHER') && patterns.length >= 2) {
54
+ score += 10;
55
+ flags.push('ctf-scramble-v2 style XOR cipher detected');
56
+ }
57
+
58
+ if (patterns.includes('EVAL_USAGE') && entropy > 5.0) {
59
+ score += 15;
60
+ flags.push('eval() with high-entropy code');
61
+ }
62
+
63
+ score = Math.max(0, Math.min(100, score));
64
+
65
+ if (score < 40) {
66
+ return null;
67
+ }
68
+
69
+ return {
70
+ flagged: true,
71
+ confidenceScore: score,
72
+ confidence: score >= 80 ? 'HIGH' : score >= 60 ? 'MEDIUM' : 'LOW',
73
+ entropy,
74
+ patterns,
75
+ patternCount,
76
+ payloadSize,
77
+ flags,
78
+ };
79
+ }
80
+
81
+ function severityLabel(sc) {
82
+ if (sc >= 90) {
83
+ return 'critical';
84
+ }
85
+ if (sc >= 70) {
86
+ return 'high';
87
+ }
88
+ if (sc >= 50) {
89
+ return 'medium';
90
+ }
91
+ return 'low';
92
+ }
93
+
94
+ function confidenceLabel(sc) {
95
+ if (sc >= 80) {
96
+ return 'HIGH';
97
+ }
98
+ if (sc >= 60) {
99
+ return 'MEDIUM';
100
+ }
101
+ return 'LOW';
102
+ }
103
+
104
+ export const name = 'tier1-obfuscation-heuristics';
105
+
106
+ export async function scan(pkgJson, jsFiles, _registryMeta, _allFiles) {
107
+ const pkgName = pkgJson?.name;
108
+ if (pkgName && KNOWN_REPUTABLE_PACKAGES.has(pkgName)) {
109
+ return [];
110
+ }
111
+
112
+ const findings = [];
113
+ const scripts = pkgJson?.scripts || {};
114
+
115
+ for (const [hookName, scriptContent] of Object.entries(scripts)) {
116
+ if (!LIFECYCLE_SCRIPTS.includes(hookName)) {
117
+ continue;
118
+ }
119
+ const result = analyze(scriptContent, hookName);
120
+ if (!result) {
121
+ continue;
122
+ }
123
+
124
+ findings.push({
125
+ detector: 'tier1-obfuscation-heuristics',
126
+ id: 'TIER1-OBFUSCATION-HEURISTICS',
127
+ severity: severityLabel(result.confidenceScore),
128
+ confidence: confidenceLabel(result.confidenceScore),
129
+ confidenceScore: result.confidenceScore,
130
+ subtype: 'obfuscated_lifecycle_script',
131
+ message: `Obfuscation detected in ${hookName} script: ${result.flags[0] || 'obfuscation patterns found'}`,
132
+ evidence: [
133
+ `script: ${hookName}`,
134
+ `entropy: ${result.entropy}`,
135
+ `patterns: ${result.patterns.join(', ') || 'none'}`,
136
+ `pattern_count: ${result.patternCount}`,
137
+ `payload_size_bytes: ${result.payloadSize}`,
138
+ ...result.flags,
139
+ ],
140
+ crossFiles: [],
141
+ locations: [{ file: 'package.json', line: 3, column: 10 }],
142
+ reference: 'Mini Shai-Hulud obfuscation campaign',
143
+ });
144
+ }
145
+
146
+ for (const f of jsFiles || []) {
147
+ const content = f.content || '';
148
+ if (content.length < 100) {
149
+ continue;
150
+ }
151
+
152
+ const result = analyze(content, f.path || 'unknown.js');
153
+ if (!result) {
154
+ continue;
155
+ }
156
+
157
+ if (result.confidenceScore < 50) {
158
+ continue;
159
+ }
160
+
161
+ findings.push({
162
+ detector: 'tier1-obfuscation-heuristics',
163
+ id: 'TIER1-OBFUSCATION-HEURISTICS',
164
+ severity: severityLabel(result.confidenceScore),
165
+ confidence: confidenceLabel(result.confidenceScore),
166
+ confidenceScore: result.confidenceScore,
167
+ subtype: 'obfuscated_js_file',
168
+ message: `Obfuscation detected in ${f.path || 'file'}: ${result.flags[0] || 'obfuscation patterns found'}`,
169
+ evidence: [
170
+ `file: ${f.path || 'unknown.js'}`,
171
+ `entropy: ${result.entropy}`,
172
+ `patterns: ${result.patterns.join(', ') || 'none'}`,
173
+ `pattern_count: ${result.patternCount}`,
174
+ `payload_size_bytes: ${result.payloadSize}`,
175
+ ...result.flags,
176
+ ],
177
+ crossFiles: [],
178
+ locations: [{ file: f.path || 'unknown.js', line: 0, column: 0 }],
179
+ reference: 'Mini Shai-Hulud obfuscation campaign',
180
+ });
181
+ }
182
+
183
+ return findings;
184
+ }
@@ -0,0 +1,115 @@
1
+ import { KNOWN_REPUTABLE_PACKAGES } from '../policy.js';
2
+
3
+ const THRESHOLDS = {
4
+ flag_threshold: 75,
5
+ warn_threshold: 60,
6
+ burst_window_minutes: 60,
7
+ min_packages_burst: 3,
8
+ identical_payload_weight: 40,
9
+ };
10
+
11
+ function parseTimeStamps(registryMeta) {
12
+ const timeData = registryMeta?.time;
13
+ if (!timeData || typeof timeData !== 'object') return [];
14
+ return Object.entries(timeData)
15
+ .map(([ver, ts]) => ({
16
+ version: ver,
17
+ time: new Date(ts).getTime(),
18
+ }))
19
+ .filter((e) => !isNaN(e.time))
20
+ .sort((a, b) => a.time - b.time);
21
+ }
22
+
23
+ function findBursts(entries, windowMs) {
24
+ const bursts = [];
25
+ for (let i = 0; i < entries.length; i++) {
26
+ const windowEnd = entries[i].time + windowMs;
27
+ const group = [];
28
+ for (let j = i; j < entries.length && entries[j].time <= windowEnd; j++) {
29
+ group.push(entries[j]);
30
+ }
31
+ if (group.length >= 3) {
32
+ bursts.push({
33
+ startVersion: group[0].version,
34
+ endVersion: group[group.length - 1].version,
35
+ count: group.length,
36
+ windowMinutes: windowMs / 60000,
37
+ versions: group.map((e) => e.version),
38
+ });
39
+ }
40
+ }
41
+ return bursts;
42
+ }
43
+
44
+ function computeConfidence(bursts, findings) {
45
+ let base = 40;
46
+ if (bursts.length > 0) {
47
+ base += 20 + Math.min(bursts[0].count * 5, 25);
48
+ }
49
+ if (findings.length > 0) {
50
+ base += THRESHOLDS.identical_payload_weight;
51
+ }
52
+ return Math.min(100, Math.max(0, base));
53
+ }
54
+
55
+ function severityLabel(score) {
56
+ if (score >= 80) return 'high';
57
+ if (score >= 60) return 'medium';
58
+ return 'low';
59
+ }
60
+
61
+ function confidenceLabel(score) {
62
+ if (score >= 80) return 'HIGH';
63
+ if (score >= 60) return 'MEDIUM';
64
+ return 'LOW';
65
+ }
66
+
67
+ export const name = 'tier1-self-propagation';
68
+
69
+ export async function scan(pkgJson, _jsFiles, registryMeta, _allFiles) {
70
+ const pkgName = pkgJson?.name;
71
+ if (!pkgName) return [];
72
+ if (KNOWN_REPUTABLE_PACKAGES.has(pkgName)) return [];
73
+
74
+ const entries = parseTimeStamps(registryMeta);
75
+ if (entries.length < 3) return [];
76
+
77
+ const windowMs = (THRESHOLDS.burst_window_minutes || 60) * 60 * 1000;
78
+ const bursts = findBursts(entries, windowMs);
79
+ if (bursts.length === 0) return [];
80
+
81
+ const burst = bursts[0];
82
+ const confidenceScore = computeConfidence(bursts, []);
83
+ if (confidenceScore < THRESHOLDS.warn_threshold) return [];
84
+
85
+ const relatedPackages = [];
86
+ const maintainer =
87
+ registryMeta?.maintainer || registryMeta?.versions?.[pkgJson.version]?._npmUser?.name;
88
+ const namespaces = registryMeta?.namespacePackages || [];
89
+ if (namespaces.length > 0) {
90
+ for (const np of namespaces) {
91
+ if (np !== pkgName) relatedPackages.push(np);
92
+ }
93
+ }
94
+
95
+ return [
96
+ {
97
+ detector: 'tier1-self-propagation',
98
+ id: 'TIER1-SELF-PROPAGATION',
99
+ severity: severityLabel(confidenceScore),
100
+ confidence: confidenceLabel(confidenceScore),
101
+ confidenceScore,
102
+ subtype: 'self_propagation_burst',
103
+ message: `Self-propagation burst detected: ${burst.count} versions in ${burst.windowMinutes} minutes`,
104
+ evidence: [
105
+ `burst: ${burst.count} versions in ${burst.windowMinutes}min`,
106
+ `window: ${burst.startVersion} -> ${burst.endVersion}`,
107
+ `related_packages: ${relatedPackages.length}`,
108
+ `maintainer: ${maintainer || 'unknown'}`,
109
+ ],
110
+ locations: [{ file: 'package.json', line: 1, column: 1 }],
111
+ crossFiles: relatedPackages.slice(0, 10),
112
+ reference: 'D10: @redhat-cloud-services Miasma self-propagation',
113
+ },
114
+ ];
115
+ }
@@ -0,0 +1,12 @@
1
+ // D8: SLSA Attestation Mismatch Detector
2
+ // TODO: Implement after npm SLSA attestation API stabilizes
3
+ // Blockers:
4
+ // - npm registry SLSA attestation API not yet widely adopted (as of June 2026)
5
+ // - Requires npm auth token to fetch provenance
6
+ // - May have rate limits
7
+
8
+ export const name = 'tier1-slsa-attestation';
9
+
10
+ export async function scan(_pkgJson, _jsFiles, _registryMeta, _allFiles) {
11
+ return [];
12
+ }
@@ -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
+ }