/* * The MIT License (MIT) * * Copyright (c) 2015 ludovicRoucoux * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ package fr.novia.zaproxyplugin; import hudson.EnvVars; import hudson.Extension; import hudson.FilePath; import hudson.FilePath.FileCallable; import hudson.Launcher; import hudson.Launcher.LocalLauncher; import hudson.Launcher.RemoteLauncher; import hudson.Util; import hudson.model.BuildListener; import hudson.model.Computer; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.Node; import hudson.remoting.VirtualChannel; import hudson.slaves.SlaveComputer; import hudson.tasks.BuildStepDescriptor; import hudson.tasks.Builder; import net.sf.json.JSONObject; import org.apache.commons.io.FileUtils; import org.apache.commons.lang.exception.ExceptionUtils; import org.jenkinsci.remoting.RoleChecker; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.StaplerRequest; import java.io.File; import java.io.IOException; /** * /!\ * Au jour du 27/03/2015 * La version 2.3.1 de ZAPROXY ne contient pas le plugin "pscanrules-release-10.zap" qui sert à * remonter les alertes lors d'un scan passif (spider). Il faut donc ajouter ce plugin manuellement ou * télécharger la prochaine version de ZAPROXY (2.4) via Custom Tools Plugin (et non la 2.3.1) * /!\ * * The main class of the plugin. This class adds a build step in a Jenkins job that allows you * to launch the ZAProxy security tool and get alerts reports from it. * * @author ludovic.roucoux * */ public class ZAProxyBuilder extends Builder { /** To start ZAP as a prebuild step */ private final boolean startZAPFirst; /** The objet to start and call ZAProxy methods */ private final ZAProxy zaproxy; /** Host configured when ZAProxy is used as proxy */ private final String zapProxyHost; /** Port configured when ZAProxy is used as proxy */ private final int zapProxyPort; // Fields in fr/novia/zaproxyplugin/ZAProxyBuilder/config.jelly must match the parameter names in the "DataBoundConstructor" @DataBoundConstructor public ZAProxyBuilder(boolean startZAPFirst, String zapProxyHost, int zapProxyPort, ZAProxy zaproxy) { this.startZAPFirst = startZAPFirst; this.zaproxy = zaproxy; this.zapProxyHost = zapProxyHost; this.zapProxyPort = zapProxyPort; this.zaproxy.setZapProxyHost(zapProxyHost); this.zaproxy.setZapProxyPort(zapProxyPort); //call the set methods of Zaoroxy to set the values this.zaproxy.setJiraBaseURL(ZAProxyBuilder.DESCRIPTOR.getJiraBaseURL()); this.zaproxy.setJiraUserName(ZAProxyBuilder.DESCRIPTOR.getJiraUserName()); this.zaproxy.setJiraPassword(ZAProxyBuilder.DESCRIPTOR.getJiraPassword()); } /* * Getters allows to access member via UI (config.jelly) */ public boolean getStartZAPFirst() { return startZAPFirst; } public ZAProxy getZaproxy() { return zaproxy; } public String getZapProxyHost() { return zapProxyHost; } public int getZapProxyPort() { return zapProxyPort; } // 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 ZAProxyBuilderDescriptorImpl getDescriptor() { return (ZAProxyBuilderDescriptorImpl)super.getDescriptor(); } // Method called before launching the build public boolean prebuild(AbstractBuild<?, ?> build, BuildListener listener) { listener.getLogger().println("------- START Replace environment variables -------"); //replace the environment variables with the corresponding values String reportName=zaproxy.getFilenameReports(); try { reportName=applyMacro( build, listener, reportName); } catch (InterruptedException e1) { listener.error(ExceptionUtils.getStackTrace(e1)); } // zaproxy.setFilenameReports(reportName); //we don't overwrite the file name containing the environment variables //the evaluated value is saved in an other file name zaproxy.setEvaluatedFilenameReports(reportName); listener.getLogger().println("ReportName : "+reportName); listener.getLogger().println("------- END Replace environment variables -------"); if(startZAPFirst) { listener.getLogger().println("------- START Prebuild -------"); try { Launcher launcher = null; Node node = build.getBuiltOn(); // Create launcher according to the build's location (Master or Slave) and the build's OS if("".equals(node.getNodeName())) { // Build on master launcher = new LocalLauncher(listener, build.getWorkspace().getChannel()); } else { // Build on slave boolean isUnix; if( "Unix".equals(((SlaveComputer)node.toComputer()).getOSDescription()) ) { isUnix = true; } else { isUnix = false; } launcher = new RemoteLauncher(listener, build.getWorkspace().getChannel(), isUnix); } zaproxy.startZAP(build, listener, launcher); } catch (Exception e) { e.printStackTrace(); listener.error(ExceptionUtils.getStackTrace(e)); return false; } listener.getLogger().println("------- END Prebuild -------"); } return true; } // Method called when the build is launching @Override public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) { listener.getLogger().println("Perform ZAProxy"); if(!startZAPFirst) { try { zaproxy.startZAP(build, listener, launcher); } catch (Exception e) { e.printStackTrace(); listener.error(ExceptionUtils.getStackTrace(e)); return false; } } boolean res; try { //copyPolicyFile(build.getWorkspace(), listener); // TODO maybe in future version res = build.getWorkspace().act(new ZAProxyCallable(this.zaproxy, listener)); } catch (Exception e) { e.printStackTrace(); listener.error(ExceptionUtils.getStackTrace(e)); return false; } return res; } /** * Replace macro with environment variable if it exists * @param build * @param listener * @param macro * @return * @throws InterruptedException */ @SuppressWarnings({ "unchecked", "rawtypes" }) public static String applyMacro(AbstractBuild build, BuildListener listener, String macro) throws InterruptedException{ try { EnvVars envVars = new EnvVars(Computer.currentComputer().getEnvironment()); envVars.putAll(build.getEnvironment(listener)); envVars.putAll(build.getBuildVariables()); return Util.replaceMacro(macro, envVars); } catch (IOException e) { listener.getLogger().println("Failed to apply macro " + macro); listener.error(ExceptionUtils.getStackTrace(e)); } return macro; } /** * Copy local policy file to slave in policies directory of ZAP default directory. * * @param workspace the workspace of the build * @param listener * @throws IOException * @throws InterruptedException */ private void copyPolicyFile(FilePath workspace, BuildListener listener) throws IOException, InterruptedException { //if(zaproxy.getScanURL() && zaproxy.pathToLocalPolicy != null && !zaproxy.pathToLocalPolicy.isEmpty()) // TODO a recup via un champ // File fileToCopy = new File(zaproxy.pathToLocalPolicy); File fileToCopy = new File("C:\\Users\\ludovic.roucoux\\OWASP ZAP\\policies\\OnlySQLInjection.policy"); String stringForLogger = "Copy [" + fileToCopy.getAbsolutePath() + "] to "; String data = FileUtils.readFileToString(fileToCopy, (String)null); stringForLogger = workspace.act(new CopyFileCallable(data, zaproxy.getZapDefaultDir(), fileToCopy.getName(), stringForLogger)); listener.getLogger().println(stringForLogger); } /** * Allows to copy local policy file to the default ZAP policies directory in slave. * * @author ludovic.roucoux * */ private static class CopyFileCallable implements FileCallable<String> { private static final long serialVersionUID = -3375349701206827354L; private String data; private String zapDefaultDir; private String copyFilename; private String stringForLogger; public CopyFileCallable(String data, String zapDefaultDir, String copyFilename, String stringForLogger) { this.data = data; this.zapDefaultDir = zapDefaultDir; this.copyFilename = copyFilename; this.stringForLogger = stringForLogger; } public String invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { File fileCopiedDir = new File(zapDefaultDir, ZAProxy.NAME_POLICIES_DIR_ZAP); File fileCopied = new File(fileCopiedDir, copyFilename); FileUtils.writeStringToFile(fileCopied, data); stringForLogger += "[" + fileCopied.getAbsolutePath() + "]"; return stringForLogger; } @Override public void checkRoles(RoleChecker checker) throws SecurityException { // Nothing to do } } /** * Descriptor for {@link ZAProxyBuilder}. Used as a singleton. * The class is marked as public so that it can be accessed from views. * * <p> * See <tt>src/main/resources/fr/novia/zaproxyplugin/ZAProxyBuilder/*.jelly</tt> * for the actual HTML fragment for the configuration screen. */ @Extension // This indicates to Jenkins this is an implementation of an extension point. public static final ZAProxyBuilderDescriptorImpl DESCRIPTOR = new ZAProxyBuilderDescriptorImpl(); public static final class ZAProxyBuilderDescriptorImpl extends BuildStepDescriptor<Builder> { /** * 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>. */ private String zapProxyDefaultHost; private int zapProxyDefaultPort; private String jiraBaseURL; private String jiraUserName; private String jiraPassword; /** * In order to load the persisted global configuration, you have to * call load() in the constructor. */ public ZAProxyBuilderDescriptorImpl() { load(); } @Override 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. */ @Override public String getDisplayName() { return "Execute ZAProxy"; } @Override public boolean configure(StaplerRequest req, JSONObject formData) throws FormException { // To persist global configuration information, // set that to properties and call save(). zapProxyDefaultHost = formData.getString("zapProxyDefaultHost"); zapProxyDefaultPort = formData.getInt("zapProxyDefaultPort"); //set the values from the global configuration for CREATE JIRA ISSUES jiraBaseURL=formData.getString("jiraBaseURL"); jiraUserName=formData.getString("jiraUserName"); jiraPassword=formData.getString("jiraPassword"); // ^Can also use req.bindJSON(this, formData); // (easier when there are many fields; need set* methods for this, like setUseFrench) save(); return super.configure(req,formData); } public String getZapProxyDefaultHost() { return zapProxyDefaultHost; } public int getZapProxyDefaultPort() { return zapProxyDefaultPort; } public String getJiraBaseURL(){return jiraBaseURL;} public String getJiraUserName(){return jiraUserName;} public String getJiraPassword(){return jiraPassword;} } /** * Used to execute ZAP remotely. * * @author ludovic.roucoux * */ private static class ZAProxyCallable implements FileCallable<Boolean> { private static final long serialVersionUID = -313398999885177679L; private ZAProxy zaproxy; private BuildListener listener; public ZAProxyCallable(ZAProxy zaproxy, BuildListener listener) { this.zaproxy = zaproxy; this.listener = listener; } @Override public Boolean invoke(File f, VirtualChannel channel) { return zaproxy.executeZAP(new FilePath(f), listener); } @Override public void checkRoles(RoleChecker checker) throws SecurityException { // Nothing to do } } }