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;
}
}
}