143 lines
5.1 KiB
Python
143 lines
5.1 KiB
Python
import json
|
|
from typing import Any, Dict, Optional
|
|
|
|
from sqlalchemy.exc import IntegrityError
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.models.domain import Domain
|
|
from app.models.report import ForensicReport
|
|
from app.services.forensic_redaction import ForensicRedactionPolicy, redact_forensic_value
|
|
from app.utils.domain_validator import DomainValidationError, validate_domain
|
|
|
|
|
|
def forensic_report_exists(db: Session, report_id: str) -> bool:
|
|
"""Return True when a forensic report ID is already persisted."""
|
|
if not str(report_id or "").strip():
|
|
return False
|
|
return (
|
|
db.query(ForensicReport.id).filter(ForensicReport.report_id == report_id).first()
|
|
is not None
|
|
)
|
|
|
|
|
|
def _domain_for_report(db: Session, domain_name: Optional[str]) -> Optional[Domain]:
|
|
if not domain_name:
|
|
return None
|
|
normalized = domain_name.lower().strip(".")
|
|
is_valid, _, error_code = validate_domain(normalized, check_dns=False)
|
|
if not is_valid and error_code != DomainValidationError.DNS_RESOLUTION_FAILED:
|
|
return None
|
|
|
|
domain = db.query(Domain).filter(Domain.name == normalized).first()
|
|
if domain is None:
|
|
domain = Domain(name=normalized)
|
|
db.add(domain)
|
|
db.flush()
|
|
return domain
|
|
|
|
|
|
def save_forensic_report(db: Session, report: Dict[str, Any]) -> tuple[ForensicReport, bool]:
|
|
"""Persist a parsed forensic report.
|
|
|
|
Returns ``(row, created)``. The caller owns the transaction and should
|
|
commit after related work has completed.
|
|
"""
|
|
report_id = str(report.get("report_id") or "").strip()
|
|
if not report_id:
|
|
raise ValueError("Forensic report_id is required")
|
|
|
|
existing = db.query(ForensicReport).filter(ForensicReport.report_id == report_id).first()
|
|
if existing is not None:
|
|
return existing, False
|
|
|
|
domain = _domain_for_report(db, report.get("reported_domain"))
|
|
feedback_headers = report.get("feedback_headers")
|
|
if isinstance(feedback_headers, dict):
|
|
feedback_headers = json.dumps(feedback_headers, sort_keys=True)
|
|
|
|
row = ForensicReport(
|
|
domain_id=domain.id if domain else None,
|
|
report_id=report_id,
|
|
source_email=report.get("source_email"),
|
|
feedback_type=report.get("feedback_type"),
|
|
user_agent=report.get("user_agent"),
|
|
version=report.get("version"),
|
|
reported_domain=report.get("reported_domain"),
|
|
source_ip=report.get("source_ip"),
|
|
auth_failure=report.get("auth_failure"),
|
|
delivery_result=report.get("delivery_result"),
|
|
arrival_date=report.get("arrival_date"),
|
|
authentication_results=report.get("authentication_results"),
|
|
original_mail_from=report.get("original_mail_from"),
|
|
original_from=report.get("original_from"),
|
|
original_to=report.get("original_to"),
|
|
original_subject=report.get("original_subject"),
|
|
original_message_id=report.get("original_message_id"),
|
|
original_date=report.get("original_date"),
|
|
feedback_headers=feedback_headers,
|
|
)
|
|
db.add(row)
|
|
try:
|
|
db.flush()
|
|
except IntegrityError:
|
|
db.rollback()
|
|
existing = db.query(ForensicReport).filter(ForensicReport.report_id == report_id).first()
|
|
if existing is not None:
|
|
return existing, False
|
|
raise
|
|
return row, True
|
|
|
|
|
|
_REDACTABLE_RESPONSE_FIELDS = {
|
|
"source_email",
|
|
"user_agent",
|
|
"authentication_results",
|
|
"original_mail_from",
|
|
"original_from",
|
|
"original_to",
|
|
"original_subject",
|
|
"feedback_headers",
|
|
}
|
|
|
|
|
|
def forensic_report_to_dict(
|
|
row: ForensicReport,
|
|
*,
|
|
redaction_policy: Optional[ForensicRedactionPolicy] = None,
|
|
) -> Dict[str, Any]:
|
|
"""Convert a forensic report row to an API-safe dictionary."""
|
|
feedback_headers = {}
|
|
if row.feedback_headers:
|
|
try:
|
|
feedback_headers = json.loads(row.feedback_headers)
|
|
except (json.JSONDecodeError, TypeError):
|
|
feedback_headers = {}
|
|
|
|
result = {
|
|
"id": row.id,
|
|
"report_id": row.report_id,
|
|
"domain": row.domain.name if row.domain else row.reported_domain,
|
|
"reported_domain": row.reported_domain,
|
|
"source_email": row.source_email,
|
|
"feedback_type": row.feedback_type,
|
|
"user_agent": row.user_agent,
|
|
"version": row.version,
|
|
"source_ip": row.source_ip,
|
|
"auth_failure": row.auth_failure,
|
|
"delivery_result": row.delivery_result,
|
|
"arrival_date": row.arrival_date.isoformat() if row.arrival_date else None,
|
|
"authentication_results": row.authentication_results,
|
|
"original_mail_from": row.original_mail_from,
|
|
"original_from": row.original_from,
|
|
"original_to": row.original_to,
|
|
"original_subject": row.original_subject,
|
|
"original_message_id": row.original_message_id,
|
|
"original_date": row.original_date,
|
|
"feedback_headers": feedback_headers,
|
|
"processed_at": row.processed_at.isoformat() if row.processed_at else None,
|
|
}
|
|
if redaction_policy is not None:
|
|
for field in _REDACTABLE_RESPONSE_FIELDS:
|
|
result[field] = redact_forensic_value(result.get(field), redaction_policy)
|
|
return result
|