Files
theHarvester/tests/test_all_source_orchestration.py
T

248 lines
8.3 KiB
Python

import json
import sys
import xml.etree.ElementTree as ElementTree
from collections import Counter
from pathlib import Path
from types import ModuleType
from typing import Any
from uuid import UUID
import pytest
from theHarvester import __main__ as theharvester_main
from theHarvester.lib.source_catalog import SOURCE_SPECS, ActivityClass
@pytest.mark.asyncio
async def test_source_help_uses_the_runtime_catalog(
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
) -> None:
monkeypatch.setattr(theharvester_main, 'SOURCE_SPECS', {'catalog-only-source': object()})
monkeypatch.setattr(sys, 'argv', ['theHarvester', '--help'])
with pytest.raises(SystemExit) as exit_info:
await theharvester_main.start()
assert exit_info.value.code == 0
help_output = capsys.readouterr().out
assert 'catalog-only-source' in help_output
assert 'linkedin_links' not in help_output
@pytest.mark.asyncio
async def test_activity_summary_includes_source_and_option_classes(
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
) -> None:
class FakeStash:
async def do_init(self) -> None:
return None
async def store_all(self, *_args: object) -> None:
return None
async def store_completed_result(self, _result: object) -> None:
return None
class FakeCriminalIP:
def __init__(self, _domain: str) -> None:
pass
async def process(self, _proxy: bool) -> None:
return None
async def get_hostnames(self) -> set[str]:
return set()
async def get_ips(self) -> set[str]:
return set()
async def get_asns(self) -> set[str]:
return set()
monkeypatch.setattr(theharvester_main.criminalip, 'SearchCriminalIP', FakeCriminalIP)
monkeypatch.setattr(theharvester_main.stash, 'StashManager', FakeStash)
monkeypatch.setattr(sys, 'argv', ['theHarvester', '-d', 'example.test', '-b', 'criminalip', '-n', '-s'])
with pytest.raises(SystemExit) as exit_info:
await theharvester_main.start()
assert exit_info.value.code == 0
assert '[*] Activity: P0 passive collection, P1 DNS interaction, P2 direct interaction' in capsys.readouterr().out
@pytest.mark.asyncio
async def test_activity_summary_covers_api_scan_without_sources(
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
) -> None:
class FakeStash:
async def do_init(self) -> None:
return None
async def store_all(self, *_args: object) -> None:
return None
async def store_completed_result(self, _result: object) -> None:
return None
def stop_api_scan(**_kwargs: object) -> None:
raise RuntimeError('offline test stop')
monkeypatch.setattr(theharvester_main.api_endpoints, 'SearchApiEndpoints', stop_api_scan)
monkeypatch.setattr(theharvester_main.stash, 'StashManager', FakeStash)
monkeypatch.setattr(sys, 'argv', ['theHarvester', '-d', 'example.test', '-a'])
with pytest.raises(SystemExit) as exit_info:
await theharvester_main.start()
assert exit_info.value.code == 0
assert '[*] Activity: P2 direct interaction' in capsys.readouterr().out
@pytest.mark.asyncio
async def test_legacy_handlerless_source_does_not_break_activity_summary(
monkeypatch: pytest.MonkeyPatch,
) -> None:
class FakeStash:
async def do_init(self) -> None:
return None
async def store_completed_result(self, _result: object) -> None:
return None
monkeypatch.setattr(theharvester_main.stash, 'StashManager', FakeStash)
monkeypatch.setattr(sys, 'argv', ['theHarvester', '-d', 'example.test', '-b', 'linkedin'])
with pytest.raises(SystemExit) as exit_info:
await theharvester_main.start()
assert exit_info.value.code == 0
@pytest.mark.asyncio
async def test_all_schedules_each_passive_catalog_source_once_and_reports_results(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
executions: Counter[str] = Counter()
passive_sources = sorted(
source
for source, spec in SOURCE_SPECS.items()
if spec.activity is ActivityClass.PASSIVE
)
class TestStash(theharvester_main.stash.StashManager):
def __init__(self) -> None:
super().__init__()
self.db = str(tmp_path / 'stash.sqlite')
async def store_all(self, *_args: Any, **_kwargs: Any) -> None:
return None
async def store(self, *_args: Any, **_kwargs: Any) -> None:
return None
class FakeAdapter:
def __init__(self, adapter: str) -> None:
self.adapter = adapter
async def process(self, *_args: object, **_kwargs: object) -> None:
executions[self.adapter] += 1
async def get_hostnames(self) -> set[str]:
return {'sub.example.test'}
async def get_emails(self) -> set[str]:
return {'user@example.test'}
async def get_ips(self) -> set[str]:
return {'192.0.2.1'}
async def get_asns(self) -> set[str]:
return {'AS64500'}
async def get_people(self) -> list[dict[str, str]]:
return [{'name': 'Example Person'}]
async def get_links(self) -> set[str]:
return {'https://sub.example.test/profile'}
async def get_urls(self) -> set[str]:
return {'https://gitlab.com/example/project'}
async def get_interestingurls(self) -> set[str]:
return {'https://sub.example.test/evidence'}
async def get_interesting_urls(self) -> set[str]:
return await self.get_interestingurls()
async def get_host_ip_pairs(self) -> set[tuple[str, str]]:
return set()
async def get_breach_names(self) -> set[str]:
return {'ExampleBreach'}
async def get_infostealers(self) -> list[dict[str, object]]:
return []
def fake_constructor(adapter: str):
def constructor(*_args: object, **_kwargs: object) -> FakeAdapter:
return FakeAdapter(adapter)
return constructor
discovery_modules = sorted(
{
value
for value in vars(theharvester_main).values()
if isinstance(value, ModuleType) and value.__name__.startswith('theHarvester.discovery.')
},
key=lambda module: module.__name__,
)
patched_classes = 0
for module in discovery_modules:
for name, value in list(vars(module).items()):
if isinstance(value, type) and value.__module__ == module.__name__:
monkeypatch.setattr(module, name, fake_constructor(f'{module.__name__}.{name}'))
patched_classes += 1
assert patched_classes
report = tmp_path / 'all-sources'
monkeypatch.setattr(theharvester_main.stash, 'StashManager', TestStash)
monkeypatch.setattr(
sys,
'argv',
['theHarvester', '-d', 'example.test', '-b', 'all', '-f', str(report)],
)
with pytest.raises(SystemExit) as exit_info:
await theharvester_main.start()
assert exit_info.value.code == 0
assert len(executions) == len(passive_sources)
assert sum(executions.values()) == len(passive_sources)
assert set(executions.values()) == {1}
json_report = json.loads(report.with_suffix('.json').read_text())
assert 'sub.example.test' in json_report['hosts']
assert 'user@example.test' in json_report['emails']
jsonl_records = [json.loads(line) for line in report.with_suffix('.jsonl').read_text().splitlines()]
assert {'type': 'hostname', 'value': 'sub.example.test'} in jsonl_records
assert {'type': 'email', 'value': 'user@example.test'} in jsonl_records
assert {'type': 'url', 'value': 'https://gitlab.com/example/project'} in jsonl_records
completed = await TestStash().load_completed_result(UUID(jsonl_records[0]['run_id']))
assert completed.target == 'example.test'
assert ('hostname', 'sub.example.test') in completed.results
assert ('email', 'user@example.test') in completed.results
assert ('ip-address', '192.0.2.1') in completed.results
xml_hosts = {
(element.findtext('hostname') or (element.text or '').strip())
for element in ElementTree.parse(report.with_suffix('.xml')).getroot().findall('host')
}
assert 'sub.example.test' in xml_hosts