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