Permalink
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
codeql-action/src/config-utils.ts
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Robert Brignull
introduce new syntax for built-in query suites
227 lines (188 sloc)
7.63 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as core from '@actions/core'; | |
import * as io from '@actions/io'; | |
import * as fs from 'fs'; | |
import * as yaml from 'js-yaml'; | |
import * as path from 'path'; | |
import * as util from './util'; | |
export class ExternalQuery { | |
public repository: string; | |
public ref: string; | |
public path = ''; | |
constructor(repository: string, ref: string) { | |
this.repository = repository; | |
this.ref = ref; | |
} | |
} | |
// The set of acceptable values for built-in suites from the codeql bundle | |
const builtinSuites = ['security-experimental', 'security-and-quality'] as const; | |
// Derive the union type from the array values | |
type BuiltInSuite = typeof builtinSuites[number]; | |
export class Config { | |
public name = ""; | |
public disableDefaultQueries = false; | |
public additionalQueries: string[] = []; | |
public externalQueries: ExternalQuery[] = []; | |
public additionalSuites: BuiltInSuite[] = []; | |
public pathsIgnore: string[] = []; | |
public paths: string[] = []; | |
public addQuery(queryUses: string) { | |
// 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(getQueryUsesBlank()); | |
} | |
// 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 = util.getRequiredEnvParam('GITHUB_WORKSPACE'); | |
const absoluteQueryPath = path.join(workspacePath, localQueryPath); | |
// Check the file exists | |
if (!fs.existsSync(absoluteQueryPath)) { | |
throw new Error(getLocalPathDoesNotExist(localQueryPath)); | |
} | |
// Check the local path doesn't jump outside the repo using '..' or symlinks | |
if (!(fs.realpathSync(absoluteQueryPath) + path.sep).startsWith(workspacePath + path.sep)) { | |
throw new Error(getLocalPathOutsideOfRepository(localQueryPath)); | |
} | |
this.additionalQueries.push(absoluteQueryPath); | |
return; | |
} | |
// Check for one of the builtin suites | |
if (queryUses.indexOf('/') === -1 && queryUses.indexOf('@') === -1) { | |
if (queryUses in builtinSuites) { | |
this.additionalSuites.push(queryUses as BuiltInSuite); | |
} else { | |
throw new Error(getQueryUsesIncorrect(queryUses)); | |
} | |
} | |
let tok = queryUses.split('@'); | |
if (tok.length !== 2) { | |
throw new Error(getQueryUsesIncorrect(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(getQueryUsesIncorrect(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(getQueryUsesIncorrect(queryUses)); | |
} | |
let external = new ExternalQuery(tok[0] + '/' + tok[1], ref); | |
if (tok.length === 3) { | |
external.path = tok[2]; | |
} | |
this.externalQueries.push(external); | |
} | |
} | |
export function getQueryUsesBlank(): string { | |
return '"uses" value for queries cannot be blank'; | |
} | |
export function getQueryUsesIncorrect(queryUses: string): string { | |
return '"uses" value for queries must be a built-in suite (' + builtinSuites.join('or') + | |
'), a relative path, or of the form owner/repo@ref\n' + | |
'Found: ' + queryUses; | |
} | |
export function getLocalPathOutsideOfRepository(localPath: string): string { | |
return 'Unable to use queries from local path "' + localPath + | |
'" as it is outside of the repository'; | |
} | |
export function getLocalPathDoesNotExist(localPath: string): string { | |
return 'Unable to use queries from local path "' + localPath + | |
'" as the path does not exist in the repository'; | |
} | |
export function getConfigFileOutsideWorkspaceErrorMessage(configFile: string): string { | |
return 'The configuration file "' + configFile + '" is outside of the workspace'; | |
} | |
export function getConfigFileDoesNotExistErrorMessage(configFile: string): string { | |
return 'The configuration file "' + configFile + '" does not exist'; | |
} | |
function initConfig(): Config { | |
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; | |
} | |
// Treat the config file as relative to the workspace | |
const workspacePath = util.getRequiredEnvParam('GITHUB_WORKSPACE'); | |
configFile = path.resolve(workspacePath, configFile); | |
// 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)); | |
} | |
const parsedYAML = yaml.safeLoad(fs.readFileSync(configFile, 'utf8')); | |
if (parsedYAML.name && typeof parsedYAML.name === "string") { | |
config.name = parsedYAML.name; | |
} | |
if (parsedYAML['disable-default-queries'] && typeof parsedYAML['disable-default-queries'] === "boolean") { | |
config.disableDefaultQueries = parsedYAML['disable-default-queries']; | |
} | |
const queries = parsedYAML.queries; | |
if (queries && queries instanceof Array) { | |
queries.forEach(query => { | |
if (typeof query.uses === "string") { | |
config.addQuery(query.uses); | |
} | |
}); | |
} | |
const pathsIgnore = parsedYAML['paths-ignore']; | |
if (pathsIgnore && pathsIgnore instanceof Array) { | |
pathsIgnore.forEach(path => { | |
if (typeof path === "string") { | |
config.pathsIgnore.push(path); | |
} | |
}); | |
} | |
const paths = parsedYAML.paths; | |
if (paths && paths instanceof Array) { | |
paths.forEach(path => { | |
if (typeof path === "string") { | |
config.paths.push(path); | |
} | |
}); | |
} | |
return config; | |
} | |
function getConfigFolder(): string { | |
return util.getRequiredEnvParam('RUNNER_WORKSPACE'); | |
} | |
export function getConfigFile(): string { | |
return path.join(getConfigFolder(), 'config'); | |
} | |
async function saveConfig(config: Config) { | |
const configString = JSON.stringify(config); | |
await io.mkdirP(getConfigFolder()); | |
fs.writeFileSync(getConfigFile(), configString, 'utf8'); | |
core.debug('Saved config:'); | |
core.debug(configString); | |
} | |
export async function loadConfig(): Promise<Config> { | |
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 = initConfig(); | |
core.debug('Initialized config:'); | |
core.debug(JSON.stringify(config)); | |
await saveConfig(config); | |
return config; | |
} | |
} |