diff --git a/extensions/ql-vscode/src/pure/sarif-utils.ts b/extensions/ql-vscode/src/pure/sarif-utils.ts index e4b651ea2..5159dc1cb 100644 --- a/extensions/ql-vscode/src/pure/sarif-utils.ts +++ b/extensions/ql-vscode/src/pure/sarif-utils.ts @@ -127,35 +127,49 @@ export function parseSarifLocation( userVisibleFile } as ParsedSarifLocation; } else { - const region = physicalLocation.region; - // We assume that the SARIF we're given always has startLine - // This is not mandated by the SARIF spec, but should be true of - // SARIF output by our own tools. - const startLine = region.startLine!; - - // These defaults are from SARIF 2.1.0 spec, section 3.30.2, "Text Regions" - // https://docs.oasis-open.org/sarif/sarif/v2.1.0/cs01/sarif-v2.1.0-cs01.html#_Ref493492556 - const endLine = region.endLine === undefined ? startLine : region.endLine; - const startColumn = region.startColumn === undefined ? 1 : region.startColumn; - - // We also assume that our tools will always supply `endColumn` field, which is - // fortunate, since the SARIF spec says that it defaults to the end of the line, whose - // length we don't know at this point in the code. - // - // It is off by one with respect to the way vscode counts columns in selections. - const endColumn = region.endColumn! - 1; + const region = parseSarifRegion(physicalLocation.region); return { uri: effectiveLocation, userVisibleFile, - startLine, - startColumn, - endLine, - endColumn, + ...region }; } } +export function parseSarifRegion( + region: Sarif.Region +): { + startLine: number, + endLine: number, + startColumn: number, + endColumn: number +} { + // The SARIF we're given should have a startLine, but we + // fall back to 1, just in case something has gone wrong. + const startLine = region.startLine ?? 1; + + // These defaults are from SARIF 2.1.0 spec, section 3.30.2, "Text Regions" + // https://docs.oasis-open.org/sarif/sarif/v2.1.0/cs01/sarif-v2.1.0-cs01.html#_Ref493492556 + const endLine = region.endLine === undefined ? startLine : region.endLine; + const startColumn = region.startColumn === undefined ? 1 : region.startColumn; + + // Our tools should always supply `endColumn` field, which is fortunate, since + // the SARIF spec says that it defaults to the end of the line, whose + // length we don't know at this point in the code. We fall back to 1, + // just in case something has gone wrong. + // + // It is off by one with respect to the way vscode counts columns in selections. + const endColumn = (region.endColumn ?? 1) - 1; + + return { + startLine, + startColumn, + endLine, + endColumn + }; +} + export function isNoLocation(loc: ParsedSarifLocation): loc is NoLocation { return 'hint' in loc; } diff --git a/extensions/ql-vscode/src/remote-queries/sample-data.ts b/extensions/ql-vscode/src/remote-queries/sample-data.ts index f0f85b821..8331f46b0 100644 --- a/extensions/ql-vscode/src/remote-queries/sample-data.ts +++ b/extensions/ql-vscode/src/remote-queries/sample-data.ts @@ -101,7 +101,35 @@ export const sampleRemoteQueryResult: RemoteQueryResult = { 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', severity: 'Error', filePath: 'npm-packages/meteor-installer/config.js', diff --git a/extensions/ql-vscode/src/remote-queries/sarif-processing.ts b/extensions/ql-vscode/src/remote-queries/sarif-processing.ts index a88d0db76..0ae886fd1 100644 --- a/extensions/ql-vscode/src/remote-queries/sarif-processing.ts +++ b/extensions/ql-vscode/src/remote-queries/sarif-processing.ts @@ -1,6 +1,16 @@ 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'; @@ -10,82 +20,76 @@ export function extractAnalysisAlerts( alerts: AnalysisAlert[], errors: string[] } { - if (!sarifLog) { - return { alerts: [], errors: ['No SARIF log was found'] }; - } - - if (!sarifLog.runs) { - return { alerts: [], errors: ['No runs found in the SARIF file'] }; - } - - const errors: string[] = []; const alerts: AnalysisAlert[] = []; + const errors: string[] = []; - for (const run of sarifLog.runs) { - if (!run.results) { - errors.push('No results found in the SARIF run'); - continue; - } - - for (const result of run.results) { - const message = result.message?.text; - if (!message) { - errors.push('No message found in the SARIF result'); + for (const run of sarifLog.runs ?? []) { + for (const result of run.results ?? []) { + try { + alerts.push(...extractResultAlerts(run, result)); + } catch (e) { + errors.push(`Error when processing SARIF result: ${e}`); 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 }; } -export function tryGetSeverity( - sarifRun: sarif.Run, +function extractResultAlerts( + run: sarif.Run, result: sarif.Result -): ResultSeverity | undefined { - if (!sarifRun || !result) { - return undefined; +): AnalysisAlert[] { + const alerts: AnalysisAlert[] = []; + + 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); - if (!rule) { + return alerts; +} + +function getShortDescription( + rule: sarif.ReportingDescriptor | undefined, + message: AnalysisMessage, +): string { + if (rule?.shortDescription?.text) { + return rule.shortDescription.text; + } + + return message.tokens.map(token => token.text).join(); +} + +export function tryGetSeverity( + sarifRun: sarif.Run, + result: sarif.Result, + rule: sarif.ReportingDescriptor | undefined +): ResultSeverity | undefined { + if (!sarifRun || !result || !rule) { return undefined; } @@ -147,142 +151,50 @@ export function tryGetRule( return undefined; } -interface Location { - message?: string; - filePath: string; - codeSnippet: CodeSnippet, - highlightedRegion: HighlightedRegion +function getCodeSnippet(region: sarif.Region): CodeSnippet { + const text = region.snippet!.text!; + const { startLine, endLine } = parseSarifRegion(region); + + return { + startLine, + endLine, + text + } as CodeSnippet; } -function validateContextRegion(contextRegion: sarif.Region | undefined): string[] { - const errors: string[] = []; +function getHighlightedRegion(region: sarif.Region): HighlightedRegion { + const { startLine, startColumn, endLine, endColumn } = parseSarifRegion(region); - 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'); - } - - 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; + return { + startLine, + startColumn, + endLine, + endColumn + }; } -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( +function getCodeFlows( result: sarif.Result -): { - codeFlows: CodeFlow[], - errors: string[] -} { +): CodeFlow[] { 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; - } + for (const threadFlowLocation of threadFlow.locations) { + const physicalLocation = threadFlowLocation!.location!.physicalLocation!; + const filePath = physicalLocation!.artifactLocation!.uri!; + const codeSnippet = getCodeSnippet(physicalLocation.contextRegion!); + const highlightedRegion = physicalLocation.region + ? getHighlightedRegion(physicalLocation.region) + : undefined; threadFlows.push({ - filePath: processedLocation!.filePath, - codeSnippet: processedLocation!.codeSnippet, - highlightedRegion: processedLocation!.highlightedRegion, - message: processedLocation!.message + filePath, + codeSnippet, + highlightedRegion } 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 }; } diff --git a/extensions/ql-vscode/src/remote-queries/shared/analysis-result.ts b/extensions/ql-vscode/src/remote-queries/shared/analysis-result.ts index ebcf63ae6..5345fb4d8 100644 --- a/extensions/ql-vscode/src/remote-queries/shared/analysis-result.ts +++ b/extensions/ql-vscode/src/remote-queries/shared/analysis-result.ts @@ -7,12 +7,12 @@ export interface AnalysisResults { } export interface AnalysisAlert { - message: string; + message: AnalysisMessage; shortDescription: string; severity: ResultSeverity; filePath: string; codeSnippet: CodeSnippet; - highlightedRegion: HighlightedRegion; + highlightedRegion?: HighlightedRegion; codeFlows: CodeFlow[]; } @@ -25,7 +25,7 @@ export interface CodeSnippet { export interface HighlightedRegion { startLine: number; startColumn: number; - endLine: number | undefined; + endLine: number; endColumn: number; } @@ -36,8 +36,30 @@ export interface CodeFlow { export interface ThreadFlow { filePath: string; codeSnippet: CodeSnippet; - highlightedRegion: HighlightedRegion; - message?: string; + highlightedRegion?: HighlightedRegion; + message?: AnalysisMessage; +} + +export interface AnalysisMessage { + tokens: AnalysisMessageToken[] +} + +export type AnalysisMessageToken = + | AnalysisMessageTextToken + | AnalysisMessageLocationToken; + +export interface AnalysisMessageTextToken { + t: 'text'; + text: string; +} + +export interface AnalysisMessageLocationToken { + t: 'location'; + text: string; + location: { + filePath: string; + highlightedRegion?: HighlightedRegion; + }; } export type ResultSeverity = 'Recommendation' | 'Warning' | 'Error'; diff --git a/extensions/ql-vscode/src/remote-queries/view/CodePaths.tsx b/extensions/ql-vscode/src/remote-queries/view/CodePaths.tsx index 9c9e307ab..9d74429cc 100644 --- a/extensions/ql-vscode/src/remote-queries/view/CodePaths.tsx +++ b/extensions/ql-vscode/src/remote-queries/view/CodePaths.tsx @@ -3,7 +3,7 @@ import { ActionList, ActionMenu, Box, Button, Label, Link, Overlay } from '@prim import * as React from 'react'; import { useRef, useState } from 'react'; 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 SectionTitle from './SectionTitle'; import VerticalSpace from './VerticalSpace'; @@ -45,7 +45,7 @@ const CodePath = ({ severity }: { codeFlow: CodeFlow; - message: string; + message: AnalysisMessage; severity: ResultSeverity; }) => { return <> @@ -122,7 +122,7 @@ const CodePaths = ({ }: { codeFlows: CodeFlow[], ruleDescription: string, - message: string, + message: AnalysisMessage, severity: ResultSeverity }) => { const [isOpen, setIsOpen] = useState(false); diff --git a/extensions/ql-vscode/src/remote-queries/view/FileCodeSnippet.tsx b/extensions/ql-vscode/src/remote-queries/view/FileCodeSnippet.tsx index 3bcebae46..1e0db50a5 100644 --- a/extensions/ql-vscode/src/remote-queries/view/FileCodeSnippet.tsx +++ b/extensions/ql-vscode/src/remote-queries/view/FileCodeSnippet.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; 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 VerticalSpace from './VerticalSpace'; @@ -75,19 +75,19 @@ const HighlightedLine = ({ text }: { text: string }) => { }; const Message = ({ - messageText, + message, currentLineNumber, highlightedRegion, borderColor, children }: { - messageText: string, + message: AnalysisMessage, currentLineNumber: number, - highlightedRegion: HighlightedRegion, + highlightedRegion?: HighlightedRegion, borderColor: string, children: React.ReactNode }) => { - if (highlightedRegion.startLine !== currentLineNumber) { + if (!highlightedRegion || highlightedRegion.startLine !== currentLineNumber) { return <>; } @@ -101,7 +101,16 @@ const Message = ({ paddingTop="1em" paddingBottom="1em"> - {messageText} + {message.tokens.map((token, index) => { + switch (token.t) { + case 'text': + return {token.text}; + case 'location': + return {token.text}; + default: + return <>; + } + })} {children && <> {children} @@ -120,9 +129,9 @@ const CodeLine = ({ }: { line: string, lineNumber: number, - highlightedRegion: HighlightedRegion + highlightedRegion?: HighlightedRegion }) => { - if (!shouldHighlightLine(lineNumber, highlightedRegion)) { + if (!highlightedRegion || !shouldHighlightLine(lineNumber, highlightedRegion)) { return ; } @@ -165,9 +174,9 @@ const FileCodeSnippet = ({ }: { filePath: string, codeSnippet: CodeSnippet, - highlightedRegion: HighlightedRegion, + highlightedRegion?: HighlightedRegion, severity?: ResultSeverity, - message?: string, + message?: AnalysisMessage, messageChildren?: React.ReactNode, }) => { @@ -184,7 +193,7 @@ const FileCodeSnippet = ({ {code.map((line, index) => (
{message && severity && diff --git a/extensions/ql-vscode/test/pure-tests/sarif-processing.test.ts b/extensions/ql-vscode/test/pure-tests/sarif-processing.test.ts index 61cd0a6ca..208db88d1 100644 --- a/extensions/ql-vscode/test/pure-tests/sarif-processing.test.ts +++ b/extensions/ql-vscode/test/pure-tests/sarif-processing.test.ts @@ -4,6 +4,7 @@ import * as chaiAsPromised from 'chai-as-promised'; import * as chai from 'chai'; import * as sarif from 'sarif'; import { extractAnalysisAlerts, tryGetRule, tryGetSeverity } from '../../src/remote-queries/sarif-processing'; +import { AnalysisMessage, AnalysisMessageLocationToken } from '../../src/remote-queries/shared/analysis-result'; chai.use(chaiAsPromised); const expect = chai.expect; @@ -288,17 +289,19 @@ describe('SARIF processing', () => { }); describe('tryGetSeverity', () => { - it('should return undefined if no rule found', () => { + it('should return undefined if no rule set', () => { const result = { - // The rule is missing here. message: 'msg' } as sarif.Result; + // The rule should be set here. + const rule: sarif.ReportingDescriptor | undefined = undefined; + const sarifRun = { results: [result] } as sarif.Run; - const severity = tryGetSeverity(sarifRun, result); + const severity = tryGetSeverity(sarifRun, result, rule); expect(severity).to.be.undefined; }); @@ -310,24 +313,26 @@ describe('SARIF processing', () => { } } as sarif.Result; + const rule = { + id: 'A', + properties: { + // Severity not set + } + } as sarif.ReportingDescriptor; + const sarifRun = { results: [result], tool: { driver: { rules: [ - { - id: 'A', - properties: { - // Severity not set - } - }, + rule, result.rule ] } } } as sarif.Run; - const severity = tryGetSeverity(sarifRun, result); + const severity = tryGetSeverity(sarifRun, result, rule); expect(severity).to.be.undefined; }); @@ -346,24 +351,26 @@ describe('SARIF processing', () => { } } as sarif.Result; + const rule = { + id: 'A', + properties: { + 'problem.severity': sarifSeverity + } + } as sarif.ReportingDescriptor; + const sarifRun = { results: [result], tool: { driver: { rules: [ - { - id: 'A', - properties: { - 'problem.severity': sarifSeverity - } - }, + rule, result.rule ] } } } as sarif.Run; - const severity = tryGetSeverity(sarifRun, result); + const severity = tryGetSeverity(sarifRun, result, rule); expect(severity).to.equal(parsedSeverity); }); }); @@ -371,7 +378,7 @@ describe('SARIF processing', () => { }); 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 = { // Runs are missing here. } as sarif.Log; @@ -379,11 +386,10 @@ describe('SARIF processing', () => { const result = extractAnalysisAlerts(sarif); expect(result).to.be.ok; - expect(result.errors.length).to.equal(1); - expect(result.errors[0]).to.equal('No runs found in the SARIF file'); + expect(result.alerts.length).to.equal(0); }); - it('should return errors for runs that have no results', () => { + it('should not return any results for runs that have no results', () => { const sarif = { runs: [ { @@ -398,8 +404,7 @@ describe('SARIF processing', () => { const result = extractAnalysisAlerts(sarif); expect(result).to.be.ok; - expect(result.errors.length).to.equal(1); - expect(result.errors[0]).to.equal('No results found in the SARIF run'); + expect(result.alerts.length).to.equal(0); }); it('should return errors for results that have no message', () => { @@ -410,7 +415,7 @@ describe('SARIF processing', () => { expect(result).to.be.ok; expect(result.errors.length).to.equal(1); - expect(result.errors[0]).to.equal('No message found in the SARIF result'); + expectResultParsingError(result.errors[0]); }); it('should return errors for result locations with no context region', () => { @@ -421,18 +426,17 @@ describe('SARIF processing', () => { expect(result).to.be.ok; expect(result.errors.length).to.equal(1); - expect(result.errors[0]).to.equal('No context region found in the SARIF result location'); + expectResultParsingError(result.errors[0]); }); - it('should return errors for result locations with no region', () => { + it('should not return errors for result locations with no region', () => { const sarif = buildValidSarifLog(); sarif.runs![0]!.results![0]!.locations![0]!.physicalLocation!.region = undefined; const result = extractAnalysisAlerts(sarif); expect(result).to.be.ok; - expect(result.errors.length).to.equal(1); - expect(result.errors[0]).to.equal('No region found in the SARIF result location'); + expect(result.alerts.length).to.equal(1); }); it('should return errors for result locations with no physical location', () => { @@ -443,7 +447,7 @@ describe('SARIF processing', () => { expect(result).to.be.ok; expect(result.errors.length).to.equal(1); - expect(result.errors[0]).to.equal('No file path found in the SARIF result location'); + expectResultParsingError(result.errors[0]); }); it('should return results for all alerts', () => { @@ -532,14 +536,61 @@ describe('SARIF processing', () => { expect(result).to.be.ok; expect(result.errors.length).to.equal(0); expect(result.alerts.length).to.equal(3); - expect(result.alerts.find(a => a.message === 'msg1' && a.codeSnippet.text === 'foo')).to.be.ok; - expect(result.alerts.find(a => a.message === 'msg1' && a.codeSnippet.text === 'bar')).to.be.ok; - expect(result.alerts.find(a => a.message === 'msg2' && a.codeSnippet.text === 'baz')).to.be.ok; + expect(result.alerts.find(a => getMessageText(a.message) === 'msg1' && a.codeSnippet.text === 'foo')).to.be.ok; + expect(result.alerts.find(a => getMessageText(a.message) === 'msg1' && a.codeSnippet.text === 'bar')).to.be.ok; + expect(result.alerts.find(a => getMessageText(a.message) === 'msg2' && a.codeSnippet.text === 'baz')).to.be.ok; expect(result.alerts.every(a => a.severity === 'Warning')).to.be.true; }); + it('should deal with complex messages', () => { + const sarif = buildValidSarifLog(); + const messageText = 'This shell command depends on an uncontrolled [absolute path](1).'; + sarif.runs![0]!.results![0]!.message!.text = messageText; + sarif.runs![0]!.results![0].relatedLocations = [ + { + id: 1, + physicalLocation: { + artifactLocation: { + uri: 'npm-packages/meteor-installer/config.js', + }, + region: { + startLine: 35, + startColumn: 20, + endColumn: 60 + } + }, + } + ]; + + const result = extractAnalysisAlerts(sarif); + + 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 { return { version: '0.0.1' as sarif.Log.version, @@ -577,4 +628,8 @@ describe('SARIF processing', () => { ] } as sarif.Log; } + + function getMessageText(message: AnalysisMessage) { + return message.tokens.map(t => t.text).join(''); + } });