@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.
Files changed (197) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +281 -0
  3. package/dist/cli.d.ts +3 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +703 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/index.d.ts +3 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +4 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/scanner/batch.d.ts +12 -0
  12. package/dist/scanner/batch.d.ts.map +1 -0
  13. package/dist/scanner/batch.js +104 -0
  14. package/dist/scanner/batch.js.map +1 -0
  15. package/dist/scanner/bundler.d.ts +35 -0
  16. package/dist/scanner/bundler.d.ts.map +1 -0
  17. package/dist/scanner/bundler.js +120 -0
  18. package/dist/scanner/bundler.js.map +1 -0
  19. package/dist/scanner/cache.d.ts +45 -0
  20. package/dist/scanner/cache.d.ts.map +1 -0
  21. package/dist/scanner/cache.js +153 -0
  22. package/dist/scanner/cache.js.map +1 -0
  23. package/dist/scanner/cache.test.d.ts +2 -0
  24. package/dist/scanner/cache.test.d.ts.map +1 -0
  25. package/dist/scanner/cache.test.js +149 -0
  26. package/dist/scanner/cache.test.js.map +1 -0
  27. package/dist/scanner/capabilities.d.ts +29 -0
  28. package/dist/scanner/capabilities.d.ts.map +1 -0
  29. package/dist/scanner/capabilities.js +217 -0
  30. package/dist/scanner/capabilities.js.map +1 -0
  31. package/dist/scanner/checks/ast.d.ts +3 -0
  32. package/dist/scanner/checks/ast.d.ts.map +1 -0
  33. package/dist/scanner/checks/ast.js +469 -0
  34. package/dist/scanner/checks/ast.js.map +1 -0
  35. package/dist/scanner/checks/ast.test.d.ts +2 -0
  36. package/dist/scanner/checks/ast.test.d.ts.map +1 -0
  37. package/dist/scanner/checks/ast.test.js +389 -0
  38. package/dist/scanner/checks/ast.test.js.map +1 -0
  39. package/dist/scanner/checks/behavioral.d.ts +3 -0
  40. package/dist/scanner/checks/behavioral.d.ts.map +1 -0
  41. package/dist/scanner/checks/behavioral.js +367 -0
  42. package/dist/scanner/checks/behavioral.js.map +1 -0
  43. package/dist/scanner/checks/blocklist.d.ts +3 -0
  44. package/dist/scanner/checks/blocklist.d.ts.map +1 -0
  45. package/dist/scanner/checks/blocklist.js +32 -0
  46. package/dist/scanner/checks/blocklist.js.map +1 -0
  47. package/dist/scanner/checks/blocklist.test.d.ts +2 -0
  48. package/dist/scanner/checks/blocklist.test.d.ts.map +1 -0
  49. package/dist/scanner/checks/blocklist.test.js +74 -0
  50. package/dist/scanner/checks/blocklist.test.js.map +1 -0
  51. package/dist/scanner/checks/chains.d.ts +35 -0
  52. package/dist/scanner/checks/chains.d.ts.map +1 -0
  53. package/dist/scanner/checks/chains.js +505 -0
  54. package/dist/scanner/checks/chains.js.map +1 -0
  55. package/dist/scanner/checks/chains.test.d.ts +2 -0
  56. package/dist/scanner/checks/chains.test.d.ts.map +1 -0
  57. package/dist/scanner/checks/chains.test.js +250 -0
  58. package/dist/scanner/checks/chains.test.js.map +1 -0
  59. package/dist/scanner/checks/dataflow.d.ts +3 -0
  60. package/dist/scanner/checks/dataflow.d.ts.map +1 -0
  61. package/dist/scanner/checks/dataflow.js +316 -0
  62. package/dist/scanner/checks/dataflow.js.map +1 -0
  63. package/dist/scanner/checks/dependencies.d.ts +13 -0
  64. package/dist/scanner/checks/dependencies.d.ts.map +1 -0
  65. package/dist/scanner/checks/dependencies.js +225 -0
  66. package/dist/scanner/checks/dependencies.js.map +1 -0
  67. package/dist/scanner/checks/dependencies.test.d.ts +2 -0
  68. package/dist/scanner/checks/dependencies.test.d.ts.map +1 -0
  69. package/dist/scanner/checks/dependencies.test.js +248 -0
  70. package/dist/scanner/checks/dependencies.test.js.map +1 -0
  71. package/dist/scanner/checks/finding-quality.test.d.ts +8 -0
  72. package/dist/scanner/checks/finding-quality.test.d.ts.map +1 -0
  73. package/dist/scanner/checks/finding-quality.test.js +164 -0
  74. package/dist/scanner/checks/finding-quality.test.js.map +1 -0
  75. package/dist/scanner/checks/ioc.d.ts +20 -0
  76. package/dist/scanner/checks/ioc.d.ts.map +1 -0
  77. package/dist/scanner/checks/ioc.js +234 -0
  78. package/dist/scanner/checks/ioc.js.map +1 -0
  79. package/dist/scanner/checks/ioc.test.d.ts +2 -0
  80. package/dist/scanner/checks/ioc.test.d.ts.map +1 -0
  81. package/dist/scanner/checks/ioc.test.js +298 -0
  82. package/dist/scanner/checks/ioc.test.js.map +1 -0
  83. package/dist/scanner/checks/manifest.d.ts +6 -0
  84. package/dist/scanner/checks/manifest.d.ts.map +1 -0
  85. package/dist/scanner/checks/manifest.js +123 -0
  86. package/dist/scanner/checks/manifest.js.map +1 -0
  87. package/dist/scanner/checks/manifest.test.d.ts +2 -0
  88. package/dist/scanner/checks/manifest.test.d.ts.map +1 -0
  89. package/dist/scanner/checks/manifest.test.js +108 -0
  90. package/dist/scanner/checks/manifest.test.js.map +1 -0
  91. package/dist/scanner/checks/obfuscation.d.ts +3 -0
  92. package/dist/scanner/checks/obfuscation.d.ts.map +1 -0
  93. package/dist/scanner/checks/obfuscation.js +432 -0
  94. package/dist/scanner/checks/obfuscation.js.map +1 -0
  95. package/dist/scanner/checks/obfuscation.test.d.ts +2 -0
  96. package/dist/scanner/checks/obfuscation.test.d.ts.map +1 -0
  97. package/dist/scanner/checks/obfuscation.test.js +399 -0
  98. package/dist/scanner/checks/obfuscation.test.js.map +1 -0
  99. package/dist/scanner/checks/package.d.ts +17 -0
  100. package/dist/scanner/checks/package.d.ts.map +1 -0
  101. package/dist/scanner/checks/package.js +422 -0
  102. package/dist/scanner/checks/package.js.map +1 -0
  103. package/dist/scanner/checks/package.test.d.ts +2 -0
  104. package/dist/scanner/checks/package.test.d.ts.map +1 -0
  105. package/dist/scanner/checks/package.test.js +518 -0
  106. package/dist/scanner/checks/package.test.js.map +1 -0
  107. package/dist/scanner/checks/patterns.d.ts +5 -0
  108. package/dist/scanner/checks/patterns.d.ts.map +1 -0
  109. package/dist/scanner/checks/patterns.js +251 -0
  110. package/dist/scanner/checks/patterns.js.map +1 -0
  111. package/dist/scanner/checks/patterns.test.d.ts +2 -0
  112. package/dist/scanner/checks/patterns.test.d.ts.map +1 -0
  113. package/dist/scanner/checks/patterns.test.js +147 -0
  114. package/dist/scanner/checks/patterns.test.js.map +1 -0
  115. package/dist/scanner/checks/unicode.d.ts +3 -0
  116. package/dist/scanner/checks/unicode.d.ts.map +1 -0
  117. package/dist/scanner/checks/unicode.js +247 -0
  118. package/dist/scanner/checks/unicode.js.map +1 -0
  119. package/dist/scanner/checks/unicode.test.d.ts +2 -0
  120. package/dist/scanner/checks/unicode.test.d.ts.map +1 -0
  121. package/dist/scanner/checks/unicode.test.js +202 -0
  122. package/dist/scanner/checks/unicode.test.js.map +1 -0
  123. package/dist/scanner/checks/yara.d.ts +23 -0
  124. package/dist/scanner/checks/yara.d.ts.map +1 -0
  125. package/dist/scanner/checks/yara.js +349 -0
  126. package/dist/scanner/checks/yara.js.map +1 -0
  127. package/dist/scanner/checks/yara.test.d.ts +2 -0
  128. package/dist/scanner/checks/yara.test.d.ts.map +1 -0
  129. package/dist/scanner/checks/yara.test.js +126 -0
  130. package/dist/scanner/checks/yara.test.js.map +1 -0
  131. package/dist/scanner/constants.d.ts +18 -0
  132. package/dist/scanner/constants.d.ts.map +1 -0
  133. package/dist/scanner/constants.js +37 -0
  134. package/dist/scanner/constants.js.map +1 -0
  135. package/dist/scanner/detection-coverage.test.d.ts +2 -0
  136. package/dist/scanner/detection-coverage.test.d.ts.map +1 -0
  137. package/dist/scanner/detection-coverage.test.js +216 -0
  138. package/dist/scanner/detection-coverage.test.js.map +1 -0
  139. package/dist/scanner/download.d.ts +76 -0
  140. package/dist/scanner/download.d.ts.map +1 -0
  141. package/dist/scanner/download.js +339 -0
  142. package/dist/scanner/download.js.map +1 -0
  143. package/dist/scanner/download.test.d.ts +2 -0
  144. package/dist/scanner/download.test.d.ts.map +1 -0
  145. package/dist/scanner/download.test.js +149 -0
  146. package/dist/scanner/download.test.js.map +1 -0
  147. package/dist/scanner/index.d.ts +8 -0
  148. package/dist/scanner/index.d.ts.map +1 -0
  149. package/dist/scanner/index.js +167 -0
  150. package/dist/scanner/index.js.map +1 -0
  151. package/dist/scanner/index.test.d.ts +2 -0
  152. package/dist/scanner/index.test.d.ts.map +1 -0
  153. package/dist/scanner/index.test.js +71 -0
  154. package/dist/scanner/index.test.js.map +1 -0
  155. package/dist/scanner/loaders/zoo.d.ts +3 -0
  156. package/dist/scanner/loaders/zoo.d.ts.map +1 -0
  157. package/dist/scanner/loaders/zoo.js +112 -0
  158. package/dist/scanner/loaders/zoo.js.map +1 -0
  159. package/dist/scanner/types.d.ts +118 -0
  160. package/dist/scanner/types.d.ts.map +1 -0
  161. package/dist/scanner/types.js +2 -0
  162. package/dist/scanner/types.js.map +1 -0
  163. package/dist/scanner/utils.d.ts +14 -0
  164. package/dist/scanner/utils.d.ts.map +1 -0
  165. package/dist/scanner/utils.js +25 -0
  166. package/dist/scanner/utils.js.map +1 -0
  167. package/dist/scanner/vsix.d.ts +6 -0
  168. package/dist/scanner/vsix.d.ts.map +1 -0
  169. package/dist/scanner/vsix.js +213 -0
  170. package/dist/scanner/vsix.js.map +1 -0
  171. package/dist/scanner/vsix.test.d.ts +2 -0
  172. package/dist/scanner/vsix.test.d.ts.map +1 -0
  173. package/dist/scanner/vsix.test.js +355 -0
  174. package/dist/scanner/vsix.test.js.map +1 -0
  175. package/package.json +60 -0
  176. package/zoo/blocklist/extensions.json +201 -0
  177. package/zoo/iocs/blockchain-extensions.txt +21 -0
  178. package/zoo/iocs/c2-domains.txt +50 -0
  179. package/zoo/iocs/c2-ips.txt +24 -0
  180. package/zoo/iocs/hashes.txt +47 -0
  181. package/zoo/iocs/malicious-npm.txt +85 -0
  182. package/zoo/iocs/wallets.txt +18 -0
  183. package/zoo/signatures/yara/README.md +46 -0
  184. package/zoo/signatures/yara/blockchain_c2.yar +48 -0
  185. package/zoo/signatures/yara/code_execution.yar +165 -0
  186. package/zoo/signatures/yara/credential_harvesting.yar +116 -0
  187. package/zoo/signatures/yara/crypto_wallet_targeting.yar +92 -0
  188. package/zoo/signatures/yara/data_exfiltration.yar +207 -0
  189. package/zoo/signatures/yara/google_calendar_c2.yar +187 -0
  190. package/zoo/signatures/yara/messaging_c2.yar +103 -0
  191. package/zoo/signatures/yara/multi_stage_attacks.yar +331 -0
  192. package/zoo/signatures/yara/obfuscation_patterns.yar +208 -0
  193. package/zoo/signatures/yara/powershell_attacks.yar +116 -0
  194. package/zoo/signatures/yara/rat_capabilities.yar +243 -0
  195. package/zoo/signatures/yara/self_propagation.yar +239 -0
  196. package/zoo/signatures/yara/unicode_stealth.yar +48 -0
  197. 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