Files
argus-wp-defence/includes/class-argus-correlation.php
root df0f2fccb8 ARGUS WordPress Defence 1.0.0 — first production release
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.
2026-08-09 13:40:16 +00:00

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
);
}
}