feat: add forensic redaction controls
This commit is contained in:
@@ -12,6 +12,7 @@ from app.services.forensic_parser import (
|
||||
_message_part_payload,
|
||||
_payload_text,
|
||||
)
|
||||
from app.services.forensic_redaction import ForensicRedactionPolicy
|
||||
|
||||
|
||||
SAMPLE_FORENSIC_EMAIL = b"""\
|
||||
@@ -77,6 +78,28 @@ def test_parse_forensic_email_redacts_and_extracts_failure_fields():
|
||||
assert "original-message@example.com" not in parsed["original_message_id"]
|
||||
|
||||
|
||||
def test_parse_forensic_email_supports_stricter_redaction_policies():
|
||||
domain_only = ForensicParser.parse_bytes(
|
||||
SAMPLE_FORENSIC_EMAIL,
|
||||
redaction_policy=ForensicRedactionPolicy(mode="domain_only"),
|
||||
)
|
||||
strict = ForensicParser.parse_bytes(
|
||||
SAMPLE_FORENSIC_EMAIL,
|
||||
redaction_policy=ForensicRedactionPolicy(mode="strict"),
|
||||
)
|
||||
token_visible = ForensicParser.parse_bytes(
|
||||
SAMPLE_FORENSIC_EMAIL,
|
||||
redaction_policy=ForensicRedactionPolicy(redact_long_tokens=False),
|
||||
)
|
||||
|
||||
assert domain_only["original_mail_from"] == "***@example.com"
|
||||
assert "***@example.com" in domain_only["original_from"]
|
||||
assert domain_only["source_email"] == "DMARC Reporter <***@example.net>"
|
||||
assert strict["original_mail_from"] == "[redacted-email]"
|
||||
assert strict["source_email"] == "DMARC Reporter <[redacted-email]>"
|
||||
assert "abcdefghijklmnopqrstuvwxyz123456" in token_visible["original_subject"]
|
||||
|
||||
|
||||
def test_non_forensic_email_is_rejected():
|
||||
content = b"From: sender@example.com\r\nSubject: hello\r\n\r\nplain email"
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ from app.services.forensic_persistence import (
|
||||
forensic_report_to_dict,
|
||||
save_forensic_report,
|
||||
)
|
||||
from app.services.forensic_redaction import ForensicRedactionPolicy
|
||||
from app.tests.test_forensic_parser import SAMPLE_FORENSIC_EMAIL
|
||||
|
||||
|
||||
@@ -62,6 +63,36 @@ def test_list_and_detail_forensic_reports(authed_client):
|
||||
assert detail_response.json()["reported_domain"] == "example.com"
|
||||
|
||||
|
||||
def test_forensic_api_applies_configured_redaction_policy(authed_client, db_session):
|
||||
authed_client.post(
|
||||
"/api/v1/forensics/upload",
|
||||
files={"file": ("report.eml", SAMPLE_FORENSIC_EMAIL, "message/rfc822")},
|
||||
)
|
||||
authed_client.put("/api/v1/settings/forensics.redaction_mode", json={"value": "strict"})
|
||||
|
||||
list_response = authed_client.get("/api/v1/forensics?domain=example.com")
|
||||
|
||||
assert list_response.status_code == 200
|
||||
item = list_response.json()["reports"][0]
|
||||
assert item["original_mail_from"] == "[redacted-email]"
|
||||
assert item["source_email"] == "DMARC Reporter <[redacted-email]>"
|
||||
stored = db_session.query(ForensicReport).one()
|
||||
assert stored.original_mail_from == "al***@example.com"
|
||||
|
||||
|
||||
def test_upload_forensic_report_uses_configured_redaction_policy(authed_client, db_session):
|
||||
authed_client.put("/api/v1/settings/forensics.redaction_mode", json={"value": "domain_only"})
|
||||
|
||||
response = authed_client.post(
|
||||
"/api/v1/forensics/upload",
|
||||
files={"file": ("report.eml", SAMPLE_FORENSIC_EMAIL, "message/rfc822")},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
report = db_session.query(ForensicReport).one()
|
||||
assert report.original_mail_from == "***@example.com"
|
||||
|
||||
|
||||
def test_upload_forensic_report_rejects_aggregate_xml(authed_client):
|
||||
response = authed_client.post(
|
||||
"/api/v1/forensics/upload",
|
||||
@@ -156,6 +187,11 @@ def test_save_forensic_report_duplicate_and_invalid_domain_paths(db_session):
|
||||
assert forensic_report_exists(db_session, parsed["report_id"]) is True
|
||||
assert forensic_report_exists(db_session, "") is False
|
||||
assert forensic_report_to_dict(first)["feedback_headers"] == {"identity_alignment": "dkim"}
|
||||
strict = forensic_report_to_dict(
|
||||
first,
|
||||
redaction_policy=ForensicRedactionPolicy(mode="strict"),
|
||||
)
|
||||
assert strict["original_mail_from"] == "[redacted-email]"
|
||||
|
||||
first.feedback_headers = "{not-json"
|
||||
assert forensic_report_to_dict(first)["feedback_headers"] == {}
|
||||
|
||||
@@ -20,6 +20,7 @@ from unittest.mock import MagicMock, patch
|
||||
import pytest
|
||||
|
||||
from app.models.report import DMARCReport, ForensicReport
|
||||
from app.models.setting import Setting
|
||||
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
|
||||
@@ -465,6 +466,32 @@ class TestProcessMessage:
|
||||
assert db_session.query(DMARCReport).count() == 0
|
||||
assert db_session.query(ForensicReport).count() == 1
|
||||
|
||||
def test_forensic_report_uses_configured_redaction_policy(self, db_session):
|
||||
setting = (
|
||||
db_session.query(Setting).filter(Setting.key == "forensics.redaction_mode").first()
|
||||
)
|
||||
if setting is None:
|
||||
setting = Setting(
|
||||
key="forensics.redaction_mode",
|
||||
value_type="string",
|
||||
category="forensics",
|
||||
)
|
||||
db_session.add(setting)
|
||||
setting.value = "strict"
|
||||
db_session.commit()
|
||||
client = _make_client(db=db_session)
|
||||
stats = {"forensic_reports_found": 0, "duplicate_forensic_reports": 0, "errors": []}
|
||||
|
||||
count = client._process_forensic_message(
|
||||
SAMPLE_FORENSIC_EMAIL,
|
||||
stats,
|
||||
message_id="msg-forensic",
|
||||
)
|
||||
|
||||
assert count == 1
|
||||
report = db_session.query(ForensicReport).one()
|
||||
assert report.original_mail_from == "[redacted-email]"
|
||||
|
||||
def test_forensic_report_without_database_is_skipped(self):
|
||||
client = _make_client()
|
||||
stats = {"forensic_reports_found": 0, "duplicate_forensic_reports": 0, "errors": []}
|
||||
|
||||
@@ -18,6 +18,7 @@ from zipfile import ZipFile
|
||||
import pytest
|
||||
|
||||
from app.models.report import DMARCReport, ForensicReport
|
||||
from app.models.setting import Setting
|
||||
from app.services.imap_client import IMAPClient
|
||||
from app.services.report_store import ReportStore
|
||||
from app.tests.test_forensic_parser import SAMPLE_FORENSIC_EMAIL
|
||||
@@ -641,6 +642,27 @@ class TestProcessSingleEmail:
|
||||
assert db_session.query(ForensicReport).count() == 1
|
||||
assert ReportStore.get_instance().get_domains() == []
|
||||
|
||||
def test_forensic_report_uses_configured_redaction_policy(self, db_session):
|
||||
setting = (
|
||||
db_session.query(Setting).filter(Setting.key == "forensics.redaction_mode").first()
|
||||
)
|
||||
if setting is None:
|
||||
setting = Setting(
|
||||
key="forensics.redaction_mode",
|
||||
value_type="string",
|
||||
category="forensics",
|
||||
)
|
||||
db_session.add(setting)
|
||||
setting.value = "domain_only"
|
||||
db_session.commit()
|
||||
client = self._make_client(db=db_session)
|
||||
stats = {"forensic_reports_found": 0, "duplicate_forensic_reports": 0, "errors": []}
|
||||
|
||||
assert client._process_forensic_email(SAMPLE_FORENSIC_EMAIL, stats=stats, message_id="1")
|
||||
|
||||
report = db_session.query(ForensicReport).one()
|
||||
assert report.original_mail_from == "***@example.com"
|
||||
|
||||
def test_forensic_report_without_database_is_skipped(self):
|
||||
client = self._make_client()
|
||||
|
||||
|
||||
@@ -123,6 +123,8 @@ class TestSettingsAPI:
|
||||
assert "notifications.summary_weekday_utc" in keys
|
||||
assert "notifications.min_send_interval_minutes" in keys
|
||||
assert "notifications.redact_pii_enabled" in keys
|
||||
assert "forensics.redaction_mode" in keys
|
||||
assert "forensics.redact_long_tokens_enabled" in keys
|
||||
|
||||
def test_list_settings_filter_by_category(self, authed_client: TestClient):
|
||||
"""GET /api/v1/settings?category=dmarc returns only dmarc settings."""
|
||||
|
||||
Reference in New Issue
Block a user