@lateos/npm-scan 0.15.3 → 0.15.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.
- package/README.md +45 -29
- package/backend/detectors/index.js +2 -0
- package/backend/detectors/mini-shai-hulud/d1-burst-publish.js +42 -0
- package/backend/detectors/mini-shai-hulud/d2-sibling-compromise.js +116 -0
- package/backend/detectors/mini-shai-hulud/d3-slsa-mismatch.js +72 -0
- package/backend/detectors/mini-shai-hulud/d4-maintainer-anomaly.js +45 -0
- package/backend/detectors/mini-shai-hulud/d5-ioc-check.js +95 -0
- package/backend/detectors/mini-shai-hulud/d6-token-exfil.js +38 -0
- package/backend/detectors/mini-shai-hulud/index.js +118 -0
- package/backend/detectors/mini-shai-hulud/iocs.json +79 -0
- package/backend/vsix-scan/detectors/activation-event-risk.js +116 -0
- package/backend/vsix-scan/detectors/burst-publish.js +52 -0
- package/backend/vsix-scan/detectors/exfil-pattern.js +88 -0
- package/backend/vsix-scan/detectors/known-ioc.js +105 -0
- package/backend/vsix-scan/detectors/orphan-commit-fetch.js +69 -0
- package/backend/vsix-scan/detectors/publisher-anomaly.js +70 -0
- package/backend/vsix-scan/index.js +183 -0
- package/backend/vsix-scan/marketplace-client.js +145 -0
- package/backend/vsix-scan/vsix-iocs.json +31 -0
- package/cli/cli.js +21 -4
- package/package.json +1 -1
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lastUpdated": "2026-05-25",
|
|
3
|
+
"schema": "vsix-ioc-v1",
|
|
4
|
+
"iocs": [
|
|
5
|
+
{
|
|
6
|
+
"type": "extensionId",
|
|
7
|
+
"value": "nrwl.angular-console",
|
|
8
|
+
"maliciousVersions": ["18.95.0"],
|
|
9
|
+
"wave": "nx-console-wave3",
|
|
10
|
+
"cve": "CVE-2026-48027",
|
|
11
|
+
"exposureWindowStart": "2026-05-18T12:30:00Z",
|
|
12
|
+
"exposureWindowEnd": "2026-05-18T13:09:00Z",
|
|
13
|
+
"registries": ["marketplace", "open-vsx"],
|
|
14
|
+
"safeVersion": ">=18.100.0",
|
|
15
|
+
"source": "https://nx.dev/blog/nx-console-v18-95-0-postmortem"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"type": "publisherAccount",
|
|
19
|
+
"value": "nrwl",
|
|
20
|
+
"compromiseWindowStart": "2026-05-11T00:00:00Z",
|
|
21
|
+
"compromiseWindowEnd": "2026-05-18T13:09:00Z",
|
|
22
|
+
"note": "Contributor token stolen via TanStack wave1 on May 11; 7-day dwell before publish"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"type": "orphanCommitHash",
|
|
26
|
+
"value": "PLACEHOLDER_UPDATE_FROM_THREAT_INTEL",
|
|
27
|
+
"repo": "nrwl/nx",
|
|
28
|
+
"note": "Dangling commit hosting 498KB Bun payload — update hash from StepSecurity IOC report"
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
package/cli/cli.js
CHANGED
|
@@ -38,6 +38,7 @@ program
|
|
|
38
38
|
.option('--cache-dir <path>', 'Cache directory for offline/air-gapped scans')
|
|
39
39
|
.option('--cache-ttl <seconds>', 'Cache TTL in seconds (default: 604800 = 7 days)', '604800')
|
|
40
40
|
.option('--cache-size <bytes>', 'Max cache size in bytes (default: 1GB)', '1000000000')
|
|
41
|
+
.option('--vsix <extensionId>', 'Scan a VS Code extension (e.g. nrwl.angular-console)')
|
|
41
42
|
.action(async (target, options) => {
|
|
42
43
|
try {
|
|
43
44
|
if (options.fips) {
|
|
@@ -50,11 +51,21 @@ program
|
|
|
50
51
|
cacheMaxSize: parseInt(options.cacheSize || '1000000000')
|
|
51
52
|
};
|
|
52
53
|
|
|
53
|
-
if (!target && !options.file) {
|
|
54
|
-
console.error('Error: specify a package name or --
|
|
54
|
+
if (!target && !options.file && !options.vsix) {
|
|
55
|
+
console.error('Error: specify a package name, --file <path>, or --vsix <extensionId>');
|
|
55
56
|
process.exit(1);
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
if (options.vsix && !target && !options.file) {
|
|
60
|
+
const { vsixScan } = await import('../backend/vsix-scan/index.js');
|
|
61
|
+
const vsixFindings = await vsixScan(options.vsix);
|
|
62
|
+
const { saveScan } = await import('../backend/db.js');
|
|
63
|
+
const scanId = await saveScan(options.vsix, 'latest', vsixFindings);
|
|
64
|
+
const vsixOutput = JSON.stringify({ scanId, findings: vsixFindings, blocked: false, riskScore: 0, vsix: true }, null, 2);
|
|
65
|
+
console.log(vsixOutput);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
58
69
|
const policy = options.policy
|
|
59
70
|
? await import('../backend/policy.js').then(m => m.loadPolicy(options.policy))
|
|
60
71
|
: null;
|
|
@@ -72,10 +83,16 @@ program
|
|
|
72
83
|
: await import('../backend/fetch.js').then(m => m.fetchPackage(target, fetchOptions));
|
|
73
84
|
const pkgName = target || pkgJson.name || 'unknown';
|
|
74
85
|
const findings = await import('../backend/detectors/index.js').then(m => m.runAll(pkgJson, jsFiles, meta, allFiles));
|
|
86
|
+
let vsixFindings = [];
|
|
87
|
+
if (options.vsix) {
|
|
88
|
+
const { vsixScan } = await import('../backend/vsix-scan/index.js');
|
|
89
|
+
vsixFindings = await vsixScan(options.vsix);
|
|
90
|
+
}
|
|
91
|
+
const allFindings = [...findings, ...vsixFindings];
|
|
75
92
|
const { saveScan } = await import('../backend/db.js');
|
|
76
|
-
const scanId = await saveScan(pkgName, 'latest',
|
|
93
|
+
const scanId = await saveScan(pkgName, 'latest', allFindings);
|
|
77
94
|
|
|
78
|
-
let outputFindings =
|
|
95
|
+
let outputFindings = allFindings;
|
|
79
96
|
let blocked = false;
|
|
80
97
|
|
|
81
98
|
if (policy) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lateos/npm-scan",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.5",
|
|
4
4
|
"description": "Modern npm supply chain security scanner — detects obfuscated payloads, credential stealers, conditional triggers, sandbox evasion, and worm-like propagation. 11 attack types, SBOM, NIST/EU CRA compliance reporting.",
|
|
5
5
|
"main": "backend/index.js",
|
|
6
6
|
"bin": {
|