@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
@@ -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 {};
@@ -7,7 +7,7 @@ const RATE_LIMIT_MS = 6000;
7
7
  let _lastFetchTime = 0;
8
8
 
9
9
  function sleep(ms) {
10
- return new Promise(r => setTimeout(r, ms));
10
+ return new Promise((r) => setTimeout(r, ms));
11
11
  }
12
12
 
13
13
  async function rateLimitedFetch(url) {
@@ -44,20 +44,22 @@ async function rateLimitedFetch(url) {
44
44
  }
45
45
  }
46
46
 
47
- function parseExtensionId(id) {
47
+ function _parseExtensionId(id) {
48
48
  const parts = id.split('.');
49
- if (parts.length < 2) throw new Error(`Invalid extension ID: ${id}`);
49
+ if (parts.length < 2) {
50
+ throw new Error(`Invalid extension ID: ${id}`);
51
+ }
50
52
  return { publisherId: parts[0], extensionName: parts.slice(1).join('.') };
51
53
  }
52
54
 
53
55
  export async function getExtensionMetadata(publisherId, extensionName) {
54
56
  const url = `${MARKETPLACE_API}/extensionquery`;
55
57
  const body = {
56
- filters: [{
57
- criteria: [
58
- { filterType: 8, value: `${publisherId}.${extensionName}` },
59
- ],
60
- }],
58
+ filters: [
59
+ {
60
+ criteria: [{ filterType: 8, value: `${publisherId}.${extensionName}` }],
61
+ },
62
+ ],
61
63
  flags: 914,
62
64
  };
63
65
 
@@ -77,13 +79,23 @@ export async function getExtensionMetadata(publisherId, extensionName) {
77
79
  try {
78
80
  res = await fetch(url, {
79
81
  method: 'POST',
80
- headers: { 'Content-Type': 'application/json', 'Accept': 'application/json;api-version=3.0-preview.1' },
82
+ headers: {
83
+ 'Content-Type': 'application/json',
84
+ Accept: 'application/json;api-version=3.0-preview.1',
85
+ },
81
86
  body: JSON.stringify(body),
82
87
  });
83
88
  if (res.status === 429) {
84
89
  const retryAfter = parseInt(res.headers.get('Retry-After') || '10', 10);
85
90
  await sleep(retryAfter * 1000);
86
- res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json;api-version=3.0-preview.1' }, body: JSON.stringify(body) });
91
+ res = await fetch(url, {
92
+ method: 'POST',
93
+ headers: {
94
+ 'Content-Type': 'application/json',
95
+ Accept: 'application/json;api-version=3.0-preview.1',
96
+ },
97
+ body: JSON.stringify(body),
98
+ });
87
99
  }
88
100
  if (!res.ok) {
89
101
  console.debug(`Marketplace API warning: ${url} returned ${res.status}`);
@@ -100,12 +112,14 @@ export async function getExtensionMetadata(publisherId, extensionName) {
100
112
 
101
113
  export async function getVersionHistory(publisherId, extensionName) {
102
114
  const data = await getExtensionMetadata(publisherId, extensionName);
103
- if (!data?.results?.[0]?.extensions?.[0]) return [];
115
+ if (!data?.results?.[0]?.extensions?.[0]) {
116
+ return [];
117
+ }
104
118
 
105
119
  const extension = data.results[0].extensions[0];
106
120
  const versions = extension.versions || [];
107
121
 
108
- return versions.map(v => ({
122
+ return versions.map((v) => ({
109
123
  version: v.version,
110
124
  publishedAt: v.lastUpdated || v.publishedDate,
111
125
  publishedBy: extension.publisher?.publisherName || publisherId,
@@ -126,7 +140,9 @@ export async function getOpenVsxMetadata(namespace, name) {
126
140
 
127
141
  export async function getOpenVsxVersionHistory(namespace, name) {
128
142
  const data = await getOpenVsxMetadata(namespace, name);
129
- if (!data) return [];
143
+ if (!data) {
144
+ return [];
145
+ }
130
146
  const versions = data.allVersions || {};
131
147
  const files = data.files || {};
132
148