From b1c484606a45fd6f4cb3b55fae95b8cd57717a1e Mon Sep 17 00:00:00 2001 From: zemik Date: Sun, 8 Feb 2026 12:18:06 +0300 Subject: [PATCH 1/5] Add --no-bootstrap option to disable Bootstrap Add --no-bootstrap option to disable Bootstrap in reports --- EyeWitness.py | 595 ++++++++++++++++++++++++++++++++++++++++++++++++++ reporting.py | 494 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1089 insertions(+) create mode 100644 EyeWitness.py create mode 100644 reporting.py diff --git a/EyeWitness.py b/EyeWitness.py new file mode 100644 index 00000000..f01ec004 --- /dev/null +++ b/EyeWitness.py @@ -0,0 +1,595 @@ +#!/usr/bin/env python3 +# PYTHON_ARGCOMPLETE_OK + +import argparse +try: + import argcomplete + from argcomplete.completers import FilesCompleter + HAS_ARGCOMPLETE = True +except ImportError: + HAS_ARGCOMPLETE = False + FilesCompleter = None +import glob +import os +import re +import shutil +import signal +import sys +import time +import webbrowser + +from modules import db_manager +from modules import objects +from modules import selenium_module +from modules.helpers import class_info +from modules.helpers import create_folders_css +from modules.helpers import default_creds_category +from modules.helpers import do_jitter +from modules.helpers import target_creator +from modules.helpers import title_screen +from modules.helpers import open_file_input +from modules.helpers import resolve_host +from modules.helpers import duplicate_check +from modules.reporting import create_table_head +from modules.reporting import create_web_index_head +from modules.reporting import sort_data_and_write +from multiprocessing import Manager +from multiprocessing import Process +from multiprocessing import current_process +import multiprocessing +from modules.platform_utils import PlatformManager, setup_virtual_display +from modules.resource_monitor import ResourceMonitor, check_disk_space, get_system_info +from modules.troubleshooting import get_progress_message + +# Initialize platform manager +platform_mgr = PlatformManager() + + +def create_cli_parser(): + parser = argparse.ArgumentParser( + add_help=False, description="EyeWitness is a tool used to capture\ + screenshots from a list of URLs") + parser.add_argument('-h', '-?', '--h', '-help', + '--help', action="store_true", help=argparse.SUPPRESS) + protocols = parser.add_argument_group('Protocols') + protocols.add_argument('--web', default=True, action='store_true', + help='HTTP Screenshot using Selenium') + + input_options = parser.add_argument_group('Input Options') + f_arg = input_options.add_argument('-f', metavar='Filename', default=None, + help='Line-separated file containing URLs to \ + capture') + if HAS_ARGCOMPLETE and FilesCompleter: + f_arg.completer = FilesCompleter() + x_arg = input_options.add_argument('-x', metavar='Filename.xml', default=None, + help='Nmap XML or .Nessus file') + if HAS_ARGCOMPLETE and FilesCompleter: + x_arg.completer = FilesCompleter(allowednames='*.xml *.nessus', directories=True) + input_options.add_argument('--single', metavar='Single URL', default=None, + help='Single URL/Host to capture') + input_options.add_argument('--no-dns', default=False, action='store_true', + help='Skip DNS resolution when connecting to \ + websites') + + timing_options = parser.add_argument_group('Timing Options') + timing_options.add_argument('--timeout', metavar='Timeout', default=7, type=int, + help='Maximum number of seconds to wait while\ + requesting a web page (Default: 7)') + timing_options.add_argument('--jitter', metavar='# of Seconds', default=0, + type=int, help='Randomize URLs and add a random\ + delay between requests') + timing_options.add_argument('--delay', metavar='# of Seconds', default=0, + type=int, help='Delay between the opening of the navigator and taking the screenshot') + # Calculate default threads based on CPU cores (2 threads per core, max 20) + default_threads = min(multiprocessing.cpu_count() * 2, 20) + timing_options.add_argument('--threads', metavar='# of Threads', default=default_threads, + type=int, help=f'Number of threads to use (default: {default_threads} based on CPU cores)') + timing_options.add_argument('--max-retries', default=1, metavar='Max retries on \ + a timeout'.replace(' ', ''), type=int, + help='Max retries on timeouts') + + report_options = parser.add_argument_group('Report Output Options') + d_arg = report_options.add_argument('-d', metavar='Output Directory', + default=None, + help='Output directory for screenshots and reports') + if HAS_ARGCOMPLETE and FilesCompleter: + from argcomplete.completers import DirectoriesCompleter + d_arg.completer = DirectoriesCompleter() + report_options.add_argument('--results', metavar='Results/Page', + default=25, type=int, help='Number of results per report page (default: 25)') + report_options.add_argument('--no-prompt', default=False, + action='store_true', + help='Skip prompt to open report when complete') + report_options.add_argument('--no-clear', default=True, + action='store_true', + help='Don\'t clear screen buffer (default behavior)') + report_options.add_argument("-b", "--no-bootstrap", + action="store_true", + help="Do not include Bootstrap CSS in generated HTML reports") + + http_options = parser.add_argument_group('Web Options') + http_options.add_argument('--user-agent', metavar='User Agent', + default=None, help='User Agent to use for all\ + requests') + http_options.add_argument('--difference', metavar='Difference Threshold', + default=50, type=int, help='Difference threshold\ + when determining if user agent requests are\ + close \"enough\" (Default: 50)') + http_options.add_argument('--proxy-ip', metavar='127.0.0.1', default=None, + help='IP of web proxy to go through') + http_options.add_argument('--proxy-port', metavar='8080', default=None, + type=int, help='Port of web proxy to go through') + http_options.add_argument('--proxy-type', metavar='socks5', default="http", + help='Proxy type (socks5/http)') + http_options.add_argument('--show-selenium', default=False, + action='store_true', help='Show display for selenium') + http_options.add_argument('--resolve', default=False, + action='store_true', help=("Resolve IP/Hostname" + " for targets")) + http_options.add_argument('--add-http-ports', default=[], + type=lambda s:[str(i) for i in s.split(",")], + help=("Comma-separated additional port(s) to assume " + "are http (e.g. '8018,8028')")) + http_options.add_argument('--add-https-ports', default=[], + type=lambda s:[str(i) for i in s.split(",")], + help=("Comma-separated additional port(s) to assume " + "are https (e.g. '8018,8028')")) + http_options.add_argument('--only-ports', default=[], + type=lambda s:[int(i) for i in s.split(",")], + help=("Comma-separated list of exclusive ports to " + "use (e.g. '80,8080')")) + http_options.add_argument('--prepend-https', default=False, action='store_true', + help='Prepend http:// and https:// to URLs without either') + http_options.add_argument('--validate-urls', default=False, action='store_true', + help='Only validate URLs without taking screenshots') + http_options.add_argument('--skip-validation', default=False, action='store_true', + help='Skip URL validation checks (use with caution)') + http_options.add_argument('--selenium-log-path', default='./chromedriver.log', action='store', + help='Selenium ChromeDriver log path') + http_options.add_argument('--cookies', metavar='key1=value1,key2=value2', default=None, + help='Additional cookies to add to the request') + http_options.add_argument('--width', metavar="1366", default=1366,type=int, + help='Screenshot window image width size. 600-7680 (eg. 1920)') + http_options.add_argument('--height', metavar="768", default=768, type=int, + help='Screenshot window image height size. 400-4320 (eg. 1080)') + + resume_options = parser.add_argument_group('Resume Options') + resume_options.add_argument('--resume', metavar='ew.db', + default=None, help='Path to db file if you want to resume') + + config_options = parser.add_argument_group('Configuration Options') + config_arg = config_options.add_argument('--config', metavar='config.json', default=None, + help='Configuration file path') + if HAS_ARGCOMPLETE and FilesCompleter: + config_arg.completer = FilesCompleter(allowednames='*.json', directories=True) + config_options.add_argument('--create-config', action='store_true', + help='Create sample configuration file') + + # Enable bash tab completion if argcomplete is available + if HAS_ARGCOMPLETE: + argcomplete.autocomplete(parser) + + args = parser.parse_args() + args.date = time.strftime('%Y/%m/%d') + args.time = time.strftime('%H:%M:%S') + + # Handle config creation + if args.create_config: + from modules.config import ConfigManager + ConfigManager.create_sample_config() + sys.exit(0) + + # Load config file if specified or found + from modules.config import ConfigManager + config = ConfigManager.load_config(args.config) + args = ConfigManager.apply_config_to_args(args, config) + + if args.h: + parser.print_help() + sys.exit() + + if args.f is None and args.single is None and args.resume is None and args.x is None: + print("[!] Error: No input specified") + print("[*] You must provide one of the following:") + print(" - URL file: -f urls.txt") + print(" - Single URL: --single http://example.com") + print(" - XML file: -x nmap.xml") + print(" - Resume scan: --resume") + print("[*] Run 'EyeWitness.py -h' for full help") + sys.exit(1) + + if ((args.f is not None) and not os.path.isfile(args.f)) or ((args.x is not None) and not os.path.isfile(args.x)): + from modules.troubleshooting import get_error_guidance + if args.f and not os.path.isfile(args.f): + print(get_error_guidance('file_not_found', path=args.f)) + if args.x and not os.path.isfile(args.x): + print(get_error_guidance('file_not_found', path=args.x)) + sys.exit(1) + + if args.width < 600 or args.width >7680: + print("\n[*] Error: Specify a width >= 600 and <= 7680, for example 1920.\n") + parser.print_help() + sys.exit() + + if args.height < 400 or args.height >4320: + print("\n[*] Error: Specify a height >= 400 and <= 4320, for example, 1080.\n") + parser.print_help() + sys.exit() + + if args.d is not None: + if args.d.startswith('/') or re.match( + '^[A-Za-z]:\\\\', args.d) is not None: + args.d = args.d.rstrip('/') + args.d = args.d.rstrip('\\') + else: + args.d = os.path.join(os.getcwd(), args.d) + + if not os.access(os.path.dirname(args.d), os.W_OK): + print('[*] Error: Please provide a valid folder name/path') + parser.print_help() + sys.exit() + else: + if not args.no_prompt: + if os.path.isdir(args.d): + overwrite_dir = input(('Directory Exists! Do you want to ' + 'overwrite? [y/n] ')) + overwrite_dir = overwrite_dir.lower().strip() + if overwrite_dir == 'n': + print('Quitting...Restart and provide the proper ' + 'directory to write to!') + sys.exit() + elif overwrite_dir == 'y': + shutil.rmtree(args.d) + pass + else: + print('Quitting since you didn\'t provide ' + 'a valid response...') + sys.exit() + + else: + output_folder = args.date.replace( + '/', '-') + '_' + args.time.replace(':', '') + args.d = os.path.join(os.getcwd(), output_folder) + + args.log_file_path = os.path.join(args.d, 'logfile.log') + + if not any((args.resume, args.web)): + print("[*] Error: You didn't give me an action to perform.") + print("[*] Error: Please use --web!\n") + parser.print_help() + sys.exit() + + if args.resume: + if not os.path.isfile(args.resume): + print(" [*] Error: No valid DB file provided for resume!") + sys.exit() + + if args.proxy_ip is not None and args.proxy_port is None: + print("[*] Error: Please provide a port for the proxy!") + parser.print_help() + sys.exit() + + if args.proxy_port is not None and args.proxy_ip is None: + print("[*] Error: Please provide an IP for the proxy!") + parser.print_help() + sys.exit() + + if args.cookies: + cookies_list = [] + for one_cookie in args.cookies.split(","): + if "=" not in one_cookie: + print("[*] Error: Cookies must be in the form of key1=value1,key2=value2") + sys.exit() + cookies_list.append({ + "name": one_cookie.split("=")[0], + "value": one_cookie.split("=")[1] + }) + args.cookies = cookies_list + args.ua_init = False + return args + + +def single_mode(cli_parsed): + display = None + driver = None + + def exitsig(*args): + if current_process().name == 'MainProcess': + print('') + print('Quitting...') + os._exit(1) + + signal.signal(signal.SIGINT, exitsig) + + if cli_parsed.web: + create_driver = selenium_module.create_driver + capture_host = selenium_module.capture_host + + # Setup virtual display with cross-platform handling + display = setup_virtual_display(platform_mgr, cli_parsed.show_selenium) + + try: + url = cli_parsed.single + http_object = objects.HTTPTableObject() + http_object.remote_system = url + http_object.set_paths( + cli_parsed.d, None) + + web_index_head = create_web_index_head(cli_parsed.date, cli_parsed.time, cli_parsed) + driver = create_driver(cli_parsed) + result, driver = capture_host(cli_parsed, http_object, driver) + result = default_creds_category(result) + if cli_parsed.resolve: + result.resolved = resolve_host(result.remote_system) + + html = result.create_table_html() + with open(os.path.join(cli_parsed.d, 'report.html'), 'w', encoding='utf-8') as f: + f.write(web_index_head) + f.write(create_table_head()) + f.write(html) + f.write("
") + finally: + if driver: + driver.quit() + if display is not None: + display.stop() + + +def worker_thread(cli_parsed, targets, lock, counter, start_time, user_agent=None): + manager = None + driver = None + + try: + manager = db_manager.DB_Manager(cli_parsed.d + '/ew.db') + manager.open_connection() + + if cli_parsed.web: + create_driver = selenium_module.create_driver + capture_host = selenium_module.capture_host + + with lock: + driver = create_driver(cli_parsed, user_agent) + + while True: + http_object = targets.get() + if http_object is None: + break + # Try to ensure object values are blank + http_object._category = None + http_object._default_creds = None + http_object._error_state = None + http_object._page_title = None + http_object._ssl_error = False + http_object.category = None + http_object.default_creds = None + http_object.error_state = None + http_object.page_title = None + http_object.resolved = None + http_object.source_code = None + # Fix our directory if its resuming from a different path + if os.path.dirname(cli_parsed.d) != os.path.dirname(http_object.screenshot_path): + http_object.set_paths( + cli_parsed.d, None) + + print('Attempting to screenshot {0}'.format(http_object.remote_system)) + + http_object.resolved = resolve_host(http_object.remote_system) + if user_agent is None: + http_object, driver = capture_host( + cli_parsed, http_object, driver) + if http_object.category is None and http_object.error_state is None: + http_object = default_creds_category(http_object) + manager.update_http_object(http_object) + else: + ua_object, driver = capture_host( + cli_parsed, http_object, driver) + if http_object.category is None and http_object.error_state is None: + ua_object = default_creds_category(ua_object) + manager.update_ua_object(ua_object) + + counter[0].value += 1 + + # Show progress with ETA every 5 completions or at milestones + if counter[0].value % 5 == 0 or counter[0].value in [1, 10, 25, 50, 100]: + progress_msg = get_progress_message( + counter[0].value, + counter[1], + start_time.value if start_time.value > 0 else None + ) + print(f'\x1b[32m{progress_msg}\x1b[0m') + + do_jitter(cli_parsed) + except KeyboardInterrupt: + pass + except Exception as e: + print(f'[!] Worker thread error: {e}') + finally: + if manager: + manager.close() + if driver: + driver.quit() + + +def multi_mode(cli_parsed): + dbm = db_manager.DB_Manager(cli_parsed.d + '/ew.db') + dbm.open_connection() + if not cli_parsed.resume: + dbm.initialize_db() + dbm.save_options(cli_parsed) + m = Manager() + targets = m.Queue() + lock = m.Lock() + multi_counter = m.Value('i', 0) + start_time = m.Value('d', 0.0) # Track start time for ETA + display = None + + def exitsig(*args): + dbm.close() + if current_process().name == 'MainProcess': + print('') + print('Resume using ./EyeWitness.py --resume {0}'.format(cli_parsed.d + '/ew.db')) + os._exit(1) + + signal.signal(signal.SIGINT, exitsig) + if cli_parsed.resume: + pass + else: + url_list = target_creator(cli_parsed) + if cli_parsed.web: + for url in url_list: + dbm.create_http_object(url, cli_parsed) + + if cli_parsed.web: + # Setup virtual display with cross-platform handling + display = setup_virtual_display(platform_mgr, cli_parsed.show_selenium) + + # Initialize resource monitor + resource_monitor = ResourceMonitor(memory_limit_percent=80) + + # Check disk space before starting + has_space, available_gb, total_gb = check_disk_space(cli_parsed.d, min_gb=1) + if not has_space: + print(f'[!] Warning: Low disk space! Only {available_gb:.1f}GB available') + print('[!] Consider freeing space or using a different output directory') + + # Get system info and recommended threads + print(f'[*] {get_system_info()}') + + multi_total = dbm.get_incomplete_http(targets) + if multi_total > 0: + if cli_parsed.resume: + print('Resuming Web Scan ({0} Hosts Remaining)'.format(str(multi_total))) + else: + print('Starting Web Requests ({0} Hosts)'.format(str(multi_total))) + + # Adjust thread count based on workload and resources + recommended_threads = resource_monitor.get_recommended_threads(cli_parsed.threads) + if recommended_threads < cli_parsed.threads: + print(f'[*] Adjusting threads from {cli_parsed.threads} to {recommended_threads} based on available memory') + + if multi_total < recommended_threads: + num_threads = multi_total + else: + num_threads = recommended_threads + + print(f'[*] Using {num_threads} threads for processing') + for i in range(num_threads): + targets.put(None) + try: + start_time.value = time.time() # Set start time + workers = [Process(target=worker_thread, args=( + cli_parsed, targets, lock, (multi_counter, multi_total), start_time)) for i in range(num_threads)] + for w in workers: + w.start() + for w in workers: + w.join() + except Exception as e: + print(str(e)) + + if display is not None: + display.stop() + results = dbm.get_complete_http() + dbm.close() + m.shutdown() + sort_data_and_write(cli_parsed, results) + + +if __name__ == "__main__": + cli_parsed = create_cli_parser() + start_time = time.time() + title_screen(cli_parsed) + + if cli_parsed.resume: + print('[*] Loading Resume Data...') + temp = cli_parsed + dbm = db_manager.DB_Manager(cli_parsed.resume) + dbm.open_connection() + cli_parsed = dbm.get_options() + cli_parsed.d = os.path.dirname(temp.resume) + cli_parsed.resume = temp.resume + if temp.results: + cli_parsed.results = temp.results + dbm.close() + + print('Loaded Resume Data with the following options:') + engines = [] + if cli_parsed.web: + engines.append('Firefox') + print('') + print('Input File: {0}'.format(cli_parsed.f)) + print('Engine(s): {0}'.format(','.join(engines))) + print('Threads: {0}'.format(cli_parsed.threads)) + print('Output Directory: {0}'.format(cli_parsed.d)) + print('Timeout: {0}'.format(cli_parsed.timeout)) + print('') + else: + create_folders_css(cli_parsed) + + # Handle validate-only mode + if cli_parsed.validate_urls: + print('[*] Running in URL validation mode only') + from modules.validation import validate_url_list + from modules.helpers import target_creator + + url_list = target_creator(cli_parsed) + valid_urls, invalid_urls = validate_url_list(url_list, require_scheme=False) + + print(f'\n[*] Validation Results:') + print(f' - Valid URLs: {len(valid_urls)}') + print(f' - Invalid URLs: {len(invalid_urls)}') + + if invalid_urls: + print('\n[!] Invalid URLs found:') + for url, error in invalid_urls[:20]: # Show first 20 + print(f' - {url}: {error}') + if len(invalid_urls) > 20: + print(f' ... and {len(invalid_urls) - 20} more') + + # Write valid URLs to file + if valid_urls: + valid_file = os.path.join(cli_parsed.d, 'valid_urls.txt') + with open(valid_file, 'w') as f: + for url in valid_urls: + f.write(url + '\n') + print(f'\n[*] Valid URLs written to: {valid_file}') + + if invalid_urls: + invalid_file = os.path.join(cli_parsed.d, 'invalid_urls.txt') + with open(invalid_file, 'w') as f: + for url, error in invalid_urls: + f.write(f'{url} # {error}\n') + print(f'[*] Invalid URLs written to: {invalid_file}') + + print(f'\n[*] Validation completed in {time.time() - start_time:.2f} seconds') + sys.exit(0) + + if cli_parsed.single: + if cli_parsed.web: + single_mode(cli_parsed) + if not cli_parsed.no_prompt: + open_file = open_file_input(cli_parsed) + if open_file: + files = glob.glob(os.path.join(cli_parsed.d, '*report.html')) + for f in files: + webbrowser.open(f) + class_info() + sys.exit() + class_info() + sys.exit() + + if cli_parsed.f is not None or cli_parsed.x is not None: + multi_mode(cli_parsed) + duplicate_check(cli_parsed) + + print('Finished in {0} seconds'.format(time.time() - start_time)) + + if not cli_parsed.no_prompt: + open_file = open_file_input(cli_parsed) + if open_file: + files = glob.glob(os.path.join(cli_parsed.d, '*report.html')) + for f in files: + webbrowser.open(f) + class_info() + sys.exit() + class_info() + sys.exit() diff --git a/reporting.py b/reporting.py new file mode 100644 index 00000000..2b6af0f0 --- /dev/null +++ b/reporting.py @@ -0,0 +1,494 @@ +import os +import sys +import urllib.parse + +BOOTSTRAP_CSS = '' + +try: + from rapidfuzz import fuzz +except ImportError: + print('[*] rapidfuzz not found.') + print('[*] Run pip list to verify installation!') + print('[*] Try: sudo apt install python3-rapidfuzz') + sys.exit() + + +def process_group( + data, group, toc, toc_table, page_num, section, + sectionid, html): + """Retreives a group from the full data, and creates toc stuff + + Args: + data (List): Full set of data containing all hosts + group (String): String representing group to process + toc (String): HTML for Table of Contents + toc_table (String): HTML for Table in ToC + page_num (int): Page number we're on in the report + section (String): Display name of the group + sectionid (String): Unique ID for ToC navigation + html (String): HTML for current page of report + + Returns: + List: Elements for category sorted and grouped + String: HTML representing ToC + String: HTML representing ToC Table + String: HTML representing current report page + """ + group_data = sorted([x for x in data if x.category == + group], key=lambda k: str(k.page_title)) + + grouped_elements = [] + if len(group_data) == 0: + return grouped_elements, toc, toc_table, html + if page_num == 0: + toc += ("
  • {1} (Page 1)
  • ").format( + sectionid, section) + else: + toc += ("
  • {2} (Page {0})
  • ").format( + str(page_num+1), sectionid, section) + + html += "

    {1}

    ".format(sectionid, section) + unknowns = [x for x in group_data if x.page_title == 'Unknown'] + group_data = [x for x in group_data if x.page_title != 'Unknown'] + while len(group_data) > 0: + test_element = group_data.pop(0) + temp = [x for x in group_data if fuzz.token_sort_ratio( + test_element.page_title, x.page_title) >= 70] + temp.append(test_element) + temp = sorted(temp, key=lambda k: k.page_title) + grouped_elements.extend(temp) + group_data = [x for x in group_data if fuzz.token_sort_ratio( + test_element.page_title, x.page_title) < 70] + + grouped_elements.extend(unknowns) + toc_table += ("{0}{1}").format(section, + str(len(grouped_elements))) + return grouped_elements, toc, toc_table, html + + +def sort_data_and_write(cli_parsed, data): + """Writes out reports for HTTP objects + + Args: + cli_parsed (TYPE): CLI Options + data (TYPE): Full set of data + """ + # We'll be using this number for our table of contents + total_results = len(data) + categories = [('highval', 'High Value Targets', 'highval'), + ('virtualization', 'Virtualization', 'virtualization'), + ('kvm', 'Remote Console/KVM', 'kvm'), + ('dirlist', 'Directory Listings', 'dirlist'), + ('cms', 'Content Management System (CMS)', 'cms'), + ('idrac', 'IDRAC/ILo/Management Interfaces', 'idrac'), + ('nas', 'Network Attached Storage (NAS)', 'nas'), + ('comms', 'Communications', 'comms'), + ('devops', 'Development Operations', 'devops'), + ('secops', 'Security Operations', 'secops'), + ('appops', 'Application Operations', 'appops'), + ('dataops', 'Data Operations', 'dataops'), + ('netdev', 'Network Devices', 'netdev'), + ('voip', 'Voice/Video over IP (VoIP)', 'voip'), + ('printer', 'Printers', 'printer'), + ('camera', 'Cameras', 'camera'), + ('infrastructure', 'Infrastructure', 'infrastructure'), + (None, 'Uncategorized', 'uncat'), + ('construction', 'Under Construction', 'construction'), + ('crap', 'Splash Pages', 'crap'), + ('empty', 'No Significant Content', 'empty'), + ('unauth', '401/403 Unauthorized', 'unauth'), + ('notfound', '404 Not Found', 'notfound'), + ('successfulLogin', 'Successful Logins', 'successfulLogin'), + ('identifiedLogin', 'Identified Logins', 'identifiedLogin'), + ('redirector', 'Redirecting Pages', 'redirector'), + ('badhost', 'Invalid Hostname', 'badhost'), + ('inerror', 'Internal Error', 'inerror'), + ('badreq', 'Bad Request', 'badreq'), + ('badgw', 'Bad Gateway', 'badgw'), + ('serviceunavailable', 'Service Unavailable', 'serviceunavailable'), + ] + if total_results == 0: + return + # Initialize stuff we need + pages = [] + toc = create_report_toc_head(cli_parsed.date, cli_parsed.time) + toc_table = "" + web_index_head = create_web_index_head( + cli_parsed.date, cli_parsed.time, cli_parsed) + table_head = create_table_head() + counter = 1 + csv_request_data = "Protocol,Port,Domain,URL,Resolved,Request Status,Title,Category,Default Creds,Screenshot Path, Source Path" + + # Generate and write json log of requests + for json_request in data: + url = urllib.parse.urlparse(json_request._remote_system) + + # CSV - PROTOCOL + csv_request_data += "\n" + url.scheme + "," + + # CSV - PORT + if url.port is not None: + csv_request_data += str(url.port) + "," + elif url.scheme == 'http': + csv_request_data += "80," + elif url.scheme == 'https': + csv_request_data += "443," + + # CSV - DOMAIN + try: + csv_request_data += url.hostname + "," + except TypeError: + print("Error when accessing a target's hostname (it's not existent)") + print("Possible bad url (improperly formatted) in the URL list.") + print("Fix your list and re-try. Killing EyeWitness....") + sys.exit(1) + + # CSV - URL + csv_request_data += json_request._remote_system + "," + + # CSV - RESOLVED + csv_request_data += json_request.resolved + "," + + # CSV - REQUEST STATUS + if json_request._error_state == None: + csv_request_data += "Successful," + else: + csv_request_data += json_request._error_state + "," + + # CSV - TITLE + try: + # get attribute safely + title = getattr(json_request, "_page_title", None) + if title is None: + title_text = "None" + else: + # ensure string, replace double-quotes so CSV remains valid + title_text = str(title).replace('"', '""') + csv_request_data += '"' + title_text + '",' + except (UnicodeDecodeError, UnicodeEncodeError, AttributeError, TypeError) as e: + # fallback for any encoding/None/attribute/concatenation issues + csv_request_data += '"!Error",' + + # CSV - CATEGORY + csv_request_data += str(json_request._category) + "," + # CSV - DEFAULT CREDS/Signature + csv_request_data += "\"" + str(json_request._default_creds) + "\"," + # CSV - SCREENSHOT PATH + csv_request_data += json_request._screenshot_path + "," + # CSV - Source Path + csv_request_data += json_request._source_path + + with open(os.path.join(cli_parsed.d, 'Requests.csv'), 'a') as f: + f.write(csv_request_data) + + # Pre-filter error entries + def key_lambda(k): + if k.error_state is None: + k.error_state = str(k.error_state) + if k.page_title is None: + k.page_title = str(k.page_title) + return (k.error_state, k.page_title) + errors = sorted([x for x in data if (x is not None) and (x.error_state is not None)], + key=key_lambda) + data[:] = [x for x in data if x.error_state is None] + data = sorted(data, key=lambda k: str(k.page_title)) + html = u"" + # Loop over our categories and populate HTML + for cat in categories: + grouped, toc, toc_table, html = process_group( + data, cat[0], toc, toc_table, len(pages), cat[1], cat[2], html) + if len(grouped) > 0: + html += table_head + pcount = 0 + for obj in grouped: + pcount += 1 + html += obj.create_table_html() + if (counter % cli_parsed.results == 0) or (counter == (total_results) - 1): + html = (web_index_head + "EW_REPLACEME" + html + + "

    ") + pages.append(html) + html = u"" + if pcount < len(grouped): + html += table_head + counter += 1 + if len(grouped) > 0 and counter - 1 % cli_parsed.results != 0: + html += "
    " + + # Add our errors here (at the very very end) + if len(errors) > 0: + html += '

    Errors

    ' + html += table_head + for obj in errors: + html += obj.create_table_html() + if (counter % cli_parsed.results == 0) or (counter == (total_results)): + html = (web_index_head + "EW_REPLACEME" + html + + "
    ") + pages.append(html) + html = u"" + table_head + counter += 1 + + # Close out any stuff thats hanging + toc += "" + toc_table += "Errors{0}".format( + str(len(errors))) + toc_table += "Total{0}".format(total_results) + toc_table += "" + + if (html != u"") and (counter - total_results != 0): + html = (web_index_head + "EW_REPLACEME" + html + + "
    ") + pages.append(html) + + toc = "
    {0}

    {1}

    ".format(toc, toc_table) + + if len(pages) == 1: + with open(os.path.join(cli_parsed.d, 'report.html'), 'a', encoding='utf-8') as f: + f.write(toc) + f.write(pages[0].replace('EW_REPLACEME', '')) + f.write("\n") + else: + num_pages = len(pages) + 1 + bottom_text = "\n

    " + bottom_text += (" Page 1") + skip_last_dummy = False + # Generate our header/footer data here + for i in range(2, num_pages): + badd_page = "
    EW_REPLACEME\n \n \n \n
    Web Request InfoWeb Screenshot

    " + if badd_page in pages[i-1]: + skip_last_dummy = True + pass + else: + bottom_text += ( + " Page {0}").format(str(i)) + bottom_text += "\n" + top_text = bottom_text + # Generate our next/previous page buttons + if skip_last_dummy: + amount = len(pages) - 1 + else: + amount = len(pages) + for i in range(0, amount): + headfoot = "

    Page {0}

    ".format(str(i+1)) + headfoot += "
    " + if i == 0: + headfoot += (" Next Page " + "
    ") + elif i == amount - 1: + if i == 1: + headfoot += (" Previous Page " + "") + else: + headfoot += (" Previous Page " + "").format(str(i)) + elif i == 1: + headfoot += ("Previous Page " + " Next Page" + "").format(str(i+2)) + else: + headfoot += ("Previous Page" + "  Next Page" + "").format(str(i), str(i+2)) + # Finalize our pages by replacing placeholder stuff and writing out + # the headers/footers + pages[i] = pages[i].replace( + 'EW_REPLACEME', headfoot + top_text) + bottom_text + '
    ' + headfoot + '' + + # Write out our report to disk! + if len(pages) == 0: + return + with open(os.path.join(cli_parsed.d, 'report.html'), 'a', encoding='utf-8') as f: + f.write(toc) + f.write(pages[0]) + write_out = len(pages) + for i in range(2, write_out + 1): + bad_page = "\n \n \n \n
    Web Request InfoWeb Screenshot

    \n

    \n \n Web Request Info\n Web Screenshot\n
    " + if (bad_page in pages[i-1]) or (badd_page2 in pages[i-1]): + pass + else: + with open(os.path.join(cli_parsed.d, 'report_page{0}.html'.format(str(i))), 'w', encoding='utf-8') as f: + f.write(pages[i - 1]) + + +def create_web_index_head(date, time, cli_parsed): + """Creates the header for a http report + + Args: + date (String): Date of report start + time (String): Time of report start + + Returns: + String: HTTP Report Start html + """ + + html = """ + + """ + if not cli_parsed.no_bootstrap: + html += BOOTSTRAP_CSS + "\n" + + html += """ + + EyeWitness Report + + + + +
    +
    Report Generated on {0} at {1}
    """.format(date, time) + + return (html) + + +def search_index_head(): + return (""" + + + EyeWitness Report + + + + +
    + """) + + +def create_table_head(): + return (""" + + + + """) + + +def create_report_toc_head(date, time): + return (""" + + EyeWitness Report Table of Contents + +

    Table of Contents

    """) + + +def search_report(cli_parsed, data, search_term): + pages = [] + web_index_head = search_index_head() + table_head = create_table_head() + counter = 1 + + data[:] = [x for x in data if x.error_state is None] + data = sorted(data, key=lambda k: k.page_title) + html = u"" + + # Add our errors here (at the very very end) + html += '

    Results for {0}

    '.format(search_term) + html += table_head + for obj in data: + html += obj.create_table_html() + if counter % cli_parsed.results == 0: + html = (web_index_head + "EW_REPLACEME" + html + + "
    Web Request InfoWeb Screenshot

    ") + pages.append(html) + html = u"" + table_head + counter += 1 + + if html != u"": + html = (web_index_head + html + "
    ") + pages.append(html) + + if len(pages) == 1: + with open(os.path.join(cli_parsed.d, 'search.html'), 'a', encoding='utf-8') as f: + f.write(pages[0].replace('EW_REPLACEME', '')) + f.write("\n") + else: + num_pages = len(pages) + 1 + bottom_text = "\n

    " + bottom_text += ("
    Page 1") + # Generate our header/footer data here + for i in range(2, num_pages): + bottom_text += (" Page {0}").format( + str(i)) + bottom_text += "
    \n" + top_text = bottom_text + # Generate our next/previous page buttons + for i in range(0, len(pages)): + headfoot = "
    " + if i == 0: + headfoot += (" Next Page " + "
    ") + elif i == len(pages) - 1: + if i == 1: + headfoot += (" Previous Page " + "
    ") + else: + headfoot += (" Previous Page " + "
    ").format(str(i)) + elif i == 1: + headfoot += ("Previous Page " + " Next Page" + "
    ").format(str(i+2)) + else: + headfoot += ("Previous Page" + "  Next Page" + "").format(str(i), str(i+2)) + # Finalize our pages by replacing placeholder stuff and writing out + # the headers/footers + pages[i] = pages[i].replace( + 'EW_REPLACEME', headfoot + top_text) + bottom_text + '
    ' + headfoot + '' + + # Write out our report to disk! + if len(pages) == 0: + return + with open(os.path.join(cli_parsed.d, 'search.html'), 'a', encoding='utf-8') as f: + try: + f.write(pages[0]) + except UnicodeEncodeError: + f.write(pages[0].encode('utf-8')) + for i in range(2, len(pages) + 1): + with open(os.path.join(cli_parsed.d, 'search_page{0}.html'.format(str(i))), 'w', encoding='utf-8') as f: + try: + f.write(pages[i - 1]) + except UnicodeEncodeError: + f.write(pages[i - 1].encode('utf-8')) From 0f466a9176c24a567cefd0bce834739b7ac048eb Mon Sep 17 00:00:00 2001 From: zemik Date: Thu, 5 Mar 2026 21:08:43 +0300 Subject: [PATCH 2/5] Update EyeWitness.py From 8e3b3dd67bec207f47261ae056efdb6ec44c814f Mon Sep 17 00:00:00 2001 From: zemik Date: Thu, 5 Mar 2026 21:09:30 +0300 Subject: [PATCH 3/5] Update reporting.py --- Python/modules/reporting.py | 53 ++++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/Python/modules/reporting.py b/Python/modules/reporting.py index 6c105b95..2b6af0f0 100644 --- a/Python/modules/reporting.py +++ b/Python/modules/reporting.py @@ -2,6 +2,8 @@ import sys import urllib.parse +BOOTSTRAP_CSS = '' + try: from rapidfuzz import fuzz except ImportError: @@ -32,7 +34,8 @@ def process_group( String: HTML representing ToC Table String: HTML representing current report page """ - group_data = sorted([x for x in data if x.category == group], key=lambda k: str(k.page_title)) + group_data = sorted([x for x in data if x.category == + group], key=lambda k: str(k.page_title)) grouped_elements = [] if len(group_data) == 0: @@ -63,7 +66,6 @@ def process_group( return grouped_elements, toc, toc_table, html - def sort_data_and_write(cli_parsed, data): """Writes out reports for HTTP objects @@ -74,8 +76,8 @@ def sort_data_and_write(cli_parsed, data): # We'll be using this number for our table of contents total_results = len(data) categories = [('highval', 'High Value Targets', 'highval'), - ('virtualization', 'Virtualization','virtualization'), - ('kvm','Remote Console/KVM','kvm'), + ('virtualization', 'Virtualization', 'virtualization'), + ('kvm', 'Remote Console/KVM', 'kvm'), ('dirlist', 'Directory Listings', 'dirlist'), ('cms', 'Content Management System (CMS)', 'cms'), ('idrac', 'IDRAC/ILo/Management Interfaces', 'idrac'), @@ -111,7 +113,8 @@ def sort_data_and_write(cli_parsed, data): pages = [] toc = create_report_toc_head(cli_parsed.date, cli_parsed.time) toc_table = "" - web_index_head = create_web_index_head(cli_parsed.date, cli_parsed.time) + web_index_head = create_web_index_head( + cli_parsed.date, cli_parsed.time, cli_parsed) table_head = create_table_head() counter = 1 csv_request_data = "Protocol,Port,Domain,URL,Resolved,Request Status,Title,Category,Default Creds,Screenshot Path, Source Path" @@ -122,7 +125,7 @@ def sort_data_and_write(cli_parsed, data): # CSV - PROTOCOL csv_request_data += "\n" + url.scheme + "," - + # CSV - PORT if url.port is not None: csv_request_data += str(url.port) + "," @@ -130,7 +133,7 @@ def sort_data_and_write(cli_parsed, data): csv_request_data += "80," elif url.scheme == 'https': csv_request_data += "443," - + # CSV - DOMAIN try: csv_request_data += url.hostname + "," @@ -139,10 +142,10 @@ def sort_data_and_write(cli_parsed, data): print("Possible bad url (improperly formatted) in the URL list.") print("Fix your list and re-try. Killing EyeWitness....") sys.exit(1) - + # CSV - URL csv_request_data += json_request._remote_system + "," - + # CSV - RESOLVED csv_request_data += json_request.resolved + "," @@ -151,7 +154,7 @@ def sort_data_and_write(cli_parsed, data): csv_request_data += "Successful," else: csv_request_data += json_request._error_state + "," - + # CSV - TITLE try: # get attribute safely @@ -165,12 +168,12 @@ def sort_data_and_write(cli_parsed, data): except (UnicodeDecodeError, UnicodeEncodeError, AttributeError, TypeError) as e: # fallback for any encoding/None/attribute/concatenation issues csv_request_data += '"!Error",' - + # CSV - CATEGORY csv_request_data += str(json_request._category) + "," # CSV - DEFAULT CREDS/Signature csv_request_data += "\"" + str(json_request._default_creds) + "\"," - # CSV - SCREENSHOT PATH + # CSV - SCREENSHOT PATH csv_request_data += json_request._screenshot_path + "," # CSV - Source Path csv_request_data += json_request._source_path @@ -186,7 +189,7 @@ def key_lambda(k): k.page_title = str(k.page_title) return (k.error_state, k.page_title) errors = sorted([x for x in data if (x is not None) and (x.error_state is not None)], - key=key_lambda) + key=key_lambda) data[:] = [x for x in data if x.error_state is None] data = sorted(data, key=lambda k: str(k.page_title)) html = u"" @@ -200,7 +203,7 @@ def key_lambda(k): for obj in grouped: pcount += 1 html += obj.create_table_html() - if (counter % cli_parsed.results == 0) or (counter == (total_results) -1): + if (counter % cli_parsed.results == 0) or (counter == (total_results) - 1): html = (web_index_head + "EW_REPLACEME" + html + "

    ") pages.append(html) @@ -255,7 +258,8 @@ def key_lambda(k): skip_last_dummy = True pass else: - bottom_text += (" Page {0}").format(str(i)) + bottom_text += ( + " Page {0}").format(str(i)) bottom_text += "\n" top_text = bottom_text # Generate our next/previous page buttons @@ -306,7 +310,7 @@ def key_lambda(k): f.write(pages[i - 1]) -def create_web_index_head(date, time): +def create_web_index_head(date, time, cli_parsed): """Creates the header for a http report Args: @@ -316,9 +320,14 @@ def create_web_index_head(date, time): Returns: String: HTTP Report Start html """ - return (""" - - + + html = """ + + """ + if not cli_parsed.no_bootstrap: + html += BOOTSTRAP_CSS + "\n" + + html += """ EyeWitness Report @@ -345,7 +354,7 @@ def create_web_index_head(date, time): break; }} }}; - + function leftArrow(){{ $('#previous')[0].click(); }}; @@ -358,7 +367,9 @@ def create_web_index_head(date, time):
    -
    Report Generated on {0} at {1}
    """).format(date, time) +
    Report Generated on {0} at {1}
    """.format(date, time) + + return (html) def search_index_head(): From 7deb692bc13d74f72980090efee0c0def9de45a3 Mon Sep 17 00:00:00 2001 From: zemik Date: Thu, 5 Mar 2026 21:10:04 +0300 Subject: [PATCH 4/5] Delete reporting.py --- reporting.py | 494 --------------------------------------------------- 1 file changed, 494 deletions(-) delete mode 100644 reporting.py diff --git a/reporting.py b/reporting.py deleted file mode 100644 index 2b6af0f0..00000000 --- a/reporting.py +++ /dev/null @@ -1,494 +0,0 @@ -import os -import sys -import urllib.parse - -BOOTSTRAP_CSS = '' - -try: - from rapidfuzz import fuzz -except ImportError: - print('[*] rapidfuzz not found.') - print('[*] Run pip list to verify installation!') - print('[*] Try: sudo apt install python3-rapidfuzz') - sys.exit() - - -def process_group( - data, group, toc, toc_table, page_num, section, - sectionid, html): - """Retreives a group from the full data, and creates toc stuff - - Args: - data (List): Full set of data containing all hosts - group (String): String representing group to process - toc (String): HTML for Table of Contents - toc_table (String): HTML for Table in ToC - page_num (int): Page number we're on in the report - section (String): Display name of the group - sectionid (String): Unique ID for ToC navigation - html (String): HTML for current page of report - - Returns: - List: Elements for category sorted and grouped - String: HTML representing ToC - String: HTML representing ToC Table - String: HTML representing current report page - """ - group_data = sorted([x for x in data if x.category == - group], key=lambda k: str(k.page_title)) - - grouped_elements = [] - if len(group_data) == 0: - return grouped_elements, toc, toc_table, html - if page_num == 0: - toc += ("
  • {1} (Page 1)
  • ").format( - sectionid, section) - else: - toc += ("
  • {2} (Page {0})
  • ").format( - str(page_num+1), sectionid, section) - - html += "

    {1}

    ".format(sectionid, section) - unknowns = [x for x in group_data if x.page_title == 'Unknown'] - group_data = [x for x in group_data if x.page_title != 'Unknown'] - while len(group_data) > 0: - test_element = group_data.pop(0) - temp = [x for x in group_data if fuzz.token_sort_ratio( - test_element.page_title, x.page_title) >= 70] - temp.append(test_element) - temp = sorted(temp, key=lambda k: k.page_title) - grouped_elements.extend(temp) - group_data = [x for x in group_data if fuzz.token_sort_ratio( - test_element.page_title, x.page_title) < 70] - - grouped_elements.extend(unknowns) - toc_table += ("{0}{1}").format(section, - str(len(grouped_elements))) - return grouped_elements, toc, toc_table, html - - -def sort_data_and_write(cli_parsed, data): - """Writes out reports for HTTP objects - - Args: - cli_parsed (TYPE): CLI Options - data (TYPE): Full set of data - """ - # We'll be using this number for our table of contents - total_results = len(data) - categories = [('highval', 'High Value Targets', 'highval'), - ('virtualization', 'Virtualization', 'virtualization'), - ('kvm', 'Remote Console/KVM', 'kvm'), - ('dirlist', 'Directory Listings', 'dirlist'), - ('cms', 'Content Management System (CMS)', 'cms'), - ('idrac', 'IDRAC/ILo/Management Interfaces', 'idrac'), - ('nas', 'Network Attached Storage (NAS)', 'nas'), - ('comms', 'Communications', 'comms'), - ('devops', 'Development Operations', 'devops'), - ('secops', 'Security Operations', 'secops'), - ('appops', 'Application Operations', 'appops'), - ('dataops', 'Data Operations', 'dataops'), - ('netdev', 'Network Devices', 'netdev'), - ('voip', 'Voice/Video over IP (VoIP)', 'voip'), - ('printer', 'Printers', 'printer'), - ('camera', 'Cameras', 'camera'), - ('infrastructure', 'Infrastructure', 'infrastructure'), - (None, 'Uncategorized', 'uncat'), - ('construction', 'Under Construction', 'construction'), - ('crap', 'Splash Pages', 'crap'), - ('empty', 'No Significant Content', 'empty'), - ('unauth', '401/403 Unauthorized', 'unauth'), - ('notfound', '404 Not Found', 'notfound'), - ('successfulLogin', 'Successful Logins', 'successfulLogin'), - ('identifiedLogin', 'Identified Logins', 'identifiedLogin'), - ('redirector', 'Redirecting Pages', 'redirector'), - ('badhost', 'Invalid Hostname', 'badhost'), - ('inerror', 'Internal Error', 'inerror'), - ('badreq', 'Bad Request', 'badreq'), - ('badgw', 'Bad Gateway', 'badgw'), - ('serviceunavailable', 'Service Unavailable', 'serviceunavailable'), - ] - if total_results == 0: - return - # Initialize stuff we need - pages = [] - toc = create_report_toc_head(cli_parsed.date, cli_parsed.time) - toc_table = "" - web_index_head = create_web_index_head( - cli_parsed.date, cli_parsed.time, cli_parsed) - table_head = create_table_head() - counter = 1 - csv_request_data = "Protocol,Port,Domain,URL,Resolved,Request Status,Title,Category,Default Creds,Screenshot Path, Source Path" - - # Generate and write json log of requests - for json_request in data: - url = urllib.parse.urlparse(json_request._remote_system) - - # CSV - PROTOCOL - csv_request_data += "\n" + url.scheme + "," - - # CSV - PORT - if url.port is not None: - csv_request_data += str(url.port) + "," - elif url.scheme == 'http': - csv_request_data += "80," - elif url.scheme == 'https': - csv_request_data += "443," - - # CSV - DOMAIN - try: - csv_request_data += url.hostname + "," - except TypeError: - print("Error when accessing a target's hostname (it's not existent)") - print("Possible bad url (improperly formatted) in the URL list.") - print("Fix your list and re-try. Killing EyeWitness....") - sys.exit(1) - - # CSV - URL - csv_request_data += json_request._remote_system + "," - - # CSV - RESOLVED - csv_request_data += json_request.resolved + "," - - # CSV - REQUEST STATUS - if json_request._error_state == None: - csv_request_data += "Successful," - else: - csv_request_data += json_request._error_state + "," - - # CSV - TITLE - try: - # get attribute safely - title = getattr(json_request, "_page_title", None) - if title is None: - title_text = "None" - else: - # ensure string, replace double-quotes so CSV remains valid - title_text = str(title).replace('"', '""') - csv_request_data += '"' + title_text + '",' - except (UnicodeDecodeError, UnicodeEncodeError, AttributeError, TypeError) as e: - # fallback for any encoding/None/attribute/concatenation issues - csv_request_data += '"!Error",' - - # CSV - CATEGORY - csv_request_data += str(json_request._category) + "," - # CSV - DEFAULT CREDS/Signature - csv_request_data += "\"" + str(json_request._default_creds) + "\"," - # CSV - SCREENSHOT PATH - csv_request_data += json_request._screenshot_path + "," - # CSV - Source Path - csv_request_data += json_request._source_path - - with open(os.path.join(cli_parsed.d, 'Requests.csv'), 'a') as f: - f.write(csv_request_data) - - # Pre-filter error entries - def key_lambda(k): - if k.error_state is None: - k.error_state = str(k.error_state) - if k.page_title is None: - k.page_title = str(k.page_title) - return (k.error_state, k.page_title) - errors = sorted([x for x in data if (x is not None) and (x.error_state is not None)], - key=key_lambda) - data[:] = [x for x in data if x.error_state is None] - data = sorted(data, key=lambda k: str(k.page_title)) - html = u"" - # Loop over our categories and populate HTML - for cat in categories: - grouped, toc, toc_table, html = process_group( - data, cat[0], toc, toc_table, len(pages), cat[1], cat[2], html) - if len(grouped) > 0: - html += table_head - pcount = 0 - for obj in grouped: - pcount += 1 - html += obj.create_table_html() - if (counter % cli_parsed.results == 0) or (counter == (total_results) - 1): - html = (web_index_head + "EW_REPLACEME" + html + - "

    ") - pages.append(html) - html = u"" - if pcount < len(grouped): - html += table_head - counter += 1 - if len(grouped) > 0 and counter - 1 % cli_parsed.results != 0: - html += "
    " - - # Add our errors here (at the very very end) - if len(errors) > 0: - html += '

    Errors

    ' - html += table_head - for obj in errors: - html += obj.create_table_html() - if (counter % cli_parsed.results == 0) or (counter == (total_results)): - html = (web_index_head + "EW_REPLACEME" + html + - "
    ") - pages.append(html) - html = u"" + table_head - counter += 1 - - # Close out any stuff thats hanging - toc += "" - toc_table += "Errors{0}".format( - str(len(errors))) - toc_table += "Total{0}".format(total_results) - toc_table += "" - - if (html != u"") and (counter - total_results != 0): - html = (web_index_head + "EW_REPLACEME" + html + - "
    ") - pages.append(html) - - toc = "
    {0}

    {1}

    ".format(toc, toc_table) - - if len(pages) == 1: - with open(os.path.join(cli_parsed.d, 'report.html'), 'a', encoding='utf-8') as f: - f.write(toc) - f.write(pages[0].replace('EW_REPLACEME', '')) - f.write("\n") - else: - num_pages = len(pages) + 1 - bottom_text = "\n

    " - bottom_text += (" Page 1") - skip_last_dummy = False - # Generate our header/footer data here - for i in range(2, num_pages): - badd_page = "
    EW_REPLACEME\n \n \n \n
    Web Request InfoWeb Screenshot

    " - if badd_page in pages[i-1]: - skip_last_dummy = True - pass - else: - bottom_text += ( - " Page {0}").format(str(i)) - bottom_text += "
    \n" - top_text = bottom_text - # Generate our next/previous page buttons - if skip_last_dummy: - amount = len(pages) - 1 - else: - amount = len(pages) - for i in range(0, amount): - headfoot = "

    Page {0}

    ".format(str(i+1)) - headfoot += "
    " - if i == 0: - headfoot += (" Next Page " - "
    ") - elif i == amount - 1: - if i == 1: - headfoot += (" Previous Page " - "") - else: - headfoot += (" Previous Page " - "").format(str(i)) - elif i == 1: - headfoot += ("Previous Page " - " Next Page" - "").format(str(i+2)) - else: - headfoot += ("Previous Page" - "  Next Page" - "").format(str(i), str(i+2)) - # Finalize our pages by replacing placeholder stuff and writing out - # the headers/footers - pages[i] = pages[i].replace( - 'EW_REPLACEME', headfoot + top_text) + bottom_text + '
    ' + headfoot + '' - - # Write out our report to disk! - if len(pages) == 0: - return - with open(os.path.join(cli_parsed.d, 'report.html'), 'a', encoding='utf-8') as f: - f.write(toc) - f.write(pages[0]) - write_out = len(pages) - for i in range(2, write_out + 1): - bad_page = "\n \n \n \n
    Web Request InfoWeb Screenshot

    \n

    \n \n Web Request Info\n Web Screenshot\n
    " - if (bad_page in pages[i-1]) or (badd_page2 in pages[i-1]): - pass - else: - with open(os.path.join(cli_parsed.d, 'report_page{0}.html'.format(str(i))), 'w', encoding='utf-8') as f: - f.write(pages[i - 1]) - - -def create_web_index_head(date, time, cli_parsed): - """Creates the header for a http report - - Args: - date (String): Date of report start - time (String): Time of report start - - Returns: - String: HTTP Report Start html - """ - - html = """ - - """ - if not cli_parsed.no_bootstrap: - html += BOOTSTRAP_CSS + "\n" - - html += """ - - EyeWitness Report - - - - -
    -
    Report Generated on {0} at {1}
    """.format(date, time) - - return (html) - - -def search_index_head(): - return (""" - - - EyeWitness Report - - - - -
    - """) - - -def create_table_head(): - return (""" - - - - """) - - -def create_report_toc_head(date, time): - return (""" - - EyeWitness Report Table of Contents - -

    Table of Contents

    """) - - -def search_report(cli_parsed, data, search_term): - pages = [] - web_index_head = search_index_head() - table_head = create_table_head() - counter = 1 - - data[:] = [x for x in data if x.error_state is None] - data = sorted(data, key=lambda k: k.page_title) - html = u"" - - # Add our errors here (at the very very end) - html += '

    Results for {0}

    '.format(search_term) - html += table_head - for obj in data: - html += obj.create_table_html() - if counter % cli_parsed.results == 0: - html = (web_index_head + "EW_REPLACEME" + html + - "
    Web Request InfoWeb Screenshot

    ") - pages.append(html) - html = u"" + table_head - counter += 1 - - if html != u"": - html = (web_index_head + html + "
    ") - pages.append(html) - - if len(pages) == 1: - with open(os.path.join(cli_parsed.d, 'search.html'), 'a', encoding='utf-8') as f: - f.write(pages[0].replace('EW_REPLACEME', '')) - f.write("\n") - else: - num_pages = len(pages) + 1 - bottom_text = "\n

    " - bottom_text += ("
    Page 1") - # Generate our header/footer data here - for i in range(2, num_pages): - bottom_text += (" Page {0}").format( - str(i)) - bottom_text += "
    \n" - top_text = bottom_text - # Generate our next/previous page buttons - for i in range(0, len(pages)): - headfoot = "
    " - if i == 0: - headfoot += (" Next Page " - "
    ") - elif i == len(pages) - 1: - if i == 1: - headfoot += (" Previous Page " - "
    ") - else: - headfoot += (" Previous Page " - "
    ").format(str(i)) - elif i == 1: - headfoot += ("Previous Page " - " Next Page" - "
    ").format(str(i+2)) - else: - headfoot += ("Previous Page" - "  Next Page" - "").format(str(i), str(i+2)) - # Finalize our pages by replacing placeholder stuff and writing out - # the headers/footers - pages[i] = pages[i].replace( - 'EW_REPLACEME', headfoot + top_text) + bottom_text + '
    ' + headfoot + '' - - # Write out our report to disk! - if len(pages) == 0: - return - with open(os.path.join(cli_parsed.d, 'search.html'), 'a', encoding='utf-8') as f: - try: - f.write(pages[0]) - except UnicodeEncodeError: - f.write(pages[0].encode('utf-8')) - for i in range(2, len(pages) + 1): - with open(os.path.join(cli_parsed.d, 'search_page{0}.html'.format(str(i))), 'w', encoding='utf-8') as f: - try: - f.write(pages[i - 1]) - except UnicodeEncodeError: - f.write(pages[i - 1].encode('utf-8')) From 3314455a438445d1e3b257ca9ba53e0b10ff7cc0 Mon Sep 17 00:00:00 2001 From: zemik Date: Thu, 5 Mar 2026 21:14:15 +0300 Subject: [PATCH 5/5] Delete EyeWitness.py --- EyeWitness.py | 595 -------------------------------------------------- 1 file changed, 595 deletions(-) delete mode 100644 EyeWitness.py diff --git a/EyeWitness.py b/EyeWitness.py deleted file mode 100644 index f01ec004..00000000 --- a/EyeWitness.py +++ /dev/null @@ -1,595 +0,0 @@ -#!/usr/bin/env python3 -# PYTHON_ARGCOMPLETE_OK - -import argparse -try: - import argcomplete - from argcomplete.completers import FilesCompleter - HAS_ARGCOMPLETE = True -except ImportError: - HAS_ARGCOMPLETE = False - FilesCompleter = None -import glob -import os -import re -import shutil -import signal -import sys -import time -import webbrowser - -from modules import db_manager -from modules import objects -from modules import selenium_module -from modules.helpers import class_info -from modules.helpers import create_folders_css -from modules.helpers import default_creds_category -from modules.helpers import do_jitter -from modules.helpers import target_creator -from modules.helpers import title_screen -from modules.helpers import open_file_input -from modules.helpers import resolve_host -from modules.helpers import duplicate_check -from modules.reporting import create_table_head -from modules.reporting import create_web_index_head -from modules.reporting import sort_data_and_write -from multiprocessing import Manager -from multiprocessing import Process -from multiprocessing import current_process -import multiprocessing -from modules.platform_utils import PlatformManager, setup_virtual_display -from modules.resource_monitor import ResourceMonitor, check_disk_space, get_system_info -from modules.troubleshooting import get_progress_message - -# Initialize platform manager -platform_mgr = PlatformManager() - - -def create_cli_parser(): - parser = argparse.ArgumentParser( - add_help=False, description="EyeWitness is a tool used to capture\ - screenshots from a list of URLs") - parser.add_argument('-h', '-?', '--h', '-help', - '--help', action="store_true", help=argparse.SUPPRESS) - protocols = parser.add_argument_group('Protocols') - protocols.add_argument('--web', default=True, action='store_true', - help='HTTP Screenshot using Selenium') - - input_options = parser.add_argument_group('Input Options') - f_arg = input_options.add_argument('-f', metavar='Filename', default=None, - help='Line-separated file containing URLs to \ - capture') - if HAS_ARGCOMPLETE and FilesCompleter: - f_arg.completer = FilesCompleter() - x_arg = input_options.add_argument('-x', metavar='Filename.xml', default=None, - help='Nmap XML or .Nessus file') - if HAS_ARGCOMPLETE and FilesCompleter: - x_arg.completer = FilesCompleter(allowednames='*.xml *.nessus', directories=True) - input_options.add_argument('--single', metavar='Single URL', default=None, - help='Single URL/Host to capture') - input_options.add_argument('--no-dns', default=False, action='store_true', - help='Skip DNS resolution when connecting to \ - websites') - - timing_options = parser.add_argument_group('Timing Options') - timing_options.add_argument('--timeout', metavar='Timeout', default=7, type=int, - help='Maximum number of seconds to wait while\ - requesting a web page (Default: 7)') - timing_options.add_argument('--jitter', metavar='# of Seconds', default=0, - type=int, help='Randomize URLs and add a random\ - delay between requests') - timing_options.add_argument('--delay', metavar='# of Seconds', default=0, - type=int, help='Delay between the opening of the navigator and taking the screenshot') - # Calculate default threads based on CPU cores (2 threads per core, max 20) - default_threads = min(multiprocessing.cpu_count() * 2, 20) - timing_options.add_argument('--threads', metavar='# of Threads', default=default_threads, - type=int, help=f'Number of threads to use (default: {default_threads} based on CPU cores)') - timing_options.add_argument('--max-retries', default=1, metavar='Max retries on \ - a timeout'.replace(' ', ''), type=int, - help='Max retries on timeouts') - - report_options = parser.add_argument_group('Report Output Options') - d_arg = report_options.add_argument('-d', metavar='Output Directory', - default=None, - help='Output directory for screenshots and reports') - if HAS_ARGCOMPLETE and FilesCompleter: - from argcomplete.completers import DirectoriesCompleter - d_arg.completer = DirectoriesCompleter() - report_options.add_argument('--results', metavar='Results/Page', - default=25, type=int, help='Number of results per report page (default: 25)') - report_options.add_argument('--no-prompt', default=False, - action='store_true', - help='Skip prompt to open report when complete') - report_options.add_argument('--no-clear', default=True, - action='store_true', - help='Don\'t clear screen buffer (default behavior)') - report_options.add_argument("-b", "--no-bootstrap", - action="store_true", - help="Do not include Bootstrap CSS in generated HTML reports") - - http_options = parser.add_argument_group('Web Options') - http_options.add_argument('--user-agent', metavar='User Agent', - default=None, help='User Agent to use for all\ - requests') - http_options.add_argument('--difference', metavar='Difference Threshold', - default=50, type=int, help='Difference threshold\ - when determining if user agent requests are\ - close \"enough\" (Default: 50)') - http_options.add_argument('--proxy-ip', metavar='127.0.0.1', default=None, - help='IP of web proxy to go through') - http_options.add_argument('--proxy-port', metavar='8080', default=None, - type=int, help='Port of web proxy to go through') - http_options.add_argument('--proxy-type', metavar='socks5', default="http", - help='Proxy type (socks5/http)') - http_options.add_argument('--show-selenium', default=False, - action='store_true', help='Show display for selenium') - http_options.add_argument('--resolve', default=False, - action='store_true', help=("Resolve IP/Hostname" - " for targets")) - http_options.add_argument('--add-http-ports', default=[], - type=lambda s:[str(i) for i in s.split(",")], - help=("Comma-separated additional port(s) to assume " - "are http (e.g. '8018,8028')")) - http_options.add_argument('--add-https-ports', default=[], - type=lambda s:[str(i) for i in s.split(",")], - help=("Comma-separated additional port(s) to assume " - "are https (e.g. '8018,8028')")) - http_options.add_argument('--only-ports', default=[], - type=lambda s:[int(i) for i in s.split(",")], - help=("Comma-separated list of exclusive ports to " - "use (e.g. '80,8080')")) - http_options.add_argument('--prepend-https', default=False, action='store_true', - help='Prepend http:// and https:// to URLs without either') - http_options.add_argument('--validate-urls', default=False, action='store_true', - help='Only validate URLs without taking screenshots') - http_options.add_argument('--skip-validation', default=False, action='store_true', - help='Skip URL validation checks (use with caution)') - http_options.add_argument('--selenium-log-path', default='./chromedriver.log', action='store', - help='Selenium ChromeDriver log path') - http_options.add_argument('--cookies', metavar='key1=value1,key2=value2', default=None, - help='Additional cookies to add to the request') - http_options.add_argument('--width', metavar="1366", default=1366,type=int, - help='Screenshot window image width size. 600-7680 (eg. 1920)') - http_options.add_argument('--height', metavar="768", default=768, type=int, - help='Screenshot window image height size. 400-4320 (eg. 1080)') - - resume_options = parser.add_argument_group('Resume Options') - resume_options.add_argument('--resume', metavar='ew.db', - default=None, help='Path to db file if you want to resume') - - config_options = parser.add_argument_group('Configuration Options') - config_arg = config_options.add_argument('--config', metavar='config.json', default=None, - help='Configuration file path') - if HAS_ARGCOMPLETE and FilesCompleter: - config_arg.completer = FilesCompleter(allowednames='*.json', directories=True) - config_options.add_argument('--create-config', action='store_true', - help='Create sample configuration file') - - # Enable bash tab completion if argcomplete is available - if HAS_ARGCOMPLETE: - argcomplete.autocomplete(parser) - - args = parser.parse_args() - args.date = time.strftime('%Y/%m/%d') - args.time = time.strftime('%H:%M:%S') - - # Handle config creation - if args.create_config: - from modules.config import ConfigManager - ConfigManager.create_sample_config() - sys.exit(0) - - # Load config file if specified or found - from modules.config import ConfigManager - config = ConfigManager.load_config(args.config) - args = ConfigManager.apply_config_to_args(args, config) - - if args.h: - parser.print_help() - sys.exit() - - if args.f is None and args.single is None and args.resume is None and args.x is None: - print("[!] Error: No input specified") - print("[*] You must provide one of the following:") - print(" - URL file: -f urls.txt") - print(" - Single URL: --single http://example.com") - print(" - XML file: -x nmap.xml") - print(" - Resume scan: --resume") - print("[*] Run 'EyeWitness.py -h' for full help") - sys.exit(1) - - if ((args.f is not None) and not os.path.isfile(args.f)) or ((args.x is not None) and not os.path.isfile(args.x)): - from modules.troubleshooting import get_error_guidance - if args.f and not os.path.isfile(args.f): - print(get_error_guidance('file_not_found', path=args.f)) - if args.x and not os.path.isfile(args.x): - print(get_error_guidance('file_not_found', path=args.x)) - sys.exit(1) - - if args.width < 600 or args.width >7680: - print("\n[*] Error: Specify a width >= 600 and <= 7680, for example 1920.\n") - parser.print_help() - sys.exit() - - if args.height < 400 or args.height >4320: - print("\n[*] Error: Specify a height >= 400 and <= 4320, for example, 1080.\n") - parser.print_help() - sys.exit() - - if args.d is not None: - if args.d.startswith('/') or re.match( - '^[A-Za-z]:\\\\', args.d) is not None: - args.d = args.d.rstrip('/') - args.d = args.d.rstrip('\\') - else: - args.d = os.path.join(os.getcwd(), args.d) - - if not os.access(os.path.dirname(args.d), os.W_OK): - print('[*] Error: Please provide a valid folder name/path') - parser.print_help() - sys.exit() - else: - if not args.no_prompt: - if os.path.isdir(args.d): - overwrite_dir = input(('Directory Exists! Do you want to ' - 'overwrite? [y/n] ')) - overwrite_dir = overwrite_dir.lower().strip() - if overwrite_dir == 'n': - print('Quitting...Restart and provide the proper ' - 'directory to write to!') - sys.exit() - elif overwrite_dir == 'y': - shutil.rmtree(args.d) - pass - else: - print('Quitting since you didn\'t provide ' - 'a valid response...') - sys.exit() - - else: - output_folder = args.date.replace( - '/', '-') + '_' + args.time.replace(':', '') - args.d = os.path.join(os.getcwd(), output_folder) - - args.log_file_path = os.path.join(args.d, 'logfile.log') - - if not any((args.resume, args.web)): - print("[*] Error: You didn't give me an action to perform.") - print("[*] Error: Please use --web!\n") - parser.print_help() - sys.exit() - - if args.resume: - if not os.path.isfile(args.resume): - print(" [*] Error: No valid DB file provided for resume!") - sys.exit() - - if args.proxy_ip is not None and args.proxy_port is None: - print("[*] Error: Please provide a port for the proxy!") - parser.print_help() - sys.exit() - - if args.proxy_port is not None and args.proxy_ip is None: - print("[*] Error: Please provide an IP for the proxy!") - parser.print_help() - sys.exit() - - if args.cookies: - cookies_list = [] - for one_cookie in args.cookies.split(","): - if "=" not in one_cookie: - print("[*] Error: Cookies must be in the form of key1=value1,key2=value2") - sys.exit() - cookies_list.append({ - "name": one_cookie.split("=")[0], - "value": one_cookie.split("=")[1] - }) - args.cookies = cookies_list - args.ua_init = False - return args - - -def single_mode(cli_parsed): - display = None - driver = None - - def exitsig(*args): - if current_process().name == 'MainProcess': - print('') - print('Quitting...') - os._exit(1) - - signal.signal(signal.SIGINT, exitsig) - - if cli_parsed.web: - create_driver = selenium_module.create_driver - capture_host = selenium_module.capture_host - - # Setup virtual display with cross-platform handling - display = setup_virtual_display(platform_mgr, cli_parsed.show_selenium) - - try: - url = cli_parsed.single - http_object = objects.HTTPTableObject() - http_object.remote_system = url - http_object.set_paths( - cli_parsed.d, None) - - web_index_head = create_web_index_head(cli_parsed.date, cli_parsed.time, cli_parsed) - driver = create_driver(cli_parsed) - result, driver = capture_host(cli_parsed, http_object, driver) - result = default_creds_category(result) - if cli_parsed.resolve: - result.resolved = resolve_host(result.remote_system) - - html = result.create_table_html() - with open(os.path.join(cli_parsed.d, 'report.html'), 'w', encoding='utf-8') as f: - f.write(web_index_head) - f.write(create_table_head()) - f.write(html) - f.write("
    ") - finally: - if driver: - driver.quit() - if display is not None: - display.stop() - - -def worker_thread(cli_parsed, targets, lock, counter, start_time, user_agent=None): - manager = None - driver = None - - try: - manager = db_manager.DB_Manager(cli_parsed.d + '/ew.db') - manager.open_connection() - - if cli_parsed.web: - create_driver = selenium_module.create_driver - capture_host = selenium_module.capture_host - - with lock: - driver = create_driver(cli_parsed, user_agent) - - while True: - http_object = targets.get() - if http_object is None: - break - # Try to ensure object values are blank - http_object._category = None - http_object._default_creds = None - http_object._error_state = None - http_object._page_title = None - http_object._ssl_error = False - http_object.category = None - http_object.default_creds = None - http_object.error_state = None - http_object.page_title = None - http_object.resolved = None - http_object.source_code = None - # Fix our directory if its resuming from a different path - if os.path.dirname(cli_parsed.d) != os.path.dirname(http_object.screenshot_path): - http_object.set_paths( - cli_parsed.d, None) - - print('Attempting to screenshot {0}'.format(http_object.remote_system)) - - http_object.resolved = resolve_host(http_object.remote_system) - if user_agent is None: - http_object, driver = capture_host( - cli_parsed, http_object, driver) - if http_object.category is None and http_object.error_state is None: - http_object = default_creds_category(http_object) - manager.update_http_object(http_object) - else: - ua_object, driver = capture_host( - cli_parsed, http_object, driver) - if http_object.category is None and http_object.error_state is None: - ua_object = default_creds_category(ua_object) - manager.update_ua_object(ua_object) - - counter[0].value += 1 - - # Show progress with ETA every 5 completions or at milestones - if counter[0].value % 5 == 0 or counter[0].value in [1, 10, 25, 50, 100]: - progress_msg = get_progress_message( - counter[0].value, - counter[1], - start_time.value if start_time.value > 0 else None - ) - print(f'\x1b[32m{progress_msg}\x1b[0m') - - do_jitter(cli_parsed) - except KeyboardInterrupt: - pass - except Exception as e: - print(f'[!] Worker thread error: {e}') - finally: - if manager: - manager.close() - if driver: - driver.quit() - - -def multi_mode(cli_parsed): - dbm = db_manager.DB_Manager(cli_parsed.d + '/ew.db') - dbm.open_connection() - if not cli_parsed.resume: - dbm.initialize_db() - dbm.save_options(cli_parsed) - m = Manager() - targets = m.Queue() - lock = m.Lock() - multi_counter = m.Value('i', 0) - start_time = m.Value('d', 0.0) # Track start time for ETA - display = None - - def exitsig(*args): - dbm.close() - if current_process().name == 'MainProcess': - print('') - print('Resume using ./EyeWitness.py --resume {0}'.format(cli_parsed.d + '/ew.db')) - os._exit(1) - - signal.signal(signal.SIGINT, exitsig) - if cli_parsed.resume: - pass - else: - url_list = target_creator(cli_parsed) - if cli_parsed.web: - for url in url_list: - dbm.create_http_object(url, cli_parsed) - - if cli_parsed.web: - # Setup virtual display with cross-platform handling - display = setup_virtual_display(platform_mgr, cli_parsed.show_selenium) - - # Initialize resource monitor - resource_monitor = ResourceMonitor(memory_limit_percent=80) - - # Check disk space before starting - has_space, available_gb, total_gb = check_disk_space(cli_parsed.d, min_gb=1) - if not has_space: - print(f'[!] Warning: Low disk space! Only {available_gb:.1f}GB available') - print('[!] Consider freeing space or using a different output directory') - - # Get system info and recommended threads - print(f'[*] {get_system_info()}') - - multi_total = dbm.get_incomplete_http(targets) - if multi_total > 0: - if cli_parsed.resume: - print('Resuming Web Scan ({0} Hosts Remaining)'.format(str(multi_total))) - else: - print('Starting Web Requests ({0} Hosts)'.format(str(multi_total))) - - # Adjust thread count based on workload and resources - recommended_threads = resource_monitor.get_recommended_threads(cli_parsed.threads) - if recommended_threads < cli_parsed.threads: - print(f'[*] Adjusting threads from {cli_parsed.threads} to {recommended_threads} based on available memory') - - if multi_total < recommended_threads: - num_threads = multi_total - else: - num_threads = recommended_threads - - print(f'[*] Using {num_threads} threads for processing') - for i in range(num_threads): - targets.put(None) - try: - start_time.value = time.time() # Set start time - workers = [Process(target=worker_thread, args=( - cli_parsed, targets, lock, (multi_counter, multi_total), start_time)) for i in range(num_threads)] - for w in workers: - w.start() - for w in workers: - w.join() - except Exception as e: - print(str(e)) - - if display is not None: - display.stop() - results = dbm.get_complete_http() - dbm.close() - m.shutdown() - sort_data_and_write(cli_parsed, results) - - -if __name__ == "__main__": - cli_parsed = create_cli_parser() - start_time = time.time() - title_screen(cli_parsed) - - if cli_parsed.resume: - print('[*] Loading Resume Data...') - temp = cli_parsed - dbm = db_manager.DB_Manager(cli_parsed.resume) - dbm.open_connection() - cli_parsed = dbm.get_options() - cli_parsed.d = os.path.dirname(temp.resume) - cli_parsed.resume = temp.resume - if temp.results: - cli_parsed.results = temp.results - dbm.close() - - print('Loaded Resume Data with the following options:') - engines = [] - if cli_parsed.web: - engines.append('Firefox') - print('') - print('Input File: {0}'.format(cli_parsed.f)) - print('Engine(s): {0}'.format(','.join(engines))) - print('Threads: {0}'.format(cli_parsed.threads)) - print('Output Directory: {0}'.format(cli_parsed.d)) - print('Timeout: {0}'.format(cli_parsed.timeout)) - print('') - else: - create_folders_css(cli_parsed) - - # Handle validate-only mode - if cli_parsed.validate_urls: - print('[*] Running in URL validation mode only') - from modules.validation import validate_url_list - from modules.helpers import target_creator - - url_list = target_creator(cli_parsed) - valid_urls, invalid_urls = validate_url_list(url_list, require_scheme=False) - - print(f'\n[*] Validation Results:') - print(f' - Valid URLs: {len(valid_urls)}') - print(f' - Invalid URLs: {len(invalid_urls)}') - - if invalid_urls: - print('\n[!] Invalid URLs found:') - for url, error in invalid_urls[:20]: # Show first 20 - print(f' - {url}: {error}') - if len(invalid_urls) > 20: - print(f' ... and {len(invalid_urls) - 20} more') - - # Write valid URLs to file - if valid_urls: - valid_file = os.path.join(cli_parsed.d, 'valid_urls.txt') - with open(valid_file, 'w') as f: - for url in valid_urls: - f.write(url + '\n') - print(f'\n[*] Valid URLs written to: {valid_file}') - - if invalid_urls: - invalid_file = os.path.join(cli_parsed.d, 'invalid_urls.txt') - with open(invalid_file, 'w') as f: - for url, error in invalid_urls: - f.write(f'{url} # {error}\n') - print(f'[*] Invalid URLs written to: {invalid_file}') - - print(f'\n[*] Validation completed in {time.time() - start_time:.2f} seconds') - sys.exit(0) - - if cli_parsed.single: - if cli_parsed.web: - single_mode(cli_parsed) - if not cli_parsed.no_prompt: - open_file = open_file_input(cli_parsed) - if open_file: - files = glob.glob(os.path.join(cli_parsed.d, '*report.html')) - for f in files: - webbrowser.open(f) - class_info() - sys.exit() - class_info() - sys.exit() - - if cli_parsed.f is not None or cli_parsed.x is not None: - multi_mode(cli_parsed) - duplicate_check(cli_parsed) - - print('Finished in {0} seconds'.format(time.time() - start_time)) - - if not cli_parsed.no_prompt: - open_file = open_file_input(cli_parsed) - if open_file: - files = glob.glob(os.path.join(cli_parsed.d, '*report.html')) - for f in files: - webbrowser.open(f) - class_info() - sys.exit() - class_info() - sys.exit()