Permalink
codeql-action/lib/config-utils.js
Newer
100644
831 lines (831 sloc)
38.2 KB
2
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
if (k2 === undefined) k2 = k;
4
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
5
}) : (function(o, m, k, k2) {
6
if (k2 === undefined) k2 = k;
7
o[k2] = m[k];
8
}));
9
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
10
Object.defineProperty(o, "default", { enumerable: true, value: v });
11
}) : function(o, v) {
12
o["default"] = v;
13
});
14
var __importStar = (this && this.__importStar) || function (mod) {
15
if (mod && mod.__esModule) return mod;
16
var result = {};
17
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
18
__setModuleDefault(result, mod);
19
return result;
20
};
21
Object.defineProperty(exports, "__esModule", { value: true });
22
exports.getConfig = exports.getPathToParsedConfigFile = exports.initConfig = exports.parsePacks = exports.parsePacksFromConfig = exports.getDefaultConfig = exports.getUnknownLanguagesError = exports.getNoLanguagesError = exports.getConfigFileDirectoryGivenMessage = exports.getConfigFileFormatInvalidMessage = exports.getConfigFileRepoFormatInvalidMessage = exports.getConfigFileDoesNotExistErrorMessage = exports.getConfigFileOutsideWorkspaceErrorMessage = exports.getLocalPathDoesNotExist = exports.getLocalPathOutsideOfRepository = exports.getPacksStrInvalid = exports.getPacksInvalid = exports.getPacksInvalidSplit = exports.getPacksRequireLanguage = exports.getPathsInvalid = exports.getPathsIgnoreInvalid = exports.getQueryUsesInvalid = exports.getQueriesInvalid = exports.getDisableDefaultQueriesInvalid = exports.getNameInvalid = exports.validateAndSanitisePath = void 0;
23
const fs = __importStar(require("fs"));
24
const path = __importStar(require("path"));
25
const yaml = __importStar(require("js-yaml"));
26
const semver = __importStar(require("semver"));
27
const api = __importStar(require("./api-client"));
28
const codeql_1 = require("./codeql");
29
const externalQueries = __importStar(require("./external-queries"));
30
const feature_flags_1 = require("./feature-flags");
31
const languages_1 = require("./languages");
32
const util_1 = require("./util");
33
// Property names from the user-supplied config file.
34
const NAME_PROPERTY = "name";
35
const DISABLE_DEFAULT_QUERIES_PROPERTY = "disable-default-queries";
36
const QUERIES_PROPERTY = "queries";
37
const QUERIES_USES_PROPERTY = "uses";
38
const PATHS_IGNORE_PROPERTY = "paths-ignore";
39
const PATHS_PROPERTY = "paths";
40
const PACKS_PROPERTY = "packs";
41
/**
42
* A list of queries from https://github.com/github/codeql that
43
* we don't want to run. Disabling them here is a quicker alternative to
44
* disabling them in the code scanning query suites. Queries should also
45
* be disabled in the suites, and removed from this list here once the
46
* bundle is updated to make those suite changes live.
47
*
48
* Format is a map from language to an array of path suffixes of .ql files.
49
*/
50
const DISABLED_BUILTIN_QUERIES = {
51
csharp: [
52
"ql/src/Security Features/CWE-937/VulnerablePackage.ql",
53
"ql/src/Security Features/CWE-451/MissingXFrameOptions.ql",
54
],
55
};
56
function queryIsDisabled(language, query) {
57
return (DISABLED_BUILTIN_QUERIES[language] || []).some((disabledQuery) => query.endsWith(disabledQuery));
59
/**
60
* Asserts that the noDeclaredLanguage and multipleDeclaredLanguages fields are
61
* both empty and errors if they are not.
62
*/
63
function validateQueries(resolvedQueries) {
64
const noDeclaredLanguage = resolvedQueries.noDeclaredLanguage;
65
const noDeclaredLanguageQueries = Object.keys(noDeclaredLanguage);
66
if (noDeclaredLanguageQueries.length !== 0) {
67
throw new Error(`${"The following queries do not declare a language. " +
68
"Their qlpack.yml files are either missing or is invalid.\n"}${noDeclaredLanguageQueries.join("\n")}`);
69
}
70
const multipleDeclaredLanguages = resolvedQueries.multipleDeclaredLanguages;
71
const multipleDeclaredLanguagesQueries = Object.keys(multipleDeclaredLanguages);
72
if (multipleDeclaredLanguagesQueries.length !== 0) {
73
throw new Error(`${"The following queries declare multiple languages. " +
74
"Their qlpack.yml files are either missing or is invalid.\n"}${multipleDeclaredLanguagesQueries.join("\n")}`);
77
/**
78
* Run 'codeql resolve queries' and add the results to resultMap
79
*
80
* If a checkout path is given then the queries are assumed to be custom queries
81
* and an error will be thrown if there is anything invalid about the queries.
82
* If a checkout path is not given then the queries are assumed to be builtin
83
* queries, and error checking will be suppressed.
84
*/
85
async function runResolveQueries(codeQL, resultMap, toResolve, extraSearchPath) {
86
const resolvedQueries = await codeQL.resolveQueries(toResolve, extraSearchPath);
87
if (extraSearchPath !== undefined) {
88
validateQueries(resolvedQueries);
89
}
90
for (const [language, queryPaths] of Object.entries(resolvedQueries.byLanguage)) {
91
if (resultMap[language] === undefined) {
92
resultMap[language] = {
93
builtin: [],
94
custom: [],
95
};
96
}
97
const queries = Object.keys(queryPaths).filter((q) => !queryIsDisabled(language, q));
98
if (extraSearchPath !== undefined) {
99
resultMap[language].custom.push({
100
searchPath: extraSearchPath,
101
queries,
102
});
103
}
104
else {
105
resultMap[language].builtin.push(...queries);
106
}
107
}
108
}
109
/**
110
* Get the set of queries included by default.
111
*/
112
async function addDefaultQueries(codeQL, languages, resultMap) {
113
const suites = languages.map((l) => `${l}-code-scanning.qls`);
114
await runResolveQueries(codeQL, resultMap, suites, undefined);
115
}
116
// The set of acceptable values for built-in suites from the codeql bundle
117
const builtinSuites = ["security-extended", "security-and-quality"];
118
/**
119
* Determine the set of queries associated with suiteName's suites and add them to resultMap.
120
* Throws an error if suiteName is not a valid builtin suite.
121
* May inject ML queries, and the return value will declare if this was done.
122
*/
123
async function addBuiltinSuiteQueries(languages, codeQL, resultMap, packs, suiteName, featureFlags, configFile) {
124
var _a;
125
let injectedMlQueries = false;
126
const found = builtinSuites.find((suite) => suite === suiteName);
127
if (!found) {
128
throw new Error(getQueryUsesInvalid(configFile, suiteName));
129
}
130
// If we're running the JavaScript security-extended analysis (or a superset of it), the repo is
131
// opted into the ML-powered queries beta, and a user hasn't already added the ML-powered query
132
// pack, then add the ML-powered query pack so that we run ML-powered queries.
133
if (
134
// Disable ML-powered queries on Windows
135
process.platform !== "win32" &&
136
languages.includes("javascript") &&
137
(found === "security-extended" || found === "security-and-quality") &&
138
!((_a = packs.javascript) === null || _a === void 0 ? void 0 : _a.some((pack) => pack.packName === util_1.ML_POWERED_JS_QUERIES_PACK_NAME)) &&
139
(await featureFlags.getValue(feature_flags_1.FeatureFlag.MlPoweredQueriesEnabled)) &&
140
(await (0, util_1.codeQlVersionAbove)(codeQL, codeql_1.CODEQL_VERSION_ML_POWERED_QUERIES))) {
141
if (!packs.javascript) {
142
packs.javascript = [];
143
}
144
packs.javascript.push(await (0, util_1.getMlPoweredJsQueriesPack)(codeQL));
145
injectedMlQueries = true;
146
}
147
const suites = languages.map((l) => `${l}-${suiteName}.qls`);
148
await runResolveQueries(codeQL, resultMap, suites, undefined);
149
return injectedMlQueries;
150
}
151
/**
152
* Retrieve the set of queries at localQueryPath and add them to resultMap.
153
*/
154
async function addLocalQueries(codeQL, resultMap, localQueryPath, workspacePath, configFile) {
155
// Resolve the local path against the workspace so that when this is
156
// passed to codeql it resolves to exactly the path we expect it to resolve to.
157
let absoluteQueryPath = path.join(workspacePath, localQueryPath);
158
// Check the file exists
159
if (!fs.existsSync(absoluteQueryPath)) {
160
throw new Error(getLocalPathDoesNotExist(configFile, localQueryPath));
161
}
162
// Call this after checking file exists, because it'll fail if file doesn't exist
163
absoluteQueryPath = fs.realpathSync(absoluteQueryPath);
164
// Check the local path doesn't jump outside the repo using '..' or symlinks
165
if (!(absoluteQueryPath + path.sep).startsWith(fs.realpathSync(workspacePath) + path.sep)) {
166
throw new Error(getLocalPathOutsideOfRepository(configFile, localQueryPath));
167
}
168
const extraSearchPath = workspacePath;
169
await runResolveQueries(codeQL, resultMap, [absoluteQueryPath], extraSearchPath);
170
}
171
/**
172
* Retrieve the set of queries at the referenced remote repo and add them to resultMap.
173
*/
174
async function addRemoteQueries(codeQL, resultMap, queryUses, tempDir, apiDetails, logger, configFile) {
175
let tok = queryUses.split("@");
176
if (tok.length !== 2) {
177
throw new Error(getQueryUsesInvalid(configFile, queryUses));
178
}
179
const ref = tok[1];
180
tok = tok[0].split("/");
181
// The first token is the owner
182
// The second token is the repo
183
// The rest is a path, if there is more than one token combine them to form the full path
184
if (tok.length < 2) {
185
throw new Error(getQueryUsesInvalid(configFile, queryUses));
186
}
187
// Check none of the parts of the repository name are empty
188
if (tok[0].trim() === "" || tok[1].trim() === "") {
189
throw new Error(getQueryUsesInvalid(configFile, queryUses));
190
}
191
const nwo = `${tok[0]}/${tok[1]}`;
192
// Checkout the external repository
193
const checkoutPath = await externalQueries.checkoutExternalRepository(nwo, ref, apiDetails, tempDir, logger);
194
const queryPath = tok.length > 2
195
? path.join(checkoutPath, tok.slice(2).join("/"))
196
: checkoutPath;
197
await runResolveQueries(codeQL, resultMap, [queryPath], checkoutPath);
198
}
199
/**
200
* Parse a query 'uses' field to a discrete set of query files and update resultMap.
201
*
202
* The logic for parsing the string is based on what actions does for
203
* parsing the 'uses' actions in the workflow file. So it can handle
204
* local paths starting with './', or references to remote repos, or
205
* a finite set of hardcoded terms for builtin suites.
206
*
207
* This may inject ML queries into the packs to use, and the return value will
208
* declare if this was done.
209
*
210
* @returns whether or not we injected ML queries into the packs
211
*/
212
async function parseQueryUses(languages, codeQL, resultMap, packs, queryUses, tempDir, workspacePath, apiDetails, featureFlags, logger, configFile) {
213
queryUses = queryUses.trim();
214
if (queryUses === "") {
215
throw new Error(getQueryUsesInvalid(configFile));
216
}
217
// Check for the local path case before we start trying to parse the repository name
218
if (queryUses.startsWith("./")) {
219
await addLocalQueries(codeQL, resultMap, queryUses.slice(2), workspacePath, configFile);
220
return false;
221
}
222
// Check for one of the builtin suites
223
if (queryUses.indexOf("/") === -1 && queryUses.indexOf("@") === -1) {
224
return await addBuiltinSuiteQueries(languages, codeQL, resultMap, packs, queryUses, featureFlags, configFile);
225
}
226
// Otherwise, must be a reference to another repo
227
await addRemoteQueries(codeQL, resultMap, queryUses, tempDir, apiDetails, logger, configFile);
228
return false;
229
}
230
// Regex validating stars in paths or paths-ignore entries.
231
// The intention is to only allow ** to appear when immediately
232
// preceded and followed by a slash.
233
const pathStarsRegex = /.*(?:\*\*[^/].*|\*\*$|[^/]\*\*.*)/;
234
// Characters that are supported by filters in workflows, but not by us.
235
// See https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet
236
const filterPatternCharactersRegex = /.*[?+[\]!].*/;
237
// Checks that a paths of paths-ignore entry is valid, possibly modifying it
238
// to make it valid, or if not possible then throws an error.
239
function validateAndSanitisePath(originalPath, propertyName, configFile, logger) {
240
// Take a copy so we don't modify the original path, so we can still construct error messages
241
let newPath = originalPath;
242
// All paths are relative to the src root, so strip off leading slashes.
243
while (newPath.charAt(0) === "/") {
244
newPath = newPath.substring(1);
245
}
246
// Trailing ** are redundant, so strip them off
247
if (newPath.endsWith("/**")) {
248
newPath = newPath.substring(0, newPath.length - 2);
250
// An empty path is not allowed as it's meaningless
252
throw new Error(getConfigFilePropertyError(configFile, propertyName, `"${originalPath}" is not an invalid path. ` +
253
`It is not necessary to include it, and it is not allowed to exclude it.`));
255
// Check for illegal uses of **
256
if (newPath.match(pathStarsRegex)) {
257
throw new Error(getConfigFilePropertyError(configFile, propertyName, `"${originalPath}" contains an invalid "**" wildcard. ` +
258
`They must be immediately preceded and followed by a slash as in "/**/", or come at the start or end.`));
260
// Check for other regex characters that we don't support.
261
// Output a warning so the user knows, but otherwise continue normally.
262
if (newPath.match(filterPatternCharactersRegex)) {
263
logger.warning(getConfigFilePropertyError(configFile, propertyName, `"${originalPath}" contains an unsupported character. ` +
264
`The filter pattern characters ?, +, [, ], ! are not supported and will be matched literally.`));
266
// Ban any uses of backslash for now.
267
// This may not play nicely with project layouts.
268
// This restriction can be lifted later if we determine they are ok.
269
if (newPath.indexOf("\\") !== -1) {
270
throw new Error(getConfigFilePropertyError(configFile, propertyName, `"${originalPath}" contains an "\\" character. These are not allowed in filters. ` +
271
`If running on windows we recommend using "/" instead for path filters.`));
274
}
275
exports.validateAndSanitisePath = validateAndSanitisePath;
276
// An undefined configFile in some of these functions indicates that
277
// the property was in a workflow file, not a config file
278
function getNameInvalid(configFile) {
279
return getConfigFilePropertyError(configFile, NAME_PROPERTY, "must be a non-empty string");
281
exports.getNameInvalid = getNameInvalid;
282
function getDisableDefaultQueriesInvalid(configFile) {
283
return getConfigFilePropertyError(configFile, DISABLE_DEFAULT_QUERIES_PROPERTY, "must be a boolean");
285
exports.getDisableDefaultQueriesInvalid = getDisableDefaultQueriesInvalid;
286
function getQueriesInvalid(configFile) {
287
return getConfigFilePropertyError(configFile, QUERIES_PROPERTY, "must be an array");
288
}
289
exports.getQueriesInvalid = getQueriesInvalid;
290
function getQueryUsesInvalid(configFile, queryUses) {
291
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}` : ""}`);
292
}
293
exports.getQueryUsesInvalid = getQueryUsesInvalid;
294
function getPathsIgnoreInvalid(configFile) {
295
return getConfigFilePropertyError(configFile, PATHS_IGNORE_PROPERTY, "must be an array of non-empty strings");
296
}
297
exports.getPathsIgnoreInvalid = getPathsIgnoreInvalid;
298
function getPathsInvalid(configFile) {
299
return getConfigFilePropertyError(configFile, PATHS_PROPERTY, "must be an array of non-empty strings");
300
}
301
exports.getPathsInvalid = getPathsInvalid;
302
function getPacksRequireLanguage(lang, configFile) {
303
return getConfigFilePropertyError(configFile, PACKS_PROPERTY, `has "${lang}", but it is not one of the languages to analyze`);
304
}
305
exports.getPacksRequireLanguage = getPacksRequireLanguage;
306
function getPacksInvalidSplit(configFile) {
307
return getConfigFilePropertyError(configFile, PACKS_PROPERTY, "must split packages by language");
308
}
309
exports.getPacksInvalidSplit = getPacksInvalidSplit;
310
function getPacksInvalid(configFile) {
311
return getConfigFilePropertyError(configFile, PACKS_PROPERTY, "must be an array of non-empty strings");
312
}
313
exports.getPacksInvalid = getPacksInvalid;
314
function getPacksStrInvalid(packStr, configFile) {
315
return configFile
316
? getConfigFilePropertyError(configFile, PACKS_PROPERTY, `"${packStr}" is not a valid pack`)
317
: `"${packStr}" is not a valid pack`;
318
}
319
exports.getPacksStrInvalid = getPacksStrInvalid;
320
function getLocalPathOutsideOfRepository(configFile, localPath) {
321
return getConfigFilePropertyError(configFile, `${QUERIES_PROPERTY}.${QUERIES_USES_PROPERTY}`, `is invalid as the local path "${localPath}" is outside of the repository`);
322
}
323
exports.getLocalPathOutsideOfRepository = getLocalPathOutsideOfRepository;
324
function getLocalPathDoesNotExist(configFile, localPath) {
325
return getConfigFilePropertyError(configFile, `${QUERIES_PROPERTY}.${QUERIES_USES_PROPERTY}`, `is invalid as the local path "${localPath}" does not exist in the repository`);
326
}
327
exports.getLocalPathDoesNotExist = getLocalPathDoesNotExist;
328
function getConfigFileOutsideWorkspaceErrorMessage(configFile) {
329
return `The configuration file "${configFile}" is outside of the workspace`;
330
}
331
exports.getConfigFileOutsideWorkspaceErrorMessage = getConfigFileOutsideWorkspaceErrorMessage;
332
function getConfigFileDoesNotExistErrorMessage(configFile) {
333
return `The configuration file "${configFile}" does not exist`;
334
}
335
exports.getConfigFileDoesNotExistErrorMessage = getConfigFileDoesNotExistErrorMessage;
336
function getConfigFileRepoFormatInvalidMessage(configFile) {
337
let error = `The configuration file "${configFile}" is not a supported remote file reference.`;
338
error += " Expected format <owner>/<repository>/<file-path>@<ref>";
339
return error;
340
}
341
exports.getConfigFileRepoFormatInvalidMessage = getConfigFileRepoFormatInvalidMessage;
342
function getConfigFileFormatInvalidMessage(configFile) {
343
return `The configuration file "${configFile}" could not be read`;
344
}
345
exports.getConfigFileFormatInvalidMessage = getConfigFileFormatInvalidMessage;
346
function getConfigFileDirectoryGivenMessage(configFile) {
347
return `The configuration file "${configFile}" looks like a directory, not a file`;
348
}
349
exports.getConfigFileDirectoryGivenMessage = getConfigFileDirectoryGivenMessage;
350
function getConfigFilePropertyError(configFile, property, error) {
351
if (configFile === undefined) {
352
return `The workflow property "${property}" is invalid: ${error}`;
353
}
354
else {
355
return `The configuration file "${configFile}" is invalid: property "${property}" ${error}`;
356
}
359
return ("Did not detect any languages to analyze. " +
360
"Please update input in workflow or check that GitHub detects the correct languages in your repository.");
361
}
362
exports.getNoLanguagesError = getNoLanguagesError;
363
function getUnknownLanguagesError(languages) {
364
return `Did not recognise the following languages: ${languages.join(", ")}`;
365
}
366
exports.getUnknownLanguagesError = getUnknownLanguagesError;
367
/**
368
* Gets the set of languages in the current repository
369
*/
370
async function getLanguagesInRepo(repository, apiDetails, logger) {
371
logger.debug(`GitHub repo ${repository.owner} ${repository.repo}`);
372
const response = await api.getApiClient(apiDetails).repos.listLanguages({
373
owner: repository.owner,
374
repo: repository.repo,
375
});
376
logger.debug(`Languages API response: ${JSON.stringify(response)}`);
377
// The GitHub API is going to return languages in order of popularity,
378
// When we pick a language to autobuild we want to pick the most popular traced language
379
// Since sets in javascript maintain insertion order, using a set here and then splatting it
380
// into an array gives us an array of languages ordered by popularity
381
const languages = new Set();
382
for (const lang of Object.keys(response.data)) {
383
const parsedLang = (0, languages_1.parseLanguage)(lang);
384
if (parsedLang !== undefined) {
385
languages.add(parsedLang);
386
}
387
}
388
return [...languages];
389
}
390
/**
391
* Get the languages to analyse.
392
*
393
* The result is obtained from the action input parameter 'languages' if that
394
* has been set, otherwise it is deduced as all languages in the repo that
395
* can be analysed.
396
*
397
* If no languages could be detected from either the workflow or the repository
398
* then throw an error.
399
*/
400
async function getLanguages(codeQL, languagesInput, repository, apiDetails, logger) {
401
// Obtain from action input 'languages' if set
402
let languages = (languagesInput || "")
403
.split(",")
404
.map((x) => x.trim())
405
.filter((x) => x.length > 0);
406
logger.info(`Languages from configuration: ${JSON.stringify(languages)}`);
407
if (languages.length === 0) {
408
// Obtain languages as all languages in the repo that can be analysed
409
languages = await getLanguagesInRepo(repository, apiDetails, logger);
410
const availableLanguages = await codeQL.resolveLanguages();
411
languages = languages.filter((value) => value in availableLanguages);
412
logger.info(`Automatically detected languages: ${JSON.stringify(languages)}`);
413
}
414
// If the languages parameter was not given and no languages were
415
// detected then fail here as this is a workflow configuration error.
416
if (languages.length === 0) {
417
throw new Error(getNoLanguagesError());
418
}
419
// Make sure they are supported
420
const parsedLanguages = [];
421
const unknownLanguages = [];
422
for (const language of languages) {
423
const parsedLanguage = (0, languages_1.parseLanguage)(language);
424
if (parsedLanguage === undefined) {
425
unknownLanguages.push(language);
426
}
427
else if (parsedLanguages.indexOf(parsedLanguage) === -1) {
428
parsedLanguages.push(parsedLanguage);
429
}
430
}
431
if (unknownLanguages.length > 0) {
432
throw new Error(getUnknownLanguagesError(unknownLanguages));
433
}
434
return parsedLanguages;
435
}
436
async function addQueriesAndPacksFromWorkflow(codeQL, queriesInput, languages, resultMap, packs, tempDir, workspacePath, apiDetails, featureFlags, logger) {
437
let injectedMlQueries = false;
438
queriesInput = queriesInput.trim();
439
// "+" means "don't override config file" - see shouldAddConfigFileQueries
440
queriesInput = queriesInput.replace(/^\+/, "");
441
for (const query of queriesInput.split(",")) {
442
const didInject = await parseQueryUses(languages, codeQL, resultMap, packs, query, tempDir, workspacePath, apiDetails, featureFlags, logger);
443
injectedMlQueries = injectedMlQueries || didInject;
444
}
445
return injectedMlQueries;
446
}
447
// Returns true if either no queries were provided in the workflow.
448
// or if the queries in the workflow were provided in "additive" mode,
449
// indicating that they shouldn't override the config queries but
450
// should instead be added in addition
451
function shouldAddConfigFileQueries(queriesInput) {
452
if (queriesInput) {
453
return queriesInput.trimStart().slice(0, 1) === "+";
454
}
455
return true;
456
}
457
/**
458
* Get the default config for when the user has not supplied one.
459
*/
460
async function getDefaultConfig(languagesInput, queriesInput, packsInput, dbLocation, debugMode, debugArtifactName, debugDatabaseName, repository, tempDir, toolCacheDir, codeQL, workspacePath, gitHubVersion, apiDetails, featureFlags, logger) {
462
const languages = await getLanguages(codeQL, languagesInput, repository, apiDetails, logger);
463
const queries = {};
464
for (const language of languages) {
465
queries[language] = {
466
builtin: [],
467
custom: [],
468
};
469
}
470
await addDefaultQueries(codeQL, languages, queries);
471
const packs = (_a = parsePacksFromInput(packsInput, languages)) !== null && _a !== void 0 ? _a : {};
472
let injectedMlQueries = false;
473
if (queriesInput) {
474
injectedMlQueries = await addQueriesAndPacksFromWorkflow(codeQL, queriesInput, languages, queries, packs, tempDir, workspacePath, apiDetails, featureFlags, logger);
476
return {
477
languages,
478
queries,
479
pathsIgnore: [],
480
paths: [],
482
originalUserInput: {},
483
tempDir,
484
toolCacheDir,
485
codeQLCmd: codeQL.getPath(),
486
gitHubVersion,
487
dbLocation: dbLocationOrDefault(dbLocation, tempDir),
488
debugMode,
489
debugArtifactName,
490
debugDatabaseName,
491
injectedMlQueries,
492
};
493
}
494
exports.getDefaultConfig = getDefaultConfig;
495
/**
496
* Load the config from the given file.
497
*/
498
async function loadConfig(languagesInput, queriesInput, packsInput, configFile, dbLocation, debugMode, debugArtifactName, debugDatabaseName, repository, tempDir, toolCacheDir, codeQL, workspacePath, gitHubVersion, apiDetails, featureFlags, logger) {
500
let parsedYAML;
501
if (isLocal(configFile)) {
502
// Treat the config file as relative to the workspace
503
configFile = path.resolve(workspacePath, configFile);
504
parsedYAML = getLocalConfig(configFile, workspacePath);
506
else {
507
parsedYAML = await getRemoteConfig(configFile, apiDetails);
509
// Validate that the 'name' property is syntactically correct,
510
// even though we don't use the value yet.
511
if (NAME_PROPERTY in parsedYAML) {
512
if (typeof parsedYAML[NAME_PROPERTY] !== "string") {
513
throw new Error(getNameInvalid(configFile));
514
}
515
if (parsedYAML[NAME_PROPERTY].length === 0) {
516
throw new Error(getNameInvalid(configFile));
517
}
519
const languages = await getLanguages(codeQL, languagesInput, repository, apiDetails, logger);
520
const queries = {};
521
for (const language of languages) {
522
queries[language] = {
523
builtin: [],
524
custom: [],
525
};
526
}
527
const pathsIgnore = [];
528
const paths = [];
529
let disableDefaultQueries = false;
530
if (DISABLE_DEFAULT_QUERIES_PROPERTY in parsedYAML) {
531
if (typeof parsedYAML[DISABLE_DEFAULT_QUERIES_PROPERTY] !== "boolean") {
532
throw new Error(getDisableDefaultQueriesInvalid(configFile));
533
}
534
disableDefaultQueries = parsedYAML[DISABLE_DEFAULT_QUERIES_PROPERTY];
535
}
536
if (!disableDefaultQueries) {
537
await addDefaultQueries(codeQL, languages, queries);
539
const packs = parsePacks((_a = parsedYAML[PACKS_PROPERTY]) !== null && _a !== void 0 ? _a : {}, packsInput, languages, configFile);
540
// If queries were provided using `with` in the action configuration,
541
// they should take precedence over the queries in the config file
542
// unless they're prefixed with "+", in which case they supplement those
543
// in the config file.
544
let injectedMlQueries = false;
545
if (queriesInput) {
546
injectedMlQueries = await addQueriesAndPacksFromWorkflow(codeQL, queriesInput, languages, queries, packs, tempDir, workspacePath, apiDetails, featureFlags, logger);
548
if (shouldAddConfigFileQueries(queriesInput) &&
549
QUERIES_PROPERTY in parsedYAML) {
550
const queriesArr = parsedYAML[QUERIES_PROPERTY];
551
if (!Array.isArray(queriesArr)) {
552
throw new Error(getQueriesInvalid(configFile));
553
}
554
for (const query of queriesArr) {
555
if (!(QUERIES_USES_PROPERTY in query) ||
556
typeof query[QUERIES_USES_PROPERTY] !== "string") {
557
throw new Error(getQueryUsesInvalid(configFile));
559
await parseQueryUses(languages, codeQL, queries, packs, query[QUERIES_USES_PROPERTY], tempDir, workspacePath, apiDetails, featureFlags, logger, configFile);
560
}
562
if (PATHS_IGNORE_PROPERTY in parsedYAML) {
563
if (!Array.isArray(parsedYAML[PATHS_IGNORE_PROPERTY])) {
564
throw new Error(getPathsIgnoreInvalid(configFile));
565
}
566
for (const ignorePath of parsedYAML[PATHS_IGNORE_PROPERTY]) {
567
if (typeof ignorePath !== "string" || ignorePath === "") {
568
throw new Error(getPathsIgnoreInvalid(configFile));
570
pathsIgnore.push(validateAndSanitisePath(ignorePath, PATHS_IGNORE_PROPERTY, configFile, logger));
571
}
573
if (PATHS_PROPERTY in parsedYAML) {
574
if (!Array.isArray(parsedYAML[PATHS_PROPERTY])) {
575
throw new Error(getPathsInvalid(configFile));
576
}
577
for (const includePath of parsedYAML[PATHS_PROPERTY]) {
578
if (typeof includePath !== "string" || includePath === "") {
579
throw new Error(getPathsInvalid(configFile));
581
paths.push(validateAndSanitisePath(includePath, PATHS_PROPERTY, configFile, logger));
582
}
584
return {
585
languages,
586
queries,
587
pathsIgnore,
588
paths,
589
packs,
590
originalUserInput: parsedYAML,
591
tempDir,
592
toolCacheDir,
593
codeQLCmd: codeQL.getPath(),
594
gitHubVersion,
595
dbLocation: dbLocationOrDefault(dbLocation, tempDir),
596
debugMode,
597
debugArtifactName,
598
debugDatabaseName,
599
injectedMlQueries,
601
}
602
/**
603
* Pack names must be in the form of `scope/name`, with only alpha-numeric characters,
604
* and `-` allowed as long as not the first or last char.
605
**/
606
const PACK_IDENTIFIER_PATTERN = (function () {
607
const alphaNumeric = "[a-z0-9]";
608
const alphaNumericDash = "[a-z0-9-]";
609
const component = `${alphaNumeric}(${alphaNumericDash}*${alphaNumeric})?`;
610
return new RegExp(`^${component}/${component}$`);
611
})();
612
// Exported for testing
613
function parsePacksFromConfig(packsByLanguage, languages, configFile) {
614
const packs = {};
615
if (Array.isArray(packsByLanguage)) {
616
if (languages.length === 1) {
617
// single language analysis, so language is implicit
618
packsByLanguage = {
619
[languages[0]]: packsByLanguage,
620
};
621
}
622
else {
623
// this is an error since multi-language analysis requires
624
// packs split by language
625
throw new Error(getPacksInvalidSplit(configFile));
626
}
627
}
628
for (const [lang, packsArr] of Object.entries(packsByLanguage)) {
629
if (!Array.isArray(packsArr)) {
630
throw new Error(getPacksInvalid(configFile));
631
}
632
if (!languages.includes(lang)) {
633
throw new Error(getPacksRequireLanguage(lang, configFile));
634
}
635
packs[lang] = [];
636
for (const packStr of packsArr) {
637
packs[lang].push(toPackWithVersion(packStr, configFile));
638
}
639
}
640
return packs;
641
}
642
exports.parsePacksFromConfig = parsePacksFromConfig;
643
function parsePacksFromInput(packsInput, languages) {
644
if (!(packsInput === null || packsInput === void 0 ? void 0 : packsInput.trim())) {
645
return undefined;
646
}
647
if (languages.length > 1) {
648
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.");
649
}
650
else if (languages.length === 0) {
651
throw new Error("No languages specified. Cannot process the packs input.");
652
}
653
packsInput = packsInput.trim();
654
if (packsInput.startsWith("+")) {
655
packsInput = packsInput.substring(1).trim();
656
if (!packsInput) {
657
throw new Error("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.");
658
}
659
}
660
return {
661
[languages[0]]: packsInput.split(",").reduce((packs, pack) => {
662
packs.push(toPackWithVersion(pack, ""));
663
return packs;
664
}, []),
665
};
666
}
667
function toPackWithVersion(packStr, configFile) {
668
if (typeof packStr !== "string") {
669
throw new Error(getPacksStrInvalid(packStr, configFile));
670
}
671
const nameWithVersion = packStr.trim().split("@");
672
let version;
673
if (nameWithVersion.length > 2 ||
674
!PACK_IDENTIFIER_PATTERN.test(nameWithVersion[0])) {
675
throw new Error(getPacksStrInvalid(packStr, configFile));
676
}
677
else if (nameWithVersion.length === 2) {
678
version = semver.clean(nameWithVersion[1]) || undefined;
679
if (!version) {
680
throw new Error(getPacksStrInvalid(packStr, configFile));
681
}
682
}
683
return {
684
packName: nameWithVersion[0].trim(),
685
version,
686
};
687
}
688
// exported for testing
689
function parsePacks(rawPacksFromConfig, rawPacksInput, languages, configFile) {
690
const packsFromInput = parsePacksFromInput(rawPacksInput, languages);
691
const packsFomConfig = parsePacksFromConfig(rawPacksFromConfig, languages, configFile);
692
if (!packsFromInput) {
693
return packsFomConfig;
694
}
695
if (!shouldCombinePacks(rawPacksInput)) {
696
return packsFromInput;
697
}
698
return combinePacks(packsFromInput, packsFomConfig);
699
}
700
exports.parsePacks = parsePacks;
701
function shouldCombinePacks(packsInput) {
702
return !!(packsInput === null || packsInput === void 0 ? void 0 : packsInput.trim().startsWith("+"));
703
}
704
function combinePacks(packs1, packs2) {
705
const packs = {};
706
for (const lang of Object.keys(packs1)) {
707
packs[lang] = packs1[lang].concat(packs2[lang] || []);
708
}
709
for (const lang of Object.keys(packs2)) {
710
if (!packs[lang]) {
711
packs[lang] = packs2[lang];
712
}
713
}
714
return packs;
715
}
716
function dbLocationOrDefault(dbLocation, tempDir) {
717
return dbLocation || path.resolve(tempDir, "codeql_databases");
718
}
719
/**
720
* Load and return the config.
721
*
722
* This will parse the config from the user input if present, or generate
723
* a default config. The parsed config is then stored to a known location.
724
*/
725
async function initConfig(languagesInput, queriesInput, packsInput, configFile, dbLocation, debugMode, debugArtifactName, debugDatabaseName, repository, tempDir, toolCacheDir, codeQL, workspacePath, gitHubVersion, apiDetails, featureFlags, logger) {
726
var _a, _b, _c;
727
let config;
728
// If no config file was provided create an empty one
729
if (!configFile) {
730
logger.debug("No configuration file was provided");
731
config = await getDefaultConfig(languagesInput, queriesInput, packsInput, dbLocation, debugMode, debugArtifactName, debugDatabaseName, repository, tempDir, toolCacheDir, codeQL, workspacePath, gitHubVersion, apiDetails, featureFlags, logger);
732
}
733
else {
734
config = await loadConfig(languagesInput, queriesInput, packsInput, configFile, dbLocation, debugMode, debugArtifactName, debugDatabaseName, repository, tempDir, toolCacheDir, codeQL, workspacePath, gitHubVersion, apiDetails, featureFlags, logger);
735
}
736
// The list of queries should not be empty for any language. If it is then
737
// it is a user configuration error.
738
for (const language of config.languages) {
739
const hasBuiltinQueries = ((_a = config.queries[language]) === null || _a === void 0 ? void 0 : _a.builtin.length) > 0;
740
const hasCustomQueries = ((_b = config.queries[language]) === null || _b === void 0 ? void 0 : _b.custom.length) > 0;
741
const hasPacks = (((_c = config.packs[language]) === null || _c === void 0 ? void 0 : _c.length) || 0) > 0;
742
if (!hasPacks && !hasBuiltinQueries && !hasCustomQueries) {
743
throw new Error(`Did not detect any queries to run for ${language}. ` +
744
"Please make sure that the default queries are enabled, or you are specifying queries to run.");
745
}
746
}
747
// Save the config so we can easily access it again in the future
748
await saveConfig(config, logger);
749
return config;
750
}
751
exports.initConfig = initConfig;
752
function isLocal(configPath) {
753
// If the path starts with ./, look locally
754
if (configPath.indexOf("./") === 0) {
755
return true;
756
}
757
return configPath.indexOf("@") === -1;
758
}
759
function getLocalConfig(configFile, workspacePath) {
760
// Error if the config file is now outside of the workspace
761
if (!(configFile + path.sep).startsWith(workspacePath + path.sep)) {
762
throw new Error(getConfigFileOutsideWorkspaceErrorMessage(configFile));
763
}
764
// Error if the file does not exist
765
if (!fs.existsSync(configFile)) {
766
throw new Error(getConfigFileDoesNotExistErrorMessage(configFile));
767
}
768
return yaml.load(fs.readFileSync(configFile, "utf8"));
769
}
770
async function getRemoteConfig(configFile, apiDetails) {
771
// retrieve the various parts of the config location, and ensure they're present
772
const format = new RegExp("(?<owner>[^/]+)/(?<repo>[^/]+)/(?<path>[^@]+)@(?<ref>.*)");
773
const pieces = format.exec(configFile);
774
// 5 = 4 groups + the whole expression
775
if (pieces === null || pieces.groups === undefined || pieces.length < 5) {
776
throw new Error(getConfigFileRepoFormatInvalidMessage(configFile));
777
}
778
const response = await api
779
.getApiClient(apiDetails, { allowExternal: true })
780
.repos.getContent({
781
owner: pieces.groups.owner,
782
repo: pieces.groups.repo,
783
path: pieces.groups.path,
784
ref: pieces.groups.ref,
785
});
786
let fileContents;
787
if ("content" in response.data && response.data.content !== undefined) {
788
fileContents = response.data.content;
789
}
790
else if (Array.isArray(response.data)) {
791
throw new Error(getConfigFileDirectoryGivenMessage(configFile));
792
}
793
else {
794
throw new Error(getConfigFileFormatInvalidMessage(configFile));
795
}
796
return yaml.load(Buffer.from(fileContents, "base64").toString("binary"));
797
}
798
/**
799
* Get the file path where the parsed config will be stored.
800
*/
801
function getPathToParsedConfigFile(tempDir) {
802
return path.join(tempDir, "config");
804
exports.getPathToParsedConfigFile = getPathToParsedConfigFile;
805
/**
806
* Store the given config to the path returned from getPathToParsedConfigFile.
807
*/
808
async function saveConfig(config, logger) {
809
const configString = JSON.stringify(config);
810
const configFile = getPathToParsedConfigFile(config.tempDir);
811
fs.mkdirSync(path.dirname(configFile), { recursive: true });
812
fs.writeFileSync(configFile, configString, "utf8");
813
logger.debug("Saved config:");
814
logger.debug(configString);
816
/**
817
* Get the config that has been saved to the given temp dir.
818
* If the config could not be found then returns undefined.
819
*/
820
async function getConfig(tempDir, logger) {
821
const configFile = getPathToParsedConfigFile(tempDir);
822
if (!fs.existsSync(configFile)) {
823
return undefined;
825
const configString = fs.readFileSync(configFile, "utf8");
826
logger.debug("Loaded config:");
827
logger.debug(configString);
828
return JSON.parse(configString);
830
exports.getConfig = getConfig;
831
//# sourceMappingURL=config-utils.js.map