/
marsela13
/
practic
Обзор
Документация
Войти
/
marsela13
/
practic
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
scanner_gui.py
771 строка
35 KB
marsela13
Upload files to ""
25 дек 2025, 15:33
25 дек 2025, 15:33
b6f708d
Код
Авторство
О чём код?
""" Модуль графического интерфейса Network Scanner GUI. Содержит классы для создания пользовательского интерфейса на базе PyQt5 для взаимодействия с функционалом NetworkScanner. Основные компоненты: - ScanThread: Класс потока для асинхронного сканирования - NetworkScannerGUI: Главное окно приложения с элементами управления Требования: - PyQt5 >= 5.15 - Python 3.8+ Архитектура: +-----------------------+ | NetworkScannerGUI | +-----------------------+ | - scanner: NetworkScanner | | - scan_thread: ScanThread | +-----------------------+ | использует v +-----------------------+ | ScanThread | +-----------------------+ | - scanner: NetworkScanner | +-----------------------+ | использует v +-----------------------+ | NetworkScanner | +-----------------------+ """ import sys import os from datetime import datetime from PyQt5.QtWidgets import ( QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QTableWidget, QTableWidgetItem, QTextEdit, QProgressBar, QGroupBox, QCheckBox, QSpinBox, QComboBox, QFileDialog, QMessageBox, QSplitter, QTabWidget, QHeaderView, QMenu, QAction, QSystemTrayIcon ) from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer from PyQt5.QtGui import QIcon, QFont, QColor import threading import json from network_scanner import NetworkScanner class ScanThread(QThread): """ Поток для выполнения асинхронного сканирования сети. Наследуется от QThread для выполнения длительных операций без блокировки графического интерфейса. Сигналы: scan_progress: Излучается при обновлении прогресса сканирования scan_finished: Излучается при завершении сканирования scan_error: Излучается при возникновении ошибки Атрибуты: scanner (NetworkScanner): Экземпляр сканера для выполнения операций ip_range (str): Диапазон IP-адресов для сканирования ports (List[int]): Список портов для сканирования """ # Сигналы PyQt для взаимодействия с GUI scan_progress = pyqtSignal(int, int, int, dict) # текущий, всего, найдено, результат scan_finished = pyqtSignal(list) # список результатов scan_error = pyqtSignal(str) # сообщение об ошибке def __init__(self, scanner: NetworkScanner, ip_range: str, ports: list): """ Инициализация потока сканирования. Args: scanner: Экземпляр NetworkScanner для выполнения операций ip_range: Диапазон IP-адресов в формате CIDR или диапазона ports: Список портов для сканирования """ super().__init__() self.scanner = scanner self.ip_range = ip_range self.ports = ports def run(self): """ Основной метод выполнения потока. Выполняет сканирование сети и излучает сигналы для обновления GUI. Обрабатывает исключения и передает их через сигнал scan_error. """ try: # Установка callback для передачи прогресса в GUI self.scanner.set_progress_callback(self.update_progress) # Выполнение сканирования сети results = self.scanner.scan_network(self.ip_range, self.ports) # Излучение сигнала о завершении с результатами self.scan_finished.emit(results) except Exception as e: # Излучение сигнала об ошибке с сообщением self.scan_error.emit(str(e)) def update_progress(self, current: int, total: int, found: int, result: dict): """ Callback-метод для передачи прогресса сканирования. Вызывается из NetworkScanner при обновлении прогресса. Args: current: Текущий обработанный хост total: Общее количество хостов для сканирования found: Количество найденных активных хостов result: Информация о последнем найденном хосте """ # Перенаправление данных через сигнал PyQt self.scan_progress.emit(current, total, found, result) class NetworkScannerGUI(QMainWindow): """ Главное окно приложения Network Scanner GUI. Наследуется от QMainWindow и предоставляет полный графический интерфейс для управления сканированием сети, просмотра результатов и экспорта данных. Атрибуты: scanner (NetworkScanner): Экземпляр сетевого сканера scan_thread (ScanThread): Текущий поток сканирования results_table (QTableWidget): Таблица для отображения результатов log_text (QTextEdit): Виджет для лог-сообщений progress_bar (QProgressBar): Индикатор прогресса сканирования status_label (QLabel): Метка статуса в status bar """ def __init__(self): """ Инициализация главного окна приложения. Создает экземпляр NetworkScanner и инициализирует графический интерфейс. """ super().__init__() self.scanner = NetworkScanner() # Создание сетевого сканера self.scan_thread = None # Ссылка на текущий поток сканирования self.init_ui() # Инициализация графического интерфейса def init_ui(self): """ Инициализация пользовательского интерфейса. Создает и настраивает все элементы GUI: 1. Панель настроек сканирования 2. Таблицу результатов 3. Лог-панель 4. Панель управления 5. Меню и status bar """ self.setWindowTitle('Network Scanner GUI v1.0') self.setGeometry(100, 100, 1200, 800) # Позиция и размер окна # Центральный виджет - контейнер для всех элементов central_widget = QWidget() self.setCentralWidget(central_widget) # Основной вертикальный layout main_layout = QVBoxLayout(central_widget) # Создание и добавление панели настроек settings_group = self.create_settings_panel() main_layout.addWidget(settings_group) # Splitter для разделения таблицы результатов и лога splitter = QSplitter(Qt.Vertical) # Таблица результатов сканирования self.results_table = self.create_results_table() splitter.addWidget(self.results_table) # Текстовое поле для лог-сообщений self.log_text = QTextEdit() self.log_text.setReadOnly(True) self.log_text.setMaximumHeight(200) splitter.addWidget(self.log_text) # Настройка начальных размеров splitter splitter.setSizes([600, 200]) main_layout.addWidget(splitter) # Прогресс-бар для отображения хода сканирования self.progress_bar = QProgressBar() main_layout.addWidget(self.progress_bar) # Status bar с меткой статуса self.status_label = QLabel("Готов к сканированию") self.statusBar().addWidget(self.status_label) # Создание меню приложения self.create_menu_bar() # Загрузка сохраненных настроек (если есть) self.load_settings() def create_settings_panel(self) -> QGroupBox: """ Создание панели настроек сканирования. Returns: QGroupBox: Группа виджетов с настройками сканирования """ group = QGroupBox("Настройки сканирования") layout = QVBoxLayout() # Поле ввода диапазона IP ip_layout = QHBoxLayout() ip_layout.addWidget(QLabel("IP диапазон:")) self.ip_input = QLineEdit() self.ip_input.setPlaceholderText("Пример: 192.168.1.1-100 или 192.168.1.0/24") self.ip_input.setText("192.168.1.1-100") # Значение по умолчанию ip_layout.addWidget(self.ip_input) layout.addLayout(ip_layout) # Поле ввода портов ports_layout = QHBoxLayout() ports_layout.addWidget(QLabel("Порты:")) self.ports_input = QLineEdit() self.ports_input.setPlaceholderText("Через запятую, например: 22,80,443,3389") self.ports_input.setText("22,80,443,3389") # Значение по умолчанию ports_layout.addWidget(self.ports_input) # Checkbox для сканирования общих портов self.common_ports_check = QCheckBox("Сканировать общие порты") self.common_ports_check.setChecked(True) ports_layout.addWidget(self.common_ports_check) layout.addLayout(ports_layout) # Настройки производительности perf_layout = QHBoxLayout() perf_layout.addWidget(QLabel("Макс. потоков:")) self.threads_spin = QSpinBox() self.threads_spin.setRange(1, 500) self.threads_spin.setValue(50) perf_layout.addWidget(self.threads_spin) perf_layout.addWidget(QLabel("Таймаут (сек):")) self.timeout_spin = QSpinBox() self.timeout_spin.setRange(1, 30) self.timeout_spin.setValue(1) perf_layout.addWidget(self.timeout_spin) layout.addLayout(perf_layout) # Кнопки управления buttons_layout = QHBoxLayout() # Кнопка начала сканирования self.scan_button = QPushButton("Начать сканирование") self.scan_button.clicked.connect(self.start_scan) self.scan_button.setStyleSheet("background-color: #4CAF50; color: white;") buttons_layout.addWidget(self.scan_button) # Кнопка остановки сканирования self.stop_button = QPushButton("Остановить") self.stop_button.clicked.connect(self.stop_scan) self.stop_button.setEnabled(False) self.stop_button.setStyleSheet("background-color: #f44336; color: white;") buttons_layout.addWidget(self.stop_button) # Кнопка экспорта результатов self.export_button = QPushButton("Экспорт результатов") self.export_button.clicked.connect(self.export_results) self.export_button.setEnabled(False) buttons_layout.addWidget(self.export_button) # Кнопка очистки результатов self.clear_button = QPushButton("Очистить") self.clear_button.clicked.connect(self.clear_results) buttons_layout.addWidget(self.clear_button) layout.addLayout(buttons_layout) group.setLayout(layout) return group def create_results_table(self) -> QTableWidget: """ Создание и настройка таблицы для отображения результатов. Returns: QTableWidget: Настроенная таблица результатов """ table = QTableWidget() table.setColumnCount(5) table.setHorizontalHeaderLabels([ "IP", "Hostname", "Status", "Открытые порты", "Время обнаружения" ]) # Настройка внешнего вида таблицы table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) table.setAlternatingRowColors(True) # Чередование цветов строк table.setSelectionBehavior(QTableWidget.SelectRows) # Выделение целых строк table.setEditTriggers(QTableWidget.NoEditTriggers) # Только чтение # Настройка контекстного меню table.setContextMenuPolicy(Qt.CustomContextMenu) table.customContextMenuRequested.connect(self.show_context_menu) return table def create_menu_bar(self): """ Создание главного меню приложения. Меню содержит: - Файл: Экспорт, выход - Настройки: Сохранение/загрузка настроек - Помощь: О программе """ menubar = self.menuBar() # Меню "Файл" file_menu = menubar.addMenu('Файл') export_action = QAction('Экспорт...', self) export_action.triggered.connect(self.export_results) file_menu.addAction(export_action) file_menu.addSeparator() exit_action = QAction('Выход', self) exit_action.triggered.connect(self.close) file_menu.addAction(exit_action) # Меню "Настройки" settings_menu = menubar.addMenu('Настройки') save_settings_action = QAction('Сохранить настройки', self) save_settings_action.triggered.connect(self.save_settings) settings_menu.addAction(save_settings_action) load_settings_action = QAction('Загрузить настройки', self) load_settings_action.triggered.connect(self.load_settings) settings_menu.addAction(load_settings_action) # Меню "Помощь" help_menu = menubar.addMenu('Помощь') about_action = QAction('О программе', self) about_action.triggered.connect(self.show_about) help_menu.addAction(about_action) def show_context_menu(self, position): """ Показ контекстного меню для таблицы результатов. Args: position: Позиция клика мыши в координатах таблицы """ menu = QMenu() # Действие: Копировать IP copy_ip_action = QAction("Копировать IP", self) copy_ip_action.triggered.connect(self.copy_selected_ip) menu.addAction(copy_ip_action) # Действие: Сканировать выбранный хост scan_host_action = QAction("Сканировать выбранный хост", self) scan_host_action.triggered.connect(self.scan_selected_host) menu.addAction(scan_host_action) # Показ меню в позиции клика menu.exec_(self.results_table.viewport().mapToGlobal(position)) def copy_selected_ip(self): """Копирование IP-адреса из выбранной строки таблицы в буфер обмена.""" selected = self.results_table.selectedItems() if selected: ip_item = self.results_table.item(selected[0].row(), 0) if ip_item: QApplication.clipboard().setText(ip_item.text()) self.log_message(f"IP скопирован: {ip_item.text()}") def scan_selected_host(self): """Запуск сканирования для IP-адреса из выбранной строки таблицы.""" selected = self.results_table.selectedItems() if selected: ip_item = self.results_table.item(selected[0].row(), 0) if ip_item: self.ip_input.setText(ip_item.text()) self.start_scan() def start_scan(self): """ Запуск процесса сканирования сети. Выполняет: 1. Валидацию входных данных 2. Настройку параметров сканера 3. Очистку предыдущих результатов 4. Запуск потока сканирования 5. Обновление состояния интерфейса """ ip_range = self.ip_input.text().strip() if not ip_range: QMessageBox.warning(self, "Ошибка", "Введите IP диапазон") return # Формирование списка портов для сканирования ports = [] if self.common_ports_check.isChecked(): ports = self.scanner.COMMON_PORTS # Добавление пользовательских портов custom_ports = self.ports_input.text().strip() if custom_ports: try: custom_ports_list = [ int(p.strip()) for p in custom_ports.split(',') if p.strip().isdigit() ] ports.extend(custom_ports_list) ports = list(set(ports)) # Удаление дубликатов except ValueError: QMessageBox.warning(self, "Ошибка", "Некорректный формат портов") return # Настройка параметров сканера self.scanner.max_threads = self.threads_spin.value() self.scanner.timeout = self.timeout_spin.value() # Очистка предыдущих результатов self.results_table.setRowCount(0) self.progress_bar.setValue(0) # Обновление состояния кнопок self.scan_button.setEnabled(False) self.stop_button.setEnabled(True) self.export_button.setEnabled(False) # Создание и запуск потока сканирования self.scan_thread = ScanThread(self.scanner, ip_range, ports) self.scan_thread.scan_progress.connect(self.update_progress) self.scan_thread.scan_finished.connect(self.on_scan_finished) self.scan_thread.scan_error.connect(self.on_scan_error) self.scan_thread.start() self.log_message(f"Начато сканирование: {ip_range}") self.status_label.setText(f"Сканирование: {ip_range}") def stop_scan(self): """ Остановка текущего процесса сканирования. Отправляет сигнал отмены в NetworkScanner и ожидает завершения потока сканирования. """ if self.scan_thread and self.scan_thread.isRunning(): self.scanner.cancel_scan() self.scan_thread.quit() self.scan_thread.wait() self.log_message("Сканирование остановлено пользователем") self.status_label.setText("Сканирование остановлено") # Восстановление состояния кнопок self.scan_button.setEnabled(True) self.stop_button.setEnabled(False) def update_progress(self, current: int, total: int, found: int, result: dict): """ Обработчик обновления прогресса сканирования. Обновляет: 1. Прогресс-бар 2. Статусную строку 3. Таблицу результатов Args: current: Текущий обработанный хост total: Общее количество хостов found: Количество найденных активных хостов result: Информация о последнем найденном хосте """ # Обновление прогресс-бара progress = int((current / total) * 100) if total > 0 else 0 self.progress_bar.setValue(progress) # Обновление статусной строки self.status_label.setText( f"Сканирование... {current}/{total} ({found} найдено)" ) # Добавление нового результата в таблицу if result: row_position = self.results_table.rowCount() self.results_table.insertRow(row_position) # IP-адрес ip_item = QTableWidgetItem(result['ip']) self.results_table.setItem(row_position, 0, ip_item) # Имя хоста hostname_item = QTableWidgetItem(result['hostname'] or "Не определено") self.results_table.setItem(row_position, 1, hostname_item) # Статус (с цветовым выделением) status_item = QTableWidgetItem(result['status']) if result['status'] == 'Online': status_item.setForeground(QColor(0, 128, 0)) # Зеленый цвет self.results_table.setItem(row_position, 2, status_item) # Открытые порты ports_str = ', '.join(map(str, result['open_ports'])) if result['open_ports'] else 'Нет' ports_item = QTableWidgetItem(ports_str) self.results_table.setItem(row_position, 3, ports_item) # Время обнаружения time_item = QTableWidgetItem(result['timestamp']) self.results_table.setItem(row_position, 4, time_item) # Автопрокрутка к новой строке self.results_table.scrollToBottom() def on_scan_finished(self, results: list): """ Обработчик завершения сканирования. Args: results: Список найденных хостов """ # Восстановление состояния кнопок self.scan_button.setEnabled(True) self.stop_button.setEnabled(False) self.export_button.setEnabled(len(results) > 0) # Логирование и обновление статуса self.log_message(f"Сканирование завершено. Найдено {len(results)} активных хостов") self.status_label.setText(f"Сканирование завершено. Найдено {len(results)} хостов") self.progress_bar.setValue(100) # Вывод статистики if results: open_ports_count = sum(len(h['open_ports']) for h in results) self.log_message(f"Всего открытых портов: {open_ports_count}") # Информационное сообщение пользователю QMessageBox.information( self, "Сканирование завершено", f"Найдено {len(results)} активных хостов" ) def on_scan_error(self, error_message: str): """ Обработчик ошибки сканирования. Args: error_message: Текст ошибки """ # Восстановление состояния кнопок self.scan_button.setEnabled(True) self.stop_button.setEnabled(False) # Логирование ошибки self.log_message(f"Ошибка сканирования: {error_message}") self.status_label.setText("Ошибка сканирования") # Сообщение об ошибке пользователю QMessageBox.critical( self, "Ошибка", f"Ошибка при сканировании:\n{error_message}" ) def export_results(self): """ Экспорт результатов сканирования в файл. Поддерживает форматы: - JSON (*.json) - CSV (*.csv) Предоставляет диалоговое окно выбора файла. """ if not self.scanner.results: QMessageBox.warning(self, "Ошибка", "Нет данных для экспорта") return # Диалог выбора файла filename, selected_filter = QFileDialog.getSaveFileName( self, "Экспорт результатов", "", "JSON files (*.json);;CSV files (*.csv);;All files (*.*)" ) if not filename: return # Выбор метода экспорта в зависимости от расширения success = False if filename.lower().endswith('.json'): success = self.scanner.export_to_json(filename) elif filename.lower().endswith('.csv'): success = self.scanner.export_to_csv(filename) else: # По умолчанию - JSON filename += '.json' success = self.scanner.export_to_json(filename) # Обработка результата экспорта if success: self.log_message(f"Результаты экспортированы в: {filename}") QMessageBox.information(self, "Успех", "Результаты успешно экспортированы") else: QMessageBox.warning(self, "Ошибка", "Не удалось экспортировать результаты") def clear_results(self): """ Очистка всех результатов и сброс интерфейса. Запрашивает подтверждение у пользователя перед очисткой. """ # Проверка, не выполняется ли сканирование if self.scan_thread and self.scan_thread.isRunning(): QMessageBox.warning(self, "Ошибка", "Сначала остановите сканирование") return # Запрос подтверждения reply = QMessageBox.question( self, 'Очистка', 'Вы уверены, что хотите очистить все результаты?', QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply == QMessageBox.Yes: self.results_table.setRowCount(0) self.scanner.results = [] self.progress_bar.setValue(0) self.log_text.clear() self.export_button.setEnabled(False) self.log_message("Результаты очищены") def log_message(self, message: str): """ Добавление сообщения в лог с временной меткой. Args: message: Текст сообщения для логирования """ timestamp = datetime.now().strftime("%H:%M:%S") formatted_message = f"[{timestamp}] {message}" self.log_text.append(formatted_message) # Автопрокрутка к последнему сообщению scrollbar = self.log_text.verticalScrollBar() scrollbar.setValue(scrollbar.maximum()) def save_settings(self): """ Сохранение текущих настроек приложения в JSON файл. Сохраняет: - Диапазон IP - Настройки портов - Параметры производительности """ settings = { 'ip_range': self.ip_input.text(), 'ports': self.ports_input.text(), 'scan_common_ports': self.common_ports_check.isChecked(), 'max_threads': self.threads_spin.value(), 'timeout': self.timeout_spin.value() } try: with open('scanner_settings.json', 'w') as f: json.dump(settings, f, indent=2) self.log_message("Настройки сохранены") except (IOError, PermissionError, json.JSONDecodeError) as e: self.log_message(f"Ошибка сохранения настроек: {e}") def load_settings(self): """ Загрузка настроек приложения из JSON файла. Восстанавливает последние использованные настройки при запуске. """ try: if os.path.exists('scanner_settings.json'): with open('scanner_settings.json', 'r') as f: settings = json.load(f) # Восстановление значений из файла настроек self.ip_input.setText(settings.get('ip_range', '')) self.ports_input.setText(settings.get('ports', '')) self.common_ports_check.setChecked(settings.get('scan_common_ports', True)) self.threads_spin.setValue(settings.get('max_threads', 50)) self.timeout_spin.setValue(settings.get('timeout', 1)) self.log_message("Настройки загружены") except (IOError, PermissionError, json.JSONDecodeError) as e: self.log_message(f"Ошибка загрузки настроек: {e}") def show_about(self): """ Показ диалогового окна "О программе". Содержит информацию о версии, возможностях и предупреждениях. """ about_text = """ <h2>Network Scanner GUI</h2> <p><b>Версия:</b> 1.0</p> <p><b>Описание:</b> Программа для сканирования сети и обнаружения активных устройств.</p> <p><b>Основные возможности:</b></p> <ul> <li>Ping-сканирование диапазонов IP-адресов</li> <li>TCP-сканирование наиболее распространенных портов</li> <li>Определение имен устройств (hostname)</li> <li>Многопоточное сканирование для ускорения процесса</li> <li>Экспорт результатов в форматы JSON и CSV</li> <li>Удобный графический интерфейс с поддержкой темной темы</li> </ul> <p><b>Технологии:</b> Python 3.8+, PyQt5</p> <p style="color: red;"><b>ВАЖНО:</b> Используйте это приложение только для сканирования сетей, которыми вы управляете или имеете явное разрешение на сканирование. Не сканируйте публичные сети или сети без соответствующего разрешения.</p> <p><b>Лицензия:</b> MIT</p> <p><b>Автор:</b> [Ваше имя/компания]</p> """ QMessageBox.about(self, "О программе", about_text) def closeEvent(self, event): """ Обработчик события закрытия окна. Проверяет, выполняется ли сканирование, и запрашивает подтверждение при необходимости. Args: event: Событие закрытия окна """ if self.scan_thread and self.scan_thread.isRunning(): # Запрос подтверждения при активном сканировании reply = QMessageBox.question( self, 'Сканирование выполняется', 'Сканирование еще выполняется. Вы уверены, что хотите выйти?', QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply == QMessageBox.Yes: self.stop_scan() event.accept() else: event.ignore() else: event.accept()