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 all 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
109 changes: 105 additions & 4 deletions packages/code-analyzer-core/src/output-format.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import {CodeLocation, RunResults, Violation} from "./results";
import {CodeLocation, RunResults, Violation, EngineRunResults} from "./results";
import {Rule, RuleType, SeverityLevel} from "./rules";
import {stringify as stringifyToCsv} from "csv-stringify/sync";
import {Options as CsvOptions} from "csv-stringify";
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,99 @@ class XmlOutputFormatter implements OutputFormatter {
}
}

class SarifOutputFormatter implements OutputFormatter {
format(results: RunResults): string {
const runDir = results.getRunDirectory();

const sarifRuns: sarif.Run[] = results.getEngineNames()
.map(engineName => results.getEngineRunResults(engineName))
.filter(engineRunResults => engineRunResults.getViolationCount() > 0)
.map(engineRunResults => toSarifRun(engineRunResults, runDir));

// Construct SARIF log
const sarifLog: sarif.Log = {
version: "2.1.0",
$schema: 'http://json.schemastore.org/sarif-2.1.0',
runs: sarifRuns,
};

// Return formatted SARIF JSON string
return JSON.stringify(sarifLog, null, 2);
}
}

function toSarifRun(engineRunResults: EngineRunResults, runDir: string): sarif.Run {
const violations: Violation[] = engineRunResults.getViolations();
const rules: Rule[] = [... new Set(violations.map(v => v.getRule()))];
const ruleNames: string[] = rules.map(r => r.getName());

return {
tool: {
driver: {
name: engineRunResults.getEngineName(),
informationUri: "https://developer.salesforce.com/docs/platform/salesforce-code-analyzer/guide/version-5.html",
rules: rules.map(toSarifReportingDescriptor),
}
},
results: violations.map(v => toSarifResult(v, ruleNames.indexOf(v.getRule().getName()))),
invocations: [
{
executionSuccessful: true,
workingDirectory: {
uri: runDir,
},
},
],
};
}

function toSarifResult(violation: Violation, ruleIndex: number) : sarif.Result {
const primaryCodeLocation = violation.getCodeLocations()[violation.getPrimaryLocationIndex()];
const result: sarif.Result = {
ruleId: violation.getRule().getName(),
ruleIndex: ruleIndex,
message: { text: violation.getMessage() },
locations: [toSarifLocation(primaryCodeLocation)],
};
if(typeSupportsMultipleLocations(violation.getRule().getType())) {
result.relatedLocations = violation.getCodeLocations().map(toSarifLocation);
}
result.level = toSarifNotificationLevel(violation.getRule().getSeverityLevel());
return result;
}

function toSarifLocation(codeLocation: CodeLocation): sarif.Location {
return {
physicalLocation: {
artifactLocation: {
uri: codeLocation.getFile(),
},
region: {
startLine: codeLocation.getStartLine(),
startColumn: codeLocation.getStartColumn(),
endLine: codeLocation.getEndLine(),
endColumn: codeLocation.getEndColumn()
} as sarif.Region
}
}
}

function toSarifReportingDescriptor(rule: Rule): sarif.ReportingDescriptor {
return {
id: rule.getName(),
properties: {
category: rule.getTags(),
severity: rule.getSeverityLevel()
},
...(rule.getResourceUrls()?.[0] && { helpUri: rule.getResourceUrls()[0] })
}
}

function toSarifNotificationLevel(severity: SeverityLevel): sarif.Notification.level {
return severity < 3 ? 'error' : 'warning'; // IF satif.Notification.level is an enum then please return the num instead of the string.
}


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 +390,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