@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,17 +1,26 @@
1
1
  const ACTIVATION_RISK_MATRIX = {
2
2
  '*': { base: 'critical', label: 'Wildcard (all files)' },
3
- 'onStartupFinished': { base: 'high', label: 'Startup finished' },
3
+ onStartupFinished: { base: 'high', label: 'Startup finished' },
4
4
  'workspaceContains:**/*': { base: 'high', label: 'Workspace contains wildcard' },
5
- 'workspaceContains': { base: 'high', label: 'Workspace contains' },
5
+ workspaceContains: { base: 'high', label: 'Workspace contains' },
6
6
  'onCommand:*': { base: 'low', label: 'Any command' },
7
7
  };
8
8
 
9
9
  const DEFAULT_BASE_RISK = 'medium';
10
10
 
11
11
  const ESCALATION_KEYWORDS = [
12
- 'npx', 'bun', 'curl', 'wget', 'fetch(',
13
- 'exec(', 'spawn(', 'execSync', 'spawnSync',
14
- 'child_process', 'shell: true', 'detached: true',
12
+ 'npx',
13
+ 'bun',
14
+ 'curl',
15
+ 'wget',
16
+ 'fetch(',
17
+ 'exec(',
18
+ 'spawn(',
19
+ 'execSync',
20
+ 'spawnSync',
21
+ 'child_process',
22
+ 'shell: true',
23
+ 'detached: true',
15
24
  ];
16
25
 
17
26
  const BUNDLED_BUN_PATTERN = /bun|runtime/;
@@ -20,7 +29,11 @@ const SIZE_DELTA_THRESHOLD = 400 * 1024;
20
29
 
21
30
  const SHELL_CMDS = ['npx', 'bun', 'curl', 'wget', 'exec', 'spawn', 'execSync'];
22
31
 
23
- export async function checkActivationEventRisk(extensionManifest, versionHistory = [], priorVersions = []) {
32
+ export async function checkActivationEventRisk(
33
+ extensionManifest,
34
+ versionHistory = [],
35
+ priorVersions = []
36
+ ) {
24
37
  const signals = [];
25
38
 
26
39
  const activationEvents = extensionManifest?.activationEvents || [];
@@ -32,7 +45,7 @@ export async function checkActivationEventRisk(extensionManifest, versionHistory
32
45
  const riskLabels = ['none', 'low', 'medium', 'high', 'critical'];
33
46
  const riskValues = { none: 0, low: 1, medium: 2, high: 3, critical: 4 };
34
47
 
35
- let worstEvent = null;
48
+ let _worstEvent = null;
36
49
  const why = [];
37
50
 
38
51
  for (const event of activationEvents) {
@@ -41,29 +54,31 @@ export async function checkActivationEventRisk(extensionManifest, versionHistory
41
54
  const baseIdx = riskValues[risk.base] || riskValues[DEFAULT_BASE_RISK];
42
55
  if (baseIdx > maxBaseRisk) {
43
56
  maxBaseRisk = baseIdx;
44
- worstEvent = event;
57
+ _worstEvent = event;
45
58
  }
46
59
  } else if (event.includes('*') && event !== 'onCommand:*') {
47
60
  const baseIdx = riskValues['high'];
48
61
  if (baseIdx > maxBaseRisk) {
49
62
  maxBaseRisk = baseIdx;
50
- worstEvent = event;
63
+ _worstEvent = event;
51
64
  }
52
65
  }
53
66
  }
54
67
 
55
68
  const contributes = extensionManifest?.contributes || {};
56
69
  const commands = contributes?.commands || [];
57
- const cmdTitles = commands.map(c => (c.title || '').toLowerCase()).join(' ');
70
+ const cmdTitles = commands.map((c) => (c.title || '').toLowerCase()).join(' ');
58
71
 
59
72
  const bundledDeps = extensionManifest?.bundledDependencies || [];
60
73
  const bundledStr = Array.isArray(bundledDeps) ? bundledDeps.join(' ') : '';
61
74
 
62
- const hasShellKeyword = SHELL_CMDS.some(cmd => cmdTitles.includes(cmd));
75
+ const hasShellKeyword = SHELL_CMDS.some((cmd) => cmdTitles.includes(cmd));
63
76
  const hasBunBundled = BUNDLED_BUN_PATTERN.test(bundledStr);
64
77
 
65
78
  const activationEventsStr = activationEvents.join(' ');
66
- const hasShellInActivationContext = ESCALATION_KEYWORDS.some(kw => activationEventsStr.toLowerCase().includes(kw.toLowerCase()));
79
+ const hasShellInActivationContext = ESCALATION_KEYWORDS.some((kw) =>
80
+ activationEventsStr.toLowerCase().includes(kw.toLowerCase())
81
+ );
67
82
 
68
83
  let escalateToCritical = false;
69
84
 
@@ -74,22 +89,22 @@ export async function checkActivationEventRisk(extensionManifest, versionHistory
74
89
 
75
90
  if (versionHistory.length >= 2) {
76
91
  const sizes = versionHistory
77
- .filter(v => v.assetSize)
78
- .map(v => v.assetSize)
92
+ .filter((v) => v.assetSize)
93
+ .map((v) => v.assetSize)
79
94
  .sort((a, b) => b - a);
80
95
 
81
- if (sizes.length >= 2 && (sizes[0] - sizes[sizes.length - 1]) > SIZE_DELTA_THRESHOLD) {
96
+ if (sizes.length >= 2 && sizes[0] - sizes[sizes.length - 1] > SIZE_DELTA_THRESHOLD) {
82
97
  escalateToCritical = true;
83
98
  why.push(`HIGH activation event + version size delta > ${SIZE_DELTA_THRESHOLD} bytes`);
84
99
  }
85
100
  }
86
101
 
87
102
  const priorActivationEvents = priorVersions
88
- .filter(v => v.activationEvents)
89
- .flatMap(v => v.activationEvents);
103
+ .filter((v) => v.activationEvents)
104
+ .flatMap((v) => v.activationEvents);
90
105
 
91
106
  if (priorActivationEvents.length > 0) {
92
- const newEvents = activationEvents.filter(e => !priorActivationEvents.includes(e));
107
+ const newEvents = activationEvents.filter((e) => !priorActivationEvents.includes(e));
93
108
  if (newEvents.length > 0) {
94
109
  why.push(`First-time activation event(s) added: ${newEvents.join(', ')}`);
95
110
  if (!escalateToCritical && maxBaseRisk >= riskValues['high']) {
@@ -103,7 +118,9 @@ export async function checkActivationEventRisk(extensionManifest, versionHistory
103
118
  riskLevel = 'critical';
104
119
  }
105
120
 
106
- if (!riskLevel) return { triggered: false, signals: [], riskLevel: null, why: [] };
121
+ if (!riskLevel) {
122
+ return { triggered: false, signals: [], riskLevel: null, why: [] };
123
+ }
107
124
 
108
125
  signals.push({
109
126
  type: 'ACTIVATION_EVENT_RISK',
@@ -4,12 +4,14 @@ export async function checkBurstPublish(versionHistory, config = {}) {
4
4
  const hotPullMinutes = config.hotPullMinutes ?? 20;
5
5
 
6
6
  const entries = versionHistory
7
- .filter(v => v.publishedAt)
8
- .map(v => ({ version: v.version, time: new Date(v.publishedAt).getTime() }))
9
- .filter(e => !Number.isNaN(e.time))
7
+ .filter((v) => v.publishedAt)
8
+ .map((v) => ({ version: v.version, time: new Date(v.publishedAt).getTime() }))
9
+ .filter((e) => !Number.isNaN(e.time))
10
10
  .sort((a, b) => a.time - b.time);
11
11
 
12
- if (entries.length < threshold) return { triggered: false };
12
+ if (entries.length < threshold) {
13
+ return { triggered: false };
14
+ }
13
15
 
14
16
  const windowMs = windowMinutes * 60 * 1000;
15
17
  let burstFound = false;
@@ -21,14 +23,14 @@ export async function checkBurstPublish(versionHistory, config = {}) {
21
23
  for (let i = 0; i < entries.length; i++) {
22
24
  const start = entries[i].time;
23
25
  const end = start + windowMs;
24
- const inWindow = entries.filter(e => e.time >= start && e.time <= end);
26
+ const inWindow = entries.filter((e) => e.time >= start && e.time <= end);
25
27
 
26
28
  if (inWindow.length >= threshold) {
27
29
  burstFound = true;
28
30
  burstWindowStart = new Date(start).toISOString();
29
31
  burstWindowEnd = new Date(end).toISOString();
30
32
  burstVersionCount = inWindow.length;
31
- burstVersions = inWindow.map(e => e.version);
33
+ burstVersions = inWindow.map((e) => e.version);
32
34
  break;
33
35
  }
34
36
  }
@@ -45,7 +47,12 @@ export async function checkBurstPublish(versionHistory, config = {}) {
45
47
  return {
46
48
  triggered: burstFound || hotPullDetected,
47
49
  burstWindow: burstFound
48
- ? { start: burstWindowStart, end: burstWindowEnd, versionCount: burstVersionCount, versions: burstVersions }
50
+ ? {
51
+ start: burstWindowStart,
52
+ end: burstWindowEnd,
53
+ versionCount: burstVersionCount,
54
+ versions: burstVersions,
55
+ }
49
56
  : null,
50
57
  hotPullDetected,
51
58
  };
@@ -35,7 +35,9 @@ const ANTI_ANALYSIS_PATTERNS = [
35
35
  ];
36
36
 
37
37
  function truncateSnippet(str, maxLen = 200) {
38
- if (!str || str.length <= maxLen) return str || '';
38
+ if (!str || str.length <= maxLen) {
39
+ return str || '';
40
+ }
39
41
  return str.slice(0, maxLen) + '...';
40
42
  }
41
43
 
@@ -46,14 +48,16 @@ export async function checkExfilPattern(extensionFiles = []) {
46
48
 
47
49
  for (const file of extensionFiles) {
48
50
  const content = typeof file.content === 'string' ? file.content : '';
49
- if (!content) continue;
51
+ if (!content) {
52
+ continue;
53
+ }
50
54
  const path = file.path || '';
51
55
 
52
56
  for (const cp of CREDENTIAL_FILE_PATTERNS) {
53
57
  const match = content.match(cp);
54
58
  if (match) {
55
59
  const snippet = truncateSnippet(match[0]);
56
- if (!exfilPatterns.some(e => e.includes(snippet))) {
60
+ if (!exfilPatterns.some((e) => e.includes(snippet))) {
57
61
  exfilPatterns.push(`${path}: ${snippet}`);
58
62
  signals.push({ type: 'CREDENTIAL_FILE_TARGET', pattern: cp.source, file: path });
59
63
  }
@@ -11,7 +11,9 @@ const __dirname = dirname(__filename);
11
11
  const IOC_PATH = join(__dirname, '..', 'vsix-iocs.json');
12
12
 
13
13
  function loadIOCData() {
14
- if (iocsLoaded) return iocsData;
14
+ if (iocsLoaded) {
15
+ return iocsData;
16
+ }
15
17
  iocsLoaded = true;
16
18
  try {
17
19
  iocsData = JSON.parse(readFileSync(IOC_PATH, 'utf8'));
@@ -32,9 +34,17 @@ export function reloadIOCData() {
32
34
  return loadIOCData();
33
35
  }
34
36
 
35
- export async function checkKnownIOC(extensionId, version, publisherAccount, orphanCommits = [], versionHistory = []) {
37
+ export async function checkKnownIOC(
38
+ extensionId,
39
+ version,
40
+ publisherAccount,
41
+ orphanCommits = [],
42
+ versionHistory = []
43
+ ) {
36
44
  const data = loadIOCData();
37
- if (!data) return { triggered: false, matches: [] };
45
+ if (!data) {
46
+ return { triggered: false, matches: [] };
47
+ }
38
48
 
39
49
  const matches = [];
40
50
  const iocs = data.iocs || [];
@@ -43,7 +53,11 @@ export async function checkKnownIOC(extensionId, version, publisherAccount, orph
43
53
  switch (ioc.type) {
44
54
  case 'extensionId': {
45
55
  if (ioc.value === extensionId) {
46
- if (!ioc.maliciousVersions || ioc.maliciousVersions.length === 0 || ioc.maliciousVersions.includes(version)) {
56
+ if (
57
+ !ioc.maliciousVersions ||
58
+ ioc.maliciousVersions.length === 0 ||
59
+ ioc.maliciousVersions.includes(version)
60
+ ) {
47
61
  matches.push({
48
62
  type: 'extensionId',
49
63
  value: extensionId,
@@ -60,9 +74,10 @@ export async function checkKnownIOC(extensionId, version, publisherAccount, orph
60
74
 
61
75
  case 'publisherAccount': {
62
76
  if (ioc.value === publisherAccount) {
63
- const pubTime = versionHistory.length > 0
64
- ? new Date(versionHistory[versionHistory.length - 1]?.publishedAt).getTime()
65
- : null;
77
+ const pubTime =
78
+ versionHistory.length > 0
79
+ ? new Date(versionHistory[versionHistory.length - 1]?.publishedAt).getTime()
80
+ : null;
66
81
 
67
82
  const windowStart = new Date(ioc.compromiseWindowStart).getTime();
68
83
  const windowEnd = ioc.compromiseWindowEnd
@@ -84,7 +99,7 @@ export async function checkKnownIOC(extensionId, version, publisherAccount, orph
84
99
 
85
100
  case 'orphanCommitHash': {
86
101
  for (const commit of orphanCommits) {
87
- if (ioc.value === commit || (ioc.value === 'PLACEHOLDER_UPDATE_FROM_THREAT_INTEL')) {
102
+ if (ioc.value === commit || ioc.value === 'PLACEHOLDER_UPDATE_FROM_THREAT_INTEL') {
88
103
  continue;
89
104
  }
90
105
  if (ioc.value && commit && ioc.value.toLowerCase() === commit.toLowerCase()) {
@@ -1,10 +1,13 @@
1
- const GITHUB_COMMIT_SHA_PATTERN = /api\.github\.com\/repos\/[^/]+\/[^/]+\/git\/commits\/[a-f0-9]{40}/;
1
+ const GITHUB_COMMIT_SHA_PATTERN =
2
+ /api\.github\.com\/repos\/[^/]+\/[^/]+\/git\/commits\/[a-f0-9]{40}/;
2
3
  const NPX_GIT_URL_PATTERN = /npx\s+.*github\.com.*#[a-f0-9]{8,}/;
3
4
  const MCP_KEYWORDS = ['mcp', 'model-context-protocol', 'claude', 'setup', 'init'];
4
- const EXTERNAL_FETCH_PATTERN = /(?:https?:\/\/)[^\s"')\]]+(?:\.com|\.io|\.org|\.dev|\.app|\.net)[^\s"')\]]*/;
5
- const NON_NPMJS_FETCH = /(?:fetch|curl|wget)\s*\(?\s*["']https?:\/\/(?!(?:.*npmjs\.org|.*npm\.js\.org|.*github\.com))[^"']+/;
5
+ const _EXTERNAL_FETCH_PATTERN =
6
+ /(?:https?:\/\/)[^\s"')\]]+(?:\.com|\.io|\.org|\.dev|\.app|\.net)[^\s"')\]]*/;
7
+ const NON_NPMJS_FETCH =
8
+ /(?:fetch|curl|wget)\s*\(?\s*["']https?:\/\/(?!(?:.*npmjs\.org|.*npm\.js\.org|.*github\.com))[^"']+/;
6
9
  const BUN_PATTERNS = [/bun\s+install/, /install\s+.*bun/, /\bbunx\b/, /\.bun\/bin\//];
7
- const NPX_GIT_SHORT = /npx\s+.*github\.com.*#[a-f0-9]{8,}/;
10
+ const _NPX_GIT_SHORT = /npx\s+.*github\.com.*#[a-f0-9]{8,}/;
8
11
 
9
12
  export async function checkOrphanCommitFetch(extensionFiles = []) {
10
13
  const signals = [];
@@ -12,7 +15,9 @@ export async function checkOrphanCommitFetch(extensionFiles = []) {
12
15
 
13
16
  for (const file of extensionFiles) {
14
17
  const content = typeof file.content === 'string' ? file.content : '';
15
- if (!content) continue;
18
+ if (!content) {
19
+ continue;
20
+ }
16
21
  const path = file.path || '';
17
22
 
18
23
  if (GITHUB_COMMIT_SHA_PATTERN.test(content)) {
@@ -39,8 +44,7 @@ export async function checkOrphanCommitFetch(extensionFiles = []) {
39
44
  }
40
45
  }
41
46
 
42
- const hasMCPKeywords = MCP_KEYWORDS.some(kw =>
43
- new RegExp(`\\b${kw}\\b`, 'i').test(content));
47
+ const hasMCPKeywords = MCP_KEYWORDS.some((kw) => new RegExp(`\\b${kw}\\b`, 'i').test(content));
44
48
  const hasExternalFetch = NON_NPMJS_FETCH.test(content);
45
49
 
46
50
  if (hasMCPKeywords && hasExternalFetch) {
@@ -1,24 +1,33 @@
1
- export async function checkPublisherAnomaly(extensionMetadata, publisherProfile, versionHistory, config = {}) {
1
+ export async function checkPublisherAnomaly(
2
+ extensionMetadata,
3
+ publisherProfile,
4
+ versionHistory,
5
+ config = {}
6
+ ) {
2
7
  const signals = [];
3
8
 
4
- const crossNamespaceThreshold = config.crossNamespaceThreshold ?? 3;
5
- const crossNamespaceDays = config.crossNamespaceDays ?? 14;
9
+ const _crossNamespaceThreshold = config.crossNamespaceThreshold ?? 3;
10
+ const _crossNamespaceDays = config.crossNamespaceDays ?? 14;
6
11
  const newAccountAgeDays = config.newAccountAgeDays ?? 30;
7
12
  const highInstallThreshold = config.highInstallThreshold ?? 100000;
8
13
  const addPublishWindowMinutes = config.addPublishWindowMinutes ?? 15;
9
14
 
10
15
  const versions = versionHistory || [];
11
- if (versions.length === 0) return { triggered: false, signals: [] };
16
+ if (versions.length === 0) {
17
+ return { triggered: false, signals: [] };
18
+ }
12
19
 
13
- const publishers = [...new Set(versions.map(v => v.publishedBy).filter(Boolean))];
14
- if (publishers.length === 0) return { triggered: false, signals: [] };
20
+ const publishers = [...new Set(versions.map((v) => v.publishedBy).filter(Boolean))];
21
+ if (publishers.length === 0) {
22
+ return { triggered: false, signals: [] };
23
+ }
15
24
 
16
25
  const sortedVersions = [...versions]
17
- .filter(v => v.publishedAt)
26
+ .filter((v) => v.publishedAt)
18
27
  .sort((a, b) => new Date(a.publishedAt) - new Date(b.publishedAt));
19
28
 
20
29
  const extPublisher = publishers[0];
21
- const allSame = publishers.every(p => p === extPublisher);
30
+ const allSame = publishers.every((p) => p === extPublisher);
22
31
 
23
32
  if (!allSame) {
24
33
  for (const pub of publishers) {
@@ -32,13 +41,18 @@ export async function checkPublisherAnomaly(extensionMetadata, publisherProfile,
32
41
  }
33
42
  }
34
43
 
35
- const extInstallCount = extensionMetadata?.statistics?.find(s => s.statisticName === 'install')?.value || 0;
44
+ const extInstallCount =
45
+ extensionMetadata?.statistics?.find((s) => s.statisticName === 'install')?.value || 0;
36
46
 
37
47
  const extAgeDays = publisherProfile?.dateCreated
38
48
  ? (Date.now() - new Date(publisherProfile.dateCreated).getTime()) / (1000 * 60 * 60 * 24)
39
49
  : null;
40
50
 
41
- if (extAgeDays !== null && extAgeDays < newAccountAgeDays && extInstallCount >= highInstallThreshold) {
51
+ if (
52
+ extAgeDays !== null &&
53
+ extAgeDays < newAccountAgeDays &&
54
+ extInstallCount >= highInstallThreshold
55
+ ) {
42
56
  signals.push({
43
57
  type: 'NEW_ACCOUNT_HIGH_INSTALL',
44
58
  accountAgeDays: Math.round(extAgeDays),
@@ -4,18 +4,32 @@ import { checkActivationEventRisk } from './detectors/activation-event-risk.js';
4
4
  import { checkOrphanCommitFetch } from './detectors/orphan-commit-fetch.js';
5
5
  import { checkKnownIOC } from './detectors/known-ioc.js';
6
6
  import { checkExfilPattern } from './detectors/exfil-pattern.js';
7
- import { getExtensionMetadata, getVersionHistory, getPublisherProfile, getOpenVsxMetadata, getOpenVsxVersionHistory } from './marketplace-client.js';
8
-
9
- const SEVERITY_SCORE = { none: 0, low: 1, medium: 2, high: 3, critical: 4 };
7
+ import {
8
+ getExtensionMetadata,
9
+ getVersionHistory,
10
+ getPublisherProfile,
11
+ getOpenVsxMetadata as _getOpenVsxMetadata,
12
+ getOpenVsxVersionHistory,
13
+ } from './marketplace-client.js';
14
+
15
+ const _SEVERITY_SCORE = { none: 0, low: 1, medium: 2, high: 3, critical: 4 };
10
16
  const SEVERITY_LABELS = ['none', 'low', 'medium', 'high', 'critical'];
11
17
 
12
18
  export async function vsixScan(extensionId, options = {}) {
13
19
  const { publisherId, extensionName } = parseExtensionId(extensionId);
14
20
 
15
- const marketplaceMeta = options.marketplaceMeta || (options.skipNetwork ? null : await getExtensionMetadata(publisherId, extensionName));
16
- const marketplaceVersions = options.marketplaceVersions || (marketplaceMeta ? await getVersionHistory(publisherId, extensionName) : []);
17
- const openVsxVersions = options.openVsxVersions || (options.skipNetwork ? [] : await getOpenVsxVersionHistory(publisherId, extensionName));
18
- const publisherProfile = options.publisherProfile || (options.skipNetwork ? null : await getPublisherProfile(publisherId));
21
+ const marketplaceMeta =
22
+ options.marketplaceMeta ||
23
+ (options.skipNetwork ? null : await getExtensionMetadata(publisherId, extensionName));
24
+ const marketplaceVersions =
25
+ options.marketplaceVersions ||
26
+ (marketplaceMeta ? await getVersionHistory(publisherId, extensionName) : []);
27
+ const openVsxVersions =
28
+ options.openVsxVersions ||
29
+ (options.skipNetwork ? [] : await getOpenVsxVersionHistory(publisherId, extensionName));
30
+ const publisherProfile =
31
+ options.publisherProfile ||
32
+ (options.skipNetwork ? null : await getPublisherProfile(publisherId));
19
33
 
20
34
  const allVersions = mergeVersionHistories(marketplaceVersions, openVsxVersions);
21
35
  const manifest = options.manifest || extractManifest(marketplaceMeta, extensionId);
@@ -25,7 +39,7 @@ export async function vsixScan(extensionId, options = {}) {
25
39
  const activationResult = await checkActivationEventRisk(
26
40
  manifest,
27
41
  allVersions,
28
- options.priorVersions || [],
42
+ options.priorVersions || []
29
43
  );
30
44
 
31
45
  const burstResult = await checkBurstPublish(allVersions, config);
@@ -34,50 +48,82 @@ export async function vsixScan(extensionId, options = {}) {
34
48
  manifest || {},
35
49
  publisherProfile || {},
36
50
  allVersions,
37
- config,
51
+ config
38
52
  );
39
53
 
40
54
  const orphanResult = await checkOrphanCommitFetch(options.extensionFiles || []);
41
55
 
42
56
  const iocResult = await checkKnownIOC(
43
57
  extensionId,
44
- options.version || (allVersions.length > 0 ? allVersions[allVersions.length - 1].version : 'unknown'),
58
+ options.version ||
59
+ (allVersions.length > 0 ? allVersions[allVersions.length - 1].version : 'unknown'),
45
60
  publisherId,
46
61
  orphanResult.signals
47
- .filter(s => s.type === 'ORPHAN_COMMIT_GITHUB_API')
48
- .map(s => s.indicator),
49
- allVersions,
62
+ .filter((s) => s.type === 'ORPHAN_COMMIT_GITHUB_API')
63
+ .map((s) => s.indicator),
64
+ allVersions
50
65
  );
51
66
 
52
67
  const exfilResult = await checkExfilPattern(options.extensionFiles || []);
53
68
 
54
69
  const triggeredSignals = [];
55
- if (burstResult.triggered) triggeredSignals.push('VSIX_BURST_PUBLISH');
56
- if (publisherResult.triggered) triggeredSignals.push('VSIX_PUBLISHER_ANOMALY');
57
- if (activationResult.triggered) triggeredSignals.push('VSIX_ACTIVATION_EVENT_RISK');
58
- if (orphanResult.triggered) triggeredSignals.push('VSIX_ORPHAN_COMMIT_FETCH');
59
- if (iocResult.triggered) triggeredSignals.push('VSIX_KNOWN_IOC');
60
- if (exfilResult.triggered) triggeredSignals.push('VSIX_EXFIL_PATTERN');
70
+ if (burstResult.triggered) {
71
+ triggeredSignals.push('VSIX_BURST_PUBLISH');
72
+ }
73
+ if (publisherResult.triggered) {
74
+ triggeredSignals.push('VSIX_PUBLISHER_ANOMALY');
75
+ }
76
+ if (activationResult.triggered) {
77
+ triggeredSignals.push('VSIX_ACTIVATION_EVENT_RISK');
78
+ }
79
+ if (orphanResult.triggered) {
80
+ triggeredSignals.push('VSIX_ORPHAN_COMMIT_FETCH');
81
+ }
82
+ if (iocResult.triggered) {
83
+ triggeredSignals.push('VSIX_KNOWN_IOC');
84
+ }
85
+ if (exfilResult.triggered) {
86
+ triggeredSignals.push('VSIX_EXFIL_PATTERN');
87
+ }
61
88
 
62
- if (triggeredSignals.length === 0) return [];
89
+ if (triggeredSignals.length === 0) {
90
+ return [];
91
+ }
63
92
 
64
93
  const registryLabels = [];
65
- if (marketplaceVersions.length > 0) registryLabels.push('marketplace');
66
- if (openVsxVersions.length > 0) registryLabels.push('open-vsx');
94
+ if (marketplaceVersions.length > 0) {
95
+ registryLabels.push('marketplace');
96
+ }
97
+ if (openVsxVersions.length > 0) {
98
+ registryLabels.push('open-vsx');
99
+ }
67
100
 
68
101
  const maxSeverity = triggeredSignals.reduce((max, s) => {
69
- if (s === 'VSIX_KNOWN_IOC' || s === 'VSIX_ORPHAN_COMMIT_FETCH') return Math.max(max, 4);
70
- if (s === 'VSIX_BURST_PUBLISH' || s === 'VSIX_PUBLISHER_ANOMALY' || s === 'VSIX_EXFIL_PATTERN') return Math.max(max, 3);
71
- if (s === 'VSIX_ACTIVATION_EVENT_RISK') return Math.max(max, 3);
102
+ if (s === 'VSIX_KNOWN_IOC' || s === 'VSIX_ORPHAN_COMMIT_FETCH') {
103
+ return Math.max(max, 4);
104
+ }
105
+ if (
106
+ s === 'VSIX_BURST_PUBLISH' ||
107
+ s === 'VSIX_PUBLISHER_ANOMALY' ||
108
+ s === 'VSIX_EXFIL_PATTERN'
109
+ ) {
110
+ return Math.max(max, 3);
111
+ }
112
+ if (s === 'VSIX_ACTIVATION_EVENT_RISK') {
113
+ return Math.max(max, 3);
114
+ }
72
115
  return max;
73
116
  }, 0);
74
117
 
75
118
  const finalSeverity = SEVERITY_LABELS[maxSeverity] || 'high';
76
119
 
77
- const latestVersion = allVersions.length > 0 ? allVersions[allVersions.length - 1].version : 'unknown';
120
+ const latestVersion =
121
+ allVersions.length > 0 ? allVersions[allVersions.length - 1].version : 'unknown';
78
122
  let exposureWindowMinutes = null;
79
123
  if (burstResult.hotPullDetected && allVersions.length >= 2) {
80
- const sorted = [...allVersions].sort((a, b) => new Date(b.publishedAt) - new Date(a.publishedAt));
124
+ const sorted = [...allVersions].sort(
125
+ (a, b) => new Date(b.publishedAt) - new Date(a.publishedAt)
126
+ );
81
127
  const gap = (new Date(sorted[0].publishedAt) - new Date(sorted[1].publishedAt)) / (1000 * 60);
82
128
  exposureWindowMinutes = Math.round(gap);
83
129
  }
@@ -92,7 +138,9 @@ export async function vsixScan(extensionId, options = {}) {
92
138
  hotPullDetected: burstResult.hotPullDetected,
93
139
  publisherSignals: publisherResult.triggered ? publisherResult.signals : null,
94
140
  activationEvents: manifest?.activationEvents || null,
95
- activationRisk: activationResult.triggered ? { riskLevel: activationResult.riskLevel, why: activationResult.why } : null,
141
+ activationRisk: activationResult.triggered
142
+ ? { riskLevel: activationResult.riskLevel, why: activationResult.why }
143
+ : null,
96
144
  orphanCommitIndicators: orphanResult.triggered ? orphanResult.indicators : null,
97
145
  iocMatches: iocResult.triggered ? iocResult.matches : null,
98
146
  exfilPatterns: exfilResult.triggered ? exfilResult.exfilPatterns : null,
@@ -101,14 +149,16 @@ export async function vsixScan(extensionId, options = {}) {
101
149
 
102
150
  const remediationGuidance = buildRemediation(triggeredSignals, extensionId);
103
151
 
104
- return [{
105
- id: 'VSIX_SCAN',
106
- severity: finalSeverity,
107
- title: `VS Code extension risk: ${extensionId}`,
108
- description: `${triggeredSignals.length} signal(s): ${triggeredSignals.join(', ')}`,
109
- evidence: JSON.stringify(evidence),
110
- mitigation: remediationGuidance,
111
- }];
152
+ return [
153
+ {
154
+ id: 'VSIX_SCAN',
155
+ severity: finalSeverity,
156
+ title: `VS Code extension risk: ${extensionId}`,
157
+ description: `${triggeredSignals.length} signal(s): ${triggeredSignals.join(', ')}`,
158
+ evidence: JSON.stringify(evidence),
159
+ mitigation: remediationGuidance,
160
+ },
161
+ ];
112
162
  }
113
163
 
114
164
  function parseExtensionId(id) {
@@ -135,7 +185,7 @@ function mergeVersionHistories(marketplace, openVsx) {
135
185
  seen.add(v.version);
136
186
  merged.push({ ...v, registries: ['open-vsx'] });
137
187
  } else {
138
- const existing = merged.find(m => m.version === v.version);
188
+ const existing = merged.find((m) => m.version === v.version);
139
189
  if (existing) {
140
190
  existing.registries.push('open-vsx');
141
191
  }
@@ -145,14 +195,20 @@ function mergeVersionHistories(marketplace, openVsx) {
145
195
  return merged.sort((a, b) => new Date(a.publishedAt) - new Date(b.publishedAt));
146
196
  }
147
197
 
148
- function extractManifest(marketplaceMeta, extensionId) {
149
- if (!marketplaceMeta?.results?.[0]?.extensions?.[0]) return {};
198
+ function extractManifest(marketplaceMeta, _extensionId) {
199
+ if (!marketplaceMeta?.results?.[0]?.extensions?.[0]) {
200
+ return {};
201
+ }
150
202
  const ext = marketplaceMeta.results[0].extensions[0];
151
203
  const manifestStr = ext.galleryApiUrl || ext.manifest;
152
- if (!manifestStr) return {};
204
+ if (!manifestStr) {
205
+ return {};
206
+ }
153
207
 
154
208
  try {
155
- if (typeof manifestStr === 'object') return manifestStr;
209
+ if (typeof manifestStr === 'object') {
210
+ return manifestStr;
211
+ }
156
212
  return JSON.parse(manifestStr);
157
213
  } catch {
158
214
  return {};