From 73c7c6812424d9b9bc3d185472dc11667ac9a368 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Jun 2025 20:30:34 +0000 Subject: [PATCH 1/3] Initial plan From 44796399fc3a0646d0cbbe07b7032c3c48f46095 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Jun 2025 20:43:25 +0000 Subject: [PATCH 2/3] Implement CodeQL discovery and auto-install feature Co-authored-by: felickz <1760475+felickz@users.noreply.github.com> --- package.json | 11 + src/extension.ts | 37 ++++ src/services/codeqlService.ts | 403 ++++++++++++++++++++++++++++++++-- 3 files changed, 434 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index a994608..77211b7 100644 --- a/package.json +++ b/package.json @@ -74,6 +74,11 @@ "title": "CodeQL: Show CLI Information", "category": "CodeQL Scanner" }, + { + "command": "codeql-scanner.installCodeQL", + "title": "CodeQL: Install/Update CLI", + "category": "CodeQL Scanner" + }, { "command": "codeql-scanner.copyFlowPath", "title": "CodeQL: Copy Flow Path", @@ -158,6 +163,12 @@ "description": "Automatically detect and use CodeQL CLI from GitHub.vscode-codeql extension if available", "scope": "application" }, + "codeql-scanner.autoInstallCodeQL": { + "type": "boolean", + "default": true, + "description": "Automatically download and install CodeQL CLI from GitHub if not found", + "scope": "application" + }, "codeql-scanner.useLocalScan": { "type": "boolean", "default": true, diff --git a/src/extension.ts b/src/extension.ts index 826731e..82a6df5 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -69,6 +69,43 @@ export async function activate(context: vscode.ExtensionContext) { resultsProvider.clearResults(); vscode.window.showInformationMessage('CodeQL diagnostics cleared.'); }), + vscode.commands.registerCommand('codeql-scanner.showCodeQLInfo', async () => { + try { + const version = await codeqlService.getVersion(); + const config = vscode.workspace.getConfiguration("codeql-scanner"); + const codeqlPath = config.get("codeqlPath", "codeql"); + + vscode.window.showInformationMessage( + `CodeQL CLI Info:\nVersion: ${version}\nPath: ${codeqlPath}`, + { modal: true } + ); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + vscode.window.showErrorMessage(`CodeQL CLI Info: ${errorMessage}`); + } + }), + vscode.commands.registerCommand('codeql-scanner.installCodeQL', async () => { + try { + await vscode.window.withProgress({ + location: vscode.ProgressLocation.Notification, + title: "Installing CodeQL CLI", + cancellable: true + }, async (progress, token) => { + // Force installation + const releaseInfo = await codeqlService.getLatestCodeQLRelease(); + const installedPath = await codeqlService.downloadAndInstallCodeQL(releaseInfo, progress, token); + + if (installedPath) { + const config = vscode.workspace.getConfiguration("codeql-scanner"); + await config.update("codeqlPath", installedPath, vscode.ConfigurationTarget.Global); + vscode.window.showInformationMessage(`CodeQL CLI installed successfully at: ${installedPath}`); + } + }); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + vscode.window.showErrorMessage(`Failed to install CodeQL CLI: ${errorMessage}`); + } + }), vscode.commands.registerCommand('codeql-scanner.copyFlowPath', async (item) => { if (item && item.result && item.result.flowSteps) { const flowPath = item.result.flowSteps.map((step: any, index: number) => { diff --git a/src/services/codeqlService.ts b/src/services/codeqlService.ts index f34edb3..c280996 100644 --- a/src/services/codeqlService.ts +++ b/src/services/codeqlService.ts @@ -7,6 +7,7 @@ import * as yaml from "js-yaml"; import { exec } from "child_process"; import { promisify } from "util"; import * as os from "os"; +import * as https from "https"; const execAsync = promisify(exec); @@ -288,24 +289,356 @@ export class CodeQLService { } public async getVersion(): Promise { - try { - const config = vscode.workspace.getConfiguration("codeql-scanner"); - const codeqlPath = config.get("codeqlPath", "codeql"); + const config = vscode.workspace.getConfiguration("codeql-scanner"); + const codeqlPath = config.get("codeqlPath", "codeql"); + return this.getVersionForPath(codeqlPath); + } + private async getVersionForPath(codeqlPath: string): Promise { + try { const { stdout } = await execAsync( - `${codeqlPath} version -v --log-to-stderr --format=json` + `"${codeqlPath}" version -v --log-to-stderr --format=json` ); const versionInfo = JSON.parse(stdout); return versionInfo.version || "unknown"; } catch (error) { - this.logger.error("CodeQLService", "Error getting CodeQL version", error); + this.logger.debug("CodeQLService", `Error getting CodeQL version for path '${codeqlPath}'`, error); throw new Error( - "Failed to get CodeQL version. Please check your configuration." + `Failed to get CodeQL version from '${codeqlPath}'. Please check your configuration.` ); } } + /** + * Discover CodeQL CLI from GitHub's CodeQL extension + */ + private async discoverCodeQLFromExtension(): Promise { + try { + // Check if GitHub's CodeQL extension is installed + const codeqlExtension = vscode.extensions.getExtension("GitHub.vscode-codeql"); + + if (!codeqlExtension) { + this.logger.debug("CodeQLService", "GitHub CodeQL extension not installed"); + return null; + } + + this.logger.debug("CodeQLService", "Found GitHub CodeQL extension, attempting to discover CLI path"); + + // Common paths where the CodeQL extension might store the CLI + const possiblePaths = [ + // Extension's bundled CLI (common pattern) + path.join(codeqlExtension.extensionPath, "dist", "codeql"), + path.join(codeqlExtension.extensionPath, "dist", "codeql.exe"), + // User's CodeQL CLI path that the extension might know about + // Try to read from extension's configuration or workspace state + ]; + + // Try each possible path + for (const possiblePath of possiblePaths) { + try { + if (fs.existsSync(possiblePath)) { + await this.getVersionForPath(possiblePath); + this.logger.debug("CodeQLService", `Found CodeQL CLI at: ${possiblePath}`); + return possiblePath; + } + } catch (error) { + // Continue trying other paths + } + } + + // Also try to get the CLI path from the extension's configuration + try { + const codeqlConfig = vscode.workspace.getConfiguration("codeQL"); + const extensionCliPath = codeqlConfig.get("cli.executablePath"); + + if (extensionCliPath && fs.existsSync(extensionCliPath)) { + await this.getVersionForPath(extensionCliPath); + this.logger.debug("CodeQLService", `Found CodeQL CLI from extension config: ${extensionCliPath}`); + return extensionCliPath; + } + } catch (error) { + // Extension config might not exist or be accessible + } + + this.logger.debug("CodeQLService", "Could not find CodeQL CLI from GitHub extension"); + return null; + } catch (error) { + this.logger.debug("CodeQLService", "Error discovering CodeQL from extension", error); + return null; + } + } + + /** + * Prompt user and download/install CodeQL CLI from GitHub + */ + private async promptAndInstallCodeQL(): Promise { + const response = await vscode.window.showInformationMessage( + "CodeQL CLI not found. Would you like to download and install it automatically?", + { modal: true }, + "Yes, Install", + "No, Configure Manually" + ); + + if (response === "Yes, Install") { + return await vscode.window.withProgress({ + location: vscode.ProgressLocation.Notification, + title: "Installing CodeQL CLI", + cancellable: true + }, async (progress, token) => { + try { + progress.report({ increment: 10, message: "Getting latest release info..." }); + + const releaseInfo = await this.getLatestCodeQLRelease(); + if (token.isCancellationRequested) { + throw new Error("Installation cancelled"); + } + + progress.report({ increment: 30, message: "Downloading CodeQL CLI..." }); + + const installPath = await this.downloadAndInstallCodeQL(releaseInfo, progress, token); + if (token.isCancellationRequested) { + throw new Error("Installation cancelled"); + } + + progress.report({ increment: 100, message: "Installation complete!" }); + + vscode.window.showInformationMessage("CodeQL CLI installed successfully!"); + return installPath; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + vscode.window.showErrorMessage(`Failed to install CodeQL CLI: ${errorMessage}`); + throw error; + } + }); + } else if (response === "No, Configure Manually") { + const openSettings = await vscode.window.showInformationMessage( + "Please configure the CodeQL CLI path in settings.", + "Open Settings" + ); + + if (openSettings === "Open Settings") { + await vscode.commands.executeCommand('workbench.action.openSettings', 'codeql-scanner.codeqlPath'); + } + } + + return null; + } + + /** + * Get the latest CodeQL release information from GitHub + */ + public async getLatestCodeQLRelease(): Promise { + return new Promise((resolve, reject) => { + const url = "https://api.github.com/repos/github/codeql-cli-binaries/releases/latest"; + + https.get(url, { + headers: { + 'User-Agent': 'codeql-scanner-vscode' + } + }, (response) => { + let data = ''; + + response.on('data', (chunk) => { + data += chunk; + }); + + response.on('end', () => { + try { + const releaseInfo = JSON.parse(data); + resolve(releaseInfo); + } catch (error) { + reject(new Error(`Failed to parse release info: ${error}`)); + } + }); + }).on('error', (error) => { + this.logger.error("CodeQLService", "Failed to get latest CodeQL release", error); + reject(new Error("Failed to get latest CodeQL release information")); + }); + }); + } + + /** + * Download and install CodeQL CLI + */ + public async downloadAndInstallCodeQL( + releaseInfo: any, + progress: vscode.Progress<{ increment?: number; message?: string }>, + cancellationToken: vscode.CancellationToken + ): Promise { + const platform = this.getCurrentPlatform(); + const asset = releaseInfo.assets.find((asset: any) => + asset.name.includes(platform) && asset.name.endsWith('.zip') + ); + + if (!asset) { + throw new Error(`No CodeQL CLI binary found for platform: ${platform}`); + } + + // Create installation directory + const installDir = path.join(os.homedir(), ".codeql", "cli"); + if (!fs.existsSync(installDir)) { + fs.mkdirSync(installDir, { recursive: true }); + } + + const zipPath = path.join(installDir, asset.name); + const extractDir = path.join(installDir, "extracted"); + + try { + // Download the file + progress.report({ increment: 20, message: "Downloading..." }); + + await this.downloadFile(asset.browser_download_url, zipPath); + + if (cancellationToken.isCancellationRequested) { + throw new Error("Installation cancelled"); + } + + progress.report({ increment: 60, message: "Extracting..." }); + + // Extract the zip file + await this.extractZip(zipPath, extractDir); + + // Find the CodeQL executable + const executableName = platform.includes('win') ? 'codeql.exe' : 'codeql'; + const executablePath = this.findCodeQLExecutable(extractDir, executableName); + + if (!executablePath) { + throw new Error("Could not find CodeQL executable after extraction"); + } + + // Make executable on Unix systems + if (!platform.includes('win')) { + await execAsync(`chmod +x "${executablePath}"`); + } + + // Clean up zip file + fs.unlinkSync(zipPath); + + progress.report({ increment: 90, message: "Verifying installation..." }); + + // Verify the installation + await this.getVersionForPath(executablePath); + + return executablePath; + } catch (error) { + // Clean up on error + if (fs.existsSync(zipPath)) { + fs.unlinkSync(zipPath); + } + if (fs.existsSync(extractDir)) { + fs.rmSync(extractDir, { recursive: true, force: true }); + } + throw error; + } + } + + /** + * Download a file from a URL + */ + private async downloadFile(url: string, filePath: string): Promise { + return new Promise((resolve, reject) => { + const file = fs.createWriteStream(filePath); + + https.get(url, { + headers: { + 'User-Agent': 'codeql-scanner-vscode' + } + }, (response) => { + // Handle redirects + if (response.statusCode === 302 || response.statusCode === 301) { + const redirectUrl = response.headers.location; + if (redirectUrl) { + return this.downloadFile(redirectUrl, filePath).then(resolve).catch(reject); + } + } + + if (response.statusCode !== 200) { + reject(new Error(`Download failed with status: ${response.statusCode}`)); + return; + } + + response.pipe(file); + + file.on('finish', () => { + file.close(); + resolve(); + }); + + file.on('error', (error) => { + fs.unlinkSync(filePath); + reject(error); + }); + }).on('error', (error) => { + reject(error); + }); + }); + } + + /** + * Get the current platform identifier for CodeQL CLI downloads + */ + private getCurrentPlatform(): string { + const platform = os.platform(); + const arch = os.arch(); + + if (platform === 'win32') { + return arch === 'x64' ? 'win64' : 'win32'; + } else if (platform === 'darwin') { + return 'osx64'; + } else if (platform === 'linux') { + return arch === 'x64' ? 'linux64' : 'linux'; + } + + throw new Error(`Unsupported platform: ${platform}-${arch}`); + } + + /** + * Extract a zip file (simple implementation) + */ + private async extractZip(zipPath: string, extractDir: string): Promise { + // For simplicity, we'll use a system command for extraction + // In a production environment, you might want to use a proper zip library + const platform = os.platform(); + + if (!fs.existsSync(extractDir)) { + fs.mkdirSync(extractDir, { recursive: true }); + } + + if (platform === 'win32') { + // Use PowerShell on Windows + await execAsync(`powershell -command "Expand-Archive -Path '${zipPath}' -DestinationPath '${extractDir}' -Force"`); + } else { + // Use unzip on Unix systems + await execAsync(`unzip -o "${zipPath}" -d "${extractDir}"`); + } + } + + /** + * Find the CodeQL executable in the extracted directory + */ + private findCodeQLExecutable(dir: string, executableName: string): string | null { + try { + const files = fs.readdirSync(dir); + + for (const file of files) { + const filePath = path.join(dir, file); + const stat = fs.statSync(filePath); + + if (stat.isDirectory()) { + const result = this.findCodeQLExecutable(filePath, executableName); + if (result) return result; + } else if (file === executableName) { + return filePath; + } + } + + return null; + } catch (error) { + return null; + } + } + /** * List of supported languages by CodeQL CLI. * @@ -710,21 +1043,57 @@ export class CodeQLService { // Local CodeQL CLI methods private async checkCodeQLCLI(): Promise { const config = vscode.workspace.getConfiguration("codeql-scanner"); - const codeqlPath = config.get("codeqlPath", "codeql"); + let codeqlPath = config.get("codeqlPath", "codeql"); + // First try the configured path try { - const version = await this.getVersion(); - this.logger.info("CodeQLService", `CodeQL CLI version: ${version}`); + const version = await this.getVersionForPath(codeqlPath); + this.logger.info("CodeQLService", `CodeQL CLI version: ${version} (path: ${codeqlPath})`); + return; } catch (error) { - this.logger.error( - "CodeQLService", - `CodeQL CLI not found at '${codeqlPath}'`, - error - ); - throw new Error( - `CodeQL CLI not found at '${codeqlPath}'. Please install CodeQL CLI and configure the path in settings.` - ); + this.logger.debug("CodeQLService", `CodeQL CLI not found at configured path '${codeqlPath}'`); + } + + // Try to discover CodeQL from GitHub extension if enabled + const autoDetectExtension = config.get("autoDetectGitHubExtension", true); + if (autoDetectExtension) { + try { + const discoveredPath = await this.discoverCodeQLFromExtension(); + if (discoveredPath) { + const version = await this.getVersionForPath(discoveredPath); + this.logger.info("CodeQLService", `Found CodeQL CLI from GitHub extension: ${version} (path: ${discoveredPath})`); + + // Update configuration with discovered path + await config.update("codeqlPath", discoveredPath, vscode.ConfigurationTarget.Global); + return; + } + } catch (error) { + this.logger.debug("CodeQLService", "Could not discover CodeQL from GitHub extension", error); + } } + + // Attempt auto-installation if enabled + const autoInstall = config.get("autoInstallCodeQL", true); + if (autoInstall) { + try { + const installedPath = await this.promptAndInstallCodeQL(); + if (installedPath) { + const version = await this.getVersionForPath(installedPath); + this.logger.info("CodeQLService", `Auto-installed CodeQL CLI: ${version} (path: ${installedPath})`); + + // Update configuration with installed path + await config.update("codeqlPath", installedPath, vscode.ConfigurationTarget.Global); + return; + } + } catch (error) { + this.logger.error("CodeQLService", "Failed to auto-install CodeQL CLI", error); + } + } + + // If all attempts failed, throw error + const errorMessage = `CodeQL CLI not found. Attempted paths: ${codeqlPath}${autoDetectExtension ? ', CodeQL extension discovery' : ''}${autoInstall ? ', auto-installation' : ''}. Please install CodeQL CLI manually or configure the path in settings.`; + this.logger.error("CodeQLService", errorMessage); + throw new Error(errorMessage); } private getCodeQLDirectory(): string { From 0b9a3f0b57ca809f0d9ea66524a65d0ccdb25d20 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Jun 2025 20:48:23 +0000 Subject: [PATCH 3/3] Complete CodeQL discovery and auto-install implementation with documentation Co-authored-by: felickz <1760475+felickz@users.noreply.github.com> --- README.md | 23 ++++++++++++++++++----- src/services/codeqlService.ts | 10 +++++++++- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e4987da..16d6b14 100644 --- a/README.md +++ b/README.md @@ -67,10 +67,11 @@ Here are some screenshots showcasing the extension's capabilities: ## 📋 Prerequisites -- **CodeQL CLI**: The extension requires the [CodeQL CLI](https://github.com/github/codeql-cli-binaries/releases) to be installed and available on your system PATH - - Download the latest release for your platform from the [CodeQL CLI releases page](https://github.com/github/codeql-cli-binaries/releases) - - Extract the archive and add the `codeql` binary to your system PATH - - Verify installation by running `codeql --version` in your terminal +- **CodeQL CLI**: The extension can automatically install the [CodeQL CLI](https://github.com/github/codeql-cli-binaries/releases) for you + - ✨ **Auto-Discovery**: The extension will automatically detect CodeQL CLI from GitHub's CodeQL extension if installed + - ✨ **Auto-Install**: If CodeQL CLI is not found, the extension will offer to download and install it automatically + - **Manual Install**: You can also download the latest release manually from the [CodeQL CLI releases page](https://github.com/github/codeql-cli-binaries/releases) and configure the path in settings + - **Verification**: Use `CodeQL: Show CLI Information` command to check your installation - **GitHub Personal Access Token**: For GitHub integration features, a GitHub token with appropriate permissions is required - Create a token at [GitHub Settings > Developer settings > Personal access tokens](https://github.com/settings/tokens) @@ -89,6 +90,7 @@ Here are some screenshots showcasing the extension's capabilities: | `CodeQL: Clear Logs` | Clear all log entries | | `CodeQL: Clear Inline Diagnostics` | Remove inline problem markers | | `CodeQL: Show CLI Information` | Display information about the CodeQL CLI | +| `CodeQL: Install/Update CLI` | ✨ Download and install CodeQL CLI automatically | | `CodeQL: Copy Flow Path` | Copy vulnerability data flow path to clipboard | | `CodeQL: Navigate Flow Steps` | Step through vulnerability data flow paths | @@ -98,10 +100,21 @@ The extension provides several configuration options to customize its behavior: ```json { - "codeql-scanner.github.token": "your-github-token" + "codeql-scanner.github.token": "your-github-token", + "codeql-scanner.codeqlPath": "codeql", + "codeql-scanner.autoDetectGitHubExtension": true, + "codeql-scanner.autoInstallCodeQL": true, + "codeql-scanner.useLocalScan": true } ``` +### Key Configuration Options + +- **`autoDetectGitHubExtension`** (default: `true`): Automatically detect and use CodeQL CLI from GitHub's CodeQL extension if available +- **`autoInstallCodeQL`** (default: `true`): Automatically download and install CodeQL CLI from GitHub if not found +- **`codeqlPath`**: Path to the CodeQL CLI executable (automatically configured when using auto-install) +- **`useLocalScan`** (default: `true`): Use local CodeQL CLI for scanning instead of GitHub Actions + ## 💡 Why CodeQL Scanner? CodeQL is GitHub's semantic code analysis engine that lets you query code as if it were data. This extension brings that power directly into VS Code, allowing you to: diff --git a/src/services/codeqlService.ts b/src/services/codeqlService.ts index c280996..411ef2a 100644 --- a/src/services/codeqlService.ts +++ b/src/services/codeqlService.ts @@ -527,7 +527,15 @@ export class CodeQLService { fs.unlinkSync(zipPath); } if (fs.existsSync(extractDir)) { - fs.rmSync(extractDir, { recursive: true, force: true }); + try { + fs.rmSync(extractDir, { recursive: true, force: true }); + } catch { + // Fallback for older Node.js versions + await execAsync(`rm -rf "${extractDir}"`).catch(() => { + // If rm command also fails, just log a warning + this.logger.warn("CodeQLService", `Could not clean up directory: ${extractDir}`); + }); + } } throw error; }