/* * * Copyright 2014 Applied Visions * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License * */ package org.jenkinsci.plugins.codedx; import com.secdec.codedx.api.client.*; import com.secdec.codedx.api.client.Job; import com.secdec.codedx.api.client.Project; import com.secdec.codedx.security.JenkinsSSLConnectionSocketFactoryFactory; import com.secdec.codedx.util.CodeDxVersion; import hudson.FilePath; import hudson.Launcher; import hudson.Extension; import hudson.model.*; import hudson.util.FormValidation; import hudson.util.ListBoxModel; import hudson.tasks.BuildStepMonitor; import hudson.tasks.BuildStepDescriptor; import hudson.tasks.Publisher; import hudson.tasks.Recorder; import net.sf.json.JSONObject; import org.apache.commons.io.IOUtils; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.impl.client.HttpClientBuilder; import org.jenkinsci.plugins.codedx.model.CodeDxReportStatistics; import org.jenkinsci.plugins.codedx.model.CodeDxGroupStatistics; import org.jenkinsci.plugins.tokenmacro.MacroEvaluationException; import org.jenkinsci.plugins.tokenmacro.TokenMacro; import org.kohsuke.stapler.AncestorInPath; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.QueryParameter; import javax.net.ssl.SSLHandshakeException; import javax.servlet.ServletException; import java.io.IOException; import java.io.InputStream; import java.io.PrintStream; import java.net.MalformedURLException; import java.net.URL; import java.security.GeneralSecurityException; import java.util.*; import java.util.logging.Logger; /** * Jenkins publisher that publishes project source, binaries, and * analysis tool output files to a CodeDx server. * * @author anthonyd */ public class CodeDxPublisher extends Recorder { private final String url; private final String key; private final String projectId; private final String sourceAndBinaryFiles; private final String toolOutputFiles; private final String excludedSourceAndBinaryFiles; private final String analysisName; private final AnalysisResultConfiguration analysisResultConfiguration; private transient CodeDxClient client; private final String selfSignedCertificateFingerprint; private final static Logger logger = Logger.getLogger(CodeDxPublisher.class.getName()); /** * @param url URL of the Code Dx server * @param key API key of the Code Dx server * @param projectId Code Dx project ID * @param sourceAndBinaryFiles Comma separated list of source/binary file Ant GLOB patterns * @param toolOutputFiles List of paths to tool output files * @param excludedSourceAndBinaryFiles Comma separated list of source/binary file Ant GLOB patterns to exclude * @param analysisResultConfiguration Contains the fields applicable when the user chooses to have Jenkins wait for * analysis runs to complete. */ @DataBoundConstructor public CodeDxPublisher( final String url, final String key, final String projectId, final String sourceAndBinaryFiles, final String toolOutputFiles, final String excludedSourceAndBinaryFiles, final String analysisName, final AnalysisResultConfiguration analysisResultConfiguration, final String selfSignedCertificateFingerprint ) { this.projectId = projectId; this.url = url; this.key = key; this.sourceAndBinaryFiles = sourceAndBinaryFiles; this.excludedSourceAndBinaryFiles = excludedSourceAndBinaryFiles; this.toolOutputFiles = toolOutputFiles; this.analysisName = analysisName.trim(); this.analysisResultConfiguration = analysisResultConfiguration; this.selfSignedCertificateFingerprint = selfSignedCertificateFingerprint; setupClient(); } private void setupClient() { if (this.client == null) { this.client = buildClient(url, key, selfSignedCertificateFingerprint); } } public AnalysisResultConfiguration getAnalysisResultConfiguration() { return analysisResultConfiguration; } public String getProjectId() { return projectId; } public String getUrl() { return url; } public String getKey() { return key; } public String getSourceAndBinaryFiles() { return sourceAndBinaryFiles; } public String getToolOutputFiles() { return toolOutputFiles; } public String getExcludedSourceAndBinaryFiles() { return excludedSourceAndBinaryFiles; } public String getSelfSignedCertificateFingerprint() { return selfSignedCertificateFingerprint; } public String getAnalysisName(){ return analysisName; } @Override public Action getProjectAction(AbstractProject<?, ?> project) { String latestUrl = null; if (projectId.length() != 0 && !projectId.equals("-1")) { setupClient(); latestUrl = client.buildLatestFindingsUrl(Integer.parseInt(projectId)); } return new CodeDxProjectAction(project, analysisResultConfiguration, latestUrl); } @Override public boolean perform( final AbstractBuild<?, ?> build, final Launcher launcher, final BuildListener listener) throws InterruptedException, IOException { Date startingDate = new Date(); setupClient(); final Map<String, InputStream> toSend = new HashMap<String, InputStream>(); final PrintStream buildOutput = listener.getLogger(); buildOutput.println("Publishing build to Code Dx:"); if (projectId.length() == 0 || projectId.equals("-1")) { buildOutput.println("No project has been selected"); return true; } buildOutput.println(String.format("Publishing to Code Dx server at %s to Code Dx project %s: ", url, projectId)); buildOutput.println("Creating source/binary zip..."); FilePath sourceAndBinaryZip = Archiver.Archive(build.getWorkspace(), Util.commaSeparatedToArray(sourceAndBinaryFiles), Util.commaSeparatedToArray(excludedSourceAndBinaryFiles), "source", buildOutput); if (sourceAndBinaryZip != null) { try { buildOutput.println("Adding source/binary zip..."); toSend.put("Jenkins-SourceAndBinary", sourceAndBinaryZip.read()); } catch (IOException e) { buildOutput.println("Failed to add source/binary zip."); } } else { buildOutput.println("No matching source/binary files."); } String[] files = Util.commaSeparatedToArray(toolOutputFiles); for (String file : files) { if (file.length() != 0) { FilePath path = build.getWorkspace().child(file); if (path.exists()) { try { buildOutput.println("Add tool output file " + path.getRemote() + " to request."); toSend.put(path.getName(), path.read()); } catch (IOException e) { buildOutput.println("Failed to add tool output file: " + path); } } } } if (toSend.size() > 0) { final CodeDxClient repeatingClient = new CodeDxRepeatingClient(this.client, buildOutput); CodeDxVersion cdxVersion = null; try { cdxVersion = repeatingClient.getCodeDxVersion(); buildOutput.println("Got Code Dx version: " + cdxVersion); } catch (CodeDxClientException e) { e.printStackTrace(buildOutput); buildOutput.println("Failed to get Code Dx version; aborting build."); return false; } try { buildOutput.println("Submitting files to Code Dx for analysis"); int projectIdInt = Integer.parseInt(projectId); StartAnalysisResponse response; try { response = repeatingClient.startAnalysis(Integer.parseInt(projectId), toSend); } catch (CodeDxClientException e) { String errorSpecificMessage; switch(e.getHttpCode()) { case 400: errorSpecificMessage = " (Bad Request: have you included files from unsupported Tools? " + "Code Dx Standard Edition does not support uploading tool results)"; break; case 403: errorSpecificMessage = " (Forbidden: have you configured your key and permissions correctly?)"; break; case 404: errorSpecificMessage = " (Project Not Found: is it possible it was deleted?)"; break; case 500: errorSpecificMessage = " (Internal Server Error: Please check your Code Dx server logs for more details)"; break; default: errorSpecificMessage = ""; } buildOutput.println(String.format("Failed to start analysis%s.", errorSpecificMessage)); buildOutput.println(String.format("Response Status: %d: %s", e.getHttpCode(), e.getResponseMessage())); buildOutput.println(String.format("Response Content: %s", e.getResponseContent())); e.printStackTrace(buildOutput); return false; } finally { // close streams after we're done sending them for(Map.Entry<String, InputStream> entry : toSend.entrySet()){ IOUtils.closeQuietly(entry.getValue()); } } buildOutput.println("Code Dx accepted files for analysis"); // Set the analysis name on the server if(response != null){ if(analysisName == null || analysisName.length() == 0){ buildOutput.println("No 'Analysis Name' was chosen."); } else { buildOutput.println("Analysis Name (raw): " + analysisName); String expandedAnalysisName = ""; try { expandedAnalysisName = TokenMacro.expand(build, listener, analysisName); buildOutput.println("Analysis Name expression expanded to: " + expandedAnalysisName); } catch (MacroEvaluationException e) { buildOutput.println("Failed to expand Analysis Name expression using TokenMacro. " + "Falling back to built-in Jenkins functionality"); e.printStackTrace(buildOutput); expandedAnalysisName = build.getEnvironment(listener).expand(analysisName); } buildOutput.println("Analysis Name: " + expandedAnalysisName); buildOutput.println("Analysis Id: " + response.getAnalysisId()); if(cdxVersion.compareTo(CodeDxVersion.MIN_FOR_ANALYSIS_NAMES) < 0){ buildOutput.println("The connected Code Dx server is only version " + cdxVersion + ", which doesn't support naming analyses (minimum supported version is " + CodeDxVersion.MIN_FOR_ANALYSIS_NAMES + "). The analysis name will not be set."); } else { try { repeatingClient.setAnalysisName(projectIdInt, response.getAnalysisId(), expandedAnalysisName); buildOutput.println("Successfully updated analysis name."); } catch (CodeDxClientException e) { buildOutput.println("Got error from Code Dx API Client while trying to set the analysis name"); e.printStackTrace(buildOutput); return false; } } } } if (analysisResultConfiguration == null) { logger.info("Project not configured to wait on analysis results"); return true; } String status = null; String oldStatus = null; try { do { Thread.sleep(3000); oldStatus = status; status = repeatingClient.getJobStatus(response.getJobId()); if (status != null && !status.equals(oldStatus)) { if (Job.QUEUED.equals(status)) { buildOutput.println("Code Dx analysis is queued"); } else if (Job.RUNNING.equals(status)) { buildOutput.println("Code Dx analysis is running"); } } } while (Job.QUEUED.equals(status) || Job.RUNNING.equals(status)); } catch (CodeDxClientException e) { buildOutput.println("Fatal Error! There was a problem querying for the analysis status."); e.printStackTrace(buildOutput); return false; } if (Job.COMPLETED.equals(status)) { try { buildOutput.println("Analysis succeeded"); buildOutput.println("Fetching severity counts"); Filter notGoneFilter = new Filter(); notGoneFilter.setNotStatus(new String[]{Filter.STATUS_GONE}); List<CountGroup> severityCounts = repeatingClient.getFindingsGroupedCounts(projectIdInt, notGoneFilter, "severity"); buildOutput.println("Fetching status counts"); Filter notAssignedFilter = new Filter(); // make sure not to put STATUS_NEW in a filter if the cdxVersion doesn't support it List<String> notAssignedStatuses = new ArrayList<String>(7); notAssignedStatuses.add(Filter.STATUS_ESCALATED); notAssignedStatuses.add(Filter.STATUS_FALSE_POSITIVE); notAssignedStatuses.add(Filter.STATUS_FIXED); notAssignedStatuses.add(Filter.STATUS_MITIGATED); notAssignedStatuses.add(Filter.STATUS_IGNORED); notAssignedStatuses.add(Filter.STATUS_UNRESOLVED); if(cdxVersion.supportsTriageNew()){ logger.fine("TriageNew supported by Code Dx version " + cdxVersion + ". Using 'New' in not-assigned status list."); notAssignedStatuses.add(Filter.STATUS_NEW); } else { logger.fine("TriageNew not supported by Code Dx version " + cdxVersion + ". Omitting it from the not-assigned status list"); } notAssignedFilter.setStatus(notAssignedStatuses.toArray(new String[notAssignedStatuses.size()])); List<CountGroup> statusCounts = repeatingClient.getFindingsGroupedCounts(projectIdInt, notAssignedFilter, "status"); Filter assignedFilter = new Filter(); assignedFilter.setStatus(new String[]{Filter.STATUS_ASSIGNED}); buildOutput.println("Fetching assigned count"); //Since CodeDx splits assigned status into different statuses (one per user), //we need to get the total assigned count and add our own CountGroup. int assignedCount = repeatingClient.getFindingsCount(projectIdInt, assignedFilter); if (assignedCount > 0) { CountGroup assignedGroup = new CountGroup(); assignedGroup.setName("Assigned"); assignedGroup.setCount(assignedCount); statusCounts.add(assignedGroup); } buildOutput.println("Building table and charts"); Map<String, CodeDxReportStatistics> statMap = new HashMap<String, CodeDxReportStatistics>(); statMap.put("severity", createStatistics(severityCounts)); statMap.put("status", createStatistics(statusCounts)); CodeDxResult result = new CodeDxResult(statMap, build); buildOutput.println("Adding CodeDx build action"); build.addAction(new CodeDxBuildAction(build, result)); AnalysisResultChecker checker = new AnalysisResultChecker(repeatingClient, cdxVersion, analysisResultConfiguration.getFailureSeverity(), analysisResultConfiguration.getUnstableSeverity(), startingDate, // the time this process started is the "new" threshold for filtering analysisResultConfiguration.isFailureOnlyNew(), analysisResultConfiguration.isUnstableOnlyNew(), projectIdInt, buildOutput); build.setResult(checker.checkResult()); } catch (CodeDxClientException e) { buildOutput.println("Fatal Error! There was a problem retrieving analysis results."); e.printStackTrace(buildOutput); return false; } return true; } else { buildOutput.println("Analysis status: " + status); return false; } } catch (NumberFormatException e) { buildOutput.println("Invalid project Id"); } finally { sourceAndBinaryZip.delete(); } } else { buildOutput.println("Nothing to send, this doesn't seem right! Please check your 'Code Dx > Source and Binary Files' configuration."); } return false; } public static CodeDxClient buildClient(String url, String key, String fingerprint) { CodeDxClient client = new CodeDxClient(url, key); try { if (fingerprint != null) { fingerprint = fingerprint.replaceAll("[^a-fA-F0-9]", ""); } URL parsedUrl = new URL(url); SSLConnectionSocketFactory socketFactory = JenkinsSSLConnectionSocketFactoryFactory.getFactory(fingerprint, parsedUrl.getHost()); HttpClientBuilder builder = HttpClientBuilder.create(); builder.setSSLSocketFactory(socketFactory); client = new CodeDxClient(url, key, builder); } catch (MalformedURLException e) { logger.warning("A valid CodeDxClient could not be built. Malformed URL: " + url); } catch (GeneralSecurityException e) { logger.warning("A valid CodeDxClient could not be built. GeneralSecurityException: url: " + url + ", fingerprint: " + fingerprint); } catch (Exception e) { logger.warning("An exception was thrown while building the client " + e); e.printStackTrace(); } return client; } private String[] getUsers(Map<String, TriageStatus> assignedStatuses) { List<String> users = new ArrayList<String>(); for (TriageStatus status : assignedStatuses.values()) { if (status.getType().equals(TriageStatus.TYPE_USER)) { users.add(status.getDisplay()); } } return users.toArray(new String[0]); } private CodeDxReportStatistics createStatistics(List<CountGroup> countGroups) { List<CodeDxGroupStatistics> groupStatsList = new ArrayList<CodeDxGroupStatistics>(); for (CountGroup group : countGroups) { CodeDxGroupStatistics stats = new CodeDxGroupStatistics(group.getName(), group.getCount()); groupStatsList.add(stats); } return new CodeDxReportStatistics(groupStatsList); } public BuildStepMonitor getRequiredMonitorService() { return BuildStepMonitor.NONE; // NONE since this is not dependent on the last step } // Overridden for better type safety. // If your plugin doesn't really define any property on Descriptor, // you don't have to do this. @Override public DescriptorImpl getDescriptor() { return (DescriptorImpl) super.getDescriptor(); } /** * Descriptor for {@link CodeDxPublisher}. Used as a singleton. * The class is marked as public so that it can be accessed from views. */ @Extension // This indicates to Jenkins that this is an implementation of an extension point. public static final class DescriptorImpl extends BuildStepDescriptor<Publisher> { /** * To persist global configuration information, * simply store it in a field and call save(). * * <p> * If you don't want fields to be persisted, use <tt>transient</tt>. */ /** * In order to load the persisted global configuration, you have to * call load() in the constructor. */ public DescriptorImpl() { load(); } public boolean isApplicable(Class<? extends AbstractProject> aClass) { // Indicates that this builder can be used with all kinds of project types return true; } /** * This human readable name is used in the configuration screen. */ public String getDisplayName() { return "Publish to Code Dx"; } public FormValidation doCheckProjectId(@QueryParameter final String value) throws IOException, ServletException { if (value.length() == 0) return FormValidation.error("Please set a project. If none are shown above, then be sure that system settings are configured correctly."); if (Integer.parseInt(value) == -1) return FormValidation.error("Failed to get available projects, please ensure systems settings are configured correctly."); return FormValidation.ok(); } public FormValidation doCheckKey(@QueryParameter final String value) throws IOException, ServletException { if (value.length() == 0) return FormValidation.error("Please set a Key."); return FormValidation.ok(); } public FormValidation doCheckUrl(@QueryParameter final String value, @QueryParameter final String selfSignedCertificateFingerprint) throws IOException, ServletException { CodeDxClient client = buildClient(value, "", selfSignedCertificateFingerprint); if (value.length() == 0) return FormValidation.error("Please set a URL."); try { new URL(value); } catch (MalformedURLException malformedURLException) { return FormValidation.error("Malformed URL"); } if (value.toLowerCase().startsWith("http:")) { return FormValidation.warning("HTTP is considered insecure, it is recommended that you use HTTPS."); } else if (value.toLowerCase().startsWith("https:")) { try { client.getProjects(); } catch (Exception e) { if (e instanceof SSLHandshakeException) { return FormValidation.warning("The SSL Certificate presented by the server is invalid. If this is expected, please input an SHA1 Fingerprint in the \"Advanced\" option"); } } return FormValidation.ok(); } else { return FormValidation.error("Invalid protocol, please use HTTPS or HTTP."); } } public FormValidation doCheckSelfSignedCertificateFingerprint(@QueryParameter final String value, @QueryParameter final String url) { if (url != null && ! url.isEmpty() && value != null && ! value.isEmpty()) { CodeDxClient client = buildClient(url, "", value); try { client.getProjects(); } catch (Exception e) { if (e instanceof SSLHandshakeException) { logger.warning("When retrieving projects: " + e); e.printStackTrace(); if (isFingerprintMismatch((SSLHandshakeException)e)) { return FormValidation.warning("The fingerprint doesn't match the fingerprint of the certificate presented by the server"); } else { return FormValidation.warning("A secure connection to the server could not be established"); } } } } return FormValidation.ok(); } public FormValidation doCheckSourceAndBinaryFiles(@QueryParameter final String value, @QueryParameter final String toolOutputFiles, @AncestorInPath AbstractProject project) { if (value.length() == 0) { if (toolOutputFiles.length() == 0) return FormValidation.error("You must specify \"Tool Output Files\" and/or \"Source and Binary Files\""); else return FormValidation.warning("It is recommended that at least source files are provided to Code Dx."); } return Util.checkCSVGlobMatches(value, project.getSomeWorkspace()); } public FormValidation doCheckExcludedSourceAndBinaryFiles(@QueryParameter final String value, @AncestorInPath AbstractProject project) { return Util.checkCSVGlobMatches(value, project.getSomeWorkspace()); } public FormValidation doCheckToolOutputFiles(@QueryParameter final String value, @QueryParameter final String sourceAndBinaryFiles, @AncestorInPath AbstractProject project) { if (value.length() == 0 && sourceAndBinaryFiles.length() == 0) { return FormValidation.error("You must specify \"Tool Output Files\" and/or \"Source and Binary Files\""); } return Util.checkCSVFileMatches(value, project.getSomeWorkspace()); } public ListBoxModel doFillProjectIdItems(@QueryParameter final String url, @QueryParameter final String selfSignedCertificateFingerprint, @QueryParameter final String key, @AncestorInPath AbstractProject project) { ListBoxModel listBox = new ListBoxModel(); CodeDxClient client = buildClient(url, key, selfSignedCertificateFingerprint); try { final List<Project> projects = client.getProjects(); Map<String, Boolean> duplicates = new HashMap<String, Boolean>(); for (Project proj : projects) { if (!duplicates.containsKey(proj.getName())) { duplicates.put(proj.getName(), false); } else { duplicates.put(proj.getName(), true); } } for (Project proj : projects) { if (!duplicates.get(proj.getName())) { listBox.add(proj.getName(), Integer.toString(proj.getId())); } else { listBox.add(proj.getName() + " (id:" + proj.getId() + ")", Integer.toString(proj.getId())); } } } catch (Exception e) { logger.warning("Exception when populating projects dropdown " + e); listBox.add("", "-1"); } return listBox; } public ListBoxModel doFillFailureSeverityItems() { return getSeverityItems(); } public ListBoxModel doFillUnstableSeverityItems() { return getSeverityItems(); } private ListBoxModel getSeverityItems() { final ListBoxModel listBox = new ListBoxModel(); listBox.add("None", "None"); listBox.add("Info or Higher", "Info"); listBox.add("Low or Higher", "Low"); listBox.add("Medium or Higher", "Medium"); listBox.add("High or Higher", "High"); listBox.add("Critical", "Critical"); return listBox; } @Override public boolean configure(final StaplerRequest req, final JSONObject formData) throws FormException { // To persist global configuration information, // set that to properties and call save(). // ^Can also use req.bindJSON(this, formData); // (easier when there are many fields; need set* methods for this, like setUseFrench) save(); System.out.println("Code Dx descriptor configure method"); return super.configure(req, formData); } @Override public Publisher newInstance(StaplerRequest req, JSONObject formData) throws FormException { return super.newInstance(req, formData); } } private static boolean isFingerprintMismatch(SSLHandshakeException exception) { return exception.getMessage().contains("None of the TrustManagers trust this certificate chain"); } }