@lateos/npm-scan 0.18.1 → 0.18.2

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 (93) hide show
  1. package/.dockerignore +20 -20
  2. package/.husky/pre-commit +1 -1
  3. package/CHANGELOG.md +233 -199
  4. package/LICENSING.md +19 -19
  5. package/README.de.md +708 -708
  6. package/README.fr.md +707 -707
  7. package/README.ja.md +704 -704
  8. package/README.md +826 -826
  9. package/README.zh.md +708 -708
  10. package/SECURITY.md +72 -72
  11. package/backend/cra.js +68 -68
  12. package/backend/db/schema.sql +32 -32
  13. package/backend/db.js +88 -88
  14. package/backend/detectors/atk-001-lifecycle.js +17 -17
  15. package/backend/detectors/atk-002-obfusc.js +261 -261
  16. package/backend/detectors/atk-003-creds.js +13 -13
  17. package/backend/detectors/atk-004-persist.js +13 -13
  18. package/backend/detectors/atk-005-exfil.js +13 -13
  19. package/backend/detectors/atk-006-depconf.js +14 -14
  20. package/backend/detectors/atk-007-typosquat.js +34 -34
  21. package/backend/detectors/atk-008-tarball-tamper.js +91 -91
  22. package/backend/detectors/atk-009-dormant-trigger.js +62 -62
  23. package/backend/detectors/atk-010-sandbox-evasion.js +50 -50
  24. package/backend/detectors/atk-011-transitive-prop.js +76 -76
  25. package/backend/detectors/cve-2026-48710-badhost/codePattern.js +99 -99
  26. package/backend/detectors/cve-2026-48710-badhost/findings.js +105 -105
  27. package/backend/detectors/cve-2026-48710-badhost/index.js +15 -15
  28. package/backend/detectors/cve-2026-48710-badhost/manifest.js +305 -305
  29. package/backend/detectors/cve-2026-48710-badhost/transitive.js +189 -189
  30. package/backend/detectors/hf-impersonation/index.js +396 -396
  31. package/backend/detectors/hf-impersonation/jaro-winkler.js +44 -44
  32. package/backend/detectors/hf-impersonation/known-orgs.js +5 -5
  33. package/backend/detectors/hf-impersonation/simhash.js +46 -46
  34. package/backend/detectors/index.js +81 -75
  35. package/backend/detectors/megalodon/d1-workflow-scan.js +147 -147
  36. package/backend/detectors/megalodon/d2-credential-harvest.js +61 -61
  37. package/backend/detectors/megalodon/d3-publish-velocity.js +67 -67
  38. package/backend/detectors/megalodon/d4-publisher-drift.js +124 -124
  39. package/backend/detectors/megalodon/d5-bot-commit-identity.js +3 -3
  40. package/backend/detectors/megalodon/d6-date-anachronism.js +3 -3
  41. package/backend/detectors/megalodon/index.js +80 -80
  42. package/backend/detectors/megalodon/types.js +9 -9
  43. package/backend/detectors/mini-shai-hulud/d1-burst-publish.js +42 -42
  44. package/backend/detectors/mini-shai-hulud/d2-sibling-compromise.js +116 -116
  45. package/backend/detectors/mini-shai-hulud/d3-slsa-mismatch.js +72 -72
  46. package/backend/detectors/mini-shai-hulud/d4-maintainer-anomaly.js +45 -45
  47. package/backend/detectors/mini-shai-hulud/d5-ioc-check.js +95 -95
  48. package/backend/detectors/mini-shai-hulud/d6-token-exfil.js +38 -38
  49. package/backend/detectors/mini-shai-hulud/index.js +118 -118
  50. package/backend/detectors/mini-shai-hulud/iocs.json +79 -79
  51. package/backend/detectors/tier1-cloud-imds.js +124 -0
  52. package/backend/detectors/tier1-infostealer.js +36 -0
  53. package/backend/detectors/tier1-multistage-postinstall.js +81 -0
  54. package/backend/detectors/tier1-version-confusion.js +107 -0
  55. package/backend/fetch.js +175 -175
  56. package/backend/index.js +4 -4
  57. package/backend/license.js +89 -89
  58. package/backend/lockfile.js +379 -379
  59. package/backend/pdf.js +245 -245
  60. package/backend/policy.js +193 -193
  61. package/backend/report.js +254 -254
  62. package/backend/sbom.js +66 -66
  63. package/backend/siem/cef.js +32 -32
  64. package/backend/siem/ecs.js +40 -40
  65. package/backend/siem/index.js +18 -18
  66. package/backend/siem/qradar.js +56 -56
  67. package/backend/siem/sentinel.js +27 -27
  68. package/backend/vsix-scan/detectors/activation-event-risk.js +116 -116
  69. package/backend/vsix-scan/detectors/burst-publish.js +52 -52
  70. package/backend/vsix-scan/detectors/exfil-pattern.js +88 -88
  71. package/backend/vsix-scan/detectors/known-ioc.js +105 -105
  72. package/backend/vsix-scan/detectors/orphan-commit-fetch.js +69 -69
  73. package/backend/vsix-scan/detectors/publisher-anomaly.js +70 -70
  74. package/backend/vsix-scan/index.js +183 -183
  75. package/backend/vsix-scan/marketplace-client.js +145 -145
  76. package/backend/vsix-scan/vsix-iocs.json +31 -31
  77. package/cli/cli.js +458 -458
  78. package/deploy/helm/npm-scan/Chart.yaml +21 -21
  79. package/deploy/helm/npm-scan/templates/_helpers.tpl +8 -8
  80. package/deploy/helm/npm-scan/templates/api.yaml +93 -93
  81. package/deploy/helm/npm-scan/templates/ingress.yaml +27 -27
  82. package/deploy/helm/npm-scan/templates/postgresql.yaml +66 -66
  83. package/deploy/helm/npm-scan/templates/secrets.yaml +18 -18
  84. package/deploy/helm/npm-scan/templates/worker.yaml +31 -31
  85. package/deploy/helm/npm-scan/values.byoc.yaml +74 -74
  86. package/deploy/helm/npm-scan/values.yaml +102 -102
  87. package/package.json +57 -57
  88. package/scripts/download-corpus.js +30 -30
  89. package/scripts/gen-mal-corpus.js +34 -34
  90. package/test/fixtures/lockfiles/npm-lock.json +68 -68
  91. package/test/fixtures/lockfiles/pnpm-lock.yaml +117 -117
  92. package/test/fixtures/lockfiles/yarn.lock +103 -103
  93. package/test/fixtures/mock-data.js +69 -69
@@ -1,79 +1,79 @@
1
- {
2
- "lastUpdated": "2026-05-24T00:00:00.000Z",
3
- "waves": {
4
- "wave1": {
5
- "id": "mini-shai-hulud-wave1",
6
- "description": "TanStack CI/CD hijack (mid-May 2026) — 84 malicious versions across 42 packages in ~6 minutes via compromised GitHub Actions CI. Forged SLSA BL3 provenance attestations.",
7
- "windowMinutes": 6,
8
- "iocs": [
9
- {
10
- "type": "packageScope",
11
- "value": "@tanstack",
12
- "maliciousVersionRanges": [],
13
- "notes": "Seed IOC — update from threat intel feed. Affected: @tanstack/router, @tanstack/react-router, @tanstack/query, @tanstack/form, @tanstack/store, @tanstack/virtual, @tanstack/ranger, @tanstack/table."
14
- }
15
- ]
16
- },
17
- "wave2": {
18
- "id": "mini-shai-hulud-wave2",
19
- "description": "AntV/atool maintainer account compromise (late May 2026) — 600+ malicious versions across 300+ packages in ~22 minutes. ~16M weekly download blast radius.",
20
- "windowMinutes": 22,
21
- "iocs": [
22
- {
23
- "type": "publisherAccount",
24
- "value": "atool",
25
- "compromiseWindowStart": "2026-05-20T00:00:00.000Z",
26
- "compromiseWindowEnd": null,
27
- "notes": "Seed IOC — compromised @antv/atool maintainer account. Update compromise window from threat intel."
28
- },
29
- {
30
- "type": "packageScope",
31
- "value": "@antv",
32
- "maliciousVersionRanges": [],
33
- "notes": "Blast radius: @antv/g2, @antv/g6, @antv/x6, @antv/l7, echarts-for-react, timeago.js. Seed IOC — update from threat intel."
34
- }
35
- ]
36
- },
37
- "wave3": {
38
- "id": "nx-console-wave3",
39
- "description": "Nx Console 18.95.0 VS Code extension compromise (May 18, 2026, CVE-2026-48027, TeamPCP) — contributor token stolen via TanStack wave1 (May 11), 7-day dwell, malicious extension published using npx to fetch 498KB obfuscated Bun payload from dangling orphan commit on nrwl/nx repo. ~3M installs exposed.",
40
- "windowMinutes": 36,
41
- "iocs": [
42
- {
43
- "type": "extensionId",
44
- "value": "nrwl.angular-console",
45
- "maliciousVersionRanges": ["18.95.0"],
46
- "notes": "Nx Console v18.95.0 — malicious VS Code extension. CVE-2026-48027. Exposure window: 11 min on Marketplace, 36 min on Open VSX."
47
- },
48
- {
49
- "type": "publisherAccount",
50
- "value": "nrwl",
51
- "compromiseWindowStart": "2026-05-11T00:00:00.000Z",
52
- "compromiseWindowEnd": "2026-05-18T13:09:00.000Z",
53
- "notes": "Nx contributor token stolen via TanStack wave1 on May 11; 7-day dwell before publishing malicious extension on May 18."
54
- },
55
- {
56
- "type": "packageScope",
57
- "value": "@nx",
58
- "maliciousVersionRanges": [],
59
- "notes": "NX_CONSOLE_DOWNSTREAM: npm packages under @nx scope deployed by compromised Nx contributor. Check for versions published within 7 days of 2026-05-18."
60
- },
61
- {
62
- "type": "packageScope",
63
- "value": "nrwl",
64
- "maliciousVersionRanges": [],
65
- "notes": "NX_CONSOLE_DOWNSTREAM: nrwl-scoped npm packages — monitor for anomalous burst publishing."
66
- }
67
- ]
68
- }
69
- },
70
- "iocs": [
71
- {
72
- "type": "sha512",
73
- "value": "sha512-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
74
- "package": "@antv/g2",
75
- "wave": 2,
76
- "notes": "Placeholder sha512 — replace with actual SHA-512 integrity hash from npm dist.integrity of a confirmed malicious version."
77
- }
78
- ]
79
- }
1
+ {
2
+ "lastUpdated": "2026-05-24T00:00:00.000Z",
3
+ "waves": {
4
+ "wave1": {
5
+ "id": "mini-shai-hulud-wave1",
6
+ "description": "TanStack CI/CD hijack (mid-May 2026) — 84 malicious versions across 42 packages in ~6 minutes via compromised GitHub Actions CI. Forged SLSA BL3 provenance attestations.",
7
+ "windowMinutes": 6,
8
+ "iocs": [
9
+ {
10
+ "type": "packageScope",
11
+ "value": "@tanstack",
12
+ "maliciousVersionRanges": [],
13
+ "notes": "Seed IOC — update from threat intel feed. Affected: @tanstack/router, @tanstack/react-router, @tanstack/query, @tanstack/form, @tanstack/store, @tanstack/virtual, @tanstack/ranger, @tanstack/table."
14
+ }
15
+ ]
16
+ },
17
+ "wave2": {
18
+ "id": "mini-shai-hulud-wave2",
19
+ "description": "AntV/atool maintainer account compromise (late May 2026) — 600+ malicious versions across 300+ packages in ~22 minutes. ~16M weekly download blast radius.",
20
+ "windowMinutes": 22,
21
+ "iocs": [
22
+ {
23
+ "type": "publisherAccount",
24
+ "value": "atool",
25
+ "compromiseWindowStart": "2026-05-20T00:00:00.000Z",
26
+ "compromiseWindowEnd": null,
27
+ "notes": "Seed IOC — compromised @antv/atool maintainer account. Update compromise window from threat intel."
28
+ },
29
+ {
30
+ "type": "packageScope",
31
+ "value": "@antv",
32
+ "maliciousVersionRanges": [],
33
+ "notes": "Blast radius: @antv/g2, @antv/g6, @antv/x6, @antv/l7, echarts-for-react, timeago.js. Seed IOC — update from threat intel."
34
+ }
35
+ ]
36
+ },
37
+ "wave3": {
38
+ "id": "nx-console-wave3",
39
+ "description": "Nx Console 18.95.0 VS Code extension compromise (May 18, 2026, CVE-2026-48027, TeamPCP) — contributor token stolen via TanStack wave1 (May 11), 7-day dwell, malicious extension published using npx to fetch 498KB obfuscated Bun payload from dangling orphan commit on nrwl/nx repo. ~3M installs exposed.",
40
+ "windowMinutes": 36,
41
+ "iocs": [
42
+ {
43
+ "type": "extensionId",
44
+ "value": "nrwl.angular-console",
45
+ "maliciousVersionRanges": ["18.95.0"],
46
+ "notes": "Nx Console v18.95.0 — malicious VS Code extension. CVE-2026-48027. Exposure window: 11 min on Marketplace, 36 min on Open VSX."
47
+ },
48
+ {
49
+ "type": "publisherAccount",
50
+ "value": "nrwl",
51
+ "compromiseWindowStart": "2026-05-11T00:00:00.000Z",
52
+ "compromiseWindowEnd": "2026-05-18T13:09:00.000Z",
53
+ "notes": "Nx contributor token stolen via TanStack wave1 on May 11; 7-day dwell before publishing malicious extension on May 18."
54
+ },
55
+ {
56
+ "type": "packageScope",
57
+ "value": "@nx",
58
+ "maliciousVersionRanges": [],
59
+ "notes": "NX_CONSOLE_DOWNSTREAM: npm packages under @nx scope deployed by compromised Nx contributor. Check for versions published within 7 days of 2026-05-18."
60
+ },
61
+ {
62
+ "type": "packageScope",
63
+ "value": "nrwl",
64
+ "maliciousVersionRanges": [],
65
+ "notes": "NX_CONSOLE_DOWNSTREAM: nrwl-scoped npm packages — monitor for anomalous burst publishing."
66
+ }
67
+ ]
68
+ }
69
+ },
70
+ "iocs": [
71
+ {
72
+ "type": "sha512",
73
+ "value": "sha512-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
74
+ "package": "@antv/g2",
75
+ "wave": 2,
76
+ "notes": "Placeholder sha512 — replace with actual SHA-512 integrity hash from npm dist.integrity of a confirmed malicious version."
77
+ }
78
+ ]
79
+ }
@@ -0,0 +1,124 @@
1
+ const GCP_PATTERNS = [
2
+ 'metadata.google.internal',
3
+ 'computeMetadata/v1',
4
+ 'metadata.google.internal/computeMetadata',
5
+ ];
6
+
7
+ const AZURE_PATTERNS = [
8
+ '169.254.169.254/metadata/instance',
9
+ '169.254.169.254/metadata/identity',
10
+ ];
11
+
12
+ const AZURE_IP = '169.254.169.254';
13
+ const METADATA_HEADER_RE = /Metadata\s*:\s*true/i;
14
+
15
+ function severityLabel(score) {
16
+ if (score >= 80) return 'high';
17
+ return 'medium';
18
+ }
19
+
20
+ function confidenceLabel(score) {
21
+ if (score >= 80) return 'HIGH';
22
+ if (score >= 60) return 'MEDIUM';
23
+ return 'LOW';
24
+ }
25
+
26
+ function hasGcpPattern(text) {
27
+ return GCP_PATTERNS.some(p => text.includes(p));
28
+ }
29
+
30
+ function hasAzurePath(text) {
31
+ return AZURE_PATTERNS.some(p => text.includes(p));
32
+ }
33
+
34
+ function hasAzureHeaderPattern(text) {
35
+ const lines = text.split('\n');
36
+ for (let i = 0; i < lines.length; i++) {
37
+ if (!lines[i].includes(AZURE_IP)) continue;
38
+ const start = Math.max(0, i - 5);
39
+ const end = Math.min(lines.length, i + 6);
40
+ for (let j = start; j < end; j++) {
41
+ if (METADATA_HEADER_RE.test(lines[j])) return true;
42
+ }
43
+ }
44
+ return false;
45
+ }
46
+
47
+ function hasAzurePattern(text) {
48
+ return hasAzurePath(text) || hasAzureHeaderPattern(text);
49
+ }
50
+
51
+ function collectTexts(pkgJson, jsFiles) {
52
+ const texts = [];
53
+
54
+ if (pkgJson?.scripts && typeof pkgJson.scripts === 'object') {
55
+ for (const value of Object.values(pkgJson.scripts)) {
56
+ if (typeof value === 'string') {
57
+ texts.push(value);
58
+ }
59
+ }
60
+ }
61
+
62
+ if (jsFiles && Array.isArray(jsFiles)) {
63
+ for (const file of jsFiles) {
64
+ if (file?.content && typeof file.content === 'string') {
65
+ texts.push(file.content);
66
+ }
67
+ }
68
+ }
69
+
70
+ return texts;
71
+ }
72
+
73
+ export const name = 'tier1-cloud-imds';
74
+
75
+ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
76
+ const texts = collectTexts(pkgJson, jsFiles);
77
+ if (texts.length === 0) return [];
78
+
79
+ let hasGcp = false;
80
+ let hasAzure = false;
81
+
82
+ for (const text of texts) {
83
+ if (!hasGcp && hasGcpPattern(text)) hasGcp = true;
84
+ if (!hasAzure && hasAzurePattern(text)) hasAzure = true;
85
+ if (hasGcp && hasAzure) break;
86
+ }
87
+
88
+ if (!hasGcp && !hasAzure) return [];
89
+
90
+ let confidenceScore;
91
+ let subtype;
92
+
93
+ if (hasGcp && hasAzure) {
94
+ confidenceScore = 92;
95
+ subtype = 'multi_cloud_imds';
96
+ } else if (hasGcp) {
97
+ confidenceScore = 82;
98
+ subtype = 'gcp_metadata';
99
+ } else {
100
+ confidenceScore = 82;
101
+ subtype = 'azure_imds';
102
+ }
103
+
104
+ return [{
105
+ detector: 'tier1-cloud-imds',
106
+ id: 'TIER1-CLOUD-IMDS',
107
+ severity: severityLabel(confidenceScore),
108
+ confidence: confidenceLabel(confidenceScore),
109
+ confidenceScore,
110
+ subtype,
111
+ message: hasGcp && hasAzure
112
+ ? `Package references both GCP metadata and Azure IMDS endpoints — cloud credential harvesting`
113
+ : hasGcp
114
+ ? `Package references GCP metadata server endpoint — cloud credential harvesting`
115
+ : `Package references Azure IMDS endpoint — cloud credential harvesting`,
116
+ evidence: [
117
+ ...(hasGcp ? ['gcp: metadata.google.internal / computeMetadata/v1 pattern detected'] : []),
118
+ ...(hasAzure ? ['azure: 169.254.169.254/metadata pattern detected'] : []),
119
+ ],
120
+ crossFiles: [],
121
+ locations: [{ file: '', line: 0 }],
122
+ reference: 'Miasma Cloud IMDS',
123
+ }];
124
+ }
@@ -21,6 +21,11 @@ const EVAL_RE = /\beval\s*\(/g;
21
21
  const FUNCTION_CTOR_RE = /\bFunction\s*\(/g;
22
22
  const B64_STRING_RE = /['"`]([A-Za-z0-9+/]{40,}={0,2})['"`]/g;
23
23
 
24
+ // Named malware signatures — zero-FP string literals for confirmed campaigns
25
+ const NAMED_SIGNATURES = [
26
+ 'Miasma: The Spreading Blight', // Miasma campaign, June 2026, @redhat-cloud-services compromise
27
+ ];
28
+
24
29
  function shannonEntropy(s) {
25
30
  const len = s.length;
26
31
  if (len === 0) return 0;
@@ -171,6 +176,37 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
171
176
  if (pkgName && KNOWN_REPUTABLE_PACKAGES.has(pkgName)) return [];
172
177
 
173
178
  const files = jsFiles || [];
179
+
180
+ // Named malware signature check — zero-FP string literals, early return
181
+ const sigTexts = [];
182
+ if (pkgJson?.scripts && typeof pkgJson.scripts === 'object') {
183
+ for (const value of Object.values(pkgJson.scripts)) {
184
+ if (typeof value === 'string') sigTexts.push(value);
185
+ }
186
+ }
187
+ for (const f of files) {
188
+ if (f?.content) sigTexts.push(f.content);
189
+ }
190
+ for (const sig of NAMED_SIGNATURES) {
191
+ for (const text of sigTexts) {
192
+ if (text.includes(sig)) {
193
+ return [{
194
+ detector: 'tier1-infostealer',
195
+ id: 'TIER1-INFOSTEALER',
196
+ severity: 'critical',
197
+ confidence: 'CRITICAL',
198
+ confidenceScore: 98,
199
+ subtype: 'named_signature_miasma',
200
+ message: `Named malware signature detected: "${sig}"`,
201
+ evidence: [sig],
202
+ locations: [{ file: '', line: 0 }],
203
+ crossFiles: [],
204
+ reference: 'Campaign 2 & 3',
205
+ }];
206
+ }
207
+ }
208
+ }
209
+
174
210
  if (files.length === 0) return [];
175
211
 
176
212
  let parseFailCount = 0;
@@ -0,0 +1,81 @@
1
+ const SCAN_HOOKS = ['preinstall', 'install', 'postinstall', 'prepare'];
2
+
3
+ const REMOTE_FETCH_RE = /\b(?:fetch|axios\.get|axios\.post|http\.get|https\.get)\(|\b(?:curl|wget)\s/;
4
+ const BINARY_EXEC_RE = /\b(?:execFile|execFileSync|execSync|exec|spawnSync|spawn)\s*\(/;
5
+ const DETACHED_RE = /detached\s*:\s*true/;
6
+
7
+ function severityLabel(score) {
8
+ if (score >= 95) return 'critical';
9
+ if (score >= 80) return 'high';
10
+ if (score >= 60) return 'medium';
11
+ return 'low';
12
+ }
13
+
14
+ function confidenceLabel(score) {
15
+ if (score >= 95) return 'CRITICAL';
16
+ if (score >= 80) return 'HIGH';
17
+ if (score >= 60) return 'MEDIUM';
18
+ return 'LOW';
19
+ }
20
+
21
+ export const name = 'tier1-multistage-postinstall';
22
+
23
+ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
24
+ const scripts = pkgJson?.scripts;
25
+ if (!scripts || typeof scripts !== 'object') return [];
26
+
27
+ const findings = [];
28
+
29
+ for (const hookName of SCAN_HOOKS) {
30
+ const content = scripts[hookName];
31
+ if (!content || typeof content !== 'string') continue;
32
+
33
+ const hasRemoteFetch = REMOTE_FETCH_RE.test(content);
34
+ const hasBinaryExec = BINARY_EXEC_RE.test(content);
35
+ const hasDetached = DETACHED_RE.test(content);
36
+
37
+ const signalA = hasRemoteFetch && hasBinaryExec;
38
+ const signalB = hasDetached;
39
+
40
+ if (!signalA && !signalB) continue;
41
+
42
+ let confidenceScore;
43
+ let subtype;
44
+
45
+ if (signalA && signalB) {
46
+ confidenceScore = 95;
47
+ subtype = 'two_stage_plus_detached';
48
+ } else if (signalA) {
49
+ confidenceScore = 82;
50
+ subtype = 'two_stage_download_exec';
51
+ } else {
52
+ confidenceScore = 78;
53
+ subtype = 'detached_background_process';
54
+ }
55
+
56
+ const evidence = [`hook: ${hookName}`];
57
+ if (hasRemoteFetch) evidence.push('pattern: remote fetch call');
58
+ if (hasBinaryExec) evidence.push('pattern: binary execution call');
59
+ if (hasDetached) evidence.push('pattern: detached background process');
60
+
61
+ findings.push({
62
+ detector: 'tier1-multistage-postinstall',
63
+ id: 'TIER1-MULTISTAGE-POSTINSTALL',
64
+ severity: severityLabel(confidenceScore),
65
+ confidence: confidenceLabel(confidenceScore),
66
+ confidenceScore,
67
+ subtype,
68
+ message: `Multi-stage install hook detected in "${hookName}" — ${subtype}`,
69
+ evidence,
70
+ locations: [{
71
+ file: 'package.json',
72
+ field: `scripts.${hookName}`,
73
+ value: content.length > 200 ? `${content.slice(0, 200)}...` : content,
74
+ }],
75
+ crossFiles: [],
76
+ reference: 'Sonatype-2026-003429',
77
+ });
78
+ }
79
+
80
+ return findings;
81
+ }
@@ -0,0 +1,107 @@
1
+ import { KNOWN_REPUTABLE_PACKAGES } from '../policy.js';
2
+
3
+ const SENTINEL_EXACT = ['99.99.99'];
4
+ const SENTINEL_FAMILY = ['9.9.9', '9.9.10', '10.10.10', '11.11.11'];
5
+
6
+ function severityLabel(score) {
7
+ if (score >= 80) return 'high';
8
+ if (score >= 60) return 'medium';
9
+ return 'low';
10
+ }
11
+
12
+ function confidenceLabel(score) {
13
+ if (score >= 80) return 'HIGH';
14
+ if (score >= 60) return 'MEDIUM';
15
+ return 'LOW';
16
+ }
17
+
18
+ function parseVersion(version) {
19
+ if (!version || typeof version !== 'string') return null;
20
+ const parts = version.split('.');
21
+ if (parts.length !== 3) return null;
22
+ const [major, minor, patch] = parts.map(Number);
23
+ if (isNaN(major) || isNaN(minor) || isNaN(patch)) return null;
24
+ return { major, minor, patch };
25
+ }
26
+
27
+ function matchesHeuristic(parsed) {
28
+ return parsed.major >= 9 && parsed.minor >= 5 && parsed.patch >= 5 && parsed.major !== 1;
29
+ }
30
+
31
+ export const name = 'tier1-version-confusion';
32
+
33
+ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
34
+ const pkgName = pkgJson?.name;
35
+ const version = pkgJson?.version;
36
+
37
+ if (!pkgName || !version) return [];
38
+ if (KNOWN_REPUTABLE_PACKAGES.has(pkgName)) return [];
39
+
40
+ const parsed = parseVersion(version);
41
+ if (!parsed) return [];
42
+
43
+ const vStr = version;
44
+
45
+ // Priority: SENTINEL_EXACT > SENTINEL_FAMILY > HEURISTIC
46
+ if (SENTINEL_EXACT.includes(vStr)) {
47
+ const score = 85;
48
+ return [{
49
+ detector: 'tier1-version-confusion',
50
+ id: 'TIER1-VERSION-CONFUSION',
51
+ severity: severityLabel(score),
52
+ confidence: confidenceLabel(score),
53
+ confidenceScore: score,
54
+ subtype: 'sentinel_exact',
55
+ message: `Package "${pkgName}" uses exact sentinel version ${vStr} — dependency confusion indicator`,
56
+ evidence: [
57
+ `version: ${vStr}`,
58
+ `sentinel: exact match`,
59
+ ],
60
+ crossFiles: [],
61
+ locations: [{ file: 'package.json', line: 3, column: 10 }],
62
+ reference: 'Sonatype-2026-003429',
63
+ }];
64
+ }
65
+
66
+ if (SENTINEL_FAMILY.includes(vStr)) {
67
+ const score = 65;
68
+ return [{
69
+ detector: 'tier1-version-confusion',
70
+ id: 'TIER1-VERSION-CONFUSION',
71
+ severity: severityLabel(score),
72
+ confidence: confidenceLabel(score),
73
+ confidenceScore: score,
74
+ subtype: 'sentinel_family',
75
+ message: `Package "${pkgName}" uses sentinel family version ${vStr} — dependency confusion indicator`,
76
+ evidence: [
77
+ `version: ${vStr}`,
78
+ `sentinel: family match`,
79
+ ],
80
+ crossFiles: [],
81
+ locations: [{ file: 'package.json', line: 3, column: 10 }],
82
+ reference: 'Sonatype-2026-003429',
83
+ }];
84
+ }
85
+
86
+ if (matchesHeuristic(parsed)) {
87
+ const score = 62;
88
+ return [{
89
+ detector: 'tier1-version-confusion',
90
+ id: 'TIER1-VERSION-CONFUSION',
91
+ severity: severityLabel(score),
92
+ confidence: confidenceLabel(score),
93
+ confidenceScore: score,
94
+ subtype: 'high_version_heuristic',
95
+ message: `Package "${pkgName}" version ${vStr} matches high-version heuristic — possible dependency confusion`,
96
+ evidence: [
97
+ `version: ${vStr}`,
98
+ `major: ${parsed.major}, minor: ${parsed.minor}, patch: ${parsed.patch}`,
99
+ ],
100
+ crossFiles: [],
101
+ locations: [{ file: 'package.json', line: 3, column: 10 }],
102
+ reference: 'Microsoft Scope Confusion',
103
+ }];
104
+ }
105
+
106
+ return [];
107
+ }