From b76335bda16df23a5b9f8ab929229320b35aed7c Mon Sep 17 00:00:00 2001 From: Simon Bennetts Date: Wed, 23 Sep 2026 10:50:22 +0200 Subject: [PATCH] GSPM: add ascan plugin listeners Signed-off-by: Simon Bennetts --- .../paros/core/scanner/PluginFactory.java | 71 +++++++++++++++++ .../core/scanner/PluginFactoryUnitTest.java | 77 +++++++++++++++++++ 2 files changed, 148 insertions(+) diff --git a/zap/src/main/java/org/parosproxy/paros/core/scanner/PluginFactory.java b/zap/src/main/java/org/parosproxy/paros/core/scanner/PluginFactory.java index 6d7f0eddf0b..6b90c1aa268 100644 --- a/zap/src/main/java/org/parosproxy/paros/core/scanner/PluginFactory.java +++ b/zap/src/main/java/org/parosproxy/paros/core/scanner/PluginFactory.java @@ -61,6 +61,8 @@ // ZAP: 2022/09/21 Use format specifiers instead of concatenation when logging. // ZAP: 2023/01/10 Tidy up logger. // ZAP: 2025/11/17 Support locked policy. +// ZAP: 2026/09/22 From now on we will not be recording changes here as the files have changed so +// much. package org.parosproxy.paros.core.scanner; import java.util.ArrayList; @@ -72,6 +74,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; import org.apache.commons.configuration.BaseConfiguration; import org.apache.commons.configuration.Configuration; import org.apache.commons.configuration.ConfigurationException; @@ -86,6 +89,7 @@ public class PluginFactory { private static final Logger LOGGER = LogManager.getLogger(PluginFactory.class); private static List loadedPlugins = null; private static Map mapLoadedPlugins; + private static final List LOADER_LISTENERS = new CopyOnWriteArrayList<>(); private List listAllPlugin = new ArrayList<>(); private LinkedHashMap mapAllPlugin = @@ -219,6 +223,7 @@ public static void loadedPlugin(AbstractPlugin plugin) { getLoadedPlugins().add(plugin); mapLoadedPlugins.put(plugin.getId(), plugin); Collections.sort(loadedPlugins, riskComparator); + notifyPluginLoaded(plugin); } } @@ -230,11 +235,77 @@ public static void unloadedPlugin(AbstractPlugin plugin) { if (it.next() == plugin) { it.remove(); mapLoadedPlugins.remove(plugin.getId()); + notifyPluginUnloaded(plugin); return; } } } + /** + * Adds a listener notified whenever a plugin is loaded or unloaded via {@link + * #loadedPlugin(AbstractPlugin)} / {@link #unloadedPlugin(AbstractPlugin)}. This is not part of + * the public API. + * + * @param listener the listener to add. + * @since 2.18.0 + */ + public static void addPluginLoaderListener(PluginLoaderListener listener) { + LOADER_LISTENERS.add(listener); + } + + /** + * Removes a previously added {@link PluginLoaderListener}. This is not part of the public API. + * + * @param listener the listener to remove. + * @since 2.18.0 + */ + public static void removePluginLoaderListener(PluginLoaderListener listener) { + LOADER_LISTENERS.remove(listener); + } + + private static void notifyPluginLoaded(AbstractPlugin plugin) { + for (PluginLoaderListener listener : LOADER_LISTENERS) { + try { + listener.pluginLoaded(plugin); + } catch (Exception e) { + LOGGER.error("Failed to notify listener of loaded plugin {}", plugin.getId(), e); + } + } + } + + private static void notifyPluginUnloaded(AbstractPlugin plugin) { + for (PluginLoaderListener listener : LOADER_LISTENERS) { + try { + listener.pluginUnloaded(plugin); + } catch (Exception e) { + LOGGER.error("Failed to notify listener of unloaded plugin {}", plugin.getId(), e); + } + } + } + + /** + * Notified when a plugin is loaded or unloaded. This is not part of the public API. + * + * @see PluginFactory#addPluginLoaderListener(PluginLoaderListener) + * @since 2.18.0 + */ + public interface PluginLoaderListener { + + /** + * Called after a plugin has been loaded. + * + * @param plugin the plugin that was loaded. + */ + void pluginLoaded(AbstractPlugin plugin); + + /** + * Called after a plugin has been unloaded. + * + * @param plugin the plugin that was unloaded. + */ + void pluginUnloaded(AbstractPlugin plugin); + } + // now order the list by the highest risk thrown, in descending order (to execute the more // critical checks first) private static final Comparator riskComparator = diff --git a/zap/src/test/java/org/parosproxy/paros/core/scanner/PluginFactoryUnitTest.java b/zap/src/test/java/org/parosproxy/paros/core/scanner/PluginFactoryUnitTest.java index a72fb12be06..8bcaf88814d 100644 --- a/zap/src/test/java/org/parosproxy/paros/core/scanner/PluginFactoryUnitTest.java +++ b/zap/src/test/java/org/parosproxy/paros/core/scanner/PluginFactoryUnitTest.java @@ -30,9 +30,13 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.withSettings; +import java.util.ArrayList; import java.util.List; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -60,6 +64,79 @@ void setUp() throws Exception { PluginFactory.init(false); } + @AfterEach + void tearDown() { + loaderListeners.forEach(PluginFactory::removePluginLoaderListener); + } + + private final List loaderListeners = new ArrayList<>(); + + private PluginFactory.PluginLoaderListener addMockLoaderListener() { + PluginFactory.PluginLoaderListener listener = mock(); + loaderListeners.add(listener); + PluginFactory.addPluginLoaderListener(listener); + return listener; + } + + @Test + void shouldNotifyListenerWhenPluginLoaded() { + // Given + AbstractPlugin plugin = createAbstractPlugin(); + PluginFactory.PluginLoaderListener listener = addMockLoaderListener(); + // When + PluginFactory.loadedPlugin(plugin); + // Then + verify(listener).pluginLoaded(plugin); + verify(listener, never()).pluginUnloaded(any()); + } + + @Test + void shouldNotifyListenerWhenPluginUnloaded() { + // Given + AbstractPlugin plugin = createAbstractPlugin(); + PluginFactory.loadedPlugin(plugin); + PluginFactory.PluginLoaderListener listener = addMockLoaderListener(); + // When + PluginFactory.unloadedPlugin(plugin); + // Then + verify(listener).pluginUnloaded(plugin); + } + + @Test + void shouldNotNotifyListenerWhenLoadingAnAlreadyLoadedPlugin() { + // Given + AbstractPlugin plugin = createAbstractPlugin(); + PluginFactory.loadedPlugin(plugin); + PluginFactory.PluginLoaderListener listener = addMockLoaderListener(); + // When + PluginFactory.loadedPlugin(plugin); + // Then + verify(listener, never()).pluginLoaded(any()); + } + + @Test + void shouldNotNotifyListenerWhenUnloadingANotYetLoadedPlugin() { + // Given + AbstractPlugin plugin = createAbstractPlugin(); + PluginFactory.PluginLoaderListener listener = addMockLoaderListener(); + // When + PluginFactory.unloadedPlugin(plugin); + // Then + verify(listener, never()).pluginUnloaded(any()); + } + + @Test + void shouldNotNotifyRemovedListener() { + // Given + AbstractPlugin plugin = createAbstractPlugin(); + PluginFactory.PluginLoaderListener listener = addMockLoaderListener(); + PluginFactory.removePluginLoaderListener(listener); + // When + PluginFactory.loadedPlugin(plugin); + // Then + verify(listener, never()).pluginLoaded(any()); + } + @Test void shouldNotHaveUndefinedPluginLoaded() { // Given