fix: harden forensic report ingestion

This commit is contained in:
Christian Krakau-Louis
2026-05-23 13:02:01 +02:00
parent 908d4cd2cd
commit 817270b1fb
8 changed files with 89 additions and 23 deletions
+12
View File
@@ -92,6 +92,18 @@ def test_parse_forensic_email_handles_invalid_dates():
assert parsed["arrival_date"] is None
def test_parse_forensic_email_normalizes_offset_dates_to_utc():
content = SAMPLE_FORENSIC_EMAIL.replace(
b"Arrival-Date: Fri, 22 May 2026 10:15:00 +0000",
b"Arrival-Date: Fri, 22 May 2026 12:15:00 +0200",
)
parsed = ForensicParser.parse_bytes(content)
assert parsed["arrival_date"].hour == 10
assert parsed["arrival_date"].tzinfo is None
def test_parse_forensic_email_falls_back_to_dkim_domain_and_content_hash():
content = SAMPLE_FORENSIC_EMAIL.replace(b"Message-ID: <report-1@example.net>\n", b"")
content = content.replace(b"Reported-Domain: example.com\n", b"DKIM-Domain: fallback.test\n")
+14
View File
@@ -1,3 +1,5 @@
import pytest
from app.models.report import ForensicReport
from app.services.forensic_parser import ForensicParser
from app.services.forensic_persistence import (
@@ -19,6 +21,10 @@ def test_upload_forensic_report_persists_redacted_metadata(authed_client, db_ses
assert data["success"] is True
assert data["domain"] == "example.com"
assert db_session.query(ForensicReport).count() == 1
report = db_session.query(ForensicReport).one()
assert report.original_mail_from == "al***@example.com"
assert "original-message@example.com" not in report.original_message_id
assert not report.original_message_id.startswith("<")
def test_upload_forensic_report_rejects_duplicates(authed_client):
@@ -99,6 +105,14 @@ def test_save_forensic_report_duplicate_and_invalid_domain_paths(db_session):
assert forensic_report_exists(db_session, "") is False
assert forensic_report_to_dict(first)["feedback_headers"] == {"identity_alignment": "dkim"}
first.feedback_headers = "{not-json"
assert forensic_report_to_dict(first)["feedback_headers"] == {}
missing_id = dict(parsed)
missing_id["report_id"] = " "
with pytest.raises(ValueError, match="report_id"):
save_forensic_report(db_session, missing_id)
invalid = dict(parsed)
invalid["report_id"] = "ruf-invalid-domain"
invalid["reported_domain"] = "bad domain"
+17 -4
View File
@@ -20,7 +20,7 @@ from unittest.mock import MagicMock, patch
import pytest
from app.models.report import DMARCReport, ForensicReport
from app.services.gmail_client import GmailClient
from app.services.gmail_client import GmailClient, RETRYABLE_MESSAGE_FAILURE
from app.services.report_store import ReportStore
from app.tests.test_data import SAMPLE_XML
from app.tests.test_forensic_parser import SAMPLE_FORENSIC_EMAIL
@@ -475,7 +475,7 @@ class TestProcessMessage:
message_id="msg-forensic",
)
assert imported is False
assert imported == RETRYABLE_MESSAGE_FAILURE
assert stats["details"][0]["reason"] == "forensic_report_requires_database"
def test_duplicate_forensic_report_is_skipped(self, db_session):
@@ -489,7 +489,7 @@ class TestProcessMessage:
message_id="msg-1",
)
assert imported is False
assert imported == 0
assert stats["duplicate_forensic_reports"] == 1
assert stats["details"][1]["status"] == "duplicate"
@@ -503,7 +503,7 @@ class TestProcessMessage:
message_id="bad",
)
assert imported is False
assert imported == RETRYABLE_MESSAGE_FAILURE
assert stats["errors"]
assert stats["details"][0]["reason"] == "forensic_parse_failed"
@@ -743,6 +743,19 @@ class TestFetchReports:
assert "id1" in result["new_ingested_ids"]
assert "id2" in result["new_ingested_ids"]
def test_retryable_processing_failure_is_not_marked_ingested(self):
client = _make_client()
mock_service = MagicMock()
with (
patch.object(client, "_build_service", return_value=mock_service),
patch.object(client, "_list_dmarc_message_ids", return_value=["id1"]),
patch.object(client, "_process_message", return_value=RETRYABLE_MESSAGE_FAILURE),
):
result = client.fetch_reports()
assert result["new_ingested_ids"] == []
assert client.already_ingested_ids == []
def test_reports_new_domains(self):
"""fetch_reports should report domains that appear after ingestion."""
from app.services.report_store import ReportStore
+1
View File
@@ -618,6 +618,7 @@ class TestProcessSingleEmail:
assert stats["details"][0]["status"] == "imported"
def test_processes_forensic_report_without_aggregate_count(self, db_session):
ReportStore.get_instance().clear()
client = self._make_client(db=db_session)
mock_mail = MagicMock()
mock_mail.fetch.return_value = ("OK", [(b"1", SAMPLE_FORENSIC_EMAIL)])