Add capability to filter queries

This change adds a `query-filters` property to the codeql-config file.

This property is an array of `exclude`/`include` entries for a query
suite. These filters are appended to the generated query suite files
and used to filter queries after they are selected.

A related change is that now, all pack references are run in a single
query suite, which has the query filters appended to them.
This commit is contained in:
Andrew Eisenberg 2022-06-13 17:59:03 -07:00
parent bcb7fad5b3
commit 40b280032c
18 changed files with 637 additions and 101 deletions

View file

@ -152,7 +152,7 @@ function dbIsFinalized(
try {
const dbInfo = yaml.load(
fs.readFileSync(path.resolve(dbPath, "codeql-database.yml"), "utf8")
);
) as { inProgress?: boolean };
return !("inProgress" in dbInfo);
} catch (e) {
logger.warning(
@ -224,6 +224,9 @@ export async function runQueries(
for (const language of config.languages) {
const queries = config.queries[language];
const queryFilters = validateQueryFilters(
config.originalUserInput["query-filters"]
);
const packsWithVersion = config.packs[language] || [];
const hasBuiltinQueries = queries?.builtin.length > 0;
@ -261,7 +264,7 @@ export async function runQueries(
await runQueryGroup(
language,
"builtin",
createQuerySuiteContents(queries["builtin"]),
createQuerySuiteContents(queries["builtin"], queryFilters),
undefined
)
);
@ -276,7 +279,10 @@ export async function runQueries(
await runQueryGroup(
language,
`custom-${i}`,
createQuerySuiteContents(queries["custom"][i].queries),
createQuerySuiteContents(
queries["custom"][i].queries,
queryFilters
),
queries["custom"][i].searchPath
)
);
@ -285,12 +291,7 @@ export async function runQueries(
}
if (packsWithVersion.length > 0) {
querySuitePaths.push(
...(await runQueryPacks(
language,
"packs",
packsWithVersion,
undefined
))
await runQueryPacks(language, "packs", packsWithVersion, queryFilters)
);
ranCustom = true;
}
@ -392,32 +393,59 @@ export async function runQueries(
language: Language,
type: string,
packs: string[],
searchPath: string | undefined
): Promise<string[]> {
queryFilters: configUtils.QueryFilter[]
): Promise<string> {
const databasePath = util.getCodeQLDatabasePath(config, language);
// Run the queries individually instead of all at once to avoid command
// line length restrictions, particularly on windows.
for (const pack of packs) {
logger.debug(`Running query pack for ${language}-${type}: ${pack}`);
const codeql = await getCodeQL(config.codeQLCmd);
await codeql.databaseRunQueries(
databasePath,
searchPath,
pack,
memoryFlag,
threadsFlag
);
logger.debug(`BQRS results produced for ${language} (queries: ${type})"`);
}
return packs;
// combine the list of packs into a query suite in order to run them all simultaneously.
const querySuite = packs
.map(convertPackToQuerySuiteEntry)
.concat(queryFilters as any[]);
const querySuitePath = `${databasePath}-queries-${type}.qls`;
fs.writeFileSync(querySuitePath, yaml.dump(querySuite));
logger.debug(`BQRS results produced for ${language} (queries: ${type})"`);
const codeql = await getCodeQL(config.codeQLCmd);
await codeql.databaseRunQueries(
databasePath,
undefined,
querySuitePath,
memoryFlag,
threadsFlag
);
return querySuitePath;
}
}
function createQuerySuiteContents(queries: string[]) {
return queries.map((q: string) => `- query: ${q}`).join("\n");
export function convertPackToQuerySuiteEntry(packStr: string) {
const pack = configUtils.parsePacksSpecification(packStr);
return {
qlpack: !pack.path ? pack.name : undefined,
from: pack.path ? pack.name : undefined,
version: pack.version,
query: pack.path?.endsWith(".ql") ? pack.path : undefined,
queries:
!pack.path?.endsWith(".ql") && !pack.path?.endsWith(".qls")
? pack.path
: undefined,
apply: pack.path?.endsWith(".qls") ? pack.path : undefined,
};
}
export function createQuerySuiteContents(
queries: string[],
queryFilters: configUtils.QueryFilter[]
) {
return yaml.dump(
queries.map((q: string) => ({ query: q })).concat(queryFilters as any)
);
}
export async function runFinalize(
@ -505,3 +533,33 @@ function printLinesOfCodeSummary(
);
}
}
// exported for testing
export function validateQueryFilters(queryFilters?: configUtils.QueryFilter[]) {
if (!queryFilters) {
return [];
}
const errors: string[] = [];
for (const qf of queryFilters) {
const keys = Object.keys(qf);
if (keys.length !== 1) {
errors.push(
`Query filter must have exactly one key: ${JSON.stringify(qf)}`
);
}
if (!["exclude", "include"].includes(keys[0])) {
errors.push(
`Only "include" or "exclude" filters are allowed:\n${JSON.stringify(
qf
)}`
);
}
}
if (errors.length) {
throw new Error(`Invalid query filter.\n${errors.join("\n")}`);
}
return queryFilters;
}