mirror of
https://github.com/github/codeql-action.git
synced 2025-12-22 15:20:19 +08:00
328 lines
14 KiB
JavaScript
Generated
328 lines
14 KiB
JavaScript
Generated
"use strict";
|
|
var __importStar = (this && this.__importStar) || function (mod) {
|
|
if (mod && mod.__esModule) return mod;
|
|
var result = {};
|
|
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
|
|
result["default"] = mod;
|
|
return result;
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
const core = __importStar(require("@actions/core"));
|
|
const io = __importStar(require("@actions/io"));
|
|
const fs = __importStar(require("fs"));
|
|
const yaml = __importStar(require("js-yaml"));
|
|
const path = __importStar(require("path"));
|
|
const api = __importStar(require("./api-client"));
|
|
const util = __importStar(require("./util"));
|
|
const NAME_PROPERTY = 'name';
|
|
const DISPLAY_DEFAULT_QUERIES_PROPERTY = 'disable-default-queries';
|
|
const QUERIES_PROPERTY = 'queries';
|
|
const QUERIES_USES_PROPERTY = 'uses';
|
|
const PATHS_IGNORE_PROPERTY = 'paths-ignore';
|
|
const PATHS_PROPERTY = 'paths';
|
|
class ExternalQuery {
|
|
constructor(repository, ref) {
|
|
this.path = '';
|
|
this.repository = repository;
|
|
this.ref = ref;
|
|
}
|
|
}
|
|
exports.ExternalQuery = ExternalQuery;
|
|
// The set of acceptable values for built-in suites from the codeql bundle
|
|
const builtinSuites = ['security-extended', 'security-and-quality'];
|
|
class Config {
|
|
constructor() {
|
|
this.name = "";
|
|
this.disableDefaultQueries = false;
|
|
this.additionalQueries = [];
|
|
this.externalQueries = [];
|
|
this.additionalSuites = [];
|
|
this.pathsIgnore = [];
|
|
this.paths = [];
|
|
}
|
|
addQuery(configFile, queryUses) {
|
|
// The logic for parsing the string is based on what actions does for
|
|
// parsing the 'uses' actions in the workflow file
|
|
queryUses = queryUses.trim();
|
|
if (queryUses === "") {
|
|
throw new Error(getQueryUsesInvalid(configFile));
|
|
}
|
|
// Check for the local path case before we start trying to parse the repository name
|
|
if (queryUses.startsWith("./")) {
|
|
const localQueryPath = queryUses.slice(2);
|
|
// Resolve the local path against the workspace so that when this is
|
|
// passed to codeql it resolves to exactly the path we expect it to resolve to.
|
|
const workspacePath = fs.realpathSync(util.getRequiredEnvParam('GITHUB_WORKSPACE'));
|
|
let absoluteQueryPath = path.join(workspacePath, localQueryPath);
|
|
// Check the file exists
|
|
if (!fs.existsSync(absoluteQueryPath)) {
|
|
throw new Error(getLocalPathDoesNotExist(configFile, localQueryPath));
|
|
}
|
|
// Call this after checking file exists, because it'll fail if file doesn't exist
|
|
absoluteQueryPath = fs.realpathSync(absoluteQueryPath);
|
|
// Check the local path doesn't jump outside the repo using '..' or symlinks
|
|
if (!(absoluteQueryPath + path.sep).startsWith(workspacePath + path.sep)) {
|
|
throw new Error(getLocalPathOutsideOfRepository(configFile, localQueryPath));
|
|
}
|
|
this.additionalQueries.push(absoluteQueryPath);
|
|
return;
|
|
}
|
|
// Check for one of the builtin suites
|
|
if (queryUses.indexOf('/') === -1 && queryUses.indexOf('@') === -1) {
|
|
const suite = builtinSuites.find((suite) => suite === queryUses);
|
|
if (suite) {
|
|
this.additionalSuites.push(suite);
|
|
return;
|
|
}
|
|
else {
|
|
throw new Error(getQueryUsesInvalid(configFile, queryUses));
|
|
}
|
|
}
|
|
let tok = queryUses.split('@');
|
|
if (tok.length !== 2) {
|
|
throw new Error(getQueryUsesInvalid(configFile, queryUses));
|
|
}
|
|
const ref = tok[1];
|
|
tok = tok[0].split('/');
|
|
// The first token is the owner
|
|
// The second token is the repo
|
|
// The rest is a path, if there is more than one token combine them to form the full path
|
|
if (tok.length < 2) {
|
|
throw new Error(getQueryUsesInvalid(configFile, queryUses));
|
|
}
|
|
if (tok.length > 3) {
|
|
tok = [tok[0], tok[1], tok.slice(2).join('/')];
|
|
}
|
|
// Check none of the parts of the repository name are empty
|
|
if (tok[0].trim() === '' || tok[1].trim() === '') {
|
|
throw new Error(getQueryUsesInvalid(configFile, queryUses));
|
|
}
|
|
let external = new ExternalQuery(tok[0] + '/' + tok[1], ref);
|
|
if (tok.length === 3) {
|
|
external.path = tok[2];
|
|
}
|
|
this.externalQueries.push(external);
|
|
}
|
|
}
|
|
exports.Config = Config;
|
|
// Regex validating stars in paths or paths-ignore entries.
|
|
// The intention is to only allow ** to appear when immediately
|
|
// preceded and followed by a slash.
|
|
const pathStarsRegex = /.*(?:\*\*[^/].*|\*\*$|[^/]\*\*.*)/;
|
|
// Checks that a paths of paths-ignore entry is valid, possibly modifying it
|
|
// to make it valid, or if not possible then throws an error.
|
|
function validateAndSanitisePath(originalPath, propertyName, configFile) {
|
|
let path = originalPath;
|
|
if (path.endsWith('/**')) {
|
|
path = path.substring(0, path.length - 2);
|
|
}
|
|
if (path.match(pathStarsRegex)) {
|
|
throw new Error(getConfigFilePropertyError(configFile, propertyName, '"' + originalPath + '" contains an invalid "**" wildcard. ' +
|
|
'They must be immediately preceeded and followed by a slash as in "/**/".'));
|
|
}
|
|
return path;
|
|
}
|
|
exports.validateAndSanitisePath = validateAndSanitisePath;
|
|
function getNameInvalid(configFile) {
|
|
return getConfigFilePropertyError(configFile, NAME_PROPERTY, 'must be a non-empty string');
|
|
}
|
|
exports.getNameInvalid = getNameInvalid;
|
|
function getDisableDefaultQueriesInvalid(configFile) {
|
|
return getConfigFilePropertyError(configFile, DISPLAY_DEFAULT_QUERIES_PROPERTY, 'must be a boolean');
|
|
}
|
|
exports.getDisableDefaultQueriesInvalid = getDisableDefaultQueriesInvalid;
|
|
function getQueriesInvalid(configFile) {
|
|
return getConfigFilePropertyError(configFile, QUERIES_PROPERTY, 'must be an array');
|
|
}
|
|
exports.getQueriesInvalid = getQueriesInvalid;
|
|
function getQueryUsesInvalid(configFile, queryUses) {
|
|
return getConfigFilePropertyError(configFile, QUERIES_PROPERTY + '.' + QUERIES_USES_PROPERTY, 'must be a built-in suite (' + builtinSuites.join(' or ') +
|
|
'), a relative path, or be of the form "owner/repo[/path]@ref"' +
|
|
(queryUses !== undefined ? '\n Found: ' + queryUses : ''));
|
|
}
|
|
exports.getQueryUsesInvalid = getQueryUsesInvalid;
|
|
function getPathsIgnoreInvalid(configFile) {
|
|
return getConfigFilePropertyError(configFile, PATHS_IGNORE_PROPERTY, 'must be an array of non-empty strings');
|
|
}
|
|
exports.getPathsIgnoreInvalid = getPathsIgnoreInvalid;
|
|
function getPathsInvalid(configFile) {
|
|
return getConfigFilePropertyError(configFile, PATHS_PROPERTY, 'must be an array of non-empty strings');
|
|
}
|
|
exports.getPathsInvalid = getPathsInvalid;
|
|
function getLocalPathOutsideOfRepository(configFile, localPath) {
|
|
return getConfigFilePropertyError(configFile, QUERIES_PROPERTY + '.' + QUERIES_USES_PROPERTY, 'is invalid as the local path "' + localPath + '" is outside of the repository');
|
|
}
|
|
exports.getLocalPathOutsideOfRepository = getLocalPathOutsideOfRepository;
|
|
function getLocalPathDoesNotExist(configFile, localPath) {
|
|
return getConfigFilePropertyError(configFile, QUERIES_PROPERTY + '.' + QUERIES_USES_PROPERTY, 'is invalid as the local path "' + localPath + '" does not exist in the repository');
|
|
}
|
|
exports.getLocalPathDoesNotExist = getLocalPathDoesNotExist;
|
|
function getConfigFileOutsideWorkspaceErrorMessage(configFile) {
|
|
return 'The configuration file "' + configFile + '" is outside of the workspace';
|
|
}
|
|
exports.getConfigFileOutsideWorkspaceErrorMessage = getConfigFileOutsideWorkspaceErrorMessage;
|
|
function getConfigFileDoesNotExistErrorMessage(configFile) {
|
|
return 'The configuration file "' + configFile + '" does not exist';
|
|
}
|
|
exports.getConfigFileDoesNotExistErrorMessage = getConfigFileDoesNotExistErrorMessage;
|
|
function getConfigFileRepoFormatInvalidMessage(configFile) {
|
|
let error = 'The configuration file "' + configFile + '" is not a supported remote file reference.';
|
|
error += ' Expected format <owner>/<repository>/<file-path>@<ref>';
|
|
return error;
|
|
}
|
|
exports.getConfigFileRepoFormatInvalidMessage = getConfigFileRepoFormatInvalidMessage;
|
|
function getConfigFileFormatInvalidMessage(configFile) {
|
|
return 'The configuration file "' + configFile + '" could not be read';
|
|
}
|
|
exports.getConfigFileFormatInvalidMessage = getConfigFileFormatInvalidMessage;
|
|
function getConfigFileDirectoryGivenMessage(configFile) {
|
|
return 'The configuration file "' + configFile + '" looks like a directory, not a file';
|
|
}
|
|
exports.getConfigFileDirectoryGivenMessage = getConfigFileDirectoryGivenMessage;
|
|
function getConfigFilePropertyError(configFile, property, error) {
|
|
return 'The configuration file "' + configFile + '" is invalid: property "' + property + '" ' + error;
|
|
}
|
|
async function initConfig() {
|
|
let configFile = core.getInput('config-file');
|
|
const config = new Config();
|
|
// If no config file was provided create an empty one
|
|
if (configFile === '') {
|
|
core.debug('No configuration file was provided');
|
|
return config;
|
|
}
|
|
let parsedYAML;
|
|
if (isLocal(configFile)) {
|
|
// Treat the config file as relative to the workspace
|
|
const workspacePath = util.getRequiredEnvParam('GITHUB_WORKSPACE');
|
|
configFile = path.resolve(workspacePath, configFile);
|
|
parsedYAML = getLocalConfig(configFile, workspacePath);
|
|
}
|
|
else {
|
|
parsedYAML = await getRemoteConfig(configFile);
|
|
}
|
|
if (NAME_PROPERTY in parsedYAML) {
|
|
if (typeof parsedYAML[NAME_PROPERTY] !== "string") {
|
|
throw new Error(getNameInvalid(configFile));
|
|
}
|
|
if (parsedYAML[NAME_PROPERTY].length === 0) {
|
|
throw new Error(getNameInvalid(configFile));
|
|
}
|
|
config.name = parsedYAML[NAME_PROPERTY];
|
|
}
|
|
if (DISPLAY_DEFAULT_QUERIES_PROPERTY in parsedYAML) {
|
|
if (typeof parsedYAML[DISPLAY_DEFAULT_QUERIES_PROPERTY] !== "boolean") {
|
|
throw new Error(getDisableDefaultQueriesInvalid(configFile));
|
|
}
|
|
config.disableDefaultQueries = parsedYAML[DISPLAY_DEFAULT_QUERIES_PROPERTY];
|
|
}
|
|
if (QUERIES_PROPERTY in parsedYAML) {
|
|
if (!(parsedYAML[QUERIES_PROPERTY] instanceof Array)) {
|
|
throw new Error(getQueriesInvalid(configFile));
|
|
}
|
|
parsedYAML[QUERIES_PROPERTY].forEach(query => {
|
|
if (!(QUERIES_USES_PROPERTY in query) || typeof query[QUERIES_USES_PROPERTY] !== "string") {
|
|
throw new Error(getQueryUsesInvalid(configFile));
|
|
}
|
|
config.addQuery(configFile, query[QUERIES_USES_PROPERTY]);
|
|
});
|
|
}
|
|
if (PATHS_IGNORE_PROPERTY in parsedYAML) {
|
|
if (!(parsedYAML[PATHS_IGNORE_PROPERTY] instanceof Array)) {
|
|
throw new Error(getPathsIgnoreInvalid(configFile));
|
|
}
|
|
parsedYAML[PATHS_IGNORE_PROPERTY].forEach(path => {
|
|
if (typeof path !== "string" || path === '') {
|
|
throw new Error(getPathsIgnoreInvalid(configFile));
|
|
}
|
|
config.pathsIgnore.push(validateAndSanitisePath(path, PATHS_IGNORE_PROPERTY, configFile));
|
|
});
|
|
}
|
|
if (PATHS_PROPERTY in parsedYAML) {
|
|
if (!(parsedYAML[PATHS_PROPERTY] instanceof Array)) {
|
|
throw new Error(getPathsInvalid(configFile));
|
|
}
|
|
parsedYAML[PATHS_PROPERTY].forEach(path => {
|
|
if (typeof path !== "string" || path === '') {
|
|
throw new Error(getPathsInvalid(configFile));
|
|
}
|
|
config.paths.push(validateAndSanitisePath(path, PATHS_PROPERTY, configFile));
|
|
});
|
|
}
|
|
return config;
|
|
}
|
|
function isLocal(configPath) {
|
|
// If the path starts with ./, look locally
|
|
if (configPath.indexOf("./") === 0) {
|
|
return true;
|
|
}
|
|
return (configPath.indexOf("@") === -1);
|
|
}
|
|
function getLocalConfig(configFile, workspacePath) {
|
|
// Error if the config file is now outside of the workspace
|
|
if (!(configFile + path.sep).startsWith(workspacePath + path.sep)) {
|
|
throw new Error(getConfigFileOutsideWorkspaceErrorMessage(configFile));
|
|
}
|
|
// Error if the file does not exist
|
|
if (!fs.existsSync(configFile)) {
|
|
throw new Error(getConfigFileDoesNotExistErrorMessage(configFile));
|
|
}
|
|
return yaml.safeLoad(fs.readFileSync(configFile, 'utf8'));
|
|
}
|
|
async function getRemoteConfig(configFile) {
|
|
// retrieve the various parts of the config location, and ensure they're present
|
|
const format = new RegExp('(?<owner>[^/]+)/(?<repo>[^/]+)/(?<path>[^@]+)@(?<ref>.*)');
|
|
const pieces = format.exec(configFile);
|
|
// 5 = 4 groups + the whole expression
|
|
if (pieces === null || pieces.groups === undefined || pieces.length < 5) {
|
|
throw new Error(getConfigFileRepoFormatInvalidMessage(configFile));
|
|
}
|
|
const response = await api.client.repos.getContents({
|
|
owner: pieces.groups.owner,
|
|
repo: pieces.groups.repo,
|
|
path: pieces.groups.path,
|
|
ref: pieces.groups.ref,
|
|
});
|
|
let fileContents;
|
|
if ("content" in response.data && response.data.content !== undefined) {
|
|
fileContents = response.data.content;
|
|
}
|
|
else if (Array.isArray(response.data)) {
|
|
throw new Error(getConfigFileDirectoryGivenMessage(configFile));
|
|
}
|
|
else {
|
|
throw new Error(getConfigFileFormatInvalidMessage(configFile));
|
|
}
|
|
return yaml.safeLoad(Buffer.from(fileContents, 'base64').toString('binary'));
|
|
}
|
|
function getConfigFolder() {
|
|
return util.getRequiredEnvParam('RUNNER_TEMP');
|
|
}
|
|
function getConfigFile() {
|
|
return path.join(getConfigFolder(), 'config');
|
|
}
|
|
exports.getConfigFile = getConfigFile;
|
|
async function saveConfig(config) {
|
|
const configString = JSON.stringify(config);
|
|
await io.mkdirP(getConfigFolder());
|
|
fs.writeFileSync(getConfigFile(), configString, 'utf8');
|
|
core.debug('Saved config:');
|
|
core.debug(configString);
|
|
}
|
|
async function loadConfig() {
|
|
const configFile = getConfigFile();
|
|
if (fs.existsSync(configFile)) {
|
|
const configString = fs.readFileSync(configFile, 'utf8');
|
|
core.debug('Loaded config:');
|
|
core.debug(configString);
|
|
return JSON.parse(configString);
|
|
}
|
|
else {
|
|
const config = await initConfig();
|
|
core.debug('Initialized config:');
|
|
core.debug(JSON.stringify(config));
|
|
await saveConfig(config);
|
|
return config;
|
|
}
|
|
}
|
|
exports.loadConfig = loadConfig;
|
|
//# sourceMappingURL=config-utils.js.map
|