def test_wpscan_output_folder(self): RESULTS_FOLDER="./results/" WPSCAN_OUTPUT_CONFIG = DEFAULT_CONFIG+"\nwpscan_output_folder=%s"%RESULTS_FOLDER scanner=WPWatcherScanner(WPWatcherConfig(string=WPSCAN_OUTPUT_CONFIG).build_config()[0]) self.assertTrue(os.path.isdir(RESULTS_FOLDER),"WPscan results folder doesn't seem to have been init") for s in WP_SITES: report={ "site": s['url'], "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": [], "summary":None, "wpscan_output":"This is real%s"%(s) } f=scanner.write_wpscan_output(report) f1=os.path.join(RESULTS_FOLDER, 'warning/', get_valid_filename('WPScan_output_%s_%s.txt' % (s['url'], "2020-04-08T16-05-16"))) self.assertEqual(f, f1, "Inconsistent WPScan output filenames") self.assertTrue(os.path.isfile(f1),"WPscan output file doesn't exist") with open(f1, 'r') as out: self.assertEqual(out.read(), "This is real%s"%(s)) shutil.rmtree(RESULTS_FOLDER)
def write_wpscan_output(self, wp_report): '''Write WPScan output to configured place with `wpscan_output_folder` or return None ''' # Subfolder folder="%s/"%wp_report['status'].lower() # Write wpscan output wpscan_results_file=None if self.wpscan_output_folder : wpscan_results_file=os.path.join(self.wpscan_output_folder, folder , get_valid_filename('WPScan_output_%s_%s.txt' % (wp_report['site'], wp_report['datetime']))) with open(wpscan_results_file, 'wb') as wpout: self._write_wpscan_output(wp_report, wpout) return(wpscan_results_file)
def write_wpscan_output(self, wp_report): # Subfolder folder = "%s/" % wp_report['status'].lower( ) if wp_report['status'] != 'FIXED' else 'info/' # Write wpscan output wpscan_results_file = None if self.wpscan_output_folder: wpscan_results_file = os.path.join( self.wpscan_output_folder, folder, get_valid_filename('WPScan_output_%s_%s.txt' % (wp_report['site'], 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']))) return (wpscan_results_file)
def send_report( self, wp_report, email_to ): #, send_infos=False, send_warnings=True, send_errors=False, attach_wpscan_output=False): # Building message message = MIMEMultipart("html") message['Subject'] = 'WPWatcher %s report - %s - %s' % ( wp_report['status'], wp_report['site'], wp_report['datetime']) message['From'] = self.from_email message['To'] = email_to # Email body body = self.build_message( wp_report, warnings=self.send_warnings or self.send_infos, # switches to include or not warnings and infos infos=self.send_infos) message.attach(MIMEText(body)) # Attachment log if attach_wpscan_output if self.attach_wpscan_output: # Remove color wp_report['wpscan_output'] = re.sub( r'(\x1b|\[[0-9][0-9]?m)', '', str(wp_report['wpscan_output'])) # Read the WPSCan output attachment = io.BytesIO(wp_report['wpscan_output'].encode()) part = MIMEApplication(attachment.read(), Name='WPScan_output') # Sanitize WPScan report filename wpscan_report_filename = get_valid_filename( 'WPScan_output_%s_%s' % (wp_report['site'], wp_report['datetime'])) # Add header as key/value pair to attachment part part.add_header( "Content-Disposition", "attachment; filename=%s.txt" % (wpscan_report_filename), ) # Attach the report message.attach(part) log.info("%s attached" % (wpscan_report_filename)) else: log.info( "No file attached, set attach_wpscan_output=Yes or use --attach to attach WPScan output to emails" ) # # Connecting and sending self.send_mail(message, email_to) log.info("Email sent: %s to %s" % (message['Subject'], email_to))
def send_report(self, wp_report, email_to): '''Build MIME message based on report and call send_mail''' # Building message message = MIMEMultipart("html") message['Subject'] = 'WPWatcher %s report - %s - %s' % ( wp_report['status'], wp_report['site'], wp_report['datetime']) message['From'] = self.from_email message['To'] = ','.join(email_to) # Email body body = self.build_message(wp_report, warnings=self.send_warnings, infos=self.send_infos) if self.use_monospace_font: body = '<font face="Courier New, Courier, monospace" size="-1">' + body + '</font>' message.attach(MIMEText(body, 'html')) # Attachment log if attach_wpscan_output if self.attach_wpscan_output: # Remove color wp_report['wpscan_output'] = re.sub( r'(\x1b|\[[0-9][0-9]?m)', '', str(wp_report['wpscan_output'])) # Read the WPSCan output attachment = io.BytesIO(wp_report['wpscan_output'].encode()) part = MIMEApplication(attachment.read(), Name='WPScan_output') # Sanitize WPScan report filename wpscan_report_filename = get_valid_filename( 'WPScan_output_%s_%s' % (wp_report['site'], wp_report['datetime'])) # Add header as key/value pair to attachment part part.add_header( "Content-Disposition", "attachment; filename=%s.txt" % (wpscan_report_filename), ) # Attach the report message.attach(part) log.info("%s attached" % (wpscan_report_filename)) else: log.info( "No file attached, set attach_wpscan_output=Yes or use --attach to attach WPScan output to emails" ) # # Connecting and sending self.send_mail(message, email_to) log.info("Email sent: %s to %s" % (message['Subject'], email_to))
def write_wpscan_output(self, wp_report: ScanReport) -> Optional[str]: """Write WPScan output to configured place with `wpscan_output_folder` if configured""" # Subfolder folder = f"{wp_report['status'].lower()}/" # Write wpscan output if self.wpscan_output_folder: wpscan_results_file = os.path.join( self.wpscan_output_folder, folder, get_valid_filename( f"WPScan_output_{wp_report['site']}_{wp_report['datetime']}.txt" ), ) log.info(f"Saving WPScan output to file {wpscan_results_file}") with open(wpscan_results_file, "wb") as wpout: self._write_wpscan_output(wp_report, wpout) return wpout.name else: return None
def _send_report(self, wp_report: Dict[str, Any], email_to: List[str], wpscan_command: str) -> None: """Build MIME message based on report and call send_mail""" # Building message message = MIMEMultipart("html") message[ "Subject"] = f"WPWatcher {wp_report['status']} report - {wp_report['site']} - {wp_report['datetime']}" message["From"] = self.from_email message["To"] = ",".join(email_to) # Email body body = self.build_message(wp_report, wpscan_command) if self.use_monospace_font: body = ( f'<font face="Courier New, Courier, monospace" size="-1">{body}</font>' ) message.attach(MIMEText(body, "html")) # Attachment log if attach_wpscan_output if self.attach_wpscan_output: # Remove color wp_report["wpscan_output"] = re.sub( r"(\x1b|\[[0-9][0-9]?m)", "", str(wp_report["wpscan_output"])) # Read the WPSCan output attachment = io.BytesIO(wp_report["wpscan_output"].encode()) part = MIMEApplication(attachment.read(), Name="WPScan_output") # Sanitize WPScan report filename wpscan_report_filename = get_valid_filename( f"WPScan_output_{wp_report['site']}_{wp_report['datetime']}") # Add header as key/value pair to attachment part part.add_header( "Content-Disposition", f"attachment; filename={wpscan_report_filename}.txt", ) # Attach the report message.attach(part) # Connecting and sending self._send_mail(message, email_to) log.info(f"Email sent: {message['Subject']} to {email_to}")
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 send_report(self, wp_site, wp_report): # To if len(self.conf['email_errors_to'])>0 and wp_report['status']=='ERROR': to_email = ','.join( self.conf['email_errors_to'] ) else: to_email = ','.join( wp_site['email_to'] + self.conf['email_to'] ) if to_email != "": # Building message message = MIMEMultipart("html") message['Subject'] = 'WPWatcher %s report - %s - %s' % ( wp_report['status'], wp_site['url'], wp_report['datetime']) message['From'] = self.conf['from_email'] message['To'] = to_email # Email body body=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']) message.attach(MIMEText(body)) # Attachment log if attach_wpscan_output if self.conf['attach_wpscan_output']: # Remove color wp_report['wpscan_output'] = re.sub(r'(\x1b|\[[0-9][0-9]?m)','', str(wp_report['wpscan_output'])) # Read the WPSCan output attachment=io.BytesIO(wp_report['wpscan_output'].encode()) part = MIMEBase("application", "octet-stream") part.set_payload(attachment.read()) # Encode file in ASCII characters to send by email encoders.encode_base64(part) # Sanitize WPScan report filename wpscan_report_filename=get_valid_filename('WPScan_results_%s_%s' % (wp_site['url'], wp_report['datetime'])) # Add header as key/value pair to attachment part part.add_header( "Content-Disposition", "attachment; filename=%s.txt"%(wpscan_report_filename), ) # Attach the report message.attach(part) # Connecting and sending # SMTP Connection s = smtplib.SMTP(self.conf['smtp_server']) s.ehlo() # SSL if self.conf['smtp_ssl']: s.starttls() # SMTP Auth if self.conf['smtp_auth']: s.login(self.conf['smtp_user'], self.conf['smtp_pass']) # Send Email s.sendmail(self.conf['from_email'], to_email, message.as_string()) s.quit() # Store report time wp_report['last_email']=datetime.now().strftime('%Y-%m-%dT%H-%M-%S') # Discard fixed items because infos have been sent wp_report['fixed']=[] log.info("Email sent: %s to %s" % (message['Subject'], to_email)) else: log.info("Not sending WPWatcher %s email report because no email is configured for site %s"%(wp_report['status'], wp_site['url']))