Skip to content

Commit b05fe69

Browse files
committed
fix: decode SARIF location URIs in diff-informed alert filtering
1 parent 52cf185 commit b05fe69

4 files changed

Lines changed: 218 additions & 6 deletions

File tree

‎lib/entry-points.js‎

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

‎src/upload-lib.test.ts‎

Lines changed: 192 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ import * as api from "./api-client";
1212
import * as diffUtils from "./diff-informed-analysis-utils";
1313
import { Logger } from "./logging";
1414
import * as sarif from "./sarif";
15-
import { RecordingLogger, setupTests } from "./testing-utils";
15+
import {
16+
checkExpectedLogMessages,
17+
RecordingLogger,
18+
setupTests,
19+
} from "./testing-utils";
1620
import * as uploadLib from "./upload-lib";
1721
import { UploadPayload } from "./upload-lib/types";
1822
import { GitHubVariant, initializeEnvironment, withTmpDir } from "./util";
@@ -1014,12 +1018,62 @@ for (const analysisKind of analyses.supportedAnalysisKinds) {
10141018
);
10151019
}
10161020

1021+
function createMockSarifWithResults(
1022+
results: sarif.Result[],
1023+
): Partial<sarif.Log> {
1024+
return {
1025+
version: "2.1.0",
1026+
runs: [
1027+
{
1028+
tool: {
1029+
driver: {
1030+
name: "test tool",
1031+
},
1032+
},
1033+
results,
1034+
},
1035+
],
1036+
};
1037+
}
1038+
1039+
function createMockResultWithUri(
1040+
uri: string,
1041+
startLine = 2,
1042+
relatedUri?: string,
1043+
): sarif.Result {
1044+
return {
1045+
ruleId: "test-rule",
1046+
message: { text: "test message" },
1047+
locations: [
1048+
{
1049+
physicalLocation: {
1050+
artifactLocation: { uri },
1051+
region: { startLine },
1052+
},
1053+
},
1054+
],
1055+
...(relatedUri
1056+
? {
1057+
relatedLocations: [
1058+
{
1059+
physicalLocation: {
1060+
artifactLocation: { uri: relatedUri },
1061+
region: { startLine },
1062+
},
1063+
},
1064+
],
1065+
}
1066+
: {}),
1067+
};
1068+
}
1069+
10171070
function runFilterAlertsByDiffRange(
10181071
input: Partial<sarif.Log>,
10191072
diffRanges: diffUtils.DiffThunkRange[],
1073+
logger: Logger = new RecordingLogger(),
10201074
): Partial<sarif.Log> {
10211075
sinon.stub(diffUtils, "readDiffRangesJsonFile").returns(diffRanges);
1022-
return uploadLib.filterAlertsByDiffRange(new RecordingLogger(), input);
1076+
return uploadLib.filterAlertsByDiffRange(logger, input);
10231077
}
10241078

10251079
test.serial(
@@ -1043,3 +1097,139 @@ test.serial(
10431097
t.deepEqual(actualOutput, expectedOutput);
10441098
},
10451099
);
1100+
1101+
for (const [filePath, uri] of [
1102+
["with space/board.js", "with%20space/board.js"],
1103+
["src/[id]/board.js", "src/%5Bid%5D/board.js"],
1104+
["src/file#1.js", "src/file%231.js"],
1105+
["src/100%.js", "src/100%25.js"],
1106+
["src/literal%20.js", "src/literal%2520.js"],
1107+
["src/foo:bar.js", "src/foo%3Abar.js"],
1108+
["src/caf\u00e9.js", "src/caf%C3%A9.js"],
1109+
["src/caf\u00e9.js", "src/caf%c3%a9.js"],
1110+
]) {
1111+
test.serial(
1112+
`filterAlertsByDiffRange matches percent-encoded URI ${uri}`,
1113+
(t) => {
1114+
const matchingResult = createMockResultWithUri(uri);
1115+
const nonMatchingResult = createMockResultWithUri("other.js");
1116+
const input = createMockSarifWithResults([
1117+
matchingResult,
1118+
nonMatchingResult,
1119+
]);
1120+
1121+
const actualOutput = runFilterAlertsByDiffRange(input, [
1122+
{ path: filePath, startLine: 1, endLine: 3 },
1123+
]);
1124+
1125+
t.deepEqual(actualOutput.runs?.[0].results, [matchingResult]);
1126+
},
1127+
);
1128+
}
1129+
1130+
test.serial(
1131+
"filterAlertsByDiffRange filters out percent-encoded URI when outside diff line range",
1132+
(t) => {
1133+
const outOfRangeResult = createMockResultWithUri(
1134+
"with%20space/board.js",
1135+
2,
1136+
);
1137+
const inRangeResult = createMockResultWithUri("with%20space/board.js", 15);
1138+
const input = createMockSarifWithResults([outOfRangeResult, inRangeResult]);
1139+
const actualOutput = runFilterAlertsByDiffRange(input, [
1140+
{ path: "with space/board.js", startLine: 10, endLine: 20 },
1141+
]);
1142+
1143+
t.deepEqual(actualOutput.runs?.[0].results, [inRangeResult]);
1144+
},
1145+
);
1146+
1147+
test.serial(
1148+
"filterAlertsByDiffRange filters out percent-encoded URI when path does not match",
1149+
(t) => {
1150+
const input = createMockSarifWithResults([
1151+
createMockResultWithUri("with%20space/board.js"),
1152+
]);
1153+
const actualOutput = runFilterAlertsByDiffRange(input, [
1154+
{ path: "other/board.js", startLine: 1, endLine: 3 },
1155+
]);
1156+
1157+
t.deepEqual(actualOutput.runs?.[0].results, []);
1158+
},
1159+
);
1160+
1161+
test.serial(
1162+
"filterAlertsByDiffRange keeps alert when relatedLocations has a matching percent-encoded URI",
1163+
(t) => {
1164+
const matchingRelatedResult = createMockResultWithUri(
1165+
"src/promiseUtils.js",
1166+
2,
1167+
"with%20space/ParseObject.js",
1168+
);
1169+
const nonMatchingResult = createMockResultWithUri("other.js");
1170+
const input = createMockSarifWithResults([
1171+
matchingRelatedResult,
1172+
nonMatchingResult,
1173+
]);
1174+
1175+
const actualOutput = runFilterAlertsByDiffRange(input, [
1176+
{ path: "with space/ParseObject.js", startLine: 1, endLine: 3 },
1177+
]);
1178+
1179+
t.deepEqual(actualOutput.runs?.[0].results, [matchingRelatedResult]);
1180+
},
1181+
);
1182+
1183+
for (const uri of ["dir%ZZ/board.js", "src/bad%.js", "src/bad%C3%28.js"]) {
1184+
test.serial(
1185+
`filterAlertsByDiffRange falls back to raw path for malformed URI ${uri}`,
1186+
(t) => {
1187+
const logger = new RecordingLogger();
1188+
const matchingResult = createMockResultWithUri(uri);
1189+
const input = createMockSarifWithResults([matchingResult]);
1190+
const actualOutput = runFilterAlertsByDiffRange(
1191+
input,
1192+
[{ path: uri, startLine: 1, endLine: 3 }],
1193+
logger,
1194+
);
1195+
1196+
t.deepEqual(actualOutput.runs?.[0].results, [matchingResult]);
1197+
checkExpectedLogMessages(t, logger.messages, [
1198+
`Failed to decode location URI "${uri}", comparing it as a raw path`,
1199+
]);
1200+
},
1201+
);
1202+
}
1203+
1204+
test.serial(
1205+
"filterAlertsByDiffRange filters out malformed URI when raw path does not match",
1206+
(t) => {
1207+
const logger = new RecordingLogger();
1208+
const input = createMockSarifWithResults([
1209+
createMockResultWithUri("dir%ZZ/board.js"),
1210+
]);
1211+
const actualOutput = runFilterAlertsByDiffRange(
1212+
input,
1213+
[{ path: "src/different.js", startLine: 1, endLine: 3 }],
1214+
logger,
1215+
);
1216+
1217+
t.deepEqual(actualOutput.runs?.[0].results, []);
1218+
checkExpectedLogMessages(t, logger.messages, [
1219+
'Failed to decode location URI "dir%ZZ/board.js", comparing it as a raw path',
1220+
]);
1221+
},
1222+
);
1223+
1224+
test.serial(
1225+
"filterAlertsByDiffRange matches raw unencoded path containing valid percent-encoding sequence",
1226+
(t) => {
1227+
const matchingResult = createMockResultWithUri("dir/100%20done.js");
1228+
const input = createMockSarifWithResults([matchingResult]);
1229+
const actualOutput = runFilterAlertsByDiffRange(input, [
1230+
{ path: "dir/100%20done.js", startLine: 1, endLine: 3 },
1231+
]);
1232+
1233+
t.deepEqual(actualOutput.runs?.[0].results, [matchingResult]);
1234+
},
1235+
);

‎src/upload-lib.ts‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1099,17 +1099,30 @@ export function filterAlertsByDiffRange(
10991099
return locations.some((physicalLocation) => {
11001100
const locationUri = physicalLocation?.artifactLocation?.uri;
11011101
const locationStartLine = physicalLocation?.region?.startLine;
1102-
if (!locationUri || locationStartLine === undefined) {
1102+
if (
1103+
typeof locationUri !== "string" ||
1104+
!locationUri ||
1105+
locationStartLine === undefined
1106+
) {
11031107
return false;
11041108
}
1109+
let decodedLocationUri = locationUri;
1110+
try {
1111+
decodedLocationUri = decodeURIComponent(locationUri);
1112+
} catch {
1113+
logger.debug(
1114+
`Failed to decode location URI "${locationUri}", comparing it as a raw path`,
1115+
);
1116+
}
11051117
// Alert filtering here replicates the same behavior as the restrictAlertsTo
11061118
// extensible predicate in CodeQL. See the restrictAlertsTo documentation
11071119
// https://codeql.github.com/codeql-standard-libraries/csharp/codeql/util/AlertFiltering.qll/predicate.AlertFiltering$restrictAlertsTo.3.html
11081120
// for more details, such as why the filtering applies only to the first line
11091121
// of an alert location.
11101122
return diffRanges.some(
11111123
(range) =>
1112-
range.path === locationUri &&
1124+
(range.path === decodedLocationUri ||
1125+
range.path === locationUri) &&
11131126
((range.startLine <= locationStartLine &&
11141127
range.endLine >= locationStartLine) ||
11151128
(range.startLine === 0 && range.endLine === 0)),
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Fixed diff-informed alert filtering incorrectly removing alerts in changed files whose paths contain spaces, special characters, or non-ASCII characters. [#4283](https://github.com/github/codeql-action/pull/4283)

0 commit comments

Comments
 (0)