453 lines
12 KiB
Python
453 lines
12 KiB
Python
"""
|
|
Configs API blueprint.
|
|
|
|
Handles endpoints for managing scan configuration files, including CSV/YAML upload,
|
|
template download, and config management.
|
|
"""
|
|
|
|
import logging
|
|
import io
|
|
from flask import Blueprint, jsonify, request, send_file
|
|
from werkzeug.utils import secure_filename
|
|
|
|
from web.auth.decorators import api_auth_required
|
|
from web.services.config_service import ConfigService
|
|
|
|
bp = Blueprint('configs', __name__)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@bp.route('', methods=['GET'])
|
|
@api_auth_required
|
|
def list_configs():
|
|
"""
|
|
List all config files with metadata.
|
|
|
|
Returns:
|
|
JSON response with list of configs:
|
|
{
|
|
"configs": [
|
|
{
|
|
"filename": "prod-scan.yaml",
|
|
"title": "Prod Scan",
|
|
"path": "/app/configs/prod-scan.yaml",
|
|
"created_at": "2025-11-15T10:30:00Z",
|
|
"size_bytes": 1234,
|
|
"used_by_schedules": ["Daily Scan"]
|
|
}
|
|
]
|
|
}
|
|
"""
|
|
try:
|
|
config_service = ConfigService()
|
|
configs = config_service.list_configs()
|
|
|
|
logger.info(f"Listed {len(configs)} config files")
|
|
|
|
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('/<filename>', methods=['GET'])
|
|
@api_auth_required
|
|
def get_config(filename: str):
|
|
"""
|
|
Get config file content and parsed data.
|
|
|
|
Args:
|
|
filename: Config filename
|
|
|
|
Returns:
|
|
JSON response with config content:
|
|
{
|
|
"filename": "prod-scan.yaml",
|
|
"content": "title: Prod Scan\n...",
|
|
"parsed": {"title": "Prod Scan", "sites": [...]}
|
|
}
|
|
"""
|
|
try:
|
|
# Sanitize filename
|
|
filename = secure_filename(filename)
|
|
|
|
config_service = ConfigService()
|
|
config_data = config_service.get_config(filename)
|
|
|
|
logger.info(f"Retrieved config file: {filename}")
|
|
|
|
return jsonify(config_data)
|
|
|
|
except FileNotFoundError as e:
|
|
logger.warning(f"Config file not found: {filename}")
|
|
return jsonify({
|
|
'error': 'Not found',
|
|
'message': str(e)
|
|
}), 404
|
|
|
|
except ValueError as e:
|
|
logger.warning(f"Invalid config file: {filename} - {str(e)}")
|
|
return jsonify({
|
|
'error': 'Invalid config',
|
|
'message': str(e)
|
|
}), 400
|
|
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error getting config {filename}: {str(e)}", exc_info=True)
|
|
return jsonify({
|
|
'error': 'Internal server error',
|
|
'message': 'An unexpected error occurred'
|
|
}), 500
|
|
|
|
|
|
@bp.route('/create-from-cidr', methods=['POST'])
|
|
@api_auth_required
|
|
def create_from_cidr():
|
|
"""
|
|
Create config from CIDR range.
|
|
|
|
Request:
|
|
JSON with:
|
|
{
|
|
"title": "My Scan",
|
|
"cidr": "10.0.0.0/24",
|
|
"site_name": "Production" (optional),
|
|
"ping_default": false (optional)
|
|
}
|
|
|
|
Returns:
|
|
JSON response with created config info:
|
|
{
|
|
"success": true,
|
|
"filename": "my-scan.yaml",
|
|
"preview": "title: My Scan\n..."
|
|
}
|
|
"""
|
|
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 'cidr' not in data:
|
|
return jsonify({
|
|
'error': 'Bad request',
|
|
'message': 'Missing required field: cidr'
|
|
}), 400
|
|
|
|
title = data['title']
|
|
cidr = data['cidr']
|
|
site_name = data.get('site_name', None)
|
|
ping_default = data.get('ping_default', False)
|
|
|
|
# Validate title
|
|
if not title or not title.strip():
|
|
return jsonify({
|
|
'error': 'Validation error',
|
|
'message': 'Title cannot be empty'
|
|
}), 400
|
|
|
|
# Create config from CIDR
|
|
config_service = ConfigService()
|
|
filename, yaml_preview = config_service.create_from_cidr(
|
|
title=title,
|
|
cidr=cidr,
|
|
site_name=site_name,
|
|
ping_default=ping_default
|
|
)
|
|
|
|
logger.info(f"Created config from CIDR {cidr}: {filename}")
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'filename': filename,
|
|
'preview': yaml_preview
|
|
})
|
|
|
|
except ValueError as e:
|
|
logger.warning(f"CIDR validation failed: {str(e)}")
|
|
return jsonify({
|
|
'error': 'Validation error',
|
|
'message': str(e)
|
|
}), 400
|
|
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error creating config from CIDR: {str(e)}", exc_info=True)
|
|
return jsonify({
|
|
'error': 'Internal server error',
|
|
'message': 'An unexpected error occurred'
|
|
}), 500
|
|
|
|
|
|
@bp.route('/upload-yaml', methods=['POST'])
|
|
@api_auth_required
|
|
def upload_yaml():
|
|
"""
|
|
Upload YAML config file directly.
|
|
|
|
Request:
|
|
multipart/form-data with 'file' field containing YAML file
|
|
Optional 'filename' field for custom filename
|
|
|
|
Returns:
|
|
JSON response with created config info:
|
|
{
|
|
"success": true,
|
|
"filename": "prod-scan.yaml"
|
|
}
|
|
"""
|
|
try:
|
|
# Check if file is present
|
|
if 'file' not in request.files:
|
|
return jsonify({
|
|
'error': 'Bad request',
|
|
'message': 'No file provided'
|
|
}), 400
|
|
|
|
file = request.files['file']
|
|
|
|
# Check if file is selected
|
|
if file.filename == '':
|
|
return jsonify({
|
|
'error': 'Bad request',
|
|
'message': 'No file selected'
|
|
}), 400
|
|
|
|
# Check file extension
|
|
if not (file.filename.endswith('.yaml') or file.filename.endswith('.yml')):
|
|
return jsonify({
|
|
'error': 'Bad request',
|
|
'message': 'File must be a YAML file (.yaml or .yml extension)'
|
|
}), 400
|
|
|
|
# Read YAML content
|
|
yaml_content = file.read().decode('utf-8')
|
|
|
|
# Get filename (use uploaded filename or custom)
|
|
filename = request.form.get('filename', file.filename)
|
|
filename = secure_filename(filename)
|
|
|
|
# Create config from YAML
|
|
config_service = ConfigService()
|
|
final_filename = config_service.create_from_yaml(filename, yaml_content)
|
|
|
|
logger.info(f"Created config from YAML upload: {final_filename}")
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'filename': final_filename
|
|
})
|
|
|
|
except ValueError as e:
|
|
logger.warning(f"YAML validation failed: {str(e)}")
|
|
return jsonify({
|
|
'error': 'Validation error',
|
|
'message': str(e)
|
|
}), 400
|
|
|
|
except UnicodeDecodeError:
|
|
logger.warning("YAML file encoding error")
|
|
return jsonify({
|
|
'error': 'Encoding error',
|
|
'message': 'YAML file must be UTF-8 encoded'
|
|
}), 400
|
|
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error uploading YAML: {str(e)}", exc_info=True)
|
|
return jsonify({
|
|
'error': 'Internal server error',
|
|
'message': 'An unexpected error occurred'
|
|
}), 500
|
|
|
|
|
|
|
|
|
|
@bp.route('/<filename>/download', methods=['GET'])
|
|
@api_auth_required
|
|
def download_config(filename: str):
|
|
"""
|
|
Download existing config file.
|
|
|
|
Args:
|
|
filename: Config filename
|
|
|
|
Returns:
|
|
YAML file download
|
|
"""
|
|
try:
|
|
# Sanitize filename
|
|
filename = secure_filename(filename)
|
|
|
|
config_service = ConfigService()
|
|
config_data = config_service.get_config(filename)
|
|
|
|
# Create file-like object
|
|
yaml_file = io.BytesIO(config_data['content'].encode('utf-8'))
|
|
yaml_file.seek(0)
|
|
|
|
logger.info(f"Config file downloaded: {filename}")
|
|
|
|
# Send file
|
|
return send_file(
|
|
yaml_file,
|
|
mimetype='application/x-yaml',
|
|
as_attachment=True,
|
|
download_name=filename
|
|
)
|
|
|
|
except FileNotFoundError as e:
|
|
logger.warning(f"Config file not found: {filename}")
|
|
return jsonify({
|
|
'error': 'Not found',
|
|
'message': str(e)
|
|
}), 404
|
|
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error downloading config {filename}: {str(e)}", exc_info=True)
|
|
return jsonify({
|
|
'error': 'Internal server error',
|
|
'message': 'An unexpected error occurred'
|
|
}), 500
|
|
|
|
|
|
@bp.route('/<filename>', methods=['PUT'])
|
|
@api_auth_required
|
|
def update_config(filename: str):
|
|
"""
|
|
Update existing config file with new YAML content.
|
|
|
|
Args:
|
|
filename: Config filename
|
|
|
|
Request:
|
|
JSON with:
|
|
{
|
|
"content": "title: My Scan\nsites: ..."
|
|
}
|
|
|
|
Returns:
|
|
JSON response with success status:
|
|
{
|
|
"success": true,
|
|
"message": "Config updated successfully"
|
|
}
|
|
|
|
Error responses:
|
|
- 400: Invalid YAML or config structure
|
|
- 404: Config file not found
|
|
- 500: Internal server error
|
|
"""
|
|
try:
|
|
# Sanitize filename
|
|
filename = secure_filename(filename)
|
|
|
|
data = request.get_json()
|
|
|
|
if not data or 'content' not in data:
|
|
return jsonify({
|
|
'error': 'Bad request',
|
|
'message': 'Missing required field: content'
|
|
}), 400
|
|
|
|
yaml_content = data['content']
|
|
|
|
# Update config
|
|
config_service = ConfigService()
|
|
config_service.update_config(filename, yaml_content)
|
|
|
|
logger.info(f"Updated config file: {filename}")
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'message': 'Config updated successfully'
|
|
})
|
|
|
|
except FileNotFoundError as e:
|
|
logger.warning(f"Config file not found: {filename}")
|
|
return jsonify({
|
|
'error': 'Not found',
|
|
'message': str(e)
|
|
}), 404
|
|
|
|
except ValueError as e:
|
|
logger.warning(f"Invalid config content for {filename}: {str(e)}")
|
|
return jsonify({
|
|
'error': 'Validation error',
|
|
'message': str(e)
|
|
}), 400
|
|
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error updating config {filename}: {str(e)}", exc_info=True)
|
|
return jsonify({
|
|
'error': 'Internal server error',
|
|
'message': 'An unexpected error occurred'
|
|
}), 500
|
|
|
|
|
|
@bp.route('/<filename>', methods=['DELETE'])
|
|
@api_auth_required
|
|
def delete_config(filename: str):
|
|
"""
|
|
Delete config file and cascade delete associated schedules.
|
|
|
|
When a config is deleted, all schedules using that config (both enabled
|
|
and disabled) are automatically deleted as well.
|
|
|
|
Args:
|
|
filename: Config filename
|
|
|
|
Returns:
|
|
JSON response with success status:
|
|
{
|
|
"success": true,
|
|
"message": "Config deleted successfully"
|
|
}
|
|
|
|
Error responses:
|
|
- 404: Config file not found
|
|
- 500: Internal server error
|
|
"""
|
|
try:
|
|
# Sanitize filename
|
|
filename = secure_filename(filename)
|
|
|
|
config_service = ConfigService()
|
|
config_service.delete_config(filename)
|
|
|
|
logger.info(f"Deleted config file: {filename}")
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'message': 'Config deleted successfully'
|
|
})
|
|
|
|
except FileNotFoundError as e:
|
|
logger.warning(f"Config file not found: {filename}")
|
|
return jsonify({
|
|
'error': 'Not found',
|
|
'message': str(e)
|
|
}), 404
|
|
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error deleting config {filename}: {str(e)}", exc_info=True)
|
|
return jsonify({
|
|
'error': 'Internal server error',
|
|
'message': 'An unexpected error occurred'
|
|
}), 500
|