Compare commits

...
4 Commits
Author SHA1 Message Date
stud178875 69b22c8c30 docs: up readme 2025-05-16 08:56:02 +03:00
stud178875 9788b5aa4e rm: dld.py, test 2025-05-03 08:28:34 +03:00
stud178875 44ce626a1b feat: celery 2025-05-03 08:25:15 +03:00
stud178875 166ad0ba2f fix: logging 2025-04-13 21:15:50 +03:00
18 changed files with 639 additions and 221 deletions
+3
View File
@@ -2,3 +2,6 @@
*.log *.log
**/__pycache__/ **/__pycache__/
/web/static/*.mp4 /web/static/*.mp4
/celerybeat-schedule.bak
/celerybeat-schedule.dat
/celerybeat-schedule.dir
+61 -5
View File
@@ -1,12 +1,68 @@
# Tempora
Новицкий Глеб Павлович Новицкий Глеб Павлович
Терехов Павел Сергеевич Терехов Павел Сергеевич
``` Система мониторинга утечек данных предназначена для выявления инцидентов, связанных с публикацией конфиденциальной информации в открытых источниках (форумы, даркнет, Telegram-каналы). Система осуществляет непрерывный сбор, анализ и визуализацию упоминаний, связанных с потенциальными утечками.
____________ ______ ____ ___ ___
/_ __/ __/ |/ / _ \/ __ \/ _ \/ _ |
/ / / _// /|_/ / ___/ /_/ / , _/ __ |
/_/ /___/_/ /_/_/ \____/_/|_/_/ |_|
Проект разработан в рамках учебной практики студентом направления «Информационная безопасность автоматизированных систем».
``` ```
├── Driver/ Основной компонент для работы с веб-ресурсами
│ ├── chrome_proxy.py Работа с Chrome через undetected-chromedriver + прокси
│ └── driver_creator.py Создание и настройка экземпляра драйвера
├── Tasks/ Модуль задач парсинга
│ ├── forum_crawler.py Парсинг данных с форумов
│ └── tg_crawler.py Парсинг Telegram-каналов
├── Utils/ Вспомогательные утилиты
│ └── logg.py Логирование работы системы
├── Web/ Веб-интерфейс
│ ├── Static/ CSS/JS/Изображения
│ ├── Templates/ HTML-шаблоны
│ └── app.py Flask приложение
└── Основные файлы:
├── .env Конфигурации, секреты приложения
├── .gitignore Игнорируемые файлы для Git
├── celery_app.py Конфигурация Celery
├── celery_beat.py Планировщик задач Celery
├── initiator.py Точка входа
├── requirements.txt Зависимости Python
└── README.md Описание проекта
```
### Основные функции
* Мониторинг 3 телеграм каналов, одного форума
* Визуализация данных на веб-интерфейсе
* Централизованное логирование и уведомления об ошибках
### Запуск проекта
pip install -r requirements.txt
В отдельных окнах
python celery_beat.py
python initiator.py
### Зависимости
| Библиотека | Назначение | Область применения |
| -------------------------- | --------------------------------------------------------------------- | -------------------------------------------- |
| `requests` | HTTP-клиент для отправки запросов и получения ответов | Парсинг, работа с API |
| `setuptools` | Инструмент для упаковки Python-проектов и управления зависимостями | Сборка, установка пакетов |
| `python-dotenv` | Загрузка переменных окружения из файла `.env` | Конфигурация, безопасное хранение секретов |
| `tls-client` | Кастомный HTTP-клиент с поддержкой TLS и антибот-защиты | Парсинг сайтов с защитой |
| `undetected-chromedriver` | Модифицированный ChromeDriver, обходящий антибот-защиту | Веб-скрапинг с защитой (Cloudflare и др.) |
| `selenium` | Управление браузером для автоматизации действий | Парсинг, тестирование веб-интерфейсов |
| `grpc_interceptor_headers` | Расширение gRPC-перехватчиков с поддержкой заголовков | gRPC API, метаданные |
| `telethon` | Асинхронная библиотека для работы с Telegram API через MTProto | Мониторинг Telegram-каналов |
| `schedule` | Простое планирование задач в Python | Локальный запуск задач по расписанию |
| `psycopg2-binary` | PostgreSQL-драйвер для Python | Работа с СУБД PostgreSQL |
| `docker` | Python-интерфейс к Docker Engine API | Управление контейнерами программно |
| `asyncio` | Базовая библиотека Python для асинхронного программирования | Асинхронные задачи, неблокирующий ввод/вывод |
| `pytz` | Работа с часовыми поясами | Корректная работа с временем |
| `flask` | Легковесный веб-фреймворк | Веб-интерфейсы, REST API |
| `apscheduler` | Планировщик задач с расширенными возможностями (cron, interval, date) | Автоматизация периодических задач |
+37
View File
@@ -0,0 +1,37 @@
from celery import Celery
from kombu import Queue
app = Celery(
'leak_monitor',
broker='memory://localhost',
# backend='rpc://',
include=['tasks.tg_crawler', 'tasks.forum_crawler']
)
app.conf.update(
worker_pool='solo',
worker_max_tasks_per_child=100,
task_serializer='json',
result_serializer='json',
accept_content=['json'],
timezone='Europe/Moscow',
enable_utc=True,
)
app.conf.task_queues = (
Queue('telegram', routing_key='telegram.#'),
Queue('forum', routing_key='forum.#'),
)
app.conf.beat_schedule = {
'monitor-telegram-channels': {
'task': 'tasks.tg_crawler.monitor_channels',
'schedule': 10.0,
'options': {'queue': 'telegram'}
},
'crawl-forum': {
'task': 'forum_crawler.crawl_forum_task',
'schedule': 3600.0 * 24, # час
'args': ('https://thehackernews.com/search/label/data%20breach', [])
},
}
+19
View File
@@ -0,0 +1,19 @@
from celery_app import app
from utils.logg import LoggerSingleton
logger = LoggerSingleton.get_logger()
if __name__ == '__main__':
logger.info("Starting Celery beat scheduler...")
try:
app.loader.import_default_modules()
beat = app.Beat(
logfile=None,
loglevel='info',
socket_timeout=30
)
beat.run()
except Exception as e:
logger.error(f"Beat failed: {e}")
raise
+26 -6
View File
@@ -7,21 +7,41 @@ services:
ports: ports:
- "5000:5000" - "5000:5000"
volumes: volumes:
- .:/app - ./web:/app/web
- /var/run/docker.sock:/var/run/docker.sock
- type: bind
source: ./tg_nodes.log
target: /app/tg_nodes.log
read_only: true
environment:
- PYTHONUNBUFFERED=1
privileged: true
restart: unless-stopped
command: python web/app.py command: python web/app.py
depends_on:
- tg_node_0
- tg_node_1
tg_node_0: tg_node_0:
build: . build: .
volumes:
- type: bind
source: ./tg_nodes.log
target: /app/tg_nodes.log
environment:
- PYTHONUNBUFFERED=1
container_name: tg-node-0 container_name: tg-node-0
command: python tg/node_tg_0.py restart: unless-stopped
command: python tg/tg_node_0.py
tg_node_1: tg_node_1:
build: . build: .
volumes:
- type: bind
source: ./tg_nodes.log
target: /app/tg_nodes.log
environment:
- PYTHONUNBUFFERED=1
container_name: tg-node-1 container_name: tg-node-1
command: python tg/node_tg_1.py restart: unless-stopped
command: python tg/tg_node_1.py
volumes: volumes:
pg_data: pg_data:
+4 -17
View File
@@ -27,34 +27,21 @@ class DriverCreator:
).create_extension() ).create_extension()
def get_driver(self): def get_driver(self):
''' extension_path = self._switch_proxy()
Отключает JS
Каждый запрос получает `сырой` html
'''
# extension_path = self._switch_proxy()
options = uc.ChromeOptions() options = uc.ChromeOptions()
# options.add_argument(f"--load-extension={extension_path}") # временно options.add_argument(f"--load-extension={extension_path}") # временно
options.add_argument("--headless=new") # options.add_argument("--headless=new")
options.add_argument("--disable-gpu") options.add_argument("--disable-gpu")
options.add_argument("--disable-dev-shm-usage") options.add_argument("--disable-dev-shm-usage")
options.add_argument("--no-sandbox") options.add_argument("--no-sandbox")
options.add_argument("--disable-webgl")
options.add_argument("--disable-software-rasterizer")
# options.add_argument("--disable-extensions")
prefs = {"profile.managed_default_content_settings.javascript": 2}
options.experimental_options["prefs"] = prefs
driver = uc.Chrome( driver = uc.Chrome(
options=options, options=options,
version_main=132, version_main=135,
# user_multi_procs=True # user_multi_procs=True
) )
driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
"source": "Object.defineProperty(navigator, 'javaEnabled', {get: () => false});"
})
driver.execute_cdp_cmd("Emulation.setScriptExecutionDisabled", {"value": True})
return driver return driver
+29
View File
@@ -0,0 +1,29 @@
from celery_app import app
from utils.logg import LoggerSingleton
logger = LoggerSingleton.get_logger()
# Брокер центральный узел, который:
# Принимает задачи от beat (планировщика)
# Распределяет их по очередям (telegram, forum).
# Передаёт задачи воркерам, которые подписаны на эти очереди.
def main():
try:
logger.info("Starting Celery worker...")
app.worker_main(
argv=[
'worker',
'--loglevel=debug',
'--pool=solo',
'-Q', 'telegram',
'--without-heartbeat',
'--without-gossip',
# '-B'
]
)
except Exception as e:
logger.error(f"Failed to start worker: {e}")
if __name__ == '__main__':
main()
+5
View File
@@ -8,3 +8,8 @@ grpc_interceptor_headers
telethon telethon
schedule schedule
psycopg2-binary psycopg2-binary
docker
asyncio
pytz
flask
apscheduler
View File
+97
View File
@@ -0,0 +1,97 @@
from bs4 import BeautifulSoup
import os
import psycopg2
from dotenv import load_dotenv
from celery import Celery
from utils.logg import LoggerSingleton
from driver.driver_creator import DriverCreator
from selenium.webdriver.common.by import By
from time import sleep
from celery_app import app
load_dotenv()
logger = LoggerSingleton.get_logger()
class ForumCrawler:
def __init__(self, forum_url, proxy_list):
self.forum_url = forum_url
self.proxy_list = proxy_list
self.db_config = {
'host': os.getenv('DB_HOST'),
'port': os.getenv('DB_PORT'),
'database': os.getenv('DB_NAME'),
'user': os.getenv('DB_USER'),
'password': os.getenv('DB_PASSWORD')
}
self.conn = None
self._connect()
def _connect(self):
try:
self.conn = psycopg2.connect(**self.db_config)
logger.info("Database connection established")
except Exception as e:
logger.error(f"Database connection error: {e}")
raise
def _close(self):
if self.conn and not self.conn.closed:
try:
self.conn.close()
logger.info("Database connection closed")
except Exception as e:
logger.error(f"Error closing database connection: {e}")
def _crawl_leaks(self):
driver_creator = DriverCreator(self.proxy_list)
driver = driver_creator.get_driver()
try:
logger.info(f"Starting forum crawl: {self.forum_url}")
driver.get(self.forum_url)
sleep(2)
posts = driver.find_elements(By.CSS_SELECTOR, 'a.story-link h2.home-title')
if not posts:
logger.info("No posts found on the page")
return
last_post = posts[0]
title = last_post.text.strip()
link = last_post.get_attribute('href')
post_content = f"{title} - {link}"
if 'data breach' in title.lower() or 'leak' in title.lower():
self._save_to_db('Hacker News', post_content)
logger.info(f"New leak found: {title} - {link}")
else:
logger.info("Last post is not about leaks")
except Exception as e:
logger.error(f"Error during forum crawling: {e}")
raise
finally:
driver.quit()
logger.info("WebDriver session closed")
def _save_to_db(self, source, message):
if not self.conn or self.conn.closed:
self._connect()
try:
with self.conn.cursor() as cur:
cur.execute(
"INSERT INTO leaks (resource_name, message) VALUES (%s, %s)",
(source, message)
)
self.conn.commit()
logger.info(f"Leak from {source} saved in the database")
except Exception as e:
logger.error(f"Error writing to the database: {e}")
@app.task(name='forum_crawler.crawl_forum_task')
def crawl_forum_task(forum_url, proxy_list):
crawler = ForumCrawler(forum_url, proxy_list)
try:
crawler._crawl_leaks()
finally:
crawler._close()
+121
View File
@@ -0,0 +1,121 @@
import logging
import os
import asyncio
import psycopg2
from datetime import datetime
from telethon.sync import TelegramClient
from telethon.tl.functions.messages import GetHistoryRequest
from dotenv import load_dotenv
from celery import Celery
from utils.logg import LoggerSingleton
from celery_app import app
load_dotenv()
logger = LoggerSingleton.get_logger()
class TelegramMonitor:
def __init__(self):
self.db_config = {
'host': os.getenv('DB_HOST'),
'port': os.getenv('DB_PORT'),
'database': os.getenv('DB_NAME'),
'user': os.getenv('DB_USER'),
'password': os.getenv('DB_PASSWORD')
}
self.conn = None
self._connect()
def _connect(self):
try:
self.conn = psycopg2.connect(**self.db_config)
logger.info("Database connection established")
except Exception as e:
logger.error(f"Database connection error: {e}")
raise
def _close(self):
if self.conn and not self.conn.closed:
try:
self.conn.close()
logger.info("Database connection closed")
except Exception as e:
logger.error(f"Error closing database connection: {e}")
def _fetch_post(self, channel_username, source_name):
try:
with TelegramClient('session', os.getenv('API_ID'), os.getenv('API_HASH')) as client:
entity = client.get_entity(channel_username)
history = client(GetHistoryRequest(
peer=entity,
limit=1,
offset_date=None,
offset_id=0,
max_id=0,
min_id=0,
add_offset=0,
hash=0
))
if history.messages:
self._save_to_db(source_name, history.messages[0].message)
except Exception as e:
logger.error(f"Error fetching post from {source_name}: {e}")
raise
def _save_to_db(self, source, message):
if not self.conn or self.conn.closed:
self._connect()
try:
with self.conn.cursor() as cur:
cur.execute(
"INSERT INTO leaks (resource_name, message) VALUES (%s, %s)",
(source, message)
)
self.conn.commit()
logger.info(f"Data from {source} saved successfully")
except Exception as e:
logger.error(f"Database save error for {source}: {e}")
self.conn.rollback()
raise
@app.task(bind=True, name='tasks.tg_crawler.monitor_channels')
def monitor_channels(self):
channels = [
('trueosint', 'trueosint'),
('dataleak', 'dataleak'),
('Vaultofdataleaksss', 'Vaultofdataleaksss')
]
logger.info("Starting Telegram channels monitoring")
for channel, source in channels:
try:
monitor_channel.apply_async(
args=(channel, source),
queue='telegram',
retry=True,
retry_policy={
'max_retries': 3,
'interval_start': 2,
'interval_step': 5,
'interval_max': 20
}
)
logger.info(f"Sent task for channel: {channel}")
except Exception as e:
logger.error(f"Failed to send task for {channel}: {e}")
raise self.retry(exc=e)
@app.task(bind=True, name='tasks.tg_crawler.monitor_channel')
def monitor_channel(self, channel, source):
logger.info(f"Starting monitoring channel: {channel}")
monitor = TelegramMonitor()
try:
monitor._fetch_post(channel, source)
logger.info(f"Successfully monitored channel: {channel}")
except Exception as e:
logger.error(f"Channel monitoring failed for {channel}: {e}")
raise self.retry(exc=e)
finally:
monitor._close()
-73
View File
@@ -1,73 +0,0 @@
import logging
import psycopg2
from datetime import datetime
from telethon.sync import TelegramClient
from telethon.tl.functions.messages import GetHistoryRequest
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('tg_nodes.log'),
logging.StreamHandler()
]
)
class TelegramChannelMonitor:
db_config = None
def __init__(self, session_name, api_id, api_hash, channel_username, source_name):
self.session_name = session_name
self.api_id = api_id
self.api_hash = api_hash
self.channel_username = channel_username
self.source_name = source_name
@classmethod
def set_db_config(cls, config):
cls.db_config = config
def fetch_last_post(self):
logging.info(f"[{self.source_name}] checking a new post...")
try:
with TelegramClient(self.session_name, self.api_id, self.api_hash) as client:
entity = client.get_entity(self.channel_username)
history = client(GetHistoryRequest(
peer=entity,
limit=1,
offset_date=None,
offset_id=0,
max_id=0,
min_id=0,
add_offset=0,
hash=0
))
if history.messages:
msg = history.messages[0]
logging.info(f"[{self.source_name}] received a post: {msg.message[:60]}...")
self.save_to_db(self.source_name, msg.message)
else:
logging.info(f"[{self.source_name}] there is no new messages")
except Exception as e:
logging.error(f"[{self.source_name}] error when receiving a post: {e}")
def save_to_db(self, source, message):
if not self.db_config:
logging.error("DB config is not set")
return
try:
conn = psycopg2.connect(**self.db_config)
cur = conn.cursor()
cur.execute(
"INSERT INTO leaks (source, message) VALUES (%s, %s)",
(source, message)
)
conn.commit()
cur.close()
conn.close()
logging.info(f"[{self.source_name}] message is recorded in the database")
except Exception as e:
logging.error(f"[{self.source_name}] error when writing to the database: {e}")
-34
View File
@@ -1,34 +0,0 @@
import asyncio
import os
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from pytz import timezone
from tg_crawler import TelegramChannelMonitor
from dotenv import load_dotenv
load_dotenv()
TelegramChannelMonitor.set_db_config({
'host': os.getenv("HOST"),
'port': os.getenv("PORT"),
'database': os.getenv("DBNAME"),
'user': os.getenv("USER"),
'password': os.getenv("PASSWORD")
})
monitor = TelegramChannelMonitor(
session_name='session_trueosint',
api_id=os.getenv("TELETHON_API_ID"),
api_hash=os.getenv("TELETHON_API_HASH"),
channel_username='trueosint',
source_name='trueosint'
)
def main():
scheduler = AsyncIOScheduler()
scheduler.add_job(monitor.fetch_last_post, "cron", hour=9, minute=0, timezone=timezone("Europe/Moscow"))
scheduler.start()
asyncio.get_event_loop().run_forever()
if __name__ == '__main__':
main()
-34
View File
@@ -1,34 +0,0 @@
import asyncio
import os
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from pytz import timezone
from tg_crawler import TelegramChannelMonitor
from dotenv import load_dotenv
load_dotenv()
TelegramChannelMonitor.set_db_config({
'host': os.getenv("HOST"),
'port': os.getenv("PORT"),
'database': os.getenv("DBNAME"),
'user': os.getenv("USER"),
'password': os.getenv("PASSWORD")
})
monitor = TelegramChannelMonitor(
session_name='session_dataleak',
api_id=os.getenv("TELETHON_API_ID"),
api_hash=os.getenv("TELETHON_API_HASH"),
channel_username='dataleak',
source_name='dataleak'
)
def main():
scheduler = AsyncIOScheduler()
scheduler.add_job(monitor.fetch_last_post, "cron", hour=9, minute=0, timezone=timezone("Europe/Moscow"))
scheduler.start()
asyncio.get_event_loop().run_forever()
if __name__ == '__main__':
main()
+27
View File
@@ -0,0 +1,27 @@
import logging
class LoggerSingleton:
_logger = None
@staticmethod
def get_logger():
if LoggerSingleton._logger is None:
LoggerSingleton._logger = logging.getLogger(__name__)
LoggerSingleton._logger.setLevel(logging.INFO)
formatter = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S"
)
console_handler = logging.StreamHandler()
console_handler.setFormatter(formatter)
file_handler = logging.FileHandler('tempora.log', encoding='utf-8')
file_handler.setFormatter(formatter)
LoggerSingleton._logger.addHandler(console_handler)
LoggerSingleton._logger.addHandler(file_handler)
return LoggerSingleton._logger
+118 -11
View File
@@ -1,9 +1,13 @@
import os import os
from flask import Flask, render_template from flask import Flask, render_template
import psycopg2 import psycopg2
from datetime import datetime
import docker
app = Flask(__name__) app = Flask(__name__)
docker_client = docker.from_env()
DB_CONFIG = { DB_CONFIG = {
'host': os.getenv("HOST"), 'host': os.getenv("HOST"),
'port': os.getenv("PORT"), 'port': os.getenv("PORT"),
@@ -12,23 +16,126 @@ DB_CONFIG = {
'password': os.getenv("PASSWORD") 'password': os.getenv("PASSWORD")
} }
@app.route("/") def parse_log_line(line):
def index(): """Парсит строку лога в формате: [timestamp] сообщение"""
with psycopg2.connect(**DB_CONFIG) as conn: try:
with conn.cursor() as cur: line = line.strip()
cur.execute("SELECT source, message, created_at FROM leaks ORDER BY created_at DESC LIMIT 50") if line.startswith('[') and ']' in line:
leaks = cur.fetchall() return line
return render_template("index.html", leaks=leaks) return f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {line}"
except:
return line
def get_parser_status():
"""Получает статус парсеров из Docker"""
try:
containers = docker_client.containers.list(all=True)
status = {
'tg-node-0': False,
'tg-node-1': False,
'total': 0,
'active': 0,
'errors': 0
}
for container in containers:
if container.name == 'tg-node-0':
status['tg-node-0'] = container.status == 'running'
elif container.name == 'tg-node-1':
status['tg-node-1'] = container.status == 'running'
status['active'] = sum([status['tg-node-0'], status['tg-node-1']])
status['errors'] = 2 - status['active'] # Просто для примера
status['total'] = 2
return status
except Exception as e:
print(f"Error getting docker status: {e}")
return None
def get_leaks_stats():
"""Получает статистику по утечкам из базы данных"""
try:
conn = psycopg2.connect(**DB_CONFIG)
cursor = conn.cursor()
cursor.execute("SELECT COUNT(*) FROM leaks")
total_leaks = cursor.fetchone()[0]
cursor.execute("""
SELECT DATE(created_at) as day, COUNT(*)
FROM leaks
WHERE created_at >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY day
ORDER BY day
""")
leaks_by_day = cursor.fetchall()
cursor.execute("""
SELECT resource_name, COUNT(*)
FROM leaks
GROUP BY resource_name
ORDER BY COUNT(*) DESC
LIMIT 3
""")
leaks_by_source = cursor.fetchall()
cursor.execute("""
SELECT resource_name, message, created_at
FROM leaks
ORDER BY created_at DESC
LIMIT 10
""")
recent_leaks = cursor.fetchall()
return {
'total_leaks': total_leaks,
'leaks_by_day': leaks_by_day,
'leaks_by_source': leaks_by_source,
'recent_leaks': recent_leaks
}
except Exception as e:
print(f"Database error: {e}")
return None
finally:
if 'conn' in locals():
conn.close()
@app.route("/")
@app.route("/index.html")
def index():
parser_status = get_parser_status()
leaks_stats = get_leaks_stats()
if not leaks_stats:
leaks_stats = {
'total_leaks': 150,
'leaks_by_day': [(datetime.now().date(), 5)],
'leaks_by_source': [('Telegram', 80), ('Форум', 50), ('Даркнет', 20)],
'recent_leaks': [('Telegram', 'Новая утечка данных', datetime.now())]
}
return render_template(
"index.html",
parser_status=parser_status,
leaks_stats=leaks_stats
)
@app.route("/logs.html")
@app.route("/logs") @app.route("/logs")
def logs(): def logs():
log_path = os.path.join(os.path.dirname(__file__), '..', 'tg_nodes.log') log_path = '/app/tg_nodes.log'
try: try:
with open(log_path, 'r', encoding='utf-8') as f: with open(log_path, 'r', encoding='utf-8') as f:
lines = f.readlines()[-100:] lines = f.readlines()[-100:]
parsed_logs = [parse_log_line(line) for line in lines if line.strip()]
except FileNotFoundError: except FileNotFoundError:
lines = ["Лог-файл не найден"] parsed_logs = ["[ERROR] Лог-файл не найден"]
return render_template("logs.html", logs=lines) except Exception as e:
parsed_logs = [f"[ERROR] Ошибка чтения лог-файла: {str(e)}"]
return render_template("logs.html", logs=parsed_logs)
if __name__ == "__main__": if __name__ == "__main__":
app.run(debug=True) app.run(host='0.0.0.0', port=5000)
+69 -13
View File
@@ -1,5 +1,6 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="ru"> <html lang="ru">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -17,6 +18,7 @@
min-height: 100vh; min-height: 100vh;
overflow: hidden; overflow: hidden;
} }
.sidebar { .sidebar {
width: 250px; width: 250px;
background-color: #2e2e2e; background-color: #2e2e2e;
@@ -30,13 +32,16 @@
bottom: 0; bottom: 0;
z-index: 1; z-index: 1;
} }
.sidebar.collapsed { .sidebar.collapsed {
width: 60px; width: 60px;
} }
.sidebar.collapsed h2, .sidebar.collapsed h2,
.sidebar.collapsed ul li a span { .sidebar.collapsed ul li a span {
display: none; display: none;
} }
.sidebar h2 { .sidebar h2 {
color: #3399FF; color: #3399FF;
text-align: center; text-align: center;
@@ -44,13 +49,16 @@
font-size: 24px; font-size: 24px;
transition: opacity 0.3s ease; transition: opacity 0.3s ease;
} }
.sidebar ul { .sidebar ul {
list-style: none; list-style: none;
padding: 0; padding: 0;
} }
.sidebar ul li { .sidebar ul li {
margin: 15px 0; margin: 15px 0;
} }
.sidebar ul li a { .sidebar ul li a {
color: #ffffff; color: #ffffff;
text-decoration: none; text-decoration: none;
@@ -59,16 +67,20 @@
display: flex; display: flex;
align-items: center; align-items: center;
} }
.sidebar ul li a:hover { .sidebar ul li a:hover {
color: #3399FF; color: #3399FF;
} }
.sidebar ul li a i { .sidebar ul li a i {
margin-right: 10px; margin-right: 10px;
font-size: 20px; font-size: 20px;
} }
.sidebar ul li a span { .sidebar ul li a span {
transition: opacity 0.3s ease; transition: opacity 0.3s ease;
} }
.toggle-btn { .toggle-btn {
position: fixed; position: fixed;
left: 10px; left: 10px;
@@ -81,6 +93,7 @@
cursor: pointer; cursor: pointer;
z-index: 1000; z-index: 1000;
} }
.container { .container {
flex: 1; flex: 1;
padding: 20px; padding: 20px;
@@ -91,42 +104,50 @@
position: relative; position: relative;
z-index: 0; z-index: 0;
} }
.container.collapsed { .container.collapsed {
margin-left: 60px; margin-left: 60px;
} }
h1 { h1 {
color: #3399FF; color: #3399FF;
text-align: center; text-align: center;
margin-bottom: 20px; margin-bottom: 20px;
} }
.grid { .grid {
display: grid; display: grid;
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
gap: 20px; gap: 20px;
margin-top: 20px; margin-top: 20px;
} }
.card { .card {
background-color: #2e2e2e; background-color: #2e2e2e;
padding: 20px; padding: 20px;
border-radius: 8px; border-radius: 8px;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.2); box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
} }
#map { #map {
height: 400px; height: 400px;
border-radius: 8px; border-radius: 8px;
grid-column: span 2; grid-column: span 2;
} }
.screenshots { .screenshots {
display: flex; display: flex;
overflow-x: auto; overflow-x: auto;
gap: 10px; gap: 10px;
padding: 10px; padding: 10px;
} }
.screenshots img { .screenshots img {
max-height: 200px; max-height: 200px;
border-radius: 5px; border-radius: 5px;
border: 2px solid #3399FF; border: 2px solid #3399FF;
} }
.logs { .logs {
background-color: #2e2e2e; background-color: #2e2e2e;
padding: 15px; padding: 15px;
@@ -137,6 +158,7 @@
font-size: 14px; font-size: 14px;
color: #3399FF; color: #3399FF;
} }
.logs p { .logs p {
margin: 5px 0; margin: 5px 0;
} }
@@ -149,10 +171,12 @@
height: 20px !important; height: 20px !important;
box-shadow: 0 0 10px rgba(51, 153, 255, 0.5); box-shadow: 0 0 10px rgba(51, 153, 255, 0.5);
} }
.leaflet-popup-content { .leaflet-popup-content {
color: #1e1e1e; color: #1e1e1e;
font-size: 14px; font-size: 14px;
} }
.leaflet-popup-content-wrapper { .leaflet-popup-content-wrapper {
border-radius: 8px; border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
@@ -160,6 +184,7 @@
</style> </style>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
</head> </head>
<body> <body>
<!-- Боковое меню --> <!-- Боковое меню -->
<div class="sidebar" id="sidebar"> <div class="sidebar" id="sidebar">
@@ -196,8 +221,20 @@
</div> </div>
<div class="card"> <div class="card">
<h2>Состояние парсеров</h2> <h2>Состояние парсеров</h2>
<div class="parsers-status">
<p>TG Node 0:
<span class="status-{% if parser_status['tg-node-0'] %}active{% else %}error{% endif %}">
{% if parser_status['tg-node-0'] %}Активен{% else %}Ошибка{% endif %}
</span>
</p>
<p>TG Node 1:
<span class="status-{% if parser_status['tg-node-1'] %}active{% else %}error{% endif %}">
{% if parser_status['tg-node-1'] %}Активен{% else %}Ошибка{% endif %}
</span>
</p>
<canvas id="parsersStatusChart"></canvas> <canvas id="parsersStatusChart"></canvas>
</div> </div>
</div>
<div class="card"> <div class="card">
<h2>Источники утечек</h2> <h2>Источники утечек</h2>
<canvas id="sourcesChart"></canvas> <canvas id="sourcesChart"></canvas>
@@ -229,19 +266,26 @@
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"></script> <script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"></script>
<script> <script>
// График утечек за месяц
const monthlyLeaksCtx = document.getElementById('monthlyLeaksChart').getContext('2d'); const monthlyLeaksCtx = document.getElementById('monthlyLeaksChart').getContext('2d');
new Chart(monthlyLeaksCtx, { new Chart(monthlyLeaksCtx, {
type: 'line', type: 'line',
data: { data: {
labels: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15'], labels: [
{% for day in leaks_stats['leaks_by_day'] %}
"{{ day[0].strftime('%d.%m') }}"{% if not loop.last %}, {% endif %}
{% endfor %}
],
datasets: [{ datasets: [{
label: 'Утечки', label: 'Утечки',
data: [12, 19, 3, 5, 2, 3, 15, 20, 10, 8, 12, 14, 18, 20, 22], data: [
{% for day in leaks_stats['leaks_by_day'] %}
{{ day[1] }}{% if not loop.last %}, {% endif %}
{% endfor %}
],
borderColor: '#3399FF', borderColor: '#3399FF',
tension: 0.1, tension: 0.1,
fill: true, fill: true,
backgroundColor: 'rgba(51, 153, 255, 0.1)', backgroundColor: 'rgba(51, 153, 255, 0.1)'
}] }]
}, },
options: { options: {
@@ -249,7 +293,7 @@
plugins: { plugins: {
legend: { legend: {
labels: { labels: {
color: '#fff', color: '#fff'
} }
} }
}, },
@@ -257,24 +301,23 @@
y: { y: {
beginAtZero: true, beginAtZero: true,
grid: { grid: {
color: '#444', color: '#444'
}, },
ticks: { ticks: {
color: '#fff', color: '#fff'
} }
}, },
x: { x: {
grid: { grid: {
color: '#444', color: '#444'
}, },
ticks: { ticks: {
color: '#fff', color: '#fff'
} }
} }
} }
} }
}); });
// График состояния парсеров // График состояния парсеров
const parsersStatusCtx = document.getElementById('parsersStatusChart').getContext('2d'); const parsersStatusCtx = document.getElementById('parsersStatusChart').getContext('2d');
new Chart(parsersStatusCtx, { new Chart(parsersStatusCtx, {
@@ -283,7 +326,11 @@
labels: ['Активны', 'Ошибки', 'Неактивны'], labels: ['Активны', 'Ошибки', 'Неактивны'],
datasets: [{ datasets: [{
label: 'Состояние', label: 'Состояние',
data: [8, 1, 1], data: [
{{ parser_status['active'] }},
{{ parser_status['errors'] }},
0 // Неактивных нет в текущей логике
],
backgroundColor: ['#3399FF', '#ff4444', '#666666'], backgroundColor: ['#3399FF', '#ff4444', '#666666'],
}] }]
}, },
@@ -304,10 +351,18 @@
new Chart(sourcesCtx, { new Chart(sourcesCtx, {
type: 'bar', type: 'bar',
data: { data: {
labels: ['Форумы', 'Даркнет', 'Telegram'], labels: [
{% for source in leaks_stats['leaks_by_source'] %}
'{{ source[0] }}'{% if not loop.last %},{% endif %}
{% endfor %}
],
datasets: [{ datasets: [{
label: 'Количество утечек', label: 'Количество утечек',
data: [45, 30, 25], data: [
{% for source in leaks_stats['leaks_by_source'] %}
{{ source[1] }}{% if not loop.last %},{% endif %}
{% endfor %}
],
backgroundColor: ['#3399FF', '#00cc99', '#0099cc'], backgroundColor: ['#3399FF', '#00cc99', '#0099cc'],
}] }]
}, },
@@ -365,4 +420,5 @@
}); });
</script> </script>
</body> </body>
</html> </html>
+16 -21
View File
@@ -99,8 +99,10 @@
padding: 20px; padding: 20px;
margin-left: 250px; margin-left: 250px;
transition: margin-left 0.3s ease; transition: margin-left 0.3s ease;
position: relative; min-height: 100vh;
z-index: 1; display: flex;
justify-content: center;
align-items: center;
} }
.container.collapsed { .container.collapsed {
@@ -116,7 +118,7 @@
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
z-index: -1; z-index: -1;
opacity: 0.8; /* Полупрозрачность видео */ opacity: 0.8;
} }
/* Затемнение поверх видео */ /* Затемнение поверх видео */
@@ -126,23 +128,18 @@
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background: rgba(0, 0, 0, 0.7); /* Черный полупрозрачный слой */ background: rgba(0, 0, 0, 0.7);
z-index: -1; z-index: -1;
} }
/* Контейнер для логов */ /* Контейнер для логов */
.logs-container { .logs-container {
position: absolute; width: 90%;
top: 50%; max-width: 800px;
left: 20px; background-color: rgba(46, 46, 46, 0.9);
transform: translateY(-50%);
width: 40%;
max-width: 600px;
background-color: rgba(46, 46, 46, 0.9); /* Полупрозрачный фон */
padding: 20px; padding: 20px;
border-radius: 10px; border-radius: 10px;
box-shadow: 0 0 20px rgba(51, 153, 255, 0.5); box-shadow: 0 0 20px rgba(51, 153, 255, 0.5);
z-index: 1;
} }
.logs-container h2 { .logs-container h2 {
@@ -218,15 +215,13 @@
<!-- Контейнер для логов --> <!-- Контейнер для логов -->
<div class="logs-container"> <div class="logs-container">
<h2>Логи системы</h2> <h2>Логи системы (tg_nodes.log)</h2>
<div class="logs"> <div class="logs" id="logs-container">
<p>[2023-10-01 12:34] Парсер форума запущен.</p> {% for log in logs %}
<p>[2023-10-01 12:35] Найдена новая утечка на форуме X.</p> <p>{{ log }}</p>
<p>[2023-10-01 12:36] Ошибка парсера Telegram: timeout.</p> {% else %}
<p>[2023-10-01 12:37] Утечка данных в Москве зафиксирована.</p> <p>Нет записей в логах</p>
<p>[2023-10-01 12:38] Парсер даркнета завершил работу.</p> {% endfor %}
<p>[2023-10-01 12:39] Новый скриншот добавлен в базу.</p>
<!-- Добавьте больше логов по необходимости -->
</div> </div>
</div> </div>
</div> </div>