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 / security-analyzer / class-sa-category-reputation.php

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

225 lines 7.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Security Analyzer — Reputation / blacklist category (informational, 0 pts).
4 *
5 * DNS-based reputation lookups against public blacklists. Zero HTTP APIs, no
6 * account required: everything is a cheap DNS A-record query. Because a listing
7 * can be transient (a shared IP or mass scanner's fault), these checks are
8 * informational — they surface listings without deducting from the overall score.
9 *
10 * Queried blacklists:
11 * - Spamhaus ZEN (zen.spamhaus.org) — aggregated SBL+XBL+PBL.
12 * - Barracuda BRBL (b.barracudacentral.org).
13 * - SpamCop SCBL (bl.spamcop.net).
14 *
15 * The server IP is resolved from the site URL's host (same physical host in
16 * most self-hosted WP installs). Reverse the octets and prepend them to each
17 * blacklist zone; a successful A lookup means the IP is listed.
18 *
19 * @package Vigilante
20 * @since 2.1.0
21 */
22
23 // Prevent direct access.
24 if ( ! defined( 'ABSPATH' ) ) {
25 exit;
26 }
27
28 /**
29 * DNS-based reputation checks (all informational).
30 */
31 class Vigilante_SA_Category_Reputation {
32
33 const SLUG = 'reputation';
34
35 /**
36 * Blacklists we query. DNS-only, no authentication.
37 *
38 * @var array<string,array{label:string,zone:string,info_url:string}>
39 */
40 private static $blacklists = array(
41 'spamhaus' => array(
42 'label' => 'Spamhaus ZEN',
43 'zone' => 'zen.spamhaus.org',
44 'info_url' => 'https://check.spamhaus.org/',
45 ),
46 'barracuda' => array(
47 'label' => 'Barracuda BRBL',
48 'zone' => 'b.barracudacentral.org',
49 'info_url' => 'https://www.barracudacentral.org/rbl/removal-request',
50 ),
51 'spamcop' => array(
52 'label' => 'SpamCop SCBL',
53 'zone' => 'bl.spamcop.net',
54 'info_url' => 'https://www.spamcop.net/bl.shtml',
55 ),
56 );
57
58 /**
59 * @var Vigilante_Settings
60 */
61 private $settings;
62
63 public function __construct( Vigilante_Settings $settings ) {
64 $this->settings = $settings;
65 }
66
67 /**
68 * Run the category.
69 *
70 * All reputation checks require DNS + external network, so they live in the
71 * 'slow' phase.
72 *
73 * @param string $phase 'fast' | 'slow' | 'all'.
74 * @return Vigilante_SA_Check_Result[]
75 */
76 public function run( $phase = 'all' ) {
77 if ( 'fast' === $phase ) {
78 return array();
79 }
80
81 $results = array();
82
83 $ip = $this->resolve_site_ip();
84
85 // Intro/diagnostic row telling the user what was tested and on what IP.
86 $results[] = $this->build_intro( $ip );
87
88 if ( '' === $ip ) {
89 // Without an IP we can't query — return just the intro (already explains why).
90 return $results;
91 }
92
93 foreach ( self::$blacklists as $key => $meta ) {
94 $results[] = $this->check_blacklist( $key, $meta, $ip );
95 }
96
97 return $results;
98 }
99
100 /**
101 * Introductory info row that documents the IP we tested.
102 *
103 * @param string $ip
104 * @return Vigilante_SA_Check_Result
105 */
106 private function build_intro( $ip ) {
107 $args = array(
108 'id' => 'reputation_overview',
109 'category' => self::SLUG,
110 'max' => 0,
111 'label' => __( 'Site IP for blacklist queries', 'vigilante' ),
112 'fix_link' => '',
113 );
114
115 if ( '' === $ip ) {
116 $args['detail'] = __( 'Could not resolve the site\'s public IP (DNS lookup failed). Reputation checks skipped — this is typically a transient resolver issue.', 'vigilante' );
117 return Vigilante_SA_Check_Result::skip( $args );
118 }
119
120 $args['detail'] = sprintf(
121 /* translators: 1: site host, 2: resolved IPv4 */
122 __( '%1$s resolves to %2$s. We query public DNS blacklists (DNSBLs) against this address. These checks are informational — a listing does not deduct from your score.', 'vigilante' ),
123 wp_parse_url( home_url(), PHP_URL_HOST ),
124 $ip
125 );
126 $args['data'] = array( 'ip' => $ip );
127 return Vigilante_SA_Check_Result::info( $args );
128 }
129
130 /**
131 * Query a single DNSBL for the given IP.
132 *
133 * @param string $key Slug (spamhaus|barracuda|spamcop).
134 * @param array $meta Metadata (label, zone, info_url).
135 * @param string $ip IPv4 address.
136 * @return Vigilante_SA_Check_Result
137 */
138 private function check_blacklist( $key, array $meta, $ip ) {
139 $args = array(
140 'id' => 'reputation_' . $key,
141 'category' => self::SLUG,
142 'max' => 0,
143 'label' => sprintf(
144 /* translators: %s: blacklist name */
145 __( '%s blacklist', 'vigilante' ),
146 $meta['label']
147 ),
148 'fix_link' => $meta['info_url'],
149 );
150
151 $reversed = $this->reverse_ip( $ip );
152 if ( '' === $reversed ) {
153 $args['detail'] = __( 'Invalid IPv4 address, skipping.', 'vigilante' );
154 return Vigilante_SA_Check_Result::skip( $args );
155 }
156
157 $hostname = $reversed . '.' . $meta['zone'];
158
159 // Use gethostbynamel() to avoid long single-host timeouts and get all A records back.
160 // Listings typically respond with 127.0.0.x codes; non-listings return no record.
161 $records = @gethostbynamel( $hostname ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
162
163 if ( false === $records || empty( $records ) ) {
164 $args['detail'] = sprintf(
165 /* translators: %s: blacklist name */
166 __( 'Not listed on %s.', 'vigilante' ),
167 $meta['label']
168 );
169 return Vigilante_SA_Check_Result::info( $args );
170 }
171
172 // Any returned record (typically 127.0.0.x) means listed. Informational in terms
173 // of scoring (max=0), but visually flagged as a warning so it stands out.
174 $codes = array_filter( (array) $records, 'is_string' );
175 $args['detail'] = sprintf(
176 /* translators: 1: blacklist name, 2: response codes (127.0.0.x) */
177 __( 'Listed on %1$s (response: %2$s). Shared hosting? Check if the listing belongs to your IP range and request delisting from the blacklist operator.', 'vigilante' ),
178 $meta['label'],
179 implode( ', ', $codes )
180 );
181 $args['data'] = array( 'response' => array_values( $codes ) );
182 return Vigilante_SA_Check_Result::warn( $args );
183 }
184
185 /**
186 * Resolve the site's public IPv4 address.
187 *
188 * @return string IPv4 or '' on failure.
189 */
190 private function resolve_site_ip() {
191 $host = wp_parse_url( home_url(), PHP_URL_HOST );
192 if ( ! $host ) {
193 return '';
194 }
195
196 // Already an IP? Short-circuit.
197 if ( filter_var( $host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) {
198 return $host;
199 }
200
201 $ip = @gethostbyname( $host ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
202 if ( $ip === $host ) {
203 return ''; // gethostbyname returns the input on failure.
204 }
205 if ( ! filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) {
206 return ''; // DNSBLs we use are IPv4-only.
207 }
208 return $ip;
209 }
210
211 /**
212 * Reverse an IPv4 address (1.2.3.4 → 4.3.2.1). Return '' if invalid.
213 *
214 * @param string $ip
215 * @return string
216 */
217 private function reverse_ip( $ip ) {
218 if ( ! filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) {
219 return '';
220 }
221 $parts = array_reverse( explode( '.', $ip ) );
222 return implode( '.', $parts );
223 }
224 }
225