diff --git a/addOns/automation/CHANGELOG.md b/addOns/automation/CHANGELOG.md index d93eb36eb46..03878799e0c 100644 --- a/addOns/automation/CHANGELOG.md +++ b/addOns/automation/CHANGELOG.md @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Add job to configure the active scanner, `activeScan-config`. - Allow to enable/disable jobs (Issue 5845). - Method to allow the user to set the exit code via a script. +- Add exitStatus job (Issue #6928) ### Changed - Maintenance changes. diff --git a/addOns/automation/src/main/java/org/zaproxy/addon/automation/ExtensionAutomation.java b/addOns/automation/src/main/java/org/zaproxy/addon/automation/ExtensionAutomation.java index 3d6ee873a5c..c00b9e183ff 100644 --- a/addOns/automation/src/main/java/org/zaproxy/addon/automation/ExtensionAutomation.java +++ b/addOns/automation/src/main/java/org/zaproxy/addon/automation/ExtensionAutomation.java @@ -66,6 +66,7 @@ import org.zaproxy.addon.automation.jobs.ActiveScanJob; import org.zaproxy.addon.automation.jobs.ActiveScanPolicyJob; import org.zaproxy.addon.automation.jobs.DelayJob; +import org.zaproxy.addon.automation.jobs.ExitStatusJob; import org.zaproxy.addon.automation.jobs.ParamsJob; import org.zaproxy.addon.automation.jobs.RequestorJob; import org.zaproxy.zap.ZAP; @@ -84,6 +85,9 @@ public class ExtensionAutomation extends ExtensionAdaptor implements CommandLine public static final String PREFIX = "automation"; public static final String RESOURCES_DIR = "/org/zaproxy/addon/automation/resources/"; + public static final int OK_EXIT_VALUE = 0; + public static final int ERROR_EXIT_VALUE = 1; + public static final int WARN_EXIT_VALUE = 2; protected static final String PLANS_RUN_STATS = "stats.auto.plans.run"; protected static final String TOTAL_JOBS_RUN_STATS = "stats.auto.jobs.run"; @@ -114,7 +118,7 @@ public class ExtensionAutomation extends ExtensionAdaptor implements CommandLine private AutomationPanel automationPanel; - private Integer exitOverride; + private static Integer exitOverride; public ExtensionAutomation() { super(NAME); @@ -132,6 +136,7 @@ public void init() { registerAutomationJob(new org.zaproxy.addon.automation.jobs.AddOnJob()); registerAutomationJob(new RequestorJob()); registerAutomationJob(new DelayJob()); + registerAutomationJob(new ExitStatusJob()); registerAutomationJob( new ActiveScanConfigJob( Control.getSingleton() @@ -701,18 +706,18 @@ private void runPlanCommandLine(String source) { if (exitOverride != null) { setExitStatus(exitOverride, "set by user", false); } else if (progress == null || progress.hasErrors()) { - setExitStatus(1, "plan errors", false); + setExitStatus(ERROR_EXIT_VALUE, "plan errors", false); } else if (progress.hasWarnings()) { - setExitStatus(2, "plan warnings", false); + setExitStatus(WARN_EXIT_VALUE, "plan warnings", false); } } - public Integer getExitOverride() { + public static Integer getExitOverride() { return exitOverride; } - public void setExitOverride(Integer exitOverride) { - this.exitOverride = exitOverride; + public static void setExitOverride(Integer exitOverride) { + ExtensionAutomation.exitOverride = exitOverride; } private static URI createUri(String source) { diff --git a/addOns/automation/src/main/java/org/zaproxy/addon/automation/gui/ExitStatusJobDialog.java b/addOns/automation/src/main/java/org/zaproxy/addon/automation/gui/ExitStatusJobDialog.java new file mode 100644 index 00000000000..b587fa46b7d --- /dev/null +++ b/addOns/automation/src/main/java/org/zaproxy/addon/automation/gui/ExitStatusJobDialog.java @@ -0,0 +1,143 @@ +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Copyright 2024 The ZAP Development Team + * + * 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.zaproxy.addon.automation.gui; + +import org.apache.commons.lang3.ArrayUtils; +import org.parosproxy.paros.Constant; +import org.parosproxy.paros.core.scanner.Alert; +import org.parosproxy.paros.view.View; +import org.zaproxy.addon.automation.ExtensionAutomation; +import org.zaproxy.addon.automation.jobs.ExitStatusJob; +import org.zaproxy.addon.automation.jobs.JobUtils; +import org.zaproxy.zap.utils.DisplayUtils; +import org.zaproxy.zap.view.StandardFieldsDialog; + +@SuppressWarnings("serial") +public class ExitStatusJobDialog extends StandardFieldsDialog { + + private static final long serialVersionUID = 1L; + + private static final String TITLE = "automation.dialog.exitstatus.title"; + private static final String NAME_PARAM = "automation.dialog.all.name"; + private static final String ERROR_LEVEL_PARAM = "automation.dialog.exitstatus.errorLevel"; + private static final String WARN_LEVEL_PARAM = "automation.dialog.exitstatus.warnLevel"; + private static final String OK_EXIT_VALUE_PARAM = "automation.dialog.exitstatus.okExitValue"; + private static final String WARN_EXIT_VALUE_PARAM = + "automation.dialog.exitstatus.warnExitValue"; + private static final String ERROR_EXIT_VALUE_PARAM = + "automation.dialog.exitstatus.errorExitValue"; + + private ExitStatusJob job; + + private static String[] getRiskOptions() { + return ArrayUtils.add(Alert.MSG_RISK, ""); + } + + public ExitStatusJobDialog(ExitStatusJob job) { + super(View.getSingleton().getMainFrame(), TITLE, DisplayUtils.getScaledDimension(300, 270)); + this.job = job; + + this.addTextField(NAME_PARAM, this.job.getData().getName()); + this.addComboField( + ERROR_LEVEL_PARAM, + getRiskOptions(), + this.job.getData().getParameters().getErrorLevel()); + this.addComboField( + WARN_LEVEL_PARAM, + getRiskOptions(), + this.job.getData().getParameters().getWarnLevel()); + this.addNumberField( + OK_EXIT_VALUE_PARAM, + 0, + 255, + getInt( + this.job.getParameters().getOkExitValue(), + ExtensionAutomation.OK_EXIT_VALUE)); + this.addNumberField( + WARN_EXIT_VALUE_PARAM, + 0, + 255, + getInt( + this.job.getParameters().getWarnExitValue(), + ExtensionAutomation.WARN_EXIT_VALUE)); + this.addNumberField( + ERROR_EXIT_VALUE_PARAM, + 0, + 255, + getInt( + this.job.getParameters().getErrorExitValue(), + ExtensionAutomation.ERROR_EXIT_VALUE)); + + this.addPadding(); + } + + private int getInt(Integer integer, int defaultValue) { + if (integer == null) { + return defaultValue; + } + return integer.intValue(); + } + + private Integer getInteger(int i, int defaultValue) { + if (i == defaultValue) { + return null; + } + return Integer.valueOf(i); + } + + @Override + public void save() { + this.job.getData().setName(this.getStringValue(NAME_PARAM)); + this.job.getParameters().setErrorLevel(this.getStringValue(ERROR_LEVEL_PARAM)); + this.job.getParameters().setWarnLevel(this.getStringValue(WARN_LEVEL_PARAM)); + this.job + .getParameters() + .setOkExitValue( + getInteger( + this.getIntValue(OK_EXIT_VALUE_PARAM), + ExtensionAutomation.OK_EXIT_VALUE)); + this.job + .getParameters() + .setWarnExitValue( + getInteger( + this.getIntValue(WARN_EXIT_VALUE_PARAM), + ExtensionAutomation.WARN_EXIT_VALUE)); + this.job + .getParameters() + .setErrorExitValue( + getInteger( + this.getIntValue(ERROR_EXIT_VALUE_PARAM), + ExtensionAutomation.ERROR_EXIT_VALUE)); + this.job.resetAndSetChanged(); + } + + @Override + public String validateFields() { + Integer errorRisk = JobUtils.parseAlertRisk(this.getStringValue(ERROR_LEVEL_PARAM)); + Integer warnRisk = JobUtils.parseAlertRisk(this.getStringValue(WARN_LEVEL_PARAM)); + if (warnRisk != null && errorRisk != null && warnRisk > errorRisk) { + return Constant.messages.getString( + "automation.exitstatus.error.badlevels", + this.getStringValue(ERROR_LEVEL_PARAM), + this.getStringValue(WARN_LEVEL_PARAM)); + } + return null; + } +} diff --git a/addOns/automation/src/main/java/org/zaproxy/addon/automation/gui/NewPlanDialog.java b/addOns/automation/src/main/java/org/zaproxy/addon/automation/gui/NewPlanDialog.java index 817ed8c8106..74d241d4168 100644 --- a/addOns/automation/src/main/java/org/zaproxy/addon/automation/gui/NewPlanDialog.java +++ b/addOns/automation/src/main/java/org/zaproxy/addon/automation/gui/NewPlanDialog.java @@ -40,6 +40,7 @@ import org.zaproxy.addon.automation.AutomationPlan; import org.zaproxy.addon.automation.ExtensionAutomation; import org.zaproxy.addon.automation.jobs.ActiveScanJob; +import org.zaproxy.addon.automation.jobs.ExitStatusJob; import org.zaproxy.zap.utils.DisplayUtils; import org.zaproxy.zap.view.StandardFieldsDialog; @@ -64,7 +65,12 @@ public class NewPlanDialog extends StandardFieldsDialog { private static final String REPORT_JOB_NAME = "report"; private static final String[] BASELINE_PROFILE = { - "passiveScan-config", "spider", "spiderAjax", "passiveScan-wait", REPORT_JOB_NAME + "passiveScan-config", + "spider", + "spiderAjax", + "passiveScan-wait", + REPORT_JOB_NAME, + ExitStatusJob.JOB_NAME }; private static final String[] IMPORT_PROFILE = { "passiveScan-config", @@ -73,16 +79,32 @@ public class NewPlanDialog extends StandardFieldsDialog { "spiderAjax", "passiveScan-wait", ActiveScanJob.JOB_NAME, - REPORT_JOB_NAME + REPORT_JOB_NAME, + ExitStatusJob.JOB_NAME }; private static final String[] OPENAPI_PROFILE = { - "passiveScan-config", "openapi", "passiveScan-wait", ActiveScanJob.JOB_NAME, REPORT_JOB_NAME + "passiveScan-config", + "openapi", + "passiveScan-wait", + ActiveScanJob.JOB_NAME, + REPORT_JOB_NAME, + ExitStatusJob.JOB_NAME }; private static final String[] GRAPHQL_PROFILE = { - "passiveScan-config", "graphql", "passiveScan-wait", ActiveScanJob.JOB_NAME, REPORT_JOB_NAME + "passiveScan-config", + "graphql", + "passiveScan-wait", + ActiveScanJob.JOB_NAME, + REPORT_JOB_NAME, + ExitStatusJob.JOB_NAME }; private static final String[] SOAP_PROFILE = { - "passiveScan-config", "soap", "passiveScan-wait", ActiveScanJob.JOB_NAME, REPORT_JOB_NAME + "passiveScan-config", + "soap", + "passiveScan-wait", + ActiveScanJob.JOB_NAME, + REPORT_JOB_NAME, + ExitStatusJob.JOB_NAME }; private static final String[] FULL_SCAN_PROFILE = { "passiveScan-config", @@ -90,7 +112,8 @@ public class NewPlanDialog extends StandardFieldsDialog { "spiderAjax", "passiveScan-wait", ActiveScanJob.JOB_NAME, - REPORT_JOB_NAME + REPORT_JOB_NAME, + ExitStatusJob.JOB_NAME }; private JList contextList; diff --git a/addOns/automation/src/main/java/org/zaproxy/addon/automation/jobs/ExitStatusJob.java b/addOns/automation/src/main/java/org/zaproxy/addon/automation/jobs/ExitStatusJob.java new file mode 100644 index 00000000000..bf6ee701b75 --- /dev/null +++ b/addOns/automation/src/main/java/org/zaproxy/addon/automation/jobs/ExitStatusJob.java @@ -0,0 +1,178 @@ +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Copyright 2024 The ZAP Development Team + * + * 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.zaproxy.addon.automation.jobs; + +import java.util.LinkedHashMap; +import java.util.Map; +import lombok.Getter; +import lombok.Setter; +import org.parosproxy.paros.Constant; +import org.parosproxy.paros.core.scanner.Alert; +import org.zaproxy.addon.automation.AutomationData; +import org.zaproxy.addon.automation.AutomationEnvironment; +import org.zaproxy.addon.automation.AutomationJob; +import org.zaproxy.addon.automation.AutomationProgress; +import org.zaproxy.addon.automation.ExtensionAutomation; +import org.zaproxy.addon.automation.JobResultData; +import org.zaproxy.addon.automation.gui.ExitStatusJobDialog; + +public class ExitStatusJob extends AutomationJob { + + public static final String JOB_NAME = "exitStatus"; + + private Data data; + private Parameters parameters = new Parameters(); + + public ExitStatusJob() { + this.data = new Data(this, parameters); + } + + @Override + public void verifyParameters(AutomationProgress progress) { + Map jobData = this.getJobData(); + if (jobData != null) { + JobUtils.applyParamsToObject( + (LinkedHashMap) jobData.get("parameters"), + this.parameters, + this.getName(), + null, + progress); + } + + Integer errorRisk = + JobUtils.parseAlertRisk(parameters.getErrorLevel(), this.getName(), progress); + Integer warnRisk = + JobUtils.parseAlertRisk(parameters.getWarnLevel(), this.getName(), progress); + if (warnRisk != null && errorRisk != null && warnRisk > errorRisk) { + progress.warn( + Constant.messages.getString( + "automation.exitstatus.error.badlevels", + parameters.getErrorLevel(), + parameters.getWarnLevel())); + } + } + + @Override + public void applyParameters(AutomationProgress progress) {} + + @Override + public void runJob(AutomationEnvironment env, AutomationProgress progress) { + Integer errorRisk = JobUtils.parseAlertRisk(parameters.getErrorLevel()); + Integer warnRisk = JobUtils.parseAlertRisk(parameters.getWarnLevel()); + boolean warningRaised = false; + + try { + for (JobResultData data : progress.getAllJobResultData()) { + for (Alert alert : data.getAllAlertData()) { + if (errorRisk != null && errorRisk <= alert.getRisk()) { + progress.error( + Constant.messages.getString( + "automation.exitstatus.alert", parameters.getErrorLevel())); + return; + } + if (!warningRaised && warnRisk != null && warnRisk <= alert.getRisk()) { + progress.warn( + Constant.messages.getString( + "automation.exitstatus.alert", parameters.getWarnLevel())); + warningRaised = true; + } + } + } + } finally { + // Set the exit value, if configured + if (progress.hasErrors()) { + if (parameters.getErrorExitValue() != null) { + ExtensionAutomation.setExitOverride(parameters.getErrorExitValue()); + } + } else if (progress.hasWarnings()) { + if (parameters.getWarnExitValue() != null) { + ExtensionAutomation.setExitOverride(parameters.getWarnExitValue()); + } + } else { + if (parameters.getOkExitValue() != null) { + ExtensionAutomation.setExitOverride(parameters.getOkExitValue()); + } + } + } + } + + @Override + public String getType() { + return JOB_NAME; + } + + @Override + public Order getOrder() { + return Order.RUN_LAST; + } + + @Override + public Object getParamMethodObject() { + return null; + } + + @Override + public String getParamMethodName() { + return null; + } + + @Override + public void showDialog() { + new ExitStatusJobDialog(this).setVisible(true); + } + + @Override + public String getSummary() { + return Constant.messages.getString( + "automation.dialog.exitstatus.summary", + this.getData().getParameters().getErrorLevel(), + this.getData().getParameters().getWarnLevel()); + } + + @Override + public Data getData() { + return data; + } + + @Override + public Parameters getParameters() { + return this.parameters; + } + + @Getter + public static class Data extends JobData { + private final Parameters parameters; + + public Data(AutomationJob job, Parameters parameters) { + super(job); + this.parameters = parameters; + } + } + + @Getter + @Setter + public static class Parameters extends AutomationData { + private String errorLevel = ""; + private String warnLevel = ""; + private Integer okExitValue; + private Integer warnExitValue; + private Integer errorExitValue; + } +} diff --git a/addOns/automation/src/main/javahelp/org/zaproxy/addon/automation/resources/help/contents/automation.html b/addOns/automation/src/main/javahelp/org/zaproxy/addon/automation/resources/help/contents/automation.html index 2b2a8f6ff51..14d15debacf 100644 --- a/addOns/automation/src/main/javahelp/org/zaproxy/addon/automation/resources/help/contents/automation.html +++ b/addOns/automation/src/main/javahelp/org/zaproxy/addon/automation/resources/help/contents/automation.html @@ -8,7 +8,9 @@

Automation Framework

This add-on provides a framework that allows ZAP to be automated in an easy and flexible way. -

+ +

Command Line Options

+ It provides the following command line options: -If the -autorun option is used with the ZAP -cmd option then the ZAP exit value will be set as follows: +

Exit Codes

+If the -autorun option is used with the ZAP -cmd option then the ZAP exit value will be set by default as follows: +These values can be overridden by the exitStatus job.
Whether the plan completed after encountering errors or warnings will depend on the settings used in the environment. -

+ +

Usage

To use the automation framework:
  1. Generate a template automation file using one of the -autogen* command line options @@ -38,16 +43,16 @@

    Automation Framework

    and ZAP exits as soon as it has finished generating or running the jobs defined in the file. However you can choose to run Automation Framework jobs using the ZAP desktop to help you debug issues. -

    Authentication

    +

    Authentication

    The Automation Framework supports all of the authentication mechanisms supported by ZAP. -

    GUI

    +

    GUI

    A GUI is under development and provides an ever increasing set of features. -

    Options

    +

    Options

    The Automation Options screen allows you to configure specific options. -

    API

    +

    API

    The following API endpoints are provided by this add-on:
    • Action: runPlan(filePath) - loads and asynchronously runs the plan in the specified file, returning a planId
    • @@ -55,14 +60,14 @@

      API

    If the ZAP desktop is being used then the plan will also be shown in the GUI to make it easier to diagnose any problems. -

    Environment

    +

    Environment

    The environment section of the file defines the applications which the rest of the jobs can act on. -

    File Paths

    +

    File Paths

    All file and directory paths can either be absolute or relative to the directory containing the plan. Relative paths are recommended for portability. -

    Jobs

    +

    Jobs

    The jobs can be enabled/disabled through the GUI and the automation plan, with the enabled flag. Jobs are enabled by default.

    The following automation jobs are supported by this add-on: @@ -73,6 +78,7 @@

    Jobs

  2. delay - pauses the plan for a specified period of time or a specific condition is met
  3. requestor - crafts specific requests to send to the corresponding targets
  4. activeScan - runs the active scanner
  5. +
  6. exitStatus - sets ZAP's exit code based on scan results
  7. Importance of Job Order

    diff --git a/addOns/automation/src/main/javahelp/org/zaproxy/addon/automation/resources/help/contents/job-exitstatus.html b/addOns/automation/src/main/javahelp/org/zaproxy/addon/automation/resources/help/contents/job-exitstatus.html new file mode 100644 index 00000000000..6cae027f9e8 --- /dev/null +++ b/addOns/automation/src/main/javahelp/org/zaproxy/addon/automation/resources/help/contents/job-exitstatus.html @@ -0,0 +1,37 @@ + + + + +Automation Framework - exitStatus Job + + + +

    Automation Framework - exitStatus Job

    + +This job sets ZAP's exit code based on scan results. +It also allows you to choose which exit values are used. +It should typically be the last job in a plan. +

    +If warnLevel or errorLevel are set then the job will report a warning or error if any alerts +are raised which have the same risk level or greater. +

    +By default when ZAP is run with the -cmd and -autorun options then it will +exit with a 1 if there are any errors, with a 2 if there are any warnings, and if everything is ok +then it will exit with a 0. +These values can be overriden by the *ExitValue options. The *ExitValues can be used together +with the warn/errorLevel or completely independently of them. + +

    YAML

    + +
    +  - type: exitStatus                   # Sets the exit code based on scan results
    +    parameters:
    +      errorLevel:                      # String: Informational, Low, Medium, High, default: not set
    +      warnLevel:                       # String: Informational, Low, Medium, High, default: not set
    +      okExitValue:                     # Integer: Exit value if all ok, default 0
    +      errorExitValue:                  # Integer: Exit value if there are errors, default 1
    +      warnExitValue:                   # Integer: Exit value if there are warnings, default 2
    + 
    + + + diff --git a/addOns/automation/src/main/javahelp/org/zaproxy/addon/automation/resources/help/index.xml b/addOns/automation/src/main/javahelp/org/zaproxy/addon/automation/resources/help/index.xml index 60bb6ae4fd1..742e58abe76 100644 --- a/addOns/automation/src/main/javahelp/org/zaproxy/addon/automation/resources/help/index.xml +++ b/addOns/automation/src/main/javahelp/org/zaproxy/addon/automation/resources/help/index.xml @@ -15,6 +15,7 @@ + diff --git a/addOns/automation/src/main/javahelp/org/zaproxy/addon/automation/resources/help/map.jhm b/addOns/automation/src/main/javahelp/org/zaproxy/addon/automation/resources/help/map.jhm index d5a97fbd8d3..d7039cb59cf 100644 --- a/addOns/automation/src/main/javahelp/org/zaproxy/addon/automation/resources/help/map.jhm +++ b/addOns/automation/src/main/javahelp/org/zaproxy/addon/automation/resources/help/map.jhm @@ -15,6 +15,7 @@ + diff --git a/addOns/automation/src/main/javahelp/org/zaproxy/addon/automation/resources/help/toc.xml b/addOns/automation/src/main/javahelp/org/zaproxy/addon/automation/resources/help/toc.xml index afcfa7779c0..eaea56350d3 100644 --- a/addOns/automation/src/main/javahelp/org/zaproxy/addon/automation/resources/help/toc.xml +++ b/addOns/automation/src/main/javahelp/org/zaproxy/addon/automation/resources/help/toc.xml @@ -16,6 +16,7 @@ + diff --git a/addOns/automation/src/main/resources/org/zaproxy/addon/automation/resources/Messages.properties b/addOns/automation/src/main/resources/org/zaproxy/addon/automation/resources/Messages.properties index 189ee561e15..067cdeaf7a6 100644 --- a/addOns/automation/src/main/resources/org/zaproxy/addon/automation/resources/Messages.properties +++ b/addOns/automation/src/main/resources/org/zaproxy/addon/automation/resources/Messages.properties @@ -174,6 +174,14 @@ automation.dialog.envvar.value = Value: automation.dialog.error.misc = Unexpected error: {0} automation.dialog.error.save = Failed to save plan: {0} +automation.dialog.exitstatus.errorExitValue = Error Exit Value: + +automation.dialog.exitstatus.errorLevel = Error Level: +automation.dialog.exitstatus.okExitValue = OK Exit Value: +automation.dialog.exitstatus.summary = Error: {0}, Warn: {1} +automation.dialog.exitstatus.title = Exit Status Job +automation.dialog.exitstatus.warnExitValue = Warn Exit Value: +automation.dialog.exitstatus.warnLevel = Warning Level: automation.dialog.header.name = Name automation.dialog.header.remove.confirm = Are you sure you want to remove this header? @@ -362,6 +370,9 @@ automation.error.unexpected = Unexpected error accessing file {0} : {1} - see lo automation.error.unexpected.internal = Unexpected error {0} - see log for details automation.error.urlsfound = Job {0} only found {1} URLs, expected at least {2} automation.error.write = Cannot write to file: {0} +automation.exitstatus.alert = An alert has been raised with a risk of at least: {0} + +automation.exitstatus.error.badlevels = Error level: {0} is lower than warn level: {1} automation.info.addons.noupdate = The updateAddons option has been disabled due to problems updating the framework and jobs while they are running automation.info.ascan.rule.setstrength = Job {0} set rule {1} strength to {2} diff --git a/addOns/automation/src/main/resources/org/zaproxy/addon/automation/resources/exitStatus-max.yaml b/addOns/automation/src/main/resources/org/zaproxy/addon/automation/resources/exitStatus-max.yaml new file mode 100644 index 00000000000..cf79abbe1ee --- /dev/null +++ b/addOns/automation/src/main/resources/org/zaproxy/addon/automation/resources/exitStatus-max.yaml @@ -0,0 +1,7 @@ + - type: exitStatus # Sets the exit code based on scan results + parameters: + errorLevel: # String: Informational, Low, Medium, High, default: not set + warnLevel: # String: Informational, Low, Medium, High, default: not set + okExitValue: # Integer: Exit value if all ok, default 0 + errorExitValue: # Integer: Exit value if there are errors, default 1 + warnExitValue: # Integer: Exit value if there are warnings, default 2 diff --git a/addOns/automation/src/main/resources/org/zaproxy/addon/automation/resources/exitStatus-min.yaml b/addOns/automation/src/main/resources/org/zaproxy/addon/automation/resources/exitStatus-min.yaml new file mode 100644 index 00000000000..b9687217a6c --- /dev/null +++ b/addOns/automation/src/main/resources/org/zaproxy/addon/automation/resources/exitStatus-min.yaml @@ -0,0 +1,4 @@ + - type: exitStatus # Sets the exit code based on scan results + parameters: + errorLevel: # String: Informational, Low, Medium, High, default: not set + warnLevel: # String: Informational, Low, Medium, High, default: not set diff --git a/addOns/automation/src/test/java/org/zaproxy/addon/automation/ExtentionAutomationUnitTest.java b/addOns/automation/src/test/java/org/zaproxy/addon/automation/ExtentionAutomationUnitTest.java index 953830d9a3d..1c14a86e905 100644 --- a/addOns/automation/src/test/java/org/zaproxy/addon/automation/ExtentionAutomationUnitTest.java +++ b/addOns/automation/src/test/java/org/zaproxy/addon/automation/ExtentionAutomationUnitTest.java @@ -58,6 +58,7 @@ import org.zaproxy.addon.automation.jobs.ActiveScanJob; import org.zaproxy.addon.automation.jobs.ActiveScanPolicyJob; import org.zaproxy.addon.automation.jobs.DelayJob; +import org.zaproxy.addon.automation.jobs.ExitStatusJob; import org.zaproxy.addon.automation.jobs.ParamsJob; import org.zaproxy.addon.automation.jobs.RequestorJob; import org.zaproxy.zap.extension.stats.InMemoryStats; @@ -117,7 +118,7 @@ void shouldRegisterBuiltInJobsOnInit() { Map jobs = extAuto.getAutomationJobs(); // Then - assertThat(jobs.size(), is(equalTo(7))); + assertThat(jobs.size(), is(equalTo(8))); assertThat( jobs.containsKey(org.zaproxy.addon.automation.jobs.AddOnJob.JOB_NAME), is(equalTo(true))); @@ -127,6 +128,7 @@ void shouldRegisterBuiltInJobsOnInit() { assertThat(jobs.containsKey(ActiveScanPolicyJob.JOB_NAME), is(equalTo(true))); assertThat(jobs.containsKey(ParamsJob.JOB_NAME), is(equalTo(true))); assertThat(jobs.containsKey(RequestorJob.JOB_NAME), is(equalTo(true))); + assertThat(jobs.containsKey(ExitStatusJob.JOB_NAME), is(equalTo(true))); } @Test diff --git a/addOns/automation/src/test/java/org/zaproxy/addon/automation/jobs/ExitStatusJobUnitTest.java b/addOns/automation/src/test/java/org/zaproxy/addon/automation/jobs/ExitStatusJobUnitTest.java new file mode 100644 index 00000000000..e11bf1b2267 --- /dev/null +++ b/addOns/automation/src/test/java/org/zaproxy/addon/automation/jobs/ExitStatusJobUnitTest.java @@ -0,0 +1,369 @@ +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Copyright 2024 The ZAP Development Team + * + * 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.zaproxy.addon.automation.jobs; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; + +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedHashMap; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.parosproxy.paros.core.scanner.Alert; +import org.yaml.snakeyaml.Yaml; +import org.zaproxy.addon.automation.AutomationEnvironment; +import org.zaproxy.addon.automation.AutomationJob.Order; +import org.zaproxy.addon.automation.AutomationProgress; +import org.zaproxy.addon.automation.ExtensionAutomation; +import org.zaproxy.addon.automation.JobResultData; +import org.zaproxy.zap.testutils.TestUtils; + +class ExitStatusJobUnitTest extends TestUtils { + + @BeforeAll + static void setUp() { + mockMessages(new ExtensionAutomation()); + } + + @Test + void shouldNotFailIfNoConfigs() { + // Given + ExitStatusJob job = new ExitStatusJob(); + AutomationProgress progress = new AutomationProgress(); + AutomationEnvironment env = new AutomationEnvironment(progress); + + // When + job.applyParameters(progress); + job.runJob(env, progress); + + // Then + assertThat(progress.hasErrors(), is(equalTo(false))); + assertThat(progress.hasWarnings(), is(equalTo(false))); + } + + @Test + void shouldReturnDefaultInfo() { + // Given / When + ExitStatusJob job = new ExitStatusJob(); + job.getParameters().setErrorLevel("High"); + job.getParameters().setWarnLevel("Medium"); + + // Then + assertThat(job.getName(), is(equalTo("exitStatus"))); + assertThat(job.getSummary(), is(equalTo("Error: High, Warn: Medium"))); + assertThat(job.getOrder(), is(equalTo(Order.RUN_LAST))); + assertThat(job.getParamMethodName(), is(nullValue())); + assertThat(job.getParamMethodObject(), is(nullValue())); + } + + @Test + void shouldVerifyParameters() { + // Given + ExitStatusJob job = new ExitStatusJob(); + AutomationProgress progress = new AutomationProgress(); + + String yamlStr = + "parameters:\n" + + " errorLevel: high\n" + + " warnLevel: LOW\n" + + " okExitValue: 1\n" + + " warnExitValue: 2\n" + + " errorExitValue: 3"; + Object data = new Yaml().load(yamlStr); + job.setJobData(((LinkedHashMap) data)); + + // When + job.verifyParameters(progress); + + // Then + assertThat(job.getParameters().getErrorLevel(), is(equalTo("high"))); + assertThat(job.getParameters().getWarnLevel(), is(equalTo("LOW"))); + assertThat(job.getParameters().getOkExitValue(), is(equalTo(1))); + assertThat(job.getParameters().getWarnExitValue(), is(equalTo(2))); + assertThat(job.getParameters().getErrorExitValue(), is(equalTo(3))); + } + + @ParameterizedTest + @ValueSource(strings = {"informational", "LOW", "Medium", "HiGH", " "}) + void shouldNotWarnOnValidErrorLevel(String badrisk) { + // Given + ExitStatusJob job = new ExitStatusJob(); + AutomationProgress progress = new AutomationProgress(); + + // When + job.getParameters().setErrorLevel(badrisk); + job.verifyParameters(progress); + + // Then + assertThat(progress.hasWarnings(), is(equalTo(false))); + } + + @ParameterizedTest + @ValueSource(strings = {"Invalid", "Info", "None", "-"}) + void shouldWarnOnInvalidErrorLevel(String badrisk) { + // Given + ExitStatusJob job = new ExitStatusJob(); + AutomationProgress progress = new AutomationProgress(); + + // When + job.getParameters().setErrorLevel(badrisk); + job.verifyParameters(progress); + + // Then + assertThat(progress.hasErrors(), is(equalTo(false))); + assertThat(progress.hasWarnings(), is(equalTo(true))); + assertThat(progress.getWarnings().size(), is(equalTo(1))); + assertThat( + progress.getWarnings().get(0), + is(equalTo("Invalid risk for job exitStatus : " + badrisk))); + } + + @ParameterizedTest + @ValueSource(strings = {"informational", "LOW", "Medium", "HiGH", " "}) + void shouldNotWarnOnValidWarnLevel(String badrisk) { + // Given + ExitStatusJob job = new ExitStatusJob(); + AutomationProgress progress = new AutomationProgress(); + + // When + job.getParameters().setWarnLevel(badrisk); + job.verifyParameters(progress); + + // Then + assertThat(progress.hasWarnings(), is(equalTo(false))); + assertThat(progress.hasErrors(), is(equalTo(false))); + } + + @ParameterizedTest + @ValueSource(strings = {"Invalid", "Info", "None", "-"}) + void shouldWarnOnInvalidWarnLevel(String badrisk) { + // Given + ExitStatusJob job = new ExitStatusJob(); + AutomationProgress progress = new AutomationProgress(); + + // When + job.getParameters().setWarnLevel(badrisk); + job.verifyParameters(progress); + + // Then + assertThat(progress.hasErrors(), is(equalTo(false))); + assertThat(progress.hasWarnings(), is(equalTo(true))); + assertThat(progress.hasErrors(), is(equalTo(false))); + assertThat(progress.getWarnings().size(), is(equalTo(1))); + assertThat( + progress.getWarnings().get(0), + is(equalTo("Invalid risk for job exitStatus : " + badrisk))); + } + + @ParameterizedTest + @CsvSource({ + "high,Medium", + "HIGH, Medium", + "High,Informational", + "medium,LOW", + "medium,INFORMATIOnaL", + "low,informational" + }) + void shouldWarnOnWarnGreaterThanError(String warnrisk, String errorrisk) { + // Given + ExitStatusJob job = new ExitStatusJob(); + AutomationProgress progress = new AutomationProgress(); + + // When + job.getParameters().setWarnLevel(warnrisk); + job.getParameters().setErrorLevel(errorrisk); + job.verifyParameters(progress); + + // Then + assertThat(progress.hasErrors(), is(equalTo(false))); + assertThat(progress.hasWarnings(), is(equalTo(true))); + assertThat(progress.getWarnings().size(), is(equalTo(1))); + assertThat( + progress.getWarnings().get(0), + is( + equalTo( + "Error level: " + + errorrisk + + " is lower than warn level: " + + warnrisk))); + } + + @ParameterizedTest + @CsvSource({ + "high,Medium", + "HIGH, Medium", + "High,Informational", + "medium,LOW", + "medium,INFORMATIOnaL", + "low,informational" + }) + void shouldNotWarnOnAlertsLessThanMinimumLevel(String warnrisk, String alertrisk) { + // Given + ExitStatusJob job = new ExitStatusJob(); + AutomationProgress progress = new AutomationProgress(); + progress.addJobResultData(getTestData(alertrisk)); + + // When + job.getParameters().setWarnLevel(warnrisk); + job.verifyParameters(progress); + job.runJob(new AutomationEnvironment(progress), progress); + + // Then + assertThat(progress.hasWarnings(), is(equalTo(false))); + assertThat(progress.hasErrors(), is(equalTo(false))); + } + + @ParameterizedTest + @CsvSource({ + "high,Medium", + "HIGH, Medium", + "High,Informational", + "medium,LOW", + "medium,INFORMATIOnaL", + "low,informational" + }) + void shouldNotErrorOnAlertsLessThanMinimumLevel(String errorrisk, String alertrisk) { + // Given + ExitStatusJob job = new ExitStatusJob(); + AutomationProgress progress = new AutomationProgress(); + progress.addJobResultData(getTestData(alertrisk)); + + // When + job.getParameters().setErrorLevel(errorrisk); + job.verifyParameters(progress); + job.runJob(new AutomationEnvironment(progress), progress); + + // Then + assertThat(progress.hasWarnings(), is(equalTo(false))); + assertThat(progress.hasErrors(), is(equalTo(false))); + } + + @ParameterizedTest + @CsvSource({ + "HIGH,high", + "high,Medium", + "HIGH, Medium", + "High,Informational", + "medium,medium", + "medium,LOW", + "medium,INFORMATIOnaL", + "low,LOW", + "low,informational", + "informational,informational" + }) + void shouldWarnOnAlertsWithMinimumLevel(String alertrisk, String warnrisk) { + // Given + ExitStatusJob job = new ExitStatusJob(); + AutomationProgress progress = new AutomationProgress(); + progress.addJobResultData(getTestData(alertrisk)); + + // When + job.getParameters().setWarnLevel(warnrisk); + job.verifyParameters(progress); + job.runJob(new AutomationEnvironment(progress), progress); + + // Then + assertThat(progress.hasErrors(), is(equalTo(false))); + assertThat(progress.hasWarnings(), is(equalTo(true))); + assertThat(progress.getWarnings().size(), is(equalTo(1))); + assertThat( + progress.getWarnings().get(0), + is(equalTo("An alert has been raised with a risk of at least: " + warnrisk))); + } + + @ParameterizedTest + @CsvSource({ + "HIGH,high", + "high,Medium", + "HIGH, Medium", + "High,Informational", + "medium,medium", + "medium,LOW", + "medium,INFORMATIOnaL", + "low,LOW", + "low,informational", + "informational,informational" + }) + void shouldErrorOnAlertsWithMinimumLevel(String alertrisk, String errorrisk) { + // Given + ExitStatusJob job = new ExitStatusJob(); + AutomationProgress progress = new AutomationProgress(); + progress.addJobResultData(getTestData(alertrisk)); + + // When + job.getParameters().setErrorLevel(errorrisk); + job.verifyParameters(progress); + job.runJob(new AutomationEnvironment(progress), progress); + + // Then + assertThat(progress.hasWarnings(), is(equalTo(false))); + assertThat(progress.hasErrors(), is(equalTo(true))); + assertThat(progress.getErrors().size(), is(equalTo(1))); + assertThat( + progress.getErrors().get(0), + is(equalTo("An alert has been raised with a risk of at least: " + errorrisk))); + } + + @ParameterizedTest + @CsvSource({"HIGH,4", "medium,3", "low,2"}) + void shouldSetExitCode(String alertrisk, String exitcode) { + // Given + ExitStatusJob job = new ExitStatusJob(); + AutomationProgress progress = new AutomationProgress(); + progress.addJobResultData(getTestData(alertrisk)); + + // When + job.getParameters().setOkExitValue(2); + job.getParameters().setWarnExitValue(3); + job.getParameters().setErrorExitValue(4); + job.getParameters().setErrorLevel("high"); + job.getParameters().setWarnLevel("medium"); + job.verifyParameters(progress); + job.runJob(new AutomationEnvironment(progress), progress); + + // Then + assertThat(ExtensionAutomation.getExitOverride(), is(equalTo(Integer.parseInt(exitcode)))); + } + + private JobResultData getTestData(String alertLevel) { + Alert alert = new Alert(-1, JobUtils.parseAlertRisk(alertLevel), 2, "test"); + + JobResultData data = + new JobResultData("test") { + + @Override + public String getKey() { + return "test"; + } + + @Override + public Collection getAllAlertData() { + return Arrays.asList(alert); + } + }; + return data; + } +} diff --git a/addOns/automation/src/test/resources/org/zaproxy/addon/automation/resources/template-config.yaml b/addOns/automation/src/test/resources/org/zaproxy/addon/automation/resources/template-config.yaml index fcbd8df37cd..2b7a3016f75 100644 --- a/addOns/automation/src/test/resources/org/zaproxy/addon/automation/resources/template-config.yaml +++ b/addOns/automation/src/test/resources/org/zaproxy/addon/automation/resources/template-config.yaml @@ -39,3 +39,7 @@ jobs: parameters: context: + - type: exitStatus + name: exitStatus + parameters: + diff --git a/addOns/automation/src/test/resources/org/zaproxy/addon/automation/resources/template-max.yaml b/addOns/automation/src/test/resources/org/zaproxy/addon/automation/resources/template-max.yaml index 3cc75c9e362..e38e14273a3 100644 --- a/addOns/automation/src/test/resources/org/zaproxy/addon/automation/resources/template-max.yaml +++ b/addOns/automation/src/test/resources/org/zaproxy/addon/automation/resources/template-max.yaml @@ -157,3 +157,11 @@ jobs: otherInfo: # String: Addional information corresponding to the alert, optional onFail: 'info' # String: One of 'warn', 'error', 'info', mandatory + - type: exitStatus # Sets the exit code based on scan results + parameters: + errorLevel: # String: Informational, Low, Medium, High, default: not set + warnLevel: # String: Informational, Low, Medium, High, default: not set + okExitValue: # Integer: Exit value if all ok, default 0 + errorExitValue: # Integer: Exit value if there are errors, default 1 + warnExitValue: # Integer: Exit value if there are warnings, default 2 + diff --git a/addOns/automation/src/test/resources/org/zaproxy/addon/automation/resources/template-min.yaml b/addOns/automation/src/test/resources/org/zaproxy/addon/automation/resources/template-min.yaml index 366d49de6eb..b12f3ee4d15 100644 --- a/addOns/automation/src/test/resources/org/zaproxy/addon/automation/resources/template-min.yaml +++ b/addOns/automation/src/test/resources/org/zaproxy/addon/automation/resources/template-min.yaml @@ -66,3 +66,8 @@ jobs: otherInfo: # String: Addional information corresponding to the alert, optional onFail: 'info' # String: One of 'warn', 'error', 'info', mandatory + - type: exitStatus # Sets the exit code based on scan results + parameters: + errorLevel: # String: Informational, Low, Medium, High, default: not set + warnLevel: # String: Informational, Low, Medium, High, default: not set +