aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/test_report.py208
1 files changed, 208 insertions, 0 deletions
diff --git a/tests/test_report.py b/tests/test_report.py
index 5649b0b..b572b54 100644
--- a/tests/test_report.py
+++ b/tests/test_report.py
@@ -306,5 +306,213 @@ class Grouping(unittest.TestCase):
self.assertEqual(crowded[2]["id"], alone[0]["id"])
+class Unreportable(unittest.TestCase):
+ def test_an_ioc_with_no_desk_is_listed_with_its_reason(self):
+ contacts = [
+ {"iocs": ["ioc-1"], "query": "198.51.100.7",
+ "abuse": ["abuse@host.invalid"], "source": "rdap"},
+ {"iocs": ["ioc-2", "ioc-3"], "query": "example.invalid",
+ "abuse": [], "source": "rdap",
+ "error": "no abuse role published"},
+ ]
+ self.assertEqual(
+ report.unreportable(contacts),
+ [
+ {"ioc": "ioc-2", "reason": "no abuse role published"},
+ {"ioc": "ioc-3", "reason": "no abuse role published"},
+ ],
+ )
+
+ def test_a_missing_reason_still_produces_an_entry(self):
+ contacts = [{"iocs": ["ioc-9"], "query": "x.invalid", "abuse": [],
+ "source": "rdap"}]
+ self.assertEqual(
+ report.unreportable(contacts),
+ [{"ioc": "ioc-9", "reason": "no abuse address resolved"}],
+ )
+
+ def test_an_empty_reason_does_not_read_as_no_reason(self):
+ """`error: ""` must not be reported as the literal empty string.
+
+ A contact entry is written by contacts.resolve, but a manifest is
+ a file on disk that a user edits during review. An empty reason
+ renders as a blank cell in the report the user reads, which says
+ nothing at all; the default at least says what happened.
+ """
+ contacts = [{"iocs": ["ioc-9"], "query": "x.invalid", "abuse": [],
+ "source": "rdap", "error": ""}]
+ self.assertEqual(
+ report.unreportable(contacts),
+ [{"ioc": "ioc-9", "reason": "no abuse address resolved"}],
+ )
+
+ def test_nothing_unreportable_is_an_empty_list_not_an_error(self):
+ contacts = [{"iocs": ["ioc-1"], "query": "198.51.100.7",
+ "abuse": ["abuse@host.invalid"], "source": "rdap"}]
+ self.assertEqual(report.unreportable(contacts), [])
+
+ def test_an_ioc_that_reached_a_desk_elsewhere_is_not_unreportable(self):
+ """Hosts fold, so one IOC can sit in a resolved and an unresolved
+ contact at once. It IS reportable, and listing it says otherwise.
+
+ The plan's implementation listed it regardless, which puts an
+ indicator in both the destination list and the "no desk found"
+ list of one manifest. A reviewer reading the second acts on an
+ indicator that is already on its way to a desk, and the whole
+ point of the array is that it can be trusted without diffing.
+ """
+ contacts = [
+ {"iocs": ["ioc-1", "ioc-2"], "query": "198.51.100.7",
+ "abuse": ["abuse@host.invalid"], "source": "rdap"},
+ {"iocs": ["ioc-2", "ioc-3"], "query": "example.invalid",
+ "abuse": [], "source": "rdap",
+ "error": "no abuse role published"},
+ ]
+ self.assertEqual(
+ report.unreportable(contacts),
+ [{"ioc": "ioc-3", "reason": "no abuse role published"}],
+ )
+
+ def test_one_ioc_unresolved_twice_is_listed_once(self):
+ """Two contacts, both unresolved, one shared indicator.
+
+ A duplicate row is a second line in the report about one
+ indicator, and the reasons may differ, so which one wins has to
+ be decided rather than left to whichever contact came last.
+ First reason seen wins, matching the first-seen ordering the
+ destinations use.
+ """
+ contacts = [
+ {"iocs": ["ioc-1"], "query": "example.invalid", "abuse": [],
+ "source": "rdap", "error": "no abuse role published"},
+ {"iocs": ["ioc-1"], "query": "198.51.100.7", "abuse": [],
+ "source": "rdap", "error": "no rdap server for this tld, "
+ "or no answer"},
+ ]
+ self.assertEqual(
+ report.unreportable(contacts),
+ [{"ioc": "ioc-1", "reason": "no abuse role published"}],
+ )
+
+ def test_the_contacts_passed_in_are_not_modified(self):
+ contacts = [
+ {"iocs": ["ioc-1"], "query": "example.invalid", "abuse": [],
+ "source": "rdap", "error": "no abuse role published"},
+ {"iocs": ["ioc-2"], "query": "198.51.100.7",
+ "abuse": ["abuse@host.invalid"], "source": "rdap"},
+ ]
+ before = copy.deepcopy(contacts)
+ report.unreportable(contacts)
+ self.assertEqual(contacts, before)
+
+
+class MalformedAddresses(unittest.TestCase):
+ """An abuse "address" with no @ cannot be mailed.
+
+ RDAP jCard data is third-party and occasionally malformed, and a
+ destination built from such a value carries an unsendable target with
+ status "pending". That is the failure mode the unreportable array
+ exists to prevent: the indicator appears reportable, no desk ever
+ receives it, and nothing in the manifest says so.
+ """
+
+ def test_a_target_with_no_at_creates_no_destination(self):
+ contacts = [
+ {"iocs": ["ioc-1"], "query": "example.invalid",
+ "abuse": ["not-an-address"], "source": "rdap"},
+ {"iocs": ["ioc-2"], "query": "198.51.100.7",
+ "abuse": ["abuse@host.invalid"], "source": "rdap"},
+ ]
+ destinations = report.email_destinations(contacts)
+ self.assertEqual([d["target"] for d in destinations],
+ ["abuse@host.invalid"])
+ self.assertEqual(destinations[0]["iocs"], ["ioc-2"])
+
+ def test_an_ioc_whose_only_address_is_malformed_is_unreportable(self):
+ contacts = [
+ {"iocs": ["ioc-1"], "query": "example.invalid",
+ "abuse": ["not-an-address"], "source": "rdap"},
+ ]
+ self.assertEqual(
+ report.unreportable(contacts),
+ [{"ioc": "ioc-1",
+ "reason": "no usable abuse address published"}],
+ )
+
+ def test_a_usable_address_beside_a_malformed_one_still_reports(self):
+ """The good half of a jCard must survive the bad half.
+
+ Discarding the contact wholesale would lose a real desk over a
+ neighbouring malformed row.
+ """
+ contacts = [
+ {"iocs": ["ioc-1"], "query": "example.invalid",
+ "abuse": ["not-an-address", "abuse@host.invalid"],
+ "source": "rdap"},
+ ]
+ destinations = report.email_destinations(contacts)
+ self.assertEqual([d["target"] for d in destinations],
+ ["abuse@host.invalid"])
+ self.assertEqual(report.unreportable(contacts), [])
+
+ def test_an_addresss_own_error_is_not_overwritten_by_the_default(self):
+ """A contact that has both a reason and a malformed address.
+
+ The contact's own error says more than "no usable address", so it
+ wins; the default is only for a contact that offered no reason.
+ """
+ contacts = [
+ {"iocs": ["ioc-1"], "query": "example.invalid",
+ "abuse": ["not-an-address"], "source": "rdap",
+ "error": "no abuse role published"},
+ ]
+ self.assertEqual(
+ report.unreportable(contacts),
+ [{"ioc": "ioc-1", "reason": "no abuse role published"}],
+ )
+
+ def test_the_two_lists_partition_every_indicator(self):
+ """The invariant the pair is for: each IOC is in exactly one.
+
+ Every other test here pins one side. This pins the relationship,
+ which is what a reviewer actually relies on: an indicator missing
+ from both is silently unreported, and one in both is reported and
+ also flagged as unreported. Both failures come from the two
+ functions disagreeing about what counts as a desk, so they are
+ asserted against one input that exercises every branch.
+ """
+ contacts = [
+ {"iocs": ["ioc-1", "ioc-2"], "query": "198.51.100.7",
+ "abuse": ["Abuse@Host.Invalid"], "source": "rdap"},
+ {"iocs": ["ioc-2", "ioc-3"], "query": "example.invalid",
+ "abuse": [], "source": "rdap",
+ "error": "no abuse role published"},
+ {"iocs": ["ioc-4"], "query": "other.invalid",
+ "abuse": ["not-an-address"], "source": "rdap"},
+ {"iocs": ["ioc-5"], "query": "mixed.invalid",
+ "abuse": ["broken", "abuse@host.invalid"], "source": "rdap"},
+ ]
+ destinations = report.email_destinations(contacts)
+ reported = {i for d in destinations for i in d["iocs"]}
+ flagged = {e["ioc"] for e in report.unreportable(contacts)}
+ every = {i for c in contacts for i in c["iocs"]}
+
+ self.assertEqual(reported & flagged, set())
+ self.assertEqual(reported | flagged, every)
+ self.assertEqual(reported, {"ioc-1", "ioc-2", "ioc-5"})
+ self.assertEqual(flagged, {"ioc-3", "ioc-4"})
+
+ def test_an_empty_or_whitespace_target_is_not_a_desk(self):
+ for value in ("", " ", "@host.invalid", "abuse@"):
+ with self.subTest(value=value):
+ contacts = [{"iocs": ["ioc-1"], "query": "example.invalid",
+ "abuse": [value], "source": "rdap"}]
+ self.assertEqual(report.email_destinations(contacts), [])
+ self.assertEqual(
+ report.unreportable(contacts),
+ [{"ioc": "ioc-1",
+ "reason": "no usable abuse address published"}])
+
+
if __name__ == "__main__":
unittest.main()