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 833b19ffb2d..10234feac00 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 @@ -456,6 +456,15 @@ public boolean addComponentToNode(ClientNode node, ClientSideComponent component return false; } + public boolean setRedirect(String originalUrl, String redirectedUrl) { + ClientNode node = this.clientTree.setRedirect(originalUrl, redirectedUrl); + if (node != null) { + this.clientNodeChanged(node); + return true; + } + return false; + } + public void deleteNodes(List nodes) { this.clientTree.deleteNodes(nodes); if (View.isInitialised()) { 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 beee0c02e6b..911f4f28862 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 @@ -167,6 +167,28 @@ public boolean addComponentToNode(ClientNode node, ClientSideComponent component } return componentAdded; } + + public ClientNode setRedirect(String originalUrl, String redirectedUrl) { + ClientNode node = getNode(originalUrl, false, false); + if (node != null) { + node.getUserObject().setRedirect(true); + node.getUserObject().setVisited(true); + node.getUserObject() + .addComponent( + new ClientSideComponent( + ClientSideComponent.REDIRECT, + null, + originalUrl, + redirectedUrl, + ClientSideComponent.REDIRECT, + null, + null, + -1)); + return node; + } + LOGGER.debug("setRedirect, no node for URL {}", originalUrl); + return null; + } } /** 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 8abfcf8fb88..ddedaf86beb 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 @@ -20,12 +20,18 @@ package org.zaproxy.addon.client.internal; import java.util.Objects; +import lombok.AllArgsConstructor; +import lombok.Getter; import net.sf.json.JSONObject; import org.parosproxy.paros.Constant; import org.zaproxy.addon.client.ExtensionClientIntegration; +@Getter +@AllArgsConstructor public class ClientSideComponent { + public static String REDIRECT = "Redirect"; + private String tagName; private String id; private String parentUrl; @@ -74,31 +80,10 @@ public String getTypeForDisplay() { } } - public String getTagName() { - return tagName; - } - - public String getId() { - return id; - } - - public String getParentUrl() { - return parentUrl; - } - - public String getHref() { - return href; - } - - public String getText() { - return text; - } - - public String getType() { - return type; - } - public boolean isStorageEvent() { + if (type == null) { + return false; + } switch (type) { case "Cookies", "localStorage", "sessionStorage": return true; @@ -107,14 +92,6 @@ public boolean isStorageEvent() { } } - public String getTagType() { - return tagType; - } - - public int getFormId() { - return formId; - } - @Override public int hashCode() { return Objects.hash(href, id, parentUrl, tagName, text); diff --git a/addOns/client/src/main/java/org/zaproxy/addon/client/internal/ClientSideDetails.java b/addOns/client/src/main/java/org/zaproxy/addon/client/internal/ClientSideDetails.java index 95720ff6cc6..9427917bc82 100644 --- a/addOns/client/src/main/java/org/zaproxy/addon/client/internal/ClientSideDetails.java +++ b/addOns/client/src/main/java/org/zaproxy/addon/client/internal/ClientSideDetails.java @@ -27,6 +27,7 @@ public class ClientSideDetails { private String url; private boolean visited; private boolean storage; + private boolean redirect; private Set components = new HashSet<>(); @@ -72,4 +73,12 @@ public boolean isStorage() { protected void setStorage(boolean storage) { this.storage = storage; } + + public boolean isRedirect() { + return redirect; + } + + public void setRedirect(boolean redirect) { + this.redirect = redirect; + } } 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 9a962ae5a08..9e8b59a99ab 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 @@ -284,6 +284,10 @@ private void addUriToAddedNodesModel(final String uri) { }); } + protected void setRedirect(String originalUrl, String redirectedUrl) { + ThreadUtils.invokeLater(() -> extClient.setRedirect(originalUrl, redirectedUrl)); + } + @Override public int getProgress() { if (finished && !stopped) { 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 01d3cdc3094..c2cd5bb6867 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 @@ -58,6 +58,10 @@ public void run() { 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); + } ok = true; } catch (Exception e) { LOGGER.warn("Task failed {} {}", url, e.getMessage(), e); diff --git a/addOns/client/src/main/java/org/zaproxy/addon/client/ui/ClientMapTreeCellRenderer.java b/addOns/client/src/main/java/org/zaproxy/addon/client/ui/ClientMapTreeCellRenderer.java index f6a58a2e47e..13481438f94 100644 --- a/addOns/client/src/main/java/org/zaproxy/addon/client/ui/ClientMapTreeCellRenderer.java +++ b/addOns/client/src/main/java/org/zaproxy/addon/client/ui/ClientMapTreeCellRenderer.java @@ -38,16 +38,16 @@ public class ClientMapTreeCellRenderer extends DefaultTreeCellRenderer { ExtensionClientIntegration.getIcon("sitemap-application-blue.png"); private static final ImageIcon LEAF_ICON = ExtensionClientIntegration.getIcon("blue-document.png"); - private static final ImageIcon LEAF_NOT_VISITED_ICON = - ExtensionClientIntegration.getIcon("blue-document--minus.png"); private static final ImageIcon FRAGMENT_ICON = ExtensionClientIntegration.getIcon("blue-document-number.png"); - private static final ImageIcon FRAGMENT_NOT_VISITED_ICON = - ExtensionClientIntegration.getIcon("blue-document-number-minus.png"); private static final ImageIcon FOLDER_OPEN_ICON = ExtensionClientIntegration.getIcon("blue-folder-horizontal-open.png"); private static final ImageIcon FOLDER_CLOSED_ICON = ExtensionClientIntegration.getIcon("blue-folder-horizontal.png"); + private static final ImageIcon NOT_VISITED_OVERLAY = + ExtensionClientIntegration.getIcon("overlay-minus.png"); + private static final ImageIcon REDIRECT_OVERLAY = + ExtensionClientIntegration.getIcon("overlay-redirect.png"); private static final ImageIcon DATABASE_ICON = ExtensionClientIntegration.getIcon("database.png"); @@ -83,27 +83,23 @@ public Component getTreeCellRendererComponent( setPreferredSize(null); // clears the preferred size, making the node visible super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus); - // folder / file icons with scope 'target' if relevant if (node.isRoot()) { - component.add(wrap(ROOT_ICON)); // 'World' icon + component.add(wrap(ROOT_ICON)); } else { ClientSideDetails csd = node.getUserObject(); OverlayIcon icon; if (csd.isStorage()) { icon = new OverlayIcon(DATABASE_ICON); } else if (leaf) { - if (csd.isVisited()) { - if (csd.getUrl().contains("#")) { - icon = new OverlayIcon(FRAGMENT_ICON); - } else { - icon = new OverlayIcon(LEAF_ICON); - } + if (csd.getUrl().contains("#")) { + icon = new OverlayIcon(FRAGMENT_ICON); } else { - if (csd.getUrl().contains("#")) { - icon = new OverlayIcon(FRAGMENT_NOT_VISITED_ICON); - } else { - icon = new OverlayIcon(LEAF_NOT_VISITED_ICON); - } + icon = new OverlayIcon(LEAF_ICON); + } + if (!csd.isVisited()) { + icon.add(NOT_VISITED_OVERLAY); + } else if (csd.isRedirect()) { + icon.add(REDIRECT_OVERLAY); } } else { if (expanded) { diff --git a/addOns/client/src/main/resources/org/zaproxy/addon/client/resources/blue-document--minus.png b/addOns/client/src/main/resources/org/zaproxy/addon/client/resources/blue-document--minus.png deleted file mode 100644 index adcb19c6d56..00000000000 Binary files a/addOns/client/src/main/resources/org/zaproxy/addon/client/resources/blue-document--minus.png and /dev/null differ diff --git a/addOns/client/src/main/resources/org/zaproxy/addon/client/resources/blue-document-number-minus.png b/addOns/client/src/main/resources/org/zaproxy/addon/client/resources/blue-document-number-minus.png deleted file mode 100644 index 89b7fbd5200..00000000000 Binary files a/addOns/client/src/main/resources/org/zaproxy/addon/client/resources/blue-document-number-minus.png and /dev/null differ diff --git a/addOns/client/src/main/resources/org/zaproxy/addon/client/resources/overlay-minus.png b/addOns/client/src/main/resources/org/zaproxy/addon/client/resources/overlay-minus.png new file mode 100644 index 00000000000..336956cd3d5 Binary files /dev/null and b/addOns/client/src/main/resources/org/zaproxy/addon/client/resources/overlay-minus.png differ diff --git a/addOns/client/src/main/resources/org/zaproxy/addon/client/resources/overlay-redirect.png b/addOns/client/src/main/resources/org/zaproxy/addon/client/resources/overlay-redirect.png new file mode 100644 index 00000000000..b8671212b30 Binary files /dev/null and b/addOns/client/src/main/resources/org/zaproxy/addon/client/resources/overlay-redirect.png differ diff --git a/addOns/client/src/test/java/org/zaproxy/addon/client/internal/ClientMapUnitTest.java b/addOns/client/src/test/java/org/zaproxy/addon/client/internal/ClientMapUnitTest.java index b69ec433b31..1aea7f2f668 100644 --- a/addOns/client/src/test/java/org/zaproxy/addon/client/internal/ClientMapUnitTest.java +++ b/addOns/client/src/test/java/org/zaproxy/addon/client/internal/ClientMapUnitTest.java @@ -21,12 +21,14 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; +import java.util.Set; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -336,4 +338,32 @@ void shouldClearTheMap() { // Then assertThat(map.getRoot().getChildCount(), is(0)); } + + @Test + void shouldSetKnownRedirectDetails() { + // Given + ClientNode node1 = map.getOrAddNode(BBB_DDD_URL + "/", false, false); + + // When + ClientNode node2 = map.setRedirect(BBB_DDD_URL + "/", AAA_URL); + Set components = node2.getUserObject().getComponents(); + ClientSideComponent c0 = components.iterator().next(); + + // Then + assertThat(node1, is(node2)); + assertThat(node2.getUserObject().isVisited(), is(true)); + assertThat(node2.getUserObject().isRedirect(), is(true)); + assertThat(components.size(), is(1)); + assertThat(c0.getTagName(), is("Redirect")); + assertThat(c0.getHref(), is(AAA_URL)); + } + + @Test + void shouldIngnoreUnknownRedirectDetails() { + // Given / When + ClientNode node = map.setRedirect(BBB_DDD_URL + "/", AAA_URL); + + // Then + assertThat(node, is(nullValue())); + } }