Files
vscode-codeql/extensions/ql-vscode/test/unit-tests/sarif-processing.test.ts
2024-01-05 17:13:45 +01:00

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("");
}
});