/*
*
* Paros and its related class files.
*
* Paros is an HTTP/HTTPS proxy for assessing web application security.
* Copyright (C) 2003-2004 Chinotec Technologies Company
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the Clarified Artistic License
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* Clarified Artistic License for more details.
*
* You should have received a copy of the Clarified Artistic License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
// ZAP: 2011/08/30 Support for scanner levels
// ZAP: 2012/04/23 Changed the method loadAllPlugin to reflect the changes made
// in the method DynamicLoader.getFilteredObject(Class).
// ZAP: 2012/04/25 Removed unnecessary casts, changed to use the method
// Integer.valueOf and added logging of exception.
// ZAP: 2012/11/20 Issue 419: Restructure jar loading code
// ZAP: 2013/01/16 Issue 453: Dynamic loading and unloading of add-ons
// ZAP: 2013/01/19 Issue 460 Add support for a scan progress dialog
// ZAP: 2013/01/25 Catch any exceptions thrown when loading plugins to allow ZAP to still start
// ZAP: 2013/03/18 Issue 564: Active scanner can hang if dependencies used
// ZAP: 2013/05/02 Re-arranged all modifiers into Java coding standard order
// ZAP: 2014/01/16 Added skip support functions and changed obsolete collections
// ZAP: 2014/02/12 Issue 1030: Load and save scan policies
// ZAP: 2014/02/21 Issue 1043: Custom active scan dialog
// ZAP: 2014/05/20 Issue 377: Unfulfilled dependencies hang the active scan
// ZAP: 2014/11/19 Issue 1412: Manage scan policies
// ZAP: 2014/11/19 Issue 1412: Init scan rule status (quality) from add-on
// ZAP: 2015/01/04 Issue 1484: NullPointerException during uninstallation of an add-on with active scanners
// ZAP: 2015/01/04 Issue 1486: Add-on components leak
// ZAP: 2015/07/25 Do not log error if the duplicated scanner is (apparently) a newer/older version
// ZAP: 2015/08/19 Issue 1785: Plugin enabled even if dependencies are not, "hangs" active scan
// ZAP: 2015/11/02 Issue 1969: Issues with installation of scanners
// ZAP: 2015/12/21 Issue 2112: Wrong policy on active Scan
// ZAP: 2016/01/26 Fixed findbugs warning
// ZAP: 2016/05/04 Use existing Plugin instances when setting them as completed
// ZAP: 2016/06/27 Reduce log level when loading the plugins
// ZAP: 2016/06/29 Do not log when cloning PluginFactory
// ZAP: 2016/07/25 Fix to correct handling of lists in plugins
package org.parosproxy.paros.core.scanner;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.configuration.BaseConfiguration;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.HierarchicalConfiguration;
import org.apache.log4j.Logger;
import org.zaproxy.zap.control.CoreFunctionality;
import org.zaproxy.zap.control.ExtensionFactory;
public class PluginFactory {
private static Logger log = Logger.getLogger(PluginFactory.class);
private static List<AbstractPlugin> loadedPlugins = null;
private List<Plugin> listAllPlugin = new ArrayList<Plugin>();
private LinkedHashMap<Integer, Plugin> mapAllPlugin = new LinkedHashMap<>(); //insertion-ordered
private LinkedHashMap<String, Plugin> mapAllPluginOrderCodeName = new LinkedHashMap<>(); //insertion-ordered
private List<Plugin> listPending = new ArrayList<Plugin>();
private List<Plugin> listRunning = new ArrayList<Plugin>();
private List<Plugin> listCompleted = new ArrayList<Plugin>();
private int totalPluginToRun = 0;
private boolean init = false;
private Configuration config;
public PluginFactory() {
super();
HierarchicalConfiguration configuration = new HierarchicalConfiguration();
configuration.setDelimiterParsingDisabled(true);
config = configuration;
}
private static synchronized void initPlugins() {
if (loadedPlugins == null) {
loadedPlugins = new ArrayList<>(CoreFunctionality.getBuiltInActiveScanRules());
loadedPlugins.addAll(ExtensionFactory.getAddOnLoader().getActiveScanRules());
//sort by the criteria below.
Collections.sort(loadedPlugins, riskComparator);
}
}
private static List<AbstractPlugin> getLoadedPlugins() {
if (loadedPlugins == null) {
initPlugins();
}
return loadedPlugins;
}
/**
* Tells whether or not the given {@code plugin} was already loaded.
*
* @param plugin the plugin that will be checked
* @return {@code true} if the plugin was already loaded, {@code false} otherwise
* @since 2.4.3
*/
public static boolean isPluginLoaded(AbstractPlugin plugin) {
if (loadedPlugins == null) {
return false;
}
return isPluginLoadedImpl(plugin);
}
private static boolean isPluginLoadedImpl(AbstractPlugin plugin) {
for (AbstractPlugin otherPlugin : getLoadedPlugins()) {
if (otherPlugin == plugin) {
return true;
}
}
return false;
}
/**
* Adds the given loaded {@code plugin} to the {@code PluginFactory}. Loaded plugins, are used by the active scanner, if
* enabled.
* <p>
* Call to this method has not effect it the {@code plugin} was already added.
*
* @param plugin the plugin that should be loaded
* @since 2.4.0
* @see #isPluginLoaded(AbstractPlugin)
*/
public static void loadedPlugin(AbstractPlugin plugin) {
if (!isPluginLoadedImpl(plugin)) {
getLoadedPlugins().add(plugin);
Collections.sort(loadedPlugins, riskComparator);
}
}
/**
* @deprecated (2.4.3) Use {@link #loadedPlugin(AbstractPlugin)} instead, the status of the scanner is not
* properly set.
* @see AbstractPlugin#getStatus()
*/
@Deprecated
@SuppressWarnings("javadoc")
public static boolean loadedPlugin(String className) {
try {
Class<?> c = ExtensionFactory.getAddOnLoader().loadClass(className);
loadedPlugin((AbstractPlugin) c.newInstance());
return true;
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
}
public static void unloadedPlugin(AbstractPlugin plugin) {
if (loadedPlugins == null) {
return;
}
for (Iterator<AbstractPlugin> it = getLoadedPlugins().iterator(); it.hasNext();) {
if (it.next() == plugin) {
it.remove();
return;
}
}
}
/**
* @deprecated (2.4.3) Use {@link #unloadedPlugin(AbstractPlugin)} instead, which ensures that the exact scanner
* instance is unloaded.
*/
@Deprecated
@SuppressWarnings("javadoc")
public static boolean unloadedPlugin(String className) {
if (loadedPlugins == null) {
return true;
}
for (AbstractPlugin plugin : loadedPlugins) {
if (plugin.getClass().getName().equals(className)) {
loadedPlugins.remove(plugin);
return true;
}
}
return false;
}
//now order the list by the highest risk thrown, in descending order (to execute the more critical checks first)
private static final Comparator<AbstractPlugin> riskComparator = new Comparator<AbstractPlugin>() {
@Override
public int compare(AbstractPlugin e1, AbstractPlugin e2) {
if (e1.getStatus().ordinal() > e2.getStatus().ordinal()) {
//High Risk alerts are checked before low risk alerts
return -1;
}
if (e1.getStatus().ordinal() < e2.getStatus().ordinal()) {
//High Risk alerts are checked before low risk alerts
return 1;
}
if (e1.getRisk() > e2.getRisk()) {
//High Risk alerts are checked before low risk alerts
return -1;
} else if (e1.getRisk() < e2.getRisk()) {
return 1;
} else {
//need to look at a secondary factor (the Id of the plugin) to decide. Run older plugins first, followed by newer plugins
if (e1.getId() < e2.getId()) {
//log numbered (older) plugins are run before newer plugins
return -1;
} else if (e1.getId() > e2.getId()) {
return 1;
} else {
return 0;
}
}
}
};
public void reset () {
Iterator<Plugin> iterator;
Plugin plugin;
synchronized (mapAllPlugin) {
this.listPending.clear();
this.listRunning.clear();
this.listCompleted.clear();
// pass 1 - enable all plugin's dependency
iterator = mapAllPlugin.values().iterator();
while (iterator.hasNext()) {
// ZAP: Removed unnecessary cast.
plugin = iterator.next();
if (plugin.isEnabled()) {
enableDependency(plugin);
}
}
// pass 2 - put enabled dependency in listPending
iterator = mapAllPlugin.values().iterator();
while (iterator.hasNext()) {
// ZAP: Removed unnecessary cast.
plugin = iterator.next();
if (plugin.isEnabled()) {
listPending.add(plugin);
}
}
totalPluginToRun = listPending.size();
}
this.init = true;
}
private void enableDependency(Plugin plugin) {
String[] dependency = plugin.getDependency();
if (dependency == null || dependency.length == 0) {
return;
}
List<Plugin> dependencies = new ArrayList<>(dependency.length);
if (addAllDependencies(plugin, dependencies)) {
for (Plugin dep : dependencies) {
if (!dep.isEnabled()) {
dep.setEnabled(true);
}
}
} else {
plugin.setEnabled(false);
plugin.setAlertThreshold(Plugin.AlertThreshold.OFF);
log.warn("Disabled scanner '" + plugin.getName() + "' because of unfulfilled dependencies.");
}
}
public boolean hasAllDependenciesAvailable(Plugin plugin) {
List<Plugin> deps = new ArrayList<>();
return addAllDependencies(plugin, deps);
}
public boolean addAllDependencies(Plugin plugin, List<Plugin> to) {
String[] dependencies = plugin.getDependency();
if (dependencies == null || dependencies.length == 0) {
return true;
}
boolean allDepsAvailable = true;
List<String> deps = new ArrayList<>(Arrays.asList(dependencies));
for (String dependency : deps) {
Plugin pluginDep = mapAllPluginOrderCodeName.get(dependency);
if (pluginDep == null) {
allDepsAvailable = false;
} else if (!to.contains(pluginDep)) {
to.add(pluginDep);
allDepsAvailable &= addAllDependencies(pluginDep, to);
}
}
return allDepsAvailable;
}
public List<Plugin> getDependentPlugins(Plugin plugin) {
List<Plugin> dependentPlugins = new ArrayList<>();
addDependentPlugins(plugin.getCodeName(), dependentPlugins);
return dependentPlugins;
}
private void addDependentPlugins(String pluginName, List<Plugin> to) {
for (Plugin plugin : listAllPlugin) {
String[] dependencies = plugin.getDependency();
if (dependencies != null && dependencies.length != 0) {
if (Arrays.asList(dependencies).contains(pluginName) && !to.contains(plugin)) {
to.add(plugin);
addDependentPlugins(plugin.getCodeName(), to);
}
}
}
}
public List<Plugin> getDependencies(Plugin plugin) {
String[] dependencies = plugin.getDependency();
if (dependencies == null || dependencies.length == 0) {
return Collections.emptyList();
}
List<String> deps = new ArrayList<>(Arrays.asList(dependencies));
List<Plugin> depsPlugins = new ArrayList<>(deps.size());
for (String dependency : deps) {
Plugin pluginDep = mapAllPluginOrderCodeName.get(dependency);
if (pluginDep != null) {
depsPlugins.add(pluginDep);
}
}
return depsPlugins;
}
/**
*
* @param config
*/
public synchronized void loadAllPlugin(Configuration config) {
log.debug("loadAllPlugin");
this.config = config;
//mapAllPlugin is ordered by insertion order, so the ordering of plugins in listTest is used
//when mapAllPlugin is iterated
synchronized (mapAllPlugin) {
mapAllPlugin.clear();
listAllPlugin.clear();
mapAllPluginOrderCodeName.clear();
for (int i = 0; i < getLoadedPlugins().size(); i++) {
// ZAP: Removed unnecessary cast.
try {
Plugin loadedPlugin = getLoadedPlugins().get(i);
if (!loadedPlugin.isVisible()) {
log.info("Plugin " + loadedPlugin.getName() + " not visible");
continue;
}
if (loadedPlugin.isDepreciated()) {
// ZAP: ignore all depreciated plugins
log.info("Plugin " + loadedPlugin.getName() + " depricated");
continue;
}
if (!canAddPlugin(mapAllPlugin, loadedPlugin)) {
continue;
}
Plugin plugin = createNewPlugin(loadedPlugin, config);
if (log.isDebugEnabled()) {
log.debug("loaded plugin " + plugin.getName() +
" with: Threshold=" + plugin.getAlertThreshold().name() +
" Strength=" + plugin.getAttackStrength().toString());
}
// ZAP: Changed to use the method Integer.valueOf.
mapAllPlugin.put(Integer.valueOf(plugin.getId()), plugin);
mapAllPluginOrderCodeName.put(plugin.getCodeName(), plugin);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
Iterator<Plugin> iterator = mapAllPlugin.values().iterator();
while (iterator.hasNext()) {
listAllPlugin.add(iterator.next());
}
}
}
private static Plugin createNewPlugin(Plugin plugin, Configuration config) throws ReflectiveOperationException {
Plugin newPlugin = plugin.getClass().newInstance();
newPlugin.setConfig(new BaseConfiguration());
plugin.cloneInto(newPlugin);
newPlugin.setConfig(config);
newPlugin.createParamIfNotExist();
newPlugin.loadFrom(config);
return newPlugin;
}
private static boolean canAddPlugin(Map<Integer, Plugin> plugins, Plugin plugin) {
Plugin existingPlugin = plugins.get(Integer.valueOf(plugin.getId()));
if (existingPlugin == null) {
return true;
}
// Check if it has also the same name, might be the same scanner but a newer/older version
if (existingPlugin.getName().equals(plugin.getName())) {
if (existingPlugin.getStatus().compareTo(plugin.getStatus()) > 0) {
log.info("Ignoring (apparently) less stable scanner version, id=" + plugin.getId() + ", ExistingPlugin[Status="
+ existingPlugin.getStatus() + ", Class=" + existingPlugin.getClass().getCanonicalName()
+ "], LessStablePlugin[Status=" + plugin.getStatus() + ", Class="
+ plugin.getClass().getCanonicalName() + "]");
return false;
}
if (existingPlugin.getStatus() != plugin.getStatus()) {
log.info("Replacing existing scanner with (apparently) stabler version, id=" + plugin.getId()
+ ", ExistingPlugin[Status=" + existingPlugin.getStatus() + ", Class="
+ existingPlugin.getClass().getCanonicalName() + "], StablerPlugin[Status=" + plugin.getStatus()
+ ", Class=" + plugin.getClass().getCanonicalName() + "]");
return true;
}
}
log.error("Duplicate id " + plugin.getId() + " " + plugin.getClass().getCanonicalName() + " "
+ existingPlugin.getClass().getCanonicalName());
return true;
}
public synchronized void loadFrom(PluginFactory pf) {
log.debug("loadFrom " + pf.listAllPlugin.size());
for (Plugin plugin : pf.listAllPlugin) {
Plugin p = this.mapAllPlugin.get(plugin.getId());
if (p != null) {
plugin.cloneInto(p);
}
}
}
public List<Plugin> getAllPlugin() {
return listAllPlugin;
}
@Override
public PluginFactory clone () {
PluginFactory clone = new PluginFactory();
Plugin pluginCopy;
for (Plugin plugin : listAllPlugin) {
try {
pluginCopy = plugin.getClass().newInstance();
pluginCopy.setConfig(clone.config);
plugin.cloneInto(pluginCopy);
clone.addPlugin(pluginCopy);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
return clone;
}
public boolean addPlugin(String name) {
try {
Class<?> c = ExtensionFactory.getAddOnLoader().loadClass(name);
Plugin plugin = (AbstractPlugin) c.newInstance();
boolean duplicatedId = mapAllPlugin.get(Integer.valueOf(plugin.getId())) != null;
if (this.addPlugin(plugin)) {
log.info("loaded plugin " + plugin.getName());
if (duplicatedId) {
log.error("Duplicate id " + plugin.getName() + " "
+ mapAllPlugin.get(Integer.valueOf(plugin.getId())).getName());
}
return true;
}
if (!plugin.isVisible()) {
log.info("Plugin " + plugin.getName() + " not visible");
return false;
}
if (plugin.isDepreciated()) {
log.info("Plugin " + plugin.getName() + " deprecated");
return false;
}
return false;
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
}
private boolean addPlugin(Plugin plugin) {
listAllPlugin.add(plugin);
plugin.setConfig(this.config);
plugin.createParamIfNotExist();
if (!plugin.isVisible()) {
return false;
}
if (plugin.isDepreciated()) {
return false;
}
mapAllPlugin.put(Integer.valueOf(plugin.getId()), plugin);
mapAllPluginOrderCodeName.put(plugin.getCodeName(), plugin);
return true;
}
public boolean removePlugin(String className) {
for (int i = 0; i < listAllPlugin.size(); i++) {
Plugin plugin = listAllPlugin.get(i);
if (plugin.getClass().getName().equals(className)) {
listAllPlugin.remove(plugin);
mapAllPlugin.remove(Integer.valueOf(plugin.getId()));
mapAllPluginOrderCodeName.remove(plugin.getCodeName());
return true;
}
}
return false;
}
public Plugin getPlugin(int id) {
// ZAP: Removed unnecessary cast and changed to use the method
// Integer.valueOf.
return mapAllPlugin.get(Integer.valueOf(id));
}
public void setAllPluginEnabled(boolean enabled) {
for (int i = 0; i < listAllPlugin.size(); i++) {
// ZAP: Removed unnecessary cast.
Plugin plugin = listAllPlugin.get(i);
plugin.setEnabled(enabled);
}
}
synchronized boolean existPluginToRun() {
if (!init) {
this.reset();
}
if (probeNextPlugin() != null) {
return true;
}
// no test ready to run. still exist test if due to dependency
if (!listPending.isEmpty() || !listRunning.isEmpty()) {
return true;
}
return false;
}
/**
* Get next test ready to be run. Null = none. Test dependendent on others
* will not be obtained.
*
* @return
*/
private Plugin probeNextPlugin() {
Plugin plugin = null;
int i = 0;
while (i < listPending.size()) {
// ZAP: Removed unnecessary cast.
plugin = listPending.get(i);
if (isAllDependencyCompleted(plugin)) {
return plugin;
}
i++;
}
return null;
}
/**
* Get next plugin ready to be run without any dependency outstanding.
*
* @return new instance of next plugin to be run.
*/
synchronized Plugin nextPlugin() {
if (!init) {
this.reset();
}
Plugin plugin = probeNextPlugin();
if (plugin == null) {
return null;
}
listPending.remove(plugin);
plugin.setTimeStarted();
listRunning.add(plugin);
return plugin;
}
private boolean isAllDependencyCompleted(Plugin plugin) {
// note the plugin object checked may not be the exact plugin object stored in the completed list.
// but the comparison is basing on pluginId (see equals method) so it will work.
String[] dependency = plugin.getDependency();
if (dependency == null || dependency.length == 0) {
return true;
}
synchronized (listCompleted) {
for (int i = 0; i < dependency.length; i++) {
boolean isFound = false;
for (int j = 0; j < listCompleted.size() && !isFound; j++) {
// ZAP: Removed unnecessary cast.
Plugin completed = listCompleted.get(j);
if (completed.getCodeName().equalsIgnoreCase(dependency[i])) {
isFound = true;
}
}
if (!isFound) {
return false;
}
}
}
return true;
}
public void saveTo(Configuration conf) throws ConfigurationException {
for (Plugin plugin : listAllPlugin) {
plugin.saveTo(conf);
}
}
public void loadFrom(Configuration config) throws ConfigurationException {
for (Plugin plugin : listAllPlugin) {
plugin.loadFrom(config);
}
}
synchronized void setRunningPluginCompleted(Plugin plugin) {
if (listRunning.remove(plugin)) {
Plugin completedPlugin = mapAllPlugin.get(plugin.getId());
listCompleted.add(completedPlugin);
completedPlugin.setTimeFinished();
}
}
boolean isRunning(Plugin plugin) {
return listRunning.contains(plugin);
}
int totalPluginToRun() {
return totalPluginToRun;
}
int totalPluginCompleted() {
return listCompleted.size();
}
List<Plugin> getPending() {
return this.listPending;
}
List<Plugin> getRunning() {
return this.listRunning;
}
List<Plugin> getCompleted() {
return this.listCompleted;
}
public int getEnabledPluginCount () {
int count = 0;
for (Plugin plugin : listAllPlugin) {
if (plugin.isEnabled()) {
count ++;
}
}
return count;
}
}