PluginProbe
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… / 2.9.6
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… v2.9.6
3.0.0 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 All 88 releases
vigilante / includes / class-settings.php

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

942 lines 39.3 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 'disable_xmlrpc' => true,
213 'disable_xmlrpc_pingback' => true,
214 'disable_application_passwords' => false,
215 'notify_on_lockout' => false,
216 'notify_on_admin_login' => false,
217 'ip_whitelist' => array(),
218 'custom_login_url' => '',
219 'notify_on_login_url_change' => true,
220 // Two-Factor Authentication
221 'two_factor' => array(
222 'enabled' => false,
223 'method' => 'email',
224 'enforced_roles' => array( 'administrator', 'editor' ),
225 'excluded_users' => array(),
226 'remember_device_days' => 30,
227 'allow_remember_device' => false,
228 'code_expiry_minutes' => 10,
229 'max_attempts' => 3,
230 'email_from_name' => '',
231 'notify_on_enable' => true,
232 'grace_period_days' => 3,
233 ),
234 ),
235
236 // REST API Security settings
237 'rest_api_security' => array(
238 'enabled' => true,
239 'mode' => 'selective',
240 'block_user_enumeration' => true,
241 'disable_jsonp' => true,
242 // Empty by default: /wp/v2/users used to live here, but that
243 // duplicated the dedicated "Block user enumeration" toggle.
244 // Now there is one knob = one behaviour. If you want to
245 // protect additional endpoints in selective mode, add them
246 // explicitly via this setting (or via a filter).
247 'protected_endpoints' => array(),
248 'allowed_public_endpoints' => array(
249 '/wp/v2/posts',
250 '/wp/v2/pages',
251 '/wp/v2/categories',
252 '/wp/v2/tags',
253 '/oembed/',
254 ),
255 'plugin_compatibility' => array(
256 'woocommerce' => true,
257 'contact_form_7' => true,
258 'elementor' => true,
259 ),
260 ),
261
262 // User Security settings
263 'user_security' => array(
264 'enabled' => true,
265 'block_insecure_usernames'=> true,
266 'insecure_usernames' => array(
267 'admin', 'administrator', 'user', 'test', 'guest',
268 'info', 'root', 'adm', 'sysadmin', 'support',
269 'webmaster', 'master', 'owner', 'manager', 'demo',
270 ),
271 'block_author_scanning' => true,
272 'force_strong_passwords' => true,
273 'min_password_length' => 12,
274
275 // Granular password policy. Applies only while
276 // force_strong_passwords is on. Defaults reproduce the previous
277 // all-requirements behaviour so existing sites keep the same
278 // rules until the admin relaxes them. block_username is the only
279 // new opt-in rule (off by default to avoid rejecting passwords
280 // that were valid before). affected_roles empty = all roles.
281 'password_policy' => array(
282 'require_uppercase' => true,
283 'require_lowercase' => true,
284 'require_number' => true,
285 'require_special' => true,
286 'block_common' => true,
287 'block_username' => false,
288 'affected_roles' => array(),
289 ),
290
291 'prevent_display_name_login_match' => true,
292
293 // Admin monitoring
294 'admin_monitoring' => array(
295 'alert_new_admin' => false,
296 'alert_admin_email_change' => false,
297 'alert_permission_elevation' => false,
298 'alert_admin_password_change' => false,
299 ),
300
301 // Force password reset (no options, uses native WordPress flow)
302
303 // Registration approval
304 'registration_approval' => array(
305 'enabled' => false,
306 'notify_admin' => false,
307 'auto_reject_days' => 0,
308 'affected_roles' => array( 'subscriber' ),
309 ),
310
311 // Session management
312 'session_management' => array(
313 'enabled' => true,
314 'show_in_profile' => true,
315 ),
316
317 // Session limits
318 'session_limits' => array(
319 'enabled' => false,
320 'max_sessions' => 3,
321 'behavior' => 'close_oldest',
322 'exclude_admins' => false,
323 ),
324
325 // Password expiration
326 'password_expiration' => array(
327 'enabled' => true,
328 'expire_days' => 90,
329 'warning_days' => 14,
330 'affected_roles' => array( 'administrator', 'editor' ),
331 'excluded_users' => array(),
332 'password_history' => 3,
333 'send_reminder' => false,
334 ),
335
336 // Email verification
337 'email_verification' => array(
338 'enabled' => false,
339 'token_expiry_hours' => 24,
340 'allow_resend' => true,
341 'auto_delete_days' => 7,
342 ),
343 ),
344
345 // WordPress Hardening (combines wp-config, comments, feeds, head cleaner)
346 'wp_hardening' => array(
347 'enabled' => true,
348
349 // wp-config security
350 'disallow_file_edit' => true,
351 'disallow_file_mods' => false,
352 'force_ssl_admin' => true,
353 'wp_debug' => true,
354 // Off by default — only safe when host has a real server-side cron job;
355 // pairs with firewall.protect_wp_cron to block both internal triggering
356 // (this constant) and external HTTP abuse (the .htaccess rule).
357 'disable_wp_cron' => false,
358
359 // Comment security
360 'disable_pingbacks' => true,
361 'disable_trackbacks' => true,
362 'require_comment_moderation' => true,
363 'close_old_comments' => false,
364 'close_comments_after_days' => 30,
365 'honeypot_comments' => true,
366
367 // Head cleaner
368 'remove_wp_generator' => true,
369 'remove_wp_version_assets' => false,
370 'remove_rsd_link' => true,
371 'remove_wlw_manifest' => true,
372 'remove_shortlink' => true,
373 'remove_rest_api_link' => false,
374
375 // Feed manager
376 'disable_feeds' => false,
377 'disable_if_no_content' => true,
378 'remove_feed_version' => true,
379 ),
380
381 // File Integrity settings
382 'file_integrity' => array(
383 'enabled' => true,
384 'scan_core' => true,
385 'scan_plugins' => true,
386 'scan_themes' => true,
387 'scan_uploads' => true,
388 'scan_critical_config' => true,
389 'check_closed_plugins' => true,
390 'auto_scan' => true,
391 'scan_frequency' => 'daily',
392 'notify_level' => 'suspicious_only',
393 'instant_alert' => false,
394 'excluded_paths' => array(
395 'wp-content/cache',
396 ),
397 'excluded_extensions' => array(
398 // Translations (regenerated per-locale, never in checksums).
399 '.po', '.mo', '.pot',
400 // Binary images (cosmetic, not executable; often rewritten by image-optimizer plugins).
401 '.jpg', '.jpeg', '.png', '.gif', '.ico', '.webp', '.avif',
402 // Stylesheets: frequently rewritten by themes and optimizer
403 // plugins, a common source of post-update false positives.
404 // Strict-mode users can remove it (CSS injection is still a
405 // vector, defended primarily by CSP in the headers module).
406 '.css',
407 ),
408 'suspicious_patterns' => array(
409 'eval(',
410 'base64_decode(',
411 'gzinflate(',
412 'str_rot13(',
413 'exec(',
414 'shell_exec(',
415 'system(',
416 'passthru(',
417 'assert(',
418 ),
419 ),
420
421 // Activity Log settings
422 'activity_log' => array(
423 'retention_days' => 30,
424 'max_entries' => 10000,
425 'log_logins' => true,
426 'log_failed_logins' => true,
427 'log_user_changes' => true,
428 'log_post_changes' => true,
429 'log_plugin_changes' => true,
430 'log_theme_changes' => true,
431 'log_option_changes' => false,
432 'log_file_changes' => true,
433 'log_comments' => true,
434 'log_media' => true,
435 'excluded_users' => array(),
436 'excluded_ips' => array(),
437 'tracked_options' => array(),
438 ),
439
440 // Backup settings
441 'backup' => array(
442 'auto_backup' => true,
443 'backup_before_update' => true,
444 'keep_backups' => 5,
445 ),
446
447 // Notification settings (centralized recipients for all admin emails)
448 'email' => array(
449 'send_to_admin_email' => true,
450 'additional_recipients' => array(),
451 'send_deactivation_email' => true,
452 ),
453
454 // Advanced settings
455 'advanced' => array(
456 'remove_readme' => true,
457 'remove_license' => true,
458 'block_author_archives' => false,
459 'disable_embeds' => false,
460 'uninstall_cleanup' => true,
461 'debug_mode' => false,
462 ),
463
464 // Security Analyzer (v2.1.0) — on-demand + weekly Security Check
465 'security_analyzer' => array(
466 'weekly_scan_enabled' => true,
467 'email_on_regression' => false,
468 ),
469
470 // Audit Alerts (v2.8.0) — alerting layer on top of Security Audit.
471 // The engine subscribes to logged events and only runs when the
472 // Security Audit (activity_log) module is enabled. Opt-in: both
473 // legs start OFF so it never duplicates the per-module emails that
474 // already exist (User Security admin monitoring, Plugin Status...).
475 'audit_alerts' => array(
476 // Shared anti-repeat cooldown (minutes). After an alert, do not
477 // send another about the same thing (same event type for
478 // immediate, same category for threshold) until this passes.
479 // Prevents a flood during a sustained attack.
480 'cooldown_minutes' => 60,
481 // #38 Immediate alerts: selected event types email right away.
482 'immediate' => array(
483 'enabled' => false,
484 // Alert on any logged event at or above this severity. A new
485 // admin, a closed plugin or a privilege escalation are all
486 // logged as "critical", so "critical" already covers them.
487 'min_severity' => 'critical', // 'critical' | 'warning'
488 ),
489 // #10 Threshold alerts: N events of a category within a window.
490 'threshold' => array(
491 'enabled' => false,
492 'window' => '1h', // 30m | 1h | 6h | 24h
493 // Per-category trigger counts (warning/critical events only);
494 // 0 disables that category. Covers every event type that can
495 // log a warning or critical. Keep in sync with
496 // Vigilante_Audit_Alerts::category_labels().
497 'categories' => array(
498 'firewall' => 50,
499 'login' => 20,
500 'user' => 5,
501 'plugin' => 0,
502 'file' => 0,
503 'security' => 0,
504 'system' => 0,
505 'settings' => 0,
506 'theme' => 0,
507 'content' => 0,
508 'comment' => 0,
509 'media' => 0,
510 ),
511 ),
512 ),
513 );
514 }
515
516 /**
517 * Get all options (merged with defaults)
518 *
519 * @return array All options
520 */
521 public function get_all_options() {
522 if ( null === $this->options ) {
523 $saved = get_option( self::OPTION_NAME, array() );
524 $this->options = $this->array_merge_deep( $this->get_default_options(), $saved );
525 }
526 return $this->options;
527 }
528
529 /**
530 * Deep merge arrays
531 *
532 * @param array $defaults Default values.
533 * @param array $saved Saved values.
534 * @return array Merged array.
535 */
536 private function array_merge_deep( $defaults, $saved ) {
537 $result = $defaults;
538
539 foreach ( $saved as $key => $value ) {
540 if ( is_array( $value ) && isset( $result[ $key ] ) && is_array( $result[ $key ] ) ) {
541 $result[ $key ] = $this->array_merge_deep( $result[ $key ], $value );
542 } else {
543 $result[ $key ] = $value;
544 }
545 }
546
547 return $result;
548 }
549
550 /**
551 * Get a specific section
552 *
553 * @param string $section Section name.
554 * @return array Section options.
555 */
556 public function get_section( $section ) {
557 $options = $this->get_all_options();
558 return isset( $options[ $section ] ) ? $options[ $section ] : array();
559 }
560
561 /**
562 * Get a specific option
563 *
564 * @param string $section Section name.
565 * @param string $key Option key.
566 * @param mixed $default Default value.
567 * @return mixed Option value.
568 */
569 public function get_option( $section, $key, $default = null ) {
570 $options = $this->get_all_options();
571
572 if ( isset( $options[ $section ][ $key ] ) ) {
573 return $options[ $section ][ $key ];
574 }
575
576 return $default;
577 }
578
579 /**
580 * Check if a module is enabled
581 *
582 * @param string $module Module name.
583 * @return bool Whether module is enabled.
584 */
585 public function is_module_enabled( $module ) {
586 $options = $this->get_all_options();
587 return ! empty( $options['modules'][ $module ] );
588 }
589
590 /**
591 * Save options
592 *
593 * @param array $options Options to save.
594 * @return bool Success status.
595 */
596 public function save_options( $options ) {
597 $this->options = null;
598 return update_option( self::OPTION_NAME, $options );
599 }
600
601 /**
602 * Update a section
603 *
604 * @param string $section Section name.
605 * @param array $data Section data.
606 * @return bool Success status.
607 */
608 public function update_section( $section, $data ) {
609 $options = get_option( self::OPTION_NAME, array() );
610 $options[ $section ] = $data;
611 $this->options = null;
612 return update_option( self::OPTION_NAME, $options );
613 }
614
615 /**
616 * Update multiple sections at once
617 *
618 * @param array $sections Associative array of section => data.
619 * @return bool Success status.
620 */
621 public function update_options( $sections ) {
622 $options = get_option( self::OPTION_NAME, array() );
623
624 foreach ( $sections as $section => $data ) {
625 $options[ $section ] = $data;
626 }
627
628 $this->options = null;
629 return update_option( self::OPTION_NAME, $options );
630 }
631
632 /**
633 * Clear the options cache
634 */
635 public function clear_cache() {
636 $this->options = null;
637 wp_cache_delete( self::OPTION_NAME, 'options' );
638 }
639
640 /**
641 * Get presets with descriptions
642 *
643 * @return array Presets configuration.
644 */
645 public function get_presets() {
646 return array(
647 'standard' => array(
648 'name' => __( 'Standard', 'vigilante' ),
649 'description' => __( 'Balanced security suitable for most websites. Enables all modules with sensible defaults.', 'vigilante' ),
650 'modules' => array(
651 'firewall' => true,
652 'security_headers' => true,
653 'login_security' => true,
654 'rest_api_security'=> true,
655 'user_security' => true,
656 'wp_hardening' => true,
657 'file_integrity' => true,
658 'activity_log' => true,
659 ),
660 'firewall' => array(
661 'block_bad_query_strings' => true,
662 'block_sql_injection' => true,
663 'block_xss_attacks' => true,
664 'rate_limiting' => array(
665 'enabled' => true,
666 'requests_per_minute' => 120,
667 ),
668 ),
669 'login_security' => array(
670 'max_attempts' => 5,
671 'lockout_duration' => 1800,
672 'disable_xmlrpc' => true,
673 ),
674 'rest_api_security' => array(
675 'mode' => 'selective',
676 ),
677 'user_security' => array(
678 'prevent_display_name_login_match' => true,
679 ),
680 'file_integrity' => array(
681 'notify_level' => 'suspicious_only',
682 ),
683 ),
684
685 'maximum' => array(
686 'name' => __( 'Maximum Security', 'vigilante' ),
687 'description' => __( 'Strictest settings for high-security sites. CSP is set to report-only mode to prevent breaking the admin interface.', 'vigilante' ),
688 'modules' => array(
689 'firewall' => true,
690 'security_headers' => true,
691 'login_security' => true,
692 'rest_api_security'=> true,
693 'user_security' => true,
694 'wp_hardening' => true,
695 'file_integrity' => true,
696 'activity_log' => true,
697 ),
698 'firewall' => array(
699 'block_bad_query_strings' => true,
700 'block_sql_injection' => true,
701 'block_xss_attacks' => true,
702 'block_file_inclusion' => true,
703 'block_directory_traversal' => true,
704 'block_bad_bots' => true,
705 'block_empty_user_agent' => true,
706 'rate_limiting' => array(
707 'enabled' => true,
708 'requests_per_minute' => 60,
709 'block_duration' => 600,
710 'progressive' => true,
711 'max_block_duration' => 86400,
712 ),
713 ),
714 'security_headers' => array(
715 'x_frame_options' => 'DENY',
716 // HSTS is intentionally NOT enabled by Maximum: forcing HSTS on a site
717 // that doesn't have a healthy HTTPS setup (or temporarily falls back to
718 // HTTP) locks visitors out for the full max_age. Leaving HSTS off keeps
719 // it as an explicit opt-in decision per site.
720 'csp' => array(
721 'enabled' => true,
722 'report_only' => false,
723 'directives' => array(
724 'default-src' => "'self'",
725 'script-src' => "'self' 'unsafe-inline' 'unsafe-eval'",
726 'style-src' => "'self' 'unsafe-inline'",
727 'img-src' => "'self' data: https: blob:",
728 'font-src' => "'self' data:",
729 'connect-src' => "'self' https: blob:",
730 'frame-src' => "'self' blob:",
731 'frame-ancestors' => "'none'",
732 'worker-src' => "'self' blob:",
733 'object-src' => "'none'",
734 'base-uri' => "'self'",
735 ),
736 ),
737 ),
738 'rest_api_security' => array(
739 'mode' => 'authenticated_only',
740 ),
741 'login_security' => array(
742 'max_attempts' => 3,
743 'lockout_duration' => 3600,
744 'lockout_increment' => true,
745 'disable_xmlrpc' => true,
746 'notify_on_lockout' => true,
747 'notify_on_admin_login' => true,
748 ),
749 'wp_hardening' => array(
750 'disallow_file_edit' => true,
751 'disallow_file_mods' => true,
752 // close_old_comments is intentionally NOT touched by Maximum:
753 // it would unilaterally close discussion on every old post,
754 // which is a content decision, not a security one.
755 ),
756 'user_security' => array(
757 'prevent_display_name_login_match' => true,
758 'min_password_length' => 16,
759 'password_policy' => array(
760 'require_uppercase' => true,
761 'require_lowercase' => true,
762 'require_number' => true,
763 'require_special' => true,
764 'block_common' => true,
765 'block_username' => true,
766 'affected_roles' => array(),
767 ),
768 'admin_monitoring' => array(
769 'alert_new_admin' => true,
770 'alert_admin_email_change' => true,
771 'alert_permission_elevation' => true,
772 'alert_admin_password_change' => true,
773 ),
774 'registration_approval' => array(
775 'enabled' => true,
776 'notify_admin' => true,
777 'auto_reject_days' => 7,
778 'affected_roles' => array( 'subscriber', 'contributor', 'author', 'editor' ),
779 ),
780 'session_limits' => array(
781 'enabled' => true,
782 'max_sessions' => 1,
783 'behavior' => 'close_oldest',
784 'exclude_admins' => false,
785 ),
786 'password_expiration' => array(
787 'enabled' => true,
788 'expire_days' => 30,
789 'warning_days' => 7,
790 'affected_roles' => array( 'administrator', 'editor', 'author', 'contributor', 'subscriber' ),
791 'password_history' => 5,
792 'send_reminder' => true,
793 ),
794 'email_verification' => array(
795 'enabled' => true,
796 'token_expiry_hours' => 24,
797 'allow_resend' => true,
798 'auto_delete_days' => 3,
799 ),
800 ),
801 'file_integrity' => array(
802 'scan_core' => true,
803 'scan_plugins' => true,
804 'scan_themes' => true,
805 'scan_uploads' => true,
806 'scan_critical_config' => true,
807 'auto_scan' => true,
808 'scan_frequency' => 'daily',
809 'notify_level' => 'all',
810 'instant_alert' => true,
811 ),
812 'activity_log' => array(
813 'log_logins' => true,
814 'log_failed_logins' => true,
815 'log_user_changes' => true,
816 'log_post_changes' => true,
817 'log_plugin_changes' => true,
818 'log_theme_changes' => true,
819 'log_option_changes' => true,
820 'log_file_changes' => true,
821 'log_comments' => true,
822 'log_media' => true,
823 ),
824 ),
825 );
826 }
827
828 /**
829 * Get module labels for display
830 *
831 * @return array Module labels.
832 */
833 public function get_module_labels() {
834 return array(
835 'firewall' => __( 'Firewall', 'vigilante' ),
836 'security_headers' => __( 'Security Headers', 'vigilante' ),
837 'login_security' => __( 'Login Security', 'vigilante' ),
838 'rest_api_security'=> __( 'REST API Security', 'vigilante' ),
839 'user_security' => __( 'User Security', 'vigilante' ),
840 'wp_hardening' => __( 'WordPress Hardening', 'vigilante' ),
841 'file_integrity' => __( 'File Integrity', 'vigilante' ),
842 'activity_log' => __( 'Security Audit', 'vigilante' ),
843 );
844 }
845
846 /**
847 * Get module descriptions for display
848 *
849 * @return array Module descriptions.
850 */
851 public function get_module_descriptions() {
852 return array(
853 'firewall' => __( 'Blocks malicious requests, SQL injection, XSS attacks, and bad bots. Includes rate limiting and file protection.', 'vigilante' ),
854 'security_headers' => __( 'Adds HTTP security headers like CSP, HSTS, X-Frame-Options. Forces HTTPS and fixes mixed content.', 'vigilante' ),
855 'login_security' => __( 'Brute force protection, 2FA, login attempt limits, XML-RPC control, and notifications.', 'vigilante' ),
856 'rest_api_security'=> __( 'Controls REST API access, blocks user enumeration, and protects sensitive endpoints.', 'vigilante' ),
857 'user_security' => __( 'Blocks insecure usernames, enforces strong passwords, and prevents author scanning.', 'vigilante' ),
858 'wp_hardening' => __( 'Hardens wp-config.php, manages comments, cleans header output, and controls feeds.', 'vigilante' ),
859 'file_integrity' => __( 'Scans WordPress core, plugins, and themes for unauthorized changes and suspicious code.', 'vigilante' ),
860 'activity_log' => __( 'Records user actions, logins, content changes, and security events for security auditing.', 'vigilante' ),
861 );
862 }
863
864 /**
865 * Validate options before saving
866 *
867 * @param array $input Raw input to validate.
868 * @return array Validated options.
869 */
870 public function validate_options( $input ) {
871 $validated = array();
872 $defaults = $this->get_default_options();
873
874 // Validate each section that exists in input
875 foreach ( $input as $section => $data ) {
876 if ( ! is_array( $data ) ) {
877 continue;
878 }
879
880 if ( 'modules' === $section ) {
881 // Validate modules (booleans)
882 foreach ( $defaults['modules'] as $module => $default_value ) {
883 $validated['modules'][ $module ] = isset( $data[ $module ] )
884 ? (bool) $data[ $module ]
885 : false;
886 }
887 } elseif ( isset( $defaults[ $section ] ) ) {
888 // Validate other sections using generic validator
889 $validated[ $section ] = $this->validate_section( $data, $defaults[ $section ] );
890 }
891 }
892
893 return apply_filters( 'vigilante_validate_options', $validated, $input );
894 }
895
896 /**
897 * Validate a section based on defaults
898 *
899 * @param array $input Input values.
900 * @param array $defaults Default values.
901 * @return array Validated values.
902 */
903 private function validate_section( $input, $defaults ) {
904 $validated = array();
905
906 foreach ( $defaults as $key => $default_value ) {
907 if ( ! isset( $input[ $key ] ) ) {
908 $validated[ $key ] = $default_value;
909 continue;
910 }
911
912 $value = $input[ $key ];
913
914 if ( is_bool( $default_value ) ) {
915 $validated[ $key ] = (bool) $value;
916 } elseif ( is_int( $default_value ) ) {
917 $validated[ $key ] = intval( $value );
918 } elseif ( is_array( $default_value ) ) {
919 if ( is_array( $value ) ) {
920 $validated[ $key ] = $this->validate_section( $value, $default_value );
921 } else {
922 $validated[ $key ] = $default_value;
923 }
924 } else {
925 $validated[ $key ] = sanitize_text_field( $value );
926 }
927 }
928
929 // Include any extra keys from input
930 foreach ( $input as $key => $value ) {
931 if ( ! isset( $validated[ $key ] ) ) {
932 if ( is_array( $value ) ) {
933 $validated[ $key ] = array_map( 'sanitize_text_field', $value );
934 } else {
935 $validated[ $key ] = sanitize_text_field( $value );
936 }
937 }
938 }
939
940 return $validated;
941 }
942 }