Add SARIF processing and basic alert rendering (#1171)
This commit is contained in:
@@ -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.
|
||||
|
||||
sarifLog.runs?.forEach(run => {
|
||||
run?.results?.forEach(result => {
|
||||
if (result?.message?.text) {
|
||||
queryResults.push({
|
||||
message: result.message.text
|
||||
});
|
||||
const processedSarif = extractAnalysisAlerts(sarifLog);
|
||||
if (processedSarif.errors) {
|
||||
void this.logger.log(`Error processing SARIF file: ${os.EOL}${processedSarif.errors.join(os.EOL)}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return queryResults;
|
||||
return processedSarif.alerts;
|
||||
}
|
||||
|
||||
private isAnalysisInMemory(analysis: AnalysisSummary): boolean {
|
||||
|
||||
@@ -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[] = [
|
||||
{
|
||||
|
||||
203
extensions/ql-vscode/src/remote-queries/sarif-processing.ts
Normal file
203
extensions/ql-vscode/src/remote-queries/sarif-processing.ts
Normal 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;
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
@@ -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} />
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
580
extensions/ql-vscode/test/pure-tests/sarif-processing.test.ts
Normal file
580
extensions/ql-vscode/test/pure-tests/sarif-processing.test.ts
Normal 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;
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user