Plugin Directory

Changeset 3682123


Ignore:
Timestamp:
09/05/2026 05:52:11 AM (7 weeks ago)
Author:
gopalbogati
Message:

Release 1.4.0: guided security dashboard, bounded file checks, saved reports, clearer listing and refreshed assets

Location:
site-security-auditor
Files:
17 added
8 edited
1 copied

Legend:

Unmodified
Added
Removed
  • site-security-auditor/tags/1.4.0/languages/site-security-auditor.pot

    r3365444 r3682123  
    1 # POT placeholder
     1# Copyright (C) 2026 Gopal Bogati
     2# This file is distributed under the GPL-2.0-or-later.
     3msgid ""
     4msgstr ""
     5"Project-Id-Version: Site Security Auditor 1.4.0\n"
     6"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/trunk\n"
     7"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
     8"Language-Team: LANGUAGE <LL@li.org>\n"
     9"MIME-Version: 1.0\n"
     10"Content-Type: text/plain; charset=UTF-8\n"
     11"Content-Transfer-Encoding: 8bit\n"
     12"POT-Creation-Date: 2026-09-05T05:49:37+00:00\n"
     13"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
     14"X-Generator: WP-CLI 2.12.0\n"
     15"X-Domain: site-security-auditor\n"
     16
     17#. Plugin Name of the plugin
     18#: site-security-auditor.php
     19msgid "Site Security Auditor"
     20msgstr ""
     21
     22#. Description of the plugin
     23#: site-security-auditor.php
     24msgid "Review plugin updates, compare plugin and theme files, and follow a clear WordPress security checklist."
     25msgstr ""
     26
     27#. Author of the plugin
     28#: site-security-auditor.php
     29msgid "Gopal Bogati"
     30msgstr ""
     31
     32#. Author URI of the plugin
     33#: site-security-auditor.php
     34msgid "https://profiles.wordpress.org/gopalbogati/"
     35msgstr ""
     36
     37#: includes/dashboard.php:8
     38msgid "SITE SECURITY AUDITOR"
     39msgstr ""
     40
     41#: includes/dashboard.php:9
     42msgid "Your security review"
     43msgstr ""
     44
     45#: includes/dashboard.php:10
     46msgid "Know what changed. Understand your next step."
     47msgstr ""
     48
     49#: includes/dashboard.php:13
     50msgid "Plugin updates available"
     51msgstr ""
     52
     53#: includes/dashboard.php:13
     54msgid "Review plugin information"
     55msgstr ""
     56
     57#: includes/dashboard.php:14
     58msgid "Settings to review"
     59msgstr ""
     60
     61#: includes/dashboard.php:14
     62msgid "Open your checklist"
     63msgstr ""
     64
     65#: includes/dashboard.php:15
     66msgid "File comparison"
     67msgstr ""
     68
     69#: includes/dashboard.php:15
     70msgid "Baseline ready"
     71msgstr ""
     72
     73#: includes/dashboard.php:15
     74msgid "Set up first"
     75msgstr ""
     76
     77#: includes/dashboard.php:15
     78msgid "Review files"
     79msgstr ""
     80
     81#: includes/dashboard.php:17
     82msgid "These checks reduce guesswork, not all security risks. This is not a malware scanner, firewall, or vulnerability database."
     83msgstr ""
     84
     85#: includes/dashboard.php:19
     86msgid "1. Check for file changes"
     87msgstr ""
     88
     89#: includes/dashboard.php:20
     90msgid "A baseline is a snapshot of files you trust. Compare against it after maintenance or whenever you want to investigate unexpected changes."
     91msgstr ""
     92
     93#: includes/dashboard.php:22
     94msgid "Back up your site and confirm its files are trustworthy."
     95msgstr ""
     96
     97#: includes/dashboard.php:23
     98msgid "Save a baseline, then run a comparison when you return."
     99msgstr ""
     100
     101#: includes/dashboard.php:24
     102msgid "Review changes with your update history or developer before replacing the baseline."
     103msgstr ""
     104
     105#: includes/dashboard.php:27
     106msgid "Baseline saved:"
     107msgstr ""
     108
     109#: includes/dashboard.php:29
     110msgid "Compare files now"
     111msgstr ""
     112
     113#: includes/dashboard.php:31
     114msgid "Replace the trusted baseline"
     115msgstr ""
     116
     117#: includes/dashboard.php:31
     118msgid "Create your first baseline"
     119msgstr ""
     120
     121#: includes/dashboard.php:32
     122msgid "Saving a snapshot does not prove that files are clean. Replacing it accepts all current changes as your new reference."
     123msgstr ""
     124
     125#: includes/dashboard.php:33
     126msgid "Save trusted baseline"
     127msgstr ""
     128
     129#: includes/dashboard.php:36
     130#: includes/dashboard.php:37
     131msgid "Latest file report"
     132msgstr ""
     133
     134#. translators: %d: number of files read.
     135#: includes/dashboard.php:39
     136#, php-format
     137msgid "%d files read"
     138msgstr ""
     139
     140#: includes/dashboard.php:41
     141msgid "Incomplete — no comparison or baseline replacement was made."
     142msgstr ""
     143
     144#: includes/dashboard.php:42
     145msgid "Finish any updates, check file permissions with your host, and retry. Large sites may need a host-level integrity tool."
     146msgstr ""
     147
     148#: includes/dashboard.php:45
     149msgid "Baseline saved. Your next comparison will use this snapshot."
     150msgstr ""
     151
     152#: includes/dashboard.php:47
     153msgid "Changes are not necessarily malicious: updates and intentional edits also change files. No changes does not guarantee a secure site."
     154msgstr ""
     155
     156#: includes/dashboard.php:48
     157msgid "Added"
     158msgstr ""
     159
     160#: includes/dashboard.php:48
     161msgid "Modified"
     162msgstr ""
     163
     164#: includes/dashboard.php:48
     165msgid "Removed"
     166msgstr ""
     167
     168#: includes/dashboard.php:53
     169msgid "Showing the first 100 paths. Download the full report below."
     170msgstr ""
     171
     172#: includes/dashboard.php:57
     173msgid "Download file report (JSON)"
     174msgstr ""
     175
     176#: includes/dashboard.php:58
     177msgid "The download includes relative file paths. Share only with people you trust."
     178msgstr ""
     179
     180#: includes/dashboard.php:61
     181msgid "What is included in a scan?"
     182msgstr ""
     183
     184#: includes/dashboard.php:62
     185msgid "Plugin and registered theme directories: PHP, PHP7, PHTML, INC, TWIG, JSON, JS and CSS files. WordPress core, uploads, must-use plugins, database content and other file types are excluded. Symbolic links are not followed and make a scan incomplete."
     186msgstr ""
     187
     188#: includes/dashboard.php:63
     189msgid "Limits: 20,000 directory entries, 100 MiB total, 8 MiB per file and an approximately 8-second processing budget. An incomplete scan never replaces your baseline. Files should not be edited while a scan runs."
     190msgstr ""
     191
     192#: includes/dashboard.php:64
     193msgid "Your older baseline excludes JS and CSS. Comparisons keep that scope until you save a new trusted baseline."
     194msgstr ""
     195
     196#: includes/dashboard.php:68
     197msgid "2. Review your plugins"
     198msgstr ""
     199
     200#: includes/dashboard.php:69
     201msgid "Uses the update information already stored by WordPress. Unknown means information is missing or more than two days old; premium plugins may use their own update systems."
     202msgstr ""
     203
     204#: includes/dashboard.php:70
     205msgid "Open WordPress Updates"
     206msgstr ""
     207
     208#: includes/dashboard.php:71
     209msgid "Manage plugins"
     210msgstr ""
     211
     212#: includes/dashboard.php:72
     213msgid "Plugin maintenance table"
     214msgstr ""
     215
     216#: includes/dashboard.php:74
     217msgid "Plugin"
     218msgstr ""
     219
     220#: includes/dashboard.php:74
     221msgid "Status"
     222msgstr ""
     223
     224#: includes/dashboard.php:74
     225msgid "Update information"
     226msgstr ""
     227
     228#: includes/dashboard.php:74
     229msgid "Next step"
     230msgstr ""
     231
     232#: includes/dashboard.php:78
     233msgid "Active"
     234msgstr ""
     235
     236#: includes/dashboard.php:78
     237msgid "Inactive"
     238msgstr ""
     239
     240#: includes/dashboard.php:79
     241msgid "Update available"
     242msgstr ""
     243
     244#: includes/dashboard.php:79
     245msgid "Unknown — refresh in Updates"
     246msgstr ""
     247
     248#: includes/dashboard.php:79
     249msgid "No update reported"
     250msgstr ""
     251
     252#: includes/dashboard.php:80
     253msgid "Back up, review the release notes, then update."
     254msgstr ""
     255
     256#: includes/dashboard.php:80
     257msgid "Remove it if no longer needed, after checking dependencies."
     258msgstr ""
     259
     260#: includes/dashboard.php:80
     261msgid "Keep maintained and review auto-update settings."
     262msgstr ""
     263
     264#: includes/dashboard.php:81
     265msgid "Auto-updates selected in WordPress; host or plugin policy may override this."
     266msgstr ""
     267
     268#: includes/dashboard.php:84
     269msgid "No update reported is not a security verdict. This tool does not check a vulnerability database or certify plugins as safe."
     270msgstr ""
     271
     272#: includes/dashboard.php:87
     273msgid "3. Follow your security checklist"
     274msgstr ""
     275
     276#: includes/dashboard.php:89
     277msgid "Configured"
     278msgstr ""
     279
     280#: includes/dashboard.php:89
     281msgid "Review"
     282msgstr ""
     283
     284#: includes/dashboard.php:92
     285msgid "Also check these with your host or administrator"
     286msgstr ""
     287
     288#: includes/dashboard.php:93
     289msgid "Test your backups, use unique passwords and two-factor authentication, remove unused accounts, and keep WordPress and themes updated. These are manual checks; this plugin does not verify them."
     290msgstr ""
     291
     292#: includes/dashboard.php:94
     293msgid "Open WordPress Site Health"
     294msgstr ""
     295
     296#: includes/dashboard.php:95
     297msgid "Read the official security guide"
     298msgstr ""
     299
     300#: includes/dashboard.php:97
     301msgid "Read-only checks. No automatic file edits, tracking, or additional remote metadata requests. Only the latest file report is stored."
     302msgstr ""
     303
     304#: site-security-auditor.php:36
     305msgid "Security Audit"
     306msgstr ""
     307
     308#: site-security-auditor.php:45
     309msgid "You are not allowed to perform this action."
     310msgstr ""
     311
     312#: site-security-auditor.php:66
     313msgid "Run a file check first."
     314msgstr ""
     315
     316#: site-security-auditor.php:95
     317msgid "Create a trusted baseline before comparing files."
     318msgstr ""
     319
     320#: site-security-auditor.php:139
     321msgid "Encrypted website address"
     322msgstr ""
     323
     324#: site-security-auditor.php:139
     325msgid "Use HTTPS for your public site. Ask your host to configure a certificate and redirects."
     326msgstr ""
     327
     328#: site-security-auditor.php:140
     329msgid "HTTPS required for administration"
     330msgstr ""
     331
     332#: site-security-auditor.php:140
     333msgid "Ask your host to check FORCE_SSL_ADMIN and your HTTPS proxy configuration."
     334msgstr ""
     335
     336#: site-security-auditor.php:141
     337msgid "Dashboard code editing disabled"
     338msgstr ""
     339
     340#: site-security-auditor.php:141
     341msgid "Consider DISALLOW_FILE_EDIT in wp-config.php. Back up the file and ask your developer before editing it."
     342msgstr ""
     343
     344#: site-security-auditor.php:142
     345msgid "WordPress debug display off"
     346msgstr ""
     347
     348#: site-security-auditor.php:142
     349msgid "On production sites, keep WordPress debug messages out of public pages. Server-level error display needs a separate host check."
     350msgstr ""
     351
     352#: site-security-auditor.php:160
     353msgid "I have reviewed this site and trust its current files. This replaces the previous baseline."
     354msgstr ""
  • site-security-auditor/tags/1.4.0/readme.txt

    r3365450 r3682123  
    1 === Site Security Auditor ===
     1=== Site Security Auditor – Plugin & File Checks ===
    22Contributors: gopalbogati
     3Tags: security audit, file integrity, plugin updates, security checklist, site health
     4Requires at least: 5.8
     5Tested up to: 7.0
    36Requires PHP: 7.4
    4 Tested up to: 6.8
    5 Stable tag: 1.0
     7Stable tag: 1.4.0
    68License: GPLv2 or later
    79License URI: https://www.gnu.org/licenses/gpl-2.0.html
    810
    9 A simple admin tool to audit plugins, file integrity (baseline + changes), and hardening settings. No tracking or updater hooks.
     11Review plugin updates, detect plugin and theme file changes, and follow a clear WordPress security checklist. No account required.
    1012
    1113== Description ==
    12 Site Security Auditor helps admins spot risks from outdated/inactive plugins, monitor file changes in plugins/themes, and review hardening settings like DISALLOW_FILE_EDIT or FORCE_SSL_ADMIN. It uses only WordPress core APIs and stores data in options.
     14
     15Site Security Auditor gives WordPress administrators a practical security review dashboard. See plugin update information, compare plugin and theme files against a trusted snapshot, and understand which settings deserve attention.
     16
     17Open **Tools → Security Audit** to start. Each section explains what it checks, what the result means, and what to do next. It is designed for routine maintenance and investigating unexpected file changes, without automatically editing your site.
     18
     19= What you can do =
     20
     21* **Review plugin updates:** see available updates, inactive plugins and missing or stale update information. Open the familiar WordPress update screen to take action.
     22* **Compare files:** save a trusted SHA-256 baseline, then identify added, modified and removed code files in plugin and theme directories.
     23* **Return to your latest report:** the most recent file check stays visible with its timestamp and file count. Simply opening the dashboard does not start another scan.
     24* **Download a file report:** export the latest result as JSON to review with your developer or host.
     25* **Follow a plain-language checklist:** review HTTPS configuration, dashboard code editing and WordPress debug display, with links to Site Health and official guidance.
     26* **Use a responsive dashboard:** clear sections, keyboard-accessible controls and a scrollable plugin table on small screens.
     27
     28= A useful maintenance routine =
     29
     301. Review available updates and take a backup.
     312. Confirm your files are trustworthy before saving the first baseline.
     323. Run a comparison after maintenance or when investigating changes.
     334. Match changed files to intentional updates or edits.
     345. Replace the baseline only after reviewing and trusting the current files.
     35
     36A baseline records the current state; it does not prove that state is clean. Keep independent backups and ask a trusted professional about unexplained changes.
     37
     38= Scope and limitations =
     39
     40New baselines cover PHP, PHP7, PHTML, INC, TWIG, JSON, JS and CSS files inside the configured plugin directory and registered theme directories. WordPress core, uploads, must-use plugins, database content and other file types are excluded. Symbolic links are not followed and cause an incomplete result.
     41
     42Each run is bounded to 20,000 directory entries, 100 MiB of file data, 8 MiB per file and an approximately 8-second processing budget. Unreadable files or exceeded limits produce an incomplete report. Incomplete scans never replace a baseline or present a partial comparison as complete. Large sites may need a host-level integrity tool. Avoid editing files or running updates during a scan.
     43
     44This plugin is **not a malware scanner, firewall or vulnerability database**. It does not repair files, guarantee security, or certify a plugin as safe. An update or legitimate edit can change a file; unchanged files can still contain problems.
     45
     46= Privacy and performance =
     47
     48No account, API key, telemetry or additional remote metadata requests are required. The dashboard uses the update information already maintained by WordPress; WordPress itself may contact its normal update services.
     49
     50File hashes, relative paths, baseline time and the latest file report are stored in the site's options, without autoloading them. Reports are restricted to administrators; on multisite they require a network administrator. Downloaded reports contain relative file paths, so share them only with trusted people. Deactivation and deletion preserve these records to avoid silently discarding your trusted reference.
    1351
    1452== Installation ==
    15 1. Upload the ZIP via Plugins → Add New → Upload Plugin
    16 2. Activate
    17 3. Go to Tools → Security Audit
    18 4. Create/Update Baseline → Scan Now
    1953
    20 == Changeling ==
     541. Install Site Security Auditor from Plugins → Add New, or upload the plugin ZIP.
     552. Activate the plugin.
     563. Open Tools → Security Audit using an administrator account.
     574. Read the baseline warning, confirm that you trust the current files, and choose Save trusted baseline.
     585. Return and select Compare files now to review changes.
     59
     60On multisite, a network administrator can open the dashboard from a site's Tools menu. Baselines and reports are stored per site, while plugin and theme files may be shared across the network.
     61
     62== Frequently Asked Questions ==
     63
     64= Does it scan for malware or known vulnerabilities? =
     65
     66No. It compares code-file hashes with your own baseline and shows maintenance/configuration checks. Use other appropriate controls and professional advice alongside this tool.
     67
     68= When should I replace the baseline? =
     69
     70Only after verifying the current files, for example after reviewing an intentional plugin update. Replacing it accepts the current state as the reference and removes the previous comparison point.
     71
     72= Why does a plugin say Unknown? =
     73
     74WordPress has not supplied current update information for it, or that information is more than two days old. Open WordPress Updates to refresh it. Some premium or custom plugins use separate update systems. Unknown does not mean unsafe or safe.
     75
     76= What happens to my existing baseline when I update? =
     77
     78Your baseline is retained. Baselines made before 1.4.0 continue comparing their original file types, without JS and CSS. A newly saved trusted baseline includes JS and CSS. Review changes before replacing it.
     79
     80= Why is a scan incomplete? =
     81
     82The scan reached a resource limit, encountered a symbolic link, could not read a directory/file, or found a file changing during the run. The report explains the issue. Finish updates and ask your host to check permissions; large sites may need another integrity tool.
     83
     84= Does it change my security settings automatically? =
     85
     86No. It provides guidance and links. Configuration changes can affect hosting or integrations, so review them with your administrator.
     87
     88= Can I use this every day? =
     89
     90You can manually review the dashboard and compare files whenever useful. There are no scheduled scans or email alerts in this version. The dashboard stores only the latest file report; download reports you need to keep.
     91
     92== Screenshots ==
     93
     941. Security review dashboard with maintenance summaries and the trusted-baseline workflow.
     952. Saved file comparison with added, modified and removed file groups.
     963. Plugin maintenance information and a plain-language configuration checklist.
     97
     98== Changelog ==
     99
     100= 1.4.0 =
     101* New responsive dashboard, guided baseline workflow and actionable security checklist.
     102* Save the latest file report and download it as JSON.
     103* Run scans only through authenticated, nonce-protected POST actions.
     104* Preserve baselines when scans are incomplete; add resource limits and explicit coverage warnings.
     105* Include JS and CSS in new baselines while preserving legacy comparison scope.
     106* Use cached WordPress update data instead of per-plugin remote lookups on dashboard views.
     107* Recognize network-active plugins and restrict multisite access to network administrators.
     108* Refresh directory branding, documentation, screenshots and release metadata.
     109
    21110= 1.0 =
    22 * Add full dashboard sections and escaping
    23 * Fix text domain packaging
     111* Initial dashboard for plugin information, file baselines and hardening checks.
     112
     113== Upgrade Notice ==
     114
     115= 1.4.0 =
     116New dashboard and safer file checks. Existing baselines remain intact; newly saved baselines also include JS and CSS. Review changes before replacing a baseline.
     117
  • site-security-auditor/tags/1.4.0/site-security-auditor.php

    r3365444 r3682123  
    11<?php
    22/**
    3  * Plugin Name:       Site Security Auditor
    4  * Description:       Audits plugins, file integrity, and hardening to improve site security.
    5  * Version:           1.3.3
    6  * Author:            Gopal Bogati
    7  * Author URI:        https://wordpress.org/
    8  * Text Domain:       site-security-auditor
    9  * Domain Path:       /languages
     3 * Plugin Name: Site Security Auditor
     4 * Description: Review plugin updates, compare plugin and theme files, and follow a clear WordPress security checklist.
     5 * Version: 1.4.0
     6 * Author: Gopal Bogati
     7 * Author URI: https://profiles.wordpress.org/gopalbogati/
     8 * Text Domain: site-security-auditor
     9 * Domain Path: /languages
    1010 * Requires at least: 5.8
    11  * Tested up to:      6.8
    12  * Requires PHP:      7.4
    13  * License:           GPL-2.0+
     11 * Requires PHP: 7.4
     12 * License: GPL-2.0-or-later
    1413 */
     14if ( ! defined( 'ABSPATH' ) ) { exit; }
    1515define( 'SITESEAU_PLUGIN_FILE', __FILE__ );
    1616define( 'SITESEAU_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
    1717define( 'SITESEAU_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
     18require_once SITESEAU_PLUGIN_DIR . 'includes/class-scanner.php';
    1819
     20class Siteseau_Site_Security_Auditor {
     21        const OPT_HASHES = 'siteseau_hashes';
     22        const OPT_HASHES_TIME = 'siteseau_hashes_time';
     23        const OPT_REPORT = 'siteseau_report';
    1924
    20 if ( ! defined( 'ABSPATH' ) ) {
    21         exit;
    22 }
    23 
    24 if ( ! class_exists( 'Siteseau_Site_Security_Auditor' ) ) :
    25 
    26 /**
    27  * Main plugin class.
    28  */
    29 class Siteseau_Site_Security_Auditor {
    30 
    31         const OPT_HASHES      = 'siteseau_hashes';
    32         const OPT_HASHES_TIME = 'siteseau_hashes_time';
    33 
    34         /**
    35          * Bootstrap.
    36          */
    3725        public static function init() {
    3826                add_action( 'admin_menu', array( __CLASS__, 'add_menu' ) );
     27                add_action( 'admin_enqueue_scripts', array( __CLASS__, 'assets' ) );
    3928                add_action( 'admin_post_ssa_make_baseline', array( __CLASS__, 'handle_baseline' ) );
    4029                add_action( 'admin_post_ssa_run_scan', array( __CLASS__, 'handle_scan' ) );
     30                add_action( 'admin_post_ssa_export', array( __CLASS__, 'handle_export' ) );
    4131        }
    42 
    43         /**
    44          * Add Tools -> Security Audit page.
    45          */
     32        public static function capability() {
     33                return is_multisite() ? 'manage_network_options' : 'manage_options';
     34        }
    4635        public static function add_menu() {
    47                 add_management_page(
    48                         __( 'Security Audit', 'site-security-auditor' ),
    49                         __( 'Security Audit', 'site-security-auditor' ),
    50                         'manage_options',
    51                         'site-security-auditor',
    52                         array( __CLASS__, 'render_page' )
    53                 );
     36                add_management_page( __( 'Security Audit', 'site-security-auditor' ), __( 'Security Audit', 'site-security-auditor' ), self::capability(), 'site-security-auditor', array( __CLASS__, 'render_page' ) );
    5437        }
    55 
    56         /**
    57          * Create/update baseline handler.
    58          */
     38        public static function assets( $hook ) {
     39                if ( 'tools_page_site-security-auditor' === $hook ) {
     40                        wp_enqueue_style( 'siteseau-admin', SITESEAU_PLUGIN_URL . 'assets/admin.css', array(), '1.4.0' );
     41                }
     42        }
     43        private static function authorize( $action ) {
     44                if ( ! current_user_can( self::capability() ) || 'POST' !== ( isset( $_SERVER['REQUEST_METHOD'] ) ? $_SERVER['REQUEST_METHOD'] : '' ) ) {
     45                        wp_die( esc_html__( 'You are not allowed to perform this action.', 'site-security-auditor' ), '', array( 'response' => 403 ) );
     46                }
     47                check_admin_referer( $action );
     48        }
    5949        public static function handle_baseline() {
    60                 if ( ! current_user_can( 'manage_options' ) ) {
    61                         wp_die( esc_html__( 'Sorry, you are not allowed to do that.', 'site-security-auditor' ) );
    62                 }
    63                 check_admin_referer( 'ssa_make_baseline' );
    64 
    65                 $paths  = self::get_scan_paths();
    66                 $hashes = self::hash_paths( $paths );
    67                 update_option( self::OPT_HASHES, $hashes, false );
    68                 update_option( self::OPT_HASHES_TIME, time(), false );
    69 
    70                 wp_safe_redirect( add_query_arg( array( 'page' => 'site-security-auditor', 'ssa_msg' => 'baseline_done' ), admin_url( 'tools.php' ) ) );
     50                self::authorize( 'ssa_make_baseline' );
     51                self::run_audit( true );
     52                self::redirect();
     53        }
     54        public static function handle_scan() {
     55                self::authorize( 'ssa_run_scan' );
     56                self::run_audit( false );
     57                self::redirect();
     58        }
     59        private static function redirect() {
     60                wp_safe_redirect( admin_url( 'tools.php?page=site-security-auditor' ) );
    7161                exit;
    7262        }
    73 
    74         /**
    75          * Run scan handler.
    76          */
    77         public static function handle_scan() {
    78                 if ( ! current_user_can( 'manage_options' ) ) {
    79                         wp_die( esc_html__( 'Sorry, you are not allowed to do that.', 'site-security-auditor' ) );
    80                 }
    81                 check_admin_referer( 'ssa_run_scan' );
    82 
    83                 wp_safe_redirect( add_query_arg( array( 'page' => 'site-security-auditor', 'ssa_scan' => '1' ), admin_url( 'tools.php' ) ) );
     63        public static function handle_export() {
     64                self::authorize( 'ssa_export' );
     65                $report = get_option( self::OPT_REPORT, array() );
     66                if ( ! $report ) { wp_die( esc_html__( 'Run a file check first.', 'site-security-auditor' ) ); }
     67                nocache_headers();
     68                header( 'Content-Type: application/json; charset=utf-8' );
     69                header( 'Content-Disposition: attachment; filename="site-security-auditor-report.json"' );
     70                echo wp_json_encode( $report, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES );
    8471                exit;
    8572        }
    86 
    87         /**
    88          * Render admin page.
    89          */
    90         public static function render_page() {
    91                 if ( ! current_user_can( 'manage_options' ) ) {
    92                         return;
     73        protected static function scan_roots() {
     74                global $wp_theme_directories;
     75                $roots = array( 'wp-content/plugins' => WP_PLUGIN_DIR );
     76                $themes = (array) $wp_theme_directories;
     77                if ( ! $themes ) { $themes = array( WP_CONTENT_DIR . '/themes' ); }
     78                foreach ( array_values( array_unique( $themes ) ) as $i => $path ) {
     79                        $label = wp_normalize_path( $path ) === wp_normalize_path( WP_CONTENT_DIR . '/themes' ) ? 'wp-content/themes' : 'theme-root-' . $i;
     80                        $roots[ $label ] = $path;
    9381                }
    94 
    95                 $plugins_info = self::collect_plugins_info();
    96                 $hardening    = self::hardening_checks();
    97                 $baseline_ts  = intval( get_option( self::OPT_HASHES_TIME, 0 ) );
    98                 $hashes       = get_option( self::OPT_HASHES, array() );
    99                 $scan_results = array();
    100 
    101                 if ( isset( $_GET['ssa_scan'] ) && '1' === $_GET['ssa_scan'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
    102                         $scan_results = self::compare_hashes( is_array( $hashes ) ? $hashes : array(), self::hash_paths( self::get_scan_paths() ) );
    103                 }
    104 
    105                 echo '<div class="wrap">';
    106                 echo '<h1>' . esc_html__( 'Security Audit Dashboard', 'site-security-auditor' ) . '</h1>';
    107 
    108                 if ( isset( $_GET['ssa_msg'] ) && 'baseline_done' === $_GET['ssa_msg'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
    109                         echo '<div class="notice notice-success"><p>' . esc_html__( 'File integrity baseline created/updated.', 'site-security-auditor' ) . '</p></div>';
    110                 }
    111 
    112                 // Plugins Risk Overview.
    113                 echo '<h2>' . esc_html__( 'Plugins Risk Overview', 'site-security-auditor' ) . '</h2>';
    114                 echo '<p>' . esc_html__( 'Flags plugins with updates available, last updated over a year ago, or inactive status.', 'site-security-auditor' ) . '</p>';
    115                 echo '<table class="widefat striped"><thead><tr>';
    116                 echo '<th>' . esc_html__( 'Plugin', 'site-security-auditor' ) . '</th>';
    117                 echo '<th>' . esc_html__( 'Version', 'site-security-auditor' ) . '</th>';
    118                 echo '<th>' . esc_html__( 'Status', 'site-security-auditor' ) . '</th>';
    119                 echo '<th>' . esc_html__( 'Update Available', 'site-security-auditor' ) . '</th>';
    120                 echo '<th>' . esc_html__( 'Last Updated', 'site-security-auditor' ) . '</th>';
    121                 echo '<th>' . esc_html__( 'Risk', 'site-security-auditor' ) . '</th>';
    122                 echo '</tr></thead><tbody>';
    123 
    124                 if ( empty( $plugins_info ) ) {
    125                         echo '<tr><td colspan="6">' . esc_html__( 'No plugins found.', 'site-security-auditor' ) . '</td></tr>';
    126                 } else {
    127                         foreach ( $plugins_info as $row ) {
    128                                 $risk_labels = array();
    129                                 if ( ! empty( $row['update_available'] ) ) {
    130                                         $risk_labels[] = __( 'Update available', 'site-security-auditor' );
    131                                 }
    132                                 if ( ! empty( $row['stale'] ) ) {
    133                                         $risk_labels[] = __( 'Stale (> 1 year)', 'site-security-auditor' );
    134                                 }
    135                                 if ( empty( $row['active'] ) ) {
    136                                         $risk_labels[] = __( 'Inactive', 'site-security-auditor' );
    137                                 }
    138                                 echo '<tr>';
    139                                 echo '<td>' . esc_html( $row['name'] ) . '</td>';
    140                                 echo '<td>' . esc_html( $row['version'] ) . '</td>';
    141                                 echo '<td>' . ( ! empty( $row['active'] ) ? esc_html__( 'Active', 'site-security-auditor' ) : esc_html__( 'Inactive', 'site-security-auditor' ) ) . '</td>';
    142                                 echo '<td>' . ( ! empty( $row['update_available'] ) ? esc_html__( 'Yes', 'site-security-auditor' ) : esc_html__( 'No', 'site-security-auditor' ) ) . '</td>';
    143                                 echo '<td>' . esc_html( $row['last_updated'] ) . '</td>';
    144                                 echo '<td>' . ( $risk_labels ? esc_html( implode( ', ', $risk_labels ) ) : esc_html__( 'OK', 'site-security-auditor' ) ) . '</td>';
    145                                 echo '</tr>';
    146                         }
    147                 }
    148                 echo '</tbody></table>';
    149 
    150                 // File Integrity Monitor.
    151                 echo '<hr /><h2>' . esc_html__( 'File Integrity Monitor', 'site-security-auditor' ) . '</h2>';
    152                 echo '<p>' . esc_html__( 'Create a hash baseline for plugins/themes and scan to detect added, modified or removed files.', 'site-security-auditor' ) . '</p>';
    153 
    154                 if ( $baseline_ts ) {
    155                         /* translators: %s: date/time. */
    156                         $baseline_text = sprintf( esc_html__( 'Baseline last updated: %s', 'site-security-auditor' ), wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $baseline_ts ) );
    157                         echo '<p>' . esc_html( $baseline_text ) . '</p>';
    158                 } else {
    159                         echo '<p>' . esc_html__( 'No baseline yet.', 'site-security-auditor' ) . '</p>';
    160                 }
    161 
    162                 echo '<form method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '" style="display:inline-block;margin-right:10px;">';
    163                 wp_nonce_field( 'ssa_make_baseline' );
    164                 echo '<input type="hidden" name="action" value="ssa_make_baseline" />';
    165                 echo '<button type="submit" class="button button-primary" onclick="return confirm(\'' . esc_js( __( 'Create/Update baseline now?', 'site-security-auditor' ) ) . '\');">' . esc_html__( 'Create/Update Baseline', 'site-security-auditor' ) . '</button>';
    166                 echo '</form>';
    167 
    168                 echo '<form method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '" style="display:inline-block;">';
    169                 wp_nonce_field( 'ssa_run_scan' );
    170                 echo '<input type="hidden" name="action" value="ssa_run_scan" />';
    171                 echo '<button type="submit" class="button">' . esc_html__( 'Scan Now', 'site-security-auditor' ) . '</button>';
    172                 echo '</form>';
    173 
    174                 if ( ! empty( $scan_results ) ) {
    175                         echo '<h3 style="margin-top:20px;">' . esc_html__( 'Scan Results', 'site-security-auditor' ) . '</h3>';
    176                         echo '<table class="widefat striped"><thead><tr><th>' . esc_html__( 'Change', 'site-security-auditor' ) . '</th><th>' . esc_html__( 'File', 'site-security-auditor' ) . '</th></tr></thead><tbody>';
    177                         $printed = false;
    178                         foreach ( array( 'added', 'modified', 'removed' ) as $key ) {
    179                                 if ( ! empty( $scan_results[ $key ] ) ) {
    180                                         foreach ( $scan_results[ $key ] as $file ) {
    181                                                 echo '<tr><td>' . esc_html( ucfirst( $key ) ) . '</td><td>' . esc_html( $file ) . '</td></tr>';
    182                                                 $printed = true;
     82                return $roots;
     83        }
     84        /** Protected operation, invoked only by the authorized POST handlers. */
     85        protected static function run_audit( $make_baseline ) {
     86                $lock = (int) get_option( 'siteseau_scan_lock', 0 );
     87                if ( $lock && $lock < time() - 300 ) { delete_option( 'siteseau_scan_lock' ); }
     88                if ( ! add_option( 'siteseau_scan_lock', time(), '', false ) ) { return; }
     89                try {
     90                        $baseline_time = (int) get_option( self::OPT_HASHES_TIME, 0 );
     91                        $old = get_option( self::OPT_HASHES, array() );
     92                        $scope = $make_baseline ? 2 : (int) get_option( 'siteseau_scope', 1 );
     93                        $report = array( 'time' => time(), 'baseline_time' => $baseline_time, 'scope' => $scope, 'kind' => $make_baseline ? 'baseline' : 'comparison', 'complete' => false, 'files' => 0, 'issues' => array(), 'changes' => array() );
     94                        if ( ! $make_baseline && ( ! $baseline_time || ! is_array( $old ) ) ) {
     95                                $report['issues'][] = __( 'Create a trusted baseline before comparing files.', 'site-security-auditor' );
     96                        } else {
     97                                $limits = 1 === $scope ? array( 'extensions' => array( 'php', 'php7', 'phtml', 'inc', 'twig', 'json' ) ) : array();
     98                                // Older releases scanned these fixed directories, even with custom plugin/theme roots.
     99                                $roots = 1 === $scope ? array( 'wp-content/plugins' => WP_CONTENT_DIR . '/plugins', 'wp-content/themes' => WP_CONTENT_DIR . '/themes' ) : self::scan_roots();
     100                                $scan = Siteseau_Scanner::scan( $roots, $limits );
     101                                $report['complete'] = $scan['complete'];
     102                                $report['files'] = count( $scan['hashes'] );
     103                                $report['issues'] = $scan['issues'];
     104                                if ( $scan['complete'] ) {
     105                                        if ( $make_baseline ) {
     106                                                update_option( self::OPT_HASHES, $scan['hashes'], false );
     107                                                update_option( self::OPT_HASHES_TIME, $report['time'], false );
     108                                                update_option( 'siteseau_scope', 2, false );
     109                                                $report['baseline_time'] = $report['time'];
     110                                        } else {
     111                                                $report['changes'] = Siteseau_Scanner::compare( $old, $scan['hashes'] );
    183112                                        }
    184113                                }
    185114                        }
    186                         if ( ! $printed ) {
    187                                 echo '<tr><td colspan="2">' . esc_html__( 'No changes detected.', 'site-security-auditor' ) . '</td></tr>';
    188                         }
    189                         echo '</tbody></table>';
     115                        update_option( self::OPT_REPORT, $report, false );
     116                } finally {
     117                        delete_option( 'siteseau_scan_lock' );
    190118                }
    191 
    192                 // Hardening checks.
    193                 echo '<hr /><h2>' . esc_html__( 'Hardening Checks', 'site-security-auditor' ) . '</h2>';
    194                 echo '<table class="widefat striped"><thead><tr><th>' . esc_html__( 'Check', 'site-security-auditor' ) . '</th><th>' . esc_html__( 'Status', 'site-security-auditor' ) . '</th></tr></thead><tbody>';
    195 
    196                 echo '<tr><td>' . esc_html__( 'DISALLOW_FILE_EDIT', 'site-security-auditor' ) . '</td><td>' . ( ! empty( $hardening['disallow_file_edit'] ) ? esc_html__( 'Enabled', 'site-security-auditor' ) : esc_html__( 'Disabled', 'site-security-auditor' ) ) . '</td></tr>';
    197                 echo '<tr><td>' . esc_html__( 'FORCE_SSL_ADMIN', 'site-security-auditor' ) . '</td><td>' . ( ! empty( $hardening['force_ssl_admin'] ) ? esc_html__( 'Enabled', 'site-security-auditor' ) : esc_html__( 'Disabled', 'site-security-auditor' ) ) . '</td></tr>';
    198                 echo '<tr><td>' . esc_html__( 'Plugin auto-updates (general)', 'site-security-auditor' ) . '</td><td>' . ( ! empty( $hardening['plugin_updates_enabled'] ) ? esc_html__( 'Enabled', 'site-security-auditor' ) : esc_html__( 'Disabled', 'site-security-auditor' ) ) . '</td></tr>';
    199                 echo '<tr><td>' . esc_html__( 'XML-RPC availability', 'site-security-auditor' ) . '</td><td>' . ( ! empty( $hardening['xmlrpc_enabled'] ) ? esc_html__( 'Enabled', 'site-security-auditor' ) : esc_html__( 'Disabled', 'site-security-auditor' ) ) . '</td></tr>';
    200 
    201                 echo '</tbody></table>';
    202                 echo '</div>';
    203119        }
    204 
    205         /**
    206          * Collect plugin info and risk signals.
    207          *
    208          * @return array
    209          */
    210120        protected static function collect_plugins_info() {
    211                 if ( ! function_exists( 'get_plugins' ) ) {
    212                         require_once ABSPATH . 'wp-admin/includes/plugin.php';
     121                if ( ! function_exists( 'get_plugins' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; }
     122                $updates = get_site_transient( 'update_plugins' );
     123                $recent = is_object( $updates ) && ! empty( $updates->last_checked ) && $updates->last_checked > time() - 2 * DAY_IN_SECONDS;
     124                $auto = (array) get_site_option( 'auto_update_plugins', array() );
     125                $results = array();
     126                foreach ( get_plugins() as $file => $data ) {
     127                        $known = $recent && isset( $updates->checked[ $file ] ) && $updates->checked[ $file ] === $data['Version'] && ( isset( $updates->response[ $file ] ) || isset( $updates->no_update[ $file ] ) );
     128                        $results[] = array(
     129                                'name' => $data['Name'], 'version' => $data['Version'],
     130                                'active' => is_plugin_active( $file ) || is_plugin_active_for_network( $file ),
     131                                'update' => $known ? ( isset( $updates->response[ $file ] ) ? 'available' : 'none' ) : 'unknown',
     132                                'auto' => in_array( $file, $auto, true ),
     133                        );
    213134                }
    214                 $all_plugins = get_plugins();
    215                 $active      = get_option( 'active_plugins', array() );
    216                 $active_map  = array_fill_keys( $active, true );
    217 
    218                 $updates     = get_site_transient( 'update_plugins' );
    219                 $updates_map = array();
    220                 if ( isset( $updates->response ) && is_array( $updates->response ) ) {
    221                         foreach ( $updates->response as $file => $obj ) {
    222                                 $updates_map[ $file ] = true;
    223                         }
    224                 }
    225 
    226                 $results = array();
    227                 foreach ( $all_plugins as $file => $data ) {
    228                         $slug = dirname( $file );
    229                         $info = array(
    230                                 'name'            => isset( $data['Name'] ) ? $data['Name'] : $file,
    231                                 'version'         => isset( $data['Version'] ) ? $data['Version'] : '',
    232                                 'active'          => isset( $active_map[ $file ] ),
    233                                 'update_available'=> isset( $updates_map[ $file ] ),
    234                                 'last_updated'    => __( 'Unknown', 'site-security-auditor' ),
    235                                 'stale'           => false,
    236                         );
    237 
    238                         // Try wp.org API for last_updated.
    239                         if ( ! function_exists( 'plugins_api' ) ) {
    240                                
    241                         }
    242                         $args = array(
    243                                 'slug'   => $slug,
    244                                 'fields' => array( 'last_updated' => true ),
    245                         );
    246 if ( is_admin() && ! function_exists( 'plugins_api' ) ) {
    247         require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
    248 }
    249                         $api = @plugins_api( 'plugin_information', $args ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
    250                         if ( ! is_wp_error( $api ) && ! empty( $api->last_updated ) ) {
    251                                 $info['last_updated'] = mysql2date( get_option( 'date_format' ), $api->last_updated );
    252                                 $ts = strtotime( $api->last_updated );
    253                                 if ( $ts && ( time() - $ts ) > YEAR_IN_SECONDS ) {
    254                                         $info['stale'] = true;
    255                                 }
    256                         }
    257 
    258                         $results[] = $info;
    259                 }
    260 
    261135                return $results;
    262136        }
    263 
    264         /**
    265          * Directories to scan.
    266          *
    267          * @return array
    268          */
    269         protected static function get_scan_paths() {
     137        protected static function hardening_checks() {
    270138                return array(
    271                         WP_CONTENT_DIR . '/plugins',
    272                         WP_CONTENT_DIR . '/themes',
     139                        array( __( 'Encrypted website address', 'site-security-auditor' ), 'https' === wp_parse_url( home_url(), PHP_URL_SCHEME ), __( 'Use HTTPS for your public site. Ask your host to configure a certificate and redirects.', 'site-security-auditor' ) ),
     140                        array( __( 'HTTPS required for administration', 'site-security-auditor' ), (bool) force_ssl_admin(), __( 'Ask your host to check FORCE_SSL_ADMIN and your HTTPS proxy configuration.', 'site-security-auditor' ) ),
     141                        array( __( 'Dashboard code editing disabled', 'site-security-auditor' ), ( defined( 'DISALLOW_FILE_EDIT' ) && DISALLOW_FILE_EDIT ) || ( defined( 'DISALLOW_FILE_MODS' ) && DISALLOW_FILE_MODS ), __( 'Consider DISALLOW_FILE_EDIT in wp-config.php. Back up the file and ask your developer before editing it.', 'site-security-auditor' ) ),
     142                        array( __( 'WordPress debug display off', 'site-security-auditor' ), ! ( defined( 'WP_DEBUG' ) && WP_DEBUG && ( ! defined( 'WP_DEBUG_DISPLAY' ) || WP_DEBUG_DISPLAY ) ), __( 'On production sites, keep WordPress debug messages out of public pages. Server-level error display needs a separate host check.', 'site-security-auditor' ) ),
    273143                );
    274144        }
    275 
    276         /**
    277          * Hash files in given paths.
    278          *
    279          * @param array $paths Paths.
    280          * @return array Map file => hash.
    281          */
    282         protected static function hash_paths( $paths ) {
    283                 $map = array();
    284 
    285                 foreach ( $paths as $path ) {
    286                         if ( ! is_dir( $path ) ) {
    287                                 continue;
    288                         }
    289                         $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $path, FilesystemIterator::SKIP_DOTS ) );
    290                         foreach ( $iterator as $file ) {
    291                                 /** @var SplFileInfo $file */
    292                                 $ext = strtolower( pathinfo( $file->getFilename(), PATHINFO_EXTENSION ) );
    293                                 if ( in_array( $ext, array( 'php', 'php7', 'phtml', 'inc', 'twig', 'json' ), true ) ) {
    294                                         $real = $file->getPathname();
    295                                         $rel  = ltrim( str_replace( WP_CONTENT_DIR, 'wp-content', $real ), '/' );
    296                                         $hash = @hash_file( 'sha256', $real ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
    297                                         if ( $hash ) {
    298                                                 $map[ $rel ] = $hash;
    299                                         }
    300                                 }
    301                         }
     145        public static function render_page() {
     146                if ( ! current_user_can( self::capability() ) ) { return; }
     147                $plugins = self::collect_plugins_info();
     148                $checks = self::hardening_checks();
     149                $report = get_option( self::OPT_REPORT, array() );
     150                $baseline_time = (int) get_option( self::OPT_HASHES_TIME, 0 );
     151                $updates = count( array_filter( $plugins, function( $row ) { return 'available' === $row['update']; } ) );
     152                $review = count( array_filter( $checks, function( $row ) { return ! $row[1]; } ) );
     153                require SITESEAU_PLUGIN_DIR . 'includes/dashboard.php';
     154        }
     155        public static function action_form( $action, $label, $disabled = false, $confirm = false ) {
     156                echo '<form method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '" class="ssa-action">';
     157                wp_nonce_field( $action );
     158                echo '<input type="hidden" name="action" value="' . esc_attr( $action ) . '">';
     159                if ( $confirm ) {
     160                        echo '<label class="ssa-confirm"><input type="checkbox" required> ' . esc_html__( 'I have reviewed this site and trust its current files. This replaces the previous baseline.', 'site-security-auditor' ) . '</label>';
    302161                }
    303                 ksort( $map );
    304                 return $map;
    305         }
    306 
    307         /**
    308          * Compare old and new hash maps.
    309          *
    310          * @param array $old Old.
    311          * @param array $new New.
    312          * @return array
    313          */
    314         protected static function compare_hashes( $old, $new ) {
    315                 $old = is_array( $old ) ? $old : array();
    316                 $new = is_array( $new ) ? $new : array();
    317 
    318                 $added    = array_diff_key( $new, $old );
    319                 $removed  = array_diff_key( $old, $new );
    320                 $modified = array();
    321 
    322                 $common = array_intersect_key( $new, $old );
    323                 foreach ( $common as $file => $hash ) {
    324                         if ( $old[ $file ] !== $hash ) {
    325                                 $modified[ $file ] = $hash;
    326                         }
    327                 }
    328 
    329                 return array(
    330                         'added'    => array_keys( $added ),
    331                         'modified' => array_keys( $modified ),
    332                         'removed'  => array_keys( $removed ),
    333                 );
    334         }
    335 
    336         /**
    337          * Hardening checks.
    338          *
    339          * @return array
    340          */
    341         protected static function hardening_checks() {
    342                 // Generic on/off for auto-updates by type (does not hook into filters).
    343                 $auto_plugins = function_exists( 'wp_is_auto_update_enabled_for_type' ) ? (bool) wp_is_auto_update_enabled_for_type( 'plugin' ) : false;
    344                 $xmlrpc_on    = apply_filters( 'xmlrpc_enabled', true );
    345 
    346                 return array(
    347                         'disallow_file_edit' => defined( 'DISALLOW_FILE_EDIT' ) && DISALLOW_FILE_EDIT,
    348                         'force_ssl_admin'    => defined( 'FORCE_SSL_ADMIN' ) && FORCE_SSL_ADMIN,
    349                         'plugin_updates_enabled'=> $auto_plugins,
    350                         'xmlrpc_enabled'     => (bool) $xmlrpc_on,
    351                 );
     162                echo '<button class="button ' . ( 'ssa_run_scan' === $action ? 'button-primary' : '' ) . '" type="submit"' . disabled( $disabled, true, false ) . '>' . esc_html( $label ) . '</button></form>';
    352163        }
    353164}
    354 
    355165Siteseau_Site_Security_Auditor::init();
    356 
    357 endif;
  • site-security-auditor/trunk/languages/site-security-auditor.pot

    r3365444 r3682123  
    1 # POT placeholder
     1# Copyright (C) 2026 Gopal Bogati
     2# This file is distributed under the GPL-2.0-or-later.
     3msgid ""
     4msgstr ""
     5"Project-Id-Version: Site Security Auditor 1.4.0\n"
     6"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/trunk\n"
     7"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
     8"Language-Team: LANGUAGE <LL@li.org>\n"
     9"MIME-Version: 1.0\n"
     10"Content-Type: text/plain; charset=UTF-8\n"
     11"Content-Transfer-Encoding: 8bit\n"
     12"POT-Creation-Date: 2026-09-05T05:49:37+00:00\n"
     13"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
     14"X-Generator: WP-CLI 2.12.0\n"
     15"X-Domain: site-security-auditor\n"
     16
     17#. Plugin Name of the plugin
     18#: site-security-auditor.php
     19msgid "Site Security Auditor"
     20msgstr ""
     21
     22#. Description of the plugin
     23#: site-security-auditor.php
     24msgid "Review plugin updates, compare plugin and theme files, and follow a clear WordPress security checklist."
     25msgstr ""
     26
     27#. Author of the plugin
     28#: site-security-auditor.php
     29msgid "Gopal Bogati"
     30msgstr ""
     31
     32#. Author URI of the plugin
     33#: site-security-auditor.php
     34msgid "https://profiles.wordpress.org/gopalbogati/"
     35msgstr ""
     36
     37#: includes/dashboard.php:8
     38msgid "SITE SECURITY AUDITOR"
     39msgstr ""
     40
     41#: includes/dashboard.php:9
     42msgid "Your security review"
     43msgstr ""
     44
     45#: includes/dashboard.php:10
     46msgid "Know what changed. Understand your next step."
     47msgstr ""
     48
     49#: includes/dashboard.php:13
     50msgid "Plugin updates available"
     51msgstr ""
     52
     53#: includes/dashboard.php:13
     54msgid "Review plugin information"
     55msgstr ""
     56
     57#: includes/dashboard.php:14
     58msgid "Settings to review"
     59msgstr ""
     60
     61#: includes/dashboard.php:14
     62msgid "Open your checklist"
     63msgstr ""
     64
     65#: includes/dashboard.php:15
     66msgid "File comparison"
     67msgstr ""
     68
     69#: includes/dashboard.php:15
     70msgid "Baseline ready"
     71msgstr ""
     72
     73#: includes/dashboard.php:15
     74msgid "Set up first"
     75msgstr ""
     76
     77#: includes/dashboard.php:15
     78msgid "Review files"
     79msgstr ""
     80
     81#: includes/dashboard.php:17
     82msgid "These checks reduce guesswork, not all security risks. This is not a malware scanner, firewall, or vulnerability database."
     83msgstr ""
     84
     85#: includes/dashboard.php:19
     86msgid "1. Check for file changes"
     87msgstr ""
     88
     89#: includes/dashboard.php:20
     90msgid "A baseline is a snapshot of files you trust. Compare against it after maintenance or whenever you want to investigate unexpected changes."
     91msgstr ""
     92
     93#: includes/dashboard.php:22
     94msgid "Back up your site and confirm its files are trustworthy."
     95msgstr ""
     96
     97#: includes/dashboard.php:23
     98msgid "Save a baseline, then run a comparison when you return."
     99msgstr ""
     100
     101#: includes/dashboard.php:24
     102msgid "Review changes with your update history or developer before replacing the baseline."
     103msgstr ""
     104
     105#: includes/dashboard.php:27
     106msgid "Baseline saved:"
     107msgstr ""
     108
     109#: includes/dashboard.php:29
     110msgid "Compare files now"
     111msgstr ""
     112
     113#: includes/dashboard.php:31
     114msgid "Replace the trusted baseline"
     115msgstr ""
     116
     117#: includes/dashboard.php:31
     118msgid "Create your first baseline"
     119msgstr ""
     120
     121#: includes/dashboard.php:32
     122msgid "Saving a snapshot does not prove that files are clean. Replacing it accepts all current changes as your new reference."
     123msgstr ""
     124
     125#: includes/dashboard.php:33
     126msgid "Save trusted baseline"
     127msgstr ""
     128
     129#: includes/dashboard.php:36
     130#: includes/dashboard.php:37
     131msgid "Latest file report"
     132msgstr ""
     133
     134#. translators: %d: number of files read.
     135#: includes/dashboard.php:39
     136#, php-format
     137msgid "%d files read"
     138msgstr ""
     139
     140#: includes/dashboard.php:41
     141msgid "Incomplete — no comparison or baseline replacement was made."
     142msgstr ""
     143
     144#: includes/dashboard.php:42
     145msgid "Finish any updates, check file permissions with your host, and retry. Large sites may need a host-level integrity tool."
     146msgstr ""
     147
     148#: includes/dashboard.php:45
     149msgid "Baseline saved. Your next comparison will use this snapshot."
     150msgstr ""
     151
     152#: includes/dashboard.php:47
     153msgid "Changes are not necessarily malicious: updates and intentional edits also change files. No changes does not guarantee a secure site."
     154msgstr ""
     155
     156#: includes/dashboard.php:48
     157msgid "Added"
     158msgstr ""
     159
     160#: includes/dashboard.php:48
     161msgid "Modified"
     162msgstr ""
     163
     164#: includes/dashboard.php:48
     165msgid "Removed"
     166msgstr ""
     167
     168#: includes/dashboard.php:53
     169msgid "Showing the first 100 paths. Download the full report below."
     170msgstr ""
     171
     172#: includes/dashboard.php:57
     173msgid "Download file report (JSON)"
     174msgstr ""
     175
     176#: includes/dashboard.php:58
     177msgid "The download includes relative file paths. Share only with people you trust."
     178msgstr ""
     179
     180#: includes/dashboard.php:61
     181msgid "What is included in a scan?"
     182msgstr ""
     183
     184#: includes/dashboard.php:62
     185msgid "Plugin and registered theme directories: PHP, PHP7, PHTML, INC, TWIG, JSON, JS and CSS files. WordPress core, uploads, must-use plugins, database content and other file types are excluded. Symbolic links are not followed and make a scan incomplete."
     186msgstr ""
     187
     188#: includes/dashboard.php:63
     189msgid "Limits: 20,000 directory entries, 100 MiB total, 8 MiB per file and an approximately 8-second processing budget. An incomplete scan never replaces your baseline. Files should not be edited while a scan runs."
     190msgstr ""
     191
     192#: includes/dashboard.php:64
     193msgid "Your older baseline excludes JS and CSS. Comparisons keep that scope until you save a new trusted baseline."
     194msgstr ""
     195
     196#: includes/dashboard.php:68
     197msgid "2. Review your plugins"
     198msgstr ""
     199
     200#: includes/dashboard.php:69
     201msgid "Uses the update information already stored by WordPress. Unknown means information is missing or more than two days old; premium plugins may use their own update systems."
     202msgstr ""
     203
     204#: includes/dashboard.php:70
     205msgid "Open WordPress Updates"
     206msgstr ""
     207
     208#: includes/dashboard.php:71
     209msgid "Manage plugins"
     210msgstr ""
     211
     212#: includes/dashboard.php:72
     213msgid "Plugin maintenance table"
     214msgstr ""
     215
     216#: includes/dashboard.php:74
     217msgid "Plugin"
     218msgstr ""
     219
     220#: includes/dashboard.php:74
     221msgid "Status"
     222msgstr ""
     223
     224#: includes/dashboard.php:74
     225msgid "Update information"
     226msgstr ""
     227
     228#: includes/dashboard.php:74
     229msgid "Next step"
     230msgstr ""
     231
     232#: includes/dashboard.php:78
     233msgid "Active"
     234msgstr ""
     235
     236#: includes/dashboard.php:78
     237msgid "Inactive"
     238msgstr ""
     239
     240#: includes/dashboard.php:79
     241msgid "Update available"
     242msgstr ""
     243
     244#: includes/dashboard.php:79
     245msgid "Unknown — refresh in Updates"
     246msgstr ""
     247
     248#: includes/dashboard.php:79
     249msgid "No update reported"
     250msgstr ""
     251
     252#: includes/dashboard.php:80
     253msgid "Back up, review the release notes, then update."
     254msgstr ""
     255
     256#: includes/dashboard.php:80
     257msgid "Remove it if no longer needed, after checking dependencies."
     258msgstr ""
     259
     260#: includes/dashboard.php:80
     261msgid "Keep maintained and review auto-update settings."
     262msgstr ""
     263
     264#: includes/dashboard.php:81
     265msgid "Auto-updates selected in WordPress; host or plugin policy may override this."
     266msgstr ""
     267
     268#: includes/dashboard.php:84
     269msgid "No update reported is not a security verdict. This tool does not check a vulnerability database or certify plugins as safe."
     270msgstr ""
     271
     272#: includes/dashboard.php:87
     273msgid "3. Follow your security checklist"
     274msgstr ""
     275
     276#: includes/dashboard.php:89
     277msgid "Configured"
     278msgstr ""
     279
     280#: includes/dashboard.php:89
     281msgid "Review"
     282msgstr ""
     283
     284#: includes/dashboard.php:92
     285msgid "Also check these with your host or administrator"
     286msgstr ""
     287
     288#: includes/dashboard.php:93
     289msgid "Test your backups, use unique passwords and two-factor authentication, remove unused accounts, and keep WordPress and themes updated. These are manual checks; this plugin does not verify them."
     290msgstr ""
     291
     292#: includes/dashboard.php:94
     293msgid "Open WordPress Site Health"
     294msgstr ""
     295
     296#: includes/dashboard.php:95
     297msgid "Read the official security guide"
     298msgstr ""
     299
     300#: includes/dashboard.php:97
     301msgid "Read-only checks. No automatic file edits, tracking, or additional remote metadata requests. Only the latest file report is stored."
     302msgstr ""
     303
     304#: site-security-auditor.php:36
     305msgid "Security Audit"
     306msgstr ""
     307
     308#: site-security-auditor.php:45
     309msgid "You are not allowed to perform this action."
     310msgstr ""
     311
     312#: site-security-auditor.php:66
     313msgid "Run a file check first."
     314msgstr ""
     315
     316#: site-security-auditor.php:95
     317msgid "Create a trusted baseline before comparing files."
     318msgstr ""
     319
     320#: site-security-auditor.php:139
     321msgid "Encrypted website address"
     322msgstr ""
     323
     324#: site-security-auditor.php:139
     325msgid "Use HTTPS for your public site. Ask your host to configure a certificate and redirects."
     326msgstr ""
     327
     328#: site-security-auditor.php:140
     329msgid "HTTPS required for administration"
     330msgstr ""
     331
     332#: site-security-auditor.php:140
     333msgid "Ask your host to check FORCE_SSL_ADMIN and your HTTPS proxy configuration."
     334msgstr ""
     335
     336#: site-security-auditor.php:141
     337msgid "Dashboard code editing disabled"
     338msgstr ""
     339
     340#: site-security-auditor.php:141
     341msgid "Consider DISALLOW_FILE_EDIT in wp-config.php. Back up the file and ask your developer before editing it."
     342msgstr ""
     343
     344#: site-security-auditor.php:142
     345msgid "WordPress debug display off"
     346msgstr ""
     347
     348#: site-security-auditor.php:142
     349msgid "On production sites, keep WordPress debug messages out of public pages. Server-level error display needs a separate host check."
     350msgstr ""
     351
     352#: site-security-auditor.php:160
     353msgid "I have reviewed this site and trust its current files. This replaces the previous baseline."
     354msgstr ""
  • site-security-auditor/trunk/readme.txt

    r3365450 r3682123  
    1 === Site Security Auditor ===
     1=== Site Security Auditor – Plugin & File Checks ===
    22Contributors: gopalbogati
     3Tags: security audit, file integrity, plugin updates, security checklist, site health
     4Requires at least: 5.8
     5Tested up to: 7.0
    36Requires PHP: 7.4
    4 Tested up to: 6.8
    5 Stable tag: 1.0
     7Stable tag: 1.4.0
    68License: GPLv2 or later
    79License URI: https://www.gnu.org/licenses/gpl-2.0.html
    810
    9 A simple admin tool to audit plugins, file integrity (baseline + changes), and hardening settings. No tracking or updater hooks.
     11Review plugin updates, detect plugin and theme file changes, and follow a clear WordPress security checklist. No account required.
    1012
    1113== Description ==
    12 Site Security Auditor helps admins spot risks from outdated/inactive plugins, monitor file changes in plugins/themes, and review hardening settings like DISALLOW_FILE_EDIT or FORCE_SSL_ADMIN. It uses only WordPress core APIs and stores data in options.
     14
     15Site Security Auditor gives WordPress administrators a practical security review dashboard. See plugin update information, compare plugin and theme files against a trusted snapshot, and understand which settings deserve attention.
     16
     17Open **Tools → Security Audit** to start. Each section explains what it checks, what the result means, and what to do next. It is designed for routine maintenance and investigating unexpected file changes, without automatically editing your site.
     18
     19= What you can do =
     20
     21* **Review plugin updates:** see available updates, inactive plugins and missing or stale update information. Open the familiar WordPress update screen to take action.
     22* **Compare files:** save a trusted SHA-256 baseline, then identify added, modified and removed code files in plugin and theme directories.
     23* **Return to your latest report:** the most recent file check stays visible with its timestamp and file count. Simply opening the dashboard does not start another scan.
     24* **Download a file report:** export the latest result as JSON to review with your developer or host.
     25* **Follow a plain-language checklist:** review HTTPS configuration, dashboard code editing and WordPress debug display, with links to Site Health and official guidance.
     26* **Use a responsive dashboard:** clear sections, keyboard-accessible controls and a scrollable plugin table on small screens.
     27
     28= A useful maintenance routine =
     29
     301. Review available updates and take a backup.
     312. Confirm your files are trustworthy before saving the first baseline.
     323. Run a comparison after maintenance or when investigating changes.
     334. Match changed files to intentional updates or edits.
     345. Replace the baseline only after reviewing and trusting the current files.
     35
     36A baseline records the current state; it does not prove that state is clean. Keep independent backups and ask a trusted professional about unexplained changes.
     37
     38= Scope and limitations =
     39
     40New baselines cover PHP, PHP7, PHTML, INC, TWIG, JSON, JS and CSS files inside the configured plugin directory and registered theme directories. WordPress core, uploads, must-use plugins, database content and other file types are excluded. Symbolic links are not followed and cause an incomplete result.
     41
     42Each run is bounded to 20,000 directory entries, 100 MiB of file data, 8 MiB per file and an approximately 8-second processing budget. Unreadable files or exceeded limits produce an incomplete report. Incomplete scans never replace a baseline or present a partial comparison as complete. Large sites may need a host-level integrity tool. Avoid editing files or running updates during a scan.
     43
     44This plugin is **not a malware scanner, firewall or vulnerability database**. It does not repair files, guarantee security, or certify a plugin as safe. An update or legitimate edit can change a file; unchanged files can still contain problems.
     45
     46= Privacy and performance =
     47
     48No account, API key, telemetry or additional remote metadata requests are required. The dashboard uses the update information already maintained by WordPress; WordPress itself may contact its normal update services.
     49
     50File hashes, relative paths, baseline time and the latest file report are stored in the site's options, without autoloading them. Reports are restricted to administrators; on multisite they require a network administrator. Downloaded reports contain relative file paths, so share them only with trusted people. Deactivation and deletion preserve these records to avoid silently discarding your trusted reference.
    1351
    1452== Installation ==
    15 1. Upload the ZIP via Plugins → Add New → Upload Plugin
    16 2. Activate
    17 3. Go to Tools → Security Audit
    18 4. Create/Update Baseline → Scan Now
    1953
    20 == Changeling ==
     541. Install Site Security Auditor from Plugins → Add New, or upload the plugin ZIP.
     552. Activate the plugin.
     563. Open Tools → Security Audit using an administrator account.
     574. Read the baseline warning, confirm that you trust the current files, and choose Save trusted baseline.
     585. Return and select Compare files now to review changes.
     59
     60On multisite, a network administrator can open the dashboard from a site's Tools menu. Baselines and reports are stored per site, while plugin and theme files may be shared across the network.
     61
     62== Frequently Asked Questions ==
     63
     64= Does it scan for malware or known vulnerabilities? =
     65
     66No. It compares code-file hashes with your own baseline and shows maintenance/configuration checks. Use other appropriate controls and professional advice alongside this tool.
     67
     68= When should I replace the baseline? =
     69
     70Only after verifying the current files, for example after reviewing an intentional plugin update. Replacing it accepts the current state as the reference and removes the previous comparison point.
     71
     72= Why does a plugin say Unknown? =
     73
     74WordPress has not supplied current update information for it, or that information is more than two days old. Open WordPress Updates to refresh it. Some premium or custom plugins use separate update systems. Unknown does not mean unsafe or safe.
     75
     76= What happens to my existing baseline when I update? =
     77
     78Your baseline is retained. Baselines made before 1.4.0 continue comparing their original file types, without JS and CSS. A newly saved trusted baseline includes JS and CSS. Review changes before replacing it.
     79
     80= Why is a scan incomplete? =
     81
     82The scan reached a resource limit, encountered a symbolic link, could not read a directory/file, or found a file changing during the run. The report explains the issue. Finish updates and ask your host to check permissions; large sites may need another integrity tool.
     83
     84= Does it change my security settings automatically? =
     85
     86No. It provides guidance and links. Configuration changes can affect hosting or integrations, so review them with your administrator.
     87
     88= Can I use this every day? =
     89
     90You can manually review the dashboard and compare files whenever useful. There are no scheduled scans or email alerts in this version. The dashboard stores only the latest file report; download reports you need to keep.
     91
     92== Screenshots ==
     93
     941. Security review dashboard with maintenance summaries and the trusted-baseline workflow.
     952. Saved file comparison with added, modified and removed file groups.
     963. Plugin maintenance information and a plain-language configuration checklist.
     97
     98== Changelog ==
     99
     100= 1.4.0 =
     101* New responsive dashboard, guided baseline workflow and actionable security checklist.
     102* Save the latest file report and download it as JSON.
     103* Run scans only through authenticated, nonce-protected POST actions.
     104* Preserve baselines when scans are incomplete; add resource limits and explicit coverage warnings.
     105* Include JS and CSS in new baselines while preserving legacy comparison scope.
     106* Use cached WordPress update data instead of per-plugin remote lookups on dashboard views.
     107* Recognize network-active plugins and restrict multisite access to network administrators.
     108* Refresh directory branding, documentation, screenshots and release metadata.
     109
    21110= 1.0 =
    22 * Add full dashboard sections and escaping
    23 * Fix text domain packaging
     111* Initial dashboard for plugin information, file baselines and hardening checks.
     112
     113== Upgrade Notice ==
     114
     115= 1.4.0 =
     116New dashboard and safer file checks. Existing baselines remain intact; newly saved baselines also include JS and CSS. Review changes before replacing a baseline.
     117
  • site-security-auditor/trunk/site-security-auditor.php

    r3365444 r3682123  
    11<?php
    22/**
    3  * Plugin Name:       Site Security Auditor
    4  * Description:       Audits plugins, file integrity, and hardening to improve site security.
    5  * Version:           1.3.3
    6  * Author:            Gopal Bogati
    7  * Author URI:        https://wordpress.org/
    8  * Text Domain:       site-security-auditor
    9  * Domain Path:       /languages
     3 * Plugin Name: Site Security Auditor
     4 * Description: Review plugin updates, compare plugin and theme files, and follow a clear WordPress security checklist.
     5 * Version: 1.4.0
     6 * Author: Gopal Bogati
     7 * Author URI: https://profiles.wordpress.org/gopalbogati/
     8 * Text Domain: site-security-auditor
     9 * Domain Path: /languages
    1010 * Requires at least: 5.8
    11  * Tested up to:      6.8
    12  * Requires PHP:      7.4
    13  * License:           GPL-2.0+
     11 * Requires PHP: 7.4
     12 * License: GPL-2.0-or-later
    1413 */
     14if ( ! defined( 'ABSPATH' ) ) { exit; }
    1515define( 'SITESEAU_PLUGIN_FILE', __FILE__ );
    1616define( 'SITESEAU_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
    1717define( 'SITESEAU_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
     18require_once SITESEAU_PLUGIN_DIR . 'includes/class-scanner.php';
    1819
     20class Siteseau_Site_Security_Auditor {
     21        const OPT_HASHES = 'siteseau_hashes';
     22        const OPT_HASHES_TIME = 'siteseau_hashes_time';
     23        const OPT_REPORT = 'siteseau_report';
    1924
    20 if ( ! defined( 'ABSPATH' ) ) {
    21         exit;
    22 }
    23 
    24 if ( ! class_exists( 'Siteseau_Site_Security_Auditor' ) ) :
    25 
    26 /**
    27  * Main plugin class.
    28  */
    29 class Siteseau_Site_Security_Auditor {
    30 
    31         const OPT_HASHES      = 'siteseau_hashes';
    32         const OPT_HASHES_TIME = 'siteseau_hashes_time';
    33 
    34         /**
    35          * Bootstrap.
    36          */
    3725        public static function init() {
    3826                add_action( 'admin_menu', array( __CLASS__, 'add_menu' ) );
     27                add_action( 'admin_enqueue_scripts', array( __CLASS__, 'assets' ) );
    3928                add_action( 'admin_post_ssa_make_baseline', array( __CLASS__, 'handle_baseline' ) );
    4029                add_action( 'admin_post_ssa_run_scan', array( __CLASS__, 'handle_scan' ) );
     30                add_action( 'admin_post_ssa_export', array( __CLASS__, 'handle_export' ) );
    4131        }
    42 
    43         /**
    44          * Add Tools -> Security Audit page.
    45          */
     32        public static function capability() {
     33                return is_multisite() ? 'manage_network_options' : 'manage_options';
     34        }
    4635        public static function add_menu() {
    47                 add_management_page(
    48                         __( 'Security Audit', 'site-security-auditor' ),
    49                         __( 'Security Audit', 'site-security-auditor' ),
    50                         'manage_options',
    51                         'site-security-auditor',
    52                         array( __CLASS__, 'render_page' )
    53                 );
     36                add_management_page( __( 'Security Audit', 'site-security-auditor' ), __( 'Security Audit', 'site-security-auditor' ), self::capability(), 'site-security-auditor', array( __CLASS__, 'render_page' ) );
    5437        }
    55 
    56         /**
    57          * Create/update baseline handler.
    58          */
     38        public static function assets( $hook ) {
     39                if ( 'tools_page_site-security-auditor' === $hook ) {
     40                        wp_enqueue_style( 'siteseau-admin', SITESEAU_PLUGIN_URL . 'assets/admin.css', array(), '1.4.0' );
     41                }
     42        }
     43        private static function authorize( $action ) {
     44                if ( ! current_user_can( self::capability() ) || 'POST' !== ( isset( $_SERVER['REQUEST_METHOD'] ) ? $_SERVER['REQUEST_METHOD'] : '' ) ) {
     45                        wp_die( esc_html__( 'You are not allowed to perform this action.', 'site-security-auditor' ), '', array( 'response' => 403 ) );
     46                }
     47                check_admin_referer( $action );
     48        }
    5949        public static function handle_baseline() {
    60                 if ( ! current_user_can( 'manage_options' ) ) {
    61                         wp_die( esc_html__( 'Sorry, you are not allowed to do that.', 'site-security-auditor' ) );
    62                 }
    63                 check_admin_referer( 'ssa_make_baseline' );
    64 
    65                 $paths  = self::get_scan_paths();
    66                 $hashes = self::hash_paths( $paths );
    67                 update_option( self::OPT_HASHES, $hashes, false );
    68                 update_option( self::OPT_HASHES_TIME, time(), false );
    69 
    70                 wp_safe_redirect( add_query_arg( array( 'page' => 'site-security-auditor', 'ssa_msg' => 'baseline_done' ), admin_url( 'tools.php' ) ) );
     50                self::authorize( 'ssa_make_baseline' );
     51                self::run_audit( true );
     52                self::redirect();
     53        }
     54        public static function handle_scan() {
     55                self::authorize( 'ssa_run_scan' );
     56                self::run_audit( false );
     57                self::redirect();
     58        }
     59        private static function redirect() {
     60                wp_safe_redirect( admin_url( 'tools.php?page=site-security-auditor' ) );
    7161                exit;
    7262        }
    73 
    74         /**
    75          * Run scan handler.
    76          */
    77         public static function handle_scan() {
    78                 if ( ! current_user_can( 'manage_options' ) ) {
    79                         wp_die( esc_html__( 'Sorry, you are not allowed to do that.', 'site-security-auditor' ) );
    80                 }
    81                 check_admin_referer( 'ssa_run_scan' );
    82 
    83                 wp_safe_redirect( add_query_arg( array( 'page' => 'site-security-auditor', 'ssa_scan' => '1' ), admin_url( 'tools.php' ) ) );
     63        public static function handle_export() {
     64                self::authorize( 'ssa_export' );
     65                $report = get_option( self::OPT_REPORT, array() );
     66                if ( ! $report ) { wp_die( esc_html__( 'Run a file check first.', 'site-security-auditor' ) ); }
     67                nocache_headers();
     68                header( 'Content-Type: application/json; charset=utf-8' );
     69                header( 'Content-Disposition: attachment; filename="site-security-auditor-report.json"' );
     70                echo wp_json_encode( $report, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES );
    8471                exit;
    8572        }
    86 
    87         /**
    88          * Render admin page.
    89          */
    90         public static function render_page() {
    91                 if ( ! current_user_can( 'manage_options' ) ) {
    92                         return;
     73        protected static function scan_roots() {
     74                global $wp_theme_directories;
     75                $roots = array( 'wp-content/plugins' => WP_PLUGIN_DIR );
     76                $themes = (array) $wp_theme_directories;
     77                if ( ! $themes ) { $themes = array( WP_CONTENT_DIR . '/themes' ); }
     78                foreach ( array_values( array_unique( $themes ) ) as $i => $path ) {
     79                        $label = wp_normalize_path( $path ) === wp_normalize_path( WP_CONTENT_DIR . '/themes' ) ? 'wp-content/themes' : 'theme-root-' . $i;
     80                        $roots[ $label ] = $path;
    9381                }
    94 
    95                 $plugins_info = self::collect_plugins_info();
    96                 $hardening    = self::hardening_checks();
    97                 $baseline_ts  = intval( get_option( self::OPT_HASHES_TIME, 0 ) );
    98                 $hashes       = get_option( self::OPT_HASHES, array() );
    99                 $scan_results = array();
    100 
    101                 if ( isset( $_GET['ssa_scan'] ) && '1' === $_GET['ssa_scan'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
    102                         $scan_results = self::compare_hashes( is_array( $hashes ) ? $hashes : array(), self::hash_paths( self::get_scan_paths() ) );
    103                 }
    104 
    105                 echo '<div class="wrap">';
    106                 echo '<h1>' . esc_html__( 'Security Audit Dashboard', 'site-security-auditor' ) . '</h1>';
    107 
    108                 if ( isset( $_GET['ssa_msg'] ) && 'baseline_done' === $_GET['ssa_msg'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
    109                         echo '<div class="notice notice-success"><p>' . esc_html__( 'File integrity baseline created/updated.', 'site-security-auditor' ) . '</p></div>';
    110                 }
    111 
    112                 // Plugins Risk Overview.
    113                 echo '<h2>' . esc_html__( 'Plugins Risk Overview', 'site-security-auditor' ) . '</h2>';
    114                 echo '<p>' . esc_html__( 'Flags plugins with updates available, last updated over a year ago, or inactive status.', 'site-security-auditor' ) . '</p>';
    115                 echo '<table class="widefat striped"><thead><tr>';
    116                 echo '<th>' . esc_html__( 'Plugin', 'site-security-auditor' ) . '</th>';
    117                 echo '<th>' . esc_html__( 'Version', 'site-security-auditor' ) . '</th>';
    118                 echo '<th>' . esc_html__( 'Status', 'site-security-auditor' ) . '</th>';
    119                 echo '<th>' . esc_html__( 'Update Available', 'site-security-auditor' ) . '</th>';
    120                 echo '<th>' . esc_html__( 'Last Updated', 'site-security-auditor' ) . '</th>';
    121                 echo '<th>' . esc_html__( 'Risk', 'site-security-auditor' ) . '</th>';
    122                 echo '</tr></thead><tbody>';
    123 
    124                 if ( empty( $plugins_info ) ) {
    125                         echo '<tr><td colspan="6">' . esc_html__( 'No plugins found.', 'site-security-auditor' ) . '</td></tr>';
    126                 } else {
    127                         foreach ( $plugins_info as $row ) {
    128                                 $risk_labels = array();
    129                                 if ( ! empty( $row['update_available'] ) ) {
    130                                         $risk_labels[] = __( 'Update available', 'site-security-auditor' );
    131                                 }
    132                                 if ( ! empty( $row['stale'] ) ) {
    133                                         $risk_labels[] = __( 'Stale (> 1 year)', 'site-security-auditor' );
    134                                 }
    135                                 if ( empty( $row['active'] ) ) {
    136                                         $risk_labels[] = __( 'Inactive', 'site-security-auditor' );
    137                                 }
    138                                 echo '<tr>';
    139                                 echo '<td>' . esc_html( $row['name'] ) . '</td>';
    140                                 echo '<td>' . esc_html( $row['version'] ) . '</td>';
    141                                 echo '<td>' . ( ! empty( $row['active'] ) ? esc_html__( 'Active', 'site-security-auditor' ) : esc_html__( 'Inactive', 'site-security-auditor' ) ) . '</td>';
    142                                 echo '<td>' . ( ! empty( $row['update_available'] ) ? esc_html__( 'Yes', 'site-security-auditor' ) : esc_html__( 'No', 'site-security-auditor' ) ) . '</td>';
    143                                 echo '<td>' . esc_html( $row['last_updated'] ) . '</td>';
    144                                 echo '<td>' . ( $risk_labels ? esc_html( implode( ', ', $risk_labels ) ) : esc_html__( 'OK', 'site-security-auditor' ) ) . '</td>';
    145                                 echo '</tr>';
    146                         }
    147                 }
    148                 echo '</tbody></table>';
    149 
    150                 // File Integrity Monitor.
    151                 echo '<hr /><h2>' . esc_html__( 'File Integrity Monitor', 'site-security-auditor' ) . '</h2>';
    152                 echo '<p>' . esc_html__( 'Create a hash baseline for plugins/themes and scan to detect added, modified or removed files.', 'site-security-auditor' ) . '</p>';
    153 
    154                 if ( $baseline_ts ) {
    155                         /* translators: %s: date/time. */
    156                         $baseline_text = sprintf( esc_html__( 'Baseline last updated: %s', 'site-security-auditor' ), wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $baseline_ts ) );
    157                         echo '<p>' . esc_html( $baseline_text ) . '</p>';
    158                 } else {
    159                         echo '<p>' . esc_html__( 'No baseline yet.', 'site-security-auditor' ) . '</p>';
    160                 }
    161 
    162                 echo '<form method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '" style="display:inline-block;margin-right:10px;">';
    163                 wp_nonce_field( 'ssa_make_baseline' );
    164                 echo '<input type="hidden" name="action" value="ssa_make_baseline" />';
    165                 echo '<button type="submit" class="button button-primary" onclick="return confirm(\'' . esc_js( __( 'Create/Update baseline now?', 'site-security-auditor' ) ) . '\');">' . esc_html__( 'Create/Update Baseline', 'site-security-auditor' ) . '</button>';
    166                 echo '</form>';
    167 
    168                 echo '<form method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '" style="display:inline-block;">';
    169                 wp_nonce_field( 'ssa_run_scan' );
    170                 echo '<input type="hidden" name="action" value="ssa_run_scan" />';
    171                 echo '<button type="submit" class="button">' . esc_html__( 'Scan Now', 'site-security-auditor' ) . '</button>';
    172                 echo '</form>';
    173 
    174                 if ( ! empty( $scan_results ) ) {
    175                         echo '<h3 style="margin-top:20px;">' . esc_html__( 'Scan Results', 'site-security-auditor' ) . '</h3>';
    176                         echo '<table class="widefat striped"><thead><tr><th>' . esc_html__( 'Change', 'site-security-auditor' ) . '</th><th>' . esc_html__( 'File', 'site-security-auditor' ) . '</th></tr></thead><tbody>';
    177                         $printed = false;
    178                         foreach ( array( 'added', 'modified', 'removed' ) as $key ) {
    179                                 if ( ! empty( $scan_results[ $key ] ) ) {
    180                                         foreach ( $scan_results[ $key ] as $file ) {
    181                                                 echo '<tr><td>' . esc_html( ucfirst( $key ) ) . '</td><td>' . esc_html( $file ) . '</td></tr>';
    182                                                 $printed = true;
     82                return $roots;
     83        }
     84        /** Protected operation, invoked only by the authorized POST handlers. */
     85        protected static function run_audit( $make_baseline ) {
     86                $lock = (int) get_option( 'siteseau_scan_lock', 0 );
     87                if ( $lock && $lock < time() - 300 ) { delete_option( 'siteseau_scan_lock' ); }
     88                if ( ! add_option( 'siteseau_scan_lock', time(), '', false ) ) { return; }
     89                try {
     90                        $baseline_time = (int) get_option( self::OPT_HASHES_TIME, 0 );
     91                        $old = get_option( self::OPT_HASHES, array() );
     92                        $scope = $make_baseline ? 2 : (int) get_option( 'siteseau_scope', 1 );
     93                        $report = array( 'time' => time(), 'baseline_time' => $baseline_time, 'scope' => $scope, 'kind' => $make_baseline ? 'baseline' : 'comparison', 'complete' => false, 'files' => 0, 'issues' => array(), 'changes' => array() );
     94                        if ( ! $make_baseline && ( ! $baseline_time || ! is_array( $old ) ) ) {
     95                                $report['issues'][] = __( 'Create a trusted baseline before comparing files.', 'site-security-auditor' );
     96                        } else {
     97                                $limits = 1 === $scope ? array( 'extensions' => array( 'php', 'php7', 'phtml', 'inc', 'twig', 'json' ) ) : array();
     98                                // Older releases scanned these fixed directories, even with custom plugin/theme roots.
     99                                $roots = 1 === $scope ? array( 'wp-content/plugins' => WP_CONTENT_DIR . '/plugins', 'wp-content/themes' => WP_CONTENT_DIR . '/themes' ) : self::scan_roots();
     100                                $scan = Siteseau_Scanner::scan( $roots, $limits );
     101                                $report['complete'] = $scan['complete'];
     102                                $report['files'] = count( $scan['hashes'] );
     103                                $report['issues'] = $scan['issues'];
     104                                if ( $scan['complete'] ) {
     105                                        if ( $make_baseline ) {
     106                                                update_option( self::OPT_HASHES, $scan['hashes'], false );
     107                                                update_option( self::OPT_HASHES_TIME, $report['time'], false );
     108                                                update_option( 'siteseau_scope', 2, false );
     109                                                $report['baseline_time'] = $report['time'];
     110                                        } else {
     111                                                $report['changes'] = Siteseau_Scanner::compare( $old, $scan['hashes'] );
    183112                                        }
    184113                                }
    185114                        }
    186                         if ( ! $printed ) {
    187                                 echo '<tr><td colspan="2">' . esc_html__( 'No changes detected.', 'site-security-auditor' ) . '</td></tr>';
    188                         }
    189                         echo '</tbody></table>';
     115                        update_option( self::OPT_REPORT, $report, false );
     116                } finally {
     117                        delete_option( 'siteseau_scan_lock' );
    190118                }
    191 
    192                 // Hardening checks.
    193                 echo '<hr /><h2>' . esc_html__( 'Hardening Checks', 'site-security-auditor' ) . '</h2>';
    194                 echo '<table class="widefat striped"><thead><tr><th>' . esc_html__( 'Check', 'site-security-auditor' ) . '</th><th>' . esc_html__( 'Status', 'site-security-auditor' ) . '</th></tr></thead><tbody>';
    195 
    196                 echo '<tr><td>' . esc_html__( 'DISALLOW_FILE_EDIT', 'site-security-auditor' ) . '</td><td>' . ( ! empty( $hardening['disallow_file_edit'] ) ? esc_html__( 'Enabled', 'site-security-auditor' ) : esc_html__( 'Disabled', 'site-security-auditor' ) ) . '</td></tr>';
    197                 echo '<tr><td>' . esc_html__( 'FORCE_SSL_ADMIN', 'site-security-auditor' ) . '</td><td>' . ( ! empty( $hardening['force_ssl_admin'] ) ? esc_html__( 'Enabled', 'site-security-auditor' ) : esc_html__( 'Disabled', 'site-security-auditor' ) ) . '</td></tr>';
    198                 echo '<tr><td>' . esc_html__( 'Plugin auto-updates (general)', 'site-security-auditor' ) . '</td><td>' . ( ! empty( $hardening['plugin_updates_enabled'] ) ? esc_html__( 'Enabled', 'site-security-auditor' ) : esc_html__( 'Disabled', 'site-security-auditor' ) ) . '</td></tr>';
    199                 echo '<tr><td>' . esc_html__( 'XML-RPC availability', 'site-security-auditor' ) . '</td><td>' . ( ! empty( $hardening['xmlrpc_enabled'] ) ? esc_html__( 'Enabled', 'site-security-auditor' ) : esc_html__( 'Disabled', 'site-security-auditor' ) ) . '</td></tr>';
    200 
    201                 echo '</tbody></table>';
    202                 echo '</div>';
    203119        }
    204 
    205         /**
    206          * Collect plugin info and risk signals.
    207          *
    208          * @return array
    209          */
    210120        protected static function collect_plugins_info() {
    211                 if ( ! function_exists( 'get_plugins' ) ) {
    212                         require_once ABSPATH . 'wp-admin/includes/plugin.php';
     121                if ( ! function_exists( 'get_plugins' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; }
     122                $updates = get_site_transient( 'update_plugins' );
     123                $recent = is_object( $updates ) && ! empty( $updates->last_checked ) && $updates->last_checked > time() - 2 * DAY_IN_SECONDS;
     124                $auto = (array) get_site_option( 'auto_update_plugins', array() );
     125                $results = array();
     126                foreach ( get_plugins() as $file => $data ) {
     127                        $known = $recent && isset( $updates->checked[ $file ] ) && $updates->checked[ $file ] === $data['Version'] && ( isset( $updates->response[ $file ] ) || isset( $updates->no_update[ $file ] ) );
     128                        $results[] = array(
     129                                'name' => $data['Name'], 'version' => $data['Version'],
     130                                'active' => is_plugin_active( $file ) || is_plugin_active_for_network( $file ),
     131                                'update' => $known ? ( isset( $updates->response[ $file ] ) ? 'available' : 'none' ) : 'unknown',
     132                                'auto' => in_array( $file, $auto, true ),
     133                        );
    213134                }
    214                 $all_plugins = get_plugins();
    215                 $active      = get_option( 'active_plugins', array() );
    216                 $active_map  = array_fill_keys( $active, true );
    217 
    218                 $updates     = get_site_transient( 'update_plugins' );
    219                 $updates_map = array();
    220                 if ( isset( $updates->response ) && is_array( $updates->response ) ) {
    221                         foreach ( $updates->response as $file => $obj ) {
    222                                 $updates_map[ $file ] = true;
    223                         }
    224                 }
    225 
    226                 $results = array();
    227                 foreach ( $all_plugins as $file => $data ) {
    228                         $slug = dirname( $file );
    229                         $info = array(
    230                                 'name'            => isset( $data['Name'] ) ? $data['Name'] : $file,
    231                                 'version'         => isset( $data['Version'] ) ? $data['Version'] : '',
    232                                 'active'          => isset( $active_map[ $file ] ),
    233                                 'update_available'=> isset( $updates_map[ $file ] ),
    234                                 'last_updated'    => __( 'Unknown', 'site-security-auditor' ),
    235                                 'stale'           => false,
    236                         );
    237 
    238                         // Try wp.org API for last_updated.
    239                         if ( ! function_exists( 'plugins_api' ) ) {
    240                                
    241                         }
    242                         $args = array(
    243                                 'slug'   => $slug,
    244                                 'fields' => array( 'last_updated' => true ),
    245                         );
    246 if ( is_admin() && ! function_exists( 'plugins_api' ) ) {
    247         require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
    248 }
    249                         $api = @plugins_api( 'plugin_information', $args ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
    250                         if ( ! is_wp_error( $api ) && ! empty( $api->last_updated ) ) {
    251                                 $info['last_updated'] = mysql2date( get_option( 'date_format' ), $api->last_updated );
    252                                 $ts = strtotime( $api->last_updated );
    253                                 if ( $ts && ( time() - $ts ) > YEAR_IN_SECONDS ) {
    254                                         $info['stale'] = true;
    255                                 }
    256                         }
    257 
    258                         $results[] = $info;
    259                 }
    260 
    261135                return $results;
    262136        }
    263 
    264         /**
    265          * Directories to scan.
    266          *
    267          * @return array
    268          */
    269         protected static function get_scan_paths() {
     137        protected static function hardening_checks() {
    270138                return array(
    271                         WP_CONTENT_DIR . '/plugins',
    272                         WP_CONTENT_DIR . '/themes',
     139                        array( __( 'Encrypted website address', 'site-security-auditor' ), 'https' === wp_parse_url( home_url(), PHP_URL_SCHEME ), __( 'Use HTTPS for your public site. Ask your host to configure a certificate and redirects.', 'site-security-auditor' ) ),
     140                        array( __( 'HTTPS required for administration', 'site-security-auditor' ), (bool) force_ssl_admin(), __( 'Ask your host to check FORCE_SSL_ADMIN and your HTTPS proxy configuration.', 'site-security-auditor' ) ),
     141                        array( __( 'Dashboard code editing disabled', 'site-security-auditor' ), ( defined( 'DISALLOW_FILE_EDIT' ) && DISALLOW_FILE_EDIT ) || ( defined( 'DISALLOW_FILE_MODS' ) && DISALLOW_FILE_MODS ), __( 'Consider DISALLOW_FILE_EDIT in wp-config.php. Back up the file and ask your developer before editing it.', 'site-security-auditor' ) ),
     142                        array( __( 'WordPress debug display off', 'site-security-auditor' ), ! ( defined( 'WP_DEBUG' ) && WP_DEBUG && ( ! defined( 'WP_DEBUG_DISPLAY' ) || WP_DEBUG_DISPLAY ) ), __( 'On production sites, keep WordPress debug messages out of public pages. Server-level error display needs a separate host check.', 'site-security-auditor' ) ),
    273143                );
    274144        }
    275 
    276         /**
    277          * Hash files in given paths.
    278          *
    279          * @param array $paths Paths.
    280          * @return array Map file => hash.
    281          */
    282         protected static function hash_paths( $paths ) {
    283                 $map = array();
    284 
    285                 foreach ( $paths as $path ) {
    286                         if ( ! is_dir( $path ) ) {
    287                                 continue;
    288                         }
    289                         $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $path, FilesystemIterator::SKIP_DOTS ) );
    290                         foreach ( $iterator as $file ) {
    291                                 /** @var SplFileInfo $file */
    292                                 $ext = strtolower( pathinfo( $file->getFilename(), PATHINFO_EXTENSION ) );
    293                                 if ( in_array( $ext, array( 'php', 'php7', 'phtml', 'inc', 'twig', 'json' ), true ) ) {
    294                                         $real = $file->getPathname();
    295                                         $rel  = ltrim( str_replace( WP_CONTENT_DIR, 'wp-content', $real ), '/' );
    296                                         $hash = @hash_file( 'sha256', $real ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
    297                                         if ( $hash ) {
    298                                                 $map[ $rel ] = $hash;
    299                                         }
    300                                 }
    301                         }
     145        public static function render_page() {
     146                if ( ! current_user_can( self::capability() ) ) { return; }
     147                $plugins = self::collect_plugins_info();
     148                $checks = self::hardening_checks();
     149                $report = get_option( self::OPT_REPORT, array() );
     150                $baseline_time = (int) get_option( self::OPT_HASHES_TIME, 0 );
     151                $updates = count( array_filter( $plugins, function( $row ) { return 'available' === $row['update']; } ) );
     152                $review = count( array_filter( $checks, function( $row ) { return ! $row[1]; } ) );
     153                require SITESEAU_PLUGIN_DIR . 'includes/dashboard.php';
     154        }
     155        public static function action_form( $action, $label, $disabled = false, $confirm = false ) {
     156                echo '<form method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '" class="ssa-action">';
     157                wp_nonce_field( $action );
     158                echo '<input type="hidden" name="action" value="' . esc_attr( $action ) . '">';
     159                if ( $confirm ) {
     160                        echo '<label class="ssa-confirm"><input type="checkbox" required> ' . esc_html__( 'I have reviewed this site and trust its current files. This replaces the previous baseline.', 'site-security-auditor' ) . '</label>';
    302161                }
    303                 ksort( $map );
    304                 return $map;
    305         }
    306 
    307         /**
    308          * Compare old and new hash maps.
    309          *
    310          * @param array $old Old.
    311          * @param array $new New.
    312          * @return array
    313          */
    314         protected static function compare_hashes( $old, $new ) {
    315                 $old = is_array( $old ) ? $old : array();
    316                 $new = is_array( $new ) ? $new : array();
    317 
    318                 $added    = array_diff_key( $new, $old );
    319                 $removed  = array_diff_key( $old, $new );
    320                 $modified = array();
    321 
    322                 $common = array_intersect_key( $new, $old );
    323                 foreach ( $common as $file => $hash ) {
    324                         if ( $old[ $file ] !== $hash ) {
    325                                 $modified[ $file ] = $hash;
    326                         }
    327                 }
    328 
    329                 return array(
    330                         'added'    => array_keys( $added ),
    331                         'modified' => array_keys( $modified ),
    332                         'removed'  => array_keys( $removed ),
    333                 );
    334         }
    335 
    336         /**
    337          * Hardening checks.
    338          *
    339          * @return array
    340          */
    341         protected static function hardening_checks() {
    342                 // Generic on/off for auto-updates by type (does not hook into filters).
    343                 $auto_plugins = function_exists( 'wp_is_auto_update_enabled_for_type' ) ? (bool) wp_is_auto_update_enabled_for_type( 'plugin' ) : false;
    344                 $xmlrpc_on    = apply_filters( 'xmlrpc_enabled', true );
    345 
    346                 return array(
    347                         'disallow_file_edit' => defined( 'DISALLOW_FILE_EDIT' ) && DISALLOW_FILE_EDIT,
    348                         'force_ssl_admin'    => defined( 'FORCE_SSL_ADMIN' ) && FORCE_SSL_ADMIN,
    349                         'plugin_updates_enabled'=> $auto_plugins,
    350                         'xmlrpc_enabled'     => (bool) $xmlrpc_on,
    351                 );
     162                echo '<button class="button ' . ( 'ssa_run_scan' === $action ? 'button-primary' : '' ) . '" type="submit"' . disabled( $disabled, true, false ) . '>' . esc_html( $label ) . '</button></form>';
    352163        }
    353164}
    354 
    355165Siteseau_Site_Security_Auditor::init();
    356 
    357 endif;
Note: See TracChangeset for help on using the changeset viewer.