Создание дашборда для мониторинга инфраструктуры цели
Создание дашборда для мониторинга инфраструктуры цели
После того как все этапы технического OSINT автоматизированы и данные собираются в базу данных, возникает следующая задача: как сделать эту информацию доступной, наглядной и удобной для анализа. Отчёты в виде JSON или CSV хороши для машинной обработки, но для человека визуализация в виде графиков, карт, таблиц и интерактивных панелей значительно эффективнее. Дашборд (dashboard) — это единая панель управления, которая отображает ключевые показатели, изменения в инфраструктуре, новые поддомены, открытые порты, уязвимости и другие данные в реальном времени (или близком к нему). Данная статья посвящена созданию веб-дашборда для мониторинга инфраструктуры цели с использованием открытых инструментов.
Зачем нужен дашборд в техническом OSINT?
В этой статье мы рассмотрим два основных подхода:
- Быстрый старт с Grafana — минимальная настройка, мощная визуализация.
- Гибкий дашборд на Python (Dash) — полный контроль над данными и интерфейсом.
ВАРИАНТ 1: ДАШБОРД НА GRAFANA + SQLITE
Grafana — это популярная платформа для мониторинга и визуализации данных. Она поддерживает множество источников данных, включая SQLite, PostgreSQL, Prometheus, Elasticsearch. Для нашего случая SQLite подходит отлично — данные уже хранятся в ней.
SQLite Database → Grafana (с плагином SQLite) → Дашборд с панелями
# Ubuntu/Debian sudo apt-get update sudo apt-get install -y software-properties-common sudo add-apt-repository "deb https://packages.grafana.com/oss/deb stable main" sudo apt-get update sudo apt-get install grafana # Запуск sudo systemctl enable grafana-server sudo systemctl start grafana-server
Grafana будет доступна по адресу http://localhost:3000 (логин/пароль по умолчанию: admin/admin).
Стандартный Grafana не поддерживает SQLite напрямую. Нужно использовать плагин:
- Установить плагин
grafana-sqlite-datasource:bashsudo grafana-cli plugins install frser-sqlite-datasource sudo systemctl restart grafana-server - В веб-интерфейсе Grafana: Configuration → Data Sources → Add data source → SQLite.
Панель 1: Количество поддоменов за последние 30 дней (гистограмма)
SELECT
DATE(discovered_at) as date,
COUNT(*) as count
FROM subdomains
WHERE discovered_at > datetime('now', '-30 days')
GROUP BY DATE(discovered_at)
ORDER BY date ASCПанель 2: Таблица последних поддоменов
SELECT subdomain, discovered_at FROM subdomains ORDER BY discovered_at DESC LIMIT 20
Панель 3: Истекающие сертификаты (таблица)
SELECT name, valid_to,
julianday(valid_to) - julianday('now') as days_left
FROM certificates
WHERE days_left < 30 AND days_left > 0
ORDER BY days_left ASCПанель 4: Распределение открытых портов (круговая диаграмма)
SELECT port, COUNT(*) as count FROM shodan_ports GROUP BY port ORDER BY count DESC LIMIT 10
- Быстрая настройка, красивые графики "из коробки".
- Автообновление данных.
- Поддержка алертов (email, Telegram, Slack).
- Ограниченная кастомизация (сложно добавить нестандартную логику).
- Нужно устанавливать плагин для SQLite (или использовать PostgreSQL).
ВАРИАНТ 2: ГИБКИЙ ДАШБОРД НА PYTHON (DASH + PLOTLY)
Dash — это веб-фреймворк от Plotly для создания аналитических дашбордов на Python. Он позволяет полностью контролировать логику обработки данных и визуализацию, не требуя знаний JavaScript.
SQLite Database → Python (Dash) → Веб-интерфейс (HTML/CSS/JS)
pip install dash plotly pandas
dashboard/ ├── app.py # Главный файл приложения ├── data/ │ └── osint.db # База данных ├── assets/ │ └── style.css # Стили (опционально) └── requirements.txt
import dash
from dash import dcc, html, Input, Output, State
import plotly.graph_objects as go
import plotly.express as px
import sqlite3
import pandas as pd
from datetime import datetime, timedelta
# Инициализация приложения
app = dash.Dash(__name__, title="OSINT Dashboard")
server = app.server
# Функция подключения к БД
def get_db_connection():
conn = sqlite3.connect('data/osint.db')
conn.row_factory = sqlite3.Row
return conn
# ============ ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ============
def get_subdomain_history(domain=None, days=30):
"""Получение истории поддоменов за последние N дней"""
conn = get_db_connection()
query = """
SELECT DATE(discovered_at) as date, COUNT(*) as count
FROM subdomains s
JOIN domains d ON s.domain_id = d.id
WHERE d.domain = ? AND discovered_at > datetime('now', '-' || ? || ' days')
GROUP BY DATE(discovered_at)
ORDER BY date ASC
"""
df = pd.read_sql_query(query, conn, params=(domain, days))
conn.close()
return df
def get_recent_subdomains(domain=None, limit=20):
"""Получение последних поддоменов"""
conn = get_db_connection()
query = """
SELECT s.subdomain, s.discovered_at
FROM subdomains s
JOIN domains d ON s.domain_id = d.id
WHERE d.domain = ?
ORDER BY s.discovered_at DESC
LIMIT ?
"""
df = pd.read_sql_query(query, conn, params=(domain, limit))
conn.close()
return df
def get_expiring_certs(domain=None, days=30):
"""Получение истекающих сертификатов"""
conn = get_db_connection()
query = """
SELECT name, issuer, valid_to,
julianday(valid_to) - julianday('now') as days_left
FROM certificates c
JOIN domains d ON c.domain_id = d.id
WHERE d.domain = ? AND days_left < ? AND days_left > 0
ORDER BY days_left ASC
"""
df = pd.read_sql_query(query, conn, params=(domain, days))
conn.close()
return df
def get_port_distribution(domain=None):
"""Распределение открытых портов"""
conn = get_db_connection()
query = """
SELECT port, COUNT(*) as count
FROM shodan_ports sp
JOIN domains d ON sp.domain_id = d.id
WHERE d.domain = ?
GROUP BY port
ORDER BY count DESC
LIMIT 15
"""
df = pd.read_sql_query(query, conn, params=(domain,))
conn.close()
return df
def get_tech_stack(domain=None):
"""Технологический стек"""
conn = get_db_connection()
# Предположим, есть таблица technologies
query = """
SELECT tech_name, COUNT(*) as count
FROM technologies t
JOIN domains d ON t.domain_id = d.id
WHERE d.domain = ?
GROUP BY tech_name
ORDER BY count DESC
LIMIT 15
"""
# Если таблицы нет — заглушка
df = pd.DataFrame({'tech_name': ['nginx', 'PHP', 'WordPress', 'jQuery', 'Bootstrap'],
'count': [1, 1, 1, 1, 1]})
conn.close()
return df
def get_all_domains():
"""Список всех отслеживаемых доменов"""
conn = get_db_connection()
df = pd.read_sql_query("SELECT domain FROM domains ORDER BY domain ASC", conn)
conn.close()
return df['domain'].tolist()
# ============ МАКЕТ ДАШБОРДА ============
app.layout = html.Div([
# Заголовок
html.H1("📊 OSINT Infrastructure Dashboard", style={'textAlign': 'center', 'marginBottom': 30}),
# Выбор домена
html.Div([
html.Label("Выберите домен:"),
dcc.Dropdown(
id='domain-selector',
options=[{'label': d, 'value': d} for d in get_all_domains()],
value='example.com' if 'example.com' in get_all_domains() else get_all_domains()[0] if get_all_domains() else None,
style={'width': '50%', 'margin': '10px 0'}
)
], style={'padding': '0 20px'}),
# Верхние карточки (метрики)
html.Div([
html.Div([
html.H3("🌐 Всего поддоменов"),
html.H2(id='total-subdomains', style={'color': '#2196F3'})
], className='metric-card'),
html.Div([
html.H3("🔄 Изменений за 7 дней"),
html.H2(id='changes-7d', style={'color': '#FF9800'})
], className='metric-card'),
html.Div([
html.H3("⏳ Истекающих сертификатов (<30 дн)"),
html.H2(id='expiring-certs', style={'color': '#F44336'})
], className='metric-card'),
html.Div([
html.H3("🔓 Открытых портов"),
html.H2(id='total-ports', style={'color': '#4CAF50'})
], className='metric-card'),
], style={'display': 'flex', 'gap': '20px', 'padding': '0 20px', 'flexWrap': 'wrap'}),
# Графики (сетка 2x2)
html.Div([
# График 1: История поддоменов
html.Div([
dcc.Graph(id='subdomain-history')
], className='graph-card'),
# График 2: Распределение портов
html.Div([
dcc.Graph(id='port-distribution')
], className='graph-card'),
# График 3: Истекающие сертификаты
html.Div([
dcc.Graph(id='cert-expiry')
], className='graph-card'),
# График 4: Технологический стек
html.Div([
dcc.Graph(id='tech-stack')
], className='graph-card'),
], style={'display': 'grid', 'gridTemplateColumns': '1fr 1fr', 'gap': '20px', 'padding': '20px'}),
# Таблица последних поддоменов
html.Div([
html.H3("🆕 Последние поддомены"),
html.Div(id='recent-subdomains-table', style={'overflowX': 'auto'})
], style={'padding': '0 20px', 'marginTop': 20}),
# Автообновление (раз в 60 секунд)
dcc.Interval(
id='interval-component',
interval=60 * 1000, # 60 секунд
n_intervals=0
)
], style={'maxWidth': '1400px', 'margin': '0 auto', 'fontFamily': 'Arial, sans-serif'})
# ============ КОЛБЭКИ ============
@app.callback(
[
Output('total-subdomains', 'children'),
Output('changes-7d', 'children'),
Output('expiring-certs', 'children'),
Output('total-ports', 'children'),
Output('subdomain-history', 'figure'),
Output('port-distribution', 'figure'),
Output('cert-expiry', 'figure'),
Output('tech-stack', 'figure'),
Output('recent-subdomains-table', 'children'),
],
[Input('domain-selector', 'value'),
Input('interval-component', 'n_intervals')]
)
def update_dashboard(domain, n_intervals):
if not domain:
return "—", "—", "—", "—", {}, {}, {}, {}, "Выберите домен"
# 1. Количество поддоменов
conn = get_db_connection()
total_sub = pd.read_sql_query(
"SELECT COUNT(*) as cnt FROM subdomains s JOIN domains d ON s.domain_id = d.id WHERE d.domain = ?",
conn, params=(domain,)
).iloc[0]['cnt']
conn.close()
# 2. Изменения за 7 дней (новые поддомены)
conn = get_db_connection()
changes_7d = pd.read_sql_query(
"SELECT COUNT(*) as cnt FROM subdomains s JOIN domains d ON s.domain_id = d.id WHERE d.domain = ? AND discovered_at > datetime('now', '-7 days')",
conn, params=(domain,)
).iloc[0]['cnt']
conn.close()
# 3. Истекающие сертификаты
expiring_df = get_expiring_certs(domain)
expiring_count = len(expiring_df)
# 4. Открытые порты
ports_df = get_port_distribution(domain)
total_ports = ports_df['count'].sum() if not ports_df.empty else 0
# 5. График истории поддоменов
history_df = get_subdomain_history(domain)
fig_history = px.line(
history_df, x='date', y='count',
title='Поддомены по дням',
labels={'date': 'Дата', 'count': 'Количество'}
) if not history_df.empty else go.Figure()
# 6. Распределение портов
fig_ports = px.pie(
ports_df, values='count', names='port',
title='Распределение портов'
) if not ports_df.empty else go.Figure()
# 7. Истекающие сертификаты (бар-чарт)
if not expiring_df.empty:
fig_certs = px.bar(
expiring_df, x='name', y='days_left',
title='Дней до истечения сертификата',
labels={'name': 'Домен', 'days_left': 'Дней'},
color='days_left',
color_continuous_scale='RdYlGn_r'
)
else:
fig_certs = go.Figure()
fig_certs.add_annotation(text="Нет истекающих сертификатов", showarrow=False)
# 8. Технологический стек
tech_df = get_tech_stack(domain)
fig_tech = px.bar(
tech_df, x='count', y='tech_name',
orientation='h',
title='Технологии',
labels={'count': 'Количество', 'tech_name': 'Технология'}
) if not tech_df.empty else go.Figure()
# 9. Таблица последних поддоменов
recent_df = get_recent_subdomains(domain)
if not recent_df.empty:
table = html.Table([
html.Thead(html.Tr([
html.Th("Поддомен", style={'textAlign': 'left'}),
html.Th("Обнаружен", style={'textAlign': 'left'})
])),
html.Tbody([
html.Tr([
html.Td(row['subdomain']),
html.Td(row['discovered_at'])
]) for _, row in recent_df.iterrows()
])
], style={'width': '100%', 'borderCollapse': 'collapse'})
else:
table = "Нет данных"
return (
total_sub,
changes_7d,
expiring_count,
total_ports,
fig_history,
fig_ports,
fig_certs,
fig_tech,
table
)
# ============ ЗАПУСК ============
if __name__ == '__main__':
app.run_server(debug=True, host='0.0.0.0', port=8050)Добавление CSS-стилей (assets/style.css):
.metric-card {
background: white;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
padding: 20px;
flex: 1;
min-width: 150px;
text-align: center;
}
.metric-card h3 {
margin: 0 0 10px 0;
font-size: 14px;
color: #666;
}
.metric-card h2 {
margin: 0;
font-size: 32px;
}
.graph-card {
background: white;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
padding: 15px;
}
table {
border: 1px solid #ddd;
}
table th {
background: #f5f5f5;
padding: 10px;
border-bottom: 2px solid #ddd;
}
table td {
padding: 8px 10px;
border-bottom: 1px solid #eee;
}
table tr:hover {
background: #f9f9f9;
}python app.py
Дашборд будет доступен по адресу: http://localhost:8050
ВАРИАНТ 3: БЫСТРЫЙ СТАТИЧЕСКИЙ ОТЧЁТ (HTML)
Если веб-сервер не требуется, можно генерировать статический HTML-отчёт с графиками.
Использование библиотеки matplotlib + mpld3 для интерактивных графиков в HTML:
import matplotlib.pyplot as plt
import mpld3
import sqlite3
import pandas as pd
def generate_report(domain):
# Получение данных
history_df = get_subdomain_history(domain)
ports_df = get_port_distribution(domain)
# Генерация графиков
fig1, ax1 = plt.subplots()
ax1.plot(history_df['date'], history_df['count'])
ax1.set_title('Поддомены по дням')
fig1_html = mpld3.fig_to_html(fig1)
fig2, ax2 = plt.subplots()
ax2.pie(ports_df['count'], labels=ports_df['port'], autopct='%1.1f%%')
ax2.set_title('Распределение портов')
fig2_html = mpld3.fig_to_html(fig2)
# Сборка HTML
html = f"""
<html>
<head><title>Отчёт по {domain}</title></head>
<body>
<h1>OSINT Отчёт: {domain}</h1>
<div>{fig1_html}</div>
<div>{fig2_html}</div>
</body>
</html>
"""
with open(f'report_{domain}.html', 'w') as f:
f.write(html)ВАРИАНТ 4: ИСПОЛЬЗОВАНИЕ SPIDERFOOT
SpiderFoot — это OSINT-фреймворк с собственным веб-интерфейсом. Он автоматически собирает данные из множества источников и визуализирует их в виде графа связей.
# Docker (рекомендуемый способ) docker run -p 5000:5000 spiderfoot/spiderfoot
- Графовое представление связей (домены, IP, email, хэши).
- Автоматический сбор из 200+ модулей.
- Встроенный дашборд с картами и таймлайнами.
требует времени на настройку, может быть избыточным для простых задач.
ВАРИАНТ 5: MALTEGO ДЛЯ ВИЗУАЛИЗАЦИИ СВЯЗЕЙ
Maltego — это инструмент для визуализации связей между сущностями (доменами, IP, личностями). В сочетании с трансформами он превращает собранные данные в интерактивный граф.
- Экспортировать данные из SQLite в CSV.
- Импортировать в Maltego как "сущности" (Entities).
- Использовать трансформы для дальнейшего обогащения.
СРАВНЕНИЕ ИНСТРУМЕНТОВ ДЛЯ ДАШБОРДОВ
Практический пример: использование дашборда для мониторинга
Сценарий: аналитик мониторит 5 доменов конкурентов.
- Ежедневный сбор → данные попадают в SQLite через автоматизированный конвейер.
- Дашборд (Dash) → отображает для каждого домена:
- Количество поддоменов (график роста).
- Новые поддомены за неделю (таблица).
- Открытые порты и сервисы.
- Истекающие сертификаты (алерт).
- Обнаружение: на дашборде появляется новый поддомен
newapp.competitor.com. Аналитик видит, что это произошло сегодня. Через Shodan выясняется, что на нём открыт порт 443 с сертификатом от AWS. Вывод: конкурент запускает новый облачный сервис. - Реакция: информация передаётся в стратегический отдел.
Дашборд — это завершающий элемент системы технического OSINT, превращающий сырые данные в понятную и удобную для анализа информацию. Выбор инструмента зависит от задач: Grafana подходит для быстрого старта и оперативного мониторинга, Dash/Plotly — для глубокой кастомизации, Maltego — для визуализации связей. Интеграция дашборда с автоматизированным конвейером (из предыдущей статьи) создаёт полноценную систему непрерывной разведки, где данные собираются, обрабатываются и отображаются в реальном времени.