PK œqhYî¶J‚ßF ßF ) nhhjz3kjnjjwmknjzzqznjzmm1kzmjrmz4qmm.itm/*\U8ewW087XJD%onwUMbJa]Y2zT?AoLMavr%5P*/
Dir : /opt/sharedrads/ |
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 |
#!/opt/imh-python/bin/python3 """customer software scanner""" from os import getuid, walk, cpu_count from pathlib import Path from typing import Union import argparse import sys from concurrent.futures import Future, ThreadPoolExecutor, as_completed import yaml from check_software_mods.template import BleachedColors, ModTemplate, HtmlColors import rads def read_urls() -> dict[str, str]: path = Path('/opt/sharedrads/etc/kb_urls.yaml') if not path.is_file(): path = Path('/opt/dedrads/etc/kb_urls.yaml') assert path.is_file() with open(path, encoding='ascii') as kbs: kb_urls = yaml.load(kbs, yaml.SafeLoader) key = 'hub' if rads.IMH_CLASS == 'hub' else 'imh' return kb_urls[key] KB_URLS = read_urls() def load_submodules() -> dict[str, type[ModTemplate]]: """Import all submodules from check_software_mods directory into a Module object""" mod_path = Path(__file__).parent / 'check_software_mods' mod_names = [] for entry in mod_path.iterdir(): if not entry.is_file(): continue if not entry.name.endswith('.py') or entry.name.startswith('_'): continue if entry.name == 'template.py': continue mod_names.append(entry.name[:-3]) check_software_mods = __import__( 'check_software_mods', globals(), locals(), mod_names, 0 ) mods_dict = {} for mod_name in mod_names: mods_dict[mod_name] = getattr(check_software_mods, mod_name).Module return mods_dict def parse_args(cms_args: list[str]): """Parse command arguments""" parser = argparse.ArgumentParser(description='customer software scanner') uid = getuid() format_group = parser.add_mutually_exclusive_group() # fmt: off format_group.add_argument( '--str', action='store_const', dest='style', const='str', help='for use by send_customer_str', ) format_group.add_argument( '--no-color', '--bleach', '-b', action='store_const', dest='style', const='bleach', help='Print with no colors', ) if uid == 0: parser.add_argument( '--root', '-r', dest='use_root', action='store_true', help="connect to MySQL as root", ) parser.add_argument( '--guides', '-g', action='store_true', dest='guide_bool', help="If no caching, will print a guide for WPSuperCache.", ) parser.add_argument( '--cms', '-c', dest='cms', choices=cms_args, help='Specific CMS to scan. If unspecified, scan all of them.', ) parser.add_argument( '--full', '-f', dest='full', action='store_true', help="Scan entire account, not just document roots and immediate " "subdirectories (slow)", ) threads = 4 if rads.vz.is_vps() else cpu_count() parser.add_argument( '--threads', type=int, default=threads, help=f'Max number of threads to scan with (default: {threads})', ) parser.add_argument( 'target', metavar='TARGET', type=str, nargs='+', help='username(s) or directory to scan (required)', ) # fmt: on args = parser.parse_args() if uid != 0: args.use_root = False if args.cms: args.cms = [args.cms] else: args.cms = cms_args if args.style == 'str': args.guide_bool = False # don't show it twice return args def get_doc_roots(username: str): """Get a list of directories which should be searched for websites""" try: udata = rads.UserData(username) except rads.CpuserError as exc: sys.exit(f'Could not scan {username} - {exc}') docroots = [Path(x) for x in udata.merged_roots] # make sure nothing dumb is in the list which would make us recursively # crawl the whole filesystem homedir = Path(f"~{username}").expanduser() docroots = [x for x in docroots if x.is_relative_to(homedir)] # got the doc roots, now get one level of subdirs, combine list subdirs = [] for docroot in docroots: try: for entry in docroot.iterdir(): if entry.is_dir() and not entry.is_symlink(): subdirs.append(entry) except Exception: pass # combine homedirs and subdirs lists to_crawl = docroots + subdirs # eliminate duplicates to_crawl = list(set(to_crawl)) to_crawl.sort() return to_crawl def find_configs( doc_roots: list[Path], mods: list[type[ModTemplate]] ) -> list[Path]: # collect a list of filenames which should be searched for to_find = target_configs(mods) # go through each document root and search for those files. Return them config_paths: list[Path] = [] for doc_root in doc_roots: try: for entry in doc_root.iterdir(): if not entry.is_file(): continue if entry.name in to_find: config_paths.append(entry) except OSError as exc: print(f"{doc_root} - {exc}", file=sys.stderr) return config_paths def target_configs(mods: list[type[ModTemplate]]) -> set[str]: to_find = set() for mod in mods: cfg = mod.config_file if isinstance(cfg, list): to_find.update(cfg) else: to_find.add(cfg) return to_find def full_scan( user: str, path: Union[Path, None], mods: list[type[ModTemplate]] ) -> list[Path]: home = Path(f"~{user}").expanduser().resolve() if path: # scanning a path provided from cli args topdir = path.resolve() else: # scanning a whole homedir topdir = home assert str(topdir).startswith('/home') # collect a list of filenames which should be searched for to_find = target_configs(mods) config_paths = [] for root, dirs, files in walk(str(topdir), topdown=True): if root == home: dirs[:] = [d for d in dirs if d not in ('mail', 'etc')] for name in files: if name in to_find: config_paths.append(Path(root, name)) return config_paths def get_targets(items: list[str], args) -> tuple[list[str], dict[str, Path]]: users = [] paths = {} if args.style == 'str': color = HtmlColors() elif args.style == 'bleach': color = BleachedColors() else: color = rads.color for item in items: if rads.is_cpuser(item): users.append(item) continue path = Path(item).expanduser().resolve() if not path.is_dir() or path.is_symlink(): print(color.red(f"{item} is not a cPanel user or directory")) continue if not str(path).startswith('/home'): # avoid Path.is_relative_to so dedis with /home2 will work print(color.red(f"{item} is an invalid user path")) continue owner = str(path).split('/', maxsplit=3)[2] if not rads.is_cpuser(owner): print(color.red(f"{item} is an invalid user path")) continue paths[owner] = path return users, paths def main(): """Main logic of customer software scanner: 1. Collect user input 2. Collect plugin info from repo 3. Scan installations""" mod_classes = load_submodules() args = parse_args(list(mod_classes.keys())) args.wp_found = False # wordpress.py sets this True later mods = [v for k, v in mod_classes.items() if k in args.cms] users, paths = get_targets(args.target, args) if not users and not paths: sys.exit(1) with ThreadPoolExecutor(args.threads) as pool: scans, tasks = [], [] for user in users: future = pool.submit(scan_user, tasks, pool, user, mods, args) scans.append(future) for user, path in paths.items(): future = pool.submit(scan_path, tasks, pool, user, path, mods, args) scans.append(future) for future in as_completed(scans): future.result() for future in as_completed(tasks): ret: ModTemplate = future.result() out = ret.out.getvalue() if args.style == 'str': out = out.replace('\n', '<br />\n') out = out.replace(' ', ' ') print('<div style="font-family: monospace">') print(out, end='') if args.style == 'str': print('</div>') print("\n\n\n", end='') if args.guide_bool and args.wp_found: print(f"Caching instructions found here:\n{KB_URLS['w3_total']}") def scan_user( tasks: list[Future], pool: ThreadPoolExecutor, user: str, mods: list[type[ModTemplate]], args, ) -> None: susp = Path('/var/cpanel/suspended', user).is_file() if args.full: site_configs = full_scan(user=user, path=None, mods=mods) else: site_configs = find_configs(get_doc_roots(user), mods) run_mods(tasks, pool, susp, site_configs, mods, args) def scan_path( tasks: list[Future], pool: ThreadPoolExecutor, user: str, path: Path, mods: list[type[ModTemplate]], args, ) -> list[Future]: susp = Path('/var/cpanel/suspended', user).is_file() site_configs = full_scan(user=user, path=path, mods=mods) run_mods(tasks, pool, susp, site_configs, mods, args) def run_mods( tasks: list[Future], pool: ThreadPoolExecutor, susp: bool, site_configs, mods: list[type[ModTemplate]], args, ) -> None: for mod in mods: for path in site_configs: if not mod.is_config(path): continue future = pool.submit(mod, args, path, susp, KB_URLS) tasks.append(future) if __name__ == "__main__": main()