diff --git a/pom.xml b/pom.xml index 205a9ac..a6cb0af 100644 --- a/pom.xml +++ b/pom.xml @@ -1,5 +1,7 @@ - + 4.0.0 fr.cnes.sonar.plugins.scan @@ -18,7 +20,7 @@ 2.11.0 5.11.0-M2 0.8.12 - 17 + 17 https://cnes.fr CNES @@ -48,7 +50,7 @@ Topin2001 - Topin + Topin @@ -173,4 +175,4 @@ - + \ No newline at end of file diff --git a/src/main/java/fr/cnes/sonar/plugins/scan/tasks/AbstractTask.java b/src/main/java/fr/cnes/sonar/plugins/scan/tasks/AbstractTask.java index 20cb575..b469edd 100644 --- a/src/main/java/fr/cnes/sonar/plugins/scan/tasks/AbstractTask.java +++ b/src/main/java/fr/cnes/sonar/plugins/scan/tasks/AbstractTask.java @@ -49,8 +49,6 @@ public abstract class AbstractTask implements RequestHandler { * @throws InterruptedException when a command is not finished */ protected String executeCommand(final String command) throws IOException, InterruptedException { - // log the command to execute - LOGGER.info(command); // prepare a string builder for the output gathering final StringBuilder output = new StringBuilder(); diff --git a/src/main/java/fr/cnes/sonar/plugins/scan/tasks/AnalysisTask.java b/src/main/java/fr/cnes/sonar/plugins/scan/tasks/AnalysisTask.java index 7fa0417..ec8ff15 100644 --- a/src/main/java/fr/cnes/sonar/plugins/scan/tasks/AnalysisTask.java +++ b/src/main/java/fr/cnes/sonar/plugins/scan/tasks/AnalysisTask.java @@ -17,41 +17,48 @@ package fr.cnes.sonar.plugins.scan.tasks; import fr.cnes.sonar.plugins.scan.utils.StringManager; + import org.sonar.api.config.Configuration; import org.sonar.api.server.ws.Request; import org.sonar.api.server.ws.Response; import org.sonar.api.utils.text.JsonWriter; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + import java.io.File; import java.io.FileWriter; import java.io.IOException; +import java.lang.reflect.Type; import java.nio.file.Files; import java.nio.file.Paths; import java.text.SimpleDateFormat; +import java.util.ArrayList; import java.util.Date; +import java.util.List; import java.util.concurrent.*; /** * Execute the scan of a project + * * @author lequal */ public class AnalysisTask extends AbstractTask { private final Configuration config; - public AnalysisTask(Configuration config){ + public AnalysisTask(Configuration config) { this.config = config; } + /** * Logged message when a file can not be deleted */ - private static final String FILE_DELETION_ERROR = - "The following file could not be deleted: %s."; + private static final String FILE_DELETION_ERROR = "The following file could not be deleted: %s."; /** * Logged message when a file can not be set as executable */ - private static final String FILE_PERMISSIONS_ERROR = - "Permissions of the following file could not be changed: %s."; + private static final String FILE_PERMISSIONS_ERROR = "Permissions of the following file could not be changed: %s."; /** * Just a slash */ @@ -69,40 +76,48 @@ public AnalysisTask(Configuration config){ */ private static final String NEW_LINE = "\n"; - - /** * Execute the scan of a project - * @param projectName name of the project to analyze - * @param projectFolder url of the folder containing the project to analyze + * + * @param projectName name of the project to analyze + * @param projectFolder url of the folder containing the project to + * analyze * @param sonarProjectProperties the sonar-project.properties as string + * @param qualityProfiles the quality profiles as string * @return logs - * @throws IOException when a file writing goes wrong + * @throws IOException when a file writing goes wrong * @throws InterruptedException when a command is not finished */ - private String analyze(final String projectName, final String projectFolder, final String sonarProjectProperties) + private String analyze(final String projectName, final String projectFolder, final String sonarProjectProperties, + final String qualityProfiles) throws IOException, InterruptedException { // setting a timer based on user's timeout property configuration - final Integer timeout = Integer.parseInt(config.get(StringManager.string(StringManager.TIMEOUT_PROP_DEF_KEY)).orElse(StringManager.string(StringManager.DEFAULT_STRING))); + final Integer timeout = Integer.parseInt(config.get(StringManager.string(StringManager.TIMEOUT_PROP_DEF_KEY)) + .orElse(StringManager.string(StringManager.DEFAULT_STRING))); final ExecutorService service = Executors.newSingleThreadExecutor(); try { final Runnable task = () -> { try { // path where spp should be written final String sppPath = String.format(StringManager.string(StringManager.CNES_SPP_PATH), - config.get(StringManager.string(StringManager.WORKSPACE_PROP_DEF_KEY)).orElse(StringManager.string(StringManager.DEFAULT_STRING)), projectFolder); + config.get(StringManager.string(StringManager.WORKSPACE_PROP_DEF_KEY)) + .orElse(StringManager.string(StringManager.DEFAULT_STRING)), + projectFolder); // write sonar-project.properties in the project folder writeTextFile(sppPath, sonarProjectProperties); // build the scan command final String analysisCommand = String.format( StringManager.string(StringManager.CNES_COMMAND_SCAN), - config.get(StringManager.string(StringManager.SCANNER_PROP_DEF_KEY)).orElse(StringManager.string(StringManager.DEFAULT_STRING)), - config.get(StringManager.string(StringManager.WORKSPACE_PROP_DEF_KEY)).orElse(StringManager.string(StringManager.DEFAULT_STRING)), + config.get(StringManager.string(StringManager.SCANNER_PROP_DEF_KEY)) + .orElse(StringManager.string(StringManager.DEFAULT_STRING)), + config.get(StringManager.string(StringManager.WORKSPACE_PROP_DEF_KEY)) + .orElse(StringManager.string(StringManager.DEFAULT_STRING)), projectFolder, - config.get(StringManager.string(StringManager.WORKSPACE_PROP_DEF_KEY)).orElse(StringManager.string(StringManager.DEFAULT_STRING)), + config.get(StringManager.string(StringManager.WORKSPACE_PROP_DEF_KEY)) + .orElse(StringManager.string(StringManager.DEFAULT_STRING)), projectFolder); - final File script = createScript(projectFolder, analysisCommand); + final File script = createScript(projectFolder, analysisCommand, qualityProfiles); // string formatted date as string final String date = new SimpleDateFormat( @@ -110,14 +125,18 @@ private String analyze(final String projectName, final String projectFolder, fin // export log file final String logPath = String.format(StringManager.string(StringManager.CNES_LOG_PATH), - config.get(StringManager.string(StringManager.WORKSPACE_PROP_DEF_KEY)).orElse(StringManager.string(StringManager.DEFAULT_STRING)), date, projectName); + config.get(StringManager.string(StringManager.WORKSPACE_PROP_DEF_KEY)) + .orElse(StringManager.string(StringManager.DEFAULT_STRING)), + date, projectName); // scan execution - final String scriptCommand = config.get(StringManager.string(StringManager.WORKSPACE_PROP_DEF_KEY)).orElse(StringManager.string(StringManager.DEFAULT_STRING)) + + final String scriptCommand = config.get(StringManager.string(StringManager.WORKSPACE_PROP_DEF_KEY)) + .orElse(StringManager.string(StringManager.DEFAULT_STRING)) + SLASH + projectFolder + SLASH + CAT_SCAN_SCRIPT; log(executeCommand(scriptCommand)); // log output file - final String path = config.get(StringManager.string(StringManager.WORKSPACE_PROP_DEF_KEY)).orElse(StringManager.string(StringManager.DEFAULT_STRING)) + + final String path = config.get(StringManager.string(StringManager.WORKSPACE_PROP_DEF_KEY)) + .orElse(StringManager.string(StringManager.DEFAULT_STRING)) + SLASH + projectFolder + SLASH + CAT_LOG_FILE; for (final String line : Files.readAllLines(Paths.get(path))) { log(line + NEW_LINE); @@ -137,7 +156,6 @@ private String analyze(final String projectName, final String projectFolder, fin LOGGER.severe(e.getMessage()); } - }; final Future execution = service.submit(task); @@ -153,13 +171,13 @@ private String analyze(final String projectName, final String projectFolder, fin service.shutdown(); } - // return the complete logs return getLogs(); } /** * Export the sonar-project.properties in the corresponding folder + * * @param path Output folder * @param data Data to write * @throws IOException when a file writing goes wrong @@ -170,7 +188,7 @@ private void writeTextFile(final String path, final String data) throws IOExcept // create the writer // true to append; false to overwrite. - try(FileWriter fileWriter = new FileWriter(spp, false)) { + try (FileWriter fileWriter = new FileWriter(spp, false)) { // write the data fileWriter.write(data); } @@ -178,9 +196,10 @@ private void writeTextFile(final String path, final String data) throws IOExcept /** * Use the user's request to launch an scan - * @param request request coming from the user + * + * @param request request coming from the user * @param response response to send to the user - * @throws IOException when communicating with the client + * @throws IOException when communicating with the client * @throws InterruptedException ... */ @Override @@ -196,9 +215,11 @@ public void handle(final Request request, final Response response) StringManager.string(StringManager.ANALYZE_FOLDER_NAME)); final String sonarProjectProperties = request.mandatoryParam( StringManager.string(StringManager.ANALYZE_SPP_NAME)); + final String qualityProfiles = request.mandatoryParam( + StringManager.string(StringManager.ANALYZE_QUALITY_PROFILES_NAME)); // concrete scan - final String result = analyze(projectName, workspace, sonarProjectProperties); + final String result = analyze(projectName, workspace, sonarProjectProperties, qualityProfiles); // write the json response try (JsonWriter jsonWriter = response.newJsonWriter()) { @@ -209,33 +230,83 @@ public void handle(final Request request, final Response response) } } + private String setupExternalTools(String qualityProfile) { + StringBuilder setupExternalTools = new StringBuilder(); + + Gson gson = new Gson(); + Type outerListType = new TypeToken>() { + }.getType(); + List outerList = gson.fromJson(qualityProfile, outerListType); + List> qualityProfiles = new ArrayList<>(); + for (String innerJson : outerList) { + Type innerListType = new TypeToken>() { + }.getType(); + List innerList = gson.fromJson(innerJson, innerListType); + qualityProfiles.add(innerList); + } + + for (List qp : qualityProfiles) { + if (qp.get(0).equals("py")) { + LOGGER.info("Setup pylint"); + // Detect and run correct pylintrc according to RNC + String pylintrc = "/opt/python/pylintrc_RNC2015_D"; + switch (qp.get(1)) { + case "RNC A": + pylintrc = "/opt/python/pylintrc_RNC2015_A_B"; + break; + case "RNC B": + pylintrc = "/opt/python/pylintrc_RNC2015_A_B"; + break; + case "RNC C": + pylintrc = "/opt/python/pylintrc_RNC2015_C"; + break; + default: + break; + } + setupExternalTools.append( + "\npylint --rcfile=" + pylintrc + + " --load-plugins=pylint_sonarjson --output-format=sonarjson --output=pylint-report.json *.py"); + } + if (qp.contains("docker")) { + LOGGER.info("Setup hadolint"); + setupExternalTools.append( + "\nhadolint -f sonarqube --no-fail --config=/opt/hadolint/hadolint_RNC_A_B_C_D.yaml Dockerfile > hadolint-report.json"); + } + } + return setupExternalTools.toString(); + } + /** - * Create a temporary script containing dedicated command executing sonar-scanner - * @param project repository containing the source code + * Create a temporary script containing dedicated command executing + * sonar-scanner + * + * @param project repository containing the source code * @param commandLine command line to execute * @return The created file */ - private File createScript(final String project, final String commandLine) { + private File createScript(final String project, final String commandLine, final String qualityProfiles) { // path to the workspace - final String workspace = config.get(StringManager.string(StringManager.WORKSPACE_PROP_DEF_KEY)).orElse(StringManager.string(StringManager.DEFAULT_STRING))+ - SLASH +project+ SLASH; + final String workspace = config.get(StringManager.string(StringManager.WORKSPACE_PROP_DEF_KEY)) + .orElse(StringManager.string(StringManager.DEFAULT_STRING)) + + SLASH + project + SLASH; // create script in a file located in the project's repository final File scriptOutput = new File(workspace + CAT_SCAN_SCRIPT); + String setupExternalTools = setupExternalTools(qualityProfiles); + // Write all command lines in a single temporary script try ( - FileWriter script = new FileWriter(scriptOutput) - ){ - script.write("#!/bin/bash -e"); - script.write("\ncd "+workspace); - script.write(StringManager.string(StringManager.CNES_LOG_SEPARATOR)+commandLine); - LOGGER.info("commandLine : " + commandLine); + FileWriter script = new FileWriter(scriptOutput)) { + script.write("#!/bin/bash"); + script.write("\ncd " + workspace); + script.write("\n" + setupExternalTools); + script.write(StringManager.string(StringManager.CNES_LOG_SEPARATOR) + commandLine); } catch (IOException e) { LOGGER.severe(e.getMessage()); } // give execution rights on the script - if(!scriptOutput.setExecutable(true)) { + if (!scriptOutput.setExecutable(true)) { LOGGER.severe(String.format(FILE_PERMISSIONS_ERROR, scriptOutput.getName())); } diff --git a/src/main/java/fr/cnes/sonar/plugins/scan/utils/StringManager.java b/src/main/java/fr/cnes/sonar/plugins/scan/utils/StringManager.java index 75d8e01..193c16a 100644 --- a/src/main/java/fr/cnes/sonar/plugins/scan/utils/StringManager.java +++ b/src/main/java/fr/cnes/sonar/plugins/scan/utils/StringManager.java @@ -62,6 +62,11 @@ public final class StringManager { * Property for action 1 (scan) param 6 description */ public static final String ANALYZE_SPP_DESC = "cnes.action.analyze.param.spp.desc"; + /** + * Property for action 1 (scan) param 7 description + */ + public static final String ANALYZE_QUALITY_PROFILES_DESC = + "cnes.action.analyze.param.profiles.desc"; /** * Property for quality profiles separator */ @@ -191,6 +196,10 @@ public final class StringManager { * Define the name of the projects's sonar-project.properties parameter */ public static final String ANALYZE_SPP_NAME = "cnes.action.analyze.param.spp.name"; + /** + * Define the name of the project's quality profiles parameter + */ + public static final String ANALYZE_QUALITY_PROFILES_NAME = "cnes.action.analyze.param.profiles.name"; /** * Define the name of the returned log filed */ diff --git a/src/main/java/fr/cnes/sonar/plugins/scan/ws/CnesWs.java b/src/main/java/fr/cnes/sonar/plugins/scan/ws/CnesWs.java index 218c62b..17ed339 100644 --- a/src/main/java/fr/cnes/sonar/plugins/scan/ws/CnesWs.java +++ b/src/main/java/fr/cnes/sonar/plugins/scan/ws/CnesWs.java @@ -118,6 +118,10 @@ private void analyzeAction(final NewController controller) { newParam = analysis.createParam(StringManager.string(StringManager.ANALYZE_SPP_NAME)); newParam.setDescription(StringManager.string(StringManager.ANALYZE_SPP_DESC)); newParam.setRequired(true); + // quality profiles parameter + newParam = analysis.createParam(StringManager.string(StringManager.ANALYZE_QUALITY_PROFILES_NAME)); + newParam.setDescription(StringManager.string(StringManager.ANALYZE_QUALITY_PROFILES_DESC)); + newParam.setRequired(true); } diff --git a/src/main/resources/static/analysis.js b/src/main/resources/static/analysis.js index 34f21d0..6fdd0d8 100644 --- a/src/main/resources/static/analysis.js +++ b/src/main/resources/static/analysis.js @@ -233,33 +233,31 @@ function registerScan(options, token) { // complete the spp with sources repository spp = spp.concat("\nsonar.sources=" + sources); - //This part is not usefull anymore, since we need to implement the pylint analysis + const externalReportSonar = "\nsonar.externalIssuesReportPaths="; + let toolsPath = "" + // if a python quality profile is set and there are no pylintrc set - // for (const element of qualityprofiles) { - // const qualityprofile = JSON.parse(element); - // if (qualityprofile[0].toLowerCase() == "py" && spp.indexOf("sonar.python.pylint.reportPaths") === -1) { - // // sonar pylint configuration property - // let pylintrcSonar = "\nsonar.python.pylint.reportPaths="; - // // name of the configuration file to use - // let filename = "pylintrc_RNC2015_D"; - // // we append the appropriate one - // // check if there is a rated A or B profile and add the corresponding file - // if (qualityprofile[1] == "RNC A" || qualityprofile[1] == "RNC B") { - // filename = "pylintrc_RNC2015_A_B"; - // spp = spp.concat(pylintrcSonar + pylintrcfolder + filename); - // info("Use of configuration file " + filename + " for Pylint."); - // // check if there is a rated C profile and add the corresponding file - // } else if (qualityprofile[1] == "RNC C") { - // filename = "pylintrc_RNC2015_C"; - // spp = spp.concat(pylintrcSonar + pylintrcfolder + filename); - // info("Use of configuration file " + filename + " for Pylint."); - // // otherwise it is a D configuration to use - // } else { - // spp = spp.concat(pylintrcSonar + pylintrcfolder + filename); - // info("Use of configuration file " + filename + " for Pylint."); - // } - // } - // } + for (const element of qualityprofiles) { + const qualityprofile = JSON.parse(element); + if (qualityprofile[0].toLowerCase() == "py") { + if (toolsPath == "") { + toolsPath += "./pylint-report.json" + } else { + toolsPath += ",./pylint-report.json" + } + } + if (qualityprofile[0].toLowerCase() == "docker") { + if (toolsPath == "") { + toolsPath += "./hadolint-report.json" + } else { + toolsPath += ",./hadolint-report.json" + } + } + + } + if (toolsPath != "") { + spp = spp.concat(externalReportSonar + toolsPath); + } return spp; @@ -290,13 +288,14 @@ function registerScan(options, token) { // log the finally used spp info("Here comes the finally used sonar-project.properties:\n" + spp); info("The analysis is running, please wait."); + //change json to string for qualityProfile + const qualityprofiles = JSON.stringify(qualityprofile); // send post request to the cnes web service window.SonarRequest.postJSON( '/api/cnes/analyze', - { key: key, name: name, folder: folder, sonarProjectProperties: spp } + { key: key, name: name, folder: folder, sonarProjectProperties: spp, qualityProfiles: qualityprofiles } ).then(function (response) { - // on success // log output info("Project analysis response: \n" + response.logs); // wait that sonarqube has finished to import the report to produce the report @@ -432,7 +431,9 @@ function registerScan(options, token) { // Check if there are no queued tasks, indicating readiness to report if (response.queue.length === 0) { // Produce the report - callback(key, author, token); + console.log('SonarQube done importing the report'); + setEnabled(true); + //callback(key, author, token); } else { // Retry after 2 seconds setTimeout(() => waitSonarQube(key, author, token, callback), 2000); @@ -529,26 +530,6 @@ function registerScan(options, token) { }); }; - /** - * Return a well formatted string for the profile argument of the web service - * @param options - */ - let optionsToString = function (options) { - let result = ""; - - // we concatenate all profiles' name in a string - // separated by ';' - for (let i = 0; i < options.length; ++i) { - // add a separator when necessary - if (i > 0) { - result = result + ';'; - } - result = result + options[i].value; - } - - return result.replace(new RegExp('\\+', "g"), '%2B'); - }; - // once the request is done, and the page is still displayed (not closed already) if (isDisplayedAnalysis) { diff --git a/src/main/resources/strings.properties b/src/main/resources/strings.properties index 7d03c6e..a4c3395 100644 --- a/src/main/resources/strings.properties +++ b/src/main/resources/strings.properties @@ -79,6 +79,8 @@ cnes.action.analyze.param.name.name=name cnes.action.analyze.param.folder.name=folder # Define the name of the projects's sonar-project.properties parameter cnes.action.analyze.param.spp.name=sonarProjectProperties +# Define the name of the project's quality profiles parameter +cnes.action.analyze.param.profiles.name=qualityProfiles # Define the name of the returned log filed cnes.action.analyze.response.log=logs # Property for controller description @@ -97,6 +99,8 @@ cnes.action.analyze.param.name.desc=Name of the project to analyze. cnes.action.analyze.param.folder.desc=Name of the project folder. # Property for action 1 (scan) param 6 description cnes.action.analyze.param.spp.desc=The classical sonar-project.properties content. +# Property for action 1 (scan) param 7 description +cnes.action.analyze.param.profiles.desc=Quality profiles to use. # Property for action 2 (reporting) param 1 name