UNPKG

@sonar/scan

Version:
415 lines (414 loc) 18.2 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.getHostProperties = getHostProperties; exports.getProperties = getProperties; /* * sonar-scanner-npm * Copyright (C) 2022-2025 SonarSource SA * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ const fs_extra_1 = __importDefault(require("fs-extra")); const path_1 = __importDefault(require("path")); const properties_file_1 = require("properties-file"); const proxy_from_env_1 = require("proxy-from-env"); const slugify_1 = __importDefault(require("slugify")); const constants_1 = require("./constants"); const logging_1 = require("./logging"); const platform_1 = require("./platform"); const types_1 = require("./types"); function getDefaultProperties() { return { [types_1.ScannerProperty.SonarUserHome]: path_1.default.join(process.env.HOME ?? process.env.USERPROFILE ?? '', constants_1.SONAR_DIR_DEFAULT), [types_1.ScannerProperty.SonarWorkingDirectory]: '.scannerwork', [types_1.ScannerProperty.SonarScannerOs]: (0, platform_1.getSupportedOS)(), [types_1.ScannerProperty.SonarScannerArch]: (0, platform_1.getArch)(), }; } /** * Convert the name of a sonar property from its environment variable form * (eg SONAR_SCANNER_FOO_BAR) to its sonar form (eg sonar.scanner.fooBar). */ function envNameToSonarPropertyNameMapper(envName) { // Extract the name and convert to camel case const sonarScannerKey = envName .substring(constants_1.ENV_VAR_PREFIX.length) .toLowerCase() .replace(/_([a-z])/g, g => g[1].toUpperCase()); return `sonar.scanner.${sonarScannerKey}`; } /** * Convert the name of a sonar property from its environment variable form * (eg npm_config_sonar_scanner_) to its sonar form (eg sonar.scanner.fooBar). */ function npmConfigEnvNameToSonarPropertyNameMapper(envName) { // Extract the name and convert to camel case const sonarScannerKey = envName .substring(constants_1.NPM_CONFIG_ENV_VAR_PREFIX.length) .toLowerCase() .replace(/_([a-z])/g, g => g[1].toUpperCase()); return `sonar.scanner.${sonarScannerKey}`; } /** * Build the config. */ function getPackageJsonProperties(projectBaseDir, sonarBaseExclusions) { const pkg = readPackageJson(projectBaseDir); if (!pkg) { return { [types_1.ScannerProperty.SonarExclusions]: sonarBaseExclusions, }; } (0, logging_1.log)(logging_1.LogLevel.INFO, 'Retrieving info from "package.json" file'); const packageJsonParams = { [types_1.ScannerProperty.SonarExclusions]: sonarBaseExclusions, }; populatePackageParams(packageJsonParams, pkg); populateCoverageParams(packageJsonParams, pkg, projectBaseDir, sonarBaseExclusions); populateTestExecutionParams(packageJsonParams, pkg, projectBaseDir); return packageJsonParams; } function readPackageJson(projectBaseDir) { const packageFile = path_1.default.join(projectBaseDir, 'package.json'); try { const packageData = fs_extra_1.default.readFileSync(packageFile).toString(); return JSON.parse(packageData); } catch (error) { (0, logging_1.log)(logging_1.LogLevel.INFO, `Unable to read "package.json" file`); return null; } } function fileExistsInProjectSync(projectBaseDir, file) { return fs_extra_1.default.existsSync(path_1.default.join(projectBaseDir, file)); } function dependenceExists(pkg, pkgName) { return ['devDependencies', 'dependencies', 'peerDependencies'].some(function (prop) { const dependencyGroup = pkg[prop]; return (typeof dependencyGroup === 'object' && dependencyGroup !== null && pkgName in dependencyGroup); }); } function populatePackageParams(params, pkg) { const invalidCharacterRegex = /[?$*+~.()'"!:@/]/g; if (pkg.name) { params['sonar.projectKey'] = (0, slugify_1.default)(pkg.name, { remove: invalidCharacterRegex, }); params['sonar.projectName'] = pkg.name; } if (pkg.version) { params['sonar.projectVersion'] = pkg.version; } if (pkg.description) { params['sonar.projectDescription'] = pkg.description; } if (pkg.homepage) { params['sonar.links.homepage'] = pkg.homepage; } if (pkg.bugs?.url) { params['sonar.links.issue'] = pkg.bugs.url; } if (pkg.repository?.url) { params['sonar.links.scm'] = pkg.repository.url; } } function populateCoverageParams(params, pkg, projectBaseDir, sonarBaseExclusions) { const potentialCoverageDirs = [ // nyc coverage output directory // See: https://github.com/istanbuljs/nyc#configuring-nyc pkg['nyc']?.['report-dir'], // jest coverage output directory // See: http://facebook.github.io/jest/docs/en/configuration.html#coveragedirectory-string pkg['jest']?.['coverageDirectory'], ] .filter(Boolean) .concat( // default coverage output directory 'coverage'); const uniqueCoverageDirs = Array.from(new Set(potentialCoverageDirs)); params[types_1.ScannerProperty.SonarExclusions] = sonarBaseExclusions; for (const lcovReportDir of uniqueCoverageDirs) { const lcovReportPath = lcovReportDir && path_1.default.posix.join(lcovReportDir, 'lcov.info'); if (lcovReportPath && fileExistsInProjectSync(projectBaseDir, lcovReportPath)) { params[types_1.ScannerProperty.SonarExclusions] += (params[types_1.ScannerProperty.SonarExclusions].length > 0 ? ',' : '') + path_1.default.posix.join(lcovReportDir, '**'); // TODO: (SCANNPM-34) use Generic Test Data to remove dependence of SonarJS, it is need transformation lcov to sonar generic coverage format params['sonar.javascript.lcov.reportPaths'] = lcovReportPath; // https://docs.sonarsource.com/sonarqube/latest/analyzing-source-code/test-coverage/javascript-typescript-test-coverage/ } } } function populateTestExecutionParams(params, pkg, projectBaseDir) { if (dependenceExists(pkg, 'mocha-sonarqube-reporter') && fileExistsInProjectSync(projectBaseDir, 'xunit.xml')) { // https://docs.sonarqube.org/display/SONAR/Generic+Test+Data params['sonar.testExecutionReportPaths'] = 'xunit.xml'; // TODO: (SCANNPM-13) use `glob` to lookup xunit format files and transformation to sonar generic report format } } /** * Convert CLI args into scanner properties. */ function getCommandLineProperties(cliArgs) { const properties = {}; if (cliArgs?.debug) { properties[types_1.ScannerProperty.SonarVerbose] = 'true'; } const { define } = cliArgs ?? {}; if (!define || define.length === 0) { return properties; } // Parse CLI args (eg: -Dsonar.token=xxx) for (const arg of define) { const [key, ...value] = arg.split('='); properties[key] = value.join('='); } return properties; } /** * Parse properties stored in sonar project properties file, if it exists. * Return an empty object if the file does not exist. */ function getSonarFileProperties(projectBaseDir) { // Read sonar project properties file in project base dir try { const sonarPropertiesFile = path_1.default.join(projectBaseDir, constants_1.SONAR_PROJECT_FILENAME); const data = fs_extra_1.default.readFileSync(sonarPropertiesFile); return (0, properties_file_1.getProperties)(data); } catch (error) { (0, logging_1.log)(logging_1.LogLevel.DEBUG, `Failed to read ${constants_1.SONAR_PROJECT_FILENAME} file: ${error}`); return {}; } } /** * Get scanner properties from scan option object (JS API). */ function getScanOptionsProperties(scanOptions) { const properties = { ...scanOptions.options, }; if (typeof scanOptions.serverUrl !== 'undefined') { properties[types_1.ScannerProperty.SonarHostUrl] = scanOptions.serverUrl; } if (typeof scanOptions.token !== 'undefined') { properties[types_1.ScannerProperty.SonarToken] = scanOptions.token; } if (typeof scanOptions.verbose !== 'undefined') { properties[types_1.ScannerProperty.SonarVerbose] = scanOptions.verbose ? 'true' : 'false'; } if (typeof scanOptions.version !== 'undefined') { properties[types_1.ScannerProperty.SonarScannerCliVersion] = scanOptions.version; } return properties; } /** * Automatically parse properties from environment variables. */ function getEnvironmentProperties() { const { env } = process; const jsonEnvVariables = ['SONAR_SCANNER_JSON_PARAMS', 'SONARQUBE_SCANNER_PARAMS']; let properties = {}; // Get known environment variables for (const [envName, scannerProperty] of constants_1.ENV_TO_PROPERTY_NAME) { if (envName in env) { const envValue = env[envName]; if (typeof envValue !== 'undefined') { properties[scannerProperty] = envValue; } } } // Get generic environment variables properties = { ...properties, ...Object.fromEntries(Object.entries(env) .filter(([key]) => key.startsWith(constants_1.NPM_CONFIG_ENV_VAR_PREFIX)) .filter(([key]) => !jsonEnvVariables.includes(key)) .map(([key, value]) => [npmConfigEnvNameToSonarPropertyNameMapper(key), value])), ...Object.fromEntries(Object.entries(env) .filter(([key]) => key.startsWith(constants_1.ENV_VAR_PREFIX)) .filter(([key]) => !jsonEnvVariables.includes(key)) .map(([key, value]) => [envNameToSonarPropertyNameMapper(key), value])), }; // Get JSON parameters from env try { const jsonParams = env.SONAR_SCANNER_JSON_PARAMS ?? env.SONARQUBE_SCANNER_PARAMS; if (jsonParams) { properties = { ...JSON.parse(jsonParams), ...properties, }; } if (!env.SONAR_SCANNER_JSON_PARAMS && env.SONARQUBE_SCANNER_PARAMS) { (0, logging_1.log)(logging_1.LogLevel.WARN, 'SONARQUBE_SCANNER_PARAMS is deprecated, please use SONAR_SCANNER_JSON_PARAMS instead'); } } catch (e) { (0, logging_1.log)(logging_1.LogLevel.WARN, `Failed to parse JSON parameters from ENV: ${e}`); } return properties; } /** * Get bootstrapper properties, that can not be overridden. */ function getBootstrapperProperties(startTimestampMs) { return { 'sonar.scanner.app': constants_1.SCANNER_BOOTSTRAPPER_NAME, 'sonar.scanner.appVersion': '4.3.2', 'sonar.scanner.bootstrapStartTime': startTimestampMs.toString(), // These cache statuses are set during the bootstrapping process. // We already set them here to prevent them from being overridden. 'sonar.scanner.wasJreCacheHit': types_1.CacheStatus.Disabled, 'sonar.scanner.wasEngineCacheHit': 'false', }; } /** * Get endpoint properties from scanner properties. */ function getHostProperties(properties) { const sonarHostUrl = properties[types_1.ScannerProperty.SonarHostUrl]?.replace(/\/$/, '')?.trim(); const sonarApiBaseUrl = properties[types_1.ScannerProperty.SonarScannerApiBaseUrl]; const sonarCloudSpecified = isSonarCloud(properties, sonarHostUrl); if (!sonarHostUrl || sonarCloudSpecified) { const region = (properties[types_1.ScannerProperty.SonarRegion] ?? '').toLowerCase(); if (isSonarCloudUS(sonarHostUrl) || region === constants_1.REGION_US) { return { [types_1.ScannerProperty.SonarScannerInternalIsSonarCloud]: 'true', [types_1.ScannerProperty.SonarHostUrl]: properties[types_1.ScannerProperty.SonarScannerSonarCloudUrl] ?? constants_1.SONARCLOUD_URL_US, [types_1.ScannerProperty.SonarScannerApiBaseUrl]: sonarApiBaseUrl ?? constants_1.SONARCLOUD_API_BASE_URL_US, }; } else if (isSonarCloudEU(sonarHostUrl) || region === '') { return { [types_1.ScannerProperty.SonarScannerInternalIsSonarCloud]: 'true', [types_1.ScannerProperty.SonarHostUrl]: properties[types_1.ScannerProperty.SonarScannerSonarCloudUrl] ?? constants_1.SONARCLOUD_URL, [types_1.ScannerProperty.SonarScannerApiBaseUrl]: sonarApiBaseUrl ?? constants_1.SONARCLOUD_API_BASE_URL, }; } else { const regionsPrint = constants_1.REGIONS.map(r => `"${r}"`); throw new Error(`Unsupported region '${region}'. List of supported regions: ${regionsPrint}. Please check the '${types_1.ScannerProperty.SonarRegion}' property or the 'SONAR_REGION' environment variable.`); } } else { return { [types_1.ScannerProperty.SonarScannerInternalIsSonarCloud]: 'false', [types_1.ScannerProperty.SonarHostUrl]: sonarHostUrl, [types_1.ScannerProperty.SonarScannerApiBaseUrl]: sonarApiBaseUrl ?? `${sonarHostUrl}/api/v2`, }; } } function isSonarCloud(properties, sonarHostUrl) { return (properties[types_1.ScannerProperty.SonarScannerSonarCloudUrl] === sonarHostUrl || isSonarCloudEU(sonarHostUrl) || isSonarCloudUS(sonarHostUrl)); } function isSonarCloudEU(sonarHostUrl) { return constants_1.SONARCLOUD_URL_REGEX.exec(sonarHostUrl ?? ''); } function isSonarCloudUS(sonarHostUrl) { return constants_1.SONARCLOUD_US_URL_REGEX.exec(sonarHostUrl ?? ''); } function getHttpProxyEnvProperties(serverUrl) { const proxyUrl = (0, proxy_from_env_1.getProxyForUrl)(serverUrl); // If no proxy is set, return the properties as is if (!proxyUrl) { return {}; } // Parse the proxy URL const url = new URL(proxyUrl); const properties = {}; properties[types_1.ScannerProperty.SonarScannerProxyHost] = url.hostname; if (url.port) { properties[types_1.ScannerProperty.SonarScannerProxyPort] = url.port; } if (url.username) { properties[types_1.ScannerProperty.SonarScannerProxyUser] = url.username; } if (url.password) { properties[types_1.ScannerProperty.SonarScannerProxyPassword] = url.password; } return properties; } function hotfixDeprecatedProperties(properties) { for (const [oldProp, newProp] of constants_1.SCANNER_DEPRECATED_PROPERTIES) { if (typeof properties[oldProp] !== 'undefined') { if (typeof properties[newProp] === 'undefined') { (0, logging_1.log)(logging_1.LogLevel.WARN, `Property "${oldProp}" is deprecated and will be removed in a future version. Please use "${newProp}" instead.`); properties[newProp] = properties[oldProp]; } else { (0, logging_1.log)(logging_1.LogLevel.WARN, `Both properties "${oldProp}" and "${newProp}" are set. "${oldProp}" is deprecated and will be removed in a future version. Value of deprecated property "${oldProp}" will be ignored.`); properties[oldProp] = properties[newProp]; } } } return properties; } function normalizeProperties(properties) { for (const [key, value] of Object.entries(properties)) { if (value === null) { properties[key] = ''; } else if (typeof value === 'undefined') { delete properties[key]; } else { properties[key] = value.toString().trim(); } } return properties; } function getProperties(scanOptions, startTimestampMs, cliArgs) { const envProperties = getEnvironmentProperties(); const scanOptionsProperties = getScanOptionsProperties(scanOptions); const cliProperties = getCommandLineProperties(cliArgs); const userProperties = { ...envProperties, ...scanOptionsProperties, ...cliProperties, }; // Compute default base dir and exclusions respecting order of precedence we use for the final merge const projectBaseDir = userProperties[types_1.ScannerProperty.SonarProjectBaseDir] ?? process.cwd(); const baseSonarExclusions = userProperties[types_1.ScannerProperty.SonarExclusions] ?? constants_1.DEFAULT_SONAR_EXCLUSIONS; // Infer specific properties from project files const inferredProperties = { ...getPackageJsonProperties(projectBaseDir, baseSonarExclusions), ...getSonarFileProperties(projectBaseDir), }; // Generate proxy properties from HTTP[S]_PROXY env variables, if not already set const httpProxyProperties = getHttpProxyEnvProperties(userProperties[types_1.ScannerProperty.SonarHostUrl]); // Merge properties respecting order of precedence let properties = { ...getDefaultProperties(), // fallback to default if nothing was provided for these properties ...inferredProperties, ...httpProxyProperties, ...userProperties, // Highest precedence }; properties = hotfixDeprecatedProperties({ ...properties, // Can't be overridden: ...getHostProperties(properties), // Hotfix host properties with custom SonarCloud URL ...getBootstrapperProperties(startTimestampMs), 'sonar.projectBaseDir': projectBaseDir, }); properties = normalizeProperties(properties); return properties; }