@lateos/npm-scan 0.18.1 → 0.18.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/.dockerignore +20 -20
  2. package/.husky/pre-commit +1 -1
  3. package/CHANGELOG.md +233 -199
  4. package/LICENSING.md +19 -19
  5. package/README.de.md +708 -708
  6. package/README.fr.md +707 -707
  7. package/README.ja.md +704 -704
  8. package/README.md +826 -826
  9. package/README.zh.md +708 -708
  10. package/SECURITY.md +72 -72
  11. package/backend/cra.js +68 -68
  12. package/backend/db/schema.sql +32 -32
  13. package/backend/db.js +88 -88
  14. package/backend/detectors/atk-001-lifecycle.js +17 -17
  15. package/backend/detectors/atk-002-obfusc.js +261 -261
  16. package/backend/detectors/atk-003-creds.js +13 -13
  17. package/backend/detectors/atk-004-persist.js +13 -13
  18. package/backend/detectors/atk-005-exfil.js +13 -13
  19. package/backend/detectors/atk-006-depconf.js +14 -14
  20. package/backend/detectors/atk-007-typosquat.js +34 -34
  21. package/backend/detectors/atk-008-tarball-tamper.js +91 -91
  22. package/backend/detectors/atk-009-dormant-trigger.js +62 -62
  23. package/backend/detectors/atk-010-sandbox-evasion.js +50 -50
  24. package/backend/detectors/atk-011-transitive-prop.js +76 -76
  25. package/backend/detectors/cve-2026-48710-badhost/codePattern.js +99 -99
  26. package/backend/detectors/cve-2026-48710-badhost/findings.js +105 -105
  27. package/backend/detectors/cve-2026-48710-badhost/index.js +15 -15
  28. package/backend/detectors/cve-2026-48710-badhost/manifest.js +305 -305
  29. package/backend/detectors/cve-2026-48710-badhost/transitive.js +189 -189
  30. package/backend/detectors/hf-impersonation/index.js +396 -396
  31. package/backend/detectors/hf-impersonation/jaro-winkler.js +44 -44
  32. package/backend/detectors/hf-impersonation/known-orgs.js +5 -5
  33. package/backend/detectors/hf-impersonation/simhash.js +46 -46
  34. package/backend/detectors/index.js +81 -75
  35. package/backend/detectors/megalodon/d1-workflow-scan.js +147 -147
  36. package/backend/detectors/megalodon/d2-credential-harvest.js +61 -61
  37. package/backend/detectors/megalodon/d3-publish-velocity.js +67 -67
  38. package/backend/detectors/megalodon/d4-publisher-drift.js +124 -124
  39. package/backend/detectors/megalodon/d5-bot-commit-identity.js +3 -3
  40. package/backend/detectors/megalodon/d6-date-anachronism.js +3 -3
  41. package/backend/detectors/megalodon/index.js +80 -80
  42. package/backend/detectors/megalodon/types.js +9 -9
  43. package/backend/detectors/mini-shai-hulud/d1-burst-publish.js +42 -42
  44. package/backend/detectors/mini-shai-hulud/d2-sibling-compromise.js +116 -116
  45. package/backend/detectors/mini-shai-hulud/d3-slsa-mismatch.js +72 -72
  46. package/backend/detectors/mini-shai-hulud/d4-maintainer-anomaly.js +45 -45
  47. package/backend/detectors/mini-shai-hulud/d5-ioc-check.js +95 -95
  48. package/backend/detectors/mini-shai-hulud/d6-token-exfil.js +38 -38
  49. package/backend/detectors/mini-shai-hulud/index.js +118 -118
  50. package/backend/detectors/mini-shai-hulud/iocs.json +79 -79
  51. package/backend/detectors/tier1-cloud-imds.js +124 -0
  52. package/backend/detectors/tier1-infostealer.js +36 -0
  53. package/backend/detectors/tier1-multistage-postinstall.js +81 -0
  54. package/backend/detectors/tier1-version-confusion.js +107 -0
  55. package/backend/fetch.js +175 -175
  56. package/backend/index.js +4 -4
  57. package/backend/license.js +89 -89
  58. package/backend/lockfile.js +379 -379
  59. package/backend/pdf.js +245 -245
  60. package/backend/policy.js +193 -193
  61. package/backend/report.js +254 -254
  62. package/backend/sbom.js +66 -66
  63. package/backend/siem/cef.js +32 -32
  64. package/backend/siem/ecs.js +40 -40
  65. package/backend/siem/index.js +18 -18
  66. package/backend/siem/qradar.js +56 -56
  67. package/backend/siem/sentinel.js +27 -27
  68. package/backend/vsix-scan/detectors/activation-event-risk.js +116 -116
  69. package/backend/vsix-scan/detectors/burst-publish.js +52 -52
  70. package/backend/vsix-scan/detectors/exfil-pattern.js +88 -88
  71. package/backend/vsix-scan/detectors/known-ioc.js +105 -105
  72. package/backend/vsix-scan/detectors/orphan-commit-fetch.js +69 -69
  73. package/backend/vsix-scan/detectors/publisher-anomaly.js +70 -70
  74. package/backend/vsix-scan/index.js +183 -183
  75. package/backend/vsix-scan/marketplace-client.js +145 -145
  76. package/backend/vsix-scan/vsix-iocs.json +31 -31
  77. package/cli/cli.js +458 -458
  78. package/deploy/helm/npm-scan/Chart.yaml +21 -21
  79. package/deploy/helm/npm-scan/templates/_helpers.tpl +8 -8
  80. package/deploy/helm/npm-scan/templates/api.yaml +93 -93
  81. package/deploy/helm/npm-scan/templates/ingress.yaml +27 -27
  82. package/deploy/helm/npm-scan/templates/postgresql.yaml +66 -66
  83. package/deploy/helm/npm-scan/templates/secrets.yaml +18 -18
  84. package/deploy/helm/npm-scan/templates/worker.yaml +31 -31
  85. package/deploy/helm/npm-scan/values.byoc.yaml +74 -74
  86. package/deploy/helm/npm-scan/values.yaml +102 -102
  87. package/package.json +57 -57
  88. package/scripts/download-corpus.js +30 -30
  89. package/scripts/gen-mal-corpus.js +34 -34
  90. package/test/fixtures/lockfiles/npm-lock.json +68 -68
  91. package/test/fixtures/lockfiles/pnpm-lock.yaml +117 -117
  92. package/test/fixtures/lockfiles/yarn.lock +103 -103
  93. package/test/fixtures/mock-data.js +69 -69
@@ -1,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
  }