diff --git a/subsurfer/core/handler/passive/freecampdev.py b/subsurfer/core/handler/passive/freecampdev.py index 9570313..5cf9f8c 100644 --- a/subsurfer/core/handler/passive/freecampdev.py +++ b/subsurfer/core/handler/passive/freecampdev.py @@ -14,6 +14,8 @@ class FreecampDevScanner: """FreecampDev API를 통한 서브도메인 스캐너""" + + MAX_SUBDOMAINS = 10000 def __init__(self, domain: str, silent: bool = False): """ @@ -25,6 +27,27 @@ def __init__(self, domain: str, silent: bool = False): self.base_url = "https://freecamp.dev/api/tools/network/subdomains/" self.subdomains = set() self.silent = silent + + def _parse_results(self, json_content: str) -> Set[str]: + """응답 JSON에서 유효한 서브도메인만 추출한다.""" + data = json.loads(json_content) + results = data.get("results", []) + found_subdomains = set() + + for result in results: + subdomain = result.get("subdomain", "") + if subdomain and subdomain.endswith(f".{self.domain}"): + found_subdomains.add(subdomain.lower()) + + if len(found_subdomains) > self.MAX_SUBDOMAINS: + if not self.silent: + console.print( + f"[yellow][!][/] FreecampDev 결과 {len(found_subdomains)}개 감지: " + f"{self.MAX_SUBDOMAINS}개 초과로 FP로 판단하고 건너뜁니다" + ) + return set() + + return found_subdomains async def scan(self) -> Set[str]: """ @@ -48,14 +71,7 @@ async def scan(self) -> Set[str]: # JSON 파싱 try: - data = json.loads(json_content) - results = data.get("results", []) - - # 결과 처리 - for result in results: - subdomain = result.get("subdomain", "") - if subdomain and subdomain.endswith(f".{self.domain}"): - self.subdomains.add(subdomain.lower()) + self.subdomains = self._parse_results(json_content) except json.JSONDecodeError as e: if not self.silent: console.print(f"[bold red][-][/] JSON 파싱 오류: {str(e)}") @@ -85,4 +101,4 @@ async def main(): except Exception as e: console.print(f"[bold red][-] 메인 함수 실행 중 오류 발생: {str(e)}[/]") - asyncio.run(main()) \ No newline at end of file + asyncio.run(main()) diff --git a/tests/handlers/test_freecampdev_scanner.py b/tests/handlers/test_freecampdev_scanner.py new file mode 100644 index 0000000..4ece8b1 --- /dev/null +++ b/tests/handlers/test_freecampdev_scanner.py @@ -0,0 +1,81 @@ +import json + +import pytest + +from subsurfer.core.handler.passive.freecampdev import FreecampDevScanner + + +class MockResponse: + def __init__(self, status, text): + self.status = status + self._text = text + + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc, tb): + return None + + async def text(self): + return self._text + + +class MockSession: + def __init__(self, response): + self.response = response + + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc, tb): + return None + + def post(self, *args, **kwargs): + return self.response + + +@pytest.mark.asyncio +async def test_freecampdev_scanner_returns_valid_results(monkeypatch): + domain = "example.com" + payload = json.dumps( + { + "results": [ + {"subdomain": f"www.{domain}"}, + {"subdomain": f"api.{domain}"}, + {"subdomain": "invalid.test"}, + ] + } + ) + + monkeypatch.setattr( + "subsurfer.core.handler.passive.freecampdev.aiohttp.ClientSession", + lambda: MockSession(MockResponse(200, payload)), + ) + + scanner = FreecampDevScanner(domain, silent=True) + results = await scanner.scan() + + assert results == {f"www.{domain}", f"api.{domain}"} + + +@pytest.mark.asyncio +async def test_freecampdev_scanner_skips_false_positive_over_threshold(monkeypatch): + domain = "example.com" + payload = json.dumps( + { + "results": [ + {"subdomain": f"sub{i}.{domain}"} + for i in range(FreecampDevScanner.MAX_SUBDOMAINS + 1) + ] + } + ) + + monkeypatch.setattr( + "subsurfer.core.handler.passive.freecampdev.aiohttp.ClientSession", + lambda: MockSession(MockResponse(200, payload)), + ) + + scanner = FreecampDevScanner(domain, silent=True) + results = await scanner.scan() + + assert results == set()