diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index d6467871a..ca84204a3 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,7 +1,7 @@ ### Merge / deployment checklist -- Run test builds as necessary. Can be on this repository or elsewhere as needed in order to test the change - please include links to tests in otehr repos! - - [ ] CodeQL using init/finish actions +- Run test builds as necessary. Can be on this repository or elsewhere as needed in order to test the change - please include links to tests in other repos! + - [ ] CodeQL using init/analyze actions - [ ] 3rd party tool using upload action - [ ] Confirm this change is backwards compatible with existing workflows. -- [ ] Confirm the [readme](https://github.com/github/codeql-action/blob/master/README.md) has been updated if necessary. \ No newline at end of file +- [ ] Confirm the [readme](https://github.com/github/codeql-action/blob/master/README.md) has been updated if necessary. diff --git a/README.md b/README.md index a296ea3d4..f61fdee5c 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,6 @@ This action runs GitHub's industry-leading static analysis engine, CodeQL, against a repository's source code to find security vulnerabilities. It then automatically uploads the results to GitHub so they can be displayed in the repository's security tab. CodeQL runs an extensible set of [queries](https://github.com/semmle/ql), which have been developed by the community and the [GitHub Security Lab](https://securitylab.github.com/) to find common vulnerabilities in your code. -[Sign up for the Advanced Security beta](https://github.com/features/security/advanced-security/signup) - ## Usage To get code scanning results from CodeQL analysis on your repo you can use the following workflow as a template: @@ -82,6 +80,8 @@ The CodeQL action should be run on `push` events, and on a `schedule`. `Push` ev You may optionally specify additional queries for CodeQL to execute by using a config file. The queries must belong to a [QL pack](https://help.semmle.com/codeql/codeql-cli/reference/qlpack-overview.html) and can be in your repository or any public repository. You can choose a single .ql file, a folder containing multiple .ql files, a .qls [query suite](https://help.semmle.com/codeql/codeql-cli/procedures/query-suites.html) file, or any combination of the above. To use queries from other repositories use the same syntax as when [using an action](https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idstepsuses). +You can disable the default queries using `disable-default-queries: true`. + You can choose to ignore some files or folders from the analysis, or include additional files/folders for analysis. This *only* works for Javascript and Python analysis. Identifying potential files for extraction: @@ -102,6 +102,8 @@ A config file looks like this: ```yaml name: "My CodeQL config" +disable-default-queries: true + queries: - name: In-repo queries (Runs the queries located in the my-queries folder of the repo) uses: ./my-queries diff --git a/lib/autobuild.js b/lib/autobuild.js index bce7f3a34..6776b80ff 100644 --- a/lib/autobuild.js +++ b/lib/autobuild.js @@ -22,12 +22,16 @@ async function run() { // We want pick the dominant language in the repo from the ones we're able to build // The languages are sorted in order specified by user or by lines of code if we got // them from the GitHub API, so try to build the first language on the list. - const language = (_a = process.env[sharedEnv.CODEQL_ACTION_TRACED_LANGUAGES]) === null || _a === void 0 ? void 0 : _a.split(',')[0]; + const autobuildLanguages = ((_a = process.env[sharedEnv.CODEQL_ACTION_TRACED_LANGUAGES]) === null || _a === void 0 ? void 0 : _a.split(',')) || []; + const language = autobuildLanguages[0]; if (!language) { core.info("None of the languages in this project require extra build steps"); return; } core.debug(`Detected dominant traced language: ${language}`); + if (autobuildLanguages.length > 1) { + core.warning(`We will only automatically build ${language} code. If you wish to scan ${autobuildLanguages.slice(1).join(' and ')}, you must replace this block with custom build steps.`); + } core.startGroup(`Attempting to automatically build ${language} code`); // TODO: share config accross actions better via env variables const codeqlCmd = util.getRequiredEnvParam(sharedEnv.CODEQL_ACTION_CMD); @@ -44,13 +48,13 @@ async function run() { core.endGroup(); } catch (error) { - core.setFailed(error.message); + core.setFailed("We were unable to automatically build your code. Please replace the call to the autobuild action with your custom build steps. " + error.message); await util.reportActionFailed('autobuild', error.message, error.stack); return; } await util.reportActionSucceeded('autobuild'); } run().catch(e => { - core.setFailed("autobuild action failed: " + e); + core.setFailed("autobuild action failed. " + e); console.log(e); }); diff --git a/lib/config-utils.js b/lib/config-utils.js index 20c2e5945..360db685b 100644 --- a/lib/config-utils.js +++ b/lib/config-utils.js @@ -23,6 +23,7 @@ exports.ExternalQuery = ExternalQuery; class Config { constructor() { this.name = ""; + this.disableDefaultQueries = false; this.additionalQueries = []; this.externalQueries = []; this.pathsIgnore = []; @@ -75,6 +76,9 @@ function initConfig() { 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 => { diff --git a/lib/finalize-db.js b/lib/finalize-db.js index 8679a7d0f..caaa4ce83 100644 --- a/lib/finalize-db.js +++ b/lib/finalize-db.js @@ -73,12 +73,12 @@ async function resolveQueryLanguages(codeqlCmd, config) { const noDeclaredLanguage = resolveQueriesOutputObject.noDeclaredLanguage; const noDeclaredLanguageQueries = Object.keys(noDeclaredLanguage); if (noDeclaredLanguageQueries.length !== 0) { - core.warning('Some queries do not declare a language:\n' + noDeclaredLanguageQueries.join('\n')); + throw new Error('Some queries do not declare a language, their qlpack.yml file is missing or is invalid'); } const multipleDeclaredLanguages = resolveQueriesOutputObject.multipleDeclaredLanguages; const multipleDeclaredLanguagesQueries = Object.keys(multipleDeclaredLanguages); if (multipleDeclaredLanguagesQueries.length !== 0) { - core.warning('Some queries declare multiple languages:\n' + multipleDeclaredLanguagesQueries.join('\n')); + throw new Error('Some queries declare multiple languages, their qlpack.yml file is missing or is invalid'); } } return res; @@ -88,7 +88,11 @@ async function runQueries(codeqlCmd, databaseFolder, sarifFolder, config) { const queriesPerLanguage = await resolveQueryLanguages(codeqlCmd, config); for (let database of fs.readdirSync(databaseFolder)) { core.startGroup('Analyzing ' + database); - const additionalQueries = queriesPerLanguage[database] || []; + const queries = []; + if (!config.disableDefaultQueries) { + queries.push(database + '-code-scanning.qls'); + } + queries.push(...(queriesPerLanguage[database] || [])); const sarifFile = path.join(sarifFolder, database + '.sarif'); await exec.exec(codeqlCmd, [ 'database', @@ -97,8 +101,7 @@ async function runQueries(codeqlCmd, databaseFolder, sarifFolder, config) { '--format=sarif-latest', '--output=' + sarifFile, '--no-sarif-add-snippets', - database + '-code-scanning.qls', - ...additionalQueries, + ...queries ]); core.debug('SARIF results for database ' + database + ' created at "' + sarifFile + '"'); core.endGroup(); @@ -122,7 +125,10 @@ async function run() { core.info('Analyzing database'); await runQueries(codeqlCmd, databaseFolder, sarifFolder, config); if ('true' === core.getInput('upload')) { - await upload_lib.upload(sarifFolder); + if (!await upload_lib.upload(sarifFolder)) { + await util.reportActionFailed('failed', 'upload'); + return; + } } } catch (error) { diff --git a/lib/upload-lib.js b/lib/upload-lib.js index 7896d419e..6c8100d58 100644 --- a/lib/upload-lib.js +++ b/lib/upload-lib.js @@ -54,23 +54,77 @@ function combineSarifFiles(sarifFiles) { return JSON.stringify(combinedSarif); } exports.combineSarifFiles = combineSarifFiles; +// Upload the given payload. +// If the request fails then this will retry a small number of times. +async function uploadPayload(payload) { + core.info('Uploading results'); + const githubToken = core.getInput('token'); + const ph = new auth.BearerCredentialHandler(githubToken); + const client = new http.HttpClient('Code Scanning : Upload SARIF', [ph]); + const url = 'https://api.github.com/repos/' + process.env['GITHUB_REPOSITORY'] + '/code-scanning/analysis'; + // Make up to 4 attempts to upload, and sleep for these + // number of seconds between each attempt. + // We don't want to backoff too much to avoid wasting action + // minutes, but just waiting a little bit could maybe help. + const backoffPeriods = [1, 5, 15]; + for (let attempt = 0; attempt <= backoffPeriods.length; attempt++) { + const res = await client.put(url, payload); + core.debug('response status: ' + res.message.statusCode); + const statusCode = res.message.statusCode; + if (statusCode === 202) { + core.info("Successfully uploaded results"); + return true; + } + const requestID = res.message.headers["x-github-request-id"]; + // On any other status code that's not 5xx mark the upload as failed + if (!statusCode || statusCode < 500 || statusCode >= 600) { + core.setFailed('Upload failed (' + requestID + '): (' + statusCode + ') ' + await res.readBody()); + return false; + } + // On a 5xx status code we may retry the request + if (attempt < backoffPeriods.length) { + // Log the failure as a warning but don't mark the action as failed yet + core.warning('Upload attempt (' + (attempt + 1) + ' of ' + (backoffPeriods.length + 1) + + ') failed (' + requestID + '). Retrying in ' + backoffPeriods[attempt] + + ' seconds: (' + statusCode + ') ' + await res.readBody()); + // Sleep for the backoff period + await new Promise(r => setTimeout(r, backoffPeriods[attempt] * 1000)); + continue; + } + else { + // If the upload fails with 5xx then we assume it is a temporary problem + // and not an error that the user has caused or can fix. + // We avoid marking the job as failed to avoid breaking CI workflows. + core.error('Upload failed (' + requestID + '): (' + statusCode + ') ' + await res.readBody()); + return false; + } + } + return false; +} // Uploads a single sarif file or a directory of sarif files // depending on what the path happens to refer to. +// Returns true iff the upload occurred and succeeded async function upload(input) { if (fs.lstatSync(input).isDirectory()) { const sarifFiles = fs.readdirSync(input) .filter(f => f.endsWith(".sarif")) .map(f => path.resolve(input, f)); - await uploadFiles(sarifFiles); + if (sarifFiles.length === 0) { + core.setFailed("No SARIF files found to upload in \"" + input + "\"."); + return false; + } + return await uploadFiles(sarifFiles); } else { - await uploadFiles([input]); + return await uploadFiles([input]); } } exports.upload = upload; // Uploads the given set of sarif files. +// Returns true iff the upload occurred and succeeded async function uploadFiles(sarifFiles) { core.startGroup("Uploading results"); + let succeeded = false; try { // Check if an upload has happened before. If so then abort. // This is intended to catch when the finish and upload-sarif actions @@ -78,14 +132,14 @@ async function uploadFiles(sarifFiles) { const sentinelFile = await getSentinelFilePath(); if (fs.existsSync(sentinelFile)) { core.info("Aborting as an upload has already happened from this job"); - return; + return false; } const commitOid = util.getRequiredEnvParam('GITHUB_SHA'); const workflowRunIDStr = util.getRequiredEnvParam('GITHUB_RUN_ID'); const ref = util.getRequiredEnvParam('GITHUB_REF'); // it's in the form "refs/heads/master" const analysisName = util.getRequiredEnvParam('GITHUB_WORKFLOW'); const startedAt = process.env[sharedEnv.CODEQL_ACTION_STARTED_AT]; - core.debug("Uploading sarif files: " + JSON.stringify(sarifFiles)); + core.info("Uploading sarif files: " + JSON.stringify(sarifFiles)); let sarifPayload = combineSarifFiles(sarifFiles); sarifPayload = fingerprints.addFingerprints(sarifPayload); const zipped_sarif = zlib_1.default.gzipSync(sarifPayload).toString('base64'); @@ -94,12 +148,13 @@ async function uploadFiles(sarifFiles) { const workflowRunID = parseInt(workflowRunIDStr, 10); if (Number.isNaN(workflowRunID)) { core.setFailed('GITHUB_RUN_ID must define a non NaN workflow run ID'); - return; + return false; } let matrix = core.getInput('matrix'); if (matrix === "null" || matrix === "") { matrix = undefined; } + const toolNames = util.getToolNames(sarifPayload); const payload = JSON.stringify({ "commit_oid": commitOid, "ref": ref, @@ -108,28 +163,11 @@ async function uploadFiles(sarifFiles) { "workflow_run_id": workflowRunID, "checkout_uri": checkoutURI, "environment": matrix, - "started_at": startedAt + "started_at": startedAt, + "tool_names": toolNames, }); - core.info('Uploading results'); - const githubToken = core.getInput('token'); - const ph = new auth.BearerCredentialHandler(githubToken); - const client = new http.HttpClient('Code Scanning : Upload SARIF', [ph]); - const url = 'https://api.github.com/repos/' + process.env['GITHUB_REPOSITORY'] + '/code-scanning/analysis'; - const res = await client.put(url, payload); - const requestID = res.message.headers["x-github-request-id"]; - core.debug('response status: ' + res.message.statusCode); - if (res.message.statusCode === 500) { - // If the upload fails with 500 then we assume it is a temporary problem - // with turbo-scan and not an error that the user has caused or can fix. - // We avoid marking the job as failed to avoid breaking CI workflows. - core.error('Upload failed (' + requestID + '): ' + await res.readBody()); - } - else if (res.message.statusCode !== 202) { - core.setFailed('Upload failed (' + requestID + '): ' + await res.readBody()); - } - else { - core.info("Successfully uploaded results"); - } + // Make the upload + succeeded = await uploadPayload(payload); // Mark that we have made an upload fs.writeFileSync(sentinelFile, ''); } @@ -137,4 +175,5 @@ async function uploadFiles(sarifFiles) { core.setFailed(error.message); } core.endGroup(); + return succeeded; } diff --git a/lib/upload-sarif.js b/lib/upload-sarif.js index 5bd8e593d..b22d2ac62 100644 --- a/lib/upload-sarif.js +++ b/lib/upload-sarif.js @@ -15,16 +15,20 @@ async function run() { return; } try { - await upload_lib.upload(core.getInput('sarif_file')); + if (await upload_lib.upload(core.getInput('sarif_file'))) { + await util.reportActionSucceeded('upload-sarif'); + } + else { + await util.reportActionFailed('upload-sarif', 'upload'); + } } catch (error) { core.setFailed(error.message); await util.reportActionFailed('upload-sarif', error.message, error.stack); return; } - await util.reportActionSucceeded('upload-sarif'); } run().catch(e => { - core.setFailed("upload-sarif action failed: " + e); + core.setFailed("codeql/upload-sarif action failed: " + e); console.log(e); }); diff --git a/lib/util.js b/lib/util.js index 0612c1268..d12a91044 100644 --- a/lib/util.js +++ b/lib/util.js @@ -262,3 +262,21 @@ async function reportActionSucceeded(action) { await sendStatusReport(await createStatusReport(action, 'success')); } exports.reportActionSucceeded = reportActionSucceeded; +/** + * Get the array of all the tool names contained in the given sarif contents. + * + * Returns an array of unique string tool names. + */ +function getToolNames(sarifContents) { + const sarif = JSON.parse(sarifContents); + const toolNames = {}; + for (const run of sarif.runs || []) { + const tool = run.tool || {}; + const driver = tool.driver || {}; + if (typeof driver.name === "string" && driver.name.length > 0) { + toolNames[driver.name] = true; + } + } + return Object.keys(toolNames); +} +exports.getToolNames = getToolNames; diff --git a/package-lock.json b/package-lock.json index 63573714f..f3d9d22af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,9 +15,9 @@ "integrity": "sha512-nvFkxwiicvpzNiCBF4wFBDfnBvi7xp/as7LE1hBxBxKG2L29+gkIPBiLKMVORL+Hg3JNf07AKRfl0V5djoypjQ==" }, "@actions/http-client": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.4.tgz", - "integrity": "sha512-6EzXhqapKKtYr21ZnFQVBYwfrYPKPCivuSkUN/66/BDakkH2EPjUZH8tZ3MgHdI+gQIdcsY0ybbxw9ZEOmJB6g==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.8.tgz", + "integrity": "sha512-G4JjJ6f9Hb3Zvejj+ewLLKLf99ZC+9v+yCxoYf9vSyH+WkzPLB2LuUtRMGNkooMqdugGBFStIKXOuvH1W+EctA==", "requires": { "tunnel": "0.0.6" }, diff --git a/package.json b/package.json index 922361cd6..00e36b072 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "dependencies": { "@actions/core": "^1.0.0", "@actions/exec": "^1.0.1", - "@actions/http-client": "^1.0.4", + "@actions/http-client": "^1.0.8", "@actions/io": "^1.0.1", "@actions/tool-cache": "^1.1.2", "@octokit/rest": "^17.1.0", diff --git a/src/autobuild.ts b/src/autobuild.ts index bd7b37f25..4dedb3343 100644 --- a/src/autobuild.ts +++ b/src/autobuild.ts @@ -15,7 +15,8 @@ async function run() { // We want pick the dominant language in the repo from the ones we're able to build // The languages are sorted in order specified by user or by lines of code if we got // them from the GitHub API, so try to build the first language on the list. - const language = process.env[sharedEnv.CODEQL_ACTION_TRACED_LANGUAGES]?.split(',')[0]; + const autobuildLanguages = process.env[sharedEnv.CODEQL_ACTION_TRACED_LANGUAGES]?.split(',') || []; + const language = autobuildLanguages[0]; if (!language) { core.info("None of the languages in this project require extra build steps"); @@ -24,6 +25,10 @@ async function run() { core.debug(`Detected dominant traced language: ${language}`); + if (autobuildLanguages.length > 1) { + core.warning(`We will only automatically build ${language} code. If you wish to scan ${autobuildLanguages.slice(1).join(' and ')}, you must replace this block with custom build steps.`); + } + core.startGroup(`Attempting to automatically build ${language} code`); // TODO: share config accross actions better via env variables const codeqlCmd = util.getRequiredEnvParam(sharedEnv.CODEQL_ACTION_CMD); @@ -44,7 +49,7 @@ async function run() { core.endGroup(); } catch (error) { - core.setFailed(error.message); + core.setFailed("We were unable to automatically build your code. Please replace the call to the autobuild action with your custom build steps. " + error.message); await util.reportActionFailed('autobuild', error.message, error.stack); return; } @@ -53,6 +58,6 @@ async function run() { } run().catch(e => { - core.setFailed("autobuild action failed: " + e); + core.setFailed("autobuild action failed. " + e); console.log(e); }); diff --git a/src/config-utils.ts b/src/config-utils.ts index 407fb5ac0..2c31495cd 100644 --- a/src/config-utils.ts +++ b/src/config-utils.ts @@ -17,6 +17,7 @@ export class ExternalQuery { export class Config { public name = ""; + public disableDefaultQueries = false; public additionalQueries: string[] = []; public externalQueries: ExternalQuery[] = []; public pathsIgnore: string[] = []; @@ -81,6 +82,10 @@ function initConfig(): Config { 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 => { diff --git a/src/finalize-db.ts b/src/finalize-db.ts index a03e68a1b..f58ab6c1c 100644 --- a/src/finalize-db.ts +++ b/src/finalize-db.ts @@ -82,13 +82,13 @@ async function resolveQueryLanguages(codeqlCmd: string, config: configUtils.Conf const noDeclaredLanguage = resolveQueriesOutputObject.noDeclaredLanguage; const noDeclaredLanguageQueries = Object.keys(noDeclaredLanguage); if (noDeclaredLanguageQueries.length !== 0) { - core.warning('Some queries do not declare a language:\n' + noDeclaredLanguageQueries.join('\n')); + throw new Error('Some queries do not declare a language, their qlpack.yml file is missing or is invalid'); } const multipleDeclaredLanguages = resolveQueriesOutputObject.multipleDeclaredLanguages; const multipleDeclaredLanguagesQueries = Object.keys(multipleDeclaredLanguages); if (multipleDeclaredLanguagesQueries.length !== 0) { - core.warning('Some queries declare multiple languages:\n' + multipleDeclaredLanguagesQueries.join('\n')); + throw new Error('Some queries declare multiple languages, their qlpack.yml file is missing or is invalid'); } } @@ -102,7 +102,12 @@ async function runQueries(codeqlCmd: string, databaseFolder: string, sarifFolder for (let database of fs.readdirSync(databaseFolder)) { core.startGroup('Analyzing ' + database); - const additionalQueries = queriesPerLanguage[database] || []; + const queries: string[] = []; + if (!config.disableDefaultQueries) { + queries.push(database + '-code-scanning.qls'); + } + queries.push(...(queriesPerLanguage[database] || [])); + const sarifFile = path.join(sarifFolder, database + '.sarif'); await exec.exec(codeqlCmd, [ @@ -112,8 +117,7 @@ async function runQueries(codeqlCmd: string, databaseFolder: string, sarifFolder '--format=sarif-latest', '--output=' + sarifFile, '--no-sarif-add-snippets', - database + '-code-scanning.qls', - ...additionalQueries, + ...queries ]); core.debug('SARIF results for database ' + database + ' created at "' + sarifFile + '"'); @@ -146,7 +150,10 @@ async function run() { await runQueries(codeqlCmd, databaseFolder, sarifFolder, config); if ('true' === core.getInput('upload')) { - await upload_lib.upload(sarifFolder); + if (!await upload_lib.upload(sarifFolder)) { + await util.reportActionFailed('failed', 'upload'); + return; + } } } catch (error) { diff --git a/src/testdata/tool-names.sarif b/src/testdata/tool-names.sarif new file mode 100644 index 000000000..ee6cd8cd7 --- /dev/null +++ b/src/testdata/tool-names.sarif @@ -0,0 +1,41 @@ +{ + "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json", + "version": "2.1.0", + "runs": [ + { + "tool": { + "driver": { + "name": "CodeQL command-line toolchain" + } + } + }, + { + "tool": { + "driver": { + "name": "CodeQL command-line toolchain" + } + } + }, + { + "tool": { + "driver": { + "name": "ESLint" + } + } + }, + { + "tool": { + "driver": { + "name": "" + } + } + }, + { + "tool": { + "driver": { + "name": null + } + } + } + ] +} diff --git a/src/upload-lib.ts b/src/upload-lib.ts index 1d35b0b2a..7eb44a635 100644 --- a/src/upload-lib.ts +++ b/src/upload-lib.ts @@ -47,22 +47,86 @@ export function combineSarifFiles(sarifFiles: string[]): string { return JSON.stringify(combinedSarif); } +// Upload the given payload. +// If the request fails then this will retry a small number of times. +async function uploadPayload(payload): Promise { + core.info('Uploading results'); + + const githubToken = core.getInput('token'); + const ph: auth.BearerCredentialHandler = new auth.BearerCredentialHandler(githubToken); + const client = new http.HttpClient('Code Scanning : Upload SARIF', [ph]); + const url = 'https://api.github.com/repos/' + process.env['GITHUB_REPOSITORY'] + '/code-scanning/analysis'; + + // Make up to 4 attempts to upload, and sleep for these + // number of seconds between each attempt. + // We don't want to backoff too much to avoid wasting action + // minutes, but just waiting a little bit could maybe help. + const backoffPeriods = [1, 5, 15]; + + for (let attempt = 0; attempt <= backoffPeriods.length; attempt++) { + + const res: http.HttpClientResponse = await client.put(url, payload); + core.debug('response status: ' + res.message.statusCode); + + const statusCode = res.message.statusCode; + if (statusCode === 202) { + core.info("Successfully uploaded results"); + return true; + } + + const requestID = res.message.headers["x-github-request-id"]; + + // On any other status code that's not 5xx mark the upload as failed + if (!statusCode || statusCode < 500 || statusCode >= 600) { + core.setFailed('Upload failed (' + requestID + '): (' + statusCode + ') ' + await res.readBody()); + return false; + } + + // On a 5xx status code we may retry the request + if (attempt < backoffPeriods.length) { + // Log the failure as a warning but don't mark the action as failed yet + core.warning('Upload attempt (' + (attempt + 1) + ' of ' + (backoffPeriods.length + 1) + + ') failed (' + requestID + '). Retrying in ' + backoffPeriods[attempt] + + ' seconds: (' + statusCode + ') ' + await res.readBody()); + // Sleep for the backoff period + await new Promise(r => setTimeout(r, backoffPeriods[attempt] * 1000)); + continue; + + } else { + // If the upload fails with 5xx then we assume it is a temporary problem + // and not an error that the user has caused or can fix. + // We avoid marking the job as failed to avoid breaking CI workflows. + core.error('Upload failed (' + requestID + '): (' + statusCode + ') ' + await res.readBody()); + return false; + } + } + + return false; +} + // Uploads a single sarif file or a directory of sarif files // depending on what the path happens to refer to. -export async function upload(input: string) { +// Returns true iff the upload occurred and succeeded +export async function upload(input: string): Promise { if (fs.lstatSync(input).isDirectory()) { const sarifFiles = fs.readdirSync(input) .filter(f => f.endsWith(".sarif")) .map(f => path.resolve(input, f)); - await uploadFiles(sarifFiles); + if (sarifFiles.length === 0) { + core.setFailed("No SARIF files found to upload in \"" + input + "\"."); + return false; + } + return await uploadFiles(sarifFiles); } else { - await uploadFiles([input]); + return await uploadFiles([input]); } } // Uploads the given set of sarif files. -async function uploadFiles(sarifFiles: string[]) { +// Returns true iff the upload occurred and succeeded +async function uploadFiles(sarifFiles: string[]): Promise { core.startGroup("Uploading results"); + let succeeded = false; try { // Check if an upload has happened before. If so then abort. // This is intended to catch when the finish and upload-sarif actions @@ -70,7 +134,7 @@ async function uploadFiles(sarifFiles: string[]) { const sentinelFile = await getSentinelFilePath(); if (fs.existsSync(sentinelFile)) { core.info("Aborting as an upload has already happened from this job"); - return; + return false; } const commitOid = util.getRequiredEnvParam('GITHUB_SHA'); @@ -79,7 +143,7 @@ async function uploadFiles(sarifFiles: string[]) { const analysisName = util.getRequiredEnvParam('GITHUB_WORKFLOW'); const startedAt = process.env[sharedEnv.CODEQL_ACTION_STARTED_AT]; - core.debug("Uploading sarif files: " + JSON.stringify(sarifFiles)); + core.info("Uploading sarif files: " + JSON.stringify(sarifFiles)); let sarifPayload = combineSarifFiles(sarifFiles); sarifPayload = fingerprints.addFingerprints(sarifPayload); @@ -90,7 +154,7 @@ async function uploadFiles(sarifFiles: string[]) { if (Number.isNaN(workflowRunID)) { core.setFailed('GITHUB_RUN_ID must define a non NaN workflow run ID'); - return; + return false; } let matrix: string | undefined = core.getInput('matrix'); @@ -98,6 +162,8 @@ async function uploadFiles(sarifFiles: string[]) { matrix = undefined; } + const toolNames = util.getToolNames(sarifPayload); + const payload = JSON.stringify({ "commit_oid": commitOid, "ref": ref, @@ -106,28 +172,12 @@ async function uploadFiles(sarifFiles: string[]) { "workflow_run_id": workflowRunID, "checkout_uri": checkoutURI, "environment": matrix, - "started_at": startedAt + "started_at": startedAt, + "tool_names": toolNames, }); - core.info('Uploading results'); - const githubToken = core.getInput('token'); - const ph: auth.BearerCredentialHandler = new auth.BearerCredentialHandler(githubToken); - const client = new http.HttpClient('Code Scanning : Upload SARIF', [ph]); - const url = 'https://api.github.com/repos/' + process.env['GITHUB_REPOSITORY'] + '/code-scanning/analysis'; - const res: http.HttpClientResponse = await client.put(url, payload); - const requestID = res.message.headers["x-github-request-id"]; - - core.debug('response status: ' + res.message.statusCode); - if (res.message.statusCode === 500) { - // If the upload fails with 500 then we assume it is a temporary problem - // with turbo-scan and not an error that the user has caused or can fix. - // We avoid marking the job as failed to avoid breaking CI workflows. - core.error('Upload failed (' + requestID + '): ' + await res.readBody()); - } else if (res.message.statusCode !== 202) { - core.setFailed('Upload failed (' + requestID + '): ' + await res.readBody()); - } else { - core.info("Successfully uploaded results"); - } + // Make the upload + succeeded = await uploadPayload(payload); // Mark that we have made an upload fs.writeFileSync(sentinelFile, ''); @@ -136,4 +186,6 @@ async function uploadFiles(sarifFiles: string[]) { core.setFailed(error.message); } core.endGroup(); + + return succeeded; } diff --git a/src/upload-sarif.ts b/src/upload-sarif.ts index b0aedce5a..418769c27 100644 --- a/src/upload-sarif.ts +++ b/src/upload-sarif.ts @@ -9,17 +9,19 @@ async function run() { } try { - await upload_lib.upload(core.getInput('sarif_file')); + if (await upload_lib.upload(core.getInput('sarif_file'))) { + await util.reportActionSucceeded('upload-sarif'); + } else { + await util.reportActionFailed('upload-sarif', 'upload'); + } } catch (error) { core.setFailed(error.message); await util.reportActionFailed('upload-sarif', error.message, error.stack); return; } - - await util.reportActionSucceeded('upload-sarif'); } run().catch(e => { - core.setFailed("upload-sarif action failed: " + e); + core.setFailed("codeql/upload-sarif action failed: " + e); console.log(e); }); diff --git a/src/util.test.ts b/src/util.test.ts new file mode 100644 index 000000000..3dfd2f72d --- /dev/null +++ b/src/util.test.ts @@ -0,0 +1,9 @@ +import * as fs from 'fs'; + +import * as util from './util'; + +test('getToolNames', () => { + const input = fs.readFileSync(__dirname + '/testdata/tool-names.sarif', 'utf8') + const toolNames = util.getToolNames(input); + expect(toolNames).toStrictEqual(["CodeQL command-line toolchain", "ESLint"]) +}) diff --git a/src/util.ts b/src/util.ts index 7bb3ec0a5..cfdd2419c 100644 --- a/src/util.ts +++ b/src/util.ts @@ -293,3 +293,23 @@ export async function reportActionFailed(action: string, cause?: string, excepti export async function reportActionSucceeded(action: string) { await sendStatusReport(await createStatusReport(action, 'success')); } + +/** + * Get the array of all the tool names contained in the given sarif contents. + * + * Returns an array of unique string tool names. + */ +export function getToolNames(sarifContents: string): string[] { + const sarif = JSON.parse(sarifContents); + const toolNames = {}; + + for (const run of sarif.runs || []) { + const tool = run.tool || {}; + const driver = tool.driver || {}; + if (typeof driver.name === "string" && driver.name.length > 0) { + toolNames[driver.name] = true; + } + } + + return Object.keys(toolNames); +}