2020package org .zaproxy .zap .extension .scripts .automation .actions ;
2121
2222import java .lang .reflect .InvocationTargetException ;
23+ import java .lang .reflect .Method ;
2324import java .security .InvalidParameterException ;
2425import java .util .ArrayList ;
2526import java .util .Arrays ;
2627import java .util .List ;
2728import java .util .Locale ;
29+ import java .util .function .Consumer ;
2830import org .apache .commons .httpclient .URI ;
2931import org .apache .commons .lang3 .StringUtils ;
3032import org .parosproxy .paros .Constant ;
33+ import org .parosproxy .paros .control .Control ;
3134import org .parosproxy .paros .model .Model ;
3235import org .parosproxy .paros .model .SiteNode ;
3336import org .parosproxy .paros .network .HttpMessage ;
@@ -45,6 +48,8 @@ public class RunScriptAction extends ScriptAction {
4548
4649 public static final String NAME = "run" ;
4750 private static final String ZEST_ENGINE_NAME = "Mozilla Zest" ;
51+ private static final String EXTENSION_ZEST_NAME = "ExtensionZest" ;
52+ private static final String RUN_NAME_CHAIN_PREFIX = "chain_" ;
4853 private static final List <String > SCRIPT_TYPES =
4954 Arrays .asList (ExtensionScript .TYPE_STANDALONE , ExtensionScript .TYPE_TARGETED );
5055 private static final List <String > DISABLED_FIELDS =
@@ -77,7 +82,18 @@ public List<String> verifyParameters(
7782 String issue ;
7883 String scriptType = params .getType ();
7984
80- if (StringUtils .isEmpty (params .getName ())) {
85+ boolean hasName = StringUtils .isNotEmpty (params .getName ());
86+ boolean hasChain = params .getChain () != null && !params .getChain ().isEmpty ();
87+
88+ if (hasName && hasChain ) {
89+ issue =
90+ Constant .messages .getString (
91+ "scripts.automation.warn.chainAndNameBothSpecified" , jobName );
92+ list .add (issue );
93+ if (progress != null ) {
94+ progress .warn (issue );
95+ }
96+ } else if (!hasName && !hasChain ) {
8197 issue =
8298 Constant .messages .getString (
8399 "scripts.automation.error.scriptNameIsNull" , jobName );
@@ -107,8 +123,16 @@ public List<String> verifyParameters(
107123 if (progress != null ) {
108124 progress .error (issue );
109125 }
126+ } else if (hasChain && !ExtensionScript .TYPE_STANDALONE .equals (scriptType )) {
127+ issue =
128+ Constant .messages .getString (
129+ "scripts.automation.error.chainRequiresStandalone" , jobName );
130+ list .add (issue );
131+ if (progress != null ) {
132+ progress .error (issue );
133+ }
110134 }
111- // Note dont warn/error if script not currently in ZAP - it might be added by another job
135+ // Script/chain existence not validated here; chain validated at runtime in runScriptChain()
112136 if (!StringUtils .isEmpty (params .getSource ())) {
113137 issue =
114138 Constant .messages .getString (
@@ -192,63 +216,170 @@ public void runJob(String jobName, AutomationEnvironment env, AutomationProgress
192216 }
193217 }
194218
195- ScriptJobOutputListener scriptJobOutputListener =
196- new ScriptJobOutputListener (progress , parameters .getName ());
219+ if (parameters .getChain () != null && !parameters .getChain ().isEmpty ()) {
220+ runScriptChain (jobName , user , progress );
221+ } else {
222+ runSingleScript (jobName , user , progress );
223+ }
224+ }
225+
226+ private void runScriptChain (String jobName , User user , AutomationProgress progress ) {
227+ if (!ExtensionScript .TYPE_STANDALONE .equals (parameters .getType ())) {
228+ progress .error (
229+ Constant .messages .getString (
230+ "scripts.automation.error.chainRequiresStandalone" , jobName ));
231+ return ;
232+ }
233+
234+ List <ScriptWrapper > scriptWrappers =
235+ validateChainScripts (parameters .getChain (), jobName , progress );
236+ if (scriptWrappers == null ) {
237+ return ; // Validation failed, error already reported
238+ }
239+
240+ ScriptWrapper firstScript = scriptWrappers .get (0 );
241+ String runName = RUN_NAME_CHAIN_PREFIX + firstScript .getName ();
242+ ScriptWrapper chainScript ;
197243 try {
198- extScript .addScriptOutputListener (scriptJobOutputListener );
199- ScriptWrapper script = findScript ();
200- if (script == null ) {
201- progress .error (
202- Constant .messages .getString (
203- "scripts.automation.error.scriptNameNotFound" ,
204- jobName ,
205- parameters .getName ()));
206- return ;
207- }
244+ chainScript = getChainScriptViaReflection (scriptWrappers , runName );
245+ } catch (Exception e ) {
246+ progress .error (
247+ Constant .messages .getString (
248+ "scripts.automation.error.chainPreparationFailed" ,
249+ jobName ,
250+ e .getMessage ()));
251+ return ;
252+ }
253+ if (chainScript == null ) {
254+ progress .error (
255+ Constant .messages .getString (
256+ "scripts.automation.error.chainReflectionFailed" ,
257+ jobName ,
258+ firstScript .getName ()));
259+ return ;
260+ }
208261
209- if (!getSupportedScriptTypes ().contains (script .getTypeName ())) {
210- progress .error (
211- Constant .messages .getString (
212- "scripts.automation.error.scriptTypeNotSupported" ,
213- jobName ,
214- script .getTypeName (),
215- getName (),
216- String .join (", " , getSupportedScriptTypes ())));
217- return ;
218- }
262+ setUserOnZestWrapper (chainScript , user );
219263
220- if (parameters .getType ().equals (ExtensionScript .TYPE_TARGETED )) {
221- URI targetUri = new URI (parameters .getTarget (), true );
222- SiteNode siteNode =
223- Model .getSingleton ().getSession ().getSiteTree ().findNode (targetUri );
224- if (siteNode == null ) {
225- progress .error (
226- Constant .messages .getString (
227- "scripts.automation.error.scriptTargetNotFound" ,
228- jobName ,
229- parameters .getTarget ()));
230- return ;
231- }
264+ progress .info (
265+ Constant .messages .getString (
266+ "scripts.automation.info.chainExecuting" , jobName , scriptWrappers .size ()));
232267
233- HttpMessage httpMessage = siteNode .getHistoryReference ().getHttpMessage ();
234- extScript .invokeTargetedScript (script , httpMessage );
235- } else {
236- setUserOnZestWrapper (script , user );
237- extScript .invokeScript (script );
238- }
268+ if (executeScriptWithOutputListener (
269+ chainScript ,
270+ progress ,
271+ () -> extScript .invokeScript (chainScript ),
272+ (e ) ->
273+ progress .error (
274+ Constant .messages .getString (
275+ "scripts.automation.error.chainExecutionFailed" ,
276+ jobName ,
277+ e .getMessage ())))) {
278+ progress .info (
279+ Constant .messages .getString ("scripts.automation.info.chainCompleted" , jobName ));
280+ }
281+ }
282+
283+ private void runSingleScript (String jobName , User user , AutomationProgress progress ) {
284+ ScriptWrapper script = findScript ();
285+ if (script == null ) {
286+ progress .error (
287+ Constant .messages .getString (
288+ "scripts.automation.error.scriptNameNotFound" ,
289+ jobName ,
290+ parameters .getName ()));
291+ return ;
292+ }
293+
294+ if (!getSupportedScriptTypes ().contains (script .getTypeName ())) {
295+ progress .error (
296+ Constant .messages .getString (
297+ "scripts.automation.error.scriptTypeNotSupported" ,
298+ jobName ,
299+ script .getTypeName (),
300+ getName (),
301+ String .join (", " , getSupportedScriptTypes ())));
302+ return ;
303+ }
304+
305+ if (parameters .getType ().equals (ExtensionScript .TYPE_TARGETED )) {
306+ executeScriptWithOutputListener (
307+ script ,
308+ progress ,
309+ () -> {
310+ URI targetUri = new URI (parameters .getTarget (), true );
311+ SiteNode siteNode =
312+ Model .getSingleton ().getSession ().getSiteTree ().findNode (targetUri );
313+ if (siteNode == null ) {
314+ progress .error (
315+ Constant .messages .getString (
316+ "scripts.automation.error.scriptTargetNotFound" ,
317+ jobName ,
318+ parameters .getTarget ()));
319+ return ;
320+ }
321+
322+ HttpMessage httpMessage = siteNode .getHistoryReference ().getHttpMessage ();
323+ extScript .invokeTargetedScript (script , httpMessage );
324+ },
325+ (e ) -> reportScriptError (progress , jobName , parameters , e ));
326+ } else {
327+ runSingleScript (jobName , user , progress , script );
328+ }
329+ }
330+
331+ /** Runs one standalone script (non-targeted). */
332+ private void runSingleScript (
333+ String jobName , User user , AutomationProgress progress , ScriptWrapper script ) {
334+ setUserOnZestWrapper (script , user );
335+ executeScriptWithOutputListener (
336+ script ,
337+ progress ,
338+ () -> extScript .invokeScript (script ),
339+ (e ) -> reportScriptError (progress , jobName , parameters , e ));
340+ }
341+
342+ /**
343+ * Runs the script with output listener setup/teardown and error handling.
344+ *
345+ * @param script the script to execute
346+ * @param progress the automation progress for output
347+ * @param executor the script execution logic
348+ * @param errorHandler the error handler for exceptions
349+ * @return true if execution succeeded, false otherwise
350+ */
351+ private boolean executeScriptWithOutputListener (
352+ ScriptWrapper script ,
353+ AutomationProgress progress ,
354+ ScriptExecutor executor ,
355+ Consumer <Exception > errorHandler ) {
356+ ScriptJobOutputListener scriptJobOutputListener =
357+ new ScriptJobOutputListener (progress , script .getName ());
358+ try {
359+ extScript .addScriptOutputListener (scriptJobOutputListener );
360+ executor .execute ();
239361 scriptJobOutputListener .flush ();
240362
241363 if (script .getLastException () != null ) {
242- reportScriptError (progress , jobName , parameters , script .getLastException ());
364+ errorHandler .accept (script .getLastException ());
365+ return false ;
243366 }
367+ return true ;
244368 } catch (Exception e ) {
245369 LOGGER .error (e , e );
246- reportScriptError (progress , jobName , parameters , e );
370+ errorHandler .accept (e );
371+ return false ;
247372 } finally {
248373 extScript .removeScriptOutputListener (scriptJobOutputListener );
249374 }
250375 }
251376
377+ /** Script execution logic that may throw. */
378+ @ FunctionalInterface
379+ private interface ScriptExecutor {
380+ void execute () throws Exception ;
381+ }
382+
252383 private static void reportScriptError (
253384 AutomationProgress progress ,
254385 String jobName ,
@@ -281,4 +412,80 @@ private void setUserOnZestWrapper(ScriptWrapper script, User user) {
281412 LOGGER .warn ("Failed to set user on script wrapper" , e );
282413 }
283414 }
415+
416+ /**
417+ * Gets the chain script from Zest via reflection. Returns null if Zest is not loaded or the
418+ * method is missing. Throws if Zest's getChainScript throws (e.g. validation failure); the
419+ * exception message is then reported to the user.
420+ *
421+ * @param scriptWrappers validated chain in order
422+ * @param runName name for the run
423+ * @return chain script to invoke, or null
424+ * @throws Exception when Zest's getChainScript throws (cause is rethrown)
425+ */
426+ private ScriptWrapper getChainScriptViaReflection (
427+ List <ScriptWrapper > scriptWrappers , String runName ) throws Exception {
428+ Object extZest =
429+ Control .getSingleton ().getExtensionLoader ().getExtension (EXTENSION_ZEST_NAME );
430+ if (extZest == null ) {
431+ LOGGER .warn ("ExtensionZest not loaded, cannot get chain script" );
432+ return null ;
433+ }
434+ try {
435+ Method getChainScript =
436+ extZest .getClass ().getMethod ("getChainScript" , List .class , String .class );
437+ return (ScriptWrapper ) getChainScript .invoke (extZest , scriptWrappers , runName );
438+ } catch (NoSuchMethodException | IllegalAccessException | SecurityException e ) {
439+ LOGGER .warn ("Failed to get chain script via ExtensionZest" , e );
440+ return null ;
441+ } catch (InvocationTargetException e ) {
442+ Throwable cause = e .getCause ();
443+ if (cause instanceof Error ) {
444+ throw (Error ) cause ;
445+ }
446+ if (cause instanceof Exception ) {
447+ throw (Exception ) cause ;
448+ }
449+ throw new RuntimeException (cause );
450+ }
451+ }
452+
453+ /**
454+ * Validates chain scripts and returns their wrappers.
455+ *
456+ * @param chain script names in order
457+ * @param jobName for error messages
458+ * @param progress for error reporting
459+ * @return validated ScriptWrappers, or null if validation failed
460+ */
461+ private List <ScriptWrapper > validateChainScripts (
462+ List <String > chain , String jobName , AutomationProgress progress ) {
463+ List <ScriptWrapper > scriptWrappers = new ArrayList <>();
464+
465+ for (String scriptName : chain ) {
466+ ScriptWrapper script = extScript .getScript (scriptName );
467+ if (script == null ) {
468+ progress .error (
469+ Constant .messages .getString (
470+ "scripts.automation.error.chainScriptNotFound" ,
471+ jobName ,
472+ scriptName ));
473+ return null ;
474+ }
475+
476+ if (!ExtensionScript .TYPE_STANDALONE .equals (script .getTypeName ())
477+ || !ZEST_ENGINE_NAME .equals (script .getEngineName ())) {
478+ progress .error (
479+ Constant .messages .getString (
480+ "scripts.automation.error.chainScriptNotZestStandalone" ,
481+ jobName ,
482+ scriptName ));
483+ return null ;
484+ }
485+
486+ scriptWrappers .add (script );
487+ }
488+
489+ return scriptWrappers ;
490+ }
284491}
0 commit comments