absint( $b['id'] ?? 0 ); } ); $window = max( 5, absint( $minutes ) ) * MINUTE_IN_SECONDS; $chains = array(); foreach ( $events as $event ) { $time = strtotime( ( $event['created_at'] ?? '' ) . ' UTC' ) ?: 0; $component = self::event_component( $event ); $target = -1; for ( $i = count( $chains ) - 1; $i >= 0; --$i ) { $gap = $time - $chains[ $i ]['last_timestamp']; if ( $gap > $window ) { break; } if ( $component === $chains[ $i ]['component'] || 'system-state' === $component || 'system-state' === $chains[ $i ]['component'] ) { $target = $i; break; } } if ( $target < 0 ) { $chains[] = array( 'component' => $component, 'events' => array(), 'first_timestamp' => $time, 'last_timestamp' => $time ); $target = count( $chains ) - 1; } elseif ( 'system-state' === $chains[ $target ]['component'] && 'system-state' !== $component ) { $chains[ $target ]['component'] = $component; } $chains[ $target ]['events'][] = $event; $chains[ $target ]['last_timestamp'] = $time; } $capsule_map = array(); foreach ( $capsules as $capsule ) { $capsule_map[ absint( $capsule['trigger_event_id'] ?? 0 ) ] = $capsule; } $operator = get_option( 'edrad_incident_classifications', array() ); foreach ( $chains as &$chain ) { $ids = array_map( 'absint', array_column( $chain['events'], 'id' ) ); $chain['id'] = substr( hash( 'sha256', $chain['component'] . ':' . min( $ids ) ), 0, 12 ); $chain['severity'] = max( array_map( 'absint', array_column( $chain['events'], 'severity' ) ) ); $types = array_column( $chain['events'], 'event_type' ); $confirmed_fatal = false; $contained_fatal = false; foreach ( $chain['events'] as $chain_event ) { if ( 'fatal_error' !== ( $chain_event['event_type'] ?? '' ) ) { continue; } $impact = self::decode_context( $chain_event )['impact_state'] ?? ''; if ( 'contained' === $impact ) { $contained_fatal = true; } else { $confirmed_fatal = true; } } $chain['status'] = $confirmed_fatal || in_array( 'unattributed_drift', $types, true ) ? 'investigate' : ( $contained_fatal || $chain['severity'] >= 4 ? 'review' : 'nominal' ); $chain['operator_status'] = sanitize_key( $operator[ $chain['id'] ]['status'] ?? 'unclassified' ); $chain['classified_at'] = sanitize_text_field( $operator[ $chain['id'] ]['classified_at'] ?? '' ); $chain['capsules'] = array(); foreach ( $ids as $id ) { if ( isset( $capsule_map[ $id ] ) ) { $chain['capsules'][] = $capsule_map[ $id ]; } } $chain['why'] = self::chain_reasons( $chain['events'], $chain['component'] ); } unset( $chain ); return array_reverse( $chains ); } private static function chain_reasons( array $events, $component ) { $types = array_column( $events, 'event_type' ); $reasons = array(); if ( array_intersect( array( 'update_completed', 'plugin_activated', 'plugin_deactivated', 'theme_switched' ), $types ) ) { $reasons[] = 'Recorded deployment or configuration change.'; } if ( in_array( 'fatal_error', $types, true ) ) { $reasons[] = 'Runtime fatal captured inside the correlation window.'; } if ( in_array( 'unattributed_drift', $types, true ) ) { $reasons[] = 'System fingerprint changed without an attributable operation.'; } if ( count( $events ) > 1 ) { $reasons[] = count( $events ) . ' signals share temporal or component proximity.'; } if ( 'system-state' !== $component ) { $reasons[] = 'Signals resolve to the same component: ' . $component . '.'; } return $reasons ?: array( 'Single attributable operational signal.' ); } public static function decode_context( array $event ) { $context = json_decode( $event['context'] ?? '', true ); return is_array( $context ) ? $context : array(); } public static function is_noop( array $event ) { if ( 'update_completed' !== ( $event['event_type'] ?? '' ) ) { return false; } $context = self::decode_context( $event ); return empty( $context['changes'] ); } public static function analyze( array $events, $minutes = 30 ) { $cutoff = time() - absint( $minutes ) * MINUTE_IN_SECONDS; $window = array(); $meaningful = array(); $noops = 0; $suppressed = 0; $risk = 0; $severity_total = 0; foreach ( $events as $event ) { $timestamp = strtotime( $event['created_at'] . ' UTC' ); if ( false === $timestamp || $timestamp < $cutoff ) { continue; } $window[] = $event; if ( self::is_suppressed( $event ) ) { $suppressed += max( 1, absint( $event['occurrence_count'] ?? 1 ) ); continue; } if ( self::is_noop( $event ) ) { ++$noops; continue; } if ( 'baseline_created' === $event['event_type'] ) { continue; } $meaningful[] = $event; $severity_total += absint( $event['severity'] ); $risk += self::risk_weight( $event['event_type'] ); } $types = array_column( $meaningful, 'event_type' ); $has_drift = in_array( 'unattributed_drift', $types, true ); $has_confirmed_fatal = false; $has_contained_fatal = false; foreach ( $meaningful as $meaningful_event ) { if ( 'fatal_error' !== $meaningful_event['event_type'] ) { continue; } $fatal_context = self::decode_context( $meaningful_event ); if ( 'contained' === ( $fatal_context['impact_state'] ?? '' ) ) { $has_contained_fatal = true; } else { $has_confirmed_fatal = true; } } $has_identity= (bool) array_intersect( array( 'user_created', 'user_deleted', 'user_role_changed' ), $types ); $deploy_only = ! empty( $meaningful ) && ! array_diff( $types, array( 'update_completed', 'plugin_activated', 'plugin_deactivated', 'theme_switched' ) ); if ( $has_confirmed_fatal || $has_drift ) { $class = 'INCIDENT WINDOW'; $status = 'INVESTIGATE'; } elseif ( $has_contained_fatal ) { $class = 'RUNTIME FAULT WINDOW'; $status = 'REVIEW'; } elseif ( $has_identity ) { $class = 'PRIVILEGED CHANGE'; $status = 'REVIEW'; } elseif ( $deploy_only ) { $class = 'DEPLOYMENT WINDOW'; $status = 'NOMINAL'; } elseif ( empty( $meaningful ) ) { $class = 'QUIET WINDOW'; $status = 'NOMINAL'; } else { $class = 'CHANGE WINDOW'; $status = $risk >= 7 ? 'REVIEW' : 'NOMINAL'; } $evidence = self::evidence( $meaningful, $noops ); return array( 'class' => $class, 'status' => $status, 'evidence' => $evidence['level'], 'evidence_factors' => $evidence['factors'], 'raw_signals' => count( $window ), 'meaningful' => count( $meaningful ), 'noops' => $noops, 'suppressed' => $suppressed, 'risk' => min( 100, $risk * 10 ), 'window_load' => count( $meaningful ) ? (int) round( ( $severity_total / ( count( $meaningful ) * 5 ) ) * 100 ) : 0, 'assessment' => self::assessment( $class, $meaningful, $noops ), ); } public static function is_suppressed( array $event ) { if ( 'suppressed_noise' === ( $event['disposition'] ?? '' ) ) { return true; } $context = self::decode_context( $event ); return 'suppressed_noise' === ( $context['signal_policy']['disposition'] ?? '' ); } private static function risk_weight( $type ) { $weights = array( 'unattributed_drift' => 5, 'fatal_error' => 5, 'user_role_changed' => 5, 'user_created' => 4, 'user_deleted' => 4, 'theme_switched' => 2, 'plugin_deactivated' => 2, 'plugin_activated' => 1, 'update_completed' => 1, ); return $weights[ $type ] ?? 2; } private static function evidence( array $events, $noops ) { $factors = array(); if ( empty( $events ) ) { return array( 'level' => 'HIGH', 'factors' => array( 'No meaningful change signal in the analysis window.' ) ); } $with_delta = 0; $attributed = 0; $times = array(); foreach ( $events as $event ) { if ( ! empty( self::change_lines( $event ) ) || 'fatal_error' === $event['event_type'] ) { ++$with_delta; } if ( ! empty( $event['user_id'] ) || in_array( $event['event_type'], array( 'fatal_error', 'unattributed_drift' ), true ) ) { ++$attributed; } $times[] = strtotime( $event['created_at'] . ' UTC' ); } if ( $with_delta === count( $events ) ) { $factors[] = 'Every meaningful signal contains a verifiable state delta or impact record.'; } if ( $attributed === count( $events ) ) { $factors[] = 'Every meaningful signal has an accountable actor or system source.'; } if ( count( $times ) < 2 || max( $times ) - min( $times ) <= 10 * MINUTE_IN_SECONDS ) { $factors[] = 'Signals are temporally coherent within ten minutes.'; } if ( $noops ) { $factors[] = $noops . ' no-change completion signal(s) were identified and excluded.'; } $component_match = self::component_match( $events ); if ( $component_match ) { $factors[] = 'Impact and deployment reference the same component: ' . $component_match . '.'; } $count = count( $factors ); return array( 'level' => $count >= 3 ? 'HIGH' : ( $count >= 2 ? 'MEDIUM' : 'LOW' ), 'factors' => $factors ); } private static function component_match( array $events ) { $fatal_components = array(); $change_lines = array(); foreach ( $events as $event ) { if ( 'fatal_error' === $event['event_type'] ) { $context = self::decode_context( $event ); if ( ! empty( $context['component'] ) && 0 === strpos( $context['component'], 'plugin:' ) ) { $fatal_components[] = substr( $context['component'], 7 ); } } $change_lines = array_merge( $change_lines, self::change_lines( $event ) ); } foreach ( array_unique( $fatal_components ) as $component ) { foreach ( $change_lines as $line ) { if ( 0 === strpos( $line, $component . '/' ) ) { return $component; } } } return ''; } private static function assessment( $class, array $events, $noops ) { if ( 'QUIET WINDOW' === $class ) { return 'No meaningful system changes detected in the current window.'; } if ( 'DEPLOYMENT WINDOW' === $class ) { $message = count( $events ) . ' attributable deployment signal(s) detected. No unrelated system drift found.'; return $noops ? $message . ' ' . $noops . ' no-change completion signal(s) neutralized.' : $message; } if ( 'PRIVILEGED CHANGE' === $class ) { return 'A user or role operation changed the privileged-access state. Review the actor and target.'; } if ( 'INCIDENT WINDOW' === $class ) { $match = self::component_match( $events ); return $match ? 'Fatal impact followed a recorded change to the same component: ' . $match . '. Investigation recommended.' : 'A fatal impact or unattributed system drift occurred in the change window. Investigation recommended.'; } if ( 'RUNTIME FAULT WINDOW' === $class ) { return 'A PHP fatal signal was captured without a 5xx response. The visible operation may have completed; review the request context before remediation.'; } return 'Multiple change types detected. Review the operational timeline.'; } public static function change_lines( array $event ) { $context = self::decode_context( $event ); $changes = $context['changes'] ?? array(); $lines = array(); if ( 'fatal_error' === ( $event['event_type'] ?? '' ) ) { $lines[] = 'component: ' . ( $context['component'] ?? 'unknown' ); $lines[] = 'location: ' . ( $context['file'] ?? 'unknown' ) . ':' . absint( $context['line'] ?? 0 ); if ( isset( $context['response_code'] ) ) { $lines[] = 'request: ' . ( $context['request_type'] ?? 'unknown' ) . ' ' . ( $context['request_path'] ?? '' ) . ' → HTTP ' . absint( $context['response_code'] ); $lines[] = 'impact state: ' . strtoupper( sanitize_key( $context['impact_state'] ?? 'unknown' ) ); } } foreach ( $changes['plugins_changed'] ?? array() as $plugin => $change ) { $before = $change['before']['version'] ?? '?'; $after = $change['after']['version'] ?? '?'; if ( $before !== $after ) { $lines[] = $plugin . ': ' . $before . ' → ' . $after; } elseif ( ( $change['before']['active'] ?? null ) !== ( $change['after']['active'] ?? null ) ) { $lines[] = $plugin . ': ' . ( ! empty( $change['after']['active'] ) ? 'activated' : 'deactivated' ); } } foreach ( $changes['plugins_added'] ?? array() as $plugin ) { $lines[] = $plugin . ': added'; } foreach ( $changes['plugins_removed'] ?? array() as $plugin ) { $lines[] = $plugin . ': removed'; } foreach ( array( 'wp_version', 'php_version', 'active_theme', 'theme_version', 'debug_enabled' ) as $key ) { if ( isset( $changes[ $key ] ) ) { $lines[] = $key . ': ' . wp_json_encode( $changes[ $key ]['before'] ) . ' → ' . wp_json_encode( $changes[ $key ]['after'] ); } } return $lines; } }