flask app
This commit is contained in:
		
							parent
							
								
									941d815595
								
							
						
					
					
						commit
						8263f5cfa9
					
				
							
								
								
									
										107
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										107
									
								
								README.md
									
									
									
									
									
								
							@ -1,2 +1,107 @@
 | 
			
		||||
# dnsrecon
 | 
			
		||||
# DNS Reconnaissance Tool
 | 
			
		||||
 | 
			
		||||
A comprehensive DNS reconnaissance tool designed for investigators to gather intelligence on hostnames and IP addresses through multiple data sources.
 | 
			
		||||
 | 
			
		||||
## Features
 | 
			
		||||
 | 
			
		||||
- **DNS Resolution**: Query multiple DNS servers (1.1.1.1, 8.8.8.8, 9.9.9.9)
 | 
			
		||||
- **TLD Expansion**: Automatically try all IANA TLDs for hostname-only inputs
 | 
			
		||||
- **Certificate Transparency**: Query crt.sh for SSL certificate information
 | 
			
		||||
- **Recursive Discovery**: Automatically discover and analyze subdomains
 | 
			
		||||
- **External Intelligence**: Optional Shodan and VirusTotal integration
 | 
			
		||||
- **Multiple Interfaces**: Both CLI and web interface available
 | 
			
		||||
- **Comprehensive Reports**: JSON and text output formats
 | 
			
		||||
 | 
			
		||||
## Installation
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
# Clone or create the project structure
 | 
			
		||||
mkdir dns-recon-tool && cd dns-recon-tool
 | 
			
		||||
 | 
			
		||||
# Install dependencies
 | 
			
		||||
pip install -r requirements.txt
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Usage
 | 
			
		||||
 | 
			
		||||
### Command Line Interface
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
# Basic domain scan
 | 
			
		||||
python -m src.main example.com
 | 
			
		||||
 | 
			
		||||
# Try all TLDs for hostname
 | 
			
		||||
python -m src.main example
 | 
			
		||||
 | 
			
		||||
# With API keys and custom depth
 | 
			
		||||
python -m src.main example.com --shodan-key YOUR_KEY --virustotal-key YOUR_KEY --max-depth 3
 | 
			
		||||
 | 
			
		||||
# Save reports
 | 
			
		||||
python -m src.main example.com --output results
 | 
			
		||||
 | 
			
		||||
# JSON only output
 | 
			
		||||
python -m src.main example.com --json-only
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Web Interface
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
# Start web server
 | 
			
		||||
python -m src.main --web
 | 
			
		||||
 | 
			
		||||
# Custom port
 | 
			
		||||
python -m src.main --web --port 8080
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Then open http://localhost:5000 in your browser.
 | 
			
		||||
 | 
			
		||||
## Configuration
 | 
			
		||||
 | 
			
		||||
The tool uses the following default settings:
 | 
			
		||||
- DNS Servers: 1.1.1.1, 8.8.8.8, 9.9.9.9
 | 
			
		||||
- Max Recursion Depth: 2
 | 
			
		||||
- Rate Limits: DNS (10/s), crt.sh (2/s), Shodan (0.5/s), VirusTotal (0.25/s)
 | 
			
		||||
 | 
			
		||||
## API Keys
 | 
			
		||||
 | 
			
		||||
For enhanced reconnaissance, obtain API keys from:
 | 
			
		||||
- [Shodan](https://shodan.io) - Port scanning and service detection
 | 
			
		||||
- [VirusTotal](https://virustotal.com) - Security analysis and reputation
 | 
			
		||||
 | 
			
		||||
## Output
 | 
			
		||||
 | 
			
		||||
The tool generates two types of reports:
 | 
			
		||||
 | 
			
		||||
### JSON Report
 | 
			
		||||
Complete machine-readable data including:
 | 
			
		||||
- All discovered hostnames and IPs
 | 
			
		||||
- DNS records by type
 | 
			
		||||
- Certificate information
 | 
			
		||||
- External service results
 | 
			
		||||
- Metadata and timing
 | 
			
		||||
 | 
			
		||||
### Text Report
 | 
			
		||||
Human-readable summary with:
 | 
			
		||||
- Executive summary
 | 
			
		||||
- Hostnames by discovery depth
 | 
			
		||||
- IP address analysis
 | 
			
		||||
- DNS record details
 | 
			
		||||
- Certificate analysis
 | 
			
		||||
- Security findings
 | 
			
		||||
 | 
			
		||||
## Architecture
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
src/
 | 
			
		||||
├── main.py              # CLI entry point
 | 
			
		||||
├── web_app.py           # Flask web interface  
 | 
			
		||||
├── config.py            # Configuration management
 | 
			
		||||
├── data_structures.py   # Data models
 | 
			
		||||
├── dns_resolver.py      # DNS functionality
 | 
			
		||||
├── certificate_checker.py # crt.sh integration
 | 
			
		||||
├── shodan_client.py     # Shodan API
 | 
			
		||||
├── virustotal_client.py # VirusTotal API
 | 
			
		||||
├── tld_fetcher.py       # IANA TLD handling
 | 
			
		||||
├── reconnaissance.py    # Main logic
 | 
			
		||||
└── report_generator.py  # Report generation
 | 
			
		||||
```
 | 
			
		||||
							
								
								
									
										4
									
								
								requirements.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								requirements.txt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,4 @@
 | 
			
		||||
requests>=2.31.0
 | 
			
		||||
flask>=2.3.0
 | 
			
		||||
dnspython>=2.4.0
 | 
			
		||||
click>=8.1.0
 | 
			
		||||
							
								
								
									
										20
									
								
								src/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/__init__.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,20 @@
 | 
			
		||||
# File: src/__init__.py
 | 
			
		||||
"""DNS Reconnaissance Tool Package."""
 | 
			
		||||
 | 
			
		||||
__version__ = "1.0.0"
 | 
			
		||||
__author__ = "DNS Recon Tool"
 | 
			
		||||
__email__ = ""
 | 
			
		||||
__description__ = "A comprehensive DNS reconnaissance tool for investigators"
 | 
			
		||||
 | 
			
		||||
from .main import main
 | 
			
		||||
from .config import Config
 | 
			
		||||
from .reconnaissance import ReconnaissanceEngine
 | 
			
		||||
from .data_structures import ReconData
 | 
			
		||||
 | 
			
		||||
__all__ = [
 | 
			
		||||
    'main',
 | 
			
		||||
    'Config', 
 | 
			
		||||
    'ReconnaissanceEngine',
 | 
			
		||||
    'ReconData'
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										122
									
								
								src/certificate_checker.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								src/certificate_checker.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,122 @@
 | 
			
		||||
# File: src/certificate_checker.py
 | 
			
		||||
"""Certificate transparency log checker using crt.sh."""
 | 
			
		||||
 | 
			
		||||
import requests
 | 
			
		||||
import json
 | 
			
		||||
import time
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
from typing import List, Optional, Set
 | 
			
		||||
from .data_structures import Certificate
 | 
			
		||||
from .config import Config
 | 
			
		||||
 | 
			
		||||
class CertificateChecker:
 | 
			
		||||
    """Check certificates using crt.sh."""
 | 
			
		||||
    
 | 
			
		||||
    CRT_SH_URL = "https://crt.sh/"
 | 
			
		||||
    
 | 
			
		||||
    def __init__(self, config: Config):
 | 
			
		||||
        self.config = config
 | 
			
		||||
        self.last_request = 0
 | 
			
		||||
    
 | 
			
		||||
    def _rate_limit(self):
 | 
			
		||||
        """Apply rate limiting for crt.sh."""
 | 
			
		||||
        now = time.time()
 | 
			
		||||
        time_since_last = now - self.last_request
 | 
			
		||||
        min_interval = 1.0 / self.config.CRT_SH_RATE_LIMIT
 | 
			
		||||
        
 | 
			
		||||
        if time_since_last < min_interval:
 | 
			
		||||
            time.sleep(min_interval - time_since_last)
 | 
			
		||||
        
 | 
			
		||||
        self.last_request = time.time()
 | 
			
		||||
    
 | 
			
		||||
    def get_certificates(self, domain: str) -> List[Certificate]:
 | 
			
		||||
        """Get certificates for a domain from crt.sh."""
 | 
			
		||||
        certificates = []
 | 
			
		||||
        
 | 
			
		||||
        # Query for the domain
 | 
			
		||||
        certificates.extend(self._query_crt_sh(domain))
 | 
			
		||||
        
 | 
			
		||||
        # Also query for wildcard certificates
 | 
			
		||||
        certificates.extend(self._query_crt_sh(f"%.{domain}"))
 | 
			
		||||
        
 | 
			
		||||
        # Remove duplicates based on certificate ID
 | 
			
		||||
        unique_certs = {cert.id: cert for cert in certificates}
 | 
			
		||||
        return list(unique_certs.values())
 | 
			
		||||
    
 | 
			
		||||
    def _query_crt_sh(self, query: str) -> List[Certificate]:
 | 
			
		||||
        """Query crt.sh API with retry logic."""
 | 
			
		||||
        certificates = []
 | 
			
		||||
        self._rate_limit()
 | 
			
		||||
        
 | 
			
		||||
        max_retries = 3
 | 
			
		||||
        for attempt in range(max_retries):
 | 
			
		||||
            try:
 | 
			
		||||
                params = {
 | 
			
		||||
                    'q': query,
 | 
			
		||||
                    'output': 'json'
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                response = requests.get(
 | 
			
		||||
                    self.CRT_SH_URL,
 | 
			
		||||
                    params=params,
 | 
			
		||||
                    timeout=self.config.HTTP_TIMEOUT
 | 
			
		||||
                )
 | 
			
		||||
                
 | 
			
		||||
                if response.status_code == 200:
 | 
			
		||||
                    data = response.json()
 | 
			
		||||
                    for cert_data in data:
 | 
			
		||||
                        try:
 | 
			
		||||
                            certificate = Certificate(
 | 
			
		||||
                                id=cert_data.get('id'),
 | 
			
		||||
                                issuer=cert_data.get('issuer_name', ''),
 | 
			
		||||
                                subject=cert_data.get('name_value', ''),
 | 
			
		||||
                                not_before=datetime.fromisoformat(
 | 
			
		||||
                                    cert_data.get('not_before', '').replace('Z', '+00:00')
 | 
			
		||||
                                ),
 | 
			
		||||
                                not_after=datetime.fromisoformat(
 | 
			
		||||
                                    cert_data.get('not_after', '').replace('Z', '+00:00')
 | 
			
		||||
                                ),
 | 
			
		||||
                                is_wildcard='*.' in cert_data.get('name_value', '')
 | 
			
		||||
                            )
 | 
			
		||||
                            certificates.append(certificate)
 | 
			
		||||
                        except (ValueError, TypeError):
 | 
			
		||||
                            continue  # Skip malformed certificate data
 | 
			
		||||
                    return certificates # Success, exit retry loop
 | 
			
		||||
                            
 | 
			
		||||
            except requests.exceptions.RequestException as e:
 | 
			
		||||
                print(f"Error querying crt.sh for {query} (attempt {attempt+1}/{max_retries}): {e}")
 | 
			
		||||
                if attempt < max_retries - 1:
 | 
			
		||||
                    time.sleep(2) # Wait 2 seconds before retrying
 | 
			
		||||
                continue
 | 
			
		||||
        
 | 
			
		||||
        return certificates # Return what we have after all retries
 | 
			
		||||
    
 | 
			
		||||
    def extract_subdomains_from_certificates(self, certificates: List[Certificate]) -> Set[str]:
 | 
			
		||||
        """Extract subdomains from certificate subjects."""
 | 
			
		||||
        subdomains = set()
 | 
			
		||||
        
 | 
			
		||||
        for cert in certificates:
 | 
			
		||||
            # Parse subject field for domain names
 | 
			
		||||
            subjects = cert.subject.split('\n')
 | 
			
		||||
            for subject in subjects:
 | 
			
		||||
                subject = subject.strip()
 | 
			
		||||
                
 | 
			
		||||
                # Skip wildcard domains for recursion
 | 
			
		||||
                if not subject.startswith('*.'):
 | 
			
		||||
                    if self._is_valid_domain(subject):
 | 
			
		||||
                        subdomains.add(subject.lower())
 | 
			
		||||
        
 | 
			
		||||
        return subdomains
 | 
			
		||||
    
 | 
			
		||||
    def _is_valid_domain(self, domain: str) -> bool:
 | 
			
		||||
        """Basic domain validation."""
 | 
			
		||||
        if not domain or '.' not in domain:
 | 
			
		||||
            return False
 | 
			
		||||
        
 | 
			
		||||
        # Remove common prefixes
 | 
			
		||||
        domain = domain.lower()
 | 
			
		||||
        if domain.startswith('www.'):
 | 
			
		||||
            domain = domain[4:]
 | 
			
		||||
        
 | 
			
		||||
        # Basic validation
 | 
			
		||||
        return len(domain) > 0 and len(domain) < 255
 | 
			
		||||
							
								
								
									
										45
									
								
								src/config.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/config.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,45 @@
 | 
			
		||||
# File: src/config.py
 | 
			
		||||
"""Configuration settings for the reconnaissance tool."""
 | 
			
		||||
 | 
			
		||||
import os
 | 
			
		||||
from dataclasses import dataclass
 | 
			
		||||
from typing import List, Optional
 | 
			
		||||
 | 
			
		||||
@dataclass
 | 
			
		||||
class Config:
 | 
			
		||||
    """Configuration class for the reconnaissance tool."""
 | 
			
		||||
    
 | 
			
		||||
    # DNS servers to query
 | 
			
		||||
    DNS_SERVERS: List[str] = None
 | 
			
		||||
    
 | 
			
		||||
    # API keys
 | 
			
		||||
    shodan_key: Optional[str] = None
 | 
			
		||||
    virustotal_key: Optional[str] = None
 | 
			
		||||
    
 | 
			
		||||
    # Rate limiting (requests per second)
 | 
			
		||||
    DNS_RATE_LIMIT: float = 10.0
 | 
			
		||||
    CRT_SH_RATE_LIMIT: float = 2.0
 | 
			
		||||
    SHODAN_RATE_LIMIT: float = 0.5
 | 
			
		||||
    VIRUSTOTAL_RATE_LIMIT: float = 0.25
 | 
			
		||||
    
 | 
			
		||||
    # Recursive depth
 | 
			
		||||
    max_depth: int = 2
 | 
			
		||||
    
 | 
			
		||||
    # Timeouts
 | 
			
		||||
    DNS_TIMEOUT: int = 5
 | 
			
		||||
    HTTP_TIMEOUT: int = 20
 | 
			
		||||
    
 | 
			
		||||
    def __post_init__(self):
 | 
			
		||||
        if self.DNS_SERVERS is None:
 | 
			
		||||
            self.DNS_SERVERS = ['1.1.1.1', '8.8.8.8', '9.9.9.9']
 | 
			
		||||
    
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def from_args(cls, shodan_key: Optional[str] = None, 
 | 
			
		||||
                  virustotal_key: Optional[str] = None,
 | 
			
		||||
                  max_depth: int = 2) -> 'Config':
 | 
			
		||||
        """Create config from command line arguments."""
 | 
			
		||||
        return cls(
 | 
			
		||||
            shodan_key=shodan_key,
 | 
			
		||||
            virustotal_key=virustotal_key,
 | 
			
		||||
            max_depth=max_depth
 | 
			
		||||
        )
 | 
			
		||||
							
								
								
									
										142
									
								
								src/data_structures.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								src/data_structures.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,142 @@
 | 
			
		||||
# File: src/data_structures.py
 | 
			
		||||
"""Data structures for storing reconnaissance results."""
 | 
			
		||||
 | 
			
		||||
from dataclasses import dataclass, field
 | 
			
		||||
from typing import Dict, List, Set, Optional, Any
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
import json
 | 
			
		||||
 | 
			
		||||
@dataclass
 | 
			
		||||
class DNSRecord:
 | 
			
		||||
    """DNS record information."""
 | 
			
		||||
    record_type: str
 | 
			
		||||
    value: str
 | 
			
		||||
    ttl: Optional[int] = None
 | 
			
		||||
 | 
			
		||||
@dataclass
 | 
			
		||||
class Certificate:
 | 
			
		||||
    """Certificate information from crt.sh."""
 | 
			
		||||
    id: int
 | 
			
		||||
    issuer: str
 | 
			
		||||
    subject: str
 | 
			
		||||
    not_before: datetime
 | 
			
		||||
    not_after: datetime
 | 
			
		||||
    is_wildcard: bool = False
 | 
			
		||||
 | 
			
		||||
@dataclass
 | 
			
		||||
class ShodanResult:
 | 
			
		||||
    """Shodan scan result."""
 | 
			
		||||
    ip: str
 | 
			
		||||
    ports: List[int]
 | 
			
		||||
    services: Dict[str, Any]
 | 
			
		||||
    organization: Optional[str] = None
 | 
			
		||||
    country: Optional[str] = None
 | 
			
		||||
 | 
			
		||||
@dataclass
 | 
			
		||||
class VirusTotalResult:
 | 
			
		||||
    """VirusTotal scan result."""
 | 
			
		||||
    resource: str  # IP or domain
 | 
			
		||||
    positives: int
 | 
			
		||||
    total: int
 | 
			
		||||
    scan_date: datetime
 | 
			
		||||
    permalink: str
 | 
			
		||||
 | 
			
		||||
@dataclass
 | 
			
		||||
class ReconData:
 | 
			
		||||
    """Main data structure for reconnaissance results."""
 | 
			
		||||
    
 | 
			
		||||
    # Core data
 | 
			
		||||
    hostnames: Set[str] = field(default_factory=set)
 | 
			
		||||
    ip_addresses: Set[str] = field(default_factory=set)
 | 
			
		||||
    
 | 
			
		||||
    # DNS information
 | 
			
		||||
    dns_records: Dict[str, List[DNSRecord]] = field(default_factory=dict)
 | 
			
		||||
    reverse_dns: Dict[str, str] = field(default_factory=dict)
 | 
			
		||||
    
 | 
			
		||||
    # Certificate information
 | 
			
		||||
    certificates: Dict[str, List[Certificate]] = field(default_factory=dict)
 | 
			
		||||
    
 | 
			
		||||
    # External service results
 | 
			
		||||
    shodan_results: Dict[str, ShodanResult] = field(default_factory=dict)
 | 
			
		||||
    virustotal_results: Dict[str, VirusTotalResult] = field(default_factory=dict)
 | 
			
		||||
    
 | 
			
		||||
    # Metadata
 | 
			
		||||
    start_time: datetime = field(default_factory=datetime.now)
 | 
			
		||||
    end_time: Optional[datetime] = None
 | 
			
		||||
    depth_map: Dict[str, int] = field(default_factory=dict)  # Track recursion depth
 | 
			
		||||
    
 | 
			
		||||
    def add_hostname(self, hostname: str, depth: int = 0) -> None:
 | 
			
		||||
        """Add a hostname to the dataset."""
 | 
			
		||||
        self.hostnames.add(hostname.lower())
 | 
			
		||||
        self.depth_map[hostname.lower()] = depth
 | 
			
		||||
    
 | 
			
		||||
    def add_ip_address(self, ip: str) -> None:
 | 
			
		||||
        """Add an IP address to the dataset."""
 | 
			
		||||
        self.ip_addresses.add(ip)
 | 
			
		||||
    
 | 
			
		||||
    def add_dns_record(self, hostname: str, record: DNSRecord) -> None:
 | 
			
		||||
        """Add a DNS record for a hostname."""
 | 
			
		||||
        hostname = hostname.lower()
 | 
			
		||||
        if hostname not in self.dns_records:
 | 
			
		||||
            self.dns_records[hostname] = []
 | 
			
		||||
        self.dns_records[hostname].append(record)
 | 
			
		||||
    
 | 
			
		||||
    def get_new_subdomains(self, max_depth: int) -> Set[str]:
 | 
			
		||||
        """Get subdomains that haven't been processed yet and are within depth limit."""
 | 
			
		||||
        new_domains = set()
 | 
			
		||||
        for hostname in self.hostnames:
 | 
			
		||||
            if (hostname not in self.dns_records and 
 | 
			
		||||
                self.depth_map.get(hostname, 0) < max_depth):
 | 
			
		||||
                new_domains.add(hostname)
 | 
			
		||||
        return new_domains
 | 
			
		||||
    
 | 
			
		||||
    def to_dict(self) -> dict:
 | 
			
		||||
        """Export data as a serializable dictionary."""
 | 
			
		||||
        return {
 | 
			
		||||
            'hostnames': list(self.hostnames),
 | 
			
		||||
            'ip_addresses': list(self.ip_addresses),
 | 
			
		||||
            'dns_records': {
 | 
			
		||||
                host: [{'type': r.record_type, 'value': r.value, 'ttl': r.ttl} 
 | 
			
		||||
                       for r in records] 
 | 
			
		||||
                for host, records in self.dns_records.items()
 | 
			
		||||
            },
 | 
			
		||||
            'reverse_dns': self.reverse_dns,
 | 
			
		||||
            'certificates': {
 | 
			
		||||
                host: [{
 | 
			
		||||
                    'id': cert.id,
 | 
			
		||||
                    'issuer': cert.issuer,
 | 
			
		||||
                    'subject': cert.subject,
 | 
			
		||||
                    'not_before': cert.not_before.isoformat(),
 | 
			
		||||
                    'not_after': cert.not_after.isoformat(),
 | 
			
		||||
                    'is_wildcard': cert.is_wildcard
 | 
			
		||||
                } for cert in certs]
 | 
			
		||||
                for host, certs in self.certificates.items()
 | 
			
		||||
            },
 | 
			
		||||
            'shodan_results': {
 | 
			
		||||
                ip: {
 | 
			
		||||
                    'ports': result.ports,
 | 
			
		||||
                    'services': result.services,
 | 
			
		||||
                    'organization': result.organization,
 | 
			
		||||
                    'country': result.country
 | 
			
		||||
                } for ip, result in self.shodan_results.items()
 | 
			
		||||
            },
 | 
			
		||||
            'virustotal_results': {
 | 
			
		||||
                resource: {
 | 
			
		||||
                    'positives': result.positives,
 | 
			
		||||
                    'total': result.total,
 | 
			
		||||
                    'scan_date': result.scan_date.isoformat(),
 | 
			
		||||
                    'permalink': result.permalink
 | 
			
		||||
                } for resource, result in self.virustotal_results.items()
 | 
			
		||||
            },
 | 
			
		||||
            'metadata': {
 | 
			
		||||
                'start_time': self.start_time.isoformat(),
 | 
			
		||||
                'end_time': self.end_time.isoformat() if self.end_time else None,
 | 
			
		||||
                'total_hostnames': len(self.hostnames),
 | 
			
		||||
                'total_ips': len(self.ip_addresses)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    
 | 
			
		||||
    def to_json(self) -> str:
 | 
			
		||||
        """Export data as JSON."""
 | 
			
		||||
        # Now uses the to_dict method
 | 
			
		||||
        return json.dumps(self.to_dict(), indent=2, default=str)
 | 
			
		||||
							
								
								
									
										143
									
								
								src/dns_resolver.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										143
									
								
								src/dns_resolver.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,143 @@
 | 
			
		||||
# File: src/dns_resolver.py
 | 
			
		||||
"""DNS resolution functionality."""
 | 
			
		||||
 | 
			
		||||
import dns.resolver
 | 
			
		||||
import dns.reversename
 | 
			
		||||
import dns.query
 | 
			
		||||
import dns.zone
 | 
			
		||||
from typing import List, Dict, Optional, Set
 | 
			
		||||
import socket
 | 
			
		||||
import time
 | 
			
		||||
from .data_structures import DNSRecord, ReconData
 | 
			
		||||
from .config import Config
 | 
			
		||||
 | 
			
		||||
class DNSResolver:
 | 
			
		||||
    """DNS resolution and record lookup."""
 | 
			
		||||
    
 | 
			
		||||
    # All DNS record types to query
 | 
			
		||||
    RECORD_TYPES = [
 | 
			
		||||
        'A', 'AAAA', 'MX', 'NS', 'TXT', 'CNAME', 'SOA', 'PTR', 
 | 
			
		||||
        'SRV', 'CAA', 'DNSKEY', 'DS', 'RRSIG', 'NSEC', 'NSEC3'
 | 
			
		||||
    ]
 | 
			
		||||
    
 | 
			
		||||
    def __init__(self, config: Config):
 | 
			
		||||
        self.config = config
 | 
			
		||||
        self.last_request = 0
 | 
			
		||||
    
 | 
			
		||||
    def _rate_limit(self):
 | 
			
		||||
        """Apply rate limiting."""
 | 
			
		||||
        now = time.time()
 | 
			
		||||
        time_since_last = now - self.last_request
 | 
			
		||||
        min_interval = 1.0 / self.config.DNS_RATE_LIMIT
 | 
			
		||||
        
 | 
			
		||||
        if time_since_last < min_interval:
 | 
			
		||||
            time.sleep(min_interval - time_since_last)
 | 
			
		||||
        
 | 
			
		||||
        self.last_request = time.time()
 | 
			
		||||
    
 | 
			
		||||
    def resolve_hostname(self, hostname: str) -> List[str]:
 | 
			
		||||
        """Resolve hostname to IP addresses."""
 | 
			
		||||
        ips = []
 | 
			
		||||
        
 | 
			
		||||
        for dns_server in self.config.DNS_SERVERS:
 | 
			
		||||
            self._rate_limit()
 | 
			
		||||
            resolver = dns.resolver.Resolver()
 | 
			
		||||
            resolver.nameservers = [dns_server]
 | 
			
		||||
            resolver.timeout = self.config.DNS_TIMEOUT
 | 
			
		||||
            
 | 
			
		||||
            try:
 | 
			
		||||
                # Try A records
 | 
			
		||||
                answers = resolver.resolve(hostname, 'A')
 | 
			
		||||
                for answer in answers:
 | 
			
		||||
                    ips.append(str(answer))
 | 
			
		||||
            except Exception:
 | 
			
		||||
                pass
 | 
			
		||||
            
 | 
			
		||||
            try:
 | 
			
		||||
                # Try AAAA records
 | 
			
		||||
                answers = resolver.resolve(hostname, 'AAAA')
 | 
			
		||||
                for answer in answers:
 | 
			
		||||
                    ips.append(str(answer))
 | 
			
		||||
            except Exception:
 | 
			
		||||
                pass
 | 
			
		||||
        
 | 
			
		||||
        return list(set(ips))  # Remove duplicates
 | 
			
		||||
    
 | 
			
		||||
    def get_all_dns_records(self, hostname: str) -> List[DNSRecord]:
 | 
			
		||||
        """Get all DNS records for a hostname."""
 | 
			
		||||
        records = []
 | 
			
		||||
        
 | 
			
		||||
        for record_type in self.RECORD_TYPES:
 | 
			
		||||
            for dns_server in self.config.DNS_SERVERS:
 | 
			
		||||
                self._rate_limit()
 | 
			
		||||
                resolver = dns.resolver.Resolver()
 | 
			
		||||
                resolver.nameservers = [dns_server]
 | 
			
		||||
                resolver.timeout = self.config.DNS_TIMEOUT
 | 
			
		||||
                
 | 
			
		||||
                try:
 | 
			
		||||
                    answers = resolver.resolve(hostname, record_type)
 | 
			
		||||
                    for answer in answers:
 | 
			
		||||
                        records.append(DNSRecord(
 | 
			
		||||
                            record_type=record_type,
 | 
			
		||||
                            value=str(answer),
 | 
			
		||||
                            ttl=answers.ttl
 | 
			
		||||
                        ))
 | 
			
		||||
                except Exception:
 | 
			
		||||
                    continue
 | 
			
		||||
        
 | 
			
		||||
        return records
 | 
			
		||||
    
 | 
			
		||||
    def reverse_dns_lookup(self, ip: str) -> Optional[str]:
 | 
			
		||||
        """Perform reverse DNS lookup."""
 | 
			
		||||
        try:
 | 
			
		||||
            self._rate_limit()
 | 
			
		||||
            return socket.gethostbyaddr(ip)[0]
 | 
			
		||||
        except Exception:
 | 
			
		||||
            return None
 | 
			
		||||
    
 | 
			
		||||
    def extract_subdomains_from_dns(self, records: List[DNSRecord]) -> Set[str]:
 | 
			
		||||
        """Extract potential subdomains from DNS records."""
 | 
			
		||||
        subdomains = set()
 | 
			
		||||
        
 | 
			
		||||
        for record in records:
 | 
			
		||||
            value = record.value.lower()
 | 
			
		||||
            
 | 
			
		||||
            # Extract from CNAME, NS, and correctly from MX records
 | 
			
		||||
            if record.record_type == 'MX':
 | 
			
		||||
                # MX record values are like: "10 mail.example.com."
 | 
			
		||||
                # We need to extract the hostname part.
 | 
			
		||||
                parts = value.split()
 | 
			
		||||
                if len(parts) == 2:
 | 
			
		||||
                    hostname = parts[1].rstrip('.')
 | 
			
		||||
                    if self._is_valid_hostname(hostname):
 | 
			
		||||
                        subdomains.add(hostname)
 | 
			
		||||
            elif record.record_type in ['CNAME', 'NS']:
 | 
			
		||||
                # These records are just the hostname
 | 
			
		||||
                hostname = value.rstrip('.')
 | 
			
		||||
                if self._is_valid_hostname(hostname):
 | 
			
		||||
                    subdomains.add(hostname)
 | 
			
		||||
            
 | 
			
		||||
            # Extract from TXT records (sometimes contain domain references)
 | 
			
		||||
            elif record.record_type == 'TXT':
 | 
			
		||||
                # Look for domain-like strings in TXT records
 | 
			
		||||
                parts = value.split()
 | 
			
		||||
                for part in parts:
 | 
			
		||||
                    if '.' in part and not part.startswith('http'):
 | 
			
		||||
                        clean_part = part.strip('",\'()[]{}')
 | 
			
		||||
                        if self._is_valid_hostname(clean_part):
 | 
			
		||||
                            subdomains.add(clean_part)
 | 
			
		||||
        
 | 
			
		||||
        return subdomains
 | 
			
		||||
    
 | 
			
		||||
    def _is_valid_hostname(self, hostname: str) -> bool:
 | 
			
		||||
        """Basic hostname validation."""
 | 
			
		||||
        if not hostname or len(hostname) > 255:
 | 
			
		||||
            return False
 | 
			
		||||
        
 | 
			
		||||
        # Must contain at least one dot
 | 
			
		||||
        if '.' not in hostname:
 | 
			
		||||
            return False
 | 
			
		||||
        
 | 
			
		||||
        # Basic character check
 | 
			
		||||
        allowed_chars = set('abcdefghijklmnopqrstuvwxyz0123456789.-')
 | 
			
		||||
        return all(c in allowed_chars for c in hostname.lower())
 | 
			
		||||
							
								
								
									
										107
									
								
								src/main.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								src/main.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,107 @@
 | 
			
		||||
# File: src/main.py
 | 
			
		||||
"""Main CLI interface for the reconnaissance tool."""
 | 
			
		||||
 | 
			
		||||
import click
 | 
			
		||||
import json
 | 
			
		||||
import sys
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
from .config import Config
 | 
			
		||||
from .reconnaissance import ReconnaissanceEngine
 | 
			
		||||
from .report_generator import ReportGenerator
 | 
			
		||||
from .web_app import create_app
 | 
			
		||||
 | 
			
		||||
@click.command()
 | 
			
		||||
@click.argument('target', required=False)
 | 
			
		||||
@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('--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')
 | 
			
		||||
@click.option('--port', default=5000, help='Port for web interface (default: 5000)')
 | 
			
		||||
def main(target, web, shodan_key, virustotal_key, max_depth, output, json_only, text_only, port):
 | 
			
		||||
    """DNS Reconnaissance Tool
 | 
			
		||||
    
 | 
			
		||||
    Examples:
 | 
			
		||||
        recon example.com                    # Scan example.com
 | 
			
		||||
        recon example                        # Try example.* for all TLDs
 | 
			
		||||
        recon example.com --max-depth 3      # Deeper recursion
 | 
			
		||||
        recon --web                          # Start web interface
 | 
			
		||||
    """
 | 
			
		||||
    
 | 
			
		||||
    if web:
 | 
			
		||||
        # Start web interface
 | 
			
		||||
        app = create_app(Config.from_args(shodan_key, virustotal_key, max_depth))
 | 
			
		||||
        app.run(host='0.0.0.0', port=port, debug=True)
 | 
			
		||||
        return
 | 
			
		||||
    
 | 
			
		||||
    if not target:
 | 
			
		||||
        click.echo("Error: TARGET is required for CLI mode. Use --web for web interface.")
 | 
			
		||||
        sys.exit(1)
 | 
			
		||||
    
 | 
			
		||||
    # Create configuration
 | 
			
		||||
    config = Config.from_args(shodan_key, virustotal_key, max_depth)
 | 
			
		||||
    
 | 
			
		||||
    # Initialize reconnaissance engine
 | 
			
		||||
    engine = ReconnaissanceEngine(config)
 | 
			
		||||
    
 | 
			
		||||
    # Set up progress callback
 | 
			
		||||
    def progress_callback(message, percentage=None):
 | 
			
		||||
        if percentage:
 | 
			
		||||
            click.echo(f"[{percentage:3d}%] {message}")
 | 
			
		||||
        else:
 | 
			
		||||
            click.echo(f"       {message}")
 | 
			
		||||
    
 | 
			
		||||
    engine.set_progress_callback(progress_callback)
 | 
			
		||||
    
 | 
			
		||||
    # Run reconnaissance
 | 
			
		||||
    click.echo(f"Starting reconnaissance for: {target}")
 | 
			
		||||
    click.echo(f"Max recursion depth: {max_depth}")
 | 
			
		||||
    if shodan_key:
 | 
			
		||||
        click.echo("✓ Shodan integration enabled")
 | 
			
		||||
    if virustotal_key:
 | 
			
		||||
        click.echo("✓ VirusTotal integration enabled")
 | 
			
		||||
    click.echo("")
 | 
			
		||||
    
 | 
			
		||||
    try:
 | 
			
		||||
        data = engine.run_reconnaissance(target)
 | 
			
		||||
        
 | 
			
		||||
        # Generate reports
 | 
			
		||||
        report_gen = ReportGenerator(data)
 | 
			
		||||
        
 | 
			
		||||
        if output:
 | 
			
		||||
            # Save to files
 | 
			
		||||
            if not text_only:
 | 
			
		||||
                json_file = f"{output}.json"
 | 
			
		||||
                with open(json_file, 'w') as f:
 | 
			
		||||
                    f.write(data.to_json())
 | 
			
		||||
                click.echo(f"JSON report saved to: {json_file}")
 | 
			
		||||
            
 | 
			
		||||
            if not json_only:
 | 
			
		||||
                text_file = f"{output}.txt"
 | 
			
		||||
                with open(text_file, 'w') as f:
 | 
			
		||||
                    f.write(report_gen.generate_text_report())
 | 
			
		||||
                click.echo(f"Text report saved to: {text_file}")
 | 
			
		||||
        
 | 
			
		||||
        else:
 | 
			
		||||
            # Output to stdout
 | 
			
		||||
            if json_only:
 | 
			
		||||
                click.echo(data.to_json())
 | 
			
		||||
            elif text_only:
 | 
			
		||||
                click.echo(report_gen.generate_text_report())
 | 
			
		||||
            else:
 | 
			
		||||
                # Default: show text report
 | 
			
		||||
                click.echo(report_gen.generate_text_report())
 | 
			
		||||
                click.echo(f"\nTo get JSON output, use: --json-only")
 | 
			
		||||
                click.echo(f"To save reports, use: --output filename")
 | 
			
		||||
    
 | 
			
		||||
    except KeyboardInterrupt:
 | 
			
		||||
        click.echo("\nReconnaissance interrupted by user.")
 | 
			
		||||
        sys.exit(1)
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        click.echo(f"Error during reconnaissance: {e}")
 | 
			
		||||
        sys.exit(1)
 | 
			
		||||
 | 
			
		||||
if __name__ == '__main__':
 | 
			
		||||
    main()
 | 
			
		||||
							
								
								
									
										191
									
								
								src/reconnaissance.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										191
									
								
								src/reconnaissance.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,191 @@
 | 
			
		||||
# File: src/reconnaissance.py
 | 
			
		||||
"""Main reconnaissance logic."""
 | 
			
		||||
 | 
			
		||||
import threading
 | 
			
		||||
import concurrent.futures
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
from typing import Set, List, Optional
 | 
			
		||||
from .data_structures import ReconData
 | 
			
		||||
from .config import Config
 | 
			
		||||
from .dns_resolver import DNSResolver
 | 
			
		||||
from .certificate_checker import CertificateChecker
 | 
			
		||||
from .shodan_client import ShodanClient
 | 
			
		||||
from .virustotal_client import VirusTotalClient
 | 
			
		||||
from .tld_fetcher import TLDFetcher
 | 
			
		||||
 | 
			
		||||
class ReconnaissanceEngine:
 | 
			
		||||
    """Main reconnaissance engine."""
 | 
			
		||||
    
 | 
			
		||||
    def __init__(self, config: Config):
 | 
			
		||||
        self.config = config
 | 
			
		||||
        # self.data = ReconData() # <-- REMOVED FROM HERE
 | 
			
		||||
        
 | 
			
		||||
        # Initialize clients
 | 
			
		||||
        self.dns_resolver = DNSResolver(config)
 | 
			
		||||
        self.cert_checker = CertificateChecker(config)
 | 
			
		||||
        self.tld_fetcher = TLDFetcher()
 | 
			
		||||
        
 | 
			
		||||
        # Optional clients
 | 
			
		||||
        self.shodan_client = None
 | 
			
		||||
        if config.shodan_key:
 | 
			
		||||
            self.shodan_client = ShodanClient(config.shodan_key, config)
 | 
			
		||||
        
 | 
			
		||||
        self.virustotal_client = None
 | 
			
		||||
        if config.virustotal_key:
 | 
			
		||||
            self.virustotal_client = VirusTotalClient(config.virustotal_key, config)
 | 
			
		||||
        
 | 
			
		||||
        # Progress tracking
 | 
			
		||||
        self.progress_callback = None
 | 
			
		||||
        self._lock = threading.Lock()
 | 
			
		||||
    
 | 
			
		||||
    def set_progress_callback(self, callback):
 | 
			
		||||
        """Set callback for progress updates."""
 | 
			
		||||
        self.progress_callback = callback
 | 
			
		||||
    
 | 
			
		||||
    def _update_progress(self, message: str, percentage: int = None):
 | 
			
		||||
        """Update progress if callback is set."""
 | 
			
		||||
        if self.progress_callback:
 | 
			
		||||
            self.progress_callback(message, percentage)
 | 
			
		||||
    
 | 
			
		||||
    def run_reconnaissance(self, target: str) -> ReconData:
 | 
			
		||||
        """Run full reconnaissance on target."""
 | 
			
		||||
        self.data = ReconData()
 | 
			
		||||
        self.data.start_time = datetime.now()
 | 
			
		||||
        
 | 
			
		||||
        try:
 | 
			
		||||
            # Determine if target is hostname.tld or just hostname
 | 
			
		||||
            if '.' in target:
 | 
			
		||||
                self._update_progress(f"Starting reconnaissance for {target}", 0)
 | 
			
		||||
                self.data.add_hostname(target, 0)
 | 
			
		||||
                initial_targets = {target}
 | 
			
		||||
            else:
 | 
			
		||||
                self._update_progress(f"Expanding {target} to all TLDs", 5)
 | 
			
		||||
                initial_targets = self._expand_hostname_to_tlds(target)
 | 
			
		||||
            
 | 
			
		||||
            self._update_progress("Resolving initial targets", 10)
 | 
			
		||||
            
 | 
			
		||||
            # Process all targets recursively
 | 
			
		||||
            self._process_targets_recursively(initial_targets)
 | 
			
		||||
            
 | 
			
		||||
            # Final external lookups
 | 
			
		||||
            self._update_progress("Performing external service lookups", 90)
 | 
			
		||||
            self._perform_external_lookups()
 | 
			
		||||
            
 | 
			
		||||
            self._update_progress("Reconnaissance complete", 100)
 | 
			
		||||
            
 | 
			
		||||
        finally:
 | 
			
		||||
            self.data.end_time = datetime.now()
 | 
			
		||||
        
 | 
			
		||||
        return self.data
 | 
			
		||||
    
 | 
			
		||||
    def _expand_hostname_to_tlds(self, hostname: str) -> Set[str]:
 | 
			
		||||
        """Expand hostname to all possible TLDs."""
 | 
			
		||||
        tlds = self.tld_fetcher.get_tlds()
 | 
			
		||||
        targets = set()
 | 
			
		||||
        
 | 
			
		||||
        for i, tld in enumerate(tlds):
 | 
			
		||||
            full_hostname = f"{hostname}.{tld}"
 | 
			
		||||
            
 | 
			
		||||
            # Quick check if domain resolves
 | 
			
		||||
            ips = self.dns_resolver.resolve_hostname(full_hostname)
 | 
			
		||||
            if ips:
 | 
			
		||||
                self.data.add_hostname(full_hostname, 0)
 | 
			
		||||
                targets.add(full_hostname)
 | 
			
		||||
                for ip in ips:
 | 
			
		||||
                    self.data.add_ip_address(ip)
 | 
			
		||||
            
 | 
			
		||||
            # Progress update every 100 TLDs
 | 
			
		||||
            if i % 100 == 0:
 | 
			
		||||
                progress = 5 + int((i / len(tlds)) * 5)  # 5-10% range
 | 
			
		||||
                self._update_progress(f"Checked {i}/{len(tlds)} TLDs, found {len(targets)} valid domains", progress)
 | 
			
		||||
        
 | 
			
		||||
        return targets
 | 
			
		||||
    
 | 
			
		||||
    def _process_targets_recursively(self, targets: Set[str]):
 | 
			
		||||
        """Process targets with recursive subdomain discovery."""
 | 
			
		||||
        current_depth = 0
 | 
			
		||||
        
 | 
			
		||||
        while current_depth <= self.config.max_depth and targets:
 | 
			
		||||
            self._update_progress(f"Processing depth {current_depth}", 15 + (current_depth * 25))
 | 
			
		||||
            
 | 
			
		||||
            new_targets = set()
 | 
			
		||||
            
 | 
			
		||||
            for target in targets:
 | 
			
		||||
                # DNS resolution and record gathering
 | 
			
		||||
                self._process_single_target(target, current_depth)
 | 
			
		||||
                
 | 
			
		||||
                # Extract new subdomains
 | 
			
		||||
                if current_depth < self.config.max_depth:
 | 
			
		||||
                    new_subdomains = self._extract_new_subdomains(target)
 | 
			
		||||
                    for subdomain in new_subdomains:
 | 
			
		||||
                        self.data.add_hostname(subdomain, current_depth + 1)
 | 
			
		||||
                        new_targets.add(subdomain)
 | 
			
		||||
            
 | 
			
		||||
            targets = new_targets
 | 
			
		||||
            current_depth += 1
 | 
			
		||||
    
 | 
			
		||||
    def _process_single_target(self, hostname: str, depth: int):
 | 
			
		||||
        """Process a single target hostname."""
 | 
			
		||||
        # Get all DNS records
 | 
			
		||||
        dns_records = self.dns_resolver.get_all_dns_records(hostname)
 | 
			
		||||
        for record in dns_records:
 | 
			
		||||
            self.data.add_dns_record(hostname, record)
 | 
			
		||||
            
 | 
			
		||||
            # Extract IP addresses from A and AAAA records
 | 
			
		||||
            if record.record_type in ['A', 'AAAA']:
 | 
			
		||||
                self.data.add_ip_address(record.value)
 | 
			
		||||
        
 | 
			
		||||
        # Get certificates
 | 
			
		||||
        certificates = self.cert_checker.get_certificates(hostname)
 | 
			
		||||
        if certificates:
 | 
			
		||||
            self.data.certificates[hostname] = certificates
 | 
			
		||||
    
 | 
			
		||||
    def _extract_new_subdomains(self, hostname: str) -> Set[str]:
 | 
			
		||||
        """Extract new subdomains from DNS records and certificates."""
 | 
			
		||||
        new_subdomains = set()
 | 
			
		||||
        
 | 
			
		||||
        # From DNS records
 | 
			
		||||
        if hostname in self.data.dns_records:
 | 
			
		||||
            dns_subdomains = self.dns_resolver.extract_subdomains_from_dns(
 | 
			
		||||
                self.data.dns_records[hostname]
 | 
			
		||||
            )
 | 
			
		||||
            new_subdomains.update(dns_subdomains)
 | 
			
		||||
        
 | 
			
		||||
        # From certificates
 | 
			
		||||
        if hostname in self.data.certificates:
 | 
			
		||||
            cert_subdomains = self.cert_checker.extract_subdomains_from_certificates(
 | 
			
		||||
                self.data.certificates[hostname]
 | 
			
		||||
            )
 | 
			
		||||
            new_subdomains.update(cert_subdomains)
 | 
			
		||||
        
 | 
			
		||||
        # Filter out already known hostnames
 | 
			
		||||
        return new_subdomains - self.data.hostnames
 | 
			
		||||
    
 | 
			
		||||
    def _perform_external_lookups(self):
 | 
			
		||||
        """Perform Shodan and VirusTotal lookups."""
 | 
			
		||||
        # Reverse DNS for all IPs
 | 
			
		||||
        for ip in self.data.ip_addresses:
 | 
			
		||||
            reverse = self.dns_resolver.reverse_dns_lookup(ip)
 | 
			
		||||
            if reverse:
 | 
			
		||||
                self.data.reverse_dns[ip] = reverse
 | 
			
		||||
        
 | 
			
		||||
        # Shodan lookups
 | 
			
		||||
        if self.shodan_client:
 | 
			
		||||
            for ip in self.data.ip_addresses:
 | 
			
		||||
                result = self.shodan_client.lookup_ip(ip)
 | 
			
		||||
                if result:
 | 
			
		||||
                    self.data.shodan_results[ip] = result
 | 
			
		||||
        
 | 
			
		||||
        # VirusTotal lookups
 | 
			
		||||
        if self.virustotal_client:
 | 
			
		||||
            # Check IPs
 | 
			
		||||
            for ip in self.data.ip_addresses:
 | 
			
		||||
                result = self.virustotal_client.lookup_ip(ip)
 | 
			
		||||
                if result:
 | 
			
		||||
                    self.data.virustotal_results[ip] = result
 | 
			
		||||
            
 | 
			
		||||
            # Check domains
 | 
			
		||||
            for hostname in self.data.hostnames:
 | 
			
		||||
                result = self.virustotal_client.lookup_domain(hostname)
 | 
			
		||||
                if result:
 | 
			
		||||
                    self.data.virustotal_results[hostname] = result
 | 
			
		||||
							
								
								
									
										111
									
								
								src/report_generator.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								src/report_generator.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,111 @@
 | 
			
		||||
# File: src/report_generator.py
 | 
			
		||||
"""Generate reports from reconnaissance data."""
 | 
			
		||||
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
from typing import Dict, Any
 | 
			
		||||
from .data_structures import ReconData
 | 
			
		||||
 | 
			
		||||
class ReportGenerator:
 | 
			
		||||
    """Generate various report formats."""
 | 
			
		||||
    
 | 
			
		||||
    def __init__(self, data: ReconData):
 | 
			
		||||
        self.data = data
 | 
			
		||||
    
 | 
			
		||||
    def generate_text_report(self) -> str:
 | 
			
		||||
        """Generate comprehensive text report."""
 | 
			
		||||
        report = []
 | 
			
		||||
        
 | 
			
		||||
        # Header
 | 
			
		||||
        report.append("="*80)
 | 
			
		||||
        report.append("DNS RECONNAISSANCE REPORT")
 | 
			
		||||
        report.append("="*80)
 | 
			
		||||
        report.append(f"Start Time: {self.data.start_time}")
 | 
			
		||||
        report.append(f"End Time: {self.data.end_time}")
 | 
			
		||||
        if self.data.end_time:
 | 
			
		||||
            duration = self.data.end_time - self.data.start_time
 | 
			
		||||
            report.append(f"Duration: {duration}")
 | 
			
		||||
        report.append("")
 | 
			
		||||
        
 | 
			
		||||
        # Summary
 | 
			
		||||
        report.append("SUMMARY")
 | 
			
		||||
        report.append("-" * 40)
 | 
			
		||||
        report.append(f"Total Hostnames Discovered: {len(self.data.hostnames)}")
 | 
			
		||||
        report.append(f"Total IP Addresses Found: {len(self.data.ip_addresses)}")
 | 
			
		||||
        report.append(f"Total DNS Records: {sum(len(records) for records in self.data.dns_records.values())}")
 | 
			
		||||
        report.append(f"Total Certificates Found: {sum(len(certs) for certs in self.data.certificates.values())}")
 | 
			
		||||
        report.append("")
 | 
			
		||||
        
 | 
			
		||||
        # Hostnames by depth
 | 
			
		||||
        report.append("HOSTNAMES BY DISCOVERY DEPTH")
 | 
			
		||||
        report.append("-" * 40)
 | 
			
		||||
        depth_groups = {}
 | 
			
		||||
        for hostname, depth in self.data.depth_map.items():
 | 
			
		||||
            if depth not in depth_groups:
 | 
			
		||||
                depth_groups[depth] = []
 | 
			
		||||
            depth_groups[depth].append(hostname)
 | 
			
		||||
        
 | 
			
		||||
        for depth in sorted(depth_groups.keys()):
 | 
			
		||||
            report.append(f"Depth {depth}: {len(depth_groups[depth])} hostnames")
 | 
			
		||||
            for hostname in sorted(depth_groups[depth]):
 | 
			
		||||
                report.append(f"  - {hostname}")
 | 
			
		||||
        report.append("")
 | 
			
		||||
        
 | 
			
		||||
        # IP Addresses
 | 
			
		||||
        report.append("IP ADDRESSES")
 | 
			
		||||
        report.append("-" * 40)
 | 
			
		||||
        for ip in sorted(self.data.ip_addresses):
 | 
			
		||||
            report.append(f"{ip}")
 | 
			
		||||
            if ip in self.data.reverse_dns:
 | 
			
		||||
                report.append(f"  Reverse DNS: {self.data.reverse_dns[ip]}")
 | 
			
		||||
            if ip in self.data.shodan_results:
 | 
			
		||||
                shodan = self.data.shodan_results[ip]
 | 
			
		||||
                report.append(f"  Shodan: {len(shodan.ports)} open ports")
 | 
			
		||||
                if shodan.organization:
 | 
			
		||||
                    report.append(f"    Organization: {shodan.organization}")
 | 
			
		||||
                if shodan.country:
 | 
			
		||||
                    report.append(f"    Country: {shodan.country}")
 | 
			
		||||
        report.append("")
 | 
			
		||||
        
 | 
			
		||||
        # DNS Records
 | 
			
		||||
        report.append("DNS RECORDS")
 | 
			
		||||
        report.append("-" * 40)
 | 
			
		||||
        for hostname in sorted(self.data.dns_records.keys()):
 | 
			
		||||
            report.append(f"{hostname}:")
 | 
			
		||||
            records_by_type = {}
 | 
			
		||||
            for record in self.data.dns_records[hostname]:
 | 
			
		||||
                if record.record_type not in records_by_type:
 | 
			
		||||
                    records_by_type[record.record_type] = []
 | 
			
		||||
                records_by_type[record.record_type].append(record)
 | 
			
		||||
            
 | 
			
		||||
            for record_type in sorted(records_by_type.keys()):
 | 
			
		||||
                report.append(f"  {record_type}:")
 | 
			
		||||
                for record in records_by_type[record_type]:
 | 
			
		||||
                    report.append(f"    {record.value}")
 | 
			
		||||
        report.append("")
 | 
			
		||||
        
 | 
			
		||||
        # Certificates
 | 
			
		||||
        if self.data.certificates:
 | 
			
		||||
            report.append("CERTIFICATES")
 | 
			
		||||
            report.append("-" * 40)
 | 
			
		||||
            for hostname in sorted(self.data.certificates.keys()):
 | 
			
		||||
                report.append(f"{hostname}:")
 | 
			
		||||
                for cert in self.data.certificates[hostname]:
 | 
			
		||||
                    report.append(f"  Certificate ID: {cert.id}")
 | 
			
		||||
                    report.append(f"    Issuer: {cert.issuer}")
 | 
			
		||||
                    report.append(f"    Valid From: {cert.not_before}")
 | 
			
		||||
                    report.append(f"    Valid Until: {cert.not_after}")
 | 
			
		||||
                    if cert.is_wildcard:
 | 
			
		||||
                        report.append(f"    Type: Wildcard Certificate")
 | 
			
		||||
                    report.append("")
 | 
			
		||||
        
 | 
			
		||||
        # Security Analysis
 | 
			
		||||
        if self.data.virustotal_results:
 | 
			
		||||
            report.append("SECURITY ANALYSIS")
 | 
			
		||||
            report.append("-" * 40)
 | 
			
		||||
            for resource, result in self.data.virustotal_results.items():
 | 
			
		||||
                if result.positives > 0:
 | 
			
		||||
                    report.append(f"⚠️  {resource}: {result.positives}/{result.total} detections")
 | 
			
		||||
                    report.append(f"   Scan Date: {result.scan_date}")
 | 
			
		||||
                    report.append(f"   Report: {result.permalink}")
 | 
			
		||||
        
 | 
			
		||||
        return "\n".join(report)
 | 
			
		||||
							
								
								
									
										105
									
								
								src/shodan_client.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								src/shodan_client.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,105 @@
 | 
			
		||||
# File: src/shodan_client.py
 | 
			
		||||
"""Shodan API integration."""
 | 
			
		||||
 | 
			
		||||
import requests
 | 
			
		||||
import time
 | 
			
		||||
from typing import Optional, Dict, Any, List
 | 
			
		||||
from .data_structures import ShodanResult
 | 
			
		||||
from .config import Config
 | 
			
		||||
 | 
			
		||||
class ShodanClient:
 | 
			
		||||
    """Shodan API client."""
 | 
			
		||||
    
 | 
			
		||||
    BASE_URL = "https://api.shodan.io"
 | 
			
		||||
    
 | 
			
		||||
    def __init__(self, api_key: str, config: Config):
 | 
			
		||||
        self.api_key = api_key
 | 
			
		||||
        self.config = config
 | 
			
		||||
        self.last_request = 0
 | 
			
		||||
    
 | 
			
		||||
    def _rate_limit(self):
 | 
			
		||||
        """Apply rate limiting for Shodan."""
 | 
			
		||||
        now = time.time()
 | 
			
		||||
        time_since_last = now - self.last_request
 | 
			
		||||
        min_interval = 1.0 / self.config.SHODAN_RATE_LIMIT
 | 
			
		||||
        
 | 
			
		||||
        if time_since_last < min_interval:
 | 
			
		||||
            time.sleep(min_interval - time_since_last)
 | 
			
		||||
        
 | 
			
		||||
        self.last_request = time.time()
 | 
			
		||||
    
 | 
			
		||||
    def lookup_ip(self, ip: str) -> Optional[ShodanResult]:
 | 
			
		||||
        """Lookup IP address information."""
 | 
			
		||||
        self._rate_limit()
 | 
			
		||||
        
 | 
			
		||||
        try:
 | 
			
		||||
            url = f"{self.BASE_URL}/shodan/host/{ip}"
 | 
			
		||||
            params = {'key': self.api_key}
 | 
			
		||||
            
 | 
			
		||||
            response = requests.get(url, params=params, timeout=self.config.HTTP_TIMEOUT)
 | 
			
		||||
            
 | 
			
		||||
            if response.status_code == 200:
 | 
			
		||||
                data = response.json()
 | 
			
		||||
                
 | 
			
		||||
                ports = []
 | 
			
		||||
                services = {}
 | 
			
		||||
                
 | 
			
		||||
                for service in data.get('data', []):
 | 
			
		||||
                    port = service.get('port')
 | 
			
		||||
                    if port:
 | 
			
		||||
                        ports.append(port)
 | 
			
		||||
                        services[str(port)] = {
 | 
			
		||||
                            'product': service.get('product', ''),
 | 
			
		||||
                            'version': service.get('version', ''),
 | 
			
		||||
                            'banner': service.get('data', '').strip()[:200]  # Limit banner size
 | 
			
		||||
                        }
 | 
			
		||||
                
 | 
			
		||||
                return ShodanResult(
 | 
			
		||||
                    ip=ip,
 | 
			
		||||
                    ports=sorted(list(set(ports))),
 | 
			
		||||
                    services=services,
 | 
			
		||||
                    organization=data.get('org'),
 | 
			
		||||
                    country=data.get('country_name')
 | 
			
		||||
                )
 | 
			
		||||
                
 | 
			
		||||
            elif response.status_code == 404:
 | 
			
		||||
                return None  # IP not found in Shodan
 | 
			
		||||
            else:
 | 
			
		||||
                print(f"Shodan API error for {ip}: {response.status_code}")
 | 
			
		||||
                return None
 | 
			
		||||
                
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            print(f"Error querying Shodan for {ip}: {e}")
 | 
			
		||||
            return None
 | 
			
		||||
    
 | 
			
		||||
    def search_domain(self, domain: str) -> List[str]:
 | 
			
		||||
        """Search for IPs associated with a domain."""
 | 
			
		||||
        self._rate_limit()
 | 
			
		||||
        
 | 
			
		||||
        try:
 | 
			
		||||
            url = f"{self.BASE_URL}/shodan/host/search"
 | 
			
		||||
            params = {
 | 
			
		||||
                'key': self.api_key,
 | 
			
		||||
                'query': f'hostname:{domain}',
 | 
			
		||||
                'limit': 100
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            response = requests.get(url, params=params, timeout=self.config.HTTP_TIMEOUT)
 | 
			
		||||
            
 | 
			
		||||
            if response.status_code == 200:
 | 
			
		||||
                data = response.json()
 | 
			
		||||
                ips = []
 | 
			
		||||
                
 | 
			
		||||
                for match in data.get('matches', []):
 | 
			
		||||
                    ip = match.get('ip_str')
 | 
			
		||||
                    if ip:
 | 
			
		||||
                        ips.append(ip)
 | 
			
		||||
                
 | 
			
		||||
                return list(set(ips))
 | 
			
		||||
            else:
 | 
			
		||||
                print(f"Shodan search error for {domain}: {response.status_code}")
 | 
			
		||||
                return []
 | 
			
		||||
                
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            print(f"Error searching Shodan for {domain}: {e}")
 | 
			
		||||
            return []
 | 
			
		||||
							
								
								
									
										68
									
								
								src/tld_fetcher.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								src/tld_fetcher.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,68 @@
 | 
			
		||||
# File: src/tld_fetcher.py
 | 
			
		||||
"""Fetch and cache IANA TLD list."""
 | 
			
		||||
 | 
			
		||||
import requests
 | 
			
		||||
from typing import List, Set
 | 
			
		||||
import os
 | 
			
		||||
import time
 | 
			
		||||
 | 
			
		||||
class TLDFetcher:
 | 
			
		||||
    """Fetches and caches IANA TLD list."""
 | 
			
		||||
    
 | 
			
		||||
    IANA_TLD_URL = "https://data.iana.org/TLD/tlds-alpha-by-domain.txt"
 | 
			
		||||
    CACHE_FILE = "tlds_cache.txt"
 | 
			
		||||
    CACHE_DURATION = 86400  # 24 hours in seconds
 | 
			
		||||
    
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        self._tlds: Optional[Set[str]] = None
 | 
			
		||||
    
 | 
			
		||||
    def get_tlds(self) -> Set[str]:
 | 
			
		||||
        """Get list of TLDs, using cache if available."""
 | 
			
		||||
        if self._tlds is None:
 | 
			
		||||
            self._tlds = self._load_tlds()
 | 
			
		||||
        return self._tlds
 | 
			
		||||
    
 | 
			
		||||
    def _load_tlds(self) -> Set[str]:
 | 
			
		||||
        """Load TLDs from cache or fetch from IANA."""
 | 
			
		||||
        if self._is_cache_valid():
 | 
			
		||||
            return self._load_from_cache()
 | 
			
		||||
        return self._fetch_and_cache()
 | 
			
		||||
    
 | 
			
		||||
    def _is_cache_valid(self) -> bool:
 | 
			
		||||
        """Check if cache file exists and is recent."""
 | 
			
		||||
        if not os.path.exists(self.CACHE_FILE):
 | 
			
		||||
            return False
 | 
			
		||||
        
 | 
			
		||||
        cache_age = time.time() - os.path.getmtime(self.CACHE_FILE)
 | 
			
		||||
        return cache_age < self.CACHE_DURATION
 | 
			
		||||
    
 | 
			
		||||
    def _load_from_cache(self) -> Set[str]:
 | 
			
		||||
        """Load TLDs from cache file."""
 | 
			
		||||
        with open(self.CACHE_FILE, 'r') as f:
 | 
			
		||||
            return set(line.strip().lower() for line in f if not line.startswith('#'))
 | 
			
		||||
    
 | 
			
		||||
    def _fetch_and_cache(self) -> Set[str]:
 | 
			
		||||
        """Fetch TLDs from IANA and cache them."""
 | 
			
		||||
        try:
 | 
			
		||||
            response = requests.get(self.IANA_TLD_URL, timeout=10)
 | 
			
		||||
            response.raise_for_status()
 | 
			
		||||
            
 | 
			
		||||
            tlds = set()
 | 
			
		||||
            for line in response.text.split('\n'):
 | 
			
		||||
                line = line.strip().lower()
 | 
			
		||||
                if line and not line.startswith('#'):
 | 
			
		||||
                    tlds.add(line)
 | 
			
		||||
            
 | 
			
		||||
            # Cache the results
 | 
			
		||||
            with open(self.CACHE_FILE, 'w') as f:
 | 
			
		||||
                f.write(response.text)
 | 
			
		||||
            
 | 
			
		||||
            return tlds
 | 
			
		||||
            
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            print(f"Failed to fetch TLD list: {e}")
 | 
			
		||||
            # Return a minimal set if fetch fails
 | 
			
		||||
            return {
 | 
			
		||||
                'com', 'org', 'net', 'edu', 'gov', 'mil', 'int',
 | 
			
		||||
                'co.uk', 'org.uk', 'ac.uk', 'de', 'fr', 'it', 'nl', 'be'
 | 
			
		||||
            }
 | 
			
		||||
							
								
								
									
										100
									
								
								src/virustotal_client.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								src/virustotal_client.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,100 @@
 | 
			
		||||
# File: src/virustotal_client.py
 | 
			
		||||
"""VirusTotal API integration."""
 | 
			
		||||
 | 
			
		||||
import requests
 | 
			
		||||
import time
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
from typing import Optional
 | 
			
		||||
from .data_structures import VirusTotalResult
 | 
			
		||||
from .config import Config
 | 
			
		||||
 | 
			
		||||
class VirusTotalClient:
 | 
			
		||||
    """VirusTotal API client."""
 | 
			
		||||
    
 | 
			
		||||
    BASE_URL = "https://www.virustotal.com/vtapi/v2"
 | 
			
		||||
    
 | 
			
		||||
    def __init__(self, api_key: str, config: Config):
 | 
			
		||||
        self.api_key = api_key
 | 
			
		||||
        self.config = config
 | 
			
		||||
        self.last_request = 0
 | 
			
		||||
    
 | 
			
		||||
    def _rate_limit(self):
 | 
			
		||||
        """Apply rate limiting for VirusTotal."""
 | 
			
		||||
        now = time.time()
 | 
			
		||||
        time_since_last = now - self.last_request
 | 
			
		||||
        min_interval = 1.0 / self.config.VIRUSTOTAL_RATE_LIMIT
 | 
			
		||||
        
 | 
			
		||||
        if time_since_last < min_interval:
 | 
			
		||||
            time.sleep(min_interval - time_since_last)
 | 
			
		||||
        
 | 
			
		||||
        self.last_request = time.time()
 | 
			
		||||
    
 | 
			
		||||
    def lookup_ip(self, ip: str) -> Optional[VirusTotalResult]:
 | 
			
		||||
        """Lookup IP address reputation."""
 | 
			
		||||
        self._rate_limit()
 | 
			
		||||
        
 | 
			
		||||
        try:
 | 
			
		||||
            url = f"{self.BASE_URL}/ip-address/report"
 | 
			
		||||
            params = {
 | 
			
		||||
                'apikey': self.api_key,
 | 
			
		||||
                'ip': ip
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            response = requests.get(url, params=params, timeout=self.config.HTTP_TIMEOUT)
 | 
			
		||||
            
 | 
			
		||||
            if response.status_code == 200:
 | 
			
		||||
                data = response.json()
 | 
			
		||||
                
 | 
			
		||||
                if data.get('response_code') == 1:
 | 
			
		||||
                    return VirusTotalResult(
 | 
			
		||||
                        resource=ip,
 | 
			
		||||
                        positives=data.get('detected_urls', []) and len([
 | 
			
		||||
                            url for url in data.get('detected_urls', []) 
 | 
			
		||||
                            if url.get('positives', 0) > 0
 | 
			
		||||
                        ]) or 0,
 | 
			
		||||
                        total=len(data.get('detected_urls', [])),
 | 
			
		||||
                        scan_date=datetime.fromisoformat(
 | 
			
		||||
                            data.get('scan_date', datetime.now().isoformat())
 | 
			
		||||
                        ) if data.get('scan_date') else datetime.now(),
 | 
			
		||||
                        permalink=data.get('permalink', '')
 | 
			
		||||
                    )
 | 
			
		||||
                    
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            print(f"Error querying VirusTotal for {ip}: {e}")
 | 
			
		||||
        
 | 
			
		||||
        return None
 | 
			
		||||
    
 | 
			
		||||
    def lookup_domain(self, domain: str) -> Optional[VirusTotalResult]:
 | 
			
		||||
        """Lookup domain reputation."""
 | 
			
		||||
        self._rate_limit()
 | 
			
		||||
        
 | 
			
		||||
        try:
 | 
			
		||||
            url = f"{self.BASE_URL}/domain/report"
 | 
			
		||||
            params = {
 | 
			
		||||
                'apikey': self.api_key,
 | 
			
		||||
                'domain': domain
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            response = requests.get(url, params=params, timeout=self.config.HTTP_TIMEOUT)
 | 
			
		||||
            
 | 
			
		||||
            if response.status_code == 200:
 | 
			
		||||
                data = response.json()
 | 
			
		||||
                
 | 
			
		||||
                if data.get('response_code') == 1:
 | 
			
		||||
                    return VirusTotalResult(
 | 
			
		||||
                        resource=domain,
 | 
			
		||||
                        positives=data.get('detected_urls', []) and len([
 | 
			
		||||
                            url for url in data.get('detected_urls', []) 
 | 
			
		||||
                            if url.get('positives', 0) > 0
 | 
			
		||||
                        ]) or 0,
 | 
			
		||||
                        total=len(data.get('detected_urls', [])),
 | 
			
		||||
                        scan_date=datetime.fromisoformat(
 | 
			
		||||
                            data.get('scan_date', datetime.now().isoformat())
 | 
			
		||||
                        ) if data.get('scan_date') else datetime.now(),
 | 
			
		||||
                        permalink=data.get('permalink', '')
 | 
			
		||||
                    )
 | 
			
		||||
                    
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            print(f"Error querying VirusTotal for {domain}: {e}")
 | 
			
		||||
        
 | 
			
		||||
        return None
 | 
			
		||||
							
								
								
									
										139
									
								
								src/web_app.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								src/web_app.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,139 @@
 | 
			
		||||
# File: src/web_app.py
 | 
			
		||||
"""Flask web application for reconnaissance tool."""
 | 
			
		||||
 | 
			
		||||
from flask import Flask, render_template, request, jsonify, send_from_directory
 | 
			
		||||
import threading
 | 
			
		||||
import time
 | 
			
		||||
from .config import Config
 | 
			
		||||
from .reconnaissance import ReconnaissanceEngine
 | 
			
		||||
from .report_generator import ReportGenerator
 | 
			
		||||
 | 
			
		||||
# Global variables for tracking ongoing scans
 | 
			
		||||
active_scans = {}
 | 
			
		||||
scan_lock = threading.Lock()
 | 
			
		||||
 | 
			
		||||
def create_app(config: Config):
 | 
			
		||||
    """Create Flask application."""
 | 
			
		||||
    app = Flask(__name__, 
 | 
			
		||||
                template_folder='../templates',
 | 
			
		||||
                static_folder='../static')
 | 
			
		||||
    
 | 
			
		||||
    app.config['SECRET_KEY'] = 'recon-tool-secret-key'
 | 
			
		||||
    
 | 
			
		||||
    @app.route('/')
 | 
			
		||||
    def index():
 | 
			
		||||
        """Main page."""
 | 
			
		||||
        return render_template('index.html')
 | 
			
		||||
    
 | 
			
		||||
    @app.route('/api/scan', methods=['POST'])
 | 
			
		||||
    def start_scan():
 | 
			
		||||
        """Start a new reconnaissance scan."""
 | 
			
		||||
        data = request.get_json()
 | 
			
		||||
        target = data.get('target')
 | 
			
		||||
        scan_config = Config.from_args(
 | 
			
		||||
            shodan_key=data.get('shodan_key'),
 | 
			
		||||
            virustotal_key=data.get('virustotal_key'),
 | 
			
		||||
            max_depth=data.get('max_depth', 2)
 | 
			
		||||
        )
 | 
			
		||||
        
 | 
			
		||||
        if not target:
 | 
			
		||||
            return jsonify({'error': 'Target is required'}), 400
 | 
			
		||||
        
 | 
			
		||||
        # Generate scan ID
 | 
			
		||||
        scan_id = f"{target}_{int(time.time())}"
 | 
			
		||||
        
 | 
			
		||||
        # Initialize scan data
 | 
			
		||||
        with scan_lock:
 | 
			
		||||
            active_scans[scan_id] = {
 | 
			
		||||
                'status': 'starting',
 | 
			
		||||
                'progress': 0,
 | 
			
		||||
                'message': 'Initializing...',
 | 
			
		||||
                'data': None,
 | 
			
		||||
                'error': None
 | 
			
		||||
            }
 | 
			
		||||
        
 | 
			
		||||
        # Start reconnaissance in background thread
 | 
			
		||||
        thread = threading.Thread(
 | 
			
		||||
            target=run_reconnaissance_background,
 | 
			
		||||
            args=(scan_id, target, scan_config)
 | 
			
		||||
        )
 | 
			
		||||
        thread.daemon = True
 | 
			
		||||
        thread.start()
 | 
			
		||||
        
 | 
			
		||||
        return jsonify({'scan_id': scan_id})
 | 
			
		||||
    
 | 
			
		||||
    @app.route('/api/scan/<scan_id>/status')
 | 
			
		||||
    def get_scan_status(scan_id):
 | 
			
		||||
        """Get scan status and progress."""
 | 
			
		||||
        with scan_lock:
 | 
			
		||||
            if scan_id not in active_scans:
 | 
			
		||||
                return jsonify({'error': 'Scan not found'}), 404
 | 
			
		||||
            
 | 
			
		||||
            scan_data = active_scans[scan_id].copy()
 | 
			
		||||
 | 
			
		||||
        # Convert ReconData object to a dict to make it JSON serializable
 | 
			
		||||
        if scan_data.get('data'):
 | 
			
		||||
            scan_data['data'] = scan_data['data'].to_dict()
 | 
			
		||||
        
 | 
			
		||||
        return jsonify(scan_data)
 | 
			
		||||
    
 | 
			
		||||
    @app.route('/api/scan/<scan_id>/report')
 | 
			
		||||
    def get_scan_report(scan_id):
 | 
			
		||||
        """Get scan report."""
 | 
			
		||||
        with scan_lock:
 | 
			
		||||
            if scan_id not in active_scans:
 | 
			
		||||
                return jsonify({'error': 'Scan not found'}), 404
 | 
			
		||||
            
 | 
			
		||||
            scan_data = active_scans[scan_id]
 | 
			
		||||
            
 | 
			
		||||
            if scan_data['status'] != 'completed' or not scan_data['data']:
 | 
			
		||||
                return jsonify({'error': 'Scan not completed'}), 400
 | 
			
		||||
            
 | 
			
		||||
            # Generate report
 | 
			
		||||
            report_gen = ReportGenerator(scan_data['data'])
 | 
			
		||||
            
 | 
			
		||||
            return jsonify({
 | 
			
		||||
                'json_report': scan_data['data'].to_dict(), # Use to_dict for a clean JSON object
 | 
			
		||||
                'text_report': report_gen.generate_text_report()
 | 
			
		||||
            })
 | 
			
		||||
    
 | 
			
		||||
    return app
 | 
			
		||||
 | 
			
		||||
def run_reconnaissance_background(scan_id: str, target: str, config: Config):
 | 
			
		||||
    """Run reconnaissance in background thread."""
 | 
			
		||||
    
 | 
			
		||||
    def update_progress(message: str, percentage: int = None):
 | 
			
		||||
        """Update scan progress."""
 | 
			
		||||
        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
 | 
			
		||||
    
 | 
			
		||||
    try:
 | 
			
		||||
        # Initialize engine
 | 
			
		||||
        engine = ReconnaissanceEngine(config)
 | 
			
		||||
        engine.set_progress_callback(update_progress)
 | 
			
		||||
        
 | 
			
		||||
        # Update status
 | 
			
		||||
        with scan_lock:
 | 
			
		||||
            active_scans[scan_id]['status'] = 'running'
 | 
			
		||||
        
 | 
			
		||||
        # Run reconnaissance
 | 
			
		||||
        data = engine.run_reconnaissance(target)
 | 
			
		||||
        
 | 
			
		||||
        # Update with results
 | 
			
		||||
        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
 | 
			
		||||
    
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        # Handle errors
 | 
			
		||||
        with scan_lock:
 | 
			
		||||
            active_scans[scan_id]['status'] = 'error'
 | 
			
		||||
            active_scans[scan_id]['error'] = str(e)
 | 
			
		||||
            active_scans[scan_id]['message'] = f'Error: {str(e)}'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										280
									
								
								static/script.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										280
									
								
								static/script.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,280 @@
 | 
			
		||||
// DNS Reconnaissance Tool - Frontend JavaScript
 | 
			
		||||
 | 
			
		||||
class ReconTool {
 | 
			
		||||
    constructor() {
 | 
			
		||||
        this.currentScanId = null;
 | 
			
		||||
        this.pollInterval = null;
 | 
			
		||||
        this.currentReport = null;
 | 
			
		||||
        this.init();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    init() {
 | 
			
		||||
        this.bindEvents();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    bindEvents() {
 | 
			
		||||
        // Start scan button
 | 
			
		||||
        document.getElementById('startScan').addEventListener('click', () => {
 | 
			
		||||
            this.startScan();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // New scan button
 | 
			
		||||
        document.getElementById('newScan').addEventListener('click', () => {
 | 
			
		||||
            this.resetToForm();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Report view toggles
 | 
			
		||||
        document.getElementById('showJson').addEventListener('click', () => {
 | 
			
		||||
            this.showReport('json');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        document.getElementById('showText').addEventListener('click', () => {
 | 
			
		||||
            this.showReport('text');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Download buttons
 | 
			
		||||
        document.getElementById('downloadJson').addEventListener('click', () => {
 | 
			
		||||
            this.downloadReport('json');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        document.getElementById('downloadText').addEventListener('click', () => {
 | 
			
		||||
            this.downloadReport('text');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Enter key in target field
 | 
			
		||||
        document.getElementById('target').addEventListener('keypress', (e) => {
 | 
			
		||||
            if (e.key === 'Enter') {
 | 
			
		||||
                this.startScan();
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async startScan() {
 | 
			
		||||
        const target = document.getElementById('target').value.trim();
 | 
			
		||||
        
 | 
			
		||||
        if (!target) {
 | 
			
		||||
            alert('Please enter a target domain or hostname');
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const scanData = {
 | 
			
		||||
            target: target,
 | 
			
		||||
            max_depth: parseInt(document.getElementById('maxDepth').value),
 | 
			
		||||
            shodan_key: document.getElementById('shodanKey').value.trim() || null,
 | 
			
		||||
            virustotal_key: document.getElementById('virustotalKey').value.trim() || null
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            // Show progress section
 | 
			
		||||
            this.showProgressSection();
 | 
			
		||||
            this.updateProgress(0, 'Starting scan...');
 | 
			
		||||
 | 
			
		||||
            const response = await fetch('/api/scan', {
 | 
			
		||||
                method: 'POST',
 | 
			
		||||
                headers: {
 | 
			
		||||
                    'Content-Type': 'application/json',
 | 
			
		||||
                },
 | 
			
		||||
                body: JSON.stringify(scanData)
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (!response.ok) {
 | 
			
		||||
                throw new Error(`HTTP error! status: ${response.status}`);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const result = await response.json();
 | 
			
		||||
            
 | 
			
		||||
            if (result.error) {
 | 
			
		||||
                throw new Error(result.error);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.currentScanId = result.scan_id;
 | 
			
		||||
            this.startPolling();
 | 
			
		||||
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            this.showError(`Failed to start scan: ${error.message}`);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    startPolling() {
 | 
			
		||||
        // Poll every 2 seconds for updates
 | 
			
		||||
        this.pollInterval = setInterval(() => {
 | 
			
		||||
            this.checkScanStatus();
 | 
			
		||||
        }, 2000);
 | 
			
		||||
        
 | 
			
		||||
        // Also check immediately
 | 
			
		||||
        this.checkScanStatus();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async checkScanStatus() {
 | 
			
		||||
        if (!this.currentScanId) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            const response = await fetch(`/api/scan/${this.currentScanId}/status`);
 | 
			
		||||
            
 | 
			
		||||
            if (!response.ok) {
 | 
			
		||||
                throw new Error(`HTTP error! status: ${response.status}`);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const status = await response.json();
 | 
			
		||||
            
 | 
			
		||||
            if (status.error) {
 | 
			
		||||
                throw new Error(status.error);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Update progress
 | 
			
		||||
            this.updateProgress(status.progress, status.message);
 | 
			
		||||
 | 
			
		||||
            // Check if completed
 | 
			
		||||
            if (status.status === 'completed') {
 | 
			
		||||
                this.stopPolling();
 | 
			
		||||
                await this.loadScanReport();
 | 
			
		||||
            } else if (status.status === 'error') {
 | 
			
		||||
                this.stopPolling();
 | 
			
		||||
                throw new Error(status.error || 'Scan failed');
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            this.stopPolling();
 | 
			
		||||
            this.showError(`Error checking scan status: ${error.message}`);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async loadScanReport() {
 | 
			
		||||
        try {
 | 
			
		||||
            const response = await fetch(`/api/scan/${this.currentScanId}/report`);
 | 
			
		||||
            
 | 
			
		||||
            if (!response.ok) {
 | 
			
		||||
                throw new Error(`HTTP error! status: ${response.status}`);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const report = await response.json();
 | 
			
		||||
            
 | 
			
		||||
            if (report.error) {
 | 
			
		||||
                throw new Error(report.error);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.currentReport = report;
 | 
			
		||||
            this.showResultsSection();
 | 
			
		||||
            this.showReport('text'); // Default to text view
 | 
			
		||||
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            this.showError(`Error loading report: ${error.message}`);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    stopPolling() {
 | 
			
		||||
        if (this.pollInterval) {
 | 
			
		||||
            clearInterval(this.pollInterval);
 | 
			
		||||
            this.pollInterval = null;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    showProgressSection() {
 | 
			
		||||
        document.getElementById('scanForm').style.display = 'none';
 | 
			
		||||
        document.getElementById('progressSection').style.display = 'block';
 | 
			
		||||
        document.getElementById('resultsSection').style.display = 'none';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    showResultsSection() {
 | 
			
		||||
        document.getElementById('scanForm').style.display = 'none';
 | 
			
		||||
        document.getElementById('progressSection').style.display = 'none';
 | 
			
		||||
        document.getElementById('resultsSection').style.display = 'block';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    resetToForm() {
 | 
			
		||||
        this.stopPolling();
 | 
			
		||||
        this.currentScanId = null;
 | 
			
		||||
        this.currentReport = null;
 | 
			
		||||
        
 | 
			
		||||
        document.getElementById('scanForm').style.display = 'block';
 | 
			
		||||
        document.getElementById('progressSection').style.display = 'none';
 | 
			
		||||
        document.getElementById('resultsSection').style.display = 'none';
 | 
			
		||||
        
 | 
			
		||||
        // Clear form
 | 
			
		||||
        document.getElementById('target').value = '';
 | 
			
		||||
        document.getElementById('shodanKey').value = '';
 | 
			
		||||
        document.getElementById('virustotalKey').value = '';
 | 
			
		||||
        document.getElementById('maxDepth').value = '2';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    updateProgress(percentage, message) {
 | 
			
		||||
        const progressFill = document.getElementById('progressFill');
 | 
			
		||||
        const progressMessage = document.getElementById('progressMessage');
 | 
			
		||||
        
 | 
			
		||||
        progressFill.style.width = `${percentage || 0}%`;
 | 
			
		||||
        progressMessage.textContent = message || 'Processing...';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    showError(message) {
 | 
			
		||||
        // Update progress section to show error
 | 
			
		||||
        this.updateProgress(0, `Error: ${message}`);
 | 
			
		||||
        
 | 
			
		||||
        // Also alert the user
 | 
			
		||||
        alert(`Error: ${message}`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    showReport(type) {
 | 
			
		||||
        if (!this.currentReport) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const reportContent = document.getElementById('reportContent');
 | 
			
		||||
        const showJsonBtn = document.getElementById('showJson');
 | 
			
		||||
        const showTextBtn = document.getElementById('showText');
 | 
			
		||||
 | 
			
		||||
        if (type === 'json') {
 | 
			
		||||
            // Show JSON report
 | 
			
		||||
            try {
 | 
			
		||||
                const jsonData = JSON.parse(this.currentReport.json_report);
 | 
			
		||||
                reportContent.textContent = JSON.stringify(jsonData, null, 2);
 | 
			
		||||
            } catch (e) {
 | 
			
		||||
                reportContent.textContent = this.currentReport.json_report;
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            showJsonBtn.classList.add('active');
 | 
			
		||||
            showTextBtn.classList.remove('active');
 | 
			
		||||
        } else {
 | 
			
		||||
            // Show text report
 | 
			
		||||
            reportContent.textContent = this.currentReport.text_report;
 | 
			
		||||
            
 | 
			
		||||
            showTextBtn.classList.add('active');
 | 
			
		||||
            showJsonBtn.classList.remove('active');
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    downloadReport(type) {
 | 
			
		||||
        if (!this.currentReport) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let content, filename, mimeType;
 | 
			
		||||
 | 
			
		||||
        if (type === 'json') {
 | 
			
		||||
            content = this.currentReport.json_report;
 | 
			
		||||
            filename = `recon-report-${this.currentScanId}.json`;
 | 
			
		||||
            mimeType = 'application/json';
 | 
			
		||||
        } else {
 | 
			
		||||
            content = this.currentReport.text_report;
 | 
			
		||||
            filename = `recon-report-${this.currentScanId}.txt`;
 | 
			
		||||
            mimeType = 'text/plain';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Create download link
 | 
			
		||||
        const blob = new Blob([content], { type: mimeType });
 | 
			
		||||
        const url = window.URL.createObjectURL(blob);
 | 
			
		||||
        const a = document.createElement('a');
 | 
			
		||||
        a.href = url;
 | 
			
		||||
        a.download = filename;
 | 
			
		||||
        document.body.appendChild(a);
 | 
			
		||||
        a.click();
 | 
			
		||||
        window.URL.revokeObjectURL(url);
 | 
			
		||||
        document.body.removeChild(a);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Initialize the application when DOM is loaded
 | 
			
		||||
document.addEventListener('DOMContentLoaded', () => {
 | 
			
		||||
    new ReconTool();
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										258
									
								
								static/style.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										258
									
								
								static/style.css
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,258 @@
 | 
			
		||||
/*
 | 
			
		||||
    ███████╗██████╗ ███████╗ ██████╗████████╗     ██████╗  ██████╗ ██╗  ██╗███████╗
 | 
			
		||||
    ██╔════╝██╔══██╗██╔════╝██╔═══██╗╚══██╔══╝    ██╔═══██╗██╔═══██╗╚██╗██╔╝██╔════╝
 | 
			
		||||
    ███████╗██████╔╝█████╗  ██║   ██║   ██║       ██║   ██║██║   ██║ ╚███╔╝ ███████╗
 | 
			
		||||
    ╚════██║██╔══██╗██╔══╝  ██║   ██║   ██║       ██║   ██║██║   ██║ ██╔██╗ ╚════██║
 | 
			
		||||
    ███████║██║  ██║███████╗╚██████╔╝   ██║       ╚██████╔╝╚██████╔╝██╔╝ ██╗███████║
 | 
			
		||||
    ╚══════╝╚═╝  ╚═╝╚══════╝ ╚═════╝    ╚═╝        ╚═════╝  ╚═════╝ ╚═╝  ╚═╝╚══════╝
 | 
			
		||||
    
 | 
			
		||||
    TACTICAL THEME - DNS RECONNAISSANCE INTERFACE
 | 
			
		||||
    STYLE OVERRIDE
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
* {
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    padding: 0;
 | 
			
		||||
    box-sizing: border-box;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body {
 | 
			
		||||
    font-family: 'Roboto Mono', 'Lucida Console', Monaco, monospace;
 | 
			
		||||
    line-height: 1.6;
 | 
			
		||||
    color: #c7c7c7; /* Light grey for readability */
 | 
			
		||||
    /* Dark, textured background for a gritty feel */
 | 
			
		||||
    background-color: #1a1a1a;
 | 
			
		||||
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3E%3Cpath fill='%23333333' fill-opacity='0.4' d='M1 3h1v1H1V3zm2-2h1v1H3V1z'%3E%3C/path%3E%3C/svg%3E");
 | 
			
		||||
    min-height: 100vh;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.container {
 | 
			
		||||
    max-width: 1200px;
 | 
			
		||||
    margin: 0 auto;
 | 
			
		||||
    padding: 20px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
header {
 | 
			
		||||
    text-align: center;
 | 
			
		||||
    color: #e0e0e0;
 | 
			
		||||
    margin-bottom: 40px;
 | 
			
		||||
    border-bottom: 1px solid #444;
 | 
			
		||||
    padding-bottom: 20px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
header h1 {
 | 
			
		||||
    font-family: 'Special Elite', 'Courier New', monospace; /* Stencil / Typewriter font */
 | 
			
		||||
    font-size: 2.8rem;
 | 
			
		||||
    color: #00ff41; /* Night-vision green */
 | 
			
		||||
    text-shadow: 0 0 5px rgba(0, 255, 65, 0.5);
 | 
			
		||||
    margin-bottom: 10px;
 | 
			
		||||
    letter-spacing: 2px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
header p {
 | 
			
		||||
    font-size: 1.1rem;
 | 
			
		||||
    color: #a0a0a0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.scan-form, .progress-section, .results-section {
 | 
			
		||||
    background: #2a2a2a; /* Dark charcoal */
 | 
			
		||||
    border-radius: 4px; /* Sharper edges */
 | 
			
		||||
    border: 1px solid #444;
 | 
			
		||||
    box-shadow: inset 0 0 15px rgba(0,0,0,0.5);
 | 
			
		||||
    padding: 30px;
 | 
			
		||||
    margin-bottom: 25px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.scan-form h2, .progress-section h2, .results-section h2 {
 | 
			
		||||
    margin-bottom: 20px;
 | 
			
		||||
    color: #e0e0e0;
 | 
			
		||||
    border-bottom: 1px solid #555;
 | 
			
		||||
    padding-bottom: 10px;
 | 
			
		||||
    text-transform: uppercase; /* Military style */
 | 
			
		||||
    letter-spacing: 1px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.form-group {
 | 
			
		||||
    margin-bottom: 20px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.form-group label {
 | 
			
		||||
    display: block;
 | 
			
		||||
    margin-bottom: 8px;
 | 
			
		||||
    font-weight: 600;
 | 
			
		||||
    color: #b0b0b0;
 | 
			
		||||
    text-transform: uppercase;
 | 
			
		||||
    font-size: 0.9rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.form-group input, .form-group select {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    padding: 12px;
 | 
			
		||||
    background: #1a1a1a;
 | 
			
		||||
    border: 1px solid #555;
 | 
			
		||||
    border-radius: 2px;
 | 
			
		||||
    font-size: 16px;
 | 
			
		||||
    color: #00ff41; /* Green text for input fields */
 | 
			
		||||
    font-family: 'Roboto Mono', monospace;
 | 
			
		||||
    transition: all 0.2s ease-in-out;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.form-group input:focus, .form-group select:focus {
 | 
			
		||||
    outline: none;
 | 
			
		||||
    border-color: #ff9900; /* Amber focus color */
 | 
			
		||||
    box-shadow: 0 0 5px rgba(255, 153, 0, 0.5);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.api-keys {
 | 
			
		||||
    background: rgba(0,0,0,0.3);
 | 
			
		||||
    padding: 20px;
 | 
			
		||||
    border-radius: 4px;
 | 
			
		||||
    border: 1px solid #444;
 | 
			
		||||
    margin: 20px 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.api-keys h3 {
 | 
			
		||||
    margin-bottom: 15px;
 | 
			
		||||
    color: #c7c7c7;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.btn-primary, .btn-secondary {
 | 
			
		||||
    padding: 12px 24px;
 | 
			
		||||
    border: 1px solid #666;
 | 
			
		||||
    border-radius: 2px;
 | 
			
		||||
    font-size: 16px;
 | 
			
		||||
    font-weight: 600;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    transition: all 0.2s ease-in-out;
 | 
			
		||||
    margin-right: 10px;
 | 
			
		||||
    margin-bottom: 10px;
 | 
			
		||||
    text-transform: uppercase;
 | 
			
		||||
    letter-spacing: 1px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.btn-primary {
 | 
			
		||||
    background: #2c5c34; /* Dark military green */
 | 
			
		||||
    color: #e0e0e0;
 | 
			
		||||
    border-color: #3b7b46;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.btn-primary:hover {
 | 
			
		||||
    background: #3b7b46; /* Lighter green on hover */
 | 
			
		||||
    color: #fff;
 | 
			
		||||
    border-color: #4cae5c;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.btn-secondary {
 | 
			
		||||
    background: #4a4a4a; /* Dark grey */
 | 
			
		||||
    color: #c7c7c7;
 | 
			
		||||
    border-color: #666;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.btn-secondary:hover {
 | 
			
		||||
    background: #5a5a5a;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.btn-secondary.active {
 | 
			
		||||
    background: #6a4f2a; /* Amber/Brown for active state */
 | 
			
		||||
    color: #fff;
 | 
			
		||||
    border-color: #ff9900;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.progress-bar {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 20px;
 | 
			
		||||
    background: #1a1a1a;
 | 
			
		||||
    border: 1px solid #555;
 | 
			
		||||
    border-radius: 2px;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    margin-bottom: 15px;
 | 
			
		||||
    padding: 2px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.progress-fill {
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    background: #ff9900; /* Solid amber progress fill */
 | 
			
		||||
    width: 0%;
 | 
			
		||||
    transition: width 0.3s ease;
 | 
			
		||||
    border-radius: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#progressMessage {
 | 
			
		||||
    font-weight: 500;
 | 
			
		||||
    color: #a0a0a0;
 | 
			
		||||
    margin-bottom: 20px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.scan-controls {
 | 
			
		||||
    text-align: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.results-controls {
 | 
			
		||||
    margin-bottom: 20px;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.report-container {
 | 
			
		||||
    background: #0a0a0a; /* Near-black terminal background */
 | 
			
		||||
    border-radius: 4px;
 | 
			
		||||
    border: 1px solid #333;
 | 
			
		||||
    padding: 20px;
 | 
			
		||||
    max-height: 600px;
 | 
			
		||||
    overflow-y: auto;
 | 
			
		||||
    box-shadow: inset 0 0 10px #000;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#reportContent {
 | 
			
		||||
    color: #00ff41; /* Classic terminal green */
 | 
			
		||||
    font-family: 'Courier New', monospace;
 | 
			
		||||
    font-size: 14px;
 | 
			
		||||
    line-height: 1.4;
 | 
			
		||||
    white-space: pre-wrap;
 | 
			
		||||
    word-wrap: break-word;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 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); }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										80
									
								
								templates/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								templates/index.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,80 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
<head>
 | 
			
		||||
    <meta charset="UTF-8">
 | 
			
		||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
			
		||||
    <title>DNS Reconnaissance Tool</title>
 | 
			
		||||
    <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
    <div class="container">
 | 
			
		||||
        <header>
 | 
			
		||||
            <h1>🔍 DNS Reconnaissance Tool</h1>
 | 
			
		||||
            <p>Comprehensive domain and IP intelligence gathering</p>
 | 
			
		||||
        </header>
 | 
			
		||||
 | 
			
		||||
        <div class="scan-form" id="scanForm">
 | 
			
		||||
            <h2>Start New Scan</h2>
 | 
			
		||||
            
 | 
			
		||||
            <div class="form-group">
 | 
			
		||||
                <label for="target">Target (domain.com or hostname):</label>
 | 
			
		||||
                <input type="text" id="target" placeholder="example.com or example" required>
 | 
			
		||||
            </div>
 | 
			
		||||
            
 | 
			
		||||
            <div class="form-group">
 | 
			
		||||
                <label for="maxDepth">Max Recursion Depth:</label>
 | 
			
		||||
                <select id="maxDepth">
 | 
			
		||||
                    <option value="1">1</option>
 | 
			
		||||
                    <option value="2" selected>2</option>
 | 
			
		||||
                    <option value="3">3</option>
 | 
			
		||||
                    <option value="4">4</option>
 | 
			
		||||
                    <option value="5">5</option>
 | 
			
		||||
                </select>
 | 
			
		||||
            </div>
 | 
			
		||||
            
 | 
			
		||||
            <div class="api-keys">
 | 
			
		||||
                <h3>Optional API Keys</h3>
 | 
			
		||||
                <div class="form-group">
 | 
			
		||||
                    <label for="shodanKey">Shodan API Key:</label>
 | 
			
		||||
                    <input type="password" id="shodanKey" placeholder="Optional - for port scanning data">
 | 
			
		||||
                </div>
 | 
			
		||||
                
 | 
			
		||||
                <div class="form-group">
 | 
			
		||||
                    <label for="virustotalKey">VirusTotal API Key:</label>
 | 
			
		||||
                    <input type="password" id="virustotalKey" placeholder="Optional - for security analysis">
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            
 | 
			
		||||
            <button id="startScan" class="btn-primary">Start Reconnaissance</button>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="progress-section" id="progressSection" style="display: none;">
 | 
			
		||||
            <h2>Scan Progress</h2>
 | 
			
		||||
            <div class="progress-bar">
 | 
			
		||||
                <div class="progress-fill" id="progressFill"></div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <p id="progressMessage">Initializing...</p>
 | 
			
		||||
            <div class="scan-controls">
 | 
			
		||||
                <button id="newScan" class="btn-secondary">New Scan</button>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="results-section" id="resultsSection" style="display: none;">
 | 
			
		||||
            <h2>Reconnaissance Results</h2>
 | 
			
		||||
            
 | 
			
		||||
            <div class="results-controls">
 | 
			
		||||
                <button id="showJson" class="btn-secondary">Show JSON</button>
 | 
			
		||||
                <button id="showText" class="btn-secondary active">Show Text Report</button>
 | 
			
		||||
                <button id="downloadJson" class="btn-secondary">Download JSON</button>
 | 
			
		||||
                <button id="downloadText" class="btn-secondary">Download Text</button>
 | 
			
		||||
            </div>
 | 
			
		||||
            
 | 
			
		||||
            <div class="report-container">
 | 
			
		||||
                <pre id="reportContent"></pre>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <script src="{{ url_for('static', filename='script.js') }}"></script>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										1440
									
								
								tlds_cache.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1440
									
								
								tlds_cache.txt
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user