@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
@@ -1,6 +1,13 @@
1
1
  import { KNOWN_REPUTABLE_PACKAGES } from '../policy.js';
2
2
 
3
- const HOOK_NAMES = ['postinstall', 'preinstall', 'install', 'prepare', 'preuninstall', 'postuninstall'];
3
+ const HOOK_NAMES = [
4
+ 'postinstall',
5
+ 'preinstall',
6
+ 'install',
7
+ 'prepare',
8
+ 'preuninstall',
9
+ 'postuninstall',
10
+ ];
4
11
 
5
12
  const CURL_WGET_RE = /\b(?:curl|wget|powershell|bash|sh)\b/i;
6
13
  const CHILD_PROC_RE = /\b(?:exec|execSync|spawn|spawnSync|fork)\s*\(/g;
@@ -17,9 +24,13 @@ const REQUIRE_RE = /\brequire\s*\(/g;
17
24
 
18
25
  function shannonEntropy(s) {
19
26
  const len = s.length;
20
- if (len === 0) return 0;
27
+ if (len === 0) {
28
+ return 0;
29
+ }
21
30
  const freq = {};
22
- for (const ch of s) freq[ch] = (freq[ch] || 0) + 1;
31
+ for (const ch of s) {
32
+ freq[ch] = (freq[ch] || 0) + 1;
33
+ }
23
34
  let entropy = 0;
24
35
  for (const count of Object.values(freq)) {
25
36
  const p = count / len;
@@ -29,20 +40,32 @@ function shannonEntropy(s) {
29
40
  }
30
41
 
31
42
  function isObfuscated(content) {
32
- if (!content) return false;
43
+ if (!content) {
44
+ return false;
45
+ }
33
46
  const noWhitespace = !/\s/.test(content.trim());
34
47
  const identifiers = content.match(/\b[a-zA-Z_$][\w$]*\b/g);
35
48
  let avgIdLen = 0;
36
49
  if (identifiers && identifiers.length > 0) {
37
50
  avgIdLen = identifiers.reduce((s, id) => s + id.length, 0) / identifiers.length;
38
51
  }
39
- if (noWhitespace && identifiers && identifiers.length > 0 && avgIdLen < 3) return true;
40
- if (noWhitespace && /^[a-zA-Z_$][\w$]*\([^)]*\)$/.test(content.trim())) return true;
52
+ if (noWhitespace && identifiers && identifiers.length > 0 && avgIdLen < 3) {
53
+ return true;
54
+ }
55
+ if (noWhitespace && /^[a-zA-Z_$][\w$]*\([^)]*\)$/.test(content.trim())) {
56
+ return true;
57
+ }
41
58
  HEX_STRING_RE.lastIndex = 0;
42
- if (HEX_STRING_RE.test(content)) return true;
59
+ if (HEX_STRING_RE.test(content)) {
60
+ return true;
61
+ }
43
62
  B64_RE.lastIndex = 0;
44
- if (B64_RE.test(content)) return true;
45
- if (shannonEntropy(content) > 5.5) return true;
63
+ if (B64_RE.test(content)) {
64
+ return true;
65
+ }
66
+ if (shannonEntropy(content) > 5.5) {
67
+ return true;
68
+ }
46
69
  return false;
47
70
  }
48
71
 
@@ -58,9 +81,11 @@ function extractUrls(content) {
58
81
 
59
82
  export const name = 'tier1-lifecycle-hook';
60
83
 
61
- export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
84
+ export async function scan(pkgJson, _jsFiles, _registryMeta, _allFiles) {
62
85
  const pkgName = pkgJson?.name;
63
- if (pkgName && KNOWN_REPUTABLE_PACKAGES.has(pkgName)) return [];
86
+ if (pkgName && KNOWN_REPUTABLE_PACKAGES.has(pkgName)) {
87
+ return [];
88
+ }
64
89
 
65
90
  const scripts = pkgJson?.scripts || {};
66
91
  const hooks = {};
@@ -71,18 +96,23 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
71
96
  }
72
97
  }
73
98
 
74
- if (Object.keys(hooks).length === 0) return [];
99
+ if (Object.keys(hooks).length === 0) {
100
+ return [];
101
+ }
75
102
 
76
103
  const findings = [];
77
104
 
78
105
  for (const [hookName, scriptContent] of Object.entries(hooks)) {
79
106
  const content = typeof scriptContent === 'string' ? scriptContent : '';
80
- if (!content) continue;
107
+ if (!content) {
108
+ continue;
109
+ }
81
110
 
82
111
  const truncated = content.length > 10240 ? content.slice(0, 10240) : content;
83
112
 
84
113
  const obfuscated = isObfuscated(truncated);
85
- const hasEval = EVAL_RE.test(truncated) || FUNCTION_CTOR_RE.test(truncated) || ZERO_EVAL_RE.test(truncated);
114
+ const hasEval =
115
+ EVAL_RE.test(truncated) || FUNCTION_CTOR_RE.test(truncated) || ZERO_EVAL_RE.test(truncated);
86
116
  const hasNetwork = CURL_WGET_RE.test(truncated) || CHILD_PROC_RE.test(truncated);
87
117
  const hasUrls = URL_RE.test(truncated) || IP_RE.test(truncated);
88
118
  const urls = extractUrls(truncated);
@@ -111,7 +141,9 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
111
141
  }
112
142
  const domainInfo = hasInternal ? 'internal domain' : 'external URL';
113
143
  evidence.push(`patterns: hardcoded ${domainInfo} in hook`);
114
- if (urls.length > 0) evidence.push(`target: ${urls[0]}`);
144
+ if (urls.length > 0) {
145
+ evidence.push(`target: ${urls[0]}`);
146
+ }
115
147
  }
116
148
 
117
149
  if (envExfil) {
@@ -143,13 +175,19 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
143
175
  baseScore = Math.min(100, Math.round(baseScore * 2.5));
144
176
  }
145
177
 
146
- if (baseScore === 0) continue;
178
+ if (baseScore === 0) {
179
+ continue;
180
+ }
147
181
 
148
182
  const confidenceScore = Math.max(50, Math.min(100, baseScore));
149
183
 
150
184
  function confidenceLabel(score) {
151
- if (score >= 95) return 'CRITICAL';
152
- if (score >= 80) return 'HIGH';
185
+ if (score >= 95) {
186
+ return 'CRITICAL';
187
+ }
188
+ if (score >= 80) {
189
+ return 'HIGH';
190
+ }
153
191
  return 'MEDIUM';
154
192
  }
155
193
 
@@ -162,11 +200,13 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
162
200
  subtype,
163
201
  message: `Suspicious lifecycle hook "${hookName}"`,
164
202
  evidence,
165
- locations: [{
166
- file: 'package.json',
167
- field: `scripts.${hookName}`,
168
- value: content.length > 200 ? `${content.slice(0, 200)}...` : content,
169
- }],
203
+ locations: [
204
+ {
205
+ file: 'package.json',
206
+ field: `scripts.${hookName}`,
207
+ value: content.length > 200 ? `${content.slice(0, 200)}...` : content,
208
+ },
209
+ ],
170
210
  crossFiles: [],
171
211
  reference: 'Campaign 1',
172
212
  });
@@ -0,0 +1,157 @@
1
+ import { KNOWN_REPUTABLE_PACKAGES } from '../policy.js';
2
+
3
+ const THRESHOLDS = {
4
+ flag_threshold: 75,
5
+ warn_threshold: 60,
6
+ velocity_burst_multiplier: 5,
7
+ burst_window_hours: 24,
8
+ min_velocity_baseline: 0.5,
9
+ duplicate_version_weight: 40,
10
+ unusual_timing_weight: 25,
11
+ cross_package_burst_weight: 50,
12
+ };
13
+
14
+ function parseVersionHistory(registryMeta) {
15
+ const timeData = registryMeta?.time;
16
+ if (!timeData || typeof timeData !== 'object') return [];
17
+
18
+ return Object.entries(timeData)
19
+ .map(([ver, ts]) => ({
20
+ version: ver,
21
+ time: new Date(ts).getTime(),
22
+ date: new Date(ts),
23
+ }))
24
+ .filter((e) => !isNaN(e.time))
25
+ .sort((a, b) => a.time - b.time);
26
+ }
27
+
28
+ function getHour(date) {
29
+ return date.getUTCHours();
30
+ }
31
+
32
+ function calculateVelocity(history) {
33
+ if (history.length < 2) return { perWeek: 0, perDay: 0 };
34
+ const firstTime = history[0].time;
35
+ const lastTime = history[history.length - 1].time;
36
+ const spanMs = lastTime - firstTime;
37
+ const spanDays = spanMs / (1000 * 60 * 60 * 24);
38
+ if (spanDays < 1) return { perWeek: history.length, perDay: history.length };
39
+ const perDay = history.length / Math.max(spanDays, 1);
40
+ const perWeek = perDay * 7;
41
+ return { perWeek, perDay };
42
+ }
43
+
44
+ function detectBursts(history) {
45
+ const bursts = [];
46
+ const windowMs = (THRESHOLDS.burst_window_hours || 24) * 60 * 60 * 1000;
47
+
48
+ for (let i = 0; i < history.length; i++) {
49
+ const windowEnd = history[i].time + windowMs;
50
+ const group = [];
51
+ for (let j = i; j < history.length && history[j].time <= windowEnd; j++) {
52
+ group.push(history[j]);
53
+ }
54
+ if (group.length >= 3) {
55
+ const groupHour = group.map((e) => getHour(e.date));
56
+ const timings = groupHour.filter((h) => h >= 0 && h <= 5);
57
+ bursts.push({
58
+ count: group.length,
59
+ windowHours: windowMs / (1000 * 60 * 60),
60
+ versions: group.map((e) => e.version),
61
+ unusualTimings: [...new Set(timings)]
62
+ .sort()
63
+ .map((h) => `${String(h).padStart(2, '0')}:00 UTC`),
64
+ startTime: group[0].date.toISOString(),
65
+ endTime: group[group.length - 1].date.toISOString(),
66
+ });
67
+ }
68
+ }
69
+ return bursts;
70
+ }
71
+
72
+ function computeConfidence(bursts, velocity, unusualTimingsCount) {
73
+ if (bursts.length === 0) return 0;
74
+ let base = 40;
75
+
76
+ const burst = bursts[0];
77
+ const burstMultiplier =
78
+ velocity.perWeek > 0
79
+ ? burst.count / Math.max(velocity.perWeek, THRESHOLDS.min_velocity_baseline)
80
+ : burst.count;
81
+
82
+ if (burstMultiplier >= THRESHOLDS.velocity_burst_multiplier) {
83
+ base += Math.min(burstMultiplier * 3, 30);
84
+ }
85
+
86
+ if (unusualTimingsCount > 0) {
87
+ base += Math.min(unusualTimingsCount * THRESHOLDS.unusual_timing_weight, 20);
88
+ }
89
+
90
+ if (burst.count >= 10) {
91
+ base += 15;
92
+ }
93
+
94
+ return Math.min(100, Math.max(0, base));
95
+ }
96
+
97
+ function severityLabel(score) {
98
+ if (score >= 80) return 'critical';
99
+ if (score >= 60) return 'high';
100
+ return 'medium';
101
+ }
102
+
103
+ function confidenceLabel(score) {
104
+ if (score >= 80) return 'CRITICAL';
105
+ if (score >= 60) return 'HIGH';
106
+ return 'MEDIUM';
107
+ }
108
+
109
+ export const name = 'tier1-maintainer-compromise';
110
+
111
+ export async function scan(pkgJson, _jsFiles, registryMeta, _allFiles) {
112
+ const pkgName = pkgJson?.name;
113
+ if (!pkgName) return [];
114
+ if (KNOWN_REPUTABLE_PACKAGES.has(pkgName)) return [];
115
+
116
+ const history = parseVersionHistory(registryMeta);
117
+ if (history.length < 3) return [];
118
+
119
+ const velocity = calculateVelocity(history);
120
+ const bursts = detectBursts(history);
121
+ if (bursts.length === 0) return [];
122
+
123
+ const burst = bursts[0];
124
+ const unusualTimings = burst.unusualTimings;
125
+ const burstMultiplier =
126
+ velocity.perWeek > 0
127
+ ? burst.count / Math.max(velocity.perWeek, THRESHOLDS.min_velocity_baseline)
128
+ : burst.count;
129
+
130
+ const crossPackageBurst = registryMeta?.crossPackageBurst || false;
131
+
132
+ const confidenceScore = computeConfidence(bursts, velocity, unusualTimings.length);
133
+ if (confidenceScore < THRESHOLDS.warn_threshold) return [];
134
+
135
+ return [
136
+ {
137
+ detector: 'tier1-maintainer-compromise',
138
+ id: 'TIER1-MAINTAINER-COMPROMISE',
139
+ severity: severityLabel(confidenceScore),
140
+ confidence: confidenceLabel(confidenceScore),
141
+ confidenceScore,
142
+ subtype: 'maintainer_compromise_burst',
143
+ message: `Maintainer compromise detected: ${burst.count} versions in ${burst.windowHours}h window (${burstMultiplier.toFixed(1)}x normal velocity)`,
144
+ evidence: [
145
+ `normal_velocity: ${velocity.perWeek.toFixed(1)}/week (${velocity.perDay.toFixed(1)}/day)`,
146
+ `burst_count: ${burst.count} in ${burst.windowHours}h`,
147
+ `burst_multiplier: ${burstMultiplier.toFixed(1)}x`,
148
+ `unusual_timings: ${unusualTimings.length > 0 ? unusualTimings.join(', ') : 'none'}`,
149
+ `cross_package: ${crossPackageBurst}`,
150
+ `versions: ${burst.versions.slice(0, 5).join(', ')}${burst.versions.length > 5 ? `... (+${burst.versions.length - 5} more)` : ''}`,
151
+ ],
152
+ locations: [{ file: 'package.json', line: 1, column: 1 }],
153
+ crossFiles: [],
154
+ reference: 'D13: @redhat-cloud-services maintainer compromise',
155
+ },
156
+ ];
157
+ }
@@ -1,68 +1,100 @@
1
1
  import { KNOWN_REPUTABLE_PACKAGES } from '../policy.js';
2
2
 
3
3
  const INTERNAL_SUFFIX_RE = /\.(?:internal|local|corp|intra|priv|lan)(?:[.:/]|$)/i;
4
- const CORPORATE_RE = /(?:github-ent|jira-ent|github\.enterprise|internal-gitlab|gitlab\.internal|jenkins\.internal|confluence\.internal)/i;
5
- const PRIVATE_IP_RE = /^(?:10\.\d{1,3}\.\d{1,3}\.\d{1,3}|172\.(?:1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3})/;
4
+ const CORPORATE_RE =
5
+ /(?:github-ent|jira-ent|github\.enterprise|internal-gitlab|gitlab\.internal|jenkins\.internal|confluence\.internal)/i;
6
+ const PRIVATE_IP_RE =
7
+ /^(?:10\.\d{1,3}\.\d{1,3}\.\d{1,3}|172\.(?:1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3})/;
6
8
 
7
9
  function extractDomain(url) {
8
10
  try {
9
11
  const u = new URL(url);
10
12
  return u.hostname;
11
13
  } catch {
12
- const m = url.match(/^(?:https?:\/\/)?([^\/\s:]+)/);
14
+ const m = url.match(/^(?:https?:\/\/)?([^/\s:]+)/);
13
15
  return m ? m[1] : null;
14
16
  }
15
17
  }
16
18
 
17
19
  function isInternalUrl(url) {
18
- if (!url) return false;
20
+ if (!url) {
21
+ return false;
22
+ }
19
23
  const domain = extractDomain(url);
20
- if (!domain) return false;
21
- if (INTERNAL_SUFFIX_RE.test(domain)) return true;
22
- if (CORPORATE_RE.test(domain)) return true;
23
- if (PRIVATE_IP_RE.test(domain)) return true;
24
+ if (!domain) {
25
+ return false;
26
+ }
27
+ if (INTERNAL_SUFFIX_RE.test(domain)) {
28
+ return true;
29
+ }
30
+ if (CORPORATE_RE.test(domain)) {
31
+ return true;
32
+ }
33
+ if (PRIVATE_IP_RE.test(domain)) {
34
+ return true;
35
+ }
24
36
  return false;
25
37
  }
26
38
 
27
39
  function parseSemver(version) {
28
- if (!version) return null;
40
+ if (!version) {
41
+ return null;
42
+ }
29
43
  const parts = version.replace(/^[~^]/, '').split('.');
30
44
  const m = parseInt(parts[0], 10);
31
45
  const n = parseInt(parts[1], 10);
32
46
  const p = parseInt(parts[2], 10);
33
- if (isNaN(m) || isNaN(n) || isNaN(p)) return null;
47
+ if (isNaN(m) || isNaN(n) || isNaN(p)) {
48
+ return null;
49
+ }
34
50
  return { major: m, minor: n, patch: p };
35
51
  }
36
52
 
37
53
  function detectSemverInflation(currentVer, registryMeta) {
38
- if (!currentVer || !registryMeta) return null;
54
+ if (!currentVer || !registryMeta) {
55
+ return null;
56
+ }
39
57
 
40
58
  const age = registryMeta.age;
41
- if (age !== undefined && age < 7) return null;
59
+ if (age !== undefined && age < 7) {
60
+ return null;
61
+ }
42
62
 
43
63
  const previousVer = registryMeta.previousVersion || null;
44
- if (!previousVer) return null;
64
+ if (!previousVer) {
65
+ return null;
66
+ }
45
67
 
46
68
  const cur = parseSemver(currentVer);
47
69
  const prev = parseSemver(previousVer);
48
- if (!cur || !prev) return null;
70
+ if (!cur || !prev) {
71
+ return null;
72
+ }
49
73
 
50
74
  const majorJump = cur.major - prev.major;
51
75
  const minorJump = cur.minor - prev.minor;
52
76
  const patchJump = cur.patch - prev.patch;
53
77
 
54
- if (majorJump > 10) return { type: 'major', from: previousVer, to: currentVer, jump: majorJump };
55
- if (minorJump > 20) return { type: 'minor', from: previousVer, to: currentVer, jump: minorJump };
56
- if (patchJump > 50) return { type: 'patch', from: previousVer, to: currentVer, jump: patchJump };
78
+ if (majorJump > 10) {
79
+ return { type: 'major', from: previousVer, to: currentVer, jump: majorJump };
80
+ }
81
+ if (minorJump > 20) {
82
+ return { type: 'minor', from: previousVer, to: currentVer, jump: minorJump };
83
+ }
84
+ if (patchJump > 50) {
85
+ return { type: 'patch', from: previousVer, to: currentVer, jump: patchJump };
86
+ }
57
87
 
58
88
  return null;
59
89
  }
60
90
 
61
91
  export const name = 'tier1-metadata-spoof';
62
92
 
63
- export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
93
+ export async function scan(pkgJson, jsFiles, registryMeta, _allFiles) {
64
94
  const pkgName = pkgJson?.name;
65
- if (pkgName && KNOWN_REPUTABLE_PACKAGES.has(pkgName)) return [];
95
+ if (pkgName && KNOWN_REPUTABLE_PACKAGES.has(pkgName)) {
96
+ return [];
97
+ }
66
98
 
67
99
  const fieldUrls = [];
68
100
 
@@ -87,7 +119,9 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
87
119
  if (pkgJson.funding) {
88
120
  const arr = Array.isArray(pkgJson.funding) ? pkgJson.funding : [pkgJson.funding];
89
121
  for (let i = 0; i < arr.length; i++) {
90
- if (arr[i] && arr[i].url) addField(`funding[${i}].url`, arr[i].url);
122
+ if (arr[i] && arr[i].url) {
123
+ addField(`funding[${i}].url`, arr[i].url);
124
+ }
91
125
  }
92
126
  }
93
127
 
@@ -95,13 +129,15 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
95
129
  addField('author.url', pkgJson.author.url);
96
130
  }
97
131
 
98
- const internalFields = fieldUrls.filter(f => isInternalUrl(f.value));
132
+ const internalFields = fieldUrls.filter((f) => isInternalUrl(f.value));
99
133
  const hasInternalUrls = internalFields.length > 0;
100
134
 
101
135
  const currentVersion = pkgJson?.version;
102
136
  const semverInflation = detectSemverInflation(currentVersion, registryMeta);
103
137
 
104
- if (!hasInternalUrls && !semverInflation) return [];
138
+ if (!hasInternalUrls && !semverInflation) {
139
+ return [];
140
+ }
105
141
 
106
142
  let baseScore = 0;
107
143
  let subtype = '';
@@ -117,10 +153,14 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
117
153
  const domain = extractDomain(f.value);
118
154
  evidence.push(`url: ${f.field} = ${f.value}`);
119
155
 
120
- let pattern = '';
121
- if (PRIVATE_IP_RE.test(domain)) pattern = 'private IP';
122
- else if (CORPORATE_RE.test(domain)) pattern = 'corporate domain';
123
- else pattern = 'internal domain';
156
+ let pattern;
157
+ if (PRIVATE_IP_RE.test(domain)) {
158
+ pattern = 'private IP';
159
+ } else if (CORPORATE_RE.test(domain)) {
160
+ pattern = 'corporate domain';
161
+ } else {
162
+ pattern = 'internal domain';
163
+ }
124
164
  evidence.push(`pattern: ${domain} (${pattern})`);
125
165
 
126
166
  locations.push({ field: f.field, value: f.value });
@@ -140,7 +180,9 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
140
180
  if (hasInternalUrls) {
141
181
  baseScore = Math.round(baseScore * 1.3);
142
182
  evidence.push(semverMsg);
143
- evidence.push(`${semverInflation.type} version jump (${semverInflation.jump}) without changelog`);
183
+ evidence.push(
184
+ `${semverInflation.type} version jump (${semverInflation.jump}) without changelog`
185
+ );
144
186
  locations.push({ field: 'version', old: semverInflation.from, new: semverInflation.to });
145
187
  primaryMessage += ' + unjustified semver jump';
146
188
  } else {
@@ -155,26 +197,34 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
155
197
  const confidenceScore = Math.max(50, Math.min(90, baseScore));
156
198
 
157
199
  function severityLabel(sc) {
158
- if (sc >= 70) return 'high';
200
+ if (sc >= 70) {
201
+ return 'high';
202
+ }
159
203
  return 'medium';
160
204
  }
161
205
 
162
206
  function confidenceLabel(sc) {
163
- if (sc >= 80) return 'HIGH';
164
- if (sc >= 60) return 'MEDIUM';
207
+ if (sc >= 80) {
208
+ return 'HIGH';
209
+ }
210
+ if (sc >= 60) {
211
+ return 'MEDIUM';
212
+ }
165
213
  return 'LOW';
166
214
  }
167
215
 
168
- return [{
169
- detector: 'tier1-metadata-spoof',
170
- id: 'TIER1-METADATA-SPOOF',
171
- severity: severityLabel(confidenceScore),
172
- confidence: confidenceLabel(confidenceScore),
173
- confidenceScore,
174
- subtype,
175
- message: primaryMessage,
176
- evidence,
177
- locations,
178
- reference: 'Campaign 1',
179
- }];
216
+ return [
217
+ {
218
+ detector: 'tier1-metadata-spoof',
219
+ id: 'TIER1-METADATA-SPOOF',
220
+ severity: severityLabel(confidenceScore),
221
+ confidence: confidenceLabel(confidenceScore),
222
+ confidenceScore,
223
+ subtype,
224
+ message: primaryMessage,
225
+ evidence,
226
+ locations,
227
+ reference: 'Campaign 1',
228
+ },
229
+ ];
180
230
  }
@@ -1,34 +1,51 @@
1
1
  const SCAN_HOOKS = ['preinstall', 'install', 'postinstall', 'prepare'];
2
2
 
3
- const REMOTE_FETCH_RE = /\b(?:fetch|axios\.get|axios\.post|http\.get|https\.get)\(|\b(?:curl|wget)\s/;
3
+ const REMOTE_FETCH_RE =
4
+ /\b(?:fetch|axios\.get|axios\.post|http\.get|https\.get)\(|\b(?:curl|wget)\s/;
4
5
  const BINARY_EXEC_RE = /\b(?:execFile|execFileSync|execSync|exec|spawnSync|spawn)\s*\(/;
5
6
  const DETACHED_RE = /detached\s*:\s*true/;
6
7
 
7
8
  function severityLabel(score) {
8
- if (score >= 95) return 'critical';
9
- if (score >= 80) return 'high';
10
- if (score >= 60) return 'medium';
9
+ if (score >= 95) {
10
+ return 'critical';
11
+ }
12
+ if (score >= 80) {
13
+ return 'high';
14
+ }
15
+ if (score >= 60) {
16
+ return 'medium';
17
+ }
11
18
  return 'low';
12
19
  }
13
20
 
14
21
  function confidenceLabel(score) {
15
- if (score >= 95) return 'CRITICAL';
16
- if (score >= 80) return 'HIGH';
17
- if (score >= 60) return 'MEDIUM';
22
+ if (score >= 95) {
23
+ return 'CRITICAL';
24
+ }
25
+ if (score >= 80) {
26
+ return 'HIGH';
27
+ }
28
+ if (score >= 60) {
29
+ return 'MEDIUM';
30
+ }
18
31
  return 'LOW';
19
32
  }
20
33
 
21
34
  export const name = 'tier1-multistage-postinstall';
22
35
 
23
- export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
36
+ export async function scan(pkgJson, _jsFiles, _registryMeta, _allFiles) {
24
37
  const scripts = pkgJson?.scripts;
25
- if (!scripts || typeof scripts !== 'object') return [];
38
+ if (!scripts || typeof scripts !== 'object') {
39
+ return [];
40
+ }
26
41
 
27
42
  const findings = [];
28
43
 
29
44
  for (const hookName of SCAN_HOOKS) {
30
45
  const content = scripts[hookName];
31
- if (!content || typeof content !== 'string') continue;
46
+ if (!content || typeof content !== 'string') {
47
+ continue;
48
+ }
32
49
 
33
50
  const hasRemoteFetch = REMOTE_FETCH_RE.test(content);
34
51
  const hasBinaryExec = BINARY_EXEC_RE.test(content);
@@ -37,7 +54,9 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
37
54
  const signalA = hasRemoteFetch && hasBinaryExec;
38
55
  const signalB = hasDetached;
39
56
 
40
- if (!signalA && !signalB) continue;
57
+ if (!signalA && !signalB) {
58
+ continue;
59
+ }
41
60
 
42
61
  let confidenceScore;
43
62
  let subtype;
@@ -54,9 +73,15 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
54
73
  }
55
74
 
56
75
  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');
76
+ if (hasRemoteFetch) {
77
+ evidence.push('pattern: remote fetch call');
78
+ }
79
+ if (hasBinaryExec) {
80
+ evidence.push('pattern: binary execution call');
81
+ }
82
+ if (hasDetached) {
83
+ evidence.push('pattern: detached background process');
84
+ }
60
85
 
61
86
  findings.push({
62
87
  detector: 'tier1-multistage-postinstall',
@@ -67,11 +92,13 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
67
92
  subtype,
68
93
  message: `Multi-stage install hook detected in "${hookName}" — ${subtype}`,
69
94
  evidence,
70
- locations: [{
71
- file: 'package.json',
72
- field: `scripts.${hookName}`,
73
- value: content.length > 200 ? `${content.slice(0, 200)}...` : content,
74
- }],
95
+ locations: [
96
+ {
97
+ file: 'package.json',
98
+ field: `scripts.${hookName}`,
99
+ value: content.length > 200 ? `${content.slice(0, 200)}...` : content,
100
+ },
101
+ ],
75
102
  crossFiles: [],
76
103
  reference: 'Sonatype-2026-003429',
77
104
  });