@lateos/npm-scan 0.18.2 → 1.0.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 (113) hide show
  1. package/CHANGELOG.md +265 -233
  2. package/LICENSING.md +19 -19
  3. package/README.de.md +708 -708
  4. package/README.fr.md +707 -707
  5. package/README.ja.md +704 -704
  6. package/README.md +861 -826
  7. package/README.zh.md +708 -708
  8. package/VALIDATION.md +92 -0
  9. package/backend/cra.js +68 -68
  10. package/backend/db/pg-schema.sql +155 -0
  11. package/backend/db/schema.sql +32 -32
  12. package/backend/db.js +88 -88
  13. package/backend/detectors/atk-001-lifecycle.js +17 -17
  14. package/backend/detectors/atk-002-obfusc.js +261 -261
  15. package/backend/detectors/atk-003-creds.js +13 -13
  16. package/backend/detectors/atk-004-persist.js +13 -13
  17. package/backend/detectors/atk-005-exfil.js +13 -13
  18. package/backend/detectors/atk-006-depconf.js +14 -14
  19. package/backend/detectors/atk-007-typosquat.js +34 -34
  20. package/backend/detectors/atk-008-tarball-tamper.js +91 -91
  21. package/backend/detectors/atk-009-dormant-trigger.js +62 -62
  22. package/backend/detectors/atk-010-sandbox-evasion.js +50 -50
  23. package/backend/detectors/atk-011-transitive-prop.js +76 -76
  24. package/backend/detectors/config/thresholds.js +66 -0
  25. package/backend/detectors/config/whitelist.json +74 -0
  26. package/backend/detectors/cve-2026-48710-badhost/codePattern.js +99 -99
  27. package/backend/detectors/cve-2026-48710-badhost/findings.js +105 -105
  28. package/backend/detectors/cve-2026-48710-badhost/index.js +15 -15
  29. package/backend/detectors/cve-2026-48710-badhost/manifest.js +305 -305
  30. package/backend/detectors/cve-2026-48710-badhost/transitive.js +189 -189
  31. package/backend/detectors/hf-impersonation/index.js +396 -396
  32. package/backend/detectors/hf-impersonation/jaro-winkler.js +44 -44
  33. package/backend/detectors/hf-impersonation/known-orgs.js +5 -5
  34. package/backend/detectors/hf-impersonation/simhash.js +46 -46
  35. package/backend/detectors/index.js +87 -81
  36. package/backend/detectors/lib/ast-patterns.js +21 -0
  37. package/backend/detectors/lib/entropy-analyzer.js +24 -0
  38. package/backend/detectors/megalodon/d1-workflow-scan.js +147 -147
  39. package/backend/detectors/megalodon/d2-credential-harvest.js +61 -61
  40. package/backend/detectors/megalodon/d3-publish-velocity.js +67 -67
  41. package/backend/detectors/megalodon/d4-publisher-drift.js +124 -124
  42. package/backend/detectors/megalodon/d5-bot-commit-identity.js +3 -3
  43. package/backend/detectors/megalodon/d6-date-anachronism.js +3 -3
  44. package/backend/detectors/megalodon/index.js +80 -80
  45. package/backend/detectors/megalodon/types.js +9 -9
  46. package/backend/detectors/mini-shai-hulud/d1-burst-publish.js +42 -42
  47. package/backend/detectors/mini-shai-hulud/d2-sibling-compromise.js +116 -116
  48. package/backend/detectors/mini-shai-hulud/d3-slsa-mismatch.js +72 -72
  49. package/backend/detectors/mini-shai-hulud/d4-maintainer-anomaly.js +45 -45
  50. package/backend/detectors/mini-shai-hulud/d5-ioc-check.js +95 -95
  51. package/backend/detectors/mini-shai-hulud/d6-token-exfil.js +38 -38
  52. package/backend/detectors/mini-shai-hulud/index.js +118 -118
  53. package/backend/detectors/mini-shai-hulud/iocs.json +79 -79
  54. package/backend/detectors/tier1-binary-embed.js +34 -5
  55. package/backend/detectors/tier1-obfuscation-heuristics.js +156 -0
  56. package/backend/detectors/tier1-slsa-attestation.js +12 -0
  57. package/backend/detectors/tier1-version-anomaly.js +187 -0
  58. package/backend/detectors.test.js +88 -0
  59. package/backend/fetch.js +175 -175
  60. package/backend/index.js +4 -4
  61. package/backend/license.js +89 -89
  62. package/backend/lockfile.js +379 -379
  63. package/backend/pdf.js +245 -245
  64. package/backend/policy.js +193 -193
  65. package/backend/report.js +254 -254
  66. package/backend/sbom.js +66 -66
  67. package/backend/scripts/analyze-false-positives.js +146 -0
  68. package/backend/scripts/analyze-validation.js +151 -0
  69. package/backend/scripts/detect-false-positives.js +93 -0
  70. package/backend/scripts/fetch-top-packages.js +129 -0
  71. package/backend/scripts/validate-detectors.js +142 -0
  72. package/backend/siem/cef.js +32 -32
  73. package/backend/siem/ecs.js +40 -40
  74. package/backend/siem/index.js +18 -18
  75. package/backend/siem/qradar.js +56 -56
  76. package/backend/siem/sentinel.js +27 -27
  77. package/backend/tests-d5-enhanced.test.js +46 -0
  78. package/backend/tests-d6-version-anomaly.test.js +58 -0
  79. package/backend/tests-d6.test.js +116 -0
  80. package/backend/tests-d6c.test.js +106 -0
  81. package/backend/tests-d7-obfuscation.test.js +91 -0
  82. package/backend/tests.test.js +898 -0
  83. package/backend/vsix-scan/detectors/activation-event-risk.js +116 -116
  84. package/backend/vsix-scan/detectors/burst-publish.js +52 -52
  85. package/backend/vsix-scan/detectors/exfil-pattern.js +88 -88
  86. package/backend/vsix-scan/detectors/known-ioc.js +105 -105
  87. package/backend/vsix-scan/detectors/orphan-commit-fetch.js +69 -69
  88. package/backend/vsix-scan/detectors/publisher-anomaly.js +70 -70
  89. package/backend/vsix-scan/index.js +183 -183
  90. package/backend/vsix-scan/marketplace-client.js +145 -145
  91. package/backend/vsix-scan/vsix-iocs.json +31 -31
  92. package/cli/cli.js +458 -458
  93. package/package.json +74 -57
  94. package/.dockerignore +0 -20
  95. package/.husky/pre-commit +0 -1
  96. package/SECURITY.md +0 -73
  97. package/deploy/helm/npm-scan/Chart.yaml +0 -22
  98. package/deploy/helm/npm-scan/templates/_helpers.tpl +0 -9
  99. package/deploy/helm/npm-scan/templates/api.yaml +0 -94
  100. package/deploy/helm/npm-scan/templates/ingress.yaml +0 -28
  101. package/deploy/helm/npm-scan/templates/postgresql.yaml +0 -67
  102. package/deploy/helm/npm-scan/templates/secrets.yaml +0 -19
  103. package/deploy/helm/npm-scan/templates/worker.yaml +0 -32
  104. package/deploy/helm/npm-scan/values.byoc.yaml +0 -75
  105. package/deploy/helm/npm-scan/values.yaml +0 -103
  106. package/scripts/download-corpus.js +0 -30
  107. package/scripts/gen-mal-corpus.js +0 -35
  108. package/scripts/generate-campaign-fixtures.js +0 -170
  109. package/src/config/top-5000.json +0 -87
  110. package/test/fixtures/lockfiles/npm-lock.json +0 -69
  111. package/test/fixtures/lockfiles/pnpm-lock.yaml +0 -118
  112. package/test/fixtures/lockfiles/yarn.lock +0 -104
  113. package/test/fixtures/mock-data.js +0 -69
@@ -1,262 +1,262 @@
1
- const DIST_BUILD_PATTERNS = [/\/dist\//, /\/build\//, /\/bundle/, /\/min\//, /\.min\.js$/, /\.bundled?\.js$/];
2
- const TEST_FIXTURE_PATTERNS = [/\/test\//, /\/tests\//, /\/__tests__\//, /\/spec\//, /\.test\.js$/, /\.spec\.js$/, /fixtures?/];
3
- const KNOWN_SAFE_DOMAINS = [
4
- 'registry.npmjs.org', 'cdn.jsdelivr.net', 'unpkg.com', 'cdn.skypack.dev',
5
- 'esm.sh', 'deno.land', 'raw.githubusercontent.com', 'github.com',
6
- 'npmjs.com', 'nodejs.org', 'v8.dev', 'typescriptlang.org'
7
- ];
8
-
9
- const LIFECYCLE_SCRIPT_NAMES = ['install', 'postinstall', 'preinstall', 'prepare', 'prepack', 'postpack'];
10
-
11
- function extractUrlDomain(code) {
12
- const urlMatch = code.match(/https?:\/\/([^/'"\s]+)/);
13
- return urlMatch ? urlMatch[1] : null;
14
- }
15
-
16
- function isDistOrBuild(filePath) {
17
- return DIST_BUILD_PATTERNS.some(p => p.test(filePath));
18
- }
19
-
20
- function isTestOrFixture(filePath) {
21
- return TEST_FIXTURE_PATTERNS.some(p => p.test(filePath));
22
- }
23
-
24
- function isKnownSafeDomain(domain) {
25
- if (!domain) return false;
26
- return KNOWN_SAFE_DOMAINS.some(safe => domain === safe || domain.endsWith('.' + safe));
27
- }
28
-
29
- function locateLine(code, pattern) {
30
- const lines = code.split('\n');
31
- for (let i = 0; i < lines.length; i++) {
32
- if (pattern.test(lines[i])) return i + 1;
33
- }
34
- return null;
35
- }
36
-
37
- function decodePreview(code) {
38
- const b64Match = code.match(/atob\(['"]([A-Za-z0-9+/=]{10,})['"]\)/);
39
- if (b64Match) {
40
- try {
41
- const decoded = atob(b64Match[1]);
42
- return decoded.length > 80 ? decoded.slice(0, 80) + '...' : decoded;
43
- } catch {}
44
- }
45
-
46
- const hexMatch = code.match(/Buffer\.from\(['"]([0-9a-fA-F]+)['"],\s*['"]hex['"]\)/);
47
- if (hexMatch) {
48
- try {
49
- const decoded = Buffer.from(hexMatch[1], 'hex').toString();
50
- return decoded.length > 80 ? decoded.slice(0, 80) + '...' : decoded;
51
- } catch {}
52
- }
53
-
54
- const btoaMatch = code.match(/btoa\(['"]([A-Za-z0-9+/=]{10,})['"]\)/);
55
- if (btoaMatch) {
56
- try {
57
- const decoded = atob(btoaMatch[1]);
58
- return decoded.length > 80 ? decoded.slice(0, 80) + '...' : decoded;
59
- } catch {}
60
- }
61
-
62
- return null;
63
- }
64
-
65
- function detectEncodingType(code) {
66
- if (/Buffer\.from\(['"][0-9a-fA-F]+['"],\s*['"]hex['"]\)/.test(code)) return 'hex';
67
- if (/atob\(/.test(code)) return 'base64';
68
- if (/btoa\(/.test(code)) return 'base64';
69
- if (/Buffer\.from\([A-Za-z0-9+/=]{10,}/.test(code)) return 'base64';
70
- if (/String\.fromCharCode\(/.test(code)) return 'charcode';
71
- if (/btoa\(.*btoa\(|atob\(.*atob\(/.test(code)) return 'double-base64';
72
- return 'unknown';
73
- }
74
-
75
- function isFileInLifecycleScript(filePath, pkgJson) {
76
- if (!pkgJson?.scripts) return false;
77
-
78
- const scripts = pkgJson.scripts;
79
- const fileName = filePath.split('/').pop();
80
- const normalizedPath = filePath.replace(/^node_modules\//, '').replace(/^dist\//, '').replace(/^build\//, '');
81
-
82
- for (const scriptName of LIFECYCLE_SCRIPT_NAMES) {
83
- const scriptValue = scripts[scriptName];
84
- if (!scriptValue) continue;
85
-
86
- if (scriptValue.includes(filePath)) return true;
87
- if (scriptValue.includes(fileName)) return true;
88
- if (scriptValue.includes(normalizedPath)) return true;
89
-
90
- const scriptFileMatch = scriptValue.match(/[^\s'"]+\.js$/);
91
- if (scriptFileMatch && filePath.endsWith(scriptFileMatch[0])) return true;
92
- }
93
-
94
- return false;
95
- }
96
-
97
- function isLikelyLifecycleFileName(filePath) {
98
- const name = filePath.split('/').pop().replace(/\.js$/, '');
99
- return LIFECYCLE_SCRIPT_NAMES.includes(name) ||
100
- name === 'setup' ||
101
- name === 'install-helper';
102
- }
103
-
104
- function createEvidence(code, filePath, pattern, pkgJson) {
105
- const encodingType = detectEncodingType(code);
106
- const line = locateLine(code, pattern);
107
- const decodedPreview = decodePreview(code);
108
- const destinationHost = extractUrlDomain(code);
109
- const lifecycleHook = isFileInLifecycleScript(filePath, pkgJson) || isLikelyLifecycleFileName(filePath);
110
-
111
- return {
112
- file: filePath,
113
- line: line,
114
- lifecycle_hook: lifecycleHook,
115
- decoded_preview: decodedPreview,
116
- encoding_type: encodingType,
117
- destination_host: destinationHost,
118
- };
119
- }
120
-
121
- export async function scan(pkgJson, files = []) {
122
- const findings = [];
123
- const pkgName = pkgJson?.name || '';
124
- const selfName = pkgName.replace(/^@/, '').replace(/\//, '-');
125
-
126
- for (const f of files) {
127
- const code = f.content;
128
- const filePath = f.path;
129
-
130
- const isDistBuild = isDistOrBuild(filePath);
131
- const isTestFixture = isTestOrFixture(filePath);
132
- const urlDomain = extractUrlDomain(code);
133
- const isSafeDomain = isKnownSafeDomain(urlDomain);
134
-
135
- const hasEval = /eval\(|new Function\(|\bFunction\('/.test(code);
136
-
137
- if (hasEval) {
138
- const hexDecode = /Buffer\.from\(['"`][0-9a-f]+['"`],\s*['"]hex['"]/.test(code);
139
- const b64Decode = /atob\(|Buffer\.from\([A-Za-z0-9+/=]{10,}/.test(code);
140
- const b64UrlDecode = /try\s*\{[^}]*atob\s*\(/s.test(code) || /btoa\(.*\)\s*[^;]*\.replace\(/s.test(code);
141
-
142
- if (hexDecode || b64Decode || b64UrlDecode) {
143
- const evidence = createEvidence(code, filePath, /eval\(|new Function\(|\bFunction\('/, pkgJson);
144
- findings.push({
145
- id: 'ATK-002',
146
- severity: 'medium',
147
- title: 'Obfuscated payload',
148
- description: hexDecode ? 'Eval with hex-decoded payload' : 'Eval with base64-decoded payload',
149
- evidence: evidence,
150
- context: {
151
- file_path: filePath,
152
- is_dist_build: isDistBuild,
153
- is_test_fixture: isTestFixture,
154
- is_lifecycle_hook: evidence.lifecycle_hook,
155
- url_domain: urlDomain,
156
- is_known_safe_domain: isSafeDomain,
157
- },
158
- });
159
- return findings;
160
- }
161
-
162
- if (btoa(btoa('x')) === 'eDuke'.padEnd(5)) {
163
- const nested = /atob\([^)]*atob\(/s.test(code) || /btoa\([^)]*btoa\(/s.test(code);
164
- if (nested) {
165
- const evidence = createEvidence(code, filePath, /btoa\(/, pkgJson);
166
- findings.push({
167
- id: 'ATK-002',
168
- severity: 'high',
169
- title: 'Obfuscated payload',
170
- description: 'Double-encoded nested payload',
171
- evidence: { ...evidence, is_multi_layer: true },
172
- context: {
173
- file_path: filePath,
174
- is_dist_build: isDistBuild,
175
- is_test_fixture: isTestFixture,
176
- is_lifecycle_hook: evidence.lifecycle_hook,
177
- url_domain: urlDomain,
178
- is_known_safe_domain: isSafeDomain,
179
- is_multi_layer: true,
180
- },
181
- });
182
- return findings;
183
- }
184
- }
185
- }
186
-
187
- if (/atob\(|Buffer\.from/.test(code) && /url|fetch|curl|http\.request|https\.request/.test(code)) {
188
- const isNetworkObfusc = /atob\(.*(https?:\/\/|\\x|http).*\)/s.test(code) ||
189
- /Buffer\.from\(['"`][0-9a-f]+['"`],\s*['"]hex['"].*fetch\(|fetch\(.*atob\(/s.test(code);
190
- if (isNetworkObfusc) {
191
- const evidence = createEvidence(code, filePath, /atob\(|Buffer\.from/, pkgJson);
192
- findings.push({
193
- id: 'ATK-002',
194
- severity: 'medium',
195
- title: 'Obfuscated payload',
196
- description: 'Decoded string containing URL/fetch call',
197
- evidence: evidence,
198
- context: {
199
- file_path: filePath,
200
- is_dist_build: isDistBuild,
201
- is_test_fixture: isTestFixture,
202
- is_lifecycle_hook: evidence.lifecycle_hook,
203
- url_domain: urlDomain,
204
- is_known_safe_domain: isSafeDomain,
205
- },
206
- });
207
- return findings;
208
- }
209
- }
210
-
211
- if (/String\.fromCharCode\(.{20,}\)/.test(code) && hasEval) {
212
- const evidence = createEvidence(code, filePath, /String\.fromCharCode\(/, pkgJson);
213
- findings.push({
214
- id: 'ATK-002',
215
- severity: 'medium',
216
- title: 'Obfuscated payload',
217
- description: 'Eval with String.fromCharCode obfuscation',
218
- evidence: evidence,
219
- context: {
220
- file_path: filePath,
221
- is_dist_build: isDistBuild,
222
- is_test_fixture: isTestFixture,
223
- is_lifecycle_hook: evidence.lifecycle_hook,
224
- url_domain: urlDomain,
225
- is_known_safe_domain: isSafeDomain,
226
- },
227
- });
228
- return findings;
229
- }
230
-
231
- const shellPatterns = [
232
- { regex: /eval\s*\(\s*process\.env\.[A-Z_]{4,}/, name: 'env-eval' },
233
- { regex: /exec\s*\(\s*Buffer\.from\(/, name: 'exec-buffer' },
234
- { regex: /new Function\s*\(\s*(?:atob|process\.env)/, name: 'function-eval' },
235
- { regex: /eval\s*\(\s*(?:require|import\s*\()/, name: 'require-eval' },
236
- { regex: /Function\s*\(\s*'use\s*strict'\s*;?\s*(?:atob|require)/, name: 'strict-eval' },
237
- ];
238
- for (const p of shellPatterns) {
239
- if (p.regex.test(code)) {
240
- const evidence = createEvidence(code, filePath, p.regex, pkgJson);
241
- findings.push({
242
- id: 'ATK-002',
243
- severity: 'high',
244
- title: 'Obfuscated payload',
245
- description: 'Shell-code obfuscation pattern',
246
- evidence: { ...evidence, pattern: p.name },
247
- context: {
248
- file_path: filePath,
249
- is_dist_build: isDistBuild,
250
- is_test_fixture: isTestFixture,
251
- is_lifecycle_hook: evidence.lifecycle_hook,
252
- url_domain: urlDomain,
253
- is_known_safe_domain: isSafeDomain,
254
- },
255
- });
256
- return findings;
257
- }
258
- }
259
- }
260
-
261
- return findings;
1
+ const DIST_BUILD_PATTERNS = [/\/dist\//, /\/build\//, /\/bundle/, /\/min\//, /\.min\.js$/, /\.bundled?\.js$/];
2
+ const TEST_FIXTURE_PATTERNS = [/\/test\//, /\/tests\//, /\/__tests__\//, /\/spec\//, /\.test\.js$/, /\.spec\.js$/, /fixtures?/];
3
+ const KNOWN_SAFE_DOMAINS = [
4
+ 'registry.npmjs.org', 'cdn.jsdelivr.net', 'unpkg.com', 'cdn.skypack.dev',
5
+ 'esm.sh', 'deno.land', 'raw.githubusercontent.com', 'github.com',
6
+ 'npmjs.com', 'nodejs.org', 'v8.dev', 'typescriptlang.org'
7
+ ];
8
+
9
+ const LIFECYCLE_SCRIPT_NAMES = ['install', 'postinstall', 'preinstall', 'prepare', 'prepack', 'postpack'];
10
+
11
+ function extractUrlDomain(code) {
12
+ const urlMatch = code.match(/https?:\/\/([^/'"\s]+)/);
13
+ return urlMatch ? urlMatch[1] : null;
14
+ }
15
+
16
+ function isDistOrBuild(filePath) {
17
+ return DIST_BUILD_PATTERNS.some(p => p.test(filePath));
18
+ }
19
+
20
+ function isTestOrFixture(filePath) {
21
+ return TEST_FIXTURE_PATTERNS.some(p => p.test(filePath));
22
+ }
23
+
24
+ function isKnownSafeDomain(domain) {
25
+ if (!domain) return false;
26
+ return KNOWN_SAFE_DOMAINS.some(safe => domain === safe || domain.endsWith('.' + safe));
27
+ }
28
+
29
+ function locateLine(code, pattern) {
30
+ const lines = code.split('\n');
31
+ for (let i = 0; i < lines.length; i++) {
32
+ if (pattern.test(lines[i])) return i + 1;
33
+ }
34
+ return null;
35
+ }
36
+
37
+ function decodePreview(code) {
38
+ const b64Match = code.match(/atob\(['"]([A-Za-z0-9+/=]{10,})['"]\)/);
39
+ if (b64Match) {
40
+ try {
41
+ const decoded = atob(b64Match[1]);
42
+ return decoded.length > 80 ? decoded.slice(0, 80) + '...' : decoded;
43
+ } catch {}
44
+ }
45
+
46
+ const hexMatch = code.match(/Buffer\.from\(['"]([0-9a-fA-F]+)['"],\s*['"]hex['"]\)/);
47
+ if (hexMatch) {
48
+ try {
49
+ const decoded = Buffer.from(hexMatch[1], 'hex').toString();
50
+ return decoded.length > 80 ? decoded.slice(0, 80) + '...' : decoded;
51
+ } catch {}
52
+ }
53
+
54
+ const btoaMatch = code.match(/btoa\(['"]([A-Za-z0-9+/=]{10,})['"]\)/);
55
+ if (btoaMatch) {
56
+ try {
57
+ const decoded = atob(btoaMatch[1]);
58
+ return decoded.length > 80 ? decoded.slice(0, 80) + '...' : decoded;
59
+ } catch {}
60
+ }
61
+
62
+ return null;
63
+ }
64
+
65
+ function detectEncodingType(code) {
66
+ if (/Buffer\.from\(['"][0-9a-fA-F]+['"],\s*['"]hex['"]\)/.test(code)) return 'hex';
67
+ if (/atob\(/.test(code)) return 'base64';
68
+ if (/btoa\(/.test(code)) return 'base64';
69
+ if (/Buffer\.from\([A-Za-z0-9+/=]{10,}/.test(code)) return 'base64';
70
+ if (/String\.fromCharCode\(/.test(code)) return 'charcode';
71
+ if (/btoa\(.*btoa\(|atob\(.*atob\(/.test(code)) return 'double-base64';
72
+ return 'unknown';
73
+ }
74
+
75
+ function isFileInLifecycleScript(filePath, pkgJson) {
76
+ if (!pkgJson?.scripts) return false;
77
+
78
+ const scripts = pkgJson.scripts;
79
+ const fileName = filePath.split('/').pop();
80
+ const normalizedPath = filePath.replace(/^node_modules\//, '').replace(/^dist\//, '').replace(/^build\//, '');
81
+
82
+ for (const scriptName of LIFECYCLE_SCRIPT_NAMES) {
83
+ const scriptValue = scripts[scriptName];
84
+ if (!scriptValue) continue;
85
+
86
+ if (scriptValue.includes(filePath)) return true;
87
+ if (scriptValue.includes(fileName)) return true;
88
+ if (scriptValue.includes(normalizedPath)) return true;
89
+
90
+ const scriptFileMatch = scriptValue.match(/[^\s'"]+\.js$/);
91
+ if (scriptFileMatch && filePath.endsWith(scriptFileMatch[0])) return true;
92
+ }
93
+
94
+ return false;
95
+ }
96
+
97
+ function isLikelyLifecycleFileName(filePath) {
98
+ const name = filePath.split('/').pop().replace(/\.js$/, '');
99
+ return LIFECYCLE_SCRIPT_NAMES.includes(name) ||
100
+ name === 'setup' ||
101
+ name === 'install-helper';
102
+ }
103
+
104
+ function createEvidence(code, filePath, pattern, pkgJson) {
105
+ const encodingType = detectEncodingType(code);
106
+ const line = locateLine(code, pattern);
107
+ const decodedPreview = decodePreview(code);
108
+ const destinationHost = extractUrlDomain(code);
109
+ const lifecycleHook = isFileInLifecycleScript(filePath, pkgJson) || isLikelyLifecycleFileName(filePath);
110
+
111
+ return {
112
+ file: filePath,
113
+ line: line,
114
+ lifecycle_hook: lifecycleHook,
115
+ decoded_preview: decodedPreview,
116
+ encoding_type: encodingType,
117
+ destination_host: destinationHost,
118
+ };
119
+ }
120
+
121
+ export async function scan(pkgJson, files = []) {
122
+ const findings = [];
123
+ const pkgName = pkgJson?.name || '';
124
+ const selfName = pkgName.replace(/^@/, '').replace(/\//, '-');
125
+
126
+ for (const f of files) {
127
+ const code = f.content;
128
+ const filePath = f.path;
129
+
130
+ const isDistBuild = isDistOrBuild(filePath);
131
+ const isTestFixture = isTestOrFixture(filePath);
132
+ const urlDomain = extractUrlDomain(code);
133
+ const isSafeDomain = isKnownSafeDomain(urlDomain);
134
+
135
+ const hasEval = /eval\(|new Function\(|\bFunction\('/.test(code);
136
+
137
+ if (hasEval) {
138
+ const hexDecode = /Buffer\.from\(['"`][0-9a-f]+['"`],\s*['"]hex['"]/.test(code);
139
+ const b64Decode = /atob\(|Buffer\.from\([A-Za-z0-9+/=]{10,}/.test(code);
140
+ const b64UrlDecode = /try\s*\{[^}]*atob\s*\(/s.test(code) || /btoa\(.*\)\s*[^;]*\.replace\(/s.test(code);
141
+
142
+ if (hexDecode || b64Decode || b64UrlDecode) {
143
+ const evidence = createEvidence(code, filePath, /eval\(|new Function\(|\bFunction\('/, pkgJson);
144
+ findings.push({
145
+ id: 'ATK-002',
146
+ severity: 'medium',
147
+ title: 'Obfuscated payload',
148
+ description: hexDecode ? 'Eval with hex-decoded payload' : 'Eval with base64-decoded payload',
149
+ evidence: evidence,
150
+ context: {
151
+ file_path: filePath,
152
+ is_dist_build: isDistBuild,
153
+ is_test_fixture: isTestFixture,
154
+ is_lifecycle_hook: evidence.lifecycle_hook,
155
+ url_domain: urlDomain,
156
+ is_known_safe_domain: isSafeDomain,
157
+ },
158
+ });
159
+ return findings;
160
+ }
161
+
162
+ if (btoa(btoa('x')) === 'eDuke'.padEnd(5)) {
163
+ const nested = /atob\([^)]*atob\(/s.test(code) || /btoa\([^)]*btoa\(/s.test(code);
164
+ if (nested) {
165
+ const evidence = createEvidence(code, filePath, /btoa\(/, pkgJson);
166
+ findings.push({
167
+ id: 'ATK-002',
168
+ severity: 'high',
169
+ title: 'Obfuscated payload',
170
+ description: 'Double-encoded nested payload',
171
+ evidence: { ...evidence, is_multi_layer: true },
172
+ context: {
173
+ file_path: filePath,
174
+ is_dist_build: isDistBuild,
175
+ is_test_fixture: isTestFixture,
176
+ is_lifecycle_hook: evidence.lifecycle_hook,
177
+ url_domain: urlDomain,
178
+ is_known_safe_domain: isSafeDomain,
179
+ is_multi_layer: true,
180
+ },
181
+ });
182
+ return findings;
183
+ }
184
+ }
185
+ }
186
+
187
+ if (/atob\(|Buffer\.from/.test(code) && /url|fetch|curl|http\.request|https\.request/.test(code)) {
188
+ const isNetworkObfusc = /atob\(.*(https?:\/\/|\\x|http).*\)/s.test(code) ||
189
+ /Buffer\.from\(['"`][0-9a-f]+['"`],\s*['"]hex['"].*fetch\(|fetch\(.*atob\(/s.test(code);
190
+ if (isNetworkObfusc) {
191
+ const evidence = createEvidence(code, filePath, /atob\(|Buffer\.from/, pkgJson);
192
+ findings.push({
193
+ id: 'ATK-002',
194
+ severity: 'medium',
195
+ title: 'Obfuscated payload',
196
+ description: 'Decoded string containing URL/fetch call',
197
+ evidence: evidence,
198
+ context: {
199
+ file_path: filePath,
200
+ is_dist_build: isDistBuild,
201
+ is_test_fixture: isTestFixture,
202
+ is_lifecycle_hook: evidence.lifecycle_hook,
203
+ url_domain: urlDomain,
204
+ is_known_safe_domain: isSafeDomain,
205
+ },
206
+ });
207
+ return findings;
208
+ }
209
+ }
210
+
211
+ if (/String\.fromCharCode\(.{20,}\)/.test(code) && hasEval) {
212
+ const evidence = createEvidence(code, filePath, /String\.fromCharCode\(/, pkgJson);
213
+ findings.push({
214
+ id: 'ATK-002',
215
+ severity: 'medium',
216
+ title: 'Obfuscated payload',
217
+ description: 'Eval with String.fromCharCode obfuscation',
218
+ evidence: evidence,
219
+ context: {
220
+ file_path: filePath,
221
+ is_dist_build: isDistBuild,
222
+ is_test_fixture: isTestFixture,
223
+ is_lifecycle_hook: evidence.lifecycle_hook,
224
+ url_domain: urlDomain,
225
+ is_known_safe_domain: isSafeDomain,
226
+ },
227
+ });
228
+ return findings;
229
+ }
230
+
231
+ const shellPatterns = [
232
+ { regex: /eval\s*\(\s*process\.env\.[A-Z_]{4,}/, name: 'env-eval' },
233
+ { regex: /exec\s*\(\s*Buffer\.from\(/, name: 'exec-buffer' },
234
+ { regex: /new Function\s*\(\s*(?:atob|process\.env)/, name: 'function-eval' },
235
+ { regex: /eval\s*\(\s*(?:require|import\s*\()/, name: 'require-eval' },
236
+ { regex: /Function\s*\(\s*'use\s*strict'\s*;?\s*(?:atob|require)/, name: 'strict-eval' },
237
+ ];
238
+ for (const p of shellPatterns) {
239
+ if (p.regex.test(code)) {
240
+ const evidence = createEvidence(code, filePath, p.regex, pkgJson);
241
+ findings.push({
242
+ id: 'ATK-002',
243
+ severity: 'high',
244
+ title: 'Obfuscated payload',
245
+ description: 'Shell-code obfuscation pattern',
246
+ evidence: { ...evidence, pattern: p.name },
247
+ context: {
248
+ file_path: filePath,
249
+ is_dist_build: isDistBuild,
250
+ is_test_fixture: isTestFixture,
251
+ is_lifecycle_hook: evidence.lifecycle_hook,
252
+ url_domain: urlDomain,
253
+ is_known_safe_domain: isSafeDomain,
254
+ },
255
+ });
256
+ return findings;
257
+ }
258
+ }
259
+ }
260
+
261
+ return findings;
262
262
  }
@@ -1,14 +1,14 @@
1
- export async function scan(pkgJson, files = []) {
2
- const findings = [];
3
- const code = files.map(f => f.content).join('\n');
4
- if (/process\.env\.(NPM_TOKEN|GIT_TOKEN|AWS_SECRET|AWS_ACCESS|SSH_KEY)|\.npmrc|\.ssh\/id_rsa|readFile.*\.ssh/.test(code)) {
5
- findings.push({
6
- id: 'ATK-003',
7
- severity: 'high',
8
- title: 'Credential harvesting',
9
- description: 'Env vars or .npmrc/SSH key access',
10
- evidence: 'credential pattern match'
11
- });
12
- }
13
- return findings;
1
+ export async function scan(pkgJson, files = []) {
2
+ const findings = [];
3
+ const code = files.map(f => f.content).join('\n');
4
+ if (/process\.env\.(NPM_TOKEN|GIT_TOKEN|AWS_SECRET|AWS_ACCESS|SSH_KEY)|\.npmrc|\.ssh\/id_rsa|readFile.*\.ssh/.test(code)) {
5
+ findings.push({
6
+ id: 'ATK-003',
7
+ severity: 'high',
8
+ title: 'Credential harvesting',
9
+ description: 'Env vars or .npmrc/SSH key access',
10
+ evidence: 'credential pattern match'
11
+ });
12
+ }
13
+ return findings;
14
14
  }
@@ -1,14 +1,14 @@
1
- export async function scan(pkgJson, files = []) {
2
- const findings = [];
3
- const code = files.map(f => f.content).join('\n');
4
- if (/mkdir.*(\.vscode|\.claude|\.cursor)/.test(code)) {
5
- findings.push({
6
- id: 'ATK-004',
7
- severity: 'high',
8
- title: 'Persistence via editor configs',
9
- description: 'Creates .vscode/.claude/.cursor dirs',
10
- evidence: 'mkdir pattern match'
11
- });
12
- }
13
- return findings;
1
+ export async function scan(pkgJson, files = []) {
2
+ const findings = [];
3
+ const code = files.map(f => f.content).join('\n');
4
+ if (/mkdir.*(\.vscode|\.claude|\.cursor)/.test(code)) {
5
+ findings.push({
6
+ id: 'ATK-004',
7
+ severity: 'high',
8
+ title: 'Persistence via editor configs',
9
+ description: 'Creates .vscode/.claude/.cursor dirs',
10
+ evidence: 'mkdir pattern match'
11
+ });
12
+ }
13
+ return findings;
14
14
  }
@@ -1,14 +1,14 @@
1
- export async function scan(pkgJson, files = []) {
2
- const findings = [];
3
- const code = files.map(f => f.content).join('\n');
4
- if (/curl.*(-d|--data|--data-binary)|github\.com\/.*keys|pastebin|dns\.resolve.*\.com|exfil/.test(code.toLowerCase())) {
5
- findings.push({
6
- id: 'ATK-005',
7
- severity: 'critical',
8
- title: 'Network exfiltration',
9
- description: 'Suspicious network calls: curl data exfil, pastebin, dns tunneling',
10
- evidence: 'network exfil pattern'
11
- });
12
- }
13
- return findings;
1
+ export async function scan(pkgJson, files = []) {
2
+ const findings = [];
3
+ const code = files.map(f => f.content).join('\n');
4
+ if (/curl.*(-d|--data|--data-binary)|github\.com\/.*keys|pastebin|dns\.resolve.*\.com|exfil/.test(code.toLowerCase())) {
5
+ findings.push({
6
+ id: 'ATK-005',
7
+ severity: 'critical',
8
+ title: 'Network exfiltration',
9
+ description: 'Suspicious network calls: curl data exfil, pastebin, dns tunneling',
10
+ evidence: 'network exfil pattern'
11
+ });
12
+ }
13
+ return findings;
14
14
  }
@@ -1,15 +1,15 @@
1
- export async function scan(pkgJson) {
2
- const findings = [];
3
- const deps = { ...pkgJson.dependencies, ...pkgJson.devDependencies };
4
- const squat = Object.keys(deps).filter(d => /squat|confus|typo/i.test(d.toLowerCase()));
5
- if (squat.length) {
6
- findings.push({
7
- id: 'ATK-006',
8
- severity: 'medium',
9
- title: 'Dependency confusion',
10
- description: 'Suspicious dependency names',
11
- evidence: squat.join(', ')
12
- });
13
- }
14
- return findings;
1
+ export async function scan(pkgJson) {
2
+ const findings = [];
3
+ const deps = { ...pkgJson.dependencies, ...pkgJson.devDependencies };
4
+ const squat = Object.keys(deps).filter(d => /squat|confus|typo/i.test(d.toLowerCase()));
5
+ if (squat.length) {
6
+ findings.push({
7
+ id: 'ATK-006',
8
+ severity: 'medium',
9
+ title: 'Dependency confusion',
10
+ description: 'Suspicious dependency names',
11
+ evidence: squat.join(', ')
12
+ });
13
+ }
14
+ return findings;
15
15
  }