Skip to content
Merged
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
4 changes: 4 additions & 0 deletions extensions/ql-vscode/media/dark/github.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions extensions/ql-vscode/media/light/github.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
27 changes: 27 additions & 0 deletions extensions/ql-vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"onCommand:codeQLDatabases.chooseDatabaseFolder",
"onCommand:codeQLDatabases.chooseDatabaseArchive",
"onCommand:codeQLDatabases.chooseDatabaseInternet",
"onCommand:codeQLDatabases.chooseDatabaseGithub",
"onCommand:codeQLDatabases.chooseDatabaseLgtm",
"onCommand:codeQL.setCurrentDatabase",
"onCommand:codeQL.viewAst",
Expand All @@ -54,6 +55,7 @@
"onCommand:codeQL.chooseDatabaseFolder",
"onCommand:codeQL.chooseDatabaseArchive",
"onCommand:codeQL.chooseDatabaseInternet",
"onCommand:codeQL.chooseDatabaseGithub",
"onCommand:codeQL.chooseDatabaseLgtm",
"onCommand:codeQLDatabases.chooseDatabase",
"onCommand:codeQLDatabases.setCurrentDatabase",
Expand Down Expand Up @@ -356,6 +358,14 @@
"dark": "media/dark/cloud-download.svg"
}
},
{
"command": "codeQLDatabases.chooseDatabaseGithub",
"title": "Download Database from GitHub",
"icon": {
"light": "media/light/github.svg",
"dark": "media/dark/github.svg"
}
},
{
"command": "codeQLDatabases.chooseDatabaseLgtm",
"title": "Download from LGTM",
Expand Down Expand Up @@ -428,6 +438,10 @@
"command": "codeQL.chooseDatabaseInternet",
"title": "CodeQL: Download Database"
},
{
"command": "codeQL.chooseDatabaseGithub",
"title": "CodeQL: Download Database from GitHub"
},
{
"command": "codeQL.chooseDatabaseLgtm",
"title": "CodeQL: Download Database from LGTM"
Expand Down Expand Up @@ -604,6 +618,11 @@
"when": "view == codeQLDatabases",
"group": "navigation"
},
{
"command": "codeQLDatabases.chooseDatabaseGithub",
"when": "config.codeQL.canary && view == codeQLDatabases",
"group": "navigation"
},
{
"command": "codeQLDatabases.chooseDatabaseLgtm",
"when": "view == codeQLDatabases",
Expand Down Expand Up @@ -829,6 +848,10 @@
"command": "codeQL.viewCfg",
"when": "resourceScheme == codeql-zip-archive && config.codeQL.canary"
},
{
"command": "codeQL.chooseDatabaseGithub",
"when": "config.codeQL.canary"
},
{
"command": "codeQLDatabases.setCurrentDatabase",
"when": "false"
Expand Down Expand Up @@ -873,6 +896,10 @@
"command": "codeQLDatabases.chooseDatabaseInternet",
"when": "false"
},
{
"command": "codeQLDatabases.chooseDatabaseGithub",
"when": "false"
},
{
"command": "codeQLDatabases.chooseDatabaseLgtm",
"when": "false"
Expand Down
121 changes: 118 additions & 3 deletions extensions/ql-vscode/src/databaseFetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import {
} from './commandRunner';
import { logger } from './logging';
import { tmpDir } from './helpers';
import { Credentials } from './authentication';
import { REPO_REGEX } from './pure/helpers-pure';

/**
* Prompts a user to fetch a database from a remote location. Database is assumed to be an archive file.
Expand All @@ -46,6 +48,7 @@ export async function promptImportInternetDatabase(

const item = await databaseArchiveFetcher(
databaseUrl,
{},
databaseManager,
storagePath,
progress,
Expand All @@ -61,6 +64,79 @@ export async function promptImportInternetDatabase(

}

/**
* Prompts a user to fetch a database from GitHub.
* User enters a GitHub repository and then the user is asked which language
* to download (if there is more than one)
*
* @param databaseManager the DatabaseManager
* @param storagePath where to store the unzipped database.
*/
export async function promptImportGithubDatabase(
databaseManager: DatabaseManager,
storagePath: string,
credentials: Credentials,
progress: ProgressCallback,
token: CancellationToken,
cli?: CodeQLCliServer
): Promise<DatabaseItem | undefined> {
progress({
message: 'Choose repository',
step: 1,
maxStep: 2
});
const githubRepo = await window.showInputBox({
title: 'Enter a GitHub repository in the format <owner>/<repo> (e.g. github/codeql)',
placeHolder: '<owner>/<repo>',
ignoreFocusOut: true,
});
if (!githubRepo) {
return;
}

if (!REPO_REGEX.test(githubRepo)) {
throw new Error(`Invalid GitHub repository: ${githubRepo}`);
}

const databaseUrl = await convertGithubNwoToDatabaseUrl(githubRepo, credentials, progress);
if (!databaseUrl) {
return;
}

const octokit = await credentials.getOctokit();
/**
* The 'token' property of the token object returned by `octokit.auth()`.
* The object is undocumented, but looks something like this:
* {
* token: 'xxxx',
* tokenType: 'oauth',
* type: 'token',
* }
* We only need the actual token string.
*/
const octokitToken = (await octokit.auth() as { token: string })?.token;
if (!octokitToken) {
// Just print a generic error message for now. Ideally we could show more debugging info, like the
// octokit object, but that would expose a user token.
throw new Error('Unable to get GitHub token.');
}
const item = await databaseArchiveFetcher(
databaseUrl,
{ 'Accept': 'application/zip', 'Authorization': `Bearer ${octokitToken}` },
databaseManager,
storagePath,
progress,
token,
cli
);
if (item) {
await commands.executeCommand('codeQLDatabases.focus');
void showAndLogInformationMessage('Database downloaded and imported successfully.');
return item;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Minor: Move this up into the if(item) case, so it's more obvious what's going on. The return at the end of the function will handle all the fall-through cases where we didn't return early.

}
return;
}

/**
* Prompts a user to fetch a database from lgtm.
* User enters a project url and then the user is asked which language
Expand Down Expand Up @@ -94,6 +170,7 @@ export async function promptImportLgtmDatabase(
if (databaseUrl) {
const item = await databaseArchiveFetcher(
databaseUrl,
{},
databaseManager,
storagePath,
progress,
Expand Down Expand Up @@ -140,6 +217,7 @@ export async function importArchiveDatabase(
try {
const item = await databaseArchiveFetcher(
databaseUrl,
{},
databaseManager,
storagePath,
progress,
Expand All @@ -166,13 +244,15 @@ export async function importArchiveDatabase(
* or in the local filesystem.
*
* @param databaseUrl URL from which to grab the database
* @param requestHeaders Headers to send with the request
* @param databaseManager the DatabaseManager
* @param storagePath where to store the unzipped database.
* @param progress callback to send progress messages to
* @param token cancellation token
*/
async function databaseArchiveFetcher(
databaseUrl: string,
requestHeaders: { [key: string]: string },
Comment thread
shati-patel marked this conversation as resolved.
databaseManager: DatabaseManager,
storagePath: string,
progress: ProgressCallback,
Expand All @@ -193,7 +273,7 @@ async function databaseArchiveFetcher(
if (isFile(databaseUrl)) {
await readAndUnzip(databaseUrl, unzipPath, cli, progress);
} else {
await fetchAndUnzip(databaseUrl, unzipPath, cli, progress);
await fetchAndUnzip(databaseUrl, requestHeaders, unzipPath, cli, progress);
}

progress({
Expand Down Expand Up @@ -292,6 +372,7 @@ async function readAndUnzip(

async function fetchAndUnzip(
databaseUrl: string,
requestHeaders: { [key: string]: string },
unzipPath: string,
cli?: CodeQLCliServer,
progress?: ProgressCallback
Expand All @@ -310,7 +391,10 @@ async function fetchAndUnzip(
step: 1,
});

const response = await checkForFailingResponse(await fetch(databaseUrl), 'Error downloading database');
const response = await checkForFailingResponse(
await fetch(databaseUrl, { headers: requestHeaders }),
'Error downloading database'
);
const archiveFileStream = fs.createWriteStream(archivePath);

const contentLength = response.headers.get('content-length');
Expand Down Expand Up @@ -381,6 +465,37 @@ export async function findDirWithFile(
return;
}

export async function convertGithubNwoToDatabaseUrl(
githubRepo: string,
credentials: Credentials,
progress: ProgressCallback): Promise<string | undefined> {
try {
// TODO: In future, we could accept GitHub URLs in addition to NWOs.
// Similar to "looksLikeLgtmUrl".
if (!REPO_REGEX.test(githubRepo)) {
throw new Error('Invalid repository format. Must be in the format <owner>/<repo> (e.g. github/codeql)');
}

const [owner, repo] = githubRepo.split('/');

const octokit = await credentials.getOctokit();
const response = await octokit.request('GET /repos/:owner/:repo/code-scanning/codeql/databases', { owner, repo });
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm pretty sure we're ok here, but can you make sure that we can handle misplaced cApS? Remember the boinc/BOINC repo that gave us trouble for LGTM?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Also, does this support anonymous downloading of public repos?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The API currently does not allow anonymous requests because it is only staff shipped. Even in the private beta it will still require logging in. Once we get to public beta it will likely start accepting anonymous requests for public repos.

However for the extension I'd argue we still want to always make the user log in. It'll simplify our lives because we only have to handle one case. And I would expect the requirement to log in is not a blocked to users here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for the explanation. I think that's fine for now. It does seem like a small barrier for users who are not using any other github features. This is something we can discuss later when the API endpoint starts accepting anonymous requests.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm pretty sure we're ok here, but can you make sure that we can handle misplaced cApS? Remember the boinc/BOINC repo that gave us trouble for LGTM?

Good point, I hadn't thought of that 💪🏽 From a bit of manual testing, it looks like the API is case-insensitive (e.g. GitHub/CodeQL returns the same as github/codeql). So I think we're okay here!

Also, does this support anonymous downloading of public repos?

Not yet. The entire MRVA API is still staff-only, so we need to pass in credentials for now 🔒


const languages = response.data.map((db: any) => db.language);

const language = await promptForLanguage(languages, progress);
if (!language) {
return;
}

return `https://api.github.com/repos/${owner}/${repo}/code-scanning/codeql/databases/${language}`;

} catch (e) {
void logger.log(`Error: ${e.message}`);
throw new Error(`Unable to get database for '${githubRepo}'`);
}
}

/**
* The URL pattern is https://lgtm.com/projects/{provider}/{org}/{name}/{irrelevant-subpages}.
* There are several possibilities for the provider: in addition to GitHub.com (g),
Expand Down Expand Up @@ -506,7 +621,7 @@ async function promptForLanguage(
maxStep: 2
});
if (!languages.length) {
return;
throw new Error('No databases found');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is also a change to downloading LGTM databases. I can't remember if there was a reason why we didn't throw when there were no languages found for an LGTM project. Can you just make sure that nothing awkward happens when downloading LGTM dbs?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I wasn't able to find an LGTM project that doesn't have any languages, so I suspect that's a sufficiently rare case that we've never needed to handle it previously 😅

}
if (languages.length === 1) {
return languages[0];
Expand Down
34 changes: 33 additions & 1 deletion extensions/ql-vscode/src/databases-ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,13 @@ import * as qsClient from './queryserver-client';
import { upgradeDatabaseExplicit } from './upgrades';
import {
importArchiveDatabase,
promptImportGithubDatabase,
promptImportInternetDatabase,
promptImportLgtmDatabase,
} from './databaseFetcher';
import { CancellationToken } from 'vscode';
import { asyncFilter } from './pure/helpers-pure';
import { Credentials } from './authentication';

type ThemableIconPath = { light: string; dark: string } | string;

Expand Down Expand Up @@ -219,7 +221,8 @@ export class DatabaseUI extends DisposableObject {
private databaseManager: DatabaseManager,
private readonly queryServer: qsClient.QueryServerClient | undefined,
private readonly storagePath: string,
readonly extensionPath: string
readonly extensionPath: string,
private readonly getCredentials: () => Promise<Credentials>
) {
super();

Expand Down Expand Up @@ -291,6 +294,20 @@ export class DatabaseUI extends DisposableObject {
}
)
);
this.push(
commandRunnerWithProgress(
'codeQLDatabases.chooseDatabaseGithub',
async (
progress: ProgressCallback,
token: CancellationToken
) => {
const credentials = await this.getCredentials();
await this.handleChooseDatabaseGithub(credentials, progress, token);
},
{
title: 'Adding database from GitHub',
})
);
this.push(
commandRunnerWithProgress(
'codeQLDatabases.chooseDatabaseLgtm',
Expand Down Expand Up @@ -462,6 +479,21 @@ export class DatabaseUI extends DisposableObject {
);
};

handleChooseDatabaseGithub = async (
credentials: Credentials,
progress: ProgressCallback,
token: CancellationToken
): Promise<DatabaseItem | undefined> => {
return await promptImportGithubDatabase(
this.databaseManager,
this.storagePath,
credentials,
progress,
token,
this.queryServer?.cliServer
);
};

handleChooseDatabaseLgtm = async (
progress: ProgressCallback,
token: CancellationToken
Expand Down
15 changes: 14 additions & 1 deletion extensions/ql-vscode/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,8 @@ async function activateWithInstalledDistribution(
dbm,
qs,
getContextStoragePath(ctx),
ctx.extensionPath
ctx.extensionPath,
() => Credentials.initialize(ctx),
);
databaseUI.init();
ctx.subscriptions.push(databaseUI);
Expand Down Expand Up @@ -931,6 +932,18 @@ async function activateWithInstalledDistribution(
title: 'Choose a Database from an Archive'
})
);
ctx.subscriptions.push(
commandRunnerWithProgress('codeQL.chooseDatabaseGithub', async (
progress: ProgressCallback,
token: CancellationToken
) => {
const credentials = await Credentials.initialize(ctx);
await databaseUI.handleChooseDatabaseGithub(credentials, progress, token);
},
{
title: 'Adding database from GitHub',
})
);
ctx.subscriptions.push(
commandRunnerWithProgress('codeQL.chooseDatabaseLgtm', (
progress: ProgressCallback,
Expand Down
Loading