@lateos/npm-scan 0.16.0 → 0.16.5

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 (110) hide show
  1. package/.dockerignore +20 -20
  2. package/.husky/pre-commit +1 -1
  3. package/CHANGELOG.md +199 -199
  4. package/LICENSING.md +19 -19
  5. package/README.de.md +708 -669
  6. package/README.fr.md +707 -668
  7. package/README.ja.md +704 -665
  8. package/README.md +826 -801
  9. package/README.zh.md +708 -669
  10. package/SECURITY.md +72 -72
  11. package/backend/cra.js +68 -68
  12. package/backend/db/schema.sql +32 -32
  13. package/backend/db.js +88 -88
  14. package/backend/detectors/atk-001-lifecycle.js +17 -17
  15. package/backend/detectors/atk-002-obfusc.js +261 -261
  16. package/backend/detectors/atk-003-creds.js +13 -13
  17. package/backend/detectors/atk-004-persist.js +13 -13
  18. package/backend/detectors/atk-005-exfil.js +13 -13
  19. package/backend/detectors/atk-006-depconf.js +14 -14
  20. package/backend/detectors/atk-007-typosquat.js +34 -34
  21. package/backend/detectors/atk-008-tarball-tamper.js +91 -91
  22. package/backend/detectors/atk-009-dormant-trigger.js +62 -62
  23. package/backend/detectors/atk-010-sandbox-evasion.js +50 -50
  24. package/backend/detectors/atk-011-transitive-prop.js +76 -76
  25. package/backend/detectors/axios-poisoning/d1-version-fingerprint.js +24 -0
  26. package/backend/detectors/axios-poisoning/d2-decoy-dep.js +24 -0
  27. package/backend/detectors/axios-poisoning/d3-postinstall-rat.js +90 -0
  28. package/backend/detectors/axios-poisoning/index.js +94 -0
  29. package/backend/detectors/cve-2026-48710-badhost/codePattern.js +99 -99
  30. package/backend/detectors/cve-2026-48710-badhost/findings.js +105 -105
  31. package/backend/detectors/cve-2026-48710-badhost/index.js +15 -15
  32. package/backend/detectors/cve-2026-48710-badhost/manifest.js +305 -305
  33. package/backend/detectors/cve-2026-48710-badhost/transitive.js +189 -189
  34. package/backend/detectors/hf-impersonation/index.js +396 -396
  35. package/backend/detectors/hf-impersonation/jaro-winkler.js +44 -44
  36. package/backend/detectors/hf-impersonation/known-orgs.js +5 -5
  37. package/backend/detectors/hf-impersonation/simhash.js +46 -46
  38. package/backend/detectors/index.js +75 -38
  39. package/backend/detectors/megalodon/d1-workflow-scan.js +147 -147
  40. package/backend/detectors/megalodon/d2-credential-harvest.js +61 -61
  41. package/backend/detectors/megalodon/d3-publish-velocity.js +67 -67
  42. package/backend/detectors/megalodon/d4-publisher-drift.js +124 -124
  43. package/backend/detectors/megalodon/d5-bot-commit-identity.js +3 -3
  44. package/backend/detectors/megalodon/d6-date-anachronism.js +3 -3
  45. package/backend/detectors/megalodon/index.js +80 -80
  46. package/backend/detectors/megalodon/types.js +9 -9
  47. package/backend/detectors/mini-shai-hulud/d1-burst-publish.js +42 -42
  48. package/backend/detectors/mini-shai-hulud/d2-sibling-compromise.js +116 -116
  49. package/backend/detectors/mini-shai-hulud/d3-slsa-mismatch.js +72 -72
  50. package/backend/detectors/mini-shai-hulud/d4-maintainer-anomaly.js +45 -45
  51. package/backend/detectors/mini-shai-hulud/d5-ioc-check.js +95 -95
  52. package/backend/detectors/mini-shai-hulud/d6-token-exfil.js +38 -38
  53. package/backend/detectors/mini-shai-hulud/index.js +118 -118
  54. package/backend/detectors/mini-shai-hulud/iocs.json +79 -79
  55. package/backend/detectors/msh-supplement/d1-obfuscation.js +18 -0
  56. package/backend/detectors/msh-supplement/d2-persistence.js +47 -0
  57. package/backend/detectors/msh-supplement/d3-geo-killswitch.js +35 -0
  58. package/backend/detectors/msh-supplement/d4-c2-deaddrop.js +33 -0
  59. package/backend/detectors/msh-supplement/index.js +107 -0
  60. package/backend/detectors/tier1-binary-embed.js +219 -0
  61. package/backend/detectors/tier1-infostealer.js +280 -0
  62. package/backend/detectors/tier1-lifecycle-hook.js +176 -0
  63. package/backend/detectors/tier1-metadata-spoof.js +180 -0
  64. package/backend/detectors/tier1-typosquat.js +219 -0
  65. package/backend/detectors/typosquat-vpmdhaj/d1-maintainer.js +77 -0
  66. package/backend/detectors/typosquat-vpmdhaj/d2-preinstall-loader.js +37 -0
  67. package/backend/detectors/typosquat-vpmdhaj/d3-cred-exfil.js +66 -0
  68. package/backend/detectors/typosquat-vpmdhaj/index.js +98 -0
  69. package/backend/fetch.js +175 -175
  70. package/backend/index.js +4 -4
  71. package/backend/license.js +89 -89
  72. package/backend/lockfile.js +379 -379
  73. package/backend/pdf.js +245 -245
  74. package/backend/policy.js +193 -176
  75. package/backend/provenance.js +79 -0
  76. package/backend/report.js +254 -254
  77. package/backend/sbom.js +66 -66
  78. package/backend/siem/cef.js +32 -32
  79. package/backend/siem/ecs.js +40 -40
  80. package/backend/siem/index.js +18 -18
  81. package/backend/siem/qradar.js +56 -56
  82. package/backend/siem/sentinel.js +27 -27
  83. package/backend/vsix-scan/detectors/activation-event-risk.js +116 -116
  84. package/backend/vsix-scan/detectors/burst-publish.js +52 -52
  85. package/backend/vsix-scan/detectors/exfil-pattern.js +88 -88
  86. package/backend/vsix-scan/detectors/known-ioc.js +105 -105
  87. package/backend/vsix-scan/detectors/orphan-commit-fetch.js +69 -69
  88. package/backend/vsix-scan/detectors/publisher-anomaly.js +70 -70
  89. package/backend/vsix-scan/index.js +183 -183
  90. package/backend/vsix-scan/marketplace-client.js +145 -145
  91. package/backend/vsix-scan/vsix-iocs.json +31 -31
  92. package/cli/cli.js +458 -458
  93. package/deploy/helm/npm-scan/Chart.yaml +21 -21
  94. package/deploy/helm/npm-scan/templates/_helpers.tpl +8 -8
  95. package/deploy/helm/npm-scan/templates/api.yaml +93 -93
  96. package/deploy/helm/npm-scan/templates/ingress.yaml +27 -27
  97. package/deploy/helm/npm-scan/templates/postgresql.yaml +66 -66
  98. package/deploy/helm/npm-scan/templates/secrets.yaml +18 -18
  99. package/deploy/helm/npm-scan/templates/worker.yaml +31 -31
  100. package/deploy/helm/npm-scan/values.byoc.yaml +74 -74
  101. package/deploy/helm/npm-scan/values.yaml +102 -102
  102. package/package.json +57 -57
  103. package/scripts/download-corpus.js +30 -30
  104. package/scripts/gen-mal-corpus.js +34 -34
  105. package/scripts/generate-campaign-fixtures.js +170 -0
  106. package/src/config/top-5000.json +87 -0
  107. package/test/fixtures/lockfiles/npm-lock.json +68 -68
  108. package/test/fixtures/lockfiles/pnpm-lock.yaml +117 -117
  109. package/test/fixtures/lockfiles/yarn.lock +103 -103
  110. package/test/fixtures/mock-data.js +69 -69
@@ -1,70 +1,70 @@
1
- export async function checkPublisherAnomaly(extensionMetadata, publisherProfile, versionHistory, config = {}) {
2
- const signals = [];
3
-
4
- const crossNamespaceThreshold = config.crossNamespaceThreshold ?? 3;
5
- const crossNamespaceDays = config.crossNamespaceDays ?? 14;
6
- const newAccountAgeDays = config.newAccountAgeDays ?? 30;
7
- const highInstallThreshold = config.highInstallThreshold ?? 100000;
8
- const addPublishWindowMinutes = config.addPublishWindowMinutes ?? 15;
9
-
10
- const versions = versionHistory || [];
11
- if (versions.length === 0) return { triggered: false, signals: [] };
12
-
13
- const publishers = [...new Set(versions.map(v => v.publishedBy).filter(Boolean))];
14
- if (publishers.length === 0) return { triggered: false, signals: [] };
15
-
16
- const sortedVersions = [...versions]
17
- .filter(v => v.publishedAt)
18
- .sort((a, b) => new Date(a.publishedAt) - new Date(b.publishedAt));
19
-
20
- const extPublisher = publishers[0];
21
- const allSame = publishers.every(p => p === extPublisher);
22
-
23
- if (!allSame) {
24
- for (const pub of publishers) {
25
- if (pub !== extPublisher) {
26
- signals.push({
27
- type: 'PUBLISHER_ACCOUNT_SUBSTITUTION',
28
- expectedPublisher: extPublisher,
29
- unexpectedPublisher: pub,
30
- });
31
- }
32
- }
33
- }
34
-
35
- const extInstallCount = extensionMetadata?.statistics?.find(s => s.statisticName === 'install')?.value || 0;
36
-
37
- const extAgeDays = publisherProfile?.dateCreated
38
- ? (Date.now() - new Date(publisherProfile.dateCreated).getTime()) / (1000 * 60 * 60 * 24)
39
- : null;
40
-
41
- if (extAgeDays !== null && extAgeDays < newAccountAgeDays && extInstallCount >= highInstallThreshold) {
42
- signals.push({
43
- type: 'NEW_ACCOUNT_HIGH_INSTALL',
44
- accountAgeDays: Math.round(extAgeDays),
45
- installCount: extInstallCount,
46
- });
47
- }
48
-
49
- if (sortedVersions.length >= 2) {
50
- const sorted = sortedVersions;
51
- for (let i = 1; i < sorted.length; i++) {
52
- const prev = sorted[i - 1];
53
- const curr = sorted[i];
54
- if (curr.publishedBy !== prev.publishedBy) {
55
- const gapMinutes = (new Date(curr.publishedAt) - new Date(prev.publishedAt)) / (1000 * 60);
56
- if (gapMinutes <= addPublishWindowMinutes) {
57
- signals.push({
58
- type: 'ADD_PUBLISH_RAPID',
59
- version: curr.version,
60
- previousPublisher: prev.publishedBy,
61
- newPublisher: curr.publishedBy,
62
- gapMinutes: Math.round(gapMinutes * 100) / 100,
63
- });
64
- }
65
- }
66
- }
67
- }
68
-
69
- return { triggered: signals.length > 0, signals };
70
- }
1
+ export async function checkPublisherAnomaly(extensionMetadata, publisherProfile, versionHistory, config = {}) {
2
+ const signals = [];
3
+
4
+ const crossNamespaceThreshold = config.crossNamespaceThreshold ?? 3;
5
+ const crossNamespaceDays = config.crossNamespaceDays ?? 14;
6
+ const newAccountAgeDays = config.newAccountAgeDays ?? 30;
7
+ const highInstallThreshold = config.highInstallThreshold ?? 100000;
8
+ const addPublishWindowMinutes = config.addPublishWindowMinutes ?? 15;
9
+
10
+ const versions = versionHistory || [];
11
+ if (versions.length === 0) return { triggered: false, signals: [] };
12
+
13
+ const publishers = [...new Set(versions.map(v => v.publishedBy).filter(Boolean))];
14
+ if (publishers.length === 0) return { triggered: false, signals: [] };
15
+
16
+ const sortedVersions = [...versions]
17
+ .filter(v => v.publishedAt)
18
+ .sort((a, b) => new Date(a.publishedAt) - new Date(b.publishedAt));
19
+
20
+ const extPublisher = publishers[0];
21
+ const allSame = publishers.every(p => p === extPublisher);
22
+
23
+ if (!allSame) {
24
+ for (const pub of publishers) {
25
+ if (pub !== extPublisher) {
26
+ signals.push({
27
+ type: 'PUBLISHER_ACCOUNT_SUBSTITUTION',
28
+ expectedPublisher: extPublisher,
29
+ unexpectedPublisher: pub,
30
+ });
31
+ }
32
+ }
33
+ }
34
+
35
+ const extInstallCount = extensionMetadata?.statistics?.find(s => s.statisticName === 'install')?.value || 0;
36
+
37
+ const extAgeDays = publisherProfile?.dateCreated
38
+ ? (Date.now() - new Date(publisherProfile.dateCreated).getTime()) / (1000 * 60 * 60 * 24)
39
+ : null;
40
+
41
+ if (extAgeDays !== null && extAgeDays < newAccountAgeDays && extInstallCount >= highInstallThreshold) {
42
+ signals.push({
43
+ type: 'NEW_ACCOUNT_HIGH_INSTALL',
44
+ accountAgeDays: Math.round(extAgeDays),
45
+ installCount: extInstallCount,
46
+ });
47
+ }
48
+
49
+ if (sortedVersions.length >= 2) {
50
+ const sorted = sortedVersions;
51
+ for (let i = 1; i < sorted.length; i++) {
52
+ const prev = sorted[i - 1];
53
+ const curr = sorted[i];
54
+ if (curr.publishedBy !== prev.publishedBy) {
55
+ const gapMinutes = (new Date(curr.publishedAt) - new Date(prev.publishedAt)) / (1000 * 60);
56
+ if (gapMinutes <= addPublishWindowMinutes) {
57
+ signals.push({
58
+ type: 'ADD_PUBLISH_RAPID',
59
+ version: curr.version,
60
+ previousPublisher: prev.publishedBy,
61
+ newPublisher: curr.publishedBy,
62
+ gapMinutes: Math.round(gapMinutes * 100) / 100,
63
+ });
64
+ }
65
+ }
66
+ }
67
+ }
68
+
69
+ return { triggered: signals.length > 0, signals };
70
+ }
@@ -1,183 +1,183 @@
1
- import { checkBurstPublish } from './detectors/burst-publish.js';
2
- import { checkPublisherAnomaly } from './detectors/publisher-anomaly.js';
3
- import { checkActivationEventRisk } from './detectors/activation-event-risk.js';
4
- import { checkOrphanCommitFetch } from './detectors/orphan-commit-fetch.js';
5
- import { checkKnownIOC } from './detectors/known-ioc.js';
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 };
10
- const SEVERITY_LABELS = ['none', 'low', 'medium', 'high', 'critical'];
11
-
12
- export async function vsixScan(extensionId, options = {}) {
13
- const { publisherId, extensionName } = parseExtensionId(extensionId);
14
-
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));
19
-
20
- const allVersions = mergeVersionHistories(marketplaceVersions, openVsxVersions);
21
- const manifest = options.manifest || extractManifest(marketplaceMeta, extensionId);
22
-
23
- const config = options.config || {};
24
-
25
- const activationResult = await checkActivationEventRisk(
26
- manifest,
27
- allVersions,
28
- options.priorVersions || [],
29
- );
30
-
31
- const burstResult = await checkBurstPublish(allVersions, config);
32
-
33
- const publisherResult = await checkPublisherAnomaly(
34
- manifest || {},
35
- publisherProfile || {},
36
- allVersions,
37
- config,
38
- );
39
-
40
- const orphanResult = await checkOrphanCommitFetch(options.extensionFiles || []);
41
-
42
- const iocResult = await checkKnownIOC(
43
- extensionId,
44
- options.version || (allVersions.length > 0 ? allVersions[allVersions.length - 1].version : 'unknown'),
45
- publisherId,
46
- orphanResult.signals
47
- .filter(s => s.type === 'ORPHAN_COMMIT_GITHUB_API')
48
- .map(s => s.indicator),
49
- allVersions,
50
- );
51
-
52
- const exfilResult = await checkExfilPattern(options.extensionFiles || []);
53
-
54
- 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');
61
-
62
- if (triggeredSignals.length === 0) return [];
63
-
64
- const registryLabels = [];
65
- if (marketplaceVersions.length > 0) registryLabels.push('marketplace');
66
- if (openVsxVersions.length > 0) registryLabels.push('open-vsx');
67
-
68
- 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);
72
- return max;
73
- }, 0);
74
-
75
- const finalSeverity = SEVERITY_LABELS[maxSeverity] || 'high';
76
-
77
- const latestVersion = allVersions.length > 0 ? allVersions[allVersions.length - 1].version : 'unknown';
78
- let exposureWindowMinutes = null;
79
- if (burstResult.hotPullDetected && allVersions.length >= 2) {
80
- const sorted = [...allVersions].sort((a, b) => new Date(b.publishedAt) - new Date(a.publishedAt));
81
- const gap = (new Date(sorted[0].publishedAt) - new Date(sorted[1].publishedAt)) / (1000 * 60);
82
- exposureWindowMinutes = Math.round(gap);
83
- }
84
-
85
- const evidence = {
86
- extensionId,
87
- maliciousVersion: latestVersion,
88
- registries: registryLabels,
89
- exposureWindowMinutes,
90
- triggeredSignals,
91
- burstWindow: burstResult.burstWindow,
92
- hotPullDetected: burstResult.hotPullDetected,
93
- publisherSignals: publisherResult.triggered ? publisherResult.signals : null,
94
- activationEvents: manifest?.activationEvents || null,
95
- activationRisk: activationResult.triggered ? { riskLevel: activationResult.riskLevel, why: activationResult.why } : null,
96
- orphanCommitIndicators: orphanResult.triggered ? orphanResult.indicators : null,
97
- iocMatches: iocResult.triggered ? iocResult.matches : null,
98
- exfilPatterns: exfilResult.triggered ? exfilResult.exfilPatterns : null,
99
- antiAnalysisTechniques: exfilResult.triggered ? exfilResult.antiAnalysisTechniques : null,
100
- };
101
-
102
- const remediationGuidance = buildRemediation(triggeredSignals, extensionId);
103
-
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
- }];
112
- }
113
-
114
- function parseExtensionId(id) {
115
- const idx = id.indexOf('.');
116
- if (idx === -1 || idx === 0 || idx === id.length - 1) {
117
- throw new Error(`Invalid extension ID: ${id}. Expected format: publisher.extension-name`);
118
- }
119
- return { publisherId: id.slice(0, idx), extensionName: id.slice(idx + 1) };
120
- }
121
-
122
- function mergeVersionHistories(marketplace, openVsx) {
123
- const seen = new Set();
124
- const merged = [];
125
-
126
- for (const v of marketplace) {
127
- if (!seen.has(v.version)) {
128
- seen.add(v.version);
129
- merged.push({ ...v, registries: ['marketplace'] });
130
- }
131
- }
132
-
133
- for (const v of openVsx) {
134
- if (!seen.has(v.version)) {
135
- seen.add(v.version);
136
- merged.push({ ...v, registries: ['open-vsx'] });
137
- } else {
138
- const existing = merged.find(m => m.version === v.version);
139
- if (existing) {
140
- existing.registries.push('open-vsx');
141
- }
142
- }
143
- }
144
-
145
- return merged.sort((a, b) => new Date(a.publishedAt) - new Date(b.publishedAt));
146
- }
147
-
148
- function extractManifest(marketplaceMeta, extensionId) {
149
- if (!marketplaceMeta?.results?.[0]?.extensions?.[0]) return {};
150
- const ext = marketplaceMeta.results[0].extensions[0];
151
- const manifestStr = ext.galleryApiUrl || ext.manifest;
152
- if (!manifestStr) return {};
153
-
154
- try {
155
- if (typeof manifestStr === 'object') return manifestStr;
156
- return JSON.parse(manifestStr);
157
- } catch {
158
- return {};
159
- }
160
- }
161
-
162
- function buildRemediation(triggeredSignals, extensionId) {
163
- const parts = [];
164
- if (triggeredSignals.includes('VSIX_KNOWN_IOC')) {
165
- parts.push(`Extension ${extensionId} matches known campaign IOC. Remove immediately.`);
166
- }
167
- if (triggeredSignals.includes('VSIX_BURST_PUBLISH')) {
168
- parts.push('Suspicious publish velocity detected. Verify publisher release history.');
169
- }
170
- if (triggeredSignals.includes('VSIX_PUBLISHER_ANOMALY')) {
171
- parts.push('Publisher account anomaly detected. Verify publisher identity.');
172
- }
173
- if (triggeredSignals.includes('VSIX_ACTIVATION_EVENT_RISK')) {
174
- parts.push('Risky activation events detected. Review extension activation scope.');
175
- }
176
- if (triggeredSignals.includes('VSIX_ORPHAN_COMMIT_FETCH')) {
177
- parts.push('Dangling orphan commit fetch detected — technical signature of Nx Console attack.');
178
- }
179
- if (triggeredSignals.includes('VSIX_EXFIL_PATTERN')) {
180
- parts.push('Credential exfiltration patterns detected. Revoke all tokens.');
181
- }
182
- return parts.join(' ');
183
- }
1
+ import { checkBurstPublish } from './detectors/burst-publish.js';
2
+ import { checkPublisherAnomaly } from './detectors/publisher-anomaly.js';
3
+ import { checkActivationEventRisk } from './detectors/activation-event-risk.js';
4
+ import { checkOrphanCommitFetch } from './detectors/orphan-commit-fetch.js';
5
+ import { checkKnownIOC } from './detectors/known-ioc.js';
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 };
10
+ const SEVERITY_LABELS = ['none', 'low', 'medium', 'high', 'critical'];
11
+
12
+ export async function vsixScan(extensionId, options = {}) {
13
+ const { publisherId, extensionName } = parseExtensionId(extensionId);
14
+
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));
19
+
20
+ const allVersions = mergeVersionHistories(marketplaceVersions, openVsxVersions);
21
+ const manifest = options.manifest || extractManifest(marketplaceMeta, extensionId);
22
+
23
+ const config = options.config || {};
24
+
25
+ const activationResult = await checkActivationEventRisk(
26
+ manifest,
27
+ allVersions,
28
+ options.priorVersions || [],
29
+ );
30
+
31
+ const burstResult = await checkBurstPublish(allVersions, config);
32
+
33
+ const publisherResult = await checkPublisherAnomaly(
34
+ manifest || {},
35
+ publisherProfile || {},
36
+ allVersions,
37
+ config,
38
+ );
39
+
40
+ const orphanResult = await checkOrphanCommitFetch(options.extensionFiles || []);
41
+
42
+ const iocResult = await checkKnownIOC(
43
+ extensionId,
44
+ options.version || (allVersions.length > 0 ? allVersions[allVersions.length - 1].version : 'unknown'),
45
+ publisherId,
46
+ orphanResult.signals
47
+ .filter(s => s.type === 'ORPHAN_COMMIT_GITHUB_API')
48
+ .map(s => s.indicator),
49
+ allVersions,
50
+ );
51
+
52
+ const exfilResult = await checkExfilPattern(options.extensionFiles || []);
53
+
54
+ 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');
61
+
62
+ if (triggeredSignals.length === 0) return [];
63
+
64
+ const registryLabels = [];
65
+ if (marketplaceVersions.length > 0) registryLabels.push('marketplace');
66
+ if (openVsxVersions.length > 0) registryLabels.push('open-vsx');
67
+
68
+ 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);
72
+ return max;
73
+ }, 0);
74
+
75
+ const finalSeverity = SEVERITY_LABELS[maxSeverity] || 'high';
76
+
77
+ const latestVersion = allVersions.length > 0 ? allVersions[allVersions.length - 1].version : 'unknown';
78
+ let exposureWindowMinutes = null;
79
+ if (burstResult.hotPullDetected && allVersions.length >= 2) {
80
+ const sorted = [...allVersions].sort((a, b) => new Date(b.publishedAt) - new Date(a.publishedAt));
81
+ const gap = (new Date(sorted[0].publishedAt) - new Date(sorted[1].publishedAt)) / (1000 * 60);
82
+ exposureWindowMinutes = Math.round(gap);
83
+ }
84
+
85
+ const evidence = {
86
+ extensionId,
87
+ maliciousVersion: latestVersion,
88
+ registries: registryLabels,
89
+ exposureWindowMinutes,
90
+ triggeredSignals,
91
+ burstWindow: burstResult.burstWindow,
92
+ hotPullDetected: burstResult.hotPullDetected,
93
+ publisherSignals: publisherResult.triggered ? publisherResult.signals : null,
94
+ activationEvents: manifest?.activationEvents || null,
95
+ activationRisk: activationResult.triggered ? { riskLevel: activationResult.riskLevel, why: activationResult.why } : null,
96
+ orphanCommitIndicators: orphanResult.triggered ? orphanResult.indicators : null,
97
+ iocMatches: iocResult.triggered ? iocResult.matches : null,
98
+ exfilPatterns: exfilResult.triggered ? exfilResult.exfilPatterns : null,
99
+ antiAnalysisTechniques: exfilResult.triggered ? exfilResult.antiAnalysisTechniques : null,
100
+ };
101
+
102
+ const remediationGuidance = buildRemediation(triggeredSignals, extensionId);
103
+
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
+ }];
112
+ }
113
+
114
+ function parseExtensionId(id) {
115
+ const idx = id.indexOf('.');
116
+ if (idx === -1 || idx === 0 || idx === id.length - 1) {
117
+ throw new Error(`Invalid extension ID: ${id}. Expected format: publisher.extension-name`);
118
+ }
119
+ return { publisherId: id.slice(0, idx), extensionName: id.slice(idx + 1) };
120
+ }
121
+
122
+ function mergeVersionHistories(marketplace, openVsx) {
123
+ const seen = new Set();
124
+ const merged = [];
125
+
126
+ for (const v of marketplace) {
127
+ if (!seen.has(v.version)) {
128
+ seen.add(v.version);
129
+ merged.push({ ...v, registries: ['marketplace'] });
130
+ }
131
+ }
132
+
133
+ for (const v of openVsx) {
134
+ if (!seen.has(v.version)) {
135
+ seen.add(v.version);
136
+ merged.push({ ...v, registries: ['open-vsx'] });
137
+ } else {
138
+ const existing = merged.find(m => m.version === v.version);
139
+ if (existing) {
140
+ existing.registries.push('open-vsx');
141
+ }
142
+ }
143
+ }
144
+
145
+ return merged.sort((a, b) => new Date(a.publishedAt) - new Date(b.publishedAt));
146
+ }
147
+
148
+ function extractManifest(marketplaceMeta, extensionId) {
149
+ if (!marketplaceMeta?.results?.[0]?.extensions?.[0]) return {};
150
+ const ext = marketplaceMeta.results[0].extensions[0];
151
+ const manifestStr = ext.galleryApiUrl || ext.manifest;
152
+ if (!manifestStr) return {};
153
+
154
+ try {
155
+ if (typeof manifestStr === 'object') return manifestStr;
156
+ return JSON.parse(manifestStr);
157
+ } catch {
158
+ return {};
159
+ }
160
+ }
161
+
162
+ function buildRemediation(triggeredSignals, extensionId) {
163
+ const parts = [];
164
+ if (triggeredSignals.includes('VSIX_KNOWN_IOC')) {
165
+ parts.push(`Extension ${extensionId} matches known campaign IOC. Remove immediately.`);
166
+ }
167
+ if (triggeredSignals.includes('VSIX_BURST_PUBLISH')) {
168
+ parts.push('Suspicious publish velocity detected. Verify publisher release history.');
169
+ }
170
+ if (triggeredSignals.includes('VSIX_PUBLISHER_ANOMALY')) {
171
+ parts.push('Publisher account anomaly detected. Verify publisher identity.');
172
+ }
173
+ if (triggeredSignals.includes('VSIX_ACTIVATION_EVENT_RISK')) {
174
+ parts.push('Risky activation events detected. Review extension activation scope.');
175
+ }
176
+ if (triggeredSignals.includes('VSIX_ORPHAN_COMMIT_FETCH')) {
177
+ parts.push('Dangling orphan commit fetch detected — technical signature of Nx Console attack.');
178
+ }
179
+ if (triggeredSignals.includes('VSIX_EXFIL_PATTERN')) {
180
+ parts.push('Credential exfiltration patterns detected. Revoke all tokens.');
181
+ }
182
+ return parts.join(' ');
183
+ }