@lateos/npm-scan 1.0.0 → 1.1.1

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 (129) hide show
  1. package/README.de.md +3 -98
  2. package/README.fr.md +3 -98
  3. package/README.ja.md +3 -98
  4. package/README.md +2 -122
  5. package/README.zh.md +3 -98
  6. package/backend/cra.js +113 -21
  7. package/backend/db.js +18 -10
  8. package/backend/detectors/atk-001-lifecycle.js +5 -5
  9. package/backend/detectors/atk-002-obfusc.js +126 -47
  10. package/backend/detectors/atk-003-creds.js +8 -4
  11. package/backend/detectors/atk-004-persist.js +3 -3
  12. package/backend/detectors/atk-005-exfil.js +8 -4
  13. package/backend/detectors/atk-006-depconf.js +3 -3
  14. package/backend/detectors/atk-007-typosquat.js +64 -10
  15. package/backend/detectors/atk-008-tarball-tamper.js +6 -6
  16. package/backend/detectors/atk-009-dormant-trigger.js +9 -5
  17. package/backend/detectors/atk-010-sandbox-evasion.js +25 -10
  18. package/backend/detectors/atk-011-transitive-prop.js +14 -13
  19. package/backend/detectors/axios-poisoning/d1-version-fingerprint.js +4 -4
  20. package/backend/detectors/axios-poisoning/d2-decoy-dep.js +5 -1
  21. package/backend/detectors/axios-poisoning/d3-postinstall-rat.js +64 -19
  22. package/backend/detectors/axios-poisoning/index.js +77 -60
  23. package/backend/detectors/config/thresholds.js +48 -3
  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 +181 -34
  34. package/backend/detectors/lib/ast-patterns.js +4 -1
  35. package/backend/detectors/lib/entropy-analyzer.js +12 -4
  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 +109 -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 +45 -17
  73. package/backend/detectors/tier1-self-propagation.js +115 -0
  74. package/backend/detectors/tier1-slsa-attestation.js +1 -1
  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 +77 -41
  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 +78 -19
  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 +14 -8
  104. package/backend/scripts/analyze-validation.js +27 -21
  105. package/backend/scripts/detect-false-positives.js +20 -10
  106. package/backend/scripts/fetch-top-packages.js +197 -49
  107. package/backend/scripts/validate-d10-d13.js +103 -0
  108. package/backend/scripts/validate-detectors.js +26 -17
  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 +13 -12
  115. package/backend/tests-d6-version-anomaly.test.js +17 -8
  116. package/backend/tests-d6.test.js +24 -14
  117. package/backend/tests-d6c.test.js +27 -14
  118. package/backend/tests-d7-obfuscation.test.js +9 -12
  119. package/backend/tests.test.js +182 -83
  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 +12 -3
@@ -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
+ }
@@ -1,27 +1,28 @@
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
  {
7
7
  pattern: /(?:exec|execSync|spawn)\s*\([^)]*npm\s+(?:install|link)\s*\./i,
8
- label: 'programmatic self-propagation via npm install/link'
8
+ label: 'programmatic self-propagation via npm install/link',
9
9
  },
10
10
  {
11
- pattern: /fs\.(?:writeFile|writeFileSync|copyFile|copyFileSync)\s*\([^)]*(?:node_modules\/(?!\.)[^/]+).*(?:index\.js|main\.js|package\.json)/i,
12
- label: 'direct file write to peer node_modules'
11
+ pattern:
12
+ /fs\.(?:writeFile|writeFileSync|copyFile|copyFileSync)\s*\([^)]*(?:node_modules\/(?!\.)[^/]+).*(?:index\.js|main\.js|package\.json)/i,
13
+ label: 'direct file write to peer node_modules',
13
14
  },
14
15
  {
15
16
  pattern: /fs\.(?:writeFile|writeFileSync)\s*\([^)]*package\.json[^)]*["']scripts["']/i,
16
- label: 'package.json script injection in another package'
17
+ label: 'package.json script injection in another package',
17
18
  },
18
19
  {
19
20
  pattern: /fs\.(?:writeFile|writeFileSync)\s*\([^)]*\.\.\/[^)]*package\.json/i,
20
- label: 'writes modified package.json to sibling package'
21
+ label: 'writes modified package.json to sibling package',
21
22
  },
22
23
  {
23
24
  pattern: /(?:exec|execSync|spawn)\s*\([^)]*(?:\.\.\/|process\.env\.INIT_CWD).*npm\s+install/i,
24
- label: 'cross-directory npm install propagation'
25
+ label: 'cross-directory npm install propagation',
25
26
  },
26
27
  ];
27
28
 
@@ -32,7 +33,7 @@ export async function scan(pkgJson, files = []) {
32
33
  severity: 'high',
33
34
  title: 'Transitive propagation (worm)',
34
35
  description: `Package attempts lateral worm-style spread: ${label}`,
35
- evidence: 'transitive propagation pattern detected'
36
+ evidence: 'transitive propagation pattern detected',
36
37
  });
37
38
  break;
38
39
  }
@@ -42,19 +43,19 @@ export async function scan(pkgJson, files = []) {
42
43
  const mediumPatterns = [
43
44
  {
44
45
  pattern: /process\.env\.npm_package_name/,
45
- label: 'reads own package name from env (self-awareness indicator)'
46
+ label: 'reads own package name from env (self-awareness indicator)',
46
47
  },
47
48
  {
48
49
  pattern: /fs\.symlink(?:Sync)?\s*\([^)]*node_modules/,
49
- label: 'creates symlinks in node_modules (worm spreading mechanism)'
50
+ label: 'creates symlinks in node_modules (worm spreading mechanism)',
50
51
  },
51
52
  {
52
53
  pattern: /fs\.(?:mkdir|mkdirSync)\s*\([^)]*\.\.\/[^)]*node_modules/,
53
- label: 'creates directories in parent node_modules'
54
+ label: 'creates directories in parent node_modules',
54
55
  },
55
56
  {
56
57
  pattern: /__dirname.*\.\.\/[^/]+\/node_modules.*require\(/,
57
- label: 'dynamic parent-node_modules require for lateral spread'
58
+ label: 'dynamic parent-node_modules require for lateral spread',
58
59
  },
59
60
  ];
60
61
 
@@ -65,7 +66,7 @@ export async function scan(pkgJson, files = []) {
65
66
  severity: 'medium',
66
67
  title: 'Transitive propagation (worm)',
67
68
  description: label,
68
- evidence: 'potential propagation indicator'
69
+ evidence: 'potential propagation indicator',
69
70
  });
70
71
  break;
71
72
  }
@@ -1,13 +1,13 @@
1
- const BLOCKED_VERSIONS = new Map([
2
- ['axios', ['1.14.1', '0.30.4']],
3
- ]);
1
+ const BLOCKED_VERSIONS = new Map([['axios', ['1.14.1', '0.30.4']]]);
4
2
 
5
3
  export function scanVersionBlocklist(pkgJson) {
6
4
  const pkgName = pkgJson?.name || '';
7
5
  const pkgVersion = pkgJson?.version || '';
8
6
 
9
7
  const blocked = BLOCKED_VERSIONS.get(pkgName);
10
- if (!blocked) return { triggered: false, stopCondition: false, matchedVersion: null };
8
+ if (!blocked) {
9
+ return { triggered: false, stopCondition: false, matchedVersion: null };
10
+ }
11
11
 
12
12
  if (blocked.includes(pkgVersion)) {
13
13
  return {
@@ -1,7 +1,11 @@
1
1
  const KNOWN_DECOYS = ['plain-crypto-js'];
2
2
 
3
3
  export function scanDecoyDependency(pkgJson) {
4
- const deps = { ...pkgJson?.dependencies, ...pkgJson?.devDependencies, ...pkgJson?.peerDependencies };
4
+ const deps = {
5
+ ...pkgJson?.dependencies,
6
+ ...pkgJson?.devDependencies,
7
+ ...pkgJson?.peerDependencies,
8
+ };
5
9
  const findings = [];
6
10
 
7
11
  for (const depName of KNOWN_DECOYS) {
@@ -7,7 +7,8 @@ const CRON_PERSIST_RE = /crontab\s+-[ei]|@reboot\s+|@daily\s+|@hourly\s+/;
7
7
  const DLL_LOAD_RE = /LoadLibrary|dlopen|LoadLibraryEx|lib\.(?:LoadLibrary|dlopen)/;
8
8
  const PROCESS_INJECT_RE = /CreateRemoteThread|VirtualAllocEx|WriteProcessMemory|NtCreateThreadEx/;
9
9
  const NET_CALLBACK_RE = /(?:https?:\/\/|wss?:\/\/|ws:\/\/)(?:[^\s'"]*\.[^\s'"]{2,})/;
10
- const BINARY_DROP_RE = /(?:fs\.writeFileSync|writeFile|writeFileSync)\s*\([^)]*(?:\.exe|\.dll|\.bin|\.bat|\.ps1)/;
10
+ const BINARY_DROP_RE =
11
+ /(?:fs\.writeFileSync|writeFile|writeFileSync)\s*\([^)]*(?:\.exe|\.dll|\.bin|\.bat|\.ps1)/;
11
12
 
12
13
  const SUSPICIOUS_HOOK_PATTERNS = [
13
14
  /curl|wget|fetch|https?:\/\//,
@@ -22,7 +23,7 @@ const SUSPICIOUS_HOOK_PATTERNS = [
22
23
 
23
24
  export function scanPostinstallRAT(pkgJson, files = []) {
24
25
  const scripts = pkgJson?.scripts || {};
25
- const code = files.map(f => f.content || '').join('\n');
26
+ const code = files.map((f) => f.content || '').join('\n');
26
27
 
27
28
  const activeHooks = [];
28
29
  for (const hook of SUSPICIOUS_HOOKS) {
@@ -32,39 +33,76 @@ export function scanPostinstallRAT(pkgJson, files = []) {
32
33
  }
33
34
 
34
35
  if (activeHooks.length === 0) {
35
- return { triggered: false, platforms: [], c2Indicators: [], payloadType: null, hooks: [], hasBinaryDrop: false };
36
+ return {
37
+ triggered: false,
38
+ platforms: [],
39
+ c2Indicators: [],
40
+ payloadType: null,
41
+ hooks: [],
42
+ hasBinaryDrop: false,
43
+ };
36
44
  }
37
45
 
38
- const combined = code + '\n' + activeHooks.map(h => h.command).join('\n');
46
+ const combined = code + '\n' + activeHooks.map((h) => h.command).join('\n');
39
47
 
40
- const hasSuspiciousCode = SUSPICIOUS_HOOK_PATTERNS.some(p => p.test(combined));
48
+ const hasSuspiciousCode = SUSPICIOUS_HOOK_PATTERNS.some((p) => p.test(combined));
41
49
 
42
50
  if (activeHooks.length > 0 && !hasSuspiciousCode) {
43
- return { triggered: false, platforms: [], c2Indicators: [], payloadType: null, hooks: [], hasBinaryDrop: false };
51
+ return {
52
+ triggered: false,
53
+ platforms: [],
54
+ c2Indicators: [],
55
+ payloadType: null,
56
+ hooks: [],
57
+ hasBinaryDrop: false,
58
+ };
44
59
  }
45
60
 
46
61
  const platforms = [];
47
62
  let c2Indicators = [];
48
63
  let hasBinaryDrop = false;
49
64
 
50
- if (POWERSHELL_RE.test(combined)) platforms.push('windows');
51
- if (LAUNCHD_RE.test(combined)) platforms.push('macos');
52
- if (SYSTEMD_SERVICE_RE.test(combined) || CRON_PERSIST_RE.test(combined)) platforms.push('linux');
53
- if (TEMP_DIR_RE.test(combined) && (POWERSHELL_RE.test(combined) || BINARY_DROP_RE.test(combined))) {
54
- if (!platforms.includes('windows')) platforms.push('windows');
55
- if (!platforms.includes('linux')) platforms.push('linux');
56
- if (!platforms.includes('macos')) platforms.push('macos');
65
+ if (POWERSHELL_RE.test(combined)) {
66
+ platforms.push('windows');
67
+ }
68
+ if (LAUNCHD_RE.test(combined)) {
69
+ platforms.push('macos');
70
+ }
71
+ if (SYSTEMD_SERVICE_RE.test(combined) || CRON_PERSIST_RE.test(combined)) {
72
+ platforms.push('linux');
73
+ }
74
+ if (
75
+ TEMP_DIR_RE.test(combined) &&
76
+ (POWERSHELL_RE.test(combined) || BINARY_DROP_RE.test(combined))
77
+ ) {
78
+ if (!platforms.includes('windows')) {
79
+ platforms.push('windows');
80
+ }
81
+ if (!platforms.includes('linux')) {
82
+ platforms.push('linux');
83
+ }
84
+ if (!platforms.includes('macos')) {
85
+ platforms.push('macos');
86
+ }
57
87
  }
58
88
 
59
- if (DLL_LOAD_RE.test(combined)) platforms.push('windows');
60
- if (PROCESS_INJECT_RE.test(combined)) platforms.push('windows');
89
+ if (DLL_LOAD_RE.test(combined)) {
90
+ platforms.push('windows');
91
+ }
92
+ if (PROCESS_INJECT_RE.test(combined)) {
93
+ platforms.push('windows');
94
+ }
61
95
 
62
96
  if (NET_CALLBACK_RE.test(combined)) {
63
97
  const urls = combined.match(NET_CALLBACK_RE);
64
- c2Indicators = urls ? [...new Set(urls.map(u => u.replace(/['")]/g, '')))] : ['Network callback to external server'];
98
+ c2Indicators = urls
99
+ ? [...new Set(urls.map((u) => u.replace(/['")]/g, '')))]
100
+ : ['Network callback to external server'];
65
101
  }
66
102
 
67
- if (BINARY_DROP_RE.test(combined)) hasBinaryDrop = true;
103
+ if (BINARY_DROP_RE.test(combined)) {
104
+ hasBinaryDrop = true;
105
+ }
68
106
 
69
107
  let payloadType = null;
70
108
  if (platforms.length >= 2 && c2Indicators.length > 0 && hasBinaryDrop) {
@@ -81,10 +119,17 @@ export function scanPostinstallRAT(pkgJson, files = []) {
81
119
  payloadType,
82
120
  platforms: [...new Set(platforms)],
83
121
  c2Indicators,
84
- hooks: activeHooks.map(h => h.hook),
122
+ hooks: activeHooks.map((h) => h.hook),
85
123
  hasBinaryDrop,
86
124
  };
87
125
  }
88
126
 
89
- return { triggered: false, platforms: [], c2Indicators: [], payloadType: null, hooks: [], hasBinaryDrop: false };
127
+ return {
128
+ triggered: false,
129
+ platforms: [],
130
+ c2Indicators: [],
131
+ payloadType: null,
132
+ hooks: [],
133
+ hasBinaryDrop: false,
134
+ };
90
135
  }