Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion addOns/client/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ All notable changes to this add-on will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## Unreleased

### Changed
- Use new functionality from the browser extension for crawling.

## [0.28.0] - 2026-06-26
### Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,13 +236,15 @@ private boolean addComponentToNode(ClientNode node, ClientSideComponent componen
if (!wasVisited || componentAdded) {
details.setVisited(true);

int depth = node.getLevel();
int siblings = node.getChildCount();
Map<String, String> map = new HashMap<>(component.getData());
map.put(DEPTH_KEY, Integer.toString(node.getLevel()));
map.put(SIBLINGS_KEY, Integer.toString(node.getChildCount()));
map.put(DEPTH_KEY, Integer.toString(depth));
map.put(SIBLINGS_KEY, Integer.toString(siblings));
ZAP.getEventBus()
.publishSyncEvent(
this, new Event(this, MAP_COMPONENT_ADDED_EVENT, new Target(), map));
listeners.forEach(l -> l.componentAdded(map, source));
listeners.forEach(l -> l.componentAdded(component, depth, siblings, source));
notifyNodeChanged(node);
}
return componentAdded;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@
*/
package org.zaproxy.addon.client.internal;

import java.util.Map;

/** Listener notified when nodes or components are added to a {@link ClientMap}. */
public interface ClientMapListener {

Expand All @@ -38,11 +36,13 @@ public interface ClientMapListener {
/**
* Called when a component is added to a node in the map.
*
* @param parameters the component data parameters.
* @param component the component that was added.
* @param depth the depth of the node in the map.
* @param siblings the sibling count of the node (including itself) after insertion.
* @param source an identifier for the source that triggered the addition, or {@code 0} if the
* source is unknown.
*/
void componentAdded(Map<String, String> parameters, int source);
void componentAdded(ClientSideComponent component, int depth, int siblings, int source);

/**
* Called when a page-load event is reported for a URL.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,15 @@
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import lombok.AllArgsConstructor;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
import net.sf.json.JSONObject;
import org.openqa.selenium.By;
import org.parosproxy.paros.Constant;
import org.zaproxy.addon.client.ExtensionClientIntegration;

@Getter
@AllArgsConstructor
public class ClientSideComponent implements Comparable<ClientSideComponent> {

public static final String REDIRECT = "Redirect";
Expand Down Expand Up @@ -148,10 +147,14 @@ public static Type getTypeForKey(String key) {
private String parentUrl;
private String href;
private String text;
@NonNull private Type type;
private Type type;
private String tagType;
private int formId;
@Setter private InteractableState interactable;
@Setter private ElementLocator elementLocator;

@Getter(AccessLevel.NONE)
private By cachedBy;

public ClientSideComponent(
Map<String, String> data,
Expand All @@ -163,7 +166,29 @@ public ClientSideComponent(
Type type,
String tagType,
int formId) {
this(data, tagName, id, parentUrl, href, text, type, tagType, formId, null);
this.data = data;
this.tagName = tagName;
this.id = id;
this.parentUrl = parentUrl;
this.href = href;
this.text = text;
this.type = Objects.requireNonNull(type);
this.tagType = tagType;
this.formId = formId;
}

public By getBy() {
if (cachedBy == null && elementLocator != null) {
cachedBy =
switch (elementLocator.type()) {
case "id" -> By.id(elementLocator.element());
case "className" -> By.className(elementLocator.element());
case "cssSelector" -> By.cssSelector(elementLocator.element());
case "xpath" -> By.xpath(elementLocator.element());
default -> null;
};
}
return cachedBy;
}

public ClientSideComponent(JSONObject json) {
Expand Down Expand Up @@ -196,6 +221,9 @@ public ClientSideComponent(JSONObject json) {
s.optBoolean("enabled", false),
s.optBoolean("pointer", false));
}
if (json.containsKey("elementLocator") && !json.get("elementLocator").equals("null")) {
this.elementLocator = ElementLocator.fromJson(json.getJSONObject("elementLocator"));
}
}

public Map<String, String> getData() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Zed Attack Proxy (ZAP) and its related class files.
*
* ZAP is an HTTP/HTTPS proxy for assessing web application security.
*
* Copyright 2026 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.internal;

import net.sf.json.JSONObject;

public record ElementLocator(String type, String element) {

public static ElementLocator fromJson(JSONObject json) {
return new ElementLocator(json.getString("type"), json.getString("element"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
*/
package org.zaproxy.addon.client.spider;

import java.util.Map;
import org.zaproxy.addon.client.internal.ClientMapListener;
import org.zaproxy.addon.client.internal.ClientSideComponent;
import org.zaproxy.addon.client.spider.ClientSpider.WebDriverProcess;

public interface ActionWaitStrategy extends ClientMapListener {
Expand All @@ -39,5 +39,6 @@ default void onRequestCompleted(String url) {}
default void nodeAdded(String url, int depth, int siblings, int source) {}

@Override
default void componentAdded(Map<String, String> parameters, int source) {}
default void componentAdded(
ClientSideComponent component, int depth, int siblings, int source) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
Expand All @@ -40,7 +39,6 @@
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.IntSupplier;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.Stream;
Expand Down Expand Up @@ -371,9 +369,8 @@ private void addExistingTasks(ClientNode node) {
&& isUrlInScope(nodeUrl)) {
addFollowGraphTask(nodeUrl);
for (ClientSideComponent component : details.getComponents()) {
Map<String, String> data = component.getData();
if (SubmitForm.isSupported(data)) {
addSubmitTask(nodeUrl, data);
if (SubmitForm.isSupported(component)) {
addSubmitTask(nodeUrl, component);
}
}
}
Expand Down Expand Up @@ -408,12 +405,13 @@ public void returnWebDriverProcess(WebDriverProcess wdp) {
}
}

private void addSubmitTask(String nodeUrl, Map<String, String> data) {
private void addSubmitTask(String nodeUrl, ClientSideComponent component) {
addTask(
nodeUrl,
followGraphAction(nodeUrl, new SubmitForm(valueProvider, createUri(nodeUrl), data)),
followGraphAction(
nodeUrl, new SubmitForm(valueProvider, createUri(nodeUrl), component)),
Constant.messages.getString("client.spider.panel.table.action.submit"),
paramsToString(data));
paramsToString(component));
}

private ClientSpiderTask addTask(
Expand Down Expand Up @@ -490,8 +488,7 @@ private class ClientMapListenerImpl implements ClientMapListener {
private static final Pattern SCHEME_PATTERN =
Pattern.compile("^https?://", Pattern.CASE_INSENSITIVE);

private boolean shouldIgnore(
String url, int source, IntSupplier depthSupplier, IntSupplier childrenSupplier) {
private boolean shouldIgnore(String url, int source, int depth, int siblings) {
if (stopping.get() || stopped || !proxyPorts.contains(source)) {
return true;
}
Expand All @@ -505,7 +502,6 @@ private boolean shouldIgnore(
}

if (options.getMaxDepth() > 0) {
int depth = depthSupplier.getAsInt();
if (depth > options.getMaxDepth()) {
LOGGER.debug(
"Ignoring URL - too deep {} > {} : {}",
Expand All @@ -518,7 +514,6 @@ private boolean shouldIgnore(
}

if (options.getMaxChildren() > 0) {
int siblings = childrenSupplier.getAsInt();
if (siblings > options.getMaxChildren()) {
LOGGER.debug(
"Ignoring URL - too wide {} > {} : {}",
Expand All @@ -545,21 +540,21 @@ public void nodeAdded(String url, int depth, int siblings, int source) {
if (scanOptions.isExistingOnly()) {
return;
}
if (shouldIgnore(url, source, () -> depth, () -> siblings)) {
if (shouldIgnore(url, source, depth, siblings)) {
return;
}

Stats.incCounter("stats.client.spider.event.url");
addDiscoveredUrl(url);
}

private boolean isHrefAlreadyHandled(Map<String, String> parameters) {
String href = parameters.get("href");
private boolean isHrefAlreadyHandled(ClientSideComponent component) {
String href = component.getHref();
if (href == null || !SCHEME_PATTERN.matcher(href).find()) {
return false;
}

String sourceUrl = parameters.get(ClientMap.URL_KEY);
String sourceUrl = component.getParentUrl();
if (sourceUrl.equals(href)) {
return true;
}
Expand All @@ -579,34 +574,31 @@ private boolean isHrefAlreadyHandled(Map<String, String> parameters) {
}

@Override
public void componentAdded(Map<String, String> parameters, int source) {
public void componentAdded(
ClientSideComponent component, int depth, int siblings, int source) {
if (scanOptions.isExistingOnly()) {
return;
}
String url = parameters.get(ClientMap.URL_KEY);
if (shouldIgnore(
url,
source,
() -> Integer.parseInt(parameters.get(ClientMap.DEPTH_KEY)),
() -> Integer.parseInt(parameters.get(ClientMap.SIBLINGS_KEY)))) {
String url = component.getParentUrl();
if (shouldIgnore(url, source, depth, siblings)) {
return;
}

Stats.incCounter("stats.client.spider.event.component");
if (ClickElement.isSupported(ClientSpider.this::isUrlInScope, parameters)
&& !(options.isLogoutAvoidance() && isLogoutElement(parameters))
&& !isHrefAlreadyHandled(parameters)) {
if (ClickElement.isSupported(ClientSpider.this::isUrlInScope, component)
&& !(options.isLogoutAvoidance() && isLogoutElement(component))
&& !isHrefAlreadyHandled(component)) {
Stats.incCounter("stats.client.spider.event.component.click");
addTask(
url,
followGraphAction(
url,
new ClickElement(valueProvider, createUri(url), parameters, false)),
new ClickElement(valueProvider, createUri(url), component, false)),
Constant.messages.getString("client.spider.panel.table.action.click"),
paramsToString(parameters));
} else if (SubmitForm.isSupported(parameters)) {
paramsToString(component));
} else if (SubmitForm.isSupported(component)) {
Stats.incCounter("stats.client.spider.event.component.form");
addSubmitTask(url, parameters);
addSubmitTask(url, component);
}
}
}
Expand Down Expand Up @@ -666,30 +658,30 @@ protected ResourceState checkResourceState(URI uri, String hostName, boolean all
return state;
}

private static boolean isLogoutElement(Map<String, String> parameters) {
String text = parameters.get("text");
private static boolean isLogoutElement(ClientSideComponent component) {
String text = component.getText();
if (text == null || text.isBlank()) {
return false;
}
String normalized = text.toLowerCase(Locale.ROOT).replaceAll("[ -]", "");
return AuthConstants.getLogoutIndicators().stream().anyMatch(normalized::contains);
}

private static String paramsToString(Map<String, String> parameters) {
String tag = parameters.get("tagName");
private static String paramsToString(ClientSideComponent component) {
String tag = component.getTagName();
if (tag != null) {
switch (tag) {
case "A":
return Constant.messages.getString(
"client.spider.panel.table.details.link",
parameters.get("href"),
parameters.get("text"));
component.getHref(),
component.getText());
case "BUTTON":
return Constant.messages.getString(
"client.spider.panel.table.details.button", parameters.get("text"));
"client.spider.panel.table.details.button", component.getText());
}
}
return parameters.toString();
return component.getData().toString();
}

private static URI createUri(String value) {
Expand Down
Loading
Loading