@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
@@ -29,9 +29,15 @@ import { scan as tier1MultistagePostinstallScan } from './tier1-multistage-posti
29
29
  import { scan as tier1VersionAnomalyScan } from './tier1-version-anomaly.js';
30
30
  import { scan as tier1ObfuscationHeuristicsScan } from './tier1-obfuscation-heuristics.js';
31
31
  import { scan as tier1SlsaAttestationScan } from './tier1-slsa-attestation.js';
32
+ import { scan as tier1SelfPropagationScan } from './tier1-self-propagation.js';
33
+ import { scan as tier1EncryptedC2Scan } from './tier1-encrypted-c2.js';
34
+ import { scan as tier1TransitiveDepsScan } from './tier1-transitive-deps.js';
35
+ import { scan as tier1MaintainerCompromiseScan } from './tier1-maintainer-compromise.js';
32
36
 
33
37
  function timeout(ms) {
34
- return new Promise((_, reject) => setTimeout(() => reject(new Error(`timeout after ${ms}ms`)), ms));
38
+ return new Promise((_, reject) =>
39
+ setTimeout(() => reject(new Error(`timeout after ${ms}ms`)), ms)
40
+ );
35
41
  }
36
42
 
37
43
  async function runTier1(name, scanFn, pkgJson, files, registryMeta, allFiles) {
@@ -43,7 +49,9 @@ async function runTier1(name, scanFn, pkgJson, files, registryMeta, allFiles) {
43
49
  const fileCount = allFiles && allFiles.length > 0 ? allFiles.length : files.length;
44
50
  if (fileCount >= 10 && result.length > 0) {
45
51
  const hitRate = result.length / fileCount;
46
- if (hitRate > 0.8) return [];
52
+ if (hitRate > 0.8) {
53
+ return [];
54
+ }
47
55
  }
48
56
  return result;
49
57
  } catch {
@@ -53,36 +61,175 @@ async function runTier1(name, scanFn, pkgJson, files, registryMeta, allFiles) {
53
61
 
54
62
  export async function runAll(pkgJson, files = [], registryMeta = null, allFiles = null) {
55
63
  const findings = [];
56
- findings.push(...await atk001.scan(pkgJson, files));
57
- findings.push(...await atk002.scan(pkgJson, files));
58
- findings.push(...await atk003.scan(pkgJson, files));
59
- findings.push(...await atk004.scan(pkgJson, files));
60
- findings.push(...await atk005.scan(pkgJson, files));
61
- findings.push(...await atk006.scan(pkgJson, files));
62
- findings.push(...await atk007.scan(pkgJson, files));
63
- findings.push(...await atk008.scan(pkgJson, files));
64
- findings.push(...await atk009.scan(pkgJson, files));
65
- findings.push(...await atk010.scan(pkgJson, files));
66
- findings.push(...await atk011.scan(pkgJson, files));
67
- findings.push(...await megalodonScan(pkgJson, allFiles || files, registryMeta));
68
- findings.push(...await hfScan(pkgJson, files, registryMeta, allFiles || files));
69
- findings.push(...await miniShaiHuludScan(pkgJson, files, registryMeta, allFiles || files));
70
- findings.push(...await badhostScan(pkgJson, files, registryMeta, allFiles || files));
71
- findings.push(...await trapdoorScan(pkgJson, files, registryMeta, allFiles || files));
72
- findings.push(...await nodeIpcScan(pkgJson, files, registryMeta, allFiles || files));
73
- findings.push(...await mshSupplementScan(pkgJson, files, registryMeta, allFiles || files));
74
- findings.push(...await typosquatScan(pkgJson, files, registryMeta, allFiles || files));
75
- findings.push(...await axiosPoisoningScan(pkgJson, files, registryMeta, allFiles || files));
76
- findings.push(...await runTier1('tier1-typosquat', tier1TyposquatScan, pkgJson, files, registryMeta, allFiles || files));
77
- findings.push(...await runTier1('tier1-infostealer', tier1InfostealerScan, pkgJson, files, registryMeta, allFiles || files));
78
- findings.push(...await runTier1('tier1-lifecycle-hook', tier1LifecycleHookScan, pkgJson, files, registryMeta, allFiles || files));
79
- findings.push(...await runTier1('tier1-binary-embed', tier1BinaryEmbedScan, pkgJson, files, registryMeta, allFiles || files));
80
- findings.push(...await runTier1('tier1-metadata-spoof', tier1MetadataSpoofScan, pkgJson, files, registryMeta, allFiles || files));
81
- findings.push(...await runTier1('tier1-version-confusion', tier1VersionConfusionScan, pkgJson, files, registryMeta, allFiles || files));
82
- findings.push(...await runTier1('tier1-cloud-imds', tier1CloudImdsScan, pkgJson, files, registryMeta, allFiles || files));
83
- findings.push(...await runTier1('tier1-multistage-postinstall', tier1MultistagePostinstallScan, pkgJson, files, registryMeta, allFiles || files));
84
- findings.push(...await runTier1('tier1-version-anomaly', tier1VersionAnomalyScan, pkgJson, files, registryMeta, allFiles || files));
85
- findings.push(...await runTier1('tier1-obfuscation-heuristics', tier1ObfuscationHeuristicsScan, pkgJson, files, registryMeta, allFiles || files));
86
- findings.push(...await runTier1('tier1-slsa-attestation', tier1SlsaAttestationScan, pkgJson, files, registryMeta, allFiles || files));
64
+ findings.push(...(await atk001.scan(pkgJson, files)));
65
+ findings.push(...(await atk002.scan(pkgJson, files)));
66
+ findings.push(...(await atk003.scan(pkgJson, files)));
67
+ findings.push(...(await atk004.scan(pkgJson, files)));
68
+ findings.push(...(await atk005.scan(pkgJson, files)));
69
+ findings.push(...(await atk006.scan(pkgJson, files)));
70
+ findings.push(...(await atk007.scan(pkgJson, files)));
71
+ findings.push(...(await atk008.scan(pkgJson, files)));
72
+ findings.push(...(await atk009.scan(pkgJson, files)));
73
+ findings.push(...(await atk010.scan(pkgJson, files)));
74
+ findings.push(...(await atk011.scan(pkgJson, files)));
75
+ findings.push(...(await megalodonScan(pkgJson, allFiles || files, registryMeta)));
76
+ findings.push(...(await hfScan(pkgJson, files, registryMeta, allFiles || files)));
77
+ findings.push(...(await miniShaiHuludScan(pkgJson, files, registryMeta, allFiles || files)));
78
+ findings.push(...(await badhostScan(pkgJson, files, registryMeta, allFiles || files)));
79
+ findings.push(...(await trapdoorScan(pkgJson, files, registryMeta, allFiles || files)));
80
+ findings.push(...(await nodeIpcScan(pkgJson, files, registryMeta, allFiles || files)));
81
+ findings.push(...(await mshSupplementScan(pkgJson, files, registryMeta, allFiles || files)));
82
+ findings.push(...(await typosquatScan(pkgJson, files, registryMeta, allFiles || files)));
83
+ findings.push(...(await axiosPoisoningScan(pkgJson, files, registryMeta, allFiles || files)));
84
+ findings.push(
85
+ ...(await runTier1(
86
+ 'tier1-typosquat',
87
+ tier1TyposquatScan,
88
+ pkgJson,
89
+ files,
90
+ registryMeta,
91
+ allFiles || files
92
+ ))
93
+ );
94
+ findings.push(
95
+ ...(await runTier1(
96
+ 'tier1-infostealer',
97
+ tier1InfostealerScan,
98
+ pkgJson,
99
+ files,
100
+ registryMeta,
101
+ allFiles || files
102
+ ))
103
+ );
104
+ findings.push(
105
+ ...(await runTier1(
106
+ 'tier1-lifecycle-hook',
107
+ tier1LifecycleHookScan,
108
+ pkgJson,
109
+ files,
110
+ registryMeta,
111
+ allFiles || files
112
+ ))
113
+ );
114
+ findings.push(
115
+ ...(await runTier1(
116
+ 'tier1-binary-embed',
117
+ tier1BinaryEmbedScan,
118
+ pkgJson,
119
+ files,
120
+ registryMeta,
121
+ allFiles || files
122
+ ))
123
+ );
124
+ findings.push(
125
+ ...(await runTier1(
126
+ 'tier1-metadata-spoof',
127
+ tier1MetadataSpoofScan,
128
+ pkgJson,
129
+ files,
130
+ registryMeta,
131
+ allFiles || files
132
+ ))
133
+ );
134
+ findings.push(
135
+ ...(await runTier1(
136
+ 'tier1-version-confusion',
137
+ tier1VersionConfusionScan,
138
+ pkgJson,
139
+ files,
140
+ registryMeta,
141
+ allFiles || files
142
+ ))
143
+ );
144
+ findings.push(
145
+ ...(await runTier1(
146
+ 'tier1-cloud-imds',
147
+ tier1CloudImdsScan,
148
+ pkgJson,
149
+ files,
150
+ registryMeta,
151
+ allFiles || files
152
+ ))
153
+ );
154
+ findings.push(
155
+ ...(await runTier1(
156
+ 'tier1-multistage-postinstall',
157
+ tier1MultistagePostinstallScan,
158
+ pkgJson,
159
+ files,
160
+ registryMeta,
161
+ allFiles || files
162
+ ))
163
+ );
164
+ findings.push(
165
+ ...(await runTier1(
166
+ 'tier1-version-anomaly',
167
+ tier1VersionAnomalyScan,
168
+ pkgJson,
169
+ files,
170
+ registryMeta,
171
+ allFiles || files
172
+ ))
173
+ );
174
+ findings.push(
175
+ ...(await runTier1(
176
+ 'tier1-obfuscation-heuristics',
177
+ tier1ObfuscationHeuristicsScan,
178
+ pkgJson,
179
+ files,
180
+ registryMeta,
181
+ allFiles || files
182
+ ))
183
+ );
184
+ findings.push(
185
+ ...(await runTier1(
186
+ 'tier1-slsa-attestation',
187
+ tier1SlsaAttestationScan,
188
+ pkgJson,
189
+ files,
190
+ registryMeta,
191
+ allFiles || files
192
+ ))
193
+ );
194
+ findings.push(
195
+ ...(await runTier1(
196
+ 'tier1-self-propagation',
197
+ tier1SelfPropagationScan,
198
+ pkgJson,
199
+ files,
200
+ registryMeta,
201
+ allFiles || files
202
+ ))
203
+ );
204
+ findings.push(
205
+ ...(await runTier1(
206
+ 'tier1-encrypted-c2',
207
+ tier1EncryptedC2Scan,
208
+ pkgJson,
209
+ files,
210
+ registryMeta,
211
+ allFiles || files
212
+ ))
213
+ );
214
+ findings.push(
215
+ ...(await runTier1(
216
+ 'tier1-transitive-deps',
217
+ tier1TransitiveDepsScan,
218
+ pkgJson,
219
+ files,
220
+ registryMeta,
221
+ allFiles || files
222
+ ))
223
+ );
224
+ findings.push(
225
+ ...(await runTier1(
226
+ 'tier1-maintainer-compromise',
227
+ tier1MaintainerCompromiseScan,
228
+ pkgJson,
229
+ files,
230
+ registryMeta,
231
+ allFiles || files
232
+ ))
233
+ );
87
234
  return findings.sort((a, b) => b.severity.localeCompare(a.severity));
88
- }
235
+ }
@@ -1,7 +1,10 @@
1
1
  const PATTERNS = [
2
2
  { id: 'EVAL_USAGE', re: /\beval\s*\(/ },
3
3
  { id: 'FUNCTION_CONSTRUCTOR', re: /Function\s*\(/ },
4
- { id: 'STRING_REVERSAL_CHAIN', re: /\.split\s*\(\s*['"]\s*['"]\s*\)\s*\.reverse\s*\(\s*\)\s*\.join\s*\(/ },
4
+ {
5
+ id: 'STRING_REVERSAL_CHAIN',
6
+ re: /\.split\s*\(\s*['"]\s*['"]\s*\)\s*\.reverse\s*\(\s*\)\s*\.join\s*\(/,
7
+ },
5
8
  { id: 'XOR_CIPHER', re: /charCodeAt\s*\([^)]*\)\s*\^\s*\w+/ },
6
9
  { id: 'BITWISE_LOOP', re: /for\s*\([^;]+;[^;]+\)\s*\{[^}]{20,}\^[^}]*\}/ },
7
10
  { id: 'DYNAMIC_REQUIRE', re: /require\s*\(\s*(?:Buffer\.from|atob|decodeURIComponent)/ },
@@ -1,6 +1,8 @@
1
1
  export function shannonEntropy(str) {
2
2
  const len = str.length;
3
- if (len === 0) return 0;
3
+ if (len === 0) {
4
+ return 0;
5
+ }
4
6
  const freq = {};
5
7
  for (const ch of str) {
6
8
  freq[ch] = (freq[ch] || 0) + 1;
@@ -14,11 +16,17 @@ export function shannonEntropy(str) {
14
16
  }
15
17
 
16
18
  export function isMinified(code) {
17
- if (code.length < 100) return false;
19
+ if (code.length < 100) {
20
+ return false;
21
+ }
18
22
  const lines = code.split('\n');
19
- if (lines.length <= 3 && code.length > 1000) return true;
23
+ if (lines.length <= 3 && code.length > 1000) {
24
+ return true;
25
+ }
20
26
  const tokens = code.match(/\b[a-zA-Z_$][\w$]*\b/g) || [];
21
- if (tokens.length < 10) return false;
27
+ if (tokens.length < 10) {
28
+ return false;
29
+ }
22
30
  const avgLen = tokens.reduce((s, t) => s + t.length, 0) / tokens.length;
23
31
  return avgLen < 3;
24
32
  }
@@ -1,7 +1,8 @@
1
1
  import { MegalodonSignal } from './types.js';
2
2
  import yaml from 'js-yaml';
3
3
 
4
- const C2_EXFIL_RE = /curl\s+.*?https?:\/\/(?!github\.com|githubusercontent\.com|raw\.githubusercontent\.com)[^\s'"]+/i;
4
+ const C2_EXFIL_RE =
5
+ /curl\s+.*?https?:\/\/(?!github\.com|githubusercontent\.com|raw\.githubusercontent\.com)[^\s'"]+/i;
5
6
  const SECRETS_REF_RE = /\$\{\{?\s*secrets\.\w+/;
6
7
  const B64_DECODE_CHAIN_RE = /base64\s+-d\s*[|>]\s*(ba)?sh/;
7
8
 
@@ -11,16 +12,23 @@ function isWorkflowFile(f) {
11
12
  }
12
13
 
13
14
  function countExecutableLines(text) {
14
- return text.split('\n').filter(l => l.trim() && !l.trim().startsWith('#')).length;
15
+ return text.split('\n').filter((l) => l.trim() && !l.trim().startsWith('#')).length;
15
16
  }
16
17
 
17
18
  function extractRunBlocks(parsed) {
18
19
  const runs = [];
19
- if (!parsed || typeof parsed !== 'object') return runs;
20
+ if (!parsed || typeof parsed !== 'object') {
21
+ return runs;
22
+ }
20
23
 
21
24
  const walk = (obj) => {
22
- if (!obj || typeof obj !== 'object') return;
23
- if (Array.isArray(obj)) { obj.forEach(walk); return; }
25
+ if (!obj || typeof obj !== 'object') {
26
+ return;
27
+ }
28
+ if (Array.isArray(obj)) {
29
+ obj.forEach(walk);
30
+ return;
31
+ }
24
32
  for (const [k, v] of Object.entries(obj)) {
25
33
  if (k === 'run' && typeof v === 'string') {
26
34
  runs.push(v);
@@ -38,21 +46,31 @@ function extractRunBlocks(parsed) {
38
46
  function extractRunBlocksRaw(text) {
39
47
  const runs = [];
40
48
  const runMatch = text.match(/run:\s*[|>]\s*\n(\s{2,}.*(?:\n\s{2,}.*)*)/g);
41
- if (runMatch) runs.push(...runMatch.map(m => m.replace(/^run:\s*[|>]\s*\n/, '')));
49
+ if (runMatch) {
50
+ runs.push(...runMatch.map((m) => m.replace(/^run:\s*[|>]\s*\n/, '')));
51
+ }
42
52
 
43
53
  const inlineRe = /run:\s*['"](.+?)['"]\s*$/gm;
44
54
  let m;
45
- while ((m = inlineRe.exec(text)) !== null) runs.push(m[1]);
55
+ while ((m = inlineRe.exec(text)) !== null) {
56
+ runs.push(m[1]);
57
+ }
46
58
 
47
59
  const envRe = /env:\s*\n((?:\s{2,}\w+:\s*.+\n?)*)/g;
48
60
  let em;
49
- while ((em = envRe.exec(text)) !== null) runs.push({ _env: em[1] });
61
+ while ((em = envRe.exec(text)) !== null) {
62
+ runs.push({ _env: em[1] });
63
+ }
50
64
  return runs;
51
65
  }
52
66
 
53
67
  function runInStepHasBoth(step, signal) {
54
68
  const runVal = step.run;
55
- const envVals = step.env ? Object.values(step.env).filter(v => typeof v === 'string').join(' ') : '';
69
+ const envVals = step.env
70
+ ? Object.values(step.env)
71
+ .filter((v) => typeof v === 'string')
72
+ .join(' ')
73
+ : '';
56
74
  const combined = typeof runVal === 'string' ? `${runVal} ${envVals}` : '';
57
75
 
58
76
  if (signal === 'exfil') {
@@ -69,19 +87,21 @@ export async function scan(allFiles) {
69
87
  const workflowFiles = allFiles.filter(isWorkflowFile);
70
88
 
71
89
  for (const f of workflowFiles) {
72
- if (f.content.length > 512 * 1024) continue;
90
+ if (f.content.length > 512 * 1024) {
91
+ continue;
92
+ }
73
93
 
74
94
  let parsed = null;
75
- let parseError = null;
95
+ let _parseError = null;
76
96
  try {
77
97
  parsed = yaml.load(f.content);
78
98
  } catch (e) {
79
- parseError = e;
99
+ _parseError = e;
80
100
  }
81
101
 
82
102
  const rawRunBlocks = parsed ? extractRunBlocks(parsed) : extractRunBlocksRaw(f.content);
83
- const runStrings = rawRunBlocks.filter(r => typeof r === 'string');
84
- const envBlocks = rawRunBlocks.filter(r => typeof r === 'object' && r._env);
103
+ const runStrings = rawRunBlocks.filter((r) => typeof r === 'string');
104
+ const _envBlocks = rawRunBlocks.filter((r) => typeof r === 'object' && r._env);
85
105
 
86
106
  let exfilTriggered = false;
87
107
  let decodeTriggered = false;
@@ -109,7 +129,7 @@ export async function scan(allFiles) {
109
129
  }
110
130
 
111
131
  if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
112
- const steps = parsed.jobs ? Object.values(parsed.jobs).flatMap(j => j.steps || []) : [];
132
+ const steps = parsed.jobs ? Object.values(parsed.jobs).flatMap((j) => j.steps || []) : [];
113
133
  for (const step of steps) {
114
134
  if (!exfilTriggered && runInStepHasBoth(step, 'exfil')) {
115
135
  exfilTriggered = true;
@@ -136,7 +156,11 @@ export async function scan(allFiles) {
136
156
 
137
157
  const lineCount = countExecutableLines(f.content);
138
158
  if ((exfilTriggered || decodeTriggered) && lineCount >= 100 && lineCount <= 120) {
139
- const found = evidence.find(e => e.signal === MegalodonSignal.WORKFLOW_C2_EXFIL || e.signal === MegalodonSignal.WORKFLOW_DECODE_CHAIN);
159
+ const found = evidence.find(
160
+ (e) =>
161
+ e.signal === MegalodonSignal.WORKFLOW_C2_EXFIL ||
162
+ e.signal === MegalodonSignal.WORKFLOW_DECODE_CHAIN
163
+ );
140
164
  if (found) {
141
165
  found.detail += ` | Matches ${lineCount}-line Megalodon payload footprint`;
142
166
  }
@@ -3,7 +3,10 @@ import { MegalodonSignal } from './types.js';
3
3
  const CRED_PATTERNS = [
4
4
  { pattern: /\bAWS_(SECRET_ACCESS_KEY|ACCESS_KEY_ID|SESSION_TOKEN)\b/, label: 'AWS credential' },
5
5
  { pattern: /\bGOOGLE_APPLICATION_CREDENTIALS\b/, label: 'GCP credential' },
6
- { pattern: /\bAZURE_(CLIENT_SECRET|TENANT_ID|CLIENT_ID|SUBSCRIPTION_ID)\b/, label: 'Azure credential' },
6
+ {
7
+ pattern: /\bAZURE_(CLIENT_SECRET|TENANT_ID|CLIENT_ID|SUBSCRIPTION_ID)\b/,
8
+ label: 'Azure credential',
9
+ },
7
10
  { pattern: /\bGH_(TOKEN|PAT)\b/, label: 'GitHub PAT' },
8
11
  { pattern: /\bGITHUB_TOKEN\b/, label: 'GitHub token' },
9
12
  { pattern: /\bNPM_TOKEN\b/, label: 'npm token' },
@@ -15,7 +18,8 @@ const CRED_PATTERNS = [
15
18
  { pattern: /\bMONGO_(URI|URL|CONNECTION)\b/, label: 'MongoDB connection' },
16
19
  ];
17
20
 
18
- const OUTBOUND_NET_RE = /curl\s+|wget\s+|fetch\s*\(|https?\.request\s*\(|http\.request\s*\(|got\s*\(|axios\s*\.|request\s*\(|node-fetch|\.post\s*\(|\.get\s*\(/i;
21
+ const OUTBOUND_NET_RE =
22
+ /curl\s+|wget\s+|fetch\s*\(|https?\.request\s*\(|http\.request\s*\(|got\s*\(|axios\s*\.|request\s*\(|node-fetch|\.post\s*\(|\.get\s*\(/i;
19
23
 
20
24
  const TARGET_EXTENSIONS = ['.sh', '.bash', '.yml', '.yaml', '.js'];
21
25
 
@@ -37,7 +41,7 @@ export async function scan(allFiles) {
37
41
  const re = new RegExp(cp.pattern.source, 'gi');
38
42
  let m;
39
43
  while ((m = re.exec(content)) !== null) {
40
- if (!matched.some(ex => ex.label === cp.label)) {
44
+ if (!matched.some((ex) => ex.label === cp.label)) {
41
45
  matched.push({ label: cp.label, match: m[0] });
42
46
  }
43
47
  score += 3;
@@ -50,8 +54,11 @@ export async function scan(allFiles) {
50
54
  evidence.push({
51
55
  signal: MegalodonSignal.CREDENTIAL_HARVEST,
52
56
  file: f.path,
53
- excerpt: matched.map(m => m.label).join(', ').slice(0, 120),
54
- detail: `Credential env vars (${matched.map(m => m.label).join(', ')}) co-occur with outbound network call (score: ${score})`,
57
+ excerpt: matched
58
+ .map((m) => m.label)
59
+ .join(', ')
60
+ .slice(0, 120),
61
+ detail: `Credential env vars (${matched.map((m) => m.label).join(', ')}) co-occur with outbound network call (score: ${score})`,
55
62
  });
56
63
  }
57
64
  }
@@ -3,7 +3,9 @@ import { MegalodonSignal } from './types.js';
3
3
  export function detectVelocitySpike(times, windowHours = 6, threshold = 3) {
4
4
  const filtered = {};
5
5
  for (const [v, t] of Object.entries(times)) {
6
- if (v === 'created' || v === 'modified') continue;
6
+ if (v === 'created' || v === 'modified') {
7
+ continue;
8
+ }
7
9
  filtered[v] = t;
8
10
  }
9
11
 
@@ -33,7 +35,7 @@ export function detectVelocitySpike(times, windowHours = 6, threshold = 3) {
33
35
  }
34
36
 
35
37
  if (inWindow.length >= threshold) {
36
- let display = inWindow.slice(0, 10);
38
+ const display = inWindow.slice(0, 10);
37
39
  let suffix = '';
38
40
  if (inWindow.length > 10) {
39
41
  suffix = ` +${inWindow.length - 10} more`;
@@ -54,14 +56,18 @@ export async function scan(registryMeta) {
54
56
  const times = registryMeta?.time || {};
55
57
  const result = detectVelocitySpike(times);
56
58
 
57
- if (!result.triggered) return [];
59
+ if (!result.triggered) {
60
+ return [];
61
+ }
58
62
 
59
- return [{
60
- signal: MegalodonSignal.PUBLISH_VELOCITY,
61
- file: 'registry.npmjs.org',
62
- excerpt: result.versionsInWindow,
63
- detail: `Version publish velocity spike: ${result.versionsInWindow} versions in window starting ${result.windowStartISO}`,
64
- _windowStartISO: result.windowStartISO,
65
- _allVersions: result._allVersions,
66
- }];
63
+ return [
64
+ {
65
+ signal: MegalodonSignal.PUBLISH_VELOCITY,
66
+ file: 'registry.npmjs.org',
67
+ excerpt: result.versionsInWindow,
68
+ detail: `Version publish velocity spike: ${result.versionsInWindow} versions in window starting ${result.windowStartISO}`,
69
+ _windowStartISO: result.windowStartISO,
70
+ _allVersions: result._allVersions,
71
+ },
72
+ ];
67
73
  }
@@ -7,8 +7,12 @@ export async function scan(registryMeta, velocityResult) {
7
7
 
8
8
  const filteredTimes = {};
9
9
  for (const [v, t] of Object.entries(timeMap)) {
10
- if (v === 'created' || v === 'modified') continue;
11
- if (t) filteredTimes[v] = t;
10
+ if (v === 'created' || v === 'modified') {
11
+ continue;
12
+ }
13
+ if (t) {
14
+ filteredTimes[v] = t;
15
+ }
12
16
  }
13
17
 
14
18
  const sortedVersions = Object.entries(filteredTimes)
@@ -16,7 +20,9 @@ export async function scan(registryMeta, velocityResult) {
16
20
  .sort((a, b) => new Date(a[1]).getTime() - new Date(b[1]).getTime())
17
21
  .map(([v]) => v);
18
22
 
19
- if (sortedVersions.length === 0) return [];
23
+ if (sortedVersions.length === 0) {
24
+ return [];
25
+ }
20
26
 
21
27
  if (velocityResult?.triggered) {
22
28
  const windowStartISO = velocityResult.windowStartISO;
@@ -24,14 +30,20 @@ export async function scan(registryMeta, velocityResult) {
24
30
 
25
31
  const priorPublishers = new Set();
26
32
  for (const v of sortedVersions) {
27
- if (new Date(filteredTimes[v]).getTime() >= new Date(windowStartISO).getTime()) break;
33
+ if (new Date(filteredTimes[v]).getTime() >= new Date(windowStartISO).getTime()) {
34
+ break;
35
+ }
28
36
  const user = versions[v]?._npmUser?.name;
29
- if (user) priorPublishers.add(user);
37
+ if (user) {
38
+ priorPublishers.add(user);
39
+ }
30
40
  }
31
41
 
32
42
  if (priorPublishers.size === 0 && allInWindow.length > 0) {
33
43
  const firstUser = versions[allInWindow[0]]?._npmUser?.name;
34
- if (firstUser) priorPublishers.add(firstUser);
44
+ if (firstUser) {
45
+ priorPublishers.add(firstUser);
46
+ }
35
47
  }
36
48
 
37
49
  const suspiciousPublishers = [];
@@ -39,15 +51,19 @@ export async function scan(registryMeta, velocityResult) {
39
51
  for (const v of allInWindow) {
40
52
  const user = versions[v]?._npmUser?.name;
41
53
  if (user && !priorPublishers.has(user)) {
42
- if (!suspiciousPublishers.includes(user)) suspiciousPublishers.push(user);
43
- if (!affectedVersions.includes(v)) affectedVersions.push(v);
54
+ if (!suspiciousPublishers.includes(user)) {
55
+ suspiciousPublishers.push(user);
56
+ }
57
+ if (!affectedVersions.includes(v)) {
58
+ affectedVersions.push(v);
59
+ }
44
60
  }
45
61
  }
46
62
 
47
63
  if (suspiciousPublishers.length > 0) {
48
64
  const detail = `Drift detected: known publishers [${[...priorPublishers].join(', ')}], new publisher(s) [${suspiciousPublishers.join(', ')}] in versions [${affectedVersions.join(', ')}]`;
49
65
 
50
- const firstSuspiciousVer = allInWindow.find(v => affectedVersions.includes(v));
66
+ const firstSuspiciousVer = allInWindow.find((v) => affectedVersions.includes(v));
51
67
  let ageNote = '';
52
68
  if (firstSuspiciousVer && suspiciousPublishers[0]) {
53
69
  ageNote = await checkAccountAge(suspiciousPublishers[0], filteredTimes[firstSuspiciousVer]);
@@ -62,7 +78,9 @@ export async function scan(registryMeta, velocityResult) {
62
78
  });
63
79
  }
64
80
  } else {
65
- if (sortedVersions.length < 4) return [];
81
+ if (sortedVersions.length < 4) {
82
+ return [];
83
+ }
66
84
 
67
85
  const last3 = sortedVersions.slice(-3);
68
86
  const prior = sortedVersions.slice(0, -3);
@@ -70,7 +88,9 @@ export async function scan(registryMeta, velocityResult) {
70
88
  const priorPublishers = new Set();
71
89
  for (const v of prior) {
72
90
  const user = versions[v]?._npmUser?.name;
73
- if (user) priorPublishers.add(user);
91
+ if (user) {
92
+ priorPublishers.add(user);
93
+ }
74
94
  }
75
95
 
76
96
  const suspiciousPublishers = [];
@@ -78,8 +98,12 @@ export async function scan(registryMeta, velocityResult) {
78
98
  for (const v of last3) {
79
99
  const user = versions[v]?._npmUser?.name;
80
100
  if (user && !priorPublishers.has(user)) {
81
- if (!suspiciousPublishers.includes(user)) suspiciousPublishers.push(user);
82
- if (!affectedVersions.includes(v)) affectedVersions.push(v);
101
+ if (!suspiciousPublishers.includes(user)) {
102
+ suspiciousPublishers.push(user);
103
+ }
104
+ if (!affectedVersions.includes(v)) {
105
+ affectedVersions.push(v);
106
+ }
83
107
  }
84
108
  }
85
109
 
@@ -88,7 +112,10 @@ export async function scan(registryMeta, velocityResult) {
88
112
 
89
113
  let ageNote = '';
90
114
  if (suspiciousPublishers[0] && affectedVersions[0]) {
91
- ageNote = await checkAccountAge(suspiciousPublishers[0], filteredTimes[affectedVersions[0]]);
115
+ ageNote = await checkAccountAge(
116
+ suspiciousPublishers[0],
117
+ filteredTimes[affectedVersions[0]]
118
+ );
92
119
  }
93
120
 
94
121
  evidence.push({
@@ -108,10 +135,14 @@ async function checkAccountAge(npmUser, firstSuspiciousTime) {
108
135
  try {
109
136
  const url = `https://registry.npmjs.org/-/user/org.couchdb.user/${encodeURIComponent(npmUser)}`;
110
137
  const res = await fetch(url);
111
- if (!res.ok) return '';
138
+ if (!res.ok) {
139
+ return '';
140
+ }
112
141
  const data = await res.json();
113
142
  const created = data?.date;
114
- if (!created) return '';
143
+ if (!created) {
144
+ return '';
145
+ }
115
146
  const createdDate = new Date(created).getTime();
116
147
  const firstPub = new Date(firstSuspiciousTime).getTime();
117
148
  const daysDiff = (firstPub - createdDate) / (1000 * 60 * 60 * 24);
@@ -119,6 +150,7 @@ async function checkAccountAge(npmUser, firstSuspiciousTime) {
119
150
  return `Publisher account created ${Math.round(daysDiff)} days before first suspicious publish`;
120
151
  }
121
152
  } catch {
153
+ /* ignore fetch errors */
122
154
  }
123
155
  return '';
124
156
  }
@@ -1,3 +1,3 @@
1
- export async function scan(registryMeta) {
1
+ export async function scan(_registryMeta) {
2
2
  return [];
3
3
  }
@@ -1,3 +1,3 @@
1
- export async function scan(pkgJson, registryMeta) {
1
+ export async function scan(_pkgJson, _registryMeta) {
2
2
  return [];
3
3
  }