fix: remove Windvane DNS guessing fallback (#2541)

This commit is contained in:
Matt
2026-08-14 15:14:53 -04:00
committed by GitHub
parent 7c4420150d
commit 259455f1c1
6 changed files with 15 additions and 86 deletions
+1 -1
View File
@@ -70,7 +70,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Sent a stable, versioned theHarvester identity with provider and API requests while preserving explicit browser identities for sources that require them.
- Kept API endpoint scan URLs canonical instead of prefixing targets onto already complete URLs.
- Made DeHashed pagination honor the CLI limit, retain only normalized email and IP evidence, and discard raw breach rows; aligned LeakIX with its authenticated subdomain endpoint and documented rate-limit retry.
- Added offline contracts for explicitly selected DNS and direct sources, retained normalized Pentest-Tools host and IP results, and hardened Shodan InternetDB, SubdomainFinder C99, and Windvane evidence boundaries.
- Added offline contracts for explicitly selected DNS and direct sources, retained normalized Pentest-Tools host and IP results, hardened Shodan InternetDB and SubdomainFinder C99 evidence boundaries, and removed Windvane's implicit DNS-guessing fallback while retaining its provider-backed source.
- Retained relevant GitLab project, profile, and website URLs in consolidated JSONL and SQLite results while excluding unrelated user URLs.
- Standardized BuiltWith and every other URL-producing adapter on `get_urls()`.
- Made no-filename REST `/query` executions reach completed-result construction and SQLite persistence without changing the legacy response fields.
+10 -13
View File
@@ -1,4 +1,3 @@
import asyncio
import json
import socket
@@ -59,26 +58,24 @@ async def test_authenticated_results_are_normalized_and_scoped(monkeypatch) -> N
@pytest.mark.asyncio
async def test_keyless_fallback_uses_only_the_mocked_dns_boundary(monkeypatch) -> None:
async def test_keyless_provider_failure_does_not_guess_dns_names(monkeypatch) -> None:
monkeypatch.setattr(windvane.Core, 'windvane_key', lambda: None)
requests = 0
async def fake_post_fetch(*_args, **_kwargs):
nonlocal requests
requests += 1
return '{"code":1}'
async def no_sleep(*_args, **_kwargs):
return None
def fake_gethostbyname(hostname: str) -> str:
if hostname == 'api.example.test':
return '203.0.113.10'
raise socket.gaierror
def unexpected_gethostbyname(_hostname: str) -> str:
raise AssertionError('Windvane must not guess common DNS names')
monkeypatch.setattr(windvane.AsyncFetcher, 'post_fetch', fake_post_fetch)
monkeypatch.setattr(asyncio, 'sleep', no_sleep)
monkeypatch.setattr(socket, 'gethostbyname', fake_gethostbyname)
monkeypatch.setattr(socket, 'gethostbyname', unexpected_gethostbyname)
search = windvane.SearchWindvane('example.test')
await search.process()
assert await search.get_hostnames() == {'api.example.test'}
assert await search.get_ips() == {'203.0.113.10'}
assert requests == 1
assert await search.get_hostnames() == set()
assert await search.get_ips() == set()
+1 -2
View File
@@ -90,13 +90,12 @@ def test_all_selects_only_passive_catalog_sources() -> None:
"shodan": ActivityClass.DNS,
"shodanInternetDB": ActivityClass.DNS,
"subdomainfinderc99": ActivityClass.DNS,
"windvane": ActivityClass.DNS,
}
@pytest.mark.parametrize(
'source',
['criminalip', 'pentesttools', 'shodan', 'shodanInternetDB', 'subdomainfinderc99', 'windvane'],
['criminalip', 'pentesttools', 'shodan', 'shodanInternetDB', 'subdomainfinderc99'],
)
def test_non_passive_sources_run_only_when_explicitly_selected(source: str) -> None:
assert source not in Core.expand_source_selection('all')
-2
View File
@@ -19,7 +19,6 @@ NON_PASSIVE_SOURCES = (
'shodan',
'shodanInternetDB',
'subdomainfinderc99',
'windvane',
)
@@ -182,7 +181,6 @@ async def test_explicit_non_passive_source_is_scheduled_once(
'pentesttools': (theharvester_main.pentesttools, 'SearchPentestTools'),
'shodanInternetDB': (theharvester_main.shodan_internetdb, 'SearchShodanInternetDB'),
'subdomainfinderc99': (theharvester_main.subdomainfinderc99, 'SearchSubdomainfinderc99'),
'windvane': (theharvester_main.windvane, 'SearchWindvane'),
}[source]
monkeypatch.setattr(module, constructor_name, lambda *_args, **_kwargs: FakeAdapter())
+3 -67
View File
@@ -30,7 +30,7 @@ class SearchWindvane:
Note: This API requires authentication for full access.
- With API key: Full access to all endpoints with pagination
- Without API key: Limited to 5 unauthenticated requests + DNS fallback
- Without API key: Limited unauthenticated API access
Set API key via:
- Environment variable: export WINDVANE_API_KEY="your-key"
@@ -92,7 +92,7 @@ class SearchWindvane:
await self._search_dns_history(headers)
await self._search_emails(headers)
else:
# Without API key, try alternative/limited approaches
# Without API key, use the provider's limited endpoint only.
logger.info('[*] Windvane API key not found. Using limited unauthenticated access.')
await self._search_subdomains_limited(headers)
@@ -235,78 +235,14 @@ class SearchWindvane:
logger.info(f'[*] Found {len(subdomains)} subdomains with limited access')
else:
# If API call fails, try fallback approaches
await self._fallback_search()
logger.info(f'Windvane limited API returned code {response_data.get("code")}')
except Exception as e:
logger.info(f'Windvane limited API failed: {e}')
await self._fallback_search()
except Exception as e:
logger.info(f'Windvane limited search error: {e}')
async def _fallback_search(self) -> None:
"""Fallback search using common subdomain patterns when API is unavailable"""
try:
logger.info('[*] API unavailable, using fallback subdomain pattern search...')
# Common subdomain prefixes to try
common_subdomains = [
'www',
'mail',
'ftp',
'admin',
'test',
'dev',
'staging',
'api',
'cdn',
'blog',
'shop',
'portal',
'app',
'mobile',
'secure',
'login',
'support',
'help',
'docs',
'status',
]
# Try to resolve common subdomains (basic DNS lookup approach)
import asyncio
import socket
found_count = 0
for sub in common_subdomains:
subdomain = f'{sub}.{self.word}'
try:
# Simple DNS resolution check
await asyncio.sleep(0.1) # Rate limiting
# Use a simple DNS lookup (non-blocking)
loop = asyncio.get_event_loop()
try:
result = await loop.run_in_executor(None, socket.gethostbyname, subdomain)
if result:
self.totalhosts.add(subdomain.lower())
self.totalips.add(result)
found_count += 1
except socket.gaierror:
pass # Subdomain doesn't exist
except Exception:
continue
if found_count > 0:
logger.info(f'[*] Found {found_count} subdomains using DNS fallback')
else:
logger.info('[*] No additional subdomains found via fallback methods')
except Exception as e:
logger.info(f'Fallback search error: {e}')
def set_api_key(self, api_key: str) -> None:
"""Set the API key for authenticated requests
-1
View File
@@ -180,7 +180,6 @@ _SPECS = (
ResultRoute.SUBDOMAINS,
ResultRoute.EMAILS,
ResultRoute.IPS,
activity=ActivityClass.DNS,
),
_spec('yahoo', ResultRoute.SUBDOMAINS, ResultRoute.EMAILS),
_spec(