@lateos/npm-scan 0.18.2 → 1.0.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.
- package/CHANGELOG.md +265 -233
- package/LICENSING.md +19 -19
- package/README.de.md +708 -708
- package/README.fr.md +707 -707
- package/README.ja.md +704 -704
- package/README.md +861 -826
- package/README.zh.md +708 -708
- package/VALIDATION.md +92 -0
- package/backend/cra.js +68 -68
- package/backend/db/pg-schema.sql +155 -0
- package/backend/db/schema.sql +32 -32
- package/backend/db.js +88 -88
- package/backend/detectors/atk-001-lifecycle.js +17 -17
- package/backend/detectors/atk-002-obfusc.js +261 -261
- package/backend/detectors/atk-003-creds.js +13 -13
- package/backend/detectors/atk-004-persist.js +13 -13
- package/backend/detectors/atk-005-exfil.js +13 -13
- package/backend/detectors/atk-006-depconf.js +14 -14
- package/backend/detectors/atk-007-typosquat.js +34 -34
- package/backend/detectors/atk-008-tarball-tamper.js +91 -91
- package/backend/detectors/atk-009-dormant-trigger.js +62 -62
- package/backend/detectors/atk-010-sandbox-evasion.js +50 -50
- package/backend/detectors/atk-011-transitive-prop.js +76 -76
- package/backend/detectors/config/thresholds.js +66 -0
- package/backend/detectors/config/whitelist.json +74 -0
- package/backend/detectors/cve-2026-48710-badhost/codePattern.js +99 -99
- package/backend/detectors/cve-2026-48710-badhost/findings.js +105 -105
- package/backend/detectors/cve-2026-48710-badhost/index.js +15 -15
- package/backend/detectors/cve-2026-48710-badhost/manifest.js +305 -305
- package/backend/detectors/cve-2026-48710-badhost/transitive.js +189 -189
- package/backend/detectors/hf-impersonation/index.js +396 -396
- package/backend/detectors/hf-impersonation/jaro-winkler.js +44 -44
- package/backend/detectors/hf-impersonation/known-orgs.js +5 -5
- package/backend/detectors/hf-impersonation/simhash.js +46 -46
- package/backend/detectors/index.js +87 -81
- package/backend/detectors/lib/ast-patterns.js +21 -0
- package/backend/detectors/lib/entropy-analyzer.js +24 -0
- package/backend/detectors/megalodon/d1-workflow-scan.js +147 -147
- package/backend/detectors/megalodon/d2-credential-harvest.js +61 -61
- package/backend/detectors/megalodon/d3-publish-velocity.js +67 -67
- package/backend/detectors/megalodon/d4-publisher-drift.js +124 -124
- package/backend/detectors/megalodon/d5-bot-commit-identity.js +3 -3
- package/backend/detectors/megalodon/d6-date-anachronism.js +3 -3
- package/backend/detectors/megalodon/index.js +80 -80
- package/backend/detectors/megalodon/types.js +9 -9
- package/backend/detectors/mini-shai-hulud/d1-burst-publish.js +42 -42
- package/backend/detectors/mini-shai-hulud/d2-sibling-compromise.js +116 -116
- package/backend/detectors/mini-shai-hulud/d3-slsa-mismatch.js +72 -72
- package/backend/detectors/mini-shai-hulud/d4-maintainer-anomaly.js +45 -45
- package/backend/detectors/mini-shai-hulud/d5-ioc-check.js +95 -95
- package/backend/detectors/mini-shai-hulud/d6-token-exfil.js +38 -38
- package/backend/detectors/mini-shai-hulud/index.js +118 -118
- package/backend/detectors/mini-shai-hulud/iocs.json +79 -79
- package/backend/detectors/tier1-binary-embed.js +34 -5
- package/backend/detectors/tier1-obfuscation-heuristics.js +156 -0
- package/backend/detectors/tier1-slsa-attestation.js +12 -0
- package/backend/detectors/tier1-version-anomaly.js +187 -0
- package/backend/detectors.test.js +88 -0
- package/backend/fetch.js +175 -175
- package/backend/index.js +4 -4
- package/backend/license.js +89 -89
- package/backend/lockfile.js +379 -379
- package/backend/pdf.js +245 -245
- package/backend/policy.js +193 -193
- package/backend/report.js +254 -254
- package/backend/sbom.js +66 -66
- package/backend/scripts/analyze-false-positives.js +146 -0
- package/backend/scripts/analyze-validation.js +151 -0
- package/backend/scripts/detect-false-positives.js +93 -0
- package/backend/scripts/fetch-top-packages.js +129 -0
- package/backend/scripts/validate-detectors.js +142 -0
- package/backend/siem/cef.js +32 -32
- package/backend/siem/ecs.js +40 -40
- package/backend/siem/index.js +18 -18
- package/backend/siem/qradar.js +56 -56
- package/backend/siem/sentinel.js +27 -27
- package/backend/tests-d5-enhanced.test.js +46 -0
- package/backend/tests-d6-version-anomaly.test.js +58 -0
- package/backend/tests-d6.test.js +116 -0
- package/backend/tests-d6c.test.js +106 -0
- package/backend/tests-d7-obfuscation.test.js +91 -0
- package/backend/tests.test.js +898 -0
- package/backend/vsix-scan/detectors/activation-event-risk.js +116 -116
- package/backend/vsix-scan/detectors/burst-publish.js +52 -52
- package/backend/vsix-scan/detectors/exfil-pattern.js +88 -88
- package/backend/vsix-scan/detectors/known-ioc.js +105 -105
- package/backend/vsix-scan/detectors/orphan-commit-fetch.js +69 -69
- package/backend/vsix-scan/detectors/publisher-anomaly.js +70 -70
- package/backend/vsix-scan/index.js +183 -183
- package/backend/vsix-scan/marketplace-client.js +145 -145
- package/backend/vsix-scan/vsix-iocs.json +31 -31
- package/cli/cli.js +458 -458
- package/package.json +74 -57
- package/.dockerignore +0 -20
- package/.husky/pre-commit +0 -1
- package/SECURITY.md +0 -73
- package/deploy/helm/npm-scan/Chart.yaml +0 -22
- package/deploy/helm/npm-scan/templates/_helpers.tpl +0 -9
- package/deploy/helm/npm-scan/templates/api.yaml +0 -94
- package/deploy/helm/npm-scan/templates/ingress.yaml +0 -28
- package/deploy/helm/npm-scan/templates/postgresql.yaml +0 -67
- package/deploy/helm/npm-scan/templates/secrets.yaml +0 -19
- package/deploy/helm/npm-scan/templates/worker.yaml +0 -32
- package/deploy/helm/npm-scan/values.byoc.yaml +0 -75
- package/deploy/helm/npm-scan/values.yaml +0 -103
- package/scripts/download-corpus.js +0 -30
- package/scripts/gen-mal-corpus.js +0 -35
- package/scripts/generate-campaign-fixtures.js +0 -170
- package/src/config/top-5000.json +0 -87
- package/test/fixtures/lockfiles/npm-lock.json +0 -69
- package/test/fixtures/lockfiles/pnpm-lock.yaml +0 -118
- package/test/fixtures/lockfiles/yarn.lock +0 -104
- package/test/fixtures/mock-data.js +0 -69
|
@@ -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
|
+
}
|
|
@@ -1,145 +1,145 @@
|
|
|
1
|
-
const MARKETPLACE_API = 'https://marketplace.visualstudio.com/_apis/public/gallery';
|
|
2
|
-
const OPENVSX_API = 'https://open-vsx.org/api';
|
|
3
|
-
|
|
4
|
-
const _cache = new Map();
|
|
5
|
-
const CACHE_TTL = 5 * 60 * 1000;
|
|
6
|
-
const RATE_LIMIT_MS = 6000;
|
|
7
|
-
let _lastFetchTime = 0;
|
|
8
|
-
|
|
9
|
-
function sleep(ms) {
|
|
10
|
-
return new Promise(r => setTimeout(r, ms));
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
async function rateLimitedFetch(url) {
|
|
14
|
-
const now = Date.now();
|
|
15
|
-
const elapsed = now - _lastFetchTime;
|
|
16
|
-
if (elapsed < RATE_LIMIT_MS) {
|
|
17
|
-
await sleep(RATE_LIMIT_MS - elapsed);
|
|
18
|
-
}
|
|
19
|
-
_lastFetchTime = Date.now();
|
|
20
|
-
|
|
21
|
-
const cached = _cache.get(url);
|
|
22
|
-
if (cached && Date.now() - cached.fetchedAt < CACHE_TTL) {
|
|
23
|
-
return cached.data;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
let res;
|
|
27
|
-
try {
|
|
28
|
-
res = await fetch(url);
|
|
29
|
-
if (res.status === 429) {
|
|
30
|
-
const retryAfter = parseInt(res.headers.get('Retry-After') || '10', 10);
|
|
31
|
-
await sleep(retryAfter * 1000);
|
|
32
|
-
res = await fetch(url);
|
|
33
|
-
}
|
|
34
|
-
if (!res.ok) {
|
|
35
|
-
console.debug(`Marketplace API warning: ${url} returned ${res.status}`);
|
|
36
|
-
return null;
|
|
37
|
-
}
|
|
38
|
-
const data = await res.json();
|
|
39
|
-
_cache.set(url, { data, fetchedAt: Date.now() });
|
|
40
|
-
return data;
|
|
41
|
-
} catch (err) {
|
|
42
|
-
console.debug(`Marketplace API error: ${err.message}`);
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function parseExtensionId(id) {
|
|
48
|
-
const parts = id.split('.');
|
|
49
|
-
if (parts.length < 2) throw new Error(`Invalid extension ID: ${id}`);
|
|
50
|
-
return { publisherId: parts[0], extensionName: parts.slice(1).join('.') };
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export async function getExtensionMetadata(publisherId, extensionName) {
|
|
54
|
-
const url = `${MARKETPLACE_API}/extensionquery`;
|
|
55
|
-
const body = {
|
|
56
|
-
filters: [{
|
|
57
|
-
criteria: [
|
|
58
|
-
{ filterType: 8, value: `${publisherId}.${extensionName}` },
|
|
59
|
-
],
|
|
60
|
-
}],
|
|
61
|
-
flags: 914,
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
const cached = _cache.get(url + JSON.stringify(body));
|
|
65
|
-
if (cached && Date.now() - cached.fetchedAt < CACHE_TTL) {
|
|
66
|
-
return cached.data;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const now = Date.now();
|
|
70
|
-
const elapsed = now - _lastFetchTime;
|
|
71
|
-
if (elapsed < RATE_LIMIT_MS) {
|
|
72
|
-
await sleep(RATE_LIMIT_MS - elapsed);
|
|
73
|
-
}
|
|
74
|
-
_lastFetchTime = Date.now();
|
|
75
|
-
|
|
76
|
-
let res;
|
|
77
|
-
try {
|
|
78
|
-
res = await fetch(url, {
|
|
79
|
-
method: 'POST',
|
|
80
|
-
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json;api-version=3.0-preview.1' },
|
|
81
|
-
body: JSON.stringify(body),
|
|
82
|
-
});
|
|
83
|
-
if (res.status === 429) {
|
|
84
|
-
const retryAfter = parseInt(res.headers.get('Retry-After') || '10', 10);
|
|
85
|
-
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) });
|
|
87
|
-
}
|
|
88
|
-
if (!res.ok) {
|
|
89
|
-
console.debug(`Marketplace API warning: ${url} returned ${res.status}`);
|
|
90
|
-
return null;
|
|
91
|
-
}
|
|
92
|
-
const data = await res.json();
|
|
93
|
-
_cache.set(url + JSON.stringify(body), { data, fetchedAt: Date.now() });
|
|
94
|
-
return data;
|
|
95
|
-
} catch (err) {
|
|
96
|
-
console.debug(`Marketplace API error: ${err.message}`);
|
|
97
|
-
return null;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export async function getVersionHistory(publisherId, extensionName) {
|
|
102
|
-
const data = await getExtensionMetadata(publisherId, extensionName);
|
|
103
|
-
if (!data?.results?.[0]?.extensions?.[0]) return [];
|
|
104
|
-
|
|
105
|
-
const extension = data.results[0].extensions[0];
|
|
106
|
-
const versions = extension.versions || [];
|
|
107
|
-
|
|
108
|
-
return versions.map(v => ({
|
|
109
|
-
version: v.version,
|
|
110
|
-
publishedAt: v.lastUpdated || v.publishedDate,
|
|
111
|
-
publishedBy: extension.publisher?.publisherName || publisherId,
|
|
112
|
-
assetSha256: v.assetUri ? null : null,
|
|
113
|
-
flags: v.flags ? [String(v.flags)] : [],
|
|
114
|
-
}));
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export async function getPublisherProfile(publisherId) {
|
|
118
|
-
const url = `${MARKETPLACE_API}/publishers/${publisherId}`;
|
|
119
|
-
return rateLimitedFetch(url);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export async function getOpenVsxMetadata(namespace, name) {
|
|
123
|
-
const url = `${OPENVSX_API}/${namespace}/${name}`;
|
|
124
|
-
return rateLimitedFetch(url);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export async function getOpenVsxVersionHistory(namespace, name) {
|
|
128
|
-
const data = await getOpenVsxMetadata(namespace, name);
|
|
129
|
-
if (!data) return [];
|
|
130
|
-
const versions = data.allVersions || {};
|
|
131
|
-
const files = data.files || {};
|
|
132
|
-
|
|
133
|
-
return Object.entries(versions).map(([version, publishedAt]) => ({
|
|
134
|
-
version,
|
|
135
|
-
publishedAt: typeof publishedAt === 'string' ? publishedAt : data.timestamp,
|
|
136
|
-
publishedBy: data.namespace || namespace,
|
|
137
|
-
assetSha256: files?.[version]?.sha256 || null,
|
|
138
|
-
flags: [],
|
|
139
|
-
}));
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export function clearMarketplaceCache() {
|
|
143
|
-
_cache.clear();
|
|
144
|
-
_lastFetchTime = 0;
|
|
145
|
-
}
|
|
1
|
+
const MARKETPLACE_API = 'https://marketplace.visualstudio.com/_apis/public/gallery';
|
|
2
|
+
const OPENVSX_API = 'https://open-vsx.org/api';
|
|
3
|
+
|
|
4
|
+
const _cache = new Map();
|
|
5
|
+
const CACHE_TTL = 5 * 60 * 1000;
|
|
6
|
+
const RATE_LIMIT_MS = 6000;
|
|
7
|
+
let _lastFetchTime = 0;
|
|
8
|
+
|
|
9
|
+
function sleep(ms) {
|
|
10
|
+
return new Promise(r => setTimeout(r, ms));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function rateLimitedFetch(url) {
|
|
14
|
+
const now = Date.now();
|
|
15
|
+
const elapsed = now - _lastFetchTime;
|
|
16
|
+
if (elapsed < RATE_LIMIT_MS) {
|
|
17
|
+
await sleep(RATE_LIMIT_MS - elapsed);
|
|
18
|
+
}
|
|
19
|
+
_lastFetchTime = Date.now();
|
|
20
|
+
|
|
21
|
+
const cached = _cache.get(url);
|
|
22
|
+
if (cached && Date.now() - cached.fetchedAt < CACHE_TTL) {
|
|
23
|
+
return cached.data;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let res;
|
|
27
|
+
try {
|
|
28
|
+
res = await fetch(url);
|
|
29
|
+
if (res.status === 429) {
|
|
30
|
+
const retryAfter = parseInt(res.headers.get('Retry-After') || '10', 10);
|
|
31
|
+
await sleep(retryAfter * 1000);
|
|
32
|
+
res = await fetch(url);
|
|
33
|
+
}
|
|
34
|
+
if (!res.ok) {
|
|
35
|
+
console.debug(`Marketplace API warning: ${url} returned ${res.status}`);
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const data = await res.json();
|
|
39
|
+
_cache.set(url, { data, fetchedAt: Date.now() });
|
|
40
|
+
return data;
|
|
41
|
+
} catch (err) {
|
|
42
|
+
console.debug(`Marketplace API error: ${err.message}`);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseExtensionId(id) {
|
|
48
|
+
const parts = id.split('.');
|
|
49
|
+
if (parts.length < 2) throw new Error(`Invalid extension ID: ${id}`);
|
|
50
|
+
return { publisherId: parts[0], extensionName: parts.slice(1).join('.') };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function getExtensionMetadata(publisherId, extensionName) {
|
|
54
|
+
const url = `${MARKETPLACE_API}/extensionquery`;
|
|
55
|
+
const body = {
|
|
56
|
+
filters: [{
|
|
57
|
+
criteria: [
|
|
58
|
+
{ filterType: 8, value: `${publisherId}.${extensionName}` },
|
|
59
|
+
],
|
|
60
|
+
}],
|
|
61
|
+
flags: 914,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const cached = _cache.get(url + JSON.stringify(body));
|
|
65
|
+
if (cached && Date.now() - cached.fetchedAt < CACHE_TTL) {
|
|
66
|
+
return cached.data;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const now = Date.now();
|
|
70
|
+
const elapsed = now - _lastFetchTime;
|
|
71
|
+
if (elapsed < RATE_LIMIT_MS) {
|
|
72
|
+
await sleep(RATE_LIMIT_MS - elapsed);
|
|
73
|
+
}
|
|
74
|
+
_lastFetchTime = Date.now();
|
|
75
|
+
|
|
76
|
+
let res;
|
|
77
|
+
try {
|
|
78
|
+
res = await fetch(url, {
|
|
79
|
+
method: 'POST',
|
|
80
|
+
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json;api-version=3.0-preview.1' },
|
|
81
|
+
body: JSON.stringify(body),
|
|
82
|
+
});
|
|
83
|
+
if (res.status === 429) {
|
|
84
|
+
const retryAfter = parseInt(res.headers.get('Retry-After') || '10', 10);
|
|
85
|
+
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) });
|
|
87
|
+
}
|
|
88
|
+
if (!res.ok) {
|
|
89
|
+
console.debug(`Marketplace API warning: ${url} returned ${res.status}`);
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
const data = await res.json();
|
|
93
|
+
_cache.set(url + JSON.stringify(body), { data, fetchedAt: Date.now() });
|
|
94
|
+
return data;
|
|
95
|
+
} catch (err) {
|
|
96
|
+
console.debug(`Marketplace API error: ${err.message}`);
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function getVersionHistory(publisherId, extensionName) {
|
|
102
|
+
const data = await getExtensionMetadata(publisherId, extensionName);
|
|
103
|
+
if (!data?.results?.[0]?.extensions?.[0]) return [];
|
|
104
|
+
|
|
105
|
+
const extension = data.results[0].extensions[0];
|
|
106
|
+
const versions = extension.versions || [];
|
|
107
|
+
|
|
108
|
+
return versions.map(v => ({
|
|
109
|
+
version: v.version,
|
|
110
|
+
publishedAt: v.lastUpdated || v.publishedDate,
|
|
111
|
+
publishedBy: extension.publisher?.publisherName || publisherId,
|
|
112
|
+
assetSha256: v.assetUri ? null : null,
|
|
113
|
+
flags: v.flags ? [String(v.flags)] : [],
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function getPublisherProfile(publisherId) {
|
|
118
|
+
const url = `${MARKETPLACE_API}/publishers/${publisherId}`;
|
|
119
|
+
return rateLimitedFetch(url);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function getOpenVsxMetadata(namespace, name) {
|
|
123
|
+
const url = `${OPENVSX_API}/${namespace}/${name}`;
|
|
124
|
+
return rateLimitedFetch(url);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function getOpenVsxVersionHistory(namespace, name) {
|
|
128
|
+
const data = await getOpenVsxMetadata(namespace, name);
|
|
129
|
+
if (!data) return [];
|
|
130
|
+
const versions = data.allVersions || {};
|
|
131
|
+
const files = data.files || {};
|
|
132
|
+
|
|
133
|
+
return Object.entries(versions).map(([version, publishedAt]) => ({
|
|
134
|
+
version,
|
|
135
|
+
publishedAt: typeof publishedAt === 'string' ? publishedAt : data.timestamp,
|
|
136
|
+
publishedBy: data.namespace || namespace,
|
|
137
|
+
assetSha256: files?.[version]?.sha256 || null,
|
|
138
|
+
flags: [],
|
|
139
|
+
}));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function clearMarketplaceCache() {
|
|
143
|
+
_cache.clear();
|
|
144
|
+
_lastFetchTime = 0;
|
|
145
|
+
}
|