Merge pull request #45 from github/suite_syntax

introduce new syntax for built-in query suites
This commit is contained in:
Robert 2020-06-03 11:14:49 +01:00 committed by GitHub
commit a0d60d5d9e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 239 additions and 58 deletions

View file

@ -2,5 +2,12 @@ name: "CodeQL config"
queries: queries:
- name: Run custom queries - name: Run custom queries
uses: ./queries uses: ./queries
# Run all extra query suites, both because we want to
# and because it'll act as extra testing. This is why
# we include both even though one is a superset of the
# other, because we're testing the parsing logic and
# that the suites exist in the codeql bundle.
- uses: security-extended
- uses: security-and-quality
paths-ignore: paths-ignore:
- tests - tests

View file

@ -23,7 +23,9 @@ jobs:
TEST_MODE: true TEST_MODE: true
- run: | - run: |
cd "$CODEQL_ACTION_DATABASE_DIR" cd "$CODEQL_ACTION_DATABASE_DIR"
if [ "$(ls | wc -l)" != 6 ] || \ # List all directories as there will be precisely one directory per database
# but there may be other files in this directory such as query suites.
if [ "$(ls -d */ | wc -l)" != 6 ] || \
[[ ! -d cpp ]] || \ [[ ! -d cpp ]] || \
[[ ! -d csharp ]] || \ [[ ! -d csharp ]] || \
[[ ! -d go ]] || \ [[ ! -d go ]] || \

View file

@ -5,7 +5,7 @@ inputs:
tools: tools:
description: URL of CodeQL tools description: URL of CodeQL tools
required: false required: false
default: https://github.com/github/codeql-action/releases/download/codeql-bundle-20200427/codeql-bundle.tar.gz default: https://github.com/github/codeql-action/releases/download/codeql-bundle-20200601/codeql-bundle.tar.gz
languages: languages:
description: The languages to be analysed description: The languages to be analysed
required: false required: false

43
lib/config-utils.js generated
View file

@ -21,12 +21,15 @@ class ExternalQuery {
} }
} }
exports.ExternalQuery = ExternalQuery; 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 { class Config {
constructor() { constructor() {
this.name = ""; this.name = "";
this.disableDefaultQueries = false; this.disableDefaultQueries = false;
this.additionalQueries = []; this.additionalQueries = [];
this.externalQueries = []; this.externalQueries = [];
this.additionalSuites = [];
this.pathsIgnore = []; this.pathsIgnore = [];
this.paths = []; this.paths = [];
} }
@ -39,9 +42,33 @@ class Config {
} }
// Check for the local path case before we start trying to parse the repository name // Check for the local path case before we start trying to parse the repository name
if (queryUses.startsWith("./")) { if (queryUses.startsWith("./")) {
this.additionalQueries.push(queryUses.slice(2)); 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; 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(getQueryUsesIncorrect(queryUses));
}
}
let tok = queryUses.split('@'); let tok = queryUses.split('@');
if (tok.length !== 2) { if (tok.length !== 2) {
throw new Error(getQueryUsesIncorrect(queryUses)); throw new Error(getQueryUsesIncorrect(queryUses));
@ -74,9 +101,21 @@ function getQueryUsesBlank() {
} }
exports.getQueryUsesBlank = getQueryUsesBlank; exports.getQueryUsesBlank = getQueryUsesBlank;
function getQueryUsesIncorrect(queryUses) { function getQueryUsesIncorrect(queryUses) {
return '"uses" value for queries must be a path, or owner/repo@ref \n Found: ' + queryUses; 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;
} }
exports.getQueryUsesIncorrect = getQueryUsesIncorrect; exports.getQueryUsesIncorrect = getQueryUsesIncorrect;
function getLocalPathOutsideOfRepository(localPath) {
return 'Unable to use queries from local path "' + localPath +
'" as it is outside of the repository';
}
exports.getLocalPathOutsideOfRepository = getLocalPathOutsideOfRepository;
function getLocalPathDoesNotExist(localPath) {
return 'Unable to use queries from local path "' + localPath +
'" as the path does not exist in the repository';
}
exports.getLocalPathDoesNotExist = getLocalPathDoesNotExist;
function getConfigFileOutsideWorkspaceErrorMessage(configFile) { function getConfigFileOutsideWorkspaceErrorMessage(configFile) {
return 'The configuration file "' + configFile + '" is outside of the workspace'; return 'The configuration file "' + configFile + '" is outside of the workspace';
} }

File diff suppressed because one or more lines are too long

View file

@ -87,6 +87,7 @@ ava_1.default("load non-empty input", async (t) => {
name: my config name: my config
disable-default-queries: true disable-default-queries: true
queries: queries:
- uses: ./
- uses: ./foo - uses: ./foo
- uses: foo/bar@dev - uses: foo/bar@dev
paths-ignore: paths-ignore:
@ -98,12 +99,14 @@ ava_1.default("load non-empty input", async (t) => {
const expectedConfig = new configUtils.Config(); const expectedConfig = new configUtils.Config();
expectedConfig.name = 'my config'; expectedConfig.name = 'my config';
expectedConfig.disableDefaultQueries = true; expectedConfig.disableDefaultQueries = true;
expectedConfig.additionalQueries.push('foo'); expectedConfig.additionalQueries.push(tmpDir);
expectedConfig.additionalQueries.push(path.join(tmpDir, 'foo'));
expectedConfig.externalQueries = [new configUtils.ExternalQuery('foo/bar', 'dev')]; expectedConfig.externalQueries = [new configUtils.ExternalQuery('foo/bar', 'dev')];
expectedConfig.pathsIgnore = ['a', 'b']; expectedConfig.pathsIgnore = ['a', 'b'];
expectedConfig.paths = ['c/d']; expectedConfig.paths = ['c/d'];
fs.writeFileSync(path.join(tmpDir, 'input'), inputFileContents, 'utf8'); fs.writeFileSync(path.join(tmpDir, 'input'), inputFileContents, 'utf8');
setInput('config-file', 'input'); setInput('config-file', 'input');
fs.mkdirSync(path.join(tmpDir, 'foo'));
const actualConfig = await configUtils.loadConfig(); const actualConfig = await configUtils.loadConfig();
// Should exactly equal the object we constructed earlier // Should exactly equal the object we constructed earlier
t.deepEqual(actualConfig, expectedConfig); t.deepEqual(actualConfig, expectedConfig);
@ -195,7 +198,9 @@ const testInputs = {
"foo/bar": configUtils.getQueryUsesIncorrect("foo/bar"), "foo/bar": configUtils.getQueryUsesIncorrect("foo/bar"),
"foo/bar@v1@v2": configUtils.getQueryUsesIncorrect("foo/bar@v1@v2"), "foo/bar@v1@v2": configUtils.getQueryUsesIncorrect("foo/bar@v1@v2"),
"foo@master": configUtils.getQueryUsesIncorrect("foo@master"), "foo@master": configUtils.getQueryUsesIncorrect("foo@master"),
"https://github.com/foo/bar@master": configUtils.getQueryUsesIncorrect("https://github.com/foo/bar@master") "https://github.com/foo/bar@master": configUtils.getQueryUsesIncorrect("https://github.com/foo/bar@master"),
"./foo": configUtils.getLocalPathDoesNotExist("foo"),
"./..": configUtils.getLocalPathOutsideOfRepository(".."),
}; };
for (const [input, result] of Object.entries(testInputs)) { for (const [input, result] of Object.entries(testInputs)) {
ava_1.default("load invalid input - queries uses \"" + input + "\"", async (t) => { ava_1.default("load invalid input - queries uses \"" + input + "\"", async (t) => {

File diff suppressed because one or more lines are too long

71
lib/finalize-db.js generated
View file

@ -67,26 +67,50 @@ async function finalizeDatabaseCreation(codeqlCmd, databaseFolder) {
core.endGroup(); core.endGroup();
} }
} }
async function runResolveQueries(codeqlCmd, queries) {
let output = '';
const options = {
listeners: {
stdout: (data) => {
output += data.toString();
}
}
};
await exec.exec(codeqlCmd, [
'resolve',
'queries',
...queries,
'--format=bylanguage'
], options);
return JSON.parse(output);
}
async function resolveQueryLanguages(codeqlCmd, config) { async function resolveQueryLanguages(codeqlCmd, config) {
let res = new Map(); let res = new Map();
if (config.additionalQueries.length !== 0) { if (!config.disableDefaultQueries || config.additionalSuites.length !== 0) {
let resolveQueriesOutput = ''; const suites = [];
const options = { for (const language of await util.getLanguages()) {
listeners: { if (!config.disableDefaultQueries) {
stdout: (data) => { suites.push(language + '-code-scanning.qls');
resolveQueriesOutput += data.toString();
}
} }
}; for (const additionalSuite of config.additionalSuites) {
await exec.exec(codeqlCmd, [ suites.push(language + '-' + additionalSuite + '.qls');
'resolve', }
'queries', }
...config.additionalQueries, const resolveQueriesOutputObject = await runResolveQueries(codeqlCmd, suites);
'--format=bylanguage'
], options);
const resolveQueriesOutputObject = JSON.parse(resolveQueriesOutput);
for (const [language, queries] of Object.entries(resolveQueriesOutputObject.byLanguage)) { for (const [language, queries] of Object.entries(resolveQueriesOutputObject.byLanguage)) {
res[language] = Object.keys(queries); if (res[language] === undefined) {
res[language] = [];
}
res[language].push(...Object.keys(queries));
}
}
if (config.additionalQueries.length !== 0) {
const resolveQueriesOutputObject = await runResolveQueries(codeqlCmd, config.additionalQueries);
for (const [language, queries] of Object.entries(resolveQueriesOutputObject.byLanguage)) {
if (res[language] === undefined) {
res[language] = [];
}
res[language].push(...Object.keys(queries));
} }
const noDeclaredLanguage = resolveQueriesOutputObject.noDeclaredLanguage; const noDeclaredLanguage = resolveQueriesOutputObject.noDeclaredLanguage;
const noDeclaredLanguageQueries = Object.keys(noDeclaredLanguage); const noDeclaredLanguageQueries = Object.keys(noDeclaredLanguage);
@ -106,11 +130,16 @@ async function runQueries(codeqlCmd, databaseFolder, sarifFolder, config) {
const queriesPerLanguage = await resolveQueryLanguages(codeqlCmd, config); const queriesPerLanguage = await resolveQueryLanguages(codeqlCmd, config);
for (let database of fs.readdirSync(databaseFolder)) { for (let database of fs.readdirSync(databaseFolder)) {
core.startGroup('Analyzing ' + database); core.startGroup('Analyzing ' + database);
const queries = []; const queries = queriesPerLanguage[database] || [];
if (!config.disableDefaultQueries) { if (queries.length === 0) {
queries.push(database + '-code-scanning.qls'); throw new Error('Unable to analyse ' + database + ' as no queries were selected for this language');
} }
queries.push(...(queriesPerLanguage[database] || [])); // Pass the queries to codeql using a file instead of using the command
// line to avoid command line length restrictions, particularly on windows.
const querySuite = path.join(databaseFolder, database + '-queries.qls');
const querySuiteContents = queries.map(q => '- query: ' + q).join('\n');
fs.writeFileSync(querySuite, querySuiteContents);
core.debug('Query suite file for ' + database + '...\n' + querySuiteContents);
const sarifFile = path.join(sarifFolder, database + '.sarif'); const sarifFile = path.join(sarifFolder, database + '.sarif');
await exec.exec(codeqlCmd, [ await exec.exec(codeqlCmd, [
'database', 'database',
@ -120,7 +149,7 @@ async function runQueries(codeqlCmd, databaseFolder, sarifFolder, config) {
'--format=sarif-latest', '--format=sarif-latest',
'--output=' + sarifFile, '--output=' + sarifFile,
'--no-sarif-add-snippets', '--no-sarif-add-snippets',
...queries querySuite
]); ]);
core.debug('SARIF results for database ' + database + ' created at "' + sarifFile + '"'); core.debug('SARIF results for database ' + database + ' created at "' + sarifFile + '"');
core.endGroup(); core.endGroup();

File diff suppressed because one or more lines are too long

View file

@ -91,6 +91,7 @@ test("load non-empty input", async t => {
name: my config name: my config
disable-default-queries: true disable-default-queries: true
queries: queries:
- uses: ./
- uses: ./foo - uses: ./foo
- uses: foo/bar@dev - uses: foo/bar@dev
paths-ignore: paths-ignore:
@ -103,7 +104,8 @@ test("load non-empty input", async t => {
const expectedConfig = new configUtils.Config(); const expectedConfig = new configUtils.Config();
expectedConfig.name = 'my config'; expectedConfig.name = 'my config';
expectedConfig.disableDefaultQueries = true; expectedConfig.disableDefaultQueries = true;
expectedConfig.additionalQueries.push('foo'); expectedConfig.additionalQueries.push(tmpDir);
expectedConfig.additionalQueries.push(path.join(tmpDir, 'foo'));
expectedConfig.externalQueries = [new configUtils.ExternalQuery('foo/bar', 'dev')]; expectedConfig.externalQueries = [new configUtils.ExternalQuery('foo/bar', 'dev')];
expectedConfig.pathsIgnore = ['a', 'b']; expectedConfig.pathsIgnore = ['a', 'b'];
expectedConfig.paths = ['c/d']; expectedConfig.paths = ['c/d'];
@ -111,6 +113,8 @@ test("load non-empty input", async t => {
fs.writeFileSync(path.join(tmpDir, 'input'), inputFileContents, 'utf8'); fs.writeFileSync(path.join(tmpDir, 'input'), inputFileContents, 'utf8');
setInput('config-file', 'input'); setInput('config-file', 'input');
fs.mkdirSync(path.join(tmpDir, 'foo'));
const actualConfig = await configUtils.loadConfig(); const actualConfig = await configUtils.loadConfig();
// Should exactly equal the object we constructed earlier // Should exactly equal the object we constructed earlier
@ -222,7 +226,9 @@ const testInputs = {
"foo/bar": configUtils.getQueryUsesIncorrect("foo/bar"), "foo/bar": configUtils.getQueryUsesIncorrect("foo/bar"),
"foo/bar@v1@v2": configUtils.getQueryUsesIncorrect("foo/bar@v1@v2"), "foo/bar@v1@v2": configUtils.getQueryUsesIncorrect("foo/bar@v1@v2"),
"foo@master": configUtils.getQueryUsesIncorrect("foo@master"), "foo@master": configUtils.getQueryUsesIncorrect("foo@master"),
"https://github.com/foo/bar@master": configUtils.getQueryUsesIncorrect("https://github.com/foo/bar@master") "https://github.com/foo/bar@master": configUtils.getQueryUsesIncorrect("https://github.com/foo/bar@master"),
"./foo": configUtils.getLocalPathDoesNotExist("foo"),
"./..": configUtils.getLocalPathOutsideOfRepository(".."),
}; };
for (const [input, result] of Object.entries(testInputs)) { for (const [input, result] of Object.entries(testInputs)) {

View file

@ -17,11 +17,17 @@ export class ExternalQuery {
} }
} }
// The set of acceptable values for built-in suites from the codeql bundle
const builtinSuites = ['security-extended', 'security-and-quality'] as const;
// Derive the union type from the array values
type BuiltInSuite = typeof builtinSuites[number];
export class Config { export class Config {
public name = ""; public name = "";
public disableDefaultQueries = false; public disableDefaultQueries = false;
public additionalQueries: string[] = []; public additionalQueries: string[] = [];
public externalQueries: ExternalQuery[] = []; public externalQueries: ExternalQuery[] = [];
public additionalSuites: BuiltInSuite[] = [];
public pathsIgnore: string[] = []; public pathsIgnore: string[] = [];
public paths: string[] = []; public paths: string[] = [];
@ -35,10 +41,37 @@ export class Config {
// Check for the local path case before we start trying to parse the repository name // Check for the local path case before we start trying to parse the repository name
if (queryUses.startsWith("./")) { if (queryUses.startsWith("./")) {
this.additionalQueries.push(queryUses.slice(2)); 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; 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(getQueryUsesIncorrect(queryUses));
}
}
let tok = queryUses.split('@'); let tok = queryUses.split('@');
if (tok.length !== 2) { if (tok.length !== 2) {
throw new Error(getQueryUsesIncorrect(queryUses)); throw new Error(getQueryUsesIncorrect(queryUses));
@ -74,7 +107,19 @@ export function getQueryUsesBlank(): string {
} }
export function getQueryUsesIncorrect(queryUses: string): string { export function getQueryUsesIncorrect(queryUses: string): string {
return '"uses" value for queries must be a path, or owner/repo@ref \n Found: ' + queryUses; 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 { export function getConfigFileOutsideWorkspaceErrorMessage(configFile: string): string {

View file

@ -69,32 +69,74 @@ async function finalizeDatabaseCreation(codeqlCmd: string, databaseFolder: strin
} }
} }
interface ResolveQueriesOutput {
byLanguage: {
[language: string]: {
[queryPath: string]: {}
}
};
noDeclaredLanguage: {
[queryPath: string]: {}
};
multipleDeclaredLanguages: {
[queryPath: string]: {}
};
}
async function runResolveQueries(codeqlCmd: string, queries: string[]): Promise<ResolveQueriesOutput> {
let output = '';
const options = {
listeners: {
stdout: (data: Buffer) => {
output += data.toString();
}
}
};
await exec.exec(
codeqlCmd, [
'resolve',
'queries',
...queries,
'--format=bylanguage'
],
options);
return JSON.parse(output);
}
async function resolveQueryLanguages(codeqlCmd: string, config: configUtils.Config): Promise<Map<string, string[]>> { async function resolveQueryLanguages(codeqlCmd: string, config: configUtils.Config): Promise<Map<string, string[]>> {
let res = new Map(); let res = new Map();
if (config.additionalQueries.length !== 0) { if (!config.disableDefaultQueries || config.additionalSuites.length !== 0) {
let resolveQueriesOutput = ''; const suites: string[] = [];
const options = { for (const language of await util.getLanguages()) {
listeners: { if (!config.disableDefaultQueries) {
stdout: (data: Buffer) => { suites.push(language + '-code-scanning.qls');
resolveQueriesOutput += data.toString();
}
} }
}; for (const additionalSuite of config.additionalSuites) {
suites.push(language + '-' + additionalSuite + '.qls');
}
}
await exec.exec( const resolveQueriesOutputObject = await runResolveQueries(codeqlCmd, suites);
codeqlCmd, [
'resolve',
'queries',
...config.additionalQueries,
'--format=bylanguage'
],
options);
const resolveQueriesOutputObject = JSON.parse(resolveQueriesOutput);
for (const [language, queries] of Object.entries(resolveQueriesOutputObject.byLanguage)) { for (const [language, queries] of Object.entries(resolveQueriesOutputObject.byLanguage)) {
res[language] = Object.keys(<any>queries); if (res[language] === undefined) {
res[language] = [];
}
res[language].push(...Object.keys(<any>queries));
}
}
if (config.additionalQueries.length !== 0) {
const resolveQueriesOutputObject = await runResolveQueries(codeqlCmd, config.additionalQueries);
for (const [language, queries] of Object.entries(resolveQueriesOutputObject.byLanguage)) {
if (res[language] === undefined) {
res[language] = [];
}
res[language].push(...Object.keys(<any>queries));
} }
const noDeclaredLanguage = resolveQueriesOutputObject.noDeclaredLanguage; const noDeclaredLanguage = resolveQueriesOutputObject.noDeclaredLanguage;
@ -120,11 +162,17 @@ async function runQueries(codeqlCmd: string, databaseFolder: string, sarifFolder
for (let database of fs.readdirSync(databaseFolder)) { for (let database of fs.readdirSync(databaseFolder)) {
core.startGroup('Analyzing ' + database); core.startGroup('Analyzing ' + database);
const queries: string[] = []; const queries = queriesPerLanguage[database] || [];
if (!config.disableDefaultQueries) { if (queries.length === 0) {
queries.push(database + '-code-scanning.qls'); throw new Error('Unable to analyse ' + database + ' as no queries were selected for this language');
} }
queries.push(...(queriesPerLanguage[database] || []));
// Pass the queries to codeql using a file instead of using the command
// line to avoid command line length restrictions, particularly on windows.
const querySuite = path.join(databaseFolder, database + '-queries.qls');
const querySuiteContents = queries.map(q => '- query: ' + q).join('\n');
fs.writeFileSync(querySuite, querySuiteContents);
core.debug('Query suite file for ' + database + '...\n' + querySuiteContents);
const sarifFile = path.join(sarifFolder, database + '.sarif'); const sarifFile = path.join(sarifFolder, database + '.sarif');
@ -136,7 +184,7 @@ async function runQueries(codeqlCmd: string, databaseFolder: string, sarifFolder
'--format=sarif-latest', '--format=sarif-latest',
'--output=' + sarifFile, '--output=' + sarifFile,
'--no-sarif-add-snippets', '--no-sarif-add-snippets',
...queries querySuite
]); ]);
core.debug('SARIF results for database ' + database + ' created at "' + sarifFile + '"'); core.debug('SARIF results for database ' + database + ' created at "' + sarifFile + '"');