Deal with analysis messages that have links to locations (#1195)

This commit is contained in:
Charis Kyriakou
2022-03-14 08:14:09 +00:00
committed by GitHub
parent 50d495b522
commit ed61eb0a95
7 changed files with 324 additions and 259 deletions

View File

@@ -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;
} }

View File

@@ -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',

View File

@@ -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 };
} }

View File

@@ -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';

View File

@@ -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);

View File

@@ -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)}>

View File

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