Deal with analysis messages that have links to locations (#1195)
This commit is contained in:
@@ -127,35 +127,49 @@ export function parseSarifLocation(
|
|||||||
userVisibleFile
|
userVisibleFile
|
||||||
} as ParsedSarifLocation;
|
} as ParsedSarifLocation;
|
||||||
} else {
|
} else {
|
||||||
const region = physicalLocation.region;
|
const region = parseSarifRegion(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;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
uri: effectiveLocation,
|
uri: effectiveLocation,
|
||||||
userVisibleFile,
|
userVisibleFile,
|
||||||
startLine,
|
...region
|
||||||
startColumn,
|
|
||||||
endLine,
|
|
||||||
endColumn,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
export function isNoLocation(loc: ParsedSarifLocation): loc is NoLocation {
|
||||||
return 'hint' in loc;
|
return 'hint' in loc;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,7 +101,35 @@ export const sampleRemoteQueryResult: RemoteQueryResult = {
|
|||||||
|
|
||||||
const createAnalysisInterpretedResults = (n: number) => Array(n).fill(
|
const createAnalysisInterpretedResults = (n: number) => Array(n).fill(
|
||||||
{
|
{
|
||||||
message: 'This shell command depends on an uncontrolled [absolute path](1).',
|
message: {
|
||||||
|
tokens: [
|
||||||
|
{
|
||||||
|
t: 'text',
|
||||||
|
text: 'This shell command depends on an uncontrolled '
|
||||||
|
},
|
||||||
|
{
|
||||||
|
t: 'location',
|
||||||
|
text: 'absolute path',
|
||||||
|
location: {
|
||||||
|
filePath: 'npm-packages/meteor-installer/config.js',
|
||||||
|
codeSnippet: {
|
||||||
|
startLine: 33,
|
||||||
|
endLine: 37,
|
||||||
|
text: '\nconst meteorLocalFolder = \'.meteor\';\nconst meteorPath = path.resolve(rootPath, meteorLocalFolder);\n\nmodule.exports = {\n'
|
||||||
|
},
|
||||||
|
highlightedRegion: {
|
||||||
|
startLine: 35,
|
||||||
|
startColumn: 20,
|
||||||
|
endColumn: 61
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
t: 'text',
|
||||||
|
text: '.'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
shortDescription: 'Shell command built from environment values',
|
shortDescription: 'Shell command built from environment values',
|
||||||
severity: 'Error',
|
severity: 'Error',
|
||||||
filePath: 'npm-packages/meteor-installer/config.js',
|
filePath: 'npm-packages/meteor-installer/config.js',
|
||||||
|
|||||||
@@ -1,6 +1,16 @@
|
|||||||
import * as sarif from 'sarif';
|
import * as sarif from 'sarif';
|
||||||
|
import { parseSarifPlainTextMessage, parseSarifRegion } from '../pure/sarif-utils';
|
||||||
|
|
||||||
import { AnalysisAlert, CodeFlow, CodeSnippet, HighlightedRegion, ResultSeverity, ThreadFlow } from './shared/analysis-result';
|
import {
|
||||||
|
AnalysisAlert,
|
||||||
|
CodeFlow,
|
||||||
|
AnalysisMessage,
|
||||||
|
AnalysisMessageToken,
|
||||||
|
ResultSeverity,
|
||||||
|
ThreadFlow,
|
||||||
|
CodeSnippet,
|
||||||
|
HighlightedRegion
|
||||||
|
} from './shared/analysis-result';
|
||||||
|
|
||||||
const defaultSeverity = 'Warning';
|
const defaultSeverity = 'Warning';
|
||||||
|
|
||||||
@@ -10,82 +20,76 @@ export function extractAnalysisAlerts(
|
|||||||
alerts: AnalysisAlert[],
|
alerts: AnalysisAlert[],
|
||||||
errors: string[]
|
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 alerts: AnalysisAlert[] = [];
|
||||||
|
const errors: string[] = [];
|
||||||
|
|
||||||
for (const run of sarifLog.runs) {
|
for (const run of sarifLog.runs ?? []) {
|
||||||
if (!run.results) {
|
for (const result of run.results ?? []) {
|
||||||
errors.push('No results found in the SARIF run');
|
try {
|
||||||
continue;
|
alerts.push(...extractResultAlerts(run, result));
|
||||||
}
|
} catch (e) {
|
||||||
|
errors.push(`Error when processing SARIF result: ${e}`);
|
||||||
for (const result of run.results) {
|
|
||||||
const message = result.message?.text;
|
|
||||||
if (!message) {
|
|
||||||
errors.push('No message found in the SARIF result');
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
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 { processedLocation, errors: locationErrors } = extractLocation(location);
|
|
||||||
|
|
||||||
if (locationErrors.length > 0) {
|
|
||||||
errors.push(...locationErrors);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const analysisAlert = {
|
|
||||||
message,
|
|
||||||
shortDescription: shortDescription,
|
|
||||||
filePath: processedLocation!.filePath,
|
|
||||||
severity,
|
|
||||||
codeSnippet: processedLocation!.codeSnippet,
|
|
||||||
highlightedRegion: processedLocation!.highlightedRegion,
|
|
||||||
codeFlows: codeFlows
|
|
||||||
};
|
|
||||||
|
|
||||||
alerts.push(analysisAlert);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { alerts, errors };
|
return { alerts, errors };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function tryGetSeverity(
|
function extractResultAlerts(
|
||||||
sarifRun: sarif.Run,
|
run: sarif.Run,
|
||||||
result: sarif.Result
|
result: sarif.Result
|
||||||
): ResultSeverity | undefined {
|
): AnalysisAlert[] {
|
||||||
if (!sarifRun || !result) {
|
const alerts: AnalysisAlert[] = [];
|
||||||
return undefined;
|
|
||||||
|
const message = getMessage(result);
|
||||||
|
const rule = tryGetRule(run, result);
|
||||||
|
const severity = tryGetSeverity(run, result, rule) || defaultSeverity;
|
||||||
|
const codeFlows = getCodeFlows(result);
|
||||||
|
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,
|
||||||
|
filePath,
|
||||||
|
severity,
|
||||||
|
codeSnippet,
|
||||||
|
highlightedRegion,
|
||||||
|
codeFlows: codeFlows
|
||||||
|
};
|
||||||
|
|
||||||
|
alerts.push(analysisAlert);
|
||||||
}
|
}
|
||||||
|
|
||||||
const rule = tryGetRule(sarifRun, result);
|
return alerts;
|
||||||
if (!rule) {
|
}
|
||||||
|
|
||||||
|
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;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,142 +151,50 @@ export function tryGetRule(
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Location {
|
function getCodeSnippet(region: sarif.Region): CodeSnippet {
|
||||||
message?: string;
|
const text = region.snippet!.text!;
|
||||||
filePath: string;
|
const { startLine, endLine } = parseSarifRegion(region);
|
||||||
codeSnippet: CodeSnippet,
|
|
||||||
highlightedRegion: HighlightedRegion
|
return {
|
||||||
|
startLine,
|
||||||
|
endLine,
|
||||||
|
text
|
||||||
|
} as CodeSnippet;
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateContextRegion(contextRegion: sarif.Region | undefined): string[] {
|
function getHighlightedRegion(region: sarif.Region): HighlightedRegion {
|
||||||
const errors: string[] = [];
|
const { startLine, startColumn, endLine, endColumn } = parseSarifRegion(region);
|
||||||
|
|
||||||
if (!contextRegion) {
|
return {
|
||||||
errors.push('No context region found in the SARIF result location');
|
startLine,
|
||||||
return errors;
|
startColumn,
|
||||||
}
|
endLine,
|
||||||
if (contextRegion.startLine === undefined) {
|
endColumn
|
||||||
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');
|
|
||||||
}
|
|
||||||
|
|
||||||
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[] {
|
function getCodeFlows(
|
||||||
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
|
result: sarif.Result
|
||||||
): {
|
): CodeFlow[] {
|
||||||
codeFlows: CodeFlow[],
|
|
||||||
errors: string[]
|
|
||||||
} {
|
|
||||||
const codeFlows = [];
|
const codeFlows = [];
|
||||||
const errors = [];
|
|
||||||
|
|
||||||
if (result.codeFlows) {
|
if (result.codeFlows) {
|
||||||
for (const codeFlow of result.codeFlows) {
|
for (const codeFlow of result.codeFlows) {
|
||||||
const threadFlows = [];
|
const threadFlows = [];
|
||||||
|
|
||||||
for (const threadFlow of codeFlow.threadFlows) {
|
for (const threadFlow of codeFlow.threadFlows) {
|
||||||
for (const location of threadFlow.locations) {
|
for (const threadFlowLocation of threadFlow.locations) {
|
||||||
const { processedLocation, errors: locationErrors } = extractLocation(location);
|
const physicalLocation = threadFlowLocation!.location!.physicalLocation!;
|
||||||
if (locationErrors.length > 0) {
|
const filePath = physicalLocation!.artifactLocation!.uri!;
|
||||||
errors.push(...locationErrors);
|
const codeSnippet = getCodeSnippet(physicalLocation.contextRegion!);
|
||||||
continue;
|
const highlightedRegion = physicalLocation.region
|
||||||
}
|
? getHighlightedRegion(physicalLocation.region)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
threadFlows.push({
|
threadFlows.push({
|
||||||
filePath: processedLocation!.filePath,
|
filePath,
|
||||||
codeSnippet: processedLocation!.codeSnippet,
|
codeSnippet,
|
||||||
highlightedRegion: processedLocation!.highlightedRegion,
|
highlightedRegion
|
||||||
message: processedLocation!.message
|
|
||||||
} as ThreadFlow);
|
} as ThreadFlow);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -291,5 +203,30 @@ function extractCodeFlows(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { errors, codeFlows: [] };
|
return codeFlows;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMessage(result: sarif.Result): 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: {
|
||||||
|
filePath: relatedLocation!.physicalLocation!.artifactLocation!.uri!,
|
||||||
|
highlightedRegion: getHighlightedRegion(relatedLocation!.physicalLocation!.region!),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { tokens };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ export interface AnalysisResults {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface AnalysisAlert {
|
export interface AnalysisAlert {
|
||||||
message: string;
|
message: AnalysisMessage;
|
||||||
shortDescription: string;
|
shortDescription: string;
|
||||||
severity: ResultSeverity;
|
severity: ResultSeverity;
|
||||||
filePath: string;
|
filePath: string;
|
||||||
codeSnippet: CodeSnippet;
|
codeSnippet: CodeSnippet;
|
||||||
highlightedRegion: HighlightedRegion;
|
highlightedRegion?: HighlightedRegion;
|
||||||
codeFlows: CodeFlow[];
|
codeFlows: CodeFlow[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ export interface CodeSnippet {
|
|||||||
export interface HighlightedRegion {
|
export interface HighlightedRegion {
|
||||||
startLine: number;
|
startLine: number;
|
||||||
startColumn: number;
|
startColumn: number;
|
||||||
endLine: number | undefined;
|
endLine: number;
|
||||||
endColumn: number;
|
endColumn: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,8 +36,30 @@ export interface CodeFlow {
|
|||||||
export interface ThreadFlow {
|
export interface ThreadFlow {
|
||||||
filePath: string;
|
filePath: string;
|
||||||
codeSnippet: CodeSnippet;
|
codeSnippet: CodeSnippet;
|
||||||
highlightedRegion: HighlightedRegion;
|
highlightedRegion?: HighlightedRegion;
|
||||||
message?: string;
|
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: {
|
||||||
|
filePath: string;
|
||||||
|
highlightedRegion?: HighlightedRegion;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ResultSeverity = 'Recommendation' | 'Warning' | 'Error';
|
export type ResultSeverity = 'Recommendation' | 'Warning' | 'Error';
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { ActionList, ActionMenu, Box, Button, Label, Link, Overlay } from '@prim
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { CodeFlow, ResultSeverity } from '../shared/analysis-result';
|
import { CodeFlow, AnalysisMessage, ResultSeverity } from '../shared/analysis-result';
|
||||||
import FileCodeSnippet from './FileCodeSnippet';
|
import FileCodeSnippet from './FileCodeSnippet';
|
||||||
import SectionTitle from './SectionTitle';
|
import SectionTitle from './SectionTitle';
|
||||||
import VerticalSpace from './VerticalSpace';
|
import VerticalSpace from './VerticalSpace';
|
||||||
@@ -45,7 +45,7 @@ const CodePath = ({
|
|||||||
severity
|
severity
|
||||||
}: {
|
}: {
|
||||||
codeFlow: CodeFlow;
|
codeFlow: CodeFlow;
|
||||||
message: string;
|
message: AnalysisMessage;
|
||||||
severity: ResultSeverity;
|
severity: ResultSeverity;
|
||||||
}) => {
|
}) => {
|
||||||
return <>
|
return <>
|
||||||
@@ -122,7 +122,7 @@ const CodePaths = ({
|
|||||||
}: {
|
}: {
|
||||||
codeFlows: CodeFlow[],
|
codeFlows: CodeFlow[],
|
||||||
ruleDescription: string,
|
ruleDescription: string,
|
||||||
message: string,
|
message: AnalysisMessage,
|
||||||
severity: ResultSeverity
|
severity: ResultSeverity
|
||||||
}) => {
|
}) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { CodeSnippet, HighlightedRegion, ResultSeverity } from '../shared/analysis-result';
|
import { CodeSnippet, HighlightedRegion, AnalysisMessage, ResultSeverity } from '../shared/analysis-result';
|
||||||
import { Box, Link } from '@primer/react';
|
import { Box, Link } from '@primer/react';
|
||||||
import VerticalSpace from './VerticalSpace';
|
import VerticalSpace from './VerticalSpace';
|
||||||
|
|
||||||
@@ -75,19 +75,19 @@ const HighlightedLine = ({ text }: { text: string }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const Message = ({
|
const Message = ({
|
||||||
messageText,
|
message,
|
||||||
currentLineNumber,
|
currentLineNumber,
|
||||||
highlightedRegion,
|
highlightedRegion,
|
||||||
borderColor,
|
borderColor,
|
||||||
children
|
children
|
||||||
}: {
|
}: {
|
||||||
messageText: string,
|
message: AnalysisMessage,
|
||||||
currentLineNumber: number,
|
currentLineNumber: number,
|
||||||
highlightedRegion: HighlightedRegion,
|
highlightedRegion?: HighlightedRegion,
|
||||||
borderColor: string,
|
borderColor: string,
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}) => {
|
}) => {
|
||||||
if (highlightedRegion.startLine !== currentLineNumber) {
|
if (!highlightedRegion || highlightedRegion.startLine !== currentLineNumber) {
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,7 +101,16 @@ const Message = ({
|
|||||||
paddingTop="1em"
|
paddingTop="1em"
|
||||||
paddingBottom="1em">
|
paddingBottom="1em">
|
||||||
<MessageText>
|
<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='TODO'>{token.text}</Link>;
|
||||||
|
default:
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
})}
|
||||||
{children && <>
|
{children && <>
|
||||||
<VerticalSpace size={2} />
|
<VerticalSpace size={2} />
|
||||||
{children}
|
{children}
|
||||||
@@ -120,9 +129,9 @@ const CodeLine = ({
|
|||||||
}: {
|
}: {
|
||||||
line: string,
|
line: string,
|
||||||
lineNumber: number,
|
lineNumber: number,
|
||||||
highlightedRegion: HighlightedRegion
|
highlightedRegion?: HighlightedRegion
|
||||||
}) => {
|
}) => {
|
||||||
if (!shouldHighlightLine(lineNumber, highlightedRegion)) {
|
if (!highlightedRegion || !shouldHighlightLine(lineNumber, highlightedRegion)) {
|
||||||
return <PlainLine text={line} />;
|
return <PlainLine text={line} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,9 +174,9 @@ const FileCodeSnippet = ({
|
|||||||
}: {
|
}: {
|
||||||
filePath: string,
|
filePath: string,
|
||||||
codeSnippet: CodeSnippet,
|
codeSnippet: CodeSnippet,
|
||||||
highlightedRegion: HighlightedRegion,
|
highlightedRegion?: HighlightedRegion,
|
||||||
severity?: ResultSeverity,
|
severity?: ResultSeverity,
|
||||||
message?: string,
|
message?: AnalysisMessage,
|
||||||
messageChildren?: React.ReactNode,
|
messageChildren?: React.ReactNode,
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
@@ -184,7 +193,7 @@ const FileCodeSnippet = ({
|
|||||||
{code.map((line, index) => (
|
{code.map((line, index) => (
|
||||||
<div key={index}>
|
<div key={index}>
|
||||||
{message && severity && <Message
|
{message && severity && <Message
|
||||||
messageText={message}
|
message={message}
|
||||||
currentLineNumber={startingLine + index}
|
currentLineNumber={startingLine + index}
|
||||||
highlightedRegion={highlightedRegion}
|
highlightedRegion={highlightedRegion}
|
||||||
borderColor={getSeverityColor(severity)}>
|
borderColor={getSeverityColor(severity)}>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import * as chaiAsPromised from 'chai-as-promised';
|
|||||||
import * as chai from 'chai';
|
import * as chai from 'chai';
|
||||||
import * as sarif from 'sarif';
|
import * as sarif from 'sarif';
|
||||||
import { extractAnalysisAlerts, tryGetRule, tryGetSeverity } from '../../src/remote-queries/sarif-processing';
|
import { extractAnalysisAlerts, tryGetRule, tryGetSeverity } from '../../src/remote-queries/sarif-processing';
|
||||||
|
import { AnalysisMessage, AnalysisMessageLocationToken } from '../../src/remote-queries/shared/analysis-result';
|
||||||
|
|
||||||
chai.use(chaiAsPromised);
|
chai.use(chaiAsPromised);
|
||||||
const expect = chai.expect;
|
const expect = chai.expect;
|
||||||
@@ -288,17 +289,19 @@ describe('SARIF processing', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('tryGetSeverity', () => {
|
describe('tryGetSeverity', () => {
|
||||||
it('should return undefined if no rule found', () => {
|
it('should return undefined if no rule set', () => {
|
||||||
const result = {
|
const result = {
|
||||||
// The rule is missing here.
|
|
||||||
message: 'msg'
|
message: 'msg'
|
||||||
} as sarif.Result;
|
} as sarif.Result;
|
||||||
|
|
||||||
|
// The rule should be set here.
|
||||||
|
const rule: sarif.ReportingDescriptor | undefined = undefined;
|
||||||
|
|
||||||
const sarifRun = {
|
const sarifRun = {
|
||||||
results: [result]
|
results: [result]
|
||||||
} as sarif.Run;
|
} as sarif.Run;
|
||||||
|
|
||||||
const severity = tryGetSeverity(sarifRun, result);
|
const severity = tryGetSeverity(sarifRun, result, rule);
|
||||||
expect(severity).to.be.undefined;
|
expect(severity).to.be.undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -310,24 +313,26 @@ describe('SARIF processing', () => {
|
|||||||
}
|
}
|
||||||
} as sarif.Result;
|
} as sarif.Result;
|
||||||
|
|
||||||
|
const rule = {
|
||||||
|
id: 'A',
|
||||||
|
properties: {
|
||||||
|
// Severity not set
|
||||||
|
}
|
||||||
|
} as sarif.ReportingDescriptor;
|
||||||
|
|
||||||
const sarifRun = {
|
const sarifRun = {
|
||||||
results: [result],
|
results: [result],
|
||||||
tool: {
|
tool: {
|
||||||
driver: {
|
driver: {
|
||||||
rules: [
|
rules: [
|
||||||
{
|
rule,
|
||||||
id: 'A',
|
|
||||||
properties: {
|
|
||||||
// Severity not set
|
|
||||||
}
|
|
||||||
},
|
|
||||||
result.rule
|
result.rule
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} as sarif.Run;
|
} as sarif.Run;
|
||||||
|
|
||||||
const severity = tryGetSeverity(sarifRun, result);
|
const severity = tryGetSeverity(sarifRun, result, rule);
|
||||||
expect(severity).to.be.undefined;
|
expect(severity).to.be.undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -346,24 +351,26 @@ describe('SARIF processing', () => {
|
|||||||
}
|
}
|
||||||
} as sarif.Result;
|
} as sarif.Result;
|
||||||
|
|
||||||
|
const rule = {
|
||||||
|
id: 'A',
|
||||||
|
properties: {
|
||||||
|
'problem.severity': sarifSeverity
|
||||||
|
}
|
||||||
|
} as sarif.ReportingDescriptor;
|
||||||
|
|
||||||
const sarifRun = {
|
const sarifRun = {
|
||||||
results: [result],
|
results: [result],
|
||||||
tool: {
|
tool: {
|
||||||
driver: {
|
driver: {
|
||||||
rules: [
|
rules: [
|
||||||
{
|
rule,
|
||||||
id: 'A',
|
|
||||||
properties: {
|
|
||||||
'problem.severity': sarifSeverity
|
|
||||||
}
|
|
||||||
},
|
|
||||||
result.rule
|
result.rule
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} as sarif.Run;
|
} as sarif.Run;
|
||||||
|
|
||||||
const severity = tryGetSeverity(sarifRun, result);
|
const severity = tryGetSeverity(sarifRun, result, rule);
|
||||||
expect(severity).to.equal(parsedSeverity);
|
expect(severity).to.equal(parsedSeverity);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -371,7 +378,7 @@ describe('SARIF processing', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('extractAnalysisAlerts', () => {
|
describe('extractAnalysisAlerts', () => {
|
||||||
it('should return an error if no runs found in the SARIF', () => {
|
it('should not return any results if no runs found in the SARIF', () => {
|
||||||
const sarif = {
|
const sarif = {
|
||||||
// Runs are missing here.
|
// Runs are missing here.
|
||||||
} as sarif.Log;
|
} as sarif.Log;
|
||||||
@@ -379,11 +386,10 @@ describe('SARIF processing', () => {
|
|||||||
const result = extractAnalysisAlerts(sarif);
|
const result = extractAnalysisAlerts(sarif);
|
||||||
|
|
||||||
expect(result).to.be.ok;
|
expect(result).to.be.ok;
|
||||||
expect(result.errors.length).to.equal(1);
|
expect(result.alerts.length).to.equal(0);
|
||||||
expect(result.errors[0]).to.equal('No runs found in the SARIF file');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return errors for runs that have no results', () => {
|
it('should not return any results for runs that have no results', () => {
|
||||||
const sarif = {
|
const sarif = {
|
||||||
runs: [
|
runs: [
|
||||||
{
|
{
|
||||||
@@ -398,8 +404,7 @@ describe('SARIF processing', () => {
|
|||||||
const result = extractAnalysisAlerts(sarif);
|
const result = extractAnalysisAlerts(sarif);
|
||||||
|
|
||||||
expect(result).to.be.ok;
|
expect(result).to.be.ok;
|
||||||
expect(result.errors.length).to.equal(1);
|
expect(result.alerts.length).to.equal(0);
|
||||||
expect(result.errors[0]).to.equal('No results found in the SARIF run');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return errors for results that have no message', () => {
|
it('should return errors for results that have no message', () => {
|
||||||
@@ -410,7 +415,7 @@ describe('SARIF processing', () => {
|
|||||||
|
|
||||||
expect(result).to.be.ok;
|
expect(result).to.be.ok;
|
||||||
expect(result.errors.length).to.equal(1);
|
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', () => {
|
it('should return errors for result locations with no context region', () => {
|
||||||
@@ -421,18 +426,17 @@ describe('SARIF processing', () => {
|
|||||||
|
|
||||||
expect(result).to.be.ok;
|
expect(result).to.be.ok;
|
||||||
expect(result.errors.length).to.equal(1);
|
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();
|
const sarif = buildValidSarifLog();
|
||||||
sarif.runs![0]!.results![0]!.locations![0]!.physicalLocation!.region = undefined;
|
sarif.runs![0]!.results![0]!.locations![0]!.physicalLocation!.region = undefined;
|
||||||
|
|
||||||
const result = extractAnalysisAlerts(sarif);
|
const result = extractAnalysisAlerts(sarif);
|
||||||
|
|
||||||
expect(result).to.be.ok;
|
expect(result).to.be.ok;
|
||||||
expect(result.errors.length).to.equal(1);
|
expect(result.alerts.length).to.equal(1);
|
||||||
expect(result.errors[0]).to.equal('No region found in the SARIF result location');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return errors for result locations with no physical location', () => {
|
it('should return errors for result locations with no physical location', () => {
|
||||||
@@ -443,7 +447,7 @@ describe('SARIF processing', () => {
|
|||||||
|
|
||||||
expect(result).to.be.ok;
|
expect(result).to.be.ok;
|
||||||
expect(result.errors.length).to.equal(1);
|
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', () => {
|
it('should return results for all alerts', () => {
|
||||||
@@ -532,14 +536,61 @@ describe('SARIF processing', () => {
|
|||||||
expect(result).to.be.ok;
|
expect(result).to.be.ok;
|
||||||
expect(result.errors.length).to.equal(0);
|
expect(result.errors.length).to.equal(0);
|
||||||
expect(result.alerts.length).to.equal(3);
|
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 => getMessageText(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 => getMessageText(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) === 'msg2' && a.codeSnippet.text === 'baz')).to.be.ok;
|
||||||
expect(result.alerts.every(a => a.severity === 'Warning')).to.be.true;
|
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);
|
||||||
|
|
||||||
|
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({
|
||||||
|
filePath: 'npm-packages/meteor-installer/config.js',
|
||||||
|
highlightedRegion: {
|
||||||
|
startLine: 35,
|
||||||
|
startColumn: 20,
|
||||||
|
endLine: 35,
|
||||||
|
endColumn: 59
|
||||||
|
}
|
||||||
|
});
|
||||||
|
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 {
|
function buildValidSarifLog(): sarif.Log {
|
||||||
return {
|
return {
|
||||||
version: '0.0.1' as sarif.Log.version,
|
version: '0.0.1' as sarif.Log.version,
|
||||||
@@ -577,4 +628,8 @@ describe('SARIF processing', () => {
|
|||||||
]
|
]
|
||||||
} as sarif.Log;
|
} as sarif.Log;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getMessageText(message: AnalysisMessage) {
|
||||||
|
return message.tokens.map(t => t.text).join('');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user