def emit_messages(self, wp_report: Dict[str, Any]) -> None: """ Sends the CEF syslog messages for the report. """ log.debug(f"Sending Syslog messages for site {wp_report['site']}") for m in self.get_messages(wp_report): self.syslog.info(m)
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 _wpscan(self, *args): # WPScan arguments cmd = self.wpscan_executable + list(args) # Log wpscan command without api token log.debug("Running WPScan command: %s" % " ".join(safe_log_wpscan_args(cmd))) # Run wpscan process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) # Append process to current process list and launch self.processes.append(process) wpscan_output, stderr = process.communicate() self.processes.remove(process) try: wpscan_output = wpscan_output.decode("utf-8") except UnicodeDecodeError: wpscan_output = wpscan_output.decode("latin1") """ # Error when wpscan failed, except exit code 5: means the target has at least one vulnerability. # See https://github.com/wpscanteam/CMSScanner/blob/master/lib/cms_scanner/exit_code.rb if process.returncode in [0,5]: # WPScan comamnd success log.debug("WPScan raw output:\n"+wpscan_output) # Log error else : err_string, full_err_string=self.get_full_err_string(cmd, process.returncode, wpscan_output, stderr) log.error(err_string) log.debug(full_err_string) """ return (process.returncode, wpscan_output)
def get_messages(self, wp_report: Dict[str, Any]) -> List[str]: """ Return a list of CEF formatted messages """ from cefevent import CEFEvent messages = [] for v in self.EVENTS.keys(): # make sure items is a list, cast error string to list items = wp_report[v] if isinstance(wp_report[v], list) else [wp_report[v]] for msg_data in items: if msg_data: log.debug(f"Message data: {msg_data}") c = CEFEvent() # WPWatcher related fields c.set_prefix("deviceVendor", self.DEVICE_VENDOR) c.set_prefix("deviceProduct", self.DEVICE_PRODUCT) c.set_prefix("deviceVersion", self.DEVICE_VERSION) # Message common fields c.set_prefix("signatureId", self.EVENTS[v][0]) c.set_prefix("name", self.EVENTS[v][1]) c.set_prefix("severity", self.EVENTS[v][2]) # Message supp infos c.set_field("message", msg_data[:1022]) c.set_field("sourceHostName", wp_report["site"][:1022]) msg = c.build_cef() log.debug(f"Message CEF: {msg}") messages.append(msg) return messages
def get_messages(self, wp_report): """ Return a list of CEF formatted messages """ from cefevent import CEFEvent messages = [] for v in self.EVENTS.keys(): # make sure items is a list, cast error string to list items = wp_report[v] if isinstance(wp_report[v], list) else [wp_report[v]] for msg_data in items: if msg_data: log.debug("Message data: {}".format(msg_data)) c = CEFEvent() # WPWatcher related fields c.set_prefix('deviceVendor', self.DEVICE_VENDOR) c.set_prefix('deviceProduct', self.DEVICE_PRODUCT) c.set_prefix('deviceVersion', VERSION) # Message common fields c.set_prefix('signatureId', self.EVENTS[v][0]) c.set_prefix('name', self.EVENTS[v][1]) c.set_prefix('severity', self.EVENTS[v][2]) # Message supp infos c.set_field('message', msg_data[:1022]) c.set_field("sourceHostName", wp_report['site'][:1022]) msg = c.build_cef() log.debug("Message CEF: {}".format(msg)) messages.append(msg) return messages
def emit_messages(self, wp_report): """ Sends the CEF syslog messages for the report. """ log.debug("Sending Syslog messages for site {}".format( wp_report["site"])) for m in self.get_messages(wp_report): self.syslog.info(m)
def _scan_site(self, wp_site: Site, wp_report: ScanReport) -> Optional[ScanReport]: """ Handled WPScan scanning , parsing, errors and reporting. Returns filled wp_report, None if interrupted or killed. Can raise `RuntimeError` if any errors. """ # WPScan arguments wpscan_arguments = (self.wpscan_args + wp_site["wpscan_args"] + ["--url", wp_site["url"]]) # Output log.info(f"Scanning site {wp_site['url']}") # Launch WPScan wpscan_process = self.wpscan.wpscan(*wpscan_arguments) wp_report["wpscan_output"] = wpscan_process.stdout log.debug(f"WPScan raw output:\n{wp_report['wpscan_output']}") log.debug("Parsing WPScan output") try: # Use wpscan_out_parse module try: parser = WPScanJsonParser( json.loads(wp_report["wpscan_output"]), self.false_positive_strings + wp_site["false_positive_strings"] + [ "No WPVulnDB API Token given", "No WPScan API Token given" ]) except ValueError as err: parser = WPScanCliParser( wp_report["wpscan_output"], self.false_positive_strings + wp_site["false_positive_strings"] + [ "No WPVulnDB API Token given", "No WPScan API Token given" ]) finally: wp_report.load_parser(parser) except Exception as err: raise RuntimeError( f"Could not parse WPScan output for site {wp_site['url']}\nOutput:\n{wp_report['wpscan_output']}" ) from err # Exit code 0: all ok. Exit code 5: Vulnerable. Other exit code are considered as errors if wpscan_process.returncode in [0, 5]: return wp_report # Quick return if interrupting and/or if user cancelled scans if self.interrupting or wpscan_process.returncode in [2, -2, -9]: return None # Other errors codes : 127, etc, simply raise error err_str = f"WPScan failed with exit code {wpscan_process.returncode}. \nArguments: {safe_log_wpscan_args(wpscan_arguments)}. \nOutput: \n{remove_color(wp_report['wpscan_output'])}\nError: \n{wpscan_process.stderr}" raise RuntimeError(err_str)
def notify(self, wp_site, wp_report, last_wp_report): # Will print parsed readable Alerts, Warnings, etc as they will appear in email reports log.debug("%s\n" % ( WPWatcherNotification.build_message( wp_report, warnings=self.send_warnings or self. send_infos, # switches to include or not warnings and infos infos=self.send_infos))) if self.should_notify(wp_report, last_wp_report): self._notify(wp_site, wp_report, last_wp_report) return True else: return False
def wpscan(self, *args): (exit_code, output) = (0, "") # WPScan arguments cmd = shlex.split(self.path) + list(args) # Log wpscan command without api token log.debug("Running WPScan command: %s" % ' '.join(safe_log_wpscan_args(cmd))) # Run wpscan ------------------------------------------------------------------- try: process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=open(os.devnull, 'w')) # Append process to current process list and launch self.processes.append(process) wpscan_output, _ = process.communicate() self.processes.remove(process) try: wpscan_output = wpscan_output.decode("utf-8") except UnicodeDecodeError: wpscan_output = wpscan_output.decode("latin1") # Error when wpscan failed, except exit code 5: means the target has at least one vulnerability. # See https://github.com/wpscanteam/CMSScanner/blob/master/lib/cms_scanner/exit_code.rb if process.returncode not in [0, 5]: # Handle error err_string = "WPScan command '%s' failed with exit code %s %s" % ( ' '.join(safe_log_wpscan_args(cmd)), str( process.returncode), ". WPScan output: %s" % wpscan_output if wpscan_output else '') log.error(oneline(err_string)) else: # WPScan comamnd success log.debug("WPScan raw output:\n" + wpscan_output) (exit_code, output) = (process.returncode, wpscan_output) except (CalledProcessError) as err: # Handle error -------------------------------------------------- wpscan_output = str(err.output) err_string = "WPScan command '%s' failed with exit code %s %s\nError:\n%s" % ( ' '.join(safe_log_wpscan_args(cmd)), str(process.returncode), ". WPScan output: %s" % wpscan_output if wpscan_output else '', traceback.format_exc()) log.error(oneline(err_string)) (exit_code, output) = (err.returncode, wpscan_output) except FileNotFoundError as err: err_string = "Could not find wpscan executable. \nError:\n%s" % ( traceback.format_exc()) log.error(oneline(err_string)) (exit_code, output) = (-1, "") return ((exit_code, output))
def open(self) -> None: """ Acquire the file lock for the DB file. """ try: self._wp_report_file_lock.acquire(timeout=1) except Timeout as err: raise RuntimeError(f"Could not use the database file '{self.filepath}' because another instance of WPWatcher is using it. ") from err log.debug(f"Acquired DB lock file '{self.filepath}.lock'") try: self.write() except: log.error( f"Could not write wp_reports database: {self.filepath}. Use '--reports null' to ignore local Json database." ) raise
def __init__(self, conf: Config): """ Arguments: - `conf`: the configuration dict. Required """ # (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: DataBase = DataBase(filepath=conf["wp_reports"], daemon=conf['daemon']) # Update config before passing it to WPWatcherScanner conf.update({"wp_reports": self.wp_reports.filepath}) # Init scanner self.scanner: Scanner = Scanner(conf) # Save sites conf["wp_sites"] = [Site(site_conf) for site_conf in conf["wp_sites"]] self.wp_sites: List[Site] = conf["wp_sites"] # Asynchronous executor self._executor: concurrent.futures.ThreadPoolExecutor = ( concurrent.futures.ThreadPoolExecutor( max_workers=conf["asynch_workers"])) # List of conccurent futures self._futures: List[concurrent.futures.Future] = [ ] # type: ignore [type-arg] # 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) self.new_reports = ReportCollection() "New reports, cleared and filled when running `run_scans`." self.all_reports = ReportCollection() "All reports an instance of `WPWatcher` have generated using `run_scans`." # Dump config log.debug(f"Configuration:{repr(conf)}")
def _wpscan_site(self, wp_site, wp_report): # WPScan arguments wpscan_arguments = self.wpscan_args + wp_site['wpscan_args'] + [ '--url', wp_site['url'] ] # Output log.info("Scanning site %s" % wp_site['url']) # Launch WPScan (wpscan_exit_code, wp_report["wpscan_output"]) = self.wpscan.wpscan(*wpscan_arguments) # Exit code 0: all ok. Exit code 5: Vulnerable. Other exit code are considered as errors if wpscan_exit_code in [0, 5]: # Call parse_result from parser.py log.debug("Parsing WPScan output") try: wp_report['infos'], wp_report['warnings'], wp_report[ 'alerts'] = parse_results( wp_report['wpscan_output'], self.false_positive_strings + wp_site['false_positive_strings'] + ['No WPVulnDB API Token given']) wp_report['errors'] = [] # clear errors if any except Exception as err: err_str = "Could not parse WPScan output for site %s\n%s" % ( wp_site['url'], traceback.format_exc()) log.error(err_str) raise RuntimeError(err_str) from err else: return wp_report # Handle scan errors ----- # Quick return if interrupting and/or if user cacelled scans if self.interrupting or wpscan_exit_code in [2, -2, -9]: return None # Other errors codes : -9, -2, 127, etc: # or wpscan_exit_code not in [1,3,4] # If WPScan error, add the error to the reports # This types if errors will be written into the Json database file exit codes 1,3,4 err_str = "WPScan failed with exit code %s. \nWPScan arguments: %s. \nWPScan output: \n%s" % ( (wpscan_exit_code, safe_log_wpscan_args(wpscan_arguments), wp_report['wpscan_output'])) raise RuntimeError(err_str)
def _wpscan( self, *args: str ) -> subprocess.CompletedProcess: # type: ignore [type-arg] # WPScan arguments arguments = list(args) if arguments[0] == 'wpscan': arguments.pop(0) cmd = self._wpscan_path + arguments # Log wpscan command without api token log.debug( f"Running WPScan command: {' '.join(safe_log_wpscan_args(cmd))}") # Run wpscan process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) # Append process to current process list and launch self.processes.append(process) if self._scan_timeout: try: stdout, stderr = timeout(self._scan_timeout.total_seconds(), process.communicate) except TimeoutError as err: process.kill() # Raise error err_str = f"WPScan process '{safe_log_wpscan_args(cmd)}' timed out after {self._scan_timeout.total_seconds()} seconds. Setup 'scan_timeout' to allow more time. " raise RuntimeError(err_str) from err else: stdout, stderr = process.communicate() self.processes.remove(process) try: out_decoded = stdout.decode("utf-8") err_decoded = stderr.decode("utf-8") except UnicodeDecodeError: out_decoded = stdout.decode("latin1", errors="replace") err_decoded = stderr.decode("latin1", errors="replace") finally: return subprocess.CompletedProcess(args=cmd, returncode=process.returncode, stdout=out_decoded, stderr=err_decoded)
def _wpscan_site(self, wp_site, wp_report): '''Handled WPScan scanning , parsing, errors and reporting. Returns filled wp_report, None if interrupted or killed. Can raise RuntimeError if WPScan failed''' # WPScan arguments wpscan_arguments=self.wpscan_args+wp_site['wpscan_args']+['--url', wp_site['url']] # Output log.info("Scanning site %s"%wp_site['url'] ) # Launch WPScan (wpscan_exit_code, wp_report["wpscan_output"]) = self.wpscan.wpscan(*wpscan_arguments) log.debug("Parsing WPScan output") try: # Call parse_results_from_string from wpscan_out_parse module results = parse_results_from_string(wp_report['wpscan_output'] , self.false_positive_strings + wp_site['false_positive_strings'] + ['No WPVulnDB API Token given'] ) wp_report['infos'], wp_report['warnings'] , wp_report['alerts'], wp_report['summary'] = results['infos'], results['warnings'], results['alerts'], results['summary'] if results['error']: wp_report['error']+=results['error'] except Exception as err: err_str="Could not parse WPScan output for site %s\n%s"%(wp_site['url'],traceback.format_exc()) log.error(err_str) raise RuntimeError(err_str) from err # Exit code 0: all ok. Exit code 5: Vulnerable. Other exit code are considered as errors if wpscan_exit_code in [0,5]: return wp_report # Quick return if interrupting and/or if user cacelled scans if self.interrupting or wpscan_exit_code in [2, -2, -9]: return None # Other errors codes : 127, etc, simply raise error err_str="WPScan failed with exit code %s. \nArguments: %s. \nOutput: \n%s"%((wpscan_exit_code, safe_log_wpscan_args(wpscan_arguments), re.sub(r'(\x1b|\[[0-9][0-9]?m)','', wp_report['wpscan_output']) )) raise RuntimeError(err_str)
def _wpscan(self, *args): (exit_code, output) = (0, "") # WPScan arguments cmd = self.wpscan_executable + list(args) # Log wpscan command without api token log.debug("Running WPScan command: %s" % ' '.join(safe_log_wpscan_args(cmd))) # Run wpscan ------------------------------------------------------------------- try: process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) # Append process to current process list and launch self.processes.append(process) wpscan_output, stderr = process.communicate() self.processes.remove(process) try: wpscan_output = wpscan_output.decode("utf-8") except UnicodeDecodeError: wpscan_output = wpscan_output.decode("latin1") # Error when wpscan failed, except exit code 5: means the target has at least one vulnerability. # See https://github.com/wpscanteam/CMSScanner/blob/master/lib/cms_scanner/exit_code.rb if process.returncode in [0, 5]: # WPScan comamnd success log.debug("WPScan raw output:\n" + wpscan_output) # Log error ---- else: err_string, full_err_string = self.get_full_err_string( cmd, process.returncode, wpscan_output, stderr) log.error(err_string) log.debug(full_err_string) return ((process.returncode, wpscan_output)) except FileNotFoundError as err: err_string = "Could not find wpscan executable.\n%s" % ( traceback.format_exc()) log.error(oneline(err_string)) raise RuntimeError(err_string) from err
def scan_site(self, wp_site, last_wp_report=None): # Init report variables wp_report = { "site": wp_site['url'], "status": None, "datetime": datetime.now().strftime(DATE_FORMAT), "last_email": None, "errors": [], "infos": [], "warnings": [], "alerts": [], "fixed": [], "wpscan_output": "" # will be deleted } # Skip if the daemon mode is enabled and scan already happend in the last configured `daemon_loop_wait` if last_wp_report and self.skip_this_site(wp_report, last_wp_report): return None # Launch WPScan try: # Abnormal failure exit codes not in 0-5 if not self.wpscan_site(wp_site, wp_report): log.error( "Abnormal failure scanning %s exit codes not 0 or 5" % (wp_site['url'])) return None except RuntimeError as err: # Try to handle error and return, recall scan_site() wp_report_new, handled = self.handle_wpscan_err(wp_site, wp_report) if handled: wp_report.update(wp_report_new) return wp_report elif not self.interrupting: log.error("Could not scan site %s" % wp_site['url']) log.debug(traceback.format_exc()) wp_report['errors'].append(str(err)) # Fail fast self.check_fail_fast() else: return None self.fill_report_status(wp_report) # Prescan handling if self.prescan_without_api_token and not self.retreive_api_token( wp_site['wpscan_args']) and wp_report['status'] in [ 'WARNING', 'ALERT' ]: self.prescanned_sites_warn.append(wp_site) log.warning( "Site %s triggered prescan warning, it will be scanned with API token at the end" % (wp_site['url'])) return None self.log_report_results(wp_report) # Write wpscan output wpscan_results_file = self.write_wpscan_output(wp_report) if wpscan_results_file: log.info("WPScan output saved to file %s" % wpscan_results_file) # Updating report entry with data from last scan self.update_report(wp_report, last_wp_report, wp_site) # Notify recepients if match triggers try: if self.mail.notify(wp_site, wp_report, last_wp_report): # Store report time wp_report['last_email'] = datetime.now().strftime(DATE_FORMAT) # Discard fixed items because infos have been sent wp_report['fixed'] = [] except RuntimeError: # Fail fast self.check_fail_fast() # Save scanned site self.scanned_sites.append(wp_site['url']) self.terminate_scan(wp_site, wp_report) return (wp_report)
def scan_site(self, wp_site): wp_site=self.format_site(wp_site) # Init report variables wp_report={ "site":wp_site['url'], "status":None, "datetime": datetime.now().strftime('%Y-%m-%dT%H-%M-%S'), "last_email":None, "errors":[], "infos":[], "warnings":[], "alerts":[], "fixed":[], "wpscan_output":None # will be deleted } # Find last site result if any last_wp_report=[r for r in self.wp_reports if r['site']==wp_site['url']] if len(last_wp_report)>0: last_wp_report=last_wp_report[0] # Skip if the daemon mode is enabled and scan already happend in the last configured `daemon_loop_wait` if ( self.conf['daemon'] and datetime.strptime(wp_report['datetime'],'%Y-%m-%dT%H-%M-%S') - datetime.strptime(last_wp_report['datetime'],'%Y-%m-%dT%H-%M-%S') < self.conf['daemon_loop_sleep']): log.info("Daemon skipping site %s because already scanned in the last %s"%(wp_site['url'] , self.conf['daemon_loop_sleep'])) self.scanned_sites.append(None) return None else: last_wp_report=None # WPScan arguments wpscan_arguments=self.conf['wpscan_args']+wp_site['wpscan_args']+['--url', wp_site['url']] # Output log.info("Scanning site %s"%wp_site['url'] ) # Launch WPScan ------------------------------------------------------- (wpscan_exit_code, wp_report["wpscan_output"]) = self.wpscan.wpscan(*wpscan_arguments) # Exit code 0: all ok. Exit code 5: Vulnerable. Other exit code are considered as errors # Handle scan errors if wpscan_exit_code not in [0,5]: # Quick return if interrupting if self.interrupting: return None # Quick return if user cacelled scans if wpscan_exit_code in [2]: return None # Fail fast if self.conf['fail_fast']: if not self.interrupting: log.error("Failure") self.interrupt() else: return None # Interrupt will generate other errors # If WPScan error, add the error to the reports # This types if errors will be written into the Json database file if wpscan_exit_code in [1,3,4]: err_str="WPScan failed with exit code %s. \nWPScan arguments: %s. \nWPScan output: \n%s"%((wpscan_exit_code, safe_log_wpscan_args(wpscan_arguments), wp_report['wpscan_output'])) wp_report['errors'].append(err_str) log.error("Could not scan site %s"%wp_site['url']) # Try to handle error and return wp_report, handled = self.handle_wpscan_err(wp_site, wp_report) if handled: return wp_report # Other errors codes : -9, -2, 127, etc: Just return None right away else: return None # No errors with wpscan ----------------------------- else: # Write wpscan output wpscan_results_file=None if self.conf['wpscan_output_folder'] : wpscan_results_file=os.path.join(self.conf['wpscan_output_folder'], get_valid_filename('WPScan_results_%s_%s.txt' % (wp_site['url'], wp_report['datetime']))) with open(wpscan_results_file, 'w') as wpout: wpout.write(re.sub(r'(\x1b|\[[0-9][0-9]?m)','', str(wp_report['wpscan_output']))) log.debug("Parsing WPScan output") # Call parse_result from parser.py ------------------------ wp_report['infos'], wp_report['warnings'] , wp_report['alerts'] = parse_results(wp_report['wpscan_output'] , self.conf['false_positive_strings']+wp_site['false_positive_strings'] ) # Updating report entry with data from last scan if any if last_wp_report: self.update_report(wp_report, last_wp_report) # Print WPScan findings ------------------------------------------------------ for info in wp_report['infos']: log.info(oneline("** WPScan INFO %s ** %s" % (wp_site['url'], info ))) for fix in wp_report['fixed']: log.info(oneline("** FIXED %s ** %s" % (wp_site['url'], fix ))) for warning in wp_report['warnings']: log.warning(oneline("** WPScan WARNING %s ** %s" % (wp_site['url'], warning ))) for alert in wp_report['alerts']: log.critical(oneline("** WPScan ALERT %s ** %s" % (wp_site['url'], alert ))) if wpscan_results_file: log.info("WPScan results saved to file %s"%wpscan_results_file) # Report status ------------------------------------------------ if len(wp_report['errors'])>0:wp_report['status']="ERROR" elif len(wp_report['warnings'])>0 and len(wp_report['alerts']) == 0: wp_report['status']='WARNING' elif len(wp_report['alerts'])>0: wp_report['status']='ALERT' elif len(wp_report['fixed'])>0: wp_report['status']='FIXED' else: wp_report['status']='INFO' # Will print parsed readable Alerts, Warnings, etc as they will appear in email reports log.debug("\n%s\n"%(build_message(wp_report, warnings=self.conf['send_warnings'] or self.conf['send_infos'], # switches to include or not warnings and infos infos=self.conf['send_infos']))) # Notify recepients if match triggers and no errors self.notify(wp_site, wp_report, last_wp_report) # Save scanned site self.scanned_sites.append(wp_site['url']) # Discard wpscan_output from report del wp_report['wpscan_output'] # Save report in global instance database and to file when a site has been scanned self.update_and_write_wp_reports([wp_report]) # Print progress print_progress_bar(len(self.scanned_sites), len(self.conf['wp_sites'])) return(wp_report)
def close(self) -> None: """ Release the file lock. """ self._wp_report_file_lock.release() log.debug(f"Released DB lock file '{self.filepath}.lock'")
def scan_site(self, wp_site, last_wp_report=None): '''Orchestrate the scanning of a site. Return the final wp_report or None if something happened. ''' # Init report variables wp_report={ "site":wp_site['url'], "status":None, "datetime": datetime.now().strftime(DATE_FORMAT), "last_email":None, "error":'', "infos":[], "warnings":[], "alerts":[], "fixed":[], "summary":None, "wpscan_output":"" # will be deleted } # Skip if the daemon mode is enabled and scan already happend in the last configured `daemon_loop_wait` if last_wp_report and self.skip_this_site(wp_report, last_wp_report): return None # Launch WPScan try: # If report is None, return None right away if not self.wpscan_site(wp_site, wp_report): return None except RuntimeError as err: # Try to handle error and return, will recall scan_site() wp_report_new, handled = self.handle_wpscan_err(wp_site, wp_report) if handled: wp_report=wp_report_new return wp_report elif not self.interrupting: log.error("Could not scan site %s"%wp_site['url']) log.debug(traceback.format_exc()) wp_report['error']+=str(err) # Fail fast self.check_fail_fast() else: return None self.fill_report_status(wp_report) self.log_report_results(wp_report) # Write wpscan output wpscan_results_file=self.write_wpscan_output(wp_report) if wpscan_results_file: log.info("WPScan output saved to file %s"%wpscan_results_file) # Updating report entry with data from last scan self.update_report(wp_report, last_wp_report, wp_site) # Notify recepients if match triggers try: # Will print parsed readable Alerts, Warnings, etc as they will appear in email reports log.debug("%s\n"%(WPWatcherNotification.build_message(wp_report, warnings=True, infos=True))) if self.mail.notify(wp_site, wp_report, last_wp_report): # Store report time wp_report['last_email']=datetime.now().strftime(DATE_FORMAT) # Discard fixed items because infos have been sent wp_report['fixed']=[] except RuntimeError: # Fail fast self.check_fail_fast() self.fill_report_status(wp_report) # Discard wpscan_output from report if 'wpscan_output' in wp_report: del wp_report['wpscan_output'] # Send syslog if self.syslog is not None if self.syslog: try: self.syslog.emit_messages(wp_report) except Exception as err: log.error("Unable to send the syslog messages for site "+wp_site['url']+"\n"+traceback.format_exc()) wp_report['error']+="Unable to send all syslog messages for site "+wp_site['url']+"\n"+traceback.format_exc() self.check_fail_fast() self.fill_report_status(wp_report) # Save scanned site self.scanned_sites.append(wp_site['url']) return(wp_report)