@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,12 +1,43 @@
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?/];
1
+ const DIST_BUILD_PATTERNS = [
2
+ /\/dist\//,
3
+ /\/build\//,
4
+ /\/bundle/,
5
+ /\/min\//,
6
+ /\.min\.js$/,
7
+ /\.bundled?\.js$/,
8
+ ];
9
+ const TEST_FIXTURE_PATTERNS = [
10
+ /\/test\//,
11
+ /\/tests\//,
12
+ /\/__tests__\//,
13
+ /\/spec\//,
14
+ /\.test\.js$/,
15
+ /\.spec\.js$/,
16
+ /fixtures?/,
17
+ ];
3
18
  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'
19
+ 'registry.npmjs.org',
20
+ 'cdn.jsdelivr.net',
21
+ 'unpkg.com',
22
+ 'cdn.skypack.dev',
23
+ 'esm.sh',
24
+ 'deno.land',
25
+ 'raw.githubusercontent.com',
26
+ 'github.com',
27
+ 'npmjs.com',
28
+ 'nodejs.org',
29
+ 'v8.dev',
30
+ 'typescriptlang.org',
7
31
  ];
8
32
 
9
- const LIFECYCLE_SCRIPT_NAMES = ['install', 'postinstall', 'preinstall', 'prepare', 'prepack', 'postpack'];
33
+ const LIFECYCLE_SCRIPT_NAMES = [
34
+ 'install',
35
+ 'postinstall',
36
+ 'preinstall',
37
+ 'prepare',
38
+ 'prepack',
39
+ 'postpack',
40
+ ];
10
41
 
11
42
  function extractUrlDomain(code) {
12
43
  const urlMatch = code.match(/https?:\/\/([^/'"\s]+)/);
@@ -14,22 +45,26 @@ function extractUrlDomain(code) {
14
45
  }
15
46
 
16
47
  function isDistOrBuild(filePath) {
17
- return DIST_BUILD_PATTERNS.some(p => p.test(filePath));
48
+ return DIST_BUILD_PATTERNS.some((p) => p.test(filePath));
18
49
  }
19
50
 
20
51
  function isTestOrFixture(filePath) {
21
- return TEST_FIXTURE_PATTERNS.some(p => p.test(filePath));
52
+ return TEST_FIXTURE_PATTERNS.some((p) => p.test(filePath));
22
53
  }
23
54
 
24
55
  function isKnownSafeDomain(domain) {
25
- if (!domain) return false;
26
- return KNOWN_SAFE_DOMAINS.some(safe => domain === safe || domain.endsWith('.' + safe));
56
+ if (!domain) {
57
+ return false;
58
+ }
59
+ return KNOWN_SAFE_DOMAINS.some((safe) => domain === safe || domain.endsWith('.' + safe));
27
60
  }
28
61
 
29
62
  function locateLine(code, pattern) {
30
63
  const lines = code.split('\n');
31
64
  for (let i = 0; i < lines.length; i++) {
32
- if (pattern.test(lines[i])) return i + 1;
65
+ if (pattern.test(lines[i])) {
66
+ return i + 1;
67
+ }
33
68
  }
34
69
  return null;
35
70
  }
@@ -40,65 +75,96 @@ function decodePreview(code) {
40
75
  try {
41
76
  const decoded = atob(b64Match[1]);
42
77
  return decoded.length > 80 ? decoded.slice(0, 80) + '...' : decoded;
43
- } catch {}
78
+ } catch {
79
+ /* ignore decode errors */
80
+ }
44
81
  }
45
-
82
+
46
83
  const hexMatch = code.match(/Buffer\.from\(['"]([0-9a-fA-F]+)['"],\s*['"]hex['"]\)/);
47
84
  if (hexMatch) {
48
85
  try {
49
86
  const decoded = Buffer.from(hexMatch[1], 'hex').toString();
50
87
  return decoded.length > 80 ? decoded.slice(0, 80) + '...' : decoded;
51
- } catch {}
88
+ } catch {
89
+ /* ignore decode errors */
90
+ }
52
91
  }
53
-
92
+
54
93
  const btoaMatch = code.match(/btoa\(['"]([A-Za-z0-9+/=]{10,})['"]\)/);
55
94
  if (btoaMatch) {
56
95
  try {
57
96
  const decoded = atob(btoaMatch[1]);
58
97
  return decoded.length > 80 ? decoded.slice(0, 80) + '...' : decoded;
59
- } catch {}
98
+ } catch {
99
+ /* ignore decode errors */
100
+ }
60
101
  }
61
-
102
+
62
103
  return null;
63
104
  }
64
105
 
65
106
  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';
107
+ if (/Buffer\.from\(['"][0-9a-fA-F]+['"],\s*['"]hex['"]\)/.test(code)) {
108
+ return 'hex';
109
+ }
110
+ if (/atob\(/.test(code)) {
111
+ return 'base64';
112
+ }
113
+ if (/btoa\(/.test(code)) {
114
+ return 'base64';
115
+ }
116
+ if (/Buffer\.from\([A-Za-z0-9+/=]{10,}/.test(code)) {
117
+ return 'base64';
118
+ }
119
+ if (/String\.fromCharCode\(/.test(code)) {
120
+ return 'charcode';
121
+ }
122
+ if (/btoa\(.*btoa\(|atob\(.*atob\(/.test(code)) {
123
+ return 'double-base64';
124
+ }
72
125
  return 'unknown';
73
126
  }
74
127
 
75
128
  function isFileInLifecycleScript(filePath, pkgJson) {
76
- if (!pkgJson?.scripts) return false;
77
-
129
+ if (!pkgJson?.scripts) {
130
+ return false;
131
+ }
132
+
78
133
  const scripts = pkgJson.scripts;
79
134
  const fileName = filePath.split('/').pop();
80
- const normalizedPath = filePath.replace(/^node_modules\//, '').replace(/^dist\//, '').replace(/^build\//, '');
81
-
135
+ const normalizedPath = filePath
136
+ .replace(/^node_modules\//, '')
137
+ .replace(/^dist\//, '')
138
+ .replace(/^build\//, '');
139
+
82
140
  for (const scriptName of LIFECYCLE_SCRIPT_NAMES) {
83
141
  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
-
142
+ if (!scriptValue) {
143
+ continue;
144
+ }
145
+
146
+ if (scriptValue.includes(filePath)) {
147
+ return true;
148
+ }
149
+ if (scriptValue.includes(fileName)) {
150
+ return true;
151
+ }
152
+ if (scriptValue.includes(normalizedPath)) {
153
+ return true;
154
+ }
155
+
90
156
  const scriptFileMatch = scriptValue.match(/[^\s'"]+\.js$/);
91
- if (scriptFileMatch && filePath.endsWith(scriptFileMatch[0])) return true;
157
+ if (scriptFileMatch && filePath.endsWith(scriptFileMatch[0])) {
158
+ return true;
159
+ }
92
160
  }
93
-
161
+
94
162
  return false;
95
163
  }
96
164
 
97
165
  function isLikelyLifecycleFileName(filePath) {
98
166
  const name = filePath.split('/').pop().replace(/\.js$/, '');
99
- return LIFECYCLE_SCRIPT_NAMES.includes(name) ||
100
- name === 'setup' ||
101
- name === 'install-helper';
167
+ return LIFECYCLE_SCRIPT_NAMES.includes(name) || name === 'setup' || name === 'install-helper';
102
168
  }
103
169
 
104
170
  function createEvidence(code, filePath, pattern, pkgJson) {
@@ -106,8 +172,9 @@ function createEvidence(code, filePath, pattern, pkgJson) {
106
172
  const line = locateLine(code, pattern);
107
173
  const decodedPreview = decodePreview(code);
108
174
  const destinationHost = extractUrlDomain(code);
109
- const lifecycleHook = isFileInLifecycleScript(filePath, pkgJson) || isLikelyLifecycleFileName(filePath);
110
-
175
+ const lifecycleHook =
176
+ isFileInLifecycleScript(filePath, pkgJson) || isLikelyLifecycleFileName(filePath);
177
+
111
178
  return {
112
179
  file: filePath,
113
180
  line: line,
@@ -121,7 +188,7 @@ function createEvidence(code, filePath, pattern, pkgJson) {
121
188
  export async function scan(pkgJson, files = []) {
122
189
  const findings = [];
123
190
  const pkgName = pkgJson?.name || '';
124
- const selfName = pkgName.replace(/^@/, '').replace(/\//, '-');
191
+ const _selfName = pkgName.replace(/^@/, '').replace(/\//, '-');
125
192
 
126
193
  for (const f of files) {
127
194
  const code = f.content;
@@ -137,15 +204,23 @@ export async function scan(pkgJson, files = []) {
137
204
  if (hasEval) {
138
205
  const hexDecode = /Buffer\.from\(['"`][0-9a-f]+['"`],\s*['"]hex['"]/.test(code);
139
206
  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);
207
+ const b64UrlDecode =
208
+ /try\s*\{[^}]*atob\s*\(/s.test(code) || /btoa\(.*\)\s*[^;]*\.replace\(/s.test(code);
141
209
 
142
210
  if (hexDecode || b64Decode || b64UrlDecode) {
143
- const evidence = createEvidence(code, filePath, /eval\(|new Function\(|\bFunction\('/, pkgJson);
211
+ const evidence = createEvidence(
212
+ code,
213
+ filePath,
214
+ /eval\(|new Function\(|\bFunction\('/,
215
+ pkgJson
216
+ );
144
217
  findings.push({
145
218
  id: 'ATK-002',
146
219
  severity: 'medium',
147
220
  title: 'Obfuscated payload',
148
- description: hexDecode ? 'Eval with hex-decoded payload' : 'Eval with base64-decoded payload',
221
+ description: hexDecode
222
+ ? 'Eval with hex-decoded payload'
223
+ : 'Eval with base64-decoded payload',
149
224
  evidence: evidence,
150
225
  context: {
151
226
  file_path: filePath,
@@ -184,8 +259,12 @@ export async function scan(pkgJson, files = []) {
184
259
  }
185
260
  }
186
261
 
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) ||
262
+ if (
263
+ /atob\(|Buffer\.from/.test(code) &&
264
+ /url|fetch|curl|http\.request|https\.request/.test(code)
265
+ ) {
266
+ const isNetworkObfusc =
267
+ /atob\(.*(https?:\/\/|\\x|http).*\)/s.test(code) ||
189
268
  /Buffer\.from\(['"`][0-9a-f]+['"`],\s*['"]hex['"].*fetch\(|fetch\(.*atob\(/s.test(code);
190
269
  if (isNetworkObfusc) {
191
270
  const evidence = createEvidence(code, filePath, /atob\(|Buffer\.from/, pkgJson);
@@ -259,4 +338,4 @@ export async function scan(pkgJson, files = []) {
259
338
  }
260
339
 
261
340
  return findings;
262
- }
341
+ }
@@ -1,14 +1,18 @@
1
1
  export async function scan(pkgJson, files = []) {
2
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)) {
3
+ const code = files.map((f) => f.content).join('\n');
4
+ if (
5
+ /process\.env\.(NPM_TOKEN|GIT_TOKEN|AWS_SECRET|AWS_ACCESS|SSH_KEY)|\.npmrc|\.ssh\/id_rsa|readFile.*\.ssh/.test(
6
+ code
7
+ )
8
+ ) {
5
9
  findings.push({
6
10
  id: 'ATK-003',
7
11
  severity: 'high',
8
12
  title: 'Credential harvesting',
9
13
  description: 'Env vars or .npmrc/SSH key access',
10
- evidence: 'credential pattern match'
14
+ evidence: 'credential pattern match',
11
15
  });
12
16
  }
13
17
  return findings;
14
- }
18
+ }
@@ -1,14 +1,14 @@
1
1
  export async function scan(pkgJson, files = []) {
2
2
  const findings = [];
3
- const code = files.map(f => f.content).join('\n');
3
+ const code = files.map((f) => f.content).join('\n');
4
4
  if (/mkdir.*(\.vscode|\.claude|\.cursor)/.test(code)) {
5
5
  findings.push({
6
6
  id: 'ATK-004',
7
7
  severity: 'high',
8
8
  title: 'Persistence via editor configs',
9
9
  description: 'Creates .vscode/.claude/.cursor dirs',
10
- evidence: 'mkdir pattern match'
10
+ evidence: 'mkdir pattern match',
11
11
  });
12
12
  }
13
13
  return findings;
14
- }
14
+ }
@@ -1,14 +1,18 @@
1
1
  export async function scan(pkgJson, files = []) {
2
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())) {
3
+ const code = files.map((f) => f.content).join('\n');
4
+ if (
5
+ /curl.*(-d|--data|--data-binary)|github\.com\/.*keys|pastebin|dns\.resolve.*\.com|exfil/.test(
6
+ code.toLowerCase()
7
+ )
8
+ ) {
5
9
  findings.push({
6
10
  id: 'ATK-005',
7
11
  severity: 'critical',
8
12
  title: 'Network exfiltration',
9
13
  description: 'Suspicious network calls: curl data exfil, pastebin, dns tunneling',
10
- evidence: 'network exfil pattern'
14
+ evidence: 'network exfil pattern',
11
15
  });
12
16
  }
13
17
  return findings;
14
- }
18
+ }
@@ -1,15 +1,15 @@
1
1
  export async function scan(pkgJson) {
2
2
  const findings = [];
3
3
  const deps = { ...pkgJson.dependencies, ...pkgJson.devDependencies };
4
- const squat = Object.keys(deps).filter(d => /squat|confus|typo/i.test(d.toLowerCase()));
4
+ const squat = Object.keys(deps).filter((d) => /squat|confus|typo/i.test(d.toLowerCase()));
5
5
  if (squat.length) {
6
6
  findings.push({
7
7
  id: 'ATK-006',
8
8
  severity: 'medium',
9
9
  title: 'Dependency confusion',
10
10
  description: 'Suspicious dependency names',
11
- evidence: squat.join(', ')
11
+ evidence: squat.join(', '),
12
12
  });
13
13
  }
14
14
  return findings;
15
- }
15
+ }
@@ -1,12 +1,62 @@
1
- const TOP_PKGS = ['lodash', 'react', 'express', 'axios', 'chalk', 'vue', 'typescript', 'moment', 'uuid', 'commander', 'debug', 'semver', 'underscore', 'request', 'async', 'cheerio', 'bluebird', 'jest', 'mocha', 'dotenv', 'glob', 'minimist', 'body-parser', 'cors', 'helmet', 'jsonwebtoken', 'socket.io', 'redis', 'mongoose', 'sequelize', 'pg', 'passport', 'nodemailer', 'multer', 'bcrypt', 'winston', 'luxon', 'dayjs', 'rxjs', 'redux'];
1
+ const TOP_PKGS = [
2
+ 'lodash',
3
+ 'react',
4
+ 'express',
5
+ 'axios',
6
+ 'chalk',
7
+ 'vue',
8
+ 'typescript',
9
+ 'moment',
10
+ 'uuid',
11
+ 'commander',
12
+ 'debug',
13
+ 'semver',
14
+ 'underscore',
15
+ 'request',
16
+ 'async',
17
+ 'cheerio',
18
+ 'bluebird',
19
+ 'jest',
20
+ 'mocha',
21
+ 'dotenv',
22
+ 'glob',
23
+ 'minimist',
24
+ 'body-parser',
25
+ 'cors',
26
+ 'helmet',
27
+ 'jsonwebtoken',
28
+ 'socket.io',
29
+ 'redis',
30
+ 'mongoose',
31
+ 'sequelize',
32
+ 'pg',
33
+ 'passport',
34
+ 'nodemailer',
35
+ 'multer',
36
+ 'bcrypt',
37
+ 'winston',
38
+ 'luxon',
39
+ 'dayjs',
40
+ 'rxjs',
41
+ 'redux',
42
+ ];
2
43
 
3
44
  function levenshtein(a, b) {
4
- const m = a.length, n = b.length;
45
+ const m = a.length,
46
+ n = b.length;
5
47
  const d = Array.from({ length: m + 1 }, (_, i) => [i]);
6
- for (let j = 0; j <= n; j++) d[0][j] = j;
7
- for (let i = 1; i <= m; i++)
8
- for (let j = 1; j <= n; j++)
9
- d[i][j] = Math.min(d[i-1][j]+1, d[i][j-1]+1, d[i-1][j-1]+(a[i-1]===b[j-1]?0:1));
48
+ for (let j = 0; j <= n; j++) {
49
+ d[0][j] = j;
50
+ }
51
+ for (let i = 1; i <= m; i++) {
52
+ for (let j = 1; j <= n; j++) {
53
+ d[i][j] = Math.min(
54
+ d[i - 1][j] + 1,
55
+ d[i][j - 1] + 1,
56
+ d[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1)
57
+ );
58
+ }
59
+ }
10
60
  return d[m][n];
11
61
  }
12
62
 
@@ -14,9 +64,13 @@ export async function scan(pkgJson) {
14
64
  const findings = [];
15
65
  const deps = { ...pkgJson.dependencies, ...pkgJson.devDependencies };
16
66
  const names = Object.keys(deps);
17
- if (names.length === 0) return findings;
67
+ if (names.length === 0) {
68
+ return findings;
69
+ }
18
70
  for (const d of names) {
19
- if (d.length < 4) continue;
71
+ if (d.length < 4) {
72
+ continue;
73
+ }
20
74
  for (const top of TOP_PKGS) {
21
75
  const dist = levenshtein(d, top);
22
76
  if (dist > 0 && dist <= 2 && d !== top) {
@@ -25,11 +79,11 @@ export async function scan(pkgJson) {
25
79
  severity: 'low',
26
80
  title: 'Typosquatting suspect',
27
81
  description: `"${d}" is edit-distance ${dist} from "${top}"`,
28
- evidence: d
82
+ evidence: d,
29
83
  });
30
84
  break;
31
85
  }
32
86
  }
33
87
  }
34
88
  return findings;
35
- }
89
+ }
@@ -1,7 +1,7 @@
1
1
  export async function scan(pkgJson, files = []) {
2
2
  const findings = [];
3
3
  const repo = pkgJson.repository || {};
4
- const repoUrl = typeof repo === 'string' ? repo : (repo.url || '');
4
+ const repoUrl = typeof repo === 'string' ? repo : repo.url || '';
5
5
  const pkgName = (pkgJson.name || '').toLowerCase();
6
6
 
7
7
  const knownRepos = {
@@ -29,7 +29,7 @@ export async function scan(pkgJson, files = []) {
29
29
  };
30
30
 
31
31
  if (repoUrl && repoUrl.includes('github.com')) {
32
- const repoMatch = repoUrl.match(/github\.com[\/:]([\w.-]+\/[\w.-]+?)(?:\.git)?$/);
32
+ const repoMatch = repoUrl.match(/github\.com[/:]([\w.-]+\/[\w.-]+?)(?:\.git)?$/);
33
33
  if (repoMatch) {
34
34
  const ghRepo = repoMatch[1].toLowerCase();
35
35
  const ghName = ghRepo.split('/')[1];
@@ -45,7 +45,7 @@ export async function scan(pkgJson, files = []) {
45
45
  severity: 'high',
46
46
  title: 'Tarball tampering suspect',
47
47
  description: `Repository "${ghRepo}" does not match expected "${expectedRepo}" for package "${pkgName}"`,
48
- evidence: `repo: ${ghRepo}, expected: ${expectedRepo}`
48
+ evidence: `repo: ${ghRepo}, expected: ${expectedRepo}`,
49
49
  });
50
50
  } else {
51
51
  const orgExpected = knownRepos[shortName];
@@ -57,7 +57,7 @@ export async function scan(pkgJson, files = []) {
57
57
  severity: 'medium',
58
58
  title: 'Tarball tampering suspect',
59
59
  description: `Repository "${ghRepo}" is a different repo under a different org (legitimate: ${expectedRepo})`,
60
- evidence: `org mismatch: ${ghOrg} vs ${expectedOrg}`
60
+ evidence: `org mismatch: ${ghOrg} vs ${expectedOrg}`,
61
61
  });
62
62
  }
63
63
  }
@@ -66,7 +66,7 @@ export async function scan(pkgJson, files = []) {
66
66
  }
67
67
  }
68
68
 
69
- const code = files.map(f => f.content).join('\n');
69
+ const code = files.map((f) => f.content).join('\n');
70
70
  const embeddedIntros = code.match(/\/\/\s*Source:\s*(https?:\/\/[^\s]+)/gi);
71
71
  if (embeddedIntros && repoUrl) {
72
72
  for (const intro of embeddedIntros) {
@@ -78,7 +78,7 @@ export async function scan(pkgJson, files = []) {
78
78
  severity: 'medium',
79
79
  title: 'Tarball tampering suspect',
80
80
  description: 'Source URL in file does not match declared repository',
81
- evidence: srcUrl
81
+ evidence: srcUrl,
82
82
  });
83
83
  }
84
84
  } catch {
@@ -1,10 +1,13 @@
1
1
  export async function scan(pkgJson, files = []) {
2
2
  const findings = [];
3
- const code = files.map(f => f.content).join('\n');
3
+ const code = files.map((f) => f.content).join('\n');
4
4
 
5
5
  const ciPatterns = [
6
6
  { pattern: /process\.env\.CI\b/, label: 'CI env check' },
7
- { pattern: /process\.env\.(TRAVIS|CIRCLECI|GITHUB_ACTIONS|JENKINS|GITLAB_CI|CODEBUILD)/, label: 'CI platform check' },
7
+ {
8
+ pattern: /process\.env\.(TRAVIS|CIRCLECI|GITHUB_ACTIONS|JENKINS|GITLAB_CI|CODEBUILD)/,
9
+ label: 'CI platform check',
10
+ },
8
11
  { pattern: /\bisCI\b/, label: 'isCI utility check' },
9
12
  ];
10
13
 
@@ -15,7 +18,7 @@ export async function scan(pkgJson, files = []) {
15
18
  severity: 'high',
16
19
  title: 'Conditional trigger (CI/production env)',
17
20
  description: `Package checks for CI or production environment: ${label}`,
18
- evidence: 'conditional trigger detected'
21
+ evidence: 'conditional trigger detected',
19
22
  });
20
23
  break;
21
24
  }
@@ -24,7 +27,8 @@ export async function scan(pkgJson, files = []) {
24
27
  const suspiciousCode = /\beval\(|atob\(|btoa\(|new Function\(|child_process\b|\.exec\(|spawn\(/;
25
28
  const suspiciousNetwork = /\.fetch\(|http\.request\(|https\.request\(|dns\.lookup\(/;
26
29
  const suspiciousEnv = /process\.env\.(?!NODE_ENV)[A-Z_]{4,}/;
27
- const hasSuspicious = suspiciousCode.test(code) || suspiciousNetwork.test(code) || suspiciousEnv.test(code);
30
+ const hasSuspicious =
31
+ suspiciousCode.test(code) || suspiciousNetwork.test(code) || suspiciousEnv.test(code);
28
32
 
29
33
  const timePatterns = [
30
34
  {
@@ -52,7 +56,7 @@ export async function scan(pkgJson, files = []) {
52
56
  severity: hasSuspicious ? 'high' : 'medium',
53
57
  title: 'Conditional trigger (time-based)',
54
58
  description: `Package uses ${label}`,
55
- evidence: `${label}${hasSuspicious ? ' — elevated (suspicious context: eval/network/exec detected)' : ''}`
59
+ evidence: `${label}${hasSuspicious ? ' — elevated (suspicious context: eval/network/exec detected)' : ''}`,
56
60
  });
57
61
  break;
58
62
  }
@@ -1,13 +1,22 @@
1
1
  export async function scan(pkgJson, files = []) {
2
2
  const findings = [];
3
- const code = files.map(f => f.content).join('\n');
3
+ const code = files.map((f) => f.content).join('\n');
4
4
 
5
5
  const highPatterns = [
6
6
  { pattern: /\bdebugger\s*;?(\s*\/\/|\s*$|\)|\])/m, label: 'debugger statement' },
7
- { pattern: /process\.argv.*['"]--inspect['"]|process\.argv.*\binspect\b(?!.*argv)/, label: 'inspect/debug flag detection' },
8
- { pattern: /hostname.*(?:docker|sandbox|container|vmware|vbox)/i, label: 'anti-sandbox hostname check' },
7
+ {
8
+ pattern: /process\.argv.*['"]--inspect['"]|process\.argv.*\binspect\b(?!.*argv)/,
9
+ label: 'inspect/debug flag detection',
10
+ },
11
+ {
12
+ pattern: /hostname.*(?:docker|sandbox|container|vmware|vbox)/i,
13
+ label: 'anti-sandbox hostname check',
14
+ },
9
15
  { pattern: /detect.*(?:sandbox|debugger|analysis|virtual)/i, label: 'explicit evasion probe' },
10
- { pattern: /e\.stack\b.*(?:sandbox|docker|container|vmware)/i, label: 'stack trace sandbox probe' },
16
+ {
17
+ pattern: /e\.stack\b.*(?:sandbox|docker|container|vmware)/i,
18
+ label: 'stack trace sandbox probe',
19
+ },
11
20
  ];
12
21
 
13
22
  for (const { pattern, label } of highPatterns) {
@@ -17,35 +26,41 @@ export async function scan(pkgJson, files = []) {
17
26
  severity: 'high',
18
27
  title: 'Sandbox evasion / anti-analysis',
19
28
  description: `Package performs anti-analysis behavior: ${label}`,
20
- evidence: 'evasion pattern detected'
29
+ evidence: 'evasion pattern detected',
21
30
  });
22
31
  break;
23
32
  }
24
33
  }
25
34
 
26
35
  if (findings.length === 0) {
27
- const multiApi = ['process.pid', 'process.ppid', 'os.hostname', 'os.cpus', 'process.arch'].filter(api => code.includes(api));
36
+ const multiApi = [
37
+ 'process.pid',
38
+ 'process.ppid',
39
+ 'os.hostname',
40
+ 'os.cpus',
41
+ 'process.arch',
42
+ ].filter((api) => code.includes(api));
28
43
  if (multiApi.length >= 3) {
29
44
  findings.push({
30
45
  id: 'ATK-010',
31
46
  severity: 'medium',
32
47
  title: 'Sandbox evasion / anti-analysis',
33
48
  description: 'Multiple system fingerprinting APIs detected',
34
- evidence: `${multiApi.length} fingerprinting APIs: ${multiApi.join(', ')}`
49
+ evidence: `${multiApi.length} fingerprinting APIs: ${multiApi.join(', ')}`,
35
50
  });
36
51
  }
37
52
  }
38
53
 
39
- const multiStack = ['Error().stack', 'new Error().stack'].filter(s => code.includes(s));
54
+ const multiStack = ['Error().stack', 'new Error().stack'].filter((s) => code.includes(s));
40
55
  if (multiStack.length > 0 && /atob|eval|execSync|spawn|child_process/.test(code)) {
41
56
  findings.push({
42
57
  id: 'ATK-010',
43
58
  severity: 'medium',
44
59
  title: 'Sandbox evasion / anti-analysis',
45
60
  description: 'Stack trace capture combined with code execution',
46
- evidence: 'stack trace + execution'
61
+ evidence: 'stack trace + execution',
47
62
  });
48
63
  }
49
64
 
50
65
  return findings;
51
- }
66
+ }