From ff2ddce6ce093b60021fc21c4f21bec929d826f6 Mon Sep 17 00:00:00 2001 From: thc202 Date: Mon, 23 Dec 2024 09:34:16 +0000 Subject: [PATCH] client: add element interactions to the spider Fill input elements. Click reported elements. Submit reported forms. Do not show URLs already found in the spider results. Signed-off-by: thc202 --- .../client/ExtensionClientIntegration.java | 10 +- .../addon/client/internal/ClientMap.java | 11 +- .../client/internal/ClientSideComponent.java | 13 ++ .../addon/client/spider/ClientSpider.java | 93 ++++++++++--- .../addon/client/spider/ClientSpiderTask.java | 22 ++-- .../addon/client/spider/SpiderAction.java | 27 ++++ .../client/spider/SpiderScanController.java | 15 ++- .../addon/client/spider/UrlTableModel.java | 6 +- .../spider/actions/BaseElementAction.java | 122 ++++++++++++++++++ .../client/spider/actions/ClickElement.java | 116 +++++++++++++++++ .../addon/client/spider/actions/OpenUrl.java | 42 ++++++ .../client/spider/actions/SubmitForm.java | 76 +++++++++++ .../ExtensionClientIntegrationUnitTest.java | 3 + .../client/spider/UrlTableModelUnitTest.java | 70 ++++++++++ 14 files changed, 593 insertions(+), 33 deletions(-) create mode 100644 addOns/client/src/main/java/org/zaproxy/addon/client/spider/SpiderAction.java create mode 100644 addOns/client/src/main/java/org/zaproxy/addon/client/spider/actions/BaseElementAction.java create mode 100644 addOns/client/src/main/java/org/zaproxy/addon/client/spider/actions/ClickElement.java create mode 100644 addOns/client/src/main/java/org/zaproxy/addon/client/spider/actions/OpenUrl.java create mode 100644 addOns/client/src/main/java/org/zaproxy/addon/client/spider/actions/SubmitForm.java create mode 100644 addOns/client/src/test/java/org/zaproxy/addon/client/spider/UrlTableModelUnitTest.java diff --git a/addOns/client/src/main/java/org/zaproxy/addon/client/ExtensionClientIntegration.java b/addOns/client/src/main/java/org/zaproxy/addon/client/ExtensionClientIntegration.java index 10234feac0..b45ae9d280 100644 --- a/addOns/client/src/main/java/org/zaproxy/addon/client/ExtensionClientIntegration.java +++ b/addOns/client/src/main/java/org/zaproxy/addon/client/ExtensionClientIntegration.java @@ -81,6 +81,7 @@ import org.zaproxy.addon.client.ui.PopupMenuClientHistoryCopy; import org.zaproxy.addon.client.ui.PopupMenuClientOpenInBrowser; import org.zaproxy.addon.client.ui.PopupMenuClientShowInSites; +import org.zaproxy.addon.commonlib.ExtensionCommonlib; import org.zaproxy.addon.network.ExtensionNetwork; import org.zaproxy.zap.ZAP; import org.zaproxy.zap.eventBus.Event; @@ -114,6 +115,7 @@ public class ExtensionClientIntegration extends ExtensionAdaptor { private static final List> EXTENSION_DEPENDENCIES = List.of( ExtensionAlert.class, + ExtensionCommonlib.class, ExtensionHistory.class, ExtensionNetwork.class, ExtensionSelenium.class); @@ -155,7 +157,13 @@ public void initModel(Model model) { new ClientSideDetails( Constant.messages.getString("client.tree.title"), null), this.getModel().getSession())); - spiderScanController = new SpiderScanController(this); + spiderScanController = + new SpiderScanController( + this, + Control.getSingleton() + .getExtensionLoader() + .getExtension(ExtensionCommonlib.class) + .getValueProvider()); passiveScanController = new ClientPassiveScanController(); } diff --git a/addOns/client/src/main/java/org/zaproxy/addon/client/internal/ClientMap.java b/addOns/client/src/main/java/org/zaproxy/addon/client/internal/ClientMap.java index 911f4f2886..8b000eb8dc 100644 --- a/addOns/client/src/main/java/org/zaproxy/addon/client/internal/ClientMap.java +++ b/addOns/client/src/main/java/org/zaproxy/addon/client/internal/ClientMap.java @@ -37,6 +37,7 @@ public class ClientMap extends SortedTreeModel implements EventPublisher { public static final String MAP_NODE_ADDED_EVENT = "client.mapNode.added"; + public static final String MAP_COMPONENT_ADDED_EVENT = "client.mapComponent.added"; public static final String DEPTH_KEY = "depth"; public static final String SIBLINGS_KEY = "siblings"; public static final String URL_KEY = "url"; @@ -48,7 +49,7 @@ public class ClientMap extends SortedTreeModel implements EventPublisher { public ClientMap(ClientNode root) { super(root); this.root = root; - ZAP.getEventBus().registerPublisher(this, MAP_NODE_ADDED_EVENT); + ZAP.getEventBus().registerPublisher(this, MAP_NODE_ADDED_EVENT, MAP_COMPONENT_ADDED_EVENT); } @Override @@ -164,6 +165,13 @@ public boolean addComponentToNode(ClientNode node, ClientSideComponent component boolean componentAdded = details.addComponent(component); if (!wasVisited || componentAdded) { details.setVisited(true); + + Map map = new HashMap<>(component.getData()); + map.put(DEPTH_KEY, Integer.toString(node.getLevel())); + map.put(SIBLINGS_KEY, Integer.toString(node.getChildCount())); + ZAP.getEventBus() + .publishSyncEvent( + this, new Event(this, MAP_COMPONENT_ADDED_EVENT, new Target(), map)); } return componentAdded; } @@ -176,6 +184,7 @@ public ClientNode setRedirect(String originalUrl, String redirectedUrl) { node.getUserObject() .addComponent( new ClientSideComponent( + Map.of(), ClientSideComponent.REDIRECT, null, originalUrl, diff --git a/addOns/client/src/main/java/org/zaproxy/addon/client/internal/ClientSideComponent.java b/addOns/client/src/main/java/org/zaproxy/addon/client/internal/ClientSideComponent.java index ddedaf86be..59097ac842 100644 --- a/addOns/client/src/main/java/org/zaproxy/addon/client/internal/ClientSideComponent.java +++ b/addOns/client/src/main/java/org/zaproxy/addon/client/internal/ClientSideComponent.java @@ -19,6 +19,8 @@ */ package org.zaproxy.addon.client.internal; +import java.util.HashMap; +import java.util.Map; import java.util.Objects; import lombok.AllArgsConstructor; import lombok.Getter; @@ -32,6 +34,8 @@ public class ClientSideComponent { public static String REDIRECT = "Redirect"; + private final Map data; + private String tagName; private String id; private String parentUrl; @@ -42,6 +46,11 @@ public class ClientSideComponent { private int formId = -1; public ClientSideComponent(JSONObject json) { + data = new HashMap<>(); + for (Object key : json.keySet()) { + data.put(key.toString(), json.get(key).toString()); + } + this.tagName = json.getString("tagName"); this.id = json.getString("id"); this.parentUrl = json.getString("url"); @@ -60,6 +69,10 @@ public ClientSideComponent(JSONObject json) { } } + public Map getData() { + return data; + } + public String getTypeForDisplay() { switch (tagName) { case "A": diff --git a/addOns/client/src/main/java/org/zaproxy/addon/client/spider/ClientSpider.java b/addOns/client/src/main/java/org/zaproxy/addon/client/spider/ClientSpider.java index 9e8b59a99a..6c586860d8 100644 --- a/addOns/client/src/main/java/org/zaproxy/addon/client/spider/ClientSpider.java +++ b/addOns/client/src/main/java/org/zaproxy/addon/client/spider/ClientSpider.java @@ -22,6 +22,7 @@ import java.util.ArrayList; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -29,7 +30,10 @@ import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; import javax.swing.table.TableModel; +import org.apache.commons.httpclient.URI; +import org.apache.commons.httpclient.URIException; import org.apache.commons.lang3.time.DurationFormatUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -39,6 +43,10 @@ import org.zaproxy.addon.client.ExtensionClientIntegration; import org.zaproxy.addon.client.internal.ClientMap; import org.zaproxy.addon.client.internal.ClientNode; +import org.zaproxy.addon.client.spider.actions.ClickElement; +import org.zaproxy.addon.client.spider.actions.OpenUrl; +import org.zaproxy.addon.client.spider.actions.SubmitForm; +import org.zaproxy.addon.commonlib.ValueProvider; import org.zaproxy.zap.ZAP; import org.zaproxy.zap.eventBus.Event; import org.zaproxy.zap.eventBus.EventConsumer; @@ -60,8 +68,6 @@ public class ClientSpider implements EventConsumer, GenericScanner2 { * Help pages * * The following features should be implemented in future releases: - * Filling in forms - * Clicking on buttons * Clicking on likely navigation elements * Preventing reqs to out of scope sites (via navigation elements) * Automation framework support @@ -73,6 +79,7 @@ public class ClientSpider implements EventConsumer, GenericScanner2 { private ExecutorService threadPool; + private final ValueProvider valueProvider; private ClientOptions options; private int scanId; private String displayName; @@ -94,7 +101,7 @@ public class ClientSpider implements EventConsumer, GenericScanner2 { private boolean stopped; private int tasksDoneCount; - private int tasksTotalCount; + private final AtomicInteger tasksTotalCount; private UrlTableModel addedNodesModel; private ScanListenner2 listener; @@ -105,13 +112,16 @@ public ClientSpider( String targetUrl, ClientOptions options, int id, - User user) { + User user, + ValueProvider valueProvider) { this.extClient = extClient; this.displayName = displayName; this.targetUrl = targetUrl; this.options = options; this.scanId = id; + this.tasksTotalCount = new AtomicInteger(); this.user = user; + this.valueProvider = valueProvider; this.addedNodesModel = new UrlTableModel(); ZAP.getEventBus().registerConsumer(this, ClientMap.class.getCanonicalName()); @@ -126,7 +136,7 @@ public ClientSpider( String targetUrl, ClientOptions options, int id) { - this(extClient, displayName, targetUrl, options, id, null); + this(extClient, displayName, targetUrl, options, id, null, null); } @Override @@ -152,10 +162,35 @@ public void run() { List unvisitedUrls = getUnvisitedUrls(); - addTask(targetUrl, options.getInitialLoadTimeInSecs()); + addInitialOpenUrlTask(targetUrl); // Add all of the known but unvisited URLs otherwise these will get ignored - unvisitedUrls.forEach(url -> addTask(url, options.getInitialLoadTimeInSecs())); + unvisitedUrls.forEach(this::addInitialOpenUrlTask); + } + + private void addInitialOpenUrlTask(String url) { + addOpenUrlTask(url, options.getInitialLoadTimeInSecs()); + } + + private void addOpenUrlTask(String url, int loadTimeInSecs) { + addTask(openAction(url), loadTimeInSecs); + } + + private List openAction(String url, SpiderAction... additionalActions) { + List actions = new ArrayList<>(5); + actions.add(new OpenUrl(url)); + actions.add(wd -> checkRedirect(url, wd)); + if (additionalActions != null) { + Stream.of(additionalActions).forEach(actions::add); + } + return actions; + } + + private void checkRedirect(String url, WebDriver wd) { + String actualUrl = wd.getCurrentUrl(); + if (!url.equals(actualUrl)) { + setRedirect(url, actualUrl); + } } private List getUnvisitedUrls() { @@ -203,10 +238,10 @@ public void returnWebDriver(WebDriver wd) { } } - private void addTask(String url, int loadTimeInSecs) { - this.tasksTotalCount++; + private void addTask(List actions, int loadTimeInSecs) { + int id = tasksTotalCount.incrementAndGet(); try { - ClientSpiderTask task = new ClientSpiderTask(this, url, loadTimeInSecs); + ClientSpiderTask task = new ClientSpiderTask(id, this, actions, loadTimeInSecs); if (paused) { this.pausedTasks.add(task); } else { @@ -246,12 +281,13 @@ public void eventReceived(Event event) { return; } - String url = event.getParameters().get(ClientMap.URL_KEY); + Map parameters = event.getParameters(); + String url = parameters.get(ClientMap.URL_KEY); if (url.startsWith(targetUrl)) { addUriToAddedNodesModel(url); if (options.getMaxDepth() > 0) { - int depth = Integer.parseInt(event.getParameters().get(ClientMap.DEPTH_KEY)); + int depth = Integer.parseInt(parameters.get(ClientMap.DEPTH_KEY)); if (depth > options.getMaxDepth()) { LOGGER.debug( "Ignoring URL - too deep {} > {} : {}", @@ -262,7 +298,7 @@ public void eventReceived(Event event) { } } if (options.getMaxChildren() > 0) { - int siblings = Integer.parseInt(event.getParameters().get(ClientMap.SIBLINGS_KEY)); + int siblings = Integer.parseInt(parameters.get(ClientMap.SIBLINGS_KEY)); if (siblings > options.getMaxChildren()) { LOGGER.debug( "Ignoring URL - too wide {} > {} : {}", @@ -272,8 +308,33 @@ public void eventReceived(Event event) { return; } } - addTask(url, options.getPageLoadTimeInSecs()); + + if (ClientMap.MAP_COMPONENT_ADDED_EVENT.equals(event.getEventType())) { + if (ClickElement.isSupported(href -> href.startsWith(targetUrl), parameters)) { + addTask( + openAction( + url, + new ClickElement(valueProvider, createURI(url), parameters)), + options.getPageLoadTimeInSecs()); + } else if (SubmitForm.isSupported(parameters)) { + addTask( + openAction( + url, new SubmitForm(valueProvider, createURI(url), parameters)), + options.getPageLoadTimeInSecs()); + } + } else { + addOpenUrlTask(url, options.getPageLoadTimeInSecs()); + } + } + } + + private URI createURI(String value) { + try { + return new URI(value, true); + } catch (URIException | NullPointerException e) { + LOGGER.warn("Failed to create URI from {}", value, e); } + return null; } private void addUriToAddedNodesModel(final String uri) { @@ -292,11 +353,11 @@ protected void setRedirect(String originalUrl, String redirectedUrl) { public int getProgress() { if (finished && !stopped) { return 100; - } else if (this.tasksTotalCount <= 1) { + } else if (tasksTotalCount.get() <= 1) { // Still waiting for the first request to be processed return 0; } - return (this.tasksDoneCount * 100) / this.tasksTotalCount; + return (this.tasksDoneCount * 100) / tasksTotalCount.get(); } @Override diff --git a/addOns/client/src/main/java/org/zaproxy/addon/client/spider/ClientSpiderTask.java b/addOns/client/src/main/java/org/zaproxy/addon/client/spider/ClientSpiderTask.java index c2cd5bb686..b5d3a38112 100644 --- a/addOns/client/src/main/java/org/zaproxy/addon/client/spider/ClientSpiderTask.java +++ b/addOns/client/src/main/java/org/zaproxy/addon/client/spider/ClientSpiderTask.java @@ -20,6 +20,7 @@ package org.zaproxy.addon.client.spider; import java.time.Duration; +import java.util.List; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.openqa.selenium.WebDriver; @@ -28,14 +29,17 @@ public class ClientSpiderTask implements Runnable { private static final Logger LOGGER = LogManager.getLogger(ClientSpiderTask.class); + private final int id; private ClientSpider clientSpider; - private String url; + private List actions; private int timeout; private WebDriver wd; - public ClientSpiderTask(ClientSpider clientSpider, String url, int timeout) { + public ClientSpiderTask( + int id, ClientSpider clientSpider, List actions, int timeout) { + this.id = id; this.clientSpider = clientSpider; - this.url = url; + this.actions = actions; this.timeout = timeout; } @@ -57,21 +61,17 @@ public void run() { wd = this.clientSpider.getWebDriver(); startTime = System.currentTimeMillis(); wd.manage().timeouts().pageLoadTimeout(Duration.ofSeconds(this.timeout)); - wd.get(url); - String actualUrl = wd.getCurrentUrl(); - if (!url.equals(actualUrl)) { - clientSpider.setRedirect(url, actualUrl); - } + actions.forEach(e -> e.run(wd)); ok = true; } catch (Exception e) { - LOGGER.warn("Task failed {} {}", url, e.getMessage(), e); + LOGGER.warn("Task {} failed {}", id, e.getMessage(), e); } if (wd != null) { this.clientSpider.returnWebDriver(wd); } LOGGER.debug( - "Task completed {} {} in {} secs", - url, + "Task {} completed {} in {} secs", + id, ok, (System.currentTimeMillis() - startTime) / 1000); this.clientSpider.postTaskExecution(this); diff --git a/addOns/client/src/main/java/org/zaproxy/addon/client/spider/SpiderAction.java b/addOns/client/src/main/java/org/zaproxy/addon/client/spider/SpiderAction.java new file mode 100644 index 0000000000..0b47c98ed1 --- /dev/null +++ b/addOns/client/src/main/java/org/zaproxy/addon/client/spider/SpiderAction.java @@ -0,0 +1,27 @@ +/* + * 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.client.spider; + +import org.openqa.selenium.WebDriver; + +public interface SpiderAction { + + void run(WebDriver wd); +} diff --git a/addOns/client/src/main/java/org/zaproxy/addon/client/spider/SpiderScanController.java b/addOns/client/src/main/java/org/zaproxy/addon/client/spider/SpiderScanController.java index 3a5139f983..3ee72dfc2b 100644 --- a/addOns/client/src/main/java/org/zaproxy/addon/client/spider/SpiderScanController.java +++ b/addOns/client/src/main/java/org/zaproxy/addon/client/spider/SpiderScanController.java @@ -31,6 +31,7 @@ import org.apache.logging.log4j.Logger; import org.zaproxy.addon.client.ClientOptions; import org.zaproxy.addon.client.ExtensionClientIntegration; +import org.zaproxy.addon.commonlib.ValueProvider; import org.zaproxy.zap.model.ScanController; import org.zaproxy.zap.model.Target; import org.zaproxy.zap.users.User; @@ -41,6 +42,8 @@ public class SpiderScanController implements ScanController { private ExtensionClientIntegration extension; + private final ValueProvider valueProvider; + /** * The {@code Lock} for exclusive access of instance variables related to multiple active scans. * @@ -81,9 +84,10 @@ public class SpiderScanController implements ScanController { */ private List clientSpiderList; - public SpiderScanController(ExtensionClientIntegration extension) { + public SpiderScanController(ExtensionClientIntegration extension, ValueProvider valueProvider) { this.clientSpidersLock = new ReentrantLock(); this.extension = extension; + this.valueProvider = valueProvider; this.clientSpiderMap = new HashMap<>(); this.clientSpiderList = new ArrayList<>(); } @@ -113,7 +117,14 @@ public int startScan(String name, Target target, User user, Object[] contextSpec } ClientSpider scan = - new ClientSpider(extension, name, startUri.toString(), clientOptions, id, user); + new ClientSpider( + extension, + name, + startUri.toString(), + clientOptions, + id, + user, + valueProvider); this.clientSpiderMap.put(id, scan); this.clientSpiderList.add(scan); diff --git a/addOns/client/src/main/java/org/zaproxy/addon/client/spider/UrlTableModel.java b/addOns/client/src/main/java/org/zaproxy/addon/client/spider/UrlTableModel.java index 1c8b5c63d2..44a48ef375 100644 --- a/addOns/client/src/main/java/org/zaproxy/addon/client/spider/UrlTableModel.java +++ b/addOns/client/src/main/java/org/zaproxy/addon/client/spider/UrlTableModel.java @@ -78,8 +78,10 @@ public void removeAllElements() { public void addScanResult(String uri) { UrlScanResult result = new UrlScanResult(uri); - scanResults.add(result); - fireTableRowsInserted(scanResults.size() - 1, scanResults.size() - 1); + if (!scanResults.contains(result)) { + scanResults.add(result); + fireTableRowsInserted(scanResults.size() - 1, scanResults.size() - 1); + } } public void removesScanResult(String uri) { diff --git a/addOns/client/src/main/java/org/zaproxy/addon/client/spider/actions/BaseElementAction.java b/addOns/client/src/main/java/org/zaproxy/addon/client/spider/actions/BaseElementAction.java new file mode 100644 index 0000000000..d9f1ab4213 --- /dev/null +++ b/addOns/client/src/main/java/org/zaproxy/addon/client/spider/actions/BaseElementAction.java @@ -0,0 +1,122 @@ +/* + * 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.client.spider.actions; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import org.apache.commons.httpclient.URI; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.zaproxy.addon.client.spider.SpiderAction; +import org.zaproxy.addon.commonlib.ValueProvider; +import org.zaproxy.zap.utils.Stats; + +abstract class BaseElementAction implements SpiderAction { + + private static final Logger LOGGER = LogManager.getLogger(BaseElementAction.class); + + private final ValueProvider valueProvider; + private final URI uri; + + protected BaseElementAction(ValueProvider valueProvider, URI uri) { + this.valueProvider = Objects.requireNonNull(valueProvider); + this.uri = Objects.requireNonNull(uri); + } + + protected URI getUri() { + return uri; + } + + @Override + public final void run(WebDriver wd) { + String statsPrefix = getStatsPrefix(); + Stats.incCounter(statsPrefix); + + By by = getElementBy(); + if (by == null) { + Stats.incCounter(statsPrefix + ".noby"); + return; + } + + WebElement element; + try { + element = wd.findElement(by); + } catch (Exception e) { + Stats.incCounter(statsPrefix + ".notfound"); + return; + } + + if (!element.isDisplayed()) { + Stats.incCounter(statsPrefix + ".notdisplayed"); + return; + } + + run(wd, element, statsPrefix); + } + + protected abstract String getStatsPrefix(); + + protected abstract By getElementBy(); + + protected abstract void run(WebDriver wd, WebElement element, String statsPrefix); + + protected void fillInputs(List inputs, String action, String statsPrefix) { + inputs.forEach(input -> fillInput(input, action, statsPrefix)); + } + + protected void fillInput(WebElement input, String action, String statsPrefix) { + if (!input.isDisplayed()) { + Stats.incCounter(statsPrefix + ".input.notdisplayed"); + return; + } + + String type = input.getDomAttribute("type"); + if (type == null) { + Stats.incCounter(statsPrefix + ".input.notype"); + return; + } + + String value = + valueProvider.getValue( + uri, + action, + input.getDomAttribute("name"), + input.getDomAttribute("value"), + List.of(), + Map.of(), + Map.of("Control Type", type)); + + try { + input.sendKeys(value); + Stats.incCounter(statsPrefix + ".input." + type + ".sendkeys"); + } catch (Exception e) { + Stats.incCounter(statsPrefix + ".input." + type + ".exception"); + LOGGER.debug("An error occurred while filling form input:", e); + } + } + + protected static String getTagName(Map data) { + return data.get("tagName"); + } +} diff --git a/addOns/client/src/main/java/org/zaproxy/addon/client/spider/actions/ClickElement.java b/addOns/client/src/main/java/org/zaproxy/addon/client/spider/actions/ClickElement.java new file mode 100644 index 0000000000..29fb2148ea --- /dev/null +++ b/addOns/client/src/main/java/org/zaproxy/addon/client/spider/actions/ClickElement.java @@ -0,0 +1,116 @@ +/* + * 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.client.spider.actions; + +import java.util.Map; +import java.util.Objects; +import java.util.function.Predicate; +import org.apache.commons.httpclient.URI; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.zaproxy.addon.commonlib.ValueProvider; +import org.zaproxy.zap.utils.Stats; + +public class ClickElement extends BaseElementAction { + + private static final Logger LOGGER = LogManager.getLogger(ClickElement.class); + + private static final String STATS_PREFIX = "client.spider.click"; + + private final Map elementData; + private final String tagName; + + public ClickElement(ValueProvider valueProvider, URI uri, Map elementData) { + super(valueProvider, uri); + + this.elementData = Objects.requireNonNull(elementData); + tagName = getTagName(elementData); + } + + @Override + public void run(WebDriver wd, WebElement element, String statsPrefix) { + fillInputs(wd.findElements(By.xpath("//input")), getUri().toString(), statsPrefix); + + try { + element.click(); + Stats.incCounter(statsPrefix + ".clicked"); + } catch (Exception e) { + Stats.incCounter(statsPrefix + ".exception"); + LOGGER.debug("An error occurred while clicking the element:", e); + } + } + + @Override + protected By getElementBy() { + return getBy(elementData); + } + + @Override + protected String getStatsPrefix() { + return STATS_PREFIX + ".tag." + tagName; + } + + private static By getBy(Map data) { + String id = data.get("id"); + if (StringUtils.isNotBlank(id)) { + return By.id(id); + } + + String tag = getTagName(data); + String text = data.get("text"); + if ("INPUT".equalsIgnoreCase(tag)) { + return By.xpath("//" + tag + "[@value='" + text + "']"); + } + + if (StringUtils.isNotBlank(text)) { + return By.xpath("//" + tag + "[contains(text(), '" + text + "')]"); + } + + return By.tagName(tag); + } + + public static boolean isSupported(Predicate scopeChecker, Map data) { + String tag = getTagName(data); + if (tag == null) { + return false; + } + + String href = data.get("href"); + if (href != null && !scopeChecker.test(href)) { + return false; + } + + switch (tag) { + case "A", "BUTTON": + return true; + + case "INPUT": + String type = data.get("tagType"); + return "submit".equalsIgnoreCase(type) || "button".equalsIgnoreCase(type); + + default: + return false; + } + } +} diff --git a/addOns/client/src/main/java/org/zaproxy/addon/client/spider/actions/OpenUrl.java b/addOns/client/src/main/java/org/zaproxy/addon/client/spider/actions/OpenUrl.java new file mode 100644 index 0000000000..f9327c2fe8 --- /dev/null +++ b/addOns/client/src/main/java/org/zaproxy/addon/client/spider/actions/OpenUrl.java @@ -0,0 +1,42 @@ +/* + * 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.client.spider.actions; + +import java.util.Objects; +import org.openqa.selenium.WebDriver; +import org.zaproxy.addon.client.spider.SpiderAction; +import org.zaproxy.zap.utils.Stats; + +public class OpenUrl implements SpiderAction { + + private static final String STATS_PREFIX = "client.spider.url"; + + private final String url; + + public OpenUrl(String url) { + this.url = Objects.requireNonNull(url); + } + + @Override + public void run(WebDriver wd) { + Stats.incCounter(STATS_PREFIX); + wd.get(url); + } +} diff --git a/addOns/client/src/main/java/org/zaproxy/addon/client/spider/actions/SubmitForm.java b/addOns/client/src/main/java/org/zaproxy/addon/client/spider/actions/SubmitForm.java new file mode 100644 index 0000000000..8357c06540 --- /dev/null +++ b/addOns/client/src/main/java/org/zaproxy/addon/client/spider/actions/SubmitForm.java @@ -0,0 +1,76 @@ +/* + * 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.client.spider.actions; + +import java.util.Map; +import java.util.Objects; +import org.apache.commons.httpclient.URI; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.zaproxy.addon.commonlib.ValueProvider; +import org.zaproxy.zap.utils.Stats; + +public class SubmitForm extends BaseElementAction { + + private static final Logger LOGGER = LogManager.getLogger(SubmitForm.class); + + private static final String STATS_PREFIX = "client.spider.form"; + + private final String tagName; + private final int formIndex; + + public SubmitForm(ValueProvider valueProvider, URI uri, Map elementData) { + super(valueProvider, uri); + Objects.requireNonNull(elementData); + tagName = getTagName(elementData); + formIndex = Integer.valueOf(elementData.get("formId")); + } + + @Override + public void run(WebDriver wd, WebElement form, String statsPrefix) { + String action = form.getDomAttribute("action"); + fillInputs(form.findElements(By.xpath("//input")), action, statsPrefix); + + try { + form.submit(); + Stats.incCounter(statsPrefix + ".submitted"); + } catch (Exception e) { + Stats.incCounter(statsPrefix + ".exception"); + LOGGER.debug("An error occurred while submitting the form:", e); + } + } + + @Override + protected By getElementBy() { + return By.xpath("(//" + tagName + ")[" + (formIndex + 1) + "]"); + } + + @Override + protected String getStatsPrefix() { + return STATS_PREFIX + "." + formIndex; + } + + public static boolean isSupported(Map data) { + return data.containsKey("formId") && "FORM".equalsIgnoreCase(getTagName(data)); + } +} diff --git a/addOns/client/src/test/java/org/zaproxy/addon/client/ExtensionClientIntegrationUnitTest.java b/addOns/client/src/test/java/org/zaproxy/addon/client/ExtensionClientIntegrationUnitTest.java index f847aa75bf..ad253eea0b 100644 --- a/addOns/client/src/test/java/org/zaproxy/addon/client/ExtensionClientIntegrationUnitTest.java +++ b/addOns/client/src/test/java/org/zaproxy/addon/client/ExtensionClientIntegrationUnitTest.java @@ -44,6 +44,7 @@ import org.parosproxy.paros.model.Model; import org.parosproxy.paros.model.Session; import org.zaproxy.addon.client.spider.ClientSpider; +import org.zaproxy.addon.commonlib.ExtensionCommonlib; import org.zaproxy.zap.extension.selenium.Browser; import org.zaproxy.zap.extension.selenium.ExtensionSelenium; import org.zaproxy.zap.extension.selenium.internal.FirefoxProfileManager; @@ -140,6 +141,8 @@ void shouldStartSpider() throws IOException { ExtensionSelenium extSel = mock(ExtensionSelenium.class); when(extensionLoader.getExtension(ExtensionSelenium.class)).thenReturn(extSel); given(extSel.getProxiedBrowser(anyString(), anyString())).willReturn(mock(WebDriver.class)); + ExtensionCommonlib extCommonLib = mock(ExtensionCommonlib.class); + when(extensionLoader.getExtension(ExtensionCommonlib.class)).thenReturn(extCommonLib); ExtensionClientIntegration extClient = new ExtensionClientIntegration(); extClient.initModel(model); extClient.init(); diff --git a/addOns/client/src/test/java/org/zaproxy/addon/client/spider/UrlTableModelUnitTest.java b/addOns/client/src/test/java/org/zaproxy/addon/client/spider/UrlTableModelUnitTest.java new file mode 100644 index 0000000000..589d27bec6 --- /dev/null +++ b/addOns/client/src/test/java/org/zaproxy/addon/client/spider/UrlTableModelUnitTest.java @@ -0,0 +1,70 @@ +/* + * 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.client.spider; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.hasSize; + +import java.util.Locale; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.parosproxy.paros.Constant; +import org.zaproxy.zap.utils.I18N; + +/** Unit test for {@link UrlTableModel}. */ +class UrlTableModelUnitTest { + + private UrlTableModel model; + + @BeforeAll + static void setupAll() { + Constant.messages = new I18N(Locale.ROOT); + } + + @BeforeEach + void setup() { + model = new UrlTableModel(); + } + + @Test + void shouldAddUrls() { + // Given + String urlA = "https://example.org/path/a"; + String urlB = "https://example.org/path/b"; + // When + model.addScanResult(urlA); + model.addScanResult(urlB); + // Then + assertThat(model.getAddedNodes(), contains(urlA, urlB)); + } + + @Test + void shouldNotAddUrlAlreadyAdded() { + // Given + String url = "https://example.org"; + model.addScanResult(url); + // When + model.addScanResult(url); + // Then + assertThat(model.getAddedNodes(), hasSize(1)); + } +}