Skip to content

Commit

Permalink
Add self-signed CA support (#71)
Browse files Browse the repository at this point in the history
  • Loading branch information
ilia-kebets-sonarsource authored Nov 3, 2023
1 parent 060b6d9 commit 4f67c10
Show file tree
Hide file tree
Showing 5 changed files with 74 additions and 20 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
17 changes: 17 additions & 0 deletions src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -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;
}
}

/**
Expand Down
9 changes: 2 additions & 7 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/*
Expand All @@ -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);
Expand Down
18 changes: 16 additions & 2 deletions src/sonar-scanner-executable.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: '=',
Expand All @@ -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
*/
Expand Down
49 changes: 38 additions & 11 deletions test/unit/sonar-scanner-executable.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);
Expand All @@ -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');
});
});
});

0 comments on commit 4f67c10

Please sign in to comment.