Mark configuration errors as user errors

This commit is contained in:
Henry Mercer 2023-07-11 11:05:40 +01:00
parent 95a5fda31a
commit 6639a31758
6 changed files with 208 additions and 194 deletions

92
lib/config-utils.js generated
View file

@ -81,13 +81,13 @@ function validateQueries(resolvedQueries) {
const noDeclaredLanguage = resolvedQueries.noDeclaredLanguage;
const noDeclaredLanguageQueries = Object.keys(noDeclaredLanguage);
if (noDeclaredLanguageQueries.length !== 0) {
throw new Error(`${"The following queries do not declare a language. " +
throw new util_1.UserError(`${"The following queries do not declare a language. " +
"Their qlpack.yml files are either missing or is invalid.\n"}${noDeclaredLanguageQueries.join("\n")}`);
}
const multipleDeclaredLanguages = resolvedQueries.multipleDeclaredLanguages;
const multipleDeclaredLanguagesQueries = Object.keys(multipleDeclaredLanguages);
if (multipleDeclaredLanguagesQueries.length !== 0) {
throw new Error(`${"The following queries declare multiple languages. " +
throw new util_1.UserError(`${"The following queries declare multiple languages. " +
"Their qlpack.yml files are either missing or is invalid.\n"}${multipleDeclaredLanguagesQueries.join("\n")}`);
}
}
@ -145,11 +145,11 @@ async function addBuiltinSuiteQueries(languages, codeQL, resultMap, packs, suite
let injectedMlQueries = false;
const found = builtinSuites.find((suite) => suite === suiteName);
if (!found) {
throw new Error(getQueryUsesInvalid(configFile, suiteName));
throw new util_1.UserError(getQueryUsesInvalid(configFile, suiteName));
}
if (suiteName === "security-experimental" &&
!(await (0, util_1.codeQlVersionAbove)(codeQL, codeql_1.CODEQL_VERSION_SECURITY_EXPERIMENTAL_SUITE))) {
throw new Error(`The 'security-experimental' suite is not supported on CodeQL CLI versions earlier than
throw new util_1.UserError(`The 'security-experimental' suite is not supported on CodeQL CLI versions earlier than
${codeql_1.CODEQL_VERSION_SECURITY_EXPERIMENTAL_SUITE}. Please upgrade to CodeQL CLI version
${codeql_1.CODEQL_VERSION_SECURITY_EXPERIMENTAL_SUITE} or later.`);
}
@ -188,13 +188,13 @@ async function addLocalQueries(codeQL, resultMap, localQueryPath, workspacePath,
let absoluteQueryPath = path.join(workspacePath, localQueryPath);
// Check the file exists
if (!fs.existsSync(absoluteQueryPath)) {
throw new Error(getLocalPathDoesNotExist(configFile, localQueryPath));
throw new util_1.UserError(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(fs.realpathSync(workspacePath) + path.sep)) {
throw new Error(getLocalPathOutsideOfRepository(configFile, localQueryPath));
throw new util_1.UserError(getLocalPathOutsideOfRepository(configFile, localQueryPath));
}
const extraSearchPath = workspacePath;
await runResolveQueries(codeQL, resultMap, [absoluteQueryPath], extraSearchPath);
@ -205,7 +205,7 @@ async function addLocalQueries(codeQL, resultMap, localQueryPath, workspacePath,
async function addRemoteQueries(codeQL, resultMap, queryUses, tempDir, apiDetails, logger, configFile) {
let tok = queryUses.split("@");
if (tok.length !== 2) {
throw new Error(getQueryUsesInvalid(configFile, queryUses));
throw new util_1.UserError(getQueryUsesInvalid(configFile, queryUses));
}
const ref = tok[1];
tok = tok[0].split("/");
@ -213,11 +213,11 @@ async function addRemoteQueries(codeQL, resultMap, queryUses, tempDir, apiDetail
// 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));
throw new util_1.UserError(getQueryUsesInvalid(configFile, queryUses));
}
// Check none of the parts of the repository name are empty
if (tok[0].trim() === "" || tok[1].trim() === "") {
throw new Error(getQueryUsesInvalid(configFile, queryUses));
throw new util_1.UserError(getQueryUsesInvalid(configFile, queryUses));
}
const nwo = `${tok[0]}/${tok[1]}`;
// Checkout the external repository
@ -243,7 +243,7 @@ async function addRemoteQueries(codeQL, resultMap, queryUses, tempDir, apiDetail
async function parseQueryUses(languages, codeQL, resultMap, packs, queryUses, tempDir, workspacePath, apiDetails, features, logger, configFile) {
queryUses = queryUses.trim();
if (queryUses === "") {
throw new Error(getQueryUsesInvalid(configFile));
throw new util_1.UserError(getQueryUsesInvalid(configFile));
}
// Check for the local path case before we start trying to parse the repository name
if (queryUses.startsWith("./")) {
@ -284,12 +284,12 @@ function validateAndSanitisePath(originalPath, propertyName, configFile, logger)
}
// An empty path is not allowed as it's meaningless
if (newPath === "") {
throw new Error(getConfigFilePropertyError(configFile, propertyName, `"${originalPath}" is not an invalid path. ` +
throw new util_1.UserError(getConfigFilePropertyError(configFile, propertyName, `"${originalPath}" is not an invalid path. ` +
`It is not necessary to include it, and it is not allowed to exclude it.`));
}
// Check for illegal uses of **
if (newPath.match(pathStarsRegex)) {
throw new Error(getConfigFilePropertyError(configFile, propertyName, `"${originalPath}" contains an invalid "**" wildcard. ` +
throw new util_1.UserError(getConfigFilePropertyError(configFile, propertyName, `"${originalPath}" contains an invalid "**" wildcard. ` +
`They must be immediately preceded and followed by a slash as in "/**/", or come at the start or end.`));
}
// Check for other regex characters that we don't support.
@ -302,7 +302,7 @@ function validateAndSanitisePath(originalPath, propertyName, configFile, logger)
// This may not play nicely with project layouts.
// This restriction can be lifted later if we determine they are ok.
if (newPath.indexOf("\\") !== -1) {
throw new Error(getConfigFilePropertyError(configFile, propertyName, `"${originalPath}" contains an "\\" character. These are not allowed in filters. ` +
throw new util_1.UserError(getConfigFilePropertyError(configFile, propertyName, `"${originalPath}" contains an "\\" character. These are not allowed in filters. ` +
`If running on windows we recommend using "/" instead for path filters.`));
}
return newPath;
@ -452,7 +452,7 @@ async function getLanguages(codeQL, languagesInput, repository, logger) {
// If the languages parameter was not given and no languages were
// detected then fail here as this is a workflow configuration error.
if (languages.length === 0) {
throw new Error(getNoLanguagesError());
throw new util_1.UserError(getNoLanguagesError());
}
// Make sure they are supported
const parsedLanguages = [];
@ -470,7 +470,7 @@ async function getLanguages(codeQL, languagesInput, repository, logger) {
// Any unknown languages here would have come directly from the input
// since we filter unknown languages coming from the GitHub API.
if (unknownLanguages.length > 0) {
throw new Error(getUnknownLanguagesError(unknownLanguages));
throw new util_1.UserError(getUnknownLanguagesError(unknownLanguages));
}
return parsedLanguages;
}
@ -595,10 +595,10 @@ async function loadConfig(languagesInput, rawQueriesInput, rawPacksInput, config
// even though we don't use the value yet.
if (NAME_PROPERTY in parsedYAML) {
if (typeof parsedYAML[NAME_PROPERTY] !== "string") {
throw new Error(getNameInvalid(configFile));
throw new util_1.UserError(getNameInvalid(configFile));
}
if (parsedYAML[NAME_PROPERTY].length === 0) {
throw new Error(getNameInvalid(configFile));
throw new util_1.UserError(getNameInvalid(configFile));
}
}
const languages = await getLanguages(codeQL, languagesInput, repository, logger);
@ -614,7 +614,7 @@ async function loadConfig(languagesInput, rawQueriesInput, rawPacksInput, config
let disableDefaultQueries = false;
if (DISABLE_DEFAULT_QUERIES_PROPERTY in parsedYAML) {
if (typeof parsedYAML[DISABLE_DEFAULT_QUERIES_PROPERTY] !== "boolean") {
throw new Error(getDisableDefaultQueriesInvalid(configFile));
throw new util_1.UserError(getDisableDefaultQueriesInvalid(configFile));
}
disableDefaultQueries = parsedYAML[DISABLE_DEFAULT_QUERIES_PROPERTY];
}
@ -635,33 +635,33 @@ async function loadConfig(languagesInput, rawQueriesInput, rawPacksInput, config
QUERIES_PROPERTY in parsedYAML) {
const queriesArr = parsedYAML[QUERIES_PROPERTY];
if (!Array.isArray(queriesArr)) {
throw new Error(getQueriesInvalid(configFile));
throw new util_1.UserError(getQueriesInvalid(configFile));
}
for (const query of queriesArr) {
if (typeof query[QUERIES_USES_PROPERTY] !== "string") {
throw new Error(getQueriesMissingUses(configFile));
throw new util_1.UserError(getQueriesMissingUses(configFile));
}
await parseQueryUses(languages, codeQL, queries, packs, query[QUERIES_USES_PROPERTY], tempDir, workspacePath, apiDetails, features, logger, configFile);
}
}
if (PATHS_IGNORE_PROPERTY in parsedYAML) {
if (!Array.isArray(parsedYAML[PATHS_IGNORE_PROPERTY])) {
throw new Error(getPathsIgnoreInvalid(configFile));
throw new util_1.UserError(getPathsIgnoreInvalid(configFile));
}
for (const ignorePath of parsedYAML[PATHS_IGNORE_PROPERTY]) {
if (typeof ignorePath !== "string" || ignorePath === "") {
throw new Error(getPathsIgnoreInvalid(configFile));
throw new util_1.UserError(getPathsIgnoreInvalid(configFile));
}
pathsIgnore.push(validateAndSanitisePath(ignorePath, PATHS_IGNORE_PROPERTY, configFile, logger));
}
}
if (PATHS_PROPERTY in parsedYAML) {
if (!Array.isArray(parsedYAML[PATHS_PROPERTY])) {
throw new Error(getPathsInvalid(configFile));
throw new util_1.UserError(getPathsInvalid(configFile));
}
for (const includePath of parsedYAML[PATHS_PROPERTY]) {
if (typeof includePath !== "string" || includePath === "") {
throw new Error(getPathsInvalid(configFile));
throw new util_1.UserError(getPathsInvalid(configFile));
}
paths.push(validateAndSanitisePath(includePath, PATHS_PROPERTY, configFile, logger));
}
@ -727,7 +727,7 @@ function parseQueriesFromInput(rawQueriesInput, queriesInputCombines) {
? rawQueriesInput.trim().slice(1).trim()
: rawQueriesInput?.trim() ?? "";
if (queriesInputCombines && trimmedInput.length === 0) {
throw new Error(getConfigFilePropertyError(undefined, "queries", "A '+' was used in the 'queries' input to specify that you wished to add some packs to your CodeQL analysis. However, no packs were specified. Please either remove the '+' or specify some packs."));
throw new util_1.UserError(getConfigFilePropertyError(undefined, "queries", "A '+' was used in the 'queries' input to specify that you wished to add some packs to your CodeQL analysis. However, no packs were specified. Please either remove the '+' or specify some packs."));
}
return trimmedInput.split(",").map((query) => ({ uses: query.trim() }));
}
@ -754,12 +754,12 @@ function parsePacksFromConfig(packsByLanguage, languages, configFile, logger) {
else {
// this is an error since multi-language analysis requires
// packs split by language
throw new Error(getPacksInvalidSplit(configFile));
throw new util_1.UserError(getPacksInvalidSplit(configFile));
}
}
for (const [lang, packsArr] of Object.entries(packsByLanguage)) {
if (!Array.isArray(packsArr)) {
throw new Error(getPacksInvalid(configFile));
throw new util_1.UserError(getPacksInvalid(configFile));
}
if (!languages.includes(lang)) {
// This particular language is not being analyzed in this run.
@ -769,7 +769,7 @@ function parsePacksFromConfig(packsByLanguage, languages, configFile, logger) {
}
else {
// This language is invalid, probably a misspelling
throw new Error(getPacksRequireLanguage(configFile, lang));
throw new util_1.UserError(getPacksRequireLanguage(configFile, lang));
}
}
packs[lang] = packsArr.map((packStr) => validatePackSpecification(packStr, configFile));
@ -782,16 +782,16 @@ function parsePacksFromInput(rawPacksInput, languages, packsInputCombines) {
return undefined;
}
if (languages.length > 1) {
throw new Error("Cannot specify a 'packs' input in a multi-language analysis. Use a codeql-config.yml file instead and specify packs by language.");
throw new util_1.UserError("Cannot specify a 'packs' input in a multi-language analysis. Use a codeql-config.yml file instead and specify packs by language.");
}
else if (languages.length === 0) {
throw new Error("No languages specified. Cannot process the packs input.");
throw new util_1.UserError("No languages specified. Cannot process the packs input.");
}
rawPacksInput = rawPacksInput.trim();
if (packsInputCombines) {
rawPacksInput = rawPacksInput.trim().substring(1).trim();
if (!rawPacksInput) {
throw new Error(getConfigFilePropertyError(undefined, "packs", "A '+' was used in the 'packs' input to specify that you wished to add some packs to your CodeQL analysis. However, no packs were specified. Please either remove the '+' or specify some packs."));
throw new util_1.UserError(getConfigFilePropertyError(undefined, "packs", "A '+' was used in the 'packs' input to specify that you wished to add some packs to your CodeQL analysis. However, no packs were specified. Please either remove the '+' or specify some packs."));
}
}
return {
@ -821,7 +821,7 @@ function parsePacksFromInput(rawPacksInput, languages, packsInputCombines) {
*/
function parsePacksSpecification(packStr, configFile) {
if (typeof packStr !== "string") {
throw new Error(getPacksStrInvalid(packStr, configFile));
throw new util_1.UserError(getPacksStrInvalid(packStr, configFile));
}
packStr = packStr.trim();
const atIndex = packStr.indexOf("@");
@ -842,7 +842,7 @@ function parsePacksSpecification(packStr, configFile) {
? packStr.slice(pathStart, pathEnd).trim()
: undefined;
if (!PACK_IDENTIFIER_PATTERN.test(packName)) {
throw new Error(getPacksStrInvalid(packStr, configFile));
throw new util_1.UserError(getPacksStrInvalid(packStr, configFile));
}
if (version) {
try {
@ -850,7 +850,7 @@ function parsePacksSpecification(packStr, configFile) {
}
catch (e) {
// The range string is invalid. OK to ignore the caught error
throw new Error(getPacksStrInvalid(packStr, configFile));
throw new util_1.UserError(getPacksStrInvalid(packStr, configFile));
}
}
if (packPath &&
@ -861,11 +861,11 @@ function parsePacksSpecification(packStr, configFile) {
// which seems more awkward.
path.normalize(packPath).split(path.sep).join("/") !==
packPath.split(path.sep).join("/"))) {
throw new Error(getPacksStrInvalid(packStr, configFile));
throw new util_1.UserError(getPacksStrInvalid(packStr, configFile));
}
if (!packPath && pathStart) {
// 0 length path
throw new Error(getPacksStrInvalid(packStr, configFile));
throw new util_1.UserError(getPacksStrInvalid(packStr, configFile));
}
return {
name: packName,
@ -891,7 +891,7 @@ function parsePacks(rawPacksFromConfig, rawPacksFromInput, packsInputCombines, l
}
if (!packsInputCombines) {
if (!packsFromInput) {
throw new Error(getPacksInvalid(configFile));
throw new util_1.UserError(getPacksInvalid(configFile));
}
return packsFromInput;
}
@ -964,7 +964,7 @@ async function initConfig(languagesInput, queriesInput, packsInput, registriesIn
const hasCustomQueries = config.queries[language]?.custom.length > 0;
const hasPacks = (config.packs[language]?.length || 0) > 0;
if (!hasPacks && !hasBuiltinQueries && !hasCustomQueries) {
throw new Error(`Did not detect any queries to run for ${language}. ` +
throw new util_1.UserError(`Did not detect any queries to run for ${language}. ` +
"Please make sure that the default queries are enabled, or you are specifying queries to run.");
}
}
@ -982,7 +982,7 @@ function parseRegistries(registriesInput) {
: undefined;
}
catch (e) {
throw new Error("Invalid registries input. Must be a YAML string.");
throw new util_1.UserError("Invalid registries input. Must be a YAML string.");
}
}
function isLocal(configPath) {
@ -995,11 +995,11 @@ function isLocal(configPath) {
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));
throw new util_1.UserError(getConfigFileOutsideWorkspaceErrorMessage(configFile));
}
// Error if the file does not exist
if (!fs.existsSync(configFile)) {
throw new Error(getConfigFileDoesNotExistErrorMessage(configFile));
throw new util_1.UserError(getConfigFileDoesNotExistErrorMessage(configFile));
}
return yaml.load(fs.readFileSync(configFile, "utf8"));
}
@ -1009,7 +1009,7 @@ async function getRemoteConfig(configFile, apiDetails) {
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));
throw new util_1.UserError(getConfigFileRepoFormatInvalidMessage(configFile));
}
const response = await api
.getApiClientWithExternalAuth(apiDetails)
@ -1024,10 +1024,10 @@ async function getRemoteConfig(configFile, apiDetails) {
fileContents = response.data.content;
}
else if (Array.isArray(response.data)) {
throw new Error(getConfigFileDirectoryGivenMessage(configFile));
throw new util_1.UserError(getConfigFileDirectoryGivenMessage(configFile));
}
else {
throw new Error(getConfigFileFormatInvalidMessage(configFile));
throw new util_1.UserError(getConfigFileFormatInvalidMessage(configFile));
}
return yaml.load(Buffer.from(fileContents, "base64").toString("binary"));
}
@ -1112,7 +1112,7 @@ async function generateRegistries(registriesInput, codeQL, tempDir, logger) {
let qlconfigFile;
if (registries) {
if (!(await (0, util_1.codeQlVersionAbove)(codeQL, codeql_1.CODEQL_VERSION_GHES_PACK_DOWNLOAD))) {
throw new Error(`The 'registries' input is not supported on CodeQL CLI versions earlier than ${codeql_1.CODEQL_VERSION_GHES_PACK_DOWNLOAD}. Please upgrade to CodeQL CLI version ${codeql_1.CODEQL_VERSION_GHES_PACK_DOWNLOAD} or later.`);
throw new util_1.UserError(`The 'registries' input is not supported on CodeQL CLI versions earlier than ${codeql_1.CODEQL_VERSION_GHES_PACK_DOWNLOAD}. Please upgrade to CodeQL CLI version ${codeql_1.CODEQL_VERSION_GHES_PACK_DOWNLOAD} or later.`);
}
// generate a qlconfig.yml file to hold the registry configs.
const qlconfig = createRegistriesBlock(registries);
@ -1139,7 +1139,7 @@ exports.generateRegistries = generateRegistries;
function createRegistriesBlock(registries) {
if (!Array.isArray(registries) ||
registries.some((r) => !r.url || !r.packages)) {
throw new Error("Invalid 'registries' input. Must be an array of objects with 'url' and 'packages' properties.");
throw new util_1.UserError("Invalid 'registries' input. Must be an array of objects with 'url' and 'packages' properties.");
}
// be sure to remove the `token` field from the registry before writing it to disk.
const safeRegistries = registries.map((registry) => ({