462 lines
13 KiB
Python
462 lines
13 KiB
Python
"""
|
|
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('/<int:config_id>', 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('/<int:config_id>', 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('/<int:config_id>', 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('/<int:config_id>/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('/<int:config_id>/sites/<int:site_id>', 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
|