diff --git a/extensions/ql-vscode/src/remote-queries/export-results.ts b/extensions/ql-vscode/src/remote-queries/export-results.ts index 1cbea1dde..ad6555ea8 100644 --- a/extensions/ql-vscode/src/remote-queries/export-results.ts +++ b/extensions/ql-vscode/src/remote-queries/export-results.ts @@ -74,7 +74,7 @@ async function determineExportFormat( /** * Converts the results of a remote query to markdown and uploads the files as a secret gist. */ -async function exportResultsToGist( +export async function exportResultsToGist( ctx: ExtensionContext, query: RemoteQuery, analysesResults: AnalysisResults[] diff --git a/extensions/ql-vscode/src/vscode-tests/no-workspace/data/remote-queries/query-with-results/analyses-results.json b/extensions/ql-vscode/src/vscode-tests/no-workspace/data/remote-queries/query-with-results/analyses-results.json new file mode 100644 index 000000000..9b94336d7 --- /dev/null +++ b/extensions/ql-vscode/src/vscode-tests/no-workspace/data/remote-queries/query-with-results/analyses-results.json @@ -0,0 +1,462 @@ +[ + { + "nwo": "github/codeql", + "status": "Completed", + "interpretedResults": [ + { + "message": { + "tokens": [ + { + "t": "text", + "text": "This shell command depends on an uncontrolled " + }, + { + "t": "location", + "text": "absolute path", + "location": { + "fileLink": { + "fileLinkPrefix": "https://github.com/github/codeql/blob/48015e5a2e6202131f2d1062cc066dc33ed69a9b", + "filePath": "javascript/ql/src/Security/CWE-078/examples/shell-command-injection-from-environment.js" + }, + "highlightedRegion": { + "startLine": 4, + "startColumn": 35, + "endLine": 4, + "endColumn": 44 + } + } + }, + { "t": "text", "text": "." } + ] + }, + "shortDescription": "This shell command depends on an uncontrolled ,absolute path,.", + "fileLink": { + "fileLinkPrefix": "https://github.com/github/codeql/blob/48015e5a2e6202131f2d1062cc066dc33ed69a9b", + "filePath": "javascript/ql/src/Security/CWE-078/examples/shell-command-injection-from-environment.js" + }, + "severity": "Warning", + "codeSnippet": { + "startLine": 3, + "endLine": 6, + "text": "function cleanupTemp() {\n let cmd = \"rm -rf \" + path.join(__dirname, \"temp\");\n cp.execSync(cmd); // BAD\n}\n" + }, + "highlightedRegion": { + "startLine": 5, + "startColumn": 15, + "endLine": 5, + "endColumn": 18 + }, + "codeFlows": [ + { + "threadFlows": [ + { + "fileLink": { + "fileLinkPrefix": "https://github.com/github/codeql/blob/48015e5a2e6202131f2d1062cc066dc33ed69a9b", + "filePath": "javascript/ql/src/Security/CWE-078/examples/shell-command-injection-from-environment.js" + }, + "codeSnippet": { + "startLine": 2, + "endLine": 6, + "text": " path = require(\"path\");\nfunction cleanupTemp() {\n let cmd = \"rm -rf \" + path.join(__dirname, \"temp\");\n cp.execSync(cmd); // BAD\n}\n" + }, + "highlightedRegion": { + "startLine": 4, + "startColumn": 35, + "endLine": 4, + "endColumn": 44 + } + }, + { + "fileLink": { + "fileLinkPrefix": "https://github.com/github/codeql/blob/48015e5a2e6202131f2d1062cc066dc33ed69a9b", + "filePath": "javascript/ql/src/Security/CWE-078/examples/shell-command-injection-from-environment.js" + }, + "codeSnippet": { + "startLine": 2, + "endLine": 6, + "text": " path = require(\"path\");\nfunction cleanupTemp() {\n let cmd = \"rm -rf \" + path.join(__dirname, \"temp\");\n cp.execSync(cmd); // BAD\n}\n" + }, + "highlightedRegion": { + "startLine": 4, + "startColumn": 25, + "endLine": 4, + "endColumn": 53 + } + }, + { + "fileLink": { + "fileLinkPrefix": "https://github.com/github/codeql/blob/48015e5a2e6202131f2d1062cc066dc33ed69a9b", + "filePath": "javascript/ql/src/Security/CWE-078/examples/shell-command-injection-from-environment.js" + }, + "codeSnippet": { + "startLine": 2, + "endLine": 6, + "text": " path = require(\"path\");\nfunction cleanupTemp() {\n let cmd = \"rm -rf \" + path.join(__dirname, \"temp\");\n cp.execSync(cmd); // BAD\n}\n" + }, + "highlightedRegion": { + "startLine": 4, + "startColumn": 13, + "endLine": 4, + "endColumn": 53 + } + }, + { + "fileLink": { + "fileLinkPrefix": "https://github.com/github/codeql/blob/48015e5a2e6202131f2d1062cc066dc33ed69a9b", + "filePath": "javascript/ql/src/Security/CWE-078/examples/shell-command-injection-from-environment.js" + }, + "codeSnippet": { + "startLine": 2, + "endLine": 6, + "text": " path = require(\"path\");\nfunction cleanupTemp() {\n let cmd = \"rm -rf \" + path.join(__dirname, \"temp\");\n cp.execSync(cmd); // BAD\n}\n" + }, + "highlightedRegion": { + "startLine": 4, + "startColumn": 7, + "endLine": 4, + "endColumn": 53 + } + }, + { + "fileLink": { + "fileLinkPrefix": "https://github.com/github/codeql/blob/48015e5a2e6202131f2d1062cc066dc33ed69a9b", + "filePath": "javascript/ql/src/Security/CWE-078/examples/shell-command-injection-from-environment.js" + }, + "codeSnippet": { + "startLine": 3, + "endLine": 6, + "text": "function cleanupTemp() {\n let cmd = \"rm -rf \" + path.join(__dirname, \"temp\");\n cp.execSync(cmd); // BAD\n}\n" + }, + "highlightedRegion": { + "startLine": 5, + "startColumn": 15, + "endLine": 5, + "endColumn": 18 + } + } + ] + } + ] + }, + { + "message": { + "tokens": [ + { + "t": "text", + "text": "This shell command depends on an uncontrolled " + }, + { + "t": "location", + "text": "absolute path", + "location": { + "fileLink": { + "fileLinkPrefix": "https://github.com/github/codeql/blob/48015e5a2e6202131f2d1062cc066dc33ed69a9b", + "filePath": "javascript/ql/test/query-tests/Security/CWE-078/tst_shell-command-injection-from-environment.js" + }, + "highlightedRegion": { + "startLine": 6, + "startColumn": 36, + "endLine": 6, + "endColumn": 45 + } + } + }, + { "t": "text", "text": "." } + ] + }, + "shortDescription": "This shell command depends on an uncontrolled ,absolute path,.", + "fileLink": { + "fileLinkPrefix": "https://github.com/github/codeql/blob/48015e5a2e6202131f2d1062cc066dc33ed69a9b", + "filePath": "javascript/ql/test/query-tests/Security/CWE-078/tst_shell-command-injection-from-environment.js" + }, + "severity": "Warning", + "codeSnippet": { + "startLine": 4, + "endLine": 8, + "text": "(function() {\n\tcp.execFileSync('rm', ['-rf', path.join(__dirname, \"temp\")]); // GOOD\n\tcp.execSync('rm -rf ' + path.join(__dirname, \"temp\")); // BAD\n\n\texeca.shell('rm -rf ' + path.join(__dirname, \"temp\")); // NOT OK\n" + }, + "highlightedRegion": { + "startLine": 6, + "startColumn": 14, + "endLine": 6, + "endColumn": 54 + }, + "codeFlows": [ + { + "threadFlows": [ + { + "fileLink": { + "fileLinkPrefix": "https://github.com/github/codeql/blob/48015e5a2e6202131f2d1062cc066dc33ed69a9b", + "filePath": "javascript/ql/test/query-tests/Security/CWE-078/tst_shell-command-injection-from-environment.js" + }, + "codeSnippet": { + "startLine": 4, + "endLine": 8, + "text": "(function() {\n\tcp.execFileSync('rm', ['-rf', path.join(__dirname, \"temp\")]); // GOOD\n\tcp.execSync('rm -rf ' + path.join(__dirname, \"temp\")); // BAD\n\n\texeca.shell('rm -rf ' + path.join(__dirname, \"temp\")); // NOT OK\n" + }, + "highlightedRegion": { + "startLine": 6, + "startColumn": 36, + "endLine": 6, + "endColumn": 45 + } + }, + { + "fileLink": { + "fileLinkPrefix": "https://github.com/github/codeql/blob/48015e5a2e6202131f2d1062cc066dc33ed69a9b", + "filePath": "javascript/ql/test/query-tests/Security/CWE-078/tst_shell-command-injection-from-environment.js" + }, + "codeSnippet": { + "startLine": 4, + "endLine": 8, + "text": "(function() {\n\tcp.execFileSync('rm', ['-rf', path.join(__dirname, \"temp\")]); // GOOD\n\tcp.execSync('rm -rf ' + path.join(__dirname, \"temp\")); // BAD\n\n\texeca.shell('rm -rf ' + path.join(__dirname, \"temp\")); // NOT OK\n" + }, + "highlightedRegion": { + "startLine": 6, + "startColumn": 26, + "endLine": 6, + "endColumn": 54 + } + }, + { + "fileLink": { + "fileLinkPrefix": "https://github.com/github/codeql/blob/48015e5a2e6202131f2d1062cc066dc33ed69a9b", + "filePath": "javascript/ql/test/query-tests/Security/CWE-078/tst_shell-command-injection-from-environment.js" + }, + "codeSnippet": { + "startLine": 4, + "endLine": 8, + "text": "(function() {\n\tcp.execFileSync('rm', ['-rf', path.join(__dirname, \"temp\")]); // GOOD\n\tcp.execSync('rm -rf ' + path.join(__dirname, \"temp\")); // BAD\n\n\texeca.shell('rm -rf ' + path.join(__dirname, \"temp\")); // NOT OK\n" + }, + "highlightedRegion": { + "startLine": 6, + "startColumn": 14, + "endLine": 6, + "endColumn": 54 + } + } + ] + } + ] + } + ] + }, + { + "nwo": "test/no-results", + "status": "Completed", + "interpretedResults": [] + }, + { + "nwo": "meteor/meteor", + "status": "Completed", + "interpretedResults": [ + { + "message": { + "tokens": [ + { + "t": "text", + "text": "This shell command depends on an uncontrolled " + }, + { + "t": "location", + "text": "absolute path", + "location": { + "fileLink": { + "fileLinkPrefix": "https://github.com/meteor/meteor/blob/73b538fe201cbfe89dd0c709689023f9b3eab1ec", + "filePath": "npm-packages/meteor-installer/config.js" + }, + "highlightedRegion": { + "startLine": 39, + "startColumn": 20, + "endLine": 39, + "endColumn": 61 + } + } + }, + { "t": "text", "text": "." } + ] + }, + "shortDescription": "This shell command depends on an uncontrolled ,absolute path,.", + "fileLink": { + "fileLinkPrefix": "https://github.com/meteor/meteor/blob/73b538fe201cbfe89dd0c709689023f9b3eab1ec", + "filePath": "npm-packages/meteor-installer/install.js" + }, + "severity": "Warning", + "codeSnippet": { + "startLine": 257, + "endLine": 261, + "text": " if (isWindows()) {\n //set for the current session and beyond\n child_process.execSync(`setx path \"${meteorPath}/;%path%`);\n return;\n }\n" + }, + "highlightedRegion": { + "startLine": 259, + "startColumn": 28, + "endLine": 259, + "endColumn": 62 + }, + "codeFlows": [ + { + "threadFlows": [ + { + "fileLink": { + "fileLinkPrefix": "https://github.com/meteor/meteor/blob/73b538fe201cbfe89dd0c709689023f9b3eab1ec", + "filePath": "npm-packages/meteor-installer/config.js" + }, + "codeSnippet": { + "startLine": 37, + "endLine": 41, + "text": "\nconst meteorLocalFolder = '.meteor';\nconst meteorPath = path.resolve(rootPath, meteorLocalFolder);\n\nmodule.exports = {\n" + }, + "highlightedRegion": { + "startLine": 39, + "startColumn": 20, + "endLine": 39, + "endColumn": 61 + } + }, + { + "fileLink": { + "fileLinkPrefix": "https://github.com/meteor/meteor/blob/73b538fe201cbfe89dd0c709689023f9b3eab1ec", + "filePath": "npm-packages/meteor-installer/config.js" + }, + "codeSnippet": { + "startLine": 37, + "endLine": 41, + "text": "\nconst meteorLocalFolder = '.meteor';\nconst meteorPath = path.resolve(rootPath, meteorLocalFolder);\n\nmodule.exports = {\n" + }, + "highlightedRegion": { + "startLine": 39, + "startColumn": 7, + "endLine": 39, + "endColumn": 61 + } + }, + { + "fileLink": { + "fileLinkPrefix": "https://github.com/meteor/meteor/blob/73b538fe201cbfe89dd0c709689023f9b3eab1ec", + "filePath": "npm-packages/meteor-installer/config.js" + }, + "codeSnippet": { + "startLine": 42, + "endLine": 46, + "text": " METEOR_LATEST_VERSION,\n extractPath: rootPath,\n meteorPath,\n release: process.env.INSTALL_METEOR_VERSION || METEOR_LATEST_VERSION,\n rootPath,\n" + }, + "highlightedRegion": { + "startLine": 44, + "startColumn": 3, + "endLine": 44, + "endColumn": 13 + } + }, + { + "fileLink": { + "fileLinkPrefix": "https://github.com/meteor/meteor/blob/73b538fe201cbfe89dd0c709689023f9b3eab1ec", + "filePath": "npm-packages/meteor-installer/install.js" + }, + "codeSnippet": { + "startLine": 10, + "endLine": 14, + "text": "const os = require('os');\nconst {\n meteorPath,\n release,\n startedPath,\n" + }, + "highlightedRegion": { + "startLine": 12, + "startColumn": 3, + "endLine": 12, + "endColumn": 13 + } + }, + { + "fileLink": { + "fileLinkPrefix": "https://github.com/meteor/meteor/blob/73b538fe201cbfe89dd0c709689023f9b3eab1ec", + "filePath": "npm-packages/meteor-installer/install.js" + }, + "codeSnippet": { + "startLine": 9, + "endLine": 25, + "text": "const tmp = require('tmp');\nconst os = require('os');\nconst {\n meteorPath,\n release,\n startedPath,\n extractPath,\n isWindows,\n rootPath,\n sudoUser,\n isSudo,\n isMac,\n METEOR_LATEST_VERSION,\n shouldSetupExecPath,\n} = require('./config.js');\nconst { uninstall } = require('./uninstall');\nconst {\n" + }, + "highlightedRegion": { + "startLine": 11, + "startColumn": 7, + "endLine": 23, + "endColumn": 27 + } + }, + { + "fileLink": { + "fileLinkPrefix": "https://github.com/meteor/meteor/blob/73b538fe201cbfe89dd0c709689023f9b3eab1ec", + "filePath": "npm-packages/meteor-installer/install.js" + }, + "codeSnippet": { + "startLine": 257, + "endLine": 261, + "text": " if (isWindows()) {\n //set for the current session and beyond\n child_process.execSync(`setx path \"${meteorPath}/;%path%`);\n return;\n }\n" + }, + "highlightedRegion": { + "startLine": 259, + "startColumn": 42, + "endLine": 259, + "endColumn": 52 + } + }, + { + "fileLink": { + "fileLinkPrefix": "https://github.com/meteor/meteor/blob/73b538fe201cbfe89dd0c709689023f9b3eab1ec", + "filePath": "npm-packages/meteor-installer/install.js" + }, + "codeSnippet": { + "startLine": 257, + "endLine": 261, + "text": " if (isWindows()) {\n //set for the current session and beyond\n child_process.execSync(`setx path \"${meteorPath}/;%path%`);\n return;\n }\n" + }, + "highlightedRegion": { + "startLine": 259, + "startColumn": 28, + "endLine": 259, + "endColumn": 62 + } + } + ] + }, + { + "threadFlows": [ + { + "fileLink": { + "fileLinkPrefix": "https://github.com/meteor/meteor/blob/73b538fe201cbfe89dd0c709689023f9b3eab1ec", + "filePath": "npm-packages/meteor-installer/config.js" + }, + "codeSnippet": { + "startLine": 37, + "endLine": 41, + "text": "\nconst meteorLocalFolder = '.meteor';\nconst meteorPath = path.resolve(rootPath, meteorLocalFolder);\n\nmodule.exports = {\n" + }, + "highlightedRegion": { + "startLine": 39, + "startColumn": 20, + "endLine": 39, + "endColumn": 61 + } + }, + { + "fileLink": { + "fileLinkPrefix": "https://github.com/meteor/meteor/blob/73b538fe201cbfe89dd0c709689023f9b3eab1ec", + "filePath": "npm-packages/meteor-installer/install.js" + }, + "codeSnippet": { + "startLine": 257, + "endLine": 261, + "text": " if (isWindows()) {\n //set for the current session and beyond\n child_process.execSync(`setx path \"${meteorPath}/;%path%`);\n return;\n }\n" + }, + "highlightedRegion": { + "startLine": 259, + "startColumn": 28, + "endLine": 259, + "endColumn": 62 + } + } + ] + } + ] + } + ] + } +] diff --git a/extensions/ql-vscode/src/vscode-tests/no-workspace/data/remote-queries/query-with-results/query.json b/extensions/ql-vscode/src/vscode-tests/no-workspace/data/remote-queries/query-with-results/query.json new file mode 100644 index 000000000..93aafac06 --- /dev/null +++ b/extensions/ql-vscode/src/vscode-tests/no-workspace/data/remote-queries/query-with-results/query.json @@ -0,0 +1,10 @@ +{ + "queryName": "Shell command built from environment values", + "queryFilePath": "c:\\git-repo\\vscode-codeql-starter\\ql\\javascript\\ql\\src\\Security\\CWE-078\\ShellCommandInjectionFromEnvironment.ql", + "queryText": "/**\n * @name Shell command built from environment values\n * @description Building a shell command string with values from the enclosing\n * environment may cause subtle bugs or vulnerabilities.\n * @kind path-problem\n * @problem.severity warning\n * @security-severity 6.3\n * @precision high\n * @id js/shell-command-injection-from-environment\n * @tags correctness\n * security\n * external/cwe/cwe-078\n * external/cwe/cwe-088\n */\n\nimport javascript\nimport DataFlow::PathGraph\nimport semmle.javascript.security.dataflow.ShellCommandInjectionFromEnvironmentQuery\n\nfrom\n Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink, DataFlow::Node highlight,\n Source sourceNode\nwhere\n sourceNode = source.getNode() and\n cfg.hasFlowPath(source, sink) and\n if cfg.isSinkWithHighlight(sink.getNode(), _)\n then cfg.isSinkWithHighlight(sink.getNode(), highlight)\n else highlight = sink.getNode()\nselect highlight, source, sink, \"This shell command depends on an uncontrolled $@.\", sourceNode,\n sourceNode.getSourceType()\n", + "language": "javascript", + "controllerRepository": { "owner": "dsp-testing", "name": "qc-controller" }, + "executionStartTime": 1649419081990, + "actionsWorkflowRunId": 2115000864, + "numRepositoriesQueried": 10 +} diff --git a/extensions/ql-vscode/src/vscode-tests/no-workspace/remote-queries/export-results.test.ts b/extensions/ql-vscode/src/vscode-tests/no-workspace/remote-queries/export-results.test.ts new file mode 100644 index 000000000..b64e91ff0 --- /dev/null +++ b/extensions/ql-vscode/src/vscode-tests/no-workspace/remote-queries/export-results.test.ts @@ -0,0 +1,56 @@ +import { expect } from 'chai'; +import * as path from 'path'; +import * as fs from 'fs-extra'; +import * as sinon from 'sinon'; +import * as pq from 'proxyquire'; +import { ExtensionContext } from 'vscode'; +import { createMockExtensionContext } from '../index'; +import { Credentials } from '../../../authentication'; +import { MarkdownFile } from '../../../remote-queries/remote-queries-markdown-generation'; +import * as actionsApiClient from '../../../remote-queries/gh-actions-api-client'; +import { exportResultsToGist } from '../../../remote-queries/export-results'; + +const proxyquire = pq.noPreserveCache(); + +describe('export results', async function() { + describe('exportResultsToGist', async function() { + let sandbox: sinon.SinonSandbox; + let mockCredentials: Credentials; + let mockResponse: sinon.SinonStub>; + let mockCreateGist: sinon.SinonStub; + let ctx: ExtensionContext; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + + mockCredentials = { + getOctokit: () => Promise.resolve({ + request: mockResponse + }) + } as unknown as Credentials; + sandbox.stub(Credentials, 'initialize').resolves(mockCredentials); + + const resultFiles = [] as MarkdownFile[]; + proxyquire('../../../remote-queries/remote-queries-markdown-generation', { + 'generateMarkdown': sinon.stub().returns(resultFiles) + }); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should call the GitHub Actions API with the correct gist title', async function() { + mockCreateGist = sinon.stub(actionsApiClient, 'createGist'); + + ctx = createMockExtensionContext(); + const query = JSON.parse(await fs.readFile(path.join(__dirname, '../data/remote-queries/query-with-results/query.json'), 'utf8')); + const analysesResults = JSON.parse(await fs.readFile(path.join(__dirname, '../data/remote-queries/query-with-results/analyses-results.json'), 'utf8')); + + await exportResultsToGist(ctx, query, analysesResults); + + expect(mockCreateGist.calledOnce).to.be.true; + expect(mockCreateGist.firstCall.args[1]).to.equal('Shell command built from environment values (javascript) 3 results (10 repositories)'); + }); + }); +});