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), }