Compare commits

...

38 Commits

Author SHA1 Message Date
Elena Tanasoiu
4499773f6f Merge pull request #1440 from github/v1.6.9
Some checks failed
Release / Release (push) Has been cancelled
Release / Publish to VS Code Marketplace (push) Has been cancelled
Release / Publish to Open VSX Registry (push) Has been cancelled
v1.6.9
2022-07-20 10:16:21 +01:00
Elena Tanasoiu
1d3b0e0ca9 v1.6.9 2022-07-20 10:01:12 +01:00
Elena Tanasoiu
98e503c768 Merge pull request #1438 from github/shati-patel/gist-description
MRVA: Fix Gist description when repository count is undefined
2022-07-20 09:46:22 +01:00
Elena Tanasoiu
62c3974d35 Check for undefined, null or zero repositories
`undefined`, `null` and 0 will evaluate to `false` so if we only want to
display the repository count when these values are not present we can
check for a truthy value:

```
query.repositoryCount ? `(${pluralize(...)})` : '';
```

instead of checking explicitly:

```
query.repositoryCount !== undefined && query.repositoryCount !== null && query.repositoryCount != 0 ? `(${pluralize(...)})` : '';
```
2022-07-20 09:30:54 +01:00
shati-patel
ab1c2e0a0d Explicitly check for undefined 2022-07-19 20:00:10 +01:00
shati-patel
d918c41197 Fix Gist description when repository count is undefined 2022-07-19 18:25:25 +01:00
Elena Tanasoiu
2cf5b39cfe Merge pull request #1432 from github/charisk-elena/result-count-on-history-labels
Add result count to remote queries in Query History
2022-07-19 13:50:22 +01:00
Elena Tanasoiu
13921bf8a2 Extract sum method for adding up repo results
When a queryResult is created, it comes with an array for AnalysisSummaries.
There is one summary per repository.

We've had to calculate the total number of results for all summaries in multiple
places, so let's extract a method for this as well.
2022-07-19 13:26:56 +01:00
Elena Tanasoiu
12a97ecba2 Shorten param forwarding for repositoryCount 2022-07-19 13:26:54 +01:00
Elena Tanasoiu
26529232f4 Rename numRepositoriesQueries to repositoryCount
To make it consistent with `resultCount`.
2022-07-19 13:25:48 +01:00
Elena Tanasoiu
1b425fc261 DRY up labels using the new pluralize method 2022-07-19 13:25:40 +01:00
Elena Tanasoiu
9c598c2f06 Extract pluralize method
There are at least 4 different files where this method could DRY things up,
so let's extract it.

I've chosen to move it to src/helpers.ts but happy to be told there's a better
place for shared utility methods like this one.
2022-07-19 12:32:24 +01:00
Elena Tanasoiu
99a784f072 Be able to sort remote queries by number of results
Previously we would set all remote query results to -1 when someone
attempted to sort queries.

We would then only sort local queries as those had access to the number
of results.

Let's include number of results for remote queries in the sorting.

Co-authored-by: Shati Patel <shati-patel@github.com>
2022-07-19 12:32:24 +01:00
Elena Tanasoiu
030488a459 Make local and remote query results match
In the previous commit we're now displaying number of results for remote
queries.

Previously we could only do this for local queries.

Let's make the format match for both types of queries by displaying
number of results in parentheses: `(x results)`.

Co-authored-by: Shati Patel <shati-patel@github.com>
2022-07-19 12:32:24 +01:00
Elena Tanasoiu
377f7965b1 Add result count to remote queries in Query History
When you run a remote query, we'd like to display more information about
it in the Query History panel.

At the moment we've improved this [1] by adding the language and number of repositories.

In this commit we're also adding the number of results for a remote query.

So the final format of the query history item will change from:

`<query_name> - <query_status>`

to

`<query_name> (<language>) on x repositories (y results) - <query_status>`

[1]: https://github.com/github/vscode-codeql/pull/1427

Co-authored-by: Charis Kyriakou <charisk@github.com>
Co-authored-by: Shati Patel <shati-patel@github.com>
2022-07-19 12:32:22 +01:00
Charis Kyriakou
651a6fbda8 Ensure completed flag is set on remote query history items (#1434) 2022-07-19 10:40:02 +01:00
Elena Tanasoiu
55ffdf7963 Merge pull request #1431 from github/shati-elena/rename-gist
Add useful information to MRVA gist titles
2022-07-19 09:11:47 +01:00
Elena Tanasoiu
cc907d2f31 Add test for exportResultsToGist method
While we're here we're also adding a test for the `exportResultsToGist`
method, as there were no tests for the `export-results.ts` file.

We initially attempted to add the test to the pure-tests folder, but the
`export-results.ts` file imports some components from `vscode`, which
meant we needed to set up the test in an environment where VSCode
dependencies are available.

We chose to add the test to `vscode-tests/no-workspace` for convenience,
as there are already other unit tests there.

We've also had to import our own query and analysis result to be able
to work with data closer to reality for exported results.

Since we've introduced functionality to build a gist title, let's check
that the `exportResultsToGist` method will forward the correct title to
the GitHub Actions API.

Co-authored-by: Shati Patel <shati-patel@github.com>
2022-07-18 19:52:51 +01:00
Elena Tanasoiu
c4df9dbec8 Extract method for creating Extension context
We'd like to re-use this to test the `exportResultsToGist` method in
`export-results.ts`.

So let's move it to a shared folder in the `vscode-tests/no-workspace` folder.

Since there's no `helper.ts` file in this folder and to avoid any confusion with
the `helpers.test.ts` file, I've opted to put this shared method into `index.ts`.

Happy to be told there's a better pattern for this as it doesn't feel very nice!
2022-07-18 19:22:44 +01:00
Elena Tanasoiu
c384a631dc Handle missing repo count gracefully
Let's handle this case gracefully and skip displaying the number of repositories
when they're not available.

Similarly let's add a check to see if we should pluralize the `repository` noun
or not.

Co-authored-by: Shati Patel <shati-patel@github.com>
2022-07-18 19:22:44 +01:00
Elena Tanasoiu
b079690f0e Add useful information to MRVA gist titles
All exported MRVA gists are given the name `CodeQL variant analysis
results', which makes it hard to work out what it contains at a glance.

We're adding more information in the gist title to make it more useful.

Example of new title:

`Empty Block (Go) x results (y repositories)`

This translates to:

`<query name> (<query language>) <number of results> results (<number of repositories> repositories)`

Co-authored-by: Shati Patel <shati-patel@github.com>
2022-07-18 19:22:41 +01:00
Elena Tanasoiu
4e863e995b Introduce method to add analysis results
We'd like to improve MRVA query gists by giving them more descriptive
titles that contain useful information about the query.

Let's add the number of query results to the title of the gist.

To do this we'll first need to count all the results provided to us in
the `analysisResults` array. There is an item in this array for each of
the repositories we've queried, so we're introducing a method to sum up
results for all the items in the array.

Co-authored-by: Shati Patel <shati-patel@github.com>
2022-07-18 19:20:58 +01:00
Shati Patel
f992679e94 MRVA: Include more info in query history label (#1427)
Co-authored-by: Elena Tanasoiu <elenatanasoiu@github.com>
2022-07-15 13:58:45 +01:00
Shati Patel
ffe1704ac0 Replace code paths dropdown with VS Code UI Toolkit (#1429) 2022-07-15 13:04:36 +01:00
Edoardo Pirovano
bd2dd04ac6 Regularly scrub query history view 2022-07-14 16:59:08 +01:00
Edoardo Pirovano
bbf4a03b03 Fix typo in config parameter name 2022-07-13 16:34:18 +01:00
Shati Patel
f38eb4895d Replace "repository search" filter box with VS Code UI Toolkit (#1424) 2022-07-13 15:13:31 +01:00
Andrew Eisenberg
f559b59ee5 Merge pull request #1420 from github/robertbrignull/api-retry
Add API retries for octokit requests
2022-07-12 08:12:21 -07:00
Angela P Wen
c9d895ea42 Parse summary of evaluator logs into data model (#1405)
Co-authored-by: Aditya Sharad <6874315+adityasharad@users.noreply.github.com>
Co-authored-by: Andrew Eisenberg <aeisenberg@github.com>
2022-07-12 14:04:55 +02:00
Shati Patel
e57bbcb711 Use VSCodeTags instead of Primer Labels in webview (#1421) 2022-07-01 16:21:44 +01:00
Shati Patel
b311991644 MRVA: Fix grammar in pop-up message (#1416) 2022-07-01 12:43:46 +01:00
Robert
825054a271 Use octokit retry module 2022-07-01 11:19:49 +00:00
Robert
f7aa0a5ae5 Install @octokot/plugin-retry 2022-07-01 11:06:22 +00:00
Andrew Eisenberg
f486ccfac6 Merge pull request #1418 from github/aeisenberg/resolve-ml-libs
Resolve ml-queries from directory
2022-06-30 08:56:15 -07:00
Andrew Eisenberg
70f74d3baf Resolve ml-queries from directory
Previously, there was a bug where quick eval queries would crash when
the eval snippet is in a library file.

The problem was that the `codeql resolve queries` command fails when
passed a library file. The fix is to avoid passing the library file at
all. Instead, pass the directory. This is safe because the resolve
queries command only needs to know which query pack the file is
contained in. Passing in the parent directory is the same as passing in
a file in this particular case.
2022-06-30 08:36:55 -07:00
Charis Kyriakou
ebad1844df MRVA: Don't show notification if user aborts firing off a query (#1417) 2022-06-30 14:35:33 +01:00
Charis Kyriakou
a40a2edaf2 Merge pull request #1414 from github/version/bump-to-v1.6.9
Bump version to v1.6.9
2022-06-29 13:17:30 +01:00
charisk
5f3d525ff8 Bump version to v1.6.9 2022-06-29 11:56:36 +00:00
39 changed files with 1192 additions and 148 deletions

View File

@@ -1,5 +1,9 @@
# CodeQL for Visual Studio Code: Changelog
## 1.6.9 - 20 July 2022
No user facing changes.
## 1.6.8 - 29 June 2022
- Fix a bug where quick queries cannot be compiled if the core libraries are not in the workspace. [#1411](https://github.com/github/vscode-codeql/pull/1411)

View File

@@ -1,17 +1,19 @@
{
"name": "vscode-codeql",
"version": "1.6.8",
"version": "1.6.9",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "vscode-codeql",
"version": "1.6.8",
"version": "1.6.9",
"license": "MIT",
"dependencies": {
"@octokit/plugin-retry": "^3.0.9",
"@octokit/rest": "^18.5.6",
"@primer/octicons-react": "^16.3.0",
"@primer/react": "^35.0.0",
"@vscode/codicons": "^0.0.31",
"@vscode/webview-ui-toolkit": "^1.0.0",
"child-process-promise": "^2.2.1",
"classnames": "~2.2.6",
@@ -697,6 +699,15 @@
"@octokit/core": ">=3"
}
},
"node_modules/@octokit/plugin-retry": {
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-3.0.9.tgz",
"integrity": "sha512-r+fArdP5+TG6l1Rv/C9hVoty6tldw6cE2pRHNGmFPdyfrc696R6JjrQ3d7HdVqGwuzfyrcaLAKD7K8TX8aehUQ==",
"dependencies": {
"@octokit/types": "^6.0.3",
"bottleneck": "^2.15.3"
}
},
"node_modules/@octokit/request": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.6.0.tgz",
@@ -2151,6 +2162,11 @@
"integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==",
"dev": true
},
"node_modules/@vscode/codicons": {
"version": "0.0.31",
"resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.31.tgz",
"integrity": "sha512-fldpXy7pHsQAMlU1pnGI23ypQ6xLk5u6SiABMFoAmlj4f2MR0iwg7C19IB1xvAEGG+dkxOfRSrbKF8ry7QqGQA=="
},
"node_modules/@vscode/webview-ui-toolkit": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@vscode/webview-ui-toolkit/-/webview-ui-toolkit-1.0.0.tgz",
@@ -3174,6 +3190,11 @@
"integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=",
"dev": true
},
"node_modules/bottleneck": {
"version": "2.19.5",
"resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz",
"integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw=="
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -14724,6 +14745,15 @@
"deprecation": "^2.3.1"
}
},
"@octokit/plugin-retry": {
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-3.0.9.tgz",
"integrity": "sha512-r+fArdP5+TG6l1Rv/C9hVoty6tldw6cE2pRHNGmFPdyfrc696R6JjrQ3d7HdVqGwuzfyrcaLAKD7K8TX8aehUQ==",
"requires": {
"@octokit/types": "^6.0.3",
"bottleneck": "^2.15.3"
}
},
"@octokit/request": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.6.0.tgz",
@@ -16016,6 +16046,11 @@
"integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==",
"dev": true
},
"@vscode/codicons": {
"version": "0.0.31",
"resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.31.tgz",
"integrity": "sha512-fldpXy7pHsQAMlU1pnGI23ypQ6xLk5u6SiABMFoAmlj4f2MR0iwg7C19IB1xvAEGG+dkxOfRSrbKF8ry7QqGQA=="
},
"@vscode/webview-ui-toolkit": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@vscode/webview-ui-toolkit/-/webview-ui-toolkit-1.0.0.tgz",
@@ -16860,6 +16895,11 @@
"integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=",
"dev": true
},
"bottleneck": {
"version": "2.19.5",
"resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz",
"integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw=="
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",

View File

@@ -4,7 +4,7 @@
"description": "CodeQL for Visual Studio Code",
"author": "GitHub",
"private": true,
"version": "1.6.8",
"version": "1.6.9",
"publisher": "GitHub",
"license": "MIT",
"icon": "media/VS-marketplace-CodeQL-icon.png",
@@ -224,7 +224,7 @@
},
"codeQL.queryHistory.format": {
"type": "string",
"default": "%q on %d - %s, %r [%t]",
"default": "%q on %d - %s %r [%t]",
"markdownDescription": "Default string for how to label query history items.\n* %t is the time of the query\n* %q is the human-readable query name\n* %f is the query file name\n* %d is the database name\n* %r is the number of results\n* %s is a status string"
},
"codeQL.queryHistory.ttl": {
@@ -1144,8 +1144,10 @@
},
"dependencies": {
"@octokit/rest": "^18.5.6",
"@octokit/plugin-retry": "^3.0.9",
"@primer/octicons-react": "^16.3.0",
"@primer/react": "^35.0.0",
"@vscode/codicons": "^0.0.31",
"@vscode/webview-ui-toolkit": "^1.0.0",
"child-process-promise": "^2.2.1",
"classnames": "~2.2.6",
@@ -1161,8 +1163,8 @@
"react": "^17.0.2",
"react-dom": "^17.0.2",
"semver": "~7.3.2",
"source-map-support": "^0.5.21",
"source-map": "^0.7.4",
"source-map-support": "^0.5.21",
"stream": "^0.0.2",
"stream-chain": "~2.2.4",
"stream-json": "~1.7.3",

View File

@@ -1,5 +1,6 @@
import * as vscode from 'vscode';
import * as Octokit from '@octokit/rest';
import { retry } from '@octokit/plugin-retry';
const GITHUB_AUTH_PROVIDER_ID = 'github';
@@ -51,14 +52,15 @@ export class Credentials {
private async createOctokit(createIfNone: boolean, overrideToken?: string): Promise<Octokit.Octokit | undefined> {
if (overrideToken) {
return new Octokit.Octokit({ auth: overrideToken });
return new Octokit.Octokit({ auth: overrideToken, retry });
}
const session = await vscode.authentication.getSession(GITHUB_AUTH_PROVIDER_ID, SCOPES, { createIfNone });
if (session) {
return new Octokit.Octokit({
auth: session.accessToken
auth: session.accessToken,
retry
});
} else {
return undefined;

View File

@@ -606,7 +606,8 @@ export class CodeQLCliServer implements Disposable {
/** Resolves the ML models that should be available when evaluating a query. */
async resolveMlModels(additionalPacks: string[], queryPath: string): Promise<MlModelsInfo> {
const args = await this.cliConstraints.supportsPreciseResolveMlModels()
? [...this.getAdditionalPacksArg(additionalPacks), queryPath]
// use the dirname of the path so that we can handle query libraries
? [...this.getAdditionalPacksArg(additionalPacks), path.dirname(queryPath)]
: this.getAdditionalPacksArg(additionalPacks);
return await this.runJsonCodeQlCliCommand<MlModelsInfo>(
['resolve', 'ml-models'],
@@ -688,6 +689,23 @@ export class CodeQLCliServer implements Disposable {
return await this.runCodeQlCliCommand(['generate', 'log-summary'], subcommandArgs, 'Generating log summary');
}
/**
* Generate a JSON summary of an evaluation log.
* @param inputPath The path of an evaluation event log.
* @param outputPath The path to write a JSON summary of it to.
*/
async generateJsonLogSummary(
inputPath: string,
outputPath: string,
): Promise<string> {
const subcommandArgs = [
'--format=predicates',
inputPath,
outputPath
];
return await this.runCodeQlCliCommand(['generate', 'log-summary'], subcommandArgs, 'Generating JSON log summary');
}
/**
* Gets the results from a bqrs.
* @param bqrsPath The path to the bqrs.

View File

@@ -59,7 +59,7 @@ const PERSONAL_ACCESS_TOKEN_SETTING = new Setting('personalAccessToken', DISTRIB
// Query History configuration
const QUERY_HISTORY_SETTING = new Setting('queryHistory', ROOT_SETTING);
const QUERY_HISTORY_FORMAT_SETTING = new Setting('format', QUERY_HISTORY_SETTING);
const QUERY_HISTORY_TTL = new Setting('format', QUERY_HISTORY_SETTING);
const QUERY_HISTORY_TTL = new Setting('ttl', QUERY_HISTORY_SETTING);
/** When these settings change, the distribution should be updated. */
const DISTRIBUTION_CHANGE_SETTINGS = [CUSTOM_CODEQL_PATH_SETTING, INCLUDE_PRERELEASE_SETTING, PERSONAL_ACCESS_TOKEN_SETTING];

View File

@@ -581,3 +581,11 @@ export async function* walkDirectory(dir: string): AsyncIterableIterator<string>
}
}
}
/**
* Pluralizes a word.
* Example: Returns "N repository" if N is one, "N repositories" otherwise.
*/
export function pluralize(numItems: number | undefined, singular: string, plural: string): string {
return numItems ? `${numItems} ${numItems === 1 ? singular : plural}` : '';
}

View File

@@ -3,6 +3,7 @@ import * as path from 'path';
import { QueryHistoryConfig } from './config';
import { LocalQueryInfo, QueryHistoryInfo } from './query-results';
import { RemoteQueryHistoryItem } from './remote-queries/remote-query-history-item';
import { pluralize } from './helpers';
interface InterpolateReplacements {
t: string; // Start time
@@ -57,23 +58,30 @@ export class HistoryItemLabelProvider {
t: item.startTime,
q: item.getQueryName(),
d: item.initialInfo.databaseInfo.name,
r: `${resultCount} results`,
r: `(${resultCount} results)`,
s: statusString,
f: item.getQueryFileName(),
'%': '%',
};
}
// Return the number of repositories queried if available. Otherwise, use the controller repository name.
private buildRepoLabel(item: RemoteQueryHistoryItem): string {
const repositoryCount = item.remoteQuery.repositoryCount;
if (repositoryCount) {
return pluralize(repositoryCount, 'repository', 'repositories');
}
return `${item.remoteQuery.controllerRepository.owner}/${item.remoteQuery.controllerRepository.name}`;
}
private getRemoteInterpolateReplacements(item: RemoteQueryHistoryItem): InterpolateReplacements {
return {
t: new Date(item.remoteQuery.executionStartTime).toLocaleString(env.language),
q: item.remoteQuery.queryName,
// There is no database name for remote queries. Instead use the controller repository name.
d: `${item.remoteQuery.controllerRepository.owner}/${item.remoteQuery.controllerRepository.name}`,
// There is no synchronous way to get the results count.
r: '',
q: `${item.remoteQuery.queryName} (${item.remoteQuery.language})`,
d: this.buildRepoLabel(item),
r: `(${pluralize(item.resultCount, 'result', 'results')})`,
s: item.status,
f: path.basename(item.remoteQuery.queryFilePath),
'%': '%'

View File

@@ -137,6 +137,8 @@ export function getHtmlForWebview(
? `${webview.cspSource} vscode-file: 'unsafe-inline'`
: `'nonce-${nonce}'`;
const fontSrc = webview.cspSource;
/*
* Content security policy:
* default-src: allow nothing by default.
@@ -149,7 +151,7 @@ export function getHtmlForWebview(
<html>
<head>
<meta http-equiv="Content-Security-Policy"
content="default-src 'none'; script-src 'nonce-${nonce}'; style-src ${styleSrc}; connect-src ${webview.cspSource};">
content="default-src 'none'; script-src 'nonce-${nonce}'; font-src ${fontSrc}; style-src ${styleSrc}; connect-src ${webview.cspSource};">
${stylesheetsHtmlLines.join(` ${os.EOL}`)}
</head>
<body>

View File

@@ -0,0 +1,44 @@
import * as os from 'os';
// TODO(angelapwen): Only load in necessary information and
// location in bytes for this log to save memory.
export interface EvaluatorLogData {
queryCausingWork: string;
predicateName: string;
millis: number;
resultSize: number;
ra: Pipelines;
}
interface Pipelines {
// Key: pipeline identifier; Value: array of pipeline steps
pipelineNamesToSteps: Map<string, string[]>;
}
/**
* A pure method that parses a string of evaluator log summaries into
* an array of EvaluatorLogData objects.
*
*/
export function parseVisualizerData(logSummary: string): EvaluatorLogData[] {
// Remove newline delimiters because summary is in .jsonl format.
const jsonSummaryObjects: string[] = logSummary.split(os.EOL + os.EOL);
const visualizerData: EvaluatorLogData[] = [];
for (const obj of jsonSummaryObjects) {
const jsonObj = JSON.parse(obj);
// Only convert log items that have an RA and millis field
if (jsonObj.ra !== undefined && jsonObj.millis !== undefined) {
const newLogData: EvaluatorLogData = {
queryCausingWork: jsonObj.queryCausingWork,
predicateName: jsonObj.predicateName,
millis: jsonObj.millis,
resultSize: jsonObj.resultSize,
ra: jsonObj.ra
};
visualizerData.push(newLogData);
}
}
return visualizerData;
}

View File

@@ -3,6 +3,7 @@ import * as os from 'os';
import * as path from 'path';
import { Disposable, ExtensionContext } from 'vscode';
import { logger } from './logging';
import { QueryHistoryManager } from './query-history';
const LAST_SCRUB_TIME_KEY = 'lastScrubTime';
@@ -30,12 +31,13 @@ export function registerQueryHistoryScubber(
throttleTime: number,
maxQueryTime: number,
queryDirectory: string,
qhm: QueryHistoryManager,
ctx: ExtensionContext,
// optional counter to keep track of how many times the scrubber has run
counter?: Counter
): Disposable {
const deregister = setInterval(scrubQueries, wakeInterval, throttleTime, maxQueryTime, queryDirectory, ctx, counter);
const deregister = setInterval(scrubQueries, wakeInterval, throttleTime, maxQueryTime, queryDirectory, qhm, ctx, counter);
return {
dispose: () => {
@@ -48,6 +50,7 @@ async function scrubQueries(
throttleTime: number,
maxQueryTime: number,
queryDirectory: string,
qhm: QueryHistoryManager,
ctx: ExtensionContext,
counter?: Counter
) {
@@ -89,6 +92,7 @@ async function scrubQueries(
} finally {
void logger.log(`Scrubbed ${scrubCount} old queries.`);
}
await qhm.removeDeletedQueries();
}
}

View File

@@ -205,13 +205,12 @@ export class HistoryTreeDataProvider extends DisposableObject {
? h2.initialInfo.start.getTime()
: h2.remoteQuery?.executionStartTime;
// result count for remote queries is not available here.
const resultCount1 = h1.t === 'local'
? h1.completedQuery?.resultCount ?? -1
: -1;
: h1.resultCount ?? -1;
const resultCount2 = h2.t === 'local'
? h2.completedQuery?.resultCount ?? -1
: -1;
: h2.resultCount ?? -1;
switch (this.sortOrder) {
case SortOrder.NameAsc:
@@ -505,7 +504,7 @@ export class QueryHistoryManager extends DisposableObject {
this.push(
queryHistoryConfigListener.onDidChangeConfiguration(() => {
this.treeDataProvider.refresh();
this.registerQueryHistoryScrubber(queryHistoryConfigListener, ctx);
this.registerQueryHistoryScrubber(queryHistoryConfigListener, this, ctx);
})
);
@@ -524,7 +523,7 @@ export class QueryHistoryManager extends DisposableObject {
},
}));
this.registerQueryHistoryScrubber(queryHistoryConfigListener, ctx);
this.registerQueryHistoryScrubber(queryHistoryConfigListener, this, ctx);
this.registerToRemoteQueriesEvents();
}
@@ -535,7 +534,7 @@ export class QueryHistoryManager extends DisposableObject {
/**
* Register and create the history scrubber.
*/
private registerQueryHistoryScrubber(queryHistoryConfigListener: QueryHistoryConfig, ctx: ExtensionContext) {
private registerQueryHistoryScrubber(queryHistoryConfigListener: QueryHistoryConfig, qhm: QueryHistoryManager, ctx: ExtensionContext) {
this.queryHistoryScrubber?.dispose();
// Every hour check if we need to re-run the query history scrubber.
this.queryHistoryScrubber = this.push(
@@ -544,6 +543,7 @@ export class QueryHistoryManager extends DisposableObject {
TWO_HOURS_IN_MS,
queryHistoryConfigListener.ttlInMillis,
this.queryStorageDir,
qhm,
ctx
)
);
@@ -573,6 +573,10 @@ export class QueryHistoryManager extends DisposableObject {
const remoteQueryHistoryItem = item as RemoteQueryHistoryItem;
remoteQueryHistoryItem.status = event.status;
remoteQueryHistoryItem.failureReason = event.failureReason;
remoteQueryHistoryItem.resultCount = event.resultCount;
if (event.status === QueryStatus.Completed) {
remoteQueryHistoryItem.completed = true;
}
await this.refreshTreeView();
} else {
void logger.log('Variant analysis status update event received for unknown variant analysis');
@@ -639,6 +643,15 @@ export class QueryHistoryManager extends DisposableObject {
return this.treeDataProvider.getCurrent();
}
async removeDeletedQueries() {
await Promise.all(this.treeDataProvider.allHistory.map(async (item) => {
if (item.t == 'local' && item.completedQuery && !(await fs.pathExists(item.completedQuery?.query.querySaveDir))) {
this.treeDataProvider.remove(item);
item.completedQuery?.dispose();
}
}));
}
async handleRemoveHistoryItem(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[] = []

View File

@@ -4,6 +4,7 @@ import * as path from 'path';
import { showAndLogErrorMessage } from './helpers';
import { asyncFilter, getErrorMessage, getErrorStack } from './pure/helpers-pure';
import { CompletedQueryInfo, LocalQueryInfo, QueryHistoryInfo } from './query-results';
import { QueryStatus } from './query-status';
import { QueryEvaluationInfo } from './run-queries';
export async function slurpQueryHistory(fsPath: string): Promise<QueryHistoryInfo[]> {
@@ -39,7 +40,12 @@ export async function slurpQueryHistory(fsPath: string): Promise<QueryHistoryInf
q.completedQuery.dispose = () => { /**/ };
}
} else if (q.t === 'remote') {
// noop
// A bug was introduced that didn't set the completed flag in query history
// items. The following code makes sure that the flag is set in order to
// "patch" older query history items.
if (q.status === QueryStatus.Completed) {
q.completed = true;
}
}
return q;
});

View File

@@ -267,6 +267,10 @@ export function findQueryEvalLogSummaryFile(resultPath: string): string {
return path.join(resultPath, 'evaluator-log.summary');
}
export function findJsonQueryEvalLogSummaryFile(resultPath: string): string {
return path.join(resultPath, 'evaluator-log.summary.jsonl');
}
export function findQueryEvalLogEndSummaryFile(resultPath: string): string {
return path.join(resultPath, 'evaluator-log-end.summary');
}

View File

@@ -4,14 +4,17 @@ import * as fs from 'fs-extra';
import { window, commands, Uri, ExtensionContext, QuickPickItem, workspace, ViewColumn } from 'vscode';
import { Credentials } from '../authentication';
import { UserCancellationException } from '../commandRunner';
import { showInformationMessageWithAction } from '../helpers';
import {
showInformationMessageWithAction,
pluralize
} from '../helpers';
import { logger } from '../logging';
import { QueryHistoryManager } from '../query-history';
import { createGist } from './gh-actions-api-client';
import { RemoteQueriesManager } from './remote-queries-manager';
import { generateMarkdown } from './remote-queries-markdown-generation';
import { RemoteQuery } from './remote-query';
import { AnalysisResults } from './shared/analysis-result';
import { AnalysisResults, sumAnalysesResults } from './shared/analysis-result';
/**
* Exports the results of the currently-selected remote query.
@@ -74,13 +77,13 @@ async function determineExportFormat(
/**
* Converts the results of a remote query to markdown and uploads the files as a secret gist.
*/
async function exportResultsToGist(
export async function exportResultsToGist(
ctx: ExtensionContext,
query: RemoteQuery,
analysesResults: AnalysisResults[]
): Promise<void> {
const credentials = await Credentials.initialize(ctx);
const description = 'CodeQL Variant Analysis Results';
const description = buildGistDescription(query, analysesResults);
const markdownFiles = generateMarkdown(query, analysesResults, 'gist');
// Convert markdownFiles to the appropriate format for uploading to gist
const gistFiles = markdownFiles.reduce((acc, cur) => {
@@ -100,6 +103,17 @@ async function exportResultsToGist(
}
}
/**
* Builds Gist description
* Ex: Empty Block (Go) x results (y repositories)
*/
const buildGistDescription = (query: RemoteQuery, analysesResults: AnalysisResults[]) => {
const resultCount = sumAnalysesResults(analysesResults);
const resultLabel = pluralize(resultCount, 'result', 'results');
const repositoryLabel = query.repositoryCount ? `(${pluralize(query.repositoryCount, 'repository', 'repositories')})` : '';
return `${query.queryName} (${query.language}) ${resultLabel} ${repositoryLabel}`;
};
/**
* Converts the results of a remote query to markdown and saves the files locally
* in the query directory (where query results and metadata are also saved).

View File

@@ -18,10 +18,16 @@ import {
import { Logger } from '../logging';
import { getHtmlForWebview } from '../interface-utils';
import { assertNever } from '../pure/helpers-pure';
import { AnalysisSummary, RemoteQueryResult } from './remote-query-result';
import {
AnalysisSummary,
RemoteQueryResult,
sumAnalysisSummariesResults
} from './remote-query-result';
import { RemoteQuery } from './remote-query';
import { RemoteQueryResult as RemoteQueryResultViewModel } from './shared/remote-query-result';
import { AnalysisSummary as AnalysisResultViewModel } from './shared/remote-query-result';
import {
AnalysisSummary as AnalysisResultViewModel,
RemoteQueryResult as RemoteQueryResultViewModel
} from './shared/remote-query-result';
import { showAndLogWarningMessage } from '../helpers';
import { URLSearchParams } from 'url';
import { SHOW_QUERY_TEXT_MSG } from '../query-history';
@@ -73,7 +79,7 @@ export class RemoteQueriesInterfaceManager {
*/
private buildViewModel(query: RemoteQuery, queryResult: RemoteQueryResult): RemoteQueryResultViewModel {
const queryFileName = path.basename(query.queryFilePath);
const totalResultCount = queryResult.analysisSummaries.reduce((acc, cur) => acc + cur.resultCount, 0);
const totalResultCount = sumAnalysisSummariesResults(queryResult.analysisSummaries);
const executionDuration = this.getDuration(queryResult.executionEndTime, query.executionStartTime);
const analysisSummaries = this.buildAnalysisSummaries(queryResult.analysisSummaries);
const totalRepositoryCount = queryResult.analysisSummaries.length;
@@ -111,6 +117,7 @@ export class RemoteQueriesInterfaceManager {
localResourceRoots: [
Uri.file(this.analysesResultsManager.storagePath),
Uri.file(path.join(this.ctx.extensionPath, 'out')),
Uri.file(path.join(this.ctx.extensionPath, 'node_modules/@vscode/codicons/dist')),
],
}
));
@@ -135,10 +142,16 @@ export class RemoteQueriesInterfaceManager {
ctx.asAbsolutePath('out/remote-queries/view/remoteQueries.css')
);
// Allows use of the VS Code "codicons" icon set.
// See https://github.com/microsoft/vscode-codicons
const codiconsPathOnDisk = Uri.file(
ctx.asAbsolutePath('node_modules/@vscode/codicons/dist/codicon.css')
);
panel.webview.html = getHtmlForWebview(
panel.webview,
scriptPathOnDisk,
[baseStylesheetUriOnDisk, stylesheetPathOnDisk],
[baseStylesheetUriOnDisk, stylesheetPathOnDisk, codiconsPathOnDisk],
true
);
ctx.subscriptions.push(

View File

@@ -15,7 +15,7 @@ import { RemoteQuery } from './remote-query';
import { RemoteQueriesMonitor } from './remote-queries-monitor';
import { getRemoteQueryIndex, getRepositoriesMetadata, RepositoriesMetadata } from './gh-actions-api-client';
import { RemoteQueryResultIndex } from './remote-query-result-index';
import { RemoteQueryResult } from './remote-query-result';
import { RemoteQueryResult, sumAnalysisSummariesResults } from './remote-query-result';
import { DownloadLink } from './download-link';
import { AnalysesResultsManager } from './analyses-results-manager';
import { assertNever } from '../pure/helpers-pure';
@@ -41,6 +41,8 @@ export interface UpdatedQueryStatusEvent {
queryId: string;
status: QueryStatus;
failureReason?: string;
repositoryCount?: number;
resultCount?: number;
}
export class RemoteQueriesManager extends DisposableObject {
@@ -248,7 +250,7 @@ export class RemoteQueriesManager extends DisposableObject {
}
private async askToOpenResults(query: RemoteQuery, queryResult: RemoteQueryResult): Promise<void> {
const totalResultCount = queryResult.analysisSummaries.reduce((acc, cur) => acc + cur.resultCount, 0);
const totalResultCount = sumAnalysisSummariesResults(queryResult.analysisSummaries);
const totalRepoCount = queryResult.analysisSummaries.length;
const message = `Query "${query.queryName}" run on ${totalRepoCount} repositories and returned ${totalResultCount} results`;
@@ -314,9 +316,15 @@ export class RemoteQueriesManager extends DisposableObject {
): Promise<void> {
const resultIndex = await getRemoteQueryIndex(credentials, remoteQuery);
if (resultIndex) {
this.remoteQueryStatusUpdateEventEmitter.fire({ queryId, status: QueryStatus.Completed });
const metadata = await this.getRepositoriesMetadata(resultIndex, credentials);
const queryResult = this.mapQueryResult(executionEndTime, resultIndex, queryId, metadata);
const resultCount = sumAnalysisSummariesResults(queryResult.analysisSummaries);
this.remoteQueryStatusUpdateEventEmitter.fire({
queryId,
status: QueryStatus.Completed,
repositoryCount: queryResult.analysisSummaries.length,
resultCount
});
await this.storeJsonFile(queryId, 'query-result.json', queryResult);

View File

@@ -7,6 +7,7 @@ import { RemoteQuery } from './remote-query';
export interface RemoteQueryHistoryItem {
readonly t: 'remote';
failureReason?: string;
resultCount?: number;
status: QueryStatus;
completed: boolean;
readonly queryId: string,

View File

@@ -18,3 +18,10 @@ export interface AnalysisSummary {
starCount?: number,
lastUpdated?: number,
}
/**
* Sums up the number of results for all repos queried via a remote query.
*/
export const sumAnalysisSummariesResults = (analysisSummaries: AnalysisSummary[]): number => {
return analysisSummaries.reduce((acc, cur) => acc + cur.resultCount, 0);
};

View File

@@ -8,4 +8,5 @@ export interface RemoteQuery {
controllerRepository: Repository;
executionStartTime: number; // Use number here since it needs to be serialized and desserialized.
actionsWorkflowRunId: number;
repositoryCount: number;
}

View File

@@ -52,6 +52,10 @@ export async function getRepositorySelection(): Promise<RepositorySelection> {
return { repositoryLists: [quickpick.repositoryList] };
} else if (quickpick?.useCustomRepo) {
const customRepo = await getCustomRepo();
if (customRepo === undefined) {
// The user cancelled, do nothing.
throw new UserCancellationException('No repositories selected', true);
}
if (!customRepo || !REPO_REGEX.test(customRepo)) {
throw new UserCancellationException('Invalid repository format. Please enter a valid repository in the format <owner>/<repo> (e.g. github/codeql)');
}
@@ -59,6 +63,10 @@ export async function getRepositorySelection(): Promise<RepositorySelection> {
return { repositories: [customRepo] };
} else if (quickpick?.useAllReposOfOwner) {
const owner = await getOwner();
if (owner === undefined) {
// The user cancelled, do nothing.
throw new UserCancellationException('No repositories selected', true);
}
if (!owner || !OWNER_REGEX.test(owner)) {
throw new Error(`Invalid user or organization: ${owner}`);
}
@@ -197,6 +205,6 @@ async function getCustomRepo(): Promise<string | undefined> {
async function getOwner(): Promise<string | undefined> {
return await window.showInputBox({
title: 'Enter a GitHub user or organization',
ignoreFocusOut: true,
ignoreFocusOut: true
});
}

View File

@@ -11,6 +11,7 @@ import {
showAndLogErrorMessage,
showAndLogInformationMessage,
tryGetQueryMetadata,
pluralize,
tmpDir
} from '../helpers';
import { Credentials } from '../authentication';
@@ -142,7 +143,7 @@ async function findPackRoot(queryFile: string): Promise<string> {
while (!(await fs.pathExists(path.join(dir, 'qlpack.yml')))) {
dir = path.dirname(dir);
if (isFileSystemRoot(dir)) {
// there is no qlpack.yml in this direcory or any parent directory.
// there is no qlpack.yml in this directory or any parent directory.
// just use the query file's directory as the pack root.
return path.dirname(queryFile);
}
@@ -258,17 +259,19 @@ export async function runRemoteQuery(
});
const actionBranch = getActionBranch();
const workflowRunId = await runRemoteQueriesApiRequest(credentials, actionBranch, language, repoSelection, owner, repo, base64Pack, dryRun);
const apiResponse = await runRemoteQueriesApiRequest(credentials, actionBranch, language, repoSelection, owner, repo, base64Pack, dryRun);
const queryStartTime = Date.now();
const queryMetadata = await tryGetQueryMetadata(cliServer, queryFile);
if (dryRun) {
return { queryDirPath: remoteQueryDir.path };
} else {
if (!workflowRunId) {
if (!apiResponse) {
return;
}
const workflowRunId = apiResponse.workflow_run_id;
const repositoryCount = apiResponse.repositories_queried.length;
const remoteQuery = await buildRemoteQueryEntity(
queryFile,
queryMetadata,
@@ -276,7 +279,8 @@ export async function runRemoteQuery(
repo,
queryStartTime,
workflowRunId,
language);
language,
repositoryCount);
// don't return the path because it has been deleted
return { query: remoteQuery };
@@ -301,7 +305,7 @@ async function runRemoteQueriesApiRequest(
repo: string,
queryPackBase64: string,
dryRun = false
): Promise<void | number> {
): Promise<void | QueriesResponse> {
const data = {
ref,
language,
@@ -336,7 +340,7 @@ async function runRemoteQueriesApiRequest(
);
const { popupMessage, logMessage } = parseResponse(owner, repo, response.data);
void showAndLogInformationMessage(popupMessage, { fullMessage: logMessage });
return response.data.workflow_run_id;
return response.data;
} catch (error: any) {
if (error.status === 404) {
void showAndLogErrorMessage(`Controller repository was not found. Please make sure it's a valid repo name.${eol}`);
@@ -352,33 +356,34 @@ const eol2 = os.EOL + os.EOL;
// exported for testing only
export function parseResponse(owner: string, repo: string, response: QueriesResponse) {
const repositoriesQueried = response.repositories_queried;
const numRepositoriesQueried = repositoriesQueried.length;
const repositoryCount = repositoriesQueried.length;
const popupMessage = `Successfully scheduled runs on ${numRepositoriesQueried} repositories. [Click here to see the progress](https://github.com/${owner}/${repo}/actions/runs/${response.workflow_run_id}).`
const popupMessage = `Successfully scheduled runs on ${pluralize(repositoryCount, 'repository', 'repositories')}. [Click here to see the progress](https://github.com/${owner}/${repo}/actions/runs/${response.workflow_run_id}).`
+ (response.errors ? `${eol2}Some repositories could not be scheduled. See extension log for details.` : '');
let logMessage = `Successfully scheduled runs on ${numRepositoriesQueried} repositories. See https://github.com/${owner}/${repo}/actions/runs/${response.workflow_run_id}.`;
let logMessage = `Successfully scheduled runs on ${pluralize(repositoryCount, 'repository', 'repositories')}. See https://github.com/${owner}/${repo}/actions/runs/${response.workflow_run_id}.`;
logMessage += `${eol2}Repositories queried:${eol}${repositoriesQueried.join(', ')}`;
if (response.errors) {
const { invalid_repositories, repositories_without_database, private_repositories, cutoff_repositories, cutoff_repositories_count } = response.errors;
logMessage += `${eol2}Some repositories could not be scheduled.`;
if (invalid_repositories?.length) {
logMessage += `${eol2}${invalid_repositories.length} repositories were invalid and could not be found:${eol}${invalid_repositories.join(', ')}`;
logMessage += `${eol2}${pluralize(invalid_repositories.length, 'repository', 'repositories')} invalid and could not be found:${eol}${invalid_repositories.join(', ')}`;
}
if (repositories_without_database?.length) {
logMessage += `${eol2}${repositories_without_database.length} repositories did not have a CodeQL database available:${eol}${repositories_without_database.join(', ')}`;
logMessage += `${eol2}${pluralize(repositories_without_database.length, 'repository', 'repositories')} did not have a CodeQL database available:${eol}${repositories_without_database.join(', ')}`;
logMessage += `${eol}For each public repository that has not yet been added to the database service, we will try to create a database next time the store is updated.`;
}
if (private_repositories?.length) {
logMessage += `${eol2}${private_repositories.length} repositories are not public:${eol}${private_repositories.join(', ')}`;
logMessage += `${eol2}${pluralize(private_repositories.length, 'repository', 'repositories')} not public:${eol}${private_repositories.join(', ')}`;
logMessage += `${eol}When using a public controller repository, only public repositories can be queried.`;
}
if (cutoff_repositories_count) {
logMessage += `${eol2}${cutoff_repositories_count} repositories over the limit for a single request`;
logMessage += `${eol2}${pluralize(cutoff_repositories_count, 'repository', 'repositories')} over the limit for a single request`;
if (cutoff_repositories) {
logMessage += `:${eol}${cutoff_repositories.join(', ')}`;
if (cutoff_repositories_count !== cutoff_repositories.length) {
logMessage += `${eol}...${eol}And ${cutoff_repositories_count - cutoff_repositories.length} more repositrories.`;
const moreRepositories = cutoff_repositories_count - cutoff_repositories.length;
logMessage += `${eol}...${eol}And another ${pluralize(moreRepositories, 'repository', 'repositories')}.`;
}
} else {
logMessage += '.';
@@ -424,7 +429,8 @@ async function buildRemoteQueryEntity(
controllerRepoName: string,
queryStartTime: number,
workflowRunId: number,
language: string
language: string,
repositoryCount: number
): Promise<RemoteQuery> {
// The query name is either the name as specified in the query metadata, or the file name.
const queryName = queryMetadata?.name ?? path.basename(queryFilePath);
@@ -441,6 +447,7 @@ async function buildRemoteQueryEntity(
name: controllerRepoName,
},
executionStartTime: queryStartTime,
actionsWorkflowRunId: workflowRunId
actionsWorkflowRunId: workflowRunId,
repositoryCount,
};
}

View File

@@ -90,3 +90,9 @@ export const getAnalysisResultCount = (analysisResults: AnalysisResults): number
const rawResultCount = analysisResults.rawResults?.resultSet.rows.length || 0;
return analysisResults.interpretedResults.length + rawResultCount;
};
/**
* Returns the total number of results for an analysis by adding all individual repo results.
*/
export const sumAnalysesResults = (analysesResults: AnalysisResults[]) =>
analysesResults.reduce((acc, curr) => acc + getAnalysisResultCount(curr), 0);

View File

@@ -1,8 +1,8 @@
import { TriangleDownIcon, XCircleIcon } from '@primer/octicons-react';
import { ActionList, ActionMenu, Button, Label, Overlay } from '@primer/react';
import { VSCodeLink } from '@vscode/webview-ui-toolkit/react';
import { XCircleIcon } from '@primer/octicons-react';
import { Overlay } from '@primer/react';
import { VSCodeDropdown, VSCodeLink, VSCodeOption, VSCodeTag } from '@vscode/webview-ui-toolkit/react';
import * as React from 'react';
import { useRef, useState } from 'react';
import { ChangeEvent, useRef, useState } from 'react';
import styled from 'styled-components';
import { CodeFlow, AnalysisMessage, ResultSeverity } from '../shared/analysis-result';
import FileCodeSnippet from './FileCodeSnippet';
@@ -60,12 +60,12 @@ const CodePath = ({
</div>
{index === 0 &&
<div style={{ padding: 0, border: 'none' }}>
<Label>Source</Label>
<VSCodeTag>Source</VSCodeTag>
</div>
}
{index === codeFlow.threadFlows.length - 1 &&
<div style={{ padding: 0, border: 'none' }}>
<Label>Sink</Label>
<VSCodeTag>Sink</VSCodeTag>
</div>
}
</div>
@@ -94,25 +94,22 @@ const Menu = ({
codeFlows: CodeFlow[],
setSelectedCodeFlow: (value: React.SetStateAction<CodeFlow>) => void
}) => {
return <ActionMenu>
<ActionMenu.Anchor>
<Button variant="invisible" sx={{ fontWeight: 'normal', color: 'var(--vscode-editor-foreground);', padding: 0 }} >
{getCodeFlowName(codeFlows[0])}
<TriangleDownIcon size={16} />
</Button>
</ActionMenu.Anchor>
<ActionMenu.Overlay sx={{ backgroundColor: 'var(--vscode-editor-background)' }}>
<ActionList>
{codeFlows.map((codeFlow, index) =>
<ActionList.Item
key={`codeflow-${index}'`}
onSelect={(e: React.MouseEvent) => { setSelectedCodeFlow(codeFlow); }}>
{getCodeFlowName(codeFlow)}
</ActionList.Item>
)}
</ActionList>
</ActionMenu.Overlay>
</ActionMenu>;
return <VSCodeDropdown
onChange={(event: ChangeEvent<HTMLSelectElement>) => {
const selectedOption = event.target;
const selectedIndex = selectedOption.value as unknown as number;
setSelectedCodeFlow(codeFlows[selectedIndex]);
}}
>
{codeFlows.map((codeFlow, index) =>
<VSCodeOption
key={`codeflow-${index}'`}
value={index}
>
{getCodeFlowName(codeFlow)}
</VSCodeOption>
)}
</VSCodeDropdown>;
};
const CodePaths = ({

View File

@@ -1,7 +1,5 @@
import * as React from 'react';
import { ChangeEvent } from 'react';
import { TextInput } from '@primer/react';
import { SearchIcon } from '@primer/octicons-react';
import { VSCodeTextField } from '@vscode/webview-ui-toolkit/react';
interface RepositoriesSearchProps {
filterValue: string;
@@ -10,20 +8,16 @@ interface RepositoriesSearchProps {
const RepositoriesSearch = ({ filterValue, setFilterValue }: RepositoriesSearchProps) => {
return <>
<TextInput
block
sx={{
backgroundColor: 'var(--vscode-editor-background);',
color: 'var(--vscode-editor-foreground);',
width: 'calc(100% - 14px)',
}}
leadingVisual={SearchIcon}
aria-label="Repository search"
<VSCodeTextField
style={{ width: '100%' }}
placeholder='Filter by repository owner/name'
ariaLabel="Repository search"
name="repository-search"
placeholder="Filter by repository owner/name"
value={filterValue}
onChange={(e: ChangeEvent) => setFilterValue((e.target as HTMLInputElement).value)}
/>
onInput={(e: InputEvent) => setFilterValue((e.target as HTMLInputElement).value)}
>
<span slot="start" className="codicon codicon-search"></span>
</VSCodeTextField>
</>;
};

View File

@@ -37,6 +37,7 @@ import { ensureMetadataIsComplete } from './query-results';
import { SELECT_QUERY_NAME } from './contextual/locationFinder';
import { DecodedBqrsChunk } from './pure/bqrs-cli-types';
import { getErrorMessage } from './pure/helpers-pure';
import { parseVisualizerData } from './pure/log-summary-parser';
/**
* run-queries.ts
@@ -103,6 +104,10 @@ export class QueryEvaluationInfo {
return qsClient.findQueryEvalLogSummaryFile(this.querySaveDir);
}
get jsonEvalLogSummaryPath() {
return qsClient.findJsonQueryEvalLogSummaryFile(this.querySaveDir);
}
get evalLogEndSummaryPath() {
return qsClient.findQueryEvalLogEndSummaryFile(this.querySaveDir);
}
@@ -198,22 +203,10 @@ export class QueryEvaluationInfo {
logPath: this.evalLogPath,
});
if (await this.hasEvalLog()) {
queryInfo.evalLogLocation = this.evalLogPath;
void qs.cliServer.generateLogSummary(this.evalLogPath, this.evalLogSummaryPath, this.evalLogEndSummaryPath)
.then(() => {
queryInfo.evalLogSummaryLocation = this.evalLogSummaryPath;
fs.readFile(this.evalLogEndSummaryPath, (err, buffer) => {
if (err) {
throw new Error(`Could not read structured evaluator log end of summary file at ${this.evalLogEndSummaryPath}.`);
}
void qs.logger.log(' --- Evaluator Log Summary --- ', { additionalLogLocation: this.logPath });
void qs.logger.log(buffer.toString(), { additionalLogLocation: this.logPath });
});
})
.catch(err => {
void showAndLogWarningMessage(`Failed to generate structured evaluator log summary. Reason: ${err.message}`);
});
this.displayHumanReadableLogSummary(queryInfo, qs);
if (config.isCanary()) {
this.parseJsonLogSummary(qs.cliServer);
}
} else {
void showAndLogWarningMessage(`Failed to write structured evaluator log to ${this.evalLogPath}.`);
}
@@ -338,6 +331,48 @@ export class QueryEvaluationInfo {
return fs.pathExists(this.evalLogPath);
}
/**
* Calls the appropriate CLI command to generate a human-readable log summary
* and logs to the Query Server console and query log file.
*/
displayHumanReadableLogSummary(queryInfo: LocalQueryInfo, qs: qsClient.QueryServerClient): void {
queryInfo.evalLogLocation = this.evalLogPath;
void qs.cliServer.generateLogSummary(this.evalLogPath, this.evalLogSummaryPath, this.evalLogEndSummaryPath)
.then(() => {
queryInfo.evalLogSummaryLocation = this.evalLogSummaryPath;
fs.readFile(this.evalLogEndSummaryPath, (err, buffer) => {
if (err) {
throw new Error(`Could not read structured evaluator log end of summary file at ${this.evalLogEndSummaryPath}.`);
}
void qs.logger.log(' --- Evaluator Log Summary --- ', { additionalLogLocation: this.logPath });
void qs.logger.log(buffer.toString(), { additionalLogLocation: this.logPath });
});
})
.catch(err => {
void showAndLogWarningMessage(`Failed to generate human-readable structured evaluator log summary. Reason: ${err.message}`);
});
}
/**
* Calls the appropriate CLI command to generate a JSON log summary and parse it
* into the appropriate data model for the log visualizer.
*/
parseJsonLogSummary(cliServer: cli.CodeQLCliServer): void {
void cliServer.generateJsonLogSummary(this.evalLogPath, this.jsonEvalLogSummaryPath)
.then(() => {
// TODO(angelapwen): Stream the file in.
fs.readFile(this.jsonEvalLogSummaryPath, (err, buffer) => {
if (err) {
throw new Error(`Could not read structured evaluator log summary JSON file at ${this.jsonEvalLogSummaryPath}.`);
}
parseVisualizerData(buffer.toString()); // Eventually this return value will feed into the tree visualizer.
});
})
.catch(err => {
void showAndLogWarningMessage(`Failed to generate JSON structured evaluator log summary. Reason: ${err.message}`);
});
}
/**
* Creates the CSV file containing the results of this query. This will only be called if the query
* does not have interpreted results and the CSV file does not already exist.
@@ -789,9 +824,7 @@ export async function compileAndRunQueryAgainstDatabase(
const metadata = await tryGetQueryMetadata(cliServer, qlProgram.queryPath);
let availableMlModels: cli.MlModelInfo[] = [];
if (!initialInfo.queryPath.endsWith('.ql')) {
void logger.log('Quick evaluation within a query library does not currently support using ML models. Continuing without any ML models.');
} else if (!await cliServer.cliConstraints.supportsResolveMlModels()) {
if (!await cliServer.cliConstraints.supportsResolveMlModels()) {
void logger.log('Resolving ML models is unsupported by this version of the CLI. Running the query without any ML models.');
} else {
try {

View File

@@ -0,0 +1,462 @@
[
{
"nwo": "github/codeql",
"status": "Completed",
"interpretedResults": [
{
"message": {
"tokens": [
{
"t": "text",
"text": "This shell command depends on an uncontrolled "
},
{
"t": "location",
"text": "absolute path",
"location": {
"fileLink": {
"fileLinkPrefix": "https://github.com/github/codeql/blob/48015e5a2e6202131f2d1062cc066dc33ed69a9b",
"filePath": "javascript/ql/src/Security/CWE-078/examples/shell-command-injection-from-environment.js"
},
"highlightedRegion": {
"startLine": 4,
"startColumn": 35,
"endLine": 4,
"endColumn": 44
}
}
},
{ "t": "text", "text": "." }
]
},
"shortDescription": "This shell command depends on an uncontrolled ,absolute path,.",
"fileLink": {
"fileLinkPrefix": "https://github.com/github/codeql/blob/48015e5a2e6202131f2d1062cc066dc33ed69a9b",
"filePath": "javascript/ql/src/Security/CWE-078/examples/shell-command-injection-from-environment.js"
},
"severity": "Warning",
"codeSnippet": {
"startLine": 3,
"endLine": 6,
"text": "function cleanupTemp() {\n let cmd = \"rm -rf \" + path.join(__dirname, \"temp\");\n cp.execSync(cmd); // BAD\n}\n"
},
"highlightedRegion": {
"startLine": 5,
"startColumn": 15,
"endLine": 5,
"endColumn": 18
},
"codeFlows": [
{
"threadFlows": [
{
"fileLink": {
"fileLinkPrefix": "https://github.com/github/codeql/blob/48015e5a2e6202131f2d1062cc066dc33ed69a9b",
"filePath": "javascript/ql/src/Security/CWE-078/examples/shell-command-injection-from-environment.js"
},
"codeSnippet": {
"startLine": 2,
"endLine": 6,
"text": " path = require(\"path\");\nfunction cleanupTemp() {\n let cmd = \"rm -rf \" + path.join(__dirname, \"temp\");\n cp.execSync(cmd); // BAD\n}\n"
},
"highlightedRegion": {
"startLine": 4,
"startColumn": 35,
"endLine": 4,
"endColumn": 44
}
},
{
"fileLink": {
"fileLinkPrefix": "https://github.com/github/codeql/blob/48015e5a2e6202131f2d1062cc066dc33ed69a9b",
"filePath": "javascript/ql/src/Security/CWE-078/examples/shell-command-injection-from-environment.js"
},
"codeSnippet": {
"startLine": 2,
"endLine": 6,
"text": " path = require(\"path\");\nfunction cleanupTemp() {\n let cmd = \"rm -rf \" + path.join(__dirname, \"temp\");\n cp.execSync(cmd); // BAD\n}\n"
},
"highlightedRegion": {
"startLine": 4,
"startColumn": 25,
"endLine": 4,
"endColumn": 53
}
},
{
"fileLink": {
"fileLinkPrefix": "https://github.com/github/codeql/blob/48015e5a2e6202131f2d1062cc066dc33ed69a9b",
"filePath": "javascript/ql/src/Security/CWE-078/examples/shell-command-injection-from-environment.js"
},
"codeSnippet": {
"startLine": 2,
"endLine": 6,
"text": " path = require(\"path\");\nfunction cleanupTemp() {\n let cmd = \"rm -rf \" + path.join(__dirname, \"temp\");\n cp.execSync(cmd); // BAD\n}\n"
},
"highlightedRegion": {
"startLine": 4,
"startColumn": 13,
"endLine": 4,
"endColumn": 53
}
},
{
"fileLink": {
"fileLinkPrefix": "https://github.com/github/codeql/blob/48015e5a2e6202131f2d1062cc066dc33ed69a9b",
"filePath": "javascript/ql/src/Security/CWE-078/examples/shell-command-injection-from-environment.js"
},
"codeSnippet": {
"startLine": 2,
"endLine": 6,
"text": " path = require(\"path\");\nfunction cleanupTemp() {\n let cmd = \"rm -rf \" + path.join(__dirname, \"temp\");\n cp.execSync(cmd); // BAD\n}\n"
},
"highlightedRegion": {
"startLine": 4,
"startColumn": 7,
"endLine": 4,
"endColumn": 53
}
},
{
"fileLink": {
"fileLinkPrefix": "https://github.com/github/codeql/blob/48015e5a2e6202131f2d1062cc066dc33ed69a9b",
"filePath": "javascript/ql/src/Security/CWE-078/examples/shell-command-injection-from-environment.js"
},
"codeSnippet": {
"startLine": 3,
"endLine": 6,
"text": "function cleanupTemp() {\n let cmd = \"rm -rf \" + path.join(__dirname, \"temp\");\n cp.execSync(cmd); // BAD\n}\n"
},
"highlightedRegion": {
"startLine": 5,
"startColumn": 15,
"endLine": 5,
"endColumn": 18
}
}
]
}
]
},
{
"message": {
"tokens": [
{
"t": "text",
"text": "This shell command depends on an uncontrolled "
},
{
"t": "location",
"text": "absolute path",
"location": {
"fileLink": {
"fileLinkPrefix": "https://github.com/github/codeql/blob/48015e5a2e6202131f2d1062cc066dc33ed69a9b",
"filePath": "javascript/ql/test/query-tests/Security/CWE-078/tst_shell-command-injection-from-environment.js"
},
"highlightedRegion": {
"startLine": 6,
"startColumn": 36,
"endLine": 6,
"endColumn": 45
}
}
},
{ "t": "text", "text": "." }
]
},
"shortDescription": "This shell command depends on an uncontrolled ,absolute path,.",
"fileLink": {
"fileLinkPrefix": "https://github.com/github/codeql/blob/48015e5a2e6202131f2d1062cc066dc33ed69a9b",
"filePath": "javascript/ql/test/query-tests/Security/CWE-078/tst_shell-command-injection-from-environment.js"
},
"severity": "Warning",
"codeSnippet": {
"startLine": 4,
"endLine": 8,
"text": "(function() {\n\tcp.execFileSync('rm', ['-rf', path.join(__dirname, \"temp\")]); // GOOD\n\tcp.execSync('rm -rf ' + path.join(__dirname, \"temp\")); // BAD\n\n\texeca.shell('rm -rf ' + path.join(__dirname, \"temp\")); // NOT OK\n"
},
"highlightedRegion": {
"startLine": 6,
"startColumn": 14,
"endLine": 6,
"endColumn": 54
},
"codeFlows": [
{
"threadFlows": [
{
"fileLink": {
"fileLinkPrefix": "https://github.com/github/codeql/blob/48015e5a2e6202131f2d1062cc066dc33ed69a9b",
"filePath": "javascript/ql/test/query-tests/Security/CWE-078/tst_shell-command-injection-from-environment.js"
},
"codeSnippet": {
"startLine": 4,
"endLine": 8,
"text": "(function() {\n\tcp.execFileSync('rm', ['-rf', path.join(__dirname, \"temp\")]); // GOOD\n\tcp.execSync('rm -rf ' + path.join(__dirname, \"temp\")); // BAD\n\n\texeca.shell('rm -rf ' + path.join(__dirname, \"temp\")); // NOT OK\n"
},
"highlightedRegion": {
"startLine": 6,
"startColumn": 36,
"endLine": 6,
"endColumn": 45
}
},
{
"fileLink": {
"fileLinkPrefix": "https://github.com/github/codeql/blob/48015e5a2e6202131f2d1062cc066dc33ed69a9b",
"filePath": "javascript/ql/test/query-tests/Security/CWE-078/tst_shell-command-injection-from-environment.js"
},
"codeSnippet": {
"startLine": 4,
"endLine": 8,
"text": "(function() {\n\tcp.execFileSync('rm', ['-rf', path.join(__dirname, \"temp\")]); // GOOD\n\tcp.execSync('rm -rf ' + path.join(__dirname, \"temp\")); // BAD\n\n\texeca.shell('rm -rf ' + path.join(__dirname, \"temp\")); // NOT OK\n"
},
"highlightedRegion": {
"startLine": 6,
"startColumn": 26,
"endLine": 6,
"endColumn": 54
}
},
{
"fileLink": {
"fileLinkPrefix": "https://github.com/github/codeql/blob/48015e5a2e6202131f2d1062cc066dc33ed69a9b",
"filePath": "javascript/ql/test/query-tests/Security/CWE-078/tst_shell-command-injection-from-environment.js"
},
"codeSnippet": {
"startLine": 4,
"endLine": 8,
"text": "(function() {\n\tcp.execFileSync('rm', ['-rf', path.join(__dirname, \"temp\")]); // GOOD\n\tcp.execSync('rm -rf ' + path.join(__dirname, \"temp\")); // BAD\n\n\texeca.shell('rm -rf ' + path.join(__dirname, \"temp\")); // NOT OK\n"
},
"highlightedRegion": {
"startLine": 6,
"startColumn": 14,
"endLine": 6,
"endColumn": 54
}
}
]
}
]
}
]
},
{
"nwo": "test/no-results",
"status": "Completed",
"interpretedResults": []
},
{
"nwo": "meteor/meteor",
"status": "Completed",
"interpretedResults": [
{
"message": {
"tokens": [
{
"t": "text",
"text": "This shell command depends on an uncontrolled "
},
{
"t": "location",
"text": "absolute path",
"location": {
"fileLink": {
"fileLinkPrefix": "https://github.com/meteor/meteor/blob/73b538fe201cbfe89dd0c709689023f9b3eab1ec",
"filePath": "npm-packages/meteor-installer/config.js"
},
"highlightedRegion": {
"startLine": 39,
"startColumn": 20,
"endLine": 39,
"endColumn": 61
}
}
},
{ "t": "text", "text": "." }
]
},
"shortDescription": "This shell command depends on an uncontrolled ,absolute path,.",
"fileLink": {
"fileLinkPrefix": "https://github.com/meteor/meteor/blob/73b538fe201cbfe89dd0c709689023f9b3eab1ec",
"filePath": "npm-packages/meteor-installer/install.js"
},
"severity": "Warning",
"codeSnippet": {
"startLine": 257,
"endLine": 261,
"text": " if (isWindows()) {\n //set for the current session and beyond\n child_process.execSync(`setx path \"${meteorPath}/;%path%`);\n return;\n }\n"
},
"highlightedRegion": {
"startLine": 259,
"startColumn": 28,
"endLine": 259,
"endColumn": 62
},
"codeFlows": [
{
"threadFlows": [
{
"fileLink": {
"fileLinkPrefix": "https://github.com/meteor/meteor/blob/73b538fe201cbfe89dd0c709689023f9b3eab1ec",
"filePath": "npm-packages/meteor-installer/config.js"
},
"codeSnippet": {
"startLine": 37,
"endLine": 41,
"text": "\nconst meteorLocalFolder = '.meteor';\nconst meteorPath = path.resolve(rootPath, meteorLocalFolder);\n\nmodule.exports = {\n"
},
"highlightedRegion": {
"startLine": 39,
"startColumn": 20,
"endLine": 39,
"endColumn": 61
}
},
{
"fileLink": {
"fileLinkPrefix": "https://github.com/meteor/meteor/blob/73b538fe201cbfe89dd0c709689023f9b3eab1ec",
"filePath": "npm-packages/meteor-installer/config.js"
},
"codeSnippet": {
"startLine": 37,
"endLine": 41,
"text": "\nconst meteorLocalFolder = '.meteor';\nconst meteorPath = path.resolve(rootPath, meteorLocalFolder);\n\nmodule.exports = {\n"
},
"highlightedRegion": {
"startLine": 39,
"startColumn": 7,
"endLine": 39,
"endColumn": 61
}
},
{
"fileLink": {
"fileLinkPrefix": "https://github.com/meteor/meteor/blob/73b538fe201cbfe89dd0c709689023f9b3eab1ec",
"filePath": "npm-packages/meteor-installer/config.js"
},
"codeSnippet": {
"startLine": 42,
"endLine": 46,
"text": " METEOR_LATEST_VERSION,\n extractPath: rootPath,\n meteorPath,\n release: process.env.INSTALL_METEOR_VERSION || METEOR_LATEST_VERSION,\n rootPath,\n"
},
"highlightedRegion": {
"startLine": 44,
"startColumn": 3,
"endLine": 44,
"endColumn": 13
}
},
{
"fileLink": {
"fileLinkPrefix": "https://github.com/meteor/meteor/blob/73b538fe201cbfe89dd0c709689023f9b3eab1ec",
"filePath": "npm-packages/meteor-installer/install.js"
},
"codeSnippet": {
"startLine": 10,
"endLine": 14,
"text": "const os = require('os');\nconst {\n meteorPath,\n release,\n startedPath,\n"
},
"highlightedRegion": {
"startLine": 12,
"startColumn": 3,
"endLine": 12,
"endColumn": 13
}
},
{
"fileLink": {
"fileLinkPrefix": "https://github.com/meteor/meteor/blob/73b538fe201cbfe89dd0c709689023f9b3eab1ec",
"filePath": "npm-packages/meteor-installer/install.js"
},
"codeSnippet": {
"startLine": 9,
"endLine": 25,
"text": "const tmp = require('tmp');\nconst os = require('os');\nconst {\n meteorPath,\n release,\n startedPath,\n extractPath,\n isWindows,\n rootPath,\n sudoUser,\n isSudo,\n isMac,\n METEOR_LATEST_VERSION,\n shouldSetupExecPath,\n} = require('./config.js');\nconst { uninstall } = require('./uninstall');\nconst {\n"
},
"highlightedRegion": {
"startLine": 11,
"startColumn": 7,
"endLine": 23,
"endColumn": 27
}
},
{
"fileLink": {
"fileLinkPrefix": "https://github.com/meteor/meteor/blob/73b538fe201cbfe89dd0c709689023f9b3eab1ec",
"filePath": "npm-packages/meteor-installer/install.js"
},
"codeSnippet": {
"startLine": 257,
"endLine": 261,
"text": " if (isWindows()) {\n //set for the current session and beyond\n child_process.execSync(`setx path \"${meteorPath}/;%path%`);\n return;\n }\n"
},
"highlightedRegion": {
"startLine": 259,
"startColumn": 42,
"endLine": 259,
"endColumn": 52
}
},
{
"fileLink": {
"fileLinkPrefix": "https://github.com/meteor/meteor/blob/73b538fe201cbfe89dd0c709689023f9b3eab1ec",
"filePath": "npm-packages/meteor-installer/install.js"
},
"codeSnippet": {
"startLine": 257,
"endLine": 261,
"text": " if (isWindows()) {\n //set for the current session and beyond\n child_process.execSync(`setx path \"${meteorPath}/;%path%`);\n return;\n }\n"
},
"highlightedRegion": {
"startLine": 259,
"startColumn": 28,
"endLine": 259,
"endColumn": 62
}
}
]
},
{
"threadFlows": [
{
"fileLink": {
"fileLinkPrefix": "https://github.com/meteor/meteor/blob/73b538fe201cbfe89dd0c709689023f9b3eab1ec",
"filePath": "npm-packages/meteor-installer/config.js"
},
"codeSnippet": {
"startLine": 37,
"endLine": 41,
"text": "\nconst meteorLocalFolder = '.meteor';\nconst meteorPath = path.resolve(rootPath, meteorLocalFolder);\n\nmodule.exports = {\n"
},
"highlightedRegion": {
"startLine": 39,
"startColumn": 20,
"endLine": 39,
"endColumn": 61
}
},
{
"fileLink": {
"fileLinkPrefix": "https://github.com/meteor/meteor/blob/73b538fe201cbfe89dd0c709689023f9b3eab1ec",
"filePath": "npm-packages/meteor-installer/install.js"
},
"codeSnippet": {
"startLine": 257,
"endLine": 261,
"text": " if (isWindows()) {\n //set for the current session and beyond\n child_process.execSync(`setx path \"${meteorPath}/;%path%`);\n return;\n }\n"
},
"highlightedRegion": {
"startLine": 259,
"startColumn": 28,
"endLine": 259,
"endColumn": 62
}
}
]
}
]
}
]
}
]

View File

@@ -0,0 +1,10 @@
{
"queryName": "Shell command built from environment values",
"queryFilePath": "c:\\git-repo\\vscode-codeql-starter\\ql\\javascript\\ql\\src\\Security\\CWE-078\\ShellCommandInjectionFromEnvironment.ql",
"queryText": "/**\n * @name Shell command built from environment values\n * @description Building a shell command string with values from the enclosing\n * environment may cause subtle bugs or vulnerabilities.\n * @kind path-problem\n * @problem.severity warning\n * @security-severity 6.3\n * @precision high\n * @id js/shell-command-injection-from-environment\n * @tags correctness\n * security\n * external/cwe/cwe-078\n * external/cwe/cwe-088\n */\n\nimport javascript\nimport DataFlow::PathGraph\nimport semmle.javascript.security.dataflow.ShellCommandInjectionFromEnvironmentQuery\n\nfrom\n Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink, DataFlow::Node highlight,\n Source sourceNode\nwhere\n sourceNode = source.getNode() and\n cfg.hasFlowPath(source, sink) and\n if cfg.isSinkWithHighlight(sink.getNode(), _)\n then cfg.isSinkWithHighlight(sink.getNode(), highlight)\n else highlight = sink.getNode()\nselect highlight, source, sink, \"This shell command depends on an uncontrolled $@.\", sourceNode,\n sourceNode.getSourceType()\n",
"language": "javascript",
"controllerRepository": { "owner": "dsp-testing", "name": "qc-controller" },
"executionStartTime": 1649419081990,
"actionsWorkflowRunId": 2115000864,
"repositoryCount": 10
}

View File

@@ -27,10 +27,10 @@ describe('HistoryItemLabelProvider', () => {
expect(labelProvider.getLabel(fqi)).to.eq('xxx');
fqi.userSpecifiedLabel = '%t %q %d %s %f %r %%';
expect(labelProvider.getLabel(fqi)).to.eq(`${dateStr} query-name db-name in progress query-file.ql 456 results %`);
expect(labelProvider.getLabel(fqi)).to.eq(`${dateStr} query-name db-name in progress query-file.ql (456 results) %`);
fqi.userSpecifiedLabel = '%t %q %d %s %f %r %%::%t %q %d %s %f %r %%';
expect(labelProvider.getLabel(fqi)).to.eq(`${dateStr} query-name db-name in progress query-file.ql 456 results %::${dateStr} query-name db-name in progress query-file.ql 456 results %`);
expect(labelProvider.getLabel(fqi)).to.eq(`${dateStr} query-name db-name in progress query-file.ql (456 results) %::${dateStr} query-name db-name in progress query-file.ql (456 results) %`);
});
it('should interpolate query when not user specified', () => {
@@ -40,10 +40,10 @@ describe('HistoryItemLabelProvider', () => {
config.format = '%t %q %d %s %f %r %%';
expect(labelProvider.getLabel(fqi)).to.eq(`${dateStr} query-name db-name in progress query-file.ql 456 results %`);
expect(labelProvider.getLabel(fqi)).to.eq(`${dateStr} query-name db-name in progress query-file.ql (456 results) %`);
config.format = '%t %q %d %s %f %r %%::%t %q %d %s %f %r %%';
expect(labelProvider.getLabel(fqi)).to.eq(`${dateStr} query-name db-name in progress query-file.ql 456 results %::${dateStr} query-name db-name in progress query-file.ql 456 results %`);
expect(labelProvider.getLabel(fqi)).to.eq(`${dateStr} query-name db-name in progress query-file.ql (456 results) %::${dateStr} query-name db-name in progress query-file.ql (456 results) %`);
});
it('should get query short label', () => {
@@ -89,23 +89,30 @@ describe('HistoryItemLabelProvider', () => {
expect(labelProvider.getLabel(fqi)).to.eq('xxx');
fqi.userSpecifiedLabel = '%t %q %d %s %%';
expect(labelProvider.getLabel(fqi)).to.eq(`${dateStr} query-name github/vscode-codeql-integration-tests in progress %`);
expect(labelProvider.getLabel(fqi)).to.eq(`${dateStr} query-name (javascript) github/vscode-codeql-integration-tests in progress %`);
fqi.userSpecifiedLabel = '%t %q %d %s %%::%t %q %d %s %%';
expect(labelProvider.getLabel(fqi)).to.eq(`${dateStr} query-name github/vscode-codeql-integration-tests in progress %::${dateStr} query-name github/vscode-codeql-integration-tests in progress %`);
expect(labelProvider.getLabel(fqi)).to.eq(`${dateStr} query-name (javascript) github/vscode-codeql-integration-tests in progress %::${dateStr} query-name (javascript) github/vscode-codeql-integration-tests in progress %`);
});
it('should interpolate query when not user specified', () => {
const fqi = createMockRemoteQueryInfo();
expect(labelProvider.getLabel(fqi)).to.eq('xxx query-name xxx');
expect(labelProvider.getLabel(fqi)).to.eq('xxx query-name (javascript) xxx');
config.format = '%t %q %d %s %f %r %%';
expect(labelProvider.getLabel(fqi)).to.eq(`${dateStr} query-name github/vscode-codeql-integration-tests in progress query-file.ql %`);
expect(labelProvider.getLabel(fqi)).to.eq(`${dateStr} query-name (javascript) github/vscode-codeql-integration-tests in progress query-file.ql (16 results) %`);
config.format = '%t %q %d %s %f %r %%::%t %q %d %s %f %r %%';
expect(labelProvider.getLabel(fqi)).to.eq(`${dateStr} query-name github/vscode-codeql-integration-tests in progress query-file.ql %::${dateStr} query-name github/vscode-codeql-integration-tests in progress query-file.ql %`);
expect(labelProvider.getLabel(fqi)).to.eq(`${dateStr} query-name (javascript) github/vscode-codeql-integration-tests in progress query-file.ql (16 results) %::${dateStr} query-name (javascript) github/vscode-codeql-integration-tests in progress query-file.ql (16 results) %`);
});
it('should use number of repositories instead of controller repo if available', () => {
const fqi = createMockRemoteQueryInfo(undefined, 2);
config.format = '%t %q %d %s %f %r %%';
expect(labelProvider.getLabel(fqi)).to.eq(`${dateStr} query-name (javascript) 2 repositories in progress query-file.ql (16 results) %`);
});
it('should get query short label', () => {
@@ -119,7 +126,7 @@ describe('HistoryItemLabelProvider', () => {
expect(labelProvider.getShortLabel(fqi)).to.eq('query-name');
});
function createMockRemoteQueryInfo(userSpecifiedLabel?: string) {
function createMockRemoteQueryInfo(userSpecifiedLabel?: string, repositoryCount?: number) {
return {
t: 'remote',
userSpecifiedLabel,
@@ -130,9 +137,12 @@ describe('HistoryItemLabelProvider', () => {
controllerRepository: {
owner: 'github',
name: 'vscode-codeql-integration-tests'
}
},
language: 'javascript',
repositoryCount,
},
status: 'in progress',
resultCount: 16,
} as unknown as RemoteQueryHistoryItem;
}
});

View File

@@ -4,6 +4,7 @@ import * as sinonChai from 'sinon-chai';
import * as chai from 'chai';
import * as chaiAsPromised from 'chai-as-promised';
import 'chai/register-should';
import { ExtensionContext } from 'vscode';
import { runTestsInDirectory } from '../index-template';
@@ -13,3 +14,19 @@ chai.use(sinonChai);
export function run(): Promise<void> {
return runTestsInDirectory(__dirname);
}
export function createMockExtensionContext(): ExtensionContext {
return {
globalState: {
_state: {
'telemetry-request-viewed': true
} as Record<string, any>,
get(key: string) {
return this._state[key];
},
update(key: string, val: any) {
this._state[key] = val;
}
}
} as any;
}

View File

@@ -456,11 +456,11 @@ describe('query-history', () => {
describe('getChildren', () => {
const history = [
item('a', 2, 'remote'),
item('a', 2, 'remote', 24),
item('b', 10, 'local', 20),
item('c', 5, 'local', 30),
item('d', 1, 'local', 25),
item('e', 6, 'remote'),
item('e', 6, 'remote', 5),
];
let treeDataProvider: HistoryTreeDataProvider;
@@ -503,7 +503,7 @@ describe('query-history', () => {
});
it('should get children for result count ascending', async () => {
const expected = [history[0], history[4], history[1], history[3], history[2]];
const expected = [history[4], history[1], history[0], history[3], history[2]];
treeDataProvider.sortOrder = SortOrder.CountAsc;
const children = await treeDataProvider.getChildren();
@@ -511,7 +511,7 @@ describe('query-history', () => {
});
it('should get children for result count descending', async () => {
const expected = [history[0], history[4], history[1], history[3], history[2]].reverse();
const expected = [history[4], history[1], history[0], history[3], history[2]].reverse();
treeDataProvider.sortOrder = SortOrder.CountDesc;
const children = await treeDataProvider.getChildren();
@@ -573,6 +573,7 @@ describe('query-history', () => {
},
repositories: []
},
resultCount,
t
};
}
@@ -762,6 +763,9 @@ describe('query-history', () => {
TWO_HOURS_IN_MS,
LESS_THAN_ONE_DAY,
dir,
{
removeDeletedQueries: () => { return Promise.resolve(); }
} as QueryHistoryManager,
mockCtx,
{
increment: () => runCount++

View File

@@ -0,0 +1,56 @@
import { expect } from 'chai';
import * as path from 'path';
import * as fs from 'fs-extra';
import * as sinon from 'sinon';
import * as pq from 'proxyquire';
import { ExtensionContext } from 'vscode';
import { createMockExtensionContext } from '../index';
import { Credentials } from '../../../authentication';
import { MarkdownFile } from '../../../remote-queries/remote-queries-markdown-generation';
import * as actionsApiClient from '../../../remote-queries/gh-actions-api-client';
import { exportResultsToGist } from '../../../remote-queries/export-results';
const proxyquire = pq.noPreserveCache();
describe('export results', async function() {
describe('exportResultsToGist', async function() {
let sandbox: sinon.SinonSandbox;
let mockCredentials: Credentials;
let mockResponse: sinon.SinonStub<any, Promise<{ status: number }>>;
let mockCreateGist: sinon.SinonStub;
let ctx: ExtensionContext;
beforeEach(() => {
sandbox = sinon.createSandbox();
mockCredentials = {
getOctokit: () => Promise.resolve({
request: mockResponse
})
} as unknown as Credentials;
sandbox.stub(Credentials, 'initialize').resolves(mockCredentials);
const resultFiles = [] as MarkdownFile[];
proxyquire('../../../remote-queries/remote-queries-markdown-generation', {
'generateMarkdown': sinon.stub().returns(resultFiles)
});
});
afterEach(() => {
sandbox.restore();
});
it('should call the GitHub Actions API with the correct gist title', async function() {
mockCreateGist = sinon.stub(actionsApiClient, 'createGist');
ctx = createMockExtensionContext();
const query = JSON.parse(await fs.readFile(path.join(__dirname, '../data/remote-queries/query-with-results/query.json'), 'utf8'));
const analysesResults = JSON.parse(await fs.readFile(path.join(__dirname, '../data/remote-queries/query-with-results/analyses-results.json'), 'utf8'));
await exportResultsToGist(ctx, query, analysesResults);
expect(mockCreateGist.calledOnce).to.be.true;
expect(mockCreateGist.firstCall.args[1]).to.equal('Shell command built from environment values (javascript) 3 results (10 repositories)');
});
});
});

View File

@@ -100,7 +100,9 @@ describe('repository selection', async () => {
['top_100']
);
});
});
describe('custom owner', async () => {
// Test the owner regex in various "good" cases
const goodOwners = [
'owner',
@@ -146,6 +148,18 @@ describe('repository selection', async () => {
await expect(mod.getRepositorySelection()).to.be.rejectedWith(Error, `Invalid user or organization: ${owner}`);
});
});
it('should be ok for the user to change their mind', async () => {
quickPickSpy.resolves(
{ useAllReposOfOwner: true }
);
getRemoteRepositoryListsSpy.returns({});
// The user pressed escape to cancel the operation
showInputBoxSpy.resolves(undefined);
await expect(mod.getRepositorySelection()).to.be.rejectedWith(UserCancellationException, 'No repositories selected');
});
});
describe('custom repo', async () => {
@@ -196,6 +210,18 @@ describe('repository selection', async () => {
await expect(mod.getRepositorySelection()).to.be.rejectedWith(UserCancellationException, 'Invalid repository format');
});
});
it('should be ok for the user to change their mind', async () => {
quickPickSpy.resolves(
{ useCustomRepo: true }
);
getRemoteRepositoryListsSpy.returns({});
// The user pressed escape to cancel the operation
showInputBoxSpy.resolves(undefined);
await expect(mod.getRepositorySelection()).to.be.rejectedWith(UserCancellationException, 'No repositories selected');
});
});
describe('external repository lists file', async () => {

View File

@@ -41,7 +41,7 @@ describe('run-remote-query', () => {
'',
'Some repositories could not be scheduled.',
'',
'2 repositories were invalid and could not be found:',
'2 repositories invalid and could not be found:',
'e/f, g/h'].join(os.EOL)
);
});
@@ -96,7 +96,7 @@ describe('run-remote-query', () => {
'',
'Some repositories could not be scheduled.',
'',
'2 repositories are not public:',
'2 repositories not public:',
'e/f, g/h',
'When using a public controller repository, only public repositories can be queried.'].join(os.EOL)
);
@@ -181,7 +181,7 @@ describe('run-remote-query', () => {
'',
'Some repositories could not be scheduled.',
'',
'2 repositories were invalid and could not be found:',
'2 repositories invalid and could not be found:',
'e/f, g/h',
'',
'2 repositories did not have a CodeQL database available:',
@@ -189,5 +189,50 @@ describe('run-remote-query', () => {
'For each public repository that has not yet been added to the database service, we will try to create a database next time the store is updated.'].join(os.EOL)
);
});
it('should parse a response with one repo of each category, and not pluralize "repositories"', () => {
const result = parseResponse('org', 'name', {
workflow_run_id: 123,
repositories_queried: ['a/b'],
errors: {
private_repositories: ['e/f'],
cutoff_repositories: ['i/j'],
cutoff_repositories_count: 1,
invalid_repositories: ['m/n'],
repositories_without_database: ['q/r'],
}
});
expect(result.popupMessage).to.equal(
['Successfully scheduled runs on 1 repository. [Click here to see the progress](https://github.com/org/name/actions/runs/123).',
'',
'Some repositories could not be scheduled. See extension log for details.'].join(os.EOL)
);
expect(result.logMessage).to.equal(
[
'Successfully scheduled runs on 1 repository. See https://github.com/org/name/actions/runs/123.',
'',
'Repositories queried:',
'a/b',
'',
'Some repositories could not be scheduled.',
'',
'1 repository invalid and could not be found:',
'm/n',
'',
'1 repository did not have a CodeQL database available:',
'q/r',
'For each public repository that has not yet been added to the database service, we will try to create a database next time the store is updated.',
'',
'1 repository not public:',
'e/f',
'When using a public controller repository, only public repositories can be queried.',
'',
'1 repository over the limit for a single request:',
'i/j',
'Repositories were selected based on how recently they had been updated.',
].join(os.EOL)
);
});
});
});

View File

@@ -6,6 +6,7 @@ import { TelemetryListener, telemetryListener as globalTelemetryListener } from
import { UserCancellationException } from '../../commandRunner';
import { fail } from 'assert';
import { ENABLE_TELEMETRY } from '../../config';
import { createMockExtensionContext } from './index';
const sandbox = sinon.createSandbox();
@@ -339,22 +340,6 @@ describe('telemetry reporting', function() {
expect(window.showInformationMessage).to.have.been.calledOnce;
});
function createMockExtensionContext(): ExtensionContext {
return {
globalState: {
_state: {
'telemetry-request-viewed': true
} as Record<string, any>,
get(key: string) {
return this._state[key];
},
update(key: string, val: any) {
this._state[key] = val;
}
}
} as any;
}
async function enableTelemetry(section: string, value: boolean | undefined) {
await workspace.getConfiguration(section).update(
'enableTelemetry', value, ConfigurationTarget.Global

View File

@@ -0,0 +1,5 @@
{
"summaryLogVersion" : "0.3.0",
"codeqlVersion" : "2.9.0+202204201304plus",
"startTime" : "2022-06-23T14:02:41.607Z"
}

View File

@@ -0,0 +1,14 @@
{
"completionTime" : "2022-06-23T14:02:42.019Z",
"raHash" : "8caddafufc3it9svjph9m8d43me",
"predicateName" : "function_instantiation",
"appearsAs" : {
"function_instantiation" : {
"uboot-taint.ql" : [ 3, 9, 10, 11, 13, 14, 15, 17, 18, 20, 21, 22, 23, 24 ]
}
},
"queryCausingWork" : "uboot-taint.ql",
"evaluationStrategy" : "EXTENSIONAL",
"millis" : 0,
"resultSize" : 0
}

View File

@@ -0,0 +1,124 @@
{
"completionTime" : "2022-06-23T14:02:42.007Z",
"raHash" : "e77dfaa9ciimqv7gb3imoesdb11",
"predicateName" : "query::ClassPointerType::getBaseType#dispred#f0820431#ff",
"appearsAs" : {
"query::ClassPointerType::getBaseType#dispred#f0820431#ff" : {
"uboot-taint.ql" : [ 3 ]
}
},
"queryCausingWork" : "uboot-taint.ql",
"evaluationStrategy" : "COMPUTE_SIMPLE",
"millis" : 11,
"resultSize" : 1413,
"dependencies" : {
"derivedtypes_2013#join_rhs" : "0da8f1fqbiin9i0mcitjedvlbc7",
"query::Class#class#f0820431#f" : "2e5d24rnudi1l99mvtse9u567b7",
"derivedtypes" : "070e120iu2i3dhj6pt13oqnbi66"
},
"ra" : {
"pipeline" : [
" {1} r1 = CONSTANT(unique int)[1]",
" {3} r2 = JOIN r1 WITH derivedtypes_2013#join_rhs ON FIRST 1 OUTPUT Rhs.3, Rhs.1, Rhs.2",
" {4} r3 = JOIN r2 WITH query::Class#class#f0820431#f ON FIRST 1 OUTPUT Lhs.1, Lhs.2, 1, Lhs.0",
" {2} r4 = JOIN r3 WITH derivedtypes ON FIRST 4 OUTPUT Lhs.0, Lhs.3",
" return r4"
]
},
"pipelineRuns" : [ {
"raReference" : "pipeline"
} ]
}
{
"completionTime" : "2022-06-23T14:02:42.072Z",
"raHash" : "0a0e3cicgtsru1m0cun896qhi61",
"predicateName" : "query::DefinedMemberFunction#class#f0820431#f",
"appearsAs" : {
"query::DefinedMemberFunction#class#f0820431#f" : {
"uboot-taint.ql" : [ 3 ]
}
},
"queryCausingWork" : "uboot-taint.ql",
"evaluationStrategy" : "COMPUTE_SIMPLE",
"millis" : 20,
"resultSize" : 8740,
"dependencies" : {
"fun_decls" : "e6e2e6vn02fcrrtp1ks1f75cd01",
"fun_def" : "6bec314lcq8sm3skg9mpecscp02",
"function_instantiation" : "8caddafufc3it9svjph9m8d43me",
"fun_decls_10#join_rhs" : "16474d6lehg7ssk83gnv2q7r8t8"
},
"ra" : {
"pipeline" : [
" {2} r1 = SCAN fun_decls OUTPUT In.0, In.1",
" {2} r2 = STREAM DEDUP r1",
" {1} r3 = JOIN r2 WITH fun_def ON FIRST 1 OUTPUT Lhs.1",
"",
" {2} r4 = SCAN function_instantiation OUTPUT In.1, In.0",
" {2} r5 = JOIN r4 WITH fun_decls_10#join_rhs ON FIRST 1 OUTPUT Rhs.1, Lhs.1",
" {1} r6 = JOIN r5 WITH fun_def ON FIRST 1 OUTPUT Lhs.1",
"",
" {1} r7 = r3 UNION r6",
" return r7"
]
},
"pipelineRuns" : [ {
"raReference" : "pipeline"
} ]
}
{
"completionTime" : "2022-06-23T14:02:43.799Z",
"raHash" : "1d0c6wplpr6bnlnii51r7f6lh85",
"predicateName" : "QualifiedName::Namespace::getAQualifierForMembers#f0820431#ff",
"appearsAs" : {
"QualifiedName::Namespace::getAQualifierForMembers#f0820431#ff" : {
"uboot-taint.ql" : [ 10 ]
}
},
"queryCausingWork" : "uboot-taint.ql",
"evaluationStrategy" : "COMPUTE_RECURSIVE",
"millis" : 2,
"predicateIterationMillis" : [ 1, 0 ],
"deltaSizes" : [ 1, 0 ],
"resultSize" : 1,
"dependencies" : {
"namespaces" : "bf72dcmerq68kur5k2uttmjoco4",
"namespacembrs_1#antijoin_rhs" : "3c47bbkgae024k8hgf148mgeqi2",
"QualifiedName::Namespace::getAQualifierForMembers#f0820431#ff#join_rhs#1" : "d55d698lva8n15u4v7055ma1vl9",
"namespacembrs" : "08a148i70tonoa8fp0mb5eb44r1",
"QualifiedName::Namespace::getAQualifierForMembers#f0820431#ff#join_rhs" : "77cd9dha843p3v12qso4qpe4qoe"
},
"layerSize" : 1,
"ra" : {
"base" : [
" {2} r1 = namespaces AND NOT namespacembrs_1#antijoin_rhs(Lhs.0)",
" return r1"
],
"standard" : [
" {2} r1 = JOIN QualifiedName::Namespace::getAQualifierForMembers#f0820431#ff#prev_delta WITH QualifiedName::Namespace::getAQualifierForMembers#f0820431#ff#join_rhs#1 ON FIRST 1 OUTPUT Rhs.1, Lhs.1",
"",
" {2} r2 = JOIN QualifiedName::Namespace::getAQualifierForMembers#f0820431#ff#prev_delta WITH QualifiedName::Namespace::getAQualifierForMembers#f0820431#ff#join_rhs ON FIRST 1 OUTPUT Rhs.1, (Lhs.1 ++ \"::\" ++ Rhs.2)",
"",
" {2} r3 = r1 UNION r2",
" {2} r4 = r3 AND NOT QualifiedName::Namespace::getAQualifierForMembers#f0820431#ff#prev(Lhs.0, Lhs.1)",
" return r4"
],
"order_500000" : [
" {2} r1 = JOIN QualifiedName::Namespace::getAQualifierForMembers#f0820431#ff#prev_delta WITH QualifiedName::Namespace::getAQualifierForMembers#f0820431#ff#join_rhs#1 ON FIRST 1 OUTPUT Rhs.1, Lhs.1",
"",
" {2} r2 = JOIN QualifiedName::Namespace::getAQualifierForMembers#f0820431#ff#prev_delta WITH namespacembrs ON FIRST 1 OUTPUT Rhs.1, Lhs.1",
" {2} r3 = JOIN r2 WITH namespaces ON FIRST 1 OUTPUT Lhs.0, (Lhs.1 ++ \"::\" ++ Rhs.1)",
"",
" {2} r4 = r1 UNION r3",
" {2} r5 = r4 AND NOT QualifiedName::Namespace::getAQualifierForMembers#f0820431#ff#prev(Lhs.0, Lhs.1)",
" return r5"
]
},
"pipelineRuns" : [ {
"raReference" : "base"
}, {
"raReference" : "order_500000"
} ]
}

View File

@@ -0,0 +1,42 @@
import { expect } from 'chai';
import * as fs from 'fs-extra';
import * as path from 'path';
import 'mocha';
import { parseVisualizerData } from '../../src/pure/log-summary-parser';
describe('Evaluator log summary tests', async function () {
describe('for a valid summary text', async function () {
it('should return only valid EvaluatorLogData objects', async function () {
const validSummaryText = await fs.readFile(path.join(__dirname, 'evaluator-log-summaries/valid-summary.jsonl'), 'utf8');
const logDataItems = parseVisualizerData(validSummaryText.toString());
expect(logDataItems).to.not.be.undefined;
expect (logDataItems.length).to.eq(3);
for (const item of logDataItems) {
expect(item.queryCausingWork).to.not.be.empty;
expect(item.predicateName).to.not.be.empty;
expect(item.millis).to.be.a('number');
expect(item.resultSize).to.be.a('number');
expect(item.ra).to.not.be.undefined;
expect(item.ra).to.not.be.empty;
for (const [pipeline, steps] of Object.entries(item.ra)) {
expect (pipeline).to.not.be.empty;
expect (steps).to.not.be.undefined;
expect (steps.length).to.be.greaterThan(0);
}
}
});
it('should not parse a summary header object', async function () {
const invalidHeaderText = await fs.readFile(path.join(__dirname, 'evaluator-log-summaries/invalid-header.jsonl'), 'utf8');
const logDataItems = parseVisualizerData(invalidHeaderText);
expect (logDataItems.length).to.eq(0);
});
it('should not parse a log event missing RA or millis fields', async function () {
const invalidSummaryText = await fs.readFile(path.join(__dirname, 'evaluator-log-summaries/invalid-summary.jsonl'), 'utf8');
const logDataItems = parseVisualizerData(invalidSummaryText);
expect (logDataItems.length).to.eq(0);
});
});
});