From c9b1ab9d397127186d4c865b9ded5220626d0145 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20L=C3=B3pez?= Date: Wed, 16 Apr 2025 19:58:10 +0200 Subject: [PATCH 1/2] Setup ZAP in the test environment to run passive checks through the activation tests. --- .../Environments/E2E/E2EEnvInfo.php | 6 ++ .../Environments/E2E/E2EEnvironment.php | 74 +++++++++++++++++++ .../Environment/Environments/Environment.php | 22 ++++++ .../E2E/Runner/PlaywrightRunner.php | 74 +++++++++++++++++++ 4 files changed, 176 insertions(+) diff --git a/src/src/Environment/Environments/E2E/E2EEnvInfo.php b/src/src/Environment/Environments/E2E/E2EEnvInfo.php index 4bc64760f..0ca8150b8 100644 --- a/src/src/Environment/Environments/E2E/E2EEnvInfo.php +++ b/src/src/Environment/Environments/E2E/E2EEnvInfo.php @@ -57,4 +57,10 @@ class E2EEnvInfo extends EnvInfo { /** @var string The playwright test tag to be executed*/ public $pw_test_tag = ''; + + /** @var string The ZAP container name. */ + public $zap_container; + + /** @var string The ZAP proxy URL. */ + public $zap_proxy; } diff --git a/src/src/Environment/Environments/E2E/E2EEnvironment.php b/src/src/Environment/Environments/E2E/E2EEnvironment.php index 4653c02ea..3eb6b13f5 100644 --- a/src/src/Environment/Environments/E2E/E2EEnvironment.php +++ b/src/src/Environment/Environments/E2E/E2EEnvironment.php @@ -124,6 +124,46 @@ protected function post_up(): void { ); $theme_activation->auto_activate_themes(); } + + // Install Zaproxy for E2E security checks. + if ( getenv( 'QIT_SECURITY_CHECKS_PROXY' ) ) { + $this->output->writeln( 'Setting up Zaproxy for E2E security checks...' ); + $zap_image = 'zaproxy/zap-stable'; + $zap_container_name = "qit_env_zap_{$this->env_info->env_id}"; + $zap_port = 8079; + + // Pull the ZAP image if needed + App::make( Docker::class )->maybe_pull_image( $zap_image ); + + // Run the ZAP container. + // Start ZAP container + $process = new Process([ + App::make(Docker::class)->find_docker(), + 'run', + '-d', + '-p', "{$zap_port}:{$zap_port}", + "--name=$zap_container_name", + "--network={$this->env_info->docker_network}", + $zap_image, + 'zap.sh', + '-daemon', + '-host', '0.0.0.0', + '-port', $zap_port, + '-config', 'api.addrs.addr.name=.*', + '-config', 'api.addrs.addr.regex=true', + '-config', 'api.disablekey=true' + ]); + + $process->setTimeout(300); + $process->run(); + + // Store ZAP info in environment + $this->env_info->zap_container = $zap_container_name; + $this->env_info->zap_proxy = "http://host.docker.internal:$zap_port"; + + // Wait for ZAP to be ready + $this->wait_for_zap_ready($zap_container_name); + } } protected function additional_output(): void { @@ -254,4 +294,38 @@ protected function additional_default_volumes( array $default_volumes ): array { return $default_volumes; } + + protected function wait_for_zap_ready( string $zap_container_name, int $timeout = 120 ): void { + $this->output->writeln( 'Waiting for Zaproxy to be ready...' ); + + $start = time(); + while ( ( time() - $start ) < $timeout ) { + // Simple check: try to get ZAP version + $check_process = new Process( [ + App::make( Docker::class )->find_docker(), + 'exec', + $zap_container_name, + 'curl', + '-s', + '-L', + '--fail', + "{$this->env_info->zap_proxy}/JSON/core/view/version/" + ] ); + + $check_process->run(); + + if ( $check_process->isSuccessful() ) { + $this->output->writeln( 'Zaproxy is ready!' ); + return; + } + + sleep( 2 ); + } + + throw new \RuntimeException( sprintf( + 'Zaproxy container (%s) failed to start within %d seconds', + $zap_container_name, + $timeout + ) ); + } } diff --git a/src/src/Environment/Environments/Environment.php b/src/src/Environment/Environments/Environment.php index abdeef56b..da0fcf1ad 100644 --- a/src/src/Environment/Environments/Environment.php +++ b/src/src/Environment/Environments/Environment.php @@ -326,6 +326,28 @@ public static function down( EnvInfo $env_info, ?OutputInterface $output = null $output = $output ?? App::make( OutputInterface::class ); $environment_monitor = App::make( EnvironmentMonitor::class ); + if ( getenv( 'QIT_SECURITY_CHECKS_PROXY' ) ) { + $zap_container_name = "qit_env_zap_{$env_info->env_id}"; + try { + $down_zap_process = new Process( [ + App::make( Docker::class )->find_docker(), + 'rm', + '-f', + $zap_container_name, + ] ); + + $down_zap_process->run(); + + if ( $output->isVerbose() ) { + $output->writeln( "Removed Zaproxy container: $zap_container_name" ); + } + } catch ( \Exception $e ) { + if ( $output->isVerbose() ) { + $output->writeln( "Failed to remove Zaproxy container: {$e->getMessage()}" ); + } + } + } + if ( ! file_exists( $env_info->temporary_env ) ) { if ( $output->isVerbose() ) { $output->writeln( sprintf( 'Tried to stop environment %s, but it does not exist.', $env_info->temporary_env ) ); diff --git a/src/src/LocalTests/E2E/Runner/PlaywrightRunner.php b/src/src/LocalTests/E2E/Runner/PlaywrightRunner.php index 64fa6a29f..73d8f08f9 100644 --- a/src/src/LocalTests/E2E/Runner/PlaywrightRunner.php +++ b/src/src/LocalTests/E2E/Runner/PlaywrightRunner.php @@ -73,6 +73,12 @@ public function run_test( E2EEnvInfo $env_info, array $test_infos, TestResult $t } } + // Special setting for running security checks on Playwright tests through Zaproxy. + if ( getenv( 'QIT_SECURITY_CHECKS_PROXY' ) ) { + $env_info->playwright_config['use']['ignoreHTTPSErrors'] = true; + $env_info->playwright_config['use']['proxy']['server'] = $env_info->zap_proxy; + } + // Generate playwright-config. $process = new Process( [ PHP_BINARY, $env_info->temporary_env . '/playwright/playwright-config-generator.php' ] ); $process->setEnv( [ @@ -450,6 +456,74 @@ public function run_test( E2EEnvInfo $env_info, array $test_infos, TestResult $t $exit_status_code = $playwright_process->getExitCode(); + /* + * Download ZAP report if security checks were enabled. + */ + if ( getenv( 'QIT_SECURITY_CHECKS_PROXY' ) && $exit_status_code !== 143 ) { + $this->output->writeln( 'Downloading ZAP reports...' ); + + // Generate the report in JSON format + $generate_report_process = new Process( [ + App::make( Docker::class )->find_docker(), + 'exec', + $env_info->zap_container, + 'curl', + '-s', + '-X', + 'GET', + "{$env_info->zap_proxy}/JSON/reports/action/generate/?title=Zap+Report&template=traditional-json&theme=&description=&contexts=&sites=§ions=&includedConfidences=&includedRisks=&reportFileName=zap-report&reportFileNamePattern=&reportDir=&display=" + ] ); + + $generate_report_process->run(); + + if ( $generate_report_process->isSuccessful() && isset( json_decode( $generate_report_process->getOutput(), true )['generate'] ) ) { + $report_types = [ + 'html' => '/OTHER/core/other/htmlreport/', + 'json' => '/OTHER/core/other/jsonreport/' + ]; + + $success = true; + foreach ( $report_types as $type => $endpoint ) { + // Download report + $download_process = new Process( [ + App::make( Docker::class )->find_docker(), + 'exec', + $env_info->zap_container, + 'curl', + '-s', + '-o', + "/tmp/zap-report.{$type}", + "{$env_info->zap_proxy}{$endpoint}", + ] ); + + $download_process->run(); + + if ( $download_process->isSuccessful() ) { + // Copy report to host + $copy_process = new Process( [ + App::make( Docker::class )->find_docker(), + 'cp', + "{$env_info->zap_container}:/tmp/zap-report.{$type}", + $results_dir . "/zap-report.{$type}", + ] ); + + $copy_process->run(); + $success = $success && $copy_process->isSuccessful(); + } else { + $success = false; + } + } + + if ( $success ) { + $this->output->writeln( 'ZAP reports downloaded successfully.' ); + } else { + $this->output->writeln( 'Failed to download or copy ZAP reports.' ); + } + } else { + $this->output->writeln( 'Failed to generate ZAP report.' ); + } + } + /* * Upload test media if test not aborted. */ From be8cbd970f70aa6972286fa939a488982348eca8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20L=C3=B3pez?= Date: Wed, 30 Apr 2025 20:37:48 +0200 Subject: [PATCH 2/2] Disable rules that do not offer actual feedback for WordPress plugins security. --- .../Environments/E2E/E2EEnvironment.php | 53 +++++++++++++++---- 1 file changed, 44 insertions(+), 9 deletions(-) diff --git a/src/src/Environment/Environments/E2E/E2EEnvironment.php b/src/src/Environment/Environments/E2E/E2EEnvironment.php index 3eb6b13f5..964329dc0 100644 --- a/src/src/Environment/Environments/E2E/E2EEnvironment.php +++ b/src/src/Environment/Environments/E2E/E2EEnvironment.php @@ -127,16 +127,20 @@ protected function post_up(): void { // Install Zaproxy for E2E security checks. if ( getenv( 'QIT_SECURITY_CHECKS_PROXY' ) ) { - $this->output->writeln( 'Setting up Zaproxy for E2E security checks...' ); $zap_image = 'zaproxy/zap-stable'; $zap_container_name = "qit_env_zap_{$this->env_info->env_id}"; $zap_port = 8079; + $rule_ids_to_disable = [ + '10011', '10035', '10040', '10041', '10042', '10020', '10038', + '10055', '10021', '10037', '10036', '10033', '10034', '2', '3', + '90001', '10032', '10061', '10039', '10052', '10056', '90030', + '10015', '10050', '10096', '10105', '10098', + ]; - // Pull the ZAP image if needed + $this->output->writeln( 'Setting up Zaproxy for E2E security checks...' ); App::make( Docker::class )->maybe_pull_image( $zap_image ); - // Run the ZAP container. - // Start ZAP container + // Start ZAP container. $process = new Process([ App::make(Docker::class)->find_docker(), 'run', @@ -161,8 +165,8 @@ protected function post_up(): void { $this->env_info->zap_container = $zap_container_name; $this->env_info->zap_proxy = "http://host.docker.internal:$zap_port"; - // Wait for ZAP to be ready - $this->wait_for_zap_ready($zap_container_name); + $this->wait_for_zap_ready(); + $this->disable_zap_rules( $rule_ids_to_disable ); } } @@ -295,7 +299,7 @@ protected function additional_default_volumes( array $default_volumes ): array { return $default_volumes; } - protected function wait_for_zap_ready( string $zap_container_name, int $timeout = 120 ): void { + protected function wait_for_zap_ready( int $timeout = 120 ): void { $this->output->writeln( 'Waiting for Zaproxy to be ready...' ); $start = time(); @@ -304,7 +308,7 @@ protected function wait_for_zap_ready( string $zap_container_name, int $timeout $check_process = new Process( [ App::make( Docker::class )->find_docker(), 'exec', - $zap_container_name, + $this->env_info->zap_container, 'curl', '-s', '-L', @@ -324,8 +328,39 @@ protected function wait_for_zap_ready( string $zap_container_name, int $timeout throw new \RuntimeException( sprintf( 'Zaproxy container (%s) failed to start within %d seconds', - $zap_container_name, + $this->env_info->zap_container, $timeout ) ); } + + protected function disable_zap_rules( array $rule_ids ): void { + $this->output->writeln( "Disabling specified passive scan rules via API..." ); + + $process = new Process( [ + App::make( Docker::class )->find_docker(), + 'exec', + $this->env_info->zap_container, + 'curl', + '-s', + '-L', + '--fail', + "{$this->env_info->zap_proxy}/JSON/pscan/action/disableScanners/?ids=" . urlencode( implode( ',', $rule_ids ) ) + ] ); + + $process->run(); + + // Check if the process was successful + if ( $process->isSuccessful() ) { + $output = $process->getOutput(); + $result = json_decode( $output, true ); + + if ( isset( $result['Result'] ) && $result['Result'] === 'OK' ) { + echo "Successfully disabled all passive scan rules in ZAP\n"; + } else { + echo "Request succeeded but returned unexpected response: " . $output . "\n"; + } + } else { + echo "Failed to disable passive scan rules: " . $process->getErrorOutput() . "\n"; + } + } }