def test_init_config_from_string(self): # Test minimal config config_dict = Config.fromstring(DEFAULT_CONFIG) self.assertEqual(config_dict['email_to'], ["*****@*****.**"]) self.assertEqual(config_dict['from_email'], "*****@*****.**") self.assertEqual(config_dict['smtp_server'], "localhost:1025") # Test config template file config_dict2 = Config.fromstring(Config.TEMPLATE_FILE) self.assertEqual(config_dict2['smtp_server'], "mailserver.de:587")
def test_init_wpwatcher(self): # Init deafult watcher wpwatcher = WPWatcher(Config.fromstring(DEFAULT_CONFIG)) self.assertEqual(type(wpwatcher.scanner), Scanner, "Scanner doesn't seem to have been initialized") self.assertEqual(type(wpwatcher.scanner.mail), EmailSender, "EmailSender 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(Config.fromstring(DEFAULT_CONFIG)['wpscan_path']), wpwatcher.scanner.wpscan._wpscan_path, "WPScan path seems to be wrong")
def test_scan_localhost_error_not_wordpress(self): # test info, warnings and alerts scanner = Scanner(Config.fromstring(DEFAULT_CONFIG)) report = scanner.scan_site(Site({'url': 'http://localhost:8080'})) self.assertEqual(report['status'], 'ERROR') self.assertRegex(report['error'], 'does not seem to be running WordPress')
def test_wpscan_output_folder(self): RESULTS_FOLDER="./results/" WPSCAN_OUTPUT_CONFIG = DEFAULT_CONFIG+"\nwpscan_output_folder=%s"%RESULTS_FOLDER scanner=Scanner(Config.fromstring(WPSCAN_OUTPUT_CONFIG)) 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 __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 test_daemon(self): # test daemon_loop_sleep and daemon mode conf = Config.fromstring(DEFAULT_CONFIG) conf['asynch_workers'] += 1 daemon = Daemon(conf) daemon.loop(ttl=timedelta(seconds=5)) self.assertTrue(not any( [r.status() != 'ERROR' for r in daemon.wpwatcher.new_reports])) self.assertGreater(len(daemon.wpwatcher.new_reports), 1)
def main(_args: Optional[Sequence[Text]] = None) -> None: """Main program entrypoint""" # Parse arguments args: argparse.Namespace = get_arg_parser().parse_args(_args) # Init logger with CLi arguments _init_log(args.verbose, args.quiet) # If template conf , print and exit if args.template_conf: template_conf() # Print "banner" log.info( "WPWatcher - Automating WPscan to scan and report vulnerable Wordpress sites" ) if args.version: # Print and exit version() if args.wprs != False: # Init WPWatcherDataBase object and dump reports wprs(filepath=args.wprs, daemon=args.daemon) # Read config configuration = Config.fromcliargs(args) if args.show: # Init WPWatcherDataBase object and dump cli formatted report show( urlpart=args.show, filepath=configuration["wp_reports"], daemon=args.daemon, ) # Launch syslog test if args.syslog_test: syslog_test(configuration) # If daemon lopping if configuration["daemon"]: # Run 4 ever daemon = Daemon(configuration) daemon.loop() else: # Run scans and quit wpwatcher = WPWatcher(configuration) exit_code, reports = wpwatcher.run_scans() 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 = Config.find_config_files() existent_files = [] if len(paths_found) == 0: paths_found = Config.find_config_files(create=True) else: existent_files = paths_found for p in paths_found: os.rename(p, '%s.temp' % p) paths_found = Config.find_config_files(create=True) # Init config and compare config_object = Config.fromenv() config_object2 = Config.fromfiles(paths_found) self.assertEqual( config_object, config_object2, "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 test_build_config_cli(self): parser = get_arg_parser() tmp = tempfile.NamedTemporaryFile('w', delete=False) tmp.write("site10.com\nsite11.org\nsite12.fr") tmp.flush() args = parser.parse_args([ '--url', 'site1.ca', 'site2.ca', '--urls', tmp.name, '--resend', '2m', '--loop', '60s', '--wpargs', '--format cli' ]) wpwatcher_configuration = Config.fromcliargs(args) self.assertEqual(wpwatcher_configuration.get('wp_sites'), [{ "url": "site1.ca" }, { "url": "site2.ca" }, { "url": "site10.com" }, { "url": "site11.org" }, { "url": "site12.fr" }]) self.assertIsInstance(wpwatcher_configuration.get('daemon_loop_sleep'), timedelta) self.assertIsInstance( wpwatcher_configuration.get('resend_emails_after'), timedelta) self.assertEqual(wpwatcher_configuration.get('wpscan_args'), [ "--random-user-agent", "--format", "json", "--cache-ttl", "0", "--format", "cli" ]) os.remove(tmp.name)
def test_send_report(self): # Init WPWatcher wpwatcher = WPWatcher( Config.fromstring(DEFAULT_CONFIG + "\nattach_wpscan_output=Yes")) 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) } wpwatcher.scanner.mail._send_report(report, email_to='test', wpscan_command='just testing')
def test_read_config_error(self): with self.assertRaisesRegex( (ValueError), 'Make sure the file exists and you have correct access right'): Config.fromfiles(['/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'): Config.fromstring(WRONG_CONFIG) WRONG_CONFIG = DEFAULT_CONFIG + '\nwpscan_args=["forgot", "a" "commas"]' with self.assertRaisesRegex( ValueError, 'Could not read JSON value in config file'): Config.fromstring(WRONG_CONFIG)
def _find_db_file(daemon: bool = False) -> str: files = [DEFAULT_REPORTS] if not daemon else [DEFAULT_REPORTS_DAEMON] env = ["HOME", "PWD", "XDG_CONFIG_HOME", "APPDATA"] return Config.find_files(env, files, "[]", create=True)[0]
def test_update_report(self): # Init Scanner scanner = Scanner(Config.fromstring(DEFAULT_CONFIG)) for s in WP_SITES: old=ScanReport({ "site": s['url'], "status": "WARNING", "datetime": "2020-04-08T16-05-16", "last_email": "2020-04-08T16-05-17", "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":"" }) parser = WPScanJsonParser(data={}) new=ScanReport({ "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_parser": parser, "wpscan_output":"" }) expected=ScanReport({ "site": s['url'], "status": "WARNING", "datetime": "2020-04-10T16-00-00", "last_email": "2020-04-08T16-05-17", "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\nWarning: This issue is unfixed since 2020-04-08T16-05-16" ], "alerts": [], "fixed": [ "This issue was fixed", 'Issue regarding component "%s" has been fixed since the last scan.'%("[+] WordPress version 5.2.2 identified (Insecure, released on 2019-06-18).") ], "summary":None, "wpscan_parser": parser, "wpscan_output":"" }) new.update_report(old) self.assertEqual(dict(new), dict(expected), "There is an issue with fixed issues feature: the expected report do not match the report returned by update_report()")
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(Config.fromstring(CONFIG))
def test_wp_reports_read_write(self): SPECIFIC_WP_REPORTS_FILE_CONFIG = DEFAULT_CONFIG + "\nwp_reports=%s" # Compare with config and no config db = DataBase() paths_found = db._find_db_file() db2 = DataBase( filepath=Config.fromstring(SPECIFIC_WP_REPORTS_FILE_CONFIG % (paths_found))['wp_reports']) self.assertEqual( db._data, db2._data, "WP reports database are different even if files are the same") # Test Reports database reports = [ ScanReport({ "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": [] }), ScanReport({ "site": "exemple2.com", "status": "INFO", "datetime": "2020-04-08T16-05-16", "last_email": None, "error": '', "infos": ["[+]", "blablabla"], "warnings": [], "alerts": [], "fixed": [] }) ] db = DataBase() db.open() db.write(reports) db.close() # Test internal _data gets updated after write() 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 = ReportCollection( ScanReport(item) for item in db._build_db(db.filepath)) with open(db.filepath, 'r') as dbf: wrote_db_alt = ReportCollection( ScanReport(item) for item in json.load(dbf)) for r in reports: self.assertIn( r, list(wrote_db), "The report do not seem to have been saved into db file") self.assertIn( r, list(wrote_db_alt), "The report do not seem to have been saved into db file (directly read with json.load)" ) self.assertIsNotNone( db.find(ScanReport(site=r['site'])), "The report do not seem to have been saved into db, cannot find it using find(). " ) self.assertEqual( list(db._data), list(wrote_db_alt), "The database file wrote (directly read with json.load) differ from in memory database" ) self.assertEqual( list(db._data), list(wrote_db), "The database file wrote differ from in memory database")
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(Config.fromstring(CONFIG1)) # Run scans res1 = w1.run_scans_and_notify() # Init WPWatcher w2 = WPWatcher(Config.fromstring(CONFIG2)) # Run scans res2 = w2.run_scans_and_notify() # Init WPWatcher w3 = WPWatcher(Config.fromstring(CONFIG3)) # 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_interrupt(self): wpwatcher = WPWatcher(Config.fromstring(DEFAULT_CONFIG)) with self.assertRaises(SystemExit): wpwatcher.interrupt()