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 { Logger } from '../logging';
|
||||||
import { downloadArtifactFromLink } from './gh-actions-api-client';
|
import { downloadArtifactFromLink } from './gh-actions-api-client';
|
||||||
import { AnalysisSummary } from './shared/remote-query-result';
|
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 { UserCancellationException } from '../commandRunner';
|
||||||
import { sarifParser } from '../sarif-parser';
|
import { sarifParser } from '../sarif-parser';
|
||||||
|
import { extractAnalysisAlerts } from './sarif-processing';
|
||||||
|
|
||||||
export class AnalysesResultsManager {
|
export class AnalysesResultsManager {
|
||||||
// Store for the results of various analyses for each remote query.
|
// Store for the results of various analyses for each remote query.
|
||||||
@@ -136,26 +137,15 @@ export class AnalysesResultsManager {
|
|||||||
void publishResults([...resultsForQuery]);
|
void publishResults([...resultsForQuery]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async readResults(filePath: string): Promise<QueryResult[]> {
|
private async readResults(filePath: string): Promise<AnalysisAlert[]> {
|
||||||
const queryResults: QueryResult[] = [];
|
|
||||||
|
|
||||||
const sarifLog = await sarifParser(filePath);
|
const sarifLog = await sarifParser(filePath);
|
||||||
|
|
||||||
// Read the sarif file and extract information that we want to display
|
const processedSarif = extractAnalysisAlerts(sarifLog);
|
||||||
// in the UI. For now we're only getting the message texts but we'll gradually
|
if (processedSarif.errors) {
|
||||||
// extract more information based on the UX we want to build.
|
void this.logger.log(`Error processing SARIF file: ${os.EOL}${processedSarif.errors.join(os.EOL)}`);
|
||||||
|
}
|
||||||
|
|
||||||
sarifLog.runs?.forEach(run => {
|
return processedSarif.alerts;
|
||||||
run?.results?.forEach(result => {
|
|
||||||
if (result?.message?.text) {
|
|
||||||
queryResults.push({
|
|
||||||
message: result.message.text
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return queryResults;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private isAnalysisInMemory(analysis: AnalysisSummary): boolean {
|
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[] = [
|
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 {
|
export interface AnalysisResults {
|
||||||
nwo: string;
|
nwo: string;
|
||||||
status: AnalysisResultStatus;
|
status: AnalysisResultStatus;
|
||||||
results: QueryResult[];
|
results: AnalysisAlert[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QueryResult {
|
export interface AnalysisAlert {
|
||||||
message?: string;
|
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 DownloadSpinner from './DownloadSpinner';
|
||||||
import CollapsibleItem from './CollapsibleItem';
|
import CollapsibleItem from './CollapsibleItem';
|
||||||
import { AlertIcon, CodeSquareIcon, FileCodeIcon, FileSymlinkFileIcon, RepoIcon, TerminalIcon } from '@primer/octicons-react';
|
import { AlertIcon, CodeSquareIcon, FileCodeIcon, FileSymlinkFileIcon, RepoIcon, TerminalIcon } from '@primer/octicons-react';
|
||||||
|
import AnalysisAlertResult from './AnalysisAlertResult';
|
||||||
|
|
||||||
const numOfReposInContractedMode = 10;
|
const numOfReposInContractedMode = 10;
|
||||||
|
|
||||||
@@ -221,7 +222,7 @@ const Summary = ({
|
|||||||
analysesResults={analysesResults} />
|
analysesResults={analysesResults} />
|
||||||
}
|
}
|
||||||
|
|
||||||
<ul className="vscode-codeql__analysis-summaries-list">
|
<ul className="vscode-codeql__flat-list">
|
||||||
{queryResult.analysisSummaries.slice(0, numOfReposToShow).map((summary, i) =>
|
{queryResult.analysisSummaries.slice(0, numOfReposToShow).map((summary, i) =>
|
||||||
<li key={summary.nwo} className="vscode-codeql__analysis-summaries-list-item">
|
<li key={summary.nwo} className="vscode-codeql__analysis-summaries-list-item">
|
||||||
<SummaryItem
|
<SummaryItem
|
||||||
@@ -268,7 +269,13 @@ const RepoAnalysisResults = (analysisResults: AnalysisResults) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<CollapsibleItem title={title}>
|
<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>
|
</CollapsibleItem>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -289,7 +296,7 @@ const AnalysesResults = ({ analysesResults, totalResults }: { analysesResults: A
|
|||||||
<AnalysesResultsDescription
|
<AnalysesResultsDescription
|
||||||
totalAnalysesResults={totalAnalysesResults}
|
totalAnalysesResults={totalAnalysesResults}
|
||||||
totalResults={totalResults} />
|
totalResults={totalResults} />
|
||||||
<ul className="vscode-codeql__analyses-results-list">
|
<ul className="vscode-codeql__flat-list">
|
||||||
{analysesResults.filter(a => a.results.length > 0).map(r =>
|
{analysesResults.filter(a => a.results.length > 0).map(r =>
|
||||||
<li key={r.nwo} className="vscode-codeql__analyses-results-list-item">
|
<li key={r.nwo} className="vscode-codeql__analyses-results-list-item">
|
||||||
<RepoAnalysisResults {...r} />
|
<RepoAnalysisResults {...r} />
|
||||||
|
|||||||
@@ -12,22 +12,10 @@
|
|||||||
padding-top: 1.5em;
|
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 {
|
.vscode-codeql__analysis-summaries-list-item {
|
||||||
margin-top: 0.5em;
|
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 {
|
.vscode-codeql__analyses-results-list-item {
|
||||||
padding-top: 0.5em;
|
padding-top: 0.5em;
|
||||||
}
|
}
|
||||||
@@ -55,3 +43,9 @@
|
|||||||
Liberation Mono, monospace;
|
Liberation Mono, monospace;
|
||||||
color: var(--vscode-editor-foreground);
|
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