Add SARIF processing and basic alert rendering (#1171)

This commit is contained in:
Charis Kyriakou
2022-03-03 09:03:27 +00:00
committed by GitHub
parent c18f7953e7
commit c609377a9c
8 changed files with 1017 additions and 37 deletions

View File

@@ -6,9 +6,10 @@ import { Credentials } from '../authentication';
import { Logger } from '../logging';
import { downloadArtifactFromLink } from './gh-actions-api-client';
import { AnalysisSummary } from './shared/remote-query-result';
import { AnalysisResults, QueryResult } from './shared/analysis-result';
import { AnalysisResults, AnalysisAlert } from './shared/analysis-result';
import { UserCancellationException } from '../commandRunner';
import { sarifParser } from '../sarif-parser';
import { extractAnalysisAlerts } from './sarif-processing';
export class AnalysesResultsManager {
// Store for the results of various analyses for each remote query.
@@ -136,26 +137,15 @@ export class AnalysesResultsManager {
void publishResults([...resultsForQuery]);
}
private async readResults(filePath: string): Promise<QueryResult[]> {
const queryResults: QueryResult[] = [];
private async readResults(filePath: string): Promise<AnalysisAlert[]> {
const sarifLog = await sarifParser(filePath);
// Read the sarif file and extract information that we want to display
// in the UI. For now we're only getting the message texts but we'll gradually
// extract more information based on the UX we want to build.
const processedSarif = extractAnalysisAlerts(sarifLog);
if (processedSarif.errors) {
void this.logger.log(`Error processing SARIF file: ${os.EOL}${processedSarif.errors.join(os.EOL)}`);
}
sarifLog.runs?.forEach(run => {
run?.results?.forEach(result => {
if (result?.message?.text) {
queryResults.push({
message: result.message.text
});
}
});
});
return queryResults;
return processedSarif.alerts;
}
private isAnalysisInMemory(analysis: AnalysisSummary): boolean {

View File

@@ -99,7 +99,23 @@ export const sampleRemoteQueryResult: RemoteQueryResult = {
};
const createAnalysisResults = (n: number) => Array(n).fill({ 'message': 'Sample text' });
const createAnalysisResults = (n: number) => Array(n).fill(
{
message: 'This shell command depends on an uncontrolled [absolute path](1).',
severity: 'Error',
filePath: 'npm-packages/meteor-installer/config.js',
codeSnippet: {
startLine: 253,
endLine: 257,
text: ' if (isWindows()) {\n //set for the current session and beyond\n child_process.execSync(`setx path "${meteorPath}/;%path%`);\n return;\n }\n',
},
highlightedRegion: {
startLine: 255,
startColumn: 28,
endColumn: 62
}
}
);
export const sampleAnalysesResultsStage1: AnalysisResults[] = [
{

View File

@@ -0,0 +1,203 @@
import * as sarif from 'sarif';
import { AnalysisAlert, ResultSeverity } from './shared/analysis-result';
const defaultSeverity = 'Warning';
export function extractAnalysisAlerts(
sarifLog: sarif.Log
): {
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[] = [];
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');
continue;
}
const severity = tryGetSeverity(run, result) || defaultSeverity;
if (!result.locations) {
errors.push('No locations found in the SARIF result');
continue;
}
for (const location of result.locations) {
const contextRegion = location.physicalLocation?.contextRegion;
if (!contextRegion) {
errors.push('No context region found in the SARIF result location');
continue;
}
if (contextRegion.startLine === undefined) {
errors.push('No start line set for a result context region');
continue;
}
if (contextRegion.endLine === undefined) {
errors.push('No end line set for a result context region');
continue;
}
if (!contextRegion.snippet?.text) {
errors.push('No text set for a result context region');
continue;
}
const region = location.physicalLocation?.region;
if (!region) {
errors.push('No region found in the SARIF result location');
continue;
}
if (region.startLine === undefined) {
errors.push('No start line set for a result region');
continue;
}
if (region.startColumn === undefined) {
errors.push('No start column set for a result region');
continue;
}
if (region.endColumn === undefined) {
errors.push('No end column set for a result region');
continue;
}
const filePath = location.physicalLocation?.artifactLocation?.uri;
if (!filePath) {
errors.push('No file path found in the SARIF result location');
continue;
}
const analysisAlert = {
message,
filePath,
severity,
codeSnippet: {
startLine: contextRegion.startLine,
endLine: contextRegion.endLine,
text: contextRegion.snippet.text
},
highlightedRegion: {
startLine: region.startLine,
startColumn: region.startColumn,
endLine: region.endLine,
endColumn: region.endColumn
}
};
const validationErrors = getAlertValidationErrors(analysisAlert);
if (validationErrors.length > 0) {
errors.push(...validationErrors);
continue;
}
alerts.push(analysisAlert);
}
}
}
return { alerts, errors };
}
export function tryGetSeverity(
sarifRun: sarif.Run,
result: sarif.Result
): ResultSeverity | undefined {
if (!sarifRun || !result) {
return undefined;
}
const rule = tryGetRule(sarifRun, result);
if (!rule) {
return undefined;
}
const severity = rule.properties?.['problem.severity'];
if (!severity) {
return undefined;
}
switch (severity.toLowerCase()) {
case 'recommendation':
return 'Recommendation';
case 'warning':
return 'Warning';
case 'error':
return 'Error';
}
return undefined;
}
export function tryGetRule(
sarifRun: sarif.Run,
result: sarif.Result
): sarif.ReportingDescriptor | undefined {
if (!sarifRun || !result) {
return undefined;
}
const resultRule = result.rule;
if (!resultRule) {
return undefined;
}
// The rule can found in two places:
// - Either in the run's tool driver tool component
// - Or in the run's tool extensions tool component
const ruleId = resultRule.id;
if (ruleId) {
const rule = sarifRun.tool.driver.rules?.find(r => r.id === ruleId);
if (rule) {
return rule;
}
}
const ruleIndex = resultRule.index;
if (ruleIndex != undefined) {
const toolComponentIndex = result.rule?.toolComponent?.index;
const toolExtensions = sarifRun.tool.extensions;
if (toolComponentIndex !== undefined && toolExtensions !== undefined) {
const toolComponent = toolExtensions[toolComponentIndex];
if (toolComponent?.rules !== undefined) {
return toolComponent.rules[ruleIndex];
}
}
}
// Couldn't find the rule.
return undefined;
}
function getAlertValidationErrors(alert: AnalysisAlert): string[] {
const errors = [];
if (alert.codeSnippet.startLine > alert.codeSnippet.endLine) {
errors.push('The code snippet start line is greater than the end line');
}
const highlightedRegion = alert.highlightedRegion;
if (highlightedRegion.endLine === highlightedRegion.startLine &&
highlightedRegion.endColumn < highlightedRegion.startColumn) {
errors.push('The highlighted region end column is greater than the start column');
}
return errors;
}

View File

@@ -3,9 +3,28 @@ export type AnalysisResultStatus = 'InProgress' | 'Completed' | 'Failed';
export interface AnalysisResults {
nwo: string;
status: AnalysisResultStatus;
results: QueryResult[];
results: AnalysisAlert[];
}
export interface QueryResult {
message?: string;
export interface AnalysisAlert {
message: string;
severity: ResultSeverity;
filePath: string;
codeSnippet: CodeSnippet
highlightedRegion: HighlightedRegion
}
export interface CodeSnippet {
startLine: number;
endLine: number;
text: string;
}
export interface HighlightedRegion {
startLine: number;
startColumn: number;
endLine: number | undefined;
endColumn: number;
}
export type ResultSeverity = 'Recommendation' | 'Warning' | 'Error';

View File

@@ -0,0 +1,171 @@
import * as React from 'react';
import styled from 'styled-components';
import { Box, Link } from '@primer/react';
import { AnalysisAlert, HighlightedRegion, ResultSeverity } from '../shared/analysis-result';
const borderColor = 'var(--vscode-editor-snippetFinalTabstopHighlightBorder)';
const warningColor = '#966C23';
const highlightColor = '#534425';
const getSeverityColor = (severity: ResultSeverity) => {
switch (severity) {
case 'Recommendation':
return 'blue';
case 'Warning':
return warningColor;
case 'Error':
return 'red';
}
};
const Container = styled.div`
font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
`;
const TitleContainer = styled.div`
border: 0.1em solid ${borderColor};
border-top-left-radius: 0.2em;
border-top-right-radius: 0.2em;
padding: 0.5em;
`;
const CodeContainer = styled.div`
font-size: x-small;
border-left: 0.1em solid ${borderColor};
border-right: 0.1em solid ${borderColor};
border-bottom: 0.1em solid ${borderColor};
border-bottom-left-radius: 0.2em;
border-bottom-right-radius: 0.2em;
padding-top: 1em;
padding-bottom: 1em;
`;
const MessageText = styled.span<{ severity: ResultSeverity }>`
font-size: x-small;
color: ${props => getSeverityColor(props.severity)};
padding-left: 0.5em;
`;
const MessageContainer = styled.div`
padding-top: 0.5em;
padding-bottom: 0.5em;
`;
const Message = ({ alert, currentLineNumber }: {
alert: AnalysisAlert,
currentLineNumber: number
}) => {
if (alert.highlightedRegion.startLine !== currentLineNumber) {
return <></>;
}
return <MessageContainer>
<Box
borderColor="border.default"
borderWidth={1}
borderStyle="solid"
borderLeftColor={getSeverityColor(alert.severity)}
borderLeftWidth={3}
paddingTop="1em"
paddingBottom="1em">
<MessageText severity={alert.severity}>{alert.message}</MessageText>
</Box>
</MessageContainer>;
};
const replaceSpaceChar = (text: string) => text.replaceAll(' ', '\u00a0');
const PlainLine = ({ text }: { text: string }) => {
return <span>{replaceSpaceChar(text)}</span>;
};
const HighlightedLine = ({ text }: { text: string }) => {
return <span style={{ backgroundColor: highlightColor }}>{replaceSpaceChar(text)}</span>;
};
const shouldHighlightLine = (lineNumber: number, highlightedRegion: HighlightedRegion) => {
if (lineNumber < highlightedRegion.startLine) {
return false;
}
if (highlightedRegion.endLine) {
return lineNumber <= highlightedRegion.endLine;
}
return true;
};
const CodeLine = ({
line,
lineNumber,
highlightedRegion
}: {
line: string,
lineNumber: number,
highlightedRegion: HighlightedRegion
}) => {
if (!shouldHighlightLine(lineNumber, highlightedRegion)) {
return <PlainLine text={line} />;
}
const section1 = line.substring(0, highlightedRegion.startColumn - 1);
const section2 = line.substring(highlightedRegion.startColumn - 1, highlightedRegion.endColumn - 1);
const section3 = line.substring(highlightedRegion.endColumn - 1, line.length);
return (
<>
<PlainLine text={section1} />
<HighlightedLine text={section2} />
<PlainLine text={section3} />
</>
);
};
const AnalysisAlertResult = ({ alert }: { alert: AnalysisAlert }) => {
const code = alert.codeSnippet.text
.split('\n')
.filter(line => line.replace('\n', '').length > 0);
const startingLine = alert.codeSnippet.startLine;
return (
<Container>
<TitleContainer>
<Link>{alert.filePath}</Link>
</TitleContainer>
<CodeContainer>
{code.map((line, index) => (
<div key={index}>
<Message alert={alert} currentLineNumber={startingLine + index} />
<Box display="flex">
<Box
p={2}
borderStyle="none"
paddingTop="0.01em"
paddingLeft="0.5em"
paddingRight="0.5em"
paddingBottom="0.2em">
{startingLine + index}
</Box>
<Box
flexGrow={1}
p={2}
borderStyle="none"
paddingTop="0.01em"
paddingLeft="1.5em"
paddingRight="0.5em"
paddingBottom="0.2em">
<CodeLine
line={line}
lineNumber={startingLine + index}
highlightedRegion={alert.highlightedRegion} />
</Box>
</Box>
</div>
))}
</CodeContainer>
</Container>
);
};
export default AnalysisAlertResult;

View File

@@ -17,6 +17,7 @@ import { AnalysisResults } from '../shared/analysis-result';
import DownloadSpinner from './DownloadSpinner';
import CollapsibleItem from './CollapsibleItem';
import { AlertIcon, CodeSquareIcon, FileCodeIcon, FileSymlinkFileIcon, RepoIcon, TerminalIcon } from '@primer/octicons-react';
import AnalysisAlertResult from './AnalysisAlertResult';
const numOfReposInContractedMode = 10;
@@ -221,7 +222,7 @@ const Summary = ({
analysesResults={analysesResults} />
}
<ul className="vscode-codeql__analysis-summaries-list">
<ul className="vscode-codeql__flat-list">
{queryResult.analysisSummaries.slice(0, numOfReposToShow).map((summary, i) =>
<li key={summary.nwo} className="vscode-codeql__analysis-summaries-list-item">
<SummaryItem
@@ -268,7 +269,13 @@ const RepoAnalysisResults = (analysisResults: AnalysisResults) => {
return (
<CollapsibleItem title={title}>
{analysisResults.results.map((r, i) => (<p key={i} >{r.message}</p>))}
<ul className="vscode-codeql__flat-list" >
{analysisResults.results.map((r, i) =>
<li key={i}>
<AnalysisAlertResult alert={r} />
<VerticalSpace size={2} />
</li>)}
</ul>
</CollapsibleItem>
);
};
@@ -289,7 +296,7 @@ const AnalysesResults = ({ analysesResults, totalResults }: { analysesResults: A
<AnalysesResultsDescription
totalAnalysesResults={totalAnalysesResults}
totalResults={totalResults} />
<ul className="vscode-codeql__analyses-results-list">
<ul className="vscode-codeql__flat-list">
{analysesResults.filter(a => a.results.length > 0).map(r =>
<li key={r.nwo} className="vscode-codeql__analyses-results-list-item">
<RepoAnalysisResults {...r} />

View File

@@ -12,22 +12,10 @@
padding-top: 1.5em;
}
.vscode-codeql__analysis-summaries-list {
list-style-type: none;
margin: 0;
padding: 0.5em 0 0 0;
}
.vscode-codeql__analysis-summaries-list-item {
margin-top: 0.5em;
}
.vscode-codeql__analyses-results-list {
list-style-type: none;
margin: 0;
padding: 0.5em 0 0 0;
}
.vscode-codeql__analyses-results-list-item {
padding-top: 0.5em;
}
@@ -55,3 +43,9 @@
Liberation Mono, monospace;
color: var(--vscode-editor-foreground);
}
.vscode-codeql__flat-list {
list-style-type: none;
margin: 0;
padding: 0.5em 0 0 0;
}

View File

@@ -0,0 +1,580 @@
import 'vscode-test';
import 'mocha';
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';
chai.use(chaiAsPromised);
const expect = chai.expect;
describe('SARIF processing', () => {
describe('tryGetRule', () => {
describe('Using the tool driver', () => {
it('should return undefined if no rule has been set on the result', () => {
const result = {
message: 'msg'
// Rule is missing here.
} as sarif.Result;
const sarifRun = {
results: [result]
} as sarif.Run;
const rule = tryGetRule(sarifRun, result);
expect(rule).to.be.undefined;
});
it('should return undefined if rule missing from tool driver', () => {
const result = {
message: 'msg',
rule: {
id: 'NonExistentRule'
}
} as sarif.Result;
const sarifRun = {
results: [result],
tool: {
driver: {
rules: [
// No rule with id 'NonExistentRule' is set here.
{
id: 'A',
},
{
id: 'B'
}
]
}
}
} as sarif.Run;
const rule = tryGetRule(sarifRun, result);
expect(rule).to.be.undefined;
});
it('should return rule if it has been set on the tool driver', () => {
const result = {
message: 'msg',
rule: {
id: 'B'
}
} as sarif.Result;
const sarifRun = {
results: [result],
tool: {
driver: {
rules: [
{
id: 'A',
},
result.rule
]
}
}
} as sarif.Run;
const rule = tryGetRule(sarifRun, result);
expect(rule).to.be.ok;
expect(rule!.id).to.equal(result!.rule!.id);
});
});
describe('Using the tool extensions', () => {
it('should return undefined if rule index not set', () => {
const result = {
message: 'msg',
rule: {
// The rule index should be set here.
toolComponent: {
index: 1
}
}
} as sarif.Result;
const sarifRun = {
results: [result],
tool: {
extensions: [
{
name: 'foo',
rules: [
{
id: 'A',
},
{
id: 'B'
}
]
},
{
name: 'bar',
rules: [
{
id: 'C',
},
{
id: 'D'
}
]
}
]
}
} as sarif.Run;
const rule = tryGetRule(sarifRun, result);
expect(rule).to.be.undefined;
});
it('should return undefined if tool component index not set', () => {
const result = {
message: 'msg',
rule: {
index: 1,
toolComponent: {
// The tool component index should be set here.
}
}
} as sarif.Result;
const sarifRun = {
results: [result],
tool: {
extensions: [
{
name: 'foo',
rules: [
{
id: 'A',
},
{
id: 'B'
}
]
},
{
name: 'bar',
rules: [
{
id: 'C',
},
{
id: 'D'
}
]
}
]
}
} as sarif.Run;
const rule = tryGetRule(sarifRun, result);
expect(rule).to.be.undefined;
});
it('should return undefined if tool extensions not set', () => {
const result = {
message: 'msg',
rule: {
index: 1,
toolComponent: {
index: 1
}
}
} as sarif.Result;
const sarifRun = {
results: [result],
tool: {
// Extensions should be set here.
}
} as sarif.Run;
const rule = tryGetRule(sarifRun, result);
expect(rule).to.be.undefined;
});
it('should return undefined if tool extensions do not contain index', () => {
const result = {
message: 'msg',
rule: {
index: 1,
toolComponent: {
index: 1
}
}
} as sarif.Result;
const sarifRun = {
results: [result],
tool: {
extensions: [
{
name: 'foo',
rules: [
{
id: 'A',
},
{
id: 'B'
}
]
}
// There should be one more extension here (index 1).
]
}
} as sarif.Run;
const rule = tryGetRule(sarifRun, result);
expect(rule).to.be.undefined;
});
it('should return rule if all information is defined', () => {
const result = {
message: 'msg',
ruleIndex: 1,
rule: {
index: 1,
toolComponent: {
index: 1
}
}
} as sarif.Result;
const sarifRun = {
results: [result],
tool: {
extensions: [
{
name: 'foo',
rules: [
{
id: 'A',
},
{
id: 'B'
}
]
},
{
name: 'bar',
rules: [
{
id: 'C',
},
{
id: 'D',
}
]
}
]
}
} as sarif.Run;
const rule = tryGetRule(sarifRun, result);
expect(rule).to.be.ok;
expect(rule!.id).to.equal('D');
});
});
});
describe('tryGetSeverity', () => {
it('should return undefined if no rule found', () => {
const result = {
// The rule is missing here.
message: 'msg'
} as sarif.Result;
const sarifRun = {
results: [result]
} as sarif.Run;
const severity = tryGetSeverity(sarifRun, result);
expect(severity).to.be.undefined;
});
it('should return undefined if severity not set on rule', () => {
const result = {
message: 'msg',
rule: {
id: 'A'
}
} as sarif.Result;
const sarifRun = {
results: [result],
tool: {
driver: {
rules: [
{
id: 'A',
properties: {
// Severity not set
}
},
result.rule
]
}
}
} as sarif.Run;
const severity = tryGetSeverity(sarifRun, result);
expect(severity).to.be.undefined;
});
const severityMap = {
recommendation: 'Recommendation',
warning: 'Warning',
error: 'Error'
};
Object.entries(severityMap).forEach(([sarifSeverity, parsedSeverity]) => {
it(`should get ${parsedSeverity} severity`, () => {
const result = {
message: 'msg',
rule: {
id: 'A'
}
} as sarif.Result;
const sarifRun = {
results: [result],
tool: {
driver: {
rules: [
{
id: 'A',
properties: {
'problem.severity': sarifSeverity
}
},
result.rule
]
}
}
} as sarif.Run;
const severity = tryGetSeverity(sarifRun, result);
expect(severity).to.equal(parsedSeverity);
});
});
});
describe('extractAnalysisAlerts', () => {
it('should return an error if no runs found in the SARIF', () => {
const sarif = {
// Runs are missing here.
} as sarif.Log;
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');
});
it('should return errors for runs that have no results', () => {
const sarif = {
runs: [
{
results: []
},
{
// Results are missing here.
}
]
} as sarif.Log;
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');
});
it('should return errors for results that have no message', () => {
const sarif = buildValidSarifLog();
sarif.runs![0]!.results![0]!.message.text = undefined;
const result = extractAnalysisAlerts(sarif);
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');
});
it('should return errors for result locations with no context region', () => {
const sarif = buildValidSarifLog();
sarif.runs![0]!.results![0]!.locations![0]!.physicalLocation!.contextRegion = undefined;
const result = extractAnalysisAlerts(sarif);
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');
});
it('should 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');
});
it('should return errors for result locations with no physical location', () => {
const sarif = buildValidSarifLog();
sarif.runs![0]!.results![0]!.locations![0]!.physicalLocation!.artifactLocation = undefined;
const result = extractAnalysisAlerts(sarif);
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');
});
it('should return results for all alerts', () => {
const sarif = {
version: '0.0.1' as sarif.Log.version,
runs: [
{
results: [
{
message: {
text: 'msg1'
},
locations: [
{
physicalLocation: {
contextRegion: {
startLine: 10,
endLine: 12,
snippet: {
text: 'foo'
}
},
region: {
startLine: 10,
startColumn: 1,
endColumn: 3
},
artifactLocation: {
uri: 'foo.js'
}
}
},
{
physicalLocation: {
contextRegion: {
startLine: 10,
endLine: 12,
snippet: {
text: 'bar'
}
},
region: {
startLine: 10,
startColumn: 1,
endColumn: 3
},
artifactLocation: {
uri: 'bar.js'
}
}
}
]
},
{
message: {
text: 'msg2'
},
locations: [
{
physicalLocation: {
contextRegion: {
startLine: 10,
endLine: 12,
snippet: {
text: 'baz'
}
},
region: {
startLine: 10,
startColumn: 1,
endColumn: 3
},
artifactLocation: {
uri: 'baz.js'
}
}
}
]
}
]
}
]
} as sarif.Log;
const result = extractAnalysisAlerts(sarif);
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.every(a => a.severity === 'Warning')).to.be.true;
});
});
function buildValidSarifLog(): sarif.Log {
return {
version: '0.0.1' as sarif.Log.version,
runs: [
{
results: [
{
message: {
text: 'msg'
},
locations: [
{
physicalLocation: {
contextRegion: {
startLine: 10,
endLine: 12,
snippet: {
text: 'Foo'
}
},
region: {
startLine: 10,
startColumn: 1,
endColumn: 3
},
artifactLocation: {
uri: 'foo.js'
}
}
}
]
}
]
}
]
} as sarif.Log;
}
});