/******************************************************************************* * Copyright (c) 2017 Synopsys, Inc * 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 * * Contributors: * Synopsys, Inc - initial implementation and documentation *******************************************************************************/ package jenkins.plugins.coverity; import com.coverity.ws.v9.StreamDataObj; import com.coverity.ws.v9.StreamFilterSpecDataObj; import com.coverity.ws.v9.CovRemoteServiceException_Exception; import hudson.EnvVars; import hudson.Extension; import hudson.FilePath; import hudson.Launcher; import hudson.Util; import hudson.model.*; import hudson.tasks.BuildStepDescriptor; import hudson.tasks.BuildStepMonitor; import hudson.tasks.Publisher; import hudson.tasks.Recorder; import hudson.util.FormValidation; import hudson.util.ListBoxModel; import jenkins.model.Jenkins; import jenkins.plugins.coverity.CoverityTool.CoverityToolHandler; import jenkins.plugins.coverity.ws.CimCache; import net.sf.json.JSON; import net.sf.json.JSONArray; import net.sf.json.JSONObject; import org.apache.commons.lang.StringUtils; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import org.kohsuke.stapler.bind.JavaScriptMethod; import javax.annotation.CheckForNull; import javax.annotation.Nonnull; import javax.servlet.ServletException; import javax.servlet.ServletOutputStream; import javax.xml.ws.WebServiceException; import java.io.File; import java.io.IOException; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.*; import java.util.logging.Logger; /** * This publisher optionally invokes cov-analyze/cov-analyze-java and cov-commit-defects. Afterwards the latest list of * defects is queried from the webservice, filtered, and attached to the build. If defects are found, the build can be * flagged as failed and a mail is sent. */ public class CoverityPublisher extends Recorder { private static final Logger logger = Logger.getLogger(CoverityPublisher.class.getName()); // deprecated fields which were removed in plugin version 1.2 private transient String cimInstance; private transient String project; private transient String stream; private transient DefectFilters defectFilters; // deprecated field removed in plugin version 1.9 (removed multiple CIMStreams) private transient List<CIMStream> cimStreams; /** * Configured CIM stream */ private CIMStream cimStream; /** * Configuration for the invocation assistance feature. Null if this should not be used. */ private InvocationAssistance invocationAssistance; /** * Should the build be marked as failed if defects are present ? */ private final boolean failBuild; /** * Should the build be marked as unstable if defects are present ? */ private final boolean unstable; /** * Should the intermediate directory be preserved after each build? */ private final boolean keepIntDir; /** * Should defects be fetched after each build? Enabling this prevents the build from being failed due to defects. */ private final boolean skipFetchingDefects; /** * Hide the chart to make page loads faster */ private final boolean hideChart; private final TaOptionBlock taOptionBlock; private final ScmOptionBlock scmOptionBlock; /** * Internal variable to notify the Publisher that the build should be marked as unstable * since we cannot set the build as unstable within the tool handler */ private boolean unstableBuild; @DataBoundConstructor public CoverityPublisher(CIMStream cimStream, InvocationAssistance invocationAssistance, boolean failBuild, boolean unstable, boolean keepIntDir, boolean skipFetchingDefects, boolean hideChart, TaOptionBlock taOptionBlock, ScmOptionBlock scmOptionBlock) { this.cimStream = cimStream; this.invocationAssistance = invocationAssistance; this.failBuild = failBuild; this.unstable = unstable; this.keepIntDir = keepIntDir; this.skipFetchingDefects = skipFetchingDefects; this.hideChart = hideChart; this.taOptionBlock = taOptionBlock; this.scmOptionBlock = scmOptionBlock; this.unstableBuild = false; } /** * Implement readResolve to update the de-serialized object in the case transient data was found. Transient fields * will be read during de-serialization and readResolve allow updating the Publisher object after being created. */ protected Object readResolve() { // Check for values removed in plugin version 1.2 (cimInstance, project, stream, defectFilters) set if(cimInstance != null || project != null || stream != null || defectFilters != null) { logger.info("Old data format detected. Converting to new format."); convertTransientDataFields(); return this; } // Check for values removed in plugin version 1.9 (streams collection) if(cimStreams != null && !cimStreams.isEmpty()) { logger.info("Old data format detected. Converting to new format."); if (cimStreams.size() > 1) { logger.info("Found multiple commit streams configured. Discarding all but the first stream configured"); } cimStream = cimStreams.get(0); cimStreams = null; // merge in any invocation assistance override values if (cimStream.getInvocationAssistanceOverride() != null) { this.invocationAssistance = this.getInvocationAssistance().merge(cimStream.getInvocationAssistanceOverride()); } } return this; } /** * Converts the old data values cimInstance, project, stream, defectFilters (which were removed in plugin version 1.2) * to a {@link CIMStream} object */ private void convertTransientDataFields() { CIMStream newcs = new CIMStream(cimInstance, project, stream, defectFilters); cimInstance = null; project = null; stream = null; defectFilters = null; if(cimStream == null) { this.cimStream = newcs; } } public CIMStream getCimStream() { return cimStream; } public String getCimInstance() { return cimInstance; } public String getProject() { return project; } public String getStream() { return stream; } public DefectFilters getDefectFilters() { return defectFilters; } public BuildStepMonitor getRequiredMonitorService() { return BuildStepMonitor.STEP; } public InvocationAssistance getInvocationAssistance() { return invocationAssistance; } public boolean isFailBuild() { return failBuild; } public boolean isKeepIntDir() { return keepIntDir; } public boolean isSkipFetchingDefects() { return skipFetchingDefects; } public boolean isHideChart() { return hideChart; } public boolean isUnstable(){ return unstable; } public boolean isUnstableBuild(){ return unstableBuild; } public void setUnstableBuild(boolean unstable){ unstableBuild = unstable; } public TaOptionBlock getTaOptionBlock(){return taOptionBlock;} public ScmOptionBlock getScmOptionBlock(){return scmOptionBlock;} public List<CIMStream> getCimStreams() { if(cimStreams == null) { return new ArrayList<CIMStream>(); } return cimStreams; } @Override public Action getProjectAction(AbstractProject<?, ?> project) { return hideChart ? super.getProjectAction(project) : new CoverityProjectAction(project); } @Override public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) { // set initial state for unstable build to false this.unstableBuild = false; if(build.getResult().isWorseOrEqualTo(Result.FAILURE)) return true; try{ CoverityVersion version = CheckConfig.checkNode(this, build, launcher, listener).getVersion(); if(version == null){ throw new Exception("Coverity Version is null. Please verify the version file under your Coverity Analysis installation."); } CoverityToolHandler cth = new CoverityToolHandler(version); cth.perform(build, launcher, listener, this); if(isUnstableBuild()){ build.setResult(Result.UNSTABLE); } // Delete intermediate directory unless user checked to preserve the intermediate directory option. // Deletion of the intermediate directory will occurr regardless of the result of the build job. deleteIntermediateDirectory(listener, build.getAction(CoverityTempDir.class)); return true; } catch(com.coverity.ws.v9.CovRemoteServiceException_Exception e){ CoverityUtils.handleException("Cov Remote Service Error: \n" + e.getMessage(), build, listener, e); return false; } catch (Exception e) { CoverityUtils.handleException("Exception message: \n" + e.getMessage(), build, listener, e); return false; } } public void deleteIntermediateDirectory(BuildListener listener, CoverityTempDir temp) { if (temp != null) { try{ if(!isKeepIntDir() || temp.isDef()) { listener.getLogger().println("[Coverity] deleting intermediate directory: " + temp.getTempDir()); temp.getTempDir().deleteRecursive(); listener.getLogger().println("[Coverity] deleting intermediate directory \"" + temp.getTempDir() + "\" was successful"); } else { listener.getLogger().println("[Coverity] preserving intermediate directory: " + temp.getTempDir()); } } catch (InterruptedException e) { listener.getLogger().println("[Coverity] Interrupted Exception occurred during deletion of intermediate directory: " + temp.getTempDir()); } catch (IOException e) { listener.getLogger().println("[Coverity] IOException Exception occurred during deletion of intermediate directory: " + temp.getTempDir()); } } } public DescriptorImpl getDescriptor() { return (DescriptorImpl) super.getDescriptor(); } @Extension public static class DescriptorImpl extends BuildStepDescriptor<Publisher> { private List<CIMInstance> instances = new ArrayList<CIMInstance>(); private String home; private SSLConfigurations sslConfigurations; public DescriptorImpl() { super(CoverityPublisher.class); load(); } public CIMStream.DescriptorImpl getCIMStreamDescriptor() { return Jenkins.getInstance().getDescriptorByType(CIMStream.DescriptorImpl.class); } public static List<String> toStrings(ListBoxModel list) { List<String> result = new ArrayList<String>(); for(ListBoxModel.Option option : list) result.add(option.name); return result; } @Override public boolean isApplicable(Class<? extends AbstractProject> jobType) { return true; } @Override public boolean configure(StaplerRequest req, JSONObject json) throws FormException { req.bindJSON(this, json); home = Util.fixEmpty(home); save(); return true; } public String getHome() { return home; } public void setHome(String home) { this.home = home; } public void setSslConfigurations(SSLConfigurations sslConfigurations) { this.sslConfigurations = sslConfigurations; } public SSLConfigurations getSslConfigurations() { /** * Fix Bug:85629 * If SSL were not configured that resulted on a null pointer exception that marked the build as a failure. * In the case SSL is not configured, by default SSL configurations would be set up to not trust self-signed * certificates and no CA file would be present. */ if(this.sslConfigurations != null){ return this.sslConfigurations; } else { return new SSLConfigurations(false, null); } } public String getHome(Node node, EnvVars environment) { CoverityInstallation install = node.getNodeProperties().get(CoverityInstallation.class); if(install != null) { return install.forEnvironment(environment).getHome(); } else if(home != null) { return new CoverityInstallation(home).forEnvironment(environment).getHome(); } else { return null; } } public List<CIMInstance> getInstances() { return instances; } public void setInstances(List<CIMInstance> instances) { this.instances = instances; } public CIMInstance getInstance(String name) { for(CIMInstance instance : instances) { if(instance.getName().equals(name)) { return instance; } } return null; } @Override public String getDisplayName() { return "Coverity"; } public FormValidation doCheckInstance(@QueryParameter String host, @QueryParameter int port, @QueryParameter String user, @QueryParameter String password, @QueryParameter boolean useSSL, @QueryParameter int dataPort) throws IOException { return new CIMInstance("", host, port, user, password, useSSL, dataPort).doCheck(); } public FormValidation doCheckAnalysisLocation(@QueryParameter String home) throws IOException { File analysisDir = new File(home); File analysisVersionXml = new File(home, "VERSION.xml"); if(analysisDir.exists()){ if(analysisVersionXml.isFile()){ try { // check the version file value and validate it is greater than minimum version CoverityVersion version = CheckConfig.getVersion(new FilePath(analysisDir), null); if(version.compareTo(CoverityVersion.MINIMUM_SUPPORTED_VERSION) < 0) { return FormValidation.error("\"Coverity Static Analysis\" version " + version.toString() + " detected. " + "The minimum supported version is " + CoverityVersion.MINIMUM_SUPPORTED_VERSION.getEffectiveVersion().toString()); } } catch (InterruptedException e) { return FormValidation.error("Unable to verify the \"Coverity Static Analysis\" directory version."); } return FormValidation.ok("Analysis installation directory has been verified."); } else{ return FormValidation.error("The specified \"Coverity Static Analysis\" directory doesn't contain a VERSION.xml file."); } } else{ return FormValidation.error("The specified \"Coverity Static Analysis\" directory doesn't exists."); } } public FormValidation doCheckCutOffDate(@QueryParameter String value) throws FormException { try { if(!StringUtils.isEmpty(value)) new SimpleDateFormat("yyyy-MM-dd").parse(value); return FormValidation.ok(); } catch(ParseException e) { return FormValidation.error("yyyy-MM-dd expected"); } } public FormValidation doCheckDate(@QueryParameter String date) { try { if(!StringUtils.isEmpty(date.trim())) { new SimpleDateFormat("yyyy-MM-dd").parse(date); } return FormValidation.ok(); } catch(ParseException e) { return FormValidation.error("Date in yyyy-mm-dd format expected"); } } @Override public Publisher newInstance(@CheckForNull StaplerRequest req, @Nonnull JSONObject formData) throws FormException { logger.info(formData.toString()); // even though request is always non-null, needs check (see note on Descriptor.newInstance) if (req == null) { return super.newInstance(req, formData); } String cutOffDate = Util.fixEmpty(req.getParameter("cutOffDate")); try { if(cutOffDate != null) new SimpleDateFormat("yyyy-MM-dd").parse(cutOffDate); } catch(ParseException e) { throw new Descriptor.FormException("Could not parse date '" + cutOffDate + "', yyyy-MM-dd expected", "cutOffDate"); } CoverityPublisher publisher = (CoverityPublisher) super.newInstance(req, formData); CIMStream cimStream = publisher.getCimStream(); CIMStream.DescriptorImpl cimStreamDescriptor = ((CIMStream.DescriptorImpl) cimStream.getDescriptor()); String cimInstance = cimStream.getInstance(); try { if(cimStream.isValid()) { DefectFilters defectFilters = cimStream.getDefectFilters(); if(defectFilters != null) { List<String> allCheckers = getInstance(cimStream.getInstance()).getCimInstanceCheckers(); List<String> allComponents = toStrings(cimStreamDescriptor.doFillComponentDefectFilterItems(cimInstance, cimStream.getStream())); defectFilters.invertCheckers(allCheckers); defectFilters.invertComponents(allComponents); } } } catch (CovRemoteServiceException_Exception | WebServiceException e) { throw new Descriptor.FormException( "There was an exception from the configured Coverity Connect server (instance: " + cimInstance + "). Please verify the Coverity Connect instance configuration is valid.", e, "defectFilters"); } catch (IOException e) { throw new RuntimeException(e); } return publisher; } private JSONObject getJSONClassObject(JSONObject o, String targetClass) { //try old-style json format JSONObject jsonA = o.getJSONObject(getJsonSafeClassName()); if(jsonA == null || jsonA.toString().equals("null")) { //new style json format JSON jsonB = (JSON) o.get("publisher"); if(jsonB.isArray()) { JSONArray arr = (JSONArray) jsonB; for(Object i : arr) { JSONObject ji = (JSONObject) i; if(targetClass.equals(ji.get("stapler-class"))) { return ji; } } } else { return (JSONObject) jsonB; } } else { return jsonA; } return null; } public void doCheckConfig(StaplerRequest req, StaplerResponse rsp) throws ServletException, IOException { JSONObject json = getJSONClassObject(req.getSubmittedForm(), getId()); if(json != null && !json.isNullObject()) { CoverityPublisher publisher = req.bindJSON(CoverityPublisher.class, json); CheckConfig ccs = new CheckConfig(publisher, null, null, null); ccs.check(); req.setAttribute("descriptor", ccs.getDescriptor()); req.setAttribute("instance", ccs); rsp.forward(ccs.getDescriptor(), "checkConfig", req); } } public void doDefectFiltersConfig(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException, CovRemoteServiceException_Exception { logger.info(req.getSubmittedForm().toString()); JSONObject json = getJSONClassObject(req.getSubmittedForm(), getId()); CIMStream.DescriptorImpl cimStreamDescriptor = null; if(json != null && !json.isNullObject()) { CoverityPublisher publisher = req.bindJSON(CoverityPublisher.class, json); CIMStream cimStream = publisher.getCimStream(); if (cimStream != null) cimStreamDescriptor = ((CIMStream.DescriptorImpl) cimStream.getDescriptor()); if (cimStreamDescriptor != null) { if (StringUtils.isEmpty(cimStream.getInstance()) || StringUtils.isEmpty(cimStream.getProject()) || StringUtils.isEmpty(cimStream.getStream())) { //do nothing when any of instance / project / stream is not yet configured } else { //initialize 'new' defectFilters item with default values selected List<String> allCheckers = getInstance(cimStream.getInstance()).getCimInstanceCheckers(); DefectFilters defectFilters = cimStream.getDefectFilters(); if (defectFilters != null) { try { cimStream.getDefectFilters().initializeFilter( allCheckers, toStrings(cimStreamDescriptor.doFillClassificationDefectFilterItems(cimStream.getInstance())), toStrings(cimStreamDescriptor.doFillActionDefectFilterItems(cimStream.getInstance())), toStrings(cimStreamDescriptor.doFillSeveritiesDefectFilterItems(cimStream.getInstance())), toStrings(cimStreamDescriptor.doFillComponentDefectFilterItems(cimStream.getInstance(), cimStream.getStream())), toStrings(cimStreamDescriptor.doFillImpactDefectFilterItems(cimStream.getInstance()))); } catch (CovRemoteServiceException_Exception e) { throw new IOException(e); } } } req.setAttribute("descriptor", cimStreamDescriptor); req.setAttribute("instance", cimStream); } } rsp.forward(cimStreamDescriptor, "defectFilters", req); } @JavaScriptMethod public void doLoadProjectsForInstance(StaplerRequest req, StaplerResponse rsp) throws ServletException, IOException { JSONObject json = getJSONClassObject(req.getSubmittedForm(), getId()); if(json != null && !json.isNullObject()) { CoverityPublisher publisher = req.bindJSON(CoverityPublisher.class, json); CIMStream cimStream = publisher.getCimStream(); if (cimStream != null) { CIMInstance cimInstance = publisher.getDescriptor().getInstance(cimStream.getInstance()); final List<String> projects = new ArrayList<>(CimCache.getInstance().getProjects(cimInstance)); final String selectedProject = cimStream.getProject(); boolean selectedProjectIsvalid = true; if (!StringUtils.isEmpty(selectedProject) && !projects.contains(selectedProject)) { projects.add(selectedProject); selectedProjectIsvalid = false; } rsp.setContentType("application/json; charset=utf-8"); final ServletOutputStream outputStream = rsp.getOutputStream(); JSONObject responseObject = new JSONObject(); responseObject.put("projects", projects); responseObject.put("selectedProject", selectedProject); responseObject.put("validSelection", selectedProjectIsvalid); String jsonString = responseObject.toString(); outputStream.write(jsonString.getBytes("UTF-8")); } } } @JavaScriptMethod public void doLoadStreamsForProject(StaplerRequest req, StaplerResponse rsp) throws ServletException, IOException { JSONObject json = getJSONClassObject(req.getSubmittedForm(), getId()); if(json != null && !json.isNullObject()) { CoverityPublisher publisher = req.bindJSON(CoverityPublisher.class, json); CIMStream cimStream = publisher.getCimStream(); if (cimStream != null) { CIMInstance cimInstance = publisher.getDescriptor().getInstance(cimStream.getInstance()); final List<String> streams = new ArrayList<>(CimCache.getInstance().getStreams(cimInstance ,cimStream.getProject())); final String selectedStream = cimStream.getStream(); boolean selectedStreamIsvalid = true; if (!StringUtils.isEmpty(selectedStream) && !streams.contains(selectedStream)) { streams.add(selectedStream); selectedStreamIsvalid = false; } rsp.setContentType("application/json; charset=utf-8"); final ServletOutputStream outputStream = rsp.getOutputStream(); JSONObject responseObject = new JSONObject(); responseObject.put("streams", streams); responseObject.put("selectedStream", selectedStream); responseObject.put("validSelection", selectedStreamIsvalid); String jsonString = responseObject.toString(); outputStream.write(jsonString.getBytes("UTF-8")); } } } } }