Compare commits

..

29 Commits

Author SHA1 Message Date
Charis Kyriakou
3f2c9b647c v1.6.1 (#1220)
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
2022-03-17 12:04:37 +00:00
Shati Patel
7d5b4369c1 Fix highlighting issues (#1219) 2022-03-17 11:45:31 +00:00
Shati Patel
aade33fa88 Minor webview fixes (#1217) 2022-03-17 11:12:50 +00:00
Shati Patel
2a8a90bdfc Change public occurrences of "remote queries" (#1215) 2022-03-17 10:14:32 +00:00
Shati Patel
f36048cc95 Use variable for highlighting code (#1216) 2022-03-17 10:08:42 +00:00
Charis Kyriakou
517feeca21 Remove SARIF viewer support (#1213) 2022-03-16 14:39:52 +00:00
Charis Kyriakou
9436a49118 Remove helper command for working on the Remote Query results view (#1214) 2022-03-16 14:19:19 +00:00
Charis Kyriakou
0e02cb08fd Enable viewing of analyses results (#1212) 2022-03-16 14:15:43 +00:00
Shati Patel
26244efc50 Create remote file links to GitHub URL (#1209)
Co-authored-by: Charis Kyriakou <charisk@github.com>
2022-03-16 14:11:17 +00:00
Charis Kyriakou
6339eeffe5 Minor styling fix for raw results (#1211) 2022-03-16 11:44:51 +00:00
Charis Kyriakou
8cc2f598eb Fix highlight region end column calculation (#1210) 2022-03-16 09:47:09 +00:00
Charis Kyriakou
46a1dd57f4 Minor style fixes around result rendering (#1208) 2022-03-15 14:43:24 +00:00
shati-patel
9d99fc521e Get database sha from result index 2022-03-15 10:30:01 +00:00
Shati Patel
bcf79354ee Bump CLI version in integration tests 2022-03-15 10:22:18 +00:00
Charis Kyriakou
27a8636bac Deal with non-printable characters when rendering raw results (#1203) 2022-03-14 11:25:33 +00:00
Charis Kyriakou
92a99938c9 Add support for remote queries raw results (#1198) 2022-03-14 08:18:43 +00:00
Charis Kyriakou
ed61eb0a95 Deal with analysis messages that have links to locations (#1195) 2022-03-14 08:14:09 +00:00
Andrew Eisenberg
50d495b522 Merge pull request #1201 from mrysav/patch-1
Install Dependency Review Action
2022-03-11 10:40:06 -08:00
Andrew Eisenberg
526d5c2c44 Apply suggestions from code review 2022-03-11 10:29:02 -08:00
Charis Kyriakou
1720f9201e Update Primer React to v35 (#1199) 2022-03-10 20:24:12 +00:00
Mitchell Rysavy
e62de1ca22 Create dependency-review.yml 2022-03-10 14:48:06 -05:00
Charis Kyriakou
d052ddb742 Rename analysis alert results (#1197) 2022-03-10 07:56:05 +00:00
Andrew Eisenberg
af53a02ea5 Merge pull request #1192 from github/aeisenberg/disable-openvsx-deploy
Disable the open-vsx-publish job
2022-03-09 09:27:17 -08:00
Charis Kyriakou
8e2d18da8c Rename ColumnValue to CellValue (#1196) 2022-03-09 16:44:15 +00:00
Charis Kyriakou
2c5004387d Add support for showing code flows (#1187) 2022-03-09 09:15:45 +00:00
Charis Kyriakou
3fc3b259ba Add pre-push hook check to block leftover .only()s (#1189) 2022-03-08 09:32:18 +00:00
Andrew Eisenberg
cd95f68692 Merge pull request #1191 from github/version/bump-to-v1.6.1
Bump version to v1.6.1
2022-03-07 10:25:23 -08:00
Andrew Eisenberg
59c3b1ba2f Disable the open-vsx-publish job
It is failing, blocked on #1085
2022-03-07 10:19:42 -08:00
aeisenberg
fa85865fe5 Bump version to v1.6.1 2022-03-07 18:04:29 +00:00
44 changed files with 1051 additions and 617 deletions

17
.github/workflows/dependency-review.yml vendored Normal file
View File

@@ -0,0 +1,17 @@
name: 'Dependency Review'
on:
- pull_request
- workflow_dispatch
permissions:
actions: read
pull-requests: read
jobs:
dependency-review:
runs-on: ubuntu-latest
steps:
- name: 'Checkout Repository'
uses: actions/checkout@v3
- name: 'Dependency Review'
uses: dsp-testing/dependency-review-action@main

View File

@@ -135,7 +135,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
version: ['v2.3.3', 'v2.4.6', 'v2.5.9', 'v2.6.3', 'v2.7.6', 'v2.8.2', 'nightly']
version: ['v2.3.3', 'v2.4.6', 'v2.5.9', 'v2.6.3', 'v2.7.6', 'v2.8.3', 'nightly']
env:
CLI_VERSION: ${{ matrix.version }}
NIGHTLY_URL: ${{ needs.find-nightly.outputs.url }}

View File

@@ -147,11 +147,13 @@ jobs:
If this was an authentication problem, please make sure the \
auth token hasn't expired."
# TODO This job is currently broken and is blocked on https://github.com/github/vscode-codeql/issues/1085
open-vsx-publish:
name: Publish to Open VSX Registry
needs: build
environment: publish-open-vsx
runs-on: ubuntu-latest
if: 1 == 0
env:
OPEN_VSX_TOKEN: ${{ secrets.OPEN_VSX_TOKEN }}
steps:

View File

@@ -1,5 +1,9 @@
# CodeQL for Visual Studio Code: Changelog
## 1.6.1 - 17 March 2022
No user facing changes.
## 1.6.0 - 7 March 2022
- Fix a bug where database upgrades could not be resolved if some of the target pack's dependencies are outside of the workspace. [#1138](https://github.com/github/vscode-codeql/pull/1138)

View File

@@ -1,17 +1,17 @@
{
"name": "vscode-codeql",
"version": "1.6.0",
"version": "1.6.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "vscode-codeql",
"version": "1.6.0",
"version": "1.6.1",
"license": "MIT",
"dependencies": {
"@octokit/rest": "^18.5.6",
"@primer/octicons-react": "^16.3.0",
"@primer/react": "^34.3.0",
"@primer/react": "^35.0.0",
"child-process-promise": "^2.2.1",
"classnames": "~2.2.6",
"d3": "^6.3.1",
@@ -647,9 +647,9 @@
}
},
"node_modules/@primer/behaviors": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@primer/behaviors/-/behaviors-1.0.3.tgz",
"integrity": "sha512-zh1FKvAXLjKs0rr9Ik9E5M3Q9/npa9hmpuHKmYZn7u9QnSl+X13jFPme3AmtokOlfduFYeHfQyzSIJEhSEVl3w=="
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@primer/behaviors/-/behaviors-1.1.0.tgz",
"integrity": "sha512-Ej2OUc3ZIFaR7WwIUqESO1DTzmpb7wc8xbTVRT9s52jZQDjN7g5iljoK3ocYZm+BIAcKn3MvcwB42hEk4Ga4xQ=="
},
"node_modules/@primer/octicons-react": {
"version": "16.3.0",
@@ -668,11 +668,11 @@
"integrity": "sha512-+Gwo89YK1OFi6oubTlah/zPxxzMNaMLy+inECAYI646KIFdzzhAsKWb3z5tSOu5Ff7no4isRV64rWfMSKLZclw=="
},
"node_modules/@primer/react": {
"version": "34.3.0",
"resolved": "https://registry.npmjs.org/@primer/react/-/react-34.3.0.tgz",
"integrity": "sha512-C0qrULg4pcfcPaHwZVyU86HACPP/xVjhNqQaEgvdPV8tFA86ql7EVMuoA973Ke42rUvrhmeO4GBILwQD+3im5A==",
"version": "35.0.0",
"resolved": "https://registry.npmjs.org/@primer/react/-/react-35.0.0.tgz",
"integrity": "sha512-pjraRDHoT6Lwmto31ZN+WrtNCDA6lieOhr+4XC1z8wuq/JSGJVB3gHePi2/yIZldy2WoK55O1lsyp8llUiakog==",
"dependencies": {
"@primer/behaviors": "1.0.3",
"@primer/behaviors": "1.1.0",
"@primer/octicons-react": "16.1.1",
"@primer/primitives": "7.1.1",
"@radix-ui/react-polymorphic": "0.0.14",
@@ -688,8 +688,13 @@
"color2k": "1.2.4",
"deepmerge": "4.2.2",
"focus-visible": "5.2.0",
"history": "5.0.0",
"styled-system": "5.1.5"
},
"engines": {
"node": ">=12",
"npm": ">=7"
},
"peerDependencies": {
"react": "^17.0.0",
"react-dom": "^17.0.0",
@@ -6568,6 +6573,14 @@
"he": "bin/he"
}
},
"node_modules/history": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/history/-/history-5.0.0.tgz",
"integrity": "sha512-3NyRMKIiFSJmIPdq7FxkNMJkQ7ZEtVblOQ38VtKaA0zZMW1Eo6Q6W8oDKEflr1kNNTItSnk4JMCO1deeSgbLLg==",
"dependencies": {
"@babel/runtime": "^7.7.6"
}
},
"node_modules/hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
@@ -13761,9 +13774,9 @@
}
},
"@primer/behaviors": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@primer/behaviors/-/behaviors-1.0.3.tgz",
"integrity": "sha512-zh1FKvAXLjKs0rr9Ik9E5M3Q9/npa9hmpuHKmYZn7u9QnSl+X13jFPme3AmtokOlfduFYeHfQyzSIJEhSEVl3w=="
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@primer/behaviors/-/behaviors-1.1.0.tgz",
"integrity": "sha512-Ej2OUc3ZIFaR7WwIUqESO1DTzmpb7wc8xbTVRT9s52jZQDjN7g5iljoK3ocYZm+BIAcKn3MvcwB42hEk4Ga4xQ=="
},
"@primer/octicons-react": {
"version": "16.3.0",
@@ -13777,11 +13790,11 @@
"integrity": "sha512-+Gwo89YK1OFi6oubTlah/zPxxzMNaMLy+inECAYI646KIFdzzhAsKWb3z5tSOu5Ff7no4isRV64rWfMSKLZclw=="
},
"@primer/react": {
"version": "34.3.0",
"resolved": "https://registry.npmjs.org/@primer/react/-/react-34.3.0.tgz",
"integrity": "sha512-C0qrULg4pcfcPaHwZVyU86HACPP/xVjhNqQaEgvdPV8tFA86ql7EVMuoA973Ke42rUvrhmeO4GBILwQD+3im5A==",
"version": "35.0.0",
"resolved": "https://registry.npmjs.org/@primer/react/-/react-35.0.0.tgz",
"integrity": "sha512-pjraRDHoT6Lwmto31ZN+WrtNCDA6lieOhr+4XC1z8wuq/JSGJVB3gHePi2/yIZldy2WoK55O1lsyp8llUiakog==",
"requires": {
"@primer/behaviors": "1.0.3",
"@primer/behaviors": "1.1.0",
"@primer/octicons-react": "16.1.1",
"@primer/primitives": "7.1.1",
"@radix-ui/react-polymorphic": "0.0.14",
@@ -13797,6 +13810,7 @@
"color2k": "1.2.4",
"deepmerge": "4.2.2",
"focus-visible": "5.2.0",
"history": "5.0.0",
"styled-system": "5.1.5"
},
"dependencies": {
@@ -18799,6 +18813,14 @@
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
"dev": true
},
"history": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/history/-/history-5.0.0.tgz",
"integrity": "sha512-3NyRMKIiFSJmIPdq7FxkNMJkQ7ZEtVblOQ38VtKaA0zZMW1Eo6Q6W8oDKEflr1kNNTItSnk4JMCO1deeSgbLLg==",
"requires": {
"@babel/runtime": "^7.7.6"
}
},
"hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",

View File

@@ -4,7 +4,7 @@
"description": "CodeQL for Visual Studio Code",
"author": "GitHub",
"private": true,
"version": "1.6.0",
"version": "1.6.1",
"publisher": "GitHub",
"license": "MIT",
"icon": "media/VS-marketplace-CodeQL-icon.png",
@@ -258,7 +258,7 @@
"scope": "application",
"description": "Specifies whether or not to write telemetry events to the extension log."
},
"codeQL.remoteQueries.repositoryLists": {
"codeQL.variantAnalysis.repositoryLists": {
"type": [
"object",
null
@@ -272,14 +272,14 @@
}
},
"default": null,
"markdownDescription": "[For internal use only] Lists of GitHub repositories that you want to query remotely. This should be a JSON object where each key is a user-specified name for this repository list, and the value is an array of GitHub repositories (of the form `<owner>/<repo>`)."
"markdownDescription": "[For internal use only] Lists of GitHub repositories that you want to run variant analysis against. This should be a JSON object where each key is a user-specified name for this repository list, and the value is an array of GitHub repositories (of the form `<owner>/<repo>`)."
},
"codeQL.remoteQueries.controllerRepo": {
"codeQL.variantAnalysis.controllerRepo": {
"type": "string",
"default": "",
"pattern": "^$|^(?:[a-zA-Z0-9]+-)*[a-zA-Z0-9]+/[a-zA-Z0-9-_]+$",
"patternErrorMessage": "Please enter a valid GitHub repository",
"markdownDescription": "[For internal use only] The name of the GitHub repository where you can view the progress and results of the \"Run Remote query\" command. The repository should be of the form `<owner>/<repo>`)."
"markdownDescription": "[For internal use only] The name of the GitHub repository where you can view the progress and results of the \"Run Variant Analysis\" command. The repository should be of the form `<owner>/<repo>`)."
}
}
},
@@ -297,12 +297,8 @@
"title": "CodeQL: Run Query on Multiple Databases"
},
{
"command": "codeQL.runRemoteQuery",
"title": "CodeQL: Run Remote Query"
},
{
"command": "codeQL.showFakeRemoteQueryResults",
"title": "CodeQL: [Internal] Show fake remote query results"
"command": "codeQL.runVariantAnalysis",
"title": "CodeQL: Run Variant Analysis"
},
{
"command": "codeQL.runQueries",
@@ -546,7 +542,7 @@
},
{
"command": "codeQLQueryHistory.openOnGithub",
"title": "Open Remote Query on GitHub"
"title": "Open Variant Analysis on GitHub"
},
{
"command": "codeQLQueryResults.nextPathStep",
@@ -802,13 +798,9 @@
"when": "resourceLangId == ql && resourceExtname == .ql"
},
{
"command": "codeQL.runRemoteQuery",
"command": "codeQL.runVariantAnalysis",
"when": "config.codeQL.canary && editorLangId == ql && resourceExtname == .ql"
},
{
"command": "codeQL.showFakeRemoteQueryResults",
"when": "config.codeQL.canary"
},
{
"command": "codeQL.runQueries",
"when": "false"
@@ -984,7 +976,7 @@
"when": "editorLangId == ql && resourceExtname == .ql"
},
{
"command": "codeQL.runRemoteQuery",
"command": "codeQL.runVariantAnalysis",
"when": "config.codeQL.canary && editorLangId == ql && resourceExtname == .ql"
},
{
@@ -1065,7 +1057,7 @@
"dependencies": {
"@octokit/rest": "^18.5.6",
"@primer/octicons-react": "^16.3.0",
"@primer/react": "^34.3.0",
"@primer/react": "^35.0.0",
"child-process-promise": "^2.2.1",
"classnames": "~2.2.6",
"d3": "^6.3.1",
@@ -1170,7 +1162,7 @@
"husky": {
"hooks": {
"pre-commit": "npm run format-staged",
"pre-push": "npm run lint"
"pre-push": "npm run lint && scripts/forbid-mocha-only"
}
},
"lint-staged": {

View File

@@ -0,0 +1,6 @@
if grep -rq --include '*.test.ts' 'it.only\|describe.only' './test' './src'; then
echo 'There is a .only() in the tests. Please remove it.'
exit 1;
else
exit 0;
fi

View File

@@ -323,10 +323,10 @@ export function isCanary() {
export const NO_CACHE_AST_VIEWER = new Setting('disableCache', AST_VIEWER_SETTING);
// Settings for remote queries
const REMOTE_QUERIES_SETTING = new Setting('remoteQueries', ROOT_SETTING);
const REMOTE_QUERIES_SETTING = new Setting('variantAnalysis', ROOT_SETTING);
/**
* Lists of GitHub repositories that you want to query remotely via the "Run Remote query" command.
* Lists of GitHub repositories that you want to query remotely via the "Run Variant Analysis" command.
* Note: This command is only available for internal users.
*
* This setting should be a JSON object where each key is a user-specified name (string),
@@ -343,7 +343,7 @@ export async function setRemoteRepositoryLists(lists: Record<string, string[]> |
}
/**
* The name of the "controller" repository that you want to use with the "Run Remote query" command.
* The name of the "controller" repository that you want to use with the "Run Variant Analysis" command.
* Note: This command is only available for internal users.
*
* This setting should be a GitHub repository of the form `<owner>/<repo>`.

View File

@@ -93,10 +93,7 @@ import { Credentials } from './authentication';
import { RemoteQueriesManager } from './remote-queries/remote-queries-manager';
import { RemoteQueryResult } from './remote-queries/remote-query-result';
import { URLSearchParams } from 'url';
import { RemoteQueriesInterfaceManager } from './remote-queries/remote-queries-interface';
import * as sampleData from './remote-queries/sample-data';
import { handleDownloadPacks, handleInstallPackDependencies } from './packaging';
import { AnalysesResultsManager } from './remote-queries/analyses-results-manager';
import { RemoteQueryHistoryItem } from './remote-queries/remote-query-history-item';
/**
@@ -847,9 +844,9 @@ async function activateWithInstalledDistribution(
registerRemoteQueryTextProvider();
// The "runRemoteQuery" command is internal-only.
// The "runVariantAnalysis" command is internal-only.
ctx.subscriptions.push(
commandRunnerWithProgress('codeQL.runRemoteQuery', async (
commandRunnerWithProgress('codeQL.runVariantAnalysis', async (
progress: ProgressCallback,
token: CancellationToken,
uri: Uri | undefined
@@ -869,7 +866,7 @@ async function activateWithInstalledDistribution(
throw new Error('Remote queries require the CodeQL Canary version to run.');
}
}, {
title: 'Run Remote Query',
title: 'Run Variant Analysis',
cancellable: true
})
);
@@ -888,17 +885,6 @@ async function activateWithInstalledDistribution(
await rqm.autoDownloadRemoteQueryResults(queryResult, token);
}));
ctx.subscriptions.push(
commandRunner('codeQL.showFakeRemoteQueryResults', async () => {
const analysisResultsManager = new AnalysesResultsManager(ctx, queryStorageDir, logger);
const rqim = new RemoteQueriesInterfaceManager(ctx, logger, analysisResultsManager);
await rqim.showResults(sampleData.sampleRemoteQuery, sampleData.sampleRemoteQueryResult);
await rqim.setAnalysisResults(sampleData.sampleAnalysesResultsStage1);
await rqim.setAnalysisResults(sampleData.sampleAnalysesResultsStage2);
await rqim.setAnalysisResults(sampleData.sampleAnalysesResultsStage3);
}));
ctx.subscriptions.push(
commandRunner(
'codeQL.openReferencedFile',

View File

@@ -79,11 +79,11 @@ export interface WholeFileLocation {
export type ResolvableLocationValue = WholeFileLocation | LineColumnLocation;
export type UrlValue = ResolvableLocationValue | string;
export type UrlValue = ResolvableLocationValue | string;
export type ColumnValue = EntityValue | number | string | boolean;
export type CellValue = EntityValue | number | string | boolean;
export type ResultRow = ColumnValue[];
export type ResultRow = CellValue[];
export interface RawResultSet {
readonly schema: ResultSetSchema;
@@ -104,6 +104,6 @@ export function transformBqrsResultSet(
}
export interface DecodedBqrsChunk {
tuples: ColumnValue[][];
tuples: CellValue[][];
next?: number;
}

View File

@@ -4,6 +4,7 @@ import {
LineColumnLocation,
WholeFileLocation
} from './bqrs-cli-types';
import { createRemoteFileRef } from './location-link-utils';
/**
* The CodeQL filesystem libraries use this pattern in `getURL()` predicates
@@ -93,3 +94,30 @@ export function isWholeFileLoc(loc: UrlValue): loc is WholeFileLocation {
export function isStringLoc(loc: UrlValue): loc is string {
return typeof loc === 'string';
}
export function tryGetRemoteLocation(
loc: UrlValue | undefined,
fileLinkPrefix: string
): string | undefined {
const resolvableLocation = tryGetResolvableLocation(loc);
if (!resolvableLocation) {
return undefined;
}
// Remote locations have the following format:
// file:/home/runner/work/<repo>/<repo/relative/path/to/file
// So we need to drop the first 6 parts of the path.
// TODO: We can make this more robust to other path formats.
const locationParts = resolvableLocation.uri.split('/');
const trimmedLocation = locationParts.slice(6, locationParts.length).join('/');
const fileLink = {
fileLinkPrefix,
filePath: trimmedLocation,
};
return createRemoteFileRef(
fileLink,
resolvableLocation.startLine,
resolvableLocation.endLine);
}

View File

@@ -394,8 +394,7 @@ export type FromRemoteQueriesMessage =
| OpenFileMsg
| OpenVirtualFileMsg
| RemoteQueryDownloadAnalysisResultsMessage
| RemoteQueryDownloadAllAnalysesResultsMessage
| RemoteQueryViewAnalysisResultsMessage;
| RemoteQueryDownloadAllAnalysesResultsMessage;
export type ToRemoteQueriesMessage =
| SetRemoteQueryResultMessage
@@ -430,7 +429,3 @@ export interface RemoteQueryDownloadAllAnalysesResultsMessage {
analysisSummaries: AnalysisSummary[];
}
export interface RemoteQueryViewAnalysisResultsMessage {
t: 'remoteQueryViewAnalysisResults';
analysisSummary: AnalysisSummary
}

View File

@@ -0,0 +1,15 @@
import { FileLink } from '../remote-queries/shared/analysis-result';
export function createRemoteFileRef(
fileLink: FileLink,
startLine?: number,
endLine?: number
): string {
if (startLine && endLine) {
return `${fileLink.fileLinkPrefix}/${fileLink.filePath}#L${startLine}-L${endLine}`;
} else if (startLine) {
return `${fileLink.fileLinkPrefix}/${fileLink.filePath}#L${startLine}`;
} else {
return `${fileLink.fileLinkPrefix}/${fileLink.filePath}`;
}
}

View File

@@ -127,35 +127,49 @@ export function parseSarifLocation(
userVisibleFile
} as ParsedSarifLocation;
} else {
const region = physicalLocation.region;
// We assume that the SARIF we're given always has startLine
// This is not mandated by the SARIF spec, but should be true of
// SARIF output by our own tools.
const startLine = region.startLine!;
// These defaults are from SARIF 2.1.0 spec, section 3.30.2, "Text Regions"
// https://docs.oasis-open.org/sarif/sarif/v2.1.0/cs01/sarif-v2.1.0-cs01.html#_Ref493492556
const endLine = region.endLine === undefined ? startLine : region.endLine;
const startColumn = region.startColumn === undefined ? 1 : region.startColumn;
// We also assume that our tools will always supply `endColumn` field, which is
// fortunate, since the SARIF spec says that it defaults to the end of the line, whose
// length we don't know at this point in the code.
//
// It is off by one with respect to the way vscode counts columns in selections.
const endColumn = region.endColumn! - 1;
const region = parseSarifRegion(physicalLocation.region);
return {
uri: effectiveLocation,
userVisibleFile,
startLine,
startColumn,
endLine,
endColumn,
...region
};
}
}
export function parseSarifRegion(
region: Sarif.Region
): {
startLine: number,
endLine: number,
startColumn: number,
endColumn: number
} {
// The SARIF we're given should have a startLine, but we
// fall back to 1, just in case something has gone wrong.
const startLine = region.startLine ?? 1;
// These defaults are from SARIF 2.1.0 spec, section 3.30.2, "Text Regions"
// https://docs.oasis-open.org/sarif/sarif/v2.1.0/cs01/sarif-v2.1.0-cs01.html#_Ref493492556
const endLine = region.endLine === undefined ? startLine : region.endLine;
const startColumn = region.startColumn === undefined ? 1 : region.startColumn;
// Our tools should always supply `endColumn` field, which is fortunate, since
// the SARIF spec says that it defaults to the end of the line, whose
// length we don't know at this point in the code. We fall back to 1,
// just in case something has gone wrong.
//
// It is off by one with respect to the way vscode counts columns in selections.
const endColumn = (region.endColumn ?? 1) - 1;
return {
startLine,
startColumn,
endLine,
endColumn
};
}
export function isNoLocation(loc: ParsedSarifLocation): loc is NoLocation {
return 'hint' in loc;
}

View File

@@ -577,7 +577,7 @@ export class QueryHistoryManager extends DisposableObject {
this.treeDataProvider.remove(item);
void logger.log(`Deleted ${item.label}.`);
if (item.status === QueryStatus.InProgress) {
void logger.log('The remote query is still running on GitHub Actions. To cancel there, you must go to the query run in your browser.');
void logger.log('The variant analysis is still running on GitHub Actions. To cancel there, you must go to the workflow run in your browser.');
}
this._onDidRemoveQueryItem.fire(item);

View File

@@ -6,10 +6,12 @@ import { Credentials } from '../authentication';
import { Logger } from '../logging';
import { downloadArtifactFromLink } from './gh-actions-api-client';
import { AnalysisSummary } from './shared/remote-query-result';
import { AnalysisResults, AnalysisAlert } from './shared/analysis-result';
import { AnalysisResults, AnalysisAlert, AnalysisRawResults } from './shared/analysis-result';
import { UserCancellationException } from '../commandRunner';
import { sarifParser } from '../sarif-parser';
import { extractAnalysisAlerts } from './sarif-processing';
import { CodeQLCliServer } from '../cli';
import { extractRawResults } from './bqrs-processing';
export class AnalysesResultsManager {
// Store for the results of various analyses for each remote query.
@@ -18,6 +20,7 @@ export class AnalysesResultsManager {
constructor(
private readonly ctx: ExtensionContext,
private readonly cliServer: CodeQLCliServer,
readonly storagePath: string,
private readonly logger: Logger,
) {
@@ -101,7 +104,7 @@ export class AnalysesResultsManager {
const analysisResults: AnalysisResults = {
nwo: analysis.nwo,
status: 'InProgress',
results: []
interpretedResults: []
};
const queryId = analysis.downloadLink.queryId;
const resultsForQuery = this.internalGetAnalysesResults(queryId);
@@ -118,16 +121,26 @@ export class AnalysesResultsManager {
throw new Error(`Could not download the analysis results for ${analysis.nwo}: ${e.message}`);
}
const fileLinkPrefix = this.createGitHubDotcomFileLinkPrefix(analysis.nwo, analysis.databaseSha);
let newAnaysisResults: AnalysisResults;
if (path.extname(artifactPath) === '.sarif') {
const queryResults = await this.readResults(artifactPath);
const fileExtension = path.extname(artifactPath);
if (fileExtension === '.sarif') {
const queryResults = await this.readSarifResults(artifactPath, fileLinkPrefix);
newAnaysisResults = {
...analysisResults,
results: queryResults,
interpretedResults: queryResults,
status: 'Completed'
};
} else if (fileExtension === '.bqrs') {
const queryResults = await this.readBqrsResults(artifactPath, fileLinkPrefix);
newAnaysisResults = {
...analysisResults,
rawResults: queryResults,
status: 'Completed'
};
} else {
void this.logger.log('Cannot download results. Only alert and path queries are fully supported.');
void this.logger.log(`Cannot download results. File type '${fileExtension}' not supported.`);
newAnaysisResults = {
...analysisResults,
status: 'Failed'
@@ -137,11 +150,15 @@ export class AnalysesResultsManager {
void publishResults([...resultsForQuery]);
}
private async readResults(filePath: string): Promise<AnalysisAlert[]> {
private async readBqrsResults(filePath: string, fileLinkPrefix: string): Promise<AnalysisRawResults> {
return await extractRawResults(this.cliServer, this.logger, filePath, fileLinkPrefix);
}
private async readSarifResults(filePath: string, fileLinkPrefix: string): Promise<AnalysisAlert[]> {
const sarifLog = await sarifParser(filePath);
const processedSarif = extractAnalysisAlerts(sarifLog);
if (processedSarif.errors) {
const processedSarif = extractAnalysisAlerts(sarifLog, fileLinkPrefix);
if (processedSarif.errors.length) {
void this.logger.log(`Error processing SARIF file: ${os.EOL}${processedSarif.errors.join(os.EOL)}`);
}
@@ -151,4 +168,8 @@ export class AnalysesResultsManager {
private isAnalysisInMemory(analysis: AnalysisSummary): boolean {
return this.internalGetAnalysesResults(analysis.downloadLink.queryId).some(x => x.nwo === analysis.nwo);
}
private createGitHubDotcomFileLinkPrefix(nwo: string, sha: string): string {
return `https://github.com/${nwo}/blob/${sha}`;
}
}

View File

@@ -0,0 +1,35 @@
import { CodeQLCliServer } from '../cli';
import { Logger } from '../logging';
import { transformBqrsResultSet } from '../pure/bqrs-cli-types';
import { AnalysisRawResults } from './shared/analysis-result';
import { MAX_RAW_RESULTS } from './shared/result-limits';
export async function extractRawResults(
cliServer: CodeQLCliServer,
logger: Logger,
filePath: string,
fileLinkPrefix: string,
): Promise<AnalysisRawResults> {
const bqrsInfo = await cliServer.bqrsInfo(filePath);
const resultSets = bqrsInfo['result-sets'];
if (resultSets.length < 1) {
throw new Error('No result sets found in results file.');
}
if (resultSets.length > 1) {
void logger.log('Multiple result sets found in results file. Only the first one will be used.');
}
const schema = resultSets[0];
const chunk = await cliServer.bqrsDecode(
filePath,
schema.name,
{ pageSize: MAX_RAW_RESULTS });
const resultSet = transformBqrsResultSet(schema, chunk);
const capped = !!chunk.next;
return { schema, resultSet, fileLinkPrefix, capped };
}

View File

@@ -12,6 +12,7 @@ import { RemoteQueryFailureIndexItem, RemoteQueryResultIndex, RemoteQuerySuccess
interface ApiSuccessIndexItem {
nwo: string;
id: string;
sha?: string;
results_count: number;
bqrs_file_size: number;
sarif_file_size?: number;
@@ -51,6 +52,7 @@ export async function getRemoteQueryIndex(
id: item.id.toString(),
artifactId: artifactId,
nwo: item.nwo,
sha: item.sha,
resultCount: item.results_count,
bqrsFileSize: item.bqrs_file_size,
sarifFileSize: item.sarif_file_size
@@ -265,18 +267,18 @@ function getWorkflowError(conclusion: string | null): string {
}
if (conclusion === 'cancelled') {
return 'The remote query execution was cancelled.';
return 'Variant analysis execution was cancelled.';
}
if (conclusion === 'timed_out') {
return 'The remote query execution timed out.';
return 'Variant analysis execution timed out.';
}
if (conclusion === 'failure') {
// TODO: Get the actual error from the workflow or potentially
// from an artifact from the action itself.
return 'The remote query execution has failed.';
return 'Variant analysis execution has failed.';
}
return `Unexpected query execution conclusion: ${conclusion}`;
return `Unexpected variant analysis execution conclusion: ${conclusion}`;
}

View File

@@ -4,9 +4,7 @@ import {
window as Window,
ViewColumn,
Uri,
workspace,
extensions,
commands,
workspace
} from 'vscode';
import * as path from 'path';
@@ -14,8 +12,7 @@ import {
ToRemoteQueriesMessage,
FromRemoteQueriesMessage,
RemoteQueryDownloadAnalysisResultsMessage,
RemoteQueryDownloadAllAnalysesResultsMessage,
RemoteQueryViewAnalysisResultsMessage,
RemoteQueryDownloadAllAnalysesResultsMessage
} from '../pure/interface-types';
import { Logger } from '../logging';
import { getHtmlForWebview } from '../interface-utils';
@@ -94,7 +91,7 @@ export class RemoteQueriesInterfaceManager {
const { ctx } = this;
const panel = (this.panel = Window.createWebviewPanel(
'remoteQueriesView',
'Remote Query Results',
'CodeQL Query Results',
{ viewColumn: ViewColumn.Active, preserveFocus: true },
{
enableScripts: true,
@@ -189,7 +186,7 @@ export class RemoteQueriesInterfaceManager {
break;
case 'remoteQueryError':
void this.logger.log(
`Remote query error: ${msg.error}`
`Variant analysis error: ${msg.error}`
);
break;
case 'openFile':
@@ -204,9 +201,6 @@ export class RemoteQueriesInterfaceManager {
case 'remoteQueryDownloadAllAnalysesResults':
await this.downloadAllAnalysesResults(msg);
break;
case 'remoteQueryViewAnalysisResults':
await this.viewAnalysisResults(msg);
break;
default:
assertNever(msg);
}
@@ -225,31 +219,6 @@ export class RemoteQueriesInterfaceManager {
results => this.setAnalysisResults(results));
}
private async viewAnalysisResults(msg: RemoteQueryViewAnalysisResultsMessage): Promise<void> {
const downloadLink = msg.analysisSummary.downloadLink;
const filePath = path.join(this.analysesResultsManager.storagePath, downloadLink.queryId, downloadLink.id, downloadLink.innerFilePath || '');
const sarifViewerExtensionId = 'MS-SarifVSCode.sarif-viewer';
const sarifExt = extensions.getExtension(sarifViewerExtensionId);
if (!sarifExt) {
// Ask the user if they want to install the extension to view the results.
void commands.executeCommand('workbench.extensions.installExtension', sarifViewerExtensionId);
return;
}
if (!sarifExt.isActive) {
await sarifExt.activate();
}
// Clear any previous results before showing new results
await sarifExt.exports.closeAllLogs();
await sarifExt.exports.openLogs([
Uri.file(filePath),
]);
}
public async setAnalysisResults(analysesResults: AnalysisResults[]): Promise<void> {
if (this.panel?.active) {
await this.postMessage({
@@ -319,6 +288,7 @@ export class RemoteQueriesInterfaceManager {
return sortedAnalysisSummaries.map((analysisResult) => ({
nwo: analysisResult.nwo,
databaseSha: analysisResult.databaseSha || 'HEAD',
resultCount: analysisResult.resultCount,
downloadLink: analysisResult.downloadLink,
fileSize: this.formatFileSize(analysisResult.fileSizeInBytes)

View File

@@ -41,7 +41,7 @@ export class RemoteQueriesManager extends DisposableObject {
logger: Logger,
) {
super();
this.analysesResultsManager = new AnalysesResultsManager(ctx, storagePath, logger);
this.analysesResultsManager = new AnalysesResultsManager(ctx, cliServer, storagePath, logger);
this.interfaceManager = new RemoteQueriesInterfaceManager(ctx, logger, this.analysesResultsManager);
this.remoteQueriesMonitor = new RemoteQueriesMonitor(ctx, logger);
@@ -157,11 +157,11 @@ export class RemoteQueriesManager extends DisposableObject {
} else if (queryWorkflowResult.status === 'CompletedUnsuccessfully') {
queryItem.failureReason = queryWorkflowResult.error;
queryItem.status = QueryStatus.Failed;
void showAndLogErrorMessage(`Remote query execution failed. Error: ${queryWorkflowResult.error}`);
void showAndLogErrorMessage(`Variant analysis execution failed. Error: ${queryWorkflowResult.error}`);
} else if (queryWorkflowResult.status === 'Cancelled') {
queryItem.failureReason = 'Cancelled';
queryItem.status = QueryStatus.Failed;
void showAndLogErrorMessage('Remote query monitoring was cancelled');
void showAndLogErrorMessage('Variant analysis monitoring was cancelled');
} else if (queryWorkflowResult.status === 'InProgress') {
// Should not get here. Only including this to ensure `assertNever` uses proper type checking.
void showAndLogErrorMessage(`Unexpected status: ${queryWorkflowResult.status}`);
@@ -181,6 +181,7 @@ export class RemoteQueriesManager extends DisposableObject {
.slice(0, autoDownloadMaxCount)
.map(a => ({
nwo: a.nwo,
databaseSha: a.databaseSha,
resultCount: a.resultCount,
downloadLink: a.downloadLink,
fileSize: String(a.fileSizeInBytes)
@@ -196,6 +197,7 @@ export class RemoteQueriesManager extends DisposableObject {
const analysisSummaries = resultIndex.successes.map(item => ({
nwo: item.nwo,
databaseSha: item.sha || 'HEAD',
resultCount: item.resultCount,
fileSizeInBytes: item.sarifFileSize ? item.sarifFileSize : item.bqrsFileSize,
downloadLink: {

View File

@@ -49,7 +49,7 @@ export class RemoteQueriesMonitor {
attemptCount++;
}
void this.logger.log('Remote query monitoring timed out after 2 days');
void this.logger.log('Variant analysis monitoring timed out after 2 days');
return { status: 'Cancelled' };
}

View File

@@ -8,6 +8,7 @@ export interface RemoteQuerySuccessIndexItem {
id: string;
artifactId: number;
nwo: string;
sha?: string;
resultCount: number;
bqrsFileSize: number;
sarifFileSize?: number;

View File

@@ -10,6 +10,7 @@ export interface RemoteQueryResult {
export interface AnalysisSummary {
nwo: string,
databaseSha: string,
resultCount: number,
downloadLink: DownloadLink,
fileSizeInBytes: number

View File

@@ -65,7 +65,7 @@ export async function getRepositories(): Promise<string[] | undefined> {
const quickpick = await window.showQuickPick<RepoListQuickPickItem>(
quickPickItems,
{
placeHolder: 'Select a repository list. You can define repository lists in the `codeQL.remoteQueries.repositoryLists` setting.',
placeHolder: 'Select a repository list. You can define repository lists in the `codeQL.variantAnalysis.repositoryLists` setting.',
ignoreFocusOut: true,
});
if (quickpick?.repoList.length) {
@@ -80,7 +80,7 @@ export async function getRepositories(): Promise<string[] | undefined> {
const remoteRepo = await window.showInputBox({
title: 'Enter a GitHub repository in the format <owner>/<repo> (e.g. github/codeql)',
placeHolder: '<owner>/<repo>',
prompt: 'Tip: you can save frequently used repositories in the `codeQL.remoteQueries.repositoryLists` setting',
prompt: 'Tip: you can save frequently used repositories in the `codeQL.variantAnalysis.repositoryLists` setting',
ignoreFocusOut: true,
});
if (!remoteRepo) {
@@ -430,7 +430,7 @@ async function ensureNameAndSuite(queryPackDir: string, packRelativePath: string
qlpack.name = QUERY_PACK_NAME;
qlpack.defaultSuite = [{
description: 'Query suite for remote query'
description: 'Query suite for variant analysis'
}, {
query: packRelativePath.replace(/\\/g, '/')
}];

View File

@@ -1,207 +0,0 @@
import { RemoteQuery } from './remote-query';
import { RemoteQueryResult } from './remote-query-result';
import { AnalysisResults } from './shared/analysis-result';
export const sampleRemoteQuery: RemoteQuery = {
queryName: 'Inefficient regular expression',
queryFilePath: '/Users/foo/dev/vscode-codeql-starter/ql/javascript/ql/src/Performance/ReDoS.ql',
queryText: '/**\n * @name Inefficient regular expression\n * @description A regular expression that requires exponential time to match certain inputs\n * can be a performance bottleneck, and may be vulnerable to denial-of-service\n * attacks.\n * @kind problem\n * @problem.severity error\n * @security-severity 7.5\n * @precision high\n * @id js/redos\n * @tags security\n * external/cwe/cwe-1333\n * external/cwe/cwe-730\n * external/cwe/cwe-400\n */\n\nimport javascript\nimport semmle.javascript.security.performance.ReDoSUtil\nimport semmle.javascript.security.performance.ExponentialBackTracking\n\nfrom RegExpTerm t, string pump, State s, string prefixMsg\nwhere hasReDoSResult(t, pump, s, prefixMsg)\nselect t,\n "This part of the regular expression may cause exponential backtracking on strings " + prefixMsg +\n "containing many repetitions of \'" + pump + "\'."\n',
language: 'javascript',
controllerRepository: {
owner: 'big-corp',
name: 'controller-repo'
},
repositories: [
{
owner: 'big-corp',
name: 'repo1'
},
{
owner: 'big-corp',
name: 'repo2'
},
{
owner: 'big-corp',
name: 'repo3'
},
{
owner: 'big-corp',
name: 'repo4'
},
{
owner: 'big-corp',
name: 'repo5'
}
],
executionStartTime: new Date('2022-01-06T17:02:15.026Z').getTime(),
actionsWorkflowRunId: 1662757118
};
export const sampleRemoteQueryResult: RemoteQueryResult = {
queryId: 'query123',
executionEndTime: new Date('2022-01-06T17:04:37.026Z').getTime(),
analysisSummaries: [
{
nwo: 'big-corp/repo1',
resultCount: 85,
fileSizeInBytes: 14123,
downloadLink: {
id: '137697017',
urlPath: '/repos/big-corp/controller-repo/actions/artifacts/137697017',
innerFilePath: 'results.sarif',
queryId: 'query.ql-123-xyz'
}
},
{
nwo: 'big-corp/repo2',
resultCount: 20,
fileSizeInBytes: 8698,
downloadLink: {
id: '137697018',
urlPath: '/repos/big-corp/controller-repo/actions/artifacts/137697018',
innerFilePath: 'results.sarif',
queryId: 'query.ql-123-xyz'
}
},
{
nwo: 'big-corp/repo3',
resultCount: 8,
fileSizeInBytes: 4123,
downloadLink: {
id: '137697019',
urlPath: '/repos/big-corp/controller-repo/actions/artifacts/137697019',
innerFilePath: 'results.sarif',
queryId: 'query.ql-123-xyz'
}
},
{
nwo: 'big-corp/repo4',
resultCount: 3,
fileSizeInBytes: 3313,
downloadLink: {
id: '137697020',
urlPath: '/repos/big-corp/controller-repo/actions/artifacts/137697020',
innerFilePath: 'results.sarif',
queryId: 'query.ql-123-xyz'
}
}
],
analysisFailures: [
{
nwo: 'big-corp/repo5',
error: 'Error message'
},
{
nwo: 'big-corp/repo6',
error: 'Error message'
},
]
};
const createAnalysisResults = (n: number) => Array(n).fill(
{
message: 'This shell command depends on an uncontrolled [absolute path](1).',
severity: 'Error',
filePath: 'npm-packages/meteor-installer/config.js',
codeSnippet: {
startLine: 253,
endLine: 257,
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: 255,
startColumn: 28,
endColumn: 62
}
}
);
export const sampleAnalysesResultsStage1: AnalysisResults[] = [
{
nwo: 'big-corp/repo1',
status: 'InProgress',
results: []
},
{
nwo: 'big-corp/repo2',
status: 'InProgress',
results: []
},
{
nwo: 'big-corp/repo3',
status: 'InProgress',
results: []
},
// No entries for repo4
];
export const sampleAnalysesResultsStage2: AnalysisResults[] = [
{
nwo: 'big-corp/repo1',
status: 'Completed',
results: createAnalysisResults(85)
},
{
nwo: 'big-corp/repo2',
status: 'Completed',
results: createAnalysisResults(20)
},
{
nwo: 'big-corp/repo3',
status: 'InProgress',
results: []
},
{
nwo: 'big-corp/repo4',
status: 'InProgress',
results: []
},
];
export const sampleAnalysesResultsStage3: AnalysisResults[] = [
{
nwo: 'big-corp/repo1',
status: 'Completed',
results: createAnalysisResults(85)
},
{
nwo: 'big-corp/repo2',
status: 'Completed',
results: createAnalysisResults(20)
},
{
nwo: 'big-corp/repo3',
status: 'Completed',
results: createAnalysisResults(8)
},
{
nwo: 'big-corp/repo4',
status: 'Completed',
results: createAnalysisResults(3)
},
];
export const sampleAnalysesResultsWithFailure: AnalysisResults[] = [
{
nwo: 'big-corp/repo1',
status: 'Completed',
results: createAnalysisResults(85)
},
{
nwo: 'big-corp/repo2',
status: 'Completed',
results: createAnalysisResults(20)
},
{
nwo: 'big-corp/repo3',
status: 'Failed',
results: []
},
{
nwo: 'big-corp/repo4',
status: 'Completed',
results: createAnalysisResults(3)
},
];

View File

@@ -1,130 +1,100 @@
import * as sarif from 'sarif';
import { parseSarifPlainTextMessage, parseSarifRegion } from '../pure/sarif-utils';
import { AnalysisAlert, ResultSeverity } from './shared/analysis-result';
import {
AnalysisAlert,
CodeFlow,
AnalysisMessage,
AnalysisMessageToken,
ResultSeverity,
ThreadFlow,
CodeSnippet,
HighlightedRegion
} from './shared/analysis-result';
const defaultSeverity = 'Warning';
export function extractAnalysisAlerts(
sarifLog: sarif.Log
sarifLog: sarif.Log,
fileLinkPrefix: string
): {
alerts: AnalysisAlert[],
errors: string[]
} {
if (!sarifLog) {
return { alerts: [], errors: ['No SARIF log was found'] };
}
if (!sarifLog.runs) {
return { alerts: [], errors: ['No runs found in the SARIF file'] };
}
const errors: string[] = [];
const alerts: AnalysisAlert[] = [];
const errors: string[] = [];
for (const run of sarifLog.runs) {
if (!run.results) {
errors.push('No results found in the SARIF run');
continue;
}
for (const result of run.results) {
const message = result.message?.text;
if (!message) {
errors.push('No message found in the SARIF result');
for (const run of sarifLog.runs ?? []) {
for (const result of run.results ?? []) {
try {
alerts.push(...extractResultAlerts(run, result, fileLinkPrefix));
} catch (e) {
errors.push(`Error when processing SARIF result: ${e}`);
continue;
}
const severity = tryGetSeverity(run, result) || defaultSeverity;
if (!result.locations) {
errors.push('No locations found in the SARIF result');
continue;
}
for (const location of result.locations) {
const contextRegion = location.physicalLocation?.contextRegion;
if (!contextRegion) {
errors.push('No context region found in the SARIF result location');
continue;
}
if (contextRegion.startLine === undefined) {
errors.push('No start line set for a result context region');
continue;
}
if (contextRegion.endLine === undefined) {
errors.push('No end line set for a result context region');
continue;
}
if (!contextRegion.snippet?.text) {
errors.push('No text set for a result context region');
continue;
}
const region = location.physicalLocation?.region;
if (!region) {
errors.push('No region found in the SARIF result location');
continue;
}
if (region.startLine === undefined) {
errors.push('No start line set for a result region');
continue;
}
if (region.startColumn === undefined) {
errors.push('No start column set for a result region');
continue;
}
if (region.endColumn === undefined) {
errors.push('No end column set for a result region');
continue;
}
const filePath = location.physicalLocation?.artifactLocation?.uri;
if (!filePath) {
errors.push('No file path found in the SARIF result location');
continue;
}
const analysisAlert = {
message,
filePath,
severity,
codeSnippet: {
startLine: contextRegion.startLine,
endLine: contextRegion.endLine,
text: contextRegion.snippet.text
},
highlightedRegion: {
startLine: region.startLine,
startColumn: region.startColumn,
endLine: region.endLine,
endColumn: region.endColumn
}
};
const validationErrors = getAlertValidationErrors(analysisAlert);
if (validationErrors.length > 0) {
errors.push(...validationErrors);
continue;
}
alerts.push(analysisAlert);
}
}
}
return { alerts, errors };
}
export function tryGetSeverity(
sarifRun: sarif.Run,
result: sarif.Result
): ResultSeverity | undefined {
if (!sarifRun || !result) {
return undefined;
function extractResultAlerts(
run: sarif.Run,
result: sarif.Result,
fileLinkPrefix: string
): AnalysisAlert[] {
const alerts: AnalysisAlert[] = [];
const message = getMessage(result, fileLinkPrefix);
const rule = tryGetRule(run, result);
const severity = tryGetSeverity(run, result, rule) || defaultSeverity;
const codeFlows = getCodeFlows(result, fileLinkPrefix);
const shortDescription = getShortDescription(rule, message!);
for (const location of result.locations ?? []) {
const physicalLocation = location.physicalLocation!;
const filePath = physicalLocation.artifactLocation!.uri!;
const codeSnippet = getCodeSnippet(physicalLocation.contextRegion!);
const highlightedRegion = physicalLocation.region
? getHighlightedRegion(physicalLocation.region!)
: undefined;
const analysisAlert: AnalysisAlert = {
message,
shortDescription,
fileLink: {
fileLinkPrefix,
filePath,
},
severity,
codeSnippet,
highlightedRegion,
codeFlows: codeFlows
};
alerts.push(analysisAlert);
}
const rule = tryGetRule(sarifRun, result);
if (!rule) {
return alerts;
}
function getShortDescription(
rule: sarif.ReportingDescriptor | undefined,
message: AnalysisMessage,
): string {
if (rule?.shortDescription?.text) {
return rule.shortDescription.text;
}
return message.tokens.map(token => token.text).join();
}
export function tryGetSeverity(
sarifRun: sarif.Run,
result: sarif.Result,
rule: sarif.ReportingDescriptor | undefined
): ResultSeverity | undefined {
if (!sarifRun || !result || !rule) {
return undefined;
}
@@ -186,18 +156,92 @@ export function tryGetRule(
return undefined;
}
function getAlertValidationErrors(alert: AnalysisAlert): string[] {
const errors = [];
function getCodeSnippet(region: sarif.Region): CodeSnippet {
const text = region.snippet!.text!;
const { startLine, endLine } = parseSarifRegion(region);
if (alert.codeSnippet.startLine > alert.codeSnippet.endLine) {
errors.push('The code snippet start line is greater than the end line');
}
const highlightedRegion = alert.highlightedRegion;
if (highlightedRegion.endLine === highlightedRegion.startLine &&
highlightedRegion.endColumn < highlightedRegion.startColumn) {
errors.push('The highlighted region end column is greater than the start column');
}
return errors;
return {
startLine,
endLine,
text
} as CodeSnippet;
}
function getHighlightedRegion(region: sarif.Region): HighlightedRegion {
const { startLine, startColumn, endLine, endColumn } = parseSarifRegion(region);
return {
startLine,
startColumn,
endLine,
// parseSarifRegion currently shifts the end column by 1 to account
// for the way vscode counts columns so we need to shift it back.
endColumn: endColumn + 1
};
}
function getCodeFlows(
result: sarif.Result,
fileLinkPrefix: string
): CodeFlow[] {
const codeFlows = [];
if (result.codeFlows) {
for (const codeFlow of result.codeFlows) {
const threadFlows = [];
for (const threadFlow of codeFlow.threadFlows) {
for (const threadFlowLocation of threadFlow.locations) {
const physicalLocation = threadFlowLocation!.location!.physicalLocation!;
const filePath = physicalLocation!.artifactLocation!.uri!;
const codeSnippet = getCodeSnippet(physicalLocation.contextRegion!);
const highlightedRegion = physicalLocation.region
? getHighlightedRegion(physicalLocation.region)
: undefined;
threadFlows.push({
fileLink: {
fileLinkPrefix,
filePath,
},
codeSnippet,
highlightedRegion
} as ThreadFlow);
}
}
codeFlows.push({ threadFlows } as CodeFlow);
}
}
return codeFlows;
}
function getMessage(result: sarif.Result, fileLinkPrefix: string): AnalysisMessage {
const tokens: AnalysisMessageToken[] = [];
const messageText = result.message!.text!;
const messageParts = parseSarifPlainTextMessage(messageText);
for (const messagePart of messageParts) {
if (typeof messagePart === 'string') {
tokens.push({ t: 'text', text: messagePart });
} else {
const relatedLocation = result.relatedLocations!.find(rl => rl.id === messagePart.dest);
tokens.push({
t: 'location',
text: messagePart.text,
location: {
fileLink: {
fileLinkPrefix: fileLinkPrefix,
filePath: relatedLocation!.physicalLocation!.artifactLocation!.uri!,
},
highlightedRegion: getHighlightedRegion(relatedLocation!.physicalLocation!.region!),
}
});
}
}
return { tokens };
}

View File

@@ -1,17 +1,34 @@
import { RawResultSet, ResultSetSchema } from '../../pure/bqrs-cli-types';
export type AnalysisResultStatus = 'InProgress' | 'Completed' | 'Failed';
export interface AnalysisResults {
nwo: string;
status: AnalysisResultStatus;
results: AnalysisAlert[];
interpretedResults: AnalysisAlert[];
rawResults?: AnalysisRawResults;
}
export interface AnalysisRawResults {
schema: ResultSetSchema;
resultSet: RawResultSet;
fileLinkPrefix: string;
capped: boolean;
}
export interface AnalysisAlert {
message: string;
message: AnalysisMessage;
shortDescription: string;
severity: ResultSeverity;
fileLink: FileLink;
codeSnippet: CodeSnippet;
highlightedRegion?: HighlightedRegion;
codeFlows: CodeFlow[];
}
export interface FileLink {
fileLinkPrefix: string;
filePath: string;
codeSnippet: CodeSnippet
highlightedRegion: HighlightedRegion
}
export interface CodeSnippet {
@@ -23,8 +40,41 @@ export interface CodeSnippet {
export interface HighlightedRegion {
startLine: number;
startColumn: number;
endLine: number | undefined;
endLine: number;
endColumn: number;
}
export interface CodeFlow {
threadFlows: ThreadFlow[];
}
export interface ThreadFlow {
fileLink: FileLink;
codeSnippet: CodeSnippet;
highlightedRegion?: HighlightedRegion;
message?: AnalysisMessage;
}
export interface AnalysisMessage {
tokens: AnalysisMessageToken[]
}
export type AnalysisMessageToken =
| AnalysisMessageTextToken
| AnalysisMessageLocationToken;
export interface AnalysisMessageTextToken {
t: 'text';
text: string;
}
export interface AnalysisMessageLocationToken {
t: 'location';
text: string;
location: {
fileLink: FileLink;
highlightedRegion?: HighlightedRegion;
};
}
export type ResultSeverity = 'Recommendation' | 'Warning' | 'Error';

View File

@@ -19,6 +19,7 @@ export interface RemoteQueryResult {
export interface AnalysisSummary {
nwo: string,
databaseSha: string,
resultCount: number,
downloadLink: DownloadLink,
fileSize: string,

View File

@@ -0,0 +1,5 @@
// The maximum number of raw results to read from a BQRS file.
// This is used to avoid reading the entire result set into memory
// and trying to render it on screen. Users will be warned if the
// results are capped.
export const MAX_RAW_RESULTS = 500;

View File

@@ -1,15 +1,25 @@
import * as React from 'react';
import { AnalysisAlert } from '../shared/analysis-result';
import CodePaths from './CodePaths';
import FileCodeSnippet from './FileCodeSnippet';
const AnalysisAlertResult = ({ alert }: { alert: AnalysisAlert }) => {
const showPathsLink = alert.codeFlows.length > 0;
return <FileCodeSnippet
filePath={alert.filePath}
fileLink={alert.fileLink}
codeSnippet={alert.codeSnippet}
highlightedRegion={alert.highlightedRegion}
severity={alert.severity}
message={alert.message}
messageChildren={
showPathsLink && <CodePaths
codeFlows={alert.codeFlows}
ruleDescription={alert.shortDescription}
severity={alert.severity}
message={alert.message}
/>
}
/>;
};

View File

@@ -0,0 +1,180 @@
import { TriangleDownIcon, XCircleIcon } from '@primer/octicons-react';
import { ActionList, ActionMenu, Box, Button, Label, Link, Overlay } from '@primer/react';
import * as React from 'react';
import { useRef, useState } from 'react';
import styled from 'styled-components';
import { CodeFlow, AnalysisMessage, ResultSeverity } from '../shared/analysis-result';
import FileCodeSnippet from './FileCodeSnippet';
import SectionTitle from './SectionTitle';
import VerticalSpace from './VerticalSpace';
const StyledCloseButton = styled.button`
position: absolute;
top: 1em;
right: 4em;
background-color: var(--vscode-editor-background);
color: var(--vscode-editor-foreground);
border: none;
&:focus-visible {
outline: none
}
`;
const OverlayContainer = styled.div`
padding: 1em;
height: 100%;
width: 100%;
padding: 2em;
position: fixed;
top: 0;
left: 0;
background-color: var(--vscode-editor-background);
color: var(--vscode-editor-foreground);
overflow-y: scroll;
`;
const CloseButton = ({ onClick }: { onClick: () => void }) => (
<StyledCloseButton onClick={onClick} tabIndex={-1} >
<XCircleIcon size={24} />
</StyledCloseButton>
);
const CodePath = ({
codeFlow,
message,
severity
}: {
codeFlow: CodeFlow;
message: AnalysisMessage;
severity: ResultSeverity;
}) => {
return <>
{codeFlow.threadFlows.map((threadFlow, index) =>
<div key={`thread-flow-${index}`}>
{index !== 0 && <VerticalSpace size={3} />}
<Box display="flex" justifyContent="center" alignItems="center" width="42.5em">
<Box flexGrow={1} p={0} border="none">
<SectionTitle>Step {index + 1}</SectionTitle>
</Box>
{index === 0 &&
<Box p={0} border="none">
<Label>Source</Label>
</Box>
}
{index === codeFlow.threadFlows.length - 1 &&
<Box p={0} border="none">
<Label>Sink</Label>
</Box>
}
</Box>
<VerticalSpace size={2} />
<FileCodeSnippet
fileLink={threadFlow.fileLink}
codeSnippet={threadFlow.codeSnippet}
highlightedRegion={threadFlow.highlightedRegion}
severity={severity}
message={index === codeFlow.threadFlows.length - 1 ? message : threadFlow.message} />
</div>
)}
</>;
};
const getCodeFlowName = (codeFlow: CodeFlow) => {
const filePath = codeFlow.threadFlows[codeFlow.threadFlows.length - 1].fileLink.filePath;
return filePath.substring(filePath.lastIndexOf('/') + 1);
};
const Menu = ({
codeFlows,
setSelectedCodeFlow
}: {
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>;
};
const CodePaths = ({
codeFlows,
ruleDescription,
message,
severity
}: {
codeFlows: CodeFlow[],
ruleDescription: string,
message: AnalysisMessage,
severity: ResultSeverity
}) => {
const [isOpen, setIsOpen] = useState(false);
const [selectedCodeFlow, setSelectedCodeFlow] = useState(codeFlows[0]);
const anchorRef = useRef<HTMLDivElement>(null);
const linkRef = useRef<HTMLAnchorElement>(null);
const closeOverlay = () => setIsOpen(false);
return (
<Box ref={anchorRef}>
<Link
onClick={() => setIsOpen(true)}
ref={linkRef}
sx={{ cursor: 'pointer' }}>
Show paths
</Link>
{isOpen && (
<Overlay
returnFocusRef={linkRef}
onEscape={closeOverlay}
onClickOutside={closeOverlay}
anchorSide="outside-top">
<OverlayContainer>
<CloseButton onClick={closeOverlay} />
<SectionTitle>{ruleDescription}</SectionTitle>
<VerticalSpace size={2} />
<Box display="flex" justifyContent="center" alignItems="center">
<Box p={0} border="none">
{codeFlows.length} paths available: {selectedCodeFlow.threadFlows.length} steps in
</Box>
<Box flexGrow={1} p={0} paddingLeft="0.2em" border="none">
<Menu codeFlows={codeFlows} setSelectedCodeFlow={setSelectedCodeFlow} />
</Box>
</Box>
<VerticalSpace size={2} />
<CodePath
codeFlow={selectedCodeFlow}
severity={severity}
message={message} />
<VerticalSpace size={3} />
</OverlayContainer>
</Overlay>
)}
</Box>
);
};
export default CodePaths;

View File

@@ -1,12 +1,13 @@
import * as React from 'react';
import styled from 'styled-components';
import { CodeSnippet, HighlightedRegion, ResultSeverity } from '../shared/analysis-result';
import { CodeSnippet, FileLink, HighlightedRegion, AnalysisMessage, ResultSeverity } from '../shared/analysis-result';
import { Box, Link } from '@primer/react';
import VerticalSpace from './VerticalSpace';
import { createRemoteFileRef } from '../../pure/location-link-utils';
const borderColor = 'var(--vscode-editor-snippetFinalTabstopHighlightBorder)';
const warningColor = '#966C23';
const highlightColor = '#534425';
const highlightColor = 'var(--vscode-editor-findMatchHighlightBackground)';
const getSeverityColor = (severity: ResultSeverity) => {
switch (severity) {
@@ -75,19 +76,19 @@ const HighlightedLine = ({ text }: { text: string }) => {
};
const Message = ({
messageText,
message,
currentLineNumber,
highlightedRegion,
borderColor,
children
}: {
messageText: string,
message: AnalysisMessage,
currentLineNumber: number,
highlightedRegion: HighlightedRegion,
highlightedRegion?: HighlightedRegion,
borderColor: string,
children: React.ReactNode
}) => {
if (highlightedRegion.startLine !== currentLineNumber) {
if (!highlightedRegion || highlightedRegion.endLine !== currentLineNumber) {
return <></>;
}
@@ -101,7 +102,23 @@ const Message = ({
paddingTop="1em"
paddingBottom="1em">
<MessageText>
{messageText}
{message.tokens.map((token, index) => {
switch (token.t) {
case 'text':
return <span key={`token-${index}`}>{token.text}</span>;
case 'location':
return <Link
key={`token-${index}`}
href={createRemoteFileRef(
token.location.fileLink,
token.location.highlightedRegion?.startLine,
token.location.highlightedRegion?.endLine)}>
{token.text}
</Link>;
default:
return <></>;
}
})}
{children && <>
<VerticalSpace size={2} />
{children}
@@ -120,9 +137,9 @@ const CodeLine = ({
}: {
line: string,
lineNumber: number,
highlightedRegion: HighlightedRegion
highlightedRegion?: HighlightedRegion
}) => {
if (!shouldHighlightLine(lineNumber, highlightedRegion)) {
if (!highlightedRegion || !shouldHighlightLine(lineNumber, highlightedRegion)) {
return <PlainLine text={line} />;
}
@@ -140,7 +157,7 @@ const CodeLine = ({
? highlightedRegion.endColumn
: isLastHighlightedLine
? highlightedRegion.endColumn
: line.length;
: line.length + 1;
const section1 = line.substring(0, highlightStartColumn - 1);
const section2 = line.substring(highlightStartColumn - 1, highlightEndColumn - 1);
@@ -156,40 +173,39 @@ const CodeLine = ({
};
const FileCodeSnippet = ({
filePath,
fileLink,
codeSnippet,
highlightedRegion,
severity,
message,
messageChildren,
}: {
filePath: string,
fileLink: FileLink,
codeSnippet: CodeSnippet,
highlightedRegion: HighlightedRegion,
highlightedRegion?: HighlightedRegion,
severity?: ResultSeverity,
message?: string,
message?: AnalysisMessage,
messageChildren?: React.ReactNode,
}) => {
const code = codeSnippet.text.split('\n');
const startingLine = codeSnippet.startLine;
const endingLine = codeSnippet.endLine;
const titleFileUri = createRemoteFileRef(
fileLink,
startingLine,
endingLine);
return (
<Container>
<TitleContainer>
<Link>{filePath}</Link>
<Link href={titleFileUri}>{fileLink.filePath}</Link>
</TitleContainer>
<CodeContainer>
{code.map((line, index) => (
<div key={index}>
{message && severity && <Message
messageText={message}
currentLineNumber={startingLine + index}
highlightedRegion={highlightedRegion}
borderColor={getSeverityColor(severity)}>
{messageChildren}
</Message>}
<Box display="flex">
<Box
p={2}
@@ -207,13 +223,21 @@ const FileCodeSnippet = ({
paddingTop="0.01em"
paddingLeft="1.5em"
paddingRight="0.5em"
paddingBottom="0.2em">
paddingBottom="0.2em"
sx={{ wordBreak: 'break-word' }}>
<CodeLine
line={line}
lineNumber={startingLine + index}
highlightedRegion={highlightedRegion} />
</Box>
</Box>
{message && severity && <Message
message={message}
currentLineNumber={startingLine + index}
highlightedRegion={highlightedRegion}
borderColor={getSeverityColor(severity)}>
{messageChildren}
</Message>}
</div>
))}
</CodeContainer>

View File

@@ -0,0 +1,91 @@
import * as React from 'react';
import { Box, Link } from '@primer/react';
import { CellValue, RawResultSet, ResultSetSchema } from '../../pure/bqrs-cli-types';
import { tryGetRemoteLocation } from '../../pure/bqrs-utils';
import { useState } from 'react';
import TextButton from './TextButton';
import { convertNonPrintableChars } from '../../text-utils';
const numOfResultsInContractedMode = 5;
const Row = ({
row,
fileLinkPrefix
}: {
row: CellValue[],
fileLinkPrefix: string
}) => (
<>
{row.map((cell, cellIndex) => (
<Box key={cellIndex}
borderColor="border.default"
borderStyle="solid"
justifyContent="center"
alignItems="center"
p={2}
sx={{ wordBreak: 'break-word' }}>
<Cell value={cell} fileLinkPrefix={fileLinkPrefix} />
</Box>
))}
</>
);
const Cell = ({
value,
fileLinkPrefix
}: {
value: CellValue,
fileLinkPrefix: string
}) => {
switch (typeof value) {
case 'string':
case 'number':
case 'boolean':
return <span>{convertNonPrintableChars(value.toString())}</span>;
case 'object': {
const url = tryGetRemoteLocation(value.url, fileLinkPrefix);
return <Link href={url}>{convertNonPrintableChars(value.label)}</Link>;
}
}
};
const RawResultsTable = ({
schema,
results,
fileLinkPrefix
}: {
schema: ResultSetSchema,
results: RawResultSet,
fileLinkPrefix: string
}) => {
const [tableExpanded, setTableExpanded] = useState(false);
const numOfResultsToShow = tableExpanded ? results.rows.length : numOfResultsInContractedMode;
const showButton = results.rows.length > numOfResultsInContractedMode;
// Create n equal size columns. We use minmax(0, 1fr) because the
// minimum width of 1fr is auto, not 0.
// https://css-tricks.com/equal-width-columns-in-css-grid-are-kinda-weird/
const gridTemplateColumns = `repeat(${schema.columns.length}, minmax(0, 1fr))`;
return (
<>
<Box
display="grid"
gridTemplateColumns={gridTemplateColumns}
maxWidth="45rem"
p={2}>
{results.rows.slice(0, numOfResultsToShow).map((row, rowIndex) => (
<Row key={rowIndex} row={row} fileLinkPrefix={fileLinkPrefix} />
))}
</Box>
{
showButton &&
<TextButton size='x-small' onClick={() => setTableExpanded(!tableExpanded)}>
{tableExpanded ? (<span>View less</span>) : (<span>View all</span>)}
</TextButton>
}
</>
);
};
export default RawResultsTable;

View File

@@ -4,7 +4,7 @@ import * as Rdom from 'react-dom';
import { Flash, ThemeProvider } from '@primer/react';
import { ToRemoteQueriesMessage } from '../../pure/interface-types';
import { AnalysisSummary, RemoteQueryResult } from '../shared/remote-query-result';
import { MAX_RAW_RESULTS } from '../shared/result-limits';
import { vscode } from '../../view/vscode-api';
import SectionTitle from './SectionTitle';
@@ -16,8 +16,9 @@ import DownloadButton from './DownloadButton';
import { AnalysisResults } from '../shared/analysis-result';
import DownloadSpinner from './DownloadSpinner';
import CollapsibleItem from './CollapsibleItem';
import { AlertIcon, CodeSquareIcon, FileCodeIcon, FileSymlinkFileIcon, RepoIcon, TerminalIcon } from '@primer/octicons-react';
import { AlertIcon, CodeSquareIcon, FileCodeIcon, RepoIcon, TerminalIcon } from '@primer/octicons-react';
import AnalysisAlertResult from './AnalysisAlertResult';
import RawResultsTable from './RawResultsTable';
const numOfReposInContractedMode = 10;
@@ -51,13 +52,6 @@ const downloadAllAnalysesResults = (query: RemoteQueryResult) => {
});
};
const viewAnalysisResults = (analysisSummary: AnalysisSummary) => {
vscode.postMessage({
t: 'remoteQueryViewAnalysisResults',
analysisSummary
});
};
const openQueryFile = (queryResult: RemoteQueryResult) => {
vscode.postMessage({
t: 'openFile',
@@ -72,8 +66,13 @@ const openQueryTextVirtualFile = (queryResult: RemoteQueryResult) => {
});
};
const getAnalysisResultCount = (analysisResults: AnalysisResults): number => {
const rawResultCount = analysisResults.rawResults?.resultSet.rows.length || 0;
return analysisResults.interpretedResults.length + rawResultCount;
};
const sumAnalysesResults = (analysesResults: AnalysisResults[]) =>
analysesResults.reduce((acc, curr) => acc + curr.results.length, 0);
analysesResults.reduce((acc, curr) => acc + getAnalysisResultCount(curr), 0);
const QueryInfo = (queryResult: RemoteQueryResult) => (
<>
@@ -154,7 +153,7 @@ const SummaryTitleNoResults = () => (
</div>
);
const SummaryItemDownloadAndView = ({
const SummaryItemDownload = ({
analysisSummary,
analysisResults
}: {
@@ -174,13 +173,7 @@ const SummaryItemDownloadAndView = ({
</>;
}
return <>
<HorizontalSpace size={2} />
<a className="vscode-codeql__analysis-result-file-link"
onClick={() => viewAnalysisResults(analysisSummary)} >
<FileSymlinkFileIcon size={16} />
</a>
</>;
return <></>;
};
const SummaryItem = ({
@@ -195,7 +188,7 @@ const SummaryItem = ({
<span className="vscode-codeql__analysis-item">{analysisSummary.nwo}</span>
<span className="vscode-codeql__analysis-item"><Badge text={analysisSummary.resultCount.toString()} /></span>
<span className="vscode-codeql__analysis-item">
<SummaryItemDownloadAndView
<SummaryItemDownload
analysisSummary={analysisSummary}
analysisResults={analysisResults} />
</span>
@@ -249,38 +242,71 @@ const AnalysesResultsTitle = ({ totalAnalysesResults, totalResults }: { totalAna
return <SectionTitle>{totalAnalysesResults}/{totalResults} results</SectionTitle>;
};
const AnalysesResultsDescription = ({ totalAnalysesResults, totalResults }: { totalAnalysesResults: number, totalResults: number }) => {
if (totalAnalysesResults < totalResults) {
return <>
<VerticalSpace size={1} />
Some results haven&apos;t been downloaded automatically because of their size or because enough were downloaded already.
Download them manually from the list above if you want to see them here.
</>;
}
const AnalysesResultsDescription = ({
queryResult,
analysesResults,
}: {
queryResult: RemoteQueryResult
analysesResults: AnalysisResults[],
}) => {
const showDownloadsMessage = queryResult.analysisSummaries.some(
s => !analysesResults.some(a => a.nwo === s.nwo && a.status === 'Completed'));
const downloadsMessage = <>
<VerticalSpace size={1} />
Some results haven&apos;t been downloaded automatically because of their size or because enough were downloaded already.
Download them manually from the list above if you want to see them here.
</>;
return <></>;
const showMaxResultsMessage = analysesResults.some(a => a.rawResults?.capped);
const maxRawResultsMessage = <>
<VerticalSpace size={1} />
Some repositories have more than {MAX_RAW_RESULTS} results. We will only show you up to&nbsp;
{MAX_RAW_RESULTS} results for each repository.
</>;
return (
<>
{showDownloadsMessage && downloadsMessage}
{showMaxResultsMessage && maxRawResultsMessage}
</>
);
};
const RepoAnalysisResults = (analysisResults: AnalysisResults) => {
const numOfResults = getAnalysisResultCount(analysisResults);
const title = <>
{analysisResults.nwo}
<Badge text={analysisResults.results.length.toString()} />
<Badge text={numOfResults.toString()} />
</>;
return (
<CollapsibleItem title={title}>
<ul className="vscode-codeql__flat-list" >
{analysisResults.results.map((r, i) =>
{analysisResults.interpretedResults.map((r, i) =>
<li key={i}>
<AnalysisAlertResult alert={r} />
<VerticalSpace size={2} />
</li>)}
</ul>
{analysisResults.rawResults &&
<RawResultsTable
schema={analysisResults.rawResults.schema}
results={analysisResults.rawResults.resultSet}
fileLinkPrefix={analysisResults.rawResults.fileLinkPrefix} />
}
</CollapsibleItem>
);
};
const AnalysesResults = ({ analysesResults, totalResults }: { analysesResults: AnalysisResults[], totalResults: number }) => {
const AnalysesResults = ({
queryResult,
analysesResults,
totalResults
}: {
queryResult: RemoteQueryResult,
analysesResults: AnalysisResults[],
totalResults: number
}) => {
const totalAnalysesResults = sumAnalysesResults(analysesResults);
if (totalResults === 0) {
@@ -294,10 +320,10 @@ const AnalysesResults = ({ analysesResults, totalResults }: { analysesResults: A
totalAnalysesResults={totalAnalysesResults}
totalResults={totalResults} />
<AnalysesResultsDescription
totalAnalysesResults={totalAnalysesResults}
totalResults={totalResults} />
queryResult={queryResult}
analysesResults={analysesResults} />
<ul className="vscode-codeql__flat-list">
{analysesResults.filter(a => a.results.length > 0).map(r =>
{analysesResults.filter(a => a.interpretedResults.length > 0 || a.rawResults).map(r =>
<li key={r.nwo} className="vscode-codeql__analyses-results-list-item">
<RepoAnalysisResults {...r} />
</li>)}
@@ -331,8 +357,6 @@ export function RemoteQueries(): JSX.Element {
return <div>Waiting for results to load.</div>;
}
const showAnalysesResults = false;
try {
return <div>
<ThemeProvider colorMode="auto">
@@ -340,7 +364,10 @@ export function RemoteQueries(): JSX.Element {
<QueryInfo {...queryResult} />
<Failures {...queryResult} />
<Summary queryResult={queryResult} analysesResults={analysesResults} />
{showAnalysesResults && <AnalysesResults analysesResults={analysesResults} totalResults={queryResult.totalResultCount} />}
<AnalysesResults
queryResult={queryResult}
analysesResults={analysesResults}
totalResults={queryResult.totalResultCount} />
</ThemeProvider>
</div>;
} catch (err) {

View File

@@ -0,0 +1,30 @@
import * as React from 'react';
import styled from 'styled-components';
type Size = 'x-small' | 'small' | 'medium' | 'large' | 'x-large';
const StyledButton = styled.button<{ size: Size }>`
background: none;
color: var(--vscode-textLink-foreground);
border: none;
cursor: pointer;
font-size: ${props => props.size};
`;
const TextButton = ({
size,
onClick,
children
}: {
size: Size,
onClick: () => void,
children: React.ReactNode
}) => (
<StyledButton
size={size}
onClick={onClick}>
{children}
</StyledButton>
);
export default TextButton;

View File

@@ -33,10 +33,6 @@
font-size: x-small;
}
.vscode-codeql__analysis-result-file-link {
vertical-align: middle;
}
.vscode-codeql__analysis-failure {
margin: 0;
font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas,

View File

@@ -0,0 +1,31 @@
const CONTROL_CODE = '\u001F'.codePointAt(0)!;
const CONTROL_LABEL = '\u2400'.codePointAt(0)!;
/**
* Converts the given text so that any non-printable characters are replaced.
* @param label The text to convert.
* @returns The converted text.
*/
export function convertNonPrintableChars(label: string | undefined) {
// If the label was empty, use a placeholder instead, so the link is still clickable.
if (!label) {
return '[empty string]';
} else if (label.match(/^\s+$/)) {
return `[whitespace: "${label}"]`;
} else {
/**
* If the label contains certain non-printable characters, loop through each
* character and replace it with the cooresponding unicode control label.
*/
const convertedLabelArray: any[] = [];
for (let i = 0; i < label.length; i++) {
const labelCheck = label.codePointAt(i)!;
if (labelCheck <= CONTROL_CODE) {
convertedLabelArray[i] = String.fromCodePoint(labelCheck + CONTROL_LABEL);
} else {
convertedLabelArray[i] = label.charAt(i);
}
}
return convertedLabelArray.join('');
}
}

View File

@@ -1,10 +1,10 @@
import * as React from 'react';
import { renderLocation } from './result-table-utils';
import { ColumnValue } from '../pure/bqrs-cli-types';
import { CellValue } from '../pure/bqrs-cli-types';
interface Props {
value: ColumnValue;
value: CellValue;
databaseUri: string;
}

View File

@@ -5,6 +5,7 @@ import { RawResultsSortState, QueryMetadata, SortDirection } from '../pure/inter
import { assertNever } from '../pure/helpers-pure';
import { ResultSet } from '../pure/interface-types';
import { vscode } from './vscode-api';
import { convertNonPrintableChars } from '../text-utils';
export interface ResultTableProps {
resultSet: ResultSet;
@@ -37,9 +38,6 @@ export const oddRowClassName = 'vscode-codeql__result-table-row--odd';
export const pathRowClassName = 'vscode-codeql__result-table-row--path';
export const selectedRowClassName = 'vscode-codeql__result-table-row--selected';
const CONTROL_CODE = '\u001F'.codePointAt(0)!;
const CONTROL_LABEL = '\u2400'.codePointAt(0)!;
export function jumpToLocationHandler(
loc: ResolvableLocationValue,
databaseUri: string,
@@ -70,30 +68,6 @@ export function openFile(filePath: string): void {
});
}
function convertedNonprintableChars(label: string) {
// If the label was empty, use a placeholder instead, so the link is still clickable.
if (!label) {
return '[empty string]';
} else if (label.match(/^\s+$/)) {
return `[whitespace: "${label}"]`;
} else {
/**
* If the label contains certain non-printable characters, loop through each
* character and replace it with the cooresponding unicode control label.
*/
const convertedLabelArray: any[] = [];
for (let i = 0; i < label.length; i++) {
const labelCheck = label.codePointAt(i)!;
if (labelCheck <= CONTROL_CODE) {
convertedLabelArray[i] = String.fromCodePoint(labelCheck + CONTROL_LABEL);
} else {
convertedLabelArray[i] = label.charAt(i);
}
}
return convertedLabelArray.join('');
}
}
/**
* Render a location as a link which when clicked displays the original location.
*/
@@ -105,7 +79,7 @@ export function renderLocation(
callback?: () => void
): JSX.Element {
const displayLabel = convertedNonprintableChars(label!);
const displayLabel = convertNonPrintableChars(label!);
if (loc === undefined) {
return <span>{displayLabel}</span>;

View File

@@ -8,7 +8,7 @@ import { CancellationTokenSource } from 'vscode-jsonrpc';
import * as messages from '../../pure/messages';
import * as qsClient from '../../queryserver-client';
import * as cli from '../../cli';
import { ColumnValue } from '../../pure/bqrs-cli-types';
import { CellValue } from '../../pure/bqrs-cli-types';
import { extensions } from 'vscode';
import { CodeQLExtensionInterface } from '../../extension';
import { fail } from 'assert';
@@ -53,7 +53,7 @@ class Checkpoint<T> {
}
type ResultSets = {
[name: string]: ColumnValue[][];
[name: string]: CellValue[][];
}
type QueryTestCase = {

View File

@@ -279,7 +279,7 @@ describe('Remote queries', function() {
},
library: false,
defaultSuite: [{
description: 'Query suite for remote query'
description: 'Query suite for variant analysis'
}, {
query: queryPath
}]

View File

@@ -44,7 +44,7 @@ const _10MB = _1MB * 10;
// CLI version to test. Hard code the latest as default. And be sure
// to update the env if it is not otherwise set.
const CLI_VERSION = process.env.CLI_VERSION || 'v2.8.2';
const CLI_VERSION = process.env.CLI_VERSION || 'v2.8.3';
process.env.CLI_VERSION = CLI_VERSION;
// Base dir where CLIs will be downloaded into

View File

@@ -176,6 +176,7 @@ describe('Remote queries and query history manager', function() {
let mockCredentials: any;
let mockOctokit: any;
let mockLogger: any;
let mockCliServer: any;
let arm: AnalysesResultsManager;
beforeEach(() => {
@@ -188,10 +189,15 @@ describe('Remote queries and query history manager', function() {
mockLogger = {
log: sandbox.spy()
};
mockCliServer = {
bqrsInfo: sandbox.spy(),
bqrsDecode: sandbox.spy()
};
sandbox.stub(Credentials, 'initialize').resolves(mockCredentials);
arm = new AnalysesResultsManager(
{} as ExtensionContext,
mockCliServer,
path.join(STORAGE_DIR, 'queries'),
mockLogger
);
@@ -211,14 +217,14 @@ describe('Remote queries and query history manager', function() {
expect(publisher.getCall(0).args[0][0]).to.include({
nwo: 'github/vscode-codeql',
status: 'InProgress',
// results: ... avoid checking the results object since it is complex
// interpretedResults: ... avoid checking the interpretedResults object since it is complex
});
// second time, it has the path to the sarif file.
expect(publisher.getCall(1).args[0][0]).to.include({
nwo: 'github/vscode-codeql',
status: 'Completed',
// results: ... avoid checking the results object since it is complex
// interpretedResults: ... avoid checking the interpretedResults object since it is complex
});
expect(publisher).to.have.been.calledTwice;
@@ -226,7 +232,7 @@ describe('Remote queries and query history manager', function() {
expect(arm.getAnalysesResults(rawQueryHistory[0].queryId)[0]).to.include({
nwo: 'github/vscode-codeql',
status: 'Completed',
// results: ... avoid checking the results object since it is complex
// interpretedResults: ... avoid checking the interpretedResults object since it is complex
});
publisher.resetHistory();
@@ -242,7 +248,7 @@ describe('Remote queries and query history manager', function() {
await arm.downloadAnalysesResults(analysisSummaries, undefined, publisher);
const trimmed = publisher.getCalls().map(call => call.args[0]).map(args => {
args.forEach((analysisResult: any) => delete analysisResult.results);
args.forEach((analysisResult: any) => delete analysisResult.interpretedResults);
return args;
});

View File

@@ -4,6 +4,7 @@ import * as chaiAsPromised from 'chai-as-promised';
import * as chai from 'chai';
import * as sarif from 'sarif';
import { extractAnalysisAlerts, tryGetRule, tryGetSeverity } from '../../src/remote-queries/sarif-processing';
import { AnalysisMessage, AnalysisMessageLocationToken } from '../../src/remote-queries/shared/analysis-result';
chai.use(chaiAsPromised);
const expect = chai.expect;
@@ -288,17 +289,19 @@ describe('SARIF processing', () => {
});
describe('tryGetSeverity', () => {
it('should return undefined if no rule found', () => {
it('should return undefined if no rule set', () => {
const result = {
// The rule is missing here.
message: 'msg'
} as sarif.Result;
// The rule should be set here.
const rule: sarif.ReportingDescriptor | undefined = undefined;
const sarifRun = {
results: [result]
} as sarif.Run;
const severity = tryGetSeverity(sarifRun, result);
const severity = tryGetSeverity(sarifRun, result, rule);
expect(severity).to.be.undefined;
});
@@ -310,24 +313,26 @@ describe('SARIF processing', () => {
}
} as sarif.Result;
const rule = {
id: 'A',
properties: {
// Severity not set
}
} as sarif.ReportingDescriptor;
const sarifRun = {
results: [result],
tool: {
driver: {
rules: [
{
id: 'A',
properties: {
// Severity not set
}
},
rule,
result.rule
]
}
}
} as sarif.Run;
const severity = tryGetSeverity(sarifRun, result);
const severity = tryGetSeverity(sarifRun, result, rule);
expect(severity).to.be.undefined;
});
@@ -346,24 +351,26 @@ describe('SARIF processing', () => {
}
} as sarif.Result;
const rule = {
id: 'A',
properties: {
'problem.severity': sarifSeverity
}
} as sarif.ReportingDescriptor;
const sarifRun = {
results: [result],
tool: {
driver: {
rules: [
{
id: 'A',
properties: {
'problem.severity': sarifSeverity
}
},
rule,
result.rule
]
}
}
} as sarif.Run;
const severity = tryGetSeverity(sarifRun, result);
const severity = tryGetSeverity(sarifRun, result, rule);
expect(severity).to.equal(parsedSeverity);
});
});
@@ -371,19 +378,19 @@ describe('SARIF processing', () => {
});
describe('extractAnalysisAlerts', () => {
it('should return an error if no runs found in the SARIF', () => {
const fakefileLinkPrefix = 'https://example.com';
it('should not return any results if no runs found in the SARIF', () => {
const sarif = {
// Runs are missing here.
} as sarif.Log;
const result = extractAnalysisAlerts(sarif);
const result = extractAnalysisAlerts(sarif, fakefileLinkPrefix);
expect(result).to.be.ok;
expect(result.errors.length).to.equal(1);
expect(result.errors[0]).to.equal('No runs found in the SARIF file');
expect(result.alerts.length).to.equal(0);
});
it('should return errors for runs that have no results', () => {
it('should not return any results for runs that have no results', () => {
const sarif = {
runs: [
{
@@ -395,55 +402,53 @@ describe('SARIF processing', () => {
]
} as sarif.Log;
const result = extractAnalysisAlerts(sarif);
const result = extractAnalysisAlerts(sarif, fakefileLinkPrefix);
expect(result).to.be.ok;
expect(result.errors.length).to.equal(1);
expect(result.errors[0]).to.equal('No results found in the SARIF run');
expect(result.alerts.length).to.equal(0);
});
it('should return errors for results that have no message', () => {
const sarif = buildValidSarifLog();
sarif.runs![0]!.results![0]!.message.text = undefined;
const result = extractAnalysisAlerts(sarif);
const result = extractAnalysisAlerts(sarif, fakefileLinkPrefix);
expect(result).to.be.ok;
expect(result.errors.length).to.equal(1);
expect(result.errors[0]).to.equal('No message found in the SARIF result');
expectResultParsingError(result.errors[0]);
});
it('should return errors for result locations with no context region', () => {
const sarif = buildValidSarifLog();
sarif.runs![0]!.results![0]!.locations![0]!.physicalLocation!.contextRegion = undefined;
const result = extractAnalysisAlerts(sarif);
const result = extractAnalysisAlerts(sarif, fakefileLinkPrefix);
expect(result).to.be.ok;
expect(result.errors.length).to.equal(1);
expect(result.errors[0]).to.equal('No context region found in the SARIF result location');
expectResultParsingError(result.errors[0]);
});
it('should return errors for result locations with no region', () => {
it('should not return errors for result locations with no region', () => {
const sarif = buildValidSarifLog();
sarif.runs![0]!.results![0]!.locations![0]!.physicalLocation!.region = undefined;
const result = extractAnalysisAlerts(sarif);
const result = extractAnalysisAlerts(sarif, fakefileLinkPrefix);
expect(result).to.be.ok;
expect(result.errors.length).to.equal(1);
expect(result.errors[0]).to.equal('No region found in the SARIF result location');
expect(result.alerts.length).to.equal(1);
});
it('should return errors for result locations with no physical location', () => {
const sarif = buildValidSarifLog();
sarif.runs![0]!.results![0]!.locations![0]!.physicalLocation!.artifactLocation = undefined;
const result = extractAnalysisAlerts(sarif);
const result = extractAnalysisAlerts(sarif, fakefileLinkPrefix);
expect(result).to.be.ok;
expect(result.errors.length).to.equal(1);
expect(result.errors[0]).to.equal('No file path found in the SARIF result location');
expectResultParsingError(result.errors[0]);
});
it('should return results for all alerts', () => {
@@ -528,18 +533,68 @@ describe('SARIF processing', () => {
]
} as sarif.Log;
const result = extractAnalysisAlerts(sarif);
const result = extractAnalysisAlerts(sarif, fakefileLinkPrefix);
expect(result).to.be.ok;
expect(result.errors.length).to.equal(0);
expect(result.alerts.length).to.equal(3);
expect(result.alerts.find(a => a.message === 'msg1' && a.codeSnippet.text === 'foo')).to.be.ok;
expect(result.alerts.find(a => a.message === 'msg1' && a.codeSnippet.text === 'bar')).to.be.ok;
expect(result.alerts.find(a => a.message === 'msg2' && a.codeSnippet.text === 'baz')).to.be.ok;
expect(result.alerts.find(a => getMessageText(a.message) === 'msg1' && a.codeSnippet.text === 'foo')).to.be.ok;
expect(result.alerts.find(a => getMessageText(a.message) === 'msg1' && a.codeSnippet.text === 'bar')).to.be.ok;
expect(result.alerts.find(a => getMessageText(a.message) === 'msg2' && a.codeSnippet.text === 'baz')).to.be.ok;
expect(result.alerts.every(a => a.severity === 'Warning')).to.be.true;
});
it('should deal with complex messages', () => {
const sarif = buildValidSarifLog();
const messageText = 'This shell command depends on an uncontrolled [absolute path](1).';
sarif.runs![0]!.results![0]!.message!.text = messageText;
sarif.runs![0]!.results![0].relatedLocations = [
{
id: 1,
physicalLocation: {
artifactLocation: {
uri: 'npm-packages/meteor-installer/config.js',
},
region: {
startLine: 35,
startColumn: 20,
endColumn: 60
}
},
}
];
const result = extractAnalysisAlerts(sarif, fakefileLinkPrefix);
expect(result).to.be.ok;
expect(result.errors.length).to.equal(0);
expect(result.alerts.length).to.equal(1);
const message = result.alerts[0].message;
expect(message.tokens.length).to.equal(3);
expect(message.tokens[0].t).to.equal('text');
expect(message.tokens[0].text).to.equal('This shell command depends on an uncontrolled ');
expect(message.tokens[1].t).to.equal('location');
expect(message.tokens[1].text).to.equal('absolute path');
expect((message.tokens[1] as AnalysisMessageLocationToken).location).to.deep.equal({
fileLink: {
fileLinkPrefix: fakefileLinkPrefix,
filePath: 'npm-packages/meteor-installer/config.js',
},
highlightedRegion: {
startLine: 35,
startColumn: 20,
endLine: 35,
endColumn: 60
}
});
expect(message.tokens[2].t).to.equal('text');
expect(message.tokens[2].text).to.equal('.');
});
});
function expectResultParsingError(msg: string) {
expect(msg.startsWith('Error when processing SARIF result')).to.be.true;
}
function buildValidSarifLog(): sarif.Log {
return {
version: '0.0.1' as sarif.Log.version,
@@ -577,4 +632,8 @@ describe('SARIF processing', () => {
]
} as sarif.Log;
}
function getMessageText(message: AnalysisMessage) {
return message.tokens.map(t => t.text).join('');
}
});