Add support for showing code flows (#1187)
This commit is contained in:
52
extensions/ql-vscode/package-lock.json
generated
52
extensions/ql-vscode/package-lock.json
generated
@@ -11,7 +11,7 @@
|
||||
"dependencies": {
|
||||
"@octokit/rest": "^18.5.6",
|
||||
"@primer/octicons-react": "^16.3.0",
|
||||
"@primer/react": "^34.3.0",
|
||||
"@primer/react": "^35.0.0-rc.c106d292",
|
||||
"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-rc.c106d292",
|
||||
"resolved": "https://registry.npmjs.org/@primer/react/-/react-35.0.0-rc.c106d292.tgz",
|
||||
"integrity": "sha512-4CyB2OvwMOt1ZULbOUD6Nz7LnE433OT/h6hJDld/IE4klGAtF1HxDVUG7PfbGmlpw87I79WGjvH2+qx6EIhtZA==",
|
||||
"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-rc.c106d292",
|
||||
"resolved": "https://registry.npmjs.org/@primer/react/-/react-35.0.0-rc.c106d292.tgz",
|
||||
"integrity": "sha512-4CyB2OvwMOt1ZULbOUD6Nz7LnE433OT/h6hJDld/IE4klGAtF1HxDVUG7PfbGmlpw87I79WGjvH2+qx6EIhtZA==",
|
||||
"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",
|
||||
|
||||
@@ -1065,7 +1065,7 @@
|
||||
"dependencies": {
|
||||
"@octokit/rest": "^18.5.6",
|
||||
"@primer/octicons-react": "^16.3.0",
|
||||
"@primer/react": "^34.3.0",
|
||||
"@primer/react": "^35.0.0-rc.c106d292",
|
||||
"child-process-promise": "^2.2.1",
|
||||
"classnames": "~2.2.6",
|
||||
"d3": "^6.3.1",
|
||||
|
||||
@@ -102,6 +102,7 @@ export const sampleRemoteQueryResult: RemoteQueryResult = {
|
||||
const createAnalysisResults = (n: number) => Array(n).fill(
|
||||
{
|
||||
message: 'This shell command depends on an uncontrolled [absolute path](1).',
|
||||
shortDescription: 'Shell command built from environment values',
|
||||
severity: 'Error',
|
||||
filePath: 'npm-packages/meteor-installer/config.js',
|
||||
codeSnippet: {
|
||||
@@ -113,7 +114,202 @@ const createAnalysisResults = (n: number) => Array(n).fill(
|
||||
startLine: 255,
|
||||
startColumn: 28,
|
||||
endColumn: 62
|
||||
}
|
||||
},
|
||||
codeFlows: [
|
||||
{
|
||||
threadFlows: [
|
||||
{
|
||||
filePath: 'npm-packages/meteor-installer/config.js',
|
||||
highlightedRegion: {
|
||||
startLine: 35,
|
||||
startColumn: 20,
|
||||
endColumn: 61
|
||||
},
|
||||
codeSnippet: {
|
||||
startLine: 33,
|
||||
endLine: 37,
|
||||
text: '\nconst meteorLocalFolder = \'.meteor\';\nconst meteorPath = path.resolve(rootPath, meteorLocalFolder);\n\nmodule.exports = {\n'
|
||||
}
|
||||
},
|
||||
{
|
||||
filePath: 'npm-packages/meteor-installer/config.js',
|
||||
highlightedRegion: {
|
||||
startLine: 35,
|
||||
startColumn: 7,
|
||||
endColumn: 61
|
||||
},
|
||||
codeSnippet: {
|
||||
startLine: 33,
|
||||
endLine: 37,
|
||||
text: '\nconst meteorLocalFolder = \'.meteor\';\nconst meteorPath = path.resolve(rootPath, meteorLocalFolder);\n\nmodule.exports = {\n'
|
||||
}
|
||||
},
|
||||
{
|
||||
filePath: 'npm-packages/meteor-installer/config.js',
|
||||
highlightedRegion: {
|
||||
startLine: 40,
|
||||
startColumn: 3,
|
||||
endColumn: 13
|
||||
},
|
||||
codeSnippet: {
|
||||
startLine: 38,
|
||||
endLine: 42,
|
||||
text: ' METEOR_LATEST_VERSION,\n extractPath: rootPath,\n meteorPath,\n release: process.env.INSTALL_METEOR_VERSION || METEOR_LATEST_VERSION,\n rootPath,\n'
|
||||
}
|
||||
},
|
||||
{
|
||||
filePath: 'npm-packages/meteor-installer/install.js',
|
||||
highlightedRegion: {
|
||||
startLine: 12,
|
||||
startColumn: 3,
|
||||
endColumn: 13
|
||||
},
|
||||
codeSnippet: {
|
||||
startLine: 10,
|
||||
endLine: 14,
|
||||
text: 'const os = require(\'os\');\nconst {\n meteorPath,\n release,\n startedPath,\n'
|
||||
}
|
||||
},
|
||||
{
|
||||
filePath: 'npm-packages/meteor-installer/install.js',
|
||||
highlightedRegion: {
|
||||
startLine: 11,
|
||||
startColumn: 7,
|
||||
endLine: 22,
|
||||
endColumn: 27
|
||||
},
|
||||
codeSnippet: {
|
||||
startLine: 9,
|
||||
endLine: 24,
|
||||
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} = require(\'./config.js\');\nconst { uninstall } = require(\'./uninstall\');\nconst {\n'
|
||||
}
|
||||
},
|
||||
{
|
||||
filePath: 'npm-packages/meteor-installer/install.js',
|
||||
highlightedRegion: {
|
||||
startLine: 255,
|
||||
startColumn: 42,
|
||||
endColumn: 52
|
||||
},
|
||||
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'
|
||||
}
|
||||
},
|
||||
{
|
||||
filePath: 'npm-packages/meteor-installer/install.js',
|
||||
highlightedRegion: {
|
||||
startLine: 255,
|
||||
startColumn: 28,
|
||||
endColumn: 62
|
||||
},
|
||||
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'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
threadFlows: [
|
||||
{
|
||||
filePath: 'npm-packages/meteor-installer/config2.js',
|
||||
highlightedRegion: {
|
||||
startLine: 35,
|
||||
startColumn: 20,
|
||||
endColumn: 61
|
||||
},
|
||||
codeSnippet: {
|
||||
startLine: 33,
|
||||
endLine: 37,
|
||||
text: '\nconst meteorLocalFolder = \'.meteor\';\nconst meteorPath = path.resolve(rootPath, meteorLocalFolder);\n\nmodule.exports = {\n'
|
||||
}
|
||||
},
|
||||
{
|
||||
filePath: 'npm-packages/meteor-installer/config2.js',
|
||||
highlightedRegion: {
|
||||
startLine: 35,
|
||||
startColumn: 7,
|
||||
endColumn: 61
|
||||
},
|
||||
codeSnippet: {
|
||||
startLine: 33,
|
||||
endLine: 37,
|
||||
text: '\nconst meteorLocalFolder = \'.meteor\';\nconst meteorPath = path.resolve(rootPath, meteorLocalFolder);\n\nmodule.exports = {\n'
|
||||
}
|
||||
},
|
||||
{
|
||||
filePath: 'npm-packages/meteor-installer/config2.js',
|
||||
highlightedRegion: {
|
||||
startLine: 40,
|
||||
startColumn: 3,
|
||||
endColumn: 13
|
||||
},
|
||||
codeSnippet: {
|
||||
startLine: 38,
|
||||
endLine: 42,
|
||||
text: ' METEOR_LATEST_VERSION,\n extractPath: rootPath,\n meteorPath,\n release: process.env.INSTALL_METEOR_VERSION || METEOR_LATEST_VERSION,\n rootPath,\n'
|
||||
}
|
||||
},
|
||||
{
|
||||
filePath: 'npm-packages/meteor-installer/install2.js',
|
||||
highlightedRegion: {
|
||||
startLine: 12,
|
||||
startColumn: 3,
|
||||
endColumn: 13
|
||||
},
|
||||
codeSnippet: {
|
||||
startLine: 10,
|
||||
endLine: 14,
|
||||
text: 'const os = require(\'os\');\nconst {\n meteorPath,\n release,\n startedPath,\n'
|
||||
}
|
||||
},
|
||||
{
|
||||
filePath: 'npm-packages/meteor-installer/install2.js',
|
||||
highlightedRegion: {
|
||||
startLine: 11,
|
||||
startColumn: 7,
|
||||
endLine: 22,
|
||||
endColumn: 27
|
||||
},
|
||||
codeSnippet: {
|
||||
startLine: 9,
|
||||
endLine: 24,
|
||||
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} = require(\'./config.js\');\nconst { uninstall } = require(\'./uninstall\');\nconst {\n'
|
||||
}
|
||||
},
|
||||
{
|
||||
filePath: 'npm-packages/meteor-installer/install2.js',
|
||||
highlightedRegion: {
|
||||
startLine: 255,
|
||||
startColumn: 42,
|
||||
endColumn: 52
|
||||
},
|
||||
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'
|
||||
}
|
||||
},
|
||||
{
|
||||
filePath: 'npm-packages/meteor-installer/install2.js',
|
||||
highlightedRegion: {
|
||||
startLine: 255,
|
||||
startColumn: 28,
|
||||
endColumn: 62
|
||||
},
|
||||
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'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as sarif from 'sarif';
|
||||
|
||||
import { AnalysisAlert, ResultSeverity } from './shared/analysis-result';
|
||||
import { AnalysisAlert, CodeFlow, CodeSnippet, HighlightedRegion, ResultSeverity, ThreadFlow } from './shared/analysis-result';
|
||||
|
||||
const defaultSeverity = 'Warning';
|
||||
|
||||
@@ -36,77 +36,38 @@ export function extractAnalysisAlerts(
|
||||
|
||||
const severity = tryGetSeverity(run, result) || defaultSeverity;
|
||||
|
||||
const { codeFlows, errors: codeFlowsErrors } = extractCodeFlows(result);
|
||||
if (codeFlowsErrors.length > 0) {
|
||||
errors.push(...codeFlowsErrors);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!result.locations) {
|
||||
errors.push('No locations found in the SARIF result');
|
||||
continue;
|
||||
}
|
||||
|
||||
const rule = tryGetRule(run, result);
|
||||
const shortDescription = rule?.shortDescription?.text || message;
|
||||
|
||||
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 { processedLocation, errors: locationErrors } = extractLocation(location);
|
||||
|
||||
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');
|
||||
if (locationErrors.length > 0) {
|
||||
errors.push(...locationErrors);
|
||||
continue;
|
||||
}
|
||||
|
||||
const analysisAlert = {
|
||||
message,
|
||||
filePath,
|
||||
shortDescription: shortDescription,
|
||||
filePath: processedLocation!.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
|
||||
}
|
||||
codeSnippet: processedLocation!.codeSnippet,
|
||||
highlightedRegion: processedLocation!.highlightedRegion,
|
||||
codeFlows: codeFlows
|
||||
};
|
||||
|
||||
const validationErrors = getAlertValidationErrors(analysisAlert);
|
||||
if (validationErrors.length > 0) {
|
||||
errors.push(...validationErrors);
|
||||
continue;
|
||||
}
|
||||
|
||||
alerts.push(analysisAlert);
|
||||
}
|
||||
}
|
||||
@@ -186,18 +147,149 @@ export function tryGetRule(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getAlertValidationErrors(alert: AnalysisAlert): string[] {
|
||||
const errors = [];
|
||||
interface Location {
|
||||
message?: string;
|
||||
filePath: string;
|
||||
codeSnippet: CodeSnippet,
|
||||
highlightedRegion: HighlightedRegion
|
||||
}
|
||||
|
||||
if (alert.codeSnippet.startLine > alert.codeSnippet.endLine) {
|
||||
errors.push('The code snippet start line is greater than the end line');
|
||||
function validateContextRegion(contextRegion: sarif.Region | undefined): string[] {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (!contextRegion) {
|
||||
errors.push('No context region found in the SARIF result location');
|
||||
return errors;
|
||||
}
|
||||
if (contextRegion.startLine === undefined) {
|
||||
errors.push('No start line set for a result context region');
|
||||
}
|
||||
if (contextRegion.endLine === undefined) {
|
||||
errors.push('No end line set for a result context region');
|
||||
}
|
||||
if (!contextRegion.snippet?.text) {
|
||||
errors.push('No text set for a result context region');
|
||||
}
|
||||
|
||||
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');
|
||||
if (errors.length > 0) {
|
||||
return errors;
|
||||
}
|
||||
|
||||
if (contextRegion.startLine! > contextRegion.endLine!) {
|
||||
errors.push('Start line is greater than the end line in result context region');
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
function validateRegion(region: sarif.Region | undefined): string[] {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (!region) {
|
||||
errors.push('No region found in the SARIF result location');
|
||||
return errors;
|
||||
}
|
||||
if (region.startLine === undefined) {
|
||||
errors.push('No start line set for a result region');
|
||||
}
|
||||
if (region.startColumn === undefined) {
|
||||
errors.push('No start column set for a result region');
|
||||
}
|
||||
if (region.endColumn === undefined) {
|
||||
errors.push('No end column set for a result region');
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
return errors;
|
||||
}
|
||||
|
||||
if (region.endLine! === region.startLine! &&
|
||||
region.endColumn! < region.startColumn!) {
|
||||
errors.push('End column is greater than the start column in a result region');
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
function extractLocation(
|
||||
location: sarif.Location
|
||||
): {
|
||||
processedLocation: Location | undefined,
|
||||
errors: string[]
|
||||
} {
|
||||
const message = location.message?.text;
|
||||
|
||||
const errors = [];
|
||||
|
||||
const contextRegion = location.physicalLocation?.contextRegion;
|
||||
const contextRegionErrors = validateContextRegion(contextRegion);
|
||||
errors.push(...contextRegionErrors);
|
||||
|
||||
const region = location.physicalLocation?.region;
|
||||
const regionErrors = validateRegion(region);
|
||||
errors.push(...regionErrors);
|
||||
|
||||
const filePath = location.physicalLocation?.artifactLocation?.uri;
|
||||
if (!filePath) {
|
||||
errors.push('No file path found in the SARIF result location');
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
return { processedLocation: undefined, errors };
|
||||
}
|
||||
|
||||
const processedLocation = {
|
||||
message,
|
||||
filePath,
|
||||
codeSnippet: {
|
||||
startLine: contextRegion!.startLine,
|
||||
endLine: contextRegion!.endLine,
|
||||
text: contextRegion!.snippet!.text
|
||||
},
|
||||
highlightedRegion: {
|
||||
startLine: region!.startLine,
|
||||
startColumn: region!.startColumn,
|
||||
endLine: region!.endLine,
|
||||
endColumn: region!.endColumn
|
||||
}
|
||||
} as Location;
|
||||
|
||||
return { processedLocation, errors: [] };
|
||||
}
|
||||
|
||||
function extractCodeFlows(
|
||||
result: sarif.Result
|
||||
): {
|
||||
codeFlows: CodeFlow[],
|
||||
errors: string[]
|
||||
} {
|
||||
const codeFlows = [];
|
||||
const errors = [];
|
||||
|
||||
if (result.codeFlows) {
|
||||
for (const codeFlow of result.codeFlows) {
|
||||
const threadFlows = [];
|
||||
|
||||
for (const threadFlow of codeFlow.threadFlows) {
|
||||
for (const location of threadFlow.locations) {
|
||||
const { processedLocation, errors: locationErrors } = extractLocation(location);
|
||||
if (locationErrors.length > 0) {
|
||||
errors.push(...locationErrors);
|
||||
continue;
|
||||
}
|
||||
|
||||
threadFlows.push({
|
||||
filePath: processedLocation!.filePath,
|
||||
codeSnippet: processedLocation!.codeSnippet,
|
||||
highlightedRegion: processedLocation!.highlightedRegion,
|
||||
message: processedLocation!.message
|
||||
} as ThreadFlow);
|
||||
}
|
||||
}
|
||||
|
||||
codeFlows.push({ threadFlows } as CodeFlow);
|
||||
}
|
||||
}
|
||||
|
||||
return { errors, codeFlows: [] };
|
||||
}
|
||||
|
||||
@@ -8,10 +8,12 @@ export interface AnalysisResults {
|
||||
|
||||
export interface AnalysisAlert {
|
||||
message: string;
|
||||
shortDescription: string;
|
||||
severity: ResultSeverity;
|
||||
filePath: string;
|
||||
codeSnippet: CodeSnippet
|
||||
highlightedRegion: HighlightedRegion
|
||||
codeSnippet: CodeSnippet;
|
||||
highlightedRegion: HighlightedRegion;
|
||||
codeFlows: CodeFlow[];
|
||||
}
|
||||
|
||||
export interface CodeSnippet {
|
||||
@@ -27,4 +29,15 @@ export interface HighlightedRegion {
|
||||
endColumn: number;
|
||||
}
|
||||
|
||||
export interface CodeFlow {
|
||||
threadFlows: ThreadFlow[];
|
||||
}
|
||||
|
||||
export interface ThreadFlow {
|
||||
filePath: string;
|
||||
codeSnippet: CodeSnippet;
|
||||
highlightedRegion: HighlightedRegion;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export type ResultSeverity = 'Recommendation' | 'Warning' | 'Error';
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
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}
|
||||
@@ -10,6 +12,14 @@ const AnalysisAlertResult = ({ alert }: { alert: AnalysisAlert }) => {
|
||||
highlightedRegion={alert.highlightedRegion}
|
||||
severity={alert.severity}
|
||||
message={alert.message}
|
||||
messageChildren={
|
||||
showPathsLink && <CodePaths
|
||||
codeFlows={alert.codeFlows}
|
||||
ruleDescription={alert.shortDescription}
|
||||
severity={alert.severity}
|
||||
message={alert.message}
|
||||
/>
|
||||
}
|
||||
/>;
|
||||
};
|
||||
|
||||
|
||||
180
extensions/ql-vscode/src/remote-queries/view/CodePaths.tsx
Normal file
180
extensions/ql-vscode/src/remote-queries/view/CodePaths.tsx
Normal 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, 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: string;
|
||||
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
|
||||
filePath={threadFlow.filePath}
|
||||
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].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: string,
|
||||
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;
|
||||
Reference in New Issue
Block a user