Пишем свой масс-сканер и OSINT-комбайн на Python: от скрипта «в лоб» до асинхронности и SQLite

в 15:41, , рубрики: multithreading, OSINT, requests, sqlite3, whois

В процессе изучения сетевой безопасности и автоматизации OSINT на базе Linux передо мной встала задача — создать быстрый и отказоустойчивый инструмент для первоначального сбора данных и фиксации сетевых инцидентов, который не требует сложной настройки, защищен от блокировок анти-бот систем и сохраняет всю историю проверок в структурированную локальную базу данных для последующего анализа. В данной статье хочу поделиться с Вами каждым этапом разработки первых трёх версий моего инструмента Web-Recon. Вместе мы пройдем путь от простейшего, ничем не примечательного скрипта, спотыкающегося о первые блокировки и фейковые ошибки 403, до создания асинхронного пула потоков. Особое внимание я уделю проектированию структуры БД на SQLite3 и решению проблемы параллельной записи, из-за которой многопоточные приложения часто ловят ошибку database is locked. Я очень надеюсь, что этот практический опыт поможет другим разработчикам не наступать на типичные архитектурные грабли при проектировании асинхронных логгеров.

Версия 0.1.0: Первый рабочий прототип и основные недостатки кода

Изначально программа была простейшим исследовательским скриптом, написанным мной в процессе изучения библиотеки requests в Python. Также в стек входил модуль json для упаковки отчёта об ошибке (в случае неудачного сканирования) в файл с расширением .json. Никаких баз данных и асинхронности на момент создания версии 0.1.0 не было. Программа принимала одну ссылку через стандартный input():

url = input("Please, paste link of your site here: ")

Далее скрипт проверял её на ошибки, используя кастомный блок маппинга сетевых ошибок. Выглядело это как цепочка условий if/elif внутри конструкции try/except:

try:
r = requests.get(url, stream=True, timeout=30, headers=headers)

if r.status_code == 200:
print("Link is working! Processing...")

elif r.status_code == 400:
error_msg = f"Error code {r.status_code}. Bad request!"

# ... дальнейший маппинг кодов ошибок

Если утилита фиксировала сбой, она собирала базовую телеметрию и сохраняла всё в обычный локальный JSON-файл. Именно на этом этапе я столкнулся с первой серьезной недоработкой кода, которая вскрыла моё тогдашнее незнание классической проблемы автоматизированного парсинга — ложного срабатывания систем защиты. При сканировании некоторых крупных сайтов скрипт внезапно начал получать коды ответа 403 Forbidden или 401 Unauthorized. При этом, если я открывал ту же самую ссылку в обычном браузере, она загружалась без каких-либо проблем. Погуглив и изучив особенности библиотеки requests, я выяснил, что защитные системы веб-серверов (например, Cloudflare) анализируют HTTP-заголовки. По умолчанию requests отправляет свой собственный заголовок User-Agent, который довольно громко заявляет серверу: «Добрый вечер, я автоматический скрипт на Питоне!», после чего система безопасности мгновенно блокирует его как подозрительного бота. Казалось бы, я обречён, и скрипт не будет работать так, как задумывалось. Но, как это часто бывает в разработке, решение оказалось очень простым. Нужно было всего лишь внедрить подмену заголовков (Header Spoofing), добавив словарь headers с валидным параметром User-Agent от реального современного браузера Google Chrome под управлением Windows:

headers = {
’User-Agent’: ’Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36′
}

Как только я начал использовать эти заголовки, удаленные серверы стали принимать мой скрипт за живого человека, и ложные ошибки 403 полностью исчезли.

Версия 0.2.1 и патч 0.2.2: Переход на многопоточность и первый архитектурный аудит.

Почему же версия 0.1.0 перестала меня устраивать? Через время я задумался, а что если нужно будет проверить список из 50 сайтов? Из 100? Вводить ссылку каждого сайта в поле input() и ждать отклика WHOIS очень неудобно и времязатратно. Именно в этот момент обычный код стал перерастать в что то более весомое, а именно в версию 0.2.1. Прежде чем начинать внедрять новый блок кода с многопоточностью, нужно было сделать так, чтобы программа смогла читать целый файл с адресами, а сделать это было довольно просто, используя стандартные функции:

with open(file_path, ’r’, encoding=’utf-8′) as infile:
urls_list = [line.strip() for line in infile if line.strip()]

Благодаря компактному генератору списков (List Comprehension) скрипт одной строчкой не просто читал файл, а на лету очищал его от невидимого мусора: обрезал технические переносы строк n с помощью метода .strip() и полностью игнорировал пустые строки.

Но главное было впереди — разгон скорости. Сетевые запросы относятся к типу I/O-bound задач, то есть процессор 99% времени просто сидит и курит бамбук, ожидая, пока удаленный сервер соизволит ответить по сети. Проверять их по очереди — непозволительная роскошь. Чтобы заставить программу опрашивать десятки сайтов одновременно, я интегрировал асинхронный пул потоков ThreadPoolExecutor из встроенного модуля concurrent.futures (изначально моя идея казалась заоблачной и невыполнимой для такого новичка, как я, но я справился):

with ThreadPoolExecutor(max_workers=10) as executor:
results = executor.map(check_single_link, urls_list)

Я выделил команду из 10 параллельных рабочих потоков (max_workers=10), которые наперегонки ринулись опрашивать сеть. Скорость сканирования взлетела в разы! В этой же версии v0.2.1 я добавил и базу данных SQLite, чтобы результаты наконец-то сохранялись в постоянный файл reconReport.db, а не исчезали после закрытия терминала. Казалось бы, проект готов, скорость бешеная, логи пишутся в базу. Но радовался я недолго.

Патч v0.2.2: Первые серьезные архитектурные грабли

Сразу после запуска и первых тестов v0.2.1 я провел аудит собственной архитектуры и застыл в ступоре. Я обнаружил критический дефект логики (Logic Flaw). Мой инструмент успешно вытаскивал глубокую OSINT-телеметрию через модуль WHOIS, но из-за невнимательности при проектировании конвейера данных этот паспорт домена… полностью пролетал мимо вызова функции записи! База данных работала просто как банальный сетевой пинговалка статус-кодов, а вся собранная разведка испарялась из оперативной памяти! Так родился экстренный патч v0.2.2. Мне пришлось решить две серьезные инженерные задачи (по крайней мере, серьёзные для меня):

  1. Перестроить возвраты потоков: Я заставил функцию проверки check_single_link внутри каждого потока самостоятельно дожидаться ответа WHOIS, упаковывать все данные в единый потокобезопасный словарь и возвращать его наружу.

  2. Обойти блокировку SQLite (database is locked): SQLite очень не любит, когда 10 потоков одновременно пытаются записать что-то в один файл. Чтобы избежать состояния гонки (Race Condition), я применил изящное архитектурное решение: потоки только собирают данные в память, а запись в 4 колонки таблицы происходит строго в один поток через цикл for уже после закрытия пула воркеров.

В итоге данный патч 0.2.2 сделал код монолитным и стабильным.

Проделав этот путь от скрипта «в лоб» до многопоточного OSINT-комбайна, я пришел к трем главным выводам, которые, как я надеюсь, пригодятся другим начинающим разработчикам:

  1. Архитектура важнее синтаксиса. Мало просто уметь вызывать библиотеки вроде requests или sqlite3. Самое сложное и интересное начинается на стыке технологий — когда тебе нужно правильно подружить асинхронные потоки с базой данных, не поймав при этом взаимных блокировок файлов.

  2. Логи должны жить долго. Текстовые отчёты или JSON-файлы хороши для разовых тестов. Но если вы строите систему долгосрочного мониторинга сетевых инцидентов, реляционная база данных превращает ваш софт в полноценный инструмент аудита с хронологической историей.

  3. Не бойтесь тыкать, ошибаться, делать или пытаться делать, на первый взгляд невозможные вещи. Это делает вас сильнее!

Что же будет дальше с проектом?

Я не собираюсь останавливаться на достигнутом и в скором времени открою ветку разработки версии 0.3.0 в которой планирую реализовать пару важных вещей:

  1. Бенчмаркинг скорости

  2. Автоматическое скачивание целевого файла сайта для аудита безопасности, если утилита будет возвращать код 200.

Весь актуальный код проекта, историю релизов по правилам и документацию вы можете найти в моем репозитории на GitHub. Буду рад конструктивной критике, пул-реквестам и звёздочкам!

Ссылка на репозиторий: https://github.com/joavat-2009/Web-Recon

Также хочу сказать огромное спасибо хабр-сообществу и всем практикующим специалистам за то, что делитесь опытом в своих статьях — именно на ваших разборах я учился обходить блокировки и настраивать потоки. Поскольку я только в начале своего пути в сфере ИТ и кибербезопасности, для меня критически важен ваш взгляд со стороны. Если вы видите, где код можно оптимизировать, как сделать архитектуру еще более гибкой и что по вашему мнению стоит добавить в версию v0.3.0 — обязательно пишите в комментарии или открывайте Issues на гитхабе. Буду благодарен за любые советы и наставления от старших коллег по цеху! Спасибо за внимание!)

Автор: joavat

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js