Files
DMARC-Sentinel/app/homepage.py
T
2026-05-16 12:05:36 -03:00

262 lines
11 KiB
Python

from __future__ import annotations
from datetime import date, datetime, timedelta, timezone
from sqlalchemy import func, select
from sqlalchemy.orm import Session
from app.models import Alert, DailyStat, InboxStatus, LLMReport, Record, Report
def _pct(pass_count: int, total: int) -> str:
return f"{(pass_count / total * 100):.1f}%" if total else "0.0%"
def _as_utc(value: datetime | str | None) -> datetime | None:
if value is None:
return None
if isinstance(value, str):
try:
value = datetime.fromisoformat(value.replace("Z", "+00:00"))
except ValueError:
return None
if value.tzinfo is None:
return value.replace(tzinfo=timezone.utc)
return value
def report_timestamp(report: Report) -> datetime:
return _as_utc(report.date_end or report.date_begin or report.created_at) or datetime.now(timezone.utc)
def _report_date_expr():
return func.coalesce(Report.date_end, Report.date_begin, Report.created_at)
def _display_day(value: date) -> str:
return value.strftime("%d/%m/%Y")
def report_bounds(session: Session, domain: str | None = None) -> tuple[datetime | None, datetime | None]:
stmt = select(func.min(_report_date_expr()), func.max(_report_date_expr()))
if domain:
stmt = stmt.where(Report.domain == domain)
start, end = session.execute(stmt).one()
return _as_utc(start), _as_utc(end)
def resolve_date_range(
session: Session,
*,
period: str = "all",
domain: str | None = None,
date_from: str | None = None,
date_to: str | None = None,
) -> tuple[datetime | None, datetime | None, str]:
first, latest = report_bounds(session, domain)
if not latest:
return None, None, "No reports"
if period == "custom":
start = _as_utc(f"{date_from}T00:00:00+00:00") if date_from else first
end = _as_utc(f"{date_to}T23:59:59+00:00") if date_to else latest
return start, end, "Custom range"
if period == "24h":
return latest - timedelta(days=1), latest, "Latest 24h"
if period == "7d":
return latest - timedelta(days=7), latest, "Latest 7 days"
if period == "30d":
return latest - timedelta(days=30), latest, "Latest 30 days"
if period == "365d":
return latest - timedelta(days=365), latest, "Latest year"
return first, latest, "All imported reports"
def _range_filter(stmt, start: datetime | None, end: datetime | None):
expr = _report_date_expr()
if start:
stmt = stmt.where(expr >= start)
if end:
stmt = stmt.where(expr <= end)
return stmt
def latest_summary(session: Session, domain: str | None = None) -> str:
posture_stmt = select(LLMReport).where(LLMReport.report_type == "posture").order_by(LLMReport.created_at.desc(), LLMReport.period_end.desc())
if domain:
posture_stmt = posture_stmt.where(LLMReport.domain == domain)
else:
posture_stmt = posture_stmt.where(LLMReport.domain == "__all__")
report = session.scalar(posture_stmt.limit(1))
if not report:
stmt = select(LLMReport).where(LLMReport.report_type == "daily").order_by(LLMReport.period_end.desc(), LLMReport.created_at.desc())
if domain:
stmt = stmt.where(LLMReport.domain == domain)
else:
stmt = stmt.where(LLMReport.domain == "__all__")
report = session.scalar(stmt.limit(1))
if report:
return report.plain_text
if domain:
return "No domain posture digest has been generated yet."
return "No portfolio posture digest has been generated yet."
def homepage_summary(
session: Session,
*,
period: str = "all",
domain: str | None = None,
date_from: str | None = None,
date_to: str | None = None,
) -> dict:
start, end, scope_label = resolve_date_range(session, period=period, domain=domain, date_from=date_from, date_to=date_to)
domains = session.scalar(select(func.count(func.distinct(Report.domain)))) or 0
reports_stmt = select(func.count(Report.id))
records_stmt = select(Record).join(Report)
unknown_stmt = select(func.count(func.distinct(Record.source_ip))).join(Report).where(Record.is_known_sender.is_(False))
if domain:
reports_stmt = reports_stmt.where(Report.domain == domain)
records_stmt = records_stmt.where(Report.domain == domain)
unknown_stmt = unknown_stmt.where(Report.domain == domain)
reports_stmt = _range_filter(reports_stmt, start, end)
records_stmt = _range_filter(records_stmt, start, end)
unknown_stmt = _range_filter(unknown_stmt, start, end)
reports_in_range = session.scalar(reports_stmt) or 0
records = session.execute(records_stmt).scalars().all()
messages_in_range = sum(row.count for row in records)
pass_count = sum(row.count for row in records if row.dmarc_pass)
critical = session.scalar(select(func.count(Alert.id)).where(Alert.status == "open", Alert.severity == "critical")) or 0
warnings = session.scalar(select(func.count(Alert.id)).where(Alert.status == "open", Alert.severity == "warning")) or 0
unknown_sources = session.scalar(unknown_stmt) or 0
last_check = session.scalar(select(func.max(InboxStatus.last_success_at)))
return {
"status": "critical" if critical else "warning" if warnings else "ok",
"domains": domains,
"reports_today": reports_in_range,
"messages_today": messages_in_range,
"dmarc_pass_count": pass_count,
"dmarc_fail_count": messages_in_range - pass_count,
"dmarc_pass_rate": _pct(pass_count, messages_in_range),
"dmarc_pass_rate_value": round(pass_count / messages_in_range * 100, 1) if messages_in_range else None,
"critical_alerts": critical,
"warnings": warnings,
"unknown_sources": unknown_sources,
"last_check": last_check.isoformat() if last_check else None,
"report_day": None,
"scope_label": scope_label,
"scope_start": start.isoformat() if start else None,
"scope_end": end.isoformat() if end else None,
"summary": latest_summary(session, domain),
}
def domain_metrics(session: Session, domain: str) -> dict:
records = session.execute(select(Record).join(Report).where(Report.domain == domain)).scalars().all()
total = sum(row.count for row in records)
dmarc_pass = sum(row.count for row in records if row.dmarc_pass)
spf_aligned = sum(row.count for row in records if row.spf_aligned)
dkim_aligned = sum(row.count for row in records if row.dkim_aligned)
return {
"messages": total,
"dmarc_pass": dmarc_pass,
"dmarc_fail": total - dmarc_pass,
"pass_rate": _pct(dmarc_pass, total),
"spf_aligned": spf_aligned,
"spf_rate": _pct(spf_aligned, total),
"dkim_aligned": dkim_aligned,
"dkim_rate": _pct(dkim_aligned, total),
"unknown_sources": len({row.source_ip for row in records if not row.is_known_sender}),
}
def traffic_distribution(
session: Session,
*,
period: str = "all",
domain: str | None = None,
date_from: str | None = None,
date_to: str | None = None,
buckets: int | None = None,
) -> list[dict]:
start, now, _ = resolve_date_range(session, period=period, domain=domain, date_from=date_from, date_to=date_to)
if not start or not now:
return []
default_buckets = {"24h": 12, "7d": 7, "30d": 10, "365d": 12}.get(period, 14)
bucket_count = buckets or default_buckets
duration = (now - start).total_seconds()
bucket_seconds = max(1, duration / bucket_count)
rows = []
stmt = select(Record, Report).join(Report).where(_report_date_expr() >= start, _report_date_expr() <= now)
if domain:
stmt = stmt.where(Report.domain == domain)
for record, report in session.execute(stmt).all():
created = report_timestamp(report)
index = int((created - start).total_seconds() / bucket_seconds)
index = min(bucket_count - 1, max(0, index))
rows.append((index, record))
data = []
for i in range(bucket_count):
bucket_start = start + timedelta(seconds=bucket_seconds * i)
bucket_end = start + timedelta(seconds=bucket_seconds * (i + 1))
if i == bucket_count - 1:
bucket_end = now
start_day = bucket_start.date()
end_day = bucket_end.date()
if start_day == end_day:
label = _display_day(start_day)
else:
label = f"{_display_day(start_day)} to {_display_day(end_day)}"
data.append(
{
"label": label,
"date_from": start_day.isoformat(),
"date_to": end_day.isoformat(),
"valid": 0,
"failed": 0,
"total": 0,
}
)
for index, record in rows:
key = "valid" if record.dmarc_pass else "failed"
data[index][key] += record.count
data[index]["total"] += record.count
max_total = max([item["total"] for item in data] or [0])
for item in data:
item["height"] = round(item["total"] / max_total * 100) if max_total else 0
item["failed_height"] = round(item["failed"] / item["total"] * item["height"]) if item["total"] else 0
item["valid_height"] = max(0, item["height"] - item["failed_height"])
return data
def domain_homepage_summary(session: Session, domain: str) -> dict:
latest = _as_utc(session.scalar(select(func.max(func.coalesce(Report.date_end, Report.date_begin, Report.created_at))).where(Report.domain == domain)))
end = latest or datetime.now(timezone.utc)
since = end - timedelta(days=1)
records = session.execute(
select(Record)
.join(Report)
.where(Report.domain == domain, func.coalesce(Report.date_end, Report.date_begin, Report.created_at) >= since, func.coalesce(Report.date_end, Report.date_begin, Report.created_at) <= end)
).scalars().all()
total = sum(row.count for row in records)
passed = sum(row.count for row in records if row.dmarc_pass)
failed = total - passed
unknown = len({row.source_ip for row in records if not row.is_known_sender})
critical = session.scalar(
select(func.count(Alert.id)).where(Alert.status == "open", Alert.domain == domain, Alert.severity == "critical")
) or 0
warnings = session.scalar(
select(func.count(Alert.id)).where(Alert.status == "open", Alert.domain == domain, Alert.severity == "warning")
) or 0
return {
"status": "critical" if critical else "warning" if warnings else "ok",
"domain": domain,
"messages_24h": total,
"pass_rate": _pct(passed, total),
"failed": failed,
"unknown_sources": unknown,
"critical_alerts": critical,
"warnings": warnings,
"summary": latest_summary(session, domain),
}