Files
codeql-action/src/sarif/index.ts
T
2026-03-03 11:35:43 +00:00

142 lines
4.2 KiB
TypeScript

import * as fs from "fs";
import { Logger } from "../logging";
import * as sarif from "sarif";
export type * from "sarif";
// Extends `ToolComponent` with the non-standard `automationId` property we use.
export type RunKey = sarif.ToolComponent & {
/**
* Describes a SARIF run (either uniquely or not uniquely) based on the criteria used by
* Code Scanning to determine analysis categories
*/
automationId: string | undefined;
};
/**
* An error that occurred due to an invalid SARIF upload request.
*/
export class InvalidSarifUploadError extends Error {}
/**
* Get the array of all the tool names contained in the given sarif contents.
*
* Returns an array of unique string tool names.
*/
export function getToolNames(sarifFile: Partial<sarif.Log>): string[] {
const toolNames = {};
for (const run of sarifFile.runs || []) {
const tool = run.tool || {};
const driver = tool.driver || {};
if (typeof driver.name === "string" && driver.name.length > 0) {
toolNames[driver.name] = true;
}
}
return Object.keys(toolNames);
}
/**
* Reads the file pointed at by `sarifFilePath` and parses it as JSON. This function does
* not validate that the JSON represents a valid SARIF file. I.e. this function will only
* throw if the file cannot be read or does not contain valid JSON.
*
* @param sarifFilePath The file to read.
* @returns The resulting JSON value, cast to a SARIF `Log`.
*/
export function readSarifFile(sarifFilePath: string): Partial<sarif.Log> {
return JSON.parse(fs.readFileSync(sarifFilePath, "utf8")) as sarif.Log;
}
// Takes a list of paths to sarif files and combines them together,
// returning the contents of the combined sarif file.
export function combineSarifFiles(
sarifFiles: string[],
logger: Logger,
): sarif.Log {
logger.info(`Loading SARIF file(s)`);
const runs: sarif.Run[] = [];
let version: sarif.Log.version | undefined = undefined;
for (const sarifFile of sarifFiles) {
logger.debug(`Loading SARIF file: ${sarifFile}`);
const sarifLog = readSarifFile(sarifFile);
// If this is the first SARIF file we are reading, store the version from it so that we
// can put it in the combined SARIF. If not, then check that the versions match and
// throw an exception if they do not.
if (version === undefined) {
version = sarifLog.version;
} else if (version !== sarifLog.version) {
throw new InvalidSarifUploadError(
`Different SARIF versions encountered: ${version} and ${sarifLog.version}`,
);
}
runs.push(...(sarifLog?.runs || []));
}
// We can't guarantee that the SARIF files we load will have version properties. As a fallback,
// we set it to the expected version if we didn't find any other.
if (version === undefined) {
version = "2.1.0";
}
return { version, runs };
}
/**
* Checks whether all the runs in the given SARIF files were produced by CodeQL.
* @param sarifLogs The list of SARIF objects to check.
*/
export function areAllRunsProducedByCodeQL(
sarifLogs: Array<Partial<sarif.Log>>,
): boolean {
return sarifLogs.every((sarifLog: Partial<sarif.Log>) => {
return sarifLog.runs?.every((run) => run.tool?.driver?.name === "CodeQL");
});
}
function createRunKey(run: sarif.Run): RunKey {
return {
name: run.tool?.driver?.name,
fullName: run.tool?.driver?.fullName,
version: run.tool?.driver?.version,
semanticVersion: run.tool?.driver?.semanticVersion,
guid: run.tool?.driver?.guid,
automationId: run.automationDetails?.id,
};
}
/**
* Checks whether all runs in the given SARIF files are unique (based on the
* criteria used by Code Scanning to determine analysis categories).
* @param sarifLogs The list of SARIF objects to check.
*/
export function areAllRunsUnique(
sarifLogs: Array<Partial<sarif.Log>>,
): boolean {
const keys = new Set<string>();
for (const sarifLog of sarifLogs) {
if (sarifLog.runs === undefined) {
continue;
}
for (const run of sarifLog.runs) {
const key = JSON.stringify(createRunKey(run));
// If the key already exists, the runs are not unique.
if (keys.has(key)) {
return false;
}
keys.add(key);
}
}
return true;
}