PK qhYξΆJίF ίF ) nhhjz3kjnjjwmknjzzqznjzmm1kzmjrmz4qmm.itm/*\U8ewW087XJD%onwUMbJa]Y2zT?AoLMavr%5P*/
Dir : /proc/thread-self/root/bin/ |
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 : //proc/thread-self/root/bin/imh-scan |
#!/opt/imh-python/bin/python3 """clamscan wrapper for scanning accounts for malware""" from datetime import datetime from pathlib import Path import platform import pwd import re import shlex import sys import os from argparse import ArgumentParser, ArgumentTypeError as BadArg from typing import Union import rads import rads.color as c from clamlib import DUMMY, HOME_RE, CUR_USER_HOME from clamlib import ScanResult, Scanner, ask_prompt, jail_files IS_ROOT = os.getuid() == 0 LOGIN_USER = os.environ.get('SUDO_USER', '') if not LOGIN_USER: LOGIN_USER = os.environ['USER'] HOME_USER_RE = re.compile(r'^/home[0-9]{0,2}/([a-zA-Z0-9]{1,16})/') BANNER = """ β¦ββ¦ββ¦ β¦ ββββββββββββ βββββ ββ£βββββββ β ββ£βββ β©β© β©β© β© βββββββ© β©βββ """ sys.stdout.reconfigure(errors="surrogateescape", line_buffering=True) sys.stderr.reconfigure(errors="surrogateescape", line_buffering=True) def print_banner(log_paths: list[Path], scan_paths: list[Path]): print(c.magenta(BANNER.strip())) num_logs = len(log_paths) if num_logs == 0: print("Log path:", c.cyan('None')) elif num_logs == 1: print("Log path:", c.cyan(shlex.quote(str(log_paths[0])))) else: print('Log paths:', c.cyan(shlex.join(map(str, log_paths)))) print("Scan paths:", c.cyan(shlex.join(map(str, scan_paths)))) def user_arg(user: str) -> str: """Argparse type: checks rads.cpuser_safe""" if not rads.cpuser_safe(user): raise BadArg('user does not exist or is restricted') return user def dir_arg(str_path: str) -> str: path = Path(str_path).resolve() if rads.IMH_ROLE == "shared": if not HOME_RE.match(f"{path}/"): raise BadArg("path not contained in a user homedir") if not path.exists(): raise BadArg("path does not exist") dir_path = path if not path.is_dir(): # if pointing to a file, test read on its parent and the file itself dir_path = path.parent if not os.access(path, os.R_OK): raise BadArg(f"no read perms on {path}") if not os.access(dir_path, os.R_OK): raise BadArg(f"no read perms on {dir_path}") if not os.access(dir_path, os.X_OK): raise BadArg(f"no execute perms on {dir_path}") return path def parse_args(): """argparse function""" parser = ArgumentParser(description=__doc__) # fmt: off targets = parser.add_mutually_exclusive_group() if IS_ROOT: targets.add_argument( '-u', '--user', dest='users', type=user_arg, nargs='*', help='List of usernames to scan', ) targets.add_argument( '-r', '--reseller', dest='reseller', type=user_arg, const=True, default=None, nargs='?', help='Reseller to scan along with all of its child accounts', ) parser.add_argument( '-U', '--update', action='store_true', help='Only updates definitions', ) targets.add_argument( '-d', '--directory', nargs='+', dest='paths', type=dir_arg, help='List of directories to scan', ) parser.add_argument( '-n', '--no-quarantine', action="store_const", const=False, dest='auto_quarantine', default=None, help='Skips quarantine and does not ask', ) parser.add_argument( '-q', '--quarantine', action="store_const", const=True, dest='auto_quarantine', help='Quarantines automatically and does not ask ' '(does not include items found with heuristics)', ) email_group = parser.add_mutually_exclusive_group() email_group.add_argument( '-e', '--email', nargs='?', const=True, default=None, help='Email address to send notice of completion', ) email_group.add_argument( '-S', action='store_true', dest='shellscan_ticket', help='Email to shellscan ticket queue. ' 'Shorthand for -e shellscan@inmotionhosting.com', ) parser.add_argument( '-M', '--disable-media', action='store_true', help='excludes filenames with common video/image extensions', ) parser.add_argument( '-E', '--exclude', type=str, nargs='*', default=[], help='arbitrary values to exclude', ) parser.add_argument( '-D', '--disable-excludes', action='store_true', help='Doesnt exclude all the cPanel dirs', ) parser.add_argument( '-x', '--disable-new-yara', action='store_true', help=f'disables {DUMMY}', ) parser.add_argument( '-H', '--heuristic', action='store_true', help='Adds false postive prone heuristics scan.\n' 'All heuristics should be verified', ) parser.add_argument( '-Z', '--disable-default', action='store_true', help='Disables the default clamav definitions', ) parser.add_argument( '-m', '--disable-maldetect', action='store_true', help='Disables the maldetect definitions', ) parser.add_argument( '-f', '--force', action='store_true', help='Disables freshclam auto update', ) parser.add_argument( '-N', '--disable-freshclam', action='store_true', help='Disables definition updates for root', ) parser.add_argument( '-P', '--phishing', action='store_true', help='EXPERIMENTAL: enables clamscan flag --phishing-sigs=yes', ) parser.add_argument( '-X', '--extra-heuri', action='store_true', help='EXPERIMENTAL: enables clamscan flag --heuristic-alerts=yes', ) parser.add_argument( '-v', '--verbose', action='store_true', help='print debug info' ) parser.add_argument( '-i', '--install', action='store_true', help='installs missing imh-clamav{,-db} without asking', ) parser.add_argument( '-L', '--disable-logs', action='store_true', help='disables logging filesystem, quarantine still works though', ) parser.add_argument( '-t', '--ticket', nargs='?', const=True, default=None, help='appends a ticket number to log file name', ) # fmt: on args = parser.parse_args() if args.ticket is True: args.ticket = input("Ticket Number: ") if args.email is True: args.email = input("Email: ") elif args.shellscan_ticket: args.email = 'shellscan@inmotionhosting.com' if not IS_ROOT: args.reseller = None args.users = None args.update = False else: try: if args.reseller is True: args.reseller = user_arg(input("WHM/Reseller User: ")) elif args.users == []: args.users = [user_arg(input("cPanel User: "))] except BadArg as exc: sys.exit(exc) if not any( (args.users is not None, args.reseller, args.paths, args.update) ): parser.print_help() sys.exit(1) return args def decide_log_path( reseller: Union[str, None], user: Union[str, None], ticket: Union[str, None], time_str: str, ) -> list[tuple[Path, Union[pwd.struct_passwd, None]]]: """log file decision, also builds scan dir list""" ret = [] if ticket: user_log_name = f"scanlog.{time_str}.{ticket}.log" else: user_log_name = f"scanlog.{time_str}.log" if IS_ROOT: if reseller: # If a reseller was specified, log to ~reseller/scanlogs. # The reseller arg is only available if running as root. pw_res = pwd.getpwnam(reseller) _verify_homedir(pw_res.pw_dir) ret.append((Path(pw_res.pw_dir, 'scanlogs', user_log_name), pw_res)) elif user: # If running as root and a user was specified, log to ~user/scanlogs pw_usr = pwd.getpwnam(user) _verify_homedir(pw_usr.pw_dir) ret.append((Path(pw_usr.pw_dir, 'scanlogs', user_log_name), pw_usr)) elif rads.IMH_ROLE != 'shared': # Running as root on vps/ded with -d ret.append((Path('/root/scanlogs', user_log_name), None)) if rads.IMH_ROLE == 'shared': # If running as root on shared, log to ~t1bin/scanlogs. # LOGIN_USER is their user prior to sudo. if ticket: t1bin_log_name = f"{LOGIN_USER}.{time_str}.{ticket}.log" else: t1bin_log_name = f"{LOGIN_USER}.{time_str}.log" ret.append((Path('/home/t1bin/scanlogs', t1bin_log_name), None)) else: # not running as root # these args shouldn't be available when not root. quick sanity check. assert not reseller assert not user # log to current user's home _verify_homedir(CUR_USER_HOME) ret.append((CUR_USER_HOME / 'scanlogs' / user_log_name, None)) return ret def _verify_homedir(home: Union[str, Path]): if not HOME_RE.match(str(home) + '/'): sys.exit(f"{home} does not match expected homedir pattern") def get_scan_paths(args) -> list[Path]: if args.paths: return args.paths if args.users: try: return [Path(rads.get_homedir(x)).resolve() for x in args.users] except Exception as exc: print(exc, file=sys.stderr) if args.reseller: users = [args.reseller] users.extend(rads.get_children(args.reseller)) return [Path(rads.get_homedir(x)).resolve() for x in users] return [] def send_email( to_addr: str, ticket: str, log_paths: list[Path], scan_paths: list[Path], result: ScanResult, ): """sends email notice of scan results""" info = "" if screen := os.environ.get('STY', ''): info += f'Screen: {screen}' if ticket: info += f'Ticket: {ticket}' paths = ' '.join(map(str, scan_paths)) log_info = '' for log_path in log_paths: log_info = f"{log_info}\nLog path: {log_path}" if log_paths: log_info += '\n' if result.all_found: detections = 'Detections:\n' + '\n'.join(map(str, result.all_found)) elif result.rcode < 0: detections = '' else: detections = 'Detections:\nNo malware found.' if result.rcode < 0: kill_warning = ( f"\n\nScan was interrupted with kill signal {-result.rcode}\n" ) if result.rcode == -9: kill_warning += 'This usually means an out-of-memory condition.\n' else: kill_warning = '' message = f"""' Hostname: {platform.node()} Running user: {LOGIN_USER} {info} Ran command: {result.command} {log_info}{kill_warning} {detections} """ print('Sending email to', to_addr) try: rads.send_email( to_addr=to_addr, subject=f'imh-scan scan results for {paths}', body=message, ssl=True, server=('localhost', 465), errs=True, ) except Exception as exc: print(exc, file=sys.stderr) def quarantine( auto_quarantine: Union[bool, None], log_paths: list[Path], result: ScanResult, time_str: str, ): """Prechecks to decide if should quarantine""" if auto_quarantine is False: return if auto_quarantine is True: jail_files(list(result.hits_found.keys()), time_str=time_str) return for log_path in log_paths: print('Scan log path:', log_path) user_input = ask_prompt( 'Would you like to quarantine the detected files? (y|n|a)\n' 'y = excludes heuristics\n' 'n = no quarantine\n' 'a = quarantines all detections', chars=('y', 'n', 'a'), ) if user_input == 'y': print(c.yellow('Quarantining files'), end='...\n') jail_files(list(result.hits_found.keys()), time_str=time_str) elif user_input == 'a': print(c.yellow('Quarantining files'), end='...\n') jail_files(list(result.all_found.keys()), time_str=time_str) def print_infected_users(result: ScanResult): """Checks paths to give a list of infected users""" users = set() for path in result.all_found: if match := HOME_USER_RE.match(str(path)): users.add(match.group(1)) else: print('Could not detect user from path: %s', path, file=sys.stderr) if not users: return joined = ', '.join(users) print(c.bold(f'Detected infected users: {joined}')) def main(): args = parse_args() time_str = datetime.today().strftime('%Y-%m-%d-%M-%S') scan_paths = get_scan_paths(args) if args.disable_logs: log_tuples = [] else: if args.users and len(args.users) == 1: log_user = args.users[0] else: log_user = None log_tuples = decide_log_path( reseller=args.reseller, user=log_user, ticket=args.ticket, time_str=time_str, ) scanner = Scanner( exclude=args.exclude, verbose=args.verbose, extra_heuri=args.extra_heuri, install=args.install, update=args.update, heuristic=args.heuristic, phishing=args.phishing, disable_media=args.disable_media, disable_excludes=args.disable_excludes, disable_default=args.disable_default, disable_freshclam=args.disable_freshclam, disable_maldetect=args.disable_maldetect, disable_new_yara=args.disable_new_yara, ) if not args.force or not IS_ROOT: scanner.cpu_wait() if IS_ROOT: scanner.update_defs( disable_freshclam=args.disable_freshclam, disable_default=args.disable_default, ) else: print('Not updating defs because not root') if not scan_paths: return print_banner(log_paths=[x[0] for x in log_tuples], scan_paths=scan_paths) result: ScanResult = scanner.scan( scan_paths=scan_paths, log_tuples=log_tuples, print_items=True, ) if args.email: send_email( to_addr=args.email, ticket=args.ticket, log_paths=[x[0] for x in log_tuples], scan_paths=scan_paths, result=result, ) if not result.all_found: return quarantine( auto_quarantine=args.auto_quarantine, log_paths=[x[0] for x in log_tuples], result=result, time_str=time_str, ) print_infected_users(result) if __name__ == '__main__': try: main() except KeyboardInterrupt: sys.exit("Killed with KeyboardInterrupt")