mirror of
https://github.com/laramies/theHarvester.git
synced 2026-08-23 14:22:24 +02:00
180 lines
6.4 KiB
Python
180 lines
6.4 KiB
Python
import json
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import pytest
|
|
|
|
from theHarvester import __main__ as theharvester_main
|
|
from theHarvester.discovery import gitlabsearch
|
|
from theHarvester.lib.completed_result import CompletedResult
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_public_discovery_normalizes_evidence_and_uses_bounded_requests(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
requests: list[dict[str, object]] = []
|
|
projects = [
|
|
{
|
|
'id': 'group/project',
|
|
'default_branch': 'feature/readme',
|
|
'description': 'API.Example.TEST. and false.example.test.evil',
|
|
'name': 'Example project',
|
|
'path_with_namespace': 'group/project',
|
|
'web_url': 'https://gitlab.com/group/project',
|
|
}
|
|
]
|
|
users = [
|
|
{
|
|
'name': 'Example user',
|
|
'username': 'example',
|
|
'bio': 'Status.Example.TEST.',
|
|
'web_url': 'https://gitlab.com/example',
|
|
'website_url': 'https://Portal.Example.TEST./profile',
|
|
'public_email': 'SECURITY@Example.TEST',
|
|
},
|
|
{
|
|
'name': 'Outsider',
|
|
'username': 'outsider',
|
|
'bio': 'api.notexample.test and example.test.evil',
|
|
'web_url': 'https://gitlab.com/outsider',
|
|
'website_url': 'https://api.notexample.test',
|
|
'public_email': 'outsider@notexample.test',
|
|
},
|
|
]
|
|
|
|
async def fake_fetch_all(
|
|
urls: list[str] | set[str],
|
|
headers: dict[str, str] | None = None,
|
|
proxy: bool = False,
|
|
**_kwargs: Any,
|
|
) -> list[str]:
|
|
url = next(iter(urls))
|
|
requests.append({'url': url, 'headers': headers, 'proxy': proxy})
|
|
responses = {
|
|
'https://gitlab.com/api/v4/projects?search=example.test&per_page=20': json.dumps(projects),
|
|
'https://gitlab.com/api/v4/projects/group%2Fproject/repository/files/README.md/raw?ref=feature%2Freadme': (
|
|
'Contact Admin@Example.TEST. at docs.example.test; ignore admin@notexample.test'
|
|
),
|
|
'https://gitlab.com/api/v4/projects?search=*.example.test&per_page=20': '[]',
|
|
'https://gitlab.com/api/v4/users?search=example.test&per_page=10': json.dumps(users),
|
|
}
|
|
if url not in responses:
|
|
raise AssertionError(f'unexpected GitLab request: {url}')
|
|
return [responses[url]]
|
|
|
|
monkeypatch.setattr(gitlabsearch.Core, 'get_user_agent', staticmethod(lambda: 'UA'))
|
|
monkeypatch.setattr(gitlabsearch.AsyncFetcher, 'fetch_all', fake_fetch_all)
|
|
search = gitlabsearch.SearchGitlab('example.test')
|
|
|
|
await search.process(proxy=True)
|
|
|
|
assert requests == [
|
|
{'url': url, 'headers': {'User-agent': 'UA'}, 'proxy': True}
|
|
for url in (
|
|
'https://gitlab.com/api/v4/projects?search=example.test&per_page=20',
|
|
'https://gitlab.com/api/v4/projects/group%2Fproject/repository/files/README.md/raw?ref=feature%2Freadme',
|
|
'https://gitlab.com/api/v4/projects?search=*.example.test&per_page=20',
|
|
'https://gitlab.com/api/v4/users?search=example.test&per_page=10',
|
|
)
|
|
]
|
|
assert await search.get_hostnames() == {
|
|
'api.example.test',
|
|
'docs.example.test',
|
|
'example.test',
|
|
'portal.example.test',
|
|
'status.example.test',
|
|
}
|
|
assert await search.get_emails() == {'admin@example.test', 'security@example.test'}
|
|
assert await search.get_urls() == {
|
|
'https://gitlab.com/example',
|
|
'https://gitlab.com/group/project',
|
|
'https://Portal.Example.TEST./profile',
|
|
}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_decoded_pages_are_accepted_without_silent_slicing(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
projects = [
|
|
{
|
|
'id': index,
|
|
'default_branch': None,
|
|
'description': f'project-{index}.example.test',
|
|
}
|
|
for index in range(1, 22)
|
|
]
|
|
users = [{'public_email': f'user-{index}@example.test'} for index in range(1, 12)]
|
|
|
|
async def fake_fetch_all(urls: list[str] | set[str], **_kwargs: Any) -> list[object]:
|
|
url = next(iter(urls))
|
|
if 'projects?search=example.test&' in url:
|
|
return [projects]
|
|
if 'projects?search=*.example.test&' in url:
|
|
return [[]]
|
|
if '/users?' in url:
|
|
return [users]
|
|
raise AssertionError(f'unexpected GitLab request: {url}')
|
|
|
|
monkeypatch.setattr(gitlabsearch.AsyncFetcher, 'fetch_all', fake_fetch_all)
|
|
search = gitlabsearch.SearchGitlab('example.test')
|
|
|
|
await search.process()
|
|
|
|
hostnames = await search.get_hostnames()
|
|
emails = await search.get_emails()
|
|
assert len(hostnames) == 21
|
|
assert 'project-21.example.test' in hostnames
|
|
assert len(emails) == 11
|
|
assert 'user-11@example.test' in emails
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_gitlab_urls_reach_completed_jsonl(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
completed_results: list[CompletedResult] = []
|
|
|
|
class FakeResultStore:
|
|
async def initialize(self) -> None:
|
|
return None
|
|
|
|
async def record_observations(self, *_args: object) -> None:
|
|
return None
|
|
|
|
async def save_run(self, result: CompletedResult) -> None:
|
|
completed_results.append(result)
|
|
|
|
class FakeGitlab:
|
|
def __init__(self, domain: str) -> None:
|
|
assert domain == 'example.test'
|
|
|
|
async def process(self, _proxy: bool) -> None:
|
|
return None
|
|
|
|
async def get_hostnames(self) -> set[str]:
|
|
return set()
|
|
|
|
async def get_emails(self) -> set[str]:
|
|
return set()
|
|
|
|
async def get_urls(self) -> set[str]:
|
|
return {'https://gitlab.com/group/project'}
|
|
|
|
report = tmp_path / 'gitlab-report'
|
|
monkeypatch.setattr(theharvester_main, 'ResultStore', FakeResultStore)
|
|
monkeypatch.setattr(gitlabsearch, 'SearchGitlab', FakeGitlab)
|
|
monkeypatch.setattr(sys, 'argv', ['theHarvester', '-d', 'example.test', '-b', 'gitlab', '-f', str(report)])
|
|
|
|
with pytest.raises(SystemExit) as exit_info:
|
|
await theharvester_main.start()
|
|
|
|
assert exit_info.value.code == 0
|
|
assert completed_results[0].results == (('url', 'https://gitlab.com/group/project'),)
|
|
records = [json.loads(line) for line in report.with_suffix('.jsonl').read_text().splitlines()]
|
|
assert {'type': 'url', 'value': 'https://gitlab.com/group/project', 'sources': ['gitlab']} in records
|
|
|
|
|
|
pytestmark = pytest.mark.provider_contract('gitlab')
|