mirror of
https://github.com/laramies/theHarvester.git
synced 2026-09-30 21:44:54 +02:00
248 lines
8.3 KiB
Python
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
|