/* * Coverity Sonar Plugin * Copyright (c) 2017 Synopsys, Inc * support@coverity.com * * All rights reserved. This program and the accompanying materials are made * available under the terms of the Eclipse Public License v1.0 which * accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html. */ package org.sonar.plugins.coverity.batch; import com.coverity.ws.v9.*; import org.apache.commons.lang.StringEscapeUtils; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.sonar.api.CoreProperties; import org.sonar.api.batch.fs.FileSystem; import org.sonar.api.batch.fs.InputFile; import org.sonar.api.batch.fs.internal.DefaultTextPointer; import org.sonar.api.batch.fs.internal.DefaultTextRange; import org.sonar.api.batch.rule.ActiveRule; import org.sonar.api.batch.sensor.Sensor; import org.sonar.api.batch.sensor.SensorContext; import org.sonar.api.batch.sensor.SensorDescriptor; import org.sonar.api.batch.sensor.issue.NewIssue; import org.sonar.api.batch.sensor.issue.NewIssueLocation; import org.sonar.api.config.Settings; import org.sonar.api.rule.RuleKey; import org.sonar.plugins.coverity.CoverityPlugin; import org.sonar.plugins.coverity.base.CoverityPluginMetrics; import org.sonar.plugins.coverity.util.CoverityUtil; import org.sonar.plugins.coverity.ws.CIMClient; import org.sonar.plugins.coverity.ws.CIMClientFactory; import java.io.File; import java.io.IOException; import java.util.*; import static org.sonar.plugins.coverity.util.CoverityUtil.createURL; public class CoveritySensor implements Sensor { private static final Logger LOG = LoggerFactory.getLogger(CoveritySensor.class); private final String HIGH = "High"; private final String MEDIUM = "Medium"; private final String LOW = "Low"; private int totalDefects = 0; private int highImpactDefects = 0; private int mediumImpactDefects = 0; private int lowImpactDefects = 0; private String platform; private CIMClientFactory cimClientFactory; public CoveritySensor(CIMClientFactory cimClientFactory) { this.cimClientFactory = cimClientFactory; platform = System.getProperty("os.name"); } @Override public void describe(SensorDescriptor descriptor) { String[] repositories = new String[CoverityPlugin.COVERITY_LANGUAGES.size()]; for(int i = 0; i < CoverityPlugin.COVERITY_LANGUAGES.size(); i++) { repositories[i] = CoverityPlugin.REPOSITORY_KEY + "-" + CoverityPlugin.COVERITY_LANGUAGES.get(i); } descriptor.name(this.toString()) .createIssuesForRuleRepositories(repositories) // Coverity project is the only required value which does not provide a default (other properties validates at runtime) .requireProperties(CoverityPlugin.COVERITY_PROJECT); } @Override public void execute(SensorContext context) { Settings settings = context.settings(); boolean enabled = settings.getBoolean(CoverityPlugin.COVERITY_ENABLE); int totalDefectsCounter = 0; int highImpactDefectsCounter = 0; int mediumImpactDefectsCounter = 0; int lowImpactDefectsCounter = 0; LOG.info(CoverityPlugin.COVERITY_ENABLE + "=" + enabled); if(!enabled) { return; } //make sure to use the right SAAJ library. The one included with some JREs is missing a required file (a // LocalStrings bundle) ClassLoader oldCL = Thread.currentThread().getContextClassLoader(); Thread.currentThread().setContextClassLoader(getClass().getClassLoader()); System.setProperty("javax.xml.soap.MetaFactory", "com.sun.xml.messaging.saaj.soap.SAAJMetaFactoryImpl"); String covProject = settings.getString(CoverityPlugin.COVERITY_PROJECT); String stripPrefix = settings.getString(CoverityPlugin.COVERITY_PREFIX); String covSrcDir = settings.getString(CoverityPlugin.COVERITY_SOURCE_DIRECTORY); /** * Checks whether a project has been specified. */ if(covProject == null || covProject.isEmpty()) { LOG.error("Couldn't find project: " + covProject); Thread.currentThread().setContextClassLoader(oldCL); return; } CIMClient instance = cimClientFactory.create(settings); //find the configured project ProjectDataObj covProjectObj = null; try { covProjectObj = instance.getProject(covProject); if(covProjectObj == null) { LOG.error("Couldn't find project: " + covProject); Thread.currentThread().setContextClassLoader(oldCL); return; } LOG.info("Found project: " + covProject + " (" + covProjectObj.getProjectKey() + ")"); } catch (IOException | CovRemoteServiceException_Exception e) { LOG.error("Error while trying to find project: " + covProject); Thread.currentThread().setContextClassLoader(oldCL); return; } try { LOG.info("Fetching defects for project: " + covProject); List<MergedDefectDataObj> defects = instance.getDefects(covProject); Map<Long, StreamDefectDataObj> streamDefects = instance.getStreamDefectsForMergedDefects(defects); LOG.info("Found " + streamDefects.size() + " defects"); String currentDir = System.getProperty("user.dir"); File currenDirFile = new File(currentDir); LOG.info("Current Directory: " + currentDir); List<File> listOfFiles = new ArrayList<File>(); String sonarSourcesString = null; if(covSrcDir != null && !covSrcDir.isEmpty()){ sonarSourcesString = covSrcDir; } else { sonarSourcesString = settings.getString("sonar.sources"); } if(sonarSourcesString != null && !sonarSourcesString.isEmpty()){ List<String> sonarSources = Arrays.asList(sonarSourcesString.split(",")); for(String dir : sonarSources){ File folder = new File(dir); listOfFiles.addAll(CoverityUtil.listFiles(folder)); } } for(MergedDefectDataObj mddo : defects) { String status = ""; String impact = ""; List<DefectInstanceDataObj> didos = streamDefects.get(mddo.getCid()).getDefectInstances(); if (didos == null || didos.isEmpty()) { LOG.info("The merged defect with CID " + mddo.getCid() + "has no defect instances defined."); continue; } impact = didos.get(0).getImpact().getDisplayName(); List<DefectStateAttributeValueDataObj> listOfAttributes = mddo.getDefectStateAttributeValues(); for(DefectStateAttributeValueDataObj defectAttribute : listOfAttributes){ if(defectAttribute.getAttributeDefinitionId().getName().equals("DefectStatus")){ status = defectAttribute.getAttributeValueId().getName(); } } if ("Dismissed".equals(status) || "Fixed".equals(status) || "Absent Dismissed".equals(status)) { LOG.info("Skipping resolved defect (CID " + mddo.getCid() + ", status '" + status + "')"); continue; } InputFile inputFile; String filePath = mddo.getFilePathname(); if (stripPrefix != null && !stripPrefix.isEmpty() && filePath.startsWith(stripPrefix)){ String strippedFilePath = filePath.substring(stripPrefix.length()); filePath = new File(currenDirFile, strippedFilePath).getAbsolutePath(); LOG.info("Full path after prefix being stripped: " + filePath); } if (platform.startsWith("Windows")) { filePath = filePath.replace("/", "\\"); } final FileSystem fileSystem = context.fileSystem(); inputFile = fileSystem.inputFile(fileSystem.predicates().hasPath(filePath)); if(impact != null){ totalDefectsCounter++; if (impact.equals(HIGH)) { highImpactDefectsCounter++; }else if (impact.equals(MEDIUM)) { mediumImpactDefectsCounter++; }else { lowImpactDefectsCounter++; } } if(inputFile == null) { for(File possibleFile : listOfFiles){ if(possibleFile.getAbsolutePath().endsWith(filePath)){ inputFile = fileSystem.inputFile(fileSystem.predicates().hasPath(possibleFile.getAbsolutePath())); break; } } } if(inputFile == null) { LOG.info("Cannot find the file '" + filePath + "', skipping defect (CID " + mddo.getCid() + ")"); continue; } for(DefectInstanceDataObj dido : didos) { //find the main event, so we can use its line number EventDataObj mainEvent = getMainEvent(dido); String subcategory = dido.getSubcategory(); if (StringUtils.isEmpty(subcategory)) { subcategory = "none"; } ActiveRule ar = findActiveRule(context, dido.getDomain(), dido.getCheckerName(), subcategory, inputFile.language()); LOG.debug("mainEvent=" + mainEvent); LOG.debug("ar=" + ar); if(mainEvent != null && ar != null) { LOG.debug("instance=" + instance); LOG.debug("covProjectObj=" + covProjectObj); LOG.debug("mddo=" + mddo); LOG.debug("dido=" + dido); String message = getIssueMessage(instance, covProjectObj, mddo, dido); final DefaultTextPointer start = new DefaultTextPointer(mainEvent.getLineNumber(), 0); NewIssue issue = context.newIssue(); NewIssueLocation issueLocation = issue .newLocation() .on(inputFile) .at(new DefaultTextRange(start, start)) .message(message); issue.forRule(ar.ruleKey()) .at(issueLocation); LOG.debug("issue=" + issue); issue.save(); } else { LOG.info("Couldn't create issue: " + mddo.getCid()); } } } } catch(IOException | CovRemoteServiceException_Exception e) { LOG.error("Error fetching defects", e); } totalDefects = totalDefectsCounter; highImpactDefects = highImpactDefectsCounter; mediumImpactDefects = mediumImpactDefectsCounter; lowImpactDefects = lowImpactDefectsCounter; Thread.currentThread().setContextClassLoader(oldCL); // Display a clickable Coverity Logo getCoverityLogoMeasures(context, instance, covProjectObj); } protected String getIssueMessage(CIMClient instance, ProjectDataObj covProjectObj, MergedDefectDataObj mddo, DefectInstanceDataObj dido) throws CovRemoteServiceException_Exception, IOException { String url = getDefectURL(instance, covProjectObj, mddo); String description = dido.getLongDescription(); return StringEscapeUtils.unescapeHtml(description) + "\n\nView in Coverity Connect: \n" + url; } //Replacing "#" for "&" in order to fix bug 62066. protected String getDefectURL(CIMClient instance, ProjectDataObj covProjectObj, MergedDefectDataObj mddo) { return String.format("%s://%s:%d/sourcebrowser.htm?projectId=%s&mergedDefectId=%d", instance.isUseSSL() ? "https" : "http", instance.getHost(), instance.getPort(), covProjectObj.getProjectKey(), mddo.getCid()); } protected EventDataObj getMainEvent(DefectInstanceDataObj dido) { if(dido.getEvents() != null && !dido.getEvents().isEmpty()){ for(EventDataObj edo : dido.getEvents()) { if(edo.isMain()) { return edo; } } // If no event is marked as "main" the first event is returned. return dido.getEvents().get(0); } return null; } @Override public String toString() { return getClass().getSimpleName(); } /* * This method constructs measures from metrics. It adds the required data to the measures, such as a URL, and then * saves the measures into sensorContext. This method is called by analyse(). * */ private void getCoverityLogoMeasures(SensorContext sensorContext, CIMClient client, ProjectDataObj covProjectObj) { String covProject = sensorContext.settings().getString(CoverityPlugin.COVERITY_PROJECT); if (covProject != null) { sensorContext .<String>newMeasure() .forMetric(CoverityPluginMetrics.COVERITY_PROJECT_NAME) .on(sensorContext.module()) .withValue(covProject) .save(); } String ProjectUrl = createURL(client); if (ProjectUrl != null) { sensorContext .<String>newMeasure() .forMetric(CoverityPluginMetrics.COVERITY_URL_CIM_METRIC) .on(sensorContext.module()) .withValue(ProjectUrl) .save(); } String ProductKey= String.valueOf(covProjectObj.getProjectKey()); ProjectUrl = ProjectUrl+"reports.htm#p"+ProductKey; sensorContext .<String>newMeasure() .forMetric(CoverityPluginMetrics.COVERITY_PROJECT_URL) .on(sensorContext.module()) .withValue(ProjectUrl) .save(); sensorContext .<Integer>newMeasure() .forMetric(CoverityPluginMetrics.COVERITY_OUTSTANDING_ISSUES) .on(sensorContext.module()) .withValue(totalDefects) .save(); sensorContext .<Integer>newMeasure() .forMetric(CoverityPluginMetrics.COVERITY_HIGH_IMPACT) .on(sensorContext.module()) .withValue(highImpactDefects) .save(); sensorContext .<Integer>newMeasure() .forMetric(CoverityPluginMetrics.COVERITY_MEDIUM_IMPACT) .on(sensorContext.module()) .withValue(mediumImpactDefects) .save(); sensorContext .<Integer>newMeasure() .forMetric(CoverityPluginMetrics.COVERITY_LOW_IMPACT) .on(sensorContext.module()) .withValue(lowImpactDefects) .save(); } protected ActiveRule findActiveRule(SensorContext context, String domain, String checkerName, String subCategory, String lang) { String key = domain + "_" + checkerName; RuleKey rk = CoverityUtil.getRuleKey(lang, key + "_" + subCategory); ActiveRule ar = context.activeRules().find(rk); if(ar == null && !subCategory.equals("none")){ rk = CoverityUtil.getRuleKey(lang, key + "_" + "none"); ar = context.activeRules().find(rk); } if (ar == null) { if (domain.equals("STATIC_C")) { if (ar == null && checkerName.startsWith("MISRA C")) { rk = CoverityUtil.getRuleKey(lang, "STATIC_C_MISRA.*"); ar = context.activeRules().find(rk); } else if (ar == null && checkerName.startsWith("PW.")) { rk = CoverityUtil.getRuleKey(lang, "STATIC_C_PW.*"); ar = context.activeRules().find(rk); } else if (ar == null && checkerName.startsWith("SW.")) { rk = CoverityUtil.getRuleKey(lang, "STATIC_C_SW.*"); ar = context.activeRules().find(rk); } else if (ar == null && checkerName.startsWith("RW.")) { rk = CoverityUtil.getRuleKey(lang, "STATIC_C_RW.*"); ar = context.activeRules().find(rk); } else { rk = CoverityUtil.getRuleKey(lang, "STATIC_C_coverity-cpp"); ar = context.activeRules().find(rk); } } else if (domain.equals("STATIC_CS")) { if ( ar == null && checkerName.startsWith("MSVSCA")) { rk = CoverityUtil.getRuleKey(lang, "STATIC_CS_MSVSCA.*"); ar = context.activeRules().find(rk); } else { rk = CoverityUtil.getRuleKey(lang, "STATIC_CS_coverity-cs"); ar = context.activeRules().find(rk); } } else if (domain.equals("STATIC_JAVA")) { rk = CoverityUtil.getRuleKey(lang, "STATIC_JAVA_coverity-java"); ar = context.activeRules().find(rk); } else if (domain.equals("OTHER") && lang.equals("js")) { if ( ar == null && checkerName.startsWith("JSHINT")) { rk = CoverityUtil.getRuleKey(lang, "OTHER_JSHINT.*"); ar = context.activeRules().find(rk); } else { rk = CoverityUtil.getRuleKey(lang, "OTHER_coverity-js"); ar = context.activeRules().find(rk); } } else if (domain.equals("OTHER") && lang.equals("py")) { rk = CoverityUtil.getRuleKey(lang, "OTHER_coverity-py"); ar = context.activeRules().find(rk); } else if (domain.equals("OTHER") && lang.equals("php")) { rk = CoverityUtil.getRuleKey(lang, "OTHER_coverity-php"); ar = context.activeRules().find(rk); } } return ar; } }