From 7c1494cd502e1a99f96f27b12ddf23a84de188b5 Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Sun, 6 Jun 2021 01:37:50 +0300 Subject: refactor code, add manual-scan script --- lib/results.py | 52 ++++++++++++ lib/scanner.py | 85 +++++++++++++++++++ lib/util.py | 4 + lib/worker.py | 38 +++++++++ manual-scan | 27 ++++++ scanner.py | 85 ------------------- suddenly-opened-ports-checker | 88 ++++++++++++++++++++ suddenly-opened-ports-checker.py | 174 --------------------------------------- 8 files changed, 294 insertions(+), 259 deletions(-) create mode 100644 lib/results.py create mode 100644 lib/scanner.py create mode 100644 lib/util.py create mode 100644 lib/worker.py create mode 100755 manual-scan delete mode 100644 scanner.py create mode 100755 suddenly-opened-ports-checker delete mode 100755 suddenly-opened-ports-checker.py diff --git a/lib/results.py b/lib/results.py new file mode 100644 index 0000000..1565c4b --- /dev/null +++ b/lib/results.py @@ -0,0 +1,52 @@ +from threading import Lock +from lib.util import Colored +from lib.scanner import PortState +from ch1p import telegram_notify + + +class Results: + def __init__(self): + self.warnings = [] + self.mutex = Lock() + + def add(self, worker): + host = worker.get_host() + with self.mutex: + if not worker.done: + print(f'{Colored.RED}{worker.name}: scanning failed{Colored.END}') + return + + if worker.name != host: + print(f'{worker.name} ({host}):') + else: + print(f'{host}:') + + opened = [] + results = worker.get_results() + for port, state in results: + if state != PortState.OPEN: + continue + + opened.append(port) + if not worker.is_expected(port): + self.warnings.append(f'{worker.name} ({host}): port {port} is open') + print(f' {Colored.RED}{port} opened{Colored.END}') + else: + print(f' {Colored.GREEN}{port} opened{Colored.END}') + + if worker.opened: + for port in worker.opened: + if port not in opened: + self.warnings.append( + f'{worker.name} ({host}): port {port} is NOT open') + print(f' {Colored.RED}{port} not opened{Colored.END}') + print() + + def has_warnings(self): + return len(self.warnings) > 0 + + def notify(self, chat_id=None, token=None): + text = '❗️Attention!\n\n' + text += '\n'.join(self.warnings) + + telegram_notify(text, parse_mode='html', chat_id=chat_id, token=token) diff --git a/lib/scanner.py b/lib/scanner.py new file mode 100644 index 0000000..f78e4f6 --- /dev/null +++ b/lib/scanner.py @@ -0,0 +1,85 @@ +import struct +import socket +import threading +import queue +import logging + +from enum import Enum, auto + +logger = logging.getLogger(__name__) + + +class PortState(Enum): + OPEN = auto() + CLOSED = auto() + FILTERED = auto() + + +class TCPScanner: + def __init__(self, host, ports, timeout=5): + self.host = host + self.ports = ports + self.timeout = timeout + self.results = [] + self.q = queue.SimpleQueue() + self.failed = False + + def scan(self, num_threads=5): + for port in self.ports: + self.q.put(port) + + threads = [] + for i in range(num_threads): + t = threading.Thread(target=self.run) + t.start() + threads.append(t) + + for t in threads: + t.join() + + def run(self): + while True: + if self.failed: + break + + try: + port = self.q.get(block=False) + except queue.Empty: + break + + try: + self._scan(port) + except Exception as e: + logger.exception(e) + self.failed = True + break + + def _scan(self, port): + try: + conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + conn.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack("ii", 1, 0)) + conn.settimeout(self.timeout) + + ret = conn.connect_ex((self.host, port)) + + # DATA RECEIVED - SYN ACK + if ret == 0: + logger.debug('%s:%d - tcp open (SYN-ACK packet)' % (self.host, port)) + self.results.append((port, PortState.OPEN)) + + # RST RECEIVED - PORT CLOSED + elif ret == 111: + logger.debug('%s:%d - tcp closed (RST packet)' % (self.host, port)) + self.results.append((port, PortState.CLOSED)) + + # ERR CODE 11 - TIMEOUT + elif ret == 11: + self.results.append((port, PortState.FILTERED)) + + else: + logger.debug('%s:%d - code %d' % (self.host, port, ret)) + + conn.close() + + except socket.timeout: + self.results.append((port, PortState.FILTERED)) \ No newline at end of file diff --git a/lib/util.py b/lib/util.py new file mode 100644 index 0000000..c6087a7 --- /dev/null +++ b/lib/util.py @@ -0,0 +1,4 @@ +class Colored: + GREEN = '\033[92m' + RED = '\033[91m' + END = '\033[0m' \ No newline at end of file diff --git a/lib/worker.py b/lib/worker.py new file mode 100644 index 0000000..f8d6c55 --- /dev/null +++ b/lib/worker.py @@ -0,0 +1,38 @@ +import logging + +from threading import Thread +from lib.scanner import TCPScanner + +logger = logging.getLogger(__name__) + + +class Worker(Thread): + def __init__(self, name, host, opened=None, concurrency=None, timeout=None): + Thread.__init__(self) + + assert concurrency is not None + + self.done = False + self.name = name + self.concurrency = concurrency + self.opened = opened + + scanner_kw = {} + if timeout is not None: + scanner_kw['timeout'] = timeout + self.scanner = TCPScanner(host, range(0, 65535), **scanner_kw) + + def run(self): + logger.info(f'starting {self.name} ({self.concurrency} threads)') + self.scanner.scan(num_threads=self.concurrency) + self.done = not self.scanner.failed + logger.info(f'finished {self.name}') + + def get_results(self): + return self.scanner.results + + def is_expected(self, port): + return (self.opened is not None) and (port in self.opened) + + def get_host(self): + return self.scanner.host diff --git a/manual-scan b/manual-scan new file mode 100755 index 0000000..3318ae1 --- /dev/null +++ b/manual-scan @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +import logging + +from argparse import ArgumentParser +from lib.worker import Worker +from lib.results import Results + + +if __name__ == '__main__': + parser = ArgumentParser() + parser.add_argument('--host', type=str, required=True) + parser.add_argument('--threads', type=int, default=200) + parser.add_argument('--timeout', type=int, default=5) + parser.add_argument('--verbose', action='store_true') + args = parser.parse_args() + + logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + level=(logging.DEBUG if args.verbose else logging.INFO)) + + results = Results() + worker = Worker(args.host, args.host, [], + concurrency=args.threads, + timeout=args.timeout) + worker.start() + worker.join() + + results.add(worker) \ No newline at end of file diff --git a/scanner.py b/scanner.py deleted file mode 100644 index f78e4f6..0000000 --- a/scanner.py +++ /dev/null @@ -1,85 +0,0 @@ -import struct -import socket -import threading -import queue -import logging - -from enum import Enum, auto - -logger = logging.getLogger(__name__) - - -class PortState(Enum): - OPEN = auto() - CLOSED = auto() - FILTERED = auto() - - -class TCPScanner: - def __init__(self, host, ports, timeout=5): - self.host = host - self.ports = ports - self.timeout = timeout - self.results = [] - self.q = queue.SimpleQueue() - self.failed = False - - def scan(self, num_threads=5): - for port in self.ports: - self.q.put(port) - - threads = [] - for i in range(num_threads): - t = threading.Thread(target=self.run) - t.start() - threads.append(t) - - for t in threads: - t.join() - - def run(self): - while True: - if self.failed: - break - - try: - port = self.q.get(block=False) - except queue.Empty: - break - - try: - self._scan(port) - except Exception as e: - logger.exception(e) - self.failed = True - break - - def _scan(self, port): - try: - conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - conn.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack("ii", 1, 0)) - conn.settimeout(self.timeout) - - ret = conn.connect_ex((self.host, port)) - - # DATA RECEIVED - SYN ACK - if ret == 0: - logger.debug('%s:%d - tcp open (SYN-ACK packet)' % (self.host, port)) - self.results.append((port, PortState.OPEN)) - - # RST RECEIVED - PORT CLOSED - elif ret == 111: - logger.debug('%s:%d - tcp closed (RST packet)' % (self.host, port)) - self.results.append((port, PortState.CLOSED)) - - # ERR CODE 11 - TIMEOUT - elif ret == 11: - self.results.append((port, PortState.FILTERED)) - - else: - logger.debug('%s:%d - code %d' % (self.host, port, ret)) - - conn.close() - - except socket.timeout: - self.results.append((port, PortState.FILTERED)) \ No newline at end of file diff --git a/suddenly-opened-ports-checker b/suddenly-opened-ports-checker new file mode 100755 index 0000000..4cf423c --- /dev/null +++ b/suddenly-opened-ports-checker @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +import logging +import yaml +import math + +from pprint import pprint +from argparse import ArgumentParser +from lib.worker import Worker +from lib.results import Results + +logger = logging.getLogger(__name__) + + +def main(): + parser = ArgumentParser() + parser.add_argument('--config', type=str, required=True, + help='path to config file in yaml format') + parser.add_argument('--verbose', action='store_true', + help='set logging level to DEBUG') + parser.add_argument('--concurrency', default=200, type=int, + help='default number of threads per target') + parser.add_argument('--timeout', default=5, type=int, + help='default timeout') + parser.add_argument('--threads-limit', default=0, type=int, + help='global threads limit') + parser.add_argument('--no-telegram', action='store_true', + help='just print results, don\'t send to telegram') + args = parser.parse_args() + + # setup logging + logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + level=(logging.DEBUG if args.verbose else logging.INFO)) + + # load config + with open(args.config, 'r') as f: + config = yaml.safe_load(f) + # pprint(config) + + assert isinstance(config, dict) + assert 'servers' in config + if not args.no_telegram: + assert 'telegram' in config + + # let's go + results = Results() + max_threads = math.inf if args.threads_limit == 0 else args.threads_limit + active_threads = 1 + + def get_active_threads(): + n = active_threads + if workers: + n += workers[0].concurrency + return n + + workers = [] + for name, data in config['servers'].items(): + w = Worker(name, data['host'], data['opened'], + concurrency=int(data['concurrency']) if 'concurrency' in data else args.concurrency, + timeout=int(data['timeout']) if 'timeout' in data else args.timeout) + workers.append(w) + + current_workers = [] + while workers: + w = workers.pop(0) + active_threads += w.concurrency+1 + + current_workers.append(w) + w.start() + + while current_workers and get_active_threads() >= max_threads: + for cw in current_workers: + cw.join(timeout=0.1) + if not cw.is_alive(): + results.add(cw) + current_workers.remove(cw) + active_threads -= cw.concurrency+1 + + for cw in current_workers: + cw.join() + results.add(cw) + + if results.has_warnings() and not args.no_telegram: + results.notify(chat_id=config['telegram']['chat-id'], + token=config['telegram']['token']) + + +if __name__ == '__main__': + main() diff --git a/suddenly-opened-ports-checker.py b/suddenly-opened-ports-checker.py deleted file mode 100755 index f45f0ff..0000000 --- a/suddenly-opened-ports-checker.py +++ /dev/null @@ -1,174 +0,0 @@ -#!/usr/bin/env python3 -import logging -import yaml -import math - -from pprint import pprint -from argparse import ArgumentParser -from ch1p import telegram_notify -from threading import Thread, Lock -from html import escape -from scanner import TCPScanner, PortState - -mutex = Lock() -logger = logging.getLogger(__name__) - - -class Colored: - GREEN = '\033[92m' - RED = '\033[91m' - END = '\033[0m' - - -class Results: - def __init__(self): - self.warnings = [] - self.mutex = Lock() - - def add(self, worker): - host = worker.get_host() - with self.mutex: - if not worker.done: - print(f'{Colored.RED}{worker.name}: scanning failed{Colored.END}') - return - - print(f'{worker.name} ({host}):') - - opened = [] - results = worker.get_results() - for port, state in results: - if state != PortState.OPEN: - continue - - opened.append(port) - if not worker.is_expected(port): - self.warnings.append(f'{worker.name} ({host}): port {port} is open') - print(f' {Colored.RED}{port} opened{Colored.END}') - else: - print(f' {Colored.GREEN}{port} opened{Colored.END}') - - if worker.opened: - for port in worker.opened: - if port not in opened: - self.warnings.append( - f'{worker.name} ({host}): port {port} is NOT open') - print(f' {Colored.RED}{port} not opened{Colored.END}') - print() - - def has_warnings(self): - return len(self.warnings) > 0 - - def notify(self, chat_id=None, token=None): - text = '❗️Attention!\n\n' - text += '\n'.join(self.warnings) - - telegram_notify(text, parse_mode='html', chat_id=chat_id, token=token) - - -class Worker(Thread): - def __init__(self, name, host, opened=None, concurrency=None, timeout=None): - Thread.__init__(self) - - assert concurrency is not None - - self.done = False - self.name = name - self.concurrency = concurrency - self.opened = opened - - scanner_kw = {} - if timeout is not None: - scanner_kw['timeout'] = timeout - self.scanner = TCPScanner(host, range(0, 65535), **scanner_kw) - - def run(self): - logger.info(f'starting {self.name} ({self.concurrency} threads)') - self.scanner.scan(num_threads=self.concurrency) - self.done = not self.scanner.failed - logger.info(f'finished {self.name}') - - def get_results(self): - return self.scanner.results - - def is_expected(self, port): - return (self.opened is not None) and (port in self.opened) - - def get_host(self): - return self.scanner.host - - -def main(): - parser = ArgumentParser() - parser.add_argument('--config', type=str, required=True, - help='path to config file in yaml format') - parser.add_argument('--verbose', action='store_true', - help='set logging level to DEBUG') - parser.add_argument('--concurrency', default=200, type=int, - help='default number of threads per target') - parser.add_argument('--timeout', default=5, type=int, - help='default timeout') - parser.add_argument('--threads-limit', default=0, type=int, - help='global threads limit') - parser.add_argument('--no-telegram', action='store_true', - help='just print results, don\'t send to telegram') - args = parser.parse_args() - - # setup loggign - logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - level=(logging.DEBUG if args.verbose else logging.INFO)) - - # load config - with open(args.config, 'r') as f: - config = yaml.safe_load(f) - # pprint(config) - - assert isinstance(config, dict) - assert 'servers' in config - if not args.no_telegram: - assert 'telegram' in config - - # let's go - results = Results() - max_threads = math.inf if args.threads_limit == 0 else args.threads_limit - active_threads = 1 - - def get_active_threads(): - n = active_threads - if workers: - n += workers[0].concurrency - return n - - workers = [] - for name, data in config['servers'].items(): - w = Worker(name, data['host'], data['opened'], - concurrency=int(data['concurrency']) if 'concurrency' in data else args.concurrency, - timeout=int(data['timeout']) if 'timeout' in data else args.timeout) - workers.append(w) - - current_workers = [] - while workers: - w = workers.pop(0) - active_threads += w.concurrency+1 - - current_workers.append(w) - w.start() - - while current_workers and get_active_threads() >= max_threads: - for cw in current_workers: - cw.join(timeout=0.1) - if not cw.is_alive(): - results.add(cw) - current_workers.remove(cw) - active_threads -= cw.concurrency+1 - - for cw in current_workers: - cw.join() - results.add(cw) - - if results.has_warnings() and not args.no_telegram: - results.notify(chat_id=config['telegram']['chat-id'], - token=config['telegram']['token']) - - -if __name__ == '__main__': - main() -- cgit v1.2.3