progress
This commit is contained in:
		
							parent
							
								
									0c9cf00a3b
								
							
						
					
					
						commit
						c105ebbb4b
					
				@ -5,6 +5,7 @@ import requests
 | 
			
		||||
import json
 | 
			
		||||
import time
 | 
			
		||||
import logging
 | 
			
		||||
import socket
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
from typing import List, Optional, Set
 | 
			
		||||
from .data_structures import Certificate
 | 
			
		||||
@ -22,9 +23,52 @@ class CertificateChecker:
 | 
			
		||||
        self.config = config
 | 
			
		||||
        self.last_request = 0
 | 
			
		||||
        self.query_count = 0
 | 
			
		||||
        self.connection_failures = 0
 | 
			
		||||
        self.max_connection_failures = 3  # Stop trying after 3 consecutive failures
 | 
			
		||||
        
 | 
			
		||||
        logger.info("🔐 Certificate checker initialized")
 | 
			
		||||
        
 | 
			
		||||
        # Test connectivity to crt.sh on initialization
 | 
			
		||||
        self._test_connectivity()
 | 
			
		||||
    
 | 
			
		||||
    def _test_connectivity(self):
 | 
			
		||||
        """Test if we can reach crt.sh."""
 | 
			
		||||
        try:
 | 
			
		||||
            logger.info("🔗 Testing connectivity to crt.sh...")
 | 
			
		||||
            
 | 
			
		||||
            # First test DNS resolution
 | 
			
		||||
            try:
 | 
			
		||||
                socket.gethostbyname('crt.sh')
 | 
			
		||||
                logger.debug("✅ DNS resolution for crt.sh successful")
 | 
			
		||||
            except socket.gaierror as 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'},
 | 
			
		||||
                timeout=10,
 | 
			
		||||
                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")
 | 
			
		||||
                return True
 | 
			
		||||
            else:
 | 
			
		||||
                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}")
 | 
			
		||||
            return False
 | 
			
		||||
        except requests.exceptions.Timeout:
 | 
			
		||||
            logger.warning("⚠️ crt.sh connectivity test timed out")
 | 
			
		||||
            return False
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            logger.warning(f"⚠️ Unexpected error testing crt.sh connectivity: {e}")
 | 
			
		||||
            return False
 | 
			
		||||
    
 | 
			
		||||
    def _rate_limit(self):
 | 
			
		||||
        """Apply rate limiting for crt.sh."""
 | 
			
		||||
        now = time.time()
 | 
			
		||||
@ -33,7 +77,7 @@ 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()
 | 
			
		||||
@ -41,7 +85,12 @@ class CertificateChecker:
 | 
			
		||||
    
 | 
			
		||||
    def get_certificates(self, domain: str) -> List[Certificate]:
 | 
			
		||||
        """Get certificates for a domain from crt.sh."""
 | 
			
		||||
        logger.debug(f"🔐 Getting certificates for domain: {domain}")
 | 
			
		||||
        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")
 | 
			
		||||
            return []
 | 
			
		||||
        
 | 
			
		||||
        certificates = []
 | 
			
		||||
        
 | 
			
		||||
@ -49,9 +98,10 @@ class CertificateChecker:
 | 
			
		||||
        domain_certs = self._query_crt_sh(domain)
 | 
			
		||||
        certificates.extend(domain_certs)
 | 
			
		||||
        
 | 
			
		||||
        # Also query for wildcard certificates
 | 
			
		||||
        wildcard_certs = self._query_crt_sh(f"%.{domain}")
 | 
			
		||||
        certificates.extend(wildcard_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)
 | 
			
		||||
        
 | 
			
		||||
        # Remove duplicates based on certificate ID
 | 
			
		||||
        unique_certs = {cert.id: cert for cert in certificates}
 | 
			
		||||
@ -65,13 +115,15 @@ class CertificateChecker:
 | 
			
		||||
        return final_certs
 | 
			
		||||
    
 | 
			
		||||
    def _query_crt_sh(self, query: str) -> List[Certificate]:
 | 
			
		||||
        """Query crt.sh API with retry logic."""
 | 
			
		||||
        """Query crt.sh API with retry logic and better error handling."""
 | 
			
		||||
        certificates = []
 | 
			
		||||
        self._rate_limit()
 | 
			
		||||
        
 | 
			
		||||
        logger.debug(f"📡 Querying crt.sh for: {query}")
 | 
			
		||||
        
 | 
			
		||||
        max_retries = 3
 | 
			
		||||
        max_retries = 2  # Reduced retries for faster failure
 | 
			
		||||
        backoff_delays = [1, 3]  # Shorter delays
 | 
			
		||||
        
 | 
			
		||||
        for attempt in range(max_retries):
 | 
			
		||||
            try:
 | 
			
		||||
                params = {
 | 
			
		||||
@ -111,50 +163,68 @@ class CertificateChecker:
 | 
			
		||||
                                    certificates.append(certificate)
 | 
			
		||||
                                    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}")
 | 
			
		||||
                                logger.debug(f"⚠️ Error parsing certificate data: {e}")
 | 
			
		||||
                                continue  # Skip malformed certificate data
 | 
			
		||||
                        
 | 
			
		||||
                        # Success! Reset connection failure counter
 | 
			
		||||
                        self.connection_failures = 0
 | 
			
		||||
                        logger.info(f"✅ Successfully processed {len(certificates)} certificates from crt.sh for {query}")
 | 
			
		||||
                        return certificates # Success, exit retry loop
 | 
			
		||||
                        return certificates
 | 
			
		||||
                    
 | 
			
		||||
                    except json.JSONDecodeError as e:
 | 
			
		||||
                        logger.warning(f"❌ Invalid JSON response from crt.sh for {query}: {e}")
 | 
			
		||||
                        if attempt < max_retries - 1:
 | 
			
		||||
                            time.sleep(2 ** attempt)  # Exponential backoff
 | 
			
		||||
                            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
 | 
			
		||||
                    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
 | 
			
		||||
                        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(2)
 | 
			
		||||
                        time.sleep(backoff_delays[attempt])
 | 
			
		||||
                        continue
 | 
			
		||||
                    return certificates
 | 
			
		||||
                            
 | 
			
		||||
            except requests.exceptions.Timeout:
 | 
			
		||||
                logger.warning(f"⏱️  crt.sh query timeout for {query} (attempt {attempt+1}/{max_retries})")
 | 
			
		||||
            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}")
 | 
			
		||||
                
 | 
			
		||||
                # Track connection failures
 | 
			
		||||
                if isinstance(e, requests.exceptions.ConnectionError):
 | 
			
		||||
                    self.connection_failures += 1
 | 
			
		||||
                    if self.connection_failures >= self.max_connection_failures:
 | 
			
		||||
                        logger.error(f"❌ Too many connection failures to crt.sh. Disabling certificate lookups.")
 | 
			
		||||
                        return certificates
 | 
			
		||||
                
 | 
			
		||||
                if attempt < max_retries - 1:
 | 
			
		||||
                    time.sleep(2)
 | 
			
		||||
                    time.sleep(backoff_delays[attempt])
 | 
			
		||||
                    continue
 | 
			
		||||
                    
 | 
			
		||||
            except requests.exceptions.RequestException as e:
 | 
			
		||||
                logger.warning(f"🌐 crt.sh network error for {query} (attempt {attempt+1}/{max_retries}): {e}")
 | 
			
		||||
                if attempt < max_retries - 1:
 | 
			
		||||
                    time.sleep(2)
 | 
			
		||||
                    time.sleep(backoff_delays[attempt])
 | 
			
		||||
                    continue
 | 
			
		||||
            except Exception as e:
 | 
			
		||||
                logger.error(f"❌ Unexpected error querying crt.sh for {query}: {e}")
 | 
			
		||||
                if attempt < max_retries - 1:
 | 
			
		||||
                    time.sleep(2)
 | 
			
		||||
                    time.sleep(backoff_delays[attempt])
 | 
			
		||||
                    continue
 | 
			
		||||
        
 | 
			
		||||
        # If we get here, all retries failed
 | 
			
		||||
@ -187,7 +257,7 @@ class CertificateChecker:
 | 
			
		||||
        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]:
 | 
			
		||||
 | 
			
		||||
@ -34,23 +34,31 @@ class ReconnaissanceEngine:
 | 
			
		||||
            self.shodan_client = ShodanClient(config.shodan_key, config)
 | 
			
		||||
            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")
 | 
			
		||||
        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
 | 
			
		||||
        self._lock = threading.Lock()
 | 
			
		||||
        
 | 
			
		||||
        # Shared data object for live updates
 | 
			
		||||
        self.shared_data = None
 | 
			
		||||
    
 | 
			
		||||
    def set_progress_callback(self, callback):
 | 
			
		||||
        """Set callback for progress updates."""
 | 
			
		||||
        self.progress_callback = callback
 | 
			
		||||
    
 | 
			
		||||
    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")
 | 
			
		||||
    
 | 
			
		||||
    def _update_progress(self, message: str, percentage: int = None):
 | 
			
		||||
        """Update progress if callback is set."""
 | 
			
		||||
        logger.info(f"Progress: {message} ({percentage}%)" if percentage else f"Progress: {message}")
 | 
			
		||||
@ -59,7 +67,14 @@ class ReconnaissanceEngine:
 | 
			
		||||
    
 | 
			
		||||
    def run_reconnaissance(self, target: str) -> ReconData:
 | 
			
		||||
        """Run full reconnaissance on target."""
 | 
			
		||||
        self.data = ReconData()
 | 
			
		||||
        # 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")
 | 
			
		||||
        else:
 | 
			
		||||
            self.data = ReconData()
 | 
			
		||||
            logger.info("📊 Created new data object for reconnaissance")
 | 
			
		||||
            
 | 
			
		||||
        self.data.start_time = datetime.now()
 | 
			
		||||
        
 | 
			
		||||
        logger.info(f"🚀 Starting reconnaissance for target: {target}")
 | 
			
		||||
@ -100,7 +115,7 @@ class ReconnaissanceEngine:
 | 
			
		||||
        finally:
 | 
			
		||||
            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
 | 
			
		||||
    
 | 
			
		||||
@ -108,7 +123,7 @@ class ReconnaissanceEngine:
 | 
			
		||||
        """Expand hostname to all possible TLDs."""
 | 
			
		||||
        logger.info(f"🌐 Fetching TLD list for hostname expansion")
 | 
			
		||||
        tlds = self.tld_fetcher.get_tlds()
 | 
			
		||||
        logger.info(f"📝 Testing against {len(tlds)} TLDs")
 | 
			
		||||
        logger.info(f"🔍 Testing against {len(tlds)} TLDs")
 | 
			
		||||
        
 | 
			
		||||
        targets = set()
 | 
			
		||||
        tested_count = 0
 | 
			
		||||
@ -182,7 +197,7 @@ 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
 | 
			
		||||
@ -234,7 +249,7 @@ class ReconnaissanceEngine:
 | 
			
		||||
        
 | 
			
		||||
        # 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:
 | 
			
		||||
@ -248,16 +263,16 @@ class ReconnaissanceEngine:
 | 
			
		||||
                    else:
 | 
			
		||||
                        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")
 | 
			
		||||
        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
 | 
			
		||||
@ -268,11 +283,11 @@ class ReconnaissanceEngine:
 | 
			
		||||
                    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}")
 | 
			
		||||
                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:
 | 
			
		||||
@ -282,15 +297,15 @@ class ReconnaissanceEngine:
 | 
			
		||||
                    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}")
 | 
			
		||||
                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")
 | 
			
		||||
        else:
 | 
			
		||||
            logger.info("⚠️  Skipping VirusTotal lookups (no API key)")
 | 
			
		||||
            logger.info("⚠️ Skipping VirusTotal lookups (no API key)")
 | 
			
		||||
        
 | 
			
		||||
        # Final external lookup summary
 | 
			
		||||
        ext_stats = {
 | 
			
		||||
 | 
			
		||||
@ -8,6 +8,7 @@ import logging
 | 
			
		||||
from .config import Config
 | 
			
		||||
from .reconnaissance import ReconnaissanceEngine
 | 
			
		||||
from .report_generator import ReportGenerator
 | 
			
		||||
from .data_structures import ReconData
 | 
			
		||||
 | 
			
		||||
# Set up logging for this module
 | 
			
		||||
logger = logging.getLogger(__name__)
 | 
			
		||||
@ -46,20 +47,23 @@ def create_app(config: Config):
 | 
			
		||||
            )
 | 
			
		||||
            
 | 
			
		||||
            if not target:
 | 
			
		||||
                logger.warning("⚠️  Scan request missing target")
 | 
			
		||||
                logger.warning("⚠️ Scan request missing target")
 | 
			
		||||
                return jsonify({'error': 'Target is required'}), 400
 | 
			
		||||
            
 | 
			
		||||
            # Generate scan ID
 | 
			
		||||
            scan_id = f"{target}_{int(time.time())}"
 | 
			
		||||
            logger.info(f"🚀 Starting new scan: {scan_id} for target: {target}")
 | 
			
		||||
            
 | 
			
		||||
            # Initialize scan data
 | 
			
		||||
            # Create shared ReconData object for live updates
 | 
			
		||||
            shared_data = ReconData()
 | 
			
		||||
            
 | 
			
		||||
            # Initialize scan data with the shared data object
 | 
			
		||||
            with scan_lock:
 | 
			
		||||
                active_scans[scan_id] = {
 | 
			
		||||
                    'status': 'starting',
 | 
			
		||||
                    'progress': 0,
 | 
			
		||||
                    'message': 'Initializing...',
 | 
			
		||||
                    'data': None,
 | 
			
		||||
                    'data': shared_data,  # Share the data object from the start!
 | 
			
		||||
                    'error': None,
 | 
			
		||||
                    'live_stats': {
 | 
			
		||||
                        'hostnames': 0,
 | 
			
		||||
@ -75,7 +79,7 @@ def create_app(config: Config):
 | 
			
		||||
            # Start reconnaissance in background thread
 | 
			
		||||
            thread = threading.Thread(
 | 
			
		||||
                target=run_reconnaissance_background,
 | 
			
		||||
                args=(scan_id, target, scan_config)
 | 
			
		||||
                args=(scan_id, target, scan_config, shared_data)
 | 
			
		||||
            )
 | 
			
		||||
            thread.daemon = True
 | 
			
		||||
            thread.start()
 | 
			
		||||
@ -118,7 +122,7 @@ def create_app(config: Config):
 | 
			
		||||
            report_gen = ReportGenerator(scan_data['data'])
 | 
			
		||||
            
 | 
			
		||||
            return jsonify({
 | 
			
		||||
                'json_report': scan_data['data'].to_json(),  # This should now work properly
 | 
			
		||||
                'json_report': scan_data['data'].to_json(),
 | 
			
		||||
                'text_report': report_gen.generate_text_report()
 | 
			
		||||
            })
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
@ -134,37 +138,41 @@ def create_app(config: Config):
 | 
			
		||||
            
 | 
			
		||||
            scan_data = active_scans[scan_id]
 | 
			
		||||
            
 | 
			
		||||
            if not scan_data['data']:
 | 
			
		||||
            # Now we always have a data object, even if it's empty initially
 | 
			
		||||
            data_obj = scan_data['data']
 | 
			
		||||
            
 | 
			
		||||
            if not data_obj:
 | 
			
		||||
                return jsonify({
 | 
			
		||||
                    'hostnames': [],
 | 
			
		||||
                    'ip_addresses': [],
 | 
			
		||||
                    'stats': scan_data['live_stats']
 | 
			
		||||
                    'stats': scan_data['live_stats'],
 | 
			
		||||
                    'latest_discoveries': []
 | 
			
		||||
                })
 | 
			
		||||
            
 | 
			
		||||
            # Return current discoveries
 | 
			
		||||
            # Return current discoveries from the shared data object
 | 
			
		||||
            return jsonify({
 | 
			
		||||
                'hostnames': sorted(list(scan_data['data'].hostnames)),
 | 
			
		||||
                'ip_addresses': sorted(list(scan_data['data'].ip_addresses)),
 | 
			
		||||
                'stats': scan_data['data'].get_stats(),
 | 
			
		||||
                'hostnames': sorted(list(data_obj.hostnames)),
 | 
			
		||||
                'ip_addresses': sorted(list(data_obj.ip_addresses)),
 | 
			
		||||
                'stats': data_obj.get_stats(),
 | 
			
		||||
                'latest_discoveries': scan_data.get('latest_discoveries', [])
 | 
			
		||||
            })
 | 
			
		||||
    
 | 
			
		||||
    return app
 | 
			
		||||
 | 
			
		||||
def run_reconnaissance_background(scan_id: str, target: str, config: Config):
 | 
			
		||||
    """Run reconnaissance in background thread."""
 | 
			
		||||
def run_reconnaissance_background(scan_id: str, target: str, config: Config, shared_data: ReconData):
 | 
			
		||||
    """Run reconnaissance in background thread with shared data object."""
 | 
			
		||||
    
 | 
			
		||||
    def update_progress(message: str, percentage: int = None):
 | 
			
		||||
        """Update scan progress."""
 | 
			
		||||
        """Update scan progress and live statistics."""
 | 
			
		||||
        with scan_lock:
 | 
			
		||||
            if scan_id in active_scans:
 | 
			
		||||
                active_scans[scan_id]['message'] = message
 | 
			
		||||
                if percentage is not None:
 | 
			
		||||
                    active_scans[scan_id]['progress'] = percentage
 | 
			
		||||
                
 | 
			
		||||
                # Update live stats if we have data
 | 
			
		||||
                if active_scans[scan_id]['data']:
 | 
			
		||||
                    active_scans[scan_id]['live_stats'] = active_scans[scan_id]['data'].get_stats()
 | 
			
		||||
                # Update live stats from the shared data object
 | 
			
		||||
                if shared_data:
 | 
			
		||||
                    active_scans[scan_id]['live_stats'] = shared_data.get_stats()
 | 
			
		||||
                    
 | 
			
		||||
                    # Add to latest discoveries (keep last 10)
 | 
			
		||||
                    if 'latest_discoveries' not in active_scans[scan_id]:
 | 
			
		||||
@ -188,27 +196,30 @@ def run_reconnaissance_background(scan_id: str, target: str, config: Config):
 | 
			
		||||
        engine = ReconnaissanceEngine(config)
 | 
			
		||||
        engine.set_progress_callback(update_progress)
 | 
			
		||||
        
 | 
			
		||||
        # IMPORTANT: Pass the shared data object to the engine
 | 
			
		||||
        engine.set_shared_data(shared_data)
 | 
			
		||||
        
 | 
			
		||||
        # Update status
 | 
			
		||||
        with scan_lock:
 | 
			
		||||
            active_scans[scan_id]['status'] = 'running'
 | 
			
		||||
        
 | 
			
		||||
        logger.info(f"🚀 Starting reconnaissance for: {target}")
 | 
			
		||||
        
 | 
			
		||||
        # Run reconnaissance
 | 
			
		||||
        data = engine.run_reconnaissance(target)
 | 
			
		||||
        # Run reconnaissance - this will populate the shared_data object incrementally
 | 
			
		||||
        final_data = engine.run_reconnaissance(target)
 | 
			
		||||
        
 | 
			
		||||
        logger.info(f"✅ Reconnaissance completed for scan: {scan_id}")
 | 
			
		||||
        
 | 
			
		||||
        # Update with results
 | 
			
		||||
        # Update with final results (the shared_data should already be populated)
 | 
			
		||||
        with scan_lock:
 | 
			
		||||
            active_scans[scan_id]['status'] = 'completed'
 | 
			
		||||
            active_scans[scan_id]['progress'] = 100
 | 
			
		||||
            active_scans[scan_id]['message'] = 'Reconnaissance completed'
 | 
			
		||||
            active_scans[scan_id]['data'] = data
 | 
			
		||||
            active_scans[scan_id]['live_stats'] = data.get_stats()
 | 
			
		||||
            active_scans[scan_id]['data'] = final_data  # This should be the same as shared_data
 | 
			
		||||
            active_scans[scan_id]['live_stats'] = final_data.get_stats()
 | 
			
		||||
        
 | 
			
		||||
        # Log final statistics
 | 
			
		||||
        final_stats = data.get_stats()
 | 
			
		||||
        final_stats = final_data.get_stats()
 | 
			
		||||
        logger.info(f"📊 Final stats for {scan_id}: {final_stats}")
 | 
			
		||||
    
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										107
									
								
								static/script.js
									
									
									
									
									
								
							
							
						
						
									
										107
									
								
								static/script.js
									
									
									
									
									
								
							@ -1,4 +1,4 @@
 | 
			
		||||
// DNS Reconnaissance Tool - Enhanced Frontend JavaScript with Real-time Updates
 | 
			
		||||
// DNS Reconnaissance Tool - Enhanced Frontend JavaScript with Debug Output
 | 
			
		||||
 | 
			
		||||
class ReconTool {
 | 
			
		||||
    constructor() {
 | 
			
		||||
@ -6,9 +6,20 @@ class ReconTool {
 | 
			
		||||
        this.pollInterval = null;
 | 
			
		||||
        this.liveDataInterval = null;
 | 
			
		||||
        this.currentReport = null;
 | 
			
		||||
        this.debugMode = true; // Enable debug logging
 | 
			
		||||
        this.init();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    debug(message, data = null) {
 | 
			
		||||
        if (this.debugMode) {
 | 
			
		||||
            if (data) {
 | 
			
		||||
                console.log(`🔍 DEBUG: ${message}`, data);
 | 
			
		||||
            } else {
 | 
			
		||||
                console.log(`🔍 DEBUG: ${message}`);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    init() {
 | 
			
		||||
        this.bindEvents();
 | 
			
		||||
        this.setupRealtimeElements();
 | 
			
		||||
@ -67,6 +78,7 @@ class ReconTool {
 | 
			
		||||
                </div>
 | 
			
		||||
            `;
 | 
			
		||||
            progressSection.appendChild(liveDiv);
 | 
			
		||||
            this.debug("Live discoveries container created");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -127,7 +139,7 @@ class ReconTool {
 | 
			
		||||
            this.showProgressSection();
 | 
			
		||||
            this.updateProgress(0, 'Starting scan...');
 | 
			
		||||
 | 
			
		||||
            console.log('🚀 Starting scan with data:', scanData);
 | 
			
		||||
            this.debug('Starting scan with data:', scanData);
 | 
			
		||||
 | 
			
		||||
            const response = await fetch('/api/scan', {
 | 
			
		||||
                method: 'POST',
 | 
			
		||||
@ -148,7 +160,7 @@ class ReconTool {
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.currentScanId = result.scan_id;
 | 
			
		||||
            console.log('✅ Scan started with ID:', this.currentScanId);
 | 
			
		||||
            this.debug('Scan started with ID:', this.currentScanId);
 | 
			
		||||
            
 | 
			
		||||
            this.startPolling();
 | 
			
		||||
            this.startLiveDataPolling();
 | 
			
		||||
@ -160,6 +172,7 @@ class ReconTool {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    startPolling() {
 | 
			
		||||
        this.debug('Starting status polling...');
 | 
			
		||||
        // Poll every 2 seconds for status updates
 | 
			
		||||
        this.pollInterval = setInterval(() => {
 | 
			
		||||
            this.checkScanStatus();
 | 
			
		||||
@ -170,6 +183,7 @@ class ReconTool {
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    startLiveDataPolling() {
 | 
			
		||||
        this.debug('Starting live data polling...');
 | 
			
		||||
        // Poll every 3 seconds for live data updates
 | 
			
		||||
        this.liveDataInterval = setInterval(() => {
 | 
			
		||||
            this.updateLiveData();
 | 
			
		||||
@ -179,6 +193,9 @@ class ReconTool {
 | 
			
		||||
        const liveSection = document.querySelector('.live-discoveries');
 | 
			
		||||
        if (liveSection) {
 | 
			
		||||
            liveSection.style.display = 'block';
 | 
			
		||||
            this.debug('Live discoveries section made visible');
 | 
			
		||||
        } else {
 | 
			
		||||
            this.debug('ERROR: Live discoveries section not found!');
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // Also update immediately
 | 
			
		||||
@ -208,12 +225,13 @@ class ReconTool {
 | 
			
		||||
            
 | 
			
		||||
            // Update live stats
 | 
			
		||||
            if (status.live_stats) {
 | 
			
		||||
                this.debug('Received live stats:', status.live_stats);
 | 
			
		||||
                this.updateLiveStats(status.live_stats);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Check if completed
 | 
			
		||||
            if (status.status === 'completed') {
 | 
			
		||||
                console.log('✅ Scan completed');
 | 
			
		||||
                this.debug('Scan completed, loading report...');
 | 
			
		||||
                this.stopPolling();
 | 
			
		||||
                await this.loadScanReport();
 | 
			
		||||
            } else if (status.status === 'error') {
 | 
			
		||||
@ -233,29 +251,37 @@ class ReconTool {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        this.debug(`Fetching live data for scan: ${this.currentScanId}`);
 | 
			
		||||
        
 | 
			
		||||
        try {
 | 
			
		||||
            const response = await fetch(`/api/scan/${this.currentScanId}/live-data`);
 | 
			
		||||
            
 | 
			
		||||
            if (!response.ok) {
 | 
			
		||||
                this.debug(`Live data request failed: HTTP ${response.status}`);
 | 
			
		||||
                return; // Silently fail for live data
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            const data = await response.json();
 | 
			
		||||
            
 | 
			
		||||
            if (data.error) {
 | 
			
		||||
                this.debug('Live data error:', data.error);
 | 
			
		||||
                return; // Silently fail for live data
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            this.debug('Received live data:', data);
 | 
			
		||||
            
 | 
			
		||||
            // Update live discoveries
 | 
			
		||||
            this.updateLiveDiscoveries(data);
 | 
			
		||||
            
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            // Silently fail for live data updates
 | 
			
		||||
            console.debug('Live data update failed:', error);
 | 
			
		||||
            this.debug('Live data update failed:', error);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    updateLiveStats(stats) {
 | 
			
		||||
        this.debug('Updating live stats:', stats);
 | 
			
		||||
        
 | 
			
		||||
        // Update the live statistics counters
 | 
			
		||||
        const statElements = {
 | 
			
		||||
            'liveHostnames': stats.hostnames || 0,
 | 
			
		||||
@ -269,54 +295,69 @@ class ReconTool {
 | 
			
		||||
        Object.entries(statElements).forEach(([elementId, value]) => {
 | 
			
		||||
            const element = document.getElementById(elementId);
 | 
			
		||||
            if (element) {
 | 
			
		||||
                const currentValue = element.textContent;
 | 
			
		||||
                element.textContent = value;
 | 
			
		||||
                
 | 
			
		||||
                // Add a brief highlight effect when value changes
 | 
			
		||||
                if (element.textContent !== value.toString()) {
 | 
			
		||||
                if (currentValue !== value.toString()) {
 | 
			
		||||
                    this.debug(`Updated ${elementId}: ${currentValue} -> ${value}`);
 | 
			
		||||
                    // Add a brief highlight effect when value changes
 | 
			
		||||
                    element.style.backgroundColor = '#ff9900';
 | 
			
		||||
                    setTimeout(() => {
 | 
			
		||||
                        element.style.backgroundColor = '';
 | 
			
		||||
                    }, 1000);
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                this.debug(`ERROR: Element ${elementId} not found!`);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    updateLiveDiscoveries(data) {
 | 
			
		||||
        this.debug('Updating live discoveries with data:', data);
 | 
			
		||||
        
 | 
			
		||||
        // Update hostnames list
 | 
			
		||||
        const hostnameList = document.querySelector('#recentHostnames .hostname-list');
 | 
			
		||||
        if (hostnameList && data.hostnames) {
 | 
			
		||||
        if (hostnameList && data.hostnames && data.hostnames.length > 0) {
 | 
			
		||||
            // Show last 10 hostnames
 | 
			
		||||
            const recentHostnames = data.hostnames.slice(-10);
 | 
			
		||||
            hostnameList.innerHTML = recentHostnames.map(hostname => 
 | 
			
		||||
                `<span class="discovery-item">${hostname}</span>`
 | 
			
		||||
            ).join('');
 | 
			
		||||
            this.debug(`Updated hostname list with ${recentHostnames.length} items`);
 | 
			
		||||
        } else if (hostnameList) {
 | 
			
		||||
            this.debug(`No hostnames to display (${data.hostnames ? data.hostnames.length : 0} total)`);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // Update IP addresses list
 | 
			
		||||
        const ipList = document.querySelector('#recentIPs .ip-list');
 | 
			
		||||
        if (ipList && data.ip_addresses) {
 | 
			
		||||
        if (ipList && data.ip_addresses && data.ip_addresses.length > 0) {
 | 
			
		||||
            // Show last 10 IPs
 | 
			
		||||
            const recentIPs = data.ip_addresses.slice(-10);
 | 
			
		||||
            ipList.innerHTML = recentIPs.map(ip => 
 | 
			
		||||
                `<span class="discovery-item">${ip}</span>`
 | 
			
		||||
            ).join('');
 | 
			
		||||
            this.debug(`Updated IP list with ${recentIPs.length} items`);
 | 
			
		||||
        } else if (ipList) {
 | 
			
		||||
            this.debug(`No IPs to display (${data.ip_addresses ? data.ip_addresses.length : 0} total)`);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // Update activity log
 | 
			
		||||
        const activityList = document.querySelector('#activityLog .activity-list');
 | 
			
		||||
        if (activityList && data.latest_discoveries) {
 | 
			
		||||
        if (activityList && data.latest_discoveries && data.latest_discoveries.length > 0) {
 | 
			
		||||
            const activities = data.latest_discoveries.slice(-5); // Last 5 activities
 | 
			
		||||
            activityList.innerHTML = activities.map(activity => {
 | 
			
		||||
                const time = new Date(activity.timestamp * 1000).toLocaleTimeString();
 | 
			
		||||
                return `<div class="activity-item">[${time}] ${activity.message}</div>`;
 | 
			
		||||
            }).join('');
 | 
			
		||||
            this.debug(`Updated activity log with ${activities.length} items`);
 | 
			
		||||
        } else if (activityList) {
 | 
			
		||||
            this.debug(`No activities to display (${data.latest_discoveries ? data.latest_discoveries.length : 0} total)`);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async loadScanReport() {
 | 
			
		||||
        try {
 | 
			
		||||
            console.log('📄 Loading scan report...');
 | 
			
		||||
            this.debug('Loading scan report...');
 | 
			
		||||
            const response = await fetch(`/api/scan/${this.currentScanId}/report`);
 | 
			
		||||
            
 | 
			
		||||
            if (!response.ok) {
 | 
			
		||||
@ -330,7 +371,7 @@ class ReconTool {
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.currentReport = report;
 | 
			
		||||
            console.log('✅ Report loaded successfully');
 | 
			
		||||
            this.debug('Report loaded successfully');
 | 
			
		||||
            this.showResultsSection();
 | 
			
		||||
            this.showReport('text'); // Default to text view
 | 
			
		||||
 | 
			
		||||
@ -341,6 +382,7 @@ class ReconTool {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    stopPolling() {
 | 
			
		||||
        this.debug('Stopping polling intervals...');
 | 
			
		||||
        if (this.pollInterval) {
 | 
			
		||||
            clearInterval(this.pollInterval);
 | 
			
		||||
            this.pollInterval = null;
 | 
			
		||||
@ -355,18 +397,34 @@ class ReconTool {
 | 
			
		||||
        document.getElementById('scanForm').style.display = 'none';
 | 
			
		||||
        document.getElementById('progressSection').style.display = 'block';
 | 
			
		||||
        document.getElementById('resultsSection').style.display = 'none';
 | 
			
		||||
        this.debug('Showing progress section');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    showResultsSection() {
 | 
			
		||||
        document.getElementById('scanForm').style.display = 'none';
 | 
			
		||||
        document.getElementById('progressSection').style.display = 'none';
 | 
			
		||||
        document.getElementById('progressSection').style.display = 'block'; // Keep visible
 | 
			
		||||
        document.getElementById('resultsSection').style.display = 'block';
 | 
			
		||||
        
 | 
			
		||||
        // Hide live discoveries in results section
 | 
			
		||||
        // Change the title to show it's the final summary
 | 
			
		||||
        const liveSection = document.querySelector('.live-discoveries');
 | 
			
		||||
        if (liveSection) {
 | 
			
		||||
            liveSection.style.display = 'none';
 | 
			
		||||
            const title = liveSection.querySelector('h3');
 | 
			
		||||
            if (title) {
 | 
			
		||||
                title.textContent = '📊 Final Discovery Summary';
 | 
			
		||||
            }
 | 
			
		||||
            liveSection.style.display = 'block';
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // Hide just the progress bar and scan controls
 | 
			
		||||
        const progressBar = document.querySelector('.progress-bar');
 | 
			
		||||
        const progressMessage = document.getElementById('progressMessage');
 | 
			
		||||
        const scanControls = document.querySelector('.scan-controls');
 | 
			
		||||
        
 | 
			
		||||
        if (progressBar) progressBar.style.display = 'none';
 | 
			
		||||
        if (progressMessage) progressMessage.style.display = 'none';
 | 
			
		||||
        if (scanControls) scanControls.style.display = 'none';
 | 
			
		||||
        
 | 
			
		||||
        this.debug('Showing results section with live discoveries');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    resetToForm() {
 | 
			
		||||
@ -378,10 +436,23 @@ class ReconTool {
 | 
			
		||||
        document.getElementById('progressSection').style.display = 'none';
 | 
			
		||||
        document.getElementById('resultsSection').style.display = 'none';
 | 
			
		||||
        
 | 
			
		||||
        // Hide live discoveries
 | 
			
		||||
        // Show progress elements again
 | 
			
		||||
        const progressBar = document.querySelector('.progress-bar');
 | 
			
		||||
        const progressMessage = document.getElementById('progressMessage');
 | 
			
		||||
        const scanControls = document.querySelector('.scan-controls');
 | 
			
		||||
        
 | 
			
		||||
        if (progressBar) progressBar.style.display = 'block';
 | 
			
		||||
        if (progressMessage) progressMessage.style.display = 'block';
 | 
			
		||||
        if (scanControls) scanControls.style.display = 'block';
 | 
			
		||||
        
 | 
			
		||||
        // Hide live discoveries and reset title
 | 
			
		||||
        const liveSection = document.querySelector('.live-discoveries');
 | 
			
		||||
        if (liveSection) {
 | 
			
		||||
            liveSection.style.display = 'none';
 | 
			
		||||
            const title = liveSection.querySelector('h3');
 | 
			
		||||
            if (title) {
 | 
			
		||||
                title.textContent = '🔍 Live Discoveries';
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // Clear form
 | 
			
		||||
@ -389,6 +460,8 @@ class ReconTool {
 | 
			
		||||
        document.getElementById('shodanKey').value = '';
 | 
			
		||||
        document.getElementById('virustotalKey').value = '';
 | 
			
		||||
        document.getElementById('maxDepth').value = '2';
 | 
			
		||||
        
 | 
			
		||||
        this.debug('Reset to form view');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    updateProgress(percentage, message) {
 | 
			
		||||
@ -477,6 +550,6 @@ class ReconTool {
 | 
			
		||||
 | 
			
		||||
// Initialize the application when DOM is loaded
 | 
			
		||||
document.addEventListener('DOMContentLoaded', () => {
 | 
			
		||||
    console.log('🌐 DNS Reconnaissance Tool initialized');
 | 
			
		||||
    console.log('🌐 DNS Reconnaissance Tool initialized with debug mode');
 | 
			
		||||
    new ReconTool();
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										181
									
								
								static/style.css
									
									
									
									
									
								
							
							
						
						
									
										181
									
								
								static/style.css
									
									
									
									
									
								
							@ -211,7 +211,6 @@ header p {
 | 
			
		||||
    word-wrap: break-word;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.hostname-list, .ip-list {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-wrap: wrap;
 | 
			
		||||
@ -245,65 +244,7 @@ header p {
 | 
			
		||||
    border-bottom: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Responsive design adjustments */
 | 
			
		||||
@media (max-width: 768px) {
 | 
			
		||||
    .container {
 | 
			
		||||
        padding: 10px;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    header h1 {
 | 
			
		||||
        font-size: 2.2rem;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .scan-form, .progress-section, .results-section {
 | 
			
		||||
        padding: 20px;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .btn-primary, .btn-secondary {
 | 
			
		||||
        width: 100%;
 | 
			
		||||
        margin-right: 0;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .results-controls {
 | 
			
		||||
        display: flex;
 | 
			
		||||
        flex-wrap: wrap;
 | 
			
		||||
        justify-content: center;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .results-controls button {
 | 
			
		||||
        flex: 1;
 | 
			
		||||
        min-width: 120px;
 | 
			
		||||
    }
 | 
			
		||||
    .stats-grid {
 | 
			
		||||
        grid-template-columns: repeat(2, 1fr);
 | 
			
		||||
        gap: 10px;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .stat-item {
 | 
			
		||||
        padding: 6px 8px;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .stat-label, .stat-value {
 | 
			
		||||
        font-size: 0.8rem;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .hostname-list, .ip-list {
 | 
			
		||||
        flex-direction: column;
 | 
			
		||||
        align-items: flex-start;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Tactical loading spinner */
 | 
			
		||||
.loading {
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    width: 20px;
 | 
			
		||||
    height: 20px;
 | 
			
		||||
    border: 3px solid rgba(199, 199, 199, 0.3);
 | 
			
		||||
    border-radius: 50%;
 | 
			
		||||
    border-top-color: #00ff41; /* Night-vision green spinner */
 | 
			
		||||
    animation: spin 1s linear infinite;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Live Discoveries Base Styling */
 | 
			
		||||
.live-discoveries {
 | 
			
		||||
    background: rgba(0, 20, 0, 0.6);
 | 
			
		||||
    border: 1px solid #00ff41;
 | 
			
		||||
@ -319,6 +260,44 @@ header p {
 | 
			
		||||
    letter-spacing: 1px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Enhanced styling for live discoveries when shown in results view */
 | 
			
		||||
.results-section .live-discoveries {
 | 
			
		||||
    background: rgba(0, 40, 0, 0.8);
 | 
			
		||||
    border: 2px solid #00ff41;
 | 
			
		||||
    border-radius: 4px;
 | 
			
		||||
    padding: 20px;
 | 
			
		||||
    margin-bottom: 25px;
 | 
			
		||||
    box-shadow: 0 0 10px rgba(0, 255, 65, 0.3);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.results-section .live-discoveries h3 {
 | 
			
		||||
    color: #00ff41;
 | 
			
		||||
    text-shadow: 0 0 3px rgba(0, 255, 65, 0.5);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Ensure the progress section flows nicely when showing both progress and results */
 | 
			
		||||
.progress-section.with-results {
 | 
			
		||||
    margin-bottom: 0;
 | 
			
		||||
    border-bottom: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.results-section.with-live-data {
 | 
			
		||||
    border-top: 1px solid #444;
 | 
			
		||||
    padding-top: 20px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Better spacing for the combined view */
 | 
			
		||||
.progress-section + .results-section {
 | 
			
		||||
    margin-top: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Hide specific progress elements while keeping the section visible */
 | 
			
		||||
.progress-section .progress-bar.hidden,
 | 
			
		||||
.progress-section #progressMessage.hidden,
 | 
			
		||||
.progress-section .scan-controls.hidden {
 | 
			
		||||
    display: none !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.stats-grid {
 | 
			
		||||
    display: grid;
 | 
			
		||||
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
 | 
			
		||||
@ -348,6 +327,16 @@ header p {
 | 
			
		||||
    transition: background-color 0.3s ease;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Animation for final stats highlight */
 | 
			
		||||
@keyframes finalHighlight {
 | 
			
		||||
    0% { background-color: #ff9900; }
 | 
			
		||||
    100% { background-color: transparent; }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.stat-value.final {
 | 
			
		||||
    animation: finalHighlight 2s ease-in-out;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.discoveries-list {
 | 
			
		||||
    margin-top: 20px;
 | 
			
		||||
}
 | 
			
		||||
@ -374,9 +363,77 @@ header p {
 | 
			
		||||
    font-size: 0.9rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/* Tactical loading spinner */
 | 
			
		||||
.loading {
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    width: 20px;
 | 
			
		||||
    height: 20px;
 | 
			
		||||
    border: 3px solid rgba(199, 199, 199, 0.3);
 | 
			
		||||
    border-radius: 50%;
 | 
			
		||||
    border-top-color: #00ff41; /* Night-vision green spinner */
 | 
			
		||||
    animation: spin 1s linear infinite;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes spin {
 | 
			
		||||
    to { transform: rotate(360deg); }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Responsive design adjustments */
 | 
			
		||||
@media (max-width: 768px) {
 | 
			
		||||
    .container {
 | 
			
		||||
        padding: 10px;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    header h1 {
 | 
			
		||||
        font-size: 2.2rem;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .scan-form, .progress-section, .results-section {
 | 
			
		||||
        padding: 20px;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .btn-primary, .btn-secondary {
 | 
			
		||||
        width: 100%;
 | 
			
		||||
        margin-right: 0;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .results-controls {
 | 
			
		||||
        display: flex;
 | 
			
		||||
        flex-wrap: wrap;
 | 
			
		||||
        justify-content: center;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .results-controls button {
 | 
			
		||||
        flex: 1;
 | 
			
		||||
        min-width: 120px;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .stats-grid {
 | 
			
		||||
        grid-template-columns: repeat(2, 1fr);
 | 
			
		||||
        gap: 10px;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .stat-item {
 | 
			
		||||
        padding: 6px 8px;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .stat-label, .stat-value {
 | 
			
		||||
        font-size: 0.8rem;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .hostname-list, .ip-list {
 | 
			
		||||
        flex-direction: column;
 | 
			
		||||
        align-items: flex-start;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* Responsive adjustments for the combined view */
 | 
			
		||||
    .results-section .live-discoveries {
 | 
			
		||||
        padding: 15px;
 | 
			
		||||
        margin-bottom: 15px;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .results-section .live-discoveries .stats-grid {
 | 
			
		||||
        grid-template-columns: repeat(2, 1fr);
 | 
			
		||||
        gap: 10px;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user