progress
This commit is contained in:
		
							parent
							
								
									cd80d6f569
								
							
						
					
					
						commit
						cee620f5f6
					
				@ -1,5 +1,5 @@
 | 
			
		||||
# File: src/certificate_checker.py
 | 
			
		||||
"""Certificate transparency log checker using crt.sh."""
 | 
			
		||||
"""Certificate transparency log checker using crt.sh with minimal query caching."""
 | 
			
		||||
 | 
			
		||||
import requests
 | 
			
		||||
import json
 | 
			
		||||
@ -15,7 +15,7 @@ from .config import Config
 | 
			
		||||
logger = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
class CertificateChecker:
 | 
			
		||||
    """Check certificates using crt.sh."""
 | 
			
		||||
    """Check certificates using crt.sh with simple query caching to prevent duplicate HTTP requests."""
 | 
			
		||||
    
 | 
			
		||||
    CRT_SH_URL = "https://crt.sh/"
 | 
			
		||||
    
 | 
			
		||||
@ -24,27 +24,26 @@ class CertificateChecker:
 | 
			
		||||
        self.last_request = 0
 | 
			
		||||
        self.query_count = 0
 | 
			
		||||
        self.connection_failures = 0
 | 
			
		||||
        self.max_connection_failures = 3  # Stop trying after 3 consecutive failures
 | 
			
		||||
        self.max_connection_failures = 3
 | 
			
		||||
        
 | 
			
		||||
        logger.info("🔐 Certificate checker initialized")
 | 
			
		||||
        # Simple HTTP request cache to avoid duplicate queries
 | 
			
		||||
        self._http_cache = {}  # query_string -> List[Certificate]
 | 
			
		||||
        
 | 
			
		||||
        # Test connectivity to crt.sh on initialization
 | 
			
		||||
        logger.info("Certificate checker initialized with HTTP request caching")
 | 
			
		||||
        self._test_connectivity()
 | 
			
		||||
    
 | 
			
		||||
    def _test_connectivity(self):
 | 
			
		||||
        """Test if we can reach crt.sh."""
 | 
			
		||||
        try:
 | 
			
		||||
            logger.info("🔗 Testing connectivity to crt.sh...")
 | 
			
		||||
            logger.info("Testing connectivity to crt.sh...")
 | 
			
		||||
            
 | 
			
		||||
            # First test DNS resolution
 | 
			
		||||
            try:
 | 
			
		||||
                socket.gethostbyname('crt.sh')
 | 
			
		||||
                logger.debug("✅ DNS resolution for crt.sh successful")
 | 
			
		||||
                logger.debug("DNS resolution for crt.sh successful")
 | 
			
		||||
            except socket.gaierror as e:
 | 
			
		||||
                logger.warning(f"⚠️ DNS resolution failed for crt.sh: {e}")
 | 
			
		||||
                logger.warning(f"DNS resolution failed for crt.sh: {e}")
 | 
			
		||||
                return False
 | 
			
		||||
            
 | 
			
		||||
            # Test HTTP connection with a simple request
 | 
			
		||||
            response = requests.get(
 | 
			
		||||
                self.CRT_SH_URL,
 | 
			
		||||
                params={'q': 'example.com', 'output': 'json'},
 | 
			
		||||
@ -52,21 +51,21 @@ class CertificateChecker:
 | 
			
		||||
                headers={'User-Agent': 'DNS-Recon-Tool/1.0'}
 | 
			
		||||
            )
 | 
			
		||||
            
 | 
			
		||||
            if response.status_code in [200, 404]:  # 404 is also acceptable (no results)
 | 
			
		||||
                logger.info("✅ crt.sh connectivity test successful")
 | 
			
		||||
            if response.status_code in [200, 404]:
 | 
			
		||||
                logger.info("crt.sh connectivity test successful")
 | 
			
		||||
                return True
 | 
			
		||||
            else:
 | 
			
		||||
                logger.warning(f"⚠️ crt.sh returned status {response.status_code}")
 | 
			
		||||
                logger.warning(f"crt.sh returned status {response.status_code}")
 | 
			
		||||
                return False
 | 
			
		||||
                
 | 
			
		||||
        except requests.exceptions.ConnectionError as e:
 | 
			
		||||
            logger.warning(f"⚠️ Cannot reach crt.sh: {e}")
 | 
			
		||||
            logger.warning(f"Cannot reach crt.sh: {e}")
 | 
			
		||||
            return False
 | 
			
		||||
        except requests.exceptions.Timeout:
 | 
			
		||||
            logger.warning("⚠️ crt.sh connectivity test timed out")
 | 
			
		||||
            logger.warning("crt.sh connectivity test timed out")
 | 
			
		||||
            return False
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            logger.warning(f"⚠️ Unexpected error testing crt.sh connectivity: {e}")
 | 
			
		||||
            logger.warning(f"Unexpected error testing crt.sh connectivity: {e}")
 | 
			
		||||
            return False
 | 
			
		||||
    
 | 
			
		||||
    def _rate_limit(self):
 | 
			
		||||
@ -77,52 +76,66 @@ class CertificateChecker:
 | 
			
		||||
        
 | 
			
		||||
        if time_since_last < min_interval:
 | 
			
		||||
            sleep_time = min_interval - time_since_last
 | 
			
		||||
            logger.debug(f"⏸️ crt.sh rate limiting: sleeping for {sleep_time:.2f}s")
 | 
			
		||||
            logger.debug(f"crt.sh rate limiting: sleeping for {sleep_time:.2f}s")
 | 
			
		||||
            time.sleep(sleep_time)
 | 
			
		||||
        
 | 
			
		||||
        self.last_request = time.time()
 | 
			
		||||
        self.query_count += 1
 | 
			
		||||
    
 | 
			
		||||
    def get_certificates(self, domain: str) -> List[Certificate]:
 | 
			
		||||
        """Get certificates for a domain from crt.sh."""
 | 
			
		||||
        logger.debug(f"🔍 Getting certificates for domain: {domain}")
 | 
			
		||||
        """Get certificates for a domain - EXACTLY the same behavior as original, just with HTTP caching."""
 | 
			
		||||
        logger.debug(f"Getting certificates for domain: {domain}")
 | 
			
		||||
        
 | 
			
		||||
        # Skip if we've had too many connection failures
 | 
			
		||||
        if self.connection_failures >= self.max_connection_failures:
 | 
			
		||||
            logger.warning(f"⚠️ Skipping certificate lookup for {domain} due to repeated connection failures")
 | 
			
		||||
            logger.warning(f"Skipping certificate lookup for {domain} due to repeated connection failures")
 | 
			
		||||
            return []
 | 
			
		||||
        
 | 
			
		||||
        certificates = []
 | 
			
		||||
        
 | 
			
		||||
        # Query for the domain
 | 
			
		||||
        # Query for the domain itself
 | 
			
		||||
        domain_certs = self._query_crt_sh(domain)
 | 
			
		||||
        certificates.extend(domain_certs)
 | 
			
		||||
        
 | 
			
		||||
        # Also query for wildcard certificates (if the main query succeeded)
 | 
			
		||||
        if domain_certs or self.connection_failures < self.max_connection_failures:
 | 
			
		||||
            wildcard_certs = self._query_crt_sh(f"%.{domain}")
 | 
			
		||||
            certificates.extend(wildcard_certs)
 | 
			
		||||
        # Query for wildcard certificates  
 | 
			
		||||
        wildcard_certs = self._query_crt_sh(f"%.{domain}")
 | 
			
		||||
        certificates.extend(wildcard_certs)
 | 
			
		||||
        
 | 
			
		||||
        # Remove duplicates based on certificate ID
 | 
			
		||||
        unique_certs = {cert.id: cert for cert in certificates}
 | 
			
		||||
        final_certs = list(unique_certs.values())
 | 
			
		||||
        
 | 
			
		||||
        if final_certs:
 | 
			
		||||
            logger.info(f"📜 Found {len(final_certs)} unique certificates for {domain}")
 | 
			
		||||
            logger.info(f"Found {len(final_certs)} unique certificates for {domain}")
 | 
			
		||||
        else:
 | 
			
		||||
            logger.debug(f"❌ No certificates found for {domain}")
 | 
			
		||||
            logger.debug(f"No certificates found for {domain}")
 | 
			
		||||
        
 | 
			
		||||
        return final_certs
 | 
			
		||||
    
 | 
			
		||||
    def _query_crt_sh(self, query: str) -> List[Certificate]:
 | 
			
		||||
        """Query crt.sh API with retry logic and better error handling."""
 | 
			
		||||
        """Query crt.sh API with HTTP caching to avoid duplicate requests."""
 | 
			
		||||
        
 | 
			
		||||
        # Check HTTP cache first
 | 
			
		||||
        if query in self._http_cache:
 | 
			
		||||
            logger.debug(f"Using cached HTTP result for crt.sh query: {query}")
 | 
			
		||||
            return self._http_cache[query]
 | 
			
		||||
        
 | 
			
		||||
        # Not cached, make the HTTP request
 | 
			
		||||
        certificates = self._make_http_request(query)
 | 
			
		||||
        
 | 
			
		||||
        # Cache the HTTP result
 | 
			
		||||
        self._http_cache[query] = certificates
 | 
			
		||||
        
 | 
			
		||||
        return certificates
 | 
			
		||||
    
 | 
			
		||||
    def _make_http_request(self, query: str) -> List[Certificate]:
 | 
			
		||||
        """Make actual HTTP request to crt.sh API with retry logic."""
 | 
			
		||||
        certificates = []
 | 
			
		||||
        self._rate_limit()
 | 
			
		||||
        
 | 
			
		||||
        logger.debug(f"📡 Querying crt.sh for: {query}")
 | 
			
		||||
        logger.debug(f"Making HTTP request to crt.sh for: {query}")
 | 
			
		||||
        
 | 
			
		||||
        max_retries = 2  # Reduced retries for faster failure
 | 
			
		||||
        backoff_delays = [1, 3]  # Shorter delays
 | 
			
		||||
        max_retries = 2
 | 
			
		||||
        backoff_delays = [1, 3]
 | 
			
		||||
        
 | 
			
		||||
        for attempt in range(max_retries):
 | 
			
		||||
            try:
 | 
			
		||||
@ -138,16 +151,15 @@ class CertificateChecker:
 | 
			
		||||
                    headers={'User-Agent': 'DNS-Recon-Tool/1.0'}
 | 
			
		||||
                )
 | 
			
		||||
                
 | 
			
		||||
                logger.debug(f"📡 crt.sh API response for {query}: {response.status_code}")
 | 
			
		||||
                logger.debug(f"crt.sh API response for {query}: {response.status_code}")
 | 
			
		||||
                
 | 
			
		||||
                if response.status_code == 200:
 | 
			
		||||
                    try:
 | 
			
		||||
                        data = response.json()
 | 
			
		||||
                        logger.debug(f"📊 crt.sh returned {len(data)} certificate entries for {query}")
 | 
			
		||||
                        logger.debug(f"crt.sh returned {len(data)} certificate entries for {query}")
 | 
			
		||||
                        
 | 
			
		||||
                        for cert_data in data:
 | 
			
		||||
                            try:
 | 
			
		||||
                                # Parse dates with better error handling
 | 
			
		||||
                                not_before = self._parse_date(cert_data.get('not_before'))
 | 
			
		||||
                                not_after = self._parse_date(cert_data.get('not_after'))
 | 
			
		||||
                                
 | 
			
		||||
@ -161,41 +173,39 @@ class CertificateChecker:
 | 
			
		||||
                                        is_wildcard='*.' in cert_data.get('name_value', '')
 | 
			
		||||
                                    )
 | 
			
		||||
                                    certificates.append(certificate)
 | 
			
		||||
                                    logger.debug(f"✅ Parsed certificate ID {certificate.id} for {query}")
 | 
			
		||||
                                    logger.debug(f"Parsed certificate ID {certificate.id} for {query}")
 | 
			
		||||
                                else:
 | 
			
		||||
                                    logger.debug(f"⚠️ Skipped certificate with invalid dates: {cert_data.get('id')}")
 | 
			
		||||
                                    logger.debug(f"Skipped certificate with invalid dates: {cert_data.get('id')}")
 | 
			
		||||
                                    
 | 
			
		||||
                            except (ValueError, TypeError, KeyError) as e:
 | 
			
		||||
                                logger.debug(f"⚠️ Error parsing certificate data: {e}")
 | 
			
		||||
                                continue  # Skip malformed certificate data
 | 
			
		||||
                                logger.debug(f"Error parsing certificate data: {e}")
 | 
			
		||||
                                continue
 | 
			
		||||
                        
 | 
			
		||||
                        # Success! Reset connection failure counter
 | 
			
		||||
                        self.connection_failures = 0
 | 
			
		||||
                        logger.info(f"✅ Successfully processed {len(certificates)} certificates from crt.sh for {query}")
 | 
			
		||||
                        logger.info(f"Successfully processed {len(certificates)} certificates from crt.sh for {query}")
 | 
			
		||||
                        return certificates
 | 
			
		||||
                    
 | 
			
		||||
                    except json.JSONDecodeError as e:
 | 
			
		||||
                        logger.warning(f"❌ Invalid JSON response from crt.sh for {query}: {e}")
 | 
			
		||||
                        logger.warning(f"Invalid JSON response from crt.sh for {query}: {e}")
 | 
			
		||||
                        if attempt < max_retries - 1:
 | 
			
		||||
                            time.sleep(backoff_delays[attempt])
 | 
			
		||||
                            continue
 | 
			
		||||
                        return certificates
 | 
			
		||||
                
 | 
			
		||||
                elif response.status_code == 404:
 | 
			
		||||
                    # 404 is normal - no certificates found
 | 
			
		||||
                    logger.debug(f"ℹ️ No certificates found for {query} (404)")
 | 
			
		||||
                    self.connection_failures = 0  # Reset counter for successful connection
 | 
			
		||||
                    logger.debug(f"No certificates found for {query} (404)")
 | 
			
		||||
                    self.connection_failures = 0
 | 
			
		||||
                    return certificates
 | 
			
		||||
                
 | 
			
		||||
                elif response.status_code == 429:
 | 
			
		||||
                    logger.warning(f"⚠️ crt.sh rate limit exceeded for {query}")
 | 
			
		||||
                    logger.warning(f"crt.sh rate limit exceeded for {query}")
 | 
			
		||||
                    if attempt < max_retries - 1:
 | 
			
		||||
                        time.sleep(5)  # Wait longer for rate limits
 | 
			
		||||
                        time.sleep(5)
 | 
			
		||||
                        continue
 | 
			
		||||
                    return certificates
 | 
			
		||||
                    
 | 
			
		||||
                else:
 | 
			
		||||
                    logger.warning(f"⚠️ crt.sh HTTP error for {query}: {response.status_code}")
 | 
			
		||||
                    logger.warning(f"crt.sh HTTP error for {query}: {response.status_code}")
 | 
			
		||||
                    if attempt < max_retries - 1:
 | 
			
		||||
                        time.sleep(backoff_delays[attempt])
 | 
			
		||||
                        continue
 | 
			
		||||
@ -203,9 +213,8 @@ class CertificateChecker:
 | 
			
		||||
                            
 | 
			
		||||
            except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as e:
 | 
			
		||||
                error_type = "Connection Error" if isinstance(e, requests.exceptions.ConnectionError) else "Timeout"
 | 
			
		||||
                logger.warning(f"🌐 crt.sh {error_type} for {query} (attempt {attempt+1}/{max_retries}): {e}")
 | 
			
		||||
                logger.warning(f"crt.sh {error_type} for {query} (attempt {attempt+1}/{max_retries}): {e}")
 | 
			
		||||
                
 | 
			
		||||
                # Track connection failures
 | 
			
		||||
                if isinstance(e, requests.exceptions.ConnectionError):
 | 
			
		||||
                    self.connection_failures += 1
 | 
			
		||||
                
 | 
			
		||||
@ -214,18 +223,17 @@ class CertificateChecker:
 | 
			
		||||
                    continue
 | 
			
		||||
                    
 | 
			
		||||
            except requests.exceptions.RequestException as e:
 | 
			
		||||
                logger.warning(f"🌐 crt.sh network error for {query} (attempt {attempt+1}/{max_retries}): {e}")
 | 
			
		||||
                logger.warning(f"crt.sh network error for {query} (attempt {attempt+1}/{max_retries}): {e}")
 | 
			
		||||
                if attempt < max_retries - 1:
 | 
			
		||||
                    time.sleep(backoff_delays[attempt])
 | 
			
		||||
                    continue
 | 
			
		||||
            except Exception as e:
 | 
			
		||||
                logger.error(f"❌ Unexpected error querying crt.sh for {query}: {e}")
 | 
			
		||||
                logger.error(f"Unexpected error querying crt.sh for {query}: {e}")
 | 
			
		||||
                if attempt < max_retries - 1:
 | 
			
		||||
                    time.sleep(backoff_delays[attempt])
 | 
			
		||||
                    continue
 | 
			
		||||
        
 | 
			
		||||
        # If we get here, all retries failed
 | 
			
		||||
        logger.warning(f"❌ All {max_retries} attempts failed for crt.sh query: {query}")
 | 
			
		||||
        logger.warning(f"All {max_retries} attempts failed for crt.sh query: {query}")
 | 
			
		||||
        return certificates
 | 
			
		||||
    
 | 
			
		||||
    def _parse_date(self, date_str: str) -> Optional[datetime]:
 | 
			
		||||
@ -233,13 +241,12 @@ class CertificateChecker:
 | 
			
		||||
        if not date_str:
 | 
			
		||||
            return None
 | 
			
		||||
        
 | 
			
		||||
        # Common date formats from crt.sh
 | 
			
		||||
        date_formats = [
 | 
			
		||||
            '%Y-%m-%dT%H:%M:%S',         # ISO format without timezone
 | 
			
		||||
            '%Y-%m-%dT%H:%M:%SZ',        # ISO format with Z
 | 
			
		||||
            '%Y-%m-%d %H:%M:%S',         # Space separated
 | 
			
		||||
            '%Y-%m-%dT%H:%M:%S.%f',      # With microseconds
 | 
			
		||||
            '%Y-%m-%dT%H:%M:%S.%fZ',     # With microseconds and Z
 | 
			
		||||
            '%Y-%m-%dT%H:%M:%S',
 | 
			
		||||
            '%Y-%m-%dT%H:%M:%SZ',
 | 
			
		||||
            '%Y-%m-%d %H:%M:%S',
 | 
			
		||||
            '%Y-%m-%dT%H:%M:%S.%f',
 | 
			
		||||
            '%Y-%m-%dT%H:%M:%S.%fZ',
 | 
			
		||||
        ]
 | 
			
		||||
        
 | 
			
		||||
        for fmt in date_formats:
 | 
			
		||||
@ -248,24 +255,22 @@ class CertificateChecker:
 | 
			
		||||
            except ValueError:
 | 
			
		||||
                continue
 | 
			
		||||
        
 | 
			
		||||
        # Try with timezone info
 | 
			
		||||
        try:
 | 
			
		||||
            return datetime.fromisoformat(date_str.replace('Z', '+00:00'))
 | 
			
		||||
        except ValueError:
 | 
			
		||||
            pass
 | 
			
		||||
        
 | 
			
		||||
        logger.debug(f"⚠️ Could not parse date: {date_str}")
 | 
			
		||||
        logger.debug(f"Could not parse date: {date_str}")
 | 
			
		||||
        return None
 | 
			
		||||
    
 | 
			
		||||
    def extract_subdomains_from_certificates(self, certificates: List[Certificate]) -> Set[str]:
 | 
			
		||||
        """Extract subdomains from certificate subjects."""
 | 
			
		||||
        """Extract subdomains from certificate subjects - EXACTLY the same as original."""
 | 
			
		||||
        subdomains = set()
 | 
			
		||||
        
 | 
			
		||||
        logger.debug(f"🌿 Extracting subdomains from {len(certificates)} certificates")
 | 
			
		||||
        logger.debug(f"Extracting subdomains from {len(certificates)} certificates")
 | 
			
		||||
        
 | 
			
		||||
        for cert in certificates:
 | 
			
		||||
            # Parse subject field for domain names
 | 
			
		||||
            # Certificate subjects can be multi-line with multiple domains
 | 
			
		||||
            subject_lines = cert.subject.split('\n')
 | 
			
		||||
            
 | 
			
		||||
            for line in subject_lines:
 | 
			
		||||
@ -273,39 +278,36 @@ class CertificateChecker:
 | 
			
		||||
                
 | 
			
		||||
                # Skip wildcard domains for recursion (they don't resolve directly)
 | 
			
		||||
                if line.startswith('*.'):
 | 
			
		||||
                    logger.debug(f"🌿 Skipping wildcard domain: {line}")
 | 
			
		||||
                    logger.debug(f"Skipping wildcard domain: {line}")
 | 
			
		||||
                    continue
 | 
			
		||||
                
 | 
			
		||||
                if self._is_valid_domain(line):
 | 
			
		||||
                    subdomains.add(line.lower())
 | 
			
		||||
                    logger.debug(f"🌿 Found subdomain from certificate: {line}")
 | 
			
		||||
                    logger.debug(f"Found subdomain from certificate: {line}")
 | 
			
		||||
        
 | 
			
		||||
        if subdomains:
 | 
			
		||||
            logger.info(f"🌿 Extracted {len(subdomains)} subdomains from certificates")
 | 
			
		||||
            logger.info(f"Extracted {len(subdomains)} subdomains from certificates")
 | 
			
		||||
        else:
 | 
			
		||||
            logger.debug("❌ No subdomains extracted from certificates")
 | 
			
		||||
            logger.debug("No subdomains extracted from certificates")
 | 
			
		||||
        
 | 
			
		||||
        return subdomains
 | 
			
		||||
    
 | 
			
		||||
    def _is_valid_domain(self, domain: str) -> bool:
 | 
			
		||||
        """Basic domain validation."""
 | 
			
		||||
        """Basic domain validation - EXACTLY the same as original."""
 | 
			
		||||
        if not domain or '.' not in domain:
 | 
			
		||||
            return False
 | 
			
		||||
        
 | 
			
		||||
        # Remove common prefixes
 | 
			
		||||
        domain = domain.lower().strip()
 | 
			
		||||
        if domain.startswith('www.'):
 | 
			
		||||
            domain = domain[4:]
 | 
			
		||||
        
 | 
			
		||||
        # Basic validation
 | 
			
		||||
        if len(domain) < 3 or len(domain) > 255:
 | 
			
		||||
            return False
 | 
			
		||||
        
 | 
			
		||||
        # Must not be an IP address
 | 
			
		||||
        try:
 | 
			
		||||
            import socket
 | 
			
		||||
            socket.inet_aton(domain)
 | 
			
		||||
            return False  # It's an IPv4 address
 | 
			
		||||
            return False
 | 
			
		||||
        except socket.error:
 | 
			
		||||
            pass
 | 
			
		||||
        
 | 
			
		||||
@ -314,7 +316,6 @@ class CertificateChecker:
 | 
			
		||||
        if len(parts) < 2:
 | 
			
		||||
            return False
 | 
			
		||||
        
 | 
			
		||||
        # Each part should be reasonable
 | 
			
		||||
        for part in parts:
 | 
			
		||||
            if len(part) < 1 or len(part) > 63:
 | 
			
		||||
                return False
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										112
									
								
								src/main.py
									
									
									
									
									
								
							
							
						
						
									
										112
									
								
								src/main.py
									
									
									
									
									
								
							@ -1,5 +1,5 @@
 | 
			
		||||
# File: src/main.py
 | 
			
		||||
"""Main CLI interface for the reconnaissance tool."""
 | 
			
		||||
"""Main CLI interface for the reconnaissance tool with two-mode operation."""
 | 
			
		||||
 | 
			
		||||
import click
 | 
			
		||||
import json
 | 
			
		||||
@ -19,7 +19,7 @@ logger = logging.getLogger(__name__)
 | 
			
		||||
@click.option('--web', is_flag=True, help='Start web interface instead of CLI')
 | 
			
		||||
@click.option('--shodan-key', help='Shodan API key')
 | 
			
		||||
@click.option('--virustotal-key', help='VirusTotal API key')
 | 
			
		||||
@click.option('--max-depth', default=2, help='Maximum recursion depth (default: 2)')
 | 
			
		||||
@click.option('--max-depth', default=2, help='Maximum recursion depth for full domain mode (default: 2)')
 | 
			
		||||
@click.option('--output', '-o', help='Output file prefix (will create .json and .txt files)')
 | 
			
		||||
@click.option('--json-only', is_flag=True, help='Only output JSON')
 | 
			
		||||
@click.option('--text-only', is_flag=True, help='Only output text report')
 | 
			
		||||
@ -27,13 +27,23 @@ logger = logging.getLogger(__name__)
 | 
			
		||||
@click.option('--verbose', '-v', is_flag=True, help='Enable verbose logging (DEBUG level)')
 | 
			
		||||
@click.option('--quiet', '-q', is_flag=True, help='Quiet mode (WARNING level only)')
 | 
			
		||||
def main(target, web, shodan_key, virustotal_key, max_depth, output, json_only, text_only, port, verbose, quiet):
 | 
			
		||||
    """DNS Reconnaissance Tool
 | 
			
		||||
    """DNS Reconnaissance Tool - Two-Mode Operation
 | 
			
		||||
    
 | 
			
		||||
    MODE 1 - Hostname-only (e.g., 'cc24'):
 | 
			
		||||
      Expands hostname to all TLDs (cc24.com, cc24.net, etc.)
 | 
			
		||||
      No recursive enumeration to avoid third-party infrastructure noise
 | 
			
		||||
      Perfect for discovering domains using a specific hostname
 | 
			
		||||
    
 | 
			
		||||
    MODE 2 - Full domain (e.g., 'cc24.com'):
 | 
			
		||||
      Full recursive reconnaissance with subdomain discovery
 | 
			
		||||
      Maps complete infrastructure of the specified domain
 | 
			
		||||
      Uses max-depth for recursive enumeration
 | 
			
		||||
    
 | 
			
		||||
    Examples:
 | 
			
		||||
        recon example.com                    # Scan example.com
 | 
			
		||||
        recon example                        # Try example.* for all TLDs
 | 
			
		||||
        recon example.com --max-depth 3      # Deeper recursion
 | 
			
		||||
        recon example.com -v                 # Verbose logging
 | 
			
		||||
        recon cc24                           # Mode 1: Find all cc24.* domains (no recursion)
 | 
			
		||||
        recon cc24.com                       # Mode 2: Map cc24.com infrastructure (with recursion)
 | 
			
		||||
        recon cc24.com --max-depth 3         # Mode 2: Deeper recursive enumeration
 | 
			
		||||
        recon cc24 -v                        # Mode 1: Verbose TLD expansion
 | 
			
		||||
        recon --web                          # Start web interface
 | 
			
		||||
    """
 | 
			
		||||
    
 | 
			
		||||
@ -51,18 +61,18 @@ def main(target, web, shodan_key, virustotal_key, max_depth, output, json_only,
 | 
			
		||||
    
 | 
			
		||||
    if web:
 | 
			
		||||
        # Start web interface
 | 
			
		||||
        logger.info("🌐 Starting web interface...")
 | 
			
		||||
        logger.info("Starting web interface...")
 | 
			
		||||
        app = create_app(config)
 | 
			
		||||
        logger.info(f"🚀 Web interface starting on http://0.0.0.0:{port}")
 | 
			
		||||
        logger.info(f"Web interface starting on http://0.0.0.0:{port}")
 | 
			
		||||
        app.run(host='0.0.0.0', port=port, debug=False)  # Changed debug to False to reduce noise
 | 
			
		||||
        return
 | 
			
		||||
    
 | 
			
		||||
    if not target:
 | 
			
		||||
        click.echo("❌ Error: TARGET is required for CLI mode. Use --web for web interface.")
 | 
			
		||||
        click.echo("Error: TARGET is required for CLI mode. Use --web for web interface.")
 | 
			
		||||
        sys.exit(1)
 | 
			
		||||
    
 | 
			
		||||
    # Initialize reconnaissance engine
 | 
			
		||||
    logger.info("🔧 Initializing reconnaissance engine...")
 | 
			
		||||
    logger.info("Initializing reconnaissance engine...")
 | 
			
		||||
    engine = ReconnaissanceEngine(config)
 | 
			
		||||
    
 | 
			
		||||
    # Set up progress callback for CLI
 | 
			
		||||
@ -76,54 +86,62 @@ def main(target, web, shodan_key, virustotal_key, max_depth, output, json_only,
 | 
			
		||||
    
 | 
			
		||||
    # Display startup information
 | 
			
		||||
    click.echo("=" * 60)
 | 
			
		||||
    click.echo("🔍 DNS RECONNAISSANCE TOOL")
 | 
			
		||||
    click.echo("DNS RECONNAISSANCE TOOL")
 | 
			
		||||
    click.echo("=" * 60)
 | 
			
		||||
    click.echo(f"🎯 Target: {target}")
 | 
			
		||||
    click.echo(f"📊 Max recursion depth: {max_depth}")
 | 
			
		||||
    click.echo(f"🌐 DNS servers: {', '.join(config.DNS_SERVERS[:3])}{'...' if len(config.DNS_SERVERS) > 3 else ''}")
 | 
			
		||||
    click.echo(f"⚡ DNS rate limit: {config.DNS_RATE_LIMIT}/s")
 | 
			
		||||
    click.echo(f"Target: {target}")
 | 
			
		||||
    
 | 
			
		||||
    # Show operation mode
 | 
			
		||||
    if '.' in target:
 | 
			
		||||
        click.echo(f"Mode: Full domain reconnaissance (recursive depth: {max_depth})")
 | 
			
		||||
        click.echo("   → Will map complete infrastructure of the specified domain")
 | 
			
		||||
    else:
 | 
			
		||||
        click.echo(f"Mode: Hostname-only reconnaissance (TLD expansion)")
 | 
			
		||||
        click.echo("   → Will find all domains using this hostname (no recursion)")
 | 
			
		||||
    
 | 
			
		||||
    click.echo(f"DNS servers: {', '.join(config.DNS_SERVERS[:3])}{'...' if len(config.DNS_SERVERS) > 3 else ''}")
 | 
			
		||||
    click.echo(f"DNS rate limit: {config.DNS_RATE_LIMIT}/s")
 | 
			
		||||
    
 | 
			
		||||
    if shodan_key:
 | 
			
		||||
        click.echo("✅ Shodan integration enabled")
 | 
			
		||||
        logger.info(f"🕵️  Shodan API key provided (ends with: ...{shodan_key[-4:] if len(shodan_key) > 4 else shodan_key})")
 | 
			
		||||
        click.echo("Shodan integration enabled")
 | 
			
		||||
        logger.info(f"Shodan API key provided (ends with: ...{shodan_key[-4:] if len(shodan_key) > 4 else shodan_key})")
 | 
			
		||||
    else:
 | 
			
		||||
        click.echo("⚠️  Shodan integration disabled (no API key)")
 | 
			
		||||
        click.echo("Shodan integration disabled (no API key)")
 | 
			
		||||
        
 | 
			
		||||
    if virustotal_key:
 | 
			
		||||
        click.echo("✅ VirusTotal integration enabled")
 | 
			
		||||
        logger.info(f"🛡️  VirusTotal API key provided (ends with: ...{virustotal_key[-4:] if len(virustotal_key) > 4 else virustotal_key})")
 | 
			
		||||
        click.echo("VirusTotal integration enabled")
 | 
			
		||||
        logger.info(f"VirusTotal API key provided (ends with: ...{virustotal_key[-4:] if len(virustotal_key) > 4 else virustotal_key})")
 | 
			
		||||
    else:
 | 
			
		||||
        click.echo("⚠️  VirusTotal integration disabled (no API key)")
 | 
			
		||||
        click.echo("VirusTotal integration disabled (no API key)")
 | 
			
		||||
    
 | 
			
		||||
    click.echo("")
 | 
			
		||||
    
 | 
			
		||||
    # Run reconnaissance
 | 
			
		||||
    try:
 | 
			
		||||
        logger.info(f"🚀 Starting reconnaissance for target: {target}")
 | 
			
		||||
        logger.info(f"Starting reconnaissance for target: {target}")
 | 
			
		||||
        data = engine.run_reconnaissance(target)
 | 
			
		||||
        
 | 
			
		||||
        # Display final statistics
 | 
			
		||||
        stats = data.get_stats()
 | 
			
		||||
        click.echo("")
 | 
			
		||||
        click.echo("=" * 60)
 | 
			
		||||
        click.echo("📊 RECONNAISSANCE COMPLETE")
 | 
			
		||||
        click.echo("RECONNAISSANCE COMPLETE")
 | 
			
		||||
        click.echo("=" * 60)
 | 
			
		||||
        click.echo(f"🏠 Hostnames discovered: {stats['hostnames']}")
 | 
			
		||||
        click.echo(f"🌐 IP addresses found: {stats['ip_addresses']}")
 | 
			
		||||
        click.echo(f"📋 DNS records collected: {stats['dns_records']}")
 | 
			
		||||
        click.echo(f"📜 Certificates found: {stats['certificates']}")
 | 
			
		||||
        click.echo(f"🕵️  Shodan results: {stats['shodan_results']}")
 | 
			
		||||
        click.echo(f"🛡️  VirusTotal results: {stats['virustotal_results']}")
 | 
			
		||||
        click.echo(f"Hostnames discovered: {stats['hostnames']}")
 | 
			
		||||
        click.echo(f"IP addresses found: {stats['ip_addresses']}")
 | 
			
		||||
        click.echo(f"DNS records collected: {stats['dns_records']}")
 | 
			
		||||
        click.echo(f"Certificates found: {stats['certificates']}")
 | 
			
		||||
        click.echo(f"Shodan results: {stats['shodan_results']}")
 | 
			
		||||
        click.echo(f"VirusTotal results: {stats['virustotal_results']}")
 | 
			
		||||
        
 | 
			
		||||
        # Calculate and display timing
 | 
			
		||||
        if data.end_time and data.start_time:
 | 
			
		||||
            duration = data.end_time - data.start_time
 | 
			
		||||
            click.echo(f"⏱️  Total time: {duration}")
 | 
			
		||||
            click.echo(f"Total time: {duration}")
 | 
			
		||||
        
 | 
			
		||||
        click.echo("")
 | 
			
		||||
        
 | 
			
		||||
        # Generate reports
 | 
			
		||||
        logger.info("📄 Generating reports...")
 | 
			
		||||
        logger.info("Generating reports...")
 | 
			
		||||
        report_gen = ReportGenerator(data)
 | 
			
		||||
        
 | 
			
		||||
        if output:
 | 
			
		||||
@ -137,9 +155,9 @@ def main(target, web, shodan_key, virustotal_key, max_depth, output, json_only,
 | 
			
		||||
                    with open(json_file, 'w', encoding='utf-8') as f:
 | 
			
		||||
                        f.write(json_content)
 | 
			
		||||
                    saved_files.append(json_file)
 | 
			
		||||
                    logger.info(f"💾 JSON report saved: {json_file}")
 | 
			
		||||
                    logger.info(f"JSON report saved: {json_file}")
 | 
			
		||||
                except Exception as e:
 | 
			
		||||
                    logger.error(f"❌ Failed to save JSON report: {e}")
 | 
			
		||||
                    logger.error(f"Failed to save JSON report: {e}")
 | 
			
		||||
            
 | 
			
		||||
            if not json_only:
 | 
			
		||||
                text_file = f"{output}.txt"
 | 
			
		||||
@ -147,14 +165,14 @@ def main(target, web, shodan_key, virustotal_key, max_depth, output, json_only,
 | 
			
		||||
                    with open(text_file, 'w', encoding='utf-8') as f:
 | 
			
		||||
                        f.write(report_gen.generate_text_report())
 | 
			
		||||
                    saved_files.append(text_file)
 | 
			
		||||
                    logger.info(f"💾 Text report saved: {text_file}")
 | 
			
		||||
                    logger.info(f"Text report saved: {text_file}")
 | 
			
		||||
                except Exception as e:
 | 
			
		||||
                    logger.error(f"❌ Failed to save text report: {e}")
 | 
			
		||||
                    logger.error(f"Failed to save text report: {e}")
 | 
			
		||||
            
 | 
			
		||||
            if saved_files:
 | 
			
		||||
                click.echo(f"💾 Reports saved:")
 | 
			
		||||
                click.echo(f"Reports saved:")
 | 
			
		||||
                for file in saved_files:
 | 
			
		||||
                    click.echo(f"   📄 {file}")
 | 
			
		||||
                    click.echo(f"   {file}")
 | 
			
		||||
        
 | 
			
		||||
        else:
 | 
			
		||||
            # Output to stdout
 | 
			
		||||
@ -162,31 +180,31 @@ def main(target, web, shodan_key, virustotal_key, max_depth, output, json_only,
 | 
			
		||||
                try:
 | 
			
		||||
                    click.echo(data.to_json())
 | 
			
		||||
                except Exception as e:
 | 
			
		||||
                    logger.error(f"❌ Failed to generate JSON output: {e}")
 | 
			
		||||
                    logger.error(f"Failed to generate JSON output: {e}")
 | 
			
		||||
                    click.echo(f"Error generating JSON: {e}")
 | 
			
		||||
            elif text_only:
 | 
			
		||||
                try:
 | 
			
		||||
                    click.echo(report_gen.generate_text_report())
 | 
			
		||||
                except Exception as e:
 | 
			
		||||
                    logger.error(f"❌ Failed to generate text report: {e}")
 | 
			
		||||
                    logger.error(f"Failed to generate text report: {e}")
 | 
			
		||||
                    click.echo(f"Error generating text report: {e}")
 | 
			
		||||
            else:
 | 
			
		||||
                # Default: show text report
 | 
			
		||||
                try:
 | 
			
		||||
                    click.echo(report_gen.generate_text_report())
 | 
			
		||||
                    click.echo(f"\n💡 To get JSON output, use: --json-only")
 | 
			
		||||
                    click.echo(f"💡 To save reports, use: --output filename")
 | 
			
		||||
                    click.echo(f"\nTo get JSON output, use: --json-only")
 | 
			
		||||
                    click.echo(f"To save reports, use: --output filename")
 | 
			
		||||
                except Exception as e:
 | 
			
		||||
                    logger.error(f"❌ Failed to generate report: {e}")
 | 
			
		||||
                    logger.error(f"Failed to generate report: {e}")
 | 
			
		||||
                    click.echo(f"Error generating report: {e}")
 | 
			
		||||
    
 | 
			
		||||
    except KeyboardInterrupt:
 | 
			
		||||
        logger.warning("⚠️  Reconnaissance interrupted by user")
 | 
			
		||||
        click.echo("\n⚠️  Reconnaissance interrupted by user.")
 | 
			
		||||
        logger.warning("Reconnaissance interrupted by user")
 | 
			
		||||
        click.echo("\nReconnaissance interrupted by user.")
 | 
			
		||||
        sys.exit(1)
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        logger.error(f"❌ Error during reconnaissance: {e}", exc_info=True)
 | 
			
		||||
        click.echo(f"❌ Error during reconnaissance: {e}")
 | 
			
		||||
        logger.error(f"Error during reconnaissance: {e}", exc_info=True)
 | 
			
		||||
        click.echo(f"Error during reconnaissance: {e}")
 | 
			
		||||
        if verbose:
 | 
			
		||||
            raise  # Re-raise in verbose mode to show full traceback
 | 
			
		||||
        sys.exit(1)
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
# File: src/reconnaissance.py
 | 
			
		||||
"""Main reconnaissance logic with enhanced TLD expansion."""
 | 
			
		||||
"""Main reconnaissance logic with two-mode operation for hostname vs domain targets."""
 | 
			
		||||
 | 
			
		||||
import threading
 | 
			
		||||
import concurrent.futures
 | 
			
		||||
@ -18,7 +18,7 @@ from .tld_fetcher import TLDFetcher
 | 
			
		||||
logger = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
class ReconnaissanceEngine:
 | 
			
		||||
    """Main reconnaissance engine with smart TLD expansion."""
 | 
			
		||||
    """Main reconnaissance engine with two-mode operation: hostname-only vs full domain."""
 | 
			
		||||
    
 | 
			
		||||
    def __init__(self, config: Config):
 | 
			
		||||
        self.config = config
 | 
			
		||||
@ -32,16 +32,16 @@ class ReconnaissanceEngine:
 | 
			
		||||
        self.shodan_client = None
 | 
			
		||||
        if config.shodan_key:
 | 
			
		||||
            self.shodan_client = ShodanClient(config.shodan_key, config)
 | 
			
		||||
            logger.info("✅ Shodan client initialized")
 | 
			
		||||
            logger.info("Shodan client initialized")
 | 
			
		||||
        else:
 | 
			
		||||
            logger.info("⚠️ Shodan API key not provided, skipping Shodan integration")
 | 
			
		||||
            logger.info("Shodan API key not provided, skipping Shodan integration")
 | 
			
		||||
        
 | 
			
		||||
        self.virustotal_client = None
 | 
			
		||||
        if config.virustotal_key:
 | 
			
		||||
            self.virustotal_client = VirusTotalClient(config.virustotal_key, config)
 | 
			
		||||
            logger.info("✅ VirusTotal client initialized")
 | 
			
		||||
            logger.info("VirusTotal client initialized")
 | 
			
		||||
        else:
 | 
			
		||||
            logger.info("⚠️ VirusTotal API key not provided, skipping VirusTotal integration")
 | 
			
		||||
            logger.info("VirusTotal API key not provided, skipping VirusTotal integration")
 | 
			
		||||
        
 | 
			
		||||
        # Progress tracking
 | 
			
		||||
        self.progress_callback = None
 | 
			
		||||
@ -57,7 +57,7 @@ class ReconnaissanceEngine:
 | 
			
		||||
    def set_shared_data(self, shared_data: ReconData):
 | 
			
		||||
        """Set shared data object for live updates during web interface usage."""
 | 
			
		||||
        self.shared_data = shared_data
 | 
			
		||||
        logger.info("📊 Using shared data object for live updates")
 | 
			
		||||
        logger.info("Using shared data object for live updates")
 | 
			
		||||
    
 | 
			
		||||
    def _update_progress(self, message: str, percentage: int = None):
 | 
			
		||||
        """Update progress if callback is set."""
 | 
			
		||||
@ -66,73 +66,92 @@ class ReconnaissanceEngine:
 | 
			
		||||
            self.progress_callback(message, percentage)
 | 
			
		||||
    
 | 
			
		||||
    def run_reconnaissance(self, target: str) -> ReconData:
 | 
			
		||||
        """Run full reconnaissance on target."""
 | 
			
		||||
        """Run reconnaissance on target using appropriate mode based on input type."""
 | 
			
		||||
        # Use shared data object if available, otherwise create new one
 | 
			
		||||
        if self.shared_data is not None:
 | 
			
		||||
            self.data = self.shared_data
 | 
			
		||||
            logger.info("📊 Using shared data object for reconnaissance")
 | 
			
		||||
            logger.info("Using shared data object for reconnaissance")
 | 
			
		||||
        else:
 | 
			
		||||
            self.data = ReconData()
 | 
			
		||||
            logger.info("📊 Created new data object for reconnaissance")
 | 
			
		||||
            logger.info("Created new data object for reconnaissance")
 | 
			
		||||
            
 | 
			
		||||
        self.data.start_time = datetime.now()
 | 
			
		||||
        
 | 
			
		||||
        logger.info(f"🚀 Starting reconnaissance for target: {target}")
 | 
			
		||||
        logger.info(f"📊 Configuration: max_depth={self.config.max_depth}, "
 | 
			
		||||
        logger.info(f"Starting reconnaissance for target: {target}")
 | 
			
		||||
        logger.info(f"Configuration: max_depth={self.config.max_depth}, "
 | 
			
		||||
                   f"DNS_rate={self.config.DNS_RATE_LIMIT}/s")
 | 
			
		||||
        
 | 
			
		||||
        # Store original max_depth for potential restoration
 | 
			
		||||
        original_max_depth = self.config.max_depth
 | 
			
		||||
        reconnaissance_mode = "full_domain" if '.' in target else "hostname_only"
 | 
			
		||||
        
 | 
			
		||||
        try:
 | 
			
		||||
            # Determine if target is hostname.tld or just hostname
 | 
			
		||||
            # Determine operation mode based on target format
 | 
			
		||||
            if '.' in target:
 | 
			
		||||
                logger.info(f"🎯 Target '{target}' appears to be a full domain name")
 | 
			
		||||
                logger.info(f"Target '{target}' appears to be a full domain name")
 | 
			
		||||
                logger.info(f"Mode: Full domain reconnaissance with recursive enumeration (max_depth={self.config.max_depth})")
 | 
			
		||||
                self._update_progress(f"Starting reconnaissance for {target}", 0)
 | 
			
		||||
                self.data.add_hostname(target, 0)
 | 
			
		||||
                initial_targets = {target}
 | 
			
		||||
            else:
 | 
			
		||||
                logger.info(f"🔍 Target '{target}' appears to be a hostname, expanding to all TLDs")
 | 
			
		||||
                logger.info(f"Target '{target}' appears to be a hostname, expanding to all TLDs")
 | 
			
		||||
                logger.info(f"Mode: Hostname-only reconnaissance - TLD expansion without recursion")
 | 
			
		||||
                self._update_progress(f"Expanding {target} to all TLDs", 5)
 | 
			
		||||
                initial_targets = self._expand_hostname_to_tlds_smart(target)
 | 
			
		||||
                logger.info(f"📋 Found {len(initial_targets)} valid domains after TLD expansion")
 | 
			
		||||
                
 | 
			
		||||
                # Override max_depth for hostname-only queries to prevent infrastructure noise
 | 
			
		||||
                self.config.max_depth = 0
 | 
			
		||||
                logger.info(f"Found {len(initial_targets)} valid domains after TLD expansion")
 | 
			
		||||
                logger.info(f"Set max_depth=0 for hostname-only reconnaissance (avoiding third-party infrastructure)")
 | 
			
		||||
            
 | 
			
		||||
            self._update_progress("Resolving initial targets", 10)
 | 
			
		||||
            
 | 
			
		||||
            # Process all targets recursively
 | 
			
		||||
            # Process all targets with appropriate recursion depth
 | 
			
		||||
            self._process_targets_recursively(initial_targets)
 | 
			
		||||
            
 | 
			
		||||
            # Final external lookups
 | 
			
		||||
            self._update_progress("Performing external service lookups", 90)
 | 
			
		||||
            self._perform_external_lookups()
 | 
			
		||||
            
 | 
			
		||||
            # Log final statistics
 | 
			
		||||
            # Log final statistics with reconnaissance mode
 | 
			
		||||
            stats = self.data.get_stats()
 | 
			
		||||
            logger.info(f"📈 Final statistics: {stats}")
 | 
			
		||||
            logger.info(f"Final statistics ({reconnaissance_mode}): {stats}")
 | 
			
		||||
            
 | 
			
		||||
            if reconnaissance_mode == "hostname_only":
 | 
			
		||||
                logger.info(f"Hostname-only reconnaissance complete: discovered {stats['hostnames']} domains using '{target}' hostname")
 | 
			
		||||
                logger.info(f"To perform recursive enumeration on specific domains, run with full domain names (e.g., '{target}.com')")
 | 
			
		||||
            else:
 | 
			
		||||
                logger.info(f"Full domain reconnaissance complete with recursive depth {original_max_depth}")
 | 
			
		||||
            
 | 
			
		||||
            self._update_progress("Reconnaissance complete", 100)
 | 
			
		||||
            
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            logger.error(f"❌ Error during reconnaissance: {e}", exc_info=True)
 | 
			
		||||
            logger.error(f"Error during reconnaissance: {e}", exc_info=True)
 | 
			
		||||
            raise
 | 
			
		||||
        finally:
 | 
			
		||||
            # Restore original max_depth (though this engine instance is typically discarded)
 | 
			
		||||
            self.config.max_depth = original_max_depth
 | 
			
		||||
            
 | 
			
		||||
            self.data.end_time = datetime.now()
 | 
			
		||||
            duration = self.data.end_time - self.data.start_time
 | 
			
		||||
            logger.info(f"⏱️ Total reconnaissance time: {duration}")
 | 
			
		||||
            logger.info(f"Total reconnaissance time: {duration}")
 | 
			
		||||
        
 | 
			
		||||
        return self.data
 | 
			
		||||
    
 | 
			
		||||
    def _expand_hostname_to_tlds_smart(self, hostname: str) -> Set[str]:
 | 
			
		||||
        """Smart TLD expansion with prioritization and parallel processing."""
 | 
			
		||||
        logger.info(f"🌐 Starting smart TLD expansion for hostname: {hostname}")
 | 
			
		||||
        logger.info(f"Starting smart TLD expansion for hostname: {hostname}")
 | 
			
		||||
        
 | 
			
		||||
        # Get prioritized TLD lists
 | 
			
		||||
        priority_tlds, normal_tlds, deprioritized_tlds = self.tld_fetcher.get_prioritized_tlds()
 | 
			
		||||
        
 | 
			
		||||
        logger.info(f"📊 TLD categories: {len(priority_tlds)} priority, "
 | 
			
		||||
        logger.info(f"TLD categories: {len(priority_tlds)} priority, "
 | 
			
		||||
                   f"{len(normal_tlds)} normal, {len(deprioritized_tlds)} deprioritized")
 | 
			
		||||
        
 | 
			
		||||
        valid_domains = set()
 | 
			
		||||
        
 | 
			
		||||
        # Phase 1: Check priority TLDs first (parallel processing)
 | 
			
		||||
        logger.info("🚀 Phase 1: Checking priority TLDs...")
 | 
			
		||||
        logger.info("Phase 1: Checking priority TLDs...")
 | 
			
		||||
        priority_results = self._check_tlds_parallel(hostname, priority_tlds, "priority")
 | 
			
		||||
        valid_domains.update(priority_results)
 | 
			
		||||
        
 | 
			
		||||
@ -140,37 +159,36 @@ class ReconnaissanceEngine:
 | 
			
		||||
        
 | 
			
		||||
        # Phase 2: Check normal TLDs (if we found fewer than 5 results)
 | 
			
		||||
        if len(valid_domains) < 5:
 | 
			
		||||
            logger.info("🔍 Phase 2: Checking normal TLDs...")
 | 
			
		||||
            logger.info("Phase 2: Checking normal TLDs...")
 | 
			
		||||
            normal_results = self._check_tlds_parallel(hostname, normal_tlds, "normal")
 | 
			
		||||
            valid_domains.update(normal_results)
 | 
			
		||||
            
 | 
			
		||||
            self._update_progress(f"Phase 2 complete: {len(normal_results)} normal TLD matches", 8)
 | 
			
		||||
        else:
 | 
			
		||||
            logger.info(f"⏭️ Skipping normal TLDs (found {len(valid_domains)} matches in priority)")
 | 
			
		||||
            logger.info(f"Skipping normal TLDs (found {len(valid_domains)} matches in priority)")
 | 
			
		||||
        
 | 
			
		||||
        # Phase 3: Check deprioritized TLDs only if we found very few results
 | 
			
		||||
        if len(valid_domains) < 2:
 | 
			
		||||
            logger.info("🔍 Phase 3: Checking deprioritized TLDs (limited results so far)...")
 | 
			
		||||
            logger.info("Phase 3: Checking deprioritized TLDs (limited results so far)...")
 | 
			
		||||
            depri_results = self._check_tlds_parallel(hostname, deprioritized_tlds, "deprioritized")
 | 
			
		||||
            valid_domains.update(depri_results)
 | 
			
		||||
            
 | 
			
		||||
            self._update_progress(f"Phase 3 complete: {len(depri_results)} deprioritized TLD matches", 9)
 | 
			
		||||
        else:
 | 
			
		||||
            logger.info(f"⏭️ Skipping deprioritized TLDs (found {len(valid_domains)} matches already)")
 | 
			
		||||
            logger.info(f"Skipping deprioritized TLDs (found {len(valid_domains)} matches already)")
 | 
			
		||||
        
 | 
			
		||||
        logger.info(f"🎯 Smart TLD expansion complete: found {len(valid_domains)} valid domains")
 | 
			
		||||
        logger.info(f"Smart TLD expansion complete: found {len(valid_domains)} valid domains")
 | 
			
		||||
        return valid_domains
 | 
			
		||||
    
 | 
			
		||||
    def _check_tlds_parallel(self, hostname: str, tlds: List[str], phase_name: str) -> Set[str]:
 | 
			
		||||
        """Check TLDs in parallel with optimized settings."""
 | 
			
		||||
        valid_domains = set()
 | 
			
		||||
        tested_count = 0
 | 
			
		||||
        wildcard_detected = set()
 | 
			
		||||
        
 | 
			
		||||
        # Use thread pool for parallel processing
 | 
			
		||||
        max_workers = min(20, len(tlds))  # Limit concurrent requests
 | 
			
		||||
        
 | 
			
		||||
        logger.info(f"⚡ Starting parallel check of {len(tlds)} {phase_name} TLDs "
 | 
			
		||||
        logger.info(f"Starting parallel check of {len(tlds)} {phase_name} TLDs "
 | 
			
		||||
                   f"with {max_workers} workers")
 | 
			
		||||
        
 | 
			
		||||
        with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
 | 
			
		||||
@ -191,8 +209,7 @@ class ReconnaissanceEngine:
 | 
			
		||||
                    if result:
 | 
			
		||||
                        full_hostname, ips = result
 | 
			
		||||
                        
 | 
			
		||||
                        
 | 
			
		||||
                        logger.info(f"✅ Valid domain found: {full_hostname} -> {ips}")
 | 
			
		||||
                        logger.info(f"Valid domain found: {full_hostname} -> {ips}")
 | 
			
		||||
                        self.data.add_hostname(full_hostname, 0)
 | 
			
		||||
                        valid_domains.add(full_hostname)
 | 
			
		||||
                        
 | 
			
		||||
@ -201,18 +218,17 @@ class ReconnaissanceEngine:
 | 
			
		||||
                    
 | 
			
		||||
                    # Progress update every 50 TLDs in this phase
 | 
			
		||||
                    if tested_count % 50 == 0:
 | 
			
		||||
                        logger.info(f"📊 {phase_name.title()} phase progress: "
 | 
			
		||||
                        logger.info(f"{phase_name.title()} phase progress: "
 | 
			
		||||
                                  f"{tested_count}/{len(tlds)} tested, "
 | 
			
		||||
                                  f"{len(valid_domains)} found")
 | 
			
		||||
                        
 | 
			
		||||
                except concurrent.futures.TimeoutError:
 | 
			
		||||
                    logger.debug(f"⏱️ Timeout checking {hostname}.{tld}")
 | 
			
		||||
                    logger.debug(f"Timeout checking {hostname}.{tld}")
 | 
			
		||||
                except Exception as e:
 | 
			
		||||
                    logger.debug(f"⚠️ Error checking {hostname}.{tld}: {e}")
 | 
			
		||||
                    logger.debug(f"Error checking {hostname}.{tld}: {e}")
 | 
			
		||||
        
 | 
			
		||||
        logger.info(f"📊 {phase_name.title()} phase complete: "
 | 
			
		||||
                   f"tested {tested_count} TLDs, found {len(valid_domains)} valid domains, "
 | 
			
		||||
                   f"detected {len(wildcard_detected)} wildcards")
 | 
			
		||||
        logger.info(f"{phase_name.title()} phase complete: "
 | 
			
		||||
                   f"tested {tested_count} TLDs, found {len(valid_domains)} valid domains")
 | 
			
		||||
        
 | 
			
		||||
        return valid_domains
 | 
			
		||||
    
 | 
			
		||||
@ -224,7 +240,7 @@ class ReconnaissanceEngine:
 | 
			
		||||
        ips = self.dns_resolver.resolve_hostname_fast(full_hostname)
 | 
			
		||||
        
 | 
			
		||||
        if ips:
 | 
			
		||||
            logger.debug(f"✅ {full_hostname} -> {ips}")
 | 
			
		||||
            logger.debug(f"{full_hostname} -> {ips}")
 | 
			
		||||
            return (full_hostname, ips)
 | 
			
		||||
        
 | 
			
		||||
        return None
 | 
			
		||||
@ -234,13 +250,13 @@ class ReconnaissanceEngine:
 | 
			
		||||
        current_depth = 0
 | 
			
		||||
        
 | 
			
		||||
        while current_depth <= self.config.max_depth and targets:
 | 
			
		||||
            logger.info(f"🔄 Processing depth {current_depth} with {len(targets)} targets")
 | 
			
		||||
            logger.info(f"Processing depth {current_depth} with {len(targets)} targets")
 | 
			
		||||
            self._update_progress(f"Processing depth {current_depth} ({len(targets)} targets)", 15 + (current_depth * 25))
 | 
			
		||||
            
 | 
			
		||||
            new_targets = set()
 | 
			
		||||
            
 | 
			
		||||
            for target in targets:
 | 
			
		||||
                logger.debug(f"🎯 Processing target: {target}")
 | 
			
		||||
                logger.debug(f"Processing target: {target}")
 | 
			
		||||
                
 | 
			
		||||
                # DNS resolution and record gathering
 | 
			
		||||
                self._process_single_target(target, current_depth)
 | 
			
		||||
@ -248,25 +264,25 @@ class ReconnaissanceEngine:
 | 
			
		||||
                # Extract new subdomains
 | 
			
		||||
                if current_depth < self.config.max_depth:
 | 
			
		||||
                    new_subdomains = self._extract_new_subdomains(target)
 | 
			
		||||
                    logger.debug(f"🌿 Found {len(new_subdomains)} new subdomains from {target}")
 | 
			
		||||
                    logger.debug(f"Found {len(new_subdomains)} new subdomains from {target}")
 | 
			
		||||
                    
 | 
			
		||||
                    for subdomain in new_subdomains:
 | 
			
		||||
                        self.data.add_hostname(subdomain, current_depth + 1)
 | 
			
		||||
                        new_targets.add(subdomain)
 | 
			
		||||
            
 | 
			
		||||
            logger.info(f"📊 Depth {current_depth} complete. Found {len(new_targets)} new targets for next depth")
 | 
			
		||||
            logger.info(f"Depth {current_depth} complete. Found {len(new_targets)} new targets for next depth")
 | 
			
		||||
            targets = new_targets
 | 
			
		||||
            current_depth += 1
 | 
			
		||||
        
 | 
			
		||||
        logger.info(f"🏁 Recursive processing complete after {current_depth} levels")
 | 
			
		||||
        logger.info(f"Recursive processing complete after {current_depth} levels")
 | 
			
		||||
    
 | 
			
		||||
    def _process_single_target(self, hostname: str, depth: int):
 | 
			
		||||
        """Process a single target hostname."""
 | 
			
		||||
        logger.debug(f"🎯 Processing single target: {hostname} at depth {depth}")
 | 
			
		||||
        logger.debug(f"Processing single target: {hostname} at depth {depth}")
 | 
			
		||||
        
 | 
			
		||||
        # Get all DNS records
 | 
			
		||||
        dns_records = self.dns_resolver.get_all_dns_records(hostname)
 | 
			
		||||
        logger.debug(f"📋 Found {len(dns_records)} DNS records for {hostname}")
 | 
			
		||||
        logger.debug(f"Found {len(dns_records)} DNS records for {hostname}")
 | 
			
		||||
        
 | 
			
		||||
        for record in dns_records:
 | 
			
		||||
            self.data.add_dns_record(hostname, record)
 | 
			
		||||
@ -276,13 +292,13 @@ class ReconnaissanceEngine:
 | 
			
		||||
                self.data.add_ip_address(record.value)
 | 
			
		||||
        
 | 
			
		||||
        # Get certificates
 | 
			
		||||
        logger.debug(f"🔍 Checking certificates for {hostname}")
 | 
			
		||||
        logger.debug(f"Checking certificates for {hostname}")
 | 
			
		||||
        certificates = self.cert_checker.get_certificates(hostname)
 | 
			
		||||
        if certificates:
 | 
			
		||||
            self.data.certificates[hostname] = certificates
 | 
			
		||||
            logger.info(f"📜 Found {len(certificates)} certificates for {hostname}")
 | 
			
		||||
            logger.info(f"Found {len(certificates)} certificates for {hostname}")
 | 
			
		||||
        else:
 | 
			
		||||
            logger.debug(f"❌ No certificates found for {hostname}")
 | 
			
		||||
            logger.debug(f"No certificates found for {hostname}")
 | 
			
		||||
    
 | 
			
		||||
    def _extract_new_subdomains(self, hostname: str) -> Set[str]:
 | 
			
		||||
        """Extract new subdomains from DNS records and certificates."""
 | 
			
		||||
@ -294,7 +310,7 @@ class ReconnaissanceEngine:
 | 
			
		||||
                self.data.dns_records[hostname]
 | 
			
		||||
            )
 | 
			
		||||
            new_subdomains.update(dns_subdomains)
 | 
			
		||||
            logger.debug(f"🌐 Extracted {len(dns_subdomains)} subdomains from DNS records of {hostname}")
 | 
			
		||||
            logger.debug(f"Extracted {len(dns_subdomains)} subdomains from DNS records of {hostname}")
 | 
			
		||||
        
 | 
			
		||||
        # From certificates
 | 
			
		||||
        if hostname in self.data.certificates:
 | 
			
		||||
@ -302,89 +318,89 @@ class ReconnaissanceEngine:
 | 
			
		||||
                self.data.certificates[hostname]
 | 
			
		||||
            )
 | 
			
		||||
            new_subdomains.update(cert_subdomains)
 | 
			
		||||
            logger.debug(f"🔍 Extracted {len(cert_subdomains)} subdomains from certificates of {hostname}")
 | 
			
		||||
            logger.debug(f"Extracted {len(cert_subdomains)} subdomains from certificates of {hostname}")
 | 
			
		||||
        
 | 
			
		||||
        # Filter out already known hostnames
 | 
			
		||||
        filtered_subdomains = new_subdomains - self.data.hostnames
 | 
			
		||||
        logger.debug(f"🆕 {len(filtered_subdomains)} new subdomains after filtering")
 | 
			
		||||
        logger.debug(f"{len(filtered_subdomains)} new subdomains after filtering")
 | 
			
		||||
        
 | 
			
		||||
        return filtered_subdomains
 | 
			
		||||
    
 | 
			
		||||
    def _perform_external_lookups(self):
 | 
			
		||||
        """Perform Shodan and VirusTotal lookups."""
 | 
			
		||||
        logger.info(f"🔍 Starting external lookups for {len(self.data.ip_addresses)} IPs and {len(self.data.hostnames)} hostnames")
 | 
			
		||||
        logger.info(f"Starting external lookups for {len(self.data.ip_addresses)} IPs and {len(self.data.hostnames)} hostnames")
 | 
			
		||||
        
 | 
			
		||||
        # Reverse DNS for all IPs
 | 
			
		||||
        logger.info("🔄 Performing reverse DNS lookups")
 | 
			
		||||
        logger.info("Performing reverse DNS lookups")
 | 
			
		||||
        reverse_dns_count = 0
 | 
			
		||||
        for ip in self.data.ip_addresses:
 | 
			
		||||
            reverse = self.dns_resolver.reverse_dns_lookup(ip)
 | 
			
		||||
            if reverse:
 | 
			
		||||
                self.data.reverse_dns[ip] = reverse
 | 
			
		||||
                reverse_dns_count += 1
 | 
			
		||||
                logger.debug(f"🔙 Reverse DNS for {ip}: {reverse}")
 | 
			
		||||
                logger.debug(f"Reverse DNS for {ip}: {reverse}")
 | 
			
		||||
        
 | 
			
		||||
        logger.info(f"✅ Completed reverse DNS: {reverse_dns_count}/{len(self.data.ip_addresses)} successful")
 | 
			
		||||
        logger.info(f"Completed reverse DNS: {reverse_dns_count}/{len(self.data.ip_addresses)} successful")
 | 
			
		||||
        
 | 
			
		||||
        # Shodan lookups
 | 
			
		||||
        if self.shodan_client:
 | 
			
		||||
            logger.info(f"🕵️ Starting Shodan lookups for {len(self.data.ip_addresses)} IPs")
 | 
			
		||||
            logger.info(f"Starting Shodan lookups for {len(self.data.ip_addresses)} IPs")
 | 
			
		||||
            shodan_success_count = 0
 | 
			
		||||
            
 | 
			
		||||
            for ip in self.data.ip_addresses:
 | 
			
		||||
                try:
 | 
			
		||||
                    logger.debug(f"🔍 Querying Shodan for IP: {ip}")
 | 
			
		||||
                    logger.debug(f"Querying Shodan for IP: {ip}")
 | 
			
		||||
                    result = self.shodan_client.lookup_ip(ip)
 | 
			
		||||
                    if result:
 | 
			
		||||
                        self.data.add_shodan_result(ip, result)
 | 
			
		||||
                        shodan_success_count += 1
 | 
			
		||||
                        logger.info(f"✅ Shodan result for {ip}: {len(result.ports)} ports")
 | 
			
		||||
                        logger.info(f"Shodan result for {ip}: {len(result.ports)} ports")
 | 
			
		||||
                    else:
 | 
			
		||||
                        logger.debug(f"❌ No Shodan data for {ip}")
 | 
			
		||||
                        logger.debug(f"No Shodan data for {ip}")
 | 
			
		||||
                except Exception as e:
 | 
			
		||||
                    logger.warning(f"⚠️ Error querying Shodan for {ip}: {e}")
 | 
			
		||||
                    logger.warning(f"Error querying Shodan for {ip}: {e}")
 | 
			
		||||
            
 | 
			
		||||
            logger.info(f"✅ Shodan lookups complete: {shodan_success_count}/{len(self.data.ip_addresses)} successful")
 | 
			
		||||
            logger.info(f"Shodan lookups complete: {shodan_success_count}/{len(self.data.ip_addresses)} successful")
 | 
			
		||||
        else:
 | 
			
		||||
            logger.info("⚠️ Skipping Shodan lookups (no API key)")
 | 
			
		||||
            logger.info("Skipping Shodan lookups (no API key)")
 | 
			
		||||
        
 | 
			
		||||
        # VirusTotal lookups
 | 
			
		||||
        if self.virustotal_client:
 | 
			
		||||
            total_resources = len(self.data.ip_addresses) + len(self.data.hostnames)
 | 
			
		||||
            logger.info(f"🛡️ Starting VirusTotal lookups for {total_resources} resources")
 | 
			
		||||
            logger.info(f"Starting VirusTotal lookups for {total_resources} resources")
 | 
			
		||||
            vt_success_count = 0
 | 
			
		||||
            
 | 
			
		||||
            # Check IPs
 | 
			
		||||
            for ip in self.data.ip_addresses:
 | 
			
		||||
                try:
 | 
			
		||||
                    logger.debug(f"🔍 Querying VirusTotal for IP: {ip}")
 | 
			
		||||
                    logger.debug(f"Querying VirusTotal for IP: {ip}")
 | 
			
		||||
                    result = self.virustotal_client.lookup_ip(ip)
 | 
			
		||||
                    if result:
 | 
			
		||||
                        self.data.add_virustotal_result(ip, result)
 | 
			
		||||
                        vt_success_count += 1
 | 
			
		||||
                        logger.info(f"🛡️ VirusTotal result for {ip}: {result.positives}/{result.total} detections")
 | 
			
		||||
                        logger.info(f"VirusTotal result for {ip}: {result.positives}/{result.total} detections")
 | 
			
		||||
                    else:
 | 
			
		||||
                        logger.debug(f"❌ No VirusTotal data for {ip}")
 | 
			
		||||
                        logger.debug(f"No VirusTotal data for {ip}")
 | 
			
		||||
                except Exception as e:
 | 
			
		||||
                    logger.warning(f"⚠️ Error querying VirusTotal for IP {ip}: {e}")
 | 
			
		||||
                    logger.warning(f"Error querying VirusTotal for IP {ip}: {e}")
 | 
			
		||||
            
 | 
			
		||||
            # Check domains
 | 
			
		||||
            for hostname in self.data.hostnames:
 | 
			
		||||
                try:
 | 
			
		||||
                    logger.debug(f"🔍 Querying VirusTotal for domain: {hostname}")
 | 
			
		||||
                    logger.debug(f"Querying VirusTotal for domain: {hostname}")
 | 
			
		||||
                    result = self.virustotal_client.lookup_domain(hostname)
 | 
			
		||||
                    if result:
 | 
			
		||||
                        self.data.add_virustotal_result(hostname, result)
 | 
			
		||||
                        vt_success_count += 1
 | 
			
		||||
                        logger.info(f"🛡️ VirusTotal result for {hostname}: {result.positives}/{result.total} detections")
 | 
			
		||||
                        logger.info(f"VirusTotal result for {hostname}: {result.positives}/{result.total} detections")
 | 
			
		||||
                    else:
 | 
			
		||||
                        logger.debug(f"❌ No VirusTotal data for {hostname}")
 | 
			
		||||
                        logger.debug(f"No VirusTotal data for {hostname}")
 | 
			
		||||
                except Exception as e:
 | 
			
		||||
                    logger.warning(f"⚠️ Error querying VirusTotal for domain {hostname}: {e}")
 | 
			
		||||
                    logger.warning(f"Error querying VirusTotal for domain {hostname}: {e}")
 | 
			
		||||
            
 | 
			
		||||
            logger.info(f"✅ VirusTotal lookups complete: {vt_success_count}/{total_resources} successful")
 | 
			
		||||
            logger.info(f"VirusTotal lookups complete: {vt_success_count}/{total_resources} successful")
 | 
			
		||||
        else:
 | 
			
		||||
            logger.info("⚠️ Skipping VirusTotal lookups (no API key)")
 | 
			
		||||
            logger.info("Skipping VirusTotal lookups (no API key)")
 | 
			
		||||
        
 | 
			
		||||
        # Final external lookup summary
 | 
			
		||||
        ext_stats = {
 | 
			
		||||
@ -392,9 +408,4 @@ class ReconnaissanceEngine:
 | 
			
		||||
            'shodan_results': len(self.data.shodan_results),
 | 
			
		||||
            'virustotal_results': len(self.data.virustotal_results)
 | 
			
		||||
        }
 | 
			
		||||
        logger.info(f"📊 External lookups summary: {ext_stats}")
 | 
			
		||||
    
 | 
			
		||||
    # Keep the original method name for backward compatibility
 | 
			
		||||
    def _expand_hostname_to_tlds(self, hostname: str) -> Set[str]:
 | 
			
		||||
        """Legacy method - redirects to smart expansion."""
 | 
			
		||||
        return self._expand_hostname_to_tlds_smart(hostname)
 | 
			
		||||
        logger.info(f"External lookups summary: {ext_stats}")
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user