From 58f39a7d42573bc829dedf07fca13bdd4c21e495 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Fri, 31 Jul 2026 13:24:27 +0100 Subject: [PATCH] Convert `check-sarif/index.js` and add tests --- .github/actions/check-sarif/action.yml | 22 +++- .github/actions/check-sarif/index.js | 43 ------- pr-checks/check-sarif.test.ts | 159 +++++++++++++++++++++++++ pr-checks/check-sarif.ts | 125 +++++++++++++++++++ 4 files changed, 304 insertions(+), 45 deletions(-) delete mode 100644 .github/actions/check-sarif/index.js create mode 100644 pr-checks/check-sarif.test.ts create mode 100644 pr-checks/check-sarif.ts diff --git a/.github/actions/check-sarif/action.yml b/.github/actions/check-sarif/action.yml index bfa1c3b9d..936747cb6 100644 --- a/.github/actions/check-sarif/action.yml +++ b/.github/actions/check-sarif/action.yml @@ -16,5 +16,23 @@ inputs: Comma separated list of query ids that should NOT be included in this SARIF file. runs: - using: node24 - main: index.js + using: "composite" + steps: + - name: Run `check-sarif.ts` + shell: bash + env: + SARIF_FILE: ${{ inputs.sarif-file }} + QUERIES_RUN: ${{ inputs.queries-run }} + QUERIES_NOT_RUN: ${{ inputs.queries-not-run }} + run: | + if [[ -d pr-checks ]]; then + npx tsx ./pr-checks/check-sarif.ts \ + --sarif-file "$SARIF_FILE" \ + --queries-run "$QUERIES_RUN" \ + --queries-not-run "$QUERIES_NOT_RUN" + else + npx tsx ../action/pr-checks/check-sarif.ts \ + --sarif-file "$SARIF_FILE" \ + --queries-run "$QUERIES_RUN" \ + --queries-not-run "$QUERIES_NOT_RUN" + fi diff --git a/.github/actions/check-sarif/index.js b/.github/actions/check-sarif/index.js deleted file mode 100644 index c002b0769..000000000 --- a/.github/actions/check-sarif/index.js +++ /dev/null @@ -1,43 +0,0 @@ -'use strict' - -const core = require('@actions/core') -const fs = require('fs') - -const sarif = JSON.parse(fs.readFileSync(core.getInput('sarif-file'), 'utf8')) -const rules = sarif.runs[0].tool.extensions.flatMap(ext => ext.rules || []) -const ruleIds = rules.map(rule => rule.id) - -// Check that all the expected queries ran -const expectedQueriesRun = getQueryIdsInput('queries-run') -const queriesThatShouldHaveRunButDidNot = expectedQueriesRun.filter(queryId => !ruleIds.includes(queryId)) - -if (queriesThatShouldHaveRunButDidNot.length > 0) { - core.setFailed(`The following queries were expected to run but did not: ${queriesThatShouldHaveRunButDidNot.join(', ')}`) -} - -// Check that all the unexpected queries did not run -const expectedQueriesNotRun = getQueryIdsInput('queries-not-run') - -const queriesThatShouldNotHaveRunButDid = expectedQueriesNotRun.filter(queryId => ruleIds.includes(queryId)) - -if (queriesThatShouldNotHaveRunButDid.length > 0) { - core.setFailed(`The following queries were NOT expected to have run but did: ${queriesThatShouldNotHaveRunButDid.join(', ')}`) -} - - -core.startGroup('All queries run') -rules.forEach(rule => { - core.info(`${rule.id}: ${(rule.properties && rule.properties.name) || rule.name}`) -}) -core.endGroup() - -core.startGroup('Full SARIF') -core.info(JSON.stringify(sarif, null, 2)) -core.endGroup() - -function getQueryIdsInput(name) { - return core.getInput(name) - .split(',') - .map(q => q.trim()) - .filter(q => q.length > 0) -} diff --git a/pr-checks/check-sarif.test.ts b/pr-checks/check-sarif.test.ts new file mode 100644 index 000000000..81290659d --- /dev/null +++ b/pr-checks/check-sarif.test.ts @@ -0,0 +1,159 @@ +/** + * Tests for `check-sarif.ts`. + */ + +import * as assert from "node:assert/strict"; +import { describe, it } from "node:test"; + +import type { Log } from "sarif"; + +import { checkSarif } from "./check-sarif"; + +/** Builds a minimal SARIF Log with the given rule IDs spread across extensions. */ +function buildSarifLog(ruleIds: string[]): Log { + return { + version: "2.1.0", + $schema: + "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json", + runs: [ + { + tool: { + driver: { name: "CodeQL" }, + extensions: [ + { + name: "test-pack", + rules: ruleIds.map((id) => ({ id })), + }, + ], + }, + results: [], + }, + ], + }; +} + +describe("checkSarif", async () => { + await it("returns 0 when all expected queries ran and no unexpected queries ran", () => { + const sarif = buildSarifLog(["js/sql-injection", "js/xss"]); + const exitCode = checkSarif(sarif, { + sarifFile: "test.sarif", + queriesRun: "js/sql-injection, js/xss", + queriesNotRun: "js/hardcoded-credentials", + }); + assert.equal(exitCode, 0); + }); + + await it("returns -2 when an expected query did not run", () => { + const sarif = buildSarifLog(["js/sql-injection"]); + const exitCode = checkSarif(sarif, { + sarifFile: "test.sarif", + queriesRun: "js/sql-injection, js/xss", + queriesNotRun: "", + }); + assert.equal(exitCode, -2); + }); + + await it("returns -2 when an unexpected query ran", () => { + const sarif = buildSarifLog(["js/sql-injection", "js/xss"]); + const exitCode = checkSarif(sarif, { + sarifFile: "test.sarif", + queriesRun: "js/sql-injection", + queriesNotRun: "js/xss", + }); + assert.equal(exitCode, -2); + }); + + await it("handles empty queries-run and queries-not-run inputs", () => { + const sarif = buildSarifLog(["js/sql-injection"]); + const exitCode = checkSarif(sarif, { + sarifFile: "test.sarif", + queriesRun: "", + queriesNotRun: "", + }); + assert.equal(exitCode, 0); + }); + + await it("handles multiple extensions with rules", () => { + const sarif: Log = { + version: "2.1.0", + $schema: + "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json", + runs: [ + { + tool: { + driver: { name: "CodeQL" }, + extensions: [ + { + name: "pack-a", + rules: [{ id: "js/sql-injection" }], + }, + { + name: "pack-b", + rules: [{ id: "js/xss" }], + }, + ], + }, + results: [], + }, + ], + }; + const exitCode = checkSarif(sarif, { + sarifFile: "test.sarif", + queriesRun: "js/sql-injection, js/xss", + queriesNotRun: "", + }); + assert.equal(exitCode, 0); + }); + + await it("handles extensions with no rules", () => { + const sarif: Log = { + version: "2.1.0", + $schema: + "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json", + runs: [ + { + tool: { + driver: { name: "CodeQL" }, + extensions: [ + { name: "empty-pack" }, + { + name: "pack-with-rules", + rules: [{ id: "js/xss" }], + }, + ], + }, + results: [], + }, + ], + }; + const exitCode = checkSarif(sarif, { + sarifFile: "test.sarif", + queriesRun: "js/xss", + queriesNotRun: "js/sql-injection", + }); + assert.equal(exitCode, 0); + }); + + await it("throws when tool extensions are undefined", () => { + const sarif: Log = { + version: "2.1.0", + $schema: + "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json", + runs: [ + { + tool: { driver: { name: "CodeQL" } }, + results: [], + }, + ], + }; + assert.throws( + () => + checkSarif(sarif, { + sarifFile: "test.sarif", + queriesRun: "js/xss", + queriesNotRun: "", + }), + { message: /Couldn't find tool extensions/ }, + ); + }); +}); diff --git a/pr-checks/check-sarif.ts b/pr-checks/check-sarif.ts new file mode 100644 index 000000000..7c744c34c --- /dev/null +++ b/pr-checks/check-sarif.ts @@ -0,0 +1,125 @@ +#!/usr/bin/env npx tsx + +/** Checks a SARIF file to see if certain queries were run and others were not run. */ + +import * as fs from "node:fs"; +import { parseArgs } from "node:util"; + +import * as core from "@actions/core"; +import type { ReportingDescriptor, Log } from "sarif"; + +import { getErrorMessage } from "./util"; + +type Options = { sarifFile: string; queriesRun: string; queriesNotRun: string }; + +function getOptions(): Options { + const { values } = parseArgs({ + options: { + // The path of the SARIF file to check. + "sarif-file": { + type: "string", + }, + // The query ids to check are present. + "queries-run": { + type: "string", + }, + // The query ids to check are absent. + "queries-not-run": { + type: "string", + }, + }, + strict: true, + }); + + if (values["sarif-file"] === undefined) { + throw new Error("The '--sarif-file' input is required."); + } + if (values["queries-run"] === undefined) { + throw new Error("The '--queries-run' input is required."); + } + if (values["queries-not-run"] === undefined) { + throw new Error("The '--queries-not-run' input is required."); + } + + return { + sarifFile: values["sarif-file"], + queriesRun: values["queries-run"], + queriesNotRun: values["queries-not-run"], + }; +} + +function parseQueryIdsInput(queriesRun: string): string[] { + return queriesRun + .split(",") + .map((q) => q.trim()) + .filter((q) => q.length > 0); +} + +export function checkSarif(sarif: Log, options: Options) { + if (sarif.runs[0].tool.extensions === undefined) { + throw new Error(`Couldn't find tool extensions in the SARIF file.`); + } + + let exitCode = 0; + + // Extract the rule ids from the SARIF file. + const rules: ReportingDescriptor[] = sarif.runs[0].tool.extensions.flatMap( + (ext) => ext.rules || [], + ); + const ruleIds: string[] = rules.map((rule) => rule.id); + + // Check that all the expected queries ran + const expectedQueriesRun = parseQueryIdsInput(options.queriesRun); + const queriesThatShouldHaveRunButDidNot = expectedQueriesRun.filter( + (queryId) => !ruleIds.includes(queryId), + ); + + if (queriesThatShouldHaveRunButDidNot.length > 0) { + core.error( + `The following queries were expected to run but did not: ${queriesThatShouldHaveRunButDidNot.join(", ")}`, + ); + exitCode = -2; + } + + // Check that all the unexpected queries did not run + const expectedQueriesNotRun = parseQueryIdsInput(options.queriesNotRun); + + const queriesThatShouldNotHaveRunButDid = expectedQueriesNotRun.filter( + (queryId) => ruleIds.includes(queryId), + ); + + if (queriesThatShouldNotHaveRunButDid.length > 0) { + core.error( + `The following queries were NOT expected to have run but did: ${queriesThatShouldNotHaveRunButDid.join(", ")}`, + ); + exitCode = -2; + } + + core.startGroup("All queries that ran"); + for (const rule of rules) { + core.info(`${rule.id}: ${rule.properties?.name || rule.name}`); + } + core.endGroup(); + + core.startGroup("Full SARIF"); + core.info(JSON.stringify(sarif, null, 2)); + core.endGroup(); + + return exitCode; +} + +function main() { + try { + const options = getOptions(); + const sarif: Log = JSON.parse(fs.readFileSync(options.sarifFile, "utf8")); + + return checkSarif(sarif, options); + } catch (err) { + core.error(`Failed to check SARIF file: ${getErrorMessage(err)}`); + return -1; + } +} + +if (require.main === module) { + process.exit(main()); +}