def __init__(self, conf): # (Re)init logger with config init_log(verbose=conf['verbose'], quiet=conf['quiet'], logfile=conf['log_file']) self.delete_tmp_wpscan_files() # Init DB interface self.wp_reports = WPWatcherDataBase(conf['wp_reports']) # Dump config conf.update({'wp_reports': self.wp_reports.filepath}) log.debug("WPWatcher configuration:{}".format(self.dump_config(conf))) # Init scanner self.scanner = WPWatcherScanner(conf) # Save sites self.wp_sites = conf['wp_sites'] # Asynchronous executor self.executor = concurrent.futures.ThreadPoolExecutor( max_workers=conf['asynch_workers']) # List of conccurent futures self.futures = [] # Register the signals to be caught ^C , SIGTERM (kill) , service restart , will trigger interrupt() signal.signal(signal.SIGINT, self.interrupt) signal.signal(signal.SIGTERM, self.interrupt) #new reports self.new_reports = []
def wprs(filepath=None, daemon=False): """Generate JSON file database summary""" db = WPWatcherDataBase(filepath, daemon=daemon) sys.stdout.buffer.write( WPWatcher.results_summary(db._data).encode("utf8")) sys.stdout.flush() exit(0)
def show(urlpart, filepath=None, daemon=False): """Inspect a report in database""" db = WPWatcherDataBase(filepath, daemon=daemon) matching_reports = [r for r in db._data if urlpart in r["site"]] eq_reports = [r for r in db._data if urlpart == r["site"]] if len(eq_reports): sys.stdout.buffer.write( format_results(eq_reports[0], format="cli").encode("utf8")) elif len(matching_reports) == 1: sys.stdout.buffer.write( format_results(matching_reports[0], format="cli").encode("utf8")) elif len(matching_reports) > 1: sys.stdout.buffer.write( "The following sites match your search: \n".encode("utf8")) sys.stdout.buffer.write( WPWatcher.results_summary(matching_reports).encode("utf8")) sys.stdout.buffer.write( "\nPlease be more specific. \n".encode("utf8")) else: sys.stdout.buffer.write("No report found".encode("utf8")) exit(1) exit(0)
def test_wp_reports_read_write(self): SPECIFIC_WP_REPORTS_FILE_CONFIG = DEFAULT_CONFIG + "\nwp_reports=%s" # Compare with config and no config db = WPWatcherDataBase() paths_found = db.find_wp_reports_file() db2 = WPWatcherDataBase( WPWatcherConfig(string=SPECIFIC_WP_REPORTS_FILE_CONFIG % (paths_found)).build_config()[0]['wp_reports']) self.assertEqual( db._data, db2._data, "WP reports database are different even if files are the same") # Test Reports database reports = [{ "site": "exemple.com", "status": "WARNING", "datetime": "2020-04-08T16-05-16", "last_email": None, "error": '', "infos": ["[+]", "blablabla"], "warnings": [ "[+] WordPress version 5.2.2 identified (Insecure, released on 2019-06-18).\n| Found By: Emoji Settings (Passive Detection)\n", "[!] No WPVulnDB API Token given, as a result vulnerability data has not been output.\n[!] You can get a free API token with 50 daily requests by registering at https://wpvulndb.com/users/sign_up" ], "alerts": [], "fixed": [] }, { "site": "exemple2.com", "status": "INFO", "datetime": "2020-04-08T16-05-16", "last_email": None, "error": '', "infos": ["[+]", "blablabla"], "warnings": [], "alerts": [], "fixed": [] }] db = WPWatcherDataBase() db.update_and_write_wp_reports(reports) # Test internal _data gets updated after update_and_write_wp_reports() method for r in reports: self.assertIn( r, db._data, "The report do not seem to have been saved into WPWatcher.wp_report list" ) # Test write method wrote_db = db.build_wp_reports(db.filepath) with open(db.filepath, 'r') as dbf: wrote_db_alt = json.load(dbf) for r in reports: self.assertIn( r, wrote_db, "The report do not seem to have been saved into db file") self.assertIn( r, wrote_db_alt, "The report do not seem to have been saved into db file (directly read with json.load)" ) self.assertEqual( db._data, wrote_db_alt, "The database file wrote (directly read with json.load) differ from in memory database" ) self.assertEqual( db._data, wrote_db, "The database file wrote differ from in memory database")
class WPWatcher(): '''WPWacther object Arguments: - `conf`: the configuration dict. Required Usage exemple: from wpwatcher.config import WPWatcherConfig from wpwatcher.core import WPWatcher config, files = WPWatcherConfig().build_config() config.update({ 'send_infos': True, 'wp_sites': [ {'url':'exemple1.com'}, {'url':'exemple2.com'} ], 'wpscan_args': ['--format', 'json', '--stealthy'] }) w=WPWatcher(config) exit_code, reports = w.run_scans_and_notify() for r in reports: print("%s\t\t%s"%( r['site'], r['status'] )) ''' # WPWatcher must use a configuration dict def __init__(self, conf): # (Re)init logger with config init_log(verbose=conf['verbose'], quiet=conf['quiet'], logfile=conf['log_file']) self.delete_tmp_wpscan_files() # Init DB interface self.wp_reports = WPWatcherDataBase(conf['wp_reports']) # Update config before passing it to WPWatcherScanner conf.update({'wp_reports': self.wp_reports.filepath}) # Init scanner self.scanner = WPWatcherScanner(conf) # Dump config log.debug("WPWatcher configuration:{}".format(self.dump_config(conf))) # Save sites self.wp_sites = conf['wp_sites'] # Asynchronous executor self.executor = concurrent.futures.ThreadPoolExecutor( max_workers=conf['asynch_workers']) # List of conccurent futures self.futures = [] # Register the signals to be caught ^C , SIGTERM (kill) , service restart , will trigger interrupt() signal.signal(signal.SIGINT, self.interrupt) signal.signal(signal.SIGTERM, self.interrupt) #new reports self.new_reports = [] @staticmethod def delete_tmp_wpscan_files(): '''Delete temp wpcan files''' # Try delete temp files. if os.path.isdir('/tmp/wpscan'): try: shutil.rmtree('/tmp/wpscan') log.info("Deleted temp WPScan files in /tmp/wpscan/") except (FileNotFoundError, OSError, Exception): log.info( "Could not delete temp WPScan files in /tmp/wpscan/\n%s" % (traceback.format_exc())) @staticmethod def dump_config(conf): '''Print the config without passwords''' dump_conf = copy.deepcopy(conf) string = '' for k in dump_conf: v = dump_conf[k] if k == 'wpscan_args': v = safe_log_wpscan_args(v) if k == 'smtp_pass' and v != "": v = '***' if isinstance(v, (list, dict)): v = json.dumps(v) else: v = str(v) string += ("\n{:<25}\t=\t{}".format(k, v)) return (string) def cancel_pending_futures(self): '''Cancel all asynchronous jobs''' for f in self.futures: if not f.done(): f.cancel() def interrupt(self, sig=None, frame=None): '''Interrupt sequence''' log.error("Interrupting...") # If called inside ThreadPoolExecutor, raise Exeception if not isinstance(threading.current_thread(), threading._MainThread): raise InterruptedError() # Cancel all scans self.cancel_pending_futures() # future scans # Wait all scans finished self.scanner.cancel_scans() # running scans # Give a 5 seconds timeout to buggy WPScan jobs to finish or ignore them try: timeout(5, self.executor.shutdown, kwargs=dict(wait=True)) except TimeoutError: pass # Recover reports from futures results new_reports = [] for f in self.futures: if f.done(): try: new_reports.append(f.result()) except Exception: pass # Display results and quit self.print_scanned_sites_results(new_reports) log.info("Scans interrupted.") sys.exit(-1) def print_scanned_sites_results(self, new_reports): '''Print the result summary for the scanned sites''' new_reports = [n for n in new_reports if n] if len(new_reports) > 0: log.info(self.results_summary(new_reports)) if self.wp_reports.filepath != "null": log.info("Updated %s reports in database: %s" % (len(new_reports), self.wp_reports.filepath)) else: log.info("No reports updated in local database") @staticmethod def results_summary(results): '''Print the summary table of all sites. Columns : "Site", "Status", "Last scan", "Last email", "Issues", "Problematic component(s)" ''' string = 'Results summary\n' header = ("Site", "Status", "Last scan", "Last email", "Issues", "Problematic component(s)") sites_w = 20 # Determine the longest width for site column for r in results: sites_w = len( r['site']) + 4 if r and len(r['site']) > sites_w else sites_w frow = "{:<%d} {:<8} {:<20} {:<20} {:<8} {}" % sites_w string += frow.format(*header) for row in results: pb_components = [] for m in row['alerts'] + row['warnings']: pb_components.append(m.splitlines()[0]) if row['error']: pb_components.append("Failure") string += '\n' string += frow.format(str(row['site']), str(row['status']), str(row['datetime']), str(row['last_email']), len(row['alerts'] + row['warnings']), ', '.join(pb_components)) return string @staticmethod def format_site(wp_site): '''Make sure the site structure is correct, parse 'url', init optionals 'email_to','false_positive_strings','wpscan_args' to empty list if not present. Raise ValueError if url key is not present''' if 'url' not in wp_site: raise ValueError("Invalid site %s\nMust contain 'url' key" % wp_site) else: # Strip URL string wp_site['url'] = wp_site['url'].strip() # Format sites with scheme indication p_url = list(urlparse(wp_site['url'])) if p_url[0] == "": wp_site['url'] = 'http://' + wp_site['url'] # Read the wp_site dict and assing default values if needed optionals = ['email_to', 'false_positive_strings', 'wpscan_args'] for op in optionals: if op not in wp_site or wp_site[op] is None: wp_site[op] = [] return wp_site def scan_site_wrapper(self, wp_site): """Helper method to wrap the raw scanning process that offer WPWatcherScanner.scan_site() and add the following: - Handle site structure formatting - Find the last report in the database and launch the scan - Write it in DB after scan. - Print progress bar This function will be called asynchronously. Return one report""" wp_site = self.format_site(wp_site) last_wp_report = self.wp_reports.find_last_wp_report( {'site': wp_site['url']}) # Launch scanner wp_report = self.scanner.scan_site(wp_site, last_wp_report) # Save report in global instance database and to file when a site has been scanned if wp_report: self.wp_reports.update_and_write_wp_reports([wp_report]) else: log.info("No report saved for site %s" % wp_site['url']) # Print progress print_progress_bar(len(self.scanner.scanned_sites), len(self.wp_sites)) return (wp_report) def run_scans_wrapper(self, wp_sites): """Helper method to deal with : - executor, concurent futures - Trigger self.interrupt() on InterruptedError (raised if fail fast enabled) Pass `kwargs` arguments to scan_site_wrapper() """ log.info("Starting scans on %s configured sites" % (len(wp_sites))) for wp_site in wp_sites: self.futures.append( self.executor.submit(self.scan_site_wrapper, wp_site)) for f in self.futures: try: self.new_reports.append(f.result()) # Handle interruption from inside threads when using --ff except InterruptedError: self.interrupt() except concurrent.futures.CancelledError: pass # Ensure everything is down self.cancel_pending_futures() return self.new_reports def run_scans_and_notify(self): """ Run WPScan on defined websites and send notifications. Returns a `tuple (exit code, reports)`""" # Check sites are in the config if len(self.wp_sites) == 0: log.error( "No sites configured, please provide wp_sites in config file or use arguments --url URL [URL...] or --urls File path" ) return ((-1, [])) new_reports = self.run_scans_wrapper(self.wp_sites) # Print results and finish self.print_scanned_sites_results(new_reports) if not any([r['status'] == 'ERROR' for r in new_reports if r]): log.info("Scans finished successfully.") return ((0, new_reports)) else: log.info("Scans finished with errors.") return ((-1, new_reports))
class WPWatcher(): # WPWatcher must use a configuration dict def __init__(self, conf): # (Re)init logger with config init_log(verbose=conf['verbose'], quiet=conf['quiet'], logfile=conf['log_file']) self.delete_tmp_wpscan_files() # Init DB interface self.wp_reports = WPWatcherDataBase(conf['wp_reports']) # Dump config conf.update({'wp_reports': self.wp_reports.filepath}) log.debug("WPWatcher configuration:{}".format(self.dump_config(conf))) # Init scanner self.scanner = WPWatcherScanner(conf) # Save sites self.wp_sites = conf['wp_sites'] # Asynchronous executor self.executor = concurrent.futures.ThreadPoolExecutor( max_workers=conf['asynch_workers']) # List of conccurent futures self.futures = [] # Register the signals to be caught ^C , SIGTERM (kill) , service restart , will trigger interrupt() signal.signal(signal.SIGINT, self.interrupt) signal.signal(signal.SIGTERM, self.interrupt) #new reports self.new_reports = [] @staticmethod def delete_tmp_wpscan_files(): # Try delete temp files. if os.path.isdir('/tmp/wpscan'): try: shutil.rmtree('/tmp/wpscan') log.info("Deleted temp WPScan files in /tmp/wpscan/") except (FileNotFoundError, OSError, Exception): log.info( "Could not delete temp WPScan files in /tmp/wpscan/\n%s" % (traceback.format_exc())) @staticmethod def dump_config(conf): dump_conf = copy.deepcopy(conf) string = '' for k in dump_conf: v = dump_conf[k] if k == 'wpscan_args': v = safe_log_wpscan_args(v) if k == 'smtp_pass' and v != "": v = '***' if isinstance(v, (list, dict)): v = json.dumps(v) else: v = str(v) string += ("\n{:<25}\t=\t{}".format(k, v)) return (string) # def wait_all_wpscan_process(self): # while len(self.scanner.wpscan.processes)>0: # time.sleep(0.05) def tear_down_jobs(self): # Cancel all jobs for f in self.futures: if not f.done(): f.cancel() def interrupt(self, sig=None, frame=None): # Lock for interrupting log.error("Interrupting...") # If called inside ThreadPoolExecutor, raise Exeception if not isinstance(threading.current_thread(), threading._MainThread): raise InterruptedError() # Cancel all scans self.scanner.cancel_scans() # Wait all scans finished, print results and quit self.tear_down_jobs() # Give a 5 seconds timeout to buggy WPScan jobs to finish or ignore them try: timeout(5, self.executor.shutdown, kwargs=dict(wait=True)) except TimeoutError: pass new_reports = [] for f in self.futures: if f.done(): try: new_reports.append(f.result()) except Exception: pass self.print_scanned_sites_results(new_reports) log.info("Scans interrupted.") exit(-1) def print_scanned_sites_results(self, new_reports): new_reports = [n for n in new_reports if n] if len(new_reports) > 0: log.info(results_summary(new_reports)) if self.wp_reports.filepath != "null": log.info("Updated %s reports in database: %s" % (len(new_reports), self.wp_reports.filepath)) else: log.info("No reports updated in local database") @staticmethod def format_site(wp_site): if 'url' not in wp_site: log.error("Invalid site %s" % wp_site) wp_site = {'url': ''} else: #Strip URL string wp_site['url'] = wp_site['url'].strip() # Format sites with scheme indication p_url = list(urlparse(wp_site['url'])) if p_url[0] == "": wp_site['url'] = 'http://' + wp_site['url'] # Read the wp_site dict and assing default values if needed optionals = ['email_to', 'false_positive_strings', 'wpscan_args'] for op in optionals: if op not in wp_site or wp_site[op] is None: wp_site[op] = [] return wp_site # Orchestrate the scanning of a site def scan_site_wrapper(self, wp_site, with_api_token=False): wp_site = self.format_site(wp_site) last_wp_report = self.wp_reports.find_last_wp_report( {'site': wp_site['url']}) if with_api_token: wp_site['wpscan_args'].extend( ["--api-token", self.scanner.api_token]) # Launch scanner wp_report = self.scanner.scan_site(wp_site, last_wp_report) # Save report in global instance database and to file when a site has been scanned if wp_report: self.wp_reports.update_and_write_wp_reports([wp_report]) else: log.info("No report saved for site %s" % wp_site['url']) # Print progress print_progress_bar(len(self.scanner.scanned_sites), len(self.wp_sites)) return (wp_report) def run_scans_wrapper(self, wp_sites, **kwargs): log.info("Starting scans on %s configured sites" % (len(wp_sites))) for wp_site in wp_sites: self.futures.append( self.executor.submit(self.scan_site_wrapper, wp_site, **kwargs)) for f in self.futures: try: self.new_reports.append(f.result()) # Handle interruption from inside threads when using --ff except InterruptedError: self.interrupt() except concurrent.futures.CancelledError: pass # Ensure everything is down self.tear_down_jobs() return self.new_reports # Run WPScan on defined websites def run_scans_and_notify(self): # Check sites are in the config if len(self.wp_sites) == 0: log.error( "No sites configured, please provide wp_sites in config file or use arguments --url URL [URL...] or --urls File path" ) return ((-1, [])) new_reports = self.run_scans_wrapper(self.wp_sites) # Print results and finish self.print_scanned_sites_results(new_reports) # Second scans if needed if len(self.scanner.prescanned_sites_warn) > 0: new_reports += self.re_run_scans( self.scanner.prescanned_sites_warn) self.print_scanned_sites_results(new_reports) if not any([r['status'] == 'ERROR' for r in new_reports if r]): log.info("Scans finished successfully.") return ((0, new_reports)) else: log.info("Scans finished with errors.") return ((-1, new_reports)) def re_run_scans(self, wp_sites): self.scanner.scanned_sites = [] self.futures = [] self.wp_sites = wp_sites return self.run_scans_wrapper(wp_sites, with_api_token=True)
def wprs(filepath=None, daemon=False): """Generate JSON file database summary""" db = WPWatcherDataBase(filepath, daemon=daemon) print(results_summary(db._data)) exit(0)