diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 9a120d6..045bff8 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,3 +1,3 @@ -# Codeowners +# This project is maintained with love by * @advanced-security/oss-maintainers diff --git a/package.json b/package.json index 0187f8b..c3cb6cc 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "codeql-scanner-vscode", "displayName": "CodeQL Scanner", "description": "VSCode extension for CodeQL to scan and analyze code for vulnerabilities using GitHub's CodeQL.", - "version": "0.1.2", + "version": "0.2.0", "repository": { "type": "git", "url": "https://github.com/geekmasher/codeql-scanner-vscode.git" @@ -37,17 +37,20 @@ { "command": "codeql-scanner.scan", "title": "CodeQL: Run Scan", - "category": "CodeQL Scanner" + "category": "CodeQL Scanner", + "enablement": "codeql-scanner.codeQLEnabled" }, { "command": "codeql-scanner.init", "title": "CodeQL: Initialize Repository", - "category": "CodeQL Scanner" + "category": "CodeQL Scanner", + "enablement": "codeql-scanner.codeQLEnabled" }, { "command": "codeql-scanner.analysis", "title": "CodeQL: Run Analysis", - "category": "CodeQL Scanner" + "category": "CodeQL Scanner", + "enablement": "codeql-scanner.codeQLEnabled" }, { "command": "codeql-scanner.configure", @@ -194,15 +197,15 @@ "commandPalette": [ { "command": "codeql-scanner.scan", - "when": "workspaceContainsCodeqlConfig || true" + "when": "(workspaceContainsCodeqlConfig || true) && codeql-scanner.codeQLEnabled" }, { "command": "codeql-scanner.init", - "when": "workspaceContainsCodeqlConfig || true" + "when": "(workspaceContainsCodeqlConfig || true) && codeql-scanner.codeQLEnabled" }, { "command": "codeql-scanner.analysis", - "when": "workspaceContainsCodeqlConfig || true" + "when": "(workspaceContainsCodeqlConfig || true) && codeql-scanner.codeQLEnabled" }, { "command": "codeql-scanner.configure", @@ -224,7 +227,8 @@ "explorer/context": [ { "command": "codeql-scanner.scan", - "group": "codeql@1" + "group": "codeql@1", + "when": "codeql-scanner.codeQLEnabled" } ], "view/item/context": [ diff --git a/src/providers/uiProvider.ts b/src/providers/uiProvider.ts index 8aeb14c..4558ca9 100644 --- a/src/providers/uiProvider.ts +++ b/src/providers/uiProvider.ts @@ -57,6 +57,12 @@ export class UiProvider implements vscode.WebviewViewProvider { case "testConnection": this.testGitHubConnection(); break; + case "checkCodeQLEnabled": + this.checkCodeQLEnabled(); + break; + case "updateRepositoryInfo": + this.updateRepositoryInfo(message.owner, message.repo, message.url); + break; case "runLocalScan": this.runLocalScan(); break; @@ -92,6 +98,7 @@ export class UiProvider implements vscode.WebviewViewProvider { `Saving configuration: ${JSON.stringify(config, null, 2)}` ); + // Update the standard workspace configuration await Promise.all([ workspaceConfig.update( "suites", @@ -109,6 +116,36 @@ export class UiProvider implements vscode.WebviewViewProvider { vscode.ConfigurationTarget.Workspace ), ]); + + // Update GitHub URL if provided + if (config.githubUrl) { + let apiUrl = "https://api.github.com"; + + if (config.githubUrl === "github.com" || config.githubUrl === "https://github.com") { + apiUrl = "https://api.github.com"; + } else { + // Remove https:// prefix if present + const cleanUrl = config.githubUrl.replace(/^https?:\/\//, ''); + + // For GitHub Enterprise, convert to API URL format + apiUrl = `https://${cleanUrl}`; + if (!apiUrl.includes('/api/v3')) { + apiUrl = apiUrl.endsWith('/') ? `${apiUrl}api/v3` : `${apiUrl}/api/v3`; + } + } + + this.logger.info( + "UiProvider", + "Updating GitHub base URL configuration", + { userInput: config.githubUrl, apiUrl } + ); + + await workspaceConfig.update( + "github.baseUrl", + apiUrl, + vscode.ConfigurationTarget.Global + ); + } this.logger.logServiceCall( "UiProvider", @@ -164,6 +201,39 @@ export class UiProvider implements vscode.WebviewViewProvider { ); } this.logger.info("UiProvider", `Using threat model: ${threatModel}`); + + // Check if CodeQL is enabled for the configured repository + const owner = config.get("github.owner"); + const repo = config.get("github.repo"); + if (owner && repo) { + this.logger.info( + "UiProvider", + `Checking CodeQL status during configuration load for ${owner}/${repo}` + ); + + // We'll check CodeQL status, but won't block configuration loading + this.checkCodeQLEnabled() + .then(isEnabled => { + this.logger.info( + "UiProvider", + `CodeQL status check during configuration load: ${isEnabled ? 'ENABLED' : 'NOT ENABLED'} for ${owner}/${repo}`, + { owner, repo, codeqlEnabled: isEnabled } + ); + }) + .catch(error => { + this.logger.warn( + "UiProvider", + "Failed to check CodeQL status during configuration load", + error + ); + }); + } else { + this.logger.info( + "UiProvider", + "Skipping CodeQL status check - repository information not configured", + { owner, repo } + ); + } // Auto-select GitHub repository languages if no manual selection exists let languages = config.get("languages", []); @@ -180,6 +250,7 @@ export class UiProvider implements vscode.WebviewViewProvider { githubToken: config.get("github.token", ""), githubOwner: config.get("github.owner", ""), githubRepo: config.get("github.repo", ""), + githubUrl: config.get("github.baseUrl", "https://api.github.com"), githubLanguages: config.get("github.languages", []), suites: config.get("suites", ["default"]), languages: languages, @@ -212,8 +283,10 @@ export class UiProvider implements vscode.WebviewViewProvider { } try { - // Update the service with the current token - this._githubService.updateToken(token); + const baseUrl = config.get("github.baseUrl"); + + // Update the service with the current token and base URL + this._githubService.updateToken(token, baseUrl); // Test the connection by getting repository info await this._githubService.getRepositoryInfo(); @@ -438,6 +511,36 @@ export class UiProvider implements vscode.WebviewViewProvider { return; } + // Check if CodeQL is enabled for the repository + const config = vscode.workspace.getConfiguration("codeql-scanner"); + const owner = config.get("github.owner"); + const repo = config.get("github.repo"); + + if (!owner || !repo) { + this.logger.warn("UiProvider", "Repository information not configured"); + this._view?.webview.postMessage({ + command: "scanBlocked", + success: false, + message: "Repository information not configured. Please set up your repository connection first.", + }); + return; + } + + try { + const isEnabled = await this._githubService.isCodeQLEnabled(owner, repo); + if (!isEnabled) { + this.logger.warn("UiProvider", `CodeQL is not enabled for ${owner}/${repo}`); + this._view?.webview.postMessage({ + command: "scanBlocked", + success: false, + message: `CodeQL is not enabled for ${owner}/${repo}. Please enable CodeQL in your repository settings.`, + }); + return; + } + } catch (error) { + this.logger.warn("UiProvider", "Failed to check if CodeQL is enabled", error); + } + try { // Set scan in progress flag this._scanInProgress = true; @@ -481,10 +584,20 @@ export class UiProvider implements vscode.WebviewViewProvider { } } + /** + * Fetch remote CodeQL alerts from GitHub for the configured repository + * Requires CodeQL to be enabled on the repository + */ private async fetchRemoteAlerts() { + this.logger.logServiceCall("UiProvider", "fetchRemoteAlerts", "started"); + // Check if a scan is already in progress if (this._scanInProgress) { - this.logger.warn("UiProvider", "Attempted to fetch alerts while a scan is in progress"); + this.logger.warn( + "UiProvider", + "Attempted to fetch alerts while a scan is in progress", + { scanInProgress: this._scanInProgress } + ); // Send message to UI this._view?.webview.postMessage({ @@ -499,11 +612,82 @@ export class UiProvider implements vscode.WebviewViewProvider { return; } + // Check if repository information is configured + const config = vscode.workspace.getConfiguration("codeql-scanner"); + const owner = config.get("github.owner"); + const repo = config.get("github.repo"); + + this.logger.info( + "UiProvider", + "Fetching remote alerts with repository configuration", + { owner, repo } + ); + + if (!owner || !repo) { + this.logger.warn( + "UiProvider", + "Repository information not configured for fetching alerts" + ); + this._view?.webview.postMessage({ + command: "fetchBlocked", + success: false, + message: "Repository information not configured. Please set up your repository connection first.", + }); + return; + } + + // Check if CodeQL is enabled for the repository + try { + this.logger.info( + "UiProvider", + `Checking if CodeQL is enabled for ${owner}/${repo} before fetching alerts` + ); + + const isEnabled = await this._githubService.isCodeQLEnabled(owner, repo); + + if (!isEnabled) { + this.logger.warn( + "UiProvider", + `CodeQL is not enabled for ${owner}/${repo}, cannot fetch alerts`, + { owner, repo, codeqlEnabled: false } + ); + this._view?.webview.postMessage({ + command: "fetchBlocked", + success: false, + message: `CodeQL is not enabled for ${owner}/${repo}. Please enable CodeQL in your repository settings.`, + }); + return; + } + + this.logger.info( + "UiProvider", + `CodeQL is enabled for ${owner}/${repo}, proceeding with alert fetch` + ); + } catch (error) { + this.logger.warn( + "UiProvider", + "Failed to check if CodeQL is enabled before fetching alerts", + error + ); + } + // Set scan in progress flag for the duration of the fetch this._scanInProgress = true; + this.logger.debug( + "UiProvider", + "Setting scan in progress flag for fetch operation", + { scanInProgress: this._scanInProgress } + ); + try { this._fetchStartTime = Date.now(); + + this.logger.debug( + "UiProvider", + "Starting fetch operation timer", + { fetchStartTime: this._fetchStartTime } + ); this._view?.webview.postMessage({ command: "fetchStarted", @@ -516,46 +700,95 @@ export class UiProvider implements vscode.WebviewViewProvider { const owner = config.get("github.owner"); const repo = config.get("github.repo"); + this.logger.debug( + "UiProvider", + "Verifying GitHub configuration for alert fetch", + { + hasToken: !!token, + owner, + repo + } + ); + if (!token || !owner || !repo) { - throw new Error( + const error = new Error( "GitHub configuration is incomplete. Please configure token, owner, and repo." ); + this.logger.error("UiProvider", "GitHub configuration incomplete for alert fetch", error); + throw error; } // Update the service with the current token + this.logger.debug("UiProvider", "Updating GitHub token for alert fetch"); this._githubService.updateToken(token); // Use GitHubService to fetch CodeQL alerts + this.logger.info( + "UiProvider", + `Fetching CodeQL alerts from GitHub for ${owner}/${repo}` + ); + const codeqlAlerts = await this._githubService.getCodeQLAlerts( owner, repo ); + + this.logger.info( + "UiProvider", + `Retrieved ${codeqlAlerts.length} CodeQL alerts from GitHub`, + { alertCount: codeqlAlerts.length } + ); // Convert GitHub alerts to our ScanResult format - const scanResults = codeqlAlerts.map((alert: any) => ({ - ruleId: alert.rule?.id || "unknown", - severity: this.mapGitHubSeverityToLocal( + this.logger.debug( + "UiProvider", + "Converting GitHub alerts to ScanResult format" + ); + + const scanResults = codeqlAlerts.map((alert: any) => { + const severity = this.mapGitHubSeverityToLocal( alert.rule?.security_severity_level || alert.rule?.severity - ), - language: this.mapCodeQLAlertLanguage(alert.rule?.id), - message: - alert.message?.text || alert.rule?.description || "No description", - location: { - file: alert.most_recent_instance?.location?.path - ? path.join(vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || "", alert.most_recent_instance.location.path) - : "unknown", - startLine: alert.most_recent_instance?.location?.start_line || 1, - startColumn: alert.most_recent_instance?.location?.start_column || 1, - endLine: alert.most_recent_instance?.location?.end_line || 1, - endColumn: alert.most_recent_instance?.location?.end_column || 1, - }, - })); + ); + const language = this.mapCodeQLAlertLanguage(alert.rule?.id); + + return { + ruleId: alert.rule?.id || "unknown", + severity: severity, + language: language, + message: + alert.message?.text || alert.rule?.description || "No description", + location: { + file: alert.most_recent_instance?.location?.path + ? path.join(vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || "", alert.most_recent_instance.location.path) + : "unknown", + startLine: alert.most_recent_instance?.location?.start_line || 1, + startColumn: alert.most_recent_instance?.location?.start_column || 1, + endLine: alert.most_recent_instance?.location?.end_line || 1, + endColumn: alert.most_recent_instance?.location?.end_column || 1, + }, + }; + }); + + this.logger.info( + "UiProvider", + `Converted ${scanResults.length} GitHub alerts to ScanResult format`, + { + resultCount: scanResults.length + } + ); // Update the scan results and refresh summary + this.logger.debug("UiProvider", "Updating scan results with fetched alerts"); this.updateScanResults(scanResults); // Also update the results provider if available if (this._resultsProvider) { + this.logger.debug( + "UiProvider", + "Updating results provider with fetched alerts", + { resultsCount: scanResults.length } + ); + this._resultsProvider.setResults(scanResults); vscode.commands.executeCommand( "setContext", @@ -568,6 +801,15 @@ export class UiProvider implements vscode.WebviewViewProvider { ? Date.now() - this._fetchStartTime : 0; const durationText = this.formatDuration(fetchDuration); + + this.logger.info( + "UiProvider", + `Fetch operation completed in ${durationText}`, + { + fetchDuration, + alertsCount: scanResults.length + } + ); this._view?.webview.postMessage({ command: "fetchCompleted", @@ -580,6 +822,12 @@ export class UiProvider implements vscode.WebviewViewProvider { ? Date.now() - this._fetchStartTime : 0; const durationText = this.formatDuration(fetchDuration); + + this.logger.error( + "UiProvider", + `Failed to fetch remote alerts after ${durationText}`, + error + ); this._view?.webview.postMessage({ command: "fetchCompleted", @@ -590,6 +838,18 @@ export class UiProvider implements vscode.WebviewViewProvider { } finally { // Reset scan in progress flag regardless of success or failure this._scanInProgress = false; + + this.logger.debug( + "UiProvider", + "Resetting scan in progress flag after fetch operation", + { scanInProgress: false } + ); + + this.logger.logServiceCall( + "UiProvider", + "fetchRemoteAlerts", + "completed" + ); } } @@ -750,6 +1010,261 @@ export class UiProvider implements vscode.WebviewViewProvider { : `${hours}h`; } + /** + * Check if CodeQL is enabled on the configured GitHub repository + * This prevents scanning when CodeQL is not enabled + * Sets VS Code context to control UI visibility based on CodeQL status + */ + private async checkCodeQLEnabled(): Promise { + this.logger.logServiceCall("UiProvider", "checkCodeQLEnabled", "started"); + + const config = vscode.workspace.getConfiguration("codeql-scanner"); + const token = config.get("github.token"); + const owner = config.get("github.owner"); + const repo = config.get("github.repo"); + + this.logger.info( + "UiProvider", + "Checking CodeQL status for repository", + { owner, repo, tokenConfigured: !!token } + ); + + // Default to hiding scanning UI if we can't verify CodeQL status + await vscode.commands.executeCommand('setContext', 'codeql-scanner.codeQLEnabled', false); + + this.logger.debug("UiProvider", "Setting CodeQL enabled context to false by default"); + + if (!token) { + this.logger.warn( + "UiProvider", + "GitHub token not configured for checking CodeQL status" + ); + this._view?.webview.postMessage({ + command: "codeqlStatusChecked", + success: false, + enabled: false, + message: "GitHub token is required", + owner: owner || "", + repo: repo || "", + }); + return false; + } + + if (!owner || !repo) { + this.logger.warn( + "UiProvider", + "GitHub repository information not configured", + { owner, repo } + ); + this._view?.webview.postMessage({ + command: "codeqlStatusChecked", + success: false, + enabled: false, + message: "Repository owner and name must be configured", + owner: owner || "", + repo: repo || "", + }); + return false; + } + + try { + this.logger.debug( + "UiProvider", + "Updating GitHub token for CodeQL status check" + ); + + // Update the service with the current token + this._githubService.updateToken(token); + + this.logger.info( + "UiProvider", + `Checking if CodeQL is enabled for ${owner}/${repo}` + ); + + // Check if CodeQL is enabled + const isEnabled = await this._githubService.isCodeQLEnabled(owner, repo); + + this.logger.logServiceCall( + "UiProvider", + "checkCodeQLEnabled", + "completed", + { owner, repo, enabled: isEnabled } + ); + + this.logger.info( + "UiProvider", + `CodeQL status check result: ${isEnabled ? 'ENABLED' : 'NOT ENABLED'} for ${owner}/${repo}` + ); + + // Update VS Code context to control UI visibility based on CodeQL status + await vscode.commands.executeCommand('setContext', 'codeql-scanner.codeQLEnabled', isEnabled); + + this.logger.info( + "UiProvider", + `Setting CodeQL enabled context to ${isEnabled}`, + { contextUpdated: true, codeQLEnabled: isEnabled } + ); + + this._view?.webview.postMessage({ + command: "codeqlStatusChecked", + success: true, + enabled: isEnabled, + message: isEnabled ? + `CodeQL is enabled for ${owner}/${repo}` : + `CodeQL is not enabled for ${owner}/${repo}. Scanning functionality is disabled.`, + owner: owner || "", + repo: repo || "", + }); + + return isEnabled; + } catch (error) { + this.logger.logServiceCall( + "UiProvider", + "checkCodeQLEnabled", + "failed", + error + ); + + this.logger.error( + "UiProvider", + `Failed to check CodeQL status for ${owner}/${repo}`, + error + ); + this._view?.webview.postMessage({ + command: "codeqlStatusChecked", + success: false, + enabled: false, + message: `Failed to check CodeQL status: ${error}`, + owner: owner || "", + repo: repo || "", + }); + return false; + } + } + + /** + * Update repository information when provided from the UI + * Updates the configuration settings and checks if CodeQL is enabled + * + * @param owner Repository owner + * @param repo Repository name + */ + private async updateRepositoryInfo(owner: string, repo: string, url?: string): Promise { + this.logger.logServiceCall("UiProvider", "updateRepositoryInfo", "started", { + owner, repo, url + }); + + if (!owner || !repo) { + this.logger.warn( + "UiProvider", + "Invalid repository information provided", + { providedOwner: owner, providedRepo: repo } + ); + this._view?.webview.postMessage({ + command: "repositoryInfoUpdated", + success: false, + message: "Repository owner and name must be provided", + }); + return; + } + + const config = vscode.workspace.getConfiguration("codeql-scanner"); + const oldOwner = config.get("github.owner"); + const oldRepo = config.get("github.repo"); + + this.logger.debug( + "UiProvider", + "Updating repository information", + { + oldOwner, + oldRepo, + newOwner: owner, + newRepo: repo + } + ); + + try { + // Update repository settings + this.logger.debug("UiProvider", "Updating github.owner setting", { owner }); + await config.update("github.owner", owner, vscode.ConfigurationTarget.Workspace); + + this.logger.debug("UiProvider", "Updating github.repo setting", { repo }); + await config.update("github.repo", repo, vscode.ConfigurationTarget.Workspace); + + // Update GitHub URL if provided + if (url) { + // Convert web URL to API URL + let apiUrl = "https://api.github.com"; // Default API URL + + if (url === "github.com" || url === "https://github.com") { + apiUrl = "https://api.github.com"; + } else { + // Remove https:// prefix if present + const cleanUrl = url.replace(/^https?:\/\//, ''); + + // For GitHub Enterprise, convert to API URL + apiUrl = `https://${cleanUrl}`; + if (!apiUrl.includes('/api/v3')) { + apiUrl = apiUrl.endsWith('/') ? `${apiUrl}api/v3` : `${apiUrl}/api/v3`; + } + } + + this.logger.debug("UiProvider", "Updating github.baseUrl setting", { url, apiUrl }); + await config.update("github.baseUrl", apiUrl, vscode.ConfigurationTarget.Global); + } + + this.logger.info( + "UiProvider", + `Repository information updated from ${oldOwner}/${oldRepo} to ${owner}/${repo}` + ); + + this.logger.info( + "UiProvider", + "Checking CodeQL status for the updated repository" + ); + + // Check if CodeQL is enabled on the updated repository + const codeqlStatus = await this.checkCodeQLEnabled(); + + this.logger.info( + "UiProvider", + `CodeQL status for updated repository ${owner}/${repo}: ${codeqlStatus ? 'ENABLED' : 'NOT ENABLED'}` + ); + + this.logger.logServiceCall( + "UiProvider", + "updateRepositoryInfo", + "completed", + { owner, repo, codeqlEnabled: codeqlStatus } + ); + + this._view?.webview.postMessage({ + command: "repositoryInfoUpdated", + success: true, + message: `Repository information updated to ${owner}/${repo}`, + }); + } catch (error) { + this.logger.logServiceCall( + "UiProvider", + "updateRepositoryInfo", + "failed", + error + ); + + this.logger.error( + "UiProvider", + `Failed to update repository information for ${owner}/${repo}`, + error + ); + + this._view?.webview.postMessage({ + command: "repositoryInfoUpdated", + success: false, + message: `Failed to update repository information: ${error}`, + }); + } + } + private _getHtmlForWebview(webview: vscode.Webview) { return ` @@ -1169,6 +1684,37 @@ export class UiProvider implements vscode.WebviewViewProvider { color: var(--vscode-foreground); } + /* Collapsible section styles */ + .collapsible-header { + cursor: pointer; + display: flex; + justify-content: space-between; + align-items: center; + padding: 5px 0; + user-select: none; + } + + .collapsible-header .toggle-icon { + transition: transform 0.3s ease; + margin-left: 8px; + font-size: 12px; + } + + .collapsible-header.collapsed .toggle-icon { + transform: rotate(-90deg); + } + + .collapsible-content { + max-height: 1000px; + overflow: hidden; + transition: max-height 0.4s ease; + } + + .collapsible-content.collapsed { + max-height: 0; + overflow: hidden; + } + #message { margin-top: 15px; padding: 10px; @@ -1858,128 +2404,171 @@ export class UiProvider implements vscode.WebviewViewProvider { -