From 6b3277c7dc1f91765fa52d4c7cef37bcad744a3a Mon Sep 17 00:00:00 2001 From: Zecora Date: Mon, 29 Jul 2019 13:25:11 +0300 Subject: [PATCH 1/3] FTPScanner implementation added --- modules/network_scan/FTPScanner.py | 85 +++++++++++++++++++++++ modules/network_scan/test_FTPScanner.py | 92 +++++++++++++++++++++++++ 2 files changed, 177 insertions(+) create mode 100644 modules/network_scan/FTPScanner.py create mode 100644 modules/network_scan/test_FTPScanner.py diff --git a/modules/network_scan/FTPScanner.py b/modules/network_scan/FTPScanner.py new file mode 100644 index 0000000..3f2ab40 --- /dev/null +++ b/modules/network_scan/FTPScanner.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python +# -*- coding:utf-8 -*- + +from core.prototypes.AbstractScanner import AbstractScanner +import ftplib +from ftplib import FTP + +MAX_ERRORS = 3 + + +class FTPScanner(AbstractScanner): + def __init__(self, timeout): + self.__timeout__ = timeout + + def scan_address(self, host: 'ipv4_str or hostname', port: 'port', credentials: 'tuples with login and password') -> {'scan_result'}: + result = self.ftp_anonymous_login(host, port, self.__timeout__) + if result['status'] == 'error' or result['anonymous_login']: + return result + result['credentials'] = self.ftp_bruteforce( + host, port, credentials, self.__timeout__) + return result + + @staticmethod + def ftp_anonymous_login(host, port, timeout): + '''Get version and check if anonympous login is enabled''' + result = {} + ftp_connection = FTP(timeout=timeout) + try: + version = ftp_connection.connect(host=host, port=port) + # Get something like "220 Twisted 16.6.0 FTP Server" + result['ftp_version'] = version.lstrip('220 ') + # Try to login as anonymous user + ftp_connection.login() + result['anonymous_login'] = True + result['status'] = 'ok' + except ftplib.error_perm as e: + if str(e).startswith("530"): + result['status'] = 'ok' + result['anonymous_login'] = False + except ftplib.all_errors as e: + result['status'] = 'error' + result['error_type'] = str(e) + return result + finally: + ftp_connection.close() + return result + + @staticmethod + def ftp_bruteforce(host, port, creds, timeout): + '''Attempt to brute force login/password pair''' + # We want maintain connection to speed up bruteforce + # but we also want to reconnect if necessary. + # That is why I use cred iterator to pick up new login/pass only when + # we need to. + error_count = 0 + it = iter(creds) + cred = next(it, "") + ftp_connection = FTP(timeout=timeout) + while error_count < MAX_ERRORS: + try: + # Connecting to server + ftp_connection.connect(host=host, port=port) + while cred and error_count < MAX_ERRORS: + user, password = cred + # Trying to log in + try: + ftp_connection.login(user, password) + ftp_connection.close() + return user, password + except ftplib.error_perm as e: + # Password was wrong, checking another + cred = next(it, "") + continue + except ftplib.all_errors as e: + error_count += 1 + # Connection was dropped or another network error happened + # We must connection, error_count would help us to + # avoid deadlock on mumbling host + break + except ftplib.all_errors as e: + # Cannot reconnect, give up + break + finally: + ftp_connection.close() + return None diff --git a/modules/network_scan/test_FTPScanner.py b/modules/network_scan/test_FTPScanner.py new file mode 100644 index 0000000..e97826e --- /dev/null +++ b/modules/network_scan/test_FTPScanner.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python +# -*- coding:utf-8 -*- + +from modules.network_scan.FTPScanner import FTPScanner +from pyftpdlib.authorizers import DummyAuthorizer +from pyftpdlib.handlers import FTPHandler +from pyftpdlib.servers import FTPServer +import unittest +from tempfile import mkdtemp +import multiprocessing +from time import sleep + +import http.server +import socketserver +import os + +TEST_CREDS = (("admin", "admin"), ("1", "1"), ('user', 'password')) +PORT = 2121 + + +def run_anonymous_ftp(temp_dir): + authorizer = DummyAuthorizer() + authorizer.add_anonymous(temp_dir) + handler = FTPHandler + handler.authorizer = authorizer + server = FTPServer(("127.0.0.1", PORT), handler) + server.serve_forever() + + +def run_bruteforce_ftp(temp_dir): + authorizer = DummyAuthorizer() + user, password = TEST_CREDS[-1] + authorizer.add_user(user, password, temp_dir, perm="elradfmw") + handler = FTPHandler + handler.authorizer = authorizer + handler.max_login_attempts = 2 # Drop connection on each 2 incorrect attempts + server = FTPServer(("127.0.0.1", PORT), handler) + server.serve_forever() + + +def run_mumble(): + handler = http.server.SimpleHTTPRequestHandler + httpd = socketserver.TCPServer(("127.0.0.1", PORT), handler) + httpd.serve_forever() + + +class TestFTPScanner(unittest.TestCase): + def test_closed_port(self): + scanner = FTPScanner(timeout=10) + result = scanner.scan_address('127.0.0.1', 31337, credentials=TEST_CREDS) + print(result) + self.assertEqual(result['status'], 'error', "Should be error") + self.assertTrue("Connection refused" in result['error_type'], "Connection refused") + + def test_mumble(self): + p = multiprocessing.Process(target=run_mumble) + p.start() + sleep(5) + scanner = FTPScanner(timeout=10) + result = scanner.scan_address('127.0.0.1', PORT, credentials=TEST_CREDS) + print(result) + self.assertEqual(result['status'], 'error', "Should be error") + self.assertTrue("timed out" in result['error_type'], "Timed out") + p.terminate() + + def test_anonymous_login(self): + temp_dir = mkdtemp() + p = multiprocessing.Process(target=run_anonymous_ftp, args=(temp_dir,)) + p.start() + sleep(5) + scanner = FTPScanner(timeout=10) + result = scanner.scan_address('127.0.0.1', PORT, credentials=TEST_CREDS) + print(result) + self.assertEqual(result['anonymous_login'], True, "Should be True") + p.terminate() + os.rmdir(temp_dir) + + def test_bruteforce(self): + temp_dir = mkdtemp() + p = multiprocessing.Process(target=run_bruteforce_ftp, args=(temp_dir,)) + p.start() + sleep(5) + scanner = FTPScanner(timeout=10) + result = scanner.scan_address('127.0.0.1', PORT, credentials=TEST_CREDS) + print(result) + self.assertEqual(result['credentials'], TEST_CREDS[-1], "Should be True") + p.terminate() + os.rmdir(temp_dir) + + +if __name__ == '__main__': + unittest.main() From 8e1fc5a369bdf211a471e6b6cca025a219898e21 Mon Sep 17 00:00:00 2001 From: Zloooy Date: Wed, 13 Nov 2019 19:15:54 +0300 Subject: [PATCH 2/3] Added some russian docs. Changed prototype behaviour. Added __init__ args to config. Now JSONStorage supports dynamic database schemes defined in config. --- README.md | 4 +- README.ru.md | 14 ++- config.py | 59 ++++++++++- core/MainPresenter.py | 79 ++++++++------- core/communication/ConvertTable.py | 25 +++++ core/communication/communication_utils.py | 17 +++- core/prototypes/AbstractAddressGenerator.py | 2 + core/prototypes/AbstractModuleClass.py | 35 ++++--- core/prototypes/AbstractParser.py | 3 +- core/prototypes/AbstractScanner.py | 2 + docs/classes.ru.md | 43 ++++++++ docs/config.ru.md | 15 +++ docs/developing_custom_module.ru.md | 17 ++++ .../GDocsAddressGenerator.py | 15 ++- .../convert_functions/gdoc_prefix_hash2Url.py | 2 + modules/convert_functions/response2text.py | 2 + modules/network_scan/CoreModel.py | 2 +- modules/network_scan/GDocsScanner.py | 36 +++++++ modules/network_scan/GoogleSearcher.py | 31 ++++++ modules/network_scan/URLScanner.py | 2 +- modules/storage/GDocsStorage.py | 15 ++- modules/storage/JSONStorage.py | 97 +++++++++++++++++-- results.json | 1 + 23 files changed, 433 insertions(+), 85 deletions(-) create mode 100644 docs/classes.ru.md create mode 100644 docs/config.ru.md create mode 100644 docs/developing_custom_module.ru.md create mode 100644 modules/convert_functions/gdoc_prefix_hash2Url.py create mode 100644 modules/convert_functions/response2text.py create mode 100644 modules/network_scan/GDocsScanner.py create mode 100644 modules/network_scan/GoogleSearcher.py create mode 100644 results.json diff --git a/README.md b/README.md index 8aad164..476c5eb 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,8 @@ INSTALLATION ------------ Just run ```bash -git clone http://github.com/ChronosX88/PySca.git -cd PySca +git clone http://github.com/Zloooy/PySca.git +cd PyNesca pip install -r requirements.txt ``` diff --git a/README.ru.md b/README.ru.md index afb12e1..bf1c5c5 100644 --- a/README.ru.md +++ b/README.ru.md @@ -10,8 +10,8 @@ PySca - сетевой сканер, переписанный на Python ------------ Введите в терминале ```bash -git clone http://github.com/ChronosX88/PySca.git -cd PySca +git clone http://github.com/Zloooy/PySca.git +cd PyNesca pip install -r requirements.txt ``` @@ -22,3 +22,13 @@ pip install -r requirements.txt python main.py ``` Находясь в корневой папке PySca + +НАСТРОЙКА +------------ +Тут [описание синтаксиса config.py](./docs/config.ru.md) + + +ХОЧУ СДЕЛАТЬ СВОЙ МОДУЛЬ ДЛЯ PySca +------------- +Тут [общие требования к оформлению модуля](./docs/developing_custom_module.md) +Тут [описание функций конкретных модулей](./docs/classes.ru.md) diff --git a/config.py b/config.py index bade370..d444d27 100644 --- a/config.py +++ b/config.py @@ -1,7 +1,56 @@ -#modules selection +#modules and selection and init args setup config = { -"parser" : "GDocsHashParser", -"address_generator" : "GDocsAddressGenerator", -"scanner" : "URLScanner", -"storage" : "GDocsStorage" + "parser" : + { + "name":"GDocsHashParser", + "init_args":{} + }, + "address_generator" : + { + "name":"GDocsAddressGenerator", + "init_args":{} + }, + "scanner" : + { + "name":"GDocsScanner", + "init_args":{} + }, + "storage" : + { + "name":"JSONStorage", + "init_args": + { + "path":"results.json", + "json_scheme":{ + "status": + { + "gdoc_prefix": + [ + { + "@hash": "gdoc_hash", + "@title": "gdoc_title" + } + ] + } + } + } + } } + +'''scheme for url scanner +{ + "status": + { + "url" + } +}''' +'''scheme for port scanner +{ + "ipv4_str": + { + "port_status_str": + { + "port" + } + } +}''' diff --git a/core/MainPresenter.py b/core/MainPresenter.py index 87bb65a..e3492a1 100644 --- a/core/MainPresenter.py +++ b/core/MainPresenter.py @@ -6,19 +6,19 @@ from PyQt5.Qt import QThread, pyqtSignal from PyQt5.QtCore import QObject, pyqtSlot from config import config from inspect import isfunction -from communication.communication_utils import complitable_functions, get_converted_arguments +from communication.communication_utils import complitable_functions, get_converted_arguments, get_argument_annotations, get_return_annotations CoreModel = import_utils.import_class("modules/network_scan/%s.py" % -config["scanner"]) +config["scanner"]["name"]) Parser = import_utils.import_class("modules/address_generation/%s.py" % - config["parser"] + config["parser"]["name"] ) IpGenerator = import_utils.import_class( "modules/address_generation/%s.py" % -config["address_generator"] +config["address_generator"]["name"] ) JSONStorage = import_utils.import_class("modules/storage/%s.py" % -config["storage"]) +config["storage"]["name"]) convert_table = ConvertTable() for func in import_utils.import_matching( "modules/convert_functions/", @@ -36,15 +36,42 @@ for function in [ msg = "%s is not complitable with %s" print(msg % (function, previous)) previous = function +convert_for_parser = convert_table.get_metaconverter( + {'address_field','port_field'}, + get_argument_annotations(Parser.parse_fields) +) +convert_for_address_generator = convert_table.get_metaconverter( + get_return_annotations(Parser.parse_fields), + get_argument_annotations(IpGenerator.set_parsed_fields) +) +convert_for_scanner = convert_table.get_metaconverter( + get_return_annotations(IpGenerator.get_next_address), + get_argument_annotations(CoreModel.scan_address) +) +convert_for_address_generator_reverse = convert_table.get_metaconverter( + get_return_annotations(IpGenerator.get_next_address).union(get_return_annotations(CoreModel.scan_address)), + get_argument_annotations(IpGenerator.get_next_address) +) +convert_for_storage = None class MainPresenter: def __init__(self, ui): self.ui = ui self.threads = [] self.isScanEnabled = False - self.parser = Parser() + self.parser = Parser(*get_converted_arguments(Parser.__init__, + config['parser']['init_args'], convert_table)) #needed config to specify path - self.storage = JSONStorage("results.json") + print(*get_converted_arguments(JSONStorage.__init__, + config["storage"]["init_args"], convert_table)) + self.storage = JSONStorage(*get_converted_arguments(JSONStorage.__init__, + config["storage"]["init_args"], convert_table)) + print(get_argument_annotations(self.storage.put_responce)) + global convert_for_storage + convert_for_storage = convert_table.get_metaconverter( + get_return_annotations(IpGenerator.get_next_address).union(get_return_annotations(CoreModel.scan_address)), + get_argument_annotations(self.storage.put_responce) + ) self.exit_lock = RLock() def startScan(self, ipRanges, portsStr, threadNumber, timeout): @@ -52,25 +79,21 @@ class MainPresenter: addresses = None parser_args = {'port_field':portsStr, 'address_field':ipRanges} fields = self.parser.parse_fields( - *get_converted_arguments( - self.parser.parse_fields, - parser_args, - convert_table - ) + *convert_for_parser(parser_args) ) - self.scanner = CoreModel(timeout) + config["scanner"]["init_args"]["timeout"] = timeout + self.scanner = CoreModel(*get_converted_arguments(CoreModel.__init__, + config["scanner"]["init_args"], convert_table)) if CoreModel.INDEPENDENT_THREAD_MANAGEMENT: addresses = self.parser.get_all_addresses(ipRanges) self.ip_generator = PlugAddressGenerator(addresses, ports) threadNumber = 1 else: - self.ip_generator = IpGenerator() + self.ip_generator = IpGenerator( + *get_converted_arguments(IpGenerator.__init__, + config["address_generator"]["init_args"], convert_table)) self.ip_generator.set_parsed_fields( - *get_converted_arguments( - self.ip_generator.set_parsed_fields, - fields, - convert_table - ) + *convert_for_address_generator(fields) ) threadNumber = int(threadNumber) print("thread %i number set" % threadNumber) @@ -142,30 +165,18 @@ class ScanWorker(QObject): while self.isRunning: print("worker start") scan_address = self.ip_generator.get_next_address( - *get_converted_arguments( - self.ip_generator.get_next_address, - self.previous_address, - convert_table - ) + *convert_for_address_generator_reverse(self.previous_address) ) if not scan_address: break scan_result = self.scanner.scan_address( - *get_converted_arguments( - self.scanner.scan_address, - scan_address, - convert_table - ) + *convert_for_scanner(scan_address) ) print(scan_result) scan_address.update(scan_result) self.previous_address = scan_address self.storage.put_responce( - *get_converted_arguments( - self.storage.put_responce, - scan_address, - convert_table - ) + *convert_for_storage(scan_address) ) string_scan_address = " ".join(key + ":" + str(scan_address[key]) for key in scan_address.keys()) diff --git a/core/communication/ConvertTable.py b/core/communication/ConvertTable.py index 9e0d0d8..f27856d 100644 --- a/core/communication/ConvertTable.py +++ b/core/communication/ConvertTable.py @@ -34,3 +34,28 @@ class ConvertTable(): raise Exception("There is no converter for %s to %s" % (from_keys, to_key)) return None, None + + def get_metaconverter(self, from_keys, to_keys): + '''This function constructs and returns new function used to provide fast + conversion from from_keys to to_keys''' + converters_args = [] + converters = [] + for key in to_keys: + keys_to_convert, converter = None, None + if key in from_keys: + keys_to_convert = [key] + converter = lambda x : {key: x} + else: + keys_to_convert, converter = self.get_converter(from_keys, key) + converters_args.append(keys_to_convert) + converters.append(converter) + + def metaconverter(args_dict): + if args_dict == None: + return [None] * len(converters) + res = [] + for i,conv in enumerate(converters): + args = [args_dict[arg] for arg in converters_args[i]] + res.append(*[value for key, value in conv(*args).items()]) + return res + return metaconverter diff --git a/core/communication/communication_utils.py b/core/communication/communication_utils.py index 07c7070..67320a6 100644 --- a/core/communication/communication_utils.py +++ b/core/communication/communication_utils.py @@ -1,7 +1,13 @@ +def get_argument_annotations(func): + return list(value for key, value in func.__annotations__.items() if key != + 'return') + +def get_return_annotations(func): + return func.__annotations__['return'] + def complitable_functions(output_function, input_function, convert_table): - input_keys = set(value for key, value in - input_function.__annotations__.items() if key != 'return') - return_keys = set(output_function.__annotations__["return"]) + input_keys = set(get_argument_annotations(input_function)) + return_keys = set(get_return_annotations(output_function)) all_possible_return_keys = return_keys.union( convert_table.all_possible_conversions(return_keys) ) @@ -10,9 +16,13 @@ def complitable_functions(output_function, input_function, convert_table): return input_keys.issubset(all_possible_return_keys) def get_converted_arguments(function, simple_arg_dict, convert_table): + #This function returns list of arguments needed by function made from + #simple_arg_dict by convert_table if simple_arg_dict == None: return [None for key in function.__annotations__.keys() if key != 'return'] result = [] + if not hasattr(function, "__annotations__"): + return result for key, value in function.__annotations__.items(): if key != 'return': converted_arg = None @@ -29,4 +39,3 @@ def get_converted_arguments(function, simple_arg_dict, convert_table): )[value] result.append(converted_arg) return result - diff --git a/core/prototypes/AbstractAddressGenerator.py b/core/prototypes/AbstractAddressGenerator.py index 07a30f9..90d0c3c 100644 --- a/core/prototypes/AbstractAddressGenerator.py +++ b/core/prototypes/AbstractAddressGenerator.py @@ -3,6 +3,8 @@ from core.prototypes.AbstractModuleClass import AbstractModuleClass class AbstractAddressGenerator(AbstractModuleClass): '''The class describes addess generation mechanism.''' + INPUT_FUNCTIONS = {"set_parsed_fields", "get_next_address"} + OUTPUT_FUNCTIONS = {"get_next_address", "get_all_addresses"} @abstractmethod def set_parsed_fields(self): '''This method is called after generator initialization. It is used to diff --git a/core/prototypes/AbstractModuleClass.py b/core/prototypes/AbstractModuleClass.py index a041373..7a949ab 100644 --- a/core/prototypes/AbstractModuleClass.py +++ b/core/prototypes/AbstractModuleClass.py @@ -1,17 +1,30 @@ -def internal(func): - func.is_internal = True - return func - class AbstractModuleClassType(type): def __new__(self, name, bases, attrs): print("creating class", name) + base_class = None + if len(bases) != 0: + base_class = bases[0] + input_function_names = None + output_function_names = None + if base_class: + input_function_names = getattr(base_class, "INPUT_FUNCTIONS") + output_function_names = getattr(base_class, "OUTPUT_FUNCTIONS") + else: + input_function_names = attrs["INPUT_FUNCTIONS"] + output_function_names = attrs["OUTPUT_FUNCTIONS"] + if not name.startswith("Abstract"): for attrname, attrvalue in attrs.items(): if type(attrvalue).__name__ == 'function': - if attrvalue.__name__ not in ["__init__", "save"] and not (hasattr(attrvalue, "is_internal") and attrvalue.is_internal - ): - if not name.endswith("Storage"): + if attrvalue.__name__ in input_function_names: + if len(list(filter(lambda x: x!= "return", + attrvalue.__annotations__.keys()))) == 0: + raise Exception( + "%s.%s:no input annotations." % + (name, attrname) + ) + if attrvalue.__name__ in output_function_names: try: attrvalue.__annotations__["return"] except KeyError: @@ -19,14 +32,10 @@ class AbstractModuleClassType(type): "%s.%s: return type is not defined!" % (name, attrname) ) - if not name.endswith("Parser"): - if not attrvalue.__annotations__: - raise Exception( - "%s.%s: arguments missing annotations!" % - (name, attrname) - ) return super().__new__(self, name, bases, attrs) class AbstractModuleClass(metaclass = AbstractModuleClassType): REQUIED_INPUT_KEYS = None OUTPUT_KEYS = [] + INPUT_FUNCTIONS = {} + OUTPUT_FUNCTIONS = {} diff --git a/core/prototypes/AbstractParser.py b/core/prototypes/AbstractParser.py index 3f55f93..891903c 100644 --- a/core/prototypes/AbstractParser.py +++ b/core/prototypes/AbstractParser.py @@ -4,7 +4,8 @@ from core.prototypes.AbstractModuleClass import AbstractModuleClass class AbstractParser(AbstractModuleClass): '''The class describes fields parsing mechanisms''' - + INPUT_FUNCTIONS = {} + OUTPUT_FUNCTIONS = {"parse_fields"} @abstractmethod def parse_fields(self, args): '''In address field can be plased any text, describing address of diff --git a/core/prototypes/AbstractScanner.py b/core/prototypes/AbstractScanner.py index d959bd4..85c7eeb 100644 --- a/core/prototypes/AbstractScanner.py +++ b/core/prototypes/AbstractScanner.py @@ -6,6 +6,8 @@ class AbstractScanner(AbstractModuleClass): If it can manage many threads by itself set INDEPENDENT_THREAD_MANAGEMENT to "True"''' INDEPENDENT_THREAD_MANAGEMENT = False + INPUT_FUNCTIONS = {"scan_address"} + OUTPUT_FUNCTIONS = {"scan_address"} @abstractmethod def scan_address(self, address): diff --git a/docs/classes.ru.md b/docs/classes.ru.md new file mode 100644 index 0000000..1442b2c --- /dev/null +++ b/docs/classes.ru.md @@ -0,0 +1,43 @@ +# Классы-прототипы модулей PySca +Процесс сканирования в PySca разделён на этапы. За каждый этап сканирования отвечает отдельный класс - модуль, реализующий все необходимые для данного этапа функции, декларированнынные в соответствующем классе-прототипе. +Каждая функция модуля PySca - элемент конвеера. На вход она получает результаты выполнения предидущей функции (явно - в виде словаря или же как значения аргументов согласно аннотациям к аргументам) другого модуля и возвращет значение в следующую в цепочке функцию. Связи между функциями можно представить в виде таблицы: + Название функции в абстрактном классе | Функция-источник аргументов | Функция - приёмник результатов +:------------------------------------: | :---------------------------: | :------------------------------: + AbstractParser.parse_fields | вводится пользователем | AbstractAddresGenerator.set_parsed_fields + AbstractAddressGenerator.set_parsed_fields | AbstractParser.parse_fields | нет + AbstractAddressGenerator.get_next_address | AbstractScanner.scan_address + AbstractAddressGenerator.get_next_address или None при первом запросе адреса | AbstractScanner.scan_address и AbstractStorage.put_responce + AbstractScanner.scan_address | AbstractAddressGenerator.get_next_address | AbstractAddressGenerator.get_next_address и AbstractStorage.put_responce + AbstractStorage.put_responce | AbstractAddressGenerator.get_next_address + AbstractScanner.scan_address | нет + AbstractStorage.save | нет | нет +## Описание классов модулей по отдельности +### AbstractParser + Задача этого класса - обработка пользовательского ввода, преобразование строк в именованные python-объекты. +#### Методы AbstractParser + * __init__() + В аргументы передаются запрошенные параметры из config.py + * parse_fields() + В аргументы функции передаётся содержимое текстовых полей, введённое пользователем. +### AbstractAddressGenerator + Задача модулей-наследников этого класса - обработка вывода парсера и генерация адресов - задач для сканирования на основе не только данных парсера, но и результатов, полученных от сканирования предидущих адресов. +#### Методы AbstractAddressGenerator + * __init__() + В аргументы передаются запрошенные параметры из config.py + * set_parsed_fields() + В аргументы получает разультаты AbstractParser.parse_fields. Если нужно обрабатывает их и сохраняет как поля класса. + * get_next_address() + В аргументы получает либо None как значение всех аргументов - для получения первого адреса, либо результаты работы AbstractScanner.scan_address + результаты собственной работы (тот адрес, который сканировал экземпляр AbstractScanner). На основе полученных данных/внутренних полей возвращает адрес для последующего сканирования либо None, если адресов больше нет. + ВАЖНО: Так как обращения к функции класса возможны в асинхронном виде, рекомндуется либо оборачивать код функции в lock класса Threading, либо использоват потокобезопасные структуры как поля класса (Queue и т. п.). +### AbstractScanner + Модули этого класса отвечают за сам процесс сканирования. На данный момент доступно сканирование через функцию только одного адреса, своя реализация параллелизма пока невозможна. +#### Методы AbstractScanner + * __init__() + В аргументы передаются запрошенные параметры из config.py + * scan_address() + На вход метод получает адрес, сгенерированный AbstractAddresGenerator'ом, возвращает результаты сканирования. В процессе сканирования не рекомендуется менять поля класса, а если менять, то только потокобезопасно. +#### Методы AbstractStorage + * __init__() + В аргументы передаются запрошенные параметры из config.py + * put_responce() + На вход получает сумму результатов выполнения AbstractAddressGenerator.get_next_address и AbstractScammer.scan_address и сохраняет их себе в поля/ в реальном времени записывает их в файл. + * save() + Метод вызывается в конце сканирования, когда все потоки скаенра остановлены. Сохраняет информацию в файл. diff --git a/docs/config.ru.md b/docs/config.ru.md new file mode 100644 index 0000000..d706505 --- /dev/null +++ b/docs/config.ru.md @@ -0,0 +1,15 @@ +# Что такое config.py +config.py - инструмент для выбора модулей цепочки и их предварительной конфигурации. +Синтаксис [config.py](../config.py) выглядит так: +``` +config = { + "parser": + { + "name":[ИМЯ_ПАРСЕРА] + "init_args":[словарь с аргументами инициализации] + } +... +} +``` +Здесь ИМЯ_ПАРСЕРА - это название файла модуля без расширения и класса в нём, который будет отвечать за парсинг пользовательского ввода. +словарь с аргументами инициализации - все аргументы(кроме self), которые могут понадобиться при инициализации(__init__) модуля этой направленности (не только выбранного, но и любого другого. Может сохраняться при смене модуля, если другой имеет все требуемые аргументы в этом словаре. diff --git a/docs/developing_custom_module.ru.md b/docs/developing_custom_module.ru.md new file mode 100644 index 0000000..5c9eaa4 --- /dev/null +++ b/docs/developing_custom_module.ru.md @@ -0,0 +1,17 @@ +# Создание собственного модуля +Каждый модуль PySca - класс, отвечающий за конкретные задачи в цепочке сканирования. Чтобы ваш код стал модулем сканера, необходимо соблюдать следующие условия: +* Модуль должен представлять из себя отдельный файл, содержащий в себе единственный класс, в методах которого и реализована логика модуля. Имя файла модуля должно совпадать с именем содержащегося в нём класса. +* Наследование класса модуля от одного из абстрактных классов AbstractParser, AbstractAddressGenerator, AbstractScanner, AbstractStorage в соответствии с функционалом. +* Расположение файла модуля в общей системе папок PySca зависит от класса-родителя модуля и определяется по этой таблице: + Класс-родитель | Адрес[^pysca_folder_system] +:--------------:|:-----: + AbstractParser | modules/address_generation + AbstractAddressGenerator | modules/address_generation + AbstractScanner | modules/network_scan + AbstractStorage | modules/storage +[^pysca_folder_system]: Все адреса приведены относительно корневого каталога PySca. +* Реализация всех методов класса-родителя с соблюдением условий: + * Аннотация всех аргументов методов (если они необходимы)[^self_argument] ключами требуемых значений. + * Аннотация возвращаемых методами значений в виде множества/списка возвращаемых ключей (если метод должен что-то возвращать). + * Соответствие возвращаемых методом значений аннотации - возвращение словаря с парами ключ-значение, где ключ - элемент из списка заявленных ключей, а значение - соответствующие ему данные, либо возвращение None. +[^self_argument]: Разумеется, self в аннотации не нуждается, более того, аннотация self может сломать сканер. diff --git a/modules/address_generation/GDocsAddressGenerator.py b/modules/address_generation/GDocsAddressGenerator.py index 39648db..aec0c6e 100644 --- a/modules/address_generation/GDocsAddressGenerator.py +++ b/modules/address_generation/GDocsAddressGenerator.py @@ -1,5 +1,4 @@ from core.prototypes.AbstractAddressGenerator import AbstractAddressGenerator -from core.prototypes.AbstractModuleClass import internal class GDocsAddressGenerator(AbstractAddressGenerator): def set_parsed_fields(self, prefix:"gdocs_prefix", @@ -13,7 +12,6 @@ class GDocsAddressGenerator(AbstractAddressGenerator): self.hashlen = len(ranges[0][0]) self.currange = self.ranges.pop(0) - @internal def hash2int(self, gdhash): alen = len(self.alphabet) res = 0 @@ -22,7 +20,6 @@ class GDocsAddressGenerator(AbstractAddressGenerator): res += self.revsymbols[symb] return res - @internal def int2hash(self, hint): alen = len(self.alphabet) reshash = [self.alphabet[0]]*self.hashlen @@ -31,15 +28,15 @@ class GDocsAddressGenerator(AbstractAddressGenerator): reshash[i] = self.alphabet[rest] return "".join(reshash) - def get_next_address(self, prev_url:'url') -> {"url"}: - if not prev_url: - return {'url':self.prefix + self.currange[0]} - prev_hash = prev_url[prev_url.rfind('/') + 1:] + def get_next_address(self, prev_hash:'gdoc_hash') -> {"gdoc_prefix", "gdoc_hash"}: + if not prev_hash: + return {'gdoc_prefix':self.prefix, "gdoc_hash":self.currange[0]} + #prev_hash = prev_url[prev_url.rfind('/') + 1:] if self.hash2int(self.currange[1]) <= self.hash2int(prev_hash): if not self.ranges: return None self.currange = self.ranges.pop(0) - return {'url' : self.prefix + self.currange[0]} - return {'url' : self.prefix + self.int2hash(self.hash2int(prev_hash) + + return {'gdoc_prefix' : self.prefix, 'gdoc_hash':self.currange[0]} + return {'gdoc_prefix' : self.prefix, 'gdoc_hash':self.int2hash(self.hash2int(prev_hash) + 1)} def get_all_addresses(self) -> {'gdocs_prefix', 'gdocs_hash_ranges'}: diff --git a/modules/convert_functions/gdoc_prefix_hash2Url.py b/modules/convert_functions/gdoc_prefix_hash2Url.py new file mode 100644 index 0000000..ae0608a --- /dev/null +++ b/modules/convert_functions/gdoc_prefix_hash2Url.py @@ -0,0 +1,2 @@ +def gdocs_prefix_hash2Url(prefix:'gdoc_prefix', ghash:'gdoc_hash') -> {'url'}: + return {'url':prefix + ghash} diff --git a/modules/convert_functions/response2text.py b/modules/convert_functions/response2text.py new file mode 100644 index 0000000..5c1c986 --- /dev/null +++ b/modules/convert_functions/response2text.py @@ -0,0 +1,2 @@ +def response2text(response:'response') -> {'text'}: + return {'text': response.text} diff --git a/modules/network_scan/CoreModel.py b/modules/network_scan/CoreModel.py index 733537f..d1c425e 100644 --- a/modules/network_scan/CoreModel.py +++ b/modules/network_scan/CoreModel.py @@ -2,7 +2,7 @@ import socket from core.prototypes.AbstractScanner import AbstractScanner class CoreModel(AbstractScanner): - def __init__(self, timeout): + def __init__(self, timeout:"timeout"): self.defSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.defSocket.settimeout(int(timeout)) diff --git a/modules/network_scan/GDocsScanner.py b/modules/network_scan/GDocsScanner.py new file mode 100644 index 0000000..0ec8a31 --- /dev/null +++ b/modules/network_scan/GDocsScanner.py @@ -0,0 +1,36 @@ +import json +import requests +from urllib.parse import urlencode, urljoin +import lxml.html +from core.prototypes.AbstractScanner import AbstractScanner +class GDocsScanner(AbstractScanner): + def __init__(self, timeout:"timeout"): + pass + def scan_address(self, prefix:"gdoc_prefix", ghash:"gdoc_hash") -> {"response", + "gdoc_info", "gdoc_title"}: + print("Scanning", prefix, ghash) + response = requests.get(prefix+ghash) + if response.status_code != 200: + return {"response":response, "gdoc_info":None, "gdoc_title":None} + print(response.status_code) + response_tree = lxml.html.fromstring(response.text) + (title,) = response_tree.xpath("//meta[@property='og:title']/@content") + (token_container,) = response_tree.xpath('//script[contains(text(),"token")]') + token_container = token_container.text + token_container = token_container[token_container.find("{"):token_container.rfind("}") + 1] + #print(json.dumps(json.loads(token_container), indent=4, sort_keys=True)) + try: + info_params = json.loads(token_container)["info_params"] + except json.JSONDecodeError: + return {"response":response, "gdoc_info":None, "gdoc_title":None} + #print(info_params) + info = None + if "token" in info_params.keys(): + info_params.update({"id":ghash}) + info_url = urljoin(prefix, ghash+"/docdetails/read?"+urlencode(info_params)) + print(info_url) + info_text = requests.get(info_url).text + info = json.loads(info_text[info_text.find("\n") + 1:]) + print(info) + return {"response":response, "gdoc_info":info, + "gdoc_title":title} diff --git a/modules/network_scan/GoogleSearcher.py b/modules/network_scan/GoogleSearcher.py new file mode 100644 index 0000000..0f1313c --- /dev/null +++ b/modules/network_scan/GoogleSearcher.py @@ -0,0 +1,31 @@ +from core.prototypes import AbstractScanner +from urllib.parse import urlencode +import requests +import re +STATS_SEARCHPATTERN = r'