Skip to content

Commit

Permalink
client: Export Client Map
Browse files Browse the repository at this point in the history
Signed-off-by: kingthorin <[email protected]>
  • Loading branch information
kingthorin committed Jan 9, 2025
1 parent f47474c commit d477b57
Show file tree
Hide file tree
Showing 8 changed files with 889 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.Writer;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
Expand Down Expand Up @@ -57,6 +59,7 @@
import org.parosproxy.paros.view.View;
import org.zaproxy.addon.client.impl.ClientZestRecorder;
import org.zaproxy.addon.client.internal.ClientMap;
import org.zaproxy.addon.client.internal.ClientMapWriter;
import org.zaproxy.addon.client.internal.ClientNode;
import org.zaproxy.addon.client.internal.ClientSideComponent;
import org.zaproxy.addon.client.internal.ClientSideDetails;
Expand All @@ -82,6 +85,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.client.ui.PopupMenuExportClientMap;
import org.zaproxy.addon.commonlib.ExtensionCommonlib;
import org.zaproxy.addon.network.ExtensionNetwork;
import org.zaproxy.zap.ZAP;
Expand Down Expand Up @@ -291,6 +295,13 @@ public void hook(ExtensionHook extensionHook) {
.getMainFrame()
.getMainFooterPanel()
.addFooterToolbarRightComponent(pscanStatus.getCountLabel());

extensionHook
.getHookMenu()
.addPopupMenuItem(
new PopupMenuExportClientMap(
Constant.messages.getString("client.tree.popup.export.menu"),
this));
}
}

Expand Down Expand Up @@ -863,4 +874,19 @@ public void sessionModeChanged(Mode mode) {
}
}
}

public void exportClientMap(File file) {
try (Writer fileWriter = new FileWriter(file, false)) {
ClientMapWriter.exportClientMap(fileWriter, clientTree);
} catch (IOException e) {
LOGGER.warn(
"Failed to create file writer for: {}, while exporting Client Map",
file.getAbsolutePath(),
e);
}
}

public void exportClientMap(String path) {
this.exportClientMap(new File(path));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
/*
* Zed Attack Proxy (ZAP) and its related class files.
*
* ZAP is an HTTP/HTTPS proxy for assessing web application security.
*
* Copyright 2025 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 com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator.Feature;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.io.Writer;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.stream.Collectors;
import org.parosproxy.paros.control.Control;
import org.zaproxy.addon.client.ExtensionClientIntegration;
import org.zaproxy.zap.utils.Stats;

public final class ClientMapWriter {

private static final String NODE_KEY = "node";
private static final String ROOT_NODE_NAME = "ClientMap";
private static final String CHILDREN_KEY = "children";
private static final String STORAGE_KEY = "storage";
private static final String VISITED_KEY = "visited";

private static final List<String> COMPONENT_TYPES_TO_SKIP_LC =
List.of(
ClientSideComponent.REDIRECT.toLowerCase(Locale.ROOT),
ClientSideComponent.CONTENT_LOADED.toLowerCase(Locale.ROOT));

ClientMapWriter() {}

public static void exportClientMap(File file) throws IOException {
Control.getSingleton()
.getExtensionLoader()
.getExtension(ExtensionClientIntegration.class)
.exportClientMap(file);
}

public static void exportClientMap(Writer fw, ClientMap clientMap) throws IOException {
try (BufferedWriter bw = new BufferedWriter(fw)) {
outputNode(bw, clientMap.getRoot(), 0);
}
}

private static boolean outputKV(
Writer fw, String indent, boolean first, String key, Object value) throws IOException {
if (value == null) {
return first;
}
fw.write(indent);
if (first) {
fw.write("- ");
} else {
fw.write(" ");
}
fw.write(key);
fw.write(": ");
ObjectMapper mapper =
new ObjectMapper(
new YAMLFactory()
.enable(Feature.LITERAL_BLOCK_STYLE)
.disable(Feature.WRITE_DOC_START_MARKER));
// For some reason the disable start marker doesn't seem to work
String output = mapper.writeValueAsString(value).replace("--- ", "");
fw.write(output);
return false;
}

private static void outputNode(Writer fw, ClientNode node, int level) throws IOException {
if (node.isStorage()) {
// Skip storage nodes in the tree
// Those details are represented as components of their source
return;
}
// We could create a set of data structures and use jackson, but the format is very
// simple - it still uses jackson for value output
String indent = " ".repeat(level * 2);

outputKV(
fw,
indent,
true,
NODE_KEY,
level == 0 ? ROOT_NODE_NAME : node.getUserObject().getName());

if (node.getUserObject().isStorage()) {
outputKV(fw, indent, false, STORAGE_KEY, node.getUserObject().isStorage());
}
if (!node.getUserObject().isVisited()) {
outputKV(fw, indent, false, VISITED_KEY, node.getUserObject().isVisited());
}
if (node.getUserObject().getComponents() != null
&& !node.getUserObject().getComponents().isEmpty()) {
for (ClientSideComponent component : node.getUserObject().getComponents()) {
if (component.getTypeForDisplay().equalsIgnoreCase(ClientSideComponent.REDIRECT)) {
outputKV(
fw,
indent,
false,
ClientSideComponent.REDIRECT.toLowerCase(Locale.ROOT),
component.getHref());
} else if (component
.getTypeForDisplay()
.equalsIgnoreCase(ClientSideComponent.CONTENT_LOADED)) {
outputKV(fw, indent, false, "contentLoaded", true);
}
}
indent = outputComponents(fw, node.getUserObject().getComponents(), level, indent);
}

Stats.incCounter(ExtensionClientIntegration.PREFIX + ".export.clientmap.node");

if (node.getChildCount() > 0) {
fw.write(indent);
fw.write(" ");
fw.write(CHILDREN_KEY);
fw.write(":");
fw.write('\n');
node.children()
.asIterator()
.forEachRemaining(
c -> {
try {
outputNode(fw, (ClientNode) c, level + 1);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
}
}

private static String outputComponents(
Writer fw, Set<ClientSideComponent> components, int level, String indent)
throws IOException {
fw.write(indent + " ");
fw.write("components:\n");
indent = " ".repeat((level + 1) * 2);

LinkedHashSet<ClientSideComponent> sortedComponents =
components.stream().sorted().collect(Collectors.toCollection(LinkedHashSet::new));
for (ClientSideComponent component : sortedComponents) {
boolean first = true;
if ((component.getTypeForDisplay() != null
&& COMPONENT_TYPES_TO_SKIP_LC.contains(
component.getTypeForDisplay().toLowerCase(Locale.ROOT)))) {
continue;
}
first = outputKV(fw, indent, first, "nodeType", component.getTypeForDisplay());
String href =
component.getHref() == null ? component.getParentUrl() : component.getHref();
first = outputKV(fw, indent, first, "href", href);
if (!component.isStorageEvent()) {
first = outputKV(fw, indent, first, "text", component.getText());
}
first = outputKV(fw, indent, first, "id", component.getId());
first = outputKV(fw, indent, first, "tagName", component.getTagName());
first = outputKV(fw, indent, first, "tagType", component.getTagType());
if (component.getFormId() != -1) {
first = outputKV(fw, indent, first, "formId", component.getFormId());
}
if (component.isStorageEvent()) {
outputKV(fw, indent, first, "storageEvent", component.isStorageEvent());
}
}
return indent;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@

@Getter
@AllArgsConstructor
public class ClientSideComponent {

public class ClientSideComponent implements Comparable<ClientSideComponent> {
public static final String REDIRECT = "Redirect";

public static final String CONTENT_LOADED = "ContentLoaded";
Expand Down Expand Up @@ -124,4 +123,54 @@ public boolean equals(Object obj) {
&& Objects.equals(tagName, other.tagName)
&& Objects.equals(text, other.text);
}

@Override
public int compareTo(ClientSideComponent other) {
int result = stringCompare(this.getTypeForDisplay(), other.getTypeForDisplay());
if (result != 0) {
return result;
}
result = stringCompare(this.href, other.href);
if (result != 0) {
return result;
}
result = stringCompare(this.text, other.text);
if (result != 0) {
return result;
}
result = stringCompare(this.id, other.id);
if (result != 0) {
return result;
}
result = stringCompare(this.tagName, other.tagName);
if (result != 0) {
return result;
}
result = stringCompare(this.tagType, other.tagType);
if (result != 0) {
return result;
}
result = Integer.compare(this.formId, other.formId);
if (result != 0) {
return result;
}
return 0;
}

private static int stringCompare(String here, String other) {
if (here == null || other == null) {
return nullCompare(here, other);
}
return here.compareTo(other);
}

private static int nullCompare(Object here, Object other) {
if (here == other) {
return 0;
}
if (here == null) {
return -1;
}
return 1;
}
}
Loading

0 comments on commit d477b57

Please sign in to comment.