Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NEW @W-17100356@ Add sarif formatter to output formats #117

Merged
merged 13 commits into from
Nov 11, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/code-analyzer-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"@salesforce/code-analyzer-engine-api": "0.12.0",
"@types/js-yaml": "^4.0.9",
"@types/node": "^20.0.0",
"@types/sarif": "^2.1.7",
"csv-stringify": "^6.5.0",
"js-yaml": "^4.1.0",
"xmlbuilder": "^15.1.1"
Expand Down
138 changes: 135 additions & 3 deletions packages/code-analyzer-core/src/output-format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import * as xmlbuilder from "xmlbuilder";
import * as fs from 'fs';
import path from "node:path";
import {Clock, RealClock} from "./utils";
import * as sarif from "sarif";

export enum OutputFormat {
CSV = "CSV",
JSON = "JSON",
XML = "XML",
HTML = "HTML"
HTML = "HTML",
SARIF = "SARIF"
}

export abstract class OutputFormatter {
Expand All @@ -27,6 +29,8 @@ export abstract class OutputFormatter {
return new XmlOutputFormatter();
case OutputFormat.HTML:
return new HtmlOutputFormatter(clock);
case OutputFormat.SARIF:
return new SarifOutputFormatter();
default:
throw new Error(`Unsupported output format: ${format}`);
}
Expand Down Expand Up @@ -231,6 +235,130 @@ class XmlOutputFormatter implements OutputFormatter {
}
}

class SarifOutputFormatter implements OutputFormatter {

format(results: RunResults): string {
const resultsByEngine = this.groupViolationsByEngine(results);
const sarifRuns = Array.from(resultsByEngine.entries()).map(([engine, violations]) =>
this.createSarifRun(engine, violations, results)
);
const sarifLog = this.createSarifLog(sarifRuns);
return JSON.stringify(sarifLog, null, 2);
}

private groupViolationsByEngine(results: RunResults): Map<string, Violation[]> {
const resultsByEngine = new Map<string, Violation[]>();
for (const engine of results.getEngineNames()) {
resultsByEngine.set(engine, []);
}
for (const violation of results.getViolations()) {
resultsByEngine.get(violation.getRule().getEngineName())?.push(violation);
}
return resultsByEngine;
}
Copy link
Collaborator

@stephen-carter-at-sf stephen-carter-at-sf Nov 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again this isn't needed. The violations are already grouped by engine with results.getEngineRunResults(engineName)


private getRelatedLocations(violation: Violation): sarif.Location[] {
if (!typeSupportsMultipleLocations(violation.getRule().getType())) return [];

return violation.getCodeLocations().map(location => ({
physicalLocation: {
artifactLocation: { uri: location.getFile() },
region: {
startLine: location.getStartLine(),
startColumn: location.getStartColumn(),
endLine: location.getEndLine(),
endColumn: location.getEndColumn(),
} as sarif.Region
}
}));
}

private createSarifRun(engine: string, violations: Violation[], results: RunResults): sarif.Run {
const ruleMap = new Map<string, number>();
const rules = this.populateRuleMap(violations, ruleMap);
const sarifResults = violations.map(violation => this.createSarifResult(violation, ruleMap));

return {
tool: {
driver: {
name: engine,
informationUri: "https://developer.salesforce.com/docs/platform/salesforce-code-analyzer/guide/version-5.html",
rules: rules,
}
},
results: sarifResults,
invocations: [
{
executionSuccessful: true,
workingDirectory: { uri: results.getRunDirectory() },
},
],
};
}

private createSarifResult(violation: Violation, ruleMap: Map<string, number>): sarif.Result {
const primaryLocation = violation.getCodeLocations()[violation.getPrimaryLocationIndex()];
const location: sarif.Location = {
physicalLocation: {
artifactLocation: { uri: primaryLocation.getFile() },
region: {
startLine: primaryLocation.getStartLine(),
startColumn: primaryLocation.getStartColumn(),
endLine: primaryLocation.getEndLine(),
endColumn: primaryLocation.getEndColumn()
} as sarif.Region
}
};

const relatedLocations = this.getRelatedLocations(violation);

return {
ruleId: violation.getRule().getName(),
ruleIndex: ruleMap.get(violation.getRule().getName()),
message: { text: violation.getMessage() },
locations: [location],
...(relatedLocations.length > 0 && { relatedLocations }),
level: this.getLevel(violation.getRule().getSeverityLevel()),
};
}

private createSarifLog(sarifRuns: sarif.Run[]): sarif.Log {
return {
version: "2.1.0",
$schema: 'http://json.schemastore.org/sarif-2.1.0',
runs: sarifRuns,
};
}

private getLevel(ruleViolation: number): sarif.Notification.level {
return ruleViolation < 3 ? 'error' : 'warning';
}

private populateRuleMap(violations: Violation[], ruleMap: Map<string, number>): sarif.ReportingDescriptor[] {
const rules: sarif.ReportingDescriptor[] = [];
for (const v of violations) {
if (!ruleMap.has(v.getRule().getName())) {
ruleMap.set(v.getRule().getName(), ruleMap.size);
const rule = {
id: v.getRule().getName(),
properties: {
category: v.getRule().getTags(),
severity: v.getRule().getSeverityLevel()
},
helpUri: ''
};
if (v.getResourceUrls()) {
rule['helpUri'] = v.getResourceUrls()[0];
}
rules.push(rule);
}
}

return rules;
}
}


const HTML_TEMPLATE_VERSION: string = '0.0.1';
const HTML_TEMPLATE_FILE: string = path.resolve(__dirname, '..', 'output-templates', `html-template-${HTML_TEMPLATE_VERSION}.txt`);
class HtmlOutputFormatter implements OutputFormatter {
Expand Down Expand Up @@ -293,13 +421,17 @@ function createViolationOutput(violation: Violation, runDir: string, sanitizeFcn
column: primaryLocation.getStartColumn(),
endLine: primaryLocation.getEndLine(),
endColumn: primaryLocation.getEndColumn(),
primaryLocationIndex: [RuleType.DataFlow, RuleType.Flow].includes(rule.getType()) ? violation.getPrimaryLocationIndex() : undefined,
locations: [RuleType.DataFlow, RuleType.Flow].includes(rule.getType()) ? createCodeLocationOutputs(codeLocations, runDir) : undefined,
primaryLocationIndex: typeSupportsMultipleLocations(rule.getType()) ? violation.getPrimaryLocationIndex() : undefined,
locations: typeSupportsMultipleLocations(rule.getType()) ? createCodeLocationOutputs(codeLocations, runDir) : undefined,
message: sanitizeFcn(violation.getMessage()),
resources: violation.getResourceUrls()
};
}

function typeSupportsMultipleLocations(ruleType: RuleType) {
return [RuleType.DataFlow, RuleType.Flow, RuleType.MultiLocation].includes(ruleType);
}

function createCodeLocationOutputs(codeLocations: CodeLocation[], runDir: string): CodeLocationOutput[] {
return codeLocations.map(loc => {
return new CodeLocationOutput(loc, runDir);
Expand Down
22 changes: 22 additions & 0 deletions packages/code-analyzer-core/test/output-format.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,28 @@ describe("Tests for the XML output format", () => {
});
});

describe("Tests for the SARIF output format", () => {
it("When an empty result is provided, we create a sarif text with summary having zeros", () => {
const results: RunResults = new RunResultsImpl();
const formattedText: string = results.toFormattedOutput(OutputFormat.SARIF);
const expectedText: string = getContentsOfExpectedOutputFile('zeroViolations.goldfile.sarif', true, true);
expect(formattedText).toEqual(expectedText);
});

it("When results contain multiple violations , we create sarif text correctly", () => {
const formattedText: string = runResults.toFormattedOutput(OutputFormat.SARIF);
const expectedText: string = getContentsOfExpectedOutputFile('multipleViolations.goldfile.sarif', true, true);
expect(formattedText).toEqual(expectedText);
});

it("When results contain violation of type UnexpectedError, we create sarif text correctly", async () => {
const resultsWithUnexpectedError: RunResults = await createResultsWithUnexpectedError();
const formattedText: string = resultsWithUnexpectedError.toFormattedOutput(OutputFormat.SARIF);
const expectedText: string = getContentsOfExpectedOutputFile('unexpectedEngineErrorViolation.goldfile.sarif', true, true);
expect(formattedText).toEqual(expectedText);
});
});

describe("Other misc output formatting tests", () => {
it("When an output format is not supported, then we error", () => {
// This test is just a sanity check in case we add in an output format in the future without updating the
Expand Down
Loading