PluginProbe
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… / 2.9.7
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… v2.9.7
2.11.12 2.11.11 2.11.10 2.11.9 2.11.7 2.11.8 2.11.6 2.11.5 2.11.4 2.11.3 2.11.1 2.11.2 2.11.0 2.10.5 2.10.4 2.10.3 2.10.2 2.10.1 2.10.0 2.9.9 2.9.8 2.9.6 2.9.7 2.9.5 2.9.4 All 87 releases
vigilante / includes / class-settings.php

class-settings.php in Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… 2.9.7, at includes/class-settings.php

951 lines 40.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Settings Class
4 *
5 * Centralized settings management with default values
6 *
7 * @package Vigilante
8 */
9
10 // Prevent direct access
11 if ( ! defined( 'ABSPATH' ) ) {
12 exit;
13 }
14
15 /**
16 * Class Vigilante_Settings
17 *
18 * Handles all plugin settings with defaults, getters and setters
19 */
20 class Vigilante_Settings {
21
22 /**
23 * Option name in database
24 */
25 const OPTION_NAME = 'vigilante_options';
26
27 /**
28 * Cached options
29 *
30 * @var array|null
31 */
32 private $options = null;
33
34 /**
35 * Default options structure
36 *
37 * @var array
38 */
39 private $defaults;
40
41 /**
42 * Constructor
43 */
44 public function __construct() {
45 $this->defaults = $this->get_default_options();
46 }
47
48 /**
49 * Get all default options
50 *
51 * @return array Complete default options array
52 */
53 public function get_default_options() {
54 return array(
55 // Module toggles - 8 modules that match tabs
56 'modules' => array(
57 'firewall' => true,
58 'security_headers' => true,
59 'login_security' => true,
60 'rest_api_security'=> true,
61 'user_security' => true,
62 'wp_hardening' => true,
63 'file_integrity' => true,
64 'activity_log' => true,
65 ),
66
67 // Firewall settings (includes htaccess, rate limiting, file protection)
68 'firewall' => array(
69 // Request filtering (PHP-based)
70 'block_bad_query_strings' => true,
71 'block_sql_injection' => true,
72 'block_xss_attacks' => true,
73 'block_file_inclusion' => true,
74 'block_directory_traversal' => true,
75
76 // Bot protection
77 'block_bad_bots' => true,
78 'block_empty_user_agent' => false,
79
80 // Rate limiting
81 'rate_limiting' => array(
82 'enabled' => true,
83 'requests_per_minute' => 120,
84 'block_duration' => 300,
85 'progressive' => false,
86 'max_block_duration' => 86400,
87 ),
88
89 // IP management
90 'ip_whitelist' => array(),
91 'ip_blacklist' => array(),
92
93 // Proxy / CDN: forwarded header to trust for the visitor IP.
94 // Empty = trust only REMOTE_ADDR (the real connection, unspoofable).
95 'trusted_proxy_header' => '',
96
97 // User-Agent management
98 'ua_whitelist' => array(),
99 'ua_blacklist' => array(),
100 'country_blocking' => array(
101 'enabled' => false,
102 'mode' => 'blacklist',
103 'countries' => array(),
104 ),
105
106 // File protection (htaccess-based)
107 'disable_directory_browsing' => true,
108 'protect_wp_config' => true,
109 'protect_wp_includes' => true,
110 'protect_uploads_php' => true,
111 'protect_sensitive_files' => true,
112 // Off by default — only safe when host has a real server-side cron job
113 // calling wp-cron.php; otherwise scheduled tasks stop running silently.
114 'protect_wp_cron' => false,
115 'block_php_in_plugins' => false,
116 'block_php_in_themes' => false,
117 'limit_http_methods' => true,
118 // All methods needed for WordPress core, Gutenberg, REST API, and page builders
119 'allowed_http_methods' => array( 'GET', 'POST', 'HEAD', 'OPTIONS', 'PUT', 'PATCH', 'DELETE' ),
120 'protected_file_extensions' => array(
121 'htaccess', 'htpasswd', 'ini', 'log', 'sql',
122 'bak', 'old', 'tmp', 'swp', 'save', 'backup'
123 ),
124 ),
125
126 // Security Headers settings (includes HTTPS enforcer)
127 'security_headers' => array(
128 'enabled' => true,
129
130 // Basic headers
131 'x_frame_options' => 'SAMEORIGIN',
132 'x_content_type_options' => true,
133 'referrer_policy' => 'strict-origin-when-cross-origin',
134
135 // HSTS
136 'hsts' => array(
137 'enabled' => false,
138 'max_age' => 31536000,
139 'include_subdomains' => false,
140 'preload' => false,
141 ),
142
143 // Permissions Policy
144 'permissions_policy' => array(
145 'enabled' => true,
146 'geolocation' => '()',
147 'microphone' => '()',
148 'camera' => '()',
149 'payment' => '(self)',
150 'usb' => '()',
151 ),
152
153 // CSP - WordPress/Gutenberg compatible defaults
154 // Note: blob: is required in frame-src and worker-src for the block editor,
155 // and in connect-src for the client-side media processing WordPress 7.1
156 // introduced: @wordpress/vips puts its WebAssembly binary in a blob: URL and
157 // fetches it, and fetch() is governed by connect-src, where 'self' does not
158 // cover blob:. Without it the editor cannot process images before upload.
159 'csp' => array(
160 'enabled' => true,
161 'report_only' => false,
162 'report_uri' => '',
163 'directives' => array(
164 'default-src' => "'self'",
165 'script-src' => "'self' 'unsafe-inline' 'unsafe-eval' https:",
166 'style-src' => "'self' 'unsafe-inline' https:",
167 'img-src' => "'self' data: https: blob:",
168 'font-src' => "'self' data: https:",
169 'connect-src' => "'self' https: wss: blob:",
170 'media-src' => "'self' https: blob:",
171 'frame-src' => "'self' https: blob:",
172 'frame-ancestors' => "'self'",
173 'base-uri' => "'self'",
174 'form-action' => "'self' https:",
175 'object-src' => "'none'",
176 'worker-src' => "'self' blob:",
177 'upgrade-insecure-requests'=> true,
178 ),
179 ),
180
181 // Cross-origin policies
182 'cross_origin_policies' => array(
183 'embedder_policy' => 'unsafe-none',
184 'opener_policy' => 'same-origin-allow-popups',
185 'resource_policy' => 'cross-origin',
186 ),
187
188 // HTTPS Enforcer (moved from separate module)
189 //
190 // force_https rewrites siteurl/home to https on activation, so it
191 // ships off: a site without working HTTPS would end up pointing at
192 // an address that does not answer. It is an opt-in decision per
193 // site, made from the Security Headers tab. Sites whose URLs a
194 // previous version already rewrote keep them; nothing reverts them.
195 'force_https' => false,
196 'redirect_http_to_https' => true,
197 'fix_mixed_content' => true,
198
199 // Server Protection (moved from firewall in v2.0.0)
200 'hide_server_signature' => true,
201 'remove_fingerprinting_headers' => true,
202 ),
203
204 // Login Security settings
205 'login_security' => array(
206 'enabled' => true,
207 'max_attempts' => 5,
208 'lockout_duration' => 1800,
209 'lockout_increment' => true,
210 'max_lockout_duration' => 86400,
211 'hide_login_errors' => true,
212 // XML-RPC se movio a wp_hardening en la 2.9.7; lo resuelve
213 // Vigilante_Comment_Security::resolve_xmlrpc_mode(), que
214 // sustituye a las dos casillas anteriores (disable_xmlrpc y
215 // disable_xmlrpc_pingback), que podian estar activas a la vez y
216 // contradecirse. A proposito NO se declara aqui ningun default: si se
217 // declarara, el merge con los defaults lo rellenaria siempre y taparia el
218 // respaldo que lee el ajuste antiguo de los sitios que aun no han vuelto a
219 // guardar la pestana. Sin nada guardado, el resolutor devuelve 'full', que
220 // es lo que hacia el default anterior.
221 'disable_application_passwords' => false,
222 'notify_on_lockout' => false,
223 'notify_on_admin_login' => false,
224 'ip_whitelist' => array(),
225 'custom_login_url' => '',
226 'notify_on_login_url_change' => true,
227 // Two-Factor Authentication
228 'two_factor' => array(
229 'enabled' => false,
230 'method' => 'email',
231 'enforced_roles' => array( 'administrator', 'editor' ),
232 'excluded_users' => array(),
233 'remember_device_days' => 30,
234 'allow_remember_device' => false,
235 'code_expiry_minutes' => 10,
236 'max_attempts' => 3,
237 'email_from_name' => '',
238 'notify_on_enable' => true,
239 'grace_period_days' => 3,
240 ),
241 ),
242
243 // REST API Security settings
244 'rest_api_security' => array(
245 'enabled' => true,
246 'mode' => 'selective',
247 'block_user_enumeration' => true,
248 'disable_jsonp' => true,
249 // Empty by default: /wp/v2/users used to live here, but that
250 // duplicated the dedicated "Block user enumeration" toggle.
251 // Now there is one knob = one behaviour. If you want to
252 // protect additional endpoints in selective mode, add them
253 // explicitly via this setting (or via a filter).
254 'protected_endpoints' => array(),
255 'allowed_public_endpoints' => array(
256 '/wp/v2/posts',
257 '/wp/v2/pages',
258 '/wp/v2/categories',
259 '/wp/v2/tags',
260 '/oembed/',
261 ),
262 'plugin_compatibility' => array(
263 'woocommerce' => true,
264 'contact_form_7' => true,
265 'elementor' => true,
266 ),
267 ),
268
269 // User Security settings
270 'user_security' => array(
271 'enabled' => true,
272 'block_insecure_usernames'=> true,
273 'insecure_usernames' => array(
274 'admin', 'administrator', 'user', 'test', 'guest',
275 'info', 'root', 'adm', 'sysadmin', 'support',
276 'webmaster', 'master', 'owner', 'manager', 'demo',
277 ),
278 'block_author_scanning' => true,
279 'force_strong_passwords' => true,
280 'min_password_length' => 12,
281
282 // Granular password policy. Applies only while
283 // force_strong_passwords is on. Defaults reproduce the previous
284 // all-requirements behaviour so existing sites keep the same
285 // rules until the admin relaxes them. block_username is the only
286 // new opt-in rule (off by default to avoid rejecting passwords
287 // that were valid before). affected_roles empty = all roles.
288 'password_policy' => array(
289 'require_uppercase' => true,
290 'require_lowercase' => true,
291 'require_number' => true,
292 'require_special' => true,
293 'block_common' => true,
294 'block_username' => false,
295 'affected_roles' => array(),
296 ),
297
298 'prevent_display_name_login_match' => true,
299
300 // Admin monitoring
301 'admin_monitoring' => array(
302 'alert_new_admin' => false,
303 'alert_admin_email_change' => false,
304 'alert_permission_elevation' => false,
305 'alert_admin_password_change' => false,
306 ),
307
308 // Force password reset (no options, uses native WordPress flow)
309
310 // Registration approval
311 'registration_approval' => array(
312 'enabled' => false,
313 'notify_admin' => false,
314 'auto_reject_days' => 0,
315 'affected_roles' => array( 'subscriber' ),
316 ),
317
318 // Session management
319 'session_management' => array(
320 'enabled' => true,
321 'show_in_profile' => true,
322 ),
323
324 // Session limits
325 'session_limits' => array(
326 'enabled' => false,
327 'max_sessions' => 3,
328 'behavior' => 'close_oldest',
329 'exclude_admins' => false,
330 ),
331
332 // Password expiration
333 'password_expiration' => array(
334 'enabled' => true,
335 'expire_days' => 90,
336 'warning_days' => 14,
337 'affected_roles' => array( 'administrator', 'editor' ),
338 'excluded_users' => array(),
339 'password_history' => 3,
340 'send_reminder' => false,
341 ),
342
343 // Email verification
344 'email_verification' => array(
345 'enabled' => false,
346 'token_expiry_hours' => 24,
347 'allow_resend' => true,
348 'auto_delete_days' => 7,
349 ),
350 ),
351
352 // WordPress Hardening (combines wp-config, comments, feeds, head cleaner)
353 'wp_hardening' => array(
354 'enabled' => true,
355
356 // wp-config security
357 'disallow_file_edit' => true,
358 'disallow_file_mods' => false,
359 'force_ssl_admin' => true,
360 'wp_debug' => true,
361 // Off by default — only safe when host has a real server-side cron job;
362 // pairs with firewall.protect_wp_cron to block both internal triggering
363 // (this constant) and external HTTP abuse (the .htaccess rule).
364 'disable_wp_cron' => false,
365
366 // Comment security
367 'disable_pingbacks' => true,
368 'disable_trackbacks' => true,
369 'require_comment_moderation' => true,
370 'close_old_comments' => false,
371 'close_comments_after_days' => 30,
372 'honeypot_comments' => true,
373
374 // Head cleaner
375 'remove_wp_generator' => true,
376 'remove_wp_version_assets' => false,
377 'remove_rsd_link' => true,
378 'remove_wlw_manifest' => true,
379 'remove_shortlink' => true,
380 'remove_rest_api_link' => false,
381
382 // Feed manager
383 'disable_feeds' => false,
384 'disable_if_no_content' => true,
385 'remove_feed_version' => true,
386 ),
387
388 // File Integrity settings
389 'file_integrity' => array(
390 'enabled' => true,
391 'scan_core' => true,
392 'scan_plugins' => true,
393 'scan_themes' => true,
394 'scan_uploads' => true,
395 'scan_critical_config' => true,
396 'check_closed_plugins' => true,
397 'auto_scan' => true,
398 'scan_frequency' => 'daily',
399 'notify_level' => 'suspicious_only',
400 'instant_alert' => false,
401 'excluded_paths' => array(
402 'wp-content/cache',
403 ),
404 'excluded_extensions' => array(
405 // Translations (regenerated per-locale, never in checksums).
406 '.po', '.mo', '.pot',
407 // Binary images (cosmetic, not executable; often rewritten by image-optimizer plugins).
408 '.jpg', '.jpeg', '.png', '.gif', '.ico', '.webp', '.avif',
409 // Stylesheets: frequently rewritten by themes and optimizer
410 // plugins, a common source of post-update false positives.
411 // Strict-mode users can remove it (CSS injection is still a
412 // vector, defended primarily by CSP in the headers module).
413 '.css',
414 ),
415 'suspicious_patterns' => array(
416 'eval(',
417 'base64_decode(',
418 'gzinflate(',
419 'str_rot13(',
420 'exec(',
421 'shell_exec(',
422 'system(',
423 'passthru(',
424 'assert(',
425 ),
426 ),
427
428 // Activity Log settings
429 'activity_log' => array(
430 'retention_days' => 30,
431 'max_entries' => 10000,
432 'log_logins' => true,
433 'log_failed_logins' => true,
434 'log_user_changes' => true,
435 'log_post_changes' => true,
436 'log_plugin_changes' => true,
437 'log_theme_changes' => true,
438 'log_option_changes' => false,
439 'log_file_changes' => true,
440 'log_comments' => true,
441 'log_media' => true,
442 'excluded_users' => array(),
443 'excluded_ips' => array(),
444 'tracked_options' => array(),
445 ),
446
447 // Backup settings
448 'backup' => array(
449 'auto_backup' => true,
450 'backup_before_update' => true,
451 'keep_backups' => 5,
452 ),
453
454 // Notification settings (centralized recipients for all admin emails)
455 'email' => array(
456 'send_to_admin_email' => true,
457 'additional_recipients' => array(),
458 'send_deactivation_email' => true,
459 ),
460
461 // Advanced settings
462 'advanced' => array(
463 'remove_readme' => true,
464 'remove_license' => true,
465 'block_author_archives' => false,
466 'disable_embeds' => false,
467 'uninstall_cleanup' => true,
468 'debug_mode' => false,
469 ),
470
471 // Security Analyzer (v2.1.0) — on-demand + weekly Security Check
472 'security_analyzer' => array(
473 'weekly_scan_enabled' => true,
474 'email_on_regression' => false,
475 ),
476
477 // Audit Alerts (v2.8.0) — alerting layer on top of Security Audit.
478 // The engine subscribes to logged events and only runs when the
479 // Security Audit (activity_log) module is enabled. Opt-in: both
480 // legs start OFF so it never duplicates the per-module emails that
481 // already exist (User Security admin monitoring, Plugin Status...).
482 'audit_alerts' => array(
483 // Shared anti-repeat cooldown (minutes). After an alert, do not
484 // send another about the same thing (same event type for
485 // immediate, same category for threshold) until this passes.
486 // Prevents a flood during a sustained attack.
487 'cooldown_minutes' => 60,
488 // #38 Immediate alerts: selected event types email right away.
489 'immediate' => array(
490 'enabled' => false,
491 // Alert on any logged event at or above this severity. A new
492 // admin, a closed plugin or a privilege escalation are all
493 // logged as "critical", so "critical" already covers them.
494 'min_severity' => 'critical', // 'critical' | 'warning'
495 ),
496 // #10 Threshold alerts: N events of a category within a window.
497 'threshold' => array(
498 'enabled' => false,
499 'window' => '1h', // 30m | 1h | 6h | 24h
500 // Per-category trigger counts (warning/critical events only);
501 // 0 disables that category. Covers every event type that can
502 // log a warning or critical. Keep in sync with
503 // Vigilante_Audit_Alerts::category_labels().
504 'categories' => array(
505 'firewall' => 50,
506 'login' => 20,
507 'user' => 5,
508 'plugin' => 0,
509 'file' => 0,
510 'security' => 0,
511 'system' => 0,
512 'settings' => 0,
513 'theme' => 0,
514 'content' => 0,
515 'comment' => 0,
516 'media' => 0,
517 ),
518 ),
519 ),
520 );
521 }
522
523 /**
524 * Get all options (merged with defaults)
525 *
526 * @return array All options
527 */
528 public function get_all_options() {
529 if ( null === $this->options ) {
530 $saved = get_option( self::OPTION_NAME, array() );
531 $this->options = $this->array_merge_deep( $this->get_default_options(), $saved );
532 }
533 return $this->options;
534 }
535
536 /**
537 * Deep merge arrays
538 *
539 * @param array $defaults Default values.
540 * @param array $saved Saved values.
541 * @return array Merged array.
542 */
543 private function array_merge_deep( $defaults, $saved ) {
544 $result = $defaults;
545
546 foreach ( $saved as $key => $value ) {
547 if ( is_array( $value ) && isset( $result[ $key ] ) && is_array( $result[ $key ] ) ) {
548 $result[ $key ] = $this->array_merge_deep( $result[ $key ], $value );
549 } else {
550 $result[ $key ] = $value;
551 }
552 }
553
554 return $result;
555 }
556
557 /**
558 * Get a specific section
559 *
560 * @param string $section Section name.
561 * @return array Section options.
562 */
563 public function get_section( $section ) {
564 $options = $this->get_all_options();
565 return isset( $options[ $section ] ) ? $options[ $section ] : array();
566 }
567
568 /**
569 * Get a specific option
570 *
571 * @param string $section Section name.
572 * @param string $key Option key.
573 * @param mixed $default Default value.
574 * @return mixed Option value.
575 */
576 public function get_option( $section, $key, $default = null ) {
577 $options = $this->get_all_options();
578
579 if ( isset( $options[ $section ][ $key ] ) ) {
580 return $options[ $section ][ $key ];
581 }
582
583 return $default;
584 }
585
586 /**
587 * Check if a module is enabled
588 *
589 * @param string $module Module name.
590 * @return bool Whether module is enabled.
591 */
592 public function is_module_enabled( $module ) {
593 $options = $this->get_all_options();
594 return ! empty( $options['modules'][ $module ] );
595 }
596
597 /**
598 * Save options
599 *
600 * @param array $options Options to save.
601 * @return bool Success status.
602 */
603 public function save_options( $options ) {
604 $this->options = null;
605 return update_option( self::OPTION_NAME, $options );
606 }
607
608 /**
609 * Update a section
610 *
611 * @param string $section Section name.
612 * @param array $data Section data.
613 * @return bool Success status.
614 */
615 public function update_section( $section, $data ) {
616 $options = get_option( self::OPTION_NAME, array() );
617 $options[ $section ] = $data;
618 $this->options = null;
619 return update_option( self::OPTION_NAME, $options );
620 }
621
622 /**
623 * Update multiple sections at once
624 *
625 * @param array $sections Associative array of section => data.
626 * @return bool Success status.
627 */
628 public function update_options( $sections ) {
629 $options = get_option( self::OPTION_NAME, array() );
630
631 foreach ( $sections as $section => $data ) {
632 $options[ $section ] = $data;
633 }
634
635 $this->options = null;
636 return update_option( self::OPTION_NAME, $options );
637 }
638
639 /**
640 * Clear the options cache
641 */
642 public function clear_cache() {
643 $this->options = null;
644 wp_cache_delete( self::OPTION_NAME, 'options' );
645 }
646
647 /**
648 * Get presets with descriptions
649 *
650 * @return array Presets configuration.
651 */
652 public function get_presets() {
653 return array(
654 'standard' => array(
655 'name' => __( 'Standard', 'vigilante' ),
656 'description' => __( 'Balanced security suitable for most websites. Enables all modules with sensible defaults.', 'vigilante' ),
657 'modules' => array(
658 'firewall' => true,
659 'security_headers' => true,
660 'login_security' => true,
661 'rest_api_security'=> true,
662 'user_security' => true,
663 'wp_hardening' => true,
664 'file_integrity' => true,
665 'activity_log' => true,
666 ),
667 'firewall' => array(
668 'block_bad_query_strings' => true,
669 'block_sql_injection' => true,
670 'block_xss_attacks' => true,
671 'rate_limiting' => array(
672 'enabled' => true,
673 'requests_per_minute' => 120,
674 ),
675 ),
676 'login_security' => array(
677 'max_attempts' => 5,
678 'lockout_duration' => 1800,
679 ),
680 'rest_api_security' => array(
681 'mode' => 'selective',
682 ),
683 'user_security' => array(
684 'prevent_display_name_login_match' => true,
685 ),
686 'file_integrity' => array(
687 'notify_level' => 'suspicious_only',
688 ),
689 'wp_hardening' => array(
690 'xmlrpc_mode' => 'full',
691 ),
692 ),
693
694 'maximum' => array(
695 'name' => __( 'Maximum Security', 'vigilante' ),
696 'description' => __( 'Strictest settings for high-security sites. CSP is set to report-only mode to prevent breaking the admin interface.', 'vigilante' ),
697 'modules' => array(
698 'firewall' => true,
699 'security_headers' => true,
700 'login_security' => true,
701 'rest_api_security'=> true,
702 'user_security' => true,
703 'wp_hardening' => true,
704 'file_integrity' => true,
705 'activity_log' => true,
706 ),
707 'firewall' => array(
708 'block_bad_query_strings' => true,
709 'block_sql_injection' => true,
710 'block_xss_attacks' => true,
711 'block_file_inclusion' => true,
712 'block_directory_traversal' => true,
713 'block_bad_bots' => true,
714 'block_empty_user_agent' => true,
715 'rate_limiting' => array(
716 'enabled' => true,
717 'requests_per_minute' => 60,
718 'block_duration' => 600,
719 'progressive' => true,
720 'max_block_duration' => 86400,
721 ),
722 ),
723 'security_headers' => array(
724 'x_frame_options' => 'DENY',
725 // HSTS is intentionally NOT enabled by Maximum: forcing HSTS on a site
726 // that doesn't have a healthy HTTPS setup (or temporarily falls back to
727 // HTTP) locks visitors out for the full max_age. Leaving HSTS off keeps
728 // it as an explicit opt-in decision per site.
729 'csp' => array(
730 'enabled' => true,
731 'report_only' => false,
732 'directives' => array(
733 'default-src' => "'self'",
734 'script-src' => "'self' 'unsafe-inline' 'unsafe-eval'",
735 'style-src' => "'self' 'unsafe-inline'",
736 'img-src' => "'self' data: https: blob:",
737 'font-src' => "'self' data:",
738 'connect-src' => "'self' https: blob:",
739 'frame-src' => "'self' blob:",
740 'frame-ancestors' => "'none'",
741 'worker-src' => "'self' blob:",
742 'object-src' => "'none'",
743 'base-uri' => "'self'",
744 ),
745 ),
746 ),
747 'rest_api_security' => array(
748 'mode' => 'authenticated_only',
749 ),
750 'login_security' => array(
751 'max_attempts' => 3,
752 'lockout_duration' => 3600,
753 'lockout_increment' => true,
754 'notify_on_lockout' => true,
755 'notify_on_admin_login' => true,
756 ),
757 'wp_hardening' => array(
758 'xmlrpc_mode' => 'full',
759 'disallow_file_edit' => true,
760 'disallow_file_mods' => true,
761 // close_old_comments is intentionally NOT touched by Maximum:
762 // it would unilaterally close discussion on every old post,
763 // which is a content decision, not a security one.
764 ),
765 'user_security' => array(
766 'prevent_display_name_login_match' => true,
767 'min_password_length' => 16,
768 'password_policy' => array(
769 'require_uppercase' => true,
770 'require_lowercase' => true,
771 'require_number' => true,
772 'require_special' => true,
773 'block_common' => true,
774 'block_username' => true,
775 'affected_roles' => array(),
776 ),
777 'admin_monitoring' => array(
778 'alert_new_admin' => true,
779 'alert_admin_email_change' => true,
780 'alert_permission_elevation' => true,
781 'alert_admin_password_change' => true,
782 ),
783 'registration_approval' => array(
784 'enabled' => true,
785 'notify_admin' => true,
786 'auto_reject_days' => 7,
787 'affected_roles' => array( 'subscriber', 'contributor', 'author', 'editor' ),
788 ),
789 'session_limits' => array(
790 'enabled' => true,
791 'max_sessions' => 1,
792 'behavior' => 'close_oldest',
793 'exclude_admins' => false,
794 ),
795 'password_expiration' => array(
796 'enabled' => true,
797 'expire_days' => 30,
798 'warning_days' => 7,
799 'affected_roles' => array( 'administrator', 'editor', 'author', 'contributor', 'subscriber' ),
800 'password_history' => 5,
801 'send_reminder' => true,
802 ),
803 'email_verification' => array(
804 'enabled' => true,
805 'token_expiry_hours' => 24,
806 'allow_resend' => true,
807 'auto_delete_days' => 3,
808 ),
809 ),
810 'file_integrity' => array(
811 'scan_core' => true,
812 'scan_plugins' => true,
813 'scan_themes' => true,
814 'scan_uploads' => true,
815 'scan_critical_config' => true,
816 'auto_scan' => true,
817 'scan_frequency' => 'daily',
818 'notify_level' => 'all',
819 'instant_alert' => true,
820 ),
821 'activity_log' => array(
822 'log_logins' => true,
823 'log_failed_logins' => true,
824 'log_user_changes' => true,
825 'log_post_changes' => true,
826 'log_plugin_changes' => true,
827 'log_theme_changes' => true,
828 'log_option_changes' => true,
829 'log_file_changes' => true,
830 'log_comments' => true,
831 'log_media' => true,
832 ),
833 ),
834 );
835 }
836
837 /**
838 * Get module labels for display
839 *
840 * @return array Module labels.
841 */
842 public function get_module_labels() {
843 return array(
844 'firewall' => __( 'Firewall', 'vigilante' ),
845 'security_headers' => __( 'Security Headers', 'vigilante' ),
846 'login_security' => __( 'Login Security', 'vigilante' ),
847 'rest_api_security'=> __( 'REST API Security', 'vigilante' ),
848 'user_security' => __( 'User Security', 'vigilante' ),
849 'wp_hardening' => __( 'WordPress Hardening', 'vigilante' ),
850 'file_integrity' => __( 'File Integrity', 'vigilante' ),
851 'activity_log' => __( 'Security Audit', 'vigilante' ),
852 );
853 }
854
855 /**
856 * Get module descriptions for display
857 *
858 * @return array Module descriptions.
859 */
860 public function get_module_descriptions() {
861 return array(
862 'firewall' => __( 'Blocks malicious requests, SQL injection, XSS attacks, and bad bots. Includes rate limiting and file protection.', 'vigilante' ),
863 'security_headers' => __( 'Adds HTTP security headers like CSP, HSTS, X-Frame-Options. Forces HTTPS and fixes mixed content.', 'vigilante' ),
864 'login_security' => __( 'Brute force protection, 2FA, login attempt limits, XML-RPC control, and notifications.', 'vigilante' ),
865 'rest_api_security'=> __( 'Controls REST API access, blocks user enumeration, and protects sensitive endpoints.', 'vigilante' ),
866 'user_security' => __( 'Blocks insecure usernames, enforces strong passwords, and prevents author scanning.', 'vigilante' ),
867 'wp_hardening' => __( 'Hardens wp-config.php, manages comments, cleans header output, and controls feeds.', 'vigilante' ),
868 'file_integrity' => __( 'Scans WordPress core, plugins, and themes for unauthorized changes and suspicious code.', 'vigilante' ),
869 'activity_log' => __( 'Records user actions, logins, content changes, and security events for security auditing.', 'vigilante' ),
870 );
871 }
872
873 /**
874 * Validate options before saving
875 *
876 * @param array $input Raw input to validate.
877 * @return array Validated options.
878 */
879 public function validate_options( $input ) {
880 $validated = array();
881 $defaults = $this->get_default_options();
882
883 // Validate each section that exists in input
884 foreach ( $input as $section => $data ) {
885 if ( ! is_array( $data ) ) {
886 continue;
887 }
888
889 if ( 'modules' === $section ) {
890 // Validate modules (booleans)
891 foreach ( $defaults['modules'] as $module => $default_value ) {
892 $validated['modules'][ $module ] = isset( $data[ $module ] )
893 ? (bool) $data[ $module ]
894 : false;
895 }
896 } elseif ( isset( $defaults[ $section ] ) ) {
897 // Validate other sections using generic validator
898 $validated[ $section ] = $this->validate_section( $data, $defaults[ $section ] );
899 }
900 }
901
902 return apply_filters( 'vigilante_validate_options', $validated, $input );
903 }
904
905 /**
906 * Validate a section based on defaults
907 *
908 * @param array $input Input values.
909 * @param array $defaults Default values.
910 * @return array Validated values.
911 */
912 private function validate_section( $input, $defaults ) {
913 $validated = array();
914
915 foreach ( $defaults as $key => $default_value ) {
916 if ( ! isset( $input[ $key ] ) ) {
917 $validated[ $key ] = $default_value;
918 continue;
919 }
920
921 $value = $input[ $key ];
922
923 if ( is_bool( $default_value ) ) {
924 $validated[ $key ] = (bool) $value;
925 } elseif ( is_int( $default_value ) ) {
926 $validated[ $key ] = intval( $value );
927 } elseif ( is_array( $default_value ) ) {
928 if ( is_array( $value ) ) {
929 $validated[ $key ] = $this->validate_section( $value, $default_value );
930 } else {
931 $validated[ $key ] = $default_value;
932 }
933 } else {
934 $validated[ $key ] = sanitize_text_field( $value );
935 }
936 }
937
938 // Include any extra keys from input
939 foreach ( $input as $key => $value ) {
940 if ( ! isset( $validated[ $key ] ) ) {
941 if ( is_array( $value ) ) {
942 $validated[ $key ] = array_map( 'sanitize_text_field', $value );
943 } else {
944 $validated[ $key ] = sanitize_text_field( $value );
945 }
946 }
947 }
948
949 return $validated;
950 }
951 }