Skip to content

Commit 5444607

Browse files
authored
NEW @W-17100356@ Add sarif formatter to output formats (#117)
1 parent 28c73a7 commit 5444607

File tree

7 files changed

+492
-4
lines changed

7 files changed

+492
-4
lines changed

package-lock.json

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/code-analyzer-core/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"@salesforce/code-analyzer-engine-api": "0.12.0",
1717
"@types/js-yaml": "^4.0.9",
1818
"@types/node": "^20.0.0",
19+
"@types/sarif": "^2.1.7",
1920
"csv-stringify": "^6.5.0",
2021
"js-yaml": "^4.1.0",
2122
"xmlbuilder": "^15.1.1"

packages/code-analyzer-core/src/output-format.ts

Lines changed: 105 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
1-
import {CodeLocation, RunResults, Violation} from "./results";
1+
import {CodeLocation, RunResults, Violation, EngineRunResults} from "./results";
22
import {Rule, RuleType, SeverityLevel} from "./rules";
33
import {stringify as stringifyToCsv} from "csv-stringify/sync";
44
import {Options as CsvOptions} from "csv-stringify";
55
import * as xmlbuilder from "xmlbuilder";
66
import * as fs from 'fs';
77
import path from "node:path";
88
import {Clock, RealClock} from "./utils";
9+
import * as sarif from "sarif";
910

1011
export enum OutputFormat {
1112
CSV = "CSV",
1213
JSON = "JSON",
1314
XML = "XML",
14-
HTML = "HTML"
15+
HTML = "HTML",
16+
SARIF = "SARIF"
1517
}
1618

1719
export abstract class OutputFormatter {
@@ -27,6 +29,8 @@ export abstract class OutputFormatter {
2729
return new XmlOutputFormatter();
2830
case OutputFormat.HTML:
2931
return new HtmlOutputFormatter(clock);
32+
case OutputFormat.SARIF:
33+
return new SarifOutputFormatter();
3034
default:
3135
throw new Error(`Unsupported output format: ${format}`);
3236
}
@@ -231,6 +235,99 @@ class XmlOutputFormatter implements OutputFormatter {
231235
}
232236
}
233237

238+
class SarifOutputFormatter implements OutputFormatter {
239+
format(results: RunResults): string {
240+
const runDir = results.getRunDirectory();
241+
242+
const sarifRuns: sarif.Run[] = results.getEngineNames()
243+
.map(engineName => results.getEngineRunResults(engineName))
244+
.filter(engineRunResults => engineRunResults.getViolationCount() > 0)
245+
.map(engineRunResults => toSarifRun(engineRunResults, runDir));
246+
247+
// Construct SARIF log
248+
const sarifLog: sarif.Log = {
249+
version: "2.1.0",
250+
$schema: 'http://json.schemastore.org/sarif-2.1.0',
251+
runs: sarifRuns,
252+
};
253+
254+
// Return formatted SARIF JSON string
255+
return JSON.stringify(sarifLog, null, 2);
256+
}
257+
}
258+
259+
function toSarifRun(engineRunResults: EngineRunResults, runDir: string): sarif.Run {
260+
const violations: Violation[] = engineRunResults.getViolations();
261+
const rules: Rule[] = [... new Set(violations.map(v => v.getRule()))];
262+
const ruleNames: string[] = rules.map(r => r.getName());
263+
264+
return {
265+
tool: {
266+
driver: {
267+
name: engineRunResults.getEngineName(),
268+
informationUri: "https://developer.salesforce.com/docs/platform/salesforce-code-analyzer/guide/version-5.html",
269+
rules: rules.map(toSarifReportingDescriptor),
270+
}
271+
},
272+
results: violations.map(v => toSarifResult(v, ruleNames.indexOf(v.getRule().getName()))),
273+
invocations: [
274+
{
275+
executionSuccessful: true,
276+
workingDirectory: {
277+
uri: runDir,
278+
},
279+
},
280+
],
281+
};
282+
}
283+
284+
function toSarifResult(violation: Violation, ruleIndex: number) : sarif.Result {
285+
const primaryCodeLocation = violation.getCodeLocations()[violation.getPrimaryLocationIndex()];
286+
const result: sarif.Result = {
287+
ruleId: violation.getRule().getName(),
288+
ruleIndex: ruleIndex,
289+
message: { text: violation.getMessage() },
290+
locations: [toSarifLocation(primaryCodeLocation)],
291+
};
292+
if(typeSupportsMultipleLocations(violation.getRule().getType())) {
293+
result.relatedLocations = violation.getCodeLocations().map(toSarifLocation);
294+
}
295+
result.level = toSarifNotificationLevel(violation.getRule().getSeverityLevel());
296+
return result;
297+
}
298+
299+
function toSarifLocation(codeLocation: CodeLocation): sarif.Location {
300+
return {
301+
physicalLocation: {
302+
artifactLocation: {
303+
uri: codeLocation.getFile(),
304+
},
305+
region: {
306+
startLine: codeLocation.getStartLine(),
307+
startColumn: codeLocation.getStartColumn(),
308+
endLine: codeLocation.getEndLine(),
309+
endColumn: codeLocation.getEndColumn()
310+
} as sarif.Region
311+
}
312+
}
313+
}
314+
315+
function toSarifReportingDescriptor(rule: Rule): sarif.ReportingDescriptor {
316+
return {
317+
id: rule.getName(),
318+
properties: {
319+
category: rule.getTags(),
320+
severity: rule.getSeverityLevel()
321+
},
322+
...(rule.getResourceUrls()?.[0] && { helpUri: rule.getResourceUrls()[0] })
323+
}
324+
}
325+
326+
function toSarifNotificationLevel(severity: SeverityLevel): sarif.Notification.level {
327+
return severity < 3 ? 'error' : 'warning'; // IF satif.Notification.level is an enum then please return the num instead of the string.
328+
}
329+
330+
234331
const HTML_TEMPLATE_VERSION: string = '0.0.1';
235332
const HTML_TEMPLATE_FILE: string = path.resolve(__dirname, '..', 'output-templates', `html-template-${HTML_TEMPLATE_VERSION}.txt`);
236333
class HtmlOutputFormatter implements OutputFormatter {
@@ -293,13 +390,17 @@ function createViolationOutput(violation: Violation, runDir: string, sanitizeFcn
293390
column: primaryLocation.getStartColumn(),
294391
endLine: primaryLocation.getEndLine(),
295392
endColumn: primaryLocation.getEndColumn(),
296-
primaryLocationIndex: [RuleType.DataFlow, RuleType.Flow].includes(rule.getType()) ? violation.getPrimaryLocationIndex() : undefined,
297-
locations: [RuleType.DataFlow, RuleType.Flow].includes(rule.getType()) ? createCodeLocationOutputs(codeLocations, runDir) : undefined,
393+
primaryLocationIndex: typeSupportsMultipleLocations(rule.getType()) ? violation.getPrimaryLocationIndex() : undefined,
394+
locations: typeSupportsMultipleLocations(rule.getType()) ? createCodeLocationOutputs(codeLocations, runDir) : undefined,
298395
message: sanitizeFcn(violation.getMessage()),
299396
resources: violation.getResourceUrls()
300397
};
301398
}
302399

400+
function typeSupportsMultipleLocations(ruleType: RuleType) {
401+
return [RuleType.DataFlow, RuleType.Flow, RuleType.MultiLocation].includes(ruleType);
402+
}
403+
303404
function createCodeLocationOutputs(codeLocations: CodeLocation[], runDir: string): CodeLocationOutput[] {
304405
return codeLocations.map(loc => {
305406
return new CodeLocationOutput(loc, runDir);

packages/code-analyzer-core/test/output-format.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,28 @@ describe("Tests for the XML output format", () => {
116116
});
117117
});
118118

119+
describe("Tests for the SARIF output format", () => {
120+
it("When an empty result is provided, we create a sarif text with summary having zeros", () => {
121+
const results: RunResults = new RunResultsImpl();
122+
const formattedText: string = results.toFormattedOutput(OutputFormat.SARIF);
123+
const expectedText: string = getContentsOfExpectedOutputFile('zeroViolations.goldfile.sarif', true, true);
124+
expect(formattedText).toEqual(expectedText);
125+
});
126+
127+
it("When results contain multiple violations , we create sarif text correctly", () => {
128+
const formattedText: string = runResults.toFormattedOutput(OutputFormat.SARIF);
129+
const expectedText: string = getContentsOfExpectedOutputFile('multipleViolations.goldfile.sarif', true, true);
130+
expect(formattedText).toEqual(expectedText);
131+
});
132+
133+
it("When results contain violation of type UnexpectedError, we create sarif text correctly", async () => {
134+
const resultsWithUnexpectedError: RunResults = await createResultsWithUnexpectedError();
135+
const formattedText: string = resultsWithUnexpectedError.toFormattedOutput(OutputFormat.SARIF);
136+
const expectedText: string = getContentsOfExpectedOutputFile('unexpectedEngineErrorViolation.goldfile.sarif', true, true);
137+
expect(formattedText).toEqual(expectedText);
138+
});
139+
});
140+
119141
describe("Other misc output formatting tests", () => {
120142
it("When an output format is not supported, then we error", () => {
121143
// This test is just a sanity check in case we add in an output format in the future without updating the

0 commit comments

Comments
 (0)