aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--abusectl/report.py32
-rw-r--r--tests/test_report.py20
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"))