Skip to content

Commit c609377

Browse files
authored
Add SARIF processing and basic alert rendering (#1171)
1 parent c18f795 commit c609377

8 files changed

Lines changed: 1018 additions & 38 deletions

File tree

extensions/ql-vscode/src/remote-queries/analyses-results-manager.ts

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ import { Credentials } from '../authentication';
66
import { Logger } from '../logging';
77
import { downloadArtifactFromLink } from './gh-actions-api-client';
88
import { AnalysisSummary } from './shared/remote-query-result';
9-
import { AnalysisResults, QueryResult } from './shared/analysis-result';
9+
import { AnalysisResults, AnalysisAlert } from './shared/analysis-result';
1010
import { UserCancellationException } from '../commandRunner';
1111
import { sarifParser } from '../sarif-parser';
12+
import { extractAnalysisAlerts } from './sarif-processing';
1213

1314
export class AnalysesResultsManager {
1415
// Store for the results of various analyses for each remote query.
@@ -136,26 +137,15 @@ export class AnalysesResultsManager {
136137
void publishResults([...resultsForQuery]);
137138
}
138139

139-
private async readResults(filePath: string): Promise<QueryResult[]> {
140-
const queryResults: QueryResult[] = [];
141-
140+
private async readResults(filePath: string): Promise<AnalysisAlert[]> {
142141
const sarifLog = await sarifParser(filePath);
143142

144-
// Read the sarif file and extract information that we want to display
145-
// in the UI. For now we're only getting the message texts but we'll gradually
146-
// extract more information based on the UX we want to build.
147-
148-
sarifLog.runs?.forEach(run => {
149-
run?.results?.forEach(result => {
150-
if (result?.message?.text) {
151-
queryResults.push({
152-
message: result.message.text
153-
});
154-
}
155-
});
156-
});
157-
158-
return queryResults;
143+
const processedSarif = extractAnalysisAlerts(sarifLog);
144+
if (processedSarif.errors) {
145+
void this.logger.log(`Error processing SARIF file: ${os.EOL}${processedSarif.errors.join(os.EOL)}`);
146+
}
147+
148+
return processedSarif.alerts;
159149
}
160150

161151
private isAnalysisInMemory(analysis: AnalysisSummary): boolean {

extensions/ql-vscode/src/remote-queries/sample-data.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,23 @@ export const sampleRemoteQueryResult: RemoteQueryResult = {
9999
};
100100

101101

102-
const createAnalysisResults = (n: number) => Array(n).fill({ 'message': 'Sample text' });
102+
const createAnalysisResults = (n: number) => Array(n).fill(
103+
{
104+
message: 'This shell command depends on an uncontrolled [absolute path](1).',
105+
severity: 'Error',
106+
filePath: 'npm-packages/meteor-installer/config.js',
107+
codeSnippet: {
108+
startLine: 253,
109+
endLine: 257,
110+
text: ' if (isWindows()) {\n //set for the current session and beyond\n child_process.execSync(`setx path "${meteorPath}/;%path%`);\n return;\n }\n',
111+
},
112+
highlightedRegion: {
113+
startLine: 255,
114+
startColumn: 28,
115+
endColumn: 62
116+
}
117+
}
118+
);
103119

104120
export const sampleAnalysesResultsStage1: AnalysisResults[] = [
105121
{
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
import * as sarif from 'sarif';
2+
3+
import { AnalysisAlert, ResultSeverity } from './shared/analysis-result';
4+
5+
const defaultSeverity = 'Warning';
6+
7+
export function extractAnalysisAlerts(
8+
sarifLog: sarif.Log
9+
): {
10+
alerts: AnalysisAlert[],
11+
errors: string[]
12+
} {
13+
if (!sarifLog) {
14+
return { alerts: [], errors: ['No SARIF log was found'] };
15+
}
16+
17+
if (!sarifLog.runs) {
18+
return { alerts: [], errors: ['No runs found in the SARIF file'] };
19+
}
20+
21+
const errors: string[] = [];
22+
const alerts: AnalysisAlert[] = [];
23+
24+
for (const run of sarifLog.runs) {
25+
if (!run.results) {
26+
errors.push('No results found in the SARIF run');
27+
continue;
28+
}
29+
30+
for (const result of run.results) {
31+
const message = result.message?.text;
32+
if (!message) {
33+
errors.push('No message found in the SARIF result');
34+
continue;
35+
}
36+
37+
const severity = tryGetSeverity(run, result) || defaultSeverity;
38+
39+
if (!result.locations) {
40+
errors.push('No locations found in the SARIF result');
41+
continue;
42+
}
43+
44+
for (const location of result.locations) {
45+
const contextRegion = location.physicalLocation?.contextRegion;
46+
if (!contextRegion) {
47+
errors.push('No context region found in the SARIF result location');
48+
continue;
49+
}
50+
if (contextRegion.startLine === undefined) {
51+
errors.push('No start line set for a result context region');
52+
continue;
53+
}
54+
if (contextRegion.endLine === undefined) {
55+
errors.push('No end line set for a result context region');
56+
continue;
57+
}
58+
if (!contextRegion.snippet?.text) {
59+
errors.push('No text set for a result context region');
60+
continue;
61+
}
62+
63+
const region = location.physicalLocation?.region;
64+
if (!region) {
65+
errors.push('No region found in the SARIF result location');
66+
continue;
67+
}
68+
if (region.startLine === undefined) {
69+
errors.push('No start line set for a result region');
70+
continue;
71+
}
72+
if (region.startColumn === undefined) {
73+
errors.push('No start column set for a result region');
74+
continue;
75+
}
76+
if (region.endColumn === undefined) {
77+
errors.push('No end column set for a result region');
78+
continue;
79+
}
80+
81+
const filePath = location.physicalLocation?.artifactLocation?.uri;
82+
if (!filePath) {
83+
errors.push('No file path found in the SARIF result location');
84+
continue;
85+
}
86+
87+
const analysisAlert = {
88+
message,
89+
filePath,
90+
severity,
91+
codeSnippet: {
92+
startLine: contextRegion.startLine,
93+
endLine: contextRegion.endLine,
94+
text: contextRegion.snippet.text
95+
},
96+
highlightedRegion: {
97+
startLine: region.startLine,
98+
startColumn: region.startColumn,
99+
endLine: region.endLine,
100+
endColumn: region.endColumn
101+
}
102+
};
103+
104+
const validationErrors = getAlertValidationErrors(analysisAlert);
105+
if (validationErrors.length > 0) {
106+
errors.push(...validationErrors);
107+
continue;
108+
}
109+
110+
alerts.push(analysisAlert);
111+
}
112+
}
113+
}
114+
115+
return { alerts, errors };
116+
}
117+
118+
export function tryGetSeverity(
119+
sarifRun: sarif.Run,
120+
result: sarif.Result
121+
): ResultSeverity | undefined {
122+
if (!sarifRun || !result) {
123+
return undefined;
124+
}
125+
126+
const rule = tryGetRule(sarifRun, result);
127+
if (!rule) {
128+
return undefined;
129+
}
130+
131+
const severity = rule.properties?.['problem.severity'];
132+
if (!severity) {
133+
return undefined;
134+
}
135+
136+
switch (severity.toLowerCase()) {
137+
case 'recommendation':
138+
return 'Recommendation';
139+
case 'warning':
140+
return 'Warning';
141+
case 'error':
142+
return 'Error';
143+
}
144+
145+
return undefined;
146+
}
147+
148+
export function tryGetRule(
149+
sarifRun: sarif.Run,
150+
result: sarif.Result
151+
): sarif.ReportingDescriptor | undefined {
152+
if (!sarifRun || !result) {
153+
return undefined;
154+
}
155+
156+
const resultRule = result.rule;
157+
if (!resultRule) {
158+
return undefined;
159+
}
160+
161+
// The rule can found in two places:
162+
// - Either in the run's tool driver tool component
163+
// - Or in the run's tool extensions tool component
164+
165+
const ruleId = resultRule.id;
166+
if (ruleId) {
167+
const rule = sarifRun.tool.driver.rules?.find(r => r.id === ruleId);
168+
if (rule) {
169+
return rule;
170+
}
171+
}
172+
173+
const ruleIndex = resultRule.index;
174+
if (ruleIndex != undefined) {
175+
const toolComponentIndex = result.rule?.toolComponent?.index;
176+
const toolExtensions = sarifRun.tool.extensions;
177+
if (toolComponentIndex !== undefined && toolExtensions !== undefined) {
178+
const toolComponent = toolExtensions[toolComponentIndex];
179+
if (toolComponent?.rules !== undefined) {
180+
return toolComponent.rules[ruleIndex];
181+
}
182+
}
183+
}
184+
185+
// Couldn't find the rule.
186+
return undefined;
187+
}
188+
189+
function getAlertValidationErrors(alert: AnalysisAlert): string[] {
190+
const errors = [];
191+
192+
if (alert.codeSnippet.startLine > alert.codeSnippet.endLine) {
193+
errors.push('The code snippet start line is greater than the end line');
194+
}
195+
196+
const highlightedRegion = alert.highlightedRegion;
197+
if (highlightedRegion.endLine === highlightedRegion.startLine &&
198+
highlightedRegion.endColumn < highlightedRegion.startColumn) {
199+
errors.push('The highlighted region end column is greater than the start column');
200+
}
201+
202+
return errors;
203+
}

extensions/ql-vscode/src/remote-queries/shared/analysis-result.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,28 @@ export type AnalysisResultStatus = 'InProgress' | 'Completed' | 'Failed';
33
export interface AnalysisResults {
44
nwo: string;
55
status: AnalysisResultStatus;
6-
results: QueryResult[];
6+
results: AnalysisAlert[];
77
}
88

9-
export interface QueryResult {
10-
message?: string;
9+
export interface AnalysisAlert {
10+
message: string;
11+
severity: ResultSeverity;
12+
filePath: string;
13+
codeSnippet: CodeSnippet
14+
highlightedRegion: HighlightedRegion
1115
}
16+
17+
export interface CodeSnippet {
18+
startLine: number;
19+
endLine: number;
20+
text: string;
21+
}
22+
23+
export interface HighlightedRegion {
24+
startLine: number;
25+
startColumn: number;
26+
endLine: number | undefined;
27+
endColumn: number;
28+
}
29+
30+
export type ResultSeverity = 'Recommendation' | 'Warning' | 'Error';

0 commit comments

Comments
 (0)