""" Configs API blueprint. Handles endpoints for managing scan configurations stored in the database. Provides REST API for creating, updating, and deleting configs that reference sites. """ import logging from flask import Blueprint, jsonify, request, current_app from web.auth.decorators import api_auth_required from web.services.config_service import ConfigService bp = Blueprint('configs', __name__) logger = logging.getLogger(__name__) # ============================================================================ # Database-based Config Endpoints (Primary) # ============================================================================ @bp.route('', methods=['GET']) @api_auth_required def list_configs(): """ List all scan configurations from database. Returns: JSON response with list of configs: { "configs": [ { "id": 1, "title": "Production Scan", "description": "Weekly production scan", "site_count": 3, "sites": [ {"id": 1, "name": "Production DC"}, {"id": 2, "name": "DMZ"} ], "created_at": "2025-11-19T10:30:00Z", "updated_at": "2025-11-19T10:30:00Z" } ] } """ try: config_service = ConfigService(db_session=current_app.db_session) configs = config_service.list_configs_db() logger.info(f"Listed {len(configs)} configs from database") return jsonify({ 'configs': configs }) except Exception as e: logger.error(f"Unexpected error listing configs: {str(e)}", exc_info=True) return jsonify({ 'error': 'Internal server error', 'message': 'An unexpected error occurred' }), 500 @bp.route('', methods=['POST']) @api_auth_required def create_config(): """ Create a new scan configuration in the database. Request: JSON with: { "title": "Production Scan", "description": "Weekly production scan (optional)", "site_ids": [1, 2, 3] } Returns: JSON response with created config: { "success": true, "config": { "id": 1, "title": "Production Scan", "description": "...", "site_count": 3, "sites": [...], "created_at": "2025-11-19T10:30:00Z", "updated_at": "2025-11-19T10:30:00Z" } } Error responses: - 400: Validation error or missing fields - 500: Internal server error """ try: data = request.get_json() if not data: return jsonify({ 'error': 'Bad request', 'message': 'Request body must be JSON' }), 400 # Validate required fields if 'title' not in data: return jsonify({ 'error': 'Bad request', 'message': 'Missing required field: title' }), 400 if 'site_ids' not in data: return jsonify({ 'error': 'Bad request', 'message': 'Missing required field: site_ids' }), 400 title = data['title'] description = data.get('description', None) site_ids = data['site_ids'] if not isinstance(site_ids, list): return jsonify({ 'error': 'Bad request', 'message': 'Field site_ids must be an array' }), 400 # Create config config_service = ConfigService(db_session=current_app.db_session) config = config_service.create_config(title, description, site_ids) logger.info(f"Created config: {config['title']} (ID: {config['id']})") return jsonify({ 'success': True, 'config': config }), 201 except ValueError as e: logger.warning(f"Config validation failed: {str(e)}") return jsonify({ 'error': 'Validation error', 'message': str(e) }), 400 except Exception as e: logger.error(f"Unexpected error creating config: {str(e)}", exc_info=True) return jsonify({ 'error': 'Internal server error', 'message': 'An unexpected error occurred' }), 500 @bp.route('/', methods=['GET']) @api_auth_required def get_config(config_id: int): """ Get a scan configuration by ID. Args: config_id: Configuration ID Returns: JSON response with config details: { "id": 1, "title": "Production Scan", "description": "...", "site_count": 3, "sites": [ { "id": 1, "name": "Production DC", "description": "...", "cidr_count": 5 } ], "created_at": "2025-11-19T10:30:00Z", "updated_at": "2025-11-19T10:30:00Z" } Error responses: - 404: Config not found - 500: Internal server error """ try: config_service = ConfigService(db_session=current_app.db_session) config = config_service.get_config_by_id(config_id) logger.info(f"Retrieved config: {config['title']} (ID: {config_id})") return jsonify(config) except ValueError as e: logger.warning(f"Config not found: {config_id}") return jsonify({ 'error': 'Not found', 'message': str(e) }), 404 except Exception as e: logger.error(f"Unexpected error getting config {config_id}: {str(e)}", exc_info=True) return jsonify({ 'error': 'Internal server error', 'message': 'An unexpected error occurred' }), 500 @bp.route('/', methods=['PUT']) @api_auth_required def update_config(config_id: int): """ Update an existing scan configuration. Args: config_id: Configuration ID Request: JSON with (all fields optional): { "title": "New Title", "description": "New Description", "site_ids": [1, 2, 3] } Returns: JSON response with updated config: { "success": true, "config": {...} } Error responses: - 400: Validation error - 404: Config not found - 500: Internal server error """ try: data = request.get_json() if not data: return jsonify({ 'error': 'Bad request', 'message': 'Request body must be JSON' }), 400 title = data.get('title', None) description = data.get('description', None) site_ids = data.get('site_ids', None) if site_ids is not None and not isinstance(site_ids, list): return jsonify({ 'error': 'Bad request', 'message': 'Field site_ids must be an array' }), 400 # Update config config_service = ConfigService(db_session=current_app.db_session) config = config_service.update_config(config_id, title, description, site_ids) logger.info(f"Updated config: {config['title']} (ID: {config_id})") return jsonify({ 'success': True, 'config': config }) except ValueError as e: if 'not found' in str(e).lower(): logger.warning(f"Config not found: {config_id}") return jsonify({ 'error': 'Not found', 'message': str(e) }), 404 else: logger.warning(f"Config validation failed: {str(e)}") return jsonify({ 'error': 'Validation error', 'message': str(e) }), 400 except Exception as e: logger.error(f"Unexpected error updating config {config_id}: {str(e)}", exc_info=True) return jsonify({ 'error': 'Internal server error', 'message': 'An unexpected error occurred' }), 500 @bp.route('/', methods=['DELETE']) @api_auth_required def delete_config(config_id: int): """ Delete a scan configuration. Args: config_id: Configuration ID Returns: JSON response with success status: { "success": true, "message": "Config deleted successfully" } Error responses: - 404: Config not found - 500: Internal server error """ try: config_service = ConfigService(db_session=current_app.db_session) config_service.delete_config(config_id) logger.info(f"Deleted config (ID: {config_id})") return jsonify({ 'success': True, 'message': 'Config deleted successfully' }) except ValueError as e: logger.warning(f"Config not found: {config_id}") return jsonify({ 'error': 'Not found', 'message': str(e) }), 404 except Exception as e: logger.error(f"Unexpected error deleting config {config_id}: {str(e)}", exc_info=True) return jsonify({ 'error': 'Internal server error', 'message': 'An unexpected error occurred' }), 500 @bp.route('//sites', methods=['POST']) @api_auth_required def add_site_to_config(config_id: int): """ Add a site to an existing config. Args: config_id: Configuration ID Request: JSON with: { "site_id": 5 } Returns: JSON response with updated config: { "success": true, "config": {...} } Error responses: - 400: Validation error or site already in config - 404: Config or site not found - 500: Internal server error """ try: data = request.get_json() if not data or 'site_id' not in data: return jsonify({ 'error': 'Bad request', 'message': 'Missing required field: site_id' }), 400 site_id = data['site_id'] # Add site to config config_service = ConfigService(db_session=current_app.db_session) config = config_service.add_site_to_config(config_id, site_id) logger.info(f"Added site {site_id} to config {config_id}") return jsonify({ 'success': True, 'config': config }) except ValueError as e: if 'not found' in str(e).lower(): logger.warning(f"Config or site not found: {str(e)}") return jsonify({ 'error': 'Not found', 'message': str(e) }), 404 else: logger.warning(f"Validation error: {str(e)}") return jsonify({ 'error': 'Validation error', 'message': str(e) }), 400 except Exception as e: logger.error(f"Unexpected error adding site to config: {str(e)}", exc_info=True) return jsonify({ 'error': 'Internal server error', 'message': 'An unexpected error occurred' }), 500 @bp.route('//sites/', methods=['DELETE']) @api_auth_required def remove_site_from_config(config_id: int, site_id: int): """ Remove a site from a config. Args: config_id: Configuration ID site_id: Site ID to remove Returns: JSON response with updated config: { "success": true, "config": {...} } Error responses: - 400: Validation error (e.g., last site cannot be removed) - 404: Config not found or site not in config - 500: Internal server error """ try: config_service = ConfigService(db_session=current_app.db_session) config = config_service.remove_site_from_config(config_id, site_id) logger.info(f"Removed site {site_id} from config {config_id}") return jsonify({ 'success': True, 'config': config }) except ValueError as e: if 'not found' in str(e).lower() or 'not in this config' in str(e).lower(): logger.warning(f"Config or site not found: {str(e)}") return jsonify({ 'error': 'Not found', 'message': str(e) }), 404 else: logger.warning(f"Validation error: {str(e)}") return jsonify({ 'error': 'Validation error', 'message': str(e) }), 400 except Exception as e: logger.error(f"Unexpected error removing site from config: {str(e)}", exc_info=True) return jsonify({ 'error': 'Internal server error', 'message': 'An unexpected error occurred' }), 500