Download databases from GitHub (#1229)

This commit is contained in:
Shati Patel 2022-03-25 15:24:09 +00:00 коммит произвёл GitHub
Родитель 3e3e12afb9
Коммит 90d636a026
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
10 изменённых файлов: 347 добавлений и 13 удалений

Просмотреть файл

@ -0,0 +1,4 @@
<!-- From https://github.com/microsoft/vscode-icons -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.97553 0C3.57186 0 0 3.57186 0 7.97553C0 11.4985 2.29969 14.4832 5.43119 15.5596C5.82263 15.6086 5.96942 15.3639 5.96942 15.1682C5.96942 14.9725 5.96942 14.4832 5.96942 13.7982C3.76758 14.2875 3.27829 12.7217 3.27829 12.7217C2.93578 11.792 2.39755 11.5474 2.39755 11.5474C1.66361 11.0581 2.44648 11.0581 2.44648 11.0581C3.22936 11.107 3.66972 11.8899 3.66972 11.8899C4.40367 13.1131 5.52905 12.7706 5.96942 12.5749C6.01835 12.0367 6.263 11.6942 6.45872 11.4985C4.69725 11.3028 2.83792 10.6177 2.83792 7.53517C2.83792 6.65443 3.1315 5.96942 3.66972 5.38226C3.62079 5.23547 3.32722 4.40367 3.76758 3.32722C3.76758 3.32722 4.4526 3.1315 5.96942 4.15902C6.6055 3.9633 7.29052 3.91437 7.97553 3.91437C8.66055 3.91437 9.34557 4.01223 9.98165 4.15902C11.4985 3.1315 12.1835 3.32722 12.1835 3.32722C12.6239 4.40367 12.3303 5.23547 12.2813 5.43119C12.7706 5.96942 13.1131 6.70336 13.1131 7.5841C13.1131 10.6667 11.2538 11.3028 9.49235 11.4985C9.78593 11.7431 10.0306 12.2324 10.0306 12.9664C10.0306 14.0428 10.0306 14.8746 10.0306 15.1682C10.0306 15.3639 10.1774 15.6086 10.5688 15.5596C13.7492 14.4832 16 11.4985 16 7.97553C15.9511 3.57186 12.3792 0 7.97553 0Z" fill="#C5C5C5"/>
</svg>

После

Ширина:  |  Высота:  |  Размер: 1.3 KiB

Просмотреть файл

@ -0,0 +1,11 @@
<!-- From https://github.com/microsoft/vscode-icons -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.97578 0C3.57211 0 0.000244141 3.57186 0.000244141 7.97553C0.000244141 11.4985 2.29994 14.4832 5.43144 15.5596C5.82287 15.6086 5.96966 15.3639 5.96966 15.1682C5.96966 14.9725 5.96966 14.4832 5.96966 13.7982C3.76783 14.2875 3.27853 12.7217 3.27853 12.7217C2.93602 11.792 2.3978 11.5474 2.3978 11.5474C1.66385 11.0581 2.44673 11.0581 2.44673 11.0581C3.2296 11.107 3.66997 11.8899 3.66997 11.8899C4.40391 13.1131 5.5293 12.7706 5.96966 12.5749C6.01859 12.0367 6.26324 11.6942 6.45896 11.4985C4.69749 11.3028 2.83816 10.6177 2.83816 7.53517C2.83816 6.65443 3.13174 5.96942 3.66997 5.38226C3.62104 5.23547 3.32746 4.40367 3.76783 3.32722C3.76783 3.32722 4.45284 3.1315 5.96966 4.15902C6.60575 3.9633 7.29076 3.91437 7.97578 3.91437C8.66079 3.91437 9.34581 4.01223 9.98189 4.15902C11.4987 3.1315 12.1837 3.32722 12.1837 3.32722C12.6241 4.40367 12.3305 5.23547 12.2816 5.43119C12.7709 5.96942 13.1134 6.70336 13.1134 7.5841C13.1134 10.6667 11.2541 11.3028 9.4926 11.4985C9.78618 11.7431 10.0308 12.2324 10.0308 12.9664C10.0308 14.0428 10.0308 14.8746 10.0308 15.1682C10.0308 15.3639 10.1776 15.6086 10.5691 15.5596C13.7495 14.4832 16.0002 11.4985 16.0002 7.97553C15.9513 3.57186 12.3794 0 7.97578 0Z" fill="#424242"/>
</g>
<defs>
<clipPath id="clip0">
<rect width="16" height="16" fill="white" transform="translate(0.000244141)"/>
</clipPath>
</defs>
</svg>

После

Ширина:  |  Высота:  |  Размер: 1.5 KiB

Просмотреть файл

@ -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",
@ -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",
@ -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",
@ -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"
@ -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",
@ -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"
@ -873,6 +896,10 @@
"command": "codeQLDatabases.chooseDatabaseInternet",
"when": "false"
},
{
"command": "codeQLDatabases.chooseDatabaseGithub",
"when": "false"
},
{
"command": "codeQLDatabases.chooseDatabaseLgtm",
"when": "false"

Просмотреть файл

@ -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.
@ -46,6 +48,7 @@ export async function promptImportInternetDatabase(
const item = await databaseArchiveFetcher(
databaseUrl,
{},
databaseManager,
storagePath,
progress,
@ -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;
}
return;
}
/**
* Prompts a user to fetch a database from lgtm.
* User enters a project url and then the user is asked which language
@ -94,6 +170,7 @@ export async function promptImportLgtmDatabase(
if (databaseUrl) {
const item = await databaseArchiveFetcher(
databaseUrl,
{},
databaseManager,
storagePath,
progress,
@ -140,6 +217,7 @@ export async function importArchiveDatabase(
try {
const item = await databaseArchiveFetcher(
databaseUrl,
{},
databaseManager,
storagePath,
progress,
@ -166,6 +244,7 @@ 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
@ -173,6 +252,7 @@ export async function importArchiveDatabase(
*/
async function databaseArchiveFetcher(
databaseUrl: string,
requestHeaders: { [key: string]: string },
databaseManager: DatabaseManager,
storagePath: string,
progress: ProgressCallback,
@ -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({
@ -292,6 +372,7 @@ async function readAndUnzip(
async function fetchAndUnzip(
databaseUrl: string,
requestHeaders: { [key: string]: string },
unzipPath: string,
cli?: CodeQLCliServer,
progress?: ProgressCallback
@ -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');
@ -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 });
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),
@ -506,7 +621,7 @@ async function promptForLanguage(
maxStep: 2
});
if (!languages.length) {
return;
throw new Error('No databases found');
}
if (languages.length === 1) {
return languages[0];

Просмотреть файл

@ -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;
@ -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();
@ -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',
@ -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

Просмотреть файл

@ -433,7 +433,8 @@ async function activateWithInstalledDistribution(
dbm,
qs,
getContextStoragePath(ctx),
ctx.extensionPath
ctx.extensionPath,
() => Credentials.initialize(ctx),
);
databaseUI.init();
ctx.subscriptions.push(databaseUI);
@ -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,

Просмотреть файл

@ -35,3 +35,10 @@ export const ONE_DAY_IN_MS = 24 * 60 * 60 * 1000;
export const ONE_HOUR_IN_MS = 1000 * 60 * 60;
export const TWO_HOURS_IN_MS = 1000 * 60 * 60 * 2;
export const THREE_HOURS_IN_MS = 1000 * 60 * 60 * 3;
/**
* This regex matches strings of the form `owner/repo` where:
* - `owner` is made up of alphanumeric characters or single hyphens, starting and ending in an alphanumeric character
* - `repo` is made up of alphanumeric characters, hyphens, or underscores
*/
export const REPO_REGEX = /^(?:[a-zA-Z0-9]+-)*[a-zA-Z0-9]+\/[a-zA-Z0-9-_]+$/;

Просмотреть файл

@ -22,6 +22,7 @@ import { OctokitResponse } from '@octokit/types/dist-types';
import { RemoteQuery } from './remote-query';
import { RemoteQuerySubmissionResult } from './remote-query-submission-result';
import { QueryMetadata } from '../pure/interface-types';
import { REPO_REGEX } from '../pure/helpers-pure';
export interface QlPack {
name: string;
@ -38,13 +39,6 @@ interface QueriesResponse {
workflow_run_id: number
}
/**
* This regex matches strings of the form `owner/repo` where:
* - `owner` is made up of alphanumeric characters or single hyphens, starting and ending in an alphanumeric character
* - `repo` is made up of alphanumeric characters, hyphens, or underscores
*/
const REPO_REGEX = /^(?:[a-zA-Z0-9]+-)*[a-zA-Z0-9]+\/[a-zA-Z0-9-_]+$/;
/**
* Well-known names for the query pack used by the server.
*/

Просмотреть файл

@ -14,6 +14,9 @@ import {
findDirWithFile,
} from '../../databaseFetcher';
import { ProgressCallback } from '../../commandRunner';
import * as pq from 'proxyquire';
const proxyquire = pq.noPreserveCache();
chai.use(chaiAsPromised);
const expect = chai.expect;
@ -21,6 +24,132 @@ describe('databaseFetcher', function() {
// These tests make API calls and may need extra time to complete.
this.timeout(10000);
describe('convertGithubNwoToDatabaseUrl', () => {
let sandbox: sinon.SinonSandbox;
let quickPickSpy: sinon.SinonStub;
let progressSpy: ProgressCallback;
let mockRequest: sinon.SinonStub;
let mod: any;
const credentials = getMockCredentials(0);
beforeEach(() => {
sandbox = sinon.createSandbox();
quickPickSpy = sandbox.stub(window, 'showQuickPick');
progressSpy = sandbox.spy();
mockRequest = sandbox.stub();
mod = proxyquire('../../databaseFetcher', {
'./authentication': {
Credentials: credentials,
},
});
});
afterEach(() => {
sandbox.restore();
});
it('should convert a GitHub nwo to a database url', async () => {
// We can't make the real octokit request (since we need credentials), so we mock the response.
const mockApiResponse = {
data: [
{
id: 1495869,
name: 'csharp-database',
language: 'csharp',
uploader: {},
content_type: 'application/zip',
state: 'uploaded',
size: 55599715,
created_at: '2022-03-24T10:46:24Z',
updated_at: '2022-03-24T10:46:27Z',
url: 'https://api.github.com/repositories/143040428/code-scanning/codeql/databases/csharp',
},
{
id: 1100671,
name: 'database.zip',
language: 'javascript',
uploader: {},
content_type: 'application/zip',
state: 'uploaded',
size: 29294434,
created_at: '2022-03-01T16:00:04Z',
updated_at: '2022-03-01T16:00:06Z',
url: 'https://api.github.com/repositories/143040428/code-scanning/codeql/databases/javascript',
},
{
id: 648738,
name: 'ql-database',
language: 'ql',
uploader: {},
content_type: 'application/json; charset=utf-8',
state: 'uploaded',
size: 39735500,
created_at: '2022-02-02T09:38:50Z',
updated_at: '2022-02-02T09:38:51Z',
url: 'https://api.github.com/repositories/143040428/code-scanning/codeql/databases/ql',
},
],
};
mockRequest.resolves(mockApiResponse);
quickPickSpy.resolves('javascript');
const githubRepo = 'github/codeql';
const dbUrl = await mod.convertGithubNwoToDatabaseUrl(
githubRepo,
credentials,
progressSpy
);
expect(dbUrl).to.equal(
'https://api.github.com/repos/github/codeql/code-scanning/codeql/databases/javascript'
);
expect(quickPickSpy.firstCall.args[0]).to.deep.equal([
'csharp',
'javascript',
'ql',
]);
});
// Repository doesn't exist, or the user has no access to the repository.
it('should fail on an invalid/inaccessible repository', async () => {
const mockApiResponse = {
data: {
message: 'Not Found',
},
status: 404,
};
mockRequest.resolves(mockApiResponse);
const githubRepo = 'foo/bar-not-real';
await expect(
mod.convertGithubNwoToDatabaseUrl(githubRepo, credentials, progressSpy)
).to.be.rejectedWith(/Unable to get database/);
expect(progressSpy).to.have.callCount(0);
});
// User has access to the repository, but there are no databases for any language.
it('should fail on a repository with no databases', async () => {
const mockApiResponse = {
data: [],
};
mockRequest.resolves(mockApiResponse);
const githubRepo = 'foo/bar-with-no-dbs';
await expect(
mod.convertGithubNwoToDatabaseUrl(githubRepo, credentials, progressSpy)
).to.be.rejectedWith(/Unable to get database/);
expect(progressSpy).to.have.been.calledOnce;
});
function getMockCredentials(response: any) {
mockRequest = sinon.stub().resolves(response);
return {
getOctokit: () => ({
request: mockRequest,
}),
};
}
});
describe('convertLgtmUrlToDatabaseUrl', () => {
let sandbox: sinon.SinonSandbox;
let quickPickSpy: sinon.SinonStub;

Просмотреть файл

@ -8,6 +8,7 @@ import { Uri } from 'vscode';
import { DatabaseUI } from '../../databases-ui';
import { testDisposeHandler } from '../test-dispose-handler';
import { Credentials } from '../../authentication';
describe('databases-ui', () => {
describe('fixDbUri', () => {
@ -78,7 +79,8 @@ describe('databases-ui', () => {
} as any,
{} as any,
storageDir,
storageDir
storageDir,
() => Promise.resolve({} as Credentials),
);
await databaseUI.handleRemoveOrphanedDatabases();