Skip to content

Commit a7bfa21

Browse files
committed
Convert check-sarif/index.js and add tests
1 parent 8f7a2dd commit a7bfa21

4 files changed

Lines changed: 304 additions & 45 deletions

File tree

.github/actions/check-sarif/action.yml

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,23 @@ inputs:
1616
Comma separated list of query ids that should NOT be included in this SARIF file.
1717
1818
runs:
19-
using: node24
20-
main: index.js
19+
using: "composite"
20+
steps:
21+
- name: Run `check-sarif.ts`
22+
shell: bash
23+
env:
24+
SARIF_FILE: ${{ inputs.sarif-file }}
25+
QUERIES_RUN: ${{ inputs.queries-run }}
26+
QUERIES_NOT_RUN: ${{ inputs.queries-not-run }}
27+
run: |
28+
if [[ -d pr-checks ]]; then
29+
npx tsx ./pr-checks/check-sarif.ts \
30+
--sarif-file "$SARIF_FILE" \
31+
--queries-run "$QUERIES_RUN" \
32+
--queries-not-run "$QUERIES_NOT_RUN"
33+
else
34+
npx tsx ../action/pr-checks/check-sarif.ts \
35+
--sarif-file "$SARIF_FILE" \
36+
--queries-run "$QUERIES_RUN" \
37+
--queries-not-run "$QUERIES_NOT_RUN"
38+
fi

.github/actions/check-sarif/index.js

Lines changed: 0 additions & 43 deletions
This file was deleted.

pr-checks/check-sarif.test.ts

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/**
2+
* Tests for `check-sarif.ts`.
3+
*/
4+
5+
import * as assert from "node:assert/strict";
6+
import { describe, it } from "node:test";
7+
8+
import type { Log } from "sarif";
9+
10+
import { checkSarif } from "./check-sarif";
11+
12+
/** Builds a minimal SARIF Log with the given rule IDs spread across extensions. */
13+
function buildSarifLog(ruleIds: string[]): Log {
14+
return {
15+
version: "2.1.0",
16+
$schema:
17+
"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json",
18+
runs: [
19+
{
20+
tool: {
21+
driver: { name: "CodeQL" },
22+
extensions: [
23+
{
24+
name: "test-pack",
25+
rules: ruleIds.map((id) => ({ id })),
26+
},
27+
],
28+
},
29+
results: [],
30+
},
31+
],
32+
};
33+
}
34+
35+
describe("checkSarif", async () => {
36+
await it("returns 0 when all expected queries ran and no unexpected queries ran", () => {
37+
const sarif = buildSarifLog(["js/sql-injection", "js/xss"]);
38+
const exitCode = checkSarif(sarif, {
39+
sarifFile: "test.sarif",
40+
queriesRun: "js/sql-injection, js/xss",
41+
queriesNotRun: "js/hardcoded-credentials",
42+
});
43+
assert.equal(exitCode, 0);
44+
});
45+
46+
await it("returns -2 when an expected query did not run", () => {
47+
const sarif = buildSarifLog(["js/sql-injection"]);
48+
const exitCode = checkSarif(sarif, {
49+
sarifFile: "test.sarif",
50+
queriesRun: "js/sql-injection, js/xss",
51+
queriesNotRun: "",
52+
});
53+
assert.equal(exitCode, -2);
54+
});
55+
56+
await it("returns -2 when an unexpected query ran", () => {
57+
const sarif = buildSarifLog(["js/sql-injection", "js/xss"]);
58+
const exitCode = checkSarif(sarif, {
59+
sarifFile: "test.sarif",
60+
queriesRun: "js/sql-injection",
61+
queriesNotRun: "js/xss",
62+
});
63+
assert.equal(exitCode, -2);
64+
});
65+
66+
await it("handles empty queries-run and queries-not-run inputs", () => {
67+
const sarif = buildSarifLog(["js/sql-injection"]);
68+
const exitCode = checkSarif(sarif, {
69+
sarifFile: "test.sarif",
70+
queriesRun: "",
71+
queriesNotRun: "",
72+
});
73+
assert.equal(exitCode, 0);
74+
});
75+
76+
await it("handles multiple extensions with rules", () => {
77+
const sarif: Log = {
78+
version: "2.1.0",
79+
$schema:
80+
"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json",
81+
runs: [
82+
{
83+
tool: {
84+
driver: { name: "CodeQL" },
85+
extensions: [
86+
{
87+
name: "pack-a",
88+
rules: [{ id: "js/sql-injection" }],
89+
},
90+
{
91+
name: "pack-b",
92+
rules: [{ id: "js/xss" }],
93+
},
94+
],
95+
},
96+
results: [],
97+
},
98+
],
99+
};
100+
const exitCode = checkSarif(sarif, {
101+
sarifFile: "test.sarif",
102+
queriesRun: "js/sql-injection, js/xss",
103+
queriesNotRun: "",
104+
});
105+
assert.equal(exitCode, 0);
106+
});
107+
108+
await it("handles extensions with no rules", () => {
109+
const sarif: Log = {
110+
version: "2.1.0",
111+
$schema:
112+
"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json",
113+
runs: [
114+
{
115+
tool: {
116+
driver: { name: "CodeQL" },
117+
extensions: [
118+
{ name: "empty-pack" },
119+
{
120+
name: "pack-with-rules",
121+
rules: [{ id: "js/xss" }],
122+
},
123+
],
124+
},
125+
results: [],
126+
},
127+
],
128+
};
129+
const exitCode = checkSarif(sarif, {
130+
sarifFile: "test.sarif",
131+
queriesRun: "js/xss",
132+
queriesNotRun: "js/sql-injection",
133+
});
134+
assert.equal(exitCode, 0);
135+
});
136+
137+
await it("throws when tool extensions are undefined", () => {
138+
const sarif: Log = {
139+
version: "2.1.0",
140+
$schema:
141+
"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json",
142+
runs: [
143+
{
144+
tool: { driver: { name: "CodeQL" } },
145+
results: [],
146+
},
147+
],
148+
};
149+
assert.throws(
150+
() =>
151+
checkSarif(sarif, {
152+
sarifFile: "test.sarif",
153+
queriesRun: "js/xss",
154+
queriesNotRun: "",
155+
}),
156+
{ message: /Couldn't find tool extensions/ },
157+
);
158+
});
159+
});

pr-checks/check-sarif.ts

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
#!/usr/bin/env npx tsx
2+
3+
/** Checks a SARIF file to see if certain queries were run and others were not run. */
4+
5+
import * as fs from "node:fs";
6+
import { parseArgs } from "node:util";
7+
8+
import * as core from "@actions/core";
9+
import type { ReportingDescriptor, Log } from "sarif";
10+
11+
import { getErrorMessage } from "./util";
12+
13+
type Options = { sarifFile: string; queriesRun: string; queriesNotRun: string };
14+
15+
function getOptions(): Options {
16+
const { values } = parseArgs({
17+
options: {
18+
// The path of the SARIF file to check.
19+
"sarif-file": {
20+
type: "string",
21+
},
22+
// The query ids to check are present.
23+
"queries-run": {
24+
type: "string",
25+
},
26+
// The query ids to check are absent.
27+
"queries-not-run": {
28+
type: "string",
29+
},
30+
},
31+
strict: true,
32+
});
33+
34+
if (values["sarif-file"] === undefined) {
35+
throw new Error("The '--sarif-file' input is required.");
36+
}
37+
if (values["queries-run"] === undefined) {
38+
throw new Error("The '--queries-run' input is required.");
39+
}
40+
if (values["queries-not-run"] === undefined) {
41+
throw new Error("The '--queries-not-run' input is required.");
42+
}
43+
44+
return {
45+
sarifFile: values["sarif-file"],
46+
queriesRun: values["queries-run"],
47+
queriesNotRun: values["queries-not-run"],
48+
};
49+
}
50+
51+
function parseQueryIdsInput(queriesRun: string): string[] {
52+
return queriesRun
53+
.split(",")
54+
.map((q) => q.trim())
55+
.filter((q) => q.length > 0);
56+
}
57+
58+
export function checkSarif(sarif: Log, options: Options) {
59+
if (sarif.runs[0].tool.extensions === undefined) {
60+
throw new Error(`Couldn't find tool extensions in the SARIF file.`);
61+
}
62+
63+
let exitCode = 0;
64+
65+
// Extract the rule ids from the SARIF file.
66+
const rules: ReportingDescriptor[] = sarif.runs[0].tool.extensions.flatMap(
67+
(ext) => ext.rules || [],
68+
);
69+
const ruleIds: string[] = rules.map((rule) => rule.id);
70+
71+
// Check that all the expected queries ran
72+
const expectedQueriesRun = parseQueryIdsInput(options.queriesRun);
73+
const queriesThatShouldHaveRunButDidNot = expectedQueriesRun.filter(
74+
(queryId) => !ruleIds.includes(queryId),
75+
);
76+
77+
if (queriesThatShouldHaveRunButDidNot.length > 0) {
78+
core.error(
79+
`The following queries were expected to run but did not: ${queriesThatShouldHaveRunButDidNot.join(", ")}`,
80+
);
81+
exitCode = -2;
82+
}
83+
84+
// Check that all the unexpected queries did not run
85+
const expectedQueriesNotRun = parseQueryIdsInput(options.queriesNotRun);
86+
87+
const queriesThatShouldNotHaveRunButDid = expectedQueriesNotRun.filter(
88+
(queryId) => ruleIds.includes(queryId),
89+
);
90+
91+
if (queriesThatShouldNotHaveRunButDid.length > 0) {
92+
core.error(
93+
`The following queries were NOT expected to have run but did: ${queriesThatShouldNotHaveRunButDid.join(", ")}`,
94+
);
95+
exitCode = -2;
96+
}
97+
98+
core.startGroup("All queries that ran");
99+
for (const rule of rules) {
100+
core.info(`${rule.id}: ${rule.properties?.name || rule.name}`);
101+
}
102+
core.endGroup();
103+
104+
core.startGroup("Full SARIF");
105+
core.info(JSON.stringify(sarif, null, 2));
106+
core.endGroup();
107+
108+
return exitCode;
109+
}
110+
111+
function main() {
112+
try {
113+
const options = getOptions();
114+
const sarif: Log = JSON.parse(fs.readFileSync(options.sarifFile, "utf8"));
115+
116+
return checkSarif(sarif, options);
117+
} catch (err) {
118+
core.error(`Failed to check SARIF file: ${getErrorMessage(err)}`);
119+
return -1;
120+
}
121+
}
122+
123+
if (require.main === module) {
124+
process.exit(main());
125+
}

0 commit comments

Comments
 (0)