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