Skip to content

Commit

Permalink
client: Export Client Map
Browse files Browse the repository at this point in the history
Signed-off-by: kingthorin <kingthorin@users.noreply.github.com>
  • Loading branch information
kingthorin committed Jan 7, 2025
1 parent d424fbf commit 1b4cc93
Show file tree
Hide file tree
Showing 6 changed files with 502 additions and 12 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,27 @@ public void sessionModeChanged(Mode mode) {
}
}
}

public ClientMap getClientTree() {
return clientTree;
}

public void exportClientMap(String path) {
File file = new File(path);
Writer fileWriter = null;

try {
fileWriter = new FileWriter(file, false);
} catch (IOException e) {
LOGGER.warn(
"Failed to create file writer for: {}, while exporting Client Map", path, e);
}

try {
ClientMapWriter.exportClientMap(fileWriter, getClientTree());
} catch (IOException e) {
LOGGER.warn("Failure while exporting Client Map: {}", path, e);
}
LOGGER.info("Wrote: {}", file.getAbsolutePath());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/*
* 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.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.parosproxy.paros.control.Control;
import org.zaproxy.addon.client.ExtensionClientIntegration;
import org.zaproxy.addon.client.ui.PopupMenuExportClientMap;
import org.zaproxy.zap.utils.Stats;

public final class ClientMapWriter {

private static final Logger LOGGER = LogManager.getLogger(ClientMapWriter.class);

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 NAME_KEY = "name";
private static final String STORAGE_KEY = "isStorage";
private static final String VISITED_KEY = "visited";

private static ExtensionClientIntegration extension;

ClientMapWriter() {}

public static void exportClientMap(File file) throws IOException {
try (FileWriter fw = new FileWriter(file, false)) {
extension =
Control.getSingleton()
.getExtensionLoader()
.getExtension(ExtensionClientIntegration.class);
exportClientMap(fw);
}
}

public static void exportClientMap(Writer fw) throws IOException {
exportClientMap(fw, extension.getClientTree());
}

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 {
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 and this is much more memory efficient - it still uses jackson for value
// output
String indent = " ".repeat(level * 2);

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

outputKV(fw, indent, false, NAME_KEY, node.getUserObject().getName());
outputKV(fw, indent, false, STORAGE_KEY, node.getUserObject().isStorage());
outputKV(fw, indent, false, VISITED_KEY, node.getUserObject().isVisited());
if (node.getUserObject().getComponents() != null
&& !node.getUserObject().getComponents().isEmpty()) {
indent = outputComponents(fw, node.getUserObject().getComponents(), level, indent);
}

Stats.incCounter(PopupMenuExportClientMap.STATS_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) {
LOGGER.error(e.getMessage(), 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);
boolean first = true;

LinkedHashSet<ClientSideComponent> sortedComponents =
components.stream().sorted().collect(Collectors.toCollection(LinkedHashSet::new));
for (ClientSideComponent component : sortedComponents) {
first = outputKV(fw, indent, first, "typeForDisplay", component.getTypeForDisplay());
first = outputKV(fw, indent, first, "href", component.getHref());
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());
first = outputKV(fw, indent, first, "formId", component.getFormId());
first = outputKV(fw, indent, first, "type", component.getType());
outputKV(fw, indent, first, "isStorageEvent", component.isStorageEvent());
first = true;
}
return indent;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
package org.zaproxy.addon.client.internal;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import lombok.AllArgsConstructor;
Expand All @@ -30,12 +31,13 @@

@Getter
@AllArgsConstructor
public class ClientSideComponent {

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

public static final String CONTENT_LOADED = "ContentLoaded";

private static final List<String> STORAGE_TYPES =
List.of("Cookies", "localStorage", "sessionStorage");
private final Map<String, String> data;

private String tagName;
Expand All @@ -53,21 +55,29 @@ public ClientSideComponent(JSONObject json) {
data.put(key.toString(), json.get(key).toString());
}

this.tagName = json.getString("tagName");
this.id = json.getString("id");
this.parentUrl = json.getString("url");
this.type = json.getString("type");
this.tagName = json.optString("tagName", "");
this.id = json.optString("id", "");
this.parentUrl = json.optString("url", "");
this.type = json.optString("type", "");
if (json.containsKey("href")) {
this.href = json.getString("href");
} else {
this.href = "";
}
if (json.containsKey("text")) {
this.text = json.getString("text").trim();
} else {
this.text = "";
}
if (json.containsKey("tagType")) {
this.tagType = json.getString("tagType").trim();
} else {
this.tagType = "";
}
if (json.containsKey("formId")) {
this.formId = json.getInt("formId");
} else {
this.formId = Integer.MIN_VALUE;
}
}

Expand Down Expand Up @@ -99,12 +109,7 @@ public boolean isStorageEvent() {
if (type == null) {
return false;
}
switch (type) {
case "Cookies", "localStorage", "sessionStorage":
return true;
default:
return false;
}
return STORAGE_TYPES.contains(this.type);
}

@Override
Expand All @@ -124,4 +129,39 @@ public boolean equals(Object obj) {
&& Objects.equals(tagName, other.tagName)
&& Objects.equals(text, other.text);
}

@Override
public int compareTo(ClientSideComponent other) {
int result = this.getTypeForDisplay().compareTo(other.getTypeForDisplay());
if (result != 0) {
return result;
}
// Types are the same, check next relevant property(ies)
result = this.href.compareTo(other.href);
if (result != 0) {
return result;
}
result = this.text.compareTo(other.text);
if (result != 0) {
return result;
}
result = this.id.compareTo(other.id);
if (result != 0) {
return result;
}
result = this.tagName.compareTo(other.tagName);
if (result != 0) {
return result;
}
result = this.tagType.compareTo(other.tagType);
if (result != 0) {
return result;
}
if (this.formId > other.formId) {
return 1;
} else if (this.formId < other.formId) {
return -1;
}
return 0;
}
}
Loading

0 comments on commit 1b4cc93

Please sign in to comment.