diff --git a/README.md b/README.md index 49159849..2884bcf1 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ scanner( - `serverUrl` _String_ (optional) The URL of the SonarQube server. Defaults to http://localhost:9000 - `login` _String_ (optional) The login used to connect to the SonarQube server up to version 9. Empty by default. - `token` _String_ (optional) The token used to connect to the SonarQube server v10+ or SonarCloud. Empty by default. + - `caPath` _String_ (optional) the path to a CA to pass as `https.request()` [options](https://nodejs.org/api/https.html#https_https_request_options_callback). - `options` _Map_ (optional) Used to pass extra parameters for the analysis. See the [official documentation](http://redirect.sonarsource.com/doc/analysis-parameters.html) for more details. - `callback` _Function_ (optional) Callback (the execution of the analysis is asynchronous). diff --git a/src/config.js b/src/config.js index 0add9304..8dc350dd 100644 --- a/src/config.js +++ b/src/config.js @@ -21,6 +21,7 @@ const sonarScannerParams = require('./sonar-scanner-params'); const { findTargetOS, buildInstallFolderPath, buildExecutablePath } = require('./utils'); const os = require('os'); +const fs = require('fs'); const log = require('fancy-log'); const { HttpsProxyAgent } = require('https-proxy-agent'); @@ -117,9 +118,25 @@ function getExecutableParams(params = {}) { 'Basic ' + Buffer.from(finalUrl.username + ':' + finalUrl.password).toString('base64'), }; } + + if (params.caPath) { + config.httpOptions.ca = extractCa(params.caPath); + } + log(`Executable parameters built:`); log(config); return config; + + function extractCa(caPath) { + if (!fs.existsSync(caPath)) { + throw new Error(`Provided CA certificate path does not exist: ${caPath}`); + } + const ca = fs.readFileSync(caPath, 'utf8'); + if (!ca.startsWith('-----BEGIN CERTIFICATE-----')) { + throw new Error('Invalid CA certificate'); + } + return ca; + } } /** diff --git a/src/index.js b/src/index.js index e188be2e..1e9fb70a 100644 --- a/src/index.js +++ b/src/index.js @@ -21,10 +21,7 @@ const exec = require('child_process').execFileSync; const log = require('fancy-log'); const { getScannerParams, extendWithExecParams } = require('./config'); -const { - getSonarScannerExecutable, - getLocalSonarScannerExecutable, -} = require('./sonar-scanner-executable'); +const { getScannerExecutable } = require('./sonar-scanner-executable'); const version = require('../package.json').version; /* @@ -34,9 +31,7 @@ async function scan(params, cliArgs = [], localScanner = false) { log('Starting analysis...'); // determine the command to run and execute it - const sqScannerCommand = await (localScanner - ? getLocalSonarScannerExecutable - : getSonarScannerExecutable)(); + const sqScannerCommand = await getScannerExecutable(localScanner, params); // prepare the exec options, most notably with the SQ params const scannerParams = getScannerParams(process.cwd(), params); diff --git a/src/sonar-scanner-executable.js b/src/sonar-scanner-executable.js index 0635d156..5e07ce80 100644 --- a/src/sonar-scanner-executable.js +++ b/src/sonar-scanner-executable.js @@ -28,8 +28,7 @@ const logError = log.error; const path = require('path'); const { getExecutableParams } = require('./config'); -module.exports.getSonarScannerExecutable = getSonarScannerExecutable; -module.exports.getLocalSonarScannerExecutable = getLocalSonarScannerExecutable; +module.exports.getScannerExecutable = getScannerExecutable; const bar = new ProgressBar('[:bar] :percent :etas', { complete: '=', @@ -38,6 +37,21 @@ const bar = new ProgressBar('[:bar] :percent :etas', { total: 0, }); +/** + * If localScanner is true, returns the command to use the local scanner executable. + * Otherwise, returns a promise to download the scanner executable and the command to use it. + * + * @param {*} localScanner + * @param {*} params + * @returns + */ +function getScannerExecutable(localScanner = false, params = {}) { + if (localScanner) { + return getLocalSonarScannerExecutable(); + } + return getSonarScannerExecutable(params); +} + /* * Returns the SQ Scanner executable for the current platform */ diff --git a/test/unit/sonar-scanner-executable.test.js b/test/unit/sonar-scanner-executable.test.js index 094d6c1d..f041438e 100644 --- a/test/unit/sonar-scanner-executable.test.js +++ b/test/unit/sonar-scanner-executable.test.js @@ -24,20 +24,17 @@ const fs = require('fs'); const os = require('os'); const mkdirpSync = require('mkdirp').sync; const rimraf = require('rimraf'); -const { - getSonarScannerExecutable, - getLocalSonarScannerExecutable, -} = require('../../src/sonar-scanner-executable'); +const { getScannerExecutable } = require('../../src/sonar-scanner-executable'); const { DEFAULT_SCANNER_VERSION, getExecutableParams } = require('../../src/config'); const { buildInstallFolderPath, buildExecutablePath } = require('../../src/utils'); const { startServer, closeServerPromise } = require('./fixtures/webserver/server'); describe('sqScannerExecutable', function () { - describe('getSonarScannerExecutable()', function () { + describe('Sonar: getScannerExecutable(false)', function () { it('should throw exception when the download of executable fails', async function () { process.env.SONAR_SCANNER_MIRROR = 'http://fake.url/sonar-scanner'; try { - await getSonarScannerExecutable({ + await getScannerExecutable(false, { basePath: os.tmpdir(), }); assert.fail(); @@ -62,7 +59,7 @@ describe('sqScannerExecutable', function () { rimraf.sync(filepath); }); it('should return the path to it', async function () { - const receivedExecutable = await getSonarScannerExecutable({ + const receivedExecutable = await getScannerExecutable(false, { basePath: os.tmpdir(), }); assert.equal(receivedExecutable, filepath); @@ -85,21 +82,51 @@ describe('sqScannerExecutable', function () { rimraf.sync(pathToUnzippedExecutable); }); it('should download the executable, unzip it and return a path to it.', async function () { - const execPath = await getSonarScannerExecutable({ + const execPath = await getScannerExecutable(false, { baseUrl: `http://${server.address().address}:${server.address().port}`, fileName: FILENAME, }); assert.equal(execPath, expectedPlatformExecutablePath); }); }); + + describe('when providing a self-signed CA certificate', function () { + let caPath; + beforeAll(() => { + caPath = path.join(os.tmpdir(), 'ca.pem'); + fs.writeFileSync(caPath, '-----BEGIN CERTIFICATE-----'); + }); + + it('should fail if the provided path is invalid', async function () { + try { + await getScannerExecutable(false, { caPath: 'invalid-path' }); + assert.fail('should have thrown'); + } catch (e) { + assert.equal(e.message, 'Provided CA certificate path does not exist: invalid-path'); + } + }); + it('should proceed with the download if the provided CA certificate is valid', async function () { + process.env.SONAR_SCANNER_MIRROR = 'http://fake.url/sonar-scanner'; + try { + await getScannerExecutable(false, { + caPath: caPath, + basePath: os.tmpdir(), + }); + assert.fail('should have thrown'); + } catch (e) { + assert.equal(e.message, 'getaddrinfo ENOTFOUND fake.url'); + } + }); + }); }); - describe('getLocalSonarScannerExecutable', () => { - it('should fail when the executable is not found', () => { + describe('local: getScannerExecutable(true)', () => { + it('should fail when the executable is not found', async () => { assert.throws( - getLocalSonarScannerExecutable, + getScannerExecutable.bind(null, true), 'Local install of SonarScanner not found in: sonar-scanner', ); + //expect(getScannerExecutable(true)).to.eventually.be.rejectedWith('Local install of SonarScanner not found in: sonar-scanner'); }); }); });