Skip to content

Commit 4798745

Browse files
committed
feat: Refactor scanner functionality and improve error handling
1 parent af68356 commit 4798745

9 files changed

Lines changed: 183 additions & 47 deletions

File tree

workspaces/ballerina/ballerina-extension/src/features/ai/agent/tool-registry.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import { createHurlTool, HURL_TOOL_NAME } from './tools/hurl-tool';
5656
import { createWebSearchTool, WEB_SEARCH_TOOL_NAME, createWebFetchTool, WEB_FETCH_TOOL_NAME } from './tools/web-tools';
5757
import { createClarifyTool, CLARIFY_TOOL } from './tools/clarify';
5858
import { createSecurityTool, SECURITY_TOOL_NAME } from './tools/security-diagnostics';
59+
import { isScannerConfigEnabled } from '../../scanner/scan-utils';
5960

6061
export interface ToolRegistryOptions {
6162
eventHandler: CopilotEventHandler;
@@ -124,11 +125,14 @@ export function createToolRegistry(opts: ToolRegistryOptions) {
124125
[FILE_READ_TOOL_NAME]: createReadTool(
125126
createReadExecute(eventHandler, tempProjectPath)
126127
),
127-
[SECURITY_TOOL_NAME]: createSecurityTool(tempProjectPath, eventHandler, {
128-
workspacePath: projectRootPath,
129-
projectPath: ctx.projectPath,
130-
modifiedFiles,
131-
}),
128+
// Security tool — registered only when the scanner configuration is explicitly enabled
129+
...(isScannerConfigEnabled() ? {
130+
[SECURITY_TOOL_NAME]: createSecurityTool(tempProjectPath, eventHandler, {
131+
workspacePath: projectRootPath,
132+
projectPath: ctx.projectPath,
133+
modifiedFiles,
134+
})
135+
} : {}),
132136
[DIAGNOSTICS_TOOL_NAME]: createDiagnosticsTool(tempProjectPath, eventHandler),
133137
[TEST_RUNNER_TOOL_NAME]: createTestRunnerTool(tempProjectPath, eventHandler, modifiedFiles, allModifiedFiles, ctx),
134138
// Migration source tools — registered only when a source project path is available

workspaces/ballerina/ballerina-extension/src/features/scanner/activator.ts

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
*/
1818

1919
import * as vscode from 'vscode';
20-
import { exec } from 'child_process';
20+
import { execFile } from 'child_process';
2121
import { BallerinaExtension } from 'src/core';
2222
import { scannerContentChanged } from '@wso2/ballerina-core';
2323
import { isScannerConfigEnabled, isScannerVersionSupported, setScannerVersionSupported, isScannerActive, getScannerOutputChannel, setScannerState, pullOrUpdateScannerTool } from './scan-utils';
@@ -30,8 +30,13 @@ import { RPCLayer } from '../../RPCLayer';
3030
* Checks if the scan tool version is greater than 0.11.0.
3131
*/
3232
function checkScanToolVersion(callback: (state: ScannerToolState) => void) {
33-
exec('bal tool list', (error, stdout) => {
33+
execFile('bal', ['tool', 'list'], { timeout: 15000 }, (error, stdout) => {
3434
if (error) {
35+
if (error.killed || (error as any).code === 'ETIMEDOUT') {
36+
console.warn('[Scanner] bal tool list timed out');
37+
callback('NOT_FOUND');
38+
return;
39+
}
3540
callback('NOT_FOUND');
3641
return;
3742
}
@@ -107,6 +112,12 @@ function resolveProjectRoot(uri?: vscode.Uri): vscode.Uri | undefined {
107112
* Activates the Ballerina Security Scanner feature.
108113
*/
109114
export function activate(ballerinaExtInstance: BallerinaExtension): void {
115+
const langClient = ballerinaExtInstance.langClient;
116+
if (!langClient) {
117+
vscode.window.showErrorMessage("Ballerina Language Server is not ready. Scanner disabled.");
118+
return;
119+
}
120+
110121
let scannerContentChangedDebounce: NodeJS.Timeout | undefined;
111122
const scannerRpcManager = new ScannerRpcManager();
112123

@@ -161,11 +172,7 @@ export function activate(ballerinaExtInstance: BallerinaExtension): void {
161172
await pullOrUpdateScannerTool();
162173
});
163174

164-
const langClient = ballerinaExtInstance.langClient;
165-
if (!langClient) {
166-
vscode.window.showErrorMessage("Ballerina Language Server is not ready. Scanner disabled.");
167-
return;
168-
}
175+
169176

170177
// Register Scan Command — delegates to the RPC manager (no direct LS calls here)
171178
const scanDisposable = vscode.commands.registerCommand('ballerina.scan.project', async (uri?: vscode.Uri) => {
@@ -188,7 +195,7 @@ export function activate(ballerinaExtInstance: BallerinaExtension): void {
188195
return;
189196
}
190197

191-
if (!isScannerVersionSupported) {
198+
if (!isScannerVersionSupported()) {
192199
vscode.window.showErrorMessage("Ballerina Security Scanner requires the 'scan' tool version > 0.11.0. Please update your tool.");
193200
return;
194201
}
@@ -197,7 +204,15 @@ export function activate(ballerinaExtInstance: BallerinaExtension): void {
197204
outputChannel.appendLine(`[INFO] [SCAN] Start: ${projectRootUri.fsPath}`);
198205

199206
// Delegate to the RPC manager — the single source of truth for LS calls
200-
const result = await scannerRpcManager.scanProject({ projectPath: projectRootUri.fsPath });
207+
let result;
208+
try {
209+
result = await scannerRpcManager.scanProject({ projectPath: projectRootUri.fsPath });
210+
} catch (err) {
211+
const message = err instanceof Error ? err.message : String(err);
212+
outputChannel.appendLine(`[ERROR] [SCAN] Failed: ${message}`);
213+
vscode.window.showErrorMessage(`Security Scan Failed: ${message}`);
214+
return;
215+
}
201216

202217
const scanError = result.errorMsg || result.error;
203218
if (scanError) {

workspaces/ballerina/ballerina-extension/src/features/scanner/scan-utils.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ export type ScannerToolState = 'NOT_FOUND' | 'INCOMPATIBLE' | 'SUPPORTED';
3030
/**
3131
* Tracks whether the scanner tool version is supported in the current workspace.
3232
*/
33-
export let isScannerVersionSupported = false;
33+
let _isScannerVersionSupported = false;
34+
export function isScannerVersionSupported(): boolean {
35+
return _isScannerVersionSupported;
36+
}
3437

3538
export const DEFAULT_SCAN_TIMEOUT_MS = 120000;
3639

@@ -61,13 +64,16 @@ function runBalToolCommand(command: string): Promise<void> {
6164
}
6265

6366
export function setScannerVersionSupported(supported: boolean) {
64-
isScannerVersionSupported = supported;
67+
_isScannerVersionSupported = supported;
6568
}
6669

67-
export let scannerState: ScannerToolState = 'NOT_FOUND';
70+
let _scannerState: ScannerToolState = 'NOT_FOUND';
71+
export function scannerState(): ScannerToolState {
72+
return _scannerState;
73+
}
6874

6975
export function setScannerState(state: ScannerToolState) {
70-
scannerState = state;
76+
_scannerState = state;
7177
}
7278

7379
export function isScannerConfigEnabled(): boolean {
@@ -78,12 +84,12 @@ export function isScannerConfigEnabled(): boolean {
7884
}
7985

8086
export function isScannerActive(): boolean {
81-
return isScannerConfigEnabled() && isScannerVersionSupported;
87+
return isScannerConfigEnabled() && isScannerVersionSupported();
8288
}
8389

8490
export async function pullOrUpdateScannerTool(): Promise<boolean> {
8591
const outputChannel = getScannerOutputChannel();
86-
const isNotInstalled = scannerState === 'NOT_FOUND';
92+
const isNotInstalled = scannerState() === 'NOT_FOUND';
8793
const command = isNotInstalled ? 'bal tool pull scan' : 'bal tool update scan';
8894
const actionLabel = isNotInstalled ? 'Pulling' : 'Updating';
8995
const completionLabel = isNotInstalled ? 'pulled' : 'updated';

workspaces/ballerina/ballerina-extension/src/features/scanner/security-rules.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ const HINT_BY_RULE_ID = new Map<string, string>(
8181
* Returns the security severity for a given rule ID.
8282
*/
8383
export function getRuleSeverity(ruleId: string): ScannerRuleSeverity {
84-
return SEVERITY_BY_RULE_ID.get(ruleId);
84+
return SEVERITY_BY_RULE_ID.get(ruleId) ?? 'MEDIUM';
8585
}
8686

8787
/**

workspaces/ballerina/ballerina-extension/src/rpc-managers/scanner/rpc-manager.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@ export class ScannerRpcManager implements ScannerAPI {
313313
const documentUri = this.resolveDocumentUri(params as DisableRuleRequest & { filePath?: string });
314314
if (!documentUri) {
315315
vscode.window.showErrorMessage("No document found to resolve project root for global exclusion.");
316-
return;
316+
return false;
317317
}
318318

319319
const response = await withTimeout(
@@ -494,7 +494,7 @@ export class ScannerRpcManager implements ScannerAPI {
494494
if (!isScannerConfigEnabled()) {
495495
return { success: false, activeIssues: [], excludedIssues: [], errorMsg: "Scanner is disabled via settings." };
496496
}
497-
if (!isScannerVersionSupported) {
497+
if (!isScannerVersionSupported()) {
498498
const errorMsg = "Ballerina Security Scanner requires the 'scan' tool version > 0.11.0. Please update your tool by running 'bal tool pull scan'.";
499499
vscode.window.showErrorMessage(errorMsg);
500500
return { success: false, activeIssues: [], excludedIssues: [], errorMsg };

workspaces/ballerina/ballerina-extension/src/views/scanner/webview.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@ export class ScannerWebview {
7070

7171
private getWebviewContent(webView: vscode.Webview): string {
7272
const scannerEnabled = isScannerConfigEnabled();
73-
const scannerVersionSupported = isScannerVersionSupported;
74-
const currentScannerState = scannerState;
73+
const scannerVersionSupported = isScannerVersionSupported();
74+
const currentScannerState = scannerState();
7575
const activeEditorUri = vscode.window.activeTextEditor?.document.uri;
7676
const activeWorkspaceFolder = activeEditorUri ? vscode.workspace.getWorkspaceFolder(activeEditorUri) : undefined;
7777
const fallbackWorkspaceFolder = vscode.workspace.workspaceFolders?.[0];
@@ -120,13 +120,22 @@ export class ScannerWebview {
120120
100% {transform:scaleY(-1) rotate(-135deg)}
121121
}
122122
`;
123+
const safeCurrentScannerState = JSON.stringify(currentScannerState)
124+
.replace(/</g, '\\u003c')
125+
.replace(/\u2028/g, '\\u2028')
126+
.replace(/\u2029/g, '\\u2029');
127+
const safeProjectPath = JSON.stringify(projectPath)
128+
.replace(/</g, '\\u003c')
129+
.replace(/\u2028/g, '\\u2028')
130+
.replace(/\u2029/g, '\\u2029');
131+
123132
const scripts = `
124133
function loadedScript() {
125134
function renderDiagrams() {
126135
window.__SCANNER_ENABLED__ = ${scannerEnabled};
127136
window.__SCANNER_VERSION_SUPPORTED__ = ${scannerVersionSupported};
128-
window.__SCANNER_STATE__ = ${JSON.stringify(currentScannerState)};
129-
window.__SCANNER_PROJECT_PATH__ = ${JSON.stringify(projectPath)};
137+
window.__SCANNER_STATE__ = ${safeCurrentScannerState};
138+
window.__SCANNER_PROJECT_PATH__ = ${safeProjectPath};
130139
window.__SCANNER_DEPLOY_MODE__ = ${this._mode === 'deploy'};
131140
visualizerWebview.renderWebview("scanner", document.getElementById("webview-container"));
132141
}

workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/components/AgentStreamView/StreamEntry.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,8 +156,8 @@ function getToolResultDisplay(toolName: string | undefined, toolOutput: any, hin
156156
return { label: count > 0 ? `Found ${count} error(s)` : "No issues found" };
157157
}
158158
case "getSecurityVulnerabilities": {
159-
if (toolOutput?.success === false && toolOutput?.message) {
160-
return { label: toolOutput.message };
159+
if (toolOutput?.success === false) {
160+
return { label: toolOutput?.message || "Security scan failed" };
161161
}
162162
const count = toolOutput?.count ?? 0;
163163
return { label: count > 0 ? `Found ${count} security issue(s)` : "No security issues found" };

0 commit comments

Comments
 (0)