Automatic WordPress security: local firewall, malware and file-integrity scanning, vulnerability protection, quarantine, scheduled backups, an optional page cache, and automatic global threat intelligence. See README.md for installation, update, and uninstall instructions.
125 lines
4.3 KiB
PHP
125 lines
4.3 KiB
PHP
<?php
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
class Argus_Correlation {
|
|
|
|
const WATERMARK_OPTION = 'argus_wpd_correlation_watermark';
|
|
const WINDOW_SECONDS = 15 * MINUTE_IN_SECONDS;
|
|
|
|
public static function run() {
|
|
global $wpdb;
|
|
$events_table = Argus_DB::table( 'events' );
|
|
$since = get_option( self::WATERMARK_OPTION, gmdate( 'Y-m-d H:i:s', time() - DAY_IN_SECONDS ) );
|
|
$now = current_time( 'mysql', true );
|
|
|
|
$suspicious = $wpdb->get_results(
|
|
$wpdb->prepare(
|
|
"SELECT * FROM {$events_table} WHERE event_type IN ('ban','policy_observed','login_failed') AND severity IN ('high','critical') AND ip IS NOT NULL AND created_at >= %s ORDER BY created_at ASC", // phpcs:ignore
|
|
$since
|
|
)
|
|
);
|
|
|
|
if ( empty( $suspicious ) ) {
|
|
update_option( self::WATERMARK_OPTION, $now, false );
|
|
return 0;
|
|
}
|
|
|
|
$file_events = $wpdb->get_results(
|
|
$wpdb->prepare(
|
|
"SELECT * FROM {$events_table} WHERE event_type IN ('integrity_new_file','integrity_changed_file') AND created_at >= %s ORDER BY created_at ASC", // phpcs:ignore
|
|
$since
|
|
)
|
|
);
|
|
|
|
$by_ip = array();
|
|
foreach ( $suspicious as $event ) {
|
|
$by_ip[ $event->ip ][] = $event;
|
|
}
|
|
|
|
$correlated = 0;
|
|
|
|
foreach ( $by_ip as $ip => $ip_events ) {
|
|
$request_event = $ip_events[0];
|
|
$request_time = strtotime( $request_event->created_at . ' UTC' );
|
|
|
|
$best_delta = null;
|
|
$best_file = null;
|
|
$best_payload = null;
|
|
|
|
foreach ( $file_events as $file_event ) {
|
|
$payload = json_decode( $file_event->payload, true );
|
|
$file_mtime = isset( $payload['file_mtime'] ) ? strtotime( $payload['file_mtime'] . ' UTC' ) : null;
|
|
if ( null === $file_mtime ) {
|
|
continue;
|
|
}
|
|
|
|
$delta = $file_mtime - $request_time;
|
|
if ( $delta < 0 || $delta > self::WINDOW_SECONDS ) {
|
|
continue;
|
|
}
|
|
|
|
if ( null === $best_delta || $delta < $best_delta ) {
|
|
$best_delta = $delta;
|
|
$best_file = $file_event;
|
|
$best_payload = $payload;
|
|
}
|
|
}
|
|
|
|
if ( null !== $best_file ) {
|
|
self::record_correlated_finding( $request_event, $best_file, $best_payload, $best_delta );
|
|
$correlated++;
|
|
}
|
|
}
|
|
|
|
update_option( self::WATERMARK_OPTION, $now, false );
|
|
return $correlated;
|
|
}
|
|
|
|
protected static function record_correlated_finding( $request_event, $file_event, array $file_payload, $delta_seconds ) {
|
|
Argus_Findings::record(
|
|
'correlation',
|
|
'critical',
|
|
array(
|
|
'what_happened' => sprintf(
|
|
'A high-severity request from %s was followed %d seconds later by a %s: %s',
|
|
$request_event->ip,
|
|
$delta_seconds,
|
|
'integrity_new_file' === $file_event->event_type ? 'new file' : 'changed file',
|
|
$file_payload['file_path'] ?? 'unknown'
|
|
),
|
|
'why_it_matters' => 'A suspicious request closely followed by a filesystem change is a much stronger signal than either event alone -- this is the pattern a successful exploit-then-webshell-drop typically produces.',
|
|
'what_argus_found' => sprintf(
|
|
'Request event: %s (%s, severity %s) at %s. File event: %s at %s.',
|
|
$request_event->event_type,
|
|
$request_event->summary,
|
|
$request_event->severity,
|
|
$request_event->created_at,
|
|
$file_event->summary,
|
|
$file_event->created_at
|
|
),
|
|
'when_it_happened' => $file_event->created_at,
|
|
'why_suspicious' => sprintf( 'The file change happened within %d seconds of the suspicious request -- close enough in time to be a plausible cause-and-effect, though ARGUS cannot prove the same actor performed both (a filesystem write carries no IP attribution).', self::WINDOW_SECONDS ),
|
|
'what_could_be_affected' => 'If this represents a successful exploit, the new/changed file may itself be malicious code with the same access as your web application.',
|
|
'what_should_you_do' => 'Review the file named above immediately, and treat the source IP as a confirmed active threat rather than a routine block.',
|
|
),
|
|
array(
|
|
'request_event_id' => $request_event->id,
|
|
'file_event_id' => $file_event->id,
|
|
'ip' => $request_event->ip,
|
|
'delta_seconds' => $delta_seconds,
|
|
)
|
|
);
|
|
|
|
Argus_Ban_Engine::ban(
|
|
$request_event->ip,
|
|
Argus_Ban_Engine::SOURCE_POLICY_ENGINE,
|
|
'Correlated with a filesystem change ' . $delta_seconds . 's later',
|
|
array( 'request_event_id' => $request_event->id, 'file_event_id' => $file_event->id ),
|
|
Argus_Ban_Engine::LEVEL_EXTENDED
|
|
);
|
|
}
|
|
}
|