| @@ -551,46 +551,114 @@ | ||
| 551 | 551 | return implode( "\n", $rules ); |
| 552 | 552 | } |
| 553 | 553 | |
| 554 | 554 | /** |
| 555 | - * Warnings about the Content-Security-Policy as it is configured | |
| 555 | + * Get security grade based on enabled headers | |
| 556 | 556 | * |
| 557 | - * They are about what is saved in the tab, not about what reaches the | |
| 558 | - * browser: that is measured by the checks of Security Check, which is | |
| 559 | - * also what the Test Headers button shows. Until 3.0.4 that button graded | |
| 560 | - * these settings and listed them as enabled headers without asking the | |
| 561 | - * site for anything. | |
| 562 | - * | |
| 563 | - * @since 3.0.5 | |
| 564 | - * | |
| 565 | - * @return string[] | |
| 557 | + * @return array | |
| 566 | 558 | */ |
| 567 | - public function get_csp_warnings() { | |
| 559 | + public function get_security_grade() { | |
| 560 | + $score = 0; | |
| 561 | + $enabled = array(); | |
| 562 | + $missing = array(); | |
| 568 | 563 | $warnings = array(); |
| 569 | 564 | |
| 570 | - if ( empty( $this->options['csp']['enabled'] ) ) { | |
| 571 | - return $warnings; | |
| 565 | + // X-Frame-Options (15 points) | |
| 566 | + if ( ! empty( $this->options['x_frame_options'] ) ) { | |
| 567 | + $score += 15; | |
| 568 | + $enabled[] = 'X-Frame-Options: ' . $this->options['x_frame_options']; | |
| 569 | + } else { | |
| 570 | + $missing[] = 'X-Frame-Options'; | |
| 572 | 571 | } |
| 573 | 572 | |
| 574 | - if ( ! empty( $this->options['csp']['report_only'] ) ) { | |
| 575 | - $warnings[] = __( 'CSP is in report-only mode: the browser reports what the policy would block, and blocks nothing.', 'vigilante' ); | |
| 576 | - return $warnings; | |
| 573 | + // X-Content-Type-Options (15 points) | |
| 574 | + if ( ! empty( $this->options['x_content_type_options'] ) ) { | |
| 575 | + $score += 15; | |
| 576 | + $enabled[] = 'X-Content-Type-Options: nosniff'; | |
| 577 | + } else { | |
| 578 | + $missing[] = 'X-Content-Type-Options'; | |
| 577 | 579 | } |
| 578 | 580 | |
| 579 | - if ( $this->is_csp_restrictive() ) { | |
| 580 | - $warnings[] = __( 'Restrictive CSP detected. Admin area is automatically excluded to prevent breaking the dashboard and block editor.', 'vigilante' ); | |
| 581 | + // HSTS (20 points) | |
| 582 | + if ( ! empty( $this->options['hsts']['enabled'] ) ) { | |
| 583 | + $hsts = $this->options['hsts']; | |
| 584 | + if ( $hsts['max_age'] >= 31536000 ) { | |
| 585 | + $score += 20; | |
| 586 | + } else { | |
| 587 | + $score += 10; | |
| 588 | + $warnings[] = 'HSTS max-age should be at least 1 year (31536000 seconds)'; | |
| 589 | + } | |
| 590 | + $enabled[] = 'Strict-Transport-Security'; | |
| 591 | + } else { | |
| 592 | + $missing[] = 'Strict-Transport-Security (HSTS)'; | |
| 581 | 593 | } |
| 582 | 594 | |
| 583 | - foreach ( $this->get_csp_compatibility_issues() as $issue ) { | |
| 584 | - if ( 'high' === $issue['severity'] ) { | |
| 585 | - $warnings[] = sprintf( | |
| 586 | - /* translators: 1: CSP directive name, 2: Issue description */ | |
| 587 | - __( 'CSP %1$s: %2$s', 'vigilante' ), | |
| 588 | - $issue['directive'], | |
| 589 | - $issue['issue'] | |
| 590 | - ); | |
| 595 | + // CSP (30 points; absorbs the 10 points freed by retiring the deprecated X-XSS-Protection) | |
| 596 | + if ( ! empty( $this->options['csp']['enabled'] ) ) { | |
| 597 | + if ( empty( $this->options['csp']['report_only'] ) ) { | |
| 598 | + $score += 30; | |
| 599 | + $enabled[] = 'Content-Security-Policy'; | |
| 600 | + | |
| 601 | + // Add warning if CSP is restrictive | |
| 602 | + if ( $this->is_csp_restrictive() ) { | |
| 603 | + $warnings[] = __( 'Restrictive CSP detected. Admin area is automatically excluded to prevent breaking the dashboard and block editor.', 'vigilante' ); | |
| 604 | + } | |
| 605 | + | |
| 606 | + // Check for specific compatibility issues | |
| 607 | + $csp_issues = $this->get_csp_compatibility_issues(); | |
| 608 | + foreach ( $csp_issues as $issue ) { | |
| 609 | + if ( 'high' === $issue['severity'] ) { | |
| 610 | + $warnings[] = sprintf( | |
| 611 | + /* translators: 1: CSP directive name, 2: Issue description */ | |
| 612 | + __( 'CSP %1$s: %2$s', 'vigilante' ), | |
| 613 | + $issue['directive'], | |
| 614 | + $issue['issue'] | |
| 615 | + ); | |
| 616 | + } | |
| 617 | + } | |
| 618 | + } else { | |
| 619 | + $score += 15; | |
| 620 | + $enabled[] = 'Content-Security-Policy-Report-Only'; | |
| 621 | + $warnings[] = 'CSP is in report-only mode (recommended for testing)'; | |
| 591 | 622 | } |
| 623 | + } else { | |
| 624 | + $missing[] = 'Content-Security-Policy'; | |
| 592 | 625 | } |
| 593 | 626 | |
| 594 | - return $warnings; | |
| 627 | + // Referrer-Policy (10 points) | |
| 628 | + if ( ! empty( $this->options['referrer_policy'] ) ) { | |
| 629 | + $score += 10; | |
| 630 | + $enabled[] = 'Referrer-Policy: ' . $this->options['referrer_policy']; | |
| 631 | + } else { | |
| 632 | + $missing[] = 'Referrer-Policy'; | |
| 633 | + } | |
| 634 | + | |
| 635 | + // Permissions-Policy (10 points) | |
| 636 | + if ( ! empty( $this->options['permissions_policy']['enabled'] ) ) { | |
| 637 | + $score += 10; | |
| 638 | + $enabled[] = 'Permissions-Policy'; | |
| 639 | + } else { | |
| 640 | + $missing[] = 'Permissions-Policy'; | |
| 641 | + } | |
| 642 | + | |
| 643 | + // Calculate grade | |
| 644 | + if ( $score >= 90 ) { | |
| 645 | + $grade = 'A'; | |
| 646 | + } elseif ( $score >= 80 ) { | |
| 647 | + $grade = 'B'; | |
| 648 | + } elseif ( $score >= 70 ) { | |
| 649 | + $grade = 'C'; | |
| 650 | + } elseif ( $score >= 60 ) { | |
| 651 | + $grade = 'D'; | |
| 652 | + } else { | |
| 653 | + $grade = 'F'; | |
| 654 | + } | |
| 655 | + | |
| 656 | + return array( | |
| 657 | + 'grade' => $grade, | |
| 658 | + 'score' => $score, | |
| 659 | + 'headers' => $enabled, | |
| 660 | + 'missing' => $missing, | |
| 661 | + 'warnings' => $warnings, | |
| 662 | + ); | |
| 595 | 663 | } |
| 596 | 664 | } |