@trailofbits/vsix-audit 0.1.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/LICENSE +661 -0
- package/README.md +281 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +703 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/scanner/batch.d.ts +12 -0
- package/dist/scanner/batch.d.ts.map +1 -0
- package/dist/scanner/batch.js +104 -0
- package/dist/scanner/batch.js.map +1 -0
- package/dist/scanner/bundler.d.ts +35 -0
- package/dist/scanner/bundler.d.ts.map +1 -0
- package/dist/scanner/bundler.js +120 -0
- package/dist/scanner/bundler.js.map +1 -0
- package/dist/scanner/cache.d.ts +45 -0
- package/dist/scanner/cache.d.ts.map +1 -0
- package/dist/scanner/cache.js +153 -0
- package/dist/scanner/cache.js.map +1 -0
- package/dist/scanner/cache.test.d.ts +2 -0
- package/dist/scanner/cache.test.d.ts.map +1 -0
- package/dist/scanner/cache.test.js +149 -0
- package/dist/scanner/cache.test.js.map +1 -0
- package/dist/scanner/capabilities.d.ts +29 -0
- package/dist/scanner/capabilities.d.ts.map +1 -0
- package/dist/scanner/capabilities.js +217 -0
- package/dist/scanner/capabilities.js.map +1 -0
- package/dist/scanner/checks/ast.d.ts +3 -0
- package/dist/scanner/checks/ast.d.ts.map +1 -0
- package/dist/scanner/checks/ast.js +469 -0
- package/dist/scanner/checks/ast.js.map +1 -0
- package/dist/scanner/checks/ast.test.d.ts +2 -0
- package/dist/scanner/checks/ast.test.d.ts.map +1 -0
- package/dist/scanner/checks/ast.test.js +389 -0
- package/dist/scanner/checks/ast.test.js.map +1 -0
- package/dist/scanner/checks/behavioral.d.ts +3 -0
- package/dist/scanner/checks/behavioral.d.ts.map +1 -0
- package/dist/scanner/checks/behavioral.js +367 -0
- package/dist/scanner/checks/behavioral.js.map +1 -0
- package/dist/scanner/checks/blocklist.d.ts +3 -0
- package/dist/scanner/checks/blocklist.d.ts.map +1 -0
- package/dist/scanner/checks/blocklist.js +32 -0
- package/dist/scanner/checks/blocklist.js.map +1 -0
- package/dist/scanner/checks/blocklist.test.d.ts +2 -0
- package/dist/scanner/checks/blocklist.test.d.ts.map +1 -0
- package/dist/scanner/checks/blocklist.test.js +74 -0
- package/dist/scanner/checks/blocklist.test.js.map +1 -0
- package/dist/scanner/checks/chains.d.ts +35 -0
- package/dist/scanner/checks/chains.d.ts.map +1 -0
- package/dist/scanner/checks/chains.js +505 -0
- package/dist/scanner/checks/chains.js.map +1 -0
- package/dist/scanner/checks/chains.test.d.ts +2 -0
- package/dist/scanner/checks/chains.test.d.ts.map +1 -0
- package/dist/scanner/checks/chains.test.js +250 -0
- package/dist/scanner/checks/chains.test.js.map +1 -0
- package/dist/scanner/checks/dataflow.d.ts +3 -0
- package/dist/scanner/checks/dataflow.d.ts.map +1 -0
- package/dist/scanner/checks/dataflow.js +316 -0
- package/dist/scanner/checks/dataflow.js.map +1 -0
- package/dist/scanner/checks/dependencies.d.ts +13 -0
- package/dist/scanner/checks/dependencies.d.ts.map +1 -0
- package/dist/scanner/checks/dependencies.js +225 -0
- package/dist/scanner/checks/dependencies.js.map +1 -0
- package/dist/scanner/checks/dependencies.test.d.ts +2 -0
- package/dist/scanner/checks/dependencies.test.d.ts.map +1 -0
- package/dist/scanner/checks/dependencies.test.js +248 -0
- package/dist/scanner/checks/dependencies.test.js.map +1 -0
- package/dist/scanner/checks/finding-quality.test.d.ts +8 -0
- package/dist/scanner/checks/finding-quality.test.d.ts.map +1 -0
- package/dist/scanner/checks/finding-quality.test.js +164 -0
- package/dist/scanner/checks/finding-quality.test.js.map +1 -0
- package/dist/scanner/checks/ioc.d.ts +20 -0
- package/dist/scanner/checks/ioc.d.ts.map +1 -0
- package/dist/scanner/checks/ioc.js +234 -0
- package/dist/scanner/checks/ioc.js.map +1 -0
- package/dist/scanner/checks/ioc.test.d.ts +2 -0
- package/dist/scanner/checks/ioc.test.d.ts.map +1 -0
- package/dist/scanner/checks/ioc.test.js +298 -0
- package/dist/scanner/checks/ioc.test.js.map +1 -0
- package/dist/scanner/checks/manifest.d.ts +6 -0
- package/dist/scanner/checks/manifest.d.ts.map +1 -0
- package/dist/scanner/checks/manifest.js +123 -0
- package/dist/scanner/checks/manifest.js.map +1 -0
- package/dist/scanner/checks/manifest.test.d.ts +2 -0
- package/dist/scanner/checks/manifest.test.d.ts.map +1 -0
- package/dist/scanner/checks/manifest.test.js +108 -0
- package/dist/scanner/checks/manifest.test.js.map +1 -0
- package/dist/scanner/checks/obfuscation.d.ts +3 -0
- package/dist/scanner/checks/obfuscation.d.ts.map +1 -0
- package/dist/scanner/checks/obfuscation.js +432 -0
- package/dist/scanner/checks/obfuscation.js.map +1 -0
- package/dist/scanner/checks/obfuscation.test.d.ts +2 -0
- package/dist/scanner/checks/obfuscation.test.d.ts.map +1 -0
- package/dist/scanner/checks/obfuscation.test.js +399 -0
- package/dist/scanner/checks/obfuscation.test.js.map +1 -0
- package/dist/scanner/checks/package.d.ts +17 -0
- package/dist/scanner/checks/package.d.ts.map +1 -0
- package/dist/scanner/checks/package.js +422 -0
- package/dist/scanner/checks/package.js.map +1 -0
- package/dist/scanner/checks/package.test.d.ts +2 -0
- package/dist/scanner/checks/package.test.d.ts.map +1 -0
- package/dist/scanner/checks/package.test.js +518 -0
- package/dist/scanner/checks/package.test.js.map +1 -0
- package/dist/scanner/checks/patterns.d.ts +5 -0
- package/dist/scanner/checks/patterns.d.ts.map +1 -0
- package/dist/scanner/checks/patterns.js +251 -0
- package/dist/scanner/checks/patterns.js.map +1 -0
- package/dist/scanner/checks/patterns.test.d.ts +2 -0
- package/dist/scanner/checks/patterns.test.d.ts.map +1 -0
- package/dist/scanner/checks/patterns.test.js +147 -0
- package/dist/scanner/checks/patterns.test.js.map +1 -0
- package/dist/scanner/checks/unicode.d.ts +3 -0
- package/dist/scanner/checks/unicode.d.ts.map +1 -0
- package/dist/scanner/checks/unicode.js +247 -0
- package/dist/scanner/checks/unicode.js.map +1 -0
- package/dist/scanner/checks/unicode.test.d.ts +2 -0
- package/dist/scanner/checks/unicode.test.d.ts.map +1 -0
- package/dist/scanner/checks/unicode.test.js +202 -0
- package/dist/scanner/checks/unicode.test.js.map +1 -0
- package/dist/scanner/checks/yara.d.ts +23 -0
- package/dist/scanner/checks/yara.d.ts.map +1 -0
- package/dist/scanner/checks/yara.js +349 -0
- package/dist/scanner/checks/yara.js.map +1 -0
- package/dist/scanner/checks/yara.test.d.ts +2 -0
- package/dist/scanner/checks/yara.test.d.ts.map +1 -0
- package/dist/scanner/checks/yara.test.js +126 -0
- package/dist/scanner/checks/yara.test.js.map +1 -0
- package/dist/scanner/constants.d.ts +18 -0
- package/dist/scanner/constants.d.ts.map +1 -0
- package/dist/scanner/constants.js +37 -0
- package/dist/scanner/constants.js.map +1 -0
- package/dist/scanner/detection-coverage.test.d.ts +2 -0
- package/dist/scanner/detection-coverage.test.d.ts.map +1 -0
- package/dist/scanner/detection-coverage.test.js +216 -0
- package/dist/scanner/detection-coverage.test.js.map +1 -0
- package/dist/scanner/download.d.ts +76 -0
- package/dist/scanner/download.d.ts.map +1 -0
- package/dist/scanner/download.js +339 -0
- package/dist/scanner/download.js.map +1 -0
- package/dist/scanner/download.test.d.ts +2 -0
- package/dist/scanner/download.test.d.ts.map +1 -0
- package/dist/scanner/download.test.js +149 -0
- package/dist/scanner/download.test.js.map +1 -0
- package/dist/scanner/index.d.ts +8 -0
- package/dist/scanner/index.d.ts.map +1 -0
- package/dist/scanner/index.js +167 -0
- package/dist/scanner/index.js.map +1 -0
- package/dist/scanner/index.test.d.ts +2 -0
- package/dist/scanner/index.test.d.ts.map +1 -0
- package/dist/scanner/index.test.js +71 -0
- package/dist/scanner/index.test.js.map +1 -0
- package/dist/scanner/loaders/zoo.d.ts +3 -0
- package/dist/scanner/loaders/zoo.d.ts.map +1 -0
- package/dist/scanner/loaders/zoo.js +112 -0
- package/dist/scanner/loaders/zoo.js.map +1 -0
- package/dist/scanner/types.d.ts +118 -0
- package/dist/scanner/types.d.ts.map +1 -0
- package/dist/scanner/types.js +2 -0
- package/dist/scanner/types.js.map +1 -0
- package/dist/scanner/utils.d.ts +14 -0
- package/dist/scanner/utils.d.ts.map +1 -0
- package/dist/scanner/utils.js +25 -0
- package/dist/scanner/utils.js.map +1 -0
- package/dist/scanner/vsix.d.ts +6 -0
- package/dist/scanner/vsix.d.ts.map +1 -0
- package/dist/scanner/vsix.js +213 -0
- package/dist/scanner/vsix.js.map +1 -0
- package/dist/scanner/vsix.test.d.ts +2 -0
- package/dist/scanner/vsix.test.d.ts.map +1 -0
- package/dist/scanner/vsix.test.js +355 -0
- package/dist/scanner/vsix.test.js.map +1 -0
- package/package.json +60 -0
- package/zoo/blocklist/extensions.json +201 -0
- package/zoo/iocs/blockchain-extensions.txt +21 -0
- package/zoo/iocs/c2-domains.txt +50 -0
- package/zoo/iocs/c2-ips.txt +24 -0
- package/zoo/iocs/hashes.txt +47 -0
- package/zoo/iocs/malicious-npm.txt +85 -0
- package/zoo/iocs/wallets.txt +18 -0
- package/zoo/signatures/yara/README.md +46 -0
- package/zoo/signatures/yara/blockchain_c2.yar +48 -0
- package/zoo/signatures/yara/code_execution.yar +165 -0
- package/zoo/signatures/yara/credential_harvesting.yar +116 -0
- package/zoo/signatures/yara/crypto_wallet_targeting.yar +92 -0
- package/zoo/signatures/yara/data_exfiltration.yar +207 -0
- package/zoo/signatures/yara/google_calendar_c2.yar +187 -0
- package/zoo/signatures/yara/messaging_c2.yar +103 -0
- package/zoo/signatures/yara/multi_stage_attacks.yar +331 -0
- package/zoo/signatures/yara/obfuscation_patterns.yar +208 -0
- package/zoo/signatures/yara/powershell_attacks.yar +116 -0
- package/zoo/signatures/yara/rat_capabilities.yar +243 -0
- package/zoo/signatures/yara/self_propagation.yar +239 -0
- package/zoo/signatures/yara/unicode_stealth.yar +48 -0
- package/zoo/signatures/yara/websocket_c2.yar +83 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { scanExtension } from "./index.js";
|
|
5
|
+
const ZOO_ROOT = join(import.meta.dirname, "..", "..", "zoo");
|
|
6
|
+
const SAMPLES_DIR = process.env["VSIX_ZOO_PATH"] || join(ZOO_ROOT, "samples");
|
|
7
|
+
const TEST_CORPUS_DIR = join(import.meta.dirname, "..", "..", "test-corpus");
|
|
8
|
+
const CLEAN_DIR = join(TEST_CORPUS_DIR, "clean");
|
|
9
|
+
const hasSamples = existsSync(join(SAMPLES_DIR, "apollyon"));
|
|
10
|
+
const hasCleanCorpus = existsSync(CLEAN_DIR);
|
|
11
|
+
const MALWARE_SAMPLES = [
|
|
12
|
+
{
|
|
13
|
+
path: "apollyon",
|
|
14
|
+
description: "Discord webhook exfiltration PoC",
|
|
15
|
+
expectedFindings: [
|
|
16
|
+
// Discord webhook YARA rule is high severity (per rule metadata)
|
|
17
|
+
{ id: "YARA_C2_JS_Discord_Webhook_Jan25", severity: "high" },
|
|
18
|
+
],
|
|
19
|
+
optionalFindings: ["OBFUSCATION_HIGH_ENTROPY"],
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
path: "kagema/ShowSnowcrypto.SnowShoNo/showsnowcrypto.snowshono-0.6.0",
|
|
23
|
+
description: "SnowShoNo C2 malware",
|
|
24
|
+
expectedFindings: [
|
|
25
|
+
{ id: "KNOWN_C2_DOMAIN", severity: "critical", metadata: { domain: "niggboo.com" } },
|
|
26
|
+
],
|
|
27
|
+
optionalFindings: ["YARA_SUSP_JS_Obfuscator_Hex_Vars_Jan25", "ACTIVATION_STARTUP"],
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
path: "glassworm/icon-theme-materiall.vsix",
|
|
31
|
+
description: "GlassWorm supply chain malware with Rust implant",
|
|
32
|
+
expectedFindings: [
|
|
33
|
+
{ id: "BLOCKLIST_MATCH", severity: "critical" },
|
|
34
|
+
{ id: "KNOWN_MALWARE_HASH", severity: "critical" },
|
|
35
|
+
],
|
|
36
|
+
optionalFindings: ["ACTIVATION_WILDCARD", "THEME_WITH_CODE", "LIFECYCLE_SCRIPT"],
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
path: "ecm3401/Extension-Attack-Suite",
|
|
40
|
+
description: "Educational attack suite with multiple techniques",
|
|
41
|
+
expectedFindings: [
|
|
42
|
+
{ id: "KNOWN_MALWARE_HASH", severity: "critical" },
|
|
43
|
+
{ id: "BIDI_OVERRIDE", severity: "critical" },
|
|
44
|
+
],
|
|
45
|
+
optionalFindings: [
|
|
46
|
+
"AST_EVAL_DYNAMIC",
|
|
47
|
+
"AST_FUNCTION_CONSTRUCTOR",
|
|
48
|
+
"AST_PROCESS_BINDING",
|
|
49
|
+
"YARA_MAL_JS_GlassWorm_Unicode_Stealth_Jan25",
|
|
50
|
+
"YARA_C2_JS_WebSocket_Command_Exec_Jan25",
|
|
51
|
+
"YARA_LOADER_PS_Download_Execute_Jan25",
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
/**
|
|
56
|
+
* Clean extensions that should produce minimal findings.
|
|
57
|
+
* Used for false positive baseline testing.
|
|
58
|
+
*/
|
|
59
|
+
const CLEAN_EXTENSIONS = [
|
|
60
|
+
"esbenp.prettier-vscode.vsix",
|
|
61
|
+
"dbaeumer.vscode-eslint.vsix",
|
|
62
|
+
"4ops.packer.vsix",
|
|
63
|
+
"yzhang.markdown-all-in-one.vsix",
|
|
64
|
+
];
|
|
65
|
+
/**
|
|
66
|
+
* Finding IDs that are acceptable in clean extensions.
|
|
67
|
+
* These represent patterns that occur in legitimate code but are flagged
|
|
68
|
+
* for completeness.
|
|
69
|
+
*/
|
|
70
|
+
const ACCEPTABLE_CLEAN_FINDINGS = new Set([
|
|
71
|
+
// Legitimate activation patterns
|
|
72
|
+
"ACTIVATION_STARTUP",
|
|
73
|
+
"ACTIVATION_WILDCARD",
|
|
74
|
+
// Entropy from minified code is expected
|
|
75
|
+
"OBFUSCATION_HIGH_ENTROPY",
|
|
76
|
+
// Dynamic imports are common in modern bundled code
|
|
77
|
+
"AST_DYNAMIC_IMPORT",
|
|
78
|
+
// Process bindings in Node.js polyfills
|
|
79
|
+
"AST_PROCESS_BINDING",
|
|
80
|
+
// Wallet-like strings in package-lock.json (integrity hashes)
|
|
81
|
+
"CRYPTO_WALLET_DETECTED",
|
|
82
|
+
]);
|
|
83
|
+
/**
|
|
84
|
+
* Finding IDs that should NEVER appear in clean extensions.
|
|
85
|
+
* If these appear, they indicate a false positive that needs investigation.
|
|
86
|
+
*/
|
|
87
|
+
const NEVER_IN_CLEAN_FINDINGS = new Set([
|
|
88
|
+
"BLOCKLIST_MATCH",
|
|
89
|
+
"KNOWN_MALWARE_HASH",
|
|
90
|
+
"KNOWN_C2_DOMAIN",
|
|
91
|
+
"KNOWN_C2_IP",
|
|
92
|
+
"KNOWN_MALWARE_WALLET",
|
|
93
|
+
"MALICIOUS_NPM_PACKAGE",
|
|
94
|
+
"BIDI_OVERRIDE",
|
|
95
|
+
"INVISIBLE_CODE_EXECUTION",
|
|
96
|
+
]);
|
|
97
|
+
const defaultOptions = {
|
|
98
|
+
output: "text",
|
|
99
|
+
severity: "low",
|
|
100
|
+
network: false,
|
|
101
|
+
};
|
|
102
|
+
describe.skipIf(!hasSamples)("Malware Sample Detection Coverage", () => {
|
|
103
|
+
for (const sample of MALWARE_SAMPLES) {
|
|
104
|
+
describe(sample.description, () => {
|
|
105
|
+
it(`detects expected findings in ${sample.path}`, async () => {
|
|
106
|
+
const samplePath = join(SAMPLES_DIR, sample.path);
|
|
107
|
+
const result = await scanExtension(samplePath, defaultOptions);
|
|
108
|
+
const findingIds = new Set(result.findings.map((f) => f.id));
|
|
109
|
+
// Check all expected findings are present
|
|
110
|
+
for (const expected of sample.expectedFindings) {
|
|
111
|
+
const finding = result.findings.find((f) => f.id === expected.id);
|
|
112
|
+
expect(finding, `Expected finding ${expected.id} not found. Found: ${[...findingIds].join(", ")}`).toBeDefined();
|
|
113
|
+
if (expected.severity) {
|
|
114
|
+
expect(finding?.severity).toBe(expected.severity);
|
|
115
|
+
}
|
|
116
|
+
if (expected.metadata) {
|
|
117
|
+
for (const [key, value] of Object.entries(expected.metadata)) {
|
|
118
|
+
expect(finding?.metadata?.[key]).toBe(value);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}, 30000); // 30s timeout for large samples
|
|
123
|
+
it(`produces at least one meaningful finding for ${sample.path}`, async () => {
|
|
124
|
+
const samplePath = join(SAMPLES_DIR, sample.path);
|
|
125
|
+
const result = await scanExtension(samplePath, defaultOptions);
|
|
126
|
+
// Every malware sample should produce at least one finding at medium+ severity
|
|
127
|
+
const meaningfulFindings = result.findings.filter((f) => f.severity === "critical" || f.severity === "high" || f.severity === "medium");
|
|
128
|
+
expect(meaningfulFindings.length).toBeGreaterThan(0);
|
|
129
|
+
}, 30000);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
describe.skipIf(!hasCleanCorpus)("Clean Extension False Positive Testing", () => {
|
|
134
|
+
for (const ext of CLEAN_EXTENSIONS) {
|
|
135
|
+
it(`${ext} has no critical findings that indicate malware`, async () => {
|
|
136
|
+
const extPath = join(CLEAN_DIR, ext);
|
|
137
|
+
if (!existsSync(extPath)) {
|
|
138
|
+
console.warn(`Skipping ${ext}: not found in clean corpus`);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const result = await scanExtension(extPath, defaultOptions);
|
|
142
|
+
// Check that no "never in clean" findings appear
|
|
143
|
+
const badFindings = result.findings.filter((f) => NEVER_IN_CLEAN_FINDINGS.has(f.id));
|
|
144
|
+
expect(badFindings, `Clean extension ${ext} has findings that should never appear in clean code: ${badFindings.map((f) => f.id).join(", ")}`).toHaveLength(0);
|
|
145
|
+
}, 30000);
|
|
146
|
+
}
|
|
147
|
+
it("summarizes findings across clean corpus", async () => {
|
|
148
|
+
const findingCounts = {};
|
|
149
|
+
let totalScanned = 0;
|
|
150
|
+
for (const ext of CLEAN_EXTENSIONS) {
|
|
151
|
+
const extPath = join(CLEAN_DIR, ext);
|
|
152
|
+
if (!existsSync(extPath))
|
|
153
|
+
continue;
|
|
154
|
+
const result = await scanExtension(extPath, defaultOptions);
|
|
155
|
+
totalScanned++;
|
|
156
|
+
for (const finding of result.findings) {
|
|
157
|
+
findingCounts[finding.id] = (findingCounts[finding.id] || 0) + 1;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// Log summary for analysis (not a hard failure)
|
|
161
|
+
console.log(`\nClean corpus summary (${totalScanned} extensions):`);
|
|
162
|
+
const sorted = Object.entries(findingCounts).sort((a, b) => b[1] - a[1]);
|
|
163
|
+
for (const [id, count] of sorted) {
|
|
164
|
+
const pct = ((count / totalScanned) * 100).toFixed(0);
|
|
165
|
+
const status = ACCEPTABLE_CLEAN_FINDINGS.has(id) ? "✓" : "⚠";
|
|
166
|
+
console.log(` ${status} ${id}: ${count}/${totalScanned} (${pct}%)`);
|
|
167
|
+
}
|
|
168
|
+
expect(totalScanned).toBeGreaterThan(0);
|
|
169
|
+
}, 120000);
|
|
170
|
+
});
|
|
171
|
+
describe.skipIf(!hasSamples)("Detection Quality Assertions", () => {
|
|
172
|
+
it("all expected YARA rules fire on their target samples", async () => {
|
|
173
|
+
const yaraFindings = {};
|
|
174
|
+
for (const sample of MALWARE_SAMPLES) {
|
|
175
|
+
const samplePath = join(SAMPLES_DIR, sample.path);
|
|
176
|
+
const result = await scanExtension(samplePath, defaultOptions);
|
|
177
|
+
const yaraIds = result.findings.filter((f) => f.id.startsWith("YARA_")).map((f) => f.id);
|
|
178
|
+
yaraFindings[sample.path] = yaraIds;
|
|
179
|
+
}
|
|
180
|
+
// Verify at least one YARA rule fires on each malware sample
|
|
181
|
+
for (const [path, rules] of Object.entries(yaraFindings)) {
|
|
182
|
+
// apollyon should trigger Discord webhook rule
|
|
183
|
+
if (path === "apollyon") {
|
|
184
|
+
expect(rules).toContain("YARA_C2_JS_Discord_Webhook_Jan25");
|
|
185
|
+
}
|
|
186
|
+
// ecm3401 should trigger multiple YARA rules
|
|
187
|
+
if (path.includes("ecm3401")) {
|
|
188
|
+
expect(rules.length).toBeGreaterThan(0);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}, 60000);
|
|
192
|
+
it("IOC checks detect known C2 infrastructure", async () => {
|
|
193
|
+
const kagemaSample = join(SAMPLES_DIR, "kagema/ShowSnowcrypto.SnowShoNo/showsnowcrypto.snowshono-0.6.0");
|
|
194
|
+
const result = await scanExtension(kagemaSample, defaultOptions);
|
|
195
|
+
const c2Finding = result.findings.find((f) => f.id === "KNOWN_C2_DOMAIN");
|
|
196
|
+
expect(c2Finding).toBeDefined();
|
|
197
|
+
expect(c2Finding?.metadata?.["domain"]).toBe("niggboo.com");
|
|
198
|
+
}, 30000);
|
|
199
|
+
it("hash matching catches known malware files", async () => {
|
|
200
|
+
const glasswormSample = join(SAMPLES_DIR, "glassworm/icon-theme-materiall.vsix");
|
|
201
|
+
const result = await scanExtension(glasswormSample, defaultOptions);
|
|
202
|
+
const hashFindings = result.findings.filter((f) => f.id === "KNOWN_MALWARE_HASH");
|
|
203
|
+
expect(hashFindings.length).toBeGreaterThan(0);
|
|
204
|
+
// Should detect the darwin.node, os.node, and extension.js hashes
|
|
205
|
+
const files = hashFindings.map((f) => f.location?.file);
|
|
206
|
+
expect(files.some((f) => f?.includes("darwin.node"))).toBe(true);
|
|
207
|
+
}, 30000);
|
|
208
|
+
it("blocklist matching catches known malicious extension IDs", async () => {
|
|
209
|
+
const glasswormSample = join(SAMPLES_DIR, "glassworm/icon-theme-materiall.vsix");
|
|
210
|
+
const result = await scanExtension(glasswormSample, defaultOptions);
|
|
211
|
+
const blocklistFinding = result.findings.find((f) => f.id === "BLOCKLIST_MATCH");
|
|
212
|
+
expect(blocklistFinding).toBeDefined();
|
|
213
|
+
expect(blocklistFinding?.severity).toBe("critical");
|
|
214
|
+
}, 30000);
|
|
215
|
+
});
|
|
216
|
+
//# sourceMappingURL=detection-coverage.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"detection-coverage.test.js","sourceRoot":"","sources":["../../src/scanner/detection-coverage.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { Registry } from "./types.js";
|
|
2
|
+
export interface ExtensionMetadata {
|
|
3
|
+
extensionId: string;
|
|
4
|
+
publisher: string;
|
|
5
|
+
name: string;
|
|
6
|
+
version: string;
|
|
7
|
+
displayName?: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
installCount?: number;
|
|
10
|
+
lastUpdated?: string;
|
|
11
|
+
registry?: Registry;
|
|
12
|
+
}
|
|
13
|
+
export interface DownloadOptions {
|
|
14
|
+
destDir?: string;
|
|
15
|
+
useCache?: boolean;
|
|
16
|
+
forceDownload?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface DownloadResult {
|
|
19
|
+
path: string;
|
|
20
|
+
metadata: ExtensionMetadata;
|
|
21
|
+
fromCache?: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface ParsedExtensionId {
|
|
24
|
+
publisher: string;
|
|
25
|
+
name: string;
|
|
26
|
+
version?: string;
|
|
27
|
+
registry: Registry;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Parse an extension ID in the format "publisher.name" or "publisher.name@version"
|
|
31
|
+
* Optionally with registry prefix: "openvsx:publisher.name" or "marketplace:publisher.name"
|
|
32
|
+
*/
|
|
33
|
+
export declare function parseExtensionId(input: string): ParsedExtensionId;
|
|
34
|
+
/**
|
|
35
|
+
* Query the VS Code Marketplace for extension metadata
|
|
36
|
+
*/
|
|
37
|
+
export declare function queryExtension(publisher: string, name: string, version?: string): Promise<ExtensionMetadata>;
|
|
38
|
+
/**
|
|
39
|
+
* Query OpenVSX for extension metadata
|
|
40
|
+
*/
|
|
41
|
+
export declare function queryOpenVSX(publisher: string, name: string, version?: string): Promise<ExtensionMetadata>;
|
|
42
|
+
/**
|
|
43
|
+
* Query Cursor Extension Marketplace for extension metadata
|
|
44
|
+
*/
|
|
45
|
+
export declare function queryCursor(publisher: string, name: string, version?: string): Promise<ExtensionMetadata>;
|
|
46
|
+
/**
|
|
47
|
+
* Get the download URL for a VSIX package from the VS Code Marketplace
|
|
48
|
+
*/
|
|
49
|
+
export declare function getMarketplaceDownloadUrl(publisher: string, name: string, version: string): string;
|
|
50
|
+
/**
|
|
51
|
+
* Get the download URL for a VSIX package from OpenVSX
|
|
52
|
+
*/
|
|
53
|
+
export declare function getOpenVSXDownloadUrl(publisher: string, name: string, version: string): string;
|
|
54
|
+
/**
|
|
55
|
+
* Get the download URL for a VSIX package from Cursor Extension Marketplace
|
|
56
|
+
*/
|
|
57
|
+
export declare function getCursorDownloadUrl(publisher: string, name: string, version: string): string;
|
|
58
|
+
/**
|
|
59
|
+
* Get the download URL for a VSIX package
|
|
60
|
+
* @deprecated Use getMarketplaceDownloadUrl or getOpenVSXDownloadUrl instead
|
|
61
|
+
*/
|
|
62
|
+
export declare function getDownloadUrl(publisher: string, name: string, version: string): string;
|
|
63
|
+
/**
|
|
64
|
+
* Download a VSIX from the marketplace
|
|
65
|
+
*/
|
|
66
|
+
export declare function downloadVsix(publisher: string, name: string, version: string, destPath: string, registry?: Registry): Promise<void>;
|
|
67
|
+
/**
|
|
68
|
+
* Download an extension from the VS Code Marketplace or OpenVSX
|
|
69
|
+
*
|
|
70
|
+
* @param extensionId - Extension ID in format "publisher.name", "publisher.name@version",
|
|
71
|
+
* or with registry prefix: "openvsx:publisher.name", "marketplace:publisher.name"
|
|
72
|
+
* @param options - Optional settings
|
|
73
|
+
* @returns Path to downloaded VSIX and extension metadata
|
|
74
|
+
*/
|
|
75
|
+
export declare function downloadExtension(extensionId: string, options?: DownloadOptions): Promise<DownloadResult>;
|
|
76
|
+
//# sourceMappingURL=download.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"download.d.ts","sourceRoot":"","sources":["../../src/scanner/download.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,QAAQ,CAAC;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,iBAAiB,CAAC;IAC5B,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AA0CD,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,QAAQ,CAAC;CACpB;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,iBAAiB,CAgDjE;AAED;;GAEG;AACH,wBAAsB,cAAc,CAClC,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,iBAAiB,CAAC,CA4E5B;AAED;;GAEG;AACH,wBAAsB,YAAY,CAChC,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,iBAAiB,CAAC,CAsC5B;AAED;;GAEG;AACH,wBAAsB,WAAW,CAC/B,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,iBAAiB,CAAC,CA4E5B;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,GACd,MAAM,CAER;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAE9F;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAE7F;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAEvF;AAyBD;;GAEG;AACH,wBAAsB,YAAY,CAChC,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,QAAQ,GAAE,QAAwB,GACjC,OAAO,CAAC,IAAI,CAAC,CAWf;AAED;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CACrC,WAAW,EAAE,MAAM,EACnB,OAAO,CAAC,EAAE,eAAe,GACxB,OAAO,CAAC,cAAc,CAAC,CA2DzB"}
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { createWriteStream } from "node:fs";
|
|
2
|
+
import { copyFile, mkdir } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { pipeline } from "node:stream/promises";
|
|
5
|
+
import { Readable } from "node:stream";
|
|
6
|
+
import { ensureCacheDir, getCachedPath, isCached } from "./cache.js";
|
|
7
|
+
const GALLERY_API_URL = "https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery";
|
|
8
|
+
const GALLERY_API_VERSION = "7.1-preview.1";
|
|
9
|
+
const OPENVSX_API_URL = "https://open-vsx.org/api";
|
|
10
|
+
const CURSOR_API_URL = "https://marketplace.cursorapi.com/_apis/public/gallery/extensionquery";
|
|
11
|
+
/**
|
|
12
|
+
* Parse an extension ID in the format "publisher.name" or "publisher.name@version"
|
|
13
|
+
* Optionally with registry prefix: "openvsx:publisher.name" or "marketplace:publisher.name"
|
|
14
|
+
*/
|
|
15
|
+
export function parseExtensionId(input) {
|
|
16
|
+
let registry = "marketplace";
|
|
17
|
+
let rest = input;
|
|
18
|
+
// Check for registry prefix
|
|
19
|
+
if (input.startsWith("openvsx:")) {
|
|
20
|
+
registry = "openvsx";
|
|
21
|
+
rest = input.slice(8);
|
|
22
|
+
}
|
|
23
|
+
else if (input.startsWith("marketplace:")) {
|
|
24
|
+
registry = "marketplace";
|
|
25
|
+
rest = input.slice(12);
|
|
26
|
+
}
|
|
27
|
+
else if (input.startsWith("cursor:")) {
|
|
28
|
+
registry = "cursor";
|
|
29
|
+
rest = input.slice(7);
|
|
30
|
+
}
|
|
31
|
+
// Check for version suffix
|
|
32
|
+
const atIndex = rest.lastIndexOf("@");
|
|
33
|
+
let identifier = rest;
|
|
34
|
+
let version;
|
|
35
|
+
if (atIndex > 0) {
|
|
36
|
+
identifier = rest.slice(0, atIndex);
|
|
37
|
+
version = rest.slice(atIndex + 1);
|
|
38
|
+
}
|
|
39
|
+
// Split publisher.name
|
|
40
|
+
const dotIndex = identifier.indexOf(".");
|
|
41
|
+
if (dotIndex <= 0) {
|
|
42
|
+
throw new Error(`Invalid extension ID: "${input}". Expected format: publisher.name or publisher.name@version`);
|
|
43
|
+
}
|
|
44
|
+
const publisher = identifier.slice(0, dotIndex);
|
|
45
|
+
const name = identifier.slice(dotIndex + 1);
|
|
46
|
+
if (!publisher || !name) {
|
|
47
|
+
throw new Error(`Invalid extension ID: "${input}". Expected format: publisher.name or publisher.name@version`);
|
|
48
|
+
}
|
|
49
|
+
const result = { publisher, name, registry };
|
|
50
|
+
if (version !== undefined) {
|
|
51
|
+
result.version = version;
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Query the VS Code Marketplace for extension metadata
|
|
57
|
+
*/
|
|
58
|
+
export async function queryExtension(publisher, name, version) {
|
|
59
|
+
const extensionId = `${publisher}.${name}`;
|
|
60
|
+
const requestBody = {
|
|
61
|
+
filters: [
|
|
62
|
+
{
|
|
63
|
+
criteria: [{ filterType: 7, value: extensionId }],
|
|
64
|
+
pageSize: 1,
|
|
65
|
+
pageNumber: 1,
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
flags: 0x200 | 0x80 | 0x1, // Include versions, files, and statistics
|
|
69
|
+
};
|
|
70
|
+
const response = await fetch(GALLERY_API_URL, {
|
|
71
|
+
method: "POST",
|
|
72
|
+
headers: {
|
|
73
|
+
"Content-Type": "application/json",
|
|
74
|
+
Accept: `application/json;api-version=${GALLERY_API_VERSION}`,
|
|
75
|
+
},
|
|
76
|
+
body: JSON.stringify(requestBody),
|
|
77
|
+
});
|
|
78
|
+
if (!response.ok) {
|
|
79
|
+
throw new Error(`Marketplace API error: ${response.status} ${response.statusText}`);
|
|
80
|
+
}
|
|
81
|
+
const data = (await response.json());
|
|
82
|
+
const extensions = data.results?.[0]?.extensions;
|
|
83
|
+
const ext = extensions?.[0];
|
|
84
|
+
if (!ext) {
|
|
85
|
+
throw new Error(`Extension not found: ${extensionId}`);
|
|
86
|
+
}
|
|
87
|
+
const versions = ext.versions ?? [];
|
|
88
|
+
// Find the requested version or use latest
|
|
89
|
+
let targetVersion = versions[0];
|
|
90
|
+
if (version) {
|
|
91
|
+
const found = versions.find((v) => v.version === version);
|
|
92
|
+
if (!found) {
|
|
93
|
+
throw new Error(`Version ${version} not found for ${extensionId}. Latest: ${versions[0]?.version}`);
|
|
94
|
+
}
|
|
95
|
+
targetVersion = found;
|
|
96
|
+
}
|
|
97
|
+
if (!targetVersion) {
|
|
98
|
+
throw new Error(`No versions available for ${extensionId}`);
|
|
99
|
+
}
|
|
100
|
+
// Get install count from statistics
|
|
101
|
+
const installStat = ext.statistics?.find((s) => s.statisticName === "install");
|
|
102
|
+
const result = {
|
|
103
|
+
extensionId,
|
|
104
|
+
publisher: ext.publisher.publisherName,
|
|
105
|
+
name: ext.extensionName,
|
|
106
|
+
version: targetVersion.version,
|
|
107
|
+
lastUpdated: targetVersion.lastUpdated,
|
|
108
|
+
registry: "marketplace",
|
|
109
|
+
};
|
|
110
|
+
if (ext.displayName) {
|
|
111
|
+
result.displayName = ext.displayName;
|
|
112
|
+
}
|
|
113
|
+
if (ext.shortDescription) {
|
|
114
|
+
result.description = ext.shortDescription;
|
|
115
|
+
}
|
|
116
|
+
if (installStat?.value !== undefined) {
|
|
117
|
+
result.installCount = installStat.value;
|
|
118
|
+
}
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Query OpenVSX for extension metadata
|
|
123
|
+
*/
|
|
124
|
+
export async function queryOpenVSX(publisher, name, version) {
|
|
125
|
+
const extensionId = `${publisher}.${name}`;
|
|
126
|
+
const url = version
|
|
127
|
+
? `${OPENVSX_API_URL}/${publisher}/${name}/${version}`
|
|
128
|
+
: `${OPENVSX_API_URL}/${publisher}/${name}`;
|
|
129
|
+
const response = await fetch(url);
|
|
130
|
+
if (!response.ok) {
|
|
131
|
+
if (response.status === 404) {
|
|
132
|
+
throw new Error(`Extension not found on OpenVSX: ${extensionId}`);
|
|
133
|
+
}
|
|
134
|
+
throw new Error(`OpenVSX API error: ${response.status} ${response.statusText}`);
|
|
135
|
+
}
|
|
136
|
+
const data = (await response.json());
|
|
137
|
+
const result = {
|
|
138
|
+
extensionId,
|
|
139
|
+
publisher: data.namespace,
|
|
140
|
+
name: data.name,
|
|
141
|
+
version: data.version,
|
|
142
|
+
registry: "openvsx",
|
|
143
|
+
};
|
|
144
|
+
if (data.timestamp) {
|
|
145
|
+
result.lastUpdated = data.timestamp;
|
|
146
|
+
}
|
|
147
|
+
if (data.displayName) {
|
|
148
|
+
result.displayName = data.displayName;
|
|
149
|
+
}
|
|
150
|
+
if (data.description) {
|
|
151
|
+
result.description = data.description;
|
|
152
|
+
}
|
|
153
|
+
if (data.downloadCount !== undefined) {
|
|
154
|
+
result.installCount = data.downloadCount;
|
|
155
|
+
}
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Query Cursor Extension Marketplace for extension metadata
|
|
160
|
+
*/
|
|
161
|
+
export async function queryCursor(publisher, name, version) {
|
|
162
|
+
const extensionId = `${publisher}.${name}`;
|
|
163
|
+
const requestBody = {
|
|
164
|
+
filters: [
|
|
165
|
+
{
|
|
166
|
+
criteria: [{ filterType: 7, value: extensionId }],
|
|
167
|
+
pageSize: 1,
|
|
168
|
+
pageNumber: 1,
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
flags: 0x200 | 0x80 | 0x1, // Include versions, files, and statistics
|
|
172
|
+
};
|
|
173
|
+
const response = await fetch(CURSOR_API_URL, {
|
|
174
|
+
method: "POST",
|
|
175
|
+
headers: {
|
|
176
|
+
"Content-Type": "application/json",
|
|
177
|
+
Accept: `application/json;api-version=${GALLERY_API_VERSION}`,
|
|
178
|
+
},
|
|
179
|
+
body: JSON.stringify(requestBody),
|
|
180
|
+
});
|
|
181
|
+
if (!response.ok) {
|
|
182
|
+
throw new Error(`Cursor API error: ${response.status} ${response.statusText}`);
|
|
183
|
+
}
|
|
184
|
+
const data = (await response.json());
|
|
185
|
+
const extensions = data.results?.[0]?.extensions;
|
|
186
|
+
const ext = extensions?.[0];
|
|
187
|
+
if (!ext) {
|
|
188
|
+
throw new Error(`Extension not found on Cursor: ${extensionId}`);
|
|
189
|
+
}
|
|
190
|
+
const versions = ext.versions ?? [];
|
|
191
|
+
// Find the requested version or use latest
|
|
192
|
+
let targetVersion = versions[0];
|
|
193
|
+
if (version) {
|
|
194
|
+
const found = versions.find((v) => v.version === version);
|
|
195
|
+
if (!found) {
|
|
196
|
+
throw new Error(`Version ${version} not found for ${extensionId}. Latest: ${versions[0]?.version}`);
|
|
197
|
+
}
|
|
198
|
+
targetVersion = found;
|
|
199
|
+
}
|
|
200
|
+
if (!targetVersion) {
|
|
201
|
+
throw new Error(`No versions available for ${extensionId}`);
|
|
202
|
+
}
|
|
203
|
+
// Get install count from statistics
|
|
204
|
+
const installStat = ext.statistics?.find((s) => s.statisticName === "install");
|
|
205
|
+
const result = {
|
|
206
|
+
extensionId,
|
|
207
|
+
publisher: ext.publisher.publisherName,
|
|
208
|
+
name: ext.extensionName,
|
|
209
|
+
version: targetVersion.version,
|
|
210
|
+
lastUpdated: targetVersion.lastUpdated,
|
|
211
|
+
registry: "cursor",
|
|
212
|
+
};
|
|
213
|
+
if (ext.displayName) {
|
|
214
|
+
result.displayName = ext.displayName;
|
|
215
|
+
}
|
|
216
|
+
if (ext.shortDescription) {
|
|
217
|
+
result.description = ext.shortDescription;
|
|
218
|
+
}
|
|
219
|
+
if (installStat?.value !== undefined) {
|
|
220
|
+
result.installCount = installStat.value;
|
|
221
|
+
}
|
|
222
|
+
return result;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Get the download URL for a VSIX package from the VS Code Marketplace
|
|
226
|
+
*/
|
|
227
|
+
export function getMarketplaceDownloadUrl(publisher, name, version) {
|
|
228
|
+
return `https://${publisher}.gallery.vsassets.io/_apis/public/gallery/publisher/${publisher}/extension/${name}/${version}/assetbyname/Microsoft.VisualStudio.Services.VSIXPackage`;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Get the download URL for a VSIX package from OpenVSX
|
|
232
|
+
*/
|
|
233
|
+
export function getOpenVSXDownloadUrl(publisher, name, version) {
|
|
234
|
+
return `${OPENVSX_API_URL}/${publisher}/${name}/${version}/file/${publisher}.${name}-${version}.vsix`;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Get the download URL for a VSIX package from Cursor Extension Marketplace
|
|
238
|
+
*/
|
|
239
|
+
export function getCursorDownloadUrl(publisher, name, version) {
|
|
240
|
+
return `https://marketplace.cursorapi.com/_apis/public/gallery/publishers/${publisher}/vsextensions/${name}/${version}/vspackage`;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Get the download URL for a VSIX package
|
|
244
|
+
* @deprecated Use getMarketplaceDownloadUrl or getOpenVSXDownloadUrl instead
|
|
245
|
+
*/
|
|
246
|
+
export function getDownloadUrl(publisher, name, version) {
|
|
247
|
+
return getMarketplaceDownloadUrl(publisher, name, version);
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Download a VSIX from a URL
|
|
251
|
+
*/
|
|
252
|
+
async function downloadVsixFromUrl(url, destPath) {
|
|
253
|
+
const response = await fetch(url);
|
|
254
|
+
if (!response.ok) {
|
|
255
|
+
throw new Error(`Download failed: ${response.status} ${response.statusText}`);
|
|
256
|
+
}
|
|
257
|
+
if (!response.body) {
|
|
258
|
+
throw new Error("Empty response body");
|
|
259
|
+
}
|
|
260
|
+
// Ensure destination directory exists
|
|
261
|
+
await mkdir(dirname(destPath), { recursive: true });
|
|
262
|
+
// Stream the response to a file
|
|
263
|
+
const nodeStream = Readable.fromWeb(response.body);
|
|
264
|
+
const fileStream = createWriteStream(destPath);
|
|
265
|
+
await pipeline(nodeStream, fileStream);
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Download a VSIX from the marketplace
|
|
269
|
+
*/
|
|
270
|
+
export async function downloadVsix(publisher, name, version, destPath, registry = "marketplace") {
|
|
271
|
+
let url;
|
|
272
|
+
if (registry === "openvsx") {
|
|
273
|
+
url = getOpenVSXDownloadUrl(publisher, name, version);
|
|
274
|
+
}
|
|
275
|
+
else if (registry === "cursor") {
|
|
276
|
+
url = getCursorDownloadUrl(publisher, name, version);
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
url = getMarketplaceDownloadUrl(publisher, name, version);
|
|
280
|
+
}
|
|
281
|
+
await downloadVsixFromUrl(url, destPath);
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Download an extension from the VS Code Marketplace or OpenVSX
|
|
285
|
+
*
|
|
286
|
+
* @param extensionId - Extension ID in format "publisher.name", "publisher.name@version",
|
|
287
|
+
* or with registry prefix: "openvsx:publisher.name", "marketplace:publisher.name"
|
|
288
|
+
* @param options - Optional settings
|
|
289
|
+
* @returns Path to downloaded VSIX and extension metadata
|
|
290
|
+
*/
|
|
291
|
+
export async function downloadExtension(extensionId, options) {
|
|
292
|
+
const { publisher, name, version, registry } = parseExtensionId(extensionId);
|
|
293
|
+
const useCache = options?.useCache !== false;
|
|
294
|
+
const forceDownload = options?.forceDownload === true;
|
|
295
|
+
// Query the appropriate registry for metadata
|
|
296
|
+
let metadata;
|
|
297
|
+
if (registry === "openvsx") {
|
|
298
|
+
metadata = await queryOpenVSX(publisher, name, version);
|
|
299
|
+
}
|
|
300
|
+
else if (registry === "cursor") {
|
|
301
|
+
metadata = await queryCursor(publisher, name, version);
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
metadata = await queryExtension(publisher, name, version);
|
|
305
|
+
}
|
|
306
|
+
// If destDir is explicitly provided, download directly there (bypasses cache)
|
|
307
|
+
if (options?.destDir) {
|
|
308
|
+
const filename = `${metadata.publisher}.${metadata.name}-${metadata.version}.vsix`;
|
|
309
|
+
const destPath = join(options.destDir, filename);
|
|
310
|
+
// Check cache first if enabled
|
|
311
|
+
if (useCache && !forceDownload) {
|
|
312
|
+
const cachedPath = getCachedPath(registry, metadata.publisher, metadata.name, metadata.version);
|
|
313
|
+
const cached = await isCached(registry, metadata.publisher, metadata.name, metadata.version);
|
|
314
|
+
if (cached) {
|
|
315
|
+
// Copy from cache to destination
|
|
316
|
+
await mkdir(options.destDir, { recursive: true });
|
|
317
|
+
await copyFile(cachedPath, destPath);
|
|
318
|
+
return { path: destPath, metadata, fromCache: true };
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
// Download fresh
|
|
322
|
+
await downloadVsix(metadata.publisher, metadata.name, metadata.version, destPath, registry);
|
|
323
|
+
return { path: destPath, metadata, fromCache: false };
|
|
324
|
+
}
|
|
325
|
+
// Use cache directory
|
|
326
|
+
const cachedPath = getCachedPath(registry, metadata.publisher, metadata.name, metadata.version);
|
|
327
|
+
// Check if already cached
|
|
328
|
+
if (useCache && !forceDownload) {
|
|
329
|
+
const cached = await isCached(registry, metadata.publisher, metadata.name, metadata.version);
|
|
330
|
+
if (cached) {
|
|
331
|
+
return { path: cachedPath, metadata, fromCache: true };
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
// Download to cache
|
|
335
|
+
await ensureCacheDir(registry);
|
|
336
|
+
await downloadVsix(metadata.publisher, metadata.name, metadata.version, cachedPath, registry);
|
|
337
|
+
return { path: cachedPath, metadata, fromCache: false };
|
|
338
|
+
}
|
|
339
|
+
//# sourceMappingURL=download.js.map
|