""" 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 )