@lateos/npm-scan 0.16.4 → 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 (96) 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 -708
  6. package/README.fr.md +707 -707
  7. package/README.ja.md +704 -704
  8. package/README.md +826 -826
  9. package/README.zh.md +708 -708
  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/cve-2026-48710-badhost/codePattern.js +99 -99
  26. package/backend/detectors/cve-2026-48710-badhost/findings.js +105 -105
  27. package/backend/detectors/cve-2026-48710-badhost/index.js +15 -15
  28. package/backend/detectors/cve-2026-48710-badhost/manifest.js +305 -305
  29. package/backend/detectors/cve-2026-48710-badhost/transitive.js +189 -189
  30. package/backend/detectors/hf-impersonation/index.js +396 -396
  31. package/backend/detectors/hf-impersonation/jaro-winkler.js +44 -44
  32. package/backend/detectors/hf-impersonation/known-orgs.js +5 -5
  33. package/backend/detectors/hf-impersonation/simhash.js +46 -46
  34. package/backend/detectors/index.js +75 -44
  35. package/backend/detectors/megalodon/d1-workflow-scan.js +147 -147
  36. package/backend/detectors/megalodon/d2-credential-harvest.js +61 -61
  37. package/backend/detectors/megalodon/d3-publish-velocity.js +67 -67
  38. package/backend/detectors/megalodon/d4-publisher-drift.js +124 -124
  39. package/backend/detectors/megalodon/d5-bot-commit-identity.js +3 -3
  40. package/backend/detectors/megalodon/d6-date-anachronism.js +3 -3
  41. package/backend/detectors/megalodon/index.js +80 -80
  42. package/backend/detectors/megalodon/types.js +9 -9
  43. package/backend/detectors/mini-shai-hulud/d1-burst-publish.js +42 -42
  44. package/backend/detectors/mini-shai-hulud/d2-sibling-compromise.js +116 -116
  45. package/backend/detectors/mini-shai-hulud/d3-slsa-mismatch.js +72 -72
  46. package/backend/detectors/mini-shai-hulud/d4-maintainer-anomaly.js +45 -45
  47. package/backend/detectors/mini-shai-hulud/d5-ioc-check.js +95 -95
  48. package/backend/detectors/mini-shai-hulud/d6-token-exfil.js +38 -38
  49. package/backend/detectors/mini-shai-hulud/index.js +118 -118
  50. package/backend/detectors/mini-shai-hulud/iocs.json +79 -79
  51. package/backend/detectors/tier1-binary-embed.js +219 -0
  52. package/backend/detectors/tier1-infostealer.js +280 -0
  53. package/backend/detectors/tier1-lifecycle-hook.js +176 -0
  54. package/backend/detectors/tier1-metadata-spoof.js +180 -0
  55. package/backend/detectors/tier1-typosquat.js +219 -0
  56. package/backend/fetch.js +175 -175
  57. package/backend/index.js +4 -4
  58. package/backend/license.js +89 -89
  59. package/backend/lockfile.js +379 -379
  60. package/backend/pdf.js +245 -245
  61. package/backend/policy.js +193 -176
  62. package/backend/report.js +254 -254
  63. package/backend/sbom.js +66 -66
  64. package/backend/siem/cef.js +32 -32
  65. package/backend/siem/ecs.js +40 -40
  66. package/backend/siem/index.js +18 -18
  67. package/backend/siem/qradar.js +56 -56
  68. package/backend/siem/sentinel.js +27 -27
  69. package/backend/vsix-scan/detectors/activation-event-risk.js +116 -116
  70. package/backend/vsix-scan/detectors/burst-publish.js +52 -52
  71. package/backend/vsix-scan/detectors/exfil-pattern.js +88 -88
  72. package/backend/vsix-scan/detectors/known-ioc.js +105 -105
  73. package/backend/vsix-scan/detectors/orphan-commit-fetch.js +69 -69
  74. package/backend/vsix-scan/detectors/publisher-anomaly.js +70 -70
  75. package/backend/vsix-scan/index.js +183 -183
  76. package/backend/vsix-scan/marketplace-client.js +145 -145
  77. package/backend/vsix-scan/vsix-iocs.json +31 -31
  78. package/cli/cli.js +458 -458
  79. package/deploy/helm/npm-scan/Chart.yaml +21 -21
  80. package/deploy/helm/npm-scan/templates/_helpers.tpl +8 -8
  81. package/deploy/helm/npm-scan/templates/api.yaml +93 -93
  82. package/deploy/helm/npm-scan/templates/ingress.yaml +27 -27
  83. package/deploy/helm/npm-scan/templates/postgresql.yaml +66 -66
  84. package/deploy/helm/npm-scan/templates/secrets.yaml +18 -18
  85. package/deploy/helm/npm-scan/templates/worker.yaml +31 -31
  86. package/deploy/helm/npm-scan/values.byoc.yaml +74 -74
  87. package/deploy/helm/npm-scan/values.yaml +102 -102
  88. package/package.json +57 -57
  89. package/scripts/download-corpus.js +30 -30
  90. package/scripts/gen-mal-corpus.js +34 -34
  91. package/scripts/generate-campaign-fixtures.js +170 -0
  92. package/src/config/top-5000.json +87 -0
  93. package/test/fixtures/lockfiles/npm-lock.json +68 -68
  94. package/test/fixtures/lockfiles/pnpm-lock.yaml +117 -117
  95. package/test/fixtures/lockfiles/yarn.lock +103 -103
  96. 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
+ }