aboutsummaryrefslogtreecommitdiffstats
path: root/tests/test_report.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/test_report.py')
-rw-r--r--tests/test_report.py41
1 files changed, 40 insertions, 1 deletions
diff --git a/tests/test_report.py b/tests/test_report.py
index 9159dc7..9a19d00 100644
--- a/tests/test_report.py
+++ b/tests/test_report.py
@@ -8,7 +8,7 @@ import tempfile
import unittest
from pathlib import Path
-from abusectl import report
+from abusectl import parse, report
class Grouping(unittest.TestCase):
@@ -1958,3 +1958,42 @@ class Writing(unittest.TestCase):
if __name__ == "__main__":
unittest.main()
+
+
+class PersonalisedSubject(unittest.TestCase):
+ """The documented limit, pinned so a later change has to face it.
+
+ A phishing kit personalises the Subject with the recipient's local part.
+ The whitelist governs WHICH headers travel, never what is inside one, so
+ that local part is published. The spec accepts this deliberately: Subject
+ is what lets a desk recognise a campaign, and filtering free text is the
+ judgement-shaped problem AGENTS.md names as the origin of every leak this
+ project has had.
+
+ Measured, not theoretical: a sweep of 92 real messages found 14 carrying
+ the recipient's local part in the Subject, and none in the From display
+ name. This test exists so that number cannot change silently.
+ """
+
+ def _headers(self):
+ raw = (Path(__file__).parent
+ / "fixtures" / "personalised-subject.eml").read_bytes()
+ return dict(parse.report_headers(raw, trusted=["192.0.2.0/24"]))
+
+ def test_the_envelope_recipient_is_cut_from_the_received_line(self):
+ # The protection that DOES hold: our own relay wrote the "for"
+ # clause, and it is cut before the line is ever stored.
+ received = self._headers()["Received"]
+ self.assertNotIn("alicejones", received)
+ self.assertNotIn("@example.org", received)
+
+ def test_no_published_header_carries_the_full_recipient_address(self):
+ published = " ".join(self._headers().values())
+ self.assertNotIn("alicejones@example.org", published)
+
+ def test_the_subject_still_carries_the_local_part(self):
+ # Deliberate. If this ever fails because Subject was filtered, the
+ # spec's "considered and not built" section is what to read first:
+ # the fix is a redaction rule with its own tests, not a passthrough
+ # quietly turned into a filter.
+ self.assertIn("alicejones", self._headers()["Subject"])