Дашборд метрик ИБ на Python: собрать цифры из логов в одну страницу

Статья «Метрики ИБ для руководства» разбирает, какие восемь метрик стоит считать и что с ними делать. Здесь — конкретная реализация одной части этого процесса: скрипт, который сам достаёт цифры из обычных файлов (журнал SSH, список хостов, отчёт сканера) и складывает их в одну страницу, которую можно открыть в браузере или приложить к письму.
Статья опирается на логику направления «Отчётность и метрики» из методики зрелости Группы Сбер (источник); это методика для крупных компаний, мы используем её как ориентир и пересказываем своими словами. Скрипт проверен на тестовых данных, он не требует внешних библиотек и ничего не отправляет по сети.
Что должно получиться
| Уровень | Что это значит на практике |
|---|---|
| 2. Гигиенический | Метрики считаются автоматически из существующих источников данных, а не собираются вручную перед каждым отчётом |
Критерии сформулированы нами по мотивам методики, это ориентир, а не оценка соответствия.
Откуда берутся цифры
Скрипт не подключается ни к чему сам — он читает три файла, которые у вас, скорее всего, уже есть или легко получить существующими средствами:
auth.log— системный журнал с попытками входа по SSH (на Debian/Ubuntu обычно/var/log/auth.log).inventory.csv— результат инвентаризации сети.trivy.json— отчёт сканера образов Trivy в формате JSON (trivy image -f json).
Любой из трёх источников необязателен: если файла нет, скрипт просто не покажет соответствующую плитку, а не упадёт с ошибкой.
Скрипт
#!/usr/bin/env python3
"""Простой дашборд метрик ИБ: собирает цифры из нескольких источников и строит одну HTML-страницу.
Источники: auth.log (неудачные входы по SSH), inventory.csv (инвентаризация сети), trivy.json (уязвимости образов).
Ничего не отправляет наружу, не требует базы данных — один файл на входе каждого источника, один файл на выходе."""
import csv
import json
import re
import sys
from collections import Counter
from datetime import datetime
from pathlib import Path
def count_failed_logins(path):
if not Path(path).exists():
return None
pattern = re.compile(r"Failed password for (?:invalid user )?(\S+) from (\S+)")
by_ip = Counter()
total = 0
for line in Path(path).read_text(errors="ignore").splitlines():
m = pattern.search(line)
if m:
total += 1
by_ip[m.group(2)] += 1
top = by_ip.most_common(5)
return {"total": total, "top_ips": top}
def count_inventory(path):
if not Path(path).exists():
return None
hosts = set()
services = Counter()
with open(path, newline="") as f:
for row in csv.DictReader(f):
hosts.add(row["ip"])
services[row["service"]] += 1
return {"hosts": len(hosts), "services": services.most_common(10)}
def count_vulnerabilities(path):
if not Path(path).exists():
return None
data = json.loads(Path(path).read_text())
sev = Counter()
for result in data.get("Results", []):
for v in result.get("Vulnerabilities") or []:
sev[v["Severity"]] += 1
return dict(sev)
def render(auth, inventory, vulns, out_path):
rows = []
if auth is not None:
rows.append(f"<div class='tile'><div class='num'>{auth['total']}</div><div class='label'>неудачных входов по SSH</div></div>")
if inventory is not None:
rows.append(f"<div class='tile'><div class='num'>{inventory['hosts']}</div><div class='label'>отвечающих хостов в сети</div></div>")
if vulns is not None:
crit = vulns.get("CRITICAL", 0) + vulns.get("HIGH", 0)
rows.append(f"<div class='tile'><div class='num'>{crit}</div><div class='label'>уязвимостей HIGH/CRITICAL</div></div>")
top_ips_html = ""
if auth and auth["top_ips"]:
items = "".join(f"<li>{ip} — {n}</li>" for ip, n in auth["top_ips"])
top_ips_html = f"<h2>Топ источников неудачных входов</h2><ul>{items}</ul>"
html = f"""<!doctype html><html lang="ru"><head><meta charset="utf-8">
<title>Дашборд метрик ИБ — {datetime.now():%Y-%m-%d %H:%M}</title>
<style>
body{{font-family:sans-serif;background:#0f172a;color:#e2e8f0;margin:0;padding:24px}}
.tiles{{display:flex;gap:16px;flex-wrap:wrap}}
.tile{{background:#1e293b;border-radius:8px;padding:16px 24px;min-width:160px}}
.num{{font-size:32px;font-weight:bold}}
.label{{color:#94a3b8;font-size:14px}}
h1{{font-size:20px}}
</style></head><body>
<h1>Дашборд метрик ИБ — {datetime.now():%Y-%m-%d %H:%M}</h1>
<div class="tiles">{''.join(rows)}</div>
{top_ips_html}
</body></html>"""
Path(out_path).write_text(html)
if __name__ == "__main__":
auth = count_failed_logins("auth.log")
inventory = count_inventory("inventory.csv")
vulns = count_vulnerabilities("trivy.json")
render(auth, inventory, vulns, "dashboard.html")
print("готово: dashboard.html")
Запуск: python3 dashboard.py в каталоге с исходными файлами — на выходе dashboard.html. Требуется только стандартная библиотека Python 3, ничего ставить не нужно.
Результат проверки
На тестовых данных (8+3+2 неудачных входа с трёх адресов, три хоста в инвентаризации, 5 находок HIGH и 2 CRITICAL) скрипт построил страницу с тремя плитками: «13 неудачных входов по SSH», «3 отвечающих хоста в сети», «7 уязвимостей HIGH/CRITICAL» — и списком топ-адресов по числу неудачных входов, отсортированным по убыванию. При удалении файлов auth.log и trivy.json скрипт отработал без ошибок и показал только оставшуюся плитку по инвентаризации — поведение «отсутствующий источник пропускается» подтверждено.
Как использовать регулярно
- Разместите скрипт на сервере, где удобно собирать все три файла (или адаптируйте пути под реальное расположение логов и отчётов).
- Добавьте сбор исходных файлов в то же задание по расписанию, что запускает инвентаризацию и сканирование — так дашборд всегда будет свежим.
- Публикуйте
dashboard.htmlво внутренней сети (простой веб-сервер или общая папка) либо прикладывайте к регулярному отчёту. - Расширяйте список плиток под свои источники: формат скрипта — одна функция на источник данных, которая возвращает словарь с цифрами или
None, если источника нет.
Диагностика
| Симптом | Причина и что делать |
|---|---|
| Плитка не появляется, хотя файл есть | Проверьте путь и права на чтение файла из каталога, где запускается скрипт |
| Число неудачных входов занижено | Журнал ротировался (auth.log.1, сжатые файлы). Обрабатывайте все актуальные файлы ротации или собирайте через journalctl |
| В топ-адресах постоянно один и тот же IP | Это не ошибка скрипта, а реальный сигнал: разберитесь, заблокирован ли адрес на файрволе или fail2ban |
trivy.json не читается |
Файл не в формате JSON сканирования образов (например, сохранён вывод в текстовом виде). Формируйте отчёт с ключом -f json |
Ограничения
Это заготовка, а не готовый продукт мониторинга: нет хранения истории (каждый запуск перезаписывает файл), нет уведомлений при превышении порога, нет доступа по паролю к готовой странице. Для немногочисленных цифр на одной странице этого достаточно; при росте числа метрик и источников стоит перейти на полноценную систему мониторинга с хранением истории: материалы по мониторингу.
Где это в модели зрелости
Материал относится к направлению «Отчётность и метрики» (материалы направления, карта зрелости). Уровень 2 требует, чтобы отчётность строилась на фактических данных и не отнимала много ручного труда: автоматический сбор показателей из уже существующих источников даёт это без выделенной системы отчётности.
Как проверить
- Скрипт запускается по расписанию и формирует актуальную страницу без ручного вмешательства.
- Отсутствие одного из источников не приводит к ошибке, страница строится по оставшимся.
- Цифры на странице сверены с источником хотя бы один раз вручную, чтобы исключить ошибку подсчёта.
- Страница доступна тем, кому она нужна (внутренняя сеть или регулярная рассылка), без открытия наружу.
Источники
- Методика оценки уровня зрелости кибербезопасности компании, Группа Сбер: sberbank.ru.


















