diff options
| author | Danilo M. <danix@danix.xyz> | 2026-09-09 09:25:36 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-09-09 09:25:36 +0200 |
| commit | 96beac190b8444fc5b7629daebbf4a67f5ab11e2 (patch) | |
| tree | e83249d544e363bfc708095ad0441da57ef02a43 /tests/test_contacts.py | |
| parent | 3eba0ecca47435f04e1558c5a7c60479f76cf973 (diff) | |
| download | abusectl-96beac190b8444fc5b7629daebbf4a67f5ab11e2.tar.gz abusectl-96beac190b8444fc5b7629daebbf4a67f5ab11e2.zip | |
feat: resolve the worklist to abuse contacts
Failure is per query and never stops the run: a timeout on one indicator
must not cost the contacts that did resolve, and a missing contact is a
normal outcome rather than an error.
Bootstraps are passed in rather than fetched here, so this stays
testable with no network and the caller owns the cache policy.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wrfqr2xqQfhtXCscU7zrdz
Diffstat (limited to 'tests/test_contacts.py')
| -rw-r--r-- | tests/test_contacts.py | 126 |
1 files changed, 126 insertions, 0 deletions
diff --git a/tests/test_contacts.py b/tests/test_contacts.py index a6ddabe..0ac197d 100644 --- a/tests/test_contacts.py +++ b/tests/test_contacts.py @@ -109,5 +109,131 @@ class Worklist(unittest.TestCase): self.assertEqual(contacts.worklist(iocs), []) +class Resolve(unittest.TestCase): + IPV4 = {"services": [[["198.51.100.0/24"], ["https://rir.example.invalid/"]]]} + IPV6 = {"services": []} + DNS = {"services": [[["invalid"], ["https://registry.example.invalid/"]]]} + + def _bootstraps(self): + return {"ipv4": self.IPV4, "ipv6": self.IPV6, "dns": self.DNS} + + def test_an_ip_resolves_to_its_abuse_desk(self): + def fetch(url): + return { + "handle": "NET-1", + "entities": [{ + "roles": ["abuse"], + "vcardArray": ["vcard", [ + ["version", {}, "text", "4.0"], + ["email", {}, "text", "abuse@example.invalid"], + ]], + }], + } + + iocs = [{"id": "ioc-1", "type": "ipv4", "value": "198.51.100.7"}] + result = contacts.resolve( + iocs, bootstraps=self._bootstraps(), fetch=fetch + ) + + self.assertEqual(len(result), 1) + self.assertEqual(result[0]["iocs"], ["ioc-1"]) + self.assertEqual(result[0]["query"], "198.51.100.7") + self.assertEqual(result[0]["abuse"], ["abuse@example.invalid"]) + self.assertEqual(result[0]["handle"], "NET-1") + self.assertNotIn("error", result[0]) + + def test_no_abuse_role_records_a_reason_not_an_error(self): + def fetch(url): + return {"handle": "NET-2", "entities": []} + + iocs = [{"id": "ioc-1", "type": "ipv4", "value": "198.51.100.7"}] + result = contacts.resolve( + iocs, bootstraps=self._bootstraps(), fetch=fetch + ) + + self.assertEqual(result[0]["abuse"], []) + self.assertEqual(result[0]["error"], "no abuse role published") + + def test_no_rdap_server_records_a_reason(self): + def fetch(url): + raise AssertionError(f"should not have fetched {url}") + + iocs = [{"id": "ioc-1", "type": "domain", "value": "example.test"}] + result = contacts.resolve( + iocs, bootstraps=self._bootstraps(), fetch=fetch + ) + + self.assertEqual(result[0]["abuse"], []) + self.assertIn("no rdap server", result[0]["error"]) + + def test_a_network_failure_is_per_query_and_does_not_stop_the_run(self): + def fetch(url): + if "198.51.100.7" in url: + raise OSError("connection timed out") + return { + "handle": "DOM-1", + "entities": [{ + "roles": ["abuse"], + "vcardArray": ["vcard", [ + ["version", {}, "text", "4.0"], + ["email", {}, "text", "abuse@example.invalid"], + ]], + }], + } + + iocs = [ + {"id": "ioc-1", "type": "ipv4", "value": "198.51.100.7"}, + {"id": "ioc-2", "type": "domain", "value": "example.invalid"}, + ] + result = contacts.resolve( + iocs, bootstraps=self._bootstraps(), fetch=fetch + ) + + self.assertEqual(len(result), 2) + failed = [r for r in result if r["query"] == "198.51.100.7"][0] + worked = [r for r in result if r["query"] == "example.invalid"][0] + self.assertIn("connection timed out", failed["error"]) + self.assertEqual(worked["abuse"], ["abuse@example.invalid"]) + + def test_one_query_per_host_however_many_iocs(self): + calls = [] + + def fetch(url): + calls.append(url) + return {"handle": "DOM-1", "entities": []} + + iocs = [ + {"id": f"ioc-{n}", "type": "url", + "value": f"https://a.example.invalid/page{n}"} + for n in range(20) + ] + result = contacts.resolve( + iocs, bootstraps=self._bootstraps(), fetch=fetch + ) + + self.assertEqual(len(calls), 1) + self.assertEqual(len(result[0]["iocs"]), 20) + + def test_no_query_ever_carries_a_path(self): + """THE FOURTH PROPERTY, asserted at the transport.""" + calls = [] + + def fetch(url): + calls.append(url) + return {"handle": "DOM-1", "entities": []} + + iocs = [{ + "id": "ioc-1", "type": "url", + "value": "https://a.example.invalid/verify/victim%40example.org?e=x", + }] + contacts.resolve(iocs, bootstraps=self._bootstraps(), fetch=fetch) + + for url in calls: + self.assertNotIn("victim", url) + self.assertNotIn("verify", url) + self.assertNotIn("%40", url) + self.assertNotIn("?", url) + + if __name__ == "__main__": unittest.main() |
