Files
theHarvester/tests/discovery/test_dymosearch.py
T
Chris (ChrisJr404) 131e23813a feat(discovery): add Dymo API data verifier source (#2300)
Adds a new passive source that calls the Dymo data verifier endpoint
(https://api.tpeoficial.com/v1/private/secure/verify) with the target
domain. The response is stored on the source for downstream inspection
and any matching canonical domain or didYouMean suggestion is added to
the host set so the rest of the pipeline can resolve it.

The source follows the existing plugin shape: SearchDymo lives in
theHarvester/discovery/dymosearch.py, the API key is read from the
``dymo`` block in api-keys.yaml via Core.dymo_key(), and the engine is
registered in get_supportedengines() and the -b/--source dispatcher.

Tests cover the missing-key path, header/payload shape, the empty,
unrelated-suggestion and non-dict-response branches, and the
get_supportedengines() registration. Closes #2300.
2026-05-06 11:04:34 -04:00

146 lines
5.4 KiB
Python

import pytest
from theHarvester.discovery import dymosearch
from theHarvester.discovery.constants import MissingKey
def _patch_dymo_key(monkeypatch, value):
import theHarvester.lib.core as core_module
monkeypatch.setattr(core_module.Core, 'dymo_key', staticmethod(lambda: value), raising=True)
monkeypatch.setattr(core_module.Core, 'get_user_agent', staticmethod(lambda: 'UA'), raising=True)
class TestDymoSearch:
def test_missing_key_raises(self, monkeypatch):
_patch_dymo_key(monkeypatch, None)
with pytest.raises(MissingKey):
dymosearch.SearchDymo('example.com')
def test_init_sets_state(self, monkeypatch):
_patch_dymo_key(monkeypatch, 'token-123')
search = dymosearch.SearchDymo('example.com')
assert search.word == 'example.com'
assert search.key == 'token-123'
assert search.proxy is False
assert search.totalhosts == set()
assert search.results == {}
def test_headers_use_bearer(self, monkeypatch):
_patch_dymo_key(monkeypatch, 'token-abc')
search = dymosearch.SearchDymo('example.com')
headers = search._headers()
assert headers['Authorization'] == 'Bearer token-abc'
assert headers['Content-Type'] == 'application/json'
assert headers['User-Agent'] == 'UA'
@pytest.mark.asyncio
async def test_process_extracts_canonical_and_suggestion(self, monkeypatch):
_patch_dymo_key(monkeypatch, 'token-xyz')
captured = {}
async def fake_post_fetch(url, headers=None, data='', params='', json=False, proxy=False):
captured['url'] = url
captured['headers'] = headers
captured['data'] = data
captured['proxy'] = proxy
return {
'domain': {
'valid': True,
'fraud': False,
'freeSubdomain': False,
'domain': 'exemple.com',
'didYouMean': 'www.exemple.com',
},
'url': {
'valid': True,
'domain': 'exemple.com',
'didYouMean': None,
},
}
import theHarvester.lib.core as core_module
monkeypatch.setattr(core_module.AsyncFetcher, 'post_fetch', classmethod(lambda cls, *a, **kw: fake_post_fetch(*a, **kw)))
search = dymosearch.SearchDymo('exemple.com')
await search.process(proxy=True)
assert captured['url'] == dymosearch.SearchDymo.VERIFY_URL
assert captured['proxy'] is True
assert captured['data'] == {'domain': 'exemple.com', 'url': 'https://exemple.com'}
assert captured['headers']['Authorization'] == 'Bearer token-xyz'
hosts = await search.get_hostnames()
results = await search.get_results()
assert 'exemple.com' in hosts
assert 'www.exemple.com' in hosts
assert results['domain']['valid'] is True
@pytest.mark.asyncio
async def test_process_handles_empty_payload(self, monkeypatch):
_patch_dymo_key(monkeypatch, 'token')
async def fake_post_fetch(url, headers=None, data='', params='', json=False, proxy=False):
return {}
import theHarvester.lib.core as core_module
monkeypatch.setattr(core_module.AsyncFetcher, 'post_fetch', classmethod(lambda cls, *a, **kw: fake_post_fetch(*a, **kw)))
search = dymosearch.SearchDymo('example.com')
await search.process()
assert await search.get_hostnames() == set()
assert await search.get_results() == {}
@pytest.mark.asyncio
async def test_process_ignores_unrelated_suggestion(self, monkeypatch):
_patch_dymo_key(monkeypatch, 'token')
async def fake_post_fetch(url, headers=None, data='', params='', json=False, proxy=False):
return {
'domain': {
'valid': True,
'domain': 'totally-different.org',
'didYouMean': 'somewhere-else.net',
},
}
import theHarvester.lib.core as core_module
monkeypatch.setattr(core_module.AsyncFetcher, 'post_fetch', classmethod(lambda cls, *a, **kw: fake_post_fetch(*a, **kw)))
search = dymosearch.SearchDymo('example.com')
await search.process()
# Neither contains 'example.com', so neither should be added.
assert await search.get_hostnames() == set()
@pytest.mark.asyncio
async def test_process_handles_non_dict_response(self, monkeypatch):
_patch_dymo_key(monkeypatch, 'token')
async def fake_post_fetch(url, headers=None, data='', params='', json=False, proxy=False):
return '<html>error</html>'
import theHarvester.lib.core as core_module
monkeypatch.setattr(core_module.AsyncFetcher, 'post_fetch', classmethod(lambda cls, *a, **kw: fake_post_fetch(*a, **kw)))
search = dymosearch.SearchDymo('example.com')
await search.process()
assert await search.get_hostnames() == set()
assert search.results == {}
class TestDymoIntegration:
def test_module_exposes_class(self, monkeypatch):
from theHarvester.discovery import dymosearch as mod
assert hasattr(mod, 'SearchDymo')
def test_supportedengines_lists_dymo(self):
from theHarvester.lib.core import Core
assert 'dymo' in Core.get_supportedengines()