@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
package/dist/cli.js
ADDED
|
@@ -0,0 +1,703 @@
|
|
|
1
|
+
import { stat } from "node:fs/promises";
|
|
2
|
+
import { basename } from "node:path";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import pc from "picocolors";
|
|
5
|
+
import { clearCache, getCacheDir, getCachedVersions, listCached } from "./scanner/cache.js";
|
|
6
|
+
import { extractCapabilities } from "./scanner/capabilities.js";
|
|
7
|
+
import { downloadExtension, parseExtensionId } from "./scanner/download.js";
|
|
8
|
+
import { MODULE_NAMES, scanDirectory, scanExtension } from "./scanner/index.js";
|
|
9
|
+
import { loadExtension } from "./scanner/vsix.js";
|
|
10
|
+
const REGISTRIES = ["marketplace", "openvsx", "cursor"];
|
|
11
|
+
/**
|
|
12
|
+
* Strip registry prefix from an extension ID for path-checking purposes
|
|
13
|
+
*/
|
|
14
|
+
function stripRegistryPrefix(target) {
|
|
15
|
+
if (target.startsWith("openvsx:"))
|
|
16
|
+
return target.slice(8);
|
|
17
|
+
if (target.startsWith("marketplace:"))
|
|
18
|
+
return target.slice(12);
|
|
19
|
+
if (target.startsWith("cursor:"))
|
|
20
|
+
return target.slice(7);
|
|
21
|
+
return target;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Check if a target looks like an extension ID vs a local path
|
|
25
|
+
*/
|
|
26
|
+
function isExtensionId(target) {
|
|
27
|
+
// Strip registry prefix for path validation
|
|
28
|
+
const id = stripRegistryPrefix(target);
|
|
29
|
+
// Local paths: start with /, ./, ~, or contain path separators
|
|
30
|
+
if (id.startsWith("/") || id.startsWith("./") || id.startsWith("~")) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
if (id.includes("/") || id.includes("\\")) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
// Extension IDs: publisher.name or publisher.name@version
|
|
37
|
+
try {
|
|
38
|
+
parseExtensionId(target);
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export const cli = new Command()
|
|
46
|
+
.name("vsix-audit")
|
|
47
|
+
.description("Security scanner for VS Code extensions")
|
|
48
|
+
.version("0.1.0");
|
|
49
|
+
cli
|
|
50
|
+
.command("scan")
|
|
51
|
+
.description("Scan a VS Code extension for security issues")
|
|
52
|
+
.argument("<target>", "Path to .vsix file or extension ID (e.g., publisher.extension)")
|
|
53
|
+
.option("-o, --output <format>", "Output format (text, json, sarif)", "text")
|
|
54
|
+
.option("-s, --severity <level>", "Minimum severity to report (low, medium, high, critical)", "low")
|
|
55
|
+
.option("--no-network", "Disable network-based checks")
|
|
56
|
+
.option("--all-registries", "Scan from all registries (Marketplace + OpenVSX + Cursor)")
|
|
57
|
+
.option("--no-cache", "Bypass cache, download fresh")
|
|
58
|
+
.option("--force", "Re-download even if cached")
|
|
59
|
+
.option("-r, --recursive", "Recursively scan all .vsix files in a directory")
|
|
60
|
+
.option("-j, --jobs <n>", "Number of parallel scans (default: 4)", "4")
|
|
61
|
+
.option("-m, --module <names>", "Run only specific modules (comma-separated: package,obfuscation,ast,ioc,yara)")
|
|
62
|
+
.option("--profile", "Show detailed timing breakdown for each module")
|
|
63
|
+
.action(async (target, options) => {
|
|
64
|
+
try {
|
|
65
|
+
const useCache = options.noCache !== true;
|
|
66
|
+
const forceDownload = options.force === true;
|
|
67
|
+
// Parse and validate module filter
|
|
68
|
+
const modules = options.module?.split(",").map((m) => m.trim());
|
|
69
|
+
if (modules) {
|
|
70
|
+
const invalid = modules.filter((m) => !MODULE_NAMES.includes(m));
|
|
71
|
+
if (invalid.length > 0) {
|
|
72
|
+
console.error(pc.red("Error:"), `Unknown module(s): ${invalid.join(", ")}. Valid modules: ${MODULE_NAMES.join(", ")}`);
|
|
73
|
+
process.exit(2);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const scanOptions = {
|
|
77
|
+
output: options.output,
|
|
78
|
+
severity: options.severity,
|
|
79
|
+
network: options.network,
|
|
80
|
+
...(options.profile ? { profile: true } : {}),
|
|
81
|
+
...(modules ? { modules } : {}),
|
|
82
|
+
};
|
|
83
|
+
// Handle --all-registries mode for extension IDs
|
|
84
|
+
if (options.allRegistries && isExtensionId(target)) {
|
|
85
|
+
const results = [];
|
|
86
|
+
const baseId = stripRegistryPrefix(target);
|
|
87
|
+
for (const registry of REGISTRIES) {
|
|
88
|
+
const prefixedId = `${registry}:${baseId}`;
|
|
89
|
+
try {
|
|
90
|
+
console.log(pc.cyan(`Downloading from ${registry}:`), baseId);
|
|
91
|
+
const downloaded = await downloadExtension(prefixedId, {
|
|
92
|
+
useCache,
|
|
93
|
+
forceDownload,
|
|
94
|
+
});
|
|
95
|
+
if (downloaded.fromCache) {
|
|
96
|
+
console.log(pc.green("✓ Using cached"), pc.dim(downloaded.path));
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
console.log(pc.green("✓ Downloaded"), pc.dim(downloaded.path));
|
|
100
|
+
}
|
|
101
|
+
const result = await scanExtension(downloaded.path, scanOptions);
|
|
102
|
+
result.metadata = { ...result.metadata, registry };
|
|
103
|
+
results.push(result);
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
// Extension may not exist in this registry - continue
|
|
107
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
108
|
+
console.log(pc.dim(` Not found in ${registry}: ${msg}`));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
console.log();
|
|
112
|
+
if (results.length === 0) {
|
|
113
|
+
console.error(pc.red("Error:"), `Extension not found in any registry: ${baseId}`);
|
|
114
|
+
process.exit(2);
|
|
115
|
+
}
|
|
116
|
+
// Output results
|
|
117
|
+
if (options.output === "json") {
|
|
118
|
+
console.log(JSON.stringify(results, null, 2));
|
|
119
|
+
}
|
|
120
|
+
else if (options.output === "sarif") {
|
|
121
|
+
// Combine SARIF results
|
|
122
|
+
const combined = {
|
|
123
|
+
$schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
|
|
124
|
+
version: "2.1.0",
|
|
125
|
+
runs: results.map((r) => toSarif(r).runs[0]),
|
|
126
|
+
};
|
|
127
|
+
console.log(JSON.stringify(combined, null, 2));
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
for (const result of results) {
|
|
131
|
+
const registry = result.metadata.registry ?? "unknown";
|
|
132
|
+
console.log(pc.bold(`Registry: ${registry}`));
|
|
133
|
+
printTextReport(result);
|
|
134
|
+
console.log();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const hasFindings = results.some((r) => r.findings.length > 0);
|
|
138
|
+
process.exit(hasFindings ? 1 : 0);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
// Warn if -j used without -r
|
|
142
|
+
if (options.jobs && options.jobs !== "4" && !options.recursive) {
|
|
143
|
+
console.log(pc.yellow("Warning:"), "--jobs is only used with --recursive, ignoring");
|
|
144
|
+
}
|
|
145
|
+
// Handle --recursive mode for directories
|
|
146
|
+
if (options.recursive) {
|
|
147
|
+
const targetStat = await stat(target).catch(() => null);
|
|
148
|
+
if (!targetStat?.isDirectory()) {
|
|
149
|
+
console.error(pc.red("Error:"), "--recursive requires a directory path");
|
|
150
|
+
process.exit(2);
|
|
151
|
+
}
|
|
152
|
+
// Validate --jobs
|
|
153
|
+
const concurrency = parseInt(options.jobs ?? "4", 10);
|
|
154
|
+
if (isNaN(concurrency) || concurrency < 1) {
|
|
155
|
+
console.error(pc.red("Error:"), "--jobs must be a positive integer");
|
|
156
|
+
process.exit(2);
|
|
157
|
+
}
|
|
158
|
+
const isParallel = concurrency > 1;
|
|
159
|
+
console.log(pc.cyan("Scanning directory:"), target);
|
|
160
|
+
if (isParallel) {
|
|
161
|
+
console.log(pc.dim(`Parallel mode: ${concurrency} concurrent scans`));
|
|
162
|
+
}
|
|
163
|
+
console.log();
|
|
164
|
+
const batchResult = await scanDirectory(target, scanOptions, {
|
|
165
|
+
onProgress: (completed, total, _path) => {
|
|
166
|
+
if (isParallel) {
|
|
167
|
+
// No line clearing in parallel mode - just update progress
|
|
168
|
+
process.stderr.write(`\r[${completed}/${total}] Scanning...`);
|
|
169
|
+
}
|
|
170
|
+
// Sequential mode: progress shown via onResult
|
|
171
|
+
},
|
|
172
|
+
onResult: (_path, result) => {
|
|
173
|
+
if (isParallel) {
|
|
174
|
+
// Clear progress line before printing result
|
|
175
|
+
process.stderr.write("\r\x1b[K");
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
process.stdout.write("\r\x1b[K");
|
|
179
|
+
}
|
|
180
|
+
const count = result.findings.length;
|
|
181
|
+
if (count === 0) {
|
|
182
|
+
console.log(`[${pc.green("OK")}] ${result.extension.name} v${result.extension.version}`);
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
const summary = formatFindingSummary(result.findings);
|
|
186
|
+
console.log(`[${pc.yellow("WARN")}] ${result.extension.name} v${result.extension.version} - ${count} issue(s) (${summary})`);
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
onError: (path, error) => {
|
|
190
|
+
if (isParallel) {
|
|
191
|
+
process.stderr.write("\r\x1b[K");
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
process.stdout.write("\r\x1b[K");
|
|
195
|
+
}
|
|
196
|
+
console.log(`[${pc.red("ERROR")}] ${basename(path)} - Error: ${error}`);
|
|
197
|
+
},
|
|
198
|
+
}, { concurrency });
|
|
199
|
+
// Output results
|
|
200
|
+
if (options.output === "json") {
|
|
201
|
+
console.log(JSON.stringify(batchResult, null, 2));
|
|
202
|
+
}
|
|
203
|
+
else if (options.output === "sarif") {
|
|
204
|
+
const combined = {
|
|
205
|
+
$schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
|
|
206
|
+
version: "2.1.0",
|
|
207
|
+
runs: batchResult.results.map((r) => toSarif(r).runs[0]),
|
|
208
|
+
};
|
|
209
|
+
console.log(JSON.stringify(combined, null, 2));
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
printBatchSummary(batchResult);
|
|
213
|
+
}
|
|
214
|
+
// Exit codes: 0=clean, 1=findings, 2=errors only
|
|
215
|
+
if (batchResult.summary.totalFindings > 0) {
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
else if (batchResult.summary.failedFiles > 0 && batchResult.summary.scannedFiles === 0) {
|
|
219
|
+
process.exit(2);
|
|
220
|
+
}
|
|
221
|
+
process.exit(0);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
// Standard single-registry scan
|
|
225
|
+
let scanTarget = target;
|
|
226
|
+
if (isExtensionId(target)) {
|
|
227
|
+
console.log(pc.cyan("Downloading:"), target);
|
|
228
|
+
const result = await downloadExtension(target, {
|
|
229
|
+
useCache,
|
|
230
|
+
forceDownload,
|
|
231
|
+
});
|
|
232
|
+
scanTarget = result.path;
|
|
233
|
+
if (result.fromCache) {
|
|
234
|
+
console.log(pc.green("✓ Using cached"), pc.dim(result.path));
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
console.log(pc.green("✓ Downloaded"), pc.dim(result.path));
|
|
238
|
+
}
|
|
239
|
+
console.log();
|
|
240
|
+
}
|
|
241
|
+
const result = await scanExtension(scanTarget, scanOptions);
|
|
242
|
+
if (scanOptions.output === "json") {
|
|
243
|
+
console.log(JSON.stringify(result, null, 2));
|
|
244
|
+
}
|
|
245
|
+
else if (scanOptions.output === "sarif") {
|
|
246
|
+
console.log(JSON.stringify(toSarif(result), null, 2));
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
printTextReport(result);
|
|
250
|
+
}
|
|
251
|
+
process.exit(result.findings.length > 0 ? 1 : 0);
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
console.error(pc.red("Error:"), error instanceof Error ? error.message : error);
|
|
255
|
+
process.exit(2);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
cli
|
|
259
|
+
.command("download")
|
|
260
|
+
.description("Download a VS Code extension from the marketplace")
|
|
261
|
+
.argument("<extension-id>", "Extension ID (e.g., ms-python.python or ms-python.python@2024.1.0)")
|
|
262
|
+
.option("-o, --output <dir>", "Also copy to this directory (in addition to cache)")
|
|
263
|
+
.option("--no-cache", "Bypass cache, download fresh")
|
|
264
|
+
.option("--force", "Re-download even if cached")
|
|
265
|
+
.action(async (extensionId, options) => {
|
|
266
|
+
try {
|
|
267
|
+
const useCache = options.noCache !== true;
|
|
268
|
+
const forceDownload = options.force === true;
|
|
269
|
+
console.log(pc.cyan("Downloading:"), extensionId);
|
|
270
|
+
const downloadOptions = {
|
|
271
|
+
useCache,
|
|
272
|
+
forceDownload,
|
|
273
|
+
...(options.output ? { destDir: options.output } : {}),
|
|
274
|
+
};
|
|
275
|
+
const result = await downloadExtension(extensionId, downloadOptions);
|
|
276
|
+
console.log();
|
|
277
|
+
if (result.fromCache) {
|
|
278
|
+
console.log(pc.green("✓ Using cached version"));
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
console.log(pc.green("✓ Downloaded successfully"));
|
|
282
|
+
}
|
|
283
|
+
console.log(pc.dim("─".repeat(50)));
|
|
284
|
+
console.log(`${pc.cyan("Name:")} ${result.metadata.displayName ?? result.metadata.name}`);
|
|
285
|
+
console.log(`${pc.cyan("Publisher:")} ${result.metadata.publisher}`);
|
|
286
|
+
console.log(`${pc.cyan("Version:")} ${result.metadata.version}`);
|
|
287
|
+
if (result.metadata.installCount) {
|
|
288
|
+
console.log(`${pc.cyan("Installs:")} ${result.metadata.installCount.toLocaleString()}`);
|
|
289
|
+
}
|
|
290
|
+
console.log(`${pc.cyan("Path:")} ${result.path}`);
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
console.error(pc.red("Error:"), error instanceof Error ? error.message : error);
|
|
294
|
+
process.exit(2);
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
cli
|
|
298
|
+
.command("info")
|
|
299
|
+
.description("Display metadata and capabilities of a VS Code extension")
|
|
300
|
+
.argument("<target>", "Path to .vsix file, directory, or extension ID (e.g., publisher.extension)")
|
|
301
|
+
.option("-v, --verbose", "Show detailed evidence for each capability")
|
|
302
|
+
.action(async (target, options) => {
|
|
303
|
+
try {
|
|
304
|
+
let infoTarget = target;
|
|
305
|
+
if (isExtensionId(target)) {
|
|
306
|
+
console.log(pc.cyan("Downloading:"), target);
|
|
307
|
+
const result = await downloadExtension(target);
|
|
308
|
+
infoTarget = result.path;
|
|
309
|
+
if (result.fromCache) {
|
|
310
|
+
console.log(pc.green("✓ Using cached"), pc.dim(result.path));
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
console.log(pc.green("✓ Downloaded"), pc.dim(result.path));
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
const contents = await loadExtension(infoTarget);
|
|
317
|
+
const manifest = contents.manifest;
|
|
318
|
+
// Run scanner to get findings for capability extraction
|
|
319
|
+
const scanResult = await scanExtension(infoTarget, {
|
|
320
|
+
output: "text",
|
|
321
|
+
severity: "low",
|
|
322
|
+
network: true,
|
|
323
|
+
});
|
|
324
|
+
const capabilities = extractCapabilities(scanResult.findings);
|
|
325
|
+
console.log();
|
|
326
|
+
console.log(pc.bold(`Extension: ${manifest.displayName ?? manifest.name} v${manifest.version}`));
|
|
327
|
+
console.log(`${pc.cyan("Publisher:")} ${manifest.publisher}`);
|
|
328
|
+
if (manifest.description) {
|
|
329
|
+
console.log(`${pc.dim(manifest.description)}`);
|
|
330
|
+
}
|
|
331
|
+
// Capabilities section
|
|
332
|
+
console.log();
|
|
333
|
+
console.log(pc.dim("── Capabilities ") + pc.dim("─".repeat(34)));
|
|
334
|
+
printCapabilities(capabilities, options.verbose ?? false);
|
|
335
|
+
// Manifest section
|
|
336
|
+
console.log();
|
|
337
|
+
console.log(pc.dim("── Manifest ") + pc.dim("─".repeat(38)));
|
|
338
|
+
// Activation events
|
|
339
|
+
const events = manifest.activationEvents ?? [];
|
|
340
|
+
console.log(`${pc.cyan("Activation:".padEnd(16))}${events.length > 0 ? events.join(", ") : pc.dim("(none)")}`);
|
|
341
|
+
// Entry points
|
|
342
|
+
const entryPoints = [];
|
|
343
|
+
if (manifest.main)
|
|
344
|
+
entryPoints.push(`main: ${manifest.main}`);
|
|
345
|
+
if (manifest.browser)
|
|
346
|
+
entryPoints.push(`browser: ${manifest.browser}`);
|
|
347
|
+
if (entryPoints.length > 0) {
|
|
348
|
+
console.log(`${pc.cyan("Entry Points:".padEnd(16))}${entryPoints.join(", ")}`);
|
|
349
|
+
}
|
|
350
|
+
// Contributions summary
|
|
351
|
+
const contributes = manifest.contributes ?? {};
|
|
352
|
+
const contributionSummary = Object.entries(contributes)
|
|
353
|
+
.filter(([, val]) => (Array.isArray(val) ? val.length > 0 : val !== undefined))
|
|
354
|
+
.map(([key, val]) => {
|
|
355
|
+
const count = Array.isArray(val) ? val.length : 1;
|
|
356
|
+
return `${key} (${count})`;
|
|
357
|
+
});
|
|
358
|
+
if (contributionSummary.length > 0) {
|
|
359
|
+
console.log(`${pc.cyan("Contributes:".padEnd(16))}${contributionSummary.join(", ")}`);
|
|
360
|
+
}
|
|
361
|
+
// Dependencies
|
|
362
|
+
const deps = manifest["extensionDependencies"];
|
|
363
|
+
if (deps && deps.length > 0) {
|
|
364
|
+
console.log(`${pc.cyan("Dependencies:".padEnd(16))}${deps.join(", ")}`);
|
|
365
|
+
}
|
|
366
|
+
// Stats section
|
|
367
|
+
console.log();
|
|
368
|
+
console.log(pc.dim("── Stats ") + pc.dim("─".repeat(41)));
|
|
369
|
+
const codeExtensions = [".js", ".ts", ".mjs", ".cjs"];
|
|
370
|
+
const codeFileCount = [...contents.files.keys()].filter((f) => codeExtensions.some((ext) => f.endsWith(ext))).length;
|
|
371
|
+
console.log(`${pc.cyan("Files:".padEnd(16))}${contents.files.size} (${codeFileCount} code)`);
|
|
372
|
+
const totalSize = [...contents.files.values()].reduce((sum, buf) => sum + buf.length, 0);
|
|
373
|
+
console.log(`${pc.cyan("Size:".padEnd(16))}${formatBytes(totalSize)}`);
|
|
374
|
+
console.log();
|
|
375
|
+
}
|
|
376
|
+
catch (error) {
|
|
377
|
+
console.error(pc.red("Error:"), error instanceof Error ? error.message : error);
|
|
378
|
+
process.exit(2);
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
function printCapabilities(capabilities, verbose) {
|
|
382
|
+
const capEntries = [
|
|
383
|
+
["Network", "network"],
|
|
384
|
+
["Execution", "execution"],
|
|
385
|
+
["File Access", "fileAccess"],
|
|
386
|
+
["Credentials", "credentials"],
|
|
387
|
+
["Obfuscation", "obfuscation"],
|
|
388
|
+
];
|
|
389
|
+
for (const [label, key] of capEntries) {
|
|
390
|
+
const cap = capabilities[key];
|
|
391
|
+
const icon = cap.detected ? pc.green("✓") : pc.dim("✗");
|
|
392
|
+
const summary = cap.detected ? cap.summary.join(", ") : pc.dim("None detected");
|
|
393
|
+
console.log(`${label.padEnd(16)}${icon} ${summary}`);
|
|
394
|
+
if (verbose && cap.detected && cap.evidence.length > 0) {
|
|
395
|
+
for (const ev of cap.evidence.slice(0, 5)) {
|
|
396
|
+
const loc = ev.line ? `${ev.file}:${ev.line}` : ev.file;
|
|
397
|
+
const matched = ev.matched ? pc.dim(` (${ev.matched.slice(0, 40)})`) : "";
|
|
398
|
+
console.log(` ${pc.dim("└")} ${pc.dim(loc)}${matched}`);
|
|
399
|
+
}
|
|
400
|
+
if (cap.evidence.length > 5) {
|
|
401
|
+
console.log(` ${pc.dim(`... and ${cap.evidence.length - 5} more`)}`);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
// Cache subcommand
|
|
407
|
+
const cacheCommand = cli.command("cache").description("Manage the extension cache");
|
|
408
|
+
cacheCommand
|
|
409
|
+
.command("path")
|
|
410
|
+
.description("Print the cache directory path")
|
|
411
|
+
.action(() => {
|
|
412
|
+
console.log(getCacheDir());
|
|
413
|
+
});
|
|
414
|
+
cacheCommand
|
|
415
|
+
.command("list")
|
|
416
|
+
.description("List cached extensions")
|
|
417
|
+
.option("--json", "Output as JSON")
|
|
418
|
+
.action(async (options) => {
|
|
419
|
+
try {
|
|
420
|
+
const extensions = await listCached();
|
|
421
|
+
if (extensions.length === 0) {
|
|
422
|
+
if (options.json) {
|
|
423
|
+
console.log("[]");
|
|
424
|
+
}
|
|
425
|
+
else {
|
|
426
|
+
console.log(pc.dim("Cache is empty"));
|
|
427
|
+
}
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
if (options.json) {
|
|
431
|
+
console.log(JSON.stringify(extensions, null, 2));
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
console.log();
|
|
435
|
+
console.log(pc.bold("Cached Extensions"));
|
|
436
|
+
console.log(pc.dim("─".repeat(70)));
|
|
437
|
+
for (const ext of extensions) {
|
|
438
|
+
const extensionId = `${ext.publisher}.${ext.name}`;
|
|
439
|
+
const cachedDate = ext.cachedAt.toLocaleDateString();
|
|
440
|
+
console.log(` ${pc.cyan(extensionId.padEnd(40))} ` +
|
|
441
|
+
`${pc.dim(ext.version.padEnd(12))} ` +
|
|
442
|
+
`${pc.dim(formatBytes(ext.size).padEnd(10))} ` +
|
|
443
|
+
`${pc.dim(ext.registry.padEnd(12))} ` +
|
|
444
|
+
`${pc.dim(cachedDate)}`);
|
|
445
|
+
}
|
|
446
|
+
console.log();
|
|
447
|
+
const totalSize = extensions.reduce((sum, ext) => sum + ext.size, 0);
|
|
448
|
+
console.log(`${pc.cyan("Total:")} ${extensions.length} extensions, ${formatBytes(totalSize)}`);
|
|
449
|
+
}
|
|
450
|
+
catch (error) {
|
|
451
|
+
console.error(pc.red("Error:"), error instanceof Error ? error.message : error);
|
|
452
|
+
process.exit(2);
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
cacheCommand
|
|
456
|
+
.command("clear")
|
|
457
|
+
.description("Clear cached extensions")
|
|
458
|
+
.argument("[pattern]", "Optional glob pattern (e.g., ms-python.* or *.python)")
|
|
459
|
+
.action(async (pattern) => {
|
|
460
|
+
try {
|
|
461
|
+
const deleted = await clearCache(pattern);
|
|
462
|
+
if (deleted === 0) {
|
|
463
|
+
if (pattern) {
|
|
464
|
+
console.log(pc.dim(`No extensions matching "${pattern}" found in cache`));
|
|
465
|
+
}
|
|
466
|
+
else {
|
|
467
|
+
console.log(pc.dim("Cache is already empty"));
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
console.log(pc.green(`✓ Cleared ${deleted} extension(s) from cache`));
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
catch (error) {
|
|
475
|
+
console.error(pc.red("Error:"), error instanceof Error ? error.message : error);
|
|
476
|
+
process.exit(2);
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
cacheCommand
|
|
480
|
+
.command("info")
|
|
481
|
+
.description("Show cached versions of an extension")
|
|
482
|
+
.argument("<extension-id>", "Extension ID (e.g., ms-python.python)")
|
|
483
|
+
.action(async (extensionId) => {
|
|
484
|
+
try {
|
|
485
|
+
const { publisher, name } = parseExtensionId(extensionId);
|
|
486
|
+
const versions = await getCachedVersions(publisher, name);
|
|
487
|
+
if (versions.length === 0) {
|
|
488
|
+
console.log(pc.dim(`No cached versions of ${extensionId}`));
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
console.log();
|
|
492
|
+
console.log(pc.bold(`Cached versions of ${extensionId}`));
|
|
493
|
+
console.log(pc.dim("─".repeat(50)));
|
|
494
|
+
for (const ext of versions) {
|
|
495
|
+
const cachedDate = ext.cachedAt.toLocaleDateString();
|
|
496
|
+
console.log(` ${pc.cyan(ext.version.padEnd(15))} ` +
|
|
497
|
+
`${pc.dim(ext.registry.padEnd(12))} ` +
|
|
498
|
+
`${pc.dim(formatBytes(ext.size).padEnd(10))} ` +
|
|
499
|
+
`${pc.dim(cachedDate)}`);
|
|
500
|
+
}
|
|
501
|
+
console.log();
|
|
502
|
+
}
|
|
503
|
+
catch (error) {
|
|
504
|
+
console.error(pc.red("Error:"), error instanceof Error ? error.message : error);
|
|
505
|
+
process.exit(2);
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
function formatBytes(bytes) {
|
|
509
|
+
if (bytes < 1024)
|
|
510
|
+
return `${bytes} B`;
|
|
511
|
+
if (bytes < 1024 * 1024)
|
|
512
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
513
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
514
|
+
}
|
|
515
|
+
function formatMs(ms) {
|
|
516
|
+
if (ms < 1000)
|
|
517
|
+
return `${ms.toFixed(0)}ms`;
|
|
518
|
+
return `${(ms / 1000).toFixed(2)}s`;
|
|
519
|
+
}
|
|
520
|
+
function printTimings(timings) {
|
|
521
|
+
console.log(pc.cyan("Module timings:"));
|
|
522
|
+
const entries = [
|
|
523
|
+
["Load", timings.load],
|
|
524
|
+
...(timings.package !== undefined ? [["Package", timings.package]] : []),
|
|
525
|
+
...(timings.obfuscation !== undefined
|
|
526
|
+
? [["Obfuscation", timings.obfuscation]]
|
|
527
|
+
: []),
|
|
528
|
+
...(timings.ast !== undefined ? [["AST", timings.ast]] : []),
|
|
529
|
+
...(timings.ioc !== undefined ? [["IOC", timings.ioc]] : []),
|
|
530
|
+
...(timings.yara !== undefined ? [["YARA", timings.yara]] : []),
|
|
531
|
+
];
|
|
532
|
+
// Find longest module name for alignment
|
|
533
|
+
const maxNameLen = Math.max(...entries.map(([name]) => name.length));
|
|
534
|
+
// Calculate percentages and find max for bar scaling
|
|
535
|
+
const moduleTotal = entries.reduce((sum, [, ms]) => sum + ms, 0);
|
|
536
|
+
for (const [name, ms] of entries) {
|
|
537
|
+
const pct = moduleTotal > 0 ? (ms / moduleTotal) * 100 : 0;
|
|
538
|
+
const barLen = Math.round(pct / 2); // Scale to ~50 char max
|
|
539
|
+
const bar = "█".repeat(barLen);
|
|
540
|
+
const padded = name.padEnd(maxNameLen);
|
|
541
|
+
const timeStr = formatMs(ms).padStart(8);
|
|
542
|
+
const pctStr = `${pct.toFixed(1)}%`.padStart(6);
|
|
543
|
+
console.log(` ${pc.bold(padded)} ${timeStr} ${pctStr} ${pc.dim(bar)}`);
|
|
544
|
+
}
|
|
545
|
+
console.log(` ${"─".repeat(maxNameLen + 20)}`);
|
|
546
|
+
console.log(` ${pc.bold("Total".padEnd(maxNameLen))} ${formatMs(timings.total).padStart(8)}`);
|
|
547
|
+
console.log();
|
|
548
|
+
}
|
|
549
|
+
function printTextReport(result) {
|
|
550
|
+
console.log();
|
|
551
|
+
console.log(pc.bold("vsix-audit scan results"));
|
|
552
|
+
console.log(pc.dim("─".repeat(50)));
|
|
553
|
+
console.log();
|
|
554
|
+
console.log(`${pc.cyan("Extension:")} ${result.extension.name} v${result.extension.version}`);
|
|
555
|
+
console.log(`${pc.cyan("Publisher:")} ${result.extension.publisher}`);
|
|
556
|
+
console.log(`${pc.cyan("Scanned:")} ${result.metadata.scannedAt}`);
|
|
557
|
+
console.log();
|
|
558
|
+
// Print inventory of checks performed
|
|
559
|
+
if (result.inventory && result.inventory.length > 0) {
|
|
560
|
+
console.log(pc.cyan("Checks performed:"));
|
|
561
|
+
for (const check of result.inventory) {
|
|
562
|
+
if (check.enabled) {
|
|
563
|
+
console.log(` ${pc.green("✓")} ${pc.bold(check.name.padEnd(14))}${check.description}`);
|
|
564
|
+
}
|
|
565
|
+
else {
|
|
566
|
+
console.log(` ${pc.yellow("⚠")} ${pc.bold(check.name.padEnd(14))}${pc.dim(`Skipped (${check.skipReason})`)}`);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
console.log();
|
|
570
|
+
}
|
|
571
|
+
// Print timings if profiling enabled
|
|
572
|
+
if (result.metadata.timings) {
|
|
573
|
+
printTimings(result.metadata.timings);
|
|
574
|
+
}
|
|
575
|
+
if (result.findings.length === 0) {
|
|
576
|
+
console.log(pc.green("✓ No security issues found"));
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
console.log(pc.yellow(`Found ${result.findings.length} issue(s):`));
|
|
580
|
+
console.log();
|
|
581
|
+
for (const finding of result.findings) {
|
|
582
|
+
const severityColor = {
|
|
583
|
+
critical: pc.red,
|
|
584
|
+
high: pc.red,
|
|
585
|
+
medium: pc.yellow,
|
|
586
|
+
low: pc.blue,
|
|
587
|
+
}[finding.severity];
|
|
588
|
+
console.log(` ${severityColor(`[${finding.severity.toUpperCase()}]`)} ${finding.title}`);
|
|
589
|
+
console.log(` ${pc.dim(finding.description)}`);
|
|
590
|
+
if (finding.location) {
|
|
591
|
+
console.log(` ${pc.dim(`at ${finding.location.file}${finding.location.line ? `:${finding.location.line}` : ""}`)}`);
|
|
592
|
+
}
|
|
593
|
+
console.log();
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
function toSarif(result) {
|
|
597
|
+
return {
|
|
598
|
+
$schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
|
|
599
|
+
version: "2.1.0",
|
|
600
|
+
runs: [
|
|
601
|
+
{
|
|
602
|
+
tool: {
|
|
603
|
+
driver: {
|
|
604
|
+
name: "vsix-audit",
|
|
605
|
+
version: "0.1.0",
|
|
606
|
+
informationUri: "https://github.com/trailofbits/vsix-audit",
|
|
607
|
+
},
|
|
608
|
+
},
|
|
609
|
+
results: result.findings.map((f) => ({
|
|
610
|
+
ruleId: f.id,
|
|
611
|
+
level: f.severity === "critical" || f.severity === "high" ? "error" : "warning",
|
|
612
|
+
message: { text: f.description },
|
|
613
|
+
locations: f.location
|
|
614
|
+
? [
|
|
615
|
+
{
|
|
616
|
+
physicalLocation: {
|
|
617
|
+
artifactLocation: { uri: f.location.file },
|
|
618
|
+
region: f.location.line ? { startLine: f.location.line } : undefined,
|
|
619
|
+
},
|
|
620
|
+
},
|
|
621
|
+
]
|
|
622
|
+
: [],
|
|
623
|
+
})),
|
|
624
|
+
},
|
|
625
|
+
],
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
function formatFindingSummary(findings) {
|
|
629
|
+
const counts = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
630
|
+
for (const f of findings) {
|
|
631
|
+
counts[f.severity]++;
|
|
632
|
+
}
|
|
633
|
+
const parts = [];
|
|
634
|
+
if (counts.critical > 0)
|
|
635
|
+
parts.push(`${counts.critical} critical`);
|
|
636
|
+
if (counts.high > 0)
|
|
637
|
+
parts.push(`${counts.high} high`);
|
|
638
|
+
if (counts.medium > 0)
|
|
639
|
+
parts.push(`${counts.medium} medium`);
|
|
640
|
+
if (counts.low > 0)
|
|
641
|
+
parts.push(`${counts.low} low`);
|
|
642
|
+
return parts.join(", ");
|
|
643
|
+
}
|
|
644
|
+
function printBatchSummary(batch) {
|
|
645
|
+
const { summary, results, errors } = batch;
|
|
646
|
+
console.log();
|
|
647
|
+
console.log(pc.bold("Batch Scan Summary"));
|
|
648
|
+
console.log(pc.dim("─".repeat(50)));
|
|
649
|
+
console.log();
|
|
650
|
+
console.log(`Files scanned: ${summary.scannedFiles}/${summary.totalFiles}`);
|
|
651
|
+
if (summary.failedFiles > 0) {
|
|
652
|
+
console.log(`Failed: ${pc.red(String(summary.failedFiles))}`);
|
|
653
|
+
}
|
|
654
|
+
console.log(`Duration: ${(summary.scanDuration / 1000).toFixed(1)}s`);
|
|
655
|
+
console.log();
|
|
656
|
+
if (summary.totalFindings === 0) {
|
|
657
|
+
console.log(pc.green("No issues found across all extensions"));
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
console.log(pc.yellow(`Found ${summary.totalFindings} issue(s) across all extensions:`));
|
|
661
|
+
const sev = summary.findingsBySeverity;
|
|
662
|
+
if (sev.critical > 0)
|
|
663
|
+
console.log(` ${pc.red("Critical:")} ${sev.critical}`);
|
|
664
|
+
if (sev.high > 0)
|
|
665
|
+
console.log(` ${pc.red("High:")} ${sev.high}`);
|
|
666
|
+
if (sev.medium > 0)
|
|
667
|
+
console.log(` ${pc.yellow("Medium:")} ${sev.medium}`);
|
|
668
|
+
if (sev.low > 0)
|
|
669
|
+
console.log(` ${pc.blue("Low:")} ${sev.low}`);
|
|
670
|
+
console.log();
|
|
671
|
+
const withFindings = results.filter((r) => r.findings.length > 0);
|
|
672
|
+
if (withFindings.length > 0) {
|
|
673
|
+
console.log(pc.cyan("Extensions with findings:"));
|
|
674
|
+
console.log();
|
|
675
|
+
for (const r of withFindings) {
|
|
676
|
+
console.log(pc.bold(`${r.extension.name} v${r.extension.version}`));
|
|
677
|
+
console.log(pc.dim("─".repeat(40)));
|
|
678
|
+
for (const finding of r.findings) {
|
|
679
|
+
const severityColor = {
|
|
680
|
+
critical: pc.red,
|
|
681
|
+
high: pc.red,
|
|
682
|
+
medium: pc.yellow,
|
|
683
|
+
low: pc.blue,
|
|
684
|
+
}[finding.severity];
|
|
685
|
+
console.log(` ${severityColor(`[${finding.severity.toUpperCase()}]`)} ${finding.title}`);
|
|
686
|
+
console.log(` ${pc.dim(finding.description)}`);
|
|
687
|
+
if (finding.location) {
|
|
688
|
+
console.log(` ${pc.dim(`at ${finding.location.file}${finding.location.line ? `:${finding.location.line}` : ""}`)}`);
|
|
689
|
+
}
|
|
690
|
+
console.log();
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
if (errors.length > 0) {
|
|
695
|
+
console.log();
|
|
696
|
+
console.log(pc.cyan("Failed files:"));
|
|
697
|
+
for (const e of errors) {
|
|
698
|
+
console.log(` - ${e.path}`);
|
|
699
|
+
console.log(` ${pc.dim(e.error)}`);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
//# sourceMappingURL=cli.js.map
|