diff --git a/scanners/zap/scripts/export-site-tree.js b/scanners/zap/scripts/export-site-tree.js new file mode 100644 index 00000000..59f2569d --- /dev/null +++ b/scanners/zap/scripts/export-site-tree.js @@ -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); +} diff --git a/scanners/zap/zap.py b/scanners/zap/zap.py index c4a9b5bc..b03afc4e 100644 --- a/scanners/zap/zap.py +++ b/scanners/zap/zap.py @@ -1,3 +1,4 @@ +# pylint: disable=too-many-lines import glob import logging import os @@ -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") @@ -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) @@ -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() @@ -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("/"): diff --git a/tests/scanners/zap/test_copy_site_tree.py b/tests/scanners/zap/test_copy_site_tree.py new file mode 100644 index 00000000..40ce41d4 --- /dev/null +++ b/tests/scanners/zap/test_copy_site_tree.py @@ -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 diff --git a/tests/scanners/zap/test_setup.py b/tests/scanners/zap/test_setup.py index b9ddde13..a9054ed0 100644 --- a/tests/scanners/zap/test_setup.py +++ b/tests/scanners/zap/test_setup.py @@ -1,4 +1,5 @@ import os +import pathlib import re from pathlib import Path @@ -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"