Java tutorial
package com.checkmarx.jenkins; import com.checkmarx.jenkins.opensourceanalysis.DependencyFolder; import com.checkmarx.jenkins.opensourceanalysis.OpenSourceAnalyzerService; import com.checkmarx.jenkins.web.client.RestClient; import com.checkmarx.jenkins.web.model.AuthenticationRequest; import hudson.AbortException; import hudson.EnvVars; import hudson.Extension; import hudson.FilePath; import hudson.Launcher; import hudson.model.*; import hudson.tasks.BuildStepDescriptor; import hudson.tasks.Builder; import hudson.triggers.SCMTrigger; import hudson.util.ComboBoxModel; import hudson.util.FormValidation; import hudson.util.Secret; import hudson.util.ListBoxModel; import java.io.File; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URLDecoder; import java.util.LinkedList; import java.util.List; import java.util.regex.Pattern; import net.sf.json.JSONObject; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; import org.apache.log4j.BasicConfigurator; import org.apache.log4j.FileAppender; import org.apache.log4j.Level; import org.apache.log4j.Logger; import org.apache.log4j.PatternLayout; import org.apache.log4j.WriterAppender; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; import com.checkmarx.components.zipper.Zipper; import com.checkmarx.ws.CxJenkinsWebService.CliScanArgs; import com.checkmarx.ws.CxJenkinsWebService.ConfigurationSet; import com.checkmarx.ws.CxJenkinsWebService.CxWSBasicRepsonse; import com.checkmarx.ws.CxJenkinsWebService.CxWSCreateReportResponse; import com.checkmarx.ws.CxJenkinsWebService.CxWSReportType; import com.checkmarx.ws.CxJenkinsWebService.CxWSResponseRunID; import com.checkmarx.ws.CxJenkinsWebService.Group; import com.checkmarx.ws.CxJenkinsWebService.LocalCodeContainer; import com.checkmarx.ws.CxJenkinsWebService.Preset; import com.checkmarx.ws.CxJenkinsWebService.ProjectDisplayData; import com.checkmarx.ws.CxJenkinsWebService.ProjectSettings; import com.checkmarx.ws.CxJenkinsWebService.SourceCodeSettings; import com.checkmarx.ws.CxJenkinsWebService.SourceLocationType; import com.thoughtworks.xstream.annotations.XStreamOmitField; import javax.xml.ws.WebServiceException; /** * The main entry point for Checkmarx plugin. This class implements the Builder * build stage that scans the source code. * * @author Denis Krivitski * @since 3/10/13 */ public class CxScanBuilder extends Builder { ////////////////////////////////////////////////////////////////////////////////////// // Persistent plugin configuration parameters ////////////////////////////////////////////////////////////////////////////////////// private boolean useOwnServerCredentials; @Nullable private String serverUrl; @Nullable private String username; @Nullable private String password; @Nullable private String projectName; @Nullable private String groupId; @Nullable private long projectId; @Nullable private String preset; private boolean presetSpecified; @Nullable private String excludeFolders; @Nullable private String filterPattern; private boolean incremental; private boolean fullScansScheduled; private int fullScanCycle; private boolean isThisBuildIncremental; @Nullable private String sourceEncoding; @Nullable private String comment; private boolean skipSCMTriggers; private boolean waitForResultsEnabled; private boolean vulnerabilityThresholdEnabled; private int highThreshold; private int mediumThreshold; private int lowThreshold; private boolean generatePdfReport; @Nullable private String includeOpenSourceFolders; @Nullable private String excludeOpenSourceFolders; ////////////////////////////////////////////////////////////////////////////////////// // Private variables ////////////////////////////////////////////////////////////////////////////////////// static { BasicConfigurator.configure(); // Set the log4j system to log to console } // Kept for backward compatibility with old serialized plugin configuration. private static transient Logger staticLogger; private static final transient Logger LOGGER = Logger.getLogger(CxScanBuilder.class); @XStreamOmitField private transient Logger instanceLogger = LOGGER; // Instance logger redirects to static logger until // it is initialized in perform method @XStreamOmitField private transient FileAppender fileAppender; private JobStatusOnError jobStatusOnError; private String thresholdSettings; private Result vulnerabilityThresholdResult; ////////////////////////////////////////////////////////////////////////////////////// // Constructors ////////////////////////////////////////////////////////////////////////////////////// @DataBoundConstructor public CxScanBuilder(boolean useOwnServerCredentials, // NOSONAR @Nullable String serverUrl, @Nullable String username, @Nullable String password, String projectName, long projectId, String buildStep, @Nullable String groupId, @Nullable String preset, JobStatusOnError jobStatusOnError, boolean presetSpecified, @Nullable String excludeFolders, @Nullable String filterPattern, boolean incremental, boolean fullScansScheduled, int fullScanCycle, @Nullable String sourceEncoding, @Nullable String comment, boolean skipSCMTriggers, boolean waitForResultsEnabled, boolean vulnerabilityThresholdEnabled, int highThreshold, int mediumThreshold, int lowThreshold, boolean generatePdfReport, String thresholdSettings, Result vulnerabilityThresholdResult, @Nullable String includeOpenSourceFolders, @Nullable String excludeOpenSourceFolders) { this.useOwnServerCredentials = useOwnServerCredentials; this.serverUrl = serverUrl; this.username = username; this.password = Secret.fromString(password).getEncryptedValue(); // Workaround for compatibility with Conditional BuildStep Plugin this.projectName = (projectName == null) ? buildStep : projectName; this.projectId = projectId; this.groupId = groupId; this.preset = preset; this.jobStatusOnError = jobStatusOnError; this.presetSpecified = presetSpecified; this.excludeFolders = excludeFolders; this.filterPattern = filterPattern; this.incremental = incremental; this.fullScansScheduled = fullScansScheduled; this.fullScanCycle = fullScanCycle; this.sourceEncoding = sourceEncoding; this.comment = comment; this.skipSCMTriggers = skipSCMTriggers; this.waitForResultsEnabled = waitForResultsEnabled; this.vulnerabilityThresholdEnabled = vulnerabilityThresholdEnabled; this.highThreshold = highThreshold; this.mediumThreshold = mediumThreshold; this.lowThreshold = lowThreshold; this.generatePdfReport = generatePdfReport; this.includeOpenSourceFolders = includeOpenSourceFolders; this.excludeOpenSourceFolders = excludeOpenSourceFolders; this.thresholdSettings = thresholdSettings; this.vulnerabilityThresholdResult = vulnerabilityThresholdResult; init(); } private void init() { updateJobOnGlobalConfigChange(); } private void updateJobOnGlobalConfigChange() { if (!getDescriptor().isForcingVulnerabilityThresholdEnabled() && shouldUseGlobalThreshold()) { vulnerabilityThresholdEnabled = false; } } ////////////////////////////////////////////////////////////////////////////////////// // Configuration fields getters ////////////////////////////////////////////////////////////////////////////////////// public boolean isUseOwnServerCredentials() { return useOwnServerCredentials; } @Nullable public String getServerUrl() { return serverUrl; } @Nullable public String getUsername() { return username; } @Nullable public String getPassword() { return Secret.fromString(password).getPlainText(); } @Nullable public String getProjectName() { return projectName; } // Workaround for compatibility with Conditional BuildStep Plugin @Nullable public String getBuildStep() { return projectName; } @Nullable public String getGroupId() { return groupId; } @Nullable public String getPreset() { return preset; } public boolean isPresetSpecified() { return presetSpecified; } @Nullable public String getExcludeFolders() { return excludeFolders; } @Nullable public String getFilterPattern() { return filterPattern; } public boolean isIncremental() { return incremental; } public boolean isFullScansScheduled() { return fullScansScheduled; } public int getFullScanCycle() { return fullScanCycle; } @Nullable public String getSourceEncoding() { return sourceEncoding; } @Nullable public String getComment() { return comment; } public JobStatusOnError getJobStatusOnError() { return (null == jobStatusOnError) ? JobStatusOnError.GLOBAL : jobStatusOnError; } public boolean isSkipSCMTriggers() { return skipSCMTriggers; } public boolean isWaitForResultsEnabled() { return waitForResultsEnabled; } public boolean isVulnerabilityThresholdEnabled() { updateJobOnGlobalConfigChange(); return vulnerabilityThresholdEnabled; } public int getHighThreshold() { return highThreshold; } public int getMediumThreshold() { return mediumThreshold; } public int getLowThreshold() { return lowThreshold; } @Nullable public String getExcludeOpenSourceFolders() { return excludeOpenSourceFolders; } @Nullable public String getIncludeOpenSourceFolders() { return includeOpenSourceFolders; } public boolean isGeneratePdfReport() { return generatePdfReport; } public void setThresholdSettings(String thresholdSettings) { this.thresholdSettings = thresholdSettings; } public String getThresholdSettings() { return thresholdSettings; } public void setVulnerabilityThresholdResult(Result result) { this.vulnerabilityThresholdResult = result; } public Result getVulnerabilityThresholdResult() { return vulnerabilityThresholdResult; } @Override public boolean perform(final AbstractBuild<?, ?> build, final Launcher launcher, final BuildListener listener) throws InterruptedException, IOException { final DescriptorImpl descriptor = getDescriptor(); CxWSResponseRunID cxWSResponseRunID = null; CxWebService cxWebService = null; CxWSCreateReportResponse reportResponse = null; try { File checkmarxBuildDir = new File(build.getRootDir(), "checkmarx"); checkmarxBuildDir.mkdir(); initLogger(checkmarxBuildDir, listener, instanceLoggerSuffix(build)); instanceLogger.info("Checkmarx Jenkins plugin version: " + CxConfig.version()); if (isSkipScan(build)) { instanceLogger.info("Checkmarx scan skipped since the build was triggered by SCM. " + "Visit plugin configuration page to disable this skip."); return true; } final String serverUrlToUse = isUseOwnServerCredentials() ? getServerUrl() : descriptor.getServerUrl(); final String usernameToUse = isUseOwnServerCredentials() ? getUsername() : descriptor.getUsername(); final String passwordToUse = isUseOwnServerCredentials() ? getPassword() : descriptor.getPassword(); String serverUrlToUseNotNull = serverUrlToUse != null ? serverUrlToUse : ""; cxWebService = new CxWebService(serverUrlToUseNotNull, instanceLoggerSuffix(build)); cxWebService.login(usernameToUse, passwordToUse); instanceLogger.info("Checkmarx server login successful"); cxWSResponseRunID = submitScan(build, cxWebService, listener); instanceLogger.info("\nScan job submitted successfully\n"); if (!isWaitForResultsEnabled() && !(descriptor.isForcingVulnerabilityThresholdEnabled() && descriptor.isLockVulnerabilitySettings())) { analyzeOpenSources(build, serverUrlToUseNotNull, usernameToUse, passwordToUse, projectId, cxWebService); return true; } long scanId = cxWebService.trackScanProgress(cxWSResponseRunID, usernameToUse, passwordToUse, descriptor.getScanTimeOutEnabled(), descriptor.getScanTimeoutDuration()); if (scanId == 0) { build.setResult(Result.UNSTABLE); return true; } reportResponse = cxWebService.generateScanReport(scanId, CxWSReportType.XML); File xmlReportFile = new File(checkmarxBuildDir, "ScanReport.xml"); cxWebService.retrieveScanReport(reportResponse.getID(), xmlReportFile, CxWSReportType.XML); if (generatePdfReport) { reportResponse = cxWebService.generateScanReport(scanId, CxWSReportType.PDF); File pdfReportFile = new File(checkmarxBuildDir, CxScanResult.PDF_REPORT_NAME); cxWebService.retrieveScanReport(reportResponse.getID(), pdfReportFile, CxWSReportType.PDF); } // Parse scan report and present results in Jenkins CxScanResult cxScanResult = new CxScanResult(build, instanceLoggerSuffix(build), serverUrlToUse); cxScanResult.readScanXMLReport(xmlReportFile); build.addAction(cxScanResult); // Set scan results to environment EnvVarAction envVarAction = new EnvVarAction(); envVarAction.setCxSastResults(cxScanResult); build.addAction(envVarAction); ThresholdConfig thresholdConfig = createThresholdConfig(); if ((descriptor.isForcingVulnerabilityThresholdEnabled() && descriptor.isLockVulnerabilitySettings() || isVulnerabilityThresholdEnabled()) && isThresholdCrossed(thresholdConfig, cxScanResult)) { build.setResult(thresholdConfig.getBuildStatus()); return true; } analyzeOpenSources(build, serverUrlToUseNotNull, usernameToUse, passwordToUse, projectId, cxWebService); return true; } catch (IOException | WebServiceException e) { if (useUnstableOnError(descriptor)) { build.setResult(Result.UNSTABLE); instanceLogger.error(e.getMessage(), e); return true; } else { throw e; } } catch (InterruptedException e) { if (reportResponse != null) { instanceLogger.error("Cancelling report generation on the Checkmarx server..."); cxWebService.cancelScanReport(reportResponse.getID()); } else if (cxWSResponseRunID != null) { instanceLogger.error("Cancelling scan on the Checkmarx server..."); cxWebService.cancelScan(cxWSResponseRunID.getRunId()); } throw e; } finally { closeLogger(); } } private void analyzeOpenSources(AbstractBuild<?, ?> build, String baseUri, String user, String password, long projectId, CxWebService webServiceClient) throws IOException, InterruptedException { DependencyFolder folders = new DependencyFolder(includeOpenSourceFolders, excludeOpenSourceFolders); AuthenticationRequest authReq = new AuthenticationRequest(user, password); try (RestClient restClient = new RestClient(baseUri, authReq)) { OpenSourceAnalyzerService osaService = new OpenSourceAnalyzerService(build, folders, restClient, projectId, instanceLogger, webServiceClient); osaService.analyze(); } } private ThresholdConfig createThresholdConfig() { ThresholdConfig config = new ThresholdConfig(); if (shouldUseGlobalThreshold()) { final DescriptorImpl descriptor = getDescriptor(); config.setHighSeverity(descriptor.getHighThresholdEnforcement()); config.setMediumSeverity(descriptor.getMediumThresholdEnforcement()); config.setLowSeverity(descriptor.getLowThresholdEnforcement()); config.setBuildStatus(Result.fromString(descriptor.getJobGlobalStatusOnThresholdViolation().name())); } else { config.setHighSeverity(getHighThreshold()); config.setMediumSeverity(getMediumThreshold()); config.setLowSeverity(getLowThreshold()); config.setBuildStatus(getVulnerabilityThresholdResult()); } return config; } private boolean shouldUseGlobalThreshold() { final DescriptorImpl descriptor = getDescriptor(); return descriptor.isForcingVulnerabilityThresholdEnabled() && descriptor.isLockVulnerabilitySettings() || "global".equals(getThresholdSettings()); } /** * Checks if job should fail with <code>UNSTABLE</code> status instead of <code>FAILED</code> * * @param descriptor * Descriptor of the current build step * @return if job should fail with <code>UNSTABLE</code> status instead of <code>FAILED</code> */ private boolean useUnstableOnError(final DescriptorImpl descriptor) { return JobStatusOnError.UNSTABLE.equals(getJobStatusOnError()) || (JobStatusOnError.GLOBAL.equals(getJobStatusOnError()) && JobGlobalStatusOnError.UNSTABLE.equals(descriptor.getJobGlobalStatusOnError())); } private boolean isThresholdCrossed(ThresholdConfig thresholdConfig, @NotNull final CxScanResult cxScanResult) { logFoundVulnerabilities("high", cxScanResult.getHighCount(), thresholdConfig.getHighSeverity()); logFoundVulnerabilities("medium", cxScanResult.getMediumCount(), thresholdConfig.getMediumSeverity()); logFoundVulnerabilities("low", cxScanResult.getLowCount(), thresholdConfig.getLowSeverity()); return cxScanResult.getHighCount() > thresholdConfig.getHighSeverity() || cxScanResult.getMediumCount() > thresholdConfig.getMediumSeverity() || cxScanResult.getLowCount() > thresholdConfig.getLowSeverity(); } private void logFoundVulnerabilities(String severity, int actualNumber, int configuredHighThreshold) { instanceLogger.info("Number of " + severity + " severity vulnerabilities: " + actualNumber + " stability threshold: " + configuredHighThreshold); } private String instanceLoggerSuffix(final AbstractBuild<?, ?> build) { return build.getProject().getDisplayName() + "-" + build.getDisplayName(); } private void initLogger(final File checkmarxBuildDir, final BuildListener listener, final String loggerSuffix) { instanceLogger = CxLogUtils.loggerWithSuffix(getClass(), loggerSuffix); final WriterAppender writerAppender = new WriterAppender(new PatternLayout("%m%n"), listener.getLogger()); writerAppender.setThreshold(Level.INFO); final Logger parentLogger = CxLogUtils.parentLoggerWithSuffix(loggerSuffix); parentLogger.addAppender(writerAppender); String logFileName = checkmarxBuildDir.getAbsolutePath() + File.separator + "checkmarx.log"; try { fileAppender = new FileAppender(new PatternLayout("%C: [%d] %-5p: %m%n"), logFileName); fileAppender.setThreshold(Level.DEBUG); parentLogger.addAppender(fileAppender); } catch (IOException e) { LOGGER.warn("Could not open log file for writing: " + logFileName); LOGGER.debug(e); } } private void closeLogger() { instanceLogger.removeAppender(fileAppender); fileAppender.close(); instanceLogger = LOGGER; // Redirect all logs back to static logger } private CxWSResponseRunID submitScan(final AbstractBuild<?, ?> build, final CxWebService cxWebService, final BuildListener listener) throws IOException { isThisBuildIncremental = isThisBuildIncremental(build.getNumber()); if (isThisBuildIncremental) { instanceLogger.info("\nScan job started in incremental scan mode\n"); } else { instanceLogger.info("\nScan job started in full scan mode\n"); } instanceLogger.info("Started zipping the workspace"); try { // hudson.FilePath will work in distributed Jenkins installation FilePath baseDir = build.getWorkspace(); if (baseDir == null) { throw new AbortException( "Checkmarx Scan Failed: cannot acquire Jenkins workspace location. It can be due to workspace residing on a disconnected slave."); } EnvVars env = build.getEnvironment(listener); String combinedFilterPattern = env.expand(getFilterPattern()) + "," + processExcludeFolders(env.expand(getExcludeFolders())); // Implementation of FilePath.FileCallable allows extracting a java.io.File from // hudson.FilePath and still working with it in remote mode CxZipperCallable zipperCallable = new CxZipperCallable(combinedFilterPattern); final CxZipResult zipResult = baseDir.act(zipperCallable); instanceLogger.info(zipResult.getLogMessage()); final FilePath tempFile = zipResult.getTempFile(); final int numOfZippedFiles = zipResult.getNumOfZippedFiles(); instanceLogger.info("Zipping complete with " + numOfZippedFiles + " files, total compressed size: " + FileUtils.byteCountToDisplaySize(tempFile.length() / 8 * 6)); // We print here the size of compressed sources before encoding to base 64 instanceLogger.info("Temporary file with zipped and base64 encoded sources was created at: " + tempFile.getRemote()); listener.getLogger().flush(); // Create cliScanArgs object with dummy byte array for zippedFile field // Streaming scan web service will nullify zippedFile filed and use tempFile // instead final CliScanArgs cliScanArgs = createCliScanArgs(new byte[] {}, env); // Check if the project already exists final CxWSBasicRepsonse validateProjectRespnse = cxWebService.validateProjectName(projectName, groupId); CxWSResponseRunID cxWSResponseRunID; if (validateProjectRespnse.isIsSuccesfull()) { cxWSResponseRunID = cxWebService.createAndRunProject(cliScanArgs.getPrjSettings(), cliScanArgs.getSrcCodeSettings().getPackagedCode(), true, true, tempFile); } else { if (projectId == 0) { projectId = cxWebService.getProjectId(cliScanArgs.getPrjSettings()); } cliScanArgs.getPrjSettings().setProjectID(projectId); if (incremental) { cxWSResponseRunID = cxWebService.runIncrementalScan(cliScanArgs.getPrjSettings(), cliScanArgs.getSrcCodeSettings().getPackagedCode(), true, true, tempFile); } else { cxWSResponseRunID = cxWebService.runScanAndAddToProject(cliScanArgs.getPrjSettings(), cliScanArgs.getSrcCodeSettings().getPackagedCode(), true, true, tempFile); } } tempFile.delete(); instanceLogger.info("Temporary file deleted"); return cxWSResponseRunID; } catch (Zipper.MaxZipSizeReached e) { throw new AbortException("Checkmarx Scan Failed: Reached maximum upload size limit of " + FileUtils.byteCountToDisplaySize(CxConfig.maxZipSize())); } catch (Zipper.NoFilesToZip e) { throw new AbortException("Checkmarx Scan Failed: No files to scan"); } catch (InterruptedException e) { throw new AbortException("Remote operation failed on slave node: " + e.getMessage()); } } @NotNull private String processExcludeFolders(String excludeFolders) { if (excludeFolders == null) { return ""; } StringBuilder result = new StringBuilder(); String[] patterns = StringUtils.split(excludeFolders, ",\n"); for (String p : patterns) { p = p.trim(); if (p.length() > 0) { result.append("!**/"); result.append(p); result.append("/**/*, "); } } instanceLogger.debug("Exclude folders converted to: " + result.toString()); return result.toString(); } private CliScanArgs createCliScanArgs(byte[] compressedSources, EnvVars env) { ProjectSettings projectSettings = new ProjectSettings(); long presetLong = 0; // Default value to use in case of exception try { presetLong = Long.parseLong(getPreset()); } catch (Exception e) { instanceLogger.error("Encountered illegal preset value: " + getPreset() + ". Using default preset."); } projectSettings.setPresetID(presetLong); projectSettings.setProjectName(env.expand(getProjectName())); projectSettings.setAssociatedGroupID(getGroupId()); long configuration = 0; // Default value to use in case of exception try { configuration = Long.parseLong(getSourceEncoding()); } catch (Exception e) { instanceLogger.error("Encountered illegal source encoding (configuration) value: " + getSourceEncoding() + ". Using default configuration."); } projectSettings.setScanConfigurationID(configuration); LocalCodeContainer localCodeContainer = new LocalCodeContainer(); localCodeContainer.setFileName("src.zip"); localCodeContainer.setZippedFile(compressedSources); SourceCodeSettings sourceCodeSettings = new SourceCodeSettings(); sourceCodeSettings.setSourceOrigin(SourceLocationType.LOCAL); sourceCodeSettings.setPackagedCode(localCodeContainer); String commentText = getComment() != null ? env.expand(getComment()) : ""; commentText = commentText.trim(); CliScanArgs args = new CliScanArgs(); args.setIsIncremental(isThisBuildIncremental); args.setIsPrivateScan(false); args.setPrjSettings(projectSettings); args.setSrcCodeSettings(sourceCodeSettings); args.setComment(commentText); return args; } private boolean isThisBuildIncremental(int buildNumber) { boolean askedForIncremental = isIncremental(); if (!askedForIncremental) { return false; } boolean askedForPeriodicFullScans = isFullScansScheduled(); if (!askedForPeriodicFullScans) { return true; } // if user entered invalid value for full scan cycle - all scans will be incremental if (fullScanCycle < DescriptorImpl.FULL_SCAN_CYCLE_MIN || fullScanCycle > DescriptorImpl.FULL_SCAN_CYCLE_MAX) { return true; } // If user asked to perform full scan after every 9 incremental scans - // it means that every 10th scan should be full, // that is the ordinal numbers of full scans will be "1", "11", "21" and so on... boolean shouldBeFullScan = buildNumber % (fullScanCycle + 1) == 1; return !shouldBeFullScan; } // Check what triggered this build, and in case the trigger was SCM // and the build is configured to skip those triggers, return true. private boolean isSkipScan(final AbstractBuild<?, ?> build) { if (!isSkipSCMTriggers()) { return false; } final List<Cause> causes = build.getCauses(); final List<Cause> allowedCauses = new LinkedList<>(); for (Cause c : causes) { if (!(c instanceof SCMTrigger.SCMTriggerCause)) { allowedCauses.add(c); } } return allowedCauses.isEmpty(); } ////////////////////////////////////////////////////////////////////////////////////////////// // // Descriptor class // ////////////////////////////////////////////////////////////////////////////////////////////// @Override public DescriptorImpl getDescriptor() { return (DescriptorImpl) super.getDescriptor(); } @Extension public static final class DescriptorImpl extends BuildStepDescriptor<Builder> { public static final String DEFAULT_FILTER_PATTERNS = CxConfig.defaultFilterPattern(); public static final int FULL_SCAN_CYCLE_MIN = 1; public static final int FULL_SCAN_CYCLE_MAX = 99; private static final Logger logger = Logger.getLogger(DescriptorImpl.class); ////////////////////////////////////////////////////////////////////////////////////// // Persistent plugin global configuration parameters ////////////////////////////////////////////////////////////////////////////////////// @Nullable private String serverUrl; @Nullable private String username; @Nullable private String password; private boolean hideResults; private boolean enableCertificateValidation; private boolean forcingVulnerabilityThresholdEnabled; private int highThresholdEnforcement; private int mediumThresholdEnforcement; private int lowThresholdEnforcement; private JobGlobalStatusOnError jobGlobalStatusOnError; private JobGlobalStatusOnError jobGlobalStatusOnThresholdViolation = JobGlobalStatusOnError.FAILURE; private boolean scanTimeOutEnabled; private int scanTimeoutDuration; private boolean lockVulnerabilitySettings = true; private final transient Pattern msGuid = Pattern .compile("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"); public DescriptorImpl() { load(); } @Nullable public String getServerUrl() { return serverUrl; } public void setServerUrl(@Nullable String serverUrl) { this.serverUrl = serverUrl; } @Nullable public String getUsername() { return username; } public void setUsername(@Nullable String username) { this.username = username; } @Nullable public String getPassword() { return Secret.fromString(password).getPlainText(); } public void setPassword(@Nullable String password) { this.password = Secret.fromString(password).getEncryptedValue(); } public boolean isHideResults() { return hideResults; } public void setHideResults(boolean hideResults) { this.hideResults = hideResults; } public boolean isEnableCertificateValidation() { return enableCertificateValidation; } public void setEnableCertificateValidation(final boolean enableCertificateValidation) { if (!this.enableCertificateValidation && enableCertificateValidation) { /* This condition in needed to re-enable immediately the verification of server certificates as the user changes the setting. This alleviates the requirement to restart the Jenkins server for configuration to take effect. */ CxSSLUtility.enableSSLCertificateVerification(); } this.enableCertificateValidation = enableCertificateValidation; } public boolean isForcingVulnerabilityThresholdEnabled() { return forcingVulnerabilityThresholdEnabled; } public void setForcingVulnerabilityThresholdEnabled(boolean forcingVulnerabilityThresholdEnabled) { this.forcingVulnerabilityThresholdEnabled = forcingVulnerabilityThresholdEnabled; } public int getHighThresholdEnforcement() { return highThresholdEnforcement; } public void setHighThresholdEnforcement(int highThresholdEnforcement) { this.highThresholdEnforcement = highThresholdEnforcement; } public int getMediumThresholdEnforcement() { return mediumThresholdEnforcement; } public void setMediumThresholdEnforcement(int mediumThresholdEnforcement) { this.mediumThresholdEnforcement = mediumThresholdEnforcement; } public int getLowThresholdEnforcement() { return lowThresholdEnforcement; } public void setLowThresholdEnforcement(int lowThresholdEnforcement) { this.lowThresholdEnforcement = lowThresholdEnforcement; } public boolean getScanTimeOutEnabled() { return scanTimeOutEnabled; } public void setScanTimeOutEnabled(boolean scanTimeOutEnabled) { this.scanTimeOutEnabled = scanTimeOutEnabled; } public int getScanTimeoutDuration() { if (scanTimeoutDuration < 1) { scanTimeoutDuration = 1; } return scanTimeoutDuration; } public void setScanTimeoutDuration(int scanTimeoutDuration) { if (scanTimeoutDuration >= 1) { this.scanTimeoutDuration = scanTimeoutDuration; } } @Override public boolean isApplicable(Class<? extends AbstractProject> aClass) { return true; } ////////////////////////////////////////////////////////////////////////////////////// // Helper methods for jelly views ////////////////////////////////////////////////////////////////////////////////////// // Provides a description string to be displayed near "Use default server credentials" // configuration option public String getCredentialsDescription() { if (getServerUrl() == null || getServerUrl().isEmpty() || getUsername() == null || getUsername().isEmpty()) { return "not set"; } return "Server URL: " + getServerUrl() + " username: " + getUsername(); } /* * Used to fill the value of hidden timestamp textbox, which in turn is used for Internet Explorer cache invalidation */ @NotNull public String getCurrentTime() { return String.valueOf(System.currentTimeMillis()); } ////////////////////////////////////////////////////////////////////////////////////// // Field value validators ////////////////////////////////////////////////////////////////////////////////////// /* * Note: This method is called concurrently by multiple threads, refrain from using mutable * shared state to avoid synchronization issues. */ public FormValidation doCheckIncludeOpenSourceFolders(@QueryParameter final boolean useOwnServerCredentials, @QueryParameter final String serverUrl, @QueryParameter final String password, @QueryParameter final String username, @QueryParameter final String includeOpenSourceFolders, @QueryParameter final String timestamp) { // timestamp is not used in code, it is one of the arguments to invalidate Internet Explorer cache CxWebService cxWebService = null; if (!osaConfigured(includeOpenSourceFolders)) { return FormValidation.ok(); } try { cxWebService = prepareLoggedInWebservice(useOwnServerCredentials, serverUrl, username, password); } catch (Exception e) { logger.debug(e); return FormValidation.ok(); } try { Boolean isOsaLicenseValid = cxWebService.isOsaLicenseValid(); if (!isOsaLicenseValid) { return FormValidation.error(OpenSourceAnalyzerService.NO_LICENSE_ERROR); } return FormValidation.ok(); } catch (Exception e) { return FormValidation.error(e.getMessage()); } } private boolean osaConfigured(String includeOpenSourceFolders) { return !org.apache.commons.lang.StringUtils.isEmpty(includeOpenSourceFolders); } /* * Note: This method is called concurrently by multiple threads, refrain from using mutable * shared state to avoid synchronization issues. */ public FormValidation doTestConnection(@QueryParameter final String serverUrl, @QueryParameter final String password, @QueryParameter final String username, @QueryParameter final String timestamp) { // timestamp is not used in code, it is one of the arguments to invalidate Internet Explorer cache CxWebService cxWebService = null; try { cxWebService = new CxWebService(serverUrl); } catch (Exception e) { logger.debug(e); return FormValidation.error("Invalid system URL"); } try { cxWebService.login(username, password); return FormValidation.ok("Success"); } catch (Exception e) { return FormValidation.error(e.getMessage()); } } // Prepares a this.cxWebService object to be connected and logged in /* * Note: This method is called concurrently by multiple threads, refrain from using mutable * shared state to avoid synchronization issues. */ private CxWebService prepareLoggedInWebservice(boolean useOwnServerCredentials, String serverUrl, String username, String password) throws AbortException, MalformedURLException { String serverUrlToUse = !useOwnServerCredentials ? serverUrl : getServerUrl(); String usernameToUse = !useOwnServerCredentials ? username : getUsername(); String passwordToUse = !useOwnServerCredentials ? password : getPassword(); logger.debug("prepareLoggedInWebservice: server: " + serverUrlToUse + " user: " + usernameToUse); CxWebService cxWebService = new CxWebService(serverUrlToUse); logger.debug("prepareLoggedInWebservice: created cxWebService"); cxWebService.login(usernameToUse, passwordToUse); logger.debug("prepareLoggedInWebservice: logged in"); return cxWebService; } /* * Note: This method is called concurrently by multiple threads, refrain from using mutable * shared state to avoid synchronization issues. */ public ComboBoxModel doFillProjectNameItems(@QueryParameter final boolean useOwnServerCredentials, @QueryParameter final String serverUrl, @QueryParameter final String username, @QueryParameter final String password, @QueryParameter final String timestamp) { // timestamp is not used in code, it is one of the arguments to invalidate Internet Explorer cache ComboBoxModel projectNames = new ComboBoxModel(); try { final CxWebService cxWebService = prepareLoggedInWebservice(useOwnServerCredentials, serverUrl, username, password); List<ProjectDisplayData> projectsDisplayData = cxWebService.getProjectsDisplayData(); for (ProjectDisplayData pd : projectsDisplayData) { projectNames.add(pd.getProjectName()); } logger.debug("Projects list: " + projectNames.size()); return projectNames; } catch (Exception e) { logger.debug("Projects list: empty"); return projectNames; // Return empty list of project names } } /* * Note: This method is called concurrently by multiple threads, refrain from using mutable * shared state to avoid synchronization issues. */ public FormValidation doCheckProjectName(@QueryParameter final String projectName, @QueryParameter final boolean useOwnServerCredentials, @QueryParameter final String serverUrl, @QueryParameter final String username, @QueryParameter final String password, @QueryParameter final String groupId, @QueryParameter final String timestamp) { // timestamp is not used in code, it is one of the arguments to invalidate Internet Explorer cache try { final CxWebService cxWebService = prepareLoggedInWebservice(useOwnServerCredentials, serverUrl, username, password); if (msGuid.matcher(groupId).matches()) { CxWSBasicRepsonse cxWSBasicRepsonse = cxWebService.validateProjectName(projectName, groupId); if (cxWSBasicRepsonse.isIsSuccesfull()) { return FormValidation.ok("Project Name Validated Successfully"); } else { if (cxWSBasicRepsonse.getErrorMessage() .startsWith("project name validation failed: duplicate name, project name") || cxWSBasicRepsonse.getErrorMessage() .equalsIgnoreCase("Project name already exists")) { return FormValidation.ok("Scan will be added to existing project"); } else if (cxWSBasicRepsonse.getErrorMessage() .equalsIgnoreCase("project name validation failed: unauthorized user") || cxWSBasicRepsonse.getErrorMessage().equalsIgnoreCase("Unauthorized user")) { return FormValidation .error("The user is not authorized to create/run Checkmarx projects"); } else if (cxWSBasicRepsonse.getErrorMessage() .startsWith("Exception occurred at IsValidProjectCreationRequest:")) { logger.warn("Couldn't validate project name with Checkmarx sever:\n" + cxWSBasicRepsonse.getErrorMessage()); return FormValidation.warning(cxWSBasicRepsonse.getErrorMessage()); } else { return FormValidation.error(cxWSBasicRepsonse.getErrorMessage()); } } } else { return FormValidation.ok(); } } catch (Exception e) { logger.warn("Couldn't validate project name with Checkmarx sever:\n" + e.getLocalizedMessage()); return FormValidation.warning("Can't reach server to validate project name"); } } /** * Provides a list of presets from Checkmarx server for dynamic drop-down list in configuration page * * @param useOwnServerCredentials * @param serverUrl * @param username * @param password * @param timestamp * @return list of presets */ /* * Note: This method is called concurrently by multiple threads, refrain from using mutable * shared state to avoid synchronization issues. */ public ListBoxModel doFillPresetItems(@QueryParameter final boolean useOwnServerCredentials, @QueryParameter final String serverUrl, @QueryParameter final String username, @QueryParameter final String password, @QueryParameter final String timestamp) { // timestamp is not used in code, it is one of the arguments to invalidate Internet Explorer cache ListBoxModel listBoxModel = new ListBoxModel(); try { final CxWebService cxWebService = prepareLoggedInWebservice(useOwnServerCredentials, serverUrl, username, password); final List<Preset> presets = cxWebService.getPresets(); for (Preset p : presets) { listBoxModel.add(new ListBoxModel.Option(p.getPresetName(), Long.toString(p.getID()))); } logger.debug("Presets list: " + listBoxModel.size()); return listBoxModel; } catch (Exception e) { logger.debug("Presets list: empty"); String message = "Provide Checkmarx server credentials to see presets list"; listBoxModel.add(new ListBoxModel.Option(message, message)); return listBoxModel; // Return empty list of project names } } /** * Validates frequency of full scans * * @param value * @return if frequency is valid */ /* * Note: This method is called concurrently by multiple threads, refrain from using mutable * shared state to avoid synchronization issues. */ public FormValidation doCheckFullScanCycle(@QueryParameter final int value) { if (value >= FULL_SCAN_CYCLE_MIN && value <= FULL_SCAN_CYCLE_MAX) { return FormValidation.ok(); } else { return FormValidation .error("Number must be in the range " + FULL_SCAN_CYCLE_MIN + "-" + FULL_SCAN_CYCLE_MAX); } } public ListBoxModel doFillSourceEncodingItems(@QueryParameter final boolean useOwnServerCredentials, @QueryParameter final String serverUrl, @QueryParameter final String username, @QueryParameter final String password, @QueryParameter final String timestamp) { // timestamp is not used in code, it is one of the arguments to invalidate Internet Explorer cache ListBoxModel listBoxModel = new ListBoxModel(); try { final CxWebService cxWebService = prepareLoggedInWebservice(useOwnServerCredentials, serverUrl, username, password); final List<ConfigurationSet> sourceEncodings = cxWebService.getSourceEncodings(); for (ConfigurationSet cs : sourceEncodings) { listBoxModel.add(new ListBoxModel.Option(cs.getConfigSetName(), Long.toString(cs.getID()))); } logger.debug("Source encodings list: " + listBoxModel.size()); } catch (Exception e) { logger.debug("Source encodings list: empty"); String message = "Provide Checkmarx server credentials to see source encodings list"; listBoxModel.add(new ListBoxModel.Option(message, message)); } return listBoxModel; } // Provides a list of source encodings from checkmarx server for dynamic drop-down list in configuration page /* * Note: This method is called concurrently by multiple threads, refrain from using mutable * shared state to avoid synchronization issues. */ public ListBoxModel doFillGroupIdItems(@QueryParameter final boolean useOwnServerCredentials, @QueryParameter final String serverUrl, @QueryParameter final String username, @QueryParameter final String password, @QueryParameter final String timestamp) { // timestamp is not used in code, it is one of the arguments to invalidate Internet Explorer cache ListBoxModel listBoxModel = new ListBoxModel(); try { final CxWebService cxWebService = prepareLoggedInWebservice(useOwnServerCredentials, serverUrl, username, password); final List<Group> groups = cxWebService.getAssociatedGroups(); for (Group group : groups) { listBoxModel.add(new ListBoxModel.Option(group.getGroupName(), group.getID())); } logger.debug("Associated groups list: " + listBoxModel.size()); return listBoxModel; } catch (Exception e) { logger.debug("Associated groups: empty"); String message = "Provide Checkmarx server credentials to see teams list"; listBoxModel.add(new ListBoxModel.Option(message, message)); return listBoxModel; // Return empty list of project names } } public ListBoxModel doFillVulnerabilityThresholdResultItems() { ListBoxModel listBoxModel = new ListBoxModel(); for (JobStatusOnError status : JobStatusOnError.values()) { if (status != JobStatusOnError.GLOBAL) { listBoxModel.add(new ListBoxModel.Option(status.getDisplayName(), status.name())); } } return listBoxModel; } /* * Note: This method is called concurrently by multiple threads, refrain from using mutable shared state to * avoid synchronization issues. */ public FormValidation doCheckHighThreshold(@QueryParameter final int value) { return checkNonNegativeValue(value); } /* * Note: This method is called concurrently by multiple threads, refrain from using mutable shared state to * avoid synchronization issues. */ public FormValidation doCheckMediumThreshold(@QueryParameter final int value) { return checkNonNegativeValue(value); } /* * Note: This method is called concurrently by multiple threads, refrain from using mutable shared state to * avoid synchronization issues. */ public FormValidation doCheckLowThreshold(@QueryParameter final int value) { return checkNonNegativeValue(value); } /* * Note: This method is called concurrently by multiple threads, refrain from using mutable shared state to * avoid synchronization issues. */ public FormValidation doCheckHighThresholdEnforcement(@QueryParameter final int value) { return checkNonNegativeValue(value); } /* * Note: This method is called concurrently by multiple threads, refrain from using mutable shared state to * avoid synchronization issues. */ public FormValidation doCheckMediumThresholdEnforcement(@QueryParameter final int value) { return checkNonNegativeValue(value); } /* * Note: This method is called concurrently by multiple threads, refrain from using mutable shared state to * avoid synchronization issues. */ public FormValidation doCheckLowThresholdEnforcement(@QueryParameter final int value) { return checkNonNegativeValue(value); } /* * Note: This method is called concurrently by multiple threads, refrain from using mutable * shared state to avoid synchronization issues. */ private FormValidation checkNonNegativeValue(final int value) { if (value >= 0) { return FormValidation.ok(); } else { return FormValidation.error("Number must be non-negative"); } } public String getDefaultProjectName() { // Retrieves the job name from request URL, cleans it from special characters,\ // and returns as a default project name. final String url = getCurrentDescriptorByNameUrl(); String decodedUrl = null; try { decodedUrl = URLDecoder.decode(url, "UTF-8"); } catch (UnsupportedEncodingException e) { decodedUrl = url; } final String[] urlComponents = decodedUrl.split("/"); if (urlComponents.length > 0) { return urlComponents[urlComponents.length - 1]; } // This is a fallback if the above code fails return ""; } /** * This human readable name is used in the configuration screen. */ @Override public String getDisplayName() { return "Execute Checkmarx Scan"; } @Override public boolean configure(StaplerRequest req, 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) req.bindJSON(this, formData.getJSONObject("checkmarx")); save(); return super.configure(req, formData); } public JobGlobalStatusOnError getJobGlobalStatusOnError() { return jobGlobalStatusOnError; } public void setJobGlobalStatusOnError(JobGlobalStatusOnError jobGlobalStatusOnError) { this.jobGlobalStatusOnError = (null == jobGlobalStatusOnError) ? JobGlobalStatusOnError.FAILURE : jobGlobalStatusOnError; } public JobGlobalStatusOnError getJobGlobalStatusOnThresholdViolation() { return jobGlobalStatusOnThresholdViolation; } public void setJobGlobalStatusOnThresholdViolation( JobGlobalStatusOnError jobGlobalStatusOnThresholdViolation) { this.jobGlobalStatusOnThresholdViolation = jobGlobalStatusOnThresholdViolation; } public boolean isLockVulnerabilitySettings() { return lockVulnerabilitySettings; } public void setLockVulnerabilitySettings(boolean lockVulnerabilitySettings) { this.lockVulnerabilitySettings = lockVulnerabilitySettings; } } }