From 51d76e4ed16cc042b2b74f456118593f2cb6aade Mon Sep 17 00:00:00 2001 From: L1ghtn1ng Date: Fri, 8 Aug 2025 01:51:44 +0100 Subject: [PATCH] Fix mypy errors lee to qa --- tests/discovery/test_githubcode.py | 58 +++++++------ theHarvester/__main__.py | 7 +- theHarvester/discovery/additional_apis.py | 51 +++++++----- theHarvester/discovery/api_endpoints.py | 4 +- theHarvester/discovery/builtwith.py | 17 ++-- theHarvester/discovery/duckduckgosearch.py | 14 ++-- theHarvester/discovery/fullhuntsearch.py | 2 +- theHarvester/discovery/haveibeenpwned.py | 14 ++-- theHarvester/discovery/leaklookup.py | 12 +-- theHarvester/discovery/projectdiscovery.py | 4 +- theHarvester/discovery/rapiddns.py | 11 ++- theHarvester/discovery/securityscorecard.py | 28 +++++-- theHarvester/discovery/sitedossier.py | 19 +++-- theHarvester/discovery/subdomainfinderc99.py | 12 ++- theHarvester/discovery/virustotal.py | 13 +-- theHarvester/discovery/whoisxml.py | 4 +- theHarvester/lib/core.py | 88 +++++++++++++++----- theHarvester/lib/hostchecker.py | 45 +++++----- theHarvester/lib/stash.py | 70 +++++++++------- theHarvester/theHarvester.py | 3 +- 20 files changed, 297 insertions(+), 179 deletions(-) diff --git a/tests/discovery/test_githubcode.py b/tests/discovery/test_githubcode.py index ef80ee63..10099694 100644 --- a/tests/discovery/test_githubcode.py +++ b/tests/discovery/test_githubcode.py @@ -17,13 +17,17 @@ class TestSearchGithubCode: def __init__(self): self.response = Response() self.response.status_code = 200 - self.response.json = MagicMock( - return_value={ - "items": [ - {"text_matches": [{"fragment": "test1"}]}, - {"text_matches": [{"fragment": "test2"}]}, - ] - } + object.__setattr__( + self.response, + "json", + MagicMock( + return_value={ + "items": [ + {"text_matches": [{"fragment": "test1"}]}, + {"text_matches": [{"fragment": "test2"}]}, + ] + } + ), ) class FailureResponse: @@ -32,13 +36,13 @@ class TestSearchGithubCode: def __init__(self): self.response = Response() self.response.status_code = 401 - self.response.json = MagicMock(return_value={}) + object.__setattr__(self.response, "json", MagicMock(return_value={})) class RetryResponse: def __init__(self): self.response = Response() self.response.status_code = 403 - self.response.json = MagicMock(return_value={}) + object.__setattr__(self.response, "json", MagicMock(return_value={})) class MalformedResponse: response = Response() @@ -46,23 +50,27 @@ class TestSearchGithubCode: def __init__(self): self.response = Response() self.response.status_code = 200 - self.response.json = MagicMock( - return_value={ - "items": [ - {"fail": True}, - {"text_matches": []}, - {"text_matches": [{"weird": "result"}]}, - ] - } + object.__setattr__( + self.response, + "json", + MagicMock( + return_value={ + "items": [ + {"fail": True}, + {"text_matches": []}, + {"text_matches": [{"weird": "result"}]}, + ] + } + ), ) async def test_missing_key(self): with pytest.raises(MissingKey): - Core.github_key = MagicMock(return_value=None) + Core.github_key = MagicMock(return_value=None) # type: ignore[method-assign] githubcode.SearchGithubCode(word="test", limit=500) async def test_fragments_from_response(self): - Core.github_key = MagicMock(return_value="test_key") + Core.github_key = MagicMock(return_value="test_key") # type: ignore[method-assign] test_class_instance = githubcode.SearchGithubCode(word="test", limit=500) test_result = await test_class_instance.fragments_from_response( self.OkResponse().response.json() @@ -71,7 +79,7 @@ class TestSearchGithubCode: assert test_result == ["test1", "test2"] async def test_invalid_fragments_from_response(self): - Core.github_key = MagicMock(return_value="test_key") + Core.github_key = MagicMock(return_value="test_key") # type: ignore[method-assign] test_class_instance = githubcode.SearchGithubCode(word="test", limit=500) test_result = await test_class_instance.fragments_from_response( self.MalformedResponse().response.json() @@ -79,20 +87,20 @@ class TestSearchGithubCode: assert test_result == [] async def test_next_page(self): - Core.github_key = MagicMock(return_value="test_key") + Core.github_key = MagicMock(return_value="test_key") # type: ignore[method-assign] test_class_instance = githubcode.SearchGithubCode(word="test", limit=500) test_result = githubcode.SuccessResult(list(), next_page=2, last_page=4) assert 2 == await test_class_instance.next_page_or_end(test_result) async def test_last_page(self): - Core.github_key = MagicMock(return_value="test_key") + Core.github_key = MagicMock(return_value="test_key") # type: ignore[method-assign] test_class_instance = githubcode.SearchGithubCode(word="test", limit=500) test_result = githubcode.SuccessResult(list(), 0, 0) assert await test_class_instance.next_page_or_end(test_result) is 0 async def test_infinite_loop_fix_page_zero(self): """Test that the loop condition properly exits when page becomes 0""" - Core.github_key = MagicMock(return_value="test_key") + Core.github_key = MagicMock(return_value="test_key") # type: ignore[method-assign] test_class_instance = githubcode.SearchGithubCode(word="test", limit=500) # Test the fixed condition: page != 0 @@ -106,7 +114,7 @@ class TestSearchGithubCode: async def test_infinite_loop_fix_page_nonzero(self): """Test that the loop condition continues when page is non-zero""" - Core.github_key = MagicMock(return_value="test_key") + Core.github_key = MagicMock(return_value="test_key") # type: ignore[method-assign] test_class_instance = githubcode.SearchGithubCode(word="test", limit=500) # Test with non-zero page values @@ -120,7 +128,7 @@ class TestSearchGithubCode: async def test_infinite_loop_fix_old_vs_new_condition(self): """Test that demonstrates the difference between old and new conditions""" - Core.github_key = MagicMock(return_value="test_key") + Core.github_key = MagicMock(return_value="test_key") # type: ignore[method-assign] test_class_instance = githubcode.SearchGithubCode(word="test", limit=500) page = 0 diff --git a/theHarvester/__main__.py b/theHarvester/__main__.py index 5e6d0c1e..de65319c 100644 --- a/theHarvester/__main__.py +++ b/theHarvester/__main__.py @@ -8,7 +8,7 @@ import string import sys import time import traceback -from typing import Any +from typing import Any, Awaitable import netaddr import ujson @@ -18,6 +18,7 @@ from theHarvester.discovery import ( api_endpoints, baidusearch, bevigil, + bingsearch, bravesearch, bufferoverun, builtwith, @@ -999,7 +1000,7 @@ async def start(rest_args: argparse.Namespace | None = None): queue.task_done() async def handler(lst): - queue = asyncio.Queue() + queue: asyncio.Queue[Awaitable[Any]] = asyncio.Queue() for stor_method in lst: # enqueue the coroutines queue.put_nowait(stor_method) @@ -1515,7 +1516,7 @@ async def start(rest_args: argparse.Namespace | None = None): rate_limits = api_scanner.get_rate_limits() print(f'\n[*] Rate limited endpoints: {len(rate_limits)}') for endpoint, info in rate_limits.items(): - print(f' - {endpoint} ({info["method"]})') + print(f' - {endpoint} ({info.method})') methods = api_scanner.get_methods() print(f'\n[*] HTTP methods used: {", ".join(methods)}') diff --git a/theHarvester/discovery/additional_apis.py b/theHarvester/discovery/additional_apis.py index a40e6689..bda09d93 100644 --- a/theHarvester/discovery/additional_apis.py +++ b/theHarvester/discovery/additional_apis.py @@ -20,18 +20,23 @@ class AdditionalAPIs: self.leaklookup = SearchLeakLookup(domain) self.securityscorecard = SearchSecurityScorecard(domain) self.builtwith = SearchBuiltWith(domain) - self.shodan = None # Will be initialized when needed + self.shodan: SearchShodan | None = None # Will be initialized when needed + + # Aggregated sets for results + self.hosts: set[str] = set() + self.emails: set[str] = set() # Results storage - self.results = { + self.results: dict[str, Any] = { 'breaches': [], 'leaks': [], 'security_score': {}, 'tech_stack': {}, 'shodan_data': {}, - 'hosts': set(), - 'emails': set(), + 'hosts': [], + 'emails': [], } + self.shodan_data: dict[str, Any] = {} async def process(self, proxy: bool = False) -> dict[str, Any]: """Process all additional API services and return combined results.""" @@ -45,9 +50,10 @@ class AdditionalAPIs: await asyncio.gather(*tasks, return_exceptions=True) - # Convert sets to lists for JSON serialization - self.results['hosts'] = list(self.results['hosts']) - self.results['emails'] = list(self.results['emails']) + # Convert aggregated sets to lists for JSON serialization + self.results['hosts'] = list(self.hosts) + self.results['emails'] = list(self.emails) + self.results['shodan_data'] = self.shodan_data return self.results @@ -56,8 +62,8 @@ class AdditionalAPIs: try: await self.haveibeenpwned.process(proxy) self.results['breaches'] = self.haveibeenpwned.breaches - self.results['hosts'].update(self.haveibeenpwned.hosts) - self.results['emails'].update(self.haveibeenpwned.emails) + self.hosts.update(self.haveibeenpwned.hosts) + self.emails.update(self.haveibeenpwned.emails) except Exception as e: print(f'Error processing HaveIBeenPwned: {e}') @@ -66,8 +72,8 @@ class AdditionalAPIs: try: await self.leaklookup.process(proxy) self.results['leaks'] = self.leaklookup.leaks - self.results['hosts'].update(self.leaklookup.hosts) - self.results['emails'].update(self.leaklookup.emails) + self.hosts.update(self.leaklookup.hosts) + self.emails.update(self.leaklookup.emails) except Exception as e: print(f'Error processing Leak-Lookup: {e}') @@ -81,7 +87,7 @@ class AdditionalAPIs: 'issues': self.securityscorecard.issues, 'recommendations': self.securityscorecard.recommendations, } - self.results['hosts'].update(self.securityscorecard.hosts) + self.hosts.update(self.securityscorecard.hosts) except Exception as e: print(f'Error processing SecurityScorecard: {e}') @@ -97,7 +103,7 @@ class AdditionalAPIs: 'analytics': list(self.builtwith.analytics), 'interesting_urls': list(self.builtwith.interesting_urls), } - self.results['hosts'].update(self.builtwith.hosts) + self.hosts.update(self.builtwith.hosts) except Exception as e: print(f'Error processing BuiltWith: {e}') @@ -111,7 +117,7 @@ class AdditionalAPIs: # Get IPs from hosts for Shodan lookup import socket - ips_to_search = set() + ips_to_search: set[str] = set() # Try to resolve domain to IP try: @@ -121,7 +127,7 @@ class AdditionalAPIs: pass # Add any IPs from other results - for host in self.results['hosts']: + for host in self.hosts: if ':' in host: # Extract IP from host:ip format parts = host.split(':') @@ -137,7 +143,7 @@ class AdditionalAPIs: shodan_result = await self.shodan.search_ip(ip) if ip in shodan_result and isinstance(shodan_result[ip], dict): - self.results['shodan_data'][ip] = shodan_result[ip] + self.shodan_data[ip] = shodan_result[ip] elif ip in shodan_result and isinstance(shodan_result[ip], str): print(f'{ip}: {shodan_result[ip]}') @@ -149,8 +155,9 @@ class AdditionalAPIs: except Exception as e: print(f'Error processing Shodan: {e}') - def _is_valid_ip(self, ip_str: str) -> bool: - """Check if string is a valid IP address.""" + @staticmethod + def _is_valid_ip(ip_str: str) -> bool: + """Check if a string is a valid IP address.""" import ipaddress try: @@ -159,10 +166,10 @@ class AdditionalAPIs: except ValueError: return False - async def get_hosts(self) -> set: + async def get_hosts(self) -> set[str]: """Get all discovered hosts.""" - return self.results['hosts'] + return self.hosts - async def get_emails(self) -> set: + async def get_emails(self) -> set[str]: """Get all discovered emails.""" - return self.results['emails'] + return self.emails diff --git a/theHarvester/discovery/api_endpoints.py b/theHarvester/discovery/api_endpoints.py index c4384ed5..e6dd8332 100644 --- a/theHarvester/discovery/api_endpoints.py +++ b/theHarvester/discovery/api_endpoints.py @@ -382,7 +382,7 @@ class SearchApiEndpoints: } # Initialize results storage - self.results = [] + self.results: list[EndpointResult] = [] # Logger setup self.logger = logger @@ -719,7 +719,7 @@ class SearchApiEndpoints: def _get_tech_stack_summary(self) -> dict[str, int]: """Summarize detected technologies.""" - summary = {} + summary: dict[str, int] = {} for url, techs in self.tech_stack.items(): for tech in techs: summary[tech] = summary.get(tech, 0) + 1 diff --git a/theHarvester/discovery/builtwith.py b/theHarvester/discovery/builtwith.py index c5faa8dc..11ebb9cb 100644 --- a/theHarvester/discovery/builtwith.py +++ b/theHarvester/discovery/builtwith.py @@ -1,4 +1,5 @@ import aiohttp +from typing import Any from theHarvester.discovery.constants import MissingKey from theHarvester.lib.core import AsyncFetcher, Core @@ -10,14 +11,14 @@ class SearchBuiltWith: self.api_key = Core.builtwith_key() self.base_url = 'https://api.builtwith.com/v21/api.json' self.headers = {'Authorization': f'Bearer {self.api_key}', 'Content-Type': 'application/json'} - self.hosts = set() - self.tech_stack = {} - self.interesting_urls = set() - self.frameworks = set() - self.languages = set() - self.servers = set() - self.cms = set() - self.analytics = set() + self.hosts: set[str] = set() + self.tech_stack: dict[str, Any] = {} + self.interesting_urls: set[str] = set() + self.frameworks: set[str] = set() + self.languages: set[str] = set() + self.servers: set[str] = set() + self.cms: set[str] = set() + self.analytics: set[str] = set() async def process(self, proxy: bool = False) -> None: """Get technology stack information for a domain.""" diff --git a/theHarvester/discovery/duckduckgosearch.py b/theHarvester/discovery/duckduckgosearch.py index 5ed4512f..2af3a92b 100644 --- a/theHarvester/discovery/duckduckgosearch.py +++ b/theHarvester/discovery/duckduckgosearch.py @@ -9,13 +9,13 @@ class SearchDuckDuckGo: self.word = word self.results = '' self.totalresults = '' - self.dorks: list = [] - self.links: list = [] + self.dorks: list[str] = [] + self.links: list[str] = [] self.database = 'https://duckduckgo.com/?q=' self.api = 'https://api.duckduckgo.com/?q=x&format=json&pretty=1' # Currently using API. self.quantity = '100' self.limit = limit - self.proxy = False + self.proxy: bool = False async def do_search(self) -> None: # Do normal scraping. @@ -29,7 +29,7 @@ class SearchDuckDuckGo: all_resps = await AsyncFetcher.fetch_all(urls) self.totalresults += ''.join(all_resps) - async def crawl(self, text): + async def crawl(self, text: str) -> set[str]: """ Function parses json and returns URLs. :param text: formatted json @@ -52,10 +52,10 @@ class SearchDuckDuckGo: if isinstance(val, dict): # Validation check. for key in val.keys(): value = val.get(key) - if isinstance(value, str) and value != '' and 'https://' in value or 'http://' in value: + if isinstance(value, str) and value != '' and ('https://' in value or 'http://' in value): urls.add(value) - if isinstance(val, str) and val != '' and 'https://' in val or 'http://' in val: + if isinstance(val, str) and val != '' and ('https://' in val or 'http://' in val): urls.add(val) tmp = set() for url in urls: @@ -73,7 +73,7 @@ class SearchDuckDuckGo: return tmp except Exception as e: print(f'Exception occurred: {e}') - return [] + return set() async def get_emails(self): rawres = myparser.Parser(self.totalresults, self.word) diff --git a/theHarvester/discovery/fullhuntsearch.py b/theHarvester/discovery/fullhuntsearch.py index 77802a61..4732396a 100644 --- a/theHarvester/discovery/fullhuntsearch.py +++ b/theHarvester/discovery/fullhuntsearch.py @@ -131,7 +131,7 @@ class SearchFullHunt: 'search_results': [], # Raw search results from advanced queries } self.proxy = False - self.filters = {} # Store filters for advanced searches + self.filters: dict[str, str] = {} # Store filters for advanced searches def _get_headers(self) -> dict[str, str]: """Returns the headers needed for API requests""" diff --git a/theHarvester/discovery/haveibeenpwned.py b/theHarvester/discovery/haveibeenpwned.py index 6fe2368f..e40cc56a 100644 --- a/theHarvester/discovery/haveibeenpwned.py +++ b/theHarvester/discovery/haveibeenpwned.py @@ -10,13 +10,13 @@ class SearchHaveIBeenPwned: self.api_key = Core.haveibeenpwned_key() self.base_url = 'https://haveibeenpwned.com/api/v3' self.headers = {'hibp-api-key': self.api_key, 'user-agent': 'theHarvester', 'Content-Type': 'application/json'} - self.hosts = set() - self.emails = set() - self.breaches = [] - self.pastes = [] - self.breach_dates = set() - self.breach_types = set() - self.affected_data = set() + self.hosts: set[str] = set() + self.emails: set[str] = set() + self.breaches: list[dict] = [] + self.pastes: list[dict] = [] + self.breach_dates: set[str] = set() + self.breach_types: set[str] = set() + self.affected_data: set[str] = set() async def process(self, proxy: bool = False) -> None: """Search for breaches associated with a domain or email.""" diff --git a/theHarvester/discovery/leaklookup.py b/theHarvester/discovery/leaklookup.py index bf66e52e..4d2cd6e5 100644 --- a/theHarvester/discovery/leaklookup.py +++ b/theHarvester/discovery/leaklookup.py @@ -10,12 +10,12 @@ class SearchLeakLookup: self.api_key = Core.leaklookup_key() self.base_url = 'https://leak-lookup.com/api' self.headers = {'Authorization': f'Bearer {self.api_key}', 'Content-Type': 'application/json'} - self.hosts = set() - self.emails = set() - self.leaks = [] - self.passwords = set() - self.sources = set() - self.leak_dates = set() + self.hosts: set[str] = set() + self.emails: set[str] = set() + self.leaks: list[dict] = [] + self.passwords: set[str] = set() + self.sources: set[str] = set() + self.leak_dates: set[str] = set() async def process(self, proxy: bool = False) -> None: """Search for leaked credentials associated with an email.""" diff --git a/theHarvester/discovery/projectdiscovery.py b/theHarvester/discovery/projectdiscovery.py index 39963cea..b812ca45 100644 --- a/theHarvester/discovery/projectdiscovery.py +++ b/theHarvester/discovery/projectdiscovery.py @@ -8,8 +8,8 @@ class SearchDiscovery: self.key = Core.projectdiscovery_key() if self.key is None: raise MissingKey('ProjectDiscovery') - self.total_results = None - self.proxy = False + self.total_results: list[str] = [] + self.proxy: bool = False async def do_search(self): url = f'https://dns.projectdiscovery.io/dns/{self.word}/subdomains' diff --git a/theHarvester/discovery/rapiddns.py b/theHarvester/discovery/rapiddns.py index 2e7b861e..9d3c9876 100644 --- a/theHarvester/discovery/rapiddns.py +++ b/theHarvester/discovery/rapiddns.py @@ -1,4 +1,5 @@ from bs4 import BeautifulSoup +from bs4.element import Tag from theHarvester.lib.core import AsyncFetcher, Core @@ -19,10 +20,18 @@ class SearchRapidDns: if len(responses[0]) <= 1: return self.total_results soup = BeautifulSoup(responses[0], 'html.parser') - rows = soup.find('table').find('tbody').find_all('tr') + table_el = soup.find('table') + if not isinstance(table_el, Tag): + return self.total_results + tbody_el = table_el.find('tbody') + if not isinstance(tbody_el, Tag): + return self.total_results + rows = tbody_el.find_all('tr') if rows: # Validation check for row in rows: + if not isinstance(row, Tag): + continue cells = row.find_all('td') if len(cells) > 0: # sanity check diff --git a/theHarvester/discovery/securityscorecard.py b/theHarvester/discovery/securityscorecard.py index 3241d067..14a71aa0 100644 --- a/theHarvester/discovery/securityscorecard.py +++ b/theHarvester/discovery/securityscorecard.py @@ -10,12 +10,13 @@ class SearchSecurityScorecard: self.api_key = Core.securityscorecard_key() self.base_url = 'https://api.securityscorecard.io' self.headers = {'Authorization': f'Token {self.api_key}', 'Content-Type': 'application/json'} - self.hosts = set() - self.score = 0 - self.grades = {} - self.issues = [] - self.recommendations = [] - self.history = [] + self.hosts: set[str] = set() + self.score: int = 0 + self.grades: dict = {} + self.issues: list[dict] = [] + self.recommendations: list[dict] = [] + self.history: list[dict] = [] + self.ips: list[str] = [] async def process(self, proxy: bool = False) -> None: """Get security scorecard information for a domain.""" @@ -58,9 +59,24 @@ class SearchSecurityScorecard: if 'domains' in data: self.hosts.update(data['domains']) + # Some responses may include IP addresses under different keys + ips = [] + if isinstance(data.get('ips'), list): + ips = [str(ip) for ip in data.get('ips', []) if isinstance(ip, str | int)] + elif isinstance(data.get('ip_addresses'), list): + ips = [str(ip) for ip in data.get('ip_addresses', []) if isinstance(ip, str | int)] + elif isinstance(data.get('associated_ips'), list): + ips = [str(ip) for ip in data.get('associated_ips', []) if isinstance(ip, str | int)] + if ips: + # Deduplicate while preserving already stored entries + self.ips = list({*self.ips, *ips}) + async def get_hostnames(self) -> set[str]: return self.hosts + async def get_ips(self) -> list[str]: + return self.ips + async def get_score(self) -> int: return self.score diff --git a/theHarvester/discovery/sitedossier.py b/theHarvester/discovery/sitedossier.py index 007daefc..12731765 100644 --- a/theHarvester/discovery/sitedossier.py +++ b/theHarvester/discovery/sitedossier.py @@ -1,6 +1,7 @@ import asyncio from bs4 import BeautifulSoup +from bs4.element import Tag from theHarvester.discovery.constants import get_delay from theHarvester.lib.core import AsyncFetcher, Core @@ -44,12 +45,16 @@ class SearchSitedossier: if ( stop_conditions[0] not in base_response and stop_conditions[1] not in base_response ) and bot_string not in base_response: - total_number = soup.find('i') - total_number = int(total_number.text.strip().split(' ')[-1].replace(',', '')) + total_number_el = soup.find('i') + if not isinstance(total_number_el, Tag) or not total_number_el.text: + return + total_number = int(total_number_el.text.strip().split(' ')[-1].replace(',', '')) hrefs = soup.find_all('a', href=True) for a in hrefs: - unparsed = a['href'] - if '/site/' in unparsed: + if not isinstance(a, Tag): + continue + unparsed = a.get('href') + if isinstance(unparsed, str) and '/site/' in unparsed: subdomain = str(unparsed.split('/')[-1]).lower() self.totalhosts.add(subdomain) await asyncio.sleep(get_delay() + 15 + get_delay()) @@ -85,8 +90,10 @@ class SearchSitedossier: soup = BeautifulSoup(response, 'html.parser') hrefs = soup.find_all('a', href=True) for a in hrefs: - unparsed = a['href'] - if '/site/' in unparsed: + if not isinstance(a, Tag): + continue + unparsed = a.get('href') + if isinstance(unparsed, str) and '/site/' in unparsed: subdomain = str(unparsed.split('/')[-1]).lower() self.totalhosts.add(subdomain) await asyncio.sleep(get_delay() + 15 + get_delay()) diff --git a/theHarvester/discovery/subdomainfinderc99.py b/theHarvester/discovery/subdomainfinderc99.py index 2b897cd7..dee44a64 100644 --- a/theHarvester/discovery/subdomainfinderc99.py +++ b/theHarvester/discovery/subdomainfinderc99.py @@ -2,6 +2,7 @@ import asyncio import ujson from bs4 import BeautifulSoup +from bs4.element import Tag from theHarvester.discovery.constants import get_delay from theHarvester.lib.core import AsyncFetcher, Core @@ -50,11 +51,18 @@ class SearchSubdomainfinderc99: @staticmethod async def get_csrf_params(data): - csrf_params = {} + csrf_params: dict[str, str] = {} html = BeautifulSoup(data, 'html.parser').find('div', {'class': 'input-group'}) + if not isinstance(html, Tag): + return csrf_params for c in html.find_all('input'): try: - csrf_params[c.get('name')] = c.get('value') + if not isinstance(c, Tag): + continue + name = c.get('name') + value = c.get('value') + if isinstance(name, str) and value is not None: + csrf_params[name] = str(value) except Exception: continue diff --git a/theHarvester/discovery/virustotal.py b/theHarvester/discovery/virustotal.py index 1fe47c53..4292b225 100644 --- a/theHarvester/discovery/virustotal.py +++ b/theHarvester/discovery/virustotal.py @@ -65,7 +65,7 @@ class SearchVirustotal: @staticmethod async def parse_hostnames(data, word): - total_subdomains = set() + total_subdomains: set[str] = set() for attribute in data: total_subdomains.add(attribute['id'].replace('"', '').replace('www.', '')) attributes = attribute['attributes'] @@ -84,17 +84,18 @@ class SearchVirustotal: if word in value } ) - total_subdomains = list(sorted(total_subdomains)) + # Convert to list for further processing without changing variable type mid-function + subdomains_list: list[str] = list(sorted(total_subdomains)) # Other false positives may occur over time and yes there are other ways to parse this, feel free to implement # them and submit a PR or raise an issue if you run into this filtering not being enough # TODO determine if parsing 'v=spf1 include:_spf-x.acme.com include:_spf-x.acme.com' is worth parsing - total_subdomains = [ + subdomains_list = [ x - for x in total_subdomains + for x in subdomains_list if 'edgekey.net' not in str(x) and 'akadns.net' not in str(x) and 'include:_spf' not in str(x) ] - total_subdomains.sort() - return total_subdomains + subdomains_list.sort() + return subdomains_list async def process(self, proxy: bool = False) -> None: self.proxy = proxy diff --git a/theHarvester/discovery/whoisxml.py b/theHarvester/discovery/whoisxml.py index 1faaf6bb..ef1aad56 100644 --- a/theHarvester/discovery/whoisxml.py +++ b/theHarvester/discovery/whoisxml.py @@ -8,8 +8,8 @@ class SearchWhoisXML: self.key = Core.whoisxml_key() if self.key is None: raise MissingKey('whoisxml') - self.total_results = None - self.proxy = False + self.total_results: list[str] = [] + self.proxy: bool = False async def do_search(self): # https://subdomains.whoisxmlapi.com/api/documentation/making-requests diff --git a/theHarvester/lib/core.py b/theHarvester/lib/core.py index c023a985..f2380582 100644 --- a/theHarvester/lib/core.py +++ b/theHarvester/lib/core.py @@ -383,33 +383,75 @@ class AsyncFetcher: return '' @classmethod - async def fetch(cls, session, url, params: Sized = '', json: bool = False, proxy: str = '') -> str | dict | list | bool: - # This fetch method solely focuses on get requests - # Wrap in try except due to 0x89 png/jpg files + async def fetch( + cls, + session: aiohttp.ClientSession | None = None, + url: str = '', + params: Sized = '', + json: bool = False, + proxy: str | bool | None = '', + headers: dict[str, str] | None = None, + method: str = 'GET', + verify: bool | None = None, + follow_redirects: bool | None = None, + timeout: int | None = None, + ) -> Any: + """ + Generic HTTP request helper. + - If a session is not provided, one will be created and closed automatically. + - Supports optional headers, method selection, proxy, ssl verification, redirects and timeout. + - Returns response text or json depending on `json` flag. + """ try: - if proxy != '': - proxy = str(random.choice(cls().proxy_list)) - if len(params) != 0: - sslcontext = ssl.create_default_context(cafile=certifi.where()) - async with session.get(url, ssl=sslcontext, params=params, proxy=proxy) as response: - return await response.text() if json is False else await response.json() - else: - sslcontext = ssl.create_default_context(cafile=certifi.where()) - async with session.get(url, ssl=sslcontext, proxy=proxy) as response: - await asyncio.sleep(5) - return await response.text() if json is False else await response.json() - - if len(params) != 0: - sslcontext = ssl.create_default_context(cafile=certifi.where()) - async with session.get(url, ssl=sslcontext, params=params) as response: - await asyncio.sleep(5) - return await response.text() if json is False else await response.json() - + # Prepare SSL argument + ssl_arg: ssl.SSLContext | bool | None + if verify is False: + ssl_arg = False else: - sslcontext = ssl.create_default_context(cafile=certifi.where()) - async with session.get(url, ssl=sslcontext) as response: + # default True or None -> verify + ssl_arg = ssl.create_default_context(cafile=certifi.where()) + + # Resolve proxy parameter + proxy_url: str | None = None + if isinstance(proxy, str) and proxy != '': + proxy_url = proxy + elif isinstance(proxy, bool) and proxy: + try: + proxy_choice = random.choice(cls().proxy_list) + proxy_url = str(proxy_choice) if proxy_choice else None + except Exception: + proxy_url = None + + # Prepare timeout + client_timeout = aiohttp.ClientTimeout(total=timeout) if timeout else None + + # Use provided headers or default UA + req_headers = headers if headers is not None else {'User-Agent': Core.get_user_agent()} + + # Decide whether we need to manage the session + owns_session = session is None + if owns_session: + session = aiohttp.ClientSession(headers=req_headers, timeout=client_timeout) + assert session is not None + + try: + request_kwargs: dict[str, Any] = { + 'ssl': ssl_arg, + } + if proxy_url: + request_kwargs['proxy'] = proxy_url + if follow_redirects is not None: + request_kwargs['allow_redirects'] = follow_redirects + if params != '': + request_kwargs['params'] = params + + async with session.request(method.upper(), url, **request_kwargs) as response: + # small backoff similar to previous implementation await asyncio.sleep(5) return await response.text() if json is False else await response.json() + finally: + if owns_session: + await session.close() except Exception as e: print(f'An exception has occurred: {e}') return '' diff --git a/theHarvester/lib/hostchecker.py b/theHarvester/lib/hostchecker.py index 3a489dc1..4e185a64 100644 --- a/theHarvester/lib/hostchecker.py +++ b/theHarvester/lib/hostchecker.py @@ -9,17 +9,20 @@ from __future__ import annotations import asyncio import socket -from typing import Any +from typing import TYPE_CHECKING import aiodns +if TYPE_CHECKING: + from collections.abc import Iterator + class Checker: - def __init__(self, hosts: list, nameservers: list) -> None: - self.hosts = hosts - self.realhosts: list = [] - self.addresses: set = set() - self.nameservers = nameservers + def __init__(self, hosts: list[str], nameservers: list[str]) -> None: + self.hosts: list[str] = hosts + self.realhosts: list[str] = [] + self.addresses: set[str] = set() + self.nameservers: list[str] = nameservers # @staticmethod # async def query(host, resolver) -> Tuple[str, Any]: @@ -34,40 +37,39 @@ class Checker: # return f"{host}", tuple() @staticmethod - async def resolve_host(host, resolver) -> str: + async def resolve_host(host: str, resolver: aiodns.DNSResolver) -> str: try: # TODO add check for ipv6 addrs as well result = await resolver.gethostbyname(host, socket.AF_INET) - addresses = result.addresses - if addresses == [] or addresses is None or result is None: + addresses_list = result.addresses + if addresses_list == [] or addresses_list is None or result is None: return f'{host}:' else: - addresses = ','.join(map(str, list(sorted(set(addresses))))) - # addresses = list(sorted(addresses)) - return f'{host}:{addresses}' + addresses_str = ','.join(map(str, list(sorted(set(addresses_list))))) + return f'{host}:{addresses_str}' except Exception: return f'{host}:' # https://stackoverflow.com/questions/312443/how-do-i-split-a-list-into-equally-sized-chunks @staticmethod - def chunks(lst, n): + def chunks(lst: list[str], n: int) -> Iterator[list[str]]: """Yield successive n-sized chunks from lst.""" for i in range(0, len(lst), n): yield lst[i : i + n] - async def query_all(self, resolver, hosts) -> list[Any]: + async def query_all(self, resolver: aiodns.DNSResolver, hosts: list[str]) -> list[str]: # TODO chunk list into 50 pieces regardless of IPs and subnets - results = await asyncio.gather(*[asyncio.create_task(self.resolve_host(host, resolver)) for host in hosts]) + results: list[str] = await asyncio.gather(*[asyncio.create_task(self.resolve_host(host, resolver)) for host in hosts]) return results - async def check(self): + async def check(self) -> tuple[list[str], list[str], list[str]]: loop = asyncio.get_event_loop() resolver = ( aiodns.DNSResolver(loop=loop, timeout=8) if len(self.nameservers) == 0 else aiodns.DNSResolver(loop=loop, timeout=8, nameservers=self.nameservers) ) - all_results = set() + all_results: set[str] = set() for chunk in self.chunks(self.hosts, 50): # TODO split this to get IPs added total ips results = await self.query_all(resolver, chunk) @@ -75,10 +77,11 @@ class Checker: for pair in results: host, addresses = pair.split(':') self.realhosts.append(host) - self.addresses.update({addr for addr in addresses.split(',')}) + # address may be a list of ips; filter out empties + self.addresses.update({addr for addr in addresses.split(',') if addr}) # address may be a list of ips # and do a set comprehension to remove duplicates self.realhosts.sort() - self.addresses = list(self.addresses) - all_results = list(sorted(all_results)) - return all_results, self.realhosts, self.addresses + addresses_list: list[str] = sorted(self.addresses) + all_results_list: list[str] = sorted(all_results) + return all_results_list, self.realhosts, addresses_list diff --git a/theHarvester/lib/stash.py b/theHarvester/lib/stash.py index c1ff2b3c..35bcf95c 100644 --- a/theHarvester/lib/stash.py +++ b/theHarvester/lib/stash.py @@ -23,6 +23,18 @@ class StashManager: self.latestscanresults: list = [] self.previousscanresults: list = [] + @staticmethod + def _col0_int(row: Row | None) -> int: + try: + val = row[0] if row is not None else None + return int(val) if val is not None else 0 + except Exception: + return 0 + + @staticmethod + def _col0_value(row: Row | None): + return row[0] if row is not None else None + async def do_init(self) -> None: async with aiosqlite.connect(self.db) as db: await db.execute( @@ -77,35 +89,35 @@ class StashManager: (domain,), ) data = await cursor.fetchone() - self.latestscandomain['host'] = data[0] + self.latestscandomain['host'] = self._col0_int(data) cursor = await conn.execute( '''SELECT COUNT(*) from results WHERE domain=? AND type="email"''', (domain,), ) data = await cursor.fetchone() - self.latestscandomain['email'] = data[0] + self.latestscandomain['email'] = self._col0_int(data) cursor = await conn.execute( '''SELECT COUNT(*) from results WHERE domain=? AND type="ip"''', (domain,), ) data = await cursor.fetchone() - self.latestscandomain['ip'] = data[0] + self.latestscandomain['ip'] = self._col0_int(data) cursor = await conn.execute( '''SELECT COUNT(*) from results WHERE domain=? AND type="vhost"''', (domain,), ) data = await cursor.fetchone() - self.latestscandomain['vhost'] = data[0] + self.latestscandomain['vhost'] = self._col0_int(data) cursor = await conn.execute( '''SELECT COUNT(*) from results WHERE domain=? AND type="shodan"''', (domain,), ) data = await cursor.fetchone() - self.latestscandomain['shodan'] = data[0] + self.latestscandomain['shodan'] = self._col0_int(data) cursor = await conn.execute("""SELECT MAX(find_date) FROM results WHERE domain=?""", (domain,)) data = await cursor.fetchone() - self.latestscandomain['latestdate'] = data[0] - latestdate = data[0] + self.latestscandomain['latestdate'] = self._col0_value(data) + latestdate = self._col0_value(data) cursor = await conn.execute( '''SELECT * FROM results WHERE domain=? AND find_date=? AND type="host"''', ( @@ -168,7 +180,8 @@ class StashManager: (domain,), ) previousscandate = await cursor.fetchone() - if not previousscandate: # When theHarvester runs first time/day, this query will return. + prev_date = self._col0_value(previousscandate) + if not prev_date: # When theHarvester runs first time/day, this query will return. self.previousscanresults = [ 'No results', 'No results', @@ -185,7 +198,7 @@ class StashManager: ORDER BY source,type """, ( - previousscandate[0], + prev_date, domain, ), ) @@ -201,6 +214,7 @@ class StashManager: (domain,), ) latestscandate = await cursor.fetchone() + latest_date = self._col0_value(latestscandate) cursor = await conn.execute( """ SELECT find_date, domain, source, type, resource @@ -209,7 +223,7 @@ class StashManager: ORDER BY source,type """, ( - latestscandate, + latest_date, domain, ), ) @@ -227,22 +241,22 @@ class StashManager: async with aiosqlite.connect(self.db, timeout=30) as conn: cursor = await conn.execute('''SELECT COUNT(*) from results WHERE type="host"''') data = await cursor.fetchone() - self.scanboarddata['host'] = data[0] + self.scanboarddata['host'] = self._col0_int(data) cursor = await conn.execute('''SELECT COUNT(*) from results WHERE type="email"''') data = await cursor.fetchone() - self.scanboarddata['email'] = data[0] + self.scanboarddata['email'] = self._col0_int(data) cursor = await conn.execute('''SELECT COUNT(*) from results WHERE type="ip"''') data = await cursor.fetchone() - self.scanboarddata['ip'] = data[0] + self.scanboarddata['ip'] = self._col0_int(data) cursor = await conn.execute('''SELECT COUNT(*) from results WHERE type="vhost"''') data = await cursor.fetchone() - self.scanboarddata['vhost'] = data[0] + self.scanboarddata['vhost'] = self._col0_int(data) cursor = await conn.execute('''SELECT COUNT(*) from results WHERE type="shodan"''') data = await cursor.fetchone() - self.scanboarddata['shodan'] = data[0] + self.scanboarddata['shodan'] = self._col0_int(data) cursor = await conn.execute("""SELECT COUNT(DISTINCT(domain)) FROM results """) data = await cursor.fetchone() - self.scanboarddata['domains'] = data[0] + self.scanboarddata['domains'] = self._col0_int(data) return self.scanboarddata except Exception as e: print(e) @@ -283,11 +297,11 @@ class StashManager: countshodan = await cursor.fetchone() results = { 'date': str(date[0]), - 'hosts': str(counthost[0]), - 'email': str(countemail[0]), - 'ip': str(countip[0]), - 'vhost': str(countvhost[0]), - 'shodan': str(countshodan[0]), + 'hosts': str(self._col0_int(counthost)), + 'email': str(self._col0_int(countemail)), + 'ip': str(self._col0_int(countip)), + 'vhost': str(self._col0_int(countvhost)), + 'shodan': str(self._col0_int(countshodan)), } self.domainscanhistory.append(results) return self.domainscanhistory @@ -319,35 +333,35 @@ class StashManager: (domain,), ) data = await cursor.fetchone() - self.latestscandomain['host'] = data[0] + self.latestscandomain['host'] = self._col0_int(data) cursor = await conn.execute( '''SELECT COUNT(*) from results WHERE domain=? AND type="email"''', (domain,), ) data = await cursor.fetchone() - self.latestscandomain['email'] = data[0] + self.latestscandomain['email'] = self._col0_int(data) cursor = await conn.execute( '''SELECT COUNT(*) from results WHERE domain=? AND type="ip"''', (domain,), ) data = await cursor.fetchone() - self.latestscandomain['ip'] = data[0] + self.latestscandomain['ip'] = self._col0_int(data) cursor = await conn.execute( '''SELECT COUNT(*) from results WHERE domain=? AND type="vhost"''', (domain,), ) data = await cursor.fetchone() - self.latestscandomain['vhost'] = data[0] + self.latestscandomain['vhost'] = self._col0_int(data) cursor = await conn.execute( '''SELECT COUNT(*) from results WHERE domain=? AND type="shodan"''', (domain,), ) data = await cursor.fetchone() - self.latestscandomain['shodan'] = data[0] + self.latestscandomain['shodan'] = self._col0_int(data) cursor = await conn.execute("""SELECT MAX(find_date) FROM results WHERE domain=?""", (domain,)) data = await cursor.fetchone() - self.latestscandomain['latestdate'] = data[0] - latestdate = data[0] + self.latestscandomain['latestdate'] = self._col0_value(data) + latestdate = self._col0_value(data) cursor = await conn.execute( '''SELECT * FROM results WHERE domain=? AND find_date=? AND type="host"''', ( diff --git a/theHarvester/theHarvester.py b/theHarvester/theHarvester.py index 80692f0f..091894fc 100644 --- a/theHarvester/theHarvester.py +++ b/theHarvester/theHarvester.py @@ -17,7 +17,8 @@ def main(): asyncio.DefaultEventLoopPolicy = winloop.EventLoopPolicy except ModuleNotFoundError: - asyncio.DefaultEventLoopPolicy = asyncio.WindowsSelectorEventLoopPolicy + # Fallback to WindowsSelectorEventLoopPolicy if available, else keep default + asyncio.DefaultEventLoopPolicy = getattr(asyncio, 'WindowsSelectorEventLoopPolicy', asyncio.DefaultEventLoopPolicy) else: import uvloop