captaincore-manager/app/WebRiskCheck.php
2026-02-01 06:26:36 -05:00

306 lines
9.4 KiB
PHP

<?php
/**
* Google Web Risk Check
*
* Checks all production site URLs against Google's Web Risk API
* and sends email notifications if threats are detected.
*
* @author Austin Ginder
*/
namespace CaptainCore;
use CaptainCore\Remote\GoogleWebRisk;
class WebRiskCheck {
/**
* Run the web risk check via WP-CLI.
*
* ## OPTIONS
*
* [--dry-run]
* : Show what would be checked without calling the API.
*
* [--test]
* : Send a test email with sample threat data.
*
* ## EXAMPLES
*
* wp captaincore web-risk-check
* wp captaincore web-risk-check --dry-run
* wp captaincore web-risk-check --test
*
* @param array $args Positional arguments.
* @param array $assoc_args Associative arguments.
*/
public static function run( $args = [], $assoc_args = [] ) {
$dry_run = isset( $assoc_args['dry-run'] );
$test = isset( $assoc_args['test'] );
// Handle test mode - send sample email with fake threats
if ( $test ) {
\WP_CLI::log( 'Sending test email with sample threat data...' );
$sample_threats = [
[
'site_id' => 123,
'site_name' => 'example-site.com',
'home_url' => 'https://example-site.com',
'threat_types' => [ 'MALWARE' ],
'expire_time' => date( 'c', strtotime( '+1 day' ) ),
],
[
'site_id' => 456,
'site_name' => 'test-website.org',
'home_url' => 'https://test-website.org',
'threat_types' => [ 'SOCIAL_ENGINEERING', 'UNWANTED_SOFTWARE' ],
'expire_time' => date( 'c', strtotime( '+1 day' ) ),
],
];
self::send_summary_email( $sample_threats, 1307 );
\WP_CLI::success( 'Test email sent to ' . get_option( 'admin_email' ) );
return;
}
// Check if API key is configured (skip for dry-run)
if ( ! $dry_run && ! defined( 'GOOGLE_WEB_RISK_API_KEY' ) ) {
\WP_CLI::error( 'GOOGLE_WEB_RISK_API_KEY constant is not defined.' );
return;
}
$sites = self::get_production_urls();
if ( empty( $sites ) ) {
\WP_CLI::log( 'No production URLs to check.' );
return;
}
\WP_CLI::log( sprintf( 'Found %d production URLs to check.', count( $sites ) ) );
if ( $dry_run ) {
\WP_CLI::log( '' );
\WP_CLI::log( '--- Dry Run Mode ---' );
\WP_CLI::log( 'The following URLs would be checked:' );
\WP_CLI::log( '' );
foreach ( $sites as $site ) {
\WP_CLI::log( sprintf( ' [%d] %s - %s', $site->site_id, $site->name, $site->home_url ) );
}
\WP_CLI::log( '' );
\WP_CLI::success( sprintf( 'Dry run complete. %d sites would be checked.', count( $sites ) ) );
return;
}
$threats_found = [];
$errors = [];
$progress = \WP_CLI\Utils\make_progress_bar( 'Checking URLs', count( $sites ) );
foreach ( $sites as $site ) {
$result = GoogleWebRisk::get( $site->home_url );
if ( is_wp_error( $result ) ) {
$errors[] = [
'site_id' => $site->site_id,
'name' => $site->name,
'home_url' => $site->home_url,
'error' => $result->get_error_message(),
];
$progress->tick();
continue;
}
if ( isset( $result->threat ) && ! empty( $result->threat ) ) {
$threats_found[] = [
'site_id' => $site->site_id,
'site_name' => $site->name,
'home_url' => $site->home_url,
'threat_types' => $result->threat->threatTypes ?? [],
'expire_time' => $result->threat->expireTime ?? null,
];
}
$progress->tick();
}
$progress->finish();
// Report any API errors
if ( ! empty( $errors ) ) {
\WP_CLI::warning( sprintf( '%d site(s) had API errors:', count( $errors ) ) );
foreach ( $errors as $error ) {
\WP_CLI::log( sprintf( ' - %s: %s', $error['name'], $error['error'] ) );
}
}
// Save results to database
self::save_log( count( $sites ), $threats_found, $errors );
// No threats found
if ( empty( $threats_found ) ) {
\WP_CLI::success( sprintf( 'All %d sites are clean. No threats detected.', count( $sites ) ) );
return;
}
// Threats found - display and send email
\WP_CLI::warning( sprintf( 'Found %d site(s) with threats!', count( $threats_found ) ) );
foreach ( $threats_found as $threat ) {
$types = implode( ', ', $threat['threat_types'] );
\WP_CLI::log( sprintf( ' - %s (%s): %s', $threat['site_name'], $threat['home_url'], $types ) );
}
// Send email notification
self::send_summary_email( $threats_found, count( $sites ) );
\WP_CLI::success( 'Summary email sent to administrator.' );
}
/**
* Save check results to database.
*
* @param int $total_sites Total number of sites checked.
* @param array $threats_found Array of threat data.
* @param array $errors Array of error data.
*/
private static function save_log( $total_sites, $threats_found, $errors ) {
$log_data = [
'total_sites' => $total_sites,
'threats_found' => count( $threats_found ),
'errors_count' => count( $errors ),
'details' => json_encode( [
'threats' => $threats_found,
'errors' => $errors,
] ),
'created_at' => date( 'Y-m-d H:i:s' ),
];
( new WebRiskLogs() )->insert( $log_data );
}
/**
* Get all production URLs from environments table.
*
* @return array Array of site objects with site_id, name, and home_url.
*/
private static function get_production_urls() {
global $wpdb;
$table_environments = $wpdb->prefix . 'captaincore_environments';
$table_sites = $wpdb->prefix . 'captaincore_sites';
return $wpdb->get_results( "
SELECT e.site_id, e.home_url, s.name
FROM {$table_environments} e
JOIN {$table_sites} s ON e.site_id = s.site_id
WHERE e.environment = 'Production'
AND e.home_url IS NOT NULL
AND e.home_url != ''
AND s.status = 'active'
ORDER BY s.name ASC
" );
}
/**
* Send summary email using Mailer.
*
* @param array $threats Array of threat data.
* @param int $total_checked Total number of sites checked.
*/
private static function send_summary_email( $threats, $total_checked ) {
$admin_email = get_option( 'admin_email' );
$subject = 'Google Web Risk Summary';
$headline = 'Web Risk Check Results';
$subheadline = sprintf(
'%d threat(s) detected out of %d sites checked',
count( $threats ),
$total_checked
);
// Build HTML table of threats
$table_rows = '';
foreach ( $threats as $threat ) {
$threat_types_str = implode( ', ', $threat['threat_types'] );
$threat_labels = self::format_threat_types( $threat['threat_types'] );
$table_rows .= sprintf(
'<tr>
<td style="padding: 12px; border-bottom: 1px solid #edf2f7; vertical-align: top;">
<strong style="color: #2d3748;">%s</strong><br>
<a href="%s" style="color: #718096; font-size: 13px; text-decoration: none;">%s</a>
</td>
<td style="padding: 12px; border-bottom: 1px solid #edf2f7; vertical-align: top;">%s</td>
</tr>',
esc_html( $threat['site_name'] ),
esc_url( $threat['home_url'] ),
esc_html( $threat['home_url'] ),
$threat_labels
);
}
$content = sprintf(
'<p style="margin-bottom: 25px; line-height: 1.6; color: #4a5568;">
The daily Web Risk check has completed. The following site(s) were flagged by Google\'s Web Risk API:
</p>
<table style="width: 100%%; border-collapse: collapse; margin-bottom: 25px;">
<thead>
<tr style="background-color: #f7fafc;">
<th style="padding: 12px; text-align: left; border-bottom: 2px solid #e2e8f0; color: #4a5568; font-size: 12px; text-transform: uppercase; letter-spacing: 0.05em;">Site</th>
<th style="padding: 12px; text-align: left; border-bottom: 2px solid #e2e8f0; color: #4a5568; font-size: 12px; text-transform: uppercase; letter-spacing: 0.05em;">Threat Types</th>
</tr>
</thead>
<tbody>%s</tbody>
</table>
<div style="background-color: #f7fafc; border: 1px solid #e2e8f0; border-radius: 6px; padding: 15px; margin-top: 20px; text-align: left;">
<h4 style="margin: 0 0 10px; font-size: 11px; text-transform: uppercase; color: #a0aec0; letter-spacing: 0.05em;">Threat Type Reference</h4>
<ul style="margin: 0; padding-left: 20px; color: #4a5568; font-size: 13px; line-height: 1.8; text-align: left;">
<li style="text-align: left;"><strong>MALWARE</strong> - Site may be distributing malicious software</li>
<li style="text-align: left;"><strong>SOCIAL_ENGINEERING</strong> - Site may be involved in phishing or deceptive practices</li>
<li style="text-align: left;"><strong>UNWANTED_SOFTWARE</strong> - Site may be distributing unwanted software</li>
</ul>
</div>',
$table_rows
);
Mailer::send_process_completed(
$admin_email,
$subject,
$headline,
$subheadline,
$content,
''
);
}
/**
* Format threat types with colored badges.
*
* @param array $threat_types Array of threat type strings.
* @return string HTML badges.
*/
private static function format_threat_types( $threat_types ) {
$badges = [];
$colors = [
'MALWARE' => '#e53e3e',
'SOCIAL_ENGINEERING' => '#dd6b20',
'UNWANTED_SOFTWARE' => '#d69e2e',
'SOCIAL_ENGINEERING_EXTENDED_COVERAGE' => '#dd6b20',
];
foreach ( $threat_types as $type ) {
$color = $colors[ $type ] ?? '#718096';
$label = str_replace( '_', ' ', $type );
$badges[] = sprintf(
'<span style="display: inline-block; background-color: %s; color: #ffffff; font-size: 11px; font-weight: 600; padding: 4px 8px; border-radius: 4px; margin: 2px 0;">%s</span>',
$color,
esc_html( $label )
);
}
return implode( ' ', $badges );
}
}