July 7

Создание дашборда для мониторинга инфраструктуры цели

Создание дашборда для мониторинга инфраструктуры цели

После того как все этапы технического OSINT автоматизированы и данные собираются в базу данных, возникает следующая задача: как сделать эту информацию доступной, наглядной и удобной для анализа. Отчёты в виде JSON или CSV хороши для машинной обработки, но для человека визуализация в виде графиков, карт, таблиц и интерактивных панелей значительно эффективнее. Дашборд (dashboard) — это единая панель управления, которая отображает ключевые показатели, изменения в инфраструктуре, новые поддомены, открытые порты, уязвимости и другие данные в реальном времени (или близком к нему). Данная статья посвящена созданию веб-дашборда для мониторинга инфраструктуры цели с использованием открытых инструментов.

Зачем нужен дашборд в техническом OSINT?

Подходы к созданию дашборда

В этой статье мы рассмотрим два основных подхода:

  1. Быстрый старт с Grafana — минимальная настройка, мощная визуализация.
  2. Гибкий дашборд на Python (Dash) — полный контроль над данными и интерфейсом.

ВАРИАНТ 1: ДАШБОРД НА GRAFANA + SQLITE

Grafana — это популярная платформа для мониторинга и визуализации данных. Она поддерживает множество источников данных, включая SQLite, PostgreSQL, Prometheus, Elasticsearch. Для нашего случая SQLite подходит отлично — данные уже хранятся в ней.

Архитектура:

SQLite Database → Grafana (с плагином SQLite) → Дашборд с панелями

Установка Grafana:

# 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).

Подключение SQLite к Grafana:

Стандартный Grafana не поддерживает SQLite напрямую. Нужно использовать плагин:

  1. Установить плагин grafana-sqlite-datasource:bashsudo grafana-cli plugins install frser-sqlite-datasource sudo systemctl restart grafana-server
  2. В веб-интерфейсе Grafana: Configuration → Data Sources → Add data source → SQLite.
    • Указать путь к файлу БД: /path/to/osint.db

Создание панелей в Grafana:

Панель 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

Grafana преимущества:

  • Быстрая настройка, красивые графики "из коробки".
  • Автообновление данных.
  • Поддержка алертов (email, Telegram, Slack).

Grafana недостатки:

  • Ограниченная кастомизация (сложно добавить нестандартную логику).
  • Нужно устанавливать плагин для 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

Создание дашборда (app.py):

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-фреймворк с собственным веб-интерфейсом. Он автоматически собирает данные из множества источников и визуализирует их в виде графа связей.

Установка SpiderFoot:

# Docker (рекомендуемый способ)
docker run -p 5000:5000 spiderfoot/spiderfoot

Особенности SpiderFoot:

  • Графовое представление связей (домены, IP, email, хэши).
  • Автоматический сбор из 200+ модулей.
  • Встроенный дашборд с картами и таймлайнами.

требует времени на настройку, может быть избыточным для простых задач.

ВАРИАНТ 5: MALTEGO ДЛЯ ВИЗУАЛИЗАЦИИ СВЯЗЕЙ

Maltego — это инструмент для визуализации связей между сущностями (доменами, IP, личностями). В сочетании с трансформами он превращает собранные данные в интерактивный граф.

Как использовать:

  • Экспортировать данные из SQLite в CSV.
  • Импортировать в Maltego как "сущности" (Entities).
  • Использовать трансформы для дальнейшего обогащения.

СРАВНЕНИЕ ИНСТРУМЕНТОВ ДЛЯ ДАШБОРДОВ


Практический пример: использование дашборда для мониторинга

Сценарий: аналитик мониторит 5 доменов конкурентов.

  1. Ежедневный сбор → данные попадают в SQLite через автоматизированный конвейер.
  2. Дашборд (Dash) → отображает для каждого домена:
    • Количество поддоменов (график роста).
    • Новые поддомены за неделю (таблица).
    • Открытые порты и сервисы.
    • Истекающие сертификаты (алерт).
  3. Обнаружение: на дашборде появляется новый поддомен newapp.competitor.com. Аналитик видит, что это произошло сегодня. Через Shodan выясняется, что на нём открыт порт 443 с сертификатом от AWS. Вывод: конкурент запускает новый облачный сервис.
  4. Реакция: информация передаётся в стратегический отдел.

РЕКОМЕНДАЦИИ ПО ВЫБОРУ

Дашборд — это завершающий элемент системы технического OSINT, превращающий сырые данные в понятную и удобную для анализа информацию. Выбор инструмента зависит от задач: Grafana подходит для быстрого старта и оперативного мониторинга, Dash/Plotly — для глубокой кастомизации, Maltego — для визуализации связей. Интеграция дашборда с автоматизированным конвейером (из предыдущей статьи) создаёт полноценную систему непрерывной разведки, где данные собираются, обрабатываются и отображаются в реальном времени.