def run(): log = Log(name='main ') # parse start arguments args = ArgPars() args() # display current version if args.version: print(f'Gummy version {gummy.__version__} (https://github.com/v-yar/gummy)') sys.exit() # get default config config = Config() # create a default configuration if required if args.create_default_config: config.create_default_config() sys.exit() config.read_default_config() # set logger settings log.initialization(config.default_config['LOGING']) db = Storage() scanner = Scanner(db) log.info(f'Start time: {datetime.datetime.now().strftime("%Y.%m.%d %H:%M:%S")}') # set workspace name if args.workspase is None: workspace = datetime.datetime.now().strftime('%Y%m%d-%H%M%S') else: workspace = args.workspase config.start_config.set('MAIN', 'workspase', workspace) config + args.to_dict() # set current network cidr if args.target == 'auto': config.start_config.set('MASSCAN', 'target', get_ip()) # start shell shell = GummyShell(config=config, db=db, scanner=scanner) shell()
class Config: """class for managing script configurations.""" def __init__(self): """initializing the configuration class.""" DIR = Path( os.path.abspath(os.path.join(os.path.dirname(__file__), '../'))) self.log = Log(name='conf ') self.default_config = None self.start_config_path = None self.start_config = None self.default_config_path = os.path.join(DIR, 'setting.ini') self.DEFAULT_CONFIG = { 'MAIN': { '# Contains the main application settings': None, '# Path:': None, 'result_path': Path(os.path.abspath(os.path.join(DIR, '../', 'scans'))), 'masscan_path': '/usr/bin/masscan', 'nmap_path': '/usr/bin/nmap', '# Reporting:': None, 'rep_type': 'None' }, 'LOGING': { '# Contains the logging settings': None, '# Logging:': None, '# log_level: DEBUG, INFO, WARNING, ERROR, CRITICAL': None, '# log_format: https://docs.python.org/3/library/logging.html#logrecord-attributes': None, '# log_format_date: ‘%Y-%m-%d %H:%M:%S,uuu’': None, 'log_level': 'INFO', 'log_format': '%(asctime)s | %(name)s | %(levelname)s | %(message)s', 'log_format_date': '%H:%M:%S', 'log_file_path': os.path.join(DIR, 'log') }, 'CONSTANTS': { '# Contains non-modifiable values': None, 'private_cidr': '10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16' }, 'MASSCAN': { '# The first step - hosts discovery': None, 'target': '127.0.0.1', 'target_exclude': '', 'port': '', 'top_ports': '', 'rate': '10000' }, 'NMAP': { '# The second step - detailed scanning of discovered hosts': None, 'scan_type': 'basic' }, } def __add__(self, other): """ this method allows you to update the workload configuration data with startup arguments. :param other: argument dictionary of the required format. """ for key in list(other.keys()): self.start_config[key].update(other[key]) self.log.debug(list(self.start_config[key].values())) def create_default_config(self): """method for creating a basic configuration file.""" config = configparser.RawConfigParser(allow_no_value=True, delimiters='=') config.read_dict(self.DEFAULT_CONFIG) with open(self.default_config_path, 'w') as config_file: config.write(config_file) self.log.info( f'Default configuration file {self.default_config_path} successfully created' ) def read_default_config(self): """method for read a basic configuration file.""" if not os.path.exists(self.default_config_path): self.log.info( f'The configuration file {self.default_config_path} was not found.' ) self.create_default_config() self.log.info('Read configuration file') try: config = configparser.RawConfigParser() config.read(self.default_config_path) self.log.info( f'Default configuration file {self.default_config_path} successfully read' ) except Exception: self.log.warning( f'Default configuration file {self.default_config_path} incorrect!' ) raise Exception() self.default_config = config self.start_config = config def read_start_config(self, file): """method for read a basic configuration file.""" if not os.path.exists(file): self.log.warning(f'The configuration file {file} was not found!') else: self.log.info('Read configuration file') try: config = configparser.RawConfigParser() config.read(file) self.log.info(f'Configuration file {file} successfully read') except Exception: self.log.warning(f'Configuration file {file} incorrect!') raise Exception() self.default_config = config self.start_config = config def create_start_config(self): """method for writing start parameters to the startup configuration file.""" with open(self.start_config_path, 'w') as config_file: self.start_config.write(config_file) self.log.info( f'Startup configuration file {self.start_config_path} successfully created' ) @property def get_start_config(self): """method for print start config in console""" list_start_config = list() for section in self.start_config.sections(): section_pr = section for key in self.start_config[section]: list_start_config.append( [section_pr, key, self.start_config[section][key]]) section_pr = '' return list_start_config def set_start_config_key(self, key, value): """method for changing parameter about current configuration""" for section in self.start_config.sections(): for k in self.start_config[section]: if k == key: self.start_config.set(section, k, value) return True return False def get_start_config_key(self, key): """method for getting current cinf parameter""" for section in self.start_config.sections(): for k in self.start_config[section]: if k == key: return self.start_config[section][k] return 'no key' def get_all_start_config_key(self): """method for generating a list of possible configuration parameters (for shell completer)""" keys = dict() for section in self.start_config.sections(): for key in self.start_config[section]: keys[key] = '' return keys
class GummyShell: """Forms the cli control interface of the scanner""" def __init__(self, config, db, scanner): """class object initialization""" self.log = Log(name='shell') self.config = config self.db = db self.scan = scanner self.parser = Parser() self.collors = ( '#000000', '#800000', '#008000', '#808000', '#000080', '#800080', '#008080', '#c0c0c0', '#808080', '#ff0000', '#00ff00', '#ffff00', '#0000ff', '#ff00ff', '#00ffff', '#ffffff', '#000000', '#00005f', '#000087', '#0000af', '#0000d7', '#0000ff', '#005f00', '#005f5f', '#005f87', '#005faf', '#005fd7', '#005fff', '#008700', '#00875f', '#008787', '#0087af', '#0087d7', '#0087ff', '#00af00', '#00af5f', '#00af87', '#00afaf', '#00afd7', '#00afff', '#00d700', '#00d75f', '#00d787', '#00d7af', '#00d7d7', '#00d7ff', '#00ff00', '#00ff5f', '#00ff87', '#00ffaf', '#00ffd7', '#00ffff', '#5f0000', '#5f005f', '#5f5fd7', '#5faf5f', '#5f0087', '#5f00af', '#5f00d7', '#5f00ff', '#5f5f00', '#5f5f5f', '#5f5f87', '#5f5faf', '#5f5fff', '#5f8700', '#5f875f', '#5f8787', '#5f87af', '#5f87d7', '#5f87ff', '#5faf00', '#5faf87', '#5fafaf', '#5fafd7', '#5fafff', '#5fd700', '#5fd75f', '#5fd787', '#5fd7af', '#5fd7ff', '#5fff00', '#5fff5f', '#5fff87', '#5fffaf', '#5fffd7', '#5fffff', '#870000', '#870087', '#8700af', '#8700d7', '#8700ff', '#875f00', '#875f5f', '#875f87', '#875faf', '#875fff', '#878700', '#87875f', '#878787', '#8787af', '#8787d7', '#8787ff', '#87af00', '#87af87', '#87afaf', '#87afd7', '#87afff', '#87d700', '#87d75f', '#87d787', '#87d7af', '#87d7ff', '#87ff00', '#87ff5f', '#87ff87', '#87ffaf', '#87ffd7', '#87ffff', '#af0000', '#af0087', '#af00af', '#af00d7', '#af00ff', '#af5f00', '#af5f5f', '#af5f87', '#af5faf', '#af5fff', '#af8700', '#af875f', '#af8787', '#af87af', '#af87d7', '#af87ff', '#afaf00', '#afaf87', '#afafaf', '#afafd7', '#afafff', '#afd700', '#afd75f', '#afd787', '#afd7af', '#afd7ff', '#afff00', '#afff5f', '#afff87', '#afffaf', '#afffd7', '#afffff', '#d70000', '#d70087', '#d700af', '#d700d7', '#d700ff', '#d75f00', '#d75f5f', '#d75f87', '#d75faf', '#d75fff', '#d78700', '#d7875f', '#d78787', '#d787af', '#d787d7', '#d787ff', '#d7af00', '#d7af87', '#d7afaf', '#d7afd7', '#d7afff', '#d7d700', '#d7d75f', '#d7d787', '#d7d7af', '#d7d7ff', '#d7ff00', '#d7ff5f', '#d7ff87', '#d7ffaf', '#d7ffd7', '#d7ffff', '#ff0000', '#ff0087', '#ff00af', '#ff00d7', '#ff00ff', '#ff5f00', '#ff5f5f', '#ff5f87', '#ff5faf', '#ff5fff', '#ff8700', '#ff875f', '#ff8787', '#ff87af', '#ff87d7', '#ff87ff', '#ffaf00', '#ffaf87', '#ffafaf', '#ffafd7', '#ffafff', '#ffd700', '#ffd75f', '#ffd787', '#ffd7af', '#ffd7ff', '#ffff00', '#ffff5f', '#ffff87', '#ffffaf', '#ffffd7', '#ffffff', '#080808', '#1c1c1c', '#262626', '#303030', '#3a3a3a', '#444444', '#4e4e4e', '#585858', '#626262', '#767676', '#808080', '#8a8a8a', '#949494', '#9e9e9e', '#a8a8a8', '#b2b2b2', '#bcbcbc', '#d0d0d0', '#dadada', '#e4e4e4', '#eeeeee', '#5fd7d7', '#87005f', '#875fd7', '#875fd7', '#87af5f', '#87d7d7', '#af005f', '#af5fd7', '#afaf5f', '#afd7d7', '#d7005f', '#d75fd7', '#d7af5f', '#d7d7d7', '#ff005f', '#ff5fd7', '#ffaf5f', '#ffd7d7', '#121212', '#6c6c6c', '#c6c6c6', ) self.commands = { 'set': self.config.get_all_start_config_key(), 'show': { 'config': 'print curent config (takes param)', 'host': 'print host table (takes param)', 'port': 'print port table', 'task': 'print running tasks', 'log': 'print the last n lines of the log file' }, 'sync': { 'config': 'synchronizes the configuration file' }, 'run': self.get_scanner_methods(self.scan), 'workspase': self.get_all_workspase(), 'flush': {}, 'kill': {}, 'help': {}, 'exit': {} } self.c_function = { 'set': self.f_set, 'show': self.f_show, 'sync': self.f_sync, 'run': self.f_run, 'workspase': self.f_workspase, 'flush': self.f_flush, 'kill': self.f_kill, 'help': self.f_help, 'exit': self.f_exit } self.grammar = compile(""" (\s* (?P<command>[a-z]+) \s*) | (\s* (?P<command>[a-z]+) \s+ (?P<operator>[A-Za-z0-9_-]+) \s*) | (\s* (?P<command>[a-z]+) \s+ (?P<operator>[A-Za-z0-9_-]+) \s+ (?P<parameter>[A-Za-z0-9.,-_/+*]+) \s*) """) self.style = Style.from_dict({ 'command': '#216f21 bold', 'operator': '#6f216f bold', 'parameter': '#ff0000 bold', 'trailing-input': 'bg:#662222 #ffffff', 'bottom-toolbar': '#6f216f bg:#ffffff', # Logo. 'bear': random.choice(self.collors), 'text': random.choice(self.collors), # User input (default text). '': '#ff0066', # Prompt. 'prompt_for_input': '#6f216f', }) self.lexer = GrammarLexer(self.grammar, lexers={ 'command': SimpleLexer('class:command'), 'operator': SimpleLexer('class:operator'), 'parameter': SimpleLexer('class:parameter') }) self.completer = GCompleter(self.commands) self.history_path = Path( os.path.abspath( os.path.join(os.path.dirname(__file__), '../.history'))) self.history = FileHistory(self.history_path) version_str = ''.join(['v', gummy.__version__, ' ']) self.logo = HTML(f''' <text> </text><bear> _ _ </bear> <text> _____ _ _ __ __ __ ____ __</text><bear> (c).-.(c) </bear> <text> / ____| | | | \/ | \/ \ \ / /</text><bear> / ._. \ </bear> <text> | | __| | | | \ / | \ / |\ \_/ / </text><bear> __\( Y )/__ </bear> <text> | | |_ | | | | |\/| | |\/| | \ / </text><bear> (_.-/'-'\-._)</bear> <text> | |__| | |__| | | | | | | | | | </text><bear> || </bear><text>G</text><bear> || </bear> <text> \_____|\____/|_| |_|_| |_| |_| </text><bear> _.' `-' '._ </bear> <text> </text><bear> (.-./`-'\.-.)</bear> <text>{version_str:>38}</text><bear> `-' `-'</bear> ''') self.prompt_str = [('class:prompt_for_input', '>>> ')] self.counter = 0 self.sync_config_stat = 0 # 0 - never synchronized # 1 - changed but not synchronized # 7 - synchronized # user-invoked function block: def f_show(self, **kwargs): def show_port(self): for line in str(self.db.get_ports_info()).split('\n'): self.log.info(line) def show_task(self): for item_task in asyncio.Task.all_tasks(): self.log.info('-' * 50) self.log.info(item_task) def show_log(self, pr): pr = int(pr) if pr else 100 try: line_need = int(pr) except ValueError: self.log.info('use int in param') line_need = 100 with open( self.config.start_config['LOGING']['log_file_path']) as lp: log = lp.read().split('\n') print('-' * 8) for ind, line in enumerate(log): if len(log) - line_need <= ind: print(f'log {ind:4}| {line}') print('-' * 8) def show_config(self, pr): if pr: vl = self.config.get_start_config_key(key=pr) self.log.info(f'{pr}: {vl}') else: table = PrettyTable() table.field_names = ['SECTOR', 'KEY', 'VALUE'] table.align = 'l' table.align['SECTOR'] = 'c' conf = self.config.get_start_config for item in conf: table.add_row(item) for line in str(table).split('\n'): self.log.info(line) def show_host(self, pr): if pr: pp = pprint.PrettyPrinter(width=80) pr = pr.replace('*', '[\d.]*') pr = pr.replace('+', '[\d.]+') pr = ''.join([pr, '$']) try: regex = re.compile(pr) except Exception: self.log.warning('Invalid regexp') else: for host in self.db.data: try: search = regex.search(host['addr']) except Exception: search = False self.log.warning('Invalid regexp') if search: for line in pp.pformat(host).split('\n'): self.log.info(line) else: for line in str(self.db.get_table()).split('\n'): self.log.info(line) if kwargs.get('operator'): op = kwargs.get('operator') if op == 'config': show_config(self, pr=kwargs.get('parameter')) elif op == 'log': show_log(self, pr=kwargs.get('parameter')) elif op == 'host': show_host(self, pr=kwargs.get('parameter')) elif op == 'task': show_task(self) elif op == 'port': show_port(self) else: self.log.info('What to show?') self.log.info(', '.join(self.commands.get('show'))) def f_sync(self, **kwargs): if kwargs.get('operator'): op = kwargs.get('operator') if op == 'config': # create workspace folders result_path = self.config.default_config.get( "MAIN", "result_path") workspace = self.config.start_config['MAIN']['workspase'] workspace_path = '/'.join([result_path, workspace]) self.config.start_config.set('MAIN', 'workspace_path', workspace_path) start_config_path = '/'.join( [workspace_path, 'start_config.ini']) self.config.start_config.set('MAIN', 'start_config_path', start_config_path) mk_dir(result_path) mk_dir(workspace_path) # create starting config file self.config.start_config_path = start_config_path self.config.create_start_config() # sync gummy gummy_scan cinf self.scan.sync(self.config.start_config) self.sync_config_stat = 7 else: self.log.info('What to sync?') self.log.info(', '.join(self.commands.get('sync'))) def f_set(self, **kwargs): if kwargs.get('operator'): op = kwargs.get('operator') if kwargs.get('parameter'): pr = kwargs.get('parameter') else: pr = '' self.sync_config_stat = 1 self.config.set_start_config_key(key=op, value=pr) else: self.log.info('What to set?') self.log.info(', '.join(self.commands.get('set'))) def f_run(self, **kwargs): if self.sync_config_stat == 1: self.log.info('configuration changed but not synchronized!') if self.sync_config_stat in [0, 1]: self.log.info('automatic synchronization start') self.f_sync(operator='config') if kwargs.get('operator'): op = '_' + kwargs.get('operator') getattr(self.scan, op)() else: self.log.info('What to run?') self.log.info(', '.join(self.commands.get('run'))) def f_workspase(self, **kwargs): if kwargs.get('operator'): op = kwargs.get('operator') result_path = self.config.start_config['MAIN']['result_path'] workspase_path = f'{result_path}/{op}' self.log.info(f'Ok loding {result_path}/{op}') counter = self.get_max_scans(workspase_path) self.log.info(f'Set gummy_scan counter is: {counter}') self.scan.counter = counter workspase_config_path = f'{workspase_path}/start_config.ini' self.log.info(f'Read workspase config: {workspase_config_path}') self.config.read_start_config(file=workspase_config_path) self.log.info('Load gummy_scan results:') for scaner in ['m', 'n']: for file in self.get_xml_files(scan_path=workspase_path, scaner=scaner): self.log.info(f' -- {file}') self.parser(file) self.db + self.parser.result else: self.log.info('What workspace to load?') self.log.info(', '.join(self.get_all_workspase())) def f_kill(self): for item_task in asyncio.Task.all_tasks(): if '<Task pending coro=<GummyShell.start()' not in str(item_task): self.log.info('-' * 50) self.log.info(item_task) self.log.info(item_task.cancel()) def f_flush(self): scan_path = self.config.start_config['MAIN']['result_path'] log_path = self.config.start_config['LOGING']['log_file_path'] list_scans = os.listdir(path=scan_path) self.log.info('clear log file...') os.truncate(log_path, 0) self.log.info('clear history file...') os.truncate(self.history_path, 0) for scan in list_scans: current_path = f'{scan_path}/{scan}' self.log.info(f'remove gummy_scan: {current_path}') shutil.rmtree(current_path, ignore_errors=True) self.f_exit() def f_help(self): self.log.info('No one will help you.') def f_exit(self): self.log.info('...') raise EOFError @staticmethod def get_max_scans(path): """workspase function, updates the gummy_scan counter""" xml_files = glob.glob(pathname=f'{path}/[0-9][0-9][0-9]-[nm]-*.xml') regex = re.compile(f'^{path}/(?P<num>[0-9]{"{3}"}).*$') nums = [0] for file in xml_files: remach = regex.match(file) if remach: nums.append(int(remach.group('num'))) return max(nums) @staticmethod def get_xml_files(scan_path, scaner): """workspase function, getting all gummy_scan results in a directory""" xml_files = glob.glob( pathname=f'{scan_path}/[0-9][0-9][0-9]-{scaner}-*.xml') xml_files.sort() return xml_files def get_all_workspase(self): """workspase function, used to generate shell subcommands for workspase command""" result_path = self.config.start_config['MAIN']['result_path'] commands = dict() if os.path.exists(result_path): subfolders = [ f.path for f in os.scandir(result_path) if f.is_dir() ] for i, w in enumerate(subfolders): w_name = w[len(result_path) + 1:] m_len = len(self.get_xml_files(scan_path=w, scaner='m')) n_len = len(self.get_xml_files(scan_path=w, scaner='n')) commands[w_name] = f'scans: m[{m_len}], n[{n_len}]' return commands @staticmethod def get_scanner_methods(scanner): """function, used to generate shell subcommands for run command""" methods = dict() for func in dir(scanner): if callable(getattr(scanner, func)) and re.match( r'^_\d{3}_\w*$', func): methods[func[1:]] = getattr(scanner, func).__doc__ return methods @property def get_toolbar(self): """function to display the bottom toolbar""" t_left = f'workspace: {self.config.start_config["MAIN"]["workspase"]} | ' \ f'host: {self.db.get_count_host} | ' \ f'socket: {self.db.get_count_socket}' t_right = f'{datetime.datetime.now().strftime("%H:%M:%S")} bat: {get_battery()}' rows, columns = os.popen('stty size', 'r').read().split() toolbar = t_left + ' ' * (int(columns) - len(t_left) - len(t_right)) + t_right return toolbar async def start(self): """main function starting the interface loop task""" os.system('clear') print_formatted_text(self.logo, style=self.style) # Create Prompt. while True: try: session = PromptSession(message=self.prompt_str, completer=self.completer, lexer=self.lexer, style=self.style, history=self.history, enable_history_search=True, auto_suggest=AutoSuggestFromHistory(), bottom_toolbar=self.get_toolbar, wrap_lines=False) result = await session.prompt(async_=True) self.log.debug(f'input: {result}') if not result: continue elif result == 'clear': clear() continue elif result.strip().startswith('!P'): try: eval(result.strip().replace('!P', '')) except Exception as e: self.log.warning(e) continue else: m = self.grammar.match(result) if m: m_vars = m.variables() else: self.log.info('Invalid match') continue if m_vars.get('command') \ and m_vars.get('command') in list(self.commands.keys()) \ and m_vars.get('command') in list(self.c_function.keys()): cm = m_vars.get('command') cur_function = self.c_function[cm] if len(self.commands.get(cm)) == 0: cur_function() else: if m_vars.get('operator') and m_vars.get( 'operator') in list(self.commands.get(cm)): op = m_vars.get('operator') else: op = '' if m_vars.get('parameter'): pr = m_vars.get('parameter') else: pr = '' cur_function(operator=op, parameter=pr) else: self.log.info('invalid command') continue except (EOFError, KeyboardInterrupt): return def __call__(self): """function startin main asyncio loop""" async def sig_exit(): self.log.info('Why are you so?') loop = asyncio.get_event_loop() for sig_name in ('SIGINT', 'SIGTERM'): loop.add_signal_handler(getattr(signal, sig_name), lambda: asyncio.ensure_future(sig_exit())) use_asyncio_event_loop() shell_task = asyncio.gather(self.start()) with patch_stdout(): loop.run_until_complete(shell_task) for task in asyncio.Task.all_tasks(loop=loop): if task is not asyncio.Task.current_task(): task.cancel()
class Mscanner: """masscan port scanner module class""" def __init__(self, prog_path, scans_path, db): """ Initialization masscan scanner class object :param prog_path: path to the program :param scans_path: gummy_scan directory """ self.log = Log(name='mscan') self.db = db self._prog_path = prog_path self._scans_path = scans_path self.scan_name = None self.target = None self.target_exclude = None self.target_exclude_file = None self.port = None self.udp_port = None self.top_ports = None self.rate = None self._ob_last_name = '' self._ox_last_name = '' self._conf_last_name = '' self._hosts_file_last_name = '' self.ob_last_path = '' self.ox_last_path = '' self.conf_last_path = '' self.hosts_file_last_path = '' self._args = [] self.counter = 0 self.version = '' self.host = {} self._check_prog() def _check_prog(self): """checking the correctness of the path to the program""" m_reg = re.compile(r'(?:Masscan version) (?P<ver>[\d.]*) (?:[\s\S]+)$') try: procc = subprocess.Popen([self._prog_path, '-V'], bufsize=10000, stdout=subprocess.PIPE) mach = m_reg.search(bytes.decode(procc.communicate()[0])) self.version = mach.group('ver') self.log.info(f'Use: {self._prog_path} (Version {self.version})') except Exception: self.log.warning('Masscan was not found') raise Exception() async def __call__(self, **kwargs): """ start gummy_scan and save result in binary. :param kwargs: scan_name = first counter = 1 target = 10.10.1.0/16 includefile = <filename> target_exclude = 10.10.1.0/24, 10.10.2.0/24 port = '443' or '80,443' or '22-25' udp_port = '443' or '80,443' or '22-25' top_ports = 100 rate = 25000 """ self.scan_name = kwargs.get('scan_name') self.target = kwargs.get('target') self.includefile = kwargs.get('includefile') self.target_exclude = kwargs.get('target_exclude') self.hosts_file_last_path = kwargs.get('hosts_file_last_path') self.port = kwargs.get('port') self.udp_port = kwargs.get('udp_port') self.top_ports = kwargs.get('top_ports') self.rate = kwargs.get('rate') # parse start args if kwargs.get('counter') and kwargs.get('counter') is not None: self.counter = kwargs.get('counter') if self.scan_name and self.scan_name is not None: num = str(self.counter).zfill(3) if self.target: targ = self.target.replace('.', '-').replace('/', '#') targ = targ[:18] + '...' if len(targ) > 17 else targ else: targ = '' self._ob_last_name = f'{num}-m-{self.scan_name}-[{targ}].masscan' self._ox_last_name = f'{num}-m-{self.scan_name}-[{targ}].xml' self._conf_last_name = f'{num}-m-{self.scan_name}-[{targ}].conf' self._hosts_file_last_name = f'{num}-m-{self.scan_name}-[{targ}].host' self.ob_last_path = '/'.join((self._scans_path, self._ob_last_name)) self.ox_last_path = '/'.join((self._scans_path, self._ox_last_name)) self.conf_last_path = '/'.join((self._scans_path, self._conf_last_name)) self.hosts_file_last_path = '/'.join((self._scans_path, self._hosts_file_last_name)) else: self.log.warning('Missing required parameter: scan_name') return # generate masscan arg self._gen_args() self.counter += 1 await self._run_scan() await self._convert_masscan_to_xml() def _gen_args(self): """generating arguments to run gummy_scan""" # clear list self._args.clear() # prog_path self._args.append(self._prog_path) # target if self.target: self._args.append('--range') self._args.append(self.target) self.log.debug(f'Set: {"target":10} Value: {self.target}') elif self.includefile: self._args.append('--includefile') self._args.append(self.includefile) self.log.debug(f'Set: {"includefile":10} Value: {self.includefile}') else: self.log.warning('Missing required parameter: target') return # target_exclude if self.target_exclude: self._args.append('--exclude') self._args.append(self.target_exclude) self.log.debug(f'Set: {"target_exclude":10} Value: {self.target_exclude}') # target_exclude_file if self.target_exclude_file: self._args.append('--excludefile') self._args.append(self.target_exclude) self.log.debug(f'Set: {"target_exclude_file":10} Value: {self.target_exclude_file}') # port or top-ports if self.port or self.udp_port: if self.port: self._args.append('--ports') self._args.append(self.port) self.log.debug(f'Set: {"port":10} Value: {self.port}') if self.udp_port: self._args.append('--udp-ports') self._args.append(self.udp_port) self.log.debug(f'Set: {"udp-ports":10} Value: {self.udp_port}') elif self.top_ports: self._args.append('--top-ports') self._args.append(self.top_ports) self.log.debug(f'Set: {"top_ports":10} Value: {self.top_ports}') else: self.log.warning('Missing required parameter: port or top-ports or udp-ports') return # output self._args.append('-oB') self._args.append(self.ob_last_path) self.log.debug(f'Set: {"output":10} Value: {self.ob_last_path}') # rate if self.rate: self._args.append('--rate') self._args.append(self.rate) self.log.debug(f'Set: {"rate":10} Value: {self.rate}') else: self.log.warning('Argument "rate" not set base value is used') # static args self._args.append('--wait') self._args.append('1') self._args.append('--interactive') async def _read_stream(self, stream): """asynchronous output processing""" res_reg_rem = re.compile(r'(?:rate:\s*)' r'(?P<Rate>[\d.]+)' r'(?:[-,\w]+\s+)' r'(?P<Persent>[\d.]*)' r'(?:%\s*done,\s*)' r'(?P<Time>[\d:]*)' r'(?:\s*remaining,\s*found=)' r'(?P<Found>[\d]*)') res_reg_dis = re.compile(r'(?:Discovered open port )' r'(?P<Port>\d+)' r'(?:/)' r'(?P<Protocol>\w+)' r'(?: on )' r'(?P<IP>[\d.]+)') old_line = '' persent_old, found_old = 0, 0 mach_udp = 0 temp_soc_stor = list() while True: line = await stream.read(n=1000) if line: line = line.decode(locale.getpreferredencoding(False)) line = line.replace('\n', '') line = line.replace('\r', '') if line != old_line: mach_rem = res_reg_rem.search(line) mach_dis = res_reg_dis.search(line) if mach_rem: persent_new = mach_rem.group("Persent") found_new = mach_rem.group("Found") if found_new != found_old or float(persent_new) >= float(persent_old) + 5: persent_old, found_old = persent_new, found_new self.log.info(f'[{persent_new}%] ' f'Time: {mach_rem.group("Time")} ' f'Found: {int(found_new) + mach_udp}') if mach_dis: mach_soc = [{'addr': mach_dis.group('IP'), 'ports': [{'protocol': mach_dis.group('Protocol'), 'portid': mach_dis.group('Port'), 'state': 'open'}]}] if mach_soc not in temp_soc_stor: # check for duplicate records: temp_soc_stor.append(mach_soc) if mach_dis.group('Protocol') == 'udp': mach_udp += 1 self.db + mach_soc old_line = line else: break async def _run_scan(self): """run a gummy_scan using arguments""" self.log.debug(f'Write the command to a file {self.conf_last_path}') with open(self.conf_last_path, "w") as text_file: text_file.write(' '.join(self._args)) self.log.info('Scan start') self.log.debug(f'run: {" ".join(self._args)}') proc = await asyncio.create_subprocess_exec(*self._args, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT) await asyncio.wait([self._read_stream(proc.stdout)]) await proc.wait() self.log.info('Scan complete') async def _convert_masscan_to_xml(self): """convert masscan binary to xml format""" if os.stat(self.ob_last_path).st_size == 0: self.log.warning('The file is empty') else: self.log.debug(f'Сonvert {self.ob_last_path} to {self.ox_last_path}') args = [self._prog_path] + \ ['--readscan', self.ob_last_path] + \ ['-oX', self.ox_last_path] proc = await asyncio.create_subprocess_exec(*args, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT) stdout, stderr = await proc.communicate() self.log.info(stdout.decode().strip())
class Nscanner: """nmap scanner module class""" # TODO the process does not end at task.cancel() def __init__(self, prog_path, db, scans_path): """ initialization nmap scanner class object :param prog_path: path to the program :param scans_path: gummy_scan directory """ self.log = Log(name='nscan') self.db = db self.parser = Parser() self._prog_path = prog_path self._scans_path = scans_path self.scan_name = None self.target = None self.port = None self.udp_port = None self.scan_type = None self.version = None self._ox_last_name = None self.ox_last_path = None self._t_missing = 'Missing required parameter: {}' self._args = list() self.counter = 0 self._check_prog() self._args_basic = [ '-sV', '-Pn', '--disable-arp-ping', '-T4', '-O', '--version-light', '--stats-every', '1s' ] self._args_arp = ['-PR', '-sn', '--stats-every', '1s'] self._args_dns = ['-sL'] def _check_prog(self): """checking the correctness of the path to the program""" m_reg = re.compile(r'(?:Nmap version) (?P<ver>[\d.]*) (?:[\s\S]+)$') try: procc = subprocess.Popen([self._prog_path, '-V'], bufsize=10000, stdout=subprocess.PIPE) mach = m_reg.search(bytes.decode(procc.communicate()[0])) self.version = mach.group('ver') self.log.debug(f'Use: {self._prog_path} (Version {self.version})') except Exception: self.log.warning('Nmap was not found') raise Exception() async def __call__(self, **kwargs): """ Start gummy_scan and save result in XML. 001-basic-n-[192-168-1-50#24]-basic.xml :param kwargs: scan_name = first counter = 1 target = 10.10.1.0/16 port = '443' or '80,443' or '22-25' udp_port = '443' or '80,443' or '22-25' scan_type = 'fast' or 'basic' or 'full' :return: """ self.scan_name = kwargs.get('scan_name') self.target = kwargs.get('target') self.port = kwargs.get('port') self.udp_port = kwargs.get('udp_port') self.scan_type = kwargs.get('scan_type') if kwargs.get('counter') and kwargs.get('counter') is not None: self.counter = kwargs.get('counter') targ = self.target.replace('.', '-').replace('/', '#') targ = targ[:18] + '...' if len(targ) > 17 else targ self._ox_last_name = f'{str(self.counter).zfill(3)}-n-{self.scan_name}-[{targ}]-{self.scan_type}.xml' self.ox_last_path = '/'.join((self._scans_path, self._ox_last_name)) if self._gen_args(): await self._run_scan() self.parser(self.ox_last_path) # noinspection PyStatementEffect self.db + self.parser.result def _gen_args(self): """generating arguments to run gummy_scan""" # clear list self._args.clear() # required parameters: # prog_path self._args.append(self._prog_path) # target if self.target: self._args.append(self.target) self.log.debug(f'Set: {"target":10} Value: {self.target}') else: self.log.warning(self._t_missing.format('target')) return False # output self._args.append('-oX') self._args.append(self.ox_last_path) self.log.debug(f'Set: {"output":10} Value: {self.ox_last_path}') # optional parameters # port temp_ports_arg = [] temp_port = [] if self.port or self.udp_port: if self.port: temp_ports_arg.append('-sS') temp_port.append('T:' + self.port) if self.udp_port: temp_ports_arg.append('-sU') temp_port.append('U:' + self.udp_port) temp_ports_arg.append('-p') temp_ports_arg.append(','.join(temp_port)) self.log.debug(f'Set: {"port":10} Value: {",".join(temp_port)}') if self.scan_type == 'basic': self._args += temp_ports_arg self._args += self._args_basic elif self.scan_type == 'arp': self._args += self._args_arp elif self.scan_type == 'dns': self._args += self._args_dns return True async def _read_stream(self, stream): """asynchronous output processing""" full_body = '' last_print_line = 0 exclude_lines = [ r'^WARNING: Running Nmap setuid, as you are doing, is a major security risk.$', r'^WARNING: Running Nmap setgid, as you are doing, is a major security risk.$', r'^Starting Nmap .*$', r'^$', r'^Host is up.$', r'^Nmap scan report for [\d.]*$' ] while True: line = await stream.read(n=100) if line: line = line.decode(locale.getpreferredencoding(False)) full_body += line full_body_list = full_body.split('\n') total_line = len(full_body_list) - 1 if total_line > last_print_line: for line in full_body_list[last_print_line:total_line]: if any( re.search(regex, line) for regex in exclude_lines): self.log.debug(line) else: self.log.info(line) last_print_line = total_line else: break async def _run_scan(self): """run a gummy_scan using arguments""" self.log.info('Scan start') self.log.debug(f'run: {" ".join(self._args)}') proc = await asyncio.create_subprocess_exec( *self._args, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT) await asyncio.wait([self._read_stream(proc.stdout)]) await proc.wait() self.counter += 1 self.log.info('Scan complete')
class Scanner: """This class describes gummy_scan profiles, links gummy_scan modules and auxiliary modules.""" def __init__(self, db=None): """class object initialization""" # basic param: self.db = db self.log = Log(name='gummy') self.port_master = PortMaster() self.counter = 0 # complex gummy_scan param: self.complex_n_scan = None self.complex_m_scan = None self.complex_pars = None self.complex_res = None self.complex_hosts_file_last_path = None self.complex_step1 = False self.complex_step2 = False self.complex_step3 = False self.complex_step4 = False self.complex_confirmation_time = datetime.datetime.now() # config param: self.config = None self.masscan_path = None self.nmap_path = None self.workspace_path = None self.target = None self.target_exclude = None self.port = None self.top_ports = None self.rate = None self.scan_type = None # port ranges param: self.tcp_stage_1 = self.port_master(start=1, end=1000, protocol='tcp') self.tcp_stage_2 = self.port_master(start=1001, end=65535, protocol='tcp') self.udp_stage_1 = self.port_master(start=1, end=1000, protocol='udp') self.udp_stage_2 = self.port_master(start=1001, end=4000, protocol='udp') def create_hosts_file(self, file, hosts): """function that creates a file with a list of hosts""" try: with open(file, "w") as host_file: for host in hosts: host_file.write(host + '\n') except IOError: self.log.warning('failed to create file!') def sync(self, config): """updates settings from configuration class instance""" self.config = config self.masscan_path = config['MAIN'].get('masscan_path') self.nmap_path = config['MAIN'].get('nmap_path') self.workspace_path = config['MAIN'].get('workspace_path') self.target = config['MASSCAN'].get('target') self.target_exclude = config['MASSCAN'].get('target_exclude') self.port = config['MASSCAN'].get('port') self.top_ports = config['MASSCAN'].get('top_ports') self.rate = config['MASSCAN'].get('rate') self.scan_type = config['NMAP'].get('scan_type') async def __complex(self, stage): """ updates settings from configuration class instance takes an array with the necessary scanning steps used own variables of self.complex_... type :param stage: list[int] :return: """ if 1 in stage: if all([i is None for i in [self.complex_m_scan, self.complex_n_scan, self.complex_pars]]) \ or datetime.datetime.now() < (self.complex_confirmation_time + datetime.timedelta(seconds=10)): self.complex_step2, self.complex_step3, self.complex_step4 = False, False, False self.complex_n_scan = Nscanner(prog_path=self.nmap_path, scans_path=self.workspace_path, db=self.db) self.complex_m_scan = Mscanner(prog_path=self.masscan_path, scans_path=self.workspace_path, db=self.db) self.complex_pars = Parser() self.counter += 1 self.log.info(f'{" STEP 1 ":#^40}') await self.complex_n_scan(scan_name='arp', counter=self.counter, target=self.target, scan_type='arp') self.complex_pars(file=self.complex_n_scan.ox_last_path) arp_host = self.complex_pars.hosts self.counter += 1 await self.complex_m_scan(scan_name='stage_1', counter=self.counter, target=self.target, target_exclude=self.target_exclude, port=self.tcp_stage_1, udp_port=self.udp_stage_1, rate=self.rate) self.complex_pars(file=self.complex_m_scan.ox_last_path) self.create_hosts_file( hosts=set(arp_host + self.complex_pars.hosts), file=self.complex_m_scan.hosts_file_last_path) self.complex_hosts_file_last_path = self.complex_m_scan.hosts_file_last_path self.complex_res = self.complex_pars.result self.db + self.complex_pars.result self.complex_step1 = True else: self.log.info( 'А complex gummy_scan has already been started, if you want to override it - ' 'repeat start command for the next 10 seconds') self.complex_confirmation_time = datetime.datetime.now() return if 2 in stage: if self.complex_step1 and len(self.complex_pars.result) != 0: self.counter += 1 self.log.info(f'{" STEP 2 ":#^40}') await self.complex_m_scan( scan_name='stage_2', counter=self.counter, # target=','.join(self.complex_pars.hosts), includefile=self.complex_hosts_file_last_path, target_exclude=self.target_exclude, port=self.tcp_stage_2, udp_port=self.udp_stage_2, rate=self.rate) self.complex_pars(file=self.complex_m_scan.ox_last_path) self.create_hosts_file( hosts=self.complex_pars.hosts, file=self.complex_m_scan.hosts_file_last_path) self.complex_res = self.db.merge(self.complex_res, self.complex_pars.result) self.db + self.complex_pars.result self.complex_step2 = True else: self.log.info('There are no results of the previous stage') if 3 in stage: if self.complex_step2: self.log.info(f'{" STEP 3 ":#^40}') self.complex_n_scan = Nscanner(prog_path=self.nmap_path, scans_path=self.workspace_path, db=self.db) for host in self.complex_res: self.counter += 1 tcp = list() udp = list() for port in host['ports']: if port['state'] == 'open': if port['protocol'] == 'tcp': tcp.append(port['portid']) elif port['protocol'] == 'udp': udp.append(port['portid']) self.log.info( f'{host["addr"]} tcp:{",".join(tcp)} udp:{",".join(udp)}' ) await self.complex_n_scan(scan_name='basic', counter=self.counter, target=host['addr'], port=','.join(tcp), udp_port=','.join(udp), scan_type='basic') self.complex_step3 = True else: self.log.info('There are no results of the previous stage') if 4 in stage: if self.complex_step3: self.counter += 1 self.log.info(f'{" STEP 4 ":#^40}') await self.complex_m_scan( scan_name='swamp', counter=self.counter, target=self.target, target_exclude=self.complex_hosts_file_last_path, port=self.tcp_stage_2, udp_port=self.udp_stage_2, rate=self.rate) self.complex_pars(file=self.complex_m_scan.ox_last_path) self.create_hosts_file( hosts=self.complex_pars.hosts, file=self.complex_m_scan.hosts_file_last_path) self.db + self.complex_pars.result self.log.info('I found something in the swamp !!!') self.log.info(self.complex_pars.hosts) self.complex_step4 = True self.log.info(f'{" END ":#^40}') else: self.log.info('There are no results of the previous stage') # next following are the functions displayed in the user interface # you must use the functions starting with _iii_... # and add a short description def _101_complex_1(self): """M Scan the top 1000 TCP and top 1000 UDP ports of the current range""" asyncio.gather(self.__complex(stage=[1])) def _102_complex_2(self): """M Scan the bottom 64553 TCP and next 3000 UDP ports of the detected hosts""" asyncio.gather(self.__complex(stage=[2])) def _103_complex_3(self): """N Scan the detected hosts (found ports)""" asyncio.gather(self.__complex(stage=[3])) def _104_complex_4(self): """M Scan the remaining swamp""" asyncio.gather(self.__complex(stage=[4])) def _111_complex_1_2(self): """Sequential start of steps 1-2 complex gummy_scan""" asyncio.gather(self.__complex(stage=[1, 2])) def _112_complex_1_3(self): """Sequential start of steps 1-3 complex gummy_scan""" asyncio.gather(self.__complex(stage=[1, 2, 3])) def _113_complex_1_4(self): """Sequential start of steps 1-4 complex gummy_scan""" asyncio.gather(self.__complex(stage=[1, 2, 3, 4])) def _001_masscan(self): """Run Masscan manually""" self.counter += 1 m_scan = Mscanner(prog_path=self.masscan_path, scans_path=self.workspace_path, db=self.db) asyncio.gather( m_scan(scan_name='basic', counter=self.counter, target=self.target, target_exclude=self.target_exclude, port=self.port, top_ports=self.top_ports, rate=self.rate)) def _002_nmap(self): """Run Nmap manually""" self.counter += 1 n_scan = Nscanner(prog_path=self.nmap_path, scans_path=self.workspace_path, db=self.db) asyncio.gather( n_scan(scan_name=self.scan_type, counter=self.counter, target=self.target, port=self.port, scan_type=self.scan_type)) def _201_arp_discovery(self): """Discovering hosts with ARP ping scans (-PR)""" self.counter += 1 n_scan = Nscanner(prog_path=self.nmap_path, scans_path=self.workspace_path, db=self.db) asyncio.gather( n_scan(scan_name='arp', counter=self.counter, target=self.target, scan_type='arp')) def _202_dns_discovery(self): """Reverse DNS resolution (-sL)""" self.counter += 1 n_scan = Nscanner(prog_path=self.nmap_path, scans_path=self.workspace_path, db=self.db) asyncio.gather( n_scan(scan_name='dns', counter=self.counter, target=self.target, scan_type='dns'))