def test_read_config_error(self): with self.assertRaisesRegex((ValueError), 'Make sure the file exists and you have correct access right'): WPWatcherConfig(files=['/tmp/this_file_is_inexistent.conf']) WRONG_CONFIG=DEFAULT_CONFIG+'\nverbose=I dont know' with self.assertRaisesRegex(ValueError, 'Could not read boolean value in config file'): WPWatcherConfig(string=WRONG_CONFIG).build_config() WRONG_CONFIG=DEFAULT_CONFIG+'\nwpscan_args=["forgot", "a" "commas"]' with self.assertRaisesRegex(ValueError, 'Could not read JSON value in config file'): WPWatcherConfig(string=WRONG_CONFIG).build_config()
def test_send_report(self): # Init WPWatcher wpwatcher = WPWatcher(WPWatcherConfig(string=DEFAULT_CONFIG+"\nattach_wpscan_output=Yes").build_config()[0]) print(wpwatcher.__dict__) print(wpwatcher.scanner.__dict__) print(wpwatcher.scanner.mail.__dict__) # Send mail 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": ["This issue was fixed"], "summary":None, "wpscan_output":"This is real%s"%(s) } # notif=WPWatcherNotification(WPWatcherConfig(string=DEFAULT_CONFIG+"\nattach_wpscan_output=Yes").build_config()[0]) wpwatcher.scanner.mail.send_report(report, email_to='test', wpscan_command= 'just testing')
def test_should_notify(self): # test send_errors, send_infos, send_warnings, resend_emails_after, email_errors_to # Init WPWatcher CONFIG=DEFAULT_CONFIG+"\nsend_infos=Yes\nsend_errors=Yes\nsend_warnings=No" wpwatcher = WPWatcher(WPWatcherConfig(string=CONFIG).build_config()[0]) # wpwatcher.scanner.mail # TODO
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 test_scan_localhost_error_not_wordpress(self): # test info, warnings and alerts scanner = WPWatcherScanner( WPWatcherConfig(string=DEFAULT_CONFIG).build_config()[0]) report = scanner.scan_site( WPWatcher.format_site({'url': 'http://localhost:8080'})) self.assertEqual(report['status'], 'ERROR') self.assertRegex(report['error'], 'does not seem to be running WordPress')
def build_config_cli(args): args = vars(args) if hasattr( args, '__dict__') and not type(args) == dict else args # Configuration variables conf_files = args['conf'] if 'conf' in args else None # Init config dict: read config files configuration, files = WPWatcherConfig(files=conf_files).build_config() if files: log.info("Load config file(s) : %s" % files) conf_args = {} # Sorting out only args that matches config options and that are not None or False for k in args: if k in WPWatcherConfig.DEFAULT_CONFIG.keys() and args[k]: conf_args.update({k: args[k]}) # Append or init list of urls from file if any if 'wp_sites_list' in args and args['wp_sites_list']: with open(args['wp_sites_list'], 'r') as urlsfile: sites = [ site.replace('\n', '') for site in urlsfile.readlines() ] conf_args[ 'wp_sites'] = sites if 'wp_sites' not in conf_args else conf_args[ 'wp_sites'] + sites # Adjust special case of urls that are list of dict if 'wp_sites' in conf_args: conf_args['wp_sites'] = [{ "url": site } for site in conf_args['wp_sites']] # Adjust special case of resend_emails_after if 'resend_emails_after' in conf_args: conf_args['resend_emails_after'] = parse_timedelta( conf_args['resend_emails_after']) # Adjust special case of daemon_loop_sleep if 'daemon_loop_sleep' in conf_args: conf_args['daemon_loop_sleep'] = parse_timedelta( conf_args['daemon_loop_sleep']) # Adjust special case of wpscan_args if 'wpscan_args' in conf_args: conf_args['wpscan_args'] = shlex.split(conf_args['wpscan_args']) # if vars(args)['resend']: conf_args['resend_email_after']=timedelta(seconds=0) # Overwrite with conf dict biult from CLI Args if conf_args: configuration.update(conf_args) return configuration
def test_init_wpwatcher(self): # Init deafult watcher wpwatcher = WPWatcher( WPWatcherConfig(string=DEFAULT_CONFIG).build_config()[0]) flag = WPWatcherConfig(string=DEFAULT_CONFIG).build_config()[0] self.assertEqual( type(wpwatcher.scanner), WPWatcherScanner, "WPWatcherScanner doesn't seem to have been initialized") self.assertEqual( type(wpwatcher.scanner.mail), WPWatcherNotification, "WPWatcherNotification doesn't seem to have been initialized") self.assertEqual( type(wpwatcher.scanner.wpscan), WPScanWrapper, "WPScanWrapper doesn't seem to have been initialized") self.assertEqual( shlex.split( WPWatcherConfig( string=DEFAULT_CONFIG).build_config()[0]['wpscan_path']), wpwatcher.scanner.wpscan.wpscan_executable, "WPScan path seems to be wrong")
def __init__(self): args = self.parse_args() init_log(args.verbose, args.quiet) # If template conf , print and exit if args.template_conf: print(WPWatcherConfig.TEMPLATE_FILE) exit(0) log.info( "WPWatcher - Automating WPscan to scan and report vulnerable Wordpress sites" ) # If version, print and exit if args.version: log.info("Version:\t\t%s" % VERSION) log.info("Authors:\t\t%s" "" % AUTHORS) exit(0) # Init WPWatcher obhect and dump reports if args.wprs != False: if args.wprs == None: f = WPWatcher(WPWatcherConfig().build_config() [0]).find_wp_reports_file() else: f = args.wprs log.info("Reports: %s" % (f)) with open(f) as r: results = json.load(r) print(results_summary(results)) exit(0) # Read config configuration = self.build_config_cli(args) # Create main object wpwatcher = WPWatcher(configuration) # If daemon lopping if wpwatcher.conf['daemon']: log.info("Daemon mode selected, looping for ever...") results = None # Keep databse in memory while True: # Run scans for ever exit_code, results = wpwatcher.run_scans_and_notify() timesleep = wpwatcher.conf['daemon_loop_sleep'] log.info("Daemon sleeping %s and scanning again..." % timesleep) time.sleep(timesleep.total_seconds()) wpwatcher = WPWatcher(self.build_config_cli(args)) wpwatcher.wp_reports = results # Run scans and quit else: exit_code, results = wpwatcher.run_scans_and_notify() exit(exit_code)
def test_init_config_from_file(self): # Test find config file, rename default file if already exist and restore after test paths_found=WPWatcherConfig.find_config_files() existent_files=[] if len(paths_found)==0: paths_found=WPWatcherConfig.find_config_files(create=True) else: existent_files=paths_found for p in paths_found: os.rename(p,'%s.temp'%p) paths_found=WPWatcherConfig.find_config_files(create=True) # Init config and compare config_object=WPWatcherConfig() config_object2=WPWatcherConfig(files=paths_found) self.assertEqual(config_object.build_config(), config_object2.build_config(), "Config built with config path and without are different even if files are the same") for f in paths_found: os.remove(f) for f in existent_files: os.rename('%s.temp'%f , f)
def build_config_cli(args): """Assemble the config dict from args and from file. Arguments: - 'args': Namespace from `ArgumentParser.parse_args()` """ args = vars( args ) # if hasattr(args, '__dict__') and not type(args)==dict else args # Configuration variables conf_files = args["conf"] if "conf" in args else None # Init config dict: read config files configuration, files = WPWatcherConfig(files=conf_files).build_config() if files: log.info("Load config file(s) : %s" % files) # Sorting out only args that matches config options and that are not None or False conf_args = {} for k in args: if k in WPWatcherConfig.DEFAULT_CONFIG.keys() and args[k]: conf_args.update({k: args[k]}) # Append or init list of urls from file if any if args.get("wp_sites_list", None): with open(args["wp_sites_list"], "r") as urlsfile: sites = [ site.replace("\n", "") for site in urlsfile.readlines() ] conf_args["wp_sites"] = (sites if "wp_sites" not in conf_args else conf_args["wp_sites"] + sites) conf_args = WPWatcherCLI.adjust_special_cli_args(conf_args) # Overwrite with conf dict built from CLI Args if conf_args: for k in conf_args: if k == "wpscan_args": # MAke sure to append new WPScan arguments after defaults configuration[k].extend(conf_args[k]) else: configuration[k] = conf_args[k] return configuration
def test_init_config_from_string(self): # Test minimal config config_object=WPWatcherConfig(string=DEFAULT_CONFIG) self.assertEqual(0, len(config_object.files), "Files seems to have been loaded even if custom string passed to config oject") config_dict, files=config_object.build_config() self.assertEqual(0, len(files), "Files seems to have been loaded even if custom string passed to config oject") # Test config template file config_object=WPWatcherConfig(string=WPWatcherConfig.TEMPLATE_FILE) self.assertEqual(0, len(config_object.files), "Files seems to have been loaded even if custom string passed to config oject") config_dict, files=config_object.build_config() self.assertEqual(0, len(files), "Files seems to have been loaded even if custom string passed to config oject")
def test_config(self): config=""" [wpwatcher] wpscan_args=[ "--format", "cli", "--no-banner", "--random-user-agent", "--disable-tls-checks" ] wp_sites=%s send_email_report=Yes send_infos=Yes send_errors=Yes send_warnings=No attach_wpscan_output=Yes resend_emails_after=5d wp_reports=./test.json follow_redirect=Yes """%(json.dumps(self.get_sites())) w=WPWatcher(WPWatcherConfig(string=config).build_config()[0]) exit_code, results=w.run_scans_and_notify() self.assertEqual(0, exit_code)
def check_api_token_not_installed(): if 'WPSCAN_API_TOKEN' in os.environ: log.error( "WPSCAN_API_TOKEN environnement varible is set, please remove it to allow WPWatcher to handle WPScan API token" ) return False files = ['.wpscan/scan.json', '.wpscan/scan.yml'] env = ['HOME', 'XDG_CONFIG_HOME', 'APPDATA', 'PWD'] for wpscan_config_file in WPWatcherConfig.find_files(env, files): with open(wpscan_config_file, 'r') as wpscancfg: if any([ 'api_token' in line and line.strip()[0] != "#" for line in wpscancfg.readlines() ]): log.error( 'API token is set in the config file %s, please remove it to allow WPWatcher to handle WPScan API token' % (wpscan_config_file)) return False return True
def test_update_report(self): # Init Scanner scanner = WPWatcherScanner(WPWatcherConfig(string=DEFAULT_CONFIG).build_config()[0]) for s in WP_SITES: old={ "site": s['url'], "status": "WARNING", "datetime": "2020-04-08T16-05-16", "last_email": "2020-04-08T16-05-16", "error": '', "infos": [ "[+]","blablabla"], "warnings": [ "[+] WordPress version 5.2.2 identified (Insecure, released on 2019-06-18).\nblablabla\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": ["This issue was fixed"], "summary":None, "wpscan_output":"" } new={ "site": s['url'], "status": "", "datetime": "2020-04-10T16-00-00", "last_email": None, "error": '', "infos": [ "[+]","blablabla"], "warnings": [ "[!] 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":"" } expected={ "site": s['url'], "status": "", "datetime": "2020-04-10T16-00-00", "last_email": "2020-04-08T16-05-16", "error": '', "infos": [ "[+]","blablabla"], "warnings": [ "[!] 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": [ "This issue was fixed", 'Issue regarding component "%s" has been fixed since last report.\nLast report datetime is: %s'%("[+] WordPress version 5.2.2 identified (Insecure, released on 2019-06-18).",old['last_email']) ], "summary":None, "wpscan_output":"" } scanner.update_report(new,old,s) print(new) print(expected) self.assertEqual(new, expected, "There is an issue with fixed issues feature: the expected report do not match the report returned by update_report()")
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")
def find_wp_reports_file(self, create=False, daemon=False): files=[DEFAULT_REPORTS] if not daemon else [DEFAULT_REPORTS_DAEMON] env=['HOME', 'PWD', 'XDG_CONFIG_HOME', 'APPDATA'] return(WPWatcherConfig.find_files(env, files, "[]", create=True)[0])
def test_interrupt(self): wpwatcher = WPWatcher( WPWatcherConfig(string=DEFAULT_CONFIG).build_config()[0]) with self.assertRaises(SystemExit): wpwatcher.interrupt()
def test_scan_radom_sites(self): # This test might be illegal in your country # Get list of Wordpress sites if not already downloaded filename='/tmp/wp_sites' if not os.path.isfile(filename): myfile = requests.get(SOURCE) open(filename, 'wb').write(myfile.content) # Select X from the 50M idxs = random.sample(range(50000), HOW_MANY) urls=[linecache.getline(filename, i) for i in idxs] # Prepare scan config CONFIG1=""" [wpwatcher] wp_sites=%s smtp_server=localhost:1025 [email protected] email_to=["*****@*****.**"] wpscan_args=["--rua", "--stealthy", "--format", "cli", "--no-banner", "--disable-tls-checks"] false_positive_strings=["You can get a free API token with 50 daily requests by registering at https://wpvulndb.com/users/sign_up"] send_email_report=Yes log_file=./TEST-wpwatcher.log.conf wp_reports=./TEST-wp_reports.json.conf asynch_workers=10 follow_redirect=Yes wpscan_output_folder=./TEST-wpscan-results/ send_infos=Yes """%json.dumps([{'url':s.strip()} for s in urls]) # Select X from the 50M idxs = random.sample(range(50000), HOW_MANY) urls=[linecache.getline(filename, i) for i in idxs] # Prepare scan config CONFIG2=""" [wpwatcher] wp_sites=%s smtp_server=localhost:1025 [email protected] email_to=["*****@*****.**"] wpscan_args=["--rua", "--stealthy", "--format", "json", "--no-banner", "--disable-tls-checks"] false_positive_strings=["You can get a free API token with 50 daily requests by registering at https://wpvulndb.com/users/sign_up"] send_email_report=Yes log_file=./TEST-wpwatcher.log.conf wp_reports=./TEST-wp_reports.json.conf asynch_workers=10 follow_redirect=Yes wpscan_output_folder=./TEST-wpscan-results/ attach_wpscan_output=Yes send_infos=Yes send_errors=Yes email_errors_to=["admins@domain"] # prescan_without_api_token=Yes """%json.dumps([{'url':s.strip()} for s in urls]) # Select X from the 50M idxs = random.sample(range(50000), HOW_MANY) urls=[linecache.getline(filename, i) for i in idxs] # Prepare scan config CONFIG3=""" [wpwatcher] wp_sites=%s smtp_server=localhost:1025 [email protected] email_to=["*****@*****.**"] wpscan_args=["--rua", "--stealthy", "--format", "json", "--no-banner", "--disable-tls-checks"] false_positive_strings=["You can get a free API token with 50 daily requests by registering at https://wpvulndb.com/users/sign_up"] send_email_report=Yes log_file=./TEST-wpwatcher.log.conf wp_reports=./TEST-wp_reports.json.conf asynch_workers=10 follow_redirect=Yes wpscan_output_folder=./TEST-wpscan-results/ attach_wpscan_output=Yes send_warnings=No send_errors=Yes fail_fast=Yes """%json.dumps([{'url':s.strip()} for s in urls]) # Launch SMPT debbug server smtpd.DebuggingServer(('localhost',1025), None ) executor = concurrent.futures.ThreadPoolExecutor(1) executor.submit(asyncore.loop) # Init WPWatcher w1 = WPWatcher(WPWatcherConfig(string=CONFIG1).build_config()[0]) # Run scans res1=w1.run_scans_and_notify() # Init WPWatcher w2 = WPWatcher(WPWatcherConfig(string=CONFIG2).build_config()[0]) # Run scans res2=w2.run_scans_and_notify() # Init WPWatcher w3 = WPWatcher(WPWatcherConfig(string=CONFIG3).build_config()[0]) # Run scans res3=w3.run_scans_and_notify() # Close mail server asyncore.close_all() self.assertEqual(type(res1), tuple, "run_scans_and_notify returned an invalied result")
def test_config(self): # Test minimal config config_object = WPWatcherConfig(string=DEFAULT_CONFIG) self.assertEqual( 0, len(config_object.files), "Files seems to have been loaded even if custom string passed to config oject" ) config_dict, files = config_object.build_config() self.assertEqual( 0, len(files), "Files seems to have been loaded even if custom string passed to config oject" ) self.assertEqual( NUMBER_OF_CONFIG_VALUES, len(config_dict), "The number of config values if not right or you forgot to change the value of NUMBER_OF_CONFIG_VALUES" ) # Test config template file config_object = WPWatcherConfig(string=WPWatcherConfig.TEMPLATE_FILE) self.assertEqual( 0, len(config_object.files), "Files seems to have been loaded even if custom string passed to config oject" ) config_dict, files = config_object.build_config() self.assertEqual( 0, len(files), "Files seems to have been loaded even if custom string passed to config oject" ) self.assertEqual( NUMBER_OF_CONFIG_VALUES, len(config_dict), "The number of config values if not right or you forgot to change the value of NUMBER_OF_CONFIG_VALUES" ) # Test config template file config_object = WPWatcherConfig(string=WPWatcherConfig.TEMPLATE_FILE) self.assertEqual( 0, len(config_object.files), "Files seems to have been loaded even if custom string passed to config oject" ) config_dict, files = config_object.build_config() self.assertEqual( 0, len(files), "Files seems to have been loaded even if custom string passed to config oject" ) self.assertEqual( NUMBER_OF_CONFIG_VALUES, len(config_dict), "The number of config values if not right or you forgot to change the value of NUMBER_OF_CONFIG_VALUES" ) # Test find config file, rename default file if already exist and restore after test paths_found = WPWatcherConfig.find_config_files() existent_files = [] if len(paths_found) == 0: paths_found = WPWatcherConfig.find_config_files(create=True) else: existent_files = paths_found for p in paths_found: os.rename(p, '%s.temp' % p) paths_found = WPWatcherConfig.find_config_files(create=True) config_object = WPWatcherConfig() config_object2 = WPWatcherConfig(files=paths_found) self.assertEqual( config_object.build_config(), config_object2.build_config(), "Config built with config path and without are dirrent even if files are the same" ) for f in paths_found: os.remove(f) for f in existent_files: os.rename('%s.temp' % f, f)
def find_wp_reports_file(self, create=False, daemon=False): files = [DEFAULT_REPORTS] if not daemon else [DEFAULT_REPORTS_DAEMON] env = ["HOME", "PWD", "XDG_CONFIG_HOME", "APPDATA"] return WPWatcherConfig.find_files(env, files, "[]", create=True)[0]