insert( Argus_DB::table( 'quarantine' ), array( 'original_path' => $rel_path, 'quarantine_filename' => $filename, 'original_filename' => basename( $rel_path ), 'file_size' => $size, 'file_hash' => $hash, 'original_mtime' => $mtime, 'original_perms' => $perms, 'detection_engine' => $detection['engine'], 'detection_rule' => $detection['rule'] ?? null, 'detection_type' => $detection['type'], 'severity' => $detection['severity'], 'status' => self::STATUS_QUARANTINED, 'quarantined_at' => $now, ), array( '%s', '%s', '%s', '%d', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s' ) ); $id = (int) $wpdb->insert_id; Argus_Events::record( 'quarantine_added', 'high', sprintf( 'Quarantined %s (%s)', $rel_path, $detection['engine'] ), array( 'quarantine_id' => $id, 'original_path' => $rel_path, 'detection_type' => $detection['type'] ) ); return array( 'id' => $id, 'hash' => $hash, 'quarantine_filename' => $filename ); } protected static function store_path( $row ) { return trailingslashit( self::quarantine_dir() ) . $row->quarantine_filename; } public static function restore( $id ) { $row = self::get( $id ); if ( ! $row || self::STATUS_QUARANTINED !== $row->status ) { return array( 'success' => false, 'message' => __( 'That item is not currently quarantined.', 'argus-wordpress-defence' ) ); } $store_path = self::store_path( $row ); if ( ! file_exists( $store_path ) ) { return array( 'success' => false, 'message' => __( 'The quarantined file is missing from the quarantine store.', 'argus-wordpress-defence' ) ); } $content = file_get_contents( $store_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions list( $score, $matched ) = Argus_Malware_Scanner::score_content( (string) $content ); global $wpdb; if ( $score >= Argus_Malware_Scanner::THRESHOLD_MEDIUM ) { $wpdb->update( Argus_DB::table( 'quarantine' ), array( 'restore_attempts' => (int) $row->restore_attempts + 1 ), array( 'id' => $row->id ), array( '%d' ), array( '%d' ) ); return array( 'success' => false, 'message' => __( 'Restore cancelled -- the restored file still triggers the security engine.', 'argus-wordpress-defence' ), ); } $abs_original = ABSPATH . ltrim( $row->original_path, '/' ); if ( file_exists( $abs_original ) ) { return array( 'success' => false, 'message' => __( 'A file already exists at the original location.', 'argus-wordpress-defence' ) ); } $moved = @rename( $store_path, $abs_original ); // phpcs:ignore WordPress.PHP.NoSilencedErrors if ( ! $moved ) { return array( 'success' => false, 'message' => __( 'Could not move the file back to its original location.', 'argus-wordpress-defence' ) ); } if ( $row->original_perms ) { @chmod( $abs_original, octdec( $row->original_perms ) ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_chmod, WordPress.PHP.NoSilencedErrors } $now = current_time( 'mysql', true ); $wpdb->update( Argus_DB::table( 'quarantine' ), array( 'status' => self::STATUS_RESTORED, 'resolved_at' => $now, 'restore_attempts' => (int) $row->restore_attempts + 1 ), array( 'id' => $row->id ), array( '%s', '%s', '%d' ), array( '%d' ) ); Argus_Malware_Scanner::mark_restored_trusted( $row->original_path, hash_file( 'sha256', $abs_original ) ); if ( $row->finding_id ) { Argus_Findings::set_status( $row->finding_id, Argus_Findings::STATUS_RESOLVED ); } Argus_Events::record( 'quarantine_restored', 'info', sprintf( 'Restored %s from quarantine', $row->original_path ), array( 'quarantine_id' => $row->id, 'original_path' => $row->original_path, 'score' => $score ) ); return array( 'success' => true, 'message' => __( 'Restored -- ARGUS\'s static-analysis engine found no suspicious patterns in the current content. This is not a guarantee the file is safe, only that it did not match ARGUS\'s known indicators.', 'argus-wordpress-defence' ), ); } public static function delete( $id ) { global $wpdb; $row = self::get( $id ); if ( ! $row || self::STATUS_QUARANTINED !== $row->status ) { return array( 'success' => false, 'message' => __( 'That item is not currently quarantined.', 'argus-wordpress-defence' ) ); } $store_path = self::store_path( $row ); if ( file_exists( $store_path ) && ! @unlink( $store_path ) ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors return array( 'success' => false, 'message' => __( 'Could not delete the quarantined file.', 'argus-wordpress-defence' ) ); } $now = current_time( 'mysql', true ); $wpdb->update( Argus_DB::table( 'quarantine' ), array( 'status' => self::STATUS_DELETED, 'resolved_at' => $now ), array( 'id' => $row->id ), array( '%s', '%s' ), array( '%d' ) ); if ( $row->finding_id ) { Argus_Findings::set_status( $row->finding_id, Argus_Findings::STATUS_RESOLVED ); } Argus_Events::record( 'quarantine_deleted', 'info', sprintf( 'Permanently deleted quarantined file: %s', $row->original_path ), array( 'quarantine_id' => $row->id, 'original_path' => $row->original_path ) ); return array( 'success' => true, 'message' => __( 'Permanently deleted.', 'argus-wordpress-defence' ) ); } public static function analyse( $id ) { global $wpdb; $row = self::get( $id ); if ( ! $row ) { return array( 'success' => false, 'message' => __( 'Quarantine item not found.', 'argus-wordpress-defence' ) ); } $store_path = self::store_path( $row ); if ( ! file_exists( $store_path ) ) { return array( 'success' => false, 'message' => __( 'The quarantined file is missing from the quarantine store.', 'argus-wordpress-defence' ) ); } $content = file_get_contents( $store_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions list( $score, $matched ) = Argus_Malware_Scanner::score_content( (string) $content ); $wpdb->update( Argus_DB::table( 'quarantine' ), array( 'confidence_score' => $score, 'matched_rules' => wp_json_encode( wp_list_pluck( $matched, 'id' ) ), 'analysed_at' => current_time( 'mysql', true ), ), array( 'id' => $row->id ), array( '%d', '%s', '%s' ), array( '%d' ) ); return array( 'success' => true, 'score' => $score, 'matched' => $matched ); } public static function get( $id ) { global $wpdb; $table = Argus_DB::table( 'quarantine' ); return $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE id = %d", $id ) ); // phpcs:ignore } const PER_PAGE = 10; public static function paginated( $page = 1, $status = null, $per_page = self::PER_PAGE ) { global $wpdb; $table = Argus_DB::table( 'quarantine' ); $page = max( 1, (int) $page ); $offset = ( $page - 1 ) * $per_page; if ( $status ) { $total = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE status = %s", $status ) ); // phpcs:ignore $rows = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$table} WHERE status = %s ORDER BY quarantined_at DESC LIMIT %d OFFSET %d", $status, $per_page, $offset ) // phpcs:ignore ); } else { $total = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table}" ); // phpcs:ignore $rows = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$table} ORDER BY quarantined_at DESC LIMIT %d OFFSET %d", $per_page, $offset ) // phpcs:ignore ); } foreach ( $rows as &$row ) { $row->matched_rules = $row->matched_rules ? json_decode( $row->matched_rules, true ) : array(); } return array( 'rows' => $rows, 'total' => $total, 'total_pages' => max( 1, (int) ceil( $total / $per_page ) ), 'page' => $page, ); } public static function count_open() { global $wpdb; $table = Argus_DB::table( 'quarantine' ); return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE status = %s", self::STATUS_QUARANTINED ) // phpcs:ignore ); } public static function counts_by_status() { global $wpdb; $table = Argus_DB::table( 'quarantine' ); $rows = $wpdb->get_results( "SELECT status, COUNT(*) AS cnt FROM {$table} GROUP BY status" ); // phpcs:ignore $out = array( self::STATUS_QUARANTINED => 0, self::STATUS_RESTORED => 0, self::STATUS_DELETED => 0 ); foreach ( $rows as $row ) { if ( isset( $out[ $row->status ] ) ) { $out[ $row->status ] = (int) $row->cnt; } } return $out; } public static function legacy_count() { $uploads = wp_get_upload_dir(); $dir = $uploads['basedir'] ?? ''; if ( ! $dir || ! is_dir( $dir ) ) { return 0; } $count = 0; $suffix = Argus_Malware_Scanner::QUARANTINE_SUFFIX; $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $dir, FilesystemIterator::SKIP_DOTS ), RecursiveIteratorIterator::LEAVES_ONLY ); foreach ( $iterator as $file ) { if ( $file->isFile() && $suffix === substr( $file->getFilename(), -strlen( $suffix ) ) ) { $count++; } } return $count; } }