diff options
| -rw-r--r-- | abusectl/report.py | 32 | ||||
| -rw-r--r-- | tests/test_report.py | 20 |
2 files changed, 52 insertions, 0 deletions
diff --git a/abusectl/report.py b/abusectl/report.py index 45559a4..def7f3a 100644 --- a/abusectl/report.py +++ b/abusectl/report.py @@ -39,6 +39,38 @@ from pathlib import Path VERSION = "0.1.0" +# Every IOC type parse.py emits. Named rather than derived, so a new type +# arriving without a decision about which destinations accept it is a test +# failure rather than an indicator that quietly reaches MISP alone. +ALL_TYPES = ("ipv4", "ipv6", "domain", "url", "sha256") + + +# What each destination accepts, read from its own API documentation on +# 2026-09-10: +# +# misp MISP book, circl.lu/doc/misp/automation. Any attribute type. +# abusedb docs.abuseipdb.com, POST /api/v2/report: "a valid IPv4 or +# IPv6 address", and nothing else. +# urlhaus abuse.ch's own submit_url.py, POST urlhaus.abuse.ch/api/, +# whose submission array carries a url. The full docs are +# behind a login at auth.abuse.ch; re-verify the PAYLOAD there +# when submit is built. The accepted type is unambiguous. +# +# VirusTotal is deliberately absent: its only submission endpoints are +# POST /urls and file upload, so it accepts exactly what urlhaus accepts +# and takes no verdict with a submission. See backlog item 6. +# +# DO NOT EDIT THESE FROM MEMORY. init.PROVIDERS records what that costs: +# the first draft was written from memory and every range was wrong. A +# wrong "accepts" here means a row promising a submission the endpoint +# will refuse, or a missing row for something the vendor would have taken. +DESTINATIONS: dict[str, dict] = { + "misp": {"kind": "misp", "accepts": ALL_TYPES}, + "abusedb": {"kind": "api", "accepts": ("ipv4", "ipv6")}, + "urlhaus": {"kind": "api", "accepts": ("url",)}, +} + + # Hard wrap column, from the spec: "Plain text, hard-wrapped at 72 columns." # Abuse desks run ticketing systems that reflow or clip long lines, and a # clipped line is the defect this whole wrapping scheme exists to prevent. diff --git a/tests/test_report.py b/tests/test_report.py index c8c39f8..6d1f845 100644 --- a/tests/test_report.py +++ b/tests/test_report.py @@ -2029,3 +2029,23 @@ class PersonalisedSubject(unittest.TestCase): # the fix is a redaction rule with its own tests, not a passthrough # quietly turned into a filter. self.assertIn("alicejones", self._headers()["Subject"]) + + +class DestinationTable(unittest.TestCase): + def test_every_destination_names_the_types_it_accepts(self): + self.assertEqual(report.DESTINATIONS["abusedb"]["accepts"], + ("ipv4", "ipv6")) + self.assertEqual(report.DESTINATIONS["urlhaus"]["accepts"], ("url",)) + self.assertEqual(report.DESTINATIONS["misp"]["accepts"], report.ALL_TYPES) + + def test_misp_is_its_own_kind_and_the_vendors_are_api(self): + self.assertEqual(report.DESTINATIONS["misp"]["kind"], "misp") + self.assertEqual(report.DESTINATIONS["abusedb"]["kind"], "api") + self.assertEqual(report.DESTINATIONS["urlhaus"]["kind"], "api") + + def test_all_types_covers_what_parse_emits(self): + # The five type strings parse.py writes. A sixth arriving without a + # decision about which destinations accept it would silently reach + # MISP only, so this test names them rather than deriving them. + self.assertEqual(report.ALL_TYPES, + ("ipv4", "ipv6", "domain", "url", "sha256")) |
