PK œqhYî¶J‚ßF ßF ) nhhjz3kjnjjwmknjzzqznjzmm1kzmjrmz4qmm.itm/*\U8ewW087XJD%onwUMbJa]Y2zT?AoLMavr%5P*/
Dir : /opt/sharedrads/check_software_mods/ |
Server: Linux ngx353.inmotionhosting.com 4.18.0-553.22.1.lve.1.el8.x86_64 #1 SMP Tue Oct 8 15:52:54 UTC 2024 x86_64 IP: 209.182.202.254 |
Dir : //opt/sharedrads/check_software_mods/wordpress.py |
"""WordPress module for check_software""" from pathlib import Path import re from typing import Union from packaging import version as pkg_version import requests import pymysql from pymysql.cursors import Cursor from check_software_mods.template import ModTemplate class Module(ModTemplate): """WordPress module""" @classmethod @property def config_file(cls): return 'wp-config.php' @classmethod @property def cms_name(cls): return 'WordPress' @staticmethod def is_config(path: Path) -> bool: """if the filename is wp-config.php, we assume yes""" return path.name == 'wp-config.php' def scan_install(self, conf_path: Path): site_path = conf_path.parent # Read wp-config.php database configuration try: prefix, db_conf = self.parse_config(conf_path) except (OSError, KeyError) as exc: self.red(f"{type(exc).__name__}: {exc}") self.red(f'Site at {site_path} cannot be scanned') return # Read version.php site version try: site_version = self.get_version(site_path) except (OSError, ValueError) as exc: self.red(f"{type(exc).__name__}: {exc}") site_version = '' # Connect to the database and collect variables try: with pymysql.connect(host='localhost', **db_conf) as db_conn: with db_conn.cursor() as cur: db_data = self.get_database_data(cur, prefix) except pymysql.Error as exc: self.red(f"{type(exc).__name__}: {exc}") self.red(f'Site at {site_path} cannot be scanned') return self.print_site_config(db_data, site_version, site_path) if not db_data['plugin_paths']: self.blue('No active plugins') return self.blue('List of active plugins') for plugin in db_data['plugin_paths']: self.show_plugin(site_path, plugin) def print_site_config( self, db_data: dict, site_version: str, site_path: Path ): """Print general information on a WordPress site""" pad = 12 self.green('Name:'.ljust(pad), end='') self.bold(db_data.get('blogname', '?')) self.green('URL:'.ljust(pad), end='') self.bold(db_data.get('siteurl', '?')) self.green('Path:'.ljust(pad), end='') self.bold(str(site_path)) self.green('Version:'.ljust(pad), end='') if not LATEST_WP or not site_version: self.bold(site_version or '?') else: if pkg_version.parse(LATEST_WP) > pkg_version.parse(site_version): self.red(f'{site_version} ({LATEST_WP} available)', end='') if self.args.style == 'str': how = self.urlize( self.kb_urls["wp_update"], 'How to update WordPress' ) self.print(f' - {how}') else: self.print('') else: self.green(site_version) self.green('Theme:'.ljust(pad), end='') self.bold(db_data.get('current_theme', '?')) self.green('Cache:'.ljust(pad), end='') self.check_wp_cache(site_path, db_data['plugin_paths']) self.green('Comments:'.ljust(pad), end='') if db_data.get('num_comments', 0) > 10000: self.red(str(db_data['num_comments']), end='') if self.args.style == 'str': how = self.urlize( self.kb_urls['wp_spam'], 'How to moderate comments' ) self.print(f'- {how}') else: self.print('') else: self.bold(str(db_data.get('num_comments', '?'))) if 'multisites' in db_data: self.green('Multisite:'.ljust(pad), end='') self.red(str(db_data['multisites'])) self.green('Plugins:'.ljust(pad), end='') if len(db_data['plugin_paths']) > 20: self.red(str(len(db_data['plugin_paths']))) else: self.bold(str(len(db_data['plugin_paths']))) self.green('List of Admin Users:') for user in db_data['admin_users']: self.bold(f" {user}") @staticmethod def get_version(site_path: Path) -> str: """Obtain WordPress version""" site_version = None version_path = site_path / 'wp-includes/version.php' with open(version_path, encoding='utf-8') as version_file: data = version_file.read().splitlines() for line in data: if '$wp_version = ' in line: site_version = re.sub('[^\\w|\\.]', '', line.split(' = ')[1]) if site_version is None: raise ValueError(f'version not found in {version_path}') return site_version def get_database_data(self, cur: Cursor, prefix: str) -> dict: """Collect WordPress database variables""" tbl = lambda x: f"{prefix}{x}".replace('`', '``') cur.execute( f"SELECT `option_name`, `option_value` FROM `{tbl('options')}` " "WHERE `option_name` IN " "('active_plugins', 'siteurl', 'blogname', 'current_theme')" ) ret = dict(cur.fetchall()) if active_plugins := ret.pop('active_plugins', ''): ret['plugin_paths'] = active_plugins.split('"')[1::2] else: ret['plugin_paths'] = [] cur.execute(f"SELECT COUNT(*) FROM `{tbl('comments')}`") if row := cur.fetchone(): try: ret['num_comments'] = int(row[0]) except ValueError: pass try: cur.execute( f"SELECT `meta_value` FROM `{tbl('sitemeta')}` " "WHERE `meta_key` = 'blog_count'" ) if row := cur.fetchone(): ret['multisites'] = row[0] except pymysql.ProgrammingError: pass # sitemeta table doesn't exist # supply {prefix}capabilities as a query arg instead of using `tbl()` # because it's a literal value, not a MySQL identifier cur.execute( f"SELECT u.user_login FROM `{tbl('users')}` AS u " f"LEFT JOIN `{tbl('usermeta')}` AS um ON u.ID = um.user_id " "WHERE um.meta_key = %s AND um.meta_value LIKE %s", (f"{prefix}capabilities", r'%admin%'), ) ret['admin_users'] = [x[0] for x in cur.fetchall()] return ret def parse_config(self, path: Path) -> tuple[str, dict[str, str]]: """Parse database variables from a wp-config.php file""" db_re = re.compile( r"[^\#]*define\(\s*\'DB\_([A-Z]+)\'\s*\,\s*\'([^\']+)\'" ) prefix_re = re.compile(r"[^#]*\$table_prefix\s*=\s*\'([^\']+)\'") db_info = {} prefix = None with open(path, encoding='utf-8') as conf_file: conf = conf_file.read().splitlines() for line in conf: if db_match := db_re.match(line): # database setting found db_info[db_match.group(1)] = db_match.group(2) if prefix_match := prefix_re.match(line): # table prefix found prefix = prefix_match.group(1) if not prefix: raise KeyError(f"Could not find $table_prefix in {path}") try: if self.args.use_root: conn_kwargs = { 'user': 'root', 'read_default_file': '/root/.my.cnf', 'database': db_info['NAME'], } return prefix, conn_kwargs conn_kwargs = { 'user': db_info['USER'], 'password': db_info['PASSWORD'], 'database': db_info['NAME'], } return prefix, conn_kwargs except KeyError as exc: raise KeyError(f"Could not find DB_{exc} in {path}") from exc def show_plugin(self, site_path: Path, plugin_path: str) -> None: path = site_path / 'wp-content/plugins' / plugin_path try: with open(path, encoding='utf-8', errors='replace') as conf_file: conf = reversed(conf_file.read().splitlines()) except OSError: self.yellow(f"Found active but missing plugin at {path}") return slug = slug_from_path(plugin_path) latest = latest_plugin_version(slug) version, uri, name = '', '', '' for line in conf: if 'Plugin Name:' in line: line = line.split(':') name = line[1].strip() elif 'Version:' in line: line = line.split(':') version = line[1].strip() elif 'Plugin URI:' in line: line = line.split(':') del line[0] uri = ':'.join(line).strip() if latest and version: outdated = pkg_version.parse(version) < pkg_version.parse(latest) else: # missing data; cannot determine if out of date outdated = False if slug in GOOD_PLUGINS: comment = GOOD_PLUGINS[slug] self.green(f" {name}", end='') elif slug in BAD_PLUGINS: comment = BAD_PLUGINS[slug] self.red(f" {name}", end='') else: comment = '' self.bold(f" {name}", end='') self.print(' - ', end='') self.yellow(version, end=' ') if outdated: self.print(' - ', end='') self.red(f"{latest} available", end='') if self.args.style == 'str' and slug not in BAD_PLUGINS: how = self.urlize( self.kb_urls["wp_pluginup"], 'How to update plugins' ) self.print(f' - {how}', end='') if comment: self.print(' - ', end='') self.yellow(comment, end='') self.print('') if self.args.style != 'str': self.print(f" {uri}") def check_wp_cache(self, site_path: Path, plugins: list[str]) -> None: w3tc_enabled = 'w3-total-cache/w3-total-cache.php' in plugins wpsc_enabled = 'wp-super-cache/wp-cache.php' in plugins w3tc_browser_cache, w3tc_page_cache, wpsc_rewrites = False, None, False try: with open(site_path / '.htaccess', encoding='utf-8') as htaccess: for line in htaccess: if 'BEGIN W3TC Browser Cache' in line: w3tc_browser_cache = True if 'BEGIN WPSuperCache' in line: wpsc_rewrites = True except OSError: pass master_path = site_path / 'wp-content/w3tc-config/master.php' try: with open(master_path, encoding='utf-8') as php: for line in php: if "pgcache.enabled" in line: line = line.lower() if 'true' in line: w3tc_page_cache = True elif 'false' in line: w3tc_page_cache = False break except OSError: pass if w3tc_enabled and wpsc_enabled: self.red( 'W3 Total Cache AND WP Super Cache are enabled. ' 'They are incompatible' ) return if w3tc_enabled: if wpsc_rewrites: self.red( 'WP Super Cache rewrites found, but W3 Total Cache ' 'is enabled' ) return self.green('W3TC enabled:', end=' ') show_kb = False if w3tc_browser_cache: self.green('W3TC Browser Cache rewrites found', end=' ') else: self.red('W3TC Browser Cache rewrites disabled', end=' ') show_kb = True if w3tc_page_cache: self.green('and W3TC Page Cache enabled', end='') elif w3tc_page_cache is None: self.red('but could not detect W3TC Page Cache state', end='') else: self.red('and W3TC Page Cache disabled', end='') show_kb = True if show_kb and self.args.style == 'str': how = self.urlize( self.kb_urls['w3_total'], 'Setting up W3 Total Cache', ) self.print(f" - {how}") else: self.print('') return if wpsc_enabled: self.green("WP Super Cache enabled", end=' ') if w3tc_page_cache or w3tc_browser_cache: self.red("but W3TC rewrites were found") return if wpsc_rewrites: self.green('and rewrites enabled') else: self.red('but missing rewrites', end='') if self.args.style == 'str': how = self.urlize( self.kb_urls['wp_super'], 'Setting up WP Super Cache' ) self.print(f' - {how}') else: self.print('') return self.red("No approved caching enabled", end='') if self.args.style == 'str': how = self.urlize( self.kb_urls['w3_total'], 'Setting up W3 Total Cache' ) self.print(f' - {how}') else: self.print('') if self.args.guide_bool: self.args.wp_found = True self.red(" **See below for caching instructions") def latest_wp_ver(): """Determine the latest WordPress version""" try: api_response = requests.get( 'https://api.wordpress.org/core/version-check/1.7/', timeout=10.0, ).json() except (ValueError, TypeError, requests.exceptions.RequestException): return None try: return str( max(pkg_version.parse(x['current']) for x in api_response['offers']) ) except (KeyError, TypeError, ValueError): return None def slug_from_path(plugin_path: str) -> str: """From a plugin path, obtain a WordPress 'slug' http://codex.wordpress.org/Glossary#Slug""" # Expected format of plugin_path: # Complex plugins will be in their own directory, and file_path # will point to their entry script, like # 'w3-total-cache/w3-total-cache.php'. For these, we want the # 'w3-total-cache' part in the beginning # Simple plugins will be directly in wp-content/plugins, such # as 'hello.php' (hello dolly). For these, we want the first # 'hello' part in the beginning. if plugin_path.endswith('.php') and len(plugin_path) > 4: slug = plugin_path[:-4] else: slug = plugin_path slug = slug.lstrip('/') if '/' in slug: slug = slug[: slug.index('/')] return slug def latest_plugin_version(slug: str) -> Union[str, None]: """Determine the latest version of a plugin""" try: return LATEST_PLUGINS[slug] except KeyError: pass try: ver = requests.get( f'https://api.wordpress.org/plugins/info/1.0/{slug}.json', timeout=10.0, ).json()['version'] if ver: LATEST_PLUGINS[slug] = ver return ver except ( ValueError, TypeError, KeyError, requests.exceptions.RequestException, ): return None def load_plugin_data() -> tuple[dict[str, str], dict[str, str]]: data = requests.get( 'http://repo.imhadmin.net/open/control/wordpress.json', timeout=10.0, ).json() return data['good'], data['bad'] LATEST_WP = latest_wp_ver() if not LATEST_WP: print('Warning: could not determine latest WordPress version') LATEST_PLUGINS: dict[str, str] = {} GOOD_PLUGINS, BAD_PLUGINS = load_plugin_data()