flask app

This commit is contained in:
overcuriousity 2025-09-09 13:55:05 +02:00
parent 941d815595
commit 8263f5cfa9
18 changed files with 3461 additions and 1 deletions

107
README.md
View File

@ -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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff