@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
@@ -10,7 +10,9 @@ export function scanCampaignMarker(allFiles) {
10
10
  const ext = path.includes('.') ? '.' + path.split('.').pop() : '';
11
11
 
12
12
  const isTarget = TARGET_FILENAMES.has(basename) || TARGET_EXTENSIONS.includes(ext);
13
- if (!isTarget) continue;
13
+ if (!isTarget) {
14
+ continue;
15
+ }
14
16
 
15
17
  if (content.includes('P-2024-001')) {
16
18
  matches.push({ file: path });
@@ -12,7 +12,7 @@ export function scanPayloadFingerprint(allFiles) {
12
12
  }
13
13
 
14
14
  if (byteSize === 48485) {
15
- const alreadyMatched = matches.some(m => m.file === path);
15
+ const alreadyMatched = matches.some((m) => m.file === path);
16
16
  if (!alreadyMatched) {
17
17
  matches.push({ file: path, matchType: 'byteSize', byteSize });
18
18
  }
@@ -1,7 +1,8 @@
1
1
  export function scanPublisherBlocklist(pkgJson, registryMeta) {
2
- const publisherAccount = registryMeta?.versions?.[pkgJson?.version]?._npmUser?.name
3
- || registryMeta?.versions?.[Object.keys(registryMeta.versions || {})[0]]?._npmUser?.name
4
- || null;
2
+ const publisherAccount =
3
+ registryMeta?.versions?.[pkgJson?.version]?._npmUser?.name ||
4
+ registryMeta?.versions?.[Object.keys(registryMeta.versions || {})[0]]?._npmUser?.name ||
5
+ null;
5
6
 
6
7
  if (publisherAccount === 'asdxzxc') {
7
8
  return { triggered: true, publisher: publisherAccount };
@@ -3,8 +3,10 @@ const C2_PATTERNS = [/ddjidd564\.github\.io/i, /gist\.github\.com/i];
3
3
 
4
4
  function scanContent(content, filePath) {
5
5
  const matches = [];
6
- const hasC2 = C2_PATTERNS.some(p => p.test(content));
7
- if (!hasC2) return matches;
6
+ const hasC2 = C2_PATTERNS.some((p) => p.test(content));
7
+ if (!hasC2) {
8
+ return matches;
9
+ }
8
10
 
9
11
  const hasCredPath = CRED_PATH_PATTERNS.test(content);
10
12
  if (hasCredPath) {
@@ -1,6 +1,6 @@
1
1
  const ZERO_WIDTH_RANGES = [
2
- [0x200B, 0x200D],
3
- [0xFEFF, 0xFEFF],
2
+ [0x200b, 0x200d],
3
+ [0xfeff, 0xfeff],
4
4
  ];
5
5
 
6
6
  function isZeroWidthChar(code) {
@@ -14,7 +14,9 @@ export function scanAIPoisoning(allFiles) {
14
14
 
15
15
  for (const file of allFiles) {
16
16
  const path = file.path?.replace(/\\/g, '/') || '';
17
- if (!TARGET_FILES.test(path)) continue;
17
+ if (!TARGET_FILES.test(path)) {
18
+ continue;
19
+ }
18
20
 
19
21
  const content = file.content || '';
20
22
  const found = [];
@@ -12,16 +12,21 @@ const LURE_PATTERNS = [
12
12
 
13
13
  export function scanLureName(pkgJson, registryMeta) {
14
14
  const pkgName = pkgJson?.name || '';
15
- const matchedPattern = LURE_PATTERNS.find(p => p.test(pkgName));
16
- if (!matchedPattern) return { triggered: false };
15
+ const matchedPattern = LURE_PATTERNS.find((p) => p.test(pkgName));
16
+ if (!matchedPattern) {
17
+ return { triggered: false };
18
+ }
17
19
 
18
20
  const timeMap = registryMeta?.time || {};
19
- const versions = Object.keys(timeMap).filter(v => v !== 'created' && v !== 'modified');
20
- const firstVersion = versions.length > 0
21
- ? versions.sort((a, b) => new Date(timeMap[a]) - new Date(timeMap[b]))[0]
22
- : null;
21
+ const versions = Object.keys(timeMap).filter((v) => v !== 'created' && v !== 'modified');
22
+ const firstVersion =
23
+ versions.length > 0
24
+ ? versions.sort((a, b) => new Date(timeMap[a]) - new Date(timeMap[b]))[0]
25
+ : null;
23
26
 
24
- if (!firstVersion) return { triggered: false };
27
+ if (!firstVersion) {
28
+ return { triggered: false };
29
+ }
25
30
 
26
31
  const firstPubDate = new Date(timeMap[firstVersion]);
27
32
  const now = new Date();
@@ -7,8 +7,8 @@ export function scanCryptoPrimitives(allFiles, pkgJson) {
7
7
  .map(([hook, content]) => ({ file: `script:${hook}`, content }));
8
8
 
9
9
  const jsFiles = allFiles
10
- .filter(f => f.path?.endsWith('.js') || f.path?.endsWith('.mjs') || f.path?.endsWith('.cjs'))
11
- .map(f => ({ file: f.path, content: f.content || '' }));
10
+ .filter((f) => f.path?.endsWith('.js') || f.path?.endsWith('.mjs') || f.path?.endsWith('.cjs'))
11
+ .map((f) => ({ file: f.path, content: f.content || '' }));
12
12
 
13
13
  for (const { file, content } of [...scriptEntries, ...jsFiles]) {
14
14
  const hasFernet = /Fernet/i.test(content);
@@ -2,9 +2,14 @@ export function scanXorKey(allFiles) {
2
2
  const matches = [];
3
3
  for (const file of allFiles) {
4
4
  const path = file.path?.replace(/\\/g, '/') || '';
5
- const isLockFile = /(package-lock\.json|yarn\.lock|pnpm-lock\.yaml|pnpm-lock\.yml|Cargo\.lock|Cargo\.toml)/i.test(path);
5
+ const isLockFile =
6
+ /(package-lock\.json|yarn\.lock|pnpm-lock\.yaml|pnpm-lock\.yml|Cargo\.lock|Cargo\.toml)/i.test(
7
+ path
8
+ );
6
9
  const isBundled = /\.node$|vendor|native/i.test(path);
7
- if (!isLockFile && !isBundled) continue;
10
+ if (!isLockFile && !isBundled) {
11
+ continue;
12
+ }
8
13
 
9
14
  const content = file.content || '';
10
15
  if (content.includes('cargo-build-helper-2026')) {
@@ -1,7 +1,4 @@
1
- const CRED_VALIDATION_PATTERNS = [
2
- /sts\.amazonaws\.com/i,
3
- /api\.github\.com\/user/i,
4
- ];
1
+ const CRED_VALIDATION_PATTERNS = [/sts\.amazonaws\.com/i, /api\.github\.com\/user/i];
5
2
 
6
3
  export function scanCredValidation(allFiles, pkgJson) {
7
4
  const matches = [];
@@ -19,7 +16,9 @@ export function scanCredValidation(allFiles, pkgJson) {
19
16
 
20
17
  for (const file of allFiles) {
21
18
  const path = file.path || '';
22
- if (!path.endsWith('.js') && !path.endsWith('.mjs') && !path.endsWith('.cjs')) continue;
19
+ if (!path.endsWith('.js') && !path.endsWith('.mjs') && !path.endsWith('.cjs')) {
20
+ continue;
21
+ }
23
22
  const content = file.content || '';
24
23
  for (const pattern of CRED_VALIDATION_PATTERNS) {
25
24
  if (pattern.test(content)) {
@@ -24,7 +24,9 @@ const SEVERITY_ORDER = ['critical', 'high', 'medium', 'low', 'info', 'none'];
24
24
 
25
25
  function highestSeverity(severities) {
26
26
  for (const s of SEVERITY_ORDER) {
27
- if (severities.includes(s)) return s;
27
+ if (severities.includes(s)) {
28
+ return s;
29
+ }
28
30
  }
29
31
  return 'none';
30
32
  }
@@ -48,16 +50,16 @@ export async function scan(pkgJson, files = [], registryMeta = null, allFiles =
48
50
  .filter(([_, r]) => r.triggered)
49
51
  .map(([id]) => id);
50
52
 
51
- if (triggered.length === 0) return [];
53
+ if (triggered.length === 0) {
54
+ return [];
55
+ }
52
56
 
53
- const severity = highestSeverity(triggered.map(id => RULE_SEVERITY[id]));
57
+ const severity = highestSeverity(triggered.map((id) => RULE_SEVERITY[id]));
54
58
 
55
59
  const evidence = {
56
60
  campaign: 'TRAPDOOR',
57
61
  triggeredRules: triggered,
58
- details: Object.fromEntries(
59
- Object.entries(results).filter(([_, r]) => r.triggered)
60
- ),
62
+ details: Object.fromEntries(Object.entries(results).filter(([_, r]) => r.triggered)),
61
63
  iocSummary: {
62
64
  publisher: 'asdxzxc',
63
65
  c2Domain: 'ddjidd564.github.io',
@@ -66,12 +68,15 @@ export async function scan(pkgJson, files = [], registryMeta = null, allFiles =
66
68
  },
67
69
  };
68
70
 
69
- return [{
70
- id: 'TRAPDOOR',
71
- severity,
72
- title: 'TrapDoor cross-ecosystem supply chain attack campaign',
73
- description: `${triggered.length} signal(s): ${triggered.join(', ')}`,
74
- evidence: JSON.stringify(evidence),
75
- mitigation: 'Block install immediately. Revoke any npm tokens associated with this package. Rotate CI/CD secrets. Audit for postinstall scripts accessing credentials. Check for AI config poisoning (.cursorrules/CLAUDE.md). Verify all package versions from publisher asdxzxc. If confirmed compromise, follow incident response procedures per SECURITY.md.',
76
- }];
71
+ return [
72
+ {
73
+ id: 'TRAPDOOR',
74
+ severity,
75
+ title: 'TrapDoor cross-ecosystem supply chain attack campaign',
76
+ description: `${triggered.length} signal(s): ${triggered.join(', ')}`,
77
+ evidence: JSON.stringify(evidence),
78
+ mitigation:
79
+ 'Block install immediately. Revoke any npm tokens associated with this package. Rotate CI/CD secrets. Audit for postinstall scripts accessing credentials. Check for AI config poisoning (.cursorrules/CLAUDE.md). Verify all package versions from publisher asdxzxc. If confirmed compromise, follow incident response procedures per SECURITY.md.',
80
+ },
81
+ ];
77
82
  }
@@ -1,19 +1,37 @@
1
1
  const BLOCKED_MAINTAINERS = ['vpmdhaj'];
2
2
  const VPMDHAJ_PREFIX_RE = /^vpmdhaj-/;
3
3
  const TYPOSQUAT_TARGETS = [
4
- 'opensearch-setup', 'env-config-manager',
5
- 'express', 'lodash', 'axios', 'react', 'vue', 'angular',
6
- 'babel', 'webpack', 'typescript', 'moment', 'dotenv',
4
+ 'opensearch-setup',
5
+ 'env-config-manager',
6
+ 'express',
7
+ 'lodash',
8
+ 'axios',
9
+ 'react',
10
+ 'vue',
11
+ 'angular',
12
+ 'babel',
13
+ 'webpack',
14
+ 'typescript',
15
+ 'moment',
16
+ 'dotenv',
7
17
  ];
8
18
 
9
19
  function levenshteinDistance(a, b) {
10
- const m = a.length, n = b.length;
20
+ const m = a.length,
21
+ n = b.length;
11
22
  const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
12
- for (let i = 0; i <= m; i++) dp[i][0] = i;
13
- for (let j = 0; j <= n; j++) dp[0][j] = j;
23
+ for (let i = 0; i <= m; i++) {
24
+ dp[i][0] = i;
25
+ }
26
+ for (let j = 0; j <= n; j++) {
27
+ dp[0][j] = j;
28
+ }
14
29
  for (let i = 1; i <= m; i++) {
15
30
  for (let j = 1; j <= n; j++) {
16
- dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
31
+ dp[i][j] =
32
+ a[i - 1] === b[j - 1]
33
+ ? dp[i - 1][j - 1]
34
+ : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
17
35
  }
18
36
  }
19
37
  return dp[m][n];
@@ -71,7 +89,13 @@ export function scanMaintainerAnomaly(pkgJson, registryMeta) {
71
89
  };
72
90
  }
73
91
 
74
- return { triggered: false, stopCondition: false, maintainer: '', suspiciousAliases: [], reason: '' };
92
+ return {
93
+ triggered: false,
94
+ stopCondition: false,
95
+ maintainer: '',
96
+ suspiciousAliases: [],
97
+ reason: '',
98
+ };
75
99
  }
76
100
 
77
101
  export { BLOCKED_MAINTAINERS };
@@ -1,8 +1,8 @@
1
1
  const SUSPICIOUS_HOOKS = ['preinstall'];
2
- const LOADER_SCRIPTS = ['setup.mjs', 'loader.js', 'stager.js', 'init.mjs'];
2
+ const _LOADER_SCRIPTS = ['setup.mjs', 'loader.js', 'stager.js', 'init.mjs'];
3
3
  const BUN_RUN_RE = /\bbun\s+run\b/;
4
4
  const NODE_SETUP_RE = /\bnode\s+(setup\.mjs|init\.mjs|loader\.js|stager\.js)\b/;
5
- const PREINSTALL_STAGER_RE = /preinstall\s*[:=]/;
5
+ const _PREINSTALL_STAGER_RE = /preinstall\s*[:=]/;
6
6
 
7
7
  export function scanPreinstallLoader(pkgJson) {
8
8
  const scripts = pkgJson?.scripts || {};
@@ -10,7 +10,9 @@ export function scanPreinstallLoader(pkgJson) {
10
10
 
11
11
  for (const hook of SUSPICIOUS_HOOKS) {
12
12
  const cmd = scripts[hook];
13
- if (!cmd) continue;
13
+ if (!cmd) {
14
+ continue;
15
+ }
14
16
 
15
17
  const details = { hookType: hook, hookCommand: cmd };
16
18
 
@@ -4,49 +4,71 @@ const VAULT_CRED_RE = /VAULT_ADDR|VAULT_TOKEN/;
4
4
  const GITHUB_TOKEN_RE = /GITHUB_TOKEN|GH_TOKEN/;
5
5
  const AWS_ACCESS_KEY_RE = /AWS_ACCESS_KEY_ID|AWS_SECRET_ACCESS_KEY|AWS_SESSION_TOKEN/;
6
6
  const BASE64_OBFUSCATION_RE = /Buffer\.from\([^)]+['"]base64['"]\)|btoa\(|atob\(/;
7
- const HTTP_POST_EXFIL_RE = /(?:fetch|axios|request|got|curl)\s*\([^)]*(?:https?:\/\/[^'"\s)\]]+)[^)]*(?:method\s*[:=]\s*['"]POST['"]|\.post\s*\()/;
8
- const DOMAIN_EXFIL_RE = /(?:fetch|axios|request|got|curl)\s*\(['"](?:https?:\/\/)?[^'"\s)\]]*\.[^'"\s)\]]{2,}[^)]*\)/;
7
+ const HTTP_POST_EXFIL_RE =
8
+ /(?:fetch|axios|request|got|curl)\s*\([^)]*(?:https?:\/\/[^'"\s)\]]+)[^)]*(?:method\s*[:=]\s*['"]POST['"]|\.post\s*\()/;
9
+ const DOMAIN_EXFIL_RE =
10
+ /(?:fetch|axios|request|got|curl)\s*\(['"](?:https?:\/\/)?[^'"\s)\]]*\.[^'"\s)\]]{2,}[^)]*\)/;
9
11
 
10
12
  const TARGET_ENV_VARS = {
11
- AWS: ['AWS_CONTAINER_CREDENTIALS_FULL_URI', 'AWS_CONTAINER_AUTHORIZATION_TOKEN', 'AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_SESSION_TOKEN'],
13
+ AWS: [
14
+ 'AWS_CONTAINER_CREDENTIALS_FULL_URI',
15
+ 'AWS_CONTAINER_AUTHORIZATION_TOKEN',
16
+ 'AWS_ACCESS_KEY_ID',
17
+ 'AWS_SECRET_ACCESS_KEY',
18
+ 'AWS_SESSION_TOKEN',
19
+ ],
12
20
  VAULT: ['VAULT_ADDR', 'VAULT_TOKEN'],
13
21
  GITHUB: ['GITHUB_TOKEN', 'GH_TOKEN'],
14
22
  };
15
23
 
16
- export function scanCredExfil(files = [], pkgJson) {
17
- const code = files.map(f => f.content || '').join('\n');
18
- if (!code) return { triggered: false, targets: [], exfilMethod: null, detectedEnvVars: [] };
24
+ export function scanCredExfil(files = [], _pkgJson) {
25
+ const code = files.map((f) => f.content || '').join('\n');
26
+ if (!code) {
27
+ return { triggered: false, targets: [], exfilMethod: null, detectedEnvVars: [] };
28
+ }
19
29
 
20
30
  const targets = [];
21
31
  const detectedEnvVars = [];
22
32
 
23
- if (AWS_IMDS_RE.test(code)) targets.push('AWS_IMDSv2');
33
+ if (AWS_IMDS_RE.test(code)) {
34
+ targets.push('AWS_IMDSv2');
35
+ }
24
36
  if (ECS_CRED_RE.test(code)) {
25
37
  targets.push('ECS_TASK_ROLE');
26
38
  for (const v of TARGET_ENV_VARS.AWS) {
27
- if (code.includes(v)) detectedEnvVars.push(v);
39
+ if (code.includes(v)) {
40
+ detectedEnvVars.push(v);
41
+ }
28
42
  }
29
43
  }
30
44
  if (VAULT_CRED_RE.test(code)) {
31
45
  targets.push('VAULT_CREDENTIALS');
32
46
  for (const v of TARGET_ENV_VARS.VAULT) {
33
- if (code.includes(v)) detectedEnvVars.push(v);
47
+ if (code.includes(v)) {
48
+ detectedEnvVars.push(v);
49
+ }
34
50
  }
35
51
  }
36
52
  if (GITHUB_TOKEN_RE.test(code)) {
37
53
  targets.push('GITHUB_TOKEN');
38
54
  for (const v of TARGET_ENV_VARS.GITHUB) {
39
- if (code.includes(v)) detectedEnvVars.push(v);
55
+ if (code.includes(v)) {
56
+ detectedEnvVars.push(v);
57
+ }
40
58
  }
41
59
  }
42
60
  if (AWS_ACCESS_KEY_RE.test(code)) {
43
61
  targets.push('AWS_ACCESS_KEYS');
44
62
  for (const v of TARGET_ENV_VARS.AWS) {
45
- if (code.includes(v) && !detectedEnvVars.includes(v)) detectedEnvVars.push(v);
63
+ if (code.includes(v) && !detectedEnvVars.includes(v)) {
64
+ detectedEnvVars.push(v);
65
+ }
46
66
  }
47
67
  }
48
68
 
49
- if (targets.length === 0) return { triggered: false, targets: [], exfilMethod: null, detectedEnvVars: [] };
69
+ if (targets.length === 0) {
70
+ return { triggered: false, targets: [], exfilMethod: null, detectedEnvVars: [] };
71
+ }
50
72
 
51
73
  let exfilMethod = null;
52
74
  if (HTTP_POST_EXFIL_RE.test(code)) {
@@ -7,7 +7,11 @@ const RULE_SEVERITY = { D1: 'critical', D2: 'critical', D3: 'critical' };
7
7
  const SEVERITY_ORDER = ['critical', 'high', 'medium', 'low', 'info', 'none'];
8
8
 
9
9
  function highestSeverity(severities) {
10
- for (const s of SEVERITY_ORDER) if (severities.includes(s)) return s;
10
+ for (const s of SEVERITY_ORDER) {
11
+ if (severities.includes(s)) {
12
+ return s;
13
+ }
14
+ }
11
15
  return 'none';
12
16
  }
13
17
 
@@ -18,36 +22,45 @@ export async function scan(pkgJson, files = [], registryMeta = null, allFiles =
18
22
 
19
23
  const d1Result = scanMaintainerAnomaly(pkgJson, registryMeta);
20
24
  if (d1Result.stopCondition) {
21
- const evidence = attachProvenance({
22
- rule: 'TSQ-MAINT-001',
23
- campaign: 'TYPOSQUAT_VPMDHAJ',
24
- triggeredChecks: ['D1'],
25
- maintainer: d1Result.maintainer,
26
- suspiciousAliases: d1Result.suspiciousAliases,
27
- action: 'BLOCK',
28
- }, {
29
- ruleId: 'TSQ-MAINT-001',
30
- ruleName: 'Maintainer & Package Alias Anomalies',
31
- severity: 'CRITICAL',
32
- campaignName: 'Mass Typosquatting (vpmdhaj)',
33
- pkgName,
34
- pkgVersion,
35
- triggered: true,
36
- severity: 'critical',
37
- indicators: [{ type: 'blocked_maintainer', value: d1Result.maintainer }, ...d1Result.suspiciousAliases.map(a => ({ type: 'suspicious_alias', value: a }))],
38
- ruleProvenanceUrl: 'https://github.com/lateos/npm-scan/blob/main/backend/detectors/typosquat-vpmdhaj/d1-maintainer.js',
39
- campaignSourceUrl: 'https://security.researcher.org/supply-chain-report',
40
- });
25
+ const evidence = attachProvenance(
26
+ {
27
+ rule: 'TSQ-MAINT-001',
28
+ campaign: 'TYPOSQUAT_VPMDHAJ',
29
+ triggeredChecks: ['D1'],
30
+ maintainer: d1Result.maintainer,
31
+ suspiciousAliases: d1Result.suspiciousAliases,
32
+ action: 'BLOCK',
33
+ },
34
+ {
35
+ ruleId: 'TSQ-MAINT-001',
36
+ ruleName: 'Maintainer & Package Alias Anomalies',
37
+ campaignName: 'Mass Typosquatting (vpmdhaj)',
38
+ pkgName,
39
+ pkgVersion,
40
+ triggered: true,
41
+ severity: 'critical',
42
+ indicators: [
43
+ { type: 'blocked_maintainer', value: d1Result.maintainer },
44
+ ...d1Result.suspiciousAliases.map((a) => ({ type: 'suspicious_alias', value: a })),
45
+ ],
46
+ ruleProvenanceUrl:
47
+ 'https://github.com/lateos/npm-scan/blob/main/backend/detectors/typosquat-vpmdhaj/d1-maintainer.js',
48
+ campaignSourceUrl: 'https://security.researcher.org/supply-chain-report',
49
+ }
50
+ );
41
51
 
42
- return [{
43
- id: 'TYPOSQUAT_VPMDHAJ',
44
- severity: 'critical',
45
- title: 'Mass Typosquatting campaign (vpmdhaj) — blocked maintainer',
46
- description: d1Result.reason,
47
- evidence: JSON.stringify(evidence),
48
- mitigation: 'BLOCK IMMEDIATELY. Do not install packages from maintainer vpmdhaj. Audit all packages from your lockfile for this maintainer. Check for typosquatting of popular packages.',
49
- stopCondition: true,
50
- }];
52
+ return [
53
+ {
54
+ id: 'TYPOSQUAT_VPMDHAJ',
55
+ severity: 'critical',
56
+ title: 'Mass Typosquatting campaign (vpmdhaj) — blocked maintainer',
57
+ description: d1Result.reason,
58
+ evidence: JSON.stringify(evidence),
59
+ mitigation:
60
+ 'BLOCK IMMEDIATELY. Do not install packages from maintainer vpmdhaj. Audit all packages from your lockfile for this maintainer. Check for typosquatting of popular packages.',
61
+ stopCondition: true,
62
+ },
63
+ ];
51
64
  }
52
65
 
53
66
  const d2Result = scanPreinstallLoader(pkgJson);
@@ -63,36 +76,42 @@ export async function scan(pkgJson, files = [], registryMeta = null, allFiles =
63
76
  .filter(([_, r]) => r.triggered)
64
77
  .map(([id]) => id);
65
78
 
66
- if (triggered.length === 0) return [];
79
+ if (triggered.length === 0) {
80
+ return [];
81
+ }
67
82
 
68
- const severity = highestSeverity(triggered.map(id => RULE_SEVERITY[id]));
83
+ const severity = highestSeverity(triggered.map((id) => RULE_SEVERITY[id]));
69
84
 
70
- const evidence = attachProvenance({
71
- campaign: 'TYPOSQUAT_VPMDHAJ',
72
- triggeredChecks: triggered,
73
- details: Object.fromEntries(
74
- Object.entries(results).filter(([_, r]) => r.triggered)
75
- ),
76
- }, {
77
- ruleId: 'TYPOSQUAT_VPMDHAJ',
78
- ruleName: 'Mass Typosquatting Campaign Detection',
79
- severity: severity.toUpperCase(),
80
- campaignName: 'Mass Typosquatting (vpmdhaj)',
81
- pkgName,
82
- pkgVersion,
83
- triggered: true,
84
- severity,
85
- indicators: triggered.map(id => ({ type: `rule_${id}`, value: RULE_SEVERITY[id] })),
86
- ruleProvenanceUrl: 'https://github.com/lateos/npm-scan/blob/main/backend/detectors/typosquat-vpmdhaj/',
87
- campaignSourceUrl: 'https://security.researcher.org/supply-chain-report',
88
- });
85
+ const evidence = attachProvenance(
86
+ {
87
+ campaign: 'TYPOSQUAT_VPMDHAJ',
88
+ triggeredChecks: triggered,
89
+ details: Object.fromEntries(Object.entries(results).filter(([_, r]) => r.triggered)),
90
+ },
91
+ {
92
+ ruleId: 'TYPOSQUAT_VPMDHAJ',
93
+ ruleName: 'Mass Typosquatting Campaign Detection',
94
+ campaignName: 'Mass Typosquatting (vpmdhaj)',
95
+ pkgName,
96
+ pkgVersion,
97
+ triggered: true,
98
+ severity,
99
+ indicators: triggered.map((id) => ({ type: `rule_${id}`, value: RULE_SEVERITY[id] })),
100
+ ruleProvenanceUrl:
101
+ 'https://github.com/lateos/npm-scan/blob/main/backend/detectors/typosquat-vpmdhaj/',
102
+ campaignSourceUrl: 'https://security.researcher.org/supply-chain-report',
103
+ }
104
+ );
89
105
 
90
- return [{
91
- id: 'TYPOSQUAT_VPMDHAJ',
92
- severity,
93
- title: 'Mass Typosquatting campaign (vpmdhaj)',
94
- description: `${triggered.length} signal(s): ${triggered.join(', ')}`,
95
- evidence: JSON.stringify(evidence),
96
- mitigation: 'Block install immediately. Revoke any npm tokens. Rotate CI/CD secrets. Audit all packages from maintainer vpmdhaj. If credential exfiltration detected: rotate AWS IAM keys, Vault tokens, and GitHub tokens immediately. Verify CloudTrail/audit logs for unauthorized access.',
97
- }];
106
+ return [
107
+ {
108
+ id: 'TYPOSQUAT_VPMDHAJ',
109
+ severity,
110
+ title: 'Mass Typosquatting campaign (vpmdhaj)',
111
+ description: `${triggered.length} signal(s): ${triggered.join(', ')}`,
112
+ evidence: JSON.stringify(evidence),
113
+ mitigation:
114
+ 'Block install immediately. Revoke any npm tokens. Rotate CI/CD secrets. Audit all packages from maintainer vpmdhaj. If credential exfiltration detected: rotate AWS IAM keys, Vault tokens, and GitHub tokens immediately. Verify CloudTrail/audit logs for unauthorized access.',
115
+ },
116
+ ];
98
117
  }