feat: persist imported DMARC reports

This commit is contained in:
Christian Krakau-Louis
2026-05-22 19:38:03 +02:00
parent 1fdfa016ed
commit b9041012de
14 changed files with 492 additions and 82 deletions
+236
View File
@@ -0,0 +1,236 @@
import json
from datetime import datetime
from typing import Any, Dict, List, Optional
from sqlalchemy.orm import Session, selectinload
from app.models.domain import Domain
from app.models.report import DMARCReport, ReportRecord
from app.services.report_store import ReportStore
def _parse_timestamp(value: Any) -> int:
"""Return a Unix timestamp from an int-like or ISO date value."""
if value in (None, ""):
return 0
if isinstance(value, (int, float)):
return int(value)
try:
return int(value)
except (TypeError, ValueError):
pass
try:
return int(datetime.fromisoformat(str(value)).timestamp())
except (TypeError, ValueError):
return 0
def _iso_from_timestamp(value: int) -> str:
if not value:
return ""
return datetime.fromtimestamp(value).isoformat()
def _loads_json_list(value: Optional[str]) -> Optional[List[Dict[str, Any]]]:
if not value:
return None
try:
decoded = json.loads(value)
except (json.JSONDecodeError, TypeError):
return None
return decoded if isinstance(decoded, list) else None
def _policy_parts(report: Dict[str, Any]) -> Dict[str, Any]:
policy = report.get("policy") or {}
if isinstance(policy, str):
return {"p": policy, "sp": "", "pct": "100"}
if not isinstance(policy, dict):
return {"p": "none", "sp": "", "pct": "100"}
return {
"p": policy.get("p", "none"),
"sp": policy.get("sp", ""),
"pct": str(policy.get("pct", "100")),
"adkim": policy.get("adkim") or report.get("adkim"),
"aspf": policy.get("aspf") or report.get("aspf"),
}
def report_exists(db: Session, domain_name: str, report_id: str) -> bool:
"""Return True when the domain/report ID pair is already persisted."""
if not report_id:
return False
return (
db.query(DMARCReport.id)
.join(Domain, DMARCReport.domain_id == Domain.id)
.filter(Domain.name == domain_name, DMARCReport.report_id == report_id)
.first()
is not None
)
def save_parsed_report(db: Session, report: Dict[str, Any]) -> tuple[DMARCReport, bool]:
"""Persist a parsed DMARC report and its records.
Returns ``(row, created)``. The caller owns the transaction and should
commit after all related work has completed.
"""
domain_name = report.get("domain") or "unknown"
report_id = report.get("report_id") or ""
policy = _policy_parts(report)
domain = db.query(Domain).filter(Domain.name == domain_name).first()
if domain is None:
domain = Domain(name=domain_name, dmarc_policy=policy["p"])
db.add(domain)
db.flush()
elif policy.get("p"):
domain.dmarc_policy = policy["p"]
existing = (
db.query(DMARCReport)
.filter(DMARCReport.domain_id == domain.id, DMARCReport.report_id == report_id)
.first()
)
if existing is not None:
return existing, False
begin_ts = _parse_timestamp(report.get("begin_timestamp") or report.get("begin_date"))
end_ts = _parse_timestamp(report.get("end_timestamp") or report.get("end_date"))
pct = _parse_timestamp(policy.get("pct")) or 100
db_report = DMARCReport(
domain_id=domain.id,
report_id=report_id,
org_name=report.get("org_name") or "",
begin_date=begin_ts,
end_date=end_ts,
source_email=report.get("email") or report.get("source_email"),
policy=policy["p"],
subdomain_policy=policy.get("sp") or None,
adkim=policy.get("adkim") or None,
aspf=policy.get("aspf") or None,
percentage=pct,
)
db.add(db_report)
db.flush()
for record in report.get("records", []):
db.add(
ReportRecord(
report_id=db_report.id,
source_ip=record.get("source_ip") or "unknown",
count=int(record.get("count") or 0),
disposition=record.get("disposition") or "none",
dkim=record.get("dkim_result") or record.get("dkim") or "unknown",
spf=record.get("spf_result") or record.get("spf") or "unknown",
header_from=record.get("header_from"),
envelope_from=record.get("envelope_from"),
dkim_auth_details=(
json.dumps(record.get("dkim")) if isinstance(record.get("dkim"), list) else None
),
spf_auth_details=(
json.dumps(record.get("spf")) if isinstance(record.get("spf"), list) else None
),
)
)
return db_report, True
def persisted_report_to_dict(report: DMARCReport) -> Dict[str, Any]:
"""Convert persisted report rows into the parsed-report shape used by the UI."""
records: List[Dict[str, Any]] = []
total_count = 0
passed_count = 0
for record in report.records:
count = int(record.count or 0)
dkim_result = record.dkim or "unknown"
spf_result = record.spf or "unknown"
total_count += count
if dkim_result == "pass" or spf_result == "pass":
passed_count += count
records.append(
{
"source_ip": record.source_ip,
"count": count,
"disposition": record.disposition or "none",
"dkim_result": dkim_result,
"spf_result": spf_result,
"header_from": record.header_from or "",
"dkim": _loads_json_list(record.dkim_auth_details),
"spf": _loads_json_list(record.spf_auth_details),
}
)
failed_count = total_count - passed_count
pass_rate = round(passed_count / total_count * 100, 1) if total_count > 0 else 0.0
return {
"domain": report.domain.name if report.domain else "unknown",
"report_id": report.report_id,
"org_name": report.org_name,
"email": report.source_email or "",
"begin_date": _iso_from_timestamp(report.begin_date),
"end_date": _iso_from_timestamp(report.end_date),
"begin_timestamp": report.begin_date,
"end_timestamp": report.end_date,
"policy": {
"p": report.policy or "none",
"sp": report.subdomain_policy or "",
"pct": str(report.percentage or 100),
},
"records": records,
"summary": {
"total_count": total_count,
"passed_count": passed_count,
"failed_count": failed_count,
"pass_rate": pass_rate,
},
}
def hydrate_report_store_from_db(db: Session, store: ReportStore | None = None) -> int:
"""Load persisted reports into ReportStore when the database has report rows."""
report_count = db.query(DMARCReport.id).count()
if report_count == 0:
return 0
store = store or ReportStore.get_instance()
store.clear()
reports = (
db.query(DMARCReport)
.options(
selectinload(DMARCReport.domain),
selectinload(DMARCReport.records),
)
.order_by(DMARCReport.end_date.desc())
.all()
)
for report in reports:
store.add_report(persisted_report_to_dict(report))
return len(reports)
def delete_persisted_report(db: Session, domain_name: str, report_id: str) -> bool:
"""Delete a persisted report by domain/report ID."""
report = (
db.query(DMARCReport)
.join(Domain, DMARCReport.domain_id == Domain.id)
.filter(Domain.name == domain_name, DMARCReport.report_id == report_id)
.first()
)
if report is None:
return False
db.delete(report)
return True
def delete_persisted_domain(db: Session, domain_name: str) -> bool:
"""Delete a domain row and cascaded report data."""
domain = db.query(Domain).filter(Domain.name == domain_name).first()
if domain is None:
return False
db.delete(domain)
return True