Skip to content

Commit 90d636a

Browse files
authored
Download databases from GitHub (#1229)
1 parent 3e3e12a commit 90d636a

10 files changed

Lines changed: 347 additions & 13 deletions

File tree

Lines changed: 4 additions & 0 deletions
Loading
Lines changed: 11 additions & 0 deletions
Loading

extensions/ql-vscode/package.json

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"onCommand:codeQLDatabases.chooseDatabaseFolder",
4646
"onCommand:codeQLDatabases.chooseDatabaseArchive",
4747
"onCommand:codeQLDatabases.chooseDatabaseInternet",
48+
"onCommand:codeQLDatabases.chooseDatabaseGithub",
4849
"onCommand:codeQLDatabases.chooseDatabaseLgtm",
4950
"onCommand:codeQL.setCurrentDatabase",
5051
"onCommand:codeQL.viewAst",
@@ -54,6 +55,7 @@
5455
"onCommand:codeQL.chooseDatabaseFolder",
5556
"onCommand:codeQL.chooseDatabaseArchive",
5657
"onCommand:codeQL.chooseDatabaseInternet",
58+
"onCommand:codeQL.chooseDatabaseGithub",
5759
"onCommand:codeQL.chooseDatabaseLgtm",
5860
"onCommand:codeQLDatabases.chooseDatabase",
5961
"onCommand:codeQLDatabases.setCurrentDatabase",
@@ -356,6 +358,14 @@
356358
"dark": "media/dark/cloud-download.svg"
357359
}
358360
},
361+
{
362+
"command": "codeQLDatabases.chooseDatabaseGithub",
363+
"title": "Download Database from GitHub",
364+
"icon": {
365+
"light": "media/light/github.svg",
366+
"dark": "media/dark/github.svg"
367+
}
368+
},
359369
{
360370
"command": "codeQLDatabases.chooseDatabaseLgtm",
361371
"title": "Download from LGTM",
@@ -428,6 +438,10 @@
428438
"command": "codeQL.chooseDatabaseInternet",
429439
"title": "CodeQL: Download Database"
430440
},
441+
{
442+
"command": "codeQL.chooseDatabaseGithub",
443+
"title": "CodeQL: Download Database from GitHub"
444+
},
431445
{
432446
"command": "codeQL.chooseDatabaseLgtm",
433447
"title": "CodeQL: Download Database from LGTM"
@@ -604,6 +618,11 @@
604618
"when": "view == codeQLDatabases",
605619
"group": "navigation"
606620
},
621+
{
622+
"command": "codeQLDatabases.chooseDatabaseGithub",
623+
"when": "config.codeQL.canary && view == codeQLDatabases",
624+
"group": "navigation"
625+
},
607626
{
608627
"command": "codeQLDatabases.chooseDatabaseLgtm",
609628
"when": "view == codeQLDatabases",
@@ -829,6 +848,10 @@
829848
"command": "codeQL.viewCfg",
830849
"when": "resourceScheme == codeql-zip-archive && config.codeQL.canary"
831850
},
851+
{
852+
"command": "codeQL.chooseDatabaseGithub",
853+
"when": "config.codeQL.canary"
854+
},
832855
{
833856
"command": "codeQLDatabases.setCurrentDatabase",
834857
"when": "false"
@@ -873,6 +896,10 @@
873896
"command": "codeQLDatabases.chooseDatabaseInternet",
874897
"when": "false"
875898
},
899+
{
900+
"command": "codeQLDatabases.chooseDatabaseGithub",
901+
"when": "false"
902+
},
876903
{
877904
"command": "codeQLDatabases.chooseDatabaseLgtm",
878905
"when": "false"

extensions/ql-vscode/src/databaseFetcher.ts

Lines changed: 118 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import {
2121
} from './commandRunner';
2222
import { logger } from './logging';
2323
import { tmpDir } from './helpers';
24+
import { Credentials } from './authentication';
25+
import { REPO_REGEX } from './pure/helpers-pure';
2426

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

4749
const item = await databaseArchiveFetcher(
4850
databaseUrl,
51+
{},
4952
databaseManager,
5053
storagePath,
5154
progress,
@@ -61,6 +64,79 @@ export async function promptImportInternetDatabase(
6164

6265
}
6366

67+
/**
68+
* Prompts a user to fetch a database from GitHub.
69+
* User enters a GitHub repository and then the user is asked which language
70+
* to download (if there is more than one)
71+
*
72+
* @param databaseManager the DatabaseManager
73+
* @param storagePath where to store the unzipped database.
74+
*/
75+
export async function promptImportGithubDatabase(
76+
databaseManager: DatabaseManager,
77+
storagePath: string,
78+
credentials: Credentials,
79+
progress: ProgressCallback,
80+
token: CancellationToken,
81+
cli?: CodeQLCliServer
82+
): Promise<DatabaseItem | undefined> {
83+
progress({
84+
message: 'Choose repository',
85+
step: 1,
86+
maxStep: 2
87+
});
88+
const githubRepo = await window.showInputBox({
89+
title: 'Enter a GitHub repository in the format <owner>/<repo> (e.g. github/codeql)',
90+
placeHolder: '<owner>/<repo>',
91+
ignoreFocusOut: true,
92+
});
93+
if (!githubRepo) {
94+
return;
95+
}
96+
97+
if (!REPO_REGEX.test(githubRepo)) {
98+
throw new Error(`Invalid GitHub repository: ${githubRepo}`);
99+
}
100+
101+
const databaseUrl = await convertGithubNwoToDatabaseUrl(githubRepo, credentials, progress);
102+
if (!databaseUrl) {
103+
return;
104+
}
105+
106+
const octokit = await credentials.getOctokit();
107+
/**
108+
* The 'token' property of the token object returned by `octokit.auth()`.
109+
* The object is undocumented, but looks something like this:
110+
* {
111+
* token: 'xxxx',
112+
* tokenType: 'oauth',
113+
* type: 'token',
114+
* }
115+
* We only need the actual token string.
116+
*/
117+
const octokitToken = (await octokit.auth() as { token: string })?.token;
118+
if (!octokitToken) {
119+
// Just print a generic error message for now. Ideally we could show more debugging info, like the
120+
// octokit object, but that would expose a user token.
121+
throw new Error('Unable to get GitHub token.');
122+
}
123+
const item = await databaseArchiveFetcher(
124+
databaseUrl,
125+
{ 'Accept': 'application/zip', 'Authorization': `Bearer ${octokitToken}` },
126+
databaseManager,
127+
storagePath,
128+
progress,
129+
token,
130+
cli
131+
);
132+
if (item) {
133+
await commands.executeCommand('codeQLDatabases.focus');
134+
void showAndLogInformationMessage('Database downloaded and imported successfully.');
135+
return item;
136+
}
137+
return;
138+
}
139+
64140
/**
65141
* Prompts a user to fetch a database from lgtm.
66142
* User enters a project url and then the user is asked which language
@@ -94,6 +170,7 @@ export async function promptImportLgtmDatabase(
94170
if (databaseUrl) {
95171
const item = await databaseArchiveFetcher(
96172
databaseUrl,
173+
{},
97174
databaseManager,
98175
storagePath,
99176
progress,
@@ -140,6 +217,7 @@ export async function importArchiveDatabase(
140217
try {
141218
const item = await databaseArchiveFetcher(
142219
databaseUrl,
220+
{},
143221
databaseManager,
144222
storagePath,
145223
progress,
@@ -166,13 +244,15 @@ export async function importArchiveDatabase(
166244
* or in the local filesystem.
167245
*
168246
* @param databaseUrl URL from which to grab the database
247+
* @param requestHeaders Headers to send with the request
169248
* @param databaseManager the DatabaseManager
170249
* @param storagePath where to store the unzipped database.
171250
* @param progress callback to send progress messages to
172251
* @param token cancellation token
173252
*/
174253
async function databaseArchiveFetcher(
175254
databaseUrl: string,
255+
requestHeaders: { [key: string]: string },
176256
databaseManager: DatabaseManager,
177257
storagePath: string,
178258
progress: ProgressCallback,
@@ -193,7 +273,7 @@ async function databaseArchiveFetcher(
193273
if (isFile(databaseUrl)) {
194274
await readAndUnzip(databaseUrl, unzipPath, cli, progress);
195275
} else {
196-
await fetchAndUnzip(databaseUrl, unzipPath, cli, progress);
276+
await fetchAndUnzip(databaseUrl, requestHeaders, unzipPath, cli, progress);
197277
}
198278

199279
progress({
@@ -292,6 +372,7 @@ async function readAndUnzip(
292372

293373
async function fetchAndUnzip(
294374
databaseUrl: string,
375+
requestHeaders: { [key: string]: string },
295376
unzipPath: string,
296377
cli?: CodeQLCliServer,
297378
progress?: ProgressCallback
@@ -310,7 +391,10 @@ async function fetchAndUnzip(
310391
step: 1,
311392
});
312393

313-
const response = await checkForFailingResponse(await fetch(databaseUrl), 'Error downloading database');
394+
const response = await checkForFailingResponse(
395+
await fetch(databaseUrl, { headers: requestHeaders }),
396+
'Error downloading database'
397+
);
314398
const archiveFileStream = fs.createWriteStream(archivePath);
315399

316400
const contentLength = response.headers.get('content-length');
@@ -381,6 +465,37 @@ export async function findDirWithFile(
381465
return;
382466
}
383467

468+
export async function convertGithubNwoToDatabaseUrl(
469+
githubRepo: string,
470+
credentials: Credentials,
471+
progress: ProgressCallback): Promise<string | undefined> {
472+
try {
473+
// TODO: In future, we could accept GitHub URLs in addition to NWOs.
474+
// Similar to "looksLikeLgtmUrl".
475+
if (!REPO_REGEX.test(githubRepo)) {
476+
throw new Error('Invalid repository format. Must be in the format <owner>/<repo> (e.g. github/codeql)');
477+
}
478+
479+
const [owner, repo] = githubRepo.split('/');
480+
481+
const octokit = await credentials.getOctokit();
482+
const response = await octokit.request('GET /repos/:owner/:repo/code-scanning/codeql/databases', { owner, repo });
483+
484+
const languages = response.data.map((db: any) => db.language);
485+
486+
const language = await promptForLanguage(languages, progress);
487+
if (!language) {
488+
return;
489+
}
490+
491+
return `https://api.github.com/repos/${owner}/${repo}/code-scanning/codeql/databases/${language}`;
492+
493+
} catch (e) {
494+
void logger.log(`Error: ${e.message}`);
495+
throw new Error(`Unable to get database for '${githubRepo}'`);
496+
}
497+
}
498+
384499
/**
385500
* The URL pattern is https://lgtm.com/projects/{provider}/{org}/{name}/{irrelevant-subpages}.
386501
* There are several possibilities for the provider: in addition to GitHub.com (g),
@@ -506,7 +621,7 @@ async function promptForLanguage(
506621
maxStep: 2
507622
});
508623
if (!languages.length) {
509-
return;
624+
throw new Error('No databases found');
510625
}
511626
if (languages.length === 1) {
512627
return languages[0];

extensions/ql-vscode/src/databases-ui.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,13 @@ import * as qsClient from './queryserver-client';
3333
import { upgradeDatabaseExplicit } from './upgrades';
3434
import {
3535
importArchiveDatabase,
36+
promptImportGithubDatabase,
3637
promptImportInternetDatabase,
3738
promptImportLgtmDatabase,
3839
} from './databaseFetcher';
3940
import { CancellationToken } from 'vscode';
4041
import { asyncFilter } from './pure/helpers-pure';
42+
import { Credentials } from './authentication';
4143

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

@@ -219,7 +221,8 @@ export class DatabaseUI extends DisposableObject {
219221
private databaseManager: DatabaseManager,
220222
private readonly queryServer: qsClient.QueryServerClient | undefined,
221223
private readonly storagePath: string,
222-
readonly extensionPath: string
224+
readonly extensionPath: string,
225+
private readonly getCredentials: () => Promise<Credentials>
223226
) {
224227
super();
225228

@@ -291,6 +294,20 @@ export class DatabaseUI extends DisposableObject {
291294
}
292295
)
293296
);
297+
this.push(
298+
commandRunnerWithProgress(
299+
'codeQLDatabases.chooseDatabaseGithub',
300+
async (
301+
progress: ProgressCallback,
302+
token: CancellationToken
303+
) => {
304+
const credentials = await this.getCredentials();
305+
await this.handleChooseDatabaseGithub(credentials, progress, token);
306+
},
307+
{
308+
title: 'Adding database from GitHub',
309+
})
310+
);
294311
this.push(
295312
commandRunnerWithProgress(
296313
'codeQLDatabases.chooseDatabaseLgtm',
@@ -462,6 +479,21 @@ export class DatabaseUI extends DisposableObject {
462479
);
463480
};
464481

482+
handleChooseDatabaseGithub = async (
483+
credentials: Credentials,
484+
progress: ProgressCallback,
485+
token: CancellationToken
486+
): Promise<DatabaseItem | undefined> => {
487+
return await promptImportGithubDatabase(
488+
this.databaseManager,
489+
this.storagePath,
490+
credentials,
491+
progress,
492+
token,
493+
this.queryServer?.cliServer
494+
);
495+
};
496+
465497
handleChooseDatabaseLgtm = async (
466498
progress: ProgressCallback,
467499
token: CancellationToken

extensions/ql-vscode/src/extension.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -433,7 +433,8 @@ async function activateWithInstalledDistribution(
433433
dbm,
434434
qs,
435435
getContextStoragePath(ctx),
436-
ctx.extensionPath
436+
ctx.extensionPath,
437+
() => Credentials.initialize(ctx),
437438
);
438439
databaseUI.init();
439440
ctx.subscriptions.push(databaseUI);
@@ -931,6 +932,18 @@ async function activateWithInstalledDistribution(
931932
title: 'Choose a Database from an Archive'
932933
})
933934
);
935+
ctx.subscriptions.push(
936+
commandRunnerWithProgress('codeQL.chooseDatabaseGithub', async (
937+
progress: ProgressCallback,
938+
token: CancellationToken
939+
) => {
940+
const credentials = await Credentials.initialize(ctx);
941+
await databaseUI.handleChooseDatabaseGithub(credentials, progress, token);
942+
},
943+
{
944+
title: 'Adding database from GitHub',
945+
})
946+
);
934947
ctx.subscriptions.push(
935948
commandRunnerWithProgress('codeQL.chooseDatabaseLgtm', (
936949
progress: ProgressCallback,

0 commit comments

Comments
 (0)