def rename(self, old, new): """ Rename selected missions. :param str old: Name of the mission to rename :param str new: New mission name """ if old == 'default': logger.warning('Default mission cannot be renamed') return False mission = self.sqlsess.query(Mission).filter( Mission.name == old).first() if mission: new_mission = self.sqlsess.query(Mission).filter( Mission.name == new).first() if new_mission: logger.warning( 'A mission named "{name}" already exists'.format(name=new)) return False else: mission.name = new self.sqlsess.commit() logger.success('Mission renamed: {old} -> {new}'.format( old=old, new=new)) return True else: logger.warning('Mission "{name}" doesn\'t exists'.format(name=old)) return False
def __run_install_update(self, fast_mode, update=False): """ Run install or update command. :param fast_mode: Set to true to disable prompts :param update: Mode selector, True for update | False for install (default) :return: Install/Update status :rtype: bool """ if update: cmd = self.update_command.get_cmdline(self) else: cmd = self.install_command.get_cmdline(self) mode = 'update' if update else 'install' logger.info('Description: {descr}'.format(descr=self.description)) #Output.print('{mode} command : {cmd}'.format( # mode=mode.capitalize(), cmd=cmd_short)) if fast_mode \ or Output.prompt_confirm('Confirm {mode} ?'.format(mode=mode), default=True): Output.begin_cmd(cmd) returncode, _ = ProcessLauncher(cmd).start() Output.delimiter() if returncode != 0: logger.warning('Tool {mode} has finished with an error ' \ 'exit code: {code}'.format(mode=mode, code=returncode)) else: logger.success( 'Tool {mode} has finished with success exit code'.format( mode=mode)) return True else: logger.warning('Tool {mode} aborted'.format(mode=mode)) return False
def reset(self): self.sqlsess.query(Mission).delete() self.sqlsess.commit() self.sqlsess.add(Mission(name='default', comment='Default scope')) self.sqlsess.commit() logger.success( 'All missions deleted & fresh "default" mission created')
def do_nmap(self, args): """Import Nmap results""" print() file = os.path.expanduser(args.file[0]) if not FileUtils.can_read(file): logger.error('Cannot read specified file') return logger.info('Importing Nmap results from {file}'.format(file=file)) if not args.no_http_recheck: logger.info('Each service will be re-checked to detect HTTP services. Use --no-http-recheck if you want to disable it (faster import)') parser = NmapResultsParser(file, self.settings.services) results = parser.parse(not args.no_http_recheck) if results is not None: if len(results) == 0: logger.warning('No new service has been added into current mission') else: req = HostsRequester(self.sqlsess) req.select_mission(self.current_mission) for host in results: req.add_or_merge_host(host) logger.success('Nmap results imported with success into current mission') print()
def check(self): """ Check the toolbox: Run all check commands (when available) from all installed tools, in automatic mode (i.e. checks based on exit codes) In case of an error code returned by one check command (!= 0), the function stops and exits the program with exit code 1 (error). Otherwise, if all check commands have returned a success exit code (0), it exits the program with exit code 0 (success). Designed to be used for Continuous Integration. """ Output.title1('Automatic check of installed tools') for service in self.services: for tool in self.tools[service]: if tool.installed: # Automatic mode (no prompt), only based on exit status status = tool.run_check_command(fast_mode=True) if not status: logger.error('An error occured with the tool "{tool}". Exit ' \ 'check with exit code 1...'.format(tool=tool.name)) sys.exit(1) print() print() logger.success('No error has been detected with all tools check commands. ' \ 'Exit with success code 0...') sys.exit(0)
def edit_comment(self, comment): results = self.get_results() if not results: logger.error('No mission with this name') else: for r in results: r.comment = comment self.sqlsess.commit() logger.success('Comment edited')
def rename(self, old, new): if old == 'default': logger.warning('Default mission cannot be renamed') else: mission = self.sqlsess.query(Mission).filter( Mission.name == old).first() mission.name = new self.sqlsess.commit() logger.success('Mission renamed: {old} -> {new}'.format())
def delete(self): results = self.get_results() if not results: logger.error('No mission with this name') else: for r in results: self.sqlsess.delete(r) self.sqlsess.commit() logger.success('Mission deleted')
def edit_comment(self, comment): results = self.get_results() if not results: logger.error('No matching credential') else: for r in results: r.comment = comment self.sqlsess.commit() logger.success('Comment edited')
def add_service(self, ip, hostname, port, protocol, service): protocol = { 'tcp': Protocol.TCP, 'udp': Protocol.UDP }.get(protocol, Protocol.TCP) matching_service = self.sqlsess.query(Service).join(Host).join(Mission)\ .filter(Mission.name == self.current_mission)\ .filter(Host.ip == ip)\ .filter(Service.port == int(port))\ .filter(Service.protocol == protocol).first() if protocol == Protocol.TCP: up = NetUtils.is_tcp_port_open(ip, port) else: up = NetUtils.is_udp_port_open(ip, port) if matching_service: logger.warning('Service already present into database') else: if up: logger.info( 'Grabbing banner from {ip}:{port} with Nmap...'.format( ip=ip, port=port)) banner = NetUtils.grab_banner_nmap(ip, port) logger.info('Banner: {}'.format(banner or 'None')) os = NetUtils.os_from_nmap_banner(banner) if os: logger.info('Detected Host OS: {}'.format(os)) else: logger.warning('Port seems to be closed !') # Add service in db (and host if not existing) service = Service(name=service, port=int(port), protocol=protocol, up=up, banner=banner) matching_host = self.sqlsess.query(Host).join(Mission)\ .filter(Mission.name == self.current_mission)\ .filter(Host.ip == ip).first() new_host = Host(ip=ip, hostname=hostname, os=os) if matching_host: matching_host.merge(new_host) self.sqlsess.commit() service.host = matching_host else: mission = self.sqlsess.query(Mission).filter( Mission.name == self.current_mission).first() new_host.mission = mission service.host = new_host self.sqlsess.add(new_host) self.sqlsess.add(service) self.sqlsess.commit() logger.success('Service added')
def switch_https(self): results = self.get_results() if not results: logger.error('No matching service') else: for r in results: if r.url: r.url = WebUtils.switch_http_https(r.url) self.sqlsess.commit() logger.success('Switch done')
def add_cred(self, service_id, username, password, auth_type=None): """ Add new credential for a given service. :param int service_id: Id of service :param str username: Username :param str password: Password (None if unknown) :param str auth_type: Authentication type for HTTP service """ cred = self.sqlsess.query(Credential).join(Service)\ .filter(Service.id == service_id)\ .filter(Credential.username == username)\ .filter(Credential.password == password)\ .filter(Credential.type == auth_type).first() if cred: logger.warning('Credential already exists in database') else: service = self.sqlsess.query(Service).filter(Service.id == service_id)\ .first() if not service: logger.error( 'Service id {id} is invalid'.format(id=service_id)) else: cred = Credential( username=username, password=password, type=auth_type if service.name == 'http' else None) self.sqlsess.add(cred) service.credentials.append(cred) username = '******' if username == '' else username password = { '': '<empty>', None: '<???>' }.get(password, password) auth_typ = '(' + str(auth_type) + ')' if auth_type else '' hostname = '(' + service.host.hostname + ')' if service.host.hostname else '' protocol = { Protocol.TCP: 'tcp', Protocol.UDP: 'udp' }.get(service.protocol) logger.success('Credential {username}/{password}{auth_type} added ' \ 'to service {service} host={ip}{hostname} ' \ 'port={port}/{proto}'.format( username = username, password = password, auth_type = auth_typ, service = service.name, ip = service.host.ip, hostname = hostname, port = service.port, proto = protocol)) self.sqlsess.commit()
def switch_https(self): """Switch between HTTP and HTTPS on selected services""" results = self.get_results() if not results: logger.error('No matching service') else: for r in results: if r.url: r.url = WebUtils.switch_http_https(r.url) self.sqlsess.commit() logger.success('Switch done')
def __check_post_install_update(self, settings, fast_mode, update=False): """ Post-install/update checks :param settings: Settings instance :param fast_mode: Boolean indicating whether prompts must be displayed or not :return: Boolean indicating status """ mode = ('update', 'updated') if update else ('install', 'installed') status = True if not fast_mode: if not self.check_command: logger.info('No check_command defined in settings for {tool}, will assume it is ' \ 'correctly {mode}'.format(tool=self.name_display, mode=mode[1])) else: logger.info( 'Now, checking if {tool} has been {mode} correctly. Hit any key to run test...' .format(tool=self.name_display, mode=mode[1])) CLIUtils.getch() status = self.__run_check_command() # Change install status in configuration file if status: try: if settings.change_installed_status(self.target_service, self.name_clean, install_status=True): logger.success( 'Tool {tool} has been marked as successfully {mode}'. format(tool=self.name_display, mode=mode[1])) return True else: logger.error( 'Error when updating configuration file "{filename}{ext}"' .format(filename=INSTALL_STATUS_CONF_FILE, ext=CONF_EXT)) return False except SettingsException as e: logger.error('An unexpected error occured when trying to mark the tool as {mode}: ' \ '{exception}'.format(exception=e, mode=mode[1])) if not update: self.remove(settings) return False else: logger.warning('Tool {tool} has not been marked as {mode}'.format( tool=self.name_display, mode=mode[1])) if not update: self.remove(settings) else: if not fast_mode and Output.prompt_confirm( 'Do you want to try to re-install ?', default=True): return self.__reinstall(settings, fast_mode) return False
def add_target(self, target): """ Add a new service into the current mission scope in database from a Target object. :param Target target: Target to add """ mission = self.sqlsess.query(Mission)\ .filter(Mission.name == self.current_mission).first() matching_service = self.sqlsess.query(Service)\ .join(Host)\ .join(Mission)\ .filter(Host.ip == target.get_ip())\ .filter(Mission.name == self.current_mission)\ .filter(Service.name == target.get_service_name())\ .filter(Service.port == target.get_port())\ .filter(Service.protocol == target.get_protocol2())\ .filter(Service.url == target.get_url()).first() # If service already exists in db, update it if necessary if matching_service: logger.info('A matching service has been found in the database') matching_service.merge(target.service) self.sqlsess.commit() # Make sure to replace target info by newly created service target.service = matching_service # Add host in db if it does not exist or update its info (merging) else: host = self.sqlsess.query(Host).join(Mission)\ .filter(Mission.name == self.current_mission)\ .filter(Host.ip == target.get_ip()).first() if host: host.merge(target.service.host) self.sqlsess.commit() target.service.host = host else: self.sqlsess.add(target.service.host) mission.hosts.append(target.service.host) self.sqlsess.commit() # Add service in db self.sqlsess.add(target.service) self.sqlsess.commit() logger.success('{action}: host {ip} | port {port}/{proto} | ' \ 'service {service}'.format( action = 'Updated' if matching_service else 'Added', ip = target.get_ip(), port = target.get_port(), proto = target.get_protocol(), service = target.get_service_name()))
def add_cred(self, username, password, auth_type=None): """ Add new credential for selected service(s). :param str username: Username :param str password: Password (None if unknown) :param str auth_type: Authentication type for HTTP service """ results = self.get_results() if not results: logger.error('No matching service') else: for r in results: cred = self.sqlsess.query(Credential).join(Service)\ .filter(Service.id == r.id)\ .filter(Credential.username == username)\ .filter(Credential.password == password)\ .filter(Credential.type == auth_type).first() if not cred: cred = Credential( username=username, password=password, type=auth_type if r.name == 'http' else None) self.sqlsess.add(cred) r.credentials.append(cred) username = '******' if cred.username == '' else cred.username password = { '': '<empty>', None: '<???>' }.get(cred.password, cred.password) auth_type = '('+str(auth_type)+')' if \ (auth_type and r.name == 'http') else '' hostname = '(' + r.host.hostname + ')' if r.host.hostname else '' protocol = { Protocol.TCP: 'tcp', Protocol.UDP: 'udp' }.get(r.protocol) logger.success('Credential {username}/{password}{auth_type} ' \ 'added to service {service} host={ip}{hostname} ' \ 'port={port}/{proto}'.format( username = username, password = password, auth_type = auth_type, service = r.name, ip = r.host.ip, hostname = hostname, port = r.port, proto = protocol)) self.sqlsess.commit()
def add(self, name): mission = self.sqlsess.query(Mission).filter( Mission.name == name).first() if mission: logger.warning('A mission named "{name}" already exists'.format( name=mission.name)) return False else: self.sqlsess.add(Mission(name=name)) self.sqlsess.commit() logger.success( 'Mission "{name}" successfully added'.format(name=name)) return True
def rename(self, old, new): """ Rename selected missions. :param str old: Name of the mission to rename :param str new: New mission name """ if old == 'default': logger.warning('Default mission cannot be renamed') else: mission = self.sqlsess.query(Mission).filter(Mission.name == old).first() mission.name = new self.sqlsess.commit() logger.success('Mission renamed: {old} -> {new}'.format())
def edit_comment(self, comment): """ Edit comment of selected missions. :param str comment: New comment """ results = self.get_results() if not results: logger.error('No mission with this name') else: for r in results: r.comment = comment self.sqlsess.commit() logger.success('Comment edited')
def edit_comment(self, comment): """ Edit comment of selected credentials. :param str comment: New comment """ results = self.get_results() if not results: logger.error('No matching credential') else: for r in results: r.comment = comment self.sqlsess.commit() logger.success('Comment edited')
def remove(self, settings): """ Remove the tool: - Remove tool directory into toolbox - Change install status to false. :param Settings settings: Settings from config files :return: Removal status :rtype: bool """ # Delete tool directory if tool was installed inside toolbox directory if self.install_command: if not FileUtils.is_dir(self.tool_dir): logger.warning('Directory "{dir}" does not exist'.format( dir=self.tool_dir)) #return False elif not FileUtils.remove_directory(self.tool_dir): logger.error('Unable to delete directory "{dir}". ' \ 'Check permissions and/or re-run with sudo'.format( dir=self.tool_dir)) return False else: logger.success( 'Tool directory "{dir}" deleted'.format(dir=self.tool_dir)) # Remove virtualenv files if necessary virtualenv_dir = '{}/{}'.format(VIRTUALENVS_DIR, self.name) if FileUtils.is_dir(virtualenv_dir): if FileUtils.remove_directory(virtualenv_dir): logger.success('Virtualenv directory deleted') else: logger.warning('Unable to delete Virtualenv directory') if self.virtualenv.startswith('ruby'): logger.info('Delete RVM environment ({ruby}@{name})...'.format( ruby=self.virtualenv, name=self.name)) cmd = 'source /usr/local/rvm/scripts/rvm; rvm use {ruby} && ' \ 'rvm gemset delete {name} --force'.format( ruby=self.virtualenv, name=self.name) returncode, _ = ProcessLauncher(cmd).start() if returncode == 0: logger.success('RVM environment deleted with success') else: logger.warning('Unable to delete RVM environment') # Make sure "installed" option in config file is set to False if settings.change_installed_status(self.target_service, self.name, install_status=False): logger.success('Tool marked as uninstalled') else: logger.error('An unexpected error occured when trying to mark the tool ' \ 'as uninstalled !') return False self.installed = False return True
def reset(self): """Delete all missions in database (re-create a fresh "default" mission)""" self.sqlsess.query(Mission).delete() self.sqlsess.query(CommandOutput).delete() self.sqlsess.query(Credential).delete() self.sqlsess.query(Host).delete() self.sqlsess.query(Option).delete() self.sqlsess.query(Product).delete() self.sqlsess.query(Result).delete() self.sqlsess.query(Screenshot).delete() self.sqlsess.query(Service).delete() self.sqlsess.query(Vuln).delete() self.sqlsess.commit() self.sqlsess.add(Mission(name='default', comment='Default scope')) self.sqlsess.commit() logger.success('All missions deleted & fresh "default" mission created')
def __check_pre_install(self, settings, fast_mode=False): """ Perform some checks before trying to install the tool (already installed ?, install command ?). :param Settings settings: Settings from config files :param bool fast_mode: Set to true to disable prompts :return: Result of checks :rtype: bool """ if self.installed: logger.info('{tool} is already installed (according to settings), ' \ 'skipped'.format(tool=self.name)) return False elif not self.install_command: logger.warning('The tool {tool} has no installation command specified in ' \ 'config file'.format(tool=self.name)) if fast_mode \ or Output.prompt_confirm('Is the tool already installed on your system ?', default=True): try: if settings.change_installed_status(self.target_service, self.name, True): logger.success('Tool {tool} has been marked as installed in ' \ 'settings'.format(tool=self.name)) return True else: logger.error('Error when saving the configuration file ' \ '"{filename}{ext}"'.format( filename=INSTALL_STATUS_CONF_FILE, ext=CONF_EXT)) return False except SettingsException as e: logger.error(e) self.remove(settings) return False else: logger.info('Tool {tool} is still not marked as installed in ' \ 'settings'.format(tool=self.name)) return False return True
def __check_pre_install(self, settings, fast_mode): """ Checks to run before installing the tool :param settings: Settings instance :param fast_mode: Boolean indicating whether prompts must be displayed or not :return: Boolean indicating status """ if self.installed: logger.info( '{tool} is already installed (according to settings), skipped'. format(tool=self.name_display)) return False elif not self.install_command: logger.warning( 'The tool {tool} has no installation command specified in config file' .format(tool=self.name_display)) if fast_mode or Output.prompt_confirm( 'Is the tool already installed on your system ?', default=True): try: if settings.change_installed_status( self.target_service, self.name_clean, True): logger.success( 'Tool {tool} has been marked as installed in settings' .format(tool=self.name_display)) return True else: logger.error( 'Error when saving the configuration file "{filename}{ext}"' .format(filename=INSTALL_STATUS_CONF_FILE, ext=CONF_EXT)) return False except SettingsException as e: logger.error(e) self.remove(settings) return False else: logger.info( 'Tool {tool} is still not marked as installed in settings'. format(tool=self.name_display)) return False return True
def remove_for_service(self, service): """ Remove the tools for a given service. :param str service: Name of the service targeted by the tools to remove (may be "multi") """ if service not in self.services: return Output.title1( 'Remove tools for service: {service}'.format(service=service)) if not self.tools[service]: logger.info('No tool specific to this service in the toolbox') else: i = 1 status = True for tool in self.tools[service]: if i > 1: print() Output.title2( '[{svc}][{i:02}/{max:02}] Remove {tool_name}:'.format( svc=service, i=i, max=len(self.tools[service]), tool_name=tool.name)) status &= tool.remove(self.settings) i += 1 # Remove the service directory if all tools successfully removed if status: short_svc_path = '{toolbox}/{service}'.format( toolbox=TOOLBOX_DIR, service=service) full_svc_path = FileUtils.absolute_path(short_svc_path) if FileUtils.remove_directory(full_svc_path): logger.success( 'Toolbox service directory "{path}" deleted'.format( path=short_svc_path)) else: logger.warning('Toolbox service directory "{path}" cannot be ' \ 'deleted because it still stores some files'.format( path=short_svc_path))
def remove(self, settings): """ Remove the tool: - Remove tool directory into toolbox - Change install status to false. :param Settings settings: Settings from config files :return: Removal status :rtype: bool """ # Delete tool directory if tool was installed inside toolbox directory if self.install_command: if not FileUtils.is_dir(self.tool_dir): logger.warning('Directory "{dir}" does not exist'.format( dir=self.tool_dir)) return False elif not FileUtils.remove_directory(self.tool_dir): logger.error('Unable to delete directory "{dir}". ' \ 'Check permissions and/or re-run with sudo'.format( dir=self.tool_dir)) return False else: logger.success('Tool directory "{dir}" deleted'.format( dir=self.tool_dir)) # Make sure "installed" option in config file is set to False if settings.change_installed_status(self.target_service, self.name, install_status=False): logger.success('Tool marked as uninstalled') else: logger.error('An unexpected error occured when trying to mark the tool ' \ 'as uninstalled !') return False self.installed = False return True
def __run_install_update(self, fast_mode, update=False): """ Run install/update command :param update: Mode selector, True for update | False for install (default) :return: Boolean indicating status """ if update: cmd = self.update_command.get_cmdline(self.tool_dir) else: cmd = self.install_command.get_cmdline(self.tool_dir) mode = 'update' if update else 'install' logger.info('Description: {descr}'.format(descr=self.description)) #Output.print('{mode} command : {cmd}'.format(mode=mode.capitalize(), cmd=cmd_short)) if fast_mode or Output.prompt_confirm( 'Confirm {mode} ?'.format(mode=mode), default=True): Output.begin_cmd(cmd) ProcessLauncher(cmd).start() Output.delimiter() logger.success('Tool {mode} has finished'.format(mode=mode)) return True else: logger.warning('Tool {mode} aborted'.format(mode=mode)) return False
def run_check_command(self, fast_mode=False): """ Run the check command. The goal is to quickly check if the tool is not buggy or missing some dependencies. The user must analyze the output and gives an answer. :param bool fast_mode: Set to true to disable prompts :return: Response from user in interactive mode, otherwise status based on exit code (True if exit code is 0) :rtype: bool """ if not self.check_command: logger.info('No check_command defined in settings for the tool ' \ '{tool}'.format(tool=self.name)) return True logger.info('Running the check command for the tool {tool}...'.format( tool=self.name)) cmd = self.check_command.get_cmdline(self) Output.begin_cmd(cmd) returncode, _ = ProcessLauncher(cmd).start() Output.delimiter() if returncode != 0: logger.warning('Check command has finished with an error ' \ 'exit code: {code}'.format(code=returncode)) else: logger.success('Check command has finished with success exit code') if fast_mode: return (returncode == 0) else: return Output.prompt_confirm('Does the tool {tool} seem to be running ' \ 'correctly ?'.format(tool=self.name), default=True)
def __run_for_single_target(self, args): """Run attack against a single target specified into args""" req = ServicesRequester(self.sqlsess) mission = None # Get Mission if target must be added into a mission scope if args.add: mission = self.sqlsess.query(Mission).filter(Mission.name == args.add).first() if not mission: raise AttackException('The specified mission does not exist in the ' \ 'database. You should create it if needed') # Create new Service/Host objects (if service already exist, # will be merged by ServicesRequester.add_target) url = args.target_ip_or_url if args.target_mode == TargetMode.URL else '' ip = args.target_ip_or_url if args.target_mode == TargetMode.IP else '' service = Service( name=args.service, port=int(args.target_port), protocol=self.settings.services.get_protocol2(args.service), url=url) host = Host(ip=ip) # Will be updated when initializing Target() host.services.append(service) # Update context (credentials, options, products) if specified in command-line if args.creds: for c in args.creds[args.service]: self.sqlsess.add(c) service.credentials.append(c) if args.users: for u in args.users[args.service]: self.sqlsess.add(u) service.credentials.append(u) if args.products: for p in args.products[args.service]: self.sqlsess.add(p) service.products.append(p) if args.options: for o in args.options[args.service]: self.sqlsess.add(o) service.options.append(o) # Initialize Target try: target = Target(service, self.settings.services) except TargetException as e: logger.error(e) sys.exit(1) # Check Target and update its information: # - Reverse DNS lookup: by default # - Port check: always # - Nmap service detection: by default # - HTML title grabbing: always # - Web technologies detection: always # - Context initialization via SmartStart: always reachable = target.smart_check( reverse_dns_lookup=(args.reverse_dns is None or args.reverse_dns == 'on'), availability_check=True, nmap_banner_grabbing=(args.nmap_banner_grab is None \ or args.nmap_banner_grab == 'on'), html_title_grabbing=True, web_technos_detection=True, smart_context_initialize=True) # Display availability status, exit if not reachable if args.target_mode == TargetMode.IP: msg = 'Target service {neg}reachable: {target}'.format( neg='not ' if not reachable else '', target=target) else: msg = 'Target URL {url} is {neg}reachable'.format( url=target.get_url(), neg='not ' if not reachable else '') if reachable: logger.success(msg) else: logger.error(msg) return # Commit the target with updated information inside the appropriate # mission in the database if mission: logger.info('Results from this attack will be saved under mission ' \ '"{mission}" in database'.format(mission=mission.name)) req.select_mission(mission.name) req.add_target(target) # Run the attack self.attack_scope.add_target(target) self.attack_scope.attack() return
def run(self): # Create report directory dirname = '{mission}-{datetime}'.format( mission=StringUtils.clean(self.mission.replace(' ', '_'), allowed_specials=('_', '-')), datetime=datetime.datetime.now().strftime('%Y%m%d%H%M%S')) self.output_path = self.output_path + '/' + dirname if not FileUtils.create_directory(self.output_path): logger.error('Unable to create report directory: "{path}"'.format( path=self.output_path)) return False # Retrieve all services in selected mission req = ServicesRequester(self.sqlsession) req.select_mission(self.mission) services = req.get_results() # Generate screenshots processor = ScreenshotsProcessor(self.mission, self.sqlsession) processor.run() screens_dir = self.output_path + '/screenshots' if not FileUtils.create_directory(screens_dir): logger.warning( 'Unable to create screenshots directory: "{path}"'.format( path=screens_dir)) else: for service in services: if service.name == 'http' and service.screenshot is not None \ and service.screenshot.status == ScreenStatus.OK: img_name = 'scren-{ip}-{port}-{id}'.format( ip=str(service.host.ip), port=service.port, id=service.id) path = screens_dir + '/' + img_name ImageUtils.save_image(service.screenshot.image, path + '.png') ImageUtils.save_image(service.screenshot.thumbnail, path + '.thumb.png') # Create index.html html = self.__generate_index() if FileUtils.write(self.output_path + '/index.html', html): logger.info('index.html file generated') else: logger.error('An error occured while generating index.html') return False # Create results-<service>.html (1 for each service) for service in services: # Useless to create page when no check has been run for the service if len(service.results) == 0: continue html = self.__generate_results_page(service) # Create a unique name for the service HTML file filename = 'results-{ip}-{port}-{service}-{id}.html'.format( ip=str(service.host.ip), port=service.port, service=service.name, id=service.id) if FileUtils.write(self.output_path + '/' + filename, html): logger.info( '{filename} file generated'.format(filename=filename)) else: logger.error( 'An error occured while generating {filename}'.format( filename=filename)) return False logger.success('HTML Report written with success in: {path}'.format( path=self.output_path)) logger.info('Important: If running from Docker container, make sure to run ' \ '"xhost +" on the host before') if Output.prompt_confirm('Would you like to open the report now ?', default=True): webbrowser.open(self.output_path + '/index.html') return True