Migrate from file-based configs to database with per-IP site configuration
Major architectural changes: - Replace YAML config files with database-stored ScanConfig model - Remove CIDR block support in favor of individual IP addresses per site - Each IP now has its own expected_ping, expected_tcp_ports, expected_udp_ports - AlertRule now uses config_id FK instead of config_file string API changes: - POST /api/scans now requires config_id instead of config_file - Alert rules API uses config_id with validation - All config dropdowns fetch from /api/configs dynamically Template updates: - scans.html, dashboard.html, alert_rules.html load configs via API - Display format: Config Title (X sites) in dropdowns - Removed Jinja2 config_files loops Migrations: - 008: Expand CIDRs to individual IPs with per-IP port configs - 009: Remove CIDR-related columns - 010: Add config_id to alert_rules, remove config_file
This commit is contained in:
@@ -158,21 +158,12 @@ def create_site():
|
||||
}), 400
|
||||
|
||||
description = data.get('description')
|
||||
cidrs = data.get('cidrs', [])
|
||||
|
||||
# Validate cidrs is a list
|
||||
if not isinstance(cidrs, list):
|
||||
return jsonify({
|
||||
'error': 'Invalid request',
|
||||
'message': 'cidrs must be a list'
|
||||
}), 400
|
||||
|
||||
# Create site
|
||||
# Create site (empty initially)
|
||||
site_service = SiteService(current_app.db_session)
|
||||
site = site_service.create_site(
|
||||
name=name,
|
||||
description=description,
|
||||
cidrs=cidrs if cidrs else None
|
||||
description=description
|
||||
)
|
||||
|
||||
logger.info(f"Created site '{name}' (id={site['id']})")
|
||||
@@ -294,135 +285,178 @@ def delete_site(site_id):
|
||||
}), 500
|
||||
|
||||
|
||||
@bp.route('/<int:site_id>/cidrs', methods=['POST'])
|
||||
@bp.route('/<int:site_id>/ips/bulk', methods=['POST'])
|
||||
@api_auth_required
|
||||
def add_cidr(site_id):
|
||||
def bulk_add_ips(site_id):
|
||||
"""
|
||||
Add a CIDR range to a site.
|
||||
Bulk add IPs to a site from CIDR or list.
|
||||
|
||||
Args:
|
||||
site_id: Site ID
|
||||
|
||||
Request body:
|
||||
cidr: CIDR notation (required, e.g., "10.0.0.0/24")
|
||||
expected_ping: Expected ping response (optional)
|
||||
expected_tcp_ports: List of expected TCP ports (optional)
|
||||
expected_udp_ports: List of expected UDP ports (optional)
|
||||
source_type: "cidr" or "list" (required)
|
||||
cidr: CIDR notation if source_type="cidr" (e.g., "10.0.0.0/24")
|
||||
ips: List of IP addresses if source_type="list" (e.g., ["10.0.0.1", "10.0.0.2"])
|
||||
expected_ping: Expected ping response for all IPs (optional)
|
||||
expected_tcp_ports: List of expected TCP ports for all IPs (optional)
|
||||
expected_udp_ports: List of expected UDP ports for all IPs (optional)
|
||||
|
||||
Returns:
|
||||
JSON response with created CIDR data
|
||||
JSON response with count of IPs added and any errors
|
||||
"""
|
||||
try:
|
||||
data = request.get_json() or {}
|
||||
|
||||
# Validate required fields
|
||||
cidr = data.get('cidr')
|
||||
if not cidr:
|
||||
logger.warning("CIDR creation request missing cidr")
|
||||
source_type = data.get('source_type')
|
||||
if source_type not in ['cidr', 'list']:
|
||||
return jsonify({
|
||||
'error': 'Invalid request',
|
||||
'message': 'cidr is required'
|
||||
'message': 'source_type must be "cidr" or "list"'
|
||||
}), 400
|
||||
|
||||
expected_ping = data.get('expected_ping')
|
||||
expected_tcp_ports = data.get('expected_tcp_ports', [])
|
||||
expected_udp_ports = data.get('expected_udp_ports', [])
|
||||
|
||||
# Add CIDR
|
||||
site_service = SiteService(current_app.db_session)
|
||||
cidr_data = site_service.add_cidr(
|
||||
site_id=site_id,
|
||||
cidr=cidr,
|
||||
expected_ping=expected_ping,
|
||||
expected_tcp_ports=expected_tcp_ports,
|
||||
expected_udp_ports=expected_udp_ports
|
||||
)
|
||||
|
||||
logger.info(f"Added CIDR '{cidr}' to site {site_id}")
|
||||
return jsonify(cidr_data), 201
|
||||
if source_type == 'cidr':
|
||||
cidr = data.get('cidr')
|
||||
if not cidr:
|
||||
return jsonify({
|
||||
'error': 'Invalid request',
|
||||
'message': 'cidr is required when source_type="cidr"'
|
||||
}), 400
|
||||
|
||||
result = site_service.bulk_add_ips_from_cidr(
|
||||
site_id=site_id,
|
||||
cidr=cidr,
|
||||
expected_ping=expected_ping,
|
||||
expected_tcp_ports=expected_tcp_ports,
|
||||
expected_udp_ports=expected_udp_ports
|
||||
)
|
||||
|
||||
logger.info(f"Bulk added {result['ip_count']} IPs from CIDR '{cidr}' to site {site_id}")
|
||||
return jsonify(result), 201
|
||||
|
||||
else: # source_type == 'list'
|
||||
ip_list = data.get('ips', [])
|
||||
if not isinstance(ip_list, list):
|
||||
return jsonify({
|
||||
'error': 'Invalid request',
|
||||
'message': 'ips must be a list when source_type="list"'
|
||||
}), 400
|
||||
|
||||
result = site_service.bulk_add_ips_from_list(
|
||||
site_id=site_id,
|
||||
ip_list=ip_list,
|
||||
expected_ping=expected_ping,
|
||||
expected_tcp_ports=expected_tcp_ports,
|
||||
expected_udp_ports=expected_udp_ports
|
||||
)
|
||||
|
||||
logger.info(f"Bulk added {result['ip_count']} IPs from list to site {site_id}")
|
||||
return jsonify(result), 201
|
||||
|
||||
except ValueError as e:
|
||||
logger.warning(f"Invalid CIDR creation request: {str(e)}")
|
||||
logger.warning(f"Invalid bulk IP request: {str(e)}")
|
||||
return jsonify({
|
||||
'error': 'Invalid request',
|
||||
'message': str(e)
|
||||
}), 400
|
||||
except SQLAlchemyError as e:
|
||||
logger.error(f"Database error adding CIDR to site {site_id}: {str(e)}")
|
||||
logger.error(f"Database error bulk adding IPs to site {site_id}: {str(e)}")
|
||||
return jsonify({
|
||||
'error': 'Database error',
|
||||
'message': 'Failed to add CIDR'
|
||||
'message': 'Failed to add IPs'
|
||||
}), 500
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error adding CIDR to site {site_id}: {str(e)}", exc_info=True)
|
||||
logger.error(f"Unexpected error bulk adding IPs to site {site_id}: {str(e)}", exc_info=True)
|
||||
return jsonify({
|
||||
'error': 'Internal server error',
|
||||
'message': 'An unexpected error occurred'
|
||||
}), 500
|
||||
|
||||
|
||||
@bp.route('/<int:site_id>/cidrs/<int:cidr_id>', methods=['DELETE'])
|
||||
@bp.route('/<int:site_id>/ips', methods=['GET'])
|
||||
@api_auth_required
|
||||
def remove_cidr(site_id, cidr_id):
|
||||
def list_ips(site_id):
|
||||
"""
|
||||
Remove a CIDR range from a site.
|
||||
List IPs in a site with pagination.
|
||||
|
||||
Prevents removal if it's the last CIDR.
|
||||
|
||||
Args:
|
||||
site_id: Site ID
|
||||
cidr_id: CIDR ID
|
||||
Query params:
|
||||
page: Page number (default: 1)
|
||||
per_page: Items per page (default: 50, max: 200)
|
||||
|
||||
Returns:
|
||||
JSON response with success message
|
||||
JSON response with IPs list and pagination info
|
||||
"""
|
||||
try:
|
||||
site_service = SiteService(current_app.db_session)
|
||||
site_service.remove_cidr(site_id, cidr_id)
|
||||
# Get and validate query parameters
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = request.args.get('per_page', 50, type=int)
|
||||
|
||||
# Validate pagination params
|
||||
page, per_page = validate_page_params(page, per_page, max_per_page=200)
|
||||
|
||||
# Get IPs from service
|
||||
site_service = SiteService(current_app.db_session)
|
||||
paginated_result = site_service.list_ips(
|
||||
site_id=site_id,
|
||||
page=page,
|
||||
per_page=per_page
|
||||
)
|
||||
|
||||
logger.info(f"Listed IPs for site {site_id}: page={page}, per_page={per_page}, total={paginated_result.total}")
|
||||
|
||||
logger.info(f"Removed CIDR {cidr_id} from site {site_id}")
|
||||
return jsonify({
|
||||
'message': f'CIDR {cidr_id} removed successfully'
|
||||
'ips': paginated_result.items,
|
||||
'total': paginated_result.total,
|
||||
'page': paginated_result.page,
|
||||
'per_page': paginated_result.per_page,
|
||||
'total_pages': paginated_result.pages,
|
||||
'has_prev': paginated_result.has_prev,
|
||||
'has_next': paginated_result.has_next
|
||||
})
|
||||
|
||||
except ValueError as e:
|
||||
logger.warning(f"Cannot remove CIDR {cidr_id} from site {site_id}: {str(e)}")
|
||||
logger.warning(f"Invalid request parameters: {str(e)}")
|
||||
return jsonify({
|
||||
'error': 'Invalid request',
|
||||
'message': str(e)
|
||||
}), 400
|
||||
except SQLAlchemyError as e:
|
||||
logger.error(f"Database error removing CIDR {cidr_id} from site {site_id}: {str(e)}")
|
||||
logger.error(f"Database error listing IPs for site {site_id}: {str(e)}")
|
||||
return jsonify({
|
||||
'error': 'Database error',
|
||||
'message': 'Failed to remove CIDR'
|
||||
'message': 'Failed to retrieve IPs'
|
||||
}), 500
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error removing CIDR {cidr_id} from site {site_id}: {str(e)}", exc_info=True)
|
||||
logger.error(f"Unexpected error listing IPs for site {site_id}: {str(e)}", exc_info=True)
|
||||
return jsonify({
|
||||
'error': 'Internal server error',
|
||||
'message': 'An unexpected error occurred'
|
||||
}), 500
|
||||
|
||||
|
||||
@bp.route('/<int:site_id>/cidrs/<int:cidr_id>/ips', methods=['POST'])
|
||||
@bp.route('/<int:site_id>/ips', methods=['POST'])
|
||||
@api_auth_required
|
||||
def add_ip_override(site_id, cidr_id):
|
||||
def add_standalone_ip(site_id):
|
||||
"""
|
||||
Add an IP-level expectation override within a CIDR.
|
||||
Add a standalone IP (without CIDR parent) to a site.
|
||||
|
||||
Args:
|
||||
site_id: Site ID (for validation)
|
||||
cidr_id: CIDR ID
|
||||
site_id: Site ID
|
||||
|
||||
Request body:
|
||||
ip_address: IP address (required)
|
||||
expected_ping: Override ping expectation (optional)
|
||||
expected_tcp_ports: Override TCP ports expectation (optional)
|
||||
expected_udp_ports: Override UDP ports expectation (optional)
|
||||
expected_ping: Expected ping response (optional)
|
||||
expected_tcp_ports: List of expected TCP ports (optional)
|
||||
expected_udp_ports: List of expected UDP ports (optional)
|
||||
|
||||
Returns:
|
||||
JSON response with created IP override data
|
||||
JSON response with created IP data
|
||||
"""
|
||||
try:
|
||||
data = request.get_json() or {}
|
||||
@@ -430,7 +464,7 @@ def add_ip_override(site_id, cidr_id):
|
||||
# Validate required fields
|
||||
ip_address = data.get('ip_address')
|
||||
if not ip_address:
|
||||
logger.warning("IP override creation request missing ip_address")
|
||||
logger.warning("Standalone IP creation request missing ip_address")
|
||||
return jsonify({
|
||||
'error': 'Invalid request',
|
||||
'message': 'ip_address is required'
|
||||
@@ -440,76 +474,133 @@ def add_ip_override(site_id, cidr_id):
|
||||
expected_tcp_ports = data.get('expected_tcp_ports', [])
|
||||
expected_udp_ports = data.get('expected_udp_ports', [])
|
||||
|
||||
# Add IP override
|
||||
# Add standalone IP
|
||||
site_service = SiteService(current_app.db_session)
|
||||
ip_data = site_service.add_ip_override(
|
||||
cidr_id=cidr_id,
|
||||
ip_data = site_service.add_standalone_ip(
|
||||
site_id=site_id,
|
||||
ip_address=ip_address,
|
||||
expected_ping=expected_ping,
|
||||
expected_tcp_ports=expected_tcp_ports,
|
||||
expected_udp_ports=expected_udp_ports
|
||||
)
|
||||
|
||||
logger.info(f"Added IP override '{ip_address}' to CIDR {cidr_id} in site {site_id}")
|
||||
logger.info(f"Added standalone IP '{ip_address}' to site {site_id}")
|
||||
return jsonify(ip_data), 201
|
||||
|
||||
except ValueError as e:
|
||||
logger.warning(f"Invalid IP override creation request: {str(e)}")
|
||||
logger.warning(f"Invalid standalone IP creation request: {str(e)}")
|
||||
return jsonify({
|
||||
'error': 'Invalid request',
|
||||
'message': str(e)
|
||||
}), 400
|
||||
except SQLAlchemyError as e:
|
||||
logger.error(f"Database error adding IP override to CIDR {cidr_id}: {str(e)}")
|
||||
logger.error(f"Database error adding standalone IP to site {site_id}: {str(e)}")
|
||||
return jsonify({
|
||||
'error': 'Database error',
|
||||
'message': 'Failed to add IP override'
|
||||
'message': 'Failed to add IP'
|
||||
}), 500
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error adding IP override to CIDR {cidr_id}: {str(e)}", exc_info=True)
|
||||
logger.error(f"Unexpected error adding standalone IP to site {site_id}: {str(e)}", exc_info=True)
|
||||
return jsonify({
|
||||
'error': 'Internal server error',
|
||||
'message': 'An unexpected error occurred'
|
||||
}), 500
|
||||
|
||||
|
||||
@bp.route('/<int:site_id>/cidrs/<int:cidr_id>/ips/<int:ip_id>', methods=['DELETE'])
|
||||
@bp.route('/<int:site_id>/ips/<int:ip_id>', methods=['PUT'])
|
||||
@api_auth_required
|
||||
def remove_ip_override(site_id, cidr_id, ip_id):
|
||||
def update_ip_settings(site_id, ip_id):
|
||||
"""
|
||||
Remove an IP-level override.
|
||||
Update settings for an individual IP.
|
||||
|
||||
Args:
|
||||
site_id: Site ID (for validation)
|
||||
cidr_id: CIDR ID
|
||||
ip_id: IP override ID
|
||||
site_id: Site ID
|
||||
ip_id: IP ID
|
||||
|
||||
Request body:
|
||||
expected_ping: New ping expectation (optional)
|
||||
expected_tcp_ports: New TCP ports expectation (optional)
|
||||
expected_udp_ports: New UDP ports expectation (optional)
|
||||
|
||||
Returns:
|
||||
JSON response with updated IP data
|
||||
"""
|
||||
try:
|
||||
data = request.get_json() or {}
|
||||
|
||||
expected_ping = data.get('expected_ping')
|
||||
expected_tcp_ports = data.get('expected_tcp_ports')
|
||||
expected_udp_ports = data.get('expected_udp_ports')
|
||||
|
||||
# Update IP settings
|
||||
site_service = SiteService(current_app.db_session)
|
||||
ip_data = site_service.update_ip_settings(
|
||||
site_id=site_id,
|
||||
ip_id=ip_id,
|
||||
expected_ping=expected_ping,
|
||||
expected_tcp_ports=expected_tcp_ports,
|
||||
expected_udp_ports=expected_udp_ports
|
||||
)
|
||||
|
||||
logger.info(f"Updated IP {ip_id} in site {site_id}")
|
||||
return jsonify(ip_data)
|
||||
|
||||
except ValueError as e:
|
||||
logger.warning(f"Invalid IP update request: {str(e)}")
|
||||
return jsonify({
|
||||
'error': 'Invalid request',
|
||||
'message': str(e)
|
||||
}), 400
|
||||
except SQLAlchemyError as e:
|
||||
logger.error(f"Database error updating IP {ip_id} in site {site_id}: {str(e)}")
|
||||
return jsonify({
|
||||
'error': 'Database error',
|
||||
'message': 'Failed to update IP'
|
||||
}), 500
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error updating IP {ip_id} in site {site_id}: {str(e)}", exc_info=True)
|
||||
return jsonify({
|
||||
'error': 'Internal server error',
|
||||
'message': 'An unexpected error occurred'
|
||||
}), 500
|
||||
|
||||
|
||||
@bp.route('/<int:site_id>/ips/<int:ip_id>', methods=['DELETE'])
|
||||
@api_auth_required
|
||||
def remove_ip(site_id, ip_id):
|
||||
"""
|
||||
Remove an IP from a site.
|
||||
|
||||
Args:
|
||||
site_id: Site ID
|
||||
ip_id: IP ID
|
||||
|
||||
Returns:
|
||||
JSON response with success message
|
||||
"""
|
||||
try:
|
||||
site_service = SiteService(current_app.db_session)
|
||||
site_service.remove_ip_override(cidr_id, ip_id)
|
||||
site_service.remove_ip(site_id, ip_id)
|
||||
|
||||
logger.info(f"Removed IP override {ip_id} from CIDR {cidr_id} in site {site_id}")
|
||||
logger.info(f"Removed IP {ip_id} from site {site_id}")
|
||||
return jsonify({
|
||||
'message': f'IP override {ip_id} removed successfully'
|
||||
'message': f'IP {ip_id} removed successfully'
|
||||
})
|
||||
|
||||
except ValueError as e:
|
||||
logger.warning(f"Cannot remove IP override {ip_id}: {str(e)}")
|
||||
logger.warning(f"Cannot remove IP {ip_id}: {str(e)}")
|
||||
return jsonify({
|
||||
'error': 'Invalid request',
|
||||
'message': str(e)
|
||||
}), 400
|
||||
except SQLAlchemyError as e:
|
||||
logger.error(f"Database error removing IP override {ip_id}: {str(e)}")
|
||||
logger.error(f"Database error removing IP {ip_id}: {str(e)}")
|
||||
return jsonify({
|
||||
'error': 'Database error',
|
||||
'message': 'Failed to remove IP override'
|
||||
'message': 'Failed to remove IP'
|
||||
}), 500
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error removing IP override {ip_id}: {str(e)}", exc_info=True)
|
||||
logger.error(f"Unexpected error removing IP {ip_id}: {str(e)}", exc_info=True)
|
||||
return jsonify({
|
||||
'error': 'Internal server error',
|
||||
'message': 'An unexpected error occurred'
|
||||
|
||||
Reference in New Issue
Block a user