Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ codeql-development-mcp-server.code-workspace
# The 'codeql test run` command generates `<QueryBaseName>.testproj` test database directories
*.testproj

# CodeQL CLI diagnostic files generated during query runs
**/diagnostic/cli-diagnostics-*.json

# Prevent accidentally committing integration test output files in root directory
# These should only be in client/integration-tests/primitives/tools/*/after/ directories
/evaluator-log.json
Expand Down
20 changes: 20 additions & 0 deletions extensions/vscode/esbuild.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,22 @@ const testSuiteConfig = {
},
};

// Extended integration tests — standalone (no vscode API dependency).
// Built separately so they can run via `node` without the Extension Host.
const extendedTestConfig = {
...shared,
entryPoints: [
'test/extended/run-extended-tests.ts',
],
outdir: 'dist/test/extended',
outfile: undefined,
outExtension: { '.js': '.cjs' },
external: [], // No externals — fully self-contained
logOverride: {
Comment on lines +62 to +66
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extendedTestConfig sets external: [], but the extended test code dynamically imports 'vscode'. With no externalization, esbuild will try to resolve/bundle 'vscode' and the build will fail (since it's provided by the extension host, not npm). Keep 'vscode' external for this target so the optional import can be attempted and caught at runtime.

Copilot uses AI. Check for mistakes.
'require-resolve-not-external': 'silent',
},
};

const isWatch = process.argv.includes('--watch');

if (isWatch) {
Expand All @@ -67,6 +83,10 @@ if (isWatch) {
await build(testSuiteConfig);
console.log('✅ Test suite build completed successfully');
console.log(`📦 Generated: dist/test/suite/*.cjs`);

await build(extendedTestConfig);
console.log('✅ Extended test build completed successfully');
console.log(`📦 Generated: dist/test/extended/*.cjs`);
} catch (error) {
console.error('❌ Build failed:', error);
process.exit(1);
Expand Down
5 changes: 3 additions & 2 deletions extensions/vscode/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,10 @@ export default [
sourceType: 'module',
parser: typescript.parser,
globals: {
process: 'readonly',
console: 'readonly',
Buffer: 'readonly',
console: 'readonly',
fetch: 'readonly',
process: 'readonly',
__dirname: 'readonly',
__filename: 'readonly',
},
Expand Down
1 change: 1 addition & 0 deletions extensions/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@
"test": "npm run test:coverage && npm run test:integration",
"test:coverage": "vitest --run --coverage",
"test:integration": "npm run download:vscode && vscode-test",
"test:integration:extended": "npm run bundle && node dist/test/extended/run-extended-tests.cjs",
"test:integration:label": "vscode-test --label",
"test:watch": "vitest --watch",
"vscode:prepublish": "npm run clean && npm run lint && npm run bundle && npm run bundle:server",
Expand Down
258 changes: 258 additions & 0 deletions extensions/vscode/test/extended/download-databases.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
/**
* Download CodeQL databases using the GitHub REST API.
*
* When running inside the VS Code Extension Development Host, this uses
* the VS Code GitHub authentication session (same auth as vscode-codeql).
* When running standalone, it falls back to the GH_TOKEN env var.
*
* Downloads are cached on disk and reused if less than 24 hours old.
*/

import { createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, statSync } from 'fs';
import { join } from 'path';
import { execFileSync } from 'child_process';
import { homedir } from 'os';
import { pipeline } from 'stream/promises';

const MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours

export interface RepoConfig {
callGraphFromTo?: { sourceFunction: string; targetFunction: string };
language: string;
owner: string;
repo: string;
}

/**
* Get a GitHub token. Tries VS Code auth session first, then GH_TOKEN env var,
* then `gh auth token` CLI.
*/
async function getGitHubToken(): Promise<string | undefined> {
// Try VS Code authentication (when running in Extension Host)
try {
const vscode = await import('vscode');
const session = await vscode.authentication.getSession('github', ['repo'], { createIfNone: false });
if (session?.accessToken) {
console.log(' 🔑 Using VS Code GitHub authentication');
return session.accessToken;
}
} catch {
// Not in VS Code — fall through
}

// Try GH_TOKEN env var
if (process.env.GH_TOKEN) {
console.log(' 🔑 Using GH_TOKEN environment variable');
return process.env.GH_TOKEN;
}

// Try `gh auth token` CLI
try {
const { execFileSync } = await import('child_process');
const token = execFileSync('gh', ['auth', 'token'], { encoding: 'utf8', timeout: 5000 }).trim();
if (token) {
console.log(' 🔑 Using GitHub CLI (gh auth token)');
return token;
}
} catch {
// gh CLI not available or not authenticated
}

return undefined;
}

/**
* Download a CodeQL database for a repository via GitHub REST API.
* Returns the path to the extracted database, or null if download failed.
*/
async function downloadDatabase(
repo: RepoConfig,
databaseDir: string,
token: string,
): Promise<string | null> {
const { language, owner, repo: repoName } = repo;
const repoDir = join(databaseDir, owner, repoName);
const dbDir = join(repoDir, language);
const zipPath = join(repoDir, `${language}.zip`);
const markerFile = join(dbDir, 'codeql-database.yml');

// Check cache
if (existsSync(markerFile)) {
try {
const mtime = statSync(markerFile).mtimeMs;
if (Date.now() - mtime < MAX_AGE_MS) {
console.log(` ✓ Cached: ${owner}/${repoName} (${language})`);
return dbDir;
}
} catch {
// Fall through to download
}
}

mkdirSync(repoDir, { recursive: true });

const url = `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repoName)}/code-scanning/codeql/databases/${encodeURIComponent(language)}`;
console.log(` ⬇ Downloading: ${owner}/${repoName} (${language})...`);

try {
const response = await fetch(url, {

Check warning

Code scanning / CodeQL

File data in outbound network request Medium test

Outbound network request depends on
file data
.
Outbound network request depends on
file data
.
headers: {
Accept: 'application/zip',
Authorization: `Bearer ${token}`,
'User-Agent': 'codeql-development-mcp-server-extended-tests',
},
});

if (!response.ok) {
console.error(` ✗ Download failed: ${response.status} ${response.statusText}`);
return null;
}

if (!response.body) {
console.error(` ✗ Empty response body`);
return null;
}

// Stream to zip file
const dest = createWriteStream(zipPath);
// @ts-expect-error — ReadableStream → NodeJS.ReadableStream interop
await pipeline(response.body, dest);

Comment on lines +116 to +120
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

response.body from fetch() is a Web ReadableStream in Node 18+; stream/promises.pipeline expects a Node stream/AsyncIterable. This is likely to throw at runtime. Convert via Readable.fromWeb(response.body) (or buffer the response) before piping to the file stream.

Copilot uses AI. Check for mistakes.
// Extract
console.log(` 📦 Extracting: ${owner}/${repoName} (${language})...`);
mkdirSync(dbDir, { recursive: true });
execFileSync('unzip', ['-o', '-q', zipPath, '-d', dbDir]);

Comment on lines +122 to +125
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shelling out to unzip here makes downloads non-portable (not available on Windows and some minimal environments). Consider using a JS zip extraction library or, at minimum, detect when unzip is unavailable and fail/skip with a clear message.

Copilot uses AI. Check for mistakes.
// Flatten if single nested directory (zip often has one top-level dir)
const entries = readdirSync(dbDir);
if (entries.length === 1 && !existsSync(join(dbDir, 'codeql-database.yml'))) {
const nested = join(dbDir, entries[0]);
if (existsSync(join(nested, 'codeql-database.yml'))) {
// Copy all contents up, then remove the nested directory
execFileSync('bash', ['-c', `cp -a "${nested}"/. "${dbDir}/" && rm -rf "${nested}"`]);

Check warning

Code scanning / CodeQL

Shell command built from environment values Medium test

This shell command depends on an uncontrolled
absolute path
.
This shell command depends on an uncontrolled
absolute path
.
This shell command depends on an uncontrolled
absolute path
.

Copilot Autofix

AI 1 day ago

In general, to fix this kind of problem you should avoid passing dynamically constructed command strings to a shell (bash -c or sh -c). Instead, either (1) call the underlying program directly with arguments passed as an array, so that they are not interpreted by the shell, or (2) implement the needed filesystem operations directly in your language using standard libraries. Both approaches prevent shell metacharacters in paths from changing the command’s meaning.

For this specific case, the shell is only being used to run cp -a "${nested}"/. "${dbDir}/" && rm -rf "${nested}", which copies the contents of one directory to another and then deletes the source. We can replicate this logic in pure Node.js using fs operations, avoiding bash entirely. Since the surrounding code already uses synchronous filesystem operations (existsSync, mkdirSync, readdirSync, statSync), it is consistent to use synchronous copy and delete functions as well.

The best minimal-impact fix is:

  • Remove the bash usage and implement a small helper that:
    • Ensures the destination directory exists (mkdirSync with { recursive: true }).
    • Recursively copies all files and subdirectories from nested into dbDir. The Node.js fs module (from Node 16.7+ / 18+ depending on tooling) provides cpSync for this, which preserves directory structure and can copy recursively. If available in the project’s runtime, this is the simplest and most robust choice.
    • Removes the nested directory once the copy succeeds, using rmSync(nested, { recursive: true, force: true }) (Node 14.14+).
  • To avoid broad changes, add cpSync and rmSync to the existing fs import in extensions/vscode/test/extended/download-databases.ts and replace only the execFileSync('bash', ...) call with these fs operations.

Concretely:

  • In extensions/vscode/test/extended/download-databases.ts, update the import from fs to include cpSync and rmSync.
  • Replace lines 131–133 (the comment and the execFileSync('bash', ...) line) with a comment plus equivalent cpSync and rmSync calls. Keep the behavior (copy contents of nested into dbDir and then delete nested), and preserve the directory structure.

No changes are required in run-extended-tests.ts; once the shell usage is removed, all three CodeQL variants will be addressed because the tainted path no longer reaches a shell sink.

Suggested changeset 1
extensions/vscode/test/extended/download-databases.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/extensions/vscode/test/extended/download-databases.ts b/extensions/vscode/test/extended/download-databases.ts
--- a/extensions/vscode/test/extended/download-databases.ts
+++ b/extensions/vscode/test/extended/download-databases.ts
@@ -8,7 +8,7 @@
  * Downloads are cached on disk and reused if less than 24 hours old.
  */
 
-import { createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, statSync } from 'fs';
+import { createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, statSync, cpSync, rmSync } from 'fs';
 import { join } from 'path';
 import { execFileSync } from 'child_process';
 import { homedir } from 'os';
@@ -129,7 +129,8 @@
       const nested = join(dbDir, entries[0]);
       if (existsSync(join(nested, 'codeql-database.yml'))) {
         // Copy all contents up, then remove the nested directory
-        execFileSync('bash', ['-c', `cp -a "${nested}"/. "${dbDir}/" && rm -rf "${nested}"`]);
+        cpSync(nested, dbDir, { recursive: true });
+        rmSync(nested, { recursive: true, force: true });
       }
     }
 
EOF
@@ -8,7 +8,7 @@
* Downloads are cached on disk and reused if less than 24 hours old.
*/

import { createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, statSync } from 'fs';
import { createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, statSync, cpSync, rmSync } from 'fs';
import { join } from 'path';
import { execFileSync } from 'child_process';
import { homedir } from 'os';
@@ -129,7 +129,8 @@
const nested = join(dbDir, entries[0]);
if (existsSync(join(nested, 'codeql-database.yml'))) {
// Copy all contents up, then remove the nested directory
execFileSync('bash', ['-c', `cp -a "${nested}"/. "${dbDir}/" && rm -rf "${nested}"`]);
cpSync(nested, dbDir, { recursive: true });
rmSync(nested, { recursive: true, force: true });
}
}

Copilot is powered by AI and may make mistakes. Always verify output.
}
Comment on lines +129 to +133
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flattening with bash -c cp ... && rm -rf ... is not cross-platform (Windows) and assumes GNU tools. Prefer implementing the flatten step with Node fs APIs so the downloader works consistently across dev environments.

Copilot uses AI. Check for mistakes.
}

if (!existsSync(markerFile)) {
console.error(` ✗ Extraction failed: ${markerFile} not found`);
return null;
}

// Clean up zip
try { const { unlinkSync } = await import('fs'); unlinkSync(zipPath); } catch { /* best effort */ }

console.log(` ✓ Ready: ${owner}/${repoName} (${language})`);
return dbDir;
} catch (err) {
console.error(` ✗ Error downloading ${owner}/${repoName}: ${err}`);
return null;
}
}

/**
* Get the default vscode-codeql global storage paths (platform-dependent).
*/
function getVscodeCodeqlStoragePaths(): string[] {
const home = homedir();
const candidates = [
join(home, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'GitHub.vscode-codeql'),
join(home, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'github.vscode-codeql'),
join(home, '.config', 'Code', 'User', 'globalStorage', 'GitHub.vscode-codeql'),
join(home, '.config', 'Code', 'User', 'globalStorage', 'github.vscode-codeql'),
];
Comment on lines +157 to +162
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This list of vscode-codeql globalStorage locations only covers macOS/Linux. On Windows, the globalStorage directory is typically under %APPDATA%/Code/User/globalStorage/...; without adding Windows candidates, discovery will miss existing databases unless users manually configure search dirs.

Suggested change
const candidates = [
join(home, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'GitHub.vscode-codeql'),
join(home, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'github.vscode-codeql'),
join(home, '.config', 'Code', 'User', 'globalStorage', 'GitHub.vscode-codeql'),
join(home, '.config', 'Code', 'User', 'globalStorage', 'github.vscode-codeql'),
];
const candidates = [
// macOS
join(home, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'GitHub.vscode-codeql'),
join(home, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'github.vscode-codeql'),
// Linux
join(home, '.config', 'Code', 'User', 'globalStorage', 'GitHub.vscode-codeql'),
join(home, '.config', 'Code', 'User', 'globalStorage', 'github.vscode-codeql'),
];
// Windows: %APPDATA%\Code\User\globalStorage\GitHub.vscode-codeql
if (process.platform === 'win32' && process.env.APPDATA) {
candidates.push(
join(process.env.APPDATA, 'Code', 'User', 'globalStorage', 'GitHub.vscode-codeql'),
join(process.env.APPDATA, 'Code', 'User', 'globalStorage', 'github.vscode-codeql'),
);
}

Copilot uses AI. Check for mistakes.
return candidates.filter(p => existsSync(p));
}

/**
* Scan directories for CodeQL databases (by codeql-database.yml marker).
*/
function scanForDatabases(dir: string, found: Map<string, { language: string; path: string }>, depth: number): void {
if (depth > 4) return;
const markerPath = join(dir, 'codeql-database.yml');
if (existsSync(markerPath)) {
try {
const yml = readFileSync(markerPath, 'utf8');
const langMatch = yml.match(/primaryLanguage:\s*(\S+)/);
found.set(dir, { language: langMatch?.[1] ?? 'unknown', path: dir });
} catch { /* skip */ }
return;
}
try {
for (const entry of readdirSync(dir)) {
if (entry.startsWith('.') || entry === 'node_modules') continue;
const full = join(dir, entry);
try { if (statSync(full).isDirectory()) scanForDatabases(full, found, depth + 1); } catch { /* skip */ }
}
} catch { /* skip */ }
}

/**
* Discover and/or download databases for the requested repos.
* Returns a map of "owner/repo" → database path.
*/
export async function resolveAllDatabases(
repos: RepoConfig[],
additionalDirs: string[],
): Promise<{ databases: Map<string, string>; missing: RepoConfig[] }> {
const databases = new Map<string, string>();
const missing: RepoConfig[] = [];

// First: discover existing databases on disk
const searchDirs = [...additionalDirs, ...getVscodeCodeqlStoragePaths()];
const envDirs = process.env.CODEQL_DATABASES_BASE_DIRS;
if (envDirs) searchDirs.push(...envDirs.split(':').filter(Boolean));

Comment on lines +202 to +204
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This splits CODEQL_DATABASES_BASE_DIRS on ':' but the server expects a platform-delimited list (path.delimiter). Use path.delimiter here to support Windows (';') and keep behavior consistent with server discovery-config parsing.

Copilot uses AI. Check for mistakes.
console.log(` Searching ${searchDirs.length} directories for existing databases...`);
const existing = new Map<string, { language: string; path: string }>();
for (const dir of searchDirs) {
if (existsSync(dir)) scanForDatabases(dir, existing, 0);
}
console.log(` Found ${existing.size} existing database(s) on disk`);

// Match existing databases to requested repos
for (const repo of repos) {
let found = false;
for (const [dbPath, info] of existing) {
if (info.language === repo.language) {
const pathLower = dbPath.toLowerCase();
if (pathLower.includes(repo.repo.toLowerCase()) || pathLower.includes(repo.owner.toLowerCase())) {
databases.set(`${repo.owner}/${repo.repo}`, dbPath);
found = true;
console.log(` ✓ Found: ${repo.owner}/${repo.repo} → ${dbPath}`);
break;
}
}
}
if (!found) missing.push(repo);
}

// Second: try to download missing databases
if (missing.length > 0) {
const token = await getGitHubToken();
if (token) {
console.log(`\n ⬇ Attempting to download ${missing.length} missing database(s)...`);
const downloadDir = additionalDirs[0] || join(homedir(), '.codeql-mcp-test-databases');
mkdirSync(downloadDir, { recursive: true });

const stillMissing: RepoConfig[] = [];
for (const repo of missing) {
const dbPath = await downloadDatabase(repo, downloadDir, token);
if (dbPath) {
databases.set(`${repo.owner}/${repo.repo}`, dbPath);
} else {
stillMissing.push(repo);
}
}
return { databases, missing: stillMissing };
} else {
console.log(`\n ⚠️ No GitHub token available for downloading missing databases.`);
console.log(` 💡 Options to provide databases:`);
console.log(` 1. Open VS Code, use "CodeQL: Download Database from GitHub"`);
console.log(` 2. Set GH_TOKEN env var for automatic download`);
console.log(` 3. Set CODEQL_DATABASES_BASE_DIRS to point to existing databases`);
}
}

return { databases, missing };
}

37 changes: 37 additions & 0 deletions extensions/vscode/test/extended/repos.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"description": "Repositories for extended MCP integration testing with CallGraphFromTo source/target function pairs.",
"repositories": [
{
"owner": "gin-gonic",
"repo": "gin",
"language": "go",
"callGraphFromTo": { "sourceFunction": "handleHTTPRequest", "targetFunction": "ServeHTTP" }
},
{
"owner": "expressjs",
"repo": "express",
"language": "javascript",
"callGraphFromTo": { "sourceFunction": "json", "targetFunction": "send" }
},
{
"owner": "checkstyle",
"repo": "checkstyle",
"language": "java",
"callGraphFromTo": { "sourceFunction": "process", "targetFunction": "log" }
},
{
"owner": "PyCQA",
"repo": "flake8",
"language": "python",
"callGraphFromTo": { "sourceFunction": "run", "targetFunction": "report" }
}
],
"settings": {
"databaseDir": ".tmp/extended-test-databases",
"fixtureSearchDirs": [
"test/fixtures/single-folder-workspace/codeql-storage/databases",
"test/fixtures/multi-root-workspace/folder-a/codeql-storage/databases"
],
"timeoutMs": 600000
}
}
Loading
Loading