Skip to content

PoC: Setup ZAP in the test environment to run passive checks through the activation tests #287

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: trunk
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/src/Environment/Environments/E2E/E2EEnvInfo.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
109 changes: 109 additions & 0 deletions src/src/Environment/Environments/E2E/E2EEnvironment.php
Original file line number Diff line number Diff line change
Expand Up @@ -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( '<info>Setting up Zaproxy for E2E security checks...</info>' );
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 {
Expand Down Expand Up @@ -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( '<info>Waiting for Zaproxy to be ready...</info>' );

$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( '<info>Zaproxy is ready!</info>' );
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";
}
}
}
22 changes: 22 additions & 0 deletions src/src/Environment/Environments/Environment.php
Original file line number Diff line number Diff line change
Expand Up @@ -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( "<comment>Failed to remove Zaproxy container: {$e->getMessage()}</comment>" );
}
}
}

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 ) );
Expand Down
74 changes: 74 additions & 0 deletions src/src/LocalTests/E2E/Runner/PlaywrightRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -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( [
Expand Down Expand Up @@ -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( '<info>Downloading ZAP reports...</info>' );

// 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=&sections=&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( '<info>ZAP reports downloaded successfully.</info>' );
} else {
$this->output->writeln( '<error>Failed to download or copy ZAP reports.</error>' );
}
} else {
$this->output->writeln( '<error>Failed to generate ZAP report.</error>' );
}
}

/*
* Upload test media if test not aborted.
*/
Expand Down