package org.sif.launcher.jenkins; import hudson.EnvVars; import hudson.Extension; import hudson.FilePath; import hudson.Launcher; import hudson.model.BuildListener; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.Result; import hudson.tasks.BuildStepDescriptor; import hudson.tasks.Builder; import hudson.util.FormValidation; import hudson.util.ListBoxModel; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; import javax.servlet.ServletException; import net.sf.json.JSONObject; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; import org.owasp.esapi.EncryptedProperties; import org.owasp.esapi.errors.EncryptionException; import org.owasp.esapi.reference.crypto.DefaultEncryptedProperties; import org.owasp.esapi.reference.crypto.ReferenceEncryptedProperties; import org.sif.Job; import org.sif.Publisher; import org.sif.PublisherParameters; import org.sif.Sensor; import org.sif.SensorIntegrationFramework; import org.sif.Notifier; import org.sif.SensorParameters; import org.sif.core.concurrency.NativeExecutionException; /** * A Jenkins plugin to interact with the IBM AppScan Source tool. The functions * of the tool that are exposed to the Jenkins user currently include: sense * publish * * <p> * When the user configures the project and enables this builder, * {@link DescriptorImpl#newInstance(StaplerRequest)} is invoked and a new * {@link JenkinsPlugin} is created. The created instance is persisted to the * project configuration XML by using XStream, so this allows you to use * instance fields (like {@link #name}) to remember the configuration. * * <p> * When a build is performed, the * {@link #perform(AbstractBuild, Launcher, BuildListener)} method will be * invoked. * */ public class JenkinsPlugin extends Builder { private String applicationName; private String notificationRecipients; private String aseUrl; private String aseDomain; private String aseUsername; private String asePassword; private String asseUrl; private String asseUsername; private String assePassword; private String scanConfiguration; private boolean disableScan; private boolean disablePublish; private boolean disableAll; private String sensorClass; private String publisherClass; private String notifierClass; final SensorIntegrationFramework sifFramework; Map<String, String> parameters = new HashMap<String, String>(); // Fields in config.jelly must match the parameter names in the // "DataBoundConstructor" @DataBoundConstructor public JenkinsPlugin( String sensorClass, String publisherClass, String notifierClass, String notificationRecipients, String scanConfiguration, boolean disableScan, boolean disablePublish, boolean disableAll) { sifFramework = getDescriptor().sifFramework; this.sensorClass = sensorClass; this.publisherClass = publisherClass; this.notifierClass = notifierClass; this.notificationRecipients = notificationRecipients; this.scanConfiguration = scanConfiguration; this.disableScan = disableScan; this.disablePublish = disablePublish; this.disableAll = disableAll; } public String getSensorClass() { return sensorClass; } public String getPublisherClass() { return publisherClass; } public String getNotifierClass() { return notifierClass; } public String getNotificationRecipients() { return notificationRecipients; } public String getScanConfiguration() { return scanConfiguration; } @Override public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) { boolean success = false; Job job = new Job(); applicationName = build.getParent().getName(); listener.getLogger().println( "Application name is " + applicationName ); // All console output in this process will now be directed to the Jenkins console log. System.setOut( listener.getLogger() ); String sensorClass = getSensorClass(); String publisherClass = getPublisherClass(); String notifierClass = getPublisherClass(); Notifier notifier = sifFramework.getNotifier( notifierClass ); Sensor sensor = sifFramework.getSensor( sensorClass ); Publisher publisher = sifFramework.getPublisher( publisherClass ); try { if ( !disableScan ) { // FIXME: There should be a way for the sensor integrator to specify // all parameters needed by each implemented 'command'. // We only want to send the necessary parameters to the command // handler. Also, parsing and validation should be // common to the CLI and to the JenkinsPlugin. parameters.put("scanWorkspace.dir", build.getWorkspace().getRemote()); parameters.put("sif.dir", getSifHome()); parameters.put("asse.dir", getAppScanSourceHome()); parameters.put("scan.config", getScanConfiguration()); parameters.put("notificationRecipients", getNotificationRecipients()); parameters.put("asse.url", getDescriptor().getAsseHostname()); parameters.put("asse.username", getDescriptor().getAsseUsername()); parameters.put("asse.password", getDescriptor().getAssePassword()); parameters.put("application", applicationName); sensor.sense( job, notifier, parameters ); } // if ( !disableReport ) // { // reporter.report(job, notifier, reporterParameters); // } if ( !disablePublish ) { parameters.put("scanWorkspace.dir", build.getWorkspace().getRemote()); parameters.put("ase.target.dir", translateWorkspaceDirectoryToTargetFolder(build.getWorkspace().getRemote())); parameters.put("sif.dir", getSifHome()); parameters.put("asse.dir", getAppScanSourceHome()); parameters.put("scan.config", getScanConfiguration()); parameters.put("notificationRecipients", getNotificationRecipients()); parameters.put("asse.url", getDescriptor().getAsseHostname()); parameters.put("asse.username", getDescriptor().getAsseUsername()); parameters.put("asse.password", getDescriptor().getAssePassword()); parameters.put("ase.url", getDescriptor().getAseHostname()); parameters.put("ase.domain", getDescriptor().getAseDomain()); parameters.put("ase.username", getDescriptor().getAseUsername()); parameters.put("ase.password", getDescriptor().getAsePassword()); publisher.publish( job, notifier, parameters ); } success = true; // FIXME: Still some design work to do on Notifier. notifier.notifySuccess( "Scan job complete" ); } catch (NativeExecutionException e) { handlePerformException( job, notifier, listener, e, "Failure executing command, with return code " + e.getReturnCode() + ": " + e.getMessage() ); } catch (ExecutionException e) { handlePerformException( job, notifier, listener, e, "Failure executing command: " + e.getMessage() ); } catch (InterruptedException e) { handlePerformException( job, notifier, listener, e, "Job interrupted by the user: " + e.getMessage() ); } catch (TimeoutException e) { handlePerformException( job, notifier, listener, e, "Executed process timed out: " + e.getMessage() ); } catch (Exception e) { handlePerformException( job, notifier, listener, e, "Unexpected exception: " + e.getMessage() ); } File assessmentsDirectory = new File( build.getWorkspace().getRemote(), "assessments" ); assessmentsDirectory.mkdir(); File assessmentFile = new File( assessmentsDirectory, "assessment.ozasmt" ); File reportsDirectory = new File( build.getWorkspace().getRemote(), "reports" ); reportsDirectory.mkdir(); File asseTempDirectory = new File( build.getWorkspace().getRemote(), "temp" ); asseTempDirectory.mkdir(); // FIXME: We would like to have a summary of the scan results in this object for notifications. ScanResult result = new ScanResult( success, assessmentFile ); String errorsContent = result.getErrorSummary(); if ( errorsContent != null ) { File file = new File( asseTempDirectory, "errors.log" ); listener.getLogger().println( "Writing scan errors to " + file ); FileWriter writer = null; try { writer = new FileWriter( file ); writer.write( errorsContent ); writer.flush(); } catch (IOException e) { if ( writer != null ) { try { writer.close(); } catch (IOException ee) { } } listener.getLogger().println( "Unexpected exception: " + e.getMessage() ); e.printStackTrace( listener.getLogger() ); } } if ( success ) build.setResult( Result.SUCCESS ); else build.setResult( Result.FAILURE ); return success; } /** * Get the directory path of the parent directory of the given directory path, relative to that given * directory path. * * So, * * in: /home/scanner/scan_root/jobs/OrganizationA/Project1/workspace * out: OrganizationA/Project1 * * @param workspaceDirectoryPath * @return */ private String translateWorkspaceDirectoryToTargetFolder(String workspaceDirectoryPath) { File workspaceDirectory = new File(workspaceDirectoryPath); File jobDirectory = workspaceDirectory.getParentFile(); File jobsDirectory = jobDirectory.getParentFile(); String relativeTargetFolder = jobsDirectory.toURI().relativize( jobDirectory.toURI() ).getPath(); return relativeTargetFolder; } private void handlePerformException(Job job, Notifier notifier, BuildListener listener, Exception e, String message) { try { notifier.notifyFailure( "Scan job failed", e ); } catch (Exception e1) { listener.getLogger().println( "Could not notify" ); } listener.getLogger().println( message ); e.printStackTrace( listener.getLogger() ); } // 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(); } public boolean getDisableAll() { return disableAll; } public boolean getDisablePublish() { return disablePublish; } public boolean getDisableScan() { return disableScan; } // public String getPafFile() // { // return applicationName + ".paf"; // } public String getAseHostname() { return getDescriptor().getAseHostname(); } public String getAseUsername() { return getDescriptor().getAseUsername(); } public String getAsePassword() { return getDescriptor().getAsePassword(); } public String getAsseUsername() { return getDescriptor().getAsseUsername(); } public String getAssePassword() { return getDescriptor().getAssePassword(); } public String getAsseHostname() { return getDescriptor().getAsseHostname(); } public String getAseDomain() { return getDescriptor().getAseDomain(); } public String getAppScanSourceHome() { return getDescriptor().getAppScanSourceHome(); } public String getSifHome() { return getDescriptor().getSifHome(); } /** * Descriptor for {@link JenkinsPlugin}. Used as a singleton. The class is * marked as public so that it can be accessed from views. * * <p> * See * <tt>src/main/resources/hudson/plugins/hello_world/HelloWorldBuilder/*.jelly</tt> * for the actual HTML fragment for the configuration screen. */ @Extension // This indicates to Jenkins that this is an implementation of an extension // point. public static final class DescriptorImpl 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 appScanSourceHome; private String sifHome; private String processTimeout; private String reportTemplateFile; private String exclusionFilterPropertiesFile; private String asseHostname; private String asseUsername; private String assePassword; private String aseHostname; private String aseDomain; private String aseUsername; private String asePassword; SensorIntegrationFramework sifFramework = null; public DescriptorImpl() { load(); } // /** // * Performs on-the-fly validation of the form field 'name'. // * // * @param value // * This parameter receives the value that the user has typed. // * @return Indicates the outcome of the validation. This is sent to the // * browser. // */ // public FormValidation doCheckName(@QueryParameter String value) throws IOException, ServletException // { // if ( value.length() == 0 ) // return FormValidation.error( "Please set a name" ); // if ( value.length() < 4 ) // return FormValidation.warning( "Isn't the name too short?" ); // return FormValidation.ok(); // } 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 "Sensor Integration Framework Jenkins Plugin"; } @Override public boolean configure(StaplerRequest req, JSONObject formData) throws FormException { // To persist global configuration information, // set that to properties and call save(). sifHome = formData.getString( "sifHome" ); sifFramework = new SensorIntegrationFramework(getSifHome()); appScanSourceHome = formData.getString( "appScanSourceHome" ); asseHostname = formData.getString( "asseHostname" ); asseUsername = formData.getString( "asseUsername" ); assePassword = formData.getString( "assePassword" ); aseHostname = formData.getString( "aseHostname" ); aseDomain = formData.getString( "aseDomain" ); aseUsername = formData.getString( "aseUsername" ); asePassword = formData.getString( "asePassword" ); // ^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 ); } /** * @return the appScanSourceHome */ public String getAppScanSourceHome() { return appScanSourceHome; } public String getSifHome() { return sifHome; } public String getProcessTimeout() { return processTimeout; } /** * @return the asseHostname */ public String getAsseHostname() { return asseHostname; } public String getAsseUsername() { return asseUsername; } public String getAssePassword() { return assePassword; } public String getAseHostname() { return aseHostname; } public String getAseDomain() { return aseDomain; } public String getAseUsername() { return aseUsername; } public String getAsePassword() { return asePassword; } public void setAppScanSourceHome(String appScanSourceHome) { this.appScanSourceHome = appScanSourceHome; } public void setSifHome(String sifHome) { this.sifHome = sifHome; } public void setAsseHostname(String asseHostname) { this.asseHostname = asseHostname; } public void setAsseUsername(String asseUsername) { this.asseUsername = asseUsername; } public void setAssePassword(String assePassword) { this.assePassword = assePassword; } public void setAseHostname(String aseHostname) { this.aseHostname = aseHostname; } public void setAseDomain(String aseDomain) { this.aseDomain = aseDomain; } public void setAseUsername(String aseUsername) { this.aseUsername = aseUsername; } public void setAsePassword(String asePassword) { this.asePassword = asePassword; } public List<String> getAllSensorClasses() { return sifFramework.getSensorClasses(); } public ListBoxModel doFillSensorClassItems() { ListBoxModel model = new ListBoxModel(); for (String sensorClass : getAllSensorClasses()) { // FIXME: Make the display name more user-friendly String displayName = sensorClass; model.add(displayName, sensorClass); } // model.get( 1 ).selected = true; return model; } public List<String> getAllPublisherClasses() { return sifFramework.getPublisherClasses(); } public ListBoxModel doFillPublisherClassItems() { ListBoxModel model = new ListBoxModel(); for (String publisherClass : getAllPublisherClasses()) { // FIXME: Make the display name more user-friendly String displayName = publisherClass; model.add(displayName, publisherClass); } // model.get( 1 ).selected = true; return model; } public List<String> getAllNotifierClasses() { return sifFramework.getNotifierClasses(); } public ListBoxModel doFillNotifierClassItems() { ListBoxModel model = new ListBoxModel(); for (String notifierClass : getAllNotifierClasses()) { // FIXME: Make the display name more user-friendly String displayName = notifierClass; model.add(displayName, notifierClass); } // model.get( 1 ).selected = true; return model; } } }