diff options
Diffstat (limited to 'tests/test_report.py')
| -rw-r--r-- | tests/test_report.py | 208 |
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() |
