admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'miintg_admin' ), 'cronDisabled' => miintg_cron_disabled(), ) ) . ';', 'before' ); } /** * @return string Current tab slug. */ function miintg_current_tab() { $tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'instructions'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended return in_array( $tab, array( 'instructions', 'settings', 'run-scan', 'results', 'logs', 'support' ), true ) ? $tab : 'instructions'; } /** * @return string Current Settings sub-tab slug. */ function miintg_current_subtab() { $subtab = isset( $_GET['subtab'] ) ? sanitize_key( wp_unslash( $_GET['subtab'] ) ) : 'connectors'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended return in_array( $subtab, array( 'connectors', 'general' ), true ) ? $subtab : 'connectors'; } function miintg_render_admin_page() { if ( ! current_user_can( 'manage_options' ) ) { return; } $tab = miintg_current_tab(); $base_url = admin_url( 'admin.php?page=mi-integrity' ); $tabs = array( 'instructions' => array( 'label' => 'Instructions', 'icon' => 'dashicons-info' ), 'settings' => array( 'label' => 'Settings', 'icon' => 'dashicons-admin-generic' ), 'run-scan' => array( 'label' => 'Run Scan', 'icon' => 'dashicons-shield' ), 'results' => array( 'label' => 'Results', 'icon' => 'dashicons-list-view' ), 'logs' => array( 'label' => 'Logs', 'icon' => 'dashicons-media-text' ), 'support' => array( 'label' => 'Support', 'icon' => 'dashicons-sos' ), ); ?>

Quick Integrity Scanner

Read-only file integrity checks — by Medium Interactive

'settings', 'subtab' => 'connectors' ), $base ); ?>

This plugin checks your site's files for unexpected changes. It works on its own, with no account and no external service to connect. Nothing is changed on your site; every check is read-only.

1

Run a scan

Open Run Scan and click Run Full Scan. It checks six areas and usually finishes within a minute or two.

2

Read the results

Open Results and click View Details. Each area is a section you can expand or collapse. Critical items are worth checking right away. Review items are lower risk.

3

Mark what you have checked

If a flagged file is expected (for example a file a plugin creates), click Mark reviewed. It is left out of future scans until the file changes again.

What it checks

AreaWhat it looks for
WordPress Core FilesFiles in wp-admin, wp-includes and the site root that differ from the official WordPress.org version, are missing, or should not be there.
PluginsEach plugin's files compared with the WordPress.org copy, plus whether an update is available. Premium plugins are listed but cannot be compared.
Uploads folderPHP files, double extensions and PHP code hiding in the uploads folder, where only media should be.
Autoloaded codeMust-use plugins and drop-ins, which run without appearing in the Plugins list.
Config filesA fingerprint of wp-config.php and .htaccess so you can spot changes, and the status of a few security settings.
Administrator accountsAdministrators hidden from the normal user list, and permissions granted outside a normal role.

Good to know

Optional: connect an AI assistant

You do not need this to run scans. If you want an AI assistant such as Claude to run the checks for you, you can connect one under Settings → Connectors. It is off until you read the data-sharing notice there and confirm it.

'Connectors', 'general' => 'Other Settings', ); ?>

Optional. Scans work without this. You only need this page if you want an AI assistant (for example Claude) to run the checks for you.

What is sent to the AI assistant

Nothing is sent unless you connect an assistant and it asks for a check. Each check returns a short text summary of the results. Your files are never uploaded and file contents are never read.

SentWordPress version and locale. Relative paths of core and plugin files that are modified, missing or unexpected. Plugin names, versions and whether an update exists. File names, sizes, dates and MD5 hashes of mu-plugins, drop-ins, wp-config.php and .htaccess. Paths of unusual files in the uploads folder. The status of DISALLOW_FILE_EDIT, DISALLOW_FILE_MODS and WP_DEBUG. User IDs and logins of administrators that look hidden or have unusual permissions.
Never sentFile contents, passwords, email addresses, post or page content, or database contents.
Goes toThe AI assistant you connect and its provider (for Claude, Anthropic). That is a third party outside this website. Their own terms and privacy policy apply to what they receive.

Cost

This plugin is free. Connecting an assistant uses that assistant's own usage allowance (for Claude, your Claude plan). Because files are never uploaded, usage does not grow with the size of your site. A check returns a text summary with counts and lists of file paths, capped at 200 entries per list. A clean site returns a short summary; a site with many findings returns more. How that counts against your plan is set by your AI provider, so check their usage page.

Connection settings

One URL with your token built in. Paste it as the connector's Server URL and nothing else.

Enable MCP
Enable MCP logging
Access Token ' . esc_html( $last4 ) . '' : 'No token generated yet'; ?>  
Url for connectors ?token=
Enable OAuth 2.1

Only one token is active at a time — generating a new one immediately retires the old one and disconnects anything still using it. The plaintext token is shown once, right after you generate it; the site only ever stores a one-way hash, so it can't be shown again after that (regenerate if lost). The old header method (X-MIINTG-Key) still works too, for adapters that prefer it over a URL parameter.

Connection status

AI Copilot plugin (optional)
Data sharingConfirmed' : 'Not confirmed. Remote access is off.'; ?>
REST namespace/wp-json/mi-integrity/v1/ — active
MCP endpoint/wp-json/mi-integrity/v1/mcp — disabled'; ?>

Connector request history and scan activity moved to the Logs tab.

Other Settings

Results retentionLast runs (oldest pruned automatically after each scan)
Background executiondisabled on this site. Use Force Run on the Run Scan tab.' : 'WP-Cron — one check per cron event'; ?>
Plugin version

Reviewed files

Files you marked as reviewed on the Results tab (core and plugin findings only). Each is left out of scan counts while the file stays in the same state, and counts again if its contents change. Removing one makes it count again from the next scan.

Nothing marked as reviewed.

$entry ) : ?>
WhereFileFindingReviewed byWhen (UTC)

Run a scan

WP-Cron is turned off on this site. Scans cannot start on their own. After you click Run Full Scan, use the Run Pending Checks Now button that appears. If your host runs cron from the server instead, scans may work without it.

Runs all six checks in the background (WP-Cron) — one check per cron event, so a slow site or a cold checksum-cache never risks a page-request timeout. Results appear on the Results tab as each check finishes.

Recent runs

No scans yet — run one from the Run Scan tab.

#StartedResultDuration

Activity log

The last 50 actions taken from this plugin's admin screens — who did what, when. Never records a token or key value.

No activity logged yet.

Time (UTC)UserActionDetail

Recent MCP requests

The last 20 requests to the connector endpoint — use this to check what a failed Claude tool call actually did server-side (whether a credential arrived, and whether it was accepted) without needing browser dev tools.

No requests logged yet.

Time (UTC)MethodCredential SentAuth ResultClient
No'; } ?> Passed'; } else { echo 'Failed'; } ?>

Scan history

Permanently deletes every stored scan run and result (not just the log views above) — the Results tab will be empty afterward. Does not affect the Activity Log or MCP Request Log above.

Get Support from Medium Interactive

This plugin is built and maintained by Medium Interactive, a digital marketing and SEO agency. If you have a question, found a bug, or want a new feature, get in touch through our contact page and a real person will help.

Contact Medium Interactive

We use this plugin daily across our own client sites, so feedback directly shapes its roadmap.

'Not allowed.' ), 403 ); } $run_id = miintg_schedule_scan( null, 'manual', get_current_user_id() ); miintg_log_activity( 'Started scan', 'Run #' . $run_id ); wp_send_json_success( array( 'run_id' => $run_id ) ); } add_action( 'wp_ajax_miintg_poll_run', 'miintg_ajax_poll_run' ); function miintg_ajax_poll_run() { check_ajax_referer( 'miintg_admin' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Not allowed.' ), 403 ); } $run_id = isset( $_POST['run_id'] ) ? (int) $_POST['run_id'] : 0; $run = miintg_db_get_run_with_checks( $run_id ); if ( ! $run ) { wp_send_json_error( array( 'message' => 'No such run.' ), 404 ); } // Trim result payloads out of the poll response — the UI only needs // per-check status here, not the full JSON (that's the View button). foreach ( $run['checks'] as &$check ) { unset( $check['result'] ); } wp_send_json_success( $run ); } add_action( 'wp_ajax_miintg_view_run', 'miintg_ajax_view_run' ); function miintg_ajax_view_run() { check_ajax_referer( 'miintg_admin' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Not allowed.' ), 403 ); } $run_id = isset( $_POST['run_id'] ) ? (int) $_POST['run_id'] : 0; $run = miintg_db_get_run_with_checks( $run_id ); if ( ! $run ) { wp_send_json_error( array( 'message' => 'No such run.' ), 404 ); } wp_send_json_success( array( 'html' => miintg_render_run_report_html( $run ) ) ); } add_action( 'wp_ajax_miintg_generate_key', 'miintg_ajax_generate_key' ); function miintg_ajax_generate_key() { check_ajax_referer( 'miintg_admin' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Not allowed.' ), 403 ); } if ( ! miintg_ai_consent() ) { wp_send_json_error( array( 'message' => 'Confirm the data-sharing notice first.' ), 400 ); } $key = miintg_generate_api_key( get_current_user_id() ); miintg_log_activity( 'Generated access token', 'Ends in ' . substr( $key, -4 ) ); wp_send_json_success( array( 'key' => $key, 'last4' => substr( $key, -4 ) ) ); } add_action( 'wp_ajax_miintg_revoke_key', 'miintg_ajax_revoke_key' ); function miintg_ajax_revoke_key() { check_ajax_referer( 'miintg_admin' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Not allowed.' ), 403 ); } miintg_revoke_api_key(); miintg_log_activity( 'Revoked access token' ); wp_send_json_success(); } add_action( 'wp_ajax_miintg_force_run', 'miintg_ajax_force_run' ); function miintg_ajax_force_run() { check_ajax_referer( 'miintg_admin' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Not allowed.' ), 403 ); } $run_id = isset( $_POST['run_id'] ) ? (int) $_POST['run_id'] : 0; $run = miintg_db_get_run_with_checks( $run_id ); if ( ! $run ) { wp_send_json_error( array( 'message' => 'No such run.' ), 404 ); } foreach ( $run['checks'] as $check ) { if ( in_array( $check['status'], array( 'complete', 'error' ), true ) ) { continue; } // Drop the pending cron event for this check so it doesn't also // fire later and re-record a result that's already in. wp_clear_scheduled_hook( MIINTG_CRON_HOOK, array( $run_id, $check['check_name'] ) ); // Runs inline, in this request, instead of waiting on WP-Cron. miintg_cron_run_check( $run_id, $check['check_name'] ); } miintg_log_activity( 'Force-ran pending checks', 'Run #' . $run_id ); wp_send_json_success(); } add_action( 'wp_ajax_miintg_toggle_setting', 'miintg_ajax_toggle_setting' ); function miintg_ajax_toggle_setting() { check_ajax_referer( 'miintg_admin' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Not allowed.' ), 403 ); } $setting = isset( $_POST['setting'] ) ? sanitize_key( wp_unslash( $_POST['setting'] ) ) : ''; $allowed = array( 'miintg_mcp_enabled', 'miintg_mcp_logging_enabled', 'miintg_ai_consent' ); if ( ! in_array( $setting, $allowed, true ) ) { wp_send_json_error( array( 'message' => 'Unknown setting.' ), 400 ); } $value = isset( $_POST['value'] ) && 'true' === sanitize_text_field( wp_unslash( $_POST['value'] ) ); update_option( $setting, $value, false ); $labels = array( 'miintg_mcp_enabled' => 'MCP endpoint', 'miintg_mcp_logging_enabled' => 'MCP request logging', 'miintg_ai_consent' => 'data sharing with an AI assistant', ); miintg_log_activity( ( $value ? 'Enabled ' : 'Disabled ' ) . $labels[ $setting ] ); wp_send_json_success( array( 'value' => $value ) ); } add_action( 'wp_ajax_miintg_mark_reviewed', 'miintg_ajax_mark_reviewed' ); function miintg_ajax_mark_reviewed() { check_ajax_referer( 'miintg_admin' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Not allowed.' ), 403 ); } $scope = isset( $_POST['scope'] ) ? sanitize_text_field( wp_unslash( $_POST['scope'] ) ) : ''; $path = isset( $_POST['path'] ) ? sanitize_text_field( wp_unslash( $_POST['path'] ) ) : ''; $kind = isset( $_POST['kind'] ) ? sanitize_key( wp_unslash( $_POST['kind'] ) ) : ''; if ( ! in_array( $kind, array( 'modified', 'missing', 'unexpected' ), true ) ) { wp_send_json_error( array( 'message' => 'Unknown finding type.' ), 400 ); } // The state is read from the file now, server-side — never trusted from the browser. $state = miintg_reviewed_state( $scope, $path, $kind ); if ( null === $state ) { wp_send_json_error( array( 'message' => 'Could not read that file, or its state no longer matches this finding. Run a new scan and try again.' ), 400 ); } miintg_reviewed_add( $scope, $path, $kind, $state ); miintg_log_activity( 'Marked file as reviewed', ( 'core' === $scope ? 'core' : $scope ) . ': ' . $path ); wp_send_json_success(); } add_action( 'wp_ajax_miintg_unreview', 'miintg_ajax_unreview' ); function miintg_ajax_unreview() { check_ajax_referer( 'miintg_admin' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Not allowed.' ), 403 ); } $key = isset( $_POST['key'] ) ? sanitize_key( wp_unslash( $_POST['key'] ) ) : ''; if ( ! miintg_reviewed_remove( $key ) ) { wp_send_json_error( array( 'message' => 'That entry no longer exists.' ), 404 ); } miintg_log_activity( 'Removed a reviewed file' ); wp_send_json_success(); } add_action( 'wp_ajax_miintg_clear_reviewed', 'miintg_ajax_clear_reviewed' ); function miintg_ajax_clear_reviewed() { check_ajax_referer( 'miintg_admin' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Not allowed.' ), 403 ); } miintg_reviewed_clear(); miintg_log_activity( 'Cleared all reviewed files' ); wp_send_json_success(); } add_action( 'wp_ajax_miintg_clear_activity_log', 'miintg_ajax_clear_activity_log' ); function miintg_ajax_clear_activity_log() { check_ajax_referer( 'miintg_admin' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Not allowed.' ), 403 ); } delete_option( 'miintg_activity_log' ); wp_send_json_success(); } add_action( 'wp_ajax_miintg_clear_mcp_log', 'miintg_ajax_clear_mcp_log' ); function miintg_ajax_clear_mcp_log() { check_ajax_referer( 'miintg_admin' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Not allowed.' ), 403 ); } delete_option( 'miintg_mcp_log' ); miintg_log_activity( 'Cleared MCP request log' ); wp_send_json_success(); } add_action( 'wp_ajax_miintg_clear_scan_history', 'miintg_ajax_clear_scan_history' ); function miintg_ajax_clear_scan_history() { check_ajax_referer( 'miintg_admin' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Not allowed.' ), 403 ); } miintg_db_clear_all_runs(); miintg_log_activity( 'Cleared scan history' ); wp_send_json_success(); }