Skip to content

Commit

Permalink
Export the ZAP site tree as a JSON file (#229)
Browse files Browse the repository at this point in the history
Export the ZAP site tree as a JSON file

See:  #229
  • Loading branch information
ccronca authored Oct 29, 2024
1 parent ee14d55 commit edf666b
Show file tree
Hide file tree
Showing 4 changed files with 216 additions and 1 deletion.
66 changes: 66 additions & 0 deletions scanners/zap/scripts/export-site-tree.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
* Script to traverse the site tree and export node information to a JSON file
*
* This script retrieves the root of the site tree from the current ZAP session,
* traverses each child node, and collects relevant information such as node name,
* HTTP method, and status code. The collected data is then written to a JSON file
* named 'zap-site-tree.json' in the session's results directory
*/

var File = Java.type('java.io.File');
var FileWriter = Java.type('java.io.FileWriter');
var BufferedWriter = Java.type('java.io.BufferedWriter');

const defaultFileName = "zap-site-tree.json";

try {
var fileName = org.zaproxy.zap.extension.script.ScriptVars.getGlobalVar('siteTreeFileName') || defaultFileName;

} catch (e) {
var fileName = defaultFileName;
print("Error retrieving 'siteTreeFileName': " + e.message + ". Using default value: '" + defaultFileName);
}

function listChildren(node, resultList) {
for (var j = 0; j < node.getChildCount(); j++) {
listChildren(node.getChildAt(j), resultList);
}

if (node.getChildCount() == 0) {
var href = node.getHistoryReference();
var nodeInfo = {};
nodeInfo["name"] = node.getHierarchicNodeName();

if (href != null) {
nodeInfo["method"] = href.getMethod();
nodeInfo["status"] = href.getStatusCode();
} else {
nodeInfo["method"] = "No History Reference";
nodeInfo["status"] = "No History Reference";
}

resultList.push(nodeInfo);
}
}

try {
var root = model.getSession().getSiteTree().getRoot();
var resultList = [];

listChildren(root, resultList);

var jsonOutput = JSON.stringify(resultList, null, 4);

var defaultResultsDir = model.getSession().getSessionFolder();
var outputFilePath = new File(defaultResultsDir, fileName).getAbsolutePath();

var file = new File(outputFilePath);
var writer = new BufferedWriter(new FileWriter(file));
writer.write(jsonOutput);
writer.close();

print("Site tree data has been written to: " + outputFilePath);

} catch (e) {
print("An error occurred: " + e);
}
69 changes: 68 additions & 1 deletion scanners/zap/zap.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# pylint: disable=too-many-lines
import glob
import logging
import os
Expand Down Expand Up @@ -33,6 +34,8 @@ class Zap(RapidastScanner):

REPORTS_SUBDIR = "reports"

SITE_TREE_FILENAME = "zap-site-tree.json"

## FUNCTIONS
def __init__(self, config, ident):
logging.debug("Initializing ZAP scanner")
Expand Down Expand Up @@ -98,9 +101,25 @@ def postprocess(self):
# log path is like '/tmp/rapidast_*/zap.log'
tar.add(log, f"evidences/zap_logs/{log.split('/')[-1]}")

# Calling parent RapidastScanner postprocess
self._copy_site_tree()

super().postprocess()

def _copy_site_tree(self):
"""
Copies the site tree JSON file from the host working directory to the results directory.
"""
site_tree_path = os.path.join(self.host_work_dir, f"session_data/{self.SITE_TREE_FILENAME}")

if os.path.exists(site_tree_path):
try:
logging.info(f"Copying site tree from {site_tree_path} to {self.results_dir}")
shutil.copy(site_tree_path, self.results_dir)
except Exception as e: # pylint: disable=broad-except
logging.error(f"Failed to copy site tree: {e}")
else:
logging.warning(f"Site tree not found at {site_tree_path}")

def data_for_defect_dojo(self):
"""Returns a tuple containing:
1) Metadata for the test (dictionary)
Expand Down Expand Up @@ -345,6 +364,7 @@ def _setup_zap_automation(self):
self._setup_passive_wait()
self._setup_report()
self._setup_summary()
self._setup_export_site_tree()

# The AF should now be setup and ready to be written
self._save_automation_file()
Expand All @@ -364,6 +384,53 @@ def _setup_import_urls(self):
job["parameters"]["fileName"] = dest
self.automation_config["jobs"].append(job)

def _setup_export_site_tree(self):
scripts_dir = self.container_scripts_dir
site_tree_file_name_add = {
"name": "export-site-tree-filename-global-var-add",
"type": "script",
"parameters": {
"action": "add",
"type": "standalone",
"name": "export-site-tree-filename-global-var",
# Setting the engine to Oracle Nashorn causes the script to fail because
# the engine can't be found when using inline scripts. Not sure why this happens
"engine": "ECMAScript : Graal.js",
"inline": f"""
org.zaproxy.zap.extension.script.ScriptVars.setGlobalVar('siteTreeFileName','{self.SITE_TREE_FILENAME}')
""",
},
}
self.automation_config["jobs"].append(site_tree_file_name_add)
site_tree_file_name_run = {
"name": "export-site-tree-filename-global-var-run",
"type": "script",
"parameters": {"action": "run", "type": "standalone", "name": "export-site-tree-filename-global-var"},
}
self.automation_config["jobs"].append(site_tree_file_name_run)
setup = {
"name": "export-site-tree-add",
"type": "script",
"parameters": {
"action": "add",
"type": "standalone",
"engine": "ECMAScript : Oracle Nashorn",
"name": "export-site-tree",
"file": f"{scripts_dir}/export-site-tree.js",
},
}
self.automation_config["jobs"].append(setup)
run = {
"name": "export-site-tree-run",
"type": "script",
"parameters": {
"action": "run",
"type": "standalone",
"name": "export-site-tree",
},
}
self.automation_config["jobs"].append(run)

def _append_slash_to_url(self, url):
# For some unknown reason, ZAP appears to behave weirdly if the URL is just the hostname without '/'
if not url.endswith("/"):
Expand Down
48 changes: 48 additions & 0 deletions tests/scanners/zap/test_copy_site_tree.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import os
from unittest.mock import MagicMock
from unittest.mock import patch

import pytest

import configmodel
from scanners.zap.zap_none import ZapNone


@pytest.fixture(scope="function")
def test_config():
return configmodel.RapidastConfigModel({"application": {"url": "http://example.com"}})


@patch("os.path.exists")
@patch("scanners.zap.zap.shutil.copy")
@patch("scanners.zap.zap.shutil.copytree")
@patch("scanners.zap.zap.tarfile")
def test_zap_none_postprocess_copy_site_tree_path(mock_tarfile, mock_copytree, mock_copy, mock_exists, test_config):
mock_exists.return_value = True

test_zap = ZapNone(config=test_config)
with patch.object(test_zap, "_copy_site_tree") as mock_copy_site_tree:
test_zap.postprocess()
mock_copy_site_tree.assert_called_once()


@patch("os.path.exists")
@patch("shutil.copy")
def test_copy_site_tree_success(mock_copy, mock_exists, test_config):
mock_exists.return_value = True
test_zap = ZapNone(config=test_config)
test_zap._copy_site_tree()

mock_copy.assert_called_once_with(
os.path.join(test_zap.host_work_dir, f"session_data/{ZapNone.SITE_TREE_FILENAME}"), test_zap.results_dir
)


@patch("os.path.exists")
@patch("shutil.copy")
def test_copy_site_tree_file_not_found(mock_copy, mock_exists, test_config):
mock_exists.return_value = False
test_zap = ZapNone(config=test_config)
test_zap._copy_site_tree()

assert not mock_copy.called
34 changes: 34 additions & 0 deletions tests/scanners/zap/test_setup.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import pathlib
import re
from pathlib import Path

Expand Down Expand Up @@ -404,3 +405,36 @@ def test_get_update_command(test_config):
assert "-addonupdate" in test_zap.get_update_command()
assert "pluginA" in test_zap.get_update_command()
assert "pluginB" in test_zap.get_update_command()


# Export Site Tree


def test_setup_export_site_tree(test_config, pytestconfig):
test_zap = ZapNone(config=test_config)
test_zap.setup()

add_script = None
run_script = None
add_variable_script = None
run_variable_script = None

for item in test_zap.automation_config["jobs"]:
if item["name"] == "export-site-tree-add":
add_script = item
if item["name"] == "export-site-tree-run":
run_script = item
if item["name"] == "export-site-tree-filename-global-var-add":
add_variable_script = item
if item["name"] == "export-site-tree-filename-global-var-run":
run_variable_script = item

assert add_script and run_script and add_variable_script and run_variable_script

assert add_script["parameters"]["name"] == run_script["parameters"]["name"]
assert add_script["parameters"]["file"] == f"{pytestconfig.rootpath}/scanners/zap/scripts/export-site-tree.js"
assert add_script["parameters"]["engine"] == "ECMAScript : Oracle Nashorn"

assert add_variable_script["parameters"]["name"] == run_variable_script["parameters"]["name"]
assert add_variable_script["parameters"]["inline"]
assert add_variable_script["parameters"]["engine"] == "ECMAScript : Graal.js"

0 comments on commit edf666b

Please sign in to comment.