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.
*/