339 lines
9.9 KiB
Python
339 lines
9.9 KiB
Python
"""
|
|
Flask application entry point for DNSRecon web interface.
|
|
Provides REST API endpoints and serves the web interface.
|
|
"""
|
|
|
|
import json
|
|
import traceback
|
|
from flask import Flask, render_template, request, jsonify, send_file
|
|
from datetime import datetime, timezone
|
|
import io
|
|
|
|
from core.scanner import scanner
|
|
from config import config
|
|
|
|
|
|
app = Flask(__name__)
|
|
app.config['SECRET_KEY'] = 'dnsrecon-dev-key-change-in-production'
|
|
|
|
|
|
@app.route('/')
|
|
def index():
|
|
"""Serve the main web interface."""
|
|
return render_template('index.html')
|
|
|
|
|
|
@app.route('/api/scan/start', methods=['POST'])
|
|
def start_scan():
|
|
"""
|
|
Start a new reconnaissance scan.
|
|
|
|
Expects JSON payload:
|
|
{
|
|
"target_domain": "example.com",
|
|
"max_depth": 2
|
|
}
|
|
"""
|
|
print("=== API: /api/scan/start called ===")
|
|
|
|
try:
|
|
print("Getting JSON data from request...")
|
|
data = request.get_json()
|
|
print(f"Request data: {data}")
|
|
|
|
if not data or 'target_domain' not in data:
|
|
print("ERROR: Missing target_domain in request")
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'Missing target_domain in request'
|
|
}), 400
|
|
|
|
target_domain = data['target_domain'].strip()
|
|
max_depth = data.get('max_depth', config.default_recursion_depth)
|
|
|
|
print(f"Parsed - target_domain: '{target_domain}', max_depth: {max_depth}")
|
|
|
|
# Validation
|
|
if not target_domain:
|
|
print("ERROR: Target domain cannot be empty")
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'Target domain cannot be empty'
|
|
}), 400
|
|
|
|
if not isinstance(max_depth, int) or max_depth < 1 or max_depth > 5:
|
|
print(f"ERROR: Invalid max_depth: {max_depth}")
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'Max depth must be an integer between 1 and 5'
|
|
}), 400
|
|
|
|
print("Validation passed, calling scanner.start_scan...")
|
|
|
|
# Start scan
|
|
success = scanner.start_scan(target_domain, max_depth)
|
|
|
|
print(f"scanner.start_scan returned: {success}")
|
|
|
|
if success:
|
|
session_id = scanner.logger.session_id
|
|
print(f"Scan started successfully with session ID: {session_id}")
|
|
return jsonify({
|
|
'success': True,
|
|
'message': 'Scan started successfully',
|
|
'scan_id': session_id
|
|
})
|
|
else:
|
|
print("ERROR: Scanner returned False")
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'Failed to start scan (scan may already be running)'
|
|
}), 409
|
|
|
|
except Exception as e:
|
|
print(f"ERROR: Exception in start_scan endpoint: {e}")
|
|
traceback.print_exc()
|
|
return jsonify({
|
|
'success': False,
|
|
'error': f'Internal server error: {str(e)}'
|
|
}), 500
|
|
|
|
|
|
@app.route('/api/scan/stop', methods=['POST'])
|
|
def stop_scan():
|
|
"""Stop the current scan."""
|
|
print("=== API: /api/scan/stop called ===")
|
|
|
|
try:
|
|
success = scanner.stop_scan()
|
|
|
|
if success:
|
|
return jsonify({
|
|
'success': True,
|
|
'message': 'Scan stop requested'
|
|
})
|
|
else:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'No active scan to stop'
|
|
}), 400
|
|
|
|
except Exception as e:
|
|
print(f"ERROR: Exception in stop_scan endpoint: {e}")
|
|
traceback.print_exc()
|
|
return jsonify({
|
|
'success': False,
|
|
'error': f'Internal server error: {str(e)}'
|
|
}), 500
|
|
|
|
|
|
@app.route('/api/scan/status', methods=['GET'])
|
|
def get_scan_status():
|
|
"""Get current scan status and progress."""
|
|
try:
|
|
status = scanner.get_scan_status()
|
|
return jsonify({
|
|
'success': True,
|
|
'status': status
|
|
})
|
|
|
|
except Exception as e:
|
|
print(f"ERROR: Exception in get_scan_status endpoint: {e}")
|
|
traceback.print_exc()
|
|
return jsonify({
|
|
'success': False,
|
|
'error': f'Internal server error: {str(e)}'
|
|
}), 500
|
|
|
|
|
|
@app.route('/api/graph', methods=['GET'])
|
|
def get_graph_data():
|
|
"""Get current graph data for visualization."""
|
|
try:
|
|
graph_data = scanner.get_graph_data()
|
|
return jsonify({
|
|
'success': True,
|
|
'graph': graph_data
|
|
})
|
|
|
|
except Exception as e:
|
|
print(f"ERROR: Exception in get_graph_data endpoint: {e}")
|
|
traceback.print_exc()
|
|
return jsonify({
|
|
'success': False,
|
|
'error': f'Internal server error: {str(e)}'
|
|
}), 500
|
|
|
|
|
|
@app.route('/api/export', methods=['GET'])
|
|
def export_results():
|
|
"""Export complete scan results as downloadable JSON."""
|
|
try:
|
|
# Get complete results
|
|
results = scanner.export_results()
|
|
|
|
# Create filename with timestamp
|
|
timestamp = datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')
|
|
target = scanner.current_target or 'unknown'
|
|
filename = f"dnsrecon_{target}_{timestamp}.json"
|
|
|
|
# Create in-memory file
|
|
json_data = json.dumps(results, indent=2, ensure_ascii=False)
|
|
file_obj = io.BytesIO(json_data.encode('utf-8'))
|
|
|
|
return send_file(
|
|
file_obj,
|
|
as_attachment=True,
|
|
download_name=filename,
|
|
mimetype='application/json'
|
|
)
|
|
|
|
except Exception as e:
|
|
print(f"ERROR: Exception in export_results endpoint: {e}")
|
|
traceback.print_exc()
|
|
return jsonify({
|
|
'success': False,
|
|
'error': f'Export failed: {str(e)}'
|
|
}), 500
|
|
|
|
|
|
@app.route('/api/providers', methods=['GET'])
|
|
def get_providers():
|
|
"""Get information about available providers."""
|
|
print("=== API: /api/providers called ===")
|
|
|
|
try:
|
|
provider_stats = scanner.get_provider_statistics()
|
|
|
|
# Add configuration information
|
|
provider_info = {}
|
|
for provider_name, stats in provider_stats.items():
|
|
provider_info[provider_name] = {
|
|
'statistics': stats,
|
|
'enabled': config.is_provider_enabled(provider_name),
|
|
'rate_limit': config.get_rate_limit(provider_name),
|
|
'requires_api_key': provider_name in ['shodan', 'virustotal']
|
|
}
|
|
|
|
print(f"Returning provider info: {list(provider_info.keys())}")
|
|
return jsonify({
|
|
'success': True,
|
|
'providers': provider_info
|
|
})
|
|
|
|
except Exception as e:
|
|
print(f"ERROR: Exception in get_providers endpoint: {e}")
|
|
traceback.print_exc()
|
|
return jsonify({
|
|
'success': False,
|
|
'error': f'Internal server error: {str(e)}'
|
|
}), 500
|
|
|
|
|
|
@app.route('/api/config/api-keys', methods=['POST'])
|
|
def set_api_keys():
|
|
"""
|
|
Set API keys for providers (stored in memory only).
|
|
|
|
Expects JSON payload:
|
|
{
|
|
"shodan": "api_key_here",
|
|
"virustotal": "api_key_here"
|
|
}
|
|
"""
|
|
try:
|
|
data = request.get_json()
|
|
|
|
if not data:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'No API keys provided'
|
|
}), 400
|
|
|
|
updated_providers = []
|
|
|
|
for provider, api_key in data.items():
|
|
if provider in ['shodan', 'virustotal'] and api_key.strip():
|
|
success = config.set_api_key(provider, api_key.strip())
|
|
if success:
|
|
updated_providers.append(provider)
|
|
|
|
if updated_providers:
|
|
# Reinitialize scanner providers
|
|
scanner._initialize_providers()
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'message': f'API keys updated for: {", ".join(updated_providers)}',
|
|
'updated_providers': updated_providers
|
|
})
|
|
else:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'No valid API keys were provided'
|
|
}), 400
|
|
|
|
except Exception as e:
|
|
print(f"ERROR: Exception in set_api_keys endpoint: {e}")
|
|
traceback.print_exc()
|
|
return jsonify({
|
|
'success': False,
|
|
'error': f'Internal server error: {str(e)}'
|
|
}), 500
|
|
|
|
|
|
@app.route('/api/health', methods=['GET'])
|
|
def health_check():
|
|
"""Health check endpoint with enhanced Phase 2 information."""
|
|
return jsonify({
|
|
'success': True,
|
|
'status': 'healthy',
|
|
'timestamp': datetime.now(datetime.UTC).isoformat(),
|
|
'version': '1.0.0-phase2',
|
|
'phase': 2,
|
|
'features': {
|
|
'multi_provider': True,
|
|
'concurrent_processing': True,
|
|
'real_time_updates': True,
|
|
'api_key_management': True,
|
|
'enhanced_visualization': True,
|
|
'retry_logic': True
|
|
},
|
|
'providers_available': len(scanner.providers) if hasattr(scanner, 'providers') else 0
|
|
})
|
|
|
|
|
|
@app.errorhandler(404)
|
|
def not_found(error):
|
|
"""Handle 404 errors."""
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'Endpoint not found'
|
|
}), 404
|
|
|
|
|
|
@app.errorhandler(500)
|
|
def internal_error(error):
|
|
"""Handle 500 errors."""
|
|
print(f"ERROR: 500 Internal Server Error: {error}")
|
|
traceback.print_exc()
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'Internal server error'
|
|
}), 500
|
|
|
|
|
|
if __name__ == '__main__':
|
|
print("Starting DNSRecon Flask application...")
|
|
|
|
# Load configuration from environment
|
|
config.load_from_env()
|
|
|
|
# Start Flask application
|
|
print(f"Starting server on {config.flask_host}:{config.flask_port}")
|
|
app.run(
|
|
host=config.flask_host,
|
|
port=config.flask_port,
|
|
debug=config.flask_debug,
|
|
threaded=True
|
|
) |