"""Unit tests for the abuse.ch ThreatFox provider.""" from __future__ import annotations import json import httpx import pytest from decnet.intel.threatfox import ThreatFoxProvider def _install_transport(handler) -> list[httpx.Request]: captured: list[httpx.Request] = [] async def _wrapped(request: httpx.Request) -> httpx.Response: captured.append(request) return await handler(request) transport = httpx.MockTransport(_wrapped) from decnet.intel import threatfox as mod def _factory(): return httpx.AsyncClient( transport=transport, headers={"User-Agent": "curl/7.88.1"}, ) mod.stealth_client = _factory # type: ignore[assignment] return captured @pytest.mark.anyio async def test_match_returns_malicious(monkeypatch): monkeypatch.delenv("DECNET_THREATFOX_API_KEY", raising=False) async def handler(request: httpx.Request) -> httpx.Response: body = json.loads(request.content.decode()) assert body == {"query": "search_ioc", "search_term": "1.2.3.4"} return httpx.Response( 200, json={ "query_status": "ok", "data": [ { "ioc": "1.2.3.4", "ioc_type": "ip:port", "malware": "Cobalt Strike", "confidence_level": 80, } ], }, ) captured = _install_transport(handler) provider = ThreatFoxProvider() result = await provider.lookup("1.2.3.4") assert result.verdict == "malicious" assert result.column_updates["threatfox_listed"] is True raw = result.column_updates["threatfox_raw"] assert raw[0]["malware"] == "Cobalt Strike" # No Auth-Key when none configured. assert "auth-key" not in {h.lower() for h in captured[0].headers} @pytest.mark.anyio async def test_auth_key_sent_when_configured(monkeypatch): monkeypatch.setenv("DECNET_THREATFOX_API_KEY", "tfx-key") async def handler(request: httpx.Request) -> httpx.Response: return httpx.Response(200, json={"query_status": "no_result"}) captured = _install_transport(handler) provider = ThreatFoxProvider() await provider.lookup("8.8.8.8") assert captured[0].headers["auth-key"] == "tfx-key" @pytest.mark.anyio async def test_no_result_caches_unlisted(): async def handler(request: httpx.Request) -> httpx.Response: return httpx.Response(200, json={"query_status": "no_result"}) _install_transport(handler) provider = ThreatFoxProvider() result = await provider.lookup("8.8.8.8") assert result.verdict is None assert result.column_updates["threatfox_listed"] is False assert result.error is None @pytest.mark.anyio async def test_unexpected_status_is_error(): async def handler(request: httpx.Request) -> httpx.Response: return httpx.Response(200, json={"query_status": "illegal_search"}) _install_transport(handler) provider = ThreatFoxProvider() result = await provider.lookup("oops") assert result.error and "illegal_search" in result.error assert result.column_updates == {} @pytest.mark.anyio async def test_threat_types_and_ioc_types_flattened(monkeypatch): """Post-2026-05-02 audit: provider must extract the union of ``threat_type`` / ``ioc_type`` / ``malware`` across all matches. The IntelLifter dispatches ATT&CK on threat_type.""" monkeypatch.delenv("DECNET_THREATFOX_API_KEY", raising=False) async def handler(request: httpx.Request) -> httpx.Response: return httpx.Response( 200, json={"query_status": "ok", "data": [ { "ioc_type": "ip:port", "threat_type": "botnet_cc", "malware": "Sliver", }, { "ioc_type": "url", "threat_type": "payload_delivery", "malware_printable": "Emotet", }, { "ioc_type": "ip:port", # duplicate, dedup'd "threat_type": "botnet_cc", # duplicate "malware": "Sliver", # duplicate }, "not a dict — silently skipped", ]}, ) _install_transport(handler) provider = ThreatFoxProvider() result = await provider.lookup("1.2.3.4") cu = result.column_updates assert cu["threatfox_threat_types"] == ["botnet_cc", "payload_delivery"] assert cu["threatfox_ioc_types"] == ["ip:port", "url"] assert cu["threatfox_malware_families"] == ["Emotet", "Sliver"] @pytest.mark.anyio async def test_no_result_clears_taxonomy_columns(): async def handler(request: httpx.Request) -> httpx.Response: return httpx.Response(200, json={"query_status": "no_result"}) _install_transport(handler) provider = ThreatFoxProvider() result = await provider.lookup("8.8.8.8") cu = result.column_updates assert cu["threatfox_threat_types"] == [] assert cu["threatfox_ioc_types"] == [] assert cu["threatfox_malware_families"] == [] @pytest.mark.anyio async def test_http_error_surfaces(): async def handler(request: httpx.Request) -> httpx.Response: return httpx.Response(502) _install_transport(handler) provider = ThreatFoxProvider() result = await provider.lookup("1.1.1.1") assert result.error == "HTTP 502"