821 lines
22 KiB
TypeScript
821 lines
22 KiB
TypeScript
import type {
|
|
Log,
|
|
PhysicalLocation,
|
|
ReportingDescriptor,
|
|
Result,
|
|
Run,
|
|
} from "sarif";
|
|
import {
|
|
extractAnalysisAlerts,
|
|
tryGetFilePath,
|
|
tryGetRule,
|
|
tryGetSeverity,
|
|
} from "../../src/variant-analysis/sarif-processing";
|
|
import type {
|
|
AnalysisMessage,
|
|
AnalysisMessageLocationToken,
|
|
} from "../../src/variant-analysis/shared/analysis-result";
|
|
|
|
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 Result;
|
|
|
|
const sarifRun = {
|
|
results: [result],
|
|
} as Run;
|
|
|
|
const rule = tryGetRule(sarifRun, result);
|
|
|
|
expect(rule).toBeUndefined();
|
|
});
|
|
|
|
it("should return undefined if rule missing from tool driver", () => {
|
|
const result = {
|
|
message: "msg",
|
|
rule: {
|
|
id: "NonExistentRule",
|
|
},
|
|
} as Result;
|
|
|
|
const sarifRun = {
|
|
results: [result],
|
|
tool: {
|
|
driver: {
|
|
rules: [
|
|
// No rule with id 'NonExistentRule' is set here.
|
|
{
|
|
id: "A",
|
|
},
|
|
{
|
|
id: "B",
|
|
},
|
|
],
|
|
},
|
|
},
|
|
} as Run;
|
|
|
|
const rule = tryGetRule(sarifRun, result);
|
|
|
|
expect(rule).toBeUndefined();
|
|
});
|
|
|
|
it("should return rule if it has been set on the tool driver", () => {
|
|
const result = {
|
|
message: "msg",
|
|
rule: {
|
|
id: "B",
|
|
},
|
|
} as Result;
|
|
|
|
const sarifRun = {
|
|
results: [result],
|
|
tool: {
|
|
driver: {
|
|
rules: [
|
|
{
|
|
id: "A",
|
|
},
|
|
result.rule,
|
|
],
|
|
},
|
|
},
|
|
} as Run;
|
|
|
|
const rule = tryGetRule(sarifRun, result);
|
|
|
|
expect(rule).toBeTruthy();
|
|
expect(rule!.id).toBe(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 Result;
|
|
|
|
const sarifRun = {
|
|
results: [result],
|
|
tool: {
|
|
extensions: [
|
|
{
|
|
name: "foo",
|
|
rules: [
|
|
{
|
|
id: "A",
|
|
},
|
|
{
|
|
id: "B",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: "bar",
|
|
rules: [
|
|
{
|
|
id: "C",
|
|
},
|
|
{
|
|
id: "D",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
} as Run;
|
|
|
|
const rule = tryGetRule(sarifRun, result);
|
|
|
|
expect(rule).toBeUndefined();
|
|
});
|
|
|
|
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 Result;
|
|
|
|
const sarifRun = {
|
|
results: [result],
|
|
tool: {
|
|
extensions: [
|
|
{
|
|
name: "foo",
|
|
rules: [
|
|
{
|
|
id: "A",
|
|
},
|
|
{
|
|
id: "B",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: "bar",
|
|
rules: [
|
|
{
|
|
id: "C",
|
|
},
|
|
{
|
|
id: "D",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
} as Run;
|
|
|
|
const rule = tryGetRule(sarifRun, result);
|
|
|
|
expect(rule).toBeUndefined();
|
|
});
|
|
|
|
it("should return undefined if tool extensions not set", () => {
|
|
const result = {
|
|
message: "msg",
|
|
rule: {
|
|
index: 1,
|
|
toolComponent: {
|
|
index: 1,
|
|
},
|
|
},
|
|
} as Result;
|
|
|
|
const sarifRun = {
|
|
results: [result],
|
|
tool: {
|
|
// Extensions should be set here.
|
|
},
|
|
} as Run;
|
|
|
|
const rule = tryGetRule(sarifRun, result);
|
|
|
|
expect(rule).toBeUndefined();
|
|
});
|
|
|
|
it("should return undefined if tool extensions do not contain index", () => {
|
|
const result = {
|
|
message: "msg",
|
|
rule: {
|
|
index: 1,
|
|
toolComponent: {
|
|
index: 1,
|
|
},
|
|
},
|
|
} as Result;
|
|
|
|
const sarifRun = {
|
|
results: [result],
|
|
tool: {
|
|
extensions: [
|
|
{
|
|
name: "foo",
|
|
rules: [
|
|
{
|
|
id: "A",
|
|
},
|
|
{
|
|
id: "B",
|
|
},
|
|
],
|
|
},
|
|
// There should be one more extension here (index 1).
|
|
],
|
|
},
|
|
} as Run;
|
|
|
|
const rule = tryGetRule(sarifRun, result);
|
|
|
|
expect(rule).toBeUndefined();
|
|
});
|
|
|
|
it("should return rule if all information is defined", () => {
|
|
const result = {
|
|
message: "msg",
|
|
ruleIndex: 1,
|
|
rule: {
|
|
index: 1,
|
|
toolComponent: {
|
|
index: 1,
|
|
},
|
|
},
|
|
} as Result;
|
|
|
|
const sarifRun = {
|
|
results: [result],
|
|
tool: {
|
|
extensions: [
|
|
{
|
|
name: "foo",
|
|
rules: [
|
|
{
|
|
id: "A",
|
|
},
|
|
{
|
|
id: "B",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: "bar",
|
|
rules: [
|
|
{
|
|
id: "C",
|
|
},
|
|
{
|
|
id: "D",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
} as Run;
|
|
|
|
const rule = tryGetRule(sarifRun, result);
|
|
|
|
expect(rule).toBeTruthy();
|
|
expect(rule!.id).toBe("D");
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("tryGetFilePath", () => {
|
|
it("should return value when uri is a file path", () => {
|
|
const physicalLocation: PhysicalLocation = {
|
|
artifactLocation: {
|
|
uri: "foo/bar",
|
|
},
|
|
};
|
|
expect(tryGetFilePath(physicalLocation)).toBe("foo/bar");
|
|
});
|
|
|
|
it("should return undefined when uri has a file scheme", () => {
|
|
const physicalLocation: PhysicalLocation = {
|
|
artifactLocation: {
|
|
uri: "file:/",
|
|
},
|
|
};
|
|
expect(tryGetFilePath(physicalLocation)).toBe(undefined);
|
|
});
|
|
|
|
it("should return undefined when uri is empty", () => {
|
|
const physicalLocation: PhysicalLocation = {
|
|
artifactLocation: {
|
|
uri: "",
|
|
},
|
|
};
|
|
expect(tryGetFilePath(physicalLocation)).toBe(undefined);
|
|
});
|
|
|
|
it("should return undefined if artifact location uri is undefined", () => {
|
|
const physicalLocation: PhysicalLocation = {
|
|
artifactLocation: {
|
|
uri: undefined,
|
|
},
|
|
};
|
|
expect(tryGetFilePath(physicalLocation)).toBe(undefined);
|
|
});
|
|
|
|
it("should return undefined if artifact location is undefined", () => {
|
|
const physicalLocation: PhysicalLocation = {
|
|
artifactLocation: undefined,
|
|
};
|
|
expect(tryGetFilePath(physicalLocation)).toBe(undefined);
|
|
});
|
|
});
|
|
|
|
describe("tryGetSeverity", () => {
|
|
it("should return undefined if no rule set", () => {
|
|
const result = {
|
|
message: "msg",
|
|
} as Result;
|
|
|
|
// The rule should be set here.
|
|
const rule: ReportingDescriptor | undefined = undefined;
|
|
|
|
const sarifRun = {
|
|
results: [result],
|
|
} as Run;
|
|
|
|
const severity = tryGetSeverity(sarifRun, result, rule);
|
|
expect(severity).toBeUndefined();
|
|
});
|
|
|
|
it("should return undefined if severity not set on rule", () => {
|
|
const result = {
|
|
message: "msg",
|
|
rule: {
|
|
id: "A",
|
|
},
|
|
} as Result;
|
|
|
|
const rule = {
|
|
id: "A",
|
|
properties: {
|
|
// Severity not set
|
|
},
|
|
} as ReportingDescriptor;
|
|
|
|
const sarifRun = {
|
|
results: [result],
|
|
tool: {
|
|
driver: {
|
|
rules: [rule, result.rule],
|
|
},
|
|
},
|
|
} as Run;
|
|
|
|
const severity = tryGetSeverity(sarifRun, result, rule);
|
|
expect(severity).toBeUndefined();
|
|
});
|
|
|
|
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 Result;
|
|
|
|
const rule = {
|
|
id: "A",
|
|
properties: {
|
|
"problem.severity": sarifSeverity,
|
|
},
|
|
} as ReportingDescriptor;
|
|
|
|
const sarifRun = {
|
|
results: [result],
|
|
tool: {
|
|
driver: {
|
|
rules: [rule, result.rule],
|
|
},
|
|
},
|
|
} as Run;
|
|
|
|
const severity = tryGetSeverity(sarifRun, result, rule);
|
|
expect(severity).toBe(parsedSeverity);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("extractAnalysisAlerts", () => {
|
|
const fakefileLinkPrefix = "https://example.com";
|
|
it("should not return any results if no runs found in the SARIF", () => {
|
|
const sarif = {
|
|
// Runs are missing here.
|
|
} as Log;
|
|
|
|
const result = extractAnalysisAlerts(sarif, fakefileLinkPrefix);
|
|
|
|
expect(result).toBeTruthy();
|
|
expect(result.alerts.length).toBe(0);
|
|
});
|
|
|
|
it("should not return any results for runs that have no results", () => {
|
|
const sarif = {
|
|
runs: [
|
|
{
|
|
results: [],
|
|
},
|
|
{
|
|
// Results are missing here.
|
|
},
|
|
],
|
|
} as Log;
|
|
|
|
const result = extractAnalysisAlerts(sarif, fakefileLinkPrefix);
|
|
|
|
expect(result).toBeTruthy();
|
|
expect(result.alerts.length).toBe(0);
|
|
});
|
|
|
|
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, fakefileLinkPrefix);
|
|
|
|
expect(result).toBeTruthy();
|
|
expect(result.errors.length).toBe(1);
|
|
expectResultParsingError(result.errors[0]);
|
|
});
|
|
|
|
it("should not return errors for result locations with no snippet", () => {
|
|
const sarif = buildValidSarifLog();
|
|
sarif.runs![0]!.results![0]!.locations![0]!.physicalLocation!.contextRegion!.snippet =
|
|
undefined;
|
|
|
|
const result = extractAnalysisAlerts(sarif, fakefileLinkPrefix);
|
|
|
|
const expectedCodeSnippet = {
|
|
startLine: result.alerts[0].codeSnippet!.startLine,
|
|
endLine: result.alerts[0].codeSnippet!.endLine,
|
|
text: "",
|
|
};
|
|
|
|
const actualCodeSnippet = result.alerts[0].codeSnippet;
|
|
|
|
expect(result).toBeTruthy();
|
|
expectNoParsingError(result);
|
|
expect(actualCodeSnippet).toEqual(expectedCodeSnippet);
|
|
});
|
|
|
|
it("should use highlightedRegion for result locations with no contextRegion", () => {
|
|
const sarif = buildValidSarifLog();
|
|
sarif.runs![0]!.results![0]!.locations![0]!.physicalLocation!.contextRegion =
|
|
undefined;
|
|
|
|
const result = extractAnalysisAlerts(sarif, fakefileLinkPrefix);
|
|
|
|
const expectedCodeSnippet = {
|
|
startLine: result.alerts[0].highlightedRegion!.startLine,
|
|
endLine: result.alerts[0].highlightedRegion!.endLine,
|
|
text: "",
|
|
};
|
|
|
|
const actualCodeSnippet = result.alerts[0].codeSnippet;
|
|
|
|
expect(result).toBeTruthy();
|
|
expectNoParsingError(result);
|
|
expect(actualCodeSnippet).toEqual(expectedCodeSnippet);
|
|
});
|
|
|
|
it("should not 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, fakefileLinkPrefix);
|
|
|
|
expect(result).toBeTruthy();
|
|
expect(result.alerts.length).toBe(1);
|
|
expectNoParsingError(result);
|
|
});
|
|
|
|
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, fakefileLinkPrefix);
|
|
|
|
expect(result).toBeTruthy();
|
|
expect(result.errors.length).toBe(1);
|
|
expectResultParsingError(result.errors[0]);
|
|
});
|
|
|
|
it("should return results for all alerts", () => {
|
|
const sarif = {
|
|
version: "0.0.1" as 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 Log;
|
|
|
|
const result = extractAnalysisAlerts(sarif, fakefileLinkPrefix);
|
|
expect(result).toBeTruthy();
|
|
expect(result.errors.length).toBe(0);
|
|
expect(result.alerts.length).toBe(3);
|
|
expect(
|
|
result.alerts.find(
|
|
(a) =>
|
|
getMessageText(a.message) === "msg1" &&
|
|
a.codeSnippet!.text === "foo",
|
|
),
|
|
).toBeTruthy();
|
|
expect(
|
|
result.alerts.find(
|
|
(a) =>
|
|
getMessageText(a.message) === "msg1" &&
|
|
a.codeSnippet!.text === "bar",
|
|
),
|
|
).toBeTruthy();
|
|
expect(
|
|
result.alerts.find(
|
|
(a) =>
|
|
getMessageText(a.message) === "msg2" &&
|
|
a.codeSnippet!.text === "baz",
|
|
),
|
|
).toBeTruthy();
|
|
expect(result.alerts.every((a) => a.severity === "Warning")).toBe(true);
|
|
});
|
|
|
|
it("should deal with complex messages", () => {
|
|
const sarif = buildValidSarifLog();
|
|
const messageText =
|
|
"This shell command depends on an uncontrolled [absolute path](1).";
|
|
sarif.runs![0]!.results![0]!.message!.text = messageText;
|
|
sarif.runs![0]!.results![0].relatedLocations = [
|
|
{
|
|
id: 1,
|
|
physicalLocation: {
|
|
artifactLocation: {
|
|
uri: "npm-packages/meteor-installer/config.js",
|
|
},
|
|
region: {
|
|
startLine: 35,
|
|
startColumn: 20,
|
|
endColumn: 60,
|
|
},
|
|
},
|
|
},
|
|
];
|
|
|
|
const result = extractAnalysisAlerts(sarif, fakefileLinkPrefix);
|
|
|
|
expect(result).toBeTruthy();
|
|
expect(result.errors.length).toBe(0);
|
|
expect(result.alerts.length).toBe(1);
|
|
const message = result.alerts[0].message;
|
|
expect(message.tokens.length).toBe(3);
|
|
expect(message.tokens[0].t).toBe("text");
|
|
expect(message.tokens[0].text).toBe(
|
|
"This shell command depends on an uncontrolled ",
|
|
);
|
|
expect(message.tokens[1].t).toBe("location");
|
|
expect(message.tokens[1].text).toBe("absolute path");
|
|
expect(
|
|
(message.tokens[1] as AnalysisMessageLocationToken).location,
|
|
).toEqual({
|
|
fileLink: {
|
|
fileLinkPrefix: fakefileLinkPrefix,
|
|
filePath: "npm-packages/meteor-installer/config.js",
|
|
},
|
|
highlightedRegion: {
|
|
startLine: 35,
|
|
startColumn: 20,
|
|
endLine: 35,
|
|
endColumn: 60,
|
|
},
|
|
});
|
|
expect(message.tokens[2].t).toBe("text");
|
|
expect(message.tokens[2].text).toBe(".");
|
|
});
|
|
|
|
it("should not include snippets for large snippets", () => {
|
|
const sarif = buildValidSarifLog();
|
|
// Build string of 10 kilobytes
|
|
const snippet = new Array(10 * 1024).fill("a").join("");
|
|
sarif.runs![0]!.results![0]!.locations![0]!.physicalLocation!.contextRegion!.snippet =
|
|
{
|
|
text: snippet,
|
|
};
|
|
|
|
const result = extractAnalysisAlerts(sarif, fakefileLinkPrefix);
|
|
|
|
const actualCodeSnippet = result.alerts[0].codeSnippet;
|
|
|
|
expect(result).toBeTruthy();
|
|
expectNoParsingError(result);
|
|
expect(actualCodeSnippet).toBeUndefined();
|
|
});
|
|
|
|
it("should include snippets for large snippets which are relevant", () => {
|
|
const sarif = buildValidSarifLog();
|
|
// Build string of 10 kilobytes
|
|
const snippet = new Array(10 * 1024).fill("a").join("");
|
|
sarif.runs![0]!.results![0]!.locations![0]!.physicalLocation!.contextRegion!.snippet =
|
|
{
|
|
text: snippet,
|
|
};
|
|
sarif.runs![0]!.results![0]!.locations![0]!.physicalLocation!.region!.endColumn = 1000;
|
|
|
|
const result = extractAnalysisAlerts(sarif, fakefileLinkPrefix);
|
|
|
|
const actualCodeSnippet = result.alerts[0].codeSnippet;
|
|
|
|
expect(result).toBeTruthy();
|
|
expectNoParsingError(result);
|
|
expect(actualCodeSnippet).not.toBeUndefined();
|
|
});
|
|
|
|
it("should be able to handle when a location has no uri", () => {
|
|
const sarif = buildValidSarifLog();
|
|
sarif.runs![0].results![0].message.text = "message [String](1)";
|
|
sarif.runs![0].results![0].relatedLocations = [
|
|
{
|
|
id: 1,
|
|
physicalLocation: {
|
|
artifactLocation: {
|
|
uri: "file:/modules/java.base/java/lang/String.class",
|
|
index: 1,
|
|
},
|
|
},
|
|
message: {
|
|
text: "String",
|
|
},
|
|
},
|
|
];
|
|
|
|
const result = extractAnalysisAlerts(sarif, fakefileLinkPrefix);
|
|
|
|
expect(result).toBeTruthy();
|
|
expectNoParsingError(result);
|
|
expect(result.alerts[0].codeSnippet).not.toBeUndefined();
|
|
expect(result.alerts[0].message.tokens).toStrictEqual([
|
|
{
|
|
t: "text",
|
|
text: "message ",
|
|
},
|
|
{
|
|
t: "text",
|
|
text: "String",
|
|
},
|
|
{
|
|
t: "text",
|
|
text: "",
|
|
},
|
|
]);
|
|
});
|
|
});
|
|
|
|
function expectResultParsingError(msg: string) {
|
|
expect(msg.startsWith("Error when processing SARIF result")).toBe(true);
|
|
}
|
|
|
|
function expectNoParsingError(result: { errors: string[] }) {
|
|
const array = result.errors;
|
|
expect(array).toEqual([]);
|
|
}
|
|
|
|
function buildValidSarifLog(): Log {
|
|
return {
|
|
version: "2.1.0",
|
|
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 Log;
|
|
}
|
|
|
|
function getMessageText(message: AnalysisMessage) {
|
|
return message.tokens.map((t) => t.text).join("");
|
|
}
|
|
});
|