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..964329dc0 100644 --- a/src/src/Environment/Environments/E2E/E2EEnvironment.php +++ b/src/src/Environment/Environments/E2E/E2EEnvironment.php @@ -124,6 +124,50 @@ protected function post_up(): void { ); $theme_activation->auto_activate_themes(); } + + // Install Zaproxy for E2E security checks. + if ( getenv( 'QIT_SECURITY_CHECKS_PROXY' ) ) { + $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', + ]; + + $this->output->writeln( 'Setting up Zaproxy for E2E security checks...' ); + App::make( Docker::class )->maybe_pull_image( $zap_image ); + + // 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"; + + $this->wait_for_zap_ready(); + $this->disable_zap_rules( $rule_ids_to_disable ); + } } protected function additional_output(): void { @@ -254,4 +298,69 @@ protected function additional_default_volumes( array $default_volumes ): array { return $default_volumes; } + + protected function wait_for_zap_ready( 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', + $this->env_info->zap_container, + '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', + $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"; + } + } } 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. */